@danceiny/gotry 0.0.1-rc.5 → 0.0.1-rc.7
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 +1 -1
- package/bin/gotry-inner.js +73 -32
- package/cordis.gotry-patch.yml +2 -2
- package/data/flights_2026.json +215 -0
- package/data/golden_erhai.json +92 -0
- package/data/golden_trip_2026.json +75 -0
- package/data/hotels_2026.json +64 -0
- package/data/openflights-skeleton.json +895 -0
- package/data/yunnan-pack.json +201 -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/hbcli.js +138 -0
- package/dist/capabilities/incident-log.js +99 -0
- package/dist/capabilities/opensky.js +74 -0
- package/dist/capabilities/weather.js +180 -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 +18 -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 +85 -0
- package/dist/scripts/incident-tests.js +96 -0
- package/dist/scripts/journey-tests.js +45 -0
- package/dist/scripts/opensky-check.js +20 -0
- package/dist/scripts/opensky-tests.js +52 -0
- package/dist/scripts/probe-poi-tests.js +79 -0
- package/dist/scripts/replay-async.js +44 -0
- package/dist/scripts/replay-real.js +38 -0
- package/dist/scripts/replay.js +44 -0
- package/dist/scripts/skeleton-check.js +35 -0
- package/dist/scripts/skeleton-integration-test.js +22 -0
- package/dist/scripts/smoke.js +90 -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/contracts.js +64 -0
- package/dist/src/dsh-llm.js +154 -0
- package/dist/src/engine.js +331 -0
- package/dist/src/index.js +760 -0
- package/dist/src/journey.js +147 -0
- package/dist/src/loop.js +296 -0
- package/dist/src/mock-llm.js +98 -0
- package/dist/src/model.js +134 -0
- package/dist/src/unified.js +536 -0
- package/package.json +3 -1
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
function sh(cmd, args, opts) {
|
|
3
|
+
const ctrl = new AbortController();
|
|
4
|
+
const timer = setTimeout(()=>{
|
|
5
|
+
ctrl.abort();
|
|
6
|
+
}, opts.timeoutMs);
|
|
7
|
+
let child = null;
|
|
8
|
+
let killTimer = null;
|
|
9
|
+
return new Promise((resolve)=>{
|
|
10
|
+
child = spawn(cmd, args, {
|
|
11
|
+
env: opts.env,
|
|
12
|
+
cwd: process.cwd(),
|
|
13
|
+
signal: ctrl.signal
|
|
14
|
+
});
|
|
15
|
+
let stdout = '';
|
|
16
|
+
let stderr = '';
|
|
17
|
+
let error;
|
|
18
|
+
let killed = false;
|
|
19
|
+
child.stdout.on('data', (d)=>{
|
|
20
|
+
stdout += d.toString();
|
|
21
|
+
});
|
|
22
|
+
child.stderr.on('data', (d)=>{
|
|
23
|
+
stderr += d.toString();
|
|
24
|
+
});
|
|
25
|
+
child.on('error', (e)=>{
|
|
26
|
+
error = e.message.slice(0, 200);
|
|
27
|
+
});
|
|
28
|
+
child.on('close', (code)=>{
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
if (killTimer) clearTimeout(killTimer);
|
|
31
|
+
resolve({
|
|
32
|
+
code: killed ? -2 : code ?? -1,
|
|
33
|
+
stdout,
|
|
34
|
+
stderr,
|
|
35
|
+
error
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
ctrl.signal.addEventListener('abort', ()=>{
|
|
39
|
+
if (!child) return;
|
|
40
|
+
try {
|
|
41
|
+
child.kill('SIGTERM');
|
|
42
|
+
} catch {}
|
|
43
|
+
killTimer = setTimeout(()=>{
|
|
44
|
+
try {
|
|
45
|
+
child?.kill('SIGKILL');
|
|
46
|
+
} catch {}
|
|
47
|
+
}, 500);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const upperFirst = (s)=>s ? s[0].toUpperCase() + s.slice(1) : s;
|
|
52
|
+
const lowerFirst = (s)=>s ? s[0].toLowerCase() + s.slice(1) : s;
|
|
53
|
+
export async function anythingSearch(q) {
|
|
54
|
+
const started = Date.now();
|
|
55
|
+
const ts = new Date().toISOString();
|
|
56
|
+
const kw = (q.keyword ?? '').trim();
|
|
57
|
+
if (!kw) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
via: 'hbcli-anything-error',
|
|
61
|
+
evidence: `[实时API:hbcli-anything@error@${ts}] keyword empty`,
|
|
62
|
+
latencyMs: Date.now() - started,
|
|
63
|
+
verdict: 'error',
|
|
64
|
+
error: 'keyword is required'
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const bin = q.hbcliBin ?? 'hbcli';
|
|
68
|
+
const args = [
|
|
69
|
+
bin,
|
|
70
|
+
'search',
|
|
71
|
+
'anything',
|
|
72
|
+
kw
|
|
73
|
+
];
|
|
74
|
+
if (q.contentType) args.push('--content-type', q.contentType);
|
|
75
|
+
if (q.parentDestinationId !== undefined) {
|
|
76
|
+
args.push('--parent-destination-id', String(q.parentDestinationId));
|
|
77
|
+
}
|
|
78
|
+
const r = await sh(bin, args.slice(1), {
|
|
79
|
+
timeoutMs: q.timeoutMs ?? 12_000,
|
|
80
|
+
env: process.env
|
|
81
|
+
});
|
|
82
|
+
const latencyMs = Date.now() - started;
|
|
83
|
+
if (r.error || r.code !== 0) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
via: 'hbcli-anything-error',
|
|
87
|
+
evidence: `[实时API:hbcli-anything@error@${ts}] ${r.error ?? `exit ${r.code}`}`,
|
|
88
|
+
latencyMs,
|
|
89
|
+
verdict: 'error',
|
|
90
|
+
error: r.error ?? `${r.stderr.slice(0, 200)} (exit ${r.code})`
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
let data;
|
|
94
|
+
try {
|
|
95
|
+
const jStart = r.stdout.indexOf('{') >= 0 ? r.stdout.indexOf('{') : r.stdout.indexOf('[');
|
|
96
|
+
const json = jStart >= 0 ? r.stdout.slice(jStart) : r.stdout;
|
|
97
|
+
data = JSON.parse(json);
|
|
98
|
+
} catch {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
via: 'hbcli-anything-error',
|
|
102
|
+
evidence: `[实时API:hbcli-anything@error@${ts}] parse failed`,
|
|
103
|
+
latencyMs,
|
|
104
|
+
verdict: 'error',
|
|
105
|
+
error: 'failed to parse hbcli output as JSON'
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const rawItems = data.candidates ?? [];
|
|
109
|
+
const hits = rawItems.map((it)=>{
|
|
110
|
+
const region = it.region ?? {};
|
|
111
|
+
const hotel = it.hotel ?? {};
|
|
112
|
+
const isHotel = it.type === 'hotel' || it.type === 'SupplierHotelList' || Boolean(hotel.id);
|
|
113
|
+
return {
|
|
114
|
+
type: isHotel ? 'hotel' : it.type === 'place' || it.type === 'City' ? lowerFirst(it.type) : 'city',
|
|
115
|
+
name: it.name ?? region.name_en ?? region.name ?? hotel.name ?? '?',
|
|
116
|
+
score: it.score ?? region.typeScore,
|
|
117
|
+
latitude: hotel.latitude ?? region.latitude,
|
|
118
|
+
longitude: hotel.longitude ?? region.longitude,
|
|
119
|
+
destinationId: region.id,
|
|
120
|
+
hotelId: hotel.id
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
const verdict = hits.length > 0 ? 'hit' : 'miss';
|
|
124
|
+
return {
|
|
125
|
+
ok: true,
|
|
126
|
+
via: 'hbcli-anything',
|
|
127
|
+
evidence: `[实时API:hbcli-anything@${ts}] ${hits.length}/${rawItems.length} candidates`,
|
|
128
|
+
latencyMs,
|
|
129
|
+
verdict,
|
|
130
|
+
hits,
|
|
131
|
+
totalCandidates: rawItems.length
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export const _ = {
|
|
135
|
+
upperFirst
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/capabilities/anything.ts
|
|
@@ -0,0 +1,138 @@
|
|
|
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
|
+
];
|
|
96
|
+
if (query.destination) hbArgs.push('--destination', query.destination);
|
|
97
|
+
if (query.checkIn) hbArgs.push('--check-in', query.checkIn);
|
|
98
|
+
if (query.checkOut) hbArgs.push('--check-out', query.checkOut);
|
|
99
|
+
if (query.adults) hbArgs.push('--adults', String(query.adults));
|
|
100
|
+
const live = await callHbcliJson(hbArgs, opts);
|
|
101
|
+
if (live.via === 'hbcli-realtime') {
|
|
102
|
+
return {
|
|
103
|
+
...live,
|
|
104
|
+
hotels: live.result,
|
|
105
|
+
summary: `${query.destination}:hbcli 实时返回`
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const fallback = opts.fallbackPath;
|
|
109
|
+
if (fallback) {
|
|
110
|
+
try {
|
|
111
|
+
const { readFile } = await import('node:fs/promises');
|
|
112
|
+
const pack = JSON.parse(await readFile(fallback, 'utf-8'));
|
|
113
|
+
return {
|
|
114
|
+
...live,
|
|
115
|
+
hotels: pack,
|
|
116
|
+
summary: `${query.destination}:hbcli 不可用(${live.error ?? live.via}),降级到静态包`
|
|
117
|
+
};
|
|
118
|
+
} catch {}
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
...live,
|
|
122
|
+
summary: `${query.destination}:hbcli 不可用且无静态包(仅返回错误)`
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export async function listDestinations(opts = {}) {
|
|
126
|
+
const r = await callHbcliJson([
|
|
127
|
+
'search',
|
|
128
|
+
'destinations',
|
|
129
|
+
'--json'
|
|
130
|
+
], opts);
|
|
131
|
+
return {
|
|
132
|
+
...r,
|
|
133
|
+
destinations: r.result
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/capabilities/hbcli.ts
|
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
summary: `gotry_${name} 内部错误(已隔离,会话继续): ${err.message.slice(0, 300)}`,
|
|
79
|
+
evidence: `[incident:tool_execute_error@${new Date().toISOString()}]`
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (import.meta.url === `file://${fileURLToPath(import.meta.url)}` && process.argv[2] === '--smoke') {
|
|
85
|
+
const stateRoot = process.argv[3] ?? '.';
|
|
86
|
+
installProcessGuards(stateRoot, {
|
|
87
|
+
uncaughtException: 'smoke',
|
|
88
|
+
unhandledRejection: 'smoke'
|
|
89
|
+
});
|
|
90
|
+
writeFileSync(resolveIncidentsPath(stateRoot) + '.smoke', 'ok');
|
|
91
|
+
setTimeout(()=>Promise.reject(new Error('intentional smoke-rejection')), 50);
|
|
92
|
+
setTimeout(()=>{
|
|
93
|
+
console.log('SMOKE OK');
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}, 500);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
//# 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,180 @@
|
|
|
1
|
+
const FORECAST_BASE = 'https://api.open-meteo.com/v1/forecast';
|
|
2
|
+
const ARCHIVE_BASE = 'https://archive-api.open-meteo.com/v1/archive';
|
|
3
|
+
const GEOCODE_BASE = 'https://geocoding-api.open-meteo.com/v1/search';
|
|
4
|
+
const WMO_ZH = {
|
|
5
|
+
0: '晴',
|
|
6
|
+
1: '大致晴',
|
|
7
|
+
2: '多云',
|
|
8
|
+
3: '阴',
|
|
9
|
+
45: '雾',
|
|
10
|
+
48: '冻雾',
|
|
11
|
+
51: '毛毛雨',
|
|
12
|
+
53: '毛毛雨',
|
|
13
|
+
55: '密集毛毛雨',
|
|
14
|
+
61: '小雨',
|
|
15
|
+
63: '中雨',
|
|
16
|
+
65: '大雨',
|
|
17
|
+
66: '冻雨',
|
|
18
|
+
67: '强冻雨',
|
|
19
|
+
71: '小雪',
|
|
20
|
+
73: '中雪',
|
|
21
|
+
75: '大雪',
|
|
22
|
+
77: '雪粒',
|
|
23
|
+
80: '阵雨',
|
|
24
|
+
81: '强阵雨',
|
|
25
|
+
82: '暴雨',
|
|
26
|
+
85: '阵雪',
|
|
27
|
+
86: '强阵雪',
|
|
28
|
+
95: '雷暴',
|
|
29
|
+
96: '雷暴伴小冰雹',
|
|
30
|
+
99: '雷暴伴大冰雹'
|
|
31
|
+
};
|
|
32
|
+
export function wmoLabel(code) {
|
|
33
|
+
return WMO_ZH[code] ?? `天气码${code}`;
|
|
34
|
+
}
|
|
35
|
+
async function fetchJson(url, timeoutMs) {
|
|
36
|
+
const ctrl = new AbortController();
|
|
37
|
+
const timer = setTimeout(()=>ctrl.abort(), timeoutMs);
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch(url, {
|
|
40
|
+
signal: ctrl.signal,
|
|
41
|
+
headers: {
|
|
42
|
+
Accept: 'application/json'
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
if (!res.ok) return {
|
|
46
|
+
ok: false,
|
|
47
|
+
error: `HTTP ${res.status}`
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
ok: true,
|
|
51
|
+
data: await res.json()
|
|
52
|
+
};
|
|
53
|
+
} catch (e) {
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
error: e.message.slice(0, 200)
|
|
57
|
+
};
|
|
58
|
+
} finally{
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export async function geocodePlace(name, opts = {}) {
|
|
63
|
+
const ts = new Date().toISOString();
|
|
64
|
+
const count = opts.count ?? 5;
|
|
65
|
+
const url = `${GEOCODE_BASE}?name=${encodeURIComponent(name)}&count=${count}&language=zh&format=json`;
|
|
66
|
+
const r = await fetchJson(url, opts.timeoutMs ?? 15_000);
|
|
67
|
+
if (!r.ok) {
|
|
68
|
+
return {
|
|
69
|
+
ok: false,
|
|
70
|
+
evidence: `[实时API:open-meteo-geo@error@${ts}]`,
|
|
71
|
+
results: [],
|
|
72
|
+
error: r.error
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const results = (r.data?.results ?? []).map((item)=>{
|
|
76
|
+
const it = item;
|
|
77
|
+
return {
|
|
78
|
+
name: String(it['name'] ?? ''),
|
|
79
|
+
latitude: Number(it['latitude']),
|
|
80
|
+
longitude: Number(it['longitude']),
|
|
81
|
+
country: it['country'] ? String(it['country']) : undefined,
|
|
82
|
+
admin1: it['admin1'] ? String(it['admin1']) : undefined
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
return {
|
|
86
|
+
ok: true,
|
|
87
|
+
evidence: `[实时API:open-meteo-geo@${ts}]`,
|
|
88
|
+
results
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export async function getForecast(point, opts = {}) {
|
|
92
|
+
const started = Date.now();
|
|
93
|
+
const ts = new Date().toISOString();
|
|
94
|
+
const days = Math.min(opts.days ?? 7, 16);
|
|
95
|
+
const url = `${FORECAST_BASE}?latitude=${point.latitude}&longitude=${point.longitude}` + `&daily=temperature_2m_max,temperature_2m_min,precipitation_probability_max,weathercode` + `&forecast_days=${days}&timezone=auto`;
|
|
96
|
+
const r = await fetchJson(url, opts.timeoutMs ?? 15_000);
|
|
97
|
+
const latencyMs = Date.now() - started;
|
|
98
|
+
if (!r.ok) {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
via: 'open-meteo-error',
|
|
102
|
+
evidence: `[实时API:open-meteo@error@${ts}]`,
|
|
103
|
+
latencyMs,
|
|
104
|
+
error: r.error
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const daily = r.data.daily;
|
|
108
|
+
if (!daily) {
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
via: 'open-meteo-error',
|
|
112
|
+
evidence: `[实时API:open-meteo@error@${ts}]`,
|
|
113
|
+
latencyMs,
|
|
114
|
+
error: 'empty daily payload'
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const rows = daily.time.map((date, i)=>({
|
|
118
|
+
date,
|
|
119
|
+
tempMaxC: daily.temperature_2m_max[i] ?? NaN,
|
|
120
|
+
tempMinC: daily.temperature_2m_min[i] ?? NaN,
|
|
121
|
+
precipProbMaxPct: daily.precipitation_probability_max?.[i] ?? null,
|
|
122
|
+
weatherCode: daily.weathercode[i] ?? -1
|
|
123
|
+
}));
|
|
124
|
+
return {
|
|
125
|
+
ok: true,
|
|
126
|
+
via: 'open-meteo',
|
|
127
|
+
evidence: `[实时API:open-meteo@${ts}]`,
|
|
128
|
+
latencyMs,
|
|
129
|
+
timezone: r.data.timezone,
|
|
130
|
+
daily: rows
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export async function getClimate(point, month, opts = {}) {
|
|
134
|
+
const started = Date.now();
|
|
135
|
+
const ts = new Date().toISOString();
|
|
136
|
+
const year = opts.year ?? new Date().getFullYear() - 1;
|
|
137
|
+
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
138
|
+
const start = `${year}-${String(month).padStart(2, '0')}-01`;
|
|
139
|
+
const end = `${year}-${String(month).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
|
|
140
|
+
const url = `${ARCHIVE_BASE}?latitude=${point.latitude}&longitude=${point.longitude}` + `&start_date=${start}&end_date=${end}` + `&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,weathercode&timezone=auto`;
|
|
141
|
+
const r = await fetchJson(url, opts.timeoutMs ?? 15_000);
|
|
142
|
+
const latencyMs = Date.now() - started;
|
|
143
|
+
if (!r.ok) {
|
|
144
|
+
return {
|
|
145
|
+
ok: false,
|
|
146
|
+
via: 'open-meteo-error',
|
|
147
|
+
evidence: `[实时API:open-meteo-climate@error@${ts}]`,
|
|
148
|
+
latencyMs,
|
|
149
|
+
error: r.error
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const daily = r.data.daily;
|
|
153
|
+
if (!daily) {
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
via: 'open-meteo-error',
|
|
157
|
+
evidence: `[实时API:open-meteo-climate@error@${ts}]`,
|
|
158
|
+
latencyMs,
|
|
159
|
+
error: 'empty daily payload'
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const rows = daily.time.map((date, i)=>({
|
|
163
|
+
date,
|
|
164
|
+
tempMaxC: daily.temperature_2m_max[i] ?? NaN,
|
|
165
|
+
tempMinC: daily.temperature_2m_min[i] ?? NaN,
|
|
166
|
+
precipProbMaxPct: null,
|
|
167
|
+
weatherCode: daily.weathercode[i] ?? -1
|
|
168
|
+
}));
|
|
169
|
+
return {
|
|
170
|
+
ok: true,
|
|
171
|
+
via: 'open-meteo',
|
|
172
|
+
evidence: `[实时API:open-meteo-climate@${ts}]`,
|
|
173
|
+
latencyMs,
|
|
174
|
+
timezone: r.data.timezone,
|
|
175
|
+
daily: rows
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/capabilities/weather.ts
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { videoSubtitle, githubSearch } from '../capabilities/agent-reach-deep.js';
|
|
3
|
+
{
|
|
4
|
+
const r = await videoSubtitle({
|
|
5
|
+
url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
|
6
|
+
timeoutMs: 15_000
|
|
7
|
+
});
|
|
8
|
+
assert.ok([
|
|
9
|
+
'found',
|
|
10
|
+
'not-installed',
|
|
11
|
+
'error',
|
|
12
|
+
'timeout'
|
|
13
|
+
].includes(r.verdict), `verdict 三值之一: ${r.verdict}`);
|
|
14
|
+
assert.ok(r.evidence.includes('[agent-reach:yt-dlp@'), '证据链前缀');
|
|
15
|
+
console.log(`1. yt-dlp → ${r.verdict} (${r.latencyMs}ms) OK`);
|
|
16
|
+
}{
|
|
17
|
+
const r = await githubSearch({
|
|
18
|
+
query: 'agent-reach',
|
|
19
|
+
limit: 3
|
|
20
|
+
});
|
|
21
|
+
assert.ok([
|
|
22
|
+
'found',
|
|
23
|
+
'not-installed',
|
|
24
|
+
'error',
|
|
25
|
+
'timeout'
|
|
26
|
+
].includes(r.verdict));
|
|
27
|
+
if (r.verdict === 'found') {
|
|
28
|
+
assert.ok((r.repos?.length ?? 0) > 0, 'found 应有 repos');
|
|
29
|
+
const top = r.repos[0];
|
|
30
|
+
console.log(`2. gh search → found (top: ${top.name} ★${top.stars}) OK`);
|
|
31
|
+
} else {
|
|
32
|
+
console.log(`2. gh search → ${r.verdict} (${r.stderr?.slice(0, 60)}) OK`);
|
|
33
|
+
}
|
|
34
|
+
}{
|
|
35
|
+
const r = await githubSearch({
|
|
36
|
+
query: 'hello'
|
|
37
|
+
});
|
|
38
|
+
assert.match(r.evidence, /\[agent-reach:gh@/, 'gh 证据链');
|
|
39
|
+
console.log('3. 证据链格式 OK');
|
|
40
|
+
}{
|
|
41
|
+
const r = await githubSearch({
|
|
42
|
+
query: 'hello',
|
|
43
|
+
timeoutMs: 1
|
|
44
|
+
});
|
|
45
|
+
assert.ok([
|
|
46
|
+
'timeout',
|
|
47
|
+
'error',
|
|
48
|
+
'not-installed'
|
|
49
|
+
].includes(r.verdict));
|
|
50
|
+
console.log(`4. 超时 → ${r.verdict} OK`);
|
|
51
|
+
}console.log('\nAGENT-REACH DEEP TESTS: 4/4 OK(yt-dlp/gh 可选工具,不装则降级)');
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/agent-reach-deep-tests.ts
|