归属判定升级: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)
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
'use strict';
|
||||
/**
|
||||
* 前端分组调试:起本地假 Photon + 服务端,然后在真实页面里读候选字段。
|
||||
*/
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const http = require('http');
|
||||
|
||||
const CHROME = 'C:/Program Files/Google/Chrome/Application/chrome.exe';
|
||||
const PORT = 9356;
|
||||
const PHOTON_PORT = 9357;
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
const REAL = [
|
||||
['荔波小七孔', 25.2461, 107.8567, '黔南州', '荔波县'],
|
||||
['社更穿洞北门', 24.5701, 107.7234, '河池市', '都安瑶族自治县'],
|
||||
['万寿谷', 24.3312, 107.8123, '河池市', '都安瑶族自治县'],
|
||||
['柳州之门', 24.3089, 109.4112, '柳州市', '城中区'],
|
||||
['八里九弯', 23.6712, 107.2345, '百色市', '平果市'],
|
||||
['加榜梯田', 25.8571, 108.6388, '黔东南州', '从江县'],
|
||||
];
|
||||
|
||||
class CDP {
|
||||
constructor(ws) { this.ws = ws; this.id = 0; this.w = new Map(); }
|
||||
static async connect(u) {
|
||||
const ws = new WebSocket(u);
|
||||
await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; });
|
||||
const c = new CDP(ws);
|
||||
ws.onmessage = (e) => { const m = JSON.parse(e.data); if (m.id && c.w.has(m.id)) { c.w.get(m.id)(m); c.w.delete(m.id); } };
|
||||
return c;
|
||||
}
|
||||
send(method, params = {}, sessionId) {
|
||||
const id = ++this.id;
|
||||
return new Promise((res) => { this.w.set(id, res); this.ws.send(JSON.stringify({ id, method, params, sessionId })); });
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const photon = http.createServer((req, res) => {
|
||||
const u = new URL(req.url, 'http://x');
|
||||
const lim = Number(u.searchParams.get('limit') || 8);
|
||||
const feats = REAL.slice(0, lim).map((r, i) => ({
|
||||
type: 'Feature',
|
||||
geometry: { type: 'Point', coordinates: [r[2], r[1]] },
|
||||
properties: { name: r[0], osm_type: 'N', osm_id: 1000 + i, county: r[3], city: r[4], district: '', state: 'X' },
|
||||
}));
|
||||
res.writeHead(200, { 'content-type': 'application/json' });
|
||||
res.end(JSON.stringify({ features: feats }));
|
||||
});
|
||||
await new Promise((r) => photon.listen(PHOTON_PORT, '127.0.0.1', r));
|
||||
|
||||
const server = spawn('node', ['server.js'], {
|
||||
cwd: path.join(__dirname, '..'),
|
||||
env: { ...process.env, PHOTON_ENDPOINT: `http://127.0.0.1:${PHOTON_PORT}/api` },
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
let banner = '';
|
||||
server.stdout.on('data', (d) => { banner += d.toString(); });
|
||||
server.stderr.on('data', (d) => { banner += d.toString(); });
|
||||
await sleep(3000);
|
||||
const base = (banner.match(/http:\/\/127\.0\.0\.1:(\d+)/) || [])[1];
|
||||
console.log('base =', base);
|
||||
|
||||
const chrome = spawn(CHROME, ['--headless=new', '--disable-gpu', '--hide-scrollbars',
|
||||
`--remote-debugging-port=${PORT}`, '--user-data-dir=' + path.join(os.tmpdir(), 'dbg' + Date.now()), 'about:blank'], { stdio: 'ignore' });
|
||||
let wsUrl = null;
|
||||
for (let i = 0; i < 30 && !wsUrl; i++) {
|
||||
await sleep(500);
|
||||
try { const r = await fetch(`http://127.0.0.1:${PORT}/json/version`); wsUrl = (await r.json()).webSocketDebuggerUrl; } catch (e) { /* wait */ }
|
||||
}
|
||||
const b = await CDP.connect(wsUrl);
|
||||
const { result: { targetId } } = await b.send('Target.createTarget', { url: 'about:blank' });
|
||||
const { result: { sessionId } } = await b.send('Target.attachToTarget', { targetId, flatten: true });
|
||||
const S = (m, p) => b.send(m, p, sessionId);
|
||||
await S('Page.enable'); await S('Runtime.enable');
|
||||
await S('Page.navigate', { url: `http://127.0.0.1:${base}` });
|
||||
await sleep(2500);
|
||||
const ev = async (e) => {
|
||||
const r = await S('Runtime.evaluate', { expression: e, awaitPromise: true, returnByValue: true });
|
||||
if (r.result?.exceptionDetails) throw new Error(JSON.stringify(r.result.exceptionDetails).slice(0, 300));
|
||||
return r.result?.result?.value;
|
||||
};
|
||||
|
||||
const raw = await ev(`(async () => {
|
||||
const res = await fetch('/api/analyze', {
|
||||
method: 'POST', headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ origin: { name: '河池市金城江区', lat: 24.69562, lon: 108.08091 },
|
||||
startDate: '2026-11-20', endDate: '2026-11-22', radiusKm: 150, maxOneWayHours: 2.5, excludeLocal: true }),
|
||||
});
|
||||
const txt = await res.text();
|
||||
for (const line of txt.split('\\n')) {
|
||||
if (!line.startsWith('data:')) continue;
|
||||
const j = JSON.parse(line.slice(5).trim());
|
||||
if (j.type === 'result') return j.data;
|
||||
}
|
||||
return null;
|
||||
})()`);
|
||||
console.log('接口原始 candidates[0] 字段:', Object.keys(raw.candidates[0]).join(','));
|
||||
console.log('接口原始 place =', raw.candidates.map((c) => `${c.name}:${c.place}/${c.placeKey}/${c.placeDetail}`).join(' | '));
|
||||
|
||||
await ev(`(() => { state.analysis = ${JSON.stringify(raw)}; gotoStep(2); renderStep2(); renderCards(); return true; })()`);
|
||||
await sleep(1200);
|
||||
const after = await ev(`(() => {
|
||||
const c = state.analysis.candidates.find(x => x.name === '社更穿洞北门');
|
||||
return { cand: c ? { name: c.name, place: c.place, placeKey: c.placeKey, placeDetail: c.placeDetail } : null,
|
||||
groups: groupByPlace(sortedCandidates()).map(g => g.key + '|' + g.place) };
|
||||
})()`);
|
||||
console.log('注入后:', JSON.stringify(after, null, 1));
|
||||
|
||||
b.ws.close(); chrome.kill(); server.kill(); photon.close();
|
||||
process.exit(0);
|
||||
})().catch((e) => { console.error('FAIL', e.message); process.exit(1); });
|
||||
Reference in New Issue
Block a user