Files
arcrun-collector/probe_workersai.go
T
Leo 4d3a6a09a6 v0.18.9:collector 併進同一支執行檔——磁碟上不再攤出第二支 exe
leo 08-06 裁決:「不要兩支,寫成一支檔案」。

## 為什麼
v0.18.7-8 的「單一 exe」其實是**一支包著另一支**:collector.exe 被 go:embed
進 Arcrun.exe,執行時攤到 ~/.arcrun-rag/bin/ 再跑。
那正是防毒軟體眼中的 dropper 特徵 —— 封測者實撞
`Trojan:Win32/Sabsik.FL.A!ml`,檔案當場被隔離、自動刪除。

⚠️ 誠實界定:`!ml` 結尾=**機器學習判定**,Sabsik 是最常見的通用誤判家族,
   主因是「未簽章+下載次數少」,**不是**特別指向 dropper 行為。
   所以本次改動**不保證**解除誤判——真正的解是上架 MS Store(微軟簽章)。
   但「執行時把第二支 PE 寫到磁碟再執行」本來就該拿掉,這是對的方向且順手變小。

## 怎麼做
- `collector/` 39 個檔 `package main` → `package collector`,`main()` → 匯出的 `Run(args) int`
- 新增 `collector/cmd/collector/`(薄殼 CLI,讓單獨跑 collector 這條路仍可用)
- App 直接 import 該套件;`main()` 第一件事就判 `--collector`,是的話走 `collector.Run` 不碰 GUI
- `supervisor` 加 `ArgPrefix`,App 把 `BinPath` 指向 `os.Executable()` 自己
- 刪掉 `bundled_collector_{windows,other}.go`(embed + 攤檔那套)
- 三支打包腳本不再編/複製第二支;版本注入同時打到兩個 package
- build-win.sh 的機械閘改成**直接問它**:`--collector --version` 回得出版本才放行
  (舊閘是比大小,只能證明「有 embed」,證明不了「分派是對的」)

## 驗(真機實跑)
· `.app/Contents/MacOS/` 只有 **一個** 執行檔(原本兩個)
· 跑起來兩個行程是**同一個 exe**:
    …/MacOS/arcrun-app
    …/MacOS/arcrun-app --collector direct --config …
· `~/.arcrun-rag/bin` **不存在**(沒有任何東西被攤出來)
· 端到端:丟檔進看守資料夾 → collector.log `"status":"ingested","http_status":200`
· `lsappinfo` 仍是 `type="UIElement"`、`Version="0.18.9"`
· collector 39 檔測試全過;app 測試過;go vet 全綠;mac + windows 交叉編譯皆過
· 單檔 26MB → 22MB(不再夾帶第二份完整程式)
2026-08-06 16:00:47 +08:00

88 lines
3.9 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.
// probe_workersai.go — 探測「這個帳號的雲端知識庫,Workers AI 萃取通了沒」(t182)。
//
// 🔴 為什麼要探測(leo 2026-08-04 兩句話定的設計):
//
// ① 「順序不可反這件事**門檻太高**了,用戶一定**先看到 daemon 有更新**
// 不會想到更新網頁」
// ② 「你可以設計如果它先做了地端就要求去做雲端,**會去掃一次看雲端是否裝好**,
// **沒裝好就顯示 workers AI 還沒通,一旦通了就顯示可用**」
//
// 背景:Workers AI 萃取要**兩端都新**——daemon 有新版(會打 /portal/daemon/extract
// 且雲端實例也有新版(有這條 route)。daemon 會自動更新、雲端實例要用戶自己重跑安裝器
// ⇒ 中間必然存在一段「daemon 新、雲端舊」的空窗期。
//
// 舊設計把這段空窗丟給用戶自己想通(「你順序做錯了」),那是**我們的設計沒容錯**。
// 新設計:daemon 主動掃、把狀態講出來——
//
// 雲端還沒更新 → 托盤直接顯示「雲端 AI 還沒通 ⇒ 去 portal 按『立即更新』」
// 雲端已更新 → 顯示「雲端 AI 可用」,什麼都不必做
//
// ⚠️ 這是**被動探測**,不是輪詢:只在同步時(RunDirectOnce 每輪一次)順手打一次
// HEAD-ish 的探測請求,且結果快取在 status.json 給托盤讀。不掛任何排程。
package collector
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
)
// probeHTTP 探測用短逾時:只是要知道 route 在不在,不等 LLM 生成。
var probeHTTP = &http.Client{Timeout: 20 * time.Second}
// CloudAIState 是「某個雲端實例的 Workers AI 萃取能力」探測結果。
type CloudAIState struct {
Ready bool `json:"ready"` // true=那端有 /portal/daemon/extract(雲端 AI 可用)
Note string `json:"note,omitempty"` // 給用戶看的白話(Ready=false 時說「還沒通、該做什麼」)
}
// ProbeWorkersAI 探測指定實例是否支援雲端萃取。
//
// 手法:送一個**刻意留空 text** 的請求。
// - 舊版實例沒有這條 route ⇒ 404 ⇒ 還沒通。
// - 新版實例有 route,會因為 text 是空的回 400(參數錯)——但 400 正好證明**route 存在**
// ⇒ 算通。這樣探測不會真的燒 LLM 額度,也不會產生垃圾卡片。
// - 401 代表 route 在、只是金鑰不對 ⇒ route 存在,同樣算「雲端有這功能」,
// 金鑰問題由既有的連線流程去報,不混在這裡講。
func ProbeWorkersAI(cypherURL, apiKey string) CloudAIState {
base := strings.TrimSuffix(strings.TrimSpace(cypherURL), "/")
if base == "" {
return CloudAIState{Ready: false, Note: "還沒連上知識庫"}
}
body, _ := json.Marshal(map[string]string{"page_name": "", "text": ""})
req, err := http.NewRequest(http.MethodPost, base+"/portal/daemon/extract", bytes.NewReader(body))
if err != nil {
return CloudAIState{Ready: false, Note: "雲端 AI 狀態查不到"}
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Arcrun-API-Key", strings.TrimSpace(apiKey))
resp, err := probeHTTP.Do(req)
if err != nil {
// 連不上(離線/網路問題)≠ 雲端沒裝。講「查不到」而不是「還沒通」,
// 免得把網路問題誤報成「你沒更新」讓用戶白跑一趟。
return CloudAIState{Ready: false, Note: "連不上你的知識庫,雲端 AI 狀態查不到"}
}
defer resp.Body.Close()
switch {
case resp.StatusCode == http.StatusNotFound:
return CloudAIState{
Ready: false,
Note: "雲端 AI 還沒通 ⇒ 你的知識庫是舊版,請到 portal 按「立即更新」重裝一次",
}
case resp.StatusCode < 500:
// 200/400/401… 都代表這條 route 存在=雲端有這個功能。
return CloudAIState{Ready: true}
default:
return CloudAIState{
Ready: false,
Note: fmt.Sprintf("雲端 AI 暫時有狀況(HTTP %d),稍後會自動再試", resp.StatusCode),
}
}
}