Files
arcrun-collector/convert_docx.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

83 lines
2.5 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.
// convert_docx.go — .docx 抽文字(純 Go 標準庫,零外部依賴)
//
// 為什麼不需要外部引擎(2026-07-27 POC 實測後定案):
//
// .docx/.xlsx/.pptx Office Open XML **本質是 ZIP,裡面是 XML**
// 文字明明白白寫在 <w:t> 標籤裡。Go 標準庫 archive/zip + encoding/xml 直接讀得到。
// 實測體積代價 **+0.31 MB**(對照 PDF 走 PDFium 是 +10.43 MB,差 33 倍)。
//
// 對比 PDF 為何非要引擎不可:PDF 是**印刷格式**,記的是「這個字畫在第 3 頁座標 (120,400)」,
// 要把散落字元還原成通順文字=需要整顆排版還原引擎。兩者不是同一個難度等級。
package collector
import (
"archive/zip"
"bytes"
"encoding/xml"
"fmt"
"io"
"strings"
)
// docx 內文的位置。註:頁首/頁尾/註腳另有 header*.xml/footnotes.xml
// 目前只取主文——知識萃取要的是內容,頁首頁尾多半是雜訊(頁碼、公司名)。
const docxMainPart = "word/document.xml"
func extractDocx(data []byte) (string, error) {
zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
// .docx 不是 zip = 檔案壞了,或副檔名騙人(例如舊版 .doc 改名成 .docx)。
// 舊版 .doc 是完全不同的二進位格式(OLE),本抽取器讀不了。
return "", fmt.Errorf("docx 解壓失敗(可能是舊版 .doc 或檔案損壞):%w", err)
}
for _, f := range zr.File {
if f.Name != docxMainPart {
continue
}
rc, err := f.Open()
if err != nil {
return "", fmt.Errorf("讀 %s 失敗:%w", docxMainPart, err)
}
defer rc.Close()
return docxXMLText(rc)
}
return "", fmt.Errorf("docx 缺少 %s", docxMainPart)
}
// docxXMLText 串流解 XML 取文字。
//
// 只認三個標籤就夠:
// - w:p 段落 → 換行
// - w:tab 定位點 → tab
// - w:br 斷行 → 換行
//
// 其餘一律當文字節點收集。用 Local name 比對(不看 namespace 前綴),
// 因為不同產生器的前綴可能不同(w: / w14: 等)。
func docxXMLText(r io.Reader) (string, error) {
dec := xml.NewDecoder(r)
var sb strings.Builder
for {
tok, err := dec.Token()
if err == io.EOF {
break
}
if err != nil {
return "", fmt.Errorf("解析 document.xml 失敗:%w", err)
}
switch t := tok.(type) {
case xml.StartElement:
switch t.Name.Local {
case "p":
sb.WriteString("\n")
case "tab":
sb.WriteString("\t")
case "br":
sb.WriteString("\n")
}
case xml.CharData:
sb.Write(t)
}
}
return sb.String(), nil
}