dsh-qiyu-stack 0.15.5 → 0.15.6
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/docs/status.md +16 -1
- package/package.json +1 -1
- package/src/doctor.mjs +26 -9
- package/src/dsh-port.mjs +83 -0
package/docs/status.md
CHANGED
|
@@ -6,10 +6,25 @@
|
|
|
6
6
|
|
|
7
7
|
**能用了,也发出去了,但还没被陌生人在干净机器上验证过。**
|
|
8
8
|
|
|
9
|
-
- npm:`dsh-qiyu-stack@0.15.
|
|
9
|
+
- npm:`dsh-qiyu-stack@0.15.6`(`latest`)
|
|
10
10
|
- GitHub:<https://github.com/aesfls/dsh-qiyu-stack>(公开,main)
|
|
11
11
|
- 千语(爸爸本人的实例):正常运行中,桥、NapCat、Docker 都通
|
|
12
12
|
|
|
13
|
+
## 2026-10-23 第五轮:DSH 端口自动探测(v0.15.6)
|
|
14
|
+
|
|
15
|
+
`qiyu.config.json` 里的 `dsh.port` 是某一次写下的快照,而 DSH 的端口会变
|
|
16
|
+
(这台机器就从 64657 变到了 62853)。doctor 只信这个值,就会一直报
|
|
17
|
+
「DSH 端口 64657 没人听」——不是故障,但会让人以为桥坏了。
|
|
18
|
+
|
|
19
|
+
0.15.6 新增 `src/dsh-port.mjs`:
|
|
20
|
+
|
|
21
|
+
1. 先试配置里的端口;不通再扫本机正在监听的端口(`netstat`)
|
|
22
|
+
2. 按特征认,不猜:`/dsh-qq-bridge/state` 返回 `{"ok":true,...}` → DSH + 桥;
|
|
23
|
+
`/` 返回 401 且带 `dsh web authentication required` → DSH 在、桥没装
|
|
24
|
+
3. **不写回配置**——端口下次还会变,写回去只会制造新的过期快照
|
|
25
|
+
|
|
26
|
+
现在 doctor 会自动找到实际端口,输出里注明「是自动探测到的(配置里写的是 64657)」。
|
|
27
|
+
|
|
13
28
|
## 2026-10-23 第四轮:验收命令也补上沙箱降级(v0.15.5)
|
|
14
29
|
|
|
15
30
|
第三轮只修了 `preflight`,**验收命令 `doctor` 漏了**——在 workspace-write 下它照样报
|
package/package.json
CHANGED
package/src/doctor.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { join, dirname, resolve } from 'node:path'
|
|
|
10
10
|
import { fileURLToPath } from 'node:url'
|
|
11
11
|
import { 读配置 } from './config.mjs'
|
|
12
12
|
import { 跑, 沙箱受限过 } from './run.mjs'
|
|
13
|
+
import { 探测DSH } from './dsh-port.mjs'
|
|
13
14
|
|
|
14
15
|
const 包根 = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
15
16
|
|
|
@@ -179,26 +180,42 @@ export async function doctor({ 只输出JSON = false, 跳过WebSocket = false }
|
|
|
179
180
|
}
|
|
180
181
|
|
|
181
182
|
// ---------- 7. DSH 桥 ----------
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
//
|
|
184
|
+
// DSH 的端口每次启动可能变,不能只信 qiyu.config.json 里的快照。
|
|
185
|
+
// 先试配置端口,再扫本机监听端口,按特征认(见 src/dsh-port.mjs)。
|
|
186
|
+
const 探测 = await 探测DSH({ 已知端口: 配置.dsh.port })
|
|
187
|
+
if (!探测) {
|
|
188
|
+
if (配置.dsh.port) {
|
|
189
|
+
记(
|
|
190
|
+
'warn',
|
|
191
|
+
`DSH 端口 ${配置.dsh.port} 没人听,也没自动找到`,
|
|
192
|
+
'DSH 可能没开,或者端口变了',
|
|
193
|
+
'先把 DSH 开起来再跑一次;开了还找不到,设环境变量 QIYU_DSH_PORT'
|
|
194
|
+
)
|
|
195
|
+
} else {
|
|
196
|
+
记('skip', '跳过 DSH 桥检查(没找到运行中的 DSH)', 'DSH 的端口每台机器都不一样,所以默认不猜', '把 DSH 开起来再跑一次;或者设 QIYU_DSH_PORT')
|
|
197
|
+
}
|
|
198
|
+
} else if (!探测.有桥) {
|
|
199
|
+
记('warn', `DSH 在 ${探测.端口},但没看到 dsh-qq-bridge`, '', '确认 DSH 里装了 dsh-qq-bridge 插件')
|
|
200
|
+
} else {
|
|
186
201
|
try {
|
|
187
|
-
const r = await 请求(`http://127.0.0.1:${
|
|
202
|
+
const r = await 请求(`http://127.0.0.1:${探测.端口}/dsh-qq-bridge/state`)
|
|
188
203
|
if (r.json && r.json.NapCat状态) {
|
|
189
204
|
const 状 = r.json.NapCat状态
|
|
190
|
-
|
|
205
|
+
const 注 = 探测.来源 === '自动探测' ? `DSH 端口 ${探测.端口} 是自动探测到的(配置里写的是 ${配置.dsh.port || '空'})` : ''
|
|
206
|
+
if (状.用户) 记('ok', `DSH 桥认到:${状.昵称}(${状.用户})`, 注)
|
|
191
207
|
else 记('warn', 'DSH 桥连不上 NapCat', JSON.stringify(状).slice(0, 160), '桥的 NapCat 地址/令牌和 qiyu.config.json 要一致')
|
|
192
208
|
} else {
|
|
193
|
-
记('warn', `DSH 在 ${
|
|
209
|
+
记('warn', `DSH 在 ${探测.端口},但没看到 dsh-qq-bridge`, '', '确认 DSH 里装了 dsh-qq-bridge 插件')
|
|
194
210
|
}
|
|
195
211
|
} catch (错) {
|
|
196
212
|
记('warn', 'DSH 桥查不到', String(错.message || 错), '')
|
|
197
213
|
}
|
|
198
|
-
} else {
|
|
199
|
-
记('warn', `DSH 端口 ${桥端口} 没人听`, '', 'DSH 没开?或者端口不是这个:设 QIYU_DSH_PORT')
|
|
200
214
|
}
|
|
201
215
|
|
|
216
|
+
// 给 JSON 输出用:探测到的端口优先,没探测到就退回配置里的值
|
|
217
|
+
const 桥端口 = 探测 ? 探测.端口 : Number(配置.dsh.port) || 0
|
|
218
|
+
|
|
202
219
|
// ---------- 7.5 部署路径体检 ----------
|
|
203
220
|
if (配置.napcat && 配置.napcat.dataDir) {
|
|
204
221
|
const 目录 = String(配置.napcat.dataDir)
|
package/src/dsh-port.mjs
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// 找 DSH 正在听的端口。
|
|
2
|
+
//
|
|
3
|
+
// 为什么需要这个:DSH 的端口不是固定的(每次启动可能换一个),
|
|
4
|
+
// 而 qiyu.config.json 里的 dsh.port 是某一次写下来的快照 —— 过一天就旧了。
|
|
5
|
+
// 只信配置会让 doctor 一直报「DSH 端口没人听」,人就会以为桥坏了。
|
|
6
|
+
//
|
|
7
|
+
// 认法(按特征,不猜):
|
|
8
|
+
// · GET /dsh-qq-bridge/state → 200 且 {"ok":true,...} → DSH + 桥都在
|
|
9
|
+
// · GET / → 401 "dsh web authentication required" → DSH 在,但桥没装
|
|
10
|
+
import { 跑 } from './run.mjs'
|
|
11
|
+
|
|
12
|
+
const 探测超时 = 350
|
|
13
|
+
|
|
14
|
+
async function 取(端口, 路径) {
|
|
15
|
+
try {
|
|
16
|
+
const r = await fetch(`http://127.0.0.1:${端口}${路径}`, { signal: AbortSignal.timeout(探测超时) })
|
|
17
|
+
return { 状态: r.status, 文本: await r.text() }
|
|
18
|
+
} catch {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 这个端口是不是 DSH?返回 { 端口, 有桥 } 或 null */
|
|
24
|
+
async function 认一认(端口) {
|
|
25
|
+
const 桥 = await 取(端口, '/dsh-qq-bridge/state')
|
|
26
|
+
if (桥 && 桥.状态 === 200) {
|
|
27
|
+
try {
|
|
28
|
+
const j = JSON.parse(桥.文本)
|
|
29
|
+
if (j && j.ok === true) return { 端口, 有桥: true }
|
|
30
|
+
} catch {
|
|
31
|
+
// 不是 JSON,继续看根路径
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const 根 = await 取(端口, '/')
|
|
35
|
+
if (根 && 根.状态 === 401 && /dsh web authentication required/i.test(根.文本)) {
|
|
36
|
+
return { 端口, 有桥: false }
|
|
37
|
+
}
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 本机正在听的端口(只听回环/通配地址,跳过系统端口) */
|
|
42
|
+
function 听着的端口() {
|
|
43
|
+
const r = 跑('netstat', ['-ano', '-p', 'TCP'], 8000)
|
|
44
|
+
if (!r.ok) return []
|
|
45
|
+
const 端口们 = new Set()
|
|
46
|
+
for (const 行 of r.出.split(/\r?\n/)) {
|
|
47
|
+
const m = 行.match(/^\s*TCP\s+(\S+):(\d+)\s+\S+\s+LISTENING/i)
|
|
48
|
+
if (!m) continue
|
|
49
|
+
const 地址 = m[1]
|
|
50
|
+
const 端口 = Number(m[2])
|
|
51
|
+
if (!(地址 === '0.0.0.0' || 地址 === '127.0.0.1' || 地址 === '[::]' || 地址 === '[::1]')) continue
|
|
52
|
+
if (端口 < 1024) continue
|
|
53
|
+
端口们.add(端口)
|
|
54
|
+
}
|
|
55
|
+
return [...端口们].sort((a, b) => a - b)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 找 DSH。先试已知端口(配置里的),再扫本机监听端口。
|
|
60
|
+
* @param {{已知端口?: number, 并发?: number, 上限?: number}} 选项
|
|
61
|
+
* @returns {Promise<{端口:number, 有桥:boolean, 来源:'配置'|'自动探测'}|null>}
|
|
62
|
+
*/
|
|
63
|
+
export async function 探测DSH({ 已知端口 = 0, 并发 = 16, 上限 = 200 } = {}) {
|
|
64
|
+
const 已知 = Number(已知端口) || 0
|
|
65
|
+
if (已知) {
|
|
66
|
+
const 中 = await 认一认(已知)
|
|
67
|
+
if (中) return { ...中, 来源: '配置' }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// NapCat 的三个端口不用试;已知端口试过了也不重复
|
|
71
|
+
const 别试 = new Set([3000, 3001, 6099, 已知].filter(Boolean))
|
|
72
|
+
const 候选 = 听着的端口()
|
|
73
|
+
.filter((p) => !别试.has(p))
|
|
74
|
+
.slice(0, 上限)
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < 候选.length; i += 并发) {
|
|
77
|
+
const 批 = 候选.slice(i, i + 并发)
|
|
78
|
+
const 结果 = await Promise.all(批.map((p) => 认一认(p)))
|
|
79
|
+
const 中 = 结果.find(Boolean)
|
|
80
|
+
if (中) return { ...中, 来源: '自动探测' }
|
|
81
|
+
}
|
|
82
|
+
return null
|
|
83
|
+
}
|