// API client — reads credentials from localStorage, injects headers on every request. // All functions return parsed JSON or throw an Error with .message set. const API_BASE = ""; // same origin (served by FastAPI) const LS = { token: "jwt_token", rpaHost: "rpa_host", rpaPort: "rpa_port", rpaGitUrl: "rpa_git_url", rpaPrompt: "rpa_prompt", // 自定义分析提示词(空 = 使用服务端默认) feishuFields:"feishu_fields_json", // Hermes 推导的字段定义(JSON 字符串,仅供展示) hermesKey: "hermes_api_key", hermesUrl: "hermes_api_url", feishuId: "feishu_app_id", feishuSecret:"feishu_app_secret", feishuBase: "feishu_base_token", feishuMeta: "feishu_metadata_table_id", feishuAna: "feishu_analysis_table_id", feishuFolder:"feishu_doc_folder", userId: "user_id", userEmail: "user_email", }; window.LS = LS; function _get(key) { return localStorage.getItem(key) || ""; } function _authHeaders() { const token = _get(LS.token); if (!token) throw new Error("未登录,请先登录"); return { "Authorization": `Bearer ${token}` }; } function _keyHeaders() { const feishuId = _get(LS.feishuId); const feishuSecret = _get(LS.feishuSecret); const feishuBase = _get(LS.feishuBase); const feishuMeta = _get(LS.feishuMeta); const feishuAna = _get(LS.feishuAna); const rpaHost = _get(LS.rpaHost); const rpaPort = _get(LS.rpaPort); const rpaGitUrl = _get(LS.rpaGitUrl); const missingFeishu = !feishuId || !feishuSecret || !feishuBase || !feishuMeta || !feishuAna; const missingRpa = !rpaHost || !rpaPort || !rpaGitUrl; if (missingFeishu) throw new Error("请先在设置页填写飞书配置(App ID / Secret / Base Token / 两张表 ID)"); if (missingRpa) throw new Error("请先在设置页填写 RPA 服务地址、端口和 Git 仓库地址"); const rpaUrl = `http://${rpaHost}:${rpaPort}`; const h = { "X-Feishu-App-Id": feishuId, "X-Feishu-App-Secret": feishuSecret, "X-Feishu-Base-Token": feishuBase, "X-Feishu-Metadata-Table-Id": feishuMeta, "X-Feishu-Analysis-Table-Id": feishuAna, "X-Rpa-Url": rpaUrl, "X-Rpa-Git-Url": rpaGitUrl, }; const folder = _get(LS.feishuFolder); if (folder) h["X-Feishu-Doc-Folder"] = folder; // 自定义提示词:有内容才传,空时服务端使用默认 prompt 文件 const rpaPrompt = _get(LS.rpaPrompt); if (rpaPrompt) h["X-Rpa-Prompt"] = rpaPrompt; const hermesUrl = _get(LS.hermesUrl); const hermesKey = _get(LS.hermesKey); if (hermesUrl) h["X-Hermes-Url"] = hermesUrl; if (hermesKey) h["X-Hermes-Key"] = hermesKey; return h; } function _hermesHeaders() { /** 仅需 Hermes 凭证(用于提示词优化/字段推导,不需要飞书/RPA 配置)。 */ const hermesUrl = _get(LS.hermesUrl); const hermesKey = _get(LS.hermesKey); if (!hermesUrl || !hermesKey) throw new Error("请先在设置页填写 Hermes API URL 和 Key"); return { "X-Hermes-Url": hermesUrl, "X-Hermes-Key": hermesKey }; } async function _fetch(path, opts = {}) { const resp = await fetch(API_BASE + path, { ...opts, headers: { "Content-Type": "application/json", ...(opts.headers || {}) }, }); const text = await resp.text(); let data; try { data = JSON.parse(text); } catch { data = { detail: text }; } if (!resp.ok) { // JWT 过期 / 无效 → 清除 token,1.5s 后刷新到登录页 if (resp.status === 401) { localStorage.removeItem(LS.token); setTimeout(() => window.location.reload(), 1500); throw new Error("登录已过期,请重新登录(1.5 秒后自动跳转到登录页)"); } let detail = data.detail; if (Array.isArray(detail)) { detail = detail.map(e => e.msg || JSON.stringify(e)).join("; "); } else if (detail && typeof detail !== "string") { detail = JSON.stringify(detail); } throw new Error(detail || `HTTP ${resp.status}`); } return data; } // ── Auth ───────────────────────────────────────────────────────────────────── window.API = { async register(email, password) { const data = await _fetch("/auth/register", { method: "POST", body: JSON.stringify({ email, password }), }); localStorage.setItem(LS.token, data.token); localStorage.setItem(LS.userId, data.user_id); localStorage.setItem(LS.userEmail, email); return data; }, async login(email, password) { const data = await _fetch("/auth/login", { method: "POST", body: JSON.stringify({ email, password }), }); localStorage.setItem(LS.token, data.token); localStorage.setItem(LS.userId, data.user_id); localStorage.setItem(LS.userEmail, email); return data; }, logout() { [LS.token, LS.userId, LS.userEmail].forEach(k => localStorage.removeItem(k)); }, isLoggedIn() { return !!localStorage.getItem(LS.token); }, currentEmail() { return localStorage.getItem(LS.userEmail) || ""; }, // ── Jobs ──────────────────────────────────────────────────────────────────── async submitCollect(url) { return _fetch("/api/jobs/collect", { method: "POST", headers: { ..._authHeaders(), ..._keyHeaders() }, body: JSON.stringify({ url }), }); }, /** * 关键词搜索预览:只需 JWT,返回视频列表,不创建 Job。 * platform: "youtube" | "bilibili" */ async searchPreview(keyword, platform = "youtube", maxResults = 20) { return _fetch("/api/collect/preview", { method: "POST", headers: _authHeaders(), body: JSON.stringify({ keyword, platform, max_results: maxResults }), }); }, /** * 批量采集:为 urls 数组中每个 URL 创建独立 Job。 * 需要 JWT + API Keys(飞书 + RPA)。 */ async batchCollect(urls) { return _fetch("/api/collect/batch", { method: "POST", headers: { ..._authHeaders(), ..._keyHeaders() }, body: JSON.stringify({ urls }), }); }, async getJob(jobId) { return _fetch(`/api/jobs/${jobId}`, { headers: _authHeaders(), }); }, async listJobs() { return _fetch("/api/jobs", { headers: _authHeaders(), }); }, // ── Videos ────────────────────────────────────────────────────────────────── async listVideos(filters = {}) { const params = new URLSearchParams(); Object.entries(filters).forEach(([k, v]) => { if (v !== undefined && v !== null && v !== "") params.set(k, v); }); const qs = params.toString(); return _fetch(`/api/videos${qs ? "?" + qs : ""}`, { headers: _authHeaders(), }); }, async getVideo(videoId) { return _fetch(`/api/videos/${videoId}`, { headers: _authHeaders(), }); }, // ── Search ────────────────────────────────────────────────────────────────── async search(query, topK = 20) { return _fetch("/api/search", { method: "POST", headers: { ..._authHeaders(), ..._keyHeaders() }, body: JSON.stringify({ query, top_k: topK }), }); }, // ── Hermes ────────────────────────────────────────────────────────────────── async checkHermesWiki() { // 只需 JWT + Hermes URL/Key,不需要飞书/RPA 配置 return _fetch("/api/hermes/check-wiki", { method: "POST", headers: { ..._authHeaders(), ..._hermesHeaders() }, }); }, async initHermesWiki(agentsMd) { // 只需 JWT + Hermes URL/Key return _fetch("/api/hermes/init-wiki", { method: "POST", headers: { ..._authHeaders(), ..._hermesHeaders() }, body: JSON.stringify({ agents_md: agentsMd }), }); }, // ── Test ──────────────────────────────────────────────────────────────────── /** * Verify Feishu credentials and permissions step by step. * Reads keys from localStorage (save first if you changed them on-screen). * Returns { auth, base_read, base_write, doc_folder } — each { ok, msg }. */ async testFeishu() { const feishuId = _get(LS.feishuId); const feishuSecret = _get(LS.feishuSecret); const feishuBase = _get(LS.feishuBase); const feishuMeta = _get(LS.feishuMeta); const feishuAna = _get(LS.feishuAna); if (!feishuId || !feishuSecret || !feishuBase || !feishuMeta || !feishuAna) throw new Error("请先填写完整的飞书配置(App ID / Secret / Base Token / 两张表 ID)"); const headers = { ..._authHeaders(), "X-Feishu-App-Id": feishuId, "X-Feishu-App-Secret": feishuSecret, "X-Feishu-Base-Token": feishuBase, "X-Feishu-Metadata-Table-Id": feishuMeta, "X-Feishu-Analysis-Table-Id": feishuAna, }; const folder = _get(LS.feishuFolder); if (folder) headers["X-Feishu-Doc-Folder"] = folder; return _fetch("/api/test/feishu", { method: "POST", headers }); }, // ── Prompt ────────────────────────────────────────────────────────────────── /** 获取服务端默认提示词(首次加载时填充 textarea)。 */ async getDefaultPrompt() { return _fetch("/api/prompt/default", { headers: _authHeaders() }); }, /** * 使用 Hermes 优化提示词。 * 不需要飞书/RPA 配置,只需 Hermes URL + Key。 */ async optimizePrompt(prompt) { return _fetch("/api/prompt/optimize", { method: "POST", headers: { ..._authHeaders(), ..._hermesHeaders() }, body: JSON.stringify({ prompt }), }); }, /** * 使用 Hermes 推导飞书字段定义。 * 返回 { fields: [{name, type, label, description}] } */ async genFeishuFields(prompt) { return _fetch("/api/prompt/gen-fields", { method: "POST", headers: { ..._authHeaders(), ..._hermesHeaders() }, body: JSON.stringify({ prompt }), }); }, // ── RPA ───────────────────────────────────────────────────────────────────── /** * 检测 RPA 服务是否可达。 * 后端会代理请求到 {rpa_url}/ping,避免跨域问题。 * 返回 { reachable: bool, status?, error? } */ async pingRpa() { const rpaHost = _get(LS.rpaHost); const rpaPort = _get(LS.rpaPort); if (!rpaHost || !rpaPort) throw new Error("未配置 RPA 服务地址"); const rpaUrl = `http://${rpaHost}:${rpaPort}`; return _fetch("/api/rpa/ping", { headers: { ..._authHeaders(), "X-Rpa-Url": rpaUrl, }, }); }, // ── Health ────────────────────────────────────────────────────────────────── async health() { return _fetch("/health"); }, };