feat: 支持智能体文档附件与轻量读取

- 建立文档上传、异步读取、对象存储、补偿与聊天绑定闭环

- 按智能体 20K 上下文预算选择文档片段并保留稳定引用

- 统一聊天文件卡片、类型图标、草稿恢复与可靠下载
This commit is contained in:
2026-07-29 01:32:19 +08:00
parent fceedd02cd
commit d45c67a317
72 changed files with 5876 additions and 237 deletions

View File

@@ -1,6 +1,6 @@
import {describe, expect, it} from 'vitest';
import type { ChatTimelineMessageItem } from '@easyflow/common-ui';
import type {ChatTimelineMessageItem} from '@easyflow/common-ui';
import { describe, expect, it } from 'vitest';
import {
applyAgentSseEnvelope,
@@ -16,6 +16,18 @@ describe('agentTimelineAdapter', () => {
senderRole: 'user',
contentText: '帮我查一下',
roundId: 'r1',
contentPayload: {
attachments: [
{
attachmentRef: 'formal:r1:document:0',
downloadUrl:
'/api/v1/agent/media/document/content?reference=formal:r1:document:0',
name: '需求说明.docx',
readSnapshotId: 'snapshot-1',
size: 1024,
},
],
},
},
{
id: '2',
@@ -49,6 +61,17 @@ describe('agentTimelineAdapter', () => {
expect(
items.some((item) => item.type === 'message' && item.role === 'user'),
).toBe(true);
const user = items.find(
(item): item is ChatTimelineMessageItem =>
item.type === 'message' && item.role === 'user',
);
expect(user?.documents?.[0]).toEqual(
expect.objectContaining({
attachmentRef: 'formal:r1:document:0',
name: '需求说明.docx',
status: 'ready',
}),
);
const assistant = items.find(
(item): item is ChatTimelineMessageItem =>
item.type === 'message' && item.role === 'assistant',
@@ -219,7 +242,9 @@ describe('agentTimelineAdapter', () => {
),
).toBe(true);
expect(
items.some((item) => item.type === 'message' && item.role === 'assistant'),
items.some(
(item) => item.type === 'message' && item.role === 'assistant',
),
).toBe(true);
const assistant = items.find(
(item): item is ChatTimelineMessageItem =>
@@ -279,7 +304,9 @@ describe('agentTimelineAdapter', () => {
expect(items.some((item) => item.type === 'tool')).toBe(false);
expect(items.some((item) => item.type === 'status')).toBe(false);
expect(
items.some((item) => item.type === 'message' && item.role === 'assistant'),
items.some(
(item) => item.type === 'message' && item.role === 'assistant',
),
).toBe(true);
});
@@ -301,11 +328,7 @@ describe('agentTimelineAdapter', () => {
it('reconciles streamed text with the canonical final answer', () => {
const items: any[] = [];
for (const delta of [
'http://127.0.0.1:39',
'0',
'/easyflow/file.docx',
]) {
for (const delta of ['http://127.0.0.1:39', '0', '/easyflow/file.docx']) {
applyAgentSseEnvelope(items, {
domain: 'LLM',
type: 'MESSAGE',