feat: skills delete — 交互式删除用户 Skill

新增 ias skills delete:
  扫描 skills/ 目录,列出所有用户 Skill
  MultiSelect TUI 选择要删除的 Skill
  确认后 remove_dir_all 物理删除
This commit is contained in:
2026-06-01 22:00:48 +08:00
parent cef8bee8ee
commit 438c52e79b
12 changed files with 1582 additions and 0 deletions
+21
View File
@@ -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.
+269
View File
@@ -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=<encoded_json_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
+427
View File
@@ -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)
@@ -0,0 +1,3 @@
{
"webServiceKey": "your_amap_webservice_key_here"
}
+406
View File
@@ -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<string>} 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);
}
})();
}
+20
View File
@@ -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"
}
}
@@ -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();
@@ -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();
@@ -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();
+17
View File
@@ -0,0 +1,17 @@
---
name: fetch-page
description: Fetch and summarize a web page
argument-hint: <url>
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 35 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
+2
View File
@@ -77,4 +77,6 @@ pub enum SkillsAction {
Import,
/// 列出可导入的 Skill
List,
/// 交互式删除用户 Skill
Delete,
}
+57
View File
@@ -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<String> = 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}");
}
}
}