dsh-cnb-assist 1.0.4 → 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 +57 -87
- package/client.js +52 -34
- package/index.js +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,114 +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 反代辅助`,只有两项:
|
|
34
|
-
|
|
35
|
-
| 项 | 说明 |
|
|
36
|
-
|---|---|
|
|
37
|
-
| **CNB 令牌** | CNB 的 access token,至少要有容器与构建日志的读权限 |
|
|
38
|
-
| **模型 provider** | 写回地址时要改哪个 provider,默认 `cnb`,需与配置里的键名一致 |
|
|
39
|
-
|
|
40
|
-
令牌也可以不填,插件会按顺序兜底查找:
|
|
16
|
+
### 方式 B:另一台电脑(推荐)
|
|
41
17
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
3. 环境变量 `CNB_API_KEY`
|
|
18
|
+
**安装器只认目录或 `.tgz`/`.tar.gz`,不认 `.zip`**(`.zip` 会被当成目录去读 `package.json`,
|
|
19
|
+
所以会报"该路径不存在或不是有效的插件包")。
|
|
45
20
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
- **运行中容器** —— 反代地址,可复制、可四步重启、可一键写回模型地址
|
|
53
|
-
- **当前配置** —— 从本 profile 的 `cordis.patch.yml` 里读出来的现有地址,标了行号和 provider
|
|
54
|
-
- **额度** —— 每个组织的月额度 / 已用 / 预冻结,带进度条
|
|
55
|
-
- **本月按组织** —— 本月创建的容器按组织汇总的 credits 与 token 用量
|
|
56
|
-
|
|
57
|
-
### 3. 写回模型地址
|
|
21
|
+
```bash
|
|
22
|
+
# 把 local-dsh-cnb-assist-1.0.1.tgz 拷过去,然后在 DSH 里:
|
|
23
|
+
plugin_manager → install_bundle → target: 该 tgz 的绝对路径
|
|
24
|
+
# 也可以直接填解压后的目录绝对路径(目录里要有 package.json)
|
|
25
|
+
```
|
|
58
26
|
|
|
59
|
-
|
|
60
|
-
|
|
27
|
+
> 版本号要在两台机器上对得起来。`files` 字段只列真实存在的条目(`locale` 写成目录而不是
|
|
28
|
+
> `locale/*.json`),否则 `pnpm pack` 的 `scandir` 会直接报 ENOENT。
|
|
61
29
|
|
|
62
|
-
|
|
30
|
+
本包是纯 ESM 源码、零运行时依赖,除 `@deepseek-ai/schemastery`(从 dsh 安装里解析,不需要
|
|
31
|
+
随包分发)之外不 import 任何第三方包,所以不存在"换台机器装不上"的 node_modules 问题。
|
|
63
32
|
|
|
64
|
-
|
|
33
|
+
### 方式 C:私有 npm / git
|
|
65
34
|
|
|
66
|
-
```
|
|
67
|
-
|
|
35
|
+
```bash
|
|
36
|
+
# 推到自己的 npm 或 git 仓库后
|
|
37
|
+
plugin_manager → install_bundle → target: 包名@版本 或 git+ssh://...
|
|
68
38
|
```
|
|
69
39
|
|
|
70
|
-
|
|
71
|
-
直接拼 `https://<business_id>-9001.cnb.run`。两条路都走,有哪个显示哪个。
|
|
40
|
+
**装完重启一次 dsh**:`cordis.patch.yml` 属于 Host 层,HMR 不会重建 Host 模块。
|
|
72
41
|
|
|
73
|
-
|
|
42
|
+
## 落点
|
|
74
43
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
模型工具 `cnb_assist` 始终可用,四个动作:
|
|
78
|
-
|
|
79
|
-
| 动作 | 作用 |
|
|
44
|
+
| 位置 | 说明 |
|
|
80
45
|
|---|---|
|
|
81
|
-
|
|
|
82
|
-
|
|
|
83
|
-
| `
|
|
84
|
-
| `apply` | 把地址写进指定 provider |
|
|
85
|
-
|
|
86
|
-
---
|
|
87
|
-
|
|
88
|
-
## 常见问题
|
|
89
|
-
|
|
90
|
-
**面板说「未配置令牌」** —— 去设置里填,或把 `CNB_API_KEY` 设成环境变量后重启 dsh。
|
|
46
|
+
| 左侧栏底部按钮 + 居中浮层 | 主入口(橘黄叶片图标) |
|
|
47
|
+
| 设置 →「CNB 反代辅助」 | 只两项:CNB 令牌、模型 provider |
|
|
48
|
+
| 模型工具 `cnb_assist` | 始终注册:`snapshot` / `config` / `restart` / `apply` |
|
|
91
49
|
|
|
92
|
-
|
|
93
|
-
`repo-cnb-history:r`。
|
|
50
|
+
> 右侧栏页签已移除:与左侧按钮重复,插件也不再依赖 `dsh-better-sidebar`。
|
|
94
51
|
|
|
95
|
-
|
|
52
|
+
## 换到新机器后要做的事
|
|
96
53
|
|
|
97
|
-
|
|
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`
|
|
98
59
|
|
|
99
|
-
|
|
60
|
+
令牌与地址**不会**随包带走,每台机器各自配置。
|
|
100
61
|
|
|
101
|
-
|
|
62
|
+
## 面板有什么
|
|
102
63
|
|
|
103
|
-
|
|
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。
|
|
104
71
|
|
|
105
|
-
##
|
|
72
|
+
## 令牌从哪来
|
|
106
73
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
- 样式只用 DSH 主题变量,跟随亮色 / 暗色
|
|
74
|
+
1. 设置页 / 插件行 `config.token`
|
|
75
|
+
2. `%APPDATA%\CnbAssist\settings.json`(桌面版辅助程序已经存过的那一份,不用再抄一遍)
|
|
76
|
+
3. `CNB_API_KEY` 环境变量
|
|
111
77
|
|
|
112
|
-
##
|
|
78
|
+
## 关键约定(来自 cnb-assist-wpf/AGENTS.md)
|
|
113
79
|
|
|
114
|
-
|
|
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
|
@@ -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/index.js
CHANGED
|
@@ -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)
|