@xmanrui/dsh-im 0.7.0 → 0.7.2
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/lib/index.js +119 -121
- package/package.json +1 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +423 -8
- package/src/channels/dingtalk/harness-client.mjs +16 -366
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/discord/discord-runtime.mjs +11 -1
- package/src/channels/discord/harness-client.mjs +10 -2
- package/src/channels/feishu/bridge.mjs +571 -50
- package/src/channels/feishu/feishu-runtime.mjs +41 -1
- package/src/channels/feishu/harness-client.mjs +16 -335
- package/src/channels/qq/harness-client.mjs +10 -2
- package/src/channels/qq/qq-bridge.mjs +428 -28
- package/src/channels/qq/qq-runtime.mjs +14 -3
- package/src/channels/shared/harness-approval.mjs +472 -0
- package/src/channels/shared/harness-client.mjs +858 -0
- package/src/channels/shared/harness-question.mjs +85 -0
- package/src/channels/shared/text-harness-bridge.mjs +486 -24
- package/src/channels/slack/harness-client.mjs +10 -2
- package/src/channels/slack/slack-runtime.mjs +11 -1
- package/src/channels/telegram/harness-client.mjs +10 -2
- package/src/channels/telegram/telegram-runtime.mjs +15 -4
- package/src/channels/wecom/harness-client.mjs +10 -2
- package/src/channels/wecom/wecom-bridge.mjs +434 -14
- package/src/channels/wecom/wecom-runtime.mjs +6 -0
- package/src/channels/weixin/harness-client.mjs +16 -326
- package/src/channels/weixin/weixin-api.mjs +1 -1
- package/src/channels/weixin/weixin-bridge.mjs +451 -22
- package/src/channels/weixin/weixin-runtime.mjs +56 -7
- package/src/channels/whatsapp/harness-client.mjs +10 -2
- package/src/channels/whatsapp/whatsapp-runtime.mjs +1 -0
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
2
|
+
import {
|
|
3
|
+
harnessAnswerForQuestion,
|
|
4
|
+
harnessQuestionText,
|
|
5
|
+
validHarnessQuestion,
|
|
6
|
+
} from '../shared/harness-question.mjs';
|
|
7
|
+
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
|
2
8
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
3
9
|
|
|
10
|
+
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
|
11
|
+
|
|
4
12
|
const HELP_TEXT = [
|
|
5
13
|
'QQ 机器人已连接 DeepSeek Harness。',
|
|
6
14
|
'',
|
|
@@ -22,6 +30,17 @@ function safeText(message) {
|
|
|
22
30
|
return typeof message?.content === 'string' ? message.content.trim() : '';
|
|
23
31
|
}
|
|
24
32
|
|
|
33
|
+
function nonEmptyString(value) {
|
|
34
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function canClaimInteractionReply(message, pending) {
|
|
38
|
+
return pending.questions[pending.index]
|
|
39
|
+
&& nonEmptyString(message?.senderId) === pending.actor
|
|
40
|
+
&& (message.kind !== 'group' || message.rawEventType === 'GROUP_AT_MESSAGE_CREATE')
|
|
41
|
+
&& nonEmptyString(safeText(message));
|
|
42
|
+
}
|
|
43
|
+
|
|
25
44
|
export function createQqBridgeStatus() {
|
|
26
45
|
return {
|
|
27
46
|
messagesReceived: 0,
|
|
@@ -42,7 +61,13 @@ export class QqHarnessBridge {
|
|
|
42
61
|
#status;
|
|
43
62
|
#logger;
|
|
44
63
|
#replyTimeoutMs;
|
|
64
|
+
#signal;
|
|
45
65
|
#queues = new Map();
|
|
66
|
+
#pendingInteractions = new Map();
|
|
67
|
+
#interactionKeys = new Map();
|
|
68
|
+
#acceptedMessageIds = new Set();
|
|
69
|
+
#approvalTasks = new Set();
|
|
70
|
+
#approvals;
|
|
46
71
|
|
|
47
72
|
constructor({
|
|
48
73
|
bot,
|
|
@@ -52,6 +77,7 @@ export class QqHarnessBridge {
|
|
|
52
77
|
status = createQqBridgeStatus(),
|
|
53
78
|
logger = console,
|
|
54
79
|
replyTimeoutMs = 600_000,
|
|
80
|
+
signal,
|
|
55
81
|
}) {
|
|
56
82
|
if (!bot || typeof bot.sendText !== 'function') throw new TypeError('QQ bot client is required');
|
|
57
83
|
if (!ownerUserOpenid) throw new TypeError('QQ scanner identity is required');
|
|
@@ -63,6 +89,8 @@ export class QqHarnessBridge {
|
|
|
63
89
|
this.#status = status;
|
|
64
90
|
this.#logger = logger;
|
|
65
91
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
92
|
+
this.#signal = signal;
|
|
93
|
+
this.#approvals = new HarnessApprovalQueue({ label: 'qq', logger });
|
|
66
94
|
}
|
|
67
95
|
|
|
68
96
|
get status() {
|
|
@@ -70,12 +98,81 @@ export class QqHarnessBridge {
|
|
|
70
98
|
}
|
|
71
99
|
|
|
72
100
|
accept(message) {
|
|
101
|
+
if (this.#signal?.aborted) return Promise.resolve();
|
|
102
|
+
const messageId = nonEmptyString(message?.messageId);
|
|
103
|
+
const sender = nonEmptyString(message?.senderId);
|
|
104
|
+
if (!messageId || !sender || message?.senderIsBot === true
|
|
105
|
+
|| !['c2c', 'group'].includes(message?.kind)
|
|
106
|
+
|| this.#state.hasSeen(messageId)
|
|
107
|
+
|| this.#acceptedMessageIds.has(messageId)) return Promise.resolve();
|
|
73
108
|
const key = conversationKey(message);
|
|
109
|
+
this.#acceptedMessageIds.add(messageId);
|
|
110
|
+
const pending = this.#pendingInteractions.get(key);
|
|
111
|
+
const approval = this.#approvals.claimReply({
|
|
112
|
+
key,
|
|
113
|
+
actor: sender,
|
|
114
|
+
messageId,
|
|
115
|
+
text: safeText(message),
|
|
116
|
+
addressed: message.kind !== 'group' || message.rawEventType === 'GROUP_AT_MESSAGE_CREATE',
|
|
117
|
+
hasPendingQuestion: Boolean(pending),
|
|
118
|
+
questionCompletion: pending?.submitting || pending?.claimedReplyMessageId
|
|
119
|
+
? pending.queue
|
|
120
|
+
: null,
|
|
121
|
+
isQuestionPending: () => this.#pendingInteractions.has(key),
|
|
122
|
+
send: (text) => this.#bot.sendText(message.replyTarget, text),
|
|
123
|
+
});
|
|
124
|
+
if (approval) {
|
|
125
|
+
let task;
|
|
126
|
+
task = approval.process(async () => {
|
|
127
|
+
if (this.#state.hasSeen(messageId)) return false;
|
|
128
|
+
await this.#state.markSeen(messageId);
|
|
129
|
+
this.#status.messagesReceived += 1;
|
|
130
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
131
|
+
return true;
|
|
132
|
+
})
|
|
133
|
+
.finally(() => {
|
|
134
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
135
|
+
this.#approvalTasks.delete(task);
|
|
136
|
+
});
|
|
137
|
+
this.#approvalTasks.add(task);
|
|
138
|
+
return task;
|
|
139
|
+
}
|
|
140
|
+
if (pending && sender !== pending.actor) {
|
|
141
|
+
return this.#enqueueMessage(message, messageId, key);
|
|
142
|
+
}
|
|
143
|
+
if (pending?.submitting || pending?.claimedReplyMessageId) {
|
|
144
|
+
return this.#enqueueMessage(message, messageId, key);
|
|
145
|
+
}
|
|
146
|
+
if (pending) {
|
|
147
|
+
if (canClaimInteractionReply(message, pending)) {
|
|
148
|
+
pending.claimedReplyMessageId = messageId;
|
|
149
|
+
}
|
|
150
|
+
const previous = pending.queue ?? Promise.resolve();
|
|
151
|
+
const current = previous
|
|
152
|
+
.catch(() => undefined)
|
|
153
|
+
.then(() => this.#processInteractionReply(message, messageId, key, pending))
|
|
154
|
+
.catch((error) => this.#handleInteractionFailure(message, messageId, error))
|
|
155
|
+
.finally(() => {
|
|
156
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
157
|
+
if (pending.claimedReplyMessageId === messageId) pending.claimedReplyMessageId = null;
|
|
158
|
+
if (pending.queue === current) pending.queue = null;
|
|
159
|
+
});
|
|
160
|
+
pending.queue = current;
|
|
161
|
+
return current;
|
|
162
|
+
}
|
|
163
|
+
return this.#enqueueMessage(message, messageId, key);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
#enqueueMessage(message, messageId, key, {
|
|
167
|
+
releaseMessageId = true,
|
|
168
|
+
alreadyRecorded = false,
|
|
169
|
+
} = {}) {
|
|
74
170
|
const previous = this.#queues.get(key) ?? Promise.resolve();
|
|
75
171
|
const current = previous
|
|
76
172
|
.catch(() => undefined)
|
|
77
|
-
.then(() => this.#process(message))
|
|
173
|
+
.then(() => this.#process(message, key, { alreadyRecorded }))
|
|
78
174
|
.finally(() => {
|
|
175
|
+
if (releaseMessageId) this.#acceptedMessageIds.delete(messageId);
|
|
79
176
|
if (this.#queues.get(key) === current) this.#queues.delete(key);
|
|
80
177
|
});
|
|
81
178
|
this.#queues.set(key, current);
|
|
@@ -83,17 +180,26 @@ export class QqHarnessBridge {
|
|
|
83
180
|
}
|
|
84
181
|
|
|
85
182
|
async waitForIdle() {
|
|
86
|
-
await Promise.allSettled([
|
|
183
|
+
await Promise.allSettled([
|
|
184
|
+
...this.#queues.values(),
|
|
185
|
+
...[...this.#pendingInteractions.values()].flatMap((pending) => (
|
|
186
|
+
pending.queue ? [pending.queue] : []
|
|
187
|
+
)),
|
|
188
|
+
...this.#approvalTasks,
|
|
189
|
+
]);
|
|
87
190
|
}
|
|
88
191
|
|
|
89
|
-
async #process(message) {
|
|
90
|
-
|
|
91
|
-
const
|
|
192
|
+
async #process(message, key, { alreadyRecorded = false } = {}) {
|
|
193
|
+
if (this.#signal?.aborted) return;
|
|
194
|
+
const messageId = nonEmptyString(message?.messageId);
|
|
195
|
+
const sender = nonEmptyString(message?.senderId);
|
|
92
196
|
if (!messageId || !sender || message.senderIsBot === true) return;
|
|
93
|
-
if (!['c2c', 'group'].includes(message.kind)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
197
|
+
if (!['c2c', 'group'].includes(message.kind)) return;
|
|
198
|
+
if (!alreadyRecorded) {
|
|
199
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
200
|
+
this.#status.messagesReceived += 1;
|
|
201
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
202
|
+
}
|
|
97
203
|
if (this.#ownerUserOpenid !== '*' && sender !== this.#ownerUserOpenid) {
|
|
98
204
|
this.#status.messagesRejected += 1;
|
|
99
205
|
this.#status.lastRejectedAt = new Date().toISOString();
|
|
@@ -116,12 +222,11 @@ export class QqHarnessBridge {
|
|
|
116
222
|
return;
|
|
117
223
|
}
|
|
118
224
|
if (command === '/status') {
|
|
119
|
-
await this.#harness.ensureRunning();
|
|
225
|
+
await this.#harness.ensureRunning({ signal: this.#signal });
|
|
120
226
|
await this.#bot.sendText(target, 'QQ 机器人与 DeepSeek Harness 连接正常。');
|
|
121
227
|
await this.#state.markSeen(messageId);
|
|
122
228
|
return;
|
|
123
229
|
}
|
|
124
|
-
const key = conversationKey(message);
|
|
125
230
|
if (command === '/new') {
|
|
126
231
|
await this.#state.clearSession(key);
|
|
127
232
|
await this.#bot.sendText(target, '已开启新会话。请发送你的问题。');
|
|
@@ -146,23 +251,41 @@ export class QqHarnessBridge {
|
|
|
146
251
|
this.#logger.warn?.('[dsh-im:qq] unable to start a QQ stream; using a text reply:', error);
|
|
147
252
|
}
|
|
148
253
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
254
|
+
let answer;
|
|
255
|
+
try {
|
|
256
|
+
({ answer } = await askInWorkspaceSession({
|
|
257
|
+
harness: this.#harness,
|
|
258
|
+
state: this.#state,
|
|
259
|
+
key,
|
|
260
|
+
text,
|
|
261
|
+
createOptions: { signal: this.#signal },
|
|
262
|
+
existsOptions: { signal: this.#signal },
|
|
263
|
+
askOptions: {
|
|
264
|
+
timeoutMs: this.#replyTimeoutMs,
|
|
265
|
+
signal: this.#signal,
|
|
266
|
+
onUpdate: stream ? async (update) => {
|
|
267
|
+
const progress = update.type === 'text'
|
|
268
|
+
? update.text
|
|
269
|
+
: update.type === 'tool'
|
|
270
|
+
? `正在使用${update.name}…`
|
|
271
|
+
: update.text;
|
|
272
|
+
if (progress) await stream.update(progress);
|
|
273
|
+
} : undefined,
|
|
274
|
+
onInteraction: (interaction) => this.#handleInteraction(interaction, {
|
|
275
|
+
key,
|
|
276
|
+
actor: sender,
|
|
277
|
+
target,
|
|
278
|
+
requiresMention: message.kind === 'group',
|
|
279
|
+
}),
|
|
280
|
+
onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
|
|
281
|
+
},
|
|
282
|
+
}));
|
|
283
|
+
} finally {
|
|
284
|
+
await Promise.allSettled([
|
|
285
|
+
this.#cancelPendingInteraction(key),
|
|
286
|
+
this.#approvals.closeRoute(key),
|
|
287
|
+
]);
|
|
288
|
+
}
|
|
166
289
|
if (stream) {
|
|
167
290
|
try {
|
|
168
291
|
await stream.update(answer);
|
|
@@ -179,6 +302,7 @@ export class QqHarnessBridge {
|
|
|
179
302
|
this.#status.lastReplyAt = new Date().toISOString();
|
|
180
303
|
this.#status.lastError = null;
|
|
181
304
|
} catch (error) {
|
|
305
|
+
if (this.#signal?.aborted) return;
|
|
182
306
|
this.#status.lastError = error?.message ?? String(error);
|
|
183
307
|
this.#logger.error?.('[dsh-im:qq] failed to process an inbound message:', error);
|
|
184
308
|
try {
|
|
@@ -189,4 +313,280 @@ export class QqHarnessBridge {
|
|
|
189
313
|
}
|
|
190
314
|
}
|
|
191
315
|
}
|
|
316
|
+
|
|
317
|
+
async #processInteractionReply(message, messageId, key, expected) {
|
|
318
|
+
this.#signal?.throwIfAborted();
|
|
319
|
+
const current = this.#pendingInteractions.get(key);
|
|
320
|
+
const claimed = expected.claimedReplyMessageId === messageId;
|
|
321
|
+
if (!current || current !== expected || current.submitting) {
|
|
322
|
+
if (claimed && (!current || current !== expected)) {
|
|
323
|
+
return this.#discardResolvedInteractionReply(message, messageId);
|
|
324
|
+
}
|
|
325
|
+
return this.#enqueueMessage(message, messageId, key, { releaseMessageId: false });
|
|
326
|
+
}
|
|
327
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
328
|
+
await this.#state.markSeen(messageId);
|
|
329
|
+
this.#status.messagesReceived += 1;
|
|
330
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
331
|
+
|
|
332
|
+
if (message.kind === 'group' && message.rawEventType !== 'GROUP_AT_MESSAGE_CREATE') return;
|
|
333
|
+
const text = nonEmptyString(safeText(message));
|
|
334
|
+
if (!text) {
|
|
335
|
+
await this.#bot.sendText(message.replyTarget, '请用文字回答当前问题。');
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const pending = this.#pendingInteractions.get(key);
|
|
340
|
+
if (!pending || pending !== expected || pending.submitting) {
|
|
341
|
+
if (claimed && (!pending || pending !== expected)) {
|
|
342
|
+
await this.#bot.sendText(message.replyTarget, INTERACTION_RESOLVED_TEXT);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
return this.#enqueueMessage(message, messageId, key, {
|
|
346
|
+
releaseMessageId: false,
|
|
347
|
+
alreadyRecorded: true,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
pending.target = message.replyTarget;
|
|
351
|
+
if (pending.needsPresentation) {
|
|
352
|
+
try {
|
|
353
|
+
await this.#presentInteraction(pending);
|
|
354
|
+
} catch {
|
|
355
|
+
this.#status.lastError = 'QQ 交互问题发送失败。';
|
|
356
|
+
this.#logger.error?.('[dsh-im:qq] failed to retry an interaction question');
|
|
357
|
+
pending.interaction.reconnect?.();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const presentedPending = this.#pendingInteractions.get(key);
|
|
361
|
+
if (!presentedPending || presentedPending !== expected || presentedPending.submitting) {
|
|
362
|
+
if (claimed && (!presentedPending || presentedPending !== expected)) {
|
|
363
|
+
await this.#bot.sendText(message.replyTarget, INTERACTION_RESOLVED_TEXT)
|
|
364
|
+
.catch(() => undefined);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
return this.#enqueueMessage(message, messageId, key, {
|
|
368
|
+
releaseMessageId: false,
|
|
369
|
+
alreadyRecorded: true,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const question = pending.questions[pending.index];
|
|
375
|
+
if (!question) return;
|
|
376
|
+
pending.answers.push(harnessAnswerForQuestion(question, text));
|
|
377
|
+
pending.index += 1;
|
|
378
|
+
if (pending.index < pending.questions.length) {
|
|
379
|
+
if (pending.claimedReplyMessageId === messageId) {
|
|
380
|
+
pending.claimedReplyMessageId = null;
|
|
381
|
+
}
|
|
382
|
+
pending.needsPresentation = true;
|
|
383
|
+
try {
|
|
384
|
+
await this.#presentInteraction(pending);
|
|
385
|
+
} catch {
|
|
386
|
+
this.#status.lastError = 'QQ 交互问题发送失败。';
|
|
387
|
+
this.#logger.error?.('[dsh-im:qq] failed to send the next interaction question');
|
|
388
|
+
pending.interaction.reconnect?.();
|
|
389
|
+
}
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
pending.submitting = true;
|
|
394
|
+
try {
|
|
395
|
+
await pending.interaction.respond({
|
|
396
|
+
ok: true,
|
|
397
|
+
value: {
|
|
398
|
+
sessionId: pending.sessionId,
|
|
399
|
+
answer: { answers: pending.answers },
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
this.#clearPendingInteraction(key, pending.interactionId);
|
|
403
|
+
this.#status.lastError = null;
|
|
404
|
+
} catch (error) {
|
|
405
|
+
if (this.#signal?.aborted) return;
|
|
406
|
+
if (error?.code === 'interaction-not-pending') {
|
|
407
|
+
this.#clearPendingInteraction(key, pending.interactionId);
|
|
408
|
+
await this.#bot.sendText(pending.target, INTERACTION_RESOLVED_TEXT).catch(() => undefined);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (this.#pendingInteractions.get(key) !== pending) return;
|
|
412
|
+
pending.submitting = false;
|
|
413
|
+
pending.answers.pop();
|
|
414
|
+
pending.index -= 1;
|
|
415
|
+
this.#status.lastError = '回答提交失败。';
|
|
416
|
+
this.#logger.error?.('[dsh-im:qq] failed to answer a Harness interaction');
|
|
417
|
+
await this.#bot.sendText(pending.target, '回答提交失败,请重新发送当前问题的答案。')
|
|
418
|
+
.catch(() => undefined);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async #handleInteraction(interaction, {
|
|
423
|
+
key,
|
|
424
|
+
actor,
|
|
425
|
+
target,
|
|
426
|
+
requiresMention,
|
|
427
|
+
}) {
|
|
428
|
+
if (interaction?.kind === 'approval') {
|
|
429
|
+
return this.#approvals.handleRequested(interaction, {
|
|
430
|
+
key,
|
|
431
|
+
actor,
|
|
432
|
+
requiresMention,
|
|
433
|
+
send: (text) => this.#bot.sendText(target, text),
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
if (interaction?.kind !== 'question') return;
|
|
437
|
+
const questions = interaction?.payload?.questions;
|
|
438
|
+
const interactionId = typeof interaction?.interactionId === 'string'
|
|
439
|
+
? interaction.interactionId
|
|
440
|
+
: interaction?.rpcId;
|
|
441
|
+
if (typeof interaction?.rpcId !== 'string'
|
|
442
|
+
|| typeof interactionId !== 'string'
|
|
443
|
+
|| typeof interaction.sessionId !== 'string'
|
|
444
|
+
|| !Array.isArray(questions)
|
|
445
|
+
|| questions.length === 0
|
|
446
|
+
|| questions.some((question) => !validHarnessQuestion(question))) {
|
|
447
|
+
this.#logger.warn?.('[dsh-im:qq] ignored an invalid Harness question interaction');
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (interaction.recovered === true) {
|
|
452
|
+
await interaction.respond({
|
|
453
|
+
ok: false,
|
|
454
|
+
error: {
|
|
455
|
+
code: 'cancelled',
|
|
456
|
+
message: 'QQ safely cancelled an interaction left by an earlier client.',
|
|
457
|
+
details: {},
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
await this.#bot.sendText(
|
|
461
|
+
target,
|
|
462
|
+
'检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。',
|
|
463
|
+
).catch(() => undefined);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const existing = this.#pendingInteractions.get(key);
|
|
468
|
+
if (existing?.interactionId === interactionId) {
|
|
469
|
+
existing.interaction = interaction;
|
|
470
|
+
if (existing.needsPresentation) await this.#presentInteraction(existing);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (this.#interactionKeys.has(interactionId)) return;
|
|
474
|
+
if (existing) {
|
|
475
|
+
await interaction.respond({
|
|
476
|
+
ok: false,
|
|
477
|
+
error: {
|
|
478
|
+
code: 'cancelled',
|
|
479
|
+
message: 'QQ is already handling another user interaction.',
|
|
480
|
+
details: {},
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const pending = {
|
|
487
|
+
kind: 'question',
|
|
488
|
+
interactionId,
|
|
489
|
+
sessionId: interaction.sessionId,
|
|
490
|
+
interaction,
|
|
491
|
+
actor,
|
|
492
|
+
requiresMention,
|
|
493
|
+
questions,
|
|
494
|
+
answers: [],
|
|
495
|
+
index: 0,
|
|
496
|
+
target,
|
|
497
|
+
queue: null,
|
|
498
|
+
claimedReplyMessageId: null,
|
|
499
|
+
presentationPromise: null,
|
|
500
|
+
submitting: false,
|
|
501
|
+
needsPresentation: true,
|
|
502
|
+
};
|
|
503
|
+
this.#pendingInteractions.set(key, pending);
|
|
504
|
+
this.#interactionKeys.set(interactionId, key);
|
|
505
|
+
await this.#presentInteraction(pending);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async #handleInteractionResolved(resolution) {
|
|
509
|
+
if (resolution?.kind === 'approval') {
|
|
510
|
+
await this.#approvals.handleResolved(resolution);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const interactionId = resolution?.interactionId;
|
|
514
|
+
if (resolution?.kind !== 'question' || typeof interactionId !== 'string') return;
|
|
515
|
+
const key = this.#interactionKeys.get(interactionId);
|
|
516
|
+
if (!key) return;
|
|
517
|
+
this.#clearPendingInteraction(key, interactionId);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
#presentInteraction(pending) {
|
|
521
|
+
if (!pending.needsPresentation) return Promise.resolve();
|
|
522
|
+
if (pending.presentationPromise) return pending.presentationPromise;
|
|
523
|
+
const question = pending.questions[pending.index];
|
|
524
|
+
if (!question) return Promise.resolve();
|
|
525
|
+
const presentation = this.#bot.sendText(
|
|
526
|
+
pending.target,
|
|
527
|
+
harnessQuestionText(
|
|
528
|
+
question,
|
|
529
|
+
pending.index,
|
|
530
|
+
pending.questions.length,
|
|
531
|
+
{ requiresMention: pending.requiresMention },
|
|
532
|
+
),
|
|
533
|
+
).then(() => {
|
|
534
|
+
pending.needsPresentation = false;
|
|
535
|
+
}).finally(() => {
|
|
536
|
+
if (pending.presentationPromise === presentation) pending.presentationPromise = null;
|
|
537
|
+
});
|
|
538
|
+
pending.presentationPromise = presentation;
|
|
539
|
+
return presentation;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async #discardResolvedInteractionReply(message, messageId) {
|
|
543
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
544
|
+
await this.#state.markSeen(messageId);
|
|
545
|
+
this.#status.messagesReceived += 1;
|
|
546
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
547
|
+
await this.#bot.sendText(message.replyTarget, INTERACTION_RESOLVED_TEXT).catch(() => undefined);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
#takePendingInteraction(key, interactionId) {
|
|
551
|
+
const pending = this.#pendingInteractions.get(key);
|
|
552
|
+
if (!pending
|
|
553
|
+
|| (interactionId !== undefined && pending.interactionId !== interactionId)) return null;
|
|
554
|
+
this.#pendingInteractions.delete(key);
|
|
555
|
+
this.#interactionKeys.delete(pending.interactionId);
|
|
556
|
+
return pending;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
#clearPendingInteraction(key, interactionId) {
|
|
560
|
+
return this.#takePendingInteraction(key, interactionId) !== null;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async #cancelPendingInteraction(key) {
|
|
564
|
+
const pending = this.#takePendingInteraction(key);
|
|
565
|
+
if (!pending || pending.kind !== 'question') return;
|
|
566
|
+
try {
|
|
567
|
+
await pending.interaction.respond({
|
|
568
|
+
ok: false,
|
|
569
|
+
error: {
|
|
570
|
+
code: 'cancelled',
|
|
571
|
+
message: 'The QQ interaction ended before the user answered.',
|
|
572
|
+
details: {},
|
|
573
|
+
},
|
|
574
|
+
}, { signal: AbortSignal.timeout(5_000) });
|
|
575
|
+
} catch (error) {
|
|
576
|
+
if (error?.code !== 'interaction-not-pending') {
|
|
577
|
+
this.#logger.warn?.('[dsh-im:qq] failed to cancel a pending Harness interaction');
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
async #handleInteractionFailure(message, messageId, error) {
|
|
583
|
+
if (this.#signal?.aborted) return;
|
|
584
|
+
this.#status.lastError = error?.message ?? String(error);
|
|
585
|
+
this.#logger.error?.('[dsh-im:qq] failed to process an interaction reply:', error);
|
|
586
|
+
if (!this.#state.hasSeen(messageId)) {
|
|
587
|
+
await this.#state.markSeen(messageId).catch(() => undefined);
|
|
588
|
+
}
|
|
589
|
+
await this.#bot.sendText(message.replyTarget, '消息处理失败,请稍后重试。')
|
|
590
|
+
.catch(() => undefined);
|
|
591
|
+
}
|
|
192
592
|
}
|
|
@@ -101,6 +101,8 @@ export class QqRuntime {
|
|
|
101
101
|
if (!bot || typeof bot.start !== 'function' || typeof bot.stop !== 'function') {
|
|
102
102
|
throw new TypeError('QQ bot factory returned an invalid client');
|
|
103
103
|
}
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
this.#abortController = controller;
|
|
104
106
|
this.#bot = bot;
|
|
105
107
|
this.#bridge = new QqHarnessBridge({
|
|
106
108
|
bot,
|
|
@@ -110,6 +112,7 @@ export class QqRuntime {
|
|
|
110
112
|
status: this.#status,
|
|
111
113
|
logger: this.#logger,
|
|
112
114
|
replyTimeoutMs: this.#replyTimeoutMs,
|
|
115
|
+
signal: controller.signal,
|
|
113
116
|
});
|
|
114
117
|
bot.use?.(this.#typingMiddleware({
|
|
115
118
|
keepAlive: true,
|
|
@@ -117,8 +120,6 @@ export class QqRuntime {
|
|
|
117
120
|
|| ctx?.message?.senderId === this.#config.ownerUserOpenid,
|
|
118
121
|
}));
|
|
119
122
|
|
|
120
|
-
const controller = new AbortController();
|
|
121
|
-
this.#abortController = controller;
|
|
122
123
|
let readyResolve;
|
|
123
124
|
let readyReject;
|
|
124
125
|
const ready = new Promise((resolve, reject) => {
|
|
@@ -141,7 +142,17 @@ export class QqRuntime {
|
|
|
141
142
|
this.#logger.warn?.(`[dsh-im:qq] bot ${this.#config.botId} connection error:`, error);
|
|
142
143
|
}
|
|
143
144
|
};
|
|
144
|
-
const onMessage = (_ctx, message) =>
|
|
145
|
+
const onMessage = (_ctx, message) => {
|
|
146
|
+
const task = this.#bridge?.accept(message);
|
|
147
|
+
if (!task) return;
|
|
148
|
+
void task.catch((error) => {
|
|
149
|
+
if (controller.signal.aborted) return;
|
|
150
|
+
this.#logger.error?.(
|
|
151
|
+
`[dsh-im:qq] bot ${this.#config.botId} message handling failed:`,
|
|
152
|
+
error,
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
};
|
|
145
156
|
bot.on('ready', onReady);
|
|
146
157
|
bot.on('resumed', onReady);
|
|
147
158
|
bot.on('error', onError);
|