@xmanrui/dsh-im 0.7.1 → 0.8.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 +152 -0
- package/README.md +25 -131
- package/lib/index.js +116 -114
- package/package.json +2 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +67 -4
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/feishu/bridge.mjs +53 -4
- package/src/channels/qq/qq-bridge.mjs +52 -4
- package/src/channels/shared/harness-approval.mjs +472 -0
- package/src/channels/shared/harness-client.mjs +72 -5
- package/src/channels/shared/text-harness-bridge.mjs +55 -5
- package/src/channels/shared/workspace-command.mjs +70 -6
- package/src/channels/wecom/wecom-bridge.mjs +52 -4
- package/src/channels/weixin/weixin-api.mjs +1 -1
- package/src/channels/weixin/weixin-bridge.mjs +53 -4
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { runWorkspaceCommand } from './workspace-command.mjs';
|
|
2
2
|
import { askInWorkspaceSession } from './workspace-session.mjs';
|
|
3
|
+
import { HarnessApprovalQueue } from './harness-approval.mjs';
|
|
3
4
|
import {
|
|
4
5
|
harnessAnswerForQuestion,
|
|
5
6
|
harnessQuestionText,
|
|
@@ -43,6 +44,8 @@ export class TextHarnessBridge {
|
|
|
43
44
|
#pendingInteractions = new Map();
|
|
44
45
|
#interactionKeys = new Map();
|
|
45
46
|
#acceptedMessageIds = new Set();
|
|
47
|
+
#approvalTasks = new Set();
|
|
48
|
+
#approvals;
|
|
46
49
|
|
|
47
50
|
constructor({
|
|
48
51
|
descriptor,
|
|
@@ -65,6 +68,10 @@ export class TextHarnessBridge {
|
|
|
65
68
|
this.#logger = logger;
|
|
66
69
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
67
70
|
this.#signal = signal;
|
|
71
|
+
this.#approvals = new HarnessApprovalQueue({
|
|
72
|
+
label: descriptor.key,
|
|
73
|
+
logger,
|
|
74
|
+
});
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
get status() {
|
|
@@ -86,6 +93,35 @@ export class TextHarnessBridge {
|
|
|
86
93
|
|
|
87
94
|
const key = `${kind}:${conversationId}`;
|
|
88
95
|
const pending = this.#pendingInteractions.get(key);
|
|
96
|
+
const approval = this.#approvals.claimReply({
|
|
97
|
+
key,
|
|
98
|
+
actor: senderId,
|
|
99
|
+
messageId,
|
|
100
|
+
text: normalized.content,
|
|
101
|
+
addressed: normalized.kind !== 'group' || normalized.addressed === true,
|
|
102
|
+
hasPendingQuestion: Boolean(pending),
|
|
103
|
+
questionCompletion: pending?.submitting || pending?.claimedReplyMessageId
|
|
104
|
+
? pending.queue
|
|
105
|
+
: null,
|
|
106
|
+
isQuestionPending: () => this.#pendingInteractions.has(key),
|
|
107
|
+
send: (text) => this.#bot.sendText(normalized.replyTarget, text),
|
|
108
|
+
});
|
|
109
|
+
if (approval) {
|
|
110
|
+
let task;
|
|
111
|
+
task = approval.process(async () => {
|
|
112
|
+
if (this.#state.hasSeen(messageId)) return false;
|
|
113
|
+
await this.#state.markSeen(messageId);
|
|
114
|
+
this.#status.messagesReceived += 1;
|
|
115
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
116
|
+
return true;
|
|
117
|
+
})
|
|
118
|
+
.finally(() => {
|
|
119
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
120
|
+
this.#approvalTasks.delete(task);
|
|
121
|
+
});
|
|
122
|
+
this.#approvalTasks.add(task);
|
|
123
|
+
return task;
|
|
124
|
+
}
|
|
89
125
|
if (pending && pending.actor !== senderId) {
|
|
90
126
|
return this.#enqueueMessage(normalized, messageId, senderId, key);
|
|
91
127
|
}
|
|
@@ -147,6 +183,7 @@ export class TextHarnessBridge {
|
|
|
147
183
|
...[...this.#pendingInteractions.values()].flatMap((pending) => (
|
|
148
184
|
pending.queue ? [pending.queue] : []
|
|
149
185
|
)),
|
|
186
|
+
...this.#approvalTasks,
|
|
150
187
|
]);
|
|
151
188
|
}
|
|
152
189
|
|
|
@@ -184,7 +221,7 @@ export class TextHarnessBridge {
|
|
|
184
221
|
'/workspace 工作区绝对路径 切换工作区',
|
|
185
222
|
'/workspacelist 列出工作区绝对路径',
|
|
186
223
|
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
|
187
|
-
'/session Session ID 将当前聊天绑定到指定会话',
|
|
224
|
+
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
|
188
225
|
'/status 检查连接状态',
|
|
189
226
|
'/help 显示本帮助',
|
|
190
227
|
].join('\n'));
|
|
@@ -276,7 +313,10 @@ export class TextHarnessBridge {
|
|
|
276
313
|
);
|
|
277
314
|
}
|
|
278
315
|
} finally {
|
|
279
|
-
await
|
|
316
|
+
await Promise.allSettled([
|
|
317
|
+
this.#cancelPendingInteraction(conversationKey),
|
|
318
|
+
this.#approvals.closeRoute(conversationKey),
|
|
319
|
+
]);
|
|
280
320
|
}
|
|
281
321
|
}
|
|
282
322
|
|
|
@@ -434,8 +474,14 @@ export class TextHarnessBridge {
|
|
|
434
474
|
target,
|
|
435
475
|
requiresMention,
|
|
436
476
|
}) {
|
|
437
|
-
|
|
438
|
-
|
|
477
|
+
if (interaction?.kind === 'approval') {
|
|
478
|
+
return this.#approvals.handleRequested(interaction, {
|
|
479
|
+
key,
|
|
480
|
+
actor,
|
|
481
|
+
requiresMention,
|
|
482
|
+
send: (text) => this.#bot.sendText(target, text),
|
|
483
|
+
});
|
|
484
|
+
}
|
|
439
485
|
if (interaction?.kind !== 'question') return;
|
|
440
486
|
const questions = interaction?.payload?.questions;
|
|
441
487
|
const interactionId = cleanText(interaction?.interactionId) || cleanText(interaction?.rpcId);
|
|
@@ -510,7 +556,11 @@ export class TextHarnessBridge {
|
|
|
510
556
|
await this.#presentInteraction(pending);
|
|
511
557
|
}
|
|
512
558
|
|
|
513
|
-
#handleInteractionResolved(resolution) {
|
|
559
|
+
async #handleInteractionResolved(resolution) {
|
|
560
|
+
if (resolution?.kind === 'approval') {
|
|
561
|
+
await this.#approvals.handleResolved(resolution);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
514
564
|
const interactionId = cleanText(resolution?.interactionId);
|
|
515
565
|
if (resolution?.kind !== 'question' || !interactionId) return;
|
|
516
566
|
const key = this.#interactionKeys.get(interactionId);
|
|
@@ -13,7 +13,7 @@ const MAX_COMMAND_MESSAGE_LENGTH = 1_800;
|
|
|
13
13
|
const MAX_SESSION_ID_LENGTH = 256;
|
|
14
14
|
const UNSAFE_DISPLAY_TEXT = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
|
|
15
15
|
const UNSAFE_DISPLAY_TEXT_GLOBAL = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]+/gu;
|
|
16
|
-
const SESSION_BIND_USAGE = '用法:/session Session ID';
|
|
16
|
+
const SESSION_BIND_USAGE = '用法:/session Session ID 或当前工作区序号(/session N)';
|
|
17
17
|
const SESSION_LIST_USAGE = [
|
|
18
18
|
'用法:',
|
|
19
19
|
'/sessionlist 列出当前工作区会话',
|
|
@@ -175,14 +175,35 @@ async function resolveSessionListWorkspace(selector, harness) {
|
|
|
175
175
|
return selected;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
function
|
|
178
|
+
function formatSessionRelativeTime(value) {
|
|
179
|
+
const ms = typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
180
|
+
if (ms === null) return '';
|
|
181
|
+
const date = new Date(ms);
|
|
182
|
+
if (Number.isNaN(date.getTime())) return '';
|
|
183
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
184
|
+
const hm = `${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
185
|
+
const startOfDay = (d) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
186
|
+
const now = new Date();
|
|
187
|
+
const dayDiff = Math.round((startOfDay(now) - startOfDay(date)) / 86_400_000);
|
|
188
|
+
if (dayDiff === 0) return `今天 ${hm}`;
|
|
189
|
+
if (dayDiff === 1) return `昨天 ${hm}`;
|
|
190
|
+
if (dayDiff === 2) return `前天 ${hm}`;
|
|
191
|
+
if (date.getFullYear() === now.getFullYear()) {
|
|
192
|
+
return `${date.getMonth() + 1}月${date.getDate()}日 ${hm}`;
|
|
193
|
+
}
|
|
194
|
+
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function sessionListMessage(workspace, sessions, { currentWorkspace = false } = {}) {
|
|
179
198
|
const rows = sessions.map((session) => {
|
|
180
199
|
const sessionId = safeDisplayText(session?.sessionId);
|
|
181
200
|
if (!sessionId) throw new TypeError('Harness returned an invalid session id');
|
|
182
201
|
const title = session?.summaryAvailable === false
|
|
183
202
|
? '标题暂不可用'
|
|
184
203
|
: safeDisplayText(session?.title) || '暂无标题';
|
|
185
|
-
|
|
204
|
+
const timeText = formatSessionRelativeTime(session?.time);
|
|
205
|
+
const annotation = `${timeText ? ` · ${timeText}` : ''}${session?.archived === true ? '(已归档)' : ''}`;
|
|
206
|
+
return `${title}${annotation}\n ID: ${sessionId}`;
|
|
186
207
|
});
|
|
187
208
|
if (rows.length === 0) return `工作区:${workspace}\n该工作区暂无会话。`;
|
|
188
209
|
return [
|
|
@@ -191,10 +212,19 @@ function sessionListMessage(workspace, sessions) {
|
|
|
191
212
|
'',
|
|
192
213
|
...rows.map((row, index) => `${index + 1}. ${row}`),
|
|
193
214
|
'',
|
|
194
|
-
|
|
215
|
+
currentWorkspace
|
|
216
|
+
? '绑定用法:/session Session ID 或当前工作区序号(/session N)'
|
|
217
|
+
: '绑定用法:/session Session ID\n提示:/session N 只按机器人当前工作区的序号绑定。',
|
|
195
218
|
].join('\n');
|
|
196
219
|
}
|
|
197
220
|
|
|
221
|
+
async function currentSessionListWorkspace(harness) {
|
|
222
|
+
if (typeof harness?.currentWorkspace !== 'function') return null;
|
|
223
|
+
const [current] = await existingWorkspacePaths([harness.currentWorkspace()]);
|
|
224
|
+
harness.assertWorkspaceScope?.();
|
|
225
|
+
return current ?? null;
|
|
226
|
+
}
|
|
227
|
+
|
|
198
228
|
async function runSessionListCommand(match, harness) {
|
|
199
229
|
if (typeof harness?.listWorkspaceSessions !== 'function') {
|
|
200
230
|
return commandResult('当前机器人暂不支持列出工作区会话。');
|
|
@@ -209,7 +239,10 @@ async function runSessionListCommand(match, harness) {
|
|
|
209
239
|
}
|
|
210
240
|
harness.assertWorkspaceScope?.();
|
|
211
241
|
const workspace = normalizedWorkspacePath(listed.workspace) ?? resolved.workspace;
|
|
212
|
-
const
|
|
242
|
+
const currentWorkspace = await currentSessionListWorkspace(harness);
|
|
243
|
+
const message = sessionListMessage(workspace, listed.sessions, {
|
|
244
|
+
currentWorkspace: workspace === currentWorkspace,
|
|
245
|
+
});
|
|
213
246
|
return commandResult(message, splitWorkspaceCommandMessage(message));
|
|
214
247
|
} catch (error) {
|
|
215
248
|
if (error?.code === 'workspace-bot-not-found') {
|
|
@@ -247,7 +280,38 @@ function sessionBindErrorMessage(error) {
|
|
|
247
280
|
|
|
248
281
|
async function runSessionBindCommand(command, harness, conversationKey) {
|
|
249
282
|
const match = SESSION_BIND_COMMAND.exec(command);
|
|
250
|
-
|
|
283
|
+
let sessionId = match?.[1];
|
|
284
|
+
if (typeof sessionId === 'string' && /^\d+$/u.test(sessionId)) {
|
|
285
|
+
// 序号模式:把 /session N 解析成当前工作区会话列表中的第 N 个会话
|
|
286
|
+
if (typeof harness?.listWorkspaceSessions !== 'function'
|
|
287
|
+
|| typeof harness?.currentWorkspace !== 'function') {
|
|
288
|
+
return commandResult('当前机器人暂不支持按序号绑定,请使用 /session Session ID。');
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
const selected = await selectedWorkspacePath(harness.currentWorkspace());
|
|
292
|
+
if (selected.error) return commandResult(selected.error);
|
|
293
|
+
const listed = await harness.listWorkspaceSessions(selected.workspace);
|
|
294
|
+
if (!listed || !Array.isArray(listed.sessions)) {
|
|
295
|
+
throw new TypeError('Harness returned an invalid workspace session list');
|
|
296
|
+
}
|
|
297
|
+
harness.assertWorkspaceScope?.();
|
|
298
|
+
const position = Number(sessionId);
|
|
299
|
+
if (!Number.isSafeInteger(position) || position < 1
|
|
300
|
+
|| position > listed.sessions.length) {
|
|
301
|
+
return commandResult('会话序号不存在,请先执行 /sessionlist 查看序号。');
|
|
302
|
+
}
|
|
303
|
+
const selectedSessionId = listed.sessions[position - 1]?.sessionId;
|
|
304
|
+
if (!validSessionId(selectedSessionId)) {
|
|
305
|
+
throw new TypeError('Harness returned an invalid session id');
|
|
306
|
+
}
|
|
307
|
+
sessionId = selectedSessionId;
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (error?.code === 'workspace-bot-not-found') {
|
|
310
|
+
return commandResult(sessionBindErrorMessage(error));
|
|
311
|
+
}
|
|
312
|
+
return commandResult('暂时无法获取会话列表,请稍后重试。');
|
|
313
|
+
}
|
|
314
|
+
}
|
|
251
315
|
if (!validSessionId(sessionId)) return commandResult(SESSION_BIND_USAGE);
|
|
252
316
|
if (typeof harness?.bindWorkspaceSession !== 'function') {
|
|
253
317
|
return commandResult('当前机器人暂不支持绑定已有会话。');
|
|
@@ -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 { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
8
9
|
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
9
10
|
|
|
@@ -15,7 +16,7 @@ const HELP_TEXT = [
|
|
|
15
16
|
'/workspace 工作区绝对路径 切换工作区',
|
|
16
17
|
'/workspacelist 列出工作区绝对路径',
|
|
17
18
|
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
|
18
|
-
'/session Session ID 将当前聊天绑定到指定会话',
|
|
19
|
+
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
|
19
20
|
'/status 检查连接状态',
|
|
20
21
|
'/help 显示本帮助',
|
|
21
22
|
].join('\n');
|
|
@@ -114,6 +115,8 @@ export class WecomHarnessBridge {
|
|
|
114
115
|
#pendingInteractions = new Map();
|
|
115
116
|
#interactionKeys = new Map();
|
|
116
117
|
#acceptedMessageIds = new Set();
|
|
118
|
+
#approvalTasks = new Set();
|
|
119
|
+
#approvals;
|
|
117
120
|
|
|
118
121
|
constructor({
|
|
119
122
|
client,
|
|
@@ -137,6 +140,7 @@ export class WecomHarnessBridge {
|
|
|
137
140
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
138
141
|
this.#generateReqId = generateStreamId;
|
|
139
142
|
this.#signal = signal;
|
|
143
|
+
this.#approvals = new HarnessApprovalQueue({ label: 'wecom', logger });
|
|
140
144
|
}
|
|
141
145
|
|
|
142
146
|
get status() {
|
|
@@ -159,6 +163,35 @@ export class WecomHarnessBridge {
|
|
|
159
163
|
const key = conversationKey(frame);
|
|
160
164
|
this.#acceptedMessageIds.add(messageId);
|
|
161
165
|
const pending = this.#pendingInteractions.get(key);
|
|
166
|
+
const approval = this.#approvals.claimReply({
|
|
167
|
+
key,
|
|
168
|
+
actor: senderId,
|
|
169
|
+
messageId,
|
|
170
|
+
text: messageText(frame),
|
|
171
|
+
addressed: true,
|
|
172
|
+
hasPendingQuestion: Boolean(pending),
|
|
173
|
+
questionCompletion: pending?.submitting || pending?.claimedReplyMessageId
|
|
174
|
+
? pending.queue
|
|
175
|
+
: null,
|
|
176
|
+
isQuestionPending: () => this.#pendingInteractions.has(key),
|
|
177
|
+
send: (text) => this.#sendImmediate(frame, chatId, text),
|
|
178
|
+
});
|
|
179
|
+
if (approval) {
|
|
180
|
+
let task;
|
|
181
|
+
task = approval.process(async () => {
|
|
182
|
+
if (this.#state.hasSeen(messageId)) return false;
|
|
183
|
+
await this.#state.markSeen(messageId);
|
|
184
|
+
this.#status.messagesReceived += 1;
|
|
185
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
186
|
+
return true;
|
|
187
|
+
})
|
|
188
|
+
.finally(() => {
|
|
189
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
190
|
+
this.#approvalTasks.delete(task);
|
|
191
|
+
});
|
|
192
|
+
this.#approvalTasks.add(task);
|
|
193
|
+
return task;
|
|
194
|
+
}
|
|
162
195
|
if (pending && pending.actor !== senderId) {
|
|
163
196
|
return this.#enqueueMessage(frame, messageId, key);
|
|
164
197
|
}
|
|
@@ -215,6 +248,7 @@ export class WecomHarnessBridge {
|
|
|
215
248
|
...[...this.#pendingInteractions.values()].flatMap((pending) => (
|
|
216
249
|
pending.queue ? [pending.queue] : []
|
|
217
250
|
)),
|
|
251
|
+
...this.#approvalTasks,
|
|
218
252
|
]);
|
|
219
253
|
}
|
|
220
254
|
|
|
@@ -355,7 +389,10 @@ export class WecomHarnessBridge {
|
|
|
355
389
|
this.#logger.error?.('[dsh-im:wecom] failed to send the safe error reply');
|
|
356
390
|
}
|
|
357
391
|
} finally {
|
|
358
|
-
await
|
|
392
|
+
await Promise.allSettled([
|
|
393
|
+
this.#cancelPendingInteraction(key),
|
|
394
|
+
this.#approvals.closeRoute(key),
|
|
395
|
+
]);
|
|
359
396
|
}
|
|
360
397
|
}
|
|
361
398
|
|
|
@@ -481,7 +518,14 @@ export class WecomHarnessBridge {
|
|
|
481
518
|
chatId,
|
|
482
519
|
requiresMention,
|
|
483
520
|
}) {
|
|
484
|
-
|
|
521
|
+
if (interaction?.kind === 'approval') {
|
|
522
|
+
return this.#approvals.handleRequested(interaction, {
|
|
523
|
+
key,
|
|
524
|
+
actor,
|
|
525
|
+
requiresMention,
|
|
526
|
+
send: (text) => this.#sendActive(chatId, text),
|
|
527
|
+
});
|
|
528
|
+
}
|
|
485
529
|
if (interaction?.kind !== 'question') return;
|
|
486
530
|
const questions = interaction?.payload?.questions;
|
|
487
531
|
const interactionId = typeof interaction?.interactionId === 'string'
|
|
@@ -555,7 +599,11 @@ export class WecomHarnessBridge {
|
|
|
555
599
|
await this.#presentInteraction(pending);
|
|
556
600
|
}
|
|
557
601
|
|
|
558
|
-
#handleInteractionResolved(resolution) {
|
|
602
|
+
async #handleInteractionResolved(resolution) {
|
|
603
|
+
if (resolution?.kind === 'approval') {
|
|
604
|
+
await this.#approvals.handleResolved(resolution);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
559
607
|
const interactionId = resolution?.interactionId;
|
|
560
608
|
if (resolution?.kind !== 'question' || typeof interactionId !== 'string') return;
|
|
561
609
|
const key = this.#interactionKeys.get(interactionId);
|
|
@@ -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
|
|
|
@@ -21,7 +22,7 @@ const HELP_TEXT = [
|
|
|
21
22
|
'/workspace 工作区绝对路径 切换工作区',
|
|
22
23
|
'/workspacelist 列出工作区绝对路径',
|
|
23
24
|
'/sessionlist [工作区序号或绝对路径] 列出会话 ID 和标题',
|
|
24
|
-
'/session Session ID 将当前聊天绑定到指定会话',
|
|
25
|
+
'/session Session ID 或当前工作区序号 将当前聊天绑定到指定会话',
|
|
25
26
|
'/status 检查连接状态',
|
|
26
27
|
'/help 显示本帮助',
|
|
27
28
|
].join('\n');
|
|
@@ -68,6 +69,8 @@ export class WeixinHarnessBridge {
|
|
|
68
69
|
#pendingInteractions = new Map();
|
|
69
70
|
#interactionKeys = new Map();
|
|
70
71
|
#acceptedMessageIds = new Set();
|
|
72
|
+
#approvalTasks = new Set();
|
|
73
|
+
#approvals;
|
|
71
74
|
|
|
72
75
|
constructor({
|
|
73
76
|
api,
|
|
@@ -96,6 +99,7 @@ export class WeixinHarnessBridge {
|
|
|
96
99
|
this.#replyTimeoutMs = replyTimeoutMs;
|
|
97
100
|
this.#maxMessageChars = maxMessageChars;
|
|
98
101
|
this.#signal = signal;
|
|
102
|
+
this.#approvals = new HarnessApprovalQueue({ label: 'weixin', logger });
|
|
99
103
|
}
|
|
100
104
|
|
|
101
105
|
get status() {
|
|
@@ -111,7 +115,38 @@ export class WeixinHarnessBridge {
|
|
|
111
115
|
|| this.#acceptedMessageIds.has(messageId)) return Promise.resolve();
|
|
112
116
|
this.#acceptedMessageIds.add(messageId);
|
|
113
117
|
const key = conversationKey(sender);
|
|
118
|
+
const contextToken = nonEmptyString(message?.context_token) ?? undefined;
|
|
119
|
+
const runId = nonEmptyString(message?.run_id) ?? undefined;
|
|
114
120
|
const pending = this.#pendingInteractions.get(key);
|
|
121
|
+
const approval = this.#approvals.claimReply({
|
|
122
|
+
key,
|
|
123
|
+
actor: sender,
|
|
124
|
+
messageId,
|
|
125
|
+
text: extractWeixinText(message),
|
|
126
|
+
addressed: true,
|
|
127
|
+
hasPendingQuestion: Boolean(pending),
|
|
128
|
+
questionCompletion: pending?.submitting || pending?.claimedReplyMessageId
|
|
129
|
+
? pending.queue
|
|
130
|
+
: null,
|
|
131
|
+
isQuestionPending: () => this.#pendingInteractions.has(key),
|
|
132
|
+
send: (text) => this.#send(sender, text, contextToken, runId),
|
|
133
|
+
});
|
|
134
|
+
if (approval) {
|
|
135
|
+
let task;
|
|
136
|
+
task = approval.process(async () => {
|
|
137
|
+
if (this.#state.hasSeen(messageId)) return false;
|
|
138
|
+
await this.#state.markSeen(messageId);
|
|
139
|
+
this.#status.messagesReceived += 1;
|
|
140
|
+
this.#status.lastMessageAt = new Date().toISOString();
|
|
141
|
+
return true;
|
|
142
|
+
})
|
|
143
|
+
.finally(() => {
|
|
144
|
+
this.#acceptedMessageIds.delete(messageId);
|
|
145
|
+
this.#approvalTasks.delete(task);
|
|
146
|
+
});
|
|
147
|
+
this.#approvalTasks.add(task);
|
|
148
|
+
return task;
|
|
149
|
+
}
|
|
115
150
|
if (pending?.submitting || pending?.claimedReplyMessageId) {
|
|
116
151
|
return this.#enqueueMessage(message, messageId, key);
|
|
117
152
|
}
|
|
@@ -157,6 +192,7 @@ export class WeixinHarnessBridge {
|
|
|
157
192
|
...[...this.#pendingInteractions.values()].flatMap((pending) => (
|
|
158
193
|
pending.queue ? [pending.queue] : []
|
|
159
194
|
)),
|
|
195
|
+
...this.#approvalTasks,
|
|
160
196
|
]);
|
|
161
197
|
}
|
|
162
198
|
|
|
@@ -235,7 +271,10 @@ export class WeixinHarnessBridge {
|
|
|
235
271
|
},
|
|
236
272
|
}));
|
|
237
273
|
} finally {
|
|
238
|
-
await
|
|
274
|
+
await Promise.allSettled([
|
|
275
|
+
this.#cancelPendingInteraction(key),
|
|
276
|
+
this.#approvals.closeRoute(key),
|
|
277
|
+
]);
|
|
239
278
|
}
|
|
240
279
|
await this.#send(sender, answer, contextToken, runId);
|
|
241
280
|
await this.#state.markSeen(messageId);
|
|
@@ -391,7 +430,13 @@ export class WeixinHarnessBridge {
|
|
|
391
430
|
contextToken,
|
|
392
431
|
runId,
|
|
393
432
|
}) {
|
|
394
|
-
|
|
433
|
+
if (interaction?.kind === 'approval') {
|
|
434
|
+
return this.#approvals.handleRequested(interaction, {
|
|
435
|
+
key,
|
|
436
|
+
actor,
|
|
437
|
+
send: (text) => this.#send(actor, text, contextToken, runId),
|
|
438
|
+
});
|
|
439
|
+
}
|
|
395
440
|
if (interaction?.kind !== 'question') return;
|
|
396
441
|
const questions = interaction?.payload?.questions;
|
|
397
442
|
const interactionId = typeof interaction?.interactionId === 'string'
|
|
@@ -466,7 +511,11 @@ export class WeixinHarnessBridge {
|
|
|
466
511
|
await this.#presentInteraction(pending);
|
|
467
512
|
}
|
|
468
513
|
|
|
469
|
-
#handleInteractionResolved(resolution) {
|
|
514
|
+
async #handleInteractionResolved(resolution) {
|
|
515
|
+
if (resolution?.kind === 'approval') {
|
|
516
|
+
await this.#approvals.handleResolved(resolution);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
470
519
|
const interactionId = resolution?.interactionId;
|
|
471
520
|
if (resolution?.kind !== 'question' || typeof interactionId !== 'string') return;
|
|
472
521
|
const key = this.#interactionKeys.get(interactionId);
|