f903a3f53f
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>
48 lines
2.3 KiB
Go
48 lines
2.3 KiB
Go
// singleinstance.go — 一台機器只跑一個托盤(t176,leo 08-03 回報「點選多次後托盤產生多個 icon」)。
|
||
//
|
||
// 為什麼要自己做:托盤程式**從來沒有任何** single-instance 機制(無 mutex/lockfile/pidfile)。
|
||
// mac 上看起來不會重複開,是**借了 macOS Launch Services 對同 bundle ID .app 的內建去重**
|
||
// (`open` 喚回既有行程),不是這支程式自己做的;Windows 的 .exe 沒有這層,
|
||
// 於是每雙擊一次就真的多一個行程、多一個托盤 icon。
|
||
//
|
||
// 作法:在 ~/.arcrun-rag/tray.lock 寫入自己的 pid,啟動時檢查該 pid 是否還活著。
|
||
// 選 pidfile 而非 OS 專屬鎖(Windows CreateMutex/unix 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) }
|
||
}
|