@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
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/llm/deepseek.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * DeepSeek LLM 客户端(PRD phase-0 §5.5 / §5.7 的 v1 后端能力)\n *\n * DeepSeek API 与 OpenAI Chat Completions 协议兼容:\n * base_url: https://api.deepseek.com\n * 模型: deepseek-chat(通用对话)/ deepseek-reasoner(深度推理)\n * 鉴权: Authorization: Bearer <API_KEY>\n *\n * 设计:\n * - 依赖注入的 fetch(浏览器/Node 均可运行,Node 18+ 原生 fetch)\n * - 自动重试(指数退避)\n * - 失败降级(可传入 fallback,规则引擎兜底)\n * - 支持流式(onChunk 回调)与 JSON 输出\n */\n\nexport interface DeepSeekConfig {\n /** API Key */\n apiKey: string;\n /** 基础地址,默认 https://api.deepseek.com */\n baseUrl?: string;\n /** 模型,默认 deepseek-chat */\n model?: 'deepseek-chat' | 'deepseek-reasoner' | (string & {});\n /** 温度 0-2 */\n temperature?: number;\n /** 最大 token */\n maxTokens?: number;\n /** 超时(ms),默认 30s */\n timeoutMs?: number;\n /** 重试次数,默认 2 */\n retries?: number;\n /** 注入 fetch(测试/Node 环境),默认 globalThis.fetch */\n fetchImpl?: typeof fetch;\n}\n\nexport interface ChatMessage {\n role: 'system' | 'user' | 'assistant';\n content: string;\n}\n\nexport interface ChatOptions {\n /** 是否启用 JSON 模式(要求模型返回纯 JSON) */\n json?: boolean;\n /** 流式回调 */\n onChunk?: (delta: string) => void;\n}\n\nexport interface ChatResult {\n /** 完整回复文本 */\n content: string;\n /** 消耗 token */\n usage?: { promptTokens: number; completionTokens: number; totalTokens: number };\n /** 使用的模型 */\n model: string;\n}\n\n/** 已解析的 JSON 回复 */\nexport interface JsonChatResult<T> {\n /** 解析出的 JSON 对象 */\n data: T;\n /** 原始文本 */\n raw: string;\n model: string;\n}\n\nconst DEFAULT_BASE_URL = 'https://api.deepseek.com';\nconst DEFAULT_MODEL = 'deepseek-chat';\n\n/** 把中文异常文案简化为错误码 */\nfunction normalizeError(body: string): Error {\n const err = new Error(body || 'DeepSeek API 请求失败');\n err.name = 'DeepSeekError';\n return err;\n}\n\nexport class DeepSeekClient {\n private config: Required<Omit<DeepSeekConfig, 'fetchImpl'>> & { fetchImpl: typeof fetch };\n private lastRequestId = 0;\n\n constructor(config: DeepSeekConfig) {\n if (!config.apiKey) throw new Error('DeepSeekClient: apiKey 不能为空');\n this.config = {\n apiKey: config.apiKey,\n baseUrl: (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, ''),\n model: config.model ?? DEFAULT_MODEL,\n temperature: config.temperature ?? 0.3,\n maxTokens: config.maxTokens ?? 2048,\n timeoutMs: config.timeoutMs ?? 30000,\n retries: config.retries ?? 2,\n fetchImpl: config.fetchImpl ?? ((globalThis as { fetch?: typeof fetch }).fetch ?? fetch),\n };\n }\n\n /** 发起一次聊天补全 */\n async chat(messages: ChatMessage[], opts: ChatOptions = {}): Promise<ChatResult> {\n return this.requestWithRetry(messages, opts);\n }\n\n /** 聊天 + JSON 解析 */\n async chatJson<T>(messages: ChatMessage[]): Promise<JsonChatResult<T>> {\n const result = await this.chat(messages, { json: true });\n const raw = result.content.trim();\n // 去除可能的 ```json 围栏\n const cleaned = raw.replace(/^```(?:json)?\\s*/i, '').replace(/\\s*```$/, '');\n let data: T;\n try {\n data = JSON.parse(cleaned) as T;\n } catch (e) {\n // 尝试从文本中提取首个 JSON 对象\n const match = cleaned.match(/\\{[\\s\\S]*\\}|\\[[\\s\\S]*\\]/);\n if (match) {\n data = JSON.parse(match[0]) as T;\n } else {\n throw new Error(`DeepSeek 返回了非 JSON 内容: ${raw.slice(0, 200)}`);\n }\n }\n return { data, raw, model: result.model };\n }\n\n private async requestWithRetry(messages: ChatMessage[], opts: ChatOptions): Promise<ChatResult> {\n let lastError: Error | null = null;\n for (let attempt = 0; attempt <= this.config.retries; attempt++) {\n try {\n return await this.requestOnce(messages, opts);\n } catch (e) {\n lastError = e as Error;\n // 仅对可重试错误(网络/429/5xx)重试\n const retryable = !(e instanceof Error) || !e.name.includes('DeepSeek') || this.isRetryable(e as Error);\n if (!retryable && !this.isRetryable(e as Error)) break;\n if (attempt < this.config.retries) {\n await this.sleep(Math.min(1000 * 2 ** attempt, 8000));\n }\n }\n }\n throw lastError ?? new Error('DeepSeek 请求失败');\n }\n\n private isRetryable(err: Error): boolean {\n const msg = err.message ?? '';\n return /429|500|502|503|504|timeout|network|ECONN|abort|fetch failed/i.test(msg);\n }\n\n private async requestOnce(messages: ChatMessage[], opts: ChatOptions): Promise<ChatResult> {\n const { fetchImpl } = this.config;\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);\n\n const body: Record<string, unknown> = {\n model: this.config.model,\n messages,\n temperature: this.config.temperature,\n max_tokens: this.config.maxTokens,\n stream: Boolean(opts.onChunk),\n };\n if (opts.json) {\n body.response_format = { type: 'json_object' };\n }\n\n try {\n const res = await fetchImpl(`${this.config.baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.config.apiKey}`,\n },\n body: JSON.stringify(body),\n signal: controller.signal,\n });\n\n if (!res.ok) {\n const text = await res.text().catch(() => '');\n throw normalizeError(`DeepSeek HTTP ${res.status}: ${text}`);\n }\n\n // 流式\n if (opts.onChunk && res.body) {\n return this.parseStream(res.body, opts.onChunk);\n }\n\n // 非流式\n const json = (await res.json()) as {\n choices: Array<{ message: { content: string } }>;\n usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number };\n model?: string;\n };\n const content = json.choices?.[0]?.message?.content ?? '';\n return {\n content,\n model: json.model ?? this.config.model,\n usage: json.usage\n ? { promptTokens: json.usage.prompt_tokens, completionTokens: json.usage.completion_tokens, totalTokens: json.usage.total_tokens }\n : undefined,\n };\n } finally {\n clearTimeout(timer);\n }\n }\n\n /** 解析 SSE 流式响应 */\n private async parseStream(\n body: ReadableStream<Uint8Array>,\n onChunk: (delta: string) => void\n ): Promise<ChatResult> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let content = '';\n let buffer = '';\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split('\\n');\n buffer = lines.pop() ?? '';\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed.startsWith('data:')) continue;\n const data = trimmed.slice(5).trim();\n if (data === '[DONE]') continue;\n try {\n const json = JSON.parse(data) as { choices?: Array<{ delta?: { content?: string } }> };\n const delta = json.choices?.[0]?.delta?.content ?? '';\n if (delta) {\n content += delta;\n onChunk(delta);\n }\n } catch {\n // 忽略无法解析的行\n }\n }\n }\n return { content, model: this.config.model };\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms));\n }\n}\n\n/** 便捷工厂:创建默认配置的客户端 */\nexport function createDeepSeekClient(config: DeepSeekConfig): DeepSeekClient {\n return new DeepSeekClient(config);\n}\n"]}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * DeepSeek LLM 客户端(PRD phase-0 §5.5 / §5.7 的 v1 后端能力)
3
+ *
4
+ * DeepSeek API 与 OpenAI Chat Completions 协议兼容:
5
+ * base_url: https://api.deepseek.com
6
+ * 模型: deepseek-chat(通用对话)/ deepseek-reasoner(深度推理)
7
+ * 鉴权: Authorization: Bearer <API_KEY>
8
+ *
9
+ * 设计:
10
+ * - 依赖注入的 fetch(浏览器/Node 均可运行,Node 18+ 原生 fetch)
11
+ * - 自动重试(指数退避)
12
+ * - 失败降级(可传入 fallback,规则引擎兜底)
13
+ * - 支持流式(onChunk 回调)与 JSON 输出
14
+ */
15
+ interface DeepSeekConfig {
16
+ /** API Key */
17
+ apiKey: string;
18
+ /** 基础地址,默认 https://api.deepseek.com */
19
+ baseUrl?: string;
20
+ /** 模型,默认 deepseek-chat */
21
+ model?: 'deepseek-chat' | 'deepseek-reasoner' | (string & {});
22
+ /** 温度 0-2 */
23
+ temperature?: number;
24
+ /** 最大 token */
25
+ maxTokens?: number;
26
+ /** 超时(ms),默认 30s */
27
+ timeoutMs?: number;
28
+ /** 重试次数,默认 2 */
29
+ retries?: number;
30
+ /** 注入 fetch(测试/Node 环境),默认 globalThis.fetch */
31
+ fetchImpl?: typeof fetch;
32
+ }
33
+ interface ChatMessage {
34
+ role: 'system' | 'user' | 'assistant';
35
+ content: string;
36
+ }
37
+ interface ChatOptions {
38
+ /** 是否启用 JSON 模式(要求模型返回纯 JSON) */
39
+ json?: boolean;
40
+ /** 流式回调 */
41
+ onChunk?: (delta: string) => void;
42
+ }
43
+ interface ChatResult {
44
+ /** 完整回复文本 */
45
+ content: string;
46
+ /** 消耗 token */
47
+ usage?: {
48
+ promptTokens: number;
49
+ completionTokens: number;
50
+ totalTokens: number;
51
+ };
52
+ /** 使用的模型 */
53
+ model: string;
54
+ }
55
+ /** 已解析的 JSON 回复 */
56
+ interface JsonChatResult<T> {
57
+ /** 解析出的 JSON 对象 */
58
+ data: T;
59
+ /** 原始文本 */
60
+ raw: string;
61
+ model: string;
62
+ }
63
+ declare class DeepSeekClient {
64
+ private config;
65
+ private lastRequestId;
66
+ constructor(config: DeepSeekConfig);
67
+ /** 发起一次聊天补全 */
68
+ chat(messages: ChatMessage[], opts?: ChatOptions): Promise<ChatResult>;
69
+ /** 聊天 + JSON 解析 */
70
+ chatJson<T>(messages: ChatMessage[]): Promise<JsonChatResult<T>>;
71
+ private requestWithRetry;
72
+ private isRetryable;
73
+ private requestOnce;
74
+ /** 解析 SSE 流式响应 */
75
+ private parseStream;
76
+ private sleep;
77
+ }
78
+ /** 便捷工厂:创建默认配置的客户端 */
79
+ declare function createDeepSeekClient(config: DeepSeekConfig): DeepSeekClient;
80
+
81
+ export { type ChatMessage, type ChatOptions, type ChatResult, DeepSeekClient, type DeepSeekConfig, type JsonChatResult, createDeepSeekClient };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * DeepSeek LLM 客户端(PRD phase-0 §5.5 / §5.7 的 v1 后端能力)
3
+ *
4
+ * DeepSeek API 与 OpenAI Chat Completions 协议兼容:
5
+ * base_url: https://api.deepseek.com
6
+ * 模型: deepseek-chat(通用对话)/ deepseek-reasoner(深度推理)
7
+ * 鉴权: Authorization: Bearer <API_KEY>
8
+ *
9
+ * 设计:
10
+ * - 依赖注入的 fetch(浏览器/Node 均可运行,Node 18+ 原生 fetch)
11
+ * - 自动重试(指数退避)
12
+ * - 失败降级(可传入 fallback,规则引擎兜底)
13
+ * - 支持流式(onChunk 回调)与 JSON 输出
14
+ */
15
+ interface DeepSeekConfig {
16
+ /** API Key */
17
+ apiKey: string;
18
+ /** 基础地址,默认 https://api.deepseek.com */
19
+ baseUrl?: string;
20
+ /** 模型,默认 deepseek-chat */
21
+ model?: 'deepseek-chat' | 'deepseek-reasoner' | (string & {});
22
+ /** 温度 0-2 */
23
+ temperature?: number;
24
+ /** 最大 token */
25
+ maxTokens?: number;
26
+ /** 超时(ms),默认 30s */
27
+ timeoutMs?: number;
28
+ /** 重试次数,默认 2 */
29
+ retries?: number;
30
+ /** 注入 fetch(测试/Node 环境),默认 globalThis.fetch */
31
+ fetchImpl?: typeof fetch;
32
+ }
33
+ interface ChatMessage {
34
+ role: 'system' | 'user' | 'assistant';
35
+ content: string;
36
+ }
37
+ interface ChatOptions {
38
+ /** 是否启用 JSON 模式(要求模型返回纯 JSON) */
39
+ json?: boolean;
40
+ /** 流式回调 */
41
+ onChunk?: (delta: string) => void;
42
+ }
43
+ interface ChatResult {
44
+ /** 完整回复文本 */
45
+ content: string;
46
+ /** 消耗 token */
47
+ usage?: {
48
+ promptTokens: number;
49
+ completionTokens: number;
50
+ totalTokens: number;
51
+ };
52
+ /** 使用的模型 */
53
+ model: string;
54
+ }
55
+ /** 已解析的 JSON 回复 */
56
+ interface JsonChatResult<T> {
57
+ /** 解析出的 JSON 对象 */
58
+ data: T;
59
+ /** 原始文本 */
60
+ raw: string;
61
+ model: string;
62
+ }
63
+ declare class DeepSeekClient {
64
+ private config;
65
+ private lastRequestId;
66
+ constructor(config: DeepSeekConfig);
67
+ /** 发起一次聊天补全 */
68
+ chat(messages: ChatMessage[], opts?: ChatOptions): Promise<ChatResult>;
69
+ /** 聊天 + JSON 解析 */
70
+ chatJson<T>(messages: ChatMessage[]): Promise<JsonChatResult<T>>;
71
+ private requestWithRetry;
72
+ private isRetryable;
73
+ private requestOnce;
74
+ /** 解析 SSE 流式响应 */
75
+ private parseStream;
76
+ private sleep;
77
+ }
78
+ /** 便捷工厂:创建默认配置的客户端 */
79
+ declare function createDeepSeekClient(config: DeepSeekConfig): DeepSeekClient;
80
+
81
+ export { type ChatMessage, type ChatOptions, type ChatResult, DeepSeekClient, type DeepSeekConfig, type JsonChatResult, createDeepSeekClient };
@@ -0,0 +1,3 @@
1
+ export { DeepSeekClient, createDeepSeekClient } from '../chunk-ZI6ZF26Q.js';
2
+ //# sourceMappingURL=index.js.map
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -0,0 +1,387 @@
1
+ 'use strict';
2
+
3
+ // src/nlpg/sqlGenerator.ts
4
+ var FIELD_ALIASES = [
5
+ { field: "material", re: /材质|材料|铸铁|钢管|PE|PVC|球墨|HDPE|铜管/ },
6
+ { field: "pressure", re: /压力|水压|气压|MPa/ },
7
+ { field: "diameter", re: /管径|直径|口径|DN/ },
8
+ { field: "status", re: /状态|运行|停运|故障|检修/ },
9
+ { field: "install_date", re: /安装日期|投运|建成|敷设/ },
10
+ { field: "age", re: /年限|使用.{0,2}年|超过.{0,2}年|.{0,2}年以上|年代/ },
11
+ { field: "voltage", re: /电压|kV|kv|伏/ },
12
+ { field: "load_rate", re: /负载率|负荷率|利用率|负载|负荷/ },
13
+ { field: "fault_rate", re: /故障率/ },
14
+ { field: "flow_rate", re: /流量|流速/ },
15
+ { field: "water_level", re: /水位/ },
16
+ { field: "storage_rate", re: /蓄水率|库容/ },
17
+ { field: "rsrp", re: /RSRP|信号强度|信号/ }
18
+ ];
19
+ var TABLE_ALIASES = [
20
+ { table: "pipelines", re: /管段|管线|管道|管网|燃气管|供水管|排水管|供热管|电力管|通信管/ },
21
+ { table: "nodes", re: /节点|阀门|泵站|表|井|闸/ },
22
+ { table: "users", re: /用户|居民|小区|住户|建筑/ },
23
+ { table: "schools", re: /学校|小学|中学|大学|幼儿园/ },
24
+ { table: "hospitals", re: /医院|卫生院|诊所/ },
25
+ { table: "substations", re: /变电站|配变|台区|电站/ },
26
+ { table: "base_stations", re: /基站|宏站|微站|室分/ },
27
+ { table: "rivers", re: /河流|水系|河段|支流/ },
28
+ { table: "reservoirs", re: /水库|大坝/ },
29
+ { table: "alarms", re: /报警|告警|警报/ },
30
+ { table: "pois", re: /POI|兴趣点|场所|设施/ }
31
+ ];
32
+ var VALUE_DICT = [
33
+ { field: "material", value: "cast_iron", re: /铸铁/ },
34
+ { field: "material", value: "ductile_iron", re: /球墨/ },
35
+ { field: "material", value: "steel", re: /钢(?!筋)/ },
36
+ { field: "material", value: "pe", re: /PE/ },
37
+ { field: "material", value: "pvc", re: /PVC/ },
38
+ { field: "material", value: "hdpe", re: /HDPE/ },
39
+ { field: "status", value: "normal", re: /正常/ },
40
+ { field: "status", value: "fault", re: /故障/ },
41
+ { field: "status", value: "maintenance", re: /检修|维修/ },
42
+ { field: "status", value: "aging", re: /老化/ }
43
+ ];
44
+ function detectOperator(text) {
45
+ if (/超过|高于|大于|超出|以上|不小于/.test(text)) return text.includes("\u4EE5\u4E0A") || text.includes("\u4E0D\u5C0F\u4E8E") ? ">=" : ">";
46
+ if (/低于|小于|不到|以下|不足/.test(text)) return text.includes("\u4EE5\u4E0B") ? "<=" : "<";
47
+ if (/等于|正好|恰好|为\s*\d/.test(text)) return "=";
48
+ if (/不是|非|不等于/.test(text)) return "!=";
49
+ if (/包含|含有|含/.test(text)) return "LIKE";
50
+ return "=";
51
+ }
52
+ function detectTable(text) {
53
+ for (const t of TABLE_ALIASES) {
54
+ if (t.re.test(text)) return t.table;
55
+ }
56
+ return "pois";
57
+ }
58
+ function detectField(text) {
59
+ for (const f of FIELD_ALIASES) {
60
+ if (f.re.test(text)) return f.field;
61
+ }
62
+ return null;
63
+ }
64
+ function detectValue(text, field) {
65
+ for (const v of VALUE_DICT) {
66
+ if (v.field === field && v.re.test(text)) return v.value;
67
+ }
68
+ const numMatch = text.match(/(\d+(?:\.\d+)?)\s*(MPa|mpa|kV|kv|米|m|公里|km|%|%|方|立方米)?/);
69
+ if (numMatch) {
70
+ let value = parseFloat(numMatch[1]);
71
+ const unit = numMatch[2] ?? "";
72
+ if (/公里|km/.test(unit)) value *= 1e3;
73
+ if (/%|%/.test(unit)) value /= 100;
74
+ return value;
75
+ }
76
+ const dateMatch = text.match(/(\d{4}-\d{2}-\d{2}|\d{4}年\d{1,2}月)/);
77
+ if (dateMatch) return dateMatch[1];
78
+ return null;
79
+ }
80
+ function detectSpatial(text, geometryColumn = "geom") {
81
+ const nearby = text.match(/(\d+(?:\.\d+)?)\s*(米|m|公里|km|千米)\s*(?:内|以内|范围内|附近|周边)/);
82
+ if (nearby) {
83
+ let radius = parseFloat(nearby[1]);
84
+ if (/公里|km|千米/.test(nearby[2])) radius *= 1e3;
85
+ return { relation: "dwithin", radius, geometryColumn };
86
+ }
87
+ const point = detectReferencePoint(text);
88
+ if (/缓冲区|缓冲|范围内|区域.{0,2}内/.test(text)) {
89
+ return { relation: "buffer", point, geometryColumn };
90
+ }
91
+ if (/包含|覆盖|涵盖|包围|圈住/.test(text)) {
92
+ return { relation: "contains", point, geometryColumn };
93
+ }
94
+ if (/在.{0,2}内|内部|以内|之中/.test(text)) {
95
+ return { relation: "within", point, geometryColumn };
96
+ }
97
+ if (/相交|叠加|重叠|交叉/.test(text)) {
98
+ return { relation: "intersects", point, geometryColumn };
99
+ }
100
+ return null;
101
+ }
102
+ function detectReferencePoint(text) {
103
+ const m = text.match(/([1-2]?\d{2,3}(?:\.\d+)?)\s*[,,]\s*(\d{1,3}(?:\.\d+)?)/);
104
+ if (m) {
105
+ const lng = parseFloat(m[1]);
106
+ const lat = parseFloat(m[2]);
107
+ if (lng >= -180 && lng <= 180 && lat >= -90 && lat <= 90) return [lng, lat];
108
+ }
109
+ return void 0;
110
+ }
111
+ function quoteValue(v) {
112
+ return typeof v === "string" ? `'${v.replace(/'/g, "''")}'` : String(v);
113
+ }
114
+ function buildWhere(conditions, spatial) {
115
+ const parts = [];
116
+ for (const c of conditions) {
117
+ parts.push(`${c.field} ${c.operator} ${quoteValue(c.value)}`);
118
+ }
119
+ if (spatial) {
120
+ const col = spatial.geometryColumn;
121
+ 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;
122
+ switch (spatial.relation) {
123
+ case "dwithin":
124
+ if (spatial.point && spatial.radius !== void 0) {
125
+ parts.push(`ST_DWithin(${col}, ST_SetSRID(ST_MakePoint(${spatial.point[0]}, ${spatial.point[1]}), 4326), ${spatial.radius})`);
126
+ }
127
+ break;
128
+ case "buffer":
129
+ if (refGeom) parts.push(`ST_Intersects(${col}, ${refGeom})`);
130
+ break;
131
+ case "within":
132
+ if (refGeom) parts.push(`ST_Within(${col}, ${refGeom})`);
133
+ break;
134
+ case "contains":
135
+ if (refGeom) parts.push(`ST_Contains(${refGeom}, ${col})`);
136
+ break;
137
+ case "intersects":
138
+ if (refGeom) parts.push(`ST_Intersects(${col}, ${refGeom})`);
139
+ break;
140
+ }
141
+ }
142
+ return parts.length > 0 ? `WHERE ${parts.join(" AND ")}` : "";
143
+ }
144
+ function generatePostGISQuery(text, opts = {}) {
145
+ const center = opts.center ?? [114.305, 30.593];
146
+ const geometryColumn = opts.geometryColumn ?? "geom";
147
+ const table = detectTable(text);
148
+ const spatial = detectSpatial(text, geometryColumn);
149
+ if (spatial && !spatial.point) {
150
+ spatial.point = center;
151
+ }
152
+ if (spatial && spatial.relation !== "dwithin" && spatial.radius === void 0) {
153
+ spatial.radius = 1e3;
154
+ }
155
+ const conditions = [];
156
+ const field = detectField(text);
157
+ if (field) {
158
+ const value = detectValue(text, field);
159
+ if (value !== null) {
160
+ conditions.push({ field, operator: detectOperator(text), value });
161
+ }
162
+ }
163
+ let intent;
164
+ if (spatial && conditions.length > 0) intent = "mixed";
165
+ else if (spatial) intent = spatial.relation === "dwithin" ? "spatial_nearby" : "spatial_within";
166
+ else if (conditions.length > 0) intent = "attribute_filter";
167
+ else intent = "unknown";
168
+ const where = buildWhere(conditions, spatial);
169
+ const sql = `SELECT * FROM ${table} ${where}`.trim();
170
+ let confidence = 0.3;
171
+ if (conditions.length > 0) confidence += 0.3;
172
+ if (spatial) confidence += 0.3;
173
+ if (table !== "pois") confidence += 0.1;
174
+ confidence = Math.min(0.95, confidence);
175
+ return { intent, table, conditions, spatial, sql, confidence };
176
+ }
177
+
178
+ // src/nlpg/sqlValidator.ts
179
+ var DANGEROUS_KEYWORDS = [
180
+ "DROP",
181
+ "DELETE",
182
+ "UPDATE",
183
+ "INSERT",
184
+ "ALTER",
185
+ "TRUNCATE",
186
+ "GRANT",
187
+ "REVOKE",
188
+ "MERGE",
189
+ "CREATE",
190
+ "REPLACE",
191
+ "EXEC",
192
+ "EXECUTE",
193
+ "CALL",
194
+ "COPY",
195
+ "LOAD",
196
+ "INTO",
197
+ "SET",
198
+ "UNION",
199
+ "ATTACH",
200
+ "DETACH",
201
+ "PRAGMA",
202
+ "VACUUM",
203
+ "REINDEX"
204
+ ];
205
+ var INJECTION_PATTERNS = [
206
+ /--/,
207
+ // SQL 注释
208
+ /\/\*/,
209
+ // 块注释
210
+ /;\s*(DROP|DELETE|UPDATE|INSERT|ALTER|TRUNCATE)/i,
211
+ // 多语句注入
212
+ /'\s*OR\s*'/i,
213
+ // ' OR ' 恒真注入(含 ' OR '1'='1')
214
+ /"\s*OR\s*"/i,
215
+ // " OR " 恒真注入
216
+ /\bOR\s+\d+\s*=\s*\d+\b/i,
217
+ // OR 1=1 恒真
218
+ /\bOR\s+'\w*'\s*=\s*'\w*'/i,
219
+ // OR 'a'='a' 恒真
220
+ /UNION\s+SELECT/i
221
+ ];
222
+ var DEFAULT_ALLOWED_TABLES = [
223
+ "pipelines",
224
+ "nodes",
225
+ "users",
226
+ "schools",
227
+ "hospitals",
228
+ "substations",
229
+ "base_stations",
230
+ "rivers",
231
+ "reservoirs",
232
+ "alarms",
233
+ "pois"
234
+ ];
235
+ var ALLOWED_SPATIAL_FUNCTIONS = [
236
+ "ST_DWithin",
237
+ "ST_Within",
238
+ "ST_Intersects",
239
+ "ST_Contains",
240
+ "ST_Buffer",
241
+ "ST_MakePoint",
242
+ "ST_SetSRID",
243
+ "ST_Distance",
244
+ "ST_AsGeoJSON",
245
+ "ST_Transform"
246
+ ];
247
+ function validateSql(sql, allowedTables = DEFAULT_ALLOWED_TABLES) {
248
+ const issues = [];
249
+ const upper = sql.toUpperCase().trim();
250
+ if (!sql.trim()) {
251
+ issues.push({ severity: "error", rule: "non_empty", message: "SQL \u4E0D\u80FD\u4E3A\u7A7A" });
252
+ return { valid: false, issues };
253
+ }
254
+ if (!upper.startsWith("SELECT")) {
255
+ issues.push({ severity: "error", rule: "read_only", message: "\u4EC5\u5141\u8BB8 SELECT \u53EA\u8BFB\u67E5\u8BE2" });
256
+ }
257
+ for (const kw of DANGEROUS_KEYWORDS) {
258
+ const re = new RegExp(`\\b${kw}\\b`, "i");
259
+ if (re.test(sql) && !(kw === "SET" && /ST_SetSRID/i.test(sql))) {
260
+ issues.push({ severity: "error", rule: "dangerous_keyword", message: `\u68C0\u6D4B\u5230\u5371\u9669\u5173\u952E\u5B57 ${kw}` });
261
+ }
262
+ }
263
+ for (const p of INJECTION_PATTERNS) {
264
+ if (p.test(sql)) {
265
+ issues.push({ severity: "error", rule: "injection", message: "\u68C0\u6D4B\u5230 SQL \u6CE8\u5165\u7279\u5F81" });
266
+ }
267
+ }
268
+ const fromMatch = upper.match(/FROM\s+([A-Za-z_][\w]*)/);
269
+ if (fromMatch) {
270
+ const table = fromMatch[1].toLowerCase();
271
+ if (!allowedTables.includes(table)) {
272
+ issues.push({ severity: "error", rule: "table_whitelist", message: `\u8868 ${table} \u4E0D\u5728\u6388\u6743\u767D\u540D\u5355\u5185` });
273
+ }
274
+ }
275
+ const singleQuotes = (sql.match(/'/g) ?? []).length;
276
+ if (singleQuotes % 2 !== 0) {
277
+ issues.push({ severity: "error", rule: "syntax", message: "\u5355\u5F15\u53F7\u672A\u914D\u5BF9" });
278
+ }
279
+ const openParen = (sql.match(/\(/g) ?? []).length;
280
+ const closeParen = (sql.match(/\)/g) ?? []).length;
281
+ if (openParen !== closeParen) {
282
+ issues.push({ severity: "error", rule: "syntax", message: "\u62EC\u53F7\u672A\u914D\u5BF9" });
283
+ }
284
+ for (const fn of ["ST_"]) {
285
+ const fnMatch = sql.match(/ST_(\w+)/g) ?? [];
286
+ for (const m of fnMatch) {
287
+ if (!ALLOWED_SPATIAL_FUNCTIONS.includes(m)) {
288
+ issues.push({ severity: "error", rule: "spatial_whitelist", message: `\u7A7A\u95F4\u51FD\u6570 ${m} \u4E0D\u5728\u767D\u540D\u5355\u5185` });
289
+ }
290
+ }
291
+ }
292
+ return { valid: issues.every((i) => i.severity !== "error"), issues };
293
+ }
294
+ function parameterize(sql) {
295
+ const params = [];
296
+ const out = sql.replace(/'([^']*)'/g, (_m, v) => {
297
+ params.push(v);
298
+ return `$${params.length}`;
299
+ });
300
+ return { sql: out, params };
301
+ }
302
+
303
+ // src/nlpg/nlpg.ts
304
+ function nlpgQuery(text, opts = {}) {
305
+ const query = generatePostGISQuery(text, opts);
306
+ const validation = validateSql(query.sql);
307
+ const valid = validation.valid;
308
+ const parameterized = valid ? parameterize(query.sql) : void 0;
309
+ return { query, valid, validation, parameterized };
310
+ }
311
+
312
+ // src/nlpg/llmNlpg.ts
313
+ var SYSTEM_PROMPT = `\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
314
+
315
+ \u89C4\u5219\uFF1A
316
+ 1. \u53EA\u8FD4\u56DE JSON\uFF1A{"sql":"SELECT ...","table":"...","intent":"..."}
317
+ 2. \u8868\u540D\u53EA\u80FD\u6765\u81EA\u4EE5\u4E0B\u767D\u540D\u5355\uFF1A${DEFAULT_ALLOWED_TABLES.join(", ")}
318
+ 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
319
+ 4. \u53EA\u751F\u6210 SELECT \u8BED\u53E5\uFF0C\u7981\u6B62 DROP/DELETE/UPDATE/INSERT/ALTER \u7B49\u5199\u64CD\u4F5C
320
+ 5. \u5B57\u6BB5\u540D\u4F7F\u7528 snake_case\uFF0C\u5750\u6807\u4F7F\u7528 WGS84\uFF08SRID 4326\uFF09
321
+ 6. \u4E0D\u8981\u8F93\u51FA JSON \u4EE5\u5916\u7684\u4EFB\u4F55\u6587\u5B57\u3002`;
322
+ var LlmNlpg = class {
323
+ client;
324
+ enabled;
325
+ allowedTables;
326
+ constructor(config) {
327
+ this.client = config.client;
328
+ this.enabled = config.enabled ?? true;
329
+ this.allowedTables = config.allowedTables ?? DEFAULT_ALLOWED_TABLES;
330
+ }
331
+ /**
332
+ * 自然语言查询。
333
+ * LLM 优先,失败或校验不过时降级到规则引擎。
334
+ */
335
+ async query(text) {
336
+ if (this.enabled) {
337
+ try {
338
+ const llmResult = await this.queryWithLlm(text);
339
+ if (llmResult) return llmResult;
340
+ } catch {
341
+ }
342
+ }
343
+ return this.queryWithRules(text);
344
+ }
345
+ async queryWithLlm(text) {
346
+ const messages = [
347
+ { role: "system", content: SYSTEM_PROMPT },
348
+ { role: "user", content: text }
349
+ ];
350
+ const { data } = await this.client.chatJson(messages);
351
+ const sql = (data.sql ?? "").trim();
352
+ if (!sql) return null;
353
+ const validation = validateSql(sql, this.allowedTables);
354
+ if (!validation.valid) return null;
355
+ const query = {
356
+ intent: "mixed",
357
+ table: data.table ?? "pois",
358
+ conditions: [],
359
+ spatial: null,
360
+ sql,
361
+ confidence: 0.9
362
+ };
363
+ return { query, valid: true, parameterized: parameterize(sql) };
364
+ }
365
+ queryWithRules(text) {
366
+ const query = generatePostGISQuery(text);
367
+ const validation = validateSql(query.sql, this.allowedTables);
368
+ return {
369
+ query,
370
+ valid: validation.valid,
371
+ parameterized: validation.valid ? parameterize(query.sql) : void 0
372
+ };
373
+ }
374
+ };
375
+
376
+ exports.DEFAULT_ALLOWED_TABLES = DEFAULT_ALLOWED_TABLES;
377
+ exports.LlmNlpg = LlmNlpg;
378
+ exports.detectField = detectField;
379
+ exports.detectSpatial = detectSpatial;
380
+ exports.detectTable = detectTable;
381
+ exports.detectValue = detectValue;
382
+ exports.generatePostGISQuery = generatePostGISQuery;
383
+ exports.nlpgQuery = nlpgQuery;
384
+ exports.parameterize = parameterize;
385
+ exports.validateSql = validateSql;
386
+ //# sourceMappingURL=index.cjs.map
387
+ //# sourceMappingURL=index.cjs.map