首頁「沒被整理的檔案」修兩病:同一件事講兩次/不說是哪一個檔
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:
+21
-5
@@ -88,6 +88,7 @@ type syncStatus struct {
|
|||||||
SkippedDocs []skippedDoc `json:"skipped_docs,omitempty"`
|
SkippedDocs []skippedDoc `json:"skipped_docs,omitempty"`
|
||||||
SkippedDocCount int `json:"skipped_doc_count"`
|
SkippedDocCount int `json:"skipped_doc_count"`
|
||||||
SkippedOtherCount int `json:"skipped_other_count"`
|
SkippedOtherCount int `json:"skipped_other_count"`
|
||||||
|
SkippedOtherNames []string `json:"skipped_other_names"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type skippedDoc struct {
|
type skippedDoc struct {
|
||||||
@@ -222,15 +223,30 @@ func buildSkipped(s syncStatus) *UISkipped {
|
|||||||
// 誠實地告訴他「這不是你的錯,也不用你動手」——否則使用者會反覆重丟同一個檔。
|
// 誠實地告訴他「這不是你的錯,也不用你動手」——否則使用者會反覆重丟同一個檔。
|
||||||
u.Note = "這些格式我們還沒支援,所以沒有進你的知識庫。等支援了會自動補上,你不用重丟。" +
|
u.Note = "這些格式我們還沒支援,所以沒有進你的知識庫。等支援了會自動補上,你不用重丟。" +
|
||||||
"急著要的話,先用原本的軟體另存成 PDF 或 Word(.docx)放進同一個資料夾就行。"
|
"急著要的話,先用原本的軟體另存成 PDF 或 Word(.docx)放進同一個資料夾就行。"
|
||||||
} else {
|
// 兩種都有時,非文件檔那句用「另外」接在讀不了的檔之後(見下)。
|
||||||
// 只有非文件檔的情況(例如整個資料夾都是照片)——不需要驚動他,但也不能不說。
|
|
||||||
u.Title = "有些檔案沒有被整理"
|
|
||||||
u.Note = "看起來不是文件,所以跳過了。"
|
|
||||||
}
|
|
||||||
if s.SkippedOtherCount > 0 {
|
if s.SkippedOtherCount > 0 {
|
||||||
u.Other = fmt.Sprintf("另外有 %d 個不是文件的檔案(圖片、影片、壓縮檔之類)也沒有處理。",
|
u.Other = fmt.Sprintf("另外有 %d 個不是文件的檔案(圖片、影片、壓縮檔之類)也沒有處理。",
|
||||||
s.SkippedOtherCount)
|
s.SkippedOtherCount)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 只有非文件檔的情況(例如整個資料夾都是照片)——不需要驚動他,但也不能不說。
|
||||||
|
//
|
||||||
|
// 🔴 2026-08-06 leo 封測截圖:這張卡長成
|
||||||
|
// 「看起來不是文件,所以跳過了。」(底下空的)
|
||||||
|
// 「**另外**有 1 個不是文件的檔案(圖片、影片、壓縮檔之類)也沒有處理。」
|
||||||
|
// 兩個病:① 同一件事講兩次 ② 「另外」前面沒有東西可以「另外」——
|
||||||
|
// 因為 Files 是空的(非文件檔不逐檔點名),通用句下面什麼都沒有。
|
||||||
|
// ⇒ 這個分支**只講一句**,把數量與例子併進來,且不留 Other。
|
||||||
|
u.Title = fmt.Sprintf("有 %d 個檔案沒有被整理", s.SkippedOtherCount)
|
||||||
|
u.Note = "看起來不是文件(圖片、影片、壓縮檔之類),所以跳過了。這是正常的,你不用做什麼。"
|
||||||
|
// 🔴 少量時把檔名列出來(leo 08-06 封測):封測者放了 .md 進去說「無法通過」,
|
||||||
|
// 而畫面只寫「有 1 個不是文件的檔案」——沒說是哪一個,誰都判斷不出發生什麼事。
|
||||||
|
// `.md` 明明在支援清單裡 ⇒ 看到檔名才知道真相(副檔名被 Windows 藏起來、存錯格式…)。
|
||||||
|
u.Files = append(u.Files, s.SkippedOtherNames...)
|
||||||
|
if n := s.SkippedOtherCount - len(s.SkippedOtherNames); n > 0 {
|
||||||
|
u.More = n
|
||||||
|
}
|
||||||
|
}
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ func TestBuildSkippedShowsRemainderCount(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 整個資料夾都是照片這種情況:不必驚動他,但也不能一個字都不說。
|
// 整個資料夾都是照片這種情況:不必驚動他,但也不能一個字都不說。
|
||||||
|
//
|
||||||
|
// 🔴 2026-08-06 leo 封測截圖抓到的病:這張卡把同一件事講了兩次——
|
||||||
|
// 先一句通用的「看起來不是文件,所以跳過了。」(底下是空的,因為非文件檔不點名),
|
||||||
|
// 再一句「**另外**有 1 個…也沒有處理。」而「另外」前面根本沒有東西。
|
||||||
|
// ⇒ 只有非文件檔時**只准講一句**,而且那一句要自己帶數量。
|
||||||
func TestBuildSkippedOtherOnly(t *testing.T) {
|
func TestBuildSkippedOtherOnly(t *testing.T) {
|
||||||
u := buildSkipped(syncStatus{SkippedOtherCount: 312})
|
u := buildSkipped(syncStatus{SkippedOtherCount: 312})
|
||||||
if u == nil {
|
if u == nil {
|
||||||
@@ -76,8 +81,29 @@ func TestBuildSkippedOtherOnly(t *testing.T) {
|
|||||||
if len(u.Files) != 0 {
|
if len(u.Files) != 0 {
|
||||||
t.Error("非文件檔不逐檔點名(幾百張圖會變成噪音)")
|
t.Error("非文件檔不逐檔點名(幾百張圖會變成噪音)")
|
||||||
}
|
}
|
||||||
if !strings.Contains(u.Other, "312") {
|
if !strings.Contains(u.Title, "312") {
|
||||||
t.Errorf("要講出數量,實得 %q", u.Other)
|
t.Errorf("只有非文件檔時,數量要出現在標題(那是唯一會被看到的一句),實得 %q", u.Title)
|
||||||
|
}
|
||||||
|
if u.Other != "" {
|
||||||
|
t.Errorf("同一件事不准講兩次——沒有讀不了的文件時不該再出現一句,實得 %q", u.Other)
|
||||||
|
}
|
||||||
|
if strings.Contains(u.Title+u.Note, "另外") {
|
||||||
|
t.Errorf("前面沒有東西可以「另外」,實得 %q / %q", u.Title, u.Note)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兩種都有時,「另外」才成立——它接在「讀不了的檔」那段之後。
|
||||||
|
func TestBuildSkippedBothKinds(t *testing.T) {
|
||||||
|
u := buildSkipped(syncStatus{
|
||||||
|
SkippedDocs: []skippedDoc{{Path: "舊報告.doc", Ext: ".doc"}},
|
||||||
|
SkippedDocCount: 1,
|
||||||
|
SkippedOtherCount: 3,
|
||||||
|
})
|
||||||
|
if !strings.Contains(u.Title, "讀不了") {
|
||||||
|
t.Errorf("有讀不了的文件時,標題要講那件事,實得 %q", u.Title)
|
||||||
|
}
|
||||||
|
if !strings.Contains(u.Other, "另外") || !strings.Contains(u.Other, "3") {
|
||||||
|
t.Errorf("兩種都有時才用「另外」並帶數量,實得 %q", u.Other)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -553,6 +553,7 @@ func RunDirectOnce(cfg *DirectConfig, dryRun bool) ([]DirectResult, int, *Trigge
|
|||||||
// 使用者不該因為我們的內部結構而看到同一個檔名列兩次。
|
// 使用者不該因為我們的內部結構而看到同一個檔名列兩次。
|
||||||
skippedSeen := map[string]SkippedFile{}
|
skippedSeen := map[string]SkippedFile{}
|
||||||
skippedOther := 0
|
skippedOther := 0
|
||||||
|
var skippedOtherNames []string
|
||||||
|
|
||||||
accountDetails := map[string]AccountSyncStatus{}
|
accountDetails := map[string]AccountSyncStatus{}
|
||||||
for _, acc := range accounts {
|
for _, acc := range accounts {
|
||||||
@@ -618,6 +619,8 @@ func RunDirectOnce(cfg *DirectConfig, dryRun bool) ([]DirectResult, int, *Trigge
|
|||||||
skippedSeen[sf.Path] = sf
|
skippedSeen[sf.Path] = sf
|
||||||
}
|
}
|
||||||
skippedOther += p.SkippedOther
|
skippedOther += p.SkippedOther
|
||||||
|
// 同理,每一根的檔名都要收(上限在寫進 status 時才裁)。
|
||||||
|
skippedOtherNames = append(skippedOtherNames, p.SkippedOtherNames...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
accountDetails[accHost] = accSt
|
accountDetails[accHost] = accSt
|
||||||
@@ -634,6 +637,12 @@ func RunDirectOnce(cfg *DirectConfig, dryRun bool) ([]DirectResult, int, *Trigge
|
|||||||
// G-6.2:把「讀不了的檔」寫進狀態檔,App 首頁才有東西可以講。
|
// G-6.2:把「讀不了的檔」寫進狀態檔,App 首頁才有東西可以講。
|
||||||
// 排序=畫面每輪穩定(map 迭代順序隨機,不排的話清單會自己跳動)。
|
// 排序=畫面每輪穩定(map 迭代順序隨機,不排的話清單會自己跳動)。
|
||||||
st.SkippedOtherCount = skippedOther
|
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)
|
st.SkippedDocCount = len(skippedSeen)
|
||||||
for _, sf := range skippedSeen {
|
for _, sf := range skippedSeen {
|
||||||
st.SkippedDocs = append(st.SkippedDocs, sf)
|
st.SkippedDocs = append(st.SkippedDocs, sf)
|
||||||
@@ -994,7 +1003,6 @@ func runDirect(args []string) int {
|
|||||||
return exit
|
return exit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 四步定稿第 1 步:daemon 代裝 template——常駐看守前確保每根都鋪好(冪等,不覆寫既有檔)。
|
// 四步定稿第 1 步:daemon 代裝 template——常駐看守前確保每根都鋪好(冪等,不覆寫既有檔)。
|
||||||
// dry-run/--once 測試情境不代裝(不留副作用),由 template-install 子命令顯式做。
|
// dry-run/--once 測試情境不代裝(不留副作用),由 template-install 子命令顯式做。
|
||||||
if !*once && !*dryRun {
|
if !*once && !*dryRun {
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ var allowedExt = map[string]bool{
|
|||||||
//
|
//
|
||||||
// 加新格式的順序:先列在這裡(使用者立刻看得到「還不支援」),
|
// 加新格式的順序:先列在這裡(使用者立刻看得到「還不支援」),
|
||||||
// 等 convert.go 真的接上抽取器,再把它從這裡搬去 allowedExt。
|
// 等 convert.go 真的接上抽取器,再把它從這裡搬去 allowedExt。
|
||||||
|
// maxOtherNames=非文件檔最多點名幾個。超過就只報總數(避免幾百張圖洗版)。
|
||||||
|
const maxOtherNames = 5
|
||||||
|
|
||||||
var docLikeExt = map[string]bool{
|
var docLikeExt = map[string]bool{
|
||||||
// 舊版 Office(OLE2 二進位,與 .docx/.xlsx/.pptx 是完全不同的格式)
|
// 舊版 Office(OLE2 二進位,與 .docx/.xlsx/.pptx 是完全不同的格式)
|
||||||
".doc": true, ".xls": true, ".ppt": true,
|
".doc": true, ".xls": true, ".ppt": true,
|
||||||
@@ -63,6 +66,7 @@ var docLikeExt = map[string]bool{
|
|||||||
//
|
//
|
||||||
// ⚠️ 刻意**不寫進 manifest**:它每輪由檔案系統重算,永遠反映現況。
|
// ⚠️ 刻意**不寫進 manifest**:它每輪由檔案系統重算,永遠反映現況。
|
||||||
// (對照 t195 的坑:凡是存進 ManifestEntry 的跨輪欄位都得記得在 carry 段補一行,
|
// (對照 t195 的坑:凡是存進 ManifestEntry 的跨輪欄位都得記得在 carry 段補一行,
|
||||||
|
//
|
||||||
// 漏了就靜默歸零。這裡不建立那份債。)
|
// 漏了就靜默歸零。這裡不建立那份債。)
|
||||||
type SkippedFile struct {
|
type SkippedFile struct {
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
@@ -102,7 +106,14 @@ type TriggerPayload struct {
|
|||||||
//(BuildSendablePayload 是 `sendable := *p` 淺拷貝,有 tag 就會一起送出去)。
|
//(BuildSendablePayload 是 `sendable := *p` 淺拷貝,有 tag 就會一起送出去)。
|
||||||
// ⇒ 留在記憶體裡,由 direct.go 收進 status.json,給 App 首頁用。
|
// ⇒ 留在記憶體裡,由 direct.go 收進 status.json,給 App 首頁用。
|
||||||
Skipped []SkippedFile `json:"-"` // 像文件、但還讀不了的(逐檔點名)
|
Skipped []SkippedFile `json:"-"` // 像文件、但還讀不了的(逐檔點名)
|
||||||
SkippedOther int `json:"-"` // 其餘非文件檔(圖片/影音/程式碼…)只計數
|
SkippedOther int `json:"-"` // 其餘非文件檔(圖片/影音/程式碼…)總數
|
||||||
|
// SkippedOtherNames=上面那些檔的檔名,**最多 maxOtherNames 個**。
|
||||||
|
// 🔴 2026-08-06(leo 封測):封測者放了一個 .md 進去說「無法通過」,而畫面只寫
|
||||||
|
// 「有 1 個不是文件的檔案」——**沒說是哪一個**,於是誰也判斷不出發生什麼事
|
||||||
|
// (.md 明明在白名單裡,所以那個 1 一定是別的東西:可能副檔名被 Windows 藏起來、
|
||||||
|
// 可能存成了別的格式)。只報總數在「幾百張圖」時是對的,在「1 個」時等於沒說。
|
||||||
|
// ⇒ 少量時就點名,讓使用者自己一眼看出「喔,我存錯格式了」。
|
||||||
|
SkippedOtherNames []string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScanOptions struct {
|
type ScanOptions struct {
|
||||||
@@ -154,6 +165,7 @@ func Scan(root string, m *Manifest, opts ScanOptions) (*TriggerPayload, error) {
|
|||||||
current := map[string]fileState{}
|
current := map[string]fileState{}
|
||||||
var skipped []SkippedFile
|
var skipped []SkippedFile
|
||||||
skippedOther := 0
|
skippedOther := 0
|
||||||
|
var skippedOtherNames []string
|
||||||
err := filepath.WalkDir(root, func(p string, d fs.DirEntry, werr error) error {
|
err := filepath.WalkDir(root, func(p string, d fs.DirEntry, werr error) error {
|
||||||
if werr != nil {
|
if werr != nil {
|
||||||
return werr
|
return werr
|
||||||
@@ -184,6 +196,12 @@ func Scan(root string, m *Manifest, opts ScanOptions) (*TriggerPayload, error) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
skippedOther++
|
skippedOther++
|
||||||
|
// 只留前幾個:多了就變噪音(Obsidian 附件庫可能有幾百張 .png)。
|
||||||
|
if len(skippedOtherNames) < maxOtherNames {
|
||||||
|
if rel, rerr := filepath.Rel(root, p); rerr == nil {
|
||||||
|
skippedOtherNames = append(skippedOtherNames, filepath.ToSlash(rel))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -365,5 +383,6 @@ func Scan(root string, m *Manifest, opts ScanOptions) (*TriggerPayload, error) {
|
|||||||
Warnings: warnings,
|
Warnings: warnings,
|
||||||
Skipped: skipped,
|
Skipped: skipped,
|
||||||
SkippedOther: skippedOther,
|
SkippedOther: skippedOther,
|
||||||
|
SkippedOtherNames: skippedOtherNames,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -57,7 +57,11 @@ type SyncStatus struct {
|
|||||||
// 檔案還躺在資料夾裡,每輪都會被重新數到,所以原地重算就是對的。
|
// 檔案還躺在資料夾裡,每輪都會被重新數到,所以原地重算就是對的。
|
||||||
SkippedDocs []SkippedFile `json:"skipped_docs,omitempty"` // 逐檔點名(已排序,上限 MaxSkippedListed)
|
SkippedDocs []SkippedFile `json:"skipped_docs,omitempty"` // 逐檔點名(已排序,上限 MaxSkippedListed)
|
||||||
SkippedDocCount int `json:"skipped_doc_count"` // 文件類被略過的**總數**(可能大於清單長度)
|
SkippedDocCount int `json:"skipped_doc_count"` // 文件類被略過的**總數**(可能大於清單長度)
|
||||||
SkippedOtherCount int `json:"skipped_other_count"` // 其餘非文件檔(圖片/影音/程式碼…)只給總數
|
SkippedOtherCount int `json:"skipped_other_count"` // 其餘非文件檔(圖片/影音/程式碼…)總數
|
||||||
|
// 少量時附上檔名(上限 maxOtherNames)。只報總數在「1 個」時等於沒說——
|
||||||
|
// leo 08-06 封測者放了 .md 說「無法通過」,畫面只有「有 1 個不是文件的檔案」,
|
||||||
|
// 沒人判斷得出那到底是什麼檔。
|
||||||
|
SkippedOtherNames []string `json:"skipped_other_names,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// MaxSkippedListed:status.json 裡最多逐檔列幾個。
|
// MaxSkippedListed:status.json 裡最多逐檔列幾個。
|
||||||
|
|||||||
Reference in New Issue
Block a user