@xmanrui/dsh-im 4.35.1 → 4.36.1
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/PROACTIVE_DELIVERY.en.md +50 -0
- package/PROACTIVE_DELIVERY.md +22 -0
- package/README.en.md +3 -1
- package/README.md +3 -1
- package/lib/client.js +30 -8
- package/lib/index.js +309 -309
- package/package.json +3 -3
- package/plugin-src/client/connection-error.js +2 -1
- package/plugin-src/host/channels/dingtalk/production.mjs +1 -1
- package/plugin-src/host/channels/feishu/production.mjs +4 -2
- package/plugin-src/host/channels/matrix/production.mjs +1 -1
- package/plugin-src/host/channels/qq/production.mjs +1 -1
- package/plugin-src/host/channels/shared/production.mjs +1 -1
- package/plugin-src/host/channels/slack/production.mjs +1 -1
- package/plugin-src/host/channels/wecom/production.mjs +1 -1
- package/plugin-src/host/channels/wecom-app/production.mjs +1 -1
- package/plugin-src/host/channels/weixin/production.mjs +1 -1
- package/plugin-src/host/channels/whatsapp/production.mjs +1 -1
- package/plugin-src/host/delivery-adapter.mjs +60 -12
- package/plugin-src/host/delivery-service.mjs +127 -1
- package/plugin-src/host/delivery-suggestions.mjs +2 -1
- package/plugin-src/host/harness-connection.mjs +3 -7
- package/plugin-src/host/index.mjs +5 -2
- package/plugin-src/host/modern-harness-api.mjs +139 -4
- package/src/channels/dingtalk/dingtalk-bridge.mjs +6 -0
- package/src/channels/dingtalk/dingtalk-controller.mjs +14 -0
- package/src/channels/dingtalk/dingtalk-runtime.mjs +10 -0
- package/src/channels/discord/discord-runtime.mjs +13 -0
- package/src/channels/feishu/bridge.mjs +30 -13
- package/src/channels/feishu/external-consumer.mjs +118 -0
- package/src/channels/feishu/feishu-runtime.mjs +72 -3
- package/src/channels/feishu/history-reader.mjs +92 -0
- package/src/channels/feishu/multi-bot-controller.mjs +130 -2
- package/src/channels/feishu/nearby-history.mjs +72 -0
- package/src/channels/feishu/plugin-config-store.mjs +1 -0
- package/src/channels/matrix/matrix-controller.mjs +9 -0
- package/src/channels/matrix/matrix-runtime.mjs +14 -0
- package/src/channels/qq/qq-bridge.mjs +6 -0
- package/src/channels/qq/qq-controller.mjs +14 -0
- package/src/channels/qq/qq-runtime.mjs +10 -0
- package/src/channels/shared/connection-error.mjs +1 -0
- package/src/channels/shared/diagnostic-details.mjs +7 -1
- package/src/channels/shared/harness-approval.mjs +84 -7
- package/src/channels/shared/harness-client.mjs +51 -40
- package/src/channels/shared/i18n-en/diagnostics.mjs +3 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +3 -0
- package/src/channels/shared/message-failure.mjs +1 -1
- package/src/channels/shared/text-harness-bridge.mjs +6 -0
- package/src/channels/shared/token-bot-controller.mjs +16 -0
- package/src/channels/slack/manifest.mjs +1 -0
- package/src/channels/slack/slack-api.mjs +7 -0
- package/src/channels/slack/slack-controller.mjs +14 -0
- package/src/channels/slack/slack-runtime.mjs +12 -0
- package/src/channels/telegram/telegram-runtime.mjs +10 -0
- package/src/channels/wecom/wecom-bridge.mjs +6 -0
- package/src/channels/wecom/wecom-controller.mjs +14 -0
- package/src/channels/wecom/wecom-runtime.mjs +10 -0
- package/src/channels/wecom-app/wecom-app-bridge.mjs +6 -0
- package/src/channels/wecom-app/wecom-app-controller.mjs +14 -0
- package/src/channels/wecom-app/wecom-app-runtime.mjs +10 -0
- package/src/channels/weixin/weixin-bridge.mjs +6 -0
- package/src/channels/weixin/weixin-controller.mjs +14 -0
- package/src/channels/weixin/weixin-runtime.mjs +10 -0
- package/src/channels/whatsapp/whatsapp-controller.mjs +14 -0
- package/src/channels/whatsapp/whatsapp-runtime.mjs +10 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { t } from './i18n.mjs';
|
|
2
|
+
import { evaluateInboundAccess } from './inbound-access.mjs';
|
|
2
3
|
|
|
3
4
|
const APPROVAL_REPLIES = new Map([
|
|
4
5
|
['批准', 'allowed-once'],
|
|
@@ -9,6 +10,12 @@ const APPROVAL_REPLIES = new Map([
|
|
|
9
10
|
['no', 'rejected'],
|
|
10
11
|
]);
|
|
11
12
|
|
|
13
|
+
export const COMPETITIVE_APPROVAL_CHANNELS = Object.freeze([
|
|
14
|
+
'feishu', 'weixin', 'dingtalk', 'wecom', 'wecom-app', 'qq',
|
|
15
|
+
'telegram', 'slack', 'discord', 'whatsapp', 'matrix',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const APPROVAL_UNAVAILABLE_TEXT = '机器人端无法处理这次审批,请到 Web 查看。';
|
|
12
19
|
const APPROVAL_PROMPT = '请精准回复「批准」或「拒绝」(也支持:同意 / 不同意 / yes / no)。';
|
|
13
20
|
const APPROVAL_AFTER_QUESTION_PROMPT = '请先完成当前问题,再精准回复「批准」或「拒绝」。';
|
|
14
21
|
const APPROVAL_RESOLVED_TEXT = '该审批已处理,无需再次回复。';
|
|
@@ -95,6 +102,7 @@ function approvalResult(pending, outcome) {
|
|
|
95
102
|
}
|
|
96
103
|
|
|
97
104
|
function approvalOutcomeText(outcome) {
|
|
105
|
+
if (outcome === 'unavailable') return t(APPROVAL_UNAVAILABLE_TEXT);
|
|
98
106
|
if (outcome === 'allowed-once') return t('已批准,仅对本次操作有效。');
|
|
99
107
|
if (outcome === 'rejected') return t('已拒绝此次操作。');
|
|
100
108
|
return t(APPROVAL_RESOLVED_TEXT);
|
|
@@ -210,6 +218,51 @@ export class HarnessApprovalQueue {
|
|
|
210
218
|
};
|
|
211
219
|
}
|
|
212
220
|
|
|
221
|
+
/** Present a bound private-chat approval without creating an IM prompt. */
|
|
222
|
+
async handleSessionSyncRequested(interaction, context, { signal, completion, runtimeSignal, accessPolicy } = {}) {
|
|
223
|
+
if (signal?.aborted || runtimeSignal?.aborted) return false;
|
|
224
|
+
const validateRoute = context.validate;
|
|
225
|
+
context = { ...context, validate: async () => {
|
|
226
|
+
await validateRoute?.();
|
|
227
|
+
if (!context.actor || !evaluateInboundAccess(accessPolicy, {
|
|
228
|
+
conversationType: 'direct', senderIds: [context.actor], text: 'yes',
|
|
229
|
+
}).allowed) throw new Error('Approval recipient is no longer allowed');
|
|
230
|
+
} };
|
|
231
|
+
await context.validate();
|
|
232
|
+
if (signal?.aborted || runtimeSignal?.aborted) return false;
|
|
233
|
+
let resolved = false;
|
|
234
|
+
const finish = (outcome) => this.handleResolved({
|
|
235
|
+
kind: 'approval', interactionId: interaction.interactionId, outcome,
|
|
236
|
+
});
|
|
237
|
+
const onStop = () => {
|
|
238
|
+
queueMicrotask(async () => {
|
|
239
|
+
if (resolved) return;
|
|
240
|
+
try {
|
|
241
|
+
await interaction.withdraw?.();
|
|
242
|
+
await finish('unavailable');
|
|
243
|
+
} catch (error) {
|
|
244
|
+
this.#logger.warn?.(`[dsh-im:${this.#label}] failed to withdraw a stopped approval:`, error);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
runtimeSignal?.addEventListener('abort', onStop, { once: true });
|
|
249
|
+
Promise.resolve(completion).then(async (outcome) => {
|
|
250
|
+
resolved = true;
|
|
251
|
+
runtimeSignal?.removeEventListener('abort', onStop);
|
|
252
|
+
await finish(outcome);
|
|
253
|
+
}).catch((error) => this.#logger.warn?.(`[dsh-im:${this.#label}] failed to retire a synced approval:`, error));
|
|
254
|
+
try {
|
|
255
|
+
await this.handleRequested(interaction, context);
|
|
256
|
+
if (runtimeSignal?.aborted) onStop();
|
|
257
|
+
return this.#byId.has(interaction.interactionId);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
runtimeSignal?.removeEventListener('abort', onStop);
|
|
260
|
+
await interaction.withdraw?.();
|
|
261
|
+
await finish('unavailable');
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
213
266
|
async handleRequested(interaction, context) {
|
|
214
267
|
if (interaction?.kind !== 'approval') return false;
|
|
215
268
|
const payload = interaction.payload;
|
|
@@ -257,7 +310,7 @@ export class HarnessApprovalQueue {
|
|
|
257
310
|
if (!text) {
|
|
258
311
|
const rejected = await this.#rejectInteraction(interaction, payload);
|
|
259
312
|
await send(rejected
|
|
260
|
-
? t('无法完整展示这次操作,已安全拒绝此次审批。')
|
|
313
|
+
? (interaction.withdraw ? t(APPROVAL_UNAVAILABLE_TEXT) : t('无法完整展示这次操作,已安全拒绝此次审批。'))
|
|
261
314
|
: t(APPROVAL_RESOLVED_TEXT));
|
|
262
315
|
return true;
|
|
263
316
|
}
|
|
@@ -272,6 +325,7 @@ export class HarnessApprovalQueue {
|
|
|
272
325
|
requiresMention: context.requiresMention === true,
|
|
273
326
|
send,
|
|
274
327
|
render,
|
|
328
|
+
validate: context.validate,
|
|
275
329
|
onResolved: typeof context.onResolved === 'function' ? context.onResolved : null,
|
|
276
330
|
text,
|
|
277
331
|
presented: false,
|
|
@@ -342,13 +396,14 @@ export class HarnessApprovalQueue {
|
|
|
342
396
|
for (const pending of pendingItems) this.#remove(pending);
|
|
343
397
|
await Promise.all(pendingItems.map(async (pending) => {
|
|
344
398
|
try {
|
|
345
|
-
await pending.interaction.
|
|
399
|
+
if (pending.interaction.withdraw) await pending.interaction.withdraw();
|
|
400
|
+
else await pending.interaction.respond(
|
|
346
401
|
approvalResult(pending, 'rejected'),
|
|
347
402
|
{ signal: AbortSignal.timeout(5_000) },
|
|
348
403
|
);
|
|
349
|
-
pending.closedOutcome = 'rejected';
|
|
404
|
+
pending.closedOutcome = pending.interaction.withdraw ? 'unavailable' : 'rejected';
|
|
350
405
|
if (pending.presented || pending.deliveryCompleted) {
|
|
351
|
-
await this.#notifyResolved(pending,
|
|
406
|
+
await this.#notifyResolved(pending, pending.closedOutcome);
|
|
352
407
|
}
|
|
353
408
|
} catch (error) {
|
|
354
409
|
if (error?.code === 'interaction-not-pending') {
|
|
@@ -372,9 +427,12 @@ export class HarnessApprovalQueue {
|
|
|
372
427
|
if (pending.presentationTask) return pending.presentationTask;
|
|
373
428
|
// A channel-provided renderer shows the approval as an interactive card
|
|
374
429
|
// (e.g. approve/reject buttons); otherwise fall back to plain text.
|
|
375
|
-
const task =
|
|
376
|
-
|
|
377
|
-
|
|
430
|
+
const task = Promise.resolve().then(async () => {
|
|
431
|
+
await pending.validate?.();
|
|
432
|
+
if (pending.inactive || pending.resolving) return;
|
|
433
|
+
if (pending.render) await pending.render(pending, pending.send);
|
|
434
|
+
else await pending.send(pending.text);
|
|
435
|
+
});
|
|
378
436
|
pending.presentationTask = task;
|
|
379
437
|
try {
|
|
380
438
|
await task;
|
|
@@ -384,6 +442,12 @@ export class HarnessApprovalQueue {
|
|
|
384
442
|
} else if (pending.closedOutcome) {
|
|
385
443
|
await this.#notifyResolved(pending, pending.closedOutcome);
|
|
386
444
|
}
|
|
445
|
+
} catch (error) {
|
|
446
|
+
if (!pending.interaction.withdraw) throw error;
|
|
447
|
+
pending.presentationTask = null;
|
|
448
|
+
await pending.interaction.withdraw();
|
|
449
|
+
await this.handleResolved({ kind: 'approval', interactionId: pending.approvalId, outcome: 'unavailable' });
|
|
450
|
+
this.#logger.warn?.(`[dsh-im:${this.#label}] approval presentation unavailable:`, error);
|
|
387
451
|
} finally {
|
|
388
452
|
if (pending.presentationTask === task) pending.presentationTask = null;
|
|
389
453
|
}
|
|
@@ -392,6 +456,15 @@ export class HarnessApprovalQueue {
|
|
|
392
456
|
async #submit(pending, outcome) {
|
|
393
457
|
pending.submitting = true;
|
|
394
458
|
try {
|
|
459
|
+
if (pending.validate) {
|
|
460
|
+
try { await pending.validate(); }
|
|
461
|
+
catch (error) {
|
|
462
|
+
await pending.interaction.withdraw?.();
|
|
463
|
+
throw Object.assign(new Error('Approval route is no longer available', { cause: error }), {
|
|
464
|
+
code: 'interaction-not-pending',
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
395
468
|
await pending.interaction.respond(approvalResult(pending, outcome));
|
|
396
469
|
} catch (error) {
|
|
397
470
|
if (error?.code === 'interaction-not-pending') {
|
|
@@ -491,6 +564,10 @@ export class HarnessApprovalQueue {
|
|
|
491
564
|
|
|
492
565
|
async #rejectInteraction(interaction, payload) {
|
|
493
566
|
try {
|
|
567
|
+
if (interaction.withdraw) {
|
|
568
|
+
await interaction.withdraw();
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
494
571
|
await interaction.respond({
|
|
495
572
|
ok: true,
|
|
496
573
|
value: {
|
|
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { mkdir } from 'node:fs/promises';
|
|
4
4
|
import { isAbsolute } from 'node:path';
|
|
5
|
+
import { performance } from 'node:perf_hooks';
|
|
5
6
|
|
|
6
7
|
import { adoptRegisteredWorkspaceSession } from './harness-session-binding.mjs';
|
|
7
8
|
import { sameWorkspacePath } from './default-workspace.mjs';
|
|
@@ -504,33 +505,9 @@ function consumeInteractionOwnership(ownership, entries) {
|
|
|
504
505
|
ownership.completed = true;
|
|
505
506
|
continue;
|
|
506
507
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
&& event.data?.turn === ownership.turn
|
|
511
|
-
&& typeof event.data?.callId === 'string'
|
|
512
|
-
&& event.data.callId) {
|
|
513
|
-
toolCall = {
|
|
514
|
-
callId: event.data.callId,
|
|
515
|
-
name: event.data?.name,
|
|
516
|
-
arguments: event.data?.arguments,
|
|
517
|
-
};
|
|
518
|
-
} else if (event.type === 'tool/code-dispatch-start'
|
|
519
|
-
&& ownership.active
|
|
520
|
-
&& typeof event.data?.subCallId === 'string'
|
|
521
|
-
&& event.data.subCallId) {
|
|
522
|
-
let argumentsText;
|
|
523
|
-
try {
|
|
524
|
-
argumentsText = JSON.stringify(event.data?.arguments);
|
|
525
|
-
} catch {
|
|
526
|
-
argumentsText = undefined;
|
|
527
|
-
}
|
|
528
|
-
toolCall = {
|
|
529
|
-
callId: event.data.subCallId,
|
|
530
|
-
name: event.data?.name,
|
|
531
|
-
arguments: argumentsText,
|
|
532
|
-
};
|
|
533
|
-
}
|
|
508
|
+
const toolCall = ownership.active
|
|
509
|
+
&& (event.type !== 'tool/call' || event.data?.turn === ownership.turn)
|
|
510
|
+
? harnessToolCallFromEvent(event) : null;
|
|
534
511
|
if (toolCall) ownership.toolCalls.set(toolCall.callId, Object.freeze(toolCall));
|
|
535
512
|
}
|
|
536
513
|
}
|
|
@@ -540,17 +517,36 @@ function consumeInteractionOwnership(ownership, entries) {
|
|
|
540
517
|
* this Session. The modern Harness adapter uses the same ownership registry
|
|
541
518
|
* as HarnessClient so it never steals a browser-owned question or approval.
|
|
542
519
|
*/
|
|
543
|
-
export function
|
|
520
|
+
export function activeHarnessInteractionOwner(scope, sessionId, entries = []) {
|
|
544
521
|
if (!scope || !['object', 'function', 'string'].includes(typeof scope)
|
|
545
|
-
|| typeof sessionId !== 'string' || !sessionId) return
|
|
522
|
+
|| typeof sessionId !== 'string' || !sessionId) return null;
|
|
546
523
|
const owners = interactionRegistry(scope).ownerships.get(sessionId);
|
|
547
|
-
if (!owners || owners.size === 0) return
|
|
524
|
+
if (!owners || owners.size === 0) return null;
|
|
548
525
|
for (const ownership of owners) consumeInteractionOwnership(ownership, entries);
|
|
549
|
-
return [...owners].
|
|
526
|
+
return [...owners].filter((ownership) => (
|
|
550
527
|
ownership.active
|
|
551
528
|
&& !ownership.completed
|
|
552
529
|
&& typeof ownership.reconnect === 'function'
|
|
553
|
-
));
|
|
530
|
+
)).sort((left, right) => left.order - right.order)[0] ?? null;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** Whether a live IM consumer owns this turn. */
|
|
534
|
+
export function hasActiveHarnessInteractionOwner(scope, sessionId, entries = []) {
|
|
535
|
+
return activeHarnessInteractionOwner(scope, sessionId, entries) !== null;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/** Read the operation attached to a normal or PTC tool-call event. */
|
|
539
|
+
export function harnessToolCallFromEvent(event) {
|
|
540
|
+
const data = event?.data;
|
|
541
|
+
if (event?.type === 'tool/call' && typeof data?.callId === 'string') {
|
|
542
|
+
return { callId: data.callId, name: data.name, arguments: data.arguments };
|
|
543
|
+
}
|
|
544
|
+
if (event?.type === 'tool/code-dispatch-start' && typeof data?.subCallId === 'string') {
|
|
545
|
+
let argumentsText;
|
|
546
|
+
try { argumentsText = JSON.stringify(data.arguments); } catch { /* Unserializable details cannot be approved. */ }
|
|
547
|
+
return { callId: data.subCallId, name: data.name, arguments: argumentsText };
|
|
548
|
+
}
|
|
549
|
+
return null;
|
|
554
550
|
}
|
|
555
551
|
|
|
556
552
|
export class HarnessReplyTracker {
|
|
@@ -934,6 +930,7 @@ export class HarnessClient {
|
|
|
934
930
|
#managedProcess = null;
|
|
935
931
|
#interactionRegistry;
|
|
936
932
|
#interactionOwnerships;
|
|
933
|
+
#competitiveApprovals;
|
|
937
934
|
#interactionClaims;
|
|
938
935
|
#controlOwnerships;
|
|
939
936
|
|
|
@@ -941,6 +938,7 @@ export class HarnessClient {
|
|
|
941
938
|
baseUrl,
|
|
942
939
|
apiProxy,
|
|
943
940
|
interactionScope = apiProxy,
|
|
941
|
+
competitiveApprovals = false,
|
|
944
942
|
workspace,
|
|
945
943
|
ungroupedWorkspace,
|
|
946
944
|
agentPreset,
|
|
@@ -991,6 +989,7 @@ export class HarnessClient {
|
|
|
991
989
|
|| !['object', 'function'].includes(typeof interactionScope))) {
|
|
992
990
|
throw new TypeError('interactionScope must identify the current Host');
|
|
993
991
|
}
|
|
992
|
+
this.#competitiveApprovals = competitiveApprovals === true;
|
|
994
993
|
this.#workspace = workspace;
|
|
995
994
|
// Only the reserved IM directory bypasses grouping, even after /ws switches.
|
|
996
995
|
this.#ungroupedWorkspace = ungroupedWorkspace;
|
|
@@ -1015,6 +1014,12 @@ export class HarnessClient {
|
|
|
1015
1014
|
}
|
|
1016
1015
|
|
|
1017
1016
|
async rpc(method, payload = {}, timeoutMs = 30_000, options = {}) {
|
|
1017
|
+
const startedAt = performance.now();
|
|
1018
|
+
const withDiagnostics = error => Object.assign(error, {
|
|
1019
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
1020
|
+
timeoutMs,
|
|
1021
|
+
transport: this.#apiProxy ? 'host-api' : 'http',
|
|
1022
|
+
});
|
|
1018
1023
|
const rpcId = options.rpcId ?? `${this.#rpcIdPrefix}-${randomUUID()}`;
|
|
1019
1024
|
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
1020
1025
|
const signal = options.signal
|
|
@@ -1053,24 +1058,24 @@ export class HarnessClient {
|
|
|
1053
1058
|
// Preserve an explicit caller cancellation; it is control flow, not a
|
|
1054
1059
|
// Harness availability diagnosis.
|
|
1055
1060
|
if (options.signal?.aborted) throw error;
|
|
1056
|
-
if (error instanceof HarnessTransportError) throw error;
|
|
1057
|
-
throw new HarnessTransportError(
|
|
1061
|
+
if (error instanceof HarnessTransportError) throw withDiagnostics(error);
|
|
1062
|
+
throw withDiagnostics(new HarnessTransportError(
|
|
1058
1063
|
timeoutSignal.aborted ? 'harness-timeout' : 'harness-connect-failed',
|
|
1059
1064
|
method,
|
|
1060
1065
|
{ cause: error },
|
|
1061
|
-
);
|
|
1066
|
+
));
|
|
1062
1067
|
}
|
|
1063
1068
|
if (body?.type !== 'server-response' || body?.rpcId !== rpcId) {
|
|
1064
|
-
throw new HarnessTransportError('harness-response-invalid', method, {
|
|
1069
|
+
throw withDiagnostics(new HarnessTransportError('harness-response-invalid', method, {
|
|
1065
1070
|
cause: new Error(`Harness returned an invalid response for ${method}`),
|
|
1066
|
-
});
|
|
1071
|
+
}));
|
|
1067
1072
|
}
|
|
1068
1073
|
if (!body.result || typeof body.result !== 'object' || typeof body.result.ok !== 'boolean') {
|
|
1069
|
-
throw new HarnessTransportError('harness-response-invalid', method, {
|
|
1074
|
+
throw withDiagnostics(new HarnessTransportError('harness-response-invalid', method, {
|
|
1070
1075
|
cause: new Error(`Harness returned an invalid result for ${method}`),
|
|
1071
|
-
});
|
|
1076
|
+
}));
|
|
1072
1077
|
}
|
|
1073
|
-
if (!body.result?.ok) throw new HarnessRpcError(method, body.result?.error);
|
|
1078
|
+
if (!body.result?.ok) throw withDiagnostics(new HarnessRpcError(method, body.result?.error));
|
|
1074
1079
|
return body.result.value;
|
|
1075
1080
|
}
|
|
1076
1081
|
|
|
@@ -1783,6 +1788,7 @@ export class HarnessClient {
|
|
|
1783
1788
|
lastSeq: baselineSeq,
|
|
1784
1789
|
reconnect: null,
|
|
1785
1790
|
order: -1,
|
|
1791
|
+
competitiveApprovals: this.#competitiveApprovals,
|
|
1786
1792
|
toolCalls: new Map(),
|
|
1787
1793
|
control,
|
|
1788
1794
|
}
|
|
@@ -2130,6 +2136,11 @@ export class HarnessClient {
|
|
|
2130
2136
|
? this.#interactionClaims.get(claimKey)?.recovered === true
|
|
2131
2137
|
: false,
|
|
2132
2138
|
...(toolCall ? { toolCall } : {}),
|
|
2139
|
+
...(kind === 'approval' && payload.competitive === true ? {
|
|
2140
|
+
withdraw: () => this.respondInteraction(envelope.rpcId, {
|
|
2141
|
+
ok: false, error: { code: 'unavailable', message: 'IM cannot answer this approval', details: {} },
|
|
2142
|
+
}, { signal: AbortSignal.timeout(5_000) }),
|
|
2143
|
+
} : {}),
|
|
2133
2144
|
reconnect: close,
|
|
2134
2145
|
respond: (result, options = {}) => this.respondInteraction(
|
|
2135
2146
|
envelope.rpcId,
|
|
@@ -47,6 +47,9 @@ export default Object.freeze({
|
|
|
47
47
|
"暂未识别具体原因,请复制诊断信息,并查看相同参考号的 DSH 日志。": "The underlying cause is unknown. Copy the diagnostics and find the matching reference in the DSH logs.",
|
|
48
48
|
"多个原因": "Multiple causes",
|
|
49
49
|
"服务域名": "Service host",
|
|
50
|
+
"RPC 方法": "RPC method",
|
|
51
|
+
"调用方式": "Transport",
|
|
52
|
+
"宿主 API(进程内)": "Host API (in-process)",
|
|
50
53
|
"请求耗时(毫秒)": "Request duration (ms)",
|
|
51
54
|
"超时阈值(毫秒)": "Timeout (ms)",
|
|
52
55
|
"Node 版本": "Node version",
|
|
@@ -70,6 +70,9 @@ export default {
|
|
|
70
70
|
'已批准,仅对本次操作有效。':
|
|
71
71
|
'Approved — valid for this operation only.',
|
|
72
72
|
'已拒绝此次操作。': 'This operation was rejected.',
|
|
73
|
+
'机器人端无法处理这次审批,请到 Web 查看。': 'The bot cannot handle this approval. Please check the Web interface.',
|
|
74
|
+
'未找到可处理端,本次操作未获批准。': 'No approval interface is available. This operation was not approved.',
|
|
75
|
+
'有操作在等待审批:{tool},请到 Web 处理。': 'An operation is awaiting approval: {tool}. Please handle it in the Web interface.',
|
|
73
76
|
'无法完整展示这次操作,已安全拒绝此次审批。':
|
|
74
77
|
'The operation could not be displayed in full, so this approval was safely rejected.',
|
|
75
78
|
'审批提交失败,请重新回复「批准」或「拒绝」。':
|
|
@@ -218,7 +218,7 @@ export function classifyMessageFailure(error, {
|
|
|
218
218
|
: businessCode;
|
|
219
219
|
if (code === 'REQUEST_TIMEOUT' || code === 'NETWORK_ERROR') details.hint = connectionHint(details);
|
|
220
220
|
return Object.freeze({
|
|
221
|
-
...(code === 'INTERNAL_UNKNOWN' || details.reason !== 'unknown' || details.httpStatus || details.providerCode ? { details } : {}),
|
|
221
|
+
...(code === 'INTERNAL_UNKNOWN' || details.reason !== 'unknown' || details.httpStatus || details.providerCode || details.method ? { details } : {}),
|
|
222
222
|
code,
|
|
223
223
|
reason: safeReason ?? safeFailureReason(error?.code) ?? code,
|
|
224
224
|
message: typeof userMessage === 'string' && userMessage.trim()
|
|
@@ -1381,6 +1381,12 @@ export class TextHarnessBridge {
|
|
|
1381
1381
|
});
|
|
1382
1382
|
}
|
|
1383
1383
|
|
|
1384
|
+
async presentSessionSyncApproval(interaction, context, options = {}) {
|
|
1385
|
+
return this.#approvals.handleSessionSyncRequested(interaction, context, {
|
|
1386
|
+
...options, runtimeSignal: this.#signal ?? options.runtimeSignal, accessPolicy: this.#accessPolicy,
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1384
1390
|
async #handleInteraction(interaction, {
|
|
1385
1391
|
key,
|
|
1386
1392
|
actor,
|
|
@@ -247,6 +247,22 @@ export class TokenBotController {
|
|
|
247
247
|
});
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
async presentSessionSyncApproval(botId, target, interaction, options = {}) {
|
|
251
|
+
const config = this.#configStore.get(botId);
|
|
252
|
+
if (!config) throw new Error(`Unknown ${this.#descriptor.label} bot`);
|
|
253
|
+
return this.#withBotTransition(botId, async () => {
|
|
254
|
+
const runtime = this.#runtimes.get(botId);
|
|
255
|
+
if (!runtime?.status?.ready || typeof runtime.presentSessionSyncApproval !== 'function') {
|
|
256
|
+
const error = new Error(t('{label}机器人尚未连接', {
|
|
257
|
+
label: this.#descriptor.label,
|
|
258
|
+
}));
|
|
259
|
+
error.code = 'bot-not-connected';
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
return runtime.presentSessionSyncApproval(target, interaction, options);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
250
266
|
async deleteBot(botId) {
|
|
251
267
|
const warnings = [];
|
|
252
268
|
const config = this.#configStore.get(botId);
|
|
@@ -234,6 +234,13 @@ export class SlackApi {
|
|
|
234
234
|
});
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
async conversationInfo({ channelId, signal } = {}) {
|
|
238
|
+
const value = await this.#request('conversations.info', {
|
|
239
|
+
tokenKind: 'bot', signal, body: { channel: slackId(channelId, 'channel id') },
|
|
240
|
+
});
|
|
241
|
+
return value.channel;
|
|
242
|
+
}
|
|
243
|
+
|
|
237
244
|
async fileInfo({ fileId, signal } = {}) {
|
|
238
245
|
const value = await this.#request('files.info', {
|
|
239
246
|
tokenKind: 'bot',
|
|
@@ -207,6 +207,20 @@ export class SlackController {
|
|
|
207
207
|
});
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
async presentSessionSyncApproval(botId, target, interaction, options = {}) {
|
|
211
|
+
const config = this.#configStore.get(botId);
|
|
212
|
+
if (!config) throw new Error('Unknown Slack bot');
|
|
213
|
+
return this.#withBotTransition(botId, async () => {
|
|
214
|
+
const runtime = this.#runtimes.get(botId);
|
|
215
|
+
if (!runtime?.status?.ready || typeof runtime.presentSessionSyncApproval !== 'function') {
|
|
216
|
+
const error = new Error(t('Slack机器人尚未连接'));
|
|
217
|
+
error.code = 'bot-not-connected';
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
return runtime.presentSessionSyncApproval(target, interaction, options);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
210
224
|
async deleteBot(botId) {
|
|
211
225
|
const warnings = [];
|
|
212
226
|
const config = this.#configStore.get(botId);
|
|
@@ -473,6 +473,18 @@ export class SlackRuntime {
|
|
|
473
473
|
return this.#bridge.sendConnectionTest(text);
|
|
474
474
|
}
|
|
475
475
|
|
|
476
|
+
async presentSessionSyncApproval(target, interaction, options = {}) {
|
|
477
|
+
if (!this.#status.ready || !this.#bridge) return false;
|
|
478
|
+
const channelId = target?.route?.channelId;
|
|
479
|
+
const channel = await this.#api.conversationInfo({ channelId, signal: options.signal });
|
|
480
|
+
const actor = channel?.is_im === true && typeof channel.user === 'string' ? channel.user : '';
|
|
481
|
+
if (!actor) return false;
|
|
482
|
+
return this.#bridge.presentSessionSyncApproval(interaction, {
|
|
483
|
+
key: options.key, actor, validate: options.validate,
|
|
484
|
+
send: (text) => this.sendProactiveText(target, text),
|
|
485
|
+
}, options);
|
|
486
|
+
}
|
|
487
|
+
|
|
476
488
|
async sendProactiveText(target, text, options = {}) {
|
|
477
489
|
if (!this.#status.ready || !this.#bridge) {
|
|
478
490
|
const error = new Error('Slack bot is not connected');
|
|
@@ -1171,6 +1171,16 @@ export class TelegramRuntime {
|
|
|
1171
1171
|
return this.#bridge.sendConnectionTest(text);
|
|
1172
1172
|
}
|
|
1173
1173
|
|
|
1174
|
+
async presentSessionSyncApproval(target, interaction, options = {}) {
|
|
1175
|
+
if (!this.#status.ready || !this.#bridge) return false;
|
|
1176
|
+
const actor = typeof target?.route?.chatId === 'string' ? target.route.chatId.trim() : '';
|
|
1177
|
+
if (!actor) return false;
|
|
1178
|
+
return this.#bridge.presentSessionSyncApproval(interaction, {
|
|
1179
|
+
key: options.key, actor, validate: options.validate,
|
|
1180
|
+
send: (text) => this.sendProactiveText(target, text),
|
|
1181
|
+
}, options);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1174
1184
|
async sendProactiveText(target, text, options = {}) {
|
|
1175
1185
|
if (!this.#status.ready || !this.#bridge) {
|
|
1176
1186
|
const error = new Error('Telegram bot is not connected');
|
|
@@ -1668,6 +1668,12 @@ export class WecomHarnessBridge {
|
|
|
1668
1668
|
}
|
|
1669
1669
|
}
|
|
1670
1670
|
|
|
1671
|
+
async presentSessionSyncApproval(interaction, context, options = {}) {
|
|
1672
|
+
return this.#approvals.handleSessionSyncRequested(interaction, context, {
|
|
1673
|
+
...options, runtimeSignal: this.#signal, accessPolicy: this.#accessPolicy,
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1671
1677
|
async #handleInteraction(interaction, {
|
|
1672
1678
|
key,
|
|
1673
1679
|
actor,
|
|
@@ -281,6 +281,20 @@ export class WecomController {
|
|
|
281
281
|
});
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
async presentSessionSyncApproval(botId, target, interaction, options = {}) {
|
|
285
|
+
const config = this.#configStore.get(botId);
|
|
286
|
+
if (!config) throw new Error('Unknown Enterprise WeChat bot');
|
|
287
|
+
return this.#withBotTransition(botId, async () => {
|
|
288
|
+
const runtime = this.#runtimes.get(botId);
|
|
289
|
+
if (!runtime?.status?.ready || typeof runtime.presentSessionSyncApproval !== 'function') {
|
|
290
|
+
const error = new Error(t('企业微信连接当前离线'));
|
|
291
|
+
error.code = 'bot-not-connected';
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
return runtime.presentSessionSyncApproval(target, interaction, options);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
284
298
|
async deleteBot(botId) {
|
|
285
299
|
const warnings = [];
|
|
286
300
|
const config = this.#configStore.get(botId);
|
|
@@ -230,6 +230,16 @@ export class WecomRuntime {
|
|
|
230
230
|
});
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
async presentSessionSyncApproval(target, interaction, options = {}) {
|
|
234
|
+
if (!this.#status.ready || !this.#bridge) return false;
|
|
235
|
+
const actor = typeof target?.route?.chatId === 'string' ? target.route.chatId.trim() : '';
|
|
236
|
+
if (!actor) return false;
|
|
237
|
+
return this.#bridge.presentSessionSyncApproval(interaction, {
|
|
238
|
+
key: options.key, actor, validate: options.validate,
|
|
239
|
+
send: (text) => this.sendProactiveText(target, text),
|
|
240
|
+
}, options);
|
|
241
|
+
}
|
|
242
|
+
|
|
233
243
|
async sendProactiveText(target, text, { signal } = {}) {
|
|
234
244
|
const chatId = typeof target?.route?.chatId === 'string'
|
|
235
245
|
? target.route.chatId.trim() : '';
|
|
@@ -855,6 +855,12 @@ export class WecomAppBridge {
|
|
|
855
855
|
}
|
|
856
856
|
}
|
|
857
857
|
|
|
858
|
+
async presentSessionSyncApproval(interaction, context, options = {}) {
|
|
859
|
+
return this.#approvals.handleSessionSyncRequested(interaction, context, {
|
|
860
|
+
...options, runtimeSignal: this.#signal, accessPolicy: this.#accessPolicy,
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
|
|
858
864
|
async #handleInteraction(interaction, { key, actor }) {
|
|
859
865
|
if (interaction?.kind === 'approval') {
|
|
860
866
|
return this.#approvals.handleRequested(interaction, {
|
|
@@ -290,6 +290,20 @@ export class WecomAppController {
|
|
|
290
290
|
});
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
async presentSessionSyncApproval(botId, target, interaction, options = {}) {
|
|
294
|
+
const config = this.#configStore.get(botId);
|
|
295
|
+
if (!config) throw new Error('Unknown Enterprise WeChat app bot');
|
|
296
|
+
return this.#withBotTransition(botId, async () => {
|
|
297
|
+
const runtime = this.#runtimes.get(botId);
|
|
298
|
+
if (!runtime?.status?.ready || typeof runtime.presentSessionSyncApproval !== 'function') {
|
|
299
|
+
const error = new Error(t('企业微信应用当前离线'));
|
|
300
|
+
error.code = 'bot-not-connected';
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
return runtime.presentSessionSyncApproval(target, interaction, options);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
293
307
|
async deleteBot(botId) {
|
|
294
308
|
const warnings = [];
|
|
295
309
|
const config = this.#configStore.get(botId);
|
|
@@ -193,6 +193,16 @@ export class WecomAppRuntime {
|
|
|
193
193
|
});
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
async presentSessionSyncApproval(target, interaction, options = {}) {
|
|
197
|
+
if (!this.#started || !this.#bridge) return false;
|
|
198
|
+
const actor = typeof target?.route?.chatId === 'string' ? target.route.chatId.trim() : '';
|
|
199
|
+
if (!actor) return false;
|
|
200
|
+
return this.#bridge.presentSessionSyncApproval(interaction, {
|
|
201
|
+
key: options.key, actor, validate: options.validate,
|
|
202
|
+
send: (text) => this.sendProactiveText(target, text),
|
|
203
|
+
}, options);
|
|
204
|
+
}
|
|
205
|
+
|
|
196
206
|
async sendProactiveText(target, text, { signal } = {}) {
|
|
197
207
|
const chatId = typeof target?.route?.chatId === 'string' ? target.route.chatId.trim() : '';
|
|
198
208
|
if ((target?.kind !== 'user') || !chatId) {
|
|
@@ -1102,6 +1102,12 @@ export class WeixinHarnessBridge {
|
|
|
1102
1102
|
}
|
|
1103
1103
|
}
|
|
1104
1104
|
|
|
1105
|
+
async presentSessionSyncApproval(interaction, context, options = {}) {
|
|
1106
|
+
return this.#approvals.handleSessionSyncRequested(interaction, context, {
|
|
1107
|
+
...options, runtimeSignal: this.#signal, accessPolicy: this.#accessPolicy,
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1105
1111
|
async #handleInteraction(interaction, {
|
|
1106
1112
|
key,
|
|
1107
1113
|
actor,
|
|
@@ -289,6 +289,20 @@ export class WeixinController {
|
|
|
289
289
|
});
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
async presentSessionSyncApproval(botId, target, interaction, options = {}) {
|
|
293
|
+
const config = this.#configStore.get(botId);
|
|
294
|
+
if (!config) throw new Error('Unknown Weixin account');
|
|
295
|
+
return this.#withBotTransition(botId, async () => {
|
|
296
|
+
const runtime = this.#runtimes.get(botId);
|
|
297
|
+
if (!runtime?.status?.ready || typeof runtime.presentSessionSyncApproval !== 'function') {
|
|
298
|
+
const error = new Error(t('微信连接当前离线'));
|
|
299
|
+
error.code = 'bot-not-connected';
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
return runtime.presentSessionSyncApproval(target, interaction, options);
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
292
306
|
async deleteBot(botId) {
|
|
293
307
|
const config = this.#configStore.get(botId);
|
|
294
308
|
if (!config) throw weixinStageError('workspace-bot-not-found', undefined, 'account.remove');
|
|
@@ -392,6 +392,16 @@ export class WeixinRuntime {
|
|
|
392
392
|
return result;
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
+
async presentSessionSyncApproval(target, interaction, options = {}) {
|
|
396
|
+
if (!this.#status.ready || !this.#bridge) return false;
|
|
397
|
+
const actor = typeof target?.route?.toUserId === 'string' ? target.route.toUserId.trim() : '';
|
|
398
|
+
if (!actor) return false;
|
|
399
|
+
return this.#bridge.presentSessionSyncApproval(interaction, {
|
|
400
|
+
key: options.key, actor, validate: options.validate,
|
|
401
|
+
send: (text) => this.sendProactiveText(target, text),
|
|
402
|
+
}, options);
|
|
403
|
+
}
|
|
404
|
+
|
|
395
405
|
async sendProactiveText(target, text, { signal } = {}) {
|
|
396
406
|
const toUserId = typeof target?.route?.toUserId === 'string'
|
|
397
407
|
? target.route.toUserId.trim() : '';
|