Compare commits
3 Commits
e0330bfb28
...
591bc37990
| Author | SHA1 | Date | |
|---|---|---|---|
| 591bc37990 | |||
| 579e538aa7 | |||
| 836a33e85b |
@@ -1,30 +1,5 @@
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"files_changed": 1,
|
||||
"message": "Claude 会话结束 · 持续 0 秒 · 最近命令:claude · 分支 main · 1 项未提交变更 · 最近提交:auto-save 2026-05-20 14:23 (+1, ~1)",
|
||||
"ts": "2026-05-20T06:37:09Z",
|
||||
"type": "session-end"
|
||||
},
|
||||
{
|
||||
"files_changed": 1,
|
||||
"message": "Claude 会话结束 · 持续 0 秒 · 最近命令:claude · 分支 main · 1 项未提交变更 · 最近提交:auto-save 2026-05-20 14:23 (+1, ~1)",
|
||||
"ts": "2026-05-20T06:37:09Z",
|
||||
"type": "session-end"
|
||||
},
|
||||
{
|
||||
"files_changed": 2,
|
||||
"hash": "16f78ba",
|
||||
"message": "auto-save 2026-05-20 14:39 (+1, ~1)",
|
||||
"ts": "2026-05-20T14:39:42+08:00",
|
||||
"type": "commit"
|
||||
},
|
||||
{
|
||||
"files_changed": 2,
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 2 项未提交变更 · 最近提交:auto-save 2026-05-20 14:39 (+1, ~1)",
|
||||
"ts": "2026-05-20T06:43:58Z",
|
||||
"type": "session-heartbeat"
|
||||
},
|
||||
{
|
||||
"files_changed": 3,
|
||||
"hash": "d6bba9d",
|
||||
@@ -3206,6 +3181,31 @@
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:chore: disable password login in production",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-26T09:08:30+08:00",
|
||||
"type": "commit",
|
||||
"message": "chore: migrate legacy password data to Feishu owner",
|
||||
"hash": "e0330bf",
|
||||
"files_changed": 3
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-26T01:09:34Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:chore: migrate legacy password data to Feishu owner",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-26T01:19:34Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:chore: migrate legacy password data to Feishu owner",
|
||||
"files_changed": 1
|
||||
},
|
||||
{
|
||||
"ts": "2026-05-26T01:29:34Z",
|
||||
"type": "session-heartbeat",
|
||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 main · 1 项未提交变更 · 最近提交:chore: migrate legacy password data to Feishu owner",
|
||||
"files_changed": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
1
RULES.md
1
RULES.md
@@ -23,6 +23,7 @@
|
||||
- 最近部署验证(2026-05-26):`c9d8fa7` 对应 Postgres 持久化代码已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`。生产新增 `skg-marketing-postgres` 容器,数据库持久化在服务器 `./data/postgres`,`DATABASE_URL` / `POSTGRES_PASSWORD` 只写服务器 `deploy/.env.production`。部署前脚本备份生产私有环境、任务数据、资源库和 secrets 到 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260525225145.tgz`;生产 Docker 重建后脚本内验证通过(web/API/Postgres 容器 Up、Postgres healthy、`/` 302、`/login/` 200、缺失 `_next` 资源 404、未登录 `/api/health` 401、容器内 `api:health ok db connected`、`api:ytdlp_cookie_args []`)。文档/元数据同步后又执行 `./scripts/deploy-prod-safe.sh --no-build`,实际走过 Postgres `pg_dump` 备份路径并生成 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260525230444.tgz`,复验同样通过。补验:容器内 `/health` 返回 `database.enabled=true`、`database.connected=true`,`/api/auth/config` 返回 `feishu_enabled=true`、`password_enabled=true`、`data_isolation_enabled=true`;画布项目 API 可创建、读取、软删除记录;数据库索引计数为 users=1、jobs=26、assets=129、canvas_active=0、canvas_deleted=1、audit=2。
|
||||
- 生产登录收口(2026-05-26):已在服务器 `/opt/skg-marketing-studio/deploy/.env.production` 设置 `PASSWORD_AUTH_ENABLED=false` 并通过 `./scripts/deploy-prod-safe.sh` 重建生产。部署前脚本备份到 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260526003816.tgz`;脚本内首次验证在容器启动 3 秒时遇到根路径临时 500,随后复跑 `./scripts/verify-prod-docker.sh root@76.13.31.179` 通过(web/API/Postgres Up、`/` 302、`/login/` 200、未登录 `/api/health` 401、`api:health ok db connected`)。公网复验:`/api/auth/config` 返回 `password_enabled=false`、`feishu_enabled=true`、`data_isolation_enabled=true`;`GET /api/auth/feishu/start?next=/` 返回 302 到飞书授权页;`POST /api/auth/login` 返回 503 `账号密码登录未配置`。
|
||||
- 旧密码账号归属迁移(2026-05-26):已把旧共享密码账号 `password:skg` 下的 22 个 job、3 个画布项目和对应生成资产索引迁到飞书用户 `万康`(`feishu:ou_78276b4fd9dd818d8f70bc00d03ddbdf`)。迁移前已备份数据库和 `data/jobs` 到 `/opt/skg-marketing-studio-backups/skg-marketing-owner-migration-20260526010622.sql.gz` 与 `/opt/skg-marketing-studio-backups/skg-marketing-owner-migration-jobs-20260526010622.tgz`。复验:`job_index` 中该飞书用户 24 个 job,`canvas_projects` 中该飞书用户 3 个未删除私有画布,生成资产索引为 image completed=11、video completed=11、video failed=1;无 owner 的 4 个更早旧 job 保持未迁移,后续再确认归属。
|
||||
- 视频错误提示收口(2026-05-26):`579e538` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`,部署前备份为 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260526014111.tgz`。本次把 Seedance / Doubao 视频上游错误转换为员工可读中文后再写入 `GeneratedVideo.error`,例如 `InputImageSensitiveContentDetected.PrivacyInformation` 会提示参考图含清晰人物或疑似真实人脸,需要换无脸首帧、裁切或模糊人物脸;原始上游错误只保留在 API 日志。脚本内首次验证在容器启动 3 秒时遇到根路径临时 500,随后复跑 `./scripts/verify-prod-docker.sh root@76.13.31.179` 通过(web/API/Postgres Up、`/` 302、`/login/` 200、未登录 `/api/health` 401、`api:health ok db connected`),并在生产 API 容器内确认隐私风控码会返回中文解释。
|
||||
- 最近部署验证(2026-05-25):`cce9779` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`,恢复 `chatfire-AI/huobao-canvas` 上游画布能力但保留 SKG 后端 `/api` 接入。部署前脚本已备份生产私有环境、任务数据、资源库和 secrets 到 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260525102857.tgz`;生产 Docker 重建后脚本内验证通过(web/API 容器 Up、`/` 302、`/login/` 200、缺失 `_next` 资源 404、未登录 `/api/health` 401、容器内 `api:health ok`、`api:ytdlp_cookie_args []`、未发现本地 API/dev URL 泄漏)。补验:外部访问 `https://marketing.skg.com/` 未登录返回 302 到 `/login/?next=/`,`https://marketing.skg.com/canvas/` 返回 308 到 `/`,`https://marketing.skg.com/p/test` 未登录返回 302 到 `/login/?next=/p/test`;容器内静态 bundle 命中 `AI 润色 / 自动执行 / 推荐: / 首帧 / 尾帧 / 多角度分镜 / 儿童绘本 / 工作流模板 / 批量下载素材`,未命中上游注册链接、火宝欢迎文案、GitHub 入口或 `/huobao-canvas`。
|
||||
- 最近部署验证(2026-05-25):`e767d2b` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`,生产根域名改为直接进入个人生成画布,`/canvas/` 仅作为旧链接 308 跳转到 `/`。部署前脚本已备份生产私有环境、任务数据、资源库和 secrets 到 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260525095839.tgz`;生产 Docker 重建后脚本内验证通过(web/API 容器 Up、`/` 302、`/login/` 200、缺失 `_next` 资源 404、未登录 `/api/health` 401、容器内 `api:health ok`、`api:ytdlp_cookie_args []`、未发现本地 API/dev URL 泄漏)。补验:容器内 `/usr/share/nginx/html/index.html` 为 Vue 画布产物,引用 `/assets/index-CioZwOvT.js` 且 title 为 `SKG`;静态 bundle 命中 `文生图 / 文生视频 / 图生视频`,未命中 `首帧生视频 / 首尾帧生视频 / 上传首帧 / 上传尾帧 / 推荐:`;外部访问 `https://marketing.skg.com/` 未登录返回 302 到 `/login/?next=/`,`https://marketing.skg.com/canvas/` 返回 308 到 `/`,`/p/test` 未登录返回 302 到 `/login/?next=/p/test`。
|
||||
- 最近部署验证(2026-05-25):`2a1ceee` 已通过 `./scripts/deploy-prod-safe.sh` 部署到 `/opt/skg-marketing-studio`,可见品牌位从文字命名收敛为 logo-only:首页、登录页和画布首页只显示 SKG logo,网页 title 和画布 title 为 `SKG`,首页入口按钮文案为“画布”。部署前脚本已备份生产私有环境、任务数据、资源库和 secrets 到 `/opt/skg-marketing-studio-backups/skg-marketing-preserve-20260525092749.tgz`;生产 Docker 重建后脚本内验证通过(web/API 容器 Up、`/` 302、`/login/` 200、缺失 `_next` 资源 404、未登录 `/api/health` 401、容器内 `api:health ok`、`api:ytdlp_cookie_args []`、未发现本地 API/dev URL 泄漏)。容器内静态产物复验:`index.html` 包含 `<title>SKG</title>` 和 `/skg-logo-black.svg`,首页入口包含“画布”,登录页只保留 logo;当前 `_next` 与 `/canvas` 产物未再命中 `SKG 生图生视频`、`SKG 生成画布`、`营销内容生产平台` 或 `内容生产画布` 等旧可见文案。
|
||||
|
||||
114
api/main.py
114
api/main.py
@@ -8035,6 +8035,92 @@ def video_url_from_response(data: dict) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def _video_public_error(raw: object) -> str:
|
||||
text = str(raw or "").strip()
|
||||
lower = text.lower()
|
||||
|
||||
if any(token.lower() in lower for token in (
|
||||
"InputImageSensitiveContentDetected.PrivacyInformation".lower(),
|
||||
"privacyinformation",
|
||||
"privacy information",
|
||||
"real person",
|
||||
"input image may contain real person",
|
||||
"human face",
|
||||
"face detected",
|
||||
"肖像",
|
||||
"隐私",
|
||||
"真人",
|
||||
"人脸",
|
||||
)):
|
||||
return (
|
||||
"视频生成失败:参考图里有清晰人物或疑似真实人脸,视频模型出于肖像/隐私风控拒绝生成。"
|
||||
"请换成无可识别人脸的首帧,或先裁掉/模糊人物脸,再重新生成视频。"
|
||||
)
|
||||
|
||||
if any(token in lower for token in (
|
||||
"sensitivecontent",
|
||||
"sensitive content",
|
||||
"content policy",
|
||||
"violate",
|
||||
"violation",
|
||||
"not allowed",
|
||||
"risk control",
|
||||
"moderation",
|
||||
"敏感",
|
||||
"安全审核",
|
||||
"风控",
|
||||
"违规",
|
||||
)):
|
||||
return (
|
||||
"视频生成失败:参考图或提示词触发了视频模型的内容安全审核。"
|
||||
"请换一张更中性的参考图,避免真实人物、暴露、医疗夸大、危险动作或敏感文字后重试。"
|
||||
)
|
||||
|
||||
if any(token in lower for token in ("unauthorized", "invalid api key", "permission denied", "forbidden", "http 401", "http 403")):
|
||||
return "视频生成失败:视频通道认证或权限异常,请联系管理员检查服务器上的视频 API Key 和模型权限。"
|
||||
|
||||
if any(token in lower for token in ("http 429", "rate limit", "too many requests", "quota", "insufficient", "balance", "限流", "额度", "余额")):
|
||||
return "视频生成失败:视频模型当前限流或额度不足,请稍后重试;如果持续出现,请联系管理员检查视频通道额度。"
|
||||
|
||||
if any(token in lower for token in ("timeout", "timed out", "readtimeout", "connecttimeout", "超时")):
|
||||
return "视频生成失败:视频模型响应超时,可能是上游繁忙或网络不稳定。请稍后重试,或缩短时长后再生成。"
|
||||
|
||||
if any(token in lower for token in (
|
||||
"name or service not known",
|
||||
"temporary failure in name resolution",
|
||||
"nodename nor servname",
|
||||
"connection refused",
|
||||
"network is unreachable",
|
||||
"connecterror",
|
||||
"ssl:",
|
||||
"网络",
|
||||
"dns",
|
||||
)):
|
||||
return "视频生成失败:服务器连接视频模型网关异常,请稍后重试;如果连续失败,请联系管理员检查视频网关网络。"
|
||||
|
||||
if any(token in lower for token in ("http 404", "http 405", "unsupported", "not found", "method not allowed")):
|
||||
return "视频生成失败:当前视频模型接口路径不可用,请联系管理员检查视频网关配置。"
|
||||
|
||||
if lower.startswith("video status: failed") or "video status: failed" in lower:
|
||||
return "视频生成失败:视频模型返回生成失败。请换一张更清晰、主体更稳定的参考图,或简化提示词后重试。"
|
||||
|
||||
if text.startswith("视频生成失败:"):
|
||||
return text[:500]
|
||||
if text:
|
||||
return f"视频生成失败:{text[:460]}"
|
||||
return "视频生成失败:未知错误,请换一张参考图或稍后重试。"
|
||||
|
||||
|
||||
def _video_create_failure_message(create_errors: list[str]) -> str:
|
||||
raw = " | ".join(create_errors)
|
||||
public = _video_public_error(raw)
|
||||
if public.startswith("视频生成失败:当前视频模型接口路径不可用"):
|
||||
return public
|
||||
if public.startswith("视频生成失败:") and public != f"视频生成失败:{raw[:460]}":
|
||||
return public
|
||||
return "视频生成失败:视频模型没有接受本次请求。请换一张参考图或简化提示词后重试;如果持续失败,请联系管理员。"
|
||||
|
||||
|
||||
def download_generated_video(client, base: str, headers: dict, provider_id: str, direct_url: str, out_mp4: Path) -> None:
|
||||
if direct_url:
|
||||
url = direct_url if direct_url.startswith("http") else f"{base}{direct_url if direct_url.startswith('/') else '/' + direct_url}"
|
||||
@@ -8183,27 +8269,29 @@ def render_storyboard_video(
|
||||
for create_path in VIDEO_CREATE_PATHS:
|
||||
resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, source_ref, prepared_last_img, prepared_product_imgs, primary_role)
|
||||
if video_uses_ark() and source_ref and resp.status_code in {400, 422}:
|
||||
create_errors.append(f"{video_path(create_path)} + reference_video -> HTTP {resp.status_code}: {resp.text[:160]}")
|
||||
create_errors.append(f"{video_path(create_path)} + reference_video -> HTTP {resp.status_code}: {resp.text[:700]}")
|
||||
resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, None, prepared_last_img, prepared_product_imgs, primary_role)
|
||||
if video_uses_ark() and prepared_last_img and resp.status_code in {400, 422}:
|
||||
create_errors.append(f"{video_path(create_path)} + last_frame -> HTTP {resp.status_code}: {resp.text[:160]}")
|
||||
create_errors.append(f"{video_path(create_path)} + last_frame -> HTTP {resp.status_code}: {resp.text[:700]}")
|
||||
resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, None, None, prepared_product_imgs, primary_role)
|
||||
if video_uses_ark() and prepared_product_imgs and resp.status_code in {400, 422}:
|
||||
create_errors.append(f"{video_path(create_path)} + product_reference -> HTTP {resp.status_code}: {resp.text[:160]}")
|
||||
create_errors.append(f"{video_path(create_path)} + product_reference -> HTTP {resp.status_code}: {resp.text[:700]}")
|
||||
resp = submit_video_create(client, f"{base}{video_path(create_path)}", headers, ref_img, payload, None, prepared_last_img, None, primary_role)
|
||||
if resp.status_code < 400:
|
||||
create = resp
|
||||
break
|
||||
create_errors.append(f"{video_path(create_path)} -> HTTP {resp.status_code}: {resp.text[:160]}")
|
||||
create_errors.append(f"{video_path(create_path)} -> HTTP {resp.status_code}: {resp.text[:700]}")
|
||||
if resp.status_code not in {400, 404, 405}:
|
||||
resp.raise_for_status()
|
||||
if create is None:
|
||||
raise RuntimeError("视频模型已选择,但当前网关视频生成入口不可用;已尝试 " + " | ".join(create_errors))
|
||||
print(f"[video create failed] job={job_id} video={local_id} errors={' | '.join(create_errors)[:1800]}", flush=True)
|
||||
raise RuntimeError(_video_create_failure_message(create_errors))
|
||||
data = create.json()
|
||||
video_api_id = data.get("id") or provider_id or local_id
|
||||
status = normalize_video_status(data.get("status"))
|
||||
progress = video_progress(data, 5)
|
||||
direct_url = video_url_from_response(data)
|
||||
status_payload = data
|
||||
update_generated_video(
|
||||
job_id,
|
||||
local_id,
|
||||
@@ -8222,6 +8310,7 @@ def render_storyboard_video(
|
||||
status = normalize_video_status(pdata.get("status"))
|
||||
progress = video_progress(pdata, progress)
|
||||
direct_url = video_url_from_response(pdata) or direct_url
|
||||
status_payload = pdata
|
||||
update_generated_video(
|
||||
job_id,
|
||||
local_id,
|
||||
@@ -8231,7 +8320,17 @@ def render_storyboard_video(
|
||||
)
|
||||
|
||||
if status != "completed":
|
||||
update_generated_video(job_id, local_id, status="failed", error=f"video status: {status}", progress=progress, queue_message="")
|
||||
raw_error = ""
|
||||
if isinstance(status_payload, dict):
|
||||
raw_error = str(
|
||||
status_payload.get("error")
|
||||
or status_payload.get("message")
|
||||
or status_payload.get("reason")
|
||||
or status_payload.get("fail_reason")
|
||||
or status_payload
|
||||
)
|
||||
print(f"[video status failed] job={job_id} video={local_id} status={status} error={raw_error[:1200]}", flush=True)
|
||||
update_generated_video(job_id, local_id, status="failed", error=_video_public_error(raw_error or f"video status: {status}"), progress=progress, queue_message="")
|
||||
return
|
||||
|
||||
download_generated_video(client, base, headers, video_api_id, direct_url, out_mp4)
|
||||
@@ -8247,7 +8346,8 @@ def render_storyboard_video(
|
||||
queue_message="",
|
||||
)
|
||||
except Exception as e:
|
||||
update_generated_video(job_id, local_id, status="failed", error=str(e)[:500], queue_message="")
|
||||
print(f"[video task failed] job={job_id} video={local_id} error={str(e)[:1200]}", flush=True)
|
||||
update_generated_video(job_id, local_id, status="failed", error=_video_public_error(e), queue_message="")
|
||||
|
||||
|
||||
@app.post("/jobs/{job_id}/frames/{idx}/storyboard/quick-plan", response_model=StoryboardScene)
|
||||
|
||||
@@ -1241,6 +1241,18 @@ ProductRefStateItem {
|
||||
<h2>变更记录</h2>
|
||||
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
|
||||
<div class="changelog">
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-05-26 · 视频生成失败改为员工可读提示</h3>
|
||||
<span class="tag amber">API</span>
|
||||
<span class="tag rose">UX</span>
|
||||
</header>
|
||||
<div class="body">
|
||||
<p><strong>问题:</strong>Seedance / Doubao 视频上游返回 <code>InputImageSensitiveContentDetected.PrivacyInformation</code>、HTTP 400、429、timeout 等机器错误时,画布错误框原样展示会让员工误以为账号、模型或网关坏了,需要人工解释。</p>
|
||||
<p><strong>改动:</strong><code>api/main.py</code> 新增视频错误归一化逻辑,提交失败、轮询失败和后台任务异常都会先转换成可读中文,再写入 <code>GeneratedVideo.error</code>。例如含疑似真实人脸的参考图会提示“参考图里有清晰人物或疑似真实人脸,视频模型出于肖像/隐私风控拒绝生成”,并给出换无脸首帧、裁掉或模糊人物脸的下一步。</p>
|
||||
<p><strong>影响:</strong>前端现有视频失败卡、画布轮询错误框和详情里的 <code>video.error</code> 会自动显示中文解释;原始上游错误只写入 API 日志,方便管理员排查,不再要求用户把英文错误码发给开发者翻译。</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-05-26 · 生产登录改为仅飞书</h3>
|
||||
|
||||
Reference in New Issue
Block a user