diff --git a/RULES.md b/RULES.md index bc86f38..fafe17e 100644 --- a/RULES.md +++ b/RULES.md @@ -70,7 +70,7 @@ - 当前音频解析:`https://ai.skg.com/azure/v1` 的 `gpt-4o-transcribe` 当前返回 `DeploymentNotFound`,且官方 Azure OpenAI transcription 路径探测也未返回可用部署;生产临时复制本地成功策略,直接使用容器内多语言 `faster-whisper` 真实转写,默认语种为 `auto`,支持中文、英文和其他多语言原文识别,关闭 Gemini 多模态音频兜底。拿到真实 Azure ASR deployment 名后再恢复 `ASR_REMOTE_ENABLED=true`,并保持 `ASR_LANGUAGE` 为空或 `auto`,除非明确只想强制单一语种。 - 持久化目录:服务器 `./data/jobs` 挂载到后端 `/data/jobs`;全局资源中心持久化在 `./data/asset_library`、`./data/prompt_library` 和 `./data/_trash` - TikTok 下载登录态:公开视频默认不带 cookies 直接下载,生产环境变量必须显式保持 `YTDLP_COOKIES_FILE=`、`YTDLP_COOKIES_FROM_BROWSER=` 为空,防止容器读取不存在的浏览器 cookies。只有 TikTok 明确要求登录态时,才使用服务器私有 cookies 文件 `./secrets/tiktok_cookies.txt` 挂载到 API 容器 `/run/secrets/tiktok_cookies.txt` 并配置 `YTDLP_COOKIES_FILE=/run/secrets/tiktok_cookies.txt`;`yt-dlp` 会在任务结束时回写 cookies,因此不要把该挂载设为只读;不要使用云端浏览器读取方案,也不要把 cookies 入库。生产容器严禁使用 `YTDLP_COOKIES_FROM_BROWSER=chrome`。 -- 登录凭证:生产入口以飞书免登录为主;飞书 OAuth 的 `FEISHU_APP_ID` / `FEISHU_APP_SECRET` 只放服务器 `deploy/.env.production`,回调地址固定为 `https://marketing.skg.com/api/auth/feishu/callback` 并需要在飞书开放平台应用安全设置中登记。原账号密码登录保留为备用入口,用户名写下方快捷登录,密码明文备份只放服务器 `/root/skg-marketing-studio-login.txt`,`WEB_AUTH_PASSWORD` / `WEB_AUTH_SESSION_SECRET` 只放服务器 `deploy/.env.production`。开启 `AUTH_DATA_ISOLATION_ENABLED=true` 后,新建任务、素材任务和一键出片记录按登录用户隔离;历史无 owner 的旧任务只对备用账号可见,飞书用户互不可见。 +- 登录凭证:生产入口以飞书免登录为主;飞书 OAuth 的 `FEISHU_APP_ID` / `FEISHU_APP_SECRET` 只放服务器 `deploy/.env.production`,回调地址固定为 `https://marketing.skg.com/api/auth/feishu/callback` 并需要在飞书开放平台应用安全设置中登记。登录页读取 `/api/auth/config` 后,如果检测到飞书客户端并且 `feishu_enabled=true`,会自动跳转 `/api/auth/feishu/start`,普通浏览器仍保留“飞书免登录”按钮和备用账号。原账号密码登录保留为备用入口,用户名写下方快捷登录,密码明文备份只放服务器 `/root/skg-marketing-studio-login.txt`,`WEB_AUTH_PASSWORD` / `WEB_AUTH_SESSION_SECRET` 只放服务器 `deploy/.env.production`。开启 `AUTH_DATA_ISOLATION_ENABLED=true` 后,新建任务、素材任务和一键出片记录按登录用户隔离;历史无 owner 的旧任务只对备用账号可见,飞书用户互不可见。 - 禁止手动裸 `rsync --delete` 到服务器;必须使用 `./scripts/deploy-prod-safe.sh`。如遇极端情况必须手动同步,命令必须同时包含 protect/exclude:`.git`、`.memory`、`.logs`、`.pids`、`data`、`jobs`、`secrets`、`api/jobs`、`api/.env`、`api/.env.local`、`api/.env.production`、`deploy/.env.production`、`web/node_modules`、`web/.next`、`web/out`。不要把本地 `api/.env` 或 `deploy/.env.production` 覆盖到 `/opt/skg-marketing-studio`,也不要删除服务器 `data/jobs`,否则会清空案例、登录和模型配置。 ## 快捷登录 diff --git a/deploy/nginx.conf b/deploy/nginx.conf index beff11b..5e12e11 100644 --- a/deploy/nginx.conf +++ b/deploy/nginx.conf @@ -122,7 +122,7 @@ server { } location @login_redirect { - return 302 /login/; + return 302 /login/?next=$request_uri; } location / { diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 80a10d5..8a11d91 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -607,7 +607,7 @@ AdRecreationBoard 主题切换左侧中段 65px 胶囊工具条上方图标组里有 Sun / Moon 图标按钮,切换 skg-board-theme--light 类名,并把选择写入 localStorage["skg-board-theme"]。暗色仍是默认模式;明亮模式只改变工作台外观,不改变任务、素材、分镜、模型调用或接口数据。 SourceReferenceBuildPanel旧的“相似主体 / 主体模板”大面板代码仍保留在文件里,方便以后恢复模板库复用、入库命名和自定义视图选择;但当前源视频工作区主路径已经由 SourceSubjectPipeline 承接,不再在页面下方渲染这块,避免和“参考帧池 → 转换层 → 主体元素”重复。 web/components/media-asset-tile.tsx项目内媒体素材缩略图基底组件:图片、视频、抽帧、产品图、相似主体图、首尾帧和视频候选默认从这里获得统一交互。组件负责缩略图显示、顶层固定浮层 hover 放大、删除按钮、下载/重新生成等操作按钮、忙碌遮罩和图片/视频共用预览,避免每个新板块重复手写不同的媒体交互。hover 预览支持 previewPlacementpreviewMaxWidth,视频候选可让操作按钮常显,保证下载入口不是隐藏语义;参考帧池用左侧紧凑预览避免遮住转换层;画面胶片是例外:为了保持胶片原位浏览,不使用额外弹出预览,只让胶片缩略图自己在轨道内放大。 - web/app/login/page.tsx生产登录页:先读取 /api/auth/config,飞书 OAuth 配好时显示“飞书免登录”主按钮,账号密码表单保留为备用入口;登录成功后由后端设置 HttpOnly 会话 Cookie。当前只在原版 Digital Oasis 动态背景上叠加一个组合登录框,桌面端左侧是动态角色,右侧是图标化登录区;面板左上角展示官网 SKG 字标和中文“营销内容工作台”系统标识。 + web/app/login/page.tsx生产登录页:先读取 /api/auth/config,飞书 OAuth 配好时显示“飞书免登录”主按钮,账号密码表单保留为备用入口;如果 feishu_enabled=true 且浏览器 UA 命中飞书 / Lark 客户端,会在登录页自动跳转 /api/auth/feishu/start,让飞书客户端内打开应用时不需要再点一次按钮;登录成功后由后端设置 HttpOnly 会话 Cookie。当前只在原版 Digital Oasis 动态背景上叠加一个组合登录框,桌面端左侧是动态角色,右侧是图标化登录区;面板左上角展示官网 SKG 字标和中文“营销内容工作台”系统标识。 web/app/login/layout.tsx登录路由专属 layout:覆盖全站默认网页标题和描述为空,避免 /login 继承工作台 metadata 后在页面源码里继续出现登录界面文字以外的文案。 web/components/login/oasis-canvas.tsx登录页全屏动态视觉层:用 iframe 直接承载下载包 web/public/oasis-source/index.html 的原 WebGPU / Three.js 草场源码;父级登录页只覆盖自己的文案和表单,并在捕获阶段把全局鼠标坐标同时用原生事件和 postMessage 转发给 iframe,避免登录面板或输入框遮挡时草地失去鼠标响应。 web/public/oasis-source/index.html从下载包 remix-3d-website-the-digital-o 复制来的原始视觉源码。嵌入登录页时会隐藏 demo 站自己的导航、文字和设置面板,保留原多段滚动背景变化、WebGPU 草场、景深、风动和鼠标交互源码;末端阶段保留,只禁用原 footer 出现时把 canvas 上移的逻辑,避免底部露黑边。 @@ -1033,7 +1033,7 @@ ProductRefStateItem { 功能接口前端调用说明 - 网页登录 / 飞书免登录GET /auth/configGET /auth/feishu/startGET /auth/feishu/callbackPOST /auth/loginGET /auth/checkGET /auth/mePOST /auth/logoutweb/app/login/page.tsx、Nginx auth_request登录页先读 /api/auth/config 判断是否显示飞书按钮;飞书 OAuth 成功后后端用 open_id / union_id / email 生成多用户会话并设置 HttpOnly Cookie。账号密码登录保留为备用方式。生产 Nginx 对工作台和 /api//auth/check 做统一校验,未登录页面跳 /login/,API 返回 JSON 401。 + 网页登录 / 飞书免登录GET /auth/configGET /auth/feishu/startGET /auth/feishu/callbackPOST /auth/loginGET /auth/checkGET /auth/mePOST /auth/logoutweb/app/login/page.tsx、Nginx auth_request登录页先读 /api/auth/config 判断是否显示飞书按钮;飞书客户端内且 feishu_enabled=true 时前端自动跳转授权入口,普通浏览器保留手动按钮和备用账号。飞书 OAuth 成功后后端用 open_id / union_id / email 生成多用户会话并设置 HttpOnly Cookie。账号密码登录保留为备用方式。生产 Nginx 对工作台和 /api//auth/check 做统一校验,未登录页面跳 /login/?next=$request_uri,API 返回 JSON 401。 运行配置 / 模型标注GET /healthgetRuntimeHealthModelTrace返回 models:ASR、asr_language(默认 auto,表示中文/英文/多语言自动识别)、asr_base_urlasr_remote_enabledasr_local_fallback_enabledasr_audio_fallback_enabledfaster_whisper、本机 ASR、ASR fallback、翻译、GPT 改写、GPT 画面理解、产品视角识别 product_view、主图像模型 gpt-image-2、图片故障兜底 image_fallbacks、图片尺寸 image_size_options、短时熔断状态 image_circuit、主体 6 视图模型链路、Azure OpenAI TTS、视频别名、视频画幅 video_size_options、真实可用视频时长 video_duration_options、单条最大秒数 video_max_duration_seconds 和 Seedance 服务商。当前 REWRITE_MODELAUDIO_REWRITE_MODELVISION_MODEL 默认使用 gpt-4o;如果旧环境变量仍写 gemini-*,后端会归一化回 GPT_TEXT_MODEL / REWRITE_MODEL。语音只走 Azure OpenAI TTS,models.voice_tts_paths 会回传当前尝试的语音路径,方便区分路径错误和语音服务不可用。前端所有当前主路径里会调用模型的按钮旁显示模型名,点击弹出小窗口查看模型链路和输入输出逻辑;不返回 API Key 或敏感凭证。 历史列表GET /jobslistJobs当前登录用户可见 job 精简列表(id/url/status/thumbnail/mtime/owner…),按 state.json mtime 倒序。前端 URL 无 ?job= 时拉它回填本人历史;带 limit 可截断。开启数据隔离时,飞书用户只看到自己的任务,历史无 owner 的旧任务只对备用账号可见。 创建任务POST /jobscreateJob提交 TK 链接,后台开始下载;后端会把当前登录用户写入 Job.owner_*,后续详情、素材文件、删除和生成接口都通过统一中间件校验归属。下载阶段默认不带 cookies;生产环境必须显式保持 YTDLP_COOKIES_FILE=YTDLP_COOKIES_FROM_BROWSER= 为空,避免容器内误读被打进镜像的开发 api/.env。 @@ -1183,6 +1183,19 @@ ProductRefStateItem {

变更记录

这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。

+
+
+

2026-05-25 · 飞书客户端内登录页自动发起免登录

+ Auth + UI + Ops +
+
+

问题:当前后端已经有飞书 OAuth 的 start / callback / user info / 会话 Cookie 链路,但登录页仍要求用户点一次“飞书免登录”。用户希望在飞书里打开应用时自动免登录。

+

改动:web/app/login/page.tsx 读取 /api/auth/config 后,如果 feishu_enabled=true 且 UA 命中飞书 / Lark 客户端,会用 sessionStorage 防循环后自动跳转 /api/auth/feishu/start?next=...;普通浏览器继续显示飞书按钮和备用账号。deploy/nginx.conf 的未登录跳转改为 /login/?next=$request_uri,飞书或备用账号登录成功后回到原页面。

+

影响:该能力仍依赖生产服务器配置 FEISHU_APP_IDFEISHU_APP_SECRETFEISHU_REDIRECT_URIWEB_AUTH_SESSION_SECRET,并要求飞书开放平台登记回调 https://marketing.skg.com/api/auth/feishu/callback。未配置时 feishu_enabled=false,页面不会自动跳转。

+
+

2026-05-25 · 修复空白创作任务请求体解析失败

diff --git a/web/app/login/page.tsx b/web/app/login/page.tsx index a7e3460..b0ff19e 100644 --- a/web/app/login/page.tsx +++ b/web/app/login/page.tsx @@ -1,7 +1,7 @@ "use client" import type { FormEvent } from "react" -import { useEffect, useMemo, useState } from "react" +import { useEffect, useMemo, useRef, useState } from "react" import { ArrowRight, Building2, @@ -21,8 +21,26 @@ type AuthConfig = { feishu_enabled?: boolean } +function normalizeNextPath(value: string | null | undefined) { + const next = (value || "/").trim() || "/" + if (!next.startsWith("/") || next.startsWith("//")) return "/" + return next +} + +function loginNextPath() { + if (typeof window === "undefined") return "/" + return normalizeNextPath(new URLSearchParams(window.location.search).get("next")) +} + +function isFeishuClient() { + if (typeof window === "undefined") return false + const ua = window.navigator.userAgent.toLowerCase() + return ua.includes("feishu") || ua.includes("lark") +} + export default function LoginPage() { const [authConfig, setAuthConfig] = useState(null) + const [nextPath] = useState(loginNextPath) const [username, setUsername] = useState("") const [password, setPassword] = useState("") const [remember, setRemember] = useState(true) @@ -31,6 +49,7 @@ export default function LoginPage() { const [hasError, setHasError] = useState(false) const [status, setStatus] = useState("idle") const [eyeOffset, setEyeOffset] = useState({ x: 0, y: 0 }) + const autoFeishuAttemptedRef = useRef(false) useEffect(() => { let cancelled = false @@ -63,6 +82,16 @@ export default function LoginPage() { const feishuEnabled = Boolean(authConfig?.feishu_enabled) const passwordEnabled = authConfig?.password_enabled ?? true + useEffect(() => { + if (!feishuEnabled || status !== "idle" || autoFeishuAttemptedRef.current || !isFeishuClient()) return + const attemptKey = `skg-feishu-auto-login:${nextPath}` + if (window.sessionStorage.getItem(attemptKey) === "1") return + window.sessionStorage.setItem(attemptKey, "1") + autoFeishuAttemptedRef.current = true + setStatus("loading") + window.location.href = `/api/auth/feishu/start?next=${encodeURIComponent(nextPath)}` + }, [feishuEnabled, nextPath, status]) + const mood: LoginCharacterMood = useMemo(() => { if (status === "success") return "success" if (hasError) return "error" @@ -92,7 +121,7 @@ export default function LoginPage() { } setStatus("success") window.setTimeout(() => { - window.location.href = "/" + window.location.href = nextPath }, 420) } catch { setStatus("idle") @@ -102,7 +131,7 @@ export default function LoginPage() { function onFeishuLogin() { setStatus("loading") - window.location.href = "/api/auth/feishu/start?next=/" + window.location.href = `/api/auth/feishu/start?next=${encodeURIComponent(nextPath)}` } return (