@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.
- package/README.md +42 -16
- package/README.zh-CN.md +37 -15
- package/bin/gotry-bootstrap.js +309 -30
- package/bin/gotry-inner.js +74 -23
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +7 -8
- package/cordis.gotry-patch.yml +22 -8
- package/data/stations-12306-verify.json +524 -0
- package/dist/capabilities/agent-reach.js +4 -4
- package/dist/capabilities/anything.js +62 -29
- package/dist/capabilities/channel-health.js +103 -0
- package/dist/capabilities/channel-registry.js +213 -0
- package/dist/capabilities/doctor.js +307 -0
- package/dist/capabilities/effect.js +178 -5
- package/dist/capabilities/flyai.js +13 -0
- package/dist/capabilities/hbcli.js +28 -2
- package/dist/capabilities/session/adapters/ctrip-hotel.js +207 -0
- package/dist/capabilities/session/adapters/rail-12306.js +285 -0
- package/dist/capabilities/session/extension-bridge.js +14 -1
- package/dist/capabilities/session/extension-channel.js +4 -2
- package/dist/capabilities/session/extension-distribution.js +1 -1
- package/dist/capabilities/session-search.js +311 -0
- package/dist/capabilities/visa-policy.js +126 -0
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +305 -0
- package/dist/scripts/agent-planning-turn-deadline-tests.js +281 -0
- package/dist/scripts/anything-tests.js +70 -21
- package/dist/scripts/benchmark-environment-bridge-e2e.js +139 -37
- package/dist/scripts/benchmark-environment-bridge-tests.js +1099 -306
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +26 -26
- package/dist/scripts/{booking-copilot-availability-policy-v2-tests.js → booking-copilot-availability-policy-tests.js} +115 -115
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +41 -1
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +75 -120
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +370 -61
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +2 -2
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +7 -3
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +15 -9
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +19 -11
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +16 -52
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +3953 -155
- package/dist/scripts/booking-copilot-server-proof-tests.js +58 -23
- package/dist/scripts/booking-copilot-startup-proof-tests.js +10 -119
- package/dist/scripts/booking-surface-contract-proof-tests.js +1154 -326
- package/dist/scripts/bootstrap-tests.js +113 -2
- package/dist/scripts/build-metrics-report.js +339 -0
- package/dist/scripts/channel-probe-tests.js +148 -0
- package/dist/scripts/channel-probe.js +252 -0
- package/dist/scripts/channel-registry-tests.js +262 -0
- package/dist/scripts/doctor-tests.js +175 -0
- package/dist/scripts/effect-tests.js +274 -1
- package/dist/scripts/extension-tests.js +129 -11
- package/dist/scripts/fact-gate-tests.js +91 -2
- package/dist/scripts/flyai-tests.js +17 -1
- package/dist/scripts/hbcli-e2e-tests.js +1 -3
- package/dist/scripts/hbcli-tests.js +38 -1
- package/dist/scripts/metrics-report-tests.js +288 -0
- package/dist/scripts/persona-surface-guard-tests.js +19 -0
- package/dist/scripts/session-tests.js +401 -1
- package/dist/scripts/smoke.js +100 -83
- package/dist/scripts/turn-handoff-collect-tests.js +216 -0
- package/dist/scripts/turn-handoff-collect.js +189 -0
- package/dist/scripts/turn-policy-tests.js +56 -0
- package/dist/scripts/typed-contract-canary.js +261 -0
- package/dist/scripts/visa-policy-tests.js +75 -0
- package/dist/scripts/wish-channel-gate-tests.js +111 -0
- package/dist/src/artifact-gate.js +91 -3
- package/dist/src/benchmark-agent-conformance.js +39 -16
- package/dist/src/benchmark-environment-bridge.js +316 -52
- package/dist/src/bookable-facts.js +62 -3
- package/dist/src/booking-surface/{availability-policy-v2.js → availability-policy.js} +89 -89
- package/dist/src/booking-surface/canonical-schema.js +8 -22
- package/dist/src/booking-surface/contracts.js +70 -19
- package/dist/src/booking-surface/dsh-planner.js +310 -177
- package/dist/src/booking-surface/dsh-plugin.js +3 -3
- package/dist/src/booking-surface/index.js +1 -5
- package/dist/src/booking-surface/profile.js +1 -1
- package/dist/src/booking-surface/runtime.js +1682 -247
- package/dist/src/booking-surface/server.js +371 -185
- package/dist/src/booking-surface/startup.js +17 -27
- package/dist/src/booking-surface/validation.js +270 -760
- package/dist/src/index.js +739 -150
- package/dist/src/turn-deadline.js +292 -0
- package/dist/src/turn-policy.js +146 -0
- package/dist/src/wish-pool.js +5 -0
- package/extension/background.js +23 -4
- package/extension/content-main.js +47 -9
- package/extension/manifest.json +27 -9
- package/package.json +7 -26
- package/schemas/booking.surface.schema.json +2593 -0
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/anything.ts +89 -38
- package/ts/capabilities/flyai.ts +21 -3
- package/ts/capabilities/hbcli.ts +53 -4
- package/ts/capabilities/session/action-cache.ts +1 -1
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +293 -0
- package/ts/capabilities/session/extension-bridge.ts +32 -2
- package/ts/capabilities/session/extension-channel.ts +9 -8
- package/ts/capabilities/session/extension-distribution.ts +3 -1
- package/ts/capabilities/session/health-watch.ts +1 -1
- package/ts/capabilities/session/read-guard.ts +1 -1
- package/ts/capabilities/session/wizard.ts +1 -1
- package/ts/capabilities/session-search.ts +323 -1
- package/ts/dsh-runtime/vendor/README.md +60 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +200 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +202 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +216 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +371 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +56 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +126 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +109 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +27 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +170 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +53 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +20 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +55 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +91 -0
- package/ts/package.json +9 -0
- package/ts/scripts/turn-handoff-collect.ts +178 -0
- package/ts/src/artifact-gate.ts +82 -2
- package/ts/src/benchmark-agent-conformance.ts +54 -15
- package/ts/src/benchmark-environment-bridge.ts +234 -46
- package/ts/src/bookable-facts.ts +109 -5
- package/ts/src/booking-saga.ts +1 -1
- package/ts/src/booking-surface/{availability-policy-v2.ts → availability-policy.ts} +124 -125
- package/ts/src/booking-surface/canonical-schema.js +8 -22
- package/ts/src/booking-surface/contracts.ts +278 -239
- package/ts/src/booking-surface/dsh-planner.ts +333 -194
- package/ts/src/booking-surface/dsh-plugin.js +3 -3
- package/ts/src/booking-surface/index.ts +1 -5
- package/ts/src/booking-surface/profile.ts +11 -11
- package/ts/src/booking-surface/runtime.ts +1351 -351
- package/ts/src/booking-surface/server.ts +282 -175
- package/ts/src/booking-surface/startup.ts +36 -48
- package/ts/src/booking-surface/validation.ts +194 -442
- package/ts/src/contracts.ts +1 -1
- package/ts/src/index.ts +492 -172
- package/ts/src/loop.ts +1 -1
- package/ts/src/state-ledger.ts +1 -1
- package/ts/src/turn-deadline.ts +365 -0
- package/ts/src/turn-policy.ts +154 -0
- package/ts/src/wish-pool.ts +17 -4
- package/dist/scripts/agent-planning-budget-e2e.js +0 -227
- package/dist/scripts/agent-planning-budget-tests.js +0 -173
- package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +0 -3935
- package/dist/scripts/booking-surface-v2-contract-proof-tests.js +0 -1174
- package/dist/src/booking-surface/contracts-v2.js +0 -89
- package/dist/src/booking-surface/runtime-v2.js +0 -1771
- package/dist/src/booking-surface/server-v2.js +0 -334
- package/dist/src/booking-surface/validation-v2.js +0 -319
- package/dist/src/tool-budget.js +0 -136
- package/schemas/booking.surface.v1.schema.json +0 -927
- package/schemas/booking.surface.v2.schema.json +0 -61
- package/ts/src/booking-surface/contracts-v2.ts +0 -118
- package/ts/src/booking-surface/runtime-v2.ts +0 -1466
- package/ts/src/booking-surface/server-v2.ts +0 -247
- package/ts/src/booking-surface/validation-v2.ts +0 -205
- 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 & 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
|