@atlisp/mcp 1.8.23 → 1.8.25
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 +1422 -848
- package/dist/cad-rothelper.dll +0 -0
- package/dist/cad-worker.js +236 -174
- package/dist/config.js +59 -56
- 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/pipelines/analyze-and-report.json +0 -16
- package/dist/pipelines/batch-export-pdfs.json +0 -11
- package/dist/pipelines/batch-layer-move.json +0 -19
- package/dist/pipelines/export-current-drawing.json +0 -18
- package/dist/pipelines/purge-and-save.json +0 -16
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,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 = {
|
|
@@ -6,6 +6,7 @@ import { log } from '../logger.js';
|
|
|
6
6
|
import { parseLispList, parseLispRecursive, lispPairsToObject, escapeLispString } from '../handler-utils.js';
|
|
7
7
|
import { createError, ERROR_CODES } from '../errors.js';
|
|
8
8
|
import config from '../config.js';
|
|
9
|
+
import { tools } from '../tools/index.js';
|
|
9
10
|
import fs from 'fs';
|
|
10
11
|
import path from 'path';
|
|
11
12
|
|
|
@@ -1090,6 +1091,122 @@ async function getDwgContext() {
|
|
|
1090
1091
|
}
|
|
1091
1092
|
}
|
|
1092
1093
|
|
|
1094
|
+
async function getDrawingAnalysis(params = {}) {
|
|
1095
|
+
const snapshot = await getCadSnapshot();
|
|
1096
|
+
if (!snapshot || !snapshot.cadConnected) {
|
|
1097
|
+
return { connected: false, error: 'CAD 未连接' };
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
try {
|
|
1101
|
+
const detailedCode = `(progn
|
|
1102
|
+
(vl-load-com)
|
|
1103
|
+
(setq r nil)
|
|
1104
|
+
;; entity counts by type
|
|
1105
|
+
(if (setq ss (ssget "_X"))
|
|
1106
|
+
(progn
|
|
1107
|
+
(setq total (sslength ss) i 0 cnt nil)
|
|
1108
|
+
(while (< i total)
|
|
1109
|
+
(setq typ (cdr (assoc 0 (entget (ssname ss i)))))
|
|
1110
|
+
(setq cnt (if (assoc typ cnt) (subst (cons typ (1+ (cdr (assoc typ cnt)))) (assoc typ cnt) cnt) (cons (cons typ 1) cnt)))
|
|
1111
|
+
(setq i (1+ i)))
|
|
1112
|
+
(setq r (cons (cons "entityCount" total) r))
|
|
1113
|
+
(setq r (cons (cons "entityByType" cnt) r)))
|
|
1114
|
+
(setq r (cons (cons "entityCount" 0) r)))
|
|
1115
|
+
;; block usage
|
|
1116
|
+
(setq blocks nil)
|
|
1117
|
+
(vlax-for b (vla-get-blocks (vla-get-activedocument (vlax-get-acad-object)))
|
|
1118
|
+
(if (and (= (vla-get-IsXref b) :vlax-false) (/= (vla-get-Name b) "*Model_Space") (/= (vla-get-Name b) "*Paper_Space"))
|
|
1119
|
+
(setq blocks (cons (list "name" (vla-get-Name b) "count" (vla-get-Count b) "isDynamic" (vla-get-IsDynamicBlock b)) blocks))))
|
|
1120
|
+
(setq r (cons (cons "blockUsage" blocks) r))
|
|
1121
|
+
;; layer summary
|
|
1122
|
+
(setq layers nil le (tblnext "layer" t))
|
|
1123
|
+
(while le
|
|
1124
|
+
(setq ln (cdr (assoc 2 le)) lc (cdr (assoc 62 le)) ll (cdr (assoc 6 le)))
|
|
1125
|
+
(setq layers (cons (list "name" ln "color" (if (< lc 0) (- lc) lc) "off" (< lc 0) "linetype" ll) layers))
|
|
1126
|
+
(setq le (tblnext "layer" nil)))
|
|
1127
|
+
(setq r (cons (cons "layers" layers) r))
|
|
1128
|
+
;; text stats
|
|
1129
|
+
(if (setq ts (ssget "_X" '((0 . "TEXT,MTEXT"))))
|
|
1130
|
+
(progn
|
|
1131
|
+
(setq tc (sslength ts) th nil ti 0)
|
|
1132
|
+
(while (< ti tc)
|
|
1133
|
+
(setq h (cdr (assoc 40 (entget (ssname ts ti)))))
|
|
1134
|
+
(if h (setq th (cons h th)))
|
|
1135
|
+
(setq ti (1+ ti)))
|
|
1136
|
+
(setq th (vl-sort th '<))
|
|
1137
|
+
(setq r (cons (cons "textCount" tc) r))
|
|
1138
|
+
(setq r (cons (cons "textHeights" (list "min" (car th) "max" (last th) "median" (nth (fix (/ (length th) 2)) th))) r)))
|
|
1139
|
+
(setq r (cons (cons "textCount" 0) r)))
|
|
1140
|
+
;; system variables
|
|
1141
|
+
(setq r (cons (cons "sysvars" (list
|
|
1142
|
+
(cons "ltscale" (getvar "ltscale"))
|
|
1143
|
+
(cons "dimscale" (getvar "dimscale"))
|
|
1144
|
+
(cons "insunits" (getvar "insunits"))
|
|
1145
|
+
(cons "measurement" (getvar "measurement"))
|
|
1146
|
+
(cons "celtype" (getvar "celtype"))
|
|
1147
|
+
(cons "clayer" (getvar "clayer"))
|
|
1148
|
+
(cons "cecolor" (getvar "cecolor"))
|
|
1149
|
+
(cons "osmode" (getvar "osmode"))
|
|
1150
|
+
(cons "snapmode" (getvar "snapmode"))
|
|
1151
|
+
(cons "gridmode" (getvar "gridmode"))
|
|
1152
|
+
(cons "orthomode" (getvar "orthomode"))
|
|
1153
|
+
(cons "plinetype" (getvar "plinetype"))
|
|
1154
|
+
(cons "fillmode" (getvar "fillmode"))
|
|
1155
|
+
(cons "pickstyle" (getvar "pickstyle"))
|
|
1156
|
+
)) r))
|
|
1157
|
+
(vl-prin1-to-string (reverse r)))`;
|
|
1158
|
+
|
|
1159
|
+
const resultStr = await cad.sendCommandWithResult(detailedCode);
|
|
1160
|
+
const parsed = parseLispRecursive(resultStr);
|
|
1161
|
+
if (!parsed || !Array.isArray(parsed)) {
|
|
1162
|
+
return { connected: true, error: '分析结果解析失败' };
|
|
1163
|
+
}
|
|
1164
|
+
const analysis = {};
|
|
1165
|
+
for (const pair of parsed) {
|
|
1166
|
+
if (Array.isArray(pair) && pair.length >= 2) {
|
|
1167
|
+
analysis[String(pair[0])] = pair[1];
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
return { connected: true, ...analysis };
|
|
1171
|
+
} catch (e) {
|
|
1172
|
+
log(`getDrawingAnalysis error: ${e.message}`);
|
|
1173
|
+
return { connected: true, error: e.message };
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function getToolCategories() {
|
|
1178
|
+
const cats = {};
|
|
1179
|
+
for (const t of tools) {
|
|
1180
|
+
const cat = t.annotations?.category || '其他';
|
|
1181
|
+
if (!cats[cat]) cats[cat] = { count: 0, tools: [] };
|
|
1182
|
+
cats[cat].count++;
|
|
1183
|
+
cats[cat].tools.push(t.name);
|
|
1184
|
+
}
|
|
1185
|
+
return Object.entries(cats).map(([category, info]) => ({
|
|
1186
|
+
category, count: info.count, tools: info.tools,
|
|
1187
|
+
})).sort((a, b) => b.count - a.count);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
async function checkDrawingStandards() {
|
|
1191
|
+
const standards = getDraftingStandards();
|
|
1192
|
+
if (!standards || standards.error) {
|
|
1193
|
+
return { ok: false, error: standards?.error || '制图规范未加载' };
|
|
1194
|
+
}
|
|
1195
|
+
try {
|
|
1196
|
+
const issues = [];
|
|
1197
|
+
for (const rule of standards.layers || []) {
|
|
1198
|
+
const code = `(tblsearch "LAYER" "${escapeLispString(rule.name || rule.pattern)}")`;
|
|
1199
|
+
const exists = await cad.sendCommandWithResult(code);
|
|
1200
|
+
if (rule.required && (!exists || exists === 'nil' || exists === '')) {
|
|
1201
|
+
issues.push({ type: 'missing_layer', message: `缺少必需图层: ${rule.name}` });
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
return { ok: issues.length === 0, issues };
|
|
1205
|
+
} catch (e) {
|
|
1206
|
+
return { ok: false, error: e.message };
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1093
1210
|
function getToolRelationships() {
|
|
1094
1211
|
return {
|
|
1095
1212
|
selection: {
|
|
@@ -1385,6 +1502,12 @@ export async function readResource(uri) {
|
|
|
1385
1502
|
return await getDwgContext();
|
|
1386
1503
|
case 'atlisp://tools/relationships':
|
|
1387
1504
|
return getToolRelationships();
|
|
1505
|
+
case 'atlisp://dwg/analysis':
|
|
1506
|
+
return await getDrawingAnalysis(params);
|
|
1507
|
+
case 'atlisp://tools/categories':
|
|
1508
|
+
return getToolCategories();
|
|
1509
|
+
case 'atlisp://dwg/standards/check':
|
|
1510
|
+
return await checkDrawingStandards();
|
|
1388
1511
|
default:
|
|
1389
1512
|
throw new Error(`资源不存在: ${uri}`);
|
|
1390
1513
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
const SKILLS_DIR = path.join(os.homedir(), '.atlisp', 'skills');
|
|
6
|
+
const REGISTRY_URL = process.env.SKILL_REGISTRY_URL || 'https://raw.githubusercontent.com/atlisp/skill-registry/main/index.json';
|
|
7
|
+
const CACHE_PATH = path.join(os.homedir(), '.atlisp', 'skill-registry-cache.json');
|
|
8
|
+
|
|
9
|
+
function readCache() {
|
|
10
|
+
try { if (fs.existsSync(CACHE_PATH)) return JSON.parse(fs.readFileSync(CACHE_PATH, 'utf-8')); } catch {}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function writeCache(data) {
|
|
15
|
+
try { fs.writeFileSync(CACHE_PATH, JSON.stringify(data, null, 2), 'utf-8'); } catch {}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function fetchIndex() {
|
|
19
|
+
let index = readCache();
|
|
20
|
+
if (index && (Date.now() - index.fetchedAt < 3600000)) return index;
|
|
21
|
+
try {
|
|
22
|
+
const resp = await fetch(REGISTRY_URL, { headers: { 'User-Agent': 'atlisp-mcp' } });
|
|
23
|
+
if (resp.ok) {
|
|
24
|
+
index = await resp.json();
|
|
25
|
+
index.fetchedAt = Date.now();
|
|
26
|
+
writeCache(index);
|
|
27
|
+
}
|
|
28
|
+
} catch {}
|
|
29
|
+
return index;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function skill_search(a) {
|
|
33
|
+
const index = await fetchIndex();
|
|
34
|
+
if (!index?.skills) return { content: [{ type: 'text', text: '无法获取 skill 注册表索引' }], isError: true };
|
|
35
|
+
const q = (a.query || '').toLowerCase();
|
|
36
|
+
const results = index.skills.filter(s =>
|
|
37
|
+
s.name.toLowerCase().includes(q) ||
|
|
38
|
+
(s.description || '').toLowerCase().includes(q) ||
|
|
39
|
+
(s.tags || []).some(t => t.toLowerCase().includes(q))
|
|
40
|
+
);
|
|
41
|
+
if (!results.length) return { content: [{ type: 'text', text: `未找到匹配 "${a.query}" 的 skill` }] };
|
|
42
|
+
const lines = [`找到 ${results.length} 个 skill:\n`];
|
|
43
|
+
for (const s of results) {
|
|
44
|
+
lines.push(`**${s.name}**`);
|
|
45
|
+
if (s.description) lines.push(` 描述: ${s.description}`);
|
|
46
|
+
if (s.tags?.length) lines.push(` 标签: ${s.tags.join(', ')}`);
|
|
47
|
+
if (s.version) lines.push(` 版本: ${s.version}`);
|
|
48
|
+
lines.push('');
|
|
49
|
+
}
|
|
50
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function skill_install(a) {
|
|
54
|
+
if (!a.name) return { content: [{ type: 'text', text: '需要 skill 名称' }], isError: true };
|
|
55
|
+
const index = await fetchIndex();
|
|
56
|
+
if (!index?.skills) return { content: [{ type: 'text', text: '无法获取 skill 注册表索引' }], isError: true };
|
|
57
|
+
const skill = index.skills.find(s => s.name === a.name);
|
|
58
|
+
if (!skill) return { content: [{ type: 'text', text: `未找到 skill: ${a.name}` }], isError: true };
|
|
59
|
+
const downloadUrl = skill.download_url || skill.url;
|
|
60
|
+
if (!downloadUrl) return { content: [{ type: 'text', text: `Skill "${a.name}" 没有下载地址` }], isError: true };
|
|
61
|
+
let content;
|
|
62
|
+
try {
|
|
63
|
+
const resp = await fetch(downloadUrl, { headers: { 'User-Agent': 'atlisp-mcp' } });
|
|
64
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
65
|
+
content = await resp.text();
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return { content: [{ type: 'text', text: `下载失败: ${e.message}` }], isError: true };
|
|
68
|
+
}
|
|
69
|
+
try { fs.mkdirSync(SKILLS_DIR, { recursive: true }); } catch {}
|
|
70
|
+
const safeName = a.name.replace(/[^a-zA-Z0-9\u4e00-\u9fff_-]/g, '-').substring(0, 40);
|
|
71
|
+
const skillFile = path.join(SKILLS_DIR, `skill-${safeName}.md`);
|
|
72
|
+
if (fs.existsSync(skillFile)) {
|
|
73
|
+
return { content: [{ type: 'text', text: `Skill "${a.name}" 已安装 (${skillFile})` }], isError: true };
|
|
74
|
+
}
|
|
75
|
+
let finalContent = content;
|
|
76
|
+
if (!content.match(/^---\n/)) {
|
|
77
|
+
const tags = (skill.tags || []).join(', ');
|
|
78
|
+
finalContent = `---\nname: ${a.name}\ndescription: ${skill.description || ''}\ntags: ${tags}\n---\n\n${content}`;
|
|
79
|
+
}
|
|
80
|
+
fs.writeFileSync(skillFile, finalContent, 'utf-8');
|
|
81
|
+
return { content: [{ type: 'text', text: `${skill.name} 安装成功\n文件: ${skillFile}${skill.version ? `\n版本: ${skill.version}` : ''}` }] };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function skill_list_installed() {
|
|
85
|
+
if (!fs.existsSync(SKILLS_DIR)) return { content: [{ type: 'text', text: '暂无已安装的 skill' }] };
|
|
86
|
+
const results = [];
|
|
87
|
+
try {
|
|
88
|
+
const entries = fs.readdirSync(SKILLS_DIR, { withFileTypes: true });
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
91
|
+
const fullPath = path.join(SKILLS_DIR, entry.name);
|
|
92
|
+
try {
|
|
93
|
+
const raw = fs.readFileSync(fullPath, 'utf-8');
|
|
94
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
95
|
+
if (match) {
|
|
96
|
+
const fm = {};
|
|
97
|
+
for (const line of match[1].split('\n')) {
|
|
98
|
+
const sep = line.indexOf(':');
|
|
99
|
+
if (sep > 0) fm[line.slice(0, sep).trim()] = line.slice(sep + 1).trim();
|
|
100
|
+
}
|
|
101
|
+
results.push({ name: fm.name || entry.name.replace('.md', ''), description: fm.description || '', tags: fm.tags || '' });
|
|
102
|
+
} else {
|
|
103
|
+
results.push({ name: entry.name.replace('.md', ''), description: '', tags: '' });
|
|
104
|
+
}
|
|
105
|
+
} catch {}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch {}
|
|
109
|
+
if (!results.length) return { content: [{ type: 'text', text: '暂无已安装的 skill' }] };
|
|
110
|
+
const lines = [`已安装 ${results.length} 个 skill:\n`];
|
|
111
|
+
for (const s of results) {
|
|
112
|
+
lines.push(`**${s.name}**${s.description ? ` — ${s.description}` : ''}`);
|
|
113
|
+
if (s.tags) lines.push(` 标签: ${s.tags}`);
|
|
114
|
+
}
|
|
115
|
+
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function skill_uninstall(a) {
|
|
119
|
+
if (!a.name) return { content: [{ type: 'text', text: '需要 skill 名称' }], isError: true };
|
|
120
|
+
if (!fs.existsSync(SKILLS_DIR)) return { content: [{ type: 'text', text: `未安装 skill: ${a.name}` }], isError: true };
|
|
121
|
+
let found = null;
|
|
122
|
+
try {
|
|
123
|
+
const entries = fs.readdirSync(SKILLS_DIR, { withFileTypes: true });
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
126
|
+
const fullPath = path.join(SKILLS_DIR, entry.name);
|
|
127
|
+
try {
|
|
128
|
+
const raw = fs.readFileSync(fullPath, 'utf-8');
|
|
129
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
130
|
+
if (match) {
|
|
131
|
+
for (const line of match[1].split('\n')) {
|
|
132
|
+
if (line.startsWith('name:') && line.slice(5).trim() === a.name) { found = fullPath; break; }
|
|
133
|
+
}
|
|
134
|
+
} else if (entry.name.replace('.md', '') === a.name) { found = fullPath; }
|
|
135
|
+
} catch {}
|
|
136
|
+
if (found) break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch {}
|
|
140
|
+
if (!found) return { content: [{ type: 'text', text: `未安装 skill: ${a.name}` }], isError: true };
|
|
141
|
+
fs.unlinkSync(found);
|
|
142
|
+
return { content: [{ type: 'text', text: `${a.name} 已卸载` }] };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function skill_list_categories() {
|
|
146
|
+
const index = await fetchIndex();
|
|
147
|
+
if (!index?.skills) return { content: [{ type: 'text', text: '无法获取 skill 注册表索引' }], isError: true };
|
|
148
|
+
const cats = new Set();
|
|
149
|
+
for (const s of index.skills) for (const t of (s.tags || [])) cats.add(t);
|
|
150
|
+
const sorted = Array.from(cats).sort();
|
|
151
|
+
return { content: [{ type: 'text', text: sorted.length ? `注册表分类 (${sorted.length}):\n${sorted.join('\n')}` : '暂无分类' }] };
|
|
152
|
+
}
|