diff --git a/.gitignore b/.gitignore index 121cda6..6ad16d0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,3 @@ /logs/ /data/ .env -conf.json diff --git a/agent/escort-admin/agent.js b/agent/escort-admin/agent.js index 3ff6857..7a95456 100644 --- a/agent/escort-admin/agent.js +++ b/agent/escort-admin/agent.js @@ -1,8 +1,10 @@ import 'dotenv/config'; import { createDeepAgent, FilesystemBackend } from "deepagents"; import { AIMessageChunk, ToolMessage } from "langchain"; -import { SystemMessage, HumanMessage, AIMessage } from "@langchain/core/messages"; +import { HumanMessage } from "@langchain/core/messages"; import { ChatDeepSeek } from "@langchain/deepseek"; +import config from '../../conf.json' with { type: 'json' }; +import logger from '../../utils/logger.js'; import EscortAdminPrompts from "./prompts.js"; import { getEnvTool, webFetchTool, webSearchTool, getCalendarInfoTool, @@ -12,10 +14,14 @@ import { export default class EscortAdminAgent { constructor() { + // 按用户隔离会话:userId -> { agent, messages } + this.sessions = new Map(); + this.maxSessions = 100; } - clearMessages() { - this.messages = []; + clearMessages(userInfo) { + const userId = userInfo?._id ?? 'default-session'; + this.sessions.delete(userId); } // msg: { agent: 'escort-admin', type: 'chat', ts: "2023-08-01 10:00:00", content: "你好" } @@ -24,27 +30,47 @@ export default class EscortAdminAgent { return; } - const agent = this._genAgent(userInfo); + const userId = userInfo?._id ?? 'default-session'; + + // LRU:按用户取会话,并更新使用顺序 + let session = this.sessions.get(userId); + if (session) { + this.sessions.delete(userId); + } else { + session = { agent: null, messages: [] }; + } + this.sessions.set(userId, session); + + // 超出上限时淘汰最久未使用的会话,避免内存泄漏 + if (this.sessions.size > this.maxSessions) { + const oldest = this.sessions.keys().next().value; + this.sessions.delete(oldest); + } + msgs.forEach(msg => { if (msg.type === "clear") { - this.messages = []; + session.messages = []; + session.agent = null; } else { - this.messages.push(new HumanMessage(`${msg.ts} - ${msg.content}`)); + session.messages.push(new HumanMessage(`${msg.ts} - ${msg.content}`)); } }); - if (this.messages.length === 0) { + if (session.messages.length === 0) { return; } + const agent = session.agent ?? this._genAgent(userInfo); + session.agent = agent; + const INTERESTING_NODES = new Set(["model_request", "tools"]); for await (const [namespace, mode, data] of await agent.stream( - { messages: this.messages }, + { messages: session.messages }, { recursion_limit: 50, streamMode: ["updates", "messages", "custom"], subgraphs: true, configurable: { - thread_id: 'default-session' + thread_id: userId } })) { const isSubagent = namespace.some(s => s.startsWith("tools:")); @@ -59,18 +85,17 @@ export default class EscortAdminAgent { // Subagent results returned to main agent for (const msg of data_.messages ?? []) { if (msg.type === "tool") { - console.log(`\nSubagent complete: ${msg.name}`); - console.log(` Result: ${String(msg.content).slice(0, 200)}...`); + logger.info(`Subagent complete: ${msg.name}, Result: ${String(msg.content).slice(0, 200)}`); } } } else if (nodeName === "model_request") { - this.messages.push(...data_.messages); + session.messages.push(...data_.messages); } } } else { // Subagent updates (non-empty namespace) for (const [nodeName, data_] of Object.entries(data)) { - console.log(` [${namespace[0]}] step: ${nodeName}`); + logger.info(`[${namespace[0]}] step: ${nodeName}`); } } } @@ -91,29 +116,23 @@ export default class EscortAdminAgent { callback(source, "tool", message.text, message.id); } } else if (mode === "custom") { - this.logger.info("custom: ", data); + logger.info("custom: ", data); } } } _genAgent(userInfo) { - if (this.agent) { - return this.agent; - } - - this.messages = []; - const rootDir = process.cwd(); const backend = new FilesystemBackend({ rootDir }); this.flashModel = new ChatDeepSeek({ - model: 'deepseek-v4-flash', - apiKey: 'sk-a58ccd82b7ba4ce3ac176a88c9381095', + model: config.agent.deepseek.flashModel, + apiKey: config.agent.deepseek.apiKey, temperature: 0.0 }); this.proModel = new ChatDeepSeek({ - model: 'deepseek-v4-pro', - apiKey: 'sk-a58ccd82b7ba4ce3ac176a88c9381095', + model: config.agent.deepseek.proModel, + apiKey: config.agent.deepseek.apiKey, temperature: 0.3 }); @@ -122,7 +141,7 @@ export default class EscortAdminAgent { description: "查询和设置陪诊预约记录", systemPrompt: "根据用户指令,调用工具完成查询和设置陪诊记录。", model: this.flashModel, - tools: [escortRecordQueryTool], + tools: [escortRecordQueryTool, escortRecordSetTool], }; const escortResearchSubagent = { @@ -133,19 +152,18 @@ export default class EscortAdminAgent { tools: [webFetchTool, webSearchTool], }; - this.agent = createDeepAgent({ + return createDeepAgent({ name: "deep-agent", model: this.flashModel, systemPrompt: EscortAdminPrompts.buildSystemPrompt(userInfo), backend, tools: [getEnvTool, webFetchTool, webSearchTool, getLatLngTool, httpGetTool, httpPostTool, - getCalendarInfoTool, getLunarCalendarInfoTool, getYearHolidaysTool, getYearTermsTool, - escortRecordQueryTool, escortRecordSetTool] + getCalendarInfoTool, getLunarCalendarInfoTool, getYearHolidaysTool, getYearTermsTool, + escortRecordQueryTool, escortRecordSetTool], + subagents: [escortRecordOperSubagent, escortResearchSubagent] }); - - return this.agent; } } const adminAgent = new EscortAdminAgent(); -export { adminAgent }; \ No newline at end of file +export { adminAgent }; diff --git a/agent/escort-admin/tools/db/escort_record_query.js b/agent/escort-admin/tools/db/escort_record_query.js index fe1df31..eb083a7 100644 --- a/agent/escort-admin/tools/db/escort_record_query.js +++ b/agent/escort-admin/tools/db/escort_record_query.js @@ -59,6 +59,19 @@ const escortRecordQueryTool = tool( .describe( "Appointment status: pending (待确认), confirmed (已确认), in_progress (进行中), completed (已完成), cancelled (已取消)" ), + page: z + .number() + .int() + .min(1) + .optional() + .describe("Page number, starting from 1"), + pageSize: z + .number() + .int() + .min(1) + .max(50) + .optional() + .describe("Records per page (1-50), default 20"), }), } ); diff --git a/agent/escort-admin/tools/system/envs.js b/agent/escort-admin/tools/system/envs.js index 308d8c9..e65f7bf 100644 --- a/agent/escort-admin/tools/system/envs.js +++ b/agent/escort-admin/tools/system/envs.js @@ -13,21 +13,22 @@ export const getEnvTool = tool( async (input) => { const { name } = input || {}; + const sensitiveKeys = ['API_KEY', 'SECRET', 'PASSWORD', 'TOKEN', 'CREDENTIAL']; + const isSensitive = (key) => sensitiveKeys.some(s => key.toUpperCase().includes(s)); + if (name) { const value = process.env[name]; if (value === undefined) { return JSON.stringify({ error: `Environment variable '${name}' not found` }); } - return JSON.stringify({ name, value }); + // 按名查询同样脱敏,防止 LLM 绕过读取密钥 + return JSON.stringify({ name, value: isSensitive(name) ? '***REDACTED***' : value }); } // 返回所有环境变量(过滤敏感信息) const env = { ...process.env }; - const sensitiveKeys = ['API_KEY', 'SECRET', 'PASSWORD', 'TOKEN', 'CREDENTIAL']; - for (const key of Object.keys(env)) { - const upperKey = key.toUpperCase(); - if (sensitiveKeys.some(s => upperKey.includes(s))) { + if (isSensitive(key)) { env[key] = '***REDACTED***'; } } diff --git a/agent/escort-admin/tools/system/linux_cmd.js b/agent/escort-admin/tools/system/linux_cmd.js deleted file mode 100644 index e69de29..0000000 diff --git a/agent/escort-admin/tools/system/win_cmd.js b/agent/escort-admin/tools/system/win_cmd.js deleted file mode 100644 index 3972ce7..0000000 --- a/agent/escort-admin/tools/system/win_cmd.js +++ /dev/null @@ -1,74 +0,0 @@ -import * as z from 'zod'; -import { tool } from 'langchain'; -import { spawn } from 'child_process'; - -export const winCmdTool = tool( - async ({ command, cwd, timeout = 30000 }) => { - const workingDir = cwd || process.cwd(); - - return new Promise((resolve) => { - let stdout = ''; - let stderr = ''; - let killed = false; - - const proc = spawn('cmd.exe', ['/c', command], { - cwd: workingDir, - shell: false, - windowsHide: true - }); - - const timer = setTimeout(() => { - killed = true; - proc.kill('SIGKILL'); - }, timeout); - - proc.stdout.on('data', (data) => { - stdout += data.toString(); - }); - - proc.stderr.on('data', (data) => { - stderr += data.toString(); - }); - - proc.on('close', (code) => { - clearTimeout(timer); - if (killed) { - resolve(JSON.stringify({ - success: false, - error: `Command timed out after ${timeout}ms`, - command, - cwd: workingDir - })); - return; - } - resolve(JSON.stringify({ - success: code === 0, - exitCode: code, - stdout: stdout.trim(), - stderr: stderr.trim(), - command, - cwd: workingDir - })); - }); - - proc.on('error', (error) => { - clearTimeout(timer); - resolve(JSON.stringify({ - success: false, - error: error.message, - command, - cwd: workingDir - })); - }); - }); - }, - { - name: 'win_cmd', - description: '在 Windows 系统上执行 cmd.exe 命令行命令。适用于运行 Windows 批处理命令、文件系统操作命令(如 dir、copy、del、mkdir 等)。不支持 PowerShell 命令。', - schema: z.object({ - command: z.string().describe('要执行的 cmd.exe 命令或命令组合,使用 && 连接多个命令,如 "dir /b" 或 "echo hello && dir"'), - cwd: z.string().optional().describe('执行命令的工作目录,默认为当前工作目录'), - timeout: z.number().optional().describe('命令执行超时时间(毫秒),默认30000') - }) - } -); \ No newline at end of file diff --git a/agent/escort/agent.js b/agent/escort/agent.js index 87a5ba0..a93e149 100644 --- a/agent/escort/agent.js +++ b/agent/escort/agent.js @@ -1,10 +1,12 @@ import 'dotenv/config'; import path from 'path'; -import { createDeepAgent, FilesystemBackend, CompositeBackend, StoreBackend } from "deepagents"; +import { createDeepAgent, FilesystemBackend, CompositeBackend } from "deepagents"; import { ChatOpenAI } from "@langchain/openai"; -import { AIMessageChunk, ToolMessage, summarizationMiddleware } from "langchain"; -import { SystemMessage, HumanMessage, AIMessage } from "@langchain/core/messages"; +import { AIMessageChunk, ToolMessage } from "langchain"; +import { HumanMessage } from "@langchain/core/messages"; import { ChatDeepSeek } from "@langchain/deepseek"; +import config from '../../conf.json' with { type: 'json' }; +import logger from '../../utils/logger.js'; import Prompts from "./prompts.js"; import { getEnvTool, webFetchTool, webSearchTool, getCalendarInfoTool, @@ -62,8 +64,7 @@ export default class EscortAgent { // Subagent results returned to main agent for (const msg of data_.messages ?? []) { if (msg.type === "tool") { - console.log(`\nSubagent complete: ${msg.name}`); - console.log(` Result: ${String(msg.content).slice(0, 200)}...`); + logger.info(`Subagent complete: ${msg.name}, Result: ${String(msg.content).slice(0, 200)}`); } } } else if (nodeName === "model_request") { @@ -73,7 +74,7 @@ export default class EscortAgent { } else { // Subagent updates (non-empty namespace) for (const [nodeName, data_] of Object.entries(data)) { - console.log(` [${namespace[0]}] step: ${nodeName}`); + logger.info(`[${namespace[0]}] step: ${nodeName}`); } } } @@ -97,7 +98,7 @@ export default class EscortAgent { callback(source, "tool", message.text, message.id); } } else if (mode === "custom") { - this.logger.info("custom: ", data); + logger.info("custom: ", data); } } } @@ -113,7 +114,6 @@ export default class EscortAgent { if (userInfo) { const userMemoryPath = path.join(rootDir, "data", userInfo._id, "memories"); - console.log(userMemoryPath); backend = new CompositeBackend( new FilesystemBackend({ rootDir }), { @@ -126,15 +126,10 @@ export default class EscortAgent { } this.flashModel = new ChatDeepSeek({ - model: 'deepseek-v4-flash', - apiKey: 'sk-a58ccd82b7ba4ce3ac176a88c9381095', + model: config.agent.deepseek.flashModel, + apiKey: config.agent.deepseek.apiKey, temperature: 0.0 }); - this.proModel = new ChatDeepSeek({ - model: 'deepseek-v4-pro', - apiKey: 'sk-a58ccd82b7ba4ce3ac176a88c9381095', - temperature: 0.3 - }); this.agent = createDeepAgent({ name: "deep-agent", @@ -150,4 +145,4 @@ export default class EscortAgent { return this.agent; } -} \ No newline at end of file +} diff --git a/agent/escort/tools/db/escort_record_query.js b/agent/escort/tools/db/escort_record_query.js index 26ca95e..da930e7 100644 --- a/agent/escort/tools/db/escort_record_query.js +++ b/agent/escort/tools/db/escort_record_query.js @@ -61,12 +61,25 @@ function createEscortRecordQueryTool(userInfo) { .optional() .describe("Patient name for fuzzy search"), status: z - .enum(["pending", "confirmed", "in_progress", "completed", "cancelled"]) - .optional() - .describe( - "Appointment status: pending (待确认), confirmed (已确认), in_progress (进行中), completed (已完成), cancelled (已取消)" - ), - }), + .enum(["pending", "confirmed", "in_progress", "completed", "cancelled"]) + .optional() + .describe( + "Appointment status: pending (待确认), confirmed (已确认), in_progress (进行中), completed (已完成), cancelled (已取消)" + ), + page: z + .number() + .int() + .min(1) + .optional() + .describe("Page number, starting from 1"), + pageSize: z + .number() + .int() + .min(1) + .max(50) + .optional() + .describe("Records per page (1-50), default 20"), + }), } ); } diff --git a/agent/escort/tools/system/envs.js b/agent/escort/tools/system/envs.js index 308d8c9..e65f7bf 100644 --- a/agent/escort/tools/system/envs.js +++ b/agent/escort/tools/system/envs.js @@ -13,21 +13,22 @@ export const getEnvTool = tool( async (input) => { const { name } = input || {}; + const sensitiveKeys = ['API_KEY', 'SECRET', 'PASSWORD', 'TOKEN', 'CREDENTIAL']; + const isSensitive = (key) => sensitiveKeys.some(s => key.toUpperCase().includes(s)); + if (name) { const value = process.env[name]; if (value === undefined) { return JSON.stringify({ error: `Environment variable '${name}' not found` }); } - return JSON.stringify({ name, value }); + // 按名查询同样脱敏,防止 LLM 绕过读取密钥 + return JSON.stringify({ name, value: isSensitive(name) ? '***REDACTED***' : value }); } // 返回所有环境变量(过滤敏感信息) const env = { ...process.env }; - const sensitiveKeys = ['API_KEY', 'SECRET', 'PASSWORD', 'TOKEN', 'CREDENTIAL']; - for (const key of Object.keys(env)) { - const upperKey = key.toUpperCase(); - if (sensitiveKeys.some(s => upperKey.includes(s))) { + if (isSensitive(key)) { env[key] = '***REDACTED***'; } } diff --git a/agent/escort/tools/system/linux_cmd.js b/agent/escort/tools/system/linux_cmd.js deleted file mode 100644 index e69de29..0000000 diff --git a/agent/escort/tools/system/win_cmd.js b/agent/escort/tools/system/win_cmd.js deleted file mode 100644 index 3972ce7..0000000 --- a/agent/escort/tools/system/win_cmd.js +++ /dev/null @@ -1,74 +0,0 @@ -import * as z from 'zod'; -import { tool } from 'langchain'; -import { spawn } from 'child_process'; - -export const winCmdTool = tool( - async ({ command, cwd, timeout = 30000 }) => { - const workingDir = cwd || process.cwd(); - - return new Promise((resolve) => { - let stdout = ''; - let stderr = ''; - let killed = false; - - const proc = spawn('cmd.exe', ['/c', command], { - cwd: workingDir, - shell: false, - windowsHide: true - }); - - const timer = setTimeout(() => { - killed = true; - proc.kill('SIGKILL'); - }, timeout); - - proc.stdout.on('data', (data) => { - stdout += data.toString(); - }); - - proc.stderr.on('data', (data) => { - stderr += data.toString(); - }); - - proc.on('close', (code) => { - clearTimeout(timer); - if (killed) { - resolve(JSON.stringify({ - success: false, - error: `Command timed out after ${timeout}ms`, - command, - cwd: workingDir - })); - return; - } - resolve(JSON.stringify({ - success: code === 0, - exitCode: code, - stdout: stdout.trim(), - stderr: stderr.trim(), - command, - cwd: workingDir - })); - }); - - proc.on('error', (error) => { - clearTimeout(timer); - resolve(JSON.stringify({ - success: false, - error: error.message, - command, - cwd: workingDir - })); - }); - }); - }, - { - name: 'win_cmd', - description: '在 Windows 系统上执行 cmd.exe 命令行命令。适用于运行 Windows 批处理命令、文件系统操作命令(如 dir、copy、del、mkdir 等)。不支持 PowerShell 命令。', - schema: z.object({ - command: z.string().describe('要执行的 cmd.exe 命令或命令组合,使用 && 连接多个命令,如 "dir /b" 或 "echo hello && dir"'), - cwd: z.string().optional().describe('执行命令的工作目录,默认为当前工作目录'), - timeout: z.number().optional().describe('命令执行超时时间(毫秒),默认30000') - }) - } -); \ No newline at end of file diff --git a/conf.json b/conf.json index dff2c8e..59a53a3 100644 --- a/conf.json +++ b/conf.json @@ -5,18 +5,31 @@ "wxapp-escort-admin": { } }, + "agent": { + "deepseek": { + "apiKey": "sk-a58ccd82b7ba4ce3ac176a88c9381095", + "flashModel": "deepseek-v4-flash", + "proModel": "deepseek-v4-pro" + }, + "glm": { + "apiKey": "ed5f2ec42cb9413f87402d38321553f8.u0FalmfLdXxmst69", + "flashModel": "glm-5.3-flash", + "proModel": "glm-5.3" + } + }, "mongodb": { - "str": "mongodb://huashengtec.com:6000", + "str": "mongodb://ehason:Ehason_dbuser_2026@huashengtec.com:6000/?authSource=admin&appName=ws-health&retryWrites=true&w=majority", "host": "huashengtec.com", - "db": "health", "option": { - "user": "ehason", - "pass": "Ehason_dbuser_2026", "dbName": "eiot_health", - "authSource": "admin", "autoIndex": true, - "socketTimeoutMS": 3000, - "serverSelectionTimeoutMS": 30000 + "maxPoolSize": 50, + "minPoolSize": 5, + "maxIdleTimeMS": 60000, + "connectTimeoutMS": 10000, + "serverSelectionTimeoutMS": 30000, + "socketTimeoutMS": 45000, + "compressors": "zlib" }, "debug": true } diff --git a/models/index.js b/models/index.js index d5d67b6..54b8d4a 100644 --- a/models/index.js +++ b/models/index.js @@ -3,6 +3,8 @@ import mongoose from 'mongoose'; import { EscortRecordSchema } from "./schema/escort_record.js" import { HealthProfileSchema } from "./schema/health_profile.js" +import { OrganizationSchema } from "./schema/organization.js" +import { EmployeeSchema } from "./schema/employee.js" import config from '../conf.json' with { type: 'json' }; import logger from '../utils/logger.js'; @@ -12,6 +14,7 @@ class MongoDBSchema { this.User = null; this.EscortRecord = null; this.Organization = null; + this.Employee = null; this.HealthProfile = null; } @@ -40,6 +43,8 @@ class MongoDBSchema { this.EscortRecord = this.dbConnection.model('escort_record', EscortRecordSchema) this.HealthProfile = this.dbConnection.model('health_profile', HealthProfileSchema) + this.Organization = this.dbConnection.model('organization', OrganizationSchema) + this.Employee = this.dbConnection.model('employee', EmployeeSchema) } } diff --git a/models/schema/org.js b/models/schema/organization.js similarity index 100% rename from models/schema/org.js rename to models/schema/organization.js