Files
gongxue-base/apps/server/src/ai-chat/ai-chat.tool-actions.ts
wangziqi 24e0ecbdaf feat(ai): 业务上下文感知与 A2UI 链路统一
- 新增代码内业务上下文元数据层(实体字典 + 三大闭环工作流)
- 新增 get_business_context / get_entity_schema / get_pending_tasks 运行时工具
- SYSTEM_PROMPT 与技能目录改为先查业务流程/待办再执行
- A2UI 增加 ai_a2ui_submissions 幂等表、表单过期、ui.artifact 事件
- 提交回灌携带 submissionId / fieldErrors / 下一步建议
- 前端 uiArtifacts 归一化与过期表单禁用
2026-08-06 15:23:23 +08:00

276 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string> {
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<string> {
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<string, unknown>)
: {};
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<string> {
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';