fix(t126): 萃取引擎/金鑰改每帳號一份——多帳號不再互相覆蓋
leo:「2 個 CF 帳號一個填 gemma4 一個填 Mistral,daemon 會用誰的?別人也加就會有一樣的疑問」 實證:金鑰只有機器層一份 ⇒ 後連的覆蓋前一個。 修:AccountConfig 加 Extractor/GeminiAPIKey/LLMModel;帳號層優先、空值繼承機器層; 既有 config 遷移(頂層金鑰複製到各帳號,冪等);連線只寫該帳號不覆蓋機器層; 托盤帳號標題顯示「· Gemini」/「· Claude」(一眼看出誰用誰)。 三模組 go test 全綠(總管親跑)。(實作=子 CC;驗證+commit=總管)
This commit is contained in:
Binary file not shown.
+74
-17
@@ -60,6 +60,7 @@ func versionLabel() string {
|
||||
}
|
||||
|
||||
// accountCfg 是單一帳號的連線設定(t104 多帳號同時看守)。
|
||||
// t126:Extractor/GeminiAPIKey/LLMModel 支援帳號層覆蓋——帳號有值時優先,空值繼承機器層。
|
||||
type accountCfg struct {
|
||||
InstanceName string `json:"instance_name,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
@@ -68,6 +69,10 @@ type accountCfg struct {
|
||||
APIKey string `json:"api_key,omitempty"`
|
||||
WatchFolders []string `json:"watch_folders,omitempty"`
|
||||
Libraries map[string]string `json:"libraries,omitempty"` // t52:資料夾→庫對映
|
||||
// t126:每帳號獨立的萃取設定(空值繼承機器層)
|
||||
Extractor string `json:"extractor,omitempty"`
|
||||
GeminiAPIKey string `json:"gemini_api_key,omitempty"`
|
||||
LLMModel string `json:"llm_model,omitempty"`
|
||||
}
|
||||
|
||||
// directConfig 是 collector direct 的設定(與 collector/direct.go 的 DirectConfig 同結構)。
|
||||
@@ -210,6 +215,28 @@ func loadConfig() *directConfig {
|
||||
// 寫回磁碟(冪等:下次讀 Accounts 已存在就不再遷移)
|
||||
_ = saveConfig(c)
|
||||
}
|
||||
|
||||
// t126 遷移:把頂層金鑰複製到每個沒有金鑰的帳號(複製非搬移,頂層保留當預設;冪等)。
|
||||
// 帳號已有自己的值(非空)→ 不覆蓋,讓帳號層設定永遠優先。
|
||||
t126Migrated := false
|
||||
for i := range c.Accounts {
|
||||
if strings.TrimSpace(c.Accounts[i].Extractor) == "" && strings.TrimSpace(c.Extractor) != "" {
|
||||
c.Accounts[i].Extractor = c.Extractor
|
||||
t126Migrated = true
|
||||
}
|
||||
if strings.TrimSpace(c.Accounts[i].GeminiAPIKey) == "" && strings.TrimSpace(c.GeminiAPIKey) != "" {
|
||||
c.Accounts[i].GeminiAPIKey = c.GeminiAPIKey
|
||||
t126Migrated = true
|
||||
}
|
||||
if strings.TrimSpace(c.Accounts[i].LLMModel) == "" && strings.TrimSpace(c.LLMModel) != "" {
|
||||
c.Accounts[i].LLMModel = c.LLMModel
|
||||
t126Migrated = true
|
||||
}
|
||||
}
|
||||
if t126Migrated {
|
||||
_ = saveConfig(c)
|
||||
}
|
||||
|
||||
if len(c.Accounts) == 0 && c.WatchFolder == "" && len(c.WatchFolders) == 0 {
|
||||
c.WatchFolder = defaultWatchFolder() // 尚未連線的預設值(不觸發遷移)
|
||||
}
|
||||
@@ -349,18 +376,24 @@ func shortCypherHost(cypherURL string) string {
|
||||
// 輸入「知識庫網址 + 帳號密碼」→ 打 /portal/daemon/config → 設定自動寫好。
|
||||
// 用戶只需要記得他剛在網站設的那組帳密(他本來就記得)。
|
||||
|
||||
// daemonConfig 是 /portal/daemon/config 回應的設定欄位。
|
||||
// t126:加入 GeminiAPIKey / LLMModel——server 若下發金鑰,直接寫進帳號層。
|
||||
type daemonConfig struct {
|
||||
CypherURL string `json:"cypher_url"`
|
||||
Namespace string `json:"namespace"`
|
||||
Library string `json:"library"`
|
||||
Extractor string `json:"extractor"`
|
||||
GeminiAPIKey string `json:"gemini_api_key,omitempty"` // t126
|
||||
LLMModel string `json:"llm_model,omitempty"` // t126
|
||||
Email string `json:"email"`
|
||||
InstanceName string `json:"instance_name"`
|
||||
}
|
||||
|
||||
// daemonConfigResp 是 /portal/daemon/config 的回應(只含連線設定,不含知識內容)。
|
||||
type daemonConfigResp struct {
|
||||
Success bool `json:"success"`
|
||||
Config struct {
|
||||
CypherURL string `json:"cypher_url"`
|
||||
Namespace string `json:"namespace"`
|
||||
Library string `json:"library"`
|
||||
Extractor string `json:"extractor"`
|
||||
Email string `json:"email"`
|
||||
InstanceName string `json:"instance_name"`
|
||||
} `json:"config"`
|
||||
Error string `json:"error"`
|
||||
Success bool `json:"success"`
|
||||
Config daemonConfig `json:"config"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// normalizePortalURL 把用戶貼的東西變成可打的 origin:
|
||||
@@ -558,6 +591,7 @@ func isConnected(cfg *directConfig) bool {
|
||||
// addOrUpdateAccount 把連線精靈取回的設定加入(或更新)accounts 清單(t104)。
|
||||
// 同 host 已存在 → 更新連線欄位,WatchFolders 保留不動;不同 host → append 新帳號。
|
||||
// 回傳 true 代表是全新帳號(首次加入)。
|
||||
// t126:Extractor/GeminiAPIKey/LLMModel 寫進帳號層,不覆蓋機器層(機器層只留使用者手動設的預設)。
|
||||
func addOrUpdateAccount(cfg *directConfig, r *daemonConfigResp) bool {
|
||||
newHost := shortCypherHost(r.Config.CypherURL)
|
||||
for i := range cfg.Accounts {
|
||||
@@ -569,26 +603,33 @@ func addOrUpdateAccount(cfg *directConfig, r *daemonConfigResp) bool {
|
||||
if r.Config.InstanceName != "" {
|
||||
cfg.Accounts[i].InstanceName = r.Config.InstanceName
|
||||
}
|
||||
// t126:萃取設定寫帳號層,不覆蓋機器層
|
||||
if r.Config.Extractor != "" {
|
||||
cfg.Extractor = r.Config.Extractor // 機器層級
|
||||
cfg.Accounts[i].Extractor = r.Config.Extractor
|
||||
}
|
||||
if r.Config.GeminiAPIKey != "" {
|
||||
cfg.Accounts[i].GeminiAPIKey = r.Config.GeminiAPIKey
|
||||
}
|
||||
if r.Config.LLMModel != "" {
|
||||
cfg.Accounts[i].LLMModel = r.Config.LLMModel
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
// 新帳號
|
||||
// 新帳號:t126 萃取設定直接寫帳號層
|
||||
acc := accountCfg{
|
||||
InstanceName: r.Config.InstanceName,
|
||||
Email: r.Config.Email,
|
||||
CypherURL: strings.TrimSuffix(strings.TrimSpace(r.Config.CypherURL), "/"),
|
||||
Namespace: r.Config.Namespace,
|
||||
Extractor: r.Config.Extractor, // t126
|
||||
GeminiAPIKey: r.Config.GeminiAPIKey, // t126
|
||||
LLMModel: r.Config.LLMModel, // t126
|
||||
}
|
||||
if acc.APIKey == "" {
|
||||
acc.APIKey = acc.Namespace
|
||||
}
|
||||
cfg.Accounts = append(cfg.Accounts, acc)
|
||||
if r.Config.Extractor != "" {
|
||||
cfg.Extractor = r.Config.Extractor
|
||||
}
|
||||
if cfg.Manifest == "" {
|
||||
cfg.Manifest = filepath.Join(appDir(), "manifest.json")
|
||||
}
|
||||
@@ -624,6 +665,22 @@ func removeAccountWatchFolder(cfg *directConfig, accIdx int, p string) {
|
||||
acc.WatchFolders = out
|
||||
}
|
||||
|
||||
// accountEngineLabel 依帳號或全局預設 extractor 產生「· 引擎名」後綴(t126 托盤可見性)。
|
||||
// 帳號層有值時優先;兩者皆空回空字串。
|
||||
func accountEngineLabel(acc accountCfg, defaultExtractor string) string {
|
||||
extractor := strings.TrimSpace(acc.Extractor)
|
||||
if extractor == "" {
|
||||
extractor = strings.TrimSpace(defaultExtractor)
|
||||
}
|
||||
switch extractor {
|
||||
case "gemma":
|
||||
return " · Gemini"
|
||||
case "claude":
|
||||
return " · Claude"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// accountDisplayName 回傳帳號顯示名稱(暱稱 > email > host)。
|
||||
func accountDisplayName(acc accountCfg) string {
|
||||
if n := strings.TrimSpace(acc.InstanceName); n != "" {
|
||||
@@ -807,8 +864,8 @@ func main() {
|
||||
accIdx := ai // capture by value,供 closure 正確引用
|
||||
accHost := shortCypherHost(acc.CypherURL)
|
||||
|
||||
// 帳號標題(不可點)
|
||||
headerItem := fyne.NewMenuItem("◉ "+accountDisplayName(acc), nil)
|
||||
// 帳號標題(不可點);t126 附引擎名(如「geek6688 · Gemini」)
|
||||
headerItem := fyne.NewMenuItem("◉ "+accountDisplayName(acc)+accountEngineLabel(acc, cfg.Extractor), nil)
|
||||
headerItem.Disabled = true
|
||||
items = append(items, headerItem)
|
||||
|
||||
|
||||
+101
-14
@@ -468,20 +468,10 @@ func TestAddOrUpdateAccountPreservesMachineLayer(t *testing.T) {
|
||||
LLMModel: "gemma-4-31b-it",
|
||||
CardIngestWF: "rag_ingest_card",
|
||||
}
|
||||
resp := &daemonConfigResp{
|
||||
Success: true,
|
||||
Config: struct {
|
||||
CypherURL string `json:"cypher_url"`
|
||||
Namespace string `json:"namespace"`
|
||||
Library string `json:"library"`
|
||||
Extractor string `json:"extractor"`
|
||||
Email string `json:"email"`
|
||||
InstanceName string `json:"instance_name"`
|
||||
}{
|
||||
CypherURL: "https://new.workers.dev",
|
||||
Namespace: "newns",
|
||||
},
|
||||
}
|
||||
resp := &daemonConfigResp{Success: true}
|
||||
resp.Config.CypherURL = "https://new.workers.dev"
|
||||
resp.Config.Namespace = "newns"
|
||||
|
||||
addOrUpdateAccount(cfg, resp)
|
||||
if cfg.GeminiAPIKey != "AIzaSy-existing-key" {
|
||||
t.Errorf("GeminiAPIKey 被清空:got %q", cfg.GeminiAPIKey)
|
||||
@@ -493,3 +483,100 @@ func TestAddOrUpdateAccountPreservesMachineLayer(t *testing.T) {
|
||||
t.Errorf("CardIngestWF 被清空:got %q", cfg.CardIngestWF)
|
||||
}
|
||||
}
|
||||
|
||||
// ── t126:每帳號獨立引擎設定 ──────────────────────────────────────────────────
|
||||
|
||||
// t126①:accountEngineLabel 依帳號或 defaultExtractor 顯示引擎名(帳號層優先)。
|
||||
func TestAccountEngineLabel(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
acc accountCfg
|
||||
defaultEx string
|
||||
want string
|
||||
}{
|
||||
{"帳號有gemma", accountCfg{Extractor: "gemma"}, "", " · Gemini"},
|
||||
{"帳號有claude", accountCfg{Extractor: "claude"}, "", " · Claude"},
|
||||
{"帳號空退回default-gemma", accountCfg{}, "gemma", " · Gemini"},
|
||||
{"帳號空退回default-claude", accountCfg{}, "claude", " · Claude"},
|
||||
{"兩者皆空無標籤", accountCfg{}, "", ""},
|
||||
{"帳號層優先於default", accountCfg{Extractor: "gemma"}, "claude", " · Gemini"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got := accountEngineLabel(c.acc, c.defaultEx)
|
||||
if got != c.want {
|
||||
t.Errorf("accountEngineLabel(%+v, %q) = %q, want %q", c.acc, c.defaultEx, got, c.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// t126②:addOrUpdateAccount 把 extractor/key 寫進帳號層,機器層不被覆蓋。
|
||||
func TestAddOrUpdateAccount_WritesExtractorToAccountLayer(t *testing.T) {
|
||||
cfg := &directConfig{
|
||||
Extractor: "claude", // 機器層原有值
|
||||
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"
|
||||
|
||||
isNew := addOrUpdateAccount(cfg, resp)
|
||||
if !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].GeminiAPIKey != "account-key" {
|
||||
t.Errorf("帳號層 GeminiAPIKey 錯:got %q", cfg.Accounts[0].GeminiAPIKey)
|
||||
}
|
||||
if cfg.Accounts[0].LLMModel != "gemma-4-31b-it" {
|
||||
t.Errorf("帳號層 LLMModel 錯:got %q", cfg.Accounts[0].LLMModel)
|
||||
}
|
||||
// 機器層不應被覆蓋
|
||||
if cfg.Extractor != "claude" {
|
||||
t.Errorf("機器層 Extractor 不應被覆蓋,got %q", cfg.Extractor)
|
||||
}
|
||||
if cfg.GeminiAPIKey != "machine-key" {
|
||||
t.Errorf("機器層 GeminiAPIKey 不應被覆蓋,got %q", cfg.GeminiAPIKey)
|
||||
}
|
||||
}
|
||||
|
||||
// t126③:同 host 更新帳號時 extractor 也寫帳號層。
|
||||
func TestAddOrUpdateAccount_UpdateWritesExtractorToAccountLayer(t *testing.T) {
|
||||
cfg := &directConfig{
|
||||
Extractor: "claude", // 機器層
|
||||
Accounts: []accountCfg{{
|
||||
CypherURL: "https://inst.workers.dev",
|
||||
Namespace: "ns1",
|
||||
}},
|
||||
}
|
||||
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"
|
||||
|
||||
isNew := addOrUpdateAccount(cfg, resp)
|
||||
if isNew {
|
||||
t.Error("同 host 應為更新(回 false)")
|
||||
}
|
||||
if cfg.Accounts[0].Extractor != "gemma" {
|
||||
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.Extractor != "claude" {
|
||||
t.Errorf("機器層 Extractor 不應被覆蓋,got %q", cfg.Extractor)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import (
|
||||
|
||||
// AccountConfig 單一帳號的連線設定(t104 多帳號同時看守)。
|
||||
// 每個帳號代表一個 Arcrun 知識庫實例;Extractor/Manifest 等機器層級設定住在 DirectConfig 頂層。
|
||||
// t126:Extractor/GeminiAPIKey/LLMModel 支援帳號層覆蓋——帳號有值時優先,空值繼承機器層。
|
||||
type AccountConfig struct {
|
||||
InstanceName string `json:"instance_name,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
@@ -44,6 +45,10 @@ type AccountConfig struct {
|
||||
APIKey string `json:"api_key,omitempty"`
|
||||
WatchFolders []string `json:"watch_folders,omitempty"` // 此帳號看守的資料夾(多根)
|
||||
Libraries map[string]string `json:"libraries,omitempty"` // 資料夾→庫對映(t52)
|
||||
// t126:每帳號獨立的萃取設定(空值繼承 DirectConfig 頂層)
|
||||
Extractor string `json:"extractor,omitempty"`
|
||||
GeminiAPIKey string `json:"gemini_api_key,omitempty"`
|
||||
LLMModel string `json:"llm_model,omitempty"`
|
||||
}
|
||||
|
||||
// DirectConfig 是 direct 模式的設定檔(JSON)。設定只走檔案/環境,不落 code。
|
||||
@@ -175,6 +180,20 @@ func LoadDirectConfig(path string) (*DirectConfig, error) {
|
||||
}}
|
||||
}
|
||||
|
||||
// t126 遷移:把頂層金鑰複製到每個沒有金鑰的帳號(複製非搬移,頂層保留當預設;冪等)。
|
||||
// 帳號已有自己的值(非空)→ 不覆蓋,讓帳號層設定永遠優先。
|
||||
for i := range c.Accounts {
|
||||
if strings.TrimSpace(c.Accounts[i].Extractor) == "" && strings.TrimSpace(c.Extractor) != "" {
|
||||
c.Accounts[i].Extractor = c.Extractor
|
||||
}
|
||||
if strings.TrimSpace(c.Accounts[i].GeminiAPIKey) == "" && strings.TrimSpace(c.GeminiAPIKey) != "" {
|
||||
c.Accounts[i].GeminiAPIKey = c.GeminiAPIKey
|
||||
}
|
||||
if strings.TrimSpace(c.Accounts[i].LLMModel) == "" && strings.TrimSpace(c.LLMModel) != "" {
|
||||
c.Accounts[i].LLMModel = c.LLMModel
|
||||
}
|
||||
}
|
||||
|
||||
// 驗證
|
||||
var missing []string
|
||||
if c.Manifest == "" {
|
||||
@@ -359,6 +378,7 @@ type DirectResult struct {
|
||||
|
||||
// makeAccountSubConfig 從帳號設定建出單帳號用的 DirectConfig,繼承機器層級欄位(t104)。
|
||||
// 用於 RunDirectOnce 逐帳號掃描,每帳號得到獨立的 CypherURL/Namespace/WatchFolders 等。
|
||||
// t126:帳號層 Extractor/GeminiAPIKey/LLMModel 有值時優先覆蓋機器層(空字串不算「有值」)。
|
||||
func (c *DirectConfig) makeAccountSubConfig(acc AccountConfig) *DirectConfig {
|
||||
sub := *c // 複製機器層級欄位
|
||||
sub.Accounts = nil
|
||||
@@ -376,6 +396,16 @@ func (c *DirectConfig) makeAccountSubConfig(acc AccountConfig) *DirectConfig {
|
||||
if sub.Library == "" {
|
||||
sub.Library = "kb"
|
||||
}
|
||||
// t126:帳號層有值時優先(空字串繼承機器層,已由 sub := *c 複製)
|
||||
if strings.TrimSpace(acc.Extractor) != "" {
|
||||
sub.Extractor = acc.Extractor
|
||||
}
|
||||
if strings.TrimSpace(acc.GeminiAPIKey) != "" {
|
||||
sub.GeminiAPIKey = acc.GeminiAPIKey
|
||||
}
|
||||
if strings.TrimSpace(acc.LLMModel) != "" {
|
||||
sub.LLMModel = acc.LLMModel
|
||||
}
|
||||
return &sub
|
||||
}
|
||||
|
||||
|
||||
@@ -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("② 帳號層空時應繼承機器層 Extractor,got %q", sub.Extractor)
|
||||
}
|
||||
if sub.GeminiAPIKey != "machine-key" {
|
||||
t.Errorf("② 帳號層空時應繼承機器層 GeminiAPIKey,got %q", sub.GeminiAPIKey)
|
||||
}
|
||||
if sub.LLMModel != "model-machine" {
|
||||
t.Errorf("② 帳號層空時應繼承機器層 LLMModel,got %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 應複製頂層 key,got %q", cfg.Accounts[0].GeminiAPIKey)
|
||||
}
|
||||
if cfg.Accounts[0].Extractor != "gemma" {
|
||||
t.Errorf("③ account A 應複製頂層 extractor,got %q", cfg.Accounts[0].Extractor)
|
||||
}
|
||||
if cfg.Accounts[0].LLMModel != "model-x" {
|
||||
t.Errorf("③ account A 應複製頂層 llm_model,got %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 == "" {
|
||||
|
||||
Reference in New Issue
Block a user