@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/README.md +52 -4
- package/dist/atlisp-mcp.js +1443 -857
- package/dist/cad-rothelper.dll +0 -0
- package/dist/cad-worker.js +239 -176
- package/dist/config.js +59 -56
- package/dist/handlers/batch-doc-handlers.js +242 -0
- package/dist/handlers/cad-handlers.js +10 -3
- package/dist/handlers/meta-handlers.js +70 -0
- package/dist/handlers/resource-cache.js +48 -16
- package/dist/handlers/resource-defs.js +3 -0
- package/dist/handlers/resource-handlers.js +123 -0
- package/dist/handlers/skill-handlers.js +152 -0
- package/dist/lisp-security.js +65 -0
- package/dist/prompts/definitions/workflow-batch-doc.json +27 -0
- package/package.json +3 -2
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.
|
|
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.
|
|
12
|
-
MESSAGE_TIMEOUT: z.
|
|
13
|
-
HEARTBEAT_INTERVAL: z.
|
|
14
|
-
BUSY_RETRIES: z.
|
|
15
|
-
BUSY_DELAY: z.
|
|
16
|
-
ENABLE_CORS: z.
|
|
17
|
-
CORS_ORIGIN: z.string().optional().default('
|
|
18
|
-
RATE_LIMIT_WINDOW: z.
|
|
19
|
-
RATE_LIMIT_MAX: z.
|
|
20
|
-
RESOURCE_CACHE_TTL: z.
|
|
21
|
-
FUNCTION_LIB_CACHE_TTL: z.
|
|
22
|
-
DEBUG: z.
|
|
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.
|
|
27
|
-
METRICS_ENABLED: z.
|
|
28
|
-
REQUEST_LOG_ENABLED: z.
|
|
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.
|
|
35
|
-
LLM_TEMPERATURE: z.
|
|
36
|
-
WORKER_POOL_SIZE: z.
|
|
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.
|
|
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
|
|
170
|
-
host: fileConfig?.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
|
|
173
|
-
enableSse: fileConfig?.enableSse ??
|
|
174
|
-
messageTimeout: fileConfig?.messageTimeout
|
|
175
|
-
heartbeatInterval: fileConfig?.heartbeatInterval
|
|
176
|
-
busyRetries: fileConfig?.busyRetries
|
|
177
|
-
busyDelay: fileConfig?.busyDelay
|
|
178
|
-
enableCors: fileConfig?.enableCors ??
|
|
179
|
-
corsOrigin: fileConfig?.corsOrigin
|
|
180
|
-
rateLimitWindow: fileConfig?.rateLimitWindow
|
|
181
|
-
rateLimitMax: fileConfig?.rateLimitMax
|
|
182
|
-
resourceCacheTtl: fileConfig?.resourceCacheTtl
|
|
183
|
-
functionLibCacheTtl: fileConfig?.functionLibCacheTtl
|
|
184
|
-
debug: fileConfig?.debug ??
|
|
185
|
-
debugFile: fileConfig?.debugFile
|
|
186
|
-
functionLibUrl: fileConfig?.functionLibUrl
|
|
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
|
|
189
|
-
apiKeys: fileConfig?.apiKeys
|
|
190
|
-
metricsEnabled: fileConfig?.metricsEnabled ??
|
|
191
|
-
requestLogEnabled: fileConfig?.requestLogEnabled ??
|
|
192
|
-
sslKey: fileConfig?.sslKey
|
|
193
|
-
sslCert: fileConfig?.sslCert
|
|
194
|
-
llmApiKey: fileConfig?.llmApiKey
|
|
195
|
-
llmApiUrl: fileConfig?.llmApiUrl
|
|
196
|
-
llmModel: fileConfig?.llmModel
|
|
197
|
-
llmMaxTokens: fileConfig?.llmMaxTokens
|
|
198
|
-
llmTemperature: fileConfig?.llmTemperature
|
|
199
|
-
maxPoolSize: fileConfig?.maxPoolSize
|
|
200
|
-
workerPoolSize: fileConfig?.workerPoolSize
|
|
201
|
-
lintPreset: fileConfig?.lintPreset
|
|
202
|
-
lintConfigPath: fileConfig?.lintConfigPath
|
|
203
|
-
lintLevel: fileConfig?.lintLevel
|
|
204
|
-
noLint: fileConfig?.noLint ?? cliArgs.noLint ??
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
|
39
|
-
|
|
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
|
-
|
|
71
|
+
cache.delete(key);
|
|
43
72
|
return null;
|
|
44
73
|
}
|
|
45
74
|
|
|
46
75
|
function setCache(key, data) {
|
|
47
|
-
|
|
76
|
+
_getCache().set(key, { data, timestamp: Date.now() });
|
|
48
77
|
}
|
|
49
78
|
|
|
50
79
|
export function clearCache(uri) {
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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 = {
|