refactor(collector): 剪掉臍帶——collector/ 自己就算得出自己的版本號(D95 第一輪②)

leo 2026-08-17:「身為管理者,你要從頭到尾**不要有很多扭曲**,因為你根本不記得
你做的這些扭曲,**每次都要查**,很直接,源碼、產出物,從 stage 到 prod。」

`collector/` 原本要伸手到 repo 根才算得出自己的版本,共四條:

  ① `daemon-version.py` 讀 `docs-site/src/content/docs/help/changelog.md`
  ② `daemon-version.py` 讀 `ROOT/DAEMON_LINE`
  ③ `daemon-version.py` 的原始碼指紋從 repo 根 `git ls-files collector`
  ④ `changelog-section.sh` 讀 `$REPO_ROOT/docs-site/...` 並 exec
     `$REPO_ROOT/installer/scripts/daemon-notes.mjs`
  ⑤(複驗才發現的第五條)`build-msix.sh` 用 `git rev-parse --show-toplevel`/../../.env
     推 InkStoneCo 頂層拿 MS Store Identity

現在全部落在 `collector/` 內部:

  · `collector/CHANGELOG.md`  ← 新家。**原檔同時裝著兩條版本線**
    (桌面版 `v0.18.x` + 雲端引擎 `1.4.x`),已按版號格式拆開;
    雲端那 17 段原地不動留在 docs-site,桌面版這 30 段搬過來。
  · `collector/DAEMON_LINE`   ← 從 repo 根搬進來
  · 指紋改以 `collector/` 為根算(`cwd=COLLECTOR`、pathspec `.`)
    ⇒ 相對路徑前綴變了、而路徑有進雜湊 ⇒ `FINGERPRINT_ALGO` 3→4,
      照既有設計讓帳本自動整本作廢重記(不要手改 JSON)
  · `daemon-notes.mjs` 實作搬進 `collector/cmd/arcrun-app/`,
    `installer/scripts/daemon-notes.mjs` 變薄殼轉呼叫
    ⇒ **根可以往內伸手,collector 不可以往外伸手**,方向單向
  · `build-msix.sh` 改成往上找「帶著那把鍵的 .env」,
    leo 08-06「腳本自己去讀不要再問人」原樣保留,但不再綁目錄結構

驗收閘:`collector/check-standalone.sh`——不是 grep,是**行為證明**:
把 collector/ 的檔案單獨複製到 repo 之外的臨時目錄、在那裡 git init,
再跑版本計算與打包前置閘。跑得起來=真的自足。

自己發現並修掉的三件:
  · `arcrun-tray/assets/store/` 與 `--setup` 其實還活著(見上一顆 commit)
  · 兩支同名的 `daemon-notes.mjs` CLI 守衛比的是**檔名尾綴**
    ⇒ 兩支一起開火,問雲端版號時 collector 那支先 exit(1),
      薄殼根本沒機會查 docs-site。改成比絕對路徑。
  · `collector/CHANGELOG.md` 的「怎麼出新版」說明若照抄那行標題,
    會被 `daemon-version.py` 的純字串比對當成「有待發佈內容」而誤升版
    ⇒ 說明裡刻意不寫成真的標題,並把這個邊角寫在檔案裡

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 13:05:33 +08:00
parent cef157c584
commit bd25efaae5
10 changed files with 725 additions and 22 deletions
+374
View File
@@ -0,0 +1,374 @@
# Arcrun 桌面版(daemon)版本說明
> 🔴 **這份就是版本號的單一真相源。**2026-08-18 從 `docs-site/src/content/docs/help/changelog.md` 搬來,D95 第一輪)
>
> 為什麼搬:`daemon-version.py` 從這份檔案算出版本號,
> 而它原本住在 repo 根的 `docs-site/` 底下——`collector/` 得往上伸手才拿得到自己的版本。
> 那條臍帶讓 `collector/` **沒有資格被搬成獨立 repo**,也讓「源碼→產出物」多了一道要記得的扭曲。
> 現在它與 `DAEMON_LINE`、`daemon-version.py` 住在同一棵樹底下,`collector/` 自己就算得出自己的版本。
>
> 🔴 **雲端(Arcrun 引擎 1.4.x)的版本說明不在這裡**,它留在
> `docs-site/src/content/docs/help/changelog.md`——兩條版本線本來就是兩個產品,
> 混在一個檔裡是 D95 要拆掉的那種扭曲。
## 怎麼出新版(不要手打版本號)
在**本檔最上面**(第一個 `v0.18.x` 段落之上)加一個二級標題,標題文字就是
`下一版(未發佈)`,底下條列白話更新內容:
<二級標題> 下一版(未發佈)
- 🔴 **(用戶看得懂的白話標題)**:(以前會怎樣、現在會怎樣)
> ⚠️ 上面刻意**不寫成真的標題**(用 `<二級標題>` 代替兩個井字號)。
> `daemon-version.py` 判斷「有沒有待發佈內容」用的是**純字串比對**,
> 本檔任何位置只要出現那一行的字面值,它就會以為要升版——
> 連寫在說明或程式碼區塊裡都算。這是機制的已知邊角,別把它變成陷阱。
打包時 `daemon-version.py --stamp` 會把它戳成正式版號+今天日期。
**版本號從頭到尾沒有人打過**,而且「更新內容」與「版本」天生綁在一起。
沒有這一段 ⇒ `changelog-section.sh --check` 擋下打包。
⚠️ 寫給用戶看,不是寫給工程師看:說「以前會怎樣、現在會怎樣」,不要寫函式名或 commit 編號。
---
## v0.18.292026-08-16
**建議更新**——這版修好兩件「你以為它在做的事,其實它沒在做」。
- 🔑 **「移除資料夾」現在真的會收回**:先前你把一個資料夾從清單移除,它**只是停止繼續看**——已經整理進雲端的知識**一筆都不會消失**,照樣搜得到、AI 照樣拿它回答。現在移除時會問你要不要**連同雲端的知識一起收回**,預設是收回;只想停止監看也可以選。
- 🔑 **不會再把別人的套件當成你的知識**:如果你掛的是一個軟體專案資料夾,先前它會連 `node_modules` 那類**依賴目錄**裡的說明文件一起讀進去——實測有一次產出的 27 張卡裡,**21 張是第三方套件的 API 文件與授權條款**,你自己的東西只有 5 張。現在那些不會再被收。
- 🔑 **有沒有用 git 不再影響收哪些檔**:先前「這個資料夾要怎麼收」會看它有沒有版控——所以**只要你在自己的筆記資料夾開一次版控,收進去的東西就會大幅減少,而畫面上不會有任何提示**。現在改成看資料夾**實際裝了什麼**。
- 這版**不影響你已經同步的資料**,只改今後怎麼收、以及移除時多一個選擇。
## v0.18.282026-08-16
-**一指到資料夾,馬上就能問「裡面有什麼」**:小幫手掃完資料夾(幾秒鐘)就會先把「有哪些檔案、最近改了什麼」做成一頁總覽送上知識庫,不用等 AI 逐份整理——就算當天的 AI 額度用完了,這類問題照樣答得出來。內容的深度整理照常在背景進行,一份都不會少;檔案有增刪時總覽會自動更新,不會堆出重複資料。
- 📚 **整理出來的不再是一檔一張流水卡,而是一份看得懂的 wiki**:每份文件變成「一張總覽卡+幾張概念卡」,放在資料夾裡隱藏的 `.wiki/` 目錄——就算不裝任何軟體,用檔案總管也能點著連結讀。每一層都有自動維護的目錄(00-INDEX):你的每一份檔案都列在上面,沒有可整理內容的(例如發票、清單)也會誠實標「空」,你分得出「沒東西」和「被漏掉」。卡片之間有雙向連結、每張卡都寫明出處原稿;你的原稿一個字都不會被動到。這次也把沒接 Gemini 金鑰的免費路一起補齊,不用自己接金鑰也拿到一樣完整的整理結果。
- 🔗 **卡片之間的關聯終於連得起來**:上一版換了新的卡片格式後,小幫手其實抓不到卡片彼此的關係,於是每張卡都變成孤零零的一張,知識庫裡搜到一張也連不到相關的其他卡。這次修好,同一份資料裡的概念會正確地互相連結。
- 📍 **搬動或改名資料夾後,舊卡片不會被誤刪**:以前判斷「這份卡片是不是已經被你刪掉」只認標題不認路徑——不同資料夾裡剛好同名的檔案,其中一份可能被誤判成「已經不在了」而被清掉。現在改成認路徑,不會再誤殺。
- 🛡️ **小幫手不會再動到你自己用 git 管理的檔案**:如果你監看的資料夾本身是一個 git 專案(例如你自己維護的筆記或 wiki),小幫手現在只讀不改——不會把你自己手寫的卡片搬走改名,也不會去碰你倉庫裡的程式碼,只整理你的文件內容。
## v0.18.272026-08-13
- 🛡️ **監看的是筆記庫「裡面的某個資料夾」時,保護不會再失效**:上一版的筆記庫保護只認「你監看的那一層本身是不是庫」——如果你監看的其實是庫底下的子資料夾(例如 `KB/docs`),保護會誤判成「這不是筆記庫」,整理結果照樣被 Logseq/Obsidian 當成你自己的新頁面收編。現在改成往上找最近的庫根,不管你監看哪一層,保護都認得出來。
- 🏷️ **機器寫的檔案,一眼就看得出不是你自己的**:就算檔名不撞名,資料夾裡冒出一堆陌生新檔案還是會讓人心裡發毛。現在寫進筆記庫的整理產物一律帶固定前綴,你打開資料夾能馬上分辨哪些是自己寫的、哪些是小幫手整理出來的。
## v0.18.262026-08-11
- 🗂️ **不會再把整理結果寫進你的 Logseq/Obsidian 筆記庫**:以前小幫手不會分辨你選的資料夾是
不是筆記庫,整理出來的卡片會被 Logseq/Obsidian 當成「你自己寫的新頁面」顯示出來。
現在會先認出這是不是筆記庫,是的話卡片改放到你看不到、筆記軟體也不會掃描的位置;
一般資料夾的行為不變。
- 🛡️ **不會再無聲蓋掉你原本就有的檔案**:以前如果整理出來的檔名剛好跟你資料夾裡既有的檔案
同名,會直接覆蓋、你的原稿就沒了。現在遇到同名會先把你原本的檔案備份一份,
新內容才會寫進去,不會再有東西不見了都不知道。
## v0.18.252026-08-09
- 🌱 **第一次打開改成兩步引導**:以前打開小幫手只有兩行字兩顆按鈕,看不懂在幹嘛。
現在第一步先用三句話說清楚 Arcrun 是什麼,第二步用兩張卡片問你「已經有知識庫」還是
「還沒有」——選「還沒有」會直接告訴你回來要按哪顆鈕連線,不會丟出去就找不到路回來。
## v0.18.242026-08-08
- 🔴 **修好「失敗原因每幾秒就消失」**:以前一個檔案沒送上去,展開看原因會看到
「這個檔是在舊版失敗的,當時沒有記下原因」——即使你完全沒有更新過。
現在展開失敗檔案,看得到真正的失敗原因。
- 📊 **首頁改成看總量,不用逐檔猜**:以前首頁只顯示「這一輪」處理了幾份,
如果你的資料夾有幾千份檔案,會搞不清楚「雲端只有一百多張卡,是壞了還是
還在跑」。現在首頁直接告訴你:共幾份、已經送上去幾份、還在排隊幾份、
送不上去幾份——四個數字相加就是你資料夾裡的總檔案數。「送不上去」的
分類預設收起來,想看細節再展開。
- 🩺 **「疑難排解」按鈕搬進小幫手本體**:以前雲端網頁上的診斷按鈕看不到你電腦上的
真實狀況(總檔案數、失敗分類、小幫手版本)——因為那顆按鈕跑在瀏覽器裡,
碰不到你電腦的檔案。現在小幫手裡有一顆匯出診斷檔的按鈕,把雲端與本機的
狀況合併成一份檔案,還看得出小幫手本身是不是還活著在跑(不會誤以為當機);
回報問題時附上這份檔案,我們就能一次看懂發生什麼事。
- 🔒 **診斷檔不會洩漏你電腦上的資料夾路徑**:以前如果同步引擎剛好停住,
診斷檔裡的錯誤訊息會帶出你電腦完整的資料夾路徑(例如你的使用者名稱、
桌面上的資料夾結構)。現在只會保留到資料夾名稱本身,你可以放心把診斷檔
分享出來讓我們協助排查,不會連帶交出你電腦的目錄結構。
## v0.18.232026-08-08
- 🔴 **修好「按了檢查更新,卻更新不了」**:以前按下「檢查更新」會跳出
`解壓失敗:ditto: Couldn't read PKZip signature` 這種看不懂的錯誤,
然後就卡在那裡——**只能自己去網站手動下載安裝**。
原因是我們從 v0.18.5 起改用 `.dmg` 發佈 Mac 版,但更新程式還在用舊的解壓方式,
**這個問題已經存在七個版本沒被發現**。現在按下去會真的更新完成。
- 🔁 **已經是最新版就不會再叫你重新啟動**:以前手動裝好新版之後,
設定頁還是會顯示「新版已下載完成,重新啟動就會套用」,
明明新舊版號一模一樣,卻一直催你重開。
現在會實際比對版號,已經是最新版就把提示和殘留的暫存檔一起清掉。
- 🪟 **Windows 版現在也會自己更新**:以前 Windows 按檢查更新只會幫你開啟下載資料夾,
要自己把檔案換掉。現在會直接完成更新並重新啟動。
## v0.18.222026-08-08
- 🔴 **修好「一次丟一大批檔案,額度會爆掉」**:以前把幾百份檔案一次丟進資料夾,
小幫手會逐一觸發雲端整理,短時間內衝爆你雲端帳號的免費額度,
結果大部分檔案都失敗、只有前面幾份成功。
現在改成分批慢慢消化:每一輪只處理一部分,**今天新寫的檔案永遠優先**,
積壓的舊檔案不會擋住你正在用的東西。
- 💬 **額度用完時講人話**:偵測到雲端額度用完後,畫面會直接告訴你
「今天已經整理幾份、明天早上會自動恢復、不花錢也沒關係」,
不會再讓你看到一串看不懂的錯誤代碼。
- 🔁 **中斷後不用從頭來過**:以前同步到一半被強制關閉或電腦重開機,
已經處理好的檔案也會被視為沒做過,得整批重跑。
現在每處理完一份就立刻記住進度,下次啟動只會接著做真正還沒完成的部分。
- 🧹 **同一批資料的多種格式不會重複佔用額度**:如果你的資料夾裡有同一份筆記
的好幾種格式(例如同時有 `.md``.json``.html`),現在只會挑一種送去整理,
不會把同一份內容當三份處理。
- 🌱 **全新安裝會自動準備一個可以隨時刪除的示範資料夾**:第一次連上知識庫時,
Documents 底下會出現一個「Arcrun 範例庫(可刪除)」,裡面已經放了幾份說明
Arcrun 的筆記——不用自己找資料夾,馬上就能看到「丟檔案 → 變成知識卡 → 搜得到」
的完整過程。看不順眼可以直接刪掉,刪掉就真的沒了;一旦你自己加了資料夾,
這個示範資料夾也不會再自動出現。
## v0.18.212026-08-06
- 🏪 **開始提供 Microsoft Store 版本(`.msix`**:除了原本的 `.exe`
現在也一併發佈商店格式的安裝包,正在送審。
上架後 Windows 就不會再把 Arcrun 當成可疑程式攔下來(商店版由微軟簽章)。
## v0.18.202026-08-06
- 📋 **失敗清單改成「點開才看細節」**:以前把失敗原因直接攤在檔名旁邊,
中文檔名被擠成一行一個字,很難讀。
現在只列**檔名**,旁邊一個三角形,需要時點開才看原因(預設收合)——
重點是先讓你看到「**哪幾個檔沒過**」。
## v0.18.192026-08-06
- ✏️ **失敗訊息改成人話**:上一版雖然列出了失敗的檔案,但寫的是
「上次失敗(第 5 次),5h38m38s 後重試(上面是自動重試的排程,真正的原因是前一次失敗)」
——**那是寫給工程師看的**。現在改成:
「已經自動試過 5 次都沒成功,約 5 小時後會再試一次。」
知道原因時則直接講原因(例如「今天的免費 AI 額度用完了」),把重試排程放在後面當補充。
時間也不再精確到秒——你需要知道的是「大概多久」,不是 `5h38m38s`
## v0.18.182026-08-06
- 🔴 **失敗時會告訴你「是誰的錯、你要不要做什麼」**:以前只寫「⚠ N 份失敗」,
你分不出「等一下就會自己好」跟「這個檔永遠不會成功」——而這兩件事的處理方式完全相反。
現在會列出**檔名 + 原因**,例如:
- 「今天的免費 AI 額度用完了 ⇒ 明天會自動恢復,這些檔案會自己補上,你不用做什麼」
- 「這份 PDF 看起來是掃描的圖片 ⇒ 需要先做文字辨識(OCR),或換一份有文字的版本」
- 🧹 **不再把開發用的檔案放進你的資料夾**:以前小幫手會自動鋪一套開發者用的範本
`CLAUDE.md``scripts/``system-dev/` 共 37 個檔)到你看守的資料夾裡,
把你的資料夾弄亂,而且**那些檔還會被當成你的知識收進知識庫**(於是多出你沒建過的「庫」)。
現在完全不放了;已經放進去的那些也**不會再被當成知識**。
> 已經多出來的庫,可以在 Portal 的「庫目錄管理」自己按「移除」。
- ✏️ 「有 N 個檔案沒有被整理」不再重複列出同一個檔名。
## v0.18.172026-08-06
**如果你看到「⚠ N 份失敗」卻不知道是哪一份,請更新這一版。**
- 🔴 **失敗的檔案現在會列出檔名與原因**:以前首頁只寫「⚠ 3 份失敗」,
你完全不知道是哪幾份、為什麼失敗,也就無從判斷要不要處理。
現在會直接列出**檔名 + 失敗原因**(最多 8 份,總數照實講)。
- 🗂️ **出問題時可以一鍵打開紀錄檔資料夾**:同步引擎有狀況時,
首頁會多一張「需要回報這個問題?」的卡,按一下就開啟存放紀錄檔的資料夾。
(上一版說有這個功能,但按鈕**實際上沒做進畫面**,這版才真的有。)
## v0.18.162026-08-06
**如果你看到「還沒連上知識庫」但明明已經連上了,請更新這一版。**
- 🔴 **不再誤報「還沒連上知識庫」**:明明已經新增好知識庫帳號、側邊欄也看得到,
首頁卻跳出「需要你處理一下 ⇒ 還沒連上知識庫」。
原因是判斷時看錯了地方(看的是舊版單一帳號的欄位,而你的是新版多帳號設定)。
- 🔴 **順便修好那句話指錯路**:它叫你「點托盤選單的『+ 新增帳號…』」,
但托盤選單早就只剩「結束 Arcrun」了,那個入口根本不存在。
現在會正確指向左邊「知識庫」下方的**新增知識庫帳號**。
## v0.18.152026-08-06
- 🪵 **診斷紀錄不再印出不存在的網址**:啟動訊息以前會寫成
`→ /webhooks/named//rag_ingest/trigger`(沒有網域、還有雙斜線),
看起來像設定壞掉——其實功能完全正常,只是那行字用錯了資料來源。
誤導性的紀錄比沒有紀錄更麻煩,會害人往錯的方向找問題。現在印的是真正會用到的網址。
## v0.18.142026-08-06
**第一次安裝的人請用這一版**——修好「剛裝好、連上知識庫之後卻不會開始整理」。
- 🔴 **連上知識庫後會立刻開始,不必重開程式**:以前**第一次安裝**的人會遇到——
打開程式、連上知識庫、加了資料夾,畫面卻一直說「同步引擎沒有在跑」,
要**自己想到把程式關掉再打開**才會動。
原因是程式啟動時還沒有設定,就沒有把同步引擎準備好;之後設定好了也不會補啟動。
現在設定一完成就會自己開始。
- ✏️ 順帶把「請結束 Arcrun 再重新開啟」這句話拿掉了——
那是把該由程式做的事推給你。現在會直接說「正在啟動同步引擎,請稍候」,
還沒連知識庫時則說「按『新增知識庫帳號』就會開始」。
## v0.18.132026-08-06
**如果你更新到 v0.18.12 仍然看到「同步引擎一直啟動失敗」,請更新這一版。**
- 🔴 **已經裝過舊版的人,設定檔現在會自動修好**:上一版只修好了「全新安裝」,
但**已經存在的設定檔不會被修復**——程式打開時只是讀取它,不會重寫,
所以錯誤原封不動(畫面仍寫「缺必填欄位:manifest」)。
現在程式一打開就會檢查並自動補齊,不需要你做任何事、也不用重裝。
## v0.18.122026-08-06
- 🗂️ **出問題時,一鍵打開紀錄檔資料夾**:同步引擎有狀況時,首頁會多出一張
「需要回報這個問題?」的卡片,按一下就直接開啟存放紀錄檔的資料夾
(裡面的 `collector.log``app.log` 傳給我們就能查)。
卡片上也會直接寫出資料夾路徑,方便自己找。
> 紀錄檔**一直都在寫**,不需要開啟任何「偵錯模式」;這次只是補上找得到的入口。
## v0.18.112026-08-06
**Windows 使用者請務必更新**——這版修好「裝了完全不會動」。
- 🔴 **修好「一直在看守中和沒有在跑之間閃、重開也沒用」**
這是**全新安裝**才會撞到的問題——設定檔少了一個必要欄位,
同步引擎每次啟動都立刻失敗,程式就一直重試、畫面跟著閃。
連帶的症狀是**加了資料夾也完全沒反應**(因為同步引擎根本沒跑起來過)。
現在全新安裝就能正常運作。
> ⚠️ **這一版只修好「全新安裝」**;已經裝過舊版的人**不會**被自動修好(見 v0.18.13)。
- 🔍 順帶:同步引擎出問題時,畫面會**顯示真正的錯誤訊息**,
而不是只寫「exit status 2」這種看不懂的東西。
## v0.18.102026-08-06
**Windows 使用者請更新**——這版讓「同步引擎沒在跑」說得出原因。
- 🔍 **「同步引擎沒有在跑」現在會說出原因**:以前畫面只會在「看守中」和「沒有在跑」
之間閃、叫你重新開啟(但重開沒有用,因為原因沒變)。
現在會直接寫「**同步引擎一直啟動失敗,已自動重試 N 次**」並附上錯誤訊息,
而且畫面不再閃爍。同樣的訊息也會寫進記錄檔(`.arcrun-rag\app.log`),方便回報問題。
> ⏳ **還沒做到**:正式的安裝程式(開始功能表/解除安裝)**這版沒有**,仍是單一 `.exe`。
> 打包工具在我們的機器上壞掉了,正在換路。上架 Microsoft Store 後會一併解決。
## v0.18.92026-08-06
**Windows 使用者請更新**——這版把「防毒軟體把 Arcrun 當成病毒刪掉」的機會降到最低。
- 🛡️ **不再有第二個檔案被寫到你的電腦裡**:上一版雖然只下載一個 `Arcrun.exe`
但它執行時會**自己解出一支同步引擎程式**放到硬碟再執行。
這個動作在防毒軟體眼中跟惡意程式很像,Windows Defender 因此把整個檔案隔離
(封測者實際遇到,訊息是「檔案包含病毒或潛在的垃圾軟體」)。
現在同步引擎**直接內建在同一支程式裡**,不會再產生任何額外檔案。
- 📦 檔案也順帶變小了(26MB → 22MB)。
- 🔍 **「有些檔案沒有被整理」現在會說出是哪一個**:以前只寫「有 1 個不是文件的檔案」,
你根本無從判斷那是什麼、也不知道自己是不是存錯格式了。現在會直接列出檔名(最多 5 個)。
- ✏️ 同一張卡不再把同一件事講兩次(以前會先寫「看起來不是文件,所以跳過了」,
底下空的,然後又寫「另外有 1 個…」)。
> ℹ️ 這版**不保證**防毒軟體不再誤判——真正的解法是上架 Microsoft Store
> (商店版由微軟簽章,不會被誤判),我們正在辦。
> 如果還是被擋,請在「Windows 安全性 → 保護歷程記錄」把它選為「允許在裝置上」。
## v0.18.82026-08-06
**Mac 使用者請務必更新**——這版修好「關不掉、所以新版裝不上去」。
- 🔴 **「結束 Arcrun」現在真的會結束**:以前在選單列圖示按右鍵選「結束 Arcrun」,
程式其實**還活著**,只是視窗被藏起來——唯一關得掉的方法是「強制結束」。
最直接的後果是**下載新版時蓋不過舊版**,不知道怎麼強制結束的人就卡在那裡了。
- 🖥️ **Dock 不再出現重複的圖示**:常駐小工具照慣例只在選單列出現一個圖示,
但 Arcrun 之前**選單列和 Dock 兩邊都有**。現在只留選單列那一個。
(順帶:它也不會再出現在「強制結束」清單裡了——那是同一個原因造成的。)
- 🔳 **視窗可以全螢幕了**:Mac 版左上角的綠色按鈕之前是灰的、按不動(Windows 版正常)。
- 📐 **視窗預設大小改成螢幕的 70%**(上一版的 50% 太小)。
## v0.18.72026-08-06
**Windows 使用者請務必更新**——這版把 Windows 版幾個「根本不能用」的問題一次修掉。
Mac 使用者這版沒有功能變化,不更新也沒關係。
- 🪟 **下載下來只有一個檔案了**:以前是 zip,解開還有兩個 exe,
很容易只拿走其中一個——而少拿那個,同步就永遠不會動,畫面還不會告訴你原因。
現在**直接下載一個 `Arcrun.exe`,雙擊就跑**,不必解壓縮、不必找第二個檔。
- 🔴 **Windows 上「同步引擎沒有在跑」修好了**:以前不管同步引擎有沒有在跑,
首頁**一律**顯示「同步引擎沒有在跑」——因為判斷方式用了一個 Windows 上根本不存在的指令。
現在會誠實反映真實狀態。
- 🔴 **托盤不會再開出一堆圖示**:以前每點一次程式就多開一個,
托盤會出現好幾個一模一樣的圖示,分不清該點哪個。
現在**第二次點只會把既有視窗叫回來**。
- 🎨 **圖示換成 Arcrun 的標誌**:Windows 版的視窗與托盤圖示之前一直是開發工具的預設「W」,
現在是正確的 Arcrun 標誌。
- 📐 **視窗大小改成螢幕的一半**:以前寫死的尺寸在部分筆電上會高到超出畫面,
現在會依你的螢幕自動調整並置中。
## v0.18.62026-08-06
**建議更新**——如果你曾經丟了檔案進資料夾、卻完全看不出發生什麼事,這版會直接告訴你原因。
- 🔴 **讀不了的檔案,現在會當場說出來**:以前丟一份**舊版 Word.doc)、Keynote、Pages**
這類檔案進資料夾,小幫手會**默默跳過、一個字都不說**——你只看到「我丟了檔,然後什麼都沒發生」,
完全無從判斷是自己弄錯了、還是程式壞了。
現在首頁會直接列出**哪幾個檔讀不了、分別是什麼格式**,並告訴你不用重丟、支援之後會自動補上。
急著要的話,先用原本的軟體另存成 PDF 或 Word(.docx)放回同一個資料夾就行。
- 📄 目前讀得了的格式:Word.docx)、PowerPoint.pptx)、Excel.xlsx)、PDF、CSV、純文字、Markdown。
## v0.18.52026-08-05
**強烈建議更新**——如果你發現「語意搜尋什麼都搜不到」,或「同步明明做完了、首頁卻一直說等待中」,這版都修好了。
- 🔴 **語意搜尋搜不到東西,修好了**:上一版換了看得懂中文的新 AI 模型,但**分數門檻沒跟著調**,
結果把正確答案全部擋掉——新上傳的檔案一律 0 筆,只有標題幾乎一字不差的舊檔偶爾才中。
現在門檻改成跟著模型走,搜得到了。
(這是雲端的修復,請到 portal 設定頁按「檢查更新」。)
- 🔴 **首頁狀態不再說謊**:以前把檔案拖進資料夾,整個整理+上傳的過程首頁都顯示「等待中」,
做完了也看不出來。現在**整理中會真的顯示「同步中」**,做完會顯示「幾點整理了幾份」,
而且**不會過十幾秒就被清空**。
- 🍎 **Mac 安裝畫面補上拖曳版面**:打開 DMG 會看到 Arcrun 和「應用程式」左右並排的大圖示,
照著把左邊拖到右邊就裝好了。
(**請務必拖進「應用程式」再開**——直接在「下載」資料夾裡開,之後會更新不了。)
## v0.18.42026-08-05
**強烈建議更新**——如果你遇過「同步卡住不動」或「同一份檔案在知識庫裡出現很多次」,這版都修好了。
- 🔴 **一個檔案失敗不再拖住整個資料夾**:以前某個檔上傳失敗,小幫手會**不停重試同一個檔**
(實測撞了 1387 次、連續 11 小時),害其他檔全部排在後面。
現在失敗會**隔一段時間再試**(1 分鐘 → 5 分鐘 → 15 分鐘…),其他檔照常同步。
- 🔴 **同一份檔不再重複堆積**:以前每同步一次就在知識庫多存一份副本
(搜尋會出現 20 筆一模一樣的結果)。現在會先清掉舊的再存新的。
- 🔴 **刪掉的檔案真的從知識庫移除**:以前只是「標記為已刪除」但資料還留著,越積越多。
- 🎨 **全新桌面 App**:點小幫手圖示**直接開視窗**、每個知識庫**各自一頁**、可切換深/淺色。
- 🍎 Mac 改用 DMG 安裝(開啟後把 Arcrun 拖進「應用程式」再開)。
- 🪟 Windows 版同步更新(新介面 + 上述所有修復)。
> 💡 **雲端也要更新**:這版搭配的雲端修復包含「中文搜尋準確度大幅提升」
> (換了看得懂中文的 AI 模型)。請到 portal 設定頁按「檢查更新」。
## v0.15.72026-08-04
**建議更新**——如果你之前更新失敗過,這版修好了。
- 🔴 **修好「更新完又跳回舊版」**:以前只有把 App 放在「應用程式」資料夾才更新得了;
現在**放哪裡都能更新**
- 🍎 **Mac 改用 DMG 安裝**:打開就是「把 App 拖進應用程式」的標準畫面
- 🪟 **Windows 新增 MSIX**:可以像正式軟體那樣安裝
- App 更名為 `Arcrun`
## v0.15.62026-08-04
**強烈建議更新**——這版把「要申請 Gemini 金鑰」這道門檻整個拿掉了。
- 🔴 **整理檔案改用雲端 AI,不再需要任何 API 金鑰**
- 舊設定會自動切換過去;原本填的 Gemini 金鑰會保留,想用可以在「AI 設定…」切回去
- 整理速度快約 5 倍(約 3 秒/檔,之前約 17 秒)
- AI 問答也不再需要金鑰
## v0.15.52026-08-04
- 嘗試把預設改成雲端 AI(**但有 bug,實際沒生效**,請直接用 v0.15.6 以上)
## v0.15.42026-08-04
- 托盤點多下不再產生多個圖示
- 錯誤訊息改成白話,並指名去哪裡設定
## 更早的版本
封測初期版本,建議直接更新到最新版。
+1
View File
@@ -0,0 +1 @@
0.18
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env bash
# check-standalone.sh — 證明 `collector/` 沒有任何「往上伸」的相依(D95 第一輪②的驗收閘)
#
# 🔴 leo 2026-08-17:「身為管理者,你要從頭到尾**不要有很多扭曲**,因為你根本不記得
# 你做的這些扭曲,**每次都要查**,很直接,源碼、產出物,從 stage 到 prod。」
#
# ── 為什麼主證明不是 grep ──────────────────────────────────────────────────
# grep 只證明「我想得到的那幾種寫法沒出現」,證明不了「真的自足」。
# 少想到一種寫法(`$(dirname $0)/../../..`、環境變數繞路、執行期才拼出來的路徑…)
# 就會拿到一個**假綠**——而假綠比紅還糟,因為它讓人停止檢查。
#
# ⇒ **② 才是判準**:把 collector/ 的檔案單獨複製到一個臨時目錄
# (那裡**沒有** docs-siteinstallerlandingrepo 根的任何東西,
# 也不是原 repo 的子目錄,`git rev-parse --show-toplevel` 只會指到它自己),
# 在那裡跑真正的版本計算與打包前置閘。
# **跑得起來=真的自足**;有任何一條偷偷伸手到 repo 根,在那裡就會當場斷掉。
#
# ① 只是**快篩**,抓兩個「出現在可執行位置就一定是錯」的字樣,讓回歸早一步紅。
#
# ⚠️ 快篩必須避開一個假警報:collector 是**處理使用者知識庫**的程式,
# 它的原始碼與測試裡到處是 `system-dev/wiki/...` 這種字串——那是**使用者 vault 裡的路徑**,
# 不是本 repo 的目錄。拿目錄名當樣式掃會掃出上百筆雜訊,然後沒有人再看這份輸出。
# ⇒ 快篩只認語法構造(`git rev-parse --show-toplevel``REPO_ROOT`),且**先剝掉註解與說明字串**。
#
# 用法:
# bash collector/check-standalone.sh # 全部檢查
# KEEP=1 bash collector/check-standalone.sh # 保留臨時目錄以便查看
set -uo pipefail
cd "$(dirname "$0")"
COLLECTOR="$(pwd)"
FAIL=0
ok(){ printf " ✅ %s\n" "$1"; }
ng(){ printf " ❌ %s\n" "$1"; FAIL=1; }
# 受控檔案清單(含未提交、排除 gitignore)——與 daemon-version.py 算指紋的口徑一致。
files_list() { git -C "$COLLECTOR" ls-files -co --exclude-standard . ; }
echo "━━━ ① 快篩:可執行位置不得出現往 repo 根定位的構造 ━━━"
HITS="$(files_list | python3 -c '
import io, sys, tokenize, re
from pathlib import Path
BAD = [
(re.compile(r"rev-parse\b[^|;)]*--show-toplevel"), "git rev-parse --show-toplevel"),
(re.compile(r"\bREPO_ROOT\b"), "REPO_ROOT"),
]
SELF = {"check-standalone.sh", "CHANGELOG.md"}
def strip_py(src):
"""用 tokenize 剝掉 Python 的註解與獨立字串(docstring)——說明文字裡提到不算違規。"""
out = {}
try:
toks = list(tokenize.generate_tokens(io.StringIO(src).readline))
except Exception:
return {i + 1: l for i, l in enumerate(src.split("\n"))}
for t in toks:
if t.type in (tokenize.COMMENT, tokenize.STRING, tokenize.NL, tokenize.NEWLINE):
continue
out.setdefault(t.start[0], "")
out[t.start[0]] += t.string + " "
return out
def strip_hash(src):
"""`#` / `//` / ` * ` 行註解剝掉(sh、js、mjs、go)。"""
out = {}
for i, l in enumerate(src.split("\n"), 1):
s = l.split("#", 1)[0] if l.lstrip().startswith("#") else l
if s.lstrip().startswith(("//", "*", "/*")):
s = ""
s = re.sub(r"#.*$", "", s) if l.lstrip().startswith("#") else s
out[i] = s
return out
for rel in (x.strip() for x in sys.stdin):
if not rel or Path(rel).name in SELF:
continue
p = Path(rel)
if not p.is_file() or p.suffix in (".png", ".ico", ".sum", ".md", ".json"):
continue
try:
src = p.read_text(errors="ignore")
except Exception:
continue
lines = strip_py(src) if p.suffix == ".py" else strip_hash(src)
for n, code in lines.items():
for rx, why in BAD:
if rx.search(code):
print(f"{rel}:{n}: [{why}] {code.strip()[:100]}")
')"
if [ -n "$HITS" ]; then
ng "有往 repo 根定位的構造(下列都在可執行位置,不是註解)"
printf '%s\n' "$HITS" | sed 's/^/ /'
else
ok "沒有 git rev-parse --show-toplevel、沒有 \$REPO_ROOT"
fi
echo
echo "━━━ ② 行為證明:把 collector/ 單獨搬出去,看它還算不算得出自己的版本 ━━━"
TMP="$(mktemp -d)"
trap '[ "${KEEP:-}" = "1" ] || rm -rf "$TMP"' EXIT
DEST="$TMP/collector-standalone"
mkdir -p "$DEST"
# 只複製受控檔案——build 產物、node_modules、dist 一律不帶(它們本來就不該影響自足性)
N=0
while IFS= read -r f; do
[ -f "$COLLECTOR/$f" ] || continue
mkdir -p "$DEST/$(dirname "$f")"
cp "$COLLECTOR/$f" "$DEST/$f"
N=$((N+1))
done < <(files_list)
echo " ️ 臨時樹:$DEST$N 個檔,不在原 repo 底下)"
# 這裡是全新的 git repo——`git rev-parse --show-toplevel` 只會指到 $DEST 自己,
# 任何原本靠 repo 根才拿得到的東西都會在這裡消失。
git -C "$DEST" init -q
git -C "$DEST" add -A
git -C "$DEST" -c user.email=x@y -c user.name=z commit -qm standalone
APP="$DEST/cmd/arcrun-app"
VER="$("$APP/daemon-version.py" 2>/dev/null)"
if [ -n "$VER" ]; then ok "daemon-version.py 在獨立樹裡算得出版本:$VER"
else ng "daemon-version.py 在獨立樹裡算不出版本(還有東西往上伸):$("$APP/daemon-version.py" 2>&1 | head -2)"; fi
FP="$(cd "$APP" && python3 -c "
import importlib.util
s=importlib.util.spec_from_file_location('dv','daemon-version.py')
m=importlib.util.module_from_spec(s); s.loader.exec_module(m)
print(m.source_fingerprint())" 2>/dev/null)"
[ -n "$FP" ] && ok "原始碼指紋算得出來:$FP" || ng "原始碼指紋算不出來"
if (cd "$APP" && ./changelog-section.sh "$VER" --check >/dev/null 2>&1); then
ok "changelog-section.sh --check 通過(三支 build 腳本靠它擋『忘了寫更新內容』)"
else
ng "changelog-section.sh --check 失敗:$(cd "$APP" && ./changelog-section.sh "$VER" --check 2>&1 | head -2)"
fi
NOTES="$(cd "$APP" && ./changelog-section.sh "$VER" 2>/dev/null | head -1)"
[ -n "$NOTES" ] && ok "更新說明投影得出來:${NOTES:0:36}" || ng "更新說明投影不出來(daemon-notes.mjs 還在往上找)"
echo
[ "$FAIL" = 0 ] && echo "✅ collector/ 自足:搬出去照樣算得出自己的版本號" \
|| echo "❌ collector/ 還有往上伸的相依(見上)"
exit "$FAIL"
+2 -1
View File
@@ -14,7 +14,8 @@ VERSION="${VERSION:-$(./daemon-version.py --stamp)}"
# 🔴 2026-08-06 機械閘:這一版的「更新內容」沒寫進 changelog ⇒ 不准打包。
# leo:「給版本號、本版更新內容、打包產品、顯示在前端,這一整串都應該是機械化」。
# 單一真相源=docs-site/.../help/changelog.mdmanifest.notes 與前端全部投影自它。
# 單一真相源=collector/CHANGELOG.mdmanifest.notes 與前端全部投影自它。
# 2026-08-18 D95 第一輪:從 docs-site 搬進 collector/daemon 才算得出自己的版本。)
# 理由與格式見 changelog-section.sh。
./changelog-section.sh "$VERSION" --check
+19 -2
View File
@@ -79,8 +79,25 @@ echo "🏷 msix 版本:${MSIX_VERSION}(來源 ${VERSION_RAW}"
# 我卻每次都當成「只有他有」而開口問。**腳本自己去讀,不要再問人。**
# (對應鐵律:`system-dev/wiki/credentials-map.md` —— 用任何金鑰前先查那張表;
# 查得到就自己拿,不要說「我沒有」。)
ENV_FILE="${ENV_FILE:-$(cd "$(dirname "$0")" && git rev-parse --show-toplevel 2>/dev/null)/../../.env}"
if [[ -z "${IDENTITY_NAME:-}" && -f "$ENV_FILE" ]]; then
#
# 🔴 2026-08-18D95 第一輪):原本是 `git rev-parse --show-toplevel`/../../.env
# ——那是**用本 repo 在磁碟上的位置**去推 InkStoneCo 頂層,
# collector/ 一旦搬成獨立 repo(或只是被 clone 到別處)就指到不存在的路徑。
# 改成**往上找那個帶著這把鍵的 .env**:leo 08-06 的要求原樣保留
# (腳本自己去讀、不再問人,金鑰的家仍是頂層那一份),
# 但**不再依賴目錄結構長什麼樣**。找不到就照舊落到 PLACEHOLDER 並印警告,
# 不會安靜出一顆送不了審的 msix。
if [[ -z "${ENV_FILE:-}" ]]; then
_d="$(pwd)" # 腳本開頭已 cd 到自己的目錄
while [[ "$_d" != "/" ]]; do
if [[ -f "$_d/.env" ]] && grep -qE '^MS_STORE_ARCRUN_APP_NAME=' "$_d/.env" 2>/dev/null; then
ENV_FILE="$_d/.env"; break
fi
_d="$(dirname "$_d")"
done
fi
ENV_FILE="${ENV_FILE:-}"
if [[ -z "${IDENTITY_NAME:-}" && -n "$ENV_FILE" && -f "$ENV_FILE" ]]; then
# 只取這三個鍵,不把整包 .env 灌進環境(其他都是不該碰的金鑰)
IDENTITY_NAME="$(grep -E '^MS_STORE_ARCRUN_APP_NAME=' "$ENV_FILE" | cut -d= -f2-)"
PUBLISHER="$(grep -E '^MS_STORE_ARCRUN_APP_PUBLISHER=' "$ENV_FILE" | cut -d= -f2-)"
+3 -2
View File
@@ -23,7 +23,7 @@ export PATH="$PATH:$(go env GOPATH)/bin"
# 🔴 2026-08-06:版本號**不再手打、也不再靠 git describe**。
# 舊寫法 `git describe --tags` 在這個 repo 永遠拿不到 v0.18.x(一個 tag 都沒有)
# ⇒ 版本只能靠打包時人工 `VERSION=v0.18.4 ./build-win.sh` 敲 ⇒ manifest 說謊的病根。
# 現在由 daemon-version.py 從 DAEMON_LINE + collector/ 的 commit 數算出來,
# 現在由 daemon-version.py 從 collector/DAEMON_LINE + collector/CHANGELOG.md 算出來,
# 兩條打包線(win/mac)在同一個 commit 上必得同一個號碼。理由全文見該腳本。
VERSION="${VERSION:-$(./daemon-version.py --stamp)}"
BUILD_TIME="$(date '+%Y%m%d-%H%M')"
@@ -31,7 +31,8 @@ echo "🏷 版本:${VERSION}build ${BUILD_TIME}"
# 🔴 2026-08-06 機械閘:這一版的「更新內容」沒寫進 changelog ⇒ 不准打包。
# leo:「給版本號、本版更新內容、打包產品、顯示在前端,這一整串都應該是機械化」。
# 單一真相源=docs-site/.../help/changelog.mdmanifest.notes 與前端全部投影自它。
# 單一真相源=collector/CHANGELOG.mdmanifest.notes 與前端全部投影自它。
# 2026-08-18 D95 第一輪:從 docs-site 搬進 collector/daemon 才算得出自己的版本。)
# 理由與格式見 changelog-section.sh。
./changelog-section.sh "$VERSION" --check
+13 -6
View File
@@ -5,7 +5,11 @@
#
# 答:改的是「**寫在哪**」,不是「誰來寫」。
# 文字一定得人寫(機器編不出「語意搜尋什麼都搜不到」這種人話),
# 但**只准寫一個地方** docs-site/src/content/docs/help/changelog.md(用戶語言那份)。
# 但**只准寫一個地方** collector/CHANGELOG.md(用戶語言那份)。
# 🔴 2026-08-18(D95 第一輪):這份檔案原本住在 repo 根的
# `docs-site/src/content/docs/help/changelog.md`,而且**同時裝著兩條版本線**
# (桌面版 v0.18.x + 雲端引擎 1.4.x)。已拆開:桌面版的搬進 `collector/`
# 雲端那條原地不動。理由:daemon 得往上伸手才拿得到自己的版本號=搬不成獨立 repo。
# 其餘全是投影、不准手改(=頂層 principles「投影不可手改,要改去改源頭」):
# · manifest.daemon.notes ← 打包時由本腳本抽出
# · docs-site 的「版本說明」頁 ← 本來就是它自己
@@ -20,8 +24,10 @@
# ./changelog-section.sh v0.18.7 --check # 只檢查存不存在
set -euo pipefail
cd "$(dirname "$0")"
REPO_ROOT="$(git rev-parse --show-toplevel)"
FILE="$REPO_ROOT/docs-site/src/content/docs/help/changelog.md"
# 🔴 一律以**自己的位置**定位(往上兩層=collector/),不問 git、不問 repo 根——
# `git rev-parse --show-toplevel` 就是往上伸手的入口,剪掉它才搬得成獨立 repo。
COLLECTOR="$(cd ../.. && pwd)"
FILE="$COLLECTOR/CHANGELOG.md"
VERSION="${1:?用法:changelog-section.sh <版本,例 v0.18.7> [--check]}"
CHECK_ONLY="${2:-}"
@@ -39,7 +45,7 @@ if [ -z "$(printf '%s' "$SECTION" | tr -d '[:space:]')" ]; then
cat >&2 <<MSG
❌ changelog 裡沒有 ${VERSION} 的段落,不准打包。
請在 docs-site/src/content/docs/help/changelog.md 最上面加一段:
請在 collector/CHANGELOG.md 最上面加一段:
## ${VERSION}$(date '+%Y-%m-%d')
@@ -55,7 +61,8 @@ fi
[ "$CHECK_ONLY" = "--check" ] && { echo " ✅ changelog 有 ${VERSION} 的段落"; exit 0; }
# 給 manifest.daemon.notes 用的投影——**只有一個投影器**:installer/scripts/daemon-notes.mjs
# 給 manifest.daemon.notes 用的投影——**只有一個投影器**:同目錄的 daemon-notes.mjs
# 2026-08-18 從 installer/scripts/ 搬來;那邊只剩一層薄殼轉呼叫本目錄這支。)
#
# 🔴 2026-08-08 修(leo 真機看到 v0.18.24 的更新畫面):
# leo 原話「**不要這麼長的散文,簡短講改了什麼,細節去 docs 讀。**」
@@ -71,4 +78,4 @@ fi
# 現在改成委派給那唯一的投影器(只取每條的粗體標題、串成一行、超長就截並導去 docs),
# 而 `installer/scripts/ship.mjs` 的 notes 步驟會在每次出貨時自動套用它。
# ⇒ 兩邊同一份規則,不會漂移。上面的 --check 閘原樣保留(build-*.sh 仍靠它)。
exec node "$REPO_ROOT/installer/scripts/daemon-notes.mjs" "$VERSION"
exec node ./daemon-notes.mjs "$VERSION"
+134
View File
@@ -0,0 +1,134 @@
/**
* daemon-notes.mjs — 使用者在「版本與更新」畫面看到的那一行,**由 changelog 機械導出**
*
* ── 這支解什麼病(leo 2026-08-08 真機看到 v0.18.24 的更新畫面)────────────
* leo 原話:「**不要這麼長的散文,簡短講改了什麼,細節去 docs 讀。**」
*
* 他看到的是**一整面文字牆**,而且 `**粗體**` 原樣露在畫面上。
* 真兇不是文案沒寫好,是**出貨當下靠人手工排版**:
* 出貨時用一段臨時 python 把 changelog 的換行折掉塞進 `manifest.daemon.notes`
* 於是四條變成一大段;那段轉換每次出貨都要重寫一次,而且**沒人檢查結果長什麼樣**。
*
* ⇒ 與「版本號由內容算」同一種解法:**這一行也由單一真相源導出,不由人當場捏**。
*
* ── 為什麼是「只取粗體標題、串成一行」──────────────────────────────────
* 畫面那個欄位是**純文字**`main.js:215` 是 `<div class="d">${esc(u.notes)}</div>`
* ① `esc()` ⇒ 任何 markdown 符號都會原樣露出來(leo 看到的 `**` 就是這樣來的)
* ② HTML 不保留換行(`.d` 沒有 white-space:pre)⇒ 塞 `\n` 進去也**不會**變成分行
* ⇒ 唯一能讀的形狀就是**一行短句**。而 changelog 每條的 `**粗體標題**` 本來就是
* 那條的一句話摘要——直接拿它,不必另外維護第二份文案(第二份必然漂移)。
*
* ── 🔴 2026-08-18D95 第一輪):搬進 collector/,且不再往上伸手 ──────────
* 本檔原本住在 `installer/scripts/`,讀的是 repo 根的 `docs-site/.../changelog.md`。
* 那讓 **daemon 的更新說明投影器住在 daemon 之外**,而它讀的檔也在 daemon 之外
* ⇒ `collector/` 沒辦法自己交出「這一版對用戶意味什麼」這句話。
*
* 現在:實作住這裡,讀的是**同一棵樹裡的** `collector/CHANGELOG.md`(自我定位,不問 git)。
* `installer/scripts/daemon-notes.mjs` 變成薄殼,轉呼叫本檔——
* **根可以往內伸手,collector 不可以往外伸手**,方向是單向的。
*
* 用法(collector 內部):
* import { notesForVersion } from './daemon-notes.mjs';
* notesForVersion('v0.18.24') // → 一行字,或 nullchangelog 沒這版)
*/
import { readFileSync, existsSync } from 'node:fs';
import { join, resolve } from 'node:path';
/** daemon 的 changelog`collector/CHANGELOG.md`。由本檔位置往上兩層推出來,不問 git、不問 repo 根。 */
export const CHANGELOG_PATH = join(import.meta.dirname, '..', '..', 'CHANGELOG.md');
/** 畫面上一行讀得完的上限。超過就截,並改叫使用者去看說明文件。 */
export const NOTES_MAX = 100;
const TAIL = '(細節見說明文件)';
/** 把 markdown 行內語法剝成純文字——畫面不渲染 markdown,留著就是雜訊。 */
export function stripMarkdown(s) {
return String(s)
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') // [字](連結) → 字
.replace(/[*_`]+/g, '') // 粗體/斜體/行內碼標記
.replace(/\s+/g, ' ') // 換行與連續空白 → 單一空白
.trim();
}
/**
* 從 changelog 取某一版的「一句話摘要」清單。
* 規則:只認**頂層條目**(行首 `- `)的第一個粗體片段——那就是該條的標題。
* 沒有粗體的條目退而取整行(截短),因為「有寫總比漏掉好」。
*/
export function headlinesFor(changelogText, version) {
const lines = changelogText.split('\n');
const start = lines.findIndex((l) => new RegExp(`^##\\s+${version.replace(/[.\\]/g, '\\$&')}(\\D|$)`).test(l.trim()));
if (start < 0) return null;
const out = [];
for (let i = start + 1; i < lines.length; i++) {
const l = lines[i];
if (/^##\s/.test(l)) break; // 下一版開始
if (!/^-\s/.test(l)) continue; // 只取頂層條目(續行、巢狀一律略過)
const body = l.replace(/^-\s*/, '');
const bold = body.match(/\*\*([^*]+)\*\*/);
let h = stripMarkdown(bold ? bold[1] : body);
h = h.replace(/^[^\p{L}\p{N}「((]+/u, ''); // 去掉開頭的 emoji/符號
h = h.replace(/[:,。.]+$/, ''); // 去掉尾標點(要串接)
if (h) out.push(h);
}
return out;
}
/**
* 組成畫面上那一行。回傳 null=changelog 裡沒有這一版(呼叫端該當成錯誤)。
* `changelogPath` 只給測試/薄殼覆寫用;正常呼叫不帶,走 collector 自己的 CHANGELOG.md。
*/
export function notesForVersion(version, changelogPath = CHANGELOG_PATH) {
if (!existsSync(changelogPath)) return null;
const heads = headlinesFor(readFileSync(changelogPath, 'utf8'), version);
if (!heads || !heads.length) return null;
let line = heads.join('・');
if (line.length > NOTES_MAX) {
// 截到「最後一個完整條目」為止,再掛尾巴——不要把句子切一半給使用者看。
const kept = [];
for (const h of heads) {
if ([...kept, h].join('・').length + TAIL.length > NOTES_MAX) break;
kept.push(h);
}
line = (kept.length ? kept.join('・') : heads[0].slice(0, NOTES_MAX - TAIL.length)) + TAIL;
}
return line;
}
/**
* 機械閘用:這一行本身可不可以送到使用者眼前?
* 回傳問題清單(空=通過)。這道閘存在的理由=**手寫的那一行沒有任何人檢查**。
*/
export function checkNotes(notes) {
const problems = [];
const s = String(notes ?? '');
if (!s.trim()) return ['manifest.daemon.notes 是空的——使用者按「檢查更新」看不到這版改了什麼'];
if (/[*_`#]|\]\(/.test(s)) {
problems.push(`manifest.daemon.notes 裡有 markdown 符號,畫面是純文字會原樣露出來:${JSON.stringify(s.slice(0, 60))}`);
}
if (/\n/.test(s)) {
problems.push('manifest.daemon.notes 有換行——畫面不保留換行(.d 沒有 white-space:pre),會擠成一坨');
}
if (s.length > NOTES_MAX + TAIL.length) {
problems.push(`manifest.daemon.notes 太長(${s.length} 字,上限 ${NOTES_MAX + TAIL.length})——leo 08-08:「不要這麼長的散文,簡短講改了什麼,細節去 docs 讀」`);
}
return problems;
}
// CLI:印出某版會顯示的那一行(出貨前想先看一眼時用)
// 🔴 2026-08-18:判斷「是不是直接跑本檔」要比**絕對路徑**,不能比檔名尾綴——
// `installer/scripts/daemon-notes.mjs` 薄殼同名,用尾綴比會讓兩支 CLI 一起開火
// (實撞:問雲端版號時本檔先 process.exit(1),薄殼根本沒機會查 docs-site)。
if (process.argv[1] && resolve(process.argv[1]) === import.meta.filename) {
const v = process.argv[2];
if (!v) { console.error('用法:node collector/cmd/arcrun-app/daemon-notes.mjs <版本,例 v0.18.24>'); process.exit(2); }
const line = notesForVersion(v);
if (!line) { console.error(`❌ changelog 沒有 ${v} 這一版(${CHANGELOG_PATH}`); process.exit(1); }
// stdout **只有那一行**——它會被別的腳本(changelog-section.sh)直接取用,
// 多印一個字就會被塞進 manifest。其餘一律走 stderr。
console.log(line);
console.error(`${line.length} 字)`);
const probs = checkNotes(line);
if (probs.length) { probs.forEach((p) => console.error('❌ ' + p)); process.exit(1); }
console.error('✅ 可以送到使用者眼前');
}
+37 -11
View File
@@ -15,8 +15,24 @@
但**每個 commit 都變成一版**(一天 5 個 commit 就跳 5 版),
而且每跳一版就要補一段 changelog——把機械化變成新的手工活。
── 🔴 2026-08-18(D95 第一輪):所有輸入都搬進 collector/ 了 ────────────
leo 08-17:「身為管理者,你要從頭到尾**不要有很多扭曲**,因為你根本不記得
你做的這些扭曲,**每次都要查**,很直接,源碼、產出物,從 stage 到 prod。」
本腳本原本往 **repo 根**伸手拿三樣東西:
① docs-site/src/content/docs/help/changelog.md(版本與更新說明)
② ROOT/DAEMON_LINE(版本線)
③ `git ls-files collector`(原始碼指紋,從 repo 根算)
⇒ `collector/` 算不出自己的版本,也**沒有資格被搬成獨立 repo**。
現在三樣全在 `collector/` 底下:`CHANGELOG.md``DAEMON_LINE`/指紋以 collector 為根。
本檔一律以**自己的位置**定位(`__file__` 往上兩層=collector/),
**不再呼叫 `git rev-parse --show-toplevel`**——那是往上伸手的入口。
(三道既有修補全部原樣保留:指紋演算法版本作廢重記、帳本自我參照排除、
沒有產物的版號可重戳。只有「以什麼為根」變了。)
── 現在的做法:版本由 changelog 決定,且不用手打數字 ──────────────
單一真相源= docs-site/src/content/docs/help/changelog.md(用戶語言那份)。
單一真相源= collector/CHANGELOG.md(用戶語言那份)。
· 要出新版 ⇒ 在檔案最上面加一段標題 `## 下一版(未發佈)`,底下寫白話更新內容。
· 打包時本腳本把它**戳成正式版號**(上一版 patch + 1)並補上今天日期。
@@ -40,17 +56,21 @@ import subprocess
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
ROOT = Path(subprocess.check_output(
["git", "rev-parse", "--show-toplevel"], cwd=HERE, text=True).strip())
CHANGELOG = ROOT / "docs-site/src/content/docs/help/changelog.md"
LINE_FILE = ROOT / "DAEMON_LINE"
HERE = Path(__file__).resolve().parent # collector/cmd/arcrun-app
# 🔴 daemon 的根=`collector/`,用**自己的位置**推出來,不問 git 也不問 repo 根。
# 這一行就是「臍帶剪掉了」的本體:往上兩層剛好是 collector/,再往上一步都不走。
COLLECTOR = HERE.parents[1]
CHANGELOG = COLLECTOR / "CHANGELOG.md"
LINE_FILE = COLLECTOR / "DAEMON_LINE"
UNRELEASED = "## 下一版(未發佈)"
# 版本 → 當時原始碼指紋。用來擋「同一個版號、不同的執行檔」。
SOURCE_LOCK = HERE / ".version-source.json"
# 指紋演算法版本:改算法時 +1,帳本會自動作廢重記(見 check_or_record)。
FINGERPRINT_ALGO = 3
# 🔴 3 → 42026-08-18):指紋的**根**從 repo 根換成 collector/,被雜湊的相對路徑
# 因此全部改變(`collector/cmd/…` → `cmd/…`)⇒ 舊帳本是「用不同單位量出來的數字」,
# 比對沒有意義。照既有設計改號讓它自動整本作廢重記,不要手改 JSON。
FINGERPRINT_ALGO = 4
RELEASED_RE = re.compile(r"^## v(\d+)\.(\d+)\.(\d+)", re.M)
@@ -72,11 +92,17 @@ def source_fingerprint():
會給出不同指紋、即使內容一模一樣——戳版號時檔案還沒提交(在 diff 裡),
提交後樹變了、diff 空了。結果只改 wiki 也被判「版號已對應另一份原始碼」。
改成對工作區檔案內容取值,與有沒有提交無關。
四修(2026-08-18D95 第一輪):
改成**以 `collector/` 為根**列檔(cwd=COLLECTOR、pathspec `.`),
不再從 repo 根列 `collector` 這個子目錄。涵蓋的檔案集合完全相同,
差別只有相對路徑前綴——而路徑有進雜湊,所以 FINGERPRINT_ALGO 跟著 +1。
這樣 collector/ 搬成獨立 repo 之後,同一段程式碼算出的仍是同一個值。
"""
try:
files = subprocess.check_output(
["git", "ls-files", "-co", "--exclude-standard", "collector"],
cwd=ROOT, text=True).split("\n")
["git", "ls-files", "-co", "--exclude-standard", "."],
cwd=COLLECTOR, text=True).split("\n")
except subprocess.CalledProcessError:
return "" # 不在 git 裡就不擋(例如從 tarball 解出來 build
# 🔴 2026-08-06 三修:**帳本自己不能算進指紋**(經典的自我參照)。
@@ -86,11 +112,11 @@ def source_fingerprint():
# dist/ 也早就 gitignore,不在名單裡。真兇只有帳本。)
# 只排除帳本一個檔——**不要順手把 build/ 整個排掉**,
# 那會讓「換 app icon」不算原始碼變更,等於把閘挖個洞。
skip = {str(SOURCE_LOCK.relative_to(ROOT))}
skip = {str(SOURCE_LOCK.relative_to(COLLECTOR))}
h = hashlib.sha256()
for rel in sorted(f for f in files if f.strip() and f not in skip):
fp = ROOT / rel
fp = COLLECTOR / rel
if not fp.is_file():
continue # 已刪除的檔案
h.update(rel.encode())