design-learn-server 0.1.3 → 0.1.4
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 +0 -4
- package/package.json +3 -7
- package/src/cli.js +0 -0
- package/src/mcp/index.js +2 -2
- package/src/server.js +79 -61
- package/src/stdio.js +12 -11
- package/src/storage/index.js +2 -2
- package/src/storage/sqliteStore.js +112 -3
- package/src/check-node.js +0 -14
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "design-learn-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Design-Learn server skeleton for REST/WS/MCP routes",
|
|
6
6
|
"main": "src/server.js",
|
|
@@ -9,9 +9,6 @@
|
|
|
9
9
|
"design-learn-mcp": "src/stdio.js",
|
|
10
10
|
"design-learn-stdio": "src/stdio.js"
|
|
11
11
|
},
|
|
12
|
-
"engines": {
|
|
13
|
-
"node": ">=18 <23"
|
|
14
|
-
},
|
|
15
12
|
"files": [
|
|
16
13
|
"src",
|
|
17
14
|
"README.md"
|
|
@@ -21,13 +18,12 @@
|
|
|
21
18
|
},
|
|
22
19
|
"scripts": {
|
|
23
20
|
"start": "node src/server.js",
|
|
24
|
-
"dev": "node src/server.js"
|
|
25
|
-
"preinstall": "node src/check-node.js"
|
|
21
|
+
"dev": "node src/server.js"
|
|
26
22
|
},
|
|
27
23
|
"dependencies": {
|
|
28
24
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
29
|
-
"better-sqlite3": "^11.10.0",
|
|
30
25
|
"playwright": "^1.57.0",
|
|
26
|
+
"sql.js": "^1.13.0",
|
|
31
27
|
"zod": "^3.24.1"
|
|
32
28
|
}
|
|
33
29
|
}
|
package/src/cli.js
CHANGED
|
File without changes
|
package/src/mcp/index.js
CHANGED
|
@@ -445,8 +445,8 @@ function createMcpServer({ name, version, storage, uipro }) {
|
|
|
445
445
|
return server;
|
|
446
446
|
}
|
|
447
447
|
|
|
448
|
-
function createMcpHandler(options = {}) {
|
|
449
|
-
const storage = options.storage || createStorage({ dataDir: options.dataDir });
|
|
448
|
+
async function createMcpHandler(options = {}) {
|
|
449
|
+
const storage = options.storage || (await createStorage({ dataDir: options.dataDir }));
|
|
450
450
|
const ownsStorage = !options.storage;
|
|
451
451
|
const serverName = options.serverName || 'design-learn';
|
|
452
452
|
const serverVersion = options.serverVersion || '0.1.0';
|
package/src/server.js
CHANGED
|
@@ -22,69 +22,64 @@ const { readJson, writeJson } = require('./storage/fileStore');
|
|
|
22
22
|
const defaultDataDir = path.join(os.homedir(), '.design-learn', 'data');
|
|
23
23
|
const dataDir = process.env.DESIGN_LEARN_DATA_DIR || process.env.DATA_DIR || defaultDataDir;
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
let storage;
|
|
26
|
+
let uipro;
|
|
27
|
+
let extractionPipeline;
|
|
28
|
+
let previewPipeline;
|
|
29
|
+
let mcpHandler;
|
|
29
30
|
|
|
30
31
|
// import job -> designId,用于将 pipeline 状态回写到 design metadata(便于 UI 与 MCP 立刻可见)
|
|
31
32
|
const importJobDesignMap = new Map();
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const now = new Date().toISOString();
|
|
40
|
-
void (async () => {
|
|
41
|
-
const design = await storage.getDesign(designId);
|
|
42
|
-
const meta = design?.metadata || {};
|
|
43
|
-
const aiRequested = !!meta.aiRequested;
|
|
44
|
-
const aiCompleted = meta.processingMessage === 'ai_completed' || meta.aiCompleted;
|
|
45
|
-
|
|
46
|
-
const metaPatch = {
|
|
47
|
-
processingStatus: event.event === 'failed' ? 'failed' : event.event === 'completed' ? 'completed' : 'processing',
|
|
48
|
-
processingJobId: event.job.id,
|
|
49
|
-
processingUpdatedAt: now,
|
|
50
|
-
};
|
|
34
|
+
function registerPipelineHandlers() {
|
|
35
|
+
extractionPipeline.onProgress((event) => {
|
|
36
|
+
const designId = importJobDesignMap.get(event.job.id);
|
|
37
|
+
if (!designId) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
51
40
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
metaPatch
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
41
|
+
const now = new Date().toISOString();
|
|
42
|
+
void (async () => {
|
|
43
|
+
const design = await storage.getDesign(designId);
|
|
44
|
+
const meta = design?.metadata || {};
|
|
45
|
+
const aiRequested = !!meta.aiRequested;
|
|
46
|
+
const aiCompleted = meta.processingMessage === 'ai_completed' || meta.aiCompleted;
|
|
47
|
+
|
|
48
|
+
const metaPatch = {
|
|
49
|
+
processingStatus: event.event === 'failed' ? 'failed' : event.event === 'completed' ? 'completed' : 'processing',
|
|
50
|
+
processingJobId: event.job.id,
|
|
51
|
+
processingUpdatedAt: now,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (event.event === 'failed') {
|
|
55
|
+
metaPatch.processingError = event.job.error?.message || 'unknown_error';
|
|
56
|
+
metaPatch.processingMessage = 'failed';
|
|
66
57
|
metaPatch.processingProgress = 100;
|
|
58
|
+
} else if (event.event === 'completed') {
|
|
59
|
+
metaPatch.processingError = null;
|
|
60
|
+
metaPatch.lastImportAt = now;
|
|
61
|
+
metaPatch.lastImportVersionId = event.job.result?.versionId || null;
|
|
62
|
+
if (aiRequested && !aiCompleted) {
|
|
63
|
+
metaPatch.processingStatus = 'analyzing';
|
|
64
|
+
metaPatch.processingMessage = 'ai_pending';
|
|
65
|
+
metaPatch.processingProgress = 80;
|
|
66
|
+
} else {
|
|
67
|
+
metaPatch.processingMessage = 'completed';
|
|
68
|
+
metaPatch.processingProgress = 100;
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
metaPatch.processingMessage = event.job.message || 'processing';
|
|
72
|
+
metaPatch.processingProgress = event.job.progress || 0;
|
|
67
73
|
}
|
|
68
|
-
} else {
|
|
69
|
-
metaPatch.processingMessage = event.job.message || 'processing';
|
|
70
|
-
metaPatch.processingProgress = event.job.progress || 0;
|
|
71
|
-
}
|
|
72
74
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
await storage.updateDesign(designId, { metadata: metaPatch });
|
|
76
|
+
})().catch(() => undefined);
|
|
75
77
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
storage,
|
|
82
|
-
dataDir: process.env.DESIGN_LEARN_DATA_DIR,
|
|
83
|
-
serverName: process.env.MCP_SERVER_NAME,
|
|
84
|
-
serverVersion: process.env.MCP_SERVER_VERSION,
|
|
85
|
-
authToken: process.env.MCP_AUTH_TOKEN,
|
|
86
|
-
uipro,
|
|
87
|
-
});
|
|
78
|
+
if (event.event === 'failed' || event.event === 'completed') {
|
|
79
|
+
importJobDesignMap.delete(event.job.id);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
88
83
|
|
|
89
84
|
const routes = [
|
|
90
85
|
{
|
|
@@ -1367,17 +1362,40 @@ server.on('clientError', (err, socket) => {
|
|
|
1367
1362
|
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
1368
1363
|
});
|
|
1369
1364
|
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1365
|
+
async function start() {
|
|
1366
|
+
storage = await createStorage({ dataDir });
|
|
1367
|
+
uipro = createUipro({ dataDir });
|
|
1368
|
+
extractionPipeline = createExtractionPipeline({ storage });
|
|
1369
|
+
previewPipeline = createPreviewPipeline({ storage });
|
|
1370
|
+
registerPipelineHandlers();
|
|
1371
|
+
mcpHandler = await createMcpHandler({
|
|
1372
|
+
storage,
|
|
1373
|
+
dataDir: process.env.DESIGN_LEARN_DATA_DIR,
|
|
1374
|
+
serverName: process.env.MCP_SERVER_NAME,
|
|
1375
|
+
serverVersion: process.env.MCP_SERVER_VERSION,
|
|
1376
|
+
authToken: process.env.MCP_AUTH_TOKEN,
|
|
1377
|
+
uipro,
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
server.listen(DEFAULT_PORT, () => {
|
|
1381
|
+
console.log(`[design-learn-server] listening on http://localhost:${DEFAULT_PORT}`);
|
|
1382
|
+
console.log(`[design-learn-server] data dir: ${storage.dataDir}`);
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
start().catch((error) => {
|
|
1387
|
+
console.error('[design-learn-server] startup failed', error);
|
|
1388
|
+
process.exit(1);
|
|
1373
1389
|
});
|
|
1374
1390
|
|
|
1375
1391
|
function shutdown(signal) {
|
|
1376
1392
|
console.log(`[design-learn-server] received ${signal}, shutting down`);
|
|
1377
|
-
mcpHandler
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1393
|
+
if (mcpHandler) {
|
|
1394
|
+
mcpHandler.close().catch((error) => console.error('[mcp] close error', error));
|
|
1395
|
+
}
|
|
1396
|
+
extractionPipeline?.close();
|
|
1397
|
+
previewPipeline?.close();
|
|
1398
|
+
storage?.close();
|
|
1381
1399
|
server.close(() => process.exit(0));
|
|
1382
1400
|
}
|
|
1383
1401
|
|
package/src/stdio.js
CHANGED
|
@@ -55,11 +55,12 @@ async function ensurePlaywright() {
|
|
|
55
55
|
const os = require('os');
|
|
56
56
|
const defaultDataDir = path.join(os.homedir(), '.design-learn', 'data');
|
|
57
57
|
const dataDir = process.env.DESIGN_LEARN_DATA_DIR || process.env.DATA_DIR || defaultDataDir;
|
|
58
|
-
const
|
|
58
|
+
const storagePromise = createStorage({ dataDir });
|
|
59
59
|
const uipro = createUipro({ dataDir });
|
|
60
60
|
|
|
61
|
-
function matchDesigns(query) {
|
|
61
|
+
async function matchDesigns(query) {
|
|
62
62
|
const needle = query.toLowerCase();
|
|
63
|
+
const storage = await storagePromise;
|
|
63
64
|
const designs = storage.listDesigns();
|
|
64
65
|
return designs.filter((design) => {
|
|
65
66
|
const tags = Array.isArray(design.metadata?.tags) ? design.metadata.tags.join(' ') : '';
|
|
@@ -91,6 +92,7 @@ server.tool(
|
|
|
91
92
|
'List stored design resources',
|
|
92
93
|
{ limit: z.number().min(1).max(100).optional() },
|
|
93
94
|
async ({ limit }) => {
|
|
95
|
+
const storage = await storagePromise;
|
|
94
96
|
const designs = storage.listDesigns();
|
|
95
97
|
const data = typeof limit === 'number' ? designs.slice(0, limit) : designs;
|
|
96
98
|
return {
|
|
@@ -108,7 +110,7 @@ server.tool(
|
|
|
108
110
|
limit: z.number().min(1).max(100).optional(),
|
|
109
111
|
},
|
|
110
112
|
async ({ query, limit }) => {
|
|
111
|
-
const matches = matchDesigns(query);
|
|
113
|
+
const matches = await matchDesigns(query);
|
|
112
114
|
const data = typeof limit === 'number' ? matches.slice(0, limit) : matches;
|
|
113
115
|
return {
|
|
114
116
|
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
@@ -135,14 +137,12 @@ server.tool(
|
|
|
135
137
|
},
|
|
136
138
|
async ({ query, sources, domain, stack, limit, designLimit }) => {
|
|
137
139
|
const effectiveSources = Array.isArray(sources) && sources.length > 0 ? sources : ['designs', 'uipro'];
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
})()
|
|
145
|
-
: [];
|
|
140
|
+
let designs = [];
|
|
141
|
+
if (effectiveSources.includes('designs')) {
|
|
142
|
+
const matches = await matchDesigns(query);
|
|
143
|
+
const max = typeof designLimit === 'number' ? designLimit : typeof limit === 'number' ? limit : undefined;
|
|
144
|
+
designs = typeof max === 'number' ? matches.slice(0, max) : matches;
|
|
145
|
+
}
|
|
146
146
|
|
|
147
147
|
const resolvedDomain = domain === 'auto' ? undefined : domain;
|
|
148
148
|
const maxUipro = typeof limit === 'number' ? limit : 10;
|
|
@@ -201,6 +201,7 @@ server.tool(
|
|
|
201
201
|
'Get styleguide markdown by design ID (latest version).',
|
|
202
202
|
{ designId: z.string() },
|
|
203
203
|
async ({ designId }) => {
|
|
204
|
+
const storage = await storagePromise;
|
|
204
205
|
const versions = storage.listVersions(designId);
|
|
205
206
|
if (!versions || versions.length === 0) {
|
|
206
207
|
return {
|
package/src/storage/index.js
CHANGED
|
@@ -19,9 +19,9 @@ const {
|
|
|
19
19
|
const { ensureDir, writeJson, readJson, writeText, readText, removePath } = require('./fileStore');
|
|
20
20
|
const { openDatabase } = require('./sqliteStore');
|
|
21
21
|
|
|
22
|
-
function createStorage(options = {}) {
|
|
22
|
+
async function createStorage(options = {}) {
|
|
23
23
|
const dataDir = resolveDataDir(options.dataDir);
|
|
24
|
-
const db = openDatabase(getDatabasePath(dataDir));
|
|
24
|
+
const db = await openDatabase(getDatabasePath(dataDir));
|
|
25
25
|
|
|
26
26
|
return {
|
|
27
27
|
dataDir,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const
|
|
3
|
+
const initSqlJs = require('sql.js');
|
|
4
4
|
|
|
5
5
|
const SCHEMA_VERSION = 2;
|
|
6
6
|
|
|
@@ -8,9 +8,118 @@ function ensureDir(dirPath) {
|
|
|
8
8
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
function
|
|
11
|
+
function readDatabaseFile(dbPath) {
|
|
12
|
+
try {
|
|
13
|
+
return fs.readFileSync(dbPath);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
if (error.code === 'ENOENT') {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function wrapDatabase(rawDb, dbPath) {
|
|
23
|
+
let closed = false;
|
|
24
|
+
|
|
25
|
+
function persist() {
|
|
26
|
+
if (closed) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const data = rawDb.export();
|
|
30
|
+
const buffer = Buffer.from(data);
|
|
31
|
+
fs.writeFileSync(dbPath, buffer);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function prepare(sql) {
|
|
35
|
+
const stmt = rawDb.prepare(sql);
|
|
36
|
+
const normalizeParams = (params) => (params.length === 1 && Array.isArray(params[0]) ? params[0] : params);
|
|
37
|
+
const finalize = () => {
|
|
38
|
+
try {
|
|
39
|
+
stmt.free();
|
|
40
|
+
} catch {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
run: (...params) => {
|
|
47
|
+
const bound = normalizeParams(params);
|
|
48
|
+
stmt.run(bound);
|
|
49
|
+
const changes = typeof rawDb.getRowsModified === 'function' ? rawDb.getRowsModified() : 0;
|
|
50
|
+
finalize();
|
|
51
|
+
persist();
|
|
52
|
+
return { changes };
|
|
53
|
+
},
|
|
54
|
+
get: (...params) => {
|
|
55
|
+
const bound = normalizeParams(params);
|
|
56
|
+
stmt.bind(bound);
|
|
57
|
+
if (!stmt.step()) {
|
|
58
|
+
finalize();
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const row = stmt.getAsObject();
|
|
62
|
+
finalize();
|
|
63
|
+
return row;
|
|
64
|
+
},
|
|
65
|
+
all: (...params) => {
|
|
66
|
+
const bound = normalizeParams(params);
|
|
67
|
+
const rows = [];
|
|
68
|
+
stmt.bind(bound);
|
|
69
|
+
while (stmt.step()) {
|
|
70
|
+
rows.push(stmt.getAsObject());
|
|
71
|
+
}
|
|
72
|
+
finalize();
|
|
73
|
+
return rows;
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function exec(sql) {
|
|
79
|
+
rawDb.exec(sql);
|
|
80
|
+
persist();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function pragma(statement, options = {}) {
|
|
84
|
+
const trimmed = statement.trim();
|
|
85
|
+
if (trimmed.includes('=')) {
|
|
86
|
+
rawDb.exec(`PRAGMA ${statement}`);
|
|
87
|
+
persist();
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
const result = rawDb.exec(`PRAGMA ${statement}`);
|
|
91
|
+
if (options.simple) {
|
|
92
|
+
return result?.[0]?.values?.[0]?.[0] ?? 0;
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function close() {
|
|
98
|
+
if (closed) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
persist();
|
|
102
|
+
rawDb.close();
|
|
103
|
+
closed = true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
prepare,
|
|
108
|
+
exec,
|
|
109
|
+
pragma,
|
|
110
|
+
close,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function openDatabase(dbPath) {
|
|
12
115
|
ensureDir(path.dirname(dbPath));
|
|
13
|
-
const
|
|
116
|
+
const wasmPath = require.resolve('sql.js/dist/sql-wasm.wasm');
|
|
117
|
+
const SQL = await initSqlJs({
|
|
118
|
+
locateFile: () => wasmPath,
|
|
119
|
+
});
|
|
120
|
+
const fileBuffer = readDatabaseFile(dbPath);
|
|
121
|
+
const rawDb = fileBuffer ? new SQL.Database(fileBuffer) : new SQL.Database();
|
|
122
|
+
const db = wrapDatabase(rawDb, dbPath);
|
|
14
123
|
db.pragma('journal_mode = WAL');
|
|
15
124
|
migrate(db);
|
|
16
125
|
return db;
|
package/src/check-node.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
const major = Number(process.versions.node.split(".")[0]);
|
|
4
|
-
const isSupported = major >= 18 && major < 23;
|
|
5
|
-
|
|
6
|
-
if (!isSupported) {
|
|
7
|
-
console.error("=== Node 版本不支持 ===");
|
|
8
|
-
console.error(
|
|
9
|
-
`[design-learn] 仅支持 Node 18/20,当前版本 ${process.versions.node}。`
|
|
10
|
-
);
|
|
11
|
-
console.error("[design-learn] 请切换到 Node 18/20 后重试。");
|
|
12
|
-
process.exit(1);
|
|
13
|
-
}
|
|
14
|
-
|