dsh-notify-say 0.4.48 → 0.4.50

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
@@ -3,6 +3,14 @@
3
3
  > 本文件由 `tools/release-build.ps1` 从开发提交自动生成,**请勿手工编辑**。
4
4
  > 只列面向使用者的变更(新增功能与修复),不含内部维护提交。
5
5
 
6
+ ## v0.4.50 — 2026-10-18
7
+
8
+ - fix(update): v0.4.50 —— 两个源并行查、取较高版本
9
+
10
+ ## v0.4.49 — 2026-10-18
11
+
12
+ - fix(update): v0.4.49 —— hasUpdate 方向反了(实测抓到)
13
+
6
14
  ## v0.4.48 — 2026-10-18
7
15
 
8
16
  - feat(update): v0.4.48 —— 检查更新 + 引导升级(不做自动更新)
@@ -259,33 +259,61 @@ export async function checkForUpdate(opts = {}) {
259
259
 
260
260
  const timed = withTimeout(doFetch, opts.timeoutMs || FETCH_TIMEOUT_MS);
261
261
 
262
- // 先 npm,失败回退 GitHub
263
- let r = await fetchFromNpm(timed);
264
- let source = 'npm';
265
- if (!r.ok) {
266
- const r2 = await fetchFromGithub(timed);
267
- if (r2.ok) { r = r2; source = 'github'; }
268
- else {
269
- // 两个都失败:明确报告,但**不影响插件任何功能**
270
- const result = {
271
- ok: false,
272
- ...base,
273
- error: `npm(${r.error}) / github(${r2.error})`,
274
- cached: false,
275
- checkedAt: now,
276
- };
277
- if (cache) { cache.at = now; cache.result = result; }
278
- return result;
279
- }
262
+ // ── 并行查两个源,取较高版本 ─────────────────────────────────────────
263
+ //
264
+ // ⚠️ 不能"先 npm、失败才回退 GitHub"。实测(2026-10-18):
265
+ // npm latest = 0.4.47(暂存队列要过一阵才上架)
266
+ // GitHub main = 0.4.48
267
+ // 只查 npm 的话,这期间**所有人都看不到 0.4.48 可用** ——
268
+ // 而 GitHub 安装的用户本来就是以 main 为准的。
269
+ //
270
+ // 两个源都是同一套发布流程产出的正式版本,所以取较新的那个是安全的。
271
+ const [npmR, ghR] = await Promise.all([
272
+ fetchFromNpm(timed).catch((e) => ({ ok: false, error: String((e && e.message) || e) })),
273
+ fetchFromGithub(timed).catch((e) => ({ ok: false, error: String((e && e.message) || e) })),
274
+ ]);
275
+
276
+ // 挑出可用的版本,取最高的
277
+ const candidates = [];
278
+ if (npmR.ok) candidates.push({ version: npmR.version, source: 'npm' });
279
+ if (ghR.ok) candidates.push({ version: ghR.version, source: 'github' });
280
+
281
+ if (candidates.length === 0) {
282
+ // 两个都失败:明确报告,但**不影响插件任何功能**
283
+ const result = {
284
+ ok: false,
285
+ ...base,
286
+ error: `npm(${npmR.error}) / github(${ghR.error})`,
287
+ cached: false,
288
+ checkedAt: now,
289
+ };
290
+ if (cache) { cache.at = now; cache.result = result; }
291
+ return result;
292
+ }
293
+
294
+ let best = candidates[0];
295
+ for (const c of candidates.slice(1)) {
296
+ const d = compareVersions(c.version, best.version);
297
+ // 解析不出时保留先到的(npm 优先),不因为格式怪就丢掉可用信息
298
+ if (d === 1) best = c;
280
299
  }
281
300
 
301
+ const r = { version: best.version };
302
+ const source = candidates.length > 1 ? `${best.source}+${candidates.length - 1}` : best.source;
303
+
282
304
  const cmp = compareVersions(r.version, current);
283
305
  const result = {
284
306
  ok: true,
285
307
  ...base,
286
308
  latest: r.version,
287
- // 解析不出(比如本地是 0.0.0-stage)时不下"有新版"的结论
288
- hasUpdate: cmp === null ? null : cmp < 0,
309
+ // ⚠️ 方向是 `latest > current` 才算有新版。
310
+ //
311
+ // 第一版写成 `cmp < 0` —— **反了**。实测才发现:本地 0.4.48、
312
+ // npm 还在暂存队列里是 0.4.47,界面却显示"有新版"。
313
+ // 也就是说:npm 落后时装作有新版本,npm 超前时反而不提示。
314
+ //
315
+ // 解析不出(比如本地是 0.0.0-stage)时不下"有新版"的结论。
316
+ hasUpdate: cmp === null ? null : cmp > 0,
289
317
  source,
290
318
  cached: false,
291
319
  checkedAt: now,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-notify-say",
3
- "version": "0.4.48",
3
+ "version": "0.4.50",
4
4
  "description": "Desktop notifications for DSH Desktop that actually speak: Windows TTS out of the box (zero download), VOICEVOX voice packs optional, a selectable system-sound fallback, and an idle gate that waits for running subagents before saying the turn is done. Includes a settings-page GUI (sound source, per-event overrides, jump-back-to-session, cooldowns) and a self hot-reload for development.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -116,13 +116,18 @@ test('U5: fetch 抛异常时返回结构化错误,不抛出', async () => {
116
116
 
117
117
  // ── U6: 缓存 24 小时,force 可绕过 ──────────────────────────────────────
118
118
  test('U6: 宿主侧缓存 24 小时,force 绕过', async () => {
119
- const { checkForUpdate, AUTO_CHECK_TTL_MS } = await import('../lib/update-check.js');
119
+ const { checkForUpdate, AUTO_CHECK_TTL_MS, NPM_REGISTRY_URL } = await import('../lib/update-check.js');
120
120
  const cache = { at: 0, result: null };
121
121
 
122
+ // ⚠️ 现在一次检查会**并行查两个源**(npm + GitHub),所以不能用总请求数
123
+ // 衡量"查了几次"。只数 npm 那个请求 —— 每次检查恰好发一次。
122
124
  let calls = 0;
123
- const fake = async () => {
124
- calls++;
125
- return { ok: true, json: async () => ({ 'dist-tags': { latest: '0.4.99' } }) };
125
+ const fake = async (url) => {
126
+ if (url === NPM_REGISTRY_URL) calls++;
127
+ if (url === NPM_REGISTRY_URL) {
128
+ return { ok: true, json: async () => ({ 'dist-tags': { latest: '0.4.99' } }) };
129
+ }
130
+ return { ok: true, json: async () => ({ version: '0.4.99' }) };
126
131
  };
127
132
 
128
133
  const t0 = 1_700_000_000_000;
@@ -241,3 +246,113 @@ test('U11: 版本信息用独立 useState(避开 25 处 set 的字段遗漏坑
241
246
  assert.doesNotMatch(mainState, /^\s*version:/m,
242
247
  '版本字段不该塞进主 state');
243
248
  });
249
+
250
+ // ── U12: hasUpdate 的方向必须正确(本地旧→true,本地新→false)──────────
251
+ test('U12: hasUpdate 方向 —— 只在远端更新时为 true', async () => {
252
+ // ⚠️ 2026-10-18 实测抓到的真 bug:第一版写成 `cmp < 0`,**方向反了**。
253
+ //
254
+ // 表现:本地 0.4.48、npm 还在暂存队列里是 0.4.47 ——
255
+ // 界面却显示"有新版"。也就是 npm 落后时装作有新版本,
256
+ // npm 超前时反而不提示。
257
+ //
258
+ // 这个 bug 能溜过去,是因为 U1 只测了 compareVersions 本身,
259
+ // 没有任何用例断言 hasUpdate 的**方向**。所以补上。
260
+ const { checkForUpdate } = await import('../lib/update-check.js');
261
+
262
+ const tmp = join(ROOT, '.tmp-u12-' + Math.random().toString(36).slice(2, 8));
263
+ try {
264
+ mkdirSync(tmp, { recursive: true });
265
+
266
+ // ① 远端更新(0.9.9 > 0.4.48)→ 有新版
267
+ writeFileSync(join(tmp, 'package.json'), JSON.stringify({ version: '0.4.48' }));
268
+ const newer = async () => ({ ok: true, json: async () => ({ 'dist-tags': { latest: '0.9.9' } }) });
269
+ const r1 = await checkForUpdate({ pluginRoot: tmp, force: true, fetchImpl: newer });
270
+ assert.equal(r1.hasUpdate, true,
271
+ '远端 0.9.9 > 本地 0.4.48,必须提示有新版');
272
+
273
+ // ② 远端落后(npm 暂存队列期间会这样)→ 不能提示有新版
274
+ const older = async () => ({ ok: true, json: async () => ({ 'dist-tags': { latest: '0.4.40' } }) });
275
+ const r2 = await checkForUpdate({ pluginRoot: tmp, force: true, fetchImpl: older });
276
+ assert.equal(r2.hasUpdate, false,
277
+ '远端 0.4.40 < 本地 0.4.48,**不能**提示有新版 —— '
278
+ + 'npm 暂存队列期间就会出现这种"远端落后",提示更新会让用户困惑');
279
+
280
+ // ③ 相同版本 → 无新版
281
+ const same = async () => ({ ok: true, json: async () => ({ 'dist-tags': { latest: '0.4.48' } }) });
282
+ const r3 = await checkForUpdate({ pluginRoot: tmp, force: true, fetchImpl: same });
283
+ assert.equal(r3.hasUpdate, false, '版本相同不应提示更新');
284
+ } finally {
285
+ rmSync(tmp, { recursive: true, force: true });
286
+ }
287
+ });
288
+ // ── U13: 两个源都查,取较高版本(npm 暂存队列期间不能漏更新)────────────
289
+ test('U13: 并行查两个源,取较高版本', async () => {
290
+ // 实测场景(2026-10-18):npm 有暂存队列,发布后 latest 会落后一阵。
291
+ // npm latest = 0.4.47
292
+ // GitHub main = 0.4.48
293
+ // 若"先 npm、失败才回退 GitHub",这期间**所有人都看不到 0.4.48 可用**,
294
+ // 而 GitHub 安装的用户本来就是以 main 为准的。
295
+ //
296
+ // 所以并行查两个源、取较高版本。两个源都是同一套发布流程的正式产物。
297
+ const { checkForUpdate, NPM_REGISTRY_URL, GITHUB_RAW_PKG_URL } =
298
+ await import('../lib/update-check.js');
299
+
300
+ const tmp = join(ROOT, '.tmp-u13-' + Math.random().toString(36).slice(2, 8));
301
+ try {
302
+ mkdirSync(tmp, { recursive: true });
303
+ writeFileSync(join(tmp, 'package.json'), JSON.stringify({ version: '0.4.47' }));
304
+
305
+ const seen = [];
306
+ // npm 落后、GitHub 较新
307
+ const fake = async (url) => {
308
+ seen.push(url);
309
+ if (url === NPM_REGISTRY_URL) {
310
+ return { ok: true, json: async () => ({ 'dist-tags': { latest: '0.4.47' } }) };
311
+ }
312
+ if (url === GITHUB_RAW_PKG_URL) {
313
+ return { ok: true, json: async () => ({ version: '0.4.48' }) };
314
+ }
315
+ return { ok: false, status: 404 };
316
+ };
317
+
318
+ const r = await checkForUpdate({ pluginRoot: tmp, force: true, fetchImpl: fake });
319
+
320
+ assert.equal(r.ok, true);
321
+ assert.equal(r.latest, '0.4.48',
322
+ 'npm 落后时应采信 GitHub 的更高版本 —— 否则用户永远等不到更新提示');
323
+ assert.equal(r.hasUpdate, true, '0.4.48 > 0.4.47,应有新版');
324
+ assert.match(r.source, /github/,
325
+ 'source 应标明采用了 GitHub 的版本');
326
+
327
+ // 两个源都要真的查过(不是串行等待)
328
+ assert.ok(seen.includes(NPM_REGISTRY_URL), '应查 npm');
329
+ assert.ok(seen.includes(GITHUB_RAW_PKG_URL), '应查 GitHub');
330
+ } finally {
331
+ rmSync(tmp, { recursive: true, force: true });
332
+ }
333
+ });
334
+
335
+ // ── U14: 反向 —— 本地较新时不提示(npm 超前也不会误报)──────────────────
336
+ test('U14: 两个源都落后于本地时不提示更新', async () => {
337
+ const { checkForUpdate, NPM_REGISTRY_URL, GITHUB_RAW_PKG_URL } =
338
+ await import('../lib/update-check.js');
339
+
340
+ const tmp = join(ROOT, '.tmp-u14-' + Math.random().toString(36).slice(2, 8));
341
+ try {
342
+ mkdirSync(tmp, { recursive: true });
343
+ writeFileSync(join(tmp, 'package.json'), JSON.stringify({ version: '0.5.0' }));
344
+
345
+ const fake = async (url) => {
346
+ if (url === NPM_REGISTRY_URL) {
347
+ return { ok: true, json: async () => ({ 'dist-tags': { latest: '0.4.40' } }) };
348
+ }
349
+ return { ok: true, json: async () => ({ version: '0.4.48' }) };
350
+ };
351
+
352
+ const r = await checkForUpdate({ pluginRoot: tmp, force: true, fetchImpl: fake });
353
+ assert.equal(r.latest, '0.4.48', '取较高的那个(0.4.48 > 0.4.40)');
354
+ assert.equal(r.hasUpdate, false, '两个源都低于本地 0.5.0,不该提示更新');
355
+ } finally {
356
+ rmSync(tmp, { recursive: true, force: true });
357
+ }
358
+ });