dsh-napcat-keeper 2.7.3 → 2.8.1
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/CHANGELOG.md +31 -0
- package/README.md +79 -6
- package/lib/client.js +106 -9
- package/lib/index.js +917 -21
- package/package.json +3 -1
- package/scripts/selftest-companion.mjs +488 -0
- package/scripts/selftest-probe.mjs +191 -0
- package/scripts/smoke-client.mjs +95 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-napcat-keeper",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.1",
|
|
4
4
|
"description": "发现没有能够随着dsh打开自动启动napcat的插件,遂使用大肥鱼制作此插件,建议搭配Yuuyuko-uu的dsh-qq-bridge使用~",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
"smoke:client": "node scripts/smoke-client.mjs",
|
|
26
26
|
"selftest:scan": "node scripts/selftest-scan.mjs",
|
|
27
27
|
"selftest:save": "node scripts/selftest-save.mjs",
|
|
28
|
+
"selftest:probe": "node scripts/selftest-probe.mjs",
|
|
29
|
+
"selftest:companion": "node scripts/selftest-companion.mjs",
|
|
28
30
|
"selftest:setup": "node scripts/selftest-setup.mjs"
|
|
29
31
|
},
|
|
30
32
|
"keywords": [
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「和 dsh-onebot-qq 共存」回归自检。
|
|
3
|
+
*
|
|
4
|
+
* 要守的两件事:
|
|
5
|
+
* 1) 认得出对面这个插件 —— 装没装(profile/package.json 的 dependencies)、
|
|
6
|
+
* 挂没挂(dsh.profile.bundles + cordis.patch.yml 里的 id: dsh-onebot)、
|
|
7
|
+
* 它自己配成什么样(mode/port/url/token)。认错挂载状态就会去探一个根本不存在的端口。
|
|
8
|
+
* 2) 跟随要有分寸 —— 自己填的端口通了就绝不动它;没通才改看对面的反向 WS 端口;
|
|
9
|
+
* 对面没挂、是正向模式、或者用户把「跟随」关了,都必须老老实实不跟。
|
|
10
|
+
*
|
|
11
|
+
* 全程离线:临时 DSH_HOME + 本地假 426 服务器,不碰真配置、不碰真 NapCat、不碰真 dsh-onebot-qq。
|
|
12
|
+
* node scripts/selftest-companion.mjs
|
|
13
|
+
*/
|
|
14
|
+
import http from 'node:http'
|
|
15
|
+
import net from 'node:net'
|
|
16
|
+
import fs from 'node:fs'
|
|
17
|
+
import os from 'node:os'
|
|
18
|
+
import path from 'node:path'
|
|
19
|
+
|
|
20
|
+
let pass = 0
|
|
21
|
+
let fail = 0
|
|
22
|
+
const ok = (cond, name, extra = '') => {
|
|
23
|
+
if (cond) {
|
|
24
|
+
pass += 1
|
|
25
|
+
console.log(` ✓ ${name}`)
|
|
26
|
+
} else {
|
|
27
|
+
fail += 1
|
|
28
|
+
console.log(` ✗ ${name}${extra ? ' — ' + extra : ''}`)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* 临时家目录:必须在 import 插件之前设好(dshHome() 每次调用都读环境变量) */
|
|
33
|
+
const HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'npk-companion-'))
|
|
34
|
+
process.env.DSH_HOME = HOME
|
|
35
|
+
|
|
36
|
+
const loadInternals = async () => (await import('../lib/index.js?t=' + Date.now())).__internals
|
|
37
|
+
const {
|
|
38
|
+
detectCompanion,
|
|
39
|
+
parseCompanionConfig,
|
|
40
|
+
companionPublic,
|
|
41
|
+
companionTokenMatch,
|
|
42
|
+
probe,
|
|
43
|
+
normalizeConfig,
|
|
44
|
+
COMPANION_DEFAULTS,
|
|
45
|
+
// dsh-qq-bridge 那一套
|
|
46
|
+
detectBridge,
|
|
47
|
+
bridgePublic,
|
|
48
|
+
bridgeCheck,
|
|
49
|
+
readBridgeConfig,
|
|
50
|
+
urlPort,
|
|
51
|
+
BRIDGE_DEFAULTS,
|
|
52
|
+
BRIDGE_PACKAGE,
|
|
53
|
+
profileHits,
|
|
54
|
+
} = await loadInternals()
|
|
55
|
+
|
|
56
|
+
const mkProfile = (name, { deps = {}, bundles = null, patch = '' } = {}) => {
|
|
57
|
+
const dir = path.join(HOME, 'profiles', name)
|
|
58
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
59
|
+
const pkg = { name: 'dsh-profile-' + name, dependencies: deps }
|
|
60
|
+
if (bundles) pkg.dsh = { profile: { bundles } }
|
|
61
|
+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(pkg, null, 2))
|
|
62
|
+
if (patch !== null) fs.writeFileSync(path.join(dir, 'cordis.patch.yml'), patch)
|
|
63
|
+
return dir
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const dropPkg = (profile, pkgName, version) => {
|
|
67
|
+
const dir = path.join(HOME, 'profiles', profile, 'node_modules', pkgName)
|
|
68
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
69
|
+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: pkgName, version }))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const dropPackageJson = (profile, version = '0.9.6') => dropPkg(profile, 'dsh-onebot-qq', version)
|
|
73
|
+
|
|
74
|
+
/** 起一个只收 WebSocket 的假服务器(普通 HTTP 请求换 426),模拟 dsh-onebot-qq 的反向 WS */
|
|
75
|
+
const serveUpgradeOnly = () =>
|
|
76
|
+
new Promise((resolve) => {
|
|
77
|
+
const srv = http.createServer((req, res) => {
|
|
78
|
+
res.writeHead(426, { 'content-type': 'text/plain' })
|
|
79
|
+
res.end('Upgrade Required')
|
|
80
|
+
})
|
|
81
|
+
srv.listen(0, '127.0.0.1', () =>
|
|
82
|
+
resolve({
|
|
83
|
+
port: srv.address().port,
|
|
84
|
+
close: () => new Promise((r) => srv.close(r)),
|
|
85
|
+
}),
|
|
86
|
+
)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
/** 找个没人用的端口,用来模拟「用户填的那个端口是空的」 */
|
|
90
|
+
const deadPort = () =>
|
|
91
|
+
new Promise((resolve) => {
|
|
92
|
+
const srv = net.createServer()
|
|
93
|
+
srv.listen(0, '127.0.0.1', () => {
|
|
94
|
+
const port = srv.address().port
|
|
95
|
+
srv.close(() => resolve(port))
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
/* 1. 对面的配置长什么样 ------------------------------------------------ */
|
|
100
|
+
console.log('1) 解析 dsh-onebot-qq 的 config 块')
|
|
101
|
+
{
|
|
102
|
+
const block = parseCompanionConfig(
|
|
103
|
+
[
|
|
104
|
+
'plugins:',
|
|
105
|
+
' - insert:',
|
|
106
|
+
' - id: dsh-onebot',
|
|
107
|
+
" name: 'dsh-onebot-qq'",
|
|
108
|
+
' config:',
|
|
109
|
+
' mode: reverse',
|
|
110
|
+
' host: 127.0.0.1',
|
|
111
|
+
' port: 9527',
|
|
112
|
+
" accessToken: 'secret-token'",
|
|
113
|
+
'',
|
|
114
|
+
].join('\n'),
|
|
115
|
+
)
|
|
116
|
+
ok(block && block.port === 9527, '缩进块里挖出 port(当数字)', JSON.stringify(block))
|
|
117
|
+
ok(block && block.mode === 'reverse', '挖出 mode')
|
|
118
|
+
ok(block && block.accessToken === 'secret-token', '挖出令牌(去掉引号)')
|
|
119
|
+
ok(block && block.host === '127.0.0.1', '挖出 host')
|
|
120
|
+
|
|
121
|
+
const inline = parseCompanionConfig(
|
|
122
|
+
" - id: dsh-onebot\n name: 'dsh-onebot-qq'\n config: { mode: reverse, port: 8888 }\n",
|
|
123
|
+
)
|
|
124
|
+
ok(inline && inline.port === 8888 && inline.mode === 'reverse', '行内 {…} 写法也认', JSON.stringify(inline))
|
|
125
|
+
|
|
126
|
+
const other = parseCompanionConfig(
|
|
127
|
+
[
|
|
128
|
+
'plugins:',
|
|
129
|
+
' - id: dsh-purge',
|
|
130
|
+
' config:',
|
|
131
|
+
' port: 1234',
|
|
132
|
+
' # - id: dsh-onebot',
|
|
133
|
+
' # config: { port: 7777 }',
|
|
134
|
+
'',
|
|
135
|
+
].join('\n'),
|
|
136
|
+
)
|
|
137
|
+
ok(other === null || other.port !== 1234, '别的插件 / 注释掉的行不算', JSON.stringify(other))
|
|
138
|
+
|
|
139
|
+
const js = parseCompanionConfig(' - id: dsh-onebot\n config:\n port: !!js process.env.PORT\n')
|
|
140
|
+
ok(!js || js.port === undefined, '!!js 表达式不当真(宁可当没配)', JSON.stringify(js))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* 2. 认不认得出「装了 / 挂了」 ------------------------------------------ */
|
|
144
|
+
console.log('\n2) detectCompanion:装没装、挂没挂')
|
|
145
|
+
{
|
|
146
|
+
// 2a. 装了 + 挂进 bundles + 没改过配置 → 用它的默认 8643
|
|
147
|
+
mkProfile('alpha', { deps: { 'dsh-onebot-qq': '^0.9.6' }, bundles: ['dsh-napcat-keeper', 'dsh-onebot-qq'] })
|
|
148
|
+
dropPackageJson('alpha')
|
|
149
|
+
let comp = detectCompanion({ force: true })
|
|
150
|
+
ok(comp.found === true, '认得出装了 dsh-onebot-qq', JSON.stringify(comp))
|
|
151
|
+
ok(comp.mounted === true, '在 bundles 里 → 判为已挂载')
|
|
152
|
+
ok(comp.port === 8643, '没配过就用它的默认端口 8643', String(comp.port))
|
|
153
|
+
ok(comp.mode === 'reverse', '它默认是反向模式', comp.mode)
|
|
154
|
+
ok(comp.packageVersion === '0.9.6', '读到它自己的版本号', String(comp.packageVersion))
|
|
155
|
+
ok(COMPANION_DEFAULTS.port === 8643, 'COMPANION_DEFAULTS.port 是 8643')
|
|
156
|
+
|
|
157
|
+
// 2b. 装了 + 只写 patch(不在 bundles)→ 仍算挂载,端口用 patch 里的
|
|
158
|
+
mkProfile('beta', {
|
|
159
|
+
deps: { 'dsh-onebot-qq': '^0.9.6' },
|
|
160
|
+
bundles: ['dsh-napcat-keeper'],
|
|
161
|
+
patch: [
|
|
162
|
+
'plugins:',
|
|
163
|
+
' - insert:',
|
|
164
|
+
' - id: dsh-onebot',
|
|
165
|
+
" name: 'dsh-onebot-qq'",
|
|
166
|
+
' config:',
|
|
167
|
+
' mode: reverse',
|
|
168
|
+
' port: 9123',
|
|
169
|
+
" accessToken: 'abc'",
|
|
170
|
+
'',
|
|
171
|
+
].join('\n'),
|
|
172
|
+
})
|
|
173
|
+
dropPackageJson('beta')
|
|
174
|
+
fs.rmSync(path.join(HOME, 'profiles', 'alpha'), { recursive: true, force: true })
|
|
175
|
+
comp = detectCompanion({ force: true })
|
|
176
|
+
ok(comp.mounted === true, 'patch 里出现 id: dsh-onebot → 判为已挂载')
|
|
177
|
+
ok(comp.port === 9123, '端口取 patch 里配的 9123', String(comp.port))
|
|
178
|
+
ok(comp.tokenKnown === true, '读得到令牌 → tokenKnown')
|
|
179
|
+
ok(companionPublic(comp).token === undefined, '对外的公开信息里不带令牌明文')
|
|
180
|
+
|
|
181
|
+
// 2c. 只在 dependencies 里、没进 bundles、patch 里也没有 → 没挂载
|
|
182
|
+
mkProfile('gamma', { deps: { 'dsh-onebot-qq': '^0.9.6' }, bundles: ['dsh-napcat-keeper'], patch: 'plugins: []\n' })
|
|
183
|
+
dropPackageJson('gamma')
|
|
184
|
+
fs.rmSync(path.join(HOME, 'profiles', 'beta'), { recursive: true, force: true })
|
|
185
|
+
comp = detectCompanion({ force: true })
|
|
186
|
+
ok(comp.found === true, '装是装了的', JSON.stringify(comp))
|
|
187
|
+
ok(comp.mounted === false, '没进 bundles 也没进 patch → 判为未挂载(端口不会开)')
|
|
188
|
+
|
|
189
|
+
// 2d. 压根没装
|
|
190
|
+
fs.rmSync(path.join(HOME, 'profiles', 'gamma'), { recursive: true, force: true })
|
|
191
|
+
mkProfile('delta', { deps: { 'dsh-napcat-keeper': '^2.8.0' }, bundles: ['dsh-napcat-keeper'], patch: 'plugins: []\n' })
|
|
192
|
+
comp = detectCompanion({ force: true })
|
|
193
|
+
ok(comp.found === false, '没装时 found = false', JSON.stringify(comp))
|
|
194
|
+
ok(comp.mounted === false, '没装自然也没挂载')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* 3. 令牌对不对得上 ---------------------------------------------------- */
|
|
198
|
+
console.log('\n3) 令牌一致性')
|
|
199
|
+
{
|
|
200
|
+
ok(companionTokenMatch({ accessToken: 'a' }, { tokenKnown: true, token: 'a' }) === true, '一样 → true')
|
|
201
|
+
ok(companionTokenMatch({ accessToken: 'a' }, { tokenKnown: true, token: 'b' }) === false, '不一样 → false')
|
|
202
|
+
ok(companionTokenMatch({ accessToken: 'a' }, { tokenKnown: false, token: '' }) === null, '对面读不到 → null(未知 ≠ 不一致)')
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* 4. 跟随行为 ---------------------------------------------------------- */
|
|
206
|
+
console.log('\n4) probe 会不会跟随对面的反向 WS 端口')
|
|
207
|
+
{
|
|
208
|
+
const srv = await serveUpgradeOnly()
|
|
209
|
+
const empty = await deadPort()
|
|
210
|
+
|
|
211
|
+
// 4a. 自己填的端口是空的,对面挂着反向 WS → 跟随
|
|
212
|
+
let health = await probe(
|
|
213
|
+
{ onebotPort: empty, accessToken: '', probeMode: 'auto', companionFollow: true },
|
|
214
|
+
{ companion: { found: true, mounted: true, mode: 'reverse', host: '127.0.0.1', port: srv.port, tokenKnown: false, token: '' } },
|
|
215
|
+
)
|
|
216
|
+
ok(health.followed === true, '自己端口没服务 → 跟随', JSON.stringify({ mode: health.mode, followed: health.followed }))
|
|
217
|
+
ok(health.portUsed === srv.port, 'portUsed 换成对面的端口', String(health.portUsed))
|
|
218
|
+
ok(health.mode === 'reverse', '跟随之后按反向 WS 判', health.mode)
|
|
219
|
+
ok(health.primaryPort === empty, '原来的端口仍然记着', String(health.primaryPort))
|
|
220
|
+
ok(String(health.hint || '').includes(String(srv.port)), '提示里写清跟到了哪个端口', health.hint)
|
|
221
|
+
|
|
222
|
+
// 4b. 自己填的端口就是对的 → 不许乱跟
|
|
223
|
+
health = await probe(
|
|
224
|
+
{ onebotPort: srv.port, accessToken: '', probeMode: 'auto', companionFollow: true },
|
|
225
|
+
{ companion: { found: true, mounted: true, mode: 'reverse', host: '127.0.0.1', port: srv.port, tokenKnown: false, token: '' } },
|
|
226
|
+
)
|
|
227
|
+
ok(health.followed === false, '自己端口通了→不跟随', String(health.followed))
|
|
228
|
+
ok(health.portUsed === srv.port, 'portUsed 还是自己的', String(health.portUsed))
|
|
229
|
+
|
|
230
|
+
// 4c. 用户把「跟随」关了 → 不跟
|
|
231
|
+
const off = await probe(
|
|
232
|
+
{ onebotPort: empty, accessToken: '', probeMode: 'auto', companionFollow: false },
|
|
233
|
+
{ companion: { found: true, mounted: true, mode: 'reverse', host: '127.0.0.1', port: srv.port, tokenKnown: false, token: '' } },
|
|
234
|
+
)
|
|
235
|
+
ok(off.followed === false && off.mode === 'dead', '关了跟随 → 老老实实报 dead', off.mode)
|
|
236
|
+
|
|
237
|
+
// 4d. 对面没挂载(端口根本不会开)→ 不跟
|
|
238
|
+
const unmounted = await probe(
|
|
239
|
+
{ onebotPort: empty, accessToken: '', probeMode: 'auto', companionFollow: true },
|
|
240
|
+
{ companion: { found: true, mounted: false, mode: 'reverse', host: '127.0.0.1', port: srv.port, tokenKnown: false, token: '' } },
|
|
241
|
+
)
|
|
242
|
+
ok(unmounted.followed === false, '对面没挂载 → 不跟随', String(unmounted.followed))
|
|
243
|
+
|
|
244
|
+
// 4e. 对面是正向模式(它自己去连 NapCat,不监听端口)→ 不跟
|
|
245
|
+
const forward = await probe(
|
|
246
|
+
{ onebotPort: empty, accessToken: '', probeMode: 'auto', companionFollow: true },
|
|
247
|
+
{ companion: { found: true, mounted: true, mode: 'forward', host: '127.0.0.1', port: srv.port, url: 'ws://127.0.0.1:3001', tokenKnown: false, token: '' } },
|
|
248
|
+
)
|
|
249
|
+
ok(forward.followed === false, '对面正向模式 → 不跟随', String(forward.followed))
|
|
250
|
+
|
|
251
|
+
// 4f. 令牌不一致要说出来
|
|
252
|
+
const mismatch = await probe(
|
|
253
|
+
{ onebotPort: empty, accessToken: 'mine', probeMode: 'auto', companionFollow: true },
|
|
254
|
+
{ companion: { found: true, mounted: true, mode: 'reverse', host: '127.0.0.1', port: srv.port, tokenKnown: true, token: 'theirs' } },
|
|
255
|
+
)
|
|
256
|
+
ok(mismatch.companionTokenMatch === false, '令牌对不上 → companionTokenMatch = false', String(mismatch.companionTokenMatch))
|
|
257
|
+
|
|
258
|
+
await srv.close()
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* 5. 配置项归一化 ------------------------------------------------------ */
|
|
262
|
+
console.log('\n5) companionFollow 归一化')
|
|
263
|
+
{
|
|
264
|
+
ok(normalizeConfig({}).companionFollow === true, '默认开', String(normalizeConfig({}).companionFollow))
|
|
265
|
+
ok(normalizeConfig({ companionFollow: false }).companionFollow === false, '关掉是 false')
|
|
266
|
+
ok(
|
|
267
|
+
normalizeConfig({ companionFollow: 'false' }).companionFollow === false,
|
|
268
|
+
"字符串 'false' → false(和 enabled 那几个布尔项一个路子)",
|
|
269
|
+
)
|
|
270
|
+
ok(normalizeConfig({ companionFollow: true }).companionFollow === true, '开着是 true')
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/* 5b. resolveConfig:显示了才有默认开 ---------------------------------- *
|
|
274
|
+
* 需求:设置页那一行只有「装了 dsh-onebot-qq 且真挂进 profile(= 启用)」时才出现、
|
|
275
|
+
* 且默认开;没装或没启用就不出现、值恒为关。
|
|
276
|
+
* resolveConfig 里带 detectCompanion(20 秒缓存),所以每个场景换一份新模块实例来测。
|
|
277
|
+
*/
|
|
278
|
+
console.log('\n5b) 跟随开关:装没装 / 启没启用 决定显不显示、默认开不开')
|
|
279
|
+
{
|
|
280
|
+
const cfgFile = path.join(HOME, 'dsh-napcat-keeper', 'config.json')
|
|
281
|
+
const freshInternals = async (tag) =>
|
|
282
|
+
(await import('../lib/index.js?t=' + tag + '-' + Date.now())).__internals
|
|
283
|
+
const cleanProfiles = () => {
|
|
284
|
+
for (const name of ['desktop', 'alpha', 'beta', 'gamma', 'delta', 'omega']) {
|
|
285
|
+
fs.rmSync(path.join(HOME, 'profiles', name), { recursive: true, force: true })
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const setStored = (obj) => {
|
|
289
|
+
fs.mkdirSync(path.dirname(cfgFile), { recursive: true })
|
|
290
|
+
fs.writeFileSync(cfgFile, JSON.stringify(obj || {}, null, 2) + '\n')
|
|
291
|
+
}
|
|
292
|
+
const withCompanionMounted = (mounted) => {
|
|
293
|
+
cleanProfiles()
|
|
294
|
+
mkProfile('desktop', {
|
|
295
|
+
deps: { 'dsh-onebot-qq': '^0.9.6' },
|
|
296
|
+
bundles: mounted ? ['dsh-onebot-qq'] : [],
|
|
297
|
+
patch: 'plugins: []\n',
|
|
298
|
+
})
|
|
299
|
+
dropPkg('desktop', 'dsh-onebot-qq', '0.9.6')
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// 5b-1 压根没装
|
|
303
|
+
cleanProfiles()
|
|
304
|
+
setStored({})
|
|
305
|
+
let m = await freshInternals('none')
|
|
306
|
+
ok(m.companionReady({ found: false, mounted: true }) === false, 'companionReady:没找到 → false')
|
|
307
|
+
ok(m.companionReady({ found: true, mounted: false }) === false, 'companionReady:装了但没挂 → false')
|
|
308
|
+
ok(m.companionReady({ found: true, mounted: true }) === true, 'companionReady:装了且挂了 → true')
|
|
309
|
+
let cfg = m.resolveConfig({})
|
|
310
|
+
ok(cfg.companionReady === false, '没装 → companionReady = false(界面不画这一行)')
|
|
311
|
+
ok(cfg.companionFollow === false, '没装 → 跟随强制关(DEFAULTS 里写着 true 也不作数)', String(cfg.companionFollow))
|
|
312
|
+
|
|
313
|
+
// 5b-2 装了、只在 dependencies 里(没挂进 profile)= 没启用
|
|
314
|
+
withCompanionMounted(false)
|
|
315
|
+
setStored({ companionFollow: true })
|
|
316
|
+
m = await freshInternals('dep-only')
|
|
317
|
+
cfg = m.resolveConfig({})
|
|
318
|
+
ok(cfg.companionReady === false, '装了但没挂进 profile → companionReady = false')
|
|
319
|
+
ok(cfg.companionFollow === false, '没启用时存过 true 也按关算(不跟一个起不来的端口)', String(cfg.companionFollow))
|
|
320
|
+
|
|
321
|
+
// 5b-3 装了且挂进 profile = 启用 → 默认开
|
|
322
|
+
withCompanionMounted(true)
|
|
323
|
+
setStored({})
|
|
324
|
+
m = await freshInternals('mounted')
|
|
325
|
+
cfg = m.resolveConfig({})
|
|
326
|
+
ok(cfg.companionReady === true, '装了且挂进 profile → companionReady = true(这一行出现)')
|
|
327
|
+
ok(cfg.companionFollow === true, '可用时没存过值 → 默认开', String(cfg.companionFollow))
|
|
328
|
+
|
|
329
|
+
// 5b-4 可用时用户显式关过 → 尊重存储值,不许硬掰成开
|
|
330
|
+
setStored({ companionFollow: false })
|
|
331
|
+
cfg = m.resolveConfig({})
|
|
332
|
+
ok(cfg.companionFollow === false, '可用时存了 false → 保持关')
|
|
333
|
+
|
|
334
|
+
// 5b-5 可用时 cordis 里显式关 → 也尊重
|
|
335
|
+
setStored({})
|
|
336
|
+
cfg = m.resolveConfig({ companionFollow: false })
|
|
337
|
+
ok(cfg.companionFollow === false, '可用时 cordis 里写 false → 也是关')
|
|
338
|
+
ok(m.resolveConfig({ companionFollow: true }).companionFollow === true, 'cordis 里写 true → 开')
|
|
339
|
+
|
|
340
|
+
// 5b-6 companionPublic 的瘦身版没有 found 字段,也得认(面板走的就是这条)
|
|
341
|
+
withCompanionMounted(true)
|
|
342
|
+
setStored({})
|
|
343
|
+
m = await freshInternals('public-shape')
|
|
344
|
+
ok(m.companionReady(m.companionPublic(m.detectCompanion({ force: true }))) === true,
|
|
345
|
+
'companionReady 也吃 companionPublic 那份(没有 found,靠 version/mounted 判)')
|
|
346
|
+
|
|
347
|
+
// 5b-7 面板每轮都按最新探测结果校准这一项:中途挂上 / 摘掉不用重启 DSH
|
|
348
|
+
setStored({ onebotPort: 65531 })
|
|
349
|
+
cfg = m.resolveConfig({})
|
|
350
|
+
let ui = await m.uiState(cfg, { light: true })
|
|
351
|
+
ok(ui.runtime.companionReady === true, 'uiState:mounted → runtime.companionReady = true(这一行出现)')
|
|
352
|
+
ok(ui.config.companionFollow === true, 'uiState:mounted 且没存过值 → 面板里是开')
|
|
353
|
+
|
|
354
|
+
withCompanionMounted(false)
|
|
355
|
+
setStored({ onebotPort: 65531 })
|
|
356
|
+
m = await freshInternals('ui-unmounted')
|
|
357
|
+
cfg = m.resolveConfig({})
|
|
358
|
+
ui = await m.uiState(cfg, { light: true })
|
|
359
|
+
ok(ui.runtime.companionReady === false, 'uiState:没挂进 profile → runtime.companionReady = false(这一行消失)')
|
|
360
|
+
ok(ui.config.companionFollow === false, 'uiState:没挂进 profile → 面板里是关')
|
|
361
|
+
|
|
362
|
+
// 清场:别把这份临时配置留给后面的组
|
|
363
|
+
cleanProfiles()
|
|
364
|
+
setStored({})
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/* 6. dsh-qq-bridge 也认得出 -------------------------------------------- */
|
|
368
|
+
console.log('\n6) dsh-qq-bridge:装没装、挂没挂、桥接指向哪')
|
|
369
|
+
{
|
|
370
|
+
// 清场:前面几组造的 profile 全删掉,这一组只看自己造的
|
|
371
|
+
for (const name of ['alpha', 'beta', 'gamma', 'delta', 'desktop']) {
|
|
372
|
+
fs.rmSync(path.join(HOME, 'profiles', name), { recursive: true, force: true })
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
ok(urlPort('http://127.0.0.1:3000') === 3000, 'urlPort 抠端口', String(urlPort('http://127.0.0.1:3000')))
|
|
376
|
+
ok(urlPort('https://napcat.example.com') === 443, '没写端口 + https → 443', String(urlPort('https://napcat.example.com')))
|
|
377
|
+
ok(urlPort('不是地址') === 0, '认不出 → 0(不瞎猜)', String(urlPort('不是地址')))
|
|
378
|
+
ok(BRIDGE_DEFAULTS.napcat === 'http://127.0.0.1:3000', '它的默认 NapCat 地址端口是 3000')
|
|
379
|
+
|
|
380
|
+
// 6a. 压根没装
|
|
381
|
+
fs.rmSync(path.join(HOME, 'qq-bridge-config.json'), { force: true })
|
|
382
|
+
let b = detectBridge({ force: true })
|
|
383
|
+
ok(b.found === false, '没装时 found = false', JSON.stringify(b))
|
|
384
|
+
ok(bridgePublic(b) === null, '没装时对外是 null(界面据此说「没检测到」)')
|
|
385
|
+
ok(bridgeCheck({ mode: 'http', portUsed: 3000 }, b).state === 'absent', '没装时交叉检查是 absent')
|
|
386
|
+
|
|
387
|
+
// 6b. 装了 + 在 bundles 里 + 配置文件齐全
|
|
388
|
+
mkProfile('desktop', {
|
|
389
|
+
deps: { 'dsh-qq-bridge': '^1.0.7' },
|
|
390
|
+
bundles: ['dsh-napcat-keeper', 'dsh-qq-bridge'],
|
|
391
|
+
patch: 'plugins: []\n',
|
|
392
|
+
})
|
|
393
|
+
dropPkg('desktop', BRIDGE_PACKAGE, '1.0.7')
|
|
394
|
+
fs.writeFileSync(
|
|
395
|
+
path.join(HOME, 'qq-bridge-config.json'),
|
|
396
|
+
JSON.stringify({ 总开关: true, 试运行: false, NapCat: 'http://127.0.0.1:4000', 令牌: 'x', 绑定: [{}, {}] }, null, 2),
|
|
397
|
+
)
|
|
398
|
+
b = detectBridge({ force: true })
|
|
399
|
+
ok(b.found === true && b.mounted === true, '装了且在 bundles 里 → 已挂载', JSON.stringify({ found: b.found, mounted: b.mounted }))
|
|
400
|
+
ok(b.packageVersion === '1.0.7', '读到它的版本号', String(b.packageVersion))
|
|
401
|
+
ok(readBridgeConfig().known === true, '读得到 ~/.dsh/qq-bridge-config.json(摊在根上,不在 profile 里)')
|
|
402
|
+
ok(b.napcatPort === 4000, '桥接指向 4000', String(b.napcatPort))
|
|
403
|
+
ok(b.enabled === true && b.dryRun === false, '总开关开、试运行关', JSON.stringify({ enabled: b.enabled, dryRun: b.dryRun }))
|
|
404
|
+
ok(b.tokenKnown === true && b.bindings === 2, '令牌配了、绑定 2 条', JSON.stringify({ tokenKnown: b.tokenKnown, bindings: b.bindings }))
|
|
405
|
+
ok(bridgePublic(b).token === undefined, '公开信息里不带令牌')
|
|
406
|
+
|
|
407
|
+
// 6c. 桥接指着反向 WS 端口:HTTP 客户端敲 WebSocket 端口,敲不通
|
|
408
|
+
let check = bridgeCheck({ mode: 'reverse', portUsed: 4000 }, b)
|
|
409
|
+
ok(check.state === 'bridge-hits-reverse', '桥接连的是反向 WS 端口 → 指出来', check.state)
|
|
410
|
+
ok(String(check.note).includes('4000'), '提示里带上端口号', check.note)
|
|
411
|
+
|
|
412
|
+
// 6d. 端口对不上
|
|
413
|
+
check = bridgeCheck({ mode: 'http', portUsed: 3000 }, b)
|
|
414
|
+
ok(check.state === 'port-mismatch', '两边端口不一样 → 指出来', check.state)
|
|
415
|
+
|
|
416
|
+
// 6e. 对得上 → 没意见
|
|
417
|
+
check = bridgeCheck({ mode: 'http', portUsed: 4000 }, b)
|
|
418
|
+
ok(check.state === 'ok' && check.note === '', '对得上 → ok 且不多嘴', check.state)
|
|
419
|
+
|
|
420
|
+
// 6f. 总开关关着
|
|
421
|
+
fs.writeFileSync(
|
|
422
|
+
path.join(HOME, 'qq-bridge-config.json'),
|
|
423
|
+
JSON.stringify({ 总开关: false, 试运行: true, NapCat: 'http://127.0.0.1:3000' }),
|
|
424
|
+
)
|
|
425
|
+
b = detectBridge({ force: true })
|
|
426
|
+
ok(b.enabled === false, '总开关关 → enabled = false')
|
|
427
|
+
ok(bridgeCheck({ mode: 'http', portUsed: 3000 }, b).state === 'disabled', '交叉检查说 disabled')
|
|
428
|
+
|
|
429
|
+
// 6g. 开着但试运行
|
|
430
|
+
fs.writeFileSync(
|
|
431
|
+
path.join(HOME, 'qq-bridge-config.json'),
|
|
432
|
+
JSON.stringify({ 总开关: true, 试运行: true, NapCat: 'http://127.0.0.1:3000' }),
|
|
433
|
+
)
|
|
434
|
+
b = detectBridge({ force: true })
|
|
435
|
+
ok(bridgeCheck({ mode: 'http', portUsed: 3000 }, b).state === 'dry-run', '试运行 → dry-run')
|
|
436
|
+
|
|
437
|
+
// 6h. 装了但没挂进 profile:先报没挂,别先去怪端口
|
|
438
|
+
mkProfile('beta', { deps: { 'dsh-qq-bridge': '^1.0.7' }, bundles: ['dsh-napcat-keeper'], patch: 'plugins: []\n' })
|
|
439
|
+
dropPkg('beta', BRIDGE_PACKAGE, '1.0.7')
|
|
440
|
+
fs.rmSync(path.join(HOME, 'profiles', 'desktop'), { recursive: true, force: true })
|
|
441
|
+
b = detectBridge({ force: true })
|
|
442
|
+
ok(b.mounted === false, '只写在 dependencies 里 → 未挂载')
|
|
443
|
+
ok(bridgeCheck({ mode: 'http', portUsed: 3000 }, b).state === 'not-mounted', '未挂载优先报 not-mounted')
|
|
444
|
+
|
|
445
|
+
// 6i. 同一套扫描对两个插件都成立
|
|
446
|
+
mkProfile('desktop', {
|
|
447
|
+
deps: { 'dsh-onebot-qq': '^0.9.6' },
|
|
448
|
+
bundles: ['dsh-napcat-keeper', 'dsh-onebot-qq'],
|
|
449
|
+
patch: 'plugins: []\n',
|
|
450
|
+
})
|
|
451
|
+
dropPkg('desktop', 'dsh-onebot-qq', '0.9.6')
|
|
452
|
+
const hits = profileHits('dsh-onebot-qq', 'dsh-onebot')
|
|
453
|
+
ok(hits.length === 1 && hits[0].mounted === true, 'profileHits 认得出 onebot', JSON.stringify(hits.map((h) => h.profile)))
|
|
454
|
+
ok(
|
|
455
|
+
profileHits(BRIDGE_PACKAGE, 'qq-bridge').some((h) => !h.mounted),
|
|
456
|
+
'同一套扫描对 bridge 也适用(含未挂载那条)',
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
// 6j. probe 会把桥接的交叉检查一起带出来
|
|
460
|
+
const srv2 = await serveUpgradeOnly()
|
|
461
|
+
const health = await probe(
|
|
462
|
+
{ onebotPort: srv2.port, accessToken: '', probeMode: 'auto', companionFollow: true },
|
|
463
|
+
{
|
|
464
|
+
companion: null,
|
|
465
|
+
bridge: {
|
|
466
|
+
...b,
|
|
467
|
+
found: true,
|
|
468
|
+
mounted: true,
|
|
469
|
+
napcatUrl: 'http://127.0.0.1:' + srv2.port,
|
|
470
|
+
napcatPort: srv2.port,
|
|
471
|
+
configKnown: true,
|
|
472
|
+
enabled: true,
|
|
473
|
+
dryRun: false,
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
)
|
|
477
|
+
ok(
|
|
478
|
+
health.bridge && health.bridge.check === 'bridge-hits-reverse',
|
|
479
|
+
'probe 返回里带上桥接交叉检查',
|
|
480
|
+
JSON.stringify(health.bridge && health.bridge.check),
|
|
481
|
+
)
|
|
482
|
+
ok(Boolean(health.bridge && health.bridge.note), '而且带一句人话提示')
|
|
483
|
+
await srv2.close()
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
fs.rmSync(HOME, { recursive: true, force: true })
|
|
487
|
+
console.log(`\n结果:${pass} 通过,${fail} 失败`)
|
|
488
|
+
process.exit(fail ? 1 : 0)
|