@danceiny/gotry 0.0.1-rc.18 → 0.0.1-rc.20

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 (176) hide show
  1. package/README.md +42 -16
  2. package/README.zh-CN.md +37 -15
  3. package/bin/gotry-bootstrap.js +309 -30
  4. package/bin/gotry-inner.js +74 -23
  5. package/bin/gotry-runtime-resolution.d.ts +1 -1
  6. package/bin/gotry-runtime-resolution.js +7 -8
  7. package/cordis.gotry-patch.yml +22 -8
  8. package/data/stations-12306-verify.json +524 -0
  9. package/dist/capabilities/agent-reach.js +4 -4
  10. package/dist/capabilities/anything.js +62 -29
  11. package/dist/capabilities/channel-health.js +103 -0
  12. package/dist/capabilities/channel-registry.js +213 -0
  13. package/dist/capabilities/doctor.js +307 -0
  14. package/dist/capabilities/effect.js +178 -5
  15. package/dist/capabilities/flyai.js +13 -0
  16. package/dist/capabilities/hbcli.js +28 -2
  17. package/dist/capabilities/session/adapters/ctrip-hotel.js +207 -0
  18. package/dist/capabilities/session/adapters/rail-12306.js +285 -0
  19. package/dist/capabilities/session/extension-bridge.js +14 -1
  20. package/dist/capabilities/session/extension-channel.js +4 -2
  21. package/dist/capabilities/session/extension-distribution.js +1 -1
  22. package/dist/capabilities/session-search.js +311 -0
  23. package/dist/capabilities/visa-policy.js +126 -0
  24. package/dist/scripts/agent-planning-turn-deadline-e2e.js +305 -0
  25. package/dist/scripts/agent-planning-turn-deadline-tests.js +281 -0
  26. package/dist/scripts/anything-tests.js +70 -21
  27. package/dist/scripts/benchmark-environment-bridge-e2e.js +139 -37
  28. package/dist/scripts/benchmark-environment-bridge-tests.js +1099 -306
  29. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +26 -26
  30. package/dist/scripts/{booking-copilot-availability-policy-v2-tests.js → booking-copilot-availability-policy-tests.js} +115 -115
  31. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +41 -1
  32. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +75 -120
  33. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +370 -61
  34. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +2 -2
  35. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +7 -3
  36. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +15 -9
  37. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +19 -11
  38. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +16 -52
  39. package/dist/scripts/booking-copilot-runtime-proof-tests.js +3953 -155
  40. package/dist/scripts/booking-copilot-server-proof-tests.js +58 -23
  41. package/dist/scripts/booking-copilot-startup-proof-tests.js +10 -119
  42. package/dist/scripts/booking-surface-contract-proof-tests.js +1154 -326
  43. package/dist/scripts/bootstrap-tests.js +113 -2
  44. package/dist/scripts/build-metrics-report.js +339 -0
  45. package/dist/scripts/channel-probe-tests.js +148 -0
  46. package/dist/scripts/channel-probe.js +252 -0
  47. package/dist/scripts/channel-registry-tests.js +262 -0
  48. package/dist/scripts/doctor-tests.js +175 -0
  49. package/dist/scripts/effect-tests.js +274 -1
  50. package/dist/scripts/extension-tests.js +129 -11
  51. package/dist/scripts/fact-gate-tests.js +91 -2
  52. package/dist/scripts/flyai-tests.js +17 -1
  53. package/dist/scripts/hbcli-e2e-tests.js +1 -3
  54. package/dist/scripts/hbcli-tests.js +38 -1
  55. package/dist/scripts/metrics-report-tests.js +288 -0
  56. package/dist/scripts/persona-surface-guard-tests.js +19 -0
  57. package/dist/scripts/session-tests.js +401 -1
  58. package/dist/scripts/smoke.js +100 -83
  59. package/dist/scripts/turn-handoff-collect-tests.js +216 -0
  60. package/dist/scripts/turn-handoff-collect.js +189 -0
  61. package/dist/scripts/turn-policy-tests.js +56 -0
  62. package/dist/scripts/typed-contract-canary.js +261 -0
  63. package/dist/scripts/visa-policy-tests.js +75 -0
  64. package/dist/scripts/wish-channel-gate-tests.js +111 -0
  65. package/dist/src/artifact-gate.js +91 -3
  66. package/dist/src/benchmark-agent-conformance.js +39 -16
  67. package/dist/src/benchmark-environment-bridge.js +316 -52
  68. package/dist/src/bookable-facts.js +62 -3
  69. package/dist/src/booking-surface/{availability-policy-v2.js → availability-policy.js} +89 -89
  70. package/dist/src/booking-surface/canonical-schema.js +8 -22
  71. package/dist/src/booking-surface/contracts.js +70 -19
  72. package/dist/src/booking-surface/dsh-planner.js +310 -177
  73. package/dist/src/booking-surface/dsh-plugin.js +3 -3
  74. package/dist/src/booking-surface/index.js +1 -5
  75. package/dist/src/booking-surface/profile.js +1 -1
  76. package/dist/src/booking-surface/runtime.js +1682 -247
  77. package/dist/src/booking-surface/server.js +371 -185
  78. package/dist/src/booking-surface/startup.js +17 -27
  79. package/dist/src/booking-surface/validation.js +270 -760
  80. package/dist/src/index.js +739 -150
  81. package/dist/src/turn-deadline.js +292 -0
  82. package/dist/src/turn-policy.js +146 -0
  83. package/dist/src/wish-pool.js +5 -0
  84. package/extension/background.js +23 -4
  85. package/extension/content-main.js +47 -9
  86. package/extension/manifest.json +27 -9
  87. package/package.json +7 -26
  88. package/schemas/booking.surface.schema.json +2593 -0
  89. package/ts/capabilities/agent-reach.ts +4 -4
  90. package/ts/capabilities/anything.ts +89 -38
  91. package/ts/capabilities/flyai.ts +21 -3
  92. package/ts/capabilities/hbcli.ts +53 -4
  93. package/ts/capabilities/session/action-cache.ts +1 -1
  94. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  95. package/ts/capabilities/session/adapters/rail-12306.ts +293 -0
  96. package/ts/capabilities/session/extension-bridge.ts +32 -2
  97. package/ts/capabilities/session/extension-channel.ts +9 -8
  98. package/ts/capabilities/session/extension-distribution.ts +3 -1
  99. package/ts/capabilities/session/health-watch.ts +1 -1
  100. package/ts/capabilities/session/read-guard.ts +1 -1
  101. package/ts/capabilities/session/wizard.ts +1 -1
  102. package/ts/capabilities/session-search.ts +323 -1
  103. package/ts/dsh-runtime/vendor/README.md +60 -0
  104. package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
  105. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +200 -0
  106. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +202 -0
  107. package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +216 -0
  108. package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
  109. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +371 -0
  110. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
  111. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +56 -0
  112. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
  113. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
  114. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +126 -0
  115. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
  116. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +109 -0
  117. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +27 -0
  118. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
  119. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
  120. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +170 -0
  121. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +53 -0
  122. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
  123. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
  124. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
  125. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
  126. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +20 -0
  127. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
  128. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
  129. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
  130. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
  131. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
  132. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +16 -0
  133. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +55 -0
  134. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
  135. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +91 -0
  136. package/ts/package.json +9 -0
  137. package/ts/scripts/turn-handoff-collect.ts +178 -0
  138. package/ts/src/artifact-gate.ts +82 -2
  139. package/ts/src/benchmark-agent-conformance.ts +54 -15
  140. package/ts/src/benchmark-environment-bridge.ts +234 -46
  141. package/ts/src/bookable-facts.ts +109 -5
  142. package/ts/src/booking-saga.ts +1 -1
  143. package/ts/src/booking-surface/{availability-policy-v2.ts → availability-policy.ts} +124 -125
  144. package/ts/src/booking-surface/canonical-schema.js +8 -22
  145. package/ts/src/booking-surface/contracts.ts +278 -239
  146. package/ts/src/booking-surface/dsh-planner.ts +333 -194
  147. package/ts/src/booking-surface/dsh-plugin.js +3 -3
  148. package/ts/src/booking-surface/index.ts +1 -5
  149. package/ts/src/booking-surface/profile.ts +11 -11
  150. package/ts/src/booking-surface/runtime.ts +1351 -351
  151. package/ts/src/booking-surface/server.ts +282 -175
  152. package/ts/src/booking-surface/startup.ts +36 -48
  153. package/ts/src/booking-surface/validation.ts +194 -442
  154. package/ts/src/contracts.ts +1 -1
  155. package/ts/src/index.ts +492 -172
  156. package/ts/src/loop.ts +1 -1
  157. package/ts/src/state-ledger.ts +1 -1
  158. package/ts/src/turn-deadline.ts +365 -0
  159. package/ts/src/turn-policy.ts +154 -0
  160. package/ts/src/wish-pool.ts +17 -4
  161. package/dist/scripts/agent-planning-budget-e2e.js +0 -227
  162. package/dist/scripts/agent-planning-budget-tests.js +0 -173
  163. package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +0 -3935
  164. package/dist/scripts/booking-surface-v2-contract-proof-tests.js +0 -1174
  165. package/dist/src/booking-surface/contracts-v2.js +0 -89
  166. package/dist/src/booking-surface/runtime-v2.js +0 -1771
  167. package/dist/src/booking-surface/server-v2.js +0 -334
  168. package/dist/src/booking-surface/validation-v2.js +0 -319
  169. package/dist/src/tool-budget.js +0 -136
  170. package/schemas/booking.surface.v1.schema.json +0 -927
  171. package/schemas/booking.surface.v2.schema.json +0 -61
  172. package/ts/src/booking-surface/contracts-v2.ts +0 -118
  173. package/ts/src/booking-surface/runtime-v2.ts +0 -1466
  174. package/ts/src/booking-surface/server-v2.ts +0 -247
  175. package/ts/src/booking-surface/validation-v2.ts +0 -205
  176. package/ts/src/tool-budget.ts +0 -165
@@ -6,7 +6,7 @@
6
6
  * cdp(attach 日常 Chrome,Chrome 144+ 每连接弹权限框)降为显式后备
7
7
  * (`GOTRY_SESSION_TRANSPORT=cdp` opt-in,诊断/测试用);persistent 仅测试。
8
8
  *
9
- * 证据链(L4 增补):[会话:ctrip-flight@ts] = 用户本人会话内实时检索,非官方 API;
9
+ * 证据链(L4 增补):[会话:ctrip-flight@ts] / [会话:ctrip-hotel@ts] = 用户本人会话内实时检索,非官方 API;
10
10
  * 风控命中(verdict='challenged')= degraded,绝不重试、绝不绕过(合规支柱②)。
11
11
  * 节律(§3.4):同站点 ≥30s 间隔 + 单调冷却;超间隔返回 verdict='cooldown'。
12
12
  * 永不抛错;扩展车道 fail-closed(桥/扩展不可用即 verdict,零花费);测试/巡检用隔离 profile 与 stateRoot。
@@ -17,6 +17,8 @@ import { extensionCookieNames, extensionSearchJob, classifyBridgeFailure } from
17
17
  import { appendFileSync, mkdirSync } from 'node:fs'
18
18
  import { dirname } from 'node:path'
19
19
  import { buildEntryUrl, NETWORK_HINTS, parseBatchSearch, LOGIN_COOKIE_NAMES, SITE_DOMAIN, type SessionFlightOption } from './session/adapters/ctrip-flight.ts'
20
+ import { buildHotelEntryUrl, parseCtripHotelList, HOTEL_SITE_HOST, type SessionHotelOption } from './session/adapters/ctrip-hotel.ts'
21
+ import { buildTrainEntryUrl, parseLeftTicketQuery, TRAIN_SITE_HOST, type SessionTrainOption } from './session/adapters/rail-12306.ts'
20
22
  import { EXTENSION_STORE_URL } from './session/extension-bridge.ts'
21
23
 
22
24
  export type SessionVerdict = 'hit' | 'miss' | 'error' | 'challenged' | 'cooldown' | 'needs-login' | 'needs-attach' | 'needs-extension'
@@ -109,6 +111,326 @@ export function appendExtensionAudit(auditPath: string | undefined, entry: { kin
109
111
  } catch { /* 审计失败不阻塞检索 */ }
110
112
  }
111
113
 
114
+ // ---------------------------------------------------------------------------
115
+ // 酒店会话检索(2026-09-03 实装;与机票同构:节律闸/登录闸/needs-extension 映射/
116
+ // 挑战红线/证据链。酒店页与机票页同属 ctrip.com 风控域,但通道键独立——
117
+ // 各自 30s 预算已足够礼貌,跨通道合并预算留首个真会话风控观测后校准)
118
+ // ---------------------------------------------------------------------------
119
+
120
+ export interface SessionHotelQuery {
121
+ /** 目的地(中文);城市码表未收录时须带 cityId */
122
+ to: string
123
+ /** 显式城市 id(携程/trip.com 酒店 list 页 URL 的 city= 数字;覆盖码表) */
124
+ cityId?: number | string
125
+ /** YYYY-MM-DD */
126
+ checkIn?: string
127
+ /** YYYY-MM-DD(与 checkIn 成对) */
128
+ checkOut?: string
129
+ adults?: number
130
+ /** 隔离 profile 目录(测试必传;默认 /tmp 专用目录) */
131
+ profileDir?: string
132
+ headless?: boolean
133
+ /** ReadGuard 审计路径(测试传隔离 stateRoot 下) */
134
+ auditPath?: string
135
+ /** 等嗅探回包的上限,默认 30_000(酒店列表接口比机票慢) */
136
+ timeoutMs?: number
137
+ /** 允许匿名实例(默认 false);true 仅适配器链路自检,证据链标 anonymous */
138
+ allowAnonymous?: boolean
139
+ }
140
+
141
+ export interface SessionHotelResult {
142
+ ok: boolean
143
+ via: 'session-ctrip-hotel' | 'session-ctrip-hotel-error'
144
+ evidence: string
145
+ latencyMs: number
146
+ verdict: SessionVerdict
147
+ hotels?: SessionHotelOption[]
148
+ error?: string
149
+ /** needs-extension 时给出 Chrome Web Store URL(dsh UI 渲成可点链接) */
150
+ installUrl?: string
151
+ installAction?: 'add-to-chrome'
152
+ }
153
+
154
+ /** 酒店城市码表未收录时的人话指引(纯函数,测试锚点) */
155
+ export function hotelCityUnresolvedHint(cities: string[]): string {
156
+ return `城市 ${cities.join('/')} 不在携程酒店城市码表——先 web 搜「hotels.ctrip.com ${cities[0] ?? ''} 酒店」拿到 list 页 URL 里的 city= 数字,带 cityId 重试`
157
+ }
158
+
159
+ export async function sessionHotelSearch(q: SessionHotelQuery): Promise<SessionHotelResult> {
160
+ const started = Date.now()
161
+ const ts = new Date().toISOString()
162
+ const site = 'ctrip-hotel'
163
+ const err = (verdict: SessionVerdict, error: string): SessionHotelResult => ({
164
+ ok: false, via: 'session-ctrip-hotel-error', evidence: `[会话:${site}@error@${ts}] ${error}`, latencyMs: Date.now() - started, verdict, error,
165
+ })
166
+
167
+ // 日期对闸(与 flyai hotel 同口径):成对且 YYYY-MM-DD;过去日期不发上游
168
+ if ((q.checkIn ? 1 : 0) !== (q.checkOut ? 1 : 0) || (q.checkIn && !/^\d{4}-\d{2}-\d{2}$/.test(q.checkIn))) {
169
+ return err('error', 'checkIn/checkOut 须成对且为 YYYY-MM-DD(未定档期可不带日期)')
170
+ }
171
+ if (q.checkIn) {
172
+ const now = new Date()
173
+ const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
174
+ if (q.checkIn < today || (q.checkOut ?? '') < q.checkIn) {
175
+ return err('error', `入住 ${q.checkIn}/退房 ${q.checkOut ?? ''} 不是未来合法区间(今天 ${today})——向用户确认日期后再查`)
176
+ }
177
+ }
178
+
179
+ // 节律闸:超间隔即拒,不发起导航
180
+ const last = lastCallAt.get(site) ?? 0
181
+ if (Date.now() - last < MIN_INTERVAL_MS) {
182
+ return err('cooldown', `rate limit: last call ${Date.now() - last}ms ago, min ${MIN_INTERVAL_MS}ms`)
183
+ }
184
+ lastCallAt.set(site, Date.now())
185
+
186
+ const entry = buildHotelEntryUrl({ to: q.to, cityId: q.cityId, checkIn: q.checkIn, checkOut: q.checkOut, adults: q.adults })
187
+ if (!entry.ok || !entry.url) {
188
+ return err('error', hotelCityUnresolvedHint(entry.unresolved ?? [q.to]))
189
+ }
190
+
191
+ const mode = resolveTransportMode(q.profileDir)
192
+
193
+ if (mode === 'extension') {
194
+ // ① 登录态快查(与机票同账号体系,票据 cookie 名一致)
195
+ const login = await extensionCookieNames({ site, domain: SITE_DOMAIN.replace(/^\./, ''), ticketNames: LOGIN_COOKIE_NAMES })
196
+ if (!login.ok) {
197
+ const verdict = classifyBridgeFailure(login.kind)
198
+ if (verdict === 'needs-extension') {
199
+ return { ok: false, via: 'session-ctrip-hotel-error', evidence: '[会话:ctrip-hotel-needs-extension@ts]', latencyMs: Date.now() - started, verdict, error: login.summary, installUrl: EXTENSION_STORE_URL, installAction: 'add-to-chrome' as const }
200
+ }
201
+ return err(verdict, login.summary)
202
+ }
203
+ if (login.tickets.length === 0 && !q.allowAnonymous) {
204
+ return err('needs-login', '未检出你本人登录态——调用 gotry_session_login 为用户打开携程登录入口(登录在携程官网完成;gotry 永不经手密码/验证码/cookie 值)')
205
+ }
206
+ // ② 检索 job:后台标签 + 被动嗅探(URL hint + 形状兜底;扩展零写行为)
207
+ const r = await extensionSearchJob({ site, url: entry.url, timeoutMs: q.timeoutMs })
208
+ appendExtensionAudit(q.auditPath, {
209
+ kind: 'extension-session-job', site, url: entry.url, jobId: 'search',
210
+ result: r.ok ? (r.timedOut ? 'timeout' : `body ${r.body.length}B title="${r.title.slice(0, 60)}"`) : `${r.kind}:${r.summary.slice(0, 120)}`,
211
+ })
212
+ if (!r.ok) {
213
+ const verdict = classifyBridgeFailure(r.kind)
214
+ if (verdict === 'needs-extension') {
215
+ return { ok: false, via: 'session-ctrip-hotel-error', evidence: '[会话:ctrip-hotel-needs-extension@ts]', latencyMs: Date.now() - started, verdict, error: r.summary, installUrl: EXTENSION_STORE_URL, installAction: 'add-to-chrome' as const }
216
+ }
217
+ return err(verdict, r.summary)
218
+ }
219
+ const title = r.title
220
+ const head = r.body.slice(0, 5000)
221
+ if (CHALLENGE_RE.test(title + head)) {
222
+ return err('challenged', `风控/验证码命中(title=${title.slice(0, 60)});按红线不重试不绕过,交还用户`)
223
+ }
224
+ const hotels = parseCtripHotelList(r.body)
225
+ const verdict: SessionVerdict = hotels.length > 0 ? 'hit' : 'miss'
226
+ return {
227
+ ok: true,
228
+ via: 'session-ctrip-hotel',
229
+ evidence: `[会话:${site}@${ts}] ${hotels.length} hotels;transport=extension(被动嗅探,零系统弹窗;扩展零写行为=物理只读)${q.allowAnonymous ? ';anonymous=自检态' : ''}`,
230
+ latencyMs: Date.now() - started,
231
+ verdict,
232
+ hotels,
233
+ }
234
+ }
235
+
236
+ // cdp/persistent 车道:与机票同构——挂监听等 NETWORK hint 回包(酒店用 HOTEL hints + 形状兜底)
237
+ const t = await openSession({ profileDir: q.profileDir, headless: q.headless, auditPath: q.auditPath, mode: mode === 'persistent' ? 'persistent' : 'cdp', newPage: true })
238
+ if (!t.ok) {
239
+ return err(classifyTransportFailure(t.summary, q.profileDir === undefined), t.summary)
240
+ }
241
+ try {
242
+ const loggedIn = async (): Promise<boolean> => {
243
+ const cookies = await t.browser.cookies().catch(() => [])
244
+ return cookies.some((c) => c.domain.includes(SITE_DOMAIN.replace(/^\./, '')) && LOGIN_COOKIE_NAMES.includes(c.name))
245
+ }
246
+ if (!(await loggedIn()) && !q.allowAnonymous) {
247
+ return err('needs-login', '未检出你本人登录态——调用 gotry_session_login 为用户打开携程登录入口(登录在携程官网完成;gotry 永不经手密码/验证码/cookie 值)')
248
+ }
249
+ let settled = false
250
+ let body = ''
251
+ const heard = new Promise<void>((resolve) => {
252
+ t.page.on('response', async (res) => {
253
+ if (settled) return
254
+ const u = res.url()
255
+ const hostHit = u.includes(HOTEL_SITE_HOST)
256
+ if (!hostHit) return
257
+ try {
258
+ const text = await res.text()
259
+ if (text && (hostHit && /hotels\.ctrip\.com\/(hotels\/api|domestic\/pc\/api)|GetHotelListBySOA|GetHotelListByCity|HotelSearch|hotelsearch/i.test(u) || text.length <= 2_000_000 && /"hotelList"|"hotelMatchInfos"|"hotelName"/.test(text))) {
260
+ body = text
261
+ settled = true
262
+ resolve()
263
+ }
264
+ } catch { /* 流式/竞态不可读则继续等下一个 */ }
265
+ })
266
+ setTimeout(() => resolve(), q.timeoutMs ?? 30_000)
267
+ })
268
+ await t.page.goto(entry.url, { waitUntil: 'domcontentloaded', timeout: 30_000 })
269
+ await heard
270
+ const title = await t.page.title().catch(() => '')
271
+ const headHtml = (await t.page.content().catch(() => '')).slice(0, 5000)
272
+ if (CHALLENGE_RE.test(title + headHtml)) {
273
+ return err('challenged', `风控/验证码命中(title=${title.slice(0, 60)});按红线不重试不绕过,交还用户`)
274
+ }
275
+ const hotels = parseCtripHotelList(body)
276
+ const verdict: SessionVerdict = hotels.length > 0 ? 'hit' : 'miss'
277
+ return {
278
+ ok: true,
279
+ via: 'session-ctrip-hotel',
280
+ evidence: `[会话:${site}@${ts}] ${hotels.length} hotels;guard blocked=${t.guard.blockedCount()}/${t.guard.requestCount()}${q.allowAnonymous ? ';anonymous=自检态' : ''}`,
281
+ latencyMs: Date.now() - started,
282
+ verdict,
283
+ hotels,
284
+ }
285
+ } catch (e) {
286
+ return err('error', e instanceof Error ? e.message.slice(0, 200) : String(e))
287
+ } finally {
288
+ await t.close()
289
+ }
290
+ }
291
+
292
+
293
+ // ---------------------------------------------------------------------------
294
+ // 火车会话检索(2026-09-03 实装;12306 余票查询是公开面——登录只关系下单,
295
+ // 无账号数据过手,故无登录闸;扩展照样只读被动嗅探,证据链标注「公开查询面」)
296
+ // ---------------------------------------------------------------------------
297
+
298
+ export interface SessionTrainQuery {
299
+ from: string
300
+ to: string
301
+ /** YYYY-MM-DD */
302
+ date: string
303
+ /** 显式城市电报码(覆盖码表;kyfw 查询页 URL fs=城市,XXX 的三位码) */
304
+ fromStationTelecode?: string
305
+ toStationTelecode?: string
306
+ /** 隔离 profile 目录(测试必传;默认 /tmp 专用目录) */
307
+ profileDir?: string
308
+ headless?: boolean
309
+ /** ReadGuard 审计路径(测试传隔离 stateRoot 下) */
310
+ auditPath?: string
311
+ /** 等嗅探回包的上限,默认 25_000 */
312
+ timeoutMs?: number
313
+ }
314
+
315
+ export interface SessionTrainResult {
316
+ ok: boolean
317
+ via: 'session-train-12306' | 'session-train-12306-error'
318
+ evidence: string
319
+ latencyMs: number
320
+ verdict: SessionVerdict
321
+ trains?: SessionTrainOption[]
322
+ error?: string
323
+ /** needs-extension 时给出 Chrome Web Store URL(dsh UI 渲成可点链接) */
324
+ installUrl?: string
325
+ installAction?: 'add-to-chrome'
326
+ }
327
+
328
+ /** 车站电报码表未收录时的人话指引(纯函数,测试锚点) */
329
+ export function trainStationUnresolvedHint(cities: string[]): string {
330
+ return `城市 ${cities.join('/')} 不在 12306 城市电报码表——web 搜「12306 ${cities[0] ?? ''} 余票」拿到 kyfw 查询页 URL 里的 fs=城市,XXX 三位码,带 fromStationTelecode/toStationTelecode 重试`
331
+ }
332
+
333
+ export async function sessionTrainSearch(q: SessionTrainQuery): Promise<SessionTrainResult> {
334
+ const started = Date.now()
335
+ const ts = new Date().toISOString()
336
+ const site = 'train-12306'
337
+ const err = (verdict: SessionVerdict, error: string): SessionTrainResult => ({
338
+ ok: false, via: 'session-train-12306-error', evidence: `[会话:${site}@error@${ts}] ${error}`, latencyMs: Date.now() - started, verdict, error,
339
+ })
340
+
341
+ // 节律闸:超间隔即拒,不发起导航
342
+ const last = lastCallAt.get(site) ?? 0
343
+ if (Date.now() - last < MIN_INTERVAL_MS) {
344
+ return err('cooldown', `rate limit: last call ${Date.now() - last}ms ago, min ${MIN_INTERVAL_MS}ms`)
345
+ }
346
+ lastCallAt.set(site, Date.now())
347
+
348
+ const entry = buildTrainEntryUrl({ from: q.from, to: q.to, date: q.date, fromStationTelecode: q.fromStationTelecode, toStationTelecode: q.toStationTelecode })
349
+ if (!entry.ok || !entry.url) {
350
+ return err('error', trainStationUnresolvedHint(entry.unresolved ?? [q.from, q.to]))
351
+ }
352
+
353
+ const mode = resolveTransportMode(q.profileDir)
354
+
355
+ if (mode === 'extension') {
356
+ // 无登录闸(公开查询面):直接发起检索 job
357
+ const r = await extensionSearchJob({ site, url: entry.url, timeoutMs: q.timeoutMs })
358
+ appendExtensionAudit(q.auditPath, {
359
+ kind: 'extension-session-job', site, url: entry.url, jobId: 'search',
360
+ result: r.ok ? (r.timedOut ? 'timeout' : `body ${r.body.length}B title="${r.title.slice(0, 60)}"`) : `${r.kind}:${r.summary.slice(0, 120)}`,
361
+ })
362
+ if (!r.ok) {
363
+ const verdict = classifyBridgeFailure(r.kind)
364
+ if (verdict === 'needs-extension') {
365
+ return { ok: false, via: 'session-train-12306-error', evidence: '[会话:train-12306-needs-extension@ts]', latencyMs: Date.now() - started, verdict, error: r.summary, installUrl: EXTENSION_STORE_URL, installAction: 'add-to-chrome' as const }
366
+ }
367
+ return err(verdict, r.summary)
368
+ }
369
+ const title = r.title
370
+ const head = r.body.slice(0, 5000)
371
+ if (CHALLENGE_RE.test(title + head)) {
372
+ return err('challenged', `风控/验证码命中(title=${title.slice(0, 60)});按红线不重试不绕过,交还用户`)
373
+ }
374
+ const trains = parseLeftTicketQuery(r.body, entry.url)
375
+ const verdict: SessionVerdict = trains.length > 0 ? 'hit' : 'miss'
376
+ return {
377
+ ok: true,
378
+ via: 'session-train-12306',
379
+ evidence: `[会话:${site}@${ts}] ${trains.length} trains;transport=extension(公开查询面,被动嗅探,零系统弹窗;扩展零写行为=物理只读)`,
380
+ latencyMs: Date.now() - started,
381
+ verdict,
382
+ trains,
383
+ }
384
+ }
385
+
386
+ // cdp/persistent 车道:与机/酒同构
387
+ const t = await openSession({ profileDir: q.profileDir, headless: q.headless, auditPath: q.auditPath, mode: mode === 'persistent' ? 'persistent' : 'cdp', newPage: true })
388
+ if (!t.ok) {
389
+ return err(classifyTransportFailure(t.summary, q.profileDir === undefined), t.summary)
390
+ }
391
+ try {
392
+ let settled = false
393
+ let body = ''
394
+ const heard = new Promise<void>((resolve) => {
395
+ t.page.on('response', async (res) => {
396
+ if (settled) return
397
+ const u = res.url()
398
+ if (!/leftTicket\/query/i.test(u)) return
399
+ try {
400
+ const text = await res.text()
401
+ if (text) {
402
+ body = text
403
+ settled = true
404
+ resolve()
405
+ }
406
+ } catch { /* 流式/竞态不可读则继续等下一个 */ }
407
+ })
408
+ setTimeout(() => resolve(), q.timeoutMs ?? 25_000)
409
+ })
410
+ await t.page.goto(entry.url, { waitUntil: 'domcontentloaded', timeout: 30_000 })
411
+ await heard
412
+ const title = await t.page.title().catch(() => '')
413
+ const headHtml = (await t.page.content().catch(() => '')).slice(0, 5000)
414
+ if (CHALLENGE_RE.test(title + headHtml)) {
415
+ return err('challenged', `风控/验证码命中(title=${title.slice(0, 60)});按红线不重试不绕过,交还用户`)
416
+ }
417
+ const trains = parseLeftTicketQuery(body, entry.url)
418
+ const verdict: SessionVerdict = trains.length > 0 ? 'hit' : 'miss'
419
+ return {
420
+ ok: true,
421
+ via: 'session-train-12306',
422
+ evidence: `[会话:${site}@${ts}] ${trains.length} trains(公开查询面);guard blocked=${t.guard.blockedCount()}/${t.guard.requestCount()}`,
423
+ latencyMs: Date.now() - started,
424
+ verdict,
425
+ trains,
426
+ }
427
+ } catch (e) {
428
+ return err('error', e instanceof Error ? e.message.slice(0, 200) : String(e))
429
+ } finally {
430
+ await t.close()
431
+ }
432
+ }
433
+
112
434
  export async function sessionFlightSearch(q: SessionFlightQuery): Promise<SessionSearchResult> {
113
435
  const started = Date.now()
114
436
  const ts = new Date().toISOString()
@@ -0,0 +1,60 @@
1
+ # vendored dsh runtime(`vendor/`)
2
+
3
+ 本目录是 [DeepSeek Harness(dsh CLI)](https://github.com/deepseek-ai/DeepSeek-Harness)
4
+ dsh 发布家族的全量 vendored 成员,pnpm workspace 方式参与安装
5
+ (`ts/dsh-runtime/pnpm-workspace.yaml` 声明 `vendor/*`,`linkWorkspacePackages: true`)。
6
+
7
+ ## 当前版本
8
+
9
+ - 上游:`dsh-v0.1.2-alpha.1`(tag;alpha 面向 monorepo 内部,尚未发布到
10
+ npm 公共 registry——npm latest 仍为 `0.1.1-rc.2`,等上游 publish 后本目录
11
+ 可整体换回 npm 依赖形态)。
12
+ - 源 commit:`cd5ef814`(/tmp 构建树为浅克隆同源;发布 tag 见上游 releases)。
13
+ - License:MIT © DeepSeek(upstream LICENSE);本仓 MIT 使用。
14
+
15
+ ## 为什么 vendored(而不是 npm 依赖)
16
+
17
+ - 上游只挂 GitHub 预发布 tag;依赖闭包(dsh 家族 ~187 个内部包 + cordis/
18
+ schemastery 等 vendor 家族)里 **dsh 家族全部 0.1.2-alpha.1 均不在公共
19
+ npm**,外部环境不可安装。
20
+ - `ts/dsh-runtime` 只影响 repo 工作副本与 `./gotry`;npm 公共分发面
21
+ (root `package.json`)仍钉 rc.2,不受影响;CI 不安装本目录。
22
+ - workspace 成员 + `linkWorkspacePackages: true` 让成员间
23
+ `^0.1.2-alpha.1` 的 range 直接命中本地版本,干净克隆只依赖本仓文件 +
24
+ 公共 registry(第三方),不依赖内部镜像。
25
+
26
+ ## 非 dsh 家族成员:`vendor/dsh-map-tools`
27
+
28
+ `dsh-map-tools`(dshmarket 第三方宿主插件,地图/路线/POI,零 key 走
29
+ OSM/OSRM)与 dsh 家族不同源,进本目录的原因是 **npm 依赖形态被上游
30
+ peerDependencies 否决**:它要求 `dsh-settings`/`dsh-tools`
31
+ `>=0.1.2-rc.1`,而 gotry 锁定的运行时家族是 `0.1.2-alpha.3`
32
+ (semver 上 alpha < rc)——npm/npx 严格 peer 解析直接 ERESOLVE
33
+ (optionalDependencies 也不豁免),硬依赖会弄坏 `npx @danceiny/gotry`
34
+ 主安装路径。故以 vendor 副本随 tarball 分发(root `package.json`
35
+ `files[]` 含本目录),`bin/gotry-inner.js` 解析链 vendor 优先;
36
+ 升级 = 从 npm 拉 `dsh-map-tools@<new>` 覆盖本目录(保留来源与
37
+ license:MIT,上游 package.json 里无 devDependencies 需清理)。
38
+
39
+ ## 升级/复现流程(dsh 家族,下个版本照抄)
40
+
41
+ 1. `git clone --depth 1 --branch <tag> https://github.com/deepseek-ai/DeepSeek-Harness.git /tmp/dsh-<tag>`
42
+ 2. `cd /tmp/dsh-<tag> && pnpm install && pnpm build:official`
43
+ 3. `pnpm release:pack --family dsh --out dist/dsh`(产出 241 个 tarball)
44
+ 4. 解包 tarball(`package/` 前缀剥掉)平铺进 `vendor/<kebab-name>/`
45
+ 5. manifest 归一处:**删除各成员的 `devDependencies`**(上游 devDeps 引用
46
+ 未进发布家族的 experimental 私有包,npm 安装形态本就不装 devDeps;
47
+ 唯 `@deepseek-ai/dsh-subprocess-local` 的 `postinstall` 保
48
+ 留——恢复 node-pty spawn-helper 可执行位,终局面依赖);
49
+ `scripts` 里 dev 期 `bundle/watch/build` 一并丢弃。
50
+ 6. 更新 `package.json` 的 4 个直接依赖版本 + `pnpm install` 重新生成 lockfile
51
+ 7. 验证:`./gotry help` 报版本;`cd ts && npx tsc --noEmit && npx tsx scripts/smoke.ts`;
52
+ 全量 `bash scripts/run-all-tests.sh` 全绿后才可提交。
53
+
54
+ ## 纪律
55
+
56
+ - 本目录内容必须与上游 tag 的 tarball 逐字节一致(`devDependencies`/
57
+ dev 期 `scripts` 除外);不得手改 vendor 内代码——补丁面一律走 gotry 侧
58
+ (`cordis.gotry-patch.yml` / `bin/gotry-inner.js`)。
59
+ - `gotry-state/`、`node_modules/` 已 gitignore;`vendor/` 与三份 manifest
60
+ (package.json / pnpm-lock.yaml / pnpm-workspace.yaml / .npmrc)入 git。
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 HorusJiang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,200 @@
1
+ <p align="center">
2
+ <img src="assets/banner.svg" width="100%" alt="dsh-map-tools — Map & routing tools for DeepSeek Harness" />
3
+ </p>
4
+
5
+ # dsh-map-tools
6
+
7
+ <p align="center"><a href="README.md">中文</a> | English</p>
8
+
9
+ <p align="center">Map &amp; routing tools plugin for <a href="https://github.com/deepseek-ai/deepseek-harness">DeepSeek Harness</a>: driving/transit/walking/bicycling route planning, geocoding, reverse geocoding and POI search as <strong>native tools</strong> — the model calls them directly, no MCP server required.</p>
10
+
11
+ <p align="center">
12
+ <a href="https://www.npmjs.com/package/dsh-map-tools"><img src="https://img.shields.io/npm/v/dsh-map-tools?style=flat-square&label=npm&color=cb3837" alt="npm"></a>
13
+ <a href="https://www.npmjs.com/package/dsh-map-tools"><img src="https://img.shields.io/npm/dw/dsh-map-tools?style=flat-square&label=downloads&color=cb3837" alt="npm downloads"></a>
14
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="License"></a>
15
+ <a href="https://github.com/HorusJiang/dsh-map-tools/actions/workflows/ci.yml"><img src="https://github.com/HorusJiang/dsh-map-tools/actions/workflows/ci.yml/badge.svg?style=flat-square" alt="CI"></a>
16
+ <a href="https://github.com/HorusJiang/dsh-map-tools/releases"><img src="https://img.shields.io/github/v/release/HorusJiang/dsh-map-tools?style=flat-square&label=release" alt="Release"></a>
17
+ <a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/dsh--plugin-installable-2A6BE8?style=flat-square" alt="dsh-plugin"></a>
18
+ <a href="https://github.com/awesome-dsh-plugin/awesome-dsh-plugin"><img src="https://img.shields.io/badge/dshmarket-listed-22C55E?style=flat-square&logo=shopify&logoColor=white" alt="dshmarket"></a>
19
+ <img src="https://img.shields.io/badge/node-%3E%3D20-339933?style=flat-square&logo=node.js&logoColor=white" alt="Node.js >= 20">
20
+ </p>
21
+
22
+ ---
23
+
24
+ ## Features
25
+
26
+ - **7 native tools**: route planning (driving/transit/walking/bicycling), geocoding, reverse geocoding and POI search — called directly by the model via `map_*`.
27
+ - **Amap (高德) data source (recommended)**: configure a free Amap Web Service key for the best China coverage (transit, POI, reliable Chinese geocoding).
28
+ - **Zero-key fallback**: without a key, driving/walking/bicycling routes use free OSM/OSRM; Chinese address parsing degrades with clear guidance.
29
+ - **Settings card out of the box**: Settings → Plugins → dsh-map-tools, graphical config with an "How to get an Amap key?" link; save applies instantly (no restart).
30
+ - **China-network friendly**: clear Chinese guidance for unreachable free sources or invalid keys.
31
+
32
+ ## Install
33
+
34
+ Two ways, pick one:
35
+
36
+ ### Option 1: npm install (recommended, prebuilt, no build approval)
37
+
38
+ ```sh
39
+ dsh plugin --profile web add dsh-map-tools
40
+ ```
41
+
42
+ ### Option 2: from GitHub (source build, requires approval)
43
+
44
+ ```sh
45
+ dsh plugin --profile web add github:HorusJiang/dsh-map-tools
46
+ ```
47
+
48
+ > The published package ships **no** `prepare`/`postinstall` build scripts, so pnpm ≥10 installs it without prompting to allow build scripts or configuring `allowBuilds`.
49
+
50
+ > **Version requirement**: from 0.5.0 on, this plugin is compatible with **DeepSeek Harness 0.1.2-rc.1 and newer** (including a source-built 0.1.3-alpha.1). Earlier Harness release lines are no longer supported because `@deepseek-ai/dsh-settings` removed its legacy API.
51
+
52
+ Restart `dsh web` after install (or wait for HMR), then use the `map_*` tools in a session.
53
+
54
+ > **Development mode**: `dsh plugin add <local-path>` installs via `link:`, so source edits take effect immediately — ideal for iterating on the plugin.
55
+
56
+ ## Quick start
57
+
58
+ Configure an Amap key (~2 minutes):
59
+
60
+ 1. Open the [Amap console](https://console.amap.com/dev/key/app) → create an app → request a **"Web Service"** key (free for individuals).
61
+ 2. In DSH **Settings → Plugins → dsh-map-tools**, paste the key, set data source `amap`, save.
62
+ 3. Ask in a session:
63
+
64
+ ```
65
+ Plan a driving route from Beijing South Station to Capital Airport T3
66
+ Geocode "西湖区文三路478号" to coordinates
67
+ Any gas stations within 1km of 116.397428,39.90923?
68
+ ```
69
+
70
+ ## Tools
71
+
72
+ | Tool | What it does | Free OSM | Amap |
73
+ |---|---|---|---|
74
+ | `map_driving_route` | Driving route planning | ✅ | ✅ |
75
+ | `map_transit_route` | Transit planning | — | ✅ |
76
+ | `map_walking_route` | Walking route planning | ✅ | ✅ |
77
+ | `map_bicycling_route` | Bicycling route planning | ✅ | ✅ |
78
+ | `map_geocode` | Address → coordinates | CJK unreliable | ✅ |
79
+ | `map_reverse_geocode` | Coordinates → address | CJK unreliable | ✅ |
80
+ | `map_poi_search` | POI search | — | ✅ |
81
+
82
+ Origin/destination accept either **address text** or **`"lng,lat"` coordinates** — the plugin normalizes automatically.
83
+
84
+ ## Configuration
85
+
86
+ ### Settings card (recommended)
87
+
88
+ DSH **Settings → Plugins → dsh-map-tools** provides a graphical card: data-source selector, masked Amap key input, timeout, and apply link. Saving takes effect immediately.
89
+
90
+ Config lives in **`~/.dsh-map-tools/config.json`** (0600), decoupled from the DSH settings document and shared across profiles:
91
+
92
+ ```jsonc
93
+ // ~/.dsh-map-tools/config.json
94
+ {
95
+ "provider": "amap", // "amap" | "osm"
96
+ "amapKey": "your-amap-key",
97
+ "timeoutMs": 15000
98
+ }
99
+ ```
100
+
101
+ > Keys are only surfaced to the frontend as a boolean (`hasAmapKey`); the literal is **never echoed to the page or logs**.
102
+
103
+ ### cordis.yml defaults
104
+
105
+ Defaults can be provided in the profile's `cordis.yml` (**config-file values win over cordis.yml**):
106
+
107
+ ```yaml
108
+ - id: map-tools
109
+ name: dsh-map-tools
110
+ config:
111
+ provider: amap
112
+ ```
113
+
114
+ ## Architecture
115
+
116
+ ```
117
+ ┌─ Model ─────────────────────────────────────┐
118
+ │ map_driving_route / map_geocode / ... │ 7 native tools (ctx.tools)
119
+ └──────────────┬──────────────────────────────┘
120
+ │
121
+ ┌──────────────▼──────────────────────────────┐
122
+ │ src/tools/ tool definitions (validate+render) │
123
+ │ src/clients/ provider clients │
124
+ │ amap.ts Amap Web Service API (recommended) │
125
+ │ osrm.ts OSRM free routing (fallback) │
126
+ │ photon.ts Photon free geocoding (fallback) │
127
+ │ nominatim.ts Nominatim free geocoding (fallback)│
128
+ └──────────────┬──────────────────────────────┘
129
+ │
130
+ ┌──────────────▼──────────────────────────────┐
131
+ │ src/config-file.ts ~/.dsh-map-tools/config.json (0600) │
132
+ │ src/config-route.ts loopback route /dsh-map-tools/config │
133
+ │ src/settings-ns.ts settings namespace registration │
134
+ │ client/client.js settings card (hand-written, zero-dep)│
135
+ └──────────────────────────────────────────────┘
136
+ ```
137
+
138
+ - **Config priority**: config file (settings card) → `cordis.yml` defaults.
139
+ - **Instant apply**: tools rebuild on config change; no restart.
140
+ - **No MCP**: all capabilities are native tools; no external MCP server process.
141
+
142
+ ## Data sources
143
+
144
+ | Source | Use | Key | Notes |
145
+ |---|---|---|---|
146
+ | Amap (高德) | All tools (recommended) | Free | Best China coverage: transit, POI, Chinese geocoding |
147
+ | OSRM | driving/walking/bicycling routes | none | Free public server, rate-limited |
148
+ | Photon / Nominatim | geocoding | none | Free public; **CJK unreliable**, unreachable on some networks |
149
+
150
+ > Free-source limitations (unstable CJK geocoding) are deliberate: without a key you get clear guidance; with an Amap key the experience upgrades seamlessly.
151
+
152
+ ## FAQ
153
+
154
+ **Q: I configured an Amap key but routes still use OSM?**
155
+ A: Check the config file's `provider` is `amap` (not `osm`) and `amapKey` is non-empty.
156
+
157
+ **Q: Why do transit/POI require a key?**
158
+ A: Free OSM sources don't provide transit or POI data; those need the Amap key.
159
+
160
+ **Q: My Amap key is rejected?**
161
+ A: Make sure it's a **"Web Service"** key (not JS API / Web key), and the services are enabled in the Amap console.
162
+
163
+ **Q: Chinese geocoding reports "free source unavailable"?**
164
+ A: Free sources (Photon/Nominatim) handle Chinese poorly and may be unreachable on some CN networks. This is by design — configure an Amap key and it resolves.
165
+
166
+ ## Development
167
+
168
+ ```sh
169
+ pnpm install
170
+ pnpm run build # tsc → lib/
171
+ pnpm test # vitest unit tests (mocked network)
172
+ node scripts/smoke.mjs # smoke: 7 tools register
173
+ node scripts/integration.mjs # integration: real requests (free sources)
174
+ node scripts/amap-e2e.mjs # Amap e2e: set AMAP_API_KEY
175
+ node scripts/config-e2e.mjs # config file round-trip
176
+ ```
177
+
178
+ Conventions: see [CONTRIBUTING.md](CONTRIBUTING.md) and [AGENTS.md](AGENTS.md).
179
+
180
+ ## Publishing
181
+
182
+ ```sh
183
+ npm config set registry https://registry.npmjs.org/
184
+ npm login # npm account (a bypass-2FA publish token is recommended)
185
+ node scripts/publish.mjs # one-shot: build → pack check → publish → verify
186
+ ```
187
+
188
+ Versioning follows [SemVer](https://semver.org/); changes are tracked in [CHANGELOG.md](CHANGELOG.md).
189
+
190
+ ## Security
191
+
192
+ Key storage and vulnerability reporting: see [SECURITY.md](SECURITY.md).
193
+
194
+ ## Contributing
195
+
196
+ Issues and PRs welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for conventions.
197
+
198
+ ## License
199
+
200
+ [MIT](LICENSE) © HorusJiang