This commit is contained in:
lik
2026-09-02 22:39:04 +08:00
parent c95a9c4c95
commit f81f6906f0
24 changed files with 389 additions and 1024 deletions
+27 -2
View File
@@ -1,4 +1,5 @@
import 'dotenv/config';
import fs from 'fs';
import path from 'path';
import { createDeepAgent, FilesystemBackend, CompositeBackend } from "deepagents";
import { ChatOpenAI } from "@langchain/openai";
@@ -7,6 +8,10 @@ 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';
// 密钥由 conf.json 统一提供;百度地图 skill 通过 env_get 读环境变量,此处桥接
process.env.TAVILY_API_KEY ??= config.agent.tavily?.apiKey;
process.env.BAIDU_MAP_AUTH_TOKEN ??= config.agent.baiduMap?.authToken;
import Prompts from "./prompts.js";
import {
getEnvTool, webFetchTool, webSearchTool, getCalendarInfoTool,
@@ -19,6 +24,7 @@ export default class EscortAgent {
static MAX_HISTORY_CHARS = config.agent.maxHistoryChars || 40000;
constructor() {
this.messages = [];
}
clearMessages() {
@@ -137,12 +143,19 @@ export default class EscortAgent {
}
_genAgent(userInfo) {
const rootDir = process.cwd();
const memoryFile = userInfo
? path.join(rootDir, "data", userInfo._id, "memories", "user_memory.txt")
: null;
// 会话中记忆文件被更新后重建 agent,使 systemPrompt 中的用户记忆保持最新
if (this.agent && this._memoryMtime !== this._statMemory(memoryFile)) {
this.agent = null;
}
if (this.agent) {
return this.agent;
}
const rootDir = process.cwd();
this.messages = [];
let backend = new FilesystemBackend({ rootDir });
if (userInfo) {
@@ -164,6 +177,8 @@ export default class EscortAgent {
temperature: 0.0
});
this._memoryMtime = this._statMemory(memoryFile);
this.agent = createDeepAgent({
name: "deep-agent",
model: this.flashModel,
@@ -178,4 +193,14 @@ export default class EscortAgent {
return this.agent;
}
// 返回记忆文件 mtime,文件不存在时返回 null
_statMemory(memoryFile) {
if (!memoryFile) return null;
try {
return fs.statSync(memoryFile).mtimeMs;
} catch {
return null;
}
}
}
+2 -2
View File
@@ -1,8 +1,8 @@
import moment from "moment";
import fs from "fs";
import path from "path";
import services from "../../resource/services.js";
import agreement from "../../resource/agreement.js";
import logger from "../../utils/logger.js";
class Prompts {
static buildSystemPrompt(userInfo) {
@@ -24,7 +24,7 @@ class Prompts {
try {
usermem_str = fs.readFileSync(userMemoryPath, 'utf8');
} catch (err) {
console.log('读取用户记忆失败', err);
logger.error('读取用户记忆失败', err);
}
}
@@ -1,24 +1,33 @@
import { tool } from "langchain"
import lunarLib from "lunar-javascript"
const { Solar } = lunarLib
/**
* 特定日期日历工具
* 提供指定日期的历信息
* 基于 lunar-javascript 提供指定日期的完整农历信息
*/
export const getLunarCalendarInfoTool = tool(
async ({ year, month, day }) => {
try {
const date = new Date(year, month - 1, day)
if (isNaN(date.getTime())) {
return JSON.stringify({ error: '无效的日期' }, null, 2)
const solar = Solar.fromYmd(year, month, day)
const lunar = solar.getLunar()
const result = {
solar: solar.toString(),
weekday: `星期${solar.getWeekInChinese()}`,
lunar: lunar.toString(),
ganzhiYear: lunar.getYearInGanZhi(),
zodiac: lunar.getYearShengXiao(),
solarTerms: lunar.getJieQi() || null,
festivals: [...solar.getFestivals(), ...lunar.getFestivals()],
yi: lunar.getDayYi(),
ji: lunar.getDayJi(),
xiShen: lunar.getDayPositionXiDesc(),
caiShen: lunar.getDayPositionCaiDesc()
}
// 获取农历信息
const lunarInfo = {}
return JSON.stringify(lunarInfo, null, 2)
return JSON.stringify(result, null, 2)
} catch (error) {
console.error('Error in date calendar tool:', error)
return JSON.stringify({ error: error.message }, null, 2)
}
},
@@ -50,4 +59,4 @@ export const getLunarCalendarInfoTool = tool(
required: ["year", "month", "day"]
}
}
)
)
+2 -1
View File
@@ -40,7 +40,8 @@ export async function getAccurateTime() {
for (const source of timeSources) {
try {
const startTime = Date.now()
const res = await fetch(source.url)
// 每个时间源限时 3 秒,防止单个源 TCP 挂起长时间阻塞
const res = await fetch(source.url, { signal: AbortSignal.timeout(3000) })
const latency = Date.now() - startTime
let timestamp = null
+25 -14
View File
@@ -1,30 +1,41 @@
import { tool } from "langchain"
import lunarLib from "lunar-javascript"
const { Solar, HolidayUtil } = lunarLib
/**
* 年份节日列表工具
* 查询给定年份节日所在日期列表
* 基于 lunar-javascript 计算全年节日(公历节日 + 农历传统节日)与法定节假日安排
*/
export const getYearHolidaysTool = tool(
async ({ year }) => {
try {
// 简化实现,返回基本节日信息
const holidays = [
{ date: `${year}-01-01`, name: '元旦' },
{ date: `${year}-02-14`, name: '情人节' },
{ date: `${year}-05-01`, name: '劳动节' },
{ date: `${year}-06-01`, name: '儿童节' },
{ date: `${year}-10-01`, name: '国庆节' }
]
return JSON.stringify({ year, holidays }, null, 2)
// 法定节假日安排(含调休补班)
const legal = HolidayUtil.getHolidays(year).map(h => ({
date: h.getTarget().toString(),
name: h.getName(),
type: h.isWork() ? "调休上班" : "放假"
}))
// 遍历全年日期,收集公历节日与农历传统节日(春节、中秋、端午等)
const festivals = []
const start = Solar.fromYmd(year, 1, 1)
const days = Solar.fromYmd(year + 1, 1, 1).subtract(start)
for (let i = 0; i < days; i++) {
const d = start.next(i)
const names = [...d.getFestivals(), ...d.getLunar().getFestivals()]
if (names.length) {
festivals.push({ date: d.toString(), name: names.join("、") })
}
}
return JSON.stringify({ year, legal, festivals }, null, 2)
} catch (error) {
console.error('Error in year holidays tool:', error)
return JSON.stringify({ error: error.message }, null, 2)
}
},
{
name: "get_year_holidays",
description: "查询给定年份节日所在日期列表",
description: "查询给定年份节日所在日期列表,包括法定节假日安排(含调休)与传统节日",
schema: {
type: "object",
properties: {
@@ -38,4 +49,4 @@ export const getYearHolidaysTool = tool(
required: ["year"]
}
}
)
)
+17 -31
View File
@@ -1,43 +1,29 @@
import { tool } from "langchain"
import lunarLib from "lunar-javascript"
const { Lunar } = lunarLib
// 当年冬至在节气表中的 key 为英文(上一年冬至占用中文 key)
const EN_TERM_NAMES = { DONG_ZHI: "冬至" }
/**
* 年份节气列表工具
* 查询给定年份节气所在日期列表
* 基于 lunar-javascript 计算给定年份的 24 节气精确日期
*/
export const getYearTermsTool = tool(
async ({ year }) => {
try {
// 简化实现,返回24节气信息
const terms = [
{ date: `${year}-02-04`, name: '立春' },
{ date: `${year}-02-19`, name: '雨水' },
{ date: `${year}-03-05`, name: '惊蛰' },
{ date: `${year}-03-20`, name: '春分' },
{ date: `${year}-04-04`, name: '清明' },
{ date: `${year}-04-19`, name: '谷雨' },
{ date: `${year}-05-05`, name: '立夏' },
{ date: `${year}-05-20`, name: '小满' },
{ date: `${year}-06-05`, name: '芒种' },
{ date: `${year}-06-21`, name: '夏至' },
{ date: `${year}-07-07`, name: '小暑' },
{ date: `${year}-07-22`, name: '大暑' },
{ date: `${year}-08-07`, name: '立秋' },
{ date: `${year}-08-23`, name: '处暑' },
{ date: `${year}-09-07`, name: '白露' },
{ date: `${year}-09-23`, name: '秋分' },
{ date: `${year}-10-08`, name: '寒露' },
{ date: `${year}-10-23`, name: '霜降' },
{ date: `${year}-11-07`, name: '立冬' },
{ date: `${year}-11-22`, name: '小雪' },
{ date: `${year}-12-07`, name: '大雪' },
{ date: `${year}-12-21`, name: '冬至' },
{ date: `${year + 1}-01-05`, name: '小寒' },
{ date: `${year + 1}-01-20`, name: '大寒' }
]
const table = Lunar.fromYmd(year, 1, 1).getJieQiTable()
const terms = []
for (const [key, solar] of Object.entries(table)) {
// 只保留属于目标年份的节气(表首含上一年边界节气)
if (solar.getYear() !== year) continue
const name = EN_TERM_NAMES[key] ?? key
terms.push({ date: solar.toString(), name })
}
terms.sort((a, b) => a.date.localeCompare(b.date))
return JSON.stringify({ year, terms }, null, 2)
} catch (error) {
console.error('Error in year terms tool:', error)
return JSON.stringify({ error: error.message }, null, 2)
}
},
@@ -57,4 +43,4 @@ export const getYearTermsTool = tool(
required: ["year"]
}
}
)
)
+11 -10
View File
@@ -1,5 +1,6 @@
import { tool } from "@langchain/core/tools";
import z from "zod";
import mongoose from "mongoose";
import { DBModel } from "../../../../models/index.js";
const escortRecordSetTool = tool(
@@ -8,18 +9,18 @@ const escortRecordSetTool = tool(
if (!orderId) {
return {
success: false,
error: "Order ID (_id or orderNo) is required as the lookup key",
error: "Record ID (_id) is required as the lookup key",
};
}
const query = {
$or: [
{ _id: orderId },
{ orderNo: orderId }
]
};
if (!mongoose.Types.ObjectId.isValid(orderId)) {
return {
success: false,
error: `Invalid record ID: ${orderId}. A valid record _id is required.`,
};
}
const record = await DBModel.EscortRecord.findOne(query);
const record = await DBModel.EscortRecord.findById(orderId);
if (!record) {
return {
success: false,
@@ -79,11 +80,11 @@ const escortRecordSetTool = tool(
{
name: "escort_record_set",
description:
"Update escort record fields by order ID (_id or orderNo). Supports updating status, notes (patientNote, escortNote, medicalSummary), and payment (totalFee, paidFee, status). Only provided fields will be updated.",
"Update escort record fields by record ID (_id). Supports updating status, notes (patientNote, escortNote, medicalSummary), and payment (totalFee, paidFee, status). Only provided fields will be updated.",
schema: z.object({
orderId: z
.string()
.describe("Order ID (_id or orderNo) used as the lookup key"),
.describe("Record ID (_id) used as the lookup key"),
status: z
.enum(["pending", "confirmed", "in_progress", "completed", "cancelled"])
.optional()
+2 -1
View File
@@ -1,6 +1,7 @@
import { TavilyExtract } from "@langchain/tavily";
import { tool } from "@langchain/core/tools";
import * as z from "zod"
import config from "../../../../conf.json" with { type: "json" };
const webFetchTool = tool(
async ({
@@ -10,7 +11,7 @@ const webFetchTool = tool(
urls = [],
}) => {
const tavilyExtract = new TavilyExtract({
tavilyApiKey: process.env.TAVILY_API_KEY,
tavilyApiKey: config.agent.tavily.apiKey,
extractDepth,
includeImages,
format
+2 -1
View File
@@ -1,6 +1,7 @@
import { TavilySearch } from "@langchain/tavily";
import { tool } from "@langchain/core/tools";
import * as z from "zod"
import config from "../../../../conf.json" with { type: "json" };
const webSearchTool = tool(
async ({
@@ -11,7 +12,7 @@ const webSearchTool = tool(
}) => {
const tavilySearch = new TavilySearch({
maxResults,
tavilyApiKey: process.env.TAVILY_API_KEY,
tavilyApiKey: config.agent.tavily.apiKey,
includeRawContent,
topic,
});