dsh-repeat-guard 0.1.5 → 0.2.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/README.md CHANGED
@@ -102,6 +102,14 @@ Let me do it.
102
102
  dsh plugin --profile web add dsh-repeat-guard
103
103
  ```
104
104
 
105
+ > **兼容性**:`0.2.0` 起要求 dsh **0.1.7 及以上**——0.1.7 起宿主会在装载前校验插件的
106
+ > peer 声明,配置表单也换成了 entry Config 模型,旧写法在那上面不再工作。
107
+ > dsh `0.1.0-rc.8` ~ `0.1.6` 请装 `0.1.5`:
108
+ >
109
+ > ```bash
110
+ > dsh plugin --profile web add dsh-repeat-guard@0.1.5
111
+ > ```
112
+
105
113
  ## 许可
106
114
 
107
115
  MIT,见 `LICENSE`。
package/lib/client.js CHANGED
@@ -33,7 +33,7 @@ var __dshClientBundle = (() => {
33
33
  });
34
34
  var import_react = __require("react");
35
35
  var import_jsx_runtime = __require("react/jsx-runtime");
36
- var SETTINGS_NS = "repeat-guard";
36
+ var ENTRY_ID = "dsh-repeat-guard";
37
37
  var THRESHOLD_MIN = 1;
38
38
  var THRESHOLD_MAX = 9;
39
39
  var FIELD = `height:36px;box-sizing:border-box;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:transparent;color:var(--dsw-alias-label-primary);font-family:inherit;font-size:14px;line-height:22px`;
@@ -363,15 +363,15 @@ var __dshClientBundle = (() => {
363
363
  ] })
364
364
  ] });
365
365
  }
366
- var inject = ["slots", "settingsScope"];
366
+ var inject = ["slots", "configForms"];
367
367
  function apply(ctx) {
368
368
  injectStyle();
369
- const scope = ctx.settingsScope.bind({ namespace: SETTINGS_NS });
369
+ const form = ctx.configForms.get(ENTRY_ID);
370
370
  function Panel() {
371
- const [snapshot, setSnapshot] = (0, import_react.useState)(() => scope.getSnapshot());
371
+ const [snapshot, setSnapshot] = (0, import_react.useState)(() => form.getSnapshot());
372
372
  (0, import_react.useEffect)(
373
- () => scope.subscribe(() => {
374
- setSnapshot(scope.getSnapshot());
373
+ () => form.subscribe(() => {
374
+ setSnapshot(form.getSnapshot());
375
375
  }),
376
376
  []
377
377
  );
@@ -385,14 +385,14 @@ var __dshClientBundle = (() => {
385
385
  initial: value,
386
386
  count: value.count,
387
387
  onReset: async () => {
388
- await scope.set("count", 0);
388
+ await form.set("count", 0);
389
389
  },
390
390
  onSave: async (next) => {
391
- await scope.set("fragments", next.fragments);
392
- await scope.set("threshold", next.threshold);
393
- await scope.set("inlineRepeat", next.inlineRepeat);
394
- await scope.set("resumeText", next.resumeText);
395
- await scope.set("resumeSummary", next.resumeSummary);
391
+ await form.set("fragments", next.fragments);
392
+ await form.set("threshold", next.threshold);
393
+ await form.set("inlineRepeat", next.inlineRepeat);
394
+ await form.set("resumeText", next.resumeText);
395
+ await form.set("resumeSummary", next.resumeSummary);
396
396
  }
397
397
  }
398
398
  );
package/lib/config.d.ts CHANGED
@@ -2,15 +2,20 @@
2
2
  * 插件配置:拦截短句表、连续命中阈值、行内重复检测、续跑注入的正文与摘要,
3
3
  * 外加一个运行时计数。
4
4
  *
5
- * 配置放在 dsh 的 settings 服务里(命名空间 repeat-guard):宿主侧在这边注册
6
- * schema,客户端设置页写同一个命名空间。判定与续跑时现取,改完立即生效,不用重启。
5
+ * dsh 0.1.7 起,配置就是插件自己的 entry Config——标记 `.volatile()` 的字段由宿主
6
+ * 的设置表单读写,写回的是 profile 的 cordis patch。宿主侧不再注册命名空间,
7
+ * 而是持有这些稳定引用,需要时 `.get()` 现取,所以改完立即生效,不用重启。
7
8
  *
8
- * 计数也寄在同一个命名空间里:宿主侧每次掐断写一次,客户端读同一个值、清零就是
9
- * 把它写成 0。插件加载时先把它归零,所以口径是"自本次启动或上次清零以来"。
9
+ * 计数也寄在同一份配置里:宿主侧每次掐断写一次,客户端读同一个值、清零就是把它
10
+ * 写成 0。插件加载时先把它归零,所以口径是"自本次启动或上次清零以来"。
10
11
  */
11
12
  import type { Context } from '@deepseek-ai/cordis';
12
- /** settings 命名空间。客户端设置页必须写同一个值。 */
13
- export declare const SETTINGS_NS = "repeat-guard";
13
+ import z from '@deepseek-ai/schemastery';
14
+ /**
15
+ * profile 里本插件的条目 id。设置表单按它定位配置,宿主写回也要用它,
16
+ * 所以必须与 cordis.patch.yml 里的 `id` 逐字一致。
17
+ */
18
+ export declare const ENTRY_ID = "dsh-repeat-guard";
14
19
  /** 默认的拦截短句表,标点照原样写。 */
15
20
  export declare const DEFAULT_FRAGMENTS: readonly string[];
16
21
  /** 默认阈值:1 表示发现即拦。 */
@@ -21,6 +26,27 @@ export declare const DEFAULT_INLINE_REPEAT = false;
21
26
  export declare const DEFAULT_RESUME_TEXT: string;
22
27
  /** 默认的折叠行摘要;客户端拿它渲染折叠的 context 行,不是用户气泡。 */
23
28
  export declare const DEFAULT_RESUME_SUMMARY = "\u590D\u8BFB\u5DF2\u622A\u65AD\uFF1A\u8BF7\u7EE7\u7EED\u6267\u884C";
29
+ /**
30
+ * 插件 Config schema。只有标了 `.volatile()` 的字段会进设置表单并支持编辑,
31
+ * 其余字段仍是普通配置(只由 cordis 配置文件决定)。
32
+ */
33
+ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
34
+ fragments: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
35
+ threshold: z<number, number, "volatile-defined">;
36
+ inlineRepeat: z<boolean, boolean, "volatile-defined">;
37
+ resumeText: z<string, string, "volatile-defined">;
38
+ resumeSummary: z<string, string, "volatile-defined">;
39
+ count: z<number, number, "volatile-defined">;
40
+ }>>, Schemastery.ObjectT<NoInfer<{
41
+ fragments: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
42
+ threshold: z<number, number, "volatile-defined">;
43
+ inlineRepeat: z<boolean, boolean, "volatile-defined">;
44
+ resumeText: z<string, string, "volatile-defined">;
45
+ resumeSummary: z<string, string, "volatile-defined">;
46
+ count: z<number, number, "volatile-defined">;
47
+ }>>, "plain">;
48
+ /** loader 解析后交给插件的配置:volatile 字段是稳定引用,取值要 `.get()`。 */
49
+ export type RepeatGuardConfigSchema = Schemastery.TypeT<typeof Config>;
24
50
  /** 判定与续跑用得上的一份配置。 */
25
51
  export interface RepeatGuardConfig {
26
52
  /** 短句表,已全部小写化。 */
@@ -40,12 +66,13 @@ export type ConfigSource = () => RepeatGuardConfig;
40
66
  export interface GuardRuntime {
41
67
  /** 取当前配置。 */
42
68
  readonly read: ConfigSource;
43
- /** 记一次拦截,把计数写回 settings。 */
69
+ /** 记一次拦截,把计数写回配置。 */
44
70
  readonly countHit: () => void;
45
71
  }
46
72
  /**
47
- * 注册配置命名空间,返回宿主侧要用的运行时句柄。
73
+ * 把 volatile 引用折叠成一份快照,返回宿主侧要用的运行时句柄。
48
74
  * @param ctx - 宿主 cordis 上下文。
75
+ * @param config - loader 解析后的本插件配置。
49
76
  * @returns 取配置与记数两个入口。
50
77
  */
51
- export declare function createRuntime(ctx: Context): GuardRuntime;
78
+ export declare function createRuntime(ctx: Context, config: RepeatGuardConfigSchema): GuardRuntime;
package/lib/config.js CHANGED
@@ -2,15 +2,19 @@
2
2
  * 插件配置:拦截短句表、连续命中阈值、行内重复检测、续跑注入的正文与摘要,
3
3
  * 外加一个运行时计数。
4
4
  *
5
- * 配置放在 dsh 的 settings 服务里(命名空间 repeat-guard):宿主侧在这边注册
6
- * schema,客户端设置页写同一个命名空间。判定与续跑时现取,改完立即生效,不用重启。
5
+ * dsh 0.1.7 起,配置就是插件自己的 entry Config——标记 `.volatile()` 的字段由宿主
6
+ * 的设置表单读写,写回的是 profile 的 cordis patch。宿主侧不再注册命名空间,
7
+ * 而是持有这些稳定引用,需要时 `.get()` 现取,所以改完立即生效,不用重启。
7
8
  *
8
- * 计数也寄在同一个命名空间里:宿主侧每次掐断写一次,客户端读同一个值、清零就是
9
- * 把它写成 0。插件加载时先把它归零,所以口径是"自本次启动或上次清零以来"。
9
+ * 计数也寄在同一份配置里:宿主侧每次掐断写一次,客户端读同一个值、清零就是把它
10
+ * 写成 0。插件加载时先把它归零,所以口径是"自本次启动或上次清零以来"。
10
11
  */
11
12
  import z from '@deepseek-ai/schemastery';
12
- /** settings 命名空间。客户端设置页必须写同一个值。 */
13
- export const SETTINGS_NS = 'repeat-guard';
13
+ /**
14
+ * profile 里本插件的条目 id。设置表单按它定位配置,宿主写回也要用它,
15
+ * 所以必须与 cordis.patch.yml 里的 `id` 逐字一致。
16
+ */
17
+ export const ENTRY_ID = 'dsh-repeat-guard';
14
18
  /** 默认的拦截短句表,标点照原样写。 */
15
19
  export const DEFAULT_FRAGMENTS = [
16
20
  '好。',
@@ -58,13 +62,17 @@ export const DEFAULT_RESUME_TEXT = [
58
62
  ].join('\n');
59
63
  /** 默认的折叠行摘要;客户端拿它渲染折叠的 context 行,不是用户气泡。 */
60
64
  export const DEFAULT_RESUME_SUMMARY = '复读已截断:请继续执行';
61
- const SCHEMA = z.object({
62
- fragments: z.array(z.string()).default([...DEFAULT_FRAGMENTS]),
63
- threshold: z.number().default(DEFAULT_THRESHOLD),
64
- inlineRepeat: z.boolean().default(DEFAULT_INLINE_REPEAT),
65
- resumeText: z.string().default(DEFAULT_RESUME_TEXT),
66
- resumeSummary: z.string().default(DEFAULT_RESUME_SUMMARY),
67
- count: z.number().default(0),
65
+ /**
66
+ * 插件 Config schema。只有标了 `.volatile()` 的字段会进设置表单并支持编辑,
67
+ * 其余字段仍是普通配置(只由 cordis 配置文件决定)。
68
+ */
69
+ export const Config = z.object({
70
+ fragments: z.array(z.string()).default([...DEFAULT_FRAGMENTS]).volatile(),
71
+ threshold: z.number().default(DEFAULT_THRESHOLD).volatile(),
72
+ inlineRepeat: z.boolean().default(DEFAULT_INLINE_REPEAT).volatile(),
73
+ resumeText: z.string().default(DEFAULT_RESUME_TEXT).volatile(),
74
+ resumeSummary: z.string().default(DEFAULT_RESUME_SUMMARY).volatile(),
75
+ count: z.number().default(0).volatile(),
68
76
  });
69
77
  function compile(value) {
70
78
  return {
@@ -76,41 +84,38 @@ function compile(value) {
76
84
  };
77
85
  }
78
86
  /**
79
- * 注册配置命名空间,返回宿主侧要用的运行时句柄。
87
+ * 把 volatile 引用折叠成一份快照,返回宿主侧要用的运行时句柄。
80
88
  * @param ctx - 宿主 cordis 上下文。
89
+ * @param config - loader 解析后的本插件配置。
81
90
  * @returns 取配置与记数两个入口。
82
91
  */
83
- export function createRuntime(ctx) {
84
- let current = compile({
85
- fragments: DEFAULT_FRAGMENTS,
86
- threshold: DEFAULT_THRESHOLD,
87
- inlineRepeat: DEFAULT_INLINE_REPEAT,
88
- resumeText: DEFAULT_RESUME_TEXT,
89
- resumeSummary: DEFAULT_RESUME_SUMMARY,
92
+ export function createRuntime(ctx, config) {
93
+ const read = () => compile({
94
+ fragments: config.fragments.get(),
95
+ threshold: config.threshold.get(),
96
+ inlineRepeat: config.inlineRepeat.get(),
97
+ resumeText: config.resumeText.get(),
98
+ resumeSummary: config.resumeSummary.get(),
90
99
  });
91
- let bump;
100
+ let write;
92
101
  ctx.inject(['settings'], (settingsCtx) => {
93
- const scope = settingsCtx.settings.register(SETTINGS_NS, SCHEMA);
94
- const sync = () => {
95
- current = compile(scope.get());
96
- };
97
- const write = (patch) => {
98
- scope.update(patch).catch((error) => {
102
+ const settings = settingsCtx.settings;
103
+ // 本插件自带设置页,关掉宿主按 schema 自动生成的页面。
104
+ settingsCtx.effect(() => settings.configure({ auto: false }, ctx.fiber));
105
+ write = (count) => {
106
+ // 现取 revision:表单刚写过的话,旧 revision 会被 SETTINGS_CONFLICT 拒掉。
107
+ const revision = settings.describe().find((entry) => entry.ns === ENTRY_ID)?.revision;
108
+ settings.update(ENTRY_ID, { count }, revision).catch((error) => {
99
109
  console.log(`[repeat-guard] 写设置失败:${String(error)}`);
100
110
  });
101
111
  };
102
- sync();
103
- scope.watch(sync);
104
112
  // 计数口径是"自本次启动或上次清零以来"。
105
- write({ count: 0 });
106
- bump = () => {
107
- write({ count: scope.get().count + 1 });
108
- };
113
+ write(0);
109
114
  });
110
115
  return {
111
- read: () => current,
116
+ read,
112
117
  countHit: () => {
113
- bump?.();
118
+ write?.(config.count.get() + 1);
114
119
  },
115
120
  };
116
121
  }
package/lib/index.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
+ import { Config, type RepeatGuardConfigSchema } from './config.js';
3
+ export { Config };
2
4
  /**
3
- * 函数式插件入口。ctx 为 cordis 上下文,注册的监听随插件卸载自动释放。
5
+ * 函数式插件入口。ctx 为 cordis 上下文,config 是 loader 解析后的配置,
6
+ * 注册的监听随插件卸载自动释放。
4
7
  * @param ctx - 宿主 cordis 上下文。
8
+ * @param config - 本插件的配置;volatile 字段是稳定引用,取值要 `.get()`。
5
9
  */
6
- export default function repeatGuard(ctx: Context): void;
10
+ export declare function apply(ctx: Context, config: RepeatGuardConfigSchema): void;
package/lib/index.js CHANGED
@@ -20,18 +20,24 @@
20
20
  // `@deepseek-ai/*`——它们声明在 peerDependencies 里,由宿主提供。
21
21
  //
22
22
  // 本文件只做装配,具体逻辑在各自的模块里。
23
- import { createRuntime } from './config.js';
23
+ import { Config, createRuntime } from './config.js';
24
24
  import { createStreamGuard } from './stream-guard.js';
25
25
  import { createTurnStoppingGuard } from './turn-stopping-guard.js';
26
+ // 插件模块的导出形态:loader 的 unwrapExports 优先取 `default`,而 Config 必须挂在
27
+ // 拿到的那一个对象上。所以这里不写 default 导出,而是与官方插件一样导出
28
+ // `Config` + `apply` 两个具名,让 loader 直接拿到带 schema 的插件对象。
29
+ export { Config };
26
30
  /**
27
- * 函数式插件入口。ctx 为 cordis 上下文,注册的监听随插件卸载自动释放。
31
+ * 函数式插件入口。ctx 为 cordis 上下文,config 是 loader 解析后的配置,
32
+ * 注册的监听随插件卸载自动释放。
28
33
  * @param ctx - 宿主 cordis 上下文。
34
+ * @param config - 本插件的配置;volatile 字段是稳定引用,取值要 `.get()`。
29
35
  */
30
- export default function repeatGuard(ctx) {
36
+ export function apply(ctx, config) {
31
37
  // 直接写 stdout:dsh 把插件的 stdout 收进 journal,便于确认插件确实被加载。
32
38
  console.log('[repeat-guard] 已加载,复读拦截生效');
33
39
  const state = { pending: new Set() };
34
- const runtime = createRuntime(ctx);
40
+ const runtime = createRuntime(ctx, config);
35
41
  ctx.on('llm/stream', createStreamGuard(state, runtime.read, runtime.countHit), { global: true });
36
42
  ctx.on('agent/turn-stopping', createTurnStoppingGuard(state, runtime.read));
37
43
  }
package/lib/resume.d.ts CHANGED
@@ -15,8 +15,17 @@
15
15
  * 推什么内容由配置决定(见 config.ts 的 resumeText / resumeSummary)。
16
16
  */
17
17
  import type { Agent } from '@deepseek-ai/dsh-agent';
18
+ import type { ContextFormed } from '@deepseek-ai/dsh-llm';
18
19
  import type { RepeatGuardConfig } from './config.js';
19
20
  import type { GuardState } from './types.js';
21
+ declare module '@deepseek-ai/dsh-llm' {
22
+ interface MessageSourceMap {
23
+ /** 本插件推给模型的续跑提示。 */
24
+ 'plugin:repeat-guard': {
25
+ kind: 'plugin:repeat-guard';
26
+ } & ContextFormed;
27
+ }
28
+ }
20
29
  /**
21
30
  * 本轮即将关闭时,若上一步刚被掐断过,就推一条输入让本轮继续。
22
31
  * @param state - 跨监听保留的拦截状态。
package/lib/resume.js CHANGED
@@ -33,8 +33,7 @@ export function reviveTurn(state, agent, config) {
33
33
  // `form: 'notice'` 要求同时给出 `summary`(dsh-llm 的 ContextFormed),
34
34
  // 客户端据此把它渲染成折叠的 context 行,而不是用户气泡。
35
35
  source: {
36
- kind: 'plugin',
37
- plugin: 'repeat-guard',
36
+ kind: 'plugin:repeat-guard',
38
37
  form: 'notice',
39
38
  summary: config.resumeSummary,
40
39
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-repeat-guard",
3
- "version": "0.1.5",
3
+ "version": "0.2.1",
4
4
  "description": "流式输出退化拦截:检测碎片复读,掐断本次生成并提醒模型直接执行工具调用",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -39,23 +39,23 @@
39
39
  "license": "MIT",
40
40
  "author": "yuqing",
41
41
  "peerDependencies": {
42
- "@deepseek-ai/cordis": "^4.0.2",
43
- "@deepseek-ai/dsh-agent": ">=0.1.0-rc.8 <0.1.1-0 || >=0.1.1-0 <0.1.2-0 || >=0.1.2-0 <0.1.3-0 || >=0.1.3-0 <0.1.4-0 || >=0.1.4-0 <0.1.5-0 || >=0.1.5-0 <0.1.6-0 || >=0.1.6-0 <0.1.7-0",
44
- "@deepseek-ai/dsh-client-ui-renderer": ">=0.1.0-rc.8 <0.1.1-0 || >=0.1.1-0 <0.1.2-0 || >=0.1.2-0 <0.1.3-0 || >=0.1.3-0 <0.1.4-0 || >=0.1.4-0 <0.1.5-0 || >=0.1.5-0 <0.1.6-0 || >=0.1.6-0 <0.1.7-0",
45
- "@deepseek-ai/dsh-client-ui-settings": ">=0.1.0-rc.8 <0.1.1-0 || >=0.1.1-0 <0.1.2-0 || >=0.1.2-0 <0.1.3-0 || >=0.1.3-0 <0.1.4-0 || >=0.1.4-0 <0.1.5-0 || >=0.1.5-0 <0.1.6-0 || >=0.1.6-0 <0.1.7-0",
46
- "@deepseek-ai/dsh-llm": ">=0.1.0-rc.8 <0.1.1-0 || >=0.1.1-0 <0.1.2-0 || >=0.1.2-0 <0.1.3-0 || >=0.1.3-0 <0.1.4-0 || >=0.1.4-0 <0.1.5-0 || >=0.1.5-0 <0.1.6-0 || >=0.1.6-0 <0.1.7-0",
47
- "@deepseek-ai/dsh-settings": ">=0.1.0-rc.8 <0.1.1-0 || >=0.1.1-0 <0.1.2-0 || >=0.1.2-0 <0.1.3-0 || >=0.1.3-0 <0.1.4-0 || >=0.1.4-0 <0.1.5-0 || >=0.1.5-0 <0.1.6-0 || >=0.1.6-0 <0.1.7-0",
48
- "@deepseek-ai/schemastery": "^3.18.2",
42
+ "@deepseek-ai/cordis": "^4.0.4",
43
+ "@deepseek-ai/dsh-agent": ">=0.1.7-0 <0.1.8-0",
44
+ "@deepseek-ai/dsh-client-ui-renderer": ">=0.1.7-0 <0.1.8-0",
45
+ "@deepseek-ai/dsh-client-ui-settings": ">=0.1.7-0 <0.1.8-0",
46
+ "@deepseek-ai/dsh-llm": ">=0.1.7-0 <0.1.8-0",
47
+ "@deepseek-ai/dsh-settings": ">=0.1.7-0 <0.1.8-0",
48
+ "@deepseek-ai/schemastery": "^3.18.4",
49
49
  "react": "^18.2.0"
50
50
  },
51
51
  "devDependencies": {
52
- "@deepseek-ai/cordis": "4.0.2",
53
- "@deepseek-ai/dsh-agent": "0.1.5-rc.2",
54
- "@deepseek-ai/dsh-client-ui-renderer": "0.1.5-rc.2",
55
- "@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.2",
56
- "@deepseek-ai/dsh-llm": "0.1.5-rc.2",
57
- "@deepseek-ai/dsh-settings": "0.1.5-rc.2",
58
- "@deepseek-ai/schemastery": "3.18.2",
52
+ "@deepseek-ai/cordis": "4.0.4",
53
+ "@deepseek-ai/dsh-agent": "0.1.7-rc.2",
54
+ "@deepseek-ai/dsh-client-ui-renderer": "0.1.7-rc.2",
55
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.7-rc.2",
56
+ "@deepseek-ai/dsh-llm": "0.1.7-rc.2",
57
+ "@deepseek-ai/dsh-settings": "0.1.7-rc.2",
58
+ "@deepseek-ai/schemastery": "3.18.4",
59
59
  "@types/node": "^22.0.0",
60
60
  "@types/react": "^19.3.0",
61
61
  "esbuild": "^0.28.2",
package/src/client.tsx CHANGED
@@ -11,13 +11,13 @@
11
11
 
12
12
  import type { Context } from '@deepseek-ai/cordis';
13
13
  // 空导入:只为加载这两处的 declaration merging(往 cordis 的 Context 上补
14
- // slots 与 settingsScope 两个客户端服务)。
14
+ // slots 与 configForms 两个客户端服务)。
15
15
  import type {} from '@deepseek-ai/dsh-client-ui-renderer/client';
16
16
  import type {} from '@deepseek-ai/dsh-client-ui-settings/client';
17
17
  import { useEffect, useState, type KeyboardEvent, type ReactElement } from 'react';
18
18
 
19
- /** settings 命名空间,必须与宿主侧 config.ts 的 SETTINGS_NS 逐字一致。 */
20
- const SETTINGS_NS = 'repeat-guard';
19
+ /** profile 条目 id,必须与宿主侧 config.ts 的 ENTRY_ID 逐字一致。 */
20
+ const ENTRY_ID = 'dsh-repeat-guard';
21
21
 
22
22
  /** 阈值步进器的取值边界。 */
23
23
  const THRESHOLD_MIN = 1;
@@ -408,7 +408,7 @@ function Form({ initial, count, onReset, onSave }: FormProps): ReactElement {
408
408
  }
409
409
 
410
410
  /** 注册设置页需要的服务;这两个是 cordis 服务名,不是包名。 */
411
- export const inject = ['slots', 'settingsScope'];
411
+ export const inject = ['slots', 'configForms'];
412
412
 
413
413
  /**
414
414
  * 客户端插件入口。
@@ -416,14 +416,15 @@ export const inject = ['slots', 'settingsScope'];
416
416
  */
417
417
  export function apply(ctx: Context): void {
418
418
  injectStyle();
419
- const scope = ctx.settingsScope.bind<RepeatGuardSettings>({ namespace: SETTINGS_NS });
419
+ // 0.1.7 起表单按 profile 条目 id 定位;快照与写入接口同形。
420
+ const form = ctx.configForms.get<RepeatGuardSettings>(ENTRY_ID);
420
421
 
421
422
  function Panel(): ReactElement {
422
- const [snapshot, setSnapshot] = useState(() => scope.getSnapshot());
423
+ const [snapshot, setSnapshot] = useState(() => form.getSnapshot());
423
424
  useEffect(
424
425
  () =>
425
- scope.subscribe(() => {
426
- setSnapshot(scope.getSnapshot());
426
+ form.subscribe(() => {
427
+ setSnapshot(form.getSnapshot());
427
428
  }),
428
429
  [],
429
430
  );
@@ -436,14 +437,14 @@ export function apply(ctx: Context): void {
436
437
  initial={value}
437
438
  count={value.count}
438
439
  onReset={async () => {
439
- await scope.set('count', 0);
440
+ await form.set('count', 0);
440
441
  }}
441
442
  onSave={async (next) => {
442
- await scope.set('fragments', next.fragments);
443
- await scope.set('threshold', next.threshold);
444
- await scope.set('inlineRepeat', next.inlineRepeat);
445
- await scope.set('resumeText', next.resumeText);
446
- await scope.set('resumeSummary', next.resumeSummary);
443
+ await form.set('fragments', next.fragments);
444
+ await form.set('threshold', next.threshold);
445
+ await form.set('inlineRepeat', next.inlineRepeat);
446
+ await form.set('resumeText', next.resumeText);
447
+ await form.set('resumeSummary', next.resumeSummary);
447
448
  }}
448
449
  />
449
450
  );
package/src/config.ts CHANGED
@@ -2,11 +2,12 @@
2
2
  * 插件配置:拦截短句表、连续命中阈值、行内重复检测、续跑注入的正文与摘要,
3
3
  * 外加一个运行时计数。
4
4
  *
5
- * 配置放在 dsh 的 settings 服务里(命名空间 repeat-guard):宿主侧在这边注册
6
- * schema,客户端设置页写同一个命名空间。判定与续跑时现取,改完立即生效,不用重启。
5
+ * dsh 0.1.7 起,配置就是插件自己的 entry Config——标记 `.volatile()` 的字段由宿主
6
+ * 的设置表单读写,写回的是 profile 的 cordis patch。宿主侧不再注册命名空间,
7
+ * 而是持有这些稳定引用,需要时 `.get()` 现取,所以改完立即生效,不用重启。
7
8
  *
8
- * 计数也寄在同一个命名空间里:宿主侧每次掐断写一次,客户端读同一个值、清零就是
9
- * 把它写成 0。插件加载时先把它归零,所以口径是"自本次启动或上次清零以来"。
9
+ * 计数也寄在同一份配置里:宿主侧每次掐断写一次,客户端读同一个值、清零就是把它
10
+ * 写成 0。插件加载时先把它归零,所以口径是"自本次启动或上次清零以来"。
10
11
  */
11
12
 
12
13
  import type { Context } from '@deepseek-ai/cordis';
@@ -14,8 +15,11 @@ import type { Context } from '@deepseek-ai/cordis';
14
15
  import type {} from '@deepseek-ai/dsh-settings';
15
16
  import z from '@deepseek-ai/schemastery';
16
17
 
17
- /** settings 命名空间。客户端设置页必须写同一个值。 */
18
- export const SETTINGS_NS = 'repeat-guard';
18
+ /**
19
+ * profile 里本插件的条目 id。设置表单按它定位配置,宿主写回也要用它,
20
+ * 所以必须与 cordis.patch.yml 里的 `id` 逐字一致。
21
+ */
22
+ export const ENTRY_ID = 'dsh-repeat-guard';
19
23
 
20
24
  /** 默认的拦截短句表,标点照原样写。 */
21
25
  export const DEFAULT_FRAGMENTS: readonly string[] = [
@@ -69,15 +73,22 @@ export const DEFAULT_RESUME_TEXT = [
69
73
  /** 默认的折叠行摘要;客户端拿它渲染折叠的 context 行,不是用户气泡。 */
70
74
  export const DEFAULT_RESUME_SUMMARY = '复读已截断:请继续执行';
71
75
 
72
- const SCHEMA = z.object({
73
- fragments: z.array(z.string()).default([...DEFAULT_FRAGMENTS]),
74
- threshold: z.number().default(DEFAULT_THRESHOLD),
75
- inlineRepeat: z.boolean().default(DEFAULT_INLINE_REPEAT),
76
- resumeText: z.string().default(DEFAULT_RESUME_TEXT),
77
- resumeSummary: z.string().default(DEFAULT_RESUME_SUMMARY),
78
- count: z.number().default(0),
76
+ /**
77
+ * 插件 Config schema。只有标了 `.volatile()` 的字段会进设置表单并支持编辑,
78
+ * 其余字段仍是普通配置(只由 cordis 配置文件决定)。
79
+ */
80
+ export const Config = z.object({
81
+ fragments: z.array(z.string()).default([...DEFAULT_FRAGMENTS]).volatile(),
82
+ threshold: z.number().default(DEFAULT_THRESHOLD).volatile(),
83
+ inlineRepeat: z.boolean().default(DEFAULT_INLINE_REPEAT).volatile(),
84
+ resumeText: z.string().default(DEFAULT_RESUME_TEXT).volatile(),
85
+ resumeSummary: z.string().default(DEFAULT_RESUME_SUMMARY).volatile(),
86
+ count: z.number().default(0).volatile(),
79
87
  });
80
88
 
89
+ /** loader 解析后交给插件的配置:volatile 字段是稳定引用,取值要 `.get()`。 */
90
+ export type RepeatGuardConfigSchema = Schemastery.TypeT<typeof Config>;
91
+
81
92
  /** 判定与续跑用得上的一份配置。 */
82
93
  export interface RepeatGuardConfig {
83
94
  /** 短句表,已全部小写化。 */
@@ -99,7 +110,7 @@ export type ConfigSource = () => RepeatGuardConfig;
99
110
  export interface GuardRuntime {
100
111
  /** 取当前配置。 */
101
112
  readonly read: ConfigSource;
102
- /** 记一次拦截,把计数写回 settings。 */
113
+ /** 记一次拦截,把计数写回配置。 */
103
114
  readonly countHit: () => void;
104
115
  }
105
116
 
@@ -120,43 +131,41 @@ function compile(value: {
120
131
  }
121
132
 
122
133
  /**
123
- * 注册配置命名空间,返回宿主侧要用的运行时句柄。
134
+ * 把 volatile 引用折叠成一份快照,返回宿主侧要用的运行时句柄。
124
135
  * @param ctx - 宿主 cordis 上下文。
136
+ * @param config - loader 解析后的本插件配置。
125
137
  * @returns 取配置与记数两个入口。
126
138
  */
127
- export function createRuntime(ctx: Context): GuardRuntime {
128
- let current = compile({
129
- fragments: DEFAULT_FRAGMENTS,
130
- threshold: DEFAULT_THRESHOLD,
131
- inlineRepeat: DEFAULT_INLINE_REPEAT,
132
- resumeText: DEFAULT_RESUME_TEXT,
133
- resumeSummary: DEFAULT_RESUME_SUMMARY,
134
- });
135
- let bump: (() => void) | undefined;
139
+ export function createRuntime(ctx: Context, config: RepeatGuardConfigSchema): GuardRuntime {
140
+ const read = (): RepeatGuardConfig =>
141
+ compile({
142
+ fragments: config.fragments.get(),
143
+ threshold: config.threshold.get(),
144
+ inlineRepeat: config.inlineRepeat.get(),
145
+ resumeText: config.resumeText.get(),
146
+ resumeSummary: config.resumeSummary.get(),
147
+ });
136
148
 
149
+ let write: ((count: number) => void) | undefined;
137
150
  ctx.inject(['settings'], (settingsCtx) => {
138
- const scope = settingsCtx.settings.register(SETTINGS_NS, SCHEMA);
139
- const sync = (): void => {
140
- current = compile(scope.get());
141
- };
142
- const write = (patch: object): void => {
143
- scope.update(patch).catch((error: unknown) => {
151
+ const settings = settingsCtx.settings;
152
+ // 本插件自带设置页,关掉宿主按 schema 自动生成的页面。
153
+ settingsCtx.effect(() => settings.configure({ auto: false }, ctx.fiber));
154
+ write = (count: number): void => {
155
+ // 现取 revision:表单刚写过的话,旧 revision 会被 SETTINGS_CONFLICT 拒掉。
156
+ const revision = settings.describe().find((entry) => entry.ns === ENTRY_ID)?.revision;
157
+ settings.update(ENTRY_ID, { count }, revision).catch((error: unknown) => {
144
158
  console.log(`[repeat-guard] 写设置失败:${String(error)}`);
145
159
  });
146
160
  };
147
- sync();
148
- scope.watch(sync);
149
161
  // 计数口径是"自本次启动或上次清零以来"。
150
- write({ count: 0 });
151
- bump = (): void => {
152
- write({ count: scope.get().count + 1 });
153
- };
162
+ write(0);
154
163
  });
155
164
 
156
165
  return {
157
- read: () => current,
166
+ read,
158
167
  countHit: () => {
159
- bump?.();
168
+ write?.(config.count.get() + 1);
160
169
  },
161
170
  };
162
171
  }
package/src/index.ts CHANGED
@@ -22,20 +22,27 @@
22
22
  // 本文件只做装配,具体逻辑在各自的模块里。
23
23
 
24
24
  import type { Context } from '@deepseek-ai/cordis';
25
- import { createRuntime } from './config.js';
25
+ import { Config, createRuntime, type RepeatGuardConfigSchema } from './config.js';
26
26
  import { createStreamGuard } from './stream-guard.js';
27
27
  import { createTurnStoppingGuard } from './turn-stopping-guard.js';
28
28
  import type { GuardState } from './types.js';
29
29
 
30
+ // 插件模块的导出形态:loader 的 unwrapExports 优先取 `default`,而 Config 必须挂在
31
+ // 拿到的那一个对象上。所以这里不写 default 导出,而是与官方插件一样导出
32
+ // `Config` + `apply` 两个具名,让 loader 直接拿到带 schema 的插件对象。
33
+ export { Config };
34
+
30
35
  /**
31
- * 函数式插件入口。ctx 为 cordis 上下文,注册的监听随插件卸载自动释放。
36
+ * 函数式插件入口。ctx 为 cordis 上下文,config 是 loader 解析后的配置,
37
+ * 注册的监听随插件卸载自动释放。
32
38
  * @param ctx - 宿主 cordis 上下文。
39
+ * @param config - 本插件的配置;volatile 字段是稳定引用,取值要 `.get()`。
33
40
  */
34
- export default function repeatGuard(ctx: Context): void {
41
+ export function apply(ctx: Context, config: RepeatGuardConfigSchema): void {
35
42
  // 直接写 stdout:dsh 把插件的 stdout 收进 journal,便于确认插件确实被加载。
36
43
  console.log('[repeat-guard] 已加载,复读拦截生效');
37
44
  const state: GuardState = { pending: new Set() };
38
- const runtime = createRuntime(ctx);
45
+ const runtime = createRuntime(ctx, config);
39
46
  ctx.on('llm/stream', createStreamGuard(state, runtime.read, runtime.countHit), { global: true });
40
47
  ctx.on('agent/turn-stopping', createTurnStoppingGuard(state, runtime.read));
41
48
  }
package/src/resume.ts CHANGED
@@ -17,9 +17,23 @@
17
17
 
18
18
  import type { Agent } from '@deepseek-ai/dsh-agent';
19
19
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
20
+ import type { ContextFormed } from '@deepseek-ai/dsh-llm';
20
21
  import type { RepeatGuardConfig } from './config.js';
21
22
  import type { GuardState } from './types.js';
22
23
 
24
+ // dsh 0.1.7 起 `MessageSourceMap` 没有通用的 `plugin` kind:每个生产者在自己的模块里
25
+ // 声明自己的 kind(merge-extensible),`form` 另由 ContextFormed 提供。
26
+ // 用 `plugin:<名>` 而不是裸名:会话格式 v3→v4 的迁移器对历史消息生成的正是这个前缀
27
+ // (dsh-session-format-v3-to-v4 的 producerKind),裸名会让同一生产者出现两种 kind。
28
+ declare module '@deepseek-ai/dsh-llm' {
29
+ interface MessageSourceMap {
30
+ /** 本插件推给模型的续跑提示。 */
31
+ 'plugin:repeat-guard': {
32
+ kind: 'plugin:repeat-guard';
33
+ } & ContextFormed;
34
+ }
35
+ }
36
+
23
37
  /**
24
38
  * 本轮即将关闭时,若上一步刚被掐断过,就推一条输入让本轮继续。
25
39
  * @param state - 跨监听保留的拦截状态。
@@ -39,8 +53,7 @@ export function reviveTurn(state: GuardState, agent: Agent, config: RepeatGuardC
39
53
  // `form: 'notice'` 要求同时给出 `summary`(dsh-llm 的 ContextFormed),
40
54
  // 客户端据此把它渲染成折叠的 context 行,而不是用户气泡。
41
55
  source: {
42
- kind: 'plugin',
43
- plugin: 'repeat-guard',
56
+ kind: 'plugin:repeat-guard',
44
57
  form: 'notice',
45
58
  summary: config.resumeSummary,
46
59
  },