diff --git a/README.md b/README.md index 31fec3c..c1e93bb 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Served via jsDelivr; fetched automatically during install — you never need to - `arcrun-wait/` — **arcrun-wait**(首裝) - `daemon/` — 桌面 App(Mac/Windows)安裝檔 -Built from `Arcrun@1246d92ce791` by `installer/scripts/ship.mjs`(arcrun-rag repo,release 1.4.49,built 2026-08-18)。 +Built from `Arcrun@86fc731f375d` by `installer/scripts/ship.mjs`(arcrun-rag repo,release 1.4.51,built 2026-08-24)。 ⚠️ 這份檔案由出貨管線每次自動重寫(`installer/scripts/render-bundles-readme.mjs`)—— 不要手動改這裡列的零件清單——它是算出來的:公庫=Arcrun 這一版編了什麼, diff --git a/arcrun-cypher-executor/worker.mjs b/arcrun-cypher-executor/worker.mjs index 6128dea..8039a87 100644 --- a/arcrun-cypher-executor/worker.mjs +++ b/arcrun-cypher-executor/worker.mjs @@ -3264,8 +3264,14 @@ var init_kbdb_proxy = __esm({ const owner = tenant(c); if (!owner) return c.json(NEED_KEY, 401); const { base, headers } = kbdbBase(c.env); + const params = new URLSearchParams(); + params.set("owner_id", owner); + for (const k of ["limit", "offset"]) { + const v = c.req.query(k); + if (v) params.set(k, v); + } const res = await fetch( - `${base}/records/by-template/${encodeURIComponent(c.req.param("template"))}?owner_id=${encodeURIComponent(owner)}`, + `${base}/records/by-template/${encodeURIComponent(c.req.param("template"))}?${params.toString()}`, { headers } ); return new Response(res.body, { status: res.status, headers: { "Content-Type": "application/json" } }); @@ -15038,6 +15044,258 @@ consoleDashboardRouter.post("/console/triage-check", async (c) => { init_dist(); init_kbdb_proxy(); init_webhook_handlers(); + +// cypher-executor/src/lib/app-system.ts +init_kbdb_proxy(); +init_webhook_handlers(); +var ID_RE = /^[a-z][a-z0-9_-]{0,63}$/; +function validateAppDeclaration(raw2) { + const errors = []; + if (!raw2 || typeof raw2 !== "object" || Array.isArray(raw2)) { + return { ok: false, errors: ["\u5BA3\u544A\u5FC5\u9808\u662F\u4E00\u500B JSON \u7269\u4EF6"] }; + } + const d = raw2; + if (typeof d.id !== "string" || !ID_RE.test(d.id)) { + errors.push("id \u5FC5\u586B\uFF0C\u4E14\u53EA\u80FD\u662F\u5C0F\u5BEB\u82F1\u6578\u5B57/\u5E95\u7DDA/\u9023\u5B57\u865F\uFF0C\u958B\u982D\u9808\u70BA\u5B57\u6BCD\uFF08\u226464 \u5B57\uFF09"); + } + if (typeof d.name !== "string" || d.name.trim() === "") { + errors.push("name \u5FC5\u586B\uFF0C\u4E0D\u53EF\u7A7A\u5B57\u4E32"); + } + const hasWorkflows = Array.isArray(d.workflows) && d.workflows.length > 0; + const hasUi = d.ui && typeof d.ui === "object" && typeof d.ui.html === "string" && d.ui.html.trim() !== ""; + if (!hasWorkflows && !hasUi) { + errors.push("workflows \u6216 ui \u81F3\u5C11\u8981\u6709\u4E00\u500B\uFF08\u6C92\u6709\u756B\u9762\u4E5F\u6C92\u6709\u5DE5\u4F5C\u6D41\u7684 App \u4E0D\u6210\u7ACB\uFF0Cdesign \xA7\u4E8C\uFF09"); + } + if (d.workflows !== void 0) { + if (!Array.isArray(d.workflows)) { + errors.push("workflows \u5FC5\u9808\u662F\u9663\u5217"); + } else { + d.workflows.forEach((w, i) => { + if (!w || typeof w !== "object") { + errors.push(`workflows[${i}] \u5FC5\u9808\u662F\u7269\u4EF6`); + return; + } + const wf = w; + if (typeof wf.name !== "string" || wf.name.trim() === "") errors.push(`workflows[${i}].name \u5FC5\u586B`); + if (!wf.graph || typeof wf.graph !== "object") { + errors.push(`workflows[${i}].graph \u5FC5\u586B\uFF08\u57F7\u884C\u5716\uFF09`); + } else { + const g = wf.graph; + if (typeof g.id !== "string" || g.id.trim() === "") errors.push(`workflows[${i}].graph.id \u5FC5\u586B`); + if (typeof g.name !== "string" || g.name.trim() === "") errors.push(`workflows[${i}].graph.name \u5FC5\u586B`); + if (!Array.isArray(g.nodes)) errors.push(`workflows[${i}].graph.nodes \u5FC5\u9808\u662F\u9663\u5217`); + if (!Array.isArray(g.edges)) errors.push(`workflows[${i}].graph.edges \u5FC5\u9808\u662F\u9663\u5217`); + } + }); + } + } + if (d.ui !== void 0 && !hasUi) { + errors.push("ui.html \u5FC5\u586B\u4E14\u4E0D\u53EF\u7A7A\uFF08\u6709 ui \u6B04\u4F4D\u5C31\u8981\u6709\u756B\u9762\u5167\u5BB9\uFF09"); + } + if (d.data !== void 0) { + if (!Array.isArray(d.data)) { + errors.push("data \u5FC5\u9808\u662F\u9663\u5217"); + } else { + d.data.forEach((dt, i) => { + if (!dt || typeof dt !== "object") { + errors.push(`data[${i}] \u5FC5\u9808\u662F\u7269\u4EF6`); + return; + } + const decl = dt; + if (typeof decl.name !== "string" || decl.name.trim() === "") errors.push(`data[${i}].name \u5FC5\u586B`); + if (!Array.isArray(decl.slots) || decl.slots.some((s) => typeof s !== "string")) { + errors.push(`data[${i}].slots \u5FC5\u9808\u662F\u5B57\u4E32\u9663\u5217`); + } + }); + } + } + if (d.actions !== void 0 && (!Array.isArray(d.actions) || d.actions.some((a) => typeof a !== "string"))) { + errors.push("actions \u5FC5\u9808\u662F\u5B57\u4E32\u9663\u5217"); + } + return { ok: errors.length === 0, errors }; +} +var ICON_SET = ["\u{1F4C4}", "\u{1F5D2}\uFE0F", "\u{1F9E9}", "\u{1F4CC}", "\u{1F527}", "\u{1F4E6}", "\u{1F5C2}\uFE0F", "\u2705", "\u{1F4A1}", "\u{1F514}", "\u{1F4CA}", "\u{1F9ED}"]; +function generateIcon(name) { + let h = 0; + for (let i = 0; i < name.length; i++) h = h * 31 + name.charCodeAt(i) >>> 0; + return ICON_SET[h % ICON_SET.length]; +} +function applyDeclarationDefaults(raw2) { + const workflows = raw2.workflows ?? []; + const actions = raw2.actions ?? workflows.map((w) => w.name); + return { + ...raw2, + id: raw2.id, + name: raw2.name, + icon: raw2.icon ?? generateIcon(raw2.name), + version: raw2.version ?? "0.0.0", + workflows, + data: raw2.data ?? [], + actions, + keeps_data: raw2.remove?.keeps_data ?? true + }; +} +async function computeContentHash(raw2) { + const canonical = canonicalize(raw2); + const data = new TextEncoder().encode(canonical); + const buf = await crypto.subtle.digest("SHA-256", data); + return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join(""); +} +function canonicalize(value) { + if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`; + if (value && typeof value === "object") { + const keys = Object.keys(value).sort(); + return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(value[k])}`).join(",")}}`; + } + return JSON.stringify(value); +} +function appKey(tenant2, id) { + return `${tenant2}:app:${id}`; +} +function workflowKvKey(tenant2, wfKey) { + return `${tenant2}:wf:${wfKey}`; +} +async function getInstalledApp(env, tenant2, id) { + const raw2 = await env.WEBHOOKS.get(appKey(tenant2, id), "text"); + if (!raw2) return null; + try { + return JSON.parse(raw2); + } catch { + return null; + } +} +async function listInstalledApps(env, tenant2) { + const prefix = `${tenant2}:app:`; + const list = await env.WEBHOOKS.list({ prefix }); + const apps = await Promise.all( + list.keys.map(async (k) => { + const raw2 = await env.WEBHOOKS.get(k.name, "text"); + if (!raw2) return null; + try { + return JSON.parse(raw2); + } catch { + return null; + } + }) + ); + return apps.filter((a) => a !== null); +} +async function ensureTemplate(env, name, slots, description) { + const { base, headers } = kbdbBase(env); + const getRes = await fetch(`${base}/templates/${encodeURIComponent(name)}`, { headers }); + if (getRes.ok) { + const data = await getRes.json().catch(() => null); + if (data?.success && data.template) return; + } + await fetch(`${base}/templates`, { + method: "POST", + headers, + body: JSON.stringify({ name, slots, description }) + }); +} +async function installApp(env, tenant2, rawDecl) { + const validation = validateAppDeclaration(rawDecl); + if (!validation.ok) return { ok: false, errors: validation.errors }; + const decl = applyDeclarationDefaults(rawDecl); + const contentHash = await computeContentHash(rawDecl); + const existing = await getInstalledApp(env, tenant2, decl.id); + if (existing && existing.content_hash === contentHash && existing.status === "active") { + return { ok: true, changed: false, app: existing }; + } + for (const dt of decl.data) { + await ensureTemplate(env, dt.name, dt.slots, dt.description); + } + const workflowRefs = []; + for (const wf of decl.workflows) { + const wfKey = `${decl.id}__${wf.name}`; + await env.WEBHOOKS.put( + workflowKvKey(tenant2, wfKey), + JSON.stringify({ + graph: wf.graph, + description: wf.description || `${decl.name}: ${wf.name}`, + created_at: existing?.installed_at ?? (/* @__PURE__ */ new Date()).toISOString() + }) + ); + workflowRefs.push({ name: wf.name, wf_key: wfKey, description: wf.description || "" }); + } + if (existing) { + const keepKeys = new Set(workflowRefs.map((w) => w.wf_key)); + for (const prev of existing.workflows) { + if (!keepKeys.has(prev.wf_key)) { + await env.WEBHOOKS.delete(workflowKvKey(tenant2, prev.wf_key)); + } + } + } + const uiHtml = decl.ui?.html; + const record = { + id: decl.id, + name: decl.name, + icon: decl.icon, + version: decl.version, + content_hash: contentHash, + workflows: workflowRefs, + actions: decl.actions, + data_templates: decl.data.map((d) => d.name), + has_ui: Boolean(uiHtml), + ui_html: uiHtml, + keeps_data: decl.keeps_data, + installed_at: existing?.installed_at ?? (/* @__PURE__ */ new Date()).toISOString(), + updated_at: (/* @__PURE__ */ new Date()).toISOString(), + status: "active" + }; + await env.WEBHOOKS.put(appKey(tenant2, decl.id), JSON.stringify(record)); + return { ok: true, changed: true, app: record }; +} +async function uninstallApp(env, tenant2, id) { + const existing = await getInstalledApp(env, tenant2, id); + if (!existing) return { ok: false, error: "\u9019\u500B App \u6C92\u6709\u5B89\u88DD\u7D00\u9304" }; + for (const wf of existing.workflows) { + await env.WEBHOOKS.delete(workflowKvKey(tenant2, wf.wf_key)); + } + await env.WEBHOOKS.delete(appKey(tenant2, id)); + return { ok: true }; +} +async function runAppAction(env, tenant2, appId, action, payload, ctx) { + const app2 = await getInstalledApp(env, tenant2, appId); + if (!app2 || app2.status !== "active") return { ok: false, status: 404, error: "\u9019\u500B App \u6C92\u6709\u5B89\u88DD" }; + if (!app2.actions.includes(action)) { + return { ok: false, status: 403, error: "\u9019\u500B\u52D5\u4F5C\u4E0D\u5728\u9019\u500B App \u7684\u767D\u540D\u55AE\u5167" }; + } + const wfRef = app2.workflows.find((w) => w.name === action); + if (!wfRef) return { ok: false, status: 500, error: "\u52D5\u4F5C\u5C0D\u61C9\u7684\u5DE5\u4F5C\u6D41\u907A\u5931\uFF08\u5B89\u88DD\u614B\u640D\u6BC0\uFF09" }; + const raw2 = await env.WEBHOOKS.get(workflowKvKey(tenant2, wfRef.wf_key), "text"); + if (!raw2) return { ok: false, status: 500, error: "\u5DE5\u4F5C\u6D41\u8CC7\u6599\u907A\u5931\uFF08\u5B89\u88DD\u614B\u640D\u6BC0\uFF09" }; + let graph; + try { + const parsed = JSON.parse(raw2); + if (!parsed.graph) throw new Error("no graph"); + graph = parsed.graph; + } catch { + return { ok: false, status: 500, error: "\u5DE5\u4F5C\u6D41\u5B9A\u7FA9\u640D\u6BC0" }; + } + const result = await executeWebhookGraph(env, graph, payload, wfRef.wf_key, String(tenant2), ctx); + if (!result.success) { + return { ok: false, status: 502, error: `\u5DE5\u4F5C\u6D41\u57F7\u884C\u5931\u6557\uFF1A${result.error ?? "\u672A\u77E5\u932F\u8AA4"}` }; + } + return { ok: true, status: 200, result: result.data }; +} +function summarizeApp(app2) { + return { id: app2.id, name: app2.name, icon: app2.icon, has_ui: app2.has_ui, version: app2.version }; +} +function detailApp(app2) { + return { + id: app2.id, + name: app2.name, + icon: app2.icon, + version: app2.version, + has_ui: app2.has_ui, + ui_html: app2.ui_html, + workflows: app2.workflows.map((w) => ({ name: w.name, description: w.description })), + actions: app2.actions + }; +} + +// cypher-executor/src/routes/portal-data.ts var portalDataRouter = new Hono2(); async function getTenantWorkflowGraph(env, name) { const raw2 = await env.WEBHOOKS.get(`${knowledgeOwner(env)}:wf:${name}`, "text"); @@ -15474,6 +15732,57 @@ portalDataRouter.get( return c.json({ success: true, workflows, total: workflows.length, read_only: true }); }) ); +portalDataRouter.get( + "/portal/data/apps", + (c) => run(c, async () => { + const auth = await requirePortalUser(c); + if (!auth.ok) return auth.res; + const tenant2 = knowledgeOwner(c.env); + const apps = await listInstalledApps(c.env, tenant2); + return c.json({ apps: apps.map(summarizeApp), count: apps.length }); + }) +); +portalDataRouter.get( + "/portal/data/apps/:id", + (c) => run(c, async () => { + const auth = await requirePortalUser(c); + if (!auth.ok) return auth.res; + const tenant2 = knowledgeOwner(c.env); + const app2 = await getInstalledApp(c.env, tenant2, c.req.param("id")); + if (!app2) return c.json({ error: "\u627E\u4E0D\u5230\u9019\u500B App" }, 404); + return c.json(detailApp(app2)); + }) +); +portalDataRouter.post( + "/portal/data/apps/:id/action", + (c) => run(c, async () => { + const auth = await requirePortalUser(c); + if (!auth.ok) return auth.res; + const tenant2 = knowledgeOwner(c.env); + const appId = c.req.param("id"); + const body = await c.req.json().catch(() => null); + const action = typeof body?.action === "string" ? body.action : ""; + if (!action) return c.json({ error: "action \u5FC5\u586B" }, 400); + const payload = body?.payload && typeof body.payload === "object" ? body.payload : {}; + const result = await runAppAction(c.env, tenant2, appId, action, payload, c.executionCtx); + if (!result.ok) return c.json({ error: result.error }, result.status); + return c.json({ ok: true, result: result.result }); + }) +); +portalDataRouter.delete( + "/portal/data/apps/:id", + (c) => run(c, async () => { + const auth = await requirePortalUser(c); + if (!auth.ok) return auth.res; + if ((auth.user.values.role ?? "user") !== "admin") { + return c.json({ error: "\u9700\u8981 admin \u6B0A\u9650" }, 403); + } + const tenant2 = knowledgeOwner(c.env); + const result = await uninstallApp(c.env, tenant2, c.req.param("id")); + if (!result.ok) return c.json({ error: result.error ?? "\u5378\u8F09\u5931\u6557" }, 404); + return c.json({ removed: true }); + }) +); function recordLibrary(values) { const lib = values?.library; return typeof lib === "string" && lib.trim() ? lib.trim() : null; @@ -15611,11 +15920,16 @@ portalDataRouter.get( const auth = await requirePortalUser(c); if (!auth.ok) return auth.res; const libraries = parseLibraries(auth.user.values.libraries); - if (libraries.length === 0) return c.json({ success: true, records: [], count: 0 }); + if (libraries.length === 0) return c.json({ success: true, records: [], count: 0, total: 0 }); const tenant2 = knowledgeOwner(c.env); + const params = new URLSearchParams(ownerQuery(tenant2)); + for (const k of ["limit", "offset"]) { + const v = c.req.query(k); + if (v) params.set(k, v); + } const res = await kbdbFetch( c.env, - `/records/by-template/${encodeURIComponent(c.req.param("template"))}?${ownerQuery(tenant2)}` + `/records/by-template/${encodeURIComponent(c.req.param("template"))}?${params.toString()}` ); if (!res.ok) return c.json({ error: `KBDB \u56DE\u932F\uFF08HTTP ${res.status}\uFF09` }, 502); const body = await res.json().catch(() => null); @@ -15623,7 +15937,17 @@ portalDataRouter.get( return c.json({ error: "record \u8B80\u53D6\u5931\u6557\uFF1AKBDB \u56DE\u61C9\u4E0D\u662F\u9810\u671F\u7684 records \u6E05\u55AE" }, 502); } const records = body.records.filter((r) => canReadRecord(r, tenant2, libraries)); - return c.json({ success: true, records, count: records.length }); + return c.json({ + success: true, + records, + count: records.length, + // 上游這一頁給了幾筆、被這層的庫權限濾掉幾筆——分得出「沒有下一頁」與「這一頁你看不到」。 + page_size: body.records.length, + filtered_out: body.records.length - records.length, + limit: body.limit, + offset: body.offset, + total: body.total + }); }) ); portalDataRouter.get( @@ -15685,6 +16009,36 @@ portalDataRouter.get( }) ); +// cypher-executor/src/routes/apps.ts +init_dist(); +var appsRouter = new Hono2(); +function requireApiKey(c) { + return c.req.header("X-Arcrun-API-Key") ?? null; +} +appsRouter.post("/apps/install", async (c) => { + const apiKey = requireApiKey(c); + if (!apiKey) return c.json({ error: "\u7F3A\u5C11 X-Arcrun-API-Key header" }, 401); + const body = await c.req.json().catch(() => null); + if (!body) return c.json({ error: "\u7121\u6548\u7684 JSON body" }, 400); + const result = await installApp(c.env, apiKey, body); + if (!result.ok) return c.json({ error: "\u5BA3\u544A\u672A\u901A\u904E\u9A57\u8B49", details: result.errors }, 400); + return c.json({ installed: true, changed: result.changed, app: result.app ? summarizeApp(result.app) : void 0 }); +}); +appsRouter.delete("/apps/:id", async (c) => { + const apiKey = requireApiKey(c); + if (!apiKey) return c.json({ error: "\u7F3A\u5C11 X-Arcrun-API-Key header" }, 401); + const id = c.req.param("id"); + const result = await uninstallApp(c.env, apiKey, id); + if (!result.ok) return c.json({ error: result.error ?? "\u5378\u8F09\u5931\u6557" }, 404); + return c.json({ removed: true }); +}); +appsRouter.get("/apps", async (c) => { + const apiKey = requireApiKey(c); + if (!apiKey) return c.json({ error: "\u7F3A\u5C11 X-Arcrun-API-Key header" }, 401); + const apps = await listInstalledApps(c.env, apiKey); + return c.json({ apps: apps.map(summarizeApp), count: apps.length }); +}); + // cypher-executor/src/index.ts var app = new Hono2(); var STATIC_ORIGINS = ["https://arcrun.dev", "https://www.arcrun.dev"]; @@ -15724,6 +16078,7 @@ app.route("/", consoleAuthRouter); app.route("/", consoleDashboardRouter); app.route("/", portalRouter); app.route("/", portalDataRouter); +app.route("/", appsRouter); var index_default = { fetch: app.fetch, scheduled: handleScheduled diff --git a/arcrun-kbdb/worker.mjs b/arcrun-kbdb/worker.mjs index adde495..9a3d7ee 100644 --- a/arcrun-kbdb/worker.mjs +++ b/arcrun-kbdb/worker.mjs @@ -3491,11 +3491,19 @@ async function getRecord(db, recordId) { const identity = await db.prepare("SELECT owner_id FROM entries WHERE id = ?").bind(recordId).first(); return { record_id: recordId, template_id: belongs.dst_id, values, owner_id: identity?.owner_id ?? null }; } -async function searchByTemplate(db, template, owner_id, limit = 100, offset = 0) { +async function searchByTemplatePage(db, template, owner_id, limit = 100, offset = 0) { const tpl = await getTemplate(db, template); - if (!tpl) return []; + if (!tpl) return { records: [], total: 0 }; const cap = Math.min(Math.max(limit, 1), 500); const skip = Math.max(offset, 0); + const totalRow = owner_id ? await db.prepare( + // kbdb-sql-ok:牆內本體(kbdb/src/actions/) + `SELECT COUNT(*) AS total FROM entries WHERE rel_id = '${SYS_BELONGS}' AND dst_id = ? AND owner_id = ?` + ).bind(tpl.id, owner_id).first() : await db.prepare( + // kbdb-sql-ok:同上 + `SELECT COUNT(*) AS total FROM entries WHERE rel_id = '${SYS_BELONGS}' AND dst_id = ?` + ).bind(tpl.id).first(); + const total = totalRow?.total ?? 0; const res = owner_id ? await db.prepare( // kbdb-sql-ok:牆內本體(kbdb/src/actions/);本次 checkout 開在 worktree /private/tmp/wt-graph-first-44/,hook 逐字比對 matrix/arcrun/kbdb/src/ 吃不到,與 962d863/5919c6b 記載的是同一個假警報 `SELECT src_id AS record_id FROM entries @@ -3508,7 +3516,7 @@ async function searchByTemplate(db, template, owner_id, limit = 100, offset = 0) ORDER BY created_at DESC, rowid DESC LIMIT ? OFFSET ?` ).bind(tpl.id, cap, skip).all(); const ids = (res.results ?? []).map((r) => r.record_id); - if (ids.length === 0) return []; + if (ids.length === 0) return { records: [], total }; const byId = /* @__PURE__ */ new Map(); for (const id of ids) byId.set(id, { record_id: id, template_id: tpl.id, values: {}, owner_id: null }); for (let i = 0; i < ids.length; i += 90) { @@ -3533,7 +3541,7 @@ async function searchByTemplate(db, template, owner_id, limit = 100, offset = 0) if (rec) rec.owner_id = r.owner_id; } } - return ids.map((id) => byId.get(id)).filter((r) => !!r); + return { records: ids.map((id) => byId.get(id)).filter((r) => !!r), total }; } async function deleteRecord(db, recordId) { const belongs = await recordBelongs(db, recordId); @@ -4113,14 +4121,14 @@ var intParam = (raw2, fallback, min, max) => { recordRoutes.get("/by-template/:template", async (c) => { const limit = intParam(c.req.query("limit"), 100, 1, 500); const offset = intParam(c.req.query("offset"), 0, 0, Number.MAX_SAFE_INTEGER); - const records = await searchByTemplate( + const { records, total } = await searchByTemplatePage( c.env.DB, c.req.param("template"), c.req.query("owner_id") || void 0, limit, offset ); - return c.json({ success: true, records, count: records.length, limit, offset }); + return c.json({ success: true, records, count: records.length, limit, offset, total }); }); recordRoutes.get("/:recordId", async (c) => { const rec = await getRecord(c.env.DB, c.req.param("recordId")); diff --git a/arcrun-mcp/worker.mjs b/arcrun-mcp/worker.mjs index 5f752e6..f940330 100644 --- a/arcrun-mcp/worker.mjs +++ b/arcrun-mcp/worker.mjs @@ -24512,22 +24512,20 @@ function successResponse(data, hints) { } // mcp/src/tools/arcrun_introspection.ts -var apiKeyDesc = "\u4F60 (\u7528\u6236) \u7684 arcrun api_key (ak_xxx)\u3002\u5F9E https://arcrun.dev/me \u53D6\u5F97\u3002\u6CE8\u610F\uFF1A\u8DDF MCP \u9023\u7DDA\u7528\u7684 pk_live token \u662F\u4E0D\u540C\u5C64 auth \u2014 pk_live \u7D66 MCP \u7528\uFF0Cak_xxx \u7D66 workflow \u64CD\u4F5C\u7528"; -function registerValidateYaml(server, env) { +function registerValidateYaml(server, env, partnerToken) { server.tool( toolName("validate_yaml"), "Dry-run YAML \u6821\u9A57\u3002\u4E0D\u90E8\u7F72\u3001\u7121 side effect\u3002\u56DE {valid, errors?, nodeCount, edgeCount}\u3002**\u6C38\u9060\u5148 call \u6B64 tool \u518D push_workflow**\uFF0C\u907F\u514D\u53CD\u8986 deploy \u5931\u6557\u3002", { - api_key: external_exports.string().describe(apiKeyDesc), graph: external_exports.object({ nodes: external_exports.array(external_exports.unknown()).describe("workflow \u7BC0\u9EDE\u9663\u5217"), edges: external_exports.array(external_exports.unknown()).describe("workflow \u908A\u9663\u5217 (cypher binding \u4E09\u5143\u7D44)") }).passthrough().describe("workflow graph object\uFF08\u5DF2 parse \u904E YAML \u7684\u7D50\u69CB\uFF0C\u975E raw YAML string\uFF09") }, - async ({ api_key, graph }) => { + async ({ graph }) => { try { const res = await cypherFetch(env, "/validate", { - apiKey: api_key, + apiKey: partnerToken, method: "POST", body: graph }); @@ -24558,18 +24556,17 @@ function registerValidateYaml(server, env) { } ); } -function registerListPausedExecutions(server, env) { +function registerListPausedExecutions(server, env, partnerToken) { server.tool( toolName("list_paused_executions"), - "\u5217\u7576\u524D api_key \u4E0B\u6240\u6709 paused workflow\uFF08\u7B49 daemon callback resume \u7684\uFF09\u3002\u7D66 debug \u7528\uFF1Aclaude_api \u7B49 async \u96F6\u4EF6\u6703\u628A workflow \u66AB\u505C\uFF0C\u6B64 tool \u544A\u8A34\u4F60\u54EA\u4E9B\u9084\u6C92\u56DE\u4F86\u3002", + "\u5217\u7576\u524D\u5E33\u865F\u4E0B\u6240\u6709 paused workflow\uFF08\u7B49 daemon callback resume \u7684\uFF09\u3002\u7D66 debug \u7528\uFF1Aclaude_api \u7B49 async \u96F6\u4EF6\u6703\u628A workflow \u66AB\u505C\uFF0C\u6B64 tool \u544A\u8A34\u4F60\u54EA\u4E9B\u9084\u6C92\u56DE\u4F86\u3002", { - api_key: external_exports.string().describe(apiKeyDesc), limit: external_exports.number().int().min(1).max(100).optional().describe("\u6700\u591A\u56DE\u5E7E\u500B\uFF08\u9810\u8A2D 20\uFF0C\u6700\u591A 100\uFF09") }, - async ({ api_key, limit }) => { + async ({ limit }) => { try { const res = await cypherFetch(env, "/executions/paused", { - apiKey: api_key, + apiKey: partnerToken, query: limit ? { limit } : void 0 }); const body = await res.json().catch(() => null); @@ -24577,7 +24574,7 @@ function registerListPausedExecutions(server, env) { return errorResponse( "fetch_failed", `\u6488 paused \u5217\u8868\u5931\u6557 HTTP ${res.status}`, - ["\u6AA2\u67E5 api_key \u662F\u5426\u6B63\u78BA", "\u7A0D\u5F8C\u91CD\u8A66"], + ["\u7A0D\u5F8C\u91CD\u8A66", "\u82E5\u6301\u7E8C\u5931\u6557\uFF0C\u544A\u8A34 leo \u4E26\u8CBC\u932F\u8AA4\u8A0A\u606F"], JSON.stringify(body) ); } @@ -24592,20 +24589,19 @@ function registerListPausedExecutions(server, env) { } ); } -function registerGetExecutionTrace(server, env) { +function registerGetExecutionTrace(server, env, partnerToken) { server.tool( toolName("get_execution_trace"), "\u770B\u55AE\u4E00 paused workflow \u7684 state \u7D30\u7BC0\uFF08trace\u3001graph\u3001context\u3001pending_result\uFF09\u3002task_id \u5F9E paused \u932F\u8AA4\u8A0A\u606F\u6216 list_paused_executions \u53D6\u5F97\u3002", { - api_key: external_exports.string().describe(apiKeyDesc), task_id: external_exports.string().describe( "Paused workflow \u7684 task_id\u3002\u4F86\u6E90\uFF1Aworkflow \u89F8\u767C\u5F8C\u82E5 paused\uFF0Cerror \u8A0A\u606F\u542B 'waiting for task task_XXX'\uFF1B\u6216 list_paused_executions \u56DE\u7684 task_id \u6B04\u4F4D" ) }, - async ({ api_key, task_id }) => { + async ({ task_id }) => { try { const res = await cypherFetch(env, `/executions/${encodeURIComponent(task_id)}`, { - apiKey: api_key + apiKey: partnerToken }); const body = await res.json().catch(() => null); if (res.status === 404) { @@ -24637,22 +24633,21 @@ function registerGetExecutionTrace(server, env) { } ); } -function registerListRecentExecutions(server, env) { +function registerListRecentExecutions(server, env, partnerToken) { server.tool( toolName("list_recent_executions"), "\u5217\u67D0 workflow \u6700\u8FD1 N \u6B21\u57F7\u884C verdict\uFF08\u6210\u529F / \u5931\u6557 / duration\uFF09\u3002\u8CC7\u6599\u4F86\u6E90\u662F D1 \u57F7\u884C\u7D00\u9304\u8868\uFF0C\u7A3D\u6838\u8CC7\u6599\u2014\u2014\u9810\u8A2D\u4FDD\u7559 90 \u5929\uFF083 \u500B\u6708\uFF09\u904E\u671F\u5373\u6E05\uFF0C\u7BA1\u7406\u8005\u53EF\u5728 portal \u6539\u4FDD\u7559\u5929\u6578\u6216\u8A2D\u70BA\u4E0D\u522A\u9664\uFF1B\u7528\u91CF\u904E\u5927\u6642\u7CFB\u7D71\u6703\u81EA\u52D5\u964D\u6210\u53EA\u8A18\u5931\u6557\u3001\u751A\u81F3\u66AB\u505C\u8A18\u9304\u2014\u2014\u5DE5\u4F5C\u6D41\u672C\u8EAB\u57F7\u884C\u4E0D\u53D7\u5F71\u97FF\u3002", { - api_key: external_exports.string().describe(apiKeyDesc), workflow_name: external_exports.string().describe("workflow \u540D\u7A31\uFF08acr push \u6642\u7684 name \u6B04\uFF09"), limit: external_exports.number().int().min(1).max(100).optional().describe("\u6700\u591A\u56DE\u5E7E\u7B46\uFF08\u9810\u8A2D 10\uFF0C\u6700\u591A 100\uFF09") }, - async ({ api_key, workflow_name, limit }) => { + async ({ workflow_name, limit }) => { try { const res = await cypherFetch( env, `/workflows/${encodeURIComponent(workflow_name)}/executions`, { - apiKey: api_key, + apiKey: partnerToken, query: limit ? { limit } : void 0 } ); @@ -24686,11 +24681,11 @@ function registerListRecentExecutions(server, env) { } ); } -function registerAllIntrospectionTools(server, env) { - registerValidateYaml(server, env); - registerListPausedExecutions(server, env); - registerGetExecutionTrace(server, env); - registerListRecentExecutions(server, env); +function registerAllIntrospectionTools(server, env, partnerToken) { + registerValidateYaml(server, env, partnerToken); + registerListPausedExecutions(server, env, partnerToken); + registerGetExecutionTrace(server, env, partnerToken); + registerListRecentExecutions(server, env, partnerToken); } // mcp/node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/browser/dist/nodes/identity.js @@ -30961,13 +30956,11 @@ function parse3(src, reviver, options) { } // mcp/src/tools/arcrun_workflow_crud.ts -var apiKeyDesc2 = "\u4F60\uFF08\u7528\u6236\uFF09\u7684 arcrun api_key (ak_xxx)\u3002\u5F9E https://arcrun.dev/me \u53D6\u5F97"; -function registerPushWorkflow(server, env) { +function registerPushWorkflow(server, env, partnerToken) { server.tool( toolName("push_workflow"), "\u90E8\u7F72 workflow \u5230 arcrun\u3002\u8F38\u5165\u53EF\u4EE5\u662F YAML \u5B57\u4E32\u6216 graph \u7269\u4EF6\u3002**\u5EFA\u8B70\u5148 call arcrun_validate_yaml \u78BA\u8A8D schema**\u3002", { - api_key: external_exports.string().describe(apiKeyDesc2), yaml_content: external_exports.string().optional().describe( "YAML \u5B57\u4E32\u3002\u5167\u90E8\u6703 parse \u6210 {name, flow, config}\u3002\u512A\u5148\u65BC graph \u53C3\u6578" ), @@ -30979,7 +30972,7 @@ function registerPushWorkflow(server, env) { ), description: external_exports.string().optional().describe("workflow \u63CF\u8FF0\uFF08\u9078\u586B\uFF09") }, - async ({ api_key, yaml_content, graph, name, description }) => { + async ({ yaml_content, graph, name, description }) => { let workflowName = name; let workflowGraph = graph; let workflowConfig = void 0; @@ -31018,7 +31011,7 @@ function registerPushWorkflow(server, env) { } try { const res = await cypherFetch(env, "/webhooks/named", { - apiKey: api_key, + apiKey: partnerToken, method: "POST", body: { name: workflowName, @@ -31034,8 +31027,7 @@ function registerPushWorkflow(server, env) { `\u90E8\u7F72\u5931\u6557 HTTP ${res.status}: ${body.error ?? "unknown"}`, [ "\u5148 call arcrun_validate_yaml \u78BA\u8A8D graph schema \u6B63\u78BA", - "\u78BA\u8A8D workflow name \u7B26\u5408 [a-zA-Z0-9_-] \u683C\u5F0F", - "\u78BA\u8A8D api_key \u662F ak_xxx \u683C\u5F0F\u4E14\u6709\u6548" + "\u78BA\u8A8D workflow name \u7B26\u5408 [a-zA-Z0-9_-] \u683C\u5F0F" ], JSON.stringify(body) ); @@ -31056,24 +31048,22 @@ function registerPushWorkflow(server, env) { } ); } -function registerListWorkflows(server, env) { +function registerListWorkflows(server, env, partnerToken) { server.tool( toolName("list_workflows"), - "\u5217\u51FA\u4F60 (api_key \u5C0D\u61C9 namespace) \u5DF2\u90E8\u7F72\u7684\u6240\u6709 workflow\u3002\u56DE [{name}]\u3002", - { - api_key: external_exports.string().describe(apiKeyDesc2) - }, - async ({ api_key }) => { + "\u5217\u51FA\u4F60\u76EE\u524D\u5E33\u865F\u4E0B\u5DF2\u90E8\u7F72\u7684\u6240\u6709 workflow\u3002\u56DE [{name}]\u3002", + {}, + async () => { try { const res = await cypherFetch(env, "/webhooks/named", { - apiKey: api_key + apiKey: partnerToken }); const body = await res.json().catch(() => null); if (!res.ok) { return errorResponse( "fetch_failed", `\u6488 workflow \u5217\u8868\u5931\u6557 HTTP ${res.status}`, - ["\u78BA\u8A8D api_key \u6B63\u78BA", "\u7A0D\u5F8C\u91CD\u8A66"], + ["\u7A0D\u5F8C\u91CD\u8A66", "\u82E5\u6301\u7E8C\u5931\u6557\uFF0C\u544A\u8A34 leo \u4E26\u8CBC\u932F\u8AA4\u8A0A\u606F"], JSON.stringify(body) ); } @@ -31093,25 +31083,24 @@ function registerListWorkflows(server, env) { } ); } -function registerGetWorkflow(server, env) { +function registerGetWorkflow(server, env, partnerToken) { server.tool( toolName("get_workflow"), "\u770B\u55AE\u4E00 workflow \u7684\u5B8C\u6574\u5B9A\u7FA9\uFF08graph + config\uFF09\u3002", { - api_key: external_exports.string().describe(apiKeyDesc2), name: external_exports.string().describe("workflow \u540D\u7A31") }, - async ({ api_key, name }) => { + async ({ name }) => { try { const res = await cypherFetch(env, "/webhooks/named", { - apiKey: api_key + apiKey: partnerToken }); const body = await res.json().catch(() => null); if (!res.ok || !body?.workflows) { return errorResponse( "fetch_failed", `\u6488 workflow \u5217\u8868\u5931\u6557`, - ["\u78BA\u8A8D api_key \u6B63\u78BA"], + ["\u7A0D\u5F8C\u91CD\u8A66"], JSON.stringify(body) ); } @@ -31147,19 +31136,18 @@ function registerGetWorkflow(server, env) { } ); } -function registerDeleteWorkflow(server, env) { +function registerDeleteWorkflow(server, env, partnerToken) { server.tool( toolName("delete_workflow"), "\u522A\u9664 workflow\u3002**\u4E0D\u53EF\u9006\uFF0C\u78BA\u8A8D\u5F8C\u518D\u505A**\u3002\u6703\u6E05\u6389\u5C0D\u61C9 cron index \u8207 webhook URL\u3002", { - api_key: external_exports.string().describe(apiKeyDesc2), name: external_exports.string().describe("\u8981\u522A\u7684 workflow \u540D\u7A31"), confirm: external_exports.literal(true).describe("\u5FC5\u9808\u50B3 true \u78BA\u8A8D") }, - async ({ api_key, name, confirm: _confirm }) => { + async ({ name, confirm: _confirm }) => { try { const res = await cypherFetch(env, `/webhooks/named/${encodeURIComponent(name)}`, { - apiKey: api_key, + apiKey: partnerToken, method: "DELETE" }); const body = await res.json().catch(() => ({})); @@ -31188,25 +31176,24 @@ function registerDeleteWorkflow(server, env) { } ); } -function registerRunWorkflow(server, env) { +function registerRunWorkflow(server, env, partnerToken) { server.tool( toolName("run_workflow"), "\u89F8\u767C workflow \u57F7\u884C\u3002input \u7269\u4EF6\u5E36\u9032 trigger context\u3002\u56DE {success, data, trace?, duration_ms}\u3002", { - api_key: external_exports.string().describe(apiKeyDesc2), name: external_exports.string().describe("workflow \u540D\u7A31"), input: external_exports.record(external_exports.unknown()).optional().describe( - "trigger context\uFF08\u6703\u585E\u9032 workflow \u7B2C\u4E00\u500B\u7BC0\u9EDE\u7684\u8F38\u5165\uFF09\u3002\u8A18\u5F97\u5E36 api_key \u7D66\u5167\u90E8\u9700\u8981\u7684\u7BC0\u9EDE\u7528" + "trigger context\uFF08\u6703\u585E\u9032 workflow \u7B2C\u4E00\u500B\u7BC0\u9EDE\u7684\u8F38\u5165\uFF09" ) }, - async ({ api_key, name, input }) => { + async ({ name, input }) => { try { const triggerBody = input ?? {}; if (!("api_key" in triggerBody)) { - triggerBody.api_key = api_key; + triggerBody.api_key = partnerToken; } const res = await cypherFetch(env, `/webhooks/named/${encodeURIComponent(name)}/trigger`, { - apiKey: api_key, + apiKey: partnerToken, method: "POST", body: triggerBody }); @@ -31254,12 +31241,12 @@ function registerRunWorkflow(server, env) { } ); } -function registerAllWorkflowCrudTools(server, env) { - registerPushWorkflow(server, env); - registerListWorkflows(server, env); - registerGetWorkflow(server, env); - registerDeleteWorkflow(server, env); - registerRunWorkflow(server, env); +function registerAllWorkflowCrudTools(server, env, partnerToken) { + registerPushWorkflow(server, env, partnerToken); + registerListWorkflows(server, env, partnerToken); + registerGetWorkflow(server, env, partnerToken); + registerDeleteWorkflow(server, env, partnerToken); + registerRunWorkflow(server, env, partnerToken); } // mcp/src/lib/portal-client.ts @@ -31607,27 +31594,25 @@ function registerAllSkillExampleTools(server, env, identity) { } // mcp/src/tools/arcrun_recipe.ts -var apiKeyDesc3 = "\u4F60\uFF08\u7528\u6236\uFF09\u7684 arcrun api_key (ak_xxx)\u3002\u5F9E https://arcrun.dev/me \u53D6\u5F97"; -function registerAllRecipeTools(server, env) { - registerRecipeSearch(server, env); - registerRecipePull(server, env); - registerRecipeSubmitP(server, env); - registerRecipePush(server, env); - registerRecipeList(server, env); - registerRecipeDelete(server, env); +function registerAllRecipeTools(server, env, partnerToken) { + registerRecipeSearch(server, env, partnerToken); + registerRecipePull(server, env, partnerToken); + registerRecipeSubmitP(server, env, partnerToken); + registerRecipePush(server, env, partnerToken); + registerRecipeList(server, env, partnerToken); + registerRecipeDelete(server, env, partnerToken); } -function registerRecipeSearch(server, env) { +function registerRecipeSearch(server, env, partnerToken) { server.tool( toolName("recipe_search"), "\u641C\u5C0B\u516C\u5EAB recipe\uFF08API \u6574\u5408\u914D\u65B9\uFF09\u3002\u540C canonical_id \u53EF\u6709\u591A\u4F5C\u8005\u7248\u672C\uFF0C\u5404\u9644\u5E02\u5834\u6578\u64DA\uFF08\u6210\u529F/\u5931\u6557\u6B21\u6578\uFF09\uFF0C\u4F9D\u6578\u64DA\u9078\u6700\u4F73\u3002\u627E\u4E0D\u5230\u6642\u6703\u63D0\u793A\u53EF\u81EA\u5DF1\u505A\u4E00\u500B recipe \u6295\u7A3F\u6210\u70BA\u4F5C\u8005\u3002", { - api_key: external_exports.string().describe(apiKeyDesc3), query: external_exports.string().describe("\u641C\u5C0B\u8A5E\uFF0C\u5982\u300Cgsheets append\u300D\u300Ctelegram send\u300D") }, - async ({ api_key, query }) => { + async ({ query }) => { try { const res = await cypherFetch(env, "/public-recipes", { - apiKey: api_key, + apiKey: partnerToken, query: { q: query } }); if (!res.ok) return errorResponse("search_failed", `\u641C\u5C0B\u516C\u5EAB\u5931\u6557`, ["\u7A0D\u5F8C\u518D\u8A66"], await res.text()); @@ -31642,19 +31627,18 @@ function registerRecipeSearch(server, env) { } ); } -function registerRecipePull(server, env) { +function registerRecipePull(server, env, partnerToken) { server.tool( toolName("recipe_pull"), "\u5F9E\u516C\u5EAB\u53D6\u4E00\u4EFD recipe \u5BEB\u9032\u81EA\u5DF1\u79C1\u5EAB\uFF08\u6309\u9700\u53D6\u7528\uFF0C\u975E\u5168\u91CF\u540C\u6B65\uFF09\u3002\u4E0D\u6307\u5B9A author \u53D6\u5E02\u5834\u6700\u4F73\u7248\u672C\u3002\u53D6\u56DE\u5F8C\u53EF\u5728 workflow \u7528 component: \u3002", { - api_key: external_exports.string().describe(apiKeyDesc3), canonical_id: external_exports.string().describe("\u8981\u53D6\u7684 recipe canonical_id\uFF0C\u5982 gsheets_append"), author: external_exports.string().optional().describe("\u6307\u5B9A\u4F5C\u8005\u7248\u672C\uFF08\u4E0D\u6307\u5B9A\u53D6\u5E02\u5834\u6700\u4F73\uFF09") }, - async ({ api_key, canonical_id, author }) => { + async ({ canonical_id, author }) => { try { const pubRes = await cypherFetch(env, `/public-recipes/${encodeURIComponent(canonical_id)}`, { - apiKey: api_key, + apiKey: partnerToken, query: author ? { author } : void 0 }); if (!pubRes.ok) return errorResponse("pull_failed", `\u516C\u5EAB\u53D6 recipe \u5931\u6557`, [], await pubRes.text()); @@ -31665,7 +31649,7 @@ function registerRecipePull(server, env) { ]); } const installRes = await cypherFetch(env, "/recipes", { - apiKey: api_key, + apiKey: partnerToken, method: "POST", body: { ...pub.recipe, @@ -31686,26 +31670,25 @@ function registerRecipePull(server, env) { } ); } -function registerRecipeSubmitP(server, env) { +function registerRecipeSubmitP(server, env, partnerToken) { server.tool( toolName("recipe_submit_p"), "\u628A\u79C1\u5EAB\u67D0 recipe \u6295\u7A3F\u5230\u516C\u5EAB\uFF08app-store \u6A21\u578B\uFF1A\u65B0\u589E\u4E00\u500B\u4F5C\u8005\u7248\u672C\uFF0C\u4E0D\u8986\u84CB\u5225\u4EBA\u7684\uFF09\u3002\u6295\u7A3F = \u628A recipe \u66B4\u9732\u7D66\u5168\u7DB2\uFF0C\u9700\u5E36 exposure_consent \u660E\u793A\u540C\u610F\u3002\u5225\u4EBA\u80FD\u641C\u5230\u4E26 pull\uFF0C\u5E02\u5834\u6578\u64DA\u6C7A\u5B9A\u5B83\u88AB\u4E0D\u88AB\u9078\u7528\u3002", { - api_key: external_exports.string().describe(apiKeyDesc3), canonical_id: external_exports.string().describe("\u8981\u6295\u7A3F\u7684\u79C1\u5EAB recipe canonical_id"), author: external_exports.string().optional().describe("\u7F72\u540D\u4F5C\u8005\uFF08\u9810\u8A2D\u7528 recipe \u65E2\u6709 author\uFF09"), exposure_consent: external_exports.boolean().describe( "\u660E\u793A\u540C\u610F\u628A\u6B64 recipe \u66B4\u9732\u7D66\u516C\u5EAB\u5168\u7DB2\uFF08\u6295\u7A3F\u662F\u66B4\u9732\u9762\uFF0C\u9700\u4EBA\u985E\u540C\u610F\uFF09" ) }, - async ({ api_key, canonical_id, author, exposure_consent }) => { + async ({ canonical_id, author, exposure_consent }) => { try { if (!exposure_consent) { return errorResponse("consent_required", "\u6295\u7A3F\u5230\u516C\u5EAB\u662F\u66B4\u9732\u9762\uFF0C\u9700 exposure_consent=true \u660E\u793A\u540C\u610F", [ "\u78BA\u8A8D\u8981\u628A recipe \u516C\u958B\u7D66\u5168\u7DB2\u5F8C\uFF0C\u5E36 exposure_consent: true \u518D\u547C\u53EB" ]); } - const myRes = await cypherFetch(env, `/recipes/${encodeURIComponent(canonical_id)}`, { apiKey: api_key }); + const myRes = await cypherFetch(env, `/recipes/${encodeURIComponent(canonical_id)}`, { apiKey: partnerToken }); if (!myRes.ok) return errorResponse("not_found", `\u79C1\u5EAB\u627E\u4E0D\u5230 recipe\u300C${canonical_id}\u300D`, ["\u5148 arcrun_recipe_push \u6216 arcrun_recipe_pull"], await myRes.text()); const my = await myRes.json(); if (!my.success || !my.recipe) return errorResponse("not_found", `\u79C1\u5EAB\u7121\u6B64 recipe`, []); @@ -31715,13 +31698,13 @@ function registerRecipeSubmitP(server, env) { confirmed_at: (/* @__PURE__ */ new Date()).toISOString() }; const subRes = await cypherFetch(env, "/recipes/submit", { - apiKey: api_key, + apiKey: partnerToken, method: "POST", body: { ...my.recipe, author: author ?? my.recipe.author, derived_from: my.recipe.derived_from ?? my.recipe.uuid, - submitter: author ?? api_key, + submitter: author ?? partnerToken, exposure_consent: consent } }); @@ -31734,12 +31717,11 @@ function registerRecipeSubmitP(server, env) { } ); } -function registerRecipePush(server, env) { +function registerRecipePush(server, env, partnerToken) { server.tool( toolName("recipe_push"), "\u4E0A\u50B3\u4E00\u4EFD recipe \u5230\u81EA\u5DF1\u79C1\u5EAB\uFF08\u6216\u5C31\u5730\u66F4\u65B0\u81EA\u5DF1\u65E2\u6709\u7248\u672C\uFF09\u3002recipe = \u300Chttp_request + \u53C3\u6578\u6A21\u677F\u300D\u7684\u5177\u540D\u5C01\u88DD\uFF0C\u4E0D\u9700 deploy Worker\u3002\u8981\u6295\u7A3F\u5230\u516C\u5EAB\u7528 arcrun_recipe_submit_p\u3002", { - api_key: external_exports.string().describe(apiKeyDesc3), recipe: external_exports.object({ canonical_id: external_exports.string(), display_name: external_exports.string().optional(), @@ -31752,9 +31734,9 @@ function registerRecipePush(server, env) { author: external_exports.string().optional() }).describe("recipe \u5B9A\u7FA9\uFF08canonical_id + endpoint \u5FC5\u586B\uFF09") }, - async ({ api_key, recipe }) => { + async ({ recipe }) => { try { - const res = await cypherFetch(env, "/recipes", { apiKey: api_key, method: "POST", body: recipe }); + const res = await cypherFetch(env, "/recipes", { apiKey: partnerToken, method: "POST", body: recipe }); if (!res.ok) return errorResponse("push_failed", `\u4E0A\u50B3 recipe \u5931\u6557`, [], await res.text()); const data = await res.json(); return successResponse(data, [`workflow \u7528 component: ${recipe.canonical_id}`, "\u8981\u516C\u958B\u7D66\u5168\u7DB2 \u2192 arcrun_recipe_submit_p"]); @@ -31764,16 +31746,14 @@ function registerRecipePush(server, env) { } ); } -function registerRecipeList(server, env) { +function registerRecipeList(server, env, partnerToken) { server.tool( toolName("recipe_list"), "\u5217\u51FA\u81EA\u5DF1\u79C1\u5EAB\uFF08\u672C\u90E8\u7F72\uFF09\u7684 recipe\u3002\u8981\u627E\u516C\u5EAB\u7684\u7528 arcrun_recipe_search\u3002", - { - api_key: external_exports.string().describe(apiKeyDesc3) - }, - async ({ api_key }) => { + {}, + async () => { try { - const res = await cypherFetch(env, "/recipes", { apiKey: api_key }); + const res = await cypherFetch(env, "/recipes", { apiKey: partnerToken }); if (!res.ok) return errorResponse("list_failed", `\u5217\u51FA recipe \u5931\u6557`, [], await res.text()); const data = await res.json(); return successResponse(data); @@ -31783,17 +31763,16 @@ function registerRecipeList(server, env) { } ); } -function registerRecipeDelete(server, env) { +function registerRecipeDelete(server, env, partnerToken) { server.tool( toolName("recipe_delete"), "\u522A\u9664\u81EA\u5DF1\u79C1\u5EAB\u67D0 recipe\uFF08canonical_id / rec_hash / uuid\uFF09\u3002\u4E0D\u5F71\u97FF\u516C\u5EAB\u5225\u4EBA\u7684\u7248\u672C\u3002", { - api_key: external_exports.string().describe(apiKeyDesc3), id: external_exports.string().describe("canonical_id \u6216 rec_hash \u6216 uuid") }, - async ({ api_key, id }) => { + async ({ id }) => { try { - const res = await cypherFetch(env, `/recipes/${encodeURIComponent(id)}`, { apiKey: api_key, method: "DELETE" }); + const res = await cypherFetch(env, `/recipes/${encodeURIComponent(id)}`, { apiKey: partnerToken, method: "DELETE" }); if (!res.ok) return errorResponse("delete_failed", `\u522A\u9664 recipe \u5931\u6557`, [], await res.text()); const data = await res.json(); return successResponse(data); @@ -31944,28 +31923,44 @@ function registerGetRecord(server, env, identity) { function registerQuery(server, env, identity) { server.tool( "kbdb_query", - "\u5217\u51FA\u67D0 template \u5E95\u4E0B\u7684\u6240\u6709 record\uFF08\u7D50\u69CB\u5316\u67E5\u8A62\uFF0C\u6309 template \u53D6\u6574\u6279\u8CC7\u6599\uFF09\u3002\u8981\u6309\u95DC\u9375\u5B57\u627E\u5167\u5BB9\u7528 kbdb_search\u3002", + "\u5217\u51FA\u67D0 template \u5E95\u4E0B\u7684 record\uFF08\u7D50\u69CB\u5316\u67E5\u8A62\uFF0C\u6309 template \u53D6\u6574\u6279\u8CC7\u6599\uFF09\u3002**\u6703\u5206\u9801**\uFF1A\u56DE\u61C9\u7684 total \u662F\u7B26\u5408\u689D\u4EF6\u7684\u5168\u90E8\u7B46\u6578\u3001count \u662F\u9019\u4E00\u9801\u62FF\u5230\u5E7E\u7B46\u2014\u2014total \u6BD4\u5DF2\u53D6\u5F97\u7684\u591A\u5C31\u5E36 offset \u518D\u53EB\u4E00\u6B21\uFF0C\u4E0D\u8981\u628A\u7B2C\u4E00\u9801\u7576\u6210\u5168\u90E8\u3002\u8981\u6309\u95DC\u9375\u5B57\u627E\u5167\u5BB9\u7528 kbdb_search\u3002", { template: external_exports.string().min(1).describe("template \u7684 name \u6216 id"), - owner_id: external_exports.string().optional().describe("\u53EA\u53D6\u67D0\u6B78\u5C6C\u7684 record\uFF08\u9078\u586B\uFF1B\u767B\u5165\u8EAB\u5206\u4E0B\u4E0D\u751F\u6548\uFF0C\u7BC4\u570D\u7531\u4F60\u7684\u6B0A\u9650\u6C7A\u5B9A\uFF09") + owner_id: external_exports.string().optional().describe("\u53EA\u53D6\u67D0\u6B78\u5C6C\u7684 record\uFF08\u9078\u586B\uFF1B\u767B\u5165\u8EAB\u5206\u4E0B\u4E0D\u751F\u6548\uFF0C\u7BC4\u570D\u7531\u4F60\u7684\u6B0A\u9650\u6C7A\u5B9A\uFF09"), + limit: external_exports.number().int().positive().optional().describe("\u9019\u4E00\u9801\u8981\u5E7E\u7B46\uFF08\u9810\u8A2D 100\uFF0C\u55AE\u6B21\u4E0A\u9650 500\uFF09"), + offset: external_exports.number().int().min(0).optional().describe("\u5F9E\u7B2C\u5E7E\u7B46\u958B\u59CB\uFF08\u5206\u9801\u7528\uFF0C\u9810\u8A2D 0\uFF09") }, - async ({ template, owner_id }) => { + async ({ template, owner_id, limit, offset }) => { if (identity.kind === "stale") return staleIdentityError(); try { - const res = identity.kind === "portal" ? await portalFetch( - env, - identity.portal.session, - `/portal/data/records/by-template/${encodeURIComponent(template)}` - ) : await kbdbFetch( - env, - `/records/by-template/${encodeURIComponent(template)}` + (owner_id ? `?owner_id=${encodeURIComponent(owner_id)}` : "") - ); + const paging = new URLSearchParams(); + if (limit !== void 0) paging.set("limit", String(limit)); + if (offset !== void 0) paging.set("offset", String(offset)); + let res; + if (identity.kind === "portal") { + const qs = paging.toString(); + res = await portalFetch( + env, + identity.portal.session, + `/portal/data/records/by-template/${encodeURIComponent(template)}` + (qs ? `?${qs}` : "") + ); + } else { + const q = new URLSearchParams(paging); + if (owner_id) q.set("owner_id", owner_id); + const qs = q.toString(); + res = await kbdbFetch(env, `/records/by-template/${encodeURIComponent(template)}` + (qs ? `?${qs}` : "")); + } if (!res.ok) { if (identity.kind === "portal") return portalError(res, `\u67E5\u8A62 template\u300C${template}\u300D\u7684 record`); return errorResponse("query_failed", `\u67E5\u8A62 record \u5931\u6557`, [`\u78BA\u8A8D template\u300C${template}\u300D\u5B58\u5728`], await res.text().catch(() => "")); } const data = await res.json(); + const taken = (data.offset ?? 0) + (data.count ?? 0); + const remaining = typeof data.total === "number" ? data.total - taken : 0; return successResponse(data, [ + ...remaining > 0 ? [ + `\u26A0\uFE0F \u9019\u4E0D\u662F\u5168\u90E8\uFF1A\u7E3D\u5171 ${data.total} \u7B46\uFF0C\u4F60\u76EE\u524D\u62FF\u5230\u7B2C ${(data.offset ?? 0) + 1}\u2013${taken} \u7B46\uFF0C\u9084\u6709 ${remaining} \u7B46\u6C92\u53D6\u3002\u8981\u770B\u5168\u5C40\u5C31\u5E36 offset=${taken} \u518D\u53EB\u4E00\u6B21\uFF08\u53EF\u642D limit\uFF0C\u55AE\u6B21\u4E0A\u9650 500\uFF09\u3002` + ] : [], "\u7528 kbdb_get_record(record_id) \u53D6\u55AE\u7B46\u5168\u6587", "\u6309\u95DC\u9375\u5B57\u627E\u5167\u5BB9\u6539\u7528 kbdb_search", ...identity.kind === "portal" ? [OWNER_IGNORED_HINT] : [] @@ -32391,10 +32386,10 @@ function registerAllTools(server, env, orgNamespace, partnerToken, identity) { registerUntagResource(server, env, orgNamespace); registerGetGuiContext(server, env, orgNamespace); registerReportFeedback(server, env, orgNamespace); - registerAllIntrospectionTools(server, env); - registerAllWorkflowCrudTools(server, env); + registerAllIntrospectionTools(server, env, partnerToken); + registerAllWorkflowCrudTools(server, env, partnerToken); registerAllSkillExampleTools(server, env, identity); - registerAllRecipeTools(server, env); + registerAllRecipeTools(server, env, partnerToken); registerAllKbdbDataTools(server, env, identity); registerAllKbdbGraphTools(server, env, orgNamespace, identity); registerAllKbdbMapTools(server, env, identity); diff --git a/daemon/Arcrun-0.18.35.dmg b/daemon/Arcrun-0.18.35.dmg new file mode 100644 index 0000000..880217b Binary files /dev/null and b/daemon/Arcrun-0.18.35.dmg differ diff --git a/daemon/Arcrun-0.18.36.dmg b/daemon/Arcrun-0.18.36.dmg new file mode 100644 index 0000000..210c845 Binary files /dev/null and b/daemon/Arcrun-0.18.36.dmg differ diff --git a/daemon/Arcrun-0.18.36.msix b/daemon/Arcrun-0.18.36.msix new file mode 100644 index 0000000..b672af6 Binary files /dev/null and b/daemon/Arcrun-0.18.36.msix differ diff --git a/daemon/Arcrun-win-0.18.35.exe b/daemon/Arcrun-win-0.18.35.exe new file mode 100755 index 0000000..075593d Binary files /dev/null and b/daemon/Arcrun-win-0.18.35.exe differ diff --git a/daemon/Arcrun-win-0.18.36.exe b/daemon/Arcrun-win-0.18.36.exe new file mode 100755 index 0000000..11e39a1 Binary files /dev/null and b/daemon/Arcrun-win-0.18.36.exe differ diff --git a/manifest.json b/manifest.json index a9d3038..c141494 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "schema": 2, - "built": "2026-08-18", - "source": "Arcrun@1246d92ce791", + "built": "2026-08-24", + "source": "Arcrun@86fc731f375d", "core": [ { "name": "arcrun-array-ops", @@ -198,7 +198,7 @@ "name": "arcrun-cypher-executor", "main_module": "worker.mjs", "main_file": "arcrun-cypher-executor/worker.mjs", - "js_bytes": 593760, + "js_bytes": 608388, "modules": [], "compat_date": "2025-02-19", "compat_flags": [ @@ -239,10 +239,10 @@ "stripped": { "services": 13 }, - "source_commit": "263aa7b71a9ebc02daa53523d744e001042b27e3", - "source_content_sha256": "6076ece723c7a92e35ef902a4e830dfe8de7160cab2d86950f1d92a547f8c3c0", - "sha256": "6076ece723c7a92e35ef902a4e830dfe8de7160cab2d86950f1d92a547f8c3c0", - "bytes": 593760 + "source_commit": "c3efed7d1a01fc171df6eefb67d0bd380781be73", + "source_content_sha256": "9ef47114860d521ed2592cbd84ee855039f8fb120dcd84e9ae4288af6dbbf5af", + "sha256": "9ef47114860d521ed2592cbd84ee855039f8fb120dcd84e9ae4288af6dbbf5af", + "bytes": 608388 }, { "name": "arcrun-date-ops", @@ -396,7 +396,7 @@ "name": "arcrun-kbdb", "main_module": "worker.mjs", "main_file": "arcrun-kbdb/worker.mjs", - "js_bytes": 170840, + "js_bytes": 171380, "modules": [], "compat_date": "2025-02-19", "compat_flags": [ @@ -416,16 +416,16 @@ "ENVIRONMENT": "production" } }, - "source_commit": "466a59c5616fcd61206bc056a7a10052e2e6f3fe", - "source_content_sha256": "30d1ac22d67228cd63e04ebaa94d0d9f40597307075ac5504563590de9876776", - "sha256": "30d1ac22d67228cd63e04ebaa94d0d9f40597307075ac5504563590de9876776", - "bytes": 170840 + "source_commit": "c3efed7d1a01fc171df6eefb67d0bd380781be73", + "source_content_sha256": "64cc54878b6dcc2e0581ed8d97d5d20a0d1441963a3a8ddc704c85f953f786b4", + "sha256": "64cc54878b6dcc2e0581ed8d97d5d20a0d1441963a3a8ddc704c85f953f786b4", + "bytes": 171380 }, { "name": "arcrun-mcp", "main_module": "worker.mjs", "main_file": "arcrun-mcp/worker.mjs", - "js_bytes": 1192612, + "js_bytes": 1193125, "modules": [], "compat_date": "2024-11-27", "compat_flags": [ @@ -440,10 +440,10 @@ "ai": false, "vars": {} }, - "source_commit": "1ccee0055ebdb1f6d74094df3e807f56b849fa6f", - "source_content_sha256": "9bc8dffb34d0f36608a78b2cfeb8ee8b5c793f52b4497f5d4d5f69f70700972e", - "sha256": "9bc8dffb34d0f36608a78b2cfeb8ee8b5c793f52b4497f5d4d5f69f70700972e", - "bytes": 1192612 + "source_commit": "c3efed7d1a01fc171df6eefb67d0bd380781be73", + "source_content_sha256": "e14dedfd630fed6e30f5d5f24a9d9b885899b81f4131ffb463705b151028c579", + "sha256": "e14dedfd630fed6e30f5d5f24a9d9b885899b81f4131ffb463705b151028c579", + "bytes": 1193125 }, { "name": "arcrun-merge", @@ -507,7 +507,7 @@ "name": "arcrun-rag-ui", "main_module": "index.js", "main_file": "tier2/ui/index.js", - "js_bytes": 650313, + "js_bytes": 693162, "modules": [], "compat_date": "2026-07-01", "compat_flags": [], @@ -518,10 +518,10 @@ "ai": false, "vars": {} }, - "source_commit": "a8a029721a229573048a34f4e397d7d33b6431f7", - "source_content_sha256": "dd9b4b41624bfef4cf48dcbe95b98affbebb1480337637efa44d9e355aef34fc", - "sha256": "dd9b4b41624bfef4cf48dcbe95b98affbebb1480337637efa44d9e355aef34fc", - "bytes": 650313 + "source_commit": "5ea36ed7eacb5836fff23a607a688fb76ede8c80", + "source_content_sha256": "4b888c5abbf2fba248e68f0b8dab21d741cefd392f5c63428805af52dac14a47", + "sha256": "4b888c5abbf2fba248e68f0b8dab21d741cefd392f5c63428805af52dac14a47", + "bytes": 693162 }, { "name": "arcrun-set", @@ -699,28 +699,28 @@ } ], "daemon": { - "version": "0.18.34", - "built": "20260820-1123", - "notes": "雲端現在看得到你資料夾的完整層次,不再只有第一層・不經 AI 萃取・空的資料夾目前還不會出現(要等掃描開始記錄資料夾本身);已經同步過的內容不受影響・同一份檔同步到兩台電腦,問答的出處不再只看得到一台", + "version": "0.18.36", + "built": "20260824-1817", + "notes": "移除資料夾時,可以順便把我們放進去的隱藏檔案一起清掉・只刪我們自己建的,你的檔案一個都不會碰・這幾種情況我們刻意不刪・不勾這個框的話,行為跟以前一模一樣:硬碟上一個檔都不會少", "mac": { - "file": "daemon/Arcrun-0.18.34.dmg", - "sha256": "e1ea334ae7a0beba1058dcb70beb12fe7ac34811c82afc061c5629643b3e4e28" + "file": "daemon/Arcrun-0.18.36.dmg", + "sha256": "49ca6f32afff2cff3adb23ad83b3528558655a39ad9e437cae7b52a979975cd7" }, "win": { - "file": "daemon/Arcrun-win-0.18.34.exe", - "sha256": "8b667b8667ae60829a934a5af20aee04af1400292468a4cdc8ffde0c9c0dad3f" + "file": "daemon/Arcrun-win-0.18.36.exe", + "sha256": "7b55648142338b6cdb99022c95c94e12e518846e04999acdc0a382cdd6471913" }, "msix": { - "file": "daemon/Arcrun-0.18.34.msix", - "sha256": "da9a448cdbd25b9ac23668b6d2d1eeafd0a0d7cb66fedc15df065fcbc38cc861" + "file": "daemon/Arcrun-0.18.36.msix", + "sha256": "ee59349c1996d47fc5af65ca458bb3b4218ddc15c5739086c3bf50a6fac8e307" } }, - "release": "1.4.49", + "release": "1.4.51", "built_for": "oauth-installer-lazy-load", "notes": [ "rag_takedown_direct 用了 __CARDS_PREFIX__,但安裝器的代換表沒有它 ⇒ 這個佔位符會原封不動被推進使用者的工作流。" ], - "fingerprint": "b57b239559a6519193717df2526d42eae1ced81cc02c1b8fb66c1173caa7f4b9", + "fingerprint": "444154e7a8ac36644cc32c93d94119f201d0380b636e62183e908273a0cfd669", "library": [ { "name": "arcrun-array-ops", @@ -905,7 +905,7 @@ "name": "arcrun-cypher-executor", "main_module": "worker.mjs", "main_file": "arcrun-cypher-executor/worker.mjs", - "js_bytes": 593760, + "js_bytes": 608388, "modules": [], "compat_date": "2025-02-19", "compat_flags": [ @@ -946,8 +946,8 @@ "stripped": { "services": 13 }, - "source_commit": "263aa7b71a9ebc02daa53523d744e001042b27e3", - "source_content_sha256": "6076ece723c7a92e35ef902a4e830dfe8de7160cab2d86950f1d92a547f8c3c0", + "source_commit": "c3efed7d1a01fc171df6eefb67d0bd380781be73", + "source_content_sha256": "9ef47114860d521ed2592cbd84ee855039f8fb120dcd84e9ae4288af6dbbf5af", "first_install": true }, { @@ -1092,7 +1092,7 @@ "name": "arcrun-kbdb", "main_module": "worker.mjs", "main_file": "arcrun-kbdb/worker.mjs", - "js_bytes": 170840, + "js_bytes": 171380, "modules": [], "compat_date": "2025-02-19", "compat_flags": [ @@ -1112,15 +1112,15 @@ "ENVIRONMENT": "production" } }, - "source_commit": "466a59c5616fcd61206bc056a7a10052e2e6f3fe", - "source_content_sha256": "30d1ac22d67228cd63e04ebaa94d0d9f40597307075ac5504563590de9876776", + "source_commit": "c3efed7d1a01fc171df6eefb67d0bd380781be73", + "source_content_sha256": "64cc54878b6dcc2e0581ed8d97d5d20a0d1441963a3a8ddc704c85f953f786b4", "first_install": true }, { "name": "arcrun-mcp", "main_module": "worker.mjs", "main_file": "arcrun-mcp/worker.mjs", - "js_bytes": 1192612, + "js_bytes": 1193125, "modules": [], "compat_date": "2024-11-27", "compat_flags": [ @@ -1135,8 +1135,8 @@ "ai": false, "vars": {} }, - "source_commit": "1ccee0055ebdb1f6d74094df3e807f56b849fa6f", - "source_content_sha256": "9bc8dffb34d0f36608a78b2cfeb8ee8b5c793f52b4497f5d4d5f69f70700972e", + "source_commit": "c3efed7d1a01fc171df6eefb67d0bd380781be73", + "source_content_sha256": "e14dedfd630fed6e30f5d5f24a9d9b885899b81f4131ffb463705b151028c579", "first_install": true }, { @@ -1197,7 +1197,7 @@ "name": "arcrun-rag-ui", "main_module": "index.js", "main_file": "tier2/ui/index.js", - "js_bytes": 650313, + "js_bytes": 693162, "modules": [], "compat_date": "2026-07-01", "compat_flags": [], @@ -1208,8 +1208,8 @@ "ai": false, "vars": {} }, - "source_commit": "a8a029721a229573048a34f4e397d7d33b6431f7", - "source_content_sha256": "dd9b4b41624bfef4cf48dcbe95b98affbebb1480337637efa44d9e355aef34fc", + "source_commit": "5ea36ed7eacb5836fff23a607a688fb76ede8c80", + "source_content_sha256": "4b888c5abbf2fba248e68f0b8dab21d741cefd392f5c63428805af52dac14a47", "first_install": true }, { diff --git a/tier2/ui/index.js b/tier2/ui/index.js index fbd63ca..6faf4d1 100644 --- a/tier2/ui/index.js +++ b/tier2/ui/index.js @@ -1,6 +1,6 @@ // arcrun-rag-ui — 用戶實例的 GUI(console-ui/public 內嵌;由 Arcrun scripts/build-ui-worker.mjs 產生,勿手改) -const FILES = {"/apple-touch-icon.png":{"type":"image/png","b64":true,"data":"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"},"/console/dashboard/index.html":{"type":"text/html; charset=utf-8","b64":false,"data":"\n\n\n\n\nArcrun 駕駛艙\n\n\n\n\n\n\n
\n
\n
Arcrun 駕駛艙
\n
\n
\n \n
\n
\n
\n
\n
\n
載入中
\n
\n
\n
\n
\n
\n
今日完成
\n
/
\n
\n
\n
\n
收件匣未處理
\n
\n
來自 Telegram
\n
\n
\n
\n
等你的事
\n
載入中…
\n
\n
\n
今日路線
\n \n
本週
\n \n
系統狀況live 健康信號
\n
載入中…
\n
每 60 秒自動刷新
\n 進入完整控制台 ›\n
\n\n\n\n"},"/console/index.html":{"type":"text/html; charset=utf-8","b64":false,"data":"\n\n\n\n\nArcrun Console\n\n\n\n\n\n\n\n\n
\n
\n
\n
Arcrun
\n
私人系統・僅供擁有者進入
\n
\n
\n \n \n \n
\n
\n
這是一個人的智慧總部。
若你不是擁有者,這裡沒有你要找的東西。
\n \n \n
\n
\n\n\n
\n
\n
\n
Arcrun
\n
首次設定
\n
系統偵測到尚未設定擁有者帳號。
設定一組 Email 與密碼,之後只有你能進入。
\n
\n
\n \n \n \n \n
\n
\n \n
\n
\n\n\n
\n
\n
Arcrun
\n \n
總庫搜尋
\n
工作流
\n \n
設定
\n
☾ 切深色
\n
\n
\n
\n\n \n
\n
Arcrun 控制台
\n
\n
\n
\n
\n
\n
載入中
\n
\n
\n
\n
\n
\n
今日完成
\n
/
\n
\n
\n
\n
收件匣未處理
\n
\n
來自 Telegram
\n
\n
\n
\n
等你的事
\n
載入中…
\n
\n
\n
\n
\n
\n 今日路線狀態即時同步\n
\n
  • 載入中…
\n
本週
\n
    \n
    每 60 秒自動刷新
    \n
    \n
    \n
    \n\n \n
    \n
    總庫搜尋
    \n
    \n
    \n
    \n 藏書地圖\n
    \n
    地圖載入中…
    \n
    \n
    \n
    \n \n \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n\n \n
    \n
    \n \n 知識卡片\n
    \n
    \n
    載入中…
    \n
    \n
    關聯視圖
    \n
    \n
    \n
    \n
    \n
    \n\n \n
    \n
    工作流
    \n \n
    載入中…
    \n
    \n
    零件與 Recipes(框架資源)
    \n
    \n \n \n
    \n
    \n
    \n
    \n\n \n
    \n
    憑證管理
    \n
    \n 🛡 保險箱原則:系統只保存目錄與加密後的值,任何頁面都看不到密文內容。只能整筆替換或刪除。\n
    \n
    \n
    新增憑證
    \n
    \n \n \n \n \n
    \n
    \n
    \n
    載入中…
    \n
    \n\n \n
    \n
    分流台全部待辦・80/15/5
    \n
    \n
    載入中…
    \n
    \n\n \n
    \n
    設定
    \n
    \n
    \n
    \n
    \n
    深色模式
    \n
    預設淺色(紙感)。切換立即生效,選擇記在這台裝置(localStorage)。
    \n
    \n
    \n
    \n
    \n
    \n \n
    語意搜尋
    \n
    狀態偵測中…
    \n
    \n
    \n
    \n
    MCP token 有效期(TTL)
    \n
    讀取中…
    \n
    \n 誠實佔位:此頁還不能改這個值——可調功能=Arcrun#19(設定頁改→存 KBDB→發 token 時讀)。實作前要調整請在部署端 env MCP_TOKEN_TTL(mcp worker)設定。\n
    \n
    \n
    \n
    更換帳號密碼
    \n
    需輸入舊密碼驗證身分
    \n
    \n \n \n \n \n
    \n
    \n
    \n \n
    \n
    Portal 帳號密碼救援
    \n
    忘記某個 Portal(RAG 搜尋頁)帳號的密碼,包含你自己那組管理員帳號——不需要先登進 Portal。輸入該帳號的 Email,會產生一組新密碼,只顯示這一次,請立刻抄下並拿去 Portal 登入頁使用。
    \n
    \n \n \n
    \n
    \n
    \n
    \n
    系統資訊
    \n
    載入中…
    \n
    \n \n
    \n
    \n\n
    \n
    \n\n\n
    \n \n
    搜尋
    \n
    工作流
    \n \n
    設定
    \n
    \n\n
    \n\n\n\n\n"},"/favicon.ico":{"type":"image/x-icon","b64":true,"data":"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"},"/favicon.svg":{"type":"image/svg+xml","b64":false,"data":"arcrun icon"},"/index.html":{"type":"text/html; charset=utf-8","b64":false,"data":"\n\n\n\nArcrun RAG\n\n\n\n\n\n\n\n

    正在前往搜尋頁… 沒有自動跳轉請點這裡

    \n\n\n"},"/portal/index.html":{"type":"text/html; charset=utf-8","b64":false,"data":"\n\n\n\n\nArcrun Portal\n\n\n\n\n\n\n\n\n\n\n\n\n\n
    \n
    \n
    \n
    \"arc>run\" width=\"986\" height=\"176\">\"arc>run\" width=\"986\" height=\"176\">
    \n
    知識入口・Portal
    \n
    \n
    \n \n \n \n
    \n
    \n \n
    \n 忘記密碼了?寄一條「修改密碼」連結給我\n
    \n
    \n \n \n
    \n
    \n \n
    \n
    \n\n\n
    \n
    \n
    \n
    設定新密碼
    \n
    \n
    \n
    \n
    回登入頁
    \n
    \n
    \n
    \n\n\n
    \n
    \n
    \n
    \"arc>run\" width=\"986\" height=\"176\">\"arc>run\" width=\"986\" height=\"176\">
    \n
    歡迎!先建立你的帳號
    \n
    \n
    \n \n \n \n \n
    \n \n \n
    \n
    這組帳密就是之後登入知識庫用的,只需要設定這一次。
    \n
    \n
    \n\n\n
    \n
    \n
    \"arcrun\"\"arcrun\"
    \n
    搜尋
    \n
    總圖
    \n
    上傳
    \n
    工作流
    \n
    管理
    \n
    設定
    \n
    ☾ 切深色
    \n
    \n
    \n
    \n\n \n
    \n
    搜尋
    \n
    \n
    \n \n \n
    \n
    \n \n \n \n
    \n \n
    \n
    AI 問答・答案附出處,可回搜尋驗證
    \n
    \n \n \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n
    \n\n \n
    \n
    總圖
    \n
    \n
    \n
    \n 點節點可跳到該實體的圖譜搜尋。這張圖由知識庫的三元組即時算出——同一份地圖的文字版\n (給 AI 注入用的總庫目錄)。\n
    \n
    \n
    \n\n \n
    \n
    \n \n 知識卡片\n
    \n
    載入中…
    \n
    \n\n \n
    \n
    上傳Markdown / 純文字
    \n
    \n 上傳的文件會進入知識庫收件管線:約 1 分鐘後可在搜尋頁找到;AI 整理後會以 wiki 卡形式出現。支援 .md / .txt(一律以 .md 收件)。\n
    \n
    \n
    把 .md / .txt 檔案拖放到這裡
    \n
    \n \n \n
    \n
    \n
    \n\n \n
    \n
    工作流唯讀・系統狀態
    \n
    \n 此頁只顯示系統裡的工作流與最近執行狀態,不能觸發執行(觸發屬系統擁有者權限)。\n
    \n
    載入中…
    \n
    \n\n \n
    \n
    設定
    \n
    \n \n
    \n
    \n
    \n
    \n 版本\n \n
    \n
    查詢中…
    \n
    \n \n
    \n
    \n
    \n
    我的帳號
    \n
    載入中…
    \n
    \n
    \n
    \n
    \n
    深色模式
    \n
    預設淺色(紙感)。切換立即生效,選擇記在這台裝置。
    \n
    \n
    \n
    \n
    \n \n
    \n
    更改密碼
    \n
    需輸入舊密碼驗證身分;新密碼至少 8 碼
    \n
    \n \n \n \n \n
    \n
    \n
    \n \n
    \n \n
    \n 你的知識庫網址(小幫手連線用)\n \n \n
    \n
    同步小幫手
    \n
    把你電腦上的資料夾變成知識庫。換電腦或重裝時可以再下載一次。
    \n
    \n 下載 Mac 版\n
    封測版未簽章,第一次請右鍵→打開。裝好第一次開啟時,貼上這個網址+你的帳號密碼就連上了。
    \n
    \n
    \n \n
    \n
    \n 你的 MCP 網址(給你的 AI 連線用)\n \n \n
    \n
    接上你的 AI(MCP)
    \n
    把上面這串網址貼到 Claude、ChatGPT 等 AI 的「新增自訂連接器」欄位,就能讓你的 AI 直接查這個知識庫。
    \n
    \n \n
    \n
    AI 設定
    \n
    \n 這裡不需要任何設定。聊天問答用的是你自己 Cloudflare 帳號內建的 AI,裝好就能直接問。
    \n 文件整理成知識卡的部分,請在同步小幫手(電腦上的托盤圖示)的「AI 設定…」填一把 Gemini API Key。\n
    \n
    \n \n
    \n
    疑難排解
    \n
    要回報問題,請到你電腦上的 Arcrun(同步小幫手)「版本與更新」頁——那裡的「疑難排解」按一下就能匯出完整診斷檔給我們(只有統計數字,不含你的任何文件內容)。
    \n
    \n \n
    \n
    \n\n \n
    \n
    管理帳號與知識庫授權
    \n\n
    執行紀錄保留期
    \n
    \n
    保留天數
    \n
    執行紀錄是稽核資料,超過保留天數會被每日自動清除;預設 90 天(3 個月),也可設為「不刪除」(企業稽核用途)。
    \n
    \n \n \n \n
    \n
    \n
    \n
    \n\n
    帳號管理
    \n
    \n
    新增同仁帳號
    \n
    建立後會產生一組一次性密碼——只顯示這一次,請當場轉交同仁(同仁可在「設定」自行改密)。
    \n
    \n \n \n \n \n
    \n
    \n
    \n
    \n
    載入中…
    \n\n
    庫目錄管理
    \n
    \n 你的知識庫網址(小幫手連線用)\n \n \n
    \n
    \n \n 裝好同步小幫手並選好要看守的資料夾之後,每個資料夾會自動成為一個「庫」出現在下面——不需要人工新增。下面還是空的,代表小幫手還沒裝好或還沒選資料夾。\n
    \n
    載入中…
    \n
    \n\n
    \n
    \n\n\n
    \n
    搜尋
    \n
    總圖
    \n
    上傳
    \n
    工作流
    \n
    管理
    \n
    設定
    \n
    \n\n
    \n\n\n\n"},"/portal/os-split.test.mjs":{"type":"application/javascript; charset=utf-8","b64":false,"data":"import fs from 'node:fs';\nconst html = fs.readFileSync(new URL('./index.html', import.meta.url).pathname,'utf8');\n// 抽出 daemonPick 相關函式(從 DAEMON_BASE_DEFAULT 到 daemonHint 結尾)\n//\n// 🔴 2026-08-05:結尾標記本來寫死 daemonHint 的**整句文案**,於是同日改 Mac 提示語\n// (zip→DMG 的步驟不同)就讓這支自測直接炸「抽不到函式區塊」,而且沒人發現。\n// ⇒ 改成錨定「函式結束」這個結構,不再綁文案——文案本來就會改,測試不該為此壞掉。\nconst start = html.indexOf('var DAEMON_BASE_DEFAULT');\nconst hintAt = html.indexOf('function daemonHint', start);\nconst endMark = '\\n }';\nconst end = hintAt < 0 ? -1 : html.indexOf(endMark, hintAt) + endMark.length;\nif (start < 0 || hintAt < 0 || end < start) throw new Error('抽不到函式區塊');\nconst src = html.slice(start, end);\n\nconst cases = [\n ['Windows', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120 Safari/537.36'],\n ['Mac', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Safari/605.1.15'],\n ['iPhone', 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 Safari/604.1'],\n ['Linux', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120 Safari/537.36'],\n];\nlet pass=0, fail=0;\nconst chk=(l,c,extra='')=>{ if(c){console.log('PASS:',l);pass++;} else {console.log('FAIL:',l,extra);fail++;} };\n\nfor (const [name, ua] of cases) {\n const fn = new Function('navigator','window', src + '; return {daemonPick:daemonPick, daemonHint:daemonHint, daemonBase:daemonBase};');\n const api = fn({userAgent: ua}, {});\n const d = api.daemonPick();\n const label = d.sure ? d.pick.label : '(兩個都給)';\n const url = d.sure ? d.pick.url : d.mac.url + ' + ' + d.win.url;\n console.log(`\\n[${name}] sure=${d.sure} → ${label}`);\n console.log(` url: ${url}`);\n if (name==='Windows') {\n chk('Windows 給 win zip', d.sure && d.pick.url.endsWith('ArcrunRAG-win-unsigned.zip'), d.pick&&d.pick.url);\n chk('Windows 另一版是 Mac', d.other && d.other.url.endsWith('ArcrunRAG-mac.dmg'));\n chk('Windows 話術提 藍色視窗', api.daemonHint('win').includes('仍要執行'));\n }\n if (name==='Mac') {\n // 2026-08-05:Mac 一律給 DMG(拖進 Applications 的標準安裝畫面),不再給 zip\n // ——zip 解開就是一個裸 .app,使用者會直接在「下載」資料夾雙擊執行,自更新會蓋錯位置。\n chk('Mac 給 dmg(不是 zip)', d.sure && d.pick.url.endsWith('ArcrunRAG-mac.dmg'));\n chk('Mac 另一版是 Windows', d.other && d.other.url.endsWith('win-unsigned.zip'));\n chk('Mac 話術提 右鍵打開', api.daemonHint('mac').includes('右鍵'));\n }\n if (name==='iPhone' || name==='Linux') {\n // iPhone 含 \"Mac OS X\" 但不是桌機 Mac;Linux 兩者皆非 → 都該落在「不確定=兩個都給」\n if (name==='Linux') chk('Linux 判不出來→兩個都給', d.sure===false);\n if (name==='iPhone') chk('iPhone 不該被判成 Mac(手機→兩個都給)', d.sure===false, 'sure='+d.sure);\n }\n}\n// 舊 key 相容\nconst fn2 = new Function('navigator','window', src + '; return daemonBase();');\nconsole.log('\\n[相容] daemonDownload 舊 key →', fn2({userAgent:''},{ARCRUN_CONFIG:{daemonDownload:'https://x.dev/d/ArcrunRAG-mac-unsigned.zip'}}));\nchk('舊 key 推得出目錄', fn2({userAgent:''},{ARCRUN_CONFIG:{daemonDownload:'https://x.dev/d/ArcrunRAG-mac-unsigned.zip'}})==='https://x.dev/d/');\nchk('daemonBase 新 key 優先', fn2({userAgent:''},{ARCRUN_CONFIG:{daemonBase:'https://y.dev/z'}})==='https://y.dev/z/');\nconsole.log(`\\n=== ${pass} passed, ${fail} failed ===`);\nprocess.exit(fail?1:0);\n"},"/portal/retrieval-tree.test.mjs":{"type":"application/javascript; charset=utf-8","b64":false,"data":"// 檢索過程樹(Arcrun#44 第二輪,leo 2026-08-18「希望以樹狀圖來展示」)的資料組裝測試。\n// 只測**樹的資料**,不測 markmap 怎麼畫——畫圖是現成函式庫的事(D97),\n// 我們自己要為之負責的是「層級對不對、有沒有把讀過的東西吞掉」。\n// 跑法:node console-ui/public/portal/retrieval-tree.test.mjs\nimport fs from 'node:fs';\nconst html = fs.readFileSync(new URL('./index.html', import.meta.url).pathname, 'utf8');\n\nconst grab = (name) => {\n const i = html.indexOf(`function ${name}(`);\n if (i < 0) throw new Error(`找不到 ${name}`);\n let d = 0, j = html.indexOf('{', i);\n for (let k = j; k < html.length; k++) { if (html[k] === '{') d++; if (html[k] === '}') { d--; if (!d) return html.slice(i, k + 1); } }\n throw new Error('括號不平衡');\n};\nconst src = ['esc', 'srcLocalPath', 'machineLabelMap', 'srcCrumbs', 'retrievalTreeData', 'visibleLeaves', 'treeListHtml'].map(grab).join('\\n');\nconst fn = new Function(src + '\\nvar SEARCH_ROUTE_LABEL = { graph: \"圖譜命中子庫\", index: \"索引退回選庫\", all: \"全庫讀取\" };'\n + '\\nreturn { srcCrumbs, machineLabelMap, retrievalTreeData, visibleLeaves, treeListHtml };')();\n\nlet pass = 0, fail = 0;\nconst t = (l, c, e = '') => { c ? (console.log('PASS:', l), pass++) : (console.log('FAIL:', l, e), fail++); };\nconst txt = (n) => String(n.content).replace(/<[^>]*>/g, '');\n\n// 2026-08-18 從 youlin stage 實跑 /portal/data/chat 抓下來的真回應形狀(narrative 真的是空字串)\nconst REAL = {\n route: 'graph', vector_used: false, libraries_considered: 8,\n libraries: [{ library: 'testkb', score: 1.69, via: 'graph', matched_entities: ['Arcrun124驗證卡'] }],\n indexes_read: [\n { library: 'general', narrative: '', selected: false, top_entities: [], triplet_count: 0, entry_count: 100 },\n { library: 'testkb', narrative: '', selected: true, top_entities: ['Arcrun124驗證卡'], triplet_count: 4, entry_count: 9 },\n { library: 'kb', narrative: '', selected: false, top_entities: ['knowledge-base'], triplet_count: 7, entry_count: 16 }\n ],\n pages_read: [{ page: 'Arcrun124驗證卡', library: 'testkb', chars: 231, source: 'kb://test/arcrun-124-verify.md#0' }]\n};\n\n// ① 層級=leo 畫的那張:搜尋詞 → 命中子庫 → 索引敘事 → 原文片段\nconst tree = fn.retrievalTreeData('Arcrun124驗證卡是什麼?', REAL);\nt('第 1 層是搜尋詞', txt(tree).indexOf('Arcrun124驗證卡是什麼?') === 0, txt(tree));\nconst lib = tree.children[0];\nt('第 2 層是命中子庫(帶分數與命中實體)', /testkb/.test(txt(lib)) && /1\\.69/.test(txt(lib)) && /命中實體/.test(txt(lib)), txt(lib));\nconst idx = lib.children[0];\nt('第 3 層是該子庫的索引敘事', /三元組/.test(txt(idx)) && /筆條目/.test(txt(idx)), txt(idx));\nconst page = idx.children[0];\nt('第 4 層是原文片段(掛在索引敘事底下)', /Arcrun124驗證卡/.test(txt(page)) && /231 字/.test(txt(page)), txt(page));\nt('原文片段帶資料夾麵包屑', /test › arcrun-124-verify\\.md/.test(txt(page)), txt(page));\n\n// ② narrative 是空字串時不假裝有敘事(線上真資料就是這樣)\nt('沒有敘事就說沒有,不留空白', /索引沒有敘事/.test(txt(idx)), txt(idx));\n\n// ③ 沒被選用的子庫不可以被吞掉,但要收合起來不擠掉主線\nconst rest = tree.children[tree.children.length - 1];\nt('沒讀原文的子庫收在一個節點底下', /考慮過但沒讀原文(2 個子庫)/.test(txt(rest)), txt(rest));\nt('那個節點預設收合', rest.payload && rest.payload.fold === 1, JSON.stringify(rest.payload));\nt('主線只有命中的子庫', tree.children.length === 2, `實得 ${tree.children.length} 條分支`);\n\n// ④ 讀了原文、但沒出現在 libraries[] 的子庫(route=all)也要長出來\nconst allRoute = fn.retrievalTreeData('問句', {\n route: 'all', libraries: [], indexes_read: [],\n pages_read: [{ page: 'P', library: 'kb', chars: 10, source: 'kb://a/b/c.md#2' }]\n});\nt('route=all 時仍有子庫分支', allRoute.children.length === 1 && /kb/.test(txt(allRoute.children[0])), txt(allRoute.children[0]));\nt('沒有索引敘事時不生假的中間層', /^P/.test(txt(allRoute.children[0].children[0])), txt(allRoute.children[0].children[0]));\n\n// ⑤ 完全沒資料時不要生一棵空樹\nconst empty = fn.retrievalTreeData('問句', { route: 'all', libraries: [], indexes_read: [], pages_read: [] });\nt('沒有任何子庫時說明原因', /全庫作答|沒有回報/.test(txt(empty.children[0])), txt(empty.children[0]));\n\n// ⑥ 麵包屑:資料夾可以有很多層,全部拆開;機器那一層現在是「沒有」\nconst c = fn.srcCrumbs('kb://RFP/2026/design.md#3', 'kb');\nt('多層資料夾全部拆開', JSON.stringify(c.dirs) === '[\"RFP\",\"2026\"]', JSON.stringify(c.dirs));\nt('檔名拆得出來', c.file === 'design.md', c.file);\nt('片段編號拆得出來', c.frag === '3', c.frag);\nt('子庫帶得進來', c.library === 'kb', c.library);\nt('機器那一層是 null(資料裡真的沒有)', c.machine === null, String(c.machine));\nt('相對路徑原樣留著(複製給用戶去本機找)', c.path === 'RFP/2026/design.md', c.path);\n\nconst g = fn.srcCrumbs('gitea:Leo/InkStoneCo@wiki/x.md#1', 'kb');\nt('非 kb:// 來源不硬拆成假麵包屑', g.path === '' && g.file === '' && g.dirs.length === 0, JSON.stringify(g));\n\n// ⑦ markmap 載不進來時的退路:同一份樹資料畫成巢狀清單\nconst list = fn.treeListHtml(tree);\nt('退路是巢狀清單', /