@caoguo/maplibre-ai 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -39
- package/dist/chunk-55G7QUFA.js +293 -0
- package/dist/chunk-55G7QUFA.js.map +1 -0
- package/dist/chunk-ARYB2X4B.js +348 -0
- package/dist/chunk-ARYB2X4B.js.map +1 -0
- package/dist/chunk-HF3HJPXD.js +435 -0
- package/dist/chunk-HF3HJPXD.js.map +1 -0
- package/dist/chunk-ZI6ZF26Q.js +149 -0
- package/dist/chunk-ZI6ZF26Q.js.map +1 -0
- package/dist/copilot/index.cjs +302 -0
- package/dist/copilot/index.cjs.map +1 -0
- package/dist/copilot/index.d.cts +108 -0
- package/dist/copilot/index.d.ts +108 -0
- package/dist/copilot/index.js +3 -0
- package/dist/copilot/index.js.map +1 -0
- package/dist/geoai/index.cjs +449 -0
- package/dist/geoai/index.cjs.map +1 -0
- package/dist/geoai/index.d.cts +212 -0
- package/dist/geoai/index.d.ts +212 -0
- package/dist/geoai/index.js +3 -0
- package/dist/geoai/index.js.map +1 -0
- package/dist/index.cjs +1246 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/llm/index.cjs +152 -0
- package/dist/llm/index.cjs.map +1 -0
- package/dist/llm/index.d.cts +81 -0
- package/dist/llm/index.d.ts +81 -0
- package/dist/llm/index.js +3 -0
- package/dist/llm/index.js.map +1 -0
- package/dist/nlpg/index.cjs +359 -0
- package/dist/nlpg/index.cjs.map +1 -0
- package/dist/nlpg/index.d.cts +160 -0
- package/dist/nlpg/index.d.ts +160 -0
- package/dist/nlpg/index.js +3 -0
- package/dist/nlpg/index.js.map +1 -0
- package/package.json +36 -4
package/dist/index.cjs
CHANGED
|
@@ -237,19 +237,1265 @@ function toggleMode(style) {
|
|
|
237
237
|
};
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
// src/copilot/copilot.ts
|
|
241
|
+
var PLACE_COORDINATES = {
|
|
242
|
+
\u6B66\u6C49: [114.305, 30.593],
|
|
243
|
+
\u5149\u8C37: [114.428, 30.507],
|
|
244
|
+
\u6C49\u53E3: [114.268, 30.586],
|
|
245
|
+
\u6B66\u660C: [114.316, 30.554],
|
|
246
|
+
\u5317\u4EAC: [116.407, 39.904],
|
|
247
|
+
\u4E0A\u6D77: [121.474, 31.23],
|
|
248
|
+
\u5E7F\u5DDE: [113.264, 23.129],
|
|
249
|
+
\u6DF1\u5733: [114.058, 22.543],
|
|
250
|
+
\u6210\u90FD: [104.067, 30.573],
|
|
251
|
+
\u676D\u5DDE: [120.155, 30.274],
|
|
252
|
+
\u5357\u4EAC: [118.796, 32.06],
|
|
253
|
+
\u91CD\u5E86: [106.551, 29.563],
|
|
254
|
+
\u897F\u5B89: [108.94, 34.341],
|
|
255
|
+
\u5929\u6D25: [117.2, 39.084]
|
|
256
|
+
};
|
|
257
|
+
var COLOR_NAMES = {
|
|
258
|
+
\u7EA2: "#ef4444",
|
|
259
|
+
\u7EA2\u8272: "#ef4444",
|
|
260
|
+
\u6A59: "#f97316",
|
|
261
|
+
\u6A59\u8272: "#f97316",
|
|
262
|
+
\u9EC4: "#f59e0b",
|
|
263
|
+
\u9EC4\u8272: "#f59e0b",
|
|
264
|
+
\u7EFF: "#22c55e",
|
|
265
|
+
\u7EFF\u8272: "#22c55e",
|
|
266
|
+
\u9752: "#22d3ee",
|
|
267
|
+
\u84DD\u8272: "#3b82f6",
|
|
268
|
+
\u84DD: "#3b82f6",
|
|
269
|
+
\u7D2B: "#8b5cf6",
|
|
270
|
+
\u7D2B\u8272: "#8b5cf6",
|
|
271
|
+
\u7C89: "#ec4899",
|
|
272
|
+
\u7070: "#6b7280",
|
|
273
|
+
\u9ED1\u8272: "#111827",
|
|
274
|
+
\u767D\u8272: "#ffffff"
|
|
275
|
+
};
|
|
276
|
+
var INTENT_PATTERNS = [
|
|
277
|
+
{ intent: "popup_interaction", re: /弹窗|信息窗|弹出|popup|点击.*?显示|点击.*?弹出|点击.*?信息/, confidence: 0.92 },
|
|
278
|
+
{ intent: "heatmap", re: /热力|heatmap|热度|密度图|热图/, confidence: 0.9 },
|
|
279
|
+
{ intent: "add_line_polygon", re: /路线|连线|线段|画.{0,2}(线|面)|多边形|折线|面要素|LineString|polygon/, confidence: 0.88 },
|
|
280
|
+
{ intent: "add_marker", re: /标记|marker|点标记|加点|标注|mark/, confidence: 0.86 },
|
|
281
|
+
{ intent: "create_map", re: /地图|初始化|创建|生成|画布|渲染|map/, confidence: 0.8 }
|
|
282
|
+
];
|
|
283
|
+
function classifyIntent(query) {
|
|
284
|
+
let best = { intent: "unknown", confidence: 0 };
|
|
285
|
+
for (const p of INTENT_PATTERNS) {
|
|
286
|
+
const m = query.match(p.re);
|
|
287
|
+
if (m) {
|
|
288
|
+
const c = p.confidence * (1 + 0.03 * Math.min(m[0].length / query.length, 1));
|
|
289
|
+
if (c > best.confidence) best = { intent: p.intent, confidence: Math.min(c, 1) };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return best;
|
|
293
|
+
}
|
|
294
|
+
function extractParams(query, intent) {
|
|
295
|
+
const params = {};
|
|
296
|
+
for (const [name, coord] of Object.entries(PLACE_COORDINATES)) {
|
|
297
|
+
if (query.includes(name)) {
|
|
298
|
+
params.place = name;
|
|
299
|
+
params.center = coord;
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const zoomMatch = query.match(/缩放\s*(\d{1,2})|zoom\s*(\d{1,2})|级别\s*(\d{1,2})/i);
|
|
304
|
+
if (zoomMatch) {
|
|
305
|
+
const z = parseInt(zoomMatch[1] ?? zoomMatch[2] ?? zoomMatch[3]);
|
|
306
|
+
if (!Number.isNaN(z) && z >= 0 && z <= 22) params.zoom = z;
|
|
307
|
+
}
|
|
308
|
+
if (/暗色|暗黑|深色|dark|夜间/.test(query)) params.style = "caoguo-dark";
|
|
309
|
+
else if (/亮色|浅色|白色|light|白天/.test(query)) params.style = "caoguo-light";
|
|
310
|
+
for (const [name, hex] of Object.entries(COLOR_NAMES)) {
|
|
311
|
+
if (query.includes(name)) {
|
|
312
|
+
params.color = hex;
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const sizeMatch = query.match(/(?:宽度|半径|大小|width|size)\s*(\d+)/i);
|
|
317
|
+
if (sizeMatch) params.size = parseInt(sizeMatch[1]);
|
|
318
|
+
const layerMatch = query.match(/(?:图层|layer)\s*["']?([\w-]+)["']?/i);
|
|
319
|
+
if (layerMatch) params.layerId = layerMatch[1];
|
|
320
|
+
const textMatch = query.match(/["「『]([^"」』]+)["」』]/);
|
|
321
|
+
if (textMatch) params.text = textMatch[1];
|
|
322
|
+
return params;
|
|
323
|
+
}
|
|
324
|
+
function generateCode(intent, p) {
|
|
325
|
+
const center = p.center ?? [114.305, 30.593];
|
|
326
|
+
const zoom = p.zoom ?? 12;
|
|
327
|
+
const style = p.style ?? "caoguo-dark";
|
|
328
|
+
const color = p.color ?? "#ef4444";
|
|
329
|
+
const size = p.size ?? 8;
|
|
330
|
+
const layerId = p.layerId ?? "my-layer";
|
|
331
|
+
switch (intent) {
|
|
332
|
+
case "create_map":
|
|
333
|
+
return [
|
|
334
|
+
`const map = new CaoguoMap.Map({`,
|
|
335
|
+
` container: 'map',`,
|
|
336
|
+
` center: [${center[0]}, ${center[1]}],`,
|
|
337
|
+
` zoom: ${zoom},`,
|
|
338
|
+
` style: '${style}',`,
|
|
339
|
+
`});`
|
|
340
|
+
].join("\n");
|
|
341
|
+
case "add_marker":
|
|
342
|
+
return [
|
|
343
|
+
`map.addSource('marker-src', {`,
|
|
344
|
+
` type: 'geojson',`,
|
|
345
|
+
` data: { type: 'Feature', geometry: { type: 'Point', coordinates: [${center[0]}, ${center[1]}] }, properties: {} },`,
|
|
346
|
+
`});`,
|
|
347
|
+
`map.addLayer({`,
|
|
348
|
+
` id: '${layerId}',`,
|
|
349
|
+
` type: 'circle',`,
|
|
350
|
+
` source: 'marker-src',`,
|
|
351
|
+
` paint: { 'circle-radius': ${size}, 'circle-color': '${color}' },`,
|
|
352
|
+
`});`
|
|
353
|
+
].join("\n");
|
|
354
|
+
case "add_line_polygon":
|
|
355
|
+
return [
|
|
356
|
+
`map.addSource('line-src', {`,
|
|
357
|
+
` type: 'geojson',`,
|
|
358
|
+
` data: { type: 'Feature', geometry: { type: 'LineString', coordinates: [[${center[0]}, ${center[1]}], [${(center[0] + 0.05).toFixed(3)}, ${(center[1] + 0.03).toFixed(3)}]] }, properties: {} },`,
|
|
359
|
+
`});`,
|
|
360
|
+
`map.addLayer({`,
|
|
361
|
+
` id: '${layerId}',`,
|
|
362
|
+
` type: 'line',`,
|
|
363
|
+
` source: 'line-src',`,
|
|
364
|
+
` paint: { 'line-color': '${color}', 'line-width': ${Math.max(2, size)} },`,
|
|
365
|
+
`});`
|
|
366
|
+
].join("\n");
|
|
367
|
+
case "heatmap":
|
|
368
|
+
return [
|
|
369
|
+
`map.addSource('heat-src', {`,
|
|
370
|
+
` type: 'geojson',`,
|
|
371
|
+
` data: { type: 'FeatureCollection', features: [] },`,
|
|
372
|
+
`});`,
|
|
373
|
+
`map.addLayer({`,
|
|
374
|
+
` id: '${layerId}',`,
|
|
375
|
+
` type: 'heatmap',`,
|
|
376
|
+
` source: 'heat-src',`,
|
|
377
|
+
` paint: {`,
|
|
378
|
+
` 'heatmap-weight': 1,`,
|
|
379
|
+
` 'heatmap-color': ['interpolate', ['linear'], ['heatmap-density'], 0, 'rgba(33,102,172,0)', 0.5, '${color}', 1, 'rgb(239,68,68)'],`,
|
|
380
|
+
` 'heatmap-radius': ${Math.max(20, size * 4)},`,
|
|
381
|
+
` },`,
|
|
382
|
+
`});`
|
|
383
|
+
].join("\n");
|
|
384
|
+
case "popup_interaction":
|
|
385
|
+
return [
|
|
386
|
+
`map.on('click', '${layerId}', (e) => {`,
|
|
387
|
+
` const coords = e.features[0].geometry.coordinates.slice();`,
|
|
388
|
+
` new maplibregl.Popup()`,
|
|
389
|
+
` .setLngLat(coords)`,
|
|
390
|
+
` .setHTML('${p.text ?? "\u8981\u7D20\u4FE1\u606F"}')`,
|
|
391
|
+
` .addTo(map.instance);`,
|
|
392
|
+
`});`
|
|
393
|
+
].join("\n");
|
|
394
|
+
default:
|
|
395
|
+
return `// \u65E0\u6CD5\u8BC6\u522B\u7684\u610F\u56FE\uFF1A${intent}`;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function generateFromQuery(query) {
|
|
399
|
+
const { intent, confidence } = classifyIntent(query);
|
|
400
|
+
const params = extractParams(query);
|
|
401
|
+
const code = generateCode(intent, params);
|
|
402
|
+
return {
|
|
403
|
+
intent,
|
|
404
|
+
params,
|
|
405
|
+
code,
|
|
406
|
+
confidence,
|
|
407
|
+
description: buildDescription(intent, params)
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
function buildDescription(intent, p) {
|
|
411
|
+
const parts = [];
|
|
412
|
+
switch (intent) {
|
|
413
|
+
case "create_map":
|
|
414
|
+
parts.push("\u521B\u5EFA\u57FA\u7840\u5730\u56FE");
|
|
415
|
+
break;
|
|
416
|
+
case "add_marker":
|
|
417
|
+
parts.push("\u6DFB\u52A0\u70B9\u6807\u8BB0");
|
|
418
|
+
break;
|
|
419
|
+
case "add_line_polygon":
|
|
420
|
+
parts.push("\u6DFB\u52A0\u7EBF/\u9762\u8981\u7D20");
|
|
421
|
+
break;
|
|
422
|
+
case "heatmap":
|
|
423
|
+
parts.push("\u751F\u6210\u70ED\u529B\u56FE");
|
|
424
|
+
break;
|
|
425
|
+
case "popup_interaction":
|
|
426
|
+
parts.push("\u7ED1\u5B9A\u70B9\u51FB\u5F39\u7A97");
|
|
427
|
+
break;
|
|
428
|
+
default:
|
|
429
|
+
parts.push("\u672A\u77E5\u610F\u56FE");
|
|
430
|
+
}
|
|
431
|
+
if (p.place) parts.push(`\u5730\u70B9:${p.place}`);
|
|
432
|
+
if (p.color) parts.push(`\u989C\u8272:${p.color}`);
|
|
433
|
+
return parts.join(" ");
|
|
434
|
+
}
|
|
435
|
+
var MapCopilot = class {
|
|
436
|
+
lastIntent = "create_map";
|
|
437
|
+
lastParams = {};
|
|
438
|
+
/** 生成代码(支持上下文增量修改) */
|
|
439
|
+
generate(query) {
|
|
440
|
+
const { intent, confidence } = classifyIntent(query);
|
|
441
|
+
const isModification = /改成|改为|换成|变成|调整为|设置成/.test(query);
|
|
442
|
+
if (isModification && this.lastIntent !== "unknown") {
|
|
443
|
+
const delta = extractParams(query, this.lastIntent);
|
|
444
|
+
this.lastParams = { ...this.lastParams, ...delta };
|
|
445
|
+
const code2 = generateCode(this.lastIntent, this.lastParams);
|
|
446
|
+
return {
|
|
447
|
+
intent: this.lastIntent,
|
|
448
|
+
params: { ...this.lastParams },
|
|
449
|
+
code: code2,
|
|
450
|
+
confidence,
|
|
451
|
+
description: buildDescription(this.lastIntent, this.lastParams)
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
this.lastIntent = intent;
|
|
455
|
+
this.lastParams = extractParams(query);
|
|
456
|
+
const code = generateCode(intent, this.lastParams);
|
|
457
|
+
return {
|
|
458
|
+
intent,
|
|
459
|
+
params: { ...this.lastParams },
|
|
460
|
+
code,
|
|
461
|
+
confidence,
|
|
462
|
+
description: buildDescription(intent, this.lastParams)
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
/** 重置上下文 */
|
|
466
|
+
reset() {
|
|
467
|
+
this.lastIntent = "create_map";
|
|
468
|
+
this.lastParams = {};
|
|
469
|
+
}
|
|
470
|
+
/** 获取当前上下文 */
|
|
471
|
+
get context() {
|
|
472
|
+
return { intent: this.lastIntent, params: { ...this.lastParams } };
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
// src/copilot/llmCopilot.ts
|
|
477
|
+
var SYSTEM_PROMPT = `\u4F60\u662F"\u8349\u679C\u5730\u56FE"(CaoguoMap) \u7684\u5730\u56FE\u4EE3\u7801\u751F\u6210\u52A9\u624B\u3002\u7528\u6237\u7528\u81EA\u7136\u8BED\u8A00\u63CF\u8FF0\u5730\u56FE\u9700\u6C42\uFF0C\u4F60\u751F\u6210\u5BF9\u5E94\u7684 MapLibre GL JS \u4EE3\u7801\u7247\u6BB5\u3002
|
|
478
|
+
|
|
479
|
+
\u8349\u679C\u5730\u56FE API \u7EA6\u5B9A\uFF1A
|
|
480
|
+
- \u5730\u56FE\u5B9E\u4F8B\uFF1Anew CaoguoMap.Map({ container, center, zoom, style })
|
|
481
|
+
- \u6DFB\u52A0\u70B9\uFF1Amap.addSource + map.addLayer({ type: 'circle' })
|
|
482
|
+
- \u6DFB\u52A0\u7EBF\uFF1Amap.addLayer({ type: 'line' })
|
|
483
|
+
- \u70ED\u529B\u56FE\uFF1Amap.addLayer({ type: 'heatmap' })
|
|
484
|
+
- \u5F39\u7A97\uFF1Amap.on('click', layerId, cb) + new maplibregl.Popup()
|
|
485
|
+
|
|
486
|
+
\u8981\u6C42\uFF1A
|
|
487
|
+
1. \u53EA\u8FD4\u56DE JSON\uFF0C\u683C\u5F0F\uFF1A{"intent":"create_map|add_marker|add_line_polygon|heatmap|popup_interaction","code":"...","description":"..."}
|
|
488
|
+
2. code \u5FC5\u987B\u662F\u53EF\u76F4\u63A5\u8FD0\u884C\u7684 JavaScript \u4EE3\u7801\u7247\u6BB5\uFF08\u4E0D\u542B import\uFF09\uFF0C\u7981\u6B62\u5305\u542B\u4EFB\u4F55 fetch/\u7F51\u7EDC\u8BF7\u6C42/\u6587\u4EF6\u7CFB\u7EDF/\u5371\u9669\u64CD\u4F5C\u3002
|
|
489
|
+
3. \u82E5\u9700\u6C42\u8D85\u51FA\u5730\u56FE\u4EE3\u7801\u8303\u7574\uFF0C\u8FD4\u56DE {"intent":"unknown","code":"","description":"\u65E0\u6CD5\u8BC6\u522B\u7684\u9700\u6C42"}\u3002
|
|
490
|
+
4. \u4E0D\u8981\u8F93\u51FA JSON \u4EE5\u5916\u7684\u4EFB\u4F55\u6587\u5B57\u3002`;
|
|
491
|
+
var LlmMapCopilot = class {
|
|
492
|
+
client;
|
|
493
|
+
enabled;
|
|
494
|
+
constructor(config) {
|
|
495
|
+
this.client = config.client;
|
|
496
|
+
this.enabled = config.enabled ?? true;
|
|
497
|
+
}
|
|
498
|
+
/** 生成代码:LLM 优先,失败降级规则引擎 */
|
|
499
|
+
async generate(query) {
|
|
500
|
+
if (!this.enabled) {
|
|
501
|
+
return generateFromQuery(query);
|
|
502
|
+
}
|
|
503
|
+
try {
|
|
504
|
+
return await this.generateWithLlm(query);
|
|
505
|
+
} catch {
|
|
506
|
+
return generateFromQuery(query);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
async generateWithLlm(query) {
|
|
510
|
+
const messages = [
|
|
511
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
512
|
+
{ role: "user", content: query }
|
|
513
|
+
];
|
|
514
|
+
const { data } = await this.client.chatJson(messages);
|
|
515
|
+
const intent = data.intent ?? "unknown";
|
|
516
|
+
const code = data.code ?? "";
|
|
517
|
+
if (intent === "unknown" || !code.trim()) {
|
|
518
|
+
return generateFromQuery(query);
|
|
519
|
+
}
|
|
520
|
+
return {
|
|
521
|
+
intent,
|
|
522
|
+
params: {},
|
|
523
|
+
code,
|
|
524
|
+
confidence: 0.9,
|
|
525
|
+
description: data.description ?? `LLM \u751F\u6210\uFF08${intent}\uFF09`
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// src/geoai/addressParser.ts
|
|
531
|
+
var PROVINCES = [
|
|
532
|
+
"\u5317\u4EAC",
|
|
533
|
+
"\u4E0A\u6D77",
|
|
534
|
+
"\u5929\u6D25",
|
|
535
|
+
"\u91CD\u5E86",
|
|
536
|
+
"\u6CB3\u5317",
|
|
537
|
+
"\u5C71\u897F",
|
|
538
|
+
"\u8FBD\u5B81",
|
|
539
|
+
"\u5409\u6797",
|
|
540
|
+
"\u9ED1\u9F99\u6C5F",
|
|
541
|
+
"\u6C5F\u82CF",
|
|
542
|
+
"\u6D59\u6C5F",
|
|
543
|
+
"\u5B89\u5FBD",
|
|
544
|
+
"\u798F\u5EFA",
|
|
545
|
+
"\u6C5F\u897F",
|
|
546
|
+
"\u5C71\u4E1C",
|
|
547
|
+
"\u6CB3\u5357",
|
|
548
|
+
"\u6E56\u5317",
|
|
549
|
+
"\u6E56\u5357",
|
|
550
|
+
"\u5E7F\u4E1C",
|
|
551
|
+
"\u6D77\u5357",
|
|
552
|
+
"\u56DB\u5DDD",
|
|
553
|
+
"\u8D35\u5DDE",
|
|
554
|
+
"\u4E91\u5357",
|
|
555
|
+
"\u9655\u897F",
|
|
556
|
+
"\u7518\u8083",
|
|
557
|
+
"\u9752\u6D77",
|
|
558
|
+
"\u5185\u8499\u53E4",
|
|
559
|
+
"\u5E7F\u897F",
|
|
560
|
+
"\u897F\u85CF",
|
|
561
|
+
"\u5B81\u590F",
|
|
562
|
+
"\u65B0\u7586",
|
|
563
|
+
"\u9999\u6E2F",
|
|
564
|
+
"\u6FB3\u95E8",
|
|
565
|
+
"\u53F0\u6E7E"
|
|
566
|
+
];
|
|
567
|
+
var CITIES = {
|
|
568
|
+
\u6E56\u5317: ["\u6B66\u6C49", "\u9EC4\u77F3", "\u8944\u9633", "\u5B9C\u660C", "\u8346\u5DDE", "\u5341\u5830", "\u5B5D\u611F", "\u9EC4\u5188", "\u54B8\u5B81", "\u968F\u5DDE", "\u6069\u65BD", "\u9102\u5DDE", "\u8346\u95E8", "\u4ED9\u6843", "\u5929\u95E8", "\u6F5C\u6C5F"],
|
|
569
|
+
\u5E7F\u4E1C: ["\u5E7F\u5DDE", "\u6DF1\u5733", "\u73E0\u6D77", "\u4F5B\u5C71", "\u4E1C\u839E", "\u4E2D\u5C71", "\u60E0\u5DDE", "\u6C5F\u95E8", "\u6E5B\u6C5F", "\u8302\u540D", "\u8087\u5E86", "\u6C55\u5934", "\u97F6\u5173"],
|
|
570
|
+
\u6C5F\u82CF: ["\u5357\u4EAC", "\u82CF\u5DDE", "\u65E0\u9521", "\u5E38\u5DDE", "\u5357\u901A", "\u5F90\u5DDE", "\u626C\u5DDE", "\u9547\u6C5F", "\u6CF0\u5DDE", "\u76D0\u57CE", "\u8FDE\u4E91\u6E2F", "\u6DEE\u5B89", "\u5BBF\u8FC1"],
|
|
571
|
+
\u6D59\u6C5F: ["\u676D\u5DDE", "\u5B81\u6CE2", "\u6E29\u5DDE", "\u5609\u5174", "\u6E56\u5DDE", "\u7ECD\u5174", "\u91D1\u534E", "\u8862\u5DDE", "\u821F\u5C71", "\u53F0\u5DDE", "\u4E3D\u6C34"],
|
|
572
|
+
\u56DB\u5DDD: ["\u6210\u90FD", "\u7EF5\u9633", "\u5FB7\u9633", "\u5B9C\u5BBE", "\u6CF8\u5DDE", "\u5357\u5145", "\u8FBE\u5DDE", "\u4E50\u5C71", "\u5185\u6C5F", "\u81EA\u8D21"]
|
|
573
|
+
};
|
|
574
|
+
var FLAT_CITIES = [
|
|
575
|
+
...Object.values(CITIES).flat(),
|
|
576
|
+
"\u5317\u4EAC",
|
|
577
|
+
"\u4E0A\u6D77",
|
|
578
|
+
"\u5929\u6D25",
|
|
579
|
+
"\u91CD\u5E86"
|
|
580
|
+
];
|
|
581
|
+
var WUHAN_DISTRICTS = [
|
|
582
|
+
"\u6C5F\u5CB8\u533A",
|
|
583
|
+
"\u6C5F\u6C49\u533A",
|
|
584
|
+
"\u785A\u53E3\u533A",
|
|
585
|
+
"\u6C49\u9633\u533A",
|
|
586
|
+
"\u6B66\u660C\u533A",
|
|
587
|
+
"\u9752\u5C71\u533A",
|
|
588
|
+
"\u6D2A\u5C71\u533A",
|
|
589
|
+
"\u4E1C\u897F\u6E56\u533A",
|
|
590
|
+
"\u6C49\u5357\u533A",
|
|
591
|
+
"\u8521\u7538\u533A",
|
|
592
|
+
"\u6C5F\u590F\u533A",
|
|
593
|
+
"\u9EC4\u9642\u533A",
|
|
594
|
+
"\u65B0\u6D32\u533A"
|
|
595
|
+
];
|
|
596
|
+
var POI_DICT = [
|
|
597
|
+
"\u5149\u8C37",
|
|
598
|
+
"\u5149\u8C37\u5E7F\u573A",
|
|
599
|
+
"\u6C49\u53E3\u706B\u8F66\u7AD9",
|
|
600
|
+
"\u6B66\u660C\u706B\u8F66\u7AD9",
|
|
601
|
+
"\u6B66\u6C49\u7AD9",
|
|
602
|
+
"\u5929\u6CB3\u673A\u573A",
|
|
603
|
+
"\u6B66\u6C49\u5927\u5B66",
|
|
604
|
+
"\u534E\u4E2D\u79D1\u6280\u5927\u5B66",
|
|
605
|
+
"\u9EC4\u9E64\u697C",
|
|
606
|
+
"\u4E1C\u6E56",
|
|
607
|
+
"\u957F\u6C5F\u5927\u6865",
|
|
608
|
+
"\u6C5F\u6C49\u8DEF",
|
|
609
|
+
"\u695A\u6CB3\u6C49\u8857",
|
|
610
|
+
"\u8857\u9053\u53E3",
|
|
611
|
+
"\u4E2D\u5357\u8DEF",
|
|
612
|
+
"\u5F90\u4E1C",
|
|
613
|
+
"\u738B\u5BB6\u6E7E",
|
|
614
|
+
"\u949F\u5BB6\u6751",
|
|
615
|
+
"\u6C49\u6B63\u8857",
|
|
616
|
+
"\u8F6F\u4EF6\u56ED",
|
|
617
|
+
"\u91D1\u878D\u6E2F",
|
|
618
|
+
"\u751F\u7269\u57CE",
|
|
619
|
+
"\u672A\u6765\u79D1\u6280\u57CE"
|
|
620
|
+
];
|
|
621
|
+
function parseAddress(raw) {
|
|
622
|
+
const input = (raw ?? "").trim();
|
|
623
|
+
if (!input) return { raw: input, normalized: "", confidence: 0 };
|
|
624
|
+
const result = { raw: input, normalized: input, confidence: 0.3 };
|
|
625
|
+
let matched = 0;
|
|
626
|
+
for (const p of PROVINCES) {
|
|
627
|
+
if (input.includes(p)) {
|
|
628
|
+
result.province = p;
|
|
629
|
+
matched++;
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (result.province) {
|
|
634
|
+
const provinceCities = CITIES[result.province] ?? [];
|
|
635
|
+
for (const c of provinceCities) {
|
|
636
|
+
if (input.includes(c)) {
|
|
637
|
+
result.city = c;
|
|
638
|
+
matched++;
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (!result.city && ["\u5317\u4EAC", "\u4E0A\u6D77", "\u5929\u6D25", "\u91CD\u5E86"].includes(result.province)) {
|
|
643
|
+
result.city = result.province;
|
|
644
|
+
matched++;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
if (!result.city) {
|
|
648
|
+
for (const c of FLAT_CITIES) {
|
|
649
|
+
if (input.includes(c)) {
|
|
650
|
+
result.city = c;
|
|
651
|
+
matched++;
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
for (const d of WUHAN_DISTRICTS) {
|
|
657
|
+
if (input.includes(d)) {
|
|
658
|
+
result.district = d;
|
|
659
|
+
matched++;
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
if (!result.district) {
|
|
664
|
+
const districtMatch = input.match(/[\u4e00-\u9fa5]{2,4}(区|县)/);
|
|
665
|
+
if (districtMatch && !districtMatch[0].startsWith("\u6B66\u6C49")) {
|
|
666
|
+
result.district = districtMatch[0];
|
|
667
|
+
matched++;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
for (const poi of POI_DICT) {
|
|
671
|
+
if (input.includes(poi)) {
|
|
672
|
+
result.poi = poi;
|
|
673
|
+
matched++;
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
const streetMatch = input.match(/[\u4e00-\u9fa5]{2,8}(?:路|街|道|大道|大街)/);
|
|
678
|
+
if (streetMatch) {
|
|
679
|
+
result.street = streetMatch[0];
|
|
680
|
+
matched++;
|
|
681
|
+
}
|
|
682
|
+
const numMatch = input.match(/(\d+)\s*号/);
|
|
683
|
+
if (numMatch) {
|
|
684
|
+
result.number = numMatch[1];
|
|
685
|
+
matched++;
|
|
686
|
+
}
|
|
687
|
+
result.normalized = [
|
|
688
|
+
result.province,
|
|
689
|
+
result.city && result.city !== result.province ? result.city : "",
|
|
690
|
+
result.district,
|
|
691
|
+
result.poi,
|
|
692
|
+
result.street,
|
|
693
|
+
result.number ? `${result.number}\u53F7` : ""
|
|
694
|
+
].filter(Boolean).join("");
|
|
695
|
+
result.confidence = Math.min(0.95, 0.3 + matched * 0.15);
|
|
696
|
+
return result;
|
|
697
|
+
}
|
|
698
|
+
function isValidAddress(parsed) {
|
|
699
|
+
return Boolean(parsed.province || parsed.city || parsed.district || parsed.poi);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// src/geoai/headerDetector.ts
|
|
703
|
+
var ADDRESS_KEYS = [
|
|
704
|
+
"\u5730\u5740",
|
|
705
|
+
"\u4F4D\u7F6E",
|
|
706
|
+
"\u5730\u70B9",
|
|
707
|
+
"\u6240\u5728\u5730",
|
|
708
|
+
"\u8BE6\u7EC6\u5730\u5740",
|
|
709
|
+
"\u4F4F\u5740",
|
|
710
|
+
"\u5730\u7406\u4F4D\u7F6E",
|
|
711
|
+
"\u8054\u7CFB\u5730\u5740",
|
|
712
|
+
"address",
|
|
713
|
+
"addr",
|
|
714
|
+
"location",
|
|
715
|
+
"place"
|
|
716
|
+
];
|
|
717
|
+
var NAME_KEYS = ["\u540D\u79F0", "\u540D\u5B57", "\u5355\u4F4D", "\u673A\u6784", "\u7AD9\u70B9", "\u573A\u6240", "name", "title", "label"];
|
|
718
|
+
var CATEGORY_KEYS = ["\u7C7B\u578B", "\u5206\u7C7B", "\u7C7B\u522B", "\u884C\u4E1A", "\u4E1A\u6001", "category", "type", "kind", "class"];
|
|
719
|
+
var LNG_KEYS = ["\u7ECF\u5EA6", "lng", "lon", "longitude", "x\u5750\u6807", "\u7ECF\u5EA6\u5750\u6807", "long", "x"];
|
|
720
|
+
var LAT_KEYS = ["\u7EAC\u5EA6", "lat", "latitude", "y\u5750\u6807", "\u7EAC\u5EA6\u5750\u6807", "y"];
|
|
721
|
+
function normalize(h) {
|
|
722
|
+
return h.trim().toLowerCase().replace(/[\s_-]+/g, "");
|
|
723
|
+
}
|
|
724
|
+
function matchHeader(headers, keys) {
|
|
725
|
+
const normalized = headers.map(normalize);
|
|
726
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
727
|
+
const h = normalized[i];
|
|
728
|
+
for (const k of keys) {
|
|
729
|
+
if (h === normalize(k) || h.includes(normalize(k))) {
|
|
730
|
+
return i;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return -1;
|
|
735
|
+
}
|
|
736
|
+
function detectHeaders(headers) {
|
|
737
|
+
const addressCol = matchHeader(headers, ADDRESS_KEYS);
|
|
738
|
+
const nameCol = matchHeader(headers, NAME_KEYS);
|
|
739
|
+
const categoryCol = matchHeader(headers, CATEGORY_KEYS);
|
|
740
|
+
const lngCol = matchHeader(headers, LNG_KEYS);
|
|
741
|
+
const latCol = matchHeader(headers, LAT_KEYS);
|
|
742
|
+
const detail = {};
|
|
743
|
+
if (addressCol >= 0) detail.address = headers[addressCol];
|
|
744
|
+
if (nameCol >= 0) detail.name = headers[nameCol];
|
|
745
|
+
if (categoryCol >= 0) detail.category = headers[categoryCol];
|
|
746
|
+
if (lngCol >= 0) detail.lng = headers[lngCol];
|
|
747
|
+
if (latCol >= 0) detail.lat = headers[latCol];
|
|
748
|
+
return { addressCol, nameCol, categoryCol, lngCol, latCol, detail };
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// src/geoai/crsDetector.ts
|
|
752
|
+
var CHINA_BOUNDS = {
|
|
753
|
+
lngMin: 73,
|
|
754
|
+
lngMax: 135,
|
|
755
|
+
latMin: 18,
|
|
756
|
+
latMax: 54
|
|
757
|
+
};
|
|
758
|
+
var A = 6378245;
|
|
759
|
+
var EE = 0.006693421622965943;
|
|
760
|
+
function outOfChina(lng, lat) {
|
|
761
|
+
return lng < CHINA_BOUNDS.lngMin || lng > CHINA_BOUNDS.lngMax || lat < CHINA_BOUNDS.latMin || lat > CHINA_BOUNDS.latMax;
|
|
762
|
+
}
|
|
763
|
+
function transformLat(x, y) {
|
|
764
|
+
let ret = -100 + 2 * x + 3 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
|
|
765
|
+
ret += (20 * Math.sin(6 * x * Math.PI) + 20 * Math.sin(2 * x * Math.PI)) * 2 / 3;
|
|
766
|
+
ret += (20 * Math.sin(y * Math.PI) + 40 * Math.sin(y / 3 * Math.PI)) * 2 / 3;
|
|
767
|
+
ret += (160 * Math.sin(y / 12 * Math.PI) + 320 * Math.sin(y * Math.PI / 30)) * 2 / 3;
|
|
768
|
+
return ret;
|
|
769
|
+
}
|
|
770
|
+
function transformLng(x, y) {
|
|
771
|
+
let ret = 300 + x + 2 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
|
|
772
|
+
ret += (20 * Math.sin(6 * x * Math.PI) + 20 * Math.sin(2 * x * Math.PI)) * 2 / 3;
|
|
773
|
+
ret += (20 * Math.sin(x * Math.PI) + 40 * Math.sin(x / 3 * Math.PI)) * 2 / 3;
|
|
774
|
+
ret += (150 * Math.sin(x / 12 * Math.PI) + 300 * Math.sin(x / 30 * Math.PI)) * 2 / 3;
|
|
775
|
+
return ret;
|
|
776
|
+
}
|
|
777
|
+
function wgs84ToGcj02(lng, lat) {
|
|
778
|
+
if (outOfChina(lng, lat)) return [lng, lat];
|
|
779
|
+
let dLat = transformLat(lng - 105, lat - 35);
|
|
780
|
+
let dLng = transformLng(lng - 105, lat - 35);
|
|
781
|
+
const radLat = lat / 180 * Math.PI;
|
|
782
|
+
let magic = Math.sin(radLat);
|
|
783
|
+
magic = 1 - EE * magic * magic;
|
|
784
|
+
const sqrtMagic = Math.sqrt(magic);
|
|
785
|
+
dLat = dLat * 180 / (A * (1 - EE) / (magic * sqrtMagic) * Math.PI);
|
|
786
|
+
dLng = dLng * 180 / (A / sqrtMagic * Math.cos(radLat) * Math.PI);
|
|
787
|
+
return [lng + dLng, lat + dLat];
|
|
788
|
+
}
|
|
789
|
+
function gcj02ToWgs84(lng, lat) {
|
|
790
|
+
if (outOfChina(lng, lat)) return [lng, lat];
|
|
791
|
+
let [wgsLng, wgsLat] = [lng, lat];
|
|
792
|
+
for (let i = 0; i < 5; i++) {
|
|
793
|
+
const [gLng, gLat] = wgs84ToGcj02(wgsLng, wgsLat);
|
|
794
|
+
const dLng = gLng - lng;
|
|
795
|
+
const dLat = gLat - lat;
|
|
796
|
+
wgsLng -= dLng;
|
|
797
|
+
wgsLat -= dLat;
|
|
798
|
+
}
|
|
799
|
+
return [wgsLng, wgsLat];
|
|
800
|
+
}
|
|
801
|
+
function isInChina(lng, lat) {
|
|
802
|
+
return !outOfChina(lng, lat);
|
|
803
|
+
}
|
|
804
|
+
function detectCRS(source, sample = []) {
|
|
805
|
+
const s = (source ?? "").toLowerCase();
|
|
806
|
+
if (/(高德|腾讯|amap|qqmap|gcj|火星)/.test(s)) return "gcj02";
|
|
807
|
+
if (/(天地图|tianditu|cgcs|2000)/.test(s)) return "cgcs2000";
|
|
808
|
+
if (/(gps|wgs84|84)/.test(s)) return "wgs84";
|
|
809
|
+
if (sample.length > 0) {
|
|
810
|
+
let inChinaCount = 0;
|
|
811
|
+
for (const [lng, lat] of sample) {
|
|
812
|
+
if (isInChina(lng, lat)) {
|
|
813
|
+
inChinaCount++;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
if (sample.length > 0 && inChinaCount / sample.length < 0.5) return "wgs84";
|
|
817
|
+
}
|
|
818
|
+
return "wgs84";
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// src/geoai/geocoder.ts
|
|
822
|
+
var LOCAL_GEO_DB = {
|
|
823
|
+
// 主要城市
|
|
824
|
+
\u6B66\u6C49: [114.305, 30.593],
|
|
825
|
+
\u5317\u4EAC: [116.407, 39.904],
|
|
826
|
+
\u4E0A\u6D77: [121.474, 31.23],
|
|
827
|
+
\u5E7F\u5DDE: [113.264, 23.129],
|
|
828
|
+
\u6DF1\u5733: [114.058, 22.543],
|
|
829
|
+
\u6210\u90FD: [104.067, 30.573],
|
|
830
|
+
\u676D\u5DDE: [120.155, 30.274],
|
|
831
|
+
\u5357\u4EAC: [118.796, 32.06],
|
|
832
|
+
\u91CD\u5E86: [106.551, 29.563],
|
|
833
|
+
\u897F\u5B89: [108.94, 34.341],
|
|
834
|
+
\u5929\u6D25: [117.2, 39.084],
|
|
835
|
+
// 武汉各区
|
|
836
|
+
\u6C5F\u5CB8\u533A: [114.31, 30.6],
|
|
837
|
+
\u6C5F\u6C49\u533A: [114.27, 30.6],
|
|
838
|
+
\u785A\u53E3\u533A: [114.21, 30.58],
|
|
839
|
+
\u6C49\u9633\u533A: [114.22, 30.55],
|
|
840
|
+
\u6B66\u660C\u533A: [114.32, 30.55],
|
|
841
|
+
\u9752\u5C71\u533A: [114.39, 30.63],
|
|
842
|
+
\u6D2A\u5C71\u533A: [114.34, 30.5],
|
|
843
|
+
\u4E1C\u897F\u6E56\u533A: [114.14, 30.62],
|
|
844
|
+
\u6C49\u5357\u533A: [114.08, 30.31],
|
|
845
|
+
\u8521\u7538\u533A: [114.03, 30.58],
|
|
846
|
+
\u6C5F\u590F\u533A: [114.32, 30.35],
|
|
847
|
+
\u9EC4\u9642\u533A: [114.38, 30.88],
|
|
848
|
+
\u65B0\u6D32\u533A: [114.8, 30.85],
|
|
849
|
+
// 常见 POI(口语化地址兜底)
|
|
850
|
+
\u5149\u8C37: [114.428, 30.507],
|
|
851
|
+
\u5149\u8C37\u5E7F\u573A: [114.4, 30.507],
|
|
852
|
+
\u6C49\u53E3\u706B\u8F66\u7AD9: [114.255, 30.618],
|
|
853
|
+
\u6B66\u660C\u706B\u8F66\u7AD9: [114.317, 30.531],
|
|
854
|
+
\u6B66\u6C49\u7AD9: [114.424, 30.607],
|
|
855
|
+
\u5929\u6CB3\u673A\u573A: [114.208, 30.774],
|
|
856
|
+
\u6B66\u6C49\u5927\u5B66: [114.362, 30.541],
|
|
857
|
+
\u534E\u4E2D\u79D1\u6280\u5927\u5B66: [114.418, 30.513],
|
|
858
|
+
\u9EC4\u9E64\u697C: [114.302, 30.544],
|
|
859
|
+
\u4E1C\u6E56: [114.4, 30.55],
|
|
860
|
+
\u957F\u6C5F\u5927\u6865: [114.294, 30.55],
|
|
861
|
+
\u6C5F\u6C49\u8DEF: [114.278, 30.581],
|
|
862
|
+
\u695A\u6CB3\u6C49\u8857: [114.337, 30.561],
|
|
863
|
+
\u8857\u9053\u53E3: [114.351, 30.531],
|
|
864
|
+
\u4E2D\u5357\u8DEF: [114.331, 30.539],
|
|
865
|
+
\u5F90\u4E1C: [114.348, 30.583],
|
|
866
|
+
\u738B\u5BB6\u6E7E: [114.204, 30.557],
|
|
867
|
+
\u949F\u5BB6\u6751: [114.246, 30.549],
|
|
868
|
+
\u6C49\u6B63\u8857: [114.272, 30.573],
|
|
869
|
+
\u8F6F\u4EF6\u56ED: [114.4, 30.47],
|
|
870
|
+
\u91D1\u878D\u6E2F: [114.42, 30.47],
|
|
871
|
+
\u751F\u7269\u57CE: [114.48, 30.49],
|
|
872
|
+
\u672A\u6765\u79D1\u6280\u57CE: [114.47, 30.44]
|
|
873
|
+
};
|
|
874
|
+
function geocode(parsed) {
|
|
875
|
+
if (parsed.poi && LOCAL_GEO_DB[parsed.poi]) {
|
|
876
|
+
const c = LOCAL_GEO_DB[parsed.poi];
|
|
877
|
+
return { lng: c[0], lat: c[1], matched: parsed.poi, confidence: 0.95 };
|
|
878
|
+
}
|
|
879
|
+
if (parsed.district && LOCAL_GEO_DB[parsed.district]) {
|
|
880
|
+
const c = LOCAL_GEO_DB[parsed.district];
|
|
881
|
+
return { lng: c[0], lat: c[1], matched: parsed.district, confidence: 0.8 };
|
|
882
|
+
}
|
|
883
|
+
if (parsed.city && LOCAL_GEO_DB[parsed.city]) {
|
|
884
|
+
const c = LOCAL_GEO_DB[parsed.city];
|
|
885
|
+
return { lng: c[0], lat: c[1], matched: parsed.city, confidence: 0.6 };
|
|
886
|
+
}
|
|
887
|
+
if (parsed.province && LOCAL_GEO_DB[parsed.province]) {
|
|
888
|
+
const c = LOCAL_GEO_DB[parsed.province];
|
|
889
|
+
return { lng: c[0], lat: c[1], matched: parsed.province, confidence: 0.4 };
|
|
890
|
+
}
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
function hasLocalGeo(name) {
|
|
894
|
+
return name in LOCAL_GEO_DB;
|
|
895
|
+
}
|
|
896
|
+
function batchGeocode(rows, parse) {
|
|
897
|
+
return rows.map((row) => {
|
|
898
|
+
if (typeof row.lng === "number" && typeof row.lat === "number" && !Number.isNaN(row.lng) && !Number.isNaN(row.lat)) {
|
|
899
|
+
return { lng: row.lng, lat: row.lat, source: "provided", confidence: 1 };
|
|
900
|
+
}
|
|
901
|
+
const parsed = parse(row.address);
|
|
902
|
+
const geo = geocode(parsed);
|
|
903
|
+
if (geo) {
|
|
904
|
+
return { lng: geo.lng, lat: geo.lat, source: "geocoded", matched: geo.matched, confidence: geo.confidence };
|
|
905
|
+
}
|
|
906
|
+
return { lng: 0, lat: 0, source: "failed", confidence: 0 };
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// src/geoai/dataImporter.ts
|
|
911
|
+
function importToGeoJSON(headers, rows, opts = {}) {
|
|
912
|
+
const t0 = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
913
|
+
const parse = opts.parse ?? parseAddress;
|
|
914
|
+
const det = detectHeaders(headers);
|
|
915
|
+
const { addressCol, nameCol, categoryCol, lngCol, latCol } = det;
|
|
916
|
+
const samples = [];
|
|
917
|
+
if (lngCol >= 0 && latCol >= 0) {
|
|
918
|
+
for (const row of rows.slice(0, 50)) {
|
|
919
|
+
const lng = Number(row[lngCol]);
|
|
920
|
+
const lat = Number(row[latCol]);
|
|
921
|
+
if (!Number.isNaN(lng) && !Number.isNaN(lat)) samples.push([lng, lat]);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
const crs = detectCRS(opts.source, samples);
|
|
925
|
+
const geocodeInput = rows.map((row) => ({
|
|
926
|
+
address: String(row[addressCol] ?? ""),
|
|
927
|
+
lng: lngCol >= 0 ? Number(row[lngCol]) : void 0,
|
|
928
|
+
lat: latCol >= 0 ? Number(row[latCol]) : void 0
|
|
929
|
+
}));
|
|
930
|
+
const coded = batchGeocode(geocodeInput, parse);
|
|
931
|
+
const features = rows.map((row, i) => {
|
|
932
|
+
const c = coded[i];
|
|
933
|
+
let lng = c.lng;
|
|
934
|
+
let lat = c.lat;
|
|
935
|
+
if (c.source !== "failed" && crs === "gcj02") {
|
|
936
|
+
[lng, lat] = gcj02ToWgs84(lng, lat);
|
|
937
|
+
}
|
|
938
|
+
const properties = {};
|
|
939
|
+
if (nameCol >= 0) properties.name = row[nameCol];
|
|
940
|
+
if (categoryCol >= 0) properties.category = row[categoryCol];
|
|
941
|
+
if (addressCol >= 0) properties.address = row[addressCol];
|
|
942
|
+
if (c.matched) properties.matchedPlace = c.matched;
|
|
943
|
+
const geometry = c.source !== "failed" ? { type: "Point", coordinates: [lng, lat] } : { type: "Point", coordinates: [] };
|
|
944
|
+
return { type: "Feature", geometry, properties };
|
|
945
|
+
});
|
|
946
|
+
const success = coded.filter((c) => c.source !== "failed").length;
|
|
947
|
+
const durationMs = (typeof performance !== "undefined" ? performance.now() : Date.now()) - t0;
|
|
948
|
+
return {
|
|
949
|
+
type: "FeatureCollection",
|
|
950
|
+
features,
|
|
951
|
+
stats: {
|
|
952
|
+
total: rows.length,
|
|
953
|
+
success,
|
|
954
|
+
failed: rows.length - success,
|
|
955
|
+
successRate: rows.length > 0 ? success / rows.length : 0,
|
|
956
|
+
headers: det,
|
|
957
|
+
durationMs
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// src/nlpg/sqlGenerator.ts
|
|
963
|
+
var FIELD_ALIASES = [
|
|
964
|
+
{ field: "material", re: /材质|材料|铸铁|钢管|PE|PVC|球墨|HDPE|铜管/ },
|
|
965
|
+
{ field: "pressure", re: /压力|水压|气压|MPa/ },
|
|
966
|
+
{ field: "diameter", re: /管径|直径|口径|DN/ },
|
|
967
|
+
{ field: "status", re: /状态|运行|停运|故障|检修/ },
|
|
968
|
+
{ field: "install_date", re: /安装日期|投运|建成|敷设/ },
|
|
969
|
+
{ field: "age", re: /年限|使用.{0,2}年|超过.{0,2}年|.{0,2}年以上|年代/ },
|
|
970
|
+
{ field: "voltage", re: /电压|kV|kv|伏/ },
|
|
971
|
+
{ field: "load_rate", re: /负载率|负荷率|利用率|负载|负荷/ },
|
|
972
|
+
{ field: "fault_rate", re: /故障率/ },
|
|
973
|
+
{ field: "flow_rate", re: /流量|流速/ },
|
|
974
|
+
{ field: "water_level", re: /水位/ },
|
|
975
|
+
{ field: "storage_rate", re: /蓄水率|库容/ },
|
|
976
|
+
{ field: "rsrp", re: /RSRP|信号强度|信号/ }
|
|
977
|
+
];
|
|
978
|
+
var TABLE_ALIASES = [
|
|
979
|
+
{ table: "pipelines", re: /管段|管线|管道|管网|燃气管|供水管|排水管|供热管|电力管|通信管/ },
|
|
980
|
+
{ table: "nodes", re: /节点|阀门|泵站|表|井|闸/ },
|
|
981
|
+
{ table: "users", re: /用户|居民|小区|住户|建筑/ },
|
|
982
|
+
{ table: "schools", re: /学校|小学|中学|大学|幼儿园/ },
|
|
983
|
+
{ table: "hospitals", re: /医院|卫生院|诊所/ },
|
|
984
|
+
{ table: "substations", re: /变电站|配变|台区|电站/ },
|
|
985
|
+
{ table: "base_stations", re: /基站|宏站|微站|室分/ },
|
|
986
|
+
{ table: "rivers", re: /河流|水系|河段|支流/ },
|
|
987
|
+
{ table: "reservoirs", re: /水库|大坝/ },
|
|
988
|
+
{ table: "alarms", re: /报警|告警|警报/ },
|
|
989
|
+
{ table: "pois", re: /POI|兴趣点|场所|设施/ }
|
|
990
|
+
];
|
|
991
|
+
var VALUE_DICT = [
|
|
992
|
+
{ field: "material", value: "cast_iron", re: /铸铁/ },
|
|
993
|
+
{ field: "material", value: "ductile_iron", re: /球墨/ },
|
|
994
|
+
{ field: "material", value: "steel", re: /钢(?!筋)/ },
|
|
995
|
+
{ field: "material", value: "pe", re: /PE/ },
|
|
996
|
+
{ field: "material", value: "pvc", re: /PVC/ },
|
|
997
|
+
{ field: "material", value: "hdpe", re: /HDPE/ },
|
|
998
|
+
{ field: "status", value: "normal", re: /正常/ },
|
|
999
|
+
{ field: "status", value: "fault", re: /故障/ },
|
|
1000
|
+
{ field: "status", value: "maintenance", re: /检修|维修/ },
|
|
1001
|
+
{ field: "status", value: "aging", re: /老化/ }
|
|
1002
|
+
];
|
|
1003
|
+
function detectOperator(text) {
|
|
1004
|
+
if (/超过|高于|大于|超出|以上|不小于/.test(text)) return text.includes("\u4EE5\u4E0A") || text.includes("\u4E0D\u5C0F\u4E8E") ? ">=" : ">";
|
|
1005
|
+
if (/低于|小于|不到|以下|不足/.test(text)) return text.includes("\u4EE5\u4E0B") ? "<=" : "<";
|
|
1006
|
+
if (/等于|正好|恰好|为\s*\d/.test(text)) return "=";
|
|
1007
|
+
if (/不是|非|不等于/.test(text)) return "!=";
|
|
1008
|
+
if (/包含|含有|含/.test(text)) return "LIKE";
|
|
1009
|
+
return "=";
|
|
1010
|
+
}
|
|
1011
|
+
function detectTable(text) {
|
|
1012
|
+
for (const t of TABLE_ALIASES) {
|
|
1013
|
+
if (t.re.test(text)) return t.table;
|
|
1014
|
+
}
|
|
1015
|
+
return "pois";
|
|
1016
|
+
}
|
|
1017
|
+
function detectField(text) {
|
|
1018
|
+
for (const f of FIELD_ALIASES) {
|
|
1019
|
+
if (f.re.test(text)) return f.field;
|
|
1020
|
+
}
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
function detectValue(text, field) {
|
|
1024
|
+
for (const v of VALUE_DICT) {
|
|
1025
|
+
if (v.field === field && v.re.test(text)) return v.value;
|
|
1026
|
+
}
|
|
1027
|
+
const numMatch = text.match(/(\d+(?:\.\d+)?)\s*(MPa|mpa|kV|kv|米|m|公里|km|%|%|方|立方米)?/);
|
|
1028
|
+
if (numMatch) {
|
|
1029
|
+
let value = parseFloat(numMatch[1]);
|
|
1030
|
+
const unit = numMatch[2] ?? "";
|
|
1031
|
+
if (/公里|km/.test(unit)) value *= 1e3;
|
|
1032
|
+
if (/%|%/.test(unit)) value /= 100;
|
|
1033
|
+
return value;
|
|
1034
|
+
}
|
|
1035
|
+
const dateMatch = text.match(/(\d{4}-\d{2}-\d{2}|\d{4}年\d{1,2}月)/);
|
|
1036
|
+
if (dateMatch) return dateMatch[1];
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
function detectSpatial(text, geometryColumn = "geom") {
|
|
1040
|
+
const nearby = text.match(/(\d+(?:\.\d+)?)\s*(米|m|公里|km|千米)\s*(?:内|以内|范围内|附近|周边)/);
|
|
1041
|
+
if (nearby) {
|
|
1042
|
+
let radius = parseFloat(nearby[1]);
|
|
1043
|
+
if (/公里|km|千米/.test(nearby[2])) radius *= 1e3;
|
|
1044
|
+
return { relation: "dwithin", radius, geometryColumn };
|
|
1045
|
+
}
|
|
1046
|
+
if (/缓冲区|缓冲|范围内|区域.{0,2}内/.test(text)) {
|
|
1047
|
+
return { relation: "buffer", geometryColumn };
|
|
1048
|
+
}
|
|
1049
|
+
if (/包含|覆盖.{0,2}内|在.{0,2}内/.test(text)) {
|
|
1050
|
+
return { relation: "within", geometryColumn };
|
|
1051
|
+
}
|
|
1052
|
+
if (/相交|叠加|重叠|交叉/.test(text)) {
|
|
1053
|
+
return { relation: "intersects", geometryColumn };
|
|
1054
|
+
}
|
|
1055
|
+
return null;
|
|
1056
|
+
}
|
|
1057
|
+
function quoteValue(v) {
|
|
1058
|
+
return typeof v === "string" ? `'${v.replace(/'/g, "''")}'` : String(v);
|
|
1059
|
+
}
|
|
1060
|
+
function buildWhere(conditions, spatial) {
|
|
1061
|
+
const parts = [];
|
|
1062
|
+
for (const c of conditions) {
|
|
1063
|
+
parts.push(`${c.field} ${c.operator} ${quoteValue(c.value)}`);
|
|
1064
|
+
}
|
|
1065
|
+
if (spatial) {
|
|
1066
|
+
if (spatial.relation === "dwithin" && spatial.point && spatial.radius !== void 0) {
|
|
1067
|
+
parts.push(`ST_DWithin(${spatial.geometryColumn}, ST_SetSRID(ST_MakePoint(${spatial.point[0]}, ${spatial.point[1]}), 4326), ${spatial.radius})`);
|
|
1068
|
+
} else if (spatial.relation === "buffer") {
|
|
1069
|
+
parts.push(`ST_Intersects(${spatial.geometryColumn}, ST_Buffer(${spatial.geometryColumn}, 0))`);
|
|
1070
|
+
} else if (spatial.relation === "within") {
|
|
1071
|
+
parts.push(`ST_Within(${spatial.geometryColumn}, ${spatial.geometryColumn})`);
|
|
1072
|
+
} else if (spatial.relation === "intersects") {
|
|
1073
|
+
parts.push(`ST_Intersects(${spatial.geometryColumn}, ${spatial.geometryColumn})`);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return parts.length > 0 ? `WHERE ${parts.join(" AND ")}` : "";
|
|
1077
|
+
}
|
|
1078
|
+
function generatePostGISQuery(text, opts = {}) {
|
|
1079
|
+
const center = opts.center ?? [114.305, 30.593];
|
|
1080
|
+
const geometryColumn = opts.geometryColumn ?? "geom";
|
|
1081
|
+
const table = detectTable(text);
|
|
1082
|
+
const spatial = detectSpatial(text, geometryColumn);
|
|
1083
|
+
if (spatial && spatial.relation === "dwithin" && !spatial.point) {
|
|
1084
|
+
spatial.point = center;
|
|
1085
|
+
}
|
|
1086
|
+
const conditions = [];
|
|
1087
|
+
const field = detectField(text);
|
|
1088
|
+
if (field) {
|
|
1089
|
+
const value = detectValue(text, field);
|
|
1090
|
+
if (value !== null) {
|
|
1091
|
+
conditions.push({ field, operator: detectOperator(text), value });
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
let intent;
|
|
1095
|
+
if (spatial && conditions.length > 0) intent = "mixed";
|
|
1096
|
+
else if (spatial) intent = spatial.relation === "dwithin" ? "spatial_nearby" : "spatial_within";
|
|
1097
|
+
else if (conditions.length > 0) intent = "attribute_filter";
|
|
1098
|
+
else intent = "unknown";
|
|
1099
|
+
const where = buildWhere(conditions, spatial);
|
|
1100
|
+
const sql = `SELECT * FROM ${table} ${where}`.trim();
|
|
1101
|
+
let confidence = 0.3;
|
|
1102
|
+
if (conditions.length > 0) confidence += 0.3;
|
|
1103
|
+
if (spatial) confidence += 0.3;
|
|
1104
|
+
if (table !== "pois") confidence += 0.1;
|
|
1105
|
+
confidence = Math.min(0.95, confidence);
|
|
1106
|
+
return { intent, table, conditions, spatial, sql, confidence };
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// src/nlpg/sqlValidator.ts
|
|
1110
|
+
var DANGEROUS_KEYWORDS = [
|
|
1111
|
+
"DROP",
|
|
1112
|
+
"DELETE",
|
|
1113
|
+
"UPDATE",
|
|
1114
|
+
"INSERT",
|
|
1115
|
+
"ALTER",
|
|
1116
|
+
"TRUNCATE",
|
|
1117
|
+
"GRANT",
|
|
1118
|
+
"REVOKE",
|
|
1119
|
+
"MERGE",
|
|
1120
|
+
"CREATE",
|
|
1121
|
+
"REPLACE",
|
|
1122
|
+
"EXEC",
|
|
1123
|
+
"EXECUTE",
|
|
1124
|
+
"CALL",
|
|
1125
|
+
"COPY",
|
|
1126
|
+
"LOAD",
|
|
1127
|
+
"INTO",
|
|
1128
|
+
"SET",
|
|
1129
|
+
"UNION",
|
|
1130
|
+
"ATTACH",
|
|
1131
|
+
"DETACH",
|
|
1132
|
+
"PRAGMA",
|
|
1133
|
+
"VACUUM",
|
|
1134
|
+
"REINDEX"
|
|
1135
|
+
];
|
|
1136
|
+
var INJECTION_PATTERNS = [
|
|
1137
|
+
/--/,
|
|
1138
|
+
// SQL 注释
|
|
1139
|
+
/\/\*/,
|
|
1140
|
+
// 块注释
|
|
1141
|
+
/;\s*(DROP|DELETE|UPDATE|INSERT|ALTER|TRUNCATE)/i,
|
|
1142
|
+
// 多语句注入
|
|
1143
|
+
/'\s*OR\s*'/i,
|
|
1144
|
+
// ' OR ' 恒真注入(含 ' OR '1'='1')
|
|
1145
|
+
/"\s*OR\s*"/i,
|
|
1146
|
+
// " OR " 恒真注入
|
|
1147
|
+
/\bOR\s+\d+\s*=\s*\d+\b/i,
|
|
1148
|
+
// OR 1=1 恒真
|
|
1149
|
+
/\bOR\s+'\w*'\s*=\s*'\w*'/i,
|
|
1150
|
+
// OR 'a'='a' 恒真
|
|
1151
|
+
/UNION\s+SELECT/i
|
|
1152
|
+
];
|
|
1153
|
+
var DEFAULT_ALLOWED_TABLES = [
|
|
1154
|
+
"pipelines",
|
|
1155
|
+
"nodes",
|
|
1156
|
+
"users",
|
|
1157
|
+
"schools",
|
|
1158
|
+
"hospitals",
|
|
1159
|
+
"substations",
|
|
1160
|
+
"base_stations",
|
|
1161
|
+
"rivers",
|
|
1162
|
+
"reservoirs",
|
|
1163
|
+
"alarms",
|
|
1164
|
+
"pois"
|
|
1165
|
+
];
|
|
1166
|
+
var ALLOWED_SPATIAL_FUNCTIONS = [
|
|
1167
|
+
"ST_DWithin",
|
|
1168
|
+
"ST_Within",
|
|
1169
|
+
"ST_Intersects",
|
|
1170
|
+
"ST_Contains",
|
|
1171
|
+
"ST_Buffer",
|
|
1172
|
+
"ST_MakePoint",
|
|
1173
|
+
"ST_SetSRID",
|
|
1174
|
+
"ST_Distance",
|
|
1175
|
+
"ST_AsGeoJSON",
|
|
1176
|
+
"ST_Transform"
|
|
1177
|
+
];
|
|
1178
|
+
function validateSql(sql, allowedTables = DEFAULT_ALLOWED_TABLES) {
|
|
1179
|
+
const issues = [];
|
|
1180
|
+
const upper = sql.toUpperCase().trim();
|
|
1181
|
+
if (!sql.trim()) {
|
|
1182
|
+
issues.push({ severity: "error", rule: "non_empty", message: "SQL \u4E0D\u80FD\u4E3A\u7A7A" });
|
|
1183
|
+
return { valid: false, issues };
|
|
1184
|
+
}
|
|
1185
|
+
if (!upper.startsWith("SELECT")) {
|
|
1186
|
+
issues.push({ severity: "error", rule: "read_only", message: "\u4EC5\u5141\u8BB8 SELECT \u53EA\u8BFB\u67E5\u8BE2" });
|
|
1187
|
+
}
|
|
1188
|
+
for (const kw of DANGEROUS_KEYWORDS) {
|
|
1189
|
+
const re = new RegExp(`\\b${kw}\\b`, "i");
|
|
1190
|
+
if (re.test(sql) && !(kw === "SET" && /ST_SetSRID/i.test(sql))) {
|
|
1191
|
+
issues.push({ severity: "error", rule: "dangerous_keyword", message: `\u68C0\u6D4B\u5230\u5371\u9669\u5173\u952E\u5B57 ${kw}` });
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
for (const p of INJECTION_PATTERNS) {
|
|
1195
|
+
if (p.test(sql)) {
|
|
1196
|
+
issues.push({ severity: "error", rule: "injection", message: "\u68C0\u6D4B\u5230 SQL \u6CE8\u5165\u7279\u5F81" });
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
const fromMatch = upper.match(/FROM\s+([A-Za-z_][\w]*)/);
|
|
1200
|
+
if (fromMatch) {
|
|
1201
|
+
const table = fromMatch[1].toLowerCase();
|
|
1202
|
+
if (!allowedTables.includes(table)) {
|
|
1203
|
+
issues.push({ severity: "error", rule: "table_whitelist", message: `\u8868 ${table} \u4E0D\u5728\u6388\u6743\u767D\u540D\u5355\u5185` });
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
const singleQuotes = (sql.match(/'/g) ?? []).length;
|
|
1207
|
+
if (singleQuotes % 2 !== 0) {
|
|
1208
|
+
issues.push({ severity: "error", rule: "syntax", message: "\u5355\u5F15\u53F7\u672A\u914D\u5BF9" });
|
|
1209
|
+
}
|
|
1210
|
+
const openParen = (sql.match(/\(/g) ?? []).length;
|
|
1211
|
+
const closeParen = (sql.match(/\)/g) ?? []).length;
|
|
1212
|
+
if (openParen !== closeParen) {
|
|
1213
|
+
issues.push({ severity: "error", rule: "syntax", message: "\u62EC\u53F7\u672A\u914D\u5BF9" });
|
|
1214
|
+
}
|
|
1215
|
+
for (const fn of ["ST_"]) {
|
|
1216
|
+
const fnMatch = sql.match(/ST_(\w+)/g) ?? [];
|
|
1217
|
+
for (const m of fnMatch) {
|
|
1218
|
+
if (!ALLOWED_SPATIAL_FUNCTIONS.includes(m)) {
|
|
1219
|
+
issues.push({ severity: "error", rule: "spatial_whitelist", message: `\u7A7A\u95F4\u51FD\u6570 ${m} \u4E0D\u5728\u767D\u540D\u5355\u5185` });
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
return { valid: issues.every((i) => i.severity !== "error"), issues };
|
|
1224
|
+
}
|
|
1225
|
+
function parameterize(sql) {
|
|
1226
|
+
const params = [];
|
|
1227
|
+
const out = sql.replace(/'([^']*)'/g, (_m, v) => {
|
|
1228
|
+
params.push(v);
|
|
1229
|
+
return `$${params.length}`;
|
|
1230
|
+
});
|
|
1231
|
+
return { sql: out, params };
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// src/nlpg/nlpg.ts
|
|
1235
|
+
function nlpgQuery(text, opts = {}) {
|
|
1236
|
+
const query = generatePostGISQuery(text, opts);
|
|
1237
|
+
const validation = validateSql(query.sql);
|
|
1238
|
+
const valid = validation.valid;
|
|
1239
|
+
const parameterized = valid ? parameterize(query.sql) : void 0;
|
|
1240
|
+
return { query, valid, validation, parameterized };
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// src/nlpg/llmNlpg.ts
|
|
1244
|
+
var SYSTEM_PROMPT2 = `\u4F60\u662F"\u8349\u679C\u5730\u56FE"\u7BA1\u7F51\u81EA\u7136\u8BED\u8A00\u67E5\u8BE2\u52A9\u624B\u3002\u7528\u6237\u7528\u4E2D\u6587\u63CF\u8FF0\u6570\u636E\u67E5\u8BE2\u9700\u6C42\uFF0C\u4F60\u751F\u6210 PostGIS SQL\uFF08\u4EC5 SELECT\uFF09\u3002
|
|
1245
|
+
|
|
1246
|
+
\u89C4\u5219\uFF1A
|
|
1247
|
+
1. \u53EA\u8FD4\u56DE JSON\uFF1A{"sql":"SELECT ...","table":"...","intent":"..."}
|
|
1248
|
+
2. \u8868\u540D\u53EA\u80FD\u6765\u81EA\u4EE5\u4E0B\u767D\u540D\u5355\uFF1A${DEFAULT_ALLOWED_TABLES.join(", ")}
|
|
1249
|
+
3. \u7A7A\u95F4\u67E5\u8BE2\u53EF\u7528 ST_DWithin / ST_Within / ST_Intersects / ST_Contains / ST_Buffer / ST_MakePoint / ST_SetSRID / ST_Distance / ST_AsGeoJSON / ST_Transform
|
|
1250
|
+
4. \u53EA\u751F\u6210 SELECT \u8BED\u53E5\uFF0C\u7981\u6B62 DROP/DELETE/UPDATE/INSERT/ALTER \u7B49\u5199\u64CD\u4F5C
|
|
1251
|
+
5. \u5B57\u6BB5\u540D\u4F7F\u7528 snake_case\uFF0C\u5750\u6807\u4F7F\u7528 WGS84\uFF08SRID 4326\uFF09
|
|
1252
|
+
6. \u4E0D\u8981\u8F93\u51FA JSON \u4EE5\u5916\u7684\u4EFB\u4F55\u6587\u5B57\u3002`;
|
|
1253
|
+
var LlmNlpg = class {
|
|
1254
|
+
client;
|
|
1255
|
+
enabled;
|
|
1256
|
+
allowedTables;
|
|
1257
|
+
constructor(config) {
|
|
1258
|
+
this.client = config.client;
|
|
1259
|
+
this.enabled = config.enabled ?? true;
|
|
1260
|
+
this.allowedTables = config.allowedTables ?? DEFAULT_ALLOWED_TABLES;
|
|
1261
|
+
}
|
|
1262
|
+
/**
|
|
1263
|
+
* 自然语言查询。
|
|
1264
|
+
* LLM 优先,失败或校验不过时降级到规则引擎。
|
|
1265
|
+
*/
|
|
1266
|
+
async query(text) {
|
|
1267
|
+
if (this.enabled) {
|
|
1268
|
+
try {
|
|
1269
|
+
const llmResult = await this.queryWithLlm(text);
|
|
1270
|
+
if (llmResult) return llmResult;
|
|
1271
|
+
} catch {
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
return this.queryWithRules(text);
|
|
1275
|
+
}
|
|
1276
|
+
async queryWithLlm(text) {
|
|
1277
|
+
const messages = [
|
|
1278
|
+
{ role: "system", content: SYSTEM_PROMPT2 },
|
|
1279
|
+
{ role: "user", content: text }
|
|
1280
|
+
];
|
|
1281
|
+
const { data } = await this.client.chatJson(messages);
|
|
1282
|
+
const sql = (data.sql ?? "").trim();
|
|
1283
|
+
if (!sql) return null;
|
|
1284
|
+
const validation = validateSql(sql, this.allowedTables);
|
|
1285
|
+
if (!validation.valid) return null;
|
|
1286
|
+
const query = {
|
|
1287
|
+
intent: "mixed",
|
|
1288
|
+
table: data.table ?? "pois",
|
|
1289
|
+
conditions: [],
|
|
1290
|
+
spatial: null,
|
|
1291
|
+
sql,
|
|
1292
|
+
confidence: 0.9
|
|
1293
|
+
};
|
|
1294
|
+
return { query, valid: true, parameterized: parameterize(sql) };
|
|
1295
|
+
}
|
|
1296
|
+
queryWithRules(text) {
|
|
1297
|
+
const query = generatePostGISQuery(text);
|
|
1298
|
+
const validation = validateSql(query.sql, this.allowedTables);
|
|
1299
|
+
return {
|
|
1300
|
+
query,
|
|
1301
|
+
valid: validation.valid,
|
|
1302
|
+
parameterized: validation.valid ? parameterize(query.sql) : void 0
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
|
|
1307
|
+
// src/llm/deepseek.ts
|
|
1308
|
+
var DEFAULT_BASE_URL = "https://api.deepseek.com";
|
|
1309
|
+
var DEFAULT_MODEL = "deepseek-chat";
|
|
1310
|
+
function normalizeError(body) {
|
|
1311
|
+
const err = new Error(body || "DeepSeek API \u8BF7\u6C42\u5931\u8D25");
|
|
1312
|
+
err.name = "DeepSeekError";
|
|
1313
|
+
return err;
|
|
1314
|
+
}
|
|
1315
|
+
var DeepSeekClient = class {
|
|
1316
|
+
config;
|
|
1317
|
+
lastRequestId = 0;
|
|
1318
|
+
constructor(config) {
|
|
1319
|
+
if (!config.apiKey) throw new Error("DeepSeekClient: apiKey \u4E0D\u80FD\u4E3A\u7A7A");
|
|
1320
|
+
this.config = {
|
|
1321
|
+
apiKey: config.apiKey,
|
|
1322
|
+
baseUrl: (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ""),
|
|
1323
|
+
model: config.model ?? DEFAULT_MODEL,
|
|
1324
|
+
temperature: config.temperature ?? 0.3,
|
|
1325
|
+
maxTokens: config.maxTokens ?? 2048,
|
|
1326
|
+
timeoutMs: config.timeoutMs ?? 3e4,
|
|
1327
|
+
retries: config.retries ?? 2,
|
|
1328
|
+
fetchImpl: config.fetchImpl ?? (globalThis.fetch ?? fetch)
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
/** 发起一次聊天补全 */
|
|
1332
|
+
async chat(messages, opts = {}) {
|
|
1333
|
+
return this.requestWithRetry(messages, opts);
|
|
1334
|
+
}
|
|
1335
|
+
/** 聊天 + JSON 解析 */
|
|
1336
|
+
async chatJson(messages) {
|
|
1337
|
+
const result = await this.chat(messages, { json: true });
|
|
1338
|
+
const raw = result.content.trim();
|
|
1339
|
+
const cleaned = raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
|
|
1340
|
+
let data;
|
|
1341
|
+
try {
|
|
1342
|
+
data = JSON.parse(cleaned);
|
|
1343
|
+
} catch (e) {
|
|
1344
|
+
const match = cleaned.match(/\{[\s\S]*\}|\[[\s\S]*\]/);
|
|
1345
|
+
if (match) {
|
|
1346
|
+
data = JSON.parse(match[0]);
|
|
1347
|
+
} else {
|
|
1348
|
+
throw new Error(`DeepSeek \u8FD4\u56DE\u4E86\u975E JSON \u5185\u5BB9: ${raw.slice(0, 200)}`);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
return { data, raw, model: result.model };
|
|
1352
|
+
}
|
|
1353
|
+
async requestWithRetry(messages, opts) {
|
|
1354
|
+
let lastError = null;
|
|
1355
|
+
for (let attempt = 0; attempt <= this.config.retries; attempt++) {
|
|
1356
|
+
try {
|
|
1357
|
+
return await this.requestOnce(messages, opts);
|
|
1358
|
+
} catch (e) {
|
|
1359
|
+
lastError = e;
|
|
1360
|
+
const retryable = !(e instanceof Error) || !e.name.includes("DeepSeek") || this.isRetryable(e);
|
|
1361
|
+
if (!retryable && !this.isRetryable(e)) break;
|
|
1362
|
+
if (attempt < this.config.retries) {
|
|
1363
|
+
await this.sleep(Math.min(1e3 * 2 ** attempt, 8e3));
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
throw lastError ?? new Error("DeepSeek \u8BF7\u6C42\u5931\u8D25");
|
|
1368
|
+
}
|
|
1369
|
+
isRetryable(err) {
|
|
1370
|
+
const msg = err.message ?? "";
|
|
1371
|
+
return /429|500|502|503|504|timeout|network|ECONN|abort|fetch failed/i.test(msg);
|
|
1372
|
+
}
|
|
1373
|
+
async requestOnce(messages, opts) {
|
|
1374
|
+
const { fetchImpl } = this.config;
|
|
1375
|
+
const controller = new AbortController();
|
|
1376
|
+
const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
1377
|
+
const body = {
|
|
1378
|
+
model: this.config.model,
|
|
1379
|
+
messages,
|
|
1380
|
+
temperature: this.config.temperature,
|
|
1381
|
+
max_tokens: this.config.maxTokens,
|
|
1382
|
+
stream: Boolean(opts.onChunk)
|
|
1383
|
+
};
|
|
1384
|
+
if (opts.json) {
|
|
1385
|
+
body.response_format = { type: "json_object" };
|
|
1386
|
+
}
|
|
1387
|
+
try {
|
|
1388
|
+
const res = await fetchImpl(`${this.config.baseUrl}/chat/completions`, {
|
|
1389
|
+
method: "POST",
|
|
1390
|
+
headers: {
|
|
1391
|
+
"Content-Type": "application/json",
|
|
1392
|
+
Authorization: `Bearer ${this.config.apiKey}`
|
|
1393
|
+
},
|
|
1394
|
+
body: JSON.stringify(body),
|
|
1395
|
+
signal: controller.signal
|
|
1396
|
+
});
|
|
1397
|
+
if (!res.ok) {
|
|
1398
|
+
const text = await res.text().catch(() => "");
|
|
1399
|
+
throw normalizeError(`DeepSeek HTTP ${res.status}: ${text}`);
|
|
1400
|
+
}
|
|
1401
|
+
if (opts.onChunk && res.body) {
|
|
1402
|
+
return this.parseStream(res.body, opts.onChunk);
|
|
1403
|
+
}
|
|
1404
|
+
const json = await res.json();
|
|
1405
|
+
const content = json.choices?.[0]?.message?.content ?? "";
|
|
1406
|
+
return {
|
|
1407
|
+
content,
|
|
1408
|
+
model: json.model ?? this.config.model,
|
|
1409
|
+
usage: json.usage ? { promptTokens: json.usage.prompt_tokens, completionTokens: json.usage.completion_tokens, totalTokens: json.usage.total_tokens } : void 0
|
|
1410
|
+
};
|
|
1411
|
+
} finally {
|
|
1412
|
+
clearTimeout(timer);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
/** 解析 SSE 流式响应 */
|
|
1416
|
+
async parseStream(body, onChunk) {
|
|
1417
|
+
const reader = body.getReader();
|
|
1418
|
+
const decoder = new TextDecoder();
|
|
1419
|
+
let content = "";
|
|
1420
|
+
let buffer = "";
|
|
1421
|
+
while (true) {
|
|
1422
|
+
const { done, value } = await reader.read();
|
|
1423
|
+
if (done) break;
|
|
1424
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1425
|
+
const lines = buffer.split("\n");
|
|
1426
|
+
buffer = lines.pop() ?? "";
|
|
1427
|
+
for (const line of lines) {
|
|
1428
|
+
const trimmed = line.trim();
|
|
1429
|
+
if (!trimmed.startsWith("data:")) continue;
|
|
1430
|
+
const data = trimmed.slice(5).trim();
|
|
1431
|
+
if (data === "[DONE]") continue;
|
|
1432
|
+
try {
|
|
1433
|
+
const json = JSON.parse(data);
|
|
1434
|
+
const delta = json.choices?.[0]?.delta?.content ?? "";
|
|
1435
|
+
if (delta) {
|
|
1436
|
+
content += delta;
|
|
1437
|
+
onChunk(delta);
|
|
1438
|
+
}
|
|
1439
|
+
} catch {
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
return { content, model: this.config.model };
|
|
1444
|
+
}
|
|
1445
|
+
sleep(ms) {
|
|
1446
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
function createDeepSeekClient(config) {
|
|
1450
|
+
return new DeepSeekClient(config);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
240
1453
|
exports.CARRIER_STYLES = CARRIER_STYLES;
|
|
1454
|
+
exports.CHINA_BOUNDS = CHINA_BOUNDS;
|
|
1455
|
+
exports.COLOR_NAMES = COLOR_NAMES;
|
|
1456
|
+
exports.DEFAULT_ALLOWED_TABLES = DEFAULT_ALLOWED_TABLES;
|
|
241
1457
|
exports.DIAGNOSIS_RULES = DIAGNOSIS_RULES;
|
|
1458
|
+
exports.DeepSeekClient = DeepSeekClient;
|
|
242
1459
|
exports.INDUSTRY_TEMPLATES = INDUSTRY_TEMPLATES;
|
|
1460
|
+
exports.LOCAL_GEO_DB = LOCAL_GEO_DB;
|
|
1461
|
+
exports.LlmMapCopilot = LlmMapCopilot;
|
|
1462
|
+
exports.LlmNlpg = LlmNlpg;
|
|
1463
|
+
exports.MapCopilot = MapCopilot;
|
|
1464
|
+
exports.PLACE_COORDINATES = PLACE_COORDINATES;
|
|
243
1465
|
exports.adjustBrightness = adjustBrightness;
|
|
244
1466
|
exports.analyzePerformance = analyzePerformance;
|
|
245
1467
|
exports.analyzeTiles = analyzeTiles;
|
|
1468
|
+
exports.batchGeocode = batchGeocode;
|
|
1469
|
+
exports.classifyIntent = classifyIntent;
|
|
1470
|
+
exports.createDeepSeekClient = createDeepSeekClient;
|
|
1471
|
+
exports.detectCRS = detectCRS;
|
|
1472
|
+
exports.detectField = detectField;
|
|
1473
|
+
exports.detectHeaders = detectHeaders;
|
|
246
1474
|
exports.detectMemoryLeak = detectMemoryLeak;
|
|
1475
|
+
exports.detectSpatial = detectSpatial;
|
|
1476
|
+
exports.detectTable = detectTable;
|
|
1477
|
+
exports.detectValue = detectValue;
|
|
247
1478
|
exports.diagnose = diagnose;
|
|
248
1479
|
exports.extractDominantColor = extractDominantColor;
|
|
1480
|
+
exports.extractParams = extractParams;
|
|
1481
|
+
exports.gcj02ToWgs84 = gcj02ToWgs84;
|
|
249
1482
|
exports.generateBrandStyle = generateBrandStyle;
|
|
250
1483
|
exports.generateCarrierStyle = generateCarrierStyle;
|
|
1484
|
+
exports.generateCode = generateCode;
|
|
1485
|
+
exports.generateFromQuery = generateFromQuery;
|
|
251
1486
|
exports.generateIndustryStyle = generateIndustryStyle;
|
|
1487
|
+
exports.generatePostGISQuery = generatePostGISQuery;
|
|
1488
|
+
exports.geocode = geocode;
|
|
1489
|
+
exports.hasLocalGeo = hasLocalGeo;
|
|
1490
|
+
exports.importToGeoJSON = importToGeoJSON;
|
|
1491
|
+
exports.isInChina = isInChina;
|
|
1492
|
+
exports.isValidAddress = isValidAddress;
|
|
1493
|
+
exports.nlpgQuery = nlpgQuery;
|
|
1494
|
+
exports.parameterize = parameterize;
|
|
1495
|
+
exports.parseAddress = parseAddress;
|
|
252
1496
|
exports.suggestOptimizations = suggestOptimizations;
|
|
253
1497
|
exports.toggleMode = toggleMode;
|
|
1498
|
+
exports.validateSql = validateSql;
|
|
1499
|
+
exports.wgs84ToGcj02 = wgs84ToGcj02;
|
|
254
1500
|
//# sourceMappingURL=index.cjs.map
|
|
255
1501
|
//# sourceMappingURL=index.cjs.map
|