fix(semantic): 故障照實說是故障——不再把壞掉說成「沒開通」(leo 2026-08-09 直令)
一、文案(portal/console/kbdb hint):語意搜尋是一安裝就提供的功能,
降級=故障。橫幅改「語意搜尋目前故障/我們的問題/你不用做任何事」,
拿掉「還沒開通、想開通請匯出診斷檔」這種要使用者申請開通的假框架。
kbdb 降級回應加 degraded_reason(module_off / embed_query_failed)。
二、查詢向量化失敗不再偽裝成空結果(leo 點名的謊):
semanticSearch 舊行為「AI 額度用完 → 回 []」會讓使用者以為
自己的知識庫裡沒有這筆資料。改丟 EmbedQueryFailedError,
route 誠實降級 keyword+照實告知是暫時故障。
三、源頭機制(裝好的實例為什麼會失去語意搜尋):
- acr update:kbdb_embed 判斷 ===true → !==false。config 缺欄位時
redeploy 會把 [[vectorize]]+[ai] binding 靜默剝掉(wrangler deploy
整份覆蓋),一台正常實例就此壞掉。init 預設同步翻成 [Y/n]。
-(另 repo)deploy-all.mjs ensureVectorizeIndex 失敗改致命中止。
四、順手自癒:孤兒向量/下架殘影搜尋時背景清除;空結果且 pending>0
背景 backfill;no_index 拆「故障」vs「還沒有資料」兩態。
測試:kbdb 146/146(新增 degraded 6 案+selftest 1 案);cli 10/10;
瀏覽器端到端兩種故障畫面實測(local wrangler dev+portal 真登入)。
無 SDD 對應:leo 直令修故障(同 08-07 檢修孔前例的人閘直接授權路徑)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+37
-3
@@ -325,7 +325,16 @@ export async function embedSelfTest(
|
||||
return { enabled: true, tested: false, passed: null, note: '取樣卡片內容為空,跳過自我檢查' };
|
||||
}
|
||||
// min_score:0——自我檢查要看「找不找得到」,不能被查詢端的相對門檻先濾掉。
|
||||
const hits = await semanticSearch(env, sample, { owner_id: opts.owner_id, topK: 10, min_score: 0 });
|
||||
let hits: SemanticHit[] | null;
|
||||
try {
|
||||
hits = await semanticSearch(env, sample, { owner_id: opts.owner_id, topK: 10, min_score: 0 });
|
||||
} catch (e) {
|
||||
if (e instanceof EmbedQueryFailedError) {
|
||||
// 向量化本身失敗(額度用完/模型故障)=「這條路現在是斷的」,誠實回報,不算 passed/failed。
|
||||
return { enabled: true, tested: false, passed: null, note: `自我檢查沒跑成:${e.message}(語義搜尋此刻同樣會故障,多半是 Workers AI 額度或服務問題)` };
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
if (hits === null) {
|
||||
return { enabled: false, tested: false, passed: null, note: 'embed 模組回報未開(binding 檢查期間消失,罕見)' };
|
||||
}
|
||||
@@ -349,6 +358,22 @@ export interface SemanticHit {
|
||||
library?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查詢向量化失敗(2026-08-09 leo 直令:「查詢的向量化如果失敗(例如當天額度用完),
|
||||
* 目前會回一個空的結果集——那是騙人,不是降級」)。
|
||||
*
|
||||
* 舊行為:embedText 拿不到向量 → semanticSearch 回 [],caller 分不出
|
||||
* 「真的沒命中」和「根本沒查成」,使用者看到「查無資料」,以為知識庫裡沒有這筆東西。
|
||||
* 新行為:AI.run 丟錯(額度用完/模型故障)或回不出向量 → 丟這個錯,
|
||||
* 由 route 層誠實降級 keyword +告知「這是我們的故障」,不再偽裝成空結果。
|
||||
*/
|
||||
export class EmbedQueryFailedError extends Error {
|
||||
constructor(detail: string) {
|
||||
super(`查詢向量化失敗:${detail}`);
|
||||
this.name = 'EmbedQueryFailedError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 語義搜尋(mode:'semantic')。模組未開 → 回 null(caller 降級 keyword + 告知缺能力)。
|
||||
* owner_id / source / entry_type 過濾走 Vectorize metadata filter(entry_type 已 index,見上 upsert metadata)。
|
||||
@@ -369,8 +394,17 @@ export async function semanticSearch(
|
||||
opts: { owner_id?: string; source?: string; entry_type?: string; library?: string[]; topK?: number; min_score?: number } = {},
|
||||
): Promise<SemanticHit[] | null> {
|
||||
if (!embedEnabled(env)) return null;
|
||||
const vec = await embedText(env, q);
|
||||
if (!vec) return [];
|
||||
// 空查詢=真的沒東西可查(route 層已擋 q 必填,這裡只兜底),不算故障。
|
||||
if (!(q ?? '').trim()) return [];
|
||||
// 🔴 2026-08-09(leo 直令):向量化失敗**不准**回空結果集。空結果=「你的庫裡沒有」,
|
||||
// 向量化失敗=「我們沒查成」——兩者對使用者是完全不同的事實,混在一起就是說謊。
|
||||
let vec: number[] | null;
|
||||
try {
|
||||
vec = await embedText(env, q);
|
||||
} catch (e) {
|
||||
throw new EmbedQueryFailedError(e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
if (!vec) throw new EmbedQueryFailedError('Workers AI 沒有回出向量(回應形狀異常或空回應)');
|
||||
const filter: VectorizeVectorMetadataFilter = {};
|
||||
if (opts.owner_id) filter.owner_id = opts.owner_id;
|
||||
if (opts.source) filter.source = opts.source;
|
||||
|
||||
Reference in New Issue
Block a user