@danceiny/gotry 0.0.1-rc.15 → 0.0.1-rc.16
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 +10 -6
- package/README.zh-CN.md +10 -6
- package/bin/gotry-bootstrap.js +294 -5
- package/cordis.gotry-patch.yml +10 -0
- package/data/airline-airports.json +39 -0
- package/dist/capabilities/agent-reach-deep.js +1 -1
- package/dist/capabilities/agent-reach.js +1 -1
- package/dist/capabilities/anything.js +1 -1
- package/dist/capabilities/artifacts.js +1 -1
- package/dist/capabilities/effect.js +293 -0
- package/dist/capabilities/fact-log.js +41 -0
- package/dist/capabilities/flyai.js +1 -1
- package/dist/capabilities/hbcli.js +43 -1
- package/dist/capabilities/incident-log.js +1 -1
- package/dist/capabilities/opensky.js +1 -1
- package/dist/capabilities/resilience.js +90 -0
- package/dist/capabilities/session/action-cache.js +1 -1
- package/dist/capabilities/session/adapters/ctrip-flight.js +1 -1
- package/dist/capabilities/session/adapters/meituan-local.js +1 -1
- package/dist/capabilities/session/benchmark.js +2 -1
- package/dist/capabilities/session/extension-bridge.js +345 -0
- package/dist/capabilities/session/extension-channel.js +93 -0
- package/dist/capabilities/session/extract.js +1 -1
- package/dist/capabilities/session/health-watch.js +216 -0
- package/dist/capabilities/session/read-guard.js +1 -1
- package/dist/capabilities/session/transport.js +1 -1
- package/dist/capabilities/session/wizard.js +377 -0
- package/dist/capabilities/session-consent.js +1 -1
- package/dist/capabilities/session-login.js +77 -1
- package/dist/capabilities/session-search.js +69 -2
- package/dist/capabilities/weather.js +1 -1
- package/dist/scripts/action-cache-tests.js +1 -1
- package/dist/scripts/agent-reach-deep-tests.js +1 -1
- package/dist/scripts/agent-reach-tests.js +1 -1
- package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
- package/dist/scripts/anything-tests.js +1 -1
- package/dist/scripts/async-collect.js +1 -1
- package/dist/scripts/booking-saga-tests.js +1 -1
- package/dist/scripts/bootstrap-tests.js +39 -3
- package/dist/scripts/build-changelog.js +279 -0
- package/dist/scripts/changelog-tests.js +165 -0
- package/dist/scripts/companion-tests.js +1 -1
- package/dist/scripts/diff-test.js +1 -1
- package/dist/scripts/effect-tests.js +474 -0
- package/dist/scripts/engine-run.js +1 -1
- package/dist/scripts/engine-tests.js +1 -1
- package/dist/scripts/extension-tests.js +468 -0
- package/dist/scripts/fact-gate-tests.js +318 -0
- package/dist/scripts/flyai-tests.js +1 -1
- package/dist/scripts/hbcli-e2e-tests.js +180 -0
- package/dist/scripts/hbcli-tests.js +1 -1
- package/dist/scripts/health-watch-cli.js +51 -0
- package/dist/scripts/i18n-tests.js +1 -1
- package/dist/scripts/incident-tests.js +1 -1
- package/dist/scripts/journey-tests.js +1 -1
- package/dist/scripts/ledger-tests.js +1 -1
- package/dist/scripts/ledger-workflow-crash.js +1 -1
- package/dist/scripts/memory-capture-tests.js +1 -1
- package/dist/scripts/memory-decay-tests.js +1 -1
- package/dist/scripts/memory-metrics.js +1 -1
- package/dist/scripts/memory-value-report.js +1 -1
- package/dist/scripts/nightly-evidence-tests.js +67 -4
- package/dist/scripts/nightly-evidence.js +59 -3
- package/dist/scripts/nudge-digest.js +1 -1
- package/dist/scripts/onboarding-tests.js +218 -0
- package/dist/scripts/opensky-check.js +1 -1
- package/dist/scripts/opensky-tests.js +1 -1
- package/dist/scripts/price-drift-tests.js +441 -0
- package/dist/scripts/price-drift-watch.js +491 -0
- package/dist/scripts/probe-poi-tests.js +1 -1
- package/dist/scripts/product-metrics.js +1 -1
- package/dist/scripts/publish-preverify.js +1 -1
- package/dist/scripts/realtime-pricing-tests.js +1 -1
- package/dist/scripts/replay-async.js +1 -1
- package/dist/scripts/replay-real.js +1 -1
- package/dist/scripts/replay.js +1 -1
- package/dist/scripts/session-attach-diagnose.js +1 -1
- package/dist/scripts/session-attach-poc.js +1 -1
- package/dist/scripts/session-benchmark.js +1 -1
- package/dist/scripts/session-extract-tests.js +1 -1
- package/dist/scripts/session-login.js +1 -1
- package/dist/scripts/session-tests.js +1 -1
- package/dist/scripts/skeleton-check.js +1 -1
- package/dist/scripts/skeleton-integration-test.js +1 -1
- package/dist/scripts/skills-contract-tests.js +1 -1
- package/dist/scripts/smoke.js +76 -1
- package/dist/scripts/state-cli-tests.js +1 -1
- package/dist/scripts/state-cli.js +1 -1
- package/dist/scripts/time-eval-tests.js +1 -1
- package/dist/scripts/travel-timeline-tests.js +1 -1
- package/dist/scripts/unified-tests.js +1 -1
- package/dist/scripts/weather-tests.js +1 -1
- package/dist/scripts/wizard-bootstrap.js +32 -0
- package/dist/scripts/z3-race-tests.js +1 -1
- package/dist/src/artifact-gate.js +310 -0
- package/dist/src/bookable-facts.js +349 -0
- package/dist/src/booking-saga.js +1 -1
- package/dist/src/bridge.js +1 -1
- package/dist/src/companions.js +1 -1
- package/dist/src/contracts.js +1 -1
- package/dist/src/dsh-llm.js +1 -1
- package/dist/src/engine.js +1 -1
- package/dist/src/i18n.js +1 -1
- package/dist/src/index.js +189 -46
- package/dist/src/journey.js +1 -1
- package/dist/src/loop.js +1 -1
- package/dist/src/memory-capture.js +1 -1
- package/dist/src/memory-decay.js +1 -1
- package/dist/src/memory-utility.js +1 -1
- package/dist/src/mock-llm.js +1 -1
- package/dist/src/model.js +1 -1
- package/dist/src/realtime-pricing.js +14 -7
- package/dist/src/slot-spec.js +1 -1
- package/dist/src/state-ledger.js +1 -1
- package/dist/src/time-anchor.js +1 -1
- package/dist/src/tool-packet.js +1 -1
- package/dist/src/travel-slots.js +1 -1
- package/dist/src/travel-timeline.js +1 -1
- package/dist/src/unified.js +1 -1
- package/dist/src/wish-pool.js +1 -1
- package/dist/src/z3-shared.js +1 -1
- package/extension/README.md +26 -0
- package/extension/background.js +160 -0
- package/extension/content-bridge.js +41 -0
- package/extension/content-main.js +62 -0
- package/extension/manifest.json +32 -0
- package/package.json +6 -2
- package/ts/capabilities/fact-log.ts +54 -0
- package/ts/capabilities/hbcli.ts +46 -0
- package/ts/capabilities/session/benchmark.ts +4 -1
- package/ts/capabilities/session/extension-bridge.ts +354 -0
- package/ts/capabilities/session/extension-channel.ts +92 -0
- package/ts/capabilities/session/health-watch.ts +257 -0
- package/ts/capabilities/session/wizard.ts +282 -0
- package/ts/capabilities/session-login.ts +53 -2
- package/ts/capabilities/session-search.ts +68 -3
- package/ts/src/artifact-gate.ts +365 -0
- package/ts/src/bookable-facts.ts +557 -0
- package/ts/src/index.ts +102 -8
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
export const BRIDGE_PORTS = [
|
|
4
|
+
8791,
|
|
5
|
+
8792,
|
|
6
|
+
8793,
|
|
7
|
+
8794,
|
|
8
|
+
8795
|
|
9
|
+
];
|
|
10
|
+
export const BRIDGE_PROTOCOL = 'session-bridge.v1';
|
|
11
|
+
export const EXTENSION_ID = 'olpgkofjhhiiiahdkkbcninhjmegghfe';
|
|
12
|
+
export const EXTENSION_ORIGIN = `chrome-extension://${EXTENSION_ID}`;
|
|
13
|
+
export const EXTENSION_CONNECTED_WINDOW_MS = 45_000;
|
|
14
|
+
export const JOBS_LONG_POLL_MS = 20_000;
|
|
15
|
+
export const DEFAULT_EXTENSION_WAIT_MS = 6_000;
|
|
16
|
+
export const MAX_RESULT_BODY_BYTES = 8 * 1024 * 1024;
|
|
17
|
+
function readBody(req, cap) {
|
|
18
|
+
return new Promise((resolve)=>{
|
|
19
|
+
const chunks = [];
|
|
20
|
+
let size = 0;
|
|
21
|
+
let done = false;
|
|
22
|
+
req.on('data', (c)=>{
|
|
23
|
+
if (done) return;
|
|
24
|
+
size += c.length;
|
|
25
|
+
if (size > cap) {
|
|
26
|
+
done = true;
|
|
27
|
+
req.removeAllListeners('data');
|
|
28
|
+
resolve({
|
|
29
|
+
err: `body 超 ${cap} 字节上限(防失控回包)`
|
|
30
|
+
});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
chunks.push(c);
|
|
34
|
+
});
|
|
35
|
+
req.on('end', ()=>{
|
|
36
|
+
if (!done) {
|
|
37
|
+
done = true;
|
|
38
|
+
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
req.on('error', ()=>{
|
|
42
|
+
if (!done) {
|
|
43
|
+
done = true;
|
|
44
|
+
resolve({
|
|
45
|
+
err: 'body 读取失败'
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export async function createSessionBridge(opts = {}) {
|
|
52
|
+
const ports = opts.ports ?? [
|
|
53
|
+
...BRIDGE_PORTS
|
|
54
|
+
];
|
|
55
|
+
const extensionOrigin = opts.extensionOrigin ?? EXTENSION_ORIGIN;
|
|
56
|
+
const now = opts.now ?? Date.now;
|
|
57
|
+
const queue = [];
|
|
58
|
+
const inFlight = new Map();
|
|
59
|
+
const parked = [];
|
|
60
|
+
let lastSeenAt = 0;
|
|
61
|
+
let closed = false;
|
|
62
|
+
let port = 0;
|
|
63
|
+
const extensionConnected = ()=>lastSeenAt > 0 && now() - lastSeenAt < EXTENSION_CONNECTED_WINDOW_MS;
|
|
64
|
+
function takeQueued() {
|
|
65
|
+
const next = queue.shift();
|
|
66
|
+
if (next) inFlight.set(next.job.jobId, next);
|
|
67
|
+
return next ?? null;
|
|
68
|
+
}
|
|
69
|
+
const server = createServer((req, res)=>{
|
|
70
|
+
if (closed) {
|
|
71
|
+
res.statusCode = 503;
|
|
72
|
+
res.end();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const urlPath = (req.url ?? '').split('?')[0];
|
|
76
|
+
const origin = req.headers.origin;
|
|
77
|
+
const isPost = req.method === 'POST';
|
|
78
|
+
if (isPost && origin !== extensionOrigin) {
|
|
79
|
+
res.statusCode = 403;
|
|
80
|
+
res.setHeader('content-type', 'application/json');
|
|
81
|
+
res.end(JSON.stringify({
|
|
82
|
+
ok: false,
|
|
83
|
+
error: 'origin 不在桥白名单'
|
|
84
|
+
}));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (origin === extensionOrigin) {
|
|
88
|
+
lastSeenAt = now();
|
|
89
|
+
}
|
|
90
|
+
const finish = (code, body)=>{
|
|
91
|
+
res.statusCode = code;
|
|
92
|
+
res.setHeader('content-type', 'application/json');
|
|
93
|
+
res.end(JSON.stringify(body));
|
|
94
|
+
};
|
|
95
|
+
if (req.method === 'GET' && urlPath === '/health') {
|
|
96
|
+
finish(200, {
|
|
97
|
+
ok: true,
|
|
98
|
+
service: 'gotry-session-bridge',
|
|
99
|
+
protocol: BRIDGE_PROTOCOL
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (req.method === 'GET' && urlPath === '/status') {
|
|
104
|
+
finish(200, {
|
|
105
|
+
ok: true,
|
|
106
|
+
port,
|
|
107
|
+
protocol: BRIDGE_PROTOCOL,
|
|
108
|
+
extensionConnected: extensionConnected(),
|
|
109
|
+
lastSeenMsAgo: lastSeenAt > 0 ? now() - lastSeenAt : null,
|
|
110
|
+
queued: queue.length,
|
|
111
|
+
inFlight: inFlight.size
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (isPost && urlPath === '/jobs') {
|
|
116
|
+
const next = takeQueued();
|
|
117
|
+
if (next) {
|
|
118
|
+
finish(200, {
|
|
119
|
+
job: next.job
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const parkTimer = setTimeout(()=>{
|
|
124
|
+
const i = parked.findIndex((p)=>p.res === res);
|
|
125
|
+
if (i >= 0) parked.splice(i, 1);
|
|
126
|
+
finish(200, {
|
|
127
|
+
job: null
|
|
128
|
+
});
|
|
129
|
+
}, JOBS_LONG_POLL_MS);
|
|
130
|
+
parked.push({
|
|
131
|
+
res,
|
|
132
|
+
timer: parkTimer
|
|
133
|
+
});
|
|
134
|
+
res.on('close', ()=>{
|
|
135
|
+
const i = parked.findIndex((p)=>p.res === res);
|
|
136
|
+
if (i >= 0) parked.splice(i, 1);
|
|
137
|
+
clearTimeout(parkTimer);
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (isPost && urlPath.startsWith('/results/')) {
|
|
142
|
+
const jobId = decodeURIComponent(urlPath.slice('/results/'.length));
|
|
143
|
+
void readBody(req, MAX_RESULT_BODY_BYTES).then((body)=>{
|
|
144
|
+
if (typeof body !== 'string') {
|
|
145
|
+
finish(400, {
|
|
146
|
+
ok: false,
|
|
147
|
+
error: body.err
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
let parsed;
|
|
152
|
+
try {
|
|
153
|
+
parsed = JSON.parse(body);
|
|
154
|
+
} catch {
|
|
155
|
+
finish(400, {
|
|
156
|
+
ok: false,
|
|
157
|
+
error: '回包不是 JSON'
|
|
158
|
+
});
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
finish(200, {
|
|
162
|
+
ok: true
|
|
163
|
+
});
|
|
164
|
+
resolveJob(jobId, parsed);
|
|
165
|
+
});
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
finish(404, {
|
|
169
|
+
ok: false,
|
|
170
|
+
error: 'unknown endpoint'
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
function resolveJob(jobId, parsed) {
|
|
174
|
+
const inFlightHit = inFlight.get(jobId);
|
|
175
|
+
if (inFlightHit) {
|
|
176
|
+
inFlight.delete(jobId);
|
|
177
|
+
if (inFlightHit.timer) clearTimeout(inFlightHit.timer);
|
|
178
|
+
inFlightHit.resolve({
|
|
179
|
+
ok: true,
|
|
180
|
+
result: parsed
|
|
181
|
+
});
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const idx = queue.findIndex((q)=>q.job.jobId === jobId);
|
|
185
|
+
if (idx < 0) return;
|
|
186
|
+
const [entry] = queue.splice(idx, 1);
|
|
187
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
188
|
+
entry.resolve({
|
|
189
|
+
ok: true,
|
|
190
|
+
result: parsed
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
function dispatchToParked(entry) {
|
|
194
|
+
const p = parked.shift();
|
|
195
|
+
if (!p) return false;
|
|
196
|
+
clearTimeout(p.timer);
|
|
197
|
+
inFlight.set(entry.job.jobId, entry);
|
|
198
|
+
p.res.statusCode = 200;
|
|
199
|
+
p.res.setHeader('content-type', 'application/json');
|
|
200
|
+
p.res.end(JSON.stringify({
|
|
201
|
+
job: entry.job
|
|
202
|
+
}));
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
const bridge = {
|
|
206
|
+
port: 0,
|
|
207
|
+
extensionConnected,
|
|
208
|
+
submit (job, submitOpts = {}) {
|
|
209
|
+
const extensionWaitMs = submitOpts.extensionWaitMs ?? DEFAULT_EXTENSION_WAIT_MS;
|
|
210
|
+
const timeoutMs = submitOpts.timeoutMs ?? job.timeoutMs ?? 30_000;
|
|
211
|
+
const full = {
|
|
212
|
+
jobId: randomUUID(),
|
|
213
|
+
...job
|
|
214
|
+
};
|
|
215
|
+
return new Promise((resolve)=>{
|
|
216
|
+
if (closed) {
|
|
217
|
+
resolve({
|
|
218
|
+
ok: false,
|
|
219
|
+
reason: 'bridge-unavailable',
|
|
220
|
+
summary: '扩展桥已关闭'
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const entry = {
|
|
225
|
+
job: full,
|
|
226
|
+
resolve: null,
|
|
227
|
+
timer: null
|
|
228
|
+
};
|
|
229
|
+
const settle = (outcome)=>{
|
|
230
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
231
|
+
const i = queue.findIndex((q)=>q.job.jobId === full.jobId);
|
|
232
|
+
if (i >= 0) queue.splice(i, 1);
|
|
233
|
+
inFlight.delete(full.jobId);
|
|
234
|
+
resolve(outcome);
|
|
235
|
+
};
|
|
236
|
+
entry.resolve = settle;
|
|
237
|
+
entry.timer = setTimeout(()=>{
|
|
238
|
+
settle({
|
|
239
|
+
ok: false,
|
|
240
|
+
reason: 'timeout',
|
|
241
|
+
summary: `扩展未在 ${timeoutMs}ms 内回包(标签页无嗅探命中或扩展已停用)`
|
|
242
|
+
});
|
|
243
|
+
}, timeoutMs);
|
|
244
|
+
if (!dispatchToParked(entry)) queue.push(entry);
|
|
245
|
+
let waited = 0;
|
|
246
|
+
const waitTimer = setInterval(()=>{
|
|
247
|
+
waited += 250;
|
|
248
|
+
if (bridge.extensionConnected()) {
|
|
249
|
+
clearInterval(waitTimer);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (waited >= extensionWaitMs) {
|
|
253
|
+
clearInterval(waitTimer);
|
|
254
|
+
settle({
|
|
255
|
+
ok: false,
|
|
256
|
+
reason: 'extension-not-connected',
|
|
257
|
+
summary: 'GoTry Session Bridge 扩展未连接(未安装或已停用)。一次性安装:npx gotry setup 查看指引 → chrome://extensions 开发者模式「加载已解压的扩展程序」指向 ~/.gotry/extension'
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}, 250);
|
|
261
|
+
});
|
|
262
|
+
},
|
|
263
|
+
close: ()=>new Promise((resolve)=>{
|
|
264
|
+
closed = true;
|
|
265
|
+
const voided = {
|
|
266
|
+
ok: false,
|
|
267
|
+
reason: 'bridge-unavailable',
|
|
268
|
+
summary: '扩展桥关闭,任务作废(零花费)'
|
|
269
|
+
};
|
|
270
|
+
for (const q of queue.splice(0)){
|
|
271
|
+
if (q.timer) clearTimeout(q.timer);
|
|
272
|
+
q.resolve(voided);
|
|
273
|
+
}
|
|
274
|
+
for (const [, q] of inFlight){
|
|
275
|
+
if (q.timer) clearTimeout(q.timer);
|
|
276
|
+
q.resolve(voided);
|
|
277
|
+
}
|
|
278
|
+
inFlight.clear();
|
|
279
|
+
for (const p of parked.splice(0)){
|
|
280
|
+
clearTimeout(p.timer);
|
|
281
|
+
p.res.destroy();
|
|
282
|
+
}
|
|
283
|
+
server.close(()=>resolve());
|
|
284
|
+
server.closeAllConnections?.();
|
|
285
|
+
})
|
|
286
|
+
};
|
|
287
|
+
const listenAt = (p)=>new Promise((resolve, reject)=>{
|
|
288
|
+
const onError = (e)=>{
|
|
289
|
+
server.off('listening', onListening);
|
|
290
|
+
reject(e);
|
|
291
|
+
};
|
|
292
|
+
const onListening = ()=>{
|
|
293
|
+
server.off('error', onError);
|
|
294
|
+
resolve();
|
|
295
|
+
};
|
|
296
|
+
server.once('error', onError);
|
|
297
|
+
server.once('listening', onListening);
|
|
298
|
+
server.listen(p, '127.0.0.1');
|
|
299
|
+
});
|
|
300
|
+
for (const p of ports){
|
|
301
|
+
try {
|
|
302
|
+
await listenAt(p);
|
|
303
|
+
if (!opts.keepBridge) {
|
|
304
|
+
server.unref();
|
|
305
|
+
}
|
|
306
|
+
port = server.address().port;
|
|
307
|
+
bridge.port = port;
|
|
308
|
+
return {
|
|
309
|
+
ok: true,
|
|
310
|
+
bridge
|
|
311
|
+
};
|
|
312
|
+
} catch {}
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
ok: false,
|
|
316
|
+
summary: `扩展桥端口池 ${ports.join('/')} 全部被占(多为并行 gotry 实例);不影响官方通道 gotry_flyai_search。关闭多余实例后重试。`
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
let singleton = null;
|
|
320
|
+
export async function getOrCreateSessionBridge(opts = {}) {
|
|
321
|
+
if (singleton) return singleton;
|
|
322
|
+
const created = await createSessionBridge(opts);
|
|
323
|
+
const result = created.ok ? {
|
|
324
|
+
ok: true,
|
|
325
|
+
bridge: created.bridge
|
|
326
|
+
} : {
|
|
327
|
+
ok: false,
|
|
328
|
+
summary: created.summary
|
|
329
|
+
};
|
|
330
|
+
singleton = result;
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
export async function __resetSessionBridgeForTest() {
|
|
334
|
+
if (singleton?.ok) await singleton.bridge.close().catch(()=>{});
|
|
335
|
+
singleton = null;
|
|
336
|
+
}
|
|
337
|
+
export function __setSessionBridgeForTest(bridge) {
|
|
338
|
+
singleton = bridge ? {
|
|
339
|
+
ok: true,
|
|
340
|
+
bridge
|
|
341
|
+
} : null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
//# sourceURL=/Users/bytedance/work/gotry-publish/ts/capabilities/session/extension-bridge.ts
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { getOrCreateSessionBridge } from './extension-bridge.js';
|
|
2
|
+
export const NEEDS_EXTENSION_HINT = '需要一次性安装 GoTry Session Bridge 浏览器扩展(约 30 秒,装完零弹窗):跑 npx gotry setup 拿指引,' + '或在 chrome://extensions 开发者模式「加载已解压的扩展程序」指向 ~/.gotry/extension';
|
|
3
|
+
export function classifyBridgeFailure(kind) {
|
|
4
|
+
return kind === 'extension-not-connected' ? 'needs-extension' : 'error';
|
|
5
|
+
}
|
|
6
|
+
function bridgeReady(b) {
|
|
7
|
+
return b.ok === true;
|
|
8
|
+
}
|
|
9
|
+
export async function extensionCookieNames(q) {
|
|
10
|
+
const bridge = await getOrCreateSessionBridge();
|
|
11
|
+
if (!bridgeReady(bridge)) return {
|
|
12
|
+
ok: false,
|
|
13
|
+
kind: 'bridge-unavailable',
|
|
14
|
+
summary: bridge.summary
|
|
15
|
+
};
|
|
16
|
+
const outcome = await bridge.bridge.submit({
|
|
17
|
+
kind: 'cookie-names',
|
|
18
|
+
site: q.site,
|
|
19
|
+
timeoutMs: q.timeoutMs ?? 8_000
|
|
20
|
+
}, {
|
|
21
|
+
timeoutMs: q.timeoutMs ?? 8_000
|
|
22
|
+
});
|
|
23
|
+
if (!outcome.ok) return {
|
|
24
|
+
ok: false,
|
|
25
|
+
kind: outcome.reason,
|
|
26
|
+
summary: outcome.summary
|
|
27
|
+
};
|
|
28
|
+
const names = Array.isArray(outcome.result.names) ? outcome.result.names.filter((n)=>typeof n === 'string') : [];
|
|
29
|
+
return {
|
|
30
|
+
ok: true,
|
|
31
|
+
tickets: names
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export async function extensionOpenLogin(q) {
|
|
35
|
+
const bridge = await getOrCreateSessionBridge();
|
|
36
|
+
if (!bridgeReady(bridge)) return {
|
|
37
|
+
ok: false,
|
|
38
|
+
kind: 'bridge-unavailable',
|
|
39
|
+
summary: bridge.summary
|
|
40
|
+
};
|
|
41
|
+
const outcome = await bridge.bridge.submit({
|
|
42
|
+
kind: 'open-login',
|
|
43
|
+
site: q.site,
|
|
44
|
+
url: q.url
|
|
45
|
+
}, {
|
|
46
|
+
timeoutMs: q.timeoutMs ?? 15_000
|
|
47
|
+
});
|
|
48
|
+
if (!outcome.ok) return {
|
|
49
|
+
ok: false,
|
|
50
|
+
kind: outcome.reason,
|
|
51
|
+
summary: outcome.summary
|
|
52
|
+
};
|
|
53
|
+
if (!outcome.result.opened) return {
|
|
54
|
+
ok: false,
|
|
55
|
+
kind: 'job-error',
|
|
56
|
+
summary: outcome.result.error ?? '登录入口打开失败'
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
ok: true,
|
|
60
|
+
opened: true
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export async function extensionSearchJob(q) {
|
|
64
|
+
const bridge = await getOrCreateSessionBridge();
|
|
65
|
+
if (!bridgeReady(bridge)) return {
|
|
66
|
+
ok: false,
|
|
67
|
+
kind: 'bridge-unavailable',
|
|
68
|
+
summary: bridge.summary
|
|
69
|
+
};
|
|
70
|
+
const timeoutMs = q.timeoutMs ?? 30_000;
|
|
71
|
+
const outcome = await bridge.bridge.submit({
|
|
72
|
+
kind: 'search',
|
|
73
|
+
site: q.site,
|
|
74
|
+
url: q.url,
|
|
75
|
+
timeoutMs
|
|
76
|
+
}, {
|
|
77
|
+
timeoutMs: timeoutMs + 10_000
|
|
78
|
+
});
|
|
79
|
+
if (!outcome.ok) return {
|
|
80
|
+
ok: false,
|
|
81
|
+
kind: outcome.reason,
|
|
82
|
+
summary: outcome.summary
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
ok: true,
|
|
86
|
+
body: typeof outcome.result.body === 'string' ? outcome.result.body : '',
|
|
87
|
+
title: typeof outcome.result.title === 'string' ? outcome.result.title : '',
|
|
88
|
+
timedOut: outcome.result.timeout === true
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
//# sourceURL=/Users/bytedance/work/gotry-publish/ts/capabilities/session/extension-channel.ts
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { request } from 'node:http';
|
|
2
|
+
import { BRIDGE_PORTS, EXTENSION_CONNECTED_WINDOW_MS } from './extension-bridge.js';
|
|
3
|
+
export async function combinedProbe(opts, businessProbe) {
|
|
4
|
+
if (!await defaultProbe(opts)) return false;
|
|
5
|
+
if (!businessProbe) return true;
|
|
6
|
+
try {
|
|
7
|
+
const channel = await import('./extension-channel.js');
|
|
8
|
+
const bridge = await import('./extension-bridge.js');
|
|
9
|
+
bridge.getOrCreateSessionBridge({
|
|
10
|
+
keepBridge: opts.keepBridge === true
|
|
11
|
+
});
|
|
12
|
+
const cookie = await channel.extensionCookieNames({
|
|
13
|
+
site: 'ctrip-flight',
|
|
14
|
+
domain: 'ctrip.com',
|
|
15
|
+
ticketNames: [
|
|
16
|
+
'cticket',
|
|
17
|
+
'uid'
|
|
18
|
+
],
|
|
19
|
+
timeoutMs: 8_000
|
|
20
|
+
});
|
|
21
|
+
return cookie.ok;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function defaultProbe(opts) {
|
|
27
|
+
const ports = opts.ports ?? [
|
|
28
|
+
...BRIDGE_PORTS
|
|
29
|
+
];
|
|
30
|
+
for (const port of ports){
|
|
31
|
+
const ok = await new Promise((resolve)=>{
|
|
32
|
+
const req = request({
|
|
33
|
+
host: '127.0.0.1',
|
|
34
|
+
port,
|
|
35
|
+
path: '/status',
|
|
36
|
+
method: 'GET',
|
|
37
|
+
timeout: 2_000
|
|
38
|
+
}, (res)=>{
|
|
39
|
+
if (!res.statusCode || res.statusCode >= 400) {
|
|
40
|
+
resolve(false);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
let buf = '';
|
|
44
|
+
res.setEncoding('utf8');
|
|
45
|
+
res.on('data', (c)=>{
|
|
46
|
+
buf += c;
|
|
47
|
+
});
|
|
48
|
+
res.on('end', ()=>{
|
|
49
|
+
try {
|
|
50
|
+
const payload = JSON.parse(buf);
|
|
51
|
+
resolve(payload.extensionConnected === true);
|
|
52
|
+
} catch {
|
|
53
|
+
resolve(false);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
res.on('error', ()=>resolve(false));
|
|
57
|
+
});
|
|
58
|
+
req.on('error', ()=>resolve(false));
|
|
59
|
+
req.on('timeout', ()=>{
|
|
60
|
+
req.destroy();
|
|
61
|
+
resolve(false);
|
|
62
|
+
});
|
|
63
|
+
req.end();
|
|
64
|
+
});
|
|
65
|
+
if (ok) return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
export function startExtensionHealthWatch(opts = {}) {
|
|
70
|
+
const timeoutMs = opts.timeoutMs ?? 120_000;
|
|
71
|
+
const intervalMs = opts.intervalMs ?? 5_000;
|
|
72
|
+
const maxRetries = opts.maxRetries ?? 1;
|
|
73
|
+
const businessProbe = opts.businessProbe ?? true;
|
|
74
|
+
const probe = opts.probe ?? (async ()=>{
|
|
75
|
+
const bridge = await import('./extension-bridge.js');
|
|
76
|
+
await bridge.getOrCreateSessionBridge({
|
|
77
|
+
keepBridge: opts.keepBridge === true
|
|
78
|
+
});
|
|
79
|
+
return combinedProbe(opts, businessProbe);
|
|
80
|
+
});
|
|
81
|
+
const now = opts.now ?? Date.now;
|
|
82
|
+
const state = {
|
|
83
|
+
cancelled: false,
|
|
84
|
+
readyCount: 0,
|
|
85
|
+
resolveOutcome: null
|
|
86
|
+
};
|
|
87
|
+
const finish = (outcome)=>{
|
|
88
|
+
if (state.resolveOutcome) {
|
|
89
|
+
const resolve = state.resolveOutcome;
|
|
90
|
+
state.resolveOutcome = null;
|
|
91
|
+
resolve(outcome);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const onAbort = ()=>{
|
|
95
|
+
state.cancelled = true;
|
|
96
|
+
finish({
|
|
97
|
+
ready: false,
|
|
98
|
+
reason: 'cancelled',
|
|
99
|
+
attempts: state.readyCount,
|
|
100
|
+
waitedMs: 0
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
opts.signal?.addEventListener('abort', onAbort, {
|
|
104
|
+
once: true
|
|
105
|
+
});
|
|
106
|
+
const waitReady = ()=>new Promise((resolve)=>{
|
|
107
|
+
if (state.cancelled) {
|
|
108
|
+
resolve({
|
|
109
|
+
ready: false,
|
|
110
|
+
reason: 'cancelled',
|
|
111
|
+
attempts: 0,
|
|
112
|
+
waitedMs: 0
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (timeoutMs === 0) {
|
|
117
|
+
resolve({
|
|
118
|
+
ready: false,
|
|
119
|
+
reason: 'timeout',
|
|
120
|
+
attempts: 0,
|
|
121
|
+
waitedMs: 0
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
state.resolveOutcome = resolve;
|
|
126
|
+
const deadline = now() + timeoutMs;
|
|
127
|
+
let attempts = 0;
|
|
128
|
+
let waitedMs = 0;
|
|
129
|
+
const tick = async ()=>{
|
|
130
|
+
if (state.cancelled || state.resolveOutcome === null) return;
|
|
131
|
+
attempts += 1;
|
|
132
|
+
try {
|
|
133
|
+
const ready = await probe();
|
|
134
|
+
if (ready && !state.cancelled) {
|
|
135
|
+
state.readyCount += 1;
|
|
136
|
+
waitedMs = timeoutMs - Math.max(0, deadline - now());
|
|
137
|
+
if (state.readyCount <= maxRetries) {
|
|
138
|
+
finish({
|
|
139
|
+
ready: true,
|
|
140
|
+
attempts,
|
|
141
|
+
waitedMs: waitedMs > 0 ? waitedMs : 0
|
|
142
|
+
});
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} catch {}
|
|
147
|
+
waitedMs = timeoutMs - Math.max(0, deadline - now());
|
|
148
|
+
if (now() >= deadline) {
|
|
149
|
+
finish({
|
|
150
|
+
ready: false,
|
|
151
|
+
reason: 'timeout',
|
|
152
|
+
attempts,
|
|
153
|
+
waitedMs
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const remaining = deadline - now();
|
|
158
|
+
const delay = Math.min(intervalMs, Math.max(0, remaining));
|
|
159
|
+
if (delay <= 0) {
|
|
160
|
+
finish({
|
|
161
|
+
ready: false,
|
|
162
|
+
reason: 'timeout',
|
|
163
|
+
attempts,
|
|
164
|
+
waitedMs
|
|
165
|
+
});
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
setTimeout(tick, delay);
|
|
169
|
+
};
|
|
170
|
+
void tick();
|
|
171
|
+
});
|
|
172
|
+
return {
|
|
173
|
+
waitReady,
|
|
174
|
+
cancel: ()=>onAbort(),
|
|
175
|
+
get readyCount () {
|
|
176
|
+
return state.readyCount;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
export async function withAutoRetryOnExtension(args) {
|
|
181
|
+
const first = await args.fetch();
|
|
182
|
+
if (!args.isExtensionNeeded(first)) {
|
|
183
|
+
return {
|
|
184
|
+
result: first,
|
|
185
|
+
retried: false,
|
|
186
|
+
attempts: 1,
|
|
187
|
+
outcome: {
|
|
188
|
+
ready: true,
|
|
189
|
+
attempts: 0,
|
|
190
|
+
waitedMs: 0
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const watch = startExtensionHealthWatch(args.watch ?? {});
|
|
195
|
+
const outcome = await watch.waitReady();
|
|
196
|
+
if (!outcome.ready) {
|
|
197
|
+
return {
|
|
198
|
+
result: first,
|
|
199
|
+
retried: false,
|
|
200
|
+
attempts: 1,
|
|
201
|
+
outcome
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
const retriedResult = await (args.retry ?? args.fetch)();
|
|
205
|
+
return {
|
|
206
|
+
result: retriedResult,
|
|
207
|
+
retried: true,
|
|
208
|
+
attempts: 2,
|
|
209
|
+
outcome
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
export const HEALTH_WATCH_INTERVAL_FLOOR_MS = 1_000;
|
|
213
|
+
export const HEALTH_WATCH_INTERVAL_CEIL_MS = EXTENSION_CONNECTED_WINDOW_MS - 1_000;
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
//# sourceURL=/Users/bytedance/work/gotry-publish/ts/capabilities/session/health-watch.ts
|