G-6.2:讀不了的檔案不再安靜消失——首頁當場說出來
服務 J-1 / S6「我丟進去的檔案,查得到」的考題 G-6.2: 「要嘛查得到,要嘛**當場被告知這種檔案還不支援**,不准安靜地略過。」 本次只做後半句(轉檔本體另有人閘,等 leo 裁「那段 Go 住哪裡」)。 ## 真實現況比記載更糟 t16 寫的「PDF 被靜默略過」已不成立(t73 的轉檔層把 pdf/docx/xlsx/csv/pptx 都接上了,實測 2 頁中文 PDF 完整抽出)。**真正還在沉默的是別的東西**: 副檔名不在 allowedExt 的檔案在 scan.go 直接 `return nil`—— 不進事件、不進 manifest、不進 status、不進畫面。 使用者丟一份 .doc 進去,從頭到尾一個字都沒有。 實測基線(真檔):丟 .pdf/.md/.doc/.key/.jpg 進資料夾, `collector scan` 只吐出 pdf 與 md 兩個事件,另外三個檔沒留下任何痕跡。 ## 改了什麼 - scan.go:白名單閘不再是死巷。像文件的(.doc/.xls/.ppt/.pages/.key/ .numbers/.odt/.ods/.odp/.rtf/.epub/.wpd/.msg/.eml)逐檔留名; 其餘(圖片/影音/程式碼)只計總數——**避免 Obsidian 附件庫炸出幾百行噪音**。 - 兩個新欄位標 `json:"-"`:collector-trigger schema 是 additionalProperties:false,且 BuildSendablePayload 是淺拷貝 ⇒ 有 tag 就會漏到雲端被擋。這是給本機使用者看的,不上 wire。 - direct.go → status.json → App 首頁一張卡:講檔名與格式(「舊版報告.doc (舊版 Word)」),並告訴他不用重丟、也給替代路(另存成 PDF/.docx)。 - 刻意不進 manifest、不走 CarryForwardActivity:每輪由檔案系統重算, 不製造 t195 那種「跨輪欄位漏 carry 就靜默歸零」的債。 ## 順手修掉一個會讓驗收失效的回歸 同綑的 arcrun-collector 一路自稱 `dev`:退役 fyne 版 (arcrun-tray/build-mac.sh:24,t150/t72)本來就有版本注入, t194 換 Wails 時沒帶過來,Mac 與 Windows 兩邊都掉。 **幹活的是 collector**——它不報版本,就沒人能判斷修復有沒有到使用者手上。 ## 實測 - go test ./... 135 過 0 敗(新增 10 條:scan 5+首頁文案 5) - check-cis / check-render / check-tray 三閘全過;淺色深色都抓過畫面 - 從 **DMG 裡那支** collector 實跑:Info.plist 0.18.6、 collector 回報 v0.18.6 (build 20260806-0101)、 status.json 吐出 skipped_docs=[簡報.key, 舊版報告.doc]、other=1 ⚠️ 未出貨:推 bundles repo 在 GitHub,要 leo 開 D20 閘。 DMG 已備妥 dist/Arcrun-v0.18.6.dmg。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+103
-11
@@ -83,6 +83,46 @@ type syncStatus struct {
|
||||
LastActivityAt string `json:"last_activity_at,omitempty"`
|
||||
LastActivityOK int `json:"last_activity_ok"`
|
||||
LastActivityFailed int `json:"last_activity_failed"`
|
||||
// G-6.2(2026-08-06):collector 讀不了、因此整個跳過的檔案。
|
||||
// 以前這些檔在 scan 的白名單閘就無聲蒸發,使用者只看到「什麼都沒發生」。
|
||||
SkippedDocs []skippedDoc `json:"skipped_docs,omitempty"`
|
||||
SkippedDocCount int `json:"skipped_doc_count"`
|
||||
SkippedOtherCount int `json:"skipped_other_count"`
|
||||
}
|
||||
|
||||
type skippedDoc struct {
|
||||
Path string `json:"path"`
|
||||
Ext string `json:"ext"`
|
||||
}
|
||||
|
||||
// extLabel 把副檔名翻成使用者認得的東西。
|
||||
// 使用者不會因為看到「.pages」就懂,但看到「Pages」會——那是他自己按存檔時的名字。
|
||||
func extLabel(ext string) string {
|
||||
switch strings.ToLower(ext) {
|
||||
case ".doc":
|
||||
return "舊版 Word"
|
||||
case ".xls":
|
||||
return "舊版 Excel"
|
||||
case ".ppt":
|
||||
return "舊版 PowerPoint"
|
||||
case ".pages":
|
||||
return "Pages"
|
||||
case ".numbers":
|
||||
return "Numbers"
|
||||
case ".key":
|
||||
return "Keynote"
|
||||
case ".odt", ".ods", ".odp":
|
||||
return "OpenDocument"
|
||||
case ".rtf":
|
||||
return "RTF"
|
||||
case ".epub":
|
||||
return "EPUB"
|
||||
case ".msg", ".eml":
|
||||
return "郵件檔"
|
||||
case ".wpd":
|
||||
return "WordPerfect"
|
||||
}
|
||||
return strings.TrimPrefix(ext, ".")
|
||||
}
|
||||
|
||||
// loadCfg 同時保留原始 map ⇒ 回寫時**不會弄丟我們沒宣告的欄位**
|
||||
@@ -135,20 +175,70 @@ type UIAccount struct {
|
||||
Folders []UIFolder `json:"folders"`
|
||||
}
|
||||
type UIState struct {
|
||||
Version string `json:"version"`
|
||||
StatusBig string `json:"statusBig"`
|
||||
StatusSub string `json:"statusSub"`
|
||||
Syncing bool `json:"syncing"`
|
||||
Accounts []UIAccount `json:"accounts"`
|
||||
Engine string `json:"engine"` // "workers-ai" | "gemma"
|
||||
GeminiKey string `json:"geminiKey"` // 只回遮罩,不回真值
|
||||
ExtractedOK int `json:"extractedOK"` // 首頁「已整理幾份」
|
||||
Steps []Step `json:"steps"` // 首頁狀態時間軸(leo #6)
|
||||
Version string `json:"version"`
|
||||
StatusBig string `json:"statusBig"`
|
||||
StatusSub string `json:"statusSub"`
|
||||
Syncing bool `json:"syncing"`
|
||||
Accounts []UIAccount `json:"accounts"`
|
||||
Engine string `json:"engine"` // "workers-ai" | "gemma"
|
||||
GeminiKey string `json:"geminiKey"` // 只回遮罩,不回真值
|
||||
ExtractedOK int `json:"extractedOK"` // 首頁「已整理幾份」
|
||||
Steps []Step `json:"steps"` // 首頁狀態時間軸(leo #6)
|
||||
Skipped *UISkipped `json:"skipped"` // 讀不了的檔(沒有就是 null,前端不畫)
|
||||
}
|
||||
|
||||
// UISkipped=首頁那張「這些檔案現在還處理不了」的卡。
|
||||
//
|
||||
// 🔴 存在理由(J-1/S6 考題 G-6.2):
|
||||
//
|
||||
// 「Given 我丟進去的是 PDF 或 Word/When 我搜它的內容/
|
||||
// Then 我一樣找得到——**或當場被告知這種檔案還不支援**,不准安靜地略過。」
|
||||
//
|
||||
// 後半句在這裡兌現。文字一律白話:講「你的哪個檔沒進去」「你要不要做什麼」,
|
||||
// 不講 allowedExt、副檔名白名單、extractor 這些系統內部詞。
|
||||
type UISkipped struct {
|
||||
Title string `json:"title"` // 「有 3 個檔案現在還讀不了」
|
||||
Note string `json:"note"` // 該不該做什麼——這裡的答案是「不用,之後會自動補上」
|
||||
Files []string `json:"files"` // 「舊版報告.doc(舊版 Word)」
|
||||
More int `json:"more"` // 沒列出來的還有幾個
|
||||
Other string `json:"other"` // 非文件檔的一行說明(沒有就空字串)
|
||||
}
|
||||
|
||||
// buildSkipped 把 status.json 的三個欄位翻成首頁看得懂的一張卡。
|
||||
// 完全沒有東西被略過時回 nil ⇒ 前端不畫這張卡(沒事就別佔畫面)。
|
||||
func buildSkipped(s syncStatus) *UISkipped {
|
||||
if s.SkippedDocCount == 0 && s.SkippedOtherCount == 0 {
|
||||
return nil
|
||||
}
|
||||
u := &UISkipped{}
|
||||
for _, d := range s.SkippedDocs {
|
||||
u.Files = append(u.Files, fmt.Sprintf("%s(%s)", filepath.Base(d.Path), extLabel(d.Ext)))
|
||||
}
|
||||
if n := s.SkippedDocCount - len(s.SkippedDocs); n > 0 {
|
||||
u.More = n
|
||||
}
|
||||
if s.SkippedDocCount > 0 {
|
||||
u.Title = fmt.Sprintf("有 %d 個檔案現在還讀不了", s.SkippedDocCount)
|
||||
// 誠實地告訴他「這不是你的錯,也不用你動手」——否則使用者會反覆重丟同一個檔。
|
||||
u.Note = "這些格式我們還沒支援,所以沒有進你的知識庫。等支援了會自動補上,你不用重丟。" +
|
||||
"急著要的話,先用原本的軟體另存成 PDF 或 Word(.docx)放進同一個資料夾就行。"
|
||||
} else {
|
||||
// 只有非文件檔的情況(例如整個資料夾都是照片)——不需要驚動他,但也不能不說。
|
||||
u.Title = "有些檔案沒有被整理"
|
||||
u.Note = "看起來不是文件,所以跳過了。"
|
||||
}
|
||||
if s.SkippedOtherCount > 0 {
|
||||
u.Other = fmt.Sprintf("另外有 %d 個不是文件的檔案(圖片、影片、壓縮檔之類)也沒有處理。",
|
||||
s.SkippedOtherCount)
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
// Step 是首頁狀態時間軸的一格。
|
||||
// 🔴 leo 08-04:「首頁要顯示的應該是 status,如果**看守、發現變化、萃取、上傳…
|
||||
// 不同 status 在哪裡顯示**?」
|
||||
//
|
||||
// 不同 status 在哪裡顯示**?」
|
||||
//
|
||||
// ⇒ 把一輪同步拆成四步,讓使用者看得到「現在走到哪」,而不是只有一句「看守中」。
|
||||
type Step struct {
|
||||
Title string `json:"title"`
|
||||
@@ -226,6 +316,7 @@ func (a *App) GetState() UIState {
|
||||
st.ExtractedOK = sync.ExtractedOK
|
||||
st.Syncing, st.StatusBig, st.StatusSub = describeStatus(sync)
|
||||
st.Steps = buildSteps(sync, st.Syncing)
|
||||
st.Skipped = buildSkipped(sync)
|
||||
return st
|
||||
}
|
||||
|
||||
@@ -427,7 +518,8 @@ func (a *App) ShowWindow() {
|
||||
// Quit 真的結束程式(=停止看守)。只有托盤右鍵那一項會呼叫。
|
||||
//
|
||||
// 🔴 leo 實測④:「**用強制結束把它關掉才能測試**」——代表沒有一條正常的結束路徑。
|
||||
// 這裡先停掉 collector 子行程再關 App,否則子行程會變孤兒繼續跑。
|
||||
//
|
||||
// 這裡先停掉 collector 子行程再關 App,否則子行程會變孤兒繼續跑。
|
||||
func (a *App) Quit() {
|
||||
stopSupervisor()
|
||||
runtime.Quit(a.ctx)
|
||||
|
||||
Reference in New Issue
Block a user