@danceiny/gotry 0.0.1-rc.11 → 0.0.1-rc.13

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 (37) hide show
  1. package/README.md +111 -146
  2. package/cordis.gotry-patch.yml +14 -7
  3. package/dist/capabilities/flyai.js +135 -30
  4. package/dist/capabilities/hbcli.js +14 -2
  5. package/dist/capabilities/session/benchmark.js +252 -0
  6. package/dist/capabilities/session/transport.js +25 -10
  7. package/dist/capabilities/session-consent.js +82 -0
  8. package/dist/capabilities/session-login.js +119 -0
  9. package/dist/capabilities/session-search.js +7 -4
  10. package/dist/capabilities/weather.js +72 -17
  11. package/dist/scripts/agent-reach-wrapper-tests.js +4 -0
  12. package/dist/scripts/async-collect.js +33 -5
  13. package/dist/scripts/hbcli-tests.js +25 -4
  14. package/dist/scripts/ledger-tests.js +134 -4
  15. package/dist/scripts/memory-value-report.js +379 -0
  16. package/dist/scripts/product-metrics.js +569 -0
  17. package/dist/scripts/session-benchmark.js +261 -0
  18. package/dist/scripts/session-login.js +28 -0
  19. package/dist/scripts/session-tests.js +262 -23
  20. package/dist/scripts/smoke.js +112 -14
  21. package/dist/scripts/weather-tests.js +8 -1
  22. package/dist/src/index.js +124 -5
  23. package/dist/src/loop.js +65 -14
  24. package/dist/src/state-ledger.js +30 -4
  25. package/package.json +4 -2
  26. package/ts/capabilities/flyai.ts +177 -22
  27. package/ts/capabilities/hbcli.ts +16 -4
  28. package/ts/capabilities/session/benchmark.ts +273 -0
  29. package/ts/capabilities/session/transport.ts +41 -9
  30. package/ts/capabilities/session-consent.ts +127 -0
  31. package/ts/capabilities/session-login.ts +146 -0
  32. package/ts/capabilities/session-search.ts +13 -5
  33. package/ts/capabilities/weather.ts +84 -19
  34. package/ts/scripts/async-collect.ts +48 -7
  35. package/ts/src/index.ts +107 -11
  36. package/ts/src/loop.ts +85 -12
  37. package/ts/src/state-ledger.ts +48 -4
package/README.md CHANGED
@@ -2,161 +2,89 @@
2
2
 
3
3
  [![CI](https://github.com/Danceiny/gotry/actions/workflows/ci.yml/badge.svg)](https://github.com/Danceiny/gotry/actions/workflows/ci.yml)
4
4
 
5
- > **身体和灵魂,更多旅行,更少旅游。**
5
+ > **身体和灵魂,更多旅行,更少旅游。**
6
6
  > *Body and soul — more travel, less tourism.*
7
7
 
8
- **GoTry 是「从出发到下一次出发」的 AI 旅行 Agent**:你用一句话说想去哪、为什么想出发;它先问清楚你的工作时间和已订资源,再用**数学求解器**给你一份经过验证的行程方案。
8
+ **GoTry 是「从出发到下一次出发」的 AI 旅行 Agent**:你用一句话说想去哪、为什么想出发;它先问清楚你的工作时间和已订资源,再用**数学求解器**给你一份经过验证的行程方案。
9
9
 
10
10
  **GoTry is an AI travel agent for departure-to-next-departure.** Tell it where you want to go and why. It asks what's missing, then hands you a **formally verified itinerary** — not vibes.
11
11
 
12
- | | |
13
- |---|---|
14
- | **Version** | `v0.0.1-rc.11`(npm **latest 直指本版**,`npx @danceiny/gotry` 即得;[release notes](docs/release-notes.md)) |
15
- | **Status** | M4 记忆域推进中(2026-08-28):记忆写读闭环 + 效用 sidecar + 「下一次出发」0::1 召回;17 工具;事务化状态账本(ADR-15)+ 双形态架构冻结(ADR-16:本地+Web 一套账本语义,tenant_id 一等字段);**2026-08-29 已知限制清算第一刀**:Z3 WASM race 根治(`z3-shared.ts` 单一实例+会话级互斥,run-all §1 重试止血退役+§30 并发回归闸)、薄壳遗留(`shell/`)物理删除;全栈回归全绿(套件清单见 `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.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`) |
18
- | **License** | **MIT** (2026-08-23 落定,见 [LICENSE](LICENSE)) |
19
-
20
-
21
- ---
22
-
23
- ## ✨ 它做什么 — What it does
24
-
25
- GoTry 把「想去哪」变成「能不能、怎么去」:
26
-
27
- | 阶段 | AI/Engine | 输什么 |
28
- |---|---|---|
29
- | **动机访谈** | LLM | 必问项:工作窗口 / 已订资源 / 出发城市 |
30
- | **事实抽取** | LLM | 工作窗口生效 + 休假语义识别 |
31
- | **可行性判定** | **Z3** | 哪些候选目的地**可行**、哪些**不可行**、为什么、**如何最小化改动让它变可行** |
32
- | **门到门全成本** | 求解器 | 真实飞行时长(含时差)+ 醒来起夜惩罚 + 接驳代价 + 到达精力 % |
33
- | **证据链** | 渲染层 | 每个数字标 `[骨架:openflights]`、`[实时API:hbcli]`、`[静态包:估算]` |
34
-
35
- **不像普通 AI 聊天**——LLM 只做理解和解释,**判定与算术是数学求解器**算的。
36
-
37
- > **Unlike a regular AI chat**, the LLM only translates and explains. **Decisions and arithmetic are computed by a Z3 solver**, not guessed.
38
-
39
12
  ---
40
13
 
41
- ## 🚀 快速开始 — Quick start
42
-
43
- ### 一行启动(npm,推荐)
14
+ ## 🚀 30 秒上手
44
15
 
45
16
  ```bash
46
- npx @danceiny/gotry web # 首跑会提示在当前目录建 .env(LLM_API_KEY=<DeepSeek key>)
17
+ npx @danceiny/gotry web
18
+ # 首跑会提示建 .env:LLM_API_KEY=<DeepSeek key 或 OpenAI 兼容 key>
19
+ # → 浏览器打开 http://127.0.0.1:3080,像聊天一样说「我想去大理三天」
47
20
  ```
48
21
 
49
- 装完即得 :3080 对话界面。已装 dsh 的用户同理——上面的命令会自动以 cordis patch
50
- 挂载 gotry 插件,无需任何额外配置(零成本启动)。
51
-
52
- > rc.5 为不可用首发(已停止指向);rc.9 装得上跑不起(缺依赖声明,被 rc.10 覆盖);`latest` 为 rc.11——`npx @danceiny/gotry` 即得。
53
-
54
- ### 源码安装(开发者)
55
-
56
- ```bash
57
- git clone https://github.com/Danceiny/gotry
58
- cd gotry
59
- ```
60
-
61
- #### A. 装 vendored dsh runtime(一次性,约 1 分钟)
62
-
63
- ```bash
64
- cd ts/dsh-runtime
65
- pnpm install # 拉 @deepseek-ai/dsh 0.1.1-rc.2 + 依赖(实测 51 秒)
66
- cd ../..
67
- ```
68
-
69
- > 为什么这一步:dsh runtime 是 vendored 的(`ts/dsh-runtime/`)——
70
- > 避免 npm 一键分发时把整个 dsh 也打包进去。
71
- > 之后可以重复使用,无需重装。
72
-
73
- #### B. 环境前置 — Prerequisites
22
+ | 你想要的 | 命令 |
23
+ |---|---|
24
+ | 🖥️ 对话规划(推荐) | `npx @danceiny/gotry web` → :3080 对话界面 |
25
+ | 🤖 脚本/一次性问答 | `npx @danceiny/gotry "我想从深圳休整两天,预算 3000"` |
26
+ | 🛠️ 开发者:仓内运行 | 见下方[源码安装](#-快速开始) |
74
27
 
75
- - **Node 22+**(`nvm install 22` 或更高)
76
- - 一个 LLM API key(默认走 DeepSeek,也支持 OpenAI 兼容协议)
28
+ - 前置:Node 22+;一个 LLM API key。零成本启动——dsh 运行时以 cordis patch 自动挂载,无额外配置。
29
+ - 例外提示:`:3080` 端口被占时先 `kill <PID>`;首启 6–15 秒属正常冷启动;异常退出会留证据到 `gotry-state/incidents.jsonl`(不静默)
77
30
 
78
- #### C. 配 — Configure
31
+ <details>
32
+ <summary>🛠️ 开发者:源码安装(仓内运行)</summary>
79
33
 
80
34
  ```bash
81
- cp .env.example .env
82
- # LLM_API_KEY(deepseek: sk-...; 也兼容 OpenAI/Anthropic 兼容协议)
35
+ git clone https://github.com/Danceiny/gotry && cd gotry
36
+ cd ts/dsh-runtime && pnpm install && cd ../.. # ① vendored dsh 0.1.2-alpha.1(一次性)
37
+ cp .env.example .env # ② 填 LLM_API_KEY
38
+ ./gotry web # ③ 仓内入口,同 npm 形态
83
39
  ```
84
40
 
85
- #### D. Run(仓内入口,开发者)
86
-
87
- gotry **DeepSeek Harness (dsh) 运行时**作为唯一推荐面——任何自然语言输入在那里面都能跟 GoTry 正常对话(自动调用引擎、追问缺失字段、给出证据链)。
88
-
89
- 启动方式有三种,对应三种使用场景:
90
-
91
- | 入口 | 命令 | 什么时候用 | 给你什么 |
92
- |---|---|---|---|
93
- | **① dsh Web 对话框**(推荐) | `./gotry web` | 第一天上手;持续多轮规划;想看推理过程可视化 | `http://127.0.0.1:3080` 浏览器界面;带 GoTry 人格的对话框 |
94
- | **② headless 一次性问答** | `./gotry "一句完整任务..."` | 写脚本、CI 自动化、定向 LLM 调试 | stdout 输出 markdown 回答 |
95
- | **③ help** | `./gotry help` | 不知道用法 | 三行帮助 |
41
+ | 入口 | 命令 | 什么时候用 |
42
+ |---|---|---|
43
+ | dsh Web 对话(推荐) | `./gotry web` | 持续多轮规划,看推理可视化 → :3080 |
44
+ | headless 一问一答 | `./gotry "一句任务"` | 脚本 / CI / 定向调试 → stdout |
45
+ | help | `./gotry help` | 三行帮助 |
96
46
 
97
- ```bash
98
- # 推荐:打开对话框,正常打「我想去大理三天」,看引擎回答
99
- ./gotry web
100
- # → http://127.0.0.1:3080,在输入框里像跟 DeepSeek 对话一样打字
47
+ 为什么源码模式要先装 runtime:dsh 以 vendored tarball 进 git(`ts/dsh-runtime/`),npm 一键分发不打包它;源码检出读 `.ts` 源码,装完一次重复可用。
101
48
 
102
- # 调试/自动化:直接一问一答,无浏览器
103
- ./gotry "我想从深圳休整两天,预算 3000"
104
- # → stdout 拿到引擎判定 + 证据链 + 候选 gate
49
+ </details>
105
50
 
106
- # 不知道用法
107
- ./gotry help
108
- ```
51
+ ---
109
52
 
110
- > ⚠️ **dsh 运行时端口冲突**:如果你之前已经手动 `node ...dsh web` 跑过,而后再 `./gotry web`,会因 `:3080` 占用启动失败。要么先 `kill <PID>`,要么直接用已经在跑的那个。
111
- >
112
- > **dsh 启动耗时**:第一次 cold start 6–15 秒(cordis 组合 + plugin 加载 + LLM 准备);之后热启动 < 2 秒。
113
- >
114
- > **headless 报错兜底**:意外退出(exit ≠ 0 / signal)会 fsync 一条记录到 `gotry-state/incidents.jsonl`,作为事故证据而不是沉默(D-NEW 进程护栏)。
53
+ ## 它做什么 What it does
115
54
 
116
- <br>
55
+ GoTry 把「想去哪」变成「能不能、怎么去」:
117
56
 
118
- *One-liner mental model: `npx @danceiny/gotry@rc web` + `.env` 一行 key — done。dsh 对话框是唯一产品面(旧 shell 前端已删除)。*
57
+ | 阶段 | 由谁做 | 给你什么 |
58
+ |---|---|---|
59
+ | **动机访谈** | LLM | 必问项:工作窗口 / 已订资源 / 出发城市 |
60
+ | **事实抽取** | LLM | 工作窗口生效 + 休假语义识别 |
61
+ | **可行性判定** | **Z3 求解器** | 哪些候选可行、哪些不可行、为什么、**最小改动让它可行** |
62
+ | **门到门全成本** | 求解器 | 真实飞行时长(含时差)+ 醒来起夜惩罚 + 接驳代价 + 到达精力 % |
63
+ | **证据链** | 渲染层 | 每个数字带标签:`[骨架:openflights]` / `[实时API:flyai]` / `[静态包:估算]` |
119
64
 
120
- > 一行安装的关键点: `./gotry web`(终端粘贴即用),或 `./gotry "任务"`(一句问答)。**任何自然语言都能用**(这是 dsh runtime 投资的最大价值)。详见 [known limitations](#-known-limitations)。
65
+ **不像普通 AI 聊天**——LLM 只做理解和解释,**判定与算术是数学求解器**算的。
121
66
 
67
+ > **Unlike a regular AI chat**, the LLM only translates and explains. **Decisions and arithmetic are computed by a Z3 solver**, not guessed.
122
68
 
123
69
  ---
124
70
 
125
71
  ## 🎬 一段对话 — Demo
126
72
 
127
- 输入 → 引擎判定 → 输出(真实截图,monospace 保留原味):
73
+ 输入 → 引擎判定 → 输出(真实输出,milliseconds 保留原味):
128
74
 
129
75
  ```
130
- > 我想去洱海边发呆两三天,上海出发,预算 3000,年假别让我办公。
76
+ > 我想去洱海边发呆两三天,上海出发,预算 3000,年假别让我办公。
131
77
 
132
78
  GoTry: 收到。先把约束记下来——
133
- • 窗口: 2 天(2026 年窗口)
134
- 出发: 上海
135
- • 预算: ¥3000 全含
136
- • 动机: 休整 / [escape_rest: 0.7]
137
- • 不要求工作窗口(年假)
138
- • 还没订任何东西
79
+ • 窗口: 2 天(2026 年窗口) • 出发: 上海 • 预算: ¥3000 全含
80
+ 动机: 休整 / [escape_rest: 0.7] • 还没订任何东西
139
81
 
140
82
  引擎判定:
141
-
142
- **大理·洱海: 现在不行** —— 冲突约束: duration。
143
- 你的 2 天窗口装不下「至少 5 天的洱海休整」(你之前的画像也有此约)
144
- 放宽方案: 把行程延长到 5 天,约 ¥4950。
145
- ★已放入「下一次出发」清单: 需 5 天起,3-5 / 9-11 月最佳。
146
-
147
- **千岛湖: 可行** (G7315 06:35 出发,bus 接驳,
148
- ¥996,起床 06:35,到达精力 84%,门到门 5h20m,
149
- 有效休整 4.4h)
150
- **太湖: 可行** (G101 09:00 出发,
151
- ¥716,起床 07:12,到达精力 84%,
152
- 有效休整 4.6h)
153
-
83
+ **大理·洱海: 现在不行** —— 2 天窗口装不下「至少 5 天的洱海休整」。
84
+ 放宽方案: 行程延长到 5 天,约 ¥4950。★已放入「下一次出发」清单。
85
+ **千岛湖: 可行** (G7315 06:35 出发,¥996,到达精力 84%,有效休整 4.4h)
86
+ **太湖: 可行** (G101 09:00 出发,¥716,有效休整 4.6h)
154
87
  建议: 千岛湖(意象匹配 80%)。
155
- 备选: 太湖(¥716,匹配 60%)。
156
-
157
- **待你决定**:
158
- 1. 千岛湖还是太湖?(前者更贴意象,后者更省)
159
- 2. 出发班次选 G7315(06:35)还是更晚的?
160
88
 
161
89
  [骨架:openflights] ✓ SZX↔PVG 已校验 [实时API:hbcli] 上海机场在跑
162
90
  [静态包:估算] G7315/G7316 价格按 7-8 月淡季估算
@@ -166,12 +94,45 @@ GoTry: 收到。先把约束记下来——
166
94
 
167
95
  ---
168
96
 
97
+ ## 🧰 18 个工具 — Tools
98
+
99
+ | 组 | 工具 | 干什么 |
100
+ |---|---|---|
101
+ | **实时检索(OTA/官方只读)** | `gotry_flyai_search` | 机票/火车/酒店实时报价(飞猪官方通道;酒店价格为上游打码展示,真实价以 jumpUrl 为准) |
102
+ | | `gotry_session_search` | 在**用户本人登录态**里查携程机票(授权后,只读) |
103
+ | | `gotry_session_login` | 登录引导:自动检测已登录与否,未登录才在用户 Chrome 弹登录入口(**零终端**) |
104
+ | | `gotry_weather_check` | Open-Meteo 预报≤16 天 + 历史气候基线 |
105
+ | | `gotry_flight_verify` | OpenSky ADS-B 航班实时观测(三值) |
106
+ | | `gotry_skeleton_check` | OpenFlights 168 对枢纽通航性(三值) |
107
+ | **库存与目录** | `gotry_hotel_search` | hotel-byte 实时桥,降级静态包(证据标注) |
108
+ | | `gotry_anything_search` | 城市/酒店/地标混合目录(hotel-be Anything) |
109
+ | **判定引擎** | `gotry_feasibility_check` | 门到门真成本可行性(Z3),逐候选判定 |
110
+ | **记忆与触达** | `gotry_motivation_save` | 动机画像落盘(evidence 强制,反幻觉) |
111
+ | | `gotry_wish_pool_add` / `gotry_wish_pool_list` | 「下一次出发」愿望池 + 0..1 条件召回 |
112
+ | | `gotry_companion_save` · `gotry_trip_log` | 同行人档案 / 旅行时间线 |
113
+ | **通用外部** | `gotry_web_search` · `gotry_video_subtitle` · `gotry_github_search` · `gotry_agent_reach` | 网页/字幕/GitHub/全渠道外部信息(经 Agent-Reach) |
114
+
115
+ ---
116
+
117
+ ## 🔐 账号会话:授权与隐私 — Account consent
118
+
119
+ 会话检索使用**你本人已登录的 Chrome**读取酒店/机票实时数据,为此立了四条 hard 规则:
120
+
121
+ 1. **登录在外部网站完成** —— gotry 从不提供、不代填、不收集任何密码/验证码/cookie 值。它只回答一个布尔问题:"登录票据 cookie 存在吗"(只读**名字**,0 值过手)。
122
+ 2. **授权卡,每会话一次** —— 首次动用账号会话会弹运行时审批卡;批准后会话内记住,拒绝即本会话吊销,不再打扰。总闸 `sessionAccess: ask|allow|off` 随时可关。
123
+ 3. **物理只读** —— ReadGuard 在网络层中止一切写请求(下单/支付在传输层不可达),agent 永不接触凭证与验证码;遇到验证码立即停,交还给你。
124
+ 4. **绝不劫持你的浏览器** —— 检索/登录只开自己的独立标签页,登录页置前台、留在你那;例行动测试永不自动开浏览器窗。
125
+
126
+ > 前置(一次性):日常 Chrome 打开 `chrome://inspect/#remote-debugging` 打开开关(Chrome 144+),并保持 Chrome 在线。未开启时工具返回 `needs-attach` 并给出指引,不消耗执行配额。
127
+
128
+ ---
129
+
169
130
  ## 🏛️ 架构 — Architecture
170
131
 
171
132
  ```
172
133
  ┌──────────────────────────────────────────────────────────────┐
173
134
  │ L1 对话即界面 chat-as-UI; gates 是消息内选择题 │
174
- │ L2 编排 dsh 运行时 + GoTry 插件(ReAct);13 个工具 │
135
+ │ L2 编排 dsh 运行时 + GoTry 插件(ReAct);18 个工具 │
175
136
  │ L3 领域 统一行程模型 + Z3 可行性引擎(枚举/Z3 双形态) │
176
137
  │ L4 数据 静态数据包 + hotelbyte-cli 实时桥 + OpenFlights 骨架 │
177
138
  │ L5 治理 LoopX(objective / gates / evidence / quota) │
@@ -180,24 +141,37 @@ GoTry: 收到。先把约束记下来——
180
141
 
181
142
  | 层 | 模块 | 角色 |
182
143
  |---|---|---|
183
- | L2 | `ts/src/index.ts` (dsh 插件) | 注册 13 个工具,挂 `{{current_date}}`/`{{time_anchor_card}}`/`{{motivation_brief}}` 变量,工具 execute 异常隔离 + 平铺观察 envelope + uncaughtException 护栏 |
184
- | L3 | `ts/src/unified.ts` · `py/gotry_feasibility/unified.py` | 唯一求解入口(候选枚举 + 航班链 Z3) |
185
- | L4 | `ts/capabilities/hbcli.ts` | hotelbyte-cli 进程封装 + 降级到 `data/hotels_2026.json` |
186
- | L4 | `ts/scripts/skeleton-check.ts` | OpenFlights 168 对枢纽,三值语义(阳性/枢纽对否定≠证伪/枢纽外=无结论) |
187
-
188
- > 📖 完整 ADR / 演进阶段 / 债务清单见 [`docs/architecture.md`](docs/architecture.md)。
144
+ | L2 | `ts/src/index.ts`(dsh 插件) | 注册 18 工具,挂时间锚点/记忆 brief 变量;execute 异常隔离 + 授权闸 + 进程护栏 |
145
+ | L3 | `ts/src/unified.ts` · `py/gotry_feasibility/` | 唯一求解入口(候选枚举 + 航班链 Z3) |
146
+ | L4 | `ts/capabilities/hbcli.ts` · `skeleton-check.ts` | 实时库存桥 + OpenFlights 骨架(三值语义) |
147
+ | L5 | loopx 治理面 | objective / gates / evidence / quota |
189
148
 
149
+ > 📖 完整 ADR / 演进 / 债务清单: [`docs/architecture.md`](docs/architecture.md)
190
150
 
191
151
  ---
192
152
 
193
- ## ⚠️ Known limitations · 已知限制
153
+ ## ⚠️ 状态与限制 Status & limitations
154
+
155
+ **已就绪(全绿可验)**:
194
156
 
195
- - ✅ **License MIT** (D-1 落地 2026-08-23)
196
- - ✅ **英文面(工程层)已就绪** — 求解确定性输出面(候选/航班链 answer_md、放宽建议、排除理由)i18n catalog 化:`GOTRY_LOCALE=en` 或 `setLocale('en')` 即英文,默认 zh-CN 与金标准逐字节一致(run-all §32:en 零缺键/切换数据不动)。**残余(诚实声明)**:dsh web 界面属宿主、工具结果卡与人格对话面等 M4 校准输入落定后随校准样本补齐——种子市场仍是中国出境(G1)
197
- - ✅ **机票实时数据已接入** — dated 航班链段可经 FlyAI 官方只读通道取实时票价覆写(`GOTRY_REALTIME_PRICING=1`,证据链 `[实时API:flyai@ts]`,miss/error/打码价一律降级回静态包 `data/flights_2026.json`);静态包由「唯一来源」变为「显式降级」,零 key 可用
198
- - ✅ **Z3 WASM race 已根治** — 三模块各自持 WASM 实例 + 候选求解 `Promise.all` 共 Context 并发是双重根因;现收敛 `ts/src/z3-shared.ts`(单一实例+单一 Context+会话级互斥门),run-all §1 的「重试一次」止血已退役,新增 §30 进程内三形态并发回归闸 ×12(D-17)
199
- - ✅ **薄壳已删除** — `shell/` 目录与 `./gotry shell` 分支已移除(2026-08-29 清理;dsh web 唯一产品面,旧命令行在 git 历史/rc.6 文档内可考)
157
+ - ✅ **Z3 WASM race 已根治**(`z3-shared.ts` 单实例+会话级互斥;run-all §30 并发回归闸)
158
+ - ✅ **实时机票/酒店/会话检索**已接入:FlyAI 官方通道(机/火/酒;`GOTRY_REALTIME_PRICING=1` 可选把实时票价覆写进求解)
159
+ - ✅ **英文面(工程层)**:`GOTRY_LOCALE=en` 一键切换,en 零缺键,金标准行为不变
160
+ - ✅ **账号会话:授权闸 + 登录产品化 + 自动检测**(见上方 [🔐 账号会话](#-账号会话授权与隐私--account-consent))
161
+ - ✅ License MIT;薄壳遗留已删除(dsh web 唯一产品面)
200
162
 
163
+ **未收口(诚实清单)**:
164
+
165
+ - ⏳ **M3 Exit 未关闭** —— 工程与分发面就绪,但真实种子用户 evidence(50–200 人 cohort)未积累;合成 fixture 只证明合同不证明 business pass
166
+ - ⏳ **中文之外的界面残余** —— dsh web 界面属宿主;工具结果卡与人格对话面的英文校准件挂 M4 校准样本
167
+ - ⏳ **携程酒店/美团会话适配器** —— 机票已通,酒店会话面等登录态实测回填(下一 tick)
168
+
169
+ <details>
170
+ <summary>📖 展开完整状态表(账本/证据合同/里程碑口径)</summary>
171
+
172
+ 事务化状态账本(ADR-15,`gotry_async_terminal.v1`:4/4→`succeeded`/ledger `settled`/exit 0,非 4/4→`failed`/`failed`/exit 2,终态复诵零重算);双形态架构冻结(ADR-16:本地+Web 一套账本语义,tenant_id 一等字段);会话数据面 #21 的字段 fixture scorer、双源合同与 waiting-attach no-spend 已进确定性回归,真实 sf-01..08 尚未验收;里程碑口径(2026-08-29):M3 工程与分发面已就绪但真实种子用户 evidence 未收口,M4 由 founder 授权并行推进不构成 M3 Exit 证明,M5/M6 仅在各自 Entry gate 满足后启动——细则见 [`docs/roadmap.md`](docs/roadmap.md) 与 [`docs/architecture.md`](docs/architecture.md)。
173
+
174
+ </details>
201
175
 
202
176
  ---
203
177
 
@@ -214,7 +188,6 @@ GoTry: 收到。先把约束记下来——
214
188
  | [`docs/kimi-postmortem.md`](docs/kimi-postmortem.md) | 所有人:真实 AI 旅行规划失败复盘(反面教材) |
215
189
  | [`docs/release-notes.md`](docs/release-notes.md) | 历史:每个 tag 的发布闸勾稽 |
216
190
 
217
-
218
191
  ---
219
192
 
220
193
  ## 🧪 跑测试 — Verify
@@ -223,11 +196,7 @@ GoTry: 收到。先把约束记下来——
223
196
  ./scripts/run-all-tests.sh
224
197
  ```
225
198
 
226
- 全栈一次性绿(纯 TS,无 Python 依赖):engine/journey/unified 金标准 · 对话重放 ·
227
- 异步工单跨进程 · 插件 smoke · hbcli · 进程护栏(含工具异常隔离) · 天气 · 航班 ·
228
- Anything · probePoi · agent-reach(web/deep/wrapper)· 双路径稳定性 ·
229
- 时间感评测(锚点卡/槽位过期校验/评分器/mock 回放;真模型巡检 `time-eval-tests.ts --real`)·
230
- 记忆域(动机合并守门/效用 sidecar/只读指标投影)· **Z3 并发竞态(§30)· 实时票价桥(§31)· i18n 目录(§32)**。
199
+ 全栈一次性绿(纯 TS,无 Python 依赖):engine/journey/unified 金标准 · 对话重放 · 异步工单跨进程 · 插件 smoke · hbcli · 进程护栏(含工具异常隔离)· 天气 · 航班 · Anything · probePoi · agent-reach(web/deep/wrapper)· 双路径稳定性 · 时间感评测(锚点卡/槽位过期校验/评分器/mock 回放;真模型巡检 `time-eval-tests.ts --real`)· 记忆域(动机合并守门/效用 sidecar/只读指标投影)· **Z3 并发竞态(§30)· 实时票价桥(§31)· i18n 目录(§32)· M3 cohort 证据合同(§33)· M4 价值证据合同(§34)**。
231
200
 
232
201
  ---
233
202
 
@@ -235,21 +204,17 @@ Anything · probePoi · agent-reach(web/deep/wrapper)· 双路径稳定性 ·
235
204
 
236
205
  > *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).*
237
206
 
238
- 标准开源流程:**`main` 不直接推**——从最新 `main` 切出 `feat/ · fix/ · docs/ · chore/` 分支,本地全栈绿后开 Pull Request,CINode 22/24,typecheck + 全部套件)与维护者 review 双绿后 squash 合入。**测试红着不许合。**
207
+ 标准开源流程:**`main` 不直接推**——从最新 `main` 切出 `feat/ · fix/ · docs/ · chore/` 分支,本地全栈绿后开 Pull Request,CI(Node 22/24,typecheck + 全部套件)与维护者 review 双绿后 squash 合入。**测试红着不许合。**
239
208
 
240
209
  - 完整贡献指南(环境搭建 · 测试 · 分支与提交约定 · PR 流程): **[CONTRIBUTING.md](CONTRIBUTING.md)**
241
210
  - Bug / 功能建议:[issue 模板](.github/ISSUE_TEMPLATE/bug_report.yml)(搜过既有 issue 再提)
242
211
  - 行为或架构改动先立 ADR(`docs/architecture.md` §8);多 agent 协作契约见 [`AGENTS.md`](AGENTS.md)
243
212
 
244
- 单套件快捷命令等测试细节见上面的 [🧪 跑测试 — Verify](#-跑测试--verify)。
245
-
246
213
  ---
247
214
 
248
215
  ## 📜 License
249
216
 
250
- **MIT**(2026-08-23 落定)。与上游 dshMIT)/ loopx 一致——宽松、可商用、可闭源分叉。
251
-
252
- 文本见 [LICENSE](LICENSE)。
217
+ **MIT**(2026-08-23 落定)。与上游 dsh(MIT)/ loopx 一致——宽松、可商用、可闭源分叉。文本见 [LICENSE](LICENSE)。
253
218
 
254
219
  ---
255
220
 
@@ -257,10 +222,10 @@ Anything · probePoi · agent-reach(web/deep/wrapper)· 双路径稳定性 ·
257
222
 
258
223
  - **English section summaries** are inline above (italic blockquotes).
259
224
  - 本 README 主语言是中文,面向中国出境首发种子用户群。
260
- - 完整英文版 README 计划在 v0.1.0 同步([issue 路线](#-known-limitations))。
225
+ - 完整英文版 README 计划在 v0.1.0 同步。
261
226
 
262
227
  ---
263
228
 
264
- **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
229
+ **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
265
230
 
266
- **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))
231
+ **Last verified against `v0.0.1-rc.13`(已发布,2026-08-29)** — 全栈回归全绿 §1-§34(发布流程见 `scripts/publish-npm.sh`)。
@@ -77,10 +77,17 @@
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
- 绝不绕过。[会话:*] 证据链随结论必达用户。
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 产品工具(先自动检测票据——已登录则直接确认零弹窗;未登录才
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 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 {
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: 'origin/destination/depDate(YYYY-MM-DD) required'
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
- sub,
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
- 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 {
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
- 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
- };
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: pack,
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
  }