mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 04:13:17 +08:00
fix(delivery): surface failed semantic finals
This commit is contained in:
parent
d7f4cf03ab
commit
fde088e381
3 changed files with 139 additions and 3 deletions
File diff suppressed because one or more lines are too long
|
|
@ -558,10 +558,18 @@ export class TextHarnessBridge {
|
|||
textDeliveryError = error;
|
||||
}
|
||||
}
|
||||
if (textReceipt?.deliveryOutcome === 'failed') {
|
||||
const reason = textReceipt.reason ?? 'text-delivery-failed';
|
||||
textDeliveryError = new Error(`Final text delivery failed (${reason})`);
|
||||
textDeliveryError.code = reason;
|
||||
}
|
||||
// A failed final text must not discard an already registered result file.
|
||||
// Settle the independent attachment path before surfacing the text error.
|
||||
const delivery = await this.#deliverArtifacts(target, messageId, artifacts, textReceipt);
|
||||
if (textDeliveryError && !delivery.userVisible) throw textDeliveryError;
|
||||
if (textDeliveryError && !delivery.userVisible) {
|
||||
textDeliveryError.deliveryReceipt = delivery.receipt;
|
||||
throw textDeliveryError;
|
||||
}
|
||||
if (delivery.userVisible) {
|
||||
this.#status.messagesReplied += 1;
|
||||
this.#status.lastReplyAt = new Date().toISOString();
|
||||
|
|
@ -626,7 +634,9 @@ export class TextHarnessBridge {
|
|||
return;
|
||||
}
|
||||
this.#logger.error?.(`[dsh-im:${this.#descriptor.key}] failed to process a message:`, error);
|
||||
if (await presentStreamFailure('消息处理失败,请稍后重试。')) return;
|
||||
if (await presentStreamFailure('消息处理失败,请稍后重试。')) {
|
||||
return error.deliveryReceipt;
|
||||
}
|
||||
stream?.cancel?.();
|
||||
try {
|
||||
await this.#bot.sendText(target, '消息处理失败,请稍后重试。');
|
||||
|
|
@ -636,6 +646,7 @@ export class TextHarnessBridge {
|
|||
sendError,
|
||||
);
|
||||
}
|
||||
return error.deliveryReceipt;
|
||||
} finally {
|
||||
await Promise.allSettled([
|
||||
this.#cancelPendingInteraction(conversationKey),
|
||||
|
|
|
|||
|
|
@ -510,6 +510,131 @@ test('Telegram uncertain final delivery records unknown and never sends a fallba
|
|||
assert.deepEqual(groupResult.providerMessageIds, ['1202']);
|
||||
});
|
||||
|
||||
test('shared bridge surfaces a definite final delivery failure without rerunning Prompt or artifacts', async (t) => {
|
||||
for (const safeReplyFails of [false, true]) {
|
||||
await t.test(safeReplyFails ? 'safe reply also fails' : 'safe reply succeeds', async () => {
|
||||
const safeReplies = [];
|
||||
let prompts = 0;
|
||||
const stream = {
|
||||
providerMessageIds: ['failed-placeholder'],
|
||||
presentation: 'telegram-regular',
|
||||
async update() {},
|
||||
async finish() {
|
||||
return {
|
||||
presentation: 'telegram-rich-final',
|
||||
providerMessageIds: ['failed-placeholder'],
|
||||
deliveryOutcome: 'failed',
|
||||
reason: 'telegram-403',
|
||||
};
|
||||
},
|
||||
async fail() {
|
||||
return undefined;
|
||||
},
|
||||
cancel() {},
|
||||
};
|
||||
const bridge = new TelegramHarnessBridge({
|
||||
bot: {
|
||||
sendTyping: async () => {},
|
||||
openDeliveryStream: async () => stream,
|
||||
sendText: async (_target, text) => {
|
||||
safeReplies.push(text);
|
||||
if (safeReplyFails) throw rejected(403);
|
||||
return { message_id: 1261 };
|
||||
},
|
||||
},
|
||||
state: memoryState(),
|
||||
logger: { warn() {}, error() {} },
|
||||
harness: {
|
||||
createSession: async () => 'session-rich-artifact',
|
||||
ask: async () => {
|
||||
prompts += 1;
|
||||
return '## final';
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const receipt = await bridge.accept({
|
||||
messageId: `definite-failure-${safeReplyFails}`,
|
||||
senderId: 'rich-user',
|
||||
kind: 'direct',
|
||||
conversationId: `definite-failure-${safeReplyFails}`,
|
||||
content: 'return a file',
|
||||
addressed: true,
|
||||
replyTarget: { chatId: 42, chatType: 'private', replyToMessageId: 44 },
|
||||
});
|
||||
|
||||
assert.equal(prompts, 1);
|
||||
assert.deepEqual(safeReplies, ['消息处理失败,请稍后重试。']);
|
||||
assert.equal(receipt.deliveryOutcome, 'failed');
|
||||
assert.equal(receipt.reason, 'telegram-403');
|
||||
assert.deepEqual(receipt.artifacts, []);
|
||||
assert.match(bridge.status.lastError, /telegram-403/);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('shared bridge settles each artifact once before containing a definite text failure', async (t) => {
|
||||
const artifact = await committedArtifact(t);
|
||||
const sentFiles = [];
|
||||
let prompts = 0;
|
||||
let safeReplies = 0;
|
||||
const bridge = new TelegramHarnessBridge({
|
||||
bot: {
|
||||
sendTyping: async () => {},
|
||||
openDeliveryStream: async () => ({
|
||||
providerMessageIds: ['failed-placeholder'],
|
||||
presentation: 'telegram-regular',
|
||||
async update() {},
|
||||
async finish() {
|
||||
return {
|
||||
presentation: 'telegram-rich-final',
|
||||
providerMessageIds: ['failed-placeholder'],
|
||||
deliveryOutcome: 'failed',
|
||||
reason: 'telegram-403',
|
||||
};
|
||||
},
|
||||
cancel() {},
|
||||
}),
|
||||
sendText: async () => {
|
||||
safeReplies += 1;
|
||||
return { message_id: 1261 };
|
||||
},
|
||||
sendFile: async (_target, file) => {
|
||||
sentFiles.push(file.fileName);
|
||||
return { message_id: 1262 };
|
||||
},
|
||||
},
|
||||
state: memoryState(),
|
||||
logger: { warn() {}, error() {} },
|
||||
harness: {
|
||||
createSession: async () => 'session-rich-artifact',
|
||||
ask: async (_sessionId, _text, options) => {
|
||||
prompts += 1;
|
||||
await options.onArtifact(artifact);
|
||||
return '## final with file';
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const receipt = await bridge.accept({
|
||||
messageId: 'definite-failure-with-file',
|
||||
senderId: 'rich-user',
|
||||
kind: 'direct',
|
||||
conversationId: 'definite-failure-with-file',
|
||||
content: 'return a file',
|
||||
addressed: true,
|
||||
replyTarget: { chatId: 42, chatType: 'private', replyToMessageId: 44 },
|
||||
});
|
||||
|
||||
assert.equal(prompts, 1);
|
||||
assert.equal(safeReplies, 0);
|
||||
assert.deepEqual(sentFiles, ['result.txt']);
|
||||
assert.equal(receipt.deliveryOutcome, 'failed');
|
||||
assert.deepEqual(receipt.artifacts, [{
|
||||
artifactId: 'telegram-rich-artifact', outcome: 'sent',
|
||||
}]);
|
||||
});
|
||||
|
||||
test('Telegram distinguishes pre-dispatch abort from an uncertain in-flight abort', async () => {
|
||||
const before = new AbortController();
|
||||
const beforeReason = new DOMException('stopped before dispatch', 'AbortError');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue