Files

1 line
8.4 KiB
JSON

{"repo": "12sa3/ai-chat", "files": {"server.js": {"60159936f31f53d1795afac879967951ca2008f7": {"t": 1785768222.4284606, "n": 7686, "h": "8c8a3fe0468809c0", "c": "import http from 'http';\nimport https from 'https';\nimport fs from 'fs';\nimport path from 'path';\nimport dotenv from 'dotenv';\nimport knowledgeService from './src/services/knowledgeService.js';\nimport memoryService from './src/services/memoryService.js';\n\ndotenv.config();\n\nconst API_KEY = process.env.XUNFEI_API_KEY;\nconst PORT = process.env.PORT || 3001;\n\nconst server = http.createServer(async (req, res) => {\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type');\n \n if (req.method === 'OPTIONS') {\n res.statusCode = 200;\n res.end();\n return;\n }\n \n if (req.method === 'POST' && req.url === '/api/chat') {\n let body = '';\n \n req.on('data', (chunk) => {\n body += chunk;\n });\n \n req.on('end', async () => {\n try {\n const requestData = JSON.parse(body);\n const { messages, query, agentMode } = requestData;\n\n console.log(\n '收到请求:', query || '(无查询)',\n '| 消息数:', messages.length,\n agentMode ? '| [Agent]' : ''\n );\n\n // RAG 策略:\n // - Agent 模式:让 Agent 自主决定是否搜索知识库,不自动注入\n // - 普通模式:自动注入知识库上下文\n let systemPrompt = (!agentMode && query)\n ? await knowledgeService.buildSystemPrompt(query)\n : null;\n\n // 长期记忆:自动召回相关记忆\n const memories = query ? await memoryService.recall(query, 5) : [];\n if (memories.length > 0) {\n const memText = memoryService.buildMemoryPrompt(memories);\n if (systemPrompt) {\n // 追加到已有 system prompt 后面\n systemPrompt = systemPrompt + \"\\n\" + memText;\n }\n console.log(`[Memory] 召回了 ${memories.length} 条记忆`);\n }\n\n if (systemPrompt) {\n console.log('[RAG] 已注入知识库上下文');\n } else if (agentMode) {\n console.log('[Agent] 工具由 Agent 自主调用');\n }\n\n handleStreamRequest(messages, res, systemPrompt);\n } catch (error) {\n console.error('解析请求体错误:', error);\n res.statusCode = 400;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ error: '请求格式错误' }));\n }\n });\n } else if (req.method === 'GET' && req.url === '/health') {\n res.statusCode = 200;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ status: 'ok', message: 'AI Chat API is running' }));\n } else if (req.method === 'POST' && req.url === '/api/memory/remember') {\n // 写入长期记忆\n let body = '';\n req.on('data', (chunk) => { body += chunk; });\n req.on('end', async () => {\n try {\n const { key, content, type } = JSON.parse(body);\n await memoryService.remember(key, content, type || 'preference');\n res.statusCode = 200;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ success: true }));\n } catch (error) {\n res.statusCode = 400;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ error: error.message }));\n }\n });\n } else if (req.method === 'GET' && req.url.startsWith('/api/memory/recall')) {\n // 召回长期记忆\n const url = new URL(req.url, 'http://localhost');\n const q = url.searchParams.get('q') || '';\n const memories = await memoryService.recall(q, 5);\n res.statusCode = 200;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ query: q, memories }));\n } else if (req.method === 'POST' && req.url === '/api/knowledge/load') {\n let body = '';\n req.on('data', (chunk) => { body += chunk; });\n req.on('end', async () => {\n try {\n const { title, content } = JSON.parse(body);\n const result = await knowledgeService.loadDocument(title, content);\n res.statusCode = 200;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ success: true, ...result }));\n } catch (error) {\n res.statusCode = 400;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ error: error.message }));\n }\n });\n } else if (req.method === 'GET' && req.url === '/api/knowledge/documents') {\n const docs = await knowledgeService.listDocuments();\n res.statusCode = 200;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ documents: docs }));\n } else if (req.method === 'GET' && req.url.startsWith('/api/knowledge/search')) {\n const url = new URL(req.url, 'http://localhost');\n const q = url.searchParams.get('q') || '';\n const results = await knowledgeService.search(q, 5);\n res.statusCode = 200;\n res.setHeader('Content-Type', 'application/json');\n res.end(JSON.stringify({ query: q, results }));\n } else {\n res.statusCode = 404;\n res.end();\n }\n});\n\nfunction handleStreamRequest(messages, res, systemPrompt = null) {\n // RAG/Agent: system prompt 由前端注入,这里只做兜底自动注入\n const apiMessages = systemPrompt\n ? [{ role: 'system', content: systemPrompt }, ...messages]\n : messages;\n\n const requestBody = {\n model: 'xop35qwen2b',\n messages: apiMessages,\n max_tokens: 4000,\n temperature: 0.7,\n stream: true\n };\n \n const options = {\n hostname: 'maas-api.cn-huabei-1.xf-yun.com',\n port: 443,\n path: '/v2/chat/completions',\n method: 'POST',\n timeout: 30000,\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${API_KEY}`,\n 'User-Agent': 'Node.js-Client',\n 'Accept': '*/*'\n }\n };\n \n console.log('发送到MaaS:', '模型:', requestBody.model, '| 消息数:', requestBody.messages.length, '| RAG:', !!systemPrompt);\n \n const maasReq = https.request(options, (maasRes) => {\n console.log('MaaS API 响应状态码:', maasRes.statusCode);\n \n res.setHeader('Content-Type', 'text/event-stream');\n res.setHeader('Cache-Control', 'no-cache');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n \n maasRes.pipe(res);\n \n maasRes.on('end', () => {\n console.log('响应结束');\n });\n });\n \n maasReq.on('error', (error) => {\n console.error('请求错误:', error);\n \n res.write(`data: {\"error\": \"流式请求失败:${error.message}\"} \\n\\n`);\n res.write('data: [DONE]\\n\\n');\n res.end();\n });\n \n maasReq.on('timeout', () => {\n console.error('请求超时');\n maasReq.destroy();\n \n res.write(`data: {\"error\": \"请求超时\"} \\n\\n`);\n res.write('data: [DONE]\\n\\n');\n res.end();\n });\n \n maasReq.write(JSON.stringify(requestBody));\n maasReq.end();\n \n console.log('请求已发送');\n}\n\nserver.listen(PORT, async () => {\n console.log(`Server is running on http://localhost:${PORT}`);\n\n // 初始化 LanceDB\n await knowledgeService.init();\n\n // 初始化长期记忆\n await memoryService.init();\n\n // 启用 LLM Re-rank(使用讯飞 API 做 Cross-encoder 精排)\n if (API_KEY) {\n knowledgeService.setReranker(API_KEY);\n }\n\n // 仅在首次运行时加载文档(LanceDB 持久化,重启不丢失)\n const stats = await knowledgeService.getStats();\n if (stats.totalChunks === 0) {\n console.log('[知识库] 首次运行,加载文档...');\n const knowledgeDir = path.join(process.cwd(), 'knowledge-base');\n if (fs.existsSync(knowledgeDir)) {\n const files = fs.readdirSync(knowledgeDir).filter(f => f.endsWith('.md'));\n for (const file of files) {\n try {\n const content = fs.readFileSync(path.join(knowledgeDir, file), 'utf-8');\n const title = file.replace('.md', '');\n await knowledgeService.loadDocument(title, content);\n } catch (err) {\n console.error(`[知识库] 加载失败: ${file}`, err.message);\n }\n }\n }\n } else {\n console.log(`[知识库] 已有 ${stats.totalChunks} 个分块,跳过加载`);\n }\n});\n"}}}}