首頁「沒被整理的檔案」修兩病:同一件事講兩次/不說是哪一個檔

leo 08-06 封測回報(附截圖+「他放一個 md 檔無法通過」)。

## ① 同一件事講兩次,而且「另外」前面沒有東西
截圖實況:
  「看起來不是文件,所以跳過了。」   ← 底下是空的(非文件檔不逐檔點名)
  「**另外**有 1 個不是文件的檔案(圖片、影片、壓縮檔之類)也沒有處理。」
真兇:app.go buildSkipped 的 else 分支先寫一句通用說明,
接著無條件再寫 Other 那句——後者的「另外」是為「兩種都有」寫的,
在「只有非文件檔」時就變成前面沒有東西可以「另外」。
解:該分支只講一句、自己帶數量,且不留 Other。「另外」只在兩種都有時出現。

## ② 只報總數=等於沒說(這才是 md 那題卡住的原因)
封測者放 .md 說「無法通過」,但畫面只寫「有 1 個不是文件的檔案」,
**沒說是哪一個** ⇒ 誰也判斷不出發生什麼事。
而 `.md` 明明在 allowedExt 白名單裡(scan.go:26),我在本機實測也一次就過:
    "path":"arcrun-md-test.md","status":"ingested","http_status":200
⇒ 那個「1 個」必然不是他以為的那個檔(副檔名被 Windows 藏起來、存成別的格式…),
  但沒有檔名就永遠查不出來。
解:scan 收集非文件檔的檔名(上限 5 個),一路帶到 status.json 與首頁。
「只報總數」在幾百張圖時是對的,在 1 個時是失職——少量就點名。

## 驗
· 兩支新測試釘住規則:只有非文件檔時不准出現第二句、不准出現「另外」;
  兩種都有時「另外」才成立並帶數量
· 實際文案打出來看過(見下),不是只看測試綠
    有 1 個檔案沒有被整理
    看起來不是文件(圖片、影片、壓縮檔之類),所以跳過了。這是正常的,你不用做什麼。
      · 我的筆記.md.txt
· 312 張圖的情況:列 5 個 +「…還有 307 個」,不洗版
· collector 全測試過、app 測試過、go vet 全綠
This commit is contained in:
2026-08-06 16:12:01 +08:00
parent 4d3a6a09a6
commit 67effe3b7f
5 changed files with 106 additions and 33 deletions
+22 -14
View File
@@ -67,22 +67,22 @@ type DirectConfig struct {
InstanceName string `json:"instance_name,omitempty"` // 暱稱(舊制)
Library string `json:"library"` // 藏書地圖歸庫鍵(空=kb;per-folder 未指定時的後備)
// t52:每個看守資料夾對應自己的庫(key=絕對路徑,value=庫名);新制走 AccountConfig.Libraries。
Libraries map[string]string `json:"libraries,omitempty"`
IngestWF string `json:"ingest_workflow"` // 直送萃取 workflow 名(空=rag_ingest_direct
RemovedWF string `json:"removed_workflow"` // 下架 workflow 名(空=rag_takedown_direct
Libraries map[string]string `json:"libraries,omitempty"`
IngestWF string `json:"ingest_workflow"` // 直送萃取 workflow 名(空=rag_ingest_direct
RemovedWF string `json:"removed_workflow"` // 下架 workflow 名(空=rag_takedown_direct
// —— 四步定稿(daemon-beta t3/t4/t6):本地萃卡模式 ——
Extractor string `json:"extractor,omitempty"` // "workers-ai"(預設,免金鑰)|"gemma""claude"(已停用)
Extractor string `json:"extractor,omitempty"` // "workers-ai"(預設,免金鑰)|"gemma""claude"(已停用)
// t181:使用者**主動在托盤選過**萃取引擎才為 true。false=一律走 workers-ai。
// 判準刻意不是「有沒有金鑰」——leo 08-04:「不管你現在是否有填金鑰」都要先 default
// Workers AI,否則他得「花在解釋為什麼 Gemini 不管用上」。
// 有金鑰但沒主動選 ⇒ 金鑰留著不動,之後選 Gemini 立刻可用。
ExtractorExplicit bool `json:"extractor_explicit,omitempty"`
ClaudeBin string `json:"claude_bin,omitempty"` // claude 執行檔(空=PATH 找 claude
GeminiAPIKey string `json:"gemini_api_key,omitempty"` // gemma 路的用戶 key
LLMModel string `json:"llm_model,omitempty"` // gemma 路模型(空=gemma-4-31b-it
CardIngestWF string `json:"card_ingest_workflow,omitempty"` // 收卡 workflow(空=rag_ingest_card
PollSec int `json:"poll_interval_sec"` // 輪詢間隔秒(空/05
MaxRemoved float64 `json:"max_removed_ratio"` // 大量刪除防呆門檻(空/0=0.4)
ExtractorExplicit bool `json:"extractor_explicit,omitempty"`
ClaudeBin string `json:"claude_bin,omitempty"` // claude 執行檔(空=PATH 找 claude
GeminiAPIKey string `json:"gemini_api_key,omitempty"` // gemma 路的用戶 key
LLMModel string `json:"llm_model,omitempty"` // gemma 路模型(空=gemma-4-31b-it
CardIngestWF string `json:"card_ingest_workflow,omitempty"` // 收卡 workflow(空=rag_ingest_card
PollSec int `json:"poll_interval_sec"` // 輪詢間隔秒(空/05
MaxRemoved float64 `json:"max_removed_ratio"` // 大量刪除防呆門檻(空/0=0.4)
// ForceSync=這一輪是使用者按「立刻同步」觸發的(t195)。
// 為真時忽略失敗退避與次數上限,一律重送——**人明確要求時不該被機器的退避擋住**。
@@ -430,8 +430,8 @@ func pageNameOf(relPath string) string {
// DirectResult 是單一事件的直送結果(隨每輪 log 輸出)。
type DirectResult struct {
Account string `json:"account,omitempty"` // t104: cypher_url host(多帳號標的)
Root string `json:"root,omitempty"` // 多資料夾時標明事件屬於哪個根
Account string `json:"account,omitempty"` // t104: cypher_url host(多帳號標的)
Root string `json:"root,omitempty"` // 多資料夾時標明事件屬於哪個根
Type string `json:"type"`
Path string `json:"path"`
Status string `json:"status"` // ingested | removed | planned | failed | skipped
@@ -553,6 +553,7 @@ func RunDirectOnce(cfg *DirectConfig, dryRun bool) ([]DirectResult, int, *Trigge
// 使用者不該因為我們的內部結構而看到同一個檔名列兩次。
skippedSeen := map[string]SkippedFile{}
skippedOther := 0
var skippedOtherNames []string
accountDetails := map[string]AccountSyncStatus{}
for _, acc := range accounts {
@@ -618,6 +619,8 @@ func RunDirectOnce(cfg *DirectConfig, dryRun bool) ([]DirectResult, int, *Trigge
skippedSeen[sf.Path] = sf
}
skippedOther += p.SkippedOther
// 同理,每一根的檔名都要收(上限在寫進 status 時才裁)。
skippedOtherNames = append(skippedOtherNames, p.SkippedOtherNames...)
}
}
accountDetails[accHost] = accSt
@@ -634,6 +637,12 @@ func RunDirectOnce(cfg *DirectConfig, dryRun bool) ([]DirectResult, int, *Trigge
// G-6.2:把「讀不了的檔」寫進狀態檔,App 首頁才有東西可以講。
// 排序=畫面每輪穩定(map 迭代順序隨機,不排的話清單會自己跳動)。
st.SkippedOtherCount = skippedOther
// 少量時點名(maxOtherNames 個以內)——leo 08-06 封測:只報「1 個」等於沒說。
sort.Strings(skippedOtherNames)
if len(skippedOtherNames) > maxOtherNames {
skippedOtherNames = skippedOtherNames[:maxOtherNames]
}
st.SkippedOtherNames = skippedOtherNames
st.SkippedDocCount = len(skippedSeen)
for _, sf := range skippedSeen {
st.SkippedDocs = append(st.SkippedDocs, sf)
@@ -994,7 +1003,6 @@ func runDirect(args []string) int {
return exit
}
// 四步定稿第 1 步:daemon 代裝 template——常駐看守前確保每根都鋪好(冪等,不覆寫既有檔)。
// dry-run/--once 測試情境不代裝(不留副作用),由 template-install 子命令顯式做。
if !*once && !*dryRun {