feat(portal): 新增 GET /portal/daemon/diagnostics(t213 matrix/arcrun 半部)

InkStoneCo 總管交辦(arcrun-rag repo t213):leo 拿真診斷檔實測四個真實問題,只答得出一題
——其餘三題需要地端資料(daemon 本機 manifest 總量/失敗分類/自我更新狀態),而現有
GET /portal/data/diagnostics 只有 portal session 認證,daemon 背景行程沒有 session
(密碼只在連線精靈當下用過就丟,不落地),構不到這支端點。

本次只做 matrix/arcrun 半部(雲端這半):
- 把 /portal/data/diagnostics 的核心查詢邏輯抽成 buildDiagnostics(env, tenant)
  (portal.ts),薄殼原則:能力只實作一次
- 新增 GET /portal/daemon/diagnostics,認證比照既有 /portal/daemon/extract
  (X-Arcrun-API-Key,非 session);apiKey 當 owner_id 用,不與 portalTenant(env)
  比對(t189 教訓:daemon 的 api_key 不保證等於 worker 的 CONSOLE_TENANT)
- /portal/data/diagnostics 改呼叫共用函式,回應形狀完全不變
- 按 leo 指示刪掉「需在失敗當下由封測者截圖」那句 notes(本機那半資料到位後這句話
  失去意義)

地端那半(arcrun-app 讀 manifest 合併 + 匯出按鈕)在 products/arcrun-rag repo 進行,
待另一隻處理 t210 的 agent 落地 app.go/main.js 改動後再接線,本次不動 arcrun-app。

驗證:
- npx tsc --noEmit:與 stash 前錯誤數相同(4 個),零新增(pre-existing,與本次改動無關)
- npx vitest run:314→315 通過(+3 新測試涵蓋 401/apiKey 當 owner_id 不比對 tenant/
  回應形狀與 session 版一致),14 個既有失敗與改動前完全相同(HTML 殼 404,測試環境
  static asset 問題,與 portal.ts/portal-data.ts 邏輯無關)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
uncle6me-web
2026-08-08 12:32:10 +08:00
parent 962d863ef7
commit 93b1140bf1
3 changed files with 264 additions and 131 deletions
+85
View File
@@ -857,3 +857,88 @@ describe('GET /portal/data/diagnostics', () => {
expect(body.library_scope_check.note).toContain('查詢方式或租戶對不上');
});
});
// ═══════════ 8. GET /portal/daemon/diagnosticst213daemon 免帳密版檢修孔,2026-08-08) ═══════════
//
// 與上面 /portal/data/diagnostics 共用同一個 buildDiagnostics()portal.ts)——這裡只驗證
// ①認證換了一套(X-Arcrun-API-Key,非 session)②apiKey 當 owner_id 打 KBDB,不與
// portalTenant(env)='leo',見上方 TENANT 常數)比對/不要求相等(t189 教訓)③回應形狀
// 與 session 版一致。核心查詢邏輯已在上面 7 組測試驗過,這裡不重複。
describe('GET /portal/daemon/diagnosticst213 daemon 版)', () => {
it('沒帶 X-Arcrun-API-Key → 401,不碰 KBDB', async () => {
const res = await get('/portal/daemon/diagnostics');
expect(res.status).toBe(401);
});
it('帶 key(刻意與 CONSOLE_TENANT="leo" 不同)→ 200,且 KBDB 查詢用的 owner_id 是這把 key 本身,不是 leot189:不假設 apiKey===portalTenant', async () => {
const daemonKey = 'yuga3bse'; // 刻意選一個跟 TENANT('leo') 不同的值,比照 t189 geek6688 案例
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/embed/backfill/status') && p.includes(`owner_id=${daemonKey}`), method: 'GET' })
.reply(200, { success: true, enabled: true, pending: 2, embedded: 9 });
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/embed/selftest') && p.includes(`owner_id=${daemonKey}`), method: 'GET' })
.reply(200, { success: true, enabled: true, tested: true, passed: true, note: '' });
mockLibraryList([{ record_id: 'rec_lib_kb2', values: { name: 'kb' } }]);
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/entries/libraries') && p.includes(`owner_id=${daemonKey}`), method: 'GET' })
.reply(200, { success: true, libraries: ['general', 'kb'], count: 2 });
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/records/triplet-stats') && p.includes(`owner_id=${daemonKey}`), method: 'GET' })
.reply(200, { success: true, stats: [{ library: 'kb', triplet_count: 9 }] });
const res = await get('/portal/daemon/diagnostics', { 'X-Arcrun-API-Key': daemonKey });
expect(res.status).toBe(200);
const body = (await res.json()) as {
library_count: number;
triplet_count: number;
embedding: { module_enabled: boolean; cards_embedded: number };
instance_url: string;
bundle_version: string | null;
};
expect(body.library_count).toBe(1);
expect(body.triplet_count).toBe(9);
expect(body.embedding.module_enabled).toBe(true);
expect(body.embedding.cards_embedded).toBe(9);
expect(body.instance_url).toBe('http://localhost');
// 沒有任何 session 檢查——不打 SESSIONS_KV/portal_user record(本測試從未 seedSession/mockGetRecord
// 仍然 200,證明這條路徑真的不吃 session)。
});
it('回應形狀與 session 版一致(同一組欄位名)', async () => {
const daemonKey = 'shape-check-key';
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/embed/backfill/status'), method: 'GET' })
.reply(200, { success: true, enabled: false, pending: 0, embedded: 0 });
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/embed/selftest'), method: 'GET' })
.reply(200, { success: true, enabled: false, tested: false, passed: null, note: '' });
mockLibraryList([]);
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/entries/libraries'), method: 'GET' })
.reply(200, { success: true, libraries: [], count: 0 });
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/records/triplet-stats'), method: 'GET' })
.reply(200, { success: true, stats: [] });
fetchMock
.get(KBDB)
.intercept({ path: (p: string) => p.startsWith('/entries?'), method: 'GET' })
.reply(200, { success: true, entries: [], count: 0, total: 0 });
const res = await get('/portal/daemon/diagnostics', { 'X-Arcrun-API-Key': daemonKey });
expect(res.status).toBe(200);
const body = (await res.json()) as Record<string, unknown>;
expect(Object.keys(body).sort()).toEqual(
['generated_at', 'instance_url', 'bundle_version', 'library_count', 'triplet_count', 'library_scope_check', 'embedding', 'notes'].sort(),
);
// t213 leo 08-08 指令:舊的「需在失敗當下截圖」那句已刪,notes 不該再含這句話。
expect(JSON.stringify(body.notes)).not.toContain('截圖');
});
});