@atlisp/mcp 1.8.15 → 1.8.17
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/handlers/3d-handlers.js +128 -0
- package/dist/handlers/batch-handlers.js +260 -0
- package/dist/handlers/block-handlers.js +214 -0
- package/dist/handlers/cad-handlers.js +180 -0
- package/dist/handlers/constraint-handlers.js +130 -0
- package/dist/handlers/dim-hatch-handlers.js +340 -0
- package/dist/handlers/draw-handlers.js +175 -0
- package/dist/handlers/edit-handlers.js +179 -0
- package/dist/handlers/entity-handlers.js +196 -0
- package/dist/handlers/external-handlers.js +218 -0
- package/dist/handlers/file-handlers.js +172 -0
- package/dist/handlers/function-handlers.js +158 -0
- package/dist/handlers/layer-handlers.js +181 -0
- package/dist/handlers/package-handlers.js +89 -0
- package/dist/handlers/parametric-handlers.js +272 -0
- package/dist/handlers/pdf-annotation-handlers.js +117 -0
- package/dist/handlers/prompt-handlers.js +9 -0
- package/dist/handlers/purge-handlers.js +137 -0
- package/dist/handlers/query-print-handlers.js +662 -0
- package/dist/handlers/resource-cache.js +63 -0
- package/dist/handlers/resource-defs.js +79 -0
- package/dist/handlers/resource-handlers.js +131 -0
- package/dist/handlers/resource-readers.js +1045 -0
- package/dist/handlers/sheetset-handlers.js +430 -0
- package/dist/handlers/spatial-handlers.js +174 -0
- package/dist/handlers/style-handlers.js +162 -0
- package/dist/handlers/text-handlers.js +198 -0
- package/dist/handlers/ucs-layout-handlers.js +271 -0
- package/dist/handlers/viewport-handlers.js +150 -0
- package/dist/handlers/xdata-handlers.js +148 -0
- package/dist/handlers/xref-handlers.js +334 -0
- package/dist/lisp-security.js +7 -2
- package/package.json +2 -2
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { cad } from '../cad.js';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { log, error as logError } from '../logger.js';
|
|
6
|
+
import { escapeLispString } from '../handler-utils.js';
|
|
7
|
+
|
|
8
|
+
const DATA_DIR = path.join(os.homedir(), '.atlisp', 'data');
|
|
9
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
function ensureDataDir() {
|
|
12
|
+
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function validateSourcePath(source) {
|
|
16
|
+
const sanitized = source.replace(/[^a-zA-Z0-9_\-]/g, '_');
|
|
17
|
+
if (sanitized !== source) {
|
|
18
|
+
logError(`external-handlers: sanitized source "${source}" -> "${sanitized}"`);
|
|
19
|
+
}
|
|
20
|
+
const filePath = path.resolve(DATA_DIR, sanitized);
|
|
21
|
+
if (!filePath.startsWith(path.resolve(DATA_DIR) + path.sep) && filePath !== path.resolve(DATA_DIR)) {
|
|
22
|
+
throw new Error('路径穿越检测: 数据源路径必须位于 DATA_DIR 内');
|
|
23
|
+
}
|
|
24
|
+
return filePath;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function checkFileSize(filePath) {
|
|
28
|
+
try {
|
|
29
|
+
const stat = fs.statSync(filePath);
|
|
30
|
+
if (stat.size > MAX_FILE_SIZE) {
|
|
31
|
+
throw new Error(`文件过大 (${(stat.size / 1024 / 1024).toFixed(1)}MB),最大允许 ${MAX_FILE_SIZE / 1024 / 1024}MB`);
|
|
32
|
+
}
|
|
33
|
+
} catch (e) {
|
|
34
|
+
if (e.code === 'ENOENT') return;
|
|
35
|
+
throw e;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function scanDataSources() {
|
|
40
|
+
ensureDataDir();
|
|
41
|
+
const sources = [];
|
|
42
|
+
try {
|
|
43
|
+
const files = fs.readdirSync(DATA_DIR);
|
|
44
|
+
for (const file of files) {
|
|
45
|
+
const ext = path.extname(file).toLowerCase();
|
|
46
|
+
if (['.csv', '.json', '.db', '.sqlite'].includes(ext)) {
|
|
47
|
+
sources.push({ name: file.replace(ext, ''), file, type: ext.slice(1) });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} catch (e) { logError(`external-handlers: scanDataSources error: ${e.message}`); }
|
|
51
|
+
return sources;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function queryCSV(filePath, filters = {}) {
|
|
55
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
56
|
+
const lines = content.trim().split('\n');
|
|
57
|
+
if (lines.length < 2) return [];
|
|
58
|
+
const headers = lines[0].split(',').map(h => h.trim());
|
|
59
|
+
const rows = [];
|
|
60
|
+
for (let i = 1; i < lines.length; i++) {
|
|
61
|
+
const values = lines[i].split(',').map(v => v.trim());
|
|
62
|
+
const row = {};
|
|
63
|
+
headers.forEach((h, idx) => { row[h] = values[idx] || ''; });
|
|
64
|
+
|
|
65
|
+
let match = true;
|
|
66
|
+
for (const [key, val] of Object.entries(filters)) {
|
|
67
|
+
if (row[key] !== undefined && String(row[key]) !== String(val)) { match = false; break; }
|
|
68
|
+
}
|
|
69
|
+
if (match) rows.push(row);
|
|
70
|
+
}
|
|
71
|
+
return rows;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function externalDbQuery(args) {
|
|
75
|
+
const { source, query, filters } = args;
|
|
76
|
+
ensureDataDir();
|
|
77
|
+
|
|
78
|
+
if (!source) {
|
|
79
|
+
const sources = scanDataSources();
|
|
80
|
+
return { content: [{ type: 'text', text: JSON.stringify({ sources }) }] };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let filePath;
|
|
84
|
+
try { filePath = validateSourcePath(source); } catch (e) { return { content: [{ type: 'text', text: e.message }], isError: true }; }
|
|
85
|
+
checkFileSize(filePath);
|
|
86
|
+
if (!fs.existsSync(filePath)) {
|
|
87
|
+
for (const ext of ['.csv', '.json']) {
|
|
88
|
+
const altPath = validateSourcePath(source + ext);
|
|
89
|
+
if (fs.existsSync(altPath)) {
|
|
90
|
+
checkFileSize(altPath);
|
|
91
|
+
const ext2 = path.extname(altPath).toLowerCase();
|
|
92
|
+
if (ext2 === '.csv') {
|
|
93
|
+
const rows = queryCSV(altPath, filters || {});
|
|
94
|
+
return { content: [{ type: 'text', text: JSON.stringify({ source, rows, count: rows.length }) }] };
|
|
95
|
+
} else if (ext2 === '.json') {
|
|
96
|
+
const data = JSON.parse(fs.readFileSync(altPath, 'utf-8'));
|
|
97
|
+
return { content: [{ type: 'text', text: JSON.stringify({ source, data }) }] };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { content: [{ type: 'text', text: `数据源 "${source}" 未找到` }], isError: true };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
105
|
+
if (ext === '.csv') {
|
|
106
|
+
checkFileSize(filePath);
|
|
107
|
+
const rows = queryCSV(filePath, filters || {});
|
|
108
|
+
return { content: [{ type: 'text', text: JSON.stringify({ source, rows, count: rows.length }) }] };
|
|
109
|
+
} else if (ext === '.json') {
|
|
110
|
+
checkFileSize(filePath);
|
|
111
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
112
|
+
return { content: [{ type: 'text', text: JSON.stringify({ source, data }) }] };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { content: [{ type: 'text', text: `不支持的数据源类型: ${ext}` }], isError: true };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function importExternalData(args) {
|
|
119
|
+
const { source, targetLayer, mapping } = args;
|
|
120
|
+
ensureDataDir();
|
|
121
|
+
|
|
122
|
+
let filePath;
|
|
123
|
+
try { filePath = validateSourcePath(source); } catch (e) { return { content: [{ type: 'text', text: e.message }], isError: true }; }
|
|
124
|
+
if (!fs.existsSync(filePath)) {
|
|
125
|
+
for (const ext of ['.csv', '.json']) {
|
|
126
|
+
try {
|
|
127
|
+
const altPath = validateSourcePath(source + ext);
|
|
128
|
+
if (fs.existsSync(altPath)) { filePath = altPath; break; }
|
|
129
|
+
} catch {}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (!fs.existsSync(filePath)) {
|
|
133
|
+
return { content: [{ type: 'text', text: `数据源 "${source}" 未找到` }], isError: true };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let rows = [];
|
|
137
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
138
|
+
if (ext === '.csv') {
|
|
139
|
+
rows = queryCSV(filePath);
|
|
140
|
+
} else if (ext === '.json') {
|
|
141
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
142
|
+
rows = Array.isArray(data) ? data : (data.rows || data.data || []);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (rows.length === 0) {
|
|
146
|
+
return { content: [{ type: 'text', text: '数据为空' }] };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const layer = targetLayer || 'ImportedData';
|
|
150
|
+
const createEntities = [];
|
|
151
|
+
|
|
152
|
+
for (const row of rows) {
|
|
153
|
+
const x = parseFloat(row[mapping?.x || 'x' || 'X']);
|
|
154
|
+
const y = parseFloat(row[mapping?.y || 'y' || 'Y']);
|
|
155
|
+
const z = parseFloat(row[mapping?.z || 'z' || 'Z'] || '0');
|
|
156
|
+
const label = row[mapping?.label || 'label' || 'name'] || '';
|
|
157
|
+
|
|
158
|
+
if (!isNaN(x) && !isNaN(y)) {
|
|
159
|
+
if (label) {
|
|
160
|
+
createEntities.push(`(entmakex (list (cons 0 "POINT") (cons 10 (list ${x} ${y} ${z})) (cons 8 "${escapeLispString(layer)}") (cons 1 "${escapeLispString(label)}")))`);
|
|
161
|
+
} else {
|
|
162
|
+
createEntities.push(`(entmakex (list (cons 0 "POINT") (cons 10 (list ${x} ${y} ${z})) (cons 8 "${escapeLispString(layer)}")))`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (createEntities.length === 0) {
|
|
168
|
+
return { content: [{ type: 'text', text: '没有有效的坐标数据可以导入' }] };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return cad.sendCommandWithResult(`(progn ${createEntities.join('\n')} (princ (strcat "导入了 " (itoa ${createEntities.length}) " 个实体到图层 ${layer}")))`).then(result => {
|
|
172
|
+
return { content: [{ type: 'text', text: result || `导入了 ${createEntities.length} 个实体到图层 "${layer}"` }] };
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function exportToExternal(args) {
|
|
177
|
+
const { format, outputFile, filter } = args;
|
|
178
|
+
ensureDataDir();
|
|
179
|
+
|
|
180
|
+
const lisp = `(progn
|
|
181
|
+
(setq ss (ssget "_X" '(${filter ? `(8 . "${escapeLispString(filter.layer || '*')}")` : ''}))
|
|
182
|
+
(setq n (sslength ss))
|
|
183
|
+
(setq result nil)
|
|
184
|
+
(setq i 0)
|
|
185
|
+
(while (< i n)
|
|
186
|
+
(setq e (ssname ss i))
|
|
187
|
+
(setq elist (entget e))
|
|
188
|
+
(setq result (cons (list (cdr (assoc 0 elist)) (cdr (assoc 5 elist)) (cdr (assoc 8 elist)) (cdr (assoc 10 elist))) result))
|
|
189
|
+
(setq i (1+ i)))
|
|
190
|
+
(reverse result))`;
|
|
191
|
+
|
|
192
|
+
return cad.sendCommandWithResult(lisp).then(raw => {
|
|
193
|
+
let entities;
|
|
194
|
+
try { entities = JSON.parse(raw); } catch { entities = []; }
|
|
195
|
+
|
|
196
|
+
const exportData = entities.map(e => ({
|
|
197
|
+
type: e[0], handle: e[1], layer: e[2], point: e[3]
|
|
198
|
+
}));
|
|
199
|
+
|
|
200
|
+
const outFile = outputFile || path.join(DATA_DIR, `export_${Date.now()}.${format || 'json'}`);
|
|
201
|
+
const dir = path.dirname(outFile);
|
|
202
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
203
|
+
|
|
204
|
+
if (format === 'csv') {
|
|
205
|
+
const csv = 'type,handle,layer,point\n' + exportData.map(e => `"${e.type}","${e.handle}","${e.layer}","${e.point}"`).join('\n');
|
|
206
|
+
fs.writeFileSync(outFile, csv, 'utf-8');
|
|
207
|
+
} else {
|
|
208
|
+
fs.writeFileSync(outFile, JSON.stringify(exportData, null, 2), 'utf-8');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { content: [{ type: 'text', text: JSON.stringify({ exported: exportData.length, file: outFile, format: format || 'json' }) }] };
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function listDataSources() {
|
|
216
|
+
const sources = scanDataSources();
|
|
217
|
+
return { content: [{ type: 'text', text: JSON.stringify({ sources }) }] };
|
|
218
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { cad } from '../cad.js';
|
|
2
|
+
import { escapeLispString } from '../handler-utils.js';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
function validateFilePath(filePath) {
|
|
6
|
+
const normalized = path.normalize(filePath);
|
|
7
|
+
if (normalized.includes('..')) {
|
|
8
|
+
throw new Error(`路径不允许包含 "..": ${filePath}`);
|
|
9
|
+
}
|
|
10
|
+
const resolved = path.resolve(normalized);
|
|
11
|
+
return resolved;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function openDwg(filePath) {
|
|
15
|
+
if (!cad.connected) { await cad.connect(); }
|
|
16
|
+
if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
validateFilePath(filePath);
|
|
20
|
+
} catch (e) {
|
|
21
|
+
return { content: [{ type: 'text', text: e.message }], isError: true };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const code = `(progn
|
|
25
|
+
(vl-load-com)
|
|
26
|
+
(if (vl-file-directory-p (vl-filename-directory "${escapeLispString(filePath)}"))
|
|
27
|
+
(progn
|
|
28
|
+
(if (findfile "${escapeLispString(filePath)}")
|
|
29
|
+
(progn
|
|
30
|
+
(command "._OPEN" "${escapeLispString(filePath)}")
|
|
31
|
+
"OPENED"
|
|
32
|
+
)
|
|
33
|
+
"NOTFOUND"
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
"INVALIDPATH"
|
|
37
|
+
)
|
|
38
|
+
)`;
|
|
39
|
+
try {
|
|
40
|
+
const result = await cad.sendCommandWithResult(code);
|
|
41
|
+
if (result === 'OPENED' || result === '"OPENED"') {
|
|
42
|
+
return { content: [{ type: 'text', text: `已打开文件: ${filePath}` }] };
|
|
43
|
+
}
|
|
44
|
+
if (result === 'NOTFOUND' || result === '"NOTFOUND"') {
|
|
45
|
+
return { content: [{ type: 'text', text: `文件不存在: ${filePath}` }], isError: true };
|
|
46
|
+
}
|
|
47
|
+
if (result === 'INVALIDPATH' || result === '"INVALIDPATH"') {
|
|
48
|
+
return { content: [{ type: 'text', text: `路径无效: ${filePath}` }], isError: true };
|
|
49
|
+
}
|
|
50
|
+
return { content: [{ type: 'text', text: `打开失败: ${result}` }], isError: true };
|
|
51
|
+
} catch (e) {
|
|
52
|
+
return { content: [{ type: 'text', text: `错误: ${e.message}` }], isError: true };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function saveDwg(filePath = null) {
|
|
57
|
+
if (!cad.connected) { await cad.connect(); }
|
|
58
|
+
if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
|
|
59
|
+
|
|
60
|
+
if (filePath) {
|
|
61
|
+
try {
|
|
62
|
+
validateFilePath(filePath);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
return { content: [{ type: 'text', text: e.message }], isError: true };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const code = filePath
|
|
69
|
+
? `(progn
|
|
70
|
+
(vl-load-com)
|
|
71
|
+
(command "._SAVEAS" "${escapeLispString(filePath)}")
|
|
72
|
+
"SAVED"
|
|
73
|
+
)`
|
|
74
|
+
: `(progn
|
|
75
|
+
(vl-load-com)
|
|
76
|
+
(command "._QSAVE")
|
|
77
|
+
"SAVED"
|
|
78
|
+
)`;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const result = await cad.sendCommandWithResult(code);
|
|
82
|
+
if (result === 'SAVED' || result === '"SAVED"') {
|
|
83
|
+
return { content: [{ type: 'text', text: filePath ? `已保存为: ${filePath}` : '已保存当前文件' }] };
|
|
84
|
+
}
|
|
85
|
+
return { content: [{ type: 'text', text: `保存失败: ${result}` }], isError: true };
|
|
86
|
+
} catch (e) {
|
|
87
|
+
return { content: [{ type: 'text', text: `错误: ${e.message}` }], isError: true };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function exportPdf(outputPath, layout = 'Model') {
|
|
92
|
+
if (!cad.connected) { await cad.connect(); }
|
|
93
|
+
if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
validateFilePath(outputPath);
|
|
97
|
+
} catch (e) {
|
|
98
|
+
return { content: [{ type: 'text', text: e.message }], isError: true };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const code = `(progn
|
|
102
|
+
(vl-load-com)
|
|
103
|
+
(vlax-for doc (vla-get-Documents (vla-get-Application (vlax-get-acad-object)))
|
|
104
|
+
(if (or (eq (vla-get-Name doc) "${escapeLispString(layout)}") (eq "${escapeLispString(layout)}" "Model"))
|
|
105
|
+
(progn
|
|
106
|
+
(vla-Export doc "${escapeLispString(outputPath)}" "pdf")
|
|
107
|
+
(setq exported T)
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
(if exported "EXPORTED" "FAILED")
|
|
112
|
+
)`;
|
|
113
|
+
try {
|
|
114
|
+
const result = await cad.sendCommandWithResult(code);
|
|
115
|
+
if (result === 'EXPORTED' || result === '"EXPORTED"') {
|
|
116
|
+
return { content: [{ type: 'text', text: `已导出 PDF: ${outputPath}` }] };
|
|
117
|
+
}
|
|
118
|
+
return { content: [{ type: 'text', text: `导出失败: ${result}` }], isError: true };
|
|
119
|
+
} catch (e) {
|
|
120
|
+
return { content: [{ type: 'text', text: `错误: ${e.message}` }], isError: true };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function closeDwg(save = false) {
|
|
125
|
+
if (!cad.connected) { await cad.connect(); }
|
|
126
|
+
if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
|
|
127
|
+
|
|
128
|
+
const code = `(progn
|
|
129
|
+
(vl-load-com)
|
|
130
|
+
(if ${save ? 'T' : 'nil'}
|
|
131
|
+
(command "._CLOSE" "Y")
|
|
132
|
+
(command "._CLOSE")
|
|
133
|
+
)
|
|
134
|
+
"CLOSED"
|
|
135
|
+
)`;
|
|
136
|
+
try {
|
|
137
|
+
const result = await cad.sendCommandWithResult(code);
|
|
138
|
+
return { content: [{ type: 'text', text: '已关闭图形' }] };
|
|
139
|
+
} catch (e) {
|
|
140
|
+
return { content: [{ type: 'text', text: `错误: ${e.message}` }], isError: true };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function exportSvg(outputPath, layout = 'Model') {
|
|
145
|
+
if (!cad.connected) { await cad.connect(); }
|
|
146
|
+
if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
|
|
147
|
+
try {
|
|
148
|
+
validateFilePath(outputPath);
|
|
149
|
+
const code = `(progn (vl-load-com) (setq doc (vla-get-activedocument (vlax-get-acad-object))) (setq layouts (vla-get-layouts doc)) (setq lay (vla-item layouts "${escapeLispString(layout)}")) (vla-Export lay "${escapeLispString(outputPath)}" "svg") (princ))`;
|
|
150
|
+
await cad.sendCommand(code + '\n');
|
|
151
|
+
return { content: [{ type: 'text', text: `已导出 SVG: ${outputPath}` }] };
|
|
152
|
+
} catch (e) { return { content: [{ type: 'text', text: `错误: ${e.message}` }], isError: true }; }
|
|
153
|
+
}
|
|
154
|
+
export async function exportBmp(outputPath) {
|
|
155
|
+
if (!cad.connected) { await cad.connect(); }
|
|
156
|
+
if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
|
|
157
|
+
try {
|
|
158
|
+
validateFilePath(outputPath);
|
|
159
|
+
await cad.sendCommand(`(command "_.SAVEIMG" "${escapeLispString(outputPath)}" "b")\n`);
|
|
160
|
+
return { content: [{ type: 'text', text: `已导出 BMP: ${outputPath}` }] };
|
|
161
|
+
} catch (e) { return { content: [{ type: 'text', text: `错误: ${e.message}` }], isError: true }; }
|
|
162
|
+
}
|
|
163
|
+
export async function exportStl(outputPath, handle = null) {
|
|
164
|
+
if (!cad.connected) { await cad.connect(); }
|
|
165
|
+
if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
|
|
166
|
+
try {
|
|
167
|
+
validateFilePath(outputPath);
|
|
168
|
+
const sel = handle ? `(ssadd (handent "${escapeLispString(handle)}"))` : '(ssget "_X" \'((0 . "3DSOLID")))';
|
|
169
|
+
await cad.sendCommand(`(command "_.EXPORT" "${escapeLispString(outputPath)}" "" ${sel})\n`);
|
|
170
|
+
return { content: [{ type: 'text', text: `已导出 STL: ${outputPath}` }] };
|
|
171
|
+
} catch (e) { return { content: [{ type: 'text', text: `错误: ${e.message}` }], isError: true }; }
|
|
172
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { log } from '../logger.js';
|
|
4
|
+
import * as cadHandlers from './cad-handlers.js';
|
|
5
|
+
import os from 'os';
|
|
6
|
+
import config from '../config.js';
|
|
7
|
+
|
|
8
|
+
const FUNCTIONS_DIR = process.env.FUNCTIONS_DIR || '';
|
|
9
|
+
const FUNCTIONS_URL = process.env.FUNCTIONS_URL || config.functionLibUrl || 'http://s3.atlisp.cn/json/functions.json';
|
|
10
|
+
const CACHE_DIR = path.join(os.homedir(), '.atlisp', 'cache');
|
|
11
|
+
const FUNCLIB_CACHE_FILE = path.join(CACHE_DIR, 'functions.json');
|
|
12
|
+
const FUNCLIB_CACHE_TTL = config.functionLibCacheTtl || 24 * 60 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
let cachedFunctionLib = null;
|
|
15
|
+
let cachedAt = null;
|
|
16
|
+
|
|
17
|
+
function readCacheFromDisk() {
|
|
18
|
+
try {
|
|
19
|
+
const stat = fs.statSync(FUNCLIB_CACHE_FILE);
|
|
20
|
+
if (Date.now() - stat.mtimeMs < FUNCLIB_CACHE_TTL) {
|
|
21
|
+
const raw = fs.readFileSync(FUNCLIB_CACHE_FILE, 'utf-8');
|
|
22
|
+
return JSON.parse(raw);
|
|
23
|
+
}
|
|
24
|
+
} catch (e) { log(`function-handlers: readCache error: ${e.message}`); }
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeCacheToDisk(data) {
|
|
29
|
+
try {
|
|
30
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
31
|
+
fs.writeFileSync(FUNCLIB_CACHE_FILE, JSON.stringify(data), 'utf-8');
|
|
32
|
+
} catch (e) {
|
|
33
|
+
log(`function-handlers writeCache error: ${e.message}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function fetchFunctions() {
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
if (cachedFunctionLib && cachedAt && (now - cachedAt) < FUNCLIB_CACHE_TTL) {
|
|
40
|
+
return cachedFunctionLib;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const diskCache = readCacheFromDisk();
|
|
44
|
+
if (diskCache) {
|
|
45
|
+
cachedFunctionLib = diskCache;
|
|
46
|
+
cachedAt = now;
|
|
47
|
+
return diskCache;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const response = await fetch(FUNCTIONS_URL, {
|
|
52
|
+
headers: { 'User-Agent': 'atlisp-mcp' },
|
|
53
|
+
signal: AbortSignal.timeout(15000),
|
|
54
|
+
});
|
|
55
|
+
if (response.ok) {
|
|
56
|
+
const data = await response.json();
|
|
57
|
+
cachedFunctionLib = data;
|
|
58
|
+
cachedAt = now;
|
|
59
|
+
writeCacheToDisk(data);
|
|
60
|
+
return data;
|
|
61
|
+
}
|
|
62
|
+
} catch (e) {
|
|
63
|
+
log(`function-handlers fetch error: ${e.message}`);
|
|
64
|
+
}
|
|
65
|
+
return cachedFunctionLib;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function loadAtlibFunctionLib() {
|
|
69
|
+
return await fetchFunctions();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function getFunctionUsage(funcName, packageName) {
|
|
73
|
+
try {
|
|
74
|
+
let results = [];
|
|
75
|
+
|
|
76
|
+
const data = await fetchFunctions();
|
|
77
|
+
if (data) {
|
|
78
|
+
const funcs = Object.values(data.all_functions || {});
|
|
79
|
+
const func = funcs.find(f => f.name === funcName || f.name === `${packageName}:${funcName}`);
|
|
80
|
+
if (func) results.push(func);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (results.length === 0 && FUNCTIONS_DIR) {
|
|
84
|
+
try {
|
|
85
|
+
const files = fs.readdirSync(FUNCTIONS_DIR).filter(f => f.endsWith('.json'));
|
|
86
|
+
for (const file of files) {
|
|
87
|
+
const raw = fs.readFileSync(path.join(FUNCTIONS_DIR, file), 'utf-8');
|
|
88
|
+
const data = JSON.parse(raw);
|
|
89
|
+
const func = data.functions?.find(f => f.name === funcName);
|
|
90
|
+
if (func) results.push(func);
|
|
91
|
+
}
|
|
92
|
+
} catch (e) { log(`function-handlers: FUNCTIONS_DIR read error: ${e.message}`); }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (results.length === 0) {
|
|
96
|
+
return { content: [{ type: 'text', text: `未找到函数: ${funcName}` }], isError: true };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const f = results[0];
|
|
100
|
+
const text = `函数: ${f.name}
|
|
101
|
+
描述: ${f.description || '-'}
|
|
102
|
+
参数: ${f.params?.join(', ') || '-'}
|
|
103
|
+
返回值: ${f.return || '-'}
|
|
104
|
+
示例: ${f.usage || '-'}`;
|
|
105
|
+
|
|
106
|
+
return { content: [{ type: 'text', text }] };
|
|
107
|
+
} catch (e) {
|
|
108
|
+
return { content: [{ type: 'text', text: `错误: ${e.message}` }], isError: true };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function listSymbols(packageName) {
|
|
113
|
+
try {
|
|
114
|
+
let allFuncs = [];
|
|
115
|
+
|
|
116
|
+
const data = await fetchFunctions();
|
|
117
|
+
if (data) {
|
|
118
|
+
const funcs = Object.values(data.all_functions || {});
|
|
119
|
+
if (packageName) {
|
|
120
|
+
allFuncs = funcs.filter(f => f.name.startsWith(`${packageName}:`));
|
|
121
|
+
} else {
|
|
122
|
+
allFuncs = funcs.map(f => `${f.name}: ${f.description || '-'}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (allFuncs.length === 0 && FUNCTIONS_DIR) {
|
|
127
|
+
try {
|
|
128
|
+
const files = fs.readdirSync(FUNCTIONS_DIR).filter(f => f.endsWith('.json'));
|
|
129
|
+
for (const file of files) {
|
|
130
|
+
const raw = fs.readFileSync(path.join(FUNCTIONS_DIR, file), 'utf-8');
|
|
131
|
+
const data = JSON.parse(raw);
|
|
132
|
+
const funcs = Object.values(data.all_functions || {});
|
|
133
|
+
if (packageName) {
|
|
134
|
+
allFuncs = funcs.filter(f => f.name.startsWith(`${packageName}:`));
|
|
135
|
+
} else {
|
|
136
|
+
for (const f of funcs) {
|
|
137
|
+
allFuncs.push(`${f.name}: ${f.description || '-'}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
} catch (err) {
|
|
142
|
+
log(`listSymbols readDir error: ${err.message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (allFuncs.length === 0) {
|
|
147
|
+
return await cadHandlers.listFunctionsInCad();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (packageName) {
|
|
151
|
+
return { content: [{ type: 'text', text: `${packageName} 包函数 (${allFuncs.length}):\n${allFuncs.map(f => f.name).join('\n')}` }] };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { content: [{ type: 'text', text: allFuncs.join('\n') }] };
|
|
155
|
+
} catch (e) {
|
|
156
|
+
return { content: [{ type: 'text', text: `错误: ${e.message}` }], isError: true };
|
|
157
|
+
}
|
|
158
|
+
}
|