feat: 初始化 OnceLove GraphRAG 项目基础架构

- 添加完整的项目结构,包括前端(Vue3 + Vite)、后端(Fastify)和基础设施配置
- 实现核心 GraphRAG 服务,集成 Neo4j 图数据库和 Qdrant 向量数据库
- 添加用户认证系统和管理员登录界面
- 提供 Docker 容器化部署方案和开发环境配置
- 包含项目文档、API 文档(Swagger)和测试脚本
This commit is contained in:
KOSHM-Pig
2026-03-23 00:00:13 +08:00
commit ec21df7aa6
50 changed files with 7459 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
const createHttpError = (statusCode, message) => {
const error = new Error(message);
error.statusCode = statusCode;
return error;
};
export class LLMService {
constructor(env) {
this.baseUrl = (env.LLM_BASE_URL ?? "").replace(/\/+$/, "");
this.apiKey = env.LLM_API_KEY ?? "";
this.model = env.LLM_MODEL_NAME ?? "";
}
isEnabled() {
return Boolean(this.baseUrl && this.apiKey && this.model);
}
async chat(messages, temperature = 0.7) {
if (!this.isEnabled()) {
throw createHttpError(400, "LLM 服务未配置,请提供 LLM_BASE_URL/LLM_API_KEY/LLM_MODEL_NAME");
}
const response = await fetch(`${this.baseUrl}/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.apiKey}`
},
body: JSON.stringify({
model: this.model,
messages: messages,
temperature: temperature
})
});
if (!response.ok) {
const errorText = await response.text();
throw createHttpError(response.status, `LLM 请求失败:${errorText}`);
}
const data = await response.json();
return data;
}
async analyzeText(text) {
if (!text?.trim()) {
throw createHttpError(400, "分析文本不能为空");
}
const systemPrompt = `你是一个实体关系分析专家。请分析用户输入的文本,提取人物、事件、主题、关系。
## 输出格式
{
"persons": [{"id": "p1", "name": "人物名称", "description": "人物描述"}],
"events": [{"id": "e1", "type": "事件类型", "summary": "事件摘要", "occurred_at": "ISO 时间", "participants": ["p1"], "topics": ["t1"], "importance": 5}],
"topics": [{"id": "t1", "name": "主题名称"}],
"relations": [{"source": "p1", "target": "p2", "type": "关系类型", "description": "关系描述"}]
}
## 注意
- 时间用 ISO 格式,如文本没明确时间用当前时间
- importance 是重要性评分 1-10
- 关系类型PARTICIPATES_IN, ABOUT, LOVES, FIGHTS_WITH, GIVES, PROPOSES_TO 等
- 如果文本涉及"我",推断另一个角色(如"她"
- 即使文本很短也要提取信息,不要返回空数组
只返回 JSON不要有其他文字。`;
const messages = [
{ role: "system", content: systemPrompt },
{ role: "user", content: text }
];
console.log("[DEBUG] LLM request messages:", JSON.stringify(messages));
const result = await this.chat(messages, 0.3);
const content = result?.choices?.[0]?.message?.content;
console.log("[DEBUG] LLM raw response:", content);
if (!content) {
throw createHttpError(500, "LLM 返回内容为空");
}
let parsed;
try {
const jsonMatch = content.match(/\{[\s\S]*\}/);
parsed = jsonMatch ? JSON.parse(jsonMatch[0]) : JSON.parse(content);
} catch (e) {
throw createHttpError(500, `LLM 返回格式错误:${content.substring(0, 200)}`);
}
if (this.isEmptyAnalysis(parsed)) {
const retryMessages = [
{
role: "system",
content: "你是信息抽取器。必须输出非空 JSON{persons:[{id,name,description}],events:[{id,type,summary,occurred_at,participants,topics,importance}],topics:[{id,name}],relations:[{source,target,type,description}]}"
},
{
role: "user",
content: `从下列文本提取实体关系,至少给出 2 个 persons、1 个 event、1 个 relation且仅返回 JSON${text}`
}
];
const retryResult = await this.chat(retryMessages, 0.2);
const retryContent = retryResult?.choices?.[0]?.message?.content;
if (!retryContent) {
return parsed;
}
try {
const retryJsonMatch = retryContent.match(/\{[\s\S]*\}/);
const retryParsed = retryJsonMatch ? JSON.parse(retryJsonMatch[0]) : JSON.parse(retryContent);
if (!this.isEmptyAnalysis(retryParsed)) {
return retryParsed;
}
} catch (_) {
return parsed;
}
}
return parsed;
}
isEmptyAnalysis(data) {
return !data
|| (!Array.isArray(data.persons) || data.persons.length === 0)
&& (!Array.isArray(data.events) || data.events.length === 0)
&& (!Array.isArray(data.topics) || data.topics.length === 0)
&& (!Array.isArray(data.relations) || data.relations.length === 0);
}
}