@tencent-map/lbs-skills 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +143 -0
  2. package/bin/cli.js +503 -0
  3. package/lib/index.js +474 -0
  4. package/package.json +29 -0
package/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # @tencent-map/lbs-skills
2
+
3
+ 腾讯地图位置服务命令行工具,支持 POI 搜索、地理编码、周边搜索、路径规划、旅游规划、轨迹可视化等功能。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install -g @tencent-map/lbs-skills
9
+ ```
10
+
11
+ ## 快速开始
12
+
13
+ ### 设置 API Key
14
+
15
+ ```bash
16
+ # 方式一:命令行设置(持久化到 ~/.tmap-lbs/config.json)
17
+ tmap-lbs config set-key YOUR_KEY
18
+
19
+ # 方式二:环境变量(当前会话有效)
20
+ export TMAP_LBS_KEY=YOUR_KEY
21
+ ```
22
+
23
+ 获取 Key:https://lbs.qq.com/dev/console/application/mine
24
+
25
+ ### 查看帮助
26
+
27
+ ```bash
28
+ tmap-lbs --help
29
+ tmap-lbs <command> --help
30
+ ```
31
+
32
+ ## 命令列表
33
+
34
+ ### `config` — 管理 API Key
35
+
36
+ ```bash
37
+ tmap-lbs config set-key <key> # 保存 Key
38
+ tmap-lbs config get-key # 查看当前 Key
39
+ tmap-lbs config remove-key # 删除 Key
40
+ tmap-lbs config path # 显示配置文件路径
41
+ ```
42
+
43
+ ### `search` — POI 搜索
44
+
45
+ ```bash
46
+ # 城市关键词搜索
47
+ tmap-lbs search --keywords 肯德基 --city 北京
48
+
49
+ # 周边坐标搜索
50
+ tmap-lbs search --keywords 酒店 --location 116.397,39.909 --radius 1000
51
+
52
+ # 带分类筛选
53
+ tmap-lbs search --keywords 餐厅 --city 上海 --types 餐饮
54
+
55
+ # 输出完整 JSON
56
+ tmap-lbs search --keywords 肯德基 --city 北京 --raw
57
+ ```
58
+
59
+ ### `geocode` — 地理编码
60
+
61
+ ```bash
62
+ tmap-lbs geocode --address 西直门
63
+ tmap-lbs geocode --address 北京天安门
64
+ ```
65
+
66
+ ### `nearby` — 周边搜索(生成可视化链接)
67
+
68
+ 生成腾讯地图周边搜索可视化链接,点击即可在地图上查看结果:
69
+
70
+ ```bash
71
+ tmap-lbs nearby --location 西直门 --keywords 美食
72
+ tmap-lbs nearby --location 北京南站 --keywords 酒店
73
+ tmap-lbs nearby --keyword 天坛餐厅
74
+ ```
75
+
76
+ 如需获取结构化 POI 数据,使用 `search` + `geocode` 组合:
77
+
78
+ ```bash
79
+ tmap-lbs geocode --address 西直门
80
+ tmap-lbs search --keywords 美食 --location 116.353,39.939 --radius 1000
81
+ ```
82
+
83
+ ### `route` — 路径规划
84
+
85
+ 支持出行方式:`walk`(步行)、`drive`(驾车)、`cycle`(骑行)、`ecycle`(电动车)、`transit`(公交)
86
+
87
+ ```bash
88
+ # 步行
89
+ tmap-lbs route --mode walk --origin 116.397,39.909 --destination 116.427,39.903
90
+
91
+ # 驾车(带策略)
92
+ tmap-lbs route --mode drive --origin 116.397,39.909 --destination 116.427,39.903 --policy LEAST_TIME
93
+
94
+ # 驾车(带途经点和车牌号)
95
+ tmap-lbs route --mode drive --origin 116.397,39.909 --destination 116.427,39.903 \
96
+ --waypoints "116.41,39.91;116.42,39.92" --plate-number 京A12345
97
+
98
+ # 公交
99
+ tmap-lbs route --mode transit --origin 116.397,39.909 --destination 116.427,39.903 --policy LEAST_TRANSFER
100
+ ```
101
+
102
+ ### `travel` — 旅游规划(生成可视化链接)
103
+
104
+ 自动地理编码获取景点坐标,生成旅游规划可视化链接:
105
+
106
+ ```bash
107
+ tmap-lbs travel --city 北京 --interests 故宫,颐和园,香山
108
+ tmap-lbs travel --city 杭州 --interests 西湖,灵隐寺,龙井茶 --recommend restaurant
109
+ ```
110
+
111
+ ### `trail` — 轨迹可视化(生成可视化链接)
112
+
113
+ ```bash
114
+ tmap-lbs trail --data https://mapapi.qq.com/web/claw/trail.json
115
+ ```
116
+
117
+ ## 编程接口
118
+
119
+ 也可以作为 Node.js 模块在代码中使用:
120
+
121
+ ```js
122
+ const { searchPOI, geocode, walkRoute } = require('@tencent-map/lbs-skills');
123
+
124
+ // 搜索
125
+ const result = await searchPOI({ keywords: '肯德基', city: '北京' });
126
+
127
+ // 地理编码
128
+ const geo = await geocode({ address: '西直门' });
129
+
130
+ // 路线规划
131
+ const route = await walkRoute({
132
+ origin: '116.397,39.909',
133
+ destination: '116.427,39.903',
134
+ });
135
+ ```
136
+
137
+ ## 坐标格式
138
+
139
+ 所有命令中的坐标参数统一使用 **"经度,纬度"** 格式(经度在前),工具内部会自动转换为腾讯地图 API 所需的 "纬度,经度" 格式。
140
+
141
+ ## License
142
+
143
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,503 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const lib = require('../lib/index.js');
5
+
6
+ // ─── 工具函数 ────────────────────────────────────────────────────────
7
+
8
+ function printJSON(obj, raw) {
9
+ if (raw) {
10
+ console.log(JSON.stringify(obj, null, 2));
11
+ } else {
12
+ // 去掉 _raw 字段的简洁输出
13
+ const clean = JSON.parse(JSON.stringify(obj));
14
+ delete clean._raw;
15
+ console.log(JSON.stringify(clean, null, 2));
16
+ }
17
+ }
18
+
19
+ function fatal(msg) {
20
+ console.error(`\n❌ ${msg}\n`);
21
+ process.exit(1);
22
+ }
23
+
24
+ function parseArgs(argv) {
25
+ const args = {};
26
+ const positional = [];
27
+ for (let i = 0; i < argv.length; i++) {
28
+ const arg = argv[i];
29
+ if (arg.startsWith('--')) {
30
+ const key = arg.slice(2);
31
+ const next = argv[i + 1];
32
+ if (next && !next.startsWith('--')) {
33
+ args[key] = next;
34
+ i++;
35
+ } else {
36
+ args[key] = true;
37
+ }
38
+ } else {
39
+ positional.push(arg);
40
+ }
41
+ }
42
+ return { args, positional };
43
+ }
44
+
45
+ // ─── 帮助信息 ────────────────────────────────────────────────────────
46
+
47
+ const HELP = `
48
+ tmap-lbs — 腾讯地图位置服务 CLI
49
+
50
+ Usage:
51
+ tmap-lbs <command> [options]
52
+
53
+ Commands:
54
+ config 管理 API Key 配置
55
+ search POI 搜索(关键词/城市/周边)
56
+ geocode 地理编码(地址 → 坐标)
57
+ nearby 周边搜索(生成地图可视化链接)
58
+ route 路径规划(步行/驾车/骑行/公交等)
59
+ travel 旅游规划(生成地图可视化链接)
60
+ trail 轨迹可视化(生成地图可视化链接)
61
+
62
+ Options:
63
+ --help 显示帮助信息
64
+ --raw 输出包含原始 API 响应的完整 JSON
65
+ --json 以 JSON 格式输出(默认就是 JSON)
66
+
67
+ Run \`tmap-lbs <command> --help\` for command-specific usage.
68
+
69
+ 配置 Key:
70
+ tmap-lbs config set-key <your-key>
71
+ 或 export TMAP_LBS_KEY=<your-key>
72
+
73
+ 获取 Key: https://lbs.qq.com/dev/console/application/mine
74
+ `;
75
+
76
+ const HELP_CONFIG = `
77
+ tmap-lbs config — 管理 API Key
78
+
79
+ Usage:
80
+ tmap-lbs config set-key <key> 保存 API Key
81
+ tmap-lbs config get-key 查看当前 Key
82
+ tmap-lbs config remove-key 删除已保存的 Key
83
+ tmap-lbs config path 显示配置文件路径
84
+ `;
85
+
86
+ const HELP_SEARCH = `
87
+ tmap-lbs search — POI 搜索
88
+
89
+ Usage:
90
+ tmap-lbs search --keywords <关键词> [options]
91
+
92
+ Options:
93
+ --keywords <kw> 搜索关键词(必填)
94
+ --city <city> 限定城市
95
+ --location <lng,lat> 中心点坐标(经度,纬度)
96
+ --radius <meters> 搜索半径(米),与 --location 配合使用
97
+ --types <type> POI 分类筛选
98
+ --page <n> 页码(默认 1)
99
+ --page-size <n> 每页数量(默认 10,最大 20)
100
+ --raw 输出完整原始 JSON
101
+
102
+ Examples:
103
+ tmap-lbs search --keywords 肯德基 --city 北京
104
+ tmap-lbs search --keywords 酒店 --location 116.397,39.909 --radius 1000
105
+ tmap-lbs search --keywords 餐厅 --city 上海 --types 餐饮
106
+ `;
107
+
108
+ const HELP_GEOCODE = `
109
+ tmap-lbs geocode — 地理编码
110
+
111
+ Usage:
112
+ tmap-lbs geocode --address <地址>
113
+
114
+ Options:
115
+ --address <addr> 地址关键词(必填)
116
+ --raw 输出完整原始 JSON
117
+
118
+ Examples:
119
+ tmap-lbs geocode --address 西直门
120
+ tmap-lbs geocode --address 北京天安门
121
+ `;
122
+
123
+ const HELP_NEARBY = `
124
+ tmap-lbs nearby — 生成周边搜索可视化链接
125
+
126
+ 生成腾讯地图周边搜索的可视化链接,用户点击即可在地图上查看结果。
127
+
128
+ Usage:
129
+ tmap-lbs nearby --location <位置名称> --keywords <搜索类别>
130
+
131
+ Options:
132
+ --location <name> 位置名称(如"西直门"),与 --keywords 组合为搜索词
133
+ --keywords <kw> 搜索关键词(如"美食")
134
+ --keyword <kw> 直接指定完整搜索关键词(如"西直门美食")
135
+
136
+ Examples:
137
+ tmap-lbs nearby --location 西直门 --keywords 美食
138
+ tmap-lbs nearby --keyword 北京南站酒店
139
+ tmap-lbs nearby --location 天坛 --keywords 餐厅
140
+ `;
141
+
142
+ const HELP_ROUTE = `
143
+ tmap-lbs route — 路径规划
144
+
145
+ Usage:
146
+ tmap-lbs route --mode <mode> --origin <lng,lat> --destination <lng,lat> [options]
147
+
148
+ Modes:
149
+ walk 步行
150
+ drive 驾车
151
+ cycle 骑行(自行车)
152
+ ecycle 电动车
153
+ transit 公交
154
+
155
+ Common Options:
156
+ --origin <lng,lat> 起点坐标(经度,纬度,必填)
157
+ --destination <lng,lat> 终点坐标(经度,纬度,必填)
158
+ --mode <mode> 出行方式(默认 walk)
159
+ --raw 输出完整原始 JSON
160
+
161
+ Drive Options:
162
+ --waypoints <lng,lat;...> 途经点,多个用 ; 分隔
163
+ --policy <policy> 策略:LEAST_TIME|LEAST_FEE|AVOID_HIGHWAY|HIGHWAY_FIRST
164
+ --plate-number <plate> 车牌号(用于避开限行)
165
+
166
+ Transit Options:
167
+ --policy <policy> 策略:LEAST_TIME|LEAST_TRANSFER|LEAST_WALKING|RECOMMEND
168
+ --departure-time <ts> 出发时间(Unix 时间戳)
169
+
170
+ Examples:
171
+ tmap-lbs route --mode walk --origin 116.397,39.909 --destination 116.427,39.903
172
+ tmap-lbs route --mode drive --origin 116.397,39.909 --destination 116.427,39.903 --policy LEAST_TIME
173
+ tmap-lbs route --mode transit --origin 116.397,39.909 --destination 116.427,39.903
174
+ `;
175
+
176
+ const HELP_TRAVEL = `
177
+ tmap-lbs travel — 生成旅游规划可视化链接
178
+
179
+ 通过地理编码获取景点坐标,生成腾讯地图旅游规划可视化链接。
180
+
181
+ Usage:
182
+ tmap-lbs travel --city <城市> --interests <景点1,景点2,...> [options]
183
+
184
+ Options:
185
+ --city <city> 城市名称(必填)
186
+ --interests <kw1,kw2,...> 景点/兴趣关键词,逗号分隔(必填)
187
+ --recommend <types> 推荐类型,逗号分隔(如 restaurant,hotel)
188
+ --raw 同时输出 JSON 数据
189
+
190
+ Examples:
191
+ tmap-lbs travel --city 北京 --interests 故宫,颐和园,香山
192
+ tmap-lbs travel --city 杭州 --interests 西湖,灵隐寺,龙井茶 --recommend restaurant
193
+ `;
194
+
195
+ const HELP_TRAIL = `
196
+ tmap-lbs trail — 轨迹可视化
197
+
198
+ 生成腾讯地图轨迹可视化链接。
199
+
200
+ Usage:
201
+ tmap-lbs trail --data <数据URL>
202
+
203
+ Options:
204
+ --data <url> 轨迹数据 JSON 的 URL 地址(必填)
205
+
206
+ Examples:
207
+ tmap-lbs trail --data https://mapapi.qq.com/web/claw/trail.json
208
+ `;
209
+
210
+ // ─── 命令实现 ────────────────────────────────────────────────────────
211
+
212
+ async function cmdConfig(positional) {
213
+ const sub = positional[0];
214
+
215
+ if (!sub || sub === '--help') {
216
+ console.log(HELP_CONFIG);
217
+ return;
218
+ }
219
+
220
+ switch (sub) {
221
+ case 'set-key': {
222
+ const key = positional[1];
223
+ if (!key) fatal('请提供 API Key: tmap-lbs config set-key <key>');
224
+ lib.setKey(key);
225
+ console.log('✅ API Key 已保存');
226
+ break;
227
+ }
228
+ case 'get-key': {
229
+ const key = lib.getKey();
230
+ if (key) {
231
+ const masked = key.length > 8
232
+ ? key.slice(0, 4) + '*'.repeat(key.length - 8) + key.slice(-4)
233
+ : '****';
234
+ console.log(`当前 Key: ${masked}`);
235
+ } else {
236
+ console.log('未设置 API Key');
237
+ }
238
+ break;
239
+ }
240
+ case 'remove-key': {
241
+ lib.removeKey();
242
+ console.log('✅ API Key 已删除');
243
+ break;
244
+ }
245
+ case 'path': {
246
+ console.log(`配置目录: ${lib.CONFIG_DIR}`);
247
+ console.log(`配置文件: ${lib.CONFIG_FILE}`);
248
+ break;
249
+ }
250
+ default:
251
+ fatal(`未知 config 子命令: ${sub}\n${HELP_CONFIG}`);
252
+ }
253
+ }
254
+
255
+ async function cmdSearch(args) {
256
+ if (args.help) { console.log(HELP_SEARCH); return; }
257
+ if (!args.keywords) fatal('缺少 --keywords 参数');
258
+
259
+ const result = await lib.searchPOI({
260
+ keywords: args.keywords,
261
+ city: args.city,
262
+ location: args.location,
263
+ radius: args.radius ? Number(args.radius) : undefined,
264
+ types: args.types,
265
+ page: args.page ? Number(args.page) : 1,
266
+ pageSize: args['page-size'] ? Number(args['page-size']) : 10,
267
+ });
268
+
269
+ if (!result) fatal('搜索未返回结果');
270
+
271
+ if (args.raw) {
272
+ printJSON(result, true);
273
+ } else {
274
+ console.log(`\n🔍 搜索 "${args.keywords}" 共 ${result.count} 条结果:\n`);
275
+ result.pois.forEach((poi, i) => {
276
+ console.log(` ${i + 1}. ${poi.name}`);
277
+ if (poi.address) console.log(` 📍 ${poi.address}`);
278
+ if (poi.type) console.log(` 🏷 ${poi.type}`);
279
+ if (poi.tel) console.log(` 📞 ${poi.tel}`);
280
+ console.log(` 🌐 ${poi.location}`);
281
+ if (poi.distance !== undefined) console.log(` 📏 ${poi.distance}m`);
282
+ console.log('');
283
+ });
284
+ }
285
+ }
286
+
287
+ async function cmdGeocode(args) {
288
+ if (args.help) { console.log(HELP_GEOCODE); return; }
289
+ if (!args.address) fatal('缺少 --address 参数');
290
+
291
+ const result = await lib.geocode({ address: args.address });
292
+ if (!result) fatal('地理编码未返回结果');
293
+
294
+ if (args.raw) {
295
+ printJSON(result, true);
296
+ } else {
297
+ console.log(`\n📍 "${result.title}" 的坐标:`);
298
+ console.log(` 纬度: ${result.lat}`);
299
+ console.log(` 经度: ${result.lng}`);
300
+ console.log(` 坐标: ${result.lng},${result.lat}\n`);
301
+ }
302
+ }
303
+
304
+ async function cmdNearby(args) {
305
+ if (args.help) { console.log(HELP_NEARBY); return; }
306
+
307
+ let keyword = args.keyword;
308
+ if (!keyword) {
309
+ // 组合 location + keywords 形成搜索关键词
310
+ if (!args.location && !args.keywords) {
311
+ fatal('缺少参数。用法: tmap-lbs nearby --location <位置> --keywords <类别>\n 或: tmap-lbs nearby --keyword <完整关键词>');
312
+ }
313
+ keyword = (args.location || '') + (args.keywords || '');
314
+ }
315
+
316
+ const encoded = encodeURIComponent(keyword);
317
+ const url = `https://mapapi.qq.com/web/claw/nearby-search.html?keyword=${encoded}`;
318
+
319
+ console.log(`\n🔍 已生成周边搜索链接:\n`);
320
+ console.log(` ${url}\n`);
321
+ console.log(`搜索关键词: ${keyword}`);
322
+ console.log(`点击链接即可在地图上查看搜索结果。\n`);
323
+ }
324
+
325
+ async function cmdRoute(args) {
326
+ if (args.help) { console.log(HELP_ROUTE); return; }
327
+ if (!args.origin) fatal('缺少 --origin 参数');
328
+ if (!args.destination) fatal('缺少 --destination 参数');
329
+
330
+ const mode = args.mode || 'walk';
331
+ const modeNames = {
332
+ walk: '步行', drive: '驾车', cycle: '骑行',
333
+ ecycle: '电动车', transit: '公交',
334
+ };
335
+
336
+ if (!modeNames[mode]) {
337
+ fatal(`不支持的出行方式: ${mode}\n支持: walk, drive, cycle, ecycle, transit`);
338
+ }
339
+
340
+ console.log(`\n🗺 正在规划 ${modeNames[mode]} 路线...\n`);
341
+
342
+ let result;
343
+ switch (mode) {
344
+ case 'walk':
345
+ result = await lib.walkRoute({ origin: args.origin, destination: args.destination });
346
+ break;
347
+ case 'drive':
348
+ result = await lib.driveRoute({
349
+ origin: args.origin,
350
+ destination: args.destination,
351
+ waypoints: args.waypoints,
352
+ policy: args.policy,
353
+ plate_number: args['plate-number'],
354
+ });
355
+ break;
356
+ case 'cycle':
357
+ result = await lib.cycleRoute({ origin: args.origin, destination: args.destination });
358
+ break;
359
+ case 'ecycle':
360
+ result = await lib.ecycleRoute({ origin: args.origin, destination: args.destination });
361
+ break;
362
+ case 'transit':
363
+ result = await lib.transitRoute({
364
+ origin: args.origin,
365
+ destination: args.destination,
366
+ policy: args.policy,
367
+ departure_time: args['departure-time'],
368
+ });
369
+ break;
370
+ }
371
+
372
+ if (!result) fatal('路线规划未返回结果');
373
+
374
+ if (args.raw) {
375
+ printJSON(result, true);
376
+ } else {
377
+ if (mode === 'transit') {
378
+ const transits = result.route.transits;
379
+ console.log(`共 ${transits.length} 条线路方案:\n`);
380
+ transits.forEach((t, i) => {
381
+ console.log(` 方案 ${i + 1}: 约 ${t.duration} 分钟, ${t.distance}m`);
382
+ });
383
+ } else {
384
+ const p = result.route.paths[0];
385
+ console.log(`✅ ${modeNames[mode]}路线规划完成:`);
386
+ console.log(` 距离: ${p.distance}m`);
387
+ console.log(` 耗时: ${p.duration} 分钟`);
388
+ if (mode === 'drive') {
389
+ if (p.toll) console.log(` 过路费: ¥${p.toll}`);
390
+ if (p.traffic_light_count) console.log(` 红绿灯: ${p.traffic_light_count} 个`);
391
+ }
392
+ console.log(` 共 ${p.steps.length} 个导航步骤`);
393
+ }
394
+ console.log('');
395
+ }
396
+ }
397
+
398
+ async function cmdTravel(args) {
399
+ if (args.help) { console.log(HELP_TRAVEL); return; }
400
+ if (!args.city) fatal('缺少 --city 参数');
401
+ if (!args.interests) fatal('缺少 --interests 参数');
402
+
403
+ const interests = args.interests.split(',').map((s) => s.trim()).filter(Boolean);
404
+ const recommend = args.recommend || '';
405
+
406
+ console.log(`\n🗺 正在获取景点坐标...\n`);
407
+
408
+ // 对每个景点进行地理编码获取坐标
409
+ const spots = [];
410
+ for (const name of interests) {
411
+ try {
412
+ const geo = await lib.geocode({ address: `${args.city}${name}` });
413
+ if (geo) {
414
+ spots.push({ name, lat: geo.lat, lng: geo.lng });
415
+ console.log(` ✅ ${name}: ${geo.lng},${geo.lat}`);
416
+ } else {
417
+ console.log(` ⚠️ ${name}: 未找到坐标,跳过`);
418
+ }
419
+ } catch {
420
+ console.log(` ⚠️ ${name}: 地理编码失败,跳过`);
421
+ }
422
+ }
423
+
424
+ if (spots.length === 0) fatal('没有成功获取任何景点的坐标');
425
+
426
+ // 生成旅游规划可视化链接
427
+ const key = lib.getKey() || '';
428
+ const spotsJSON = JSON.stringify(spots);
429
+ const params = new URLSearchParams();
430
+ params.set('spots', spotsJSON);
431
+ if (recommend) params.set('recommend', recommend);
432
+ if (key) params.set('key', key);
433
+
434
+ const url = `https://mapapi.qq.com/web/claw/travel.html?${params.toString()}`;
435
+
436
+ console.log(`\n🗺 已生成旅游规划链接:\n`);
437
+ console.log(` ${url}\n`);
438
+ console.log(`景点: ${spots.map((s) => s.name).join(', ')}`);
439
+ console.log(`点击链接即可在地图上查看旅游规划。\n`);
440
+
441
+ if (args.raw) {
442
+ printJSON({ spots, url }, false);
443
+ }
444
+ }
445
+
446
+ async function cmdTrail(args) {
447
+ if (args.help) { console.log(HELP_TRAIL); return; }
448
+ if (!args.data) fatal('缺少 --data 参数');
449
+
450
+ const encoded = encodeURIComponent(args.data);
451
+ const url = `https://mapapi.qq.com/web/claw/trail-map.html?data=${encoded}`;
452
+
453
+ console.log(`\n📍 轨迹可视化链接:\n`);
454
+ console.log(` ${url}\n`);
455
+ console.log(`数据来源: ${args.data}\n`);
456
+ }
457
+
458
+ // ─── 主入口 ──────────────────────────────────────────────────────────
459
+
460
+ async function main() {
461
+ const argv = process.argv.slice(2);
462
+ const command = argv[0];
463
+
464
+ if (!command || command === '--help' || command === '-h') {
465
+ console.log(HELP);
466
+ return;
467
+ }
468
+
469
+ const rest = argv.slice(1);
470
+ const { args, positional } = parseArgs(rest);
471
+
472
+ try {
473
+ switch (command) {
474
+ case 'config':
475
+ await cmdConfig(positional);
476
+ break;
477
+ case 'search':
478
+ await cmdSearch(args);
479
+ break;
480
+ case 'geocode':
481
+ await cmdGeocode(args);
482
+ break;
483
+ case 'nearby':
484
+ await cmdNearby(args);
485
+ break;
486
+ case 'route':
487
+ await cmdRoute(args);
488
+ break;
489
+ case 'travel':
490
+ await cmdTravel(args);
491
+ break;
492
+ case 'trail':
493
+ await cmdTrail(args);
494
+ break;
495
+ default:
496
+ fatal(`未知命令: ${command}\n运行 tmap-lbs --help 查看可用命令`);
497
+ }
498
+ } catch (err) {
499
+ fatal(err.message);
500
+ }
501
+ }
502
+
503
+ main();
package/lib/index.js ADDED
@@ -0,0 +1,474 @@
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ // ─── 配置管理 ────────────────────────────────────────────────────────
8
+
9
+ const CONFIG_DIR = path.join(os.homedir(), '.tmap-lbs');
10
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
11
+
12
+ /**
13
+ * 读取持久化配置文件
14
+ * @returns {Object} 配置对象
15
+ */
16
+ function loadConfigFile() {
17
+ try {
18
+ if (fs.existsSync(CONFIG_FILE)) {
19
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
20
+ }
21
+ } catch {
22
+ // ignore
23
+ }
24
+ return {};
25
+ }
26
+
27
+ /**
28
+ * 保存配置到文件
29
+ * @param {Object} config - 配置对象
30
+ */
31
+ function saveConfigFile(config) {
32
+ if (!fs.existsSync(CONFIG_DIR)) {
33
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
34
+ }
35
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
36
+ }
37
+
38
+ /**
39
+ * 获取 API Key(优先环境变量,其次配置文件)
40
+ * @returns {string|null}
41
+ */
42
+ function getKey() {
43
+ return process.env.TMAP_LBS_KEY || process.env.TMAP_LBS_CONFIG || loadConfigFile().key || null;
44
+ }
45
+
46
+ /**
47
+ * 设置 API Key 到配置文件
48
+ * @param {string} key
49
+ */
50
+ function setKey(key) {
51
+ const config = loadConfigFile();
52
+ config.key = key;
53
+ saveConfigFile(config);
54
+ }
55
+
56
+ /**
57
+ * 删除已保存的 API Key
58
+ */
59
+ function removeKey() {
60
+ const config = loadConfigFile();
61
+ delete config.key;
62
+ saveConfigFile(config);
63
+ }
64
+
65
+ /**
66
+ * 获取 Key,未设置时抛出错误
67
+ * @returns {string}
68
+ */
69
+ function ensureKey() {
70
+ const key = getKey();
71
+ if (!key) {
72
+ const msg = [
73
+ '',
74
+ '⚠️ 未找到腾讯地图 API Key',
75
+ '',
76
+ '请通过以下方式之一设置:',
77
+ ' tmap-lbs config set-key <your-key>',
78
+ ' export TMAP_LBS_KEY=<your-key>',
79
+ '',
80
+ '获取 Key: https://lbs.qq.com/dev/console/application/mine',
81
+ '',
82
+ ].join('\n');
83
+ throw new Error(msg);
84
+ }
85
+ return key;
86
+ }
87
+
88
+ // ─── HTTP 请求 ───────────────────────────────────────────────────────
89
+
90
+ /**
91
+ * HTTP GET 请求
92
+ * @param {string} url - 请求基础 URL
93
+ * @param {Object} params - 查询参数
94
+ * @returns {Promise<Object|null>}
95
+ */
96
+ async function httpGet(url, params) {
97
+ const qs = Object.entries(params)
98
+ .filter(([, v]) => v !== undefined && v !== null && v !== '')
99
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
100
+ .join('&');
101
+
102
+ const endpoint = qs ? `${url}?${qs}` : url;
103
+
104
+ const response = await fetch(endpoint, {
105
+ method: 'GET',
106
+ signal: AbortSignal.timeout(15000),
107
+ });
108
+
109
+ if (!response.ok) {
110
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
111
+ }
112
+
113
+ return response.json();
114
+ }
115
+
116
+ // ─── 坐标工具 ────────────────────────────────────────────────────────
117
+
118
+ /**
119
+ * 将 "经度,纬度" 翻转为腾讯地图格式 "纬度,经度"
120
+ * @param {string} coordStr - "lng,lat"
121
+ * @returns {string} "lat,lng"
122
+ */
123
+ function flipCoord(coordStr) {
124
+ const [lng, lat] = coordStr.split(',').map((s) => s.trim());
125
+ return `${lat},${lng}`;
126
+ }
127
+
128
+ // ─── 地理编码 ────────────────────────────────────────────────────────
129
+
130
+ /**
131
+ * 地理编码:地址 → 坐标
132
+ * @param {Object} params
133
+ * @param {string} params.address - 地址关键词
134
+ * @returns {Promise<Object|null>}
135
+ */
136
+ async function geocode(params) {
137
+ const key = ensureKey();
138
+ const url = 'https://apis.map.qq.com/ws/geocoder/v1/';
139
+ const data = await httpGet(url, {
140
+ key,
141
+ address: params.address,
142
+ policy: 1,
143
+ output: 'json',
144
+ });
145
+
146
+ if (!data) return null;
147
+
148
+ if (data.status === 0) {
149
+ const loc = data.result.location;
150
+ return {
151
+ title: data.result.title || params.address,
152
+ lat: loc.lat,
153
+ lng: loc.lng,
154
+ _raw: data,
155
+ };
156
+ }
157
+ throw new Error(`地理编码失败: ${data.message}`);
158
+ }
159
+
160
+ // ─── POI 搜索 ────────────────────────────────────────────────────────
161
+
162
+ /**
163
+ * POI 搜索
164
+ * @param {Object} params
165
+ * @param {string} params.keywords - 搜索关键词
166
+ * @param {string} [params.city] - 城市名称
167
+ * @param {string} [params.location] - 中心点 "经度,纬度"
168
+ * @param {number} [params.radius] - 搜索半径(米)
169
+ * @param {string} [params.types] - POI 分类筛选
170
+ * @param {number} [params.page=1] - 页码
171
+ * @param {number} [params.pageSize=10] - 每页数量(最大20)
172
+ * @returns {Promise<Object|null>}
173
+ */
174
+ async function searchPOI(params) {
175
+ const key = ensureKey();
176
+ const url = 'https://apis.map.qq.com/ws/place/v1/search';
177
+ const hasCoord = params.location && params.radius;
178
+
179
+ const queryOpts = {
180
+ key,
181
+ keyword: params.keywords || '',
182
+ page_index: params.page || 1,
183
+ page_size: Math.min(params.pageSize || params.offset || 10, 20),
184
+ output: 'json',
185
+ };
186
+
187
+ if (hasCoord) {
188
+ const latLng = flipCoord(params.location);
189
+ queryOpts.boundary = `nearby(${latLng},${params.radius})`;
190
+ } else if (params.city) {
191
+ const expandArea = params.cityLimit === false ? 1 : 0;
192
+ queryOpts.boundary = `region(${params.city},${expandArea})`;
193
+ }
194
+
195
+ if (params.types) {
196
+ queryOpts.filter = `category=${params.types}`;
197
+ }
198
+
199
+ const data = await httpGet(url, queryOpts);
200
+ if (!data) return null;
201
+
202
+ if (data.status === 0) {
203
+ const count = data.count || (data.data ? data.data.length : 0);
204
+ const pois = (data.data || []).map((item) => ({
205
+ name: item.title,
206
+ address: item.address || '',
207
+ type: item.category || '',
208
+ tel: item.tel || '',
209
+ location: `${item.location.lng},${item.location.lat}`,
210
+ distance: item._distance || undefined,
211
+ id: item.id || '',
212
+ }));
213
+ return { status: '1', count, pois, _raw: data };
214
+ }
215
+ throw new Error(`POI 搜索失败: ${data.message}`);
216
+ }
217
+
218
+ // ─── 路径规划 ────────────────────────────────────────────────────────
219
+
220
+ /**
221
+ * 步行路径规划
222
+ */
223
+ async function walkRoute(params) {
224
+ const key = ensureKey();
225
+ const data = await httpGet('https://apis.map.qq.com/ws/direction/v1/walking/', {
226
+ key,
227
+ from: flipCoord(params.origin),
228
+ to: flipCoord(params.destination),
229
+ output: 'json',
230
+ });
231
+ if (!data) return null;
232
+ if (data.status === 0) {
233
+ const route = data.result.routes[0];
234
+ return {
235
+ status: '1',
236
+ route: {
237
+ paths: [{
238
+ distance: route.distance,
239
+ duration: route.duration,
240
+ direction: route.direction || '',
241
+ steps: route.steps || [],
242
+ }],
243
+ },
244
+ _raw: data,
245
+ };
246
+ }
247
+ throw new Error(`步行规划失败: ${data.message}`);
248
+ }
249
+
250
+ /**
251
+ * 驾车路径规划
252
+ */
253
+ async function driveRoute(params) {
254
+ const key = ensureKey();
255
+ const queryOpts = {
256
+ key,
257
+ from: flipCoord(params.origin),
258
+ to: flipCoord(params.destination),
259
+ output: 'json',
260
+ };
261
+ if (params.policy) queryOpts.policy = params.policy;
262
+ if (params.plate_number) queryOpts.plate_number = params.plate_number;
263
+ if (params.waypoints) {
264
+ queryOpts.waypoints = params.waypoints
265
+ .split(';')
266
+ .map((wp) => flipCoord(wp))
267
+ .join(';');
268
+ }
269
+
270
+ const data = await httpGet('https://apis.map.qq.com/ws/direction/v1/driving/', queryOpts);
271
+ if (!data) return null;
272
+ if (data.status === 0) {
273
+ const route = data.result.routes[0];
274
+ return {
275
+ status: '1',
276
+ route: {
277
+ paths: [{
278
+ distance: route.distance,
279
+ duration: route.duration,
280
+ toll: route.toll || 0,
281
+ traffic_light_count: route.traffic_light_count || 0,
282
+ restriction: route.restriction || null,
283
+ tags: route.tags || [],
284
+ steps: route.steps || [],
285
+ }],
286
+ },
287
+ _raw: data,
288
+ };
289
+ }
290
+ throw new Error(`驾车规划失败: ${data.message}`);
291
+ }
292
+
293
+ /**
294
+ * 骑行路径规划(自行车)
295
+ */
296
+ async function cycleRoute(params) {
297
+ const key = ensureKey();
298
+ const data = await httpGet('https://apis.map.qq.com/ws/direction/v1/bicycling/', {
299
+ key,
300
+ from: flipCoord(params.origin),
301
+ to: flipCoord(params.destination),
302
+ output: 'json',
303
+ });
304
+ if (!data) return null;
305
+ if (data.status === 0) {
306
+ const route = data.result.routes[0];
307
+ return {
308
+ status: '1',
309
+ route: {
310
+ paths: [{
311
+ distance: route.distance,
312
+ duration: route.duration,
313
+ direction: route.direction || '',
314
+ steps: route.steps || [],
315
+ }],
316
+ },
317
+ _raw: data,
318
+ };
319
+ }
320
+ throw new Error(`骑行规划失败: ${data.message}`);
321
+ }
322
+
323
+ /**
324
+ * 电动车路径规划
325
+ */
326
+ async function ecycleRoute(params) {
327
+ const key = ensureKey();
328
+ const data = await httpGet('https://apis.map.qq.com/ws/direction/v1/ebicycling/', {
329
+ key,
330
+ from: flipCoord(params.origin),
331
+ to: flipCoord(params.destination),
332
+ output: 'json',
333
+ });
334
+ if (!data) return null;
335
+ if (data.status === 0) {
336
+ const route = data.result.routes[0];
337
+ return {
338
+ status: '1',
339
+ route: {
340
+ paths: [{
341
+ distance: route.distance,
342
+ duration: route.duration,
343
+ direction: route.direction || '',
344
+ steps: route.steps || [],
345
+ }],
346
+ },
347
+ _raw: data,
348
+ };
349
+ }
350
+ throw new Error(`电动车规划失败: ${data.message}`);
351
+ }
352
+
353
+ /**
354
+ * 公交路径规划
355
+ */
356
+ async function transitRoute(params) {
357
+ const key = ensureKey();
358
+ const queryOpts = {
359
+ key,
360
+ from: flipCoord(params.origin),
361
+ to: flipCoord(params.destination),
362
+ output: 'json',
363
+ };
364
+ if (params.policy) queryOpts.policy = params.policy;
365
+ if (params.departure_time) queryOpts.departure_time = params.departure_time;
366
+
367
+ const data = await httpGet('https://apis.map.qq.com/ws/direction/v1/transit/', queryOpts);
368
+ if (!data) return null;
369
+ if (data.status === 0) {
370
+ const routes = data.result.routes || [];
371
+ return {
372
+ status: '1',
373
+ route: {
374
+ transits: routes.map((route) => ({
375
+ duration: route.duration,
376
+ distance: route.distance || 0,
377
+ bounds: route.bounds || '',
378
+ steps: route.steps || [],
379
+ })),
380
+ },
381
+ _raw: data,
382
+ };
383
+ }
384
+ throw new Error(`公交规划失败: ${data.message}`);
385
+ }
386
+
387
+ // ─── 旅游规划 ────────────────────────────────────────────────────────
388
+
389
+ /**
390
+ * 旅游规划助手
391
+ * @param {Object} params
392
+ * @param {string} params.city - 城市名称
393
+ * @param {string[]} params.interests - 兴趣关键词数组
394
+ * @param {string} [params.travelMode=walking] - 出行方式
395
+ * @returns {Promise<Object>}
396
+ */
397
+ async function travelPlan(params) {
398
+ const { city, interests = [], travelMode = 'walking' } = params;
399
+ const placeList = [];
400
+ const renderItems = [];
401
+
402
+ for (const interest of interests) {
403
+ const result = await searchPOI({
404
+ keywords: interest,
405
+ city,
406
+ page: 1,
407
+ pageSize: 5,
408
+ });
409
+
410
+ if (result && result.pois && result.pois.length > 0) {
411
+ placeList.push(...result.pois);
412
+ result.pois.forEach((poi) => {
413
+ const [lng, lat] = poi.location.split(',').map(Number);
414
+ renderItems.push({
415
+ type: 'poi',
416
+ lnglat: [lng, lat],
417
+ sort: poi.type || interest,
418
+ text: poi.name,
419
+ remark: poi.address || `${interest}推荐`,
420
+ });
421
+ });
422
+ }
423
+ }
424
+
425
+ if (placeList.length >= 2) {
426
+ for (let i = 0; i < placeList.length - 1; i++) {
427
+ const start = placeList[i];
428
+ const end = placeList[i + 1];
429
+ const [startLng, startLat] = start.location.split(',').map(Number);
430
+ const [endLng, endLat] = end.location.split(',').map(Number);
431
+ const pathItem = {
432
+ type: 'route',
433
+ routeType: travelMode,
434
+ start: [startLng, startLat],
435
+ end: [endLng, endLat],
436
+ remark: `从 ${start.name} 到 ${end.name}`,
437
+ };
438
+ if (travelMode === 'transfer') pathItem.city = city;
439
+ renderItems.push(pathItem);
440
+ }
441
+ }
442
+
443
+ return { pois: placeList, mapTaskData: renderItems };
444
+ }
445
+
446
+ // ─── 导出 ────────────────────────────────────────────────────────────
447
+
448
+ module.exports = {
449
+ // 配置
450
+ getKey,
451
+ setKey,
452
+ removeKey,
453
+ ensureKey,
454
+ loadConfigFile,
455
+ saveConfigFile,
456
+ CONFIG_DIR,
457
+ CONFIG_FILE,
458
+ // HTTP
459
+ httpGet,
460
+ // 坐标
461
+ flipCoord,
462
+ // 地理编码
463
+ geocode,
464
+ // POI
465
+ searchPOI,
466
+ // 路径规划
467
+ walkRoute,
468
+ driveRoute,
469
+ cycleRoute,
470
+ ecycleRoute,
471
+ transitRoute,
472
+ // 旅游
473
+ travelPlan,
474
+ };
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@tencent-map/lbs-skills",
3
+ "version": "0.0.1",
4
+ "description": "腾讯地图位置服务命令行工具,支持 POI 搜索、地理编码、周边搜索、路径规划、旅游规划、轨迹可视化等功能",
5
+ "bin": {
6
+ "tmap-lbs": "./bin/cli.js"
7
+ },
8
+ "main": "lib/index.js",
9
+ "files": [
10
+ "bin",
11
+ "lib",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "test": "echo \"no test\" && exit 0"
16
+ },
17
+ "keywords": [
18
+ "tmap",
19
+ "tencent-map",
20
+ "lbs",
21
+ "poi",
22
+ "geocoding",
23
+ "route-planning",
24
+ "cli"
25
+ ],
26
+ "author": "",
27
+ "license": "MIT",
28
+ "dependencies": {}
29
+ }