refactor(ai-chat): 移除 excel_analyze/office_analyze 工具与 OfficeKit 依赖

- 删除 excel_analyze/office_analyze 两个 A2UI 工具(schema、分发、执行器、测试)
- 附件分析改为依赖上传时自动提取的文本与 preflight_import 报告 errorSamples
- 移除 @officecli/officecli 依赖及 OfficeCliService(PPT 上传不再自动提取文本)
- 保留 AiExcelReaderService(附件文本提取与导入预检仍使用)
This commit is contained in:
2026-08-05 22:26:05 +08:00
parent d1c933f032
commit 6249fefc64
15 changed files with 2 additions and 720 deletions

View File

@@ -1517,139 +1517,6 @@ describe('AiChatService', () => {
expect(data).toMatchObject({ id: 'review-1', status: 'submitted' });
});
it('office_analyze 通过 OfficeCli 分析用户自己的附件并返回结果', async () => {
const conversation = {
id: 3,
userId: 7,
title: '测试',
lockedSkillKey: null,
lastMessageAt: null,
};
const assistant = {
id: 12,
conversationId: 3,
role: 'assistant',
content: '',
reasoningContent: null,
status: 'pending',
errorCode: null,
replyToMessageId: 11,
metadata: {},
};
const messageSave = jest.fn(async (value) => value);
const messages = {
exists: jest.fn().mockResolvedValue(false),
find: jest.fn().mockResolvedValue([]),
findOne: jest.fn().mockImplementation((options?: unknown) => {
const opts = options as { select?: { metadata?: boolean } } | undefined;
if (opts?.select?.metadata) return Promise.resolve({ metadata: {} });
return Promise.resolve(assistant);
}),
save: messageSave,
};
const manager = {
create: jest.fn((_entity, value) => value),
save: jest
.fn()
.mockResolvedValueOnce({ id: 11, conversationId: 3, role: 'user', content: '分析一下' })
.mockResolvedValueOnce(assistant),
update: jest.fn(),
};
const toolRuns = {
create: jest.fn((value) => value),
save: jest.fn(async (value) => value),
find: jest.fn().mockResolvedValue([]),
};
const modelStream = {
stream: async function* () {
yield {
type: 'complete' as const,
toolCalls: [
{
id: 'call-1',
name: 'office_analyze',
arguments: JSON.stringify({ attachmentId: 5, action: 'outline' }),
},
],
};
},
};
const officeCli = {
run: jest.fn().mockResolvedValue({
success: true,
data: { sheets: [{ name: '入住名单', rows: 360, cols: 18 }] },
}),
};
const attachmentService = {
requireReadyOwned: jest.fn().mockResolvedValue([
{
id: 5,
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
storageKey: '1/test.xlsx',
},
]),
storagePathFor: jest.fn().mockReturnValue('/tmp/attachments/1/test.xlsx'),
readStoredBuffer: jest.fn().mockResolvedValue(Buffer.from('x')),
toModelParts: jest.fn().mockResolvedValue([]),
serialize: jest.fn((value) => value),
};
const service = new AiChatService(
{ findOne: jest.fn().mockResolvedValue(conversation) } as never,
messages as never,
toolRuns as never,
{ transaction: jest.fn(async (callback) => callback(manager)) } as never,
{ getRuntimeConfig: jest.fn().mockResolvedValue({ supportsVision: false }) } as never,
{ listAvailable: jest.fn().mockReturnValue([]) } as never,
modelStream as never,
attachmentService as never,
{
createForm: jest.fn(),
findOwnedPending: jest.fn(),
validateValues: jest.fn(),
markSubmitted: jest.fn(),
serialize: jest.fn((value) => value),
} as never,
{
createReview: jest.fn(),
expirePreviousReviews: jest.fn().mockResolvedValue([]),
findOwnedPending: jest.fn(),
findPendingByAssistantMessage: jest.fn(),
serialize: jest.fn((value) => value),
submit: jest.fn(),
} as never,
{
createChart: jest.fn(),
serialize: jest.fn((value) => value),
} as never,
{ createForUser: jest.fn().mockReturnValue({}) } as never,
{ assertPermission: jest.fn(), canPermission: jest.fn() } as never,
undefined,
officeCli as never,
);
const emitted: Array<{ event: string; data: Record<string, unknown> }> = [];
await service.streamMessage(
authenticatedUser as never,
3,
{
message: '分析一下附件',
attachmentIds: [5],
skillKey: null,
clientRequestId: '6a8bc680-3cb5-4f2d-85ee-974974e0f194',
},
new AbortController().signal,
(event, data) => emitted.push({ event, data }),
jest.fn(),
);
expect(officeCli.run).toHaveBeenCalledWith([
'view',
'/tmp/attachments/1/test.xlsx',
'outline',
'--json',
]);
expect(emitted.some(({ event }) => event === 'tool.completed')).toBe(true);
});
it('删除用户消息时连同其 AI 回答一起删除并更新会话时间', async () => {
const conversation = { id: 3, userId: 7, title: '新对话' };
@@ -2086,62 +1953,6 @@ describe('AiChatService', () => {
);
});
it('excel_analyze 用 ExcelJS 读取概览并返回给模型', async () => {
const { service } = createService();
const toolRun = { id: 1, status: 'running' };
const toolRuns = {
create: jest.fn((value) => value),
save: jest.fn(async (value) => ({ ...toolRun, ...value })),
};
const attachmentService = {
requireReadyOwned: jest.fn().mockResolvedValue([
{
id: 9,
mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
originalName: 'students.xlsx',
size: 10,
},
]),
readStoredBuffer: jest.fn().mockResolvedValue(Buffer.from('x')),
};
const excelReader = {
overview: jest.fn().mockResolvedValue({
sheets: [{ name: '学生', rowCount: 2, columns: ['姓名'] }],
text: '# 学生(共 2 行)\n姓名\n张三',
}),
readRows: jest.fn(),
};
(service as unknown as { toolRuns: unknown }).toolRuns = toolRuns;
(service as unknown as { attachmentService: unknown }).attachmentService = attachmentService;
(service as unknown as { excelReader: unknown }).excelReader = excelReader;
const emitted: Array<{ event: string }> = [];
const result = await (
service as unknown as {
executeExcelAnalyze(
messageId: number,
call: { id: string; name: string; arguments: string },
userId: number,
emit: (event: string, data?: unknown) => void,
): Promise<string>;
}
).executeExcelAnalyze(
42,
{
id: 'call-1',
name: 'excel_analyze',
arguments: JSON.stringify({ attachmentId: 9, action: 'overview' }),
},
7,
(event) => emitted.push({ event }),
);
const parsed = JSON.parse(result) as { status: string; data: { sheets: unknown[] } };
expect(parsed.status).toBe('success');
expect(parsed.data.sheets).toHaveLength(1);
expect(excelReader.overview).toHaveBeenCalledWith(expect.any(Buffer));
expect(emitted.some(({ event }) => event === 'tool.completed')).toBe(true);
});
it('start_import_wizard 拒绝非法的确认参数', async () => {
const { service } = createService();