@caoguo/maplibre-ai 0.0.4 → 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.
@@ -0,0 +1,376 @@
1
+ // src/nlpg/sqlGenerator.ts
2
+ var FIELD_ALIASES = [
3
+ { field: "material", re: /材质|材料|铸铁|钢管|PE|PVC|球墨|HDPE|铜管/ },
4
+ { field: "pressure", re: /压力|水压|气压|MPa/ },
5
+ { field: "diameter", re: /管径|直径|口径|DN/ },
6
+ { field: "status", re: /状态|运行|停运|故障|检修/ },
7
+ { field: "install_date", re: /安装日期|投运|建成|敷设/ },
8
+ { field: "age", re: /年限|使用.{0,2}年|超过.{0,2}年|.{0,2}年以上|年代/ },
9
+ { field: "voltage", re: /电压|kV|kv|伏/ },
10
+ { field: "load_rate", re: /负载率|负荷率|利用率|负载|负荷/ },
11
+ { field: "fault_rate", re: /故障率/ },
12
+ { field: "flow_rate", re: /流量|流速/ },
13
+ { field: "water_level", re: /水位/ },
14
+ { field: "storage_rate", re: /蓄水率|库容/ },
15
+ { field: "rsrp", re: /RSRP|信号强度|信号/ }
16
+ ];
17
+ var TABLE_ALIASES = [
18
+ { table: "pipelines", re: /管段|管线|管道|管网|燃气管|供水管|排水管|供热管|电力管|通信管/ },
19
+ { table: "nodes", re: /节点|阀门|泵站|表|井|闸/ },
20
+ { table: "users", re: /用户|居民|小区|住户|建筑/ },
21
+ { table: "schools", re: /学校|小学|中学|大学|幼儿园/ },
22
+ { table: "hospitals", re: /医院|卫生院|诊所/ },
23
+ { table: "substations", re: /变电站|配变|台区|电站/ },
24
+ { table: "base_stations", re: /基站|宏站|微站|室分/ },
25
+ { table: "rivers", re: /河流|水系|河段|支流/ },
26
+ { table: "reservoirs", re: /水库|大坝/ },
27
+ { table: "alarms", re: /报警|告警|警报/ },
28
+ { table: "pois", re: /POI|兴趣点|场所|设施/ }
29
+ ];
30
+ var VALUE_DICT = [
31
+ { field: "material", value: "cast_iron", re: /铸铁/ },
32
+ { field: "material", value: "ductile_iron", re: /球墨/ },
33
+ { field: "material", value: "steel", re: /钢(?!筋)/ },
34
+ { field: "material", value: "pe", re: /PE/ },
35
+ { field: "material", value: "pvc", re: /PVC/ },
36
+ { field: "material", value: "hdpe", re: /HDPE/ },
37
+ { field: "status", value: "normal", re: /正常/ },
38
+ { field: "status", value: "fault", re: /故障/ },
39
+ { field: "status", value: "maintenance", re: /检修|维修/ },
40
+ { field: "status", value: "aging", re: /老化/ }
41
+ ];
42
+ function detectOperator(text) {
43
+ if (/超过|高于|大于|超出|以上|不小于/.test(text)) return text.includes("\u4EE5\u4E0A") || text.includes("\u4E0D\u5C0F\u4E8E") ? ">=" : ">";
44
+ if (/低于|小于|不到|以下|不足/.test(text)) return text.includes("\u4EE5\u4E0B") ? "<=" : "<";
45
+ if (/等于|正好|恰好|为\s*\d/.test(text)) return "=";
46
+ if (/不是|非|不等于/.test(text)) return "!=";
47
+ if (/包含|含有|含/.test(text)) return "LIKE";
48
+ return "=";
49
+ }
50
+ function detectTable(text) {
51
+ for (const t of TABLE_ALIASES) {
52
+ if (t.re.test(text)) return t.table;
53
+ }
54
+ return "pois";
55
+ }
56
+ function detectField(text) {
57
+ for (const f of FIELD_ALIASES) {
58
+ if (f.re.test(text)) return f.field;
59
+ }
60
+ return null;
61
+ }
62
+ function detectValue(text, field) {
63
+ for (const v of VALUE_DICT) {
64
+ if (v.field === field && v.re.test(text)) return v.value;
65
+ }
66
+ const numMatch = text.match(/(\d+(?:\.\d+)?)\s*(MPa|mpa|kV|kv|米|m|公里|km|%|%|方|立方米)?/);
67
+ if (numMatch) {
68
+ let value = parseFloat(numMatch[1]);
69
+ const unit = numMatch[2] ?? "";
70
+ if (/公里|km/.test(unit)) value *= 1e3;
71
+ if (/%|%/.test(unit)) value /= 100;
72
+ return value;
73
+ }
74
+ const dateMatch = text.match(/(\d{4}-\d{2}-\d{2}|\d{4}年\d{1,2}月)/);
75
+ if (dateMatch) return dateMatch[1];
76
+ return null;
77
+ }
78
+ function detectSpatial(text, geometryColumn = "geom") {
79
+ const nearby = text.match(/(\d+(?:\.\d+)?)\s*(米|m|公里|km|千米)\s*(?:内|以内|范围内|附近|周边)/);
80
+ if (nearby) {
81
+ let radius = parseFloat(nearby[1]);
82
+ if (/公里|km|千米/.test(nearby[2])) radius *= 1e3;
83
+ return { relation: "dwithin", radius, geometryColumn };
84
+ }
85
+ const point = detectReferencePoint(text);
86
+ if (/缓冲区|缓冲|范围内|区域.{0,2}内/.test(text)) {
87
+ return { relation: "buffer", point, geometryColumn };
88
+ }
89
+ if (/包含|覆盖|涵盖|包围|圈住/.test(text)) {
90
+ return { relation: "contains", point, geometryColumn };
91
+ }
92
+ if (/在.{0,2}内|内部|以内|之中/.test(text)) {
93
+ return { relation: "within", point, geometryColumn };
94
+ }
95
+ if (/相交|叠加|重叠|交叉/.test(text)) {
96
+ return { relation: "intersects", point, geometryColumn };
97
+ }
98
+ return null;
99
+ }
100
+ function detectReferencePoint(text) {
101
+ const m = text.match(/([1-2]?\d{2,3}(?:\.\d+)?)\s*[,,]\s*(\d{1,3}(?:\.\d+)?)/);
102
+ if (m) {
103
+ const lng = parseFloat(m[1]);
104
+ const lat = parseFloat(m[2]);
105
+ if (lng >= -180 && lng <= 180 && lat >= -90 && lat <= 90) return [lng, lat];
106
+ }
107
+ return void 0;
108
+ }
109
+ function quoteValue(v) {
110
+ return typeof v === "string" ? `'${v.replace(/'/g, "''")}'` : String(v);
111
+ }
112
+ function buildWhere(conditions, spatial) {
113
+ const parts = [];
114
+ for (const c of conditions) {
115
+ parts.push(`${c.field} ${c.operator} ${quoteValue(c.value)}`);
116
+ }
117
+ if (spatial) {
118
+ const col = spatial.geometryColumn;
119
+ 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;
120
+ switch (spatial.relation) {
121
+ case "dwithin":
122
+ if (spatial.point && spatial.radius !== void 0) {
123
+ parts.push(`ST_DWithin(${col}, ST_SetSRID(ST_MakePoint(${spatial.point[0]}, ${spatial.point[1]}), 4326), ${spatial.radius})`);
124
+ }
125
+ break;
126
+ case "buffer":
127
+ if (refGeom) parts.push(`ST_Intersects(${col}, ${refGeom})`);
128
+ break;
129
+ case "within":
130
+ if (refGeom) parts.push(`ST_Within(${col}, ${refGeom})`);
131
+ break;
132
+ case "contains":
133
+ if (refGeom) parts.push(`ST_Contains(${refGeom}, ${col})`);
134
+ break;
135
+ case "intersects":
136
+ if (refGeom) parts.push(`ST_Intersects(${col}, ${refGeom})`);
137
+ break;
138
+ }
139
+ }
140
+ return parts.length > 0 ? `WHERE ${parts.join(" AND ")}` : "";
141
+ }
142
+ function generatePostGISQuery(text, opts = {}) {
143
+ const center = opts.center ?? [114.305, 30.593];
144
+ const geometryColumn = opts.geometryColumn ?? "geom";
145
+ const table = detectTable(text);
146
+ const spatial = detectSpatial(text, geometryColumn);
147
+ if (spatial && !spatial.point) {
148
+ spatial.point = center;
149
+ }
150
+ if (spatial && spatial.relation !== "dwithin" && spatial.radius === void 0) {
151
+ spatial.radius = 1e3;
152
+ }
153
+ const conditions = [];
154
+ const field = detectField(text);
155
+ if (field) {
156
+ const value = detectValue(text, field);
157
+ if (value !== null) {
158
+ conditions.push({ field, operator: detectOperator(text), value });
159
+ }
160
+ }
161
+ let intent;
162
+ if (spatial && conditions.length > 0) intent = "mixed";
163
+ else if (spatial) intent = spatial.relation === "dwithin" ? "spatial_nearby" : "spatial_within";
164
+ else if (conditions.length > 0) intent = "attribute_filter";
165
+ else intent = "unknown";
166
+ const where = buildWhere(conditions, spatial);
167
+ const sql = `SELECT * FROM ${table} ${where}`.trim();
168
+ let confidence = 0.3;
169
+ if (conditions.length > 0) confidence += 0.3;
170
+ if (spatial) confidence += 0.3;
171
+ if (table !== "pois") confidence += 0.1;
172
+ confidence = Math.min(0.95, confidence);
173
+ return { intent, table, conditions, spatial, sql, confidence };
174
+ }
175
+
176
+ // src/nlpg/sqlValidator.ts
177
+ var DANGEROUS_KEYWORDS = [
178
+ "DROP",
179
+ "DELETE",
180
+ "UPDATE",
181
+ "INSERT",
182
+ "ALTER",
183
+ "TRUNCATE",
184
+ "GRANT",
185
+ "REVOKE",
186
+ "MERGE",
187
+ "CREATE",
188
+ "REPLACE",
189
+ "EXEC",
190
+ "EXECUTE",
191
+ "CALL",
192
+ "COPY",
193
+ "LOAD",
194
+ "INTO",
195
+ "SET",
196
+ "UNION",
197
+ "ATTACH",
198
+ "DETACH",
199
+ "PRAGMA",
200
+ "VACUUM",
201
+ "REINDEX"
202
+ ];
203
+ var INJECTION_PATTERNS = [
204
+ /--/,
205
+ // SQL 注释
206
+ /\/\*/,
207
+ // 块注释
208
+ /;\s*(DROP|DELETE|UPDATE|INSERT|ALTER|TRUNCATE)/i,
209
+ // 多语句注入
210
+ /'\s*OR\s*'/i,
211
+ // ' OR ' 恒真注入(含 ' OR '1'='1')
212
+ /"\s*OR\s*"/i,
213
+ // " OR " 恒真注入
214
+ /\bOR\s+\d+\s*=\s*\d+\b/i,
215
+ // OR 1=1 恒真
216
+ /\bOR\s+'\w*'\s*=\s*'\w*'/i,
217
+ // OR 'a'='a' 恒真
218
+ /UNION\s+SELECT/i
219
+ ];
220
+ var DEFAULT_ALLOWED_TABLES = [
221
+ "pipelines",
222
+ "nodes",
223
+ "users",
224
+ "schools",
225
+ "hospitals",
226
+ "substations",
227
+ "base_stations",
228
+ "rivers",
229
+ "reservoirs",
230
+ "alarms",
231
+ "pois"
232
+ ];
233
+ var ALLOWED_SPATIAL_FUNCTIONS = [
234
+ "ST_DWithin",
235
+ "ST_Within",
236
+ "ST_Intersects",
237
+ "ST_Contains",
238
+ "ST_Buffer",
239
+ "ST_MakePoint",
240
+ "ST_SetSRID",
241
+ "ST_Distance",
242
+ "ST_AsGeoJSON",
243
+ "ST_Transform"
244
+ ];
245
+ function validateSql(sql, allowedTables = DEFAULT_ALLOWED_TABLES) {
246
+ const issues = [];
247
+ const upper = sql.toUpperCase().trim();
248
+ if (!sql.trim()) {
249
+ issues.push({ severity: "error", rule: "non_empty", message: "SQL \u4E0D\u80FD\u4E3A\u7A7A" });
250
+ return { valid: false, issues };
251
+ }
252
+ if (!upper.startsWith("SELECT")) {
253
+ issues.push({ severity: "error", rule: "read_only", message: "\u4EC5\u5141\u8BB8 SELECT \u53EA\u8BFB\u67E5\u8BE2" });
254
+ }
255
+ for (const kw of DANGEROUS_KEYWORDS) {
256
+ const re = new RegExp(`\\b${kw}\\b`, "i");
257
+ if (re.test(sql) && !(kw === "SET" && /ST_SetSRID/i.test(sql))) {
258
+ issues.push({ severity: "error", rule: "dangerous_keyword", message: `\u68C0\u6D4B\u5230\u5371\u9669\u5173\u952E\u5B57 ${kw}` });
259
+ }
260
+ }
261
+ for (const p of INJECTION_PATTERNS) {
262
+ if (p.test(sql)) {
263
+ issues.push({ severity: "error", rule: "injection", message: "\u68C0\u6D4B\u5230 SQL \u6CE8\u5165\u7279\u5F81" });
264
+ }
265
+ }
266
+ const fromMatch = upper.match(/FROM\s+([A-Za-z_][\w]*)/);
267
+ if (fromMatch) {
268
+ const table = fromMatch[1].toLowerCase();
269
+ if (!allowedTables.includes(table)) {
270
+ issues.push({ severity: "error", rule: "table_whitelist", message: `\u8868 ${table} \u4E0D\u5728\u6388\u6743\u767D\u540D\u5355\u5185` });
271
+ }
272
+ }
273
+ const singleQuotes = (sql.match(/'/g) ?? []).length;
274
+ if (singleQuotes % 2 !== 0) {
275
+ issues.push({ severity: "error", rule: "syntax", message: "\u5355\u5F15\u53F7\u672A\u914D\u5BF9" });
276
+ }
277
+ const openParen = (sql.match(/\(/g) ?? []).length;
278
+ const closeParen = (sql.match(/\)/g) ?? []).length;
279
+ if (openParen !== closeParen) {
280
+ issues.push({ severity: "error", rule: "syntax", message: "\u62EC\u53F7\u672A\u914D\u5BF9" });
281
+ }
282
+ for (const fn of ["ST_"]) {
283
+ const fnMatch = sql.match(/ST_(\w+)/g) ?? [];
284
+ for (const m of fnMatch) {
285
+ if (!ALLOWED_SPATIAL_FUNCTIONS.includes(m)) {
286
+ issues.push({ severity: "error", rule: "spatial_whitelist", message: `\u7A7A\u95F4\u51FD\u6570 ${m} \u4E0D\u5728\u767D\u540D\u5355\u5185` });
287
+ }
288
+ }
289
+ }
290
+ return { valid: issues.every((i) => i.severity !== "error"), issues };
291
+ }
292
+ function parameterize(sql) {
293
+ const params = [];
294
+ const out = sql.replace(/'([^']*)'/g, (_m, v) => {
295
+ params.push(v);
296
+ return `$${params.length}`;
297
+ });
298
+ return { sql: out, params };
299
+ }
300
+
301
+ // src/nlpg/nlpg.ts
302
+ function nlpgQuery(text, opts = {}) {
303
+ const query = generatePostGISQuery(text, opts);
304
+ const validation = validateSql(query.sql);
305
+ const valid = validation.valid;
306
+ const parameterized = valid ? parameterize(query.sql) : void 0;
307
+ return { query, valid, validation, parameterized };
308
+ }
309
+
310
+ // src/nlpg/llmNlpg.ts
311
+ 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
312
+
313
+ \u89C4\u5219\uFF1A
314
+ 1. \u53EA\u8FD4\u56DE JSON\uFF1A{"sql":"SELECT ...","table":"...","intent":"..."}
315
+ 2. \u8868\u540D\u53EA\u80FD\u6765\u81EA\u4EE5\u4E0B\u767D\u540D\u5355\uFF1A${DEFAULT_ALLOWED_TABLES.join(", ")}
316
+ 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
317
+ 4. \u53EA\u751F\u6210 SELECT \u8BED\u53E5\uFF0C\u7981\u6B62 DROP/DELETE/UPDATE/INSERT/ALTER \u7B49\u5199\u64CD\u4F5C
318
+ 5. \u5B57\u6BB5\u540D\u4F7F\u7528 snake_case\uFF0C\u5750\u6807\u4F7F\u7528 WGS84\uFF08SRID 4326\uFF09
319
+ 6. \u4E0D\u8981\u8F93\u51FA JSON \u4EE5\u5916\u7684\u4EFB\u4F55\u6587\u5B57\u3002`;
320
+ var LlmNlpg = class {
321
+ client;
322
+ enabled;
323
+ allowedTables;
324
+ constructor(config) {
325
+ this.client = config.client;
326
+ this.enabled = config.enabled ?? true;
327
+ this.allowedTables = config.allowedTables ?? DEFAULT_ALLOWED_TABLES;
328
+ }
329
+ /**
330
+ * 自然语言查询。
331
+ * LLM 优先,失败或校验不过时降级到规则引擎。
332
+ */
333
+ async query(text) {
334
+ if (this.enabled) {
335
+ try {
336
+ const llmResult = await this.queryWithLlm(text);
337
+ if (llmResult) return llmResult;
338
+ } catch {
339
+ }
340
+ }
341
+ return this.queryWithRules(text);
342
+ }
343
+ async queryWithLlm(text) {
344
+ const messages = [
345
+ { role: "system", content: SYSTEM_PROMPT },
346
+ { role: "user", content: text }
347
+ ];
348
+ const { data } = await this.client.chatJson(messages);
349
+ const sql = (data.sql ?? "").trim();
350
+ if (!sql) return null;
351
+ const validation = validateSql(sql, this.allowedTables);
352
+ if (!validation.valid) return null;
353
+ const query = {
354
+ intent: "mixed",
355
+ table: data.table ?? "pois",
356
+ conditions: [],
357
+ spatial: null,
358
+ sql,
359
+ confidence: 0.9
360
+ };
361
+ return { query, valid: true, parameterized: parameterize(sql) };
362
+ }
363
+ queryWithRules(text) {
364
+ const query = generatePostGISQuery(text);
365
+ const validation = validateSql(query.sql, this.allowedTables);
366
+ return {
367
+ query,
368
+ valid: validation.valid,
369
+ parameterized: validation.valid ? parameterize(query.sql) : void 0
370
+ };
371
+ }
372
+ };
373
+
374
+ export { DEFAULT_ALLOWED_TABLES, LlmNlpg, detectField, detectSpatial, detectTable, detectValue, generatePostGISQuery, nlpgQuery, parameterize, validateSql };
375
+ //# sourceMappingURL=chunk-WVEICADA.js.map
376
+ //# sourceMappingURL=chunk-WVEICADA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/nlpg/sqlGenerator.ts","../src/nlpg/sqlValidator.ts","../src/nlpg/nlpg.ts","../src/nlpg/llmNlpg.ts"],"names":[],"mappings":"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","file":"chunk-WVEICADA.js","sourcesContent":["/**\n * NLPG 自然语言 → PostGIS SQL 生成(PRD phase-0 §5.7 N-1/N-2/N-3)\n *\n * 支持:\n * - 属性过滤(字段 + 操作符 + 值,AND/OR 组合)\n * - 空间关系(ST_DWithin / ST_Within / ST_Intersects / ST_Contains / ST_Buffer)\n * - 混合查询(空间 + 属性)\n *\n * v1 采用「字段词典 + 规则匹配」方案(不依赖大模型),SQL 生成后必须经\n * `validateSql` 安全校验层通过方可执行(见 sqlValidator.ts)。\n */\n\n// ============================================================\n// 一、类型定义\n// ============================================================\nexport type NlpgIntent = 'attribute_filter' | 'spatial_nearby' | 'spatial_within' | 'mixed' | 'unknown';\n\nexport type Operator = '>' | '<' | '>=' | '<=' | '=' | '!=' | 'LIKE';\n\nexport interface AttributeCondition {\n /** 字段名(白名单字段) */\n field: string;\n operator: Operator;\n /** 字面量值 */\n value: string | number;\n}\n\nexport interface SpatialCondition {\n /** 空间关系 */\n relation: 'dwithin' | 'within' | 'intersects' | 'contains' | 'buffer';\n /** 参考点 [lng, lat]:within/intersects/contains/buffer/dwithin 的参考几何中心 */\n point?: [number, number];\n /** 缓冲/距离半径(米,dwithin/buffer 及以参考点表达的 within·intersects·contains 用) */\n radius?: number;\n /** 参考几何列名(进阶:与某字段几何做空间关系,缺省时用 point 构造缓冲圆) */\n referenceColumn?: string;\n /** 目标几何字段 */\n geometryColumn: string;\n}\n\nexport interface GeneratedQuery {\n intent: NlpgIntent;\n /** 目标表(白名单表) */\n table: string;\n /** 属性条件 */\n conditions: AttributeCondition[];\n /** 空间条件 */\n spatial: SpatialCondition | null;\n /** 生成的 SQL */\n sql: string;\n /** 置信度 0-1 */\n confidence: number;\n}\n\n// ============================================================\n// 二、字段与表词典\n// ============================================================\n/** 字段别名 → 规范字段名 */\nconst FIELD_ALIASES: Array<{ field: string; re: RegExp }> = [\n { field: 'material', re: /材质|材料|铸铁|钢管|PE|PVC|球墨|HDPE|铜管/ },\n { field: 'pressure', re: /压力|水压|气压|MPa/ },\n { field: 'diameter', re: /管径|直径|口径|DN/ },\n { field: 'status', re: /状态|运行|停运|故障|检修/ },\n { field: 'install_date', re: /安装日期|投运|建成|敷设/ },\n { field: 'age', re: /年限|使用.{0,2}年|超过.{0,2}年|.{0,2}年以上|年代/ },\n { field: 'voltage', re: /电压|kV|kv|伏/ },\n { field: 'load_rate', re: /负载率|负荷率|利用率|负载|负荷/ },\n { field: 'fault_rate', re: /故障率/ },\n { field: 'flow_rate', re: /流量|流速/ },\n { field: 'water_level', re: /水位/ },\n { field: 'storage_rate', re: /蓄水率|库容/ },\n { field: 'rsrp', re: /RSRP|信号强度|信号/ },\n];\n\n/** 表别名 → 规范表名 */\nconst TABLE_ALIASES: Array<{ table: string; re: RegExp }> = [\n { table: 'pipelines', re: /管段|管线|管道|管网|燃气管|供水管|排水管|供热管|电力管|通信管/ },\n { table: 'nodes', re: /节点|阀门|泵站|表|井|闸/ },\n { table: 'users', re: /用户|居民|小区|住户|建筑/ },\n { table: 'schools', re: /学校|小学|中学|大学|幼儿园/ },\n { table: 'hospitals', re: /医院|卫生院|诊所/ },\n { table: 'substations', re: /变电站|配变|台区|电站/ },\n { table: 'base_stations', re: /基站|宏站|微站|室分/ },\n { table: 'rivers', re: /河流|水系|河段|支流/ },\n { table: 'reservoirs', re: /水库|大坝/ },\n { table: 'alarms', re: /报警|告警|警报/ },\n { table: 'pois', re: /POI|兴趣点|场所|设施/ },\n];\n\n/** 字段值词典(材质/状态等) */\nconst VALUE_DICT: Array<{ field: string; value: string; re: RegExp }> = [\n { field: 'material', value: 'cast_iron', re: /铸铁/ },\n { field: 'material', value: 'ductile_iron', re: /球墨/ },\n { field: 'material', value: 'steel', re: /钢(?!筋)/ },\n { field: 'material', value: 'pe', re: /PE/ },\n { field: 'material', value: 'pvc', re: /PVC/ },\n { field: 'material', value: 'hdpe', re: /HDPE/ },\n { field: 'status', value: 'normal', re: /正常/ },\n { field: 'status', value: 'fault', re: /故障/ },\n { field: 'status', value: 'maintenance', re: /检修|维修/ },\n { field: 'status', value: 'aging', re: /老化/ },\n];\n\n/** 操作符识别 */\nfunction detectOperator(text: string): Operator {\n if (/超过|高于|大于|超出|以上|不小于/.test(text)) return text.includes('以上') || text.includes('不小于') ? '>=' : '>';\n if (/低于|小于|不到|以下|不足/.test(text)) return text.includes('以下') ? '<=' : '<';\n if (/等于|正好|恰好|为\\s*\\d/.test(text)) return '=';\n if (/不是|非|不等于/.test(text)) return '!=';\n if (/包含|含有|含/.test(text)) return 'LIKE';\n return '=';\n}\n\n// ============================================================\n// 三、表名 / 字段 / 值识别\n// ============================================================\nexport function detectTable(text: string): string {\n for (const t of TABLE_ALIASES) {\n if (t.re.test(text)) return t.table;\n }\n return 'pois';\n}\n\nexport function detectField(text: string): string | null {\n for (const f of FIELD_ALIASES) {\n if (f.re.test(text)) return f.field;\n }\n return null;\n}\n\nexport function detectValue(text: string, field: string): string | number | null {\n // 词典值(材质/状态)\n for (const v of VALUE_DICT) {\n if (v.field === field && v.re.test(text)) return v.value;\n }\n // 数值 + 单位\n const numMatch = text.match(/(\\d+(?:\\.\\d+)?)\\s*(MPa|mpa|kV|kv|米|m|公里|km|%|%|方|立方米)?/);\n if (numMatch) {\n let value = parseFloat(numMatch[1]);\n const unit = numMatch[2] ?? '';\n if (/公里|km/.test(unit)) value *= 1000; // 距离转米\n if (/%|%/.test(unit)) value /= 100; // 百分比转 0-1\n return value;\n }\n // 日期\n const dateMatch = text.match(/(\\d{4}-\\d{2}-\\d{2}|\\d{4}年\\d{1,2}月)/);\n if (dateMatch) return dateMatch[1];\n return null;\n}\n\n// ============================================================\n// 四、空间关系识别\n// ============================================================\nexport function detectSpatial(text: string, geometryColumn = 'geom'): SpatialCondition | null {\n // N 米/公里 内/附近/周边:dwithin(带参考半径)\n const nearby = text.match(/(\\d+(?:\\.\\d+)?)\\s*(米|m|公里|km|千米)\\s*(?:内|以内|范围内|附近|周边)/);\n if (nearby) {\n let radius = parseFloat(nearby[1]);\n if (/公里|km|千米/.test(nearby[2])) radius *= 1000;\n return { relation: 'dwithin', radius, geometryColumn };\n }\n const point = detectReferencePoint(text);\n // 缓冲/范围内:用参考点(或默认中心)构造缓冲圆 → ST_Intersects\n if (/缓冲区|缓冲|范围内|区域.{0,2}内/.test(text)) {\n return { relation: 'buffer', point, geometryColumn };\n }\n // 包含/覆盖(参考几何包含要素):ST_Contains(refGeom, geom)\n if (/包含|覆盖|涵盖|包围|圈住/.test(text)) {\n return { relation: 'contains', point, geometryColumn };\n }\n // 在…内部/以内(要素在某几何内):ST_Within(geom, refGeom)\n if (/在.{0,2}内|内部|以内|之中/.test(text)) {\n return { relation: 'within', point, geometryColumn };\n }\n // 相交/叠加:ST_Intersects(geom, refGeom)\n if (/相交|叠加|重叠|交叉/.test(text)) {\n return { relation: 'intersects', point, geometryColumn };\n }\n return null;\n}\n\n/** 从文本中识别\"某点/坐标/某设施\"作为参考几何中心;当前仅支持显式经纬度,否则返回 undefined(由 generate 补中心) */\nfunction detectReferencePoint(text: string): [number, number] | undefined {\n const m = text.match(/([1-2]?\\d{2,3}(?:\\.\\d+)?)\\s*[,,]\\s*(\\d{1,3}(?:\\.\\d+)?)/);\n if (m) {\n const lng = parseFloat(m[1]);\n const lat = parseFloat(m[2]);\n if (lng >= -180 && lng <= 180 && lat >= -90 && lat <= 90) return [lng, lat];\n }\n return undefined;\n}\n\n// ============================================================\n// 五、SQL 组装\n// ============================================================\nfunction quoteValue(v: string | number): string {\n return typeof v === 'string' ? `'${v.replace(/'/g, \"''\")}'` : String(v);\n}\n\nfunction buildWhere(conditions: AttributeCondition[], spatial: SpatialCondition | null): string {\n const parts: string[] = [];\n for (const c of conditions) {\n parts.push(`${c.field} ${c.operator} ${quoteValue(c.value)}`);\n }\n if (spatial) {\n const col = spatial.geometryColumn;\n // 参考几何:优先 referenceColumn(字段几何),否则用 point 构造缓冲圆\n const refGeom = spatial.referenceColumn\n ? spatial.referenceColumn\n : spatial.point\n ? `ST_Buffer(ST_SetSRID(ST_MakePoint(${spatial.point[0]}, ${spatial.point[1]}), 4326), ${spatial.radius ?? 0})`\n : null;\n switch (spatial.relation) {\n case 'dwithin':\n if (spatial.point && spatial.radius !== undefined) {\n parts.push(`ST_DWithin(${col}, ST_SetSRID(ST_MakePoint(${spatial.point[0]}, ${spatial.point[1]}), 4326), ${spatial.radius})`);\n }\n break;\n case 'buffer':\n // 缓冲区内:以参考几何/参考点缓冲圆判断相交\n if (refGeom) parts.push(`ST_Intersects(${col}, ${refGeom})`);\n break;\n case 'within':\n // 要素在参考几何内\n if (refGeom) parts.push(`ST_Within(${col}, ${refGeom})`);\n break;\n case 'contains':\n // 参考几何包含要素\n if (refGeom) parts.push(`ST_Contains(${refGeom}, ${col})`);\n break;\n case 'intersects':\n // 要素与参考几何相交\n if (refGeom) parts.push(`ST_Intersects(${col}, ${refGeom})`);\n break;\n }\n }\n return parts.length > 0 ? `WHERE ${parts.join(' AND ')}` : '';\n}\n\n// ============================================================\n// 六、主入口\n// ============================================================\nexport interface GenerateOptions {\n /** 参考点(空间查询用,默认武汉中心) */\n center?: [number, number];\n /** 目标几何字段 */\n geometryColumn?: string;\n}\n\nexport function generatePostGISQuery(text: string, opts: GenerateOptions = {}): GeneratedQuery {\n const center = opts.center ?? [114.305, 30.593];\n const geometryColumn = opts.geometryColumn ?? 'geom';\n\n const table = detectTable(text);\n const spatial = detectSpatial(text, geometryColumn);\n // 空间查询需要参考点:默认使用中心坐标(buffer/within/intersects/contains 以参考点缓冲圆表达)\n if (spatial && !spatial.point) {\n spatial.point = center;\n }\n // buffer/within/intersects/contains 缺省半径时给一个合理缓冲半径\n if (spatial && spatial.relation !== 'dwithin' && spatial.radius === undefined) {\n spatial.radius = 1000;\n }\n\n // 属性条件提取\n const conditions: AttributeCondition[] = [];\n const field = detectField(text);\n if (field) {\n const value = detectValue(text, field);\n if (value !== null) {\n conditions.push({ field, operator: detectOperator(text), value });\n }\n }\n\n // 意图判定\n let intent: NlpgIntent;\n if (spatial && conditions.length > 0) intent = 'mixed';\n else if (spatial) intent = spatial.relation === 'dwithin' ? 'spatial_nearby' : 'spatial_within';\n else if (conditions.length > 0) intent = 'attribute_filter';\n else intent = 'unknown';\n\n const where = buildWhere(conditions, spatial);\n const sql = `SELECT * FROM ${table} ${where}`.trim();\n\n // 置信度\n let confidence = 0.3;\n if (conditions.length > 0) confidence += 0.3;\n if (spatial) confidence += 0.3;\n if (table !== 'pois') confidence += 0.1;\n confidence = Math.min(0.95, confidence);\n\n return { intent, table, conditions, spatial, sql, confidence };\n}\n","/**\n * NLPG SQL 安全校验层(PRD phase-0 §5.7 N-5 / §5.7.3)\n *\n * 所有 LLM/规则生成的 SQL 必须经过以下校验方可执行:\n * 1. 语法校验(引号/括号配对、非空)\n * 2. 白名单表检查(只允许查询授权表)\n * 3. 危险操作拦截(DROP/DELETE/UPDATE/INSERT/ALTER/TRUNCATE/GRANT 等全部拒绝)\n * 4. 仅允许 SELECT 只读查询\n * 5. 参数化占位(防止 SQL 注入的字面量转义)\n *\n * 纯函数,可在 Node/浏览器两侧运行。\n */\n\nexport interface ValidationIssue {\n severity: 'error' | 'warning';\n rule: string;\n message: string;\n}\n\nexport interface ValidationResult {\n /** 是否通过(无 error 级问题) */\n valid: boolean;\n issues: ValidationIssue[];\n}\n\n/** 危险关键字(拒绝执行) */\nconst DANGEROUS_KEYWORDS = [\n 'DROP', 'DELETE', 'UPDATE', 'INSERT', 'ALTER', 'TRUNCATE', 'GRANT', 'REVOKE',\n 'MERGE', 'CREATE', 'REPLACE', 'EXEC', 'EXECUTE', 'CALL', 'COPY', 'LOAD',\n 'INTO', 'SET', 'UNION', 'ATTACH', 'DETACH', 'PRAGMA', 'VACUUM', 'REINDEX',\n];\n\n/** 危险注释/注入特征 */\nconst INJECTION_PATTERNS = [\n /--/, // SQL 注释\n /\\/\\*/, // 块注释\n /;\\s*(DROP|DELETE|UPDATE|INSERT|ALTER|TRUNCATE)/i, // 多语句注入\n /'\\s*OR\\s*'/i, // ' OR ' 恒真注入(含 ' OR '1'='1')\n /\"\\s*OR\\s*\"/i, // \" OR \" 恒真注入\n /\\bOR\\s+\\d+\\s*=\\s*\\d+\\b/i, // OR 1=1 恒真\n /\\bOR\\s+'\\w*'\\s*=\\s*'\\w*'/i, // OR 'a'='a' 恒真\n /UNION\\s+SELECT/i,\n];\n\n/** 默认白名单表(只读授权) */\nexport const DEFAULT_ALLOWED_TABLES = [\n 'pipelines', 'nodes', 'users', 'schools', 'hospitals', 'substations',\n 'base_stations', 'rivers', 'reservoirs', 'alarms', 'pois',\n];\n\n/** 允许的空间函数 */\nconst ALLOWED_SPATIAL_FUNCTIONS = [\n 'ST_DWithin', 'ST_Within', 'ST_Intersects', 'ST_Contains', 'ST_Buffer',\n 'ST_MakePoint', 'ST_SetSRID', 'ST_Distance', 'ST_AsGeoJSON', 'ST_Transform',\n];\n\n/**\n * 校验 SQL。\n * @param sql 待校验 SQL\n * @param allowedTables 白名单表(默认 DEFAULT_ALLOWED_TABLES)\n */\nexport function validateSql(sql: string, allowedTables: string[] = DEFAULT_ALLOWED_TABLES): ValidationResult {\n const issues: ValidationIssue[] = [];\n const upper = sql.toUpperCase().trim();\n\n // 1) 非空\n if (!sql.trim()) {\n issues.push({ severity: 'error', rule: 'non_empty', message: 'SQL 不能为空' });\n return { valid: false, issues };\n }\n\n // 2) 必须以 SELECT 开头(只读)\n if (!upper.startsWith('SELECT')) {\n issues.push({ severity: 'error', rule: 'read_only', message: '仅允许 SELECT 只读查询' });\n }\n\n // 3) 危险操作拦截\n for (const kw of DANGEROUS_KEYWORDS) {\n // 用词边界匹配,避免误伤字段名(如 \"status\" 含 \"set\" 的误判)\n const re = new RegExp(`\\\\b${kw}\\\\b`, 'i');\n if (re.test(sql) && !(kw === 'SET' && /ST_SetSRID/i.test(sql))) {\n issues.push({ severity: 'error', rule: 'dangerous_keyword', message: `检测到危险关键字 ${kw}` });\n }\n }\n\n // 4) 注入特征拦截\n for (const p of INJECTION_PATTERNS) {\n if (p.test(sql)) {\n issues.push({ severity: 'error', rule: 'injection', message: '检测到 SQL 注入特征' });\n }\n }\n\n // 5) 白名单表检查\n const fromMatch = upper.match(/FROM\\s+([A-Za-z_][\\w]*)/);\n if (fromMatch) {\n const table = fromMatch[1].toLowerCase();\n if (!allowedTables.includes(table)) {\n issues.push({ severity: 'error', rule: 'table_whitelist', message: `表 ${table} 不在授权白名单内` });\n }\n }\n\n // 6) 引号/括号配对\n const singleQuotes = (sql.match(/'/g) ?? []).length;\n if (singleQuotes % 2 !== 0) {\n issues.push({ severity: 'error', rule: 'syntax', message: '单引号未配对' });\n }\n const openParen = (sql.match(/\\(/g) ?? []).length;\n const closeParen = (sql.match(/\\)/g) ?? []).length;\n if (openParen !== closeParen) {\n issues.push({ severity: 'error', rule: 'syntax', message: '括号未配对' });\n }\n\n // 7) 空间函数白名单检查(若用到空间函数)\n for (const fn of ['ST_']) {\n const fnMatch = sql.match(/ST_(\\w+)/g) ?? [];\n for (const m of fnMatch) {\n if (!ALLOWED_SPATIAL_FUNCTIONS.includes(m)) {\n issues.push({ severity: 'error', rule: 'spatial_whitelist', message: `空间函数 ${m} 不在白名单内` });\n }\n }\n }\n\n return { valid: issues.every((i) => i.severity !== 'error'), issues };\n}\n\n/** 参数化:把 SQL 中的字符串字面量替换为 $n 占位符(防注入) */\nexport function parameterize(sql: string): { sql: string; params: string[] } {\n const params: string[] = [];\n const out = sql.replace(/'([^']*)'/g, (_m, v: string) => {\n params.push(v);\n return `$${params.length}`;\n });\n return { sql: out, params };\n}\n","/**\n * NLPG v1 主入口(PRD phase-0 §5.7)\n *\n * 自然语言 → PostGIS SQL → 安全校验 → 可执行结果。\n *\n * 用法:\n * const result = nlpgQuery('查出使用超过 20 年的铸铁燃气管');\n * result.sql // 生成的 SQL\n * result.valid // 是否通过安全校验\n * result.validation // 校验详情\n */\n\nimport { generatePostGISQuery, type GeneratedQuery, type GenerateOptions } from './sqlGenerator';\nimport { validateSql, parameterize, type ValidationResult } from './sqlValidator';\n\nexport interface NlpgResult {\n /** 生成的查询 */\n query: GeneratedQuery;\n /** 是否通过安全校验 */\n valid: boolean;\n /** 校验详情 */\n validation: ValidationResult;\n /** 参数化后的 SQL($n 占位符) */\n parameterized?: { sql: string; params: string[] };\n}\n\n/**\n * 自然语言查询入口。\n */\nexport function nlpgQuery(text: string, opts: GenerateOptions = {}): NlpgResult {\n const query = generatePostGISQuery(text, opts);\n const validation = validateSql(query.sql);\n const valid = validation.valid;\n const parameterized = valid ? parameterize(query.sql) : undefined;\n\n return { query, valid, validation, parameterized };\n}\n\nexport type { GeneratedQuery, GenerateOptions };\nexport { generatePostGISQuery, validateSql, parameterize };\n","/**\n * NLPG LLM 模式(PRD phase-0 §5.7 N-6「LLM 生成 SQL」)\n *\n * 在规则引擎(sqlGenerator.ts)基础上,接入 DeepSeek 实现:\n * - 复杂自然语言 → PostGIS SQL(突破固定字段/空间模板)\n * - LLM 优先,失败自动降级规则引擎\n * - 生成的 SQL 必经 sqlValidator 安全校验层方可执行\n */\n\nimport type { DeepSeekClient, ChatMessage } from '../llm/deepseek';\nimport { generatePostGISQuery, type GeneratedQuery } from './sqlGenerator';\nimport { validateSql, parameterize, DEFAULT_ALLOWED_TABLES } from './sqlValidator';\n\nexport interface LlmNlpgConfig {\n client: DeepSeekClient;\n enabled?: boolean;\n /** 授权表白名单(传给校验层) */\n allowedTables?: string[];\n}\n\ninterface LlmSqlOutput {\n sql?: string;\n table?: string;\n intent?: string;\n}\n\nconst SYSTEM_PROMPT = `你是\"草果地图\"管网自然语言查询助手。用户用中文描述数据查询需求,你生成 PostGIS SQL(仅 SELECT)。\n\n规则:\n1. 只返回 JSON:{\"sql\":\"SELECT ...\",\"table\":\"...\",\"intent\":\"...\"}\n2. 表名只能来自以下白名单:${DEFAULT_ALLOWED_TABLES.join(', ')}\n3. 空间查询可用 ST_DWithin / ST_Within / ST_Intersects / ST_Contains / ST_Buffer / ST_MakePoint / ST_SetSRID / ST_Distance / ST_AsGeoJSON / ST_Transform\n4. 只生成 SELECT 语句,禁止 DROP/DELETE/UPDATE/INSERT/ALTER 等写操作\n5. 字段名使用 snake_case,坐标使用 WGS84(SRID 4326)\n6. 不要输出 JSON 以外的任何文字。`;\n\nexport class LlmNlpg {\n private client: DeepSeekClient;\n private enabled: boolean;\n private allowedTables: string[];\n\n constructor(config: LlmNlpgConfig) {\n this.client = config.client;\n this.enabled = config.enabled ?? true;\n this.allowedTables = config.allowedTables ?? DEFAULT_ALLOWED_TABLES;\n }\n\n /**\n * 自然语言查询。\n * LLM 优先,失败或校验不过时降级到规则引擎。\n */\n async query(text: string): Promise<{ query: GeneratedQuery; valid: boolean; parameterized?: { sql: string; params: string[] } }> {\n if (this.enabled) {\n try {\n const llmResult = await this.queryWithLlm(text);\n if (llmResult) return llmResult;\n } catch {\n // 降级规则引擎\n }\n }\n return this.queryWithRules(text);\n }\n\n private async queryWithLlm(\n text: string\n ): Promise<{ query: GeneratedQuery; valid: boolean; parameterized?: { sql: string; params: string[] } } | null> {\n const messages: ChatMessage[] = [\n { role: 'system', content: SYSTEM_PROMPT },\n { role: 'user', content: text },\n ];\n const { data } = await this.client.chatJson<LlmSqlOutput>(messages);\n const sql = (data.sql ?? '').trim();\n if (!sql) return null;\n\n // 必经安全校验层\n const validation = validateSql(sql, this.allowedTables);\n if (!validation.valid) return null;\n\n const query: GeneratedQuery = {\n intent: 'mixed',\n table: data.table ?? 'pois',\n conditions: [],\n spatial: null,\n sql,\n confidence: 0.9,\n };\n return { query, valid: true, parameterized: parameterize(sql) };\n }\n\n private queryWithRules(\n text: string\n ): { query: GeneratedQuery; valid: boolean; parameterized?: { sql: string; params: string[] } } {\n const query = generatePostGISQuery(text);\n const validation = validateSql(query.sql, this.allowedTables);\n return {\n query,\n valid: validation.valid,\n parameterized: validation.valid ? parameterize(query.sql) : undefined,\n };\n }\n}\n"]}
package/dist/index.cjs CHANGED
@@ -1043,17 +1043,30 @@ function detectSpatial(text, geometryColumn = "geom") {
1043
1043
  if (/公里|km|千米/.test(nearby[2])) radius *= 1e3;
1044
1044
  return { relation: "dwithin", radius, geometryColumn };
1045
1045
  }
1046
+ const point = detectReferencePoint(text);
1046
1047
  if (/缓冲区|缓冲|范围内|区域.{0,2}内/.test(text)) {
1047
- return { relation: "buffer", geometryColumn };
1048
+ return { relation: "buffer", point, geometryColumn };
1048
1049
  }
1049
- if (/包含|覆盖.{0,2}内|在.{0,2}内/.test(text)) {
1050
- return { relation: "within", geometryColumn };
1050
+ if (/包含|覆盖|涵盖|包围|圈住/.test(text)) {
1051
+ return { relation: "contains", point, geometryColumn };
1052
+ }
1053
+ if (/在.{0,2}内|内部|以内|之中/.test(text)) {
1054
+ return { relation: "within", point, geometryColumn };
1051
1055
  }
1052
1056
  if (/相交|叠加|重叠|交叉/.test(text)) {
1053
- return { relation: "intersects", geometryColumn };
1057
+ return { relation: "intersects", point, geometryColumn };
1054
1058
  }
1055
1059
  return null;
1056
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
+ }
1057
1070
  function quoteValue(v) {
1058
1071
  return typeof v === "string" ? `'${v.replace(/'/g, "''")}'` : String(v);
1059
1072
  }
@@ -1063,14 +1076,26 @@ function buildWhere(conditions, spatial) {
1063
1076
  parts.push(`${c.field} ${c.operator} ${quoteValue(c.value)}`);
1064
1077
  }
1065
1078
  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})`);
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;
1074
1099
  }
1075
1100
  }
1076
1101
  return parts.length > 0 ? `WHERE ${parts.join(" AND ")}` : "";
@@ -1080,9 +1105,12 @@ function generatePostGISQuery(text, opts = {}) {
1080
1105
  const geometryColumn = opts.geometryColumn ?? "geom";
1081
1106
  const table = detectTable(text);
1082
1107
  const spatial = detectSpatial(text, geometryColumn);
1083
- if (spatial && spatial.relation === "dwithin" && !spatial.point) {
1108
+ if (spatial && !spatial.point) {
1084
1109
  spatial.point = center;
1085
1110
  }
1111
+ if (spatial && spatial.relation !== "dwithin" && spatial.radius === void 0) {
1112
+ spatial.radius = 1e3;
1113
+ }
1086
1114
  const conditions = [];
1087
1115
  const field = detectField(text);
1088
1116
  if (field) {