import { AiReview } from './entities/ai-review.entity'; import type { AiChatServiceContext, AiSseEmitter, ModelToolCall } from './ai-chat.types'; import { buildA2uiArtifact } from './ai-a2ui.artifact'; import { finishToolRun, startToolRun } from './ai-chat.tools'; export async function executeRenderForm( context: AiChatServiceContext, messageId: number, call: ModelToolCall, userId: number, emit: AiSseEmitter, ): Promise { const { run, parsedArgs, startedAt } = await startToolRun(context, messageId, call, emit, { toolName: 'render_form', skillKey: null, }); try { const assistant = await context.messages.findOne({ where: { id: messageId } }); if (!assistant) throw new Error('assistant message missing'); const form = await context.formService.createForm( { userId, conversationId: assistant.conversationId, assistantMessageId: messageId }, parsedArgs, ); const expiredForms = await context.formService.expirePreviousForms( userId, assistant.conversationId, form.id, ); await Promise.all( expiredForms.map(async (expired) => { const oldAssistant = await context.messages.findOne({ where: { id: expired.assistantMessageId, conversationId: assistant.conversationId }, }); const oldA2ui = oldAssistant?.metadata?.a2uiForm; if (oldAssistant && oldA2ui && typeof oldA2ui === 'object' && !Array.isArray(oldA2ui)) { oldAssistant.metadata = { ...oldAssistant.metadata, a2uiForm: context.formService.serialize(expired), }; await context.messages.save(oldAssistant); } const expiredPayload = context.formService.serialize(expired); emit('ui.form', { messageId: expired.assistantMessageId, form: expiredPayload }); emit('ui.artifact', { messageId: expired.assistantMessageId, artifact: buildA2uiArtifact({ type: 'form', id: expired.id, status: 'expired', messageId: expired.assistantMessageId, conversationId: assistant.conversationId, payload: expiredPayload, }), }); }), ); assistant.metadata = { ...assistant.metadata, a2uiForm: context.formService.serialize(form), }; await context.messages.save(assistant); await finishToolRun(context, run, call, startedAt, { status: 'success', summary: '已生成表单,等待用户填写' }, emit); emit('ui.form', { messageId, form: context.formService.serialize(form), }); emit('ui.artifact', { messageId, artifact: buildA2uiArtifact({ type: 'form', id: form.id, status: form.status === 'submitted' ? 'submitted' : 'pending', messageId, conversationId: assistant.conversationId, payload: context.formService.serialize(form), }), }); return JSON.stringify({ status: 'success', formId: form.id, message: '表单已显示给用户,请提示用户填写并提交', }); } catch { await finishToolRun(context, run, call, startedAt, { status: 'failed', summary: '表单参数无效', error: '表单参数无效' }, emit); return JSON.stringify({ status: 'failed', error: '表单参数无效' }); } } export async function executeRenderReview( context: AiChatServiceContext, messageId: number, call: ModelToolCall, userId: number, emit: AiSseEmitter, ): Promise { const { run, parsedArgs, startedAt } = await startToolRun(context, messageId, call, emit, { toolName: 'render_review', skillKey: null, argumentsData: null, }); try { const existingReview = await context.reviewService.findPendingByAssistantMessage(messageId); if (existingReview) { const denial = `本回合已生成导入预览《${existingReview.title}》,请直接提示用户审阅并确认,不要再次调用 render_review;如需多个分表,应全部合并到同一张预览卡。`; await finishToolRun(context, run, call, startedAt, { status: 'failed', summary: denial, error: denial }, emit); return JSON.stringify({ status: 'failed', error: denial }); } const assistant = await context.messages.findOne({ where: { id: messageId } }); if (!assistant) throw new Error('assistant message missing'); const parsedRecord = parsedArgs && typeof parsedArgs === 'object' && !Array.isArray(parsedArgs) ? (parsedArgs as Record) : {}; const attachmentId = typeof parsedRecord.attachmentId === 'number' ? parsedRecord.attachmentId : undefined; let review: AiReview; if (Number.isInteger(attachmentId) && (attachmentId as number) > 0) { const [attachment] = await context.attachmentService.requireReadyOwned(userId, [ attachmentId as number, ]); if ( !attachment.mimeType.includes('spreadsheetml') && !attachment.mimeType.includes('excel') && !attachment.mimeType.includes('csv') ) { throw new Error('附件不是 Excel 文件,无法生成导入预览'); } if (!context.excelReader) throw new Error('Excel 解析器未配置'); const buffer = await context.attachmentService.readStoredBuffer(attachment); const sheets = await context.excelReader.loadSheets(buffer); const sections = await context.reviewService.buildSectionsFromWorkbook(sheets, parsedArgs); review = await context.reviewService.createReview( { userId, conversationId: assistant.conversationId, assistantMessageId: messageId }, { title: parsedRecord.title, summary: parsedRecord.summary ?? null, sections }, ); } else { review = await context.reviewService.createReview( { userId, conversationId: assistant.conversationId, assistantMessageId: messageId }, parsedArgs, ); } const expiredReviews = await context.reviewService.expirePreviousReviews( userId, assistant.conversationId, review.id, ); await Promise.all( expiredReviews.map(async (expired) => { const oldAssistant = await context.messages.findOne({ where: { id: expired.assistantMessageId, conversationId: assistant.conversationId }, }); const oldA2ui = oldAssistant?.metadata?.a2uiReview; if (oldAssistant && oldA2ui && typeof oldA2ui === 'object' && !Array.isArray(oldA2ui)) { oldAssistant.metadata = { ...oldAssistant.metadata, a2uiReview: context.reviewService.serialize(expired), }; await context.messages.save(oldAssistant); } emit('ui.review', { messageId: expired.assistantMessageId, review: context.reviewService.serialize(expired), }); emit('ui.artifact', { messageId: expired.assistantMessageId, artifact: buildA2uiArtifact({ type: 'review', id: expired.id, status: expired.status === 'submitted' ? 'submitted' : 'expired', messageId: expired.assistantMessageId, conversationId: assistant.conversationId, payload: context.reviewService.serialize(expired), }), }); }), ); assistant.metadata = { ...assistant.metadata, a2uiReview: context.reviewService.serialize(review), }; await context.messages.save(assistant); await finishToolRun(context, run, call, startedAt, { status: 'success', summary: '已生成导入预览,等待用户确认' }, emit); emit('ui.review', { messageId, review: context.reviewService.serialize(review), }); emit('ui.artifact', { messageId, artifact: buildA2uiArtifact({ type: 'review', id: review.id, status: review.status === 'submitted' ? 'submitted' : 'pending', messageId, conversationId: assistant.conversationId, payload: context.reviewService.serialize(review), }), }); return JSON.stringify({ status: 'success', reviewId: review.id, message: '导入预览已显示给用户,请提示用户审阅并确认', }); } catch (reason) { const errorMessage = reason instanceof Error && reason.message ? reason.message.slice(0, 120) : '导入预览参数无效'; await finishToolRun(context, run, call, startedAt, { status: 'failed', summary: errorMessage, error: errorMessage }, emit); return JSON.stringify({ status: 'failed', error: errorMessage }); } } export async function executeRenderChart( context: AiChatServiceContext, messageId: number, call: ModelToolCall, emit: AiSseEmitter, ): Promise { const { run, parsedArgs, startedAt } = await startToolRun(context, messageId, call, emit, { toolName: 'render_chart', skillKey: null, argumentsData: null, }); try { const assistant = await context.messages.findOne({ where: { id: messageId } }); if (!assistant) throw new Error('assistant message missing'); const chart = context.chartService.createChart(parsedArgs); const existingCharts = assistant.metadata?.a2uiChart; const charts = Array.isArray(existingCharts) ? [...existingCharts] : existingCharts ? [existingCharts] : []; charts.push(context.chartService.serialize(chart)); assistant.metadata = { ...assistant.metadata, a2uiChart: charts, }; await context.messages.save(assistant); await finishToolRun(context, run, call, startedAt, { status: 'success', summary: '已生成图表' }, emit); emit('ui.chart', { messageId, chart: context.chartService.serialize(chart), }); emit('ui.artifact', { messageId, artifact: buildA2uiArtifact({ type: 'chart', id: chart.id, status: 'pending', messageId, conversationId: assistant.conversationId, payload: context.chartService.serialize(chart), }), }); return JSON.stringify({ status: 'success', chartId: chart.id, message: '图表已显示给用户', }); } catch { await finishToolRun(context, run, call, startedAt, { status: 'failed', summary: '图表参数无效', error: '图表参数无效' }, emit); return JSON.stringify({ status: 'failed', error: '图表参数无效' }); } } export { compactImportWizard, executePreflightImport, executeStartImportWizard, } from './ai-chat.tool-actions.import';