@danceiny/gotry 0.0.1-rc.11 → 0.0.1-rc.13
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 +111 -146
- package/cordis.gotry-patch.yml +14 -7
- package/dist/capabilities/flyai.js +135 -30
- package/dist/capabilities/hbcli.js +14 -2
- package/dist/capabilities/session/benchmark.js +252 -0
- package/dist/capabilities/session/transport.js +25 -10
- package/dist/capabilities/session-consent.js +82 -0
- package/dist/capabilities/session-login.js +119 -0
- package/dist/capabilities/session-search.js +7 -4
- package/dist/capabilities/weather.js +72 -17
- package/dist/scripts/agent-reach-wrapper-tests.js +4 -0
- package/dist/scripts/async-collect.js +33 -5
- package/dist/scripts/hbcli-tests.js +25 -4
- package/dist/scripts/ledger-tests.js +134 -4
- package/dist/scripts/memory-value-report.js +379 -0
- package/dist/scripts/product-metrics.js +569 -0
- package/dist/scripts/session-benchmark.js +261 -0
- package/dist/scripts/session-login.js +28 -0
- package/dist/scripts/session-tests.js +262 -23
- package/dist/scripts/smoke.js +112 -14
- package/dist/scripts/weather-tests.js +8 -1
- package/dist/src/index.js +124 -5
- package/dist/src/loop.js +65 -14
- package/dist/src/state-ledger.js +30 -4
- package/package.json +4 -2
- package/ts/capabilities/flyai.ts +177 -22
- package/ts/capabilities/hbcli.ts +16 -4
- package/ts/capabilities/session/benchmark.ts +273 -0
- package/ts/capabilities/session/transport.ts +41 -9
- package/ts/capabilities/session-consent.ts +127 -0
- package/ts/capabilities/session-login.ts +146 -0
- package/ts/capabilities/session-search.ts +13 -5
- package/ts/capabilities/weather.ts +84 -19
- package/ts/scripts/async-collect.ts +48 -7
- package/ts/src/index.ts +107 -11
- package/ts/src/loop.ts +85 -12
- package/ts/src/state-ledger.ts +48 -4
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { evaluateDoubleSource, scoreSessionFixture } from '../capabilities/session/benchmark.js';
|
|
3
|
+
import { parseFlyaiItemList } from '../capabilities/flyai.js';
|
|
4
|
+
import { classifyTransportFailure } from '../capabilities/session-search.js';
|
|
5
|
+
let passed = 0;
|
|
6
|
+
function check(label, assertion) {
|
|
7
|
+
assertion();
|
|
8
|
+
passed += 1;
|
|
9
|
+
console.log(` ok - ${label}`);
|
|
10
|
+
}
|
|
11
|
+
const sessionFixture = {
|
|
12
|
+
query_id: 'sf-01',
|
|
13
|
+
route_segments: [
|
|
14
|
+
{
|
|
15
|
+
from: '上海虹桥',
|
|
16
|
+
to: '丽江三义',
|
|
17
|
+
departure_at: '2026-10-01T07:35:00+08:00',
|
|
18
|
+
arrival_at: '2026-10-01T10:55:00+08:00',
|
|
19
|
+
transport_number: 'HO5577'
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
journey_type: 'direct',
|
|
23
|
+
currency: 'CNY',
|
|
24
|
+
price: 1611,
|
|
25
|
+
source: 'ctrip-flight',
|
|
26
|
+
fetched_at: '2026-08-28T16:00:00Z',
|
|
27
|
+
verdict: 'hit',
|
|
28
|
+
latency_ms: 4200,
|
|
29
|
+
read_guard_blocked: 0
|
|
30
|
+
};
|
|
31
|
+
const officialFixture = {
|
|
32
|
+
...structuredClone(sessionFixture),
|
|
33
|
+
price: 230,
|
|
34
|
+
source: 'flyai',
|
|
35
|
+
fetched_at: '2026-08-28T15:59:00Z'
|
|
36
|
+
};
|
|
37
|
+
console.log('SESSION DOUBLE-SOURCE CONTRACT');
|
|
38
|
+
check('完整单段 fixture 为 13/13', ()=>{
|
|
39
|
+
const score = scoreSessionFixture(sessionFixture, structuredClone(sessionFixture));
|
|
40
|
+
assert.equal(score.pass, true);
|
|
41
|
+
assert.equal(score.accuracy, 1);
|
|
42
|
+
assert.equal(score.total, 13);
|
|
43
|
+
});
|
|
44
|
+
check('缺失 required 字段与错误价格都进入准确率分母', ()=>{
|
|
45
|
+
const actual = structuredClone(sessionFixture);
|
|
46
|
+
actual.currency = '';
|
|
47
|
+
actual.price = 999;
|
|
48
|
+
const score = scoreSessionFixture(sessionFixture, actual);
|
|
49
|
+
assert.equal(score.pass, false);
|
|
50
|
+
assert.deepEqual(score.missing, [
|
|
51
|
+
'currency'
|
|
52
|
+
]);
|
|
53
|
+
assert.deepEqual(score.incorrect, [
|
|
54
|
+
'price'
|
|
55
|
+
]);
|
|
56
|
+
assert.equal(score.accuracy, 11 / 13);
|
|
57
|
+
});
|
|
58
|
+
check('字段类型不一致不能靠字符串化蒙混过关', ()=>{
|
|
59
|
+
const actual = {
|
|
60
|
+
...structuredClone(sessionFixture),
|
|
61
|
+
price: '1611'
|
|
62
|
+
};
|
|
63
|
+
const score = scoreSessionFixture(sessionFixture, actual);
|
|
64
|
+
assert.equal(score.accuracy, 12 / 13);
|
|
65
|
+
assert.deepEqual(score.incorrect, [
|
|
66
|
+
'price'
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
check('非法 golden fixture 自身 fail-closed', ()=>{
|
|
70
|
+
const expected = {
|
|
71
|
+
...structuredClone(sessionFixture),
|
|
72
|
+
price: 0
|
|
73
|
+
};
|
|
74
|
+
const score = scoreSessionFixture(expected, structuredClone(expected));
|
|
75
|
+
assert.equal(score.pass, false);
|
|
76
|
+
assert.deepEqual(score.fixture_errors, [
|
|
77
|
+
'price'
|
|
78
|
+
]);
|
|
79
|
+
});
|
|
80
|
+
check('中转 fixture 逐段计分', ()=>{
|
|
81
|
+
const transfer = {
|
|
82
|
+
...structuredClone(sessionFixture),
|
|
83
|
+
query_id: 'sf-02',
|
|
84
|
+
journey_type: 'transfer',
|
|
85
|
+
route_segments: [
|
|
86
|
+
structuredClone(sessionFixture.route_segments[0]),
|
|
87
|
+
{
|
|
88
|
+
from: '丽江三义',
|
|
89
|
+
to: '西双版纳嘎洒',
|
|
90
|
+
departure_at: '2026-10-02T08:20:00+08:00',
|
|
91
|
+
arrival_at: '2026-10-02T09:35:00+08:00',
|
|
92
|
+
transport_number: '8L9608'
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
};
|
|
96
|
+
const score = scoreSessionFixture(transfer, structuredClone(transfer));
|
|
97
|
+
assert.equal(score.pass, true);
|
|
98
|
+
assert.equal(score.total, 18);
|
|
99
|
+
});
|
|
100
|
+
check('同路线班次可比,价格差只记录不判错', ()=>{
|
|
101
|
+
const result = evaluateDoubleSource({
|
|
102
|
+
official: officialFixture,
|
|
103
|
+
session: sessionFixture
|
|
104
|
+
});
|
|
105
|
+
assert.equal(result.state, 'comparable');
|
|
106
|
+
assert.deepEqual(result.mismatches, []);
|
|
107
|
+
assert.equal(result.price_delta, 1381);
|
|
108
|
+
assert.equal(result.quota_disposition, 'evidence_ready');
|
|
109
|
+
});
|
|
110
|
+
check('时刻不一致投影 divergent', ()=>{
|
|
111
|
+
const session = structuredClone(sessionFixture);
|
|
112
|
+
session.route_segments[0].arrival_at = '2026-10-01T11:15:00+08:00';
|
|
113
|
+
const result = evaluateDoubleSource({
|
|
114
|
+
official: officialFixture,
|
|
115
|
+
session
|
|
116
|
+
});
|
|
117
|
+
assert.equal(result.state, 'divergent');
|
|
118
|
+
assert.deepEqual(result.mismatches, [
|
|
119
|
+
'route_segments[0].arrival_at'
|
|
120
|
+
]);
|
|
121
|
+
});
|
|
122
|
+
check('等价时区时刻保持 comparable', ()=>{
|
|
123
|
+
const session = structuredClone(sessionFixture);
|
|
124
|
+
session.route_segments[0].departure_at = '2026-09-30T23:35:00Z';
|
|
125
|
+
session.route_segments[0].arrival_at = '2026-10-01T02:55:00Z';
|
|
126
|
+
const result = evaluateDoubleSource({
|
|
127
|
+
official: officialFixture,
|
|
128
|
+
session
|
|
129
|
+
});
|
|
130
|
+
assert.equal(result.state, 'comparable');
|
|
131
|
+
});
|
|
132
|
+
check('needs-attach 为 waiting-user no-spend', ()=>{
|
|
133
|
+
const session = {
|
|
134
|
+
...structuredClone(sessionFixture),
|
|
135
|
+
verdict: 'needs-attach'
|
|
136
|
+
};
|
|
137
|
+
const result = evaluateDoubleSource({
|
|
138
|
+
official: officialFixture,
|
|
139
|
+
session
|
|
140
|
+
});
|
|
141
|
+
assert.equal(result.state, 'waiting_attach');
|
|
142
|
+
assert.equal(result.retry_allowed, false);
|
|
143
|
+
assert.equal(result.quota_disposition, 'no_spend_waiting_user');
|
|
144
|
+
});
|
|
145
|
+
check('CDP 缺席与握手失败稳定投影 needs-attach', ()=>{
|
|
146
|
+
assert.equal(classifyTransportFailure('日常 Chrome 未开调试端口', true), 'needs-attach');
|
|
147
|
+
assert.equal(classifyTransportFailure('cdp attach 失败:socket closed', true), 'needs-attach');
|
|
148
|
+
assert.equal(classifyTransportFailure('chrome launch failed', false), 'error');
|
|
149
|
+
});
|
|
150
|
+
check('needs-login 为 waiting-user no-spend', ()=>{
|
|
151
|
+
const session = {
|
|
152
|
+
...structuredClone(sessionFixture),
|
|
153
|
+
verdict: 'needs-login'
|
|
154
|
+
};
|
|
155
|
+
const result = evaluateDoubleSource({
|
|
156
|
+
official: officialFixture,
|
|
157
|
+
session
|
|
158
|
+
});
|
|
159
|
+
assert.equal(result.state, 'waiting_login');
|
|
160
|
+
assert.equal(result.retry_allowed, false);
|
|
161
|
+
assert.equal(result.quota_disposition, 'no_spend_waiting_user');
|
|
162
|
+
});
|
|
163
|
+
check('缺失 session 证据不擅自投影用户 gate', ()=>{
|
|
164
|
+
const result = evaluateDoubleSource({
|
|
165
|
+
official: officialFixture
|
|
166
|
+
});
|
|
167
|
+
assert.equal(result.state, 'source_unavailable');
|
|
168
|
+
assert.equal(result.quota_disposition, 'no_spend_stop');
|
|
169
|
+
});
|
|
170
|
+
check('challenge 立即停止', ()=>{
|
|
171
|
+
const session = {
|
|
172
|
+
...structuredClone(sessionFixture),
|
|
173
|
+
verdict: 'challenged'
|
|
174
|
+
};
|
|
175
|
+
const result = evaluateDoubleSource({
|
|
176
|
+
official: officialFixture,
|
|
177
|
+
session
|
|
178
|
+
});
|
|
179
|
+
assert.equal(result.state, 'challenge_stop');
|
|
180
|
+
assert.equal(result.quota_disposition, 'no_spend_stop');
|
|
181
|
+
});
|
|
182
|
+
check('challenge 与 ReadGuard 优先于 waiting-user', ()=>{
|
|
183
|
+
const challengedOfficial = {
|
|
184
|
+
...structuredClone(officialFixture),
|
|
185
|
+
verdict: 'challenged'
|
|
186
|
+
};
|
|
187
|
+
const waitingSession = {
|
|
188
|
+
...structuredClone(sessionFixture),
|
|
189
|
+
verdict: 'needs-attach'
|
|
190
|
+
};
|
|
191
|
+
assert.equal(evaluateDoubleSource({
|
|
192
|
+
official: challengedOfficial,
|
|
193
|
+
session: waitingSession
|
|
194
|
+
}).state, 'challenge_stop');
|
|
195
|
+
waitingSession.read_guard_blocked = 1;
|
|
196
|
+
assert.equal(evaluateDoubleSource({
|
|
197
|
+
official: officialFixture,
|
|
198
|
+
session: waitingSession
|
|
199
|
+
}).state, 'guard_violation');
|
|
200
|
+
});
|
|
201
|
+
check('ReadGuard 非零 fail-closed', ()=>{
|
|
202
|
+
const session = {
|
|
203
|
+
...structuredClone(sessionFixture),
|
|
204
|
+
read_guard_blocked: 1
|
|
205
|
+
};
|
|
206
|
+
const result = evaluateDoubleSource({
|
|
207
|
+
official: officialFixture,
|
|
208
|
+
session
|
|
209
|
+
});
|
|
210
|
+
assert.equal(result.state, 'guard_violation');
|
|
211
|
+
assert.equal(result.quota_disposition, 'no_spend_stop');
|
|
212
|
+
});
|
|
213
|
+
check('缺 required 合同字段时 fail-closed', ()=>{
|
|
214
|
+
const session = {
|
|
215
|
+
...structuredClone(sessionFixture),
|
|
216
|
+
currency: ''
|
|
217
|
+
};
|
|
218
|
+
const result = evaluateDoubleSource({
|
|
219
|
+
official: officialFixture,
|
|
220
|
+
session
|
|
221
|
+
});
|
|
222
|
+
assert.equal(result.state, 'invalid_contract');
|
|
223
|
+
assert.deepEqual(result.missing, [
|
|
224
|
+
'session.currency'
|
|
225
|
+
]);
|
|
226
|
+
assert.equal(result.quota_disposition, 'no_spend_stop');
|
|
227
|
+
});
|
|
228
|
+
check('FlyAI stdout 前后噪声不污染完整业务 JSON', ()=>{
|
|
229
|
+
const payload = {
|
|
230
|
+
data: {
|
|
231
|
+
itemList: [
|
|
232
|
+
{
|
|
233
|
+
journeys: [],
|
|
234
|
+
systemMessage: '提示含花括号 {limited} 与引号 "quoted" 仍属于 JSON 字符串'
|
|
235
|
+
}
|
|
236
|
+
]
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
const noisy = `npm notice {not-json}\n${JSON.stringify(payload)}\npostflight log {done}`;
|
|
240
|
+
assert.deepEqual(parseFlyaiItemList(noisy), payload.data.itemList);
|
|
241
|
+
});
|
|
242
|
+
check('FlyAI stdout 可跳过前置诊断 JSON', ()=>{
|
|
243
|
+
const payload = {
|
|
244
|
+
data: {
|
|
245
|
+
itemList: [
|
|
246
|
+
{
|
|
247
|
+
ticketPrice: '230.00'
|
|
248
|
+
}
|
|
249
|
+
]
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
assert.deepEqual(parseFlyaiItemList(`{"level":"info","message":"warmup"}\n${JSON.stringify(payload)}\n`), payload.data.itemList);
|
|
253
|
+
});
|
|
254
|
+
check('FlyAI 无完整 itemList 对象时保持 fail-closed', ()=>{
|
|
255
|
+
assert.throws(()=>parseFlyaiItemList('{"data":{"itemList":['), /incomplete FlyAI JSON object/);
|
|
256
|
+
assert.throws(()=>parseFlyaiItemList('{"message":"SentinelBlockException"}'), /no complete FlyAI itemList JSON object/);
|
|
257
|
+
});
|
|
258
|
+
console.log(`SESSION DOUBLE-SOURCE CONTRACT: ${passed} pass`);
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/session-benchmark.ts
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { sessionLogin } from '../capabilities/session-login.js';
|
|
2
|
+
async function main() {
|
|
3
|
+
const site = process.argv.find((a)=>a === 'ctrip-flight' || a === 'meituan-hotel') ?? 'ctrip-flight';
|
|
4
|
+
const waitArg = process.argv.find((a)=>a.startsWith('--timeout') || a.startsWith('--wait'));
|
|
5
|
+
const waitMs = Number(waitArg?.split('=')[1] ?? 180) * 1000;
|
|
6
|
+
const r = await sessionLogin({
|
|
7
|
+
site,
|
|
8
|
+
waitMs
|
|
9
|
+
});
|
|
10
|
+
if (r.verdict === 'logged-in') {
|
|
11
|
+
console.log(`[login] OK——${r.site} 登录票据已检出 [${(r.tickets ?? []).join(', ')}](只读名字)。`);
|
|
12
|
+
console.log('[login] 说明:登录是在携程官网、用你自己的浏览器完成的;gotry 永不经手密码/验证码/cookie 值');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (r.verdict === 'pending') {
|
|
16
|
+
console.log(`[login] 登录入口已在你的 Chrome 打开(${site});在标签页里完成登录即可,无需再跑本脚本——工具面按 cookie 名自动感知`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
console.error(`[login] ${r.verdict}:${r.error ?? ''}\n${r.evidence}`);
|
|
20
|
+
process.exit(2);
|
|
21
|
+
}
|
|
22
|
+
main().catch((e)=>{
|
|
23
|
+
console.error(e);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/session-login.ts
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { mkdtempSync, readFileSync, existsSync, rmSync } from 'node:fs';
|
|
1
|
+
import { mkdtempSync, readFileSync, existsSync, rmSync, writeFileSync, chmodSync } from 'node:fs';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { classifyRequest, isSubmitText } from '../capabilities/session/read-guard.js';
|
|
5
5
|
import { buildEntryUrl, parseBatchSearch } from '../capabilities/session/adapters/ctrip-flight.js';
|
|
6
|
-
import { sessionFlightSearch, __resetRateLimiterForTest } from '../capabilities/session-search.js';
|
|
6
|
+
import { sessionFlightSearch, __resetRateLimiterForTest, classifyTransportFailure } from '../capabilities/session-search.js';
|
|
7
7
|
import { flyaiSearch } from '../capabilities/flyai.js';
|
|
8
|
+
import { createConsentGate } from '../capabilities/session-consent.js';
|
|
9
|
+
import { sessionLogin, pollTicketNames, LOGIN_TARGETS } from '../capabilities/session-login.js';
|
|
8
10
|
let pass = 0;
|
|
9
11
|
let fail = 0;
|
|
10
12
|
function assert(cond, label, detail) {
|
|
@@ -92,25 +94,52 @@ const r2 = await sessionFlightSearch({
|
|
|
92
94
|
assert(r1.verdict === 'error' && /unresolved/.test(r1.error ?? ''), '首次调用:词表外 → error(unresolved)');
|
|
93
95
|
assert(r2.verdict === 'cooldown', '30s 内二次调用 → cooldown,不发起导航');
|
|
94
96
|
__resetRateLimiterForTest();
|
|
95
|
-
console.log('F. flyaiSearch(live,飞猪官方,无 key)');
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const sentinelBlocked = fr.verdict === 'error' && /sentinel|block/i.test(fr.error ?? '');
|
|
103
|
-
if (sentinelBlocked) {
|
|
104
|
-
console.log(' WARN - 飞猪 Sentinel 限流(2026-08-28 实测,配额未文档化)——降级合同验证通过,跳过 hit 断言');
|
|
105
|
-
assert(fr.ok === false && /\[实时API:flyai@error@/.test(fr.evidence), '限流降级:结构化 error + 证据链错误形');
|
|
97
|
+
console.log('F. transport verdict + flyaiSearch(live,飞猪官方,无 key;GOTRY_SESSION_LIVE=0 跳过)');
|
|
98
|
+
assert(classifyTransportFailure('日常 Chrome 未开调试端口', true) === 'needs-attach', '调试端口未开稳定投影 needs-attach');
|
|
99
|
+
assert(classifyTransportFailure('cdp attach 失败:socket closed', true) === 'needs-attach', 'CDP 握手失败稳定投影 needs-attach');
|
|
100
|
+
assert(classifyTransportFailure('chrome launch failed', false) === 'error', '隔离 profile 启动失败不误投影用户门禁');
|
|
101
|
+
let fr = null;
|
|
102
|
+
if (process.env.GOTRY_SESSION_LIVE === '0') {
|
|
103
|
+
console.log(' SKIP - GOTRY_SESSION_LIVE=0(离线门禁不调用 FlyAI 外部实时端点)');
|
|
106
104
|
} else {
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
fr = await flyaiSearch({
|
|
106
|
+
kind: 'flight',
|
|
107
|
+
origin: '上海',
|
|
108
|
+
destination: '丽江',
|
|
109
|
+
depDate: '2026-10-01'
|
|
110
|
+
});
|
|
111
|
+
const sentinelBlocked = fr.verdict === 'error' && /sentinel|block/i.test(fr.error ?? '');
|
|
112
|
+
if (sentinelBlocked) {
|
|
113
|
+
console.log(' WARN - 飞猪 Sentinel 限流(2026-08-28 实测,配额未文档化)——降级合同验证通过,跳过 hit 断言');
|
|
114
|
+
assert(fr.ok === false && /\[实时API:flyai@error@/.test(fr.evidence), '限流降级:结构化 error + 证据链错误形');
|
|
115
|
+
} else {
|
|
116
|
+
assert(fr.ok === true && fr.verdict === 'hit', '上海→丽江 hit', fr);
|
|
117
|
+
assert((fr.options?.length ?? 0) >= 1 && (fr.options?.every((o)=>o.price > 0 && /^\d+[A-Z]\d+|^[A-Z]{2}\d+/.test(o.no)) ?? false), '结构化字段齐(price>0,航班号形)', fr.options?.[0]);
|
|
118
|
+
}
|
|
119
|
+
assert(/\[实时API:flyai/.test(fr.evidence), '证据链 [实时API:flyai@*]');
|
|
109
120
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
121
|
+
{
|
|
122
|
+
const fakeDir = mkdtempSync(join(tmpdir(), 'flyai-fake-'));
|
|
123
|
+
const fakeCli = join(fakeDir, 'flyai-cli-fake');
|
|
124
|
+
writeFileSync(fakeCli, '#!/bin/sh\necho \'{"data":null,"message":"出发日期非法","status":1,"systemMessage":null}\'\nexit 0\n', {
|
|
125
|
+
mode: 0o755
|
|
126
|
+
});
|
|
127
|
+
const fr2 = await flyaiSearch({
|
|
128
|
+
kind: 'flight',
|
|
129
|
+
origin: '深圳',
|
|
130
|
+
destination: '普吉',
|
|
131
|
+
depDate: '2026-07-18',
|
|
132
|
+
cliBin: fakeCli
|
|
133
|
+
});
|
|
134
|
+
assert(fr2.ok === false && fr2.verdict === 'error', 'data:null 语义失败 → error 终态(非 miss)', fr2);
|
|
135
|
+
assert(/出发日期非法/.test(fr2.error ?? '') && /flyai@error@/.test(fr2.evidence), '上游原话透传 + 证据链错误形', fr2);
|
|
136
|
+
rmSync(fakeDir, {
|
|
137
|
+
recursive: true,
|
|
138
|
+
force: true
|
|
139
|
+
});
|
|
140
|
+
}console.log('G. sessionFlightSearch(live,Chrome+携程;默认跳过,GOTRY_SESSION_LIVE=1 显式开启)');
|
|
141
|
+
if (process.env.GOTRY_SESSION_LIVE !== '1') {
|
|
142
|
+
console.log(' SKIP - 会话 live 探针默认关(测试不再自动开浏览器窗口;GOTRY_SESSION_LIVE=1 显式开启)');
|
|
114
143
|
} else {
|
|
115
144
|
__resetRateLimiterForTest();
|
|
116
145
|
const iso = mkdtempSync(join(tmpdir(), 'gotry-session-test-'));
|
|
@@ -137,16 +166,19 @@ if (process.env.GOTRY_SESSION_LIVE === '0') {
|
|
|
137
166
|
assert(sr.ok === false && sr.verdict === 'challenged', 'challenged = degraded 语义正确(不重试不绕过)');
|
|
138
167
|
} else if (sr.verdict === 'error' && /chrome launch failed/.test(sr.error ?? '')) {
|
|
139
168
|
console.log(' SKIP - 本机无 Chrome(channel:chrome 不可用)');
|
|
140
|
-
} else if (sr.verdict === 'error' && /chrome launch failed
|
|
169
|
+
} else if (sr.verdict === 'error' && /chrome launch failed/i.test(sr.error ?? '')) {
|
|
141
170
|
console.log(' SKIP - 本机无 Chrome(channel:chrome 不可用)');
|
|
142
171
|
} else if ((sr.verdict === 'miss' || sr.verdict === 'error') && /blocked=0/.test(sr.evidence)) {
|
|
143
172
|
console.log(` SKIP - 会话数据面 miss(外部方差,合同降级语义):${sr.evidence.slice(0, 90)}`);
|
|
173
|
+
} else if (sr.verdict === 'error' && /timeout/i.test(sr.error ?? '')) {
|
|
174
|
+
console.log(` SKIP - 携程页面加载超时(外部方差,合同降级语义):${String(sr.error).slice(0, 80)}`);
|
|
175
|
+
assert(sr.ok === false && /\[会话:ctrip-flight@error@/.test(sr.evidence), '超时降级仍带证据链错误形');
|
|
144
176
|
} else {
|
|
145
177
|
assert(sr.ok === true && sr.verdict === 'hit' && (sr.options?.length ?? 0) >= 1, '上海→丽江 会话嗅探 hit', sr);
|
|
146
178
|
assert(sr.options?.every((o)=>o.price > 0 && o.depDateTime.includes('2026-10-01')) ?? false, '班期=查询日,价格>0', sr.options?.[0]);
|
|
147
179
|
assert(/\[会话:ctrip-flight@/.test(sr.evidence) && /blocked=0/.test(sr.evidence), '证据链 [会话:*] + ReadGuard 零拦截(纯只读)');
|
|
148
180
|
assert(!existsSync(join(iso, 'audit', 'session-incidents.jsonl')), '审计文件不出现(零写请求)');
|
|
149
|
-
const flyaiMin = Math.min(...fr
|
|
181
|
+
const flyaiMin = Math.min(...fr?.options?.map((o)=>o.price).filter((p)=>p > 0) ?? [
|
|
150
182
|
0
|
|
151
183
|
]);
|
|
152
184
|
const sessionMin = Math.min(...sr.options?.map((o)=>o.price).filter((p)=>p > 0) ?? [
|
|
@@ -159,8 +191,215 @@ if (process.env.GOTRY_SESSION_LIVE === '0') {
|
|
|
159
191
|
force: true
|
|
160
192
|
});
|
|
161
193
|
}
|
|
162
|
-
console.log(
|
|
163
|
-
|
|
194
|
+
console.log('H. flyaiSearch hotel(飞猪官方 search-hotel)');
|
|
195
|
+
{
|
|
196
|
+
const hr = await flyaiSearch({
|
|
197
|
+
kind: 'hotel',
|
|
198
|
+
destName: '大理',
|
|
199
|
+
checkInDate: '2026-10-01',
|
|
200
|
+
checkOutDate: '2026-10-03'
|
|
201
|
+
});
|
|
202
|
+
const hSentinel = hr.verdict === 'error' && /sentinel|block/i.test(hr.error ?? '');
|
|
203
|
+
if (hSentinel) {
|
|
204
|
+
console.log(' WARN - 飞猪 Sentinel 限流——降级合同验证通过,跳过 hit 断言');
|
|
205
|
+
assert(hr.ok === false && /\[实时API:flyai@error@/.test(hr.evidence), '酒店限流降级:结构化 error + 证据链错误形');
|
|
206
|
+
} else if (hr.ok === false) {
|
|
207
|
+
console.log(` WARN - flyai hotel 端点降级(${String(hr.error).slice(0, 60)})——证据链合同通过,hit 断言跳过`);
|
|
208
|
+
assert(/\[实时API:flyai@error@/.test(hr.evidence), '端点降级仍带证据链错误形');
|
|
209
|
+
} else {
|
|
210
|
+
assert(hr.verdict === 'hit' && (hr.hotels?.length ?? 0) >= 1, '大理酒店 hit', hr.hotels?.[0]);
|
|
211
|
+
assert(hr.hotels?.every((h)=>!!h.name && !!h.jumpUrl) ?? false, '条目结构化(name + jumpUrl 透传)', hr.hotels?.[0]);
|
|
212
|
+
}
|
|
213
|
+
assert(/\[实时API:flyai/.test(hr.evidence), '证据链 [实时API:flyai@*]');
|
|
214
|
+
const fakeDir = mkdtempSync(join(tmpdir(), 'flyai-hotel-fake-'));
|
|
215
|
+
const fakeCliH = join(fakeDir, 'flyai-hotel-fake');
|
|
216
|
+
writeFileSync(fakeCliH, '#!/bin/sh\necho \'{"data":{"itemList":[{"name":"大理A 酒店","shId":"1","star":"高档型","rate":null,"price":"\\u00a57xx","address":"addr","interestsPoi":"近洱海","detailUrl":"https://router.feizhu.com/x"},{"star":"舒适型"}]}}\'\nexit 0\n', {
|
|
217
|
+
mode: 0o755
|
|
218
|
+
});
|
|
219
|
+
const h2 = await flyaiSearch({
|
|
220
|
+
kind: 'hotel',
|
|
221
|
+
destName: '大理',
|
|
222
|
+
cliBin: fakeCliH
|
|
223
|
+
});
|
|
224
|
+
assert(h2.verdict === 'hit' && h2.hotels?.length === 1, '酒店解析:缺名条目跳过,1 条有效', h2);
|
|
225
|
+
const h0 = h2.hotels?.[0];
|
|
226
|
+
assert(h0?.name === '大理A 酒店' && h0?.priceRaw === '¥7xx' && h0?.price === 0, '打码价保 priceRaw 原值(数字价 0)', h0);
|
|
227
|
+
assert(h0?.star === '高档型' && h0?.hotelId === '1' && h0?.jumpUrl === 'https://router.feizhu.com/x', 'star/jumpUrl(shId/detailUrl)透传', h0);
|
|
228
|
+
const hb1 = await flyaiSearch({
|
|
229
|
+
kind: 'hotel'
|
|
230
|
+
});
|
|
231
|
+
assert(hb1.verdict === 'error' && /destName|目的地/.test(hb1.error ?? ''), '缺目的地 → bad args error', hb1);
|
|
232
|
+
const hb2 = await flyaiSearch({
|
|
233
|
+
kind: 'hotel',
|
|
234
|
+
destName: '大理',
|
|
235
|
+
checkInDate: '2026-10-01'
|
|
236
|
+
});
|
|
237
|
+
assert(hb2.verdict === 'error' && /成对/.test(hb2.error ?? ''), '入住/退房不成对 → error(不静默)', hb2);
|
|
238
|
+
assert((await flyaiSearch({
|
|
239
|
+
kind: 'hotel',
|
|
240
|
+
destName: '大理',
|
|
241
|
+
checkInDate: '10月1号',
|
|
242
|
+
checkOutDate: '2026-10-03'
|
|
243
|
+
})).verdict === 'error', '非法日期格式 → error');
|
|
244
|
+
rmSync(fakeDir, {
|
|
245
|
+
recursive: true,
|
|
246
|
+
force: true
|
|
247
|
+
});
|
|
248
|
+
}console.log('I. createConsentGate(账号会话授权:每会话一次/拒绝吊销/allow/off/无通道)');
|
|
249
|
+
{
|
|
250
|
+
const next = async ()=>({
|
|
251
|
+
kind: 'allow'
|
|
252
|
+
});
|
|
253
|
+
const agentA = {
|
|
254
|
+
id: 'agent-A'
|
|
255
|
+
};
|
|
256
|
+
const agentB = {
|
|
257
|
+
id: 'agent-B'
|
|
258
|
+
};
|
|
259
|
+
const sess = (a = agentA)=>({
|
|
260
|
+
name: 'gotry_session_search',
|
|
261
|
+
agent: a,
|
|
262
|
+
callId: 'c1'
|
|
263
|
+
});
|
|
264
|
+
const other = ()=>({
|
|
265
|
+
name: 'gotry_anything_search',
|
|
266
|
+
agent: agentA
|
|
267
|
+
});
|
|
268
|
+
const mkStore = ()=>new WeakMap();
|
|
269
|
+
const mkGate = (access, seam)=>createConsentGate({
|
|
270
|
+
access: ()=>access,
|
|
271
|
+
approval: seam ? ()=>seam : undefined,
|
|
272
|
+
store: mkStore()
|
|
273
|
+
});
|
|
274
|
+
const gateBare = createConsentGate({
|
|
275
|
+
access: ()=>'ask'
|
|
276
|
+
});
|
|
277
|
+
const d1 = await gateBare({
|
|
278
|
+
name: 'gotry_session_search',
|
|
279
|
+
agent: undefined
|
|
280
|
+
}, next);
|
|
281
|
+
assert(d1.kind === 'ask', '无审批通道 → ask(交运行时 fail-closed;denies 责任在 registry)', d1);
|
|
282
|
+
assert((await gateBare({
|
|
283
|
+
name: 'gotry_anything_search',
|
|
284
|
+
agent: undefined
|
|
285
|
+
}, next)).kind === 'allow', '非账号工具不过闸,原样放行');
|
|
286
|
+
{
|
|
287
|
+
let requests = 0;
|
|
288
|
+
const seam = {
|
|
289
|
+
request: async ()=>{
|
|
290
|
+
requests += 1;
|
|
291
|
+
return 'allowed-once';
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
const gate = createConsentGate({
|
|
295
|
+
access: ()=>'ask',
|
|
296
|
+
approval: ()=>seam
|
|
297
|
+
});
|
|
298
|
+
const r1 = await gate(sess(), next);
|
|
299
|
+
assert(r1.kind === 'allow' && requests === 1, '首次调用:弹卡一次,批准后放行', {
|
|
300
|
+
r1,
|
|
301
|
+
requests
|
|
302
|
+
});
|
|
303
|
+
const r2 = await gate(sess(), next);
|
|
304
|
+
assert(r2.kind === 'allow' && requests === 1, '会话内第二次调用免弹卡直接放行(不重复骚扰)', {
|
|
305
|
+
r2: r1,
|
|
306
|
+
requests
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
{
|
|
310
|
+
let requests = 0;
|
|
311
|
+
const seam = {
|
|
312
|
+
request: async ()=>{
|
|
313
|
+
requests += 1;
|
|
314
|
+
return 'rejected';
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
const store = mkStore();
|
|
318
|
+
const gate = createConsentGate({
|
|
319
|
+
access: ()=>'ask',
|
|
320
|
+
approval: ()=>seam,
|
|
321
|
+
store
|
|
322
|
+
});
|
|
323
|
+
const d1 = await gate(sess(), next);
|
|
324
|
+
assert(d1.kind === 'deny' && /拒绝/.test(String(d1.kind === 'deny' ? d1.reason : '')), '拒绝 → deny + 明示「本会话内生效」', d1);
|
|
325
|
+
const d2 = await gate(sess(), next);
|
|
326
|
+
assert(d2.kind === 'deny' && requests === 1, '拒绝后再次调用 → 直接 deny,不再弹卡(拒绝=吊销)', {
|
|
327
|
+
d2
|
|
328
|
+
});
|
|
329
|
+
const dB = await gate({
|
|
330
|
+
name: 'gotry_session_search',
|
|
331
|
+
agent: agentB
|
|
332
|
+
}, next);
|
|
333
|
+
assert(dB.kind === 'deny' && requests === 2, '另一会话不受此前拒绝影响——会重新发起一次审批请求(seam 本例仍拒)', {
|
|
334
|
+
dB,
|
|
335
|
+
requests
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
{
|
|
339
|
+
const gate = mkGate('off');
|
|
340
|
+
const d = await gate(sess(), next);
|
|
341
|
+
assert(d.kind === 'deny' && /sessionAccess=off/.test(String(d.kind === 'deny' ? d.reason : '')), 'off → 不弹卡直接 deny', d);
|
|
342
|
+
assert((await gate(other(), next)).kind === 'allow', 'off 只关账号面工具,其余放行');
|
|
343
|
+
}
|
|
344
|
+
{
|
|
345
|
+
const gate = mkGate('allow');
|
|
346
|
+
assert((await gate(sess(), next)).kind === 'allow', 'sessionAccess=allow → 配置明示预授权,直接放行');
|
|
347
|
+
}
|
|
348
|
+
}console.log('J. sessionLogin(登录引导:无凭证语义/表格完备/pending 语义)');
|
|
349
|
+
{
|
|
350
|
+
for (const [site, t] of Object.entries(LOGIN_TARGETS)){
|
|
351
|
+
assert(!!t.domain && t.names.length > 0 && !!t.label && t.entryUrl.startsWith('https://'), `${site} 登录目标表完备`, t);
|
|
352
|
+
}
|
|
353
|
+
const unknownSite = await sessionLogin({
|
|
354
|
+
site: 'not-a-site',
|
|
355
|
+
waitMs: 0
|
|
356
|
+
});
|
|
357
|
+
assert(unknownSite.ok === false && unknownSite.verdict === 'error' && /未知站点/.test(unknownSite.error ?? ''), '未知站点 → 结构化 error(降级不抛)', unknownSite);
|
|
358
|
+
{
|
|
359
|
+
const fakeBrowser = {
|
|
360
|
+
cookies: async ()=>[
|
|
361
|
+
{
|
|
362
|
+
domain: '.ctrip.com',
|
|
363
|
+
name: 'cticket',
|
|
364
|
+
value: 'SECRET-TICKET'
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
domain: 'ctrip.com',
|
|
368
|
+
name: 'uid',
|
|
369
|
+
value: 'SECRET-UID'
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
domain: 'ctrip.com',
|
|
373
|
+
name: 'irrelevant',
|
|
374
|
+
value: 'x'
|
|
375
|
+
}
|
|
376
|
+
]
|
|
377
|
+
};
|
|
378
|
+
const names = await pollTicketNames(fakeBrowser, LOGIN_TARGETS['ctrip-flight']);
|
|
379
|
+
assert(JSON.stringify(names) === '["cticket","uid"]', '票据名级检查:只回名字;值(SOCRET)永不进入结果', names);
|
|
380
|
+
const joined = JSON.stringify(names);
|
|
381
|
+
assert(!joined.includes('SECRET'), '存在性检查零值过手(fixture 值不泄露)', joined);
|
|
382
|
+
}
|
|
383
|
+
if (process.env.GOTRY_SESSION_LIVE === '1') {
|
|
384
|
+
const lr = await sessionLogin({
|
|
385
|
+
waitMs: 2500,
|
|
386
|
+
pollMs: 500
|
|
387
|
+
});
|
|
388
|
+
if (lr.verdict === 'needs-attach') {
|
|
389
|
+
assert(lr.ok === false && /chrome:\/\/inspect/.test(lr.error ?? ''), 'live:Chrome 未开调试 → needs-attach + 一次性指引', lr);
|
|
390
|
+
} else if (lr.verdict === 'logged-in') {
|
|
391
|
+
assert(lr.ok === true && (lr.tickets?.length ?? 0) > 0, 'live:已登录自动检测 → logged-in(零弹窗,票据名级)', lr);
|
|
392
|
+
} else {
|
|
393
|
+
assert(lr.ok === true && lr.verdict === 'pending', 'live:attach 成功不交互 → pending(入口已开,等人登录)', lr);
|
|
394
|
+
const evidenceTagRe = /\[会话:ctrip-flight-login@/;
|
|
395
|
+
if (!evidenceTagRe.test(lr.evidence ?? '')) throw new Error(`FAIL: 登录证据链缺失,实际 ${lr.evidence}`);
|
|
396
|
+
pass += 1;
|
|
397
|
+
console.log(' ok - 登录证据链 [会话:ctrip-flight-login@*] 形态');
|
|
398
|
+
}
|
|
399
|
+
} else {
|
|
400
|
+
console.log(' SKIP - 登录 live 探针默认关(GOTRY_SESSION_LIVE=1 opt-in;工具面/桌面入口才真调)');
|
|
401
|
+
}
|
|
402
|
+
}console.log(`\nSESSION P1: ${pass} pass, ${fail} fail`);
|
|
164
403
|
|
|
165
404
|
|
|
166
405
|
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/session-tests.ts
|