feat: 实现阶段二实体体系——AI预处理真实化+实体CRUD+审核归一化

- 新增 entities 数据表(含迁移)、Entity 类型、DAO 层完整 CRUD
- AI 预处理管道接入真实模型(generateText),输出结构化 JSON(摘要+规范化内容+候选实体)
- 模板接口重构为 {systemPrompt, buildUserPrompt, parseOutput},general/meeting 模板真实化
- 新增 5 个实体路由端点 + 实体管理前端页面(列表/详情/编辑弹窗)
- 审核面板增强:展示 AI 预处理结构化结果+候选实体归一化面板(合并/新建/选择/放弃)
- 素材通过时根据用户确认的候选实体写入 entities 表
- 工作台菜单新增"实体"入口
- 新增 entities DAO 测试(16)、processor 测试(11)、路由测试(8),服务端 367 测试全部通过
- TypeScript 0 错误
This commit is contained in:
2026-06-08 18:49:30 +08:00
parent 034496e946
commit 12edf0b545
36 changed files with 2109 additions and 62 deletions

View File

@@ -1,12 +1,16 @@
import { Card, Descriptions, Flex, Tag, Typography } from "antd";
import type { EntityConfirmation, ProcessingResult } from "../../../../shared/api";
import type { Material, MaterialType } from "../types";
import { formatRelativeTime } from "../../../shared/utils/time";
import { STATUS_MAP } from "./constants";
import { EntityCandidatePanel } from "./EntityCandidatePanel";
interface MaterialContentProps {
material: Material;
projectId?: string;
onConfirmationsChange?: (confirmations: EntityConfirmation[]) => void;
}
const MATERIAL_TYPE_LABELS: Record<MaterialType, string> = {
@@ -14,36 +18,97 @@ const MATERIAL_TYPE_LABELS: Record<MaterialType, string> = {
meeting: "会议",
};
export function MaterialContent({ material }: MaterialContentProps) {
function parseProcessingResult(raw: null | string): ProcessingResult | null {
if (!raw) return null;
try {
const parsed = JSON.parse(raw) as Partial<ProcessingResult>;
if (parsed && typeof parsed === "object") {
return {
candidateEntities: Array.isArray(parsed.candidateEntities) ? parsed.candidateEntities : [],
normalizedContent: typeof parsed.normalizedContent === "string" ? parsed.normalizedContent : "",
summary: typeof parsed.summary === "string" ? parsed.summary : "",
};
}
return null;
} catch {
return null;
}
}
export function MaterialContent({ material, projectId, onConfirmationsChange }: MaterialContentProps) {
const statusInfo = STATUS_MAP[material.status] ?? { color: "default", label: material.status };
const typeLabel = MATERIAL_TYPE_LABELS[material.materialType] ?? material.materialType;
const processingResult = parseProcessingResult(material.processedContent);
return (
<Flex gap={12} vertical>
<Card size="small" title="基本信息">
<Flex gap={12} vertical>
<Typography.Paragraph>{material.description}</Typography.Paragraph>
{material.processedContent && (
<Card size="small" title="原始文本">
<Typography.Paragraph>{material.description}</Typography.Paragraph>
</Card>
{processingResult && (
<>
<Card size="small" title="AI 摘要">
<Typography.Paragraph>{processingResult.summary}</Typography.Paragraph>
</Card>
<Card size="small" title="规范化内容">
<Typography.Paragraph
style={{
background: "var(--ant-color-fill-quaternary)",
padding: 12,
borderRadius: 6,
padding: 12,
whiteSpace: "pre-wrap",
}}
>
{material.processedContent}
{processingResult.normalizedContent}
</Typography.Paragraph>
</Card>
{material.status === "review" && projectId && onConfirmationsChange && (
<EntityCandidatePanel
candidates={processingResult.candidateEntities}
projectId={projectId}
onConfirmationsChange={onConfirmationsChange}
/>
)}
<Descriptions column={1} size="small">
<Descriptions.Item label="状态">
<Tag color={statusInfo.color}>{statusInfo.label}</Tag>
</Descriptions.Item>
<Descriptions.Item label="素材类型">{typeLabel}</Descriptions.Item>
<Descriptions.Item label="关联时间">{material.associatedDate}</Descriptions.Item>
<Descriptions.Item label="创建时间">{formatRelativeTime(material.createdAt)}</Descriptions.Item>
</Descriptions>
</Flex>
{material.status !== "review" && processingResult.candidateEntities.length > 0 && (
<Card size="small" title="候选实体(已确认)">
<Flex gap={4} wrap="wrap">
{processingResult.candidateEntities.map((ce: { name: string }, i: number) => (
<Tag key={i}>{ce.name}</Tag>
))}
</Flex>
</Card>
)}
</>
)}
{!processingResult && material.processedContent && (
<Card size="small" title="处理结果">
<Typography.Paragraph
style={{
background: "var(--ant-color-fill-quaternary)",
borderRadius: 6,
padding: 12,
whiteSpace: "pre-wrap",
}}
>
{material.processedContent}
</Typography.Paragraph>
</Card>
)}
<Card size="small" title="基本信息">
<Descriptions column={1} size="small">
<Descriptions.Item label="状态">
<Tag color={statusInfo.color}>{statusInfo.label}</Tag>
</Descriptions.Item>
<Descriptions.Item label="素材类型">{typeLabel}</Descriptions.Item>
<Descriptions.Item label="关联时间">{material.associatedDate}</Descriptions.Item>
<Descriptions.Item label="创建时间">{formatRelativeTime(material.createdAt)}</Descriptions.Item>
</Descriptions>
</Card>
</Flex>
);