Compare commits

...

3 Commits

Author SHA1 Message Date
Leo e28e19069f t142: 雲端子庫顯示同步幾張卡+幾個三元組(政府專案驗收需求)
leo 07-29:「要在雲端子庫顯示同步了幾個 wiki,既然這樣也同步顯示有幾個三元組,
這是為了政府專案驗收。」

kbdb 兩支統計端點:
- entries.ts: COUNT(DISTINCT page_name) AS card_count
  ⚠️ 必須 distinct——算 block 數會膨脹 3-5 倍,政府驗收看到假數字比沒數字更糟
- records.ts: COUNT(*) AS triplet_count(三元組本來就算全部)
portal.ts 聚合兩者掛進庫目錄;前端 index.html 顯示。

驗:卡數確為 COUNT(DISTINCT page_name)/前端內嵌 JS node --check 全通過
(07-29 白畫面事故教訓)/portal-admin 測試 34 passed(改動前 31 passed,
唯一的 1 failed 是既有債:GET /portal 回 404,改動前後相同,非本次造成)。

註:此為子 CC 完成後未 commit 的懸置工作,總管收工檢查時發現並補收。
2026-07-29 19:55:03 +08:00
Leo cdca296044 D36:修正四個零件契約的假資訊敘述(現行沒有發 API key 的機制)
leo 07-29 指正:「現在沒有 partner key,改用 namespace,現在沒有發 API key 的機制」。
這些契約寫著 'KBDB partner key(ak_xxx)'/'租戶識別(ak_ 前綴)'=假資訊源——
總管就是被它騙的(先把範例改成不存在的 {{credential.kbdb_partner_key}}),
不修的話下一個 AI 會再挖同一個坑。

改:kbdb_upsert_block/auth_oauth2/auth_service_account/auth_static_key 的
api_key description,改述為「租戶識別=Arcrun namespace」+註明 examples 裡的
ak_test/ak_nonexistent 是測試假值不代表真實格式。

只動 description,不動 schema/欄位名/gherkin_tests
(api_key 欄位名保留——改名會破壞現有 workflow)。
驗:四檔 YAML 可解析、required 不變、欄位清單不變、gherkin_tests 全保留、
ak_test 等測試值原封不動。

人閘:leo 07-29 跑 scripts/component-arm.sh 解保險授權。
2026-07-29 19:38:14 +08:00
Leo e8bd518efa D36 第0步:範例 workflow 金鑰統一走 credential(止血——範例是用戶照抄的樣板)
改前四種寫法並存、沒一個是 credential,而執行端只認 {{credential.X}}
(graph-executor.ts:247→resolveCredentialRefs)⇒ 用戶照抄必踩坑:
  {{api_key}} 13/{{gitea_token}} 2/{{secret.GITHUB_BOT_TOKEN}} 2/{{kbdb_api_key}} 1

改後(19 處統一):
  {{credential.arcrun_namespace}} 15/{{credential.gitea_token}} 2/{{credential.github_bot_token}} 2

⚠️ 中途修正一次錯誤命名:我先改成 kbdb_partner_key(照零件契約舊敘述 ak_xxx),
leo 當場指正「現在沒有 partner key,改用 namespace,沒有發 API key 的機制」——
實際機制確為 X-Arcrun-API-Key: <namespace>(實例上活的 workflow 為證)。

不動:{{secret.LEO_TELEGRAM_CHAT_ID}} 3 處——chat_id 是聊天室 ID 不是金鑰,
無腦套規則會引進「找不到 credential」的錯誤。

驗:違規寫法歸零/12 個範例 YAML 全可解析。
殘:kbdb_upsert_block 契約仍寫『KBDB partner key(ak_xxx)』=假資訊源(我就是被它騙的),
修它被 component-guard 擋(正確),需 leo 跑 scripts/component-arm.sh。
2026-07-29 19:33:46 +08:00
18 changed files with 251 additions and 27 deletions
+12
View File
@@ -1533,6 +1533,17 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
var removeBtn = l.auto
? '<button class="btn2" style="color:#c0392b;border-color:#e57373;margin-left:auto" data-act="lib-remove-auto" data-name="' + esc(l.name) + '">移除</button>'
: '<button class="btn2" style="color:#c0392b;border-color:#e57373;margin-left:auto" data-act="lib-remove" data-id="' + esc(l.record_id) + '" data-name="' + esc(l.display_name || l.name) + '">移除</button>';
// t142:卡數+三元組數顯示(政府驗收用)。兩者皆 0 顯示「還沒有內容」,不顯示「0 張」。
var cardCount = typeof l.card_count === 'number' ? l.card_count : undefined;
var tripletCount = typeof l.triplet_count === 'number' ? l.triplet_count : undefined;
var statsHtml = '';
if (cardCount !== undefined || tripletCount !== undefined) {
var parts = [];
if (cardCount > 0) parts.push(cardCount + ' 張知識卡');
if (tripletCount > 0) parts.push(tripletCount + ' 條關聯');
statsHtml = '<div style="margin-top:5px;font-size:13px;color:rgba(var(--ink-rgb),.5)">' +
(parts.length ? parts.join('・') : '還沒有內容') + '</div>';
}
return '<div class="card-item">' +
'<div style="display:flex;align-items:baseline;gap:10px;flex-wrap:wrap">' +
'<span class="serif" style="font-size:17px;font-weight:600">' + esc(l.display_name || l.name) + '</span>' +
@@ -1542,6 +1553,7 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
: '<span class="tag ' + (disabled ? 'dim' : 'green') + '">' + (disabled ? '已停用' : '啟用中') + '</span>') +
removeBtn +
'</div>' +
statsHtml +
(!l.auto && l.description ? '<div style="margin-top:8px;font-size:14.5px;line-height:1.65;color:rgba(var(--ink-rgb),.65)">' + esc(l.description) + '</div>' : '') +
(notWatching ? '<div style="margin-top:6px;font-size:13px;color:rgba(var(--ink-rgb),.45)">小幫手目前沒有在同步這個資料夾</div>' : '') +
'</div>';
+30 -4
View File
@@ -1081,11 +1081,35 @@ portalRouter.get('/portal/admin/libraries', (c) =>
return { ...lib, ...(watching !== undefined ? { daemon_watching: watching } : {}) };
});
const known = new Set(out.map((l) => l.name));
// 資料面實際出現的庫(來自 ingest 蓋章的 metadata.library
// t142資料面實際出現的庫+統計數字(卡數、三元組數)並行撈取,避免 N+1。
// 任一端點失敗不擋登記簿列表(誠實降級:stats 保持 0,不炸主流程)。
try {
const res = await kbdbFetch(c.env, `/entries/libraries?owner_id=${encodeURIComponent(portalTenant(c.env))}`);
if (res.ok) {
const body = (await res.json()) as { libraries?: string[] };
const tenant = portalTenant(c.env);
const ownerParam = `owner_id=${encodeURIComponent(tenant)}`;
const [autoRes, cardRes, tripletRes] = await Promise.all([
kbdbFetch(c.env, `/entries/libraries?${ownerParam}`).catch(() => null),
kbdbFetch(c.env, `/entries/library-stats?${ownerParam}`).catch(() => null),
kbdbFetch(c.env, `/records/triplet-stats?${ownerParam}`).catch(() => null),
]);
// 解析統計,建成 Map 供 O(1) 查找
const cardMap = new Map<string, number>();
if (cardRes?.ok) {
const body = (await cardRes.json()) as { stats?: { library: string; card_count: number }[] };
for (const s of body.stats ?? []) cardMap.set(s.library, s.card_count);
}
const tripletMap = new Map<string, number>();
if (tripletRes?.ok) {
const body = (await tripletRes.json()) as { stats?: { library: string; triplet_count: number }[] };
for (const s of body.stats ?? []) tripletMap.set(s.library, s.triplet_count);
}
// 已登記庫補入統計
for (const lib of out) {
(lib as Record<string, unknown>).card_count = cardMap.get(lib.name) ?? 0;
(lib as Record<string, unknown>).triplet_count = tripletMap.get(lib.name) ?? 0;
}
// 資料面自動出現的庫(蓋章即現身)
if (autoRes?.ok) {
const body = (await autoRes.json()) as { libraries?: string[] };
for (const name of body.libraries ?? []) {
const n = String(name ?? '').trim();
// general 是系統內部「未標庫」桶(未標記 entry 的 fallback),不在用戶目錄露臉
@@ -1096,6 +1120,8 @@ portalRouter.get('/portal/admin/libraries', (c) =>
record_id: '', name: n, display_name: n,
description: '資料同步時自動出現(可在此補顯示名)',
status: 'active', graph_source: false, auto: true,
card_count: cardMap.get(n) ?? 0,
triplet_count: tripletMap.get(n) ?? 0,
...(watching !== undefined ? { daemon_watching: watching } : {}),
});
}
@@ -381,10 +381,19 @@ describe('/portal/admin/libraries', () => {
await seedAdminSession();
mockGetRecord('rec_admin', adminValues());
mockListByTemplate('portal_library', []);
// t142GET /portal/admin/libraries 現在並行呼叫三個 kbdb 端點,三個都要 mock
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/entries/libraries'), method: 'GET' })
.reply(200, { libraries: ['kb', 'general', 'notes'] });
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/entries/library-stats'), method: 'GET' })
.reply(200, { success: true, stats: [] });
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/records/triplet-stats'), method: 'GET' })
.reply(200, { success: true, stats: [] });
const res = await json('GET', '/portal/admin/libraries', undefined, { Authorization: 'Bearer tok-admin' });
expect(res.status).toBe(200);
const data = (await res.json()) as { libraries: { name: string; auto?: boolean }[] };
@@ -395,6 +404,90 @@ describe('/portal/admin/libraries', () => {
});
});
// ═══════════════ t142 庫目錄卡數+三元組數 ═══════════════
describe('GET /portal/admin/libraries + statst142', () => {
it('kbdb 回傳統計 → 已登記庫帶 card_count + triplet_count', async () => {
await seedAdminSession();
mockGetRecord('rec_admin', adminValues());
mockListByTemplate('portal_library', [
{ record_id: 'rec_lib_kb', values: { name: 'kb', display_name: '知識庫', status: 'active', graph_source: 'false' } },
]);
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/entries/libraries'), method: 'GET' })
.reply(200, { libraries: ['kb'] });
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/entries/library-stats'), method: 'GET' })
.reply(200, { success: true, stats: [{ library: 'kb', card_count: 42 }] });
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/records/triplet-stats'), method: 'GET' })
.reply(200, { success: true, stats: [{ library: 'kb', triplet_count: 111 }] });
const res = await json('GET', '/portal/admin/libraries', undefined, { Authorization: 'Bearer tok-admin' });
expect(res.status).toBe(200);
const data = (await res.json()) as { libraries: { name: string; card_count?: number; triplet_count?: number }[] };
const kb = data.libraries.find((l) => l.name === 'kb');
expect(kb).toBeDefined();
expect(kb!.card_count).toBe(42);
expect(kb!.triplet_count).toBe(111);
});
it('auto 庫也帶 card_count + triplet_count', async () => {
await seedAdminSession();
mockGetRecord('rec_admin', adminValues());
mockListByTemplate('portal_library', []);
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/entries/libraries'), method: 'GET' })
.reply(200, { libraries: ['notes'] });
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/entries/library-stats'), method: 'GET' })
.reply(200, { success: true, stats: [{ library: 'notes', card_count: 7 }] });
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/records/triplet-stats'), method: 'GET' })
.reply(200, { success: true, stats: [{ library: 'notes', triplet_count: 108 }] });
const res = await json('GET', '/portal/admin/libraries', undefined, { Authorization: 'Bearer tok-admin' });
expect(res.status).toBe(200);
const data = (await res.json()) as { libraries: { name: string; card_count?: number; triplet_count?: number; auto?: boolean }[] };
const notes = data.libraries.find((l) => l.name === 'notes');
expect(notes).toBeDefined();
expect(notes!.auto).toBe(true);
expect(notes!.card_count).toBe(7);
expect(notes!.triplet_count).toBe(108);
});
it('庫無內容時 card_count=0 + triplet_count=0(前端顯示「還沒有內容」)', async () => {
await seedAdminSession();
mockGetRecord('rec_admin', adminValues());
mockListByTemplate('portal_library', [
{ record_id: 'rec_lib_empty', values: { name: 'empty', display_name: '空庫', status: 'active', graph_source: 'false' } },
]);
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/entries/libraries'), method: 'GET' })
.reply(200, { libraries: [] });
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/entries/library-stats'), method: 'GET' })
.reply(200, { success: true, stats: [] });
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/records/triplet-stats'), method: 'GET' })
.reply(200, { success: true, stats: [] });
const res = await json('GET', '/portal/admin/libraries', undefined, { Authorization: 'Bearer tok-admin' });
expect(res.status).toBe(200);
const data = (await res.json()) as { libraries: { name: string; card_count: number; triplet_count: number }[] };
const empty = data.libraries.find((l) => l.name === 'empty');
expect(empty).toBeDefined();
expect(empty!.card_count).toBe(0);
expect(empty!.triplet_count).toBe(0);
});
});
// ═══════════════ t135 庫目錄移除 ═══════════════
describe('DELETE /portal/admin/librariest135', () => {
+24
View File
@@ -51,6 +51,30 @@ entryRoutes.get('/libraries', async (c) => {
return c.json({ success: true, libraries, count: libraries.length });
});
// GET /entries/library-stats?owner_id=... — 每個庫的知識卡數(distinct page_name,非 block 數)。
// t1422026-07-29):政府驗收用——一眼看出每個庫有幾張卡(page 粒度,不是 block 粒度,
// 一張卡通常對應 3-5 個 block;不含 deprecated entries)。
// 只計 entry_type='block' 的條目,因為 block 才對應知識卡的一個段落(page_name 標記所屬頁面)。
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,
COUNT(DISTINCT page_name) AS card_count
FROM entries
WHERE (?1 = '' OR owner_id = ?1)
AND entry_type = 'block'
AND page_name IS NOT NULL
AND COALESCE(json_extract(metadata_json, '$.status'), '') != 'deprecated'
GROUP BY library
ORDER BY library`,
)
.bind(owner)
.all<{ library: string; card_count: number }>();
const stats = (rows.results ?? []).map((r) => ({ library: r.library, card_count: r.card_count }));
return c.json({ success: true, stats });
});
// GET /entries — list with filters (entry_type, owner_id, parent_id, page_name, source, q/search)
// e.g. list workflows under a project: ?parent_id=PROJECT&entry_type=workflow
// e.g. get one by idempotency key: ?page_name=skill-rag_with_arcrun
+32
View File
@@ -19,6 +19,38 @@ recordRoutes.post('/', async (c) => {
}
});
// GET /records/triplet-stats?owner_id=... — 每個庫的三元組(關聯)數。
// t1422026-07-29):政府驗收——顯示每個庫整理出幾條知識關聯。
// 計法:依 triplet 型 record 的 'library' slot 值分組計數。無 library slot 的舊三元組歸 general。
// 使用子查詢先取 distinct triplet record IDs(針對 owner),再 LEFT JOIN library slot
// 避免 N+1(全部一次 SQL 完成,不逐筆 getRecord)。
recordRoutes.get('/triplet-stats', async (c) => {
const owner = c.req.query('owner_id') || '';
// 子查詢:找到屬於這個 owner 的所有 triplet recordsLEFT JOIN library slot 取庫名
const rows = await c.env.DB.prepare(
`SELECT
COALESCE(NULLIF(lib_e.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')
ORDER BY library`,
)
.bind(owner)
.all<{ library: string; triplet_count: number }>();
const stats = (rows.results ?? []).map((r) => ({ library: r.library, triplet_count: r.triplet_count }));
return c.json({ success: true, stats });
});
// GET /records/by-template/:template — list records of a template
recordRoutes.get('/by-template/:template', async (c) => {
const records = await searchByTemplate(c.env.DB, c.req.param('template'), c.req.query('owner_id') || undefined);
@@ -27,7 +27,11 @@ input_schema:
refresh — 強制重新 refresh,更新 CREDENTIALS_KV 中的 access_token/expires_at
api_key:
type: string
description: 租戶識別(ak_ 前綴),用來組 {api_key}:cred:{name} KV key
description: >-
租戶識別=Arcrun namespace,用來組 {api_key}:cred:{name} KV key。
⚠️ 2026-07-29 更正:舊敘述寫「ak_ 前綴」,但現行沒有發 API key 的機制
leo 07-29 指正)——namespace 即身分即憑證。下方 examples 的 ak_test/ak_nonexistent
是測試用假值,不代表真實格式。
service:
type: string
description: auth recipe 名稱,對應 auth_recipe:{service} 的 KV 記錄
@@ -24,7 +24,11 @@ input_schema:
description: 目前僅支援 authenticate
api_key:
type: string
description: 租戶識別(ak_ 前綴),用來組 {api_key}:cred:{name} KV key
description: >-
租戶識別=Arcrun namespace,用來組 {api_key}:cred:{name} KV key。
⚠️ 2026-07-29 更正:舊敘述寫「ak_ 前綴」,但現行沒有發 API key 的機制
leo 07-29 指正)——namespace 即身分即憑證。下方 examples 的 ak_test/ak_nonexistent
是測試用假值,不代表真實格式。
service:
type: string
description: auth recipe 名稱,對應 auth_recipe:{service} 的 KV 記錄
@@ -24,7 +24,11 @@ input_schema:
description: 目前僅支援 authenticate;static_key 無 refresh 概念
api_key:
type: string
description: 租戶識別(ak_ 前綴),用來組 {api_key}:cred:{name} KV key
description: >-
租戶識別=Arcrun namespace,用來組 {api_key}:cred:{name} KV key。
⚠️ 2026-07-29 更正:舊敘述寫「ak_ 前綴」,但現行沒有發 API key 的機制
leo 07-29 指正)——namespace 即身分即憑證。下方 examples 的 ak_test/ak_nonexistent
是測試用假值,不代表真實格式。
service:
type: string
description: auth recipe 名稱,對應 auth_recipe:{service} 的 KV 記錄
@@ -20,7 +20,12 @@ input_schema:
properties:
api_key:
type: string
description: KBDB partner keyak_xxx
description: >-
租戶識別=Arcrun namespace(送出時放 X-Arcrun-API-Key header)。
⚠️ 2026-07-29 更正:舊敘述寫「KBDB partner keyak_xxx)」,但**現行沒有發
API key 的機制**leo 07-29 指正)——namespace 即身分即憑證。
workflow 裡一律寫 {{credential.arcrun_namespace}},值由執行期解析(D36)。
欄位名維持 api_key(改名會破壞現有 workflow),只是它裝的是 namespace。
page_name:
type: string
description: 當 idempotency key。內部用 GET /blocks?page_name= 查找。
+3 -3
View File
@@ -14,7 +14,7 @@ config:
list_unprocessed:
component: kbdb_get
api_key: "{{api_key}}"
api_key: "{{credential.arcrun_namespace}}"
type: "note"
source: "user-input"
limit: 20
@@ -32,7 +32,7 @@ config:
trigger_processor:
component: trigger_workflow
workflow_name: "your_processor_workflow" # ← 改成你的處理 workflow 名
api_key: "{{api_key}}"
api_key: "{{credential.arcrun_namespace}}"
input:
api_key: "{{api_key}}"
api_key: "{{credential.arcrun_namespace}}"
block_id: "{{item.id}}"
+1 -1
View File
@@ -17,7 +17,7 @@ config:
fetch_kbdb_yesterday:
component: kbdb_get
api_key: "{{api_key}}"
api_key: "{{credential.arcrun_namespace}}"
type: "note"
source: "km-writer-direct"
limit: 50
@@ -33,7 +33,7 @@ config:
url: "https://api.github.com/repos/{{input.repository.full_name}}/issues/{{input.issue.number}}/comments"
method: POST
headers:
Authorization: "Bearer {{secret.GITHUB_BOT_TOKEN}}"
Authorization: "Bearer {{credential.github_bot_token}}"
Accept: "application/vnd.github+json"
body_json:
body: "{{analyze.first_response}}"
@@ -43,7 +43,7 @@ config:
url: "https://api.github.com/repos/{{input.repository.full_name}}/issues/{{input.issue.number}}/labels"
method: POST
headers:
Authorization: "Bearer {{secret.GITHUB_BOT_TOKEN}}"
Authorization: "Bearer {{credential.github_bot_token}}"
body_json:
labels:
- "auto-triaged"
@@ -36,7 +36,7 @@ config:
method: GET
url: "https://git.uncle6.me/api/v1/repos/{{repo}}/contents/system-dev/wiki/cards?ref={{ref}}"
headers:
Authorization: "token {{gitea_token}}"
Authorization: "token {{credential.gitea_token}}"
Accept: "application/json"
# 下游用 filter/set 取「游標之後第一張、且 .md、且非 00-INDEX」的一張。
@@ -46,7 +46,7 @@ config:
method: GET
url: "{{pick_next_card.next.download_url}}"
headers:
Authorization: "token {{gitea_token}}"
Authorization: "token {{credential.gitea_token}}"
# 4) ★ 機械解析 —— 通用 code 零件(sandbox inline JS,無 LLM、無 fs/網路,stdin→stdout JSON)。
# Arcrun#10 裁定:一次性解析邏輯走通用逃生口,不再鑄 domain 零件 km_wiki_card_parse。
@@ -414,7 +414,7 @@ config:
# metadata.embed=true → base embed 模組會補嵌 → 語意可搜。
upsert_entry:
component: kbdb_upsert_block
api_key: "{{kbdb_api_key}}"
api_key: "{{credential.arcrun_namespace}}"
kbdb_url: "{{kbdb_url}}"
page_name: "{{parse_card.data.entry.page_name}}"
type: "{{parse_card.data.entry.entry_type}}"
@@ -438,7 +438,7 @@ config:
url: "{{graph_url}}/triplets/ingest"
headers:
Content-Type: "application/json"
X-Arcrun-API-Key: "{{graph_api_key}}"
X-Arcrun-API-Key: "{{credential.arcrun_namespace}}"
body_json: "{{envelope}}" # envelope 已符合 ingest-candidate.json 契約(禁止欄位已排除)
# ── 執行環境變數(部署時注入;此檔不放密鑰)──
+1 -1
View File
@@ -24,7 +24,7 @@ config:
save_with_tag:
component: kbdb_create_block
api_key: "{{api_key}}"
api_key: "{{credential.arcrun_namespace}}"
type: "note"
source: "llm-classified"
user_id: "ai_classifier"
@@ -12,24 +12,24 @@ config:
dispatch_to_summary:
component: trigger_workflow
workflow_name: "llm_classify_example" # 改成你的 summary workflow
api_key: "{{api_key}}"
api_key: "{{credential.arcrun_namespace}}"
input:
api_key: "{{api_key}}"
api_key: "{{credential.arcrun_namespace}}"
text: "{{input.text}}"
dispatch_to_translate:
component: trigger_workflow
workflow_name: "your_translate_workflow"
api_key: "{{api_key}}"
api_key: "{{credential.arcrun_namespace}}"
input:
api_key: "{{api_key}}"
api_key: "{{credential.arcrun_namespace}}"
text: "{{input.text}}"
target_lang: "{{input.target_lang}}"
dispatch_to_classify:
component: trigger_workflow
workflow_name: "llm_classify_example"
api_key: "{{api_key}}"
api_key: "{{credential.arcrun_namespace}}"
input:
api_key: "{{api_key}}"
api_key: "{{credential.arcrun_namespace}}"
text: "{{input.text}}"
@@ -18,7 +18,7 @@ config:
# source 用 file_url 當去重 key(同 PDF 重 ingest 不會重複建)
ingest_to_kbdb:
component: kbdb_ingest
api_key: "{{api_key}}"
api_key: "{{credential.arcrun_namespace}}"
page_name: "pdf-{{input.title}}"
text: "{{convert_pdf.data.text}}"
source: "pdf:{{input.pdf_url}}"
@@ -8,7 +8,7 @@ flow:
config:
search_kbdb:
component: kbdb_search
api_key: "{{api_key}}"
api_key: "{{credential.arcrun_namespace}}"
query: "{{input.question}}"
topK: 5
user_id: "{{input.user_id}}" # 可選,限定某用戶 namespace
@@ -305,6 +305,26 @@
KV key 設計:`{tenant}:portal:ai_config`(合併設定)`{tenant}:portal:daemon_caps`(能力回報,TTL 7 天)
測試:portal-admin.test.ts 新增 7 案(全通;全套 238 tests 229 passed9 failed 皆 pre-existing)。
- [x] **t142 庫目錄顯示同步張數+關聯數(2026-07-29,任務層小改)**
來源=leo 裁定(政府專案驗收)「雲端子庫顯示同步了幾個 wiki+幾個三元組,一眼看出這個庫真的有東西」。
後端(kbdb):
`GET /entries/library-stats?owner_id=``{stats: [{library, card_count}]}`
SQL`COUNT(DISTINCT page_name)` GROUP BY library(僅 entry_type='block',排 deprecated)。
⚠️ 卡數=distinct page_name(一張卡 3-5 個 block),不是 COUNT(*)(防膨脹 3-5 倍)。
路由在 `/libraries` 之後、`/` 之前(避免被 `/:id` 吃掉)。
`GET /records/triplet-stats?owner_id=``{stats: [{library, triplet_count}]}`
SQLsubquery DISTINCT triplet record IDs + LEFT JOIN library slot(無 slot→general),
一次 SQL 完成,不 N+1。路由在 `/by-template/:template` 之前。
後端(portal.ts):`GET /portal/admin/libraries` 改並行撈三端點(Promise.all + .catch(→null));
已登記庫與 auto 庫各補 card_count + triplet_countMap O(1) 查找),任一失敗不炸主流程。
前端(index.html renderAdminLibs):每張庫卡片補一行
「N 張知識卡・M 條關聯」(只顯示非零項);兩者均 0 → 顯示「還沒有內容」,不顯示「0 張」。
測試:`kbdb/tests/library-stats.test.ts`14 新案:SQL 形狀/COUNT DISTINCT/entry_type filter/
deprecated filter/COALESCE general fallback/參數傳入/回傳結構/空陣列不炸);
`portal-admin.test.ts`(t97 既有測試補三端點 mock + t142 describe 3 案:
已登記庫帶 stats/auto 庫帶 stats/空庫 card_count=triplet_count=0)。
vitest + node --check 待 leo 驗收環境跑(本機無 Workers runtime)。
## 第二波(不在本 SDD 動工範圍,掛號)
- MCP token 綁庫集合(design §9PR#15 擴充,只動 `mcp/`