From 4d3a6a09a67c2bdc9ef0ae81c81070b75b041e26 Mon Sep 17 00:00:00 2001 From: richblack Date: Thu, 6 Aug 2026 16:00:47 +0800 Subject: [PATCH] =?UTF-8?q?v0.18.9=EF=BC=9Acollector=20=E4=BD=B5=E9=80=B2?= =?UTF-8?q?=E5=90=8C=E4=B8=80=E6=94=AF=E5=9F=B7=E8=A1=8C=E6=AA=94=E2=80=94?= =?UTF-8?q?=E2=80=94=E7=A3=81=E7=A2=9F=E4=B8=8A=E4=B8=8D=E5=86=8D=E6=94=A4?= =?UTF-8?q?=E5=87=BA=E7=AC=AC=E4=BA=8C=E6=94=AF=20exe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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(不再夾帶第二份完整程式) --- cloud_version.go | 2 +- cloud_version_test.go | 2 +- cmd/arcrun-app/build-mac.sh | 17 ++--- cmd/arcrun-app/build-msix.sh | 6 +- cmd/arcrun-app/build-win.sh | 44 ++++++------ cmd/arcrun-app/bundled_collector_other.go | 12 ---- cmd/arcrun-app/bundled_collector_windows.go | 75 --------------------- cmd/arcrun-app/go.mod | 3 + cmd/arcrun-app/go.sum | 36 ++++++++++ cmd/arcrun-app/main.go | 22 ++++++ cmd/arcrun-app/supervise.go | 35 +++++----- cmd/collector/main.go | 14 ++++ convert.go | 2 +- convert_docx.go | 2 +- convert_pdf.go | 2 +- convert_pdf_test.go | 2 +- convert_pptx.go | 2 +- convert_pptx_test.go | 2 +- convert_table.go | 2 +- convert_table_test.go | 2 +- convert_test.go | 2 +- convert_wiring_test.go | 2 +- direct.go | 2 +- direct_extract_test.go | 2 +- direct_multi_test.go | 2 +- direct_sync_now_test.go | 2 +- direct_t182_test.go | 2 +- extract.go | 2 +- extract_gemma.go | 2 +- extract_gemma_test.go | 2 +- extract_test.go | 2 +- extract_workersai.go | 2 +- main.go | 32 +++++---- manifest.go | 2 +- manifest_extractedby_test.go | 2 +- manifest_retry_test.go | 2 +- probe_workersai.go | 2 +- scan.go | 2 +- scan_skipped_test.go | 2 +- scan_test.go | 2 +- supervisor/supervisor.go | 17 +++-- sync_status.go | 2 +- sync_status_test.go | 2 +- t52_library_test.go | 2 +- template_install.go | 2 +- template_install_test.go | 2 +- trigger.go | 2 +- trigger_test.go | 2 +- upload.go | 2 +- upload_test.go | 2 +- 50 files changed, 196 insertions(+), 193 deletions(-) delete mode 100644 cmd/arcrun-app/bundled_collector_other.go delete mode 100644 cmd/arcrun-app/bundled_collector_windows.go create mode 100644 cmd/collector/main.go diff --git a/cloud_version.go b/cloud_version.go index d3a509a..7e86cff 100644 --- a/cloud_version.go +++ b/cloud_version.go @@ -1,7 +1,7 @@ // cloud_version.go — t103 daemon 雲端版本偵測。 // 每輪 GET {cypher_url}/health 取 bundle_version,比對最低相容日期, // 過舊時 status.json 記錄,托盤顯示更新入口。 -package main +package collector import ( "encoding/json" diff --git a/cloud_version_test.go b/cloud_version_test.go index 136ae85..6d30922 100644 --- a/cloud_version_test.go +++ b/cloud_version_test.go @@ -1,5 +1,5 @@ // cloud_version_test.go — t103 雲端版本判定函式 4 案單元測試。 -package main +package collector import ( "os" diff --git a/cmd/arcrun-app/build-mac.sh b/cmd/arcrun-app/build-mac.sh index b63a7ae..febeea0 100755 --- a/cmd/arcrun-app/build-mac.sh +++ b/cmd/arcrun-app/build-mac.sh @@ -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` 本來就有這個(t150/t72, -# 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-05(leo 開 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 進 .app(App 用 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」" diff --git a/cmd/arcrun-app/build-msix.sh b/cmd/arcrun-app/build-msix.sh index 4316753..ff656d9 100755 --- a/cmd/arcrun-app/build-msix.sh +++ b/cmd/arcrun-app/build-msix.sh @@ -11,7 +11,8 @@ # # 與舊 fyne 版(../arcrun-tray/build-msix.sh)的差別: # 舊版是單一 exe(collector 用 //go:embed 吃進去); -# **Wails 版是兩個 exe**(Arcrun.exe + arcrun-collector.exe,見 supervise.go 的 +# (2026-08-06 起改回一個 exe:collector 編進 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 起只有一支 exe(collector 編進去了),不再檢查第二個檔。 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/" diff --git a/cmd/arcrun-app/build-win.sh b/cmd/arcrun-app/build-win.sh index 3a6fc59..2abcb8a 100755 --- a/cmd/arcrun-app/build-win.sh +++ b/cmd/arcrun-app/build-win.sh @@ -47,12 +47,11 @@ echo "⓪ 由 appicon.png 產生 Windows 圖示(icon.ico/trayicon.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.sh:wails.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 build(windows/amd64,CGo 走 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.exe+arcrun-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 會攔(「更多資訊」→「仍要執行」)。" diff --git a/cmd/arcrun-app/bundled_collector_other.go b/cmd/arcrun-app/bundled_collector_other.go deleted file mode 100644 index 8d7d916..0000000 --- a/cmd/arcrun-app/bundled_collector_other.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build !windows - -package main - -// bundled_collector_other.go — 非 Windows 平台不內嵌 collector。 -// -// macOS 走 .app bundle:collector 就放在 Contents/MacOS/ 裡,對使用者而言 -// **本來就只有一個可拖曳的東西**(Arcrun.app),沒有「兩個檔很迷惑」的問題。 -// 所以只有 Windows 需要把 collector 收進單一 exe(見 bundled_collector_windows.go)。 - -// bundledCollectorPath 回空字串=沒有內嵌版,呼叫端退回「找同層」。 -func bundledCollectorPath() string { return "" } diff --git a/cmd/arcrun-app/bundled_collector_windows.go b/cmd/arcrun-app/bundled_collector_windows.go deleted file mode 100644 index 8a7a710..0000000 --- a/cmd/arcrun-app/bundled_collector_windows.go +++ /dev/null @@ -1,75 +0,0 @@ -//go:build windows - -package main - -// bundled_collector_windows.go — 把 collector 內嵌進單一 Arcrun.exe(2026-08-06) -// -// 🔴 leo 08-06 封測回報②: -// -// 「下載還是 ZIP ⇒ 應該是 MSIX 或 EXE;解開還是 2 個檔 ⇒ 應該只有 1 個檔案」 -// 「兩個檔案令人很迷惑,User unfriendly」 -// -// 舊做法:zip 裡放 Arcrun.exe+arcrun-collector.exe 同層,App 用 os.Executable() -// 找隔壁那支。使用者只要少解壓一個檔、或只把 Arcrun.exe 拖出來,同步就永遠不會動 -// ——而畫面不會說是這個原因。 -// -// 新做法:collector 直接 embed 進 Arcrun.exe,第一次執行時攤到 -// `~/.arcrun-rag/bin/arcrun-collector.exe`(與 config.json/collector.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("已攤開內嵌同步引擎:%s(sha256 %s)", dst, hex.EncodeToString(want[:8])) - return dst -} diff --git a/cmd/arcrun-app/go.mod b/cmd/arcrun-app/go.mod index 07a1050..0c5d612 100644 --- a/cmd/arcrun-app/go.mod +++ b/cmd/arcrun-app/go.mod @@ -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 diff --git a/cmd/arcrun-app/go.sum b/cmd/arcrun-app/go.sum index 955bc0e..71db8b8 100644 --- a/cmd/arcrun-app/go.sum +++ b/cmd/arcrun-app/go.sum @@ -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= diff --git a/cmd/arcrun-app/main.go b/cmd/arcrun-app/main.go index ae0d568..148d23f 100644 --- a/cmd/arcrun-app/main.go +++ b/cmd/arcrun-app/main.go @@ -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, diff --git a/cmd/arcrun-app/supervise.go b/cmd/arcrun-app/supervise.go index d484960..414974b 100644 --- a/cmd/arcrun-app/supervise.go +++ b/cmd/arcrun-app/supervise.go @@ -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 回傳「要跑哪一支執行檔當同步引擎」——答案是**我們自己**。 // -// 兩種同綑方式,依平台: -// - Windows:collector **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) } diff --git a/cmd/collector/main.go b/cmd/collector/main.go new file mode 100644 index 0000000..8f23317 --- /dev/null +++ b/cmd/collector/main.go @@ -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:])) } diff --git a/convert.go b/convert.go index 864e9fb..41b0464 100644 --- a/convert.go +++ b/convert.go @@ -14,7 +14,7 @@ // 所以每個抽取器都必須是**純 Go/無 CGo**(才能跨編 Windows,t72 已實測這條路可行)。 // // 加新格式=在 extractors 註冊一個 func,不動架構。 -package main +package collector import ( "fmt" diff --git a/convert_docx.go b/convert_docx.go index e4f27d1..f507eeb 100644 --- a/convert_docx.go +++ b/convert_docx.go @@ -8,7 +8,7 @@ // // 對比 PDF 為何非要引擎不可:PDF 是**印刷格式**,記的是「這個字畫在第 3 頁座標 (120,400)」, // 要把散落字元還原成通順文字=需要整顆排版還原引擎。兩者不是同一個難度等級。 -package main +package collector import ( "archive/zip" diff --git a/convert_pdf.go b/convert_pdf.go index 57973c0..ad5c032 100644 --- a/convert_pdf.go +++ b/convert_pdf.go @@ -21,7 +21,7 @@ // // ⛔ **抽不出字的情況**:掃描件/翻拍的 PDF——PDFium **不做 OCR**,會回空字串。 // 這時回 ErrNoText 讓上層給用戶明確訊息,**絕不靜默當成空內容送出去**。 -package main +package collector import ( "fmt" diff --git a/convert_pdf_test.go b/convert_pdf_test.go index 70571c5..b1cd784 100644 --- a/convert_pdf_test.go +++ b/convert_pdf_test.go @@ -1,4 +1,4 @@ -package main +package collector import ( "errors" diff --git a/convert_pptx.go b/convert_pptx.go index 7d36893..b44a94b 100644 --- a/convert_pptx.go +++ b/convert_pptx.go @@ -4,7 +4,7 @@ // 與 ppt/notesSlides/notesSlideN.xml(備註)。 // 文字全在 DrawingML 標籤裡;段落是 。 // 體積代價幾乎零(只用標準庫)。 -package main +package collector import ( "archive/zip" diff --git a/convert_pptx_test.go b/convert_pptx_test.go index 7b0832d..9039fb0 100644 --- a/convert_pptx_test.go +++ b/convert_pptx_test.go @@ -1,4 +1,4 @@ -package main +package collector import ( "archive/zip" diff --git a/convert_table.go b/convert_table.go index 1f66d3a..3576032 100644 --- a/convert_table.go +++ b/convert_table.go @@ -14,7 +14,7 @@ // // 防呆:超大表格(機器產生的那種)仍會截斷並明說截斷了——不能讓一個檔案吃掉 // 整個 context,也不能靜默丟資料(靜默是本專案一再犯的病)。 -package main +package collector import ( "archive/zip" diff --git a/convert_table_test.go b/convert_table_test.go index c30a3be..5493c17 100644 --- a/convert_table_test.go +++ b/convert_table_test.go @@ -1,4 +1,4 @@ -package main +package collector import ( "os" diff --git a/convert_test.go b/convert_test.go index 25d1dba..5093b73 100644 --- a/convert_test.go +++ b/convert_test.go @@ -1,4 +1,4 @@ -package main +package collector import ( "archive/zip" diff --git a/convert_wiring_test.go b/convert_wiring_test.go index 9a891c7..6046ce4 100644 --- a/convert_wiring_test.go +++ b/convert_wiring_test.go @@ -1,4 +1,4 @@ -package main +package collector import ( "archive/zip" diff --git a/direct.go b/direct.go index 756bb13..d351621 100644 --- a/direct.go +++ b/direct.go @@ -17,7 +17,7 @@ // --dry-run:只列出會送出的動作,不 POST、不寫 manifest。 // // 跨平台:純 stdlib、輪詢式偵測(不依賴 fsnotify)=零 CGo,darwin/arm64、windows/amd64 直接交叉編譯。 -package main +package collector import ( "bytes" diff --git a/direct_extract_test.go b/direct_extract_test.go index b0fac59..37fb0d4 100644 --- a/direct_extract_test.go +++ b/direct_extract_test.go @@ -1,5 +1,5 @@ // direct_extract_test.go — task 6:extractor 模式端到端(本地萃卡→POST rag_ingest_card)。 -package main +package collector import ( "encoding/json" diff --git a/direct_multi_test.go b/direct_multi_test.go index 715cbf1..7d13d79 100644 --- a/direct_multi_test.go +++ b/direct_multi_test.go @@ -1,5 +1,5 @@ // direct_multi_test.go — daemon-beta task 1 多資料夾 config(單數相容/manifest 分根/多根掃描)。 -package main +package collector import ( "crypto/sha256" diff --git a/direct_sync_now_test.go b/direct_sync_now_test.go index 65900b7..a9ed30d 100644 --- a/direct_sync_now_test.go +++ b/direct_sync_now_test.go @@ -1,5 +1,5 @@ // direct_sync_now_test.go — t98 立刻同步訊號檔邏輯單元測試。 -package main +package collector import ( "os" diff --git a/direct_t182_test.go b/direct_t182_test.go index 5e7f473..0452285 100644 --- a/direct_t182_test.go +++ b/direct_t182_test.go @@ -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" diff --git a/extract.go b/extract.go index a7d8c61..c455ff1 100644 --- a/extract.go +++ b/extract.go @@ -8,7 +8,7 @@ // // 兩路的產出契約相同:回傳「本次新增/變動的卡片相對路徑」,交 task 6 推 ingest。 // 原文永不出本函式(claude 路整段在用戶機器;gemma 路內容過境 API 但不落任何雲儲存)。 -package main +package collector import ( "context" diff --git a/extract_gemma.go b/extract_gemma.go index 4610832..c07202d 100644 --- a/extract_gemma.go +++ b/extract_gemma.go @@ -6,7 +6,7 @@ // // gemma-4-31b 是思考型模型:parts[0] 常是 thought=true 的思考草稿,真答案在最後一個 // 非 thought 的 text part(agent-memory §7.6 實撞教訓,讀法照抄)。 -package main +package collector import ( "bytes" diff --git a/extract_gemma_test.go b/extract_gemma_test.go index 93ab65a..d0ddbfb 100644 --- a/extract_gemma_test.go +++ b/extract_gemma_test.go @@ -1,5 +1,5 @@ // extract_gemma_test.go — task 4(httptest 替身:驗 prompt 契約/thought 淨化/落卡)。 -package main +package collector import ( "encoding/json" diff --git a/extract_test.go b/extract_test.go index 9fc00a2..6a842bf 100644 --- a/extract_test.go +++ b/extract_test.go @@ -1,5 +1,5 @@ // extract_test.go — task 3 claude 萃取路(用 stub 執行檔驗呼叫契約,不需真 claude)。 -package main +package collector import ( "fmt" diff --git a/extract_workersai.go b/extract_workersai.go index bf7c738..2d92b02 100644 --- a/extract_workersai.go +++ b/extract_workersai.go @@ -24,7 +24,7 @@ // // ⚠️ 隱私邊界不變:送出去的是**已在本機轉成純文字的原稿**,回來的是知識卡; // 原始檔案(docx/pdf/xlsx)仍然不出用戶的電腦。 -package main +package collector import ( "bytes" diff --git a/main.go b/main.go index 3c490eb..a99173c 100644 --- a/main.go +++ b/main.go @@ -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 } } diff --git a/manifest.go b/manifest.go index ccbadff..35f48d5 100644 --- a/manifest.go +++ b/manifest.go @@ -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" diff --git a/manifest_extractedby_test.go b/manifest_extractedby_test.go index b296855..f49e63a 100644 --- a/manifest_extractedby_test.go +++ b/manifest_extractedby_test.go @@ -1,4 +1,4 @@ -package main +package collector import ( "encoding/json" diff --git a/manifest_retry_test.go b/manifest_retry_test.go index 0a6bd67..4ec526b 100644 --- a/manifest_retry_test.go +++ b/manifest_retry_test.go @@ -4,7 +4,7 @@ // 原本 manifest 只記成功不記失敗 ⇒ 每輪重掃都把失敗檔當「新檔」 // ⇒ 實測 1387 輪 × 11 小時撞同一面牆,且壞檔排在前面**拖住整個資料夾**。 // 401 修好了,下次換別的錯照樣卡死——所以要測的是「退避本身」。 -package main +package collector import "testing" diff --git a/probe_workersai.go b/probe_workersai.go index 5bd3cbe..a4594ff 100644 --- a/probe_workersai.go +++ b/probe_workersai.go @@ -19,7 +19,7 @@ // // ⚠️ 這是**被動探測**,不是輪詢:只在同步時(RunDirectOnce 每輪一次)順手打一次 // HEAD-ish 的探測請求,且結果快取在 status.json 給托盤讀。不掛任何排程。 -package main +package collector import ( "bytes" diff --git a/scan.go b/scan.go index 261875e..13d36e1 100644 --- a/scan.go +++ b/scan.go @@ -2,7 +2,7 @@ // 事件順序:先把本輪 removed×added 以 content_hash 配對成 renamed(只更新路徑映射), // 再分類其餘 added/modified/removed;removed 數 > manifest 條目 × 門檻(預設 40%) // → removed 全部不執行、改發警告(R6)。本階段不接網路,事件輸出到 stdout。 -package main +package collector import ( "crypto/sha256" diff --git a/scan_skipped_test.go b/scan_skipped_test.go index b89afc2..056adcb 100644 --- a/scan_skipped_test.go +++ b/scan_skipped_test.go @@ -1,4 +1,4 @@ -package main +package collector // scan_skipped_test.go — G-6.2「不准安靜地略過」(J-1/S6,2026-08-06)。 // diff --git a/scan_test.go b/scan_test.go index 335099e..5eb8d20 100644 --- a/scan_test.go +++ b/scan_test.go @@ -1,4 +1,4 @@ -package main +package collector import ( "crypto/sha256" diff --git a/supervisor/supervisor.go b/supervisor/supervisor.go index 6b1ac6d..50fb4b1 100644 --- a/supervisor/supervisor.go +++ b/supervisor/supervisor.go @@ -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.log;t14) @@ -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...) // t75:Windows 上不要讓 collector 彈出 console 視窗(每輪閃一次,使用者會以為中毒)。 // 非 Windows 平台為 no-op,見 hidewindow_*.go。 hideChildWindow(cmd) diff --git a/sync_status.go b/sync_status.go index 2efa0cd..6ff76fc 100644 --- a/sync_status.go +++ b/sync_status.go @@ -1,6 +1,6 @@ // sync_status.go — 每輪同步後的彙總狀態(t91 狀態可見性)。 // 寫成 JSON 供托盤讀取,讓使用者第一眼看到萃取是否正常。 -package main +package collector import ( "encoding/json" diff --git a/sync_status_test.go b/sync_status_test.go index 5618d99..d17fcd8 100644 --- a/sync_status_test.go +++ b/sync_status_test.go @@ -1,5 +1,5 @@ // sync_status_test.go — t91/t92 狀態檔與 fallback 路徑邏輯單元測試。 -package main +package collector import ( "os" diff --git a/t52_library_test.go b/t52_library_test.go index f3950e5..30f5c7f 100644 --- a/t52_library_test.go +++ b/t52_library_test.go @@ -1,4 +1,4 @@ -package main +package collector import ( "crypto/sha256" diff --git a/template_install.go b/template_install.go index b103268..e05e890 100644 --- a/template_install.go +++ b/template_install.go @@ -6,7 +6,7 @@ // // 冪等鐵律:**已存在的檔案一律不覆寫**(用戶的 status.md/wiki 是他的資產; // 升級 template 版本=另案 update 流程,不在本函式)。 -package main +package collector import ( "embed" diff --git a/template_install_test.go b/template_install_test.go index d02a022..21802f6 100644 --- a/template_install_test.go +++ b/template_install_test.go @@ -1,5 +1,5 @@ // template_install_test.go — daemon-beta task 2(代裝冪等/wiki 產物區不被掃)。 -package main +package collector import ( "os" diff --git a/trigger.go b/trigger.go index 6c48005..8fe1aa6 100644 --- a/trigger.go +++ b/trigger.go @@ -14,7 +14,7 @@ // R2;沒上去就送=消費端 fetch 必 404)——下輪重試補送。renamed/removed 不依賴 R2,照送。 // - 防呆警告輪(mass_delete_guard):removed 事件已被 collector 壓下,但 payload 連同 // warnings 照送(消費端/執行紀錄看得到警告,不執行下架)。 -package main +package collector import ( "bytes" diff --git a/trigger_test.go b/trigger_test.go index 29da073..c3d024e 100644 --- a/trigger_test.go +++ b/trigger_test.go @@ -1,4 +1,4 @@ -package main +package collector import ( "encoding/json" diff --git a/upload.go b/upload.go index f741be7..66f857a 100644 --- a/upload.go +++ b/upload.go @@ -9,7 +9,7 @@ // 冪等:先做存在檢查(GET+Range 1 byte;端點不支援 HEAD,見 Exists 註解), // 已存在=no-op 跳過 PUT(content-addressed 天然去重,design §4)。 // 設定走環境變數 CF_ACCOUNT_ID / CF_API_TOKEN / R2_BUCKET,絕不落 repo。 -package main +package collector import ( "fmt" diff --git a/upload_test.go b/upload_test.go index ab352b8..0d7244c 100644 --- a/upload_test.go +++ b/upload_test.go @@ -1,4 +1,4 @@ -package main +package collector import ( "io"