feat: 导入的 skills 默认存到 ~/.ias/skills/
- Import/Delete 目标改为 ~/.ias/skills/(全局共享) - SkillRegistry::load() 返回 (Self, Vec<String>) 替代 Result 允许部分加载:格式不兼容的 skill 记录警告但不阻止加载 - 新增 global_skills_dir() 辅助函数
This commit is contained in:
@@ -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.
|
||||
@@ -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=<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
|
||||
@@ -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)
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"webServiceKey": "your_amap_webservice_key_here"
|
||||
}
|
||||
@@ -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<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);
|
||||
}
|
||||
})();
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
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 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
|
||||
+22
-17
@@ -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}");
|
||||
|
||||
+6
-11
@@ -210,12 +210,15 @@ 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) => {
|
||||
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);
|
||||
@@ -229,14 +232,6 @@ mod tests {
|
||||
assert!(names.contains(&"list_scheduled_tasks"));
|
||||
assert!(names.contains(&"manage_scheduled_task"));
|
||||
}
|
||||
Err(errors) => {
|
||||
for e in &errors {
|
||||
eprintln!("加载失败: {}", e);
|
||||
}
|
||||
panic!("技能加载失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_basic_skill() {
|
||||
|
||||
@@ -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<Self, Vec<String>> {
|
||||
pub async fn load(system_dir: &str, user_dir: &str) -> (Self, Vec<String>) {
|
||||
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<String>) {
|
||||
|
||||
Reference in New Issue
Block a user