@wenbin_wb/dsh-bridge 2.10.9 → 2.10.11

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.
@@ -0,0 +1,101 @@
1
+ // 无托管器场景下的 DSH 重启助手(由 BridgeService.restartDsh 派生为独立进程)。
2
+ //
3
+ // 为什么需要它:旧进程调用 process.exit(0) 后,端口不会立刻释放;若新进程马上启动,
4
+ // 很容易撞上 EADDRINUSE 然后无声退出(stdio 被 ignore 时连报错都看不到),
5
+ // 结果就是"点了重启,dsh 再也没起来"。本助手按顺序做三件事并把过程写进日志:
6
+ // 1. 等旧进程真正退出;
7
+ // 2. 等端口释放(能连上说明还被占着);
8
+ // 3. 才派生新的 dsh,并在最长 120s 内确认端口可连接;失败则记日志并以非 0 退出。
9
+ //
10
+ // 用法(由 lib/index.js 组装 payload):node restart-helper.mjs '<json>'
11
+ import { spawn } from 'node:child_process';
12
+ import { appendFileSync, openSync } from 'node:fs';
13
+ import { connect } from 'node:net';
14
+
15
+ const payload = JSON.parse(process.argv[2] ?? '{}');
16
+ const {
17
+ pid, port, argv, cwd, execPath, logFile, childLog,
18
+ // 超时可注入(单测用),生产用默认值
19
+ waitTimeoutMs = 30_000,
20
+ readyTimeoutMs = 120_000,
21
+ } = payload;
22
+ const startedAt = Date.now();
23
+
24
+ function log(message) {
25
+ try { appendFileSync(logFile, `[${new Date().toISOString()}] [helper] ${message}\n`); } catch { /* 日志不可写时忽略 */ }
26
+ }
27
+
28
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
29
+
30
+ function processAlive(targetPid) {
31
+ try {
32
+ process.kill(targetPid, 0);
33
+ return true;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ /** 端口上是否有人在监听(能建立连接即视为被占用) */
40
+ async function portAccepts(portNumber) {
41
+ return await new Promise((resolve) => {
42
+ let settled = false;
43
+ const done = (value) => { if (!settled) { settled = true; resolve(value); } };
44
+ let socket;
45
+ try {
46
+ socket = connect({ host: '127.0.0.1', port: portNumber });
47
+ } catch {
48
+ done(false);
49
+ return;
50
+ }
51
+ socket.setTimeout(1200);
52
+ socket.once('connect', () => { done(true); socket.destroy(); });
53
+ socket.once('timeout', () => { done(false); socket.destroy(); });
54
+ socket.once('error', () => { done(false); socket.destroy(); });
55
+ });
56
+ }
57
+
58
+ async function waitUntil(check, timeoutMs, label) {
59
+ const deadline = Date.now() + timeoutMs;
60
+ for (;;) {
61
+ if (await check()) return true;
62
+ if (Date.now() >= deadline) {
63
+ log(`等待超时:${label}(${timeoutMs}ms)`);
64
+ return false;
65
+ }
66
+ await sleep(400);
67
+ }
68
+ }
69
+
70
+ async function main() {
71
+ log(`启动:oldPid=${pid} port=${port} cwd=${cwd}`);
72
+
73
+ if (!(await waitUntil(() => !processAlive(pid), waitTimeoutMs, `旧进程 ${pid} 退出`))) {
74
+ log('旧进程仍在运行,放弃本次重启(不做任何破坏性动作)');
75
+ process.exit(2);
76
+ }
77
+ if (!(await waitUntil(async () => !(await portAccepts(port)), waitTimeoutMs, `端口 ${port} 释放`))) {
78
+ log(`端口 ${port} 仍被占用,放弃本次重启`);
79
+ process.exit(3);
80
+ }
81
+
82
+ let stdio = 'ignore';
83
+ try { stdio = openSync(childLog, 'a'); } catch { /* 打不开就用 ignore */ }
84
+ let child;
85
+ try {
86
+ child = spawn(execPath, argv, { cwd, env: process.env, detached: true, stdio: ['ignore', stdio, stdio] });
87
+ } catch (err) {
88
+ log(`派生新 dsh 失败:${err?.message ?? err}`);
89
+ process.exit(4);
90
+ }
91
+ log(`已派生新 dsh:pid=${child.pid ?? '(未知)'}(输出见 ${childLog})`);
92
+ child.unref();
93
+
94
+ const up = await waitUntil(() => portAccepts(port), readyTimeoutMs, `新 dsh 监听 ${port}`);
95
+ log(up
96
+ ? `✅ 新 dsh 已就绪,耗时 ${Math.round((Date.now() - startedAt) / 1000)}s`
97
+ : `❌ 新 dsh 未能在 ${Math.round(readyTimeoutMs / 1000)}s 内就绪,请查看 ${childLog}`);
98
+ process.exit(up ? 0 : 1);
99
+ }
100
+
101
+ void main();
@@ -1,216 +1,225 @@
1
- // dsh-bridge Telegram platform adapter
2
- // 编排 TelegramGateway(长轮询 + 代理支持)+ TelegramConversationNode(Telegram⇄DSH 会话桥)。
3
- // 作为 Platform 子类,注册进 PlatformManager 统一管理。
4
-
5
- import QRCode from 'qrcode'
6
- import { Platform } from '../platform/base.js'
7
- import { TelegramGateway } from './gateway.js'
8
- import { TelegramConversationNode } from './node.js'
9
-
10
- export class TelegramService extends Platform {
11
- /**
12
- * @param {object} opts
13
- * @param {object} opts.ctx Cordis 上下文
14
- * @param {object} opts.logger 日志器
15
- * @param {object} [opts.config] 已持久化的 telegram 配置(凭证 + allowFrom + 间隔)
16
- * @param {(patch: object) => (void|Promise<void>)} opts.onPersist 主插件保存回调
17
- */
18
- constructor({ ctx, logger, config = {}, onPersist }) {
19
- super({ ctx, logger, config, onPersist })
20
- this.id = 'telegram'
21
- this.name = 'Telegram'
22
- this._botQrCache = { username: '', qr: '' }
23
-
24
- this.gateway = new TelegramGateway(ctx, {
25
- botToken: config.botToken ?? '',
26
- proxy: config.proxy ?? '',
27
- })
28
-
29
- // 挂到 ctx 供会话节点读取
30
- try { ctx.telegram = this.gateway } catch {}
31
-
32
- this.node = new TelegramConversationNode(ctx, {
33
- allowFrom: Array.isArray(config.allowFrom) ? config.allowFrom : [],
34
- groupAutoApprove: config.groupAutoApprove,
35
- digestIntervalSec: config.digestIntervalSec,
36
- approvalTimeoutSec: config.approvalTimeoutSec,
37
- maxMessageChars: config.maxMessageChars || 4096,
38
- sendChunkDelayMs: config.sendChunkDelayMs,
39
- activeSessionId: config.activeSessionId,
40
- }, logger, {
41
- onFirstSender: () => this.persist({ allowFrom: [...(this.node?.config?.allowFrom ?? [])] }),
42
- onActiveSessionChange: (sessionId) => this.persist({ activeSessionId: sessionId }),
43
- })
44
- this.bridge = this.node
45
-
46
- if (this.gateway.configured) {
47
- void this.start().catch((err) => {
48
- this.logger.error?.('[dsh-bridge telegram] start failed:', err?.message ?? err)
49
- })
50
- }
51
- }
52
-
53
- // ---- Platform 接口 ----
54
-
55
- get configured() { return this.gateway.configured }
56
- get accountId() { return this.gateway.accountId }
57
-
58
- get capabilities() {
59
- return {
60
- group: true,
61
- media: true,
62
- approvals: true,
63
- maxMessageChars: this.node.config.maxMessageChars || 4096,
64
- }
65
- }
66
-
67
- async sendText(peerId, text, opts = {}) {
68
- return this.gateway.sendText(peerId, text, opts)
69
- }
70
-
71
- async sendTyping(peerId, opts = {}) {
72
- return this.gateway.sendTyping?.(peerId, opts)
73
- }
74
-
75
- // ---- 生命周期控制 ----
76
-
77
- async start() {
78
- if (!this.gateway.configured) {
79
- this.setStatus('idle')
80
- return { success: false, error: 'Telegram Bot Token 未配置' }
81
- }
82
- this.setStatus('starting')
83
- const ok = await this.gateway.start()
84
- if (ok) {
85
- this.setStatus('connected')
86
- return { success: true }
87
- } else {
88
- this.setStatus('error', '连接 Telegram API 失败,请检查 Bot Token 或网络代理')
89
- return { success: false, error: '连接失败' }
90
- }
91
- }
92
-
93
- async stop() {
94
- await this.gateway.stop()
95
- this.setStatus('offline')
96
- return { success: true }
97
- }
98
-
99
- /**
100
- * 配置或登录
101
- * @param {object} creds - { botToken, proxy }
102
- */
103
- async login(creds = {}) {
104
- const patch = {}
105
- if (creds.botToken !== undefined) patch.botToken = String(creds.botToken).trim()
106
- if (creds.proxy !== undefined) patch.proxy = String(creds.proxy).trim()
107
-
108
- this.gateway.setCredentials(patch)
109
- this.persist(patch)
110
-
111
- if (!this.gateway.configured) {
112
- await this.stop()
113
- return { success: false, error: '请填写完整的 Telegram Bot Token' }
114
- }
115
-
116
- return this.start()
117
- }
118
-
119
- async unbind() {
120
- await this.stop()
121
- this.gateway.setCredentials({ botToken: '', proxy: '' })
122
- this.persist({ botToken: '', proxy: '', allowFrom: [] })
123
- if (this.node) this.node.config.allowFrom = []
124
- return { success: true }
125
- }
126
-
127
- getStatus() {
128
- const allowFrom = [...(this.node?.config?.allowFrom ?? [])]
129
- const username = this.gateway.botInfo?.username || ''
130
- const botLink = username ? `https://t.me/${encodeURIComponent(username)}` : null
131
- if (botLink && this._botQrCache.username !== username) {
132
- this._botQrCache.username = username
133
- void QRCode.toDataURL(botLink, {
134
- width: 260,
135
- margin: 2,
136
- color: { dark: '#1F2421', light: '#FFFFFF' },
137
- }).then((qr) => {
138
- this._botQrCache.qr = qr
139
- }).catch(() => {})
140
- }
141
-
142
- return {
143
- id: this.id,
144
- name: this.name,
145
- status: this.status === 'connected' ? 'connected' : this.gateway.status,
146
- configured: this.gateway.configured,
147
- accountId: this.gateway.accountId || (this.gateway.configured ? '已配置 Token' : ''),
148
- allowFrom,
149
- peerId: this.node?.peerId,
150
- sessionId: this.node?.activeSessionId,
151
- login: {
152
- phase: this.status === 'connected' ? 'done' : this.status === 'error' ? 'error' : 'idle',
153
- },
154
- capabilities: { ...this.capabilities },
155
- config: {
156
- digestIntervalSec: this.node?.config?.digestIntervalSec,
157
- approvalTimeoutSec: this.node?.config?.approvalTimeoutSec,
158
- maxMessageChars: this.node?.config?.maxMessageChars,
159
- sendChunkDelayMs: this.node?.config?.sendChunkDelayMs,
160
- botToken: this.gateway.config.botToken ? '******' : '',
161
- proxy: this.gateway.config.proxy || '',
162
- },
163
- botInfo: this.gateway.botInfo,
164
- botLink,
165
- botQr: this._botQrCache.qr,
166
- }
167
- }
168
-
169
- setAllowFrom(allowFrom) {
170
- const list = Array.isArray(allowFrom) ? allowFrom.map((s) => String(s).trim()).filter(Boolean) : []
171
- if (this.node) this.node.config.allowFrom = list
172
- this.persist({ allowFrom: list })
173
- return { success: true, allowFrom: list }
174
- }
175
-
176
- async setConfig({ digestIntervalSec, approvalTimeoutSec, maxMessageChars, sendChunkDelayMs, groupAutoApprove, botToken, proxy } = {}) {
177
- if (digestIntervalSec != null) this.node.config.digestIntervalSec = Number(digestIntervalSec)
178
- if (approvalTimeoutSec != null) this.node.config.approvalTimeoutSec = Number(approvalTimeoutSec)
179
- if (maxMessageChars != null) {
180
- const val = Number(maxMessageChars)
181
- this.node.config.maxMessageChars = (val >= 200) ? val : 4096
182
- }
183
- if (sendChunkDelayMs != null) this.node.config.sendChunkDelayMs = Number(sendChunkDelayMs)
184
- if (groupAutoApprove != null) this.node.config.groupAutoApprove = groupAutoApprove === true
185
-
186
- if (botToken !== undefined || proxy !== undefined) {
187
- const patch = {}
188
- if (botToken !== undefined) patch.botToken = botToken.trim()
189
- if (proxy !== undefined) patch.proxy = proxy.trim()
190
- this.gateway.setCredentials(patch)
191
- }
192
-
193
- const patch = {
194
- digestIntervalSec: this.node.config.digestIntervalSec,
195
- approvalTimeoutSec: this.node.config.approvalTimeoutSec,
196
- maxMessageChars: this.node.config.maxMessageChars,
197
- sendChunkDelayMs: this.node.config.sendChunkDelayMs,
198
- groupAutoApprove: this.node.config.groupAutoApprove === true,
199
- }
200
- if (botToken !== undefined) patch.botToken = this.gateway.config.botToken
201
- if (proxy !== undefined) patch.proxy = this.gateway.config.proxy
202
- await this.persist(patch)
203
-
204
- if (this.gateway.configured && (botToken !== undefined || proxy !== undefined)) {
205
- await this.start().catch((err) => {
206
- this.logger?.warn?.('[dsh-bridge telegram] auto-start failed: %s', err?.message ?? err)
207
- })
208
- }
209
- return { success: true }
210
- }
211
-
212
- dispose() {
213
- super.dispose()
214
- this.gateway?.dispose?.()
215
- }
216
- }
1
+ // dsh-bridge Telegram platform adapter
2
+ // 编排 TelegramGateway(长轮询 + 代理支持)+ TelegramConversationNode(Telegram⇄DSH 会话桥)。
3
+ // 作为 Platform 子类,注册进 PlatformManager 统一管理。
4
+
5
+ import QRCode from 'qrcode'
6
+ import { Platform } from '../platform/base.js'
7
+ import { applySessionConfig, readSessionConfig } from '../platform/session-config.js'
8
+ import { TelegramGateway } from './gateway.js'
9
+ import { TelegramConversationNode } from './node.js'
10
+
11
+ export class TelegramService extends Platform {
12
+ /**
13
+ * @param {object} opts
14
+ * @param {object} opts.ctx Cordis 上下文
15
+ * @param {object} opts.logger 日志器
16
+ * @param {object} [opts.config] 已持久化的 telegram 配置(凭证 + allowFrom + 间隔)
17
+ * @param {(patch: object) => (void|Promise<void>)} opts.onPersist 主插件保存回调
18
+ */
19
+ constructor({ ctx, logger, config = {}, onPersist }) {
20
+ super({ ctx, logger, config, onPersist })
21
+ this.id = 'telegram'
22
+ this.name = 'Telegram'
23
+ this._botQrCache = { username: '', qr: '' }
24
+
25
+ this.gateway = new TelegramGateway(ctx, {
26
+ botToken: config.botToken ?? '',
27
+ proxy: config.proxy ?? '',
28
+ })
29
+
30
+ // 挂到 ctx 供会话节点读取
31
+ try { ctx.telegram = this.gateway } catch {}
32
+
33
+ this.node = new TelegramConversationNode(ctx, {
34
+ allowFrom: Array.isArray(config.allowFrom) ? config.allowFrom : [],
35
+ groupAutoApprove: config.groupAutoApprove,
36
+ digestIntervalSec: config.digestIntervalSec,
37
+ approvalTimeoutSec: config.approvalTimeoutSec,
38
+ maxMessageChars: config.maxMessageChars || 4096,
39
+ sendChunkDelayMs: config.sendChunkDelayMs,
40
+ activeSessionId: config.activeSessionId,
41
+ // 会话级配置:工作区 / Agent 预设 / 模型路由(构造期来自 cordis 配置,运行期由设置页写入)
42
+ cwd: config.cwd,
43
+ agentPreset: config.agentPreset,
44
+ agentProvider: config.agentProvider,
45
+ agentModel: config.agentModel,
46
+ }, logger, {
47
+ onFirstSender: () => this.persist({ allowFrom: [...(this.node?.config?.allowFrom ?? [])] }),
48
+ onActiveSessionChange: (sessionId) => this.persist({ activeSessionId: sessionId }),
49
+ })
50
+ this.bridge = this.node
51
+
52
+ if (this.gateway.configured) {
53
+ void this.start().catch((err) => {
54
+ this.logger.error?.('[dsh-bridge telegram] start failed:', err?.message ?? err)
55
+ })
56
+ }
57
+ }
58
+
59
+ // ---- Platform 接口 ----
60
+
61
+ get configured() { return this.gateway.configured }
62
+ get accountId() { return this.gateway.accountId }
63
+
64
+ get capabilities() {
65
+ return {
66
+ group: true,
67
+ media: true,
68
+ approvals: true,
69
+ maxMessageChars: this.node.config.maxMessageChars || 4096,
70
+ }
71
+ }
72
+
73
+ async sendText(peerId, text, opts = {}) {
74
+ return this.gateway.sendText(peerId, text, opts)
75
+ }
76
+
77
+ async sendTyping(peerId, opts = {}) {
78
+ return this.gateway.sendTyping?.(peerId, opts)
79
+ }
80
+
81
+ // ---- 生命周期控制 ----
82
+
83
+ async start() {
84
+ if (!this.gateway.configured) {
85
+ this.setStatus('idle')
86
+ return { success: false, error: 'Telegram Bot Token 未配置' }
87
+ }
88
+ this.setStatus('starting')
89
+ const ok = await this.gateway.start()
90
+ if (ok) {
91
+ this.setStatus('connected')
92
+ return { success: true }
93
+ } else {
94
+ this.setStatus('error', '连接 Telegram API 失败,请检查 Bot Token 或网络代理')
95
+ return { success: false, error: '连接失败' }
96
+ }
97
+ }
98
+
99
+ async stop() {
100
+ await this.gateway.stop()
101
+ this.setStatus('offline')
102
+ return { success: true }
103
+ }
104
+
105
+ /**
106
+ * 配置或登录
107
+ * @param {object} creds - { botToken, proxy }
108
+ */
109
+ async login(creds = {}) {
110
+ const patch = {}
111
+ if (creds.botToken !== undefined) patch.botToken = String(creds.botToken).trim()
112
+ if (creds.proxy !== undefined) patch.proxy = String(creds.proxy).trim()
113
+
114
+ this.gateway.setCredentials(patch)
115
+ this.persist(patch)
116
+
117
+ if (!this.gateway.configured) {
118
+ await this.stop()
119
+ return { success: false, error: '请填写完整的 Telegram Bot Token' }
120
+ }
121
+
122
+ return this.start()
123
+ }
124
+
125
+ async unbind() {
126
+ await this.stop()
127
+ this.gateway.setCredentials({ botToken: '', proxy: '' })
128
+ this.persist({ botToken: '', proxy: '', allowFrom: [] })
129
+ if (this.node) this.node.config.allowFrom = []
130
+ return { success: true }
131
+ }
132
+
133
+ getStatus() {
134
+ const allowFrom = [...(this.node?.config?.allowFrom ?? [])]
135
+ const username = this.gateway.botInfo?.username || ''
136
+ const botLink = username ? `https://t.me/${encodeURIComponent(username)}` : null
137
+ if (botLink && this._botQrCache.username !== username) {
138
+ this._botQrCache.username = username
139
+ void QRCode.toDataURL(botLink, {
140
+ width: 260,
141
+ margin: 2,
142
+ color: { dark: '#1F2421', light: '#FFFFFF' },
143
+ }).then((qr) => {
144
+ this._botQrCache.qr = qr
145
+ }).catch(() => {})
146
+ }
147
+
148
+ return {
149
+ id: this.id,
150
+ name: this.name,
151
+ status: this.status === 'connected' ? 'connected' : this.gateway.status,
152
+ configured: this.gateway.configured,
153
+ accountId: this.gateway.accountId || (this.gateway.configured ? '已配置 Token' : ''),
154
+ allowFrom,
155
+ peerId: this.node?.peerId,
156
+ sessionId: this.node?.activeSessionId,
157
+ login: {
158
+ phase: this.status === 'connected' ? 'done' : this.status === 'error' ? 'error' : 'idle',
159
+ },
160
+ capabilities: { ...this.capabilities },
161
+ config: {
162
+ digestIntervalSec: this.node?.config?.digestIntervalSec,
163
+ approvalTimeoutSec: this.node?.config?.approvalTimeoutSec,
164
+ maxMessageChars: this.node?.config?.maxMessageChars,
165
+ sendChunkDelayMs: this.node?.config?.sendChunkDelayMs,
166
+ botToken: this.gateway.config.botToken ? '******' : '',
167
+ proxy: this.gateway.config.proxy || '',
168
+ ...readSessionConfig(this.node?.config),
169
+ },
170
+ botInfo: this.gateway.botInfo,
171
+ botLink,
172
+ botQr: this._botQrCache.qr,
173
+ }
174
+ }
175
+
176
+ setAllowFrom(allowFrom) {
177
+ const list = Array.isArray(allowFrom) ? allowFrom.map((s) => String(s).trim()).filter(Boolean) : []
178
+ if (this.node) this.node.config.allowFrom = list
179
+ this.persist({ allowFrom: list })
180
+ return { success: true, allowFrom: list }
181
+ }
182
+
183
+ async setConfig({ digestIntervalSec, approvalTimeoutSec, maxMessageChars, sendChunkDelayMs, groupAutoApprove, botToken, proxy, agentPreset, cwd, agentProvider, agentModel } = {}) {
184
+ if (digestIntervalSec != null) this.node.config.digestIntervalSec = Number(digestIntervalSec)
185
+ if (approvalTimeoutSec != null) this.node.config.approvalTimeoutSec = Number(approvalTimeoutSec)
186
+ if (maxMessageChars != null) {
187
+ const val = Number(maxMessageChars)
188
+ this.node.config.maxMessageChars = (val >= 200) ? val : 4096
189
+ }
190
+ if (sendChunkDelayMs != null) this.node.config.sendChunkDelayMs = Number(sendChunkDelayMs)
191
+ if (groupAutoApprove != null) this.node.config.groupAutoApprove = groupAutoApprove === true
192
+ applySessionConfig(this.node.config, { agentPreset, cwd, agentProvider, agentModel })
193
+
194
+ if (botToken !== undefined || proxy !== undefined) {
195
+ const patch = {}
196
+ if (botToken !== undefined) patch.botToken = botToken.trim()
197
+ if (proxy !== undefined) patch.proxy = proxy.trim()
198
+ this.gateway.setCredentials(patch)
199
+ }
200
+
201
+ const patch = {
202
+ digestIntervalSec: this.node.config.digestIntervalSec,
203
+ approvalTimeoutSec: this.node.config.approvalTimeoutSec,
204
+ maxMessageChars: this.node.config.maxMessageChars,
205
+ sendChunkDelayMs: this.node.config.sendChunkDelayMs,
206
+ groupAutoApprove: this.node.config.groupAutoApprove === true,
207
+ ...readSessionConfig(this.node.config),
208
+ }
209
+ if (botToken !== undefined) patch.botToken = this.gateway.config.botToken
210
+ if (proxy !== undefined) patch.proxy = this.gateway.config.proxy
211
+ await this.persist(patch)
212
+
213
+ if (this.gateway.configured && (botToken !== undefined || proxy !== undefined)) {
214
+ await this.start().catch((err) => {
215
+ this.logger?.warn?.('[dsh-bridge telegram] auto-start failed: %s', err?.message ?? err)
216
+ })
217
+ }
218
+ return { success: true }
219
+ }
220
+
221
+ dispose() {
222
+ super.dispose()
223
+ this.gateway?.dispose?.()
224
+ }
225
+ }