重編出貨成品——kbdb 成品終於裝的是 v7,不是舊模型
出貨管線 preflight 擋下:kbdb 的成品比源碼舊 3 顆 commitceb7638樹狀 record 模型第一刀(entry_values 拆表,v7 定稿實作)aa49b4e刪 record 不再刪掉別人還指著的 entry(Arcrun#128 配套)5b22d56createRecord 的 slot 可指向既有 entry(Arcrun#128) 🔴 這道閘剛好攔住了今晚整件事的意義被抹掉:成品沒重編就出貨, bundle 裡裝的是舊 kbdb ⇒ youlin 依然「資料新、程式舊」, 而版本號會照跳,看起來像出貨成功。 重編 23/23,實際變動只有 3 個檔(kbdb/cypher-executor/manifest)—— 其餘 20 顆內容指紋沒變、不虛增版本,指紋機制運作正常。 arcrun-kbdb 現在 source=ceb7638d、sha256=47b09d60f8d1。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3243,15 +3243,20 @@ var init_kbdb_proxy = __esm({
|
||||
const owner = tenant(c);
|
||||
if (!owner) return c.json(NEED_KEY, 401);
|
||||
const body = await c.req.json().catch(() => null);
|
||||
if (!body || !body.template || !body.values) {
|
||||
return c.json({ error: "template \u8207 values \u5FC5\u586B" }, 400);
|
||||
if (!body || !body.template || !body.values && !body.entry_ids) {
|
||||
return c.json({ error: "template \u5FC5\u586B\uFF0Cvalues \u8207 entry_ids \u81F3\u5C11\u8981\u6709\u4E00\u500B" }, 400);
|
||||
}
|
||||
const { base, headers } = kbdbBase(c.env);
|
||||
const res = await fetch(`${base}/records`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
// 強制以租戶身份隔離:忽略 caller 自帶 owner_id,一律用 header 身份(防跨租戶寫入)
|
||||
body: JSON.stringify({ template: body.template, values: body.values, owner_id: owner })
|
||||
body: JSON.stringify({
|
||||
template: body.template,
|
||||
...body.values ? { values: body.values } : {},
|
||||
...body.entry_ids ? { entry_ids: body.entry_ids } : {},
|
||||
owner_id: owner
|
||||
})
|
||||
});
|
||||
return new Response(res.body, { status: res.status, headers: { "Content-Type": "application/json" } });
|
||||
});
|
||||
@@ -13708,12 +13713,14 @@ portalRouter.post(
|
||||
const body = await c.req.json().catch(() => null);
|
||||
const pageName = String(body?.page_name ?? "").trim();
|
||||
const srcText = String(body?.text ?? "");
|
||||
if (!pageName || !srcText.trim()) return c.json({ error: "page_name \u8207 text \u5FC5\u586B" }, 400);
|
||||
const daemonPrompt = String(body?.prompt ?? "").trim();
|
||||
if (!daemonPrompt && (!pageName || !srcText.trim()))
|
||||
return c.json({ error: "page_name \u8207 text \u5FC5\u586B" }, 400);
|
||||
if (!c.env.AI) {
|
||||
return c.json({ error: "\u9019\u500B\u90E8\u7F72\u6C92\u6709\u7D81\u5B9A Workers AI\uFF08wrangler.toml \u9700\u6709 [ai] binding\uFF09\uFF0C\u8ACB\u66F4\u65B0\u77E5\u8B58\u5EAB\u7248\u672C" }, 501);
|
||||
}
|
||||
const REL = ">".repeat(2);
|
||||
const prompt = `\u628A\u4EE5\u4E0B\u539F\u7A3F\u91CD\u5BEB\u6210\u5B9A\u7A3F\u77E5\u8B58\u5361\uFF08\u6B63\u9AD4\u4E2D\u6587\uFF09\u3002\u76F4\u63A5\u8F38\u51FA\u5361\u7247\u672C\u8EAB\uFF1A\u7B2C\u4E00\u884C\u5FC5\u9808\u662F\u300C# ${pageName}\u300D\uFF0C\u4E0D\u8981\u4EFB\u4F55\u524D\u8A00\u3001\u601D\u8003\u904E\u7A0B\u3001\u82F1\u6587\u8349\u7A3F\u6216\u8AAA\u660E\u3002\u683C\u5F0F\uFF1A
|
||||
const prompt = daemonPrompt || `\u628A\u4EE5\u4E0B\u539F\u7A3F\u91CD\u5BEB\u6210\u5B9A\u7A3F\u77E5\u8B58\u5361\uFF08\u6B63\u9AD4\u4E2D\u6587\uFF09\u3002\u76F4\u63A5\u8F38\u51FA\u5361\u7247\u672C\u8EAB\uFF1A\u7B2C\u4E00\u884C\u5FC5\u9808\u662F\u300C# ${pageName}\u300D\uFF0C\u4E0D\u8981\u4EFB\u4F55\u524D\u8A00\u3001\u601D\u8003\u904E\u7A0B\u3001\u82F1\u6587\u8349\u7A3F\u6216\u8AAA\u660E\u3002\u683C\u5F0F\uFF1A
|
||||
# ${pageName}
|
||||
## \u4E00\u53E5\u8A71\u5B9A\u7FA9
|
||||
\uFF08\u4E00\u884C\uFF09
|
||||
@@ -13729,14 +13736,17 @@ ${srcText}`;
|
||||
try {
|
||||
const out = await c.env.AI.run("@cf/meta/llama-4-scout-17b-16e-instruct", {
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
max_tokens: 2048,
|
||||
max_tokens: daemonPrompt ? 8192 : 2048,
|
||||
temperature: 0.2
|
||||
});
|
||||
const card = String(out?.response ?? "").trim();
|
||||
if (!card) return c.json({ error: "Workers AI \u6C92\u6709\u56DE\u50B3\u5167\u5BB9" }, 502);
|
||||
const raw2 = String(out?.response ?? "").trim();
|
||||
if (!raw2) return c.json({ error: "Workers AI \u6C92\u6709\u56DE\u50B3\u5167\u5BB9" }, 502);
|
||||
if (daemonPrompt) {
|
||||
return c.json({ success: true, output: raw2 });
|
||||
}
|
||||
const marker = `# ${pageName}`;
|
||||
const idx = card.lastIndexOf(marker);
|
||||
return c.json({ success: true, card: (idx >= 0 ? card.slice(idx) : card).trim() + "\n" });
|
||||
const idx = raw2.lastIndexOf(marker);
|
||||
return c.json({ success: true, card: (idx >= 0 ? raw2.slice(idx) : raw2).trim() + "\n" });
|
||||
} catch (e) {
|
||||
return c.json({ error: `Workers AI \u57F7\u884C\u5931\u6557\uFF1A${e instanceof Error ? e.message : String(e)}` }, 502);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,51 @@
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __esm = (fn, res, err) => function __init() {
|
||||
if (err) throw err[0];
|
||||
try {
|
||||
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
||||
} catch (e) {
|
||||
throw err = [e], e;
|
||||
}
|
||||
};
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
|
||||
// kbdb/src/actions/relation-orphans.ts
|
||||
var relation_orphans_exports = {};
|
||||
__export(relation_orphans_exports, {
|
||||
scanRelationOrphans: () => scanRelationOrphans
|
||||
});
|
||||
async function scanRelationOrphans(db, limit = 200) {
|
||||
const cap = Math.min(Math.max(limit, 1), 1e3);
|
||||
const res = await db.prepare(
|
||||
`SELECT relation_id, role, missing_id FROM (
|
||||
SELECT r.id AS relation_id, 'src' AS role, r.src_id AS missing_id
|
||||
FROM entries r LEFT JOIN entries t ON t.id = r.src_id
|
||||
WHERE r.src_id IS NOT NULL AND t.id IS NULL
|
||||
UNION ALL
|
||||
SELECT r.id, 'rel', r.rel_id
|
||||
FROM entries r LEFT JOIN entries t ON t.id = r.rel_id
|
||||
WHERE r.rel_id IS NOT NULL AND t.id IS NULL
|
||||
UNION ALL
|
||||
SELECT r.id, 'dst', r.dst_id
|
||||
FROM entries r LEFT JOIN entries t ON t.id = r.dst_id
|
||||
WHERE r.dst_id IS NOT NULL AND t.id IS NULL
|
||||
) LIMIT ?`
|
||||
).bind(cap + 1).all();
|
||||
const rows = res.results ?? [];
|
||||
const truncated = rows.length > cap;
|
||||
const orphans = truncated ? rows.slice(0, cap) : rows;
|
||||
return { orphans, count: orphans.length, truncated };
|
||||
}
|
||||
var init_relation_orphans = __esm({
|
||||
"kbdb/src/actions/relation-orphans.ts"() {
|
||||
"use strict";
|
||||
}
|
||||
});
|
||||
|
||||
// kbdb/node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/compose.js
|
||||
var compose = (middleware, onError, onNotFound) => {
|
||||
return (context, next) => {
|
||||
@@ -2101,12 +2149,15 @@ async function getEntry(db, id) {
|
||||
const row = await db.prepare("SELECT * FROM entries WHERE id = ?").bind(id).first();
|
||||
return row ?? null;
|
||||
}
|
||||
var NOT_MACHINERY_PREDICATE = "(src_id IS NULL AND entry_type NOT IN ('record', 'sheet', 'field', 'system'))";
|
||||
async function listEntries(db, f = {}) {
|
||||
const conds = [];
|
||||
const params = [];
|
||||
if (f.entry_type) {
|
||||
conds.push("entry_type = ?");
|
||||
params.push(f.entry_type);
|
||||
} else {
|
||||
conds.push(NOT_MACHINERY_PREDICATE);
|
||||
}
|
||||
if (f.owner_id) {
|
||||
conds.push("owner_id = ?");
|
||||
@@ -2158,7 +2209,10 @@ async function updateEntry(db, id, patch) {
|
||||
return getEntry(db, id);
|
||||
}
|
||||
async function deleteEntry(db, id) {
|
||||
const ref = await db.prepare("SELECT id FROM entries WHERE dst_id = ? LIMIT 1").bind(id).first();
|
||||
if (ref) throw new Error(`entry ${id} is still referenced by record relation ${ref.id} \u2014 delete the record (or its slot) first`);
|
||||
await db.prepare("DELETE FROM entries WHERE id = ?").bind(id).run();
|
||||
await db.prepare("DELETE FROM entries WHERE src_id = ?").bind(id).run();
|
||||
}
|
||||
async function embeddedIdsByLibrary(db, ownerId, library) {
|
||||
const rows = await db.prepare(
|
||||
@@ -2392,6 +2446,8 @@ async function searchEntries(db, q, owner_id, entry_type, limit = 50, library, s
|
||||
if (entry_type) {
|
||||
conds.push("entry_type = ?");
|
||||
params.push(entry_type);
|
||||
} else {
|
||||
conds.push(NOT_MACHINERY_PREDICATE);
|
||||
}
|
||||
if (source) {
|
||||
conds.push("json_extract(metadata_json, '$.source') = ?");
|
||||
@@ -3236,9 +3292,38 @@ entryRoutes.delete("/:id", async (c) => {
|
||||
function uid2(prefix) {
|
||||
return `${prefix}_${crypto.randomUUID()}`;
|
||||
}
|
||||
var SYS_ROOT = "sys_root";
|
||||
var SYS_BELONGS = "sys_belongs";
|
||||
var SYS_FIELD_OF = "sys_field_of";
|
||||
function fieldEntryId(templateId, slot) {
|
||||
return `fld_${templateId}_${slot}`;
|
||||
}
|
||||
async function ensureAnchors(db) {
|
||||
await db.prepare(
|
||||
`INSERT OR IGNORE INTO entries (id, content, entry_type, owner_id) VALUES
|
||||
('${SYS_ROOT}', 'root', 'system', NULL),
|
||||
('${SYS_BELONGS}', 'belongs', 'system', NULL),
|
||||
('${SYS_FIELD_OF}', 'field_of', 'system', NULL)`
|
||||
).run();
|
||||
}
|
||||
async function ensureFieldEntries(db, templateId, slots) {
|
||||
for (const slot of slots) {
|
||||
const fid = fieldEntryId(templateId, slot);
|
||||
await db.prepare(`INSERT OR IGNORE INTO entries (id, content, entry_type) VALUES (?, ?, 'field')`).bind(fid, slot).run();
|
||||
await db.prepare(
|
||||
`INSERT OR IGNORE INTO entries (id, entry_type, src_id, rel_id, dst_id) VALUES (?, 'relation', ?, '${SYS_FIELD_OF}', ?)`
|
||||
).bind(`relf_${templateId}_${slot}`, fid, templateId).run();
|
||||
}
|
||||
}
|
||||
async function createTemplate(db, input) {
|
||||
const id = input.id ?? uid2("tpl");
|
||||
await db.prepare(`INSERT INTO templates (id, name, description, slots_json, created_by) VALUES (?, ?, ?, ?, ?)`).bind(id, input.name, input.description ?? null, JSON.stringify(input.slots), input.created_by ?? null).run();
|
||||
await ensureAnchors(db);
|
||||
await db.prepare(`INSERT OR IGNORE INTO entries (id, content, entry_type) VALUES (?, ?, 'sheet')`).bind(id, input.name).run();
|
||||
await db.prepare(
|
||||
`INSERT OR IGNORE INTO entries (id, entry_type, src_id, rel_id, dst_id) VALUES (?, 'relation', ?, '${SYS_BELONGS}', '${SYS_ROOT}')`
|
||||
).bind(`relb_${id}`, id).run();
|
||||
await ensureFieldEntries(db, id, input.slots);
|
||||
const row = await getTemplate(db, id);
|
||||
if (!row) throw new Error("createTemplate: row not found after insert");
|
||||
return row;
|
||||
@@ -3265,104 +3350,187 @@ async function updateTemplate(db, id, patch) {
|
||||
if (cols.length === 0) return getTemplate(db, id);
|
||||
cols.push("updated_at = unixepoch()");
|
||||
await db.prepare(`UPDATE templates SET ${cols.join(", ")} WHERE id = ?`).bind(...params, id).run();
|
||||
if (patch.slots !== void 0) await ensureFieldEntries(db, id, patch.slots);
|
||||
return getTemplate(db, id);
|
||||
}
|
||||
async function loadReferencedEntries(db, entryIds, recordOwnerId) {
|
||||
const ids = [...new Set(Object.values(entryIds))];
|
||||
if (ids.length === 0) return /* @__PURE__ */ new Map();
|
||||
const rows = [];
|
||||
for (let i = 0; i < ids.length; i += 90) {
|
||||
const chunk = ids.slice(i, i + 90);
|
||||
const res = await db.prepare(`SELECT id, content, owner_id FROM entries WHERE id IN (${chunk.map(() => "?").join(",")})`).bind(...chunk).all();
|
||||
rows.push(...res.results ?? []);
|
||||
}
|
||||
const found = new Map(rows.map((r) => [r.id, r]));
|
||||
const missing = ids.filter((id) => !found.has(id));
|
||||
if (missing.length > 0) throw new Error(`entry not found: ${missing.join(", ")}`);
|
||||
if (recordOwnerId != null) {
|
||||
const foreign = rows.filter((r) => r.owner_id != null && r.owner_id !== recordOwnerId);
|
||||
if (foreign.length > 0) {
|
||||
throw new Error(
|
||||
`entry owner mismatch: ${foreign.map((r) => `${r.id}(${r.owner_id})`).join(", ")} != ${recordOwnerId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
return new Map(rows.map((r) => [r.id, r.content]));
|
||||
}
|
||||
async function recordBelongs(db, recordId) {
|
||||
const row = await db.prepare(`SELECT dst_id FROM entries WHERE src_id = ? AND rel_id = '${SYS_BELONGS}' AND dst_id != '${SYS_ROOT}' LIMIT 1`).bind(recordId).first();
|
||||
return row ?? null;
|
||||
}
|
||||
async function insertCellRelation(db, recordId, templateId, slot, dstEntryId, ownerId) {
|
||||
await db.prepare(
|
||||
`INSERT INTO entries (id, entry_type, owner_id, src_id, rel_id, dst_id) VALUES (?, 'relation', ?, ?, ?, ?)`
|
||||
).bind(uid2("relv"), ownerId, recordId, fieldEntryId(templateId, slot), dstEntryId).run();
|
||||
}
|
||||
async function createRecord(db, input) {
|
||||
const tpl = await getTemplate(db, input.template);
|
||||
if (!tpl) throw new Error(`template not found: ${input.template}`);
|
||||
const slots = JSON.parse(tpl.slots_json);
|
||||
const recordId = input.record_id ?? uid2("rec");
|
||||
for (const slot of slots) {
|
||||
if (!(slot in input.values)) continue;
|
||||
const entry = await createEntry(db, {
|
||||
content: input.values[slot],
|
||||
entry_type: "value",
|
||||
owner_id: input.owner_id ?? null
|
||||
});
|
||||
await db.prepare(`INSERT INTO entry_values (id, record_id, template_id, slot_name, entry_id) VALUES (?, ?, ?, ?, ?)`).bind(uid2("ev"), recordId, tpl.id, slot, entry.id).run();
|
||||
const values = input.values ?? {};
|
||||
const entryIds = input.entry_ids ?? {};
|
||||
const refSlots = Object.keys(entryIds);
|
||||
const ownerId = input.owner_id ?? null;
|
||||
const both = refSlots.filter((s) => s in values);
|
||||
if (both.length > 0) throw new Error(`slot given both value and entry_id: ${both.join(", ")}`);
|
||||
const unknown = refSlots.filter((s) => !slots.includes(s));
|
||||
if (unknown.length > 0) throw new Error(`slot not in template: ${unknown.join(", ")}`);
|
||||
const referenced = await loadReferencedEntries(db, entryIds, ownerId);
|
||||
await db.prepare(`INSERT OR IGNORE INTO entries (id, entry_type, owner_id) VALUES (?, 'record', ?)`).bind(recordId, ownerId).run();
|
||||
await db.prepare(
|
||||
`INSERT OR IGNORE INTO entries (id, entry_type, owner_id, src_id, rel_id, dst_id) VALUES (?, 'relation', ?, ?, '${SYS_BELONGS}', ?)`
|
||||
).bind(`relb_${recordId}_${tpl.id}`, ownerId, recordId, tpl.id).run();
|
||||
const writtenSlots = slots.filter((s) => s in entryIds || s in values);
|
||||
await ensureFieldEntries(db, tpl.id, writtenSlots);
|
||||
for (const slot of writtenSlots) {
|
||||
if (slot in entryIds) {
|
||||
await insertCellRelation(db, recordId, tpl.id, slot, entryIds[slot], ownerId);
|
||||
continue;
|
||||
}
|
||||
return { record_id: recordId, template_id: tpl.id, values: input.values, owner_id: input.owner_id ?? null };
|
||||
const entry = await createEntry(db, {
|
||||
content: values[slot],
|
||||
entry_type: "value",
|
||||
owner_id: ownerId
|
||||
});
|
||||
await insertCellRelation(db, recordId, tpl.id, slot, entry.id, ownerId);
|
||||
}
|
||||
const out = { ...values };
|
||||
for (const [slot, entryId] of Object.entries(entryIds)) out[slot] = referenced.get(entryId) ?? "";
|
||||
return { record_id: recordId, template_id: tpl.id, values: out, owner_id: ownerId };
|
||||
}
|
||||
async function updateRecord(db, recordId, values) {
|
||||
const evRes = await db.prepare(
|
||||
`SELECT ev.slot_name AS slot_name, ev.entry_id AS entry_id, ev.template_id AS template_id, e.owner_id AS owner_id
|
||||
FROM entry_values ev JOIN entries e ON ev.entry_id = e.id
|
||||
WHERE ev.record_id = ?`
|
||||
const belongs = await recordBelongs(db, recordId);
|
||||
if (!belongs) return null;
|
||||
const templateId = belongs.dst_id;
|
||||
const cellRes = await db.prepare(
|
||||
`SELECT f.content AS slot_name, r.dst_id AS entry_id
|
||||
FROM entries r JOIN entries f ON r.rel_id = f.id
|
||||
WHERE r.src_id = ? AND r.rel_id != '${SYS_BELONGS}'`
|
||||
).bind(recordId).all();
|
||||
const evRows = evRes.results ?? [];
|
||||
if (evRows.length === 0) return null;
|
||||
const templateId = evRows[0].template_id;
|
||||
const recordOwnerId = evRows.find((r) => r.owner_id != null)?.owner_id ?? null;
|
||||
const slotToEntry = new Map(evRows.map((r) => [r.slot_name, r.entry_id]));
|
||||
const cells = cellRes.results ?? [];
|
||||
const slotToEntries = /* @__PURE__ */ new Map();
|
||||
for (const c of cells) {
|
||||
const list = slotToEntries.get(c.slot_name) ?? [];
|
||||
list.push(c.entry_id);
|
||||
slotToEntries.set(c.slot_name, list);
|
||||
}
|
||||
const identity = await db.prepare("SELECT owner_id FROM entries WHERE id = ?").bind(recordId).first();
|
||||
const recordOwnerId = identity?.owner_id ?? null;
|
||||
const tpl = await getTemplate(db, templateId);
|
||||
const allowed = tpl ? JSON.parse(tpl.slots_json) : [...slotToEntry.keys()];
|
||||
const allowed = tpl ? JSON.parse(tpl.slots_json) : [...slotToEntries.keys()];
|
||||
for (const [slot, content] of Object.entries(values)) {
|
||||
if (!allowed.includes(slot)) {
|
||||
throw new Error(`slot not in template: ${slot}`);
|
||||
}
|
||||
const entryId = slotToEntry.get(slot);
|
||||
if (entryId) {
|
||||
const entryIds = slotToEntries.get(slot);
|
||||
if (entryIds && entryIds.length > 0) {
|
||||
for (const entryId of entryIds) {
|
||||
await db.prepare(`UPDATE entries SET content = ?, updated_at = unixepoch() WHERE id = ?`).bind(content, entryId).run();
|
||||
}
|
||||
} else {
|
||||
await ensureFieldEntries(db, templateId, [slot]);
|
||||
const entry = await createEntry(db, { content, entry_type: "value", owner_id: recordOwnerId });
|
||||
await db.prepare(`INSERT INTO entry_values (id, record_id, template_id, slot_name, entry_id) VALUES (?, ?, ?, ?, ?)`).bind(uid2("ev"), recordId, templateId, slot, entry.id).run();
|
||||
await insertCellRelation(db, recordId, templateId, slot, entry.id, recordOwnerId);
|
||||
}
|
||||
}
|
||||
return getRecord(db, recordId);
|
||||
}
|
||||
async function getRecord(db, recordId) {
|
||||
const belongs = await recordBelongs(db, recordId);
|
||||
if (!belongs) return null;
|
||||
const res = await db.prepare(
|
||||
`SELECT ev.slot_name as slot, e.content as content, ev.template_id as template_id, e.owner_id as owner_id
|
||||
FROM entry_values ev JOIN entries e ON ev.entry_id = e.id
|
||||
WHERE ev.record_id = ?`
|
||||
`SELECT f.content AS slot, v.content AS content
|
||||
FROM entries r
|
||||
JOIN entries f ON r.rel_id = f.id
|
||||
JOIN entries v ON r.dst_id = v.id
|
||||
WHERE r.src_id = ? AND r.rel_id != '${SYS_BELONGS}'`
|
||||
).bind(recordId).all();
|
||||
const rows = res.results ?? [];
|
||||
if (rows.length === 0) return null;
|
||||
const values = {};
|
||||
for (const r of rows) values[r.slot] = r.content;
|
||||
const owner_id = rows.find((r) => r.owner_id != null)?.owner_id ?? null;
|
||||
return { record_id: recordId, template_id: rows[0].template_id, values, owner_id };
|
||||
for (const r of res.results ?? []) values[r.slot] = r.content;
|
||||
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) {
|
||||
const tpl = await getTemplate(db, template);
|
||||
if (!tpl) return [];
|
||||
const cap = Math.min(limit, 500);
|
||||
const res = owner_id ? await db.prepare(
|
||||
`SELECT DISTINCT ev.record_id as record_id FROM entry_values ev
|
||||
JOIN entries e ON ev.entry_id = e.id
|
||||
WHERE ev.template_id = ? AND e.owner_id = ?
|
||||
ORDER BY ev.created_at DESC LIMIT ?`
|
||||
).bind(tpl.id, owner_id, cap).all() : await db.prepare(`SELECT DISTINCT record_id FROM entry_values WHERE template_id = ? ORDER BY created_at DESC LIMIT ?`).bind(tpl.id, cap).all();
|
||||
`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(
|
||||
`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();
|
||||
const ids = (res.results ?? []).map((r) => r.record_id);
|
||||
if (ids.length === 0) return [];
|
||||
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) {
|
||||
const chunk = ids.slice(i, i + 90);
|
||||
const placeholders = chunk.map(() => "?").join(",");
|
||||
const evRes = await db.prepare(
|
||||
`SELECT ev.record_id as record_id, ev.slot_name as slot, e.content as content, ev.template_id as template_id, e.owner_id as owner_id
|
||||
FROM entry_values ev JOIN entries e ON ev.entry_id = e.id
|
||||
WHERE ev.record_id IN (${placeholders})`
|
||||
).bind(...chunk).all();
|
||||
for (const r of evRes.results ?? []) {
|
||||
let rec = byId.get(r.record_id);
|
||||
if (!rec) {
|
||||
rec = { record_id: r.record_id, template_id: r.template_id, values: {}, owner_id: null };
|
||||
byId.set(r.record_id, rec);
|
||||
const [cellRes, identRes] = await Promise.all([
|
||||
db.prepare(
|
||||
`SELECT r.src_id AS record_id, f.content AS slot, v.content AS content
|
||||
FROM entries r
|
||||
JOIN entries f ON r.rel_id = f.id
|
||||
JOIN entries v ON r.dst_id = v.id
|
||||
WHERE r.src_id IN (${placeholders}) AND r.rel_id != '${SYS_BELONGS}'`
|
||||
).bind(...chunk).all(),
|
||||
db.prepare(`SELECT id, owner_id FROM entries WHERE id IN (${placeholders})`).bind(...chunk).all()
|
||||
]);
|
||||
for (const r of cellRes.results ?? []) {
|
||||
const rec = byId.get(r.record_id);
|
||||
if (rec) rec.values[r.slot] = r.content;
|
||||
}
|
||||
rec.values[r.slot] = r.content;
|
||||
if (rec.owner_id == null && r.owner_id != null) rec.owner_id = r.owner_id;
|
||||
for (const r of identRes.results ?? []) {
|
||||
const rec = byId.get(r.id);
|
||||
if (rec) rec.owner_id = r.owner_id;
|
||||
}
|
||||
}
|
||||
return ids.map((id) => byId.get(id)).filter((r) => !!r);
|
||||
}
|
||||
async function deleteRecord(db, recordId) {
|
||||
const evRes = await db.prepare("SELECT entry_id FROM entry_values WHERE record_id = ?").bind(recordId).all();
|
||||
const rows = evRes.results ?? [];
|
||||
if (rows.length === 0) return false;
|
||||
await db.prepare("DELETE FROM entry_values WHERE record_id = ?").bind(recordId).run();
|
||||
for (const { entry_id } of rows) {
|
||||
await db.prepare("DELETE FROM entries WHERE id = ?").bind(entry_id).run();
|
||||
const belongs = await recordBelongs(db, recordId);
|
||||
if (!belongs) return false;
|
||||
const cellRes = await db.prepare(`SELECT dst_id FROM entries WHERE src_id = ? AND rel_id != '${SYS_BELONGS}'`).bind(recordId).all();
|
||||
const dsts = (cellRes.results ?? []).map((r) => r.dst_id);
|
||||
await db.prepare(`DELETE FROM entries WHERE src_id = ?`).bind(recordId).run();
|
||||
await db.prepare(
|
||||
`DELETE FROM entries WHERE id = ?1 AND entry_type = 'record'
|
||||
AND NOT EXISTS (SELECT 1 FROM entries WHERE dst_id = ?1)`
|
||||
).bind(recordId).run();
|
||||
for (const dst of dsts) {
|
||||
await db.prepare(
|
||||
`DELETE FROM entries WHERE id = ?1
|
||||
AND entry_type NOT IN ('sheet', 'field', 'system')
|
||||
AND NOT EXISTS (SELECT 1 FROM entries WHERE dst_id = ?1)
|
||||
AND NOT EXISTS (SELECT 1 FROM entries WHERE src_id = ?1)
|
||||
AND NOT EXISTS (SELECT 1 FROM entries WHERE rel_id = ?1)`
|
||||
).bind(dst).run();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -3395,10 +3563,17 @@ templateRoutes.patch("/:id", async (c) => {
|
||||
|
||||
// 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");
|
||||
recordRoutes.post("/", async (c) => {
|
||||
const body = await c.req.json().catch(() => null);
|
||||
if (!body || !body.template || !body.values) {
|
||||
return c.json({ success: false, error: "template and values required" }, 400);
|
||||
if (!body || !body.template || !body.values && !body.entry_ids) {
|
||||
return c.json({ success: false, error: "template and values (or entry_ids) required" }, 400);
|
||||
}
|
||||
if (body.values !== void 0 && !isStringMap(body.values)) {
|
||||
return c.json({ success: false, error: "values must be an object of {slot: string}" }, 400);
|
||||
}
|
||||
if (body.entry_ids !== void 0 && !isStringMap(body.entry_ids)) {
|
||||
return c.json({ success: false, error: "entry_ids must be an object of {slot: entry_id}" }, 400);
|
||||
}
|
||||
try {
|
||||
const rec = await createRecord(c.env.DB, body);
|
||||
@@ -3411,20 +3586,14 @@ recordRoutes.get("/triplet-stats", async (c) => {
|
||||
const owner = c.req.query("owner_id") || "";
|
||||
const rows = await c.env.DB.prepare(
|
||||
`SELECT
|
||||
COALESCE(NULLIF(lib_e.content, ''), 'general') AS library,
|
||||
COALESCE(NULLIF(lib_v.content, ''), 'general') AS library,
|
||||
COUNT(*) AS triplet_count
|
||||
FROM (
|
||||
SELECT DISTINCT ev.record_id
|
||||
FROM entry_values ev
|
||||
JOIN templates t ON ev.template_id = t.id
|
||||
JOIN entries e ON ev.entry_id = e.id
|
||||
WHERE t.name = 'triplet'
|
||||
AND (?1 = '' OR e.owner_id = ?1)
|
||||
) AS tr
|
||||
LEFT JOIN entry_values lev
|
||||
ON lev.record_id = tr.record_id AND lev.slot_name = 'library'
|
||||
LEFT JOIN entries lib_e ON lib_e.id = lev.entry_id
|
||||
GROUP BY COALESCE(NULLIF(lib_e.content, ''), 'general')
|
||||
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 }));
|
||||
@@ -3604,31 +3773,35 @@ async function ensureTripletLibrarySlot(db, tripletTemplate) {
|
||||
return true;
|
||||
}
|
||||
function tripletPivotSql(ownerFiltered) {
|
||||
return `SELECT ev.record_id AS rid,
|
||||
MAX(CASE WHEN ev.slot_name = 'subject' THEN e.content END) AS subject,
|
||||
MAX(CASE WHEN ev.slot_name = 'object' THEN e.content END) AS object,
|
||||
MAX(CASE WHEN ev.slot_name = 'predicate' THEN e.content END) AS predicate,
|
||||
MAX(CASE WHEN ev.slot_name = 'status' THEN e.content END) AS status,
|
||||
MAX(CASE WHEN ev.slot_name = 'library' THEN e.content END) AS library,
|
||||
MAX(CASE WHEN ev.slot_name = 'source_uri' THEN e.content END) AS source_uri
|
||||
FROM entry_values ev JOIN entries e ON ev.entry_id = e.id
|
||||
WHERE ev.template_id = ?${ownerFiltered ? " AND e.owner_id = ?" : ""}
|
||||
GROUP BY ev.record_id`;
|
||||
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 ev.record_id AS rid,
|
||||
MAX(CASE WHEN ev.slot_name = 'library' THEN e.content END) AS library,
|
||||
MAX(CASE WHEN ev.slot_name = 'narrative' THEN e.content END) AS narrative,
|
||||
MAX(CASE WHEN ev.slot_name = 'top_entities' THEN e.content END) AS top_entities,
|
||||
MAX(CASE WHEN ev.slot_name = 'relation_profile' THEN e.content END) AS relation_profile,
|
||||
MAX(CASE WHEN ev.slot_name = 'bridges' THEN e.content END) AS bridges,
|
||||
MAX(CASE WHEN ev.slot_name = 'triplet_count' THEN e.content END) AS triplet_count,
|
||||
MAX(CASE WHEN ev.slot_name = 'commit_hash' THEN e.content END) AS commit_hash,
|
||||
MAX(CASE WHEN ev.slot_name = 'status' THEN e.content END) AS status,
|
||||
MAX(ev.created_at) AS ts
|
||||
FROM entry_values ev JOIN entries e ON ev.entry_id = e.id
|
||||
WHERE ev.template_id = ?${ownerFiltered ? " AND e.owner_id = ?" : ""}
|
||||
GROUP BY ev.record_id`;
|
||||
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 [];
|
||||
@@ -3761,18 +3934,19 @@ 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(lib_e.content, ''), 'general') AS library, COUNT(*) AS n
|
||||
`SELECT COALESCE(NULLIF(tr.library, ''), 'general') AS library, COUNT(*) AS n
|
||||
FROM (
|
||||
SELECT ev.record_id AS rid,
|
||||
MAX(CASE WHEN ev.slot_name = 'status' THEN e.content END) AS status
|
||||
FROM entry_values ev JOIN entries e ON ev.entry_id = e.id
|
||||
WHERE ev.template_id = ?${owner_id ? " AND e.owner_id = ?" : ""}
|
||||
GROUP BY ev.record_id
|
||||
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
|
||||
LEFT JOIN entry_values lev ON lev.record_id = tr.rid AND lev.slot_name = 'library'
|
||||
LEFT JOIN entries lib_e ON lib_e.id = lev.entry_id
|
||||
WHERE COALESCE(tr.status, 'active') = 'active'
|
||||
GROUP BY COALESCE(NULLIF(lib_e.content, ''), 'general')`
|
||||
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);
|
||||
@@ -3786,6 +3960,8 @@ async function liveEntryCountsByLibrary(db, owner_id) {
|
||||
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();
|
||||
@@ -3805,10 +3981,12 @@ async function knownLibraryNames(db, owner_id) {
|
||||
if (libTpl) {
|
||||
const libParams = owner_id ? [libTpl.id, owner_id] : [libTpl.id];
|
||||
const libRows = await db.prepare(
|
||||
`SELECT MAX(CASE WHEN ev.slot_name = 'name' THEN e.content END) AS name
|
||||
FROM entry_values ev JOIN entries e ON ev.entry_id = e.id
|
||||
WHERE ev.template_id = ?${owner_id ? " AND e.owner_id = ?" : ""}
|
||||
GROUP BY ev.record_id`
|
||||
`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);
|
||||
}
|
||||
@@ -4176,6 +4354,12 @@ app.use("*", async (c, next) => {
|
||||
});
|
||||
app.get("/", (c) => c.json({ service: "arcrun-kbdb", tier: "base", status: "ok" }));
|
||||
app.get("/health", (c) => c.json({ ok: true }));
|
||||
app.get("/maintenance/relation-orphans", async (c) => {
|
||||
const { scanRelationOrphans: scanRelationOrphans2 } = await Promise.resolve().then(() => (init_relation_orphans(), relation_orphans_exports));
|
||||
const limit = Number(c.req.query("limit") ?? "200");
|
||||
const report = await scanRelationOrphans2(c.env.DB, Number.isFinite(limit) ? limit : 200);
|
||||
return c.json({ success: true, ...report });
|
||||
});
|
||||
app.route("/entries", entryRoutes);
|
||||
app.route("/templates", templateRoutes);
|
||||
app.route("/records", recordRoutes);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"built_for": "arcrun-tier2-worker-artifacts",
|
||||
"generated_at": "2026-08-14T17:00:10.242Z",
|
||||
"repo_head": "45169ab25e974085aaaebaceeb4a67f785239d1b",
|
||||
"generated_at": "2026-08-15T15:53:28.612Z",
|
||||
"repo_head": "cacaa33f7d4e51d0011096250a33bb5107c67074",
|
||||
"repo_dirty": false,
|
||||
"workers": [
|
||||
{
|
||||
"name": "arcrun-cypher-executor",
|
||||
"source_dir": "cypher-executor",
|
||||
"source_commit": "340b461f58ed6a9958850f8871cf05381ce95c9d",
|
||||
"source_commit": "9fa88065abdc5a795d3847711f04101480621af0",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-cypher-executor/worker.mjs",
|
||||
"js_bytes": 590085,
|
||||
"content_sha256": "1da6a46e6a1781cb1a518635ff7e6b43a035710673066f4f764671c75441fdaf",
|
||||
"js_bytes": 590499,
|
||||
"content_sha256": "94a21ad012ee0c48d78e9a74752048917ddb4616f1ea089976834acd887a9d14",
|
||||
"modules": [],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [
|
||||
@@ -58,11 +58,11 @@
|
||||
{
|
||||
"name": "arcrun-kbdb",
|
||||
"source_dir": "kbdb",
|
||||
"source_commit": "614fe448124dbd25d8228e37723473f1704f3a7a",
|
||||
"source_commit": "ceb7638d746f0e8847552aef721f6f53ef87ff0b",
|
||||
"main_module": "worker.mjs",
|
||||
"main_file": "arcrun-kbdb/worker.mjs",
|
||||
"js_bytes": 151412,
|
||||
"content_sha256": "f15e16cca6cefb0d8631759c27fec58cd36262abf6aed7853f3b35c6e8e1e09b",
|
||||
"js_bytes": 160306,
|
||||
"content_sha256": "47b09d60f8d122f7ec234d146a99bc68b2661c56ad6a98eca146d99ec5de03ea",
|
||||
"modules": [],
|
||||
"compat_date": "2025-02-19",
|
||||
"compat_flags": [
|
||||
|
||||
Reference in New Issue
Block a user