feat(t122 🔴🔴): 萃取引擎雲端設定+隨連線下發——封測者終於萃得出東西

真兇(總管查證):daemon/config 寫死 extractor='claude' 且不下發金鑰 ⇒ 封測者 100% 萃取失敗
(leo:「地端沒有 AI 根本不能萃,那它就不能玩」)。
- POST/GET /portal/admin/extractor(admin 閘;GET 只回 has_key 不回明文)
- daemon/config 改讀設定:未設定→gemma 無金鑰;設定後→含 gemini_api_key
- portal 設定頁加「萃取引擎」區(Gemini 推薦+aistudio 連結,存後提示「小幫手點連上知識庫重連即可」)
測試 3 條新綠(vitest 17 passed;1 紅=console HTML 搬遷陳舊測試,非本案)。
(實作=子 CC;驗證+commit=總管)
This commit is contained in:
uncle6me-web
2026-07-29 13:21:26 +08:00
parent 429b2d965f
commit eb9f2db513
4 changed files with 255 additions and 12 deletions
+75 -11
View File
@@ -755,10 +755,32 @@ portalRouter.post('/portal/daemon/libraries', (c) =>
}),
);
// ── t122 萃取引擎設定(daemon 萃取用;與 chat-key AI 問答金鑰獨立管理)──────────────
// KV key = {tenant}:portal:extractor_config,存在 WEBHOOKS KV(同 chat-key 手法)。
// 金鑰不落 logGET 只回 has_key:bool,不回明文。
// daemon 未設定時預設 gemma(封測者不會有 claude,以 gemma 為友善預設)。
interface ExtractorConfig {
engine: 'gemma' | 'claude';
gemini_api_key?: string;
llm_model?: string;
}
function extractorConfigKey(env: Bindings): string {
return `${portalTenant(env)}:portal:extractor_config`;
}
async function getExtractorConfig(env: Bindings): Promise<ExtractorConfig | null> {
const raw = await env.WEBHOOKS.get(extractorConfigKey(env), 'text');
if (!raw) return null;
try { return JSON.parse(raw) as ExtractorConfig; } catch { return null; }
}
// POST /portal/daemon/config — body {email, password}。同步小幫手憑「用戶剛設的帳密」
// 直接換到自己的設定(t54,leo 07-25:「最好的就是把它的帳密直接輸入」)——
// 用戶不必再下載 config.json 丟隱藏資料夾,托盤第一次開啟輸入網址+帳密就上工。
// 認證=與 /portal/login 同一把(同樣吃節流與停用檢查);回傳只含連線設定,不含任何知識內容。
// t122extractor 改讀雲端設定(未設→預設 gemma;gemma+金鑰→一併下發金鑰)。
portalRouter.post('/portal/daemon/config', (c) =>
run(c, async () => {
const body = (await c.req.json().catch(() => null)) as { email?: string; password?: string } | null;
@@ -781,17 +803,21 @@ portalRouter.post('/portal/daemon/config', (c) =>
}
await clearLoginFail(c.env, email);
const tenant = portalTenant(c.env);
return c.json({
success: true,
config: {
cypher_url: new URL(c.req.url).origin,
namespace: tenant,
library: 'kb',
extractor: 'claude',
email,
instance_name: String(rec.values.display_name ?? ''),
},
});
const extractorCfg = await getExtractorConfig(c.env);
const engine = extractorCfg?.engine ?? 'gemma';
const daemonCfg: Record<string, string> = {
cypher_url: new URL(c.req.url).origin,
namespace: tenant,
library: 'kb',
extractor: engine,
email,
instance_name: String(rec.values.display_name ?? ''),
};
if (engine === 'gemma' && extractorCfg?.gemini_api_key) {
daemonCfg.gemini_api_key = extractorCfg.gemini_api_key;
}
if (extractorCfg?.llm_model) daemonCfg.llm_model = extractorCfg.llm_model;
return c.json({ success: true, config: daemonCfg });
}),
);
@@ -837,6 +863,44 @@ portalRouter.post('/portal/admin/chat-key', (c) =>
}),
);
// POST /portal/admin/extractor — body {engine, gemini_api_key?, llm_model?}t122)。
// admin 閘(同 chat-key 等級)。金鑰不落 log;存 WEBHOOKS KV。
portalRouter.post('/portal/admin/extractor', (c) =>
run(c, async () => {
const auth = await requirePortalAdmin(c);
if (!auth.ok) return auth.res;
const body = (await c.req.json().catch(() => null)) as { engine?: string; gemini_api_key?: string; llm_model?: string } | null;
const engine = String(body?.engine ?? '').trim().toLowerCase();
if (engine !== 'gemma' && engine !== 'claude') {
return c.json({ error: 'engine 只能是 gemma 或 claude' }, 400);
}
const cfg: ExtractorConfig = { engine: engine as 'gemma' | 'claude' };
if (engine === 'gemma') {
const key = String(body?.gemini_api_key ?? '').trim();
if (key) cfg.gemini_api_key = key;
}
const model = String(body?.llm_model ?? '').trim();
if (model) cfg.llm_model = model;
await c.env.WEBHOOKS.put(extractorConfigKey(c.env), JSON.stringify(cfg));
return c.json({ success: true, engine: cfg.engine, has_key: engine === 'gemma' && !!cfg.gemini_api_key });
}),
);
// GET /portal/admin/extractor — 回 engine + has_key(不回金鑰明文)(t122)。
portalRouter.get('/portal/admin/extractor', (c) =>
run(c, async () => {
const auth = await requirePortalAdmin(c);
if (!auth.ok) return auth.res;
const cfg = await getExtractorConfig(c.env);
return c.json({
success: true,
engine: cfg?.engine ?? 'gemma',
has_key: cfg?.engine === 'gemma' && !!cfg?.gemini_api_key,
llm_model: cfg?.llm_model ?? null,
});
}),
);
// GET /portal/admin/libraries — 庫目錄列表。
// t52leo 2026-07-26:「地端 2 個資料夾、雲端就要 2 個庫,只有一個一定被罵」):
// 除了登記簿裡的庫,**也把資料裡實際蓋過章的庫一併列出**(標 auto:true)——