Files
arcrun-collector/cmd/arcrun-tray/singleinstance.go
T
Leo f903a3f53f 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>
2026-08-03 17:48:10 +08:00

48 lines
2.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// singleinstance.go — 一台機器只跑一個托盤(t176,leo 08-03 回報「點選多次後托盤產生多個 icon」)。
//
// 為什麼要自己做:托盤程式**從來沒有任何** single-instance 機制(無 mutexlockfilepidfile)。
// mac 上看起來不會重複開,是**借了 macOS Launch Services 對同 bundle ID .app 的內建去重**
// (`open` 喚回既有行程),不是這支程式自己做的;Windows 的 .exe 沒有這層,
// 於是每雙擊一次就真的多一個行程、多一個托盤 icon。
//
// 作法:在 ~/.arcrun-rag/tray.lock 寫入自己的 pid,啟動時檢查該 pid 是否還活著。
// 選 pidfile 而非 OS 專屬鎖(Windows CreateMutexunix flock)的理由:
// 產物是 CGO_ENABLED=0 的純 stdlib 單一執行檔(見 build-*.sh),跨平台一份實作最不容易漂移。
package main
import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
)
// trayLockPath 回傳鎖檔路徑(與 config/manifest 同一個 app 目錄)。可注入(測試用)。
var trayLockPath = func() string { return filepath.Join(appDir(), "tray.lock") }
// acquireSingleInstance 嘗試取得「本機唯一托盤」的鎖。
// 回傳 false 代表已經有另一個托盤在跑(呼叫端應該直接退出,不要再建第二個托盤 icon)。
//
// 誠實限制:pid 在行程結束後可能被作業系統重用,極端情況下會誤判成「還在跑」。
// 這種情況下使用者刪掉 tray.lock 就能恢復,比「每點一次多一個 icon」好得多。
func acquireSingleInstance() (ok bool, release func()) {
path := trayLockPath()
if data, err := os.ReadFile(path); err == nil {
if pid, perr := strconv.Atoi(strings.TrimSpace(string(data))); perr == nil && pid > 0 && pid != os.Getpid() {
if processAlive(pid) {
return false, func() {}
}
// pid 已死=上次沒有正常結束(當機/強制關閉)留下的殘檔,直接接管。
}
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
// 建不了目錄就不擋啟動——寧可容忍重複開,也不要讓使用者完全打不開。
return true, func() {}
}
if err := os.WriteFile(path, []byte(fmt.Sprint(os.Getpid())), 0o644); err != nil {
return true, func() {}
}
return true, func() { _ = os.Remove(path) }
}