// ———— Settings ———— function PwdInput({ value, onChange, placeholder }) { const [show, setShow] = useState(false); return (
onChange(e.target.value)} placeholder={placeholder || "●●●●●●●●"} />
); } function SettingsPage() { // RPA const [rpaHost, setRpaHost] = useState(() => localStorage.getItem(LS.rpaHost) || "10.0.0.2"); const [rpaPort, setRpaPort] = useState(() => localStorage.getItem(LS.rpaPort) || "8000"); const [rpaGitUrl, setRpaGitUrl] = useState(() => localStorage.getItem(LS.rpaGitUrl) || ""); // Hermes(可选) const [hermesUrl, setHermesUrl] = useState(() => localStorage.getItem(LS.hermesUrl) || ""); const [hermesKey, setHermesKey] = useState(() => localStorage.getItem(LS.hermesKey) || ""); // 飞书 const [feishuAppId, setFeishuAppId] = useState(() => localStorage.getItem(LS.feishuId) || ""); const [feishuSecret, setFeishuSecret] = useState(() => localStorage.getItem(LS.feishuSecret) || ""); const [baseToken, setBaseToken] = useState(() => localStorage.getItem(LS.feishuBase) || ""); const [metaTableId, setMetaTableId] = useState(() => localStorage.getItem(LS.feishuMeta) || ""); const [anaTableId, setAnaTableId] = useState(() => localStorage.getItem(LS.feishuAna) || ""); const [docFolder, setDocFolder] = useState(() => localStorage.getItem(LS.feishuFolder) || ""); // 分析提示词(空 = 使用服务端默认文件) const [rpaPrompt, setRpaPrompt] = useState(() => localStorage.getItem(LS.rpaPrompt) || ""); const [feishuFields, setFeishuFields] = useState(() => { try { return JSON.parse(localStorage.getItem(LS.feishuFields) || "[]"); } catch { return []; } }); const [wikiState, setWikiState] = useState("idle"); const [wikiMsg, setWikiMsg] = useState(""); const [wikiCheckState, setWikiCheckState] = useState("idle"); // idle | loading | ok | error const [wikiCheckResult, setWikiCheckResult] = useState(null); // {initialized, detail} const [testState, setTestState] = useState({ status: "idle", results: null }); const [feishuTest, setFeishuTest] = useState({ status: "idle", results: null, error: "" }); const [rpaStatus, setRpaStatus] = useState("idle"); // idle | loading | ok | fail // 提示词相关操作状态 const [promptOptState, setPromptOptState] = useState("idle"); // idle | loading | ok | error const [promptOptMsg, setPromptOptMsg] = useState(""); const [fieldsState, setFieldsState] = useState("idle"); // idle | loading | ok | error const [fieldsMsg, setFieldsMsg] = useState(""); const [promptLoading, setPromptLoading] = useState(false); const [savedAt, setSavedAt] = useState(null); const [activeSection, setActiveSection] = useState("s1"); const [importMsg, setImportMsg] = useState(""); // 导入反馈 const importRef = React.useRef(null); const scrollTo = (id) => { setActiveSection(id); const el = document.getElementById(id); if (el) el.scrollIntoView({ behavior: "smooth", block: "start" }); }; const canInitWiki = hermesUrl && hermesKey; const save = () => { localStorage.setItem(LS.rpaHost, rpaHost); localStorage.setItem(LS.rpaPort, rpaPort); localStorage.setItem(LS.rpaGitUrl, rpaGitUrl); localStorage.setItem(LS.hermesUrl, hermesUrl); localStorage.setItem(LS.hermesKey, hermesKey); localStorage.setItem(LS.feishuId, feishuAppId); localStorage.setItem(LS.feishuSecret, feishuSecret); localStorage.setItem(LS.feishuBase, baseToken); localStorage.setItem(LS.feishuMeta, metaTableId); localStorage.setItem(LS.feishuAna, anaTableId); localStorage.setItem(LS.feishuFolder, docFolder); localStorage.setItem(LS.rpaPrompt, rpaPrompt); localStorage.setItem(LS.feishuFields, JSON.stringify(feishuFields)); setSavedAt(new Date()); }; const reset = () => { [LS.rpaHost, LS.rpaPort, LS.rpaGitUrl, LS.hermesUrl, LS.hermesKey, LS.feishuId, LS.feishuSecret, LS.feishuBase, LS.feishuMeta, LS.feishuAna, LS.feishuFolder, LS.rpaPrompt, LS.feishuFields, ].forEach(k => localStorage.removeItem(k)); setRpaHost("10.0.0.2"); setRpaPort("8000"); setRpaGitUrl(""); setHermesUrl(""); setHermesKey(""); setFeishuAppId(""); setFeishuSecret(""); setBaseToken(""); setMetaTableId(""); setAnaTableId(""); setDocFolder(""); setRpaPrompt(""); setFeishuFields([]); setSavedAt(null); }; // ── 导出配置 ───────────────────────────────────────────────────────────────── const exportConfig = () => { save(); // 先把屏幕上的最新值写入 localStorage const exportKeys = [ "rpaHost", "rpaPort", "rpaGitUrl", "rpaPrompt", "hermesUrl", "hermesKey", "feishuId", "feishuSecret", "feishuBase", "feishuMeta", "feishuAna", "feishuFolder", "feishuFields", ]; const cfg = {}; exportKeys.forEach(k => { const val = localStorage.getItem(LS[k]); if (val !== null && val !== "") { if (k === "feishuFields") { try { cfg[k] = JSON.parse(val); } catch { cfg[k] = []; } } else { cfg[k] = val; } } }); const blob = new Blob([JSON.stringify(cfg, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `oa-config-${new Date().toISOString().slice(0, 10)}.json`; a.click(); URL.revokeObjectURL(url); }; // ── 导入配置 ───────────────────────────────────────────────────────────────── const importConfig = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => { try { const cfg = JSON.parse(ev.target.result); const set = (lsKey, stateKey, setter) => { if (cfg[stateKey] !== undefined && cfg[stateKey] !== "") { localStorage.setItem(lsKey, cfg[stateKey]); setter(cfg[stateKey]); } }; set(LS.rpaHost, "rpaHost", setRpaHost); set(LS.rpaPort, "rpaPort", setRpaPort); set(LS.rpaGitUrl, "rpaGitUrl", setRpaGitUrl); set(LS.hermesUrl, "hermesUrl", setHermesUrl); set(LS.hermesKey, "hermesKey", setHermesKey); set(LS.feishuId, "feishuId", setFeishuAppId); set(LS.feishuSecret, "feishuSecret", setFeishuSecret); set(LS.feishuBase, "feishuBase", setBaseToken); set(LS.feishuMeta, "feishuMeta", setMetaTableId); set(LS.feishuAna, "feishuAna", setAnaTableId); set(LS.feishuFolder, "feishuFolder", setDocFolder); set(LS.rpaPrompt, "rpaPrompt", setRpaPrompt); if (Array.isArray(cfg.feishuFields)) { localStorage.setItem(LS.feishuFields, JSON.stringify(cfg.feishuFields)); setFeishuFields(cfg.feishuFields); } setSavedAt(new Date()); setImportMsg("✓ 导入成功,配置已恢复"); setTimeout(() => setImportMsg(""), 4000); } catch { setImportMsg("✗ 导入失败:不是有效的配置文件"); setTimeout(() => setImportMsg(""), 4000); } e.target.value = ""; // 允许重复选同一文件 }; reader.readAsText(file); }; // ── 提示词:加载默认值 ─────────────────────────────────────────────────────── const loadDefaultPrompt = async () => { setPromptLoading(true); try { const data = await window.API.getDefaultPrompt(); setRpaPrompt(data.prompt); setPromptOptMsg("已加载默认提示词"); setPromptOptState("ok"); } catch (e) { setPromptOptMsg(e.message); setPromptOptState("error"); } finally { setPromptLoading(false); } }; // ── 提示词:Hermes 优化 ────────────────────────────────────────────────────── const optimizePrompt = async () => { if (!rpaPrompt.trim()) { setPromptOptMsg("提示词不能为空"); setPromptOptState("error"); return; } save(); setPromptOptState("loading"); setPromptOptMsg(""); try { const data = await window.API.optimizePrompt(rpaPrompt); setRpaPrompt(data.prompt); localStorage.setItem(LS.rpaPrompt, data.prompt); setPromptOptState("ok"); setPromptOptMsg("优化完成,已更新到上方编辑框"); } catch (e) { setPromptOptState("error"); setPromptOptMsg(e.message); } }; // ── 提示词:提交并推导飞书字段 ──────────────────────────────────────────────── const genFields = async () => { if (!rpaPrompt.trim()) { setFieldsMsg("请先填写提示词"); setFieldsState("error"); return; } save(); setFieldsState("loading"); setFieldsMsg(""); try { const data = await window.API.genFeishuFields(rpaPrompt); setFeishuFields(data.fields || []); localStorage.setItem(LS.feishuFields, JSON.stringify(data.fields || [])); setFieldsState("ok"); setFieldsMsg(`推导完成,共 ${(data.fields||[]).length} 个字段`); } catch (e) { setFieldsState("error"); setFieldsMsg(e.message); } }; // ── 字段表格行编辑 ──────────────────────────────────────────────────────────── const updateField = (idx, key, val) => { setFeishuFields(prev => { const next = prev.map((f, i) => i === idx ? { ...f, [key]: val } : f); localStorage.setItem(LS.feishuFields, JSON.stringify(next)); return next; }); }; const removeField = (idx) => { setFeishuFields(prev => { const next = prev.filter((_, i) => i !== idx); localStorage.setItem(LS.feishuFields, JSON.stringify(next)); return next; }); }; const addField = () => { setFeishuFields(prev => { const next = [...prev, { name: "", type: "文本", label: "", description: "" }]; localStorage.setItem(LS.feishuFields, JSON.stringify(next)); return next; }); }; const canHermes = hermesUrl && hermesKey; const checkWiki = async () => { save(); setWikiCheckState("loading"); setWikiCheckResult(null); try { const res = await window.API.checkHermesWiki(); setWikiCheckState("ok"); setWikiCheckResult(res); } catch (err) { setWikiCheckState("error"); setWikiCheckResult({ initialized: false, detail: err.message }); } }; const initWiki = async () => { setWikiState("loading"); setWikiMsg(""); try { save(); const AGENTS_MD = `# 短视频爆款知识库 — Wiki 维护规范\n你是这个知识库唯一的维护者。收到 [INGEST] 消息时按 7 步流程处理,收到 [QUERY] 时综合 wiki 回答,末尾附 referenced_ids。`; const resp = await window.API.initHermesWiki(AGENTS_MD); setWikiState("ok"); setWikiMsg(resp.message || "初始化成功"); // 初始化完成后自动刷新检查状态 setWikiCheckResult({ initialized: true, detail: "刚刚完成初始化" }); setWikiCheckState("ok"); } catch (err) { setWikiState("error"); setWikiMsg(err.message); } }; const runTest = async () => { setTestState({ status: "loading", results: null }); setRpaStatus("loading"); const results = {}; try { const health = await window.API.health(); results.redis = health.redis_connected ? "ok" : "fail"; results.workers = health.worker_count > 0 ? "ok" : "warn"; } catch (e) { results.redis = "fail"; } setTestState({ status: "done", results }); // RPA 独立检测 try { const r = await window.API.pingRpa(); setRpaStatus(r.reachable ? "ok" : "fail"); } catch (e) { setRpaStatus("fail"); } }; const runFeishuTest = async () => { save(); // 先把屏幕上的值写入 localStorage,确保测试的是当前填写内容 setFeishuTest({ status: "loading", results: null, error: "" }); try { const results = await window.API.testFeishu(); setFeishuTest({ status: "done", results, error: "" }); } catch (e) { setFeishuTest({ status: "error", results: null, error: e.message }); } }; return ( <> {/* 隐藏的文件输入,用于导入配置 */}

设置

所有配置仅保存在浏览器 localStorage,每次请求通过 Header 传递给后端,服务器不存储。
⚠ 使用无痕模式或浏览器设置了「关闭时清除数据」会导致配置丢失,请用「导出配置」提前备份。
{importMsg && ( {importMsg} )} {savedAt && 已保存 {savedAt.toLocaleTimeString()} }
{/* ── Section 1:RPA 分析服务 ── */}

01 RPA 分析服务

视频下载后 push 到 Git 仓库,RPA 服务拉取并调用 AI 分析,分析完成后自动清理视频文件。

setRpaHost(e.target.value)} placeholder="10.0.0.2" />
RPA 服务的 IP 或域名,不含协议和端口
setRpaPort(e.target.value)} placeholder="8000" style={{maxWidth:120}} />
最终请求地址:http://{rpaHost || "10.0.0.2"}:{rpaPort || "8000"}
setRpaGitUrl(e.target.value)} placeholder="git@your-server:repo/videos.git 或 https://git.example.com/videos.git" />
视频以 video_id.mp4 命名推送;分析完成后自动 git rm 删除
setHermesUrl(e.target.value)} placeholder="http://your-server:8642" />
知识库 AI 综合洞察(搜索页用)
{/* 检查状态按钮 */} {/* 状态结果 badge */} {wikiCheckState === "ok" && wikiCheckResult && ( wikiCheckResult.initialized ? 已初始化 : 未初始化 )} {wikiCheckState === "error" && ( 检查失败 )} {/* 分隔 */} | {/* 初始化按钮 */}
{/* Hermes 回复详情 */} {wikiCheckResult && wikiCheckState === "ok" && (
{wikiCheckResult.detail}
)} {wikiMsg && (
{wikiMsg}
)} {!wikiMsg && !wikiCheckResult && (
{!canInitWiki ? "需先填写 Hermes URL + Key" : "先「检查状态」确认是否需要初始化。"}
)}
{/* ── Section 2:飞书配置 ── */}

02 飞书配置

分析结果写入飞书多维表格,AI 拆解全文存为飞书文档。视频文件不上传飞书。

setFeishuAppId(e.target.value)} placeholder="cli_xxxxxxxxxxxx" />
setBaseToken(e.target.value)} placeholder="bascn..." />
setMetaTableId(e.target.value)} placeholder="tblXXXXXXXXXXXX" />
setAnaTableId(e.target.value)} placeholder="tblXXXXXXXXXXXX" />
setDocFolder(e.target.value)} placeholder="fldbcn..." />
{/* 快捷跳转飞书表格 */}
快捷跳转: {[ { label: "元数据表", tableId: metaTableId }, { label: "分析结果表", tableId: anaTableId }, ].map(({ label, tableId }) => { const href = baseToken && tableId ? `https://www.feishu.cn/base/${baseToken}?table=${tableId}` : null; return ( e.preventDefault() : undefined} style={{ textDecoration: "none", opacity: href ? 1 : 0.4, cursor: href ? "pointer" : "not-allowed", }} > {label} ↗ ); })} {!baseToken && ( (需先填写 Base Token 和表 ID) )}
{/* ── Section 3:连接测试 ── */}

03 连接测试

验证后端服务与飞书权限,确保任务可正常运行。建议提交采集任务前先完成检测。

{/* ── 3a: 服务状态 ── */}
服务状态
{[ { k: "redis", name: "Redis", desc: "任务队列" }, { k: "workers", name: "Worker", desc: "后台处理进程" }, ].map(s => { const r = testState.results && testState.results[s.k]; return (
{s.name} {s.desc}
{!r && 未测试} {r === "ok" && 连通} {r === "warn" && 无 Worker} {r === "fail" && 失败}
); })} {/* RPA 服务状态 */}
RPA 服务 {rpaHost && rpaPort ? `http://${rpaHost}:${rpaPort}` : "未配置地址"}
{rpaStatus === "idle" && 未测试} {rpaStatus === "loading" && 检测中…} {rpaStatus === "ok" && 连通} {rpaStatus === "fail" && 不可达}
{/* ── 3b: 飞书权限 ── */}
飞书权限
会向多维表格写入并立即删除一条测试记录,以核验写入权限。
若写入权限检测失败,任务会在采集视频后才报错。请先确保所有项通过再提交任务。
{feishuTest.status === "error" && (
{feishuTest.error}
)} {[ { k:"auth", name:"应用认证", desc:"App ID + App Secret 有效" }, { k:"base_read", name:"多维表格读取", desc:"bitable:app 读取权限" }, { k:"base_write", name:"多维表格写入", desc:"bitable:app 写入权限" }, { k:"doc_folder", name:"文档文件夹", desc:"drive 写入权限(可选)" }, ].map(item => { const r = feishuTest.results && feishuTest.results[item.k]; const isOk = r && r.ok === true; const isFail = r && r.ok === false; const isSkip = r && r.ok === null; const label = r && r.msg ? r.msg : item.desc; return (
{item.name} {label}
{!r && 未检测} {isOk && 通过} {isSkip && 未配置} {isFail && 失败} {isFail && r.fix_url && ( 去授权 → )}
); })}
{/* ── Section 4:分析提示词 ── */}

04 分析提示词

发送给 AI 的分析指令。留空则使用服务端默认提示词;修改后立即生效于新提交的任务。

{/* 提示词编辑区 */}