@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,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
|