fix: support Harness approvals across IM channels

This commit is contained in:
xmanrui 2026-08-18 16:45:25 +08:00
parent ce8b9e80e7
commit 66bdad6299
17 changed files with 2970 additions and 164 deletions

View file

@ -4,6 +4,7 @@ import {
harnessQuestionText,
validHarnessQuestion,
} from '../shared/harness-question.mjs';
import { HarnessApprovalQueue } from '../shared/harness-approval.mjs';
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
@ -114,6 +115,8 @@ export class WecomHarnessBridge {
#pendingInteractions = new Map();
#interactionKeys = new Map();
#acceptedMessageIds = new Set();
#approvalTasks = new Set();
#approvals;
constructor({
client,
@ -137,6 +140,7 @@ export class WecomHarnessBridge {
this.#replyTimeoutMs = replyTimeoutMs;
this.#generateReqId = generateStreamId;
this.#signal = signal;
this.#approvals = new HarnessApprovalQueue({ label: 'wecom', logger });
}
get status() {
@ -159,6 +163,35 @@ export class WecomHarnessBridge {
const key = conversationKey(frame);
this.#acceptedMessageIds.add(messageId);
const pending = this.#pendingInteractions.get(key);
const approval = this.#approvals.claimReply({
key,
actor: senderId,
messageId,
text: messageText(frame),
addressed: true,
hasPendingQuestion: Boolean(pending),
questionCompletion: pending?.submitting || pending?.claimedReplyMessageId
? pending.queue
: null,
isQuestionPending: () => this.#pendingInteractions.has(key),
send: (text) => this.#sendImmediate(frame, chatId, text),
});
if (approval) {
let task;
task = approval.process(async () => {
if (this.#state.hasSeen(messageId)) return false;
await this.#state.markSeen(messageId);
this.#status.messagesReceived += 1;
this.#status.lastMessageAt = new Date().toISOString();
return true;
})
.finally(() => {
this.#acceptedMessageIds.delete(messageId);
this.#approvalTasks.delete(task);
});
this.#approvalTasks.add(task);
return task;
}
if (pending && pending.actor !== senderId) {
return this.#enqueueMessage(frame, messageId, key);
}
@ -215,6 +248,7 @@ export class WecomHarnessBridge {
...[...this.#pendingInteractions.values()].flatMap((pending) => (
pending.queue ? [pending.queue] : []
)),
...this.#approvalTasks,
]);
}
@ -355,7 +389,10 @@ export class WecomHarnessBridge {
this.#logger.error?.('[dsh-im:wecom] failed to send the safe error reply');
}
} finally {
await this.#cancelPendingInteraction(key);
await Promise.allSettled([
this.#cancelPendingInteraction(key),
this.#approvals.closeRoute(key),
]);
}
}
@ -481,7 +518,14 @@ export class WecomHarnessBridge {
chatId,
requiresMention,
}) {
// Approval remains unanswered until #5 supplies an authenticated policy.
if (interaction?.kind === 'approval') {
return this.#approvals.handleRequested(interaction, {
key,
actor,
requiresMention,
send: (text) => this.#sendActive(chatId, text),
});
}
if (interaction?.kind !== 'question') return;
const questions = interaction?.payload?.questions;
const interactionId = typeof interaction?.interactionId === 'string'
@ -555,7 +599,11 @@ export class WecomHarnessBridge {
await this.#presentInteraction(pending);
}
#handleInteractionResolved(resolution) {
async #handleInteractionResolved(resolution) {
if (resolution?.kind === 'approval') {
await this.#approvals.handleResolved(resolution);
return;
}
const interactionId = resolution?.interactionId;
if (resolution?.kind !== 'question' || typeof interactionId !== 'string') return;
const key = this.#interactionKeys.get(interactionId);