Skip to content

Latest commit

 

History

History
425 lines (298 loc) · 81.8 KB

AGENTS.md

File metadata and controls

425 lines (298 loc) · 81.8 KB

AGENTS.md — MapAnNai Plus 开发入口

所有接入本仓库的开发 agent 请先阅读此文件,再查看相关源码。这里记录产品理念、架构边界和已确认的交互约定,适用于整个仓库。修改功能时同步维护本文件、env.example 和相关 README,避免把过时实现当作设计要求。

用户长期约定:默认自动部署

  • 用户已明确多次要求自动部署:本仓库功能/界面修复完成并通过相关检查后,默认自动 commit、push,再部署线上,不要停在“尚未提交/部署”,不要重复询问是否部署。
  • 仅提交本次任务相关改动,保留其他未提交工作;用户明确要求不提交、不部署或仅调研时除外。发现意外的外部改动时先确认,不擅自提交。
  • 部署命令:ssh -p 2222 -o BatchMode=yes root@server 'bash /root/docker-services/build/mapannai-public/deploy.sh'。部署脚本拉取远端 master 并构建,因此必须先推送。部署后核对服务器 commit、容器健康与 https://map.ricterz.me/ HTTP 状态,完成后再报告;失败如实说明,不将“已启动”当成“已完成”。
  • 版本升级、打 tag 和创建 Release 仍按用户明确要求,不随普通部署自动执行。

产品理念:地点关联,不是导航

MapAnNai Plus(マップ案内)是地图上的地点整理与旅行行程编辑工具,也提供 MCP 让 AI 操作相同的数据。

  • 核心对象是地点(Marker)、旅行(Trip)、每日行程(TripDay)和有序地点链(chains)。用户关心地点之间的关联、访问顺序以及每日安排。
  • 连线是关联路线,不是逐路口导航。 用户不会被要求沿展示线行走。后端寻路只提供更贴合地图的连线形状,不能由此把产品改造成导航应用。
  • 路线卡片保留地点数、访问顺序、查看路线等信息;开启路线规划时,在相邻地点之间的一行左侧显示原始规划路段距离(m / km),与地点卡片左边缘对齐,向下箭头独立保持水平居中,随计算完成刷新并复用现有缓存;关闭规划时不显示距离;开启后尚在计算、请求失败、无有效路程缓存或虚线回退时均显示 WGS-84 两点距离,仅显示数值;有效规划路程返回后自动替换。API 与前端共用 routeDistance,不添加距离类型字段;服务端按坐标补算必须带 fallback,缺少有效路程/路径为 NO_ROUTE,临时失败为 PLANNING_FAILED(HTTP 500、不缓存、保留重试)。不要添加预计耗时、导航步骤。外部单地点导航入口与关联路线是不同功能。
  • 未开启路线规划时使用贝塞尔关联曲线;开启后使用后端路径并做显示平滑、局部剪枝。允许简化行程点附近的小掉头、回环和支路,保留行程点、主要走向和较大绕行。
  • 显示优化不能修改原始寻路数据、路程、时长或行程地点。线条、白色描边、动画、透明点击范围必须使用同一条显示路径。
  • 交互保持紧凑、自然,优先考虑移动端。减少 sidebar 占用、重复按钮、冗余状态文字和不必要的确认步骤。

文档维护

  • GHCR 发布入口 .github/workflows/publish-image.yml:手动构建及 Release published 触发,amd64/arm64,GITHUB_TOKEN packages:write;稳定 Release 更新 latest,预发布不覆盖,默认分支手动构建仅更新 edge。docker-compose.ghcr.yml 使用预构建镜像并持久化同一数据卷;所有服务 Key 运行时传入,不烘焙到镜像。NEXT_PUBLIC_IMAGE_DOMAINS 为构建配置,通过同名仓库变量传递。Docker context 必须排除全部 .env 变体、SQLite 与 data/,避免私人数据进入镜像层。

  • README 功能介绍只描述产品能力,不追加实现细节或阶段日志;中英文同步。部署以显式 MAP_RENDERER=amap 的高德/Google 为推荐路径,OSM 仅可选,不要求所有部署配置瓦片代理。部署指南列明全部 Key 的用途与平台、Google 旧版 Places/Directions 的兼容限制、网页/iOS SDK 所需 API 和原生 Key 的构建位置;完整变量定义仍以 env.example 为准。

  • README.md 与 README.zh.md 面向产品使用者,保持功能介绍、使用方式、部署与本地开发内容同步,并保留 iOS 客户端入口。

  • 环境变量说明集中在 env.example;架构边界、交互约定与开发验证要求维护在本文件。不要把接口细节、测试记录或逐次变更日志追加到 README 或环境变量示例。

  • 清理与当前源码不符的旧设计文档;AI 接入以现有 MCP 为准。

开发起步

  1. 查看 git status,保留用户未提交的改动。
  2. 阅读本文件、README.zh.md / README.md、env.example,再定位任务涉及的组件与服务。
  3. 以当前源码为准;历史命名可能没有更新,不要仅凭变量名推断所用 provider。
  4. 不输出 .env、API Key、token、预签名上传 URL 或其他凭据。线上数据检查尽量只读;功能测试使用 mock 或独立临时数据库。
npm ci
npm run dev                        # 默认端口 3000
npm run dev -- --port 3101          # 需要独立 review 服务时
npm run type-check
npm run build                      # 含编译、lint 检查、类型校验;standalone 输出
npm start                          # 需要先构建
git diff --check

当前没有独立测试框架或 npm test。npm run lint 存在,但没有仓库 ESLint 配置时会进入交互设置,不要把脚本存在当作已配置完成。选择与改动相关的算法检查、临时 SQLite 测试、浏览器交互测试,不为简单文案修改机械地添加测试。

better-sqlite3 是原生依赖。更换 Node 版本导致 ABI 不匹配时可用 npm rebuild better-sqlite3;生产 Docker 使用 Node 22。不要同时在同一工作目录运行 next dev 与 next build,两者会争用 .next。

技术栈与数据流

  • Next.js 14 App Router、React 18、TypeScript、Tailwind CSS。
  • Zustand 管理 UI 与行程状态;dnd-kit 实现排序与链路拖动;Tiptap 编辑地点富文本。
  • OSM 渲染使用 MapLibre / react-map-gl;高德渲染使用官方 JS API 2.0;境外使用 Google Maps JavaScript API。
  • SQLite / better-sqlite3 持久化地点、旅行、每日行程与寻路缓存。
  • MCP 使用 Streamable HTTP;腾讯 COS 保存图片;next-pwa 提供 PWA。
  • TypeScript 别名 @/* 对应 src/*。
Web UI → Zustand / fetchWithAuth → Next.js API → DB 服务 / 地图 provider
MCP client → POST /api/mcp → MCP tools → 同一 DB 服务 / 地图 provider

MCP 工具直接调用服务层,不需要绕回本站 HTTP API。Web 与 MCP 必须共享数据规则和 provider 能力,避免出现两套业务实现。

目录导航

位置 职责
src/app/page.tsx, map-client.tsx 从服务端环境变量选择渲染引擎,下发 JS 配置,完成客户端认证启动
src/app/api/ markers、trips、dataset、search、places、directions、upload、mcp 等入口
src/store/map-store.ts 地点与行程数据、选中状态、弹窗、sidebar、编辑模式、ActiveView 与业务操作
src/components/map/abstract-map.tsx 地图页面编排、定位、搜索、视图切换、marker/popup、两种 renderer 接入
src/components/map/sdk-map-renderer.tsx 复用高德 / Google SDK 地图与覆盖物、事件、React portals、路径与动画更新
src/components/map/connection-lines.tsx OSM / MapLibre 关联线、透明点击层、高亮与动画
src/components/map/view-mode-banner.tsx 顶部旅行 / 日期胶囊与日期选择菜单
src/components/sidebar/left-sidebar.tsx 旅行总览、每日行程、路线链、地点列表、编辑和紧凑路线设置
src/components/sidebar/sidebar.tsx 地点详情侧栏
src/components/ui/, modal/ 共用 Modal、富文本编辑、图标与表单
src/lib/db/ schema 与运行时迁移、marker-service、trip-service、direction-cache
src/types/ Marker、Trip、TripDay、ActiveView、地图 provider 合约
src/lib/map/providers/ 服务端 Google / 高德实现与按能力选取的 factory
src/lib/map/basemap.ts, src/lib/coord-transform.ts OSM 样式、渲染边界与坐标转换
src/lib/map/connection-geometry.ts 不寻路时的贝塞尔几何
src/lib/map/route-geometry.ts 显示路径去重、剪枝、平滑;按原始路径引用缓存结果
src/lib/map/route-cache.ts 客户端路径缓存、共享请求队列、等距离动画采样
src/lib/map/use-planned-routes.ts 当前视图路段计算、可见范围优先、进度、重试
src/lib/map/route-settings.ts, route-progress.ts 全局路线模式设置与计算进度
src/lib/map/route-presentation.ts 每日 / 链路颜色、地址简写、路线镜头计算
src/components/ai/ai-planner.tsx, src/lib/ai/, src/app/api/ai/chat/ 网页 AI 设置、本地会话、流式聊天与复用 MCP 的工具循环
src/lib/mcp/server.ts, tools/ MCP server factory、workflow prompt、地点 / 行程 / 搜索工具
src/lib/upload/, src/lib/cos-client.ts COS 直传、压缩图片展示
src/middleware.ts, src/lib/auth.ts, fetch-with-auth.ts API 认证与前端 token 注入
env.example, Dockerfile, docker-compose.yml, next.config.js 配置说明、构建、容器与 PWA

数据与业务约定

  • src/instrumentation.ts 在 Node 服务启动时注册地址补全任务,启动即扫描,之后每轮完成一小时后重扫;进程内单例、不重叠、串行限速。只处理有效 WGS-84 坐标且地址为空的 marker,国内高德优先、错误/无有效地址回退 Google,境外直接 Google;复用 provider 坐标转换,与地图点击输入一致。写入时比较原坐标并再次检查空地址,避免覆盖人工修改;不更新名称、坐标或 POI 引用。无需新增环境变量,需要常驻 Node;多副本会各自扫描,部署应保持单实例以免重复计费。验证:npx tsx scripts/test-marker-address-backfill.ts 使用隔离 SQLite 和 HTTP mock。

  • 数据库默认 ./data/mapannai.db,可通过 SQLITE_PATH 指定。Docker 默认数据目录 /app/data,需要持久化挂载。

  • schema 初始化 / 迁移入口是 src/lib/db/index.ts。新增字段需兼容现有数据库,优先非破坏性迁移。

  • Trip.destinations 为有序 {name,latitude,longitude}[],只保存所选省市名称与 WGS-84 中心坐标,不保存行政区边界、POI 引用或创建 Marker;最多20项,六位精度坐标不重复。destination 保留首项/空时null的旧客户端投影。SQLite 现有 destination JSON TEXT 保存数组,读取历史对象时转为单元素数组,无破坏性迁移;旧旅行为空列表。省略字段更新保留全部目的地,显式 destinations=[] 清除,旧 destination 对象/null 写入兼容。

  • 网页创建旅行至少选择一个省市,已选省市以 inputbox 内 tag 显示,可继续搜索添加、逐项移除,不增加“已选择”重复提示。删除 tag 的按钮为32px,覆盖通用移动端按钮规则,标签换行不撑宽弹窗。

  • GET /api/destinations?q=... 不受当前地图范围或国家限制,优先高德行政地理编码,无结果/失败回退 Google;MCP search_destinations 复用服务,create_trip.destinations 支持多选,旧 destination 可选以兼容旧客户端。公共严格 schema 见 src/lib/trips/destination.ts,Web/MCP/DB 共用;不可编造中心坐标。

  • 进入旅行以首个目的地中心定位,两种 renderer 共用 flyMap;已有日期路线首点仍优先,点击路线保留镜头。旅行与每日视图分别搜索每个目的地中心周围约30km,合并去重,不使用跨城市大包围框,不随地图拖动变化;任一失败显示搜索错误,取消/切换旅行不采用旧响应。每个目的地各请求一次搜索,可能增加调用量。旧旅行/总览仍按可见范围扩展20%;中心范围不是行政区约束,Google 搜索仍为位置偏好。

  • 验证:npx tsx scripts/test-trip-destination.ts 使用独立旧 SQLite 与 HTTP mock,验证六城市、回退/取消、Web/MCP多目的地保存校验、旧对象与写入兼容、日期顺移、分范围搜索和去重。

  • Trip.markerIds 是旅行待分配地点列表(旧数据默认空),仅保存尚未属于该旅行任何日期的地点。同一地点可以在另一旅行待分配或已排日期;保存每日成员或链时事务性移除该旅行待分配归属。旅行详情在日期列表下方显示“未分配日期”,编辑模式可拖到日期或点击“分配”;不自动创建链。旅行总览新建地点自动加入待分配列表,已有地点弹窗可“加入旅行”;从旅行列表移除不删除地点。全局独立地点和旅行地点数量、删除独占地点统计均包含旅行待分配归属。

  • TripDay.markerIds 表示当天地点成员;chains: string[][] 表示各链的访问顺序。成员关系和路线顺序不能混为一谈。

  • 一个 marker 可以属于多个路线,列表应显示全部路线归属。删除地点或从某天移除时维护相关 chains,不能留下悬空引用。

  • Marker.placeReferences 为可选 Apple / Google / 高德 POI 引用,普通 Marker 顶层字段、Dataset properties 字段;无引用返回 null。公共严格校验、按平台合并与坐标变化清理见 src/lib/places/place-references.ts 和 接口合约。SQLite markers.place_references 可空 JSON TEXT,运行时非破坏性新增列;不得推断身份、使用内部去重 ID 或按引用转换坐标。

  • 所有地点写入在事务内合并引用;坐标六位小数确实变化时清除旧引用,再应用调用方显式引用。普通 Web 编辑省略引用,不把旧引用重发成新身份。去重返回已有地点时不附加附近搜索身份。搜索/详情仅提取正式平台 ID;坐标反查身份只代表返回的附近 POI,不能自动赋给手动标点。AI 不编造 ID,update_marker 的显式引用只是调用方声明。

  • marker 去重使用坐标 hash 与距离兜底逻辑;不要因创建新链而重复创建已有地点。

  • 修改旅行开始日期沿用现有天数并顺移每日日期;删除指定一天后维护剩余日期与旅行范围。

  • TripDay.colorIndex 是持久化每日色号;日期变更、其他天删除、视图切换不应造成颜色漂移。地图、侧栏、标签使用统一色彩函数,避免相邻天都落入同一色系。

  • 地点内容主要是 Tiptap HTML;不要把历史 markdownContent 命名误认为仅支持纯 Markdown。

  • 旅行层级的 Trip.description 已停用:创建表单、Web API、数据返回和 MCP 工具不再读写/暴露。SQLite 旧列只保留兼容,更新其他旅行字段不覆盖历史值;地点富文本和路线访问/交通备注继续使用。此变更不需要新增环境变量。

路线游览与交通安排

  • TripDay.routeChains 为权威路线结构,持久化在 SQLite route_chains JSON;旧库事务迁移,为路线和每次地点访问生成稳定 ID。chains 保持 string[][] 格式,仅是 stops 的 marker ID 顺序兼容投影,两者由服务层同一事务更新,不能各自修改。
  • RouteChain={id,stops,legs,inactiveLegs?}。ChainStop={id,markerId,startTime?,durationMinutes?,note?};ChainLeg={fromStopId,toStopId,mode,serviceNumber?,startTime?,durationMinutes?,note?}。共用 Schedule 类型,不使用运行时 class。安排属于路线中的一次访问或有方向的相邻路段,不属于全局 Marker。
  • serviceNumber 是独立字符串字段,涵盖地铁线路、公交号、火车车次、航班号、船班号;不能塞进 note。note 是补充纯文本。mode 支持 walking/cycling/driving/taxi/bus/subway/train/flight/ferry/other。
  • startTime 为所属行程日当地钟表时间 HH:mm;durationMinutes 为非负整数计划分钟数,可为0;省略未知值,不自动推算下一站,不作跨时区转换,不覆盖寻路缓存中的 distance/duration。侧栏展示和编辑安排;交通方式决定该有向路段的寻路模式,时间/时长/车次备注不参与本轮寻路,不改写用户计划时长。
  • API:POST /api/trips/[id]/days/[dayId]/chains 用 markerIds 创建路线;GET/PATCH/DELETE .../chains/[chainId] 读取、局部修改、删除。PATCH 接收可选完整 markerIds 顺序、stops/legs 局部修改列表;字段省略保留,null清除可选安排字段,空白 serviceNumber/note 清除。leg remove=true清除交通记录,不能同时修改其他字段。严校验字段、时间、访问归属、方向相邻、相邻重复地点/重复修改项;失败整次回滚。
  • 服务 src/lib/db/route-chain-service.ts 和 schema src/lib/trips/route-chain-schema.ts 由 Web/MCP 共用。create_day_chain 返回 routeChain;update_day_chain 支持 chainId 或旧 chainIndex(二选一)、stops/legs;delete_day_chain 同样兼容。get_trip_detail 返回完整 routeChains。修改删除不创建或删除全局地点。
  • 旧 chains 写入先匹配原样路线,再识别无歧义的修改;安排跟随同一路线访问ID,仍有方向相邻的交通才展示,暂时断开的同路线有向边保存在 inactiveLegs,移回原方向相邻时恢复;反向边独立,不能继承原方向。删除地点/路线和明确清除安排才删除对应记录。存在多个候选且可能串用安排时拒绝旧写入,要求 chainId;不把不确定的安排迁移到其他路线。一般标题、日期、当天成员更新保留全部安排。
  • 从日期移除或全局删除 marker 时同时清理所有相关 stops/legs,保留其他访问安排;日期顺移保留钟表时间与时长。新结构允许历史单点/空链迁移,但新建路线至少两次访问,允许同一路线非相邻重复访问同一marker(ABA/ABCAD),禁止相邻重复(AA)。每次访问保留独立stopId;markerIds写入按同地点出现顺序复用访问,精确重排/移除用完整stopIds(与markerIds二选一,不重复且属于当前路线,可保留历史单访问链)。Web排序/移除按访问ID,日成员仍去重;各renderer按访问位置读取交通方式,重复路段可有不同交通安排。移除访问/地点若会产生AA则拒绝并回滚,须先调整路线,不自动合并或丢弃访问安排。重复访问跨路线拖动及插入到同地点既有访问之前不猜测身份,提示使用移除/末尾添加后按访问ID排序。无新增环境变量。
  • 验证:npx tsx scripts/test-route-chain-schedules.ts 使用隔离旧库测试迁移、ID持久化、Web/MCP安排写入与清除、错误回滚、旧客户端兼容、路线/地点删除与相邻交通清理;不可使用生产数据做写入测试。

地图与服务抽象

渲染引擎由后端 MAP_RENDERER 选择,服务端能力自动按区域选取;不要恢复前端 provider 切换 UI。

  • MAP_RENDERER=osm|amap;osm 固定 OSM,amap 按既有 isInChina 对 WGS-84 可见地图中心选取境内高德、境外 Google;移动结束时扣除左右侧栏遮挡,以未遮挡区域中心判断,不因可见范围部分进入境内而切换。初次加载使用保存镜头判断,拖动/缩放或 flyTo 结束后才切换,回境内自动恢复高德。只重建 SDK 地图,保留中心/缩放、选中地点、弹窗、旅行/日期、搜索与侧栏状态;不添加切换 UI。sdk-map-renderer.tsx 共用地点门户、路线形状、动画与点击判定,google-map-sdk.ts 接入 Google 原生覆盖物。Google 使用256px镜头尺度,Google不做GCJ转换;高德仅在SDK边界转换。
  • 服务端默认按现有 isInChina 判断:境内优先高德,失败或无有效结果时回退 Google,境外直接 Google;旧三个 MAP_*_PROVIDER 服务环境变量不再参与选择。前端底图与展示不变,只下发 regional-v1 路线缓存策略标识,隔离旧本地缓存。MCP 显式 provider 保留兼容,但跨境路线始终跳过寻路。
  • 路线两端分属境内外时,不请求任何 provider,缓存 UNSUPPORTED_REGION、WGS-84 两点距离及 null 耗时,复用前端虚线,无重试。境内回退成功会缓存最终结果;临时失败且双方无有效路线仍保留重试,不缓存最终失败。
  • 搜索按可见范围中心调用 isInChina;无范围时 country=CN(默认)优先高德,其他国家 Google。只允许首批空结果/失败时换服务;Google 后续页凭 pageToken 继续,不因高德恢复而切回,也不在高德后续页混入 Google 首批。取消请求不触发回退。
  • 验证:npx tsx scripts/test-regional-map-services.ts 使用隔离 SQLite 和 provider mock,验证区域选择、回退、缓存、临时失败重试、跨境虚线及搜索分页。
  • 合约入口 src/types/map-provider.ts;factory 在 src/lib/map/providers/map-provider-factory.ts。新能力先考虑合约,再实现各 provider,不把高德逻辑散落到通用 UI。
  • 高德底图使用官方 JS API 2.0;不要退回直接抓取 webrd*.is.autonavi.com 非标准瓦片的实现。
  • JS 地图使用 AMAP_JS_KEY 和配套 AMAP_JS_SECURITY_CODE;服务端使用独立的 Web 服务 Key AMAP_API_KEY。Google 后端使用 GOOGLE_API_KEY;前端境外底图使用可选 GOOGLE_JS_KEY,未设置时复用 GOOGLE_API_KEY,需启用 Maps JavaScript API 与计费,JS Key 会下发浏览器。其他变量详见 env.example。
  • 内部 DB、服务合约、路由缓存统一为 WGS-84,高德接口 / 渲染边界进行 GCJ-02 转换,避免双重偏移。Google 境内坐标按现有 provider 转换约定处理。
  • isInChina 使用六个包含矩形、七个排除矩形,供底图、服务选择与坐标转换共用;广岛、韩国、台湾等返回 false,境外不纠偏。仍非精确国境,朝鲜北部等部分地区可能被包含。改动转换逻辑前检查中国边界和海外地点的实际行为。
  • 网页总览与无目的地的旧旅行搜索使用当前可见范围,每侧扩展 20%;有目的地的旅行/每日视图分别使用各目的地固定中心范围;高德使用 polygon 搜索,泛词酒店 / 高铁站等映射为 POI 类型;Google 使用中心点与半径偏好。MCP 无范围名称搜索保留原行为。
  • 搜索分页:GET /api/search 使用 page=1..100、pageSize=1..25,返回 page/pageSize/hasMore/nextPage,高德返回可用 total;Google 后续页附带上一页 nextPageToken 作为 pageToken,以实际返回的 pageSize 为准。无分页参数保留旧 limit(默认5、最多20)的数组接口。分页范围/关键词保持同一快照;客户端取消和去重,不因加载更多移动镜头。MCP原有首批名称搜索兼容,不能静默分页并增加搜索费用。
  • 高德地点服务主要面向中国,海外地点服务自动选择 Google。不要把没有搜索结果一概解释为权限或政策,先检查请求参数和返回值。

连线、缓存与动画:易回归点

  • POST /api/directions 经 src/lib/db/direction-cache.ts 缓存原始路径。缓存键包含 provider、模式、起终点坐标;共享 in-flight 请求减少重复计算。
  • 客户端同时使用内存和 localStorage 缓存。只计算未缓存的路段;多终端依赖服务端缓存,不因重绘重新寻路。
  • 客户端共享队列串行请求并做高德 QPS 退避;服务器不需要照搬逐条排队。优先计算当前屏幕路段,再向外扩展。
  • 地点坐标、连接关系、路段实际寻路模式或 provider 变化时,使用正确的新缓存键;纯高亮和视图动画不应触发额外寻路。
  • 屏幕重叠分离(route-separation.ts):按当前缩放(0.5级分档缓存)在像素空间检测近距离平行的不同路段,按路段序号稳定分配车道并横向错开约5.5px,端点24px内收拢至地点;车道与高亮无关,悬停/选中不跳线;线、白边、点击区、小圆球共用分离后路径,不修改原始寻路与缓存。线宽整体收细(高亮仅+1px,白边+1.5px)。
  • 平滑 / 剪枝只修改显示几何,单段剪枝使用 smoothRoutePath,整日显示布局使用 layoutRoutePaths。短距离小转盘允许拉直;同一天持续重合的往返路段允许渐进偏离道路、向两侧舒展,端点保持不动。普通交叉点、不同日期不做这种分离。原始寻路 API 输出和缓存不变;不要把显示路径的长度当作行程路程。
  • 高德覆盖物尽量复用;已有 Polyline 路径变化必须调用 setPath(),不能只调用 setOptions()。可见线、白边、点击层都要更新,动画必须跟随同一条路径。
  • 多天路线重叠时统一按屏幕距离选择候选;相邻路线优先最近一条,重叠处使用统一候选选择,不能由覆盖物层级决定唯一可选日期。点击路线直接进入所属旅行的对应 day 视图,同步 URL、侧栏与日期高亮,保留当前地图镜头,不自动定位当天首个地点;每日视图内不重复跳转,移动端不自动展开侧栏。已锁定的日期高亮优先于悬停。
  • 路线的透明点击区域约 28px,避免只按视觉线宽响应手机触摸。保持完全透明,阻止背景点击把刚选中的路线清空。白色描边保持细,不要产生宽色带。
  • 动画使用时间而非每帧固定增量;缓存路径长度,避免每帧扫描整条路线。减少覆盖物重建,尊重 reduced-motion,后台页面避免持续计算。

已确认的 UI 约定

  • 地图顶部不显示旅行/日期胶囊导航,使用左侧栏内的旅行与日期入口;下文胶囊相关约定仅供旧组件参考。铁路数据版权链接仅在首页总览侧栏内容流最末尾居中显示,顶部留24px padding,随列表滚动,不使用固定/悬浮页脚,保留地图服务商原生署名。无新增环境变量。

  • 地图重点卡片高度随标题、时间与完整备注自然撑开,不固定高度或内部滚动;布局避让与引导线按实际测量高度计算。卡片悬停复用地点/路线的日期高亮事件,离开、隐藏或切换视图时清除。无新增环境变量。

  • 日详情序号轴须跨越地点背景与段间交通连续连接相邻访问序号,轴线层级高于地点背景、低于序号与距离文字,且不拦截点击;最后一站不向下延伸。不以独立短线替代连续阅读轴。

  • 侧栏采用已确认的第二版排版(docs/design/sidebar-review/sidebar-review-v2.png),借鉴 iOS 信息层级,不复制卡片特效:总览按年份展示平面旅行条目和独立收藏区;旅行详情以日期编号、主题、日期、各路线有序预览组织,每条最多预览3站、最多2条路线,其余显示数量,不把未加入路线的成员伪装成访问顺序。日详情以序号轴串联访问,地点图标/压缩封面与名称同排,地址和访问备注在下,计划时间与停留时长在右;交通作为段间信息平面展示,不使用独立虚线卡片。距离仍靠左、箭头独立居中,选中/悬停背景覆盖完整地点行和备注;编辑态保留拖动、排序、安排和移除操作,路线色仅用于路线标识与日期编号。无新增环境变量。验证:npx tsx scripts/test-sidebar-presentation.tsx,开发页 /dev/route-designs 使用内存 fixture,交互写入需 mock 或隔离 SQLite。

  • 浏览器 favicon 使用 /icons/places/map.svg,不再引用旧 favicon 或 PWA PNG;PWA 与 Apple 主屏幕图标独立保留。此变更无新增环境变量。

  • 浏览态地点卡片的悬停背景覆盖整张卡片(含访问备注);选中态悬停统一使用更深的浅蓝色,标题按钮不单独覆盖背景。

  • 自绘地点图标只替换展示,保留现有iconType/emoji与数据合约。SVG资源在 public/icons/places/ 与 public/icons/trips/,place-icon.tsx 通过emoji→资源映射,未覆盖的emoji保持原展示;神社风格已确认,十类地点、旅行/日期选择器全部60项及默认总览图标使用简洁彩色平面SVG,10面国旗使用对应国家图案与颜色,不显示国家代码字母;地图三种renderer共用MapMarker,正常缩放白底显示SVG,低缩放仍显示原分类色圆点。地图、列表、详情、搜索和选择器复用PlaceIcon,任意未映射的自选emoji保留原样,不改变地点分类含义。

  • 总览“新建旅行”入口保留原竖向布局、加号、虚线边框和日文副文案“旅の目的地は、まだ見ぬ地平線の向こうに”;视觉调整仅修改配色,不改文案、形状与布局。

  • 视觉参考本地 iOS docs/local/design-references/2026-10-11-travel-app/ 的模块拆分与原图,Web按PC工作区适配:白/中性灰阅读底色、黑灰标题正文、蓝灰accent仅用于选中/焦点,独立的多日路线色。不要用单一主题色染整页文字、背景和按钮。地点身份→访问安排→段间交通保持稳定阅读轴,照片用真实封面,无图不造占位照片;减少类别底色和嵌套卡片。地点详情名称/地址先于照片;日期条用深色文字及细短下划线识别当前项。

  • 行程工作区使用白色内容区、中性灰分组间隙和轻边框;日期色集中用于路线标题与标识,浏览态隐藏排序/移除控件,完整地点笔记与访问备注保持独立可编辑。已有头图可在路线浏览卡片中显示压缩缩略图。

  • 侧栏顶部统一提供“添加地点”入口,搜索仅属于此添加流程;地图不显示常驻搜索或定位按钮,不要求先开启编辑模式。从日期打开时冻结目标日期,从旅行打开时冻结旅行待分配目标;切换视图不改变本次搜索的添加归属。总览只收藏。搜索结果正文查看地图,独立加号添加:已有地点复用ID,新地点打开共用地点编辑器,不自动创建路线。搜索在同一个左侧工作区替换行程内容,输入在顶部、结果在下方;关闭搜索清除临时结果并恢复行程滚动位置。PC左侧工作区宽400px,右侧地点详情宽420px,日期横向列表直接切换;选点镜头计入侧栏遮挡,顶部胶囊位于剩余地图中间;打开地点详情时关闭地图弹窗,并计入左右两侧遮挡定位地点和顶部胶囊。移动端仅做兼容,搜索工作区随VisualViewport调整,选点时收起工作区以查看地图,地图画布保持原高度。两种renderer共用place-search-panel.tsx。无新增环境变量。

  • “查看路线/全天”和AI建点镜头按renderer瓦片尺度计算:高德 / Google256px、MapLibre512px,计入400px左侧遮挡;npx tsx scripts/test-workspace-camera.ts检查可见范围与两种尺度。

  • 搜索面板状态验证:npx tsx scripts/test-place-search-panel.tsx;添加归属、搜索返回和手机布局的浏览器验证使用隔离数据库与模拟provider。

  • 启动认证与地图模块加载共用一个“加载地图中…”提示,保持相同布局,避免连续出现重复连接提示。

  • 编辑开关用 toggle;开启后才出现增减天数、删除旅行 / 日期等编辑控件,不要增加重复的“编辑中”提示。

  • 删除旅行和指定日期共用弹出 Modal 二次确认,不恢复二段式删除按钮或只删除最后一天的逻辑。确认框提供默认关闭的“同时删除独占地点”及数量;删除前合并 markerIds、chains 与旅行待分配地点,按删除范围判断独占:删除整个旅行时,仅被该旅行使用的地点可以删除,即使关联该旅行多个日期;其他旅行的日期或待分配归属始终保护地点。删除单独日期时,其他日期或任何旅行待分配列表仍使用的地点保留。服务端事务重新计算并返回 deletedMarkerIds,网页同步移除地点和选中弹窗。MCP delete_trip 通过 deleteExclusiveMarkers 显式启用,默认保留地点。验证:npx tsx scripts/test-trip-deletion.ts 使用独立临时数据库。

  • 路线卡片单列,一个地点可显示多个“路线 N”归属。路线标题与查看按钮保持移动端可触控大小。

  • Web 路线规划始终开启,不提供关闭开关,不读取旧浏览器关闭设置;不显示“路线规划”状态框、进度或重试入口;不显示自动、步行、驾车选择。每条有向边按自己的交通安排选择寻路模式。底部设置使用 8px 顶部留白,底部取 8px 与系统安全区的较大值,不在侧栏容器重复叠加底部安全区。

  • 搜索复用左侧行程工作区,PC输入框在结果列表上方,正文只查看地点,加号执行添加。选中结果镜头居中在侧栏右侧可见地图区域;手机选点收起工作区,重新展开继续同一搜索。两种renderer共用选择、添加与镜头offset。

  • 新增与编辑地点共用 place-editor-dialog.tsx:PC 宽880px,左侧名称/分类/封面、右侧富文本笔记,移动端纵向排列;分类使用内嵌网格,不使用跨弹窗的分类菜单。支持创建时一次写入封面与笔记,Web /api/markers 的可选 headerImage 与 content 保持同一次创建。地点详情按名称、地址、行程归属和笔记组织,坐标折叠在“位置信息”中;总览未加入旅行的地点称为“收藏地点”,使用压缩封面和紧凑列表。

  • 地点创建/编辑等待服务端成功后更新本地状态并关闭表单,不提前提示保存成功、不创建临时地点。创建后加入旅行/日期失败时保留表单和实际marker ID,再次保存修改同一地点并重试归属;不重复创建、不删除已保存地点。错误在表单内显示并保留输入,保存/图片上传时禁用重复提交与关闭。不使用临时ID回调或仅显示成功提示的“添加到链”事件。

  • sidebar 可收起;移动端在 topbar 切换日期时更新内容但不自动打开 sidebar。

  • 切换某天后按当天全部有效地点(含未加入路线的地点)计算缩放,保留周边留白;镜头中心位于扣除已打开左右侧栏后的地图区域中心,与顶部胶囊一致。点击地图路线进入日期时保留当前镜头。点击地点、查看路线 / 全天时,镜头与对应高亮保持同步。

  • 顶部胶囊按钮不显示 outline;日期按钮仅截断文字,展开箭头固定保留,不对整个 flex 按钮使用 truncate。

  • 日期下拉菜单相对整个顶部胶囊水平居中,距胶囊底边 8px,左右保持至少 8px 视口边距;不得按内部按钮右对齐或用按钮底边计算垂直位置。

  • 总览胶囊文案为“总览 · 共N天”,保持单行。日期菜单不得继承胶囊横向布局,被 sidebar 遮挡或超出视口;注意长行程滚动、外部关闭与 Escape。

  • sidebar 关闭使用统一的返回地图 / X 入口,不堆叠重复按钮。

  • marker 弹窗里的操作保持短文案,例如“删除”,避免“删除地点”挤换行;按钮文字不换行,图标不被压缩。

  • 移动端页面不缩放,地图仍允许手势缩放;变更手势行为时分别测试两者。

  • 已有地点弹窗尖头与 marker 中心至少间隔 28px,避开选中 / 悬停圆环;高德 / Google 卡片按实际宽度水平居中,不使用旧宽度的固定横向偏移,纵向偏移需额外包含向上伸出的 8px 尖头。点击地点将标点定位在未被左右侧栏遮挡区域的水平与垂直正中心,不为信息窗口额外偏移镜头;各 renderer 的 offset 正负方向保持一致。

  • 保留 marker popup 小尖头;检查旋转方块的阴影、接缝和边缘,避免破坏原有指向感。

  • 面板进出动画统一使用 app-panel / panel-backdrop;退出结束后才隐藏,关闭时立即设为 inert,快速开关必须清理旧动画帧和定时器。

  • 日期 / 旅行切换立即提交内容,新内容仅播放短入场动画;不得为播放退出动画延迟导航或锁住按钮。拖拽后的短暂点击隔离保留,并清理解锁定时器。

  • 表单弹窗使用 modal-viewport 跟随 VisualViewport(含软键盘与视口偏移),地图画布不跟随键盘缩小。移动端打开表单不自动唤起键盘,弹窗保持键盘焦点与 Escape 关闭行为。

  • 地图小圆球动画通过 src/lib/ui/animation-loop.ts 调度;隐藏页面取消帧,回到前台恢复,运行时切换减少动态效果设置也应停止动画并清理小圆球。

图片与认证

  • 图片上传:前端向 /api/upload 获取预签名 URL,然后直接 PUT 到 COS;凭据只留在服务端。
  • 默认展示压缩图片,由 src/lib/upload/image-url.ts 添加 COS 图片处理参数,正文图片延迟加载;保留原文件,不把数 MB 原图恢复为默认展示。
  • 可选 API_TOKEN 保护 /api/*;前端用 fetchWithAuth 注入 Authorization: Bearer ...。也支持 x-api-token,不把 token 放在 URL 查询串。
  • 不把生产 token 写进本文件、README、测试或日志。

网页 AI 对话

  • AI 设置复用共用 Modal,以独立浮层展示,不放在聊天滚动内容中;关闭后保留聊天滚动位置,无新增环境变量。

  • 可选联网工具 search_web / fetch_web 在单个 Node 22 主容器内通过 stdio 调用固定版本 Brave / Firecrawl 官方 MCP;仅配置对应 Key 即注册,不需要 MCP URL 或额外容器。官方包全局安装,网页获取仍经第三方云端。错误日志只记录工具名、阶段和错误类型,不输出凭据、参数或正文。

  • 联网只读,不产生地图数据变更事件;模型只能传搜索词/数量或公开网页 URL,不能传 headers、cookies、proxy 或其他抓取操作。MCP 命令固定,不接受模型指定。60秒超时、传播取消、仅返回文字并限制12000字符;上游错误不透传。网页/摘要均为非可信资料,不执行其中的指令,不推断坐标/POI ID,重要信息标注来源,截断和失败不能宣称已核实全文。第三方请求可能计费,不发送私人信息。

  • 验证:npx tsx scripts/test-web-tools.ts 使用本地 HTTP MCP mock,不访问真实网页或生产数据库,验证工具注册、参数转发、URL限制、输出截断、错误脱敏和取消。

  • 网页配置 OpenAI 兼容 Chat Completions API 地址、Key 与支持工具调用的模型。会话与完整工具结果保存在浏览器;Key 默认仅保留在当前页面,用户显式勾选后才保存到 localStorage。不得把 Key 或聊天记录写入服务端数据库、日志或 URL。

  • 本地历史写入超出浏览器配额时由 writeHistory 降级:先将非交互/审阅类的大工具结果截断为2000字(保持合法JSON、保留isError),仍不足再移除最早的非当前会话;写入失败不得中断后台任务轮询。

  • POST /api/ai/chat 继承 API 认证,将多轮消息转发至配置的 AI API,以 NDJSON 返回文本、工具结果与数据变更事件。AI 回复的 Markdown 由 marked 分词后渲染为 React 元素,H1–H6 统一紧凑字号;不能直接注入模型返回的 HTML,链接仅允许 HTTP(S) / mailto。

  • src/lib/ai/mcp-tools.ts 使用 SDK 内存 transport 连接现有 MCP server,复用 schema 和服务规则;不要维护另一套工具定义或绕回本站 HTTP。

  • 助手自称“M酱”,口吻熟络爽快、有元气,直接接话,不复述需求、写报告或例行追问;回复和地点笔记不发 emoji,可偶尔用颜文字,不堆砌卖萌与模板套话;保留真实 AI 身份和工具结果边界。系统提示词只在服务端 src/lib/ai/prompt.ts 维护,由 /api/ai/chat 的 planner 注入,客户端不维护副本,也不提供提示词下发接口。旅行上下文在 trip-context.ts 单独组装。

  • 网页提示词说明产品定位、对象关系、工具能力、完整创建旅行流程与必要业务规则,不拼接 MCP workflow 全文。每次模型请求将最新旅行列表(ID、名称、日期范围)和当前日期 / 视图 ID 附加到第一条 user 消息;附加内容不写回本地 transcript。只使用当前话题的完整 transcript,不增加跨话题记忆或压缩层。

  • 话题下拉框使用自定义箭头,箭头距右边缘 12px,文本留出空间。

  • 默认仅允许公共 HTTPS endpoint;连接时校验实际 DNS 地址,不跟随重定向。仅部署者设置 AI_ALLOW_PRIVATE_ENDPOINTS=true 时允许私有地址及 HTTP。此配置需重启服务。

  • 工具按顺序执行;写入(含可能部分失败)后刷新地点与旅行。网页 useAiMapEffects hook 仅处理当前聊天 create_marker / plan_trip_day 工具结果中 status=created 的地点:刷新完成后单点定位、批量按范围定位,镜头考虑聊天面板及左栏遮挡;关闭面板取消尚未执行的定位,历史记录、已有地点及外部 MCP 不触发定位。停止请求不承诺撤销已执行操作;恢复会话补齐缺失工具结果,提示模型查询实际数据,不自动重放写入。

  • AI 聊天输入框与发送 / 停止按钮放在同一圆角输入区域,输入区默认单行约 46px 高,多行输入自适应至 120px;按钮保留 44px 点击范围,内部仅 28px 浅色圆角背景及小图标,相对输入区域垂直居中;不添加欢迎引导、示例提示或底部帮助文字,设置仅显示必要字段与操作。

  • 地图 AI 入口仅显示圆形聊天图标,不显示“AI 规划”文字,保留无障碍标签及运行状态圆点。

  • AI 面板为非模态面板,不加地图遮罩或锁定 Tab 焦点,点击 / 拖动地图不关闭聊天;PC 宽 560px,手机底部占可见视口 65%,上方保留地图。关闭按钮及面板内 Escape 可关闭,表单 Modal 保留自己的焦点约束。

  • AI 面板遵循共享进出动画、inert、焦点管理与 VisualViewport 约定。独立临时 SQLite 与模拟 AI 的验证脚本:npx tsx scripts/test-ai-planner.ts;必须在加载数据库模块前设置临时路径。Markdown 渲染与链接 / HTML 边界验证:npx tsx scripts/test-chat-markdown.ts。网页建点结果筛选验证:npx tsx scripts/test-ai-map-effects.ts。

MCP 接入

  • 入口 POST /api/mcp,每个请求创建 server 与 transport,不能复用绑定过 transport 的 McpServer 实例。
  • 工具注册在 src/lib/mcp/tools/,完整工作流参考 server.ts 的 workflow prompt。
  • plan_trip_day 按地点名称搜索 / 创建标记并加入当天,会生成链;创建同一天地点后校验坐标,任意两点超过 100km 或定位失败应报告,不默默继续。
  • create_day_chain 使用已有 marker ID 创建链,不创建地点;校验旅行 / 天、marker 是否存在及链内重复,并将尚未属于当天的 marker 加入当天成员。
  • update_day_chain 以完整已有 marker ID 列表替换一条链,校验索引、重复与地点存在,并补齐当天成员;delete_day_chain 仅移除链。二者保留地点、原有当天成员及其他路线,通过服务层事务写入。优先使用最新 get_trip_detail 的 routeChains[].id 作为 chainId;兼容 chainIndex(chains 数组从0开始的索引,二选一);删除后后续索引前移,继续操作先重读详情。网页 AI 自动复用这两个 MCP 工具,写入后刷新地图与行程。
  • assign_marker_to_trip / remove_marker_from_trip 操作旅行待分配地点;assign_marker_to_day 分配到日期时移出当前旅行待分配列表,保留其他旅行归属。
  • 搜索名称应包含城市 / 区域,海外显式传 country 并选择合适 provider。MCP 与 Web 维护相同的数据一致性。

路线安排前端

  • 正式侧栏复用 src/components/trips/route-schedule.tsx。地点时间与计划时长放在右上角,交通信息在浅灰虚线容器内,距离靠左,时间靠右;上下沿用原有折角箭头并居中。整小时显示1小时/2小时,90分钟仍显示90分钟,编辑和存储使用分钟。
  • 编辑态地点卡片顶部对齐拖拽、序号、图标、标题和时间;地点标题、地址、访问备注与交通车次/备注自然换行,不使用行数截断或省略号,地点卡片保留72px最小高度并由内容撑开;地址紧跟标题;编辑模式下未填时间和时长的地点右上角使用灰色钟表图标 + --:-- 时间占位,不使用“设时间”等行动文案;只有浏览模式不显示空占位,已有时间/时长沿用同一位置和字号。移除、上下移统一放底部32px操作栏。移动端紧凑按钮覆盖需使用不低于通用 button:not([role="switch"]) 的选择器优先级,实测 coarse pointer 的高度,不只看桌面截图。
  • 安排编辑统一由已有“编辑模式”控制,使用紧凑弹窗,不增加铅笔按钮。交通方式、线路/车次号和备注分开输入;可清除交通安排,字段留空清除对应值。保存采用服务端返回的完整 TripDay,旧 chains 重排也刷新权威 routeChains,不能让访问 ID 或安排残留在旧顺序。快速重排按日期串行写入,过时响应不覆盖较新的乐观顺序。
  • 未设置交通安排时卡片不显示推测的交通方式(寻路内部按2km阈值选择):浏览模式有距离就只展示距离,无距离时只展示一支居中箭头,不显示空虚线框;编辑模式显示水平居中的“无交通安排”入口,有距离时距离仍靠左且不影响文案居中。已填交通但无距离时仍展示交通信息,不填0km或虚构距离。
  • 开发环境 /dev/route-designs 使用正式侧栏和组件配合模拟数据,展示已设置/未设置安排的状态;生产环境返回404。测试写入由浏览器mock或独立SQLite承接,不操作线上数据。无新增环境变量。

验证与交付

  • 代码变更通常执行 npm run type-check、npm run build、git diff --check。文档变更检查内容、路径与 diff,无需无意义地重新构建。
  • 地图改动检查两种 renderer 的相关逻辑;至少说明哪种进行了真实 SDK 测试、哪种是 mock,不能把 mock 结果声称为生产验证。
  • 路径算法改动用真实缓存数据生成前后对比图,先看图再下结论;检查起终点、有限坐标、正常转弯、大掉头、远离行程点的环路、原始路径不变与缓存复用。
  • 浏览器验证用只读线上数据或 mock;临时 DB 通过 SQLITE_PATH 指向独立文件。测试图与临时导出不要混入正式资源。
  • 用户要求先图片审阅时,在审阅前不要推送 / 部署该功能;用户批准后按授权继续。常规已授权的 push / deploy 可直接执行,避免重复索要确认。
  • 提交前检查 diff,不夹带其他任务或敏感数据。完成后报告 commit、验证结果、部署状态和实际限制。
  • 本项目现有生产部署命令(仅在获得部署授权时使用):
ssh -p 2222 -o BatchMode=yes root@server \
  'bash /root/docker-services/build/mapannai-public/deploy.sh'

部署脚本位于服务器,不在本仓库。脚本使用 set -euo pipefail,拉取后仅构建 mapannai 镜像并通过生产 docker-compose.yml 更新 mapannai 一个服务,再检查并重载 nginx。Web 与 Agent 在同一容器中作为两个独立进程运行,由 scripts/start-services.cjs 管理;任一退出会终止另一进程并退出容器,由 Docker 重启。Agent 仅监听回环,内部地址由入口生成,不暴露额外端口。配置统一在 /root/docker-services/build/mapannai-public/.env,禁止增加额外 Agent env 或 Compose override;.env 不进入构建上下文。原地图数据挂载不变,agent.db 同目录独立保存。等待构建、容器更新完成,再检查 Web 与 Agent 健康及公网 HTTP 状态,不得提前宣称部署成功。

路径及明确的不可规划结果保存在 SQLite direction_cache,Web / MCP 共用。高德超范围或无有效结果时默认先尝试 Google;两家均明确无路线以及跨境路段才显示虚线贝塞尔曲线、WGS-84 两点距离及 null 耗时,不自动或手动重试。公共交通正常结果和 NO_ROUTE 不按时间过期;临时失败不缓存最终结果。坐标、模式或服务策略变化后使用新缓存键。

路线计算完成后移除进度显示;跨境以及双服务明确不可规划的路段保存为不可规划结果,显示虚线贝塞尔关联曲线,不反复请求或显示重试。仅临时失败保留重试入口。

每段路线按 routeChains 中当前有向相邻 stops 的 active leg 选择方式:步行→walking,自行车→bicycling,自驾/出租车→driving,公交/地铁/火车→transit;飞机/轮船/其他不调用寻路,使用虚线贝塞尔曲线。线型由 routeLineStyle 统一:飞机始终虚线、略细,并在显示路径中点放置按 Mercator 屏幕方向旋转的飞机图标指示方向;火车始终实线、略粗,路线规划开启时使用 OSM 铁路轨道几何(含 JR),无轨道结果时仍以实线贝塞尔显示;其他方式沿用规划状态规则。三种 renderer 共用该函数与 pathMidpointHeading。没有交通安排时按 WGS-84 起终点直线距离,小于2km步行,否则驾车。公共映射在 route-mode.ts,地图两种 renderer 与侧栏距离使用同一映射;不改变剪枝、平滑、显示路径与点击动画约定。旧本地设置仅保留 enabled,忽略原 auto/mode。缓存按实际模式复用;公共交通正常结果和 NO_ROUTE 永久缓存。接口与MCP见 行程路线合约。

每次页面启动按本地日期定位到最近尚未开始的旅行(含今天),镜头居中在第一天第一条路线的第一个有效地点(没有路线则按当天地点顺序),使用默认概览缩放 zoom=11,不使用旅行范围中心或 popup 偏移,保持总览且不选中旅行 / 日期 / 地点。不再从 URL hash 或 sessionStorage 自动恢复选中;第一天无有效地点的旅行跳过,无可定位的未来旅行时保留上次 / 默认镜头。等待地点、旅行和地图全部加载后仅执行一次,普通数据刷新不重置镜头。

  • Web 搜索结果同步显示临时蓝色圆点,地图圆点与列表共用选择和添加弹窗,选中项高亮;清空、换关键词、失败和退出地图移除临时覆盖物。两种 renderer 共享样式和POI稳定ID,高德覆盖物复用并在边界转换GCJ-02;搜索结果不会自动写入地点或行程。

  • Web 搜索同时匹配全部已保存 marker 的标题(忽略大小写、不受视口限制)及当前范围的平台结果;本地结果即时显示,平台失败不清除本地结果。平台结果与已保存地点在六位小数坐标精度下一致时,只显示已保存 marker;点击直接选中既有地点,不创建草稿,不推断或合并 POI 引用。已保存地点不重复生成蓝色搜索覆盖物。

  • 路线成功必须有平台实际路径;铁路仅有站点坐标时返回 NO_ROUTE、坐标距离及 null 耗时,不能把站点连线当作铁路轨迹。公共交通缓存服务端/Web使用 v2 键,隔离旧站点连线结果,其余模式沿用现有键。

  • 日期标题在编辑模式下点击侧栏顶部标题修改,使用共用 Modal;清空恢复“第 N 天”。通过现有 updateTripDay 仅写入 title,成功后更新侧栏与顶部胶囊,失败保留输入。

  • Google 境外底图默认使用 google-map-sdk.ts 的浅色旅行配色(薄荷公园、浅蓝水域、杏色主干道),通过内嵌 JSON styles 设置;保留交通与 POI 标签,不改变行程覆盖物、坐标或服务选择,无新增环境变量。

  • AI 创建地点最终标题只保存核实的地点本名,不添加自定义区域、日期或备注;本名中的区域保留。创建工具带城市搜索后,对 status=created 的结果用 update_marker 清理搜索前缀;已有收藏不擅自改名。网页提示词与 MCP workflow 保持该规则一致。

  • 网页 AI 独有 change_title 工具在了解旅游意图后修改当前会话标题,服务端严格校验1–60字并发送 title 事件,前端按发起请求的会话ID更新本地历史与会话列表;不写数据库、不触发行程刷新,不作为公共MCP工具。启动加载与认证提示使用 /icons/places/map.svg。

  • M酱规划节奏:自然分轮收集目的地、日期、偏好和约束,每轮1–2个关键问题,不重复问、不为凑轮数盘问;意图明确后在逐日规划前必须成功调用 change_title。逐日查资料、核实与安排地点,明确保存授权后先建立独立标记,再创建旅行并用已有ID建立每日路线,最后给简短真实保存总览。草案不写入,信息齐全/用户要求直接规划可跳过冗余访谈。游览 stops.note 不超过100字、交通 legs.note 不超过50字,仅提示词软限制:简要说明怎么做,单一明确安排,不提供多种方案,不新增API校验/截断、不影响用户手写备注。交通备注只补充必要信息,界面已有距离、起始点/终点、方式及车次字段不重复写;无补充省略,不写占位备注。此变更无新增环境变量。

  • AI SSE 安全限制区分传输开销与实际内容:总流20MB、未完成帧100万字符、正文与工具参数合计100万字符;不截断工具参数。Web文字先抽取标题/来源/正文,再截断12000字符,不对普通业务工具结果裁切ID。验证 npx tsx scripts/test-ai-response-size.ts,不以协议字节数等同可见回复长度。

  • 网页 AI 与 MCP workflow 的每日草案和最终总览使用简短文艺、贴合当天真实地点与活动的名字,各天有区分;不为取名编造季节、花期、天气或日出日落安排,优先保留用户自定义名称。每日标题通过可选 create_trip.dayTitles 保存,已有日标题用 update_trip_day 修改;不通过新增日期改名。

独立 Agent 边界

  • 生产聊天由 src/agent/main.ts 独立 Node 进程运行,Web /api/ai/chat 通过内部 WebSocket 代理并保留原 NDJSON/ChatEvent 协议,不要求客户端升级。Agent 不导入地图 DB 或 Web 服务,通过固定 HTTP MCP 调用业务能力;src/lib/ai/planner.ts 仅作隔离测试适配,不是生产回退路径。

  • LangGraph 当前为 model/tools 有界执行图,业务规划阶段仍由提示词引导;尚无强制审批、持久化 checkpoint、跨重启恢复或可靠事件重放,不得宣称已经具备。断线/停止传播取消,已保存数据不回滚。完整设计与限制见 docs/agent-service.md。

  • Agent 非回环监听必须配置独立 AGENT_TOKEN,不在 URL/浏览器发送内部凭据;Web 认证沿用现有 middleware。API_TOKEN 仅用于 Agent 到 Web MCP,模型 Key 仅用于单次执行,不落盘。联网工具保留在 MCP 服务端以兼容现有外部客户端。

  • 新工具参数保持向后兼容:create_trip.dayTitles 可选;update_trip_day 仅改标题且校验旅行归属;create_trip/create_day_chain 的可选 operationId 在 SQLite tool_operations 中与业务写入同事务保存,复用不同参数拒绝,旧调用不带则保持原行为。其他写工具尚未全部幂等,不允许对整个图盲目自动重试。

  • npx tsx scripts/test-agent-service.ts 启动真实独立子进程、本地模型 mock、HTTP MCP 和隔离 SQLite,验证 Web 协议兼容、WebSocket、标题和幂等;不等于真实模型/地图 SDK/生产部署验证。生产部署只构建同一镜像,须检查容器内两个进程,不能只验证 Web。

  • Agent assess 节点独立评估需求、缺项和用户授权原句;无授权、有关键缺项或收集/骨架/审阅/完成阶段只提供只读工具,执行端再次检查工具白名单。授权原句必须来自 user 消息,不能从工具结果推断。此轮没有 checkpoint,不能称为跨重启可恢复;scripts/test-agent-workflow.ts 检查阶段门禁。

  • 可测试 Agent 增加每日核实控制工具 finish_planning_day/submit_trip_review,按骨架顺序核实实际保存结构,提交审阅后本轮禁止写入。独立 AGENT_SQLITE_PATH 执行日志记录最终消息与工具结果,同请求重放不再执行;重启未完成请求只恢复记录并提示核实,不自动重放未知写入。日志不保存模型 Key;单 Agent 实例,不是 LangGraph checkpoint,不能称为完整自动恢复。验证 scripts/test-agent-day-loop.ts 与 scripts/test-agent-recovery.ts。

  • 聊天会话 ID 使用现有 uuid.v4,兼容局域网 HTTP 缺少 crypto.randomUUID 的环境;真实模型凭据仍建议仅通过 HTTPS 使用。

  • 底图区域中心验证:npx tsx scripts/test-regional-renderer-center.ts 检查左右侧栏偏移、WGS-84 转换、仅中心判断、经度跨日期线和固定 OSM;不替代真实高德/Google SDK 拖动测试。

  • AI 附加的历史旅行及内部ID仅用于工具定位和复用,不主动罗列历史旅行,不凭旅行名/日期推断节奏或偏好;用户要求列表时只展示名称、日期和摘要。正常回复、标题与笔记不输出业务或工具调用内部ID,用户明确索取除外;网页提示词与 MCP workflow 同步。

  • AI 顺序固定:关键需求收集完成→change_title→制定全程骨架→逐日规划。收集期或有阻塞缺项时程序不提供 change_title,执行白名单同样拒绝;改标题无需地图保存授权。评估器只提取已有骨架,不提前生成。

  • 网页 system 提示词按身份→程序指令优先→任务类型→规划流程→交互工具→失败处理→数据边界组织,保持4000字以内;标题与骨架由程序节点生成,提示词不再要求模型自行调用 change_title/制定骨架;草案阶段只读,保存顺序为 create_trip 后按日期逐天建点、建路线、finish_planning_day。程序追加的阶段/熔断尾部指令统一包在 <agent_instruction> 中,与用户发言区分。

  • 写入授权统一:非acting阶段的 workflow.authorized 由程序按草案continue或skipReviewQuote免确认推导,评估器的原话授权仅用于acting地点写入;修改草案确认后保持revising,待完成范围只覆盖affectedDays与addedDays。评估器将“排进某天/调整路线/修改某天”判为revising/planning而非acting。npx tsx scripts/test-agent-authorization.ts 验证修改范围、无草案不写入及连续失败熔断。

  • Agent 工具定义与顺序固定,不按阶段增删;权限由工具执行端独立检查,包括收集期禁止改标题。system 固定,旅行上下文及阶段指引仅追加在原始消息历史之后,不能改写第一条用户消息以破坏缓存前缀。缓存实际命中取决于上游服务,不能保证。

  • 图新增真实 title/skeleton 节点:assess 信息齐全后先生成标题并发 title 事件,成功后才生成骨架;收集/规划/审阅进入各自模型节点并复用固定工具集,骨架生成记录进入消息历史。脚本 test-agent-stage-order.ts 检查调用顺序及收集期拒绝改标题。仍无完整 checkpoint 自动续跑,不得混淆执行日志与图检查点。

  • 需求评估与 title/skeleton 的只读结构化生成同样使用流式 Chat Completions(兼容返回普通 JSON 的服务),避免依赖上游非流式支持;JSON/Schema 不合格仅修复重试一次,不重试网络请求或地图写入。失败日志区分固定 HTTP 状态类别与 StructuredOutputError,不记录模型正文或凭据。验证 npx tsx scripts/test-agent-structured-completion.ts。

  • 只读结构化阶段剥离工具调用后必须同时移除 null/空白正文消息,不能向上游发送无 tool_calls 且 content=null 的助手消息;保留其他文本原序。test-agent-workflow 与 test-agent-structured-completion 覆盖带工具历史的输入及修复请求。

  • 可选 AGENT_TRACE_ENABLED=true 将诊断 transcript 写入现有 agent.db 的 trace_records 表,按 run_id 关联输入、节点、模型请求/完整已解析回复、工具事件、HTTP错误摘要和异常栈;不记录逐token帧/隐藏推理。模型Key与环境凭据、敏感字段和签名URL脱敏,DB权限600;仍包含私人对话,严禁输出原文到公共日志。保留7天(启动/写入清理),每run上限2000万字符,不影响执行日志幂等,不自动重试未知写入。用 scripts/test-agent-trace.ts 验证;旧失败无法补录,不能宣称已还原历史根因。

诊断开关未设置时开发默认开启、生产默认关闭;AGENT_TRACE_ENABLED=true/false 显式覆盖。生产镜像设置 NODE_ENV=production。

  • AI聊天连续工具结果合并为默认折叠的进度块,执行中显示当前动作和已返回数,完成后汇总操作数,失败数量始终可见;展开按原始执行顺序逐条显示操作和结果,不合并同名操作、不显示倍数,不渲染原始工具参数/内部ID。用户/助手正文保持独立阅读顺序,空正文工具调用不拆分进度。scripts/test-ai-tool-display.ts 验证分组和错误识别;无新增环境变量。

  • 后台任务(POST /api/ai/runs)中的交互卡片不限时:发出卡片后运行挂起,transcript(不含模型Key)、待答工具调用及卡片token存入agent.db background_suspensions,运行标记waiting(对客户端仍报告running)并释放执行资源;用户经原 /api/ai/interactions 提交时附带当前模型Key,服务端校验token与选项后在同一运行ID下追加工具结果并从评估节点重建执行,重启后同样可恢复,每张卡片只能恢复一次,DELETE可取消等待。旧 /api/ai/chat 与WebSocket仍为进程内60秒等待。验证 npx tsx scripts/test-interaction-suspend.ts。

  • 网页交互工具 ask_datetime / ask_choice / preview_itinerary 使用聊天内卡片,Agent进程内一次性等待60秒,服务端截止时间与能力token校验,选择/超时/取消互斥。超时及旧客户端unsupported不构成用户确认或写入授权,不编造实际日期与预订。旧客户端不发送supportsInteractions时即时返回unsupported;新客户端通过原NDJSON interaction事件和受原认证保护的POST /api/ai/interactions提交,内部仍回环通信。停止/断线取消等待;当前不做checkpoint或刷新后恢复卡片。选择结果留在原工具历史,敏感交互token不保存到执行日志。scripts/test-ai-interactions.ts包含真实60秒超时验证。无新增环境变量。

  • AI日期卡片参照iOS TripRangeCalendar/TripDateRange:单一日历两次点选起止,反向点选自动排序,同日可选,第三次开始新范围;跨月保留选择,端点圆形与中间连续浅色带,显示含首尾天数、明确选结束提示、“完成”仅收起日历而非提交。日期时间模式分离日历和时间,不默认为用户填写时间;不并排提供原生日期输入与自绘日历。scripts/test-calendar-range.ts验证范围算法,最终提交仍由服务端校验。

  • AI交互需求收集必须逐项:先落地时间、收到结果后问起飞时间、再问是否坐船等偏好,不在正文列多个问题或“顺便问”。支持卡片时必须调用对应工具;unsupported才退回文字,超时不代表不支持。交互等待60秒。模型一次混合多个调用且包含交互时,整批在执行前拒绝并要求逐项重发,任何写入均不执行;scripts/test-ai-question-policy.ts验证。

  • Agent请求重放仅针对显式conversationId/requestId(UUID)且请求正文/配置一致的同次请求;网页每次发送生成新requestId并携带本地会话UUID,title永远不参与身份。旧客户端缺任一ID时每次分配独立执行ID,绝不能按同文输入跨设备/会话重放历史;v2哈希隔离旧日志。scripts/test-agent-recovery.ts覆盖会话隔离、不同发送、旧客户端和同请求重放。不提供聊天多端同步;UUID不是用户认证边界,API认证规则不变。

  • ask_choice单/多选固定提供“都不选,文字说明”,不由LLM生成options;工具说明和提示词明确告知此入口。提交action=none返回declined(非超时/已选),本轮后续tool_choice=none且执行端禁止工具,要求只文字开放询问并等待下一条消息;历史显示拒选结果。scripts/test-ai-interactions.ts覆盖拒选解析及返回状态。

  • 交互60秒超时与主动拒选采用同一文字询问门禁:保留timed_out/declined状态区别,但都将本轮tool_choice设为none、执行端禁止后续工具,要求只问一个开放问题后等待用户回复。不得以超时为理由自行安排或假设继续;所有交互类型适用。

  • 规划保存边界:完整未保存草案通过preview_itinerary确认后才允许地图写入;单独“保存”意图不足以绕过草案确认。continue只批准该草案,不授权其他删除/预订/付款。保存全程核实后submit_trip_review返回真实savedOverview,前端自动展示可切日期概览,进入图内chatting节点;system和工具列表保持固定,chatting的tool_choice=none且执行端拒绝工具。新任务/修改由assess流转回规划且需新草案确认。测试scripts/test-plan-review.ts、test-draft-write-gate.ts及day-loop;当前审批基于会话工具历史,不是签名版本化计划,不宣称完整审批系统。

  • 评估missing非空时强制回collecting并撤销旧草案批准,执行端禁止preview_itinerary及地图写入,保留只读查询和逐个提问。返回WORKFLOW_BLOCKED与明确澄清原因,不将已知流程拦截伪装成参数错误;其他异常继续脱敏。scripts/test-workflow-missing-gate.ts验证缺项不弹确认、不写入,以及补齐后重新确认可保存。无需新增环境变量。

  • Agent每请求最多64轮执行模型调用、1200条历史消息;LangGraph步数按轮数计算为136,避免图先于模型轮数截停。工具批次可含多个调用,交互工具仍必须单独调用;额度与上游上下文窗口限制不变。scripts/test-agent-round-limit.ts验证长循环正常完成及上限停止,无新增环境变量。

  • 骨架日期检查先验证有限时间戳再转换ISO;真实日期、连续唯一及可提取的已确认ISO范围校验在结构化输出修复重试内进行,禁止占位日期导致RangeError。评估日期为空时回收集并禁止写入;修改当前旅行且日期为空时先只读获取真实日期骨架。历史骨架仅在日期一致时复用。scripts/test-plan-dates.ts覆盖非法日期、闰年、跨年、范围及修复。

  • 新Web聊天使用后台任务API POST /api/ai/runs、GET /api/ai/runs/{id}?after=序号、DELETE显式停止;执行与订阅连接解耦,断网/刷新不取消任务。每run独立随机能力token放x-run-token(不在URL),原API认证叠加;游标和会话归属及待交互卡片保存在浏览器,消息与游标同步落盘后续拉,不用同文匹配其他端。原/api/ai/chat保持旧流兼容。事件持久化到原agent.db,模型Key仅内存;完整消息和每秒累计partial支持追上生成。进程/容器重启仅保留已落盘结果、运行任务标interrupted,不自动重放未知写入或承诺完整checkpoint恢复。后台交互仍60秒截止,离线超时转文字询问。scripts/test-background-runs.ts覆盖独立运行、能力隔离、游标、显式取消及重启读取。

  • 独立地点查询/收藏/编辑使用acting状态:无需旅行日期、每日归属、骨架和全程草案;只允许只读工具及明确授权的create_marker/update_marker,旅行写入仍走规划审批。例外为已保存行程的注记修改(标记/取消重点、已有访问/交通的时间时长车次备注、日期标题):acting下允许不带markerIds的update_day_chain与仅改标题的update_trip_day,增删地点/重排/新建删除路线仍需修改草案确认。评估missing仅限当前操作必需信息;历史草案批准不得将acting/chatting转成planning。自然聊天及超时/拒选的动态尾部不得再追加调用规划工具的指令,固定工具列表不变,不解析执行正文DSML。scripts/test-agent-acting.ts验证独立收藏和聊天尾部。无新增环境变量。

  • Web AI支持每条最多4张JPG/PNG/WebP图片,每张10MB以内,复用COS直传并保存HTTPS展示URL;发送前可移除,上传失败不发送。协议user.images为可选HTTPS URL数组,旧文本客户端兼容;模型传输边界转换为标准text/image_url内容块,需求评估和阶段生成也保留附件。需要支持视觉输入的模型及可访问图片URL,图片会发送给用户配置的模型服务,勿上传敏感资料。无需新增环境变量。scripts/test-ai-images.ts验证格式与URL限制。

  • 目标旅行不限于网页当前打开的旅行:评估器输出用户点名的 targetTripName,程序在 list_trips 中按名称唯一匹配(精确优先、再包含)得到 targetTripId,用于修改骨架/日期核对并附加到上下文;未点名、无匹配或多义时回退当前旅行。不要求用户在应用中切换旅行。

  • 修改当前旅行时日期骨架以数据库真实日期为准;affectedDays的ISO/月日(12/31、1月5日等)按该旅行日期唯一匹配;新增日期只来自评估结果显式的 addedDays(真实ISO日期,不要求紧邻首尾),并入骨架由模型先 add_day_to_trip;无法匹配的日期回到collecting询问用户,不直接终止任务。逐日写入与 finish_planning_day 可按任意顺序处理待完成日期,submit_trip_review 前须全部核实,不将“1月4日”直接与ISO日期比较。跨年不推定年份,不改旅行日期。

  • finish_planning_day核实真实日期、标题、非空地点成员及已有路线引用完整性;地点成员不强制组成路线,禁止为通过核实捏造路线。已知核实失败原因通过WORKFLOW_BLOCKED返回模型,不吞成泛化参数错误。

  • update_trip_day仅修改标题,不受逐日地点/路线执行顺序限制;草案批准、授权和审阅后停止写入门禁仍适用。写入不在待完成范围内的日期时返回可操作日期列表及WORKFLOW_BLOCKED,避免模型盲目重试。

  • Agent工具执行错误统一区分WORKFLOW_BLOCKED(已知门禁/JSON格式)、INVALID_ARGUMENTS(schema字段路径及校验码)、TOOL_EXECUTION_FAILED(调用已进入MCP、写入结果未知)、INTERNAL_ERROR(未调用工具的内部异常)。每个失败写入tool消息供下一轮模型读取,并记录脱敏tool-failure trace;禁止向模型透传未知异常正文或凭据。submit_trip_review同样保留逐日核实原因。scripts/test-agent-tool-errors.ts覆盖模型实际收到原因及未知错误脱敏。核实/概览内部读取的MCP isError正文(截断1000字)与JSON解析失败按WORKFLOW_BLOCKED返回;INVALID_ARGUMENTS附Zod message;传输失败按超时/鉴权/服务不可用分类且不透传原文;同批工具调用ID重复时改名配对并整批不执行退回模型;结构化输出修复重试附上次校验原因。同一工具同一错误码(含MCP isError)在单次请求内第二次失败即熔断:后续tool_choice=none、执行端拒绝工具,追加尾部指令要求只如实说明失败原因与所需用户操作后结束;acting阶段的未授权/旅行写入拦截文案不提及全程草案。取消、轮数/历史上限、模型网络/结构化输出失败属于任务终止,反馈客户端而不是假装模型能在同次请求继续修复。

  • 当前具体日程允许用户明确免除草案卡片确认:评估skipReviewQuote必须逐字来自用户对当前任务的免确认指令,且authorized为true、missing为空才生效。后续预览决策(含超时/拒选)或已核实完成会使旧免确认失效,后续撤销/新任务由评估清空;不可据此扩大删除/预订/付款授权。scripts/test-review-waiver.ts验证用户原话、历史失效、缺项门禁与实际写入。此机制仍依赖模型识别任务范围,不是签名审批系统,无新增环境变量。

旅行安排阅读

旅行 sidebar 按每日路线顺序展示全部有效地点,不截断路线或站点;穿插显示已有游览时间、停留、交通车次、出发时间和备注,长备注两行预览,进入日期查看全文。安排只绑定与当前路线地点顺序完整匹配的 routeChains,缺失或不匹配时不猜测。此视图展示计划,不表示实际到访或已预约;无新增配置、接口或数据库字段。

桌面地图浮动行程卡片与SDK地图同级组件必须使用独立命名空间key(itinerary:provider),禁止共用provider key,否则切页时React错误复用触发地图实例重复挂载。布局仅停稳/resize后合并更新,未变化不提交,切页忽略旧位置。回归脚本scripts/test-itinerary-map-browser.cjs使用Playwright、真实Google SDK与拦截的假行程API,检查切页、镜头、显隐、拖动、空闲请求与地图实例数量;不得用真实行程写入验证。

部署前检查服务器磁盘与Docker缓存空间,根分区可用空间低于10GB时先停止构建并处理空间;只清理可重新生成的未使用构建缓存,不清理数据卷、数据库或运行中的镜像。部署后除Web/Agent和公网HTTP检查外,确认容器健康、重启计数不再增长及磁盘余量;SQLite I/O错误必须先检查磁盘,不将HTTP首页200视为数据库健康。

旅行sidebar每日标题保留日序列,路线预览独立占满可用宽度,不继承标题缩进。旅行/日期地图在低缩放保留静态路线,overview仍按原阈值隐藏;低缩放不播放路线动画。旅行/日期不因hover将其他日期底线压淡。说明卡片208×40,日序与灰色日期/星期同排(地点视图显示地点名),不展示地点串、交通与备注;近邻放不下时搜索地图整个可用区域空位,仍避让侧栏与其他卡片;Google底图隐藏POI标签以突出行程。

进入旅行详情按该旅行所有有效成员/路线地点及待安排地点框选,使用地图工作区侧栏补偿;有地点不得跳目的地中心,空旅行才用首项目的地zoom11兜底。focusFirstMarker=false保持镜头,同页数据刷新不重复框选。

旅行地图日标签仅显示第N天及灰色日期/星期,不重复主题。自动锚点优先最长正数停留(并列全保留),无正数停留才选异常长途所在路线首尾,最后有效地点;首尾同ID的长途往返加距起点最远的不同地点,地点ID去重。路线访问支持isHighlight重点勾选,游览安排弹窗编辑;当天有效匹配路线内有重点访问时仅以重点地点作为标签锚点(marker ID去重),没有则恢复自动规则。重点属于ChainStop,不影响全局地点或其他访问;省略保留、false/null取消,沿访问ID保留排序/日期变更,Web/MCP共用校验及JSON持久化,不新增数据库列或环境变量。异常按全旅行至少3个正距离段、超过20km且超过中位数3倍识别,地理距离仅分类不展示、不额外寻路。标签布局(itinerary-card-layout.ts)为离散优化:每个锚点从固定方向(斜角为主)×短距离档候选中选一个,单项代价偏好近距离、离线留白及路线轮廓外侧的空白方向,成对代价禁止卡片重叠、惩罚引线交叉/穿卡并奖励相邻方向一致与边缘对齐,固定种子模拟退火求解。可见锚点的标签不隐藏:近处放不下退到视口空位,仍冲突时取冲突最小位置。引线沿锚点到卡片中心的射线与边框相交。

日期视图另为交通方式为火车或飞机的有向路段生成交通卡片(地铁、公交、步行等短途暂不生成),锚定在渲染器实际显示路径(含车道分离)的中点,OSM 暂用贝塞尔中点;与地点/日期卡片共用同一布局、样式、引线、不隐藏规则与原生覆盖物,点击框选该段起终点。未填交通安排的路段不生成卡片,不推测方式;卡片内容(方式+车次、出发时间·时长)后续再细化。地图行程卡片排版:日色浅底图标(地点用PlaceIcon、交通用对应方式图标)+ 标题(最多2行);交通有车次时车次作标题,副行为“方式 · 起点 → 终点”;时间行为开始时间(等宽数字浅底)、停留/时长(X小时Y分)与右侧日期色实底“第N天”及灰色日期;布局先按路线与锚点主成分识别宏观形状:线形(竖直/横向/斜向,短轴/长轴方差比≤0.4)标签分列主轴两侧平行带,同侧保持沿轴顺序、法向偏移对齐,补充垂直主轴的候选方向;环形/团状按离心放射方向排外圈并保持绕行角度顺序;备注最多3行,以细分隔线隔开;不使用左侧粗色条。同一地点在多天的重点访问合并为一张卡片,按天逐条列出“第N天/日期/时间/停留/备注”;时间行不换行,卡片宽度随内容在208–280px,布局使用实测宽高;引线1.5px带白色描边,起点为日期色实心圆点加白边;引线长度代价加重,拥挤时优先近处。

地图说明标签与锚点至少间隔80px,SDK地图按当前显示路线屏幕线段避让,OSM用地点骨架避让;全程框选额外缩小0.5档。每个标签及引线挂在锚点经纬度处的零尺寸原生覆盖物内(高德/Google 经 MapRendererHandle.createAnchor,OSM 用 MapLibre Marker),按像素偏移定位,拖动/缩放由地图同帧平移,不用 rAF 投影或隐藏;层级 125,高于地点标记、低于弹窗。停止180ms后重新布局;上一帧位置作为显式候选强滞回,仅越界或明显冲突时才移动;销毁时释放覆盖物、定时器及事件。

OSM 铁路绘图

  • 显式 train 使用内置中国/日本 OSM 铁路快照(src/lib/map/railway-data/*-railway.json.gz),运行时不访问 Overpass,无额外环境变量。邻近轨道节点5km内、最近点+100m候选匹配,无向最短物理路径,不推断班次/换乘,不按车次限制线路;公交/地铁保持原 provider。覆盖中国/日本 railway=rail 非service轨道,不保证指定运营商或特定线路;范围外/不连通返回 NO_ROUTE。

  • 原始轨道及端点接线累计距离,不是营业公里;duration=null,不修改用户安排。图结构进程内复用,Web/MCP共用服务,缓存隔离 osm-rail-offline-v4。更新快照需同步变更缓存版本。火车优先本地OSM;无覆盖/不连通/快照故障时回退原区域服务(境内高德→Google,境外Google),再回退贝塞尔示意线。任一临时故障且后续均无有效路径时保留PLANNING_FAILED与重试,不缓存最终失败;跨境沿用原不寻路规则。MCP显式provider仅限定OSM后的服务。

  • 快照来源、时间、规模与ODbL许可见同目录source JSON及README;刷新时将完整对应国家Overpass out geom导出交给 node scripts/import-railway-snapshot.mjs <export.json> [japan|china],不是部署/启动步骤。Docker和standalone必须包含快照文件;OSM署名所有renderer保留。验证:npx tsx scripts/test-railway-route.ts,禁用网络、隔离SQLite验证真实站点/缓存/API。

  • 铁路与地铁绘图分离:train仅加载rail,中国/日本;subway仅加载subway,目前内置日本地铁快照,中国及其他地区回退原provider。不自动跨轨道类型换乘;混合行程需显式中间地点分段。成功OSM路径source=osm-offline,浏览器与SQLite永久缓存,按坐标/模式/算法及快照版本失效;在线公共交通几何也永久缓存。NO_ROUTE永久缓存,临时失败不缓存。

地图标签当前采用手动模式:旅行/日期视图仅显示isHighlight重点地点访问与重点交通路段;自动停留/长途/末站及自动交通卡片暂停触发,保留算法与优先级代码,itineraryAnchors第三参数false控制,不自动恢复。

公交绘图回退:显式bus按公共交通规划→driving→walking→贝塞尔示意;每级复用区域服务与缓存,境内高德→Google,境外Google,MCP显式provider限定同一服务。缓存隔离bus-fallback-v1,不改交通安排或计划时长;任一级临时故障且全链无有效路径保留重试、不缓存最终失败。跨境规则不变,无新环境变量。

编辑态路线地点卡片隐藏地址,浏览态地址保留;访问备注显示为可点击编辑入口,空时显示“添加访问备注”,复用游览安排弹窗与ChainStop.note,不修改全局地点富文本。

重点只属于ChainStop/ChainLeg的isHighlight,与note同级;整条RouteChain不支持重点。地点/交通安排弹窗内编辑,支持所有交通方式,旅行/日期视图仅为手动重点生成地点/交通标签。字段省略保留、false/null取消、remove交通清除重点,排序跟随访问/有向交通归属。

每日路线色使用高区分度多色调色板,沿用持久化colorIndex,不修改每日色号或日期顺序;地图、侧栏和标签共用routeColor。

重点地点在编辑/浏览地点标题前显示小星标,不在右侧时间区添加“重点地点”文字;安排弹窗保留勾选名称。

手动重点地图标签不显示“地点N”“交通”等重复类型提示;地点为名称+日期,交通为方式/车次+已填时间(无时间用日期)。

手动重点标签扩展多行280×132:标题/日期、计划时间和时长、访问或交通备注;交通标题含方式/车次,地点时长显示停留。未填字段隐藏,长备注卡片内滚动,避让使用相同尺寸;不显示地点序号或重复“交通”类型提示。

  • AI/MCP 的“重点地点”为 stops.isHighlight(一次访问),“重点行程”为 legs.isHighlight(有向相邻交通),均与note同级,仅控制旅行/每日地图重点卡片,不属于整条路线或全局地点;仅按用户明确要求设置,不自动推断。update_day_chain 暴露两种字段,false/null取消、省略保留。无新增环境变量。

  • Web启动进入默认旅行全览:优先开始日期>=本地今天的最近旅行,否则选择开始日期最近的过去旅行;无旅行则总览。不因缺少地点跳过旅行,镜头复用旅行全览逻辑。切换旅行默认显示已有重点卡片,不自动标重点。

  • Web首屏先批量只读SQLite路线缓存,不经provider限速队列;缺失路段才逐段规划。客户端v3键按坐标、模式与策略,不含marker ID;真实路径同步读取,历史回退仅本次会话复用,刷新后由服务端重新核对,避免本地永久回退遮蔽已保存路径。批量接口最多500段且不触发外部寻路。无新增环境变量。