From b92853a717a489843e572f5ec12b0f0435dd60c9 Mon Sep 17 00:00:00 2001 From: lik Date: Sat, 30 May 2026 16:41:26 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=BA=86agent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 1 + agent/agent.js | 17 +- agent/prompts.js | 22 +- agent/skills/baidu-ai-map/SKILL.md | 243 ++++++++++++++++++ agent/skills/baidu-ai-map/_meta.json | 6 + agent/tools/calendar/calendar_info.js | 42 ++++ agent/tools/calendar/lunar_calendar_info.js | 53 ++++ agent/tools/calendar/utils.js | 86 +++++++ agent/tools/calendar/year_holidays.js | 41 ++++ agent/tools/calendar/year_terms.js | 60 +++++ agent/tools/geo/latlon.js | 40 +++ agent/tools/index.js | 20 ++ agent/tools/system/envs.js | 82 +++++++ agent/tools/system/fs_info.js | 257 ++++++++++++++++++++ agent/tools/system/hardware_info.js | 112 +++++++++ agent/tools/system/installed_software.js | 101 ++++++++ agent/tools/system/linux_cmd.js | 0 agent/tools/system/os_info.js | 89 +++++++ agent/tools/system/os_network_info.js | 242 ++++++++++++++++++ agent/tools/system/os_socket_info.js | 157 ++++++++++++ agent/tools/system/system_time.js | 17 ++ agent/tools/system/win_cmd.js | 74 ++++++ agent/tools/web/fetch.js | 34 +++ agent/tools/web/http_get.js | 62 +++++ agent/tools/web/http_post.js | 88 +++++++ agent/tools/web/search.js | 35 +++ index.js | 1 + package-lock.json | 9 +- package.json | 1 + resource/agreement.js | 2 +- 30 files changed, 1972 insertions(+), 22 deletions(-) create mode 100644 agent/skills/baidu-ai-map/SKILL.md create mode 100644 agent/skills/baidu-ai-map/_meta.json create mode 100644 agent/tools/calendar/calendar_info.js create mode 100644 agent/tools/calendar/lunar_calendar_info.js create mode 100644 agent/tools/calendar/utils.js create mode 100644 agent/tools/calendar/year_holidays.js create mode 100644 agent/tools/calendar/year_terms.js create mode 100644 agent/tools/geo/latlon.js create mode 100644 agent/tools/index.js create mode 100644 agent/tools/system/envs.js create mode 100644 agent/tools/system/fs_info.js create mode 100644 agent/tools/system/hardware_info.js create mode 100644 agent/tools/system/installed_software.js create mode 100644 agent/tools/system/linux_cmd.js create mode 100644 agent/tools/system/os_info.js create mode 100644 agent/tools/system/os_network_info.js create mode 100644 agent/tools/system/os_socket_info.js create mode 100644 agent/tools/system/system_time.js create mode 100644 agent/tools/system/win_cmd.js create mode 100644 agent/tools/web/fetch.js create mode 100644 agent/tools/web/http_get.js create mode 100644 agent/tools/web/http_post.js create mode 100644 agent/tools/web/search.js diff --git a/.env b/.env index d1b5f66..6b652c6 100644 --- a/.env +++ b/.env @@ -1,2 +1,3 @@ TAVILY_API_KEY=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 DEEPSEEK_API_KEY=sk-a58ccd82b7ba4ce3ac176a88c9381095 +BAIDU_MAP_AUTH_TOKEN=sk-ap-9xcqwNJ3FyJGUoAoQoCgWLqPd5o2tJKCA1xaSMRaZT0zDo0PCFm7rczL4anUuXf5 \ No newline at end of file diff --git a/agent/agent.js b/agent/agent.js index f124dfa..baea59f 100644 --- a/agent/agent.js +++ b/agent/agent.js @@ -1,10 +1,14 @@ import 'dotenv/config'; -import { createDeepAgent } from "deepagents"; +import { createDeepAgent, FilesystemBackend } from "deepagents"; import { ChatOpenAI } from "@langchain/openai"; import { AIMessageChunk, ToolMessage } from "langchain"; import { SystemMessage, HumanMessage, AIMessage } from "@langchain/core/messages"; import { ChatDeepSeek } from "@langchain/deepseek"; import Prompts from "./prompts.js"; +import { getEnvTool,webFetchTool, webSearchTool, getCalendarInfoTool, + getLunarCalendarInfoTool, getYearHolidaysTool, getYearTermsTool, getLatLngTool, + httpGetTool, httpPostTool + } from "./tools/index.js"; export default class EscortAgent { constructor() { @@ -25,7 +29,7 @@ export default class EscortAgent { if (msg.type === "clear") { this.messages = []; } else { - this.messages.push(new HumanMessage(msg.content)); + this.messages.push(new HumanMessage(`${msg.ts} - ${msg.content}`)); } }); @@ -99,6 +103,9 @@ export default class EscortAgent { this.messages = []; + const rootDir = process.cwd(); + const backend = new FilesystemBackend({ rootDir }); + this.model = new ChatDeepSeek({ model: 'deepseek-v4-pro', apiKey: 'sk-a58ccd82b7ba4ce3ac176a88c9381095', @@ -108,7 +115,11 @@ export default class EscortAgent { this.agent = createDeepAgent({ name: "deep-agent", model: this.model, - systemPrompt: Prompts.buildSystemPrompt(userInfo) + systemPrompt: Prompts.buildSystemPrompt(userInfo), + backend, + tools: [getEnvTool, webFetchTool, webSearchTool, getLatLngTool, httpGetTool, httpPostTool, + getCalendarInfoTool, getLunarCalendarInfoTool, getYearHolidaysTool, getYearTermsTool], + skills: ["./agent/skills/"] }); return this.agent; diff --git a/agent/prompts.js b/agent/prompts.js index d6da8ac..f10d31f 100644 --- a/agent/prompts.js +++ b/agent/prompts.js @@ -1,5 +1,6 @@ import moment from "moment"; import services from "../resource/services.js"; +import agreement from "../resource/agreement.js"; class Prompts { static buildSystemPrompt(userInfo) { @@ -18,11 +19,9 @@ class Prompts { return ` # 角色定义 -你是小橙,一名陪诊服务顾问。温暖、直接、高效。 +你是小橙,暖橙陪诊平台的AI客服,温暖、直接、高效的回答用户问题。 # 核心能力 -- 查询医院、科室、医生信息 -- 创建、查询陪诊订单 - 解答服务流程、价格、注意事项 - 提供就诊准备建议 @@ -31,24 +30,15 @@ class Prompts { 2. 聊专业问题时,要严肃专注,不偏离主题;要客观公正,绝不主观臆造;同时给用户专业真诚的反馈。 3. 聚焦用户最新问题,理解意图,高情商个性化的跟用户沟通,不要一开口就问“需要陪诊服务吗”。 4. 主动追问:回答后,一句追问收尾,引导用户给出下一步关键信息。 -5. 输出要排版层次清晰,格式统一整洁,不要使用markdown格式。 -6. 医疗问题时,末尾加一句"最终以医生诊断为准"。 +5. 输出要排版层次清晰,结构整洁。 +6. 医疗问题时,提示用户最终以医生诊断为准。 7. 保护用户隐私,不泄露个人信息。 -8. 用户询问怎么加入团队或怎么合作时,首先欢迎用户加入团队,然后让用户电话或微信联系。 -9. 你无法回答的业务问题,要提示用户联系客服。 - -# 工作流程 -1. 问清城市、医院、科室 -2. 推荐匹配选项 -3. 确认就诊时间 -4. 创建订单、确认细节 -5. 就诊前提醒 +8. 你无法回答的业务问题,要提示用户联系客服。 ## 参考信息 -当前日期:${moment().format("YYYY-MM-DD")}; 用户信息:${userInfo_str}; 服务项目:${JSON.stringify(services)}; -服务电话: 18618162956 (微信同号) +服务协议:${JSON.stringify(agreement)}; `; } } diff --git a/agent/skills/baidu-ai-map/SKILL.md b/agent/skills/baidu-ai-map/SKILL.md new file mode 100644 index 0000000..da2c113 --- /dev/null +++ b/agent/skills/baidu-ai-map/SKILL.md @@ -0,0 +1,243 @@ +--- +name: baidu-ai-map +description: 为Agent提供地图和位置相关的能力,例如 AI 地点检索(周边信息)、AI 路线规划、地理编码与逆地理编码、天气查询等地图工具。 +--- + +# 百度地图服务 Agent Plan + +提供大模型友好调用的地图工具skills/baidu-ai-map/SKILL.md,支持POI搜索、导航、语义化 AI 搜索、语义化 AI 路线规划、地理编码与逆地理编码、天气查询。 + +## 使用准则 + +### 准则 1:API 端点 + +所有能力统一使用: + +> **Base URL**: `https://api.map.baidu.com/` + +### 准则 2:SK 凭证安全处理 + +SK(Service Key)是调用所有 API 的必须凭证: + +1. 优先读取环境变量 `BAIDU_MAP_AUTH_TOKEN`。 +2. 如果为空,提示用户前往 [百度地图 Agent Plan](https://lbs.baidu.com/apiconsole/agentplan) 申请。 +3. 设置环境变量: + +```bash +export BAIDU_MAP_AUTH_TOKEN="你的SK" +``` + +### 准则 3:统一鉴权方式(Header 传入) + +调用所有 API 时,统一通过 Header 传入: + +- `Authorization: Bearer $BAIDU_MAP_AUTH_TOKEN` + +示例: + +```bash +curl --get "https://api.map.baidu.com/agent_plan/v1/place" \ + -H "Authorization: Bearer $BAIDU_MAP_AUTH_TOKEN" \ + --data-urlencode "user_raw_request=帮我找北京可带宠物的咖啡馆" \ + --data-urlencode "region=北京市" +``` + +### 准则 4:优先使用http_get工具,避免直接调用curl,curl在window下支持不好 + +## 全局参数与行为约束 + +1. `user_raw_request` 必须是完整的用户需求,不可压缩为关键词。 +2. `user_raw_request` 出现“我附近”等非明确地点时,可根据上下文为用户推理为具体地点名称,但不可对坐标进行推理。 +3. `user_raw_request` 需保留定语/约束词,例如“评分最高”“最近”“最便宜”“3公里内”。 +4. 不得编造坐标;`center` / `location` / `refer_pois` 仅可来自用户明确提供或可信来源,当无可靠坐标时,必须先向用户澄清或先调用 `geocoding` / `place` 等工具获取。 +5. 统一使用 Header 鉴权:`Authorization: Bearer $BAIDU_MAP_AUTH_TOKEN`。 +6. 经纬度至少保留小数点后 6 位。 +7. 所有工具返回坐标类型统一为 `gcj02`。 +8. 禁止使用 grep/sed/awk/jq/python脚本 正则裁剪响应后再推断字段,这会造成百度地图 Agent Plan 巨额的token消耗;短时间内遇到相同或高度相似请求时,应优先基于结果直接推理,避免重复发起请求。 +9. `direction` 可能返回两类结果:路线结果,或起终点澄清结果。当返回起终点澄清结果时,应先根据用户需求推理最合理候选;若仍无法唯一确定,应引导用户在候选中选择后再继续规划。 + +## 工具详解 + +### 1. Place(语义化AI地点检索) + +#### API + +`GET /agent_plan/v1/place` + +#### 参数输入(给模型) + +Required: +- `user_raw_request`: 用户原始需求,原样完整传入,不可压缩为关键词;保留约束词(如“评分最高”“最近”“3公里内”) +- `region`: 城市或区域限制 + +Optional: +- `center`: 检索中心点和排序参考点(`lat,lng`,gcj02) +- `sort`: `distance` 或 `relevance`(默认 `relevance`) + +Rules: +- `sort=distance` 时,`center` 必传 +- `center` 只能来自用户明确提供或可信来源,禁止推测/编造,可通过 `geocoding` / `place` 等工具获取 +- 经纬度至少保留小数点后 6 位 + +#### 鉴权 + +- GET:Header `Authorization: Bearer $BAIDU_MAP_AUTH_TOKEN` + +#### 示例 + +```bash +# 1) 帮我查一下八达岭长城附近的五星级酒店 +curl --get "https://api.map.baidu.com/agent_plan/v1/place" \ + -H "Authorization: Bearer $BAIDU_MAP_AUTH_TOKEN" \ + --data-urlencode "user_raw_request=帮我查一下八达岭长城附近的五星级酒店" \ + --data-urlencode "region=延庆区" \ + --data-urlencode "sort=relevance" + +# 2) 离我最近的火锅店(distance 排序) +curl --get "https://api.map.baidu.com/agent_plan/v1/place" \ + -H "Authorization: Bearer $BAIDU_MAP_AUTH_TOKEN" \ + --data-urlencode "user_raw_request=离我最近的火锅店" \ + --data-urlencode "region=北京市" \ + --data-urlencode "center=40.056800,116.308300" \ + --data-urlencode "sort=distance" +``` + +### 2. Direction(语义化AI路线规划) + +#### API + +`GET /agent_plan/v1/direction` + +#### 参数输入 + +Required: +- `user_raw_request`: 用户原始需求,包含起点和终点;保留路线约束词,需要推理并改写用户原始的交通方式 +- `location`: 用户当前位置坐标(`lat,lng`,gcj02) + +Optional: +- `refer_pois`: 地点精确映射,格式 `地点名称:uid,纬度,经度;地点名称:uid,纬度,经度` + +Rules: +- 当前仅支持驾车、步行、骑行、公交路线规划。`user_raw_request` 中出现其他交通方式时,需要改写到这四种交通方式 +- `location` 当起点有歧义(如同名地标、模糊起点)时,路线规划服务会基于当前位置推理最合理的起点位置 +- `refer_pois` 默认不传,用于同名地点消歧,仅在 `user_raw_request` 中明确有歧义地点时传入 +- `refer_pois` / `location` 的经纬度至少保留小数点后 6 位,禁止推测/编造,可通过 `geocoding` / `place` 等工具获取 + +#### 鉴权 + +- GET:Header `Authorization: Bearer $BAIDU_MAP_AUTH_TOKEN` + +#### 示例 + +```bash +# 1) 帮我规划从故宫到颐和园的驾车路线 +curl --get "https://api.map.baidu.com/agent_plan/v1/direction" \ + -H "Authorization: Bearer $BAIDU_MAP_AUTH_TOKEN" \ + --data-urlencode "user_raw_request=帮我规划从故宫到颐和园的驾车路线" \ + --data-urlencode "location=39.914590,116.403770" + +# 2) “我家”别名映射 +curl --get "https://api.map.baidu.com/agent_plan/v1/direction" \ + -H "Authorization: Bearer $BAIDU_MAP_AUTH_TOKEN" \ + --data-urlencode "user_raw_request=步行去我家附近最近的中餐厅" \ + --data-urlencode "location=40.056800,116.308300" \ + --data-urlencode "refer_pois=我家:fbc88a21464370106e3e1b52,40.092180,116.345310" + +# 2) 交通方式推理改写:从王府井打车去三里屯要多久 +curl --get "https://api.map.baidu.com/agent_plan/v1/direction" \ + -H "Authorization: Bearer $BAIDU_MAP_AUTH_TOKEN" \ + --data-urlencode "user_raw_request=从王府井驾车去三里屯要多久" \ + --data-urlencode "location=39.914590,116.403770" +``` + +### 3. Geocoding(地理编码) + +#### API + +`GET /agent_plan/v1/geocoding` + +#### 参数输入 + +Required: +- `address`: 要解析的完整地址 + +Optional: +- `region`: 城市/区域提示(减少歧义) + +Rules: +- 地址越完整,解析越稳定 + +#### 鉴权 + +- GET:Header `Authorization: Bearer $BAIDU_MAP_AUTH_TOKEN` + +#### 示例 + +```bash +curl --get "https://api.map.baidu.com/agent_plan/v1/geocoding" \ + -H "Authorization: Bearer $BAIDU_MAP_AUTH_TOKEN" \ + --data-urlencode "address=北京市海淀区上地十街10号百度大厦" \ + --data-urlencode "region=北京市" +``` + +### 4. Reverse Geocoding(逆地理编码) + +#### API + +`GET /agent_plan/v1/reverse_geocoding` + +#### 参数输入 + +Required: +- `location`: `lat,lng` 格式坐标(gcj02) + +Rules: +- 经纬度至少保留小数点后 6 位 + +#### 鉴权 + +- GET:Header `Authorization: Bearer $BAIDU_MAP_AUTH_TOKEN` + +#### 示例 + +```bash +curl --get "https://api.map.baidu.com/agent_plan/v1/reverse_geocoding" \ + -H "Authorization: Bearer $BAIDU_MAP_AUTH_TOKEN" \ + --data-urlencode "location=40.056800,116.308300" +``` + +### 5. Weather(天气查询) + +#### API + +`GET /agent_plan/v1/weather` + +#### 参数输入 + +Optional: +- `region`: 行政区划名称 +- `location`: `lat,lng` 格式坐标(gcj02) + +Rules: +- `region` 与 `location` 至少传一个 +- `location` 传入时,经纬度至少保留小数点后 6 位 + +#### 鉴权 + +- GET:Header `Authorization: Bearer $BAIDU_MAP_AUTH_TOKEN` + +#### 示例 + +```bash +# 1) 按坐标查询天气 +curl --get "https://api.map.baidu.com/agent_plan/v1/weather" \ + -H "Authorization: Bearer $BAIDU_MAP_AUTH_TOKEN" \ + --data-urlencode "location=38.766230,116.432130" +``` + +## 错误处理 + +1. 如果缺少 token,提示用户执行: + 申请地址:https://lbs.baidu.com/apiconsole/agentplan + `export BAIDU_MAP_AUTH_TOKEN=""` +2. 如果提示参数错误,重新阅读本Skills查阅如何传参 \ No newline at end of file diff --git a/agent/skills/baidu-ai-map/_meta.json b/agent/skills/baidu-ai-map/_meta.json new file mode 100644 index 0000000..6b82a79 --- /dev/null +++ b/agent/skills/baidu-ai-map/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn778b6bf766ym71fdvrc5yvg98319rv", + "slug": "baidu-ai-map", + "version": "1.0.6", + "publishedAt": 1776928968682 +} \ No newline at end of file diff --git a/agent/tools/calendar/calendar_info.js b/agent/tools/calendar/calendar_info.js new file mode 100644 index 0000000..7c87047 --- /dev/null +++ b/agent/tools/calendar/calendar_info.js @@ -0,0 +1,42 @@ +import { tool } from "langchain" +import lunar from "lunar-javascript" +const { Solar, Lunar, HolidayUtil, TermUtil } = lunar +import { getAccurateTime } from "./utils.js" + +/** + * 日历工具 + * 提供准确时间、农历信息、节气、节日和老黄历等信息 + */ +export const getCalendarInfoTool = tool( + async () => { + try { + // 获取准确时间 + const timeInfo = await getAccurateTime() + + // 获取农历信息 + const date = new Date(timeInfo.timestamp) + const solar = Solar.fromDate(date) + const lunar = solar.getLunar() + + // 组合返回结果 + const result = { + time: timeInfo, + weekday: solar.getWeek(), + lunar: { + lunar: lunar.toString(), + solar: solar.toString(), + + } + } + + return JSON.stringify(result, null, 2) + } catch (error) { + console.error('Error in calendar tool:', error) + return JSON.stringify({ error: error.message }, null, 2) + } + }, + { + name: "get_calendar_info", + description: "获取当前准确日期时间,以及对应的农历日期" + } +) \ No newline at end of file diff --git a/agent/tools/calendar/lunar_calendar_info.js b/agent/tools/calendar/lunar_calendar_info.js new file mode 100644 index 0000000..d37a5ab --- /dev/null +++ b/agent/tools/calendar/lunar_calendar_info.js @@ -0,0 +1,53 @@ +import { tool } from "langchain" + +/** + * 特定日期日历工具 + * 提供指定日期的日历信息 + */ +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 lunarInfo = {} + + return JSON.stringify(lunarInfo, null, 2) + } catch (error) { + console.error('Error in date calendar tool:', error) + return JSON.stringify({ error: error.message }, null, 2) + } + }, + { + name: "get_lunar_calendar_info", + description: "获取指定日期的日历信息,包括农历信息、老黄历、当年生肖、今日宜忌、喜神方位、财神方位、今日节气、今日节日等", + schema: { + type: "object", + properties: { + year: { + type: "number", + description: "年份", + minimum: 1900, + maximum: 2100 + }, + month: { + type: "number", + description: "月份", + minimum: 1, + maximum: 12 + }, + day: { + type: "number", + description: "日期", + minimum: 1, + maximum: 31 + } + }, + required: ["year", "month", "day"] + } + } +) \ No newline at end of file diff --git a/agent/tools/calendar/utils.js b/agent/tools/calendar/utils.js new file mode 100644 index 0000000..971a5f0 --- /dev/null +++ b/agent/tools/calendar/utils.js @@ -0,0 +1,86 @@ +import * as z from "zod" +import { tool } from "langchain" +import lunar from "lunar-javascript" +const { Solar, Lunar, HolidayUtil, TermUtil } = lunar + +/** + * 获取准确时间工具 + * 通过国内权威时间源获取当前准确时间 + * @returns {string} - 返回当前准确时间的JSON字符串 + */ +export async function getAccurateTime() { + const timeSources = [ + { + name: 'timeapi.io', + url: 'https://timeapi.io/api/Time/current/zone?timeZone=Asia/Shanghai', + method: 'json' + }, + { + name: '国家授时中心', + url: 'http://www.ntsc.ac.cn', + method: 'header' + }, + { + name: '阿里云', + url: 'https://www.aliyun.com', + method: 'header' + }, + { + name: '腾讯云', + url: 'https://cloud.tencent.com', + method: 'header' + }, + { + name: '华为云', + url: 'https://www.huaweicloud.com', + method: 'header' + } + ] + + for (const source of timeSources) { + try { + const startTime = Date.now() + const res = await fetch(source.url) + const latency = Date.now() - startTime + + let timestamp = null + let beijingTime = null + + if (source.method === 'json') { + const data = await res.json() + timestamp = new Date(data.dateTime).getTime() + beijingTime = data.dateTime + } else if (source.method === 'header') { + const dateHeader = res.headers.get('Date') + if (dateHeader) { + timestamp = new Date(dateHeader).getTime() + beijingTime = new Date(timestamp).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }) + } + } + + if (timestamp) { + return { + timestamp, + date: new Date(timestamp).toISOString(), + localTime: beijingTime, + source: source.name, + latency: latency, + success: true + } + } + } catch (error) { + console.log(`从 ${source.name} 获取时间失败: ${error.message}`) + continue + } + } + + const date = new Date() + return { + timestamp: date.getTime(), + date: date.toISOString(), + localTime: date.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }), + success: false, + message: '使用本地时间作为备份' + } +} + diff --git a/agent/tools/calendar/year_holidays.js b/agent/tools/calendar/year_holidays.js new file mode 100644 index 0000000..4954f31 --- /dev/null +++ b/agent/tools/calendar/year_holidays.js @@ -0,0 +1,41 @@ +import { tool } from "langchain" + +/** + * 年份节日列表工具 + * 查询给定年份节日所在日期列表 + */ +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) + } catch (error) { + console.error('Error in year holidays tool:', error) + return JSON.stringify({ error: error.message }, null, 2) + } + }, + { + name: "get_year_holidays", + description: "查询给定年份节日所在日期列表", + schema: { + type: "object", + properties: { + year: { + type: "number", + description: "年份", + minimum: 1900, + maximum: 2100 + } + }, + required: ["year"] + } + } +) \ No newline at end of file diff --git a/agent/tools/calendar/year_terms.js b/agent/tools/calendar/year_terms.js new file mode 100644 index 0000000..da7a38b --- /dev/null +++ b/agent/tools/calendar/year_terms.js @@ -0,0 +1,60 @@ +import { tool } from "langchain" + +/** + * 年份节气列表工具 + * 查询给定年份节气所在日期列表 + */ +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: '大寒' } + ] + + 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) + } + }, + { + name: "get_year_terms", + description: "查询给定年份节气所在日期列表", + schema: { + type: "object", + properties: { + year: { + type: "number", + description: "年份", + minimum: 1900, + maximum: 2100 + } + }, + required: ["year"] + } + } +) \ No newline at end of file diff --git a/agent/tools/geo/latlon.js b/agent/tools/geo/latlon.js new file mode 100644 index 0000000..6741188 --- /dev/null +++ b/agent/tools/geo/latlon.js @@ -0,0 +1,40 @@ +import * as z from "zod" +import { tool } from "langchain" + +/** + * 获取城市经纬度工具 + * @param {string} city - 城市名称 + * @returns {Object|null} - 返回经纬度对象 {lat, lng},如果未找到则返回null + */ +export const getLatLngTool = tool( + async (input) => { + const { city } = input + try { + // 使用open-meteo地理编码API获取经纬度 + console.log('获取城市经纬度:', city) + const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&language=zh&count=1` + const res = await fetch(url) + const data = await res.json() + + if (data.results?.length) { + return { + lat: data.results[0].latitude, + lng: data.results[0].longitude + } + } + + // 如果没有找到结果,返回null + return null + } catch (error) { + console.error('地理编码API调用失败:', error) + return null + } + }, + { + name: "get_lat_lng", + description: `根据城市名称获取经纬度信息`, + schema: z.object({ + city: z.string().describe("城市名称,例如:北京市海淀区") + }) + } +) diff --git a/agent/tools/index.js b/agent/tools/index.js new file mode 100644 index 0000000..c862dde --- /dev/null +++ b/agent/tools/index.js @@ -0,0 +1,20 @@ +// Tool 模块统一导出入口 +// 提供工具基类、注册表和所有工具类的统一导出 + +// Web工具 (LangChain format) +export { webFetchTool } from './web/fetch.js'; +export { webSearchTool } from './web/search.js'; +export { httpGetTool } from './web/http_get.js'; +export { httpPostTool } from './web/http_post.js'; + +// 日历工具 (LangChain format) +export { getCalendarInfoTool } from './calendar/calendar_info.js'; +export { getLunarCalendarInfoTool } from './calendar/lunar_calendar_info.js'; +export { getYearHolidaysTool } from './calendar/year_holidays.js'; +export { getYearTermsTool } from './calendar/year_terms.js'; + +// 地理工具 (LangChain format) +export { getLatLngTool } from './geo/latlon.js'; + +export { getEnvTool } from './system/envs.js'; + diff --git a/agent/tools/system/envs.js b/agent/tools/system/envs.js new file mode 100644 index 0000000..308d8c9 --- /dev/null +++ b/agent/tools/system/envs.js @@ -0,0 +1,82 @@ +// LangChain 环境变量工具 + +import { tool } from '@langchain/core/tools'; +import { z } from 'zod'; + +/** + * 获取环境变量工具 + * @param {Object} input - 输入参数 + * @param {string} [input.name] - 环境变量名,不指定则返回所有 + * @returns {string} - 返回环境变量值或所有环境变量的JSON字符串 + */ +export const getEnvTool = tool( + async (input) => { + const { name } = input || {}; + + if (name) { + const value = process.env[name]; + if (value === undefined) { + return JSON.stringify({ error: `Environment variable '${name}' not found` }); + } + return JSON.stringify({ name, 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))) { + env[key] = '***REDACTED***'; + } + } + + return JSON.stringify({ env }); + }, + { + name: 'env_get', + description: '获取环境变量的值。如果不指定变量名,则返回所有环境变量。', + schema: z.object({ + name: z.string().optional().describe('环境变量名,不指定则返回所有') + }) + } +); + +/** + * 设置环境变量工具 + * @param {Object} input - 输入参数 + * @param {string} input.name - 环境变量名 + * @param {string} input.value - 环境变量值 + * @returns {string} - 返回设置结果的JSON字符串 + */ +export const setEnvTool = tool( + async (input) => { + const { name, value } = input || {}; + + if (!name) { + return JSON.stringify({ error: 'Environment variable name is required' }); + } + + if (value === undefined) { + return JSON.stringify({ error: 'Environment variable value is required' }); + } + + try { + process.env[name] = value; + return JSON.stringify({ success: true, name, value }); + } catch (error) { + return JSON.stringify({ error: `Failed to set environment variable: ${error.message}` }); + } + }, + { + name: 'env_set', + description: '设置环境变量的值。', + schema: z.object({ + name: z.string().describe('环境变量名'), + value: z.string().describe('环境变量值') + }) + } +); + +export default getEnvTool; \ No newline at end of file diff --git a/agent/tools/system/fs_info.js b/agent/tools/system/fs_info.js new file mode 100644 index 0000000..f84a7a7 --- /dev/null +++ b/agent/tools/system/fs_info.js @@ -0,0 +1,257 @@ +import * as z from "zod" +import { tool } from "langchain" +import os from 'os' +import { promisify } from 'util' +import { exec } from 'child_process' + +const execPromise = promisify(exec) + +/** + * 获取系统文件信息工具 + * @returns {string} - 返回系统文件信息的 JSON 字符串,包括分区信息和各分区剩余空间 + */ +export const getSystemFileInfoTool = tool( + async (input) => { + try { + const fileSystemInfo = { + platform: os.platform(), + partitions: [], + volumeInfo: [], + diskHealth: [], + inodeInfo: [] + } + + // 获取文件系统信息(Windows) + if (os.platform() === 'win32') { + try { + const { stdout } = await execPromise('powershell -Command "Get-WmiObject Win32_LogicalDisk | Select-Object DeviceID,Description,FileSystem,Size,FreeSpace,VolumeName | ConvertTo-Json"') + const partitionData = JSON.parse(stdout) + const partitionsArray = Array.isArray(partitionData) ? partitionData : [partitionData] + + fileSystemInfo.partitions = partitionsArray.map(disk => { + const size = disk.Size ? parseInt(disk.Size) : 0 + const freeSpace = disk.FreeSpace ? parseInt(disk.FreeSpace) : 0 + const usedSpace = size - freeSpace + + return { + device: disk.DeviceID || 'N/A', + description: disk.Description || 'N/A', + filesystem: disk.FileSystem || 'N/A', + volumeName: disk.VolumeName || 'N/A', + size: size > 0 ? `${(size / (1024 * 1024 * 1024)).toFixed(2)} GB` : 'N/A', + freeSpace: freeSpace > 0 ? `${(freeSpace / (1024 * 1024 * 1024)).toFixed(2)} GB` : 'N/A', + usedSpace: usedSpace > 0 ? `${(usedSpace / (1024 * 1024 * 1024)).toFixed(2)} GB` : 'N/A', + usePercent: size > 0 ? `${((usedSpace / size) * 100).toFixed(2)}%` : 'N/A' + } + }).filter(item => item) + + // 获取卷详细信息(Windows) + try { + const { stdout } = await execPromise('powershell -Command "Get-WmiObject Win32_LogicalDisk | Select-Object VolumeName,VolumeSerialNumber,FileSystem,MaximumComponentLength,DeviceID,DriveType | ConvertTo-Json"') + const allVolumeData = JSON.parse(stdout) + const volumeArray = Array.isArray(allVolumeData) ? allVolumeData : [allVolumeData] + + fileSystemInfo.volumeInfo = volumeArray.map(vol => ({ + device: vol.DeviceID || 'N/A', + volumeName: vol.VolumeName || 'N/A', + serialNumber: vol.VolumeSerialNumber || 'N/A', + fileSystem: vol.FileSystem || 'N/A', + maxComponentLength: vol.MaximumComponentLength || 'N/A', + deviceID: vol.DeviceID || 'N/A', + driveType: vol.DriveType === 2 ? 'Removable' : vol.DriveType === 3 ? 'Local' : vol.DriveType === 4 ? 'Network' : vol.DriveType === 5 ? 'CD-ROM' : 'Unknown' + })).filter(item => item) + } catch (error) { + console.error('Error getting volume info:', error) + } + + // 获取磁盘健康状态(Windows SMART) + try { + const { stdout } = await execPromise('powershell -Command "Get-WmiObject Win32_DiskDrive | Select-Object Model,Status,SerialNumber,Size,MediaType | ConvertTo-Json"') + const diskData = JSON.parse(stdout) + const diskArray = Array.isArray(diskData) ? diskData : [diskData] + fileSystemInfo.diskHealth = diskArray.map(disk => { + return { + model: disk.Model || 'N/A', + status: disk.Status || 'N/A', + serialNumber: disk.SerialNumber || 'N/A', + size: disk.Size ? `${(parseInt(disk.Size) / (1024 * 1024 * 1024)).toFixed(2)} GB` : 'N/A' + } + }).filter(item => item) + } catch (error) { + console.error('Error getting disk health:', error) + } + } catch (error) { + console.error('Error getting file system info:', error) + fileSystemInfo.error = 'Error getting file system info' + } + } + // 获取文件系统信息(Linux) + else if (os.platform() === 'linux') { + try { + const { stdout } = await execPromise('df -h') + fileSystemInfo.partitions = stdout.trim().split('\n').slice(1).map(line => { + const parts = line.trim().split(/\s+/) + if (parts.length >= 6) { + return { + device: parts[0], + size: parts[1], + usedSpace: parts[2], + freeSpace: parts[3], + usePercent: parts[4], + mountedOn: parts[5] + } + } + return null + }).filter(item => item) + + // 获取挂载选项和文件系统类型(Linux) + try { + const { stdout } = await execPromise('mount | column -t') + const mountLines = stdout.trim().split('\n') + const mountInfo = {} + mountLines.forEach(line => { + const match = line.match(/^(.+?)\s+on\s+(.+?)\s+type\s+(.+?)\s+\((.*)\)$/) + if (match) { + mountInfo[match[2]] = { + device: match[1], + filesystem: match[3], + options: match[4] + } + } + }) + fileSystemInfo.mountInfo = mountInfo + } catch (error) { + console.error('Error getting mount info:', error) + } + + // 获取inode信息(Linux) + try { + const { stdout } = await execPromise('df -i') + fileSystemInfo.inodeInfo = stdout.trim().split('\n').slice(1).map(line => { + const parts = line.trim().split(/\s+/) + if (parts.length >= 6) { + return { + device: parts[0], + totalInodes: parts[1], + usedInodes: parts[2], + freeInodes: parts[3], + usePercent: parts[4], + mountedOn: parts[5] + } + } + return null + }).filter(item => item) + } catch (error) { + console.error('Error getting inode info:', error) + } + + // 获取磁盘健康状态(Linux SMART) + try { + const { stdout } = await execPromise('smartctl --health --json /dev/sda 2>/dev/null || echo "{}"') + if (stdout && stdout.includes('smart_status')) { + const healthData = JSON.parse(stdout) + fileSystemInfo.diskHealth.push({ + device: '/dev/sda', + status: healthData.smart_status?.passed ? 'OK' : 'Failed', + powerOnHours: healthData.power_on_time?.hours || 'N/A', + temperature: healthData.temperature?.current || 'N/A' + }) + } + } catch (error) { + console.log('SMART info not available or requires root') + } + } catch (error) { + console.error('Error getting file system info:', error) + fileSystemInfo.error = 'Error getting file system info' + } + } + // 获取文件系统信息(macOS) + else if (os.platform() === 'darwin') { + try { + const { stdout } = await execPromise('df -h') + fileSystemInfo.partitions = stdout.trim().split('\n').slice(1).map(line => { + const parts = line.trim().split(/\s+/) + if (parts.length >= 6) { + return { + device: parts[0], + size: parts[1], + usedSpace: parts[2], + freeSpace: parts[3], + usePercent: parts[4], + mountedOn: parts[5] + } + } + return null + }).filter(item => item) + + // 获取卷详细信息(macOS) + try { + const diskInfo = await execPromise('diskutil info -all') + const diskLines = diskInfo.stdout.trim().split('\n\n') + diskLines.forEach(disk => { + const volMatch = disk.match(/Volume Name:\s+(.+)/) + const devMatch = disk.match(/Device Node:\s+(.+)/) + const fsMatch = disk.match(/File System Personality:\s+(.+)/) + const serialMatch = disk.match(/Disk UUID:\s+(.+)/) + + if (devMatch) { + fileSystemInfo.volumeInfo.push({ + device: devMatch[1], + volumeName: volMatch ? volMatch[1] : 'N/A', + filesystem: fsMatch ? fsMatch[1] : 'N/A', + serialNumber: serialMatch ? serialMatch[1] : 'N/A' + }) + } + }) + } catch (error) { + console.error('Error getting volume info:', error) + } + + // 获取inode信息(macOS) + try { + const { stdout } = await execPromise('df -i') + fileSystemInfo.inodeInfo = stdout.trim().split('\n').slice(1).map(line => { + const parts = line.trim().split(/\s+/) + if (parts.length >= 6) { + return { + device: parts[0], + totalInodes: parts[1], + usedInodes: parts[2], + freeInodes: parts[3], + usePercent: parts[4], + mountedOn: parts[5] + } + } + return null + }).filter(item => item) + } catch (error) { + console.error('Error getting inode info:', error) + } + + // 获取磁盘健康状态(macOS) + try { + const { stdout } = await execPromise('diskutil smartStatus -all 2>/dev/null || echo "N/A"') + if (stdout && stdout !== 'N/A') { + fileSystemInfo.diskHealth = stdout.trim() + } + } catch (error) { + console.log('SMART status not available') + } + } catch (error) { + console.error('Error getting file system info:', error) + fileSystemInfo.error = 'Error getting file system info' + } + } + + return JSON.stringify(fileSystemInfo, null, 2) + } catch (error) { + console.error('Error getting file system info:', error) + return JSON.stringify({ error: error.message }, null, 2) + } + }, + { + name: "get_system_file_info", + description: `获取系统硬盘分区和文件系统信息,包括分区信息、卷标、序列号、磁盘健康状态、inode信息等`, + schema: z.object({}) + } +) diff --git a/agent/tools/system/hardware_info.js b/agent/tools/system/hardware_info.js new file mode 100644 index 0000000..9edfac6 --- /dev/null +++ b/agent/tools/system/hardware_info.js @@ -0,0 +1,112 @@ +import * as z from "zod" +import { tool } from "langchain" +import os from 'os' +import { promisify } from 'util' +import { exec } from 'child_process' + +const execPromise = promisify(exec) + +/** + * 获取系统硬件配置信息工具 + * @returns {string} - 返回系统硬件配置信息的 JSON 字符串 + */ +export const getHardwareInfoTool = tool( + async (input) => { + try { + const hardwareInfo = { + cpu: { + cores: os.cpus().length, + model: os.cpus()[0].model, + speed: os.cpus()[0].speed + }, + memory: { + total: `${(os.totalmem() / (1024 ** 3)).toFixed(2)} GB`, + free: `${(os.freemem() / (1024 ** 3)).toFixed(2)} GB` + }, + disks: [], + gpu: [] + } + + // 获取硬盘信息(Windows) + if (os.platform() === 'win32') { + try { + const { stdout } = await execPromise('powershell -Command "Get-WmiObject Win32_DiskDrive | Select-Object Model,InterfaceType,Size | ConvertTo-Json"') + const diskData = JSON.parse(stdout) + const diskArray = Array.isArray(diskData) ? diskData : [diskData] + hardwareInfo.disks = diskArray.map(disk => ({ + model: disk.Model || 'N/A', + interfaceType: disk.InterfaceType || 'N/A', + size: disk.Size ? `${(parseInt(disk.Size) / (1024 * 1024 * 1024)).toFixed(2)} GB` : 'N/A' + })).filter(item => item) + } catch (error) { + console.error('Error getting disk info:', error) + } + + // 获取显卡信息(Windows) + try { + const { stdout } = await execPromise('wmic path win32_videocontroller get name') + hardwareInfo.gpu = stdout.trim().split('\n').slice(1).filter(line => line.trim()) + } catch (error) { + console.error('Error getting GPU info:', error) + } + } + // 获取硬盘和显卡信息(Linux) + else if (os.platform() === 'linux') { + try { + const { stdout } = await execPromise('lsblk -o NAME,MODEL,SIZE,TYPE | grep disk') + const diskLines = stdout.trim().split('\n') + hardwareInfo.disks = diskLines.map(line => { + const parts = line.trim().split(/\s+/) + if (parts.length >= 3) { + return { + name: parts[0], + model: parts.slice(1, -1).join(' '), + size: parts[parts.length - 1] + } + } + return null + }).filter(item => item) + } catch (error) { + console.error('Error getting disk info:', error) + } + + try { + const { stdout } = await execPromise('lspci | grep VGA') + hardwareInfo.gpu = stdout.trim().split('\n').map(line => line.trim()) + } catch (error) { + console.error('Error getting GPU info:', error) + } + } + // 获取硬盘和显卡信息(macOS) + else if (os.platform() === 'darwin') { + try { + const { stdout } = await execPromise('diskutil list') + hardwareInfo.disks = [{ + info: stdout.trim() + }] + } catch (error) { + console.error('Error getting disk info:', error) + } + + try { + const { stdout } = await execPromise('system_profiler SPDisplaysDataType') + hardwareInfo.gpu = [{ + info: stdout.trim() + }] + } catch (error) { + console.error('Error getting GPU info:', error) + } + } + + return JSON.stringify(hardwareInfo, null, 2) + } catch (error) { + console.error('Error getting hardware info:', error) + return JSON.stringify({ error: error.message }, null, 2) + } + }, + { + name: "get_hardware_info", + description: `获取系统硬件配置信息,包括CPU、内存、硬盘、显卡等`, + schema: z.object({}) + } +) diff --git a/agent/tools/system/installed_software.js b/agent/tools/system/installed_software.js new file mode 100644 index 0000000..430e4db --- /dev/null +++ b/agent/tools/system/installed_software.js @@ -0,0 +1,101 @@ +import * as z from "zod" +import { tool } from "langchain" +import os from 'os' +import { promisify } from 'util' +import { exec } from 'child_process' + +const execPromise = promisify(exec) + +/** + * 获取已安装软件信息工具 + * @returns {string} - 返回已安装软件信息的 JSON 字符串 + */ +export const getInstalledSoftwareTool = tool( + async (input) => { + try { + const softwareInfo = { + platform: os.platform(), + software: [] + } + + // 获取Windows已安装软件 + if (os.platform() === 'win32') { + try { + const { stdout } = await execPromise('powershell -Command "Get-WmiObject Win32_Product | Select-Object Name,Version,Vendor,InstallDate | ConvertTo-Json"') + const softwareData = JSON.parse(stdout) + const softwareArray = Array.isArray(softwareData) ? softwareData : [softwareData] + softwareInfo.software = softwareArray.map(soft => ({ + name: soft.Name || 'N/A', + version: soft.Version || 'N/A', + vendor: soft.Vendor || 'N/A', + installDate: soft.InstallDate || 'N/A' + })).filter(item => item) + } catch (error) { + console.error('Error getting installed software:', error) + } + } + // 获取Linux已安装软件 + else if (os.platform() === 'linux') { + try { + // 尝试使用dpkg(Debian/Ubuntu) + const { stdout: dpkgOutput } = await execPromise('dpkg -l 2>/dev/null || echo "N/A"') + if (dpkgOutput && dpkgOutput !== 'N/A') { + const lines = dpkgOutput.trim().split('\n').slice(5) + softwareInfo.software = lines.map(line => { + const parts = line.trim().split(/\s+/) + if (parts.length >= 3) { + return { + name: parts[1], + version: parts[2], + description: parts.slice(3).join(' ') + } + } + return null + }).filter(item => item) + } else { + // 尝试使用rpm(RHEL/CentOS) + const { stdout: rpmOutput } = await execPromise('rpm -qa 2>/dev/null || echo "N/A"') + if (rpmOutput && rpmOutput !== 'N/A') { + const lines = rpmOutput.trim().split('\n') + softwareInfo.software = lines.map(line => { + const match = line.match(/(.+)-(.+)-(.+)/) + if (match) { + return { + name: match[1], + version: `${match[2]}-${match[3]}` + } + } + return null + }).filter(item => item) + } + } + } catch (error) { + console.error('Error getting installed software:', error) + } + } + // 获取macOS已安装软件 + else if (os.platform() === 'darwin') { + try { + const { stdout } = await execPromise('find /Applications -name "*.app" -type d | sort') + const apps = stdout.trim().split('\n') + softwareInfo.software = apps.map(app => ({ + name: app.split('/').pop().replace('.app', ''), + path: app + })).filter(item => item) + } catch (error) { + console.error('Error getting installed software:', error) + } + } + + return JSON.stringify(softwareInfo, null, 2) + } catch (error) { + console.error('Error getting installed software:', error) + return JSON.stringify({ error: error.message }, null, 2) + } + }, + { + name: "get_installed_software", + description: `获取系统已安装的软件信息,包括软件名称、版本、供应商等`, + schema: z.object({}) + } +) diff --git a/agent/tools/system/linux_cmd.js b/agent/tools/system/linux_cmd.js new file mode 100644 index 0000000..e69de29 diff --git a/agent/tools/system/os_info.js b/agent/tools/system/os_info.js new file mode 100644 index 0000000..120bd62 --- /dev/null +++ b/agent/tools/system/os_info.js @@ -0,0 +1,89 @@ +import * as z from "zod" +import { tool } from "langchain" +import os from 'os' +import { promisify } from 'util' +import { exec } from 'child_process' + +const execPromise = promisify(exec) + +/** + * 获取操作系统信息工具 + * @returns {string} - 返回操作系统信息的 JSON 字符串 + */ +export const getOsInfoTool = tool( + async (input) => { + try { + const osInfo = { + platform: os.platform(), + arch: os.arch(), + release: os.release(), + hostname: os.hostname(), + uptime: `${Math.round(os.uptime() / 3600)} hours`, + nodeVersion: process.version, + homeDir: os.homedir(), + tempDir: os.tmpdir(), + userInfo: os.userInfo() + } + + // 获取Windows详细信息 + if (os.platform() === 'win32') { + try { + const { stdout } = await execPromise('powershell -Command "Get-WmiObject Win32_OperatingSystem | Select-Object Caption,Version,BuildNumber,OSArchitecture | ConvertTo-Json"') + const winInfo = JSON.parse(stdout) + osInfo.windows = { + caption: winInfo.Caption || 'N/A', + version: winInfo.Version || 'N/A', + buildNumber: winInfo.BuildNumber || 'N/A', + architecture: winInfo.OSArchitecture || 'N/A' + } + } catch (error) { + console.error('Error getting Windows info:', error) + } + } + // 获取Linux详细信息 + else if (os.platform() === 'linux') { + try { + const { stdout } = await execPromise('cat /etc/os-release') + const lines = stdout.trim().split('\n') + const linuxInfo = {} + lines.forEach(line => { + const match = line.match(/(.+?)=(.+)/) + if (match) { + linuxInfo[match[1]] = match[2].replace(/"/g, '') + } + }) + osInfo.linux = linuxInfo + } catch (error) { + console.error('Error getting Linux info:', error) + } + } + // 获取macOS详细信息 + else if (os.platform() === 'darwin') { + try { + const { stdout } = await execPromise('sw_vers') + const lines = stdout.trim().split('\n') + const macInfo = {} + lines.forEach(line => { + const match = line.match(/(.+?):\s+(.+)/) + if (match) { + macInfo[match[1]] = match[2] + } + }) + osInfo.macOS = macInfo + } catch (error) { + console.error('Error getting macOS info:', error) + } + } + + return JSON.stringify(osInfo, null, 2) + } catch (error) { + console.error('Error getting OS info:', error) + return JSON.stringify({ error: error.message }, null, 2) + } + }, + { + name: "get_os_info", + description: `获取操作系统信息,包括平台、架构、版本、主机名、运行时间等`, + schema: z.object({}) + } +) diff --git a/agent/tools/system/os_network_info.js b/agent/tools/system/os_network_info.js new file mode 100644 index 0000000..ae47394 --- /dev/null +++ b/agent/tools/system/os_network_info.js @@ -0,0 +1,242 @@ +import * as z from "zod" +import { tool } from "langchain" +import os from 'os' +import { promisify } from 'util' +import { exec } from 'child_process' + +const execPromise = promisify(exec) + +/** + * 获取系统网卡和网络连接信息工具 + * @returns {string} - 返回网络信息的 JSON 字符串,包括网卡信息和网络连接信息 + */ +export const getNetworkInfoTool = tool( + async (input) => { + try { + const networkInfo = { + platform: os.platform(), + networkInterfaces: [], + dnsServers: [], + gateway: [] + } + + // 获取网络接口信息(跨平台) + const interfaces = os.networkInterfaces() + for (const [name, addresses] of Object.entries(interfaces)) { + networkInfo.networkInterfaces.push({ + name, + addresses: addresses.map(addr => ({ + family: addr.family, + address: addr.address, + netmask: addr.netmask, + mac: addr.mac, + internal: addr.internal, + cidr: addr.cidr + })) + }) + } + + // 获取详细网络信息(Windows) + if (os.platform() === 'win32') { + try { + const { stdout } = await execPromise('ipconfig /all') + const lines = stdout.trim().split('\n') + let currentAdapter = null + + lines.forEach(line => { + const adapterMatch = line.match(/适配器\s+(.+?):/) + if (adapterMatch) { + if (currentAdapter) { + networkInfo.networkInterfaces.push(currentAdapter) + } + currentAdapter = { + name: adapterMatch[1], + description: '', + macAddress: '', + dhcpEnabled: false, + ipAddress: [], + subnetMask: [], + defaultGateway: [], + dnsServers: [] + } + } else if (currentAdapter) { + const descMatch = line.match(/描述\s+(.+)/) + if (descMatch) currentAdapter.description = descMatch[1].trim() + + const macMatch = line.match(/物理地址\s+:\s+(.+)/) + if (macMatch) currentAdapter.macAddress = macMatch[1].trim() + + const dhcpMatch = line.match(/DHCP 已启用\s+:\s+(.+)/) + if (dhcpMatch) currentAdapter.dhcpEnabled = dhcpMatch[1].trim() === '是' + + const ipMatch = line.match(/IPv4 地址\s+:\s+(.+)/) + if (ipMatch) currentAdapter.ipAddress.push(ipMatch[1].trim()) + + const subnetMatch = line.match(/子网掩码\s+:\s+(.+)/) + if (subnetMatch) currentAdapter.subnetMask.push(subnetMatch[1].trim()) + + const gatewayMatch = line.match(/默认网关\s+:\s+(.+)/) + if (gatewayMatch && gatewayMatch[1].trim()) currentAdapter.defaultGateway.push(gatewayMatch[1].trim()) + + const dnsMatch = line.match(/DNS 服务器\s+:\s+(.+)/) + if (dnsMatch && dnsMatch[1].trim()) currentAdapter.dnsServers.push(dnsMatch[1].trim()) + } + }) + + if (currentAdapter) { + networkInfo.networkInterfaces.push(currentAdapter) + } + } catch (error) { + console.error('Error getting network config:', error) + } + + // 获取DNS服务器(Windows) + try { + const { stdout } = await execPromise('nslookup localhost 2>&1') + const dnsMatch = stdout.match(/Server:\s+(.+)/) + if (dnsMatch) { + networkInfo.dnsServers.push(dnsMatch[1].trim()) + } + } catch (error) { + console.error('Error getting DNS servers:', error) + } + } + // 获取详细网络信息(Linux) + else if (os.platform() === 'linux') { + try { + const { stdout } = await execPromise('ip addr show') + const lines = stdout.trim().split('\n') + let currentInterface = null + + lines.forEach(line => { + const interfaceMatch = line.match(/^\d+:\s+(\w+):/) + if (interfaceMatch) { + if (currentInterface) { + networkInfo.networkInterfaces.push(currentInterface) + } + currentInterface = { + name: interfaceMatch[1], + state: '', + macAddress: '', + ipAddress: [], + subnetMask: [] + } + } else if (currentInterface) { + const stateMatch = line.match(/state\s+(\w+)/) + if (stateMatch) currentInterface.state = stateMatch[1] + + const macMatch = line.match(/link\/ether\s+([a-fA-F0-9:]+)/) + if (macMatch) currentInterface.macAddress = macMatch[1] + + const ipMatch = line.match(/inet\s+(\d+\.\d+\.\d+\.\d+)\/(\d+)/) + if (ipMatch) { + currentInterface.ipAddress.push(ipMatch[1]) + const maskLength = parseInt(ipMatch[2]) + currentInterface.subnetMask.push(maskLength) + } + } + }) + + if (currentInterface) { + networkInfo.networkInterfaces.push(currentInterface) + } + } catch (error) { + console.error('Error getting network config:', error) + } + + // 获取DNS服务器(Linux) + try { + const { stdout } = await execPromise('cat /etc/resolv.conf') + const dnsMatches = stdout.match(/nameserver\s+(.+)/g) + if (dnsMatches) { + networkInfo.dnsServers = dnsMatches.map(match => match.replace('nameserver', '').trim()) + } + } catch (error) { + console.error('Error getting DNS servers:', error) + } + + // 获取网关(Linux) + try { + const { stdout } = await execPromise('ip route show default') + const gatewayMatch = stdout.match(/default via (\d+\.\d+\.\d+\.\d+)/) + if (gatewayMatch) { + networkInfo.gateway.push(gatewayMatch[1]) + } + } catch (error) { + console.error('Error getting gateway:', error) + } + } + // 获取详细网络信息(macOS) + else if (os.platform() === 'darwin') { + try { + const { stdout } = await execPromise('ifconfig') + const lines = stdout.trim().split('\n') + let currentInterface = null + + lines.forEach(line => { + const interfaceMatch = line.match(/^(\w+):/) + if (interfaceMatch) { + if (currentInterface) { + networkInfo.networkInterfaces.push(currentInterface) + } + currentInterface = { + name: interfaceMatch[1], + status: '', + macAddress: '', + ipAddress: [], + subnetMask: [] + } + } else if (currentInterface) { + const statusMatch = line.match(/status:\s+(\w+)/) + if (statusMatch) currentInterface.status = statusMatch[1] + + const macMatch = line.match(/ether\s+([a-fA-F0-9:]+)/) + if (macMatch) currentInterface.macAddress = macMatch[1] + + const ipMatch = line.match(/inet\s+(\d+\.\d+\.\d+\.\d+)\s+netmask\s+(0x[0-9a-fA-F]+)/) + if (ipMatch) { + currentInterface.ipAddress.push(ipMatch[1]) + currentInterface.subnetMask.push(ipMatch[2]) + } + } + }) + + if (currentInterface) { + networkInfo.networkInterfaces.push(currentInterface) + } + } catch (error) { + console.error('Error getting network config:', error) + } + + // 获取DNS服务器(macOS) + try { + const { stdout } = await execPromise('scutil --dns | grep nameserver | awk \'{print $3}\'') + networkInfo.dnsServers = stdout.trim().split('\n').filter(dns => dns) + } catch (error) { + console.error('Error getting DNS servers:', error) + } + + // 获取网关(macOS) + try { + const { stdout } = await execPromise('netstat -nr | grep default') + const gatewayMatch = stdout.match(/default\s+(\d+\.\d+\.\d+\.\d+)/) + if (gatewayMatch) { + networkInfo.gateway.push(gatewayMatch[1]) + } + } catch (error) { + console.error('Error getting gateway:', error) + } + } + + return JSON.stringify(networkInfo, null, 2) + } catch (error) { + console.error('Error getting network info:', error) + return JSON.stringify({ error: error.message }, null, 2) + } + }, + { + name: "get_network_info", + description: `获取系统网卡和网络信息,包括网卡配置、IP地址、MAC地址、DNS服务器、网关等`, + schema: z.object({}) + } +) diff --git a/agent/tools/system/os_socket_info.js b/agent/tools/system/os_socket_info.js new file mode 100644 index 0000000..8874aac --- /dev/null +++ b/agent/tools/system/os_socket_info.js @@ -0,0 +1,157 @@ +import * as z from "zod" +import { tool } from "langchain" +import os from 'os' +import { promisify } from 'util' +import { exec } from 'child_process' + +const execPromise = promisify(exec) + +/** + * 获取系统活动连接和Socket信息工具 + * @returns {string} - 返回Socket信息的 JSON 字符串,包括活动连接和监听端口等 + */ +export const getSocketInfoTool = tool( + async (input) => { + try { + const socketInfo = { + platform: os.platform(), + activeConnections: [], + listeningPorts: [] + } + + // 获取活动连接(Windows) + if (os.platform() === 'win32') { + try { + const { stdout } = await execPromise('netstat -ano') + const lines = stdout.trim().split('\n').slice(4) + socketInfo.activeConnections = lines.map(line => { + const parts = line.trim().split(/\s+/) + if (parts.length >= 5) { + return { + protocol: parts[0], + localAddress: parts[1], + foreignAddress: parts[2], + state: parts[3], + processId: parts[4] + } + } + return null + }).filter(item => item) + } catch (error) { + console.error('Error getting active connections:', error) + } + + // 获取监听端口(Windows) + try { + const { stdout } = await execPromise('netstat -an | findstr LISTENING') + const lines = stdout.trim().split('\n') + socketInfo.listeningPorts = lines.map(line => { + const parts = line.trim().split(/\s+/) + if (parts.length >= 4) { + return { + protocol: parts[0], + localAddress: parts[1], + state: parts[3] + } + } + return null + }).filter(item => item) + } catch (error) { + console.error('Error getting listening ports:', error) + } + } + // 获取活动连接(Linux) + else if (os.platform() === 'linux') { + try { + const { stdout } = await execPromise('ss -tuln') + const lines = stdout.trim().split('\n').slice(1) + socketInfo.activeConnections = lines.map(line => { + const parts = line.trim().split(/\s+/) + if (parts.length >= 5) { + return { + protocol: parts[0], + state: parts[1], + localAddress: parts[4], + peerAddress: parts[5] + } + } + return null + }).filter(item => item) + } catch (error) { + console.error('Error getting active connections:', error) + } + + // 获取监听端口(Linux) + try { + const { stdout } = await execPromise('ss -tulpn') + const lines = stdout.trim().split('\n').slice(1) + socketInfo.listeningPorts = lines.map(line => { + const parts = line.trim().split(/\s+/) + if (parts.length >= 6) { + return { + protocol: parts[0], + state: parts[1], + localAddress: parts[4], + processInfo: parts[5] + } + } + return null + }).filter(item => item) + } catch (error) { + console.error('Error getting listening ports:', error) + } + } + // 获取活动连接(macOS) + else if (os.platform() === 'darwin') { + try { + const { stdout } = await execPromise('netstat -an') + const lines = stdout.trim().split('\n').slice(2) + socketInfo.activeConnections = lines.map(line => { + const parts = line.trim().split(/\s+/) + if (parts.length >= 6) { + return { + protocol: parts[0], + localAddress: parts[3], + foreignAddress: parts[4], + state: parts[5] + } + } + return null + }).filter(item => item) + } catch (error) { + console.error('Error getting active connections:', error) + } + + // 获取监听端口(macOS) + try { + const { stdout } = await execPromise('netstat -an | grep LISTEN') + const lines = stdout.trim().split('\n') + socketInfo.listeningPorts = lines.map(line => { + const parts = line.trim().split(/\s+/) + if (parts.length >= 6) { + return { + protocol: parts[0], + localAddress: parts[3], + foreignAddress: parts[4], + state: parts[5] + } + } + return null + }).filter(item => item) + } catch (error) { + console.error('Error getting listening ports:', error) + } + } + + return JSON.stringify(socketInfo, null, 2) + } catch (error) { + console.error('Error getting socket info:', error) + return JSON.stringify({ error: error.message }, null, 2) + } + }, + { + name: "get_socket_info", + description: `获取系统活动连接和Socket信息,包括TCP/UDP连接、监听端口、进程ID等`, + schema: z.object({}) + } +) diff --git a/agent/tools/system/system_time.js b/agent/tools/system/system_time.js new file mode 100644 index 0000000..bd26718 --- /dev/null +++ b/agent/tools/system/system_time.js @@ -0,0 +1,17 @@ +import * as z from "zod" +import { tool } from "langchain" + +/** + * 获取系统时间工具 + * @returns {string} - 返回当前系统时间 + */ +export const getSystemTimeTool = tool( + (input) => { + return new Date().toLocaleString() + }, + { + name: "get_system_time", + description: `获取当前系统时间`, + schema: z.object({}) + } +) diff --git a/agent/tools/system/win_cmd.js b/agent/tools/system/win_cmd.js new file mode 100644 index 0000000..3972ce7 --- /dev/null +++ b/agent/tools/system/win_cmd.js @@ -0,0 +1,74 @@ +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/tools/web/fetch.js b/agent/tools/web/fetch.js new file mode 100644 index 0000000..58fc031 --- /dev/null +++ b/agent/tools/web/fetch.js @@ -0,0 +1,34 @@ +import { TavilyExtract } from "@langchain/tavily"; +import { tool } from "@langchain/core/tools"; +import * as z from "zod" + +const webFetchTool = tool( + async ({ + extractDepth = "basic", + includeImages = false, + format = "markdown", + urls = [], + }) => { + const tavilyExtract = new TavilyExtract({ + tavilyApiKey: process.env.TAVILY_API_KEY, + extractDepth, + includeImages, + format + }); + return await tavilyExtract._call({ urls }); + }, + { + name: "web_fetch", + description: "Run a web fetch", + schema: z.object({ + extractDepth: z.enum(["basic", "advanced"]).default("basic"), + includeImages: z.boolean().default(false), + format: z + .enum(["markdown", "json"]) + .default("markdown"), + urls: z.array(z.string().describe("The urls")).default([]), + }), + }, +); + +export { webFetchTool }; diff --git a/agent/tools/web/http_get.js b/agent/tools/web/http_get.js new file mode 100644 index 0000000..5af87a0 --- /dev/null +++ b/agent/tools/web/http_get.js @@ -0,0 +1,62 @@ +import { tool } from 'langchain'; +import * as z from 'zod'; + +export const httpGetTool = tool( + async ({ url, headers = {}, timeout = 30000 }) => { + try { + new URL(url); + } catch (e) { + return JSON.stringify({ success: false, error: `Invalid URL: ${url}` }); + } + + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + const response = await fetch(url, { + method: 'GET', + headers: { + 'User-Agent': 'XAgent-Bot/1.0', + ...headers + }, + signal: controller.signal + }); + + clearTimeout(timeoutId); + + const contentType = response.headers.get('content-type') || ''; + let body; + + if (contentType.includes('application/json')) { + body = await response.json(); + } else if (contentType.includes('text/') || contentType.includes('application/javascript')) { + body = await response.text(); + } else { + const buffer = await response.arrayBuffer(); + body = Buffer.from(buffer).toString('base64'); + } + + return JSON.stringify({ + success: true, + status: response.status, + statusText: response.statusText, + headers: Object.fromEntries(response.headers.entries()), + body + }); + } catch (error) { + if (error.name === 'AbortError') { + return JSON.stringify({ success: false, error: `Request timed out after ${timeout}ms` }); + } + return JSON.stringify({ success: false, error: error.message }); + } + }, + { + name: 'http_get', + description: 'Send HTTP GET request and return response content. Supports custom headers and timeout settings.', + schema: z.object({ + url: z.string().describe('Request URL'), + headers: z.record(z.string(), z.string()).optional().describe('Request headers'), + timeout: z.number().optional().describe('Timeout in milliseconds, default 30000') + }) + } +); \ No newline at end of file diff --git a/agent/tools/web/http_post.js b/agent/tools/web/http_post.js new file mode 100644 index 0000000..8461991 --- /dev/null +++ b/agent/tools/web/http_post.js @@ -0,0 +1,88 @@ +import { tool } from "@langchain/core/tools"; +import z from "zod"; + +const httpPostTool = tool( + async ({ url, body, headers = {}, contentType = 'json', timeout = 30000 }) => { + try { + new URL(url); + } catch (e) { + return { success: false, error: `Invalid URL: ${url}` }; + } + + let requestBody; + const requestHeaders = { + 'User-Agent': 'XAgent-Bot/1.0', + ...headers + }; + + switch (contentType) { + case 'json': + requestHeaders['Content-Type'] = 'application/json'; + requestBody = typeof body === 'string' ? body : JSON.stringify(body); + break; + case 'form': + requestHeaders['Content-Type'] = 'application/x-www-form-urlencoded'; + if (typeof body === 'object') { + requestBody = new URLSearchParams(body).toString(); + } else { + requestBody = body; + } + break; + case 'text': + requestHeaders['Content-Type'] = 'text/plain'; + requestBody = typeof body === 'string' ? body : JSON.stringify(body); + break; + default: + requestBody = typeof body === 'string' ? body : JSON.stringify(body); + } + + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + const response = await fetch(url, { + method: 'POST', + headers: requestHeaders, + body: requestBody, + signal: controller.signal + }); + + clearTimeout(timeoutId); + + const responseContentType = response.headers.get('content-type') || ''; + let responseBody; + + if (responseContentType.includes('application/json')) { + responseBody = await response.json(); + } else { + responseBody = await response.text(); + } + + return { + success: true, + status: response.status, + statusText: response.statusText, + headers: Object.fromEntries(response.headers.entries()), + body: responseBody + }; + } catch (error) { + if (error.name === 'AbortError') { + return { success: false, error: `Request timed out after ${timeout}ms` }; + } + return { success: false, error: error.message }; + } + }, + { + name: "http_post", + description: "Send HTTP POST request. Supports JSON and form data.", + schema: z.object({ + url: z.string().describe("Request URL"), + body: z.union([z.string(), z.object({})]).describe("Request body, can be string or JSON object"), + headers: z.record(z.string()).optional().describe("Request headers"), + contentType: z.enum(['json', 'form', 'text', 'raw']).optional().describe("Content type, default json"), + timeout: z.number().optional().describe("Timeout in milliseconds, default 30000") + }), + }, +); + +export { httpPostTool }; diff --git a/agent/tools/web/search.js b/agent/tools/web/search.js new file mode 100644 index 0000000..729d8c4 --- /dev/null +++ b/agent/tools/web/search.js @@ -0,0 +1,35 @@ +import { TavilySearch } from "@langchain/tavily"; +import { tool } from "@langchain/core/tools"; +import * as z from "zod" + +const webSearchTool = tool( + async ({ + query, + maxResults = 5, + topic = "general", + includeRawContent = false, + }) => { + const tavilySearch = new TavilySearch({ + maxResults, + tavilyApiKey: process.env.TAVILY_API_KEY, + includeRawContent, + topic, + }); + return await tavilySearch._call({ query }); + }, + { + name: "internet_search", + description: "Run a web search", + schema: z.object({ + query: z.string().describe("The search query"), + maxResults: z.number().optional().default(5), + topic: z + .enum(["general", "news", "finance"]) + .optional() + .default("general"), + includeRawContent: z.boolean().optional().default(false), + }), + }, +); + +export { webSearchTool }; diff --git a/index.js b/index.js index c08b177..734414b 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,4 @@ +import 'dotenv/config'; import { APP } from './app.js' import WebSocketServerManager from './websocket.js' diff --git a/package-lock.json b/package-lock.json index 83b17c0..1d0eef8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,6 @@ "@langchain/openai": "^1.4.7", "@langchain/tavily": "^1.2.0", "bcrypt": "^5.1.1", - "bufferutil": "^4.1.0", "deepagents": "^1.10.2", "dotenv": "^17.4.2", "koa": "^2.16.4", @@ -25,10 +24,10 @@ "koa-router": "^12.0.1", "langchain": "^1.4.2", "lodash": "^4.18.1", + "lunar-javascript": "^1.7.7", "moment": "^2.30.1", "mongoose": "^8.24.0", "node-fetch": "^3.3.2", - "utf-8-validate": "^6.0.6", "winston": "^3.19.0", "ws": "^8.21.0", "zod": "^4.4.3" @@ -2495,6 +2494,12 @@ "node": ">= 12.0.0" } }, + "node_modules/lunar-javascript": { + "version": "1.7.7", + "resolved": "https://registry.npmmirror.com/lunar-javascript/-/lunar-javascript-1.7.7.tgz", + "integrity": "sha512-u/KYiwPIBo/0bT+WWfU7qO1d+aqeB90Tuy4ErXenr2Gam0QcWeezUvtiOIyXR7HbVnW2I1DKfU0NBvzMZhbVQw==", + "license": "MIT" + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-3.1.0.tgz", diff --git a/package.json b/package.json index 95015af..de72f7d 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "koa-router": "^12.0.1", "langchain": "^1.4.2", "lodash": "^4.18.1", + "lunar-javascript": "^1.7.7", "moment": "^2.30.1", "mongoose": "^8.24.0", "node-fetch": "^3.3.2", diff --git a/resource/agreement.js b/resource/agreement.js index 549a9f2..09e5c70 100644 --- a/resource/agreement.js +++ b/resource/agreement.js @@ -10,7 +10,7 @@ const agreement = { "仅支持平台已开通地区医院的代问诊服务,仅限当次就诊门诊当天使用。", "全程陪诊半天不超过4小时,全天不超过8小时。", "预约成功后,平台将提前电话联系确认就诊资料及时间,并以短信或微信通知预约人。", - "请如实填写病情描述,因描述不清导致误诊的,平台不承担责任。", + "陪诊人员只代客户问诊并转达医嘱,不负责诊断和治疗。需要时陪诊员会让医生和客户直接通话。最终医疗听从医嘱为准。", "医生停诊等特殊情况,请及时联系客服处理。" ] },