Files
tripsense/config.js
T
tamakyi 7ad86c6619 归属判定升级:DataV 官方行政区划表 + 同名县消歧
用阿里 DataV GeoAtlas(免密钥)替换 OSM Overpass 离线县表:
- 民政部 adcode 体系,县级前 4 位即所属地级码,2832 县 100% 可定位地级
  (OSM 表只有 1182/2362,且 admin_level 层级不可靠)
- 同名县(全国 30 组)保留全部候选,运行时按坐标消歧,
  修掉「柳州的城中区被归到西宁市」

归属判定改为「按名字精确查官方表」优先于「按中心点猜最近的县」:
窄长县按中心点会跨省错判(环江喀斯特实际在环江县,按中心点判到荔波县)
查表带距离校验,精确全称命中放宽到 150km(狭长县中心离边界可达 85km+)

修掉三个真实缺陷:
- Overpass 兜底把地级塞进 osmCounty、县名塞进 osmCity,
  与 attachPlaces「先找县级」的顺序相反,导致兜底路径永远丢下层县名
- Overpass 请求无硬超时,公共实例「连着不返回」能把一次分析拖成十几分钟;
  补三层限(单次超时 / 阶段总时限 / is_in 反查次数上限),13 分钟 → 48 秒
- Photon 的 osm.place.city 类目混着景点 POI,kind==='城市' 时无条件把名字
  当地名,导致「社更穿洞北门」「柳州之门」等景点自己成一组;
  改为必须同时是行政区名且能在官方表查到

新增离线回归测试(都不打外部网络,Photon/Overpass 挂掉也能回归):
- test-admin-lookup:同名县消歧、已知边界点、狭长县远端点(19 项)
- test-overpass-fallback:本地假 Overpass,验兜底路径、简称归一、200+HTML 错误页
- test-grouping-regression:本地假 Photon,20 候选完整分组
- verify-ui-full:端到端 UI 验收(真实无头 Chrome 读真实 DOM)
2026-11-06 02:05:24 +08:00

141 lines
8.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* TripSense · 周边出行顾问 —— 全局配置
*
* 所有密钥优先从环境变量读取;也可以在网页「AI 接口设置」里临时覆盖
* (覆盖值只保存在浏览器 localStorage,并由前端通过请求头传给本服务代理)。
* 生产环境请只用环境变量,不要把密钥写进前端。
*/
const env = process.env;
module.exports = {
PORT: Number(env.PORT || 8787),
// 默认绑定 0.0.0.0,方便手机 / 同网段设备直接访问。
// 只在本机用就把 HOST 设成 127.0.0.1(`HOST=127.0.0.1 node server.js`)。
HOST: env.HOST || '0.0.0.0',
// 端口被占用时,server.js 会自动往后找可用端口,启动日志会打印实际地址。
// ---------------------------------------------------------------- 数据源
ENDPOINTS: {
geocoding: 'https://geocoding-api.open-meteo.com/v1/search',
forecast: 'https://api.open-meteo.com/v1/forecast',
archive: 'https://archive-api.open-meteo.com/v1/archive',
airQuality: 'https://air-quality-api.open-meteo.com/v1/air-quality',
photon: env.PHOTON_ENDPOINT || 'https://photon.komoot.io/api',
// Overpass 兜底:Photon 全挂时用(免密钥,但单次查询要 20~120 秒)。
// 公共实例会限流/限容量,必须多镜像轮换;osm.ch 覆盖较全但偶尔返回
// 「duplicate_query」错误页,靠响应体是否以 '{' 开头来判定真伪。
overpass: env.OVERPASS_ENDPOINTS
? env.OVERPASS_ENDPOINTS.split(',').map((s) => s.trim()).filter(Boolean)
: [
'https://overpass-api.de/api/interpreter',
'https://overpass.kumi.systems/api/interpreter',
'https://overpass.private.coffee/api/interpreter',
'https://overpass.osm.ch/api/interpreter',
'https://overpass.osm.jp/api/interpreter',
],
reverse: 'https://api.bigdatacloud.net/data/reverse-geocode-client',
osrmTable: 'https://router.project-osrm.org/table/v1/driving',
amapDriving: 'https://restapi.amap.com/v3/direction/driving',
amapAround: 'https://restapi.amap.com/v3/place/around',
amapGeocode: 'https://restapi.amap.com/v3/geocode/geo',
amapPlaceText: 'https://restapi.amap.com/v3/place/text',
// 反向地理编码(坐标 → 地名):BigDataCloud / Photon reverse 在国内常被限流或返回空,
// 高德 regeo 能稳定拿到「城市 + 区县 + 街道」,是住宿城市兜底的关键
amapRegeo: 'https://restapi.amap.com/v3/geocode/regeo',
// 静态地图(真实地图底图):仅使用合规白名单内的地图服务商,Key 由后端代理,不下发前端
amapStaticMap: 'https://restapi.amap.com/v3/staticmap',
},
// 选填:高德 Web 服务 Key。配置后公路耗时改用高德(国内路网/限行更贴近实际)
AMAP_KEY: env.AMAP_KEY || '',
// 选填:铁路/高铁接口模板(留给 12306 或第三方服务)。
// 支持的占位符:{fromLat} {fromLon} {toLat} {toLon} {date} {fromName} {toName}
// 期望返回 JSON:{ available:boolean, durationMinutes:number, distanceKm:number, fareCny:number, note:string }
// 未配置或调用失败时,自动降级为「里程-速度」估算,并在结果里明确标注「估算」。
RAIL_API_URL: env.RAIL_API_URL || '',
RAIL_API_KEY: env.RAIL_API_KEY || '',
// 选填:住宿价格数据源(高德 POI 的价格字段覆盖很差,多数酒店没有参考价)。
// 支持占位符:{name} {lat} {lon} {city}
// 期望返回 JSON:[{ name:string, price:number, currency?:string, url?:string }]
// 按酒店名模糊匹配后合并;未配置或失败时不影响主流程,只是没有价格。
HOTEL_PRICE_API_URL: env.HOTEL_PRICE_API_URL || '',
HOTEL_PRICE_API_KEY: env.HOTEL_PRICE_API_KEY || '',
// 选填:住宿数据来源。auto = 装了 rgh 且已登录就用 RollingGo,否则用高德 POI
// amap 只用高德 POI(无实时房价,但零配置)
// rollinggo 只尝试本机 rgh CLI(OAuth 登录后才有实时报价)
// 注意:rgh 只以只读方式调用(status / search-hotels / hotel-detail),
// 绝不调用 price-confirm / book / cancel 这类会产生真实交易的子命令。
HOTEL_PROVIDER: (env.HOTEL_PROVIDER || 'auto').toLowerCase(),
RHG_BIN: env.RHG_BIN || 'rgh',
ROLLINGGO_TIMEOUT_MS: Number(env.ROLLINGGO_TIMEOUT_MS || 25000),
// 检索参数
SEARCH: {
maxRadiusKm: 300, // 最大检索半径
photonConcurrency: 5, // 对公共 Photon 服务保持克制
photonPerCategory: 8, // 每个类别取多少条
photonTimeoutMs: 4000, // Photon 公共实例易被限流,快速失败并走兜底
photonRetries: 0,
maxCandidates: 28, // 进入天气/路网计算的候选上限
requestTimeoutMs: 15000, // 天气/地理编码等主数据源
osrmTimeoutMs: 12000,
amapAroundRadius: 50000, // 高德 around 检索上限 50km
// Overpass 兜底的超时。公共实例经常「连着不返回」(不报错也不结束),
// 没有硬超时的话一次兜底能把整个分析流程拖成十几分钟。
overpassTimeoutMs: Number(env.OVERPASS_TIMEOUT_MS || 20000),
// 逐点 is_in 反查的上限:最多反查这么多个点,
// 超出部分改用 bbox 行政区中心点近邻判定(近似但快得多)。
overpassIsInMax: Number(env.OVERPASS_ISIN_MAX || 8),
},
// ------------------------------------------------- OpenAI 兼容接口(预留)
AI: {
// 任何兼容 /chat/completions 的服务都可以直接填这里
BASE_URL: env.AI_BASE_URL || 'https://api.openai.com/v1',
API_KEY: env.AI_API_KEY || '',
MODEL: env.AI_MODEL || 'gpt-4o-mini',
TEMPERATURE: Number(env.AI_TEMPERATURE || 0.7),
MAX_TOKENS: Number(env.AI_MAX_TOKENS || 8000),
TIMEOUT_MS: Number(env.AI_TIMEOUT_MS || 120000),
},
// ------------------------------------------------------------ 预设提示词
// 这段提示词把「本站能做什么、数据从哪来、哪些是估算」写死给模型,
// 避免模型编造票价/班次/景点,也保证输出结构稳定。
SYSTEM_PROMPT: `你是「行知 · 周边出行顾问」的行程规划师。只使用输入 JSON 中的数据,禁止编造。
【输入】
origin 出发地;timeRange 起止/天数;constraints 单程上限与兴趣;
shortlist 候选(name/kind/tags/distanceKm/direction/score/verdict/travel/weather/aqi);
只有 rank1 带 weather.daily 与 road/rail 明细,其余候选只有区间摘要与 bestHours;
nearby 目的地周边可点名地点(food 餐饮、sights 景点,含 name/type/distanceKm/rating/cost);
hotels 住宿 POI;lodging 用户价位区间;userNeeds 用户需求;dataNotes 数据口径。
【规则】
1. 不编造未提供的景点、餐厅、票价、班次、营业时间;需要用户核实的写「出发前核对」。
2. 估算数据(铁路耗时票价、历史气候替代、路网兜底)必须标注「估算」,不得当事实。
3. 有 nearby 时,吃和玩必须从中点名:「名称(类型,距目的地 X km,评分 Y,人均 ¥Z)」,cost 没有就不写。
禁止用「附近有很多小吃」这类空话代替点名。某一类确实缺失,才退回「片区 + 筛选方法」,并说明缺的是哪类。
4. 住宿从 hotels 里挑,写名称 + 距离 + 参考价,注明房态与实时价需在平台核实;
lodging 有 min/max 就必须落在区间内,与 hotels 价格冲突时直说并建议放宽,不要硬塞进预算。
5. 住宿靠近当天主要活动地;跨片区要说明是否换住处、换的成本。
6. 发现现实冲突(时间不够、天气差、闭馆、预算不足)直接指出,不要讨好式输出。
【输出】Markdown,直接从「## 方案总览」开始,不要开场白、不要复述规则。
篇幅硬约束:全文 ≤ 900 字;表格单元格 ≤ 18 字;只写可执行结论,不复述输入数据。
## 方案总览(结论 + 主推理由 + 备选及原因 + 总节奏)
## 逐日安排(### D1(日期 星期)+ 表格:时段|安排|交通与耗时|天气应对;雨天给室内替换)
## 天气与装备(逐日摘要 + 穿衣/雨具 + 降雨大风兜底动作)
## 交通方案(按允许方式写耗时/里程/来源标注/停车换乘/末班提醒,车次余票需自行核对)
## 预算粗估(表格:交通/门票/餐饮/住宿/其他 + 合计,注明粗略、不含购物)
## 出发前清单(预订、证件、装备、需复核的信息)
## 风险与备选(2-4 条,每条「触发条件 → 替换动作」)
`,
};