@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
|
@@ -34,19 +34,21 @@ export function normalizeTelegramUpdate(update, { botId, username }) {
|
|
|
34
34
|
const addressed = direct
|
|
35
35
|
|| String(message.reply_to_message?.from?.id ?? '') === String(botId)
|
|
36
36
|
|| mentionedUsername(message, username);
|
|
37
|
+
const messageThreadId = Number.isSafeInteger(message.message_thread_id)
|
|
38
|
+
? message.message_thread_id : undefined;
|
|
37
39
|
return {
|
|
38
40
|
messageId: String(update.update_id),
|
|
39
41
|
senderId: String(senderId),
|
|
40
42
|
senderIsBot: message.from?.is_bot === true,
|
|
41
43
|
kind: direct ? 'direct' : 'group',
|
|
42
|
-
conversationId:
|
|
44
|
+
conversationId: messageThreadId === undefined
|
|
45
|
+
? String(chatId) : `${chatId}:${messageThreadId}`,
|
|
43
46
|
content: withoutBotMention(message.text ?? message.caption ?? '', username),
|
|
44
47
|
addressed,
|
|
45
48
|
replyTarget: {
|
|
46
49
|
chatId,
|
|
47
50
|
replyToMessageId: messageId,
|
|
48
|
-
messageThreadId
|
|
49
|
-
? message.message_thread_id : undefined,
|
|
51
|
+
messageThreadId,
|
|
50
52
|
},
|
|
51
53
|
};
|
|
52
54
|
}
|
|
@@ -206,6 +208,7 @@ export class TelegramRuntime {
|
|
|
206
208
|
status: this.#status,
|
|
207
209
|
logger: this.#logger,
|
|
208
210
|
replyTimeoutMs: this.#replyTimeoutMs,
|
|
211
|
+
signal: controller.signal,
|
|
209
212
|
});
|
|
210
213
|
|
|
211
214
|
let cursor = this.#state.cursor();
|
|
@@ -248,7 +251,15 @@ export class TelegramRuntime {
|
|
|
248
251
|
botId: this.#config.platformId,
|
|
249
252
|
username: this.#config.username,
|
|
250
253
|
});
|
|
251
|
-
if (message)
|
|
254
|
+
if (message) {
|
|
255
|
+
void this.#bridge.accept(message).catch((error) => {
|
|
256
|
+
if (signal.aborted) return;
|
|
257
|
+
this.#logger.error?.(
|
|
258
|
+
`[dsh-im:telegram] bot ${this.#config.botId} message handling failed:`,
|
|
259
|
+
error,
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
252
263
|
cursor = update.update_id + 1;
|
|
253
264
|
await this.#state.setCursor(cursor);
|
|
254
265
|
}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
import { HarnessClient } from '../
|
|
1
|
+
import { HarnessClient } from '../shared/harness-client.mjs';
|
|
2
2
|
|
|
3
|
-
export class WecomHarnessClient extends HarnessClient {
|
|
3
|
+
export class WecomHarnessClient extends HarnessClient {
|
|
4
|
+
constructor(options) {
|
|
5
|
+
super({
|
|
6
|
+
...options,
|
|
7
|
+
rpcIdPrefix: 'wecom',
|
|
8
|
+
logPrefix: 'dsh-wecom',
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { generateReqId } from '@wecom/aibot-node-sdk';
|
|
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 { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
3
9
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
4
10
|
|
|
@@ -15,6 +21,11 @@ const HELP_TEXT = [
|
|
|
15
21
|
'/help 显示本帮助',
|
|
16
22
|
].join('\n');
|
|
17
23
|
const MAX_REPLY_BYTES = 18_000;
|
|
24
|
+
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
|
25
|
+
|
|
26
|
+
function nonEmptyString(value) {
|
|
27
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
28
|
+
}
|
|
18
29
|
|
|
19
30
|
function bodyOf(frame) {
|
|
20
31
|
return frame?.body && typeof frame.body === 'object' ? frame.body : {};
|
|
@@ -27,16 +38,23 @@ function conversationKey(frame) {
|
|
|
27
38
|
|
|
28
39
|
function messageText(frame) {
|
|
29
40
|
const body = bodyOf(frame);
|
|
30
|
-
|
|
31
|
-
if (body.msgtype === '
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
let text = '';
|
|
42
|
+
if (body.msgtype === 'text') {
|
|
43
|
+
text = typeof body.text?.content === 'string' ? body.text.content.trim() : '';
|
|
44
|
+
} else if (body.msgtype === 'voice') {
|
|
45
|
+
text = typeof body.voice?.content === 'string' ? body.voice.content.trim() : '';
|
|
46
|
+
} else if (body.msgtype === 'mixed' && Array.isArray(body.mixed?.msg_item)) {
|
|
47
|
+
text = body.mixed.msg_item
|
|
34
48
|
.filter((item) => item?.msgtype === 'text' && typeof item.text?.content === 'string')
|
|
35
49
|
.map((item) => item.text.content)
|
|
36
50
|
.join('\n')
|
|
37
51
|
.trim();
|
|
38
52
|
}
|
|
39
|
-
|
|
53
|
+
// Group callbacks retain the leading @bot mention that caused delivery.
|
|
54
|
+
// It is routing metadata rather than part of the user's prompt or answer.
|
|
55
|
+
return body.chattype === 'group'
|
|
56
|
+
? text.replace(/^\s*@\S+(?:\s+|$)/u, '').trim()
|
|
57
|
+
: text;
|
|
40
58
|
}
|
|
41
59
|
|
|
42
60
|
function splitUtf8(text, maxBytes = MAX_REPLY_BYTES) {
|
|
@@ -66,6 +84,12 @@ function progressText(update) {
|
|
|
66
84
|
return update?.text;
|
|
67
85
|
}
|
|
68
86
|
|
|
87
|
+
function canClaimInteractionReply(frame, pending) {
|
|
88
|
+
return pending.questions[pending.index]
|
|
89
|
+
&& nonEmptyString(bodyOf(frame).from?.userid) === pending.actor
|
|
90
|
+
&& nonEmptyString(messageText(frame));
|
|
91
|
+
}
|
|
92
|
+
|
|
69
93
|
export function createWecomBridgeStatus() {
|
|
70
94
|
return {
|
|
71
95
|
messagesReceived: 0,
|
|
@@ -86,7 +110,13 @@ export class WecomHarnessBridge {
|
|
|
86
110
|
#logger;
|
|
87
111
|
#replyTimeoutMs;
|
|
88
112
|
#generateReqId;
|
|
113
|
+
#signal;
|
|
89
114
|
#queues = new Map();
|
|
115
|
+
#pendingInteractions = new Map();
|
|
116
|
+
#interactionKeys = new Map();
|
|
117
|
+
#acceptedMessageIds = new Set();
|
|
118
|
+
#approvalTasks = new Set();
|
|
119
|
+
#approvals;
|
|
90
120
|
|
|
91
121
|
constructor({
|
|
92
122
|
client,
|
|
@@ -96,6 +126,7 @@ export class WecomHarnessBridge {
|
|
|
96
126
|
logger = console,
|
|
97
127
|
replyTimeoutMs = 600_000,
|
|
98
128
|
generateStreamId = generateReqId,
|
|
129
|
+
signal,
|
|
99
130
|
}) {
|
|
100
131
|
if (!client || typeof client.replyStream !== 'function' || typeof client.sendMessage !== 'function') {
|
|
101
132
|
throw new TypeError('Enterprise WeChat client is required');
|
|
@@ -108,6 +139,8 @@ export class WecomHarnessBridge {
|
|
|
108
139
|
this.#logger = logger;
|
|
109
140
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
110
141
|
this.#generateReqId = generateStreamId;
|
|
142
|
+
this.#signal = signal;
|
|
143
|
+
this.#approvals = new HarnessApprovalQueue({ label: 'wecom', logger });
|
|
111
144
|
}
|
|
112
145
|
|
|
113
146
|
get status() {
|
|
@@ -115,12 +148,94 @@ export class WecomHarnessBridge {
|
|
|
115
148
|
}
|
|
116
149
|
|
|
117
150
|
accept(frame) {
|
|
151
|
+
if (this.#signal?.aborted) return Promise.resolve();
|
|
152
|
+
const body = bodyOf(frame);
|
|
153
|
+
const messageId = nonEmptyString(body.msgid);
|
|
154
|
+
const senderId = nonEmptyString(body.from?.userid);
|
|
155
|
+
const chatId = body.chattype === 'group'
|
|
156
|
+
? nonEmptyString(body.chatid)
|
|
157
|
+
: senderId;
|
|
158
|
+
if (!messageId || !senderId || !chatId
|
|
159
|
+
|| !['single', 'group'].includes(body.chattype)
|
|
160
|
+
|| this.#state.hasSeen(messageId)
|
|
161
|
+
|| this.#acceptedMessageIds.has(messageId)) return Promise.resolve();
|
|
162
|
+
|
|
118
163
|
const key = conversationKey(frame);
|
|
164
|
+
this.#acceptedMessageIds.add(messageId);
|
|
165
|
+
const pending = this.#pendingInteractions.get(key);
|
|
166
|
+
const approval = this.#approvals.claimReply({
|
|
167
|
+
key,
|
|
168
|
+
actor: senderId,
|
|
169
|
+
messageId,
|
|
170
|
+
text: messageText(frame),
|
|
171
|
+
addressed: true,
|
|
172
|
+
hasPendingQuestion: Boolean(pending),
|
|
173
|
+
questionCompletion: pending?.submitting || pending?.claimedReplyMessageId
|
|
174
|
+
? pending.queue
|
|
175
|
+
: null,
|
|
176
|
+
isQuestionPending: () => this.#pendingInteractions.has(key),
|
|
177
|
+
send: (text) => this.#sendImmediate(frame, chatId, text),
|
|
178
|
+
});
|
|
179
|
+
if (approval) {
|
|
180
|
+
let task;
|
|
181
|
+
task = approval.process(async () => {
|
|
182
|
+
if (this.#state.hasSeen(messageId)) return false;
|
|
183
|
+
await this.#state.markSeen(messageId);
|
|
184
|
+
this.#status.messagesReceived += 1;
|
|
185
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
186
|
+
return true;
|
|
187
|
+
})
|
|
188
|
+
.finally(() => {
|
|
189
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
190
|
+
this.#approvalTasks.delete(task);
|
|
191
|
+
});
|
|
192
|
+
this.#approvalTasks.add(task);
|
|
193
|
+
return task;
|
|
194
|
+
}
|
|
195
|
+
if (pending && pending.actor !== senderId) {
|
|
196
|
+
return this.#enqueueMessage(frame, messageId, key);
|
|
197
|
+
}
|
|
198
|
+
if (pending?.submitting || pending?.claimedReplyMessageId) {
|
|
199
|
+
return this.#enqueueMessage(frame, messageId, key);
|
|
200
|
+
}
|
|
201
|
+
if (pending) {
|
|
202
|
+
if (canClaimInteractionReply(frame, pending)) {
|
|
203
|
+
pending.claimedReplyMessageId = messageId;
|
|
204
|
+
}
|
|
205
|
+
const previous = pending.queue ?? Promise.resolve();
|
|
206
|
+
const current = previous
|
|
207
|
+
.catch(() => undefined)
|
|
208
|
+
.then(() => this.#processInteractionReply(
|
|
209
|
+
frame,
|
|
210
|
+
messageId,
|
|
211
|
+
senderId,
|
|
212
|
+
chatId,
|
|
213
|
+
key,
|
|
214
|
+
pending,
|
|
215
|
+
))
|
|
216
|
+
.finally(() => {
|
|
217
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
218
|
+
if (pending.claimedReplyMessageId === messageId) {
|
|
219
|
+
pending.claimedReplyMessageId = null;
|
|
220
|
+
}
|
|
221
|
+
if (pending.queue === current) pending.queue = null;
|
|
222
|
+
});
|
|
223
|
+
pending.queue = current;
|
|
224
|
+
return current;
|
|
225
|
+
}
|
|
226
|
+
return this.#enqueueMessage(frame, messageId, key);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
#enqueueMessage(frame, messageId, key, {
|
|
230
|
+
releaseMessageId = true,
|
|
231
|
+
alreadyRecorded = false,
|
|
232
|
+
} = {}) {
|
|
119
233
|
const previous = this.#queues.get(key) ?? Promise.resolve();
|
|
120
234
|
const current = previous
|
|
121
235
|
.catch(() => undefined)
|
|
122
|
-
.then(() => this.#process(frame))
|
|
236
|
+
.then(() => this.#process(frame, { alreadyRecorded }))
|
|
123
237
|
.finally(() => {
|
|
238
|
+
if (releaseMessageId) this.#acceptedMessageIds.delete(messageId);
|
|
124
239
|
if (this.#queues.get(key) === current) this.#queues.delete(key);
|
|
125
240
|
});
|
|
126
241
|
this.#queues.set(key, current);
|
|
@@ -128,16 +243,24 @@ export class WecomHarnessBridge {
|
|
|
128
243
|
}
|
|
129
244
|
|
|
130
245
|
async waitForIdle() {
|
|
131
|
-
await Promise.allSettled([
|
|
246
|
+
await Promise.allSettled([
|
|
247
|
+
...this.#queues.values(),
|
|
248
|
+
...[...this.#pendingInteractions.values()].flatMap((pending) => (
|
|
249
|
+
pending.queue ? [pending.queue] : []
|
|
250
|
+
)),
|
|
251
|
+
...this.#approvalTasks,
|
|
252
|
+
]);
|
|
132
253
|
}
|
|
133
254
|
|
|
134
255
|
async #sendActive(chatId, text) {
|
|
135
256
|
for (const chunk of splitUtf8(text)) {
|
|
257
|
+
this.#signal?.throwIfAborted();
|
|
136
258
|
await this.#client.sendMessage(chatId, { msgtype: 'markdown', markdown: { content: chunk } });
|
|
137
259
|
}
|
|
138
260
|
}
|
|
139
261
|
|
|
140
262
|
async #sendImmediate(frame, chatId, text) {
|
|
263
|
+
this.#signal?.throwIfAborted();
|
|
141
264
|
const chunks = splitUtf8(text);
|
|
142
265
|
if (chunks.length === 0) return;
|
|
143
266
|
try {
|
|
@@ -150,17 +273,20 @@ export class WecomHarnessBridge {
|
|
|
150
273
|
}
|
|
151
274
|
}
|
|
152
275
|
|
|
153
|
-
async #process(frame) {
|
|
276
|
+
async #process(frame, { alreadyRecorded = false } = {}) {
|
|
277
|
+
if (this.#signal?.aborted) return;
|
|
154
278
|
const body = bodyOf(frame);
|
|
155
279
|
const messageId = typeof body.msgid === 'string' ? body.msgid : '';
|
|
156
280
|
const senderId = typeof body.from?.userid === 'string' ? body.from.userid : '';
|
|
157
281
|
const chatId = body.chattype === 'group' ? body.chatid : senderId;
|
|
158
282
|
if (!messageId || !senderId || !chatId || !['single', 'group'].includes(body.chattype)) return;
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
283
|
+
if (!alreadyRecorded) {
|
|
284
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
285
|
+
this.#status.messagesReceived += 1;
|
|
286
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
287
|
+
}
|
|
163
288
|
const text = messageText(frame);
|
|
289
|
+
const key = conversationKey(frame);
|
|
164
290
|
let streamId = null;
|
|
165
291
|
let streamStarted = false;
|
|
166
292
|
try {
|
|
@@ -176,12 +302,11 @@ export class WecomHarnessBridge {
|
|
|
176
302
|
return;
|
|
177
303
|
}
|
|
178
304
|
if (command === '/status') {
|
|
179
|
-
await this.#harness.ensureRunning();
|
|
305
|
+
await this.#harness.ensureRunning({ signal: this.#signal });
|
|
180
306
|
await this.#sendImmediate(frame, chatId, '企业微信机器人与 DeepSeek Harness 连接正常。');
|
|
181
307
|
await this.#state.markSeen(messageId);
|
|
182
308
|
return;
|
|
183
309
|
}
|
|
184
|
-
const key = conversationKey(frame);
|
|
185
310
|
if (command === '/new') {
|
|
186
311
|
await this.#state.clearSession(key);
|
|
187
312
|
await this.#sendImmediate(frame, chatId, '已开启新会话。请发送你的问题。');
|
|
@@ -210,14 +335,24 @@ export class WecomHarnessBridge {
|
|
|
210
335
|
state: this.#state,
|
|
211
336
|
key,
|
|
212
337
|
text,
|
|
338
|
+
createOptions: { signal: this.#signal },
|
|
339
|
+
existsOptions: { signal: this.#signal },
|
|
213
340
|
askOptions: {
|
|
214
341
|
timeoutMs: this.#replyTimeoutMs,
|
|
342
|
+
signal: this.#signal,
|
|
215
343
|
onUpdate: streamStarted && typeof this.#client.replyStreamNonBlocking === 'function'
|
|
216
344
|
? async (update) => {
|
|
217
345
|
const progress = splitUtf8(progressText(update))[0];
|
|
218
346
|
if (progress) await this.#client.replyStreamNonBlocking(frame, streamId, progress, false);
|
|
219
347
|
}
|
|
220
348
|
: undefined,
|
|
349
|
+
onInteraction: (interaction) => this.#handleInteraction(interaction, {
|
|
350
|
+
key,
|
|
351
|
+
actor: senderId,
|
|
352
|
+
chatId,
|
|
353
|
+
requiresMention: body.chattype === 'group',
|
|
354
|
+
}),
|
|
355
|
+
onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
|
|
221
356
|
},
|
|
222
357
|
});
|
|
223
358
|
|
|
@@ -240,6 +375,7 @@ export class WecomHarnessBridge {
|
|
|
240
375
|
this.#status.lastReplyAt = new Date().toISOString();
|
|
241
376
|
this.#status.lastError = null;
|
|
242
377
|
} catch (error) {
|
|
378
|
+
if (this.#signal?.aborted) return;
|
|
243
379
|
this.#status.lastError = error?.message ?? String(error);
|
|
244
380
|
this.#logger.error?.('[dsh-im:wecom] failed to process an inbound message');
|
|
245
381
|
try {
|
|
@@ -252,6 +388,290 @@ export class WecomHarnessBridge {
|
|
|
252
388
|
} catch {
|
|
253
389
|
this.#logger.error?.('[dsh-im:wecom] failed to send the safe error reply');
|
|
254
390
|
}
|
|
391
|
+
} finally {
|
|
392
|
+
await Promise.allSettled([
|
|
393
|
+
this.#cancelPendingInteraction(key),
|
|
394
|
+
this.#approvals.closeRoute(key),
|
|
395
|
+
]);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async #processInteractionReply(frame, messageId, senderId, chatId, key, expected) {
|
|
400
|
+
if (this.#signal?.aborted) return;
|
|
401
|
+
const current = this.#pendingInteractions.get(key);
|
|
402
|
+
const claimed = expected.claimedReplyMessageId === messageId;
|
|
403
|
+
if (!current || current !== expected || current.submitting) {
|
|
404
|
+
if (claimed && (!current || current !== expected)) {
|
|
405
|
+
return this.#discardResolvedInteractionReply(frame, messageId, chatId);
|
|
406
|
+
}
|
|
407
|
+
return this.#enqueueMessage(frame, messageId, key, { releaseMessageId: false });
|
|
408
|
+
}
|
|
409
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
410
|
+
await this.#state.markSeen(messageId);
|
|
411
|
+
this.#status.messagesReceived += 1;
|
|
412
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
413
|
+
|
|
414
|
+
const text = nonEmptyString(messageText(frame));
|
|
415
|
+
if (!text) {
|
|
416
|
+
await this.#sendImmediate(frame, chatId, '请用文字或语音回答当前问题。')
|
|
417
|
+
.catch(() => undefined);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const pending = this.#pendingInteractions.get(key);
|
|
422
|
+
if (!pending || pending !== expected || pending.submitting) {
|
|
423
|
+
if (claimed && (!pending || pending !== expected)) {
|
|
424
|
+
await this.#sendImmediate(frame, chatId, INTERACTION_RESOLVED_TEXT)
|
|
425
|
+
.catch(() => undefined);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
return this.#enqueueMessage(frame, messageId, key, {
|
|
429
|
+
releaseMessageId: false,
|
|
430
|
+
alreadyRecorded: true,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
if (pending.actor !== senderId) {
|
|
434
|
+
return this.#enqueueMessage(frame, messageId, key, {
|
|
435
|
+
releaseMessageId: false,
|
|
436
|
+
alreadyRecorded: true,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
pending.chatId = chatId;
|
|
441
|
+
if (pending.needsPresentation) {
|
|
442
|
+
try {
|
|
443
|
+
await this.#presentInteraction(pending);
|
|
444
|
+
} catch {
|
|
445
|
+
this.#status.lastError = '企业微信交互问题发送失败。';
|
|
446
|
+
this.#logger.error?.('[dsh-im:wecom] failed to retry an interaction question');
|
|
447
|
+
pending.interaction.reconnect?.();
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
const presentedPending = this.#pendingInteractions.get(key);
|
|
451
|
+
if (!presentedPending || presentedPending !== expected || presentedPending.submitting) {
|
|
452
|
+
if (claimed && (!presentedPending || presentedPending !== expected)) {
|
|
453
|
+
await this.#sendImmediate(frame, chatId, INTERACTION_RESOLVED_TEXT)
|
|
454
|
+
.catch(() => undefined);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
return this.#enqueueMessage(frame, messageId, key, {
|
|
458
|
+
releaseMessageId: false,
|
|
459
|
+
alreadyRecorded: true,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const question = pending.questions[pending.index];
|
|
465
|
+
if (!question) return;
|
|
466
|
+
pending.answers.push(harnessAnswerForQuestion(question, text));
|
|
467
|
+
pending.index += 1;
|
|
468
|
+
if (pending.index < pending.questions.length) {
|
|
469
|
+
if (pending.claimedReplyMessageId === messageId) {
|
|
470
|
+
pending.claimedReplyMessageId = null;
|
|
471
|
+
}
|
|
472
|
+
pending.needsPresentation = true;
|
|
473
|
+
try {
|
|
474
|
+
await this.#presentInteraction(pending);
|
|
475
|
+
} catch {
|
|
476
|
+
this.#status.lastError = '企业微信交互问题发送失败。';
|
|
477
|
+
this.#logger.error?.('[dsh-im:wecom] failed to send the next interaction question');
|
|
478
|
+
pending.interaction.reconnect?.();
|
|
479
|
+
}
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
pending.submitting = true;
|
|
484
|
+
try {
|
|
485
|
+
await pending.interaction.respond({
|
|
486
|
+
ok: true,
|
|
487
|
+
value: {
|
|
488
|
+
sessionId: pending.sessionId,
|
|
489
|
+
answer: { answers: pending.answers },
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
this.#clearPendingInteraction(key, pending.interactionId);
|
|
493
|
+
this.#status.lastError = null;
|
|
494
|
+
} catch (error) {
|
|
495
|
+
if (this.#signal?.aborted) return;
|
|
496
|
+
if (error?.code === 'interaction-not-pending') {
|
|
497
|
+
if (this.#pendingInteractions.get(key) === pending) {
|
|
498
|
+
this.#clearPendingInteraction(key, pending.interactionId);
|
|
499
|
+
}
|
|
500
|
+
await this.#sendImmediate(frame, chatId, INTERACTION_RESOLVED_TEXT)
|
|
501
|
+
.catch(() => undefined);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (this.#pendingInteractions.get(key) !== pending) return;
|
|
505
|
+
pending.submitting = false;
|
|
506
|
+
pending.answers.pop();
|
|
507
|
+
pending.index -= 1;
|
|
508
|
+
this.#status.lastError = '回答提交失败。';
|
|
509
|
+
this.#logger.error?.('[dsh-im:wecom] failed to answer a Harness interaction');
|
|
510
|
+
await this.#sendImmediate(frame, chatId, '回答提交失败,请重新发送当前问题的答案。')
|
|
511
|
+
.catch(() => undefined);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async #handleInteraction(interaction, {
|
|
516
|
+
key,
|
|
517
|
+
actor,
|
|
518
|
+
chatId,
|
|
519
|
+
requiresMention,
|
|
520
|
+
}) {
|
|
521
|
+
if (interaction?.kind === 'approval') {
|
|
522
|
+
return this.#approvals.handleRequested(interaction, {
|
|
523
|
+
key,
|
|
524
|
+
actor,
|
|
525
|
+
requiresMention,
|
|
526
|
+
send: (text) => this.#sendActive(chatId, text),
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
if (interaction?.kind !== 'question') return;
|
|
530
|
+
const questions = interaction?.payload?.questions;
|
|
531
|
+
const interactionId = typeof interaction?.interactionId === 'string'
|
|
532
|
+
? interaction.interactionId
|
|
533
|
+
: interaction?.rpcId;
|
|
534
|
+
if (typeof interaction?.rpcId !== 'string'
|
|
535
|
+
|| typeof interactionId !== 'string'
|
|
536
|
+
|| typeof interaction.sessionId !== 'string'
|
|
537
|
+
|| !Array.isArray(questions)
|
|
538
|
+
|| questions.length === 0
|
|
539
|
+
|| questions.some((question) => !validHarnessQuestion(question))) {
|
|
540
|
+
this.#logger.warn?.('[dsh-im:wecom] ignored an invalid Harness question interaction');
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (interaction.recovered === true) {
|
|
545
|
+
await interaction.respond({
|
|
546
|
+
ok: false,
|
|
547
|
+
error: {
|
|
548
|
+
code: 'cancelled',
|
|
549
|
+
message: 'Enterprise WeChat safely cancelled an interaction left by an earlier client.',
|
|
550
|
+
details: {},
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
await this.#sendActive(
|
|
554
|
+
chatId,
|
|
555
|
+
'检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。',
|
|
556
|
+
).catch(() => undefined);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const existing = this.#pendingInteractions.get(key);
|
|
561
|
+
if (existing?.interactionId === interactionId) {
|
|
562
|
+
existing.interaction = interaction;
|
|
563
|
+
if (existing.needsPresentation) await this.#presentInteraction(existing);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (this.#interactionKeys.has(interactionId)) return;
|
|
567
|
+
if (existing) {
|
|
568
|
+
this.#logger.warn?.('[dsh-im:wecom] cancelled a second pending Harness question');
|
|
569
|
+
await interaction.respond({
|
|
570
|
+
ok: false,
|
|
571
|
+
error: {
|
|
572
|
+
code: 'cancelled',
|
|
573
|
+
message: 'Enterprise WeChat is already handling another user interaction.',
|
|
574
|
+
details: {},
|
|
575
|
+
},
|
|
576
|
+
});
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const pending = {
|
|
581
|
+
kind: 'question',
|
|
582
|
+
interactionId,
|
|
583
|
+
sessionId: interaction.sessionId,
|
|
584
|
+
interaction,
|
|
585
|
+
actor,
|
|
586
|
+
requiresMention,
|
|
587
|
+
questions,
|
|
588
|
+
answers: [],
|
|
589
|
+
index: 0,
|
|
590
|
+
chatId,
|
|
591
|
+
queue: null,
|
|
592
|
+
claimedReplyMessageId: null,
|
|
593
|
+
submitting: false,
|
|
594
|
+
needsPresentation: true,
|
|
595
|
+
presentationPromise: null,
|
|
596
|
+
};
|
|
597
|
+
this.#pendingInteractions.set(key, pending);
|
|
598
|
+
this.#interactionKeys.set(pending.interactionId, key);
|
|
599
|
+
await this.#presentInteraction(pending);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async #handleInteractionResolved(resolution) {
|
|
603
|
+
if (resolution?.kind === 'approval') {
|
|
604
|
+
await this.#approvals.handleResolved(resolution);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const interactionId = resolution?.interactionId;
|
|
608
|
+
if (resolution?.kind !== 'question' || typeof interactionId !== 'string') return;
|
|
609
|
+
const key = this.#interactionKeys.get(interactionId);
|
|
610
|
+
if (!key) return;
|
|
611
|
+
this.#clearPendingInteraction(key, interactionId);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
#presentInteraction(pending) {
|
|
615
|
+
if (!pending.needsPresentation) return Promise.resolve();
|
|
616
|
+
if (pending.presentationPromise) return pending.presentationPromise;
|
|
617
|
+
const question = pending.questions[pending.index];
|
|
618
|
+
if (!question) return Promise.resolve();
|
|
619
|
+
const presentation = this.#sendActive(
|
|
620
|
+
pending.chatId,
|
|
621
|
+
harnessQuestionText(
|
|
622
|
+
question,
|
|
623
|
+
pending.index,
|
|
624
|
+
pending.questions.length,
|
|
625
|
+
{ requiresMention: pending.requiresMention },
|
|
626
|
+
),
|
|
627
|
+
).then(() => {
|
|
628
|
+
pending.needsPresentation = false;
|
|
629
|
+
}).finally(() => {
|
|
630
|
+
if (pending.presentationPromise === presentation) {
|
|
631
|
+
pending.presentationPromise = null;
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
pending.presentationPromise = presentation;
|
|
635
|
+
return presentation;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async #discardResolvedInteractionReply(frame, messageId, chatId) {
|
|
639
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
640
|
+
await this.#state.markSeen(messageId);
|
|
641
|
+
this.#status.messagesReceived += 1;
|
|
642
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
643
|
+
await this.#sendImmediate(frame, chatId, INTERACTION_RESOLVED_TEXT).catch(() => undefined);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
#takePendingInteraction(key, interactionId) {
|
|
647
|
+
const pending = this.#pendingInteractions.get(key);
|
|
648
|
+
if (!pending
|
|
649
|
+
|| (interactionId !== undefined && pending.interactionId !== interactionId)) return null;
|
|
650
|
+
this.#pendingInteractions.delete(key);
|
|
651
|
+
this.#interactionKeys.delete(pending.interactionId);
|
|
652
|
+
return pending;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
#clearPendingInteraction(key, interactionId) {
|
|
656
|
+
return this.#takePendingInteraction(key, interactionId) !== null;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async #cancelPendingInteraction(key) {
|
|
660
|
+
const pending = this.#takePendingInteraction(key);
|
|
661
|
+
if (!pending || pending.kind !== 'question') return;
|
|
662
|
+
try {
|
|
663
|
+
await pending.interaction.respond({
|
|
664
|
+
ok: false,
|
|
665
|
+
error: {
|
|
666
|
+
code: 'cancelled',
|
|
667
|
+
message: 'The Enterprise WeChat interaction ended before the user answered.',
|
|
668
|
+
details: {},
|
|
669
|
+
},
|
|
670
|
+
}, { signal: AbortSignal.timeout(5_000) });
|
|
671
|
+
} catch (error) {
|
|
672
|
+
if (error?.code !== 'interaction-not-pending') {
|
|
673
|
+
this.#logger.warn?.('[dsh-im:wecom] failed to cancel a pending Harness interaction');
|
|
674
|
+
}
|
|
255
675
|
}
|
|
256
676
|
}
|
|
257
677
|
}
|
|
@@ -36,6 +36,7 @@ export class WecomRuntime {
|
|
|
36
36
|
#bridge = null;
|
|
37
37
|
#starting = null;
|
|
38
38
|
#startController = null;
|
|
39
|
+
#runtimeController = null;
|
|
39
40
|
|
|
40
41
|
constructor({
|
|
41
42
|
config,
|
|
@@ -69,8 +70,10 @@ export class WecomRuntime {
|
|
|
69
70
|
async start() {
|
|
70
71
|
if (this.#status.ready && this.#client) return this.status;
|
|
71
72
|
if (this.#starting) return this.#starting;
|
|
73
|
+
this.#runtimeController?.abort(new DOMException('Enterprise WeChat runtime replaced', 'AbortError'));
|
|
72
74
|
const controller = new AbortController();
|
|
73
75
|
this.#startController = controller;
|
|
76
|
+
this.#runtimeController = controller;
|
|
74
77
|
this.#starting = this.#start(controller.signal).finally(() => {
|
|
75
78
|
if (this.#startController === controller) this.#startController = null;
|
|
76
79
|
this.#starting = null;
|
|
@@ -105,6 +108,7 @@ export class WecomRuntime {
|
|
|
105
108
|
status: this.#status,
|
|
106
109
|
logger: this.#logger,
|
|
107
110
|
replyTimeoutMs: this.#replyTimeoutMs,
|
|
111
|
+
signal,
|
|
108
112
|
});
|
|
109
113
|
|
|
110
114
|
let readyResolve;
|
|
@@ -186,6 +190,8 @@ export class WecomRuntime {
|
|
|
186
190
|
async stop() {
|
|
187
191
|
const starting = this.#starting;
|
|
188
192
|
this.#startController?.abort(new DOMException('Enterprise WeChat runtime stopped', 'AbortError'));
|
|
193
|
+
this.#runtimeController?.abort(new DOMException('Enterprise WeChat runtime stopped', 'AbortError'));
|
|
194
|
+
this.#runtimeController = null;
|
|
189
195
|
await this.#stopActive();
|
|
190
196
|
await starting?.catch(() => undefined);
|
|
191
197
|
return this.status;
|