@atlisp/mcp 1.8.18 → 1.8.20

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 (44) hide show
  1. package/dist/atlisp-mcp.js +3969 -2535
  2. package/dist/cad-worker.js +53 -19
  3. package/dist/config.js +41 -2
  4. package/dist/handlers/batch-transaction-handlers.js +70 -0
  5. package/dist/handlers/cad-handlers.js +66 -91
  6. package/dist/handlers/draw-handlers.js +118 -165
  7. package/dist/handlers/entity-handlers.js +61 -111
  8. package/dist/handlers/layer-handlers.js +70 -98
  9. package/dist/handlers/lint-handlers.js +64 -0
  10. package/dist/handlers/resource-defs.js +2 -0
  11. package/dist/handlers/resource-handlers.js +1288 -26
  12. package/dist/lisp-security.js +0 -17
  13. package/dist/pipelines/analyze-and-report.json +16 -0
  14. package/dist/pipelines/batch-export-pdfs.json +11 -0
  15. package/dist/pipelines/batch-layer-move.json +19 -0
  16. package/dist/pipelines/export-current-drawing.json +18 -0
  17. package/dist/pipelines/purge-and-save.json +16 -0
  18. package/dist/prompts/definitions/analyze-drawing.json +6 -0
  19. package/dist/prompts/definitions/batch-draw-lines.json +14 -0
  20. package/dist/prompts/definitions/coding-conventions.json +11 -0
  21. package/dist/prompts/definitions/color-convert.json +30 -0
  22. package/dist/prompts/definitions/curve-analysis.json +26 -0
  23. package/dist/prompts/definitions/excel-report.json +9 -0
  24. package/dist/prompts/definitions/export-entities.json +17 -0
  25. package/dist/prompts/definitions/generate-dimension.json +8 -0
  26. package/dist/prompts/definitions/geometry-calc.json +9 -0
  27. package/dist/prompts/definitions/json-exchange.json +22 -0
  28. package/dist/prompts/definitions/manage-blocks.json +28 -0
  29. package/dist/prompts/definitions/manage-layers.json +35 -0
  30. package/dist/prompts/definitions/pickset-filter.json +31 -0
  31. package/dist/prompts/definitions/string-process.json +40 -0
  32. package/dist/prompts/definitions/test-autolisp.json +33 -0
  33. package/dist/prompts/definitions/text-process.json +26 -0
  34. package/dist/prompts/definitions/workflow-3d-model.json +24 -0
  35. package/dist/prompts/definitions/workflow-batch-operations.json +24 -0
  36. package/dist/prompts/definitions/workflow-block-manage.json +32 -0
  37. package/dist/prompts/definitions/workflow-drawing-export.json +23 -0
  38. package/dist/prompts/definitions/workflow-entity-analysis.json +23 -0
  39. package/dist/prompts/definitions/workflow-layer-management.json +38 -0
  40. package/dist/prompts/definitions/workflow-plot-publish.json +28 -0
  41. package/dist/prompts/definitions/workflow-sheetset.json +29 -0
  42. package/dist/prompts/definitions/workflow-text-process.json +29 -0
  43. package/dist/prompts/definitions/workflow-xref-manage.json +28 -0
  44. package/package.json +7 -3
@@ -13,19 +13,30 @@ const BUSY_DELAY = parseInt(process.env.BUSY_DELAY || '500', 10);
13
13
  const LISP_CACHE = new Map();
14
14
  const LISP_CACHE_MAX = parseInt(process.env.LISP_CACHE_MAX || '500', 10);
15
15
 
16
+ const CACHE_TTL = parseInt(process.env.LISP_CACHE_TTL || '300000', 10);
17
+
16
18
  function getCachedLisp(code) {
17
19
  const hash = crypto.createHash('sha256').update(code).digest('hex');
18
- if (LISP_CACHE.has(hash)) return LISP_CACHE.get(hash);
19
- return null;
20
+ const entry = LISP_CACHE.get(hash);
21
+ if (!entry) return null;
22
+ if (Date.now() - entry.ts > CACHE_TTL) {
23
+ LISP_CACHE.delete(hash);
24
+ return null;
25
+ }
26
+ entry.ts = Date.now();
27
+ return entry.compiled;
20
28
  }
21
29
 
22
30
  function setCachedLisp(code, compiled) {
23
31
  const hash = crypto.createHash('sha256').update(code).digest('hex');
24
32
  if (LISP_CACHE.size >= LISP_CACHE_MAX) {
25
- const firstKey = LISP_CACHE.keys().next().value;
26
- LISP_CACHE.delete(firstKey);
33
+ let oldest = null;
34
+ for (const [k, v] of LISP_CACHE) {
35
+ if (!oldest || v.ts < oldest.ts) oldest = { k, ts: v.ts };
36
+ }
37
+ if (oldest) LISP_CACHE.delete(oldest.k);
27
38
  }
28
- LISP_CACHE.set(hash, compiled);
39
+ LISP_CACHE.set(hash, { compiled, ts: Date.now() });
29
40
  return compiled;
30
41
  }
31
42
 
@@ -151,16 +162,34 @@ const cadSendWithResult = edge.func(function() {/*
151
162
  string code = d.code.ToString();
152
163
  string platform = d.platform.ToString();
153
164
  string encoding = d.encoding != null ? d.encoding.ToString() : "";
165
+ string codeHash = d.codeHash != null ? d.codeHash.ToString() : "";
166
+ bool cacheEnabled = d.cacheEnabled != null && d.cacheEnabled.ToString() == "True";
167
+ bool hasCacheKey = cacheEnabled && !string.IsNullOrEmpty(codeHash);
154
168
  string progId = platform == "BricsCAD" ? "BricscadApp.AcadApplication" : platform + ".Application";
155
169
  string tempDir = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "@lisp", "tmp");
156
170
  System.IO.Directory.CreateDirectory(tempDir);
157
171
 
172
+ // 结果文件始终使用 GUID 命名(每次调用唯一)
158
173
  tempFile = System.IO.Path.Combine(tempDir, "atlisp_result_" + System.Guid.NewGuid().ToString("N") + ".txt");
159
- lispFile = System.IO.Path.Combine(tempDir, "atlisp_code_" + System.Guid.NewGuid().ToString("N") + ".lsp");
160
- userCodeFile = System.IO.Path.Combine(tempDir, "atlisp_user_" + System.Guid.NewGuid().ToString("N") + ".lsp");
161
174
 
162
- // 用户代码单独写入文件,避免拼接注入风险
163
- System.IO.File.WriteAllText(userCodeFile, code, System.Text.Encoding.GetEncoding(System.Globalization.CultureInfo.CurrentCulture.TextInfo.ANSICodePage));
175
+ // 代码文件:启用缓存时使用 hash 命名,否则使用 GUID 命名
176
+ string fileKey = hasCacheKey ? codeHash : System.Guid.NewGuid().ToString("N");
177
+ userCodeFile = System.IO.Path.Combine(tempDir, "atlisp_user_" + fileKey + ".lsp");
178
+ lispFile = System.IO.Path.Combine(tempDir, "atlisp_code_" + fileKey + ".lsp");
179
+
180
+ bool useCachedUserCode = false;
181
+ if (hasCacheKey) {
182
+ if (System.IO.File.Exists(userCodeFile)) {
183
+ var userFileInfo = new System.IO.FileInfo(userCodeFile);
184
+ if ((DateTime.Now - userFileInfo.LastWriteTime).TotalHours < 1) {
185
+ useCachedUserCode = true;
186
+ }
187
+ }
188
+ }
189
+
190
+ if (!useCachedUserCode) {
191
+ System.IO.File.WriteAllText(userCodeFile, code, System.Text.Encoding.GetEncoding(System.Globalization.CultureInfo.CurrentCulture.TextInfo.ANSICodePage));
192
+ }
164
193
 
165
194
  string lispCode = "(progn"
166
195
  + "(setq f(open \"" + tempFile.Replace("\\", "\\\\") + "\" \"w\"))"
@@ -171,7 +200,6 @@ string encoding = d.encoding != null ? d.encoding.ToString() : "";
171
200
  + "(close f)"
172
201
  + "(princ))";
173
202
 
174
- // 使用系统 ANSI 编码以兼容 GstarCAD / ZWCAD
175
203
  System.IO.File.WriteAllText(lispFile, lispCode, System.Text.Encoding.GetEncoding(System.Globalization.CultureInfo.CurrentCulture.TextInfo.ANSICodePage));
176
204
 
177
205
  dynamic cad = System.Runtime.InteropServices.Marshal.GetActiveObject(progId);
@@ -193,11 +221,9 @@ string encoding = d.encoding != null ? d.encoding.ToString() : "";
193
221
  byte[] bytes = System.IO.File.ReadAllBytes(tempFile);
194
222
  System.Text.Encoding enc;
195
223
 
196
- // 优先使用指定的编码
197
224
  if (!string.IsNullOrEmpty(encoding)) {
198
225
  enc = System.Text.Encoding.GetEncoding(encoding);
199
226
  } else {
200
- // 自动检测编码:先尝试 UTF-8,如果失败则使用 GBK
201
227
  try {
202
228
  string utf8Result = System.Text.Encoding.UTF8.GetString(bytes);
203
229
  if (!utf8Result.Contains("\ufffd")) {
@@ -216,14 +242,13 @@ string encoding = d.encoding != null ? d.encoding.ToString() : "";
216
242
 
217
243
  result = enc.GetString(bytes).Trim();
218
244
 
219
- // 检查是否有错误前缀
220
245
  if (result.StartsWith("ERROR:")) {
221
246
  return new { success = false, error = result.Substring(6).Trim() };
222
247
  }
223
248
 
224
249
  try { System.IO.File.Delete(tempFile); } catch {}
225
250
  try { System.IO.File.Delete(lispFile); } catch {}
226
- try { System.IO.File.Delete(userCodeFile); } catch {}
251
+ if (!hasCacheKey) { try { if (System.IO.File.Exists(userCodeFile)) System.IO.File.Delete(userCodeFile); } catch {} }
227
252
  break;
228
253
  } catch (System.IO.IOException) {
229
254
  System.Threading.Thread.Sleep(200);
@@ -471,8 +496,6 @@ async function waitForIdle(platform) {
471
496
  return false;
472
497
  }
473
498
 
474
- export { checkParens } from './lisp-security.js';
475
-
476
499
  async function connectToPlatform(targetPlatform) {
477
500
  return new Promise((resolve, reject) => {
478
501
  cadConnectByName(targetPlatform, (e, r) => {
@@ -482,6 +505,13 @@ async function connectToPlatform(targetPlatform) {
482
505
  });
483
506
  }
484
507
 
508
+ function computeCodeHash(code, cacheEnabled) {
509
+ if (cacheEnabled) {
510
+ return crypto.createHash('sha256').update(code).digest('hex');
511
+ }
512
+ return '';
513
+ }
514
+
485
515
  async function handleMessage(msg) {
486
516
  if (msg.type === 'ping') {
487
517
  return { pong: true };
@@ -554,9 +584,11 @@ async function handleMessage(msg) {
554
584
  return { success: false, error: `安全限制: ${injectionCheck.error}` };
555
585
  }
556
586
  if (msg.code.length > 255) {
557
- log(`send (long code ${msg.code.length} chars, fallback to sendResult): ${msg.code.substring(0, 80)}...`);
587
+ const cacheEnabled = process.env.LONG_CODE_CACHE === '1';
588
+ const codeHash = computeCodeHash(msg.code, cacheEnabled);
589
+ log(`send (long code ${msg.code.length} chars, fallback to sendResult, cache=${cacheEnabled}): ${msg.code.substring(0, 80)}...`);
558
590
  const r = await new Promise((resolve, reject) => {
559
- cadSendWithResult({ code: msg.code, platform: msg.platform || 'AutoCAD', encoding: '' }, (e, r) => {
591
+ cadSendWithResult({ code: msg.code, platform: msg.platform || 'AutoCAD', encoding: '', codeHash, cacheEnabled }, (e, r) => {
560
592
  if (e) reject(e);
561
593
  else resolve(r);
562
594
  });
@@ -606,6 +638,8 @@ async function handleMessage(msg) {
606
638
  if (!injectionCheck.valid) {
607
639
  return { success: false, error: `安全限制: ${injectionCheck.error}` };
608
640
  }
641
+ const cacheEnabled = process.env.LONG_CODE_CACHE === '1';
642
+ const codeHash = computeCodeHash(msg.code, cacheEnabled);
609
643
  const maxRetriesResult = 3;
610
644
  let lastErrorResult = null;
611
645
  for (let i = 0; i < maxRetriesResult; i++) {
@@ -614,7 +648,7 @@ async function handleMessage(msg) {
614
648
  log(`sendResult: ${msg.code}`);
615
649
  try {
616
650
  const r = await new Promise((resolve, reject) => {
617
- cadSendWithResult({ code: msg.code, platform: msg.platform, encoding: msg.encoding || '' }, (e, r) => {
651
+ cadSendWithResult({ code: msg.code, platform: msg.platform, encoding: msg.encoding || '', codeHash, cacheEnabled }, (e, r) => {
618
652
  if (e) reject(e);
619
653
  else resolve(r);
620
654
  });
package/dist/config.js CHANGED
@@ -33,6 +33,11 @@ const envSchema = z.object({
33
33
  LLM_MODEL: z.string().optional().default('gpt-4o-mini'),
34
34
  LLM_MAX_TOKENS: z.string().optional().default('4096'),
35
35
  LLM_TEMPERATURE: z.string().optional().default('0.7'),
36
+ WORKER_POOL_SIZE: z.string().optional().default('2'),
37
+ LINT_PRESET: z.string().optional().default(''),
38
+ LINT_CONFIG_PATH: z.string().optional().default(''),
39
+ LINT_LEVEL: z.string().optional().default(''),
40
+ NO_LINT: z.string().optional().default(''),
36
41
  });
37
42
 
38
43
  const configSchema = z.object({
@@ -71,6 +76,11 @@ const configSchema = z.object({
71
76
  llmMaxTokens: z.number().optional(),
72
77
  llmTemperature: z.number().optional(),
73
78
  maxPoolSize: z.number().optional(),
79
+ workerPoolSize: z.number().optional(),
80
+ lintPreset: z.string().optional(),
81
+ lintConfigPath: z.string().optional(),
82
+ lintLevel: z.string().optional(),
83
+ noLint: z.boolean().optional(),
74
84
  });
75
85
 
76
86
  function parseArgs() {
@@ -101,6 +111,17 @@ function parseArgs() {
101
111
  i++;
102
112
  } else if (args[i] === '--debug') {
103
113
  result.debug = true;
114
+ } else if (args[i] === '--lint-preset' && args[i + 1]) {
115
+ result.lintPreset = args[i + 1];
116
+ i++;
117
+ } else if (args[i] === '--lint-config' && args[i + 1]) {
118
+ result.lintConfigPath = args[i + 1];
119
+ i++;
120
+ } else if (args[i] === '--lint-level' && args[i + 1]) {
121
+ result.lintLevel = args[i + 1];
122
+ i++;
123
+ } else if (args[i] === '--no-lint' || args[i] === '--lint-off') {
124
+ result.noLint = true;
104
125
  }
105
126
  }
106
127
  return result;
@@ -171,8 +192,18 @@ const config = {
171
192
  llmMaxTokens: fileConfig?.llmMaxTokens || parseInt(env.LLM_MAX_TOKENS, 10),
172
193
  llmTemperature: fileConfig?.llmTemperature || parseFloat(env.LLM_TEMPERATURE),
173
194
  maxPoolSize: fileConfig?.maxPoolSize || 3,
195
+ workerPoolSize: fileConfig?.workerPoolSize || parseInt(env.WORKER_POOL_SIZE, 10),
196
+ lintPreset: fileConfig?.lintPreset || cliArgs.lintPreset || env.LINT_PRESET || undefined,
197
+ lintConfigPath: fileConfig?.lintConfigPath || cliArgs.lintConfigPath || env.LINT_CONFIG_PATH || undefined,
198
+ lintLevel: fileConfig?.lintLevel || cliArgs.lintLevel || env.LINT_LEVEL || undefined,
199
+ noLint: fileConfig?.noLint ?? cliArgs.noLint ?? (env.NO_LINT === 'true'),
174
200
  };
175
201
 
202
+ if (config.lintConfigPath) process.env.LINT_CONFIG_PATH = config.lintConfigPath;
203
+ if (config.lintPreset) process.env.LINT_PRESET = config.lintPreset;
204
+ if (config.lintLevel) process.env.LINT_LEVEL = config.lintLevel;
205
+ if (config.noLint) process.env.NO_LINT = 'true';
206
+
176
207
  export function validateConfig() {
177
208
  const errors = [];
178
209
  if (config.transport === 'http' || config.transport === 'ws') {
@@ -201,16 +232,20 @@ export function validateConfig() {
201
232
  }
202
233
 
203
234
  let configWatchers = [];
235
+ let configReloadLock = false;
204
236
 
205
237
  if (configFilePath && fs.existsSync(configFilePath)) {
206
238
  try {
207
239
  fs.watch(configFilePath, (eventType) => {
208
240
  if (eventType === 'change') {
241
+ if (configReloadLock) return;
242
+ configReloadLock = true;
209
243
  const newFileConfig = loadConfigFile(configFilePath);
210
244
  if (newFileConfig) {
211
245
  Object.assign(config, newFileConfig);
212
- configWatchers.forEach(fn => fn(config));
246
+ configWatchers.forEach(fn => { try { fn(config); } catch {} });
213
247
  }
248
+ configReloadLock = false;
214
249
  }
215
250
  });
216
251
  } catch (e) { console.error(`Config file watch error: ${e.message}`); }
@@ -225,12 +260,16 @@ export function getAllConfig() {
225
260
  }
226
261
 
227
262
  export function reloadConfig() {
263
+ if (configReloadLock) return false;
264
+ configReloadLock = true;
228
265
  const newFileConfig = loadConfigFile(config.configFile);
229
266
  if (newFileConfig) {
230
267
  Object.assign(config, newFileConfig);
231
- configWatchers.forEach(fn => fn(config));
268
+ configWatchers.forEach(fn => { try { fn(config); } catch {} });
269
+ configReloadLock = false;
232
270
  return true;
233
271
  }
272
+ configReloadLock = false;
234
273
  return false;
235
274
  }
236
275
 
@@ -0,0 +1,70 @@
1
+ import { cad } from '../cad.js';
2
+ import { log, error as logError } from '../logger.js';
3
+ import { mcpSuccess, mcpError, ensureCadConnected } from '../handler-utils.js';
4
+ import { handleToolCall } from '../mcp-tools.js';
5
+
6
+ export async function batchTransaction({ operations }) {
7
+ try {
8
+ await ensureCadConnected();
9
+
10
+ if (!operations || !Array.isArray(operations) || operations.length === 0) {
11
+ return mcpError('operations 必须是非空数组');
12
+ }
13
+
14
+ log(`batch_transaction: starting ${operations.length} operations`);
15
+
16
+ await cad.sendCommand('(command "_.UNDO" "_BEGIN")');
17
+
18
+ const results = [];
19
+ let hasError = false;
20
+
21
+ for (let i = 0; i < operations.length; i++) {
22
+ const op = operations[i];
23
+ log(`batch_transaction: step ${i + 1}/${operations.length} -> ${op.tool}`);
24
+
25
+ try {
26
+ const result = await handleToolCall(op.tool, op.args || {});
27
+ results.push({
28
+ step: i,
29
+ tool: op.tool,
30
+ success: !result.isError,
31
+ result,
32
+ });
33
+ if (result.isError) {
34
+ hasError = true;
35
+ }
36
+ } catch (e) {
37
+ logError(`batch_transaction: step ${i} (${op.tool}) threw: ${e.message}`);
38
+ results.push({
39
+ step: i,
40
+ tool: op.tool,
41
+ success: false,
42
+ error: e.message,
43
+ });
44
+ hasError = true;
45
+ }
46
+ }
47
+
48
+ try {
49
+ await cad.sendCommand('(command "_.UNDO" "_END")');
50
+ } catch (e) {
51
+ logError(`batch_transaction: UNDO _END failed: ${e.message}`);
52
+ }
53
+
54
+ if (hasError) {
55
+ try {
56
+ await cad.sendCommand('(command "_.UNDO" "1")');
57
+ log('batch_transaction: rolled back due to errors');
58
+ } catch (e) {
59
+ logError(`batch_transaction: UNDO rollback failed: ${e.message}`);
60
+ }
61
+ return mcpError(`批量事务部分操作失败,已回滚: ${JSON.stringify(results)}`);
62
+ }
63
+
64
+ log(`batch_transaction: all ${operations.length} operations succeeded`);
65
+ return mcpSuccess(JSON.stringify(results));
66
+ } catch (e) {
67
+ logError(`batch_transaction: outer error: ${e.message}`);
68
+ return mcpError(`批量事务错误: ${e.message}`);
69
+ }
70
+ }
@@ -1,6 +1,7 @@
1
1
  import { cad } from '../cad.js';
2
2
  import { log } from '../logger.js';
3
- import { escapeLispString } from '../handler-utils.js';
3
+ import { withCadConnection, mcpSuccess, mcpError, escapeLispString } from '../handler-utils.js';
4
+ import { analyzeLispCode } from '../lint-analyzer.js';
4
5
 
5
6
  export async function connectCad(platform = null) {
6
7
  log(`connect_cad called${platform ? ` (target: ${platform})` : ''}, cad.connected before: ${cad.connected}`);
@@ -11,92 +12,82 @@ export async function connectCad(platform = null) {
11
12
  if (connected) {
12
13
  const messages = [`已连接到 ${cad.getPlatform()} ${cad.getVersion()}`];
13
14
  initAtlisp().catch(e => log(`connect_cad: initAtlisp failed: ${e.message}`));
14
- return { content: [{ type: 'text', text: messages.join(';') }] };
15
+ return mcpSuccess(messages.join(';'));
15
16
  }
16
- return { content: [{ type: 'text', text: '未能连接或启动 CAD,请确保 CAD 已安装' }], isError: true };
17
+ return mcpError('未能连接或启动 CAD,请确保 CAD 已安装');
17
18
  } catch (e) {
18
19
  log('connect_cad error: ' + e.message);
19
- return { content: [{ type: 'text', text: `连接 CAD 失败: ${e.message}` }], isError: true };
20
+ return mcpError(`连接 CAD 失败: ${e.message}`);
20
21
  }
21
22
  }
22
23
 
23
- export async function evalLisp(code, withResult = false, encoding = null) {
24
- if (!cad.connected) { await cad.connect(); }
25
- if (!cad.connected) {
26
- return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
27
- }
24
+ export const evalLisp = withCadConnection(async (code, withResult = false, encoding = null) => {
28
25
  const trimmed = (code || '').trim();
29
- if (!trimmed) return { content: [{ type: 'text', text: '命令为空,未发送' }] };
30
- try {
31
- if (withResult) {
32
- const result = await cad.sendCommandWithResult(trimmed, encoding);
33
- if (result !== null) {
34
- return { content: [{ type: 'text', text: result }] };
35
- }
36
- return { content: [{ type: 'text', text: '执行失败或无返回值' }], isError: true };
37
- } else {
38
- await cad.sendCommand(trimmed + '\n');
39
- return { content: [{ type: 'text', text: `已发送命令` }] };
26
+ if (!trimmed) return mcpSuccess('命令为空,未发送');
27
+
28
+ const analysis = analyzeLispCode(trimmed);
29
+ if (!analysis.valid) {
30
+ const detail = analysis.errors.map(e => ` L${e.line}: [${e.rule}] ${e.message}`).join('\n');
31
+ return mcpError(`代码存在安全问题:\n${detail}`);
32
+ }
33
+
34
+ if (withResult) {
35
+ const result = await cad.sendCommandWithResult(trimmed, encoding);
36
+ if (result !== null) {
37
+ return mcpSuccess(result);
40
38
  }
41
- } catch (e) {
42
- return { content: [{ type: 'text', text: `错误: ${e.message}` }], isError: true };
39
+ return mcpError('执行失败或无返回值');
43
40
  }
44
- }
41
+ await cad.sendCommand(trimmed + '\n');
42
+ return mcpSuccess('已发送命令');
43
+ });
45
44
 
46
- export async function getCadInfo() {
47
- if (!cad.connected) { await cad.connect(); }
48
- if (!cad.connected) return { content: [{ type: 'text', text: 'CAD 未连接' }] };
45
+ export const getCadInfo = withCadConnection(async () => {
49
46
  const info = await cad.getInfo();
50
- return { content: [{ type: 'text', text: info }] };
51
- }
47
+ return mcpSuccess(info);
48
+ });
52
49
 
53
- export async function atCommand(command) {
54
- if (!cad.connected) return { content: [{ type: 'text', text: '请先连接 CAD' }], isError: true };
50
+ export const atCommand = withCadConnection(async (command) => {
55
51
  const trimmed = (command || '').trim();
56
- if (!trimmed) return { content: [{ type: 'text', text: '命令为空,未发送' }] };
52
+ if (!trimmed) return mcpSuccess('命令为空,未发送');
53
+
54
+ const analysis = analyzeLispCode(trimmed);
55
+ if (!analysis.valid) {
56
+ const detail = analysis.errors.map(e => ` L${e.line}: [${e.rule}] ${e.message}`).join('\n');
57
+ return mcpError(`代码存在安全问题:\n${detail}`);
58
+ }
59
+
57
60
  await cad.sendCommand(trimmed + '\n');
58
- return { content: [{ type: 'text', text: '已发送命令' }] };
59
- }
61
+ return mcpSuccess('已发送命令');
62
+ });
60
63
 
61
- export async function newDocument() {
62
- if (!cad.connected) { await cad.connect(); }
63
- if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
64
+ export const newDocument = withCadConnection(async () => {
64
65
  const result = await cad.newDoc();
65
66
  if (result) {
66
- return { content: [{ type: 'text', text: '已在 CAD 中新建空白文档' }] };
67
+ return mcpSuccess('已在 CAD 中新建空白文档');
67
68
  }
68
- return { content: [{ type: 'text', text: '新建文档失败' }], isError: true };
69
- }
69
+ return mcpError('新建文档失败');
70
+ });
70
71
 
71
- export async function bringToFront() {
72
- if (!cad.connected) { await cad.connect(); }
73
- if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
72
+ export const bringToFront = withCadConnection(async () => {
74
73
  const result = await cad.bringToFront();
75
74
  if (result) {
76
- return { content: [{ type: 'text', text: '已将 CAD 窗口切换到前台' }] };
75
+ return mcpSuccess('已将 CAD 窗口切换到前台');
77
76
  }
78
- return { content: [{ type: 'text', text: '切换前台失败' }], isError: true };
79
- }
77
+ return mcpError('切换前台失败');
78
+ });
80
79
 
81
- export async function installAtlisp() {
82
- if (!cad.connected) { await cad.connect(); }
83
- if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
80
+ export const installAtlisp = withCadConnection(async () => {
84
81
  const installCode = '(progn(vl-load-com)(setq s strcat h "http" o(vlax-create-object (s"win"h".win"h"request.5.1"))v vlax-invoke e eval r read)(v o\'open "get" (s h"://atlisp.""cn/@"):vlax-true)(v o\'send)(v o\'WaitforResponse 1000)(e(r(vlax-get-property o\'ResponseText))))';
85
82
  await cad.sendCommand(installCode + '\n');
86
- return { content: [{ type: 'text', text: '已发送 @lisp 安装代码到 CAD' }] };
87
- }
83
+ return mcpSuccess('已发送 @lisp 安装代码到 CAD');
84
+ });
88
85
 
89
- export async function listFunctionsInCad() {
90
- if (!cad.connected) { await cad.connect(); }
91
- if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
92
- try {
93
- const result = await cad.sendCommandWithResult('(atoms-family 0)', null);
94
- if (result) return { content: [{ type: 'text', text: result }] };
95
- return { content: [{ type: 'text', text: 'CAD 函数列表为空' }] };
96
- } catch (e) {
97
- return { content: [{ type: 'text', text: `错误: ${e.message}` }], isError: true };
98
- }
99
- }
86
+ export const listFunctionsInCad = withCadConnection(async () => {
87
+ const result = await cad.sendCommandWithResult('(atoms-family 0)', null);
88
+ if (result) return mcpSuccess(result);
89
+ return mcpError('CAD 函数列表为空');
90
+ });
100
91
 
101
92
  export async function getSystemStatus() {
102
93
  const status = {
@@ -140,9 +131,7 @@ export async function getSystemStatus() {
140
131
  return { content: [{ type: 'text', text: JSON.stringify(status, null, 2) }] };
141
132
  }
142
133
 
143
- export async function initAtlisp() {
144
- if (!cad.connected) { await cad.connect(); }
145
- if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
134
+ export const initAtlisp = withCadConnection(async () => {
146
135
  const code = `(if (null @::load-module)
147
136
  (progn
148
137
  (vl-load-com)
@@ -151,30 +140,16 @@ export async function initAtlisp() {
151
140
  (v o'send)
152
141
  (v o'WaitforResponse 1000)
153
142
  (e(r(vlax-get o'ResponseText)))))`;
154
- try {
155
- await cad.sendCommand(code + '\n');
156
- return { content: [{ type: 'text', text: '已发送 @lisp 函数库加载代码到 CAD' }] };
157
- } catch (e) {
158
- return { content: [{ type: 'text', text: `错误: ${e.message}` }], isError: true };
159
- }
160
- }
161
- export async function getSystemVariable(name) {
162
- if (!cad.connected) { await cad.connect(); }
163
- if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
164
- try {
165
- const result = await cad.sendCommandWithResult(`(vl-princ-to-string (getvar "${escapeLispString(name)}"))`);
166
- return { content: [{ type: 'text', text: result || 'nil' }] };
167
- } catch (e) {
168
- return { content: [{ type: 'text', text: `错误: ${e.message}` }], isError: true };
169
- }
170
- }
171
- export async function setSystemVariable(name, value) {
172
- if (!cad.connected) { await cad.connect(); }
173
- if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
174
- try {
175
- await cad.sendCommand(`(setvar "${escapeLispString(name)}" ${value})\n`);
176
- return { content: [{ type: 'text', text: `已设置 ${name} = ${value}` }] };
177
- } catch (e) {
178
- return { content: [{ type: 'text', text: `错误: ${e.message}` }], isError: true };
179
- }
180
- }
143
+ await cad.sendCommand(code + '\n');
144
+ return mcpSuccess('已发送 @lisp 函数库加载代码到 CAD');
145
+ });
146
+
147
+ export const getSystemVariable = withCadConnection(async (name) => {
148
+ const result = await cad.sendCommandWithResult(`(vl-princ-to-string (getvar "${escapeLispString(name)}"))`);
149
+ return mcpSuccess(result || 'nil');
150
+ });
151
+
152
+ export const setSystemVariable = withCadConnection(async (name, value) => {
153
+ await cad.sendCommand(`(setvar "${escapeLispString(name)}" ${value})\n`);
154
+ return mcpSuccess(`已设置 ${name} = ${value}`);
155
+ });