forked from wangziqi/gongxue-base
130 lines
5.1 KiB
TypeScript
130 lines
5.1 KiB
TypeScript
import { MAX_SUMMARY_CHARS } from './ai-chat.constants';
|
|
import type { AiChatServiceContext, AiSseEmitter, ModelToolCall } from './ai-chat.types';
|
|
import { finishToolRun, startToolRun } from './ai-chat.tools';
|
|
|
|
export async function executeOfficeAnalyze(
|
|
context: AiChatServiceContext,
|
|
messageId: number,
|
|
call: ModelToolCall,
|
|
userId: number,
|
|
emit: AiSseEmitter,
|
|
): Promise<string> {
|
|
if (!context.officeCli) {
|
|
return JSON.stringify({ status: 'failed', error: 'OfficeCli 未配置' });
|
|
}
|
|
const parsedArgs = context.parseToolArguments(call.arguments);
|
|
const args =
|
|
parsedArgs && typeof parsedArgs === 'object' && !Array.isArray(parsedArgs)
|
|
? (parsedArgs as Record<string, unknown>)
|
|
: {};
|
|
const action = typeof args.action === 'string' ? args.action : '';
|
|
const validActions = new Set(['stats', 'outline', 'text', 'get', 'query', 'issues']);
|
|
if (!validActions.has(action)) {
|
|
return JSON.stringify({ status: 'failed', error: 'office_analyze 参数无效' });
|
|
}
|
|
|
|
const { run, startedAt } = await startToolRun(context, messageId, call, emit, {
|
|
toolName: 'office_analyze',
|
|
skillKey: null,
|
|
argumentsData: context.safeStructured(args) as Record<string, unknown> | null,
|
|
parsedArgs,
|
|
});
|
|
|
|
try {
|
|
let attachmentId = Number(args.attachmentId);
|
|
if (!Number.isInteger(attachmentId) || attachmentId <= 0) {
|
|
const assistant = await context.messages.findOne({
|
|
where: { id: messageId },
|
|
relations: { replyToMessage: { attachments: true } },
|
|
});
|
|
const officeAttachment = (assistant?.replyToMessage?.attachments ?? []).find(
|
|
(item) =>
|
|
item.mimeType?.includes('spreadsheetml') ||
|
|
item.mimeType?.includes('wordprocessingml') ||
|
|
item.mimeType?.includes('presentationml'),
|
|
);
|
|
if (!officeAttachment) throw new Error('未指定附件且当前消息没有 Office 附件');
|
|
attachmentId = officeAttachment.id;
|
|
}
|
|
const [attachment] = await context.attachmentService.requireReadyOwned(userId, [attachmentId]);
|
|
if (!attachment) throw new Error('附件不存在');
|
|
const mimeType = attachment.mimeType ?? '';
|
|
const isOffice =
|
|
mimeType.includes('spreadsheetml') ||
|
|
mimeType.includes('wordprocessingml') ||
|
|
mimeType.includes('presentationml');
|
|
if (!isOffice) throw new Error('该附件不是 Office 文档');
|
|
const filePath = context.attachmentService.storagePathFor(attachment);
|
|
|
|
const cliArgs = buildOfficeCliArgs(action, filePath, args);
|
|
const result = await context.officeCli.run(cliArgs);
|
|
if (!result.success) {
|
|
const cliError = context.redactText(String(result.error ?? 'OfficeCli 分析失败')).slice(
|
|
0,
|
|
MAX_SUMMARY_CHARS,
|
|
);
|
|
await finishToolRun(context, run, call, startedAt, { status: 'failed', summary: cliError, error: cliError }, emit);
|
|
return JSON.stringify({ status: 'failed', error: 'OfficeCli 分析失败' });
|
|
}
|
|
|
|
let payload: string;
|
|
try {
|
|
payload = JSON.stringify(result.data);
|
|
} catch {
|
|
payload = '{}';
|
|
}
|
|
const MAX_OFFICE_RESULT_CHARS = 96 * 1024;
|
|
let truncated = false;
|
|
if (payload.length > MAX_OFFICE_RESULT_CHARS) {
|
|
truncated = true;
|
|
payload = `${payload.slice(0, MAX_OFFICE_RESULT_CHARS)}\n\n[结果过大已截断,请缩小读取范围]`;
|
|
}
|
|
let parsedData: unknown;
|
|
try {
|
|
parsedData = JSON.parse(payload);
|
|
} catch {
|
|
parsedData = { raw: payload.slice(0, 4000) };
|
|
}
|
|
|
|
await finishToolRun(context, run, call, startedAt, { status: 'success', summary: context.summarize(result.data) }, emit);
|
|
return JSON.stringify({ status: 'success', data: parsedData, truncated });
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
const failureSummary = context.redactText(errorMessage).slice(0, MAX_SUMMARY_CHARS);
|
|
await finishToolRun(context, run, call, startedAt, { status: 'failed', summary: failureSummary, error: failureSummary }, emit);
|
|
return JSON.stringify({ status: 'failed', error: run.resultSummary });
|
|
}
|
|
}
|
|
|
|
export function buildOfficeCliArgs(
|
|
action: string,
|
|
filePath: string,
|
|
args: Record<string, unknown>,
|
|
): string[] {
|
|
if (action === 'get') {
|
|
const path = typeof args.path === 'string' ? args.path.slice(0, 200) : '';
|
|
if (!path.startsWith('/') || path.includes('..')) {
|
|
throw new Error('office_analyze 路径无效');
|
|
}
|
|
return ['get', filePath, path, '--json'];
|
|
}
|
|
if (action === 'query') {
|
|
const selector = typeof args.selector === 'string' ? args.selector.slice(0, 200) : '';
|
|
if (!selector) throw new Error('office_analyze 缺少 selector');
|
|
return ['query', filePath, selector, '--json'];
|
|
}
|
|
if (action === 'text') {
|
|
const extra: string[] = [];
|
|
const maxLines = Number(args.maxLines);
|
|
if (Number.isInteger(maxLines) && maxLines >= 1 && maxLines <= 200) {
|
|
extra.push('--max-lines', String(maxLines));
|
|
}
|
|
const startRow = Number(args.startRow);
|
|
if (Number.isInteger(startRow) && startRow > 1) {
|
|
extra.push('--start', String(startRow));
|
|
}
|
|
return ['view', filePath, 'text', '--json', ...extra];
|
|
}
|
|
return ['view', filePath, action, '--json'];
|
|
}
|