@caoguo/maplibre-ai 0.0.2 → 0.0.5

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.
Files changed (41) hide show
  1. package/README.md +93 -39
  2. package/dist/chunk-55G7QUFA.js +293 -0
  3. package/dist/chunk-55G7QUFA.js.map +1 -0
  4. package/dist/chunk-ARYB2X4B.js +348 -0
  5. package/dist/chunk-ARYB2X4B.js.map +1 -0
  6. package/dist/chunk-HF3HJPXD.js +435 -0
  7. package/dist/chunk-HF3HJPXD.js.map +1 -0
  8. package/dist/chunk-WVEICADA.js +376 -0
  9. package/dist/chunk-WVEICADA.js.map +1 -0
  10. package/dist/chunk-ZI6ZF26Q.js +149 -0
  11. package/dist/chunk-ZI6ZF26Q.js.map +1 -0
  12. package/dist/copilot/index.cjs +302 -0
  13. package/dist/copilot/index.cjs.map +1 -0
  14. package/dist/copilot/index.d.cts +108 -0
  15. package/dist/copilot/index.d.ts +108 -0
  16. package/dist/copilot/index.js +3 -0
  17. package/dist/copilot/index.js.map +1 -0
  18. package/dist/geoai/index.cjs +449 -0
  19. package/dist/geoai/index.cjs.map +1 -0
  20. package/dist/geoai/index.d.cts +212 -0
  21. package/dist/geoai/index.d.ts +212 -0
  22. package/dist/geoai/index.js +3 -0
  23. package/dist/geoai/index.js.map +1 -0
  24. package/dist/index.cjs +1274 -0
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +4 -0
  27. package/dist/index.d.ts +4 -0
  28. package/dist/index.js +4 -0
  29. package/dist/llm/index.cjs +152 -0
  30. package/dist/llm/index.cjs.map +1 -0
  31. package/dist/llm/index.d.cts +81 -0
  32. package/dist/llm/index.d.ts +81 -0
  33. package/dist/llm/index.js +3 -0
  34. package/dist/llm/index.js.map +1 -0
  35. package/dist/nlpg/index.cjs +387 -0
  36. package/dist/nlpg/index.cjs.map +1 -0
  37. package/dist/nlpg/index.d.cts +162 -0
  38. package/dist/nlpg/index.d.ts +162 -0
  39. package/dist/nlpg/index.js +3 -0
  40. package/dist/nlpg/index.js.map +1 -0
  41. package/package.json +39 -7
package/dist/index.cjs CHANGED
@@ -237,19 +237,1293 @@ 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
+ const point = detectReferencePoint(text);
1047
+ if (/缓冲区|缓冲|范围内|区域.{0,2}内/.test(text)) {
1048
+ return { relation: "buffer", point, geometryColumn };
1049
+ }
1050
+ if (/包含|覆盖|涵盖|包围|圈住/.test(text)) {
1051
+ return { relation: "contains", point, geometryColumn };
1052
+ }
1053
+ if (/在.{0,2}内|内部|以内|之中/.test(text)) {
1054
+ return { relation: "within", point, geometryColumn };
1055
+ }
1056
+ if (/相交|叠加|重叠|交叉/.test(text)) {
1057
+ return { relation: "intersects", point, geometryColumn };
1058
+ }
1059
+ return null;
1060
+ }
1061
+ function detectReferencePoint(text) {
1062
+ const m = text.match(/([1-2]?\d{2,3}(?:\.\d+)?)\s*[,,]\s*(\d{1,3}(?:\.\d+)?)/);
1063
+ if (m) {
1064
+ const lng = parseFloat(m[1]);
1065
+ const lat = parseFloat(m[2]);
1066
+ if (lng >= -180 && lng <= 180 && lat >= -90 && lat <= 90) return [lng, lat];
1067
+ }
1068
+ return void 0;
1069
+ }
1070
+ function quoteValue(v) {
1071
+ return typeof v === "string" ? `'${v.replace(/'/g, "''")}'` : String(v);
1072
+ }
1073
+ function buildWhere(conditions, spatial) {
1074
+ const parts = [];
1075
+ for (const c of conditions) {
1076
+ parts.push(`${c.field} ${c.operator} ${quoteValue(c.value)}`);
1077
+ }
1078
+ if (spatial) {
1079
+ const col = spatial.geometryColumn;
1080
+ const refGeom = spatial.referenceColumn ? spatial.referenceColumn : spatial.point ? `ST_Buffer(ST_SetSRID(ST_MakePoint(${spatial.point[0]}, ${spatial.point[1]}), 4326), ${spatial.radius ?? 0})` : null;
1081
+ switch (spatial.relation) {
1082
+ case "dwithin":
1083
+ if (spatial.point && spatial.radius !== void 0) {
1084
+ parts.push(`ST_DWithin(${col}, ST_SetSRID(ST_MakePoint(${spatial.point[0]}, ${spatial.point[1]}), 4326), ${spatial.radius})`);
1085
+ }
1086
+ break;
1087
+ case "buffer":
1088
+ if (refGeom) parts.push(`ST_Intersects(${col}, ${refGeom})`);
1089
+ break;
1090
+ case "within":
1091
+ if (refGeom) parts.push(`ST_Within(${col}, ${refGeom})`);
1092
+ break;
1093
+ case "contains":
1094
+ if (refGeom) parts.push(`ST_Contains(${refGeom}, ${col})`);
1095
+ break;
1096
+ case "intersects":
1097
+ if (refGeom) parts.push(`ST_Intersects(${col}, ${refGeom})`);
1098
+ break;
1099
+ }
1100
+ }
1101
+ return parts.length > 0 ? `WHERE ${parts.join(" AND ")}` : "";
1102
+ }
1103
+ function generatePostGISQuery(text, opts = {}) {
1104
+ const center = opts.center ?? [114.305, 30.593];
1105
+ const geometryColumn = opts.geometryColumn ?? "geom";
1106
+ const table = detectTable(text);
1107
+ const spatial = detectSpatial(text, geometryColumn);
1108
+ if (spatial && !spatial.point) {
1109
+ spatial.point = center;
1110
+ }
1111
+ if (spatial && spatial.relation !== "dwithin" && spatial.radius === void 0) {
1112
+ spatial.radius = 1e3;
1113
+ }
1114
+ const conditions = [];
1115
+ const field = detectField(text);
1116
+ if (field) {
1117
+ const value = detectValue(text, field);
1118
+ if (value !== null) {
1119
+ conditions.push({ field, operator: detectOperator(text), value });
1120
+ }
1121
+ }
1122
+ let intent;
1123
+ if (spatial && conditions.length > 0) intent = "mixed";
1124
+ else if (spatial) intent = spatial.relation === "dwithin" ? "spatial_nearby" : "spatial_within";
1125
+ else if (conditions.length > 0) intent = "attribute_filter";
1126
+ else intent = "unknown";
1127
+ const where = buildWhere(conditions, spatial);
1128
+ const sql = `SELECT * FROM ${table} ${where}`.trim();
1129
+ let confidence = 0.3;
1130
+ if (conditions.length > 0) confidence += 0.3;
1131
+ if (spatial) confidence += 0.3;
1132
+ if (table !== "pois") confidence += 0.1;
1133
+ confidence = Math.min(0.95, confidence);
1134
+ return { intent, table, conditions, spatial, sql, confidence };
1135
+ }
1136
+
1137
+ // src/nlpg/sqlValidator.ts
1138
+ var DANGEROUS_KEYWORDS = [
1139
+ "DROP",
1140
+ "DELETE",
1141
+ "UPDATE",
1142
+ "INSERT",
1143
+ "ALTER",
1144
+ "TRUNCATE",
1145
+ "GRANT",
1146
+ "REVOKE",
1147
+ "MERGE",
1148
+ "CREATE",
1149
+ "REPLACE",
1150
+ "EXEC",
1151
+ "EXECUTE",
1152
+ "CALL",
1153
+ "COPY",
1154
+ "LOAD",
1155
+ "INTO",
1156
+ "SET",
1157
+ "UNION",
1158
+ "ATTACH",
1159
+ "DETACH",
1160
+ "PRAGMA",
1161
+ "VACUUM",
1162
+ "REINDEX"
1163
+ ];
1164
+ var INJECTION_PATTERNS = [
1165
+ /--/,
1166
+ // SQL 注释
1167
+ /\/\*/,
1168
+ // 块注释
1169
+ /;\s*(DROP|DELETE|UPDATE|INSERT|ALTER|TRUNCATE)/i,
1170
+ // 多语句注入
1171
+ /'\s*OR\s*'/i,
1172
+ // ' OR ' 恒真注入(含 ' OR '1'='1')
1173
+ /"\s*OR\s*"/i,
1174
+ // " OR " 恒真注入
1175
+ /\bOR\s+\d+\s*=\s*\d+\b/i,
1176
+ // OR 1=1 恒真
1177
+ /\bOR\s+'\w*'\s*=\s*'\w*'/i,
1178
+ // OR 'a'='a' 恒真
1179
+ /UNION\s+SELECT/i
1180
+ ];
1181
+ var DEFAULT_ALLOWED_TABLES = [
1182
+ "pipelines",
1183
+ "nodes",
1184
+ "users",
1185
+ "schools",
1186
+ "hospitals",
1187
+ "substations",
1188
+ "base_stations",
1189
+ "rivers",
1190
+ "reservoirs",
1191
+ "alarms",
1192
+ "pois"
1193
+ ];
1194
+ var ALLOWED_SPATIAL_FUNCTIONS = [
1195
+ "ST_DWithin",
1196
+ "ST_Within",
1197
+ "ST_Intersects",
1198
+ "ST_Contains",
1199
+ "ST_Buffer",
1200
+ "ST_MakePoint",
1201
+ "ST_SetSRID",
1202
+ "ST_Distance",
1203
+ "ST_AsGeoJSON",
1204
+ "ST_Transform"
1205
+ ];
1206
+ function validateSql(sql, allowedTables = DEFAULT_ALLOWED_TABLES) {
1207
+ const issues = [];
1208
+ const upper = sql.toUpperCase().trim();
1209
+ if (!sql.trim()) {
1210
+ issues.push({ severity: "error", rule: "non_empty", message: "SQL \u4E0D\u80FD\u4E3A\u7A7A" });
1211
+ return { valid: false, issues };
1212
+ }
1213
+ if (!upper.startsWith("SELECT")) {
1214
+ issues.push({ severity: "error", rule: "read_only", message: "\u4EC5\u5141\u8BB8 SELECT \u53EA\u8BFB\u67E5\u8BE2" });
1215
+ }
1216
+ for (const kw of DANGEROUS_KEYWORDS) {
1217
+ const re = new RegExp(`\\b${kw}\\b`, "i");
1218
+ if (re.test(sql) && !(kw === "SET" && /ST_SetSRID/i.test(sql))) {
1219
+ issues.push({ severity: "error", rule: "dangerous_keyword", message: `\u68C0\u6D4B\u5230\u5371\u9669\u5173\u952E\u5B57 ${kw}` });
1220
+ }
1221
+ }
1222
+ for (const p of INJECTION_PATTERNS) {
1223
+ if (p.test(sql)) {
1224
+ issues.push({ severity: "error", rule: "injection", message: "\u68C0\u6D4B\u5230 SQL \u6CE8\u5165\u7279\u5F81" });
1225
+ }
1226
+ }
1227
+ const fromMatch = upper.match(/FROM\s+([A-Za-z_][\w]*)/);
1228
+ if (fromMatch) {
1229
+ const table = fromMatch[1].toLowerCase();
1230
+ if (!allowedTables.includes(table)) {
1231
+ issues.push({ severity: "error", rule: "table_whitelist", message: `\u8868 ${table} \u4E0D\u5728\u6388\u6743\u767D\u540D\u5355\u5185` });
1232
+ }
1233
+ }
1234
+ const singleQuotes = (sql.match(/'/g) ?? []).length;
1235
+ if (singleQuotes % 2 !== 0) {
1236
+ issues.push({ severity: "error", rule: "syntax", message: "\u5355\u5F15\u53F7\u672A\u914D\u5BF9" });
1237
+ }
1238
+ const openParen = (sql.match(/\(/g) ?? []).length;
1239
+ const closeParen = (sql.match(/\)/g) ?? []).length;
1240
+ if (openParen !== closeParen) {
1241
+ issues.push({ severity: "error", rule: "syntax", message: "\u62EC\u53F7\u672A\u914D\u5BF9" });
1242
+ }
1243
+ for (const fn of ["ST_"]) {
1244
+ const fnMatch = sql.match(/ST_(\w+)/g) ?? [];
1245
+ for (const m of fnMatch) {
1246
+ if (!ALLOWED_SPATIAL_FUNCTIONS.includes(m)) {
1247
+ issues.push({ severity: "error", rule: "spatial_whitelist", message: `\u7A7A\u95F4\u51FD\u6570 ${m} \u4E0D\u5728\u767D\u540D\u5355\u5185` });
1248
+ }
1249
+ }
1250
+ }
1251
+ return { valid: issues.every((i) => i.severity !== "error"), issues };
1252
+ }
1253
+ function parameterize(sql) {
1254
+ const params = [];
1255
+ const out = sql.replace(/'([^']*)'/g, (_m, v) => {
1256
+ params.push(v);
1257
+ return `$${params.length}`;
1258
+ });
1259
+ return { sql: out, params };
1260
+ }
1261
+
1262
+ // src/nlpg/nlpg.ts
1263
+ function nlpgQuery(text, opts = {}) {
1264
+ const query = generatePostGISQuery(text, opts);
1265
+ const validation = validateSql(query.sql);
1266
+ const valid = validation.valid;
1267
+ const parameterized = valid ? parameterize(query.sql) : void 0;
1268
+ return { query, valid, validation, parameterized };
1269
+ }
1270
+
1271
+ // src/nlpg/llmNlpg.ts
1272
+ 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
1273
+
1274
+ \u89C4\u5219\uFF1A
1275
+ 1. \u53EA\u8FD4\u56DE JSON\uFF1A{"sql":"SELECT ...","table":"...","intent":"..."}
1276
+ 2. \u8868\u540D\u53EA\u80FD\u6765\u81EA\u4EE5\u4E0B\u767D\u540D\u5355\uFF1A${DEFAULT_ALLOWED_TABLES.join(", ")}
1277
+ 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
1278
+ 4. \u53EA\u751F\u6210 SELECT \u8BED\u53E5\uFF0C\u7981\u6B62 DROP/DELETE/UPDATE/INSERT/ALTER \u7B49\u5199\u64CD\u4F5C
1279
+ 5. \u5B57\u6BB5\u540D\u4F7F\u7528 snake_case\uFF0C\u5750\u6807\u4F7F\u7528 WGS84\uFF08SRID 4326\uFF09
1280
+ 6. \u4E0D\u8981\u8F93\u51FA JSON \u4EE5\u5916\u7684\u4EFB\u4F55\u6587\u5B57\u3002`;
1281
+ var LlmNlpg = class {
1282
+ client;
1283
+ enabled;
1284
+ allowedTables;
1285
+ constructor(config) {
1286
+ this.client = config.client;
1287
+ this.enabled = config.enabled ?? true;
1288
+ this.allowedTables = config.allowedTables ?? DEFAULT_ALLOWED_TABLES;
1289
+ }
1290
+ /**
1291
+ * 自然语言查询。
1292
+ * LLM 优先,失败或校验不过时降级到规则引擎。
1293
+ */
1294
+ async query(text) {
1295
+ if (this.enabled) {
1296
+ try {
1297
+ const llmResult = await this.queryWithLlm(text);
1298
+ if (llmResult) return llmResult;
1299
+ } catch {
1300
+ }
1301
+ }
1302
+ return this.queryWithRules(text);
1303
+ }
1304
+ async queryWithLlm(text) {
1305
+ const messages = [
1306
+ { role: "system", content: SYSTEM_PROMPT2 },
1307
+ { role: "user", content: text }
1308
+ ];
1309
+ const { data } = await this.client.chatJson(messages);
1310
+ const sql = (data.sql ?? "").trim();
1311
+ if (!sql) return null;
1312
+ const validation = validateSql(sql, this.allowedTables);
1313
+ if (!validation.valid) return null;
1314
+ const query = {
1315
+ intent: "mixed",
1316
+ table: data.table ?? "pois",
1317
+ conditions: [],
1318
+ spatial: null,
1319
+ sql,
1320
+ confidence: 0.9
1321
+ };
1322
+ return { query, valid: true, parameterized: parameterize(sql) };
1323
+ }
1324
+ queryWithRules(text) {
1325
+ const query = generatePostGISQuery(text);
1326
+ const validation = validateSql(query.sql, this.allowedTables);
1327
+ return {
1328
+ query,
1329
+ valid: validation.valid,
1330
+ parameterized: validation.valid ? parameterize(query.sql) : void 0
1331
+ };
1332
+ }
1333
+ };
1334
+
1335
+ // src/llm/deepseek.ts
1336
+ var DEFAULT_BASE_URL = "https://api.deepseek.com";
1337
+ var DEFAULT_MODEL = "deepseek-chat";
1338
+ function normalizeError(body) {
1339
+ const err = new Error(body || "DeepSeek API \u8BF7\u6C42\u5931\u8D25");
1340
+ err.name = "DeepSeekError";
1341
+ return err;
1342
+ }
1343
+ var DeepSeekClient = class {
1344
+ config;
1345
+ lastRequestId = 0;
1346
+ constructor(config) {
1347
+ if (!config.apiKey) throw new Error("DeepSeekClient: apiKey \u4E0D\u80FD\u4E3A\u7A7A");
1348
+ this.config = {
1349
+ apiKey: config.apiKey,
1350
+ baseUrl: (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ""),
1351
+ model: config.model ?? DEFAULT_MODEL,
1352
+ temperature: config.temperature ?? 0.3,
1353
+ maxTokens: config.maxTokens ?? 2048,
1354
+ timeoutMs: config.timeoutMs ?? 3e4,
1355
+ retries: config.retries ?? 2,
1356
+ fetchImpl: config.fetchImpl ?? (globalThis.fetch ?? fetch)
1357
+ };
1358
+ }
1359
+ /** 发起一次聊天补全 */
1360
+ async chat(messages, opts = {}) {
1361
+ return this.requestWithRetry(messages, opts);
1362
+ }
1363
+ /** 聊天 + JSON 解析 */
1364
+ async chatJson(messages) {
1365
+ const result = await this.chat(messages, { json: true });
1366
+ const raw = result.content.trim();
1367
+ const cleaned = raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
1368
+ let data;
1369
+ try {
1370
+ data = JSON.parse(cleaned);
1371
+ } catch (e) {
1372
+ const match = cleaned.match(/\{[\s\S]*\}|\[[\s\S]*\]/);
1373
+ if (match) {
1374
+ data = JSON.parse(match[0]);
1375
+ } else {
1376
+ throw new Error(`DeepSeek \u8FD4\u56DE\u4E86\u975E JSON \u5185\u5BB9: ${raw.slice(0, 200)}`);
1377
+ }
1378
+ }
1379
+ return { data, raw, model: result.model };
1380
+ }
1381
+ async requestWithRetry(messages, opts) {
1382
+ let lastError = null;
1383
+ for (let attempt = 0; attempt <= this.config.retries; attempt++) {
1384
+ try {
1385
+ return await this.requestOnce(messages, opts);
1386
+ } catch (e) {
1387
+ lastError = e;
1388
+ const retryable = !(e instanceof Error) || !e.name.includes("DeepSeek") || this.isRetryable(e);
1389
+ if (!retryable && !this.isRetryable(e)) break;
1390
+ if (attempt < this.config.retries) {
1391
+ await this.sleep(Math.min(1e3 * 2 ** attempt, 8e3));
1392
+ }
1393
+ }
1394
+ }
1395
+ throw lastError ?? new Error("DeepSeek \u8BF7\u6C42\u5931\u8D25");
1396
+ }
1397
+ isRetryable(err) {
1398
+ const msg = err.message ?? "";
1399
+ return /429|500|502|503|504|timeout|network|ECONN|abort|fetch failed/i.test(msg);
1400
+ }
1401
+ async requestOnce(messages, opts) {
1402
+ const { fetchImpl } = this.config;
1403
+ const controller = new AbortController();
1404
+ const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
1405
+ const body = {
1406
+ model: this.config.model,
1407
+ messages,
1408
+ temperature: this.config.temperature,
1409
+ max_tokens: this.config.maxTokens,
1410
+ stream: Boolean(opts.onChunk)
1411
+ };
1412
+ if (opts.json) {
1413
+ body.response_format = { type: "json_object" };
1414
+ }
1415
+ try {
1416
+ const res = await fetchImpl(`${this.config.baseUrl}/chat/completions`, {
1417
+ method: "POST",
1418
+ headers: {
1419
+ "Content-Type": "application/json",
1420
+ Authorization: `Bearer ${this.config.apiKey}`
1421
+ },
1422
+ body: JSON.stringify(body),
1423
+ signal: controller.signal
1424
+ });
1425
+ if (!res.ok) {
1426
+ const text = await res.text().catch(() => "");
1427
+ throw normalizeError(`DeepSeek HTTP ${res.status}: ${text}`);
1428
+ }
1429
+ if (opts.onChunk && res.body) {
1430
+ return this.parseStream(res.body, opts.onChunk);
1431
+ }
1432
+ const json = await res.json();
1433
+ const content = json.choices?.[0]?.message?.content ?? "";
1434
+ return {
1435
+ content,
1436
+ model: json.model ?? this.config.model,
1437
+ usage: json.usage ? { promptTokens: json.usage.prompt_tokens, completionTokens: json.usage.completion_tokens, totalTokens: json.usage.total_tokens } : void 0
1438
+ };
1439
+ } finally {
1440
+ clearTimeout(timer);
1441
+ }
1442
+ }
1443
+ /** 解析 SSE 流式响应 */
1444
+ async parseStream(body, onChunk) {
1445
+ const reader = body.getReader();
1446
+ const decoder = new TextDecoder();
1447
+ let content = "";
1448
+ let buffer = "";
1449
+ while (true) {
1450
+ const { done, value } = await reader.read();
1451
+ if (done) break;
1452
+ buffer += decoder.decode(value, { stream: true });
1453
+ const lines = buffer.split("\n");
1454
+ buffer = lines.pop() ?? "";
1455
+ for (const line of lines) {
1456
+ const trimmed = line.trim();
1457
+ if (!trimmed.startsWith("data:")) continue;
1458
+ const data = trimmed.slice(5).trim();
1459
+ if (data === "[DONE]") continue;
1460
+ try {
1461
+ const json = JSON.parse(data);
1462
+ const delta = json.choices?.[0]?.delta?.content ?? "";
1463
+ if (delta) {
1464
+ content += delta;
1465
+ onChunk(delta);
1466
+ }
1467
+ } catch {
1468
+ }
1469
+ }
1470
+ }
1471
+ return { content, model: this.config.model };
1472
+ }
1473
+ sleep(ms) {
1474
+ return new Promise((r) => setTimeout(r, ms));
1475
+ }
1476
+ };
1477
+ function createDeepSeekClient(config) {
1478
+ return new DeepSeekClient(config);
1479
+ }
1480
+
240
1481
  exports.CARRIER_STYLES = CARRIER_STYLES;
1482
+ exports.CHINA_BOUNDS = CHINA_BOUNDS;
1483
+ exports.COLOR_NAMES = COLOR_NAMES;
1484
+ exports.DEFAULT_ALLOWED_TABLES = DEFAULT_ALLOWED_TABLES;
241
1485
  exports.DIAGNOSIS_RULES = DIAGNOSIS_RULES;
1486
+ exports.DeepSeekClient = DeepSeekClient;
242
1487
  exports.INDUSTRY_TEMPLATES = INDUSTRY_TEMPLATES;
1488
+ exports.LOCAL_GEO_DB = LOCAL_GEO_DB;
1489
+ exports.LlmMapCopilot = LlmMapCopilot;
1490
+ exports.LlmNlpg = LlmNlpg;
1491
+ exports.MapCopilot = MapCopilot;
1492
+ exports.PLACE_COORDINATES = PLACE_COORDINATES;
243
1493
  exports.adjustBrightness = adjustBrightness;
244
1494
  exports.analyzePerformance = analyzePerformance;
245
1495
  exports.analyzeTiles = analyzeTiles;
1496
+ exports.batchGeocode = batchGeocode;
1497
+ exports.classifyIntent = classifyIntent;
1498
+ exports.createDeepSeekClient = createDeepSeekClient;
1499
+ exports.detectCRS = detectCRS;
1500
+ exports.detectField = detectField;
1501
+ exports.detectHeaders = detectHeaders;
246
1502
  exports.detectMemoryLeak = detectMemoryLeak;
1503
+ exports.detectSpatial = detectSpatial;
1504
+ exports.detectTable = detectTable;
1505
+ exports.detectValue = detectValue;
247
1506
  exports.diagnose = diagnose;
248
1507
  exports.extractDominantColor = extractDominantColor;
1508
+ exports.extractParams = extractParams;
1509
+ exports.gcj02ToWgs84 = gcj02ToWgs84;
249
1510
  exports.generateBrandStyle = generateBrandStyle;
250
1511
  exports.generateCarrierStyle = generateCarrierStyle;
1512
+ exports.generateCode = generateCode;
1513
+ exports.generateFromQuery = generateFromQuery;
251
1514
  exports.generateIndustryStyle = generateIndustryStyle;
1515
+ exports.generatePostGISQuery = generatePostGISQuery;
1516
+ exports.geocode = geocode;
1517
+ exports.hasLocalGeo = hasLocalGeo;
1518
+ exports.importToGeoJSON = importToGeoJSON;
1519
+ exports.isInChina = isInChina;
1520
+ exports.isValidAddress = isValidAddress;
1521
+ exports.nlpgQuery = nlpgQuery;
1522
+ exports.parameterize = parameterize;
1523
+ exports.parseAddress = parseAddress;
252
1524
  exports.suggestOptimizations = suggestOptimizations;
253
1525
  exports.toggleMode = toggleMode;
1526
+ exports.validateSql = validateSql;
1527
+ exports.wgs84ToGcj02 = wgs84ToGcj02;
254
1528
  //# sourceMappingURL=index.cjs.map
255
1529
  //# sourceMappingURL=index.cjs.map