dsh-cnb-assist 1.0.3 → 1.0.5

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
@@ -1,101 +1,84 @@
1
- # CNB 反代辅助
1
+ # CNB 反代辅助(DSH 插件)
2
2
 
3
- DeepSeek Harness(DSH)插件。把 CNB 的额度、AI 用量和运行中容器的反代地址放进 IDE 侧栏,
4
- 并提供「一键写回模型地址」和「四步重启容器」。
3
+ 把 `cnb-assist-wpf` 那套已经跑通的 CNB 逻辑搬进 DeepSeek Harness 现在的 IDE 里。
4
+ 令牌留在 Host 进程,浏览器半身只拿算好的数字;写配置只替换 `baseURL` 那一个字符串。
5
5
 
6
- - **令牌不出 Host 进程** —— 浏览器那半只拿到算好的数字。
7
- - **写配置只改一个字符串** —— 只替换模型 `baseURL` 里那条 `*.cnb.run` 地址,不动整份 YAML。
8
- - **零运行时依赖** —— 纯 ESM 源码,装上就能用。
6
+ ## 装法
9
7
 
10
- ---
8
+ ### 方式 A:同机源码目录(开发时用)
11
9
 
12
- ## 装
13
-
14
- 在 DSH 里打开插件管理:
15
-
16
- ```
17
- plugin_manager → install_bundle → target: dsh-cnb-assist
10
+ ```bash
11
+ plugin_manager → install_bundle → target: 本目录的绝对路径
12
+ # 示例:C:\Users\LMUD\Desktop\cnb2api\dsh-cnb-assist
13
+ # 用 link: 安装,改完代码只刷新页面即可(Host 半身改动才需要重启 dsh)
18
14
  ```
19
15
 
20
- 要固定版本就写 `dsh-cnb-assist@1.0.2`。
21
-
22
- 离线安装也行,把 `dsh-cnb-assist-*.tgz` 拷到目标机器后填该文件的绝对路径。
23
- 安装器认**目录**和 **`.tgz`/`.tar.gz`**,`.zip` 不被接受。
24
-
25
- 装完**重启一次 dsh**(Host 层代码不会热更新),然后刷新页面。
26
-
27
- ---
28
-
29
- ## 用
30
-
31
- ### 1. 填令牌
32
-
33
- `设置 → CNB 反代辅助`,只有两项:
16
+ ### 方式 B:另一台电脑(推荐)
34
17
 
35
- | 项 | 说明 |
36
- |---|---|
37
- | **CNB 令牌** | CNB 的 access token,至少要有容器与构建日志的读权限 |
38
- | **模型 provider** | 写回地址时要改哪个 provider,默认 `cnb`,需与配置里的键名一致 |
39
-
40
- 令牌也可以不填,插件会按顺序兜底查找:
41
-
42
- 1. 插件设置里的 `config.token`
43
- 2. `%APPDATA%\CnbAssist\settings.json`(桌面版辅助程序存过的那份)
44
- 3. 环境变量 `CNB_API_KEY`
45
-
46
- 令牌与地址**不随包分发**,每台机器各自配置。
47
-
48
- ### 2. 打开面板
18
+ **安装器只认目录或 `.tgz`/`.tar.gz`,不认 `.zip`**(`.zip` 会被当成目录去读 `package.json`,
19
+ 所以会报"该路径不存在或不是有效的插件包")。
49
20
 
50
- 点左侧栏底部的橘黄叶片图标,面板从上到下是四块:
21
+ ```bash
22
+ # 把 local-dsh-cnb-assist-1.0.1.tgz 拷过去,然后在 DSH 里:
23
+ plugin_manager → install_bundle → target: 该 tgz 的绝对路径
24
+ # 也可以直接填解压后的目录绝对路径(目录里要有 package.json)
25
+ ```
51
26
 
52
- - **运行中容器** —— 反代地址,可复制、可四步重启、可一键写回模型地址
53
- - **当前配置** —— 从本 profile 的 `cordis.patch.yml` 里读出来的现有地址,标了行号和 provider
54
- - **额度** —— 每个组织的月额度 / 已用 / 预冻结,带进度条
55
- - **本月按组织** —— 本月创建的容器按组织汇总的 credits 与 token 用量
27
+ > 版本号要在两台机器上对得起来。`files` 字段只列真实存在的条目(`locale` 写成目录而不是
28
+ > `locale/*.json`),否则 `pnpm pack` 的 `scandir` 会直接报 ENOENT。
56
29
 
57
- ### 3. 写回模型地址
30
+ 本包是纯 ESM 源码、零运行时依赖,除 `@deepseek-ai/schemastery`(从 dsh 安装里解析,不需要
31
+ 随包分发)之外不 import 任何第三方包,所以不存在"换台机器装不上"的 node_modules 问题。
58
32
 
59
- 容器那一行点「设为模型地址」,插件把 `https://<business_id>-9001.cnb.run` 写进
60
- 设置里那个 provider 的 `baseURL`。写前会留 `.bak`,只替换地址字符串本身。
33
+ ### 方式 C:私有 npm / git
61
34
 
62
- ---
35
+ ```bash
36
+ # 推到自己的 npm 或 git 仓库后
37
+ plugin_manager → install_bundle → target: 包名@版本 或 git+ssh://...
38
+ ```
63
39
 
64
- ## 面板之外
40
+ **装完重启一次 dsh**:`cordis.patch.yml` 属于 Host 层,HMR 不会重建 Host 模块。
65
41
 
66
- 模型工具 `cnb_assist` 始终可用,四个动作:
42
+ ## 落点
67
43
 
68
- | 动作 | 作用 |
44
+ | 位置 | 说明 |
69
45
  |---|---|
70
- | `snapshot` | 额度 / 用量 / 容器 / 当前配置,一次拿全 |
71
- | `config` | 读配置,或从中解析出现有反代地址 |
72
- | `restart` | 走 stop → 等停稳 → start → 等新地址 |
73
- | `apply` | 把地址写进指定 provider |
74
-
75
- ---
76
-
77
- ## 常见问题
46
+ | 左侧栏底部按钮 + 居中浮层 | 主入口(橘黄叶片图标) |
47
+ | 设置 →「CNB 反代辅助」 | 只两项:CNB 令牌、模型 provider |
48
+ | 模型工具 `cnb_assist` | 始终注册:`snapshot` / `config` / `restart` / `apply` |
78
49
 
79
- **面板说「未配置令牌」** —— 去设置里填,或把 `CNB_API_KEY` 设成环境变量后重启 dsh。
50
+ > 右侧栏页签已移除:与左侧按钮重复,插件也不再依赖 `dsh-better-sidebar`。
80
51
 
81
- **额度是空的** —— 令牌权限不够。读容器列表要 `account-engage:r`,读构建日志要
82
- `repo-cnb-history:r`。
52
+ ## 换到新机器后要做的事
83
53
 
84
- **本月合计比网页上少** —— 已删除的容器查不到归属,会被丢弃,这是预期行为。
54
+ 1. 装好后刷新页面,点左侧栏底部的橘黄叶片
55
+ 2. 设置 →「CNB 反代辅助」→ 填 **CNB 令牌**(或让它读 `CNB_API_KEY` / 旧的
56
+ `%APPDATA%\CnbAssist\settings.json`)
57
+ 3. 确认 **模型 provider** 与 `llm-pi-ai` 行里的键名一致(默认 `cnb`)
58
+ 4. 面板里点容器那一行的「设为模型地址」,把反代地址写进该 provider 的 `baseURL`
85
59
 
86
- **重启后地址变了** —— 正常。重启会重建容器,`business_id` 变,地址跟着变。
60
+ 令牌与地址**不会**随包带走,每台机器各自配置。
87
61
 
88
- **改完配置没生效** —— `cordis.patch.yml` 属于 Host 层,要重启 dsh,刷新页面不够。
62
+ ## 面板有什么
89
63
 
90
- ---
64
+ - **额度**:每个组织的月额度 / 已用 / 预冻结,带进度条。`credit_in_milli` 一律 ÷1000;
65
+ 特权用四重判定(`total<=0 || bonus<=0 || 已过期 || bonus>total` 即视为无)。
66
+ - **本月按组织**:只统计本地时区本月创建的容器,归属只认构建日志的 `groupName`,
67
+ 查不到的容器直接丢弃(不拿 slug 兜底)。
68
+ - **运行中容器**:反代地址(`business_id` + `-9001` 优先,解析不到就从阶段日志里抠),
69
+ 复制、四步重启、一键写回模型 `baseURL`。
70
+ - **配置里现有的地址**:从本 profile 的 `cordis.patch.yml` 里正则读出来,标行号与 provider。
91
71
 
92
- ## 兼容性
72
+ ## 令牌从哪来
93
73
 
94
- - Node.js ≥ 20
95
- - 只依赖宿主提供的 `@deepseek-ai/dsh-client-ui-slots` 与 `@deepseek-ai/dsh-client-locale`,
96
- 无需额外安装任何包
97
- - 样式只用 DSH 主题变量,跟随亮色 / 暗色
74
+ 1. 设置页 / 插件行 `config.token`
75
+ 2. `%APPDATA%\CnbAssist\settings.json`(桌面版辅助程序已经存过的那一份,不用再抄一遍)
76
+ 3. `CNB_API_KEY` 环境变量
98
77
 
99
- ## 许可
78
+ ## 关键约定(来自 cnb-assist-wpf/AGENTS.md)
100
79
 
101
- MIT
80
+ - 所有 CNB 请求必须带单值 `Accept: application/vnd.cnb.api+json`,多值会 406。
81
+ - 组织归属只认 `build/logs` 的 `groupName`,查不到就丢弃。
82
+ - 「本月」按本机时区取 1 号 00:00。
83
+ - 重启必须四步:`stop → 等非 running → start → 等新地址`;`start` 的 `branch` 必填。
84
+ - 写配置绝不重写整份 YAML:只替换匹配到的 `-9001.cnb.run` 字符串,写前留 `.bak`。
package/client.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @local/dsh-cnb-assist — Client half.
2
+ * dsh-cnb-assist — Client half.
3
3
  *
4
4
  * 三个落点,按有没有装 dsh-better-sidebar 自动选:
5
5
  * 1. better-sidebar 在 → 右侧栏页签「CNB」(页签 = 一个组件,数据经 host.call 从 Host 半身取)
@@ -10,13 +10,13 @@
10
10
  * 样式只用 --dsw-alias-* 主题令牌,亮色/暗色跟随宿主。
11
11
  */
12
12
  window.__ModuleLoader__.load({
13
- id: '@local/dsh-cnb-assist',
13
+ id: 'dsh-cnb-assist',
14
14
  factory(require) {
15
15
  const React = require('react')
16
16
  const h = React.createElement
17
17
  const { useState, useEffect, useMemo, useRef, useSyncExternalStore, createContext, useContext } = React
18
18
 
19
- const PLUGIN_ID = '@local/dsh-cnb-assist'
19
+ const PLUGIN_ID = 'dsh-cnb-assist'
20
20
  const ROUTE = 'cnb-assist/api'
21
21
  const ASSIST_KEY = 'dsh-cnb-assist'
22
22
  /** 本插件在宿主 locale 注册表里的命名空间(也是 slot 项的 locale 标记)。 */
@@ -762,32 +762,49 @@ window.__ModuleLoader__.load({
762
762
  useStyles()
763
763
  const [form, setForm] = useState({ token: '', syncProvider: 'cnb' })
764
764
  const [state, setState] = useState({ saving: false, message: null, error: null })
765
-
765
+ const [saved, setSaved] = useState({ hasToken: false, tokenSource: 'none' })
766
+
767
+ /**
768
+ * 回填已保存的值。
769
+ * ★ 不能只依赖 `ctx.configForms`:那个服务在行没有 schema、或客户端没注册时
770
+ * 取不到,表单就会永远空白,用户以为"保存没生效"。所以这里始终向 Host
771
+ * 要一次当前行的 config(`method: 'loadConfig'`),configForms 只作补充。
772
+ * ★ Host 不回令牌明文,只回 hasToken;输入框留空表示"保持原值"。
773
+ */
766
774
  useEffect(() => {
767
- const face = configFormOf(ctx)
768
- if (face === undefined) return undefined
769
775
  let alive = true
770
- const read = (snapshot) => {
771
- if (!alive) return
772
- const value = snapshot?.value ?? snapshot?.config ?? {}
776
+ const apply = (value) => {
777
+ if (!alive || value === null || typeof value !== 'object') return
773
778
  setForm((current) => ({
774
- ...current,
775
- token: String(value?.token ?? current.token ?? ''),
779
+ token: current.token === '' ? String(value?.token ?? '') : current.token,
776
780
  syncProvider: String(value?.syncProvider ?? current.syncProvider ?? 'cnb') || 'cnb',
777
781
  }))
778
782
  }
779
- if (typeof face.getSnapshot === 'function') {
780
- read(face.getSnapshot())
783
+
784
+ // 1) Host 侧真实配置(唯一可靠来源)
785
+ callHost('loadConfig')
786
+ .then((result) => {
787
+ if (!alive) return
788
+ apply(result?.config)
789
+ setSaved({
790
+ hasToken: result?.hasToken === true,
791
+ tokenSource: String(result?.tokenSource ?? 'none'),
792
+ })
793
+ })
794
+ .catch(() => { /* Host 不可达时静默 */ })
795
+
796
+ // 2) configForms 有就顺带订阅
797
+ const face = configFormOf(ctx)
798
+ if (face !== undefined && typeof face.getSnapshot === 'function') {
799
+ const read = (snapshot) => {
800
+ const value = snapshot?.value ?? snapshot?.config
801
+ if (value !== undefined && value !== null) apply(value)
802
+ }
803
+ try { read(face.getSnapshot()) } catch { /* 忽略 */ }
781
804
  if (typeof face.subscribe === 'function') {
782
- const off = face.subscribe(() => { read(face.getSnapshot()) })
805
+ const off = face.subscribe(() => { try { read(face.getSnapshot()) } catch { /* 忽略 */ } })
783
806
  return () => { alive = false; if (typeof off === 'function') off() }
784
807
  }
785
- return () => { alive = false }
786
- }
787
- if (typeof face.read === 'function') {
788
- Promise.resolve(face.read())
789
- .then(read)
790
- .catch((error) => { if (alive) setState({ saving: false, message: null, error: String(error?.message ?? error) }) })
791
808
  }
792
809
  return () => { alive = false }
793
810
  }, [ctx])
@@ -799,23 +816,21 @@ window.__ModuleLoader__.load({
799
816
 
800
817
  const save = async () => {
801
818
  setState({ saving: true, message: null, error: null })
802
- // 只写这两项:其余设置项已从界面移除,配置文件里的值保持不动。
803
- const next = {
804
- token: form.token.trim(),
805
- syncProvider: form.syncProvider || 'cnb',
806
- }
819
+ // ★ 令牌留空 = 保持原值不动,不要用空串把已保存的令牌覆盖掉。
820
+ const trimmed = form.token.trim()
821
+ const next = { syncProvider: form.syncProvider || 'cnb' }
822
+ if (trimmed !== '') next.token = trimmed
807
823
  try {
808
- const face = configFormOf(ctx)
809
- if (face !== undefined && typeof face.set === 'function') {
810
- await face.set(next)
811
- } else if (face !== undefined && typeof face.update === 'function') {
812
- await face.update(next)
813
- } else {
814
- // 没有可用的表单面:交给 Host 走 Config 编辑通道。
815
- const result = await callHost('saveConfig', { config: next })
816
- if (result?.ok !== true) throw new Error(String(result?.message ?? 'save failed'))
817
- }
824
+ // ★ 一律走 Host 的 saveConfig:它经 Config 编辑通道写回 profile 的
825
+ // cordis.patch.yml,本进程立即生效,重启后也还在。
826
+ // configForms.set 只改客户端表单状态、不落盘,之前两条路径混用,
827
+ // 导致"点保存看似成功、实际读到的还是旧令牌"。
828
+ const result = await callHost('saveConfig', { config: next })
829
+ if (result?.ok !== true) throw new Error(String(result?.message ?? 'save failed'))
818
830
  setState({ saving: false, message: copy.saved, error: null })
831
+ // 存完把输入框清空并标记"已设置",避免明文长期停在输入框里。
832
+ setForm((current) => ({ ...current, token: '' }))
833
+ if (trimmed !== '') setSaved({ hasToken: true, tokenSource: 'config' })
819
834
  } catch (error) {
820
835
  setState({ saving: false, message: null, error: String(error?.message ?? error) })
821
836
  }
@@ -829,11 +844,14 @@ window.__ModuleLoader__.load({
829
844
  type: 'password',
830
845
  value: form.token,
831
846
  onChange: set('token'),
832
- placeholder: 'cnb_…',
847
+ placeholder: saved.hasToken ? '已保存,留空则不修改' : 'cnb_…',
833
848
  autoComplete: 'off',
834
849
  spellCheck: false,
835
850
  }),
836
- h('span', { className: 'cnba-note' }, copy.fTokenHint)),
851
+ h('span', { className: 'cnba-note' },
852
+ saved.hasToken
853
+ ? `当前令牌来源:${saved.tokenSource}(留空保存则保持不变)`
854
+ : copy.fTokenHint)),
837
855
  h('div', { className: 'cnba-field', key: 'provider' },
838
856
  h('span', { className: 'cnba-label' }, copy.fProvider),
839
857
  h('input', {
package/cordis.patch.yml CHANGED
@@ -1,7 +1,9 @@
1
1
  # CNB 反代辅助(DSH 插件)
2
2
  #
3
3
  # 一行 Host 半身:提供 cnbAssist 服务、/cnb-assist/api 路由、cnb_assist 工具。
4
- # 浏览器半身由 package.json 的 dsh.client 自动加载。
4
+ # 浏览器半身由 package.json 的 dsh.client 自动加载:
5
+ # · 装了 dsh-better-sidebar → 右侧栏页签「CNB」
6
+ # · 没装 → 侧栏底部一个按钮 + 居中浮层面板
5
7
  - insert:
6
8
  - id: cnb-assist
7
9
  name: 'dsh-cnb-assist'
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @local/dsh-cnb-assist — Host half.
2
+ * dsh-cnb-assist — Host half.
3
3
  *
4
4
  * 把 cnb-assist-wpf/core 里那套已经跑通的 CNB 逻辑搬进 DSH:
5
5
  * Api.cs → 这里的 callCnb / 各 endpoint
@@ -1209,11 +1209,29 @@ export function apply(ctx, config) {
1209
1209
  }
1210
1210
  }
1211
1211
 
1212
+ /**
1213
+ * 读回本行当前的 config(设置页回填用)。
1214
+ * ★ 不回令牌明文:它一旦进了页面 DOM,任何能读这个页面的人都拿得到。
1215
+ * 这里只回 `hasToken`,输入框留空即表示"保持原值不动"。
1216
+ */
1217
+ const loadConfig = async () => {
1218
+ const resolved = await resolveToken(settings.token)
1219
+ return {
1220
+ config: {
1221
+ token: '',
1222
+ syncProvider: settings.syncProvider,
1223
+ },
1224
+ hasToken: resolved.token !== '',
1225
+ tokenSource: resolved.source,
1226
+ }
1227
+ }
1228
+
1212
1229
  const dispatch = async (body) => {
1213
1230
  const method = String(body?.method ?? '')
1214
1231
  switch (method) {
1215
1232
  case 'snapshot': return await service.snapshot({ groups: body?.groups })
1216
1233
  case 'config': return await service.config()
1234
+ case 'loadConfig': return await loadConfig()
1217
1235
  case 'restart': return await service.restart(body ?? {})
1218
1236
  case 'apply': return await service.apply(body?.url)
1219
1237
  case 'saveConfig': return await saveConfig(body?.config)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-cnb-assist",
3
- "version": "1.0.3",
4
- "description": "CNB 反代辅助(DSH 插件):查看各组织额度与本月 AI 用量,管理运行中容器的反代地址,一键写回模型 baseURL",
3
+ "version": "1.0.5",
4
+ "description": "CNB 反代辅助:额度、本月 AI 用量、运行中容器反代地址,一键写回 DSH 模型配置",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "exports": {
@@ -18,26 +18,16 @@
18
18
  "locale",
19
19
  "README.md",
20
20
  "selfcheck.mjs",
21
- "selfcheck-relay.mjs"
21
+ "selfcheck-relay.mjs",
22
+ "selfcheck-identity.mjs"
22
23
  ],
23
24
  "icon": "./icon.svg",
24
25
  "keywords": [
25
26
  "dsh",
26
27
  "dsh-plugin",
27
- "cordis",
28
- "deepseek-harness",
29
- "cnb",
30
- "cnb-cool"
28
+ "cnb"
31
29
  ],
32
30
  "license": "MIT",
33
- "repository": {
34
- "type": "git",
35
- "url": "git+https://github.com/lonelymud/dsh-cnb-assist.git"
36
- },
37
- "homepage": "https://cnb.cool",
38
- "engines": {
39
- "node": ">=20"
40
- },
41
31
  "dsh": {
42
32
  "bundle": {
43
33
  "patch": "./cordis.patch.yml"
@@ -50,5 +40,9 @@
50
40
  "@deepseek-ai/dsh-client-locale"
51
41
  ]
52
42
  }
43
+ },
44
+ "scripts": {
45
+ "test": "node selfcheck-identity.mjs && node selfcheck.mjs && node selfcheck-relay.mjs",
46
+ "prepublishOnly": "node selfcheck-identity.mjs && node selfcheck.mjs && node selfcheck-relay.mjs"
53
47
  }
54
48
  }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * 发布前一致性门禁。
3
+ *
4
+ * 背景:1.0.3 发出去后 dsh 启动崩溃,根因是 client.js 里的
5
+ * `__ModuleLoader__.load({ id })` 还写着 `@local/dsh-cnb-assist`,
6
+ * 而包名已经是 `dsh-cnb-assist`。客户端按 @local/... 注册,设置表单按新包名查,
7
+ * 两边对不上直接把宿主带崩。
8
+ *
9
+ * 这个脚本把这些"必须处处一致"的标识全部对一遍,任何一个跑偏就非零退出,
10
+ * 让 npm publish 在 commit 之前就失败。
11
+ */
12
+ import assert from 'node:assert/strict'
13
+ import { readFileSync } from 'node:fs'
14
+ import { fileURLToPath } from 'node:url'
15
+ import { dirname, join } from 'node:path'
16
+
17
+ const here = dirname(fileURLToPath(import.meta.url))
18
+ const read = f => readFileSync(join(here, f), 'utf8')
19
+
20
+ const pkg = JSON.parse(read('package.json'))
21
+ const clientJs = read('client.js')
22
+ const indexJs = read('index.js')
23
+ const patchYml = read('cordis.patch.yml')
24
+
25
+ let pass = 0
26
+ function ok(name) {
27
+ pass += 1
28
+ console.log('PASS ', name)
29
+ }
30
+ function eq(name, actual, expected) {
31
+ assert.equal(actual, expected, `${name}\n actual : ${actual}\n expected: ${expected}`)
32
+ pass += 1
33
+ console.log('PASS ', name)
34
+ }
35
+
36
+ const PLUGIN = pkg.name
37
+
38
+ // ── 1. 包名必须是公开可发布的形态
39
+ ok('包名非空')
40
+ assert.ok(PLUGIN && PLUGIN.length > 0)
41
+ ok('包名不是 @local/ 私有 scope')
42
+ assert.ok(!PLUGIN.startsWith('@local/'), `包名仍是私有 scope:${PLUGIN}`)
43
+ ok('没有 private:true')
44
+ assert.notEqual(pkg.private, true)
45
+
46
+ // ── 2. client.js 的加载 id 必须与包名一致
47
+ const loadId = /__ModuleLoader__\.load\(\{\s*\n\s*id:\s*'([^']+)'/.exec(clientJs)?.[1]
48
+ eq('client.js __ModuleLoader__ id 与包名一致', loadId, PLUGIN)
49
+
50
+ const pluginId = /const PLUGIN_ID = '([^']+)'/.exec(clientJs)?.[1]
51
+ eq('client.js PLUGIN_ID 与包名一致', pluginId, PLUGIN)
52
+
53
+ // ── 3. host 半身导出名必须与包名一致
54
+ const hostName = /export const name = '([^']+)'/.exec(indexJs)?.[1]
55
+ eq('index.js export name 与包名一致', hostName, PLUGIN)
56
+
57
+ // ── 4. bundle patch 里 insert 的 name 必须与包名一致
58
+ const patchName = /^\s*name:\s*'([^']+)'/m.exec(patchYml)?.[1]
59
+ eq('cordis.patch.yml insert name 与包名一致', patchName, PLUGIN)
60
+
61
+ // ── 5. 任何文件都不该再出现 @local/
62
+ const files = ['client.js', 'index.js', 'cordis.patch.yml', 'package.json', 'README.md',
63
+ 'locale/zh.json', 'locale/en.json', 'selfcheck.mjs', 'selfcheck-relay.mjs']
64
+ const offenders = []
65
+ for (const f of files) {
66
+ let text
67
+ try { text = read(f) } catch { continue }
68
+ const lines = text.split('\n')
69
+ lines.forEach((line, i) => {
70
+ if (line.includes('@local/')) offenders.push(`${f}:${i + 1} ${line.trim()}`)
71
+ })
72
+ }
73
+ if (offenders.length > 0) {
74
+ console.log('\n发现 @local/ 残留:')
75
+ for (const o of offenders) console.log(' ' + o)
76
+ }
77
+ eq('全仓库无 @local/ 残留', offenders.length, 0)
78
+
79
+ // ── 6. files 清单里每一项都要真实存在(locale 作为目录)
80
+ const { existsSync } = await import('node:fs')
81
+ for (const entry of pkg.files ?? []) {
82
+ ok(`files 清单存在:${entry}`)
83
+ assert.ok(existsSync(join(here, entry)), `files 里列了不存在的条目:${entry}`)
84
+ }
85
+
86
+ // ── 7. 两个必需文件必须在清单里
87
+ ok('index.js 在 files 里')
88
+ assert.ok((pkg.files ?? []).includes('index.js'))
89
+ ok('client.js 在 files 里')
90
+ assert.ok((pkg.files ?? []).includes('client.js'))
91
+ ok('cordis.patch.yml 在 files 里')
92
+ assert.ok((pkg.files ?? []).includes('cordis.patch.yml'))
93
+
94
+ // ── 8. dsh 清单字段完整
95
+ eq('dsh.bundle.patch 指向 cordis.patch.yml', pkg.dsh?.bundle?.patch, './cordis.patch.yml')
96
+ eq('dsh.client.platform 是 web', pkg.dsh?.client?.platform, 'web')
97
+ ok('dsh.client.immediately 为 true')
98
+ assert.equal(pkg.dsh?.client?.immediately, true)
99
+ ok('dsh.client.inject 含 ui-slots')
100
+ assert.ok(pkg.dsh?.client?.inject?.includes('@deepseek-ai/dsh-client-ui-slots'))
101
+ ok('dsh.client.inject 含 locale')
102
+ assert.ok(pkg.dsh?.client?.inject?.includes('@deepseek-ai/dsh-client-locale'))
103
+
104
+ // ── 9. 版本号形态
105
+ ok('version 是 x.y.z')
106
+ assert.match(pkg.version, /^\d+\.\d+\.\d+$/)
107
+
108
+ console.log(`\nall ${pass} consistency checks passed (package: ${PLUGIN}@${pkg.version})`)