fix(t91+t92): 萃取狀態可見+Finder 啟動 PATH 修正——背景執行不可見的兩題同根一起解
leo 07-28 實測根因:Finder 起的 GUI app 只有最小 PATH(無 /opt/homebrew/bin) → claude 靜默找不到→整輪萃取失敗,托盤卻顯示「看守中」。leo:「我怎麼知道它有萃?」 - FindClaudeBin:LookPath 失敗後掃 4 個常見絕對路徑,找到回寫 config claude_bin - CheckExtractor 預檢+每輪寫 ~/.arcrun-rag/status.json(ok/fail 計數+失敗清單) - 托盤:引擎未就緒→「⚠ 萃取引擎未就緒:<白話原因>」;失敗>0→可點開明細;正常→「已萃 N 檔」 測試:collector+supervisor+tray 三模組 go test 全綠(總管親跑)。 (實作=子 CC;驗證+commit=總管)
This commit is contained in:
@@ -270,10 +270,32 @@ type DirectResult struct {
|
||||
|
||||
// RunDirectOnce 對每個監看根掃一輪並彙總結果(daemon-beta task 1 多資料夾)。
|
||||
// 單根行為與舊制完全相同(含 manifest 路徑)。回傳彙總結果與退出碼建議(任一根失敗=1)。
|
||||
// 額外:
|
||||
// - 預檢 extractor 可用性(t92-②),有 fallback 時更新 cfg.ClaudeBin(in-memory,呼叫端存檔)。
|
||||
// - 每輪結束寫 ~/.arcrun-rag/status.json(t91 狀態可見性)。
|
||||
func RunDirectOnce(cfg *DirectConfig, dryRun bool) ([]DirectResult, int, *TriggerPayload) {
|
||||
results := []DirectResult{}
|
||||
exit := 0
|
||||
var lastPayload *TriggerPayload
|
||||
|
||||
// t92-②:預檢 extractor,有 fallback 路徑時就地更新 cfg.ClaudeBin(供下游直接使用)。
|
||||
extractorOK := true
|
||||
extractorError := ""
|
||||
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" {
|
||||
if strings.TrimSpace(cfg.GeminiAPIKey) == "" {
|
||||
extractorOK = false
|
||||
extractorError = "金鑰是空的——請在設定裡輸入 Gemini API Key"
|
||||
}
|
||||
}
|
||||
|
||||
multi := len(cfg.Folders()) > 1
|
||||
for _, root := range cfg.Folders() {
|
||||
r, e, p := runDirectOnceRoot(cfg, root, dryRun)
|
||||
@@ -290,9 +312,75 @@ func RunDirectOnce(cfg *DirectConfig, dryRun bool) ([]DirectResult, int, *Trigge
|
||||
lastPayload = p
|
||||
}
|
||||
}
|
||||
|
||||
// t91:每輪寫狀態檔(只有 extractor 模式才有意義的計數;direct 雲端萃模式 extracted_ok=0)。
|
||||
if !dryRun && cfg.Manifest != "" {
|
||||
st := SyncStatus{
|
||||
LastSync: time.Now().Format(time.RFC3339),
|
||||
ExtractorOK: extractorOK,
|
||||
ExtractorError: extractorError,
|
||||
}
|
||||
if cfg.Extractor != "" {
|
||||
for _, r := range results {
|
||||
switch r.Status {
|
||||
case "ingested":
|
||||
st.ExtractedOK++
|
||||
case "failed":
|
||||
st.ExtractFailed++
|
||||
st.Failures = append(st.Failures, ExtractFail{
|
||||
Path: r.Path,
|
||||
Error: shortError(r.Error),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if serr := SaveSyncStatus(StatusFilePath(cfg.Manifest), st); serr != nil {
|
||||
fmt.Fprintf(os.Stderr, "status 寫入失敗(不擋看守):%v\n", serr)
|
||||
}
|
||||
}
|
||||
|
||||
return results, exit, lastPayload
|
||||
}
|
||||
|
||||
// shortError 把錯誤字串截為一句話(供 UI 顯示,不要超過 120 字)。
|
||||
func shortError(msg string) string {
|
||||
if len([]rune(msg)) <= 120 {
|
||||
return msg
|
||||
}
|
||||
runes := []rune(msg)
|
||||
return string(runes[:120]) + "…"
|
||||
}
|
||||
|
||||
// CheckExtractor 預檢萃取器是否可用(不執行萃取、不打 API)。
|
||||
// 只檢「可執行檔存在且可執行」或「金鑰非空」。
|
||||
// extractor 空(舊制直送)一律回 (true, "")。
|
||||
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, ""
|
||||
case "gemma":
|
||||
if strings.TrimSpace(cfg.GeminiAPIKey) == "" {
|
||||
return false, "金鑰是空的——請在設定裡輸入 Gemini API Key"
|
||||
}
|
||||
return true, ""
|
||||
default:
|
||||
return true, ""
|
||||
}
|
||||
}
|
||||
|
||||
// saveDirectConfig 把 DirectConfig 回寫到 configPath(t92:找到 claude fallback 路徑後持久化)。
|
||||
// 只寫 claude_bin 等萃取相關欄位不會影響用戶的其他設定(JSON 完整覆蓋整個 config)。
|
||||
func saveDirectConfig(configPath string, cfg *DirectConfig) error {
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(configPath, data, 0o600)
|
||||
}
|
||||
|
||||
// runDirectOnceRoot 對單一根掃一輪、直送 added/modified/renamed、下架 removed,2xx 後回寫該根 manifest。
|
||||
func runDirectOnceRoot(cfg *DirectConfig, root string, dryRun bool) ([]DirectResult, int, *TriggerPayload) {
|
||||
results := []DirectResult{}
|
||||
@@ -482,6 +570,9 @@ func runDirect(args []string) int {
|
||||
return 2
|
||||
}
|
||||
|
||||
// t92:第一輪若 claude 找到了 fallback 路徑,把更新後的 claude_bin 存回 config 檔(下次直達)。
|
||||
origClaudeBin := cfg.ClaudeBin
|
||||
|
||||
runOne := func() int {
|
||||
results, exit, _ := RunDirectOnce(cfg, *dryRun)
|
||||
out, _ := json.MarshalIndent(struct {
|
||||
@@ -493,6 +584,17 @@ 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 子命令顯式做。
|
||||
if !*once && !*dryRun {
|
||||
@@ -509,12 +611,15 @@ func runDirect(args []string) int {
|
||||
}
|
||||
|
||||
if *once {
|
||||
return runOne()
|
||||
code := runOne()
|
||||
persistClaudeBinIfChanged()
|
||||
return code
|
||||
}
|
||||
// 常駐輪詢:純 stdlib ticker,跨平台。首輪立即跑。
|
||||
fmt.Fprintf(os.Stderr, "collector direct daemon 啟動:監看 %s → %s(每 %ds 掃一輪)\n",
|
||||
strings.Join(cfg.Folders(), "、"), cfg.triggerURL(cfg.IngestWF), cfg.PollSec)
|
||||
runOne()
|
||||
persistClaudeBinIfChanged() // 第一輪後立即回寫(下次重啟直達)
|
||||
ticker := time.NewTicker(time.Duration(cfg.PollSec) * time.Second)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
|
||||
Reference in New Issue
Block a user