@danceiny/gotry 0.0.1-rc.19 → 0.0.1-rc.21

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 (71) hide show
  1. package/README.md +14 -3
  2. package/README.zh-CN.md +15 -4
  3. package/bin/gotry-bootstrap.js +136 -45
  4. package/bin/gotry-inner.js +11 -6
  5. package/cordis.gotry-patch.yml +19 -8
  6. package/dist/capabilities/anything.js +62 -29
  7. package/dist/capabilities/doctor.js +113 -19
  8. package/dist/capabilities/hbcli.js +28 -2
  9. package/dist/capabilities/session/health-watch.js +10 -3
  10. package/dist/scripts/agent-planning-turn-deadline-e2e.js +2 -1
  11. package/dist/scripts/anything-tests.js +70 -21
  12. package/dist/scripts/benchmark-environment-bridge-e2e.js +178 -33
  13. package/dist/scripts/benchmark-environment-bridge-tests.js +1480 -271
  14. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +211 -2
  15. package/dist/scripts/booking-copilot-runtime-proof-tests.js +132 -2
  16. package/dist/scripts/bootstrap-tests.js +30 -3
  17. package/dist/scripts/doctor-tests.js +55 -1
  18. package/dist/scripts/hbcli-e2e-tests.js +1 -1
  19. package/dist/scripts/hbcli-tests.js +38 -1
  20. package/dist/scripts/health-watch-cli.js +9 -1
  21. package/dist/scripts/map-tools-vendor-package-proof.js +276 -0
  22. package/dist/scripts/persona-surface-guard-tests.js +8 -3
  23. package/dist/src/benchmark-agent-conformance.js +187 -17
  24. package/dist/src/benchmark-environment-bridge.js +287 -102
  25. package/dist/src/booking-surface/dsh-planner.js +24 -8
  26. package/dist/src/index.js +5 -2
  27. package/extension/manifest.json +2 -2
  28. package/package.json +3 -1
  29. package/ts/capabilities/anything.ts +89 -38
  30. package/ts/capabilities/hbcli.ts +53 -4
  31. package/ts/capabilities/session/health-watch.ts +9 -2
  32. package/ts/dsh-runtime/vendor/README.md +60 -0
  33. package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
  34. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +200 -0
  35. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +202 -0
  36. package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +216 -0
  37. package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
  38. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +371 -0
  39. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
  40. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +56 -0
  41. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
  42. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
  43. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +126 -0
  44. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
  45. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +109 -0
  46. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +27 -0
  47. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
  48. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
  49. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +170 -0
  50. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +53 -0
  51. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
  52. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
  53. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
  54. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
  55. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +20 -0
  56. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
  57. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
  58. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
  59. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
  60. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
  61. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +16 -0
  62. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +55 -0
  63. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
  64. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +91 -0
  65. package/ts/package.json +17 -1
  66. package/ts/scripts/map-tools-vendor-package-proof.ts +268 -0
  67. package/ts/src/benchmark-agent-conformance.ts +175 -13
  68. package/ts/src/benchmark-environment-bridge.ts +220 -66
  69. package/ts/src/booking-surface/dsh-planner.ts +25 -13
  70. package/ts/src/index.ts +4 -2
  71. package/ts/src/turn-deadline.ts +4 -0
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Loopback settings-card route for dsh-map-tools.
3
+ *
4
+ * GET /dsh-map-tools/config → non-secret summary (has-amap-key, …)
5
+ * GET /dsh-map-tools/config?open=true → open the config file in the editor
6
+ * POST /dsh-map-tools/config → apply a provider/key/timeout patch
7
+ *
8
+ * Same-origin loopback only (mirrors dsh's own /api fence and the modlens
9
+ * pattern); the two halves of the card read/write a real file.
10
+ */
11
+ import { spawn } from 'node:child_process';
12
+ import { configSummary, applyConfig, configPath, configFileExists } from './config-file.js';
13
+ /** localhost, ::1, or anything in 127/8 — matching dsh's own /api fence. */
14
+ function isLoopbackHost(hostname) {
15
+ return hostname === 'localhost' || hostname === '::1' || hostname === '127.0.0.1' || /^127\.\d+\.\d+\.\d+$/.test(hostname);
16
+ }
17
+ /** Refuse cross-origin and non-loopback requests (the route answers same-origin loopback only). */
18
+ function isTrustedRequest(req) {
19
+ const host = req.headers?.host;
20
+ if (typeof host !== 'string' || host === '')
21
+ return false;
22
+ let hostUrl;
23
+ try {
24
+ hostUrl = new URL(`http://${host}`);
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ if (!isLoopbackHost(hostUrl.hostname))
30
+ return false;
31
+ if (req.headers?.['sec-fetch-site'] === 'cross-site')
32
+ return false;
33
+ const origin = req.headers?.origin;
34
+ if (origin === undefined)
35
+ return true;
36
+ try {
37
+ return new URL(origin).host === hostUrl.host;
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ }
43
+ /** Open the config file in the OS default editor. */
44
+ function openConfigFile() {
45
+ const file = configPath();
46
+ const [command, args] = process.platform === 'win32'
47
+ ? ['cmd', ['/c', 'start', '', file]]
48
+ : process.platform === 'darwin'
49
+ ? ['open', [file]]
50
+ : ['xdg-open', [file]];
51
+ const child = spawn(command, args, { detached: true, stdio: 'ignore' });
52
+ child.unref();
53
+ }
54
+ /**
55
+ * Register the settings-card route under the web server, when one exists.
56
+ *
57
+ * @param ctx - plugin context.
58
+ * @param reload - rebuild the tools after the card saves (the file write alone
59
+ * never reaches the registered tool instances; without this the new provider
60
+ * or key only takes effect on the next plugin reload).
61
+ */
62
+ export function installConfigRoute(ctx, reload = () => { }) {
63
+ const fn = ctx.inject;
64
+ fn(['webServer'], (scope) => {
65
+ scope.webServer.register({
66
+ kind: 'exact',
67
+ path: '/dsh-map-tools/config',
68
+ handler: async (req, res) => {
69
+ const send = (status, body) => {
70
+ res.writeHead(status, { 'content-type': 'application/json' });
71
+ res.end(JSON.stringify(body));
72
+ };
73
+ if (!isTrustedRequest(req)) {
74
+ send(403, { error: 'request refused: this route answers same-origin loopback only' });
75
+ return;
76
+ }
77
+ if (req.method === 'GET') {
78
+ try {
79
+ const summary = configSummary();
80
+ const wantsOpen = new URL(req.url ?? '/', 'http://localhost').searchParams.has('open');
81
+ if (wantsOpen && !configFileExists()) {
82
+ applyConfig({});
83
+ }
84
+ if (wantsOpen)
85
+ openConfigFile();
86
+ send(200, { ...summary, configPath: configPath() });
87
+ }
88
+ catch (error) {
89
+ send(409, { error: String(error?.message ?? error) });
90
+ }
91
+ return;
92
+ }
93
+ if (req.method !== 'POST') {
94
+ res.writeHead(405).end();
95
+ return;
96
+ }
97
+ try {
98
+ const chunks = [];
99
+ let total = 0;
100
+ for await (const chunk of req) {
101
+ total += chunk.length;
102
+ if (total > 64 * 1024) {
103
+ send(413, { error: 'config payload too large' });
104
+ req.destroy();
105
+ return;
106
+ }
107
+ chunks.push(chunk);
108
+ }
109
+ const patch = JSON.parse(Buffer.concat(chunks).toString('utf8'));
110
+ applyConfig({
111
+ provider: patch.provider,
112
+ amapKey: patch.amapKey,
113
+ timeoutMs: typeof patch.timeoutMs === 'number' ? patch.timeoutMs : undefined,
114
+ });
115
+ send(200, configSummary());
116
+ // Rebuild the tool instances so the saved provider/key/timeout is
117
+ // live immediately (the card tells the user "工具已重建").
118
+ reload();
119
+ }
120
+ catch (error) {
121
+ send(400, { error: String(error?.message ?? error) });
122
+ }
123
+ },
124
+ });
125
+ });
126
+ }
@@ -0,0 +1,16 @@
1
+ /** Plugin configuration schema. */
2
+ import Schema from '@deepseek-ai/schemastery';
3
+ /** Amap apply URL (shown as a clickable link in the settings card). */
4
+ export const AMAP_APPLY_URL = 'https://console.amap.com/dev/key/app';
5
+ export const Config = Schema.object({
6
+ provider: Schema.union(['amap', 'osm'])
7
+ .default('amap')
8
+ .description('数据源:amap=高德地图(推荐,国内数据最全);osm=免费 OSM 兜底(无需 key,能力有限)'),
9
+ amapKey: Schema.string()
10
+ .role('secret')
11
+ .description(`高德 Web 服务 key。申请:${AMAP_APPLY_URL}(创建应用后选择"Web 服务"类型)`),
12
+ timeoutMs: Schema.number().default(15000).description('单次请求超时(毫秒)'),
13
+ maxQps: Schema.number().default(2).description('高德 API 每秒最大请求数(QPS 上限,默认 2 低于高德常见上限 3,防触发 10021 配额错误)'),
14
+ defaultMode: Schema.union(['driving', 'transit', 'walking', 'bicycling']).default('driving').description('默认路线模式'),
15
+ language: Schema.union(['zh', 'en']).default('zh').description('返回语言'),
16
+ });
@@ -0,0 +1,109 @@
1
+ import { AmapClient } from './clients/amap.js';
2
+ import { OsrmClient } from './clients/osrm.js';
3
+ import { NominatimClient } from './clients/nominatim.js';
4
+ import { PhotonClient } from './clients/photon.js';
5
+ import { registerRouteTools } from './tools/routes.js';
6
+ import { registerGeocodeTools } from './tools/geocode.js';
7
+ import { registerPoiTool } from './tools/poi.js';
8
+ import { parseLngLat } from './types.js';
9
+ import { Config } from './config.js';
10
+ import { readConfig } from './config-file.js';
11
+ import { installConfigRoute } from './config-route.js';
12
+ import { installSettingsNamespace } from './settings-ns.js';
13
+ export const name = 'dsh-map-tools';
14
+ export const inject = ['tools'];
15
+ export { Config };
16
+ /** Build (or rebuild) the provider clients. Priority: config-file (settings
17
+ * card) → schema defaults. */
18
+ function buildClients(config) {
19
+ // The settings card writes ~/.dsh-map-tools/config.json; when it holds
20
+ // values, they win over the composition-entry schema defaults.
21
+ let fileConfig = {};
22
+ try {
23
+ fileConfig = readConfig();
24
+ }
25
+ catch (error) {
26
+ console.error(`[dsh-map-tools] config read failed, using schema defaults: ${error}`);
27
+ }
28
+ const provider = fileConfig.provider ?? config.provider;
29
+ const amapKey = fileConfig.amapKey ?? config.amapKey;
30
+ const timeoutMs = fileConfig.timeoutMs ?? config.timeoutMs;
31
+ const maxQps = fileConfig.maxQps ?? config.maxQps;
32
+ // Provider selection: exactly one primary source per provider.
33
+ const amap = provider === 'amap' && amapKey
34
+ ? new AmapClient({ key: amapKey, timeoutMs, maxQps })
35
+ : undefined;
36
+ const osrm = new OsrmClient({ timeoutMs });
37
+ const nominatim = new NominatimClient({
38
+ timeoutMs,
39
+ userAgent: 'dsh-map-tools/0.1.0 (DeepSeek Harness plugin; contact: https://github.com/HorusJiang/dsh-map-tools)',
40
+ });
41
+ const photon = new PhotonClient({ timeoutMs, lang: config.language });
42
+ /** Resolve an address (or `lng,lat`) to coordinates, using the active provider. */
43
+ async function resolve(text, signal) {
44
+ const coord = parseLngLat(text);
45
+ if (coord)
46
+ return coord;
47
+ if (amap) {
48
+ const r = await amap.geocode(text, signal);
49
+ return r.location;
50
+ }
51
+ // Free sources cannot reliably geocode Chinese addresses (Nominatim is
52
+ // blocked on many CN networks, Photon returns 400 for CJK queries) — give
53
+ // an actionable prompt instead of a raw provider error.
54
+ throw new Error(`免费数据源无法可靠解析中文地址:"${text}"。请直接提供 "lng,lat" 坐标,或在插件配置中设置高德 amapKey(https://console.amap.com/dev/key/app)。`);
55
+ }
56
+ /** Resolve the city code for a point (transit queries need city1/city2).
57
+ * Amap v5 transit accepts only an adcode (e.g. "110000" / "110101") or a
58
+ * citycode (e.g. "010") — bare city names like "北京" are rejected with
59
+ * INVALID_PARAMS. Prefer the adcode from geocoding; fall back to the city
60
+ * name as a last resort. */
61
+ async function resolveCity(text, signal) {
62
+ const coord = parseLngLat(text);
63
+ if (amap) {
64
+ const r = coord ? await amap.reverseGeocode(coord, signal) : await amap.geocode(text, signal);
65
+ if (r.adcode)
66
+ return r.adcode;
67
+ const city = r.city ?? '';
68
+ return city.replace(/市$/, '');
69
+ }
70
+ return '';
71
+ }
72
+ return { amap, osrm, nominatim, photon, resolve, resolveCity };
73
+ }
74
+ /** Register every tool under the current clients; returns a disposer. */
75
+ function registerAll(ctx, clients) {
76
+ const disposers = [];
77
+ const routeClients = {
78
+ amap: clients.amap,
79
+ osrm: clients.osrm,
80
+ resolve: clients.resolve,
81
+ resolveCity: clients.resolveCity,
82
+ defaultMode: 'driving',
83
+ };
84
+ registerRouteTools(ctx, routeClients, disposers);
85
+ registerGeocodeTools(ctx, { amap: clients.amap, nominatim: clients.nominatim, photon: clients.photon }, disposers);
86
+ registerPoiTool(ctx, { amap: clients.amap, resolve: clients.resolve }, disposers);
87
+ return () => {
88
+ for (const d of disposers)
89
+ d();
90
+ };
91
+ }
92
+ export function apply(ctx, config) {
93
+ // Register tools under an effect: Cordis runs the returned disposer both on
94
+ // explicit reload (we call it) and on plugin unload (fiber disposal).
95
+ let disposeTools = () => { };
96
+ ctx.effect(() => {
97
+ disposeTools = registerAll(ctx, buildClients(config));
98
+ return () => disposeTools();
99
+ });
100
+ const reload = () => {
101
+ disposeTools();
102
+ disposeTools = registerAll(ctx, buildClients(config));
103
+ };
104
+ // The settings card route + namespace (modlens pattern): the card reads and
105
+ // writes ~/.dsh-map-tools/config.json through the loopback route, and the
106
+ // tools rebuild on a save (route POST → reload).
107
+ installConfigRoute(ctx, reload);
108
+ installSettingsNamespace(ctx, config, reload);
109
+ }
@@ -0,0 +1,27 @@
1
+ import { Config as ConfigSchema } from './config.js';
2
+ /** Namespace the settings page keys this plugin's card to (plain string since DSH 0.1.2-rc.1). */
3
+ export const MAP_TOOLS_NS = 'dsh-map-tools';
4
+ /**
5
+ * Wire the settings section so the card renders. The values live in the
6
+ * config file; the section carries only the composition entry as its base so
7
+ * the page has something to dispatch on.
8
+ *
9
+ * @param ctx - plugin context.
10
+ * @param entry - the composition entry config (schema defaults).
11
+ * @param reload - rebuild tools after a settings change.
12
+ */
13
+ export function installSettingsNamespace(ctx, entry, reload) {
14
+ ctx.inject(['settings'], (scope) => {
15
+ scope.settings.installSection(ctx, MAP_TOOLS_NS, ConfigSchema, entry, {
16
+ setSource: (current) => {
17
+ // The tools read ~/.dsh-map-tools/config.json then the composition
18
+ // entry; the section's own value is a dispatch key for the card and
19
+ // is never a tool configuration source.
20
+ void current;
21
+ },
22
+ onChange: () => {
23
+ reload();
24
+ },
25
+ });
26
+ });
27
+ }
@@ -0,0 +1,127 @@
1
+ /** Geocoding tools: address → coordinates, and reverse. */
2
+ import { defineTool } from '@deepseek-ai/dsh-tools';
3
+ import { parseLngLat } from '../types.js';
4
+ export function registerGeocodeTools(ctx, clients, disposers = []) {
5
+ disposers.push(ctx.tools.register(defineTool({
6
+ name: 'map_geocode',
7
+ description: '把地址文本转换为经纬度坐标。支持中文地址、模糊地址。',
8
+ parameters: {
9
+ address: { type: 'string', required: true, description: '地址文本,如 "北京市朝阳区建国路88号"。' },
10
+ },
11
+ output: {
12
+ schema: {
13
+ type: 'object',
14
+ additionalProperties: false,
15
+ properties: {
16
+ provider: { type: 'string' },
17
+ formatted: { type: 'string' },
18
+ location: {
19
+ type: 'array',
20
+ items: { type: 'number' },
21
+ },
22
+ city: { type: 'string' },
23
+ district: { type: 'string' },
24
+ },
25
+ },
26
+ render: (_args, value) => {
27
+ const v = value;
28
+ const name = v.provider === 'amap' ? '高德' : v.provider === 'nominatim' ? 'Nominatim(OSM)' : v.provider === 'photon' ? 'Photon(OSM)' : 'inline';
29
+ const area = v.city ? `(${v.city}${v.district ? ` ${v.district}` : ''})` : '';
30
+ return [{
31
+ type: 'text',
32
+ text: `${name} 解析 "${v.formatted}" → 经纬度 ${v.location[0]}, ${v.location[1]}${area}`,
33
+ }];
34
+ },
35
+ },
36
+ async execute(args, exec) {
37
+ const coord = parseLngLat(args.address);
38
+ if (coord) {
39
+ // Input already looks like coordinates — return as-is.
40
+ return { provider: 'inline', formatted: args.address, location: coord };
41
+ }
42
+ if (clients.amap) {
43
+ const r = await clients.amap.geocode(args.address, exec.signal);
44
+ return { provider: r.provider, formatted: r.formatted, location: r.location, city: r.city, district: r.district };
45
+ }
46
+ // Free sources (Photon/Nominatim) are unreliable for Chinese addresses.
47
+ // Try them, but convert any failure into an actionable prompt.
48
+ let lastError = '免费地理编码服务不可用';
49
+ if (clients.photon) {
50
+ try {
51
+ const r = await clients.photon.geocode(args.address, exec.signal);
52
+ return { provider: r.provider, formatted: r.formatted, location: r.location, city: r.city, district: r.district };
53
+ }
54
+ catch (err) {
55
+ lastError = err instanceof Error ? err.message : String(err);
56
+ }
57
+ }
58
+ if (clients.nominatim) {
59
+ try {
60
+ const r = await clients.nominatim.geocode(args.address, exec.signal);
61
+ return { provider: r.provider, formatted: r.formatted, location: r.location, city: r.city, district: r.district };
62
+ }
63
+ catch (err) {
64
+ lastError = err instanceof Error ? err.message : String(err);
65
+ }
66
+ }
67
+ throw new Error(`免费数据源无法可靠解析中文地址:"${args.address}"(${lastError})。请直接提供 "lng,lat" 坐标,或在插件配置中设置高德 amapKey(https://console.amap.com/dev/key/app)。`);
68
+ },
69
+ })));
70
+ disposers.push(ctx.tools.register(defineTool({
71
+ name: 'map_reverse_geocode',
72
+ description: '把经纬度坐标转换为结构化地址(含城市、区县)。',
73
+ parameters: {
74
+ location: { type: 'string', required: true, description: '经纬度,格式 "lng,lat",如 "116.397428,39.90923"。' },
75
+ },
76
+ output: {
77
+ schema: {
78
+ type: 'object',
79
+ additionalProperties: false,
80
+ properties: {
81
+ provider: { type: 'string' },
82
+ formatted: { type: 'string' },
83
+ location: {
84
+ type: 'array',
85
+ items: { type: 'number' },
86
+ },
87
+ city: { type: 'string' },
88
+ district: { type: 'string' },
89
+ },
90
+ },
91
+ render: (_args, value) => {
92
+ const v = value;
93
+ const name = v.provider === 'amap' ? '高德' : v.provider === 'nominatim' ? 'Nominatim(OSM)' : v.provider === 'photon' ? 'Photon(OSM)' : '未知';
94
+ return [{ type: 'text', text: `${name}:${v.formatted}` }];
95
+ },
96
+ },
97
+ async execute(args, exec) {
98
+ const coord = parseLngLat(args.location);
99
+ if (!coord)
100
+ throw new Error(`无效的经纬度格式:${args.location}(应为 "lng,lat")`);
101
+ if (clients.amap) {
102
+ const r = await clients.amap.reverseGeocode(coord, exec.signal);
103
+ return { provider: r.provider, formatted: r.formatted, location: r.location, city: r.city, district: r.district };
104
+ }
105
+ let lastError = '免费地理编码服务不可用';
106
+ if (clients.photon) {
107
+ try {
108
+ const r = await clients.photon.reverseGeocode(coord, exec.signal);
109
+ return { provider: r.provider, formatted: r.formatted, location: r.location, city: r.city, district: r.district };
110
+ }
111
+ catch (err) {
112
+ lastError = err instanceof Error ? err.message : String(err);
113
+ }
114
+ }
115
+ if (clients.nominatim) {
116
+ try {
117
+ const r = await clients.nominatim.reverseGeocode(coord, exec.signal);
118
+ return { provider: r.provider, formatted: r.formatted, location: r.location, city: r.city, district: r.district };
119
+ }
120
+ catch (err) {
121
+ lastError = err instanceof Error ? err.message : String(err);
122
+ }
123
+ }
124
+ throw new Error(`免费逆地理编码服务不可用(${lastError})。请配置高德 amapKey(https://console.amap.com/dev/key/app)以获得稳定结果。`);
125
+ },
126
+ })));
127
+ }
@@ -0,0 +1,84 @@
1
+ /** POI search tool. */
2
+ import { defineTool } from '@deepseek-ai/dsh-tools';
3
+ import { parseLngLat } from '../types.js';
4
+ export function registerPoiTool(ctx, clients, disposers = []) {
5
+ disposers.push(ctx.tools.register(defineTool({
6
+ name: 'map_poi_search',
7
+ description: '搜索兴趣点(POI):餐厅、加油站、酒店、医院等。支持按关键词搜索或按位置周边搜索。',
8
+ parameters: {
9
+ keywords: { type: 'string', required: true, description: '搜索关键词,如 "加油站"、"咖啡"、"北京南站"。' },
10
+ location: {
11
+ type: 'string',
12
+ description: '中心点:"lng,lat" 坐标或地址文本。提供后按周边搜索(默认半径 1000 米)。',
13
+ },
14
+ radiusM: { type: 'number', description: '周边搜索半径(米),默认 1000,最大 50000。' },
15
+ region: { type: 'string', description: '限定城市(文本搜索时),如 "北京"。' },
16
+ types: { type: 'string', description: 'POI 类型编码,如 "060000"(餐饮)。' },
17
+ },
18
+ output: {
19
+ schema: {
20
+ type: 'object',
21
+ additionalProperties: false,
22
+ properties: {
23
+ count: { type: 'number' },
24
+ results: {
25
+ type: 'array',
26
+ items: {
27
+ type: 'object',
28
+ additionalProperties: false,
29
+ properties: {
30
+ name: { type: 'string' },
31
+ location: {
32
+ type: 'array',
33
+ items: { type: 'number' },
34
+ },
35
+ type: { type: 'string' },
36
+ address: { type: 'string' },
37
+ tel: { type: 'string' },
38
+ distanceM: { type: 'number' },
39
+ },
40
+ },
41
+ },
42
+ },
43
+ },
44
+ render: (_args, value) => {
45
+ const v = value;
46
+ const lines = [`找到 ${v.count} 个结果:`];
47
+ for (const r of v.results.slice(0, 10)) {
48
+ const d = r.distanceM !== undefined ? `(距中心 ${Math.round(r.distanceM)} 米)` : '';
49
+ const tel = r.tel ? ` 电话:${r.tel}` : '';
50
+ lines.push(`- ${r.name}${d}${r.type ? ` [${r.type}]` : ''}${r.address ? ` ${r.address}` : ''}${tel}`);
51
+ }
52
+ if (v.results.length > 10)
53
+ lines.push(`- …(还有 ${v.results.length - 10} 个)`);
54
+ return [{ type: 'text', text: lines.join('\n') }];
55
+ },
56
+ },
57
+ async execute(args, exec) {
58
+ if (!clients.amap)
59
+ throw new Error('POI 搜索需要配置高德 amapKey。申请:https://console.amap.com/dev/key/app');
60
+ let results;
61
+ if (args.location) {
62
+ const center = parseLngLat(args.location) ?? (await clients.resolve(args.location, exec.signal));
63
+ results = await clients.amap.poiAround(center, args.keywords, {
64
+ radiusM: args.radiusM ?? 1000,
65
+ types: args.types,
66
+ }, exec.signal);
67
+ }
68
+ else {
69
+ results = await clients.amap.poiSearch(args.keywords, { region: args.region, cityLimit: !!args.region }, exec.signal);
70
+ }
71
+ return {
72
+ count: results.length,
73
+ results: results.map((r) => ({
74
+ name: r.name,
75
+ location: r.location,
76
+ type: r.type,
77
+ address: r.address,
78
+ tel: r.tel,
79
+ distanceM: r.distanceM,
80
+ })),
81
+ };
82
+ },
83
+ })));
84
+ }
@@ -0,0 +1,170 @@
1
+ /** Route planning tools: driving / transit / walking / bicycling. */
2
+ import { defineTool } from '@deepseek-ai/dsh-tools';
3
+ import { AmapQuotaError } from '../clients/amap.js';
4
+ import { parseLngLat } from '../types.js';
5
+ function routeTool(name, description, mode, clients) {
6
+ const osrmProfile = mode === 'walking' ? 'walking' : mode === 'bicycling' ? 'cycling' : 'driving';
7
+ return defineTool({
8
+ name,
9
+ description,
10
+ parameters: {
11
+ origin: { type: 'string', required: true, description: 'Start point: an address, or "lng,lat" coordinates.' },
12
+ destination: { type: 'string', required: true, description: 'End point: an address, or "lng,lat" coordinates.' },
13
+ waypoints: { type: 'string', description: 'Optional waypoints as "lng,lat;lng,lat" (Amap driving only).' },
14
+ alternatives: { type: 'boolean', description: 'Request alternative routes (default false).' },
15
+ },
16
+ output: {
17
+ schema: {
18
+ type: 'object',
19
+ additionalProperties: false,
20
+ properties: {
21
+ provider: { type: 'string' },
22
+ distanceM: { type: 'number' },
23
+ durationS: { type: 'number' },
24
+ polyline: { type: 'string' },
25
+ steps: {
26
+ type: 'array',
27
+ items: {
28
+ type: 'object',
29
+ additionalProperties: false,
30
+ properties: {
31
+ instruction: { type: 'string' },
32
+ distanceM: { type: 'number' },
33
+ durationS: { type: 'number' },
34
+ },
35
+ },
36
+ },
37
+ alternatives: {
38
+ type: 'array',
39
+ items: {
40
+ type: 'object',
41
+ additionalProperties: false,
42
+ properties: {
43
+ provider: { type: 'string' },
44
+ distanceM: { type: 'number' },
45
+ durationS: { type: 'number' },
46
+ steps: {
47
+ type: 'array',
48
+ items: {
49
+ type: 'object',
50
+ additionalProperties: false,
51
+ properties: {
52
+ instruction: { type: 'string' },
53
+ distanceM: { type: 'number' },
54
+ durationS: { type: 'number' },
55
+ },
56
+ },
57
+ },
58
+ },
59
+ },
60
+ },
61
+ },
62
+ },
63
+ render: (_args, value) => {
64
+ const v = value;
65
+ const mins = (s) => Math.round(s / 60);
66
+ const providerName = v.provider === 'amap' ? '高德' : 'OSRM';
67
+ const distanceText = v.distanceM >= 1000 ? `${(v.distanceM / 1000).toFixed(1)} 公里` : `${v.distanceM} 米`;
68
+ // Amap v5 transit returns no duration field — report it as unknown.
69
+ const durationText = v.durationS > 0 ? `,约 ${mins(v.durationS)} 分钟` : '';
70
+ const lines = [
71
+ `${providerName} 路线:${distanceText}${durationText}`,
72
+ ];
73
+ for (const s of v.steps.slice(0, 12)) {
74
+ lines.push(`- ${s.instruction}`);
75
+ }
76
+ if (v.steps.length > 12)
77
+ lines.push(`- …(共 ${v.steps.length} 步)`);
78
+ if (v.alternatives?.length) {
79
+ lines.push(`另有 ${v.alternatives.length} 条备选路线:`);
80
+ for (const a of v.alternatives) {
81
+ const aName = a.provider === 'amap' ? '高德' : 'OSRM';
82
+ lines.push(` - ${aName}:${(a.distanceM / 1000).toFixed(1)} 公里 / ${mins(a.durationS)} 分钟`);
83
+ }
84
+ }
85
+ return [{ type: 'text', text: lines.join('\n') }];
86
+ },
87
+ },
88
+ async execute(args, exec) {
89
+ let origin;
90
+ let destination;
91
+ try {
92
+ origin = parseLngLat(args.origin) ?? (await clients.resolve(args.origin, exec.signal));
93
+ destination = parseLngLat(args.destination) ?? (await clients.resolve(args.destination, exec.signal));
94
+ }
95
+ catch (err) {
96
+ // 地址解析阶段的高德配额超限:没有坐标就无法降级,转成可操作提示。
97
+ if (err instanceof AmapQuotaError) {
98
+ throw new Error(`地址解析暂时不可用(${err.message})。请稍后重试,或直接提供 "lng,lat" 坐标。`);
99
+ }
100
+ throw err;
101
+ }
102
+ // Provider priority: Amap → OSRM free fallback.
103
+ if (clients.amap) {
104
+ try {
105
+ let city1;
106
+ let city2;
107
+ try {
108
+ city1 = mode === 'transit' ? await clients.resolveCity(args.origin, exec.signal) : undefined;
109
+ city2 = mode === 'transit' ? await clients.resolveCity(args.destination, exec.signal) : undefined;
110
+ }
111
+ catch (err) {
112
+ // 城市解析也受高德配额影响;公交没有 city 无法请求,但非公交不受影响。
113
+ if (err instanceof AmapQuotaError && mode === 'transit') {
114
+ throw new Error(`公交路线需要城市解析,但高德暂不可用(${err.message})。请稍后重试,或改用驾车/步行/骑行路线(自动降级 OSRM 免费源)。`);
115
+ }
116
+ throw err;
117
+ }
118
+ const result = await clients.amap.route(origin, destination, mode, { city1, city2 }, exec.signal);
119
+ const out = {
120
+ provider: result.provider,
121
+ distanceM: result.distanceM,
122
+ durationS: result.durationS,
123
+ polyline: result.polyline,
124
+ steps: result.steps,
125
+ };
126
+ if (args.waypoints && mode === 'driving') {
127
+ out.waypoints = args.waypoints.split(';').map((w) => w.trim()).filter(Boolean).join(';');
128
+ }
129
+ return out;
130
+ }
131
+ catch (err) {
132
+ // 高德配额超限(QPS/日配额):非公交模式自动降级 OSRM 兜底,
133
+ // 公交无免费兜底源,给用户可操作的提示。
134
+ if (err instanceof AmapQuotaError) {
135
+ if (mode === 'transit') {
136
+ throw new Error(`高德公交路线暂不可用(${err.message})。请稍后重试,或改用驾车/步行/骑行路线(自动降级 OSRM 免费源)。`);
137
+ }
138
+ const result = await clients.osrm.route(origin, destination, osrmProfile, exec.signal);
139
+ return {
140
+ provider: result.provider,
141
+ distanceM: result.distanceM,
142
+ durationS: result.durationS,
143
+ polyline: result.polyline,
144
+ steps: result.steps,
145
+ };
146
+ }
147
+ throw err;
148
+ }
149
+ }
150
+ // Fallback: OSRM (driving/walking/cycling only; no transit).
151
+ if (mode === 'transit') {
152
+ throw new Error('公交路线需要高德 key。请在插件配置中设置 amapKey(https://console.amap.com/dev/key/app)。');
153
+ }
154
+ const result = await clients.osrm.route(origin, destination, osrmProfile, exec.signal);
155
+ return {
156
+ provider: result.provider,
157
+ distanceM: result.distanceM,
158
+ durationS: result.durationS,
159
+ polyline: result.polyline,
160
+ steps: result.steps,
161
+ };
162
+ },
163
+ });
164
+ }
165
+ export function registerRouteTools(ctx, clients, disposers = []) {
166
+ disposers.push(ctx.tools.register(routeTool('map_driving_route', '规划驾车路线,返回距离、预计耗时和分段指引。起点/终点支持地址或 "lng,lat" 坐标。', 'driving', clients)));
167
+ disposers.push(ctx.tools.register(routeTool('map_transit_route', '规划公交/地铁换乘路线(需配置高德 amapKey),返回距离、耗时和多段换乘指引。', 'transit', clients)));
168
+ disposers.push(ctx.tools.register(routeTool('map_walking_route', '规划步行路线,返回距离、预计耗时和分段指引。', 'walking', clients)));
169
+ disposers.push(ctx.tools.register(routeTool('map_bicycling_route', '规划骑行路线,返回距离、预计耗时和分段指引。', 'bicycling', clients)));
170
+ }