Compare commits

...

7 Commits

Author SHA1 Message Date
ops-bot 74566b2dfe fix(format): black格式化 check_migration_safety.py
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 20s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 17s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 52s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 3m33s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
2026-07-13 22:52:50 +08:00
ops-bot 93d418fcce fix(format): black格式化 generation.py
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
2026-07-13 22:52:49 +08:00
ops-bot a532391b0a fix(format): black格式化 unified_render_service.py
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
2026-07-13 22:52:47 +08:00
ops-bot 8b5195a426 fix(frontend): prettier格式化 TitleLibrary.tsx
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 18s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 18s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 49s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 2m27s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
2026-07-13 22:48:27 +08:00
ops-bot df5d8a860f fix(frontend): prettier格式化 Dashboard.tsx 2026-07-13 22:48:26 +08:00
ops-bot c4529c9bf4 fix(frontend): prettier格式化 Accounts.tsx 2026-07-13 22:48:26 +08:00
ops-bot c284068afa fix(ci): 移除--break-system-packages,兼容旧Runner pip 22.0.2 2026-07-13 22:48:03 +08:00
7 changed files with 169 additions and 64 deletions
+10 -10
View File
@@ -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/*" \
+1 -5
View File
@@ -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;
+16 -4
View File
@@ -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
+50 -13
View File
@@ -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
+34 -9
View File
@@ -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())