用阿里 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)
141 lines
8.5 KiB
JavaScript
141 lines
8.5 KiB
JavaScript
'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 条,每条「触发条件 → 替换动作」)
|
||
`,
|
||
};
|