feat: auto-start feishu login in client
This commit is contained in:
2
RULES.md
2
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`,否则会清空案例、登录和模型配置。
|
||||
|
||||
## 快捷登录
|
||||
|
||||
@@ -122,7 +122,7 @@ server {
|
||||
}
|
||||
|
||||
location @login_redirect {
|
||||
return 302 /login/;
|
||||
return 302 /login/?next=$request_uri;
|
||||
}
|
||||
|
||||
location / {
|
||||
|
||||
@@ -607,7 +607,7 @@
|
||||
<tr><td><code>AdRecreationBoard</code> 主题切换</td><td>左侧中段 65px 胶囊工具条上方图标组里有 <code>Sun</code> / <code>Moon</code> 图标按钮,切换 <code>skg-board-theme--light</code> 类名,并把选择写入 <code>localStorage["skg-board-theme"]</code>。暗色仍是默认模式;明亮模式只改变工作台外观,不改变任务、素材、分镜、模型调用或接口数据。</td></tr>
|
||||
<tr><td><code>SourceReferenceBuildPanel</code></td><td>旧的“相似主体 / 主体模板”大面板代码仍保留在文件里,方便以后恢复模板库复用、入库命名和自定义视图选择;但当前源视频工作区主路径已经由 <code>SourceSubjectPipeline</code> 承接,不再在页面下方渲染这块,避免和“参考帧池 → 转换层 → 主体元素”重复。</td></tr>
|
||||
<tr><td><code>web/components/media-asset-tile.tsx</code></td><td>项目内媒体素材缩略图基底组件:图片、视频、抽帧、产品图、相似主体图、首尾帧和视频候选默认从这里获得统一交互。组件负责缩略图显示、顶层固定浮层 hover 放大、删除按钮、下载/重新生成等操作按钮、忙碌遮罩和图片/视频共用预览,避免每个新板块重复手写不同的媒体交互。hover 预览支持 <code>previewPlacement</code> 和 <code>previewMaxWidth</code>,视频候选可让操作按钮常显,保证下载入口不是隐藏语义;参考帧池用左侧紧凑预览避免遮住转换层;画面胶片是例外:为了保持胶片原位浏览,不使用额外弹出预览,只让胶片缩略图自己在轨道内放大。</td></tr>
|
||||
<tr><td><code>web/app/login/page.tsx</code></td><td>生产登录页:先读取 <code>/api/auth/config</code>,飞书 OAuth 配好时显示“飞书免登录”主按钮,账号密码表单保留为备用入口;登录成功后由后端设置 HttpOnly 会话 Cookie。当前只在原版 Digital Oasis 动态背景上叠加一个组合登录框,桌面端左侧是动态角色,右侧是图标化登录区;面板左上角展示官网 SKG 字标和中文“营销内容工作台”系统标识。</td></tr>
|
||||
<tr><td><code>web/app/login/page.tsx</code></td><td>生产登录页:先读取 <code>/api/auth/config</code>,飞书 OAuth 配好时显示“飞书免登录”主按钮,账号密码表单保留为备用入口;如果 <code>feishu_enabled=true</code> 且浏览器 UA 命中飞书 / Lark 客户端,会在登录页自动跳转 <code>/api/auth/feishu/start</code>,让飞书客户端内打开应用时不需要再点一次按钮;登录成功后由后端设置 HttpOnly 会话 Cookie。当前只在原版 Digital Oasis 动态背景上叠加一个组合登录框,桌面端左侧是动态角色,右侧是图标化登录区;面板左上角展示官网 SKG 字标和中文“营销内容工作台”系统标识。</td></tr>
|
||||
<tr><td><code>web/app/login/layout.tsx</code></td><td>登录路由专属 layout:覆盖全站默认网页标题和描述为空,避免 <code>/login</code> 继承工作台 metadata 后在页面源码里继续出现登录界面文字以外的文案。</td></tr>
|
||||
<tr><td><code>web/components/login/oasis-canvas.tsx</code></td><td>登录页全屏动态视觉层:用 iframe 直接承载下载包 <code>web/public/oasis-source/index.html</code> 的原 WebGPU / Three.js 草场源码;父级登录页只覆盖自己的文案和表单,并在捕获阶段把全局鼠标坐标同时用原生事件和 <code>postMessage</code> 转发给 iframe,避免登录面板或输入框遮挡时草地失去鼠标响应。</td></tr>
|
||||
<tr><td><code>web/public/oasis-source/index.html</code></td><td>从下载包 <code>remix-3d-website-the-digital-o</code> 复制来的原始视觉源码。嵌入登录页时会隐藏 demo 站自己的导航、文字和设置面板,保留原多段滚动背景变化、WebGPU 草场、景深、风动和鼠标交互源码;末端阶段保留,只禁用原 footer 出现时把 canvas 上移的逻辑,避免底部露黑边。</td></tr>
|
||||
@@ -1033,7 +1033,7 @@ ProductRefStateItem {
|
||||
<tr><th>功能</th><th>接口</th><th>前端调用</th><th>说明</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>网页登录 / 飞书免登录</td><td><code>GET /auth/config</code>、<code>GET /auth/feishu/start</code>、<code>GET /auth/feishu/callback</code>、<code>POST /auth/login</code>、<code>GET /auth/check</code>、<code>GET /auth/me</code>、<code>POST /auth/logout</code></td><td><code>web/app/login/page.tsx</code>、Nginx <code>auth_request</code></td><td>登录页先读 <code>/api/auth/config</code> 判断是否显示飞书按钮;飞书 OAuth 成功后后端用 open_id / union_id / email 生成多用户会话并设置 HttpOnly Cookie。账号密码登录保留为备用方式。生产 Nginx 对工作台和 <code>/api/</code> 调 <code>/auth/check</code> 做统一校验,未登录页面跳 <code>/login/</code>,API 返回 JSON 401。</td></tr>
|
||||
<tr><td>网页登录 / 飞书免登录</td><td><code>GET /auth/config</code>、<code>GET /auth/feishu/start</code>、<code>GET /auth/feishu/callback</code>、<code>POST /auth/login</code>、<code>GET /auth/check</code>、<code>GET /auth/me</code>、<code>POST /auth/logout</code></td><td><code>web/app/login/page.tsx</code>、Nginx <code>auth_request</code></td><td>登录页先读 <code>/api/auth/config</code> 判断是否显示飞书按钮;飞书客户端内且 <code>feishu_enabled=true</code> 时前端自动跳转授权入口,普通浏览器保留手动按钮和备用账号。飞书 OAuth 成功后后端用 open_id / union_id / email 生成多用户会话并设置 HttpOnly Cookie。账号密码登录保留为备用方式。生产 Nginx 对工作台和 <code>/api/</code> 调 <code>/auth/check</code> 做统一校验,未登录页面跳 <code>/login/?next=$request_uri</code>,API 返回 JSON 401。</td></tr>
|
||||
<tr><td>运行配置 / 模型标注</td><td><code>GET /health</code></td><td><code>getRuntimeHealth</code>、<code>ModelTrace</code></td><td>返回 <code>models</code>:ASR、<code>asr_language</code>(默认 <code>auto</code>,表示中文/英文/多语言自动识别)、<code>asr_base_url</code>、<code>asr_remote_enabled</code>、<code>asr_local_fallback_enabled</code>、<code>asr_audio_fallback_enabled</code>、<code>faster_whisper</code>、本机 ASR、ASR fallback、翻译、GPT 改写、GPT 画面理解、产品视角识别 <code>product_view</code>、主图像模型 <code>gpt-image-2</code>、图片故障兜底 <code>image_fallbacks</code>、图片尺寸 <code>image_size_options</code>、短时熔断状态 <code>image_circuit</code>、主体 6 视图模型链路、Azure OpenAI TTS、视频别名、视频画幅 <code>video_size_options</code>、真实可用视频时长 <code>video_duration_options</code>、单条最大秒数 <code>video_max_duration_seconds</code> 和 Seedance 服务商。当前 <code>REWRITE_MODEL</code>、<code>AUDIO_REWRITE_MODEL</code> 和 <code>VISION_MODEL</code> 默认使用 <code>gpt-4o</code>;如果旧环境变量仍写 <code>gemini-*</code>,后端会归一化回 <code>GPT_TEXT_MODEL</code> / <code>REWRITE_MODEL</code>。语音只走 Azure OpenAI TTS,<code>models.voice_tts_paths</code> 会回传当前尝试的语音路径,方便区分路径错误和语音服务不可用。前端所有当前主路径里会调用模型的按钮旁显示模型名,点击弹出小窗口查看模型链路和输入输出逻辑;不返回 API Key 或敏感凭证。</td></tr>
|
||||
<tr><td>历史列表</td><td><code>GET /jobs</code></td><td><code>listJobs</code></td><td>当前登录用户可见 job 精简列表(id/url/status/thumbnail/mtime/owner…),按 state.json mtime 倒序。前端 URL 无 <code>?job=</code> 时拉它回填本人历史;带 <code>limit</code> 可截断。开启数据隔离时,飞书用户只看到自己的任务,历史无 owner 的旧任务只对备用账号可见。</td></tr>
|
||||
<tr><td>创建任务</td><td><code>POST /jobs</code></td><td><code>createJob</code></td><td>提交 TK 链接,后台开始下载;后端会把当前登录用户写入 <code>Job.owner_*</code>,后续详情、素材文件、删除和生成接口都通过统一中间件校验归属。下载阶段默认不带 cookies;生产环境必须显式保持 <code>YTDLP_COOKIES_FILE=</code> 和 <code>YTDLP_COOKIES_FROM_BROWSER=</code> 为空,避免容器内误读被打进镜像的开发 <code>api/.env</code>。</td></tr>
|
||||
@@ -1183,6 +1183,19 @@ ProductRefStateItem {
|
||||
<h2>变更记录</h2>
|
||||
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
|
||||
<div class="changelog">
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-05-25 · 飞书客户端内登录页自动发起免登录</h3>
|
||||
<span class="tag amber">Auth</span>
|
||||
<span class="tag rose">UI</span>
|
||||
<span class="tag blue">Ops</span>
|
||||
</header>
|
||||
<div class="body">
|
||||
<p><strong>问题:</strong>当前后端已经有飞书 OAuth 的 start / callback / user info / 会话 Cookie 链路,但登录页仍要求用户点一次“飞书免登录”。用户希望在飞书里打开应用时自动免登录。</p>
|
||||
<p><strong>改动:</strong><code>web/app/login/page.tsx</code> 读取 <code>/api/auth/config</code> 后,如果 <code>feishu_enabled=true</code> 且 UA 命中飞书 / Lark 客户端,会用 <code>sessionStorage</code> 防循环后自动跳转 <code>/api/auth/feishu/start?next=...</code>;普通浏览器继续显示飞书按钮和备用账号。<code>deploy/nginx.conf</code> 的未登录跳转改为 <code>/login/?next=$request_uri</code>,飞书或备用账号登录成功后回到原页面。</p>
|
||||
<p><strong>影响:</strong>该能力仍依赖生产服务器配置 <code>FEISHU_APP_ID</code>、<code>FEISHU_APP_SECRET</code>、<code>FEISHU_REDIRECT_URI</code> 和 <code>WEB_AUTH_SESSION_SECRET</code>,并要求飞书开放平台登记回调 <code>https://marketing.skg.com/api/auth/feishu/callback</code>。未配置时 <code>feishu_enabled=false</code>,页面不会自动跳转。</p>
|
||||
</div>
|
||||
</article>
|
||||
<article class="change">
|
||||
<header>
|
||||
<h3>2026-05-25 · 修复空白创作任务请求体解析失败</h3>
|
||||
|
||||
@@ -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<AuthConfig | null>(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<LoginStatus>("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 (
|
||||
|
||||
Reference in New Issue
Block a user