@danceiny/gotry 0.2.0-rc.31 → 0.2.0-rc.33
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 +2 -2
- package/README.zh-CN.md +2 -2
- package/bin/gotry-bootstrap.js +63 -53
- package/bin/gotry-hbcli-release.d.ts +3 -0
- package/bin/gotry-hbcli-release.js +4 -0
- package/client/client.js +66 -21
- package/dist/capabilities/doctor.js +7 -8
- package/dist/capabilities/effect.js +45 -12
- package/dist/capabilities/flyai-config.js +12 -10
- package/dist/capabilities/flyai.js +87 -56
- package/dist/capabilities/hbcli-config.js +156 -0
- package/dist/capabilities/hbcli.js +29 -2
- package/dist/capabilities/resilience.js +6 -6
- package/dist/capabilities/session/adapters/ctrip-flight.js +42 -7
- package/dist/capabilities/session/adapters/ctrip-hotel.js +52 -3
- package/dist/capabilities/session/bridge-ledger.js +168 -67
- package/dist/capabilities/session/ctrip-city.js +186 -0
- package/dist/capabilities/session/extension-bridge.js +148 -47
- package/dist/capabilities/session/gui-flight.js +949 -0
- package/dist/capabilities/session/gui-hotel.js +470 -0
- package/dist/capabilities/session-search.js +102 -14
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +129 -2
- package/dist/scripts/booking-copilot-dsh-readiness-proof.js +53 -5
- package/dist/scripts/booking-copilot-gateway-chat-adapter-tests.js +233 -0
- package/dist/scripts/booking-copilot-planner-live-probe.js +1 -0
- package/dist/scripts/booking-copilot-startup-proof-tests.js +1 -1
- package/dist/scripts/bootstrap-tests.js +23 -6
- package/dist/scripts/bridge-ledger-tests.js +20 -18
- package/dist/scripts/ctrip-city-tests.js +80 -0
- package/dist/scripts/effect-tests.js +602 -504
- package/dist/scripts/extension-tests.js +14 -8
- package/dist/scripts/fixtures/tenant-runaway-worker-fixture.mjs +73 -0
- package/dist/scripts/flight-malformed-tests.js +1 -1
- package/dist/scripts/flyai-setup-tests.js +19 -4
- package/dist/scripts/flyai-tests.js +768 -653
- package/dist/scripts/gotry-web-api-tests.js +5 -0
- package/dist/scripts/gotry-web-client-tests.js +20 -1
- package/dist/scripts/gui-flight-live.js +27 -0
- package/dist/scripts/hbcli-tests.js +1 -1
- package/dist/scripts/hbcli-upgrade-tests.js +107 -0
- package/dist/scripts/hbcli-web-setup-tests.js +114 -0
- package/dist/scripts/hotel-date-gate-tests.js +1 -1
- package/dist/scripts/hotel-result-tests.js +169 -0
- package/dist/scripts/issue-271-liveness-tests.js +1 -1
- package/dist/scripts/mutation-check.js +153 -0
- package/dist/scripts/nightly-evidence.js +4 -111
- package/dist/scripts/session-tests.js +211 -6
- package/dist/scripts/tenant-bridge-org-tests.js +1264 -0
- package/dist/scripts/tenant-capacity-probe.js +263 -0
- package/dist/scripts/tenant-compute-isolation-tests.js +707 -0
- package/dist/scripts/tenant-egress-credentials-tests.js +822 -0
- package/dist/scripts/tenant-executor-binding-tests.js +203 -0
- package/dist/scripts/tenant-prompt-partition-tests.js +265 -0
- package/dist/scripts/tenant-seam-adversarial-tests.js +957 -0
- package/dist/scripts/tenant-seam-quota-tests.js +383 -0
- package/dist/scripts/tenant-seam-test-kit.js +292 -0
- package/dist/scripts/tenant-state-copies-tests.js +607 -0
- package/dist/scripts/tenant-state-drill-tests.js +298 -0
- package/dist/scripts/tenant-state-drill-worker.js +164 -0
- package/dist/scripts/tenant-state-durable-conformance.js +101 -0
- package/dist/scripts/tenant-state-handoff-tests.js +854 -0
- package/dist/scripts/tenant-state-lease-conformance.js +142 -0
- package/dist/scripts/tenant-state-lease-worker.js +28 -0
- package/dist/scripts/tenant-state-router-tests.js +464 -0
- package/dist/scripts/tenant-state-test-kit.js +92 -0
- package/dist/scripts/tenant-state-tests.js +564 -0
- package/dist/src/backend/modules/booking-executor.js +61 -6
- package/dist/src/backend/modules/session-search.js +136 -39
- package/dist/src/booking-surface/dsh-llm-gateway-chat.js +434 -0
- package/dist/src/booking-surface/dsh-planner.js +150 -19
- package/dist/src/booking-surface/index.js +1 -0
- package/dist/src/booking-surface/managed-dsh-run-port.js +121 -7
- package/dist/src/booking-surface/server.js +193 -80
- package/dist/src/booking-surface/startup.js +153 -0
- package/dist/src/booking-surface/tenancy.js +188 -0
- package/dist/src/gotry-web-api.js +64 -2
- package/dist/src/index.js +70 -18
- package/dist/src/llm-price-table.js +148 -0
- package/dist/src/tenant-seam/index.js +8 -0
- package/dist/src/tenant-seam/llm-broker.js +508 -0
- package/dist/src/tenant-seam/process-governor.js +281 -0
- package/dist/src/tenant-seam/quotas.js +360 -0
- package/dist/src/tenant-seam/seam.js +92 -0
- package/dist/src/tenant-state/durable-store.js +158 -0
- package/dist/src/tenant-state/epoch-copies.js +422 -0
- package/dist/src/tenant-state/errors.js +108 -0
- package/dist/src/tenant-state/ids.js +101 -0
- package/dist/src/tenant-state/index.js +11 -0
- package/dist/src/tenant-state/lease-store.js +203 -0
- package/dist/src/tenant-state/tenant-lease.js +150 -0
- package/dist/src/tenant-state/tenant-router.js +650 -0
- package/extension/manifest.json +2 -2
- package/package.json +5 -1
- package/ts/capabilities/flyai-config.ts +12 -10
- package/ts/capabilities/flyai.ts +91 -50
- package/ts/capabilities/hbcli-config.ts +128 -0
- package/ts/capabilities/hbcli.ts +30 -3
- package/ts/capabilities/session/adapters/ctrip-flight.ts +43 -7
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +51 -4
- package/ts/capabilities/session/bridge-ledger.ts +192 -78
- package/ts/capabilities/session/ctrip-city.ts +158 -0
- package/ts/capabilities/session/extension-bridge.ts +185 -67
- package/ts/capabilities/session/gui-flight.ts +1022 -0
- package/ts/capabilities/session/gui-hotel.ts +505 -0
- package/ts/capabilities/session-search.ts +130 -18
- package/ts/src/booking-surface/dsh-llm-gateway-chat.js +434 -0
- package/ts/src/booking-surface/dsh-planner.ts +169 -19
- package/ts/src/booking-surface/index.ts +1 -0
- package/ts/src/booking-surface/managed-dsh-run-port.ts +150 -7
- package/ts/src/booking-surface/server.ts +215 -84
- package/ts/src/booking-surface/startup.ts +155 -0
- package/ts/src/booking-surface/tenancy.ts +242 -0
- package/ts/src/gotry-web-api.ts +51 -2
- package/ts/src/index.ts +67 -19
- package/ts/src/llm-price-table.ts +180 -0
- package/ts/src/tenant-seam/index.ts +9 -0
- package/ts/src/tenant-seam/llm-broker.ts +520 -0
- package/ts/src/tenant-seam/process-governor.ts +343 -0
- package/ts/src/tenant-seam/quotas.ts +433 -0
- package/ts/src/tenant-seam/seam.ts +142 -0
- package/ts/src/tenant-state/durable-store.ts +171 -0
- package/ts/src/tenant-state/epoch-copies.ts +491 -0
- package/ts/src/tenant-state/errors.ts +128 -0
- package/ts/src/tenant-state/ids.ts +108 -0
- package/ts/src/tenant-state/index.ts +12 -0
- package/ts/src/tenant-state/lease-store.ts +266 -0
- package/ts/src/tenant-state/tenant-lease.ts +175 -0
- package/ts/src/tenant-state/tenant-router.ts +743 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/** Public supplier city metadata, independent of inventory and account sessions. */
|
|
2
|
+
export const CTRIP_CITY_CATALOG_URL = 'https://m.ctrip.com/restapi/soa2/17909/SearchBoxRecommend'
|
|
3
|
+
export const CTRIP_CITY_SUGGEST_URL = 'https://flights.ctrip.com/international/search/api/poi/search'
|
|
4
|
+
export const CTRIP_HOTEL_CITY_URL = 'https://m.ctrip.com/restapi/soa2/34951/getHotelKeywords'
|
|
5
|
+
export interface CtripCity {
|
|
6
|
+
name: string
|
|
7
|
+
code: string
|
|
8
|
+
aliases: string[]
|
|
9
|
+
context?: string
|
|
10
|
+
airport?: boolean
|
|
11
|
+
requiresQualification?: boolean
|
|
12
|
+
}
|
|
13
|
+
export type CityResolution =
|
|
14
|
+
| { ok: true; city: CtripCity }
|
|
15
|
+
| { ok: false; reason: 'ambiguous' | 'not-found' | 'unavailable'; candidates: CtripCity[] }
|
|
16
|
+
const normalized = (s: string) => s.normalize('NFKC').trim().replace(/^中国(?=香港|澳门)/, '').toLowerCase()
|
|
17
|
+
const text = (v: unknown) => typeof v === 'string' ? v.trim() : ''
|
|
18
|
+
const code = (v: unknown) => /^[a-z]{3}$/i.test(text(v)) ? text(v).toUpperCase() : undefined
|
|
19
|
+
const object = (v: unknown): Record<string, unknown> => v && typeof v === 'object' && !Array.isArray(v) ? v as Record<string, unknown> : {}
|
|
20
|
+
|
|
21
|
+
export function parseCtripCityCatalog(value: unknown): CtripCity[] {
|
|
22
|
+
const root = object(value)
|
|
23
|
+
if (object(root.ResponseStatus).Ack !== 'Success' || object(root.responseHead).errorCode !== '0' || !Array.isArray(root.recommendGroupList)) throw new Error('unrecognized city catalog')
|
|
24
|
+
const records: CtripCity[] = []
|
|
25
|
+
let visited = 0
|
|
26
|
+
const walk = (v: unknown, depth = 0) => {
|
|
27
|
+
if (++visited > 20_000 || depth > 12) throw new Error('city catalog bounds')
|
|
28
|
+
if (Array.isArray(v)) { for (const item of v) walk(item, depth + 1); return }
|
|
29
|
+
const r = object(v), c = code(r.code), name = text(r.name)
|
|
30
|
+
if (c && name && Number(r.id) > 0) {
|
|
31
|
+
records.push({ name, code: c, aliases: [name, text(r.eName), text(r.nameEn), text(r.namePy), text(r.specName)].filter(Boolean), context: text(r.specName) || undefined, requiresQualification: r.sameSpecCity === 1 })
|
|
32
|
+
if (Array.isArray(r.airportlst)) for (const item of r.airportlst) {
|
|
33
|
+
const a = object(item), ac = code(a.code), an = text(a.name)
|
|
34
|
+
if (ac && an) records.push({ name: an, code: ac, aliases: [an], context: name, airport: true })
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
for (const [key, child] of Object.entries(r)) if (key !== 'airportlst' && child && typeof child === 'object') walk(child, depth + 1)
|
|
38
|
+
}
|
|
39
|
+
walk(root.recommendGroupList)
|
|
40
|
+
return records
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function parseCtripCitySuggestions(value: unknown): CtripCity[] {
|
|
44
|
+
const root = object(value)
|
|
45
|
+
if (!Array.isArray(root.Data)) throw new Error('unrecognized city suggestions')
|
|
46
|
+
const records: CtripCity[] = []
|
|
47
|
+
let visited = 0
|
|
48
|
+
const walk = (items: unknown[], depth = 0) => {
|
|
49
|
+
if (depth > 5) throw new Error('city suggestion bounds')
|
|
50
|
+
for (const value of items) {
|
|
51
|
+
if (++visited > 2_000) throw new Error('city suggestion bounds')
|
|
52
|
+
const r = object(value), c = code(r.Code), name = text(r.Name)
|
|
53
|
+
if (c && name && [3, 4, 5].includes(Number(r.Type))) records.push({
|
|
54
|
+
name, code: c, aliases: [name, text(r.EName), text(r.NameAlias), text(r.ShortSpell)].filter(Boolean),
|
|
55
|
+
context: [text(r.Country), text(r.Province)].filter(Boolean).join(' / ') || undefined, airport: Number(r.Type) === 3,
|
|
56
|
+
})
|
|
57
|
+
if (Array.isArray(r.Datas)) walk(r.Datas, depth + 1)
|
|
58
|
+
// Nearby cities must not be substituted for the requested destination.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
walk(root.Data)
|
|
62
|
+
return records
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function matchCtripCity(query: string, records: CtripCity[]): CityResolution {
|
|
66
|
+
const input = normalized(query)
|
|
67
|
+
const matches = records.filter(r => r.code.toLowerCase() === input || r.aliases.some(a => normalized(a) === input))
|
|
68
|
+
const unique = [...new Map(matches.map(r => [`${r.code}:${r.context ?? ''}`, r])).values()]
|
|
69
|
+
// A city and its airport may share one code. An explicitly typed code remains exact.
|
|
70
|
+
const codes = new Set(unique.map(r => r.code))
|
|
71
|
+
if (codes.size === 1 && /^[a-z]{3}$/i.test(query.trim())) return { ok: true, city: unique[0]! }
|
|
72
|
+
if (unique.length === 1 && (!unique[0]!.requiresQualification || normalized(unique[0]!.context ?? '') === input)) return { ok: true, city: unique[0]! }
|
|
73
|
+
return { ok: false, reason: unique.length ? 'ambiguous' : 'not-found', candidates: unique.slice(0, 8) }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type CityFetch = typeof fetch
|
|
77
|
+
async function readMetadata(url: string, init: RequestInit, fetcher: CityFetch): Promise<unknown> {
|
|
78
|
+
const response = await fetcher(url, { ...init, signal: AbortSignal.timeout(8_000), redirect: 'error' })
|
|
79
|
+
if (!response.ok || !response.body) throw new Error('city metadata unavailable')
|
|
80
|
+
const reader = response.body.getReader(), chunks: Uint8Array[] = []
|
|
81
|
+
let size = 0
|
|
82
|
+
try {
|
|
83
|
+
for (;;) {
|
|
84
|
+
const { done, value } = await reader.read()
|
|
85
|
+
if (done) break
|
|
86
|
+
size += value.byteLength
|
|
87
|
+
if (size > 512_000) throw new Error('city metadata bounds')
|
|
88
|
+
chunks.push(value)
|
|
89
|
+
}
|
|
90
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
91
|
+
} finally { await reader.cancel().catch(() => {}) }
|
|
92
|
+
}
|
|
93
|
+
let catalog: { at: number; promise: Promise<CtripCity[]> } | undefined
|
|
94
|
+
const catalogBody = {
|
|
95
|
+
head: { cver: '3', cid: '', extension: [{ name: 'source', value: 'ONLINE' }, { name: 'sotpGroup', value: 'CTrip' }, { name: 'sotpLocale', value: 'zh-CN' }] },
|
|
96
|
+
locale: 'zh-CN', departureCity: '', dataType: 0,
|
|
97
|
+
}
|
|
98
|
+
export async function resolveCtripFlightCity(query: string, fetcher: CityFetch = fetch): Promise<CityResolution> {
|
|
99
|
+
if (!query.trim() || query.length > 120) return { ok: false, reason: 'not-found', candidates: [] }
|
|
100
|
+
if (fetcher === fetch && process.env.GOTRY_SESSION_LIVE === '0') return { ok: false, reason: 'unavailable', candidates: [] }
|
|
101
|
+
try {
|
|
102
|
+
const load = () => readMetadata(CTRIP_CITY_CATALOG_URL, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(catalogBody) }, fetcher).then(parseCtripCityCatalog)
|
|
103
|
+
let records: CtripCity[]
|
|
104
|
+
if (fetcher !== fetch) records = await load()
|
|
105
|
+
else {
|
|
106
|
+
if (!catalog || Date.now() - catalog.at > 3_600_000) catalog = { at: Date.now(), promise: load() }
|
|
107
|
+
try { records = await catalog.promise } catch { catalog = undefined; records = [] }
|
|
108
|
+
}
|
|
109
|
+
const matched = matchCtripCity(query, records)
|
|
110
|
+
if (matched.ok || matched.reason === 'ambiguous') return matched
|
|
111
|
+
const url = new URL(CTRIP_CITY_SUGGEST_URL)
|
|
112
|
+
url.searchParams.set('key', query.trim().toUpperCase())
|
|
113
|
+
url.searchParams.set('filterAirport', 'false')
|
|
114
|
+
return matchCtripCity(query, parseCtripCitySuggestions(await readMetadata(url.href, {}, fetcher)))
|
|
115
|
+
} catch { return { ok: false, reason: 'unavailable', candidates: [] } }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function cityResolutionHint(query: string, result: Exclude<CityResolution, { ok: true }>): string {
|
|
119
|
+
if (result.reason === 'ambiguous') return `城市「${query}」有多个候选,请确认城市或机场码:${result.candidates.map(c => `${c.name} ${c.code}${c.context ? `(${c.context})` : ''}`).join(';')}`
|
|
120
|
+
return `unresolved 城市「${query}」:${result.reason === 'unavailable' ? '携程城市解析服务暂不可用' : '供应商城市候选未精确匹配'};请确认城市/机场名称,或提供 fromCityCode/toCityCode。城市解析失败不能证明该航线无库存或不受支持。`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface CtripHotelCity { id: string; name: string; aliases: string[]; countries: string[]; context: string }
|
|
124
|
+
export type HotelCityResolution = { ok: true; city: CtripHotelCity } | { ok: false; reason: 'ambiguous' | 'not-found' | 'unavailable'; candidates: CtripHotelCity[] }
|
|
125
|
+
export function parseCtripHotelCities(value: unknown): CtripHotelCity[] {
|
|
126
|
+
const root = object(value), data = object(root.data), keywords = object(data.mainKeywordList).keywords
|
|
127
|
+
if (object(root.ResponseStatus).Ack !== 'Success' || data.htlSpiderActionErrorCode || !Array.isArray(keywords) || keywords.length > 1_000) throw new Error('hotel city metadata unavailable')
|
|
128
|
+
const records: CtripHotelCity[] = []
|
|
129
|
+
for (const item of keywords) {
|
|
130
|
+
const r = object(item), k = object(object(r.keyword).keywordContentInfo), region = object(object(r.controlInfo).regionInfo)
|
|
131
|
+
const display = object(region.displayCityModel), basic = object(region.basicCityModel), id = String(basic.cityId ?? '')
|
|
132
|
+
if (k.tripType !== 'CT' || !/^[1-9]\d{0,9}$/.test(id)) continue
|
|
133
|
+
if (k.keywordCode !== undefined && String(k.keywordCode) !== id) throw new Error('hotel city ID conflict')
|
|
134
|
+
const titles = Array.isArray(k.displayTexts) ? k.displayTexts.map(object).filter(t => t.key === 'MAIN_TITLE').map(t => text(t.value)) : []
|
|
135
|
+
const name = text(display.cityName) || text(display.destinationName)
|
|
136
|
+
if (!name) continue
|
|
137
|
+
records.push({ id, name, aliases: [name, text(display.destinationName), text(display.destinationNameEN), ...titles].filter(Boolean),
|
|
138
|
+
countries: [text(display.countryName), text(display.countryNameEN)].filter(Boolean),
|
|
139
|
+
context: [text(display.countryName), text(display.provinceName)].filter(Boolean).join(' / ') })
|
|
140
|
+
}
|
|
141
|
+
return records
|
|
142
|
+
}
|
|
143
|
+
export async function resolveCtripHotelCity(query: string, country?: string, fetcher: CityFetch = fetch): Promise<HotelCityResolution> {
|
|
144
|
+
if (!query.trim() || query.length > 120 || (country?.length ?? 0) > 120) return { ok: false, reason: 'not-found', candidates: [] }
|
|
145
|
+
if (fetcher === fetch && process.env.GOTRY_SESSION_LIVE === '0') return { ok: false, reason: 'unavailable', candidates: [] }
|
|
146
|
+
try {
|
|
147
|
+
const value = await readMetadata(CTRIP_HOTEL_CITY_URL, { method: 'POST', headers: { 'content-type': 'application/json' },
|
|
148
|
+
body: JSON.stringify({ head: { platform: 'PC', bu: 'HBU', group: 'ctrip', locale: 'zh-CN' }, queryInfo: { keyword: query.trim(), actionType: 'destination' } }) }, fetcher)
|
|
149
|
+
const input = normalized(query)
|
|
150
|
+
const candidates = parseCtripHotelCities(value).filter(c => (!country || c.countries.some(a => normalized(a) === normalized(country))) && c.aliases.some(a => normalized(a) === input))
|
|
151
|
+
const unique = [...new Map(candidates.map(c => [c.id, c])).values()]
|
|
152
|
+
return unique.length === 1 ? { ok: true, city: unique[0]! } : { ok: false, reason: unique.length ? 'ambiguous' : 'not-found', candidates: unique.slice(0, 8) }
|
|
153
|
+
} catch { return { ok: false, reason: 'unavailable', candidates: [] } }
|
|
154
|
+
}
|
|
155
|
+
export function hotelResolutionHint(query: string, result: Exclude<HotelCityResolution, { ok: true }>): string {
|
|
156
|
+
if (result.reason === 'ambiguous') return `城市「${query}」有多个候选,请确认国家或城市:${result.candidates.map(c => `${c.name}(${c.context},cityId=${c.id})`).join(';')}`
|
|
157
|
+
return `城市「${query}」${result.reason === 'unavailable' ? '的携程酒店城市解析服务暂不可用' : '未精确匹配供应商城市候选'};可提供 country 消歧,或携程酒店 list 页 city= 数字作为 cityId。城市解析失败不能证明目的地不受支持。`
|
|
158
|
+
}
|