@xmanrui/dsh-im 0.7.1 → 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 +113 -112
- package/package.json +1 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +66 -3
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/feishu/bridge.mjs +52 -3
- package/src/channels/qq/qq-bridge.mjs +51 -3
- package/src/channels/shared/harness-approval.mjs +472 -0
- package/src/channels/shared/harness-client.mjs +37 -4
- package/src/channels/shared/text-harness-bridge.mjs +54 -4
- package/src/channels/wecom/wecom-bridge.mjs +51 -3
- package/src/channels/weixin/weixin-api.mjs +1 -1
- package/src/channels/weixin/weixin-bridge.mjs +52 -3
package/package.json
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
harnessQuestionText,
|
|
9
9
|
validHarnessQuestion,
|
|
10
10
|
} from '../shared/harness-question.mjs';
|
|
11
|
+
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
|
11
12
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
12
13
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
13
14
|
|
|
@@ -123,7 +124,9 @@ export class DingtalkHarnessBridge {
|
|
|
123
124
|
#queues = new Map();
|
|
124
125
|
#pendingInteractions = new Map();
|
|
125
126
|
#interactionKeys = new Map();
|
|
127
|
+
#interactionTasks = new Set();
|
|
126
128
|
#acceptedMessageIds = new Set();
|
|
129
|
+
#approvals;
|
|
127
130
|
|
|
128
131
|
constructor({
|
|
129
132
|
api,
|
|
@@ -149,6 +152,7 @@ export class DingtalkHarnessBridge {
|
|
|
149
152
|
this.#state = state;
|
|
150
153
|
this.#status = status;
|
|
151
154
|
this.#logger = logger;
|
|
155
|
+
this.#approvals = new HarnessApprovalQueue({ label: 'DingTalk', logger });
|
|
152
156
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
153
157
|
this.#maxMessageChars = maxMessageChars;
|
|
154
158
|
this.#signal = signal;
|
|
@@ -179,7 +183,57 @@ export class DingtalkHarnessBridge {
|
|
|
179
183
|
return Promise.resolve();
|
|
180
184
|
}
|
|
181
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
|
+
}
|
|
182
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
|
+
|
|
183
237
|
if (pending && pending.actor !== sender) {
|
|
184
238
|
return this.#enqueueMessage(message, messageId, sender, key);
|
|
185
239
|
}
|
|
@@ -233,6 +287,7 @@ export class DingtalkHarnessBridge {
|
|
|
233
287
|
...[...this.#pendingInteractions.values()].flatMap((pending) => (
|
|
234
288
|
pending.queue ? [pending.queue] : []
|
|
235
289
|
)),
|
|
290
|
+
...this.#interactionTasks,
|
|
236
291
|
]);
|
|
237
292
|
}
|
|
238
293
|
|
|
@@ -344,6 +399,7 @@ export class DingtalkHarnessBridge {
|
|
|
344
399
|
}
|
|
345
400
|
} finally {
|
|
346
401
|
await this.#cancelPendingInteraction(key);
|
|
402
|
+
await this.#approvals.closeRoute(key);
|
|
347
403
|
}
|
|
348
404
|
}
|
|
349
405
|
|
|
@@ -475,8 +531,14 @@ export class DingtalkHarnessBridge {
|
|
|
475
531
|
sessionWebhook,
|
|
476
532
|
requiresMention,
|
|
477
533
|
}) {
|
|
478
|
-
|
|
479
|
-
|
|
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.
|
|
480
542
|
if (interaction?.kind !== 'question') return;
|
|
481
543
|
const questions = interaction?.payload?.questions;
|
|
482
544
|
const interactionId = typeof interaction?.interactionId === 'string'
|
|
@@ -550,7 +612,8 @@ export class DingtalkHarnessBridge {
|
|
|
550
612
|
await this.#presentInteraction(pending);
|
|
551
613
|
}
|
|
552
614
|
|
|
553
|
-
#handleInteractionResolved(resolution) {
|
|
615
|
+
async #handleInteractionResolved(resolution) {
|
|
616
|
+
if (await this.#approvals.handleResolved(resolution)) return;
|
|
554
617
|
const interactionId = resolution?.interactionId;
|
|
555
618
|
if (resolution?.kind !== 'question' || typeof interactionId !== 'string') return;
|
|
556
619
|
const key = this.#interactionKeys.get(interactionId);
|
|
@@ -108,7 +108,7 @@ export class DiscordApi {
|
|
|
108
108
|
headers: {
|
|
109
109
|
authorization: `Bot ${this.#token}`,
|
|
110
110
|
'content-type': 'application/json',
|
|
111
|
-
'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.7.
|
|
111
|
+
'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.7.2)',
|
|
112
112
|
},
|
|
113
113
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
114
114
|
signal: requestSignal(signal, timeoutMs),
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
harnessQuestionText,
|
|
11
11
|
validHarnessQuestion,
|
|
12
12
|
} from '../shared/harness-question.mjs';
|
|
13
|
+
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
|
13
14
|
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
14
15
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
15
16
|
|
|
@@ -67,6 +68,7 @@ export class FeishuHarnessBridge {
|
|
|
67
68
|
#resolvedQuestionReplies = new Map();
|
|
68
69
|
#acceptedMessageIds = new Set();
|
|
69
70
|
#interactionTasks = new Set();
|
|
71
|
+
#approvals;
|
|
70
72
|
#status;
|
|
71
73
|
#allowedSenderOpenIds;
|
|
72
74
|
#replyTimeoutMs;
|
|
@@ -95,6 +97,7 @@ export class FeishuHarnessBridge {
|
|
|
95
97
|
this.#allowedSenderOpenIds = allowedSenderOpenIds;
|
|
96
98
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
97
99
|
this.#logger = logger;
|
|
100
|
+
this.#approvals = new HarnessApprovalQueue({ label: 'Feishu', logger });
|
|
98
101
|
this.#signal = signal;
|
|
99
102
|
ensureStatus(this.#status);
|
|
100
103
|
}
|
|
@@ -138,6 +141,44 @@ export class FeishuHarnessBridge {
|
|
|
138
141
|
return current;
|
|
139
142
|
}
|
|
140
143
|
const pending = this.#pendingInteractions.get(key);
|
|
144
|
+
const approvalReply = this.#approvals.claimReply({
|
|
145
|
+
key,
|
|
146
|
+
actor: senderOpenId(event),
|
|
147
|
+
messageId,
|
|
148
|
+
text: extractText(event) ?? '',
|
|
149
|
+
addressed: event?.message?.chat_type === 'p2p'
|
|
150
|
+
|| (Array.isArray(event?.message?.mentions) && event.message.mentions.length > 0),
|
|
151
|
+
hasPendingQuestion: Boolean(pending),
|
|
152
|
+
questionCompletion: pending?.submitting || pending?.claimedReplyMessageId
|
|
153
|
+
? pending.queue
|
|
154
|
+
: null,
|
|
155
|
+
isQuestionPending: () => this.#pendingInteractions.has(key),
|
|
156
|
+
send: (text) => this.#send(event.message.chat_id, text),
|
|
157
|
+
});
|
|
158
|
+
if (approvalReply) {
|
|
159
|
+
const processing = approvalReply.process(async () => {
|
|
160
|
+
if (this.#state.hasSeen(messageId)) return false;
|
|
161
|
+
await this.#state.markSeen(messageId);
|
|
162
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
163
|
+
this.#status.messagesReceived += 1;
|
|
164
|
+
return true;
|
|
165
|
+
});
|
|
166
|
+
let current;
|
|
167
|
+
current = processing
|
|
168
|
+
.then(() => this.#finishReaction(messageId, processingReaction, 'DONE'))
|
|
169
|
+
.catch((error) => this.#handleMessageFailure(
|
|
170
|
+
event,
|
|
171
|
+
messageId,
|
|
172
|
+
processingReaction,
|
|
173
|
+
error,
|
|
174
|
+
))
|
|
175
|
+
.finally(() => {
|
|
176
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
177
|
+
this.#interactionTasks.delete(current);
|
|
178
|
+
});
|
|
179
|
+
this.#interactionTasks.add(current);
|
|
180
|
+
return current;
|
|
181
|
+
}
|
|
141
182
|
if (pending && senderOpenId(event) !== pending.actor) {
|
|
142
183
|
return this.#enqueueMessage(event, messageId, key, processingReaction);
|
|
143
184
|
}
|
|
@@ -290,6 +331,7 @@ export class FeishuHarnessBridge {
|
|
|
290
331
|
this.#status.lastError = null;
|
|
291
332
|
} finally {
|
|
292
333
|
await this.#cancelPendingInteraction(key);
|
|
334
|
+
await this.#approvals.closeRoute(key);
|
|
293
335
|
}
|
|
294
336
|
}
|
|
295
337
|
|
|
@@ -490,8 +532,14 @@ export class FeishuHarnessBridge {
|
|
|
490
532
|
chatId,
|
|
491
533
|
requiresMention,
|
|
492
534
|
}) {
|
|
493
|
-
|
|
494
|
-
|
|
535
|
+
if (await this.#approvals.handleRequested(interaction, {
|
|
536
|
+
key,
|
|
537
|
+
actor,
|
|
538
|
+
requiresMention,
|
|
539
|
+
send: (text) => this.#send(chatId, text),
|
|
540
|
+
})) return;
|
|
541
|
+
|
|
542
|
+
// Approval requests return above; the existing question state machine stays unchanged.
|
|
495
543
|
if (interaction?.kind !== 'question') return;
|
|
496
544
|
const questions = interaction?.payload?.questions;
|
|
497
545
|
const interactionId = typeof interaction?.interactionId === 'string'
|
|
@@ -566,7 +614,8 @@ export class FeishuHarnessBridge {
|
|
|
566
614
|
await this.#presentInteraction(pending);
|
|
567
615
|
}
|
|
568
616
|
|
|
569
|
-
#handleInteractionResolved(resolution) {
|
|
617
|
+
async #handleInteractionResolved(resolution) {
|
|
618
|
+
if (await this.#approvals.handleResolved(resolution)) return;
|
|
570
619
|
const interactionId = resolution?.interactionId;
|
|
571
620
|
if (resolution?.kind !== 'question' || typeof interactionId !== 'string') return;
|
|
572
621
|
const key = this.#interactionKeys.get(interactionId);
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
harnessQuestionText,
|
|
5
5
|
validHarnessQuestion,
|
|
6
6
|
} from '../shared/harness-question.mjs';
|
|
7
|
+
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
|
|
7
8
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
8
9
|
|
|
9
10
|
const INTERACTION_RESOLVED_TEXT = '这个问题已在其他客户端处理,无需再次回答。';
|
|
@@ -65,6 +66,8 @@ export class QqHarnessBridge {
|
|
|
65
66
|
#pendingInteractions = new Map();
|
|
66
67
|
#interactionKeys = new Map();
|
|
67
68
|
#acceptedMessageIds = new Set();
|
|
69
|
+
#approvalTasks = new Set();
|
|
70
|
+
#approvals;
|
|
68
71
|
|
|
69
72
|
constructor({
|
|
70
73
|
bot,
|
|
@@ -87,6 +90,7 @@ export class QqHarnessBridge {
|
|
|
87
90
|
this.#logger = logger;
|
|
88
91
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
89
92
|
this.#signal = signal;
|
|
93
|
+
this.#approvals = new HarnessApprovalQueue({ label: 'qq', logger });
|
|
90
94
|
}
|
|
91
95
|
|
|
92
96
|
get status() {
|
|
@@ -104,6 +108,35 @@ export class QqHarnessBridge {
|
|
|
104
108
|
const key = conversationKey(message);
|
|
105
109
|
this.#acceptedMessageIds.add(messageId);
|
|
106
110
|
const pending = this.#pendingInteractions.get(key);
|
|
111
|
+
const approval = this.#approvals.claimReply({
|
|
112
|
+
key,
|
|
113
|
+
actor: sender,
|
|
114
|
+
messageId,
|
|
115
|
+
text: safeText(message),
|
|
116
|
+
addressed: message.kind !== 'group' || message.rawEventType === 'GROUP_AT_MESSAGE_CREATE',
|
|
117
|
+
hasPendingQuestion: Boolean(pending),
|
|
118
|
+
questionCompletion: pending?.submitting || pending?.claimedReplyMessageId
|
|
119
|
+
? pending.queue
|
|
120
|
+
: null,
|
|
121
|
+
isQuestionPending: () => this.#pendingInteractions.has(key),
|
|
122
|
+
send: (text) => this.#bot.sendText(message.replyTarget, text),
|
|
123
|
+
});
|
|
124
|
+
if (approval) {
|
|
125
|
+
let task;
|
|
126
|
+
task = approval.process(async () => {
|
|
127
|
+
if (this.#state.hasSeen(messageId)) return false;
|
|
128
|
+
await this.#state.markSeen(messageId);
|
|
129
|
+
this.#status.messagesReceived += 1;
|
|
130
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
131
|
+
return true;
|
|
132
|
+
})
|
|
133
|
+
.finally(() => {
|
|
134
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
135
|
+
this.#approvalTasks.delete(task);
|
|
136
|
+
});
|
|
137
|
+
this.#approvalTasks.add(task);
|
|
138
|
+
return task;
|
|
139
|
+
}
|
|
107
140
|
if (pending && sender !== pending.actor) {
|
|
108
141
|
return this.#enqueueMessage(message, messageId, key);
|
|
109
142
|
}
|
|
@@ -152,6 +185,7 @@ export class QqHarnessBridge {
|
|
|
152
185
|
...[...this.#pendingInteractions.values()].flatMap((pending) => (
|
|
153
186
|
pending.queue ? [pending.queue] : []
|
|
154
187
|
)),
|
|
188
|
+
...this.#approvalTasks,
|
|
155
189
|
]);
|
|
156
190
|
}
|
|
157
191
|
|
|
@@ -247,7 +281,10 @@ export class QqHarnessBridge {
|
|
|
247
281
|
},
|
|
248
282
|
}));
|
|
249
283
|
} finally {
|
|
250
|
-
await
|
|
284
|
+
await Promise.allSettled([
|
|
285
|
+
this.#cancelPendingInteraction(key),
|
|
286
|
+
this.#approvals.closeRoute(key),
|
|
287
|
+
]);
|
|
251
288
|
}
|
|
252
289
|
if (stream) {
|
|
253
290
|
try {
|
|
@@ -388,7 +425,14 @@ export class QqHarnessBridge {
|
|
|
388
425
|
target,
|
|
389
426
|
requiresMention,
|
|
390
427
|
}) {
|
|
391
|
-
|
|
428
|
+
if (interaction?.kind === 'approval') {
|
|
429
|
+
return this.#approvals.handleRequested(interaction, {
|
|
430
|
+
key,
|
|
431
|
+
actor,
|
|
432
|
+
requiresMention,
|
|
433
|
+
send: (text) => this.#bot.sendText(target, text),
|
|
434
|
+
});
|
|
435
|
+
}
|
|
392
436
|
if (interaction?.kind !== 'question') return;
|
|
393
437
|
const questions = interaction?.payload?.questions;
|
|
394
438
|
const interactionId = typeof interaction?.interactionId === 'string'
|
|
@@ -461,7 +505,11 @@ export class QqHarnessBridge {
|
|
|
461
505
|
await this.#presentInteraction(pending);
|
|
462
506
|
}
|
|
463
507
|
|
|
464
|
-
#handleInteractionResolved(resolution) {
|
|
508
|
+
async #handleInteractionResolved(resolution) {
|
|
509
|
+
if (resolution?.kind === 'approval') {
|
|
510
|
+
await this.#approvals.handleResolved(resolution);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
465
513
|
const interactionId = resolution?.interactionId;
|
|
466
514
|
if (resolution?.kind !== 'question' || typeof interactionId !== 'string') return;
|
|
467
515
|
const key = this.#interactionKeys.get(interactionId);
|