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,18 @@
|
||||
import type Database from "bun:sqlite";
|
||||
|
||||
import { generateText } from "ai";
|
||||
|
||||
import type { MaterialType } from "../../shared/api";
|
||||
import type { Logger } from "../logger";
|
||||
|
||||
import { and, asc, eq } from "drizzle-orm";
|
||||
|
||||
import { buildProviderRegistry } from "../ai/registry";
|
||||
import { notDeleted, timestamp, wrap } from "../db/connection";
|
||||
import { listEntityNames } from "../db/entities";
|
||||
import { getModelWithProvider, listModels } from "../db/models";
|
||||
import { materials } from "../db/schema";
|
||||
import { getSettings } from "../db/settings";
|
||||
|
||||
import { getTemplate } from "./templates";
|
||||
|
||||
@@ -17,6 +23,7 @@ export interface ProcessableMaterial {
|
||||
description: string;
|
||||
id: string;
|
||||
materialType: MaterialType;
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export class MaterialProcessor {
|
||||
@@ -101,6 +108,7 @@ export class MaterialProcessor {
|
||||
description: row.description,
|
||||
id: row.id,
|
||||
materialType: row.materialType as MaterialType,
|
||||
projectId: row.projectId,
|
||||
};
|
||||
|
||||
let lastError: unknown;
|
||||
@@ -143,9 +151,48 @@ export class MaterialProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
protected processOne(material: ProcessableMaterial): Promise<string> {
|
||||
protected async processOne(material: ProcessableMaterial): Promise<string> {
|
||||
const modelInfo = getDefaultTextModel(this.db);
|
||||
if (!modelInfo) {
|
||||
throw new Error("没有可用的文本模型,请在设置中配置默认模型或添加至少一个模型");
|
||||
}
|
||||
|
||||
const registry = buildProviderRegistry(this.db);
|
||||
const model = registry.languageModel(`${modelInfo.providerId}:${modelInfo.externalId}`);
|
||||
const existingEntities = listEntityNames(this.db, material.projectId);
|
||||
const template = getTemplate(material.materialType);
|
||||
// TODO: 替换为真实 AI Agent 调用
|
||||
return Promise.resolve(template.outputTemplate.replace("{description}", material.description));
|
||||
const userPrompt = template.buildUserPrompt(material.description, existingEntities);
|
||||
|
||||
const result = await generateText({
|
||||
model,
|
||||
prompt: userPrompt,
|
||||
system: template.systemPrompt,
|
||||
});
|
||||
|
||||
const processingResult = template.parseOutput(result.text);
|
||||
return JSON.stringify(processingResult);
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultTextModel(db: Database): { externalId: string; providerId: string } | null {
|
||||
try {
|
||||
const settings = getSettings(db);
|
||||
if (settings.defaultModels?.text) {
|
||||
const result = getModelWithProvider(db, settings.defaultModels.text);
|
||||
if (!("error" in result)) {
|
||||
return { externalId: result.model.externalId, providerId: result.provider.id };
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// settings 不存在或解析失败,使用 fallback
|
||||
}
|
||||
|
||||
const fallback = listModels(db, { page: 1, pageSize: 1 });
|
||||
const firstModel = fallback.items[0];
|
||||
if (!firstModel) return null;
|
||||
|
||||
const result = getModelWithProvider(db, firstModel.id);
|
||||
if ("error" in result) return null;
|
||||
|
||||
return { externalId: result.model.externalId, providerId: result.provider.id };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user