修首頁狀態說謊+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:
+51
-17
@@ -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 據此維護 StateSyncing(supervisor.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, "看守中 · 資料夾有變動就會自動整理", "還沒有同步紀錄"
|
||||
|
||||
@@ -22,12 +22,51 @@ echo "② 放 app + Applications 捷徑"
|
||||
ditto "$APP" "$STAGE/Arcrun.app"
|
||||
ln -s /Applications "$STAGE/Applications"
|
||||
|
||||
echo "③ 產 DMG(-fs HFS+:否則含空格/中文的名稱會被截斷)"
|
||||
echo "③ 產可寫 DMG(-fs HFS+:否則含空格/中文的名稱會被截斷)"
|
||||
rm -f "$DMG"
|
||||
hdiutil create -volname "Arcrun" -srcfolder "$STAGE" -fs HFS+ -ov -format UDZO "$DMG" >/dev/null
|
||||
RW="$(mktemp -d)/rw.dmg"
|
||||
hdiutil create -volname "Arcrun" -srcfolder "$STAGE" -fs HFS+ -ov -format UDRW "$RW" >/dev/null
|
||||
rm -rf "$(dirname "$STAGE")"
|
||||
|
||||
# ── ④ 排版面:讓「拖進去」這件事一眼可見 ────────────────────────────────────
|
||||
# 🔴 leo 2026-08-05:「要跳出虛擬隨身碟,打開一個 finder 的視窗,**顯示 Arcrun 和
|
||||
# Application 的捷徑**,用戶把 App 拖進 Application」。
|
||||
# 光是「裡面有兩個項目」不夠——預設是清單視圖、位置隨機,使用者看不出「要往右拖」。
|
||||
# ⇒ 設成大圖示、固定視窗大小、兩個 icon 左右並排。
|
||||
# best-effort:Finder 自動化在沒有桌面工作階段時(ssh/CI)會失敗,
|
||||
# 失敗只警告不中止——版面是加分,DMG 本身照樣可用,不該讓打包整個掛掉。
|
||||
hdiutil attach "$RW" -noautoopen -quiet
|
||||
if osascript <<'APPLESCRIPT' >/dev/null 2>&1
|
||||
tell application "Finder"
|
||||
tell disk "Arcrun"
|
||||
open
|
||||
set current view of container window to icon view
|
||||
set toolbar visible of container window to false
|
||||
set statusbar visible of container window to false
|
||||
set the bounds of container window to {200, 160, 800, 560}
|
||||
set vopts to the icon view options of container window
|
||||
set arrangement of vopts to not arranged
|
||||
set icon size of vopts to 128
|
||||
set position of item "Arcrun.app" of container window to {150, 190}
|
||||
set position of item "Applications" of container window to {450, 190}
|
||||
update without registering applications
|
||||
close
|
||||
end tell
|
||||
end tell
|
||||
APPLESCRIPT
|
||||
then
|
||||
echo " ✅ 版面已設定(大圖示,Arcrun 在左、Applications 在右)"
|
||||
else
|
||||
echo " ⚠️ Finder 版面設定失敗(多半是沒有桌面工作階段)——DMG 仍可用,只是視圖是預設值" >&2
|
||||
fi
|
||||
sync
|
||||
hdiutil detach "/Volumes/Arcrun" -quiet
|
||||
|
||||
echo "⑤ 壓成唯讀發佈檔"
|
||||
hdiutil convert "$RW" -format UDZO -o "$DMG" >/dev/null
|
||||
rm -rf "$(dirname "$RW")"
|
||||
|
||||
SIZE=$(ls -lh "$DMG" | awk '{print $5}')
|
||||
echo "✅ 完成:${DMG}(${SIZE})"
|
||||
echo
|
||||
echo "🔬 使用者流程:開 DMG → 把 Arcrun 拖到 Applications → 從啟動台開啟"
|
||||
echo "🔬 使用者流程:開 DMG → 把 Arcrun 拖到 Applications → 退出這個虛擬磁碟 → 從啟動台開啟"
|
||||
|
||||
@@ -144,3 +144,16 @@ func collectorAlive() bool {
|
||||
}
|
||||
return processRunning(collectorBinPath())
|
||||
}
|
||||
|
||||
// collectorSyncing 回報 collector 是不是**正在跑一輪**。
|
||||
//
|
||||
// 🔴 2026-08-05:資料來源是 t191 就做好、上面這支 supervisor 一直在維護的狀態機——
|
||||
// collector 開工印 phase:"start"、跑完印 "done",supervisor 據此進出 StateSyncing。
|
||||
// 換 Wails 時 App 沒接這條線、改看 sync-now 訊號檔,於是「拖檔進資料夾」這種
|
||||
// 自動觸發的同步整輪都不會亮燈(訊號檔只有手動按鈕才產生,而且 collector 是先刪再跑)。
|
||||
func collectorSyncing() bool {
|
||||
if sup == nil {
|
||||
return false
|
||||
}
|
||||
return sup.Status().State == supervisor.StateSyncing
|
||||
}
|
||||
|
||||
@@ -640,7 +640,13 @@ func RunDirectOnce(cfg *DirectConfig, dryRun bool) ([]DirectResult, int, *Trigge
|
||||
break
|
||||
}
|
||||
}
|
||||
if serr := SaveSyncStatus(StatusFilePath(cfg.Manifest), st); serr != nil {
|
||||
// 🔴 2026-08-05(leo:「自始至終都顯示『等待中』…實際上已經做完了,這個 status 是壞的」):
|
||||
// 本輪有產出就記成「最近一次有做事」;本輪沒事做則把上一輪的原樣帶下來,
|
||||
// 不要讓「剛整理完 N 份」這個唯一的完成證據在 15 秒後被歸零抹掉。
|
||||
statusPath := StatusFilePath(cfg.Manifest)
|
||||
prev, _ := LoadSyncStatus(statusPath) // 讀不到=零值,等同「沒有上一輪」
|
||||
CarryForwardActivity(prev, &st)
|
||||
if serr := SaveSyncStatus(statusPath, st); serr != nil {
|
||||
fmt.Fprintf(os.Stderr, "status 寫入失敗(不擋看守):%v\n", serr)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,14 @@ type SyncStatus struct {
|
||||
Failures []ExtractFail `json:"failures,omitempty"` // 失敗清單(路徑+白話原因)
|
||||
ExtractorOK bool `json:"extractor_ok"` // 萃取器本身是否就緒(預檢,機器層級)
|
||||
ExtractorError string `json:"extractor_error,omitempty"` // 未就緒的白話原因
|
||||
// 🔴 最近一輪「真的有做事」的結果(2026-08-05,leo 實撞)。
|
||||
// ExtractedOK/ExtractFailed 是**本輪**計數、每輪覆寫 ⇒ 沒事做的那輪就歸零。
|
||||
// leo 拖檔進資料夾,萃取上傳都跑完了,但下一輪(15 秒後)把數字歸零
|
||||
// ⇒ 首頁「上一輪 N 份」永遠空白,看起來像從頭到尾什麼都沒發生。
|
||||
// ⇒ 另存一組「上次有產出的那輪」,沒事做的輪次原樣往下帶,不被清掉。
|
||||
LastActivityAt string `json:"last_activity_at,omitempty"` // RFC3339,上次有產出那輪的完成時間
|
||||
LastActivityOK int `json:"last_activity_ok"` // 那一輪成功幾份
|
||||
LastActivityFailed int `json:"last_activity_failed"` // 那一輪失敗幾份
|
||||
// 頂層 cloud 欄位保留向後相容(單帳號時同時填頂層+AccountDetails)
|
||||
CloudVersion string `json:"cloud_version,omitempty"` // bundle_version(單帳號時填)
|
||||
CloudCheckOK bool `json:"cloud_check_ok"` // /health 可達才為 true
|
||||
@@ -59,6 +67,27 @@ func SaveSyncStatus(path string, s SyncStatus) error {
|
||||
return os.WriteFile(path, data, 0o644)
|
||||
}
|
||||
|
||||
// CarryForwardActivity 決定「最近一次有做事」那三個欄位的值。
|
||||
//
|
||||
// 🔴 2026-08-05 leo 實撞:「拖新檔進資料夾,完成本地萃取、上傳,但自始至終 daemon 的
|
||||
// 首頁都顯示『等待中』…實際上已經做完了,這個 status 是壞的」。
|
||||
// 真兇:ExtractedOK/ExtractFailed 是**本輪**計數,每輪覆寫整個 status.json
|
||||
// ⇒ 做完事的那輪寫下 N,下一輪(十幾秒後)沒事做就把它蓋成 0,
|
||||
// 使用者看到的畫面永遠是「什麼都沒發生」。
|
||||
//
|
||||
// 規則:本輪有產出 → 記本輪;本輪沒事做 → 原樣沿用上一輪的(不清空)。
|
||||
func CarryForwardActivity(prev SyncStatus, st *SyncStatus) {
|
||||
if st.ExtractedOK > 0 || st.ExtractFailed > 0 {
|
||||
st.LastActivityAt = st.LastSync
|
||||
st.LastActivityOK = st.ExtractedOK
|
||||
st.LastActivityFailed = st.ExtractFailed
|
||||
return
|
||||
}
|
||||
st.LastActivityAt = prev.LastActivityAt
|
||||
st.LastActivityOK = prev.LastActivityOK
|
||||
st.LastActivityFailed = prev.LastActivityFailed
|
||||
}
|
||||
|
||||
// LoadSyncStatus 讀取狀態檔;不存在或解析失敗回零值+error(托盤自行降級)。
|
||||
func LoadSyncStatus(path string) (SyncStatus, error) {
|
||||
var s SyncStatus
|
||||
|
||||
@@ -225,3 +225,46 @@ func TestRunDirectOnceWritesStatusExtractorFail(t *testing.T) {
|
||||
t.Error("ExtractorError 應有白話說明")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCarryForwardActivity 鎖住「做完的證據不會被下一輪抹掉」(2026-08-05 leo 實撞的 status 假象)。
|
||||
//
|
||||
// 症狀:拖檔進資料夾 → 萃取上傳都跑完了,但首頁自始至終顯示「等待中」、綠燈沒動。
|
||||
// 真兇:ExtractedOK 是本輪計數,每輪覆寫整份 status.json ⇒ 有產出那輪寫下 N,
|
||||
// 十幾秒後沒事做的那輪把它蓋成 0,使用者永遠看不到自己剛做完的事。
|
||||
func TestCarryForwardActivity(t *testing.T) {
|
||||
t.Run("本輪有產出→記本輪", func(t *testing.T) {
|
||||
st := SyncStatus{LastSync: "2026-08-05T17:18:34+08:00", ExtractedOK: 3, ExtractFailed: 1}
|
||||
CarryForwardActivity(SyncStatus{}, &st)
|
||||
if st.LastActivityAt != st.LastSync || st.LastActivityOK != 3 || st.LastActivityFailed != 1 {
|
||||
t.Fatalf("有產出的一輪應記成 last_activity,得到 %+v", st)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("本輪沒事做→沿用上一輪,不歸零", func(t *testing.T) {
|
||||
prev := SyncStatus{LastActivityAt: "2026-08-05T17:18:34+08:00", LastActivityOK: 3, LastActivityFailed: 1}
|
||||
st := SyncStatus{LastSync: "2026-08-05T17:36:38+08:00"} // 空轉的一輪
|
||||
CarryForwardActivity(prev, &st)
|
||||
if st.LastActivityOK != 3 || st.LastActivityFailed != 1 {
|
||||
t.Fatalf("空轉的一輪不該抹掉上次成果,得到 OK=%d Failed=%d", st.LastActivityOK, st.LastActivityFailed)
|
||||
}
|
||||
if st.LastActivityAt != prev.LastActivityAt {
|
||||
t.Fatalf("時間也要沿用上一輪:want %q got %q", prev.LastActivityAt, st.LastActivityAt)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("只有失敗也算有做事", func(t *testing.T) {
|
||||
st := SyncStatus{LastSync: "2026-08-05T18:00:00+08:00", ExtractFailed: 2}
|
||||
CarryForwardActivity(SyncStatus{}, &st)
|
||||
if st.LastActivityFailed != 2 || st.LastActivityAt == "" {
|
||||
t.Fatalf("全失敗的一輪也要留痕(否則使用者不知道剛剛出過事),得到 %+v", st)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("從沒跑過→全空,不編造", func(t *testing.T) {
|
||||
st := SyncStatus{LastSync: "2026-08-05T18:00:00+08:00"}
|
||||
CarryForwardActivity(SyncStatus{}, &st)
|
||||
if st.LastActivityAt != "" || st.LastActivityOK != 0 || st.LastActivityFailed != 0 {
|
||||
t.Fatalf("沒有任何歷史時應保持空值,得到 %+v", st)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user