// Shared API client + backend-event → UI-event translator. // // Exposes `window.api` (typed-ish async fetchers for every endpoint we wire // in this pass) and `window.startAgentRun` (the polling-based run driver // that delivers tool/text/done callbacks compatible with the existing // playground UI shape). // // Auth: browser requests use Authorization: Bearer. Trusted X-User-Id headers // are reserved for the upstream gateway, not emitted by this UI. (function () { const USER_ID = localStorage.getItem("baizhi.auth.user_id") || localStorage.getItem("baizhi.user") || ""; // --------------------------------------------------------------------- // Shared toast + clipboard helpers // --------------------------------------------------------------------- let toastSeq = 0; function toastAnchorElement(anchor) { if (!anchor) return null; const el = anchor.currentTarget || anchor.target || anchor; if (!el || typeof el.getBoundingClientRect !== "function") return null; return el; } function positionToastNear(item, anchor) { const rect = anchor.getBoundingClientRect(); const itemRect = item.getBoundingClientRect(); const gap = 8; const margin = 8; const left = Math.min( Math.max(rect.left + rect.width / 2 - itemRect.width / 2, margin), Math.max(margin, window.innerWidth - itemRect.width - margin), ); let top = rect.bottom + gap; if (top + itemRect.height + margin > window.innerHeight) { top = Math.max(margin, rect.top - itemRect.height - gap); } item.style.left = `${left}px`; item.style.top = `${top}px`; } function removeToast(item, host) { item.classList.add("leaving"); window.setTimeout(() => { if (item.parentNode) item.parentNode.removeChild(item); if (host && !host.childElementCount && host.parentNode) { host.parentNode.removeChild(host); } }, 180); } function showToast(message, { duration = 1800, anchor = null } = {}) { if (!document?.body) return; const anchorEl = toastAnchorElement(anchor); if (anchorEl) { const item = document.createElement("div"); item.className = "copy-toast copy-toast-anchored"; item.dataset.toastId = `copy-toast-${++toastSeq}`; item.textContent = String(message || ""); document.body.appendChild(item); positionToastNear(item, anchorEl); window.setTimeout(() => removeToast(item), duration); return; } let host = document.querySelector(".copy-toast-container"); if (!host) { host = document.createElement("div"); host.className = "copy-toast-container"; document.body.appendChild(host); } const item = document.createElement("div"); item.className = "copy-toast"; item.dataset.toastId = `copy-toast-${++toastSeq}`; item.textContent = String(message || ""); host.appendChild(item); window.setTimeout(() => removeToast(item, host), duration); } function copyTextFallback(text) { const ta = document.createElement("textarea"); ta.value = text; ta.setAttribute("readonly", ""); ta.style.position = "fixed"; ta.style.top = "0"; ta.style.left = "-9999px"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.focus(); ta.select(); let ok = false; try { ok = document.execCommand("copy"); } finally { document.body.removeChild(ta); } return ok; } async function copyText(text, { successMessage = "Copied", failureMessage = "Copy failed, please copy manually", toast = true, anchor = null, } = {}) { const value = String(text ?? ""); let ok = false; try { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(value); ok = true; } } catch { ok = false; } if (!ok) { try { ok = copyTextFallback(value); } catch { ok = false; } } if (toast) showToast(ok ? successMessage : failureMessage, { anchor }); return ok; } window.showToast = showToast; window.copyText = copyText; // --------------------------------------------------------------------- // Low-level fetch wrapper // --------------------------------------------------------------------- // Auth: browser calls must carry a bearer token. EventSource cannot send // headers, so only the run-events SSE path receives the same token in query. function _authToken() { try { return localStorage.getItem("baizhi.auth.token"); } catch { return null; } } function _eventStreamAuthQuery() { const tok = _authToken(); if (tok) return `token=${encodeURIComponent(tok)}`; _redirectToLogin(); return "token="; } function _redirectToLogin() { try { localStorage.removeItem("baizhi.auth.token"); localStorage.removeItem("baizhi.auth.user_id"); } catch {} if (!location.pathname.startsWith("/login")) location.replace("/login"); } function createRequestId() { if (window.crypto?.randomUUID) return window.crypto.randomUUID(); return `rid-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`; } function requestHeaders(extra = {}) { return { ...extra, "X-Request-ID": createRequestId() }; } async function call(path, { method = "GET", body, form, accept } = {}) { const tok = _authToken(); if (!tok) { _redirectToLogin(); throw new Error("authentication required"); } const headers = requestHeaders({ "Authorization": "Bearer " + tok }); if (body !== undefined) headers["Content-Type"] = "application/json"; if (accept) headers["Accept"] = accept; const res = await fetch(path, { method, headers, body: form !== undefined ? form : (body !== undefined ? JSON.stringify(body) : undefined), }); if (!res.ok) { let detail; const ct = res.headers.get("content-type") || ""; try { detail = ct.includes("application/json") ? await res.json() : await res.text(); } catch { detail = res.statusText || `${res.status}`; } const msg = typeof detail === "object" && detail?.detail?.message ? detail.detail.message : (typeof detail === "string" ? detail : JSON.stringify(detail)); if (res.status === 401) _redirectToLogin(); const err = new Error(`${method} ${path} → ${res.status}: ${msg}`); err.status = res.status; err.detail = detail; throw err; } if (res.status === 204) return null; const ct = res.headers.get("content-type") || ""; if (ct.includes("application/json")) return await res.json(); return await res.text(); } // --------------------------------------------------------------------- // Avatar color / initials helpers (UI-side decoration for backend records // that don't carry display metadata) // --------------------------------------------------------------------- const PALETTE = ["#10a37f", "#2563eb", "#c026d3", "#d97706", "#ef4444", "#0ea5e9", "#7c3aed", "#0d0d0d"]; function hashColor(name) { let h = 0; for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) | 0; return PALETTE[Math.abs(h) % PALETTE.length]; } function initialsFor(name) { if (!name) return "?"; const cleaned = name.replace(/[_\-]/g, " ").trim(); const parts = cleaned.split(/\s+/); if (parts.length === 1) { // For snake/kebab-case skill names, take the first alphanumeric char. return cleaned.slice(0, 1).toUpperCase(); } return (parts[0][0] + parts[1][0]).toUpperCase(); } // --------------------------------------------------------------------- // Skills / Agents / MCP — list + CRUD // --------------------------------------------------------------------- async function listSkills({ scope, runnable = false } = {}) { const params = new URLSearchParams(); if (scope) params.set("scope", scope); if (runnable) params.set("runnable", "true"); const qs = params.toString(); const path = qs ? `/v1/skills?${qs}` : "/v1/skills"; const { items } = await call(path); return items.map(decorateSkill); } function decorateSkill(s) { return { id: s.skill_id, skill_id: s.skill_id, name: s.name, description: s.description || "", desc: s.description || "", visibility: s.visibility, owner_type: s.owner_type, owner_user_id: s.owner_user_id, editable: s.editable, latest_version: s.latest_version, installed_version: s.installed_version, version: s.installed_version || s.latest_version || "draft", enabled: s.enabled, draft_status: s.draft_status, forked_from: s.forked_from, marketplace_template_id: s.marketplace_template_id, marketplace_visibility: s.marketplace_visibility, color: hashColor(s.name), initials: initialsFor(s.name), // The Manager skill editor expects a file tree. Backend only stores // SKILL.md text on personal-skill drafts; multi-file editing is a // future feature. We synthesize a one-file tree so the editor still // renders without crashing. files: [{ path: "SKILL.md", required: true, content: "" }], }; } async function installSkill(skillId, version) { return await call(`/v1/user/skills/${skillId}/install`, { method: "POST", body: { version }, }); } async function installSkillForAgent(agentId, templateId, version) { return await call(`/v1/agents/${agentId}/skills/install/${templateId}`, { method: "POST", body: { version }, }); } // PR-S.6c:UI install 前先打这个,拿"会不会 shadow 别的同名 skill"信息。 // 返 { template_name, collisions: [{ name, source }], already_installed }。 async function preflightInstallSkill(templateId) { return await call(`/v1/marketplace/skills/${templateId}/preflight-install`, { method: "POST", }); } // PR-S.7c MCP install API。 // preflight 返 { template_name, collisions, already_installed, config_schema }。 // config_schema 是 [{name, type:"string"|"secret", description, required, default}] // —— UI 据此渲染 env+secret 表单。 async function preflightInstallMcp(templateId) { return await call(`/v1/marketplace/mcps/${templateId}/preflight-install`, { method: "POST", }); } // env 是非 secret 明文,secrets 走 envelope-encrypted 表(后端 mcp_secrets)。 async function installMcp(templateId, { env, secrets } = {}) { return await call(`/v1/user/mcps/${templateId}/install`, { method: "POST", body: { env: env || {}, secrets: secrets || {} }, }); } async function uninstallMcp(instanceId) { return await call(`/v1/user/mcps/${instanceId}`, { method: "DELETE" }); } // D2=B:agent 也是 installer subject,可填 secrets;所有跑该 agent 的 user 共享。 async function installMcpForAgent(agentId, templateId, { env, secrets } = {}) { return await call(`/v1/agents/${agentId}/mcps/install/${templateId}`, { method: "POST", body: { env: env || {}, secrets: secrets || {} }, }); } async function createPersonalSkill(name, skill_md, other_files = undefined) { return await call("/v1/personal-skills", { method: "POST", body: { name, skill_md, ...(other_files != null ? { other_files } : {}) }, }); } // Upload a ZIP containing SKILL.md (+ optional scripts/references/assets) // and create a personal skill from it in one round-trip. Backend // validates structure, rejects path traversal / binary / oversize files. async function uploadPersonalSkillZip(file, { name } = {}) { const form = new FormData(); form.append("file", file, file.name); if (name) form.append("name", name); const tok = _authToken(); if (!tok) { _redirectToLogin(); throw new Error("authentication required"); } const resp = await fetch("/v1/personal-skills/upload", { method: "POST", headers: requestHeaders({ "Authorization": "Bearer " + tok }), // DON'T set Content-Type — browser must set boundary body: form, }); if (!resp.ok) { if (resp.status === 401) _redirectToLogin(); let detail; try { detail = (await resp.json())?.detail || (await resp.text()); } catch { detail = resp.statusText; } throw new Error(typeof detail === "string" ? detail : detail.message || JSON.stringify(detail)); } return await resp.json(); } // PR-F.1: import a skill from a remote URL (direct .zip or GitHub /tree/). // Backend (POST /v1/personal-skills/import-url) does the fetch + the same // unpack/validate pipeline as the upload endpoint. Errors come back as 400 // with a user-readable `detail.message` we surface in the modal. async function importPersonalSkillFromUrl(url, { name } = {}) { // `call` JSON.stringifies the body itself — pass the raw object. return await call("/v1/personal-skills/import-url", { method: "POST", body: { url, ...(name ? { name } : {}) }, }); } // PR-S.1: marketplace endpoints. `listSkills` keeps returning the union // (mine ∪ public) for back-compat; these two specifically return only // marketplace items so the Manage UI can render a clean "browse + install" // view without filtering on the frontend. function decorateMarketplaceSkill(s) { return { ...s, id: s.skill_id, desc: s.description || "", color: hashColor(s.name), initials: initialsFor(s.name), }; } async function listMarketplaceSkillsPage({ cursor = null, limit = 50, q = "", sort = "name", installedOnly = false, } = {}) { const params = new URLSearchParams(); params.set("limit", String(limit)); if (cursor !== null && cursor !== undefined) params.set("cursor", String(cursor)); if (q && q.trim()) params.set("q", q.trim()); if (sort) params.set("sort", sort); if (installedOnly) params.set("installed_only", "true"); const resp = await call(`/v1/marketplace/skills?${params.toString()}`); const items = Array.isArray(resp.items) ? resp.items.map(decorateMarketplaceSkill) : []; return { items, next_cursor: resp.next_cursor || null, has_more: !!resp.has_more, total_count: Number.isFinite(Number(resp.total_count)) ? Number(resp.total_count) : items.length, }; } async function listMarketplaceSkills() { const raw = []; let cursor = null; do { const qs = cursor ? `?limit=200&cursor=${encodeURIComponent(cursor)}` : "?limit=200"; const { items, next_cursor } = await call(`/v1/marketplace/skills${qs}`); if (Array.isArray(items)) raw.push(...items); cursor = next_cursor || null; } while (cursor); // shallow decorate so the chip / list components reuse hashColor + initials return raw.map(decorateMarketplaceSkill); } async function listMarketplaceMcps() { const { items } = await call("/v1/marketplace/mcps"); return items.map((m) => ({ ...m, id: m.server_id, color: hashColor(m.name), initials: initialsFor(m.name), })); } async function getPersonalSkillDraft(skillId) { return await call(`/v1/personal-skills/${skillId}/draft`); } async function updatePersonalSkillDraft(skillId, skill_md, other_files = undefined) { return await call(`/v1/personal-skills/${skillId}/draft`, { method: "PATCH", body: { skill_md, ...(other_files != null ? { other_files } : {}) }, }); } async function startPersonalSkillTest(skillId) { return await call(`/v1/personal-skills/${skillId}/test`, { method: "POST" }); } async function getPersonalSkillTest(skillId) { return await call(`/v1/personal-skills/${skillId}/test`); } async function applyPersonalSkillTestProposal(skillId, proposalId) { return await call(`/v1/personal-skills/${skillId}/test/proposals/${proposalId}/apply`, { method: "POST", }); } async function publishPersonalSkill(skillId, version, install = true) { return await call(`/v1/personal-skills/${skillId}/publish`, { method: "POST", body: { version, install }, }); } async function publishPersonalSkillToMarketplace(skillId) { return await call(`/v1/personal-skills/${skillId}/marketplace`, { method: "POST", }); } async function withdrawPersonalSkillFromMarketplace(skillId) { return await call(`/v1/personal-skills/${skillId}/marketplace`, { method: "DELETE", }); } async function deletePersonalSkill(skillId) { return await call(`/v1/personal-skills/${skillId}`, { method: "DELETE" }); } async function listAgents() { const { items } = await call("/v1/agents"); return items.map(decorateAgent); } // Apps management async function listApps({ agentId } = {}) { const query = agentId ? `?agent_id=${encodeURIComponent(agentId)}` : ""; const { items } = await call(`/v1/apps${query}`); return items || []; } async function createApp(payload) { return await call("/v1/apps", { method: "POST", body: payload }); } async function getApp(appId) { return await call(`/v1/apps/${encodeURIComponent(appId)}`); } async function replaceAppExecutor(appId, payload) { return await call(`/v1/apps/${encodeURIComponent(appId)}/executor`, { method: "POST", body: payload }); } async function publishAppManifest(appId, manifest) { return await call(`/v1/apps/${encodeURIComponent(appId)}/manifest`, { method: "POST", body: { manifest } }); } async function createH5Release(appId) { return await call(`/v1/apps/${encodeURIComponent(appId)}/h5/releases`, { method: "POST" }); } async function listH5Releases(appId) { const { items } = await call(`/v1/apps/${encodeURIComponent(appId)}/h5/releases`); return items || []; } async function uploadH5Release(appId, releaseId, file) { const form = new FormData(); form.append("file", file); return await call(`/v1/apps/${encodeURIComponent(appId)}/h5/releases/${encodeURIComponent(releaseId)}/upload`, { method: "POST", form }); } async function publishH5Release(appId, releaseId) { return await call(`/v1/apps/${encodeURIComponent(appId)}/h5/releases/${encodeURIComponent(releaseId)}/publish`, { method: "POST" }); } function previewH5Release(appId, releaseId, entryFile, previewUrl) { return previewUrl || `/v1/apps/${encodeURIComponent(appId)}/h5/releases/${encodeURIComponent(releaseId)}/preview/${encodeURIComponent(entryFile || "index.html")}`; } async function rollbackH5Release(appId, releaseId) { return await call(`/v1/apps/${encodeURIComponent(appId)}/h5/releases/${encodeURIComponent(releaseId)}/rollback`, { method: "POST" }); } async function inspectAppSchema(appId) { return await call(`/v1/apps/${encodeURIComponent(appId)}/schema`); } async function listAppAudit(appId) { const { items } = await call(`/v1/apps/${encodeURIComponent(appId)}/audit`); return items || []; } async function grantAppPermission(appId, payload) { return await call(`/v1/apps/${encodeURIComponent(appId)}/permissions`, { method: "POST", body: payload }); } async function listAppTables(appId) { const { items } = await call(`/v1/apps/${encodeURIComponent(appId)}/tables`); return items || []; } async function listAppRecordPage(appId, resource, { page = 1, pageSize = 50, search = "" } = {}) { const params = new URLSearchParams(); params.set("page", String(page)); params.set("page_size", String(pageSize)); params.set("search", search); return await call(`/v1/apps/${encodeURIComponent(appId)}/tables/${encodeURIComponent(resource)}/records?${params.toString()}`); } async function listAppRecords(appId, resource) { const { items } = await call(`/v1/apps/${encodeURIComponent(appId)}/records/${encodeURIComponent(resource)}`); return items || []; } async function createAppRecord(appId, resource, data) { return await call(`/v1/apps/${encodeURIComponent(appId)}/records/${encodeURIComponent(resource)}`, { method: "POST", body: { data } }); } // Agent configs store skill instance IDs directly. function decorateAgent(a) { const ui = JSON.parse(localStorage.getItem(`baizhi.agent.${a.agent_id}.ui`) || "{}"); const modelConfig = a.model_config || {}; return { id: a.agent_id, agent_id: a.agent_id, name: a.name, desc: ui.desc || "", color: ui.color || hashColor(a.agent_id), initials: ui.initials || initialsFor(a.name), modelProvider: modelConfig.provider || "baizhi", model: modelConfig.model_name || "deepseek-v4-flash", temperature: modelConfig.temperature ?? 0.7, top_p: modelConfig.top_p ?? 1, max_tokens: modelConfig.max_tokens ?? 12000, reasoning_enabled: modelConfig.reasoning_enabled ?? false, reasoning_visible: modelConfig.reasoning_visible ?? false, system: a.system_prompt || "", promptVariables: a.prompt_variables || [], maxAgentRounds: a.max_agent_rounds ?? null, skillIds: a.skill_ids || [], enabledSkills: a.skill_ids || [], skills: (a.skills || []).map((s) => ({ ...s, id: s.skill_id || s.id, desc: s.description || "", color: hashColor(s.name || s.skill_id || s.id), initials: initialsFor(s.name || s.skill_id || s.id), })), enabledMCPs: a.mcp_ids || [], localToolNames: a.local_tool_names || [], enabledLocalTools: a.local_tool_names || [], created_at: a.created_at, updated_at: a.updated_at, }; } function persistAgentUi(agent) { const uiOnly = { desc: agent.desc, color: agent.color, initials: agent.initials, }; localStorage.setItem(`baizhi.agent.${agent.id}.ui`, JSON.stringify(uiOnly)); } function modelConfigFromAgentUi(agent = {}) { const provider = agent.modelProvider || "baizhi"; const providerConfig = (window.MODEL_PROVIDERS || []).find((item) => item.id === provider); return { provider, model_name: agent.model || providerConfig?.default_model || "deepseek-v4-flash", temperature: agent.temperature ?? 0.7, top_p: agent.top_p ?? 1, max_tokens: agent.max_tokens ?? 12000, reasoning_enabled: agent.reasoning_enabled ?? false, reasoning_visible: agent.reasoning_visible ?? false, }; } async function createAgent({ name, system_prompt = "", prompt_variables = [], max_agent_rounds = null, enabled_mcp_servers = [], local_tool_names = [], skill_ids = [], mcp_ids, model_config = modelConfigFromAgentUi() } = {}) { const sids = skill_ids || []; const mids = mcp_ids !== undefined ? mcp_ids : enabled_mcp_servers; const created = await call("/v1/agents", { method: "POST", body: { name, system_prompt, prompt_variables, max_agent_rounds, skill_ids: sids, mcp_ids: mids, local_tool_names, model_config }, }); return decorateAgent(created); } async function updateAgent(agentId, { name, system_prompt = "", prompt_variables = [], max_agent_rounds = null, enabled_mcp_servers = [], local_tool_names = [], skill_ids = [], mcp_ids, model_config } = {}) { const sids = skill_ids || []; const mids = mcp_ids !== undefined ? mcp_ids : enabled_mcp_servers; const body = { name, system_prompt, prompt_variables, max_agent_rounds, skill_ids: sids, mcp_ids: mids, local_tool_names }; if (model_config !== undefined) body.model_config = model_config; const updated = await call(`/v1/agents/${agentId}`, { method: "PATCH", body, }); return decorateAgent(updated); } // ---- User bindings(agents-global)---- // PR-S.1e:从 getTenantBindings / updateTenantBindings 改名。后端路径 // /v1/user/bindings 早就 user 化了,函数名跟齐。 async function getUserBindings() { return await call("/v1/user/bindings"); } async function updateUserBindings({ skill_ids, mcp_ids } = {}) { return await call("/v1/user/bindings", { method: "PATCH", body: { skill_ids, mcp_ids }, }); } async function deleteAgent(agentId) { return await call(`/v1/agents/${agentId}`, { method: "DELETE" }); } // LLM 目录(后端单一真相):{ items:[{id,label,context_window}], default, max_output_tokens }。 // app 挂载时 fetch,覆盖 window.MODELS,让 Playground/Manager 显示真实窗口。 async function listModels() { return await call("/v1/models"); } // Traces — list summaries + detail (run + events together). async function listTraces({ agent_id, status, q, scope, limit = 200 } = {}) { const params = new URLSearchParams(); if (agent_id) params.set("agent_id", agent_id); if (status) params.set("status", status); if (q) params.set("q", q); if (scope) params.set("scope", scope); if (limit) params.set("limit", String(limit)); const qs = params.toString(); const { items } = await call(`/v1/traces${qs ? "?" + qs : ""}`); return items; } async function getTraceDetail(runId) { return await call(`/v1/traces/${runId}`); } async function getTraceEvents(runId, { include_model_events = false } = {}) { const params = new URLSearchParams(); if (include_model_events) params.set("include_model_events", "true"); const qs = params.toString(); const { items } = await call(`/v1/traces/${runId}/events${qs ? "?" + qs : ""}`); return items || []; } async function getTraceModelIO(runId) { const { items } = await call(`/v1/traces/${runId}/model-io`); return items || []; } async function downloadTraceModelIO(runId) { const tok = _authToken(); if (!tok) { _redirectToLogin(); throw new Error("authentication required"); } const response = await fetch(`/v1/traces/${encodeURIComponent(runId)}/model-io-export`, { headers: requestHeaders({ "Authorization": "Bearer " + tok }), }); if (!response.ok) { const detail = await response.text(); if (response.status === 401) _redirectToLogin(); throw new Error(`GET trace Model IO export → ${response.status}: ${detail || response.statusText}`); } const blob = await response.blob(); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `trace-${runId}-model-io.zip`; document.body.appendChild(link); link.click(); link.remove(); window.setTimeout(() => URL.revokeObjectURL(url), 0); } async function replayTrace(runId, { thread_id } = {}) { return await call(`/v1/traces/${runId}/replay`, { method: "POST", body: thread_id ? { thread_id } : {}, }); } async function listThreadMessages(threadId) { const { items } = await call(`/v1/threads/${encodeURIComponent(threadId)}/messages`); return items; } async function listThreadRuns(threadId, limit = 100) { const result = await call( `/v1/threads/${encodeURIComponent(threadId)}/runs?limit=${encodeURIComponent(limit)}` ); return result.items || []; } // PR C: resolve a presigned download URL for a run output asset // (PPT / DOCX / arbitrary files written by run_script). // chat-panel.jsx's AssetDownloadChip calls this on click. async function createAssetDownloadUrl(assetId) { // 后端返 { download_url, filename, mime_type, expires_at };chip 读 { url } — // 在边界归一化成 url(presigned,可直接 window.open)。 const r = await call(`/v1/assets/${assetId}/download-url`, { method: "POST" }); return { url: r.download_url, filename: r.filename, mime_type: r.mime_type, expires_at: r.expires_at }; } // ----- Evals: datasets + cases + eval_runs (PR e1 endpoints, PR e2 wires // them in). EvalRunner / graders are PR e3 / e4 — runs // created here stay `status: "pending"` until then. async function listDatasets() { const { items } = await call("/v1/datasets"); return items; } async function createDataset({ name, description = "", tags = [], graders = [] } = {}) { return await call("/v1/datasets", { method: "POST", body: { name, description, tags, graders }, }); } async function getDataset(datasetId) { return await call(`/v1/datasets/${datasetId}`); } async function updateDataset(datasetId, patch) { return await call(`/v1/datasets/${datasetId}`, { method: "PATCH", body: patch }); } async function deleteDataset(datasetId) { return await call(`/v1/datasets/${datasetId}`, { method: "DELETE" }); } async function addDatasetCase(datasetId, { input, expected, tools = [], tags = [], source = { type: "manual" } } = {}) { return await call(`/v1/datasets/${datasetId}/cases`, { method: "POST", body: { input, expected, tools, tags, source }, }); } async function deleteDatasetCase(datasetId, caseId) { return await call(`/v1/datasets/${datasetId}/cases/${caseId}`, { method: "DELETE" }); } async function listEvalRuns({ dataset_id, limit = 200 } = {}) { const params = new URLSearchParams(); if (dataset_id) params.set("dataset_id", dataset_id); if (limit) params.set("limit", String(limit)); const qs = params.toString(); const { items } = await call(`/v1/eval_runs${qs ? "?" + qs : ""}`); return items; } async function getEvalRun(evalRunId) { return await call(`/v1/eval_runs/${evalRunId}`); } async function createEvalRun({ dataset_id, agent_id, model } = {}) { return await call("/v1/eval_runs", { method: "POST", body: { dataset_id, agent_id, model }, }); } async function listMcpServers() { const { items } = await call("/v1/mcp/servers"); return items.map(decorateMcp); } async function listLocalTools() { const { items } = await call("/v1/local-tools"); return (items || []).map(decorateLocalTool); } // ---- MCP CRUD(admin)---- // .baizhi-data SQLite `mcp_servers` 表 持久化(MCP store PR,2026-05-26)。 // 真 secret 不进 DB —— `auth_env_key` 只是 env 变量名。 async function createMcpServer({ server_id, name, description = "", transport, http_url, headers_template, auth_env_key, command, args, runtime_options, config_schema } = {}) { const body = { server_id, name, description, transport }; if (http_url) body.http_url = http_url; if (headers_template) body.headers_template = headers_template; if (auth_env_key) body.auth_env_key = auth_env_key; if (command) body.command = command; if (args) body.args = args; if (runtime_options) body.runtime_options = runtime_options; if (config_schema) body.config_schema = config_schema; const created = await call("/v1/mcp/servers", { method: "POST", body }); return decorateMcp({ ...created, scope: "platform" }); } async function updateMcpServer(serverId, patch) { // Translate UI shape → backend shape const body = {}; if ("name" in patch) body.name = patch.name; if ("description" in patch) body.description = patch.description; if ("transport" in patch) body.transport = patch.transport; if ("http_url" in patch || "url" in patch) body.http_url = patch.http_url ?? patch.url; if ("headers_template" in patch) body.headers_template = patch.headers_template; if ("auth_env_key" in patch) body.auth_env_key = patch.auth_env_key; if ("runtime_options" in patch) body.runtime_options = patch.runtime_options; if ("command" in patch) body.command = patch.command; if ("args" in patch) { body.args = Array.isArray(patch.args) ? patch.args : (patch.args || "").split(/\s+/).filter(Boolean); } const updated = await call(`/v1/mcp/servers/${serverId}`, { method: "PATCH", body }); return decorateMcp({ ...updated, scope: "platform" }); } async function deleteMcpServer(serverId) { return await call(`/v1/mcp/servers/${serverId}`, { method: "DELETE" }); } async function checkMcpServer(serverId) { return await call(`/v1/mcp/servers/${serverId}/check`, { method: "POST" }); } function decorateMcp(s) { // Backend wire shape(`application._available_mcp_servers`): // { server_id, name, description, transport, // http_url, headers_template, auth_env_key, ← HTTP transport // command, args, ← stdio transport // available, scope } // 之前这个 mapper 把 `http_url` / `headers_template` 错读成 `url` / `env`, // 导致 manage 页 WebSearch 详情参数全空 —— bug fix(2026-05-26)。 const headers = s.headers_template || {}; const headerPairs = Object.entries(headers).map(([k, v]) => ({ k, v })); return { id: s.server_id, server_id: s.server_id, name: s.name, description: s.description || "", desc: s.description || "", transport: s.transport || "stdio", // stdio fields command: s.command || "", args: Array.isArray(s.args) ? s.args.join(" ") : (s.args || ""), // http fields url: s.http_url || s.url || "", headers: headerPairs, // [{k, v}] 形态,跟 KV 表格直接绑 auth_env_key: s.auth_env_key || "", // PR-S.18a · install-time form schema(admin add 声明的 user-fill 字段) config_schema: Array.isArray(s.config_schema) ? s.config_schema : [], runtime_options: s.runtime_options || {}, // tools / metadata tools: s.tools || [], configStatus: s.config_status || s.configStatus || null, checkStatus: s.check_status || s.checkStatus || null, available: !!s.available, enabled: !!s.available, // backward-compat alias status: s.available ? "available" : "unavailable", scope: s.scope || "platform", color: hashColor(s.server_id), initials: initialsFor(s.name), }; } function decorateLocalTool(t) { return { ...t, id: t.id || t.name, name: t.name || t.id, label: t.label || t.name || t.id, description: t.description || "", desc: t.description || "", group: t.group || "Local", color: hashColor(t.group || t.name || t.id), initials: initialsFor(t.label || t.name || t.id), type: "local_tool", }; } // --------------------------------------------------------------------- // Run lifecycle — create + poll until done, translating backend events // --------------------------------------------------------------------- // PR pg-system-prompt-preview: fetch the runtime SYSTEM_PRELUDE so the // Observability panel can show "what the LLM sees" (block 1). Block 2 // (activated skills) is composed on the client from sessionSkills + // their descriptions (already in window.SKILLS). Block 3 (runtime facts) // and block 4 (file attachments) intentionally omitted from preview — // they're per-run, not stable for the agent. async function getSystemPromptPrelude() { return await call("/v1/system-prompt-prelude"); } async function createRun({ thread_id, message, agent_id, skill_ids = [], mcp_ids = null, local_tool_names = [], model_config = {}, sandbox_type, attachments = [], context = {}, restrict_to_agent_tools = false }) { const runOptions = { agent_id, skill_ids, mcp_ids, local_tool_names, model_config, sandbox_type, attachments, restrict_to_agent_tools }; if (context && Object.keys(context).length) runOptions.context = context; return await call("/v1/agent/runs", { method: "POST", body: { thread_id, message, // attachments: list of asset_ids from prior uploadAttachment calls. // Backend resolves each → InputAttachment + materializes under // workspace/input/ before the LLM loop. options: runOptions, }, }); } // PR chat-uploads-workspace: multipart upload of one chat attachment. // Returns {asset_id, filename, mime_type, size}. Use XHR (not fetch) so // we can surface upload progress for >MB files. Server cap default 25 MB // (BAIZHI_UPLOAD_MAX_BYTES); MIME whitelist enforced server-side. function uploadAttachment(file, { onProgress } = {}) { const tok = _authToken(); if (!tok) { _redirectToLogin(); return Promise.reject(new Error("authentication required")); } return new Promise((resolve, reject) => { const form = new FormData(); form.append("file", file, file.name || "attachment.bin"); const xhr = new XMLHttpRequest(); xhr.open("POST", "/v1/uploads"); xhr.setRequestHeader("Authorization", "Bearer " + tok); xhr.setRequestHeader("X-Request-ID", createRequestId()); if (typeof onProgress === "function" && xhr.upload) { xhr.upload.addEventListener("progress", (e) => { if (e.lengthComputable) onProgress(e.loaded / e.total); }); } xhr.onload = () => { let body = null; try { body = JSON.parse(xhr.responseText); } catch (_) { body = null; } if (xhr.status >= 200 && xhr.status < 300) { resolve(body); } else { if (xhr.status === 401) _redirectToLogin(); const msg = (body && body.detail && body.detail.message) || (body && body.detail) || `upload failed (HTTP ${xhr.status})`; reject(new Error(typeof msg === "string" ? msg : JSON.stringify(msg))); } }; xhr.onerror = () => reject(new Error("upload network error")); xhr.send(form); }); } async function getRun(runId) { return await call(`/v1/runs/${runId}`); } async function listRunEvents(runId) { const { items } = await call(`/v1/runs/${runId}/events`); return items; } async function listWorkspaceFiles(runId) { return await call(`/v1/runs/${encodeURIComponent(runId)}/workspace/files`); } async function readWorkspaceMarkdown(runId, path) { return await call( `/v1/runs/${encodeURIComponent(runId)}/workspace/markdown?path=${encodeURIComponent(path)}` ); } async function downloadWorkspaceFile(runId, path) { const tok = _authToken(); if (!tok) { _redirectToLogin(); throw new Error("authentication required"); } const encodedRunId = encodeURIComponent(runId); const encodedPath = encodeURIComponent(path); const downloadEndpoint = `/v1/runs/${encodedRunId}/workspace/download?path=${encodedPath}`; const target = await call(downloadEndpoint, { method: "POST" }); const link = document.createElement("a"); link.href = target.download_url; link.rel = "noopener noreferrer"; document.body.appendChild(link); link.click(); link.remove(); } async function listActiveRuns() { return call("/v1/runs/active"); } async function cancelRun(runId, reason = "user_abort") { return await call(`/v1/runs/${runId}/cancel`, { method: "POST", body: { reason } }); } // HITL (docs/HIL-design.md): answer an `awaiting_input` run. `values` maps // the human_request field names → the human's answers; the backend feeds // them back as the pending ask_human FunctionResponse and re-enqueues the // SAME run_id to continue. Returns the re-queued run dict. async function respondToRun(runId, values, options = {}) { return await call(`/v1/runs/${runId}/respond`, { method: "POST", body: { values: values || {}, decision: options.decision, modified_args: options.modified_args, rejection_reason: options.rejection_reason, interruption_id: options.interruption_id, }, }); } // Drive one agent run: POST, then poll until terminal status, dispatching // each new backend event through the provided callbacks. The callbacks // are designed to match what app.jsx's executeRun was building up by // hand against the mock — so the React components downstream don't need // to change shape. // // Callbacks (all optional): // onQueued({run}) — run accepted, status "queued" // onStarted({run}) — run picked up by worker // onLlmStarted({roundId, roundIndex}) // onLlmCompleted({roundId, roundIndex, duration_ms, input_tokens, output_tokens, finish_reason}) // onModelIOStarted(call) — full model input for the round // onModelIOCompleted(call) — output/usage terminal delta // onModelIOFailed(call) — error terminal delta // onThinkingStarted({thinking_id, llm_round_id}) // onThinkingDelta({thinking_id, delta}) // onThinkingCompleted({thinking_id, text}) // onSkillActivated({skill_name}) // onToolStarted({tool_name, tool_call_id, arguments}) // onToolCompleted({tool_name, tool_call_id, duration_ms, is_error, result_bytes, result_text}) // onTextDelta({delta}) — incremental assistant text // onAgentEvent({type, payload}) — raw passthrough for every backend event // onCompleted({run, events}) — terminal: completed / failed / cancelled // onError(error) // // Returns a {cancel()} handle. function startAgentRun({ thread_id, message, agent_id, skill_ids = [], mcp_ids = null, local_tool_names = [], model_config = {}, sandbox_type, attachments = [], context = {}, restrict_to_agent_tools = false, callbacks = {}, pollIntervalMs = 250, timeoutMs = 600000, transport = "auto" }) { // PR live-sse-events: default transport is "auto" — try EventSource // (real SSE, no client-side timeout, server pushes events live as // they happen). Fall back to polling if EventSource isn't supported // OR errors during connect. transport="poll" forces the old path // (handy for debugging). transport="sse" forces SSE without fallback. // // PR HITL-2-ui: a run can PAUSE mid-flight (`awaiting_input`) when the // agent calls `ask_human` — the run exits its worker thread and waits for // a human to answer (docs/HIL-design.md). The driver models this as a loop // of "segments": stream one segment until the run pauses or terminates; // on pause, hand `run.human_request` to the UI via onAwaitingInput and // await the human's answer, POST it to /respond, then attach a fresh // segment to the SAME run_id to stream the resumed work. Cross-segment // event dedup (by event_id) keeps the replayed pre-pause events from // double-firing the callbacks. let cancelled = false; let runId = null; let activeEventSource = null; let abortSegment = null; let abortAwait = null; // rejects the human-input wait if the user cancels mid-pause let rejectCancelled = null; let cancelledNotified = false; const seenEventIds = new Set(); const deliveredEvents = []; let sawAgUiTextContent = false; const agUiToolStarts = new Map(); const agUiToolStarted = new Set(); const agUiToolResults = new Set(); const cancelledError = () => { const err = new Error("cancelled"); err.name = "AbortError"; return err; }; const isCancelledError = (err) => ( cancelled || err?.message === "cancelled" || err?.name === "AbortError" ); const notifyCancelled = () => { if (cancelledNotified) return; cancelledNotified = true; callbacks.onCancelled?.({ run_id: runId, events: deliveredEvents }); }; const dispatchEvent = (evt) => { // Dedup across segments: a resumed segment re-lists/replays events the // first segment already dispatched. event_id is stable + present on // every backend event (see api.py _event / SSE `id:` line). const eid = evt && evt.event_id; if (eid !== undefined && eid !== null) { if (seenEventIds.has(eid)) return; seenEventIds.add(eid); } deliveredEvents.push(evt); const t = evt.type; const p = evt.payload || {}; callbacks.onAgentEvent?.(evt); if (t === "TEXT_MESSAGE_CONTENT") { sawAgUiTextContent = true; callbacks.onTextDelta?.({ delta: p.delta || "", messageId: p.messageId }); return; } if (t === "TOOL_CALL_START") { const toolCallId = p.toolCallId; if (toolCallId) { agUiToolStarted.add(toolCallId); agUiToolStarts.set(toolCallId, { tool_name: p.toolCallName, tool_call_id: toolCallId, arguments: {}, llm_round_id: p.parentMessageId, }); } return; } if (t === "TOOL_CALL_ARGS") { const toolCallId = p.toolCallId; const started = agUiToolStarts.get(toolCallId); if (started && !started.dispatched) { let args = {}; try { args = JSON.parse(p.delta || "{}"); } catch {} started.arguments = args; started.dispatched = true; callbacks.onToolStarted?.(started); } return; } if (t === "TOOL_CALL_RESULT") { const toolCallId = p.toolCallId; if (toolCallId) agUiToolResults.add(toolCallId); const content = (p.content && typeof p.content === "object") ? p.content : {}; const message = content.result ?? p.message ?? ""; const isError = content.is_error ?? p.isError ?? false; callbacks.onToolCompleted?.({ tool_name: p.toolCallName, tool_call_id: toolCallId, duration_ms: content.duration_ms || p.durationMs || 0, is_error: !!isError, result_bytes: content.result_bytes || new Blob([message]).size, result_text: message, }); return; } if (t === "RUN_STARTED") { callbacks.onStarted?.({ run: { run_id: p.runId || runId, status: "running" } }); return; } if (t === "RUN_ERROR") { return; } if (t === "RUN_FINISHED") { return; } if (t === "REASONING_START") { callbacks.onThinkingStarted?.({ thinking_id: p.messageId, llm_round_id: p.parentMessageId }); return; } if (t === "REASONING_MESSAGE_CONTENT") { callbacks.onThinkingDelta?.({ thinking_id: p.messageId, delta: p.delta || "" }); return; } if (t === "REASONING_END") { callbacks.onThinkingCompleted?.({ thinking_id: p.messageId, text: p.text || "" }); return; } if (t === "run.awaiting_input" && (p.response_route === "tool_approval" || p.interruption_type === "tool_approval")) { callbacks.onToolApprovalRequired?.({ run_id: runId, payload: p, respond: (decision, detail = {}) => respondToRun(runId, {}, { decision, modified_args: detail.modified_args, rejection_reason: detail.rejection_reason, interruption_id: p.interruption_id, }), }); } if (t === "run.cancelled" || t === "agent.cancelled") { notifyCancelled(); return; } if (t === "llm.started") callbacks.onLlmStarted?.(p); else if (t === "llm.completed") callbacks.onLlmCompleted?.(p); else if (t === "model_io.started") callbacks.onModelIOStarted?.(p); else if (t === "model_io.completed") callbacks.onModelIOCompleted?.(p); else if (t === "model_io.failed") callbacks.onModelIOFailed?.(p); else if (t === "thinking.started") callbacks.onThinkingStarted?.(p); else if (t === "thinking.delta") callbacks.onThinkingDelta?.(p); else if (t === "thinking.completed") callbacks.onThinkingCompleted?.(p); else if (t === "skill.activated") callbacks.onSkillActivated?.(p); else if (t === "tool.started") { if (!agUiToolStarted.has(p.tool_call_id)) callbacks.onToolStarted?.(p); } else if (t === "tool.completed") { if (!agUiToolResults.has(p.tool_call_id)) callbacks.onToolCompleted?.(p); } else if (t === "text.delta") { if (!sawAgUiTextContent) callbacks.onTextDelta?.(p); } }; const driver = (async () => { try { const run = await createRun({ thread_id, message, agent_id, skill_ids, mcp_ids, local_tool_names, model_config, sandbox_type, attachments, context, restrict_to_agent_tools }); runId = run.run_id; if (cancelled) { if (runId) { try { await cancelRun(runId, "user_abort"); } catch {} } throw cancelledError(); } callbacks.onQueued?.({ run }); let segmentRun = run; let firstSegment = true; // Segment loop — re-enters on every HITL resume until the run is // terminal. The human-wait (awaiting onAwaitingInput's respond) sits // BETWEEN segments, so no segment's timeout counts the human's time. while (true) { const useSse = firstSegment && (transport === "sse" || (transport === "auto" && typeof EventSource !== "undefined")); let finalRun; if (useSse) { try { finalRun = await runViaSse(runId, segmentRun, dispatchEvent, callbacks, () => cancelled, (es, abort) => { activeEventSource = es; abortSegment = abort; }); } catch (sseErr) { // SSE failed entirely — only fall back when transport=auto. if (isCancelledError(sseErr)) throw sseErr; if (transport !== "auto") throw sseErr; console.warn("startAgentRun: SSE failed, falling back to polling:", sseErr); activeEventSource = null; finalRun = await runViaPolling(runId, segmentRun, dispatchEvent, callbacks, () => cancelled, pollIntervalMs, timeoutMs); } } else { finalRun = await runViaPolling(runId, segmentRun, dispatchEvent, callbacks, () => cancelled, pollIntervalMs, timeoutMs); } firstSegment = false; if (cancelled) throw new Error("cancelled"); if (["awaiting_input", "interrupted_max_rounds", "interrupted_context_budget"].includes(finalRun.status)) { // HITL pause: surface the form + wait for the human. The `respond` // we hand the UI just RESOLVES this promise; the driver then does // the POST /respond + re-attach below. `cancel` lets the UI abort. const values = await new Promise((resolve, reject) => { if (!callbacks.onAwaitingInput) { reject(new Error(`run ${runId} is resumable but no onAwaitingInput handler was provided`)); return; } abortAwait = reject; // cancel() can unblock this wait callbacks.onAwaitingInput({ run: finalRun, respond: (vals) => resolve(vals || {}), cancel: () => reject(new Error("cancelled")), }); }); abortAwait = null; if (cancelled) throw new Error("cancelled"); await respondToRun(runId, values); segmentRun = await getRun(runId); // status flipped queued/running // Resume always streams via polling — robust to the replayed // history (dedup handles it) without depending on a fresh SSE. continue; } callbacks.onCompleted?.({ run: finalRun, events: deliveredEvents }); return finalRun; } } catch (err) { if (isCancelledError(err)) { notifyCancelled(); } else { callbacks.onError?.(err); } throw err; } finally { if (activeEventSource) { try { activeEventSource.close(); } catch {} } abortSegment = null; } })(); const cancelPromise = new Promise((_, reject) => { rejectCancelled = reject; }); const work = Promise.race([driver, cancelPromise]); return { promise: work, // HITL: the UI submits the human's form answer here. We don't expose the // promise-resolver directly; callers route through onAwaitingInput's // `respond`, which is what actually resolves the segment-loop wait. cancel: () => { if (cancelled) return; cancelled = true; const err = cancelledError(); notifyCancelled(); if (abortAwait) { try { abortAwait(err); } catch {} abortAwait = null; } if (abortSegment) { try { abortSegment(err); } catch {} abortSegment = null; } if (activeEventSource) { try { activeEventSource.close(); } catch {} } if (runId) cancelRun(runId, "user_abort").catch(() => {}); if (rejectCancelled) rejectCancelled(err); }, }; } // SSE path: open EventSource at /v1/runs/{id}/events?stream=1&token=... // EventSource has no API for custom headers, so the backend accepts token // query auth on this path only. // Stream ONE segment over SSE. Resolves with the final run dict when the // stream closes (`done`) — the caller (startAgentRun's segment loop) // decides terminal-vs-awaiting_input from `run.status` and fires // onCompleted / onAwaitingInput. (Pre-HITL this fn owned onCompleted; that // moved up so the resume loop owns the run's terminal contract.) function runViaSse(runId, initialRun, dispatchEvent, callbacks, isCancelled, registerEs) { const url = `/v1/runs/${runId}/events?stream=1&${_eventStreamAuthQuery()}`; let lastStatus = initialRun.status; return new Promise((resolve, reject) => { let es = null; let reconnectTimer = null; let settled = false; const clearReconnect = () => { if (reconnectTimer !== null) { clearTimeout(reconnectTimer); reconnectTimer = null; } }; const closeCurrent = () => { if (!es) return; try { es.close(); } catch {} es = null; }; const settleResolve = (run) => { if (settled) return; settled = true; clearReconnect(); closeCurrent(); resolve(run); }; const settleReject = (err) => { if (settled) return; settled = true; clearReconnect(); closeCurrent(); reject(err); }; function scheduleReconnect() { if (settled || reconnectTimer !== null) return; if (isCancelled()) { settleReject(new Error("cancelled")); return; } reconnectTimer = setTimeout(() => { reconnectTimer = null; openEventSource(); }, 1000); } const refreshRunAfterDisconnect = async () => { if (settled) return; if (isCancelled()) { settleReject(new Error("cancelled")); return; } try { const latest = await getRun(runId); if (latest.status === "queued" || latest.status === "running") { scheduleReconnect(); return; } settleResolve(latest); } catch (err) { settleReject(err); } }; // EventSource doesn't auto-fire onmessage for named events, only // the default "message" event. We bind addEventListener per type // we care about. The backend names every event by its type, so one // generic handler covers the AG-UI stream plus remaining business events. const handleData = (e) => { let evt; try { evt = JSON.parse(e.data); } catch { return; } if (evt.type === "run.started" && lastStatus !== "running") { lastStatus = "running"; callbacks.onStarted?.({ run: { ...initialRun, run_id: runId, status: "running" } }); } dispatchEvent(evt); }; // Bind known event types (incl. HITL pause/resume markers so they flow // through dispatchEvent → onAgentEvent + get recorded for dedup). const KNOWN = [ "run.queued", "skill.activated", "skill.asset.prepared", "session.created", "session.reused", "workspace.prepared", "workspace.files_synced", "workspace.sync_failed", "workspace.snapshot_saved", "sandbox.prepared", "sandbox.released", "llm.started", "llm.completed", "model_io.started", "model_io.completed", "model_io.failed", "ui.card", "asset.created", "run.cancelled", "RUN_STARTED", "RUN_FINISHED", "RUN_ERROR", "TEXT_MESSAGE_START", "TEXT_MESSAGE_CONTENT", "TEXT_MESSAGE_END", "TOOL_CALL_START", "TOOL_CALL_ARGS", "TOOL_CALL_END", "TOOL_CALL_RESULT", "REASONING_START", "REASONING_MESSAGE_START", "REASONING_MESSAGE_CONTENT", "REASONING_MESSAGE_END", "REASONING_END", "agent.skill_tool_required", "agent.max_rounds_hit", "agent.cancelled", "run.awaiting_input", "run.resumed", ]; function openEventSource() { if (settled) return; if (isCancelled()) { settleReject(new Error("cancelled")); return; } closeCurrent(); try { es = new EventSource(url); } catch (err) { settleReject(err); return; } const current = es; registerEs(current, (err) => { settleReject(err || new Error("cancelled")); }); for (const t of KNOWN) current.addEventListener(t, handleData); current.addEventListener("done", () => { refreshRunAfterDisconnect().catch(settleReject); }); current.addEventListener("error", () => { if (settled || current !== es) return; // CONNECTING means native EventSource retry is active. CLOSED means // it will not retry, so verify the run and create a fresh stream if // the backend is still working. if (current.readyState === EventSource.CLOSED) { closeCurrent(); refreshRunAfterDisconnect().catch(settleReject); } }); } openEventSource(); }); } // Polling path — fallback / debug AND the resume path (HITL re-attach). // Stops at terminal OR `awaiting_input` and resolves with the run dict; the // segment loop in startAgentRun owns onCompleted / onAwaitingInput. Events // are fed through dispatchEvent unconditionally — its event_id dedup means a // resumed segment that re-lists the full history won't re-fire callbacks for // events already seen in the first segment. timeoutMs default is 600000 // (10 min) so Office-skill runs have headroom; the human-wait is OUTSIDE // this fn (between segments) so it never counts against the deadline. async function runViaPolling(runId, initialRun, dispatchEvent, callbacks, isCancelled, pollIntervalMs, timeoutMs) { const deadline = Date.now() + timeoutMs; let lastStatus = initialRun.status; while (!isCancelled() && Date.now() < deadline) { const [latest, events] = await Promise.all([getRun(runId), listRunEvents(runId)]); if (latest.status !== lastStatus) { lastStatus = latest.status; if (latest.status === "running") callbacks.onStarted?.({ run: latest }); } for (const ev of events) dispatchEvent(ev); if (["completed", "failed", "cancelled", "timeout", "awaiting_input", "interrupted_max_rounds", "interrupted_context_budget"].includes(latest.status)) { return latest; } await sleep(pollIntervalMs); } if (isCancelled()) { try { await cancelRun(runId, "user_abort"); } catch {} throw new Error("cancelled"); } throw new Error(`run ${runId} did not finish within ${timeoutMs}ms`); } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } // --------------------------------------------------------------------- // Public surface // --------------------------------------------------------------------- // Whether the current request set carries a real auth token. UI uses this // to decide whether to show the Logout affordance. function isAuthenticated() { return !!_authToken(); } // PR-S.2: GET /v1/auth/me returns {user_id, is_admin}. UI uses it to // decide whether to render admin actions (add/edit/delete MCP catalog, // promote-skill-to-public). Server still 403s if the UI is bypassed. async function authMe() { return await call("/v1/auth/me"); } function logout() { _redirectToLogin(); } // ---- Users management(PR-S.17 · admin-only)---- async function listUsers() { const { items } = await call("/v1/users"); return items; } async function updateUser(userId, patch) { // patch: { is_admin?: bool, status?: 'active'|'banned' } return await call(`/v1/users/${userId}`, { method: "PATCH", body: patch }); } window.api = { userId: USER_ID, isAuthenticated, logout, authMe, // skills listSkills, installSkill, installSkillForAgent, preflightInstallSkill, // PR-S.7c MCP install-as-fork wire preflightInstallMcp, installMcp, uninstallMcp, installMcpForAgent, listMarketplaceSkills, listMarketplaceSkillsPage, listMarketplaceMcps, createPersonalSkill, uploadPersonalSkillZip, importPersonalSkillFromUrl, getPersonalSkillDraft, updatePersonalSkillDraft, startPersonalSkillTest, getPersonalSkillTest, applyPersonalSkillTestProposal, publishPersonalSkill, publishPersonalSkillToMarketplace, withdrawPersonalSkillFromMarketplace, deletePersonalSkill, // agents listAgents, createAgent, updateAgent, deleteAgent, // apps listApps, createApp, getApp, replaceAppExecutor, publishAppManifest, createH5Release, listH5Releases, uploadH5Release, publishH5Release, previewH5Release, rollbackH5Release, inspectAppSchema, listAppAudit, grantAppPermission, listAppTables, listAppRecordPage, listAppRecords, createAppRecord, persistAgentUi, modelConfigFromAgentUi, // LLM 目录(后端单一真相;context_window 等) listModels, // user bindings(agents-global PR;user 层 skill+mcp 订阅列表) getUserBindings, updateUserBindings, // mcp catalog(admin CRUD;per-agent enable 走 updateAgent) listMcpServers, createMcpServer, updateMcpServer, deleteMcpServer, checkMcpServer, // users management(PR-S.17 · admin-only) listUsers, updateUser, // local function-call tools(session-only in Playground) listLocalTools, // runs createRun, getRun, listRunEvents, listActiveRuns, cancelRun, respondToRun, uploadAttachment, listWorkspaceFiles, readWorkspaceMarkdown, downloadWorkspaceFile, getSystemPromptPrelude, // traces listTraces, getTraceDetail, getTraceEvents, getTraceModelIO, downloadTraceModelIO, replayTrace, // threads listThreadMessages, listThreadRuns, // assets createAssetDownloadUrl, // evals (datasets + cases + eval_runs; PR e2 wires them in) listDatasets, createDataset, getDataset, updateDataset, deleteDataset, addDatasetCase, deleteDatasetCase, listEvalRuns, getEvalRun, createEvalRun, // helpers hashColor, initialsFor, }; window.startAgentRun = startAgentRun; })();