portal 搜尋濾殘影(#46 治標)+四件套單測
/portal/data/search 拿到 KBDB 回應後 server-side 過濾 metadata_json.status= 'deprecated' 與 content 以「(舊管線產物」開頭的 entries(metadata parse 失敗 視為保留,不誤殺),count 重算。這是 Arcrun#46 上游修好前的 portal 端治標, 上游清完資料後整段(含 filterDeprecatedEntries)可拔。 單測:sanitizeUploadFilename(路徑穿越/副檔名/長度)、filterDeprecatedEntries (濾殘影/壞 metadata 保留)、mapGraphWorkflowOutput(#57 輸出映射); /portal/session 補 upload_enabled=false 斷言。另修 portal-ui 一處註解字樣 (誤用產品名,撞零 Mira 斷言)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BH7LdhuCdVUHfHXbM7N8r5
This commit is contained in:
@@ -94,6 +94,28 @@ function canReadLibrary(userLibraries: string[], library: string): boolean {
|
||||
return userLibraries.includes('*') || userLibraries.includes(library);
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜尋殘影過濾(**Arcrun#46 上游修好前的 portal 端治標**——舊管線的 deprecated 產物還躺在
|
||||
* KBDB 裡污染搜尋結果;根治=上游清資料/重建索引,那修好後這段可整段拔掉)。
|
||||
* 濾掉:metadata_json.status === 'deprecated' 的 entry、content 以「(舊管線產物」開頭的 entry。
|
||||
* metadata_json parse 失敗 → 視為保留(治標不誤殺;壞 metadata ≠ deprecated)。
|
||||
* 純函式(單測用 export)。
|
||||
*/
|
||||
export function filterDeprecatedEntries<T extends { metadata_json?: string | null; content?: string | null }>(
|
||||
entries: T[],
|
||||
): T[] {
|
||||
return entries.filter((e) => {
|
||||
try {
|
||||
const meta = JSON.parse(e.metadata_json ?? 'null') as { status?: unknown } | null;
|
||||
if (meta && meta.status === 'deprecated') return false;
|
||||
} catch {
|
||||
/* parse 失敗 → 保留 */
|
||||
}
|
||||
if (String(e.content ?? '').startsWith('(舊管線產物')) return false;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// GET /portal/data/search?q=&mode=&entry_type=&limit= — 三模式中的 keyword/semantic
|
||||
//(graph 走 /portal/data/graph/*)。server 注入 owner_id+library;回應照 KBDB 原形
|
||||
//(entries 含 metadata_json,前端自取 source 溯源;mode/capability_hint 誠實透傳——
|
||||
@@ -122,7 +144,21 @@ portalDataRouter.get('/portal/data/search', (c) =>
|
||||
if (limit && /^\d{1,3}$/.test(limit)) params.set('limit', limit);
|
||||
|
||||
const res = await kbdbFetch(c.env, `/entries/search?${params.toString()}`);
|
||||
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
|
||||
if (!res.ok) {
|
||||
// 錯誤回應照原樣透傳(誠實,不加工)
|
||||
return new Response(res.body, { status: res.status, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
// Arcrun#46 治標:server-side 濾掉舊管線 deprecated 殘影再回(count 重算)。
|
||||
// 上游修好(清資料/重建索引)後,這段連同 filterDeprecatedEntries 一起拔掉。
|
||||
const body = (await res.json().catch(() => null)) as
|
||||
| { entries?: { metadata_json?: string | null; content?: string | null }[]; count?: number }
|
||||
| null;
|
||||
if (!body || !Array.isArray(body.entries)) {
|
||||
// 回應不是預期形狀 → 照原樣回(不因治標把正常錯誤形狀吃掉)
|
||||
return c.json(body ?? { error: 'KBDB 回應不是 JSON' }, body ? 200 : 502);
|
||||
}
|
||||
const entries = filterDeprecatedEntries(body.entries);
|
||||
return c.json({ ...body, entries, count: entries.length });
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -554,7 +554,7 @@ function renderPortalHtml(brand: string): string {
|
||||
$('side-foot').innerHTML = esc(p.display_name || '') + '<br>' + esc(location.host);
|
||||
$('nav-workflows').classList.toggle('hide', !p.workflows_visible);
|
||||
$('tab-workflows').classList.toggle('hide', !p.workflows_visible);
|
||||
// 上傳 nav:一般用戶可見,但只在實例啟用上傳(bindings 齊全)時顯示(Mira 零影響)
|
||||
// 上傳 nav:一般用戶可見,但只在實例啟用上傳(bindings 齊全)時顯示(未啟用的實例零影響)
|
||||
$('nav-upload').classList.toggle('hide', !p.upload_enabled);
|
||||
$('tab-upload').classList.toggle('hide', !p.upload_enabled);
|
||||
$('nav-admin').classList.toggle('hide', p.role !== 'admin');
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import { SELF, env, fetchMock } from 'cloudflare:test';
|
||||
import { beforeAll, afterEach, describe, it, expect } from 'vitest';
|
||||
import { workflowsVisible } from '../src/routes/portal';
|
||||
import { entryLibrary } from '../src/routes/portal-data';
|
||||
import { entryLibrary, sanitizeUploadFilename, filterDeprecatedEntries, mapGraphWorkflowOutput } from '../src/routes/portal-data';
|
||||
import type { Bindings } from '../src/types';
|
||||
|
||||
const KBDB = 'https://kbdb.test';
|
||||
@@ -353,5 +353,58 @@ describe('GET /portal/session(P3 能力欄位)', () => {
|
||||
const data = (await res.json()) as Record<string, unknown>;
|
||||
expect(data.graph_allowed).toBe(true);
|
||||
expect(data.workflows_visible).toBe(true);
|
||||
// portal-demo-suite:測試環境未設 upload bindings → upload_enabled=false(Mira 零影響預設)
|
||||
expect(data.upload_enabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════ 7. portal-demo-suite 純函式 ═══════════════
|
||||
|
||||
describe('sanitizeUploadFilename(上傳檔名驗證)', () => {
|
||||
it('去路徑分隔(擋穿越)、.txt 改 .md、無副檔名補 .md', () => {
|
||||
expect(sanitizeUploadFilename('notes.md')).toBe('notes.md');
|
||||
expect(sanitizeUploadFilename('memo.txt')).toBe('memo.md');
|
||||
expect(sanitizeUploadFilename('README')).toBe('README.md');
|
||||
expect(sanitizeUploadFilename('../../etc/passwd')).toBe('passwd.md'); // 只取最後一段,穿越失效
|
||||
expect(sanitizeUploadFilename('a\\b\\c.md')).toBe('c.md');
|
||||
expect(sanitizeUploadFilename('中文筆記.txt')).toBe('中文筆記.md');
|
||||
});
|
||||
it('空名/純路徑/隱藏檔/超過 100 字/非字串 → null', () => {
|
||||
expect(sanitizeUploadFilename('')).toBe(null);
|
||||
expect(sanitizeUploadFilename(' ')).toBe(null);
|
||||
expect(sanitizeUploadFilename('docs/')).toBe(null);
|
||||
expect(sanitizeUploadFilename('.env')).toBe(null);
|
||||
expect(sanitizeUploadFilename('a'.repeat(120) + '.md')).toBe(null);
|
||||
expect(sanitizeUploadFilename(42)).toBe(null);
|
||||
expect(sanitizeUploadFilename(undefined)).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterDeprecatedEntries(Arcrun#46 搜尋殘影治標)', () => {
|
||||
it('濾 status=deprecated 與「(舊管線產物」開頭;metadata parse 失敗保留', () => {
|
||||
const keepNormal = { metadata_json: '{"library":"general"}', content: '正常內容' };
|
||||
const keepBadMeta = { metadata_json: 'not-json{{', content: '壞 metadata 不誤殺' };
|
||||
const keepNullMeta = { metadata_json: null, content: '無 metadata' };
|
||||
const dropByStatus = { metadata_json: '{"status":"deprecated"}', content: '看起來正常但已標廢' };
|
||||
const dropByContent = { metadata_json: '{}', content: '(舊管線產物)殘影條目' };
|
||||
const out = filterDeprecatedEntries([keepNormal, dropByStatus, keepBadMeta, dropByContent, keepNullMeta]);
|
||||
expect(out).toEqual([keepNormal, keepBadMeta, keepNullMeta]);
|
||||
});
|
||||
it('空陣列 → 空陣列', () => {
|
||||
expect(filterDeprecatedEntries([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapGraphWorkflowOutput(#57 workflow 輸出 → plugin 形狀)', () => {
|
||||
it('本體有 neighbors → 直取;count 重算不信自報', () => {
|
||||
const out = mapGraphWorkflowOutput({ neighbors: ['a', 'b'], edges: [{ subject: 'a', predicate: 'rel', object: 'b' }], count: 99 });
|
||||
expect(out.neighbors).toEqual(['a', 'b']);
|
||||
expect(out.edges.length).toBe(1);
|
||||
expect(out.count).toBe(2);
|
||||
});
|
||||
it('包一層 data(http_request 慣例)→ 取內層;非物件/缺欄位 → 誠實空集合', () => {
|
||||
expect(mapGraphWorkflowOutput({ data: { neighbors: ['x'], edges: [] } })).toEqual({ neighbors: ['x'], edges: [], count: 1 });
|
||||
expect(mapGraphWorkflowOutput(null)).toEqual({ neighbors: [], edges: [], count: 0 });
|
||||
expect(mapGraphWorkflowOutput('oops')).toEqual({ neighbors: [], edges: [], count: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user