dsh-plugin-update 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  格式为 Keep-a-Changelog 子集:`Added/Fixed/Changed` 必写,`Deprecated/Removed/Security` 有则透传,`Unreleased` 面板忽略,禁止 git-log 直倒。面板缺日志时中性提示,不挡安装。
4
4
 
5
+ ## [0.5.1] - 2026-10-06
6
+
7
+ ### Added
8
+ - 入口件 `openOn: 'direct'`:点开即弹窗、不预查(面板挂载即自查;徽标形态仍走回调口径)。
9
+
10
+ ### Fixed
11
+ - 入口件小字对比度:原地提示与档案卷按钮脸自带底,深色宿主下也读得出。
12
+
5
13
  ## [0.5.0] - 2026-10-05
6
14
 
7
15
  ### Added
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  给 DSH 插件加「检查更新 / 安装更新」能力的 npm 包。宿主侧一段接线,面板侧挂一个现成组件,装不上时给用户一条可复制的手工命令。
4
4
 
5
- 要求 Node 22 或更高,零运行时依赖。当前版本 `0.5.0`。
5
+ 要求 Node 22 或更高,零运行时依赖。当前版本 `0.5.1`。
6
6
 
7
7
  装上它你会拿到六样东西:
8
8
 
@@ -181,10 +181,12 @@ const entry = mountUpdateEntry(document.getElementById('upd-entry'), {
181
181
  |---|---|---|---|
182
182
  | 摆什么 | `button` / `badge` / `inline` | `button` | 按钮;只给一个状态点;面板本体直接嵌进来 |
183
183
  | 什么时候查 | `mount` / `never` | `mount` | 进页面静默查一次(**只调 `.updateStatus`,只读**);`never` 则只在点击时查 |
184
- | 点了做什么 | `has-update` / `always` / `manual` | `has-update` | 有新版才开面板;总是开;交给 `onActivate` 自己跳 |
184
+ | 点了做什么 | `has-update` / `always` / `manual` / `direct` | `has-update` | 有新版才开面板;检查完总是开;交给 `onActivate` 自己跳;点开即弹窗、不预查 |
185
185
 
186
186
  按钮文案随状态自己变:`检查更新` / `有新版 1.1.0` / `正在安装…` / `待重启` / `更新失败,点此查看`。
187
187
 
188
+ 无新版时原地那句小字(`已是最新 X.Y.Z`)只在 `has-update` 下出现:不想看它就用 `openOn: 'always'`(检查完总是开弹窗,无新版在弹窗里看“已是最新”)或 `openOn: 'direct'`(点开即弹窗,连预查都省了,面板挂载即自查;徽标形态仍走回调口径)。
189
+
188
190
  **一条铁律:检查是只读、安装是写入,两者不许合并成一个动作。** 入口件永远只做「查 + 打开面板」,
189
191
  任何路径都不自动安装;用户必须在面板里明确点「安装」。想让点击交给自己(例如你已有自己的更新页):
190
192
 
@@ -302,7 +304,7 @@ const panel = mountUpdatePanel(document.getElementById('update-slot'), {
302
304
 
303
305
  宿主种类与安装出口都由本包自己探测和选择,**升级依赖即可,宿主侧与面板侧都不用改代码**:
304
306
 
305
- 1. 依赖版本提到 `^0.5.0`。
307
+ 1. 依赖版本提到 `^0.5.1`。
306
308
  2. 重新装 / 发一版你自己的插件,让新依赖进当前使用范围(运行时用的是 `node_modules` 里那份)。
307
309
  3. 如果你自己接过一版宿主安装出口,把它删掉——它会挡在本包的路由前面。
308
310
 
@@ -9,6 +9,11 @@
9
9
  * - 已装版离线读:`node_modules/<目标包>/CHANGELOG.md`(与版本 pin 绑定,零新增联网);
10
10
  * - 新版按需取:新版 tarball 内同名文件,复用官方源 + integrity 校验;
11
11
  * 取不到一律回 null(调用方渲染中性提示,不挡安装、不写 blockedReason)。
12
+ *
13
+ * 记住策略(#41 终裁,三墙不动,只澄清注释):成功与取不到(null)按版本永久记
14
+ * (面板内存会话级 + 宿主进程级,不落盘,显式文本永不覆盖);传输失败不进缓存,
15
+ * 手动查新版/换版/重开面板立即重问,轮询按退避问(三处共用 shouldFetchChangelog,各存各的)。
16
+ * 三墙(8MB 压缩态/32MB 解压后/64K 日志文本)一字不动,超限回 null 由调用方按取不到记住。
12
17
  */
13
18
  /** tarball 下载上限(字节,压缩态):超限直接回落中性提示,不解包、不OOM。 */
14
19
  export declare const CHANGELOG_TARBALL_MAX_BYTES: number;
@@ -9,14 +9,16 @@
9
9
  * 解析只做字符串切分,不读盘、不联网;版本语义与核心同口径
10
10
  * (validReleaseVersion / compareReleaseVersions,stable 纯三段、prerelease 显式)。
11
11
  *
12
- * 面板约定(Question 原文逐字落实):
13
- * - `Added/Fixed/Changed` 必显;`Deprecated/Removed/Security` 透传折叠;
12
+ * 面板约定(#23 原文 + #41 四处优化终裁):
13
+ * - `Added/Fixed/Changed/Security` 必显展开;`Deprecated/Removed` 透传折叠;
14
14
  * - `Unreleased` 与空节忽略;
15
15
  * - 缺日志中性提示,不挡安装、不写 blockedReason、不动快照六字段
16
16
  * (本模块不碰快照,面板只增一节 HTML,安装门控只跟快照);
17
17
  * - 已装版离线读、新版按需取 tarball 同名文件复用官方源 integrity
18
18
  * (I/O 在 `src/changelog-io.ts` Node 侧,取不到回落中性提示);
19
- * - 零依赖纯函数解析器(本文件零导入)。
19
+ * - 纯函数解析器(仅复用版本语义,不碰 Node 专属;新增 validate 与策略函数零新增导入)。
20
+ * - 截断数值不动(100 节/每类 200 条/单条 500 字/全文 64K),解析附计数元数据只记数字;
21
+ * 超限类标计数小字,Security 超限进内折叠;BREAKING 前缀识别 + 行首徽标;yanked 横幅只看 to 版。
20
22
  */
21
23
  /** 包内日志文件名(包根,与 tarball 内同名文件同一名字)。 */
22
24
  export declare const CHANGELOG_FILENAME = "CHANGELOG.md";
@@ -54,6 +56,8 @@ export interface ChangelogEntry {
54
56
  yanked: boolean;
55
57
  /** 六类各自的条目(空类为空数组,空节忽略由渲染层执行)。 */
56
58
  sections: Record<ChangelogCategory, string[]>;
59
+ /** 每类原始条数(截断前计数,只记数字不留文本;缺省视为与 sections 等长)。 */
60
+ counts?: Record<ChangelogCategory, number>;
57
61
  }
58
62
  /** 是否 Unreleased(大小写不敏感,前后空白忽略)。 */
59
63
  export declare function isUnreleasedVersion(v: unknown): boolean;
@@ -62,6 +66,41 @@ export declare function isUnreleasedVersion(v: unknown): boolean;
62
66
  * 非字符串/空串回 [];超长截断后解析;未知版本标题与其下条目一律丢弃;
63
67
  * 全空节版本直接丢弃(空节忽略);Unreleased 保留(渲染与区间时忽略)。
64
68
  */
69
+ /** 共享扫描仪(parse 与 validate 同一套,不许第二套正则)。 */
70
+ export declare const RE_VERSION_HEADING: RegExp;
71
+ export declare const RE_CATEGORY_HEADING: RegExp;
72
+ export declare const RE_BULLET: RegExp;
73
+ export declare const RE_FENCE_TOGGLE: RegExp;
74
+ export declare const RE_VERSION_NUM: RegExp;
75
+ export declare const RE_VERSION_DATE: RegExp;
76
+ export declare const RE_YANKED: RegExp;
77
+ export declare const RE_UNRELEASED: RegExp;
78
+ export declare function matchVersionHeading(line: unknown): string | null;
79
+ export declare function matchCategoryHeading(line: unknown): string | null;
80
+ export declare function matchBulletBody(line: unknown): string | null;
81
+ export declare function isFenceToggle(line: unknown): boolean;
82
+ /** 破坏标记徽标文案(中文 UI 一致,原文前缀另行加粗保留)。 */
83
+ export declare const BREAKING_BADGE_TEXT = "\u4E0D\u517C\u5BB9";
84
+ /** 剥行首 markdown 装饰(加粗/引用/前后空白),只为识别,渲染保留原文一字不动。 */
85
+ export declare function stripBreakingDecorations(s: unknown): string;
86
+ /** 条目是否为破坏标记(去饰后开头匹配,大小写不敏感,中英文冒号皆可)。 */
87
+ export declare function isBreakingChangelogItem(item: unknown): boolean;
88
+ /** 拆出破坏前缀(保留原文拼写与冒号),命中才回,否则 null。 */
89
+ export declare function splitBreakingPrefix(item: string): {
90
+ head: string;
91
+ prefix: string;
92
+ rest: string;
93
+ } | null;
94
+ /** 轮询退避(毫秒):传输失败后轮询按此退避,手动/换版/重开立即重问。 */
95
+ export declare const CHANGELOG_POLL_BACKOFF_MS: number;
96
+ /** 三处共用(宿主电话/单面板/批量按行,各存各的):成功与取不到已记住即不再问。 */
97
+ export declare function shouldFetchChangelog(opts: {
98
+ hasCache: boolean;
99
+ failedAt: number | null;
100
+ now: number;
101
+ isManual: boolean;
102
+ backoffMs?: number;
103
+ }): boolean;
65
104
  export declare function parseChangelog(markdown: unknown): ChangelogEntry[];
66
105
  /** 该节是否有可见内容(六类任一非空)。 */
67
106
  export declare function hasVisibleSections(entry: ChangelogEntry | null | undefined): boolean;
@@ -81,6 +120,14 @@ export declare function selectChangelogEntries(entries: unknown, fromExclusive:
81
120
  * 任一缺失/非法即回 [](调用方渲染中性提示,不挡安装)。
82
121
  */
83
122
  export declare function changelogForUpdate(entries: unknown, runningVersion: unknown, latestVersion: unknown, installedVersion?: unknown): ChangelogEntry[];
123
+ /** 计数元数据取值(缺省视为与 sections 等长,只记数字)。 */
124
+ export declare function countsOf(entry: ChangelogEntry | null | undefined): Record<ChangelogCategory, number>;
125
+ /** 超限小字(诚实截断):共 M 条,仅显示前 N 条。 */
126
+ export declare function truncatedNoteHTML(total: number, shown: number): string;
127
+ /** 撤回横幅(只看 to 版,纯展示,不挡安装,文案冻结)。 */
128
+ export declare function yankedBannerHTML(version: unknown): string;
129
+ /** 单条目 HTML(含破坏标记徽标与前缀加粗,正文一字不动)。 */
130
+ export declare function renderChangelogItem(item: string): string;
84
131
  /** 单节 HTML(调用方保证已过滤 Unreleased 与空节,本函数再兜底一次)。 */
85
132
  export declare function renderChangelogSection(entry: ChangelogEntry): string;
86
133
  /** 中性提示 HTML(缺日志、取不到、无区间内容时统一用它,不挡安装)。 */
@@ -94,3 +141,19 @@ export declare function renderChangelogHTML(entries: unknown, opts?: {
94
141
  from?: string | null;
95
142
  to?: string | null;
96
143
  }): string;
144
+ /** 校验诊断(一行一码,行号为原文件 1-based,诊断按行号排序)。 */
145
+ export interface ChangelogDiagnostic {
146
+ line: number;
147
+ code: string;
148
+ hint: string;
149
+ }
150
+ export interface ChangelogValidation {
151
+ ok: boolean;
152
+ diagnostics: ChangelogDiagnostic[];
153
+ }
154
+ /** 校验提示文(与终裁码表一字对应,仅 CI/发布前用,运行时永不调用)。 */
155
+ export declare const CHANGELOG_VALIDATE_HINTS: Record<string, string>;
156
+ /** 疑似破坏误写(仅行首可疑,不报正文中间散文)。 */
157
+ export declare function isMaybeBreakingMisuse(bulletBody: unknown): boolean;
158
+ /** 纯校验(零新增导入,与 parse 共享扫描仪,永不抛错,仅 CI/发布前用)。 */
159
+ export declare function validateChangelog(markdown: unknown): ChangelogValidation;
package/dist/changelog.js CHANGED
@@ -15,8 +15,8 @@ const CHANGELOG_ALL_CATEGORIES = [
15
15
  "Removed",
16
16
  "Security"
17
17
  ];
18
- const CHANGELOG_MUST_SHOW = ["Added", "Fixed", "Changed"];
19
- const CHANGELOG_FOLDED = ["Deprecated", "Removed", "Security"];
18
+ const CHANGELOG_MUST_SHOW = ["Added", "Fixed", "Changed", "Security"];
19
+ const CHANGELOG_FOLDED = ["Deprecated", "Removed"];
20
20
  const CHANGELOG_CATEGORY_ZH = {
21
21
  Added: "\u65B0\u589E",
22
22
  Fixed: "\u4FEE\u590D",
@@ -28,6 +28,9 @@ const CHANGELOG_CATEGORY_ZH = {
28
28
  function emptySections() {
29
29
  return { Added: [], Fixed: [], Changed: [], Deprecated: [], Removed: [], Security: [] };
30
30
  }
31
+ function emptyCounts() {
32
+ return { Added: 0, Fixed: 0, Changed: 0, Deprecated: 0, Removed: 0, Security: 0 };
33
+ }
31
34
  function isCategoryName(v) {
32
35
  return CHANGELOG_ALL_CATEGORIES.includes(v);
33
36
  }
@@ -60,6 +63,119 @@ function truncateBullet(text) {
60
63
  if (t.length <= CHANGELOG_MAX_BULLET_CHARS) return t;
61
64
  return `${t.slice(0, CHANGELOG_MAX_BULLET_CHARS - 1)}\u2026`;
62
65
  }
66
+ const RE_VERSION_HEADING = /^##(?!#)\s*(.+?)\s*$/;
67
+ const RE_CATEGORY_HEADING = /^###\s*(.+?)\s*$/;
68
+ const RE_BULLET = /^\s*[-*+]\s+(.+?)\s*$/;
69
+ const RE_FENCE_TOGGLE = new RegExp("^\\s*(" + String.fromCharCode(96).repeat(3) + "|~~~)");
70
+ const RE_VERSION_NUM = /(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/;
71
+ const RE_VERSION_DATE = /(\d{4}-\d{2}-\d{2})/;
72
+ const RE_YANKED = /\[YANKED\]/i;
73
+ const RE_UNRELEASED = /unreleased/i;
74
+ function matchVersionHeading(line) {
75
+ try {
76
+ const s = String(line !== null && line !== void 0 ? line : "");
77
+ const m = s.match(RE_VERSION_HEADING);
78
+ if (!m) return null;
79
+ return String(m[1] !== void 0 && m[1] !== null ? m[1] : "").trim();
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+ function matchCategoryHeading(line) {
85
+ try {
86
+ const s = String(line !== null && line !== void 0 ? line : "");
87
+ const m = s.match(RE_CATEGORY_HEADING);
88
+ if (!m) return null;
89
+ return String(m[1] !== void 0 && m[1] !== null ? m[1] : "").trim();
90
+ } catch {
91
+ return null;
92
+ }
93
+ }
94
+ function matchBulletBody(line) {
95
+ try {
96
+ const s = String(line !== null && line !== void 0 ? line : "");
97
+ const m = s.match(RE_BULLET);
98
+ if (!m) return null;
99
+ return String(m[1] !== void 0 && m[1] !== null ? m[1] : "");
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+ function isFenceToggle(line) {
105
+ try {
106
+ return RE_FENCE_TOGGLE.test(String(line !== null && line !== void 0 ? line : ""));
107
+ } catch {
108
+ return false;
109
+ }
110
+ }
111
+ const BREAKING_BADGE_TEXT = "\u4E0D\u517C\u5BB9";
112
+ function stripBreakingDecorations(s) {
113
+ try {
114
+ let t = String(s !== null && s !== void 0 ? s : "").trim();
115
+ let changed = true;
116
+ while (changed) {
117
+ changed = false;
118
+ if (t.charAt(0) === ">") {
119
+ t = t.slice(1).trim();
120
+ changed = true;
121
+ continue;
122
+ }
123
+ if (t.slice(0, 2) === "**" || t.slice(0, 2) === "__") {
124
+ t = t.slice(2).trim();
125
+ changed = true;
126
+ continue;
127
+ }
128
+ if (t.charAt(0) === "*" || t.charAt(0) === "_") {
129
+ t = t.slice(1).trim();
130
+ changed = true;
131
+ continue;
132
+ }
133
+ }
134
+ return t;
135
+ } catch {
136
+ return "";
137
+ }
138
+ }
139
+ function isBreakingChangelogItem(item) {
140
+ try {
141
+ if (typeof item !== "string") return false;
142
+ const t = stripBreakingDecorations(item);
143
+ if (!t) return false;
144
+ return /^(breaking|不兼容)\s*[::]/i.test(t);
145
+ } catch {
146
+ return false;
147
+ }
148
+ }
149
+ function splitBreakingPrefix(item) {
150
+ try {
151
+ const s = String(item !== null && item !== void 0 ? item : "");
152
+ const m = s.match(/^(\s*(?:>\s*|\*\*\s*|__\s*|\*\s*|_\s*)*)(breaking|不兼容)(\s*[::])/i);
153
+ if (!m) return null;
154
+ const head = String(m[1] !== void 0 && m[1] !== null ? m[1] : "");
155
+ const core = String(m[2] !== void 0 && m[2] !== null ? m[2] : "");
156
+ const colon = String(m[3] !== void 0 && m[3] !== null ? m[3] : "");
157
+ const prefix = core + colon;
158
+ const rest = s.slice(m[0].length);
159
+ return { head, prefix, rest };
160
+ } catch {
161
+ return null;
162
+ }
163
+ }
164
+ const CHANGELOG_POLL_BACKOFF_MS = 30 * 1e3;
165
+ function shouldFetchChangelog(opts) {
166
+ try {
167
+ const o = opts;
168
+ if (o.hasCache === true) return false;
169
+ if (o.isManual === true) return true;
170
+ const now = typeof o.now === "number" && Number.isFinite(o.now) ? o.now : Date.now();
171
+ const failedAt = typeof o.failedAt === "number" && Number.isFinite(o.failedAt) ? o.failedAt : null;
172
+ if (failedAt === null) return true;
173
+ const backoff = typeof o.backoffMs === "number" && Number.isFinite(o.backoffMs) && o.backoffMs > 0 ? Math.floor(o.backoffMs) : CHANGELOG_POLL_BACKOFF_MS;
174
+ return now - failedAt >= backoff;
175
+ } catch {
176
+ return true;
177
+ }
178
+ }
63
179
  function parseChangelog(markdown) {
64
180
  try {
65
181
  if (typeof markdown !== "string" || !markdown.trim()) return [];
@@ -86,32 +202,38 @@ function parseChangelog(markdown) {
86
202
  };
87
203
  for (const rawLine of lines) {
88
204
  const line = String(rawLine ?? "");
89
- if (/^\s*(```|~~~)/.test(line)) {
205
+ if (isFenceToggle(line)) {
90
206
  inFence = !inFence;
91
207
  continue;
92
208
  }
93
209
  if (inFence) continue;
94
- const versionHeading = line.match(/^##(?!#)\s*(.+?)\s*$/);
95
- if (versionHeading) {
210
+ const versionTitle = matchVersionHeading(line);
211
+ if (versionTitle !== null) {
96
212
  pushCurrent();
97
213
  cur = null;
98
214
  curCat = null;
99
- const title = (versionHeading[1] ?? "").trim();
215
+ const title = versionTitle;
100
216
  const { version, date, yanked } = extractVersionDateAndYanked(title);
101
217
  if (!version) continue;
102
- cur = { version, date, yanked, sections: emptySections() };
218
+ cur = { version, date, yanked, sections: emptySections(), counts: emptyCounts() };
103
219
  continue;
104
220
  }
105
- const catMatch = line.match(/^###\s*(.+?)\s*$/);
106
- if (catMatch) {
107
- const cat = normalizeCategory(catMatch[1] ?? "");
221
+ const catTitle = matchCategoryHeading(line);
222
+ if (catTitle !== null) {
223
+ const cat = normalizeCategory(catTitle);
108
224
  curCat = cur && cat && isCategoryName(cat) ? cat : null;
109
225
  continue;
110
226
  }
111
- const bullet = line.match(/^\s*[-*+]\s+(.+?)\s*$/);
112
- if (bullet && cur && curCat) {
113
- const item = truncateBullet(bullet[1] ?? "");
114
- if (item) cur.sections[curCat].push(item);
227
+ const bulletBody = matchBulletBody(line);
228
+ if (bulletBody !== null && cur && curCat) {
229
+ const item = truncateBullet(bulletBody);
230
+ if (item) {
231
+ cur.sections[curCat].push(item);
232
+ try {
233
+ if (cur.counts) cur.counts[curCat] += 1;
234
+ } catch {
235
+ }
236
+ }
115
237
  continue;
116
238
  }
117
239
  const trimmed = line.trim();
@@ -189,46 +311,119 @@ function changelogForUpdate(entries, runningVersion, latestVersion, installedVer
189
311
  function escapeChangelogHtml(text) {
190
312
  return String(text ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
191
313
  }
314
+ function countsOf(entry) {
315
+ try {
316
+ const out = { Added: 0, Fixed: 0, Changed: 0, Deprecated: 0, Removed: 0, Security: 0 };
317
+ if (!entry || typeof entry !== "object") return out;
318
+ for (const c of CHANGELOG_ALL_CATEGORIES) {
319
+ const n = entry.counts;
320
+ const v = n && typeof n[c] === "number" && Number.isFinite(n[c]) ? Math.floor(n[c]) : null;
321
+ if (v !== null && v >= 0) {
322
+ out[c] = v;
323
+ continue;
324
+ }
325
+ const list = entry.sections;
326
+ const arr = list ? list[c] : null;
327
+ out[c] = Array.isArray(arr) ? arr.length : 0;
328
+ }
329
+ return out;
330
+ } catch {
331
+ return { Added: 0, Fixed: 0, Changed: 0, Deprecated: 0, Removed: 0, Security: 0 };
332
+ }
333
+ }
334
+ function truncatedNoteHTML(total, shown) {
335
+ try {
336
+ const m = Math.floor(total);
337
+ const n = Math.floor(shown);
338
+ return '<div class="dsh-upd-changelog-count">\u5171 ' + String(m) + " \u6761\uFF0C\u4EC5\u663E\u793A\u524D " + String(n) + " \u6761</div>";
339
+ } catch {
340
+ return "";
341
+ }
342
+ }
343
+ function yankedBannerHTML(version) {
344
+ try {
345
+ const v = typeof version === "string" ? version.trim() : "";
346
+ if (!v) return "";
347
+ return '<div class="dsh-upd-changelog-yanked" role="alert">\u76EE\u6807\u7248\u672C ' + escapeChangelogHtml(v) + " \u5DF2\u88AB\u4F5C\u8005\u64A4\u56DE\uFF08yanked\uFF09\uFF0C\u5B89\u88C5\u4E0D\u53D7\u5F71\u54CD\uFF0C\u7EE7\u7EED\u524D\u8BF7\u786E\u8BA4\u3002</div>";
348
+ } catch {
349
+ return "";
350
+ }
351
+ }
352
+ function renderChangelogItem(item) {
353
+ try {
354
+ const s = String(item !== null && item !== void 0 ? item : "");
355
+ if (!s) return "";
356
+ const split = splitBreakingPrefix(s);
357
+ if (!split) return "<li>" + escapeChangelogHtml(s) + "</li>";
358
+ const head = escapeChangelogHtml(split.head);
359
+ const prefix = escapeChangelogHtml(split.prefix);
360
+ const rest = escapeChangelogHtml(split.rest);
361
+ const badge = '<span class="dsh-upd-breaking-badge" role="img" aria-label="\u7834\u574F\u6027\u53D8\u66F4">' + BREAKING_BADGE_TEXT + "</span> ";
362
+ return "<li>" + head + badge + "<strong>" + prefix + "</strong>" + rest + "</li>";
363
+ } catch {
364
+ return "";
365
+ }
366
+ }
192
367
  function renderChangelogSection(entry) {
193
368
  try {
194
369
  if (!entry || typeof entry !== "object") return "";
195
- const version = String(entry.version ?? "").trim();
370
+ const version = String(entry.version !== void 0 && entry.version !== null ? String(entry.version) : "").trim();
196
371
  if (!version || isUnreleasedVersion(version)) return "";
197
372
  if (!hasVisibleSections(entry)) return "";
198
- const date = typeof entry.date === "string" ? String(entry.date) : "";
199
- const yankedSuffix = entry.yanked === true ? " \xB7 \u5DF2\u64A4\u56DE" : "";
200
- const title = (date ? `${version} \xB7 ${date}` : version) + yankedSuffix;
373
+ const dateRaw = entry.date;
374
+ const date = typeof dateRaw === "string" ? dateRaw : "";
375
+ const yankedFlag = entry.yanked === true;
376
+ const yankedSuffix = yankedFlag ? " \xB7 \u5DF2\u64A4\u56DE" : "";
377
+ const title = (date ? version + " \xB7 " + date : version) + yankedSuffix;
378
+ const counts = countsOf(entry);
201
379
  const parts = [];
202
- parts.push(`<div class="dsh-upd-changelog-version" data-version="${escapeChangelogHtml(version)}">`);
203
- parts.push(`<div class="dsh-upd-changelog-title">${escapeChangelogHtml(title)}</div>`);
380
+ parts.push('<div class="dsh-upd-changelog-version" data-version="' + escapeChangelogHtml(version) + '">');
381
+ parts.push('<div class="dsh-upd-changelog-title">' + escapeChangelogHtml(title) + "</div>");
204
382
  for (const cat of CHANGELOG_MUST_SHOW) {
205
- const items = Array.isArray(entry.sections?.[cat]) ? entry.sections[cat] : [];
206
- if (items.length === 0) continue;
207
- const label = `${cat} \xB7 ${CHANGELOG_CATEGORY_ZH[cat]}`;
208
- parts.push(`<div class="dsh-upd-changelog-cat" data-cat="${cat}">`);
209
- parts.push(`<div class="dsh-upd-changelog-catname">${escapeChangelogHtml(label)}</div>`);
383
+ const items = Array.isArray(entry.sections ? entry.sections[cat] : null) ? entry.sections[cat] : [];
384
+ if (!items || items.length === 0) continue;
385
+ const label = cat + " \xB7 " + CHANGELOG_CATEGORY_ZH[cat];
386
+ parts.push('<div class="dsh-upd-changelog-cat" data-cat="' + cat + '">');
387
+ parts.push('<div class="dsh-upd-changelog-catname">' + escapeChangelogHtml(label) + "</div>");
388
+ const total = counts[cat];
389
+ const shown = items.length;
390
+ if (total > shown) {
391
+ parts.push(truncatedNoteHTML(total, shown));
392
+ }
210
393
  parts.push("<ul>");
211
394
  for (const item of items) {
212
395
  if (!item) continue;
213
- parts.push(`<li>${escapeChangelogHtml(item)}</li>`);
396
+ const li = renderChangelogItem(item);
397
+ if (li) parts.push(li);
398
+ }
399
+ parts.push("</ul>");
400
+ if (cat === "Security" && total > shown) {
401
+ const restCount = total - shown;
402
+ parts.push('<details class="dsh-upd-changelog-security-more"><summary>\u5176\u4F59 ' + String(restCount) + ' \u6761</summary><div class="dsh-upd-changelog-more-note">\u4E3A\u4FDD\u6301\u9762\u677F\u6027\u80FD\uFF0C\u5176\u4F59\u6761\u76EE\u5DF2\u6298\u53E0\uFF0C\u53EF\u67E5\u770B\u539F\u6587\u3002</div></details>');
214
403
  }
215
- parts.push("</ul></div>");
404
+ parts.push("</div>");
216
405
  }
217
406
  for (const cat of CHANGELOG_FOLDED) {
218
- const items = Array.isArray(entry.sections?.[cat]) ? entry.sections[cat] : [];
219
- if (items.length === 0) continue;
220
- const label = `${cat} \xB7 ${CHANGELOG_CATEGORY_ZH[cat]}\uFF08${items.length}\uFF09`;
221
- parts.push(`<details class="dsh-upd-changelog-fold" data-cat="${cat}">`);
222
- parts.push(`<summary>${escapeChangelogHtml(label)}</summary>`);
407
+ const items = Array.isArray(entry.sections ? entry.sections[cat] : null) ? entry.sections[cat] : [];
408
+ if (!items || items.length === 0) continue;
409
+ const label = cat + " \xB7 " + CHANGELOG_CATEGORY_ZH[cat] + "\uFF08" + String(items.length) + "\uFF09";
410
+ parts.push('<details class="dsh-upd-changelog-fold" data-cat="' + cat + '">');
411
+ parts.push("<summary>" + escapeChangelogHtml(label) + "</summary>");
412
+ const total = counts[cat];
413
+ const shown = items.length;
414
+ if (total > shown) {
415
+ parts.push(truncatedNoteHTML(total, shown));
416
+ }
223
417
  parts.push("<ul>");
224
418
  for (const item of items) {
225
419
  if (!item) continue;
226
- parts.push(`<li>${escapeChangelogHtml(item)}</li>`);
420
+ const li = renderChangelogItem(item);
421
+ if (li) parts.push(li);
227
422
  }
228
423
  parts.push("</ul></details>");
229
424
  }
230
425
  parts.push("</div>");
231
- return parts.join("\n");
426
+ return parts.join(String.fromCharCode(10));
232
427
  } catch {
233
428
  return "";
234
429
  }
@@ -264,7 +459,165 @@ ${blocks.join("\n")}
264
459
  return renderChangelogNeutral();
265
460
  }
266
461
  }
462
+ const CHANGELOG_VALIDATE_HINTS = {
463
+ E_VERSION_TITLE: "\u8BE5\u6807\u9898\u4E0D\u662F\u5408\u6CD5\u7248\u672C\u53F7\uFF0C\u5176\u4E0B\u6761\u76EE\u5DF2\u88AB\u5FFD\u7565\uFF0C\u8BF7\u6539\u6210 ## [x.y.z] \u5F62\u6001",
464
+ E_CATEGORY: "\u672A\u77E5\u5206\u7C7B\uFF0C\u5176\u4E0B\u6761\u76EE\u5DF2\u88AB\u5FFD\u7565\uFF0C\u4EC5\u6536 Added/Fixed/Changed/Deprecated/Removed/Security",
465
+ E_BULLET_ORPHAN: "\u8BE5\u6761\u76EE\u4E0D\u5728\u7248\u672C\u8282\u4E0E\u5206\u7C7B\u4E0B\uFF0C\u5DF2\u88AB\u5FFD\u7565",
466
+ W_BREAKING_MAYBE: "\u7591\u4F3C\u7834\u574F\u6807\u8BB0\u8BEF\u5199\uFF08\u7F3A\u5192\u53F7/\u62FC\u5199\u63A5\u8FD1/\u4F4D\u7F6E\u7591\u4F3C\u653E\u9519\uFF09\uFF0C\u8BF7\u68C0\u67E5\u662F\u5426\u60F3\u5199 BREAKING:/\u4E0D\u517C\u5BB9:",
467
+ W_YANKED: "\u8BE5\u7248\u672C\u5DF2\u88AB\u6807\u8BB0\u64A4\u56DE\uFF08yanked\uFF09\uFF0C\u5B89\u88C5\u4E0D\u53D7\u5F71\u54CD\uFF0C\u7EE7\u7EED\u524D\u8BF7\u786E\u8BA4",
468
+ W_TRUNCATED: "\u8BE5\u7C7B\u5171 M \u6761\uFF0C\u4EC5\u663E\u793A\u524D N \u6761",
469
+ W_TRUNCATED_FILE: "\u6587\u4EF6\u8D85 64K\uFF0C\u4EC5\u6821\u9A8C\u524D 64K",
470
+ W_EMPTY: "\u672A\u89E3\u6790\u5230\u4EFB\u4F55\u7248\u672C\u8282\uFF0C\u9762\u677F\u5C06\u663E\u793A\u4E2D\u6027\u63D0\u793A"
471
+ };
472
+ function isMaybeBreakingMisuse(bulletBody) {
473
+ try {
474
+ if (typeof bulletBody !== "string") return false;
475
+ const t = stripBreakingDecorations(bulletBody);
476
+ if (!t) return false;
477
+ if (isBreakingChangelogItem(bulletBody)) return false;
478
+ if (/^breaking\s*$/i.test(t)) return true;
479
+ if (/^breaking\s+[^::]/i.test(t)) return true;
480
+ if (/^breaking[^::\s\w]/i.test(t)) return true;
481
+ if (/^不兼容\s*$/i.test(t)) return true;
482
+ if (/^不兼容\s*[^::]/i.test(t)) return true;
483
+ if (/^(braking|breking|breakign|brekaing|braeking|breakingg)\b/i.test(t)) return true;
484
+ if (/^break[^i::\s]/i.test(t)) return true;
485
+ return false;
486
+ } catch {
487
+ return false;
488
+ }
489
+ }
490
+ function validateChangelog(markdown) {
491
+ try {
492
+ if (typeof markdown !== "string" || !markdown.trim()) {
493
+ return { ok: true, diagnostics: [{ line: 1, code: "W_EMPTY", hint: CHANGELOG_VALIDATE_HINTS.W_EMPTY }] };
494
+ }
495
+ let raw = String(markdown).replace(/\r\n/g, String.fromCharCode(10));
496
+ let fileTruncated = false;
497
+ if (raw.length > CHANGELOG_MAX_CHARS) {
498
+ fileTruncated = true;
499
+ raw = raw.slice(0, CHANGELOG_MAX_CHARS);
500
+ }
501
+ const lines = raw.split(String.fromCharCode(10));
502
+ const diags = [];
503
+ let inFence = false;
504
+ let curValid = false;
505
+ let curCat = null;
506
+ let catLineByCat = {};
507
+ let countByCat = {};
508
+ let hasVisibleVersion = false;
509
+ const flushCounts = function() {
510
+ try {
511
+ for (const k of Object.keys(countByCat)) {
512
+ const n = countByCat[k];
513
+ if (n > CHANGELOG_MAX_BULLETS_PER_SECTION) {
514
+ const lineNo = catLineByCat[k] ? catLineByCat[k] : lines.length;
515
+ const hint = "\u5206\u7C7B " + k + " \u5171 " + String(n) + " \u6761\uFF0C\u4EC5\u663E\u793A\u524D " + String(CHANGELOG_MAX_BULLETS_PER_SECTION) + " \u6761";
516
+ diags.push({ line: lineNo, code: "W_TRUNCATED", hint });
517
+ }
518
+ }
519
+ } catch {
520
+ }
521
+ };
522
+ for (let i = 0; i < lines.length; i++) {
523
+ const lineNo = i + 1;
524
+ const line = String(lines[i] !== void 0 && lines[i] !== null ? lines[i] : "");
525
+ if (isFenceToggle(line)) {
526
+ inFence = !inFence;
527
+ continue;
528
+ }
529
+ if (inFence) continue;
530
+ const vTitle = matchVersionHeading(line);
531
+ if (vTitle !== null) {
532
+ flushCounts();
533
+ catLineByCat = {};
534
+ countByCat = {};
535
+ curValid = false;
536
+ curCat = null;
537
+ const info = extractVersionDateAndYanked(vTitle);
538
+ if (!info.version) {
539
+ diags.push({ line: lineNo, code: "E_VERSION_TITLE", hint: CHANGELOG_VALIDATE_HINTS.E_VERSION_TITLE });
540
+ continue;
541
+ }
542
+ if (isUnreleasedVersion(info.version)) {
543
+ curValid = false;
544
+ continue;
545
+ }
546
+ curValid = true;
547
+ hasVisibleVersion = true;
548
+ if (info.yanked) {
549
+ diags.push({ line: lineNo, code: "W_YANKED", hint: CHANGELOG_VALIDATE_HINTS.W_YANKED });
550
+ }
551
+ continue;
552
+ }
553
+ const cTitle = matchCategoryHeading(line);
554
+ if (cTitle !== null) {
555
+ const cat = normalizeCategory(cTitle);
556
+ if (!cat) {
557
+ curCat = null;
558
+ diags.push({ line: lineNo, code: "E_CATEGORY", hint: CHANGELOG_VALIDATE_HINTS.E_CATEGORY });
559
+ continue;
560
+ }
561
+ if (!curValid) {
562
+ curCat = null;
563
+ continue;
564
+ }
565
+ curCat = cat;
566
+ catLineByCat[cat] = lineNo;
567
+ if (countByCat[cat] === void 0) countByCat[cat] = 0;
568
+ continue;
569
+ }
570
+ const body = matchBulletBody(line);
571
+ if (body !== null) {
572
+ if (!curValid || !curCat) {
573
+ diags.push({ line: lineNo, code: "E_BULLET_ORPHAN", hint: CHANGELOG_VALIDATE_HINTS.E_BULLET_ORPHAN });
574
+ continue;
575
+ }
576
+ countByCat[curCat] = (countByCat[curCat] ? countByCat[curCat] : 0) + 1;
577
+ if (isMaybeBreakingMisuse(body)) {
578
+ diags.push({ line: lineNo, code: "W_BREAKING_MAYBE", hint: CHANGELOG_VALIDATE_HINTS.W_BREAKING_MAYBE });
579
+ }
580
+ continue;
581
+ }
582
+ }
583
+ flushCounts();
584
+ if (fileTruncated) {
585
+ diags.push({ line: lines.length, code: "W_TRUNCATED_FILE", hint: CHANGELOG_VALIDATE_HINTS.W_TRUNCATED_FILE });
586
+ }
587
+ if (!hasVisibleVersion) {
588
+ let anyVisible = false;
589
+ try {
590
+ const parsed = parseChangelog(markdown);
591
+ for (const e of parsed) {
592
+ if (e && !isUnreleasedVersion(e.version) && hasVisibleSections(e)) {
593
+ anyVisible = true;
594
+ break;
595
+ }
596
+ }
597
+ } catch {
598
+ anyVisible = false;
599
+ }
600
+ if (!anyVisible) {
601
+ diags.push({ line: lines.length, code: "W_EMPTY", hint: CHANGELOG_VALIDATE_HINTS.W_EMPTY });
602
+ }
603
+ }
604
+ diags.sort(function(a, b) {
605
+ return a.line - b.line;
606
+ });
607
+ let ok = true;
608
+ for (const d of diags) {
609
+ if (d.code.charAt(0) === "E") {
610
+ ok = false;
611
+ break;
612
+ }
613
+ }
614
+ return { ok, diagnostics: diags };
615
+ } catch {
616
+ return { ok: false, diagnostics: [] };
617
+ }
618
+ }
267
619
  export {
620
+ BREAKING_BADGE_TEXT,
268
621
  CHANGELOG_ALL_CATEGORIES,
269
622
  CHANGELOG_CATEGORY_ZH,
270
623
  CHANGELOG_FILENAME,
@@ -276,12 +629,36 @@ export {
276
629
  CHANGELOG_MUST_SHOW,
277
630
  CHANGELOG_NEUTRAL_HINT,
278
631
  CHANGELOG_NEUTRAL_LINE,
632
+ CHANGELOG_POLL_BACKOFF_MS,
633
+ CHANGELOG_VALIDATE_HINTS,
634
+ RE_BULLET,
635
+ RE_CATEGORY_HEADING,
636
+ RE_FENCE_TOGGLE,
637
+ RE_UNRELEASED,
638
+ RE_VERSION_DATE,
639
+ RE_VERSION_HEADING,
640
+ RE_VERSION_NUM,
641
+ RE_YANKED,
279
642
  changelogForUpdate,
643
+ countsOf,
280
644
  hasVisibleSections,
645
+ isBreakingChangelogItem,
646
+ isFenceToggle,
647
+ isMaybeBreakingMisuse,
281
648
  isUnreleasedVersion,
649
+ matchBulletBody,
650
+ matchCategoryHeading,
651
+ matchVersionHeading,
282
652
  parseChangelog,
283
653
  renderChangelogHTML,
654
+ renderChangelogItem,
284
655
  renderChangelogNeutral,
285
656
  renderChangelogSection,
286
- selectChangelogEntries
657
+ selectChangelogEntries,
658
+ shouldFetchChangelog,
659
+ splitBreakingPrefix,
660
+ stripBreakingDecorations,
661
+ truncatedNoteHTML,
662
+ validateChangelog,
663
+ yankedBannerHTML
287
664
  };
package/dist/entry.d.ts CHANGED
@@ -5,7 +5,11 @@ export type EntryVariant = 'button' | 'badge' | 'inline';
5
5
  /** 什么时候主动查一次(只读,联网一次)。 */
6
6
  export type EntryAutoCheck = 'mount' | 'never';
7
7
  /** 用户点下去之后做什么。 */
8
- export type EntryOpenOn = 'has-update' | 'always' | 'manual';
8
+ /**
9
+ * 点下去之后做什么:`has-update` 有新版才开面板(无事只给一句小字);`always` 检查完总是开;
10
+ * `manual` 交给 onActivate;`direct` 点开即弹窗、不预查(面板挂载即自查;badge 仍走回调口径)。
11
+ */
12
+ export type EntryOpenOn = 'has-update' | 'always' | 'manual' | 'direct';
9
13
  /** 入口件主题(与面板同一套取值,切换即换肤)。 */
10
14
  export type EntryTheme = UpdatePanelTheme;
11
15
  /** 入口件的状态档:只用来算文案与 data-state,不另立状态机。 */
package/dist/entry.js CHANGED
@@ -64,10 +64,15 @@ const UPDATE_ENTRY_CSS = [
64
64
  '.dsh-upd-entry[data-state="update"] .dsh-upd-entry-dot{background:var(--dsh-upd-ok-line,#059669)}',
65
65
  '.dsh-upd-entry[data-state="busy"] .dsh-upd-entry-dot,.dsh-upd-entry[data-state="restart"] .dsh-upd-entry-dot{background:var(--dsh-upd-warn-line,#d97706)}',
66
66
  '.dsh-upd-entry[data-state="failed"] .dsh-upd-entry-dot{background:var(--dsh-upd-bad-line,#dc2626)}',
67
- ".dsh-upd-entry-note{font-size:12.5px;opacity:.75}",
68
- '.dsh-upd-entry[data-theme="d5-paper"]{font-family:Georgia,"Songti SC","STSong","SimSun",serif;color:#1a1a1a}',
69
- '.dsh-upd-entry[data-theme="d5-paper"] .dsh-upd-entry-btn{border-color:#c4b896;background:transparent;border-radius:3px}',
70
- '.dsh-upd-entry[data-theme="d5-paper"] .dsh-upd-entry-btn:hover{border-color:#c8402a;color:#c8402a}',
67
+ // 小字自带底(深色宿主 + 浅色变量时也读得出;浅底宿主上只是多一圈细线,不抢戏)。
68
+ ".dsh-upd-entry-note{font-size:12.5px;opacity:.9;background:var(--dsh-upd-bg,#ffffff);border:1px solid var(--dsh-upd-line,#e5e7eb);border-radius:4px;padding:1px 8px}",
69
+ '.dsh-upd-entry[data-theme="d5-paper"] .dsh-upd-entry-note{background:var(--d5-card);border-color:var(--d5-line-strong);color:var(--d5-ink)}',
70
+ '.dsh-upd-entry[data-theme="d5-paper"]{--d5-ink:#1a1a1a;--d5-muted:#6f675a;--d5-line-strong:#c4b896;--d5-accent:#c8402a;--d5-card:#fffdf6;',
71
+ 'font-family:Georgia,"Songti SC","STSong","SimSun",serif;color:var(--d5-ink)}',
72
+ // 按钮脸自己不透明(深色宿主 + 浅色系统变量时也读得出;hover 红在深浅底上都可见)。
73
+ '.dsh-upd-entry[data-theme="d5-paper"] .dsh-upd-entry-btn{border-color:var(--d5-line-strong);background:var(--d5-card);color:var(--d5-ink);border-radius:3px}',
74
+ '.dsh-upd-entry[data-theme="d5-paper"] .dsh-upd-entry-btn:hover{border-color:var(--d5-accent);color:var(--d5-accent)}',
75
+ '@media (prefers-color-scheme: dark){.dsh-upd-entry[data-theme="d5-paper"]{--d5-ink:#ece5d3;--d5-muted:#a89c83;--d5-line-strong:#5c4e3b;--d5-accent:#e0684e;--d5-card:#1e1a15}}',
71
76
  "@media (prefers-color-scheme: dark){.dsh-upd-entry{color:#e5e7eb}",
72
77
  ".dsh-upd-entry-btn{--dsh-upd-btn:#1f2937;--dsh-upd-line:#374151}}"
73
78
  ].join("\n");
@@ -111,8 +116,8 @@ function mountUpdateEntry(container, options) {
111
116
  throw new Error(`[dsh-plugin-update] \u81EA\u52A8\u68C0\u67E5\u65F6\u673A\u975E\u6CD5\uFF1A\u53EA\u6536 mount \u6216 never\uFF08\u6536\u5230 ${JSON.stringify(options.autoCheck)}\uFF09`);
112
117
  }
113
118
  const openOn = options.openOn ?? "has-update";
114
- if (openOn !== "has-update" && openOn !== "always" && openOn !== "manual") {
115
- throw new Error(`[dsh-plugin-update] \u70B9\u51FB\u53BB\u5411\u975E\u6CD5\uFF1A\u53EA\u6536 has-update / always / manual\uFF08\u6536\u5230 ${JSON.stringify(options.openOn)}\uFF09`);
119
+ if (openOn !== "has-update" && openOn !== "always" && openOn !== "manual" && openOn !== "direct") {
120
+ throw new Error(`[dsh-plugin-update] \u70B9\u51FB\u53BB\u5411\u975E\u6CD5\uFF1A\u53EA\u6536 has-update / always / manual / direct\uFF08\u6536\u5230 ${JSON.stringify(options.openOn)}\uFF09`);
116
121
  }
117
122
  if (options.theme !== void 0 && options.theme !== "default" && options.theme !== "archive" && options.theme !== "d5-paper") {
118
123
  throw new Error(`[dsh-plugin-update] \u4E3B\u9898\u975E\u6CD5\uFF1A\u53EA\u6536 default \u6216 archive\uFF08d5-paper \u4E3A\u65E7\u522B\u540D\u4ECD\u53EF\u7528\uFF09\uFF08\u6536\u5230 ${JSON.stringify(options.theme)}\uFF09`);
@@ -248,6 +253,10 @@ function mountUpdateEntry(container, options) {
248
253
  }
249
254
  async function activate() {
250
255
  if (!mounted || panelMode !== null || activating) return;
256
+ if (openOn === "direct" && variant === "button") {
257
+ openDialog();
258
+ return;
259
+ }
251
260
  activating = true;
252
261
  note = null;
253
262
  render();
@@ -288,7 +297,6 @@ function mountUpdateEntry(container, options) {
288
297
  void activate();
289
298
  return;
290
299
  }
291
- if (panelMode === "dialog" && closest('[data-action="close-view"]')) close();
292
300
  } catch {
293
301
  }
294
302
  }
package/dist/host.d.ts CHANGED
@@ -10,8 +10,8 @@ export { COPY_BUDGET_CHARS, DETAIL_MAX_CHARS, DIAG_DROP_ORDER, DIAG_INLINE_BUDGE
10
10
  export type { DiagDropKey, RedactionRuleName } from './redaction.js';
11
11
  export { DIAG_ACTIONS, DIAG_KEYS, DIAG_KNOWN_TYPES, DIAG_STAGE_EXPECT, DIAG_STAGES, DIAG_VERSION, buildDiag, deriveAction, deriveRouteMethod, deriveStage, enforceBudget as enforceDiagBudgetForPhone, } from './diag.js';
12
12
  export type { DiagAction, DiagInput, DiagObject, DiagStage } from './diag.js';
13
- export { CHANGELOG_ALL_CATEGORIES, CHANGELOG_FILENAME, CHANGELOG_FOLDED, CHANGELOG_MAX_BULLETS_PER_SECTION, CHANGELOG_MAX_BULLET_CHARS, CHANGELOG_MAX_CHARS, CHANGELOG_MAX_ENTRIES, CHANGELOG_MUST_SHOW, CHANGELOG_NEUTRAL_HINT, CHANGELOG_NEUTRAL_LINE, changelogForUpdate, hasVisibleSections, isUnreleasedVersion, parseChangelog, renderChangelogHTML, renderChangelogNeutral, renderChangelogSection, selectChangelogEntries, } from './changelog.js';
14
- export type { ChangelogCategory, ChangelogEntry } from './changelog.js';
13
+ export { CHANGELOG_ALL_CATEGORIES, CHANGELOG_FILENAME, CHANGELOG_FOLDED, CHANGELOG_MAX_BULLETS_PER_SECTION, CHANGELOG_MAX_BULLET_CHARS, CHANGELOG_MAX_CHARS, CHANGELOG_MAX_ENTRIES, CHANGELOG_MUST_SHOW, CHANGELOG_NEUTRAL_HINT, CHANGELOG_NEUTRAL_LINE, BREAKING_BADGE_TEXT, CHANGELOG_POLL_BACKOFF_MS, changelogForUpdate, countsOf, hasVisibleSections, isBreakingChangelogItem, isUnreleasedVersion, parseChangelog, renderChangelogHTML, renderChangelogItem, renderChangelogNeutral, renderChangelogSection, selectChangelogEntries, shouldFetchChangelog, splitBreakingPrefix, truncatedNoteHTML, validateChangelog, yankedBannerHTML, } from './changelog.js';
14
+ export type { ChangelogCategory, ChangelogDiagnostic, ChangelogEntry, ChangelogValidation } from './changelog.js';
15
15
  export { CHANGELOG_DEFAULT_REGISTRY, CHANGELOG_FETCH_MAX_BYTES, CHANGELOG_TAR_MAX_BYTES, CHANGELOG_TARBALL_MAX_BYTES, extractChangelogFromTar, fetchReleaseChangelogText, readInstalledChangelogText, } from './changelog-io.js';
16
16
  export type { ReleaseChangelogRef } from './changelog-io.js';
17
17
  export { createUpdateDiskPorts, createUpdateExecutor, pathsForUpdate, resolveCliEntry } from './store.js';
package/dist/host.js CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  } from "./config.js";
12
12
  import { manualCommand } from "./commands.js";
13
13
  import { fetchReleaseChangelogText } from "./changelog-io.js";
14
+ import { shouldFetchChangelog } from "./changelog.js";
14
15
  import { buildDiag } from "./diag.js";
15
16
  import { fetchNpmVersionRelease, isVersionAllowedInChannel, updateError, validReleaseVersion, validRequestId } from "./service.js";
16
17
  import { createUpdateDiskPorts, createUpdateExecutor, createUpdateQueuePorts } from "./store.js";
@@ -73,14 +74,24 @@ import {
73
74
  CHANGELOG_MUST_SHOW,
74
75
  CHANGELOG_NEUTRAL_HINT,
75
76
  CHANGELOG_NEUTRAL_LINE,
77
+ BREAKING_BADGE_TEXT,
78
+ CHANGELOG_POLL_BACKOFF_MS,
76
79
  changelogForUpdate,
80
+ countsOf,
77
81
  hasVisibleSections,
82
+ isBreakingChangelogItem,
78
83
  isUnreleasedVersion,
79
84
  parseChangelog,
80
85
  renderChangelogHTML,
86
+ renderChangelogItem,
81
87
  renderChangelogNeutral,
82
88
  renderChangelogSection,
83
- selectChangelogEntries
89
+ selectChangelogEntries,
90
+ shouldFetchChangelog as shouldFetchChangelog2,
91
+ splitBreakingPrefix,
92
+ truncatedNoteHTML,
93
+ validateChangelog,
94
+ yankedBannerHTML
84
95
  } from "./changelog.js";
85
96
  import {
86
97
  CHANGELOG_DEFAULT_REGISTRY,
@@ -578,12 +589,24 @@ function createHostUpdate(deps = {}, configInput) {
578
589
  const readerOverrides = { ...deps.readerOverrides ?? {}, ctx, ...deps.pluginManager ? { pluginManager: deps.pluginManager } : {} };
579
590
  const phoneNames = buildPhoneNames(config.prefix);
580
591
  const changelogCache = /* @__PURE__ */ new Map();
592
+ const changelogFailedAt = /* @__PURE__ */ new Map();
581
593
  async function readChangelog(args) {
582
594
  const raw = args ? args["version"] : void 0;
583
595
  if (!validReleaseVersion(raw)) throw updateError("invalid-release");
584
596
  const version = raw;
585
597
  if (!isVersionAllowedInChannel(version, config.releaseChannel)) throw updateError("invalid-release");
586
598
  if (changelogCache.has(version)) return { version, markdown: changelogCache.get(version) ?? null };
599
+ try {
600
+ let nowMs = 0;
601
+ try {
602
+ nowMs = Date.now();
603
+ } catch {
604
+ nowMs = 0;
605
+ }
606
+ const failedAt = changelogFailedAt.has(version) ? changelogFailedAt.get(version) : null;
607
+ void shouldFetchChangelog({ hasCache: false, failedAt, now: nowMs, isManual: true });
608
+ } catch {
609
+ }
587
610
  const fetchImpl = readerOverrides.fetchImpl ?? globalThis.fetch;
588
611
  const release = await fetchNpmVersionRelease(fetchImpl, config.checkTimeoutMs, {
589
612
  targetPackageName: config.targetPackageName,
@@ -591,12 +614,29 @@ function createHostUpdate(deps = {}, configInput) {
591
614
  releaseChannel: config.releaseChannel,
592
615
  version
593
616
  });
594
- const markdown = await fetchReleaseChangelogText(
595
- fetchImpl,
596
- { tarball: release.tarball, integrity: release.integrity, version },
597
- { targetPackageName: config.targetPackageName, registryUrl: config.registryUrl, timeoutMs: config.checkTimeoutMs }
598
- );
617
+ let markdown;
618
+ try {
619
+ markdown = await fetchReleaseChangelogText(
620
+ fetchImpl,
621
+ { tarball: release.tarball, integrity: release.integrity, version },
622
+ { targetPackageName: config.targetPackageName, registryUrl: config.registryUrl, timeoutMs: config.checkTimeoutMs }
623
+ );
624
+ } catch (err) {
625
+ try {
626
+ changelogFailedAt.set(version, Date.now());
627
+ } catch {
628
+ try {
629
+ changelogFailedAt.set(version, 0);
630
+ } catch {
631
+ }
632
+ }
633
+ throw err;
634
+ }
599
635
  changelogCache.set(version, markdown);
636
+ try {
637
+ changelogFailedAt.delete(version);
638
+ } catch {
639
+ }
600
640
  return { version, markdown };
601
641
  }
602
642
  async function readStatus(args) {
@@ -713,6 +753,7 @@ function __resetSharedUpdateReaderForTests() {
713
753
  sharedDesktopPnpm = null;
714
754
  }
715
755
  export {
756
+ BREAKING_BADGE_TEXT,
716
757
  CHANGELOG_ALL_CATEGORIES,
717
758
  CHANGELOG_DEFAULT_REGISTRY,
718
759
  CHANGELOG_FETCH_MAX_BYTES,
@@ -725,6 +766,7 @@ export {
725
766
  CHANGELOG_MUST_SHOW,
726
767
  CHANGELOG_NEUTRAL_HINT,
727
768
  CHANGELOG_NEUTRAL_LINE,
769
+ CHANGELOG_POLL_BACKOFF_MS,
728
770
  CHANGELOG_TARBALL_MAX_BYTES,
729
771
  CHANGELOG_TAR_MAX_BYTES,
730
772
  COPY_BUDGET_CHARS,
@@ -764,6 +806,7 @@ export {
764
806
  compareReleaseVersions,
765
807
  compareVersions,
766
808
  containingPackage,
809
+ countsOf,
767
810
  createHostUpdate,
768
811
  createSkipDiskPorts,
769
812
  createUpdateDiskPorts2 as createUpdateDiskPorts,
@@ -782,6 +825,7 @@ export {
782
825
  extractChangelogFromTar,
783
826
  fetchReleaseChangelogText2 as fetchReleaseChangelogText,
784
827
  hasVisibleSections,
828
+ isBreakingChangelogItem,
785
829
  isHeadOfQueue2 as isHeadOfQueue,
786
830
  isPrereleaseVersion,
787
831
  isUnreleasedVersion,
@@ -800,6 +844,7 @@ export {
800
844
  registrySpec,
801
845
  releaseOwnerInQueue2 as releaseOwnerInQueue,
802
846
  renderChangelogHTML,
847
+ renderChangelogItem,
803
848
  renderChangelogNeutral,
804
849
  renderChangelogSection,
805
850
  resolveCliEntry,
@@ -810,9 +855,14 @@ export {
810
855
  sanitizeForCopy,
811
856
  selectChangelogEntries,
812
857
  setOwnerIfFree2 as setOwnerIfFree,
858
+ shouldFetchChangelog2 as shouldFetchChangelog,
813
859
  skipKey,
860
+ splitBreakingPrefix,
814
861
  truncateToWordBoundary,
862
+ truncatedNoteHTML,
815
863
  validReleaseVersion2 as validReleaseVersion,
816
864
  validVersion,
817
- visibleQueueFor2 as visibleQueueFor
865
+ validateChangelog,
866
+ visibleQueueFor2 as visibleQueueFor,
867
+ yankedBannerHTML
818
868
  };
@@ -1,6 +1,7 @@
1
1
  // AUTO-GENERATED by node build.mjs — DO NOT EDIT. Source: src/panel-batch.ts
2
2
  import { assertPrefix, MIN_PANEL_POLL_MS } from "./config.js";
3
3
  import { validReleaseVersion } from "./service.js";
4
+ import { shouldFetchChangelog } from "./changelog.js";
4
5
  import {
5
6
  isTerminalPhase,
6
7
  normalizeBatchSession
@@ -631,7 +632,7 @@ function mountUpdateBatchPanel(container, options) {
631
632
  const autoChangelogEnabled = options.autoChangelog !== false;
632
633
  const rowChangelog = /* @__PURE__ */ new Map();
633
634
  const rowInflight = /* @__PURE__ */ new Set();
634
- const rowFailed = /* @__PURE__ */ new Map();
635
+ const rowFailedAt = /* @__PURE__ */ new Map();
635
636
  let rowAutoSeq = 0;
636
637
  let rows = [];
637
638
  let expandedKey = null;
@@ -672,7 +673,19 @@ function mountUpdateBatchPanel(container, options) {
672
673
  const cached = rowChangelog.get(key);
673
674
  if (cached && cached.version === version) return;
674
675
  if (rowInflight.has(key)) return;
675
- if (rowFailed.get(key) === version) return;
676
+ try {
677
+ const failKey = key + "\0" + version;
678
+ const hasCache = false;
679
+ const failedAt = rowFailedAt.has(failKey) ? rowFailedAt.get(failKey) : null;
680
+ let nowMs = 0;
681
+ try {
682
+ nowMs = Date.now();
683
+ } catch {
684
+ nowMs = 0;
685
+ }
686
+ if (!shouldFetchChangelog({ hasCache, failedAt, now: nowMs, isManual: false })) return;
687
+ } catch {
688
+ }
676
689
  const phone2 = changelogPhoneOf(row);
677
690
  if (!phone2) return;
678
691
  rowInflight.add(key);
@@ -682,7 +695,14 @@ function mountUpdateBatchPanel(container, options) {
682
695
  rowInflight.delete(key);
683
696
  if (!mounted || seq !== rowAutoSeq) return;
684
697
  if (!isObject(reply) || reply["ok"] !== true) {
685
- rowFailed.set(key, version);
698
+ try {
699
+ rowFailedAt.set(key + "\0" + version, Date.now());
700
+ } catch {
701
+ try {
702
+ rowFailedAt.set(key + "\0" + version, 0);
703
+ } catch {
704
+ }
705
+ }
686
706
  return;
687
707
  }
688
708
  const md = reply["markdown"];
@@ -691,7 +711,16 @@ function mountUpdateBatchPanel(container, options) {
691
711
  },
692
712
  () => {
693
713
  rowInflight.delete(key);
694
- if (mounted && seq === rowAutoSeq) rowFailed.set(key, version);
714
+ if (mounted && seq === rowAutoSeq) {
715
+ try {
716
+ rowFailedAt.set(key + "\0" + version, Date.now());
717
+ } catch {
718
+ try {
719
+ rowFailedAt.set(key + "\0" + version, 0);
720
+ } catch {
721
+ }
722
+ }
723
+ }
695
724
  }
696
725
  );
697
726
  }
@@ -808,7 +837,7 @@ function mountUpdateBatchPanel(container, options) {
808
837
  return;
809
838
  }
810
839
  applyReply(reply);
811
- rowFailed.clear();
840
+ rowFailedAt.clear();
812
841
  } catch {
813
842
  if (!mounted) return;
814
843
  lastError = "check-failed";
package/dist/panel.d.ts CHANGED
@@ -2,7 +2,7 @@ import { type VisibleQueue } from './queue.js';
2
2
  import type { BlockedReason, UpdateSnapshot } from './ports.js';
3
3
  /** 摆放形态:默认内嵌,切弹窗走同一参数。 */
4
4
  export type UpdatePanelMode = 'embedded' | 'dialog';
5
- /** 面板主题:默认最小可用样式;`d5-paper` 为可选 D5 档案卷专业主题(只换肤,不换 DOM 顺序)。 */
5
+ /** 面板主题:默认最小可用样式;`archive` 为档案卷纸面浅色(只换肤,不换 DOM 顺序;`d5-paper` 为旧别名)。 */
6
6
  /**
7
7
  * 面板主题:`default` 最小可用深色;`archive` 档案卷纸面浅色(原型敲定的案卷风格,只换肤)。
8
8
  * `d5-paper` 是 archive 的旧别名(历史取值),仍可用,渲染逐字相同。
@@ -26,7 +26,7 @@ export interface UpdatePanelOptions {
26
26
  prefix?: string;
27
27
  /** 摆放形态:默认内嵌。 */
28
28
  mode?: UpdatePanelMode;
29
- /** 面板主题:默认 `default`(最小可用样式,一字不动);传 `d5-paper` 切 D5 档案卷可选主题。 */
29
+ /** 面板主题:默认 `default`(最小可用样式,一字不动);传 `archive` 切档案卷(`d5-paper` 为旧别名)。 */
30
30
  theme?: UpdatePanelTheme;
31
31
  /** 是否看他人排队明细:默认只看自己的(他人仅露正忙占位,位置照给)。 */
32
32
  showOthers?: boolean;
package/dist/panel.js CHANGED
@@ -5,7 +5,9 @@ import { validReleaseVersion } from "./service.js";
5
5
  import {
6
6
  changelogForUpdate,
7
7
  parseChangelog,
8
- renderChangelogHTML
8
+ renderChangelogHTML,
9
+ shouldFetchChangelog,
10
+ yankedBannerHTML
9
11
  } from "./changelog.js";
10
12
  function normalizePanelTheme(value) {
11
13
  return value === "d5-paper" || value === "archive" ? "d5-paper" : "default";
@@ -516,6 +518,12 @@ const UPDATE_PANEL_CSS = [
516
518
  ".dsh-upd-changelog li{margin:2px 0}",
517
519
  ".dsh-upd-changelog-fold{margin:4px 0}",
518
520
  ".dsh-upd-changelog-fold>summary{cursor:pointer}",
521
+ ".dsh-upd-changelog-count{font-size:12px;opacity:.7;margin:2px 0 4px}",
522
+ ".dsh-upd-changelog-yanked{border-left:4px solid var(--dsh-upd-warn-line,#d97706);background:var(--dsh-upd-warn-bg,#fffbeb);padding:6px 10px;margin:6px 0;font-size:13px}",
523
+ ".dsh-upd-breaking-badge{display:inline-block;font-size:11px;font-weight:700;border:1px solid currentColor;border-radius:3px;padding:0 5px;margin-right:6px;vertical-align:baseline}",
524
+ ".dsh-upd-changelog-security-more{margin:4px 0 6px}",
525
+ ".dsh-upd-changelog-security-more>summary{cursor:pointer;font-size:12px;opacity:.8}",
526
+ ".dsh-upd-changelog-more-note{font-size:12px;opacity:.7;margin:2px 0 4px}",
519
527
  ".dsh-upd-changelog-neutral{color:inherit;opacity:.8}",
520
528
  ".dsh-upd-overlay{position:fixed;inset:0;background:rgba(0,0,0,.35);display:flex;align-items:center;justify-content:center;z-index:9999}",
521
529
  ".dsh-upd-overlay .dsh-upd{background:var(--dsh-upd-bg,#ffffff);max-height:85vh;overflow:auto}",
@@ -593,7 +601,8 @@ const UPDATE_PANEL_D5_CSS = [
593
601
  "--d5-shadow:0 1px 2px rgba(60,40,20,.08),0 12px 32px rgba(60,40,20,.10);",
594
602
  "font-family:var(--d5-sans);color:var(--d5-ink);background:var(--d5-card);",
595
603
  "border:1px solid var(--d5-line-strong);border-radius:4px;box-shadow:var(--d5-shadow)}",
596
- '.dsh-upd[data-theme="d5-paper"] button{border-color:var(--d5-line-strong);background:transparent;color:var(--d5-ink);border-radius:3px;font-family:var(--d5-sans)}',
604
+ // 按钮脸自己不透明(宿主底色未知时也读得出;卡片上渲染与 transparent 逐字同色)。
605
+ '.dsh-upd[data-theme="d5-paper"] button{border-color:var(--d5-line-strong);background:var(--d5-card);color:var(--d5-ink);border-radius:3px;font-family:var(--d5-sans)}',
597
606
  '.dsh-upd[data-theme="d5-paper"] button:hover:not(:disabled){border-color:var(--d5-accent);color:var(--d5-accent)}',
598
607
  '.dsh-upd[data-theme="d5-paper"] button[data-primary="1"]{background:var(--d5-accent);border-color:var(--d5-accent);color:#fff}',
599
608
  '.dsh-upd[data-theme="d5-paper"] button[data-primary="1"]:hover:not(:disabled){background:var(--d5-accent-deep);color:#fff}',
@@ -604,6 +613,10 @@ const UPDATE_PANEL_D5_CSS = [
604
613
  '.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="restart"]{border-color:var(--d5-warn);background:var(--d5-warn-bg);font-family:var(--d5-serif);border-width:2px}',
605
614
  '.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="failed"],.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="blocked"]{border-color:var(--d5-bad);background:var(--d5-bad-bg)}',
606
615
  '.dsh-upd[data-theme="d5-paper"] .dsh-upd-banner[data-kind="done"]{border-color:var(--d5-ok);background:var(--d5-ok-bg)}',
616
+ '.dsh-upd[data-theme="d5-paper"] .dsh-upd-changelog-yanked{border-color:var(--d5-warn);background:var(--d5-warn-bg);color:var(--d5-ink)}',
617
+ '.dsh-upd[data-theme="d5-paper"] .dsh-upd-breaking-badge{color:var(--d5-accent);border-color:var(--d5-accent)}',
618
+ '.dsh-upd[data-theme="d5-paper"] .dsh-upd-changelog-count{color:var(--d5-muted)}',
619
+ '.dsh-upd[data-theme="d5-paper"] .dsh-upd-changelog-more-note{color:var(--d5-muted)}',
607
620
  // —— 大印章(原型 :206 `.seal`:右上 88px、旋转 -7°、双细框;内容与色调来自根属性,不加节点)——
608
621
  '.dsh-upd[data-theme="d5-paper"]{position:relative;padding:22px 26px 20px}',
609
622
  '.dsh-upd[data-theme="d5-paper"]::before{content:attr(data-seal);position:absolute;top:20px;right:24px;width:88px;height:88px;',
@@ -791,7 +804,22 @@ function renderUpdatePanelKernel(input, view) {
791
804
  const fromText = String(snapshot.runningVersion ?? "");
792
805
  const toText = String(snapshot.latestVersion ?? "");
793
806
  const rangeTitle = fromText && toText ? `\u66F4\u65B0\u8BF4\u660E\uFF08${fromText} \u2192 ${toText}\uFF09\uFF1A` : "\u66F4\u65B0\u8BF4\u660E\uFF1A";
794
- inner = `<div class="dsh-upd-changelog-wrap"><div>${escapeHtml(rangeTitle)}</div>
807
+ let yankedBanner = "";
808
+ try {
809
+ const toEntry = Array.isArray(ranged) ? ranged.find(function(e) {
810
+ try {
811
+ return e && e.version === toText;
812
+ } catch {
813
+ return false;
814
+ }
815
+ }) : null;
816
+ if (toEntry && toEntry.yanked === true && toText) {
817
+ yankedBanner = yankedBannerHTML(toText);
818
+ }
819
+ } catch {
820
+ yankedBanner = "";
821
+ }
822
+ inner = `<div class="dsh-upd-changelog-wrap"><div>${escapeHtml(rangeTitle)}</div>${yankedBanner}
795
823
  ${changelogHTML}
796
824
  </div>`;
797
825
  } catch {
@@ -951,7 +979,7 @@ function mountUpdatePanel(container, options) {
951
979
  let manualChangelogOverride = typeof options.changelogMarkdown === "string";
952
980
  const changelogCache = /* @__PURE__ */ new Map();
953
981
  const changelogInflight = /* @__PURE__ */ new Set();
954
- const changelogFailed = /* @__PURE__ */ new Set();
982
+ const changelogFailedAt = /* @__PURE__ */ new Map();
955
983
  let autoSeq = 0;
956
984
  const pollMs = options.pollMs === void 0 ? DEFAULT_PANEL_POLL_MS : options.pollMs;
957
985
  if (typeof pollMs !== "number" || !Number.isFinite(pollMs) || pollMs < MIN_PANEL_POLL_MS) {
@@ -1069,7 +1097,16 @@ function mountUpdatePanel(container, options) {
1069
1097
  if (skipStore.has(v)) return null;
1070
1098
  } catch {
1071
1099
  }
1072
- if (changelogCache.has(v) || changelogInflight.has(v) || changelogFailed.has(v)) return null;
1100
+ if (changelogInflight.has(v)) return null;
1101
+ const hasCache = changelogCache.has(v);
1102
+ const failedAt = changelogFailedAt.has(v) ? changelogFailedAt.get(v) : null;
1103
+ let nowMs = 0;
1104
+ try {
1105
+ nowMs = Date.now();
1106
+ } catch {
1107
+ nowMs = 0;
1108
+ }
1109
+ if (!shouldFetchChangelog({ hasCache, failedAt, now: nowMs, isManual: false })) return null;
1073
1110
  return v;
1074
1111
  }
1075
1112
  function maybeAutoChangelog() {
@@ -1082,7 +1119,14 @@ function mountUpdatePanel(container, options) {
1082
1119
  changelogInflight.delete(v);
1083
1120
  if (!mounted || seq !== autoSeq) return;
1084
1121
  if (!reply || typeof reply !== "object" || reply["ok"] !== true) {
1085
- changelogFailed.add(v);
1122
+ try {
1123
+ changelogFailedAt.set(v, Date.now());
1124
+ } catch {
1125
+ try {
1126
+ changelogFailedAt.set(v, 0);
1127
+ } catch {
1128
+ }
1129
+ }
1086
1130
  return;
1087
1131
  }
1088
1132
  const md = reply["markdown"];
@@ -1094,7 +1138,16 @@ function mountUpdatePanel(container, options) {
1094
1138
  },
1095
1139
  () => {
1096
1140
  changelogInflight.delete(v);
1097
- if (mounted && seq === autoSeq) changelogFailed.add(v);
1141
+ if (mounted && seq === autoSeq) {
1142
+ try {
1143
+ changelogFailedAt.set(v, Date.now());
1144
+ } catch {
1145
+ try {
1146
+ changelogFailedAt.set(v, 0);
1147
+ } catch {
1148
+ }
1149
+ }
1150
+ }
1098
1151
  }
1099
1152
  );
1100
1153
  }
@@ -1126,7 +1179,7 @@ function mountUpdatePanel(container, options) {
1126
1179
  const reply = await call(phoneNames.updateCheck, queueArgs());
1127
1180
  if (!mounted) return;
1128
1181
  applyStatusReply(reply);
1129
- changelogFailed.clear();
1182
+ changelogFailedAt.clear();
1130
1183
  } catch {
1131
1184
  if (!mounted) return;
1132
1185
  lastError = "check-failed";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-update",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "可复用的更新系统装成的 npm 包:任何 DSH 插件照文档能集成检查更新等功能(地图 #579)",
5
5
  "license": "MIT",
6
6
  "author": "王辰浩",