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:
@@ -432,15 +432,14 @@ func RunDirectOnce(cfg *DirectConfig, dryRun bool) ([]DirectResult, int, *Trigge
|
||||
// t92-②:預檢 extractor(機器層級),有 fallback 路徑時就地更新 cfg.ClaudeBin。
|
||||
extractorOK := true
|
||||
extractorError := ""
|
||||
// t176(leo 08-03 拍板):**地端先只支援 Gemini**(「地端先限制 Gemini API Key 配合客戶要求」)。
|
||||
// 殘留的 extractor:"claude"(雲端舊版下發、或舊 config 殘留)一律當 gemma 處理。
|
||||
// 這不是「自動偵測有無 claude」(那是 leo 07-27 已否決的 B 案,見 daemon-beta/tasks.md:641),
|
||||
// 而是「claude 路整條先不支援」——之後要裝回來,把這段拿掉即可。
|
||||
if cfg.Extractor == "claude" {
|
||||
resolved, ferr := FindClaudeBin(cfg.ClaudeBin)
|
||||
if ferr != nil {
|
||||
extractorOK = false
|
||||
extractorError = "找不到 Claude 指令——請確認 Claude Code 已安裝,或改用 Gemma 萃取路"
|
||||
} else if resolved != cfg.ClaudeBin {
|
||||
cfg.ClaudeBin = resolved // in-memory 回寫;runDirect 偵到變化才存磁碟
|
||||
}
|
||||
} else if cfg.Extractor == "gemma" {
|
||||
cfg.Extractor = "gemma"
|
||||
}
|
||||
if cfg.Extractor == "gemma" {
|
||||
if strings.TrimSpace(cfg.GeminiAPIKey) == "" {
|
||||
extractorOK = false
|
||||
extractorError = "金鑰是空的——請在設定裡輸入 Gemini API Key"
|
||||
@@ -561,10 +560,8 @@ func shortError(msg string) string {
|
||||
func CheckExtractor(cfg *DirectConfig) (ok bool, errMsg string) {
|
||||
switch cfg.Extractor {
|
||||
case "claude":
|
||||
if _, err := FindClaudeBin(cfg.ClaudeBin); err != nil {
|
||||
return false, "找不到 Claude 指令——請確認 Claude Code 已安裝,或改用 Gemma 萃取路"
|
||||
}
|
||||
return true, ""
|
||||
// t176:claude 先不支援,等同 gemma(與 RunDirectOnce 的正規化保持一致,避免兩處漂移)。
|
||||
fallthrough
|
||||
case "gemma":
|
||||
if strings.TrimSpace(cfg.GeminiAPIKey) == "" {
|
||||
return false, "金鑰是空的——請在設定裡輸入 Gemini API Key"
|
||||
@@ -642,10 +639,12 @@ func runDirectOnceRoot(cfg *DirectConfig, root string, dryRun bool) ([]DirectRes
|
||||
var cards []string
|
||||
var xerr error
|
||||
switch cfg.Extractor {
|
||||
case "claude":
|
||||
cards, xerr = ExtractWithClaude(cfg.ClaudeBin, absRoot, ev.Path)
|
||||
case "gemma":
|
||||
cards, xerr = ExtractWithGemma(cfg.GeminiAPIKey, cfg.LLMModel, absRoot, ev.Path)
|
||||
default:
|
||||
// t176:claude 路先不支援(RunDirectOnce 開頭已把 "claude" 正規化為 "gemma")。
|
||||
// 走到這裡代表 config 有沒見過的值——誠實報錯,不要靜默跳過(禁假綠)。
|
||||
xerr = fmt.Errorf("不支援的萃取方式 %q(目前只支援 Gemini)", cfg.Extractor)
|
||||
}
|
||||
if xerr != nil {
|
||||
res.Status, res.Error = "failed", "本地萃取失敗:"+xerr.Error()
|
||||
@@ -784,8 +783,9 @@ func runDirect(args []string) int {
|
||||
return 2
|
||||
}
|
||||
|
||||
// t92:第一輪若 claude 找到了 fallback 路徑,把更新後的 claude_bin 存回 config 檔(下次直達)。
|
||||
origClaudeBin := cfg.ClaudeBin
|
||||
// t176:t92 的 claude_bin 回寫已隨 claude 萃取路一併退役——RunDirectOnce 不再解析
|
||||
// claude 執行檔,cfg.ClaudeBin 不會被改寫,故沒有東西需要回寫。
|
||||
// (留著空轉的回寫邏輯會讓未來的人以為 claude 路還活著=錯誤的環境信號。)
|
||||
|
||||
runOne := func() int {
|
||||
results, exit, _ := RunDirectOnce(cfg, *dryRun)
|
||||
@@ -798,16 +798,6 @@ func runDirect(args []string) int {
|
||||
return exit
|
||||
}
|
||||
|
||||
// claude_bin 回寫:只在找到 fallback 路徑時才存(避免頻繁寫磁碟)
|
||||
persistClaudeBinIfChanged := func() {
|
||||
if cfg.ClaudeBin != origClaudeBin && cfg.ClaudeBin != "" && *configPath != "" {
|
||||
if serr := saveDirectConfig(*configPath, cfg); serr != nil {
|
||||
fmt.Fprintf(os.Stderr, "claude_bin 回寫 config 失敗(不擋看守):%v\n", serr)
|
||||
} else {
|
||||
origClaudeBin = cfg.ClaudeBin // 只存一次
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 四步定稿第 1 步:daemon 代裝 template——常駐看守前確保每根都鋪好(冪等,不覆寫既有檔)。
|
||||
// dry-run/--once 測試情境不代裝(不留副作用),由 template-install 子命令顯式做。
|
||||
@@ -825,9 +815,7 @@ func runDirect(args []string) int {
|
||||
}
|
||||
|
||||
if *once {
|
||||
code := runOne()
|
||||
persistClaudeBinIfChanged()
|
||||
return code
|
||||
return runOne()
|
||||
}
|
||||
// 常駐輪詢:純 stdlib,跨平台。首輪立即跑。
|
||||
// t98:每 1 秒檢查一次訊號檔(SyncNowSignalPath),命中即立刻跑一輪並刪檔;
|
||||
@@ -836,7 +824,6 @@ func runDirect(args []string) int {
|
||||
fmt.Fprintf(os.Stderr, "collector direct daemon 啟動:監看 %s → %s(每 %ds 掃一輪)\n",
|
||||
strings.Join(cfg.Folders(), "、"), cfg.triggerURL(cfg.IngestWF), cfg.PollSec)
|
||||
runOne()
|
||||
persistClaudeBinIfChanged() // 第一輪後立即回寫(下次重啟直達)
|
||||
|
||||
signalPath := SyncNowSignalPath(cfg.Manifest)
|
||||
pollInterval := time.Duration(cfg.PollSec) * time.Second
|
||||
|
||||
Reference in New Issue
Block a user