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 +16 -0
- package/lib/client.js +96 -39
- package/package.json +1 -1
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
|
-
// ----
|
|
348
|
-
//
|
|
349
|
-
//
|
|
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
|
|
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(
|
|
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)
|
|
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
|
-
//
|
|
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)
|
|
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)
|
|
401
|
+
runWrite(() => controller.set(key, value))
|
|
394
402
|
}
|
|
395
403
|
const resetAll = () => {
|
|
396
404
|
dirty.current = {}
|
|
397
405
|
setErrors({})
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
await
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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
|
-
|
|
680
|
-
|
|
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) =>
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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.
|
|
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",
|