Compare commits

...

7 Commits

Author SHA1 Message Date
Ops Bot f901705050 docs: 新增 runner-planning.md
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 17s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 47s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 37s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 3m5s
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 16:27:17 +08:00
Ops Bot 8d826d73c0 docs: 新增 ci-failure-diagnosis-20260713.md 2026-07-13 16:27:16 +08:00
xiaoxia f0dee5bbd3 fix(backend): Phase 2 后端技术债务清理
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 15s
CI/CD Pipeline / Unit Tests (push) Failing after 45s
CI/CD Pipeline / Integration Tests (push) Failing after 2m2s
CI/CD Pipeline / Frontend Lint (push) Successful in 3m53s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (push) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
Merge PR #261: fix(backend): Phase 2 后端技术债务清理 — CI修复 + OSS拼写 + 废弃API删除 into develop
2026-07-13 15:54:13 +08:00
xiaoxia 50719db7c8 feat(frontend): Phase 2 前端技术债务清理
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 27s
CI/CD Pipeline / Unit Tests (push) Failing after 30s
CI/CD Pipeline / Integration Tests (push) Failing after 39s
CI/CD Pipeline / Frontend Lint (push) Successful in 4m39s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (push) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
Merge PR #258: feat(frontend): Phase 2 前端技术债务清理(Mock 数据 + 样式迁移 + 组件合并) into develop
2026-07-13 15:53:58 +08:00
CI Test eb50442296 fix: 修复 PR #258 P1 CSS 变量问题
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 23s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 18s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 3m9s
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
CI/CD Pipeline / Integration Tests (pull_request) Failing after 28s
- 删除 .xx-card:hover(global.css V21 设计系统已定义)
- var(--xx-border) → var(--border-color)(原变量未定义)
- var(--xx-text) → var(--text-primary)(原变量未定义)
- .ant-card-head padding 修正为 20px 24px(与原 Admin.css 一致)
- .ant-card-head-title color 对齐原 Admin.css 的 var(--slate) 语义
2026-07-13 15:35:10 +08:00
xiaoxia 73375c6639 PR #252: redis_url配置修复
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 34s
CI/CD Pipeline / Unit Tests (push) Failing after 24s
CI/CD Pipeline / Frontend Lint (push) Successful in 2m40s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (push) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Failing after 40s
fix: 修复worker配置中redis_url字段未正确映射REDIS_URL环境变量

Signed-off-by: CI Bot <bot@xiaoxiajianji.com>
2026-07-13 15:17:32 +08:00
CI Test 5bde975ea6 feat(frontend): Phase 2 前端技术债务清理
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 31s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 29s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 3m45s
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
CI/CD Pipeline / Integration Tests (pull_request) Failing after 32s
1. Mock 数据清理(4 项):
   - Dashboard.tsx: kpiData/quickEntries/recentTasks 标记 TODO
   - TitleLibrary.tsx: MOCK_CATEGORIES 精简为占位+TODO
   - Accounts.tsx: 移除 2 处误导性 mock 注释(实际已用真实 API)

2. Admin.css 样式迁移:
   - 4 条 .xx-card antd 覆盖样式迁移到 ui.css
   - AdminComingSoon 页面样式不受影响

3. CloneModal 组件合并:
   - VoiceClone.tsx 改用共享 CloneModal 组件
   - 删除重复的 CloneVoiceModal.tsx + clone-voice-modal.css
   - 清理空目录 components/modals/
2026-07-13 15:15:47 +08:00
10 changed files with 270 additions and 625 deletions
@@ -1,290 +0,0 @@
/**
* CloneVoiceModal — 音色克隆弹窗
*
* 三步骤状态:input → uploading → success
* 支持上传音频文件或直接录制(mock,无真实录音)
*
* V21 Design System — 零 antd 直接导入
*/
import React, { useState, useCallback, useRef } from "react";
import { Modal, Button } from "@/components/ui";
import { createVoiceClone, toVoiceClone } from "@/api/voiceClone";
import type { VoiceClone } from "@/api/voiceClone";
import { uploadAsset } from "@/api/assets";
import "./clone-voice-modal.css";
/* ── 类型定义 ───────────────────────────────────────────── */
type ModalStep = "input" | "uploading" | "success";
export interface CloneVoiceModalProps {
/** 弹窗是否可见 */
open: boolean;
/** 关闭弹窗回调 */
onClose: () => void;
/** 克隆成功回调(返回新创建的音色) */
onSuccess?: (voice: VoiceClone) => void;
}
/* ── 默认音色名称计数器 ─────────────────────────────────── */
let cloneCounter = 1;
const getNextDefaultName = (): string => {
const name = `我的声音 ${cloneCounter}`;
cloneCounter += 1;
return name;
};
/* ── 组件 ───────────────────────────────────────────────── */
const CloneVoiceModal: React.FC<CloneVoiceModalProps> = ({
open,
onClose,
onSuccess,
}) => {
const [step, setStep] = useState<ModalStep>("input");
const [voiceName, setVoiceName] = useState("");
const [isRecording, setIsRecording] = useState(false);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [dragActive, setDragActive] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
/** 重置弹窗状态 */
const resetState = useCallback(() => {
setStep("input");
setVoiceName("");
setSelectedFile(null);
setIsRecording(false);
setDragActive(false);
}, []);
/** 关闭弹窗 */
const handleClose = useCallback(() => {
resetState();
onClose();
}, [resetState, onClose]);
/** 上传区域点击 */
const handleUploadClick = () => {
fileInputRef.current?.click();
};
/** 文件选择 */
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setSelectedFile(file);
// 清除之前的录制状态
setIsRecording(false);
}
// 清空 input 以允许重复选择同一文件
e.target.value = "";
};
/** 拖拽事件 */
const handleDrag = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") {
setDragActive(true);
} else if (e.type === "dragleave") {
setDragActive(false);
}
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
const file = e.dataTransfer.files?.[0];
if (file) {
const ext = file.name.split(".").pop()?.toLowerCase();
if (ext === "mp3" || ext === "wav") {
setSelectedFile(file);
setIsRecording(false);
}
}
};
/** 录制按钮(mock */
const handleRecord = () => {
setIsRecording((prev) => !prev);
if (!isRecording) {
// 开始录制 — 清除已选文件
setSelectedFile(null);
}
};
/** 开始克隆 */
const handleStartClone = async () => {
const name = voiceName.trim() || getNextDefaultName();
setStep("uploading");
try {
// 先上传音频文件获取真实 URL
let audioUrl: string;
if (selectedFile) {
const formData = new FormData();
formData.append("file", selectedFile);
formData.append("kind", "voice");
const uploadResult = await uploadAsset(formData);
audioUrl = uploadResult.url;
} else {
// 录制功能暂未实现,提示用户上传
setStep("input");
return;
}
// 提交克隆请求
const result = await createVoiceClone({
name,
audio_url: audioUrl,
});
setStep("success");
// 2秒后自动关闭
setTimeout(() => {
onSuccess?.(toVoiceClone(result));
handleClose();
}, 2000);
} catch {
setStep("input");
}
};
/** 弹窗打开时初始化默认名称 */
const handleAfterOpenChange = (visible: boolean) => {
if (visible) {
setVoiceName(getNextDefaultName());
}
};
const canStart = selectedFile || isRecording;
return (
<Modal
open={open}
onCancel={handleClose}
title="🎤 克隆新音色"
width={520}
footer={null}
destroyOnClose
afterOpenChange={handleAfterOpenChange}
>
{/* ── 输入步骤 ──────────────────────────────────── */}
{step === "input" && (
<div className="cvm-body">
{/* 音色名称 */}
<div className="cvm-field">
<label className="cvm-label"></label>
<input
type="text"
className="cvm-input"
value={voiceName}
onChange={(e) => setVoiceName(e.target.value)}
placeholder="输入音色名称"
/>
</div>
{/* 上传区域 */}
<div className="cvm-field">
<label className="cvm-label"></label>
<div
className={`cvm-upload-zone${dragActive ? " cvm-upload-zone--active" : ""}`}
onClick={handleUploadClick}
onDragEnter={handleDrag}
onDragOver={handleDrag}
onDragLeave={handleDrag}
onDrop={handleDrop}
>
<div className="cvm-upload-icon">🎵</div>
<p className="cvm-upload-title">
{selectedFile ? selectedFile.name : "拖拽音频文件到此处"}
</p>
<p className="cvm-upload-hint"> MP3WAV </p>
<input
ref={fileInputRef}
type="file"
accept=".mp3,.wav,audio/mpeg,audio/wav"
style={{ display: "none" }}
onChange={handleFileChange}
/>
</div>
</div>
{/* 或分隔 */}
<div className="cvm-divider">
<div className="cvm-divider-line" />
<span className="cvm-divider-text"></span>
<div className="cvm-divider-line" />
</div>
{/* 录制区域 */}
<div className="cvm-field">
<label className="cvm-label"></label>
<div className="cvm-record-area">
<p className="cvm-record-hint">
{isRecording
? "录制中…再次点击停止"
: "点击按钮开始录制你的声音"}
</p>
<button
type="button"
className={`cvm-record-btn${isRecording ? " cvm-record-btn--recording" : ""}`}
onClick={handleRecord}
>
🎙
</button>
</div>
</div>
{/* 提示 */}
<div className="cvm-tip">
<span className="cvm-tip-icon">💡</span>
<span>
10~3
</span>
</div>
{/* 底部按钮 */}
<div className="cvm-footer">
<Button buttonType="ghost" onClick={handleClose}>
</Button>
<Button
buttonType="primary"
disabled={!canStart}
onClick={handleStartClone}
>
🎤
</Button>
</div>
</div>
)}
{/* ── 上传中步骤 ────────────────────────────────── */}
{step === "uploading" && (
<div className="cvm-uploading">
<div className="cvm-uploading-spinner" />
<p className="cvm-uploading-text"></p>
<p className="cvm-uploading-sub">AI </p>
</div>
)}
{/* ── 成功步骤 ──────────────────────────────────── */}
{step === "success" && (
<div className="cvm-success">
<div className="cvm-success-icon"></div>
<h3 className="cvm-success-title"></h3>
<p className="cvm-success-desc">
</p>
</div>
)}
</Modal>
);
};
export default CloneVoiceModal;
@@ -1,325 +0,0 @@
/**
* CloneVoiceModal — V21 Design System
*
* 音色克隆弹窗样式
* 三步骤状态:input → uploading → success
*/
/* ── 弹窗内容区 ─────────────────────────────────────────── */
.cvm-body {
display: flex;
flex-direction: column;
gap: 20px;
}
/* ── 表单区 ─────────────────────────────────────────────── */
.cvm-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.cvm-label {
font-size: 13px;
font-weight: 600;
color: var(--text-secondary, #475467);
}
.cvm-input {
width: 100%;
padding: 10px 14px;
border: 1px solid var(--line, #e4e7ec);
border-radius: var(--radius-sm);
background: var(--bg-surface, #fff);
color: var(--text-primary, #101828);
font-size: 14px;
line-height: 1.5;
transition:
border-color 0.2s,
box-shadow 0.2s;
outline: none;
}
.cvm-input:focus {
border-color: var(--primary, #6366f1);
box-shadow: 0 0 0 3px
color-mix(in srgb, var(--primary-color) 12%, transparent);
}
.cvm-input::placeholder {
color: var(--muted, #98a2b3);
}
/* ── 上传区域 ───────────────────────────────────────────── */
.cvm-upload-zone {
border: 2px dashed var(--line, #e4e7ec);
border-radius: var(--radius-md);
padding: 28px 20px;
text-align: center;
background: var(--bg-subtle, #f8fafc);
cursor: pointer;
transition:
border-color 0.2s,
background 0.2s;
}
.cvm-upload-zone:hover {
border-color: var(--primary, #6366f1);
background: color-mix(in srgb, var(--primary-color) 4%, transparent);
}
.cvm-upload-zone.cvm-upload-zone--active {
border-color: var(--primary, #6366f1);
background: color-mix(in srgb, var(--primary-color) 6%, transparent);
}
.cvm-upload-icon {
font-size: 36px;
margin-bottom: 8px;
line-height: 1;
}
.cvm-upload-title {
font-size: 14px;
font-weight: 600;
color: var(--text-primary, #101828);
margin: 0 0 4px;
}
.cvm-upload-hint {
font-size: 13px;
color: var(--muted, #98a2b3);
margin: 0;
}
/* ── 或分隔线 ───────────────────────────────────────────── */
.cvm-divider {
display: flex;
align-items: center;
gap: 16px;
margin: 4px 0;
}
.cvm-divider-line {
flex: 1;
height: 1px;
background: var(--line, #e4e7ec);
}
.cvm-divider-text {
font-size: 13px;
color: var(--muted, #98a2b3);
flex-shrink: 0;
}
/* ── 录制区域 ───────────────────────────────────────────── */
.cvm-record-area {
border: 1px solid var(--line, #e4e7ec);
border-radius: var(--radius-md);
padding: 24px;
text-align: center;
}
.cvm-record-hint {
font-size: 13px;
color: var(--muted, #98a2b3);
margin: 0 0 14px;
}
.cvm-record-btn {
width: 80px;
height: 80px;
border-radius: 50%;
border: none;
cursor: pointer;
font-size: 32px;
line-height: 1;
padding: 0;
background: linear-gradient(
135deg,
var(--error-color, #ef4444),
var(--error-dark, #dc2626)
);
color: var(--text-inverse);
box-shadow: 0 4px 14px
color-mix(in srgb, var(--error-color, #ef4444) 35%, transparent);
transition:
transform 0.15s,
box-shadow 0.15s;
display: inline-flex;
align-items: center;
justify-content: center;
}
.cvm-record-btn:hover {
transform: scale(1.06);
box-shadow: 0 6px 20px
color-mix(in srgb, var(--error-color, #ef4444) 45%, transparent);
}
.cvm-record-btn:active {
transform: scale(0.96);
}
.cvm-record-btn--recording {
animation: cvm-pulse 1.2s ease-in-out infinite;
}
@keyframes cvm-pulse {
0%,
100% {
box-shadow: 0 4px 14px
color-mix(in srgb, var(--error-color, #ef4444) 35%, transparent);
}
50% {
box-shadow: 0 4px 28px
color-mix(in srgb, var(--error-color, #ef4444) 60%, transparent);
}
}
/* ── 提示条 ─────────────────────────────────────────────── */
.cvm-tip {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 12px 16px;
background: var(--warning-soft, #fef3c7);
border-radius: var(--radius-sm);
font-size: 13px;
color: var(--warning-color, #92400e);
line-height: 1.5;
}
.cvm-tip-icon {
flex-shrink: 0;
font-size: 14px;
line-height: 1.5;
}
/* ── 底部按钮 ───────────────────────────────────────────── */
.cvm-footer {
display: flex;
gap: 12px;
margin-top: 4px;
}
.cvm-footer .xx-btn {
flex: 1;
}
/* ── 上传中状态 ─────────────────────────────────────────── */
.cvm-uploading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 20px;
gap: 16px;
}
.cvm-uploading-spinner {
width: 48px;
height: 48px;
border: 3px solid var(--line, #e4e7ec);
border-top-color: var(--primary, #6366f1);
border-radius: 50%;
animation: cvm-spin 0.8s linear infinite;
}
@keyframes cvm-spin {
to {
transform: rotate(360deg);
}
}
.cvm-uploading-text {
font-size: 15px;
font-weight: 500;
color: var(--text-primary, #101828);
margin: 0;
}
.cvm-uploading-sub {
font-size: 13px;
color: var(--muted, #98a2b3);
margin: 0;
}
/* ── 成功状态 ───────────────────────────────────────────── */
.cvm-success {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 20px;
gap: 12px;
}
.cvm-success-icon {
font-size: 56px;
line-height: 1;
}
.cvm-success-title {
font-size: 18px;
font-weight: 700;
color: var(--text-primary, #101828);
margin: 0;
}
.cvm-success-desc {
font-size: 14px;
color: var(--muted, #98a2b3);
margin: 0;
}
/* ── 响应式 ─────────────────────────────────────────────── */
@media (max-width: 768px) {
.cvm-overlay {
padding: var(--space-md);
}
.cvm-modal {
width: 100%;
max-width: 100%;
padding: var(--space-lg);
}
}
@media (max-width: 576px) {
.cvm-upload-zone {
padding: 20px 14px;
}
.cvm-record-btn {
width: 64px;
height: 64px;
font-size: 26px;
}
.cvm-footer {
flex-direction: column;
}
}
@media (max-width: 480px) {
.cvm-record-btn {
width: 60px;
height: 60px;
}
.cvm-tip {
font-size: 12px;
padding: var(--space-sm);
}
}
+20
View File
@@ -532,3 +532,23 @@
padding: 8px 16px !important;
}
}
/* ── xx-card antd 子元素覆盖样式(从 Admin.css 迁移) ── */
/* AdminComingSoon 等页面使用 <Card className="xx-card"> 时需要 */
/* .xx-card 基础样式和 :hover 已在 global.css 中定义(V21 设计系统) */
.xx-card .ant-card-head {
border-bottom: 1px solid var(--border-color);
padding: 20px 24px;
}
.xx-card .ant-card-head-title {
font-weight: 800;
font-size: 17px;
color: var(--text-primary);
}
.xx-card .ant-card-body {
padding: 24px;
}
+2 -2
View File
@@ -153,7 +153,7 @@ const Accounts: React.FC = () => {
},
});
/** 绑定 mutationmock */
/** 绑定 mutation */
const bindMutation = useMutation({
mutationFn: bindAccount,
onSuccess: () => {
@@ -165,7 +165,7 @@ const Accounts: React.FC = () => {
},
});
/** 绑定新账号mock:直接创建) */
/** 绑定新账号 */
const handleBind = (platformId: PlatformId) => {
const platform = PLATFORMS.find((p) => p.id === platformId);
if (!platform) return;
@@ -29,6 +29,7 @@ interface KpiItem {
accent: string;
}
// TODO: kpiData 当前使用硬编码 mock 数据,待后端提供 Dashboard 统计 API 后替换
const kpiData: KpiItem[] = [
{
key: "projects",
@@ -81,6 +82,7 @@ interface QuickEntry {
path: string;
}
// TODO: quickEntries 描述中含硬编码计数(如 486个素材),待后端 API 后动态化
const quickEntries: QuickEntry[] = [
{
id: "titles",
@@ -135,6 +137,7 @@ const statusLabel: Record<TaskStatus, string> = {
failed: "失败",
};
// TODO: recentTasks 当前使用硬编码 mock 数据,待后端提供最近任务 API 后替换
const recentTasks: RecentTask[] = [
{
id: "t-1",
+2 -6
View File
@@ -55,13 +55,9 @@ interface TitleData {
/* ============================================================
* Mock 数据
* ============================================================ */
// TODO: 分类数据当前为前端硬编码 mock,待后端提供标题分类 API 后替换
const MOCK_CATEGORIES: CategoryItem[] = [
{ id: "cat-all", name: "全部标题", count: 15 },
{ id: "cat-1", name: "美食探店", count: 4 },
{ id: "cat-2", name: "科技数码", count: 3 },
{ id: "cat-3", name: "生活日常", count: 4 },
{ id: "cat-4", name: "美妆穿搭", count: 2 },
{ id: "cat-5", name: "教育学习", count: 2 },
{ id: "cat-all", name: "全部标题", count: 0 },
];
/** 后端 TitleItem → 前端 TitleData 映射 */
@@ -18,7 +18,7 @@ import {
CloseCircleOutlined,
} from "@ant-design/icons";
import PageHead from "@/components/layout/PageHead";
import CloneVoiceModal from "@/components/modals/CloneVoiceModal";
import CloneModal from "@/components/voice/CloneModal";
import {
getVoiceClones,
deleteVoiceClone,
@@ -356,7 +356,7 @@ const VoiceClone: React.FC = () => {
)}
{/* 克隆音色弹窗 */}
<CloneVoiceModal
<CloneModal
open={cloneModalOpen}
onClose={() => setCloneModalOpen(false)}
onSuccess={() => {
+1
View File
@@ -17,6 +17,7 @@ class WorkerSettings(BaseSettings):
database_pool_recycle: int = 3600
environment: str = "development"
auto_create_schema: bool = False
redis_url: str = "redis://redis:6379/0"
# 渲染引擎选择:legacy=旧VideoComposeServiceunified=新UnifiedRenderService
render_engine: str = "legacy"
+104
View File
@@ -0,0 +1,104 @@
# CI 大量失败根因排查报告
**排查时间:** 2026-07-13
**排查人:** 构建服务器运维Agent
**范围:** 最近15次 CI runPR #258~#265 + develop 分支多次 push
## 一、整体概况
最近 20 次 CI run 中 16 次失败,失败率 **80%**。失败集中在 3 个 Job
| Job | 失败率 | 根因类型 |
|-----|--------|----------|
| Validate Code Quality | 100% | black 代码格式检查失败 |
| Unit Tests | 100% | 测试断言未同步国际化改动 |
| Integration Tests | 100% | 密码重置接口变更未同步测试 |
| Frontend Lint | 20% | 各 PR 代码质量问题 |
**结论:3 个全局性失败点导致所有 PR CI 全红,不是代码本身问题,是基础设施/测试用例滞后。**
---
## 二、详细根因分析
### 1. Validate — black 格式检查失败
**现象:**
```
would reformat scripts/check_migration_safety.py
1 file would be reformatted, 369 files would be left unchanged.
Oh no! 💥 💔 💥
```
**根因:**
`scripts/check_migration_safety.py` 文件不符合 black 格式化规范。该文件是最近新增的迁移安全检查脚本,提交前未本地跑 black 格式化。
**影响范围:** 所有 PR 及 develop 分支,全量失败。
**修复方案:**
```bash
black scripts/check_migration_safety.py
```
---
### 2. Unit Tests — 1 个用例失败
**现象:**
```
FAILED tests/unit/test_asset_library_delete.py::TestDeleteAssetLibrary::test_delete_library_access_denied
AssertionError: assert 'Access denied' in '无权访问该项目'
```
**统计:** 1442 passed, 1 failed
**根因:**
项目之前做了国际化(i18n)改造,错误信息从英文改成了中文,但对应的单元测试断言仍然检查英文 "Access denied",导致断言失败。
**影响范围:** 所有 PR 及 develop 分支,全量失败。
**修复方案:**
修改 `tests/unit/test_asset_library_delete.py` 中的断言,将 `'Access denied'` 改为 `'无权访问该项目'`,或改为断言 HTTP 状态码(403)而不是错误消息文本。
---
### 3. Integration Tests — 1 个用例失败
**现象:**
```
FAILED tests/integration/test_auth.py::TestPasswordReset::test_request_password_reset_success
assert 404 in (200, 202)
```
**统计:** 45 passed, 1 failed, 13 deselected, 2 rerun
**根因:**
密码重置请求接口(`POST /auth/password-reset/request` 或类似路由)返回 404,说明该接口已被移除、路由变更,或对应的功能模块暂时被注释/下线。
**影响范围:** 所有 PR 及 develop 分支,全量失败。
**修复方案:**
- 如果接口确实下线了:删除或 skip 这个测试用例
- 如果是路由改了:更新测试中的 API 路径
- 如果是功能待开发:标记为 `@pytest.mark.skip` 并加上 TODO
---
## 三、修复优先级
| 优先级 | 问题 | 修复难度 | 预估时间 |
|--------|------|----------|----------|
| P0 | black 格式检查失败 | ⭐ | 5分钟 |
| P0 | 单元测试国际化断言失败 | ⭐ | 10分钟 |
| P1 | 集成测试密码重置接口404 | ⭐⭐ | 30分钟(需确认接口状态) |
**建议:** 先修前两个 P0(能让 2/3 的 job 变绿),再处理密码重置那个。
---
## 四、Runner 执行情况观察
- 当前 9 个 Runner 全部在线(构建服务器 4 个 + 新服务器 5 个)
- 失败的 Job 都是在构建服务器的 Runner 上执行的(xiaoxia-ci-runner-2/3 等)
- 新服务器 5 个 Runner 目前全部空闲(标签修复后首次接任务可能需要时间)
- 并发能力充足,瓶颈在代码/测试本身,不在 Runner 资源
+136
View File
@@ -0,0 +1,136 @@
# 三台服务器 Runner 分工规划
**制定日期:** 2026-07-13
**状态:** 规划中
---
## 一、现状总览
当前共 9 个 Gitea Actions Runner,分布在 3 台服务器上:
| 服务器 | IP | 配置 | Runner 数量 | 当前状态 |
|--------|-----|------|-------------|----------|
| 构建服务器 | 114.55.236.178 | 4核 / 7.1G RAM / 49G NVMe | 4个(ID: 8, 42, 46, 47 | ✅ 在线 |
| 新CI服务器 | 116.62.226.203 | 8核 / 14G RAM | 5个(ID: 58-62 | ✅ 在线 |
| 业务服务器 | 47.98.113.167 | - | 0个(旧3个已下线) | ⚠️ 待规划 |
**所有 Runner 共用标签:** `saas`, `runtime-builder`, `host`, `ubuntu-latest`
---
## 二、问题分析
### 2.1 标签无区分
所有 Runner 标签完全一致,CI 任务随机分配到任意 Runner,导致:
- 构建任务(Build)可能跑到配置低的机器上,构建慢
- 代码检查任务占着构建服务器,影响构建速度
- 业务服务器跑 CI 影响线上服务稳定性
### 2.2 资源浪费
- 新服务器 8核14G 跑 validate/lint 有点大材小用
- 构建服务器 4核7G 跑 Docker 构建偏紧张
---
## 三、规划方案
### 3.1 分工原则
| 服务器 | 角色 | 主要任务类型 | 标签策略 |
|--------|------|-------------|----------|
| **构建服务器** (114.55.236.178) | 构建专机 | Build Staging / Build Production / Docker 镜像构建 | 保留 `saas` + `host`,新增 `build-only` |
| **新CI服务器** (116.62.226.203) | 代码检查专机 | Validate / Unit Tests / Integration Tests / Frontend Lint | 保留 `saas` + `host`,新增 `ci-check` |
| **业务服务器** (47.98.113.167) | 部署专机 | Deploy Staging / Deploy Production / E2E Tests | 保留 `saas` + `host`,新增 `deploy-only` |
### 3.2 具体配置
#### 构建服务器(4个 Runner
- **数量:** 3个(从4个缩减,释放资源给构建缓存)
- **标签:** `saas`, `host`, `build-only`, `ubuntu-latest`
- **负责 Job**
- `build-staging`
- `build-production-runtime-images`
- 其他需要 Docker buildx 的任务
#### 新CI服务器(5个 Runner
- **数量:** 5个(保持不变)
- **标签:** `saas`, `host`, `ci-check`, `ubuntu-latest`
- **负责 Job**
- `validate`
- `unit-tests`
- `integration-tests`
- `frontend-lint`
- 安全扫描(gitleaks / pip-audit / vulture 等)
#### 业务服务器(1-2个 Runner
- **数量:** 1-2个(逐步替换旧的3个)
- **标签:** `saas`, `host`, `deploy-only`, `ubuntu-latest`
- **负责 Job**
- `deploy-staging`
- `deploy-production`
- `staging-e2e` / `production-e2e`
- `staging-api-tests`
---
## 四、实施步骤
### Phase 1: 标签打标(低风险,立即做)
1. 新服务器 5 个 Runner 添加 `ci-check` 标签
2. 构建服务器保留 3 个 Runner,添加 `build-only` 标签
3. 业务服务器部署 1 个新 Runner,标签 `deploy-only`
### Phase 2: Job 路由调整(中风险,逐步来)
1. validate / unit-tests / integration-tests / frontend-lint 改为 `runs-on: ci-check`
2. build-staging / build-production 改为 `runs-on: build-only`
3. deploy-* / e2e 改为 `runs-on: deploy-only`
### Phase 3: 旧 Runner 下线
- 业务服务器旧的 3 个 Runner 确认无任务后下线
- 构建服务器多余的 1 个 Runner 迁移到新服务器
---
## 五、并发配置优化建议
### 5.1 当前并发情况
- 首发并行 Jobvalidate + unit-tests + frontend-lint3个并行)
- integration-tests 依赖 validate(串行,浪费资源)
- 无 concurrency 限制,同一分支多次 push 会重复跑
### 5.2 优化建议
**1. integration-tests 改为与 unit-tests 并行**
```yaml
# 当前
integration-tests:
needs: validate # 没必要等validate
# 优化后
integration-tests:
needs: [] # 直接和unit-tests并行跑
```
**2. 增加分支级 concurrency,取消重复构建**
```yaml
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
```
同一 PR 多次 push 时,取消旧的构建,只跑最新的。
**3. Build Staging 移出 PR 门禁**
- 已在阶段二优化中完成(PR #245
- Build Staging 只在 develop/main 上异步构建
---
## 六、预期收益
| 指标 | 当前 | 优化后 | 提升 |
|------|------|--------|------|
| PR CI 总时长 | ~8-12分钟 | ~4-6分钟 | ⏱️ 缩短 40-50% |
| 构建速度 | 可能抢到慢机器 | 固定高配构建机 | 🚀 更稳定更快 |
| 线上稳定性 | CI和业务抢资源 | 部署独立Runner | 🛡️ 隔离保障 |
| Runner 利用率 | 随机分配 | 按任务类型调度 | 📈 更合理 |