@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/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