旅迹:把一句旅行愿望,变成真正可编辑的行程
一个由 AI 对话驱动,同时保留结构化编辑、版本管理与人工确认的旅行规划工作台。
旅行攻略从来不缺,但把散落在攻略、地图、天气、车票和酒店页面里的信息,整理成一份真正能执行的逐日计划,依然很费时间。
这也是我做「旅迹」的起点:我希望用户只需要告诉 AI“想去哪、什么时候去、几个人、预算多少、喜欢什么”,就能得到一份清晰的行程;同时,它又不能只是一次性生成一大段文字,而应该允许继续修改、锁定重要安排、查看地图与天气,并在出发前随时调整。
- 在线体验:https://lvji.liuyuan.top/
- 项目源码:GitHub - lvji-travel
- 开源协议:Apache License 2.0

它不只是“让 AI 写一篇攻略”
旅迹的核心是一份可以持续操作的结构化行程。目的地、日期、人数、预算和旅行偏好是规划输入;每天的景点、交通、住宿、用餐、时间与费用则会落到具体安排中。
用户既可以手动编辑,也可以直接对 AI 说:
第二天别排得太满,把博物馆换到上午,晚上想吃当地菜,人均控制在 150 元以内。
AI 会理解这次修改,但不会立刻覆盖原计划。所有变更先进入预览,用户确认后才会正式应用。对于旅行计划这种会不断调整、又可能包含重要预订的内容,这一步很有必要。
flowchart LR
A["输入目的地、日期、预算与偏好"] --> B["AI 生成结构化方案"]
B --> C["预览本次变更"]
C -->|确认| D["写入逐日行程"]
C -->|修订| E["继续对话调整"]
E --> B
D --> F["地图、天气、日历与外部工具"]
目前项目主要包含这些能力:
- 根据目的地、日期、人数、预算和偏好生成行程;
- 按天管理景点、交通、住宿、餐饮、时间和费用;
- 通过自然语言继续确认、修订、取消或重试规划;
- 展示地图与天气信息,辅助判断路线和出行条件;
- 将带有明确日期和时间的安排导出到日历;
- 接入 12306、搜索、酒店、机票等 MCP Server;
- 保存版本快照,保护已经锁定的重要安排;
- 隔离不同用户的数据与服务凭证。
从“能生成”到“敢修改”
这次开发中,我花了不少精力处理 AI 产品里不太显眼、却很影响可靠性的部分。
1. 所有修改都经过 preview → apply
AI 和手工操作最终走同一套变更流程。系统先计算并展示修改结果,再由用户确认写入。这样既能降低误操作,也让 AI 的行为更容易理解。
2. 用 revision 处理并发冲突
每次提交都要携带当前行程的 revision。如果用户在另一个页面或请求中已经更新过行程,旧版本的操作会收到 HTTP 409,而不是悄悄覆盖新内容。
3. 用 idempotencyKey 避免重复提交
网络重试、连续点击或任务恢复,都可能让同一操作被发送多次。幂等键可以确保一次修改只生效一次。
4. 锁定真正不能动的安排
已经买好的车票、订好的酒店不应该被 AI 随意挪动。旅迹允许锁定安排,锁定后不能修改、移动或删除;较重要的变更还会生成版本快照,方便恢复。
调度器:先判断用户想做什么,再决定调用哪条链路
对话式产品有一个很容易被忽略的问题:用户发来的每句话,并不都意味着“重新生成行程”。
例如,“酒店多少钱?”只是一个问题;“把第二天的景点换掉”是在修订方案;“不用了,就这样吧”在存在待确认方案时通常表示接受现有方案;而“不要这个方案”才是在拒绝方案。如果把这些表达全部交给同一个生成接口,很容易出现答非所问或误写入行程。
旅迹为此增加了一层对话调度器。它把判断拆成两个互相独立的维度:
pendingPlanDecision:用户对待确认方案的态度,可能是accept、revise、reject或undecided;requestKind:用户除此以外想做的事,可能是普通问答answer、规划plan或没有额外请求none。
随后,程序再把这两个判断映射成三种动作:
| 判断结果 | 调度动作 | 后续行为 |
|---|---|---|
| 接受待确认方案 | apply |
把方案送入结构化生成与应用流程 |
| 创建或修订计划 | plan |
生成新的候选方案,等待用户确认 |
| 普通问题或尚未决定 | reply |
只回复消息,不改动行程 |
flowchart TD
M["用户消息"] --> P{"存在待确认方案?"}
P --> D["确定性规则"]
D -->|高置信度命中| A{"apply / plan / reply"}
D -->|表达含糊| S["语义模型分析"]
S --> A
A -->|apply| F["结构化生成与变更预览"]
A -->|plan| C["生成候选方案"]
A -->|reply| R["仅回答,不写入行程"]
这里采用的是“确定性规则优先,语义模型兜底”的混合方式。像“确认”“就按这个”“不要这个方案”“把行程调整一下”这类高置信度控制语句,会直接在本地完成判断,响应更快,也更可预测;问题、否定和转折较复杂时,才交给模型结合待确认方案和最近几轮对话做语义分析。
调度器还会尝试从最近完成的 AI 任务中找回尚未应用的候选方案。这样即使前端没有继续携带 pendingPlan,用户随后说“就这样吧”,系统仍有机会恢复正确的上下文。同时,代码会检查较长回复是否具有“完整行程”“逐日行程”、多个 Day 标题和确认提示等特征,避免一份实际的规划结果被误当成普通聊天消息。
AI 任务本身也是一个可恢复的状态机
确定动作之后,耗时的 AI 与 MCP 工作不会挤在一次黑盒请求中完成,而是由任务调度器分阶段推进:
flowchart LR
Q["queued"] --> D["discovering_mcp"]
D --> P["planning_tool_calls"]
P -->|需要实时数据| T["calling_mcp"]
T --> C["composing_itinerary"]
C -->|继续查询| T
C -->|输出合格| V["ready_for_review"]
C -->|结构不合格| X["repairing_response"]
X --> V
在正式应用已确认方案时,还会先经过 preparing_format,读取当前日期、既有安排和锁定项目。工具发现完成后,模型可以发起一轮或多轮 MCP 调用;每次调用的状态、耗时和错误都会形成可展示的活动记录。最终输出必须通过结构与内容校验,如果 JSON 被截断、为空或不符合可执行格式,任务会进入 repairing_response 做一次严格纠错。
每个阶段都使用数据库状态进行抢占:只有 queued 任务能原子地变为 running,因此重复请求不会同时执行同一阶段。阶段失败会自动重试两次,仍然失败才把任务标为 failed。前端因此可以展示真实进度和工具轨迹,任务也能在短暂网络故障后从当前阶段继续,而不是每次都从头生成。
用 MCP 把旅行信息接进来
单靠语言模型无法保证实时车次、酒店价格、天气或搜索结果准确,因此旅迹把外部能力设计成可扩展的 MCP 工具。
当前可以配置 12306、SearXNG、高德、Tavily、RollingGo 酒店与机票等 Streamable HTTP MCP Server。应用负责工具发现、调用和轨迹记录,模型则根据规划任务选择合适的工具。
flowchart TB
U["用户"] --> UI["旅迹工作台"]
UI --> AI["OpenAI-compatible 模型"]
AI --> G["MCP Gateway"]
G --> T1["12306"]
G --> T2["搜索服务"]
G --> T3["高德地图"]
G --> T4["酒店与机票"]
UI --> DB["PostgreSQL"]
允许用户填写外部 Server 地址也带来了安全问题。项目中的 MCP Gateway 会限制目标为公开 HTTPS 地址,阻止回环、私网、链路本地和云元数据地址,同时控制重定向次数、请求超时与响应体积。AI/MCP 凭证只在服务端解密和发送,列表接口只展示掩码,尽量缩小 SSRF 与密钥泄露风险。
技术选型
Web 主分支目前采用以下技术栈:
| 部分 | 技术 |
|---|---|
| 前端 | Next.js 16、React 19、TypeScript 5.9 |
| UI | Tailwind CSS 4、Lucide React、React Markdown |
| 服务端 | Next.js App Router / Route Handlers、Node.js 22 |
| 数据 | PostgreSQL 18、Drizzle ORM |
| AI | OpenAI-compatible API |
| 外部工具 | MCP Streamable HTTP、高德地图 |
| 校验与测试 | Zod 4、ESLint 9、Node.js Test Runner |
| 部署 | Docker、Docker Compose |
代码按职责拆分:app/ 负责页面、组件与 API Routes,lib/ai/ 处理 AI 规划和任务分发,lib/mcp/ 实现 MCP 注册、网关、安全和审计,lib/trips/ 则集中管理行程操作规则。数据库 Schema 与迁移分别位于 db/ 和 drizzle-pg/。
顺带聊聊 Electron 分支
除了 Web 版,仓库还保留了一个独立的 drfccv/electron-local 分支,用来维护 Windows Electron 单机版。
它并不是把网页直接塞进一个窗口这么简单。桌面版复用了现有 React 界面和 Trips、AI Jobs、MCP、天气、版本等业务能力,同时增加了 Electron Main、preload、Renderer transport、本地数据库、备份恢复和安装包构建链路。
| Web 主分支 | Electron 本地分支 |
|---|---|
| Next.js 服务端运行 | Electron Main 进程承载本地运行时 |
| PostgreSQL 18 | better-sqlite3 + Drizzle |
| 数据保存在服务器 | 数据保存在 %APPDATA%\Lvji\data\trip-planner.db |
| 服务端加密保存凭证 | Electron safeStorage 绑定当前 Windows 账户加密 |
| Docker / Node 服务部署 | electron-builder 生成 Windows NSIS 安装程序 |
| 适合在线访问和多用户 | 适合个人离线管理本地行程数据 |
Electron 侧采用了尽量收敛的权限边界:
flowchart LR
R["Renderer:React UI"] -->|"白名单 IPC + Zod 校验"| P["Preload"]
P --> M["Electron Main"]
M --> API["本地 Route Dispatcher"]
API --> S["SQLite + Drizzle"]
M --> SS["safeStorage"]
窗口启用了 contextIsolation: true、nodeIntegration: false 和 sandbox: true,并设置 CSP;Renderer 只能通过最小化的 preload 白名单调用可信 Main 进程。生产版本默认关闭 DevTools,外部访问限制为经过允许的 HTTPS 域名,MCP 仍沿用原有的 SSRF 防护。
本地数据库会在启动时自动执行迁移,并启用外键、WAL 和 busy timeout。普通 JSON 备份不会包含 AI/MCP 凭证;导入前会先校验格式,并在数据目录中保留当前数据库副本。安装后的应用已经包含 Electron 与 SQLite 运行时,普通用户无需再安装 Node.js、pnpm 或 Wrangler。
当然,桌面分支目前仍有一些明确的限制:没有配置自动更新服务;普通备份不能迁移凭证,换电脑后需要重新填写;AI、地图、天气与 MCP 仍依赖网络和用户自己的服务配置;正式分发前也还需要准备代码签名证书。
如果你想体验或参与桌面版开发,可以执行:
git switch drfccv/electron-local
pnpm install
pnpm desktop:dev
打包 Windows 安装程序:
pnpm desktop:package
完成后,NSIS 安装程序会出现在 release 目录。
本地运行 Web 版
环境要求是 Node.js 22.13+、pnpm,以及一个 PostgreSQL 数据库。
git clone https://github.com/drfccv/lvji-travel.git
cd lvji-travel
pnpm install
cp .env.example .env
pnpm db:migrate
pnpm dev
Windows PowerShell 中可以把复制配置文件的命令换成:
Copy-Item .env.example .env
启动后访问 http://127.0.0.1:4173。至少需要在 .env 中配置 DATABASE_URL;AI、高德和 MCP 能力则可以按需填写。请不要把真实 API Key 写进 .env.example 或提交到仓库。
提交代码前,我通常会运行:
pnpm test
pnpm lint
pnpm build
写在最后
旅迹想解决的并不是“让 AI 多写一篇攻略”,而是如何把模型生成、实时工具和用户确认组合成一套可信、可编辑、可恢复的旅行规划流程。
Web 版让它可以随时访问,Electron 分支则尝试把数据和运行时留在用户自己的电脑里。两条路线面向不同场景,但共享同一个目标:让 AI 负责繁琐的信息整理,同时让最终决定始终掌握在旅行者手中。
如果这个项目对你有帮助,欢迎在 GitHub 提交 Issue、参与改进,或者分享你希望加入的旅行规划能力。