feat(ai): 业务上下文感知与 A2UI 链路统一

- 新增代码内业务上下文元数据层(实体字典 + 三大闭环工作流)
- 新增 get_business_context / get_entity_schema / get_pending_tasks 运行时工具
- SYSTEM_PROMPT 与技能目录改为先查业务流程/待办再执行
- A2UI 增加 ai_a2ui_submissions 幂等表、表单过期、ui.artifact 事件
- 提交回灌携带 submissionId / fieldErrors / 下一步建议
- 前端 uiArtifacts 归一化与过期表单禁用
This commit is contained in:
2026-08-06 15:23:23 +08:00
parent d9c541dacc
commit 24e0ecbdaf
44 changed files with 2125 additions and 41 deletions

View File

@@ -8,6 +8,7 @@ import { usePermissionStore } from '../../store/permission/permissionStore';
import { useUserStore } from '../../store/user/userStore';
import type {
AiAttachment,
AiArtifactSchema,
AiChatInput,
AiChatMessage,
AiChartSchema,
@@ -34,6 +35,7 @@ interface AiSsePayload {
durationMs?: number | null;
attachment?: AiAttachment;
form?: AiFormSchema;
artifact?: AiArtifactSchema;
review?: AiReviewSchema;
chart?: AiChartSchema;
preflight?: AiImportPreflight;
@@ -62,6 +64,7 @@ function emptyAssistant(): AiChatMessage {
toolRuns: [],
attachments: [],
forms: [],
uiArtifacts: [],
};
}
@@ -165,10 +168,50 @@ function applyMessagePayload(
message.charts,
(nested.metadata?.a2uiChart as AiChartSchema | AiChartSchema[] | undefined) ?? payload.chart,
);
const artifacts = nested.metadata?.uiArtifacts;
if (Array.isArray(artifacts)) {
for (const artifact of artifacts) {
if (artifact && typeof artifact === 'object' && typeof artifact.id === 'string') {
mergeArtifactIntoMessage(message, artifact as AiArtifactSchema);
}
}
}
message.replyToMessageId = nested.replyToMessageId ?? message.replyToMessageId;
message.metadata = nested.metadata ?? message.metadata;
}
/**
* 将统一 artifact 归入 uiArtifacts并按类型派发到 legacy 列表。
*/
export function mergeArtifactIntoMessage(
message: AiChatMessage,
artifact: AiArtifactSchema,
): AiChatMessage {
message.uiArtifacts = mergeById<AiArtifactSchema>(message.uiArtifacts, artifact);
const payload =
artifact.payload && typeof artifact.payload === 'object'
? (artifact.payload as Record<string, unknown>)
: {};
if (artifact.type === 'form') {
message.forms = mergeForms(message.forms, payload as unknown as AiFormSchema);
} else if (artifact.type === 'review') {
message.reviews = mergeById<AiReviewSchema>(
message.reviews,
payload as unknown as AiReviewSchema,
);
} else if (artifact.type === 'chart') {
message.charts = mergeById<AiChartSchema>(
message.charts,
payload as unknown as AiChartSchema,
);
} else if (artifact.type === 'import_preflight') {
message.metadata = { ...message.metadata, a2uiImportPreflight: payload };
} else if (artifact.type === 'import_wizard') {
message.metadata = { ...message.metadata, a2uiImportWizard: payload };
}
return message;
}
export function reduceAiSseMessage(
originMessage: AiChatMessage | undefined,
chunk?: AiSseChunk,
@@ -198,6 +241,8 @@ export function reduceAiSseMessage(
message.reviews = mergeById<AiReviewSchema>(message.reviews, payload.review);
} else if (event === 'ui.chart' && payload.chart) {
message.charts = mergeById<AiChartSchema>(message.charts, payload.chart);
} else if (event === 'ui.artifact' && payload.artifact) {
mergeArtifactIntoMessage(message, payload.artifact);
} else if (event === 'ui.import_preflight' && payload.preflight) {
message.metadata = { ...message.metadata, a2uiImportPreflight: payload.preflight };
} else if (event === 'ui.import_wizard' && payload.wizard) {
@@ -319,6 +364,7 @@ export class GongxueAiChatProvider extends AbstractChatProvider<
> {
/** Routes events that target another (already streamed) message. */
onExternalReview?: (messageId: number, review: AiReviewSchema) => void;
onExternalArtifact?: (messageId: number, artifact: AiArtifactSchema) => void;
constructor(url: string, onSettled?: (result?: { ok: boolean; aborted?: boolean }) => void) {
super({
@@ -404,6 +450,30 @@ export class GongxueAiChatProvider extends AbstractChatProvider<
transformMessage(info: TransformMessage<AiChatMessage, AiSseChunk>): AiChatMessage {
const { event, payload } = parseSsePayload(info.chunk);
if (
event === 'ui.artifact' &&
payload.artifact &&
typeof payload.messageId === 'number' &&
info.originMessage?.id !== payload.messageId
) {
this.onExternalArtifact?.(payload.messageId, payload.artifact);
return info.originMessage ?? emptyAssistant();
}
if (
event === 'ui.form' &&
payload.form &&
typeof payload.messageId === 'number' &&
info.originMessage?.id !== payload.messageId
) {
this.onExternalArtifact?.(payload.messageId, {
id: payload.form.id,
type: 'form',
status: payload.form.status ?? 'pending',
messageId: payload.messageId,
payload: payload.form,
});
return info.originMessage ?? emptyAssistant();
}
if (
event === 'ui.review' &&
payload.review &&