t176 daemon:LLM 設定移回地端+托盤單一實例(leo 08-03 架構翻案)
leo 回報 Windows daemon 三症狀,查完 ①③ 同根,根在「雲端控制地端」這個設計。 【①③ 真兇】雲端 extractor_config 是**全租戶共用一把 KV** (arcrun:portal.ts:43 portalTenant = worker 層級變數,不分用戶), 任一處設了 claude → 所有人的 daemon 都收到 claude。沒裝 Claude Code 的機器 FindClaudeBin 失敗 → 每檔萃取 failed、一張卡都沒建;想去 portal 改回 gemini, checkbox 卻恆 disabled(claude_available 恆 false,因 daemon 從未實作 report-capabilities 回報 → daemon_caps KV 永遠空)⇒ 用戶自己解不開。 awindhon 實證:雲端同步成功、Gemini key 有效、零張卡,config.json extractor="claude"。 【leo 裁示】「地端要用什麼模型就在 daemon 上輸入 API Key 設置,而不是雲端設置後 控制地端」「地端先限制 Gemini API Key 配合客戶要求」「雲端就是 Workers AI」。 本次(daemon 端): - addOrUpdateAccount 不再接受雲端下發的 extractor/gemini_api_key/llm_model, 只收連線欄位。t126「每帳號一份萃取設定」照舊保留——t126 修的是「存在哪一層」, 本次改的是「值從哪來」,兩者正交。 - 托盤新增「AI 設定…」:使用者自己填 Gemini API Key,寫本地 config 後立即生效。 - 萃取一律走 gemini:殘留的 extractor:"claude" 正規化為 gemma;claude 路退役。 ⚠️ 這不是「自動偵測有無 claude」(leo 07-27 已否決的 B 案),是整條路先不支援。 - 清掉隨之死亡的 claude_bin 回寫(死代碼=錯誤的環境信號)。 - 托盤單一實例(症狀②):pidfile + 跨平台 processAlive。 mac 之前不多開是借 macOS Launch Services 的巧合,Windows 沒有該層 ⇒ 每點一次多一個 icon。 Unix 用 signal 0(EPERM 也算活著,測試抓到的實際 bug)/Windows 用 OpenProcess+ExitCode。 測試:collector 全綠、tray 全綠。5 個原本用 claude stub 的測試改走**真實 gemma 路** (httptest 替身注入 gemmaBaseURL),不是改斷言充綠;t126②③ 兩案翻轉成 「雲端下發一律被忽略」的回歸守衛;新增 4 案 single-instance。 未送達:本 commit 只到 code,尚未打包出貨;雲端側(刪 portal AI 設定區塊、 extractor 下發、admin/extractor)未動,待部署授權。CP rag-beta 步驟仍為 ◐。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -511,72 +511,82 @@ func TestAccountEngineLabel(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// t126②:addOrUpdateAccount 把 extractor/key 寫進帳號層,機器層不被覆蓋。
|
||||
func TestAddOrUpdateAccount_WritesExtractorToAccountLayer(t *testing.T) {
|
||||
// t176(翻轉自 t126②):新帳號也一樣——只收連線欄位,雲端下發的 LLM 設定一律不落地。
|
||||
// 帳號層留白時,讀取端會繼承機器層(=使用者在「AI 設定…」填的那把)。
|
||||
func TestAddOrUpdateAccount_NewAccountIgnoresRemoteLLMSettings(t *testing.T) {
|
||||
cfg := &directConfig{
|
||||
Extractor: "claude", // 機器層原有值
|
||||
GeminiAPIKey: "machine-key", // 機器層原有值
|
||||
Extractor: "gemma", // 機器層=使用者自己填的
|
||||
GeminiAPIKey: "machine-key",
|
||||
}
|
||||
resp := &daemonConfigResp{Success: true}
|
||||
resp.Config.CypherURL = "https://new.workers.dev"
|
||||
resp.Config.Namespace = "ns1"
|
||||
resp.Config.Extractor = "gemma"
|
||||
resp.Config.GeminiAPIKey = "account-key"
|
||||
resp.Config.LLMModel = "gemma-4-31b-it"
|
||||
resp.Config.Extractor = "claude" // 雲端想下發 claude
|
||||
resp.Config.GeminiAPIKey = "cloud-key" // 以及別把金鑰
|
||||
resp.Config.LLMModel = "some-cloud-model" //
|
||||
|
||||
isNew := addOrUpdateAccount(cfg, resp)
|
||||
if !isNew {
|
||||
if isNew := addOrUpdateAccount(cfg, resp); !isNew {
|
||||
t.Error("應為新帳號")
|
||||
}
|
||||
if len(cfg.Accounts) == 0 {
|
||||
t.Fatal("應新增帳號")
|
||||
}
|
||||
// 帳號層應有新值
|
||||
if cfg.Accounts[0].Extractor != "gemma" {
|
||||
t.Errorf("帳號層 Extractor 錯:got %q", cfg.Accounts[0].Extractor)
|
||||
// 連線欄位要寫進去(這條路徑本來的職責)
|
||||
if cfg.Accounts[0].Namespace != "ns1" || cfg.Accounts[0].CypherURL != "https://new.workers.dev" {
|
||||
t.Errorf("連線欄位應寫入,got ns=%q url=%q", cfg.Accounts[0].Namespace, cfg.Accounts[0].CypherURL)
|
||||
}
|
||||
if cfg.Accounts[0].GeminiAPIKey != "account-key" {
|
||||
t.Errorf("帳號層 GeminiAPIKey 錯:got %q", cfg.Accounts[0].GeminiAPIKey)
|
||||
// LLM 欄位一律留白=不吃雲端的
|
||||
if cfg.Accounts[0].Extractor != "" {
|
||||
t.Errorf("不該吃雲端下發的 extractor,got %q", cfg.Accounts[0].Extractor)
|
||||
}
|
||||
if cfg.Accounts[0].LLMModel != "gemma-4-31b-it" {
|
||||
t.Errorf("帳號層 LLMModel 錯:got %q", cfg.Accounts[0].LLMModel)
|
||||
if cfg.Accounts[0].GeminiAPIKey != "" {
|
||||
t.Errorf("不該吃雲端下發的金鑰,got %q", cfg.Accounts[0].GeminiAPIKey)
|
||||
}
|
||||
// 機器層不應被覆蓋
|
||||
if cfg.Extractor != "claude" {
|
||||
t.Errorf("機器層 Extractor 不應被覆蓋,got %q", cfg.Extractor)
|
||||
if cfg.Accounts[0].LLMModel != "" {
|
||||
t.Errorf("不該吃雲端下發的模型,got %q", cfg.Accounts[0].LLMModel)
|
||||
}
|
||||
if cfg.GeminiAPIKey != "machine-key" {
|
||||
t.Errorf("機器層 GeminiAPIKey 不應被覆蓋,got %q", cfg.GeminiAPIKey)
|
||||
// 機器層(使用者自己填的)不受影響
|
||||
if cfg.Extractor != "gemma" || cfg.GeminiAPIKey != "machine-key" {
|
||||
t.Errorf("機器層不該被雲端動到,got extractor=%q key=%q", cfg.Extractor, cfg.GeminiAPIKey)
|
||||
}
|
||||
}
|
||||
|
||||
// t126③:同 host 更新帳號時 extractor 也寫帳號層。
|
||||
func TestAddOrUpdateAccount_UpdateWritesExtractorToAccountLayer(t *testing.T) {
|
||||
// t176(翻轉自 t126③):**雲端下發的 LLM 設定一律被忽略**。
|
||||
// leo 08-03:「地端要用什麼模型就在 daemon 上輸入 API Key 設置,而不是雲端設置後控制地端」。
|
||||
// 這條是回歸守衛——雲端 extractor_config 是全租戶共用一把 KV,任一處設了 claude
|
||||
// 會讓所有沒裝 Claude Code 的機器萃取全滅(awindhon 08-03 實證:零張卡)。
|
||||
func TestAddOrUpdateAccount_IgnoresRemoteLLMSettings(t *testing.T) {
|
||||
cfg := &directConfig{
|
||||
Extractor: "claude", // 機器層
|
||||
Extractor: "gemma", // 機器層=使用者自己在「AI 設定…」填的
|
||||
GeminiAPIKey: "my-own-key", //
|
||||
Accounts: []accountCfg{{
|
||||
CypherURL: "https://inst.workers.dev",
|
||||
Namespace: "ns1",
|
||||
CypherURL: "https://inst.workers.dev",
|
||||
Namespace: "ns1",
|
||||
Extractor: "gemma",
|
||||
GeminiAPIKey: "my-own-key",
|
||||
}},
|
||||
}
|
||||
// 雲端試圖下發 claude + 別把金鑰——全部都不該生效
|
||||
resp := &daemonConfigResp{Success: true}
|
||||
resp.Config.CypherURL = "https://inst.workers.dev"
|
||||
resp.Config.Namespace = "ns1"
|
||||
resp.Config.Extractor = "gemma"
|
||||
resp.Config.GeminiAPIKey = "per-account-key"
|
||||
resp.Config.Extractor = "claude"
|
||||
resp.Config.GeminiAPIKey = "cloud-pushed-key"
|
||||
|
||||
isNew := addOrUpdateAccount(cfg, resp)
|
||||
if isNew {
|
||||
if isNew := addOrUpdateAccount(cfg, resp); isNew {
|
||||
t.Error("同 host 應為更新(回 false)")
|
||||
}
|
||||
if cfg.Accounts[0].Extractor != "gemma" {
|
||||
t.Errorf("帳號層 Extractor 應更新,got %q", cfg.Accounts[0].Extractor)
|
||||
t.Errorf("雲端下發的 extractor 不該覆蓋本地設定,got %q", cfg.Accounts[0].Extractor)
|
||||
}
|
||||
if cfg.Accounts[0].GeminiAPIKey != "per-account-key" {
|
||||
t.Errorf("帳號層 GeminiAPIKey 應更新,got %q", cfg.Accounts[0].GeminiAPIKey)
|
||||
if cfg.Accounts[0].GeminiAPIKey != "my-own-key" {
|
||||
t.Errorf("雲端下發的金鑰不該覆蓋本地金鑰,got %q", cfg.Accounts[0].GeminiAPIKey)
|
||||
}
|
||||
// 機器層不變
|
||||
if cfg.Extractor != "claude" {
|
||||
t.Errorf("機器層 Extractor 不應被覆蓋,got %q", cfg.Extractor)
|
||||
if cfg.Extractor != "gemma" || cfg.GeminiAPIKey != "my-own-key" {
|
||||
t.Errorf("機器層不該被雲端動到,got extractor=%q key=%q", cfg.Extractor, cfg.GeminiAPIKey)
|
||||
}
|
||||
// 連線欄位仍要更新(這條路徑本來的職責)
|
||||
if cfg.Accounts[0].Namespace != "ns1" {
|
||||
t.Errorf("連線欄位仍應更新,got namespace=%q", cfg.Accounts[0].Namespace)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user