@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.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/bin/gotry-inner.js +73 -32
  3. package/cordis.gotry-patch.yml +2 -2
  4. package/data/flights_2026.json +215 -0
  5. package/data/golden_erhai.json +92 -0
  6. package/data/golden_trip_2026.json +75 -0
  7. package/data/hotels_2026.json +64 -0
  8. package/data/openflights-skeleton.json +895 -0
  9. package/data/yunnan-pack.json +201 -0
  10. package/dist/capabilities/agent-reach-deep.js +172 -0
  11. package/dist/capabilities/agent-reach.js +204 -0
  12. package/dist/capabilities/anything.js +139 -0
  13. package/dist/capabilities/hbcli.js +138 -0
  14. package/dist/capabilities/incident-log.js +99 -0
  15. package/dist/capabilities/opensky.js +74 -0
  16. package/dist/capabilities/weather.js +180 -0
  17. package/dist/scripts/agent-reach-deep-tests.js +54 -0
  18. package/dist/scripts/agent-reach-tests.js +38 -0
  19. package/dist/scripts/agent-reach-wrapper-tests.js +79 -0
  20. package/dist/scripts/anything-tests.js +95 -0
  21. package/dist/scripts/async-collect.js +18 -0
  22. package/dist/scripts/diff-test.js +39 -0
  23. package/dist/scripts/engine-run.js +14 -0
  24. package/dist/scripts/engine-tests.js +45 -0
  25. package/dist/scripts/hbcli-tests.js +85 -0
  26. package/dist/scripts/incident-tests.js +96 -0
  27. package/dist/scripts/journey-tests.js +45 -0
  28. package/dist/scripts/opensky-check.js +20 -0
  29. package/dist/scripts/opensky-tests.js +52 -0
  30. package/dist/scripts/probe-poi-tests.js +79 -0
  31. package/dist/scripts/replay-async.js +44 -0
  32. package/dist/scripts/replay-real.js +38 -0
  33. package/dist/scripts/replay.js +44 -0
  34. package/dist/scripts/skeleton-check.js +35 -0
  35. package/dist/scripts/skeleton-integration-test.js +22 -0
  36. package/dist/scripts/smoke.js +90 -0
  37. package/dist/scripts/unified-tests.js +49 -0
  38. package/dist/scripts/weather-tests.js +48 -0
  39. package/dist/src/bridge.js +34 -0
  40. package/dist/src/contracts.js +64 -0
  41. package/dist/src/dsh-llm.js +154 -0
  42. package/dist/src/engine.js +331 -0
  43. package/dist/src/index.js +760 -0
  44. package/dist/src/journey.js +147 -0
  45. package/dist/src/loop.js +296 -0
  46. package/dist/src/mock-llm.js +98 -0
  47. package/dist/src/model.js +134 -0
  48. package/dist/src/unified.js +536 -0
  49. 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