- 新增 entities 数据表(含迁移)、Entity 类型、DAO 层完整 CRUD
- AI 预处理管道接入真实模型(generateText),输出结构化 JSON(摘要+规范化内容+候选实体)
- 模板接口重构为 {systemPrompt, buildUserPrompt, parseOutput},general/meeting 模板真实化
- 新增 5 个实体路由端点 + 实体管理前端页面(列表/详情/编辑弹窗)
- 审核面板增强:展示 AI 预处理结构化结果+候选实体归一化面板(合并/新建/选择/放弃)
- 素材通过时根据用户确认的候选实体写入 entities 表
- 工作台菜单新增"实体"入口
- 新增 entities DAO 测试(16)、processor 测试(11)、路由测试(8),服务端 367 测试全部通过
- TypeScript 0 错误
197 lines
5.7 KiB
TypeScript
197 lines
5.7 KiB
TypeScript
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,
|
|
};
|
|
}
|