@atlisp/mcp 1.8.23 → 1.8.26

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/config.js CHANGED
@@ -4,41 +4,42 @@ import path from 'path';
4
4
  import os from 'os';
5
5
 
6
6
  const envSchema = z.object({
7
- PORT: z.string().optional().default('8110'),
7
+ PORT: z.coerce.number().optional().default(8110),
8
8
  HOST: z.string().optional().default('0.0.0.0'),
9
9
  TRANSPORT: z.enum(['http', 'stdio', 'ws']).optional().default('http'),
10
10
  MCP_API_KEY: z.string().optional().default(''),
11
- ENABLE_SSE: z.string().optional().default('true'),
12
- MESSAGE_TIMEOUT: z.string().optional().default('60000'),
13
- HEARTBEAT_INTERVAL: z.string().optional().default('30000'),
14
- BUSY_RETRIES: z.string().optional().default('10'),
15
- BUSY_DELAY: z.string().optional().default('500'),
16
- ENABLE_CORS: z.string().optional().default('true'),
17
- CORS_ORIGIN: z.string().optional().default('*'),
18
- RATE_LIMIT_WINDOW: z.string().optional().default('60000'),
19
- RATE_LIMIT_MAX: z.string().optional().default('100'),
20
- RESOURCE_CACHE_TTL: z.string().optional().default('5000'),
21
- FUNCTION_LIB_CACHE_TTL: z.string().optional().default('300000'),
22
- DEBUG: z.string().optional().default('false'),
11
+ ENABLE_SSE: z.coerce.boolean().optional().default(true),
12
+ MESSAGE_TIMEOUT: z.coerce.number().optional().default(60000),
13
+ HEARTBEAT_INTERVAL: z.coerce.number().optional().default(30000),
14
+ BUSY_RETRIES: z.coerce.number().optional().default(10),
15
+ BUSY_DELAY: z.coerce.number().optional().default(500),
16
+ ENABLE_CORS: z.coerce.boolean().optional().default(true),
17
+ CORS_ORIGIN: z.string().optional().default('https://atlisp.cn'),
18
+ RATE_LIMIT_WINDOW: z.coerce.number().optional().default(60000),
19
+ RATE_LIMIT_MAX: z.coerce.number().optional().default(100),
20
+ RESOURCE_CACHE_TTL: z.coerce.number().optional().default(5000),
21
+ FUNCTION_LIB_CACHE_TTL: z.coerce.number().optional().default(300000),
22
+ DEBUG: z.coerce.boolean().optional().default(false),
23
23
  DEBUG_FILE: z.string().optional().default(''),
24
24
  FUNCTION_LIB_URL: z.string().optional().default('http://s3.atlisp.cn/json/functions.json'),
25
25
  CONFIG_FILE: z.string().optional().default(''),
26
- API_KEY_RATE_LIMIT: z.string().optional().default('1000'),
27
- METRICS_ENABLED: z.string().optional().default('true'),
28
- REQUEST_LOG_ENABLED: z.string().optional().default('true'),
26
+ API_KEY_RATE_LIMIT: z.coerce.number().optional().default(1000),
27
+ METRICS_ENABLED: z.coerce.boolean().optional().default(true),
28
+ REQUEST_LOG_ENABLED: z.coerce.boolean().optional().default(true),
29
29
  SSL_KEY_PATH: z.string().optional().default(''),
30
30
  SSL_CERT_PATH: z.string().optional().default(''),
31
31
  LLM_API_KEY: z.string().optional().default(''),
32
32
  LLM_API_URL: z.string().optional().default('https://api.openai.com/v1/chat/completions'),
33
33
  LLM_MODEL: z.string().optional().default('gpt-4o-mini'),
34
- LLM_MAX_TOKENS: z.string().optional().default('4096'),
35
- LLM_TEMPERATURE: z.string().optional().default('0.7'),
36
- WORKER_POOL_SIZE: z.string().optional().default('2'),
34
+ LLM_MAX_TOKENS: z.coerce.number().optional().default(4096),
35
+ LLM_TEMPERATURE: z.coerce.number().optional().default(0.7),
36
+ WORKER_POOL_SIZE: z.coerce.number().optional().default(2),
37
37
  LINT_PRESET: z.string().optional().default(''),
38
38
  LINT_CONFIG_PATH: z.string().optional().default(''),
39
39
  LINT_LEVEL: z.string().optional().default(''),
40
- NO_LINT: z.string().optional().default(''),
40
+ NO_LINT: z.coerce.boolean().optional().default(false),
41
41
  SECURITY_LEVEL: z.string().optional().default('strict'),
42
+ CLIENT_ID: z.string().optional().default(''),
42
43
  });
43
44
 
44
45
  const configSchema = z.object({
@@ -83,6 +84,7 @@ const configSchema = z.object({
83
84
  lintLevel: z.string().optional(),
84
85
  noLint: z.boolean().optional(),
85
86
  securityLevel: z.enum(['strict', 'standard', 'relaxed']).optional(),
87
+ clientId: z.string().optional(),
86
88
  });
87
89
 
88
90
  function parseArgs() {
@@ -97,7 +99,7 @@ function parseArgs() {
97
99
  } else if (args[i] === '--ws') {
98
100
  result.transport = 'ws';
99
101
  } else if (args[i] === '--port' && args[i + 1]) {
100
- result.port = args[i + 1];
102
+ result.port = parseInt(args[i + 1], 10);
101
103
  i++;
102
104
  } else if (args[i] === '--host' && args[i + 1]) {
103
105
  result.host = args[i + 1];
@@ -166,43 +168,44 @@ const configFilePath = env.CONFIG_FILE || cliArgs.configFile ||
166
168
  const fileConfig = loadConfigFile(configFilePath);
167
169
 
168
170
  const config = {
169
- port: fileConfig?.port || parseInt(cliArgs.port || env.PORT, 10),
170
- host: fileConfig?.host || cliArgs.host || env.HOST,
171
+ port: fileConfig?.port ?? cliArgs.port ?? env.PORT,
172
+ host: fileConfig?.host ?? cliArgs.host ?? env.HOST,
171
173
  transport: fileConfig?.transport ?? cliArgs.transport ?? env.TRANSPORT,
172
- apiKey: fileConfig?.apiKey || env.MCP_API_KEY,
173
- enableSse: fileConfig?.enableSse ?? (env.ENABLE_SSE === 'true'),
174
- messageTimeout: fileConfig?.messageTimeout || parseInt(env.MESSAGE_TIMEOUT, 10),
175
- heartbeatInterval: fileConfig?.heartbeatInterval || parseInt(env.HEARTBEAT_INTERVAL, 10),
176
- busyRetries: fileConfig?.busyRetries || parseInt(env.BUSY_RETRIES, 10),
177
- busyDelay: fileConfig?.busyDelay || parseInt(env.BUSY_DELAY, 10),
178
- enableCors: fileConfig?.enableCors ?? (env.ENABLE_CORS === 'true'),
179
- corsOrigin: fileConfig?.corsOrigin || env.CORS_ORIGIN,
180
- rateLimitWindow: fileConfig?.rateLimitWindow || parseInt(env.RATE_LIMIT_WINDOW, 10),
181
- rateLimitMax: fileConfig?.rateLimitMax || parseInt(env.RATE_LIMIT_MAX, 10),
182
- resourceCacheTtl: fileConfig?.resourceCacheTtl || parseInt(env.RESOURCE_CACHE_TTL, 10),
183
- functionLibCacheTtl: fileConfig?.functionLibCacheTtl || parseInt(env.FUNCTION_LIB_CACHE_TTL, 10),
184
- debug: fileConfig?.debug ?? (env.DEBUG === 'true'),
185
- debugFile: fileConfig?.debugFile || env.DEBUG_FILE,
186
- functionLibUrl: fileConfig?.functionLibUrl || env.FUNCTION_LIB_URL,
174
+ apiKey: fileConfig?.apiKey ?? env.MCP_API_KEY,
175
+ enableSse: fileConfig?.enableSse ?? env.ENABLE_SSE,
176
+ messageTimeout: fileConfig?.messageTimeout ?? env.MESSAGE_TIMEOUT,
177
+ heartbeatInterval: fileConfig?.heartbeatInterval ?? env.HEARTBEAT_INTERVAL,
178
+ busyRetries: fileConfig?.busyRetries ?? env.BUSY_RETRIES,
179
+ busyDelay: fileConfig?.busyDelay ?? env.BUSY_DELAY,
180
+ enableCors: fileConfig?.enableCors ?? env.ENABLE_CORS,
181
+ corsOrigin: fileConfig?.corsOrigin ?? env.CORS_ORIGIN,
182
+ rateLimitWindow: fileConfig?.rateLimitWindow ?? env.RATE_LIMIT_WINDOW,
183
+ rateLimitMax: fileConfig?.rateLimitMax ?? env.RATE_LIMIT_MAX,
184
+ resourceCacheTtl: fileConfig?.resourceCacheTtl ?? env.RESOURCE_CACHE_TTL,
185
+ functionLibCacheTtl: fileConfig?.functionLibCacheTtl ?? env.FUNCTION_LIB_CACHE_TTL,
186
+ debug: fileConfig?.debug ?? env.DEBUG,
187
+ debugFile: fileConfig?.debugFile ?? env.DEBUG_FILE,
188
+ functionLibUrl: fileConfig?.functionLibUrl ?? env.FUNCTION_LIB_URL,
187
189
  configFile: configFilePath,
188
- apiKeyRateLimit: fileConfig?.apiKeyRateLimit || parseInt(env.API_KEY_RATE_LIMIT, 10),
189
- apiKeys: fileConfig?.apiKeys || [],
190
- metricsEnabled: fileConfig?.metricsEnabled ?? (env.METRICS_ENABLED === 'true'),
191
- requestLogEnabled: fileConfig?.requestLogEnabled ?? (env.REQUEST_LOG_ENABLED !== 'false'),
192
- sslKey: fileConfig?.sslKey || env.SSL_KEY_PATH || cliArgs.sslKey,
193
- sslCert: fileConfig?.sslCert || env.SSL_CERT_PATH || cliArgs.sslCert,
194
- llmApiKey: fileConfig?.llmApiKey || env.LLM_API_KEY,
195
- llmApiUrl: fileConfig?.llmApiUrl || env.LLM_API_URL,
196
- llmModel: fileConfig?.llmModel || env.LLM_MODEL,
197
- llmMaxTokens: fileConfig?.llmMaxTokens || parseInt(env.LLM_MAX_TOKENS, 10),
198
- llmTemperature: fileConfig?.llmTemperature || parseFloat(env.LLM_TEMPERATURE),
199
- maxPoolSize: fileConfig?.maxPoolSize || 3,
200
- workerPoolSize: fileConfig?.workerPoolSize || parseInt(env.WORKER_POOL_SIZE, 10),
201
- lintPreset: fileConfig?.lintPreset || cliArgs.lintPreset || env.LINT_PRESET || undefined,
202
- lintConfigPath: fileConfig?.lintConfigPath || cliArgs.lintConfigPath || env.LINT_CONFIG_PATH || undefined,
203
- lintLevel: fileConfig?.lintLevel || cliArgs.lintLevel || env.LINT_LEVEL || undefined,
204
- noLint: fileConfig?.noLint ?? cliArgs.noLint ?? (env.NO_LINT === 'true'),
190
+ apiKeyRateLimit: fileConfig?.apiKeyRateLimit ?? env.API_KEY_RATE_LIMIT,
191
+ apiKeys: fileConfig?.apiKeys ?? [],
192
+ metricsEnabled: fileConfig?.metricsEnabled ?? env.METRICS_ENABLED,
193
+ requestLogEnabled: fileConfig?.requestLogEnabled ?? env.REQUEST_LOG_ENABLED,
194
+ sslKey: fileConfig?.sslKey ?? env.SSL_KEY_PATH ?? cliArgs.sslKey,
195
+ sslCert: fileConfig?.sslCert ?? env.SSL_CERT_PATH ?? cliArgs.sslCert,
196
+ llmApiKey: fileConfig?.llmApiKey ?? env.LLM_API_KEY,
197
+ llmApiUrl: fileConfig?.llmApiUrl ?? env.LLM_API_URL,
198
+ llmModel: fileConfig?.llmModel ?? env.LLM_MODEL,
199
+ llmMaxTokens: fileConfig?.llmMaxTokens ?? env.LLM_MAX_TOKENS,
200
+ llmTemperature: fileConfig?.llmTemperature ?? env.LLM_TEMPERATURE,
201
+ maxPoolSize: fileConfig?.maxPoolSize ?? 3,
202
+ workerPoolSize: fileConfig?.workerPoolSize ?? env.WORKER_POOL_SIZE,
203
+ lintPreset: fileConfig?.lintPreset ?? cliArgs.lintPreset ?? (env.LINT_PRESET || undefined),
204
+ lintConfigPath: fileConfig?.lintConfigPath ?? cliArgs.lintConfigPath ?? (env.LINT_CONFIG_PATH || undefined),
205
+ lintLevel: fileConfig?.lintLevel ?? cliArgs.lintLevel ?? (env.LINT_LEVEL || undefined),
206
+ noLint: fileConfig?.noLint ?? cliArgs.noLint ?? env.NO_LINT,
205
207
  securityLevel: fileConfig?.securityLevel ?? cliArgs.securityLevel ?? (env.SECURITY_LEVEL || 'strict'),
208
+ clientId: fileConfig?.clientId ?? (env.CLIENT_ID || ''),
206
209
  };
207
210
 
208
211
  if (config.lintConfigPath) process.env.LINT_CONFIG_PATH = config.lintConfigPath;
@@ -283,4 +286,4 @@ export function onConfigChange(callback) {
283
286
  configWatchers.push(callback);
284
287
  }
285
288
 
286
- export default config;
289
+ export default config;
@@ -0,0 +1,242 @@
1
+ import { cad } from '../cad.js';
2
+ import { escapeLispString } from '../handler-utils.js';
3
+
4
+ function ensureCad() {
5
+ if (!cad.connected) throw new Error('未连接 CAD');
6
+ }
7
+
8
+ function esc(s) {
9
+ return escapeLispString(String(s));
10
+ }
11
+
12
+ function buildFileList(files) {
13
+ return files.map(f => `"${esc(f)}"`).join(' ');
14
+ }
15
+
16
+ export async function batchDocCheckStandards(args) {
17
+ ensureCad();
18
+ const { files, requiredLayers, forbiddenLayerPrefixes, requiredTextStyles, requiredDimStyles, maxLayerCount } = args;
19
+ if (!files?.length) return { content: [{ type: 'text', text: '需要 files 参数' }], isError: true };
20
+
21
+ const fileList = buildFileList(files);
22
+ const reqLayers = (requiredLayers || []).map(l => `"${esc(l)}"`).join(' ');
23
+ const forPrefixes = (forbiddenLayerPrefixes || []).map(p => `"${esc(p)}"`).join(' ');
24
+ const reqTStyles = (requiredTextStyles || []).map(s => `"${esc(s)}"`).join(' ');
25
+ const reqDStyles = (requiredDimStyles || []).map(s => `"${esc(s)}"`).join(' ');
26
+
27
+ const lisp = `(progn
28
+ (vl-load-com)
29
+ (setq files (list ${fileList}))
30
+ (setq req-layers (list ${reqLayers}))
31
+ (setq for-prefixes (list ${forPrefixes}))
32
+ (setq req-tstyles (list ${reqTStyles}))
33
+ (setq req-dstyles (list ${reqDStyles}))
34
+ (setq max-layer ${maxLayerCount || 9999})
35
+ (setq all-reports (list))
36
+ (foreach f files
37
+ (setq report (list (cons "file" f) (cons "ok" T) (cons "issues" (list))))
38
+ (if (findfile f)
39
+ (progn
40
+ (command "._OPEN" f)
41
+ (setq issues (list))
42
+ ;; check required layers
43
+ (foreach l req-layers
44
+ (if (not (tblsearch "LAYER" l))
45
+ (setq issues (cons (list "missing_layer" (strcat "缺少必需图层: " l)) issues))))
46
+ ;; check forbidden layer prefixes
47
+ (setq all-layers (list))
48
+ (setq i 0)
49
+ (while (setq e (tblnext "LAYER" (if (= i 0) T nil)))
50
+ (setq all-layers (cons (cdr (assoc 2 e)) all-layers))
51
+ (setq i (1+ i)))
52
+ (foreach p for-prefixes
53
+ (foreach l all-layers
54
+ (if (wcmatch (strcase l) (strcat (strcase p) "*"))
55
+ (setq issues (cons (list "forbidden_layer" (strcat "禁止图层前缀: " l " 匹配 " p)) issues)))))
56
+ ;; check layer count
57
+ (if (> (length all-layers) max-layer)
58
+ (setq issues (cons (list "too_many_layers" (strcat "图层数量 " (itoa (length all-layers)) " 超过限制 " (itoa max-layer))) issues)))
59
+ ;; check required text styles
60
+ (foreach s req-tstyles
61
+ (if (not (tblsearch "STYLE" s))
62
+ (setq issues (cons (list "missing_text_style" (strcat "缺少必需文字样式: " s)) issues))))
63
+ ;; check required dim styles
64
+ (foreach s req-dstyles
65
+ (if (not (tblsearch "DIMSTYLE" s))
66
+ (setq issues (cons (list "missing_dim_style" (strcat "缺少必需标注样式: " s)) issues))))
67
+ (if issues
68
+ (progn
69
+ (setq report (cons (cons "ok" nil) report))
70
+ (setq report (cons (cons "issues" issues) report)))
71
+ )
72
+ (command "_.CLOSE" "_N")
73
+ )
74
+ (setq report (cons (cons "ok" nil) (cons (cons "issues" (list (list "file_not_found" "文件不存在"))) report)))
75
+ )
76
+ (setq all-reports (cons report all-reports))
77
+ )
78
+ (princ all-reports)
79
+ (princ))
80
+ `;
81
+
82
+ try {
83
+ const raw = await cad.sendCommandWithResult(lisp);
84
+ let reports;
85
+ try {
86
+ reports = typeof raw === 'string' ? JSON.parse(raw) : raw;
87
+ } catch {
88
+ reports = [{ file: 'parse_error', ok: false, issues: [{ type: 'parse_error', msg: String(raw).substring(0, 200) }] }];
89
+ }
90
+ return { content: [{ type: 'text', text: JSON.stringify(reports, null, 2) }] };
91
+ } catch (e) {
92
+ return { content: [{ type: 'text', text: `批量检查失败: ${e.message}` }], isError: true };
93
+ }
94
+ }
95
+
96
+ export async function batchDocFindReplace(args) {
97
+ ensureCad();
98
+ const { files, pattern, replacement, caseSensitive, textTypes } = args;
99
+ if (!files?.length) return { content: [{ type: 'text', text: '需要 files 参数' }], isError: true };
100
+ if (!pattern) return { content: [{ type: 'text', text: '需要 pattern 参数' }], isError: true };
101
+
102
+ const fileList = buildFileList(files);
103
+ const caseFlag = caseSensitive ? 'nil' : 'T';
104
+ const types = textTypes || ['ALL'];
105
+ const textFilter = types.includes('ALL')
106
+ ? '(cons 0 "TEXT,MTEXT")'
107
+ : `(cons 0 "${types.join(',')}")`;
108
+
109
+ const lisp = `(progn
110
+ (vl-load-com)
111
+ (setq files (list ${fileList}))
112
+ (setq find-pat "${esc(pattern)}")
113
+ (setq repl-text "${esc(replacement)}")
114
+ (setq all-results (list))
115
+ (foreach f files
116
+ (setq result (list (cons "file" f) (cons "ok" T) (cons "count" 0)))
117
+ (if (findfile f)
118
+ (progn
119
+ (command "._OPEN" f)
120
+ (setq count 0)
121
+ (setq ss (ssget "_X" ${textFilter}))
122
+ (if ss
123
+ (progn
124
+ (setq i 0)
125
+ (while (< i (sslength ss))
126
+ (setq e (ssname ss i))
127
+ (setq elist (entget e))
128
+ (setq old (cdr (assoc 1 elist)))
129
+ (if old
130
+ (progn
131
+ (setq new (vl-string-subst repl-text find-pat old))
132
+ (if (/= new old)
133
+ (progn
134
+ (setq elist (subst (cons 1 new) (assoc 1 elist) elist))
135
+ (entmod elist)
136
+ (setq count (1+ count))
137
+ )
138
+ )
139
+ )
140
+ )
141
+ (setq i (1+ i))
142
+ )
143
+ )
144
+ )
145
+ (setq result (cons (cons "count" count) result))
146
+ (command "_.QSAVE")
147
+ (command "_.CLOSE" "_Y")
148
+ )
149
+ (setq result (cons (cons "ok" nil) (cons (cons "error" "文件不存在") result)))
150
+ )
151
+ (setq all-results (cons result all-results))
152
+ )
153
+ (princ (vl-prin1-to-string all-results))
154
+ (princ))
155
+ `;
156
+
157
+ try {
158
+ const raw = await cad.sendCommandWithResult(lisp);
159
+ let results;
160
+ try {
161
+ results = typeof raw === 'string' ? JSON.parse(raw) : raw;
162
+ } catch {
163
+ results = [{ file: 'parse_error', ok: false, error: String(raw).substring(0, 200) }];
164
+ }
165
+ const total = results.reduce((s, r) => s + (r.count || 0), 0);
166
+ const text = `替换完成: 共处理 ${results.length} 个文件,${total} 处替换\n${JSON.stringify(results, null, 2)}`;
167
+ return { content: [{ type: 'text', text }] };
168
+ } catch (e) {
169
+ return { content: [{ type: 'text', text: `批量替换失败: ${e.message}` }], isError: true };
170
+ }
171
+ }
172
+
173
+ export async function batchDocCleanup(args) {
174
+ ensureCad();
175
+ const { files, purgeBlocks, purgeLayers, purgeStyles, runAudit, runOverkill, saveAndClose } = args;
176
+ if (!files?.length) return { content: [{ type: 'text', text: '需要 files 参数' }], isError: true };
177
+
178
+ const fileList = buildFileList(files);
179
+ const doBlocks = purgeBlocks !== false;
180
+ const doLayers = purgeLayers !== false;
181
+ const doStyles = purgeStyles !== false;
182
+ const doAudit = runAudit !== false;
183
+
184
+ const lisp = `(progn
185
+ (vl-load-com)
186
+ (setq files (list ${fileList}))
187
+ (setq all-results (list))
188
+ (foreach f files
189
+ (setq result (list (cons "file" f) (cons "ok" T)))
190
+ (if (findfile f)
191
+ (progn
192
+ (command "._OPEN" f)
193
+ (setq details (list))
194
+ ;; audit
195
+ (if ${doAudit}
196
+ (progn
197
+ (command "_.AUDIT" "_Y")
198
+ (setq details (cons "audit_done" details))
199
+ )
200
+ )
201
+ ;; purge
202
+ (if ${doBlocks} (command "_.PURGE" "_B" "*" "_N"))
203
+ (if ${doLayers} (command "_.PURGE" "_LA" "*" "_N"))
204
+ (if ${doStyles} (command "_.PURGE" "_ST" "*" "_N"))
205
+ (setq details (cons "purge_done" details))
206
+ ;; overkill
207
+ (if ${runOverkill}
208
+ (progn
209
+ (command "_.OVERKILL" (ssget "_X") "" "_Y")
210
+ (setq details (cons "overkill_done" details))
211
+ )
212
+ )
213
+ (setq result (cons (cons "actions" details) result))
214
+ (if ${saveAndClose}
215
+ (progn
216
+ (command "_.QSAVE")
217
+ (command "_.CLOSE" "_Y")
218
+ )
219
+ (command "_.CLOSE" "_N")
220
+ )
221
+ )
222
+ (setq result (cons (cons "ok" nil) (cons (cons "error" "文件不存在") result)))
223
+ )
224
+ (setq all-results (cons result all-results))
225
+ )
226
+ (princ (vl-prin1-to-string all-results))
227
+ (princ))
228
+ `;
229
+
230
+ try {
231
+ const raw = await cad.sendCommandWithResult(lisp);
232
+ let results;
233
+ try {
234
+ results = typeof raw === 'string' ? JSON.parse(raw) : raw;
235
+ } catch {
236
+ results = [{ file: 'parse_error', ok: false, error: String(raw).substring(0, 200) }];
237
+ }
238
+ return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }] };
239
+ } catch (e) {
240
+ return { content: [{ type: 'text', text: `批量清理失败: ${e.message}` }], isError: true };
241
+ }
242
+ }
@@ -1,3 +1,4 @@
1
+ import iconv from 'iconv-lite';
1
2
  import { cad } from '../cad.js';
2
3
  import { log } from '../logger.js';
3
4
  import { withCadConnection, mcpSuccess, mcpError, escapeLispString } from '../handler-utils.js';
@@ -32,9 +33,15 @@ export const evalLisp = withCadConnection(async (code, withResult = false, encod
32
33
  }
33
34
 
34
35
  if (withResult) {
35
- const result = await cad.sendCommandWithResult(trimmed, encoding);
36
- if (result !== null) {
37
- return mcpSuccess(result);
36
+ const useRaw = encoding ? true : false;
37
+ const resp = await cad.sendCommandWithResult(trimmed, encoding, useRaw);
38
+ if (resp !== null) {
39
+ if (useRaw && resp.rawBase64) {
40
+ const buf = Buffer.from(resp.rawBase64, 'base64');
41
+ const decoded = iconv.decode(buf, encoding);
42
+ return mcpSuccess(decoded);
43
+ }
44
+ return mcpSuccess(resp);
38
45
  }
39
46
  return mcpError('执行失败或无返回值');
40
47
  }
@@ -0,0 +1,70 @@
1
+ import { tools } from '../tools/index.js';
2
+ import { listPrompts, getPrompt } from './prompt-handlers.js';
3
+
4
+ export async function search_tools(a) {
5
+ const q = (a.query || '').toLowerCase();
6
+ const results = tools.filter(t => t.name.toLowerCase().includes(q) || (t.description || '').toLowerCase().includes(q));
7
+ const text = results.length ? `找到 ${results.length} 个工具:\n${results.map(t => `${t.name}: ${t.description}`).join('\n')}` : `未找到包含 "${a.query}" 的工具`;
8
+ return { content: [{ type: 'text', text }] };
9
+ }
10
+
11
+ export async function list_tools_by_category(a) {
12
+ const categories = {};
13
+ for (const t of tools) {
14
+ const cat = t.annotations?.category || '其他';
15
+ if (!categories[cat]) categories[cat] = [];
16
+ categories[cat].push(t.name);
17
+ }
18
+ if (a.category) {
19
+ const names = categories[a.category];
20
+ return { content: [{ type: 'text', text: names ? `${a.category} (${names.length}):\n${names.join('\n')}` : `未找到分类: ${a.category}` }] };
21
+ }
22
+ const lines = [];
23
+ for (const [cat, names] of Object.entries(categories)) {
24
+ lines.push(`## ${cat} (${names.length})`);
25
+ }
26
+ lines.push(`\n共 ${Object.keys(categories).length} 个分类`);
27
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
28
+ }
29
+
30
+ export async function get_tool_info(a) {
31
+ if (!a.name) return { content: [{ type: 'text', text: '需要 name 参数(工具名称)' }], isError: true };
32
+ const tool = tools.find(t => t.name === a.name);
33
+ if (!tool) return { content: [{ type: 'text', text: `未找到工具: ${a.name}` }], isError: true };
34
+
35
+ const ann = tool.annotations || {};
36
+ const required = (tool.inputSchema?.required || []).join(', ') || '无';
37
+ const props = tool.inputSchema?.properties || {};
38
+ const params = Object.entries(props).map(([k, v]) => {
39
+ const req = required.includes(k) ? '必填' : '可选';
40
+ const desc = v.description || '';
41
+ return ` ${k} (${req}): ${desc}`;
42
+ }).join('\n');
43
+
44
+ const lines = [
45
+ `## ${tool.name}`,
46
+ `描述: ${tool.description || '无'}`,
47
+ `分类: ${ann.category || '其他'}`,
48
+ `只读: ${ann.readOnlyHint ? '是' : '否'}`,
49
+ `破坏性: ${ann.destructiveHint ? '是' : '否'}`,
50
+ `幂等: ${ann.idempotentHint ? '是' : '否'}`,
51
+ `开放世界: ${ann.openWorldHint ? '是' : '否'}`,
52
+ ``,
53
+ `参数:`,
54
+ params || ' 无参数',
55
+ ];
56
+ if (ann.deprecatedReason) {
57
+ lines.push(`\n已弃用: ${ann.deprecatedReason}`);
58
+ }
59
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
60
+ }
61
+
62
+ export async function list_prompts_handler() {
63
+ const prompts = await listPrompts();
64
+ return { content: [{ type: 'text', text: JSON.stringify(prompts) }] };
65
+ }
66
+
67
+ export async function get_prompt_handler(a) {
68
+ const prompt = await getPrompt(a.name, a.args || {});
69
+ return { content: prompt.messages.map(m => ({ type: 'text', text: m.content.text || m.content })) };
70
+ }
@@ -1,18 +1,36 @@
1
1
  import config from '../config.js';
2
2
 
3
- const CACHE_TTL = config.resourceCacheTtl;
4
- const resourceCache = new Map();
5
3
  const CLEANUP_INTERVAL = 60000;
6
4
 
7
5
  let cleanupTimer = null;
6
+ let _instanceKey = null;
7
+
8
+ // instance -> Map(key -> {data, timestamp})
9
+ const _instanceCaches = new Map();
10
+ const _defaultKey = '__default__';
11
+
12
+ function _getCache() {
13
+ const key = _instanceKey || _defaultKey;
14
+ if (!_instanceCaches.has(key)) {
15
+ _instanceCaches.set(key, new Map());
16
+ }
17
+ return _instanceCaches.get(key);
18
+ }
19
+
20
+ function _ttl() {
21
+ return config.resourceCacheTtl;
22
+ }
8
23
 
9
24
  function ensureCleanup() {
10
25
  if (cleanupTimer) return;
11
26
  cleanupTimer = setInterval(() => {
12
27
  const now = Date.now();
13
- for (const [key, entry] of resourceCache) {
14
- if (now - entry.timestamp >= CACHE_TTL) {
15
- resourceCache.delete(key);
28
+ const ttl = _ttl();
29
+ for (const cache of _instanceCaches.values()) {
30
+ for (const [key, entry] of cache) {
31
+ if (now - entry.timestamp >= ttl) {
32
+ cache.delete(key);
33
+ }
16
34
  }
17
35
  }
18
36
  }, CLEANUP_INTERVAL);
@@ -21,7 +39,17 @@ function ensureCleanup() {
21
39
 
22
40
  ensureCleanup();
23
41
 
24
- // 每个资源 URI 对应的 cache key 前缀映射
42
+ export function stopCleanup() {
43
+ if (cleanupTimer) {
44
+ clearInterval(cleanupTimer);
45
+ cleanupTimer = null;
46
+ }
47
+ }
48
+
49
+ export function setInstanceKey(key) {
50
+ _instanceKey = key || null;
51
+ }
52
+
25
53
  const URI_CACHE_PREFIX = {
26
54
  'atlisp://cad/info': 'cad:info',
27
55
  'atlisp://dwg/entities': 'dwg:entities',
@@ -35,28 +63,32 @@ const URI_CACHE_PREFIX = {
35
63
  };
36
64
 
37
65
  function getCached(key) {
38
- const entry = resourceCache.get(key);
39
- if (entry && Date.now() - entry.timestamp < CACHE_TTL) {
66
+ const cache = _getCache();
67
+ const entry = cache.get(key);
68
+ if (entry && Date.now() - entry.timestamp < _ttl()) {
40
69
  return entry.data;
41
70
  }
42
- resourceCache.delete(key);
71
+ cache.delete(key);
43
72
  return null;
44
73
  }
45
74
 
46
75
  function setCache(key, data) {
47
- resourceCache.set(key, { data, timestamp: Date.now() });
76
+ _getCache().set(key, { data, timestamp: Date.now() });
48
77
  }
49
78
 
50
79
  export function clearCache(uri) {
51
- const prefix = URI_CACHE_PREFIX[uri];
52
- if (prefix) {
53
- for (const key of resourceCache.keys()) {
54
- if (key === prefix || key.startsWith(prefix + ':')) {
55
- resourceCache.delete(key);
80
+ const basePrefix = URI_CACHE_PREFIX[uri];
81
+ if (basePrefix) {
82
+ const cache = _getCache();
83
+ for (const key of cache.keys()) {
84
+ if (key === basePrefix || key.startsWith(basePrefix + ':')) {
85
+ cache.delete(key);
56
86
  }
57
87
  }
58
88
  } else if (!uri) {
59
- resourceCache.clear();
89
+ const key = _instanceKey || _defaultKey;
90
+ _instanceCaches.delete(key);
91
+ _instanceCaches.set(key, new Map());
60
92
  }
61
93
  }
62
94
 
@@ -22,6 +22,9 @@ export const RESOURCES = [
22
22
  { uri: 'atlisp://standards/coding', name: 'Coding Standards', description: '@lisp 编码规范', mimeType: 'application/json', subscribable: false },
23
23
  { uri: 'atlisp://dwg/context', name: 'DWG Context', description: '当前图纸上下文摘要。返回文档名/路径/布局/UCS/图层列表/实体统计/系统变量组合信息,帮助快速了解当前 CAD 状态', mimeType: 'application/json', subscribable: true },
24
24
  { uri: 'atlisp://tools/relationships', name: 'Tool Relationships', description: '工具依赖关系图。描述工具间的调用链关系(如 select_entities → batch_delete),帮助理解工具组合使用方式', mimeType: 'application/json', subscribable: false },
25
+ { uri: 'atlisp://dwg/analysis', name: 'Drawing Analysis', description: '当前图纸综合分析:实体类型统计、块使用统计、图层摘要、文字统计、系统变量快照。支持 ?type=summary(默认)或 ?type=detailed', mimeType: 'application/json', subscribable: true },
26
+ { uri: 'atlisp://tools/categories', name: 'Tool Categories', description: '工具分类概览。列出所有分类及其包含的工具数量', mimeType: 'application/json', subscribable: false },
27
+ { uri: 'atlisp://dwg/standards/check', name: 'Drawing Standards Check', description: '检查当前图纸是否符合加载的制图规范(图层命名、颜色、线型等规则)', mimeType: 'application/json', subscribable: false },
25
28
  ];
26
29
 
27
30
  export const TBL_INFO = {