@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.
Files changed (130) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +267 -0
  3. package/bin/gotry-inner.js +214 -0
  4. package/bin/gotry-stdio-ask.js +59 -0
  5. package/bin/gotry.js +28 -0
  6. package/cordis.gotry-patch.yml +86 -0
  7. package/data/flights_2026.json +218 -0
  8. package/data/golden_erhai.json +92 -0
  9. package/data/golden_trip_2026.json +75 -0
  10. package/data/hotels_2026.json +151 -0
  11. package/data/openflights-skeleton.json +895 -0
  12. package/data/time-slot-eval.json +384 -0
  13. package/data/yunnan-pack.json +202 -0
  14. package/dist/capabilities/agent-reach-bridge.py +90 -0
  15. package/dist/capabilities/agent-reach-deep.js +172 -0
  16. package/dist/capabilities/agent-reach.js +204 -0
  17. package/dist/capabilities/anything.js +139 -0
  18. package/dist/capabilities/flyai.js +131 -0
  19. package/dist/capabilities/hbcli.js +143 -0
  20. package/dist/capabilities/incident-log.js +100 -0
  21. package/dist/capabilities/opensky.js +74 -0
  22. package/dist/capabilities/session/action-cache.js +120 -0
  23. package/dist/capabilities/session/adapters/ctrip-flight.js +83 -0
  24. package/dist/capabilities/session/adapters/meituan-local.js +73 -0
  25. package/dist/capabilities/session/extract.js +40 -0
  26. package/dist/capabilities/session/read-guard.js +99 -0
  27. package/dist/capabilities/session/transport.js +101 -0
  28. package/dist/capabilities/session-search.js +93 -0
  29. package/dist/capabilities/weather.js +180 -0
  30. package/dist/scripts/action-cache-tests.js +107 -0
  31. package/dist/scripts/agent-reach-deep-tests.js +54 -0
  32. package/dist/scripts/agent-reach-tests.js +38 -0
  33. package/dist/scripts/agent-reach-wrapper-tests.js +79 -0
  34. package/dist/scripts/anything-tests.js +95 -0
  35. package/dist/scripts/async-collect.js +37 -0
  36. package/dist/scripts/companion-tests.js +112 -0
  37. package/dist/scripts/diff-test.js +39 -0
  38. package/dist/scripts/engine-run.js +14 -0
  39. package/dist/scripts/engine-tests.js +45 -0
  40. package/dist/scripts/hbcli-tests.js +101 -0
  41. package/dist/scripts/incident-tests.js +96 -0
  42. package/dist/scripts/journey-tests.js +45 -0
  43. package/dist/scripts/ledger-tests.js +365 -0
  44. package/dist/scripts/ledger-workflow-crash.js +42 -0
  45. package/dist/scripts/memory-capture-tests.js +77 -0
  46. package/dist/scripts/memory-decay-tests.js +83 -0
  47. package/dist/scripts/memory-metrics.js +24 -0
  48. package/dist/scripts/nudge-digest.js +83 -0
  49. package/dist/scripts/opensky-check.js +20 -0
  50. package/dist/scripts/opensky-tests.js +52 -0
  51. package/dist/scripts/probe-poi-tests.js +88 -0
  52. package/dist/scripts/publish-preverify.js +68 -0
  53. package/dist/scripts/replay-async.js +44 -0
  54. package/dist/scripts/replay-real.js +38 -0
  55. package/dist/scripts/replay.js +116 -0
  56. package/dist/scripts/session-attach-diagnose.js +27 -0
  57. package/dist/scripts/session-attach-poc.js +65 -0
  58. package/dist/scripts/session-extract-tests.js +81 -0
  59. package/dist/scripts/session-tests.js +162 -0
  60. package/dist/scripts/skeleton-check.js +37 -0
  61. package/dist/scripts/skeleton-integration-test.js +22 -0
  62. package/dist/scripts/skills-contract-tests.js +85 -0
  63. package/dist/scripts/smoke.js +328 -0
  64. package/dist/scripts/state-cli-tests.js +136 -0
  65. package/dist/scripts/state-cli.js +248 -0
  66. package/dist/scripts/time-eval-tests.js +318 -0
  67. package/dist/scripts/travel-timeline-tests.js +123 -0
  68. package/dist/scripts/unified-tests.js +49 -0
  69. package/dist/scripts/weather-tests.js +48 -0
  70. package/dist/src/bridge.js +34 -0
  71. package/dist/src/companions.js +112 -0
  72. package/dist/src/contracts.js +64 -0
  73. package/dist/src/dsh-llm.js +174 -0
  74. package/dist/src/engine.js +331 -0
  75. package/dist/src/index.js +1239 -0
  76. package/dist/src/journey.js +147 -0
  77. package/dist/src/loop.js +360 -0
  78. package/dist/src/memory-capture.js +40 -0
  79. package/dist/src/memory-decay.js +31 -0
  80. package/dist/src/memory-utility.js +55 -0
  81. package/dist/src/mock-llm.js +103 -0
  82. package/dist/src/model.js +134 -0
  83. package/dist/src/slot-spec.js +165 -0
  84. package/dist/src/state-ledger.js +901 -0
  85. package/dist/src/time-anchor.js +100 -0
  86. package/dist/src/tool-packet.js +12 -0
  87. package/dist/src/travel-slots.js +144 -0
  88. package/dist/src/travel-timeline.js +78 -0
  89. package/dist/src/unified.js +538 -0
  90. package/dist/src/wish-pool.js +27 -0
  91. package/package.json +84 -0
  92. package/ts/capabilities/agent-reach-deep.ts +114 -0
  93. package/ts/capabilities/agent-reach.ts +158 -0
  94. package/ts/capabilities/anything.ts +201 -0
  95. package/ts/capabilities/flyai.ts +151 -0
  96. package/ts/capabilities/hbcli.ts +152 -0
  97. package/ts/capabilities/incident-log.ts +148 -0
  98. package/ts/capabilities/opensky.ts +121 -0
  99. package/ts/capabilities/session/action-cache.ts +148 -0
  100. package/ts/capabilities/session/adapters/ctrip-flight.ts +107 -0
  101. package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
  102. package/ts/capabilities/session/extract.ts +54 -0
  103. package/ts/capabilities/session/read-guard.ts +141 -0
  104. package/ts/capabilities/session/transport.ts +114 -0
  105. package/ts/capabilities/session-search.ts +129 -0
  106. package/ts/capabilities/weather.ts +176 -0
  107. package/ts/package.json +27 -0
  108. package/ts/scripts/async-collect.ts +50 -0
  109. package/ts/scripts/skeleton-check.ts +49 -0
  110. package/ts/scripts/skeleton-integration-test.ts +23 -0
  111. package/ts/scripts/state-cli.ts +201 -0
  112. package/ts/src/bridge.ts +44 -0
  113. package/ts/src/companions.ts +113 -0
  114. package/ts/src/contracts.ts +165 -0
  115. package/ts/src/dsh-llm.ts +155 -0
  116. package/ts/src/index.ts +968 -0
  117. package/ts/src/loop.ts +411 -0
  118. package/ts/src/memory-capture.ts +59 -0
  119. package/ts/src/memory-decay.ts +55 -0
  120. package/ts/src/memory-utility.ts +70 -0
  121. package/ts/src/mock-llm.ts +100 -0
  122. package/ts/src/model.ts +220 -0
  123. package/ts/src/slot-spec.ts +172 -0
  124. package/ts/src/state-ledger.ts +849 -0
  125. package/ts/src/time-anchor.ts +134 -0
  126. package/ts/src/tool-packet.ts +32 -0
  127. package/ts/src/travel-slots.ts +219 -0
  128. package/ts/src/travel-timeline.ts +83 -0
  129. package/ts/src/unified.ts +582 -0
  130. package/ts/src/wish-pool.ts +66 -0
@@ -0,0 +1,99 @@
1
+ import { mkdirSync, appendFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ const HARD_PAY_RE = /\/(pay|payment|cashier)(\/|\?|$)/i;
4
+ const WRITE_COMPOUND_RE = /(submitorder|createorder|placeorder|cancelorder|refundorder|orderconfirm|orderpay|confirmpay)/i;
5
+ const WRITE_URL_RE = /\/(order|orders|submit|trade|booking|book|purchase|buy|create|cancel|refund)(\/|\?|$)/i;
6
+ const WRITE_METHODS = new Set([
7
+ 'POST',
8
+ 'PUT',
9
+ 'DELETE',
10
+ 'PATCH'
11
+ ]);
12
+ export const SUBMIT_TEXT_RE = /下单|去支付|支付|预订|订票|购买|提交订单|确认订单/;
13
+ export function classifyRequest(method, url) {
14
+ if (HARD_PAY_RE.test(url) || WRITE_COMPOUND_RE.test(url)) return 'block';
15
+ if (WRITE_METHODS.has(method.toUpperCase()) && WRITE_URL_RE.test(url)) return 'block';
16
+ return 'allow';
17
+ }
18
+ export function isSubmitText(text) {
19
+ return SUBMIT_TEXT_RE.test(text ?? '');
20
+ }
21
+ export async function attachReadGuard(ctx, auditPath) {
22
+ let blocked = 0;
23
+ let total = 0;
24
+ await ctx.route('**/*', async (route)=>{
25
+ const req = route.request();
26
+ total += 1;
27
+ const verdict = classifyRequest(req.method(), req.url());
28
+ if (verdict === 'block') {
29
+ blocked += 1;
30
+ if (auditPath) {
31
+ const entry = {
32
+ ts: new Date().toISOString(),
33
+ kind: 'blocked-write-request',
34
+ method: req.method(),
35
+ url: req.url().slice(0, 400)
36
+ };
37
+ try {
38
+ mkdirSync(dirname(auditPath), {
39
+ recursive: true
40
+ });
41
+ appendFileSync(auditPath, JSON.stringify(entry) + '\n');
42
+ } catch {}
43
+ }
44
+ await route.abort('blockedbyclient');
45
+ return;
46
+ }
47
+ await route.continue();
48
+ });
49
+ return {
50
+ blockedCount: ()=>blocked,
51
+ requestCount: ()=>total
52
+ };
53
+ }
54
+ export async function attachReadGuardPuppeteer(browser, auditPath) {
55
+ let blocked = 0;
56
+ let total = 0;
57
+ const wire = (page)=>{
58
+ void (async ()=>{
59
+ try {
60
+ await page.setRequestInterception(true);
61
+ page.on('request', async (req)=>{
62
+ total += 1;
63
+ const verdict = classifyRequest(req.method(), req.url());
64
+ if (verdict === 'block') {
65
+ blocked += 1;
66
+ if (auditPath) {
67
+ const entry = {
68
+ ts: new Date().toISOString(),
69
+ kind: 'blocked-write-request',
70
+ method: req.method(),
71
+ url: req.url().slice(0, 400)
72
+ };
73
+ try {
74
+ mkdirSync(dirname(auditPath), {
75
+ recursive: true
76
+ });
77
+ appendFileSync(auditPath, JSON.stringify(entry) + '\n');
78
+ } catch {}
79
+ }
80
+ await req.abort('blockedbyclient').catch(()=>{});
81
+ return;
82
+ }
83
+ await req.continue().catch(()=>{});
84
+ });
85
+ } catch {}
86
+ })();
87
+ };
88
+ for (const p of (await browser.pages()))wire(p);
89
+ browser.on('targetcreated', (t)=>{
90
+ if (t.type() === 'page') void t.page().then(wire).catch(()=>{});
91
+ });
92
+ return {
93
+ blockedCount: ()=>blocked,
94
+ requestCount: ()=>total
95
+ };
96
+ }
97
+
98
+
99
+ //# sourceURL=/Users/bytedance/work/gotry/ts/capabilities/session/read-guard.ts
@@ -0,0 +1,101 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { attachReadGuardPuppeteer } from './read-guard.js';
4
+ async function loadPuppeteer() {
5
+ try {
6
+ return await import('puppeteer-core');
7
+ } catch {
8
+ return {
9
+ missing: true
10
+ };
11
+ }
12
+ }
13
+ function devtoolsWsEndpoint() {
14
+ try {
15
+ const udd = process.env.CHROME_USER_DATA_DIR ?? `${homedir()}/Library/Application Support/Google/Chrome`;
16
+ const [port, wsPath] = readFileSync(`${udd}/DevToolsActivePort`, 'utf8').trim().split('\n');
17
+ if (!port || !wsPath?.startsWith('/devtools/browser/')) {
18
+ return {
19
+ err: `DevToolsActivePort 内容异常: ${String(port)}/${String(wsPath)}`
20
+ };
21
+ }
22
+ return {
23
+ ws: `ws://127.0.0.1:${port.trim()}${wsPath.trim()}`
24
+ };
25
+ } catch {
26
+ return {
27
+ err: '日常 Chrome 未开调试端口。开启方法:Chrome 打开 chrome://inspect/#remote-debugging → 打开开关(Chrome 144+)'
28
+ };
29
+ }
30
+ }
31
+ export async function openSession(opts = {}) {
32
+ const pp = await loadPuppeteer();
33
+ if ('missing' in pp) {
34
+ return {
35
+ ok: false,
36
+ summary: 'puppeteer-core 未安装(可选依赖):会话检索不可用,官方通道 gotry_flyai_search 仍可用。安装:npm i -D puppeteer-core'
37
+ };
38
+ }
39
+ const puppeteer = pp.default;
40
+ const useCdp = opts.mode === 'cdp' || opts.mode !== 'persistent' && !opts.profileDir;
41
+ let browser;
42
+ let isCdp = false;
43
+ if (useCdp) {
44
+ const d = devtoolsWsEndpoint();
45
+ if ('err' in d) return {
46
+ ok: false,
47
+ summary: d.err
48
+ };
49
+ try {
50
+ browser = await puppeteer.connect({
51
+ browserWSEndpoint: d.ws,
52
+ defaultViewport: null
53
+ });
54
+ isCdp = true;
55
+ } catch (e) {
56
+ const msg = e instanceof Error ? e.message : e?.message ?? JSON.stringify(e).slice(0, 200);
57
+ return {
58
+ ok: false,
59
+ summary: `cdp attach 失败:${String(msg).split('\n')[0]}`
60
+ };
61
+ }
62
+ } else {
63
+ const { tmpdir } = await import('node:os');
64
+ const profileDir = opts.profileDir ?? `${tmpdir()}/gotry-session-profile`;
65
+ try {
66
+ browser = await puppeteer.launch({
67
+ channel: 'chrome',
68
+ headless: opts.headless ?? true,
69
+ userDataDir: profileDir
70
+ });
71
+ } catch (e) {
72
+ return {
73
+ ok: false,
74
+ summary: `chrome launch failed: ${e instanceof Error ? e.message.split('\n')[0] : String(e)}`
75
+ };
76
+ }
77
+ }
78
+ let guard;
79
+ try {
80
+ guard = await attachReadGuardPuppeteer(browser, opts.auditPath);
81
+ } catch (e) {
82
+ await (isCdp ? browser.disconnect() : browser.close()).catch(()=>{});
83
+ return {
84
+ ok: false,
85
+ summary: `read-guard attach failed: ${e instanceof Error ? e.message.split('\n')[0] : String(e)}`
86
+ };
87
+ }
88
+ const page = (await browser.pages())[0] ?? await browser.newPage();
89
+ return {
90
+ ok: true,
91
+ browser,
92
+ page,
93
+ guard,
94
+ close: async ()=>{
95
+ await (isCdp ? browser.disconnect() : browser.close()).catch(()=>{});
96
+ }
97
+ };
98
+ }
99
+
100
+
101
+ //# sourceURL=/Users/bytedance/work/gotry/ts/capabilities/session/transport.ts
@@ -0,0 +1,93 @@
1
+ import { openSession } from './session/transport.js';
2
+ import { buildEntryUrl, NETWORK_HINTS, parseBatchSearch, LOGIN_COOKIE_NAMES, SITE_DOMAIN } from './session/adapters/ctrip-flight.js';
3
+ const MIN_INTERVAL_MS = 30_000;
4
+ const lastCallAt = new Map();
5
+ export function __resetRateLimiterForTest() {
6
+ lastCallAt.clear();
7
+ }
8
+ const CHALLENGE_RE = /验证|滑块|captcha|verify/i;
9
+ export async function sessionFlightSearch(q) {
10
+ const started = Date.now();
11
+ const ts = new Date().toISOString();
12
+ const site = 'ctrip-flight';
13
+ const err = (verdict, error)=>({
14
+ ok: false,
15
+ via: 'session-ctrip-flight-error',
16
+ evidence: `[会话:${site}@error@${ts}] ${error}`,
17
+ latencyMs: Date.now() - started,
18
+ verdict,
19
+ error
20
+ });
21
+ const last = lastCallAt.get(site) ?? 0;
22
+ if (Date.now() - last < MIN_INTERVAL_MS) {
23
+ return err('cooldown', `rate limit: last call ${Date.now() - last}ms ago, min ${MIN_INTERVAL_MS}ms`);
24
+ }
25
+ lastCallAt.set(site, Date.now());
26
+ const entry = buildEntryUrl(q.from, q.to, q.date);
27
+ if (!entry.ok || !entry.url) {
28
+ return err('error', `unresolved entry: ${(entry.unresolved ?? []).join('/')} 不在城市码表`);
29
+ }
30
+ const t = await openSession({
31
+ profileDir: q.profileDir,
32
+ headless: q.headless,
33
+ auditPath: q.auditPath,
34
+ mode: q.profileDir ? 'persistent' : 'cdp'
35
+ });
36
+ if (!t.ok) {
37
+ if (q.profileDir === undefined && /cdp attach 失败/.test(t.summary)) return err('needs-attach', t.summary);
38
+ return err('error', t.summary);
39
+ }
40
+ try {
41
+ const loggedIn = async ()=>{
42
+ const cookies = await t.browser.cookies().catch(()=>[]);
43
+ return cookies.some((c)=>c.domain.includes(SITE_DOMAIN.replace(/^\./, '')) && LOGIN_COOKIE_NAMES.includes(c.name));
44
+ };
45
+ if (!await loggedIn() && !q.allowAnonymous) {
46
+ return err('needs-login', '匿名实例——先跑 scripts/session-login.ts 用用户自己的账号建立登录态(allowAnonymous 仅限链路自检)');
47
+ }
48
+ let settled = false;
49
+ let body = '';
50
+ const heard = new Promise((resolve)=>{
51
+ t.page.on('response', async (res)=>{
52
+ if (settled) return;
53
+ const u = res.url();
54
+ if (!NETWORK_HINTS.some((re)=>re.test(u))) return;
55
+ try {
56
+ body = await res.text();
57
+ } catch {}
58
+ if (body) {
59
+ settled = true;
60
+ resolve();
61
+ }
62
+ });
63
+ setTimeout(()=>resolve(), q.timeoutMs ?? 25_000);
64
+ });
65
+ await t.page.goto(entry.url, {
66
+ waitUntil: 'domcontentloaded',
67
+ timeout: 30_000
68
+ });
69
+ await heard;
70
+ const title = await t.page.title().catch(()=>'');
71
+ const headHtml = (await t.page.content().catch(()=>'')).slice(0, 5000);
72
+ if (CHALLENGE_RE.test(title + headHtml)) {
73
+ return err('challenged', `风控/验证码命中(title=${title.slice(0, 60)});按红线不重试不绕过,交还用户`);
74
+ }
75
+ const options = parseBatchSearch(body);
76
+ const verdict = options.length > 0 ? 'hit' : 'miss';
77
+ return {
78
+ ok: true,
79
+ via: 'session-ctrip-flight',
80
+ evidence: `[会话:${site}@${ts}] ${options.length} options;guard blocked=${t.guard.blockedCount()}/${t.guard.requestCount()}${q.allowAnonymous ? ';anonymous=自检态' : ''}`,
81
+ latencyMs: Date.now() - started,
82
+ verdict,
83
+ options
84
+ };
85
+ } catch (e) {
86
+ return err('error', e instanceof Error ? e.message.slice(0, 200) : String(e));
87
+ } finally{
88
+ await t.close();
89
+ }
90
+ }
91
+
92
+
93
+ //# sourceURL=/Users/bytedance/work/gotry/ts/capabilities/session-search.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,107 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { ActionCache, actionCachePath, fingerprint, templateKey } from '../capabilities/session/action-cache.js';
5
+ let pass = 0;
6
+ let fail = 0;
7
+ function assert(cond, label, detail) {
8
+ if (cond) {
9
+ pass += 1;
10
+ console.log(` ok - ${label}`);
11
+ } else {
12
+ fail += 1;
13
+ console.log(` FAIL - ${label}${detail !== undefined ? ' :: ' + String(detail) : ''}`);
14
+ }
15
+ }
16
+ const iso = mkdtempSync(join(tmpdir(), 'gotry-actioncache-'));
17
+ const path = actionCachePath(iso);
18
+ const T0 = new Date('2026-08-28T10:00:00Z');
19
+ let clock = T0.getTime();
20
+ const now = ()=>new Date(clock);
21
+ const site = 'ctrip-flight';
22
+ console.log('A. templateKey(参数变量化)');
23
+ const k1 = templateKey(site, 'search', {
24
+ from: '上海',
25
+ to: '丽江',
26
+ date: '2026-10-01'
27
+ });
28
+ const k2 = templateKey(site, 'search', {
29
+ from: '上海',
30
+ to: '丽江',
31
+ date: '2026-11-11'
32
+ });
33
+ assert(k1 === k2 && k1 === 'ctrip-flight:search:上海|丽江|%date%', '同型查询共享模板 key(日期变量化),跨日期复用');
34
+ console.log('B. 命中与指纹失配(被动失效)');
35
+ const c = new ActionCache(path, {
36
+ now
37
+ });
38
+ const fpA = fingerprint('batchSearch v1 shape {"flightItineraryList":[]}');
39
+ const fpB = fingerprint('batchSearch v2 shape {"itineraries":[]}');
40
+ c.record(k1, fpA, {
41
+ entryUrl: 'https://flights.ctrip.com/online/list/oneway-sha-ljg?depdate=%date%',
42
+ parser: 'batchSearch.v1'
43
+ }, site);
44
+ clock += 60_000;
45
+ assert(c.lookup(k1, fpA, site)?.locator.parser === 'batchSearch.v1', '指纹一致 → 命中并回放确定性载荷');
46
+ assert(c.lookup(k1, fpA, site)?.hits === 2, '命中计 hits');
47
+ assert(c.lookup(k1, fpB, site) === null, '接口改版(指纹失配)→ miss 不误放(错误的缓存点击比慢更糟)');
48
+ assert(c.lookup(templateKey(site, 'search', {
49
+ from: '北京',
50
+ to: '大理',
51
+ date: '2026-10-01'
52
+ }), fpA, site) === null, 'key 不同 → miss');
53
+ console.log('C. miss 回写与幂等');
54
+ c.record(k1, fpB, {
55
+ entryUrl: 'oneway-bjs-dlu?depdate=%date%',
56
+ parser: 'batchSearch.v2'
57
+ }, site);
58
+ assert(c.lookup(k1, fpB, site)?.locator.parser === 'batchSearch.v2', '改版后回写新载荷,下次命中');
59
+ const hitsBefore = c.lookup(k1, fpB, site).hits;
60
+ c.record(k1, fpB, {
61
+ parser: 'batchSearch.v2'
62
+ }, site);
63
+ assert(c.lookup(k1, fpB, site).hits === hitsBefore + 1, '重复 record 幂等(hits/createdAt 保留,不重置)');
64
+ console.log('D. TTL 过期(48h 默认;注入时钟)');
65
+ clock += 49 * 3600_000;
66
+ assert(c.lookup(k1, fpB, site) === null && c.size(site) === 0, '超 48h → 过期删除条目');
67
+ console.log('E. LRU 淘汰(每站点上限)');
68
+ const c2 = new ActionCache(path + '.lru', {
69
+ now,
70
+ maxEntriesPerSite: 3
71
+ });
72
+ for(let i = 0; i < 5; i++){
73
+ const key = `ctrip-flight:search:城${i}|丽江|%date%`;
74
+ c2.record(key, fingerprint(`shape-${i}`), {
75
+ i: String(i)
76
+ }, site);
77
+ clock += 1_000;
78
+ }
79
+ assert(c2.size(site) === 3, '超容 LRU 淘汰至 3 条');
80
+ console.log('F. 持久化跨实例 + 损坏文件容错');
81
+ const c3 = new ActionCache(path + '.p', {
82
+ now
83
+ });
84
+ c3.record('meituan:search:大理|民宿|%date%', fingerprint('mt-shape'), {
85
+ url: 'https://www.meituan.com/...'
86
+ }, 'meituan');
87
+ clock += 5_000;
88
+ const c4 = new ActionCache(path + '.p', {
89
+ now
90
+ });
91
+ assert(c4.lookup('meituan:search:大理|民宿|%date%', fingerprint('mt-shape'), 'meituan') !== null, '跨实例:落盘后新实例可命中');
92
+ writeFileSync(path + '.broken', '{not json', 'utf8');
93
+ const c5 = new ActionCache(path + '.broken', {
94
+ now
95
+ });
96
+ assert(c5.size() === 0 && c5.lookup('any', 'any', site) === null, '损坏 JSON 按空缓存,不抛错');
97
+ console.log('G. fingerprint 敏感度');
98
+ assert(fingerprint('abc') !== fingerprint('abd') && fingerprint('abc') === fingerprint('abc'), '微改即变,同文稳定');
99
+ rmSync(iso, {
100
+ recursive: true,
101
+ force: true
102
+ });
103
+ console.log(`\nACTION-CACHE: ${pass} pass, ${fail} fail`);
104
+ process.exit(fail > 0 ? 1 : 0);
105
+
106
+
107
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/action-cache-tests.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
@@ -0,0 +1,38 @@
1
+ import assert from 'node:assert/strict';
2
+ import { readUrl } from '../capabilities/agent-reach.js';
3
+ {
4
+ const r = await readUrl({
5
+ url: 'example.com'
6
+ });
7
+ assert.equal(r.ok, false);
8
+ assert.equal(r.via, 'r.jina.ai-error');
9
+ assert.ok(r.evidence.includes('error'), 'evidence 带 error');
10
+ console.log('1. 非法 URL 拒绝 OK');
11
+ }{
12
+ const r = await readUrl({
13
+ url: 'https://example.com',
14
+ timeoutMs: 1
15
+ });
16
+ assert.equal(r.ok, false);
17
+ assert.equal(r.via, 'r.jina.ai-error');
18
+ assert.ok(r.latencyMs >= 0, '永不抛错');
19
+ console.log('2. 超时降级 OK');
20
+ }{
21
+ const r = await readUrl({
22
+ url: 'https://example.com',
23
+ timeoutMs: 30_000
24
+ });
25
+ if (r.ok) {
26
+ assert.ok(r.evidence.includes('[agent-reach:web.read@'), '证据链');
27
+ assert.match(r.evidence, /\[agent-reach:web\.read@2\d{3}-\d{2}-\d{2}T/, '证据链带 ISO 时间戳');
28
+ assert.ok(typeof r.content === 'string' && r.content.length > 0, 'content 非空');
29
+ assert.ok(typeof r.title === 'string' && r.title.length > 0, `Title 提取,实际 ${r.title}`);
30
+ console.log(`3. readUrl example.com → ok (title=${r.title}) OK`);
31
+ } else {
32
+ assert.equal(r.via, 'r.jina.ai-error');
33
+ console.log(`3. readUrl example.com → 降级(${r.error?.slice(0, 60)})(网络边界,合法) OK`);
34
+ }
35
+ }console.log('\nAGENT-REACH TESTS: 3/3 OK(readUrl 薄壳;上游真读见 wrapper 套)');
36
+
37
+
38
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/agent-reach-tests.ts