dsh-fingerprint 0.5.1 → 0.6.0

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
@@ -16,12 +16,12 @@ dsh-fingerprint 把 DSH 里已经配置好的 `provider/model` 路由当作审
16
16
 
17
17
  插件不保存 provider API key;认证和请求路由由 DSH 的 `llm` 服务处理。无法关闭 reasoning 的模型会被硬拦截,因为论文协议需要一次直接的单次 completion。
18
18
 
19
- 这是一个手动使用的插件:正常操作都在 Web 面板里完成,默认不向 agent 注册工具,因此不占用模型上下文。需要让 agent 驱动采集做调试或自动化时,在面板底部的“设置”里把 Agent 工具开关打开即可,即时生效,不需要改配置文件。
19
+ 这是一个手动使用的插件:正常操作都在 Web 面板里完成,默认不向 agent 注册工具,因此不占用模型上下文。需要让 agent 驱动采集做调试或自动化时,在面板底部的“设置”里把 Agent 工具开关打开即可,即时生效。开关的值是插件 row 的 `config.tools`,存在 profile 的 `cordis.patch.yml`。
20
20
 
21
21
  ## 要求
22
22
 
23
- - 一个启用 Web GUI 的 DSH profile(需要 `llm`、`webServer` 服务;开启 agent 工具时还需要 `tools`)。
24
- - DSH 0.2 系列:安装、面板、API 与端到端采集已在 `0.2.1-rc.2`、`0.2.1-rc.4` 和 `0.2.2-rc.1` 上验证;面板的 Agent 工具开关和 settings API 在 `0.2.1-rc.5` 与 `0.2.2-rc.1` 上实测。`engines.dsh` 限定 `>=0.2.1-rc.2 <0.3.0-0`。DSH 0.2.2 起按 `engines.dsh` 判定插件兼容性,范围不覆盖当前运行时会拒绝安装,并在启动时跳过该 bundle(面板、API 和工具都不挂载)。
23
+ - 一个启用 Web GUI 的 DSH profile(需要 `llm`、`webServer` 服务;开启 agent 工具时还需要 `tools`)。面板开关通过 profile 的 `configEditor` 写入 row config;profile 没有这个服务时开关只读,直接改 `cordis.patch.yml` 后重启生效。
24
+ - DSH 0.2 系列:安装、面板、API 与端到端采集已在 `0.2.1-rc.2`、`0.2.1-rc.4` 和 `0.2.2-rc.1` 上验证;settings API 在 `0.2.1-rc.5` 与 `0.2.2-rc.1` 上实测。开关改为写 row config 之后,这条写入路径只在单元测试和 `0.2.1-rc.5` 的组合 schema 上验证过,尚未在 rc.2/rc.4/`0.2.2-rc.1` 上实跑。`engines.dsh` 限定 `>=0.2.1-rc.2 <0.3.0-0`。DSH 0.2.2 起按 `engines.dsh` 判定插件兼容性,范围不覆盖当前运行时会拒绝安装,并在启动时跳过该 bundle(面板、API 和工具都不挂载)。
25
25
  - 至少一个配置好的、能关闭 reasoning(`off` / `none`)或本身不推理的 provider/model。
26
26
  - Node.js(由 DSH 自带,无需单独安装)。
27
27
 
@@ -56,7 +56,7 @@ dsh plugin --profile web add github:Jerry2262/dsh-fingerprint
56
56
  5. 在“对比记录”里选两条记录,点“对比记录”。比较方式只有一种,结果会标注两边是否同一目标。
57
57
  6. 结果包含差异分数、判定线、结论、关系、cell 级差异和警告。低于判定线只说明差异小,不等于 100% 是同一个模型。
58
58
  7. 行内垃圾桶可以删除记录;删除前会二次确认。运行中的记录不能删除。
59
- 8. 面板底部的“设置”里有 Agent 工具开关,默认关闭。开启后把 6 个 `llm_fingerprint_*` 工具暴露给 agent(每个请求多约 4.3 KB 工具 schema),关闭后立即从后续请求里移除;选择保存在数据目录的 `settings.json`,删掉这个文件即回到默认关闭。
59
+ 8. 面板底部的“设置”里有 Agent 工具开关,默认关闭。开启后把 6 个 `llm_fingerprint_*` 工具暴露给 agent(每个请求多约 4.3 KB 工具 schema),关闭后立即从后续请求里移除。开关的值是插件 row 的 `config.tools`,存在 profile 的 `cordis.patch.yml`;`tools` 是 volatile 字段,切换不会重载插件。
60
60
 
61
61
  ## 工作原理
62
62
 
@@ -70,7 +70,7 @@ dsh plugin --profile web add github:Jerry2262/dsh-fingerprint
70
70
 
71
71
  - 插件不保存 API key;key 留在 DSH 的 provider/credential 层。
72
72
  - 原始响应和指纹默认写到 `$DSH_HOME/llm-fingerprint/`,也可以由插件 row 的 `dataDir` 覆盖。
73
- - 面板设置(目前只有 Agent 工具开关)存在同一数据目录的 `settings.json`。
73
+ - 面板设置(目前只有 Agent 工具开关)不在数据目录,它存在 profile 的 `cordis.patch.yml`(插件 row 的 `config.tools`)。
74
74
  - 删除记录会删除该目录下的 `meta.json`、`responses.jsonl` 和 `fingerprint.json`。
75
75
  - 无法关闭 reasoning 的模型不会被静默采集。
76
76
 
package/client.js CHANGED
@@ -130,6 +130,7 @@ window.__ModuleLoader__.load({
130
130
  toolsTitle: 'Agent tools',
131
131
  toolsHint: 'Expose the six capture and compare tools to agents. Adds about 4.3 KB of tool schema to every request; useful for debugging or automation.',
132
132
  toolsUnavailable: 'This profile does not provide the tools service, so the switch cannot be turned on.',
133
+ toolsReadOnly: 'This profile does not expose the config editor, so change this switch in cordis.patch.yml.',
133
134
  toolsUpdated: 'Agent tool visibility updated.',
134
135
  };
135
136
 
@@ -255,6 +256,7 @@ window.__ModuleLoader__.load({
255
256
  toolsTitle: 'Agent 工具',
256
257
  toolsHint: '把 6 个采集、对比工具暴露给 agent。每个请求会多约 4.3 KB 工具 schema,适合调试或自动化。',
257
258
  toolsUnavailable: '当前 profile 没有 tools 服务,无法开启这个开关。',
259
+ toolsReadOnly: '当前 profile 没有 configEditor 服务,这个开关只能在 cordis.patch.yml 里改。',
258
260
  toolsUpdated: 'agent 工具开关已更新。',
259
261
  };
260
262
 
@@ -340,6 +342,7 @@ window.__ModuleLoader__.load({
340
342
  if (/unknown fingerprint record/i.test(text)) return t('errorRecordNotFound');
341
343
  if (/no completed fingerprint|two record ids|record id is required/i.test(text)) return t('errorRecordNeedsFingerprint');
342
344
  if (/tools service is unavailable/i.test(text)) return t('toolsUnavailable');
345
+ if (/configEditor service is unavailable/i.test(text)) return t('toolsReadOnly');
343
346
  return text;
344
347
  }
345
348
 
@@ -1083,6 +1086,7 @@ window.__ModuleLoader__.load({
1083
1086
  }
1084
1087
 
1085
1088
  const estimate = 40 * Math.max(10, Math.min(100, Number(reps) || 30));
1089
+ const toolsLocked = !toolsSetting || savingTools || !toolsSetting.available || toolsSetting.editable === false;
1086
1090
 
1087
1091
  return h('div', { style: S.page },
1088
1092
  h('div', { style: S.inner, ref: pageRef },
@@ -1361,18 +1365,20 @@ window.__ModuleLoader__.load({
1361
1365
  h('p', { style: { ...S.hint, margin: '4px 0 0' } }, t('toolsHint')),
1362
1366
  toolsSetting && !toolsSetting.available
1363
1367
  ? h('p', { style: { ...S.hint, margin: '4px 0 0', color: 'var(--dsw-alias-state-warn-primary)' } }, t('toolsUnavailable'))
1364
- : null,
1368
+ : toolsSetting && toolsSetting.editable === false
1369
+ ? h('p', { style: { ...S.hint, margin: '4px 0 0', color: 'var(--dsw-alias-state-warn-primary)' } }, t('toolsReadOnly'))
1370
+ : null,
1365
1371
  ),
1366
1372
  h('button', {
1367
1373
  type: 'button',
1368
1374
  role: 'switch',
1369
1375
  'aria-checked': Boolean(toolsSetting && toolsSetting.enabled),
1370
1376
  'aria-label': t('toolsTitle'),
1371
- disabled: !toolsSetting || savingTools || !toolsSetting.available,
1377
+ disabled: toolsLocked,
1372
1378
  style: {
1373
1379
  ...S.switch,
1374
1380
  ...(toolsSetting && toolsSetting.enabled ? S.switchOn : {}),
1375
- ...(!toolsSetting || savingTools || !toolsSetting.available ? S.disabled : {}),
1381
+ ...(toolsLocked ? S.disabled : {}),
1376
1382
  },
1377
1383
  onClick: toggleTools,
1378
1384
  }, h('span', {
package/docs/DESIGN.md CHANGED
@@ -8,7 +8,7 @@
8
8
  - **指纹记录**:对某个目标在某一时刻做一次完整采集的结果。记录本身就是指纹,包含 40 个 cell 的经验答案分布和派生统计。
9
9
  - **对比**:任意两条记录之间的同一个操作。跨模型、跨服务商、同一目标的前后两次,都是同一个比较操作,只有 `relation` 标签不同。
10
10
  - **单一实现**:Host service 是唯一操作层;Web 面板通过 HTTP API 调用,可选的 agent 工具调用同一组方法。
11
- - **默认手动**:插件默认不向 agent 注册工具,所有操作在面板里完成,因此不占用模型上下文;面板“设置”里的开关打开时才注册 6 个 agent 工具,选择持久化在数据目录的 `settings.json`。
11
+ - **默认手动**:插件默认不向 agent 注册工具,所有操作在面板里完成,因此不占用模型上下文;面板“设置”里的开关打开时才注册 6 个 agent 工具。开关对应 volatile 的 `tools` row config,值存在 profile 的 `cordis.patch.yml`。
12
12
 
13
13
  ## 2. 协议常量
14
14
 
@@ -37,7 +37,6 @@ llm-fingerprint/
37
37
  responses.jsonl # 每条成功请求一行,append-only
38
38
  fingerprint.json # 派生分布、valid rate、分半一致性、cell 明细
39
39
  probes/<probe-id>.json
40
- settings.json # 面板设置(目前只有 agent 工具开关)
41
40
  ```
42
41
 
43
42
  - 记录 id 形如 `cp-20261020T034450-59c80896`,服务端用严格格式校验,防止路径穿越。
@@ -62,7 +61,7 @@ llm-fingerprint/
62
61
  - `GET /settings`
63
62
  - `POST /settings/tools`
64
63
 
65
- 所有操作都在 `createService(ctx, root)` 返回的同一组方法里实现。面板“设置”里的 Agent 工具开关调用 `POST /settings/tools`:Host 按需通过 `ctx.inject(['tools'], ...)` 注册 6 个 agent 工具(`llm_fingerprint_list_models` / `probe` / `capture` / `get_record` / `compare` / `delete_record`),关闭时 dispose 该子 fiber 立即注销;默认关闭,因此不要求 profile 提供 `tools` 服务。开关选择持久化在数据目录的 `settings.json`,插件重载后恢复。
64
+ 所有操作都在 `createService(ctx, root)` 返回的同一组方法里实现。面板“设置”里的 Agent 工具开关调用 `POST /settings/tools`,Host 通过 `ctx.configEditor.edit()` 把 `config.tools` 写回 profile 的 `cordis.patch.yml`。`tools` 在 `Config` 里声明为 `.volatile()`:Loader 对这类变更不重载插件,只更新运行中的配置引用并发 `loader/volatile-update`,插件据此注册 6 个 agent 工具(`llm_fingerprint_list_models` / `probe` / `capture` / `get_record` / `compare` / `delete_record`)或 dispose 注册它们的子 fiber。默认关闭,因此不要求 profile 提供 `tools` 服务;profile 没有 `configEditor` 时开关只读,直接改 patch 后重启生效。
66
65
 
67
66
  目标发现:
68
67
 
@@ -148,5 +147,6 @@ llm-fingerprint/
148
147
  - 提示词版本写在 `prompts.json` 的 `version` 和每条记录的 `promptVersion`;改提示词需要重新采集,旧记录不能直接和新记录混比。
149
148
  - 判定线常量在 `core.js` 的 `CUTOFF`;修改时需要同步 README/DESIGN 并说明校准来源。
150
149
  - 数据目录 append-only;分析产物可以删除后从 `responses.jsonl` 重新计算。
151
- - 面板设置只有 agent 工具开关,写在数据目录的 `settings.json`;删除该文件即恢复默认关闭,不影响记录数据。
150
+ - 面板设置只有 agent 工具开关,写在 profile 的 `cordis.patch.yml` 里插件 row 的 `config.tools`;删掉该字段即恢复默认关闭,不影响记录数据。
152
151
  - 代码无构建步骤;`npm run check` 做三个 JS 文件的语法检查,`npm test` 跑核心与插件注册回归测试。
152
+ - 唯一的运行时依赖是 `@deepseek-ai/schemastery`,用于声明 `Config`;其余只用 Node 内置模块。
package/index.js CHANGED
@@ -9,7 +9,8 @@
9
9
  * The service methods back the HTTP API that the Web panel calls; there is one
10
10
  * implementation of every operation. The panel is the normal surface; its
11
11
  * settings section can optionally expose the six agent tools, which stay out of
12
- * the model context while the toggle is off (the default).
12
+ * the model context unless the volatile `tools` row config is on (off by
13
+ * default, and always reachable from the panel).
13
14
  */
14
15
  import { randomUUID } from 'node:crypto';
15
16
  import {
@@ -25,6 +26,8 @@ import {
25
26
  import os from 'node:os';
26
27
  import path from 'node:path';
27
28
 
29
+ import Schema from '@deepseek-ai/schemastery';
30
+
28
31
  import {
29
32
  CELLS,
30
33
  CUTOFF,
@@ -41,6 +44,20 @@ import {
41
44
 
42
45
  export const inject = ['llm', 'webServer'];
43
46
 
47
+ /**
48
+ * The plugin's row config. `tools` is volatile: the panel switch and the
49
+ * Settings form edit the value stored in the profile patch, and a volatile-only
50
+ * change updates this running config reference and emits
51
+ * `loader/volatile-update` instead of reloading the plugin.
52
+ */
53
+ export const Config = Schema.object({
54
+ dataDir: Schema.string().default('').description('记录和探测日志的数据目录,默认 $DSH_HOME/llm-fingerprint。'),
55
+ tools: Schema.boolean()
56
+ .default(false)
57
+ .description('把 6 个采集、对比工具(llm_fingerprint_*)暴露给 agent。每个请求会多约 4.3 KB 工具 schema,适合调试或自动化。')
58
+ .volatile(),
59
+ });
60
+
44
61
  const CONCURRENCY = 8;
45
62
  const REQUEST_TIMEOUT_MS = 90_000;
46
63
  const MAX_TOKENS = 16;
@@ -947,39 +964,20 @@ function toolDefinitions(service) {
947
964
  ];
948
965
  }
949
966
 
950
- const AGENT_TOOLS_SETTINGS_FILE = 'settings.json';
951
-
952
- /** Read the persisted panel preferences; unreadable or malformed files fall back to the defaults. */
953
- function readSettings(file) {
954
- try {
955
- const value = JSON.parse(readFileSync(file, 'utf8'));
956
- return { tools: value?.tools === true };
957
- } catch {
958
- return { tools: false };
959
- }
960
- }
961
-
962
- /** Persist the panel preferences. Write failures surface to the caller instead of being swallowed. */
963
- function writeSettings(file, settings) {
964
- writeFileSync(file, `${JSON.stringify({ tools: settings.tools === true }, null, 2)}\n`);
965
- }
966
-
967
967
  /**
968
968
  * Own the optional agent tool surface: the six tools are registered exactly
969
- * while the Web panel's toggle is on.
969
+ * while the `tools` row config is on.
970
970
  *
971
- * The setting persists in the plugin's data directory (`settings.json`) and
972
- * defaults to off, so a fresh install adds no tool definitions to the model
973
- * context. Registration waits for the `tools` service through `ctx.inject`;
974
- * turning the toggle off disposes that child fiber, which unregisters the tools
975
- * immediately — no profile reload or DSH restart involved.
971
+ * The value lives in the profile patch and defaults to off, so a fresh install
972
+ * adds no tool definitions to the model context. Registration waits for the
973
+ * `tools` service through `ctx.inject`; turning the switch off disposes that
974
+ * child fiber, which unregisters the tools immediately.
976
975
  */
977
- function createAgentTools(ctx, service, root) {
978
- const file = path.join(root, AGENT_TOOLS_SETTINGS_FILE);
979
- let settings = readSettings(file);
976
+ function createAgentTools(ctx, service, config) {
980
977
  let fiber;
981
978
 
982
- const status = () => ({ enabled: settings.tools, available: Boolean(ctx.get('tools')) });
979
+ const enabled = () => config.tools?.get?.() === true;
980
+ const status = () => ({ enabled: enabled(), available: Boolean(ctx.get('tools')) });
983
981
  const start = () => {
984
982
  if (fiber !== undefined) return;
985
983
  fiber = ctx.inject(['tools'], function registerFingerprintTools(toolCtx) {
@@ -993,20 +991,25 @@ function createAgentTools(ctx, service, root) {
993
991
  fiber = undefined;
994
992
  if (current !== undefined) await current.dispose();
995
993
  };
996
- const setEnabled = async (enabled) => {
997
- if (enabled === true && !ctx.get('tools')) {
998
- throw new Error('the tools service is unavailable in this profile, so agent tools cannot be enabled');
999
- }
1000
- const next = { tools: enabled === true };
1001
- writeSettings(file, next);
1002
- settings = next;
1003
- if (settings.tools) start();
994
+ const sync = async () => {
995
+ if (enabled()) start();
1004
996
  else await stop();
1005
- return status();
1006
997
  };
1007
998
 
1008
- if (settings.tools) start();
1009
- return { status, setEnabled };
999
+ if (enabled()) start();
1000
+ return { status, sync };
1001
+ }
1002
+
1003
+ /**
1004
+ * The profile row that owns this plugin instance, when the active profile
1005
+ * exposes the config editor. The panel switch writes the row config through it;
1006
+ * without it the switch stays read-only and `cordis.patch.yml` is the way in.
1007
+ */
1008
+ function ownConfigEntry(ctx) {
1009
+ const editor = ctx.get('configEditor');
1010
+ const uid = ctx.fiber?.uid;
1011
+ if (!editor || uid === undefined) return undefined;
1012
+ return editor.entries().find((entry) => entry.fiber?.uid === uid);
1010
1013
  }
1011
1014
 
1012
1015
  function sendJson(res, status, value) {
@@ -1085,13 +1088,22 @@ async function handleApi(ctx, req, res, service, agentTools) {
1085
1088
  return;
1086
1089
  }
1087
1090
  if (req.method === 'GET' && pathname === '/settings') {
1088
- sendJson(res, 200, { tools: agentTools.status() });
1091
+ sendJson(res, 200, { tools: { ...agentTools.status(), editable: ownConfigEntry(ctx) !== undefined } });
1089
1092
  return;
1090
1093
  }
1091
1094
  if (req.method === 'POST' && pathname === '/settings/tools') {
1092
1095
  const body = await readRequestBody(req);
1093
1096
  if (typeof body.enabled !== 'boolean') throw new Error('enabled must be a boolean');
1094
- sendJson(res, 200, { tools: await agentTools.setEnabled(body.enabled) });
1097
+ if (body.enabled === true && !ctx.get('tools')) {
1098
+ throw new Error('the tools service is unavailable in this profile, so agent tools cannot be enabled');
1099
+ }
1100
+ const editor = ctx.get('configEditor');
1101
+ const entry = ownConfigEntry(ctx);
1102
+ if (!editor || entry === undefined) {
1103
+ throw new Error('the configEditor service is unavailable in this profile, so the switch can only be changed in cordis.patch.yml');
1104
+ }
1105
+ await editor.edit(entry, (current) => ({ ...current, tools: body.enabled }));
1106
+ sendJson(res, 200, { tools: { ...agentTools.status(), editable: true } });
1095
1107
  return;
1096
1108
  }
1097
1109
  const match = pathname.match(/^\/checkpoints\/([^/]+)$/);
@@ -1118,7 +1130,10 @@ export function apply(ctx, config = {}) {
1118
1130
  const lifetime = new AbortController();
1119
1131
  ctx.effect(() => () => { lifetime.abort(); }, 'llm-fingerprint: abort running captures');
1120
1132
  const service = createService(ctx, root, lifetime.signal);
1121
- const agentTools = createAgentTools(ctx, service, root);
1133
+ const agentTools = createAgentTools(ctx, service, config);
1134
+ ctx.on('loader/volatile-update', () => {
1135
+ agentTools.sync().catch((error) => ctx.logger?.warn?.(error));
1136
+ });
1122
1137
  ctx.effect(
1123
1138
  () => ctx.webServer.register({
1124
1139
  kind: 'prefix',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-fingerprint",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "Paper-protocol fingerprint records and comparison panel for configured DSH provider/model routes.",
6
6
  "license": "MIT",
@@ -60,5 +60,8 @@
60
60
  },
61
61
  "engines": {
62
62
  "dsh": ">=0.2.1-rc.2 <0.3.0-0"
63
+ },
64
+ "dependencies": {
65
+ "@deepseek-ai/schemastery": "~3.18.5"
63
66
  }
64
67
  }