@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.
- package/README.md +185 -148
- package/README.zh-CN.md +195 -154
- package/bin/gotry-bootstrap.js +246 -26
- package/bin/gotry-inner.js +68 -38
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +7 -8
- package/cordis.gotry-patch.yml +15 -5
- package/data/stations-12306-verify.json +524 -0
- package/dist/capabilities/agent-reach.js +4 -4
- package/dist/capabilities/channel-health.js +103 -0
- package/dist/capabilities/channel-registry.js +213 -0
- package/dist/capabilities/doctor.js +285 -0
- package/dist/capabilities/effect.js +178 -5
- package/dist/capabilities/flyai.js +13 -0
- 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 +304 -0
- package/dist/scripts/agent-planning-turn-deadline-tests.js +281 -0
- package/dist/scripts/benchmark-environment-bridge-e2e.js +5 -10
- package/dist/scripts/benchmark-environment-bridge-tests.js +148 -142
- 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 +115 -44
- 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 +121 -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/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 +7 -4
- package/dist/src/benchmark-environment-bridge.js +68 -7
- 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 +735 -149
- 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 +6 -26
- package/schemas/booking.surface.schema.json +2593 -0
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/flyai.ts +21 -3
- 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/package.json +1 -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 +14 -6
- package/ts/src/benchmark-environment-bridge.ts +29 -10
- 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 +489 -171
- package/ts/src/loop.ts +1 -1
- package/ts/src/state-ledger.ts +1 -1
- package/ts/src/turn-deadline.ts +361 -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
|
@@ -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:
|
|
283
|
+
summary: needsExtensionSummary()
|
|
271
284
|
});
|
|
272
285
|
}
|
|
273
286
|
}, 250);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { getOrCreateSessionBridge,
|
|
2
|
-
export
|
|
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
|
|
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();
|