@xmanrui/dsh-im 0.7.0 → 0.7.1
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 +118 -121
- package/package.json +1 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +360 -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 +522 -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 +380 -28
- package/src/channels/qq/qq-runtime.mjs +14 -3
- package/src/channels/shared/harness-client.mjs +825 -0
- package/src/channels/shared/harness-question.mjs +85 -0
- package/src/channels/shared/text-harness-bridge.mjs +436 -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 +386 -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 +402 -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
|
@@ -309,6 +309,7 @@ export class SlackRuntime {
|
|
|
309
309
|
status: this.#status,
|
|
310
310
|
logger: this.#logger,
|
|
311
311
|
replyTimeoutMs: this.#replyTimeoutMs,
|
|
312
|
+
signal: controller.signal,
|
|
312
313
|
});
|
|
313
314
|
let timer;
|
|
314
315
|
try {
|
|
@@ -389,7 +390,16 @@ export class SlackRuntime {
|
|
|
389
390
|
if (this.#appId && packet.payload.api_app_id
|
|
390
391
|
&& packet.payload.api_app_id !== this.#appId) return;
|
|
391
392
|
const message = normalizeSlackEvent(packet.payload, this.#config.platformId.split(':')[1]);
|
|
392
|
-
|
|
393
|
+
const bridge = this.#bridge;
|
|
394
|
+
if (message && bridge) {
|
|
395
|
+
void bridge.accept(message).catch((error) => {
|
|
396
|
+
if (generation !== this.#generation || this.#stopped) return;
|
|
397
|
+
this.#logger.error?.(
|
|
398
|
+
`[dsh-im:slack] bot ${this.#config.botId} message handling failed:`,
|
|
399
|
+
error,
|
|
400
|
+
);
|
|
401
|
+
});
|
|
402
|
+
}
|
|
393
403
|
});
|
|
394
404
|
|
|
395
405
|
addSocketListener(socket, 'close', (event = {}) => {
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
import { HarnessClient } from '../
|
|
1
|
+
import { HarnessClient } from '../shared/harness-client.mjs';
|
|
2
2
|
|
|
3
|
-
export class TelegramHarnessClient extends HarnessClient {
|
|
3
|
+
export class TelegramHarnessClient extends HarnessClient {
|
|
4
|
+
constructor(options) {
|
|
5
|
+
super({
|
|
6
|
+
...options,
|
|
7
|
+
rpcIdPrefix: 'telegram',
|
|
8
|
+
logPrefix: 'dsh-telegram',
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -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,9 @@
|
|
|
1
1
|
import { generateReqId } from '@wecom/aibot-node-sdk';
|
|
2
|
+
import {
|
|
3
|
+
harnessAnswerForQuestion,
|
|
4
|
+
harnessQuestionText,
|
|
5
|
+
validHarnessQuestion,
|
|
6
|
+
} from '../shared/harness-question.mjs';
|
|
2
7
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
3
8
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
4
9
|
|
|
@@ -15,6 +20,11 @@ const HELP_TEXT = [
|
|
|
15
20
|
'/help 显示本帮助',
|
|
16
21
|
].join('\n');
|
|
17
22
|
const MAX_REPLY_BYTES = 18_000;
|
|
23
|
+
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
|
24
|
+
|
|
25
|
+
function nonEmptyString(value) {
|
|
26
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
27
|
+
}
|
|
18
28
|
|
|
19
29
|
function bodyOf(frame) {
|
|
20
30
|
return frame?.body && typeof frame.body === 'object' ? frame.body : {};
|
|
@@ -27,16 +37,23 @@ function conversationKey(frame) {
|
|
|
27
37
|
|
|
28
38
|
function messageText(frame) {
|
|
29
39
|
const body = bodyOf(frame);
|
|
30
|
-
|
|
31
|
-
if (body.msgtype === '
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
let text = '';
|
|
41
|
+
if (body.msgtype === 'text') {
|
|
42
|
+
text = typeof body.text?.content === 'string' ? body.text.content.trim() : '';
|
|
43
|
+
} else if (body.msgtype === 'voice') {
|
|
44
|
+
text = typeof body.voice?.content === 'string' ? body.voice.content.trim() : '';
|
|
45
|
+
} else if (body.msgtype === 'mixed' && Array.isArray(body.mixed?.msg_item)) {
|
|
46
|
+
text = body.mixed.msg_item
|
|
34
47
|
.filter((item) => item?.msgtype === 'text' && typeof item.text?.content === 'string')
|
|
35
48
|
.map((item) => item.text.content)
|
|
36
49
|
.join('\n')
|
|
37
50
|
.trim();
|
|
38
51
|
}
|
|
39
|
-
|
|
52
|
+
// Group callbacks retain the leading @bot mention that caused delivery.
|
|
53
|
+
// It is routing metadata rather than part of the user's prompt or answer.
|
|
54
|
+
return body.chattype === 'group'
|
|
55
|
+
? text.replace(/^\s*@\S+(?:\s+|$)/u, '').trim()
|
|
56
|
+
: text;
|
|
40
57
|
}
|
|
41
58
|
|
|
42
59
|
function splitUtf8(text, maxBytes = MAX_REPLY_BYTES) {
|
|
@@ -66,6 +83,12 @@ function progressText(update) {
|
|
|
66
83
|
return update?.text;
|
|
67
84
|
}
|
|
68
85
|
|
|
86
|
+
function canClaimInteractionReply(frame, pending) {
|
|
87
|
+
return pending.questions[pending.index]
|
|
88
|
+
&& nonEmptyString(bodyOf(frame).from?.userid) === pending.actor
|
|
89
|
+
&& nonEmptyString(messageText(frame));
|
|
90
|
+
}
|
|
91
|
+
|
|
69
92
|
export function createWecomBridgeStatus() {
|
|
70
93
|
return {
|
|
71
94
|
messagesReceived: 0,
|
|
@@ -86,7 +109,11 @@ export class WecomHarnessBridge {
|
|
|
86
109
|
#logger;
|
|
87
110
|
#replyTimeoutMs;
|
|
88
111
|
#generateReqId;
|
|
112
|
+
#signal;
|
|
89
113
|
#queues = new Map();
|
|
114
|
+
#pendingInteractions = new Map();
|
|
115
|
+
#interactionKeys = new Map();
|
|
116
|
+
#acceptedMessageIds = new Set();
|
|
90
117
|
|
|
91
118
|
constructor({
|
|
92
119
|
client,
|
|
@@ -96,6 +123,7 @@ export class WecomHarnessBridge {
|
|
|
96
123
|
logger = console,
|
|
97
124
|
replyTimeoutMs = 600_000,
|
|
98
125
|
generateStreamId = generateReqId,
|
|
126
|
+
signal,
|
|
99
127
|
}) {
|
|
100
128
|
if (!client || typeof client.replyStream !== 'function' || typeof client.sendMessage !== 'function') {
|
|
101
129
|
throw new TypeError('Enterprise WeChat client is required');
|
|
@@ -108,6 +136,7 @@ export class WecomHarnessBridge {
|
|
|
108
136
|
this.#logger = logger;
|
|
109
137
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
110
138
|
this.#generateReqId = generateStreamId;
|
|
139
|
+
this.#signal = signal;
|
|
111
140
|
}
|
|
112
141
|
|
|
113
142
|
get status() {
|
|
@@ -115,12 +144,65 @@ export class WecomHarnessBridge {
|
|
|
115
144
|
}
|
|
116
145
|
|
|
117
146
|
accept(frame) {
|
|
147
|
+
if (this.#signal?.aborted) return Promise.resolve();
|
|
148
|
+
const body = bodyOf(frame);
|
|
149
|
+
const messageId = nonEmptyString(body.msgid);
|
|
150
|
+
const senderId = nonEmptyString(body.from?.userid);
|
|
151
|
+
const chatId = body.chattype === 'group'
|
|
152
|
+
? nonEmptyString(body.chatid)
|
|
153
|
+
: senderId;
|
|
154
|
+
if (!messageId || !senderId || !chatId
|
|
155
|
+
|| !['single', 'group'].includes(body.chattype)
|
|
156
|
+
|| this.#state.hasSeen(messageId)
|
|
157
|
+
|| this.#acceptedMessageIds.has(messageId)) return Promise.resolve();
|
|
158
|
+
|
|
118
159
|
const key = conversationKey(frame);
|
|
160
|
+
this.#acceptedMessageIds.add(messageId);
|
|
161
|
+
const pending = this.#pendingInteractions.get(key);
|
|
162
|
+
if (pending && pending.actor !== senderId) {
|
|
163
|
+
return this.#enqueueMessage(frame, messageId, key);
|
|
164
|
+
}
|
|
165
|
+
if (pending?.submitting || pending?.claimedReplyMessageId) {
|
|
166
|
+
return this.#enqueueMessage(frame, messageId, key);
|
|
167
|
+
}
|
|
168
|
+
if (pending) {
|
|
169
|
+
if (canClaimInteractionReply(frame, pending)) {
|
|
170
|
+
pending.claimedReplyMessageId = messageId;
|
|
171
|
+
}
|
|
172
|
+
const previous = pending.queue ?? Promise.resolve();
|
|
173
|
+
const current = previous
|
|
174
|
+
.catch(() => undefined)
|
|
175
|
+
.then(() => this.#processInteractionReply(
|
|
176
|
+
frame,
|
|
177
|
+
messageId,
|
|
178
|
+
senderId,
|
|
179
|
+
chatId,
|
|
180
|
+
key,
|
|
181
|
+
pending,
|
|
182
|
+
))
|
|
183
|
+
.finally(() => {
|
|
184
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
185
|
+
if (pending.claimedReplyMessageId === messageId) {
|
|
186
|
+
pending.claimedReplyMessageId = null;
|
|
187
|
+
}
|
|
188
|
+
if (pending.queue === current) pending.queue = null;
|
|
189
|
+
});
|
|
190
|
+
pending.queue = current;
|
|
191
|
+
return current;
|
|
192
|
+
}
|
|
193
|
+
return this.#enqueueMessage(frame, messageId, key);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
#enqueueMessage(frame, messageId, key, {
|
|
197
|
+
releaseMessageId = true,
|
|
198
|
+
alreadyRecorded = false,
|
|
199
|
+
} = {}) {
|
|
119
200
|
const previous = this.#queues.get(key) ?? Promise.resolve();
|
|
120
201
|
const current = previous
|
|
121
202
|
.catch(() => undefined)
|
|
122
|
-
.then(() => this.#process(frame))
|
|
203
|
+
.then(() => this.#process(frame, { alreadyRecorded }))
|
|
123
204
|
.finally(() => {
|
|
205
|
+
if (releaseMessageId) this.#acceptedMessageIds.delete(messageId);
|
|
124
206
|
if (this.#queues.get(key) === current) this.#queues.delete(key);
|
|
125
207
|
});
|
|
126
208
|
this.#queues.set(key, current);
|
|
@@ -128,16 +210,23 @@ export class WecomHarnessBridge {
|
|
|
128
210
|
}
|
|
129
211
|
|
|
130
212
|
async waitForIdle() {
|
|
131
|
-
await Promise.allSettled([
|
|
213
|
+
await Promise.allSettled([
|
|
214
|
+
...this.#queues.values(),
|
|
215
|
+
...[...this.#pendingInteractions.values()].flatMap((pending) => (
|
|
216
|
+
pending.queue ? [pending.queue] : []
|
|
217
|
+
)),
|
|
218
|
+
]);
|
|
132
219
|
}
|
|
133
220
|
|
|
134
221
|
async #sendActive(chatId, text) {
|
|
135
222
|
for (const chunk of splitUtf8(text)) {
|
|
223
|
+
this.#signal?.throwIfAborted();
|
|
136
224
|
await this.#client.sendMessage(chatId, { msgtype: 'markdown', markdown: { content: chunk } });
|
|
137
225
|
}
|
|
138
226
|
}
|
|
139
227
|
|
|
140
228
|
async #sendImmediate(frame, chatId, text) {
|
|
229
|
+
this.#signal?.throwIfAborted();
|
|
141
230
|
const chunks = splitUtf8(text);
|
|
142
231
|
if (chunks.length === 0) return;
|
|
143
232
|
try {
|
|
@@ -150,17 +239,20 @@ export class WecomHarnessBridge {
|
|
|
150
239
|
}
|
|
151
240
|
}
|
|
152
241
|
|
|
153
|
-
async #process(frame) {
|
|
242
|
+
async #process(frame, { alreadyRecorded = false } = {}) {
|
|
243
|
+
if (this.#signal?.aborted) return;
|
|
154
244
|
const body = bodyOf(frame);
|
|
155
245
|
const messageId = typeof body.msgid === 'string' ? body.msgid : '';
|
|
156
246
|
const senderId = typeof body.from?.userid === 'string' ? body.from.userid : '';
|
|
157
247
|
const chatId = body.chattype === 'group' ? body.chatid : senderId;
|
|
158
248
|
if (!messageId || !senderId || !chatId || !['single', 'group'].includes(body.chattype)) return;
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
249
|
+
if (!alreadyRecorded) {
|
|
250
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
251
|
+
this.#status.messagesReceived += 1;
|
|
252
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
253
|
+
}
|
|
163
254
|
const text = messageText(frame);
|
|
255
|
+
const key = conversationKey(frame);
|
|
164
256
|
let streamId = null;
|
|
165
257
|
let streamStarted = false;
|
|
166
258
|
try {
|
|
@@ -176,12 +268,11 @@ export class WecomHarnessBridge {
|
|
|
176
268
|
return;
|
|
177
269
|
}
|
|
178
270
|
if (command === '/status') {
|
|
179
|
-
await this.#harness.ensureRunning();
|
|
271
|
+
await this.#harness.ensureRunning({ signal: this.#signal });
|
|
180
272
|
await this.#sendImmediate(frame, chatId, '企业微信机器人与 DeepSeek Harness 连接正常。');
|
|
181
273
|
await this.#state.markSeen(messageId);
|
|
182
274
|
return;
|
|
183
275
|
}
|
|
184
|
-
const key = conversationKey(frame);
|
|
185
276
|
if (command === '/new') {
|
|
186
277
|
await this.#state.clearSession(key);
|
|
187
278
|
await this.#sendImmediate(frame, chatId, '已开启新会话。请发送你的问题。');
|
|
@@ -210,14 +301,24 @@ export class WecomHarnessBridge {
|
|
|
210
301
|
state: this.#state,
|
|
211
302
|
key,
|
|
212
303
|
text,
|
|
304
|
+
createOptions: { signal: this.#signal },
|
|
305
|
+
existsOptions: { signal: this.#signal },
|
|
213
306
|
askOptions: {
|
|
214
307
|
timeoutMs: this.#replyTimeoutMs,
|
|
308
|
+
signal: this.#signal,
|
|
215
309
|
onUpdate: streamStarted && typeof this.#client.replyStreamNonBlocking === 'function'
|
|
216
310
|
? async (update) => {
|
|
217
311
|
const progress = splitUtf8(progressText(update))[0];
|
|
218
312
|
if (progress) await this.#client.replyStreamNonBlocking(frame, streamId, progress, false);
|
|
219
313
|
}
|
|
220
314
|
: undefined,
|
|
315
|
+
onInteraction: (interaction) => this.#handleInteraction(interaction, {
|
|
316
|
+
key,
|
|
317
|
+
actor: senderId,
|
|
318
|
+
chatId,
|
|
319
|
+
requiresMention: body.chattype === 'group',
|
|
320
|
+
}),
|
|
321
|
+
onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
|
|
221
322
|
},
|
|
222
323
|
});
|
|
223
324
|
|
|
@@ -240,6 +341,7 @@ export class WecomHarnessBridge {
|
|
|
240
341
|
this.#status.lastReplyAt = new Date().toISOString();
|
|
241
342
|
this.#status.lastError = null;
|
|
242
343
|
} catch (error) {
|
|
344
|
+
if (this.#signal?.aborted) return;
|
|
243
345
|
this.#status.lastError = error?.message ?? String(error);
|
|
244
346
|
this.#logger.error?.('[dsh-im:wecom] failed to process an inbound message');
|
|
245
347
|
try {
|
|
@@ -252,6 +354,276 @@ export class WecomHarnessBridge {
|
|
|
252
354
|
} catch {
|
|
253
355
|
this.#logger.error?.('[dsh-im:wecom] failed to send the safe error reply');
|
|
254
356
|
}
|
|
357
|
+
} finally {
|
|
358
|
+
await this.#cancelPendingInteraction(key);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async #processInteractionReply(frame, messageId, senderId, chatId, key, expected) {
|
|
363
|
+
if (this.#signal?.aborted) return;
|
|
364
|
+
const current = this.#pendingInteractions.get(key);
|
|
365
|
+
const claimed = expected.claimedReplyMessageId === messageId;
|
|
366
|
+
if (!current || current !== expected || current.submitting) {
|
|
367
|
+
if (claimed && (!current || current !== expected)) {
|
|
368
|
+
return this.#discardResolvedInteractionReply(frame, messageId, chatId);
|
|
369
|
+
}
|
|
370
|
+
return this.#enqueueMessage(frame, messageId, key, { releaseMessageId: false });
|
|
371
|
+
}
|
|
372
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
373
|
+
await this.#state.markSeen(messageId);
|
|
374
|
+
this.#status.messagesReceived += 1;
|
|
375
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
376
|
+
|
|
377
|
+
const text = nonEmptyString(messageText(frame));
|
|
378
|
+
if (!text) {
|
|
379
|
+
await this.#sendImmediate(frame, chatId, '请用文字或语音回答当前问题。')
|
|
380
|
+
.catch(() => undefined);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const pending = this.#pendingInteractions.get(key);
|
|
385
|
+
if (!pending || pending !== expected || pending.submitting) {
|
|
386
|
+
if (claimed && (!pending || pending !== expected)) {
|
|
387
|
+
await this.#sendImmediate(frame, chatId, INTERACTION_RESOLVED_TEXT)
|
|
388
|
+
.catch(() => undefined);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
return this.#enqueueMessage(frame, messageId, key, {
|
|
392
|
+
releaseMessageId: false,
|
|
393
|
+
alreadyRecorded: true,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
if (pending.actor !== senderId) {
|
|
397
|
+
return this.#enqueueMessage(frame, messageId, key, {
|
|
398
|
+
releaseMessageId: false,
|
|
399
|
+
alreadyRecorded: true,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
pending.chatId = chatId;
|
|
404
|
+
if (pending.needsPresentation) {
|
|
405
|
+
try {
|
|
406
|
+
await this.#presentInteraction(pending);
|
|
407
|
+
} catch {
|
|
408
|
+
this.#status.lastError = '企业微信交互问题发送失败。';
|
|
409
|
+
this.#logger.error?.('[dsh-im:wecom] failed to retry an interaction question');
|
|
410
|
+
pending.interaction.reconnect?.();
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const presentedPending = this.#pendingInteractions.get(key);
|
|
414
|
+
if (!presentedPending || presentedPending !== expected || presentedPending.submitting) {
|
|
415
|
+
if (claimed && (!presentedPending || presentedPending !== expected)) {
|
|
416
|
+
await this.#sendImmediate(frame, chatId, INTERACTION_RESOLVED_TEXT)
|
|
417
|
+
.catch(() => undefined);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
return this.#enqueueMessage(frame, messageId, key, {
|
|
421
|
+
releaseMessageId: false,
|
|
422
|
+
alreadyRecorded: true,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const question = pending.questions[pending.index];
|
|
428
|
+
if (!question) return;
|
|
429
|
+
pending.answers.push(harnessAnswerForQuestion(question, text));
|
|
430
|
+
pending.index += 1;
|
|
431
|
+
if (pending.index < pending.questions.length) {
|
|
432
|
+
if (pending.claimedReplyMessageId === messageId) {
|
|
433
|
+
pending.claimedReplyMessageId = null;
|
|
434
|
+
}
|
|
435
|
+
pending.needsPresentation = true;
|
|
436
|
+
try {
|
|
437
|
+
await this.#presentInteraction(pending);
|
|
438
|
+
} catch {
|
|
439
|
+
this.#status.lastError = '企业微信交互问题发送失败。';
|
|
440
|
+
this.#logger.error?.('[dsh-im:wecom] failed to send the next interaction question');
|
|
441
|
+
pending.interaction.reconnect?.();
|
|
442
|
+
}
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
pending.submitting = true;
|
|
447
|
+
try {
|
|
448
|
+
await pending.interaction.respond({
|
|
449
|
+
ok: true,
|
|
450
|
+
value: {
|
|
451
|
+
sessionId: pending.sessionId,
|
|
452
|
+
answer: { answers: pending.answers },
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
this.#clearPendingInteraction(key, pending.interactionId);
|
|
456
|
+
this.#status.lastError = null;
|
|
457
|
+
} catch (error) {
|
|
458
|
+
if (this.#signal?.aborted) return;
|
|
459
|
+
if (error?.code === 'interaction-not-pending') {
|
|
460
|
+
if (this.#pendingInteractions.get(key) === pending) {
|
|
461
|
+
this.#clearPendingInteraction(key, pending.interactionId);
|
|
462
|
+
}
|
|
463
|
+
await this.#sendImmediate(frame, chatId, INTERACTION_RESOLVED_TEXT)
|
|
464
|
+
.catch(() => undefined);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (this.#pendingInteractions.get(key) !== pending) return;
|
|
468
|
+
pending.submitting = false;
|
|
469
|
+
pending.answers.pop();
|
|
470
|
+
pending.index -= 1;
|
|
471
|
+
this.#status.lastError = '回答提交失败。';
|
|
472
|
+
this.#logger.error?.('[dsh-im:wecom] failed to answer a Harness interaction');
|
|
473
|
+
await this.#sendImmediate(frame, chatId, '回答提交失败,请重新发送当前问题的答案。')
|
|
474
|
+
.catch(() => undefined);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async #handleInteraction(interaction, {
|
|
479
|
+
key,
|
|
480
|
+
actor,
|
|
481
|
+
chatId,
|
|
482
|
+
requiresMention,
|
|
483
|
+
}) {
|
|
484
|
+
// Approval remains unanswered until #5 supplies an authenticated policy.
|
|
485
|
+
if (interaction?.kind !== 'question') return;
|
|
486
|
+
const questions = interaction?.payload?.questions;
|
|
487
|
+
const interactionId = typeof interaction?.interactionId === 'string'
|
|
488
|
+
? interaction.interactionId
|
|
489
|
+
: interaction?.rpcId;
|
|
490
|
+
if (typeof interaction?.rpcId !== 'string'
|
|
491
|
+
|| typeof interactionId !== 'string'
|
|
492
|
+
|| typeof interaction.sessionId !== 'string'
|
|
493
|
+
|| !Array.isArray(questions)
|
|
494
|
+
|| questions.length === 0
|
|
495
|
+
|| questions.some((question) => !validHarnessQuestion(question))) {
|
|
496
|
+
this.#logger.warn?.('[dsh-im:wecom] ignored an invalid Harness question interaction');
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (interaction.recovered === true) {
|
|
501
|
+
await interaction.respond({
|
|
502
|
+
ok: false,
|
|
503
|
+
error: {
|
|
504
|
+
code: 'cancelled',
|
|
505
|
+
message: 'Enterprise WeChat safely cancelled an interaction left by an earlier client.',
|
|
506
|
+
details: {},
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
await this.#sendActive(
|
|
510
|
+
chatId,
|
|
511
|
+
'检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。',
|
|
512
|
+
).catch(() => undefined);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const existing = this.#pendingInteractions.get(key);
|
|
517
|
+
if (existing?.interactionId === interactionId) {
|
|
518
|
+
existing.interaction = interaction;
|
|
519
|
+
if (existing.needsPresentation) await this.#presentInteraction(existing);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (this.#interactionKeys.has(interactionId)) return;
|
|
523
|
+
if (existing) {
|
|
524
|
+
this.#logger.warn?.('[dsh-im:wecom] cancelled a second pending Harness question');
|
|
525
|
+
await interaction.respond({
|
|
526
|
+
ok: false,
|
|
527
|
+
error: {
|
|
528
|
+
code: 'cancelled',
|
|
529
|
+
message: 'Enterprise WeChat is already handling another user interaction.',
|
|
530
|
+
details: {},
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const pending = {
|
|
537
|
+
kind: 'question',
|
|
538
|
+
interactionId,
|
|
539
|
+
sessionId: interaction.sessionId,
|
|
540
|
+
interaction,
|
|
541
|
+
actor,
|
|
542
|
+
requiresMention,
|
|
543
|
+
questions,
|
|
544
|
+
answers: [],
|
|
545
|
+
index: 0,
|
|
546
|
+
chatId,
|
|
547
|
+
queue: null,
|
|
548
|
+
claimedReplyMessageId: null,
|
|
549
|
+
submitting: false,
|
|
550
|
+
needsPresentation: true,
|
|
551
|
+
presentationPromise: null,
|
|
552
|
+
};
|
|
553
|
+
this.#pendingInteractions.set(key, pending);
|
|
554
|
+
this.#interactionKeys.set(pending.interactionId, key);
|
|
555
|
+
await this.#presentInteraction(pending);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
#handleInteractionResolved(resolution) {
|
|
559
|
+
const interactionId = resolution?.interactionId;
|
|
560
|
+
if (resolution?.kind !== 'question' || typeof interactionId !== 'string') return;
|
|
561
|
+
const key = this.#interactionKeys.get(interactionId);
|
|
562
|
+
if (!key) return;
|
|
563
|
+
this.#clearPendingInteraction(key, interactionId);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
#presentInteraction(pending) {
|
|
567
|
+
if (!pending.needsPresentation) return Promise.resolve();
|
|
568
|
+
if (pending.presentationPromise) return pending.presentationPromise;
|
|
569
|
+
const question = pending.questions[pending.index];
|
|
570
|
+
if (!question) return Promise.resolve();
|
|
571
|
+
const presentation = this.#sendActive(
|
|
572
|
+
pending.chatId,
|
|
573
|
+
harnessQuestionText(
|
|
574
|
+
question,
|
|
575
|
+
pending.index,
|
|
576
|
+
pending.questions.length,
|
|
577
|
+
{ requiresMention: pending.requiresMention },
|
|
578
|
+
),
|
|
579
|
+
).then(() => {
|
|
580
|
+
pending.needsPresentation = false;
|
|
581
|
+
}).finally(() => {
|
|
582
|
+
if (pending.presentationPromise === presentation) {
|
|
583
|
+
pending.presentationPromise = null;
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
pending.presentationPromise = presentation;
|
|
587
|
+
return presentation;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async #discardResolvedInteractionReply(frame, messageId, chatId) {
|
|
591
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
592
|
+
await this.#state.markSeen(messageId);
|
|
593
|
+
this.#status.messagesReceived += 1;
|
|
594
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
595
|
+
await this.#sendImmediate(frame, chatId, INTERACTION_RESOLVED_TEXT).catch(() => undefined);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
#takePendingInteraction(key, interactionId) {
|
|
599
|
+
const pending = this.#pendingInteractions.get(key);
|
|
600
|
+
if (!pending
|
|
601
|
+
|| (interactionId !== undefined && pending.interactionId !== interactionId)) return null;
|
|
602
|
+
this.#pendingInteractions.delete(key);
|
|
603
|
+
this.#interactionKeys.delete(pending.interactionId);
|
|
604
|
+
return pending;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
#clearPendingInteraction(key, interactionId) {
|
|
608
|
+
return this.#takePendingInteraction(key, interactionId) !== null;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async #cancelPendingInteraction(key) {
|
|
612
|
+
const pending = this.#takePendingInteraction(key);
|
|
613
|
+
if (!pending || pending.kind !== 'question') return;
|
|
614
|
+
try {
|
|
615
|
+
await pending.interaction.respond({
|
|
616
|
+
ok: false,
|
|
617
|
+
error: {
|
|
618
|
+
code: 'cancelled',
|
|
619
|
+
message: 'The Enterprise WeChat interaction ended before the user answered.',
|
|
620
|
+
details: {},
|
|
621
|
+
},
|
|
622
|
+
}, { signal: AbortSignal.timeout(5_000) });
|
|
623
|
+
} catch (error) {
|
|
624
|
+
if (error?.code !== 'interaction-not-pending') {
|
|
625
|
+
this.#logger.warn?.('[dsh-im:wecom] failed to cancel a pending Harness interaction');
|
|
626
|
+
}
|
|
255
627
|
}
|
|
256
628
|
}
|
|
257
629
|
}
|
|
@@ -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;
|