@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
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
const APPROVAL_REPLIES = new Map([
|
|
2
|
+
['批准', 'allowed-once'],
|
|
3
|
+
['同意', 'allowed-once'],
|
|
4
|
+
['yes', 'allowed-once'],
|
|
5
|
+
['拒绝', 'rejected'],
|
|
6
|
+
['不同意', 'rejected'],
|
|
7
|
+
['no', 'rejected'],
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
const APPROVAL_PROMPT = '请精准回复「批准」或「拒绝」(也支持:同意 / 不同意 / yes / no)。';
|
|
11
|
+
const APPROVAL_AFTER_QUESTION_PROMPT = '请先完成当前问题,再精准回复「批准」或「拒绝」。';
|
|
12
|
+
const APPROVAL_RESOLVED_TEXT = '该审批已处理,无需再次回复。';
|
|
13
|
+
const RESOLVED_ROUTE_TTL_MS = 5 * 60_000;
|
|
14
|
+
const MAX_RESOLVED_ROUTES = 2_048;
|
|
15
|
+
|
|
16
|
+
function cleanText(value) {
|
|
17
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function printableText(value) {
|
|
21
|
+
return cleanText(value).replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, '');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function harnessApprovalDecision(text) {
|
|
25
|
+
return APPROVAL_REPLIES.get(cleanText(text).toLowerCase()) ?? null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function validHarnessApproval(payload) {
|
|
29
|
+
return payload?.type === 'approval/requested'
|
|
30
|
+
&& Boolean(cleanText(payload.sessionId))
|
|
31
|
+
&& Boolean(cleanText(payload.approvalId))
|
|
32
|
+
&& Boolean(cleanText(payload.toolName))
|
|
33
|
+
&& (payload.callId === undefined || Boolean(cleanText(payload.callId)))
|
|
34
|
+
&& (payload.reason === undefined || typeof payload.reason === 'string');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function toolArguments(toolCall) {
|
|
38
|
+
const source = toolCall?.arguments;
|
|
39
|
+
if (source !== null && typeof source === 'object') {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.stringify(source, null, 2);
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (typeof source !== 'string') return null;
|
|
47
|
+
const raw = printableText(source);
|
|
48
|
+
// Harness treats an empty tool argument string as an empty object.
|
|
49
|
+
if (!raw) return source === '' ? '{}' : null;
|
|
50
|
+
try {
|
|
51
|
+
return JSON.stringify(JSON.parse(raw), null, 2);
|
|
52
|
+
} catch {
|
|
53
|
+
return raw;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function harnessApprovalText(payload, {
|
|
58
|
+
toolCall,
|
|
59
|
+
requiresMention = false,
|
|
60
|
+
maxArgumentsLength = 6_000,
|
|
61
|
+
} = {}) {
|
|
62
|
+
if (!validHarnessApproval(payload)) return null;
|
|
63
|
+
const callId = cleanText(payload.callId);
|
|
64
|
+
if (!callId
|
|
65
|
+
|| cleanText(toolCall?.callId) !== callId
|
|
66
|
+
|| cleanText(toolCall?.name) !== cleanText(payload.toolName)) return null;
|
|
67
|
+
const operation = toolArguments(toolCall);
|
|
68
|
+
if (!operation || operation.length > maxArgumentsLength) return null;
|
|
69
|
+
|
|
70
|
+
const lines = [
|
|
71
|
+
'DeepSeek Harness 需要你的审批:',
|
|
72
|
+
'',
|
|
73
|
+
`工具:${printableText(payload.toolName)}`,
|
|
74
|
+
'操作参数:',
|
|
75
|
+
operation,
|
|
76
|
+
];
|
|
77
|
+
const reason = printableText(payload.reason);
|
|
78
|
+
if (reason) lines.push(`原因:${reason}`);
|
|
79
|
+
lines.push('', APPROVAL_PROMPT);
|
|
80
|
+
if (requiresMention) lines.push('', '群聊中请 @机器人 后发送审批决定。');
|
|
81
|
+
return lines.join('\n');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function approvalResult(pending, outcome) {
|
|
85
|
+
return {
|
|
86
|
+
ok: true,
|
|
87
|
+
value: {
|
|
88
|
+
sessionId: pending.sessionId,
|
|
89
|
+
approvalId: pending.approvalId,
|
|
90
|
+
outcome,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function approvalOutcomeText(outcome) {
|
|
96
|
+
if (outcome === 'allowed-once') return '已批准,仅对本次操作有效。';
|
|
97
|
+
if (outcome === 'rejected') return '已拒绝此次操作。';
|
|
98
|
+
return APPROVAL_RESOLVED_TEXT;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export class HarnessApprovalQueue {
|
|
102
|
+
#label;
|
|
103
|
+
#logger;
|
|
104
|
+
#byId = new Map();
|
|
105
|
+
#routes = new Map();
|
|
106
|
+
#resolvedRoutes = new Map();
|
|
107
|
+
|
|
108
|
+
constructor({ label = 'IM', logger = console } = {}) {
|
|
109
|
+
this.#label = label;
|
|
110
|
+
this.#logger = logger;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
claimReply({
|
|
114
|
+
key,
|
|
115
|
+
actor,
|
|
116
|
+
text,
|
|
117
|
+
addressed = true,
|
|
118
|
+
hasPendingQuestion = false,
|
|
119
|
+
questionCompletion,
|
|
120
|
+
isQuestionPending,
|
|
121
|
+
send,
|
|
122
|
+
}) {
|
|
123
|
+
const route = this.#routes.get(key);
|
|
124
|
+
const pending = route?.items[0];
|
|
125
|
+
const decision = harnessApprovalDecision(text);
|
|
126
|
+
const notice = (value, resolved = false) => ({
|
|
127
|
+
...(resolved ? { resolved: true } : {}),
|
|
128
|
+
process: async (before) => {
|
|
129
|
+
if (typeof before === 'function' && await before() === false) return;
|
|
130
|
+
await send(value);
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
// Match Harness' own interaction precedence: a live ask_user_question
|
|
134
|
+
// outranks sibling approvals. Otherwise a question answer such as "yes"
|
|
135
|
+
// could accidentally authorize a tool call.
|
|
136
|
+
const deferredByQuestion = hasPendingQuestion
|
|
137
|
+
&& pending
|
|
138
|
+
&& questionCompletion
|
|
139
|
+
&& typeof questionCompletion.then === 'function';
|
|
140
|
+
if (hasPendingQuestion && !deferredByQuestion) return null;
|
|
141
|
+
if (!pending || pending.inactive) {
|
|
142
|
+
const resolvedUntil = this.#resolvedRoutes.get(key) ?? 0;
|
|
143
|
+
if (resolvedUntil <= Date.now()) {
|
|
144
|
+
this.#resolvedRoutes.delete(key);
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
if (!decision) return null;
|
|
148
|
+
return notice(APPROVAL_RESOLVED_TEXT, true);
|
|
149
|
+
}
|
|
150
|
+
if (pending.actor !== actor || (pending.requiresMention && addressed !== true)) {
|
|
151
|
+
if (!decision) return null;
|
|
152
|
+
return notice('只有发起当前任务的用户可以处理这条审批。');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
process: async (before) => {
|
|
157
|
+
const presentedWhenClaimed = pending.presented;
|
|
158
|
+
const previous = pending.replyTail ?? Promise.resolve();
|
|
159
|
+
const task = previous
|
|
160
|
+
.catch(() => undefined)
|
|
161
|
+
.then(async () => {
|
|
162
|
+
if (typeof before === 'function' && await before() === false) return;
|
|
163
|
+
if (deferredByQuestion) {
|
|
164
|
+
await questionCompletion.catch(() => undefined);
|
|
165
|
+
if (pending.inactive || pending.resolving) return;
|
|
166
|
+
if (typeof isQuestionPending === 'function' && isQuestionPending()) {
|
|
167
|
+
await send(APPROVAL_AFTER_QUESTION_PROMPT);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
await pending.activationTask?.catch(() => undefined);
|
|
172
|
+
await pending.presentationTask?.catch(() => undefined);
|
|
173
|
+
if (pending.inactive || pending.resolving) {
|
|
174
|
+
await send(APPROVAL_RESOLVED_TEXT);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
pending.send = send;
|
|
178
|
+
// Never turn a decision sent before the operation was visibly
|
|
179
|
+
// presented into an approval. This also covers a failed presentation
|
|
180
|
+
// and the small FIFO promotion window before the next item is shown.
|
|
181
|
+
if (!presentedWhenClaimed || !pending.presented) {
|
|
182
|
+
if (!pending.presented) await this.#present(pending);
|
|
183
|
+
if (pending.inactive || pending.resolving) return;
|
|
184
|
+
await send(APPROVAL_PROMPT);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (pending.submitting) {
|
|
188
|
+
await send('审批决定正在提交,请稍候。');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (!decision) {
|
|
192
|
+
await send(APPROVAL_PROMPT);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
await this.#submit(pending, decision);
|
|
196
|
+
});
|
|
197
|
+
pending.replyTail = task;
|
|
198
|
+
try {
|
|
199
|
+
await task;
|
|
200
|
+
} finally {
|
|
201
|
+
if (pending.replyTail === task) pending.replyTail = null;
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async handleRequested(interaction, context) {
|
|
208
|
+
if (interaction?.kind !== 'approval') return false;
|
|
209
|
+
const payload = interaction.payload;
|
|
210
|
+
const approvalId = cleanText(payload?.approvalId);
|
|
211
|
+
if (!cleanText(interaction.rpcId)
|
|
212
|
+
|| !cleanText(interaction.sessionId)
|
|
213
|
+
|| !approvalId
|
|
214
|
+
|| !validHarnessApproval(payload)
|
|
215
|
+
|| payload.sessionId !== interaction.sessionId
|
|
216
|
+
|| typeof interaction.respond !== 'function') {
|
|
217
|
+
this.#logger.warn?.(`[dsh-im:${this.#label}] ignored an invalid Harness approval`);
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (interaction.recovered === true) {
|
|
222
|
+
await this.#rejectInteraction(interaction, payload);
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const existing = this.#byId.get(approvalId);
|
|
227
|
+
if (existing) {
|
|
228
|
+
existing.interaction = interaction;
|
|
229
|
+
existing.toolCall = interaction.toolCall;
|
|
230
|
+
if (!existing.presented) await this.#present(existing);
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const send = context?.send;
|
|
235
|
+
const key = cleanText(context?.key);
|
|
236
|
+
const actor = cleanText(context?.actor);
|
|
237
|
+
if (!key || !actor || typeof send !== 'function') {
|
|
238
|
+
this.#logger.warn?.(`[dsh-im:${this.#label}] ignored an approval without a reply route`);
|
|
239
|
+
await this.#rejectInteraction(interaction, payload);
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const text = harnessApprovalText(payload, {
|
|
244
|
+
toolCall: interaction.toolCall,
|
|
245
|
+
requiresMention: context.requiresMention === true,
|
|
246
|
+
});
|
|
247
|
+
if (!text) {
|
|
248
|
+
const rejected = await this.#rejectInteraction(interaction, payload);
|
|
249
|
+
await send(rejected
|
|
250
|
+
? '无法完整展示这次操作,已安全拒绝此次审批。'
|
|
251
|
+
: APPROVAL_RESOLVED_TEXT);
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const pending = {
|
|
256
|
+
approvalId,
|
|
257
|
+
sessionId: interaction.sessionId,
|
|
258
|
+
interaction,
|
|
259
|
+
toolCall: interaction.toolCall,
|
|
260
|
+
key,
|
|
261
|
+
actor,
|
|
262
|
+
requiresMention: context.requiresMention === true,
|
|
263
|
+
send,
|
|
264
|
+
text,
|
|
265
|
+
presented: false,
|
|
266
|
+
presentationTask: null,
|
|
267
|
+
deliveryCompleted: false,
|
|
268
|
+
replyTail: null,
|
|
269
|
+
submitting: false,
|
|
270
|
+
inactive: false,
|
|
271
|
+
resolving: false,
|
|
272
|
+
closedOutcome: null,
|
|
273
|
+
resolutionNotified: false,
|
|
274
|
+
activationTask: null,
|
|
275
|
+
};
|
|
276
|
+
this.#byId.set(approvalId, pending);
|
|
277
|
+
const route = this.#routes.get(key) ?? { items: [] };
|
|
278
|
+
route.items.push(pending);
|
|
279
|
+
this.#routes.set(key, route);
|
|
280
|
+
if (route.items[0] === pending) await this.#present(pending);
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async handleResolved(resolution) {
|
|
285
|
+
if (resolution?.kind !== 'approval') return false;
|
|
286
|
+
const pending = this.#byId.get(cleanText(resolution.interactionId));
|
|
287
|
+
if (!pending) return true;
|
|
288
|
+
// A queued item may already be the next route head while the previous
|
|
289
|
+
// item's confirmation is still in flight. Preserve that route barrier so
|
|
290
|
+
// resolving this item cannot expose a later approval out of order.
|
|
291
|
+
pending.resolving = true;
|
|
292
|
+
if (pending.activationTask) {
|
|
293
|
+
await pending.activationTask.catch(() => undefined);
|
|
294
|
+
}
|
|
295
|
+
if (pending.inactive || this.#byId.get(pending.approvalId) !== pending) return true;
|
|
296
|
+
const presentationTask = pending.presentationTask;
|
|
297
|
+
const shouldNotify = pending.presented || presentationTask;
|
|
298
|
+
const send = pending.send;
|
|
299
|
+
const next = this.#remove(pending);
|
|
300
|
+
await this.#transition(next, async () => {
|
|
301
|
+
let delivered = pending.presented;
|
|
302
|
+
if (presentationTask) {
|
|
303
|
+
delivered = await presentationTask.then(() => true, () => false);
|
|
304
|
+
}
|
|
305
|
+
if (shouldNotify && delivered) {
|
|
306
|
+
pending.resolutionNotified = true;
|
|
307
|
+
await send(approvalOutcomeText(resolution.outcome)).catch(() => undefined);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async closeRoute(key) {
|
|
314
|
+
const route = this.#routes.get(key);
|
|
315
|
+
if (!route) return;
|
|
316
|
+
const pendingItems = [...route.items];
|
|
317
|
+
for (const pending of pendingItems) this.#remove(pending);
|
|
318
|
+
await Promise.all(pendingItems.map(async (pending) => {
|
|
319
|
+
try {
|
|
320
|
+
await pending.interaction.respond(
|
|
321
|
+
approvalResult(pending, 'rejected'),
|
|
322
|
+
{ signal: AbortSignal.timeout(5_000) },
|
|
323
|
+
);
|
|
324
|
+
pending.closedOutcome = 'rejected';
|
|
325
|
+
if ((pending.presented || pending.deliveryCompleted) && !pending.resolutionNotified) {
|
|
326
|
+
pending.resolutionNotified = true;
|
|
327
|
+
await pending.send(approvalOutcomeText('rejected')).catch(() => undefined);
|
|
328
|
+
}
|
|
329
|
+
} catch (error) {
|
|
330
|
+
if (error?.code === 'interaction-not-pending') {
|
|
331
|
+
pending.closedOutcome = 'resolved';
|
|
332
|
+
if ((pending.presented || pending.deliveryCompleted) && !pending.resolutionNotified) {
|
|
333
|
+
pending.resolutionNotified = true;
|
|
334
|
+
await pending.send(APPROVAL_RESOLVED_TEXT).catch(() => undefined);
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
this.#logger.warn?.(`[dsh-im:${this.#label}] failed to reject a closing approval:`, error);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async #present(pending) {
|
|
344
|
+
if (this.#routes.get(pending.key)?.items[0] !== pending
|
|
345
|
+
|| pending.inactive || pending.resolving || pending.presented) return;
|
|
346
|
+
await pending.activationTask?.catch(() => undefined);
|
|
347
|
+
if (this.#routes.get(pending.key)?.items[0] !== pending
|
|
348
|
+
|| pending.inactive || pending.resolving || pending.presented) return;
|
|
349
|
+
if (pending.presentationTask) return pending.presentationTask;
|
|
350
|
+
const task = Promise.resolve().then(() => pending.send(pending.text));
|
|
351
|
+
pending.presentationTask = task;
|
|
352
|
+
try {
|
|
353
|
+
await task;
|
|
354
|
+
pending.deliveryCompleted = true;
|
|
355
|
+
if (!pending.inactive) {
|
|
356
|
+
pending.presented = true;
|
|
357
|
+
} else if (pending.closedOutcome && !pending.resolutionNotified) {
|
|
358
|
+
pending.resolutionNotified = true;
|
|
359
|
+
await pending.send(approvalOutcomeText(pending.closedOutcome)).catch(() => undefined);
|
|
360
|
+
}
|
|
361
|
+
} finally {
|
|
362
|
+
if (pending.presentationTask === task) pending.presentationTask = null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async #submit(pending, outcome) {
|
|
367
|
+
pending.submitting = true;
|
|
368
|
+
try {
|
|
369
|
+
await pending.interaction.respond(approvalResult(pending, outcome));
|
|
370
|
+
} catch (error) {
|
|
371
|
+
if (error?.code === 'interaction-not-pending') {
|
|
372
|
+
const send = pending.send;
|
|
373
|
+
const next = this.#remove(pending);
|
|
374
|
+
await this.#transition(next, async () => {
|
|
375
|
+
if (!pending.resolutionNotified) {
|
|
376
|
+
await send(APPROVAL_RESOLVED_TEXT).catch(() => undefined);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (pending.inactive) return;
|
|
382
|
+
pending.submitting = false;
|
|
383
|
+
this.#logger.error?.(`[dsh-im:${this.#label}] failed to submit an approval:`, error);
|
|
384
|
+
await pending.send('审批提交失败,请重新回复「批准」或「拒绝」。').catch(() => undefined);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const send = pending.send;
|
|
389
|
+
const next = this.#remove(pending);
|
|
390
|
+
await this.#transition(next, async () => {
|
|
391
|
+
if (!pending.resolutionNotified) {
|
|
392
|
+
await send(approvalOutcomeText(outcome)).catch(() => undefined);
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
async #transition(next, work) {
|
|
398
|
+
let release;
|
|
399
|
+
const barrier = new Promise((resolve) => { release = resolve; });
|
|
400
|
+
if (next) next.activationTask = barrier;
|
|
401
|
+
try {
|
|
402
|
+
await work();
|
|
403
|
+
} finally {
|
|
404
|
+
release();
|
|
405
|
+
if (next?.activationTask === barrier) next.activationTask = null;
|
|
406
|
+
}
|
|
407
|
+
await this.#promote(next);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async #promote(pending) {
|
|
411
|
+
if (!pending) return;
|
|
412
|
+
try {
|
|
413
|
+
await this.#present(pending);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
this.#logger.error?.(
|
|
416
|
+
`[dsh-im:${this.#label}] failed to present the next approval:`,
|
|
417
|
+
error,
|
|
418
|
+
);
|
|
419
|
+
try {
|
|
420
|
+
pending.interaction.reconnect?.();
|
|
421
|
+
} catch {
|
|
422
|
+
// A replay will retry presentation when the transport can reconnect.
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
#remove(pending) {
|
|
428
|
+
if (pending.inactive) return null;
|
|
429
|
+
pending.inactive = true;
|
|
430
|
+
this.#rememberResolvedRoute(pending.key);
|
|
431
|
+
this.#byId.delete(pending.approvalId);
|
|
432
|
+
const route = this.#routes.get(pending.key);
|
|
433
|
+
if (!route) return null;
|
|
434
|
+
const wasCurrent = route.items[0] === pending;
|
|
435
|
+
const index = route.items.indexOf(pending);
|
|
436
|
+
if (index !== -1) route.items.splice(index, 1);
|
|
437
|
+
if (route.items.length === 0) {
|
|
438
|
+
this.#routes.delete(pending.key);
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
return wasCurrent ? route.items[0] : null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
#rememberResolvedRoute(key) {
|
|
445
|
+
const now = Date.now();
|
|
446
|
+
for (const [routeKey, expiresAt] of this.#resolvedRoutes) {
|
|
447
|
+
if (expiresAt <= now) this.#resolvedRoutes.delete(routeKey);
|
|
448
|
+
}
|
|
449
|
+
this.#resolvedRoutes.delete(key);
|
|
450
|
+
this.#resolvedRoutes.set(key, now + RESOLVED_ROUTE_TTL_MS);
|
|
451
|
+
while (this.#resolvedRoutes.size > MAX_RESOLVED_ROUTES) {
|
|
452
|
+
this.#resolvedRoutes.delete(this.#resolvedRoutes.keys().next().value);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async #rejectInteraction(interaction, payload) {
|
|
457
|
+
try {
|
|
458
|
+
await interaction.respond({
|
|
459
|
+
ok: true,
|
|
460
|
+
value: {
|
|
461
|
+
sessionId: interaction.sessionId,
|
|
462
|
+
approvalId: payload.approvalId,
|
|
463
|
+
outcome: 'rejected',
|
|
464
|
+
},
|
|
465
|
+
}, { signal: AbortSignal.timeout(5_000) });
|
|
466
|
+
return true;
|
|
467
|
+
} catch (error) {
|
|
468
|
+
if (error?.code === 'interaction-not-pending') return false;
|
|
469
|
+
throw error;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|