fix(ai): 清理 aislop 扫描告警
- 拆分 SSE reducer 到 sseReducer.ts,provider.ts 降至 220 行 - artifact 合并工具抽到 uiArtifacts.ts,去除双重类型断言 - 业务上下文注册表改用 field/relation/entity 构建器收敛重复 - aislop 分数 71 → 81(Healthy),AI Slop 0 告警
This commit is contained in:
@@ -9,7 +9,7 @@ import type {
|
||||
AiReviewSchema,
|
||||
AiToolRun,
|
||||
} from './types';
|
||||
import { mergeArtifactIntoMessage } from './provider';
|
||||
import { mergeArtifactIntoMessage } from './uiArtifacts';
|
||||
|
||||
function mapStatus(record: AiMessageRecord): AiChatMessageStatus {
|
||||
if (record.status === 'pending') return 'loading';
|
||||
|
||||
@@ -6,280 +6,10 @@ import {
|
||||
} from '@ant-design/x-sdk';
|
||||
import { usePermissionStore } from '../../store/permission/permissionStore';
|
||||
import { useUserStore } from '../../store/user/userStore';
|
||||
import type {
|
||||
AiAttachment,
|
||||
AiArtifactSchema,
|
||||
AiChatInput,
|
||||
AiChatMessage,
|
||||
AiChartSchema,
|
||||
AiFormSchema,
|
||||
AiImportPreflight,
|
||||
AiModelRetryInfo,
|
||||
AiReviewSchema,
|
||||
AiSseChunk,
|
||||
AiToolRun,
|
||||
} from './types';
|
||||
import type { AiArtifactSchema, AiChatInput, AiChatMessage, AiReviewSchema, AiSseChunk } from './types';
|
||||
import { emptyAssistant, parseSsePayload, reduceAiSseMessage } from './sseReducer';
|
||||
|
||||
interface AiSsePayload {
|
||||
messageId?: number;
|
||||
userMessageId?: number;
|
||||
assistantMessageId?: number;
|
||||
delta?: string;
|
||||
content?: string;
|
||||
reasoningContent?: string | null;
|
||||
toolCallId?: string;
|
||||
toolName?: string;
|
||||
skillKey?: string | null;
|
||||
status?: string;
|
||||
summary?: string | null;
|
||||
durationMs?: number | null;
|
||||
attachment?: AiAttachment;
|
||||
form?: AiFormSchema;
|
||||
artifact?: AiArtifactSchema;
|
||||
review?: AiReviewSchema;
|
||||
chart?: AiChartSchema;
|
||||
preflight?: AiImportPreflight;
|
||||
wizard?: unknown;
|
||||
retry?: AiModelRetryInfo;
|
||||
message?:
|
||||
| string
|
||||
| {
|
||||
id?: number;
|
||||
content?: string;
|
||||
reasoningContent?: string | null;
|
||||
status?: string;
|
||||
toolRuns?: AiToolRun[];
|
||||
attachments?: AiAttachment[];
|
||||
replyToMessageId?: number | null;
|
||||
metadata?: Record<string, unknown> | null;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
function emptyAssistant(): AiChatMessage {
|
||||
return {
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
reasoningContent: '',
|
||||
toolRuns: [],
|
||||
attachments: [],
|
||||
forms: [],
|
||||
uiArtifacts: [],
|
||||
};
|
||||
}
|
||||
|
||||
function mergeForms(
|
||||
current: AiFormSchema[] | undefined,
|
||||
incoming: AiFormSchema | AiFormSchema[] | undefined,
|
||||
): AiFormSchema[] {
|
||||
const items = Array.isArray(incoming) ? incoming : incoming ? [incoming] : [];
|
||||
if (!items.length) return current ?? [];
|
||||
const next = [...(current ?? [])];
|
||||
for (const item of items) {
|
||||
if (item && typeof item === 'object' && !next.some((existing) => existing.id === item.id)) {
|
||||
next.push(item);
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function mergeById<T extends { id: string }>(
|
||||
current: T[] | undefined,
|
||||
incoming: T | T[] | undefined,
|
||||
): T[] {
|
||||
const items = Array.isArray(incoming) ? incoming : incoming ? [incoming] : [];
|
||||
if (!items.length) return current ?? [];
|
||||
const next = [...(current ?? [])];
|
||||
for (const item of items) {
|
||||
if (!item || typeof item !== 'object') continue;
|
||||
const index = next.findIndex((existing) => existing.id === item.id);
|
||||
if (index === -1) {
|
||||
next.push(item);
|
||||
} else {
|
||||
next[index] = item;
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export function parseSsePayload(chunk?: AiSseChunk): {
|
||||
event: string;
|
||||
payload: AiSsePayload;
|
||||
} {
|
||||
if (!chunk) return { event: '', payload: {} };
|
||||
const event = chunk.event?.trim() || 'message';
|
||||
if (!chunk.data || chunk.data === '[DONE]') return { event, payload: {} };
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(chunk.data);
|
||||
return {
|
||||
event,
|
||||
payload: parsed && typeof parsed === 'object' ? (parsed as AiSsePayload) : {},
|
||||
};
|
||||
} catch {
|
||||
return { event, payload: { delta: chunk.data } };
|
||||
}
|
||||
}
|
||||
|
||||
function upsertToolRun(
|
||||
toolRuns: AiToolRun[],
|
||||
payload: AiSsePayload,
|
||||
fallbackStatus: AiToolRun['status'],
|
||||
): AiToolRun[] {
|
||||
const toolCallId = payload.toolCallId || `${payload.toolName || 'tool'}-${toolRuns.length}`;
|
||||
const next: AiToolRun = {
|
||||
toolCallId,
|
||||
toolName: payload.toolName || '查询工具',
|
||||
skillKey: payload.skillKey,
|
||||
status: (payload.status as AiToolRun['status']) || fallbackStatus,
|
||||
summary: payload.summary,
|
||||
resultSummary: fallbackStatus === 'running' ? undefined : payload.summary,
|
||||
argumentsSummary: fallbackStatus === 'running' ? payload.summary : undefined,
|
||||
durationMs: payload.durationMs,
|
||||
};
|
||||
const index = toolRuns.findIndex((item) => item.toolCallId === toolCallId);
|
||||
if (index === -1) return [...toolRuns, next];
|
||||
return toolRuns.map((item, itemIndex) => (itemIndex === index ? { ...item, ...next } : item));
|
||||
}
|
||||
|
||||
function normalizeToolRuns(toolRuns: AiToolRun[] | undefined, fallback: AiToolRun[]): AiToolRun[] {
|
||||
if (!toolRuns) return fallback;
|
||||
return toolRuns.map((tool) => ({
|
||||
...tool,
|
||||
status: tool.status === 'error' ? 'failed' : tool.status,
|
||||
summary: tool.resultSummary ?? tool.argumentsSummary ?? tool.summary,
|
||||
}));
|
||||
}
|
||||
|
||||
function applyMessagePayload(
|
||||
message: AiChatMessage,
|
||||
nested: AiSsePayload['message'],
|
||||
payload: AiSsePayload,
|
||||
): void {
|
||||
if (typeof nested !== 'object' || nested === null) return;
|
||||
message.forms = mergeForms(
|
||||
message.forms,
|
||||
(nested.metadata?.a2uiForm as AiFormSchema | undefined) ?? payload.form,
|
||||
);
|
||||
message.reviews = mergeById<AiReviewSchema>(
|
||||
message.reviews,
|
||||
(nested.metadata?.a2uiReview as AiReviewSchema | undefined) ?? payload.review,
|
||||
);
|
||||
message.charts = mergeById<AiChartSchema>(
|
||||
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,
|
||||
): AiChatMessage {
|
||||
const message = originMessage ? { ...originMessage } : emptyAssistant();
|
||||
const { event, payload } = parseSsePayload(chunk);
|
||||
|
||||
if (event === 'message.created') {
|
||||
const nested = typeof payload.message === 'object' ? payload.message : undefined;
|
||||
message.id = nested?.id ?? payload.assistantMessageId ?? payload.messageId ?? message.id;
|
||||
message.content = nested?.content ?? message.content;
|
||||
message.reasoningContent = nested?.reasoningContent ?? message.reasoningContent;
|
||||
message.toolRuns = normalizeToolRuns(nested?.toolRuns, message.toolRuns);
|
||||
message.attachments = nested?.attachments ?? message.attachments;
|
||||
applyMessagePayload(message, nested, payload);
|
||||
} else if (event === 'reasoning.delta') {
|
||||
message.retrying = null;
|
||||
message.reasoningContent += payload.delta ?? payload.reasoningContent ?? '';
|
||||
} else if (event === 'content.delta') {
|
||||
message.retrying = null;
|
||||
message.content += payload.delta ?? payload.content ?? '';
|
||||
} else if (event === 'model.retrying' && payload.retry) {
|
||||
message.retrying = payload.retry;
|
||||
} else if (event === 'ui.form' && payload.form) {
|
||||
message.forms = mergeForms(message.forms, payload.form);
|
||||
} else if (event === 'ui.review' && payload.review) {
|
||||
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) {
|
||||
message.metadata = { ...message.metadata, a2uiImportWizard: payload.wizard };
|
||||
} else if (event === 'tool.started') {
|
||||
message.toolRuns = upsertToolRun(message.toolRuns, payload, 'running');
|
||||
} else if (event === 'tool.completed') {
|
||||
message.toolRuns = upsertToolRun(message.toolRuns, payload, 'success');
|
||||
} else if (event === 'tool.failed') {
|
||||
message.toolRuns = upsertToolRun(message.toolRuns, payload, 'failed');
|
||||
} else if (event === 'attachment.processed' && payload.attachment) {
|
||||
if (!message.attachments.some((item) => item.id === payload.attachment?.id)) {
|
||||
message.attachments = [...message.attachments, payload.attachment];
|
||||
}
|
||||
} else if (event === 'message.completed') {
|
||||
const nested = typeof payload.message === 'object' ? payload.message : undefined;
|
||||
message.id = nested?.id ?? payload.messageId ?? message.id;
|
||||
message.content = nested?.content ?? payload.content ?? message.content;
|
||||
message.reasoningContent =
|
||||
nested?.reasoningContent ?? payload.reasoningContent ?? message.reasoningContent;
|
||||
message.toolRuns = normalizeToolRuns(nested?.toolRuns, message.toolRuns);
|
||||
message.attachments = nested?.attachments ?? message.attachments;
|
||||
applyMessagePayload(message, nested, payload);
|
||||
message.retrying = null;
|
||||
} else if (event === 'message.cancelled') {
|
||||
message.id = payload.messageId ?? message.id;
|
||||
message.cancelled = true;
|
||||
message.retrying = null;
|
||||
} else if (event === 'error') {
|
||||
message.retrying = null;
|
||||
message.error =
|
||||
(typeof payload.message === 'string' ? payload.message : undefined) ||
|
||||
payload.error ||
|
||||
'AI 回答生成失败';
|
||||
}
|
||||
return message;
|
||||
}
|
||||
export { parseSsePayload, reduceAiSseMessage };
|
||||
|
||||
export async function authenticatedFetch(
|
||||
input: RequestInfo | URL,
|
||||
|
||||
208
apps/admin/src/components/AiChat/sseReducer.ts
Normal file
208
apps/admin/src/components/AiChat/sseReducer.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import type {
|
||||
AiArtifactSchema,
|
||||
AiAttachment,
|
||||
AiChartSchema,
|
||||
AiChatMessage,
|
||||
AiFormSchema,
|
||||
AiImportPreflight,
|
||||
AiModelRetryInfo,
|
||||
AiReviewSchema,
|
||||
AiSseChunk,
|
||||
AiToolRun,
|
||||
} from './types';
|
||||
import { mergeArtifactIntoMessage, mergeById, mergeForms } from './uiArtifacts';
|
||||
|
||||
export interface AiSsePayload {
|
||||
messageId?: number;
|
||||
userMessageId?: number;
|
||||
assistantMessageId?: number;
|
||||
delta?: string;
|
||||
content?: string;
|
||||
reasoningContent?: string | null;
|
||||
toolCallId?: string;
|
||||
toolName?: string;
|
||||
skillKey?: string | null;
|
||||
status?: string;
|
||||
summary?: string | null;
|
||||
durationMs?: number | null;
|
||||
attachment?: AiAttachment;
|
||||
form?: AiFormSchema;
|
||||
artifact?: AiArtifactSchema;
|
||||
review?: AiReviewSchema;
|
||||
chart?: AiChartSchema;
|
||||
preflight?: AiImportPreflight;
|
||||
wizard?: unknown;
|
||||
retry?: AiModelRetryInfo;
|
||||
message?:
|
||||
| string
|
||||
| {
|
||||
id?: number;
|
||||
content?: string;
|
||||
reasoningContent?: string | null;
|
||||
status?: string;
|
||||
toolRuns?: AiToolRun[];
|
||||
attachments?: AiAttachment[];
|
||||
replyToMessageId?: number | null;
|
||||
metadata?: Record<string, unknown> | null;
|
||||
};
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function emptyAssistant(): AiChatMessage {
|
||||
return {
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
reasoningContent: '',
|
||||
toolRuns: [],
|
||||
attachments: [],
|
||||
forms: [],
|
||||
uiArtifacts: [],
|
||||
};
|
||||
}
|
||||
|
||||
export function parseSsePayload(chunk?: AiSseChunk): {
|
||||
event: string;
|
||||
payload: AiSsePayload;
|
||||
} {
|
||||
if (!chunk) return { event: '', payload: {} };
|
||||
const event = chunk.event?.trim() || 'message';
|
||||
if (!chunk.data || chunk.data === '[DONE]') return { event, payload: {} };
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(chunk.data);
|
||||
return {
|
||||
event,
|
||||
payload: parsed && typeof parsed === 'object' ? (parsed as AiSsePayload) : {},
|
||||
};
|
||||
} catch {
|
||||
return { event, payload: { delta: chunk.data } };
|
||||
}
|
||||
}
|
||||
|
||||
function upsertToolRun(
|
||||
toolRuns: AiToolRun[],
|
||||
payload: AiSsePayload,
|
||||
fallbackStatus: AiToolRun['status'],
|
||||
): AiToolRun[] {
|
||||
const toolCallId = payload.toolCallId || `${payload.toolName || 'tool'}-${toolRuns.length}`;
|
||||
const next: AiToolRun = {
|
||||
toolCallId,
|
||||
toolName: payload.toolName || '查询工具',
|
||||
skillKey: payload.skillKey,
|
||||
status: (payload.status as AiToolRun['status']) || fallbackStatus,
|
||||
summary: payload.summary,
|
||||
resultSummary: fallbackStatus === 'running' ? undefined : payload.summary,
|
||||
argumentsSummary: fallbackStatus === 'running' ? payload.summary : undefined,
|
||||
durationMs: payload.durationMs,
|
||||
};
|
||||
const index = toolRuns.findIndex((item) => item.toolCallId === toolCallId);
|
||||
if (index === -1) return [...toolRuns, next];
|
||||
return toolRuns.map((item, itemIndex) => (itemIndex === index ? { ...item, ...next } : item));
|
||||
}
|
||||
|
||||
function normalizeToolRuns(toolRuns: AiToolRun[] | undefined, fallback: AiToolRun[]): AiToolRun[] {
|
||||
if (!toolRuns) return fallback;
|
||||
return toolRuns.map((tool) => ({
|
||||
...tool,
|
||||
status: tool.status === 'error' ? 'failed' : tool.status,
|
||||
summary: tool.resultSummary ?? tool.argumentsSummary ?? tool.summary,
|
||||
}));
|
||||
}
|
||||
|
||||
function applyMessagePayload(
|
||||
message: AiChatMessage,
|
||||
nested: AiSsePayload['message'],
|
||||
payload: AiSsePayload,
|
||||
): void {
|
||||
if (typeof nested !== 'object' || nested === null) return;
|
||||
message.forms = mergeForms(
|
||||
message.forms,
|
||||
(nested.metadata?.a2uiForm as AiFormSchema | undefined) ?? payload.form,
|
||||
);
|
||||
message.reviews = mergeById<AiReviewSchema>(
|
||||
message.reviews,
|
||||
(nested.metadata?.a2uiReview as AiReviewSchema | undefined) ?? payload.review,
|
||||
);
|
||||
message.charts = mergeById<AiChartSchema>(
|
||||
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;
|
||||
}
|
||||
|
||||
export function reduceAiSseMessage(
|
||||
originMessage: AiChatMessage | undefined,
|
||||
chunk?: AiSseChunk,
|
||||
): AiChatMessage {
|
||||
const message = originMessage ? { ...originMessage } : emptyAssistant();
|
||||
const { event, payload } = parseSsePayload(chunk);
|
||||
|
||||
if (event === 'message.created') {
|
||||
const nested = typeof payload.message === 'object' ? payload.message : undefined;
|
||||
message.id = nested?.id ?? payload.assistantMessageId ?? payload.messageId ?? message.id;
|
||||
message.content = nested?.content ?? message.content;
|
||||
message.reasoningContent = nested?.reasoningContent ?? message.reasoningContent;
|
||||
message.toolRuns = normalizeToolRuns(nested?.toolRuns, message.toolRuns);
|
||||
message.attachments = nested?.attachments ?? message.attachments;
|
||||
applyMessagePayload(message, nested, payload);
|
||||
} else if (event === 'reasoning.delta') {
|
||||
message.retrying = null;
|
||||
message.reasoningContent += payload.delta ?? payload.reasoningContent ?? '';
|
||||
} else if (event === 'content.delta') {
|
||||
message.retrying = null;
|
||||
message.content += payload.delta ?? payload.content ?? '';
|
||||
} else if (event === 'model.retrying' && payload.retry) {
|
||||
message.retrying = payload.retry;
|
||||
} else if (event === 'ui.form' && payload.form) {
|
||||
message.forms = mergeForms(message.forms, payload.form);
|
||||
} else if (event === 'ui.review' && payload.review) {
|
||||
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) {
|
||||
message.metadata = { ...message.metadata, a2uiImportWizard: payload.wizard };
|
||||
} else if (event === 'tool.started') {
|
||||
message.toolRuns = upsertToolRun(message.toolRuns, payload, 'running');
|
||||
} else if (event === 'tool.completed') {
|
||||
message.toolRuns = upsertToolRun(message.toolRuns, payload, 'success');
|
||||
} else if (event === 'tool.failed') {
|
||||
message.toolRuns = upsertToolRun(message.toolRuns, payload, 'failed');
|
||||
} else if (event === 'attachment.processed' && payload.attachment) {
|
||||
if (!message.attachments.some((item) => item.id === payload.attachment?.id)) {
|
||||
message.attachments = [...message.attachments, payload.attachment];
|
||||
}
|
||||
} else if (event === 'message.completed') {
|
||||
const nested = typeof payload.message === 'object' ? payload.message : undefined;
|
||||
message.id = nested?.id ?? payload.messageId ?? message.id;
|
||||
message.content = nested?.content ?? payload.content ?? message.content;
|
||||
message.reasoningContent =
|
||||
nested?.reasoningContent ?? payload.reasoningContent ?? message.reasoningContent;
|
||||
message.toolRuns = normalizeToolRuns(nested?.toolRuns, message.toolRuns);
|
||||
message.attachments = nested?.attachments ?? message.attachments;
|
||||
applyMessagePayload(message, nested, payload);
|
||||
message.retrying = null;
|
||||
} else if (event === 'message.cancelled') {
|
||||
message.id = payload.messageId ?? message.id;
|
||||
message.cancelled = true;
|
||||
message.retrying = null;
|
||||
} else if (event === 'error') {
|
||||
message.retrying = null;
|
||||
message.error =
|
||||
(typeof payload.message === 'string' ? payload.message : undefined) ||
|
||||
payload.error ||
|
||||
'AI 回答生成失败';
|
||||
}
|
||||
return message;
|
||||
}
|
||||
69
apps/admin/src/components/AiChat/uiArtifacts.ts
Normal file
69
apps/admin/src/components/AiChat/uiArtifacts.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type {
|
||||
AiArtifactSchema,
|
||||
AiChartSchema,
|
||||
AiChatMessage,
|
||||
AiFormSchema,
|
||||
AiReviewSchema,
|
||||
} from './types';
|
||||
|
||||
export function mergeById<T extends { id: string }>(
|
||||
current: T[] | undefined,
|
||||
incoming: T | T[] | undefined,
|
||||
): T[] {
|
||||
const items = Array.isArray(incoming) ? incoming : incoming ? [incoming] : [];
|
||||
if (!items.length) return current ?? [];
|
||||
const next = [...(current ?? [])];
|
||||
for (const item of items) {
|
||||
if (!item || typeof item !== 'object') continue;
|
||||
const index = next.findIndex((existing) => existing.id === item.id);
|
||||
if (index === -1) {
|
||||
next.push(item);
|
||||
} else {
|
||||
next[index] = item;
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export function mergeForms(
|
||||
current: AiFormSchema[] | undefined,
|
||||
incoming: AiFormSchema | AiFormSchema[] | undefined,
|
||||
): AiFormSchema[] {
|
||||
const items = Array.isArray(incoming) ? incoming : incoming ? [incoming] : [];
|
||||
if (!items.length) return current ?? [];
|
||||
const next = [...(current ?? [])];
|
||||
for (const item of items) {
|
||||
if (item && typeof item === 'object' && !next.some((existing) => existing.id === item.id)) {
|
||||
next.push(item);
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function payloadOf(artifact: AiArtifactSchema): unknown {
|
||||
return artifact.payload && typeof artifact.payload === 'object' ? artifact.payload : {};
|
||||
}
|
||||
|
||||
/**
|
||||
* 将统一 artifact 归入 uiArtifacts,并按类型派发到 legacy 列表。
|
||||
* payload 来自服务端契约(表单/审阅/图表/预检/向导),按类型做单次断言。
|
||||
*/
|
||||
export function mergeArtifactIntoMessage(
|
||||
message: AiChatMessage,
|
||||
artifact: AiArtifactSchema,
|
||||
): AiChatMessage {
|
||||
message.uiArtifacts = mergeById<AiArtifactSchema>(message.uiArtifacts, artifact);
|
||||
const payload = payloadOf(artifact);
|
||||
if (artifact.type === 'form') {
|
||||
message.forms = mergeForms(message.forms, payload as AiFormSchema);
|
||||
} else if (artifact.type === 'review') {
|
||||
message.reviews = mergeById<AiReviewSchema>(message.reviews, payload as AiReviewSchema);
|
||||
} else if (artifact.type === 'chart') {
|
||||
message.charts = mergeById<AiChartSchema>(message.charts, payload 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;
|
||||
}
|
||||
@@ -9,7 +9,8 @@ import { useSettingsStore } from '../../store/settings/settingsStore';
|
||||
import { aiChatApi, resolveImportPreflight, type ResolveImportPreflightInput } from './api';
|
||||
import { AiMessageContent } from './AiMessageContent';
|
||||
import { mapHistoryMessage } from './message-mappers';
|
||||
import { GongxueAiChatProvider, mergeArtifactIntoMessage } from './provider';
|
||||
import { GongxueAiChatProvider } from './provider';
|
||||
import { mergeArtifactIntoMessage } from './uiArtifacts';
|
||||
import {
|
||||
emptyAssistant,
|
||||
MessageHoverActions,
|
||||
|
||||
@@ -1,8 +1,49 @@
|
||||
import type {
|
||||
BusinessEntity,
|
||||
BusinessEntityField,
|
||||
BusinessEntityRelation,
|
||||
BusinessWorkflow,
|
||||
} from './business-context.types';
|
||||
|
||||
function field(
|
||||
key: string,
|
||||
label: string,
|
||||
type: BusinessEntityField['type'],
|
||||
extra: Partial<Omit<BusinessEntityField, 'key' | 'label' | 'type'>> = {},
|
||||
): BusinessEntityField {
|
||||
return { key, label, type, ...extra };
|
||||
}
|
||||
|
||||
function relation(
|
||||
entityKey: string,
|
||||
via: string,
|
||||
requiredFor: readonly string[],
|
||||
): BusinessEntityRelation {
|
||||
return { entityKey, via, requiredFor };
|
||||
}
|
||||
|
||||
function entity(
|
||||
key: string,
|
||||
name: string,
|
||||
description: string,
|
||||
options: {
|
||||
searchTool?: string;
|
||||
requiredPermissions: readonly string[];
|
||||
fields: readonly BusinessEntityField[];
|
||||
relations?: readonly BusinessEntityRelation[];
|
||||
},
|
||||
): BusinessEntity {
|
||||
return {
|
||||
key,
|
||||
name,
|
||||
description,
|
||||
...(options.searchTool ? { searchTool: options.searchTool } : {}),
|
||||
requiredPermissions: options.requiredPermissions,
|
||||
fields: options.fields,
|
||||
relations: options.relations ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 恭学系统业务上下文(代码内维护)。
|
||||
*
|
||||
@@ -11,232 +52,191 @@ import type {
|
||||
* Agent 感知业务流程。
|
||||
*/
|
||||
export const BUSINESS_ENTITIES: readonly BusinessEntity[] = [
|
||||
{
|
||||
key: 'student',
|
||||
name: '学生档案',
|
||||
description: '学生基础档案(姓名、学号、手机号、性别等),是分班、入住、账单的前置数据。',
|
||||
entity('student', '学生档案', '学生基础档案(姓名、学号、手机号、性别等),是分班、入住、账单的前置数据。', {
|
||||
searchTool: 'search_students',
|
||||
requiredPermissions: ['student:view'],
|
||||
fields: [
|
||||
{ key: 'name', label: '姓名', type: 'string', required: true },
|
||||
{ key: 'studentNo', label: '学号', type: 'string' },
|
||||
{ key: 'phone', label: '手机号', type: 'string' },
|
||||
{ key: 'gender', label: '性别', type: 'enum', enumFrom: 'student.gender' },
|
||||
{ key: 'idNumber', label: '身份证号', type: 'string' },
|
||||
field('name', '姓名', 'string', { required: true }),
|
||||
field('studentNo', '学号', 'string'),
|
||||
field('phone', '手机号', 'string'),
|
||||
field('gender', '性别', 'enum', { enumFrom: 'student.gender' }),
|
||||
field('idNumber', '身份证号', 'string'),
|
||||
],
|
||||
relations: [
|
||||
{ entityKey: 'class', via: 'class_student', requiredFor: ['class'] },
|
||||
{ entityKey: 'occupancy', via: 'occupancy', requiredFor: ['checkin'] },
|
||||
{ entityKey: 'bill', via: 'bill', requiredFor: ['bill'] },
|
||||
{ entityKey: 'exam', via: 'exam_score', requiredFor: ['exam'] },
|
||||
relation('class', 'class_student', ['class']),
|
||||
relation('occupancy', 'occupancy', ['checkin']),
|
||||
relation('bill', 'bill', ['bill']),
|
||||
relation('exam', 'exam_score', ['exam']),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'class',
|
||||
name: '班级',
|
||||
description: '班级档案与在读分班关系,排课、考勤、考试依赖班级。',
|
||||
}),
|
||||
entity('class', '班级', '班级档案与在读分班关系,排课、考勤、考试依赖班级。', {
|
||||
searchTool: 'search_classes',
|
||||
requiredPermissions: ['class:view'],
|
||||
fields: [
|
||||
{ key: 'name', label: '班级名称', type: 'string', required: true },
|
||||
{ key: 'grade', label: '年级', type: 'string' },
|
||||
{ key: 'headTeacher', label: '班主任', type: 'string' },
|
||||
field('name', '班级名称', 'string', { required: true }),
|
||||
field('grade', '年级', 'string'),
|
||||
field('headTeacher', '班主任', 'string'),
|
||||
],
|
||||
relations: [
|
||||
{ entityKey: 'student', via: 'class_student', requiredFor: ['class'] },
|
||||
{ entityKey: 'schedule', via: 'class_schedule', requiredFor: ['schedule'] },
|
||||
relation('student', 'class_student', ['class']),
|
||||
relation('schedule', 'class_schedule', ['schedule']),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'schedule',
|
||||
name: '排课/日程',
|
||||
description: '班级与教室的课程安排,考勤和教室日程依赖排课。',
|
||||
}),
|
||||
entity('schedule', '排课/日程', '班级与教室的课程安排,考勤和教室日程依赖排课。', {
|
||||
searchTool: 'search_schedules',
|
||||
requiredPermissions: ['schedule:view'],
|
||||
fields: [
|
||||
{ key: 'classId', label: '班级', type: 'number' },
|
||||
{ key: 'classroomId', label: '教室', type: 'number' },
|
||||
{ key: 'weekDay', label: '星期', type: 'number' },
|
||||
{ key: 'startTime', label: '开始时间', type: 'string' },
|
||||
{ key: 'endTime', label: '结束时间', type: 'string' },
|
||||
field('classId', '班级', 'number'),
|
||||
field('classroomId', '教室', 'number'),
|
||||
field('weekDay', '星期', 'number'),
|
||||
field('startTime', '开始时间', 'string'),
|
||||
field('endTime', '结束时间', 'string'),
|
||||
],
|
||||
relations: [
|
||||
{ entityKey: 'class', via: 'class_schedule', requiredFor: ['schedule'] },
|
||||
{ entityKey: 'classroom', via: 'class_schedule', requiredFor: ['schedule'] },
|
||||
relation('class', 'class_schedule', ['schedule']),
|
||||
relation('classroom', 'class_schedule', ['schedule']),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'attendance',
|
||||
name: '考勤',
|
||||
description: '按班级与日期的考勤记录(出勤/迟到/缺勤/请假)。',
|
||||
}),
|
||||
entity('attendance', '考勤', '按班级与日期的考勤记录(出勤/迟到/缺勤/请假)。', {
|
||||
searchTool: 'get_attendance_summary',
|
||||
requiredPermissions: ['attendance:view'],
|
||||
fields: [
|
||||
{ key: 'studentId', label: '学生', type: 'number', required: true },
|
||||
{ key: 'date', label: '日期', type: 'date', required: true },
|
||||
{ key: 'status', label: '状态', type: 'enum', enumFrom: 'attendance.status' },
|
||||
field('studentId', '学生', 'number', { required: true }),
|
||||
field('date', '日期', 'date', { required: true }),
|
||||
field('status', '状态', 'enum', { enumFrom: 'attendance.status' }),
|
||||
],
|
||||
relations: [
|
||||
{ entityKey: 'class', via: 'class_schedule', requiredFor: ['attendance'] },
|
||||
{ entityKey: 'schedule', via: 'class_schedule', requiredFor: ['attendance'] },
|
||||
relation('class', 'class_schedule', ['attendance']),
|
||||
relation('schedule', 'class_schedule', ['attendance']),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'exam',
|
||||
name: '考试/成绩',
|
||||
description: '考试安排与成绩记录,依赖班级与学生档案。',
|
||||
}),
|
||||
entity('exam', '考试/成绩', '考试安排与成绩记录,依赖班级与学生档案。', {
|
||||
searchTool: 'search_exams',
|
||||
requiredPermissions: ['exam:view'],
|
||||
fields: [
|
||||
{ key: 'name', label: '考试名称', type: 'string', required: true },
|
||||
{ key: 'date', label: '考试日期', type: 'date' },
|
||||
{ key: 'subject', label: '科目', type: 'string' },
|
||||
field('name', '考试名称', 'string', { required: true }),
|
||||
field('date', '考试日期', 'date'),
|
||||
field('subject', '科目', 'string'),
|
||||
],
|
||||
relations: [
|
||||
{ entityKey: 'class', via: 'exam_score', requiredFor: ['exam'] },
|
||||
{ entityKey: 'student', via: 'exam_score', requiredFor: ['exam'] },
|
||||
relation('class', 'exam_score', ['exam']),
|
||||
relation('student', 'exam_score', ['exam']),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'room',
|
||||
name: '宿舍档案',
|
||||
description: '宿舍/床位基础档案,入住登记的前置数据。',
|
||||
}),
|
||||
entity('room', '宿舍档案', '宿舍/床位基础档案,入住登记的前置数据。', {
|
||||
searchTool: 'search_rooms',
|
||||
requiredPermissions: ['room:view'],
|
||||
fields: [
|
||||
{ key: 'roomNumber', label: '宿舍号', type: 'string', required: true },
|
||||
{ key: 'building', label: '楼栋', type: 'string' },
|
||||
{ key: 'floor', label: '楼层', type: 'number' },
|
||||
{ key: 'capacity', label: '容量', type: 'number', required: true },
|
||||
{ key: 'roomType', label: '房型', type: 'string' },
|
||||
{ key: 'monthlyRate', label: '月租金', type: 'number' },
|
||||
field('roomNumber', '宿舍号', 'string', { required: true }),
|
||||
field('building', '楼栋', 'string'),
|
||||
field('floor', '楼层', 'number'),
|
||||
field('capacity', '容量', 'number', { required: true }),
|
||||
field('roomType', '房型', 'string'),
|
||||
field('monthlyRate', '月租金', 'number'),
|
||||
],
|
||||
relations: [
|
||||
{ entityKey: 'occupancy', via: 'occupancy', requiredFor: ['checkin'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'occupancy',
|
||||
name: '入住记录',
|
||||
description: '学生入住/换宿/退宿记录,费用与账单依赖入住状态。',
|
||||
relations: [relation('occupancy', 'occupancy', ['checkin'])],
|
||||
}),
|
||||
entity('occupancy', '入住记录', '学生入住/换宿/退宿记录,费用与账单依赖入住状态。', {
|
||||
searchTool: 'get_room_occupancy_summary',
|
||||
requiredPermissions: ['occupancy:view'],
|
||||
fields: [
|
||||
{ key: 'studentId', label: '学生', type: 'number', required: true },
|
||||
{ key: 'roomId', label: '宿舍', type: 'number', required: true },
|
||||
{ key: 'checkInDate', label: '入住日期', type: 'date', required: true },
|
||||
{ key: 'billingStartDate', label: '计费开始日期', type: 'date', required: true },
|
||||
{ key: 'stayType', label: '住宿类型', type: 'enum', enumFrom: 'occupancy.stayType' },
|
||||
field('studentId', '学生', 'number', { required: true }),
|
||||
field('roomId', '宿舍', 'number', { required: true }),
|
||||
field('checkInDate', '入住日期', 'date', { required: true }),
|
||||
field('billingStartDate', '计费开始日期', 'date', { required: true }),
|
||||
field('stayType', '住宿类型', 'enum', { enumFrom: 'occupancy.stayType' }),
|
||||
],
|
||||
relations: [
|
||||
{ entityKey: 'student', via: 'occupancy', requiredFor: ['checkin'] },
|
||||
{ entityKey: 'room', via: 'occupancy', requiredFor: ['checkin'] },
|
||||
{ entityKey: 'bill', via: 'bill', requiredFor: ['bill'] },
|
||||
relation('student', 'occupancy', ['checkin']),
|
||||
relation('room', 'occupancy', ['checkin']),
|
||||
relation('bill', 'bill', ['bill']),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'expense',
|
||||
name: '费用',
|
||||
description: '公共费用与个人费用,是生成账单的基础。',
|
||||
}),
|
||||
entity('expense', '费用', '公共费用与个人费用,是生成账单的基础。', {
|
||||
searchTool: 'search_expenses',
|
||||
requiredPermissions: ['expense:view'],
|
||||
fields: [
|
||||
{ key: 'type', label: '费用类型', type: 'string', required: true },
|
||||
{ key: 'amount', label: '金额', type: 'number', required: true },
|
||||
{ key: 'periodStart', label: '费用开始日期', type: 'date' },
|
||||
{ key: 'periodEnd', label: '费用结束日期', type: 'date' },
|
||||
field('type', '费用类型', 'string', { required: true }),
|
||||
field('amount', '金额', 'number', { required: true }),
|
||||
field('periodStart', '费用开始日期', 'date'),
|
||||
field('periodEnd', '费用结束日期', 'date'),
|
||||
],
|
||||
relations: [
|
||||
{ entityKey: 'occupancy', via: 'expense', requiredFor: ['expense'] },
|
||||
{ entityKey: 'bill', via: 'bill_item', requiredFor: ['bill'] },
|
||||
relation('occupancy', 'expense', ['expense']),
|
||||
relation('bill', 'bill_item', ['bill']),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'bill',
|
||||
name: '账单',
|
||||
description: '按学生与账期生成的账单(公共+个人费用分摊),支持确认与付款。',
|
||||
}),
|
||||
entity('bill', '账单', '按学生与账期生成的账单(公共+个人费用分摊),支持确认与付款。', {
|
||||
searchTool: 'search_bills',
|
||||
requiredPermissions: ['bill:view'],
|
||||
fields: [
|
||||
{ key: 'studentId', label: '学生', type: 'number', required: true },
|
||||
{ key: 'periodStart', label: '账期开始', type: 'date', required: true },
|
||||
{ key: 'periodEnd', label: '账期结束', type: 'date', required: true },
|
||||
{ key: 'totalAmount', label: '总金额', type: 'number', required: true },
|
||||
{ key: 'status', label: '状态', type: 'enum', enumFrom: 'bill.status' },
|
||||
field('studentId', '学生', 'number', { required: true }),
|
||||
field('periodStart', '账期开始', 'date', { required: true }),
|
||||
field('periodEnd', '账期结束', 'date', { required: true }),
|
||||
field('totalAmount', '总金额', 'number', { required: true }),
|
||||
field('status', '状态', 'enum', { enumFrom: 'bill.status' }),
|
||||
],
|
||||
relations: [
|
||||
{ entityKey: 'student', via: 'bill', requiredFor: ['bill'] },
|
||||
{ entityKey: 'occupancy', via: 'bill', requiredFor: ['bill'] },
|
||||
{ entityKey: 'deposit', via: 'deposit', requiredFor: ['deposit'] },
|
||||
relation('student', 'bill', ['bill']),
|
||||
relation('occupancy', 'bill', ['bill']),
|
||||
relation('deposit', 'deposit', ['deposit']),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'deposit',
|
||||
name: '押金',
|
||||
description: '押金收取与退还记录,通常在账单确认后处理。',
|
||||
}),
|
||||
entity('deposit', '押金', '押金收取与退还记录,通常在账单确认后处理。', {
|
||||
searchTool: 'search_deposits',
|
||||
requiredPermissions: ['deposit:view'],
|
||||
fields: [
|
||||
{ key: 'studentId', label: '学生', type: 'number', required: true },
|
||||
{ key: 'amount', label: '金额', type: 'number', required: true },
|
||||
{ key: 'status', label: '状态', type: 'enum', enumFrom: 'deposit.status' },
|
||||
field('studentId', '学生', 'number', { required: true }),
|
||||
field('amount', '金额', 'number', { required: true }),
|
||||
field('status', '状态', 'enum', { enumFrom: 'deposit.status' }),
|
||||
],
|
||||
relations: [
|
||||
{ entityKey: 'student', via: 'deposit', requiredFor: ['deposit'] },
|
||||
{ entityKey: 'bill', via: 'deposit', requiredFor: ['deposit'] },
|
||||
relation('student', 'deposit', ['deposit']),
|
||||
relation('bill', 'deposit', ['deposit']),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'classroom',
|
||||
name: '教室档案',
|
||||
description: '教室基础档案,租赁与教室日程的前置数据。',
|
||||
}),
|
||||
entity('classroom', '教室档案', '教室基础档案,租赁与教室日程的前置数据。', {
|
||||
searchTool: 'search_classrooms',
|
||||
requiredPermissions: ['classroom:view'],
|
||||
fields: [
|
||||
{ key: 'name', label: '教室名称', type: 'string', required: true },
|
||||
{ key: 'building', label: '楼栋', type: 'string' },
|
||||
{ key: 'capacity', label: '容量', type: 'number' },
|
||||
field('name', '教室名称', 'string', { required: true }),
|
||||
field('building', '楼栋', 'string'),
|
||||
field('capacity', '容量', 'number'),
|
||||
],
|
||||
relations: [
|
||||
{ entityKey: 'rental', via: 'classroom_rental', requiredFor: ['rental'] },
|
||||
{ entityKey: 'schedule', via: 'class_schedule', requiredFor: ['schedule'] },
|
||||
relation('rental', 'classroom_rental', ['rental']),
|
||||
relation('schedule', 'class_schedule', ['schedule']),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'organization',
|
||||
name: '组织/校区',
|
||||
description: '校区与组织归属,租赁双方与档案归属依赖组织。',
|
||||
}),
|
||||
entity('organization', '组织/校区', '校区与组织归属,租赁双方与档案归属依赖组织。', {
|
||||
requiredPermissions: ['organization:view'],
|
||||
fields: [
|
||||
{ key: 'name', label: '名称', type: 'string', required: true },
|
||||
{ key: 'code', label: '编码', type: 'string' },
|
||||
{ key: 'isHost', label: '是否本部', type: 'boolean' },
|
||||
field('name', '名称', 'string', { required: true }),
|
||||
field('code', '编码', 'string'),
|
||||
field('isHost', '是否本部', 'boolean'),
|
||||
],
|
||||
relations: [
|
||||
{ entityKey: 'student', via: 'organization', requiredFor: ['profile'] },
|
||||
{ entityKey: 'rental', via: 'classroom_rental', requiredFor: ['rental'] },
|
||||
relation('student', 'organization', ['profile']),
|
||||
relation('rental', 'classroom_rental', ['rental']),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'rental',
|
||||
name: '教室租赁',
|
||||
description: '教室租赁订单与合同(合同字段在租赁记录上),依赖教室与组织。',
|
||||
}),
|
||||
entity('rental', '教室租赁', '教室租赁订单与合同(合同字段在租赁记录上),依赖教室与组织。', {
|
||||
searchTool: 'search_classroom_rentals',
|
||||
requiredPermissions: ['rental:view'],
|
||||
fields: [
|
||||
{ key: 'classroomId', label: '教室', type: 'number', required: true },
|
||||
{ key: 'lesseeOrganizationId', label: '承租方', type: 'number' },
|
||||
{ key: 'startDate', label: '开始日期', type: 'date', required: true },
|
||||
{ key: 'endDate', label: '结束日期', type: 'date', required: true },
|
||||
{ key: 'dailyRate', label: '日租金', type: 'number' },
|
||||
{ key: 'contractPath', label: '合同文件', type: 'string' },
|
||||
field('classroomId', '教室', 'number', { required: true }),
|
||||
field('lesseeOrganizationId', '承租方', 'number'),
|
||||
field('startDate', '开始日期', 'date', { required: true }),
|
||||
field('endDate', '结束日期', 'date', { required: true }),
|
||||
field('dailyRate', '日租金', 'number'),
|
||||
field('contractPath', '合同文件', 'string'),
|
||||
],
|
||||
relations: [
|
||||
{ entityKey: 'classroom', via: 'classroom_rental', requiredFor: ['rental'] },
|
||||
{ entityKey: 'organization', via: 'classroom_rental', requiredFor: ['rental'] },
|
||||
{ entityKey: 'schedule', via: 'class_schedule', requiredFor: ['schedule'] },
|
||||
relation('classroom', 'classroom_rental', ['rental']),
|
||||
relation('organization', 'classroom_rental', ['rental']),
|
||||
relation('schedule', 'class_schedule', ['schedule']),
|
||||
],
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
export const BUSINESS_WORKFLOWS: readonly BusinessWorkflow[] = [
|
||||
|
||||
@@ -21,7 +21,7 @@ export interface BusinessEntityRelation {
|
||||
entityKey: string;
|
||||
via: string;
|
||||
/** 需要该关系已建立的工作流阶段 key。 */
|
||||
requiredFor: string[];
|
||||
requiredFor: readonly string[];
|
||||
}
|
||||
|
||||
export interface BusinessEntity {
|
||||
|
||||
Reference in New Issue
Block a user