内容
# 🚶 闲逛者 — 您的巴黎出行助手
[](#license)



> 为 **Mistral "Vibe" 黑客马拉松** 而建。 🌐 **在线演示:** [`idfm-mcp.onrender.com/mcp`](https://idfm-mcp.onrender.com/mcp) · [健康检查](https://idfm-mcp.onrender.com/healthz)
闲逛者可以将任何 AI 助手变成 **巴黎地区的出行助手**。给它两个地址,它就能实时规划最佳公共交通路线 — 考虑道路施工和事故 — 然后结合骑行路线、下一班车信息和天气。让您的助手回答真正重要的问题:
> *"到达那里最好的方式是什么,我能准时到达吗?"* — 不仅仅是 *"时刻表是什么?"*
它由 **两个相互补充的部分** 组成:
| 部分 | 描述 | 位置 |
|---|---|---|
| 🔌 **MCP 服务器** | 引擎。6个工具,任何MCP客户端都可以调用(Mistral Vibe、Claude等)。支持多用户,部署,HTTPS。 | [`src/flaneur/`](src/flaneur/) |
| 🧠 **智能代理** | 大脑。将这些工具编排成一个 *"带我准时到达"* 的工作流程,配合Google日历。 | [`skills/itinerary/`](skills/itinerary/SKILL.MD) |
---
## ✨ 有趣的特点
- **实时,不只是时刻表。** 每一段旅程都包含影响它的道路施工/事故,每条路线都报告其实时状态(`NO_SERVICE`、`SIGNIFICANT_DELAYS`等) — 从IDFM的PRIM API获取。
- **日历原生。** 连接 **Mistral Vibe中的Google日历连接器**,闲逛者会围绕您的 *实际* 日程规划:它读取您的下一个会议地址和时间,告诉您确切的出发时间,并可以规划连续会议之间的每一段旅程 — 标记任何两个会议之间太近而无法旅行的情况。无需复制粘贴地址。
- **天气感知模式选择。** 不会让您在雨中骑自行车。闲逛者会检查 *沿着您的路线* 的天气预报,并在 `自动` 模式下,当天气(或中断)不利时,会让您改乘其他交通工具 — *"8:10 出发,乘地铁:到 8:30 会下雨,8 号线也延迟了。"* 一个推荐来自交通 + 骑行 + 天气 + 中断,而不是四个选项卡让您自己协调。
- **自带密钥,多用户。** 每个用户在请求时传递自己的API密钥在HTTP头中;不会存储任何内容,因此一个部署可以安全地为所有人提供服务。
- **主动式,而不是反应式。** 将其连接到 **定时任务** 进行晨间简报和实时中断警报 — 请参见 [用例](#-use-cases)。
---
## 🎯 用例
### ⭐ 晨间简报和中断警报(Mistral定时任务)
主要用例。连接闲逛者到 **Mistral Vibe**,然后创建一个 **定时任务**,让您的助手在 *您醒来之前* 进行思考:
> *"每个工作日 7:30,使用闲逛者:计算从 `24 Rue Traversière, Paris` 到 `2 Rue des Mathurins, Paris` 的最佳出发时间,以便在 9:00 准时到达。如果我的线路有中断或要下雨,请告诉我 — 并建议骑自行车或替代路线。否则只需发送出发时间。"*
每天早上,您都会收到一条可行的消息:
> 🚇 *8:14 出发。8 号线今天早上有明显延误 — 改乘 87 路公交车(8:56 到达)。☔ 预计有小雨,记得带上外套。*
相同的模式支持 **实时警报**("如果我的常用线路在 6 点前中断,请通知我")和 **晚间规划**("音乐会结束后如何回家,天气适合步行吗?")。
### 其他解锁功能
- **为您决定骑自行车还是地铁** — 比较两者并考虑天气因素。
- **在繁忙的一天中 *"我是否准时?"* ** — 技能扫描您的日历并规划会议之间的行程,标记任何两个会议之间太近而无法旅行的情况。
- **自动调度通勤** — 检测日历事件之间的位置变化,并将旅行时间块直接添加到Google日历中。
- **按需规划到达时间** — "我想骑自行车,在 14:00 之前到达" 返回确切的出发时间,包括缓冲时间。
> 🔗 **日历感知答案:** 启用 **Mistral Vibe中的Google日历连接器** 以解锁上述所有读取您日程的功能 — "我什么时候离开下一个会议?",会议间行程和冲突警告。闲逛者将连接器(您的日程)与自己的工具(路线,干扰,天气)结合起来。
> 💡 **提示 — 在Mistral Libraries中保存您的默认设置。** 闲逛者在知道您的 **家庭地址**、**主要工作地址** 和 **首选交通方式** (例如 "如果距离小于 30 分钟,则骑自行车,否则乘地铁")时变得更加有用。在 **Mistral Vibe的Library中** 添加这些设置,以便自动重用它们 — 您的晨间简报然后变成一个一句话的命令( *"我什么时候应该离开去上班?"* )而无需重新输入地址。`flaneur-itinerary` 技能在规划时恰好读取这些字段(`user_home`、`user_work`,首选模式)。
---
## 🔌 MCP 服务器
八个工具,可从任何MCP客户端调用:
| 工具 | 描述 |
|---|---|
| `mobility_advice(origin, destination, when?, arrive_by?)` | **一次调用 → 一个推荐。** 融合交通 + 骑行 + 天气 + 中断,并选择最佳方式,并附有人类总结。 |
| `plan_journey(origin, destination, when?, arrive_by?, max_journeys?, wheelchair?, max_transfers?)` | 最佳实时公交路线(含中断和费用)。 |
| `line_traffic(line?)` | 交通信息(道路施工/事故):全网或单线。 |
| `next_departures(stop, limit?)` | 实时获取某个站点下一班车的出发时间。 |
| `bike_route(origin, destination, profile?)` | 骑行路线(时长 + 距离),考虑交通灯。 |
| `velib_nearby(location, limit?)` | 最近的 Vélib' 站点,实时显示自行车(机械/电动)和停车位。 |
| `weather(location, days?)` | 当前天气 + 指定地址的每日预报。 |
| `geocode_address(query)` | 地址或地名 → 坐标。 |
`origin` / `destination` 可以接受一个 **地址**("29 rue de Rivoli, Paris"),一个 **站点名称**("Châtelet"),或 **坐标**(`longitude;latitude`)。所有时间均返回为时区感知 Europe/Paris ISO 8601。
**数据源:** [PRIM](https://prim.iledefrance-mobilites.fr)(IDFM — Navitia 行程 + SIRI 实时),法国国家地理编码器(Géoplateforme),BRouter(骑行),Vélib' Métropole(自行车可用性),和 OpenWeatherMap(天气)。
## 🧠 技能:`flaneur-itinerary`
MCP 工具各自回答一个问题;技能([`skills/itinerary/SKILL.MD`](skills/itinerary/SKILL.MD))将它们转化为一个决策:
- 选择模式(`public_transport` / `bike` / `walk` / `auto`)— 对于 `auto`,在天气允许的情况下,短途骑行,否则使用公共交通。
- 计算 **推荐出发时间** 以达到目标到达时间,带有可配置的缓冲时间,**适用于所有模式**(自行车/步行出发时间根据目标到达时间反向计算)。
- 结合中断,检查骑行/步行路线上的天气,并交叉引用 **Google 日历** 以获取附近的约会和行程冲突。
- 扫描日期范围以自动规划 **会议间行程**,标记任何两个会议之间太近而无法旅行的情况。
它使用此服务器的工具以及连接的 Google 日历 MCP。在 **Mistral Vibe** 上,相同的逻辑直接表示为定时任务提示(请参见 [用例](#-use-cases));在 **Claude Code / 代理** 中,加载技能文件。
---
## 🚀 试用
> **在线端点:** `https://idfm-mcp.onrender.com/mcp` — 健康检查:[`/healthz`](https://idfm-mcp.onrender.com/healthz)
**将托管的服务器连接到MCP客户端**(Mistral Vibe,Claude,...)— 将其指向在线 `/mcp` 端点,并将您的密钥作为头部传递:
```json
{
"mcpServers": {
"flaneur": {
"url": "https://idfm-mcp.onrender.com/mcp",
"headers": {
"X-PRIM-Api-Key": "您的PRIM令牌",
"X-OpenWeather-Api-Key": "您的OPENWEATHER密钥"
}
}
}
}
```
> ℹ️ 这是一个免费的Render实例,因此在安静了一段时间后,第一个请求可能需要 ~30-60 秒才能唤醒,然后就会很快。
然后询问: *"从埃菲尔铁塔到香榭丽舍宫的最佳方式是什么?"* 或 *"从巴士底到拉德芳斯骑自行车需要多长时间,天气怎么样?"*
**或在本地运行:**
```bash
uv sync # 安装依赖
cp .env.example .env # 添加PRIM_API_KEY(+ 可选的OPENWEATHER_API_KEY)
uv run flaneur # HTTP传输在 http://localhost:8000/mcp
curl http://localhost:8000/healthz # {"status":"ok","service":"flaneur"}
```
交互式检查工具:`uv run mcp dev src/flaneur/server.py`。
示例调用(连接后):
- `mobility_advice("Gare de Lyon, Paris", "La Défense")` → 一个融合的推荐
- `plan_journey("Eiffel Tower, Paris", "Château de Vincennes")`
- `line_traffic("14")` — 然后 `line_traffic()` 获取整个网络
- `next_departures("Châtelet")`
- `bike_route("Bastille, Paris", "La Défense")` → ~12 km, ~45-50 分钟(考虑交通灯)
- `velib_nearby("Notre-Dame, Paris")` → 最近的 Vélib' 站点,实时可用性
- `weather("Eiffel Tower, Paris")`
### 一键演示
一个可运行的脚本驱动托管的服务器并打印几个真实结果:
```bash
uv run python examples/demo.py
# 指向其他地方 / 传递密钥:
FLANEUR_URL=http://localhost:8000/mcp PRIM_API_KEY=… OPENWEATHER_API_KEY=… \
uv run python examples/demo.py
```
示例输出:
```
== mobility_advice: Gare de Lyon → La Défense ==
🚇 乘坐公共交通:~33 分钟,1 次换乘,4.60 €(状态:SIGNIFICANT_DELAYS)。
== velib_nearby: Notre-Dame ==
Arcole - Notre-Dame — 115 m — 9 辆自行车,3 个停靠点
Quai aux Fleurs - Pont Saint-Louis — 121 m — 19 辆自行车,1 个停靠点
== plan_journey: Châtelet → Charles de Gaulle - Étoile ==
31 分钟,1 次换乘,2.05 €
· Tramway T3b → Porte Dauphine
· Bus 73 → Musée d'Orsay
```
## 🔑 获取密钥(两者均为免费)
- **PRIM**(需要用于交通):在 <https://prim.iledefrance-mobilites.fr> 创建帐户 → *我的帐户 → 身份验证令牌*。
- **OpenWeatherMap**(仅用于 `weather`):<https://openweathermap.org/api>。
## 🗺️ 架构
```
flaneur/
├── src/flaneur/ # MCP 服务器
│ ├── server.py # FastMCP 应用程序,工具注册,/healthz
│ ├── advice.py # mobility_advice(交通 + 骑行 + 天气融合)
│ ├── config.py # 设置(密钥,URL,缓存TTL)
│ ├── prim_client.py # 共享的 httpx 客户端:每个请求的密钥,重试,日志记录
│ ├── geocoding.py # 地址 → 坐标(国家地理编码器 + Navitia)
│ ├── journeys.py # plan_journey + 富含中断的摘要 + 票价
│ ├── disruptions.py # line_traffic( disruptions_bulk / line_reports)
│ ├── departures.py # next_departures(SIRI 停止监控)
│ ├── bike.py # bike_route(BRouter)+ 交通灯校正
│ ├── velib.py # velib_nearby(Vélib' GBFS 可用性)
│ ├── weather.py # weather(OpenWeatherMap)+ 内存缓存
│ └── models.py # Pydantic 输出模型
├── skills/itinerary/ # 智能代理(flaneur-itinerary)
├── examples/demo.py # 一键演示,针对实时/本地服务器
├── tests/ # 61 个单元测试(解析 + 工具层通过 respx)
├── Dockerfile # 可移植的容器镜像
├── render.yaml # 一键 Render 部署
├── docs/ci-workflow.yml # GitHub Actions CI(复制到 .github/workflows/ 启用)
└── pyproject.toml
```
## 🔐 身份验证 — 每个用户一个密钥
服务器是 **多用户** 的:每个客户端在HTTP头部中发送 **自己的密钥**。密钥不会通过LLM的对话流动,也不会被存储。
| 目的 | 头部(优先级顺序) | 环境变量备用 |
|---|---|---|
| 交通(PRIM) | `X-PRIM-Api-Key`、`apikey` 或 `Authorization: Bearer …` | `PRIM_API_KEY` |
| 天气(OpenWeatherMap) | `X-OpenWeather-Api-Key` | `OPENWEATHER_API_KEY` |
在共享的公共部署上,留空环境变量以要求每个用户提供自己的密钥。始终在 **HTTPS** 后部署(Render 默认如此),以便密钥在传输过程中加密。
## ☁️ 在 Render 上部署
存储库中包含一个 `render.yaml` 蓝图:
1. **新建 → 蓝图**,指向此存储库。
2. Render 在 `0.0.0.0:$PORT` 上运行 Python Web 服务(`uv run flaneur`),在 `/healthz` 处进行健康检查。
3. 留空 `PRIM_API_KEY` / `OPENWEATHER_API_KEY` 以获取多用户服务器,或设置机密以获取默认备用密钥。
4. MCP 端点在 `https://<app>.onrender.com/mcp` 处公开。
> 免费计划:服务在 15 分钟空闲后进入睡眠状态;下一个请求会支付冷启动费用。
> 如果您在 `pyproject.toml` 中重命名入口点,请更新仪表板 **启动命令** 以匹配 — 它不会从 `render.yaml` 在现有服务上重新读取。
**或使用 Docker**(在任何地方运行):
```bash
docker build -t flaneur .
docker run -p 8000:8000 flaneur # MCP 在 http://localhost:8000/mcp
```
**CI:** GitHub Actions 工作流位于 [`docs/ci-workflow.yml`](docs/ci-workflow.yml)(ruff 校验 + 格式检查 + pytest)。复制到 `.github/workflows/ci.yml` 以激活它 — 它被存储在该路径之外,因为开发令牌缺乏 `workflow` 作用域。
## 🔬 工程笔记
一些值得注意的决定:
- **考虑交通灯的骑行时间。** 原始 BRouter 时长意味着巴黎的 ~20 km/h — 对于按时规划来说过于乐观。`bike_route` 添加了 ~15 秒/个交通信号灯交叉口(BRouter 报告的每个交叉口),以达到现实的 ~12-15 km/h。已排除的 `shortest` 配置文件(该配置文件会路由到步行速度的步道)被排除。
- **弹性上游。** 共享的 HTTP 客户端重试出现 `429`/`503` 的请求,并带有退避,天气响应被缓存 ~10 分钟以尊重 OpenWeatherMap 的免费层限制。错误变成清晰的、源属性的消息,而不是原始堆栈跟踪。
- **详细 API 调整。** Navitia 的行程有效载荷很大;`journeys.py` 将它们展平为紧凑的、LLM 友好的摘要,并将每个中断重新附加到它影响的确切路段。
### 限制和假设
- 骑行/步行时间是估计值(PRIM 仅路由步行;没有步行路由器,因此步行距离使用直线估计并带有城市曲率修正)。公交时间来自真实的、具有中断意识的时间表。
- 天气预报在 5 天后达到上限(OpenWeatherMap 免费层)。
- 地理编码获取顶级匹配结果 — 包括邮政编码/城市以获取模糊的名称。
- Google 日历工具/参数名称在技能中是说明性的;请确认它们与您连接的日历 MCP 一致。
## ✅ 测试
61 个测试,无需实时网络(上游使用 `respx` 模拟):解析、计时、缓存、密钥解析、融合决策逻辑和工具层端到端。
```bash
uv run pytest # 运行测试套件
uv run ruff check . # 校验
uv run ruff format --check .
```
## License
MIT.