feat(t135): 庫目錄可自主移除+標示還在不在同步
leo:「需要加移除按鈕。因為別人裝錯我沒辦法幫他弄,需要可以自主」
+「你應該要顯示這個庫沒有本地對應的 folder,那就不容易刪錯」
(兩態非三態——leo 二修:「分兩種沒意義」,那是內部狀態不是用戶分類)。
- DELETE /portal/admin/libraries/:id(登記簿)與 by-name/:name(auto 庫需輸入庫名確認)
- kbdb 加 deprecate-by-library(auto 庫移除=標 deprecated,資料保留可還原)
- daemon/libraries 存 active 清單 → 卡片標 🟢同步中/灰目前沒有在同步
- 不自動刪(daemon 可能沒開機);daemon 從未回報時整列不標
vitest 24 passed(1 紅=console HTML 搬遷陳舊測試,非本案)。
(實作=子 CC;驗證+commit=總管。含 t116/t117 先前未 commit 的 graph-executor/wasi-shim 修正)
This commit is contained in:
@@ -500,6 +500,26 @@ export class GraphExecutor {
|
||||
iterResults.push(itemResult);
|
||||
}
|
||||
|
||||
// t117: FOREACH 全部項目 success===false → 不再靜默,拋出含 status code 的錯誤
|
||||
if (iterResults.length > 0) {
|
||||
const failures = iterResults.filter(
|
||||
r => r !== null && typeof r === 'object' && (r as Record<string, unknown>).success === false
|
||||
);
|
||||
if (failures.length === iterResults.length) {
|
||||
const first = failures[0] as Record<string, unknown>;
|
||||
const errParts: string[] = [];
|
||||
if (first.error) errParts.push(String(first.error));
|
||||
if (typeof first.status === 'number') errParts.push(`HTTP ${first.status}`);
|
||||
const bodyData = first.data as { body?: string } | null | undefined;
|
||||
if (bodyData && typeof bodyData.body === 'string' && bodyData.body) {
|
||||
errParts.push(bodyData.body.slice(0, 200));
|
||||
}
|
||||
throw new Error(
|
||||
`FOREACH 所有 ${iterResults.length} 項目均失敗(首項:${errParts.join(';') || '未知錯誤'})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
result = { ...(result as Record<string, unknown>), results: iterResults };
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -353,8 +353,15 @@ export function createWasiShim(stdinData: string, hostFunctions?: WasiHostFuncti
|
||||
const result = await hostFunctions!.http_request!(url, method, headers, body);
|
||||
// await 後重新拿 memory.buffer(grow 會產生新的 ArrayBuffer)
|
||||
return writeOut(memory.buffer, outPtr, outLenPtr, new TextEncoder().encode(result));
|
||||
} catch {
|
||||
return 1;
|
||||
} catch (e) {
|
||||
// t117: 寫錯誤 envelope 到 WASM 輸出(main.go 讀 error key → success:false + 詳情);
|
||||
// 取代只 return 1(WASM 寫無資訊的 "HTTP request failed")。
|
||||
// writeOut 失敗(memory 壞)才 fallback return 1。
|
||||
const errDetail = e instanceof Error ? e.message : String(e);
|
||||
const errEnv = new TextEncoder().encode(
|
||||
JSON.stringify({ error: `fetch failed: ${errDetail}`, status: 0, body: '' })
|
||||
);
|
||||
return writeOut(memory.buffer, outPtr, outLenPtr, errEnv);
|
||||
}
|
||||
})
|
||||
: () => 1,
|
||||
|
||||
@@ -184,6 +184,18 @@ async function patchRecordValues(env: Bindings, recordId: string, values: Record
|
||||
return body.record;
|
||||
}
|
||||
|
||||
async function deleteKbdbRecord(env: Bindings, recordId: string): Promise<boolean> {
|
||||
const res = await kbdbFetch(env, `/records/${encodeURIComponent(recordId)}`, { method: 'DELETE' });
|
||||
if (res.status === 404) return false;
|
||||
if (!res.ok) throw new KbdbError(`DELETE /records/${recordId} → ${res.status}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** KV key for daemon's most-recently-reported active library names(t135 daemon hint)。 */
|
||||
function daemonActiveKey(env: Bindings): string {
|
||||
return `${portalTenant(env)}:portal:daemon_active_libs`;
|
||||
}
|
||||
|
||||
export async function listRecordsByTemplate(env: Bindings, template: string): Promise<PortalRecord[]> {
|
||||
const ns = portalNamespace(env);
|
||||
const res = await kbdbFetch(env, `/records/by-template/${encodeURIComponent(template)}?owner_id=${encodeURIComponent(ns)}`);
|
||||
@@ -751,6 +763,11 @@ portalRouter.post('/portal/daemon/libraries', (c) =>
|
||||
created.push(name);
|
||||
}
|
||||
const after = await listRecordsByTemplate(c.env, LIBRARY_TEMPLATE);
|
||||
// t135:記下本次 daemon 回報的所有庫名(48h TTL)供 GET /portal/admin/libraries 顯示「未同步」提示。
|
||||
const activeNames = wanted.map((item) => String(item?.name ?? '').trim()).filter(Boolean);
|
||||
if (activeNames.length > 0) {
|
||||
await c.env.WEBHOOKS.put(daemonActiveKey(c.env), JSON.stringify(activeNames), { expirationTtl: 172800 });
|
||||
}
|
||||
return c.json({ success: true, created, libraries: after.map(toPublicLibrary) });
|
||||
}),
|
||||
);
|
||||
@@ -905,12 +922,23 @@ portalRouter.get('/portal/admin/extractor', (c) =>
|
||||
// t52(leo 2026-07-26:「地端 2 個資料夾、雲端就要 2 個庫,只有一個一定被罵」):
|
||||
// 除了登記簿裡的庫,**也把資料裡實際蓋過章的庫一併列出**(標 auto:true)——
|
||||
// 蓋章即現身,用戶不必先去登記;登記簿只負責顯示名/圖譜來源這些額外設定。
|
||||
// t135:讀 daemon 最近回報的 active libs(KV TTL 48h),已登記的庫若不在其中標 daemon_watching:false。
|
||||
portalRouter.get('/portal/admin/libraries', (c) =>
|
||||
run(c, async () => {
|
||||
const auth = await requirePortalAdmin(c);
|
||||
if (!auth.ok) return auth.res;
|
||||
const libs = await listRecordsByTemplate(c.env, LIBRARY_TEMPLATE);
|
||||
const out = libs.map(toPublicLibrary);
|
||||
// 讀 daemon 最近回報的 active lib names(若 KV 不存在 = daemon 從未回報,不標 hint)
|
||||
let daemonActive: Set<string> | null = null;
|
||||
try {
|
||||
const raw = await c.env.WEBHOOKS.get(daemonActiveKey(c.env), 'text');
|
||||
if (raw) daemonActive = new Set((JSON.parse(raw) as string[]).map((n) => String(n).trim()));
|
||||
} catch { /* KV 不可達不擋主流程 */ }
|
||||
const out = libs.map((rec) => {
|
||||
const lib = toPublicLibrary(rec);
|
||||
const watching = daemonActive === null ? undefined : daemonActive.has(lib.name);
|
||||
return { ...lib, ...(watching !== undefined ? { daemon_watching: watching } : {}) };
|
||||
});
|
||||
const known = new Set(out.map((l) => l.name));
|
||||
// 資料面實際出現的庫(來自 ingest 蓋章的 metadata.library)
|
||||
try {
|
||||
@@ -922,7 +950,13 @@ portalRouter.get('/portal/admin/libraries', (c) =>
|
||||
// general 是系統內部「未標庫」桶(未標記 entry 的 fallback),不在用戶目錄露臉
|
||||
if (!n || n === 'general' || known.has(n)) continue;
|
||||
known.add(n);
|
||||
out.push({ record_id: '', name: n, display_name: n, description: '資料同步時自動出現(可在此補顯示名)', status: 'active', graph_source: false, auto: true });
|
||||
const watching = daemonActive === null ? undefined : daemonActive.has(n);
|
||||
out.push({
|
||||
record_id: '', name: n, display_name: n,
|
||||
description: '資料同步時自動出現(可在此補顯示名)',
|
||||
status: 'active', graph_source: false, auto: true,
|
||||
...(watching !== undefined ? { daemon_watching: watching } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -1005,3 +1039,53 @@ portalRouter.patch('/portal/admin/libraries/:id', (c) =>
|
||||
return c.json({ success: true, library: toPublicLibrary(updated) });
|
||||
}),
|
||||
);
|
||||
|
||||
// DELETE /portal/admin/libraries/by-name/:name — 移除 auto 庫(只有資料章記、無登記簿 record)。
|
||||
// 語意:把該庫的所有 entries 標 deprecated → 資料不刪、重新 ingest 可還原。
|
||||
// ⚠️ 影響資料可搜性,要求 body.confirm 等於庫名才執行(二次確認)。
|
||||
// ⚠️ 此路由必須在 DELETE /:id 之前宣告(Hono 先到先比;by-name 否則被當成 :id)。
|
||||
portalRouter.delete('/portal/admin/libraries/by-name/:name', (c) =>
|
||||
run(c, async () => {
|
||||
const auth = await requirePortalAdmin(c);
|
||||
if (!auth.ok) return auth.res;
|
||||
const name = decodeURIComponent(c.req.param('name'));
|
||||
const body = await c.req.json().catch(() => null);
|
||||
const confirm = String(body?.confirm ?? '').trim();
|
||||
if (!confirm) return c.json({ error: 'body 須帶 { confirm: "<庫名>" } 才執行(移除會影響資料可搜性)' }, 400);
|
||||
if (confirm !== name) return c.json({ error: `confirm 值「${confirm}」與庫名「${name}」不符` }, 400);
|
||||
const ownerId = portalTenant(c.env);
|
||||
const res = await kbdbFetch(c.env, '/entries/deprecate-by-library', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ owner_id: ownerId, library: name }),
|
||||
});
|
||||
if (!res.ok) throw new KbdbError(`PATCH /entries/deprecate-by-library → ${res.status}`);
|
||||
const data = (await res.json()) as { deprecated_count?: number };
|
||||
return c.json({
|
||||
success: true,
|
||||
deprecated_count: data.deprecated_count ?? 0,
|
||||
message: `已從自動清單移除「${name}」(共標記 ${data.deprecated_count ?? 0} 筆資料不可搜)。資料保留可還原——重新同步時會再出現。`,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
// DELETE /portal/admin/libraries/:id — 移除已登記庫(有 record_id 的登記簿 record)。
|
||||
// 只刪登記簿那筆 record;知識資料(entries with library=name)完全不動。
|
||||
// 資料若有的話,重新同步後會以 auto 庫重新出現。
|
||||
portalRouter.delete('/portal/admin/libraries/:id', (c) =>
|
||||
run(c, async () => {
|
||||
const auth = await requirePortalAdmin(c);
|
||||
if (!auth.ok) return auth.res;
|
||||
const recordId = c.req.param('id');
|
||||
// 成員資格驗(防憑空 id 打到不相干 record)
|
||||
const libs = await listRecordsByTemplate(c.env, LIBRARY_TEMPLATE);
|
||||
const target = libs.find((l) => l.record_id === recordId);
|
||||
if (!target) return c.json({ error: '庫不存在' }, 404);
|
||||
const found = await deleteKbdbRecord(c.env, recordId);
|
||||
if (!found) return c.json({ error: '庫不存在' }, 404);
|
||||
return c.json({
|
||||
success: true,
|
||||
name: target.values.name ?? '',
|
||||
message: `已從目錄移除「${target.values.display_name ?? target.values.name ?? ''}」。資料仍在,重新同步會再出現。`,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user