Files
Alfred/src/server/db/entities.ts
lanyuanxiaoyao 12edf0b545 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 错误
2026-06-08 18:49:30 +08:00

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,
};
}