From f7ba23db94726e4c68985c43d5f73cd63b040ed6 Mon Sep 17 00:00:00 2001 From: wunianxiao Date: Tue, 2 Jun 2026 10:02:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AF=BC=E5=85=A5=E7=9A=84=20skills=20?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=AD=98=E5=88=B0=20~/.ias/skills/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Import/Delete 目标改为 ~/.ias/skills/(全局共享) - SkillRegistry::load() 返回 (Self, Vec) 替代 Result 允许部分加载:格式不兼容的 skill 记录警告但不阻止加载 - 新增 global_skills_dir() 辅助函数 --- skills/amap-lbs-skill/LICENSE | 21 - skills/amap-lbs-skill/README.md | 269 ----------- skills/amap-lbs-skill/SKILL.md | 427 ------------------ skills/amap-lbs-skill/config.example.json | 3 - skills/amap-lbs-skill/index.js | 406 ----------------- skills/amap-lbs-skill/package.json | 20 - skills/amap-lbs-skill/scripts/poi-search.js | 99 ---- .../amap-lbs-skill/scripts/route-planning.js | 179 -------- .../amap-lbs-skill/scripts/travel-planner.js | 82 ---- skills/sample-command/SKILL.md | 17 - src/main.rs | 39 +- src/tools/parser.rs | 37 +- src/tools/registry.rs | 11 +- 13 files changed, 40 insertions(+), 1570 deletions(-) delete mode 100644 skills/amap-lbs-skill/LICENSE delete mode 100644 skills/amap-lbs-skill/README.md delete mode 100644 skills/amap-lbs-skill/SKILL.md delete mode 100644 skills/amap-lbs-skill/config.example.json delete mode 100644 skills/amap-lbs-skill/index.js delete mode 100644 skills/amap-lbs-skill/package.json delete mode 100644 skills/amap-lbs-skill/scripts/poi-search.js delete mode 100644 skills/amap-lbs-skill/scripts/route-planning.js delete mode 100644 skills/amap-lbs-skill/scripts/travel-planner.js delete mode 100644 skills/sample-command/SKILL.md diff --git a/skills/amap-lbs-skill/LICENSE b/skills/amap-lbs-skill/LICENSE deleted file mode 100644 index 3a9c007..0000000 --- a/skills/amap-lbs-skill/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 高德地图开放平台 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/skills/amap-lbs-skill/README.md b/skills/amap-lbs-skill/README.md deleted file mode 100644 index 8859b52..0000000 --- a/skills/amap-lbs-skill/README.md +++ /dev/null @@ -1,269 +0,0 @@ -# 高德地图综合服务 Skill - -高德地图综合服务向开发者提供完整的地图数据服务,包括地点搜索、路径规划、旅游规划和数据可视化等功能。 - -## 功能特性 - -- ✅ 自动管理高德 Web Service Key -- ✅ POI 搜索功能 -- ✅ 路径规划(步行、驾车、骑行、公交) -- ✅ 智能旅游规划助手 -- ✅ 地图可视化链接生成 -- ✅ 热力图数据可视化 -- ✅ 支持命令行脚本执行 -- ✅ 配置本地持久化 - -## 安装依赖 - -```bash -npm install -``` - -## 配置 API Key - -首次使用需要配置高德 Web Service Key: - -```bash -# 方式1: 运行时通过环境变量 -export AMAP_WEBSERVICE_KEY=your_key -node scripts/poi-search.js --keywords=肯德基 --city=北京 - -# 方式2: 运行时自动提示输入(会保存到 config.json) -node scripts/poi-search.js --keywords=肯德基 --city=北京 - -# 方式3: 手动创建配置文件 -cp config.example.json config.json -# 然后编辑 config.json 填入你的 Key -``` - -获取 API Key:访问 [高德开放平台](https://lbs.amap.com/api/webservice/create-project-and-key) 创建应用并获取 Key - -## 使用方法 - -### 1. POI 搜索 - -```bash -# 基础搜索 -node scripts/poi-search.js --keywords=肯德基 --city=北京 - -# 带更多参数的搜索 -node scripts/poi-search.js --keywords=餐厅 --city=上海 --page=1 --offset=20 - -# 周边搜索(需要提供中心点坐标和半径) -node scripts/poi-search.js --keywords=酒店 --location=116.397428,39.90923 --radius=1000 -``` - -### 2. 路径规划 - -```bash -# 步行路线 -node scripts/route-planning.js --type=walking --origin=116.397428,39.90923 --destination=116.427281,39.903719 - -# 驾车路线(带途经点) -node scripts/route-planning.js --type=driving --origin=116.397428,39.90923 --destination=116.427281,39.903719 --waypoints=116.410000,39.910000 - -# 骑行路线 -node scripts/route-planning.js --type=riding --origin=116.397428,39.90923 --destination=116.427281,39.903719 - -# 公交路线 -node scripts/route-planning.js --type=transfer --origin=116.397428,39.90923 --destination=116.427281,39.903719 --city=北京 -``` - -### 3. 智能旅游规划 - -```bash -# 基础旅游规划 -node scripts/travel-planner.js --city=北京 --interests=景点,美食,酒店 - -# 指定路线类型 -node scripts/travel-planner.js --city=杭州 --interests=西湖,美食,茶馆 --routeType=walking - -# 驾车游览 -node scripts/travel-planner.js --city=上海 --interests=外滩,南京路,城隍庙 --routeType=driving -``` - -### 4. 在代码中使用 - -```javascript -const { - searchPOI, - walkingRoute, - drivingRoute, - travelPlanner, - generateMapLink -} = require('./index'); - -// POI 搜索 -async function searchExample() { - const result = await searchPOI({ - keywords: '肯德基', - city: '北京', - page: 1, - offset: 10 - }); - console.log(result); -} - -// 步行路线规划 -async function routeExample() { - const result = await walkingRoute({ - origin: '116.397428,39.90923', - destination: '116.427281,39.903719' - }); - console.log(result); -} - -// 旅游规划 -async function travelExample() { - const result = await travelPlanner({ - city: '北京', - interests: ['景点', '美食', '酒店'], - routeType: 'walking' - }); - console.log(result.mapLink); // 地图可视化链接 -} - -// 生成地图链接 -function mapLinkExample() { - const mapData = [ - { - type: 'poi', - lnglat: [116.397428, 39.90923], - sort: '风景名胜', - text: '故宫博物院', - remark: '明清两代的皇家宫殿' - }, - { - type: 'route', - routeType: 'walking', - start: [116.397428, 39.90923], - end: [116.427281, 39.903719], - remark: '步行路线' - } - ]; - - const link = generateMapLink(mapData); - console.log(link); -} -``` - -## API 参数说明 - -### POI 搜索参数 - -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| keywords | string | 是 | 查询关键字 | -| city | string | 否 | 城市名称或城市编码 | -| types | string | 否 | POI类型编码,多个用\|分隔 | -| location | string | 否 | 中心点坐标(经度,纬度) | -| radius | number | 否 | 搜索半径,单位:米 | -| page | number | 否 | 当前页数,默认1 | -| offset | number | 否 | 每页记录数,默认10,最大25 | - -### 路径规划参数 - -#### 步行路线 (walkingRoute) -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| origin | string | 是 | 起点坐标 "经度,纬度" | -| destination | string | 是 | 终点坐标 "经度,纬度" | - -#### 驾车路线 (drivingRoute) -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| origin | string | 是 | 起点坐标 "经度,纬度" | -| destination | string | 是 | 终点坐标 "经度,纬度" | -| waypoints | string | 否 | 途经点,多个用;分隔,最多16个 | -| strategy | number | 否 | 驾车策略,默认10(躲避拥堵) | - -#### 骑行路线 (ridingRoute) -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| origin | string | 是 | 起点坐标 "经度,纬度" | -| destination | string | 是 | 终点坐标 "经度,纬度" | - -#### 公交路线 (transitRoute) -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| origin | string | 是 | 起点坐标 "经度,纬度" | -| destination | string | 是 | 终点坐标 "经度,纬度" | -| city | string | 是 | 城市名称或城市编码 | -| strategy | number | 否 | 公交策略,0-5,默认0(最快捷) | -| nightflag | boolean | 否 | 是否计算夜班车,默认false | - -### 旅游规划参数 (travelPlanner) -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| city | string | 是 | 城市名称 | -| interests | array | 否 | 兴趣点关键词数组,默认['景点','美食'] | -| routeType | string | 否 | 路线类型:walking/driving/riding/transfer,默认walking | - -## 项目结构 - -``` -jsapi-skills/ -├── index.js # 主入口文件,包含核心功能 -├── scripts/ -│ ├── poi-search.js # POI 搜索脚本 -│ ├── route-planning.js # 路径规划脚本 -│ └── travel-planner.js # 智能旅游规划脚本 -├── config.json # 配置文件(自动生成,不要提交) -├── config.example.json # 配置示例 -├── package.json # 依赖配置 -├── .gitignore # Git 忽略配置 -├── SKILL.md # OpenClaw Skill 描述文件 -└── README.md # 本文件 -``` - -## 地图可视化 - -所有规划结果都会生成地图可视化链接,格式如下: - -``` -https://a.amap.com/jsapi_demo_show/static/openclaw/travel_plan.html?data= -``` - -数据格式符合 MapTaskData 接口规范,支持: -- **POI 任务**:展示兴趣点位置和信息 -- **路线任务**:展示路径规划结果 - -示例数据结构: -```javascript -[ - // POI 兴趣点 - { - type: 'poi', - lnglat: [116.397428, 39.90923], - sort: '风景名胜', - text: '故宫博物院', - remark: '明清两代的皇家宫殿,旧称紫禁城。' - }, - // 路线规划 - { - type: 'route', - routeType: 'walking', - start: [116.397428, 39.90923], - end: [116.427281, 39.903719], - remark: '步行路线' - } -] -``` - -## 注意事项 - -1. 请妥善保管你的 Web Service Key,不要提交到公开仓库 -2. `config.json` 已在 `.gitignore` 中,不会被提交 -3. 高德 Web 服务 API 有调用频率限制,请合理使用 -4. 免费用户每日调用量有限制,具体请查看高德开放平台说明 - -## 相关链接 - -- [高德开放平台](https://lbs.amap.com/) -- [创建应用和获取 Key](https://lbs.amap.com/api/webservice/create-project-and-key) -- [POI 搜索 API 文档](https://lbs.amap.com/api/webservice/guide/api-advanced/newpoisearch) -- [Web 服务 API 总览](https://lbs.amap.com/api/webservice/summary) - -## License - -MIT diff --git a/skills/amap-lbs-skill/SKILL.md b/skills/amap-lbs-skill/SKILL.md deleted file mode 100644 index cc730c3..0000000 --- a/skills/amap-lbs-skill/SKILL.md +++ /dev/null @@ -1,427 +0,0 @@ ---- -name: amap-lbs-skill -description: 高德地图综合服务,支持POI搜索、路径规划、旅游规划、周边搜索和热力图数据可视化 -version: 2.0.0 -metadata: - openclaw: - requires: - env: - - AMAP_WEBSERVICE_KEY - bins: - - node - primaryEnv: AMAP_WEBSERVICE_KEY - homepage: https://lbs.amap.com/api/webservice/summary - install: - - kind: node - package: axios - bins: [] ---- - -# 高德地图综合服务 Skill - -高德地图综合服务向开发者提供完整的地图数据服务,包括地点搜索、路径规划、旅游规划和数据可视化等功能。 - -## 功能特性 - -- 🔍 POI(地点)搜索功能 -- 🏙️ 支持关键词搜索、城市限定、类型筛选 -- 📍 支持周边搜索(基于坐标和半径) -- 🛣️ 路径规划(步行、驾车、骑行、公交) -- 🗺️ 智能旅游规划助手 -- 🔥 热力图数据可视化 -- 🔗 地图可视化链接生成 -- 💾 配置本地持久化存储 -- 🎯 自动管理高德 Web Service Key - -## 首次配置 - -首次使用时需要配置高德 Web Service Key: - -1. 访问 [高德开放平台](https://lbs.amap.com/api/webservice/create-project-and-key) 创建应用并获取 Key -2. 设置环境变量:`export AMAP_WEBSERVICE_KEY=your_key` -3. 或运行时自动提示输入并保存到本地配置文件 - -当用户想要搜索地址、地点、周边信息(如美食、酒店、景点等)、规划路线或可视化数据时,使用此 skill。 - -## 触发条件 - -用户表达了以下意图之一: -- 搜索某类地点或某个确定地点(如"搜美食"、"找酒店"、"天安门在哪") -- 基于某个位置搜索周边(如"西直门周边美食"、"北京南站附近酒店") -- 规划路线(如"从天安门到故宫怎么走"、"规划驾车路线") -- 旅游规划(如"帮我规划北京一日游"、"杭州西湖游览路线") -- 包含"搜"、"找"、"查"、"附近"、"周边"、"路线"、"规划"等关键词 -- 希望将地理数据可视化为热力图(如"生成热力图"、"用这份数据做热力图展示") - -## 场景判断 - -收到用户请求后,先判断属于哪个场景: - -- **场景一**:用户搜索一个**明确的类别**(美食、酒店)或**确定的地点**(天安门、西湖),没有指定"在哪个位置附近" -- **场景二**:用户搜索**某个位置周边**的某类地点,输入中同时包含「位置」和「搜索类别」两个要素(如"西直门周边美食"、"北京南站附近酒店") -- **场景三**:热力图数据可视化 -- **场景四**:POI 详细搜索(使用 Web 服务 API) -- **场景五**:路径规划 -- **场景六**:智能旅游规划 - ---- - -## 场景一:明确关键词搜索 - -直接搜索一个类别或地点,不涉及特定位置的周边搜索。 - -**URL 格式:** - -``` -https://www.amap.com/search?query={关键词} -``` - -- **域名**:`www.amap.com` -- **路由**:`/search` -- **参数**:`query` = 搜索关键词 - -### 执行步骤 - -1. **提取关键词**:从用户输入中识别出核心搜索词,去掉"搜"、"找"等修饰词 -2. **生成 URL**:拼接 `https://www.amap.com/search?query={关键词}` -3. **返回链接给用户** - -### 示例 - -| 用户输入 | 提取关键词 | 生成 URL | -|---------|-----------|---------| -| 搜美食 | 美食 | `https://www.amap.com/search?query=美食` | -| 找酒店 | 酒店 | `https://www.amap.com/search?query=酒店` | -| 天安门在哪 | 天安门 | `https://www.amap.com/search?query=天安门` | -| 找个加油站 | 加油站 | `https://www.amap.com/search?query=加油站` | - -### 回复模板 - -``` -🔍 已为你生成高德地图搜索链接: - -https://www.amap.com/search?query={关键词} - -点击链接即可查看搜索结果。 -``` - ---- - -## 场景二:基于位置的周边搜索 - -用户想搜索**某个位置周边**的某类地点。需要先通过地理编码 API 获取该位置的经纬度,再拼接带坐标的搜索链接。 - -**前置条件:** 需要用户提供高德开放平台的 API Key。 - -### 执行步骤 - -#### 第一步:解析用户输入 - -从用户输入中拆分出两个要素: -- **位置**:用户指定的中心位置(如"西直门"、"北京南站") -- **搜索类别**:要搜索的内容(如"美食"、"酒店") - -| 用户输入 | 位置 | 搜索类别 | -|---------|------|---------| -| 西直门周边美食 | 西直门 | 美食 | -| 北京南站附近酒店 | 北京南站 | 酒店 | -| 天坛周边有什么好吃的 | 天坛 | 美食 | - -#### 第二步:检查 API Key - -- 如果用户之前未提供过 Key,**先提示用户提供高德 API Key**,等待用户回复后再继续 -- 如果用户已提供 Key,直接使用 - -**请求 Key 的回复模板:** - -``` -🔑 搜索「{位置}」周边的{搜索类别}需要使用高德 API,请提供你的高德开放平台 API Key。 - -(如果还没有 Key,可以在 https://lbs.amap.com 注册并创建应用获取) -``` - -#### 第三步:调用地理编码 API 获取经纬度 - -**API 格式:** - -``` -https://restapi.amap.com/v3/geocode/geo?address={位置}&output=JSON&key={用户的key} -``` - -**执行 curl 请求:** - -```bash -curl -s "https://restapi.amap.com/v3/geocode/geo?address={位置}&output=JSON&key={用户的key}" -``` - -**API 返回示例:** - -```json -{ - "status": "1", - "info": "OK", - "geocodes": [ - { - "formatted_address": "北京市西城区西直门", - "location": "116.353138,39.939385" - } - ] -} -``` - -从返回结果中提取 `geocodes[0].location`,格式为 `经度,纬度`(如 `116.353138,39.939385`),拆分为: -- **经度(longitude)**:`116.353138` -- **纬度(latitude)**:`39.939385` - -#### 第四步:拼接带坐标的搜索链接 - -**URL 格式:** - -``` -https://ditu.amap.com/search?query={搜索类别}&query_type=RQBXY&longitude={经度}&latitude={纬度}&range=1000 -``` - -- **域名**:`ditu.amap.com` -- **路由**:`/search` -- **参数**: - - `query` = 搜索类别(如"美食") - - `query_type` = `RQBXY`(基于坐标的搜索类型) - - `longitude` = 经度 - - `latitude` = 纬度 - - `range` = 搜索范围(单位:米,默认 1000) - -#### 第五步:返回链接给用户 - -### 完整示例 - -**用户输入:** "搜索西直门周边美食" - -1. 解析:位置 = `西直门`,搜索类别 = `美食` -2. 调用地理编码 API:`curl -s "https://restapi.amap.com/v3/geocode/geo?address=西直门&output=JSON&key=xxx"` -3. 获取坐标:`116.353138,39.939385` → 经度 `116.353138`,纬度 `39.939385` -4. 拼接链接:`https://ditu.amap.com/search?query=美食&query_type=RQBXY&longitude=116.353138&latitude=39.939385&range=1000` - -### 回复模板 - -``` -📍 已查询到「{位置}」的坐标({经度},{纬度}),为你生成周边{搜索类别}的搜索链接: - -https://ditu.amap.com/search?query={搜索类别}&query_type=RQBXY&longitude={经度}&latitude={纬度}&range=1000 - -点击链接即可查看「{位置}」周边 1 公里内的{搜索类别}。 -``` - ---- - -## 场景三:热力图展示 - -用户有一份包含地理坐标的数据,希望在地图上以热力图的形式可视化展示。 - -### 触发条件 - -用户提到"热力图"、"数据可视化"、"地图上展示数据"等意图,并提供了数据地址。 - -### URL 格式 - -``` -http://a.amap.com/jsapi_demo_show/static/openclaw/heatmap.html?mapStyle={地图风格}&dataUrl={数据地址(URL编码)} -``` - -- **域名**:`a.amap.com` -- **路由**:`/jsapi_demo_show/static/openclaw/heatmap.html` -- **必填参数**: - - `dataUrl` = 用户数据的 URL 地址(**必须进行 URL 编码**) - - `mapStyle` = 地图风格,可选值: - - `grey` — 暗黑地图模式(深色背景,适合展示亮色热力点) - - `light` — 浅色模式(浅色背景,适合日常查看) - -### 执行步骤 - -1. **获取数据地址**:从用户输入中提取数据 URL,如果用户未提供,提示用户给出数据地址 -2. **确认地图风格**:询问用户偏好的地图风格(`grey` 或 `light`),如果用户未指定,默认使用 `grey` -3. **URL 编码**:将数据地址进行 URL 编码(将 `://` → `%3A%2F%2F`,`/` → `%2F` 等) -4. **拼接链接**:生成完整的热力图 URL -5. **返回链接给用户** - -### 示例 - -**用户输入:** "帮我用这份数据生成热力图:`https://a.amap.com/Loca/static/loca-v2/demos/mock_data/hz_house_order.json`,用暗黑模式" - -1. 数据地址:`https://a.amap.com/Loca/static/loca-v2/demos/mock_data/hz_house_order.json` -2. 地图风格:`grey` -3. URL 编码后的数据地址:`https%3A%2F%2Fa.amap.com%2FLoca%2Fstatic%2Floca-v2%2Fdemos%2Fmock_data%2Fhz_house_order.json` -4. 最终链接: - -``` -http://a.amap.com/jsapi_demo_show/static/openclaw/heatmap.html?mapStyle=grey&dataUrl=https%3A%2F%2Fa.amap.com%2FLoca%2Fstatic%2Floca-v2%2Fdemos%2Fmock_data%2Fhz_house_order.json -``` - -### 回复模板 - -``` -🔥 已为你生成热力图链接: - -http://a.amap.com/jsapi_demo_show/static/openclaw/heatmap.html?mapStyle={地图风格}&dataUrl={编码后的数据地址} - -地图风格:{grey/light} -数据来源:{原始数据地址} - -点击链接即可查看热力图展示。 -``` - -**请求数据地址的回复模板(用户未提供时):** - -``` -🔥 生成热力图需要你提供数据地址(JSON 格式的 URL),请给出数据链接。 - -另外,你希望使用哪种地图风格? -- grey(暗黑模式) -- light(浅色模式) -``` - ---- - -## 场景四:POI 详细搜索 - -使用高德 Web 服务 API 进行更详细的 POI 搜索,支持更多参数和筛选条件。 - -### 使用方法 - -```bash -# 基础搜索 -node scripts/poi-search.js --keywords=肯德基 --city=北京 - -# 搜索更多结果 -node scripts/poi-search.js --keywords=餐厅 --city=上海 --page=1 --offset=20 - -# 周边搜索(需要提供中心点坐标和搜索半径) -node scripts/poi-search.js --keywords=酒店 --location=116.397428,39.90923 --radius=1000 -``` - -### 参数说明 - -| 参数 | 说明 | 必填 | 示例 | -|------|------|------|------| -| `--keywords` | 搜索关键词 | 是 | `--keywords=肯德基` | -| `--city` | 城市名称或编码 | 否 | `--city=北京` | -| `--types` | POI 类型编码 | 否 | `--types=050000` | -| `--location` | 中心点坐标(经度,纬度) | 否 | `--location=116.397428,39.90923` | -| `--radius` | 搜索半径(米) | 否 | `--radius=1000` | -| `--page` | 页码 | 否 | `--page=1` | -| `--offset` | 每页数量(最大25) | 否 | `--offset=10` | - -### 在代码中使用 - -```javascript -const { searchPOI } = require('./index'); - -async function example() { - const result = await searchPOI({ - keywords: '咖啡厅', - city: '杭州', - page: 1, - offset: 10 - }); - - if (result && result.pois) { - result.pois.forEach(poi => { - console.log(`${poi.name} - ${poi.address}`); - }); - } -} - -example(); -``` - ---- - -## 场景五:路径规划 - -规划不同出行方式的路线。 - -### 使用方法 - -```bash -# 步行路线 -node scripts/route-planning.js --type=walking --origin=116.397428,39.90923 --destination=116.427281,39.903719 - -# 驾车路线 -node scripts/route-planning.js --type=driving --origin=116.397428,39.90923 --destination=116.427281,39.903719 - -# 公交路线 -node scripts/route-planning.js --type=transfer --origin=116.397428,39.90923 --destination=116.427281,39.903719 --city=北京 -``` - -### 路线类型 - -- `walking` - 步行路线 -- `driving` - 驾车路线 -- `riding` - 骑行路线 -- `transfer` - 公交路线(需要指定城市) - ---- - -## 场景六:智能旅游规划 - -自动搜索兴趣点并规划游览路线,生成地图可视化链接。 - -### 使用方法 - -```bash -# 基础旅游规划 -node scripts/travel-planner.js --city=北京 --interests=景点,美食,酒店 - -# 指定路线类型(walking/driving/riding/transfer) -node scripts/travel-planner.js --city=杭州 --interests=西湖,美食,茶馆 --routeType=walking - -# 驾车游览 -node scripts/travel-planner.js --city=上海 --interests=外滩,南京路,城隍庙 --routeType=driving -``` - -### 功能说明 - -- 自动搜索指定城市的兴趣点(每类最多5个) -- 按顺序规划各兴趣点之间的路线 - - - ---- - -## 配置管理 - -配置文件位于 `config.json`,包含以下内容: - -```json -{ - "webServiceKey": "your_amap_webservice_key_here" -} -``` - -设置 Key 的方式: - -1. **环境变量**:`export AMAP_WEBSERVICE_KEY=your_key` -2. **命令行参数**:`node index.js your_key` -3. **自动提示**:首次运行时自动提示输入 -4. **手动编辑**:直接编辑 `config.json` 文件 - ---- - -## 注意事项 - -- **场景判断是关键**:区分用户是"直接搜某个东西"、"在某个位置附近搜某个东西"、"规划路线"还是"旅游规划" -- 关键词应尽量精简准确,提取用户真正想搜的内容 -- URL 中的中文关键词浏览器会自动处理编码,无需手动 encode -- 场景二、四、五、六需要用户提供高德 API Key,**必须先获取 Key 后再发起请求**,不能跳过 -- 如果地理编码 API 返回 `status` 不为 `"1"`,说明请求失败,需提示用户检查 Key 是否正确或地址是否有效 -- API 返回的 `location` 格式为 `经度,纬度`(注意:经度在前,纬度在后) -- 场景二的搜索范围默认 1000 米,用户如有需要可调整 `range` 参数 -- 请妥善保管你的 Web Service Key,不要分享给他人 -- 高德 Web 服务 API 有调用频率限制,请合理使用 -- 免费用户每日调用量有限制,具体请查看高德开放平台说明 - -## 相关链接 - -- [高德开放平台](https://lbs.amap.com/) -- [创建应用和获取 Key](https://lbs.amap.com/api/webservice/create-project-and-key) -- [POI 搜索 API 文档](https://lbs.amap.com/api/webservice/guide/api-advanced/newpoisearch) -- [Web 服务 API 总览](https://lbs.amap.com/api/webservice/summary) \ No newline at end of file diff --git a/skills/amap-lbs-skill/config.example.json b/skills/amap-lbs-skill/config.example.json deleted file mode 100644 index 25cf0cb..0000000 --- a/skills/amap-lbs-skill/config.example.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "webServiceKey": "your_amap_webservice_key_here" -} diff --git a/skills/amap-lbs-skill/index.js b/skills/amap-lbs-skill/index.js deleted file mode 100644 index 66ebc3b..0000000 --- a/skills/amap-lbs-skill/index.js +++ /dev/null @@ -1,406 +0,0 @@ -const fs = require('fs'); -const path = require('path'); -const axios = require('axios'); - -// 配置文件路径 -const CONFIG_FILE = path.join(__dirname, 'config.json'); - -/** - * 读取配置文件 - */ -function readConfig() { - try { - if (fs.existsSync(CONFIG_FILE)) { - const data = fs.readFileSync(CONFIG_FILE, 'utf8'); - return JSON.parse(data); - } - } catch (error) { - console.error('读取配置文件失败:', error.message); - } - return {}; -} - -/** - * 保存配置文件 - */ -function saveConfig(config) { - try { - fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8'); - console.log('配置已保存到:', CONFIG_FILE); - return true; - } catch (error) { - console.error('保存配置文件失败:', error.message); - return false; - } -} - -/** - * 获取高德 Web Service Key - */ -function getWebServiceKey() { - const config = readConfig(); - return config.webServiceKey || null; -} - -/** - * 设置高德 Web Service Key - */ -function setWebServiceKey(key) { - const config = readConfig(); - config.webServiceKey = key; - return saveConfig(config); -} - -/** - * 检查并提示用户输入 Key - */ -async function ensureWebServiceKey() { - // 优先从环境变量读取 - let key = process.env.AMAP_KEY || process.env.AMAP_WEBSERVICE_KEY; - - if (!key) { - // 尝试从配置文件读取 - key = getWebServiceKey(); - } - - if (!key) { - console.log('\n⚠️ 未找到高德 Web Service Key'); - console.log('请访问以下地址创建应用并获取 Key:'); - console.log('https://lbs.amap.com/api/webservice/create-project-and-key\n'); - throw new Error('请设置环境变量 AMAP_KEY 或提供高德 Web Service Key'); - } - - return key; -} - -/** - * POI 搜索 - * @param {Object} params - 搜索参数 - * @param {string} params.keywords - 查询关键字 - * @param {string} params.city - 城市名称或城市编码 - * @param {string} params.types - POI类型编码 - * @param {string} params.location - 中心点坐标 - * @param {number} params.radius - 搜索半径(米) - * @param {number} params.page - 当前页数 - * @param {number} params.offset - 每页记录数 - */ -async function searchPOI(params) { - const key = await ensureWebServiceKey(); - - const url = 'https://restapi.amap.com/v5/place/text'; - - const requestParams = { - key: key, - keywords: params.keywords || '', - region: params.city || '', - city_limit: params.cityLimit !== false, - ...params - }; - - try { - console.log('🔍 正在搜索 POI...'); - const response = await axios.get(url, { params: requestParams }); - - if (response.data.status === '1') { - console.log(`✅ 搜索成功,共找到 ${response.data.count} 条结果\n`); - return response.data; - } else { - console.error('❌ 搜索失败:', response.data.info); - return null; - } - } catch (error) { - console.error('❌ 请求失败:', error.message); - return null; - } -} - -/** - * 步行路径规划 - * @param {Object} params - 规划参数 - * @param {string} params.origin - 起点坐标 "经度,纬度" - * @param {string} params.destination - 终点坐标 "经度,纬度" - */ -async function walkingRoute(params) { - const key = await ensureWebServiceKey(); - - const url = 'https://restapi.amap.com/v3/direction/walking'; - - const requestParams = { - key: key, - origin: params.origin, - destination: params.destination - }; - - try { - console.log('🚶 正在规划步行路线...'); - const response = await axios.get(url, { params: requestParams }); - - if (response.data.status === '1') { - console.log('✅ 步行路线规划成功\n'); - return response.data; - } else { - console.error('❌ 步行路线规划失败:', response.data.info); - return null; - } - } catch (error) { - console.error('❌ 请求失败:', error.message); - return null; - } -} - -/** - * 驾车路径规划 - * @param {Object} params - 规划参数 - * @param {string} params.origin - 起点坐标 "经度,纬度" - * @param {string} params.destination - 终点坐标 "经度,纬度" - * @param {string} params.waypoints - 途经点坐标,多个用;分隔 - * @param {number} params.strategy - 驾车策略,默认10 - */ -async function drivingRoute(params) { - const key = await ensureWebServiceKey(); - - const url = 'https://restapi.amap.com/v3/direction/driving'; - - const requestParams = { - key: key, - origin: params.origin, - destination: params.destination, - strategy: params.strategy || 10, - extensions: 'base' - }; - - if (params.waypoints) { - requestParams.waypoints = params.waypoints; - } - - try { - console.log('🚗 正在规划驾车路线...'); - const response = await axios.get(url, { params: requestParams }); - - if (response.data.status === '1') { - console.log('✅ 驾车路线规划成功\n'); - return response.data; - } else { - console.error('❌ 驾车路线规划失败:', response.data.info); - return null; - } - } catch (error) { - console.error('❌ 请求失败:', error.message); - return null; - } -} - -/** - * 骑行路径规划 - * @param {Object} params - 规划参数 - * @param {string} params.origin - 起点坐标 "经度,纬度" - * @param {string} params.destination - 终点坐标 "经度,纬度" - */ -async function ridingRoute(params) { - const key = await ensureWebServiceKey(); - - const url = 'https://restapi.amap.com/v4/direction/bicycling'; - - const requestParams = { - key: key, - origin: params.origin, - destination: params.destination - }; - - try { - console.log('🚴 正在规划骑行路线...'); - const response = await axios.get(url, { params: requestParams }); - - if (response.data.errcode === 0) { - console.log('✅ 骑行路线规划成功\n'); - return response.data; - } else { - console.error('❌ 骑行路线规划失败:', response.data.errmsg); - return null; - } - } catch (error) { - console.error('❌ 请求失败:', error.message); - return null; - } -} - -/** - * 公交路径规划 - * @param {Object} params - 规划参数 - * @param {string} params.origin - 起点坐标 "经度,纬度" - * @param {string} params.destination - 终点坐标 "经度,纬度" - * @param {string} params.city - 城市名称或城市编码 - * @param {number} params.strategy - 公交策略,默认0(最快捷) - * @param {boolean} params.nightflag - 是否计算夜班车,默认false - */ -async function transitRoute(params) { - const key = await ensureWebServiceKey(); - - const url = 'https://restapi.amap.com/v3/direction/transit/integrated'; - - const requestParams = { - key: key, - origin: params.origin, - destination: params.destination, - city: params.city, - strategy: params.strategy || 0, - nightflag: params.nightflag ? 1 : 0 - }; - - try { - console.log('🚌 正在规划公交路线...'); - const response = await axios.get(url, { params: requestParams }); - - if (response.data.status === '1') { - console.log('✅ 公交路线规划成功\n'); - return response.data; - } else { - console.error('❌ 公交路线规划失败:', response.data.info); - return null; - } - } catch (error) { - console.error('❌ 请求失败:', error.message); - return null; - } -} - -/** - * 生成地图可视化链接 - * @param {Array} mapTaskData - 地图任务数据数组 - * @returns {string} 可视化链接 - */ -function generateMapLink(mapTaskData) { - const baseUrl = 'https://a.amap.com/jsapi_demo_show/static/openclaw/travel_plan.html'; - const dataStr = encodeURIComponent(JSON.stringify(mapTaskData)); - return `${baseUrl}?data=${dataStr}`; -} - -/** - * 旅游规划助手 - * @param {Object} params - 规划参数 - * @param {string} params.city - 城市名称 - * @param {Array} params.interests - 兴趣点关键词数组,如 ['景点', '美食', '酒店'] - * @param {string} params.routeType - 路线类型:driving/walking/riding/transfer - * @returns {Object} 包含 pois、mapTaskData、mapLink 和 htmlLink - */ -async function travelPlanner(params) { - const { city, interests = [], routeType = 'walking' } = params; - - console.log(`\n🗺️ 开始为您规划 ${city} 的旅游行程...\n`); - - const mapTaskData = []; - const poiResults = []; - - // 搜索各类兴趣点 - for (const interest of interests) { - console.log(`📍 搜索 ${interest}...`); - const result = await searchPOI({ - keywords: interest, - city: city, - page: 1, - offset: 5 - }); - - if (result && result.pois && result.pois.length > 0) { - poiResults.push(...result.pois); - - // 添加到地图数据 - 严格按照 PoiTask 接口格式 - result.pois.forEach(poi => { - const [lng, lat] = poi.location.split(',').map(Number); - mapTaskData.push({ - type: 'poi', - lnglat: [lng, lat], - sort: poi.type || interest, - text: poi.name, - remark: poi.address || `${interest}推荐` - }); - }); - } - } - - // 如果有多个POI,规划路线 - if (poiResults.length >= 2) { - console.log(`\n🛣️ 规划游览路线(${routeType})...\n`); - - for (let i = 0; i < poiResults.length - 1; i++) { - const start = poiResults[i]; - const end = poiResults[i + 1]; - - const [startLng, startLat] = start.location.split(',').map(Number); - const [endLng, endLat] = end.location.split(',').map(Number); - - // 添加路线到地图数据 - 严格按照 RouteTask 接口格式 - const routeTask = { - type: 'route', - routeType: routeType, - start: [startLng, startLat], - end: [endLng, endLat], - remark: `从 ${start.name} 到 ${end.name}` - }; - - // 如果是公交路线,添加 city 参数 - if (routeType === 'transfer') { - routeTask.city = city; - } - - mapTaskData.push(routeTask); - } - } - - - console.log('\n✅ 旅游规划完成!\n'); - console.log('📍 推荐地点:'); - poiResults.forEach((poi, index) => { - console.log(`${index + 1}. ${poi.name}`); - console.log(` 地址: ${poi.address}`); - console.log(` 类型: ${poi.type}\n`); - }); - - return { - pois: poiResults, - }; -} - -// 导出函数供其他脚本使用 -module.exports = { - readConfig, - saveConfig, - getWebServiceKey, - setWebServiceKey, - ensureWebServiceKey, - searchPOI, - walkingRoute, - drivingRoute, - ridingRoute, - transitRoute, - generateMapLink, - travelPlanner -}; - -// 如果直接运行此文件,执行示例搜索 -if (require.main === module) { - (async () => { - try { - // 示例:搜索北京的肯德基 - const result = await searchPOI({ - keywords: '肯德基', - city: '北京', - page: 1, - offset: 10 - }); - - if (result && result.pois) { - console.log('搜索结果:'); - result.pois.forEach((poi, index) => { - console.log(`${index + 1}. ${poi.name}`); - console.log(` 地址: ${poi.address}`); - console.log(` 类型: ${poi.type}`); - console.log(` 坐标: ${poi.location}\n`); - }); - } - } catch (error) { - console.error('执行失败:', error.message); - process.exit(1); - } - })(); -} diff --git a/skills/amap-lbs-skill/package.json b/skills/amap-lbs-skill/package.json deleted file mode 100644 index f84e156..0000000 --- a/skills/amap-lbs-skill/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "amap-webservice", - "version": "1.0.0", - "description": "高德Web服务API向开发者提供HTTP接口,开发者可通过这些接口使用各类型的地理数据服务,返回结果支持JSON和XML格式。", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [ - "amap", - "webservice", - "poi", - "geocoding" - ], - "author": "", - "license": "MIT", - "dependencies": { - "axios": "^1.13.6" - } -} diff --git a/skills/amap-lbs-skill/scripts/poi-search.js b/skills/amap-lbs-skill/scripts/poi-search.js deleted file mode 100644 index d64ab55..0000000 --- a/skills/amap-lbs-skill/scripts/poi-search.js +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env node - -/** - * POI 搜索脚本 - * 使用方法: - * node scripts/poi-search.js --keywords=肯德基 --city=北京 - * 或设置环境变量: AMAP_KEY=your_key node scripts/poi-search.js --keywords=肯德基 - */ - -const { searchPOI } = require('../index'); - -// 解析命令行参数 -function parseArgs() { - const args = {}; - process.argv.slice(2).forEach(arg => { - if (arg.startsWith('--')) { - const [key, value] = arg.slice(2).split('='); - args[key] = value; - } - }); - return args; -} - -// 主函数 -async function main() { - const args = parseArgs(); - - // 检查必需参数 - if (!args.keywords) { - console.error('❌ 缺少必需参数: --keywords'); - console.log('\n使用方法:'); - console.log('node scripts/poi-search.js --keywords=关键词 [--city=城市] [--types=类型] [--page=页码] [--offset=每页数量]'); - console.log('\n示例:'); - console.log('node scripts/poi-search.js --keywords=肯德基 --city=北京 --page=1 --offset=20'); - process.exit(1); - } - - // 检查是否设置了 AMAP_KEY - if (!process.env.AMAP_KEY) { - console.error('❌ 请设置环境变量 AMAP_KEY'); - console.log('\n示例:'); - console.log('export AMAP_KEY=your_amap_key'); - console.log('node scripts/poi-search.js --keywords=美食 --location=116.397428,39.90923 --radius=1000'); - process.exit(1); - } - - // 构建搜索参数 - const params = { - keywords: args.keywords, - city: args.city || '', - types: args.types || '', - page: parseInt(args.page) || 1, - offset: parseInt(args.offset) || 10 - }; - - // 可选参数 - if (args.location) params.location = args.location; - if (args.radius) params.radius = parseInt(args.radius); - if (args.cityLimit !== undefined) params.cityLimit = args.cityLimit === 'true'; - - try { - const result = await searchPOI(params); - - if (result && result.pois && result.pois.length > 0) { - console.log(`\n📍 共找到 ${result.count} 条结果,当前显示第 ${params.page} 页:\n`); - console.log('='.repeat(80)); - - result.pois.forEach((poi, index) => { - const num = (params.page - 1) * params.offset + index + 1; - console.log(`\n${num}. ${poi.name}`); - console.log(` 📍 地址: ${poi.address || '无'}`); - console.log(` 🏷️ 类型: ${poi.type}`); - console.log(` 📞 电话: ${poi.tel || '无'}`); - console.log(` 🗺️ 坐标: ${poi.location}`); - if (poi.distance) { - console.log(` 📏 距离: ${poi.distance}米`); - } - }); - - console.log('\n' + '='.repeat(80)); - - // 输出分页信息 - const totalPages = Math.ceil(result.count / params.offset); - console.log(`\n第 ${params.page}/${totalPages} 页`); - - if (params.page < totalPages) { - console.log(`\n💡 查看下一页: node scripts/poi-search.js --keywords=${params.keywords} --city=${params.city} --page=${params.page + 1}`); - } - } else { - console.log('\n❌ 未找到相关结果'); - } - } catch (error) { - console.error('\n❌ 执行失败:', error.message); - process.exit(1); - } -} - -// 执行主函数 -main(); diff --git a/skills/amap-lbs-skill/scripts/route-planning.js b/skills/amap-lbs-skill/scripts/route-planning.js deleted file mode 100644 index f70161a..0000000 --- a/skills/amap-lbs-skill/scripts/route-planning.js +++ /dev/null @@ -1,179 +0,0 @@ -#!/usr/bin/env node - -/** - * 路径规划脚本 - * 使用方法: - * node scripts/route-planning.js --type=walking --origin=116.397428,39.90923 --destination=116.427281,39.903719 - */ - -const { walkingRoute, drivingRoute, ridingRoute, transitRoute, generateMapLink } = require('../index'); - -// 解析命令行参数 -function parseArgs() { - const args = {}; - process.argv.slice(2).forEach(arg => { - if (arg.startsWith('--')) { - const [key, value] = arg.slice(2).split('='); - args[key] = value; - } - }); - return args; -} - -// 主函数 -async function main() { - const args = parseArgs(); - - // 检查必需参数 - if (!args.type || !args.origin || !args.destination) { - console.error('❌ 缺少必需参数'); - console.log('\n使用方法:'); - console.log('node scripts/route-planning.js --type=路线类型 --origin=起点坐标 --destination=终点坐标 [其他参数]'); - console.log('\n路线类型:'); - console.log(' walking - 步行'); - console.log(' driving - 驾车'); - console.log(' riding - 骑行'); - console.log(' transfer - 公交(需要额外提供 --city 参数)'); - console.log('\n示例:'); - console.log('# 步行路线'); - console.log('node scripts/route-planning.js --type=walking --origin=116.397428,39.90923 --destination=116.427281,39.903719'); - console.log('\n# 驾车路线(带途经点)'); - console.log('node scripts/route-planning.js --type=driving --origin=116.397428,39.90923 --destination=116.427281,39.903719 --waypoints=116.410000,39.910000'); - console.log('\n# 公交路线'); - console.log('node scripts/route-planning.js --type=transfer --origin=116.397428,39.90923 --destination=116.427281,39.903719 --city=北京'); - process.exit(1); - } - - const { type, origin, destination } = args; - - try { - let result = null; - let mapTaskData = []; - - // 解析起点和终点坐标 - const [originLng, originLat] = origin.split(',').map(Number); - const [destLng, destLat] = destination.split(',').map(Number); - - // 根据类型调用不同的路径规划API - switch (type) { - case 'walking': - result = await walkingRoute({ origin, destination }); - if (result && result.route) { - mapTaskData.push({ - type: 'route', - routeType: 'walking', - start: [originLng, originLat], - end: [destLng, destLat], - remark: `步行路线,距离约 ${(result.route.paths[0].distance / 1000).toFixed(2)} 公里` - }); - - console.log('📊 路线信息:'); - console.log(` 距离: ${(result.route.paths[0].distance / 1000).toFixed(2)} 公里`); - console.log(` 预计时间: ${Math.round(result.route.paths[0].duration / 60)} 分钟\n`); - } - break; - - case 'driving': - const drivingParams = { origin, destination }; - if (args.waypoints) { - drivingParams.waypoints = args.waypoints; - } - if (args.strategy) { - drivingParams.strategy = parseInt(args.strategy); - } - - result = await drivingRoute(drivingParams); - if (result && result.route) { - const path = result.route.paths[0]; - mapTaskData.push({ - type: 'route', - routeType: 'driving', - start: [originLng, originLat], - end: [destLng, destLat], - remark: `驾车路线,距离约 ${(path.distance / 1000).toFixed(2)} 公里` - }); - - console.log('📊 路线信息:'); - console.log(` 距离: ${(path.distance / 1000).toFixed(2)} 公里`); - console.log(` 预计时间: ${Math.round(path.duration / 60)} 分钟`); - console.log(` 过路费: ${path.tolls || 0} 元`); - console.log(` 红绿灯: ${path.traffic_lights || 0} 个\n`); - } - break; - - case 'riding': - result = await ridingRoute({ origin, destination }); - if (result && result.data) { - const path = result.data.paths[0]; - mapTaskData.push({ - type: 'route', - routeType: 'riding', - start: [originLng, originLat], - end: [destLng, destLat], - remark: `骑行路线,距离约 ${(path.distance / 1000).toFixed(2)} 公里` - }); - - console.log('📊 路线信息:'); - console.log(` 距离: ${(path.distance / 1000).toFixed(2)} 公里`); - console.log(` 预计时间: ${Math.round(path.duration / 60)} 分钟\n`); - } - break; - - case 'transfer': - if (!args.city) { - console.error('❌ 公交路线规划需要提供 --city 参数'); - process.exit(1); - } - - const transitParams = { - origin, - destination, - city: args.city, - strategy: args.strategy ? parseInt(args.strategy) : 0, - nightflag: args.nightflag === 'true' - }; - - result = await transitRoute(transitParams); - if (result && result.route) { - mapTaskData.push({ - type: 'route', - routeType: 'transfer', - start: [originLng, originLat], - end: [destLng, destLat], - city: args.city, - remark: `公交路线,共 ${result.route.transits.length} 个方案` - }); - - console.log('📊 路线信息:'); - console.log(` 方案数量: ${result.route.transits.length} 个`); - if (result.route.transits.length > 0) { - const transit = result.route.transits[0]; - console.log(` 预计时间: ${Math.round(transit.duration / 60)} 分钟`); - console.log(` 费用: ${transit.cost} 元`); - console.log(` 步行距离: ${transit.walking_distance} 米\n`); - } - } - break; - - default: - console.error(`❌ 不支持的路线类型: ${type}`); - process.exit(1); - } - - if (result && mapTaskData.length > 0) { - // 生成地图链接 - const mapLink = generateMapLink(mapTaskData); - console.log('🗺️ 地图可视化链接:'); - console.log(mapLink); - console.log('\n💡 提示: 复制链接到浏览器打开即可查看路线详情\n'); - } else { - console.log('\n❌ 路线规划失败,请检查参数是否正确'); - } - } catch (error) { - console.error('\n❌ 执行失败:', error.message); - process.exit(1); - } -} - -// 执行主函数 -main(); diff --git a/skills/amap-lbs-skill/scripts/travel-planner.js b/skills/amap-lbs-skill/scripts/travel-planner.js deleted file mode 100644 index fcbbb9e..0000000 --- a/skills/amap-lbs-skill/scripts/travel-planner.js +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env node - -/** - * 旅游规划脚本 - * 使用方法: - * node scripts/travel-planner.js --city=北京 --interests=景点,美食,酒店 --routeType=walking - */ - -const { travelPlanner } = require('../index'); - -// 解析命令行参数 -function parseArgs() { - const args = {}; - process.argv.slice(2).forEach(arg => { - if (arg.startsWith('--')) { - const [key, value] = arg.slice(2).split('='); - args[key] = value; - } - }); - return args; -} - -// 主函数 -async function main() { - const args = parseArgs(); - - // 检查必需参数 - if (!args.city) { - console.error('❌ 缺少必需参数: --city'); - console.log('\n使用方法:'); - console.log('node scripts/travel-planner.js --city=城市名 [--interests=兴趣点1,兴趣点2] [--routeType=路线类型]'); - console.log('\n示例:'); - console.log('node scripts/travel-planner.js --city=北京 --interests=景点,美食,酒店 --routeType=walking'); - console.log('\n路线类型选项:'); - console.log(' walking - 步行(默认)'); - console.log(' driving - 驾车'); - console.log(' riding - 骑行'); - console.log(' transfer - 公交'); - process.exit(1); - } - - // 解析兴趣点 - const interests = args.interests ? args.interests.split(',') : ['景点', '美食']; - const routeType = args.routeType || 'walking'; - - // 验证路线类型 - const validRouteTypes = ['walking', 'driving', 'riding', 'transfer']; - if (!validRouteTypes.includes(routeType)) { - console.error(`❌ 无效的路线类型: ${routeType}`); - console.log('有效的路线类型:', validRouteTypes.join(', ')); - process.exit(1); - } - - try { - const result = await travelPlanner({ - city: args.city, - interests: interests, - routeType: routeType - }); - - if (result && result.pois.length > 0) { - console.log('═'.repeat(80)); - console.log('\n📊 规划数据统计:'); - console.log(` 兴趣点数量: ${result.pois.length} 个`); - console.log(` 路线数量: ${result.mapTaskData.filter(item => item.type === 'route').length} 条`); - console.log(` 出行方式: ${routeType === 'walking' ? '步行' : routeType === 'driving' ? '驾车' : routeType === 'riding' ? '骑行' : '公交'}`); - console.log('\n' + '═'.repeat(80)); - console.log('\n🎉 规划完成!\n'); - console.log('📋 数据格式符合 MapTaskData 接口规范'); - console.log(' - PoiTask: 兴趣点任务'); - console.log(' - RouteTask: 路线规划任务\n'); - } else { - console.log('\n❌ 未找到相关地点,请尝试更换关键词或城市。'); - } - } catch (error) { - console.error('\n❌ 执行失败:', error.message); - process.exit(1); - } -} - -// 执行主函数 -main(); diff --git a/skills/sample-command/SKILL.md b/skills/sample-command/SKILL.md deleted file mode 100644 index 8ad0d17..0000000 --- a/skills/sample-command/SKILL.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -name: fetch-page -description: Fetch and summarize a web page -argument-hint: -user-invocable: true ---- - -# Fetch Page - -Fetch the content of a URL and produce a concise summary. - -## Process - -1. Fetch the URL passed as `$1` using the `web_fetch` tool -2. Summarize the main content in 3–5 sentences, preserving key facts and figures -3. List any notable links or resources mentioned in the page -4. If the URL is inaccessible, report the error and suggest checking the URL format diff --git a/src/main.rs b/src/main.rs index 73d6807..3837a33 100644 --- a/src/main.rs +++ b/src/main.rs @@ -206,15 +206,13 @@ async fn cmd_listen( } // 加载系统 Skills - let skill_registry = match SkillRegistry::load("resources/skills", "skills").await { - Ok(r) => { - info!("已加载 {} 个技能", r.count()); - Some(Arc::new(r)) - } - Err(e) => { - info!("技能加载结果: {:?}", e); - None + let (skill_registry, _warnings) = SkillRegistry::load("resources/skills", "skills").await; + let skill_registry = { + if !_warnings.is_empty() { + info!("技能加载警告: {:?}", _warnings); } + info!("已加载 {} 个技能", skill_registry.count()); + Some(Arc::new(skill_registry)) }; // 审批管理器 @@ -795,10 +793,19 @@ async fn cmd_send( } } +fn global_skills_dir() -> std::path::PathBuf { + dirs::home_dir() + .unwrap_or_else(|| std::path::PathBuf::from(".")) + .join(".ias") + .join("skills") +} + async fn cmd_skills(action: SkillsAction) { use dialoguer::{MultiSelect, theme::ColorfulTheme}; use console::Term; + let global_dir = global_skills_dir(); + match action { SkillsAction::List => { let discovered = skills_importer::discover_skills(); @@ -833,8 +840,8 @@ async fn cmd_skills(action: SkillsAction) { return; } - let dest = std::path::PathBuf::from("skills"); - std::fs::create_dir_all(&dest).ok(); + let dest = &global_dir; + std::fs::create_dir_all(dest).ok(); let mut imported = 0; let mut skipped = 0; @@ -852,18 +859,16 @@ async fn cmd_skills(action: SkillsAction) { } } } - println!("\n完成:导入 {imported} 个,跳过 {skipped} 个"); - println!("重启 iAs 后生效"); + println!("\n完成:导入 {imported} 个,跳过 {skipped} 个 (→ {})", global_dir.display()); } SkillsAction::Delete => { - let skills_dir = std::path::PathBuf::from("skills"); - if !skills_dir.exists() { - println!("skills 目录不存在,没有可删除的 Skill"); + if !global_dir.exists() { + println!("全局 skills 目录不存在,没有可删除的 Skill"); return; } let mut entries: Vec<(String, String)> = Vec::new(); - if let Ok(dir) = std::fs::read_dir(&skills_dir) { + if let Ok(dir) = std::fs::read_dir(&global_dir) { for entry in dir.flatten() { let path = entry.path(); if path.is_dir() && path.join("SKILL.md").exists() { @@ -901,7 +906,7 @@ async fn cmd_skills(action: SkillsAction) { let mut deleted = 0; for idx in selection { let (name, _) = &entries[idx]; - let path = skills_dir.join(name); + let path = global_dir.join(name); match std::fs::remove_dir_all(&path) { Ok(_) => { println!(" ✅ 已删除 {name}"); diff --git a/src/tools/parser.rs b/src/tools/parser.rs index c10616d..44901b1 100644 --- a/src/tools/parser.rs +++ b/src/tools/parser.rs @@ -210,32 +210,27 @@ mod tests { #[test] fn test_load_all_system_skills() { let rt = tokio::runtime::Runtime::new().unwrap(); - let result = rt.block_on(crate::tools::registry::SkillRegistry::load( + let (registry, errors) = rt.block_on(crate::tools::registry::SkillRegistry::load( "resources/skills", "/nonexistent", )); - match result { - Ok(registry) => { - assert_eq!(registry.count(), 9, "应该有 9 个系统技能"); - let names: Vec<&str> = registry.list_specs().iter().map(|s| s.name.as_str()).collect(); - println!("已加载技能: {:?}", names); - assert!(names.contains(&"manage_memos")); - assert!(names.contains(&"manage_skill")); - assert!(names.contains(&"get_current_datetime")); - assert!(names.contains(&"query_weather")); - assert!(names.contains(&"search_web")); - assert!(names.contains(&"email")); - assert!(names.contains(&"execute_shell")); - assert!(names.contains(&"list_scheduled_tasks")); - assert!(names.contains(&"manage_scheduled_task")); - } - Err(errors) => { - for e in &errors { - eprintln!("加载失败: {}", e); - } - panic!("技能加载失败"); + if !errors.is_empty() { + for e in &errors { + eprintln!("加载警告: {}", e); } } + assert_eq!(registry.count(), 9, "应该有 9 个系统技能"); + let names: Vec<&str> = registry.list_specs().iter().map(|s| s.name.as_str()).collect(); + println!("已加载技能: {:?}", names); + assert!(names.contains(&"manage_memos")); + assert!(names.contains(&"manage_skill")); + assert!(names.contains(&"get_current_datetime")); + assert!(names.contains(&"query_weather")); + assert!(names.contains(&"search_web")); + assert!(names.contains(&"email")); + assert!(names.contains(&"execute_shell")); + assert!(names.contains(&"list_scheduled_tasks")); + assert!(names.contains(&"manage_scheduled_task")); } #[test] diff --git a/src/tools/registry.rs b/src/tools/registry.rs index 54d44eb..c183778 100644 --- a/src/tools/registry.rs +++ b/src/tools/registry.rs @@ -22,10 +22,7 @@ impl SkillRegistry { } /// 从 system_dir 和 user_dir 加载所有技能 - /// system_dir: resources/skills/(随 iAs 分发) - /// user_dir: skills/(用户自行添加) - /// 同名时 user 覆盖 system - pub async fn load(system_dir: &str, user_dir: &str) -> Result> { + pub async fn load(system_dir: &str, user_dir: &str) -> (Self, Vec) { let mut registry = Self::new(); let mut errors = Vec::new(); @@ -47,11 +44,7 @@ impl SkillRegistry { registry.load_from_dir(user_dir, SkillSource::User, &mut errors); } - if errors.is_empty() { - Ok(registry) - } else { - Err(errors) - } + (registry, errors) } fn load_from_dir(&mut self, dir: &str, source: SkillSource, errors: &mut Vec) {