@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
package/package.json
CHANGED
|
@@ -3,11 +3,18 @@ import {
|
|
|
3
3
|
splitDingtalkText,
|
|
4
4
|
} from './dingtalk-api.mjs';
|
|
5
5
|
import { createDingTalkCardStream } from './dingtalk-card-stream.mjs';
|
|
6
|
+
import {
|
|
7
|
+
harnessAnswerForQuestion,
|
|
8
|
+
harnessQuestionText,
|
|
9
|
+
validHarnessQuestion,
|
|
10
|
+
} from '../shared/harness-question.mjs';
|
|
11
|
+
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
|
6
12
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
7
13
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
8
14
|
|
|
9
15
|
const CARD_INITIAL_TEXT = '已连接 DeepSeek Harness,正在思考…';
|
|
10
16
|
const CARD_ERROR_TEXT = '消息处理失败,请稍后重试。';
|
|
17
|
+
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
|
11
18
|
|
|
12
19
|
const HELP_TEXT = [
|
|
13
20
|
'钉钉机器人已连接 DeepSeek Harness。',
|
|
@@ -55,6 +62,19 @@ function progressText(update) {
|
|
|
55
62
|
return `_${nonEmptyString(update?.text) ?? '正在处理…'}_`;
|
|
56
63
|
}
|
|
57
64
|
|
|
65
|
+
function canClaimInteractionReply(message, pending, sender) {
|
|
66
|
+
if (pending.needsPresentation || !pending.questions[pending.index]) return false;
|
|
67
|
+
if (pending.actor !== sender) return false;
|
|
68
|
+
if (String(message?.conversationType) === '2' && message?.isInAtList !== true) return false;
|
|
69
|
+
if (message?.msgtype !== 'text' || !nonEmptyString(message?.text?.content)) return false;
|
|
70
|
+
try {
|
|
71
|
+
normalizeDingtalkSessionWebhook(message.sessionWebhook);
|
|
72
|
+
return true;
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
58
78
|
function ensureStats(status) {
|
|
59
79
|
status.stats ??= {};
|
|
60
80
|
for (const key of ['messagesReceived', 'messagesReplied', 'messagesRejected', 'messagesIgnored']) {
|
|
@@ -102,7 +122,11 @@ export class DingtalkHarnessBridge {
|
|
|
102
122
|
#maxMessageChars;
|
|
103
123
|
#signal;
|
|
104
124
|
#queues = new Map();
|
|
125
|
+
#pendingInteractions = new Map();
|
|
126
|
+
#interactionKeys = new Map();
|
|
127
|
+
#interactionTasks = new Set();
|
|
105
128
|
#acceptedMessageIds = new Set();
|
|
129
|
+
#approvals;
|
|
106
130
|
|
|
107
131
|
constructor({
|
|
108
132
|
api,
|
|
@@ -128,6 +152,7 @@ export class DingtalkHarnessBridge {
|
|
|
128
152
|
this.#state = state;
|
|
129
153
|
this.#status = status;
|
|
130
154
|
this.#logger = logger;
|
|
155
|
+
this.#approvals = new HarnessApprovalQueue({ label: 'DingTalk', logger });
|
|
131
156
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
132
157
|
this.#maxMessageChars = maxMessageChars;
|
|
133
158
|
this.#signal = signal;
|
|
@@ -157,12 +182,99 @@ export class DingtalkHarnessBridge {
|
|
|
157
182
|
this.#status.lastRejectedAt = new Date().toISOString();
|
|
158
183
|
return Promise.resolve();
|
|
159
184
|
}
|
|
185
|
+
|
|
186
|
+
let sessionWebhook = null;
|
|
187
|
+
try {
|
|
188
|
+
sessionWebhook = normalizeDingtalkSessionWebhook(message.sessionWebhook);
|
|
189
|
+
} catch {
|
|
190
|
+
// An unsafe reply route must never be able to submit an approval.
|
|
191
|
+
}
|
|
192
|
+
const pending = this.#pendingInteractions.get(key);
|
|
193
|
+
const approvalReply = this.#approvals.claimReply({
|
|
194
|
+
key,
|
|
195
|
+
actor: sender,
|
|
196
|
+
messageId,
|
|
197
|
+
text: sessionWebhook && message?.msgtype === 'text'
|
|
198
|
+
? nonEmptyString(message?.text?.content) ?? ''
|
|
199
|
+
: '',
|
|
200
|
+
addressed: String(message?.conversationType) !== '2' || message?.isInAtList === true,
|
|
201
|
+
hasPendingQuestion: Boolean(pending),
|
|
202
|
+
questionCompletion: pending?.submitting || pending?.claimedReplyMessageId
|
|
203
|
+
? pending.queue
|
|
204
|
+
: null,
|
|
205
|
+
isQuestionPending: () => this.#pendingInteractions.has(key),
|
|
206
|
+
send: sessionWebhook
|
|
207
|
+
? (reply) => this.#send(sessionWebhook, reply)
|
|
208
|
+
: async () => undefined,
|
|
209
|
+
});
|
|
210
|
+
if (approvalReply) {
|
|
211
|
+
let current;
|
|
212
|
+
current = approvalReply.process(async () => {
|
|
213
|
+
if (this.#state.hasSeen(messageId)) return false;
|
|
214
|
+
await this.#state.markSeen(messageId);
|
|
215
|
+
increment(this.#status, 'messagesReceived');
|
|
216
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
217
|
+
if (!sessionWebhook) {
|
|
218
|
+
increment(this.#status, 'messagesRejected');
|
|
219
|
+
this.#status.lastRejectedAt = new Date().toISOString();
|
|
220
|
+
this.#status.lastError = '钉钉消息没有安全的回复地址。';
|
|
221
|
+
}
|
|
222
|
+
return true;
|
|
223
|
+
})
|
|
224
|
+
.catch((error) => {
|
|
225
|
+
if (this.#signal?.aborted) return;
|
|
226
|
+
this.#status.lastError = '钉钉审批处理失败。';
|
|
227
|
+
this.#logger.error?.('[dsh-dingtalk] failed to process an approval reply', error);
|
|
228
|
+
})
|
|
229
|
+
.finally(() => {
|
|
230
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
231
|
+
this.#interactionTasks.delete(current);
|
|
232
|
+
});
|
|
233
|
+
this.#interactionTasks.add(current);
|
|
234
|
+
return current;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (pending && pending.actor !== sender) {
|
|
238
|
+
return this.#enqueueMessage(message, messageId, sender, key);
|
|
239
|
+
}
|
|
240
|
+
// Once one valid answer has been claimed, later messages are subsequent
|
|
241
|
+
// prompts even if the network submission eventually needs a retry. Invalid
|
|
242
|
+
// replies do not claim the question, so the next valid answer can still
|
|
243
|
+
// pass through this interaction queue.
|
|
244
|
+
if (pending?.submitting || pending?.claimedReplyMessageId) {
|
|
245
|
+
return this.#enqueueMessage(message, messageId, sender, key);
|
|
246
|
+
}
|
|
247
|
+
if (pending) {
|
|
248
|
+
if (canClaimInteractionReply(message, pending, sender)) {
|
|
249
|
+
pending.claimedReplyMessageId = messageId;
|
|
250
|
+
}
|
|
251
|
+
const previous = pending.queue ?? Promise.resolve();
|
|
252
|
+
const current = previous
|
|
253
|
+
.catch(() => undefined)
|
|
254
|
+
.then(() => this.#processInteractionReply(message, messageId, sender, key, pending))
|
|
255
|
+
.finally(() => {
|
|
256
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
257
|
+
if (pending.claimedReplyMessageId === messageId) {
|
|
258
|
+
pending.claimedReplyMessageId = null;
|
|
259
|
+
}
|
|
260
|
+
if (pending.queue === current) pending.queue = null;
|
|
261
|
+
});
|
|
262
|
+
pending.queue = current;
|
|
263
|
+
return current;
|
|
264
|
+
}
|
|
265
|
+
return this.#enqueueMessage(message, messageId, sender, key);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
#enqueueMessage(message, messageId, sender, key, {
|
|
269
|
+
releaseMessageId = true,
|
|
270
|
+
alreadyRecorded = false,
|
|
271
|
+
} = {}) {
|
|
160
272
|
const previous = this.#queues.get(key) ?? Promise.resolve();
|
|
161
273
|
const current = previous
|
|
162
274
|
.catch(() => undefined)
|
|
163
|
-
.then(() => this.#process(message, messageId, sender, key))
|
|
275
|
+
.then(() => this.#process(message, messageId, sender, key, { alreadyRecorded }))
|
|
164
276
|
.finally(() => {
|
|
165
|
-
this.#acceptedMessageIds.delete(messageId);
|
|
277
|
+
if (releaseMessageId) this.#acceptedMessageIds.delete(messageId);
|
|
166
278
|
if (this.#queues.get(key) === current) this.#queues.delete(key);
|
|
167
279
|
});
|
|
168
280
|
this.#queues.set(key, current);
|
|
@@ -170,15 +282,23 @@ export class DingtalkHarnessBridge {
|
|
|
170
282
|
}
|
|
171
283
|
|
|
172
284
|
async waitForIdle() {
|
|
173
|
-
await Promise.allSettled([
|
|
285
|
+
await Promise.allSettled([
|
|
286
|
+
...this.#queues.values(),
|
|
287
|
+
...[...this.#pendingInteractions.values()].flatMap((pending) => (
|
|
288
|
+
pending.queue ? [pending.queue] : []
|
|
289
|
+
)),
|
|
290
|
+
...this.#interactionTasks,
|
|
291
|
+
]);
|
|
174
292
|
}
|
|
175
293
|
|
|
176
|
-
async #process(message, messageId, sender, key) {
|
|
294
|
+
async #process(message, messageId, sender, key, { alreadyRecorded = false } = {}) {
|
|
177
295
|
this.#signal?.throwIfAborted();
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
296
|
+
if (!alreadyRecorded) {
|
|
297
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
298
|
+
await this.#state.markSeen(messageId);
|
|
299
|
+
increment(this.#status, 'messagesReceived');
|
|
300
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
301
|
+
}
|
|
182
302
|
|
|
183
303
|
if (String(message.conversationType) === '2' && message.isInAtList !== true) {
|
|
184
304
|
increment(this.#status, 'messagesIgnored');
|
|
@@ -253,6 +373,13 @@ export class DingtalkHarnessBridge {
|
|
|
253
373
|
onUpdate: cardStarted
|
|
254
374
|
? (update) => cardStream.push(progressText(update))
|
|
255
375
|
: undefined,
|
|
376
|
+
onInteraction: (interaction) => this.#handleInteraction(interaction, {
|
|
377
|
+
key,
|
|
378
|
+
actor: sender,
|
|
379
|
+
sessionWebhook,
|
|
380
|
+
requiresMention: String(message.conversationType) === '2',
|
|
381
|
+
}),
|
|
382
|
+
onInteractionResolved: (resolution) => this.#handleInteractionResolved(resolution),
|
|
256
383
|
},
|
|
257
384
|
});
|
|
258
385
|
const streamed = cardStarted && await cardStream.finish(answer);
|
|
@@ -270,6 +397,294 @@ export class DingtalkHarnessBridge {
|
|
|
270
397
|
} catch {
|
|
271
398
|
this.#logger.error?.('[dsh-dingtalk] failed to send the safe error reply');
|
|
272
399
|
}
|
|
400
|
+
} finally {
|
|
401
|
+
await this.#cancelPendingInteraction(key);
|
|
402
|
+
await this.#approvals.closeRoute(key);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async #processInteractionReply(message, messageId, sender, key, expected) {
|
|
407
|
+
this.#signal?.throwIfAborted();
|
|
408
|
+
const current = this.#pendingInteractions.get(key);
|
|
409
|
+
const claimed = expected.claimedReplyMessageId === messageId;
|
|
410
|
+
if (!current || current !== expected || current.submitting) {
|
|
411
|
+
if (claimed && (!current || current !== expected)) {
|
|
412
|
+
return this.#discardResolvedInteractionReply(message, messageId);
|
|
413
|
+
}
|
|
414
|
+
return this.#enqueueMessage(message, messageId, sender, key, { releaseMessageId: false });
|
|
415
|
+
}
|
|
416
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
417
|
+
await this.#state.markSeen(messageId);
|
|
418
|
+
increment(this.#status, 'messagesReceived');
|
|
419
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
420
|
+
|
|
421
|
+
if (String(message.conversationType) === '2' && message.isInAtList !== true) {
|
|
422
|
+
increment(this.#status, 'messagesIgnored');
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
let sessionWebhook;
|
|
427
|
+
try {
|
|
428
|
+
sessionWebhook = normalizeDingtalkSessionWebhook(message.sessionWebhook);
|
|
429
|
+
} catch {
|
|
430
|
+
increment(this.#status, 'messagesRejected');
|
|
431
|
+
this.#status.lastRejectedAt = new Date().toISOString();
|
|
432
|
+
this.#status.lastError = '钉钉消息没有安全的回复地址。';
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const text = message?.msgtype === 'text' ? nonEmptyString(message?.text?.content) : null;
|
|
437
|
+
if (!text) {
|
|
438
|
+
try {
|
|
439
|
+
await this.#send(sessionWebhook, '请用文字回答当前问题。');
|
|
440
|
+
} catch {
|
|
441
|
+
this.#logger.error?.('[dsh-dingtalk] failed to reject a non-text interaction reply');
|
|
442
|
+
}
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const pending = this.#pendingInteractions.get(key);
|
|
447
|
+
if (!pending || pending !== expected || pending.submitting) {
|
|
448
|
+
if (claimed && (!pending || pending !== expected)) {
|
|
449
|
+
try {
|
|
450
|
+
await this.#send(sessionWebhook, INTERACTION_RESOLVED_TEXT);
|
|
451
|
+
} catch {
|
|
452
|
+
this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice');
|
|
453
|
+
}
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
return this.#enqueueMessage(message, messageId, sender, key, {
|
|
457
|
+
releaseMessageId: false,
|
|
458
|
+
alreadyRecorded: true,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
pending.sessionWebhook = sessionWebhook;
|
|
462
|
+
if (pending.needsPresentation) {
|
|
463
|
+
try {
|
|
464
|
+
await this.#presentInteraction(pending);
|
|
465
|
+
} catch {
|
|
466
|
+
this.#status.lastError = '钉钉交互问题发送失败。';
|
|
467
|
+
this.#logger.error?.('[dsh-dingtalk] failed to retry an interaction question');
|
|
468
|
+
pending.interaction.reconnect?.();
|
|
469
|
+
}
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const question = pending.questions[pending.index];
|
|
473
|
+
if (!question) return;
|
|
474
|
+
|
|
475
|
+
pending.answers.push(harnessAnswerForQuestion(question, text));
|
|
476
|
+
pending.index += 1;
|
|
477
|
+
if (pending.index < pending.questions.length) {
|
|
478
|
+
if (pending.claimedReplyMessageId === messageId) {
|
|
479
|
+
pending.claimedReplyMessageId = null;
|
|
480
|
+
}
|
|
481
|
+
pending.needsPresentation = true;
|
|
482
|
+
try {
|
|
483
|
+
await this.#presentInteraction(pending);
|
|
484
|
+
} catch {
|
|
485
|
+
this.#status.lastError = '钉钉交互问题发送失败。';
|
|
486
|
+
this.#logger.error?.('[dsh-dingtalk] failed to send the next interaction question');
|
|
487
|
+
pending.interaction.reconnect?.();
|
|
488
|
+
}
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
pending.submitting = true;
|
|
493
|
+
try {
|
|
494
|
+
await pending.interaction.respond({
|
|
495
|
+
ok: true,
|
|
496
|
+
value: {
|
|
497
|
+
sessionId: pending.sessionId,
|
|
498
|
+
answer: { answers: pending.answers },
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
this.#clearPendingInteraction(key, pending.interactionId);
|
|
502
|
+
this.#status.lastError = null;
|
|
503
|
+
} catch (error) {
|
|
504
|
+
if (this.#signal?.aborted) return;
|
|
505
|
+
if (this.#pendingInteractions.get(key) !== pending) return;
|
|
506
|
+
if (error?.code === 'interaction-not-pending') {
|
|
507
|
+
this.#clearPendingInteraction(key, pending.interactionId);
|
|
508
|
+
try {
|
|
509
|
+
await this.#send(sessionWebhook, INTERACTION_RESOLVED_TEXT);
|
|
510
|
+
} catch {
|
|
511
|
+
this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice');
|
|
512
|
+
}
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
pending.submitting = false;
|
|
516
|
+
pending.answers.pop();
|
|
517
|
+
pending.index -= 1;
|
|
518
|
+
this.#status.lastError = '回答提交失败。';
|
|
519
|
+
this.#logger.error?.('[dsh-dingtalk] failed to answer a Harness interaction');
|
|
520
|
+
try {
|
|
521
|
+
await this.#send(sessionWebhook, '回答提交失败,请重新发送当前问题的答案。');
|
|
522
|
+
} catch {
|
|
523
|
+
this.#logger.error?.('[dsh-dingtalk] failed to send an interaction retry notice');
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async #handleInteraction(interaction, {
|
|
529
|
+
key,
|
|
530
|
+
actor,
|
|
531
|
+
sessionWebhook,
|
|
532
|
+
requiresMention,
|
|
533
|
+
}) {
|
|
534
|
+
if (await this.#approvals.handleRequested(interaction, {
|
|
535
|
+
key,
|
|
536
|
+
actor,
|
|
537
|
+
requiresMention,
|
|
538
|
+
send: (text) => this.#send(sessionWebhook, text),
|
|
539
|
+
})) return;
|
|
540
|
+
|
|
541
|
+
// Approval requests return above; the existing question state machine stays unchanged.
|
|
542
|
+
if (interaction?.kind !== 'question') return;
|
|
543
|
+
const questions = interaction?.payload?.questions;
|
|
544
|
+
const interactionId = typeof interaction?.interactionId === 'string'
|
|
545
|
+
? interaction.interactionId
|
|
546
|
+
: interaction?.rpcId;
|
|
547
|
+
if (typeof interaction.rpcId !== 'string'
|
|
548
|
+
|| typeof interactionId !== 'string'
|
|
549
|
+
|| typeof interaction.sessionId !== 'string'
|
|
550
|
+
|| !Array.isArray(questions)
|
|
551
|
+
|| questions.length === 0
|
|
552
|
+
|| questions.some((question) => !validHarnessQuestion(question))) {
|
|
553
|
+
this.#logger.warn?.('[dsh-dingtalk] ignored an invalid Harness question interaction');
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (interaction.recovered === true) {
|
|
558
|
+
await interaction.respond({
|
|
559
|
+
ok: false,
|
|
560
|
+
error: {
|
|
561
|
+
code: 'cancelled',
|
|
562
|
+
message: 'DingTalk safely cancelled an interaction left by an earlier client.',
|
|
563
|
+
details: {},
|
|
564
|
+
},
|
|
565
|
+
});
|
|
566
|
+
try {
|
|
567
|
+
await this.#send(sessionWebhook, '检测到这个 Session 中遗留的待回答问题,已安全取消并继续处理你刚才的消息。');
|
|
568
|
+
} catch {
|
|
569
|
+
this.#logger.error?.('[dsh-dingtalk] failed to send an interaction recovery notice');
|
|
570
|
+
}
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const existing = this.#pendingInteractions.get(key);
|
|
575
|
+
if (existing?.interactionId === interactionId) {
|
|
576
|
+
existing.interaction = interaction;
|
|
577
|
+
if (existing.needsPresentation) await this.#presentInteraction(existing);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (this.#interactionKeys.has(interactionId)) return;
|
|
581
|
+
if (existing) {
|
|
582
|
+
this.#logger.warn?.('[dsh-dingtalk] cancelled a second pending Harness question');
|
|
583
|
+
await interaction.respond({
|
|
584
|
+
ok: false,
|
|
585
|
+
error: {
|
|
586
|
+
code: 'cancelled',
|
|
587
|
+
message: 'DingTalk is already handling another user interaction.',
|
|
588
|
+
details: {},
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const pending = {
|
|
595
|
+
kind: 'question',
|
|
596
|
+
interactionId,
|
|
597
|
+
sessionId: interaction.sessionId,
|
|
598
|
+
interaction,
|
|
599
|
+
actor,
|
|
600
|
+
requiresMention,
|
|
601
|
+
questions,
|
|
602
|
+
answers: [],
|
|
603
|
+
index: 0,
|
|
604
|
+
sessionWebhook,
|
|
605
|
+
queue: null,
|
|
606
|
+
claimedReplyMessageId: null,
|
|
607
|
+
submitting: false,
|
|
608
|
+
needsPresentation: true,
|
|
609
|
+
};
|
|
610
|
+
this.#pendingInteractions.set(key, pending);
|
|
611
|
+
this.#interactionKeys.set(pending.interactionId, key);
|
|
612
|
+
await this.#presentInteraction(pending);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
async #handleInteractionResolved(resolution) {
|
|
616
|
+
if (await this.#approvals.handleResolved(resolution)) return;
|
|
617
|
+
const interactionId = resolution?.interactionId;
|
|
618
|
+
if (resolution?.kind !== 'question' || typeof interactionId !== 'string') return;
|
|
619
|
+
const key = this.#interactionKeys.get(interactionId);
|
|
620
|
+
if (!key) return;
|
|
621
|
+
this.#clearPendingInteraction(key, interactionId);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async #presentInteraction(pending) {
|
|
625
|
+
const question = pending.questions[pending.index];
|
|
626
|
+
if (!question) return;
|
|
627
|
+
await this.#send(
|
|
628
|
+
pending.sessionWebhook,
|
|
629
|
+
harnessQuestionText(
|
|
630
|
+
question,
|
|
631
|
+
pending.index,
|
|
632
|
+
pending.questions.length,
|
|
633
|
+
{ requiresMention: pending.requiresMention },
|
|
634
|
+
),
|
|
635
|
+
);
|
|
636
|
+
pending.needsPresentation = false;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async #discardResolvedInteractionReply(message, messageId) {
|
|
640
|
+
if (this.#state.hasSeen(messageId)) return;
|
|
641
|
+
await this.#state.markSeen(messageId);
|
|
642
|
+
increment(this.#status, 'messagesReceived');
|
|
643
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
644
|
+
let sessionWebhook;
|
|
645
|
+
try {
|
|
646
|
+
sessionWebhook = normalizeDingtalkSessionWebhook(message.sessionWebhook);
|
|
647
|
+
} catch {
|
|
648
|
+
increment(this.#status, 'messagesRejected');
|
|
649
|
+
this.#status.lastRejectedAt = new Date().toISOString();
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
await this.#send(sessionWebhook, INTERACTION_RESOLVED_TEXT);
|
|
654
|
+
} catch {
|
|
655
|
+
this.#logger.error?.('[dsh-dingtalk] failed to send an expired interaction notice');
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
#takePendingInteraction(key, interactionId) {
|
|
660
|
+
const pending = this.#pendingInteractions.get(key);
|
|
661
|
+
if (!pending
|
|
662
|
+
|| (interactionId !== undefined && pending.interactionId !== interactionId)) return null;
|
|
663
|
+
this.#pendingInteractions.delete(key);
|
|
664
|
+
this.#interactionKeys.delete(pending.interactionId);
|
|
665
|
+
return pending;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
#clearPendingInteraction(key, interactionId) {
|
|
669
|
+
return this.#takePendingInteraction(key, interactionId) !== null;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
async #cancelPendingInteraction(key) {
|
|
673
|
+
const pending = this.#takePendingInteraction(key);
|
|
674
|
+
if (!pending || pending.kind !== 'question') return;
|
|
675
|
+
try {
|
|
676
|
+
await pending.interaction.respond({
|
|
677
|
+
ok: false,
|
|
678
|
+
error: {
|
|
679
|
+
code: 'cancelled',
|
|
680
|
+
message: 'The DingTalk interaction ended before the user answered.',
|
|
681
|
+
details: {},
|
|
682
|
+
},
|
|
683
|
+
}, { signal: AbortSignal.timeout(5_000) });
|
|
684
|
+
} catch (error) {
|
|
685
|
+
if (error?.code !== 'interaction-not-pending') {
|
|
686
|
+
this.#logger.warn?.('[dsh-dingtalk] failed to cancel a pending Harness interaction');
|
|
687
|
+
}
|
|
273
688
|
}
|
|
274
689
|
}
|
|
275
690
|
|