Files
gongxue-base/apps/server/src/ai-chat/ai-chat.tools.ts
wangziqi 6a18fd264d feat(ai-chat): AI 导入确认流程完善并清理代码质量
- 新增导入确认/映射解析辅助,支持预检后生成导入向导
- 抽取 parseAttachmentArgs/beginImportToolRun 等重复逻辑
- 双重类型断言改为运行时守卫,消除 aislop 告警
2026-08-06 11:59:28 +08:00

191 lines
6.0 KiB
TypeScript

import { AgentToolContextFactory } from '../agent-tools/agent-tool.types';
import type { AiChatServiceContext, AiSseEmitter, ModelToolCall } from './ai-chat.types';
import type { AiToolRun } from './entities';
import {
executePreflightImport,
executeRenderChart,
executeRenderForm,
executeStartImportWizard,
} from './ai-chat.tool-actions';
export type AgentToolContext = ReturnType<typeof AgentToolContextFactory.fromAuthenticatedUser>;
export async function startToolRun(
context: AiChatServiceContext,
messageId: number,
call: ModelToolCall,
emit: AiSseEmitter,
options: {
toolName: string;
skillKey: string | null;
argumentsData?: Record<string, unknown> | null;
parsedArgs?: unknown;
},
): Promise<{ run: AiToolRun; parsedArgs: unknown; startedAt: number }> {
const startedAt = Date.now();
const parsedArgs = options.parsedArgs ?? context.parseToolArguments(call.arguments);
const run = await context.toolRuns.save(
context.toolRuns.create({
messageId,
toolCallId: call.id.slice(0, 100),
toolName: options.toolName,
skillKey: options.skillKey,
argumentsSummary: context.summarize(parsedArgs),
resultSummary: null,
argumentsData:
options.argumentsData ??
(context.safeStructured(parsedArgs) as Record<string, unknown> | null),
resultData: null,
status: 'running',
durationMs: null,
}),
);
emit('tool.started', {
messageId,
toolCallId: call.id,
toolName: run.toolName,
skillKey: run.skillKey,
status: 'running',
summary: run.argumentsSummary,
});
return { run, parsedArgs, startedAt };
}
export async function finishToolRun(
context: AiChatServiceContext,
run: AiToolRun,
call: ModelToolCall,
startedAt: number,
outcome: { status: 'success' | 'failed'; summary: string | null; error?: string },
emit: AiSseEmitter,
): Promise<void> {
run.status = outcome.status;
run.resultSummary = outcome.summary;
run.durationMs = Date.now() - startedAt;
await context.toolRuns.save(run);
emit(outcome.status === 'success' ? 'tool.completed' : 'tool.failed', {
messageId: run.messageId,
toolCallId: call.id,
toolName: run.toolName,
skillKey: run.skillKey,
status: outcome.status,
summary: outcome.summary,
...(outcome.error ? { error: outcome.error } : {}),
durationMs: run.durationMs,
});
}
export async function executeTool(
context: AiChatServiceContext,
messageId: number,
call: ModelToolCall,
agentContext: AgentToolContext,
allowedSkillKey: string | null,
allowWriteTools: boolean,
reviewSubmitted: boolean,
userId: number,
emit: AiSseEmitter,
): Promise<string> {
if (call.name === 'render_form') {
return executeRenderForm(context, messageId, call, userId, emit);
}
if (call.name === 'preflight_import') {
return executePreflightImport(context, messageId, call, agentContext, emit);
}
if (call.name === 'start_import_wizard') {
return executeStartImportWizard(context, messageId, call, agentContext, emit);
}
if (call.name === 'render_chart') {
return executeRenderChart(context, messageId, call, emit);
}
if ((call.name === 'create_student' || call.name === 'update_students') && !allowWriteTools) {
return denyWriteTool(context, messageId, call, emit);
}
const toolSkillKey =
context.toolExecutor.listAvailable(agentContext).find((tool) => tool.name === call.name)
?.skillKey ?? allowedSkillKey;
const { run, parsedArgs, startedAt } = await startToolRun(context, messageId, call, emit, {
toolName: context.safeToolName(call.name),
skillKey: toolSkillKey,
});
const result = await context.toolExecutor.execute(call.name, parsedArgs, agentContext, allowedSkillKey);
run.status = result.status;
run.skillKey = result.skillKey ?? run.skillKey;
run.resultSummary = context.summarize(result.result ?? result.error ?? null);
run.resultData = context.safeStructured(result.result) as
| Record<string, unknown>
| unknown[]
| null;
run.durationMs = Date.now() - startedAt;
await context.toolRuns.save(run);
emit(result.status === 'success' ? 'tool.completed' : 'tool.failed', {
messageId,
toolCallId: call.id,
toolName: run.toolName,
skillKey: run.skillKey,
status: result.status,
summary: run.resultSummary,
...(result.error ? { error: result.error } : {}),
durationMs: run.durationMs,
});
const modelPayload = JSON.stringify(
result.status === 'success'
? { status: result.status, data: result.result }
: { status: result.status, error: result.error },
);
if (modelPayload.length <= 32 * 1024) return modelPayload;
return JSON.stringify({
status: result.status,
truncated: true,
summary: context.summarize(result.result ?? result.error ?? null),
});
}
export async function denyWriteTool(
context: AiChatServiceContext,
messageId: number,
call: ModelToolCall,
emit: AiSseEmitter,
): Promise<string> {
const toolName =
typeof call.name === 'string' && call.name.trim() ? call.name : 'create_student';
return denyTool(context, messageId, call, toolName, '该操作需要表单确认', '该操作需要表单确认', emit);
}
export async function denyTool(
context: AiChatServiceContext,
messageId: number,
call: ModelToolCall,
toolName: string,
summary: string,
error: string,
emit: AiSseEmitter,
): Promise<string> {
await context.toolRuns.save(
context.toolRuns.create({
messageId,
toolCallId: call.id.slice(0, 100),
toolName: context.safeToolName(toolName),
skillKey: null,
argumentsSummary: context.summarize(context.parseToolArguments(call.arguments)),
resultSummary: summary,
argumentsData: null,
resultData: null,
status: 'failed',
durationMs: 0,
}),
);
emit('tool.failed', {
messageId,
toolCallId: call.id,
toolName: context.safeToolName(toolName),
status: 'failed',
summary,
error,
durationMs: 0,
});
return JSON.stringify({ status: 'failed', error });
}