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:
196
src/server/db/entities.ts
Normal file
196
src/server/db/entities.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
|
||||
import type { CreateEntityRequest, Entity, EntityType, UpdateEntityRequest } from "../../shared/api";
|
||||
import type { Logger } from "../logger";
|
||||
|
||||
import { notDeleted, paginateQuery, softDeleteRecord, timestamp, wrap } from "./connection";
|
||||
import { entities, projects } from "./schema";
|
||||
|
||||
export function createEntity(
|
||||
raw: Database,
|
||||
projectId: string,
|
||||
request: CreateEntityRequest,
|
||||
_logger: Logger,
|
||||
): { entity: Entity } | { error: string; status: number } {
|
||||
const db = wrap(raw);
|
||||
const project = db
|
||||
.select()
|
||||
.from(projects)
|
||||
.where(and(eq(projects.id, projectId), notDeleted(projects)))
|
||||
.get();
|
||||
if (!project) return { error: "项目不存在", status: 404 };
|
||||
if (project.status === "archived") return { error: "已归档项目不可操作", status: 409 };
|
||||
|
||||
const name = request.name.trim();
|
||||
if (!name) return { error: "实体名称不能为空", status: 400 };
|
||||
|
||||
const duplicate = db
|
||||
.select({ id: entities.id })
|
||||
.from(entities)
|
||||
.where(and(eq(entities.projectId, projectId), eq(entities.name, name), notDeleted(entities)))
|
||||
.get();
|
||||
if (duplicate) return { error: "实体名称已存在", status: 409 };
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
const now = timestamp();
|
||||
|
||||
db.insert(entities)
|
||||
.values({
|
||||
aliases: JSON.stringify(request.aliases ?? []),
|
||||
createdAt: now,
|
||||
description: request.description?.trim() ?? "",
|
||||
id,
|
||||
name,
|
||||
projectId,
|
||||
type: request.type,
|
||||
updatedAt: now,
|
||||
})
|
||||
.run();
|
||||
|
||||
const row = db.select().from(entities).where(eq(entities.id, id)).get();
|
||||
return { entity: toEntity(row!) };
|
||||
}
|
||||
|
||||
export function deleteEntity(
|
||||
raw: Database,
|
||||
projectId: string,
|
||||
entityId: string,
|
||||
_logger: Logger,
|
||||
): { error: string; status: number } | { success: true } {
|
||||
const db = wrap(raw);
|
||||
const row = db
|
||||
.select()
|
||||
.from(entities)
|
||||
.where(and(eq(entities.id, entityId), notDeleted(entities)))
|
||||
.get();
|
||||
if (!row) return { error: "实体不存在", status: 404 };
|
||||
if (row.projectId !== projectId) return { error: "实体不属于该项目", status: 403 };
|
||||
|
||||
softDeleteRecord(db, entities, entityId);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
export function getEntity(
|
||||
raw: Database,
|
||||
projectId: string,
|
||||
entityId: string,
|
||||
): { entity: Entity } | { error: string; status: number } {
|
||||
const db = wrap(raw);
|
||||
const row = db
|
||||
.select()
|
||||
.from(entities)
|
||||
.where(and(eq(entities.id, entityId), notDeleted(entities)))
|
||||
.get();
|
||||
if (!row) return { error: "实体不存在", status: 404 };
|
||||
if (row.projectId !== projectId) return { error: "实体不属于该项目", status: 403 };
|
||||
|
||||
return { entity: toEntity(row) };
|
||||
}
|
||||
|
||||
export function listEntities(
|
||||
raw: Database,
|
||||
projectId: string,
|
||||
options: { page: number; pageSize: number; type?: EntityType },
|
||||
): { items: Entity[]; page: number; pageSize: number; total: number } {
|
||||
const conditions = [eq(entities.projectId, projectId)];
|
||||
|
||||
if (options.type) {
|
||||
conditions.push(eq(entities.type, options.type));
|
||||
}
|
||||
|
||||
return paginateQuery(raw, entities, {
|
||||
conditions,
|
||||
mapRow: toEntity,
|
||||
orderBy: () => desc(entities.createdAt),
|
||||
page: options.page,
|
||||
pageSize: options.pageSize,
|
||||
softDelete: entities.deletedAt,
|
||||
});
|
||||
}
|
||||
|
||||
export function listEntityNames(
|
||||
raw: Database,
|
||||
projectId: string,
|
||||
): Array<{ aliases: string[]; id: string; name: string }> {
|
||||
const db = wrap(raw);
|
||||
const rows = db
|
||||
.select({ aliases: entities.aliases, id: entities.id, name: entities.name })
|
||||
.from(entities)
|
||||
.where(and(eq(entities.projectId, projectId), notDeleted(entities)))
|
||||
.all();
|
||||
|
||||
return rows.map((row) => ({
|
||||
aliases: JSON.parse(row.aliases) as string[],
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
}));
|
||||
}
|
||||
|
||||
export function updateEntity(
|
||||
raw: Database,
|
||||
projectId: string,
|
||||
entityId: string,
|
||||
request: UpdateEntityRequest,
|
||||
_logger: Logger,
|
||||
): { entity: Entity } | { error: string; status: number } {
|
||||
const db = wrap(raw);
|
||||
const existing = db
|
||||
.select()
|
||||
.from(entities)
|
||||
.where(and(eq(entities.id, entityId), notDeleted(entities)))
|
||||
.get();
|
||||
if (!existing) return { error: "实体不存在", status: 404 };
|
||||
if (existing.projectId !== projectId) return { error: "实体不属于该项目", status: 403 };
|
||||
|
||||
const updates: Partial<typeof entities.$inferInsert> = {
|
||||
updatedAt: timestamp(),
|
||||
};
|
||||
|
||||
const name = request.name?.trim();
|
||||
if (name === "") return { error: "实体名称不能为空", status: 400 };
|
||||
if (name !== undefined && name !== existing.name) {
|
||||
const duplicate = db
|
||||
.select({ id: entities.id })
|
||||
.from(entities)
|
||||
.where(and(eq(entities.projectId, projectId), eq(entities.name, name), notDeleted(entities)))
|
||||
.get();
|
||||
if (duplicate) return { error: "实体名称已存在", status: 409 };
|
||||
updates.name = name;
|
||||
}
|
||||
|
||||
if (request.type !== undefined) {
|
||||
updates.type = request.type;
|
||||
}
|
||||
|
||||
if (request.description !== undefined) {
|
||||
updates.description = request.description.trim();
|
||||
}
|
||||
|
||||
if (request.aliases !== undefined) {
|
||||
updates.aliases = JSON.stringify(request.aliases);
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length === 1 && updates.updatedAt) {
|
||||
return { entity: toEntity(existing) };
|
||||
}
|
||||
|
||||
db.update(entities).set(updates).where(eq(entities.id, entityId)).run();
|
||||
|
||||
const updated = db.select().from(entities).where(eq(entities.id, entityId)).get();
|
||||
return { entity: toEntity(updated!) };
|
||||
}
|
||||
|
||||
function toEntity(row: typeof entities.$inferSelect): Entity {
|
||||
return {
|
||||
aliases: JSON.parse(row.aliases) as string[],
|
||||
createdAt: row.createdAt,
|
||||
description: row.description,
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
projectId: row.projectId,
|
||||
type: row.type as EntityType,
|
||||
updatedAt: row.updatedAt,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user