@wenbin_wb/dsh-bridge 2.8.7 → 2.9.0
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 +295 -250
- package/README.en.md +408 -572
- package/README.md +430 -570
- package/client/client.js +3844 -3764
- package/client/index.js +140 -876
- package/client/mobile-styles.js +802 -0
- package/docs/fix-plan-202608.md +108 -0
- package/lib/auth/login-template.js +381 -381
- package/lib/auth/manager.js +531 -469
- package/lib/bridge-rpc.js +436 -511
- package/lib/cloudflared-manager.mjs +361 -345
- package/lib/compat.js +129 -0
- package/lib/feishu/index.js +225 -222
- package/lib/feishu/node.js +433 -409
- package/lib/index.js +1765 -1804
- package/lib/platform/base.js +147 -156
- package/lib/platform/commands.js +221 -0
- package/lib/platform/conversation-bridge.js +816 -1570
- package/lib/platform/dsh-storage.js +117 -0
- package/lib/platform/index.js +10 -10
- package/lib/platform/message-split.js +191 -0
- package/lib/platform/session-catalog.js +372 -0
- package/lib/platform/stream-slices.js +21 -0
- package/lib/qq/index.js +312 -309
- package/lib/qq/node.js +532 -533
- package/lib/telegram/index.js +215 -212
- package/lib/telegram/node.js +348 -350
- package/lib/tunnel-client.mjs +39 -15
- package/lib/wechat/gateway.js +973 -960
- package/lib/wechat/index.js +244 -241
- package/lib/wechat/media.js +285 -281
- package/lib/wechat/node.js +352 -350
- package/package.json +106 -102
package/lib/telegram/node.js
CHANGED
|
@@ -1,351 +1,349 @@
|
|
|
1
|
-
// dsh-bridge Telegram conversation node
|
|
2
|
-
// 把 Telegram OpenAPI / 长轮询入站事件解析后交给平台无关的
|
|
3
|
-
// ConversationBridge 处理,出站通过 TelegramGateway 发送文本 / Inline Keyboard 按钮 / 媒体。
|
|
4
|
-
|
|
5
|
-
import fs from 'node:fs'
|
|
6
|
-
import path from 'node:path'
|
|
7
|
-
import { ConversationBridge, conversationBridgeHelpers } from '../platform/conversation-bridge.js'
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
},
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
this.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
this.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (event.type === 'turn/
|
|
80
|
-
this._inTurn =
|
|
81
|
-
this._streamMsgId
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
else if (rawMsg.
|
|
156
|
-
const res = await this.gateway.downloadFile(rawMsg.
|
|
157
|
-
if (res) downloaded.push(res)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
if (!
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
this.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
this.
|
|
344
|
-
this.
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
|
|
1
|
+
// dsh-bridge Telegram conversation node
|
|
2
|
+
// 把 Telegram OpenAPI / 长轮询入站事件解析后交给平台无关的
|
|
3
|
+
// ConversationBridge 处理,出站通过 TelegramGateway 发送文本 / Inline Keyboard 按钮 / 媒体。
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
import { ConversationBridge, conversationBridgeHelpers } from '../platform/conversation-bridge.js'
|
|
8
|
+
import { cumulativeSlices } from '../platform/stream-slices.js'
|
|
9
|
+
|
|
10
|
+
function sleep(ms) {
|
|
11
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function splitIntoIncremental(text, maxChunkSize = 200) {
|
|
15
|
+
if (!text) return []
|
|
16
|
+
const chunks = conversationBridgeHelpers.splitForIM(text, maxChunkSize)
|
|
17
|
+
if (chunks.length <= 1) {
|
|
18
|
+
if (text.length <= maxChunkSize) return [text]
|
|
19
|
+
const mid = Math.ceil(text.length / 2)
|
|
20
|
+
return [text.slice(0, mid), text]
|
|
21
|
+
}
|
|
22
|
+
return cumulativeSlices(chunks)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function makePlatform(gateway) {
|
|
26
|
+
return {
|
|
27
|
+
id: 'telegram',
|
|
28
|
+
name: 'Telegram',
|
|
29
|
+
get accountId() { return gateway.accountId ?? '' },
|
|
30
|
+
get capabilities() { return gateway.capabilities },
|
|
31
|
+
get status() { return gateway?.status ?? 'idle' },
|
|
32
|
+
async sendText(peerId, text, opts = {}) {
|
|
33
|
+
return gateway?.sendText(peerId, text, opts)
|
|
34
|
+
},
|
|
35
|
+
async sendMediaFile(peerId, filePath, opts = {}) {
|
|
36
|
+
return gateway?.sendMediaFile(peerId, filePath, opts)
|
|
37
|
+
},
|
|
38
|
+
async sendTyping(peerId) {
|
|
39
|
+
return gateway?.sendTyping(peerId)
|
|
40
|
+
},
|
|
41
|
+
dispose() {},
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class TelegramConversationNode extends ConversationBridge {
|
|
46
|
+
constructor(ctx, config = {}, logger = console, { onFirstSender, onActiveSessionChange } = {}) {
|
|
47
|
+
const gateway = ctx.telegram
|
|
48
|
+
super({
|
|
49
|
+
ctx,
|
|
50
|
+
logger,
|
|
51
|
+
config: {
|
|
52
|
+
maxMessageChars: 4096,
|
|
53
|
+
...config,
|
|
54
|
+
},
|
|
55
|
+
platform: makePlatform(gateway),
|
|
56
|
+
onFirstSender,
|
|
57
|
+
onActiveSessionChange,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
this.gateway = gateway
|
|
61
|
+
this._lastPeer = null
|
|
62
|
+
this._streamMsgId = null
|
|
63
|
+
this._streamContent = ''
|
|
64
|
+
this._inTurn = false
|
|
65
|
+
|
|
66
|
+
// 订阅网关入站消息事件
|
|
67
|
+
this.disposers.push(this.ctx.on('telegram/message', (event) => this._handleInbound(event)))
|
|
68
|
+
|
|
69
|
+
// 订阅 Inline 按钮点击交互事件(审批确认)
|
|
70
|
+
this.disposers.push(this.ctx.on('telegram/action', (event) => this._handleAction(event)))
|
|
71
|
+
|
|
72
|
+
// 监听轮次事件:turn/start 开启流式打字机,turn/end 最终刷新并重置
|
|
73
|
+
this.disposers.push(this.ctx.on('session/event', (session, event) => {
|
|
74
|
+
if (session.id !== this.activeSessionId) return
|
|
75
|
+
if (event.type === 'turn/start') {
|
|
76
|
+
this._inTurn = true
|
|
77
|
+
this._streamMsgId = null
|
|
78
|
+
this._streamContent = ''
|
|
79
|
+
} else if (event.type === 'turn/end') {
|
|
80
|
+
this._inTurn = false
|
|
81
|
+
if (this._streamMsgId && this._streamContent) {
|
|
82
|
+
const peerId = this._lastPeer?.chatId || this.peerId
|
|
83
|
+
if (peerId) {
|
|
84
|
+
void this.gateway?.editMessageText(peerId, this._streamMsgId, this._streamContent).catch(() => {})
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
this._streamMsgId = null
|
|
88
|
+
this._streamContent = ''
|
|
89
|
+
}
|
|
90
|
+
}))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async _handleInbound(event) {
|
|
94
|
+
if (this.gateway?._stopPolling) return
|
|
95
|
+
const { chatId, senderId, senderUsername, isGroup, text, messageId, raw } = event
|
|
96
|
+
this._lastPeer = { chatId, senderId, senderUsername, isGroup }
|
|
97
|
+
const authId = isGroup ? chatId : senderId
|
|
98
|
+
|
|
99
|
+
// 检查是否有富媒体附件(图片 / 文件 / 语音 / 音频)
|
|
100
|
+
let mediaFiles = []
|
|
101
|
+
if (raw) {
|
|
102
|
+
mediaFiles = await this._processInboundMedia(raw, this.config.cwd)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!text && mediaFiles.length === 0) {
|
|
106
|
+
this.logger.debug?.(`[dsh-bridge telegram] ignore empty message from ${chatId}`)
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let fullText = text || ''
|
|
111
|
+
if (mediaFiles.length > 0) {
|
|
112
|
+
const mediaDesc = mediaFiles.map((f) => `[文件: ${f.path}]`).join('\n')
|
|
113
|
+
fullText = fullText ? `${fullText}\n\n${mediaDesc}` : mediaDesc
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.logger.debug?.(`[dsh-bridge telegram] handling inbound message from ${chatId} (group=${isGroup}): ${fullText}`)
|
|
117
|
+
|
|
118
|
+
// 如果还没有活动会话,且是首条消息,自动尝试选择最新已有会话
|
|
119
|
+
if (!this.activeSessionId) {
|
|
120
|
+
await this._pickDefaultSession().catch(() => {})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 转交通用 ConversationBridge 路由
|
|
124
|
+
// outboundPeer:本轮出站事件流绑定回发起会话(chat 而非成员),替代此前被静默丢弃的 peerId 幽灵参数
|
|
125
|
+
return this.handleInbound({
|
|
126
|
+
senderId: authId,
|
|
127
|
+
isGroup,
|
|
128
|
+
text: fullText,
|
|
129
|
+
outboundPeer: { peerId: chatId, isGroup },
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async _processInboundMedia(rawMsg, sessionCwd) {
|
|
134
|
+
if (!this.gateway) return []
|
|
135
|
+
const downloaded = []
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
// 1. 照片(选择最高分辨率的一张)
|
|
139
|
+
if (Array.isArray(rawMsg.photo) && rawMsg.photo.length > 0) {
|
|
140
|
+
const bestPhoto = rawMsg.photo[rawMsg.photo.length - 1]
|
|
141
|
+
if (bestPhoto?.file_id) {
|
|
142
|
+
const res = await this.gateway.downloadFile(bestPhoto.file_id, sessionCwd)
|
|
143
|
+
if (res) downloaded.push(res)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// 2. 文档 / 文件
|
|
147
|
+
else if (rawMsg.document?.file_id) {
|
|
148
|
+
const res = await this.gateway.downloadFile(rawMsg.document.file_id, sessionCwd)
|
|
149
|
+
if (res) downloaded.push(res)
|
|
150
|
+
}
|
|
151
|
+
// 3. 语音 / 音频
|
|
152
|
+
else if (rawMsg.voice?.file_id) {
|
|
153
|
+
const res = await this.gateway.downloadFile(rawMsg.voice.file_id, sessionCwd)
|
|
154
|
+
if (res) downloaded.push(res)
|
|
155
|
+
} else if (rawMsg.audio?.file_id) {
|
|
156
|
+
const res = await this.gateway.downloadFile(rawMsg.audio.file_id, sessionCwd)
|
|
157
|
+
if (res) downloaded.push(res)
|
|
158
|
+
}
|
|
159
|
+
} catch (err) {
|
|
160
|
+
this.logger.warn?.(`[dsh-bridge telegram] process media error: ${err.message}`)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return downloaded
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async _handleAction(event) {
|
|
167
|
+
const { queryId, chatId, operatorId, data } = event
|
|
168
|
+
if (!data || !this.gateway) return
|
|
169
|
+
|
|
170
|
+
// 1. 审批决议按钮 (approve:ID / reject:ID)
|
|
171
|
+
const match = /^([a-zA-Z]+):(\d+)$/.exec(data)
|
|
172
|
+
if (match) {
|
|
173
|
+
const [, action, approvalIdStr] = match
|
|
174
|
+
const approvalId = Number(approvalIdStr)
|
|
175
|
+
const outcome = action === 'approve' ? 'allowed-once' : 'rejected'
|
|
176
|
+
const pending = this.pending.get(approvalId)
|
|
177
|
+
|
|
178
|
+
if (pending) {
|
|
179
|
+
this.clearApproval(approvalId)
|
|
180
|
+
pending.resolve(outcome)
|
|
181
|
+
await this.gateway?.answerCallbackQuery(
|
|
182
|
+
queryId,
|
|
183
|
+
action === 'approve' ? '✓ 操作已批准' : '✕ 操作已拒绝',
|
|
184
|
+
false,
|
|
185
|
+
)
|
|
186
|
+
} else {
|
|
187
|
+
await this.gateway?.answerCallbackQuery(queryId, '⚠️ 该审批已处理或已过期', false)
|
|
188
|
+
}
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 2. 快捷指令交互按键 (cmd:xxx)
|
|
193
|
+
if (data.startsWith('cmd:')) {
|
|
194
|
+
const cmdBody = data.slice(4)
|
|
195
|
+
await this.gateway?.answerCallbackQuery(queryId, `⚡ 执行: /${cmdBody.replace(':', ' ')}`, false)
|
|
196
|
+
let text = `/${cmdBody.replace(':', ' ')}`
|
|
197
|
+
if (cmdBody.startsWith('use:')) {
|
|
198
|
+
text = `/use ${cmdBody.slice(4)}`
|
|
199
|
+
}
|
|
200
|
+
return this.handleInbound({
|
|
201
|
+
senderId: operatorId || chatId,
|
|
202
|
+
isGroup: false,
|
|
203
|
+
text,
|
|
204
|
+
outboundPeer: { peerId: chatId, isGroup: false },
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async sendApprovalCard(approvalId, request) {
|
|
210
|
+
const peerId = this._lastPeer?.chatId || this.peerId
|
|
211
|
+
if (!peerId || !this.gateway) return
|
|
212
|
+
|
|
213
|
+
const toolName = request?.name || request?.tool || '系统操作'
|
|
214
|
+
const desc = request?.description || request?.summary || ''
|
|
215
|
+
const command = request?.command || request?.cmd || ''
|
|
216
|
+
|
|
217
|
+
const lines = [
|
|
218
|
+
`⚠️ **操作权限确认** (ID: <code>${approvalId}</code>)`,
|
|
219
|
+
'',
|
|
220
|
+
`• **工具**:<code>${toolName}</code>`,
|
|
221
|
+
]
|
|
222
|
+
if (desc) lines.push(`• **说明**:${desc}`)
|
|
223
|
+
if (command) lines.push(`• **命令**:<code>${command}</code>`)
|
|
224
|
+
lines.push('', '请选择审批决议(亦可直接输入 <code>1</code> 批准,<code>2</code> 拒绝):')
|
|
225
|
+
|
|
226
|
+
const buttons = [
|
|
227
|
+
[
|
|
228
|
+
{ text: '✓ 批准执行', callback_data: `approve:${approvalId}` },
|
|
229
|
+
{ text: '✕ 拒绝执行', callback_data: `reject:${approvalId}` },
|
|
230
|
+
],
|
|
231
|
+
]
|
|
232
|
+
|
|
233
|
+
return this.gateway.sendKeyboard(peerId, lines.join('\n'), buttons)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async _sendTextNow(text, opts = {}) {
|
|
237
|
+
// T2.3:轮次绑定的 outboundPeer(chat 级目标)优先生效
|
|
238
|
+
const peerId = opts?.outboundPeer?.peerId || this._lastPeer?.chatId || this.peerId
|
|
239
|
+
if (!peerId || !this.gateway) return
|
|
240
|
+
const content = String(text || '').trim()
|
|
241
|
+
if (!content) return
|
|
242
|
+
|
|
243
|
+
// 如果处于 Agent 生成轮次中(turn 期间):流式打字机逐段增量更新
|
|
244
|
+
if (this._inTurn) {
|
|
245
|
+
const maxChunk = 200
|
|
246
|
+
const slices = splitIntoIncremental(content, maxChunk)
|
|
247
|
+
const delayMs = Math.min(Math.max(this.config.sendChunkDelayMs ?? 800, 400), 2000)
|
|
248
|
+
|
|
249
|
+
if (!this._streamMsgId) {
|
|
250
|
+
// 首片:创建消息并记录 message_id
|
|
251
|
+
const firstSlice = slices[0] || content
|
|
252
|
+
try {
|
|
253
|
+
const res = await this.gateway.sendText(peerId, firstSlice)
|
|
254
|
+
this._streamMsgId = res?.message_id || null
|
|
255
|
+
this._streamContent = firstSlice
|
|
256
|
+
} catch (err) {
|
|
257
|
+
this.logger.warn?.('[dsh-bridge telegram] sendText stream initial failed:', err?.message ?? err)
|
|
258
|
+
await this.gateway.sendText(peerId, content).catch(() => {})
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 后续片依次 editMessageText(间隔 delayMs,呈现平滑打字机流式效果)
|
|
263
|
+
for (let i = 1; i < slices.length; i++) {
|
|
264
|
+
await sleep(delayMs)
|
|
265
|
+
this._streamContent = slices[i]
|
|
266
|
+
if (this._streamMsgId) {
|
|
267
|
+
await this.gateway.editMessageText(peerId, this._streamMsgId, this._streamContent).catch(() => {})
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
// 消息已存在(同轮次后续输出)
|
|
272
|
+
if (content.length <= 4000) {
|
|
273
|
+
this._streamContent = content
|
|
274
|
+
await this.gateway.editMessageText(peerId, this._streamMsgId, this._streamContent).catch(() => {})
|
|
275
|
+
} else {
|
|
276
|
+
// 超出 4000 字符,另起一条新消息继续流式
|
|
277
|
+
const res = await this.gateway.sendText(peerId, content).catch(() => null)
|
|
278
|
+
this._streamMsgId = res?.message_id || null
|
|
279
|
+
this._streamContent = content
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 非 turn 期间(指令响应、系统提示、单条通知等):
|
|
286
|
+
// 若命中帮助/状态/会话列表等特定系统命令响应,挂载原生快捷交互按钮
|
|
287
|
+
if (content.includes('命令帮助') || content.includes('/new <提示词>')) {
|
|
288
|
+
const buttons = [
|
|
289
|
+
[
|
|
290
|
+
{ text: '📋 会话列表', callback_data: 'cmd:sessions' },
|
|
291
|
+
{ text: '📁 工作区', callback_data: 'cmd:workspaces' },
|
|
292
|
+
],
|
|
293
|
+
[
|
|
294
|
+
{ text: '📊 运行状态', callback_data: 'cmd:status' },
|
|
295
|
+
{ text: '⏹ 停止任务', callback_data: 'cmd:stop' },
|
|
296
|
+
],
|
|
297
|
+
]
|
|
298
|
+
return this.gateway.sendKeyboard(peerId, content, buttons)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (content.includes('Agent 状态') || content.includes('无活动会话')) {
|
|
302
|
+
const buttons = [
|
|
303
|
+
[
|
|
304
|
+
{ text: '🔄 刷新状态', callback_data: 'cmd:status' },
|
|
305
|
+
{ text: '⏹ 停止任务', callback_data: 'cmd:stop' },
|
|
306
|
+
],
|
|
307
|
+
[
|
|
308
|
+
{ text: '📋 会话列表', callback_data: 'cmd:sessions' },
|
|
309
|
+
{ text: '🚪 结束会话', callback_data: 'cmd:end' },
|
|
310
|
+
],
|
|
311
|
+
]
|
|
312
|
+
return this.gateway.sendKeyboard(peerId, content, buttons)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (content.includes('会话列表') || content.includes('可用会话')) {
|
|
316
|
+
const buttons = [
|
|
317
|
+
[
|
|
318
|
+
{ text: '📁 可用工作区', callback_data: 'cmd:workspaces' },
|
|
319
|
+
{ text: '📊 查看状态', callback_data: 'cmd:status' },
|
|
320
|
+
],
|
|
321
|
+
]
|
|
322
|
+
return this.gateway.sendKeyboard(peerId, content, buttons)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (content.length <= 4000) {
|
|
326
|
+
return this.gateway.sendText(peerId, content)
|
|
327
|
+
}
|
|
328
|
+
const chunks = conversationBridgeHelpers.splitForIM(content, 4000)
|
|
329
|
+
for (const chunk of chunks) {
|
|
330
|
+
await this.gateway.sendText(peerId, chunk)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async sendTyping() {
|
|
335
|
+
const peerId = this._lastPeer?.chatId || this.peerId
|
|
336
|
+
if (!peerId || !this.gateway) return
|
|
337
|
+
return this.gateway.sendTyping(peerId)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
dispose() {
|
|
341
|
+
this._inTurn = false
|
|
342
|
+
this._streamMsgId = null
|
|
343
|
+
this._streamContent = ''
|
|
344
|
+
this._lastPeer = null
|
|
345
|
+
super.dispose()
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
351
349
|
export const telegramNodeHelpers = { makePlatform }
|