Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 74566b2dfe | |||
| 93d418fcce | |||
| a532391b0a | |||
| 8b5195a426 | |||
| df5d8a860f | |||
| c4529c9bf4 | |||
| c284068afa |
+10
-10
@@ -92,9 +92,9 @@ jobs:
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
python3 -m pip install --break-system-packages -q -r requirements-base.txt
|
||||
python3 -m pip install --break-system-packages -q -r requirements.txt
|
||||
python3 -m pip install --break-system-packages -q -r requirements-dev.txt
|
||||
python3 -m pip install -q -r requirements-base.txt
|
||||
python3 -m pip install -q -r requirements.txt
|
||||
python3 -m pip install -q -r requirements-dev.txt
|
||||
python3 -m black --version
|
||||
python3 -m isort --version-number
|
||||
python3 -m flake8 --version
|
||||
@@ -221,9 +221,9 @@ jobs:
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
python3 -m pip install --break-system-packages -q -r requirements-base.txt
|
||||
python3 -m pip install --break-system-packages -q -r requirements.txt
|
||||
python3 -m pip install --break-system-packages -q -r requirements-dev.txt
|
||||
python3 -m pip install -q -r requirements-base.txt
|
||||
python3 -m pip install -q -r requirements.txt
|
||||
python3 -m pip install -q -r requirements-dev.txt
|
||||
pytest --version
|
||||
|
||||
- name: Run unit tests with coverage
|
||||
@@ -320,9 +320,9 @@ jobs:
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
python3 -m pip install --break-system-packages -q -r requirements-base.txt
|
||||
python3 -m pip install --break-system-packages -q -r requirements.txt
|
||||
python3 -m pip install --break-system-packages -q -r requirements-dev.txt
|
||||
python3 -m pip install -q -r requirements-base.txt
|
||||
python3 -m pip install -q -r requirements.txt
|
||||
python3 -m pip install -q -r requirements-dev.txt
|
||||
pytest --version
|
||||
|
||||
- name: Start Redis
|
||||
@@ -394,7 +394,7 @@ jobs:
|
||||
shell: sh
|
||||
run: |
|
||||
set -eu
|
||||
pip install --break-system-packages -q pytest-rerunfailures
|
||||
pip install -q pytest-rerunfailures
|
||||
PYTHONPATH="$PWD/apps/api:$PWD" python3 -m coverage run --append \
|
||||
--source=apps/api/app,packages \
|
||||
--omit="*/migrations/*,*/tests/*,*/test_*.py,*/site-packages/*" \
|
||||
|
||||
@@ -13,11 +13,7 @@ import "./accounts.css";
|
||||
|
||||
/* ── 类型定义 ───────────────────────────────────────────── */
|
||||
|
||||
export type PlatformId =
|
||||
| "douyin"
|
||||
| "kuaishou"
|
||||
| "xiaohongshu"
|
||||
| "wechat";
|
||||
export type PlatformId = "douyin" | "kuaishou" | "xiaohongshu" | "wechat";
|
||||
|
||||
export interface Platform {
|
||||
id: PlatformId;
|
||||
|
||||
@@ -39,7 +39,11 @@ const Dashboard: React.FC = () => {
|
||||
<section className="xx-dashboard-section">
|
||||
<div className="xx-dashboard-section-header">
|
||||
<h3>最近任务</h3>
|
||||
<Button buttonType="ghost" buttonSize="sm" onClick={() => navigate("/app/history")}>
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
buttonSize="sm"
|
||||
onClick={() => navigate("/app/history")}
|
||||
>
|
||||
查看全部
|
||||
</Button>
|
||||
</div>
|
||||
@@ -51,7 +55,10 @@ const Dashboard: React.FC = () => {
|
||||
</section>
|
||||
|
||||
{/* 使用统计 */}
|
||||
<section className="xx-dashboard-section" style={{ marginTop: "var(--space-md)" }}>
|
||||
<section
|
||||
className="xx-dashboard-section"
|
||||
style={{ marginTop: "var(--space-md)" }}
|
||||
>
|
||||
<div className="xx-dashboard-section-header">
|
||||
<h3>使用统计</h3>
|
||||
</div>
|
||||
@@ -66,13 +73,18 @@ const Dashboard: React.FC = () => {
|
||||
</section>
|
||||
|
||||
{/* 公告 */}
|
||||
<section className="xx-dashboard-section" style={{ marginTop: "var(--space-md)" }}>
|
||||
<section
|
||||
className="xx-dashboard-section"
|
||||
style={{ marginTop: "var(--space-md)" }}
|
||||
>
|
||||
<div className="xx-dashboard-section-header">
|
||||
<h3>公告</h3>
|
||||
</div>
|
||||
<div className="xx-announcement-list">
|
||||
<div className="xx-announcement-item">
|
||||
<span className="xx-announcement-tag xx-announcement-tag--notice">官方</span>
|
||||
<span className="xx-announcement-tag xx-announcement-tag--notice">
|
||||
官方
|
||||
</span>
|
||||
<div className="xx-announcement-content">
|
||||
<h4>欢迎使用小应 SaaS 平台</h4>
|
||||
<time>当前为演示版本,部分功能正在开发中。</time>
|
||||
|
||||
@@ -416,7 +416,6 @@ const TitleLibrary: React.FC = () => {
|
||||
[deleteMutation],
|
||||
);
|
||||
|
||||
|
||||
/* 新建标题 */
|
||||
const handleCreateTitle = () => {
|
||||
if (!newTitleContent.trim()) {
|
||||
@@ -503,7 +502,6 @@ const TitleLibrary: React.FC = () => {
|
||||
</h4>
|
||||
<span>{cat.count} 条</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -614,8 +612,6 @@ const TitleLibrary: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* ─── 新建标题弹窗 ─── */}
|
||||
<AntModal
|
||||
title="新建标题"
|
||||
|
||||
@@ -238,10 +238,15 @@ class UnifiedRenderService:
|
||||
else:
|
||||
# 回退到带滤镜的直通渲染
|
||||
pass_through_has_audio = self._render_pass_through(
|
||||
layers, output_path, ass_path=ass_path, video_duration=video_duration
|
||||
layers,
|
||||
output_path,
|
||||
ass_path=ass_path,
|
||||
video_duration=video_duration,
|
||||
)
|
||||
else:
|
||||
filter_complex, input_args = self._build_filter_complex(layers, ass_path=ass_path)
|
||||
filter_complex, input_args = self._build_filter_complex(
|
||||
layers, ass_path=ass_path
|
||||
)
|
||||
self._execute_ffmpeg(filter_complex, input_args, video_only_path)
|
||||
|
||||
t_video_end = time.time()
|
||||
@@ -326,7 +331,9 @@ class UnifiedRenderService:
|
||||
if not main_layer or not main_layer.clips:
|
||||
return 0.0
|
||||
|
||||
total = sum(UnifiedRenderService._clip_effective_duration(c) for c in main_layer.clips)
|
||||
total = sum(
|
||||
UnifiedRenderService._clip_effective_duration(c) for c in main_layer.clips
|
||||
)
|
||||
|
||||
# 减去转场重叠时间(粗略估算)
|
||||
n_clips = len(main_layer.clips)
|
||||
@@ -434,7 +441,10 @@ class UnifiedRenderService:
|
||||
return False, f"像素格式不是yuv420p: {info.get('pix_fmt', 'unknown')}"
|
||||
|
||||
# 分辨率必须一致
|
||||
if info.get("width", 0) != self.output_width or info.get("height", 0) != self.output_height:
|
||||
if (
|
||||
info.get("width", 0) != self.output_width
|
||||
or info.get("height", 0) != self.output_height
|
||||
):
|
||||
return False, (
|
||||
f"分辨率不匹配: "
|
||||
f"{info.get('width', 0)}x{info.get('height', 0)} "
|
||||
@@ -484,7 +494,9 @@ class UnifiedRenderService:
|
||||
role = layers[0].role
|
||||
|
||||
# 判断是否满足 copy 条件
|
||||
can_copy, reason = self._can_use_stream_copy(clip, ass_path=ass_path, video_duration=video_duration)
|
||||
can_copy, reason = self._can_use_stream_copy(
|
||||
clip, ass_path=ass_path, video_duration=video_duration
|
||||
)
|
||||
if not can_copy:
|
||||
logger.info(
|
||||
"[unified-render] stream_copy 跳过: plan_id=%s reason=%s",
|
||||
@@ -512,7 +524,9 @@ class UnifiedRenderService:
|
||||
|
||||
# 计算最终时长
|
||||
final_duration = effective_duration
|
||||
if video_duration > 0 and (final_duration <= 0 or final_duration > video_duration):
|
||||
if video_duration > 0 and (
|
||||
final_duration <= 0 or final_duration > video_duration
|
||||
):
|
||||
final_duration = video_duration
|
||||
if final_duration > 0:
|
||||
command.extend(["-t", f"{final_duration:.3f}"])
|
||||
@@ -549,7 +563,9 @@ class UnifiedRenderService:
|
||||
)
|
||||
return True
|
||||
else:
|
||||
logger.warning("[unified-render] stream_copy 输出为空: plan_id=%s", self.plan.id)
|
||||
logger.warning(
|
||||
"[unified-render] stream_copy 输出为空: plan_id=%s", self.plan.id
|
||||
)
|
||||
return False
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
|
||||
logger.warning(
|
||||
@@ -612,7 +628,10 @@ class UnifiedRenderService:
|
||||
filters.append(f"scale={pip_w}:{pip_h}")
|
||||
else:
|
||||
# main / broll / background: 铺满裁剪
|
||||
filters.append(f"scale={self.output_width}:{self.output_height}" ":force_original_aspect_ratio=increase")
|
||||
filters.append(
|
||||
f"scale={self.output_width}:{self.output_height}"
|
||||
":force_original_aspect_ratio=increase"
|
||||
)
|
||||
filters.append(f"crop={self.output_width}:{self.output_height}")
|
||||
|
||||
filters.append("setpts=PTS-STARTPTS")
|
||||
@@ -628,7 +647,9 @@ class UnifiedRenderService:
|
||||
|
||||
# 最终输出时长:取 clip 有效时长和 video_duration 的较小值
|
||||
final_duration = effective_duration
|
||||
if video_duration > 0 and (final_duration <= 0 or final_duration > video_duration):
|
||||
if video_duration > 0 and (
|
||||
final_duration <= 0 or final_duration > video_duration
|
||||
):
|
||||
final_duration = video_duration
|
||||
|
||||
command = [
|
||||
@@ -727,7 +748,9 @@ class UnifiedRenderService:
|
||||
resolved.sort(key=lambda c: c.order)
|
||||
return resolved
|
||||
|
||||
def _group_clips_into_layers(self, resolved_clips: list[ResolvedClip]) -> list[RenderLayer]:
|
||||
def _group_clips_into_layers(
|
||||
self, resolved_clips: list[ResolvedClip]
|
||||
) -> list[RenderLayer]:
|
||||
"""将 ResolvedClips 分组为 RenderLayers。
|
||||
|
||||
分组规则见 _resolve_layer_role 函数文档。
|
||||
@@ -817,14 +840,16 @@ class UnifiedRenderService:
|
||||
filters.append(f"scale={pip_w}:{pip_h}")
|
||||
elif role == "background":
|
||||
filters.append(
|
||||
f"scale={self.output_width}:{self.output_height}" ":force_original_aspect_ratio=increase"
|
||||
f"scale={self.output_width}:{self.output_height}"
|
||||
":force_original_aspect_ratio=increase"
|
||||
)
|
||||
filters.append(f"crop={self.output_width}:{self.output_height}")
|
||||
else:
|
||||
# main / broll: 铺满裁剪(scale to cover + center crop)
|
||||
# 对齐链路A编辑器合成行为,与主流短视频平台一致
|
||||
filters.append(
|
||||
f"scale={self.output_width}:{self.output_height}" ":force_original_aspect_ratio=increase"
|
||||
f"scale={self.output_width}:{self.output_height}"
|
||||
":force_original_aspect_ratio=increase"
|
||||
)
|
||||
filters.append(f"crop={self.output_width}:{self.output_height}")
|
||||
|
||||
@@ -841,8 +866,13 @@ class UnifiedRenderService:
|
||||
layer_clip_indices = [all_clips.index(c) for c in layer.clips]
|
||||
layer_labels = [preprocessed_labels[i] for i in layer_clip_indices]
|
||||
# 使用 trim 后的有效时长,与 Step 1 的 trim=duration 保持一致
|
||||
layer_durations = [UnifiedRenderService._clip_effective_duration(all_clips[i]) for i in layer_clip_indices]
|
||||
layer_transitions = [all_clips[i].transition_effect for i in layer_clip_indices]
|
||||
layer_durations = [
|
||||
UnifiedRenderService._clip_effective_duration(all_clips[i])
|
||||
for i in layer_clip_indices
|
||||
]
|
||||
layer_transitions = [
|
||||
all_clips[i].transition_effect for i in layer_clip_indices
|
||||
]
|
||||
|
||||
if len(layer_labels) == 1:
|
||||
# 单 clip 层,直接使用预处理标签
|
||||
@@ -873,7 +903,8 @@ class UnifiedRenderService:
|
||||
base_label = layer_output_labels[role]
|
||||
combined_label = f"combined_{role}"
|
||||
filter_parts.append(
|
||||
f"[{final_video_label}][{base_label}]" f"overlay=(W-w)/2:(H-h)/2[{combined_label}]"
|
||||
f"[{final_video_label}][{base_label}]"
|
||||
f"overlay=(W-w)/2:(H-h)/2[{combined_label}]"
|
||||
)
|
||||
final_video_label = combined_label
|
||||
else:
|
||||
@@ -898,13 +929,18 @@ class UnifiedRenderService:
|
||||
20,
|
||||
)
|
||||
combined_label = f"combined_{layer.role}"
|
||||
filter_parts.append(f"[{final_video_label}][{overlay_label}]" f"overlay={x}:{y}[{combined_label}]")
|
||||
filter_parts.append(
|
||||
f"[{final_video_label}][{overlay_label}]"
|
||||
f"overlay={x}:{y}[{combined_label}]"
|
||||
)
|
||||
final_video_label = combined_label
|
||||
|
||||
# 叠加字幕(如有)+ 最终像素格式
|
||||
if ass_path is not None:
|
||||
ass_filter_path = str(ass_path).replace("\\", "/").replace(":", "\\:")
|
||||
filter_parts.append(f"[{final_video_label}]subtitles='{ass_filter_path}',format=yuv420p[final_video]")
|
||||
filter_parts.append(
|
||||
f"[{final_video_label}]subtitles='{ass_filter_path}',format=yuv420p[final_video]"
|
||||
)
|
||||
else:
|
||||
filter_parts.append(f"[{final_video_label}]format=yuv420p[final_video]")
|
||||
|
||||
@@ -979,6 +1015,9 @@ class UnifiedRenderService:
|
||||
def _clip_effective_duration(clip: ResolvedClip) -> float:
|
||||
"""计算 clip 的有效时长."""
|
||||
if clip.duration > 0:
|
||||
return min(clip.duration, clip.actual_duration) if clip.actual_duration > 0 else clip.duration
|
||||
return (
|
||||
min(clip.duration, clip.actual_duration)
|
||||
if clip.actual_duration > 0
|
||||
else clip.duration
|
||||
)
|
||||
return clip.actual_duration if clip.actual_duration > 0 else 0.0
|
||||
|
||||
|
||||
@@ -67,7 +67,11 @@ def _update_task_status(task_id: str, status_action: str, **kwargs) -> bool:
|
||||
|
||||
action(**kwargs)
|
||||
repo.update(task)
|
||||
logger.info("GenerationTask 状态更新成功: task_id=%s action=%s", task_id, status_action)
|
||||
logger.info(
|
||||
"GenerationTask 状态更新成功: task_id=%s action=%s",
|
||||
task_id,
|
||||
status_action,
|
||||
)
|
||||
return True
|
||||
finally:
|
||||
session.close()
|
||||
@@ -92,7 +96,11 @@ def _flush_logs(task_id: str, gen_task) -> None:
|
||||
try:
|
||||
from packages.adapters.sqlalchemy_impl.models import GenerationTaskModel
|
||||
|
||||
model = session.query(GenerationTaskModel).filter(GenerationTaskModel.id == task_id).first()
|
||||
model = (
|
||||
session.query(GenerationTaskModel)
|
||||
.filter(GenerationTaskModel.id == task_id)
|
||||
.first()
|
||||
)
|
||||
if model:
|
||||
model.logs = gen_task.logs
|
||||
session.commit()
|
||||
@@ -405,7 +413,9 @@ def _download_library_assets(
|
||||
else:
|
||||
# 未指定 asset_ids:按 library 或 project 下载全部 ready 视频
|
||||
if asset_library_id:
|
||||
query = query.filter(AssetModel.asset_library_id == asset_library_id)
|
||||
query = query.filter(
|
||||
AssetModel.asset_library_id == asset_library_id
|
||||
)
|
||||
logger.info(
|
||||
"下载素材库全部视频: asset_library_id=%s",
|
||||
asset_library_id,
|
||||
@@ -420,7 +430,11 @@ def _download_library_assets(
|
||||
assets = query.order_by(AssetModel.created_at).all()
|
||||
|
||||
if not assets:
|
||||
mode_desc = f"素材库 {asset_library_id}" if asset_library_id else f"项目 {project_id}"
|
||||
mode_desc = (
|
||||
f"素材库 {asset_library_id}"
|
||||
if asset_library_id
|
||||
else f"项目 {project_id}"
|
||||
)
|
||||
msg = f"未找到视频素材: {mode_desc}, asset_ids={asset_ids or 'all'}"
|
||||
logger.error(msg)
|
||||
raise RuntimeError(msg)
|
||||
@@ -458,7 +472,10 @@ def _download_library_assets(
|
||||
if not storage_key:
|
||||
failed_assets.append(f"{asset.name}({asset.id})")
|
||||
logger.warning(
|
||||
"[task_id=%s] 素材缺少 file_url, 跳过: asset_id=%s name=%s", task_id, asset.id, asset.name
|
||||
"[task_id=%s] 素材缺少 file_url, 跳过: asset_id=%s name=%s",
|
||||
task_id,
|
||||
asset.id,
|
||||
asset.name,
|
||||
)
|
||||
if gen_task:
|
||||
gen_task.append_log(
|
||||
@@ -472,7 +489,9 @@ def _download_library_assets(
|
||||
duration=0.0,
|
||||
)
|
||||
if strict:
|
||||
raise RuntimeError(f"素材缺少 file_url: asset_id={asset.id}, name={asset.name}")
|
||||
raise RuntimeError(
|
||||
f"素材缺少 file_url: asset_id={asset.id}, name={asset.name}"
|
||||
)
|
||||
continue
|
||||
|
||||
ext = Path(storage_key).suffix or ".mp4"
|
||||
@@ -504,7 +523,12 @@ def _download_library_assets(
|
||||
)
|
||||
else:
|
||||
failed_assets.append(f"{asset.name}({asset.id})")
|
||||
logger.warning("[task_id=%s] Failed to download asset: %s (id=%s)", task_id, asset.name, asset.id)
|
||||
logger.warning(
|
||||
"[task_id=%s] Failed to download asset: %s (id=%s)",
|
||||
task_id,
|
||||
asset.name,
|
||||
asset.id,
|
||||
)
|
||||
if gen_task:
|
||||
gen_task.append_log(
|
||||
"下载素材",
|
||||
@@ -517,7 +541,9 @@ def _download_library_assets(
|
||||
duration=round(asset_elapsed, 2),
|
||||
)
|
||||
if strict:
|
||||
raise RuntimeError(f"素材下载失败: asset_id={asset.id}, name={asset.name}")
|
||||
raise RuntimeError(
|
||||
f"素材下载失败: asset_id={asset.id}, name={asset.name}"
|
||||
)
|
||||
|
||||
# 指定了 asset_ids 但全部下载失败 → 无论 strict 与否都报错
|
||||
if asset_ids and not downloaded:
|
||||
@@ -826,7 +852,9 @@ def _render_video(
|
||||
|
||||
# 选择渲染引擎
|
||||
engine = _resolve_render_engine(user_id) if user_id else ENGINE_UNIFIED
|
||||
logger.info("[task_id=%s] [渲染] 引擎选择: %s (user_id=%s)", task_id, engine, user_id)
|
||||
logger.info(
|
||||
"[task_id=%s] [渲染] 引擎选择: %s (user_id=%s)", task_id, engine, user_id
|
||||
)
|
||||
|
||||
render_start = time.monotonic()
|
||||
render_output_path = temp_path / f"rendered-{task_id}.mp4"
|
||||
@@ -870,7 +898,9 @@ def _render_video(
|
||||
_mux_audio_track(render_output_path, voice_path, final_path)
|
||||
output_path = final_path
|
||||
except Exception as mux_err:
|
||||
logger.warning("[task_id=%s] [混音] 音频混合失败,使用无音频版本: %s", task_id, mux_err)
|
||||
logger.warning(
|
||||
"[task_id=%s] [混音] 音频混合失败,使用无音频版本: %s", task_id, mux_err
|
||||
)
|
||||
output_path = render_output_path
|
||||
else:
|
||||
output_path = render_output_path
|
||||
@@ -899,7 +929,9 @@ def _upload_and_record(
|
||||
file_url = upload_to_oss(output_path, storage_key)
|
||||
upload_elapsed = time.monotonic() - upload_start
|
||||
if not file_url:
|
||||
raise RuntimeError(f"OSS 上传失败: task_id={task_id}, storage_key={storage_key}")
|
||||
raise RuntimeError(
|
||||
f"OSS 上传失败: task_id={task_id}, storage_key={storage_key}"
|
||||
)
|
||||
|
||||
# 校验 URL 可达性(P0-2: 私有 bucket 用预签名 + object_exists 降级)
|
||||
verify_url = get_signed_download_url(file_url, expires_seconds=300) or file_url
|
||||
@@ -913,7 +945,10 @@ def _upload_and_record(
|
||||
f"OSS 上传后 URL 不可访问且 object_exists 失败: file_url={file_url}, "
|
||||
f"storage_key={storage_key}"
|
||||
)
|
||||
logger.info("URL 校验失败但 object_exists 确认文件存在,视为上传成功: storage_key=%s", key)
|
||||
logger.info(
|
||||
"URL 校验失败但 object_exists 确认文件存在,视为上传成功: storage_key=%s",
|
||||
key,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"[task_id=%s] [OSS上传] 成功: 耗时=%.1fs, file_url=%s",
|
||||
@@ -1009,7 +1044,9 @@ def generate_video(self, task_id: str) -> dict:
|
||||
_update_task_status(task_id, "mark_processing")
|
||||
|
||||
try:
|
||||
editing_mode = EditingMode(mode) if (mode := task_info["mode"]) else EditingMode.ONE_TAKE
|
||||
editing_mode = (
|
||||
EditingMode(mode) if (mode := task_info["mode"]) else EditingMode.ONE_TAKE
|
||||
)
|
||||
except ValueError:
|
||||
editing_mode = EditingMode.ONE_TAKE
|
||||
|
||||
|
||||
@@ -48,10 +48,19 @@ HIGH_RISK_PATTERNS = [
|
||||
|
||||
# 中风险模式:可能导致数据丢失或兼容性问题
|
||||
MEDIUM_RISK_PATTERNS = [
|
||||
(r"op\.alter_column\([^)]*nullable\s*=\s*False", "新增 NOT NULL 约束 - 旧数据可能为空导致迁移失败"),
|
||||
(
|
||||
r"op\.alter_column\([^)]*nullable\s*=\s*False",
|
||||
"新增 NOT NULL 约束 - 旧数据可能为空导致迁移失败",
|
||||
),
|
||||
(r"op\.alter_column\([^)]*type_\s*=", "列类型变更 - 可能导致数据截断或转换失败"),
|
||||
(r"\bop\.rename_table\(", "op.rename_table() - 重命名表,可能导致依赖该表的代码报错"),
|
||||
(r"\bop\.rename_column\(", "op.rename_column() - 重命名列,可能导致依赖该列的代码报错"),
|
||||
(
|
||||
r"\bop\.rename_table\(",
|
||||
"op.rename_table() - 重命名表,可能导致依赖该表的代码报错",
|
||||
),
|
||||
(
|
||||
r"\bop\.rename_column\(",
|
||||
"op.rename_column() - 重命名列,可能导致依赖该列的代码报错",
|
||||
),
|
||||
(r"\bop\.drop_index\(", "op.drop_index() - 删除索引,可能影响查询性能"),
|
||||
(r"\bop\.drop_constraint\(", "op.drop_constraint() - 删除约束,可能影响数据完整性"),
|
||||
]
|
||||
@@ -95,13 +104,24 @@ def get_new_migrations_via_diff(diff_target: str) -> List[Path]:
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "diff", "--name-only", "--diff-filter=A", diff_target, "HEAD", "--", "alembic/versions/"],
|
||||
[
|
||||
"git",
|
||||
"diff",
|
||||
"--name-only",
|
||||
"--diff-filter=A",
|
||||
diff_target,
|
||||
"HEAD",
|
||||
"--",
|
||||
"alembic/versions/",
|
||||
],
|
||||
cwd=str(REPO_ROOT),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
files = [line.strip() for line in result.stdout.strip().split("\n") if line.strip()]
|
||||
files = [
|
||||
line.strip() for line in result.stdout.strip().split("\n") if line.strip()
|
||||
]
|
||||
return [REPO_ROOT / f for f in files]
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"⚠️ git diff 失败({diff_target}):{e.stderr.strip()}")
|
||||
@@ -109,7 +129,9 @@ def get_new_migrations_via_diff(diff_target: str) -> List[Path]:
|
||||
return sorted(ALEMBIC_VERSIONS_DIR.glob("*.py"))
|
||||
|
||||
|
||||
def find_new_migrations(since_revision: str | None = None, diff_against: str | None = None) -> List[Path]:
|
||||
def find_new_migrations(
|
||||
since_revision: str | None = None, diff_against: str | None = None
|
||||
) -> List[Path]:
|
||||
"""
|
||||
找出需要检查的迁移文件。
|
||||
优先级:diff_against > since_revision > 全部
|
||||
@@ -161,7 +183,9 @@ def analyze_migration(file_path: Path) -> Tuple[List[str], List[str], List[str]]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser = argparse.ArgumentParser(
|
||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
parser.add_argument(
|
||||
"--since",
|
||||
default=os.getenv("MIGRATION_SINCE_REVISION"),
|
||||
@@ -224,7 +248,9 @@ def main() -> int:
|
||||
print()
|
||||
|
||||
print("=" * 60)
|
||||
print(f"检查结果:{len(all_safe)} 项安全 / {len(all_medium)} 项中风险 / {len(all_high)} 项高风险")
|
||||
print(
|
||||
f"检查结果:{len(all_safe)} 项安全 / {len(all_medium)} 项中风险 / {len(all_high)} 项高风险"
|
||||
)
|
||||
print()
|
||||
|
||||
if all_high:
|
||||
@@ -247,4 +273,3 @@ def main() -> int:
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user