dsh-dupguard 1.6.2 → 1.6.3

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,22 @@
2
2
 
3
3
  本文件遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [SemVer](https://semver.org/lang/zh-CN/)。
4
4
 
5
+ ## [1.6.3] - 2026-09-24
6
+
7
+ ### Fixed
8
+
9
+ - **写入成功却被判为「保存失败」**:`remote.settings.mutate()` 返回的命名空间视图是**写入前**的快照
10
+ (配置由 loader 异步重载后才更新),此前的实现拿它与本地期望值做回读比对,于是每次写入都
11
+ 必然不一致——面板报错、看起来「没保存」,而宿主其实已经落盘。现改为**只以宿主应答为准**
12
+ (`response.ok === false` 才算失败),与官方 `ConfigForm.mutate` 的语义一致;恢复默认同样按
13
+ 「全部 unset 是否未被拒绝」判定。
14
+
15
+ ### Added
16
+
17
+ - 设置页底部常驻诊断行 `设置通道:<状态>|<诊断>|构建 <标记>`,并在写入失败时附带宿主原始原因
18
+ 与构建标记;浏览器控制台同步打印 `[dupguard] mutate → …` / `mutate 被拒…`,用于快速区分
19
+ 「宿主拒绝」「通道未就绪」「回读不一致」三类问题。
20
+
5
21
  ## [1.6.2] - 2026-09-24
6
22
 
7
23
  ### Fixed
package/lib/client.js CHANGED
@@ -26,6 +26,8 @@ window.__ModuleLoader__.load({
26
26
  const React = require('react')
27
27
 
28
28
  const NS = 'dsh-dupguard'
29
+ /** 构建标记:显示在设置页底部,用于判断浏览器实际加载的是哪一版(排查缓存/旧包问题)。 */
30
+ const BUILD_MARK = '1.6.3'
29
31
  /** 旧版(≤ 0.1.6)register 出来的命名空间名。 */
30
32
  const SETTINGS_NS = 'dsh-dupguard'
31
33
  /** 新版(≥ 0.1.7)命名空间取自 loader entry id:本 bundle 插入的行 id 为 dupguard。 */
@@ -344,28 +346,34 @@ window.__ModuleLoader__.load({
344
346
  }
345
347
  : null
346
348
 
347
- // ---- 写路径:settingsScope 控制器(DSH 自己的设置写通道,自动携带 revision、
348
- // 串行化并发写、把宿主应答折叠回镜像)。DSH 0.1.2 起客户端不再暴露
349
- // connection.api,控制器接口自 0.1.1 起稳定,故不直接触碰 wire 面。 ----
349
+ // ---- 写路径:设置控制器(新版 remote.settings,旧版 settingsScope)。 ----
350
+ // 写成功与否**只以宿主应答为准**:宿主返回的视图是写入前的快照(配置由 loader
351
+ // 异步重载),拿它跟本地期望值比对必然不一致——这正是此前「明明保存成功却报
352
+ // 保存失败、重开后新值不见了」的原因。官方实现同样是信任 response.ok。
350
353
  const snapshotValue = () => {
351
354
  const current = controller.getSnapshot()
352
355
  return current !== undefined && current.value !== undefined ? current.value : {}
353
356
  }
354
- const sameList = (a, b) => Array.isArray(a) && Array.isArray(b) &&
355
- a.length === b.length && a.every((item, index) => item === b[index])
356
357
  const currentForm = () => (form === null ? remoteForm : form)
357
- const runWrite = (operation, verify) => {
358
+ const runWrite = (operation) => {
358
359
  setWriteState('saving')
360
+ // 失败时带上宿主返回的原始原因(writeReason)与构建标记:
361
+ // 标记同时用于判断浏览器加载的是哪一版(排查旧包/缓存)。
362
+ const failureText = () => {
363
+ const reason = typeof props.writeReason === 'function' ? props.writeReason() : null
364
+ const suffix = reason === null || reason === undefined ? '(宿主未给出原因)' : String(reason)
365
+ return t('saveFailed') + ' [diag ' + BUILD_MARK + '] ' + suffix
366
+ }
359
367
  Promise.resolve()
360
368
  .then(() => operation())
361
- .then(() => setWriteState(verify() ? 'saved' : 'error:' + t('saveFailed')), (error) => {
369
+ .then((accepted) => setWriteState(accepted === false ? 'error:' + failureText() : 'saved'), (error) => {
362
370
  setWriteState('error:' + String((error && error.message) || error))
363
371
  })
364
372
  }
365
373
  const commitList = (next) => {
366
374
  dirty.current.ignoredChars = true
367
375
  setForm({ ...currentForm(), ignoredChars: next })
368
- runWrite(() => controller.set('ignoredChars', next), () => sameList(snapshotValue().ignoredChars, next))
376
+ runWrite(() => controller.set('ignoredChars', next))
369
377
  }
370
378
  const commitNumber = (field) => {
371
379
  const message = fieldError(field)
@@ -376,7 +384,7 @@ window.__ModuleLoader__.load({
376
384
  const value = parsed[field.key]
377
385
  setErrors((prev) => ({ ...prev, [field.key]: null }))
378
386
  // 值未变化时不写入:避免仅仅聚焦/失焦就把默认值写进用户层,
379
- // 污染 settings.yaml 并让「恢复默认」失去意义。
387
+ // 污染配置文件并让「恢复默认」失去意义。
380
388
  if (snapshotValue()[field.key] === value) {
381
389
  dirty.current[field.key] = false
382
390
  setForm({ ...currentForm(), [field.key]: String(value) })
@@ -384,27 +392,20 @@ window.__ModuleLoader__.load({
384
392
  }
385
393
  dirty.current[field.key] = true
386
394
  setForm({ ...currentForm(), [field.key]: String(value) })
387
- runWrite(() => controller.set(field.key, value), () => snapshotValue()[field.key] === value)
395
+ runWrite(() => controller.set(field.key, value))
388
396
  }
389
397
  const commitBool = (key) => {
390
398
  const value = shown[key] !== true
391
399
  dirty.current[key] = true
392
400
  setForm({ ...currentForm(), [key]: value })
393
- runWrite(() => controller.set(key, value), () => snapshotValue()[key] === value)
401
+ runWrite(() => controller.set(key, value))
394
402
  }
395
403
  const resetAll = () => {
396
404
  dirty.current = {}
397
405
  setErrors({})
398
- runWrite(
399
- () => Promise.all(ALL_FIELDS.map((key) => controller.unset(key))),
400
- () => {
401
- setForm(toForm(snapshotValue()))
402
- const current = controller.getSnapshot()
403
- const user = current !== undefined ? current.user : undefined
404
- if (user === undefined || user === null) return true
405
- return ALL_FIELDS.every((key) => user[key] === undefined)
406
- },
407
- )
406
+ // 恢复默认 = 逐字段 unset;接受与否同样只看宿主应答。
407
+ runWrite(() => Promise.all(ALL_FIELDS.map((key) => controller.unset(key)))
408
+ .then((results) => results.every((item) => item !== false)))
408
409
  }
409
410
 
410
411
  const add = () => {
@@ -513,6 +514,11 @@ window.__ModuleLoader__.load({
513
514
  : writeState === 'saved'
514
515
  ? React.createElement('p', { className: 'dg-note' }, t('saved'))
515
516
  : React.createElement('p', { className: 'dg-note dg-error' }, writeState),
517
+ React.createElement('p', { className: 'dg-note' },
518
+ fmt(t('loadingDiag'), {
519
+ state: mirrorSnap ? String(mirrorSnap.status) : 'unknown',
520
+ diag: (mirrorSnap && mirrorSnap.diag ? String(mirrorSnap.diag) : '') + '|构建 ' + BUILD_MARK,
521
+ })),
516
522
  ),
517
523
  )
518
524
  }
@@ -657,6 +663,32 @@ window.__ModuleLoader__.load({
657
663
  if (!closed && mine === generation) loaded = true
658
664
  }
659
665
  }
666
+ /** 写入失败时面板上展示的宿主原始原因(诊断用)。 */
667
+ let lastWriteError = null
668
+
669
+ /** 只尝试属于本插件的命名空间名:describe 报出的 ns 与其去掉组合前缀的形式。 */
670
+ const namespaceCandidates = () => {
671
+ const list = [resolvedNamespace]
672
+ const cut = resolvedNamespace.lastIndexOf(':')
673
+ if (cut !== -1 && cut + 1 < resolvedNamespace.length) {
674
+ const stripped = resolvedNamespace.slice(cut + 1)
675
+ if (list.indexOf(stripped) === -1) list.push(stripped)
676
+ }
677
+ return list
678
+ }
679
+
680
+ const attempt = async (namespace, owned, expected) => {
681
+ let response
682
+ try {
683
+ response = await settings.mutate(namespace, owned, expected)
684
+ } catch (error) {
685
+ return { ok: false, error: errorText(error) }
686
+ }
687
+ if (response === null || typeof response !== 'object') return { ok: false, error: '宿主应答无效' }
688
+ if (response.ok === true) return { ok: true, value: response.value }
689
+ return { ok: false, error: responseError(response, '宿主拒绝了该写入') }
690
+ }
691
+
660
692
  const mutate = (ops) => {
661
693
  const owned = ops.map((op) => (op.op === 'set'
662
694
  ? { op: 'set', path: [...op.path], value: op.value }
@@ -665,26 +697,40 @@ window.__ModuleLoader__.load({
665
697
  if (closed || channel === null) return false
666
698
  // 首次写入前先完成一次 describe:否则命名空间与 revision 都还是初始值。
667
699
  if (!loaded) await load()
668
- let response
669
- try {
670
- response = await settings.mutate(resolvedNamespace, owned, revision)
671
- } catch (_error) {
672
- await load()
673
- return false
674
- }
675
- if (response === null || typeof response !== 'object' || response.ok !== true) {
676
- await load() // 写失败:回读宿主真实状态,避免界面停留在错误的本地值
677
- return false
700
+ const failures = []
701
+ for (const namespace of namespaceCandidates()) {
702
+ console.info('[dupguard] mutate → ' + namespace + ' revision=' + String(revision) +
703
+ ' paths=' + JSON.stringify(owned.map((op) => op.path)))
704
+ let result = await attempt(namespace, owned, revision)
705
+ if (!result.ok) {
706
+ // 常见于并发写入导致的 revision 过期:回读后按新 revision 重试一次。
707
+ console.warn('[dupguard] mutate 被拒(' + namespace + '):' + result.error + ',回读后重试')
708
+ await load()
709
+ result = await attempt(namespace, owned, revision)
710
+ }
711
+ if (result.ok) {
712
+ lastWriteError = null
713
+ if (!closed) acceptView(result.value)
714
+ publish(undefined, { status: mirror.status, error: null, diag: '已写入命名空间 ' + namespace })
715
+ return true
716
+ }
717
+ failures.push(namespace + ':' + result.error)
678
718
  }
679
- if (!closed) acceptView(response.value)
680
- return true
719
+ lastWriteError = failures.join(';')
720
+ // 写失败:回读宿主真实状态,避免界面停留在错误的本地值;并把原因显示到面板。
721
+ await load()
722
+ publish(undefined, { status: mirror.status, error: '写入被拒绝 —— ' + lastWriteError, diag: 'mutate 失败' })
723
+ return false
681
724
  })
682
725
  tail = task.then(() => {}, () => {})
683
726
  return task
684
727
  }
728
+ /** 供组件展示的最近一次写入失败原因。 */
729
+ const writeError = () => lastWriteError
685
730
  return {
686
731
  load,
687
732
  mutate,
733
+ writeError,
688
734
  close: () => {
689
735
  closed = true
690
736
  },
@@ -763,12 +809,20 @@ window.__ModuleLoader__.load({
763
809
  controller: {
764
810
  subscribe,
765
811
  getSnapshot: () => snapshot,
766
- set: (field, value) => (channel === null
767
- ? Promise.resolve(false)
768
- : channel.mutate([{ op: 'set', path: [field], value }])),
769
- unset: (field) => (channel === null
770
- ? Promise.resolve(false)
771
- : channel.mutate([{ op: 'unset', path: [field] }])),
812
+ set: (field, value) => {
813
+ if (channel === null) {
814
+ lastWriteError = '设置通道未就绪(channel=null)'
815
+ return Promise.resolve(false)
816
+ }
817
+ return channel.mutate([{ op: 'set', path: [field], value }])
818
+ },
819
+ unset: (field) => {
820
+ if (channel === null) {
821
+ lastWriteError = '设置通道未就绪(channel=null)'
822
+ return Promise.resolve(false)
823
+ }
824
+ return channel.mutate([{ op: 'unset', path: [field] }])
825
+ },
772
826
  },
773
827
  mirror: {
774
828
  subscribe,
@@ -781,6 +835,8 @@ window.__ModuleLoader__.load({
781
835
  },
782
836
  /** 更新面板上的通道诊断文本(不改变数据状态)。 */
783
837
  note: (text) => publish(undefined, { status: mirror.status, error: mirror.error, diag: text }),
838
+ /** 最近一次写入失败的宿主原因(旧版通道不提供时返回 null)。 */
839
+ writeReason: () => (channel !== null && typeof channel.writeError === 'function' ? channel.writeError() : null),
784
840
  disposed: () => disposed,
785
841
  /** 是否已有可用通道(旧版接上后即可停止新版探测)。 */
786
842
  hasChannel: () => channel !== null,
@@ -921,6 +977,7 @@ window.__ModuleLoader__.load({
921
977
  // 传函数而非快照:远程/只读状态可能由 describe 的 writable 或
922
978
  // remote.$host.isLoopback 稍后确定,渲染时必须取最新值。
923
979
  isLoopback: () => bridge.isLoopback(),
980
+ writeReason: () => bridge.writeReason(),
924
981
  })
925
982
  ctx.slots.inject('settings.section', () => ctx.slots.register({
926
983
  name: 'settings.section',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-dupguard",
3
- "version": "1.6.2",
3
+ "version": "1.6.3",
4
4
  "description": "Real-time repetition guard for DeepSeek Harness (DSH): stops model generation when the same string repeats >=10 times in the streamed output. 实时检测 DSH 大模型流式输出中的重复内容,同一字符串重复十次以上立即停止生成。",
5
5
  "keywords": [
6
6
  "dsh-plugin",