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 +58 -75
- package/client.js +55 -37
- package/cordis.patch.yml +3 -1
- package/index.js +19 -1
- package/package.json +9 -15
- package/selfcheck-identity.mjs +108 -0
package/README.md
CHANGED
|
@@ -1,101 +1,84 @@
|
|
|
1
|
-
# CNB
|
|
1
|
+
# CNB 反代辅助(DSH 插件)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
把 `cnb-assist-wpf` 那套已经跑通的 CNB 逻辑搬进 DeepSeek Harness 现在的 IDE 里。
|
|
4
|
+
令牌留在 Host 进程,浏览器半身只拿算好的数字;写配置只替换 `baseURL` 那一个字符串。
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
- **写配置只改一个字符串** —— 只替换模型 `baseURL` 里那条 `*.cnb.run` 地址,不动整份 YAML。
|
|
8
|
-
- **零运行时依赖** —— 纯 ESM 源码,装上就能用。
|
|
6
|
+
## 装法
|
|
9
7
|
|
|
10
|
-
|
|
8
|
+
### 方式 A:同机源码目录(开发时用)
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
- **额度** —— 每个组织的月额度 / 已用 / 预冻结,带进度条
|
|
55
|
-
- **本月按组织** —— 本月创建的容器按组织汇总的 credits 与 token 用量
|
|
27
|
+
> 版本号要在两台机器上对得起来。`files` 字段只列真实存在的条目(`locale` 写成目录而不是
|
|
28
|
+
> `locale/*.json`),否则 `pnpm pack` 的 `scandir` 会直接报 ENOENT。
|
|
56
29
|
|
|
57
|
-
|
|
30
|
+
本包是纯 ESM 源码、零运行时依赖,除 `@deepseek-ai/schemastery`(从 dsh 安装里解析,不需要
|
|
31
|
+
随包分发)之外不 import 任何第三方包,所以不存在"换台机器装不上"的 node_modules 问题。
|
|
58
32
|
|
|
59
|
-
|
|
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
|
-
|
|
42
|
+
## 落点
|
|
67
43
|
|
|
68
|
-
|
|
|
44
|
+
| 位置 | 说明 |
|
|
69
45
|
|---|---|
|
|
70
|
-
|
|
|
71
|
-
|
|
|
72
|
-
| `
|
|
73
|
-
| `apply` | 把地址写进指定 provider |
|
|
74
|
-
|
|
75
|
-
---
|
|
76
|
-
|
|
77
|
-
## 常见问题
|
|
46
|
+
| 左侧栏底部按钮 + 居中浮层 | 主入口(橘黄叶片图标) |
|
|
47
|
+
| 设置 →「CNB 反代辅助」 | 只两项:CNB 令牌、模型 provider |
|
|
48
|
+
| 模型工具 `cnb_assist` | 始终注册:`snapshot` / `config` / `restart` / `apply` |
|
|
78
49
|
|
|
79
|
-
|
|
50
|
+
> 右侧栏页签已移除:与左侧按钮重复,插件也不再依赖 `dsh-better-sidebar`。
|
|
80
51
|
|
|
81
|
-
|
|
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
|
-
|
|
60
|
+
令牌与地址**不会**随包带走,每台机器各自配置。
|
|
87
61
|
|
|
88
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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: '
|
|
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 = '
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
780
|
-
|
|
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
|
|
804
|
-
|
|
805
|
-
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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' },
|
|
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
|
-
*
|
|
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.
|
|
4
|
-
"description": "CNB
|
|
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
|
-
"
|
|
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})`)
|