diff --git a/skills/amap-lbs-skill/LICENSE b/skills/amap-lbs-skill/LICENSE new file mode 100644 index 0000000..3a9c007 --- /dev/null +++ b/skills/amap-lbs-skill/LICENSE @@ -0,0 +1,21 @@ +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 new file mode 100644 index 0000000..8859b52 --- /dev/null +++ b/skills/amap-lbs-skill/README.md @@ -0,0 +1,269 @@ +# 高德地图综合服务 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 new file mode 100644 index 0000000..cc730c3 --- /dev/null +++ b/skills/amap-lbs-skill/SKILL.md @@ -0,0 +1,427 @@ +--- +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 new file mode 100644 index 0000000..25cf0cb --- /dev/null +++ b/skills/amap-lbs-skill/config.example.json @@ -0,0 +1,3 @@ +{ + "webServiceKey": "your_amap_webservice_key_here" +} diff --git a/skills/amap-lbs-skill/index.js b/skills/amap-lbs-skill/index.js new file mode 100644 index 0000000..66ebc3b --- /dev/null +++ b/skills/amap-lbs-skill/index.js @@ -0,0 +1,406 @@ +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 new file mode 100644 index 0000000..f84e156 --- /dev/null +++ b/skills/amap-lbs-skill/package.json @@ -0,0 +1,20 @@ +{ + "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 new file mode 100644 index 0000000..d64ab55 --- /dev/null +++ b/skills/amap-lbs-skill/scripts/poi-search.js @@ -0,0 +1,99 @@ +#!/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 new file mode 100644 index 0000000..f70161a --- /dev/null +++ b/skills/amap-lbs-skill/scripts/route-planning.js @@ -0,0 +1,179 @@ +#!/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 new file mode 100644 index 0000000..fcbbb9e --- /dev/null +++ b/skills/amap-lbs-skill/scripts/travel-planner.js @@ -0,0 +1,82 @@ +#!/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 new file mode 100644 index 0000000..8ad0d17 --- /dev/null +++ b/skills/sample-command/SKILL.md @@ -0,0 +1,17 @@ +--- +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/cli.rs b/src/cli.rs index a6140ed..84f491d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -77,4 +77,6 @@ pub enum SkillsAction { Import, /// 列出可导入的 Skill List, + /// 交互式删除用户 Skill + Delete, } diff --git a/src/main.rs b/src/main.rs index 783c5c0..2eef8bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -814,5 +814,62 @@ async fn cmd_skills(action: SkillsAction) { println!("\n完成:导入 {imported} 个,跳过 {skipped} 个"); println!("重启 iAs 后生效"); } + SkillsAction::Delete => { + let skills_dir = std::path::PathBuf::from("skills"); + if !skills_dir.exists() { + println!("skills 目录不存在,没有可删除的 Skill"); + return; + } + + let mut entries: Vec<(String, String)> = Vec::new(); + if let Ok(dir) = std::fs::read_dir(&skills_dir) { + for entry in dir.flatten() { + let path = entry.path(); + if path.is_dir() && path.join("SKILL.md").exists() { + let name = path.file_name().unwrap_or_default().to_string_lossy().to_string(); + let desc = std::fs::read_to_string(path.join("SKILL.md")) + .ok() + .and_then(|s| s.lines().nth(0).map(|l| l.trim_start_matches("# ").to_string())) + .unwrap_or_default(); + entries.push((name, desc)); + } + } + } + entries.sort_by(|a, b| a.0.cmp(&b.0)); + + if entries.is_empty() { + println!("skills 目录下没有可删除的 Skill"); + return; + } + + let items: Vec = entries.iter() + .map(|(name, desc)| format!("{} — {}", name, desc)) + .collect(); + + println!("选择要删除的 Skill(空格选择,回车确认,q 退出):"); + let selection = MultiSelect::with_theme(&ColorfulTheme::default()) + .items(&items) + .interact_on(&Term::stderr()) + .unwrap_or_default(); + + if selection.is_empty() { + println!("未选择任何 Skill"); + return; + } + + let mut deleted = 0; + for idx in selection { + let (name, _) = &entries[idx]; + let path = skills_dir.join(name); + match std::fs::remove_dir_all(&path) { + Ok(_) => { + println!(" ✅ 已删除 {name}"); + deleted += 1; + } + Err(e) => println!(" ❌ 删除 {name} 失败: {e}"), + } + } + println!("\n完成:删除 {deleted} 个"); + } } }