fix(t126): 萃取引擎/金鑰改每帳號一份——多帳號不再互相覆蓋

leo:「2 個 CF 帳號一個填 gemma4 一個填 Mistral,daemon 會用誰的?別人也加就會有一樣的疑問」
實證:金鑰只有機器層一份 ⇒ 後連的覆蓋前一個。
修:AccountConfig 加 Extractor/GeminiAPIKey/LLMModel;帳號層優先、空值繼承機器層;
既有 config 遷移(頂層金鑰複製到各帳號,冪等);連線只寫該帳號不覆蓋機器層;
托盤帳號標題顯示「· Gemini」/「· Claude」(一眼看出誰用誰)。
三模組 go test 全綠(總管親跑)。(實作=子 CC;驗證+commit=總管)
This commit is contained in:
2026-07-29 14:36:44 +08:00
parent 4b60190bad
commit be5d6e3237
5 changed files with 343 additions and 33 deletions
+136
View File
@@ -622,6 +622,142 @@ func TestRunDirectOnceAccountResultsTagged(t *testing.T) {
}
}
// ── t126:每帳號獨立萃取設定 ──────────────────────────────────────────────────
// t126①:帳號層 Extractor/GeminiAPIKey/LLMModel 有值時優先覆蓋機器層。
func TestMakeAccountSubConfigExtractorOverride(t *testing.T) {
parent := &DirectConfig{
Manifest: "/tmp/m.json",
Extractor: "claude",
GeminiAPIKey: "machine-key",
LLMModel: "model-machine",
PollSec: 5,
MaxRemoved: DefaultMaxRemovedRatio,
}
acc := AccountConfig{
CypherURL: "https://a.example",
Namespace: "ns",
WatchFolders: []string{"/tmp"},
Extractor: "gemma", // 帳號層覆蓋
GeminiAPIKey: "account-key", // 帳號層覆蓋
LLMModel: "model-acc", // 帳號層覆蓋
}
sub := parent.makeAccountSubConfig(acc)
if sub.Extractor != "gemma" {
t.Errorf("① 帳號層 Extractor 應優先,got %q", sub.Extractor)
}
if sub.GeminiAPIKey != "account-key" {
t.Errorf("① 帳號層 GeminiAPIKey 應優先,got %q", sub.GeminiAPIKey)
}
if sub.LLMModel != "model-acc" {
t.Errorf("① 帳號層 LLMModel 應優先,got %q", sub.LLMModel)
}
}
// t126②:帳號層空字串不算「有值」,應繼承機器層。
func TestMakeAccountSubConfigExtractorFallback(t *testing.T) {
parent := &DirectConfig{
Manifest: "/tmp/m.json",
Extractor: "gemma",
GeminiAPIKey: "machine-key",
LLMModel: "model-machine",
PollSec: 5,
MaxRemoved: DefaultMaxRemovedRatio,
}
acc := AccountConfig{
CypherURL: "https://a.example",
Namespace: "ns",
WatchFolders: []string{"/tmp"},
// Extractor/GeminiAPIKey/LLMModel 全空 → 繼承機器層
}
sub := parent.makeAccountSubConfig(acc)
if sub.Extractor != "gemma" {
t.Errorf("② 帳號層空時應繼承機器層 Extractorgot %q", sub.Extractor)
}
if sub.GeminiAPIKey != "machine-key" {
t.Errorf("② 帳號層空時應繼承機器層 GeminiAPIKeygot %q", sub.GeminiAPIKey)
}
if sub.LLMModel != "model-machine" {
t.Errorf("② 帳號層空時應繼承機器層 LLMModelgot %q", sub.LLMModel)
}
}
// t126③ 遷移:機器層金鑰複製到沒有金鑰的帳號;已有金鑰的帳號不覆蓋。
func TestLoadDirectConfigMigratesCopyKeyToAccounts(t *testing.T) {
dir := t.TempDir()
p := writeDirectConfig(t, dir, map[string]any{
"manifest": filepath.Join(dir, "m.json"),
"extractor": "gemma",
"gemini_api_key": "top-level-key",
"llm_model": "model-x",
"accounts": []map[string]any{
{
"cypher_url": "https://a.example",
"namespace": "nsA",
"watch_folders": []string{"/tmp/a"},
// 沒有 gemini_api_key → 應複製頂層
},
{
"cypher_url": "https://b.example",
"namespace": "nsB",
"watch_folders": []string{"/tmp/b"},
"gemini_api_key": "b-own-key", // 已有自己的 key → 不覆蓋
},
},
})
cfg, err := LoadDirectConfig(p)
if err != nil {
t.Fatalf("LoadDirectConfig: %v", err)
}
// account A:無 key → 應複製頂層
if cfg.Accounts[0].GeminiAPIKey != "top-level-key" {
t.Errorf("③ account A 應複製頂層 keygot %q", cfg.Accounts[0].GeminiAPIKey)
}
if cfg.Accounts[0].Extractor != "gemma" {
t.Errorf("③ account A 應複製頂層 extractorgot %q", cfg.Accounts[0].Extractor)
}
if cfg.Accounts[0].LLMModel != "model-x" {
t.Errorf("③ account A 應複製頂層 llm_modelgot %q", cfg.Accounts[0].LLMModel)
}
// account B:已有自己的 key → 不覆蓋
if cfg.Accounts[1].GeminiAPIKey != "b-own-key" {
t.Errorf("③ account B 已有 key 不應被頂層覆蓋,got %q", cfg.Accounts[1].GeminiAPIKey)
}
// 頂層保留(複製非搬移)
if cfg.GeminiAPIKey != "top-level-key" {
t.Errorf("③ 頂層 key 應保留(複製非搬移),got %q", cfg.GeminiAPIKey)
}
}
// t126④ 冪等:帳號已有 extractor/key 時再跑遷移不覆蓋。
func TestLoadDirectConfigMigrationIdempotent(t *testing.T) {
dir := t.TempDir()
p := writeDirectConfig(t, dir, map[string]any{
"manifest": filepath.Join(dir, "m.json"),
"extractor": "gemma",
"gemini_api_key": "top-key",
"accounts": []map[string]any{
{
"cypher_url": "https://a.example",
"namespace": "nsA",
"watch_folders": []string{"/tmp/a"},
"extractor": "claude", // 帳號已有 → 不應被頂層 "gemma" 覆蓋
"gemini_api_key": "own-key", // 帳號已有 → 不應被頂層 key 覆蓋
},
},
})
cfg, err := LoadDirectConfig(p)
if err != nil {
t.Fatalf("LoadDirectConfig: %v", err)
}
if cfg.Accounts[0].Extractor != "claude" {
t.Errorf("④ 帳號已有 extractor 不應被頂層覆蓋,got %q", cfg.Accounts[0].Extractor)
}
if cfg.Accounts[0].GeminiAPIKey != "own-key" {
t.Errorf("④ 帳號已有 key 不應被頂層覆蓋,got %q", cfg.Accounts[0].GeminiAPIKey)
}
}
func TestExpandHomeEdgeCases(t *testing.T) {
home, err := os.UserHomeDir()
if err != nil || home == "" {