diff --git a/README.md b/README.md index 3b0f13b..2d00649 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@cacaa33f7d4e` by `installer/scripts/ship.mjs`(arcrun-rag repo,release 1.4.46,built 2026-08-15)。 +Built from `Arcrun@466a59c5616f` by `installer/scripts/ship.mjs`(arcrun-rag repo,release 1.4.48,built 2026-08-18)。 ⚠️ 這份檔案由出貨管線每次自動重寫(`installer/scripts/render-bundles-readme.mjs`)—— 不要手動改這裡列的零件清單——它是算出來的:公庫=Arcrun 這一版編了什麼, diff --git a/arcrun-cypher-executor/worker.mjs b/arcrun-cypher-executor/worker.mjs index 549b6a2..6235f5d 100644 --- a/arcrun-cypher-executor/worker.mjs +++ b/arcrun-cypher-executor/worker.mjs @@ -3324,7 +3324,7 @@ var init_kbdb_proxy = __esm({ const { base, headers } = kbdbBase(c.env); const params = new URLSearchParams(); params.set("owner_id", owner); - for (const k of ["entry_type", "parent_id", "page_name", "source", "library", "limit", "offset"]) { + for (const k of ["entry_type", "parent_id", "page_name", "source", "library", "exclude_kind", "limit", "offset"]) { const v = c.req.query(k); if (v) params.set(k, v); } @@ -3374,6 +3374,42 @@ var init_kbdb_proxy = __esm({ return c.json({ success: false, error: `KBDB \u4E0D\u53EF\u9054\uFF08${base}\uFF09\uFF1A${e instanceof Error ? e.message : String(e)}` }, 502); } }); + kbdbProxyRouter.put("/kbdb/map/:library/narrative", async (c) => { + if (!tenant(c)) return c.json(NEED_KEY, 401); + const body = await c.req.json().catch(() => null); + const narrative = typeof body?.narrative === "string" ? body.narrative : ""; + if (!narrative.trim()) return c.json({ error: "narrative \u5FC5\u586B\uFF08\u4E0D\u5F97\u7A7A\u767D\uFF09" }, 400); + const owner = (typeof body?.owner_id === "string" ? body.owner_id : c.req.query("owner_id")) || void 0; + const { base, headers } = kbdbBase(c.env); + try { + const res = await fetch(`${base}/map/${encodeURIComponent(c.req.param("library"))}/narrative`, { + method: "PUT", + headers, + body: JSON.stringify({ narrative, ...owner ? { owner_id: owner } : {} }) + }); + return new Response(res.body, { status: res.status, headers: { "Content-Type": "application/json" } }); + } catch (e) { + return c.json({ success: false, error: `KBDB \u4E0D\u53EF\u9054\uFF08${base}\uFF09\uFF1A${e instanceof Error ? e.message : String(e)}` }, 502); + } + }); + kbdbProxyRouter.post("/kbdb/map/recompute", async (c) => { + if (!tenant(c)) return c.json(NEED_KEY, 401); + const body = await c.req.json().catch(() => ({})); + const library = (typeof body.library === "string" ? body.library : c.req.query("library")) || ""; + if (!library.trim()) return c.json({ error: "library \u5FC5\u586B" }, 400); + const owner = (typeof body.owner_id === "string" ? body.owner_id : c.req.query("owner_id")) || void 0; + const { base, headers } = kbdbBase(c.env); + try { + const res = await fetch(`${base}/map/recompute`, { + method: "POST", + headers, + body: JSON.stringify({ ...body, library, ...owner ? { owner_id: owner } : {} }) + }); + return new Response(res.body, { status: res.status, headers: { "Content-Type": "application/json" } }); + } catch (e) { + return c.json({ success: false, error: `KBDB \u4E0D\u53EF\u9054\uFF08${base}\uFF09\uFF1A${e instanceof Error ? e.message : String(e)}` }, 502); + } + }); kbdbProxyRouter.patch("/kbdb/entries/:id", async (c) => { if (!tenant(c)) return c.json(NEED_KEY, 401); const body = await c.req.json().catch(() => ({})); @@ -15323,10 +15359,12 @@ portalDataRouter.get( } const inner = unwrapWorkflowData(result.data, "answer"); const rawSources = Array.isArray(inner.sources) ? inner.sources : []; + const retrieval = inner.retrieval; return c.json({ answer: typeof inner.answer === "string" ? inner.answer : "", sources: dedupeSourcesByPage(rawSources), - graph_facts: inner.graph_facts ?? null + graph_facts: inner.graph_facts ?? null, + retrieval: retrieval && typeof retrieval === "object" ? retrieval : null }); }) ); diff --git a/arcrun-kbdb/worker.mjs b/arcrun-kbdb/worker.mjs index 240eccb..adde495 100644 --- a/arcrun-kbdb/worker.mjs +++ b/arcrun-kbdb/worker.mjs @@ -2119,6 +2119,15 @@ var Hono2 = class extends Hono { } }; +// kbdb/src/actions/library-predicate.ts +var UNLABELLED_LIBRARY = "general"; +function libraryOf(expr) { + return `COALESCE(NULLIF(${expr}, ''), '${UNLABELLED_LIBRARY}')`; +} +var ENTRY_LIBRARY_EXPR = "json_extract(metadata_json, '$.library')"; +var ENTRY_LIBRARY = libraryOf(ENTRY_LIBRARY_EXPR); +var ENTRY_UNLABELLED = `(${ENTRY_LIBRARY_EXPR} IS NULL OR ${ENTRY_LIBRARY_EXPR} = '')`; + // kbdb/src/actions/entry-crud.ts function uid(prefix) { return `${prefix}_${crypto.randomUUID()}`; @@ -2179,6 +2188,11 @@ async function listEntries(db, f = {}) { conds.push(libraryPredicate(f.library)); params.push(...f.library); } + if (f.exclude_kind && f.exclude_kind.length > 0) { + const ph = f.exclude_kind.map(() => "?").join(","); + conds.push(`COALESCE(json_extract(metadata_json, '$.kind'), '') NOT IN (${ph})`); + params.push(...f.exclude_kind); + } if (f.q) { const m = buildContentLike(f.q); conds.push(...m.conds); @@ -2218,7 +2232,7 @@ async function embeddedIdsByLibrary(db, ownerId, library) { const rows = await db.prepare( `SELECT id FROM entries WHERE owner_id = ? - AND COALESCE(NULLIF(json_extract(metadata_json, '$.library'), ''), 'general') = ? + AND ${ENTRY_LIBRARY} = ? AND is_embedded = 1` ).bind(ownerId, library).all(); return (rows.results ?? []).map((r) => r.id); @@ -2230,11 +2244,12 @@ async function markUnembedded(db, ids) { } async function deprecateEntriesByLibrary(db, ownerId, library) { const result = await db.prepare( + // kbdb-sql-ok:牆內本體(kbdb/src/actions/),checkout 開在 worktree ⇒ hook 逐字比對 matrix/arcrun/kbdb/src/ 吃不到,同 887463c/962d863/5919c6b 已記載的假警報,非繞牆 `UPDATE entries SET metadata_json = json_set(COALESCE(metadata_json, '{}'), '$.status', 'deprecated'), updated_at = unixepoch() WHERE owner_id = ? - AND COALESCE(json_extract(metadata_json, '$.library'), 'general') = ? + AND ${ENTRY_LIBRARY} = ? AND (json_extract(metadata_json, '$.status') IS NULL OR json_extract(metadata_json, '$.status') != 'deprecated')` ).bind(ownerId, library).run(); @@ -2423,7 +2438,7 @@ function applyRelativeCut(rows) { } function libraryPredicate(libraries) { const placeholders = libraries.map(() => "?").join(","); - return `COALESCE(json_extract(metadata_json, '$.library'), 'general') IN (${placeholders})`; + return `${ENTRY_LIBRARY} IN (${placeholders})`; } var NOT_DEPRECATED_PREDICATE = "(json_extract(metadata_json, '$.status') IS NULL OR json_extract(metadata_json, '$.status') != 'deprecated')"; function isDeprecatedEntry(entry) { @@ -2641,7 +2656,7 @@ function selectionCriteriaPredicate(opts) { params.push(opts.source); } if (opts.library) { - conds.push("COALESCE(NULLIF(json_extract(metadata_json, '$.library'), ''), 'general') = ?"); + conds.push(`${ENTRY_LIBRARY} = ?`); params.push(opts.library); } if (typeof opts.since === "number") { @@ -2924,9 +2939,7 @@ async function migrateLegacyCredentialsForOwner(db, ownerId) { var MAX_PAGE_NAMES = 300; var HARD_LIMIT_CAP = 500; function criteriaPredicate(c) { - const conds = [ - "(json_extract(metadata_json, '$.library') IS NULL OR json_extract(metadata_json, '$.library') = '')" - ]; + const conds = [ENTRY_UNLABELLED]; const params = []; if (c.owner_id) { conds.push("owner_id = ?"); @@ -3034,7 +3047,7 @@ entryRoutes.post("/", async (c) => { entryRoutes.get("/libraries", async (c) => { const owner = c.req.query("owner_id") || ""; const rows = await c.env.DB.prepare( - `SELECT DISTINCT COALESCE(NULLIF(json_extract(metadata_json, '$.library'), ''), 'general') AS library + `SELECT DISTINCT ${ENTRY_LIBRARY} AS library FROM entries WHERE (?1 = '' OR owner_id = ?1) AND COALESCE(json_extract(metadata_json, '$.status'), '') != 'deprecated' @@ -3047,7 +3060,7 @@ entryRoutes.get("/library-stats", async (c) => { const owner = c.req.query("owner_id") || ""; const rows = await c.env.DB.prepare( `SELECT - COALESCE(NULLIF(json_extract(metadata_json, '$.library'), ''), 'general') AS library, + ${ENTRY_LIBRARY} AS library, COUNT(DISTINCT page_name) AS card_count FROM entries WHERE (?1 = '' OR owner_id = ?1) @@ -3075,6 +3088,12 @@ entryRoutes.get("/", async (c) => { source: c.req.query("source") || void 0, library: parseLibraryParam(c.req.query("library")), q: c.req.query("q") || c.req.query("search") || void 0, + // e.g. 只要使用者的原文、不要地圖自己產的摘要: ?exclude_kind=library_map(Arcrun#44) + // 與軟刪(metadata.status=deprecated)互補、不重疊:軟刪只標「非現役」的舊地圖, + // 而且 listEntries 這條路**根本沒有 deprecated 謂詞**(只有 searchEntries 有) + // ⇒ 對本端點軟刪救不到。這個 filter 連現役那顆一起排除——對「找原文」的呼叫端來說, + // 地圖摘要不管新舊都不是使用者寫的內容。 + exclude_kind: parseLibraryParam(c.req.query("exclude_kind")), limit: c.req.query("limit") ? Number(c.req.query("limit")) : void 0, offset: c.req.query("offset") ? Number(c.req.query("offset")) : void 0 }); @@ -3472,19 +3491,22 @@ 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) { +async function searchByTemplate(db, template, owner_id, limit = 100, offset = 0) { const tpl = await getTemplate(db, template); if (!tpl) return []; - const cap = Math.min(limit, 500); + const cap = Math.min(Math.max(limit, 1), 500); + const skip = Math.max(offset, 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 WHERE rel_id = '${SYS_BELONGS}' AND dst_id = ? AND owner_id = ? - ORDER BY created_at DESC, rowid DESC LIMIT ?` - ).bind(tpl.id, owner_id, cap).all() : await db.prepare( + ORDER BY created_at DESC, rowid DESC LIMIT ? OFFSET ?` + ).bind(tpl.id, owner_id, cap, skip).all() : await db.prepare( + // kbdb-sql-ok:同上(worktree 路徑假警報) `SELECT src_id AS record_id FROM entries WHERE rel_id = '${SYS_BELONGS}' AND dst_id = ? - ORDER BY created_at DESC, rowid DESC LIMIT ?` - ).bind(tpl.id, cap).all(); + 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 []; const byId = /* @__PURE__ */ new Map(); @@ -3561,6 +3583,500 @@ templateRoutes.patch("/:id", async (c) => { return c.json({ success: true, template: tpl }); }); +// kbdb/src/actions/library-map.ts +var LIBRARY_MAP_TEMPLATE_ID = "tpl-library-map"; +var LIBRARY_MAP_TEMPLATE_NAME = "library_map"; +var LIBRARY_MAP_SLOTS = [ + "library", + "narrative", + "top_entities", + "relation_profile", + "bridges", + "triplet_count", + "commit_hash", + "status" +]; +var DEFAULT_TRIPLET_TEMPLATE = "triplet"; +function mapContent(library, narrative, coreNames) { + const core = coreNames.length ? coreNames.join("\u3001") : "\uFF08\u5C1A\u7121 entities\uFF09"; + const text = narrative?.trim(); + if (!text) return `${library}\u3002\u6838\u5FC3\uFF1A${core}`; + const sep = /[。..!!??;;]$/.test(text) ? "" : "\u3002"; + return `${library}\uFF1A${text}${sep}\u6838\u5FC3\uFF1A${core}`; +} +var LIBRARY_MAP_TEMPLATE_DESCRIPTION = "per-library map block\uFF08\u85CF\u66F8\u5730\u5716\uFF1Atop_entities\uFF0Frelation_profile\uFF0Fbridges\uFF0Ftriplet_count \u7531 graph \u6A5F\u68B0\u5C0E\u51FA\uFF1Bnarrative \u7531 LLM \u8457\u4F5C\uFF0C\u7B97\u4E0D\u51FA\u4F86\u3001\u4E1F\u4E86\u56DE\u4E0D\u4F86\uFF1BArcrun#39\uFF0F#44\uFF09"; +async function ensureLibraryMapTemplate(db) { + const existing = await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME); + if (existing) { + if (existing.description !== LIBRARY_MAP_TEMPLATE_DESCRIPTION) { + await updateTemplate(db, existing.id, { description: LIBRARY_MAP_TEMPLATE_DESCRIPTION }); + } + return; + } + try { + await createTemplate(db, { + id: LIBRARY_MAP_TEMPLATE_ID, + name: LIBRARY_MAP_TEMPLATE_NAME, + description: LIBRARY_MAP_TEMPLATE_DESCRIPTION, + slots: LIBRARY_MAP_SLOTS, + created_by: "system" + }); + } catch { + if (!await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME)) throw new Error("ensureLibraryMapTemplate failed"); + } +} +async function ensureTripletLibrarySlot(db, tripletTemplate) { + const tpl = await getTemplate(db, tripletTemplate); + if (!tpl) throw new Error(`triplet template not found: ${tripletTemplate}`); + const slots = JSON.parse(tpl.slots_json); + if (slots.includes("library")) return false; + await updateTemplate(db, tpl.id, { slots: [...slots, "library"] }); + return true; +} +function tripletPivotSql(ownerFiltered) { + return `SELECT b.src_id AS rid, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_subject' THEN v.content END) AS subject, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_object' THEN v.content END) AS object, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_predicate' THEN v.content END) AS predicate, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_status' THEN v.content END) AS status, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_library' THEN v.content END) AS library, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_source_uri' THEN v.content END) AS source_uri + FROM entries b + LEFT JOIN entries r ON r.src_id = b.src_id AND r.rel_id != 'sys_belongs' + LEFT JOIN entries v ON v.id = r.dst_id + WHERE b.rel_id = 'sys_belongs' AND b.dst_id = ?${ownerFiltered ? " AND b.owner_id = ?" : ""} + GROUP BY b.src_id`; +} +function mapPivotSql(ownerFiltered) { + return `SELECT b.src_id AS rid, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_library' THEN v.content END) AS library, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_narrative' THEN v.content END) AS narrative, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_top_entities' THEN v.content END) AS top_entities, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_relation_profile' THEN v.content END) AS relation_profile, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_bridges' THEN v.content END) AS bridges, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_triplet_count' THEN v.content END) AS triplet_count, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_commit_hash' THEN v.content END) AS commit_hash, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_status' THEN v.content END) AS status, + MAX(r.created_at) AS ts + FROM entries b + LEFT JOIN entries r ON r.src_id = b.src_id AND r.rel_id != 'sys_belongs' + LEFT JOIN entries v ON v.id = r.dst_id + WHERE b.rel_id = 'sys_belongs' AND b.dst_id = ?${ownerFiltered ? " AND b.owner_id = ?" : ""} + GROUP BY b.src_id`; +} +function parseJsonArray(raw2) { + if (!raw2) return []; + try { + const v = JSON.parse(raw2); + return Array.isArray(v) ? v : []; + } catch { + return []; + } +} +async function deprecateStaleMapBlocks(db, library, keepId, owner_id) { + const params = [library, keepId]; + if (owner_id) params.push(owner_id); + const res = await db.prepare( + // kbdb-sql-ok:牆內本體(kbdb/src/actions/),worktree 路徑造成的假警報同本檔既有註解 + `UPDATE entries + SET metadata_json = json_set(COALESCE(metadata_json, '{}'), '$.status', 'deprecated'), + updated_at = unixepoch() + WHERE entry_type = 'block' + AND json_extract(metadata_json, '$.kind') = 'library_map' + AND json_extract(metadata_json, '$.library') = ? + AND id != ? + ${owner_id ? "AND owner_id = ?" : ""} + AND (json_extract(metadata_json, '$.status') IS NULL + OR json_extract(metadata_json, '$.status') != 'deprecated')` + ).bind(...params).run(); + return res.meta?.changes ?? 0; +} +async function recomputeLibraryMap(db, input) { + const library = input.library.trim(); + if (!library) throw new Error("library required"); + const tripletTemplateName = input.triplet_template ?? DEFAULT_TRIPLET_TEMPLATE; + const topN = Math.min(Math.max(Math.floor(input.top_n ?? 10), 1), 50); + await ensureLibraryMapTemplate(db); + const librarySlotAdded = await ensureTripletLibrarySlot(db, tripletTemplateName); + const tripletTpl = await getTemplate(db, tripletTemplateName); + if (!tripletTpl) throw new Error(`triplet template not found: ${tripletTemplateName}`); + const owner = input.owner_id || void 0; + const pivot = tripletPivotSql(!!owner); + const pivotParams = owner ? [tripletTpl.id, owner] : [tripletTpl.id]; + const libCond = input.source_prefix ? `(${libraryOf("t.library")} = ? OR (t.library IS NULL AND t.source_uri LIKE ? || '%'))` : `${libraryOf("t.library")} = ?`; + const libParams = input.source_prefix ? [library, input.source_prefix] : [library]; + const withLib = `WITH t AS (${pivot}), lib AS ( + SELECT * FROM t WHERE COALESCE(t.status, 'active') = 'active' AND ${libCond})`; + const baseParams = [...pivotParams, ...libParams]; + const [countRow, topRes, relRes, bridgeRes] = await Promise.all([ + db.prepare(`${withLib} SELECT COUNT(*) AS n FROM lib`).bind(...baseParams).first(), + // degree=entity 在該庫 active triplet 的出現次數(subject+object 兩側都算;同名並列取名字序穩定輸出) + db.prepare( + `${withLib} SELECT name, COUNT(*) AS degree FROM ( + SELECT subject AS name FROM lib UNION ALL SELECT object AS name FROM lib) + WHERE name IS NOT NULL GROUP BY name ORDER BY degree DESC, name ASC LIMIT ?` + ).bind(...baseParams, topN).all(), + // predicate 分布=庫的「性格」(spec §3 relation_profile) + db.prepare( + `${withLib} SELECT predicate, COUNT(*) AS n FROM lib + WHERE predicate IS NOT NULL GROUP BY predicate ORDER BY n DESC, predicate ASC LIMIT 100` + ).bind(...baseParams).all(), + // bridges=本庫 entity 同時出現在其他庫(跨庫 join)。對面那側只能靠 library slot 標記值 + //(source_prefix 只描述本庫的前綴,無法反推他庫)→ M3 backfill 前 bridges 會偏稀疏,誠實現況。 + db.prepare( + `${withLib}, labeled AS ( + SELECT DISTINCT name, library FROM ( + SELECT subject AS name, ${libraryOf("library")} AS library FROM t WHERE COALESCE(status,'active') = 'active' + UNION SELECT object AS name, ${libraryOf("library")} AS library FROM t WHERE COALESCE(status,'active') = 'active') + WHERE name IS NOT NULL AND library != ?), + mine AS ( + SELECT DISTINCT subject AS name FROM lib WHERE subject IS NOT NULL + UNION SELECT DISTINCT object AS name FROM lib WHERE object IS NOT NULL) + SELECT l.name AS entity, l.library AS library FROM labeled l + JOIN mine m ON m.name = l.name ORDER BY l.name ASC, l.library ASC` + ).bind(...baseParams, library).all() + ]); + const tripletCount = countRow?.n ?? 0; + const topEntities = (topRes.results ?? []).map((r) => ({ name: r.name, degree: r.degree })); + const relationProfile = (relRes.results ?? []).map((r) => ({ predicate: r.predicate, count: r.n })); + const bridgeMap = /* @__PURE__ */ new Map(); + for (const r of bridgeRes.results ?? []) { + if (!bridgeMap.has(r.entity) && bridgeMap.size >= 50) continue; + const libs = bridgeMap.get(r.entity) ?? []; + if (!libs.includes(r.library)) libs.push(r.library); + bridgeMap.set(r.entity, libs); + } + const bridges = [...bridgeMap.entries()].map(([entity, libraries]) => ({ entity, libraries })); + let narrative = input.narrative?.trim(); + if (!narrative) { + const prev = await getLibraryMapDetail(db, library, owner); + narrative = prev?.narrative?.trim() || ""; + } + const coreNames = topEntities.slice(0, 3).map((t) => t.name); + const content = mapContent(library, narrative, coreNames); + const blockEntry = await createEntry(db, { + content, + entry_type: "block", + owner_id: owner ?? null, + page_name: `library-map:${library}`, + metadata_json: JSON.stringify({ kind: "library_map", library }) + }); + const values = { + library, + narrative, + top_entities: JSON.stringify(topEntities), + relation_profile: JSON.stringify(relationProfile), + bridges: JSON.stringify(bridges), + triplet_count: String(tripletCount), + status: "active" + }; + if (input.commit_hash) values.commit_hash = input.commit_hash; + await createRecord(db, { + template: LIBRARY_MAP_TEMPLATE_NAME, + record_id: blockEntry.id, + values, + owner_id: owner ?? null + }); + const mapTpl = await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME); + const oldParams = owner ? [mapTpl.id, owner, library, blockEntry.id] : [mapTpl.id, library, blockEntry.id]; + const oldRes = await db.prepare( + `WITH m AS (${mapPivotSql(!!owner)}) + SELECT rid FROM m WHERE m.library = ? AND COALESCE(m.status, 'active') = 'active' AND m.rid != ?` + ).bind(...oldParams).all(); + const superseded = []; + for (const row of oldRes.results ?? []) { + await updateRecord(db, row.rid, { status: "superseded" }); + superseded.push(row.rid); + } + const deprecatedStaleMaps = await deprecateStaleMapBlocks(db, library, blockEntry.id, owner); + const entryCount = (await liveEntryCountsByLibrary(db, owner)).get(library) ?? 0; + return { + map: { + record_id: blockEntry.id, + library, + narrative: narrative || null, + content, + top_entities: topEntities, + relation_profile: relationProfile, + bridges, + triplet_count: tripletCount, + entry_count: entryCount, + commit_hash: input.commit_hash ?? null, + status: "active", + updated_at: blockEntry.created_at + }, + superseded, + deprecated_stale_maps: deprecatedStaleMaps, + triplet_template: tripletTemplateName, + triplet_library_slot_added: librarySlotAdded + }; +} +async function liveTripletCountsByLibrary(db, tripletTemplateId, owner_id) { + const params = owner_id ? [tripletTemplateId, owner_id] : [tripletTemplateId]; + const res = await db.prepare( + // kbdb-sql-ok:牆內本體(kbdb/src/actions/),checkout 開在巢狀 worktree matrix/arcrun/.worktree-fix-87/(避免打斷另一 session 佔用中的 matrix/arcrun 主 checkout),hook 逐字比對 matrix/arcrun/kbdb/src/ 吃不到中間多出的 worktree 目錄層,非繞牆 + `SELECT ${libraryOf("tr.library")} AS library, COUNT(*) AS n + FROM ( + SELECT b.src_id AS rid, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_status' THEN v.content END) AS status, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_library' THEN v.content END) AS library + FROM entries b + LEFT JOIN entries r ON r.src_id = b.src_id AND r.rel_id != 'sys_belongs' + LEFT JOIN entries v ON v.id = r.dst_id + WHERE b.rel_id = 'sys_belongs' AND b.dst_id = ?${owner_id ? " AND b.owner_id = ?" : ""} + GROUP BY b.src_id + ) AS tr + WHERE COALESCE(tr.status, 'active') = 'active' + GROUP BY ${libraryOf("tr.library")}` + ).bind(...params).all(); + const m = /* @__PURE__ */ new Map(); + for (const r of res.results ?? []) m.set(r.library, r.n); + return m; +} +async function tripletCountsByLibrary(db, owner_id, tripletTemplateName = DEFAULT_TRIPLET_TEMPLATE) { + const tpl = await getTemplate(db, tripletTemplateName); + if (!tpl) return /* @__PURE__ */ new Map(); + return liveTripletCountsByLibrary(db, tpl.id, owner_id); +} +async function liveEntryCountsByLibrary(db, owner_id) { + const params = owner_id ? [owner_id] : []; + const res = await db.prepare( + // kbdb-sql-ok:牆內本體(kbdb/src/actions/),checkout 開在巢狀 worktree /private/tmp/wt-arcrun-library-map-honesty-87/(同 962d863/5919c6b 已記載的假警報成因:hook 逐字比對 matrix/arcrun/kbdb/src/ 吃不到中間多出的 worktree 目錄層,非繞牆) + `SELECT ${ENTRY_LIBRARY} AS library, + COUNT(*) AS n + FROM entries + WHERE ${owner_id ? "owner_id = ? AND " : ""}entry_type != 'value' + AND src_id IS NULL + AND entry_type NOT IN ('record', 'sheet', 'field', 'system') + AND NOT (entry_type = 'block' AND COALESCE(json_extract(metadata_json, '$.kind'), '') = 'library_map') + GROUP BY ${ENTRY_LIBRARY}` + ).bind(...params).all(); + const m = /* @__PURE__ */ new Map(); + for (const r of res.results ?? []) m.set(r.library, r.n); + return m; +} +async function knownLibraryNames(db, owner_id) { + const names = /* @__PURE__ */ new Set(); + const entryParams = owner_id ? [owner_id] : []; + const entryRows = await db.prepare( + // 🔴 Arcrun#44(2026-08-17):這裡原本直接回**原始值**,沒有套歸庫規則 + // ⇒ library 是空字串的 entry:SQL 的 IS NOT NULL 放它過、下面那行 `if (r.library)` + // 又把 '' 當 falsy 丟掉 ⇒ 它**兩邊都不算**,一個庫都沒進。 + // 而同一顆 DB 上 entry_count 把它算進 general、`?library=general` 也撈得到它 + // ⇒ 「general 這個庫存不存在」會出現第三個答案(GET /map/general 回 404)。 + // 套上 ENTRY_LIBRARY 之後,'' 跟其他地方一樣正規化成 general。 + // ⚠️ WHERE 的 `IS NOT NULL` 是**刻意保留**的,不是漏掉:本函式問的是「有沒有被蓋過章」 + // (見上面註解的 t52 慣例),從來沒有 library 這個 key 的舊資料不該憑空生出一個庫名。 + // 「標了但標成空的」則是蓋過章,歸 general——這才是本次要對齊的那一格。 + `SELECT DISTINCT ${ENTRY_LIBRARY} AS library FROM entries + WHERE ${owner_id ? "owner_id = ?" : "1=1"} AND ${ENTRY_LIBRARY_EXPR} IS NOT NULL` + ).bind(...entryParams).all(); + for (const r of entryRows.results ?? []) if (r.library) names.add(r.library); + const libTpl = await getTemplate(db, "portal_library"); + if (libTpl) { + const libParams = owner_id ? [libTpl.id, owner_id] : [libTpl.id]; + const libRows = await db.prepare( + `SELECT MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_name' THEN v.content END) AS name + FROM entries b + LEFT JOIN entries r ON r.src_id = b.src_id AND r.rel_id != 'sys_belongs' + LEFT JOIN entries v ON v.id = r.dst_id + WHERE b.rel_id = 'sys_belongs' AND b.dst_id = ?${owner_id ? " AND b.owner_id = ?" : ""} + GROUP BY b.src_id` + ).bind(...libParams).all(); + for (const r of libRows.results ?? []) if (r.name) names.add(r.name); + } + return names; +} +async function ensureFreshLibraryMaps(db, owner_id, tripletTemplateName = DEFAULT_TRIPLET_TEMPLATE) { + const tripletTpl = await getTemplate(db, tripletTemplateName); + if (!tripletTpl) return; + const [liveCounts, cached, known] = await Promise.all([ + liveTripletCountsByLibrary(db, tripletTpl.id, owner_id), + listLibraryMaps(db, owner_id), + knownLibraryNames(db, owner_id) + ]); + const cachedByLib = new Map(cached.map((m) => [m.library, m])); + const stale = /* @__PURE__ */ new Set(); + for (const [library, count] of liveCounts) { + const c = cachedByLib.get(library); + if (!c || c.triplet_count !== count) stale.add(library); + } + for (const name of known) { + if (!liveCounts.has(name) && !cachedByLib.has(name)) stale.add(name); + } + for (const c of cached) { + if (!liveCounts.has(c.library) && c.triplet_count !== 0) stale.add(c.library); + } + await Promise.all( + [...stale].map( + (library) => recomputeLibraryMap(db, { library, owner_id, triplet_template: tripletTemplateName }).catch(() => { + }) + ) + ); +} +async function listLibraryMaps(db, owner_id) { + const tpl = await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME); + if (!tpl) return []; + const params = owner_id ? [tpl.id, owner_id] : [tpl.id]; + const [res, entryCounts] = await Promise.all([ + db.prepare( + // kbdb-sql-ok:牆內本體(kbdb/src/actions/),既有查詢(listLibraryMaps 原本就有)此次改包進 Promise.all 才重新觸發掃描,非新增違規;worktree 路徑假警報同上方 liveEntryCountsByLibrary 註解 + `WITH m AS (${mapPivotSql(!!owner_id)}) + SELECT * FROM m WHERE COALESCE(m.status, 'active') = 'active' AND m.library IS NOT NULL + ORDER BY m.ts DESC` + ).bind(...params).all(), + liveEntryCountsByLibrary(db, owner_id) + ]); + const byLib = /* @__PURE__ */ new Map(); + for (const r of res.results ?? []) { + if (!r.library || byLib.has(r.library)) continue; + byLib.set(r.library, { + library: r.library, + narrative: r.narrative || null, + top_entities: parseJsonArray(r.top_entities).slice(0, 3).map((t) => t.name), + triplet_count: Number(r.triplet_count ?? 0) || 0, + entry_count: entryCounts.get(r.library) ?? 0, + updated_at: r.ts + }); + } + return [...byLib.values()].sort((a, b) => a.library.localeCompare(b.library)); +} +async function getLibraryMapDetail(db, library, owner_id) { + const tpl = await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME); + if (!tpl) return null; + const params = owner_id ? [tpl.id, owner_id, library] : [tpl.id, library]; + const row = await db.prepare( + `WITH m AS (${mapPivotSql(!!owner_id)}) + SELECT * FROM m WHERE m.library = ? AND COALESCE(m.status, 'active') = 'active' + ORDER BY m.ts DESC LIMIT 1` + ).bind(...params).first(); + if (!row) return null; + const [blockEntry, entryCounts] = await Promise.all([ + getEntry(db, row.rid), + liveEntryCountsByLibrary(db, owner_id) + ]); + return { + record_id: row.rid, + library, + narrative: row.narrative || null, + content: blockEntry?.content ?? null, + top_entities: parseJsonArray(row.top_entities), + relation_profile: parseJsonArray(row.relation_profile), + bridges: parseJsonArray(row.bridges), + triplet_count: Number(row.triplet_count ?? 0) || 0, + entry_count: entryCounts.get(library) ?? 0, + commit_hash: row.commit_hash || null, + status: row.status ?? "active", + updated_at: row.ts + }; +} +async function setLibraryNarrative(db, library, narrative, owner_id) { + const lib = library.trim(); + if (!lib) throw new Error("library required"); + const text = narrative.trim(); + if (!text) throw new Error("narrative required"); + const current = await getLibraryMapDetail(db, lib, owner_id); + if (!current) { + const created = await recomputeLibraryMap(db, { library: lib, narrative: text, owner_id }); + return created.map; + } + await updateRecord(db, current.record_id, { narrative: text }); + await updateEntry(db, current.record_id, { + content: mapContent(lib, text, current.top_entities.slice(0, 3).map((t) => t.name)) + }); + const after = await getLibraryMapDetail(db, lib, owner_id); + if (!after) throw new Error("narrative written but map not readable back"); + return after; +} +function questionTokens(question) { + const t = question.normalize("NFKC").toLowerCase(); + const out = /* @__PURE__ */ new Set(); + for (const w of t.match(/[a-z0-9]{2,}/g) ?? []) out.add(w); + for (const seg of t.replace(/[^一-鿿]/g, " ").split(/\s+/)) { + for (let i = 0; i + 1 < seg.length; i++) out.add(seg.slice(i, i + 2)); + } + return out; +} +async function selectLibrariesForQuestion(db, question, opts = {}) { + const q = question.trim(); + if (!q) throw new Error("question required"); + const owner = opts.owner_id || void 0; + const maxLibs = Math.min(Math.max(Math.floor(opts.limit ?? 3), 1), 10); + const qNorm = q.normalize("NFKC").toLowerCase(); + const allMaps = await listLibraryMaps(db, owner); + const considered = allMaps.length; + const tpl = await getTemplate(db, opts.triplet_template ?? DEFAULT_TRIPLET_TEMPLATE); + const hits = /* @__PURE__ */ new Map(); + if (tpl) { + const pivot = tripletPivotSql(!!owner); + const params = owner ? [tpl.id, owner] : [tpl.id]; + const res = await db.prepare( + // kbdb-sql-ok:牆內本體(kbdb/src/actions/)。worktree 開在 /private/tmp/…/wt-gfr44/,hook 逐字比對 matrix/arcrun/kbdb/src/ 吃不到多出的目錄層——與 962d863/5919c6b/887463c 記載的是同一個假警報,非繞牆。 + `WITH t AS (${pivot}), + act AS (SELECT * FROM t WHERE COALESCE(t.status, 'active') = 'active'), + ent AS ( + SELECT subject AS name, ${libraryOf("library")} AS library FROM act WHERE subject IS NOT NULL + UNION ALL + SELECT object AS name, ${libraryOf("library")} AS library FROM act WHERE object IS NOT NULL) + SELECT name, library, COUNT(*) AS degree + FROM ent + WHERE LENGTH(name) >= 2 AND instr(?, lower(name)) > 0 + GROUP BY name, library + ORDER BY degree DESC, name ASC + LIMIT 200` + ).bind(...params, qNorm).all(); + for (const r of res.results ?? []) { + const cur = hits.get(r.library) ?? { entities: /* @__PURE__ */ new Set(), score: 0 }; + cur.entities.add(r.name); + cur.score += 1 + Math.log(1 + r.degree); + hits.set(r.library, cur); + } + } + let libraries = [...hits.entries()].map(([library, v]) => ({ + library, + score: Math.round(v.score * 100) / 100, + matched_entities: [...v.entities].slice(0, 8), + via: "graph" + })).sort((a, b) => b.score - a.score || a.library.localeCompare(b.library)).slice(0, maxLibs); + let route = libraries.length ? "graph" : "all"; + if (!libraries.length && considered) { + const qToks = questionTokens(q); + const scored = allMaps.map((m) => { + const matched = []; + let score = 0; + for (const name of [m.library, ...m.top_entities]) { + const n = name.normalize("NFKC").toLowerCase(); + if (n.length >= 2 && qNorm.includes(n)) { + matched.push(name); + score += 2; + } + } + if (m.narrative) { + const nToks = questionTokens(m.narrative); + let overlap = 0; + for (const t of qToks) if (nToks.has(t)) overlap += 1; + score += Math.min(overlap, 6) * 0.25; + } + return { library: m.library, score: Math.round(score * 100) / 100, matched_entities: matched, via: "index" }; + }).filter((r) => r.score > 0).sort((a, b) => b.score - a.score || a.library.localeCompare(b.library)).slice(0, maxLibs); + if (scored.length) { + libraries = scored; + route = "index"; + } + } + const selected = new Set(libraries.map((l) => l.library)); + return { + route, + vector_used: false, + // 檢索路徑上沒有向量——這個欄位是給前端徽章的證據,不是宣傳詞 + libraries_considered: considered, + libraries, + // 索引整份回傳(不只選中的那幾個):它就是「館藏目錄」, + // 「知識庫裡有哪些子庫、各自談什麼」這類問題的答案在這裡,不在任何一頁原文裡。 + indexes: allMaps.map((m) => ({ ...m, selected: selected.has(m.library) })) + }; +} + // kbdb/src/routes/records.ts var recordRoutes = new Hono2(); var isStringMap = (v) => !!v && typeof v === "object" && !Array.isArray(v) && Object.values(v).every((x) => typeof x === "string"); @@ -3583,25 +4099,28 @@ recordRoutes.post("/", async (c) => { } }); recordRoutes.get("/triplet-stats", async (c) => { - const owner = c.req.query("owner_id") || ""; - const rows = await c.env.DB.prepare( - `SELECT - COALESCE(NULLIF(lib_v.content, ''), 'general') AS library, - COUNT(*) AS triplet_count - FROM entries b - JOIN templates t ON b.dst_id = t.id AND t.name = 'triplet' - LEFT JOIN entries lr ON lr.src_id = b.src_id AND lr.rel_id = ('fld_' || b.dst_id || '_library') - LEFT JOIN entries lib_v ON lib_v.id = lr.dst_id - WHERE b.rel_id = 'sys_belongs' AND (?1 = '' OR b.owner_id = ?1) - GROUP BY COALESCE(NULLIF(lib_v.content, ''), 'general') - ORDER BY library` - ).bind(owner).all(); - const stats = (rows.results ?? []).map((r) => ({ library: r.library, triplet_count: r.triplet_count })); + const owner = c.req.query("owner_id") || void 0; + const counts = await tripletCountsByLibrary(c.env.DB, owner); + const stats = [...counts.entries()].map(([library, triplet_count]) => ({ library, triplet_count })).sort((a, b) => a.library.localeCompare(b.library)); return c.json({ success: true, stats }); }); +var intParam = (raw2, fallback, min, max) => { + const n = Number(raw2); + if (raw2 === void 0 || raw2 === "" || !Number.isFinite(n)) return fallback; + const v = Math.floor(n); + return v < min ? fallback : Math.min(v, max); +}; recordRoutes.get("/by-template/:template", async (c) => { - const records = await searchByTemplate(c.env.DB, c.req.param("template"), c.req.query("owner_id") || void 0); - return c.json({ success: true, records, count: records.length }); + 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( + 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 }); }); recordRoutes.get("/:recordId", async (c) => { const rec = await getRecord(c.env.DB, c.req.param("recordId")); @@ -3735,346 +4254,27 @@ embedRoutes.get("/selftest", async (c) => { return c.json({ success: true, ...result }); }); -// kbdb/src/actions/library-map.ts -var LIBRARY_MAP_TEMPLATE_ID = "tpl-library-map"; -var LIBRARY_MAP_TEMPLATE_NAME = "library_map"; -var LIBRARY_MAP_SLOTS = [ - "library", - "narrative", - "top_entities", - "relation_profile", - "bridges", - "triplet_count", - "commit_hash", - "status" -]; -var DEFAULT_TRIPLET_TEMPLATE = "triplet"; -async function ensureLibraryMapTemplate(db) { - const existing = await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME); - if (existing) return; - try { - await createTemplate(db, { - id: LIBRARY_MAP_TEMPLATE_ID, - name: LIBRARY_MAP_TEMPLATE_NAME, - description: "per-library map block\uFF08\u85CF\u66F8\u5730\u5716\uFF1Agraph \u6A5F\u68B0\u5C0E\u51FA\uFF0C\u96F6 LLM \u751F\u6210\uFF1BArcrun#39\uFF09", - slots: LIBRARY_MAP_SLOTS, - created_by: "system" - }); - } catch { - if (!await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME)) throw new Error("ensureLibraryMapTemplate failed"); - } -} -async function ensureTripletLibrarySlot(db, tripletTemplate) { - const tpl = await getTemplate(db, tripletTemplate); - if (!tpl) throw new Error(`triplet template not found: ${tripletTemplate}`); - const slots = JSON.parse(tpl.slots_json); - if (slots.includes("library")) return false; - await updateTemplate(db, tpl.id, { slots: [...slots, "library"] }); - return true; -} -function tripletPivotSql(ownerFiltered) { - return `SELECT b.src_id AS rid, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_subject' THEN v.content END) AS subject, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_object' THEN v.content END) AS object, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_predicate' THEN v.content END) AS predicate, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_status' THEN v.content END) AS status, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_library' THEN v.content END) AS library, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_source_uri' THEN v.content END) AS source_uri - FROM entries b - LEFT JOIN entries r ON r.src_id = b.src_id AND r.rel_id != 'sys_belongs' - LEFT JOIN entries v ON v.id = r.dst_id - WHERE b.rel_id = 'sys_belongs' AND b.dst_id = ?${ownerFiltered ? " AND b.owner_id = ?" : ""} - GROUP BY b.src_id`; -} -function mapPivotSql(ownerFiltered) { - return `SELECT b.src_id AS rid, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_library' THEN v.content END) AS library, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_narrative' THEN v.content END) AS narrative, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_top_entities' THEN v.content END) AS top_entities, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_relation_profile' THEN v.content END) AS relation_profile, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_bridges' THEN v.content END) AS bridges, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_triplet_count' THEN v.content END) AS triplet_count, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_commit_hash' THEN v.content END) AS commit_hash, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_status' THEN v.content END) AS status, - MAX(r.created_at) AS ts - FROM entries b - LEFT JOIN entries r ON r.src_id = b.src_id AND r.rel_id != 'sys_belongs' - LEFT JOIN entries v ON v.id = r.dst_id - WHERE b.rel_id = 'sys_belongs' AND b.dst_id = ?${ownerFiltered ? " AND b.owner_id = ?" : ""} - GROUP BY b.src_id`; -} -function parseJsonArray(raw2) { - if (!raw2) return []; - try { - const v = JSON.parse(raw2); - return Array.isArray(v) ? v : []; - } catch { - return []; - } -} -async function recomputeLibraryMap(db, input) { - const library = input.library.trim(); - if (!library) throw new Error("library required"); - const tripletTemplateName = input.triplet_template ?? DEFAULT_TRIPLET_TEMPLATE; - const topN = Math.min(Math.max(Math.floor(input.top_n ?? 10), 1), 50); - await ensureLibraryMapTemplate(db); - const librarySlotAdded = await ensureTripletLibrarySlot(db, tripletTemplateName); - const tripletTpl = await getTemplate(db, tripletTemplateName); - if (!tripletTpl) throw new Error(`triplet template not found: ${tripletTemplateName}`); - const owner = input.owner_id || void 0; - const pivot = tripletPivotSql(!!owner); - const pivotParams = owner ? [tripletTpl.id, owner] : [tripletTpl.id]; - const libCond = input.source_prefix ? `(t.library = ? OR (t.library IS NULL AND t.source_uri LIKE ? || '%'))` : `t.library = ?`; - const libParams = input.source_prefix ? [library, input.source_prefix] : [library]; - const withLib = `WITH t AS (${pivot}), lib AS ( - SELECT * FROM t WHERE COALESCE(t.status, 'active') = 'active' AND ${libCond})`; - const baseParams = [...pivotParams, ...libParams]; - const [countRow, topRes, relRes, bridgeRes] = await Promise.all([ - db.prepare(`${withLib} SELECT COUNT(*) AS n FROM lib`).bind(...baseParams).first(), - // degree=entity 在該庫 active triplet 的出現次數(subject+object 兩側都算;同名並列取名字序穩定輸出) - db.prepare( - `${withLib} SELECT name, COUNT(*) AS degree FROM ( - SELECT subject AS name FROM lib UNION ALL SELECT object AS name FROM lib) - WHERE name IS NOT NULL GROUP BY name ORDER BY degree DESC, name ASC LIMIT ?` - ).bind(...baseParams, topN).all(), - // predicate 分布=庫的「性格」(spec §3 relation_profile) - db.prepare( - `${withLib} SELECT predicate, COUNT(*) AS n FROM lib - WHERE predicate IS NOT NULL GROUP BY predicate ORDER BY n DESC, predicate ASC LIMIT 100` - ).bind(...baseParams).all(), - // bridges=本庫 entity 同時出現在其他庫(跨庫 join)。對面那側只能靠 library slot 標記值 - //(source_prefix 只描述本庫的前綴,無法反推他庫)→ M3 backfill 前 bridges 會偏稀疏,誠實現況。 - db.prepare( - `${withLib}, labeled AS ( - SELECT DISTINCT name, library FROM ( - SELECT subject AS name, library FROM t WHERE COALESCE(status,'active') = 'active' - UNION SELECT object AS name, library FROM t WHERE COALESCE(status,'active') = 'active') - WHERE name IS NOT NULL AND library IS NOT NULL AND library != ?), - mine AS ( - SELECT DISTINCT subject AS name FROM lib WHERE subject IS NOT NULL - UNION SELECT DISTINCT object AS name FROM lib WHERE object IS NOT NULL) - SELECT l.name AS entity, l.library AS library FROM labeled l - JOIN mine m ON m.name = l.name ORDER BY l.name ASC, l.library ASC` - ).bind(...baseParams, library).all() - ]); - const tripletCount = countRow?.n ?? 0; - const topEntities = (topRes.results ?? []).map((r) => ({ name: r.name, degree: r.degree })); - const relationProfile = (relRes.results ?? []).map((r) => ({ predicate: r.predicate, count: r.n })); - const bridgeMap = /* @__PURE__ */ new Map(); - for (const r of bridgeRes.results ?? []) { - if (!bridgeMap.has(r.entity) && bridgeMap.size >= 50) continue; - const libs = bridgeMap.get(r.entity) ?? []; - if (!libs.includes(r.library)) libs.push(r.library); - bridgeMap.set(r.entity, libs); - } - const bridges = [...bridgeMap.entries()].map(([entity, libraries]) => ({ entity, libraries })); - let narrative = input.narrative?.trim(); - if (!narrative) { - const prev = await getLibraryMapDetail(db, library, owner); - narrative = prev?.narrative?.trim() || ""; - } - const coreNames = topEntities.slice(0, 3).map((t) => t.name); - const content = `${library}\uFF1A${narrative || "\uFF08narrative \u5F85 ingest \u88DC\u5BEB\uFF09"}\u3002\u6838\u5FC3\uFF1A${coreNames.length ? coreNames.join("\u3001") : "\uFF08\u5C1A\u7121 entities\uFF09"}`; - const blockEntry = await createEntry(db, { - content, - entry_type: "block", - owner_id: owner ?? null, - page_name: `library-map:${library}`, - metadata_json: JSON.stringify({ kind: "library_map", library }) - }); - const values = { - library, - narrative, - top_entities: JSON.stringify(topEntities), - relation_profile: JSON.stringify(relationProfile), - bridges: JSON.stringify(bridges), - triplet_count: String(tripletCount), - status: "active" - }; - if (input.commit_hash) values.commit_hash = input.commit_hash; - await createRecord(db, { - template: LIBRARY_MAP_TEMPLATE_NAME, - record_id: blockEntry.id, - values, - owner_id: owner ?? null - }); - const mapTpl = await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME); - const oldParams = owner ? [mapTpl.id, owner, library, blockEntry.id] : [mapTpl.id, library, blockEntry.id]; - const oldRes = await db.prepare( - `WITH m AS (${mapPivotSql(!!owner)}) - SELECT rid FROM m WHERE m.library = ? AND COALESCE(m.status, 'active') = 'active' AND m.rid != ?` - ).bind(...oldParams).all(); - const superseded = []; - for (const row of oldRes.results ?? []) { - await updateRecord(db, row.rid, { status: "superseded" }); - superseded.push(row.rid); - } - const entryCount = (await liveEntryCountsByLibrary(db, owner)).get(library) ?? 0; - return { - map: { - record_id: blockEntry.id, - library, - narrative: narrative || null, - content, - top_entities: topEntities, - relation_profile: relationProfile, - bridges, - triplet_count: tripletCount, - entry_count: entryCount, - commit_hash: input.commit_hash ?? null, - status: "active", - updated_at: blockEntry.created_at - }, - superseded, - triplet_template: tripletTemplateName, - triplet_library_slot_added: librarySlotAdded - }; -} -async function liveTripletCountsByLibrary(db, tripletTemplateId, owner_id) { - const params = owner_id ? [tripletTemplateId, owner_id] : [tripletTemplateId]; - const res = await db.prepare( - // kbdb-sql-ok:牆內本體(kbdb/src/actions/),checkout 開在巢狀 worktree matrix/arcrun/.worktree-fix-87/(避免打斷另一 session 佔用中的 matrix/arcrun 主 checkout),hook 逐字比對 matrix/arcrun/kbdb/src/ 吃不到中間多出的 worktree 目錄層,非繞牆 - `SELECT COALESCE(NULLIF(tr.library, ''), 'general') AS library, COUNT(*) AS n - FROM ( - SELECT b.src_id AS rid, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_status' THEN v.content END) AS status, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_library' THEN v.content END) AS library - FROM entries b - LEFT JOIN entries r ON r.src_id = b.src_id AND r.rel_id != 'sys_belongs' - LEFT JOIN entries v ON v.id = r.dst_id - WHERE b.rel_id = 'sys_belongs' AND b.dst_id = ?${owner_id ? " AND b.owner_id = ?" : ""} - GROUP BY b.src_id - ) AS tr - WHERE COALESCE(tr.status, 'active') = 'active' - GROUP BY COALESCE(NULLIF(tr.library, ''), 'general')` - ).bind(...params).all(); - const m = /* @__PURE__ */ new Map(); - for (const r of res.results ?? []) m.set(r.library, r.n); - return m; -} -async function liveEntryCountsByLibrary(db, owner_id) { - const params = owner_id ? [owner_id] : []; - const res = await db.prepare( - // kbdb-sql-ok:牆內本體(kbdb/src/actions/),checkout 開在巢狀 worktree /private/tmp/wt-arcrun-library-map-honesty-87/(同 962d863/5919c6b 已記載的假警報成因:hook 逐字比對 matrix/arcrun/kbdb/src/ 吃不到中間多出的 worktree 目錄層,非繞牆) - `SELECT COALESCE(NULLIF(json_extract(metadata_json, '$.library'), ''), 'general') AS library, - COUNT(*) AS n - FROM entries - WHERE ${owner_id ? "owner_id = ? AND " : ""}entry_type != 'value' - AND src_id IS NULL - AND entry_type NOT IN ('record', 'sheet', 'field', 'system') - AND NOT (entry_type = 'block' AND COALESCE(json_extract(metadata_json, '$.kind'), '') = 'library_map') - GROUP BY COALESCE(NULLIF(json_extract(metadata_json, '$.library'), ''), 'general')` - ).bind(...params).all(); - const m = /* @__PURE__ */ new Map(); - for (const r of res.results ?? []) m.set(r.library, r.n); - return m; -} -async function knownLibraryNames(db, owner_id) { - const names = /* @__PURE__ */ new Set(); - const entryParams = owner_id ? [owner_id] : []; - const entryRows = await db.prepare( - `SELECT DISTINCT json_extract(metadata_json, '$.library') AS library FROM entries - WHERE ${owner_id ? "owner_id = ?" : "1=1"} AND json_extract(metadata_json, '$.library') IS NOT NULL` - ).bind(...entryParams).all(); - for (const r of entryRows.results ?? []) if (r.library) names.add(r.library); - const libTpl = await getTemplate(db, "portal_library"); - if (libTpl) { - const libParams = owner_id ? [libTpl.id, owner_id] : [libTpl.id]; - const libRows = await db.prepare( - `SELECT MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_name' THEN v.content END) AS name - FROM entries b - LEFT JOIN entries r ON r.src_id = b.src_id AND r.rel_id != 'sys_belongs' - LEFT JOIN entries v ON v.id = r.dst_id - WHERE b.rel_id = 'sys_belongs' AND b.dst_id = ?${owner_id ? " AND b.owner_id = ?" : ""} - GROUP BY b.src_id` - ).bind(...libParams).all(); - for (const r of libRows.results ?? []) if (r.name) names.add(r.name); - } - return names; -} -async function ensureFreshLibraryMaps(db, owner_id, tripletTemplateName = DEFAULT_TRIPLET_TEMPLATE) { - const tripletTpl = await getTemplate(db, tripletTemplateName); - if (!tripletTpl) return; - const [liveCounts, cached, known] = await Promise.all([ - liveTripletCountsByLibrary(db, tripletTpl.id, owner_id), - listLibraryMaps(db, owner_id), - knownLibraryNames(db, owner_id) - ]); - const cachedByLib = new Map(cached.map((m) => [m.library, m])); - const stale = /* @__PURE__ */ new Set(); - for (const [library, count] of liveCounts) { - const c = cachedByLib.get(library); - if (!c || c.triplet_count !== count) stale.add(library); - } - for (const name of known) { - if (!liveCounts.has(name) && !cachedByLib.has(name)) stale.add(name); - } - await Promise.all( - [...stale].map( - (library) => recomputeLibraryMap(db, { library, owner_id, triplet_template: tripletTemplateName }).catch(() => { - }) - ) - ); -} -async function listLibraryMaps(db, owner_id) { - const tpl = await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME); - if (!tpl) return []; - const params = owner_id ? [tpl.id, owner_id] : [tpl.id]; - const [res, entryCounts] = await Promise.all([ - db.prepare( - // kbdb-sql-ok:牆內本體(kbdb/src/actions/),既有查詢(listLibraryMaps 原本就有)此次改包進 Promise.all 才重新觸發掃描,非新增違規;worktree 路徑假警報同上方 liveEntryCountsByLibrary 註解 - `WITH m AS (${mapPivotSql(!!owner_id)}) - SELECT * FROM m WHERE COALESCE(m.status, 'active') = 'active' AND m.library IS NOT NULL - ORDER BY m.ts DESC` - ).bind(...params).all(), - liveEntryCountsByLibrary(db, owner_id) - ]); - const byLib = /* @__PURE__ */ new Map(); - for (const r of res.results ?? []) { - if (!r.library || byLib.has(r.library)) continue; - byLib.set(r.library, { - library: r.library, - narrative: r.narrative || null, - top_entities: parseJsonArray(r.top_entities).slice(0, 3).map((t) => t.name), - triplet_count: Number(r.triplet_count ?? 0) || 0, - entry_count: entryCounts.get(r.library) ?? 0, - updated_at: r.ts - }); - } - return [...byLib.values()].sort((a, b) => a.library.localeCompare(b.library)); -} -async function getLibraryMapDetail(db, library, owner_id) { - const tpl = await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME); - if (!tpl) return null; - const params = owner_id ? [tpl.id, owner_id, library] : [tpl.id, library]; - const row = await db.prepare( - `WITH m AS (${mapPivotSql(!!owner_id)}) - SELECT * FROM m WHERE m.library = ? AND COALESCE(m.status, 'active') = 'active' - ORDER BY m.ts DESC LIMIT 1` - ).bind(...params).first(); - if (!row) return null; - const [blockEntry, entryCounts] = await Promise.all([ - getEntry(db, row.rid), - liveEntryCountsByLibrary(db, owner_id) - ]); - return { - record_id: row.rid, - library, - narrative: row.narrative || null, - content: blockEntry?.content ?? null, - top_entities: parseJsonArray(row.top_entities), - relation_profile: parseJsonArray(row.relation_profile), - bridges: parseJsonArray(row.bridges), - triplet_count: Number(row.triplet_count ?? 0) || 0, - entry_count: entryCounts.get(library) ?? 0, - commit_hash: row.commit_hash || null, - status: row.status ?? "active", - updated_at: row.ts - }; -} - // kbdb/src/routes/map.ts var mapRoutes = new Hono2(); +mapRoutes.get("/select", async (c) => { + const q = c.req.query("q") || c.req.query("question") || ""; + if (!q.trim()) return c.json({ success: false, error: "q required" }, 400); + const owner = c.req.query("owner_id") || void 0; + const limitNum = Number(c.req.query("limit")); + const limit = Number.isFinite(limitNum) && limitNum > 0 ? Math.floor(limitNum) : void 0; + await ensureFreshLibraryMaps(c.env.DB, owner).catch(() => { + }); + try { + const selection = await selectLibrariesForQuestion(c.env.DB, q, { + owner_id: owner, + limit, + triplet_template: c.req.query("triplet_template") || void 0 + }); + return c.json({ success: true, ...selection }); + } catch (e) { + return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 400); + } +}); mapRoutes.post("/recompute", async (c) => { const body = await c.req.json().catch(() => ({})); const library = c.req.query("library") || (typeof body.library === "string" ? body.library : ""); @@ -4094,6 +4294,19 @@ mapRoutes.post("/recompute", async (c) => { return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 400); } }); +mapRoutes.put("/:library/narrative", async (c) => { + const body = await c.req.json().catch(() => ({})); + const library = c.req.param("library"); + const narrative = typeof body.narrative === "string" ? body.narrative : ""; + if (!narrative.trim()) return c.json({ success: false, error: "narrative required" }, 400); + const owner = (typeof body.owner_id === "string" ? body.owner_id : c.req.query("owner_id")) || void 0; + try { + const map = await setLibraryNarrative(c.env.DB, library, narrative, owner); + return c.json({ success: true, map }); + } catch (e) { + return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 400); + } +}); mapRoutes.get("/", async (c) => { const owner = c.req.query("owner_id") || void 0; await ensureFreshLibraryMaps(c.env.DB, owner).catch(() => { diff --git a/manifest.json b/manifest.json index 7fe5b8a..7bdd925 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "schema": 2, - "built": "2026-08-15", - "source": "Arcrun@cacaa33f7d4e", + "built": "2026-08-18", + "source": "Arcrun@466a59c5616f", "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": 590499, + "js_bytes": 592768, "modules": [], "compat_date": "2025-02-19", "compat_flags": [ @@ -239,10 +239,10 @@ "stripped": { "services": 13 }, - "source_commit": "9fa88065abdc5a795d3847711f04101480621af0", - "source_content_sha256": "94a21ad012ee0c48d78e9a74752048917ddb4616f1ea089976834acd887a9d14", - "sha256": "94a21ad012ee0c48d78e9a74752048917ddb4616f1ea089976834acd887a9d14", - "bytes": 590499 + "source_commit": "5d5dc9e3ceadaf40448ff286009e01299623ced1", + "source_content_sha256": "426a77c4566c177ed0f26e2178d7c7f8b989ccd16fdfb1fc446405b08d4bf4ba", + "sha256": "426a77c4566c177ed0f26e2178d7c7f8b989ccd16fdfb1fc446405b08d4bf4ba", + "bytes": 592768 }, { "name": "arcrun-date-ops", @@ -396,7 +396,7 @@ "name": "arcrun-kbdb", "main_module": "worker.mjs", "main_file": "arcrun-kbdb/worker.mjs", - "js_bytes": 160306, + "js_bytes": 170840, "modules": [], "compat_date": "2025-02-19", "compat_flags": [ @@ -416,10 +416,10 @@ "ENVIRONMENT": "production" } }, - "source_commit": "ceb7638d746f0e8847552aef721f6f53ef87ff0b", - "source_content_sha256": "47b09d60f8d122f7ec234d146a99bc68b2661c56ad6a98eca146d99ec5de03ea", - "sha256": "47b09d60f8d122f7ec234d146a99bc68b2661c56ad6a98eca146d99ec5de03ea", - "bytes": 160306 + "source_commit": "466a59c5616fcd61206bc056a7a10052e2e6f3fe", + "source_content_sha256": "30d1ac22d67228cd63e04ebaa94d0d9f40597307075ac5504563590de9876776", + "sha256": "30d1ac22d67228cd63e04ebaa94d0d9f40597307075ac5504563590de9876776", + "bytes": 170840 }, { "name": "arcrun-mcp", @@ -507,7 +507,7 @@ "name": "arcrun-rag-ui", "main_module": "index.js", "main_file": "tier2/ui/index.js", - "js_bytes": 524513, + "js_bytes": 531477, "modules": [], "compat_date": "2026-07-01", "compat_flags": [], @@ -518,10 +518,10 @@ "ai": false, "vars": {} }, - "source_commit": "614fe448124dbd25d8228e37723473f1704f3a7a", - "source_content_sha256": "5825f427e4211dd4b0eee969c94c8bfa856f65b8a48fc718e137f3b5d074e1ff", - "sha256": "5825f427e4211dd4b0eee969c94c8bfa856f65b8a48fc718e137f3b5d074e1ff", - "bytes": 524513 + "source_commit": "b48f02b242a094cd542e5d9349326b9127a006eb", + "source_content_sha256": "374dec4544348df364dc4f09c40944529f8b18cd4c42687fc318642f638b0860", + "sha256": "374dec4544348df364dc4f09c40944529f8b18cd4c42687fc318642f638b0860", + "bytes": 531477 }, { "name": "arcrun-set", @@ -716,12 +716,12 @@ "size": 9125072 } }, - "release": "1.4.46", + "release": "1.4.48", "built_for": "oauth-installer-lazy-load", "notes": [ "rag_takedown_direct 用了 __CARDS_PREFIX__,但安裝器的代換表沒有它 ⇒ 這個佔位符會原封不動被推進使用者的工作流。" ], - "fingerprint": "ba0cc9bbd58980b4a4505c365382455b307b826fad0afbc1b738f4139ee96d2c", + "fingerprint": "b5f21ff6b7a7f236be14381646075dbe491ba52e09c584cc73d6efeb5ddda642", "library": [ { "name": "arcrun-array-ops", @@ -906,7 +906,7 @@ "name": "arcrun-cypher-executor", "main_module": "worker.mjs", "main_file": "arcrun-cypher-executor/worker.mjs", - "js_bytes": 590499, + "js_bytes": 592768, "modules": [], "compat_date": "2025-02-19", "compat_flags": [ @@ -947,8 +947,8 @@ "stripped": { "services": 13 }, - "source_commit": "9fa88065abdc5a795d3847711f04101480621af0", - "source_content_sha256": "94a21ad012ee0c48d78e9a74752048917ddb4616f1ea089976834acd887a9d14", + "source_commit": "5d5dc9e3ceadaf40448ff286009e01299623ced1", + "source_content_sha256": "426a77c4566c177ed0f26e2178d7c7f8b989ccd16fdfb1fc446405b08d4bf4ba", "first_install": true }, { @@ -1093,7 +1093,7 @@ "name": "arcrun-kbdb", "main_module": "worker.mjs", "main_file": "arcrun-kbdb/worker.mjs", - "js_bytes": 160306, + "js_bytes": 170840, "modules": [], "compat_date": "2025-02-19", "compat_flags": [ @@ -1113,8 +1113,8 @@ "ENVIRONMENT": "production" } }, - "source_commit": "ceb7638d746f0e8847552aef721f6f53ef87ff0b", - "source_content_sha256": "47b09d60f8d122f7ec234d146a99bc68b2661c56ad6a98eca146d99ec5de03ea", + "source_commit": "466a59c5616fcd61206bc056a7a10052e2e6f3fe", + "source_content_sha256": "30d1ac22d67228cd63e04ebaa94d0d9f40597307075ac5504563590de9876776", "first_install": true }, { @@ -1198,7 +1198,7 @@ "name": "arcrun-rag-ui", "main_module": "index.js", "main_file": "tier2/ui/index.js", - "js_bytes": 524513, + "js_bytes": 531477, "modules": [], "compat_date": "2026-07-01", "compat_flags": [], @@ -1209,8 +1209,8 @@ "ai": false, "vars": {} }, - "source_commit": "614fe448124dbd25d8228e37723473f1704f3a7a", - "source_content_sha256": "5825f427e4211dd4b0eee969c94c8bfa856f65b8a48fc718e137f3b5d074e1ff", + "source_commit": "b48f02b242a094cd542e5d9349326b9127a006eb", + "source_content_sha256": "374dec4544348df364dc4f09c40944529f8b18cd4c42687fc318642f638b0860", "first_install": true }, { @@ -1411,27 +1411,15 @@ }, { "worker": "arcrun-http-request", - "why": "rag_chat/kw_search 用 __HTTP_REQ_URL__" + "why": "rag_chat/select_libraries 用 __HTTP_REQ_URL__" + }, + { + "worker": "arcrun-code", + "why": "rag_chat/pick_libs 用 __CODE_URL__" }, { "worker": "arcrun-http-request", - "why": "rag_chat/sem_search 用 __HTTP_REQ_URL__" - }, - { - "worker": "arcrun-http-request", - "why": "rag_chat/fetch_triplets 用 __HTTP_REQ_URL__" - }, - { - "worker": "arcrun-http-request", - "why": "rag_chat/fetch_blocks_a 用 __HTTP_REQ_URL__" - }, - { - "worker": "arcrun-http-request", - "why": "rag_chat/fetch_blocks_b 用 __HTTP_REQ_URL__" - }, - { - "worker": "arcrun-http-request", - "why": "rag_chat/fetch_blocks_c 用 __HTTP_REQ_URL__" + "why": "rag_chat/fetch_pages 用 __HTTP_REQ_URL__" }, { "worker": "arcrun-code", diff --git a/tier2/ui/index.js b/tier2/ui/index.js index 0b6daaf..945aba1 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
    \"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/safejson.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\n// 抽出 safeJson 與 friendlyErr 求值\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{c?(console.log('PASS:',l),pass++):(console.log('FAIL:',l,e),fail++)};\n\n// ① safeJson:非 JSON 不可拋例外(同事撞到的 404 HTML 頁)\nconst html404 = '404 Not Found';\nawait fn.safeJson({ text: () => Promise.resolve(html404) })\n .then(d => t('404 HTML → 回空物件不拋錯', typeof d === 'object' && d !== null))\n .catch(e => t('404 HTML → 不該拋錯', false, e.message));\n\nawait fn.safeJson({ text: () => Promise.resolve('') })\n .then(d => t('空回應 → 回空物件', JSON.stringify(d)==='{}'))\n .catch(() => t('空回應 → 不該拋錯', false));\n\nawait fn.safeJson({ text: () => Promise.resolve('{\"error\":\"帳號或密碼不對\"}') })\n .then(d => t('正常 JSON 仍要解析得出來', d.error === '帳號或密碼不對'), )\n .catch(() => t('正常 JSON 不該拋錯', false));\n\n// ② friendlyErr:不可把技術訊息噴給使用者\nconst leak = fn.friendlyErr(new Error('Unexpected non-whitespace character after JSON at position 4'));\nt('JSON 錯誤 → 不外洩原文', !/JSON|position/i.test(leak), `實得: ${leak}`);\nt('JSON 錯誤 → 說人話', /伺服器回應異常/.test(leak), `實得: ${leak}`);\n\nconst net = fn.friendlyErr(new Error('Failed to fetch'));\nt('網路錯誤 → 既有訊息保留', /連線中斷/.test(net), `實得: ${net}`);\n\nconst ours = fn.friendlyErr(new Error('帳號或密碼不對——用你在知識庫網站設定的那組'));\nt('我們自己的中文訊息 → 原樣顯示', /帳號或密碼不對/.test(ours), `實得: ${ours}`);\n\nconst stack = fn.friendlyErr(new Error('TypeError: Cannot read properties of undefined'));\nt('英文技術訊息 → 收斂不外洩', !/TypeError|undefined/.test(stack), `實得: ${stack}`);\n\nconsole.log(`\\n=== ${pass} passed, ${fail} failed ===`);\nprocess.exit(fail?1:0);\n"}}; -const UI_FINGERPRINT = "82bc636990e60172663f7ce90c75d8b434193b8532b21f1d4b57c5fb330cbe11"; +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
      \"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/safejson.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\n// 抽出 safeJson 與 friendlyErr 求值\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{c?(console.log('PASS:',l),pass++):(console.log('FAIL:',l,e),fail++)};\n\n// ① safeJson:非 JSON 不可拋例外(同事撞到的 404 HTML 頁)\nconst html404 = '404 Not Found';\nawait fn.safeJson({ text: () => Promise.resolve(html404) })\n .then(d => t('404 HTML → 回空物件不拋錯', typeof d === 'object' && d !== null))\n .catch(e => t('404 HTML → 不該拋錯', false, e.message));\n\nawait fn.safeJson({ text: () => Promise.resolve('') })\n .then(d => t('空回應 → 回空物件', JSON.stringify(d)==='{}'))\n .catch(() => t('空回應 → 不該拋錯', false));\n\nawait fn.safeJson({ text: () => Promise.resolve('{\"error\":\"帳號或密碼不對\"}') })\n .then(d => t('正常 JSON 仍要解析得出來', d.error === '帳號或密碼不對'), )\n .catch(() => t('正常 JSON 不該拋錯', false));\n\n// ② friendlyErr:不可把技術訊息噴給使用者\nconst leak = fn.friendlyErr(new Error('Unexpected non-whitespace character after JSON at position 4'));\nt('JSON 錯誤 → 不外洩原文', !/JSON|position/i.test(leak), `實得: ${leak}`);\nt('JSON 錯誤 → 說人話', /伺服器回應異常/.test(leak), `實得: ${leak}`);\n\nconst net = fn.friendlyErr(new Error('Failed to fetch'));\nt('網路錯誤 → 既有訊息保留', /連線中斷/.test(net), `實得: ${net}`);\n\nconst ours = fn.friendlyErr(new Error('帳號或密碼不對——用你在知識庫網站設定的那組'));\nt('我們自己的中文訊息 → 原樣顯示', /帳號或密碼不對/.test(ours), `實得: ${ours}`);\n\nconst stack = fn.friendlyErr(new Error('TypeError: Cannot read properties of undefined'));\nt('英文技術訊息 → 收斂不外洩', !/TypeError|undefined/.test(stack), `實得: ${stack}`);\n\nconsole.log(`\\n=== ${pass} passed, ${fail} failed ===`);\nprocess.exit(fail?1:0);\n"}}; +const UI_FINGERPRINT = "52b7f8201b9b05e1eecea7056b4d619ab9452000ce19926f5b47bb86b8945006"; function b64ToBytes(b64) { const bin = atob(b64); const a = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) a[i] = bin.charCodeAt(i); return a; } export default { async fetch(request, env) {