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

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 (137) hide show
  1. package/README.md +185 -148
  2. package/README.zh-CN.md +195 -154
  3. package/bin/gotry-bootstrap.js +246 -26
  4. package/bin/gotry-inner.js +68 -38
  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 +15 -5
  8. package/data/stations-12306-verify.json +524 -0
  9. package/dist/capabilities/agent-reach.js +4 -4
  10. package/dist/capabilities/channel-health.js +103 -0
  11. package/dist/capabilities/channel-registry.js +213 -0
  12. package/dist/capabilities/doctor.js +285 -0
  13. package/dist/capabilities/effect.js +178 -5
  14. package/dist/capabilities/flyai.js +13 -0
  15. package/dist/capabilities/session/adapters/ctrip-hotel.js +207 -0
  16. package/dist/capabilities/session/adapters/rail-12306.js +285 -0
  17. package/dist/capabilities/session/extension-bridge.js +14 -1
  18. package/dist/capabilities/session/extension-channel.js +4 -2
  19. package/dist/capabilities/session/extension-distribution.js +1 -1
  20. package/dist/capabilities/session-search.js +311 -0
  21. package/dist/capabilities/visa-policy.js +126 -0
  22. package/dist/scripts/agent-planning-turn-deadline-e2e.js +304 -0
  23. package/dist/scripts/agent-planning-turn-deadline-tests.js +281 -0
  24. package/dist/scripts/benchmark-environment-bridge-e2e.js +5 -10
  25. package/dist/scripts/benchmark-environment-bridge-tests.js +148 -142
  26. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +26 -26
  27. package/dist/scripts/{booking-copilot-availability-policy-v2-tests.js → booking-copilot-availability-policy-tests.js} +115 -115
  28. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +41 -1
  29. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +75 -120
  30. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +370 -61
  31. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +2 -2
  32. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +7 -3
  33. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +15 -9
  34. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +19 -11
  35. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +16 -52
  36. package/dist/scripts/booking-copilot-runtime-proof-tests.js +3953 -155
  37. package/dist/scripts/booking-copilot-server-proof-tests.js +58 -23
  38. package/dist/scripts/booking-copilot-startup-proof-tests.js +10 -119
  39. package/dist/scripts/booking-surface-contract-proof-tests.js +1154 -326
  40. package/dist/scripts/bootstrap-tests.js +115 -44
  41. package/dist/scripts/build-metrics-report.js +339 -0
  42. package/dist/scripts/channel-probe-tests.js +148 -0
  43. package/dist/scripts/channel-probe.js +252 -0
  44. package/dist/scripts/channel-registry-tests.js +262 -0
  45. package/dist/scripts/doctor-tests.js +121 -0
  46. package/dist/scripts/effect-tests.js +274 -1
  47. package/dist/scripts/extension-tests.js +129 -11
  48. package/dist/scripts/fact-gate-tests.js +91 -2
  49. package/dist/scripts/flyai-tests.js +17 -1
  50. package/dist/scripts/hbcli-e2e-tests.js +1 -3
  51. package/dist/scripts/metrics-report-tests.js +288 -0
  52. package/dist/scripts/persona-surface-guard-tests.js +19 -0
  53. package/dist/scripts/session-tests.js +401 -1
  54. package/dist/scripts/smoke.js +100 -83
  55. package/dist/scripts/turn-handoff-collect-tests.js +216 -0
  56. package/dist/scripts/turn-handoff-collect.js +189 -0
  57. package/dist/scripts/turn-policy-tests.js +56 -0
  58. package/dist/scripts/typed-contract-canary.js +261 -0
  59. package/dist/scripts/visa-policy-tests.js +75 -0
  60. package/dist/scripts/wish-channel-gate-tests.js +111 -0
  61. package/dist/src/artifact-gate.js +91 -3
  62. package/dist/src/benchmark-agent-conformance.js +7 -4
  63. package/dist/src/benchmark-environment-bridge.js +68 -7
  64. package/dist/src/bookable-facts.js +62 -3
  65. package/dist/src/booking-surface/{availability-policy-v2.js → availability-policy.js} +89 -89
  66. package/dist/src/booking-surface/canonical-schema.js +8 -22
  67. package/dist/src/booking-surface/contracts.js +70 -19
  68. package/dist/src/booking-surface/dsh-planner.js +310 -177
  69. package/dist/src/booking-surface/dsh-plugin.js +3 -3
  70. package/dist/src/booking-surface/index.js +1 -5
  71. package/dist/src/booking-surface/profile.js +1 -1
  72. package/dist/src/booking-surface/runtime.js +1682 -247
  73. package/dist/src/booking-surface/server.js +371 -185
  74. package/dist/src/booking-surface/startup.js +17 -27
  75. package/dist/src/booking-surface/validation.js +270 -760
  76. package/dist/src/index.js +735 -149
  77. package/dist/src/turn-deadline.js +292 -0
  78. package/dist/src/turn-policy.js +146 -0
  79. package/dist/src/wish-pool.js +5 -0
  80. package/extension/background.js +23 -4
  81. package/extension/content-main.js +47 -9
  82. package/extension/manifest.json +27 -9
  83. package/package.json +6 -26
  84. package/schemas/booking.surface.schema.json +2593 -0
  85. package/ts/capabilities/agent-reach.ts +4 -4
  86. package/ts/capabilities/flyai.ts +21 -3
  87. package/ts/capabilities/session/action-cache.ts +1 -1
  88. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  89. package/ts/capabilities/session/adapters/rail-12306.ts +293 -0
  90. package/ts/capabilities/session/extension-bridge.ts +32 -2
  91. package/ts/capabilities/session/extension-channel.ts +9 -8
  92. package/ts/capabilities/session/extension-distribution.ts +3 -1
  93. package/ts/capabilities/session/health-watch.ts +1 -1
  94. package/ts/capabilities/session/read-guard.ts +1 -1
  95. package/ts/capabilities/session/wizard.ts +1 -1
  96. package/ts/capabilities/session-search.ts +323 -1
  97. package/ts/package.json +1 -0
  98. package/ts/scripts/turn-handoff-collect.ts +178 -0
  99. package/ts/src/artifact-gate.ts +82 -2
  100. package/ts/src/benchmark-agent-conformance.ts +14 -6
  101. package/ts/src/benchmark-environment-bridge.ts +29 -10
  102. package/ts/src/bookable-facts.ts +109 -5
  103. package/ts/src/booking-saga.ts +1 -1
  104. package/ts/src/booking-surface/{availability-policy-v2.ts → availability-policy.ts} +124 -125
  105. package/ts/src/booking-surface/canonical-schema.js +8 -22
  106. package/ts/src/booking-surface/contracts.ts +278 -239
  107. package/ts/src/booking-surface/dsh-planner.ts +333 -194
  108. package/ts/src/booking-surface/dsh-plugin.js +3 -3
  109. package/ts/src/booking-surface/index.ts +1 -5
  110. package/ts/src/booking-surface/profile.ts +11 -11
  111. package/ts/src/booking-surface/runtime.ts +1351 -351
  112. package/ts/src/booking-surface/server.ts +282 -175
  113. package/ts/src/booking-surface/startup.ts +36 -48
  114. package/ts/src/booking-surface/validation.ts +194 -442
  115. package/ts/src/contracts.ts +1 -1
  116. package/ts/src/index.ts +489 -171
  117. package/ts/src/loop.ts +1 -1
  118. package/ts/src/state-ledger.ts +1 -1
  119. package/ts/src/turn-deadline.ts +361 -0
  120. package/ts/src/turn-policy.ts +154 -0
  121. package/ts/src/wish-pool.ts +17 -4
  122. package/dist/scripts/agent-planning-budget-e2e.js +0 -227
  123. package/dist/scripts/agent-planning-budget-tests.js +0 -173
  124. package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +0 -3935
  125. package/dist/scripts/booking-surface-v2-contract-proof-tests.js +0 -1174
  126. package/dist/src/booking-surface/contracts-v2.js +0 -89
  127. package/dist/src/booking-surface/runtime-v2.js +0 -1771
  128. package/dist/src/booking-surface/server-v2.js +0 -334
  129. package/dist/src/booking-surface/validation-v2.js +0 -319
  130. package/dist/src/tool-budget.js +0 -136
  131. package/schemas/booking.surface.v1.schema.json +0 -927
  132. package/schemas/booking.surface.v2.schema.json +0 -61
  133. package/ts/src/booking-surface/contracts-v2.ts +0 -118
  134. package/ts/src/booking-surface/runtime-v2.ts +0 -1466
  135. package/ts/src/booking-surface/server-v2.ts +0 -247
  136. package/ts/src/booking-surface/validation-v2.ts +0 -205
  137. package/ts/src/tool-budget.ts +0 -165
@@ -0,0 +1,285 @@
1
+ export const STATION_TELECODES = {
2
+ "三亚": 'SEQ',
3
+ "上海": 'SHH',
4
+ "上饶": 'SRG',
5
+ "东莞": 'RTQ',
6
+ "中卫": 'ZWJ',
7
+ "中山": 'ZSQ',
8
+ "丽江": 'LHM',
9
+ "乌鲁木齐": 'WAR',
10
+ "乐山": 'IVW',
11
+ "九江": 'JJG',
12
+ "伊宁": 'YMR',
13
+ "佛山": 'FSQ',
14
+ "保山": 'BAM',
15
+ "六盘水": 'UMW',
16
+ "兰州": 'LZJ',
17
+ "包头": 'BTC',
18
+ "北京": 'BJP',
19
+ "北海": 'BHZ',
20
+ "十堰": 'SNN',
21
+ "南京": 'NJH',
22
+ "南充": 'NCW',
23
+ "南宁": 'NNZ',
24
+ "南昌": 'NCG',
25
+ "南通": 'NUH',
26
+ "南阳": 'NFF',
27
+ "厦门": 'XMS',
28
+ "台州": 'TEU',
29
+ "合肥": 'HFH',
30
+ "吉林": 'JLL',
31
+ "吐鲁番": 'TFR',
32
+ "呼和浩特": 'HHC',
33
+ "咸阳": 'XYY',
34
+ "哈密": 'HMR',
35
+ "哈尔滨": 'HBB',
36
+ "喀什": 'KSR',
37
+ "嘉兴": 'JXH',
38
+ "嘉峪关": 'JGJ',
39
+ "大同": 'DTV',
40
+ "大庆": 'DZX',
41
+ "大理": 'DKM',
42
+ "大连": 'DLT',
43
+ "天水": 'TSJ',
44
+ "天津": 'TJP',
45
+ "太原": 'TYV',
46
+ "宁波": 'NGH',
47
+ "安顺": 'ASW',
48
+ "宜宾": 'YKE',
49
+ "宜昌": 'YCN',
50
+ "宝鸡": 'BJY',
51
+ "岳阳": 'YYQ',
52
+ "常州": 'CZH',
53
+ "常德": 'VGQ',
54
+ "广州": 'GZQ',
55
+ "库尔勒": 'KLR',
56
+ "延安": 'YWY',
57
+ "开封": 'KFF',
58
+ "张掖": 'ZYJ',
59
+ "徐州": 'XCH',
60
+ "德阳": 'DYW',
61
+ "惠州": 'HCQ',
62
+ "成都": 'CDW',
63
+ "扬州": 'YLH',
64
+ "承德": 'CDP',
65
+ "抚顺": 'FET',
66
+ "拉萨": 'LSO',
67
+ "揭阳": 'JYA',
68
+ "新乡": 'XXF',
69
+ "无锡": 'WGH',
70
+ "日喀则": 'RKO',
71
+ "昆明": 'KMM',
72
+ "普洱": 'PEM',
73
+ "景德镇": 'JCG',
74
+ "曲靖": 'QJM',
75
+ "杭州": 'HZH',
76
+ "林芝": 'LZO',
77
+ "柳州": 'LZZ',
78
+ "株洲": 'ZZQ',
79
+ "格尔木": 'GRO',
80
+ "桂林": 'GLZ',
81
+ "梧州": 'WZZ',
82
+ "武汉": 'WHN',
83
+ "汉中": 'HOY',
84
+ "汕头": 'OTQ',
85
+ "沈阳": 'SYT',
86
+ "泉州": 'QYS',
87
+ "泸州": 'LUE',
88
+ "洛阳": 'LYF',
89
+ "济南": 'JNK',
90
+ "深圳": 'SZQ',
91
+ "温州": 'RZH',
92
+ "湛江": 'ZJZ',
93
+ "漳州": 'ZUS',
94
+ "潮州": 'CKQ',
95
+ "牡丹江": 'MDB',
96
+ "珠海": 'ZHQ',
97
+ "盐城": 'AFH',
98
+ "石家庄": 'SJP',
99
+ "福州": 'FZS',
100
+ "秦皇岛": 'QTP',
101
+ "绍兴": 'SOH',
102
+ "绵阳": 'MYW',
103
+ "芜湖": 'WHH',
104
+ "苏州": 'SZH',
105
+ "茂名": 'MDQ',
106
+ "莆田": 'PTS',
107
+ "衡阳": 'HYQ',
108
+ "襄阳": 'XFN',
109
+ "西宁": 'XNO',
110
+ "西安": 'XAY',
111
+ "西昌": 'ECW',
112
+ "贵阳": 'GIW',
113
+ "赣州": 'GZG',
114
+ "达州": 'RXW',
115
+ "遵义": 'ZYE',
116
+ "郑州": 'ZZF',
117
+ "重庆": 'CUW',
118
+ "金华": 'JBH',
119
+ "银川": 'YIJ',
120
+ "镇江": 'ZJH',
121
+ "长春": 'CCT',
122
+ "长沙": 'CSQ',
123
+ "阜阳": 'FYH',
124
+ "青岛": 'QDK',
125
+ "鞍山": 'AST',
126
+ "韶关": 'SNQ',
127
+ "香格里拉": 'EUM',
128
+ "香港": 'XJA',
129
+ "齐齐哈尔": 'QHX',
130
+ "龙岩": 'LYS'
131
+ };
132
+ export function buildTrainEntryUrl(q) {
133
+ const unresolved = [];
134
+ const fromTc = (q.fromStationTelecode ?? '').trim().toUpperCase() || STATION_TELECODES[q.from.trim()];
135
+ const toTc = (q.toStationTelecode ?? '').trim().toUpperCase() || STATION_TELECODES[q.to.trim()];
136
+ if (!fromTc) unresolved.push(q.from);
137
+ if (!toTc) unresolved.push(q.to);
138
+ if (unresolved.length > 0 || !/^\d{4}-\d{2}-\d{2}$/.test(q.date)) {
139
+ return {
140
+ ok: false,
141
+ unresolved: unresolved.length > 0 ? unresolved : [
142
+ q.date
143
+ ]
144
+ };
145
+ }
146
+ const fs = encodeURIComponent(`${q.from.trim()},${fromTc}`);
147
+ const ts = encodeURIComponent(`${q.to.trim()},${toTc}`);
148
+ return {
149
+ ok: true,
150
+ url: `https://kyfw.12306.cn/otn/leftTicket/init?linktypeid=dc&fs=${fs}&ts=${ts}&date=${q.date}&flag=N,N,Y`
151
+ };
152
+ }
153
+ export const TRAIN_NETWORK_HINTS = [
154
+ /leftTicket\/query/i
155
+ ];
156
+ export const TRAIN_SITE_HOST = 'kyfw.12306.cn';
157
+ const SEAT_BUCKETS = [
158
+ {
159
+ index: 20,
160
+ field: 'gg_num',
161
+ label: '其他(通勤)'
162
+ },
163
+ {
164
+ index: 21,
165
+ field: 'gr_num',
166
+ label: '高级软卧'
167
+ },
168
+ {
169
+ index: 22,
170
+ field: 'qt_num',
171
+ label: '其他'
172
+ },
173
+ {
174
+ index: 23,
175
+ field: 'rw_num',
176
+ label: '软卧(一等卧)'
177
+ },
178
+ {
179
+ index: 24,
180
+ field: 'rz_num',
181
+ label: '软座'
182
+ },
183
+ {
184
+ index: 25,
185
+ field: 'tz_num',
186
+ label: '特等座'
187
+ },
188
+ {
189
+ index: 26,
190
+ field: 'wz_num',
191
+ label: '无座'
192
+ },
193
+ {
194
+ index: 27,
195
+ field: 'yb_num',
196
+ label: 'yp_b(上游席别)'
197
+ },
198
+ {
199
+ index: 28,
200
+ field: 'yw_num',
201
+ label: '硬卧(二等卧)'
202
+ },
203
+ {
204
+ index: 29,
205
+ field: 'yz_num',
206
+ label: '硬座'
207
+ },
208
+ {
209
+ index: 30,
210
+ field: 'ze_num',
211
+ label: '二等座'
212
+ },
213
+ {
214
+ index: 31,
215
+ field: 'zy_num',
216
+ label: '一等座'
217
+ },
218
+ {
219
+ index: 32,
220
+ field: 'swz_num',
221
+ label: '商务座'
222
+ },
223
+ {
224
+ index: 33,
225
+ field: 'srrb_num',
226
+ label: '动卧'
227
+ }
228
+ ];
229
+ const TRAIN_CODE_RE = /^[GDCZTKYLSF]\d{1,5}[A-Z]?$/i;
230
+ const HHMM_RE = /^\d{2}:\d{2}$/;
231
+ function lishiToMin(s) {
232
+ const m = /^(\d{1,3}):(\d{2})$/.exec((s ?? '').trim());
233
+ if (!m) return 0;
234
+ return Number(m[1]) * 60 + Number(m[2]);
235
+ }
236
+ function rowToOption(row, stationMap, entryUrl) {
237
+ const f = row.split('|');
238
+ if (f.length < 34) return null;
239
+ const trainCode = (f[3] ?? '').trim();
240
+ const depTime = (f[8] ?? '').trim();
241
+ const arrTime = (f[9] ?? '').trim();
242
+ if (!TRAIN_CODE_RE.test(trainCode) || !HHMM_RE.test(depTime) || !HHMM_RE.test(arrTime)) return null;
243
+ const fromStation = stationMap?.[f[6] ?? ''] ?? f[6] ?? '';
244
+ const toStation = stationMap?.[f[7] ?? ''] ?? f[7] ?? '';
245
+ const seats = {};
246
+ for (const b of SEAT_BUCKETS){
247
+ const v = (f[b.index] ?? '').trim();
248
+ if (v && v !== '--') seats[b.label] = v;
249
+ }
250
+ return {
251
+ trainCode,
252
+ fromStation,
253
+ toStation,
254
+ depTime,
255
+ arrTime,
256
+ durationMin: lishiToMin(f[10]),
257
+ canWebBuy: (f[11] ?? '').trim(),
258
+ seats,
259
+ jumpUrl: entryUrl
260
+ };
261
+ }
262
+ export function parseLeftTicketQuery(body, entryUrl, opts = {}) {
263
+ let raw;
264
+ try {
265
+ raw = JSON.parse(body);
266
+ } catch {
267
+ return [];
268
+ }
269
+ const rows = raw.data?.result;
270
+ if (!Array.isArray(rows)) return [];
271
+ const map = raw.data?.map && typeof raw.data.map === 'object' ? raw.data.map : undefined;
272
+ const out = [];
273
+ for (const r of rows){
274
+ if (typeof r !== 'string') continue;
275
+ const opt = rowToOption(r, map, entryUrl);
276
+ if (opt) {
277
+ out.push(opt);
278
+ if (out.length >= (opts.maxItems ?? 30)) break;
279
+ }
280
+ }
281
+ return out;
282
+ }
283
+
284
+
285
+ //# sourceURL=ts/capabilities/session/adapters/rail-12306.ts
@@ -1,4 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { existsSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
2
5
  import { createServer } from 'node:http';
3
6
  export const BRIDGE_PORTS = [
4
7
  8791,
@@ -17,6 +20,16 @@ export const EXTENSION_ORIGINS = [
17
20
  EXTENSION_ORIGIN,
18
21
  EXTENSION_ORIGIN_STORE
19
22
  ];
23
+ export function localExtensionInstalled(home = homedir()) {
24
+ return existsSync(join(home, '.gotry', 'extension', 'manifest.json'));
25
+ }
26
+ export function needsExtensionSummary(opts = {}) {
27
+ const local = opts.localInstalled ?? localExtensionInstalled(opts.home);
28
+ if (local) {
29
+ return 'GoTry Session Bridge 扩展未连接(未安装或已停用)。推荐 Chrome 应用商店一键装(自动更新) ' + `${EXTENSION_STORE_URL} ;或 npx gotry setup 落位后 chrome://extensions 开发者模式「加载已解压的扩展程序」指向 ~/.gotry/extension`;
30
+ }
31
+ return 'GoTry Session Bridge 扩展未连接。若你已从 Chrome 商店安装(自动更新):打开 Chrome 并确认扩展已启用即可自动连接,无需再装。' + `若尚未安装:应用商店一键装 ${EXTENSION_STORE_URL} (装完零弹窗,浏览器自己当安装器)`;
32
+ }
20
33
  export const EXTENSION_CONNECTED_WINDOW_MS = 45_000;
21
34
  export const JOBS_LONG_POLL_MS = 20_000;
22
35
  export const DEFAULT_EXTENSION_WAIT_MS = 6_000;
@@ -267,7 +280,7 @@ export async function createSessionBridge(opts = {}) {
267
280
  settle({
268
281
  ok: false,
269
282
  reason: 'extension-not-connected',
270
- summary: `GoTry Session Bridge 扩展未连接(未安装或已停用)。一次性安装(装完零弹窗):推荐 Chrome 应用商店一键装(自动更新) ${EXTENSION_STORE_URL} ;或 npx gotry setup 落位后 chrome://extensions 开发者模式「加载已解压的扩展程序」指向 ~/.gotry/extension`
283
+ summary: needsExtensionSummary()
271
284
  });
272
285
  }
273
286
  }, 250);
@@ -1,5 +1,7 @@
1
- import { getOrCreateSessionBridge, EXTENSION_STORE_URL } from './extension-bridge.js';
2
- export const NEEDS_EXTENSION_HINT = '需要一次性安装 GoTry Session Bridge 浏览器扩展(装完零弹窗):' + `推荐 Chrome 应用商店一键安装(自动更新) ${EXTENSION_STORE_URL} ;` + '免审核、版本更新更快的 GitHub 通道:npx gotry setup --extension-from=github(自动下载落位 ~/.gotry/extension;' + '手动下载 github.com/Danceiny/gotry/releases 标签 ext-* 的 gotry-session-bridge.tar.gz),' + '再在 chrome://extensions 开发者模式「加载已解压的扩展程序」指向解压目录';
1
+ import { getOrCreateSessionBridge, needsExtensionSummary } from './extension-bridge.js';
2
+ export function needsExtensionHint() {
3
+ return needsExtensionSummary();
4
+ }
3
5
  export function classifyBridgeFailure(kind) {
4
6
  return kind === 'extension-not-connected' ? 'needs-extension' : 'error';
5
7
  }
@@ -37,7 +37,7 @@ export function parseDistManifest(raw) {
37
37
  const tarballSha256 = obj.tarballSha256;
38
38
  const zip = obj.zip;
39
39
  const zipSha256 = obj.zipSha256;
40
- if (typeof version !== 'string' || !/^\d+\.\d+\.\d+$/.test(version)) throw new Error('dist-manifest.version 缺失或非 x.y.z');
40
+ if (typeof version !== 'string' || !/^\d+(\.\d+){1,3}$/.test(version)) throw new Error('dist-manifest.version 缺失或非 1-4 段点分整数');
41
41
  if (tarball !== DIST_ASSET_TARBALL) throw new Error(`dist-manifest.tarball 资产名漂移(期望 ${DIST_ASSET_TARBALL})`);
42
42
  if (zip !== DIST_ASSET_STORE_ZIP) throw new Error(`dist-manifest.zip 资产名漂移(期望 ${DIST_ASSET_STORE_ZIP})`);
43
43
  if (typeof tarballSha256 !== 'string' || !SHA256_RE.test(tarballSha256)) throw new Error('dist-manifest.tarballSha256 非 64-hex');
@@ -3,6 +3,8 @@ import { extensionCookieNames, extensionSearchJob, classifyBridgeFailure } from
3
3
  import { appendFileSync, mkdirSync } from 'node:fs';
4
4
  import { dirname } from 'node:path';
5
5
  import { buildEntryUrl, NETWORK_HINTS, parseBatchSearch, LOGIN_COOKIE_NAMES, SITE_DOMAIN } from './session/adapters/ctrip-flight.js';
6
+ import { buildHotelEntryUrl, parseCtripHotelList, HOTEL_SITE_HOST } from './session/adapters/ctrip-hotel.js';
7
+ import { buildTrainEntryUrl, parseLeftTicketQuery, TRAIN_SITE_HOST } from './session/adapters/rail-12306.js';
6
8
  import { EXTENSION_STORE_URL } from './session/extension-bridge.js';
7
9
  const MIN_INTERVAL_MS = 30_000;
8
10
  const lastCallAt = new Map();
@@ -48,6 +50,315 @@ export function appendExtensionAudit(auditPath, entry) {
48
50
  appendFileSync(auditPath, JSON.stringify(record) + '\n');
49
51
  } catch {}
50
52
  }
53
+ export function hotelCityUnresolvedHint(cities) {
54
+ return `城市 ${cities.join('/')} 不在携程酒店城市码表——先 web 搜「hotels.ctrip.com ${cities[0] ?? ''} 酒店」拿到 list 页 URL 里的 city= 数字,带 cityId 重试`;
55
+ }
56
+ export async function sessionHotelSearch(q) {
57
+ const started = Date.now();
58
+ const ts = new Date().toISOString();
59
+ const site = 'ctrip-hotel';
60
+ const err = (verdict, error)=>({
61
+ ok: false,
62
+ via: 'session-ctrip-hotel-error',
63
+ evidence: `[会话:${site}@error@${ts}] ${error}`,
64
+ latencyMs: Date.now() - started,
65
+ verdict,
66
+ error
67
+ });
68
+ if ((q.checkIn ? 1 : 0) !== (q.checkOut ? 1 : 0) || q.checkIn && !/^\d{4}-\d{2}-\d{2}$/.test(q.checkIn)) {
69
+ return err('error', 'checkIn/checkOut 须成对且为 YYYY-MM-DD(未定档期可不带日期)');
70
+ }
71
+ if (q.checkIn) {
72
+ const now = new Date();
73
+ const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
74
+ if (q.checkIn < today || (q.checkOut ?? '') < q.checkIn) {
75
+ return err('error', `入住 ${q.checkIn}/退房 ${q.checkOut ?? ''} 不是未来合法区间(今天 ${today})——向用户确认日期后再查`);
76
+ }
77
+ }
78
+ const last = lastCallAt.get(site) ?? 0;
79
+ if (Date.now() - last < MIN_INTERVAL_MS) {
80
+ return err('cooldown', `rate limit: last call ${Date.now() - last}ms ago, min ${MIN_INTERVAL_MS}ms`);
81
+ }
82
+ lastCallAt.set(site, Date.now());
83
+ const entry = buildHotelEntryUrl({
84
+ to: q.to,
85
+ cityId: q.cityId,
86
+ checkIn: q.checkIn,
87
+ checkOut: q.checkOut,
88
+ adults: q.adults
89
+ });
90
+ if (!entry.ok || !entry.url) {
91
+ return err('error', hotelCityUnresolvedHint(entry.unresolved ?? [
92
+ q.to
93
+ ]));
94
+ }
95
+ const mode = resolveTransportMode(q.profileDir);
96
+ if (mode === 'extension') {
97
+ const login = await extensionCookieNames({
98
+ site,
99
+ domain: SITE_DOMAIN.replace(/^\./, ''),
100
+ ticketNames: LOGIN_COOKIE_NAMES
101
+ });
102
+ if (!login.ok) {
103
+ const verdict = classifyBridgeFailure(login.kind);
104
+ if (verdict === 'needs-extension') {
105
+ return {
106
+ ok: false,
107
+ via: 'session-ctrip-hotel-error',
108
+ evidence: '[会话:ctrip-hotel-needs-extension@ts]',
109
+ latencyMs: Date.now() - started,
110
+ verdict,
111
+ error: login.summary,
112
+ installUrl: EXTENSION_STORE_URL,
113
+ installAction: 'add-to-chrome'
114
+ };
115
+ }
116
+ return err(verdict, login.summary);
117
+ }
118
+ if (login.tickets.length === 0 && !q.allowAnonymous) {
119
+ return err('needs-login', '未检出你本人登录态——调用 gotry_session_login 为用户打开携程登录入口(登录在携程官网完成;gotry 永不经手密码/验证码/cookie 值)');
120
+ }
121
+ const r = await extensionSearchJob({
122
+ site,
123
+ url: entry.url,
124
+ timeoutMs: q.timeoutMs
125
+ });
126
+ appendExtensionAudit(q.auditPath, {
127
+ kind: 'extension-session-job',
128
+ site,
129
+ url: entry.url,
130
+ jobId: 'search',
131
+ result: r.ok ? r.timedOut ? 'timeout' : `body ${r.body.length}B title="${r.title.slice(0, 60)}"` : `${r.kind}:${r.summary.slice(0, 120)}`
132
+ });
133
+ if (!r.ok) {
134
+ const verdict = classifyBridgeFailure(r.kind);
135
+ if (verdict === 'needs-extension') {
136
+ return {
137
+ ok: false,
138
+ via: 'session-ctrip-hotel-error',
139
+ evidence: '[会话:ctrip-hotel-needs-extension@ts]',
140
+ latencyMs: Date.now() - started,
141
+ verdict,
142
+ error: r.summary,
143
+ installUrl: EXTENSION_STORE_URL,
144
+ installAction: 'add-to-chrome'
145
+ };
146
+ }
147
+ return err(verdict, r.summary);
148
+ }
149
+ const title = r.title;
150
+ const head = r.body.slice(0, 5000);
151
+ if (CHALLENGE_RE.test(title + head)) {
152
+ return err('challenged', `风控/验证码命中(title=${title.slice(0, 60)});按红线不重试不绕过,交还用户`);
153
+ }
154
+ const hotels = parseCtripHotelList(r.body);
155
+ const verdict = hotels.length > 0 ? 'hit' : 'miss';
156
+ return {
157
+ ok: true,
158
+ via: 'session-ctrip-hotel',
159
+ evidence: `[会话:${site}@${ts}] ${hotels.length} hotels;transport=extension(被动嗅探,零系统弹窗;扩展零写行为=物理只读)${q.allowAnonymous ? ';anonymous=自检态' : ''}`,
160
+ latencyMs: Date.now() - started,
161
+ verdict,
162
+ hotels
163
+ };
164
+ }
165
+ const t = await openSession({
166
+ profileDir: q.profileDir,
167
+ headless: q.headless,
168
+ auditPath: q.auditPath,
169
+ mode: mode === 'persistent' ? 'persistent' : 'cdp',
170
+ newPage: true
171
+ });
172
+ if (!t.ok) {
173
+ return err(classifyTransportFailure(t.summary, q.profileDir === undefined), t.summary);
174
+ }
175
+ try {
176
+ const loggedIn = async ()=>{
177
+ const cookies = await t.browser.cookies().catch(()=>[]);
178
+ return cookies.some((c)=>c.domain.includes(SITE_DOMAIN.replace(/^\./, '')) && LOGIN_COOKIE_NAMES.includes(c.name));
179
+ };
180
+ if (!await loggedIn() && !q.allowAnonymous) {
181
+ return err('needs-login', '未检出你本人登录态——调用 gotry_session_login 为用户打开携程登录入口(登录在携程官网完成;gotry 永不经手密码/验证码/cookie 值)');
182
+ }
183
+ let settled = false;
184
+ let body = '';
185
+ const heard = new Promise((resolve)=>{
186
+ t.page.on('response', async (res)=>{
187
+ if (settled) return;
188
+ const u = res.url();
189
+ const hostHit = u.includes(HOTEL_SITE_HOST);
190
+ if (!hostHit) return;
191
+ try {
192
+ const text = await res.text();
193
+ 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))) {
194
+ body = text;
195
+ settled = true;
196
+ resolve();
197
+ }
198
+ } catch {}
199
+ });
200
+ setTimeout(()=>resolve(), q.timeoutMs ?? 30_000);
201
+ });
202
+ await t.page.goto(entry.url, {
203
+ waitUntil: 'domcontentloaded',
204
+ timeout: 30_000
205
+ });
206
+ await heard;
207
+ const title = await t.page.title().catch(()=>'');
208
+ const headHtml = (await t.page.content().catch(()=>'')).slice(0, 5000);
209
+ if (CHALLENGE_RE.test(title + headHtml)) {
210
+ return err('challenged', `风控/验证码命中(title=${title.slice(0, 60)});按红线不重试不绕过,交还用户`);
211
+ }
212
+ const hotels = parseCtripHotelList(body);
213
+ const verdict = hotels.length > 0 ? 'hit' : 'miss';
214
+ return {
215
+ ok: true,
216
+ via: 'session-ctrip-hotel',
217
+ evidence: `[会话:${site}@${ts}] ${hotels.length} hotels;guard blocked=${t.guard.blockedCount()}/${t.guard.requestCount()}${q.allowAnonymous ? ';anonymous=自检态' : ''}`,
218
+ latencyMs: Date.now() - started,
219
+ verdict,
220
+ hotels
221
+ };
222
+ } catch (e) {
223
+ return err('error', e instanceof Error ? e.message.slice(0, 200) : String(e));
224
+ } finally{
225
+ await t.close();
226
+ }
227
+ }
228
+ export function trainStationUnresolvedHint(cities) {
229
+ return `城市 ${cities.join('/')} 不在 12306 城市电报码表——web 搜「12306 ${cities[0] ?? ''} 余票」拿到 kyfw 查询页 URL 里的 fs=城市,XXX 三位码,带 fromStationTelecode/toStationTelecode 重试`;
230
+ }
231
+ export async function sessionTrainSearch(q) {
232
+ const started = Date.now();
233
+ const ts = new Date().toISOString();
234
+ const site = 'train-12306';
235
+ const err = (verdict, error)=>({
236
+ ok: false,
237
+ via: 'session-train-12306-error',
238
+ evidence: `[会话:${site}@error@${ts}] ${error}`,
239
+ latencyMs: Date.now() - started,
240
+ verdict,
241
+ error
242
+ });
243
+ const last = lastCallAt.get(site) ?? 0;
244
+ if (Date.now() - last < MIN_INTERVAL_MS) {
245
+ return err('cooldown', `rate limit: last call ${Date.now() - last}ms ago, min ${MIN_INTERVAL_MS}ms`);
246
+ }
247
+ lastCallAt.set(site, Date.now());
248
+ const entry = buildTrainEntryUrl({
249
+ from: q.from,
250
+ to: q.to,
251
+ date: q.date,
252
+ fromStationTelecode: q.fromStationTelecode,
253
+ toStationTelecode: q.toStationTelecode
254
+ });
255
+ if (!entry.ok || !entry.url) {
256
+ return err('error', trainStationUnresolvedHint(entry.unresolved ?? [
257
+ q.from,
258
+ q.to
259
+ ]));
260
+ }
261
+ const mode = resolveTransportMode(q.profileDir);
262
+ if (mode === 'extension') {
263
+ const r = await extensionSearchJob({
264
+ site,
265
+ url: entry.url,
266
+ timeoutMs: q.timeoutMs
267
+ });
268
+ appendExtensionAudit(q.auditPath, {
269
+ kind: 'extension-session-job',
270
+ site,
271
+ url: entry.url,
272
+ jobId: 'search',
273
+ result: r.ok ? r.timedOut ? 'timeout' : `body ${r.body.length}B title="${r.title.slice(0, 60)}"` : `${r.kind}:${r.summary.slice(0, 120)}`
274
+ });
275
+ if (!r.ok) {
276
+ const verdict = classifyBridgeFailure(r.kind);
277
+ if (verdict === 'needs-extension') {
278
+ return {
279
+ ok: false,
280
+ via: 'session-train-12306-error',
281
+ evidence: '[会话:train-12306-needs-extension@ts]',
282
+ latencyMs: Date.now() - started,
283
+ verdict,
284
+ error: r.summary,
285
+ installUrl: EXTENSION_STORE_URL,
286
+ installAction: 'add-to-chrome'
287
+ };
288
+ }
289
+ return err(verdict, r.summary);
290
+ }
291
+ const title = r.title;
292
+ const head = r.body.slice(0, 5000);
293
+ if (CHALLENGE_RE.test(title + head)) {
294
+ return err('challenged', `风控/验证码命中(title=${title.slice(0, 60)});按红线不重试不绕过,交还用户`);
295
+ }
296
+ const trains = parseLeftTicketQuery(r.body, entry.url);
297
+ const verdict = trains.length > 0 ? 'hit' : 'miss';
298
+ return {
299
+ ok: true,
300
+ via: 'session-train-12306',
301
+ evidence: `[会话:${site}@${ts}] ${trains.length} trains;transport=extension(公开查询面,被动嗅探,零系统弹窗;扩展零写行为=物理只读)`,
302
+ latencyMs: Date.now() - started,
303
+ verdict,
304
+ trains
305
+ };
306
+ }
307
+ const t = await openSession({
308
+ profileDir: q.profileDir,
309
+ headless: q.headless,
310
+ auditPath: q.auditPath,
311
+ mode: mode === 'persistent' ? 'persistent' : 'cdp',
312
+ newPage: true
313
+ });
314
+ if (!t.ok) {
315
+ return err(classifyTransportFailure(t.summary, q.profileDir === undefined), t.summary);
316
+ }
317
+ try {
318
+ let settled = false;
319
+ let body = '';
320
+ const heard = new Promise((resolve)=>{
321
+ t.page.on('response', async (res)=>{
322
+ if (settled) return;
323
+ const u = res.url();
324
+ if (!/leftTicket\/query/i.test(u)) return;
325
+ try {
326
+ const text = await res.text();
327
+ if (text) {
328
+ body = text;
329
+ settled = true;
330
+ resolve();
331
+ }
332
+ } catch {}
333
+ });
334
+ setTimeout(()=>resolve(), q.timeoutMs ?? 25_000);
335
+ });
336
+ await t.page.goto(entry.url, {
337
+ waitUntil: 'domcontentloaded',
338
+ timeout: 30_000
339
+ });
340
+ await heard;
341
+ const title = await t.page.title().catch(()=>'');
342
+ const headHtml = (await t.page.content().catch(()=>'')).slice(0, 5000);
343
+ if (CHALLENGE_RE.test(title + headHtml)) {
344
+ return err('challenged', `风控/验证码命中(title=${title.slice(0, 60)});按红线不重试不绕过,交还用户`);
345
+ }
346
+ const trains = parseLeftTicketQuery(body, entry.url);
347
+ const verdict = trains.length > 0 ? 'hit' : 'miss';
348
+ return {
349
+ ok: true,
350
+ via: 'session-train-12306',
351
+ evidence: `[会话:${site}@${ts}] ${trains.length} trains(公开查询面);guard blocked=${t.guard.blockedCount()}/${t.guard.requestCount()}`,
352
+ latencyMs: Date.now() - started,
353
+ verdict,
354
+ trains
355
+ };
356
+ } catch (e) {
357
+ return err('error', e instanceof Error ? e.message.slice(0, 200) : String(e));
358
+ } finally{
359
+ await t.close();
360
+ }
361
+ }
51
362
  export async function sessionFlightSearch(q) {
52
363
  const started = Date.now();
53
364
  const ts = new Date().toISOString();