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:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user