leafer-x-design-system 2.0.1
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/LICENSE +21 -0
- package/README.md +300 -0
- package/assets/README.md +16 -0
- package/assets/assetswechat-qr.png +0 -0
- package/assets/icon.svg +14 -0
- package/assets/wechat-pay.png +0 -0
- package/cli.js +163 -0
- package/examples/basic-usage.js +163 -0
- package/examples/express-server.js +279 -0
- package/index.d.ts +232 -0
- package/index.js +106 -0
- package/leafer-ai-layout-plugin-v2.js +370 -0
- package/leafer-ai-layout-plugin.js +341 -0
- package/leafer-design-system-generator.js +1102 -0
- package/leafer-design-system-pro.js +1194 -0
- package/leafer-renderer-fixed.js +661 -0
- package/leafer-renderer-v2.js +791 -0
- package/leafer-x-design-system.js +140 -0
- package/mcp-adapter.js +142 -0
- package/mcp-server-starter.js +85 -0
- package/mcp-server.js +300 -0
- package/mcp-service-config.js +24 -0
- package/package.json +55 -0
- package/start-mcp-service-v2.js +239 -0
- package/start-mcp-service.js +157 -0
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "leafer-x-design-system",
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "基于 LeaferJS 的高保真 UI 设计系统生成器",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"leafer-design": "cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node start-mcp-service-v2.js",
|
|
12
|
+
"generate": "node leafer-design-system-pro.js",
|
|
13
|
+
"render": "node render-pro-templates.js",
|
|
14
|
+
"test": "node test-mcp-render.js"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"leaferjs",
|
|
18
|
+
"ui",
|
|
19
|
+
"design-system",
|
|
20
|
+
"prototype",
|
|
21
|
+
"canvas",
|
|
22
|
+
"rendering"
|
|
23
|
+
],
|
|
24
|
+
"author": "尽为一土 <spring60@vip.qq.com>",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@leafer-ui/node": "^1.0.0",
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
29
|
+
"skia-canvas": "^1.0.0",
|
|
30
|
+
"uuid": "^9.0.0"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=16.0.0"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/q86830-hue/leafer-x-design-system.git"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/q86830-hue/leafer-x-design-system/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/q86830-hue/leafer-x-design-system#readme",
|
|
43
|
+
"files": [
|
|
44
|
+
"index.js",
|
|
45
|
+
"index.d.ts",
|
|
46
|
+
"cli.js",
|
|
47
|
+
"leafer-*.js",
|
|
48
|
+
"start-*.js",
|
|
49
|
+
"mcp-*.js",
|
|
50
|
+
"README.md",
|
|
51
|
+
"LICENSE",
|
|
52
|
+
"assets/",
|
|
53
|
+
"examples/"
|
|
54
|
+
]
|
|
55
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP 服务 V2 - 使用优化后的 LeaferRendererV2
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const http = require('http');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { v4: uuidv4 } = require('uuid');
|
|
9
|
+
|
|
10
|
+
// 使用本地输出目录
|
|
11
|
+
const outputDir = path.join(__dirname, 'output');
|
|
12
|
+
if (!fs.existsSync(outputDir)) {
|
|
13
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// 导入 V2 渲染器
|
|
17
|
+
const LeaferRendererV2 = require('./leafer-renderer-v2');
|
|
18
|
+
|
|
19
|
+
class MCPServerV2 {
|
|
20
|
+
constructor() {
|
|
21
|
+
this.port = process.env.MCP_PORT || 3456;
|
|
22
|
+
this.renderer = new LeaferRendererV2({
|
|
23
|
+
pixelRatio: 2,
|
|
24
|
+
backgroundColor: '#ffffff',
|
|
25
|
+
maxCacheSize: 100,
|
|
26
|
+
outputDir: outputDir
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async handleRequest(req, res) {
|
|
31
|
+
// 设置 CORS 头
|
|
32
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
33
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
34
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
35
|
+
|
|
36
|
+
if (req.method === 'OPTIONS') {
|
|
37
|
+
res.writeHead(200);
|
|
38
|
+
res.end();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const url = req.url;
|
|
43
|
+
const method = req.method;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// 健康检查
|
|
47
|
+
if (url === '/health' && method === 'GET') {
|
|
48
|
+
res.setHeader('Content-Type', 'application/json');
|
|
49
|
+
res.writeHead(200);
|
|
50
|
+
res.end(JSON.stringify({
|
|
51
|
+
status: 'healthy',
|
|
52
|
+
version: '2.0.0',
|
|
53
|
+
timestamp: new Date().toISOString(),
|
|
54
|
+
stats: this.renderer.getStats(),
|
|
55
|
+
outputDir: outputDir,
|
|
56
|
+
features: [
|
|
57
|
+
'rect', 'ellipse', 'circle', 'line', 'polygon', 'star',
|
|
58
|
+
'path', 'pen', 'text', 'image', 'group', 'box', 'frame'
|
|
59
|
+
]
|
|
60
|
+
}));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// UI 渲染端点
|
|
65
|
+
if (url === '/api/v1/ui/render' && method === 'POST') {
|
|
66
|
+
let body = '';
|
|
67
|
+
req.on('data', chunk => body += chunk);
|
|
68
|
+
req.on('end', async () => {
|
|
69
|
+
try {
|
|
70
|
+
const { width, height, elements, options = {} } = JSON.parse(body);
|
|
71
|
+
|
|
72
|
+
if (!width || !height || !elements) {
|
|
73
|
+
res.setHeader('Content-Type', 'application/json');
|
|
74
|
+
res.writeHead(400);
|
|
75
|
+
res.end(JSON.stringify({ error: 'Width, height, and elements are required' }));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const result = await this.renderer.render({
|
|
80
|
+
width,
|
|
81
|
+
height,
|
|
82
|
+
elements,
|
|
83
|
+
options: {
|
|
84
|
+
format: options.format || 'png',
|
|
85
|
+
quality: options.quality || 0.92,
|
|
86
|
+
pixelRatio: options.pixelRatio || 2,
|
|
87
|
+
backgroundColor: options.backgroundColor || '#ffffff'
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
res.setHeader('Content-Type', 'application/json');
|
|
92
|
+
res.writeHead(200);
|
|
93
|
+
res.end(JSON.stringify({
|
|
94
|
+
success: true,
|
|
95
|
+
requestId: uuidv4(),
|
|
96
|
+
data: result,
|
|
97
|
+
meta: {
|
|
98
|
+
generatedAt: new Date().toISOString(),
|
|
99
|
+
dimensions: { width, height }
|
|
100
|
+
}
|
|
101
|
+
}));
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('[MCP V2] Render error:', error);
|
|
104
|
+
res.setHeader('Content-Type', 'application/json');
|
|
105
|
+
res.writeHead(500);
|
|
106
|
+
res.end(JSON.stringify({
|
|
107
|
+
success: false,
|
|
108
|
+
error: error.message,
|
|
109
|
+
stack: error.stack
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 批量渲染端点
|
|
117
|
+
if (url === '/api/v1/ui/batch-render' && method === 'POST') {
|
|
118
|
+
let body = '';
|
|
119
|
+
req.on('data', chunk => body += chunk);
|
|
120
|
+
req.on('end', async () => {
|
|
121
|
+
try {
|
|
122
|
+
const { configs, options = {} } = JSON.parse(body);
|
|
123
|
+
|
|
124
|
+
if (!Array.isArray(configs)) {
|
|
125
|
+
res.setHeader('Content-Type', 'application/json');
|
|
126
|
+
res.writeHead(400);
|
|
127
|
+
res.end(JSON.stringify({ error: 'configs must be an array' }));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const results = await this.renderer.batchRender(configs, options);
|
|
132
|
+
|
|
133
|
+
res.setHeader('Content-Type', 'application/json');
|
|
134
|
+
res.writeHead(200);
|
|
135
|
+
res.end(JSON.stringify({
|
|
136
|
+
success: true,
|
|
137
|
+
requestId: uuidv4(),
|
|
138
|
+
data: results,
|
|
139
|
+
meta: {
|
|
140
|
+
generatedAt: new Date().toISOString(),
|
|
141
|
+
count: configs.length
|
|
142
|
+
}
|
|
143
|
+
}));
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error('[MCP V2] Batch render error:', error);
|
|
146
|
+
res.setHeader('Content-Type', 'application/json');
|
|
147
|
+
res.writeHead(500);
|
|
148
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 清空缓存
|
|
155
|
+
if (url === '/api/v1/cache/clear' && method === 'POST') {
|
|
156
|
+
this.renderer.clearCache();
|
|
157
|
+
res.setHeader('Content-Type', 'application/json');
|
|
158
|
+
res.writeHead(200);
|
|
159
|
+
res.end(JSON.stringify({ success: true, message: 'Cache cleared' }));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 获取统计信息
|
|
164
|
+
if (url === '/api/v1/stats' && method === 'GET') {
|
|
165
|
+
res.setHeader('Content-Type', 'application/json');
|
|
166
|
+
res.writeHead(200);
|
|
167
|
+
res.end(JSON.stringify({
|
|
168
|
+
success: true,
|
|
169
|
+
data: this.renderer.getStats()
|
|
170
|
+
}));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 获取生成的图片
|
|
175
|
+
if (url.startsWith('/output/') && method === 'GET') {
|
|
176
|
+
const filename = path.basename(url);
|
|
177
|
+
const filePath = path.join(outputDir, filename);
|
|
178
|
+
if (fs.existsSync(filePath)) {
|
|
179
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
180
|
+
const contentType = ext === '.png' ? 'image/png' :
|
|
181
|
+
ext === '.jpg' || ext === '.jpeg' ? 'image/jpeg' :
|
|
182
|
+
ext === '.webp' ? 'image/webp' : 'application/octet-stream';
|
|
183
|
+
res.setHeader('Content-Type', contentType);
|
|
184
|
+
res.writeHead(200);
|
|
185
|
+
fs.createReadStream(filePath).pipe(res);
|
|
186
|
+
} else {
|
|
187
|
+
res.setHeader('Content-Type', 'application/json');
|
|
188
|
+
res.writeHead(404);
|
|
189
|
+
res.end(JSON.stringify({ error: 'File not found' }));
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 404
|
|
195
|
+
res.setHeader('Content-Type', 'application/json');
|
|
196
|
+
res.writeHead(404);
|
|
197
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error('[MCP V2] Server error:', error);
|
|
200
|
+
res.setHeader('Content-Type', 'application/json');
|
|
201
|
+
res.writeHead(500);
|
|
202
|
+
res.end(JSON.stringify({ error: 'Internal server error' }));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
start() {
|
|
207
|
+
const server = http.createServer((req, res) => this.handleRequest(req, res));
|
|
208
|
+
|
|
209
|
+
// 处理端口冲突,自动尝试下一个端口
|
|
210
|
+
server.on('error', (err) => {
|
|
211
|
+
if (err.code === 'EADDRINUSE') {
|
|
212
|
+
console.log(`⚠️ 端口 ${this.port} 被占用,尝试端口 ${this.port + 1}...`);
|
|
213
|
+
this.port++;
|
|
214
|
+
server.listen(this.port);
|
|
215
|
+
} else {
|
|
216
|
+
console.error('❌ 服务器错误:', err.message);
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
server.listen(this.port, () => {
|
|
222
|
+
console.log('🚀 MCP Server V2 running on port', this.port);
|
|
223
|
+
console.log('📁 Output directory:', outputDir);
|
|
224
|
+
console.log('🎯 Health check: http://localhost:' + this.port + '/health');
|
|
225
|
+
console.log('✨ LeaferRenderer V2 with full feature support');
|
|
226
|
+
console.log('');
|
|
227
|
+
console.log('API Endpoints:');
|
|
228
|
+
console.log(' POST /api/v1/ui/render - Render UI elements');
|
|
229
|
+
console.log(' POST /api/v1/ui/batch-render - Batch render multiple UIs');
|
|
230
|
+
console.log(' POST /api/v1/cache/clear - Clear render cache');
|
|
231
|
+
console.log(' GET /api/v1/stats - Get renderer statistics');
|
|
232
|
+
console.log(' GET /output/:filename - Get generated image');
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 启动服务器
|
|
238
|
+
const server = new MCPServerV2();
|
|
239
|
+
server.start();
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本地 MCP 服务启动脚本 - 使用修复后的渲染器
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const http = require('http');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { v4: uuidv4 } = require('uuid');
|
|
9
|
+
|
|
10
|
+
// 使用本地输出目录
|
|
11
|
+
const outputDir = path.join(__dirname, 'output');
|
|
12
|
+
if (!fs.existsSync(outputDir)) {
|
|
13
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
14
|
+
console.log('[Local MCP] Created output directory:', outputDir);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 导入修复后的 LeaferRenderer
|
|
18
|
+
const LeaferRenderer = require('./leafer-renderer-fixed');
|
|
19
|
+
|
|
20
|
+
class LocalMCPServer {
|
|
21
|
+
constructor() {
|
|
22
|
+
this.port = process.env.MCP_PORT || 3001;
|
|
23
|
+
this.leaferRenderer = new LeaferRenderer({
|
|
24
|
+
pixelRatio: 2,
|
|
25
|
+
backgroundColor: '#ffffff',
|
|
26
|
+
maxCacheSize: 100,
|
|
27
|
+
outputDir: outputDir
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async handleRequest(req, res) {
|
|
32
|
+
// 设置 CORS 头
|
|
33
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
34
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
35
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
36
|
+
res.setHeader('Content-Type', 'application/json');
|
|
37
|
+
|
|
38
|
+
if (req.method === 'OPTIONS') {
|
|
39
|
+
res.writeHead(200);
|
|
40
|
+
res.end();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const url = req.url;
|
|
45
|
+
const method = req.method;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// 健康检查
|
|
49
|
+
if (url === '/health' && method === 'GET') {
|
|
50
|
+
res.writeHead(200);
|
|
51
|
+
res.end(JSON.stringify({
|
|
52
|
+
status: 'healthy',
|
|
53
|
+
timestamp: new Date().toISOString(),
|
|
54
|
+
version: '1.0.0',
|
|
55
|
+
outputDir: outputDir
|
|
56
|
+
}));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// UI 渲染端点
|
|
61
|
+
if (url === '/api/v1/ui/render' && method === 'POST') {
|
|
62
|
+
let body = '';
|
|
63
|
+
req.on('data', chunk => body += chunk);
|
|
64
|
+
req.on('end', async () => {
|
|
65
|
+
try {
|
|
66
|
+
const { width, height, elements, options = {} } = JSON.parse(body);
|
|
67
|
+
|
|
68
|
+
if (!width || !height || !elements) {
|
|
69
|
+
res.writeHead(400);
|
|
70
|
+
res.end(JSON.stringify({ error: 'Width, height, and elements are required' }));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log('[Local MCP] Rendering request:', { width, height, elementCount: elements.length });
|
|
75
|
+
|
|
76
|
+
const result = await this.leaferRenderer.render({
|
|
77
|
+
width,
|
|
78
|
+
height,
|
|
79
|
+
elements,
|
|
80
|
+
options: {
|
|
81
|
+
format: options.format || 'png',
|
|
82
|
+
quality: options.quality || 0.92,
|
|
83
|
+
pixelRatio: options.pixelRatio || 2,
|
|
84
|
+
backgroundColor: options.backgroundColor || '#ffffff'
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
res.writeHead(200);
|
|
89
|
+
res.end(JSON.stringify({
|
|
90
|
+
success: true,
|
|
91
|
+
requestId: uuidv4(),
|
|
92
|
+
data: {
|
|
93
|
+
url: result.url,
|
|
94
|
+
base64: result.base64,
|
|
95
|
+
format: result.format,
|
|
96
|
+
width: result.width,
|
|
97
|
+
height: result.height,
|
|
98
|
+
pixelRatio: result.pixelRatio,
|
|
99
|
+
cached: result.cached || false
|
|
100
|
+
},
|
|
101
|
+
meta: {
|
|
102
|
+
generatedAt: new Date().toISOString(),
|
|
103
|
+
dimensions: { width, height }
|
|
104
|
+
}
|
|
105
|
+
}));
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error('[Local MCP] UI render error:', error);
|
|
108
|
+
res.writeHead(500);
|
|
109
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 获取生成的图片
|
|
116
|
+
if (url.startsWith('/output/') && method === 'GET') {
|
|
117
|
+
const filename = path.basename(url);
|
|
118
|
+
const filePath = path.join(outputDir, filename);
|
|
119
|
+
if (fs.existsSync(filePath)) {
|
|
120
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
121
|
+
const contentType = ext === '.png' ? 'image/png' :
|
|
122
|
+
ext === '.jpg' || ext === '.jpeg' ? 'image/jpeg' :
|
|
123
|
+
ext === '.webp' ? 'image/webp' : 'application/octet-stream';
|
|
124
|
+
res.setHeader('Content-Type', contentType);
|
|
125
|
+
res.writeHead(200);
|
|
126
|
+
fs.createReadStream(filePath).pipe(res);
|
|
127
|
+
} else {
|
|
128
|
+
res.writeHead(404);
|
|
129
|
+
res.end(JSON.stringify({ error: 'File not found' }));
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 404
|
|
135
|
+
res.writeHead(404);
|
|
136
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error('[Local MCP] Server error:', error);
|
|
139
|
+
res.writeHead(500);
|
|
140
|
+
res.end(JSON.stringify({ error: 'Internal server error', message: error.message }));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
start() {
|
|
145
|
+
const server = http.createServer((req, res) => this.handleRequest(req, res));
|
|
146
|
+
server.listen(this.port, () => {
|
|
147
|
+
console.log(`🚀 Local MCP Server running on port ${this.port}`);
|
|
148
|
+
console.log(`📁 Output directory: ${outputDir}`);
|
|
149
|
+
console.log(`🎯 Health check: http://localhost:${this.port}/health`);
|
|
150
|
+
console.log(`✨ LeaferJS UI rendering enabled (FIXED VERSION)`);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 启动服务器
|
|
156
|
+
const server = new LocalMCPServer();
|
|
157
|
+
server.start();
|