fix: surface actionable channel message errors

This commit is contained in:
xmanrui 2026-08-25 14:54:50 +08:00
parent b42fc411c2
commit 7cbed3aefb
59 changed files with 2405 additions and 607 deletions

View file

@ -5,6 +5,7 @@ import { join } from 'node:path';
import test from 'node:test';
import {
createWecomBridgeStatus,
sendWecomImage,
WecomHarnessBridge,
wecomInboundMessage,
@ -765,7 +766,8 @@ test('Enterprise WeChat bounds prefetched image memory while a conversation is q
assert.equal(downloads.length, 4);
assert.equal(prompts.length, 5);
assert.equal(transport.streamed.some(({ content }) => (
content === '当前待处理图片较多,请稍后重新发送。'
content.startsWith('当前待处理图片较多,请稍后重新发送。')
&& /错误码:INPUT_INVALID;参考号:MF-[A-F0-9]{8}$/.test(content)
)), true);
});
@ -816,13 +818,98 @@ test('Enterprise WeChat finalizes an existing progress stream when Harness fails
});
await bridge.accept(frame());
assert.deepEqual(replies, [
{ streamId: 'stream-failure', content: '正在思考中…', finish: false },
{ streamId: 'stream-failure', content: '消息处理失败,请稍后重试。', finish: true },
]);
assert.deepEqual(replies[0], {
streamId: 'stream-failure', content: '正在思考中…', finish: false,
});
assert.equal(replies[1].streamId, 'stream-failure');
assert.equal(replies[1].finish, true);
assert.match(replies[1].content, /任务未完成,暂时无法确定原因/);
assert.match(replies[1].content, /错误码:INTERNAL_UNKNOWN;参考号:MF-[A-F0-9]{8}$/);
assert.equal(store.seen.has('msg-1'), true);
});
test('Enterprise WeChat exposes a structured model rate limit without changing connection state', async () => {
const transport = testClient();
const status = {
...createWecomBridgeStatus(),
connected: true,
connectionState: 'connected',
};
const bridge = new WecomHarnessBridge({
client: transport.client,
generateStreamId: () => 'rate-limit-stream',
harness: {
sessionExists: async () => true,
ask: async () => {
const error = new Error('private Enterprise WeChat provider rate-limit detail');
error.code = 'harness-turn-failed';
error.providerCode = 'RATE_LIMIT';
throw error;
},
},
state: state(),
status,
logger: { error() {} },
});
await bridge.accept(frame({
msgid: 'wecom-rate-limit',
text: { content: '触发模型限流' },
}));
const failure = status.lastMessageError;
const visibleError = transport.streamed.at(-1).content;
assert.equal(failure.code, 'MODEL_RATE_LIMIT');
assert.equal(failure.reason, 'MODEL_RATE_LIMIT');
assert.match(failure.referenceId, /^MF-[A-F0-9]{8}$/);
assert.match(visibleError, /模型服务正在限流,本次任务未完成。请稍后重试。/);
assert.equal(visibleError.endsWith(`参考号:${failure.referenceId}`), true);
assert.doesNotMatch(visibleError, /private Enterprise WeChat provider rate-limit detail/);
assert.equal(status.connected, true);
assert.equal(status.connectionState, 'connected');
});
test('Enterprise WeChat does not resubmit a recorded prompt when the safe error reply fails', async () => {
const store = state();
let asks = 0;
let safeReplyAttempts = 0;
const bridge = new WecomHarnessBridge({
client: {
replyStream: async (_frame, _streamId, _content, finish) => {
if (!finish) return;
safeReplyAttempts += 1;
throw new Error('safe reply unavailable');
},
replyStreamNonBlocking: async () => {},
sendMessage: async () => {},
},
generateStreamId: () => 'safe-error-replay-stream',
harness: {
sessionExists: async () => true,
ask: async () => {
asks += 1;
const error = new Error('private provider failure');
error.code = 'harness-turn-failed';
error.providerCode = 'RATE_LIMIT';
throw error;
},
},
state: store,
logger: { error() {}, warn() {} },
});
const inbound = frame({
msgid: 'wecom-safe-error-replay',
text: { content: '请执行一次' },
});
await bridge.accept(inbound);
await bridge.accept(inbound);
assert.equal(asks, 1);
assert.equal(safeReplyAttempts, 1);
assert.equal(store.seen.has('wecom-safe-error-replay'), true);
});
test('an Enterprise WeChat answer bypasses the original conversation queue', async () => {
const transport = testClient();
const answered = deferred();
@ -1600,6 +1687,9 @@ test('Enterprise WeChat sends registered files after the final text and continue
assert.equal(uploads[1].bytes.toString(), '<h1>second</h1>');
assert.equal(active.length, 1);
assert.match(active[0].content, /first\.txt.*暂时未能/);
assert.equal(status.lastMessageError.code, 'CHANNEL_DELIVERY');
assert.equal(status.lastMessageError.reason, 'ARTIFACT_PROVIDER_FAILED');
assert.equal(active[0].content.endsWith(`参考号:${status.lastMessageError.referenceId}`), true);
assert.doesNotMatch(active[0].content, /private provider detail/);
assert.equal(status.artifactsSent, 1);
assert.equal(status.artifactSendErrors, 1);
@ -1655,6 +1745,8 @@ test('Enterprise WeChat still delivers registered files when final text delivery
}]);
assert.equal(finalTextAttempts, 1, 'must not append a generic retry stream after file success');
assert.equal(activeTextAttempts, 1);
assert.equal(bridge.status.lastMessageError.code, 'CHANNEL_DELIVERY_UNCERTAIN');
assert.match(bridge.status.lastMessageError.referenceId, /^MF-[A-F0-9]{8}$/);
});
test('Enterprise WeChat returns the authoritative receipt and one safe notice when text and file delivery fail', async (t) => {
@ -1754,7 +1846,9 @@ test('Enterprise WeChat keeps the generic error when no answer or file failure n
await bridge.accept(frame({ msgid: 'wecom-no-visible-failure' }));
assert.equal(attemptedActiveTexts.length, 2);
assert.deepEqual(finalStreamTexts, ['文字结果', '消息处理失败,请稍后重试。']);
assert.equal(finalStreamTexts[0], '文字结果');
assert.match(finalStreamTexts[1], /^回复发送结果未能确认/);
assert.match(finalStreamTexts[1], /错误码:CHANNEL_DELIVERY_UNCERTAIN;参考号:MF-[A-F0-9]{8}$/);
});
test('Enterprise WeChat reports an unacknowledged file message as uncertain', async (t) => {
@ -1980,7 +2074,7 @@ test('Enterprise WeChat keeps a failed batch for retry and queues later ordinary
await bridge.accept(frame({ msgid: 'retry-start', text: { content: '/batch' } }));
await bridge.accept(frame({ msgid: 'retry-content', text: { content: '需要重试' } }));
await bridge.accept(frame({ msgid: 'retry-send-1', text: { content: '/send' } }));
assert.match(transport.streamed.at(-1).content, /消息处理失败.*已保留 1 条消息/s);
assert.match(transport.streamed.at(-1).content, /错误码:INTERNAL_UNKNOWN.*已保留 1 条消息/s);
const retry = bridge.accept(frame({ msgid: 'retry-send-2', text: { content: '/send' } }));
await eventually(() => prompts.length === 2);

View file

@ -160,7 +160,7 @@ test('Enterprise WeChat runtime aborts an in-flight Harness interaction when sto
test('Enterprise WeChat runtime enables result-file delivery without per-bot configuration', async () => {
const client = new FakeClient();
const finished = deferred();
const askObserved = deferred();
let onArtifact;
const runtime = new WecomRuntime({
config: { botId: 'wecom_bot', remoteBotId: 'remote-bot' },
@ -170,12 +170,13 @@ test('Enterprise WeChat runtime enables result-file delivery without per-bot con
sessionExists: async () => true,
ask: async (_sessionId, _text, options) => {
onArtifact = options.onArtifact;
askObserved.resolve();
return '完成';
},
},
state: {
hasSeen: () => false,
markSeen: async () => finished.resolve(),
markSeen: async () => {},
sessionFor: () => 'session-existing',
setSession: async () => {},
clearSession: async () => {},
@ -195,7 +196,7 @@ test('Enterprise WeChat runtime enables result-file delivery without per-bot con
text: { content: '生成文件' },
},
});
await finished.promise;
await askObserved.promise;
assert.equal(typeof onArtifact, 'function');
await runtime.stop();
});