修首頁狀態說謊+DMG 補上拖曳版面(leo 08-05 封測回報 ①②)

## ② 首頁狀態是壞的
leo:「拖新的檔案進資料夾,完成本地萃取、上傳,但自始至終 daemon 的首頁都顯示
『有變動就自動開始』『等待中』,都顯示綠燈沒動,實際上已經做完了。」

兩個獨立的真兇:

**A. 「同步中」判斷用錯依據(回歸)**
t191 早就做好機制:collector 開工印 phase:"start"、跑完印 "done",
supervisor 據此維護 StateSyncing。**換 Wails 時 App 沒接這條線**,改看 sync-now 訊號檔:
  ① 訊號檔只有手動按「立刻同步」才產生 ⇒ 拖檔進資料夾(自動觸發)整輪不亮燈
  ② 就算手動按,collector 是**先刪檔再跑**(consumeSyncNowSignal)⇒ 真正在跑時檔早沒了
⇒ 改讀 sup.Status().State == StateSyncing。**接回既有機制,不發明第三種判斷法。**

**B. 做完的證據被下一輪抹掉**
ExtractedOK/ExtractFailed 是**本輪**計數、每輪覆寫整份 status.json
⇒ 有產出那輪寫下 N,十幾秒後空轉的一輪把它蓋成 0
⇒ 「上一輪 N 份」永遠空白、四步時間軸的綠燈只靠「跑過任何一輪」判斷=與實際進度脫鉤。
⇒ 新增 last_activity_{at,ok,failed}:有產出記本輪、空轉沿用上一輪(CarryForwardActivity)。
   首頁改顯示「幾點整理了幾份」,綠燈只在真的整理過東西時才亮。

## ① Mac DMG 少了「看得出要拖」的版面
leo:「要跳出虛擬隨身碟,打開一個 finder 的視窗,**顯示 Arcrun 和 Application 的捷徑**,
用戶把 App 拖進 Application」。
原本 DMG 內容是對的(Arcrun.app+Applications 捷徑),但**沒設版面**
⇒ 預設清單視圖、位置隨機,看不出「要往右拖」。
⇒ build-dmg.sh 改走 UDRW → Finder AppleScript 設大圖示/視窗大小/左右並排 → 轉 UDZO。
   best-effort:無桌面工作階段時只警告不中止(版面是加分,不該讓打包掛掉)。

⚠️ 真正讓 leo 拿到 zip 的原因不在這裡——是**線上 portal 還沒出貨**(見下)。

## 驗(實測輸出)
· collector 全測綠;新增 TestCarryForwardActivity 四子測(含「空轉不該抹掉上次成果」)
· 掛載 dist/Arcrun-v0.18.5.dmg 實查:
    內容剛好兩項(Arcrun.app + Applications→/Applications 捷徑)
    .DS_Store 6148 bytes(版面已存進去)
    Contents/MacOS/ 有 arcrun-app **和 arcrun-collector**
    CFBundleShortVersionString = 0.18.5(不是 1.0.0)/LSUIElement = true/codesign -v 通過
· 三支機械閘 check-cis/check-render/check-tray 全 PASS

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 18:01:17 +08:00
parent 1a1478260b
commit 6bf0daa786
6 changed files with 185 additions and 21 deletions
+51 -17
View File
@@ -78,6 +78,11 @@ type syncStatus struct {
ExtractFailed int `json:"extract_failed"`
ExtractorOK bool `json:"extractor_ok"`
ExtractorError string `json:"extractor_error,omitempty"`
// 「上次真的有做事」那一輪(2026-08-05)——ExtractedOK 是本輪計數、會被下一輪歸零,
// 只靠它顯示成果,使用者在同步完成 15 秒後就看不到任何證據(leo 實撞)。
LastActivityAt string `json:"last_activity_at,omitempty"`
LastActivityOK int `json:"last_activity_ok"`
LastActivityFailed int `json:"last_activity_failed"`
}
// loadCfg 同時保留原始 map ⇒ 回寫時**不會弄丟我們沒宣告的欄位**
@@ -164,21 +169,33 @@ func buildSteps(s syncStatus, syncing bool) []Step {
{Title: "上傳到你的知識庫", State: ""},
}
}
// 🔴 2026-08-05:這三步以前只要「跑過任何一輪」(LastSync 非空)就全標綠,
// 而 meta 用的是**本輪**計數(下一輪歸零)⇒ 綠燈與實際進度脫鉤、數字又永遠空白,
// 正是 leo 說的「都顯示綠燈沒動,實際上已經做完了」。
// ⇒ 綠燈只在「真的有整理過東西」時才亮,且亮的是上次有產出那輪的數字+時間。
done := ""
if s.ExtractedOK > 0 || s.LastSync != "" {
if s.LastActivityOK > 0 || s.LastActivityFailed > 0 {
done = "done"
}
found := "等待中"
if done != "" {
if t, err := time.Parse(time.RFC3339, s.LastActivityAt); err == nil {
found = "上次 " + t.Local().Format("15:04")
} else {
found = "已處理"
}
}
meta := ""
if s.ExtractedOK > 0 {
meta = fmt.Sprintf("上一輪 %d 份", s.ExtractedOK)
if s.LastActivityOK > 0 {
meta = fmt.Sprintf("上 %d 份", s.LastActivityOK)
}
up := Step{Title: "上傳到你的知識庫", Meta: meta, State: done}
if s.ExtractFailed > 0 {
up.Meta = fmt.Sprintf("%s · ⚠ %d 份失敗", meta, s.ExtractFailed)
if s.LastActivityFailed > 0 {
up.Meta = strings.TrimPrefix(fmt.Sprintf("%s · ⚠ %d 份失敗", meta, s.LastActivityFailed), " · ")
}
return []Step{
watch,
{Title: "發現變化", Meta: "等待中", State: done},
{Title: "發現變化", Meta: found, State: done},
{Title: "用 AI 整理成知識卡", State: done},
up,
}
@@ -232,14 +249,23 @@ func loadSyncStatus() syncStatus {
// · collector 沒在跑 → 明說「同步引擎沒有在跑」,不要假裝在整理
// · 有訊號檔且引擎活著 → 才是真的「同步中」
// · 其餘 → 看守中
//
// 🔴 2026-08-05 第二修(leo:「拖新檔進資料夾…自始至終都顯示『等待中』,
//
// 實際上已經做完了,這個 status 是壞的」)——t195 只補了「引擎活著」那半,
// 「同步中」仍靠 sync-now 訊號檔判斷,那是錯的判準,兩個理由:
// ① 訊號檔**只有手動按「立刻同步」才會產生**;leo 這次是拖檔進資料夾
// (自動觸發),整輪從頭到尾沒有訊號檔 ⇒ 畫面永遠停在「看守中/等待中」。
// ② 就算是手動按的,collector 是**先刪檔再跑**consumeSyncNowSignal),
// 所以真正在跑的那段時間訊號檔早就不見了。
// ⇒ 改用 t191 已經做好的機制:collector 開工印 phase:"start"、跑完印 "done"
// supervisor 據此維護 StateSyncingsupervisor.go)。**那條線本來就在,
// Wails 版換代時沒接上而已**——不要再自己發明第三種判斷法。
func describeStatus(s syncStatus) (syncing bool, big, sub string) {
alive := collectorAlive()
_, queued := os.Stat(syncNowSignal())
if !alive {
if !collectorAlive() {
return false, "同步引擎沒有在跑", "資料夾不會被自動整理 ⇒ 請結束 Arcrun 再重新開啟"
}
if queued == nil {
if collectorSyncing() {
return true, "同步中… 正在讀檔並整理成知識卡", "請稍候,完成後會顯示整理了幾份"
}
if s.ExtractorError != "" && !s.ExtractorOK {
@@ -247,13 +273,21 @@ func describeStatus(s syncStatus) (syncing bool, big, sub string) {
}
parts := []string{}
if t, err := time.Parse(time.RFC3339, s.LastSync); err == nil {
parts = append(parts, "上次同步 "+t.Local().Format("15:04"))
parts = append(parts, "上次檢查 "+t.Local().Format("15:04"))
}
if s.ExtractedOK > 0 {
parts = append(parts, fmt.Sprintf("已整理 %d 份", s.ExtractedOK))
}
if s.ExtractFailed > 0 {
parts = append(parts, fmt.Sprintf("⚠ %d 份失敗", s.ExtractFailed))
// 用「上次真的有做事」那輪的數字,不用本輪計數——後者每輪歸零,
// 會讓剛整理完的成果在十幾秒後從畫面上消失(就是 leo 撞到的那個「壞掉的 status」)。
if s.LastActivityOK > 0 || s.LastActivityFailed > 0 {
when := ""
if t, err := time.Parse(time.RFC3339, s.LastActivityAt); err == nil {
when = t.Local().Format("15:04") + " "
}
if s.LastActivityOK > 0 {
parts = append(parts, fmt.Sprintf("%s已整理 %d 份", when, s.LastActivityOK))
}
if s.LastActivityFailed > 0 {
parts = append(parts, fmt.Sprintf("⚠ %d 份失敗", s.LastActivityFailed))
}
}
if len(parts) == 0 {
return false, "看守中 · 資料夾有變動就會自動整理", "還沒有同步紀錄"