3af210e03a
leo 2026-08-17:「這個頁面刪除。」 (同一件事 08-09 就講過:「不要同步,docs 的版本說明直接連回 github 的版本發佈」, 那時記在該檔檔頭當待辦,理由是「等 releases 累積出足夠版本歷史」——現有 9 筆,理由失效。) 使用者要看版本紀錄 → github.com/youlinhsieh/arcrun-rag/releases。文件站不再自己維護一份。 ── 🔴 動手前發現的事:那一頁不只是一頁 ────────────────────────────── `docs-site/src/content/docs/help/changelog.md` **同時是兩個東西**——文件站的一頁, 以及**雲端引擎 `1.4.x` 的出貨原稿**(`installer/scripts/daemon-notes.mjs` 的 `CHANGELOG_REL`, 被 `ship.mjs` 的 docs-changelog/release-record 與 `github-release.mjs` 讀)。 直接 `rm` 會讓雲端那條線的 GitHub/Gitea 版本發佈點進去變空白。 ⇒ 頁面刪掉,**原稿搬去 repo 根的 `CHANGELOG.md`**(旁邊就是根的 `RELEASE_LINE`), 形狀與桌面版那條線一致:`collector/CHANGELOG.md` + `collector/DAEMON_LINE`。 `collector/CHANGELOG.md` 一個字都沒動。 ── 投影機制整套拆掉(0fb72ae,D95 第四輪,昨天才併進 main)────────── 它存在的唯一理由是把 daemon 段落渲染回這一頁。頁面沒了,它就是沒人用的機制: · `docs-site/remark-daemon-changelog.mjs`(整支) · `astro.config.mjs` 的 import 與 `markdown.remarkPlugins` · `package.json` build 的 `--force`(那是投影的必要條件,不是通用旗標) · `collector/.../daemon-notes.mjs` 的 `releasedSections()`/`releasedSectionsFromFile()`/`RELEASED_HEADING` · `collector/.../daemon-notes.test.mjs`(4 支全是投影的演練) `daemon-notes.mjs` 其餘匯出(notesForVersion/checkNotes…)是出貨線在用的,留著。 ── 舊網址留一條轉址,不直接 404 ────────────────────────────────── 那個網址掛在側欄上、也印在 landing「這一版改了什麼」旁邊,已隨 landing 部署到使用者 瀏覽器裡。repo 內的連結本輪都改成直接指 GitHub(landing/docs 首頁/側欄), 所以轉址不服務任何內部連結,只接書籤與舊 HTML。它是 `astro.config.mjs` 的一行宣告 (沒有程式、沒有真相源可以漂),而且被 verify-docs 每次出貨夾住 ⇒ 不是要維護的機制。 ── ⚠️ 一次真的降級,標在這裡不藏 ──────────────────────────────── `verify-docs.mjs` 原本斷言「線上那一頁有這兩個版號」——沒有頁面就沒有東西可查。 改成斷言「那條轉址還在」(=線上這顆確實是這份原始碼建的)。 **舊斷言抓得到「內容停在上一版」,新斷言抓不到。** 差額由 `release-record` 站承接: 每條版本線都要有一筆版本發佈、內文抽不到就中止——那一站有牙齒且更早跑。 `docs` 站的 ① 那道「這一版寫了嗎」照舊每個目標都問(D65:不准跳)。 實測 · 建置產物:`1.4.4x`/`v0.18.2x` 命中 0 個檔;`dist/help/changelog/index.html` =轉址頁(meta refresh+canonical+a href 全指 releases) · 兩種網址(astro preview):`/docs/help/changelog` 與 `…/changelog/` 皆 200, 內容同一份轉址頁,版本號命中各 0 · 站內死連結:指向該網址的 href 0 條;pagefind 11 個 fragment 含 changelog 者 0;sitemap 未列 · 產生鏈完整:`1.4.47`→根 CHANGELOG.md(摘要+4 行內文)/`v0.18.29`→collector/CHANGELOG.md (摘要+6 行內文)/`1.4.29`→根(6 行);不存在的版號回 null · `node --test installer/scripts/*.test.mjs` 233 支全綠 (238→233:-4 投影演練、-1 htmlText,兩者的實作都已移除) · 建置那句 `/404.htmlEntry docs → 404 was not found.` 是既有行為—— 拿掉 redirects 重建仍出現,與本輪無關 Refs: inkstone/arcrun-rag#41 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
135 lines
7.4 KiB
JavaScript
135 lines
7.4 KiB
JavaScript
/**
|
||
* 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-18(D95 第一輪):搬進 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') // → 一行字,或 null(changelog 沒這版)
|
||
*/
|
||
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('✅ 可以送到使用者眼前');
|
||
}
|