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(不再夾帶第二份完整程式)
This commit is contained in:
2026-08-06 16:00:47 +08:00
parent f4eb3c9fa1
commit 4d3a6a09a6
50 changed files with 196 additions and 193 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
// cloud_version.go — t103 daemon 雲端版本偵測。
// 每輪 GET {cypher_url}/health 取 bundle_version,比對最低相容日期,
// 過舊時 status.json 記錄,托盤顯示更新入口。
package main
package collector
import (
"encoding/json"
+1 -1
View File
@@ -1,5 +1,5 @@
// cloud_version_test.go — t103 雲端版本判定函式 4 案單元測試。
package main
package collector
import (
"os"
+5 -12
View File
@@ -19,19 +19,14 @@ VERSION="${VERSION:-$(./daemon-version.py --stamp)}"
./changelog-section.sh "$VERSION" --check
BUILD_TIME="$(date '+%Y%m%d-%H%M')"
LDFLAGS_VER="-X main.version=${VERSION} -X main.buildTime=${BUILD_TIME}"
LDFLAGS_VER="-X main.version=${VERSION} -X main.buildTime=${BUILD_TIME} -X arcrun-rag/collector.version=${VERSION} -X arcrun-rag/collector.buildTime=${BUILD_TIME}"
export PATH="$PATH:$(go env GOPATH)/bin"
echo "🏷 版本:${VERSION}build ${BUILD_TIME}"
echo "① 編 collector(同綑執行檔,純 stdlib、CGO_ENABLED=0"
# 🔴 2026-08-06 補回版本注入(**Wails 換代時掉的**):
# 退役的 fyne 版 `arcrun-tray/build-mac.sh:24` 本來就有這個(t150t72
# leo 07-29「我的和下載下來的會是同一個嗎?」),t194 重寫成 Wails 版時沒帶過來
# ⇒ 同綑的 collector 一路自稱 `dev`App 門面卻顯示 v0.18.x。
# **幹活的是 collector**:它不報版本,就沒人能判斷某個修復有沒有真的到使用者手上——
# 正是「版本號是 leo 唯一驗收介面」要擋的那個病。
# 實撞:v0.18.6 打包後 `arcrun-collector --version` 回 `dev`。
( cd ../.. && CGO_ENABLED=0 go build -ldflags "-s -w ${LDFLAGS_VER}" -o "cmd/arcrun-app/arcrun-collector" . )
# 🔴 2026-08-06 起**不再單獨編 collector**(leo:「不要兩支,寫成一支檔案」)。
# collector 已改成可被引用的套件,直接編進同一個執行檔,靠 `--collector` 換身分。
# Mac 這邊原本是 .app 裡放兩個執行檔(沒有 dropper 問題),但兩邊做法統一才不會再漂移
# ——「Mac 有、Windows 漏了」這種病本 repo 已經犯過兩次(2b83c47、v0.18.6 版本注入)。
# 🔴 2026-08-05leo 開 DMG 檢查時抓到):Info.plist 的 CFBundleShortVersionString
# 是 **1.0.0**(Wails 預設),不是我們的版本號——因為 wails.json 沒有 info.productVersion
@@ -58,12 +53,10 @@ APP="build/bin/Arcrun.app"
[ -d "build/bin/arcrun-app.app" ] && { rm -rf "$APP"; mv "build/bin/arcrun-app.app" "$APP"; }
echo "③ 同綑 collector 進 .appApp 用 os.Executable() 找同層)"
cp arcrun-collector "$APP/Contents/MacOS/arcrun-collector"
echo "④ ad-hoc 重簽(改過 bundle 內容必重簽,否則 launchd 拒開)"
codesign --force --deep --sign - "$APP" 2>/dev/null || true
rm -f arcrun-collector
echo "✅ 完成:$APP"
echo " 驗法:open '$APP' → 選單列出現 icon、Dock **不出現** icon"
echo " 點 icon → 立刻展開視窗;右鍵 → 只有「結束 Arcrun」"
+3 -3
View File
@@ -11,7 +11,8 @@
#
# 與舊 fyne 版(../arcrun-tray/build-msix.sh)的差別:
# 舊版是單一 execollector 用 //go:embed 吃進去);
# **Wails 版是兩個 exe**Arcrun.exe arcrun-collector.exe,見 supervise.go 的
# 2026-08-06 起改回一個 execollector 編進 Arcrun.exe`--collector` 換身分。
# 以下敘述保留為沿革)**Wails 版曾是兩個 exe**(見 supervise.go 的
# os.Executable() 同層查找)⇒ 兩個都要進 msix root,缺一個就「裝了不會同步」。
#
# 前置(一次就好):
@@ -52,12 +53,11 @@ fi
echo "① 編 Windows 版(兩個 exe"
VERSION="$VERSION_RAW" bash build-win.sh >/dev/null
[[ -f build/bin/arcrun-app.exe ]] || { echo "❌ Arcrun.exe 沒編出來" >&2; exit 1; }
[[ -f arcrun-collector.exe ]] || { echo "❌ collector.exe 沒編出來" >&2; exit 1; }
# 2026-08-06 起只有一支 execollector 編進去了),不再檢查第二個檔。
OUT="dist-msix"
rm -rf "$OUT" && mkdir -p "$OUT/root/Assets"
cp build/bin/arcrun-app.exe "$OUT/root/Arcrun.exe"
cp arcrun-collector.exe "$OUT/root/arcrun-collector.exe"
echo "② 複製 Store 圖示(沿用 CIS 版,不重產——sips 縮放會把方形拉扁)"
cp ../arcrun-tray/assets/store/*.png "$OUT/root/Assets/"
+25 -19
View File
@@ -47,12 +47,11 @@ echo "⓪ 由 appicon.png 產生 Windows 圖示(icon.icotrayicon.ico"
# 每次 build 都重產 ⇒ 「換了 appicon 卻沒換 Windows」這種漂移不可能再發生。
python3 gen-icons.py
echo "① 編 collector.exe(會被 embed 進 Arcrun.exe,純 stdlib、CGO_ENABLED=0"
# 🔴 2026-08-06:同 build-mac.sh 補回版本注入(Wails 換代時掉的,理由見該檔註解)。
# 2b83c47 當年就修過一次「Mac 有、Windows 漏了」——換代後兩邊又一起掉,這次一起補
( cd ../.. && GOOS=windows GOARCH=amd64 CGO_ENABLED=0 \
go build -ldflags "-s -w -X main.version=${VERSION} -X main.buildTime=${BUILD_TIME}" \
-o "cmd/arcrun-app/arcrun-collector.exe" . )
# 🔴 2026-08-06 起**不再單獨編 collector.exe**(leo:「不要兩支,寫成一支檔案」)。
# collector 已改成可被引用的套件(arcrun-rag/collector.Run),直接編進 Arcrun.exe
# 靠 `--collector` 參數換身分。磁碟上永遠只有一個 exe
# 為什麼要這樣:前一版把 collector.exe embed 進去、執行時攤到磁碟再跑,
# 那是 Windows Defender 眼中的 dropper 特徵(封測者實撞 Trojan:Win32/Sabsik.FL.A!ml)。
# 🔴 同 build-mac.shwails.json 沒 info.productVersion ⇒ exe 內容資訊永遠是 1.0.0。
# Windows 使用者在「內容→詳細資料」看到的版本讀這個欄位;MSIX 送審也會對照。
@@ -71,7 +70,8 @@ trap 'mv -f wails.json.bak wails.json 2>/dev/null || true' EXIT
echo "② wails buildwindows/amd64CGo 走 mingw"
CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc \
wails build -platform windows/amd64 -clean \
-ldflags "-X main.version=${VERSION} -X main.buildTime=${BUILD_TIME}"
-ldflags "-X main.version=${VERSION} -X main.buildTime=${BUILD_TIME} \
-X arcrun-rag/collector.version=${VERSION} -X arcrun-rag/collector.buildTime=${BUILD_TIME}"
OUT="dist"; mkdir -p "$OUT"
@@ -80,10 +80,11 @@ echo "③ 產出**單一執行檔**"
# 「下載還是 ZIP ⇒ 應該是 MSIX 或 EXE」「解開還是 2 個檔 ⇒ 應該只有 1 個檔案」
# 「兩個檔案令人很迷惑,User unfriendly」
#
# 舊做法=zip 裡放 Arcrun.exearcrun-collector.exe 同層。使用者只要少解一個檔
# 或只把 Arcrun.exe 拖出來,同步就永遠不會動,而畫面不會說是這個原因
# 新做法=collector 在步驟②被 go:embed 收進 Arcrun.exe(見 bundled_collector_windows.go),
# 第一次執行時攤到 ~/.arcrun-rag/bin/。**下載下來就是一個檔,雙擊就跑。**
# 舊做法zip 裡放兩支 exe 同層。使用者只要少解一個檔,同步就永遠不會動。
# 舊做法二(v0.18.7-8)=把 collector.exe embed 起來、執行時攤到磁碟再跑
# ⇒ **Defender 判 dropper**:封測者實撞 `Trojan:Win32/Sabsik.FL.A!ml`,檔案當場被隔離。
# 現在=collector 是套件,直接編進同一個執行檔,`--collector` 換身分。
# **下載下來就是一個檔,雙擊就跑,磁碟上不會多出任何東西。**
#
# 為什麼不出 MSIX:未上架的 MSIX 要使用者先開「開發人員模式」才裝得起來,
# 比 zip 更麻煩。上架 MS Store 後再回頭走 build-msix.sh。
@@ -91,18 +92,23 @@ EXE="$(cd "$OUT" && pwd)/Arcrun-win-${VERSION}.exe"
rm -f "$EXE"
cp build/bin/arcrun-app.exe "$EXE"
# 🔴 機械閘:embed 進去的 collector 至少要有幾 MB,否則就是「編了個空殼」
# (步驟①失敗但 wails build 仍成功時,過去只會得到一個不會同步的 exe。)
APP_SIZE=$(stat -f%z "$EXE" 2>/dev/null || stat -c%s "$EXE")
COL_SIZE=$(stat -f%z arcrun-collector.exe 2>/dev/null || stat -c%s arcrun-collector.exe)
if [ "$APP_SIZE" -lt "$COL_SIZE" ]; then
echo "❌ 產出的 exe${APP_SIZE} bytes)比 collector${COL_SIZE} bytes)還小"
echo " ⇒ collector 沒被 embed 進去,這顆裝了也不會同步。中止。"
# 🔴 機械閘:這一顆必須**自己就會當 collector**
# 過去的閘是比大小(確認 embed 進去了);現在改成**直接問它**——
# 跑 `--collector --version`,回得出版本才算數。編成空殼、或分派寫錯,都會在這裡紅。
# (不能用 wine 跑 .exe,所以驗的是同一份原始碼編出來的原生版;分派邏輯與平台無關。)
PROBE="$(mktemp -d)/probe"
go build -ldflags "-X main.version=${VERSION} -X arcrun-rag/collector.version=${VERSION}" -o "$PROBE" . || { echo "❌ 探針編不出來"; exit 1; }
PROBE_OUT="$("$PROBE" --collector --version 2>&1 | head -1)"
rm -rf "$(dirname "$PROBE")"
if [ -z "$PROBE_OUT" ]; then
echo "❌ 這顆 exe 不會當 collector--collector --version 沒有輸出)"
echo " ⇒ 裝了也不會同步。中止。"
exit 1
fi
echo " ✅ 自帶同步引擎(--collector --version → ${PROBE_OUT}"
SIZE=$(ls -lh "$EXE" | awk '{print $5}')
echo "✅ 完成:${EXE}${SIZE}內含同步引擎)"
echo "✅ 完成:${EXE}${SIZE}單一檔案、自帶同步引擎)"
echo
echo "🔬 使用者流程:下載 → 雙擊 Arcrun.exe(不必解壓縮、不必找第二個檔)"
echo " ⚠️ 未簽章 ⇒ SmartScreen 會攔(「更多資訊」→「仍要執行」)。"
-12
View File
@@ -1,12 +0,0 @@
//go:build !windows
package main
// bundled_collector_other.go — 非 Windows 平台不內嵌 collector。
//
// macOS 走 .app bundlecollector 就放在 Contents/MacOS/ 裡,對使用者而言
// **本來就只有一個可拖曳的東西**(Arcrun.app),沒有「兩個檔很迷惑」的問題。
// 所以只有 Windows 需要把 collector 收進單一 exe(見 bundled_collector_windows.go)。
// bundledCollectorPath 回空字串=沒有內嵌版,呼叫端退回「找同層」。
func bundledCollectorPath() string { return "" }
@@ -1,75 +0,0 @@
//go:build windows
package main
// bundled_collector_windows.go — 把 collector 內嵌進單一 Arcrun.exe2026-08-06
//
// 🔴 leo 08-06 封測回報②:
//
// 「下載還是 ZIP ⇒ 應該是 MSIX 或 EXE;解開還是 2 個檔 ⇒ 應該只有 1 個檔案」
// 「兩個檔案令人很迷惑,User unfriendly」
//
// 舊做法:zip 裡放 Arcrun.exearcrun-collector.exe 同層,App 用 os.Executable()
// 找隔壁那支。使用者只要少解壓一個檔、或只把 Arcrun.exe 拖出來,同步就永遠不會動
// ——而畫面不會說是這個原因。
//
// 新做法:collector 直接 embed 進 Arcrun.exe,第一次執行時攤到
// `~/.arcrun-rag/bin/arcrun-collector.exe`(與 config.jsoncollector.log 同一個資料夾,
// 使用者要備份或清乾淨只有一個地方)。
//
// 為什麼不選 MSIX:未上架的 MSIX 要使用者先開「開發人員模式」才裝得起來,
// 比 zip 更麻煩(見 docs-site 的 install-windows 方式 B)。上架後再回頭走 MSIX。
//
// ⚠️ 編譯前提:`arcrun-collector.exe` 必須先存在同目錄(build-win.sh 步驟①會先編)。
// 它被 .gitignore 排除,所以**不能**直接 `go build` 一個乾淨 clone——一律走 build-win.sh。
import (
"crypto/sha256"
_ "embed"
"encoding/hex"
"os"
"path/filepath"
)
//go:embed arcrun-collector.exe
var bundledCollector []byte
// bundledCollectorPath 把內嵌的 collector 攤到磁碟並回傳路徑。
//
// 冪等:內容一樣就不重寫(避免每次啟動都寫 16MB,也避免正在跑的那支被覆寫)。
// 出錯回空字串——呼叫端會退回「找同層」的舊行為,並由 appLog 留下痕跡。
func bundledCollectorPath() string {
if len(bundledCollector) == 0 {
return ""
}
dir := filepath.Join(appDir(), "bin")
dst := filepath.Join(dir, "arcrun-collector.exe")
want := sha256.Sum256(bundledCollector)
if have, err := os.ReadFile(dst); err == nil {
if got := sha256.Sum256(have); got == want {
return dst // 已是同一版,不動它
}
}
if err := os.MkdirAll(dir, 0o755); err != nil {
appLog("攤開內嵌同步引擎失敗(建資料夾):%v", err)
return ""
}
// 先寫暫存再 rename:中途失敗不會留下半截的執行檔。
tmp := dst + ".new"
if err := os.WriteFile(tmp, bundledCollector, 0o755); err != nil {
appLog("攤開內嵌同步引擎失敗(寫檔):%v", err)
return ""
}
// Windows 不允許覆寫「正在執行」的檔案;先移開舊的再換上新的。
_ = os.Remove(dst + ".old")
_ = os.Rename(dst, dst+".old")
if err := os.Rename(tmp, dst); err != nil {
appLog("攤開內嵌同步引擎失敗(換檔):%v", err)
return ""
}
_ = os.Remove(dst + ".old")
appLog("已攤開內嵌同步引擎:%ssha256 %s", dst, hex.EncodeToString(want[:8]))
return dst
}
+3
View File
@@ -15,6 +15,8 @@ require (
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/jolestar/go-commons-pool/v2 v2.1.2 // indirect
github.com/klippa-app/go-pdfium v1.19.5 // indirect
github.com/labstack/echo/v4 v4.13.3 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
@@ -27,6 +29,7 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/samber/lo v1.49.1 // indirect
github.com/tetratelabs/wazero v1.12.0 // indirect
github.com/tkrajina/go-reflector v0.5.8 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
+36
View File
@@ -1,21 +1,39 @@
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/energye/systray v1.0.3 h1:XnyjJCeRU5z00bpNOic2fGTKz/7yHZMZjWiGIVXDS+4=
github.com/energye/systray v1.0.3/go.mod h1:HelKhC3PXwv3ryDxbuQqV+7kAxAYNzE5cfdrerGOZTc=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg=
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/jolestar/go-commons-pool/v2 v2.1.2 h1:E+XGo58F23t7HtZiC/W6jzO2Ux2IccSH/yx4nD+J1CM=
github.com/jolestar/go-commons-pool/v2 v2.1.2/go.mod h1:r4NYccrkS5UqP1YQI1COyTZ9UjPJAAGTUxzcsK1kqhY=
github.com/klippa-app/go-pdfium v1.19.5 h1:0TRc47tuumzOB6bkskS6ZP5mhspjAX+iXpvM8pUVKM8=
github.com/klippa-app/go-pdfium v1.19.5/go.mod h1:poSiUJYFicnfT8dazUfVnxcqPlKQxO3aHdGbOa1lG1w=
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
@@ -38,6 +56,10 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/onsi/ginkgo/v2 v2.32.0 h1:Hw7s2pVrQo/8Yz5N77qdnpHaoc+c6cC9WIV1Jce+J6E=
github.com/onsi/ginkgo/v2 v2.32.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44=
github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I=
github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -49,8 +71,12 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
@@ -63,11 +89,17 @@ github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhw
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v2 v2.13.0 h1:S7OgXWpj72V91unF8iDWJKbcS9ZpwCT3R0QVru4v2Mg=
github.com/wailsapp/wails/v2 v2.13.0/go.mod h1:nVr/wSIEZ7xxKPkzK65mjpKpaOPQI2k4pvLwGR/i4kc=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -81,5 +113,9 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+22
View File
@@ -3,8 +3,11 @@ package main
import (
"context"
"embed"
"os"
"sync/atomic"
collector "arcrun-rag/collector"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
@@ -36,7 +39,26 @@ func beginQuit() { quitting.Store(true) }
// 這個判斷才是 leo 08-06 那個 bug 的核心(結束與按 × 共用同一個回呼)。
func closeMeansHide() bool { return !quitting.Load() }
// collectorModeFlag 是「把自己當同步引擎跑」的第一個參數。
//
// 🔴 2026-08-06 leo 裁決:「不要兩支,寫成一支檔案」。
//
// collector 的程式碼已經編進這個執行檔(arcrun-rag/collector.Run),
// 所以 supervisor 拉起子行程時跑的是**同一個 exe**,只是多帶這個參數。
// 磁碟上不會出現第二支執行檔——那正是 Windows Defender 判 dropper 的特徵
// (封測者實撞 Trojan:Win32/Sabsik.FL.A!ml,檔案被隔離)。
//
// 用雙連字號開頭而不是子命令,是為了與 collector 自己的子命令(scan/sync/direct…)
// 區分:使用者手動打的是那些,這一個是我們內部用的。
const collectorModeFlag = "--collector"
func main() {
// 🔴 必須是 main() 的**第一件事**:這個模式下不能碰 GUI、托盤、單一實例鎖,
// 否則每跑一輪同步就會多一個托盤 icon、還會被單一實例鎖擋掉。
if len(os.Args) > 1 && os.Args[1] == collectorModeFlag {
os.Exit(collector.Run(os.Args[2:]))
}
app := NewApp()
// 🔴 托盤必須在**主執行緒**建立(NSStatusItem 內部會 new NSWindow
+17 -18
View File
@@ -13,7 +13,6 @@ import (
"os"
"os/signal"
"path/filepath"
"runtime"
"syscall"
"time"
@@ -22,32 +21,31 @@ import (
var sup *supervisor.Supervisor
// collectorBinPath 找同綑的 collector 執行檔
// collectorBinPath 回傳「要跑哪一支執行檔當同步引擎」——答案是**我們自己**
//
// 兩種同綑方式,依平台:
// - Windowscollector **embed 在 Arcrun.exe 裡**,第一次執行攤到 ~/.arcrun-rag/bin/
// leo 08-06:「解開還是 2 個檔 ⇒ 應該只有 1 個檔案」)。見 bundled_collector_windows.go。
// - macOS:放在 .app 的 Contents/MacOS/ 同層(使用者只看到一個可拖曳的 Arcrun.app)。
// 🔴 2026-08-06 leo 裁決:「不要兩支,寫成一支檔案」。
//
// 內嵌攤開失敗時**退回找同層**——舊版打包出來的資料夾仍然能跑,不會因為換機制而全壞。
// 先前兩代做法都有病:
// ① zip 裡放兩支 exe 同層 ⇒ 使用者少拿一支就永遠不同步(leo 封測回報②)
// ② 內嵌 collector.exe、執行時攤到磁碟再跑 ⇒ **Windows Defender 眼中的 dropper**
// (封測者實撞:檔案「包含病毒或潛在的垃圾軟體」當場被刪)
// 現在:collector 已改成可被引用的套件(arcrun-rag/collector.Run),
// App 直接把它編進同一個執行檔,靠 `--collector` 參數換身分。
// ⇒ **磁碟上永遠只有一個 exe,沒有任何東西被攤出來。**
func collectorBinPath() string {
if p := bundledCollectorPath(); p != "" {
return p
}
name := "arcrun-collector"
if runtime.GOOS == "windows" {
name += ".exe"
}
exe, err := os.Executable()
if err != nil {
return name
return ""
}
if p, err := filepath.EvalSymlinks(exe); err == nil {
exe = p
}
return filepath.Join(filepath.Dir(exe), name)
return exe
}
// collectorArgPrefix 是「把自己當 collector 跑」的參數(見 main.go 開頭的分派)。
func collectorArgPrefix() []string { return []string{collectorModeFlag} }
// startSupervisor 在 App 啟動時拉起 collector;沒有 config 就先不啟動
// (使用者還沒連知識庫,啟動只會一直失敗刷 log)。
func startSupervisor() {
@@ -56,8 +54,8 @@ func startSupervisor() {
// 🔴 t195:這裡以前只 Stat(config) 就靜默 return,出問題完全查不到。
// leo 實撞:App 活著、collector 從沒啟動、畫面卻顯示「正在整理知識卡」。
// ⇒ 每個 return 點都要留下痕跡(寫進 collector.log 旁邊的 app.log)。
if _, err := os.Stat(bin); err != nil {
appLog("啟動同步引擎失敗:不到 %s%v", bin, err)
if bin == "" {
appLog("啟動同步引擎失敗:不到自己的執行檔路徑")
return
}
if _, err := os.Stat(cfg); err != nil {
@@ -65,6 +63,7 @@ func startSupervisor() {
return
}
sup = supervisor.New(bin, cfg)
sup.ArgPrefix = collectorArgPrefix() // 同一支執行檔,換身分跑
sup.Start()
appLog("同步引擎已啟動:%s", bin)
}
+14
View File
@@ -0,0 +1,14 @@
// Command collector 是 collector 的獨立命令列執行檔。
//
// 真正的邏輯全在 arcrun-rag/collector 套件裡(Run)。這支只是薄薄的外殼,
// 讓「單獨跑 collector」這條路(開發、除錯、伺服器端批次)繼續可用;
// 桌面 App 則直接呼叫同一個 Run——**兩者共用同一份程式碼,不是兩份**。
package main
import (
"os"
collector "arcrun-rag/collector"
)
func main() { os.Exit(collector.Run(os.Args[1:])) }
+1 -1
View File
@@ -14,7 +14,7 @@
// 所以每個抽取器都必須是**純 Go/無 CGo**(才能跨編 Windows,t72 已實測這條路可行)。
//
// 加新格式=在 extractors 註冊一個 func,不動架構。
package main
package collector
import (
"fmt"
+1 -1
View File
@@ -8,7 +8,7 @@
//
// 對比 PDF 為何非要引擎不可:PDF 是**印刷格式**,記的是「這個字畫在第 3 頁座標 (120,400)」,
// 要把散落字元還原成通順文字=需要整顆排版還原引擎。兩者不是同一個難度等級。
package main
package collector
import (
"archive/zip"
+1 -1
View File
@@ -21,7 +21,7 @@
//
// ⛔ **抽不出字的情況**:掃描件/翻拍的 PDF——PDFium **不做 OCR**,會回空字串。
// 這時回 ErrNoText 讓上層給用戶明確訊息,**絕不靜默當成空內容送出去**。
package main
package collector
import (
"fmt"
+1 -1
View File
@@ -1,4 +1,4 @@
package main
package collector
import (
"errors"
+1 -1
View File
@@ -4,7 +4,7 @@
// 與 ppt/notesSlides/notesSlideN.xml(備註)。
// 文字全在 DrawingML <a:t> 標籤裡;段落是 <a:p>。
// 體積代價幾乎零(只用標準庫)。
package main
package collector
import (
"archive/zip"
+1 -1
View File
@@ -1,4 +1,4 @@
package main
package collector
import (
"archive/zip"
+1 -1
View File
@@ -14,7 +14,7 @@
//
// 防呆:超大表格(機器產生的那種)仍會截斷並明說截斷了——不能讓一個檔案吃掉
// 整個 context,也不能靜默丟資料(靜默是本專案一再犯的病)。
package main
package collector
import (
"archive/zip"
+1 -1
View File
@@ -1,4 +1,4 @@
package main
package collector
import (
"os"
+1 -1
View File
@@ -1,4 +1,4 @@
package main
package collector
import (
"archive/zip"
+1 -1
View File
@@ -1,4 +1,4 @@
package main
package collector
import (
"archive/zip"
+1 -1
View File
@@ -17,7 +17,7 @@
// --dry-run:只列出會送出的動作,不 POST、不寫 manifest。
//
// 跨平台:純 stdlib、輪詢式偵測(不依賴 fsnotify)=零 CGodarwin/arm64、windows/amd64 直接交叉編譯。
package main
package collector
import (
"bytes"
+1 -1
View File
@@ -1,5 +1,5 @@
// direct_extract_test.go — task 6extractor 模式端到端(本地萃卡→POST rag_ingest_card)。
package main
package collector
import (
"encoding/json"
+1 -1
View File
@@ -1,5 +1,5 @@
// direct_multi_test.go — daemon-beta task 1 多資料夾 config(單數相容/manifest 分根/多根掃描)。
package main
package collector
import (
"crypto/sha256"
+1 -1
View File
@@ -1,5 +1,5 @@
// direct_sync_now_test.go — t98 立刻同步訊號檔邏輯單元測試。
package main
package collector
import (
"os"
+1 -1
View File
@@ -7,7 +7,7 @@
// 1. 更新到 v0.15.5 後托盤**兩個帳號都還顯示 Gemini**(帳號層舊值沒清)
// 2. 丟 PDF 進去**產不出卡**(因為根本沒走到 Workers AI,還在跑 Gemini
// 3. `workers-ai` 不在合法值清單裡 ⇒ 一旦寫進 config,daemon 直接起不來
package main
package collector
import (
"encoding/json"
+1 -1
View File
@@ -8,7 +8,7 @@
//
// 兩路的產出契約相同:回傳「本次新增/變動的卡片相對路徑」,交 task 6 推 ingest。
// 原文永不出本函式(claude 路整段在用戶機器;gemma 路內容過境 API 但不落任何雲儲存)。
package main
package collector
import (
"context"
+1 -1
View File
@@ -6,7 +6,7 @@
//
// gemma-4-31b 是思考型模型:parts[0] 常是 thought=true 的思考草稿,真答案在最後一個
// 非 thought 的 text partagent-memory §7.6 實撞教訓,讀法照抄)。
package main
package collector
import (
"bytes"
+1 -1
View File
@@ -1,5 +1,5 @@
// extract_gemma_test.go — task 4httptest 替身:驗 prompt 契約/thought 淨化/落卡)。
package main
package collector
import (
"encoding/json"
+1 -1
View File
@@ -1,5 +1,5 @@
// extract_test.go — task 3 claude 萃取路(用 stub 執行檔驗呼叫契約,不需真 claude)。
package main
package collector
import (
"fmt"
+1 -1
View File
@@ -24,7 +24,7 @@
//
// ⚠️ 隱私邊界不變:送出去的是**已在本機轉成純文字的原稿**,回來的是知識卡;
// 原始檔案(docx/pdf/xlsx)仍然不出用戶的電腦。
package main
package collector
import (
"bytes"
+20 -12
View File
@@ -23,7 +23,7 @@
// 不隨 payload 送出(r2_key 語意=原稿已在 R2)。防呆警告輪照送 warnings、不含下架事件。
//
// daemon 常駐(launchd)=之後的 task(產品化段 task 11)。
package main
package collector
import (
"encoding/json"
@@ -58,28 +58,36 @@ func versionString() string {
return version + " (build " + buildTime + ")"
}
func main() {
if len(os.Args) < 2 {
// Run 是 collector 的命令列入口,回傳 exit code。
//
// 🔴 2026-08-06:這裡以前叫 main()。改成匯出函式,是為了讓桌面 App **直接呼叫**,
// 而不是「把第二支 .exe 內嵌起來、執行時攤到磁碟再跑」。
// 後者是 Windows Defender 眼中的 dropper 特徵——leo 的封測者實撞:
// 「因為檔案包含病毒或潛在的垃圾軟體,所以作業未順利完成」,檔案當場被刪。
// leo 08-06 裁決:「不要兩支,寫成一支檔案」⇒ 同一個執行檔靠參數切換身分,
// 磁碟上永遠只有一個 exe,沒有任何東西被攤出來。
func Run(args []string) int {
if len(args) < 1 {
usage()
os.Exit(2)
return 2
}
switch os.Args[1] {
switch args[0] {
case "--version", "-v", "version":
fmt.Println(versionString())
os.Exit(0)
return 0
case "scan":
os.Exit(run(os.Args[2:], runMode{}))
return run(args[1:], runMode{})
case "upload":
os.Exit(run(os.Args[2:], runMode{withUpload: true}))
return run(args[1:], runMode{withUpload: true})
case "sync":
os.Exit(run(os.Args[2:], runMode{withUpload: true, withTrigger: true}))
return run(args[1:], runMode{withUpload: true, withTrigger: true})
case "direct":
os.Exit(runDirect(os.Args[2:]))
return runDirect(args[1:])
case "template-install":
os.Exit(runTemplateInstall(os.Args[2:]))
return runTemplateInstall(args[1:])
default:
usage()
os.Exit(2)
return 2
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
// manifest.go — collector 本機 manifest 讀寫(SDD ingest-hash-trigger design §2)。
// manifest 只由 collector 寫入,雲端不得回寫(design §6-4)。
package main
package collector
import (
"crypto/rand"
+1 -1
View File
@@ -1,4 +1,4 @@
package main
package collector
import (
"encoding/json"
+1 -1
View File
@@ -4,7 +4,7 @@
// 原本 manifest 只記成功不記失敗 ⇒ 每輪重掃都把失敗檔當「新檔」
// ⇒ 實測 1387 輪 × 11 小時撞同一面牆,且壞檔排在前面**拖住整個資料夾**。
// 401 修好了,下次換別的錯照樣卡死——所以要測的是「退避本身」。
package main
package collector
import "testing"
+1 -1
View File
@@ -19,7 +19,7 @@
//
// ⚠️ 這是**被動探測**,不是輪詢:只在同步時(RunDirectOnce 每輪一次)順手打一次
// HEAD-ish 的探測請求,且結果快取在 status.json 給托盤讀。不掛任何排程。
package main
package collector
import (
"bytes"
+1 -1
View File
@@ -2,7 +2,7 @@
// 事件順序:先把本輪 removed×added 以 content_hash 配對成 renamed(只更新路徑映射),
// 再分類其餘 added/modified/removedremoved 數 > manifest 條目 × 門檻(預設 40%)
// → removed 全部不執行、改發警告(R6)。本階段不接網路,事件輸出到 stdout。
package main
package collector
import (
"crypto/sha256"
+1 -1
View File
@@ -1,4 +1,4 @@
package main
package collector
// scan_skipped_test.go — G-6.2「不准安靜地略過」(J-1/S62026-08-06)。
//
+1 -1
View File
@@ -1,4 +1,4 @@
package main
package collector
import (
"crypto/sha256"
+13 -4
View File
@@ -50,8 +50,8 @@ type Status struct {
// round 對應 collector direct 每輪印到 stdout 的 JSON(見 direct.go runOne)。
type round struct {
At string `json:"at"`
Phase string `json:"phase"` // t191"start"(開工)/"done"(跑完);舊版沒有此欄=空
At string `json:"at"`
Phase string `json:"phase"` // t191"start"(開工)/"done"(跑完);舊版沒有此欄=空
Folder string `json:"folder"`
Results []json.RawMessage `json:"results"`
@@ -101,7 +101,12 @@ func (l *rotatingLog) Write(p []byte) (int, error) {
// Supervisor 看守單一 collector direct 行程。
type Supervisor struct {
BinPath string // collector 執行檔路徑(托盤 app bundle 內)
BinPath string // collector 執行檔路徑(托盤 app bundle 內)
// ArgPrefix 插在 "direct" 之前的參數。
// 🔴 2026-08-06:桌面 App 不再內嵌第二支 .exe 再攤到磁碟(Defender 眼中的 dropper
// 特徵),改成**同一支執行檔**用 `--collector` 換身分 ⇒ BinPath 指向自己、
// ArgPrefix = ["--collector"]。leo 裁決:「不要兩支,寫成一支檔案」。
ArgPrefix []string
ConfigPath string // direct config.json 路徑
Backoff time.Duration // 非預期退出後的重起間隔(0=預設 3s)
LogPath string // 子行程輸出落地檔(空=~/.arcrun-rag/collector.logt14
@@ -257,7 +262,11 @@ func (s *Supervisor) loop(ctx context.Context) {
// runOnce 跑一次 collector direct 行程,串流其 stdout JSON 更新狀態,回傳退出原因。
func (s *Supervisor) runOnce(ctx context.Context) error {
cmd := exec.CommandContext(ctx, s.BinPath, "direct", "--config", s.ConfigPath)
// ArgPrefix 讓呼叫端把「同一支執行檔換個身分跑」的參數插在最前面
// (桌面 App 用 `Arcrun.exe --collector direct --config …` 跑自己)。
// 空的時候行為與過去完全一樣(獨立的 collector 執行檔)。
args := append(append([]string{}, s.ArgPrefix...), "direct", "--config", s.ConfigPath)
cmd := exec.CommandContext(ctx, s.BinPath, args...)
// t75Windows 上不要讓 collector 彈出 console 視窗(每輪閃一次,使用者會以為中毒)。
// 非 Windows 平台為 no-op,見 hidewindow_*.go。
hideChildWindow(cmd)
+1 -1
View File
@@ -1,6 +1,6 @@
// sync_status.go — 每輪同步後的彙總狀態(t91 狀態可見性)。
// 寫成 JSON 供托盤讀取,讓使用者第一眼看到萃取是否正常。
package main
package collector
import (
"encoding/json"
+1 -1
View File
@@ -1,5 +1,5 @@
// sync_status_test.go — t91/t92 狀態檔與 fallback 路徑邏輯單元測試。
package main
package collector
import (
"os"
+1 -1
View File
@@ -1,4 +1,4 @@
package main
package collector
import (
"crypto/sha256"
+1 -1
View File
@@ -6,7 +6,7 @@
//
// 冪等鐵律:**已存在的檔案一律不覆寫**(用戶的 status.md/wiki 是他的資產;
// 升級 template 版本=另案 update 流程,不在本函式)。
package main
package collector
import (
"embed"
+1 -1
View File
@@ -1,5 +1,5 @@
// template_install_test.go — daemon-beta task 2(代裝冪等/wiki 產物區不被掃)。
package main
package collector
import (
"os"
+1 -1
View File
@@ -14,7 +14,7 @@
// R2;沒上去就送=消費端 fetch 必 404)——下輪重試補送。renamed/removed 不依賴 R2,照送。
// - 防呆警告輪(mass_delete_guard):removed 事件已被 collector 壓下,但 payload 連同
// warnings 照送(消費端/執行紀錄看得到警告,不執行下架)。
package main
package collector
import (
"bytes"
+1 -1
View File
@@ -1,4 +1,4 @@
package main
package collector
import (
"encoding/json"
+1 -1
View File
@@ -9,7 +9,7 @@
// 冪等:先做存在檢查(GET+Range 1 byte;端點不支援 HEAD,見 Exists 註解),
// 已存在=no-op 跳過 PUTcontent-addressed 天然去重,design §4)。
// 設定走環境變數 CF_ACCOUNT_ID / CF_API_TOKEN / R2_BUCKET,絕不落 repo。
package main
package collector
import (
"fmt"
+1 -1
View File
@@ -1,4 +1,4 @@
package main
package collector
import (
"io"