design-learn-server 0.1.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.
Files changed (37) hide show
  1. package/README.md +123 -0
  2. package/package.json +29 -0
  3. package/src/cli.js +152 -0
  4. package/src/mcp/index.js +556 -0
  5. package/src/pipeline/index.js +335 -0
  6. package/src/playwrightSupport.js +65 -0
  7. package/src/preview/index.js +204 -0
  8. package/src/server.js +1385 -0
  9. package/src/stdio.js +464 -0
  10. package/src/storage/fileStore.js +45 -0
  11. package/src/storage/index.js +983 -0
  12. package/src/storage/paths.js +113 -0
  13. package/src/storage/sqliteStore.js +114 -0
  14. package/src/uipro/bm25.js +121 -0
  15. package/src/uipro/config.js +264 -0
  16. package/src/uipro/csv.js +90 -0
  17. package/src/uipro/data/charts.csv +26 -0
  18. package/src/uipro/data/colors.csv +97 -0
  19. package/src/uipro/data/icons.csv +101 -0
  20. package/src/uipro/data/landing.csv +31 -0
  21. package/src/uipro/data/products.csv +97 -0
  22. package/src/uipro/data/prompts.csv +24 -0
  23. package/src/uipro/data/stacks/flutter.csv +53 -0
  24. package/src/uipro/data/stacks/html-tailwind.csv +56 -0
  25. package/src/uipro/data/stacks/nextjs.csv +53 -0
  26. package/src/uipro/data/stacks/nuxt-ui.csv +51 -0
  27. package/src/uipro/data/stacks/nuxtjs.csv +59 -0
  28. package/src/uipro/data/stacks/react-native.csv +52 -0
  29. package/src/uipro/data/stacks/react.csv +54 -0
  30. package/src/uipro/data/stacks/shadcn.csv +61 -0
  31. package/src/uipro/data/stacks/svelte.csv +54 -0
  32. package/src/uipro/data/stacks/swiftui.csv +51 -0
  33. package/src/uipro/data/stacks/vue.csv +50 -0
  34. package/src/uipro/data/styles.csv +59 -0
  35. package/src/uipro/data/typography.csv +58 -0
  36. package/src/uipro/data/ux-guidelines.csv +100 -0
  37. package/src/uipro/index.js +581 -0
@@ -0,0 +1,983 @@
1
+ const crypto = require('crypto');
2
+ const path = require('path');
3
+
4
+ const {
5
+ resolveDataDir,
6
+ getDatabasePath,
7
+ getDesignDir,
8
+ getDesignMetaPath,
9
+ getDesignIndexPath,
10
+ getVersionDir,
11
+ getStyleguidePath,
12
+ getRulesPath,
13
+ getSnapshotsPath,
14
+ getComponentCodePath,
15
+ getComponentsIndexPath,
16
+ getRulePath,
17
+ getRulesIndexPath,
18
+ } = require('./paths');
19
+ const { ensureDir, writeJson, readJson, writeText, readText, removePath } = require('./fileStore');
20
+ const { openDatabase } = require('./sqliteStore');
21
+
22
+ function createStorage(options = {}) {
23
+ const dataDir = resolveDataDir(options.dataDir);
24
+ const db = openDatabase(getDatabasePath(dataDir));
25
+
26
+ return {
27
+ dataDir,
28
+ close: () => db.close(),
29
+ rebuildIndexes: () => rebuildIndexes(db, dataDir),
30
+ createDesign: (input) => createDesign(db, dataDir, input),
31
+ listDesigns: () => listDesigns(db),
32
+ getDesign: (designId) => getDesign(db, dataDir, designId),
33
+ updateDesign: (designId, patch) => updateDesign(db, dataDir, designId, patch),
34
+ deleteDesign: (designId) => deleteDesign(db, dataDir, designId),
35
+ createVersion: (input) => createVersion(db, dataDir, input),
36
+ listVersions: (designId) => listVersions(db, designId),
37
+ getVersion: (versionId) => getVersion(db, dataDir, versionId),
38
+ updateVersion: (versionId, patch) => updateVersion(db, dataDir, versionId, patch),
39
+ deleteVersion: (versionId) => deleteVersion(db, dataDir, versionId),
40
+ listSnapshots: (filters) => listSnapshots(db, dataDir, filters),
41
+ getSnapshot: (snapshotId) => getSnapshot(db, dataDir, snapshotId),
42
+ deleteSnapshot: (snapshotId) => deleteSnapshot(db, dataDir, snapshotId),
43
+ createComponent: (input) => createComponent(db, dataDir, input),
44
+ updateComponentPreview: (componentId, preview) =>
45
+ updateComponentPreview(db, dataDir, componentId, preview),
46
+ listComponents: (filters) => listComponents(db, dataDir, filters),
47
+ getComponent: (componentId) => getComponent(db, dataDir, componentId),
48
+ deleteComponent: (componentId) => deleteComponent(db, dataDir, componentId),
49
+ createRule: (input) => createRule(db, dataDir, input),
50
+ listRules: (versionId) => listRules(db, dataDir, versionId),
51
+ getRule: (ruleId) => getRule(db, dataDir, ruleId),
52
+ deleteRule: (ruleId) => deleteRule(db, dataDir, ruleId),
53
+ // 任务管理方法
54
+ createTask: (input) => createTask(db, input),
55
+ listTasks: (filters) => listTasks(db, filters),
56
+ getTask: (taskId) => getTask(db, taskId),
57
+ updateTask: (taskId, patch) => updateTask(db, taskId, patch),
58
+ deleteTask: (taskId) => deleteTask(db, taskId),
59
+ clearCompletedTasks: () => clearCompletedTasks(db),
60
+ };
61
+ }
62
+
63
+ function normalizeDesign(input) {
64
+ const now = new Date().toISOString();
65
+ const stats = input.stats || {};
66
+ const metadata = input.metadata || {};
67
+ return {
68
+ id: input.id || crypto.randomUUID(),
69
+ name: input.name || '',
70
+ url: input.url || '',
71
+ source: input.source || 'import',
72
+ category: input.category || '',
73
+ description: input.description || '',
74
+ thumbnail: input.thumbnail || '',
75
+ stats: {
76
+ ...stats,
77
+ components: stats.components ?? 0,
78
+ versions: stats.versions ?? 0,
79
+ lastAnalyzedAt: stats.lastAnalyzedAt ?? null,
80
+ },
81
+ metadata: {
82
+ ...metadata,
83
+ extractedFrom: metadata.extractedFrom || 'unknown',
84
+ extractorVersion: metadata.extractorVersion || '',
85
+ tags: Array.isArray(metadata.tags) ? metadata.tags : [],
86
+ },
87
+ createdAt: input.createdAt || now,
88
+ updatedAt: input.updatedAt || now,
89
+ };
90
+ }
91
+
92
+ function mapDesignRow(row) {
93
+ return {
94
+ id: row.id,
95
+ name: row.name,
96
+ url: row.url,
97
+ source: row.source,
98
+ category: row.category,
99
+ description: row.description,
100
+ thumbnail: row.thumbnail,
101
+ stats: JSON.parse(row.stats_json || '{}'),
102
+ metadata: JSON.parse(row.metadata_json || '{}'),
103
+ createdAt: row.created_at,
104
+ updatedAt: row.updated_at,
105
+ };
106
+ }
107
+
108
+ async function writeDesignIndex(db, dataDir) {
109
+ const rows = db
110
+ .prepare('SELECT id, name, url, category, updated_at FROM designs ORDER BY updated_at DESC')
111
+ .all();
112
+ await writeJson(getDesignIndexPath(dataDir), rows);
113
+ }
114
+
115
+ function shouldUseIndex() {
116
+ const flag = (process.env.DESIGN_LEARN_USE_INDEX || '').toLowerCase();
117
+ return flag === '1' || flag === 'true' || flag === 'yes';
118
+ }
119
+
120
+ async function readIndexFile(indexPath) {
121
+ try {
122
+ const data = await readJson(indexPath);
123
+ return Array.isArray(data) ? data : [];
124
+ } catch (error) {
125
+ if (error.code === 'ENOENT') {
126
+ return null;
127
+ }
128
+ throw error;
129
+ }
130
+ }
131
+
132
+ async function writeComponentIndex(db, dataDir) {
133
+ const rows = db.prepare('SELECT * FROM components ORDER BY created_at DESC').all();
134
+ const items = rows.map((row) => ({
135
+ id: row.id,
136
+ designId: row.design_id,
137
+ versionId: row.version_id,
138
+ name: row.name,
139
+ type: row.type,
140
+ createdAt: row.created_at,
141
+ }));
142
+ await writeJson(getComponentsIndexPath(dataDir), items);
143
+ }
144
+
145
+ async function writeRuleIndex(db, dataDir) {
146
+ const rows = db.prepare('SELECT * FROM rules ORDER BY created_at DESC').all();
147
+ const items = rows.map((row) => ({
148
+ id: row.id,
149
+ versionId: row.version_id,
150
+ type: row.type,
151
+ name: row.name,
152
+ value: row.value,
153
+ createdAt: row.created_at,
154
+ }));
155
+ await writeJson(getRulesIndexPath(dataDir), items);
156
+ }
157
+
158
+ async function rebuildIndexes(db, dataDir) {
159
+ await writeDesignIndex(db, dataDir);
160
+ await writeComponentIndex(db, dataDir);
161
+ await writeRuleIndex(db, dataDir);
162
+ }
163
+
164
+ async function createDesign(db, dataDir, input) {
165
+ const design = normalizeDesign(input);
166
+ const designPath = getDesignMetaPath(dataDir, design.id);
167
+
168
+ await writeJson(designPath, design);
169
+ db.prepare(
170
+ `INSERT INTO designs (
171
+ id, name, url, source, category, description, thumbnail,
172
+ stats_json, metadata_json, design_path, created_at, updated_at
173
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
174
+ ).run(
175
+ design.id,
176
+ design.name,
177
+ design.url,
178
+ design.source,
179
+ design.category,
180
+ design.description,
181
+ design.thumbnail,
182
+ JSON.stringify(design.stats),
183
+ JSON.stringify(design.metadata),
184
+ designPath,
185
+ design.createdAt,
186
+ design.updatedAt
187
+ );
188
+
189
+ await writeDesignIndex(db, dataDir);
190
+ return design;
191
+ }
192
+
193
+ function listDesigns(db) {
194
+ const rows = db.prepare('SELECT * FROM designs ORDER BY updated_at DESC').all();
195
+ return rows.map(mapDesignRow);
196
+ }
197
+
198
+ async function getDesign(db, dataDir, designId) {
199
+ const row = db.prepare('SELECT * FROM designs WHERE id = ?').get(designId);
200
+ if (!row) {
201
+ return null;
202
+ }
203
+
204
+ try {
205
+ return await readJson(getDesignMetaPath(dataDir, designId));
206
+ } catch (error) {
207
+ return mapDesignRow(row);
208
+ }
209
+ }
210
+
211
+ async function updateDesign(db, dataDir, designId, patch) {
212
+ const existing = await getDesign(db, dataDir, designId);
213
+ if (!existing) {
214
+ return null;
215
+ }
216
+
217
+ const merged = { ...existing, ...patch, id: designId, createdAt: existing.createdAt };
218
+ if (patch && typeof patch === 'object') {
219
+ if (patch.metadata && typeof patch.metadata === 'object') {
220
+ merged.metadata = { ...(existing.metadata || {}), ...(patch.metadata || {}) };
221
+ }
222
+ if (patch.stats && typeof patch.stats === 'object') {
223
+ merged.stats = { ...(existing.stats || {}), ...(patch.stats || {}) };
224
+ }
225
+ }
226
+
227
+ const updated = normalizeDesign(merged);
228
+ updated.updatedAt = new Date().toISOString();
229
+
230
+ await writeJson(getDesignMetaPath(dataDir, designId), updated);
231
+ db.prepare(
232
+ `UPDATE designs SET
233
+ name = ?, url = ?, source = ?, category = ?, description = ?, thumbnail = ?,
234
+ stats_json = ?, metadata_json = ?, updated_at = ?
235
+ WHERE id = ?`
236
+ ).run(
237
+ updated.name,
238
+ updated.url,
239
+ updated.source,
240
+ updated.category,
241
+ updated.description,
242
+ updated.thumbnail,
243
+ JSON.stringify(updated.stats),
244
+ JSON.stringify(updated.metadata),
245
+ updated.updatedAt,
246
+ designId
247
+ );
248
+
249
+ await writeDesignIndex(db, dataDir);
250
+ return updated;
251
+ }
252
+
253
+ async function deleteDesign(db, dataDir, designId) {
254
+ db.prepare('DELETE FROM designs WHERE id = ?').run(designId);
255
+ await removePath(getDesignDir(dataDir, designId));
256
+ await writeDesignIndex(db, dataDir);
257
+ }
258
+
259
+ async function createVersion(db, dataDir, input) {
260
+ const designId = input.designId;
261
+ if (!designId) {
262
+ throw new Error('designId is required');
263
+ }
264
+
265
+ const design = db.prepare('SELECT * FROM designs WHERE id = ?').get(designId);
266
+ if (!design) {
267
+ throw new Error(`design ${designId} not found`);
268
+ }
269
+
270
+ const now = new Date().toISOString();
271
+ const versionNumber =
272
+ input.versionNumber ??
273
+ (db.prepare('SELECT MAX(version_number) as max FROM versions WHERE design_id = ?').get(designId)
274
+ .max || 0) +
275
+ 1;
276
+ const versionId = input.id || crypto.randomUUID();
277
+ const versionDir = getVersionDir(dataDir, designId, versionNumber);
278
+ await ensureDir(versionDir);
279
+
280
+ const styleguidePath = getStyleguidePath(dataDir, designId, versionNumber);
281
+ const rulesPath = getRulesPath(dataDir, designId, versionNumber);
282
+ const snapshotsPath = getSnapshotsPath(dataDir, designId, versionNumber);
283
+
284
+ await writeText(styleguidePath, input.styleguideMarkdown || '');
285
+ await writeJson(rulesPath, input.rules || {});
286
+ await writeJson(snapshotsPath, input.snapshots || []);
287
+
288
+ db.prepare(
289
+ `INSERT INTO versions (
290
+ id, design_id, version_number, styleguide_path, rules_path, snapshots_path, created_at, created_by
291
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
292
+ ).run(
293
+ versionId,
294
+ designId,
295
+ versionNumber,
296
+ styleguidePath,
297
+ rulesPath,
298
+ snapshotsPath,
299
+ now,
300
+ input.createdBy || 'user'
301
+ );
302
+
303
+ const designMeta = await getDesign(db, dataDir, designId);
304
+ if (designMeta) {
305
+ designMeta.stats.versions = (designMeta.stats.versions || 0) + 1;
306
+ await updateDesign(db, dataDir, designId, designMeta);
307
+ }
308
+
309
+ return {
310
+ id: versionId,
311
+ designId,
312
+ versionNumber,
313
+ styleguideMarkdown: input.styleguideMarkdown || '',
314
+ rules: input.rules || {},
315
+ snapshots: input.snapshots || [],
316
+ createdAt: now,
317
+ createdBy: input.createdBy || 'user',
318
+ };
319
+ }
320
+
321
+ function listVersions(db, designId) {
322
+ const rows = db
323
+ .prepare('SELECT * FROM versions WHERE design_id = ? ORDER BY version_number DESC')
324
+ .all(designId);
325
+ return rows.map((row) => ({
326
+ id: row.id,
327
+ designId: row.design_id,
328
+ versionNumber: row.version_number,
329
+ createdAt: row.created_at,
330
+ createdBy: row.created_by,
331
+ }));
332
+ }
333
+
334
+ async function getVersion(db, dataDir, versionId) {
335
+ const row = db.prepare('SELECT * FROM versions WHERE id = ?').get(versionId);
336
+ if (!row) {
337
+ return null;
338
+ }
339
+
340
+ const rules = await readJson(row.rules_path);
341
+ const snapshots = await readJson(row.snapshots_path);
342
+ const styleguideMarkdown = await readText(row.styleguide_path);
343
+
344
+ return {
345
+ id: row.id,
346
+ designId: row.design_id,
347
+ versionNumber: row.version_number,
348
+ styleguideMarkdown,
349
+ rules,
350
+ snapshots,
351
+ createdAt: row.created_at,
352
+ createdBy: row.created_by,
353
+ };
354
+ }
355
+
356
+ async function updateVersion(db, dataDir, versionId, patch = {}) {
357
+ const row = db.prepare('SELECT * FROM versions WHERE id = ?').get(versionId);
358
+ if (!row) {
359
+ return null;
360
+ }
361
+
362
+ if (Object.prototype.hasOwnProperty.call(patch, 'styleguideMarkdown')) {
363
+ await writeText(row.styleguide_path, patch.styleguideMarkdown || '');
364
+ }
365
+ if (Object.prototype.hasOwnProperty.call(patch, 'rules')) {
366
+ await writeJson(row.rules_path, patch.rules || {});
367
+ }
368
+
369
+ const snapshots = await readJson(row.snapshots_path);
370
+ const styleguideMarkdown = Object.prototype.hasOwnProperty.call(patch, 'styleguideMarkdown')
371
+ ? patch.styleguideMarkdown || ''
372
+ : await readText(row.styleguide_path);
373
+ const rules = Object.prototype.hasOwnProperty.call(patch, 'rules')
374
+ ? patch.rules || {}
375
+ : await readJson(row.rules_path);
376
+
377
+ return {
378
+ id: row.id,
379
+ designId: row.design_id,
380
+ versionNumber: row.version_number,
381
+ styleguideMarkdown,
382
+ rules,
383
+ snapshots,
384
+ createdAt: row.created_at,
385
+ createdBy: row.created_by,
386
+ };
387
+ }
388
+
389
+ async function deleteVersion(db, dataDir, versionId) {
390
+ const row = db.prepare('SELECT * FROM versions WHERE id = ?').get(versionId);
391
+ if (!row) {
392
+ return;
393
+ }
394
+
395
+ db.prepare('DELETE FROM versions WHERE id = ?').run(versionId);
396
+ await removePath(getVersionDir(dataDir, row.design_id, row.version_number));
397
+
398
+ const designMeta = await getDesign(db, dataDir, row.design_id);
399
+ if (designMeta) {
400
+ designMeta.stats.versions = Math.max(0, (designMeta.stats.versions || 1) - 1);
401
+ await updateDesign(db, dataDir, row.design_id, designMeta);
402
+ }
403
+ }
404
+
405
+ function normalizeSnapshot(snapshot, versionRow, index) {
406
+ const snapshotId =
407
+ snapshot && snapshot.id != null ? String(snapshot.id) : `${versionRow.id}:${index}`;
408
+ return {
409
+ id: snapshotId,
410
+ designId: versionRow.design_id,
411
+ versionId: versionRow.id,
412
+ versionNumber: versionRow.version_number,
413
+ url: snapshot?.url || '',
414
+ title: snapshot?.title || '',
415
+ html: snapshot?.html || '',
416
+ css: snapshot?.css || '',
417
+ metadata: snapshot?.metadata || {},
418
+ createdAt: snapshot?.createdAt || versionRow.created_at,
419
+ };
420
+ }
421
+
422
+ function parseSnapshotId(snapshotId) {
423
+ if (!snapshotId || typeof snapshotId !== 'string') {
424
+ return null;
425
+ }
426
+ const parts = snapshotId.split(':');
427
+ if (parts.length !== 2) {
428
+ return null;
429
+ }
430
+ const index = Number(parts[1]);
431
+ if (!Number.isInteger(index) || index < 0) {
432
+ return null;
433
+ }
434
+ return { versionId: parts[0], index };
435
+ }
436
+
437
+ async function readSnapshotsFile(versionRow) {
438
+ try {
439
+ const snapshots = await readJson(versionRow.snapshots_path);
440
+ return Array.isArray(snapshots) ? snapshots : [];
441
+ } catch (error) {
442
+ if (error.code === 'ENOENT') {
443
+ return [];
444
+ }
445
+ throw error;
446
+ }
447
+ }
448
+
449
+ async function listSnapshots(db, dataDir, filters = {}) {
450
+ const clauses = [];
451
+ const args = [];
452
+
453
+ if (filters.designId) {
454
+ clauses.push('design_id = ?');
455
+ args.push(filters.designId);
456
+ }
457
+
458
+ if (filters.versionId) {
459
+ clauses.push('id = ?');
460
+ args.push(filters.versionId);
461
+ }
462
+
463
+ const whereClause = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
464
+ const rows = db
465
+ .prepare(`SELECT * FROM versions ${whereClause} ORDER BY created_at DESC`)
466
+ .all(...args);
467
+
468
+ const items = [];
469
+ for (const row of rows) {
470
+ const snapshots = await readSnapshotsFile(row);
471
+ snapshots.forEach((snapshot, index) => {
472
+ items.push(normalizeSnapshot(snapshot, row, index));
473
+ });
474
+ }
475
+
476
+ return items;
477
+ }
478
+
479
+ async function getSnapshot(db, dataDir, snapshotId) {
480
+ const parsed = parseSnapshotId(snapshotId);
481
+ if (parsed) {
482
+ const row = db.prepare('SELECT * FROM versions WHERE id = ?').get(parsed.versionId);
483
+ if (!row) {
484
+ return null;
485
+ }
486
+ const snapshots = await readSnapshotsFile(row);
487
+ const snapshot = snapshots[parsed.index];
488
+ if (!snapshot) {
489
+ return null;
490
+ }
491
+ return normalizeSnapshot(snapshot, row, parsed.index);
492
+ }
493
+
494
+ const snapshots = await listSnapshots(db, dataDir);
495
+ return snapshots.find((snapshot) => snapshot.id === snapshotId) || null;
496
+ }
497
+
498
+ async function deleteSnapshot(db, dataDir, snapshotId) {
499
+ const parsed = parseSnapshotId(snapshotId);
500
+ if (parsed) {
501
+ const row = db.prepare('SELECT * FROM versions WHERE id = ?').get(parsed.versionId);
502
+ if (!row) {
503
+ return null;
504
+ }
505
+ const snapshots = await readSnapshotsFile(row);
506
+ if (!snapshots[parsed.index]) {
507
+ return null;
508
+ }
509
+ const [removed] = snapshots.splice(parsed.index, 1);
510
+ await writeJson(row.snapshots_path, snapshots);
511
+ return normalizeSnapshot(removed, row, parsed.index);
512
+ }
513
+
514
+ const rows = db.prepare('SELECT * FROM versions ORDER BY created_at DESC').all();
515
+ for (const row of rows) {
516
+ const snapshots = await readSnapshotsFile(row);
517
+ const index = snapshots.findIndex(
518
+ (snapshot) => snapshot && String(snapshot.id || '') === snapshotId
519
+ );
520
+ if (index === -1) {
521
+ continue;
522
+ }
523
+ const [removed] = snapshots.splice(index, 1);
524
+ await writeJson(row.snapshots_path, snapshots);
525
+ return normalizeSnapshot(removed, row, index);
526
+ }
527
+
528
+ return null;
529
+ }
530
+
531
+ async function createComponent(db, dataDir, input) {
532
+ const versionRow = db.prepare('SELECT * FROM versions WHERE id = ?').get(input.versionId);
533
+ if (!versionRow) {
534
+ throw new Error(`version ${input.versionId} not found`);
535
+ }
536
+
537
+ const componentId = input.id || crypto.randomUUID();
538
+ const componentPath = getComponentCodePath(
539
+ dataDir,
540
+ versionRow.design_id,
541
+ versionRow.version_number,
542
+ componentId
543
+ );
544
+
545
+ const payload = {
546
+ html: input.html || '',
547
+ css: input.css || '',
548
+ structure: input.structure || {},
549
+ preview: input.preview || {},
550
+ };
551
+
552
+ await writeJson(componentPath, payload);
553
+
554
+ const now = new Date().toISOString();
555
+ db.prepare(
556
+ `INSERT INTO components (
557
+ id, design_id, version_id, name, type, structure_json, code_path, preview_path, created_at
558
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
559
+ ).run(
560
+ componentId,
561
+ versionRow.design_id,
562
+ versionRow.id,
563
+ input.name || '',
564
+ input.type || '',
565
+ JSON.stringify(payload.structure),
566
+ componentPath,
567
+ input.preview?.imageUrl || '',
568
+ now
569
+ );
570
+
571
+ const designMeta = await getDesign(db, dataDir, versionRow.design_id);
572
+ if (designMeta) {
573
+ designMeta.stats.components = (designMeta.stats.components || 0) + 1;
574
+ await updateDesign(db, dataDir, versionRow.design_id, designMeta);
575
+ }
576
+
577
+ await writeComponentIndex(db, dataDir);
578
+
579
+ return {
580
+ id: componentId,
581
+ designId: versionRow.design_id,
582
+ versionId: versionRow.id,
583
+ name: input.name || '',
584
+ type: input.type || '',
585
+ html: payload.html,
586
+ css: payload.css,
587
+ structure: payload.structure,
588
+ preview: payload.preview,
589
+ createdAt: now,
590
+ };
591
+ }
592
+
593
+ async function listComponents(db, dataDir, filters = {}) {
594
+ const applyFilters = (items) =>
595
+ items.filter((item) => {
596
+ if (filters.designId && item.designId !== filters.designId) {
597
+ return false;
598
+ }
599
+ if (filters.versionId && item.versionId !== filters.versionId) {
600
+ return false;
601
+ }
602
+ if (filters.type && item.type !== filters.type) {
603
+ return false;
604
+ }
605
+ return true;
606
+ });
607
+
608
+ if (shouldUseIndex()) {
609
+ const indexData = await readIndexFile(getComponentsIndexPath(dataDir));
610
+ if (indexData) {
611
+ return applyFilters(indexData);
612
+ }
613
+ }
614
+
615
+ const clauses = [];
616
+ const args = [];
617
+
618
+ if (filters.designId) {
619
+ clauses.push('design_id = ?');
620
+ args.push(filters.designId);
621
+ }
622
+
623
+ if (filters.versionId) {
624
+ clauses.push('version_id = ?');
625
+ args.push(filters.versionId);
626
+ }
627
+
628
+ if (filters.type) {
629
+ clauses.push('type = ?');
630
+ args.push(filters.type);
631
+ }
632
+
633
+ const whereClause = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
634
+ const rows = db.prepare(`SELECT * FROM components ${whereClause} ORDER BY created_at DESC`).all(...args);
635
+ return rows.map((row) => ({
636
+ id: row.id,
637
+ designId: row.design_id,
638
+ versionId: row.version_id,
639
+ name: row.name,
640
+ type: row.type,
641
+ createdAt: row.created_at,
642
+ }));
643
+ }
644
+
645
+ async function getComponent(db, dataDir, componentId) {
646
+ const row = db.prepare('SELECT * FROM components WHERE id = ?').get(componentId);
647
+ if (!row) {
648
+ return null;
649
+ }
650
+
651
+ const payload = await readJson(row.code_path);
652
+ return {
653
+ id: row.id,
654
+ designId: row.design_id,
655
+ versionId: row.version_id,
656
+ name: row.name,
657
+ type: row.type,
658
+ html: payload.html,
659
+ css: payload.css,
660
+ structure: payload.structure,
661
+ preview: payload.preview,
662
+ createdAt: row.created_at,
663
+ };
664
+ }
665
+
666
+ async function updateComponentPreview(db, dataDir, componentId, preview = {}) {
667
+ const row = db.prepare('SELECT * FROM components WHERE id = ?').get(componentId);
668
+ if (!row) {
669
+ return null;
670
+ }
671
+
672
+ const payload = await readJson(row.code_path);
673
+ const updated = {
674
+ ...payload,
675
+ preview,
676
+ };
677
+ await writeJson(row.code_path, updated);
678
+
679
+ const previewPath = preview.imageUrl || preview.url || '';
680
+ db.prepare('UPDATE components SET preview_path = ? WHERE id = ?').run(previewPath, componentId);
681
+
682
+ return {
683
+ id: row.id,
684
+ designId: row.design_id,
685
+ versionId: row.version_id,
686
+ name: row.name,
687
+ type: row.type,
688
+ html: updated.html,
689
+ css: updated.css,
690
+ structure: updated.structure,
691
+ preview: updated.preview,
692
+ createdAt: row.created_at,
693
+ };
694
+ }
695
+
696
+ async function deleteComponent(db, dataDir, componentId) {
697
+ const row = db.prepare('SELECT * FROM components WHERE id = ?').get(componentId);
698
+ if (!row) {
699
+ return;
700
+ }
701
+
702
+ db.prepare('DELETE FROM components WHERE id = ?').run(componentId);
703
+ await removePath(path.dirname(row.code_path));
704
+
705
+ const designMeta = await getDesign(db, dataDir, row.design_id);
706
+ if (designMeta) {
707
+ designMeta.stats.components = Math.max(0, (designMeta.stats.components || 1) - 1);
708
+ await updateDesign(db, dataDir, row.design_id, designMeta);
709
+ }
710
+
711
+ await writeComponentIndex(db, dataDir);
712
+ }
713
+
714
+ async function createRule(db, dataDir, input) {
715
+ const versionRow = db.prepare('SELECT * FROM versions WHERE id = ?').get(input.versionId);
716
+ if (!versionRow) {
717
+ throw new Error(`version ${input.versionId} not found`);
718
+ }
719
+
720
+ const ruleId = input.id || crypto.randomUUID();
721
+ const rulePath = getRulePath(
722
+ dataDir,
723
+ versionRow.design_id,
724
+ versionRow.version_number,
725
+ ruleId
726
+ );
727
+
728
+ await writeJson(rulePath, input.rawData || {});
729
+
730
+ const now = new Date().toISOString();
731
+ db.prepare(
732
+ `INSERT INTO rules (
733
+ id, version_id, type, name, value, raw_path, created_at
734
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)`
735
+ ).run(
736
+ ruleId,
737
+ versionRow.id,
738
+ input.type || '',
739
+ input.name || '',
740
+ input.value || '',
741
+ rulePath,
742
+ now
743
+ );
744
+
745
+ await writeRuleIndex(db, dataDir);
746
+
747
+ return {
748
+ id: ruleId,
749
+ versionId: versionRow.id,
750
+ type: input.type || '',
751
+ name: input.name || '',
752
+ value: input.value || '',
753
+ rawData: input.rawData || {},
754
+ createdAt: now,
755
+ };
756
+ }
757
+
758
+ async function listRules(db, dataDir, versionId) {
759
+ if (shouldUseIndex()) {
760
+ const indexData = await readIndexFile(getRulesIndexPath(dataDir));
761
+ if (indexData) {
762
+ return indexData.filter((item) => item.versionId === versionId);
763
+ }
764
+ }
765
+
766
+ const rows = db
767
+ .prepare('SELECT * FROM rules WHERE version_id = ? ORDER BY created_at DESC')
768
+ .all(versionId);
769
+ return rows.map((row) => ({
770
+ id: row.id,
771
+ versionId: row.version_id,
772
+ type: row.type,
773
+ name: row.name,
774
+ value: row.value,
775
+ createdAt: row.created_at,
776
+ }));
777
+ }
778
+
779
+ async function getRule(db, dataDir, ruleId) {
780
+ const row = db.prepare('SELECT * FROM rules WHERE id = ?').get(ruleId);
781
+ if (!row) {
782
+ return null;
783
+ }
784
+
785
+ const rawData = await readJson(row.raw_path);
786
+ return {
787
+ id: row.id,
788
+ versionId: row.version_id,
789
+ type: row.type,
790
+ name: row.name,
791
+ value: row.value,
792
+ rawData,
793
+ createdAt: row.created_at,
794
+ };
795
+ }
796
+
797
+ async function deleteRule(db, dataDir, ruleId) {
798
+ const row = db.prepare('SELECT * FROM rules WHERE id = ?').get(ruleId);
799
+ if (!row) {
800
+ return;
801
+ }
802
+
803
+ db.prepare('DELETE FROM rules WHERE id = ?').run(ruleId);
804
+ await removePath(row.raw_path);
805
+
806
+ await writeRuleIndex(db, dataDir);
807
+ }
808
+
809
+ // ==================== 任务管理 ====================
810
+
811
+ function extractDomain(url) {
812
+ try {
813
+ return new URL(url).hostname;
814
+ } catch {
815
+ return 'unknown';
816
+ }
817
+ }
818
+
819
+ function normalizeTask(input) {
820
+ const now = new Date().toISOString();
821
+ return {
822
+ id: input.id || crypto.randomUUID(),
823
+ url: input.url || '',
824
+ domain: input.domain || extractDomain(input.url || ''),
825
+ status: input.status || 'pending',
826
+ progress: input.progress ?? 0,
827
+ stage: input.stage || null,
828
+ error: input.error || null,
829
+ options: input.options || {},
830
+ createdAt: input.createdAt || now,
831
+ updatedAt: input.updatedAt || now,
832
+ completedAt: input.completedAt || null,
833
+ };
834
+ }
835
+
836
+ function mapTaskRow(row) {
837
+ return {
838
+ id: row.id,
839
+ url: row.url,
840
+ domain: row.domain,
841
+ status: row.status,
842
+ progress: row.progress,
843
+ stage: row.stage,
844
+ error: row.error ? JSON.parse(row.error) : null,
845
+ options: row.options_json ? JSON.parse(row.options_json) : {},
846
+ createdAt: row.created_at,
847
+ updatedAt: row.updated_at,
848
+ completedAt: row.completed_at,
849
+ };
850
+ }
851
+
852
+ async function createTask(db, input) {
853
+ const task = normalizeTask(input);
854
+
855
+ db.prepare(
856
+ `INSERT INTO tasks (
857
+ id, url, domain, status, progress, stage, error, options_json, created_at, updated_at, completed_at
858
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
859
+ ).run(
860
+ task.id,
861
+ task.url,
862
+ task.domain,
863
+ task.status,
864
+ task.progress,
865
+ task.stage,
866
+ task.error ? JSON.stringify(task.error) : null,
867
+ JSON.stringify(task.options),
868
+ task.createdAt,
869
+ task.updatedAt,
870
+ task.completedAt
871
+ );
872
+
873
+ return task;
874
+ }
875
+
876
+ function listTasks(db, filters = {}) {
877
+ const clauses = [];
878
+ const args = [];
879
+
880
+ if (filters.status) {
881
+ if (Array.isArray(filters.status)) {
882
+ clauses.push(`status IN (${filters.status.map(() => '?').join(',')})`);
883
+ args.push(...filters.status);
884
+ } else {
885
+ clauses.push('status = ?');
886
+ args.push(filters.status);
887
+ }
888
+ }
889
+
890
+ if (filters.domain) {
891
+ clauses.push('domain = ?');
892
+ args.push(filters.domain);
893
+ }
894
+
895
+ if (filters.excludeCompleted) {
896
+ clauses.push('status != ?');
897
+ args.push('completed');
898
+ clauses.push('status != ?');
899
+ args.push('failed');
900
+ }
901
+
902
+ const whereClause = clauses.length ? `WHERE ${clauses.join(' AND ')}` : '';
903
+ const orderBy = filters.orderBy || 'created_at';
904
+ const orderDir = filters.orderDir || 'DESC';
905
+ const limit = filters.limit ? `LIMIT ${parseInt(filters.limit)}` : '';
906
+
907
+ const rows = db
908
+ .prepare(`SELECT * FROM tasks ${whereClause} ORDER BY ${orderBy} ${orderDir} ${limit}`)
909
+ .all(...args);
910
+
911
+ return rows.map(mapTaskRow);
912
+ }
913
+
914
+ function getTask(db, taskId) {
915
+ const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
916
+ return row ? mapTaskRow(row) : null;
917
+ }
918
+
919
+ async function updateTask(db, taskId, patch) {
920
+ const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
921
+ if (!row) {
922
+ return null;
923
+ }
924
+
925
+ const now = new Date().toISOString();
926
+ const updates = [];
927
+ const args = [];
928
+
929
+ if (patch.status !== undefined) {
930
+ updates.push('status = ?');
931
+ args.push(patch.status);
932
+ if (patch.status === 'completed' || patch.status === 'failed') {
933
+ updates.push('completed_at = ?');
934
+ args.push(now);
935
+ }
936
+ }
937
+
938
+ if (patch.progress !== undefined) {
939
+ updates.push('progress = ?');
940
+ args.push(patch.progress);
941
+ }
942
+
943
+ if (patch.stage !== undefined) {
944
+ updates.push('stage = ?');
945
+ args.push(patch.stage);
946
+ }
947
+
948
+ if (patch.error !== undefined) {
949
+ updates.push('error = ?');
950
+ args.push(patch.error ? JSON.stringify(patch.error) : null);
951
+ }
952
+
953
+ updates.push('updated_at = ?');
954
+ args.push(now);
955
+
956
+ if (updates.length === 1) {
957
+ return mapTaskRow(row);
958
+ }
959
+
960
+ args.push(taskId);
961
+ db.prepare(`UPDATE tasks SET ${updates.join(', ')} WHERE id = ?`).run(...args);
962
+
963
+ return getTask(db, taskId);
964
+ }
965
+
966
+ async function deleteTask(db, taskId) {
967
+ const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId);
968
+ if (!row) {
969
+ return false;
970
+ }
971
+
972
+ db.prepare('DELETE FROM tasks WHERE id = ?').run(taskId);
973
+ return true;
974
+ }
975
+
976
+ async function clearCompletedTasks(db) {
977
+ const result = db.prepare("DELETE FROM tasks WHERE status = 'completed' OR status = 'failed'").run();
978
+ return result.changes;
979
+ }
980
+
981
+ module.exports = {
982
+ createStorage,
983
+ };