@xmanrui/dsh-im 2.4.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/lib/client.js +322 -109
- package/lib/index.js +218 -211
- package/package.json +1 -1
- package/plugin-src/client/channel-card-meta.js +36 -1
- package/plugin-src/client/channels/dingtalk/api.js +2 -0
- package/plugin-src/client/channels/dingtalk/index.js +9 -1
- package/plugin-src/client/channels/feishu/api.js +3 -0
- package/plugin-src/client/channels/feishu/index.js +9 -1
- package/plugin-src/client/channels/qq/api.js +2 -0
- package/plugin-src/client/channels/qq/index.js +9 -1
- package/plugin-src/client/channels/shared/token-api.js +2 -0
- package/plugin-src/client/channels/shared/token-channel.js +9 -1
- package/plugin-src/client/channels/wecom/api.js +2 -0
- package/plugin-src/client/channels/wecom/index.js +9 -1
- package/plugin-src/client/channels/weixin/api.js +2 -10
- package/plugin-src/client/channels/weixin/index.js +9 -5
- package/plugin-src/client/channels/whatsapp/api.js +2 -0
- package/plugin-src/client/channels/whatsapp/index.js +9 -1
- package/plugin-src/client/i18n.js +4 -0
- package/plugin-src/client/index.js +16 -2
- package/plugin-src/client/last-message-error.js +17 -0
- package/plugin-src/client/styles.js +5 -1
- package/plugin-src/host/channels/feishu/rpc.mjs +3 -0
- package/src/channels/dingtalk/dingtalk-api.mjs +102 -6
- package/src/channels/dingtalk/dingtalk-bridge.mjs +247 -38
- package/src/channels/dingtalk/dingtalk-card-stream.mjs +2 -2
- package/src/channels/dingtalk/dingtalk-controller.mjs +2 -0
- package/src/channels/discord/discord-api.mjs +35 -0
- package/src/channels/discord/discord-bridge.mjs +1 -0
- package/src/channels/discord/discord-runtime.mjs +26 -0
- package/src/channels/feishu/bridge.mjs +115 -69
- package/src/channels/feishu/multi-bot-controller.mjs +2 -0
- package/src/channels/qq/qq-bridge.mjs +83 -28
- package/src/channels/qq/qq-controller.mjs +2 -0
- package/src/channels/shared/harness-client.mjs +40 -4
- package/src/channels/shared/i18n-en/shared-a.mjs +77 -0
- package/src/channels/shared/message-failure.mjs +244 -0
- package/src/channels/shared/semantic/artifact-delivery.mjs +9 -2
- package/src/channels/shared/status-reaction.mjs +107 -0
- package/src/channels/shared/text-harness-bridge.mjs +123 -66
- package/src/channels/shared/token-bot-controller.mjs +2 -0
- package/src/channels/slack/manifest.mjs +1 -0
- package/src/channels/slack/slack-api.mjs +28 -0
- package/src/channels/slack/slack-bridge.mjs +5 -0
- package/src/channels/slack/slack-controller.mjs +2 -0
- package/src/channels/slack/slack-runtime.mjs +24 -0
- package/src/channels/telegram/telegram-api.mjs +9 -0
- package/src/channels/telegram/telegram-bridge.mjs +1 -0
- package/src/channels/telegram/telegram-runtime.mjs +20 -0
- package/src/channels/wecom/wecom-bridge.mjs +49 -15
- package/src/channels/wecom/wecom-controller.mjs +2 -0
- package/src/channels/weixin/weixin-api.mjs +47 -0
- package/src/channels/weixin/weixin-bridge.mjs +261 -43
- package/src/channels/weixin/weixin-controller.mjs +2 -15
- package/src/channels/weixin/weixin-runtime.mjs +1 -0
- package/src/channels/whatsapp/whatsapp-bridge.mjs +1 -0
- package/src/channels/whatsapp/whatsapp-controller.mjs +2 -0
- package/src/channels/whatsapp/whatsapp-runtime.mjs +36 -0
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DINGTALK_DONE_REACTION_NAME,
|
|
3
|
+
DINGTALK_ERROR_REACTION_NAME,
|
|
4
|
+
DINGTALK_THINKING_REACTION_NAME,
|
|
2
5
|
normalizeDingtalkSessionWebhook,
|
|
3
6
|
splitDingtalkText,
|
|
4
7
|
} from './dingtalk-api.mjs';
|
|
@@ -32,6 +35,7 @@ import {
|
|
|
32
35
|
} from '../shared/batch-input.mjs';
|
|
33
36
|
import {
|
|
34
37
|
hasInboundImages,
|
|
38
|
+
imagePromptDiagnostic,
|
|
35
39
|
imagePromptUserMessage,
|
|
36
40
|
promptContentForMessage,
|
|
37
41
|
} from '../shared/image-prompt.mjs';
|
|
@@ -46,10 +50,15 @@ import {
|
|
|
46
50
|
createDeliveryReceipt,
|
|
47
51
|
providerMessageIdsFor,
|
|
48
52
|
} from '../shared/semantic/delivery.mjs';
|
|
53
|
+
import {
|
|
54
|
+
channelDeliveryFailure,
|
|
55
|
+
clearLastMessageFailure,
|
|
56
|
+
messageFailureText,
|
|
57
|
+
setLastMessageFailure,
|
|
58
|
+
} from '../shared/message-failure.mjs';
|
|
49
59
|
import { t } from '../shared/i18n.mjs';
|
|
50
60
|
|
|
51
61
|
const CARD_INITIAL_TEXT = '已连接 DeepSeek Harness,正在思考…';
|
|
52
|
-
const CARD_ERROR_TEXT = '消息处理失败,请稍后重试。';
|
|
53
62
|
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
|
54
63
|
|
|
55
64
|
const HELP_TEXT_LINES = [
|
|
@@ -303,7 +312,15 @@ function canClaimInteractionReply(message, pending, sender) {
|
|
|
303
312
|
|
|
304
313
|
function ensureStats(status) {
|
|
305
314
|
status.stats ??= {};
|
|
306
|
-
for (const key of [
|
|
315
|
+
for (const key of [
|
|
316
|
+
'messagesReceived',
|
|
317
|
+
'messagesReplied',
|
|
318
|
+
'messagesRejected',
|
|
319
|
+
'messagesIgnored',
|
|
320
|
+
'reactionsAdded',
|
|
321
|
+
'reactionsRemoved',
|
|
322
|
+
'reactionErrors',
|
|
323
|
+
]) {
|
|
307
324
|
status[key] ??= 0;
|
|
308
325
|
status.stats[key] = status[key];
|
|
309
326
|
}
|
|
@@ -322,16 +339,23 @@ export function createDingtalkBridgeStatus({ pendingSenders = [] } = {}) {
|
|
|
322
339
|
messagesReplied: 0,
|
|
323
340
|
messagesRejected: 0,
|
|
324
341
|
messagesIgnored: 0,
|
|
342
|
+
reactionsAdded: 0,
|
|
343
|
+
reactionsRemoved: 0,
|
|
344
|
+
reactionErrors: 0,
|
|
325
345
|
lastMessageAt: null,
|
|
326
346
|
lastReplyAt: null,
|
|
327
347
|
lastRejectedAt: null,
|
|
328
348
|
lastError: null,
|
|
349
|
+
lastMessageError: null,
|
|
329
350
|
pendingSenders: structuredClone(pendingSenders),
|
|
330
351
|
stats: {
|
|
331
352
|
messagesReceived: 0,
|
|
332
353
|
messagesReplied: 0,
|
|
333
354
|
messagesRejected: 0,
|
|
334
355
|
messagesIgnored: 0,
|
|
356
|
+
reactionsAdded: 0,
|
|
357
|
+
reactionsRemoved: 0,
|
|
358
|
+
reactionErrors: 0,
|
|
335
359
|
},
|
|
336
360
|
};
|
|
337
361
|
}
|
|
@@ -345,6 +369,7 @@ export class DingtalkHarnessBridge {
|
|
|
345
369
|
#status;
|
|
346
370
|
#logger;
|
|
347
371
|
#replyTimeoutMs;
|
|
372
|
+
#reactionTimeoutMs;
|
|
348
373
|
#maxMessageChars;
|
|
349
374
|
#signal;
|
|
350
375
|
#queues = new Map();
|
|
@@ -365,6 +390,7 @@ export class DingtalkHarnessBridge {
|
|
|
365
390
|
status = createDingtalkBridgeStatus(),
|
|
366
391
|
logger = console,
|
|
367
392
|
replyTimeoutMs = 600_000,
|
|
393
|
+
reactionTimeoutMs = 5_000,
|
|
368
394
|
maxMessageChars = 4_000,
|
|
369
395
|
signal,
|
|
370
396
|
}) {
|
|
@@ -382,6 +408,9 @@ export class DingtalkHarnessBridge {
|
|
|
382
408
|
this.#logger = logger;
|
|
383
409
|
this.#approvals = new HarnessApprovalQueue({ label: 'DingTalk', logger });
|
|
384
410
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
411
|
+
this.#reactionTimeoutMs = Number.isFinite(reactionTimeoutMs) && reactionTimeoutMs > 0
|
|
412
|
+
? Math.floor(reactionTimeoutMs)
|
|
413
|
+
: 5_000;
|
|
385
414
|
this.#maxMessageChars = maxMessageChars;
|
|
386
415
|
this.#signal = signal;
|
|
387
416
|
ensureStats(this.#status);
|
|
@@ -429,15 +458,35 @@ export class DingtalkHarnessBridge {
|
|
|
429
458
|
const commandText = nonEmptyString(promptMessage.content) ?? '';
|
|
430
459
|
const addressed = String(message.conversationType) !== '2' || message?.isInAtList === true;
|
|
431
460
|
const direct = String(message.conversationType) !== '2';
|
|
461
|
+
const statusReaction = sessionWebhook && addressed ? this.#startStatusReaction(message) : null;
|
|
462
|
+
const finish = (task) => Promise.resolve(task).then(
|
|
463
|
+
(value) => {
|
|
464
|
+
this.#finishStatusReaction(
|
|
465
|
+
statusReaction,
|
|
466
|
+
this.#signal?.aborted ? 'clear' : 'success',
|
|
467
|
+
);
|
|
468
|
+
return value;
|
|
469
|
+
},
|
|
470
|
+
(error) => {
|
|
471
|
+
this.#finishStatusReaction(
|
|
472
|
+
statusReaction,
|
|
473
|
+
this.#signal?.aborted || error?.name === 'AbortError' || error?.code === 'turn-stopped'
|
|
474
|
+
? 'clear'
|
|
475
|
+
: 'error',
|
|
476
|
+
);
|
|
477
|
+
throw error;
|
|
478
|
+
},
|
|
479
|
+
);
|
|
432
480
|
const batchCommand = String(message?.msgtype).toLowerCase() === 'text'
|
|
433
481
|
&& isBatchInputCommand(commandText);
|
|
434
482
|
const batchStatus = this.#batchInputs.status(key);
|
|
435
483
|
if (batchCommand && !direct && sessionWebhook && addressed) {
|
|
436
|
-
return this.#finishBatchResult(
|
|
484
|
+
return finish(this.#finishBatchResult(
|
|
437
485
|
messageId,
|
|
438
486
|
sessionWebhook,
|
|
439
487
|
{ message: batchInputGroupUnsupportedMessage() },
|
|
440
|
-
|
|
488
|
+
statusReaction,
|
|
489
|
+
));
|
|
441
490
|
}
|
|
442
491
|
if (direct && sessionWebhook && (batchCommand || batchStatus.phase === 'collecting')) {
|
|
443
492
|
const exactBatchStart = /^\/batch$/iu.test(commandText);
|
|
@@ -453,7 +502,7 @@ export class DingtalkHarnessBridge {
|
|
|
453
502
|
});
|
|
454
503
|
if (result.handled) {
|
|
455
504
|
if (result.kind === 'submit') {
|
|
456
|
-
return this.#enqueueMessage(
|
|
505
|
+
return finish(this.#enqueueMessage(
|
|
457
506
|
{
|
|
458
507
|
...message,
|
|
459
508
|
msgtype: 'text',
|
|
@@ -462,10 +511,15 @@ export class DingtalkHarnessBridge {
|
|
|
462
511
|
messageId,
|
|
463
512
|
sender,
|
|
464
513
|
key,
|
|
465
|
-
{ batchSubmission: result },
|
|
466
|
-
);
|
|
514
|
+
{ batchSubmission: result, statusReaction },
|
|
515
|
+
));
|
|
467
516
|
}
|
|
468
|
-
return this.#finishBatchResult(
|
|
517
|
+
return finish(this.#finishBatchResult(
|
|
518
|
+
messageId,
|
|
519
|
+
sessionWebhook,
|
|
520
|
+
result,
|
|
521
|
+
statusReaction,
|
|
522
|
+
));
|
|
469
523
|
}
|
|
470
524
|
}
|
|
471
525
|
const commandRunner = hasInboundFiles(promptMessage) ? null : isControlCommand(commandText)
|
|
@@ -483,16 +537,24 @@ export class DingtalkHarnessBridge {
|
|
|
483
537
|
promptMessage,
|
|
484
538
|
commandRunner,
|
|
485
539
|
).catch((error) => {
|
|
486
|
-
if (error?.code === 'turn-stopped' || this.#signal?.aborted)
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
540
|
+
if (error?.code === 'turn-stopped' || this.#signal?.aborted) {
|
|
541
|
+
this.#finishStatusReaction(statusReaction, 'clear');
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
this.#finishStatusReaction(statusReaction, 'error');
|
|
545
|
+
this.#status.lastError = error?.message ?? String(error);
|
|
546
|
+
const failure = setLastMessageFailure(this.#status, error);
|
|
547
|
+
this.#logger.error?.(
|
|
548
|
+
`[dsh-dingtalk] failed to process a command [${failure.referenceId}]`,
|
|
549
|
+
safeErrorDiagnostic(error),
|
|
550
|
+
);
|
|
551
|
+
return this.#send(sessionWebhook, messageFailureText(failure)).catch(() => undefined);
|
|
490
552
|
}).finally(() => {
|
|
491
553
|
this.#acceptedMessageIds.delete(messageId);
|
|
492
554
|
this.#commandTasks.delete(task);
|
|
493
555
|
});
|
|
494
556
|
this.#commandTasks.add(task);
|
|
495
|
-
return task;
|
|
557
|
+
return finish(task);
|
|
496
558
|
}
|
|
497
559
|
const approvalReply = this.#approvals.claimReply({
|
|
498
560
|
key,
|
|
@@ -526,7 +588,11 @@ export class DingtalkHarnessBridge {
|
|
|
526
588
|
return true;
|
|
527
589
|
})
|
|
528
590
|
.catch((error) => {
|
|
529
|
-
if (this.#signal?.aborted)
|
|
591
|
+
if (this.#signal?.aborted) {
|
|
592
|
+
this.#finishStatusReaction(statusReaction, 'clear');
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
this.#finishStatusReaction(statusReaction, 'error');
|
|
530
596
|
this.#status.lastError = t('钉钉审批处理失败。');
|
|
531
597
|
this.#logger.error?.('[dsh-dingtalk] failed to process an approval reply', error);
|
|
532
598
|
})
|
|
@@ -535,18 +601,18 @@ export class DingtalkHarnessBridge {
|
|
|
535
601
|
this.#interactionTasks.delete(current);
|
|
536
602
|
});
|
|
537
603
|
this.#interactionTasks.add(current);
|
|
538
|
-
return current;
|
|
604
|
+
return finish(current);
|
|
539
605
|
}
|
|
540
606
|
|
|
541
607
|
if (pending && pending.actor !== sender) {
|
|
542
|
-
return this.#enqueueMessage(message, messageId, sender, key);
|
|
608
|
+
return finish(this.#enqueueMessage(message, messageId, sender, key, { statusReaction }));
|
|
543
609
|
}
|
|
544
610
|
// Once one valid answer has been claimed, later messages are subsequent
|
|
545
611
|
// prompts even if the network submission eventually needs a retry. Invalid
|
|
546
612
|
// replies do not claim the question, so the next valid answer can still
|
|
547
613
|
// pass through this interaction queue.
|
|
548
614
|
if (pending?.submitting || pending?.claimedReplyMessageId) {
|
|
549
|
-
return this.#enqueueMessage(message, messageId, sender, key);
|
|
615
|
+
return finish(this.#enqueueMessage(message, messageId, sender, key, { statusReaction }));
|
|
550
616
|
}
|
|
551
617
|
if (pending) {
|
|
552
618
|
if (canClaimInteractionReply(message, pending, sender)) {
|
|
@@ -555,7 +621,14 @@ export class DingtalkHarnessBridge {
|
|
|
555
621
|
const previous = pending.queue ?? Promise.resolve();
|
|
556
622
|
const current = previous
|
|
557
623
|
.catch(() => undefined)
|
|
558
|
-
.then(() => this.#processInteractionReply(
|
|
624
|
+
.then(() => this.#processInteractionReply(
|
|
625
|
+
message,
|
|
626
|
+
messageId,
|
|
627
|
+
sender,
|
|
628
|
+
key,
|
|
629
|
+
pending,
|
|
630
|
+
statusReaction,
|
|
631
|
+
))
|
|
559
632
|
.finally(() => {
|
|
560
633
|
this.#acceptedMessageIds.delete(messageId);
|
|
561
634
|
if (pending.claimedReplyMessageId === messageId) {
|
|
@@ -564,15 +637,101 @@ export class DingtalkHarnessBridge {
|
|
|
564
637
|
if (pending.queue === current) pending.queue = null;
|
|
565
638
|
});
|
|
566
639
|
pending.queue = current;
|
|
567
|
-
return current;
|
|
640
|
+
return finish(current);
|
|
568
641
|
}
|
|
569
|
-
return this.#enqueueMessage(message, messageId, sender, key);
|
|
642
|
+
return finish(this.#enqueueMessage(message, messageId, sender, key, { statusReaction }));
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
#runReactionCall(method, target, reactionName, kind) {
|
|
646
|
+
const controller = new AbortController();
|
|
647
|
+
const operation = Promise.resolve().then(() => this.#api[method]({
|
|
648
|
+
clientId: this.#clientId,
|
|
649
|
+
clientSecret: this.#clientSecret,
|
|
650
|
+
...target,
|
|
651
|
+
reactionName,
|
|
652
|
+
signal: controller.signal,
|
|
653
|
+
}));
|
|
654
|
+
let timer;
|
|
655
|
+
const timeout = new Promise((_, reject) => {
|
|
656
|
+
timer = setTimeout(() => {
|
|
657
|
+
const error = new DOMException('DingTalk reaction timed out', 'TimeoutError');
|
|
658
|
+
controller.abort(error);
|
|
659
|
+
reject(error);
|
|
660
|
+
}, this.#reactionTimeoutMs);
|
|
661
|
+
timer.unref?.();
|
|
662
|
+
});
|
|
663
|
+
return Promise.race([operation, timeout])
|
|
664
|
+
.then(() => {
|
|
665
|
+
increment(this.#status, kind === 'add' ? 'reactionsAdded' : 'reactionsRemoved');
|
|
666
|
+
return true;
|
|
667
|
+
})
|
|
668
|
+
.catch((error) => {
|
|
669
|
+
increment(this.#status, 'reactionErrors');
|
|
670
|
+
this.#logger.debug?.(`[dsh-dingtalk] ${method} failed`, safeErrorDiagnostic(error));
|
|
671
|
+
return false;
|
|
672
|
+
})
|
|
673
|
+
.finally(() => clearTimeout(timer));
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
#startStatusReaction(message) {
|
|
677
|
+
if (typeof this.#api.addReaction !== 'function'
|
|
678
|
+
|| typeof this.#api.recallReaction !== 'function') return null;
|
|
679
|
+
const messageId = nonEmptyString(message?.msgId);
|
|
680
|
+
const conversationId = nonEmptyString(message?.conversationId);
|
|
681
|
+
if (!messageId || !conversationId) return null;
|
|
682
|
+
const target = {
|
|
683
|
+
messageId,
|
|
684
|
+
conversationId,
|
|
685
|
+
robotCode: nonEmptyString(message?.robotCode) ?? this.#clientId,
|
|
686
|
+
};
|
|
687
|
+
return {
|
|
688
|
+
target,
|
|
689
|
+
attached: this.#runReactionCall(
|
|
690
|
+
'addReaction',
|
|
691
|
+
target,
|
|
692
|
+
DINGTALK_THINKING_REACTION_NAME,
|
|
693
|
+
'add',
|
|
694
|
+
),
|
|
695
|
+
terminal: false,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
#finishStatusReaction(reaction, outcome) {
|
|
700
|
+
if (!reaction || reaction.terminal) return;
|
|
701
|
+
reaction.terminal = true;
|
|
702
|
+
const terminalName = outcome === 'success'
|
|
703
|
+
? DINGTALK_DONE_REACTION_NAME
|
|
704
|
+
: outcome === 'error' ? DINGTALK_ERROR_REACTION_NAME : null;
|
|
705
|
+
// Preserve attach -> recall -> terminal ordering without extending the message task.
|
|
706
|
+
void reaction.attached.then(async (attached) => {
|
|
707
|
+
let cleaned = await this.#runReactionCall(
|
|
708
|
+
'recallReaction',
|
|
709
|
+
reaction.target,
|
|
710
|
+
DINGTALK_THINKING_REACTION_NAME,
|
|
711
|
+
'remove',
|
|
712
|
+
);
|
|
713
|
+
if (!attached || !cleaned) {
|
|
714
|
+
await new Promise((resolve) => {
|
|
715
|
+
const retry = setTimeout(resolve, Math.min(1_000, this.#reactionTimeoutMs));
|
|
716
|
+
retry.unref?.();
|
|
717
|
+
});
|
|
718
|
+
cleaned = await this.#runReactionCall(
|
|
719
|
+
'recallReaction',
|
|
720
|
+
reaction.target,
|
|
721
|
+
DINGTALK_THINKING_REACTION_NAME,
|
|
722
|
+
'remove',
|
|
723
|
+
) || cleaned;
|
|
724
|
+
}
|
|
725
|
+
if (!cleaned || !terminalName || this.#signal?.aborted) return;
|
|
726
|
+
await this.#runReactionCall('addReaction', reaction.target, terminalName, 'add');
|
|
727
|
+
}).catch(() => undefined);
|
|
570
728
|
}
|
|
571
729
|
|
|
572
730
|
#enqueueMessage(message, messageId, sender, key, {
|
|
573
731
|
releaseMessageId = true,
|
|
574
732
|
alreadyRecorded = false,
|
|
575
733
|
batchSubmission = null,
|
|
734
|
+
statusReaction = null,
|
|
576
735
|
} = {}) {
|
|
577
736
|
let hasSafeReplyRoute = false;
|
|
578
737
|
try {
|
|
@@ -596,6 +755,7 @@ export class DingtalkHarnessBridge {
|
|
|
596
755
|
alreadyRecorded,
|
|
597
756
|
preparedMessage,
|
|
598
757
|
batchSubmission,
|
|
758
|
+
statusReaction,
|
|
599
759
|
}))
|
|
600
760
|
.finally(() => {
|
|
601
761
|
if (releaseMessageId) this.#acceptedMessageIds.delete(messageId);
|
|
@@ -648,7 +808,7 @@ export class DingtalkHarnessBridge {
|
|
|
648
808
|
this.#status.lastError = null;
|
|
649
809
|
}
|
|
650
810
|
|
|
651
|
-
#finishBatchResult(messageId, sessionWebhook, result) {
|
|
811
|
+
#finishBatchResult(messageId, sessionWebhook, result, statusReaction) {
|
|
652
812
|
let task;
|
|
653
813
|
task = Promise.resolve().then(async () => {
|
|
654
814
|
if (this.#state.hasSeen(messageId)) return;
|
|
@@ -658,10 +818,18 @@ export class DingtalkHarnessBridge {
|
|
|
658
818
|
if (result.message) await this.#send(sessionWebhook, result.message);
|
|
659
819
|
this.#status.lastError = null;
|
|
660
820
|
}).catch(async (error) => {
|
|
661
|
-
if (this.#signal?.aborted)
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
821
|
+
if (this.#signal?.aborted) {
|
|
822
|
+
this.#finishStatusReaction(statusReaction, 'clear');
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
this.#finishStatusReaction(statusReaction, 'error');
|
|
826
|
+
this.#status.lastError = error?.message ?? String(error);
|
|
827
|
+
const failure = setLastMessageFailure(this.#status, error);
|
|
828
|
+
this.#logger.error?.(
|
|
829
|
+
`[dsh-dingtalk] failed to process a batch input message [${failure.referenceId}]`,
|
|
830
|
+
safeErrorDiagnostic(error),
|
|
831
|
+
);
|
|
832
|
+
await this.#send(sessionWebhook, messageFailureText(failure)).catch(() => undefined);
|
|
665
833
|
}).finally(() => {
|
|
666
834
|
this.#acceptedMessageIds.delete(messageId);
|
|
667
835
|
this.#commandTasks.delete(task);
|
|
@@ -674,6 +842,7 @@ export class DingtalkHarnessBridge {
|
|
|
674
842
|
alreadyRecorded = false,
|
|
675
843
|
preparedMessage,
|
|
676
844
|
batchSubmission = null,
|
|
845
|
+
statusReaction = null,
|
|
677
846
|
} = {}) {
|
|
678
847
|
this.#signal?.throwIfAborted();
|
|
679
848
|
if (!alreadyRecorded) {
|
|
@@ -819,7 +988,7 @@ export class DingtalkHarnessBridge {
|
|
|
819
988
|
});
|
|
820
989
|
}
|
|
821
990
|
} catch (error) {
|
|
822
|
-
textDeliveryError = error;
|
|
991
|
+
textDeliveryError = channelDeliveryFailure(error);
|
|
823
992
|
}
|
|
824
993
|
const delivery = await this.#deliverArtifacts(
|
|
825
994
|
fileTarget(message, sender, this.#clientId),
|
|
@@ -829,9 +998,15 @@ export class DingtalkHarnessBridge {
|
|
|
829
998
|
textReceipt,
|
|
830
999
|
);
|
|
831
1000
|
if (textDeliveryError && !delivery.userVisible) throw textDeliveryError;
|
|
1001
|
+
if (textDeliveryError && delivery.artifactSendErrors === 0) {
|
|
1002
|
+
setLastMessageFailure(this.#status, textDeliveryError);
|
|
1003
|
+
}
|
|
832
1004
|
increment(this.#status, 'messagesReplied');
|
|
833
1005
|
this.#status.lastReplyAt = new Date().toISOString();
|
|
834
1006
|
this.#status.lastError = null;
|
|
1007
|
+
if (!textDeliveryError && delivery.artifactSendErrors === 0) {
|
|
1008
|
+
clearLastMessageFailure(this.#status);
|
|
1009
|
+
}
|
|
835
1010
|
return delivery.receipt;
|
|
836
1011
|
} catch (error) {
|
|
837
1012
|
let batchFailureMessage = null;
|
|
@@ -844,19 +1019,28 @@ export class DingtalkHarnessBridge {
|
|
|
844
1019
|
batchSettled = true;
|
|
845
1020
|
}
|
|
846
1021
|
if (error?.code === 'turn-stopped') {
|
|
1022
|
+
this.#finishStatusReaction(statusReaction, 'clear');
|
|
847
1023
|
if (cardStarted) await cardStream.finish(t('已停止。')).catch(() => undefined);
|
|
848
1024
|
return;
|
|
849
1025
|
}
|
|
850
|
-
if (this.#signal?.aborted)
|
|
851
|
-
|
|
1026
|
+
if (this.#signal?.aborted) {
|
|
1027
|
+
this.#finishStatusReaction(statusReaction, 'clear');
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
this.#finishStatusReaction(statusReaction, 'error');
|
|
1031
|
+
this.#status.lastError = error?.message ?? String(error);
|
|
1032
|
+
const userMessage = inboundFileUserMessage(error)
|
|
1033
|
+
?? dingtalkImageErrorUserMessage(error);
|
|
1034
|
+
const failure = setLastMessageFailure(this.#status, error, {
|
|
1035
|
+
userMessage,
|
|
1036
|
+
reason: imagePromptDiagnostic(error)?.reason,
|
|
1037
|
+
});
|
|
852
1038
|
this.#logger.error?.(
|
|
853
|
-
|
|
1039
|
+
`[dsh-dingtalk] failed to process an inbound message [${failure.referenceId}]`,
|
|
854
1040
|
safeErrorDiagnostic(error),
|
|
855
1041
|
);
|
|
856
1042
|
try {
|
|
857
|
-
const errorText =
|
|
858
|
-
?? dingtalkImageErrorUserMessage(error)
|
|
859
|
-
?? t(CARD_ERROR_TEXT);
|
|
1043
|
+
const errorText = messageFailureText(failure);
|
|
860
1044
|
const visibleError = batchFailureMessage
|
|
861
1045
|
? `${errorText}\n\n${batchFailureMessage}`
|
|
862
1046
|
: errorText;
|
|
@@ -871,7 +1055,14 @@ export class DingtalkHarnessBridge {
|
|
|
871
1055
|
}
|
|
872
1056
|
}
|
|
873
1057
|
|
|
874
|
-
async #processInteractionReply(
|
|
1058
|
+
async #processInteractionReply(
|
|
1059
|
+
message,
|
|
1060
|
+
messageId,
|
|
1061
|
+
sender,
|
|
1062
|
+
key,
|
|
1063
|
+
expected,
|
|
1064
|
+
statusReaction,
|
|
1065
|
+
) {
|
|
875
1066
|
this.#signal?.throwIfAborted();
|
|
876
1067
|
const current = this.#pendingInteractions.get(key);
|
|
877
1068
|
const claimed = expected.claimedReplyMessageId === messageId;
|
|
@@ -879,7 +1070,10 @@ export class DingtalkHarnessBridge {
|
|
|
879
1070
|
if (claimed && (!current || current !== expected)) {
|
|
880
1071
|
return this.#discardResolvedInteractionReply(message, messageId);
|
|
881
1072
|
}
|
|
882
|
-
return this.#enqueueMessage(message, messageId, sender, key, {
|
|
1073
|
+
return this.#enqueueMessage(message, messageId, sender, key, {
|
|
1074
|
+
releaseMessageId: false,
|
|
1075
|
+
statusReaction,
|
|
1076
|
+
});
|
|
883
1077
|
}
|
|
884
1078
|
if (this.#state.hasSeen(messageId)) return;
|
|
885
1079
|
await this.#state.markSeen(messageId);
|
|
@@ -924,6 +1118,7 @@ export class DingtalkHarnessBridge {
|
|
|
924
1118
|
return this.#enqueueMessage(message, messageId, sender, key, {
|
|
925
1119
|
releaseMessageId: false,
|
|
926
1120
|
alreadyRecorded: true,
|
|
1121
|
+
statusReaction,
|
|
927
1122
|
});
|
|
928
1123
|
}
|
|
929
1124
|
pending.sessionWebhook = sessionWebhook;
|
|
@@ -931,6 +1126,7 @@ export class DingtalkHarnessBridge {
|
|
|
931
1126
|
try {
|
|
932
1127
|
await this.#presentInteraction(pending);
|
|
933
1128
|
} catch {
|
|
1129
|
+
this.#finishStatusReaction(statusReaction, 'error');
|
|
934
1130
|
this.#status.lastError = t('钉钉交互问题发送失败。');
|
|
935
1131
|
this.#logger.error?.('[dsh-dingtalk] failed to retry an interaction question');
|
|
936
1132
|
pending.interaction.reconnect?.();
|
|
@@ -950,6 +1146,7 @@ export class DingtalkHarnessBridge {
|
|
|
950
1146
|
try {
|
|
951
1147
|
await this.#presentInteraction(pending);
|
|
952
1148
|
} catch {
|
|
1149
|
+
this.#finishStatusReaction(statusReaction, 'error');
|
|
953
1150
|
this.#status.lastError = t('钉钉交互问题发送失败。');
|
|
954
1151
|
this.#logger.error?.('[dsh-dingtalk] failed to send the next interaction question');
|
|
955
1152
|
pending.interaction.reconnect?.();
|
|
@@ -969,7 +1166,10 @@ export class DingtalkHarnessBridge {
|
|
|
969
1166
|
this.#clearPendingInteraction(key, pending.interactionId);
|
|
970
1167
|
this.#status.lastError = null;
|
|
971
1168
|
} catch (error) {
|
|
972
|
-
if (this.#signal?.aborted)
|
|
1169
|
+
if (this.#signal?.aborted) {
|
|
1170
|
+
this.#finishStatusReaction(statusReaction, 'clear');
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
973
1173
|
if (this.#pendingInteractions.get(key) !== pending) return;
|
|
974
1174
|
if (error?.code === 'interaction-not-pending') {
|
|
975
1175
|
this.#clearPendingInteraction(key, pending.interactionId);
|
|
@@ -980,6 +1180,7 @@ export class DingtalkHarnessBridge {
|
|
|
980
1180
|
}
|
|
981
1181
|
return;
|
|
982
1182
|
}
|
|
1183
|
+
this.#finishStatusReaction(statusReaction, 'error');
|
|
983
1184
|
pending.submitting = false;
|
|
984
1185
|
pending.answers.pop();
|
|
985
1186
|
pending.index -= 1;
|
|
@@ -1201,9 +1402,13 @@ export class DingtalkHarnessBridge {
|
|
|
1201
1402
|
sendFile: typeof this.#api.sendFile === 'function'
|
|
1202
1403
|
? (file) => sendArtifact('sendFile', file)
|
|
1203
1404
|
: undefined,
|
|
1204
|
-
|
|
1405
|
+
onFailure: (artifact, error) => setLastMessageFailure(this.#status, error, {
|
|
1406
|
+
userMessage: artifactFailureText(artifact?.fileName, error),
|
|
1407
|
+
reason: error?.code,
|
|
1408
|
+
}),
|
|
1409
|
+
sendFailureNotice: (_artifact, _error, failure) => this.#send(
|
|
1205
1410
|
sessionWebhook,
|
|
1206
|
-
|
|
1411
|
+
messageFailureText(failure),
|
|
1207
1412
|
),
|
|
1208
1413
|
logger: this.#logger,
|
|
1209
1414
|
});
|
|
@@ -1211,7 +1416,11 @@ export class DingtalkHarnessBridge {
|
|
|
1211
1416
|
+ delivery.artifactsSent;
|
|
1212
1417
|
this.#status.artifactSendErrors = (this.#status.artifactSendErrors ?? 0)
|
|
1213
1418
|
+ delivery.artifactSendErrors;
|
|
1214
|
-
return {
|
|
1419
|
+
return {
|
|
1420
|
+
receipt: delivery.receipt,
|
|
1421
|
+
userVisible: delivery.userVisible,
|
|
1422
|
+
artifactSendErrors: delivery.artifactSendErrors,
|
|
1423
|
+
};
|
|
1215
1424
|
}
|
|
1216
1425
|
}
|
|
1217
1426
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t } from '../shared/i18n.mjs';
|
|
2
2
|
|
|
3
3
|
const DEFAULT_UPDATE_INTERVAL_MS = 500;
|
|
4
|
-
const
|
|
4
|
+
const CLOSED_TEXT = '卡片已结束,请查看后续消息。';
|
|
5
5
|
|
|
6
6
|
function requiredText(value, name) {
|
|
7
7
|
if (typeof value !== 'string') throw new TypeError(`${name} must be a string`);
|
|
@@ -102,7 +102,7 @@ export function createDingTalkCardStream({
|
|
|
102
102
|
if (!cleanupPromise) {
|
|
103
103
|
cleanupPromise = api.failAiCard({
|
|
104
104
|
...cardRequest,
|
|
105
|
-
text: t(
|
|
105
|
+
text: t(CLOSED_TEXT),
|
|
106
106
|
signal: AbortSignal.timeout(5_000),
|
|
107
107
|
}).then(
|
|
108
108
|
() => true,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
connectionTestTargetUnavailable,
|
|
12
12
|
} from '../shared/connection-test.mjs';
|
|
13
13
|
import { t } from '../shared/i18n.mjs';
|
|
14
|
+
import { publicMessageFailure } from '../shared/message-failure.mjs';
|
|
14
15
|
|
|
15
16
|
const ACTIVE_ATTEMPT_STATES = new Set(['starting', 'pending', 'connecting']);
|
|
16
17
|
const TERMINAL_ATTEMPT_STATES = new Set(['connected', 'expired', 'failed', 'cancelled']);
|
|
@@ -516,6 +517,7 @@ export class DingtalkController {
|
|
|
516
517
|
messagesReceived: Number(currentStatus.messagesReceived) || 0,
|
|
517
518
|
messagesReplied: Number(currentStatus.messagesReplied) || 0,
|
|
518
519
|
},
|
|
520
|
+
lastMessageError: publicMessageFailure(currentStatus.lastMessageError),
|
|
519
521
|
senders: {
|
|
520
522
|
pending,
|
|
521
523
|
approved: config.approvedSenders.map(publicApprovedSender),
|
|
@@ -88,6 +88,12 @@ function snowflake(value, name) {
|
|
|
88
88
|
return id;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
function reactionEmoji(value) {
|
|
92
|
+
const emoji = cleanString(value);
|
|
93
|
+
if (!emoji) throw new TypeError('A Discord reaction emoji is required');
|
|
94
|
+
return emoji;
|
|
95
|
+
}
|
|
96
|
+
|
|
91
97
|
export function validDiscordToken(value) {
|
|
92
98
|
return typeof value === 'string'
|
|
93
99
|
&& /^[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{20,}$/.test(value.trim());
|
|
@@ -229,6 +235,35 @@ export class DiscordApi {
|
|
|
229
235
|
});
|
|
230
236
|
}
|
|
231
237
|
|
|
238
|
+
async addOwnReaction({ channelId, messageId, emoji, signal } = {}) {
|
|
239
|
+
const normalizedEmoji = reactionEmoji(emoji);
|
|
240
|
+
await this.#request(
|
|
241
|
+
`channels/${snowflake(channelId, 'channel id')}/messages/${snowflake(messageId, 'message id')}`
|
|
242
|
+
+ `/reactions/${encodeURIComponent(normalizedEmoji)}/@me`,
|
|
243
|
+
{
|
|
244
|
+
method: 'PUT',
|
|
245
|
+
signal,
|
|
246
|
+
expectBody: false,
|
|
247
|
+
retry: false,
|
|
248
|
+
},
|
|
249
|
+
);
|
|
250
|
+
return normalizedEmoji;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
removeOwnReaction({ channelId, messageId, emoji, signal } = {}) {
|
|
254
|
+
const normalizedEmoji = reactionEmoji(emoji);
|
|
255
|
+
return this.#request(
|
|
256
|
+
`channels/${snowflake(channelId, 'channel id')}/messages/${snowflake(messageId, 'message id')}`
|
|
257
|
+
+ `/reactions/${encodeURIComponent(normalizedEmoji)}/@me`,
|
|
258
|
+
{
|
|
259
|
+
method: 'DELETE',
|
|
260
|
+
signal,
|
|
261
|
+
expectBody: false,
|
|
262
|
+
retry: false,
|
|
263
|
+
},
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
232
267
|
async #request(path, {
|
|
233
268
|
method,
|
|
234
269
|
body,
|
|
@@ -4,6 +4,7 @@ export const DISCORD_DESCRIPTOR = Object.freeze({
|
|
|
4
4
|
key: 'discord',
|
|
5
5
|
label: 'Discord',
|
|
6
6
|
connectionLabel: ' Gateway 长连接',
|
|
7
|
+
reactions: Object.freeze({ processing: '👀', success: '✅', error: '❌' }),
|
|
7
8
|
});
|
|
8
9
|
|
|
9
10
|
export class DiscordHarnessBridge extends TextHarnessBridge {
|
|
@@ -231,6 +231,10 @@ export function normalizeDiscordMessage(message, botId, { fetchImpl = fetch } =
|
|
|
231
231
|
channelId: String(message.channel_id),
|
|
232
232
|
replyToMessageId: String(message.id),
|
|
233
233
|
},
|
|
234
|
+
reactionTarget: {
|
|
235
|
+
channelId: String(message.channel_id),
|
|
236
|
+
messageId: String(message.id),
|
|
237
|
+
},
|
|
234
238
|
connectionTestTarget: { channelId: String(message.channel_id) },
|
|
235
239
|
};
|
|
236
240
|
}
|
|
@@ -350,6 +354,24 @@ export class DiscordBotClient {
|
|
|
350
354
|
});
|
|
351
355
|
}
|
|
352
356
|
|
|
357
|
+
addReaction(target, emoji, { signal } = {}) {
|
|
358
|
+
return this.#api.addOwnReaction({
|
|
359
|
+
channelId: target.channelId,
|
|
360
|
+
messageId: target.messageId,
|
|
361
|
+
emoji,
|
|
362
|
+
signal: this.#operationSignal(signal),
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
removeReaction(target, reactionKey, { signal } = {}) {
|
|
367
|
+
return this.#api.removeOwnReaction({
|
|
368
|
+
channelId: target.channelId,
|
|
369
|
+
messageId: target.messageId,
|
|
370
|
+
emoji: reactionKey,
|
|
371
|
+
signal: this.#operationSignal(signal),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
353
375
|
async openStream(target) {
|
|
354
376
|
const notice = !this.#deliveredNotices.has(target) && target?.notice
|
|
355
377
|
? String(target.notice) : null;
|
|
@@ -381,6 +403,10 @@ export class DiscordBotClient {
|
|
|
381
403
|
});
|
|
382
404
|
return stream.start();
|
|
383
405
|
}
|
|
406
|
+
|
|
407
|
+
#operationSignal(signal) {
|
|
408
|
+
return signal ?? this.#signal;
|
|
409
|
+
}
|
|
384
410
|
}
|
|
385
411
|
|
|
386
412
|
export function createDiscordRuntimeStatus() {
|