@danceiny/gotry 0.0.1-rc.7 → 0.0.1-rc.9

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 (75) hide show
  1. package/README.md +29 -74
  2. package/bin/gotry-inner.js +50 -1
  3. package/bin/gotry-stdio-ask.js +59 -0
  4. package/cordis.gotry-patch.yml +63 -5
  5. package/data/flights_2026.json +7 -4
  6. package/data/hotels_2026.json +102 -15
  7. package/data/time-slot-eval.json +384 -0
  8. package/data/yunnan-pack.json +2 -1
  9. package/dist/capabilities/agent-reach-bridge.py +90 -0
  10. package/dist/capabilities/anything.js +2 -2
  11. package/dist/capabilities/flyai.js +131 -0
  12. package/dist/capabilities/hbcli.js +11 -6
  13. package/dist/capabilities/incident-log.js +2 -1
  14. package/dist/capabilities/session/action-cache.js +120 -0
  15. package/dist/capabilities/session/adapters/ctrip-flight.js +83 -0
  16. package/dist/capabilities/session/adapters/meituan-local.js +73 -0
  17. package/dist/capabilities/session/extract.js +40 -0
  18. package/dist/capabilities/session/read-guard.js +56 -0
  19. package/dist/capabilities/session/transport.js +95 -0
  20. package/dist/capabilities/session-search.js +95 -0
  21. package/dist/scripts/action-cache-tests.js +107 -0
  22. package/dist/scripts/agent-reach-tests.js +1 -1
  23. package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
  24. package/dist/scripts/async-collect.js +26 -7
  25. package/dist/scripts/companion-tests.js +112 -0
  26. package/dist/scripts/hbcli-tests.js +18 -2
  27. package/dist/scripts/ledger-tests.js +344 -0
  28. package/dist/scripts/ledger-workflow-crash.js +42 -0
  29. package/dist/scripts/memory-capture-tests.js +77 -0
  30. package/dist/scripts/memory-decay-tests.js +83 -0
  31. package/dist/scripts/memory-metrics.js +24 -0
  32. package/dist/scripts/nudge-digest.js +83 -0
  33. package/dist/scripts/probe-poi-tests.js +9 -0
  34. package/dist/scripts/replay.js +73 -1
  35. package/dist/scripts/session-attach-diagnose.js +31 -0
  36. package/dist/scripts/session-attach-poc.js +65 -0
  37. package/dist/scripts/session-attach-wait.js +41 -0
  38. package/dist/scripts/session-extract-tests.js +81 -0
  39. package/dist/scripts/session-login.js +48 -0
  40. package/dist/scripts/session-tests.js +162 -0
  41. package/dist/scripts/skeleton-check.js +3 -1
  42. package/dist/scripts/skills-contract-tests.js +85 -0
  43. package/dist/scripts/smoke.js +242 -4
  44. package/dist/scripts/state-cli-tests.js +136 -0
  45. package/dist/scripts/state-cli.js +234 -0
  46. package/dist/scripts/time-eval-tests.js +318 -0
  47. package/dist/scripts/travel-timeline-tests.js +123 -0
  48. package/dist/scripts/unified-tests.js +1 -1
  49. package/dist/src/companions.js +112 -0
  50. package/dist/src/dsh-llm.js +27 -7
  51. package/dist/src/index.js +600 -121
  52. package/dist/src/loop.js +89 -25
  53. package/dist/src/memory-capture.js +40 -0
  54. package/dist/src/memory-decay.js +31 -0
  55. package/dist/src/memory-utility.js +55 -0
  56. package/dist/src/mock-llm.js +6 -1
  57. package/dist/src/slot-spec.js +165 -0
  58. package/dist/src/state-ledger.js +848 -0
  59. package/dist/src/time-anchor.js +100 -0
  60. package/dist/src/tool-packet.js +12 -0
  61. package/dist/src/travel-slots.js +144 -0
  62. package/dist/src/travel-timeline.js +78 -0
  63. package/dist/src/unified.js +3 -1
  64. package/dist/src/wish-pool.js +27 -0
  65. package/package.json +6 -2
  66. package/ts/capabilities/hbcli.ts +6 -6
  67. package/ts/capabilities/incident-log.ts +6 -3
  68. package/ts/package.json +3 -0
  69. package/ts/scripts/skeleton-check.ts +5 -1
  70. package/ts/src/dsh-llm.ts +27 -7
  71. package/ts/src/index.ts +439 -91
  72. package/ts/src/loop.ts +109 -34
  73. package/ts/src/mock-llm.ts +13 -1
  74. package/ts/src/unified.ts +6 -1
  75. package/ts/cordis.gotry-patch.yml +0 -36
@@ -0,0 +1,83 @@
1
+ import assert from 'node:assert/strict';
2
+ import { decayedConfidence, eventDecayScore, windowFactor } from '../src/memory-decay.js';
3
+ const NOW = new Date(2026, 7, 28, 12);
4
+ let n = 0;
5
+ function pass(name, body) {
6
+ body();
7
+ console.log(` ${++n}. ${name} OK`);
8
+ }
9
+ pass('分级窗口:边界值与地板', ()=>{
10
+ assert.equal(windowFactor(0), 1.0);
11
+ assert.equal(windowFactor(30), 1.0);
12
+ assert.equal(windowFactor(31), 0.75);
13
+ assert.equal(windowFactor(90), 0.75);
14
+ assert.equal(windowFactor(91), 0.5);
15
+ assert.equal(windowFactor(180), 0.5);
16
+ assert.equal(windowFactor(181), 0.25);
17
+ assert.equal(windowFactor(365), 0.25);
18
+ assert.equal(windowFactor(366), 0.1);
19
+ assert.equal(windowFactor(3000), 0.1, '地板 0.1:旧而不灭');
20
+ });
21
+ pass('种类权重:verified > applied > recalled(自称被召回 ≠ 有用)', ()=>{
22
+ const ts = '2026-08-27T00:00:00Z';
23
+ assert.ok(eventDecayScore({
24
+ ts,
25
+ kind: 'verified_outcome'
26
+ }, NOW) > eventDecayScore({
27
+ ts,
28
+ kind: 'applied'
29
+ }, NOW));
30
+ assert.ok(eventDecayScore({
31
+ ts,
32
+ kind: 'applied'
33
+ }, NOW) > eventDecayScore({
34
+ ts,
35
+ kind: 'recalled'
36
+ }, NOW));
37
+ });
38
+ pass('单调:同种类,新事件分 ≥ 旧事件分', ()=>{
39
+ const fresh = eventDecayScore({
40
+ ts: '2026-08-27T00:00:00Z',
41
+ kind: 'recalled'
42
+ }, NOW);
43
+ const old = eventDecayScore({
44
+ ts: '2025-01-01T00:00:00Z',
45
+ kind: 'recalled'
46
+ }, NOW);
47
+ assert.ok(fresh >= old);
48
+ assert.equal(old, 0.025, '地板乘子=0.25×0.1(一年外)');
49
+ });
50
+ pass('置信度上界 1 且多事件累加有界', ()=>{
51
+ const many = Array.from({
52
+ length: 50
53
+ }, ()=>({
54
+ ts: '2026-08-27T00:00:00Z',
55
+ kind: 'verified_outcome'
56
+ }));
57
+ assert.equal(decayedConfidence(many, NOW), 1);
58
+ const one = decayedConfidence([
59
+ {
60
+ ts: '2026-08-27T00:00:00Z',
61
+ kind: 'verified_outcome'
62
+ }
63
+ ], NOW);
64
+ assert.ok(one > 0 && one <= 1);
65
+ });
66
+ pass('动机层零衰减(构造性):本模块无作用于 motivation-profile 的 API,权重原样透传', ()=>{
67
+ const weights = {
68
+ escape_rest: 0.4,
69
+ curiosity: 0.09
70
+ };
71
+ const snapshot = JSON.stringify(weights);
72
+ void decayedConfidence([
73
+ {
74
+ ts: '2020-01-01T00:00:00Z',
75
+ kind: 'recalled'
76
+ }
77
+ ], NOW);
78
+ assert.equal(JSON.stringify(weights), snapshot);
79
+ });
80
+ console.log(`\nMEMORY DECAY TESTS: ${n}/5 OK(memory-design P3 分级窗口/单调/地板/上界/动机零衰减)`);
81
+
82
+
83
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/memory-decay-tests.ts
@@ -0,0 +1,24 @@
1
+ import { projectUtility } from '../src/memory-utility.js';
2
+ import { decayedConfidence } from '../src/memory-decay.js';
3
+ import { readUtilityEventsWithFallback, readWishPoolWithFallback } from '../src/state-ledger.js';
4
+ const stateRoot = process.argv[2] ?? '.';
5
+ const pool = readWishPoolWithFallback(stateRoot);
6
+ const events = readUtilityEventsWithFallback(stateRoot);
7
+ const projection = projectUtility(events);
8
+ const active = pool.filter((w)=>typeof w.wish_id === 'string');
9
+ const recalledWishes = Object.values(projection).filter((w)=>w.recalled > 0).length;
10
+ const verifiedWishes = Object.values(projection).filter((w)=>w.verified > 0).length;
11
+ const refluxBaseline = verifiedWishes / Math.max(recalledWishes, 1);
12
+ console.log('=== GoTry 记忆效用指标(M4 北极星过程面,只读) ===');
13
+ console.log(`wish pool: ${pool.length} 条在册(其中 ${pool.filter((w)=>w.muted).length} 条休眠,${active.length} 条有稳定 wish_id)`);
14
+ console.log(`效用事件: ${events.length} 条(recalled=${events.filter((e)=>e.kind === 'recalled').length}, applied=${events.filter((e)=>e.kind === 'applied').length}, verified=${events.filter((e)=>e.kind === 'verified_outcome').length})`);
15
+ for (const w of Object.values(projection)){
16
+ const name = pool.find((p)=>p.wish_id === w.wish_id)?.name ?? w.wish_id;
17
+ const conf = decayedConfidence(events.filter((e)=>e.wish_id === w.wish_id), new Date());
18
+ console.log(` - ${name}: status=${w.status}, recalled=${w.recalled}, applied=${w.applied}, verified=${w.verified}, 新鲜置信度=${conf}(P3 时间窗衰减)`);
19
+ }
20
+ console.log(`经验回流率基线 = ${verifiedWishes}/${recalledWishes} = ${refluxBaseline.toFixed(2)}(verified/recalled;单用户起步期样本稀疏属预期)`);
21
+ if (pool.length === 0) console.log('(wish pool 为空——首访用户,指标从首条憧憬入池开始积累)');
22
+
23
+
24
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/memory-metrics.ts
@@ -0,0 +1,83 @@
1
+ import { writeFileSync, mkdirSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { pickNudgeWish } from '../src/wish-pool.js';
4
+ import { projectUtility } from '../src/memory-utility.js';
5
+ import { buildTimeAnchor } from '../src/time-anchor.js';
6
+ import { readUtilityEventsWithFallback, readWishPoolWithFallback } from '../src/state-ledger.js';
7
+ function arg(name) {
8
+ const i = process.argv.indexOf(name);
9
+ return i >= 0 ? process.argv[i + 1] : undefined;
10
+ }
11
+ const stateRoot = arg('--state-root') ?? '.';
12
+ const stateDir = join(stateRoot, 'gotry-state');
13
+ function readEvents() {
14
+ return readUtilityEventsWithFallback(stateRoot);
15
+ }
16
+ if (process.env['GOTRY_NUDGE_ENABLED'] === 'false') {
17
+ console.log('回访已关闭(GOTRY_NUDGE_ENABLED=false)——可关闭契约,正常退出');
18
+ process.exit(0);
19
+ }
20
+ const pool = readWishPoolWithFallback(stateRoot);
21
+ const anchor = buildTimeAnchor(new Date());
22
+ const ctx = {
23
+ days: arg('--days') ? Number(arg('--days')) : undefined,
24
+ budgetCny: arg('--budget') ? Number(arg('--budget')) : undefined,
25
+ month: arg('--month') ? Number(arg('--month')) : new Date().getMonth() + 1
26
+ };
27
+ const match = pickNudgeWish(pool, ctx);
28
+ const lines = [
29
+ `# 「下一次出发」回访摘要(${anchor.today} ${anchor.todayWeekdayZh})`
30
+ ];
31
+ if (!match) {
32
+ lines.push(`在册 ${pool.filter((p)=>!p.muted).length} 条憧憬,当前窗口无可成行匹配——不打扰(0..1 纪律:不硬推)。`);
33
+ } else {
34
+ const c = match.entry.conditions ?? {};
35
+ const u = projectUtility(readEvents())[match.wishId];
36
+ lines.push(`**${String(match.entry.name ?? match.wishId)}**(${match.hits.join(' + ')},命中 ${match.score}/3 项)`);
37
+ lines.push(`- 成行条件: ${JSON.stringify(c)}`);
38
+ if (match.entry.reason) lines.push(`- 当初为什么: ${String(match.entry.reason).slice(0, 160)}`);
39
+ lines.push(`- 效用状态: ${u?.status ?? 'unknown'}(被召回 ${u?.recalled ?? 0} 次)`);
40
+ lines.push('');
41
+ lines.push('_想安排就说一声;不想被打扰说「mute <名字>」即可休眠,憧憬不被拒绝。_');
42
+ }
43
+ const digest = lines.join('\n');
44
+ const channel = process.env['GOTRY_NUDGE_CHANNEL'] ?? 'stdout';
45
+ if (channel === 'file') {
46
+ const out = process.env['GOTRY_NUDGE_FILE'] ?? join(stateDir, 'nudge-digest.md');
47
+ mkdirSync(dirname(out), {
48
+ recursive: true
49
+ });
50
+ writeFileSync(out, digest + '\n', 'utf-8');
51
+ console.log(`摘要已写入 ${out}`);
52
+ } else if (channel === 'lark') {
53
+ const webhook = process.env['GOTRY_LARK_WEBHOOK'];
54
+ if (!webhook) {
55
+ console.log('lark 通道未配置(GOTRY_LARK_WEBHOOK 缺失)——降级 stdout,即插即用等 key:');
56
+ console.log(digest);
57
+ } else {
58
+ try {
59
+ const resp = await fetch(webhook, {
60
+ method: 'POST',
61
+ headers: {
62
+ 'Content-Type': 'application/json'
63
+ },
64
+ body: JSON.stringify({
65
+ msg_type: 'text',
66
+ content: {
67
+ text: digest
68
+ }
69
+ })
70
+ });
71
+ console.log(`lark 投递 HTTP ${resp.status}`);
72
+ if (!resp.ok) console.log(digest);
73
+ } catch (e) {
74
+ console.log(`lark 投递失败(${e.message})——降级 stdout:`);
75
+ console.log(digest);
76
+ }
77
+ }
78
+ } else {
79
+ console.log(digest);
80
+ }
81
+
82
+
83
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/nudge-digest.ts
@@ -73,6 +73,15 @@ pass('6. 不抛错 + 返回类型是 string 或 null(边界)', ()=>{
73
73
  }
74
74
  }
75
75
  });
76
+ pass('7. 金标准对话噪音回归(2026-08-28 巡检:访谈答案/多段首查不再误触发)', ()=>{
77
+ assert.equal(probePoi('我的工作时间是UTC+4的早上10点到下午7点'), null, '访谈答案(>12 字含陈述动词)不触发');
78
+ assert.equal(probePoi('明天有空'), null, '短句含陈述动词不触发');
79
+ assert.equal(probePoi('7.17周五22:40落地深圳,7.18早上去香港办银行开户&保险签约;争取7.18当天飞泰国普吉岛……8.10周一凌晨从深圳起飞,周一上班前到迪拜。请给我做机票和酒店的行程规划和推荐。'), null, '多段首查无 POI 信号不触发');
80
+ const booking = probePoi('我订了酒店:The Title East Wing Rawai,7.18入住 7.23 退房');
81
+ if (!booking || !booking.includes('Title')) throw new Error(`订酒店应抓名称段,得 ${JSON.stringify(booking)}`);
82
+ console.log(' 7a. 访谈答案/多段首查 → null OK');
83
+ console.log(' 7b. 订酒店抓名称段(The Title…) OK');
84
+ });
76
85
  console.log('\nprobePoi TESTS: 6 类 OK(不崩 + 5 类触发 + 1 类不触发返 null)');
77
86
 
78
87
 
@@ -39,6 +39,78 @@ if (!state.profile.workWindow?.evidence) throw new Error('FAIL: workWindow 缺
39
39
  if (!state.profile.bookedResources?.length) throw new Error('FAIL: bookedResources 缺失');
40
40
  if (!(missing.length === 1 && missing[0] === 'budgetTier')) throw new Error(`FAIL: 剩余待问应为 [budgetTier],实为 [${missing.join(',')}]`);
41
41
  console.log('REPLAY ASSERTS OK');
42
-
42
+ {
43
+ const conflictScript = [
44
+ {
45
+ when: '重新算一下',
46
+ extraction: {
47
+ schema_version: 'travel_slot_extraction.v1',
48
+ language: 'zh',
49
+ domains: [
50
+ 'requisition'
51
+ ],
52
+ slots: {
53
+ requisition: {
54
+ mode: 'create',
55
+ destination: '普吉岛',
56
+ start_date: '2026-12-25',
57
+ trip_type: 'round_trip'
58
+ }
59
+ },
60
+ missing_slots: []
61
+ }
62
+ }
63
+ ];
64
+ const solvePort = (spec)=>{
65
+ spec.skeletonHub = true;
66
+ return solveUnified(spec);
67
+ };
68
+ const base = createMockLlm(join('..', 'data', 'flights_2026.json'), conflictScript);
69
+ const singleSpecLlm = {
70
+ ...base,
71
+ extractSpec: async ()=>({
72
+ segments: [
73
+ {
74
+ id: 's1',
75
+ role: 'choice',
76
+ date: '2026-07-01',
77
+ options: [
78
+ {
79
+ id: 'o1',
80
+ label: 'o1',
81
+ move: {
82
+ hub: 'SZX',
83
+ services: [
84
+ {
85
+ id: 'f1',
86
+ depMin: 600,
87
+ arrMin: 700
88
+ }
89
+ ],
90
+ bufferMin: 60,
91
+ originTransferMin: 30,
92
+ destTransferMin: 30
93
+ }
94
+ }
95
+ ]
96
+ }
97
+ ]
98
+ })
99
+ };
100
+ const state1 = structuredClone(state);
101
+ const solveBefore = state1.solve;
102
+ const r1 = await runTurn(state1, '12月25日出发,重新算一下', singleSpecLlm, [
103
+ ...history
104
+ ], solvePort);
105
+ if (!r1.reply.includes('日期分歧')) throw new Error(`FAIL: 单日期段分歧应被闸拦下,实际:${r1.reply.slice(0, 160)}`);
106
+ if (r1.state.solve !== solveBefore) throw new Error('FAIL: 分歧时不应求解(spec 日期未确认)');
107
+ const state2 = structuredClone(state);
108
+ const r2 = await runTurn(state2, '重新算一下', base, [
109
+ ...history
110
+ ], solvePort);
111
+ if (r2.reply.includes('日期分歧')) throw new Error(`FAIL: 多段行程应旁路日期闸(无逐段真值不判),实际:${r2.reply.slice(0, 160)}`);
112
+ if (!r2.state.solve) throw new Error('FAIL: 多段旁路后应正常求解');
113
+ console.log('SPEC-SLOT DATE GATE OK(单日期段分歧拦截 + 多段旁路不误伤)');
114
+ }
43
115
 
44
116
  //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/replay.ts
@@ -0,0 +1,31 @@
1
+ import { openSession } from '../capabilities/session/transport.js';
2
+ const t = await openSession({
3
+ mode: 'cdp'
4
+ });
5
+ if (!t.ok) {
6
+ console.log('ATTACH 失败:', t.summary);
7
+ process.exit(1);
8
+ }
9
+ try {
10
+ const all = await t.context.cookies();
11
+ const byDom = {};
12
+ for (const c of all){
13
+ const d = c.domain.replace(/^\./, '');
14
+ if (/ctrip\.com$|meituan\.com$/.test(d)) (byDom[d] ??= []).push(c.name);
15
+ }
16
+ console.log('ATTACH ok,共', all.length, '条 cookie');
17
+ for (const [d, names] of Object.entries(byDom))console.log(d, '=>', names.sort().join(','));
18
+ const ctripNames = new Set(Object.entries(byDom).filter(([d])=>d.includes('ctrip')).flatMap(([, n])=>n));
19
+ const known = [
20
+ 'cticket',
21
+ 'uid',
22
+ 'uname',
23
+ 'passport'
24
+ ].filter((k)=>ctripNames.has(k));
25
+ console.log(known.length > 0 ? `携程登录态:在(${known.join(',')})` : '携程登录态:未检出(名单待以上输出校准)');
26
+ } finally{
27
+ await t.close();
28
+ }
29
+
30
+
31
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/session-attach-diagnose.ts
@@ -0,0 +1,65 @@
1
+ import { chromium } from "playwright-core";
2
+ const route = process.argv[2] ?? "sha-ljg";
3
+ const date = process.argv[3] ?? "2026-10-01";
4
+ const url = `https://flights.ctrip.com/online/list/oneway-${route}?depdate=${date}`;
5
+ const PROFILE = "/tmp/gotry-session-poc-profile";
6
+ async function main() {
7
+ const ctx = await chromium.launchPersistentContext(PROFILE, {
8
+ channel: "chrome",
9
+ headless: false,
10
+ viewport: {
11
+ width: 1440,
12
+ height: 900
13
+ },
14
+ args: [
15
+ "--disable-blink-features=AutomationControlled"
16
+ ]
17
+ });
18
+ const page = ctx.pages()[0] ?? await ctx.newPage();
19
+ const hits = [];
20
+ const cap = (s)=>s.replace(/\s+/g, " ").slice(0, 600);
21
+ ctx.on("response", async (res)=>{
22
+ const u = res.url();
23
+ const ct = res.headers()["content-type"] ?? "";
24
+ if (!ct.includes("json") || u.length > 400) return;
25
+ if (!/search|list|flight|itinerary|poll/i.test(u)) return;
26
+ try {
27
+ const body = await res.text();
28
+ if (body.length < 200) return;
29
+ hits.push({
30
+ url: u,
31
+ bytes: body.length
32
+ });
33
+ if (hits.length <= 3) {
34
+ console.log(`\n[HIT ${hits.length}] ${res.status()} ${cap(u)}\n body(${body.length}B): ${cap(body)}`);
35
+ }
36
+ } catch {}
37
+ });
38
+ console.log(`goto ${url}`);
39
+ await page.goto(url, {
40
+ waitUntil: "domcontentloaded",
41
+ timeout: 30_000
42
+ });
43
+ await page.waitForTimeout(15_000);
44
+ const title = await page.title().catch(()=>"");
45
+ console.log(`\ntitle: ${cap(title)}`);
46
+ const html = await page.content().catch(()=>"");
47
+ const challenged = /验证|滑块|captcha|verify/i.test(title + html.slice(0, 5000));
48
+ console.log(`challenge-detected: ${challenged}`);
49
+ console.log(`\nsummary: ${hits.length} 条搜索类 JSON XHR;总命中 ${hits.reduce((a, b)=>a + b.bytes, 0)}B`);
50
+ console.log("按体积 top5(P1 适配器要嗅探的接口面):");
51
+ for (const h of [
52
+ ...hits
53
+ ].sort((a, b)=>b.bytes - a.bytes).slice(0, 5)){
54
+ console.log(` ${h.bytes}B ${cap(h.url)}`);
55
+ }
56
+ console.log(challenged ? "结论:触发风控——按红线不重试不绕过,如实记录" : "结论:嗅探链路成立(只读,零交互)");
57
+ await ctx.close();
58
+ }
59
+ main().catch((e)=>{
60
+ console.error("PoC 失败:", e instanceof Error ? e.message : e);
61
+ process.exit(1);
62
+ });
63
+
64
+
65
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/session-attach-poc.ts
@@ -0,0 +1,41 @@
1
+ import { chromium } from 'playwright-core';
2
+ import { readFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ const udd = process.env.CHROME_USER_DATA_DIR ?? `${homedir()}/Library/Application Support/Google/Chrome`;
5
+ const [port, wsPath] = readFileSync(`${udd}/DevToolsActivePort`, 'utf8').trim().split('\n');
6
+ const wsUrl = `ws://127.0.0.1:${port.trim()}${wsPath.trim()}`;
7
+ for(let round = 1; round <= 3; round++){
8
+ console.log(`第 ${round}/3 轮连接(60s 长等待——若 Chrome 弹出调试授权框,请点「允许」)...`);
9
+ try {
10
+ const browser = await chromium.connectOverCDP(wsUrl, {
11
+ timeout: 60_000
12
+ });
13
+ console.log('ATTACH 成功!');
14
+ const context = browser.contexts()[0];
15
+ const all = await context.cookies();
16
+ const byDom = {};
17
+ for (const c of all){
18
+ const d = c.domain.replace(/^\./, '');
19
+ if (/ctrip\.com$|meituan\.com$/.test(d)) (byDom[d] ??= []).push(c.name);
20
+ }
21
+ console.log(`共 ${all.length} 条 cookie;相关域:`);
22
+ for (const [d, names] of Object.entries(byDom))console.log(' ', d, '=>', names.sort().join(','));
23
+ const ctrip = new Set(Object.entries(byDom).filter(([d])=>d.includes('ctrip')).flatMap(([, n])=>n));
24
+ const hit = [
25
+ 'cticket',
26
+ 'uid',
27
+ 'uname',
28
+ 'passport'
29
+ ].filter((k)=>ctrip.has(k));
30
+ console.log(hit.length ? `携程登录态:在(${hit.join(',')})` : '携程登录态:未检出(用上面名单校准 LOGIN_COOKIE_NAMES)');
31
+ await browser.close().catch(()=>{});
32
+ process.exit(0);
33
+ } catch (e) {
34
+ console.log(` 失败:${e instanceof Error ? e.message.split('\n')[0] : String(e)}`);
35
+ }
36
+ }
37
+ console.log('三轮均未连上——回 chrome://inspect/#remote-debugging 确认开关仍开,再重跑本脚本。');
38
+ process.exit(1);
39
+
40
+
41
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/session-attach-wait.ts
@@ -0,0 +1,81 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { extractA11yEntries, filterSubmitEntries, nameAffinity } from '../capabilities/session/extract.js';
4
+ import { buildMeituanEntry, extractListings, parseMeituanSearch } from '../capabilities/session/adapters/meituan-local.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
+ console.log('A. extractA11yEntries(快照形状抽取)');
17
+ const snap = `- button "搜索" [ref=s1e5]
18
+ - textbox "出发城市" [ref=s1e6]
19
+ - heading "上海到丽江机票" [level=1]
20
+ - listitem "吉祥航空 HO5577 ¥1610 起"
21
+ - generic "广告"`;
22
+ const entries = extractA11yEntries(snap);
23
+ assert(entries.length === 5 && entries[0].role === 'button' && entries[0].name === '搜索', 'role+name 提取(ref 行)');
24
+ assert(entries[2].name === '上海到丽江机票' && entries[3].role === 'listitem', 'heading/listitem 归位');
25
+ const noName = extractA11yEntries('- img\n- separator');
26
+ assert(noName.every((e)=>typeof e.name === 'string'), '无 name 行保留空占位不炸');
27
+ assert(extractA11yEntries('').length === 0, '空快照返空');
28
+ console.log('B. filterSubmitEntries(DOM 提交件剔除)');
29
+ const kept = filterSubmitEntries(entries, (n)=>/下单|支付/.test(n));
30
+ assert(kept.length === entries.length, '无提交词:全保留');
31
+ const snap2 = extractA11yEntries('- button "立即下单" [ref=s1e9]\n- button "搜索" [ref=s2e1]');
32
+ assert(filterSubmitEntries(snap2, (n)=>/下单|支付/.test(n)).length === 1, 'button 命中提交黑名单剔除,搜索保留');
33
+ assert(filterSubmitEntries(extractA11yEntries('- heading "支付说明"'), (n)=>/下单|支付/.test(n)).length === 1, '非交互 role(heading)不剔除');
34
+ console.log('C. nameAffinity(自愈候选排序底座)');
35
+ assert(nameAffinity('吉祥航空 HO5577', '吉祥航空 HO5577') === 1, '全等=1');
36
+ assert(nameAffinity('上海到丽江机票', '机票') === 0.8, '包含=0.8');
37
+ assert(nameAffinity('abc', 'xyz') < 0.5, '无关串低分');
38
+ console.log('D. 美团适配器骨架(词表边界/占位合同)');
39
+ const mt = buildMeituanEntry('大理', 'minsu', '洱海');
40
+ assert(mt.ok === true && mt.url === 'https://minsu.meituan.com/dali/?q=%E6%B4%B1%E6%B5%B7', '民宿 entry URL(拼音+关键词编码)');
41
+ assert(buildMeituanEntry('乌兰巴托', 'hotel').ok === false, '词表外城市 unresolved');
42
+ assert(parseMeituanSearch('{"any":1}').length === 0, 'XHR 解析占位:接口实测回填前恒空(miss 语义)');
43
+ console.log('E. extractListings(a11y 兜底:名称+¥价格)');
44
+ const listings = extractListings(entries);
45
+ assert(listings.length === 1 && listings[0].name === '吉祥航空 HO5577' && listings[0].price === 1610, 'listitem 抽名称+价格,截 ¥ 后缀');
46
+ assert(extractListings(extractA11yEntries('- heading "无价格标题"')).length === 0, '无价格行不产出');
47
+ console.log('F. 金标准 20 查询 schema(只增不改语义)');
48
+ const golden = JSON.parse(readFileSync(join(import.meta.dirname, '../data/session-golden-20.json'), 'utf8'));
49
+ assert(golden.queries.length === 20, '恰 20 条');
50
+ assert(new Set(golden.queries.map((q)=>q.id)).size === 20, 'id 唯一');
51
+ const ctripCities = new Set([
52
+ '上海',
53
+ '北京',
54
+ '广州',
55
+ '深圳',
56
+ '成都',
57
+ '昆明',
58
+ '大理',
59
+ '丽江',
60
+ '西安',
61
+ '杭州',
62
+ '三亚',
63
+ '厦门',
64
+ '重庆',
65
+ '青岛',
66
+ '长沙',
67
+ '武汉',
68
+ '南京',
69
+ '郑州',
70
+ '贵阳',
71
+ '桂林',
72
+ '西双版纳',
73
+ '香格里拉'
74
+ ]);
75
+ assert(golden.queries.filter((q)=>q.kind === 'flight').every((q)=>ctripCities.has(q.from) && ctripCities.has(q.to)), 'flight 城市全在双适配器词表内');
76
+ assert(golden.queries.filter((q)=>q.kind.startsWith('meituan') || q.kind.startsWith('flyai')).every((q)=>q.id), '每条带 id');
77
+ console.log(`\nSESSION-EXTRACT: ${pass} pass, ${fail} fail`);
78
+ process.exit(fail > 0 ? 1 : 0);
79
+
80
+
81
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/session-extract-tests.ts
@@ -0,0 +1,48 @@
1
+ import { openSession } from '../capabilities/session/transport.js';
2
+ import { LOGIN_COOKIE_NAMES, SITE_DOMAIN } from '../capabilities/session/adapters/ctrip-flight.js';
3
+ const t = await openSession({
4
+ headless: false
5
+ });
6
+ if (!t.ok) {
7
+ console.error('打开会话失败:', t.summary);
8
+ process.exit(1);
9
+ }
10
+ try {
11
+ await t.page.goto('https://flights.ctrip.com/', {
12
+ waitUntil: 'domcontentloaded',
13
+ timeout: 30_000
14
+ });
15
+ const loggedIn = async ()=>{
16
+ const cookies = await t.context.cookies([
17
+ `https://flights${SITE_DOMAIN.replace(/^\./, '')}/`
18
+ ]).catch(()=>[]);
19
+ return cookies.some((c)=>LOGIN_COOKIE_NAMES.includes(c.name));
20
+ };
21
+ if (await loggedIn()) {
22
+ console.log('已是登录态(~/.gotry/session-profile 持久保存),无需操作。');
23
+ } else {
24
+ console.log('请在打开的窗口中登录携程(手机号+验证码/账密均可)。agent 不会读取或输入任何凭证;登录完成自动检测,最长等 5 分钟...');
25
+ let windowClosed = false;
26
+ t.page.on('close', ()=>{
27
+ windowClosed = true;
28
+ });
29
+ const deadline = Date.now() + 300_000;
30
+ while(Date.now() < deadline && !windowClosed && !await loggedIn()){
31
+ await new Promise((r)=>setTimeout(r, 3_000));
32
+ }
33
+ if (windowClosed) {
34
+ console.log('窗口被提前关闭——登录态未建立。重跑本脚本,等打印「登录态已建立」后再关窗。');
35
+ process.exitCode = 1;
36
+ } else if (await loggedIn()) {
37
+ console.log('登录态已建立并持久化(profile 落在 ~/.gotry/session-profile;后续会话检索直接复用)。');
38
+ } else {
39
+ console.log('超时未检测到登录票据(cookie 名单待校准,或未完成登录)——重跑本脚本即可。');
40
+ process.exitCode = 1;
41
+ }
42
+ }
43
+ } finally{
44
+ await t.close();
45
+ }
46
+
47
+
48
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/session-login.ts