@danceiny/gotry 0.0.1-rc.8 → 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.
- package/README.md +6 -4
- package/cordis.gotry-patch.yml +7 -0
- package/dist/capabilities/flyai.js +131 -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 +56 -0
- package/dist/capabilities/session/transport.js +95 -0
- package/dist/capabilities/session-search.js +95 -0
- package/dist/scripts/action-cache-tests.js +107 -0
- package/dist/scripts/async-collect.js +26 -7
- package/dist/scripts/companion-tests.js +112 -0
- package/dist/scripts/ledger-tests.js +344 -0
- package/dist/scripts/ledger-workflow-crash.js +42 -0
- package/dist/scripts/memory-decay-tests.js +83 -0
- package/dist/scripts/memory-metrics.js +6 -20
- package/dist/scripts/nudge-digest.js +4 -14
- package/dist/scripts/session-attach-diagnose.js +31 -0
- package/dist/scripts/session-attach-poc.js +65 -0
- package/dist/scripts/session-attach-wait.js +41 -0
- package/dist/scripts/session-extract-tests.js +81 -0
- package/dist/scripts/session-login.js +48 -0
- package/dist/scripts/session-tests.js +162 -0
- package/dist/scripts/smoke.js +41 -1
- package/dist/scripts/state-cli-tests.js +136 -0
- package/dist/scripts/state-cli.js +234 -0
- package/dist/scripts/travel-timeline-tests.js +123 -0
- package/dist/scripts/unified-tests.js +1 -1
- package/dist/src/companions.js +112 -0
- package/dist/src/index.js +346 -117
- package/dist/src/loop.js +52 -15
- package/dist/src/memory-decay.js +31 -0
- package/dist/src/state-ledger.js +848 -0
- package/dist/src/travel-timeline.js +78 -0
- package/dist/src/unified.js +3 -1
- package/package.json +1 -1
- package/ts/package.json +3 -0
- package/ts/src/index.ts +216 -83
- package/ts/src/loop.ts +57 -17
- package/ts/src/unified.ts +6 -1
package/README.md
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
| | |
|
|
11
11
|
|---|---|
|
|
12
|
-
| **Version** | `v0.0.1-rc.
|
|
13
|
-
| **Status** | M4 记忆域推进中(2026-08-28):记忆写读闭环 + 效用 sidecar + 「下一次出发」0::1 召回;
|
|
12
|
+
| **Version** | `v0.0.1-rc.8`(npm **latest 直指本版**,`npx @danceiny/gotry` 即得;[release notes](docs/release-notes.md)) |
|
|
13
|
+
| **Status** | M4 记忆域推进中(2026-08-28):记忆写读闭环 + 效用 sidecar + 「下一次出发」0::1 召回;17 工具;事务化状态账本(ADR-15:SQLite 单文件=唯一权威,旧 JSON/JSONL 为单向导出视图);全栈回归全绿(套件清单见 `scripts/run-all-tests.sh` 分节) |
|
|
14
14
|
| **Repo** | [github.com/Danceiny/gotry](https://github.com/Danceiny/gotry) (private) |
|
|
15
15
|
| **Runtime** | DeepSeek Harness 0.1.1-rc.2 (vendored `ts/dsh-runtime/`, [upstream](https://github.com/deepseek-ai/DeepSeek-Harness)) · Z3 (npm `z3-solver`) · LoopX (pipx, `~/.local/pipx/venvs/loopx/bin/loopx`) · Agent-Reach v1.5.0 (`.venv/bin/agent-reach`) |
|
|
16
16
|
| **License** | **MIT** (2026-08-23 落定,见 [LICENSE](LICENSE)) |
|
|
@@ -41,12 +41,14 @@ GoTry 把「想去哪」变成「能不能、怎么去」:
|
|
|
41
41
|
### 一行启动(npm,推荐)
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
npx @danceiny/gotry
|
|
44
|
+
npx @danceiny/gotry web # 首跑会提示在当前目录建 .env(LLM_API_KEY=<DeepSeek key>)
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
装完即得 :3080 对话界面。已装 dsh 的用户同理——上面的命令会自动以 cordis patch
|
|
48
48
|
挂载 gotry 插件,无需任何额外配置(零成本启动)。
|
|
49
49
|
|
|
50
|
+
> rc.5 为不可用首发(已停止指向);`@rc` tag 当前为 rc.7,`latest` 为 rc.8,两者均可跑。
|
|
51
|
+
|
|
50
52
|
### 源码安装(开发者)
|
|
51
53
|
|
|
52
54
|
```bash
|
|
@@ -262,4 +264,4 @@ ADR 与技术债见 [`docs/architecture.md` §8 / §10](docs/architecture.md)。
|
|
|
262
264
|
|
|
263
265
|
**Built with**: DeepSeek Harness 0.1.1-rc.2 · Cordis · Z3 (WASM) · loopx (pipx) · hotelbyte-cli · Agent-Reach v1.5.0 (`.venv/`) · OpenFlights · TypeScript · Bun
|
|
264
266
|
|
|
265
|
-
**Last verified against `
|
|
267
|
+
**Last verified against `v0.0.1-rc.8`(已发布,2026-08-28)** — 全栈回归全绿(发布流程见 `scripts/publish-npm.sh`,内部决策见 [docs/decisions-needed.md](docs/decisions-needed.md)、[docs/tokens.md](docs/tokens.md))。
|
package/cordis.gotry-patch.yml
CHANGED
|
@@ -77,3 +77,10 @@
|
|
|
77
77
|
evidence=用户原话),不等确认;班次/房型/落脚点等开放性选择不外问
|
|
78
78
|
用户——基于已知动机与约束给出最有说服力的方案并说明理由,用户否决
|
|
79
79
|
才改。校准类疑问永不阻塞推进。
|
|
80
|
+
(19)实时价与班期三级路由:机票/火车班次与票价先 gotry_flyai_search
|
|
81
|
+
(飞猪官方只读,主链路);关键决策(临近下单/价差存疑)再用
|
|
82
|
+
gotry_session_search 在用户本人登录态交叉验证,直达/中转分桶比对,
|
|
83
|
+
不比裸最低价;两源皆不可用退静态包并显式标 [静态包:估算]。
|
|
84
|
+
会话工具返回 needs-login→请用户跑 scripts/session-login.ts 并
|
|
85
|
+
继续用官方源推进;返回 challenged→立即停手如实告知,绝不重试
|
|
86
|
+
绝不绕过。[会话:*] 证据链随结论必达用户。
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
function sh(cmd, args, opts) {
|
|
3
|
+
const child = spawn(cmd, args, {
|
|
4
|
+
env: process.env,
|
|
5
|
+
cwd: process.cwd()
|
|
6
|
+
});
|
|
7
|
+
let stdout = '';
|
|
8
|
+
let stderr = '';
|
|
9
|
+
let error;
|
|
10
|
+
const timer = setTimeout(()=>{
|
|
11
|
+
try {
|
|
12
|
+
child.kill('SIGKILL');
|
|
13
|
+
} catch {}
|
|
14
|
+
}, opts.timeoutMs);
|
|
15
|
+
return new Promise((resolve)=>{
|
|
16
|
+
child.stdout?.on('data', (d)=>{
|
|
17
|
+
stdout += d.toString();
|
|
18
|
+
});
|
|
19
|
+
child.stderr?.on('data', (d)=>{
|
|
20
|
+
stderr += d.toString();
|
|
21
|
+
});
|
|
22
|
+
child.on('error', (e)=>{
|
|
23
|
+
error = e.message.slice(0, 200);
|
|
24
|
+
});
|
|
25
|
+
child.on('close', (code)=>{
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
resolve({
|
|
28
|
+
code: code ?? -1,
|
|
29
|
+
stdout,
|
|
30
|
+
stderr,
|
|
31
|
+
error
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}).finally(()=>clearTimeout(timer));
|
|
35
|
+
}
|
|
36
|
+
export async function flyaiSearch(q) {
|
|
37
|
+
const started = Date.now();
|
|
38
|
+
const ts = new Date().toISOString();
|
|
39
|
+
const base = {
|
|
40
|
+
kind: q.kind,
|
|
41
|
+
latencyMs: 0
|
|
42
|
+
};
|
|
43
|
+
const origin = (q.origin ?? '').trim();
|
|
44
|
+
const destination = (q.destination ?? '').trim();
|
|
45
|
+
if (!origin || !destination || !/^\d{4}-\d{2}-\d{2}$/.test(q.depDate ?? '')) {
|
|
46
|
+
return {
|
|
47
|
+
...base,
|
|
48
|
+
ok: false,
|
|
49
|
+
via: 'flyai-error',
|
|
50
|
+
verdict: 'error',
|
|
51
|
+
evidence: `[实时API:flyai@error@${ts}] bad args`,
|
|
52
|
+
error: 'origin/destination/depDate(YYYY-MM-DD) required'
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const sub = q.kind === 'flight' ? 'search-flight' : 'search-train';
|
|
56
|
+
const r = await sh(q.cliBin ?? 'npx', [
|
|
57
|
+
'-y',
|
|
58
|
+
'@fly-ai/flyai-cli',
|
|
59
|
+
sub,
|
|
60
|
+
'--origin',
|
|
61
|
+
origin,
|
|
62
|
+
'--destination',
|
|
63
|
+
destination,
|
|
64
|
+
'--dep-date',
|
|
65
|
+
q.depDate
|
|
66
|
+
], {
|
|
67
|
+
timeoutMs: q.timeoutMs ?? 30_000
|
|
68
|
+
});
|
|
69
|
+
const latencyMs = Date.now() - started;
|
|
70
|
+
if (r.error || r.code !== 0) {
|
|
71
|
+
return {
|
|
72
|
+
...base,
|
|
73
|
+
latencyMs,
|
|
74
|
+
ok: false,
|
|
75
|
+
via: 'flyai-error',
|
|
76
|
+
verdict: 'error',
|
|
77
|
+
evidence: `[实时API:flyai@error@${ts}] ${r.error ?? `exit ${r.code}`}`,
|
|
78
|
+
error: r.error ?? r.stderr.replace(/\s+/g, ' ').slice(0, 200)
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
let items;
|
|
82
|
+
try {
|
|
83
|
+
const jStart = r.stdout.indexOf('{');
|
|
84
|
+
const parsed = JSON.parse(jStart >= 0 ? r.stdout.slice(jStart) : r.stdout);
|
|
85
|
+
items = parsed.data?.itemList ?? [];
|
|
86
|
+
} catch {
|
|
87
|
+
const raw = r.stdout.replace(/\s+/g, ' ').slice(0, 160);
|
|
88
|
+
return {
|
|
89
|
+
...base,
|
|
90
|
+
latencyMs,
|
|
91
|
+
ok: false,
|
|
92
|
+
via: 'flyai-error',
|
|
93
|
+
verdict: 'error',
|
|
94
|
+
evidence: `[实时API:flyai@error@${ts}] parse failed: ${raw}`,
|
|
95
|
+
error: `failed to parse flyai output as JSON: ${raw}`
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const options = [];
|
|
99
|
+
for (const it of items){
|
|
100
|
+
const seg = it.journeys?.[0]?.segments?.[0];
|
|
101
|
+
if (!seg?.marketingTransportNo || !seg.depDateTime) continue;
|
|
102
|
+
const rawPrice = it.ticketPrice ?? it.price ?? '';
|
|
103
|
+
const numericPrice = Number(rawPrice);
|
|
104
|
+
options.push({
|
|
105
|
+
no: seg.marketingTransportNo,
|
|
106
|
+
name: seg.marketingTransportName ?? '',
|
|
107
|
+
depDateTime: seg.depDateTime ?? '',
|
|
108
|
+
arrDateTime: seg.arrDateTime ?? '',
|
|
109
|
+
depStation: seg.depStationName ?? '',
|
|
110
|
+
arrStation: seg.arrStationName ?? '',
|
|
111
|
+
durationMin: Number(seg.duration ?? 0) || 0,
|
|
112
|
+
price: Number.isFinite(numericPrice) && numericPrice > 0 ? numericPrice : 0,
|
|
113
|
+
priceRaw: /^\d+$/.test(rawPrice) ? undefined : rawPrice || undefined,
|
|
114
|
+
seatClass: seg.seatClassName,
|
|
115
|
+
jumpUrl: it.jumpUrl
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const verdict = options.length > 0 ? 'hit' : 'miss';
|
|
119
|
+
return {
|
|
120
|
+
kind: q.kind,
|
|
121
|
+
latencyMs,
|
|
122
|
+
ok: true,
|
|
123
|
+
via: 'flyai',
|
|
124
|
+
verdict,
|
|
125
|
+
evidence: `[实时API:flyai@${ts}] ${options.length}/${items.length} ${q.kind} options`,
|
|
126
|
+
options
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/capabilities/flyai.ts
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
export function templateKey(site, action, params) {
|
|
5
|
+
const val = (k)=>k === 'date' ? '%date%' : (params[k] ?? '').trim();
|
|
6
|
+
const head = [
|
|
7
|
+
'from',
|
|
8
|
+
'to',
|
|
9
|
+
'date'
|
|
10
|
+
].filter((k)=>k in params).map(val);
|
|
11
|
+
const tail = Object.keys(params).filter((k)=>![
|
|
12
|
+
'from',
|
|
13
|
+
'to',
|
|
14
|
+
'date'
|
|
15
|
+
].includes(k)).sort().map(val);
|
|
16
|
+
return `${site}:${action}:${[
|
|
17
|
+
...head,
|
|
18
|
+
...tail
|
|
19
|
+
].join('|')}`;
|
|
20
|
+
}
|
|
21
|
+
export function fingerprint(shapeText) {
|
|
22
|
+
return createHash('sha256').update(shapeText).digest('hex').slice(0, 16);
|
|
23
|
+
}
|
|
24
|
+
export class ActionCache {
|
|
25
|
+
filePath;
|
|
26
|
+
entries = new Map();
|
|
27
|
+
loaded = false;
|
|
28
|
+
ttlMs;
|
|
29
|
+
maxPerSite;
|
|
30
|
+
now;
|
|
31
|
+
constructor(filePath, opts = {}){
|
|
32
|
+
this.filePath = filePath;
|
|
33
|
+
this.ttlMs = opts.ttlMs ?? 48 * 3600_000;
|
|
34
|
+
this.maxPerSite = opts.maxEntriesPerSite ?? 50;
|
|
35
|
+
this.now = opts.now ?? (()=>new Date());
|
|
36
|
+
}
|
|
37
|
+
load() {
|
|
38
|
+
if (this.loaded) return;
|
|
39
|
+
this.loaded = true;
|
|
40
|
+
try {
|
|
41
|
+
if (existsSync(this.filePath)) {
|
|
42
|
+
const raw = JSON.parse(readFileSync(this.filePath, 'utf8'));
|
|
43
|
+
for (const e of raw){
|
|
44
|
+
if (e?.key && e.domFingerprint && e.locator) this.entries.set(e.key, e);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
persist() {
|
|
50
|
+
try {
|
|
51
|
+
mkdirSync(dirname(this.filePath), {
|
|
52
|
+
recursive: true
|
|
53
|
+
});
|
|
54
|
+
const tmp = this.filePath + '.tmp';
|
|
55
|
+
writeFileSync(tmp, JSON.stringify([
|
|
56
|
+
...this.entries.values()
|
|
57
|
+
], null, 0), 'utf8');
|
|
58
|
+
writeFileSync(this.filePath, readFileSync(tmp, 'utf8'), 'utf8');
|
|
59
|
+
try {
|
|
60
|
+
require('node:fs').rmSync(tmp);
|
|
61
|
+
} catch {}
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
siteEntries(site) {
|
|
65
|
+
return [
|
|
66
|
+
...this.entries.values()
|
|
67
|
+
].filter((e)=>e.key.startsWith(site + ':'));
|
|
68
|
+
}
|
|
69
|
+
lookup(key, domFingerprint, site) {
|
|
70
|
+
this.load();
|
|
71
|
+
const e = this.entries.get(key);
|
|
72
|
+
if (!e) return null;
|
|
73
|
+
if (this.now().getTime() - Date.parse(e.lastHitAt || e.createdAt) > this.ttlMs) {
|
|
74
|
+
this.entries.delete(key);
|
|
75
|
+
this.persist();
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
if (e.domFingerprint !== domFingerprint) return null;
|
|
79
|
+
e.hits += 1;
|
|
80
|
+
e.lastHitAt = this.now().toISOString();
|
|
81
|
+
this.persist();
|
|
82
|
+
void site;
|
|
83
|
+
return e;
|
|
84
|
+
}
|
|
85
|
+
record(key, domFingerprint, locator, site) {
|
|
86
|
+
this.load();
|
|
87
|
+
const ts = this.now().toISOString();
|
|
88
|
+
const prev = this.entries.get(key);
|
|
89
|
+
this.entries.set(key, {
|
|
90
|
+
key,
|
|
91
|
+
domFingerprint,
|
|
92
|
+
locator,
|
|
93
|
+
createdAt: prev?.createdAt ?? ts,
|
|
94
|
+
lastHitAt: ts,
|
|
95
|
+
hits: prev?.hits ?? 0
|
|
96
|
+
});
|
|
97
|
+
const siteList = this.siteEntries(site);
|
|
98
|
+
if (siteList.length > this.maxPerSite) {
|
|
99
|
+
const victim = siteList.sort((a, b)=>Date.parse(a.lastHitAt) - Date.parse(b.lastHitAt))[0];
|
|
100
|
+
if (victim) this.entries.delete(victim.key);
|
|
101
|
+
}
|
|
102
|
+
this.persist();
|
|
103
|
+
return this.entries.get(key);
|
|
104
|
+
}
|
|
105
|
+
invalidate(key) {
|
|
106
|
+
this.load();
|
|
107
|
+
this.entries.delete(key);
|
|
108
|
+
this.persist();
|
|
109
|
+
}
|
|
110
|
+
size(site) {
|
|
111
|
+
this.load();
|
|
112
|
+
return site ? this.siteEntries(site).length : this.entries.size;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export function actionCachePath(stateRoot) {
|
|
116
|
+
return join(stateRoot, 'session-action-cache.json');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/capabilities/session/action-cache.ts
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const CITY_CODES = {
|
|
2
|
+
上海: 'sha',
|
|
3
|
+
北京: 'bjs',
|
|
4
|
+
广州: 'can',
|
|
5
|
+
深圳: 'szx',
|
|
6
|
+
成都: 'ctu',
|
|
7
|
+
昆明: 'kmg',
|
|
8
|
+
大理: 'dlu',
|
|
9
|
+
丽江: 'ljg',
|
|
10
|
+
西安: 'sia',
|
|
11
|
+
杭州: 'hgh',
|
|
12
|
+
三亚: 'syx',
|
|
13
|
+
厦门: 'xmn',
|
|
14
|
+
重庆: 'cqg',
|
|
15
|
+
青岛: 'taa',
|
|
16
|
+
长沙: 'csx',
|
|
17
|
+
武汉: 'wuh',
|
|
18
|
+
南京: 'nkg',
|
|
19
|
+
郑州: 'cgo',
|
|
20
|
+
贵阳: 'kwe',
|
|
21
|
+
桂林: 'kwl',
|
|
22
|
+
西双版纳: 'jhg',
|
|
23
|
+
香格里拉: 'dig'
|
|
24
|
+
};
|
|
25
|
+
export function buildEntryUrl(from, to, depDate) {
|
|
26
|
+
const unresolved = [];
|
|
27
|
+
const fromCode = CITY_CODES[from];
|
|
28
|
+
const toCode = CITY_CODES[to];
|
|
29
|
+
if (!fromCode) unresolved.push(from);
|
|
30
|
+
if (!toCode) unresolved.push(to);
|
|
31
|
+
if (unresolved.length > 0 || !/^\d{4}-\d{2}-\d{2}$/.test(depDate)) {
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
unresolved: unresolved.length > 0 ? unresolved : [
|
|
35
|
+
depDate
|
|
36
|
+
]
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
ok: true,
|
|
41
|
+
url: `https://flights.ctrip.com/online/list/oneway-${fromCode}-${toCode}?depdate=${depDate}`
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export const NETWORK_HINTS = [
|
|
45
|
+
/search\/api\/search\/batchSearch/
|
|
46
|
+
];
|
|
47
|
+
export const LOGIN_COOKIE_NAMES = [
|
|
48
|
+
'cticket',
|
|
49
|
+
'uid',
|
|
50
|
+
'uname',
|
|
51
|
+
'passport'
|
|
52
|
+
];
|
|
53
|
+
export const SITE_DOMAIN = '.ctrip.com';
|
|
54
|
+
export function parseBatchSearch(body) {
|
|
55
|
+
let raw;
|
|
56
|
+
try {
|
|
57
|
+
raw = JSON.parse(body);
|
|
58
|
+
} catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
const out = [];
|
|
62
|
+
for (const it of raw.data?.flightItineraryList ?? []){
|
|
63
|
+
const seg = it.flightSegments?.[0];
|
|
64
|
+
const fl = seg?.flightList?.[0];
|
|
65
|
+
if (!fl?.flightNo || !fl.departureDateTime) continue;
|
|
66
|
+
const prices = (it.priceList ?? []).map((p)=>Number(p.adultPrice ?? 0)).filter((n)=>n > 0);
|
|
67
|
+
out.push({
|
|
68
|
+
flightNo: fl.flightNo,
|
|
69
|
+
airline: seg?.airlineName ?? '',
|
|
70
|
+
depDateTime: fl.departureDateTime ?? '',
|
|
71
|
+
arrDateTime: fl.arrivalDateTime ?? '',
|
|
72
|
+
depAirport: fl.departureAirportName ?? '',
|
|
73
|
+
arrAirport: fl.arrivalAirportName ?? '',
|
|
74
|
+
durationMin: Number(seg?.duration ?? 0) || 0,
|
|
75
|
+
price: prices.length > 0 ? Math.min(...prices) : 0,
|
|
76
|
+
aircraft: fl.aircraftName
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/capabilities/session/adapters/ctrip-flight.ts
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const CITY_SLUGS = {
|
|
2
|
+
上海: 'shanghai',
|
|
3
|
+
北京: 'beijing',
|
|
4
|
+
广州: 'guangzhou',
|
|
5
|
+
深圳: 'shenzhen',
|
|
6
|
+
成都: 'chengdu',
|
|
7
|
+
昆明: 'kunming',
|
|
8
|
+
大理: 'dali',
|
|
9
|
+
丽江: 'lijiang',
|
|
10
|
+
西安: 'xian',
|
|
11
|
+
杭州: 'hangzhou',
|
|
12
|
+
三亚: 'sanya',
|
|
13
|
+
厦门: 'xiamen',
|
|
14
|
+
重庆: 'chongqing',
|
|
15
|
+
青岛: 'qingdao',
|
|
16
|
+
长沙: 'changsha',
|
|
17
|
+
武汉: 'wuhan',
|
|
18
|
+
南京: 'nanjing',
|
|
19
|
+
郑州: 'zhengzhou',
|
|
20
|
+
贵阳: 'guiyang',
|
|
21
|
+
桂林: 'guilin'
|
|
22
|
+
};
|
|
23
|
+
export const SITE_DOMAIN = 'meituan.com';
|
|
24
|
+
export const LOGIN_COOKIE_NAMES = [
|
|
25
|
+
'lt',
|
|
26
|
+
'u',
|
|
27
|
+
'token',
|
|
28
|
+
'n'
|
|
29
|
+
];
|
|
30
|
+
export function buildMeituanEntry(city, vertical, keyword) {
|
|
31
|
+
const slug = CITY_SLUGS[city];
|
|
32
|
+
if (!slug) return {
|
|
33
|
+
ok: false,
|
|
34
|
+
unresolved: [
|
|
35
|
+
city
|
|
36
|
+
]
|
|
37
|
+
};
|
|
38
|
+
const base = vertical === 'hotel' ? `https://hotel.meituan.com/${slug}/` : `https://minsu.meituan.com/${slug}/`;
|
|
39
|
+
return {
|
|
40
|
+
ok: true,
|
|
41
|
+
url: keyword ? base + `?q=${encodeURIComponent(keyword)}` : base
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export const NETWORK_HINTS = [];
|
|
45
|
+
export function parseMeituanSearch(_body) {
|
|
46
|
+
void _body;
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
export function extractListings(entries, opts = {}) {
|
|
50
|
+
const out = [];
|
|
51
|
+
const roles = new Set([
|
|
52
|
+
'heading',
|
|
53
|
+
'listitem',
|
|
54
|
+
'link',
|
|
55
|
+
'article'
|
|
56
|
+
]);
|
|
57
|
+
for (const e of entries){
|
|
58
|
+
if (out.length >= (opts.max ?? 30)) break;
|
|
59
|
+
if (!roles.has(e.role) || !e.name) continue;
|
|
60
|
+
const pm = /¥\s*(\d+)/.exec(e.name);
|
|
61
|
+
if (!pm) continue;
|
|
62
|
+
const name = e.name.replace(/¥\s*\d+.*$/, '').trim().slice(0, 80);
|
|
63
|
+
if (!name) continue;
|
|
64
|
+
out.push({
|
|
65
|
+
name,
|
|
66
|
+
price: Number(pm[1])
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/capabilities/session/adapters/meituan-local.ts
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function extractA11yEntries(snapshotText, opts = {}) {
|
|
2
|
+
const cap = opts.maxEntries ?? 500;
|
|
3
|
+
const out = [];
|
|
4
|
+
const lines = snapshotText.split('\n');
|
|
5
|
+
for(let i = 0; i < lines.length && out.length < cap; i++){
|
|
6
|
+
const m = /^[\s*-]*\s*([a-zA-Z][\w-]*)(?:\s+"((?:[^"\\]|\\.)*)")?/.exec(lines[i] ?? '');
|
|
7
|
+
if (!m) continue;
|
|
8
|
+
const name = (m[2] ?? '').replace(/\\"/g, '"').slice(0, 200);
|
|
9
|
+
out.push({
|
|
10
|
+
role: m[1].toLowerCase(),
|
|
11
|
+
name,
|
|
12
|
+
line: i
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
export function filterSubmitEntries(entries, isSubmit) {
|
|
18
|
+
const interactive = new Set([
|
|
19
|
+
'button',
|
|
20
|
+
'link',
|
|
21
|
+
'menuitem',
|
|
22
|
+
'option'
|
|
23
|
+
]);
|
|
24
|
+
return entries.filter((e)=>!(interactive.has(e.role) && isSubmit(e.name)));
|
|
25
|
+
}
|
|
26
|
+
export function nameAffinity(a, b) {
|
|
27
|
+
const x = a.trim();
|
|
28
|
+
const y = b.trim();
|
|
29
|
+
if (!x || !y) return 0;
|
|
30
|
+
if (x === y) return 1;
|
|
31
|
+
if (x.includes(y) || y.includes(x)) return 0.8;
|
|
32
|
+
const sa = new Set(x);
|
|
33
|
+
const sb = new Set(y);
|
|
34
|
+
let common = 0;
|
|
35
|
+
for (const ch of sa)if (sb.has(ch)) common += 1;
|
|
36
|
+
return common / Math.max(sa.size, sb.size);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/capabilities/session/extract.ts
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
|
|
55
|
+
|
|
56
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/capabilities/session/read-guard.ts
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { chromium } from 'playwright-core';
|
|
2
|
+
import { attachReadGuard } from './read-guard.js';
|
|
3
|
+
export async function openSession(opts = {}) {
|
|
4
|
+
if (opts.mode === 'cdp' || opts.mode !== 'persistent' && !opts.profileDir) {
|
|
5
|
+
try {
|
|
6
|
+
const { homedir: _hd } = await import('node:os');
|
|
7
|
+
const udd = process.env.CHROME_USER_DATA_DIR ?? `${_hd()}/Library/Application Support/Google/Chrome`;
|
|
8
|
+
let endpoint = `http://127.0.0.1:${opts.cdpPort ?? 9222}`;
|
|
9
|
+
try {
|
|
10
|
+
const { readFileSync: _rf } = await import('node:fs');
|
|
11
|
+
const portFile = `${udd}/DevToolsActivePort`;
|
|
12
|
+
const [port, wsPath] = _rf(portFile, 'utf8').trim().split('\n');
|
|
13
|
+
if (port && wsPath?.startsWith('/devtools/browser/')) endpoint = `ws://127.0.0.1:${port.trim()}${wsPath.trim()}`;
|
|
14
|
+
} catch {}
|
|
15
|
+
const browser = await chromium.connectOverCDP(endpoint, {
|
|
16
|
+
timeout: 4_000
|
|
17
|
+
});
|
|
18
|
+
const context = browser.contexts()[0];
|
|
19
|
+
if (!context) {
|
|
20
|
+
await browser.close().catch(()=>{});
|
|
21
|
+
return {
|
|
22
|
+
ok: false,
|
|
23
|
+
summary: 'cdp attached but no default context'
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
let guard;
|
|
27
|
+
try {
|
|
28
|
+
guard = await attachReadGuard(context, opts.auditPath);
|
|
29
|
+
} catch (e) {
|
|
30
|
+
await browser.close().catch(()=>{});
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
summary: `read-guard attach failed: ${e instanceof Error ? e.message.slice(0, 200) : String(e)}`
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const page = context.pages()[0] ?? await context.newPage();
|
|
37
|
+
return {
|
|
38
|
+
ok: true,
|
|
39
|
+
context,
|
|
40
|
+
page,
|
|
41
|
+
guard,
|
|
42
|
+
close: async ()=>{
|
|
43
|
+
await browser.close().catch(()=>{});
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
} catch (e) {
|
|
47
|
+
const msg = e instanceof Error ? e.message.slice(0, 160) : String(e);
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
summary: `cdp attach 失败(日常 Chrome 未开调试端口):${msg}。开启方法:日常 Chrome 打开 chrome://inspect/#remote-debugging → 打开开关并确认弹窗(Chrome 144+)`
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const { homedir } = await import('node:os');
|
|
55
|
+
const profileDir = opts.profileDir ?? `${homedir()}/.gotry/session-profile`;
|
|
56
|
+
let context;
|
|
57
|
+
try {
|
|
58
|
+
context = await chromium.launchPersistentContext(profileDir, {
|
|
59
|
+
channel: 'chrome',
|
|
60
|
+
headless: opts.headless ?? true,
|
|
61
|
+
viewport: {
|
|
62
|
+
width: 1366,
|
|
63
|
+
height: 850
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
summary: `chrome launch failed: ${e instanceof Error ? e.message.slice(0, 200) : String(e)}`
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
let guard;
|
|
73
|
+
try {
|
|
74
|
+
guard = await attachReadGuard(context, opts.auditPath);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
await context.close().catch(()=>{});
|
|
77
|
+
return {
|
|
78
|
+
ok: false,
|
|
79
|
+
summary: `read-guard attach failed: ${e instanceof Error ? e.message.slice(0, 200) : String(e)}`
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const page = context.pages()[0] ?? await context.newPage();
|
|
83
|
+
return {
|
|
84
|
+
ok: true,
|
|
85
|
+
context,
|
|
86
|
+
page,
|
|
87
|
+
guard,
|
|
88
|
+
close: async ()=>{
|
|
89
|
+
await context.close().catch(()=>{});
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/capabilities/session/transport.ts
|