feat: add native image delivery across channels

This commit is contained in:
xmanrui 2026-08-24 03:25:38 +08:00
parent dbc0a4b730
commit 3ec5f1eca7
31 changed files with 2754 additions and 846 deletions

View file

@ -1977,6 +1977,121 @@ test('reaction failures do not block streaming replies', async () => {
assert.equal(status.streamResponses, 1);
});
test('Feishu routes Artifact images natively and preserves the shared fallback boundary', async (t) => {
const scenarios = [
{
name: 'native image',
fileName: 'native.png',
content: PNG_1X1,
expectedCalls: ['image'],
expectedPresentation: 'feishu-image',
expectedProviderIds: ['om-native-image'],
},
{
name: 'ordinary file',
fileName: 'ordinary.txt',
content: 'ordinary file',
expectedCalls: ['file'],
expectedPresentation: 'feishu-file',
expectedProviderIds: ['om-native-file'],
},
{
name: 'definite image rejection falls back',
fileName: 'fallback.png',
content: PNG_1X1,
imageError: 'artifact-provider-rejected',
expectedCalls: ['image', 'file'],
expectedPresentation: 'feishu-file',
expectedProviderIds: ['om-native-file'],
},
{
name: 'uncertain image never falls back',
fileName: 'uncertain.png',
content: PNG_1X1,
imageError: 'artifact-delivery-uncertain',
expectedCalls: ['image'],
expectedPresentation: 'text-fallback',
expectedProviderIds: [],
expectedOutcome: 'unknown',
},
];
for (const [index, scenario] of scenarios.entries()) {
await t.test(scenario.name, async (subtest) => {
const artifact = await committedArtifact(
subtest,
scenario.fileName,
scenario.content,
`bridge-image-route-${index}`,
);
const calls = [];
const status = bridgeStatus();
const resultFor = (file, presentation, providerMessageId) => ({
schemaVersion: 1,
deliveryId: file.deliveryKey,
presentation,
providerMessageIds: [providerMessageId],
artifacts: [{ artifactId: file.artifactId, outcome: 'sent' }],
});
const bridge = new FeishuHarnessBridge({
client: textClient(async () => { throw new Error('text intentionally unavailable'); }),
channel: {
addReaction: async () => 'reaction',
removeReaction: async () => undefined,
sendImage: async (chatId, file, options) => {
calls.push('image');
assert.equal(chatId, 'oc_chat');
assert.equal(file.fileName, scenario.fileName);
assert.equal(file.mediaType, 'image/png');
assert.equal(options.replyTo, `om-feishu-image-route-${index}`);
if (scenario.imageError) {
const error = new Error('private image result');
error.code = scenario.imageError;
throw error;
}
return resultFor(file, 'feishu-image', 'om-native-image');
},
sendFile: async (chatId, file, options) => {
calls.push('file');
assert.equal(chatId, 'oc_chat');
assert.equal(file.fileName, scenario.fileName);
assert.equal(options.replyTo, `om-feishu-image-route-${index}`);
return resultFor(file, 'feishu-file', 'om-native-file');
},
},
harness: {
sessionExists: async () => true,
ask: async (_sessionId, _text, options) => {
await options.onArtifact(artifact);
return '';
},
},
state: stateFixture([
['p2p:ou_user', `session-feishu-image-route-${index}`],
]).state,
status,
allowedSenderOpenIds: new Set(['ou_user']),
logger: { info() {}, warn() {}, error() {} },
});
const receipt = await bridge.accept(event(`om-feishu-image-route-${index}`, '生成产物'));
assert.deepEqual(calls, scenario.expectedCalls);
assert.equal(receipt.presentation, scenario.expectedPresentation);
assert.deepEqual(receipt.providerMessageIds, scenario.expectedProviderIds);
assert.deepEqual(receipt.artifacts, [{
artifactId: artifact.artifactId,
outcome: scenario.expectedOutcome ?? 'sent',
...(scenario.imageError === 'artifact-delivery-uncertain'
? { reason: 'artifact-delivery-uncertain' }
: {}),
}]);
assert.equal(status.artifactsSent, scenario.expectedOutcome === 'unknown' ? 0 : 1);
assert.equal(status.artifactSendErrors, scenario.expectedOutcome === 'unknown' ? 1 : 0);
});
}
});
test('Feishu finalizes the answer card before delivering registered result files and reports partial failure', async (t) => {
const html = await committedArtifact(t, 'result.html', '<h1>result</h1>', 'html');
const generic = await committedArtifact(t, 'notes.txt', 'notes', 'notes');

View file

@ -11,6 +11,7 @@ function fakeClient(overrides = {}) {
reactionsAdded: [],
reactionsRemoved: [],
fileUploads: [],
imageUploads: [],
};
const client = {
cardkit: { v1: {
@ -35,6 +36,12 @@ function fakeClient(overrides = {}) {
return { file_key: 'file-key-test' };
},
},
image: {
create: async (request) => {
calls.imageUploads.push(request);
return { data: { image_key: 'image-key-test' } };
},
},
message: {
reply: async (request) => {
calls.replies.push(request);
@ -62,6 +69,7 @@ function fakeClient(overrides = {}) {
if (overrides.updateContent) client.cardkit.v1.cardElement.content = overrides.updateContent;
if (overrides.finishCard) client.cardkit.v1.card.settings = overrides.finishCard;
if (overrides.uploadFile) client.im.v1.file.create = overrides.uploadFile;
if (overrides.uploadImage) client.im.v1.image.create = overrides.uploadImage;
if (overrides.replyMessage) client.im.v1.message.reply = overrides.replyMessage;
if (overrides.createMessage) client.im.v1.message.create = overrides.createMessage;
return { client, calls };
@ -172,6 +180,40 @@ test('VerifiedFeishuChannel uploads a materialized result and replies with a nat
});
});
test('VerifiedFeishuChannel uploads an image and replies with a native image message', async () => {
const { client, calls } = fakeClient();
const channel = new VerifiedFeishuChannel({ client });
const file = {
artifactId: 'artifact-image',
deliveryKey: 'delivery-image',
fileName: 'result.png',
mediaType: 'image/png',
size: 12,
bytes: Buffer.from('png contents'),
};
const receipt = await channel.sendImage('oc_chat', file, { replyTo: 'om_user' });
assert.equal(calls.fileUploads.length, 0);
assert.equal(calls.imageUploads.length, 1);
assert.deepEqual(calls.imageUploads[0].data, {
image_type: 'message',
image: file.bytes,
});
assert.equal(calls.replies.length, 1);
assert.equal(calls.replies[0].path.message_id, 'om_user');
assert.equal(calls.replies[0].data.msg_type, 'image');
assert.deepEqual(JSON.parse(calls.replies[0].data.content), { image_key: 'image-key-test' });
assert.match(calls.replies[0].data.uuid, /^dshim_[a-f0-9]{40}$/);
assert.deepEqual(receipt, {
schemaVersion: 1,
deliveryId: 'delivery-image',
presentation: 'feishu-image',
providerMessageIds: ['om-stream'],
artifacts: [{ artifactId: 'artifact-image', outcome: 'sent' }],
});
});
test('VerifiedFeishuChannel sends to the current chat when no reply target exists', async () => {
const { client, calls } = fakeClient({
createMessage: async (request) => {