@danceiny/gotry 0.0.1-rc.10 → 0.0.1-rc.12
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 +35 -24
- package/cordis.gotry-patch.yml +14 -7
- package/dist/capabilities/flyai.js +135 -30
- package/dist/capabilities/hbcli.js +14 -2
- package/dist/capabilities/session/benchmark.js +252 -0
- package/dist/capabilities/session/transport.js +14 -9
- package/dist/capabilities/session-consent.js +82 -0
- package/dist/capabilities/session-login.js +103 -0
- package/dist/capabilities/session-search.js +5 -3
- package/dist/capabilities/weather.js +72 -17
- package/dist/scripts/agent-reach-wrapper-tests.js +4 -0
- package/dist/scripts/async-collect.js +33 -5
- package/dist/scripts/hbcli-tests.js +25 -4
- package/dist/scripts/i18n-tests.js +34 -0
- package/dist/scripts/ledger-tests.js +134 -4
- package/dist/scripts/memory-value-report.js +379 -0
- package/dist/scripts/product-metrics.js +569 -0
- package/dist/scripts/realtime-pricing-tests.js +115 -0
- package/dist/scripts/replay-real.js +3 -1
- package/dist/scripts/session-benchmark.js +261 -0
- package/dist/scripts/session-login.js +28 -0
- package/dist/scripts/session-tests.js +263 -22
- package/dist/scripts/smoke.js +112 -14
- package/dist/scripts/weather-tests.js +8 -1
- package/dist/scripts/z3-race-tests.js +35 -0
- package/dist/src/engine.js +185 -116
- package/dist/src/i18n.js +77 -0
- package/dist/src/index.js +124 -5
- package/dist/src/journey.js +78 -82
- package/dist/src/loop.js +65 -14
- package/dist/src/realtime-pricing.js +105 -0
- package/dist/src/state-ledger.js +30 -4
- package/dist/src/unified.js +158 -105
- package/dist/src/z3-shared.js +18 -0
- package/package.json +5 -3
- package/ts/capabilities/flyai.ts +177 -22
- package/ts/capabilities/hbcli.ts +16 -4
- package/ts/capabilities/session/benchmark.ts +273 -0
- package/ts/capabilities/session/transport.ts +19 -8
- package/ts/capabilities/session-consent.ts +127 -0
- package/ts/capabilities/session-login.ts +130 -0
- package/ts/capabilities/session-search.ts +11 -4
- package/ts/capabilities/weather.ts +84 -19
- package/ts/scripts/async-collect.ts +48 -7
- package/ts/src/index.ts +107 -11
- package/ts/src/loop.ts +85 -12
- package/ts/src/state-ledger.ts +48 -4
- package/ts/src/unified.ts +40 -36
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# GoTry
|
|
2
2
|
|
|
3
|
+
[](https://github.com/Danceiny/gotry/actions/workflows/ci.yml)
|
|
4
|
+
|
|
3
5
|
> **身体和灵魂,更多旅行,更少旅游。**
|
|
4
6
|
> *Body and soul — more travel, less tourism.*
|
|
5
7
|
|
|
@@ -9,10 +11,10 @@
|
|
|
9
11
|
|
|
10
12
|
| | |
|
|
11
13
|
|---|---|
|
|
12
|
-
| **Version** | `v0.0.1-rc.
|
|
13
|
-
| **Status** | M4
|
|
14
|
-
| **Repo** | [github.com/Danceiny/gotry](https://github.com/Danceiny/gotry) (
|
|
15
|
-
| **Runtime** | DeepSeek Harness 0.1.1-
|
|
14
|
+
| **Version** | `v0.0.1-rc.11`(npm **latest 直指本版**,`npx @danceiny/gotry` 即得;[release notes](docs/release-notes.md)) |
|
|
15
|
+
| **Status** | **M3 工程与分发面已就绪,但真实种子用户 evidence 未收口,M3 Exit 仍开放;M4 记忆域由 founder 授权并行推进,不构成 M3 Exit 证明;M5/M6 仅在各自 Entry gate 满足后启动。** M4 Issue #20 的 paired-cohort/active-planning/experience-reflux 证据合同与 synthetic fixture scorer 已接入 run-all §34(合成 N=3 不充当 Exit,真实 `observed_private` N≥5 paired cohort 待积累);18 工具(2026-08-29 +`gotry_session_login`);事务化状态账本(ADR-15,`gotry_async_terminal.v1`:4/4→`succeeded`/ledger `settled`/exit 0,非 4/4→`failed`/ledger `failed`/exit 2,终态复诵零重算)+ 双形态架构冻结(ADR-16:本地+Web 一套账本语义,tenant_id 一等字段);**会话数据面 #21**:字段 fixture scorer、双源合同与 waiting-attach no-spend 已进确定性回归,真实 sf-01..08 尚未验收;**2026-08-29 已知限制清算第一刀**:Z3 WASM race 根治(`z3-shared.ts` 单一实例+会话级互斥,run-all §1 重试止血退役+§30 并发回归闸)、薄壳遗留(`shell/`)物理删除;**2026-08-29 第二批:OTA 扁平化 + 账号授权闸 + 登录产品化**——飞猪 `search-hotel` 接入(酒店实时报价)、OTA 工具面去「主/降级」路由话术、会话检索授权闸(每会话一次 + 拒绝即会话内吊销)、`gotry_session_login` 登录产品化(登录在携程官网完成,gotry 只读票据名零值过手);全栈回归以 `scripts/run-all-tests.sh` 为准 |
|
|
16
|
+
| **Repo** | [github.com/Danceiny/gotry](https://github.com/Danceiny/gotry) · CI: typecheck + 全栈回归(Node 22/24) |
|
|
17
|
+
| **Runtime** | DeepSeek Harness **0.1.2-alpha.1**(vendored 源码 tarball `ts/dsh-runtime/vendor/`,上游 npm 未发版、从 GitHub tag `dsh-v0.1.2-alpha.1` 构建,溯源见 [`ts/dsh-runtime/vendor/README.md`](ts/dsh-runtime/vendor/README.md);[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
18
|
| **License** | **MIT** (2026-08-23 落定,见 [LICENSE](LICENSE)) |
|
|
17
19
|
|
|
18
20
|
|
|
@@ -47,7 +49,7 @@ npx @danceiny/gotry web # 首跑会提示在当前目录建 .env(LLM_AP
|
|
|
47
49
|
装完即得 :3080 对话界面。已装 dsh 的用户同理——上面的命令会自动以 cordis patch
|
|
48
50
|
挂载 gotry 插件,无需任何额外配置(零成本启动)。
|
|
49
51
|
|
|
50
|
-
> rc.5 为不可用首发(已停止指向)
|
|
52
|
+
> rc.5 为不可用首发(已停止指向);rc.9 装得上跑不起(缺依赖声明,被 rc.10 覆盖);`latest` 为 rc.11——`npx @danceiny/gotry` 即得。
|
|
51
53
|
|
|
52
54
|
### 源码安装(开发者)
|
|
53
55
|
|
|
@@ -60,7 +62,7 @@ cd gotry
|
|
|
60
62
|
|
|
61
63
|
```bash
|
|
62
64
|
cd ts/dsh-runtime
|
|
63
|
-
pnpm install #
|
|
65
|
+
pnpm install # 装 vendored dsh 0.1.2-alpha.1 + 第三方依赖 (tarball 在 vendor/, 溯源见 vendor/README.md)
|
|
64
66
|
cd ../..
|
|
65
67
|
```
|
|
66
68
|
|
|
@@ -117,6 +119,16 @@ gotry 把 **DeepSeek Harness (dsh) 运行时**作为唯一推荐面——任何
|
|
|
117
119
|
|
|
118
120
|
> 一行安装的关键点: `./gotry web`(终端粘贴即用),或 `./gotry "任务"`(一句问答)。**任何自然语言都能用**(这是 dsh runtime 投资的最大价值)。详见 [known limitations](#-known-limitations)。
|
|
119
121
|
|
|
122
|
+
### 可选:启用登录态会话检索
|
|
123
|
+
|
|
124
|
+
会话检索默认只读,并复用用户本人已登录的日常 Chrome。使用前需要 Chrome 144+:
|
|
125
|
+
|
|
126
|
+
1. 在日常 Chrome 打开 `chrome://inspect/#remote-debugging` 并启用 Remote Debugging;
|
|
127
|
+
2. 保持 Chrome 打开;GoTry 发起连接后,在 Chrome 弹出的权限框中确认连接;
|
|
128
|
+
3. 若遇到登录、OTP、验证码或 challenge,由用户本人处理,GoTry 不读取或代填这些信息。
|
|
129
|
+
|
|
130
|
+
**仅打开该页面或启用开关不代表已经 attach 成功。** 必须等权限确认和 CDP 握手完成;在此之前会稳定返回 `needs-attach`,停止并且不消耗执行配额。
|
|
131
|
+
|
|
120
132
|
|
|
121
133
|
---
|
|
122
134
|
|
|
@@ -169,7 +181,7 @@ GoTry: 收到。先把约束记下来——
|
|
|
169
181
|
```
|
|
170
182
|
┌──────────────────────────────────────────────────────────────┐
|
|
171
183
|
│ L1 对话即界面 chat-as-UI; gates 是消息内选择题 │
|
|
172
|
-
│ L2 编排 dsh 运行时 + GoTry 插件(ReAct);
|
|
184
|
+
│ L2 编排 dsh 运行时 + GoTry 插件(ReAct);18 个工具 │
|
|
173
185
|
│ L3 领域 统一行程模型 + Z3 可行性引擎(枚举/Z3 双形态) │
|
|
174
186
|
│ L4 数据 静态数据包 + hotelbyte-cli 实时桥 + OpenFlights 骨架 │
|
|
175
187
|
│ L5 治理 LoopX(objective / gates / evidence / quota) │
|
|
@@ -178,7 +190,7 @@ GoTry: 收到。先把约束记下来——
|
|
|
178
190
|
|
|
179
191
|
| 层 | 模块 | 角色 |
|
|
180
192
|
|---|---|---|
|
|
181
|
-
| L2 | `ts/src/index.ts` (dsh 插件) | 注册
|
|
193
|
+
| L2 | `ts/src/index.ts` (dsh 插件) | 注册 18 个工具,挂 `{{current_date}}`/`{{time_anchor_card}}`/`{{motivation_brief}}` 变量,工具 execute 异常隔离 + 平铺观察 envelope + uncaughtException 护栏;账号会话工具挂授权闸(`tools/pre-execute` → 运行时审批卡,每会话首次请求、拒绝即会话内吊销,`sessionAccess: ask\|allow\|off` 总闸) |
|
|
182
194
|
| L3 | `ts/src/unified.ts` · `py/gotry_feasibility/unified.py` | 唯一求解入口(候选枚举 + 航班链 Z3) |
|
|
183
195
|
| L4 | `ts/capabilities/hbcli.ts` | hotelbyte-cli 进程封装 + 降级到 `data/hotels_2026.json` |
|
|
184
196
|
| L4 | `ts/scripts/skeleton-check.ts` | OpenFlights 168 对枢纽,三值语义(阳性/枢纽对否定≠证伪/枢纽外=无结论) |
|
|
@@ -191,10 +203,12 @@ GoTry: 收到。先把约束记下来——
|
|
|
191
203
|
## ⚠️ Known limitations · 已知限制
|
|
192
204
|
|
|
193
205
|
- ✅ **License MIT** (D-1 落地 2026-08-23)
|
|
194
|
-
-
|
|
195
|
-
-
|
|
196
|
-
-
|
|
197
|
-
-
|
|
206
|
+
- ✅ **英文面(工程层)已就绪** — 求解确定性输出面(候选/航班链 answer_md、放宽建议、排除理由)i18n catalog 化:`GOTRY_LOCALE=en` 或 `setLocale('en')` 即英文,默认 zh-CN 与金标准逐字节一致(run-all §32:en 零缺键/切换数据不动)。**残余(诚实声明)**:dsh web 界面属宿主、工具结果卡与人格对话面等 M4 校准输入落定后随校准样本补齐——种子市场仍是中国出境(G1)
|
|
207
|
+
- ✅ **机票实时数据已接入** — dated 航班链段可经 FlyAI 官方只读通道取实时票价覆写(`GOTRY_REALTIME_PRICING=1`,证据链 `[实时API:flyai@ts]`,miss/error/打码价一律降级回静态包 `data/flights_2026.json`);静态包由「唯一来源」变为「显式降级」,零 key 可用
|
|
208
|
+
- ✅ **酒店实时报价已接入(OTA 平铺)** — `gotry_flyai_search` kind=hotel 直连飞猪官方 `search-hotel`(2026-08-29 实测:结构化 名称/档级/打码价);未鉴权价格上游打码(如 ¥7xx),工具保 priceRaw 原值不伪装成数字价,真实价以 jumpUrl 酒店页为准;OTA 工具面平铺,无「主链路/降级」路由——按查询取用,证据链逐源标注
|
|
209
|
+
- ✅ **账号登录已产品化(第 18 工具)** — `gotry_session_login`:会话检索遇 `needs-login` 时 agent 直接调它,在你的 Chrome 弹出携程登录页、等你在**携程官网**完成登录——全程无需终端。**登录永远发生在外部网站、不在 gotry 里进行;gotry 永不收集/存储/传输密码、验证码或任何 cookie 值,只读「票据 cookie 是否存在」这一个事实(名称级,0 值过手)**。授权闸 v2:每会话首次调用弹审批卡、会话内记住;拒绝 = 本会话吊销;`sessionAccess: ask|allow|off` 总闸随时可关(session-tests §I/§J + smoke §13 验证)
|
|
210
|
+
- ✅ **Z3 WASM race 已根治** — 三模块各自持 WASM 实例 + 候选求解 `Promise.all` 共 Context 并发是双重根因;现收敛 `ts/src/z3-shared.ts`(单一实例+单一 Context+会话级互斥门),run-all §1 的「重试一次」止血已退役,新增 §30 进程内三形态并发回归闸 ×12(D-17)
|
|
211
|
+
- ✅ **薄壳已删除** — `shell/` 目录与 `./gotry shell` 分支已移除(2026-08-29 清理;dsh web 唯一产品面,旧命令行在 git 历史/rc.6 文档内可考)
|
|
198
212
|
|
|
199
213
|
|
|
200
214
|
---
|
|
@@ -225,24 +239,21 @@ GoTry: 收到。先把约束记下来——
|
|
|
225
239
|
异步工单跨进程 · 插件 smoke · hbcli · 进程护栏(含工具异常隔离) · 天气 · 航班 ·
|
|
226
240
|
Anything · probePoi · agent-reach(web/deep/wrapper)· 双路径稳定性 ·
|
|
227
241
|
时间感评测(锚点卡/槽位过期校验/评分器/mock 回放;真模型巡检 `time-eval-tests.ts --real`)·
|
|
228
|
-
记忆域(动机合并守门/效用 sidecar/只读指标投影)
|
|
242
|
+
记忆域(动机合并守门/效用 sidecar/只读指标投影)· **Z3 并发竞态(§30)· 实时票价桥(§31)· i18n 目录(§32)· M3 cohort 证据合同(§33)· M4 价值证据合同(§34)**。
|
|
229
243
|
|
|
230
244
|
---
|
|
231
245
|
|
|
232
246
|
## 🤝 参与开发 — Contributing
|
|
233
247
|
|
|
234
|
-
|
|
248
|
+
> *PR-based flow: branch off the latest `main`, full suite green, open a Pull Request — `main` never takes direct pushes; merge after review. Full guide: [CONTRIBUTING.md](CONTRIBUTING.md).*
|
|
235
249
|
|
|
236
|
-
|
|
237
|
-
./scripts/run-all-tests.sh # 提交前必跑
|
|
250
|
+
标准开源流程:**`main` 不直接推**——从最新 `main` 切出 `feat/ · fix/ · docs/ · chore/` 分支,本地全栈绿后开 Pull Request,CI(Node 22/24,typecheck + 全部套件)与维护者 review 双绿后 squash 合入。**测试红着不许合。**
|
|
238
251
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
cd ts && npx tsx scripts/incident-tests.ts # 进程护栏
|
|
243
|
-
```
|
|
252
|
+
- 完整贡献指南(环境搭建 · 测试 · 分支与提交约定 · PR 流程): **[CONTRIBUTING.md](CONTRIBUTING.md)**
|
|
253
|
+
- Bug / 功能建议:[issue 模板](.github/ISSUE_TEMPLATE/bug_report.yml)(搜过既有 issue 再提)
|
|
254
|
+
- 行为或架构改动先立 ADR(`docs/architecture.md` §8);多 agent 协作契约见 [`AGENTS.md`](AGENTS.md)
|
|
244
255
|
|
|
245
|
-
|
|
256
|
+
单套件快捷命令等测试细节见上面的 [🧪 跑测试 — Verify](#-跑测试--verify)。
|
|
246
257
|
|
|
247
258
|
---
|
|
248
259
|
|
|
@@ -262,6 +273,6 @@ ADR 与技术债见 [`docs/architecture.md` §8 / §10](docs/architecture.md)。
|
|
|
262
273
|
|
|
263
274
|
---
|
|
264
275
|
|
|
265
|
-
**Built with**: DeepSeek Harness 0.1.
|
|
276
|
+
**Built with**: DeepSeek Harness 0.1.2-alpha.1 (vendored) · Cordis · Z3 (WASM) · loopx (pipx) · hotelbyte-cli · Agent-Reach v1.5.0 (`.venv/`) · OpenFlights · TypeScript · Bun
|
|
266
277
|
|
|
267
|
-
**Last verified against `v0.0.1-rc.
|
|
278
|
+
**Last verified against `v0.0.1-rc.11`(已发布,2026-08-29)** — 全栈回归全绿 §1-§32(发布流程见 `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,10 +77,17 @@
|
|
|
77
77
|
evidence=用户原话),不等确认;班次/房型/落脚点等开放性选择不外问
|
|
78
78
|
用户——基于已知动机与约束给出最有说服力的方案并说明理由,用户否决
|
|
79
79
|
才改。校准类疑问永不阻塞推进。
|
|
80
|
-
(19)
|
|
81
|
-
(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
80
|
+
(19)OTA 检索(平铺工具,无预设路由优先级):实时机票/火车用
|
|
81
|
+
gotry_flyai_search(kind=flight|train),实时酒店报价也用它
|
|
82
|
+
(kind=hotel;未鉴权价格多为打码如 ¥7xx,如实以区间呈现并请用户到
|
|
83
|
+
jumpUrl 酒店页核实真实价)。账号会话面 gotry_session_search(用户本人
|
|
84
|
+
登录态,现携程机票):**首次调用**经运行时审批卡向用户请求授权,本会话
|
|
85
|
+
内记住、不再重复弹卡;用户拒绝即本会话吊销——不再请求也不再执行,改走
|
|
86
|
+
其他工具推进;无审批通道(headless)不执行。返回 needs-login→调
|
|
87
|
+
gotry_session_login 产品工具(在用户自己的 Chrome 打开携程登录入口并等待
|
|
88
|
+
其完成登录,不需要任何终端命令;**登录永远发生在携程官网、不在 gotry
|
|
89
|
+
里进行**——gotry 永不收集/存储/传输密码、验证码或任何 cookie 值,只检查
|
|
90
|
+
"是否已登录"这一个事实;向用户说明这一点),完成前用官方源推进,不反复
|
|
91
|
+
开页探测;返回 challenged→立即停手如实告知,绝不重试
|
|
92
|
+
绝不绕过。跨源结论各带来源标注([实时API:flyai@ts]/[会话:ctrip-flight@ts]/
|
|
93
|
+
[静态包:估算]),分桶比对直达/中转,不比裸最低价;证据链随结论必达用户。
|
|
@@ -1,4 +1,44 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
export function parseFlyaiItemList(stdout) {
|
|
3
|
+
let start = -1;
|
|
4
|
+
let depth = 0;
|
|
5
|
+
let inString = false;
|
|
6
|
+
let escaped = false;
|
|
7
|
+
let sawIncompleteObject = false;
|
|
8
|
+
for(let index = 0; index < stdout.length; index += 1){
|
|
9
|
+
const char = stdout[index];
|
|
10
|
+
if (start < 0) {
|
|
11
|
+
if (char === '{') {
|
|
12
|
+
start = index;
|
|
13
|
+
depth = 1;
|
|
14
|
+
inString = false;
|
|
15
|
+
escaped = false;
|
|
16
|
+
sawIncompleteObject = true;
|
|
17
|
+
}
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (inString) {
|
|
21
|
+
if (escaped) escaped = false;
|
|
22
|
+
else if (char === '\\') escaped = true;
|
|
23
|
+
else if (char === '"') inString = false;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (char === '"') inString = true;
|
|
27
|
+
else if (char === '{') depth += 1;
|
|
28
|
+
else if (char === '}') {
|
|
29
|
+
depth -= 1;
|
|
30
|
+
if (depth !== 0) continue;
|
|
31
|
+
const candidate = stdout.slice(start, index + 1);
|
|
32
|
+
start = -1;
|
|
33
|
+
sawIncompleteObject = false;
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(candidate);
|
|
36
|
+
if (Array.isArray(parsed.data?.itemList)) return parsed.data.itemList;
|
|
37
|
+
} catch {}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
throw new Error(sawIncompleteObject ? 'incomplete FlyAI JSON object' : 'no complete FlyAI itemList JSON object');
|
|
41
|
+
}
|
|
2
42
|
function sh(cmd, args, opts) {
|
|
3
43
|
const child = spawn(cmd, args, {
|
|
4
44
|
env: process.env,
|
|
@@ -33,6 +73,7 @@ function sh(cmd, args, opts) {
|
|
|
33
73
|
});
|
|
34
74
|
}).finally(()=>clearTimeout(timer));
|
|
35
75
|
}
|
|
76
|
+
const YMD = /^\d{4}-\d{2}-\d{2}$/;
|
|
36
77
|
export async function flyaiSearch(q) {
|
|
37
78
|
const started = Date.now();
|
|
38
79
|
const ts = new Date().toISOString();
|
|
@@ -40,29 +81,56 @@ export async function flyaiSearch(q) {
|
|
|
40
81
|
kind: q.kind,
|
|
41
82
|
latencyMs: 0
|
|
42
83
|
};
|
|
43
|
-
const
|
|
44
|
-
const destination = (q.destination ?? '').trim();
|
|
45
|
-
if (!origin || !destination || !/^\d{4}-\d{2}-\d{2}$/.test(q.depDate ?? '')) {
|
|
46
|
-
return {
|
|
84
|
+
const argErr = (error)=>({
|
|
47
85
|
...base,
|
|
48
86
|
ok: false,
|
|
49
87
|
via: 'flyai-error',
|
|
50
88
|
verdict: 'error',
|
|
51
89
|
evidence: `[实时API:flyai@error@${ts}] bad args`,
|
|
52
|
-
error
|
|
53
|
-
};
|
|
90
|
+
error
|
|
91
|
+
});
|
|
92
|
+
let cliArgs;
|
|
93
|
+
if (q.kind === 'hotel') {
|
|
94
|
+
const dest = (q.destName ?? q.destination ?? '').trim();
|
|
95
|
+
if (!dest) return argErr('hotel 需要 to/destName(目的地中文)');
|
|
96
|
+
const badPair = (q.checkInDate ? 1 : 0) !== (q.checkOutDate ? 1 : 0) || q.checkInDate && q.checkOutDate && (!YMD.test(q.checkInDate) || !YMD.test(q.checkOutDate));
|
|
97
|
+
if (badPair) return argErr('checkInDate/checkOutDate 须成对且为 YYYY-MM-DD(未定档期可不带日期先摸底)');
|
|
98
|
+
cliArgs = [
|
|
99
|
+
'search-hotel',
|
|
100
|
+
'--dest-name',
|
|
101
|
+
dest,
|
|
102
|
+
...q.checkInDate && q.checkOutDate ? [
|
|
103
|
+
'--check-in-date',
|
|
104
|
+
q.checkInDate,
|
|
105
|
+
'--check-out-date',
|
|
106
|
+
q.checkOutDate
|
|
107
|
+
] : [],
|
|
108
|
+
...q.keyWords ? [
|
|
109
|
+
'--key-words',
|
|
110
|
+
q.keyWords
|
|
111
|
+
] : []
|
|
112
|
+
];
|
|
113
|
+
} else {
|
|
114
|
+
const origin = (q.origin ?? '').trim();
|
|
115
|
+
const destination = (q.destination ?? '').trim();
|
|
116
|
+
if (!origin || !destination || !YMD.test(q.depDate ?? '')) {
|
|
117
|
+
return argErr('origin/destination/depDate(YYYY-MM-DD) required');
|
|
118
|
+
}
|
|
119
|
+
const sub = q.kind === 'flight' ? 'search-flight' : 'search-train';
|
|
120
|
+
cliArgs = [
|
|
121
|
+
sub,
|
|
122
|
+
'--origin',
|
|
123
|
+
origin,
|
|
124
|
+
'--destination',
|
|
125
|
+
destination,
|
|
126
|
+
'--dep-date',
|
|
127
|
+
q.depDate ?? ''
|
|
128
|
+
];
|
|
54
129
|
}
|
|
55
|
-
const sub = q.kind === 'flight' ? 'search-flight' : 'search-train';
|
|
56
130
|
const r = await sh(q.cliBin ?? 'npx', [
|
|
57
131
|
'-y',
|
|
58
132
|
'@fly-ai/flyai-cli',
|
|
59
|
-
|
|
60
|
-
'--origin',
|
|
61
|
-
origin,
|
|
62
|
-
'--destination',
|
|
63
|
-
destination,
|
|
64
|
-
'--dep-date',
|
|
65
|
-
q.depDate
|
|
133
|
+
...cliArgs
|
|
66
134
|
], {
|
|
67
135
|
timeoutMs: q.timeoutMs ?? 30_000
|
|
68
136
|
});
|
|
@@ -80,21 +148,46 @@ export async function flyaiSearch(q) {
|
|
|
80
148
|
}
|
|
81
149
|
let items;
|
|
82
150
|
try {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
items = parsed.data?.itemList ?? [];
|
|
86
|
-
} catch {
|
|
151
|
+
items = parseFlyaiItemList(r.stdout);
|
|
152
|
+
} catch (e) {
|
|
87
153
|
const raw = r.stdout.replace(/\s+/g, ' ').slice(0, 160);
|
|
154
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
88
155
|
return {
|
|
89
156
|
...base,
|
|
90
157
|
latencyMs,
|
|
91
158
|
ok: false,
|
|
92
159
|
via: 'flyai-error',
|
|
93
160
|
verdict: 'error',
|
|
94
|
-
evidence: `[实时API:flyai@error@${ts}] parse failed: ${raw}`,
|
|
95
|
-
error: `failed to parse flyai output as JSON: ${raw}`
|
|
161
|
+
evidence: `[实时API:flyai@error@${ts}] parse failed(${reason}): ${raw}`,
|
|
162
|
+
error: `failed to parse flyai output as JSON (${reason}): ${raw}`
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (q.kind === 'hotel') {
|
|
166
|
+
const hotels = parseHotelItems(items);
|
|
167
|
+
const verdict = hotels.length > 0 ? 'hit' : 'miss';
|
|
168
|
+
return {
|
|
169
|
+
kind: q.kind,
|
|
170
|
+
latencyMs,
|
|
171
|
+
ok: true,
|
|
172
|
+
via: 'flyai',
|
|
173
|
+
verdict,
|
|
174
|
+
evidence: `[实时API:flyai@${ts}] ${hotels.length}/${items.length} hotel options`,
|
|
175
|
+
hotels
|
|
96
176
|
};
|
|
97
177
|
}
|
|
178
|
+
const options = parseTransportItems(items);
|
|
179
|
+
const verdict = options.length > 0 ? 'hit' : 'miss';
|
|
180
|
+
return {
|
|
181
|
+
kind: q.kind,
|
|
182
|
+
latencyMs,
|
|
183
|
+
ok: true,
|
|
184
|
+
via: 'flyai',
|
|
185
|
+
verdict,
|
|
186
|
+
evidence: `[实时API:flyai@${ts}] ${options.length}/${items.length} ${q.kind} options`,
|
|
187
|
+
options
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function parseTransportItems(items) {
|
|
98
191
|
const options = [];
|
|
99
192
|
for (const it of items){
|
|
100
193
|
const seg = it.journeys?.[0]?.segments?.[0];
|
|
@@ -115,16 +208,28 @@ export async function flyaiSearch(q) {
|
|
|
115
208
|
jumpUrl: it.jumpUrl
|
|
116
209
|
});
|
|
117
210
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
211
|
+
return options;
|
|
212
|
+
}
|
|
213
|
+
function parseHotelItems(items) {
|
|
214
|
+
const hotels = [];
|
|
215
|
+
for (const it of items){
|
|
216
|
+
if (!it.name) continue;
|
|
217
|
+
const rawPrice = String(it.price ?? '');
|
|
218
|
+
const bare = rawPrice.replace(/^[¥]/, '');
|
|
219
|
+
const numericPrice = Number(bare);
|
|
220
|
+
hotels.push({
|
|
221
|
+
name: it.name,
|
|
222
|
+
star: it.star,
|
|
223
|
+
price: /^\d+(\.\d+)?$/.test(bare) && numericPrice > 0 ? numericPrice : 0,
|
|
224
|
+
priceRaw: rawPrice || undefined,
|
|
225
|
+
rate: it.rate != null ? String(it.rate) : undefined,
|
|
226
|
+
address: it.address,
|
|
227
|
+
poi: it.interestsPoi,
|
|
228
|
+
hotelId: it.shId,
|
|
229
|
+
jumpUrl: it.detailUrl
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return hotels;
|
|
128
233
|
}
|
|
129
234
|
|
|
130
235
|
|
|
@@ -115,10 +115,22 @@ export async function searchHotels(query, opts = {}) {
|
|
|
115
115
|
try {
|
|
116
116
|
const { readFile } = await import('node:fs/promises');
|
|
117
117
|
const pack = JSON.parse(await readFile(fallback, 'utf-8'));
|
|
118
|
+
const kw = query.destination.trim();
|
|
119
|
+
const stays = Array.isArray(pack.stays) ? pack.stays : [];
|
|
120
|
+
const matched = kw ? stays.filter((s)=>JSON.stringify(s).includes(kw)) : stays;
|
|
121
|
+
if (matched.length) {
|
|
122
|
+
return {
|
|
123
|
+
...live,
|
|
124
|
+
hotels: {
|
|
125
|
+
stays: matched
|
|
126
|
+
},
|
|
127
|
+
summary: `${query.destination}:hbcli 不可用(${live.error ?? live.via}),降级到静态包,命中 ${matched.length} 个住宿块`
|
|
128
|
+
};
|
|
129
|
+
}
|
|
118
130
|
return {
|
|
119
131
|
...live,
|
|
120
|
-
hotels:
|
|
121
|
-
summary: `${query.destination}:hbcli 不可用(${live.error ?? live.via})
|
|
132
|
+
hotels: null,
|
|
133
|
+
summary: `${query.destination}:hbcli 不可用(${live.error ?? live.via}),且静态包无「${query.destination}」住宿数据(静态包仅覆盖内置场景)`
|
|
122
134
|
};
|
|
123
135
|
} catch {}
|
|
124
136
|
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
export const SESSION_BENCHMARK_SCHEMA_VERSION = 'session-double-source.v1';
|
|
2
|
+
export const SESSION_FIELD_ACCURACY_THRESHOLD = 0.9;
|
|
3
|
+
export const REQUIRED_COMPARABLE_FIELDS = [
|
|
4
|
+
'query_id',
|
|
5
|
+
'route_segments',
|
|
6
|
+
'journey_type',
|
|
7
|
+
'route_segments[].departure_at',
|
|
8
|
+
'route_segments[].arrival_at',
|
|
9
|
+
'route_segments[].transport_number',
|
|
10
|
+
'currency',
|
|
11
|
+
'price',
|
|
12
|
+
'source',
|
|
13
|
+
'fetched_at',
|
|
14
|
+
'verdict'
|
|
15
|
+
];
|
|
16
|
+
function isMissing(value) {
|
|
17
|
+
return value === undefined || value === null || typeof value === 'string' && value.trim() === '';
|
|
18
|
+
}
|
|
19
|
+
function sameValue(left, right) {
|
|
20
|
+
if (typeof left !== typeof right) return false;
|
|
21
|
+
return typeof left === 'number' && typeof right === 'number' ? Object.is(left, right) : left === right;
|
|
22
|
+
}
|
|
23
|
+
function sameComparison(row) {
|
|
24
|
+
if (/\.(?:departure_at|arrival_at)$/.test(row.path) && typeof row.expected === 'string' && typeof row.actual === 'string') {
|
|
25
|
+
const expectedAt = Date.parse(row.expected);
|
|
26
|
+
const actualAt = Date.parse(row.actual);
|
|
27
|
+
if (Number.isFinite(expectedAt) && Number.isFinite(actualAt)) return expectedAt === actualAt;
|
|
28
|
+
}
|
|
29
|
+
return sameValue(row.expected, row.actual);
|
|
30
|
+
}
|
|
31
|
+
function fixtureComparisons(expected, actual) {
|
|
32
|
+
const rows = [
|
|
33
|
+
{
|
|
34
|
+
path: 'query_id',
|
|
35
|
+
expected: expected.query_id,
|
|
36
|
+
actual: actual.query_id
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
path: 'route_segments.length',
|
|
40
|
+
expected: expected.route_segments.length,
|
|
41
|
+
actual: actual.route_segments?.length
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
path: 'journey_type',
|
|
45
|
+
expected: expected.journey_type,
|
|
46
|
+
actual: actual.journey_type
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
path: 'currency',
|
|
50
|
+
expected: expected.currency,
|
|
51
|
+
actual: actual.currency
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
path: 'price',
|
|
55
|
+
expected: expected.price,
|
|
56
|
+
actual: actual.price
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
path: 'source',
|
|
60
|
+
expected: expected.source,
|
|
61
|
+
actual: actual.source
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
path: 'fetched_at',
|
|
65
|
+
expected: expected.fetched_at,
|
|
66
|
+
actual: actual.fetched_at
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
path: 'verdict',
|
|
70
|
+
expected: expected.verdict,
|
|
71
|
+
actual: actual.verdict
|
|
72
|
+
}
|
|
73
|
+
];
|
|
74
|
+
for(let index = 0; index < expected.route_segments.length; index += 1){
|
|
75
|
+
const exp = expected.route_segments[index];
|
|
76
|
+
const act = actual.route_segments?.[index];
|
|
77
|
+
rows.push({
|
|
78
|
+
path: `route_segments[${index}].from`,
|
|
79
|
+
expected: exp.from,
|
|
80
|
+
actual: act?.from
|
|
81
|
+
}, {
|
|
82
|
+
path: `route_segments[${index}].to`,
|
|
83
|
+
expected: exp.to,
|
|
84
|
+
actual: act?.to
|
|
85
|
+
}, {
|
|
86
|
+
path: `route_segments[${index}].departure_at`,
|
|
87
|
+
expected: exp.departure_at,
|
|
88
|
+
actual: act?.departure_at
|
|
89
|
+
}, {
|
|
90
|
+
path: `route_segments[${index}].arrival_at`,
|
|
91
|
+
expected: exp.arrival_at,
|
|
92
|
+
actual: act?.arrival_at
|
|
93
|
+
}, {
|
|
94
|
+
path: `route_segments[${index}].transport_number`,
|
|
95
|
+
expected: exp.transport_number,
|
|
96
|
+
actual: act?.transport_number
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return rows;
|
|
100
|
+
}
|
|
101
|
+
export function scoreSessionFixture(expected, actual, threshold = SESSION_FIELD_ACCURACY_THRESHOLD) {
|
|
102
|
+
const rows = fixtureComparisons(expected, actual);
|
|
103
|
+
const fixtureErrors = [
|
|
104
|
+
...rows.filter((row)=>isMissing(row.expected)).map((row)=>row.path),
|
|
105
|
+
...requiredMissing(expected)
|
|
106
|
+
];
|
|
107
|
+
const missing = [];
|
|
108
|
+
const incorrect = [];
|
|
109
|
+
let correct = 0;
|
|
110
|
+
for (const row of rows){
|
|
111
|
+
if (isMissing(row.actual)) {
|
|
112
|
+
missing.push(row.path);
|
|
113
|
+
} else if (!sameValue(row.expected, row.actual)) {
|
|
114
|
+
incorrect.push(row.path);
|
|
115
|
+
} else {
|
|
116
|
+
correct += 1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const total = rows.length;
|
|
120
|
+
const accuracy = total > 0 ? correct / total : 0;
|
|
121
|
+
return {
|
|
122
|
+
pass: fixtureErrors.length === 0 && accuracy >= threshold,
|
|
123
|
+
threshold,
|
|
124
|
+
correct,
|
|
125
|
+
total,
|
|
126
|
+
accuracy,
|
|
127
|
+
missing,
|
|
128
|
+
incorrect,
|
|
129
|
+
fixture_errors: [
|
|
130
|
+
...new Set(fixtureErrors)
|
|
131
|
+
]
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function isIsoTimestamp(value) {
|
|
135
|
+
return value.trim() !== '' && Number.isFinite(Date.parse(value));
|
|
136
|
+
}
|
|
137
|
+
function requiredMissing(record) {
|
|
138
|
+
const missing = [];
|
|
139
|
+
if (!record.query_id.trim()) missing.push('query_id');
|
|
140
|
+
if (record.route_segments.length === 0) missing.push('route_segments');
|
|
141
|
+
if (!record.currency.trim()) missing.push('currency');
|
|
142
|
+
if (!(record.price > 0)) missing.push('price');
|
|
143
|
+
if (!record.source.trim()) missing.push('source');
|
|
144
|
+
if (!isIsoTimestamp(record.fetched_at)) missing.push('fetched_at');
|
|
145
|
+
if (record.journey_type === 'direct' && record.route_segments.length !== 1) missing.push('journey_type/route_segments');
|
|
146
|
+
if (record.journey_type === 'transfer' && record.route_segments.length < 2) missing.push('journey_type/route_segments');
|
|
147
|
+
for(let index = 0; index < record.route_segments.length; index += 1){
|
|
148
|
+
const segment = record.route_segments[index];
|
|
149
|
+
if (!segment.from.trim()) missing.push(`route_segments[${index}].from`);
|
|
150
|
+
if (!segment.to.trim()) missing.push(`route_segments[${index}].to`);
|
|
151
|
+
if (!isIsoTimestamp(segment.departure_at)) missing.push(`route_segments[${index}].departure_at`);
|
|
152
|
+
if (!isIsoTimestamp(segment.arrival_at)) missing.push(`route_segments[${index}].arrival_at`);
|
|
153
|
+
if (!segment.transport_number.trim()) missing.push(`route_segments[${index}].transport_number`);
|
|
154
|
+
}
|
|
155
|
+
return missing;
|
|
156
|
+
}
|
|
157
|
+
function baseEvaluation(state, quotaDisposition, extra = {}) {
|
|
158
|
+
return {
|
|
159
|
+
state,
|
|
160
|
+
retry_allowed: false,
|
|
161
|
+
quota_disposition: quotaDisposition,
|
|
162
|
+
mismatches: extra.mismatches ?? [],
|
|
163
|
+
missing: extra.missing ?? [],
|
|
164
|
+
...extra.price_delta === undefined ? {} : {
|
|
165
|
+
price_delta: extra.price_delta
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function alignmentComparisons(official, session) {
|
|
170
|
+
const rows = [
|
|
171
|
+
{
|
|
172
|
+
path: 'query_id',
|
|
173
|
+
expected: official.query_id,
|
|
174
|
+
actual: session.query_id
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
path: 'route_segments.length',
|
|
178
|
+
expected: official.route_segments.length,
|
|
179
|
+
actual: session.route_segments.length
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
path: 'journey_type',
|
|
183
|
+
expected: official.journey_type,
|
|
184
|
+
actual: session.journey_type
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
path: 'currency',
|
|
188
|
+
expected: official.currency,
|
|
189
|
+
actual: session.currency
|
|
190
|
+
}
|
|
191
|
+
];
|
|
192
|
+
const comparableSegments = Math.min(official.route_segments.length, session.route_segments.length);
|
|
193
|
+
for(let index = 0; index < comparableSegments; index += 1){
|
|
194
|
+
const exp = official.route_segments[index];
|
|
195
|
+
const act = session.route_segments[index];
|
|
196
|
+
rows.push({
|
|
197
|
+
path: `route_segments[${index}].from`,
|
|
198
|
+
expected: exp.from,
|
|
199
|
+
actual: act.from
|
|
200
|
+
}, {
|
|
201
|
+
path: `route_segments[${index}].to`,
|
|
202
|
+
expected: exp.to,
|
|
203
|
+
actual: act.to
|
|
204
|
+
}, {
|
|
205
|
+
path: `route_segments[${index}].departure_at`,
|
|
206
|
+
expected: exp.departure_at,
|
|
207
|
+
actual: act.departure_at
|
|
208
|
+
}, {
|
|
209
|
+
path: `route_segments[${index}].arrival_at`,
|
|
210
|
+
expected: exp.arrival_at,
|
|
211
|
+
actual: act.arrival_at
|
|
212
|
+
}, {
|
|
213
|
+
path: `route_segments[${index}].transport_number`,
|
|
214
|
+
expected: exp.transport_number,
|
|
215
|
+
actual: act.transport_number
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
return rows;
|
|
219
|
+
}
|
|
220
|
+
export function evaluateDoubleSource(input) {
|
|
221
|
+
const official = input.official;
|
|
222
|
+
const session = input.session;
|
|
223
|
+
if (session?.verdict === 'challenged' || official?.verdict === 'challenged') {
|
|
224
|
+
return baseEvaluation('challenge_stop', 'no_spend_stop');
|
|
225
|
+
}
|
|
226
|
+
if ((session?.read_guard_blocked ?? 0) !== 0 || (official?.read_guard_blocked ?? 0) !== 0) {
|
|
227
|
+
return baseEvaluation('guard_violation', 'no_spend_stop');
|
|
228
|
+
}
|
|
229
|
+
if (!session) return baseEvaluation('source_unavailable', 'no_spend_stop');
|
|
230
|
+
if (session.verdict === 'needs-attach') return baseEvaluation('waiting_attach', 'no_spend_waiting_user');
|
|
231
|
+
if (session.verdict === 'needs-login') return baseEvaluation('waiting_login', 'no_spend_waiting_user');
|
|
232
|
+
if (!official || official.verdict !== 'hit' || session.verdict !== 'hit') {
|
|
233
|
+
return baseEvaluation('source_unavailable', 'no_spend_stop');
|
|
234
|
+
}
|
|
235
|
+
const missing = [
|
|
236
|
+
...requiredMissing(official).map((path)=>`official.${path}`),
|
|
237
|
+
...requiredMissing(session).map((path)=>`session.${path}`)
|
|
238
|
+
];
|
|
239
|
+
if (missing.length > 0) {
|
|
240
|
+
return baseEvaluation('invalid_contract', 'no_spend_stop', {
|
|
241
|
+
missing
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
const mismatches = alignmentComparisons(official, session).filter((row)=>!sameComparison(row)).map((row)=>row.path);
|
|
245
|
+
return baseEvaluation(mismatches.length === 0 ? 'comparable' : 'divergent', 'evidence_ready', {
|
|
246
|
+
mismatches,
|
|
247
|
+
price_delta: session.price - official.price
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/capabilities/session/benchmark.ts
|