dsh-cnb-assist 1.0.2 → 1.0.4
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 +15 -2
- package/cordis.patch.yml +3 -1
- package/index.js +81 -16
- package/package.json +10 -15
- package/selfcheck-identity.mjs +108 -0
- package/selfcheck-relay.mjs +127 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CNB 反代辅助
|
|
2
2
|
|
|
3
|
-
DeepSeek Harness(DSH)插件。把 CNB 的额度、AI 用量和运行中容器的反代地址放进 IDE 侧栏,
|
|
3
|
+
DeepSeek Harness(DSH)插件。把 CNB 的额度、AI 用量和运行中容器的反代地址放进 IDE 侧栏,
|
|
4
4
|
并提供「一键写回模型地址」和「四步重启容器」。
|
|
5
5
|
|
|
6
6
|
- **令牌不出 Host 进程** —— 浏览器那半只拿到算好的数字。
|
|
@@ -17,7 +17,7 @@ DeepSeek Harness(DSH)插件。把 CNB 的额度、AI 用量和运行中容
|
|
|
17
17
|
plugin_manager → install_bundle → target: dsh-cnb-assist
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
要固定版本就写 `dsh-cnb-assist@1.0.
|
|
20
|
+
要固定版本就写 `dsh-cnb-assist@1.0.4`。
|
|
21
21
|
|
|
22
22
|
离线安装也行,把 `dsh-cnb-assist-*.tgz` 拷到目标机器后填该文件的绝对路径。
|
|
23
23
|
安装器认**目录**和 **`.tgz`/`.tar.gz`**,`.zip` 不被接受。
|
|
@@ -59,6 +59,17 @@ plugin_manager → install_bundle → target: dsh-cnb-assist
|
|
|
59
59
|
容器那一行点「设为模型地址」,插件把 `https://<business_id>-9001.cnb.run` 写进
|
|
60
60
|
设置里那个 provider 的 `baseURL`。写前会留 `.bak`,只替换地址字符串本身。
|
|
61
61
|
|
|
62
|
+
### 反代地址从哪来
|
|
63
|
+
|
|
64
|
+
优先读容器构建日志里平台注入的模板行:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
PROXY_URI=https://<business_id>-{{port}}.cnb.run
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
把 `{{port}}` 换成 `9001` 后作为地址显示。日志里取不到时,才用容器的 `business_id`
|
|
71
|
+
直接拼 `https://<business_id>-9001.cnb.run`。两条路都走,有哪个显示哪个。
|
|
72
|
+
|
|
62
73
|
---
|
|
63
74
|
|
|
64
75
|
## 面板之外
|
|
@@ -85,6 +96,8 @@ plugin_manager → install_bundle → target: dsh-cnb-assist
|
|
|
85
96
|
|
|
86
97
|
**重启后地址变了** —— 正常。重启会重建容器,`business_id` 变,地址跟着变。
|
|
87
98
|
|
|
99
|
+
**地址里出现 `{{port}}`** —— 升级到 1.0.4 及以上。
|
|
100
|
+
|
|
88
101
|
**改完配置没生效** —— `cordis.patch.yml` 属于 Host 层,要重启 dsh,刷新页面不够。
|
|
89
102
|
|
|
90
103
|
---
|
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
|
@@ -29,6 +29,17 @@ export const inject = ['webServer', 'tools']
|
|
|
29
29
|
/** CNB 地址固定的反代端口(用户已确认固定,见 AGENTS.md §7)。 */
|
|
30
30
|
const RELAY_PORT = 9001
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* 阶段日志里平台注入的地址模板,形如:
|
|
34
|
+
* PROXY_URI=https://n2lc6c2bdm-{{port}}.cnb.run
|
|
35
|
+
* ★ {{port}} 是字面量双大括号,不是模板字符串语法。
|
|
36
|
+
* ★ 这一行有时会和后面的输出粘在同一行尾(`... ===== register skipped`),必须按空白截断。
|
|
37
|
+
*/
|
|
38
|
+
const PROXY_URI_RX = /PROXY_URI\s*=\s*([^\s"'`,;]+)/
|
|
39
|
+
|
|
40
|
+
/** 把地址模板里的 {{port}} 换成真实端口。 */
|
|
41
|
+
const PORT_PLACEHOLDER_RX = /\{\{\s*port\s*\}\}/gi
|
|
42
|
+
|
|
32
43
|
/** 浏览器半身投递的路由。 */
|
|
33
44
|
const ROUTE = '/cnb-assist/api'
|
|
34
45
|
|
|
@@ -542,15 +553,47 @@ async function buildMonthSummary(call) {
|
|
|
542
553
|
|
|
543
554
|
// ───────────────────────────── 反代地址 ─────────────────────────────
|
|
544
555
|
|
|
545
|
-
/** business_id +
|
|
556
|
+
/** business_id + 固定端口拼地址(兜底之一)。 */
|
|
546
557
|
function relayFromBusinessId(businessId) {
|
|
547
558
|
const id = String(businessId ?? '').trim()
|
|
548
559
|
return id === '' ? null : `https://${id}-${RELAY_PORT}.cnb.run/v1`
|
|
549
560
|
}
|
|
550
561
|
|
|
562
|
+
/**
|
|
563
|
+
* 把地址里的 {{port}} 占位符换成真实端口,再补成可用的 /v1 地址。
|
|
564
|
+
* 已经是成品地址的(没有占位符)原样保留,只补 /v1。
|
|
565
|
+
*/
|
|
566
|
+
function materializeRelayUrl(raw) {
|
|
567
|
+
const text = String(raw ?? '').trim()
|
|
568
|
+
if (text === '') return null
|
|
569
|
+
let url = text.replace(PORT_PLACEHOLDER_RX, String(RELAY_PORT))
|
|
570
|
+
let parsed
|
|
571
|
+
try {
|
|
572
|
+
parsed = new URL(url)
|
|
573
|
+
} catch {
|
|
574
|
+
return null
|
|
575
|
+
}
|
|
576
|
+
if (!parsed.host.toLowerCase().endsWith('.cnb.run')) return null
|
|
577
|
+
const path = parsed.pathname.replace(/\/+$/, '')
|
|
578
|
+
parsed.pathname = path.toLowerCase().endsWith('/v1') ? path : `${path}/v1`
|
|
579
|
+
return parsed.toString()
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* 从阶段日志里取 PROXY_URI 模板。
|
|
584
|
+
* ★ 这是最权威的来源:容器重建后 business_id 由平台重新分配,模板里的就是新值。
|
|
585
|
+
*/
|
|
586
|
+
function relayFromProxyUri(text) {
|
|
587
|
+
if (typeof text !== 'string' || text === '') return null
|
|
588
|
+
const match = PROXY_URI_RX.exec(text)
|
|
589
|
+
return match === null ? null : materializeRelayUrl(match[1])
|
|
590
|
+
}
|
|
591
|
+
|
|
551
592
|
/**
|
|
552
593
|
* 从阶段日志里抠地址,带打分:来自该 business_id 的 > 路径以 /v1 结尾的 >
|
|
553
594
|
* 端口是 9001 的。找不到 cnb.run 就返回 null。
|
|
595
|
+
* ★ new URL() 能"接受" {{port}}(它把整段当主机名),所以旧写法不会报错,
|
|
596
|
+
* 只会把没替换占位符的字符串原样展示出去。必须先 materialize 再判分。
|
|
554
597
|
*/
|
|
555
598
|
function extractRelayAddress(text, businessId = '') {
|
|
556
599
|
if (typeof text !== 'string' || text === '') return null
|
|
@@ -561,9 +604,11 @@ function extractRelayAddress(text, businessId = '') {
|
|
|
561
604
|
const raw = match[0].replace(/[.,。;]+$/, '')
|
|
562
605
|
if (raw.length <= 12) continue
|
|
563
606
|
if (!raw.includes('.cnb.run')) continue
|
|
607
|
+
const normalized = materializeRelayUrl(raw)
|
|
608
|
+
if (normalized === null) continue
|
|
564
609
|
let url
|
|
565
610
|
try {
|
|
566
|
-
url = new URL(
|
|
611
|
+
url = new URL(normalized)
|
|
567
612
|
} catch {
|
|
568
613
|
continue
|
|
569
614
|
}
|
|
@@ -571,17 +616,18 @@ function extractRelayAddress(text, businessId = '') {
|
|
|
571
616
|
let score = 1
|
|
572
617
|
if (businessId !== '' && host.startsWith(`${businessId.toLowerCase()}-`)) score += 10
|
|
573
618
|
if (url.pathname.replace(/\/+$/, '').toLowerCase().endsWith('/v1')) score += 4
|
|
574
|
-
if (
|
|
619
|
+
if (host.endsWith(`-${RELAY_PORT}.cnb.run`)) score += 3
|
|
620
|
+
if (PORT_PLACEHOLDER_RX.test(raw)) score += 5
|
|
621
|
+
PORT_PLACEHOLDER_RX.lastIndex = 0
|
|
575
622
|
if (score > bestScore) {
|
|
576
623
|
bestScore = score
|
|
577
|
-
best =
|
|
624
|
+
best = normalized
|
|
578
625
|
}
|
|
579
626
|
}
|
|
580
627
|
return bestScore > 0 ? best : null
|
|
581
628
|
}
|
|
582
629
|
|
|
583
630
|
async function resolveRelayAddress(call, slug, sn, pipelineId) {
|
|
584
|
-
const businessRelay = (item) => relayFromBusinessId(item?.business_id)
|
|
585
631
|
if (slug === '' || sn === '') return null
|
|
586
632
|
const status = await call(`/${enc(slug)}/-/build/status/${enc(sn)}`)
|
|
587
633
|
const stageIds = []
|
|
@@ -595,18 +641,25 @@ async function resolveRelayAddress(call, slug, sn, pipelineId) {
|
|
|
595
641
|
}
|
|
596
642
|
// stage-0 优先(AGENTS.md §7.3:不同仓库可能没有这个阶段名,所以逐阶段扫)。
|
|
597
643
|
stageIds.sort((a, b) => (a === 'stage-0' ? 0 : 1) - (b === 'stage-0' ? 0 : 1))
|
|
644
|
+
|
|
645
|
+
// 先找 PROXY_URI 那一行(权威来源),扫不到再退回通用打分。两轮都扫同一批日志。
|
|
646
|
+
let scanned = ''
|
|
598
647
|
for (const stage of stageIds) {
|
|
599
648
|
const log = await call(
|
|
600
649
|
`/${enc(slug)}/-/build/logs/stage/${enc(sn)}/${enc(pipelineId)}/${enc(stage)}`,
|
|
601
650
|
)
|
|
602
651
|
if (!log.ok) continue
|
|
603
652
|
const content = Array.isArray(log.json?.content) ? log.json.content.join('\n') : ''
|
|
604
|
-
|
|
605
|
-
|
|
653
|
+
scanned += `${content}\n`
|
|
654
|
+
const fromTemplate = relayFromProxyUri(content)
|
|
655
|
+
if (fromTemplate !== null) return fromTemplate
|
|
606
656
|
}
|
|
657
|
+
const fromLog = extractRelayAddress(scanned, '')
|
|
658
|
+
if (fromLog !== null) return fromLog
|
|
659
|
+
|
|
607
660
|
const listed = await listWorkspaces(call)
|
|
608
661
|
const me = listed.items.find(item => String(item?.sn ?? '') === sn)
|
|
609
|
-
return
|
|
662
|
+
return relayFromBusinessId(me?.business_id)
|
|
610
663
|
}
|
|
611
664
|
|
|
612
665
|
/** 运行中容器一行:地址还没解析出来时先给 business_id 兜底值。 */
|
|
@@ -622,13 +675,19 @@ async function buildRelayRows(call) {
|
|
|
622
675
|
const slug = String(item?.slug ?? '')
|
|
623
676
|
const pipelineId = String(item?.pipeline_id ?? '')
|
|
624
677
|
const businessRelay = relayFromBusinessId(item?.business_id)
|
|
625
|
-
|
|
678
|
+
// ★ 两路都要试:日志里的 PROXY_URI 模板是平台重建后注入的新值,比 business_id 新。
|
|
679
|
+
// 先查日志,查不到才退回 business_id 拼的地址。
|
|
680
|
+
let address = null
|
|
681
|
+
let source = 'none'
|
|
682
|
+
try {
|
|
683
|
+
address = await resolveRelayAddress(call, slug, sn, pipelineId)
|
|
684
|
+
if (address !== null) source = 'log'
|
|
685
|
+
} catch {
|
|
686
|
+
address = null
|
|
687
|
+
}
|
|
626
688
|
if (address === null) {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
} catch {
|
|
630
|
-
address = null
|
|
631
|
-
}
|
|
689
|
+
address = businessRelay
|
|
690
|
+
if (address !== null) source = 'businessId'
|
|
632
691
|
}
|
|
633
692
|
const created = parseTime(item?.create_time)
|
|
634
693
|
const seconds = created === null ? Number(item?.duration) || 0 : (Date.now() - created) / 1000
|
|
@@ -641,6 +700,7 @@ async function buildRelayRows(call) {
|
|
|
641
700
|
pipelineId,
|
|
642
701
|
businessId: String(item?.business_id ?? ''),
|
|
643
702
|
address,
|
|
703
|
+
addressSource: source,
|
|
644
704
|
createdAt: created,
|
|
645
705
|
runningSeconds: Math.max(0, seconds),
|
|
646
706
|
text: {
|
|
@@ -734,8 +794,13 @@ async function runRestart(call, input, settings) {
|
|
|
734
794
|
const listed = await listWorkspaces(call)
|
|
735
795
|
const me = listed.items.find(item => String(item?.sn ?? '') === newSn)
|
|
736
796
|
if (me === undefined) continue
|
|
737
|
-
|
|
738
|
-
|
|
797
|
+
let fromLog = null
|
|
798
|
+
try {
|
|
799
|
+
fromLog = await resolveRelayAddress(call, String(me?.slug ?? slug), newSn, String(me?.pipeline_id ?? ''))
|
|
800
|
+
} catch {
|
|
801
|
+
fromLog = null
|
|
802
|
+
}
|
|
803
|
+
address = fromLog ?? relayFromBusinessId(me?.business_id)
|
|
739
804
|
if (address !== null) break
|
|
740
805
|
}
|
|
741
806
|
|
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.4",
|
|
4
|
+
"description": "CNB 反代辅助:额度、本月 AI 用量、运行中容器反代地址,一键写回 DSH 模型配置",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"exports": {
|
|
@@ -17,26 +17,17 @@
|
|
|
17
17
|
"icon.svg",
|
|
18
18
|
"locale",
|
|
19
19
|
"README.md",
|
|
20
|
-
"selfcheck.mjs"
|
|
20
|
+
"selfcheck.mjs",
|
|
21
|
+
"selfcheck-relay.mjs",
|
|
22
|
+
"selfcheck-identity.mjs"
|
|
21
23
|
],
|
|
22
24
|
"icon": "./icon.svg",
|
|
23
25
|
"keywords": [
|
|
24
26
|
"dsh",
|
|
25
27
|
"dsh-plugin",
|
|
26
|
-
"
|
|
27
|
-
"deepseek-harness",
|
|
28
|
-
"cnb",
|
|
29
|
-
"cnb-cool"
|
|
28
|
+
"cnb"
|
|
30
29
|
],
|
|
31
30
|
"license": "MIT",
|
|
32
|
-
"repository": {
|
|
33
|
-
"type": "git",
|
|
34
|
-
"url": "git+https://github.com/lonelymud/dsh-cnb-assist.git"
|
|
35
|
-
},
|
|
36
|
-
"homepage": "https://cnb.cool",
|
|
37
|
-
"engines": {
|
|
38
|
-
"node": ">=20"
|
|
39
|
-
},
|
|
40
31
|
"dsh": {
|
|
41
32
|
"bundle": {
|
|
42
33
|
"patch": "./cordis.patch.yml"
|
|
@@ -49,5 +40,9 @@
|
|
|
49
40
|
"@deepseek-ai/dsh-client-locale"
|
|
50
41
|
]
|
|
51
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"
|
|
52
47
|
}
|
|
53
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})`)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 反代地址规则的离线自检:不联网,纯字符串。
|
|
3
|
+
* 覆盖两种来源(PROXY_URI 模板 / business_id 拼)、占位符替换、以及旧版会漏掉的情形。
|
|
4
|
+
*/
|
|
5
|
+
import assert from 'node:assert/strict'
|
|
6
|
+
|
|
7
|
+
const RELAY_PORT = 9001
|
|
8
|
+
const PROXY_URI_RX = /PROXY_URI\s*=\s*([^\s"'`,;]+)/
|
|
9
|
+
const PORT_PLACEHOLDER_RX = /\{\{\s*port\s*\}\}/gi
|
|
10
|
+
|
|
11
|
+
function materializeRelayUrl(raw) {
|
|
12
|
+
const text = String(raw ?? '').trim()
|
|
13
|
+
if (text === '') return null
|
|
14
|
+
let url = text.replace(PORT_PLACEHOLDER_RX, String(RELAY_PORT))
|
|
15
|
+
let parsed
|
|
16
|
+
try {
|
|
17
|
+
parsed = new URL(url)
|
|
18
|
+
} catch {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
if (!parsed.host.toLowerCase().endsWith('.cnb.run')) return null
|
|
22
|
+
const path = parsed.pathname.replace(/\/+$/, '')
|
|
23
|
+
parsed.pathname = path.toLowerCase().endsWith('/v1') ? path : `${path}/v1`
|
|
24
|
+
return parsed.toString()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function relayFromProxyUri(text) {
|
|
28
|
+
if (typeof text !== 'string' || text === '') return null
|
|
29
|
+
const match = PROXY_URI_RX.exec(text)
|
|
30
|
+
return match === null ? null : materializeRelayUrl(match[1])
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function relayFromBusinessId(businessId) {
|
|
34
|
+
const id = String(businessId ?? '').trim()
|
|
35
|
+
return id === '' ? null : `https://${id}-${RELAY_PORT}.cnb.run/v1`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let pass = 0
|
|
39
|
+
function ok(name, cond) {
|
|
40
|
+
assert.ok(cond, name)
|
|
41
|
+
pass += 1
|
|
42
|
+
console.log('PASS ', name)
|
|
43
|
+
}
|
|
44
|
+
function eq(name, actual, expected) {
|
|
45
|
+
assert.equal(actual, expected, `${name}\n actual : ${actual}\n expected: ${expected}`)
|
|
46
|
+
pass += 1
|
|
47
|
+
console.log('PASS ', name)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── 1. 真实日志原样(今天从 cnb-ps0-1k77t7qst 抓的)
|
|
51
|
+
const REAL = [
|
|
52
|
+
'Runner[10.235.16.12][docker] 2026-11-12 20:55:52 $',
|
|
53
|
+
'===== proxy self-check: health OK =====',
|
|
54
|
+
'{"status":"ok","uptime_sec":1}',
|
|
55
|
+
'PROXY_URI=https://n2lc6c2bdm-{{port}}.cnb.run',
|
|
56
|
+
'===== register skipped (REGISTER_URL / REG_TOKEN / PROXY_URI missing) =====',
|
|
57
|
+
].join('\n')
|
|
58
|
+
|
|
59
|
+
eq('真实日志 → 模板地址',
|
|
60
|
+
relayFromProxyUri(REAL),
|
|
61
|
+
'https://n2lc6c2bdm-9001.cnb.run/v1')
|
|
62
|
+
|
|
63
|
+
// ── 2. 占位符替换本身
|
|
64
|
+
eq('{{port}} 换成 9001',
|
|
65
|
+
materializeRelayUrl('https://n2lc6c2bdm-{{port}}.cnb.run'),
|
|
66
|
+
'https://n2lc6c2bdm-9001.cnb.run/v1')
|
|
67
|
+
eq('带空格 {{ port }} 也认',
|
|
68
|
+
materializeRelayUrl('https://abc-{{ port }}.cnb.run'),
|
|
69
|
+
'https://abc-9001.cnb.run/v1')
|
|
70
|
+
eq('大写 {{PORT}} 也认',
|
|
71
|
+
materializeRelayUrl('https://abc-{{PORT}}.cnb.run'),
|
|
72
|
+
'https://abc-9001.cnb.run/v1')
|
|
73
|
+
|
|
74
|
+
// ── 3. 已经是成品地址的不该被破坏
|
|
75
|
+
eq('成品地址原样保留',
|
|
76
|
+
materializeRelayUrl('https://n2lc6c2bdm-9001.cnb.run/v1'),
|
|
77
|
+
'https://n2lc6c2bdm-9001.cnb.run/v1')
|
|
78
|
+
eq('成品地址缺 /v1 要补',
|
|
79
|
+
materializeRelayUrl('https://n2lc6c2bdm-9001.cnb.run'),
|
|
80
|
+
'https://n2lc6c2bdm-9001.cnb.run/v1')
|
|
81
|
+
|
|
82
|
+
// ── 4. 粘在行尾的情形(真实日志里出现过)
|
|
83
|
+
eq('PROXY_URI 后面粘了别的输出',
|
|
84
|
+
relayFromProxyUri('PROXY_URI=https://n2lc6c2bdm-{{port}}.cnb.run ===== register skipped ====='),
|
|
85
|
+
'https://n2lc6c2bdm-9001.cnb.run/v1')
|
|
86
|
+
|
|
87
|
+
// ── 5. 多行里夹杂其它 URL,只认 PROXY_URI 那行
|
|
88
|
+
eq('只认 PROXY_URI 那行',
|
|
89
|
+
relayFromProxyUri('see https://example.com/x\nPROXY_URI=https://zz9-{{port}}.cnb.run\nmore https://other.cnb.run/y'),
|
|
90
|
+
'https://zz9-9001.cnb.run/v1')
|
|
91
|
+
|
|
92
|
+
// ── 6. 没有模板行 → null,交给 business_id 兜底
|
|
93
|
+
eq('无模板行返回 null', relayFromProxyUri('nothing here'), null)
|
|
94
|
+
eq('空串返回 null', relayFromProxyUri(''), null)
|
|
95
|
+
eq('非字符串返回 null', relayFromProxyUri(null), null)
|
|
96
|
+
|
|
97
|
+
// ── 7. business_id 兜底
|
|
98
|
+
eq('business_id 拼地址',
|
|
99
|
+
relayFromBusinessId('n2lc6c2bdm'),
|
|
100
|
+
'https://n2lc6c2bdm-9001.cnb.run/v1')
|
|
101
|
+
eq('business_id 为空返回 null', relayFromBusinessId(''), null)
|
|
102
|
+
eq('business_id 是 undefined 返回 null', relayFromBusinessId(undefined), null)
|
|
103
|
+
|
|
104
|
+
// ── 8. 两种来源必须同址(本容器实测)
|
|
105
|
+
eq('模板与 business_id 结果一致',
|
|
106
|
+
relayFromProxyUri(REAL),
|
|
107
|
+
relayFromBusinessId('n2lc6c2bdm'))
|
|
108
|
+
|
|
109
|
+
// ── 9. 旧版行为回归:模板行不会被 new URL() 拒绝(它把 {{port}} 当主机名的一部分),
|
|
110
|
+
// 但旧代码会以"未替换占位符"的形态胜出并展示出去,且拿不到端口那 3 分。
|
|
111
|
+
// ★ 这一点很容易误判成"旧代码会抛异常"——实测不会。
|
|
112
|
+
const oldParsed = new URL('https://n2lc6c2bdm-{{port}}.cnb.run')
|
|
113
|
+
ok('模板能被 new URL() 解析(所以旧代码不是抛异常,是错在别处)',
|
|
114
|
+
oldParsed.host === 'n2lc6c2bdm-{{port}}.cnb.run')
|
|
115
|
+
ok('旧代码的端口判分条件不成立(拿不到 +3 分)',
|
|
116
|
+
oldParsed.host.toLowerCase().endsWith(`-${RELAY_PORT}.cnb.run`) === false)
|
|
117
|
+
ok('旧代码会原样输出未替换的占位符',
|
|
118
|
+
oldParsed.toString().includes('{{port}}'))
|
|
119
|
+
ok('新代码输出已替换的成品地址',
|
|
120
|
+
relayFromProxyUri(REAL) === 'https://n2lc6c2bdm-9001.cnb.run/v1')
|
|
121
|
+
ok('新代码输出不再含占位符',
|
|
122
|
+
!relayFromProxyUri(REAL).includes('{{'))
|
|
123
|
+
|
|
124
|
+
// ── 10. 非 cnb.run 域名不该被当成反代地址
|
|
125
|
+
eq('非 cnb.run 返回 null', materializeRelayUrl('https://evil.example.com/{{port}}'), null)
|
|
126
|
+
|
|
127
|
+
console.log(`\nall ${pass} checks passed`)
|