@danceiny/gotry 0.0.1-rc.10
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/LICENSE +21 -0
- package/README.md +267 -0
- package/bin/gotry-inner.js +214 -0
- package/bin/gotry-stdio-ask.js +59 -0
- package/bin/gotry.js +28 -0
- package/cordis.gotry-patch.yml +86 -0
- package/data/flights_2026.json +218 -0
- package/data/golden_erhai.json +92 -0
- package/data/golden_trip_2026.json +75 -0
- package/data/hotels_2026.json +151 -0
- package/data/openflights-skeleton.json +895 -0
- package/data/time-slot-eval.json +384 -0
- package/data/yunnan-pack.json +202 -0
- package/dist/capabilities/agent-reach-bridge.py +90 -0
- package/dist/capabilities/agent-reach-deep.js +172 -0
- package/dist/capabilities/agent-reach.js +204 -0
- package/dist/capabilities/anything.js +139 -0
- package/dist/capabilities/flyai.js +131 -0
- package/dist/capabilities/hbcli.js +143 -0
- package/dist/capabilities/incident-log.js +100 -0
- package/dist/capabilities/opensky.js +74 -0
- package/dist/capabilities/session/action-cache.js +120 -0
- package/dist/capabilities/session/adapters/ctrip-flight.js +83 -0
- package/dist/capabilities/session/adapters/meituan-local.js +73 -0
- package/dist/capabilities/session/extract.js +40 -0
- package/dist/capabilities/session/read-guard.js +99 -0
- package/dist/capabilities/session/transport.js +101 -0
- package/dist/capabilities/session-search.js +93 -0
- package/dist/capabilities/weather.js +180 -0
- package/dist/scripts/action-cache-tests.js +107 -0
- package/dist/scripts/agent-reach-deep-tests.js +54 -0
- package/dist/scripts/agent-reach-tests.js +38 -0
- package/dist/scripts/agent-reach-wrapper-tests.js +79 -0
- package/dist/scripts/anything-tests.js +95 -0
- package/dist/scripts/async-collect.js +37 -0
- package/dist/scripts/companion-tests.js +112 -0
- package/dist/scripts/diff-test.js +39 -0
- package/dist/scripts/engine-run.js +14 -0
- package/dist/scripts/engine-tests.js +45 -0
- package/dist/scripts/hbcli-tests.js +101 -0
- package/dist/scripts/incident-tests.js +96 -0
- package/dist/scripts/journey-tests.js +45 -0
- package/dist/scripts/ledger-tests.js +365 -0
- package/dist/scripts/ledger-workflow-crash.js +42 -0
- package/dist/scripts/memory-capture-tests.js +77 -0
- package/dist/scripts/memory-decay-tests.js +83 -0
- package/dist/scripts/memory-metrics.js +24 -0
- package/dist/scripts/nudge-digest.js +83 -0
- package/dist/scripts/opensky-check.js +20 -0
- package/dist/scripts/opensky-tests.js +52 -0
- package/dist/scripts/probe-poi-tests.js +88 -0
- package/dist/scripts/publish-preverify.js +68 -0
- package/dist/scripts/replay-async.js +44 -0
- package/dist/scripts/replay-real.js +38 -0
- package/dist/scripts/replay.js +116 -0
- package/dist/scripts/session-attach-diagnose.js +27 -0
- package/dist/scripts/session-attach-poc.js +65 -0
- package/dist/scripts/session-extract-tests.js +81 -0
- package/dist/scripts/session-tests.js +162 -0
- package/dist/scripts/skeleton-check.js +37 -0
- package/dist/scripts/skeleton-integration-test.js +22 -0
- package/dist/scripts/skills-contract-tests.js +85 -0
- package/dist/scripts/smoke.js +328 -0
- package/dist/scripts/state-cli-tests.js +136 -0
- package/dist/scripts/state-cli.js +248 -0
- package/dist/scripts/time-eval-tests.js +318 -0
- package/dist/scripts/travel-timeline-tests.js +123 -0
- package/dist/scripts/unified-tests.js +49 -0
- package/dist/scripts/weather-tests.js +48 -0
- package/dist/src/bridge.js +34 -0
- package/dist/src/companions.js +112 -0
- package/dist/src/contracts.js +64 -0
- package/dist/src/dsh-llm.js +174 -0
- package/dist/src/engine.js +331 -0
- package/dist/src/index.js +1239 -0
- package/dist/src/journey.js +147 -0
- package/dist/src/loop.js +360 -0
- package/dist/src/memory-capture.js +40 -0
- package/dist/src/memory-decay.js +31 -0
- package/dist/src/memory-utility.js +55 -0
- package/dist/src/mock-llm.js +103 -0
- package/dist/src/model.js +134 -0
- package/dist/src/slot-spec.js +165 -0
- package/dist/src/state-ledger.js +901 -0
- package/dist/src/time-anchor.js +100 -0
- package/dist/src/tool-packet.js +12 -0
- package/dist/src/travel-slots.js +144 -0
- package/dist/src/travel-timeline.js +78 -0
- package/dist/src/unified.js +538 -0
- package/dist/src/wish-pool.js +27 -0
- package/package.json +84 -0
- package/ts/capabilities/agent-reach-deep.ts +114 -0
- package/ts/capabilities/agent-reach.ts +158 -0
- package/ts/capabilities/anything.ts +201 -0
- package/ts/capabilities/flyai.ts +151 -0
- package/ts/capabilities/hbcli.ts +152 -0
- package/ts/capabilities/incident-log.ts +148 -0
- package/ts/capabilities/opensky.ts +121 -0
- package/ts/capabilities/session/action-cache.ts +148 -0
- package/ts/capabilities/session/adapters/ctrip-flight.ts +107 -0
- package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
- package/ts/capabilities/session/extract.ts +54 -0
- package/ts/capabilities/session/read-guard.ts +141 -0
- package/ts/capabilities/session/transport.ts +114 -0
- package/ts/capabilities/session-search.ts +129 -0
- package/ts/capabilities/weather.ts +176 -0
- package/ts/package.json +27 -0
- package/ts/scripts/async-collect.ts +50 -0
- package/ts/scripts/skeleton-check.ts +49 -0
- package/ts/scripts/skeleton-integration-test.ts +23 -0
- package/ts/scripts/state-cli.ts +201 -0
- package/ts/src/bridge.ts +44 -0
- package/ts/src/companions.ts +113 -0
- package/ts/src/contracts.ts +165 -0
- package/ts/src/dsh-llm.ts +155 -0
- package/ts/src/index.ts +968 -0
- package/ts/src/loop.ts +411 -0
- package/ts/src/memory-capture.ts +59 -0
- package/ts/src/memory-decay.ts +55 -0
- package/ts/src/memory-utility.ts +70 -0
- package/ts/src/mock-llm.ts +100 -0
- package/ts/src/model.ts +220 -0
- package/ts/src/slot-spec.ts +172 -0
- package/ts/src/state-ledger.ts +849 -0
- package/ts/src/time-anchor.ts +134 -0
- package/ts/src/tool-packet.ts +32 -0
- package/ts/src/travel-slots.ts +219 -0
- package/ts/src/travel-timeline.ts +83 -0
- package/ts/src/unified.ts +582 -0
- package/ts/src/wish-pool.ts +66 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
export async function callHbcliJson(args, opts = {}) {
|
|
3
|
+
const started = Date.now();
|
|
4
|
+
const bin = opts.hbcliBin ?? 'hbcli';
|
|
5
|
+
const timeoutMs = opts.timeoutMs ?? 15_000;
|
|
6
|
+
const env = opts.env ?? 'uat';
|
|
7
|
+
const envVars = {
|
|
8
|
+
HOTELBYTE_ENV: env
|
|
9
|
+
};
|
|
10
|
+
if (opts.token) envVars['HOTELBYTE_TOKEN'] = opts.token;
|
|
11
|
+
return new Promise((resolve)=>{
|
|
12
|
+
let stdout = '';
|
|
13
|
+
let stderr = '';
|
|
14
|
+
let settled = false;
|
|
15
|
+
const child = spawn(bin, args, {
|
|
16
|
+
env: {
|
|
17
|
+
...process.env,
|
|
18
|
+
...envVars
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
const timer = setTimeout(()=>{
|
|
22
|
+
if (!settled) {
|
|
23
|
+
settled = true;
|
|
24
|
+
child.kill('SIGKILL');
|
|
25
|
+
resolve({
|
|
26
|
+
via: 'hbcli-error',
|
|
27
|
+
exitCode: -1,
|
|
28
|
+
result: null,
|
|
29
|
+
evidence: `[实时API:hbcli@timeout@${new Date().toISOString()}]`,
|
|
30
|
+
latencyMs: Date.now() - started,
|
|
31
|
+
error: `timeout after ${timeoutMs}ms`
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}, timeoutMs);
|
|
35
|
+
child.stdout.on('data', (d)=>{
|
|
36
|
+
stdout += d.toString();
|
|
37
|
+
});
|
|
38
|
+
child.stderr.on('data', (d)=>{
|
|
39
|
+
stderr += d.toString();
|
|
40
|
+
});
|
|
41
|
+
child.on('close', (code)=>{
|
|
42
|
+
if (settled) return;
|
|
43
|
+
settled = true;
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
const latencyMs = Date.now() - started;
|
|
46
|
+
if (code !== 0) {
|
|
47
|
+
resolve({
|
|
48
|
+
via: 'hbcli-error',
|
|
49
|
+
exitCode: code ?? -1,
|
|
50
|
+
result: null,
|
|
51
|
+
evidence: `[实时API:hbcli@error@${new Date().toISOString()}]`,
|
|
52
|
+
stderr: stderr.slice(0, 2000),
|
|
53
|
+
latencyMs,
|
|
54
|
+
error: stderr.trim().slice(0, 200) || `exit ${code}`
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const jStart = stdout.search(/[\{\[]/);
|
|
59
|
+
const jsonStr = jStart >= 0 ? stdout.slice(jStart) : '';
|
|
60
|
+
let result = null;
|
|
61
|
+
if (jsonStr) {
|
|
62
|
+
try {
|
|
63
|
+
result = JSON.parse(jsonStr);
|
|
64
|
+
} catch {}
|
|
65
|
+
}
|
|
66
|
+
resolve({
|
|
67
|
+
via: 'hbcli-realtime',
|
|
68
|
+
exitCode: 0,
|
|
69
|
+
result,
|
|
70
|
+
evidence: `[实时API:hbcli@${new Date().toISOString()}]`,
|
|
71
|
+
stdout: stdout.slice(0, 2000),
|
|
72
|
+
latencyMs
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
child.on('error', (e)=>{
|
|
76
|
+
if (settled) return;
|
|
77
|
+
settled = true;
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
resolve({
|
|
80
|
+
via: 'hbcli-error',
|
|
81
|
+
exitCode: -1,
|
|
82
|
+
result: null,
|
|
83
|
+
evidence: `[实时API:hbcli@spawn_error@${new Date().toISOString()}]`,
|
|
84
|
+
latencyMs: Date.now() - started,
|
|
85
|
+
error: e.message
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
export async function searchHotels(query, opts = {}) {
|
|
91
|
+
const hbArgs = [
|
|
92
|
+
'search',
|
|
93
|
+
'hotel-list',
|
|
94
|
+
'--json',
|
|
95
|
+
'--page-size',
|
|
96
|
+
'10'
|
|
97
|
+
];
|
|
98
|
+
if (query.destination) hbArgs.push('--destination-name', query.destination);
|
|
99
|
+
if (query.adults) hbArgs.push('--room-occupancies', JSON.stringify([
|
|
100
|
+
{
|
|
101
|
+
adultCount: query.adults,
|
|
102
|
+
childrenAges: []
|
|
103
|
+
}
|
|
104
|
+
]));
|
|
105
|
+
const live = await callHbcliJson(hbArgs, opts);
|
|
106
|
+
if (live.via === 'hbcli-realtime') {
|
|
107
|
+
return {
|
|
108
|
+
...live,
|
|
109
|
+
hotels: live.result,
|
|
110
|
+
summary: `${query.destination}:hbcli 实时返回${query.checkIn || query.checkOut ? '(日期不传上游 list,以当前窗口房价返回)' : ''}`
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const fallback = opts.fallbackPath;
|
|
114
|
+
if (fallback) {
|
|
115
|
+
try {
|
|
116
|
+
const { readFile } = await import('node:fs/promises');
|
|
117
|
+
const pack = JSON.parse(await readFile(fallback, 'utf-8'));
|
|
118
|
+
return {
|
|
119
|
+
...live,
|
|
120
|
+
hotels: pack,
|
|
121
|
+
summary: `${query.destination}:hbcli 不可用(${live.error ?? live.via}),降级到静态包`
|
|
122
|
+
};
|
|
123
|
+
} catch {}
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
...live,
|
|
127
|
+
summary: `${query.destination}:hbcli 不可用且无静态包(仅返回错误)`
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export async function listDestinations(opts = {}) {
|
|
131
|
+
const r = await callHbcliJson([
|
|
132
|
+
'search',
|
|
133
|
+
'destinations',
|
|
134
|
+
'--json'
|
|
135
|
+
], opts);
|
|
136
|
+
return {
|
|
137
|
+
...r,
|
|
138
|
+
destinations: r.result
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/capabilities/hbcli.ts
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
export function resolveIncidentsPath(stateRoot, filename = 'incidents.jsonl') {
|
|
5
|
+
const root = isAbsolute(stateRoot) ? stateRoot : join(process.cwd(), stateRoot);
|
|
6
|
+
return join(root, 'gotry-state', filename);
|
|
7
|
+
}
|
|
8
|
+
function ensureDir(path) {
|
|
9
|
+
const dir = dirname(path);
|
|
10
|
+
if (!existsSync(dir)) mkdirSync(dir, {
|
|
11
|
+
recursive: true
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
let installed = false;
|
|
15
|
+
let logPath = null;
|
|
16
|
+
export function recordIncident(inc, stateRoot) {
|
|
17
|
+
const target = stateRoot ? resolveIncidentsPath(stateRoot) : logPath ?? resolveIncidentsPath('.');
|
|
18
|
+
try {
|
|
19
|
+
ensureDir(target);
|
|
20
|
+
appendFileSync(target, JSON.stringify(inc) + '\n', {
|
|
21
|
+
flag: 'a'
|
|
22
|
+
});
|
|
23
|
+
const fd = require('node:fs').openSync(target, 'a');
|
|
24
|
+
require('node:fs').fsyncSync(fd);
|
|
25
|
+
require('node:fs').closeSync(fd);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function installProcessGuards(stateRoot, labels) {
|
|
32
|
+
if (installed) return ()=>{};
|
|
33
|
+
installed = true;
|
|
34
|
+
logPath = resolveIncidentsPath(stateRoot);
|
|
35
|
+
const uncaughtHandler = (err, origin)=>{
|
|
36
|
+
recordIncident({
|
|
37
|
+
ts: new Date().toISOString(),
|
|
38
|
+
kind: 'uncaughtException',
|
|
39
|
+
message: (err?.message ?? String(err)).slice(0, 2000),
|
|
40
|
+
stack: (err?.stack ?? '').slice(0, 4000),
|
|
41
|
+
source: labels?.uncaughtException ?? String(origin)
|
|
42
|
+
}, stateRoot);
|
|
43
|
+
};
|
|
44
|
+
const rejectionHandler = (reason, promise)=>{
|
|
45
|
+
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
46
|
+
const stack = reason instanceof Error ? reason.stack ?? '' : '';
|
|
47
|
+
recordIncident({
|
|
48
|
+
ts: new Date().toISOString(),
|
|
49
|
+
kind: 'unhandledRejection',
|
|
50
|
+
message: msg.slice(0, 2000),
|
|
51
|
+
stack: stack.slice(0, 4000),
|
|
52
|
+
source: labels?.unhandledRejection ?? 'promise'
|
|
53
|
+
}, stateRoot);
|
|
54
|
+
};
|
|
55
|
+
process.on('uncaughtException', uncaughtHandler);
|
|
56
|
+
process.on('unhandledRejection', rejectionHandler);
|
|
57
|
+
return ()=>{
|
|
58
|
+
process.off('uncaughtException', uncaughtHandler);
|
|
59
|
+
process.off('unhandledRejection', rejectionHandler);
|
|
60
|
+
installed = false;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function guardToolExecute(name, stateRoot, execute) {
|
|
64
|
+
return async (args, exec)=>{
|
|
65
|
+
try {
|
|
66
|
+
return await execute(args, exec);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
69
|
+
recordIncident({
|
|
70
|
+
ts: new Date().toISOString(),
|
|
71
|
+
kind: 'tool_execute_error',
|
|
72
|
+
message: `${name}: ${err.message}`.slice(0, 2000),
|
|
73
|
+
stack: (err.stack ?? '').slice(0, 4000),
|
|
74
|
+
source: name
|
|
75
|
+
}, stateRoot);
|
|
76
|
+
const failure = {
|
|
77
|
+
ok: false,
|
|
78
|
+
summary: `gotry_${name} 内部错误(已隔离,会话继续): ${err.message.slice(0, 300)}`,
|
|
79
|
+
evidence: `[incident:tool_execute_error@${new Date().toISOString()}]`
|
|
80
|
+
};
|
|
81
|
+
return failure;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (import.meta.url === `file://${fileURLToPath(import.meta.url)}` && process.argv[2] === '--smoke') {
|
|
86
|
+
const stateRoot = process.argv[3] ?? '.';
|
|
87
|
+
installProcessGuards(stateRoot, {
|
|
88
|
+
uncaughtException: 'smoke',
|
|
89
|
+
unhandledRejection: 'smoke'
|
|
90
|
+
});
|
|
91
|
+
writeFileSync(resolveIncidentsPath(stateRoot) + '.smoke', 'ok');
|
|
92
|
+
setTimeout(()=>Promise.reject(new Error('intentional smoke-rejection')), 50);
|
|
93
|
+
setTimeout(()=>{
|
|
94
|
+
console.log('SMOKE OK');
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}, 500);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/capabilities/incident-log.ts
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const BASE = 'https://opensky-network.org/api';
|
|
2
|
+
async function fetchOpenSky(url, timeoutMs) {
|
|
3
|
+
const ctrl = new AbortController();
|
|
4
|
+
const timer = setTimeout(()=>ctrl.abort(), timeoutMs);
|
|
5
|
+
try {
|
|
6
|
+
const res = await fetch(url, {
|
|
7
|
+
signal: ctrl.signal,
|
|
8
|
+
headers: {
|
|
9
|
+
Accept: 'application/json',
|
|
10
|
+
'User-Agent': 'gotry/0.0.1-rc.3 (+https://github.com/Danceiny/gotry)'
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
if (!res.ok) return {
|
|
14
|
+
ok: false,
|
|
15
|
+
error: `opensky HTTP ${res.status}${res.status === 429 ? ' (rate limited, 400 credits/day anonymous)' : ''}`
|
|
16
|
+
};
|
|
17
|
+
return {
|
|
18
|
+
ok: true,
|
|
19
|
+
data: await res.json()
|
|
20
|
+
};
|
|
21
|
+
} catch (e) {
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
error: e.message.slice(0, 200)
|
|
25
|
+
};
|
|
26
|
+
} finally{
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const upper = (s)=>s.trim().toUpperCase();
|
|
31
|
+
export async function verifyFlight(query) {
|
|
32
|
+
const started = Date.now();
|
|
33
|
+
const ts = new Date().toISOString();
|
|
34
|
+
const cs = upper(query.callsign);
|
|
35
|
+
const airport = query.airport ? upper(query.airport) : null;
|
|
36
|
+
const r = await fetchOpenSky(`${BASE}/states/all`, query.timeoutMs ?? 10_000);
|
|
37
|
+
const latencyMs = Date.now() - started;
|
|
38
|
+
if (!r.ok) {
|
|
39
|
+
return {
|
|
40
|
+
verdict: 'unavailable',
|
|
41
|
+
callsign: cs,
|
|
42
|
+
airport,
|
|
43
|
+
sampleSize: 0,
|
|
44
|
+
hits: [],
|
|
45
|
+
evidence: `[实时API:opensky@error@${ts}] ${r.error}`,
|
|
46
|
+
via: 'opensky-error',
|
|
47
|
+
latencyMs,
|
|
48
|
+
error: r.error
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const rawStates = (r.data ?? {}).states ?? [];
|
|
52
|
+
const observed = rawStates.filter((row)=>(row[1] ?? '').trim().toUpperCase().includes(cs)).map((row)=>({
|
|
53
|
+
icao24: String(row[0] ?? ''),
|
|
54
|
+
callsign: (row[1] ?? '').trim() || null,
|
|
55
|
+
lastSeen: Number(row[4] ?? 0),
|
|
56
|
+
firstSeen: Number(row[4] ?? 0),
|
|
57
|
+
estArrivalAirport: airport
|
|
58
|
+
}));
|
|
59
|
+
const verdict = observed.length > 0 ? 'observed' : 'not_observed';
|
|
60
|
+
const evidence = observed.length > 0 ? `[实时API:opensky@${ts}] ✅ 全球 ADS-B 当前观测命中 ${cs} (${observed.length} 架)` : `[实时API:opensky@${ts}] ○ 当前 ADS-B 全球观测列表未见 ${cs}(ADS-B 覆盖有限,不作否定结论)`;
|
|
61
|
+
return {
|
|
62
|
+
verdict,
|
|
63
|
+
callsign: cs,
|
|
64
|
+
airport,
|
|
65
|
+
sampleSize: rawStates.length,
|
|
66
|
+
hits: observed,
|
|
67
|
+
evidence,
|
|
68
|
+
via: 'opensky',
|
|
69
|
+
latencyMs
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/capabilities/opensky.ts
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
export function templateKey(site, action, params) {
|
|
5
|
+
const val = (k)=>k === 'date' ? '%date%' : (params[k] ?? '').trim();
|
|
6
|
+
const head = [
|
|
7
|
+
'from',
|
|
8
|
+
'to',
|
|
9
|
+
'date'
|
|
10
|
+
].filter((k)=>k in params).map(val);
|
|
11
|
+
const tail = Object.keys(params).filter((k)=>![
|
|
12
|
+
'from',
|
|
13
|
+
'to',
|
|
14
|
+
'date'
|
|
15
|
+
].includes(k)).sort().map(val);
|
|
16
|
+
return `${site}:${action}:${[
|
|
17
|
+
...head,
|
|
18
|
+
...tail
|
|
19
|
+
].join('|')}`;
|
|
20
|
+
}
|
|
21
|
+
export function fingerprint(shapeText) {
|
|
22
|
+
return createHash('sha256').update(shapeText).digest('hex').slice(0, 16);
|
|
23
|
+
}
|
|
24
|
+
export class ActionCache {
|
|
25
|
+
filePath;
|
|
26
|
+
entries = new Map();
|
|
27
|
+
loaded = false;
|
|
28
|
+
ttlMs;
|
|
29
|
+
maxPerSite;
|
|
30
|
+
now;
|
|
31
|
+
constructor(filePath, opts = {}){
|
|
32
|
+
this.filePath = filePath;
|
|
33
|
+
this.ttlMs = opts.ttlMs ?? 48 * 3600_000;
|
|
34
|
+
this.maxPerSite = opts.maxEntriesPerSite ?? 50;
|
|
35
|
+
this.now = opts.now ?? (()=>new Date());
|
|
36
|
+
}
|
|
37
|
+
load() {
|
|
38
|
+
if (this.loaded) return;
|
|
39
|
+
this.loaded = true;
|
|
40
|
+
try {
|
|
41
|
+
if (existsSync(this.filePath)) {
|
|
42
|
+
const raw = JSON.parse(readFileSync(this.filePath, 'utf8'));
|
|
43
|
+
for (const e of raw){
|
|
44
|
+
if (e?.key && e.domFingerprint && e.locator) this.entries.set(e.key, e);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
persist() {
|
|
50
|
+
try {
|
|
51
|
+
mkdirSync(dirname(this.filePath), {
|
|
52
|
+
recursive: true
|
|
53
|
+
});
|
|
54
|
+
const tmp = this.filePath + '.tmp';
|
|
55
|
+
writeFileSync(tmp, JSON.stringify([
|
|
56
|
+
...this.entries.values()
|
|
57
|
+
], null, 0), 'utf8');
|
|
58
|
+
writeFileSync(this.filePath, readFileSync(tmp, 'utf8'), 'utf8');
|
|
59
|
+
try {
|
|
60
|
+
require('node:fs').rmSync(tmp);
|
|
61
|
+
} catch {}
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
siteEntries(site) {
|
|
65
|
+
return [
|
|
66
|
+
...this.entries.values()
|
|
67
|
+
].filter((e)=>e.key.startsWith(site + ':'));
|
|
68
|
+
}
|
|
69
|
+
lookup(key, domFingerprint, site) {
|
|
70
|
+
this.load();
|
|
71
|
+
const e = this.entries.get(key);
|
|
72
|
+
if (!e) return null;
|
|
73
|
+
if (this.now().getTime() - Date.parse(e.lastHitAt || e.createdAt) > this.ttlMs) {
|
|
74
|
+
this.entries.delete(key);
|
|
75
|
+
this.persist();
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
if (e.domFingerprint !== domFingerprint) return null;
|
|
79
|
+
e.hits += 1;
|
|
80
|
+
e.lastHitAt = this.now().toISOString();
|
|
81
|
+
this.persist();
|
|
82
|
+
void site;
|
|
83
|
+
return e;
|
|
84
|
+
}
|
|
85
|
+
record(key, domFingerprint, locator, site) {
|
|
86
|
+
this.load();
|
|
87
|
+
const ts = this.now().toISOString();
|
|
88
|
+
const prev = this.entries.get(key);
|
|
89
|
+
this.entries.set(key, {
|
|
90
|
+
key,
|
|
91
|
+
domFingerprint,
|
|
92
|
+
locator,
|
|
93
|
+
createdAt: prev?.createdAt ?? ts,
|
|
94
|
+
lastHitAt: ts,
|
|
95
|
+
hits: prev?.hits ?? 0
|
|
96
|
+
});
|
|
97
|
+
const siteList = this.siteEntries(site);
|
|
98
|
+
if (siteList.length > this.maxPerSite) {
|
|
99
|
+
const victim = siteList.sort((a, b)=>Date.parse(a.lastHitAt) - Date.parse(b.lastHitAt))[0];
|
|
100
|
+
if (victim) this.entries.delete(victim.key);
|
|
101
|
+
}
|
|
102
|
+
this.persist();
|
|
103
|
+
return this.entries.get(key);
|
|
104
|
+
}
|
|
105
|
+
invalidate(key) {
|
|
106
|
+
this.load();
|
|
107
|
+
this.entries.delete(key);
|
|
108
|
+
this.persist();
|
|
109
|
+
}
|
|
110
|
+
size(site) {
|
|
111
|
+
this.load();
|
|
112
|
+
return site ? this.siteEntries(site).length : this.entries.size;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export function actionCachePath(stateRoot) {
|
|
116
|
+
return join(stateRoot, 'session-action-cache.json');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/capabilities/session/action-cache.ts
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const CITY_CODES = {
|
|
2
|
+
上海: 'sha',
|
|
3
|
+
北京: 'bjs',
|
|
4
|
+
广州: 'can',
|
|
5
|
+
深圳: 'szx',
|
|
6
|
+
成都: 'ctu',
|
|
7
|
+
昆明: 'kmg',
|
|
8
|
+
大理: 'dlu',
|
|
9
|
+
丽江: 'ljg',
|
|
10
|
+
西安: 'sia',
|
|
11
|
+
杭州: 'hgh',
|
|
12
|
+
三亚: 'syx',
|
|
13
|
+
厦门: 'xmn',
|
|
14
|
+
重庆: 'cqg',
|
|
15
|
+
青岛: 'taa',
|
|
16
|
+
长沙: 'csx',
|
|
17
|
+
武汉: 'wuh',
|
|
18
|
+
南京: 'nkg',
|
|
19
|
+
郑州: 'cgo',
|
|
20
|
+
贵阳: 'kwe',
|
|
21
|
+
桂林: 'kwl',
|
|
22
|
+
西双版纳: 'jhg',
|
|
23
|
+
香格里拉: 'dig'
|
|
24
|
+
};
|
|
25
|
+
export function buildEntryUrl(from, to, depDate) {
|
|
26
|
+
const unresolved = [];
|
|
27
|
+
const fromCode = CITY_CODES[from];
|
|
28
|
+
const toCode = CITY_CODES[to];
|
|
29
|
+
if (!fromCode) unresolved.push(from);
|
|
30
|
+
if (!toCode) unresolved.push(to);
|
|
31
|
+
if (unresolved.length > 0 || !/^\d{4}-\d{2}-\d{2}$/.test(depDate)) {
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
unresolved: unresolved.length > 0 ? unresolved : [
|
|
35
|
+
depDate
|
|
36
|
+
]
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
ok: true,
|
|
41
|
+
url: `https://flights.ctrip.com/online/list/oneway-${fromCode}-${toCode}?depdate=${depDate}`
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export const NETWORK_HINTS = [
|
|
45
|
+
/search\/api\/search\/batchSearch/
|
|
46
|
+
];
|
|
47
|
+
export const LOGIN_COOKIE_NAMES = [
|
|
48
|
+
'cticket',
|
|
49
|
+
'uid',
|
|
50
|
+
'uname',
|
|
51
|
+
'passport'
|
|
52
|
+
];
|
|
53
|
+
export const SITE_DOMAIN = '.ctrip.com';
|
|
54
|
+
export function parseBatchSearch(body) {
|
|
55
|
+
let raw;
|
|
56
|
+
try {
|
|
57
|
+
raw = JSON.parse(body);
|
|
58
|
+
} catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
const out = [];
|
|
62
|
+
for (const it of raw.data?.flightItineraryList ?? []){
|
|
63
|
+
const seg = it.flightSegments?.[0];
|
|
64
|
+
const fl = seg?.flightList?.[0];
|
|
65
|
+
if (!fl?.flightNo || !fl.departureDateTime) continue;
|
|
66
|
+
const prices = (it.priceList ?? []).map((p)=>Number(p.adultPrice ?? 0)).filter((n)=>n > 0);
|
|
67
|
+
out.push({
|
|
68
|
+
flightNo: fl.flightNo,
|
|
69
|
+
airline: seg?.airlineName ?? '',
|
|
70
|
+
depDateTime: fl.departureDateTime ?? '',
|
|
71
|
+
arrDateTime: fl.arrivalDateTime ?? '',
|
|
72
|
+
depAirport: fl.departureAirportName ?? '',
|
|
73
|
+
arrAirport: fl.arrivalAirportName ?? '',
|
|
74
|
+
durationMin: Number(seg?.duration ?? 0) || 0,
|
|
75
|
+
price: prices.length > 0 ? Math.min(...prices) : 0,
|
|
76
|
+
aircraft: fl.aircraftName
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/capabilities/session/adapters/ctrip-flight.ts
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const CITY_SLUGS = {
|
|
2
|
+
上海: 'shanghai',
|
|
3
|
+
北京: 'beijing',
|
|
4
|
+
广州: 'guangzhou',
|
|
5
|
+
深圳: 'shenzhen',
|
|
6
|
+
成都: 'chengdu',
|
|
7
|
+
昆明: 'kunming',
|
|
8
|
+
大理: 'dali',
|
|
9
|
+
丽江: 'lijiang',
|
|
10
|
+
西安: 'xian',
|
|
11
|
+
杭州: 'hangzhou',
|
|
12
|
+
三亚: 'sanya',
|
|
13
|
+
厦门: 'xiamen',
|
|
14
|
+
重庆: 'chongqing',
|
|
15
|
+
青岛: 'qingdao',
|
|
16
|
+
长沙: 'changsha',
|
|
17
|
+
武汉: 'wuhan',
|
|
18
|
+
南京: 'nanjing',
|
|
19
|
+
郑州: 'zhengzhou',
|
|
20
|
+
贵阳: 'guiyang',
|
|
21
|
+
桂林: 'guilin'
|
|
22
|
+
};
|
|
23
|
+
export const SITE_DOMAIN = 'meituan.com';
|
|
24
|
+
export const LOGIN_COOKIE_NAMES = [
|
|
25
|
+
'lt',
|
|
26
|
+
'u',
|
|
27
|
+
'token',
|
|
28
|
+
'n'
|
|
29
|
+
];
|
|
30
|
+
export function buildMeituanEntry(city, vertical, keyword) {
|
|
31
|
+
const slug = CITY_SLUGS[city];
|
|
32
|
+
if (!slug) return {
|
|
33
|
+
ok: false,
|
|
34
|
+
unresolved: [
|
|
35
|
+
city
|
|
36
|
+
]
|
|
37
|
+
};
|
|
38
|
+
const base = vertical === 'hotel' ? `https://hotel.meituan.com/${slug}/` : `https://minsu.meituan.com/${slug}/`;
|
|
39
|
+
return {
|
|
40
|
+
ok: true,
|
|
41
|
+
url: keyword ? base + `?q=${encodeURIComponent(keyword)}` : base
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export const NETWORK_HINTS = [];
|
|
45
|
+
export function parseMeituanSearch(_body) {
|
|
46
|
+
void _body;
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
export function extractListings(entries, opts = {}) {
|
|
50
|
+
const out = [];
|
|
51
|
+
const roles = new Set([
|
|
52
|
+
'heading',
|
|
53
|
+
'listitem',
|
|
54
|
+
'link',
|
|
55
|
+
'article'
|
|
56
|
+
]);
|
|
57
|
+
for (const e of entries){
|
|
58
|
+
if (out.length >= (opts.max ?? 30)) break;
|
|
59
|
+
if (!roles.has(e.role) || !e.name) continue;
|
|
60
|
+
const pm = /¥\s*(\d+)/.exec(e.name);
|
|
61
|
+
if (!pm) continue;
|
|
62
|
+
const name = e.name.replace(/¥\s*\d+.*$/, '').trim().slice(0, 80);
|
|
63
|
+
if (!name) continue;
|
|
64
|
+
out.push({
|
|
65
|
+
name,
|
|
66
|
+
price: Number(pm[1])
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/capabilities/session/adapters/meituan-local.ts
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function extractA11yEntries(snapshotText, opts = {}) {
|
|
2
|
+
const cap = opts.maxEntries ?? 500;
|
|
3
|
+
const out = [];
|
|
4
|
+
const lines = snapshotText.split('\n');
|
|
5
|
+
for(let i = 0; i < lines.length && out.length < cap; i++){
|
|
6
|
+
const m = /^[\s*-]*\s*([a-zA-Z][\w-]*)(?:\s+"((?:[^"\\]|\\.)*)")?/.exec(lines[i] ?? '');
|
|
7
|
+
if (!m) continue;
|
|
8
|
+
const name = (m[2] ?? '').replace(/\\"/g, '"').slice(0, 200);
|
|
9
|
+
out.push({
|
|
10
|
+
role: m[1].toLowerCase(),
|
|
11
|
+
name,
|
|
12
|
+
line: i
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
export function filterSubmitEntries(entries, isSubmit) {
|
|
18
|
+
const interactive = new Set([
|
|
19
|
+
'button',
|
|
20
|
+
'link',
|
|
21
|
+
'menuitem',
|
|
22
|
+
'option'
|
|
23
|
+
]);
|
|
24
|
+
return entries.filter((e)=>!(interactive.has(e.role) && isSubmit(e.name)));
|
|
25
|
+
}
|
|
26
|
+
export function nameAffinity(a, b) {
|
|
27
|
+
const x = a.trim();
|
|
28
|
+
const y = b.trim();
|
|
29
|
+
if (!x || !y) return 0;
|
|
30
|
+
if (x === y) return 1;
|
|
31
|
+
if (x.includes(y) || y.includes(x)) return 0.8;
|
|
32
|
+
const sa = new Set(x);
|
|
33
|
+
const sb = new Set(y);
|
|
34
|
+
let common = 0;
|
|
35
|
+
for (const ch of sa)if (sb.has(ch)) common += 1;
|
|
36
|
+
return common / Math.max(sa.size, sb.size);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/capabilities/session/extract.ts
|