@atlisp/mcp 1.0.18 → 1.0.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.
- package/README.md +116 -0
- package/package.json +2 -1
- package/src/atlisp-mcp.js +290 -195
- package/src/cad-worker.js +11 -4
- package/src/cad.js +33 -26
- package/src/config.js +28 -0
- package/src/constants.js +2 -1
- package/src/errors.js +55 -0
- package/src/handlers/cad-handlers.js +2 -2
- package/src/handlers/function-handlers.js +25 -13
- package/src/handlers/prompt-handlers.js +268 -0
- package/src/handlers/resource-handlers.js +82 -35
- package/src/sse-session-manager.js +1 -3
- package/src/sse-session.js +125 -15
- package/src/subscription-manager.js +11 -0
package/src/cad.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
|
-
import fs from 'fs';
|
|
5
3
|
import { fileURLToPath } from 'url';
|
|
6
4
|
import { CAD_PLATFORMS, FILE_EXTENSIONS } from './constants.js';
|
|
5
|
+
import config from './config.js';
|
|
7
6
|
|
|
8
|
-
const MESSAGE_TIMEOUT =
|
|
9
|
-
const
|
|
7
|
+
const MESSAGE_TIMEOUT = config.messageTimeout;
|
|
8
|
+
const HEARTBEAT_INTERVAL = config.heartbeatInterval;
|
|
10
9
|
|
|
11
10
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
11
|
const workerPath = path.join(__dirname, 'cad-worker.js');
|
|
@@ -15,7 +14,6 @@ let worker = null;
|
|
|
15
14
|
let _platform = null;
|
|
16
15
|
let _version = null;
|
|
17
16
|
|
|
18
|
-
const HEARTBEAT_INTERVAL = parseInt(process.env.HEARTBEAT_INTERVAL || '30000', 10);
|
|
19
17
|
let heartbeatTimer = null;
|
|
20
18
|
|
|
21
19
|
export function resetWorker() {
|
|
@@ -73,39 +71,48 @@ export function sendMessage(msg) {
|
|
|
73
71
|
return new Promise((resolve, reject) => {
|
|
74
72
|
const w = getWorker();
|
|
75
73
|
let responded = false;
|
|
76
|
-
|
|
74
|
+
let buffer = '';
|
|
75
|
+
|
|
77
76
|
const timeout = setTimeout(() => {
|
|
78
77
|
if (!responded) {
|
|
79
78
|
responded = true;
|
|
80
79
|
reject(new Error('Timeout waiting for worker'));
|
|
81
80
|
}
|
|
82
81
|
}, MESSAGE_TIMEOUT);
|
|
83
|
-
|
|
82
|
+
|
|
84
83
|
const handler = (data) => {
|
|
85
84
|
try {
|
|
86
|
-
|
|
85
|
+
buffer += data.toString();
|
|
86
|
+
const lines = buffer.split('\n');
|
|
87
|
+
buffer = lines.pop() || '';
|
|
87
88
|
for (const line of lines) {
|
|
88
89
|
if (!line.trim()) continue;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (
|
|
92
|
-
responded
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
responded
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
90
|
+
try {
|
|
91
|
+
const result = JSON.parse(line);
|
|
92
|
+
if (result.error) {
|
|
93
|
+
if (!responded) {
|
|
94
|
+
responded = true;
|
|
95
|
+
clearTimeout(timeout);
|
|
96
|
+
w.stdout.off('data', handler);
|
|
97
|
+
reject(new Error(result.error));
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
if (!responded) {
|
|
101
|
+
responded = true;
|
|
102
|
+
clearTimeout(timeout);
|
|
103
|
+
w.stdout.off('data', handler);
|
|
104
|
+
resolve(result);
|
|
105
|
+
}
|
|
103
106
|
}
|
|
107
|
+
} catch (parseErr) {
|
|
108
|
+
console.error('JSON parse error in worker output:', parseErr.message, 'line:', line.slice(0, 200));
|
|
104
109
|
}
|
|
105
110
|
}
|
|
106
|
-
} catch (e) {
|
|
111
|
+
} catch (e) {
|
|
112
|
+
console.error('Unexpected error in worker stdout handler:', e.message);
|
|
113
|
+
}
|
|
107
114
|
};
|
|
108
|
-
|
|
115
|
+
|
|
109
116
|
w.stdout.on('data', handler);
|
|
110
117
|
w.stdin.write(JSON.stringify(msg) + '\n');
|
|
111
118
|
});
|
|
@@ -146,9 +153,9 @@ class CadConnection {
|
|
|
146
153
|
throw new Error(result.error || '发送失败');
|
|
147
154
|
}
|
|
148
155
|
|
|
149
|
-
async sendCommandWithResult(code) {
|
|
156
|
+
async sendCommandWithResult(code, encoding = null) {
|
|
150
157
|
if (!this.connected) throw new Error('未连接 CAD');
|
|
151
|
-
const result = await sendMessage({ type: 'sendResult', code: code, platform: _platform, encoding:
|
|
158
|
+
const result = await sendMessage({ type: 'sendResult', code: code, platform: _platform, encoding: encoding || '' });
|
|
152
159
|
if (result.success) {
|
|
153
160
|
return result.result || '';
|
|
154
161
|
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const config = {
|
|
2
|
+
port: parseInt(process.env.PORT || '8110', 10),
|
|
3
|
+
host: process.env.HOST || '0.0.0.0',
|
|
4
|
+
transport: process.env.TRANSPORT || 'http',
|
|
5
|
+
apiKey: process.env.MCP_API_KEY || '',
|
|
6
|
+
enableSse: process.env.ENABLE_SSE !== 'false',
|
|
7
|
+
messageTimeout: parseInt(process.env.MESSAGE_TIMEOUT || '60000', 10),
|
|
8
|
+
heartbeatInterval: parseInt(process.env.HEARTBEAT_INTERVAL || '30000', 10),
|
|
9
|
+
busyRetries: parseInt(process.env.BUSY_RETRIES || '10', 10),
|
|
10
|
+
busyDelay: parseInt(process.env.BUSY_DELAY || '500', 10),
|
|
11
|
+
enableCors: process.env.ENABLE_CORS !== 'false',
|
|
12
|
+
corsOrigin: process.env.CORS_ORIGIN || '*',
|
|
13
|
+
rateLimitWindow: parseInt(process.env.RATE_LIMIT_WINDOW || '60000', 10),
|
|
14
|
+
rateLimitMax: parseInt(process.env.RATE_LIMIT_MAX || '100', 10),
|
|
15
|
+
resourceCacheTtl: parseInt(process.env.RESOURCE_CACHE_TTL || '5000', 10),
|
|
16
|
+
debug: process.env.DEBUG === 'true',
|
|
17
|
+
debugFile: process.env.DEBUG_FILE || '',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function getConfig(key) {
|
|
21
|
+
return config[key];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getAllConfig() {
|
|
25
|
+
return { ...config };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default config;
|
package/src/constants.js
CHANGED
|
@@ -2,12 +2,13 @@ export const CAD_PLATFORMS = ['AutoCAD', 'ZWCAD', 'GStarCAD', 'BricsCAD'];
|
|
|
2
2
|
export const FILE_EXTENSIONS = {
|
|
3
3
|
'AutoCAD': ['.fas', '.vlx'],
|
|
4
4
|
'ZWCAD': ['.zelx', '.vls'],
|
|
5
|
+
'GStarCAD': ['.fas', '.vlx'],
|
|
5
6
|
'BricsCAD': ['.des']
|
|
6
7
|
};
|
|
7
8
|
|
|
8
9
|
export const PROTOCOL_VERSION = '2024-11-05';
|
|
9
10
|
export const SERVER_NAME = 'atlisp-mcp-server';
|
|
10
|
-
export const SERVER_VERSION = '1.0.
|
|
11
|
+
export const SERVER_VERSION = '1.0.20';
|
|
11
12
|
|
|
12
13
|
export const MOCK_PACKAGES = [
|
|
13
14
|
{ name: 'base', description: '基础函数库', version: '1.0.0' },
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const ERROR_CODES = {
|
|
2
|
+
CAD_NOT_CONNECTED: 'CAD_NOT_CONNECTED',
|
|
3
|
+
CAD_BUSY: 'CAD_BUSY',
|
|
4
|
+
CAD_NO_DOCUMENT: 'CAD_NO_DOCUMENT',
|
|
5
|
+
LISP_PAREN_MISMATCH: 'LISP_PAREN_MISMATCH',
|
|
6
|
+
TIMEOUT: 'TIMEOUT',
|
|
7
|
+
INVALID_PARAMS: 'INVALID_PARAMS',
|
|
8
|
+
METHOD_NOT_FOUND: 'METHOD_NOT_FOUND',
|
|
9
|
+
RESOURCE_NOT_FOUND: 'RESOURCE_NOT_FOUND',
|
|
10
|
+
PACKAGE_NOT_FOUND: 'PACKAGE_NOT_FOUND',
|
|
11
|
+
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const ERROR_MESSAGES = {
|
|
15
|
+
[ERROR_CODES.CAD_NOT_CONNECTED]: '未连接 CAD,请先调用 connect_cad',
|
|
16
|
+
[ERROR_CODES.CAD_BUSY]: 'CAD 忙碌,请稍后重试',
|
|
17
|
+
[ERROR_CODES.CAD_NO_DOCUMENT]: 'CAD 中没有打开的文档',
|
|
18
|
+
[ERROR_CODES.LISP_PAREN_MISMATCH]: 'LISP 代码括号不匹配',
|
|
19
|
+
[ERROR_CODES.TIMEOUT]: '操作超时',
|
|
20
|
+
[ERROR_CODES.INVALID_PARAMS]: '无效的参数',
|
|
21
|
+
[ERROR_CODES.METHOD_NOT_FOUND]: '方法不存在',
|
|
22
|
+
[ERROR_CODES.RESOURCE_NOT_FOUND]: '资源不存在',
|
|
23
|
+
[ERROR_CODES.PACKAGE_NOT_FOUND]: '包不存在',
|
|
24
|
+
[ERROR_CODES.INTERNAL_ERROR]: '内部错误',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class MCPError extends Error {
|
|
28
|
+
constructor(code, message, details = {}) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.code = code;
|
|
31
|
+
this.details = details;
|
|
32
|
+
this.name = 'MCPError';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
toJSON() {
|
|
36
|
+
return {
|
|
37
|
+
code: this.code,
|
|
38
|
+
message: this.message,
|
|
39
|
+
details: this.details
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createError(code, details = {}) {
|
|
45
|
+
const message = ERROR_MESSAGES[code] || ERROR_MESSAGES[ERROR_CODES.INTERNAL_ERROR];
|
|
46
|
+
return new MCPError(code, message, details);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function toMcpError(error) {
|
|
50
|
+
if (error instanceof MCPError) return error;
|
|
51
|
+
if (error instanceof Error) {
|
|
52
|
+
return new MCPError(ERROR_CODES.INTERNAL_ERROR, error.message, { stack: error.stack });
|
|
53
|
+
}
|
|
54
|
+
return new MCPError(ERROR_CODES.INTERNAL_ERROR, String(error));
|
|
55
|
+
}
|
|
@@ -16,14 +16,14 @@ export async function connectCad() {
|
|
|
16
16
|
return { content: [{ type: 'text', text: '未能连接或启动 CAD,请确保 CAD 已安装' }], isError: true };
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export async function evalLisp(code, withResult = false) {
|
|
19
|
+
export async function evalLisp(code, withResult = false, encoding = null) {
|
|
20
20
|
if (!cad.connected) { await cad.connect(); }
|
|
21
21
|
if (!cad.connected) {
|
|
22
22
|
return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
|
|
23
23
|
}
|
|
24
24
|
try {
|
|
25
25
|
if (withResult) {
|
|
26
|
-
const result = await cad.sendCommandWithResult(code);
|
|
26
|
+
const result = await cad.sendCommandWithResult(code, encoding);
|
|
27
27
|
if (result !== null) {
|
|
28
28
|
return { content: [{ type: 'text', text: result }] };
|
|
29
29
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { log } from '../logger.js';
|
|
3
4
|
|
|
4
|
-
const FUNCTIONS_DIR = process.env.FUNCTIONS_DIR || '
|
|
5
|
-
const FUNCTIONS_URL = process.env.FUNCTIONS_URL || '
|
|
5
|
+
const FUNCTIONS_DIR = process.env.FUNCTIONS_DIR || '';
|
|
6
|
+
const FUNCTIONS_URL = process.env.FUNCTIONS_URL || 'https://s3.atlisp.cn/json/';
|
|
7
|
+
|
|
8
|
+
async function fileExists(filePath) {
|
|
9
|
+
try { await fs.access(filePath); return true; } catch { return false; }
|
|
10
|
+
}
|
|
6
11
|
|
|
7
12
|
export async function getFunctionUsage(funcName, packageName) {
|
|
8
13
|
try {
|
|
@@ -21,20 +26,24 @@ export async function getFunctionUsage(funcName, packageName) {
|
|
|
21
26
|
if (func) results.push(func);
|
|
22
27
|
}
|
|
23
28
|
}
|
|
24
|
-
} catch (e) {
|
|
29
|
+
} catch (e) {
|
|
30
|
+
log(`function-handlers fetch error: ${e.message}`);
|
|
31
|
+
}
|
|
25
32
|
|
|
26
|
-
if (results.length === 0 &&
|
|
33
|
+
if (results.length === 0 && FUNCTIONS_DIR && await fileExists(FUNCTIONS_DIR)) {
|
|
27
34
|
if (packageName) {
|
|
28
35
|
const filePath = path.join(FUNCTIONS_DIR, `${packageName}.json`);
|
|
29
|
-
if (
|
|
30
|
-
const
|
|
36
|
+
if (await fileExists(filePath)) {
|
|
37
|
+
const raw = await fs.readFile(filePath, 'utf-8');
|
|
38
|
+
const data = JSON.parse(raw);
|
|
31
39
|
const func = data.functions?.find(f => f.name === funcName);
|
|
32
40
|
if (func) results.push(func);
|
|
33
41
|
}
|
|
34
42
|
} else {
|
|
35
|
-
const files = fs.
|
|
43
|
+
const files = (await fs.readdir(FUNCTIONS_DIR)).filter(f => f.endsWith('.json'));
|
|
36
44
|
for (const file of files) {
|
|
37
|
-
const
|
|
45
|
+
const raw = await fs.readFile(path.join(FUNCTIONS_DIR, file), 'utf-8');
|
|
46
|
+
const data = JSON.parse(raw);
|
|
38
47
|
const func = data.functions?.find(f => f.name === funcName);
|
|
39
48
|
if (func) results.push(func);
|
|
40
49
|
}
|
|
@@ -76,12 +85,15 @@ export async function listFunctions(packageName) {
|
|
|
76
85
|
: data.functions.map(f => `${f.name}: ${f.description || '-'}`);
|
|
77
86
|
}
|
|
78
87
|
}
|
|
79
|
-
} catch (e) {
|
|
88
|
+
} catch (e) {
|
|
89
|
+
log(`function-handlers list fetch error: ${e.message}`);
|
|
90
|
+
}
|
|
80
91
|
|
|
81
|
-
if (allFuncs.length === 0 &&
|
|
82
|
-
const files = fs.
|
|
92
|
+
if (allFuncs.length === 0 && FUNCTIONS_DIR && await fileExists(FUNCTIONS_DIR)) {
|
|
93
|
+
const files = (await fs.readdir(FUNCTIONS_DIR)).filter(f => f.endsWith('.json'));
|
|
83
94
|
for (const file of files) {
|
|
84
|
-
const
|
|
95
|
+
const raw = await fs.readFile(path.join(FUNCTIONS_DIR, file), 'utf-8');
|
|
96
|
+
const data = JSON.parse(raw);
|
|
85
97
|
if (packageName) {
|
|
86
98
|
if (data.package === packageName) {
|
|
87
99
|
allFuncs = data.functions || [];
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { cad } from '../cad.js';
|
|
2
|
+
|
|
3
|
+
const CACHE_TTL = 60000;
|
|
4
|
+
const cache = new Map();
|
|
5
|
+
|
|
6
|
+
function getCached(key) {
|
|
7
|
+
const entry = cache.get(key);
|
|
8
|
+
if (entry && Date.now() - entry.timestamp < CACHE_TTL) return entry.data;
|
|
9
|
+
cache.delete(key);
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function setCache(key, data) {
|
|
14
|
+
cache.set(key, { data, timestamp: Date.now() });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const PROMPTS = [
|
|
18
|
+
{
|
|
19
|
+
name: "draw-residential",
|
|
20
|
+
description: "生成农村民居平面布置图",
|
|
21
|
+
arguments: [
|
|
22
|
+
{ name: "width", description: "建筑宽度 (mm)", required: true },
|
|
23
|
+
{ name: "depth", description: "建筑深度 (mm)", required: true },
|
|
24
|
+
{ name: "rooms", description: "房间数量", required: false }
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "draw-floor-plan",
|
|
29
|
+
description: "生成建筑楼层平面图",
|
|
30
|
+
arguments: [
|
|
31
|
+
{ name: "floor", description: "楼层名称", required: true },
|
|
32
|
+
{ name: "scale", description: "图纸比例", required: false }
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "analyze-drawing",
|
|
37
|
+
description: "分析当前图纸统计信息",
|
|
38
|
+
arguments: []
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "batch-draw-lines",
|
|
42
|
+
description: "批量绘制线段",
|
|
43
|
+
arguments: [
|
|
44
|
+
{ name: "count", description: "线段数量", required: true },
|
|
45
|
+
{ name: "length", description: "线段长度", required: false }
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "generate-dimension",
|
|
50
|
+
description: "生成尺寸标注",
|
|
51
|
+
arguments: [
|
|
52
|
+
{ name: "style", description: "标注样式 (horizontal/vertical/aligned)", required: false }
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "export-entities",
|
|
57
|
+
description: "导出图纸实体统计",
|
|
58
|
+
arguments: [
|
|
59
|
+
{ name: "format", description: "导出格式 (json/list)", required: false }
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
export async function listPrompts() {
|
|
65
|
+
return PROMPTS;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function getPrompt(name, arguments_ = {}) {
|
|
69
|
+
switch (name) {
|
|
70
|
+
case "draw-residential":
|
|
71
|
+
return generateResidentialPrompt(arguments_);
|
|
72
|
+
case "draw-floor-plan":
|
|
73
|
+
return generateFloorPlanPrompt(arguments_);
|
|
74
|
+
case "analyze-drawing":
|
|
75
|
+
return generateAnalyzePrompt();
|
|
76
|
+
case "batch-draw-lines":
|
|
77
|
+
return generateBatchLinesPrompt(arguments_);
|
|
78
|
+
case "generate-dimension":
|
|
79
|
+
return generateDimensionPrompt(arguments_);
|
|
80
|
+
case "export-entities":
|
|
81
|
+
return generateExportPrompt(arguments_);
|
|
82
|
+
default:
|
|
83
|
+
throw new Error(`Unknown prompt: ${name}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function generateResidentialPrompt(args) {
|
|
88
|
+
const { width = 15000, depth = 12000, rooms = 6 } = args;
|
|
89
|
+
const code = `(defun c:draw-residential (/ old-layer p)
|
|
90
|
+
(setq old-layer (getvar "clayer"))
|
|
91
|
+
(setvar "clayer" "0")
|
|
92
|
+
(setq p (list 0 0))
|
|
93
|
+
(command "_.rectangle" p (list ${width} ${depth}))
|
|
94
|
+
${generateRoomLayout(width, depth, rooms)}
|
|
95
|
+
(setvar "clayer" old-layer)
|
|
96
|
+
(princ)
|
|
97
|
+
)
|
|
98
|
+
(c:draw-residential)`;
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
messages: [{
|
|
102
|
+
role: "user",
|
|
103
|
+
content: {
|
|
104
|
+
type: "text",
|
|
105
|
+
text: `请在 CAD 中绘制农村民居平面图,尺寸 ${width}x${depth}mm,${rooms} 个房间。\n\n生成的 AutoLISP 代码:\n\`\`\`lisp\n${code}\n\`\`\`\n\n直接执行此代码即可生成图纸。`
|
|
106
|
+
}
|
|
107
|
+
}]
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function generateRoomLayout(width, depth, rooms) {
|
|
112
|
+
if (rooms === 6) {
|
|
113
|
+
return `(command "_.line" (list 5000 0) (list 5000 ${depth}) "")
|
|
114
|
+
(command "_.line" (list 0 4000) (list ${width} 4000) "")
|
|
115
|
+
(command "_.text" "j" "c" "2500,2000" 800 0 "客厅")
|
|
116
|
+
(command "_.text" "j" "c" "7500,6000" 800 0 "餐厅")
|
|
117
|
+
(command "_.text" "j" "c" "12500,2000" 800 0 "主卧")`;
|
|
118
|
+
}
|
|
119
|
+
return `(command "_.text" "j" "c" (list (/ ${width} 2) (/ ${depth} 2)) 800 0 "房间")`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function generateFloorPlanPrompt(args) {
|
|
123
|
+
const { floor = "一层", scale = "1:100" } = args;
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
messages: [{
|
|
127
|
+
role: "user",
|
|
128
|
+
content: {
|
|
129
|
+
type: "text",
|
|
130
|
+
text: `生成 ${floor} 平面图,比例 ${scale}。\n\n提示:\n1. 使用 RECTANGLE 命令绘制外墙\n2. 使用 LINE 命令绘制内部隔墙\n3. 使用 TEXT 命令添加房间标注\n4. 使用 DIMLINEAR 或 DIMALIGNED 添加尺寸标注\n\n需要我执行哪些操作?`
|
|
131
|
+
}
|
|
132
|
+
}]
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function generateAnalyzePrompt() {
|
|
137
|
+
let entities = { total: 0, byType: {} };
|
|
138
|
+
let layers = [];
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
if (cad.connected) {
|
|
142
|
+
const entResult = await cad.sendCommandWithResult(
|
|
143
|
+
`(progn(setq ss(ssget "_X"))(if ss(progn(setq elst(mapcar(quote(lambda(x)(cdr(assoc 0(entget x))))) (pickset:to-list ss)))(stat:stat elst))))`
|
|
144
|
+
);
|
|
145
|
+
if (entResult) {
|
|
146
|
+
try { entities = JSON.parse(entResult); } catch {}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const layerResult = await cad.sendCommandWithResult(
|
|
150
|
+
`(progn(setq res nil)(while(setq e(tblnext "LAYER"(null res)))(setq res(cons(cdr(assoc 2 e))res)))(reverse res))`
|
|
151
|
+
);
|
|
152
|
+
if (layerResult) {
|
|
153
|
+
try { layers = JSON.parse(layerResult); } catch {}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} catch (e) {}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
messages: [{
|
|
160
|
+
role: "user",
|
|
161
|
+
content: {
|
|
162
|
+
type: "text",
|
|
163
|
+
text: `图纸分析报告:
|
|
164
|
+
|
|
165
|
+
**实体统计:** ${entities.total} 个
|
|
166
|
+
${Object.entries(entities.byType).map(([t, c]) => `- ${t}: ${c}`).join('\n')}
|
|
167
|
+
|
|
168
|
+
**图层列表:** ${layers.length} 个
|
|
169
|
+
${layers.slice(0, 10).join(', ')}${layers.length > 10 ? '...' : ''}
|
|
170
|
+
|
|
171
|
+
**建议:**\n${generateSuggestions(entities, layers)}`
|
|
172
|
+
}
|
|
173
|
+
}]
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function generateSuggestions(entities, layers) {
|
|
178
|
+
const suggestions = [];
|
|
179
|
+
if (entities.total > 1000) suggestions.push("- 图纸实体较多,建议使用图层分类管理");
|
|
180
|
+
if (layers.length > 20) suggestions.push("- 图层较多,建议清理未使用的图层");
|
|
181
|
+
if (entities.byType.LINE && entities.byType.LINE > 500) suggestions.push("- 线条较多,可考虑使用 BLOCK 减少实体数量");
|
|
182
|
+
if (!entities.byType.DIMENSION) suggestions.push("- 未发现尺寸标注,建议添加标注");
|
|
183
|
+
return suggestions.length ? suggestions.join('\n') : '- 图纸结构良好';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function generateBatchLinesPrompt(args) {
|
|
187
|
+
const { count = 5, length = 1000 } = args;
|
|
188
|
+
|
|
189
|
+
const code = `(defun c:batch-lines (/ i)
|
|
190
|
+
(setq i 0)
|
|
191
|
+
(repeat ${count}
|
|
192
|
+
(command "_.line" (list (* i ${length}) 0) (list (* (1+ i) ${length}) 0) "")
|
|
193
|
+
(setq i (1+ i))
|
|
194
|
+
)
|
|
195
|
+
(princ)
|
|
196
|
+
)
|
|
197
|
+
(c:batch-lines)`;
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
messages: [{
|
|
201
|
+
role: "user",
|
|
202
|
+
content: {
|
|
203
|
+
type: "text",
|
|
204
|
+
text: `批量绘制 ${count} 条长度为 ${length} 的水平线段:\n\n\`\`\`lisp\n${code}\n\`\`\`\n\n此代码将绘制 ${count} 条首尾相连的水平线段。`
|
|
205
|
+
}
|
|
206
|
+
}]
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function generateDimensionPrompt(args) {
|
|
211
|
+
const { style = "aligned" } = args;
|
|
212
|
+
|
|
213
|
+
let dimStyle = "aligned";
|
|
214
|
+
let lispStyle = "DIMALIGNED";
|
|
215
|
+
if (style === "horizontal") { dimStyle = "水平"; lispStyle = "DIMLINEAR"; }
|
|
216
|
+
if (style === "vertical") { dimStyle = "垂直"; lispStyle = "DIMLINEAR"; }
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
messages: [{
|
|
220
|
+
role: "user",
|
|
221
|
+
content: {
|
|
222
|
+
type: "text",
|
|
223
|
+
text: `生成 ${dimStyle} 尺寸标注代码:\n\n\`\`\`lisp\n(command "_${lispStyle}" "0,0" "1000,0" "500,200" "")
|
|
224
|
+
\`\`\`\n\n参数说明:\n- 起点: 0,0\n- 终点: 1000,0\n- 标注位置: 500,200\n\n执行后将创建 ${dimStyle} 尺寸标注。`
|
|
225
|
+
}
|
|
226
|
+
}]
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function generateExportPrompt(args) {
|
|
231
|
+
const { format = "json" } = args;
|
|
232
|
+
|
|
233
|
+
const code = format === "json"
|
|
234
|
+
? `(progn
|
|
235
|
+
(setq ss(ssget "_X"))
|
|
236
|
+
(if ss
|
|
237
|
+
(progn
|
|
238
|
+
(setq elst(mapcar(quote(lambda(x)(list(cdr(assoc 0(entget x)))(vlax-get x 'ObjectName))))(pickset:to-list ss)))
|
|
239
|
+
(stat:stat(mapcar(quote car)elst))
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
)`
|
|
243
|
+
: `(progn
|
|
244
|
+
(princ "\\n实体统计:\\n")
|
|
245
|
+
(princ "类型 数量\\n")
|
|
246
|
+
(princ "---------------------\\n")
|
|
247
|
+
(setq ss(ssget "_X"))
|
|
248
|
+
(if ss
|
|
249
|
+
(progn
|
|
250
|
+
(setq elst(mapcar(quote(lambda(x)(cdr(assoc 0(entget x))))) (pickset:to-list ss)))
|
|
251
|
+
(foreach t '("LINE" "CIRCLE" "ARC" "TEXT" "MTEXT" "POLYLINE" "LWPOLYLINE" "INSERT")
|
|
252
|
+
(setq cnt(length(member t elst)))
|
|
253
|
+
(if(> cnt 0)(princ(strcat t " "(itoa cnt) "\\n")))
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
)`;
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
messages: [{
|
|
261
|
+
role: "user",
|
|
262
|
+
content: {
|
|
263
|
+
type: "text",
|
|
264
|
+
text: `导出实体统计 (${format} 格式):\n\n\`\`\`lisp\n${code}\n\`\`\`\n\n执行后将输出实体统计信息。`
|
|
265
|
+
}
|
|
266
|
+
}]
|
|
267
|
+
};
|
|
268
|
+
}
|