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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user