draftgo-cli 3.0.0 → 3.0.29
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 +67 -17
- package/package.json +13 -8
- package/resources/skill/SKILL.md +118 -22
- package/resources/skill/core/architecture.md +4 -24
- package/resources/skill/core/modules.md +14 -4
- package/resources/skill/init/SKILL.md +3 -4
- package/resources/skill/practices/anti-patterns.md +14 -4
- package/resources/skill/practices/best-practices.md +25 -6
- package/resources/skill/practices/dev-declaration.md +23 -3
- package/resources/skill/pull/SKILL.md +9 -1
- package/resources/skill/push/SKILL.md +103 -68
- package/resources/skill/quickref/api-endpoints.md +63 -41
- package/resources/skill/quickref/api.json +5084 -4975
- package/resources/skill/quickref/app-api.md +4 -14
- package/resources/skill/rules/dev-workflow.md +154 -57
- package/resources/skill/rules/frontend.md +569 -21
- package/resources/skill/rules/parallel.md +10 -10
- package/resources/skill/scripts/__pycache__/draftgo_pull.cpython-312.pyc +0 -0
- package/resources/skill/scripts/__pycache__/draftgo_push.cpython-312.pyc +0 -0
- package/resources/skill/scripts/draftgo_delete.py +0 -2
- package/resources/skill/scripts/draftgo_init.py +15 -3
- package/resources/skill/scripts/draftgo_pull.py +154 -87
- package/resources/skill/scripts/draftgo_push.py +363 -174
- package/resources/skill/specs/custom-services.md +199 -0
- package/resources/skill/specs/data.md +195 -5
- package/resources/skill/specs/db-relations.md +227 -0
- package/resources/skill/specs/runtime.md +30 -0
- package/resources/skill/specs/security.md +3 -3
- package/resources/skill/specs/ui-protocol.md +79 -48
- package/resources/skill/story/SKILL.md +2 -7
- package/src/cli.js +9 -0
- package/src/commands/api.js +59 -0
- package/src/commands/autoPush.js +41 -0
- package/src/commands/check.js +27 -17
- package/src/commands/delete.js +6 -4
- package/src/commands/deploy.js +31 -0
- package/src/commands/doctor.js +1 -1
- package/src/commands/help.js +27 -9
- package/src/commands/init.js +17 -2
- package/src/commands/map.js +18 -7
- package/src/commands/new.js +20 -17
- package/src/commands/sync.js +10 -3
- package/src/commands/update.js +15 -56
- package/src/commands/upgrade.js +52 -0
- package/src/commands/verifyUi.js +199 -0
- package/src/index.js +12 -1
- package/src/localdev/compose.js +8 -1
- package/src/platforms.js +3 -3
- package/src/projectConfig.js +11 -1
- package/src/projectMap.js +274 -39
- package/src/skill.js +113 -29
- package/src/updateCheck.js +37 -5
- package/resources/skill/quickref/dg-components.md +0 -198
package/src/projectMap.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const parse5 = require('parse5');
|
|
5
6
|
|
|
6
7
|
function exists(p) {
|
|
7
8
|
try { fs.accessSync(p); return true; } catch { return false; }
|
|
@@ -60,20 +61,52 @@ function itemFile(projectDir, section, item) {
|
|
|
60
61
|
function extractRoutes(html) {
|
|
61
62
|
const routes = new Set();
|
|
62
63
|
if (!html) return routes;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
let m;
|
|
69
|
-
while ((m = re.exec(html))) {
|
|
70
|
-
const route = normalizeRoute(m[1]);
|
|
71
|
-
if (route) routes.add(route);
|
|
72
|
-
}
|
|
64
|
+
let document;
|
|
65
|
+
try {
|
|
66
|
+
document = parse5.parse(html);
|
|
67
|
+
} catch {
|
|
68
|
+
return routes;
|
|
73
69
|
}
|
|
70
|
+
const visit = (node) => {
|
|
71
|
+
for (const attr of node.attrs || []) {
|
|
72
|
+
if (attr.name === 'href' || attr.name === 'data-page-route') {
|
|
73
|
+
const route = normalizeRoute(attr.value);
|
|
74
|
+
if (route) routes.add(route);
|
|
75
|
+
}
|
|
76
|
+
if (attr.name.startsWith('on')) {
|
|
77
|
+
const re = /\b(?:App\.)?(?:navigate|go|openPage)\s*\(\s*["']([^"']+)["']/g;
|
|
78
|
+
let match;
|
|
79
|
+
while ((match = re.exec(attr.value))) {
|
|
80
|
+
const route = normalizeRoute(match[1]);
|
|
81
|
+
if (route) routes.add(route);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const child of node.childNodes || []) visit(child);
|
|
86
|
+
};
|
|
87
|
+
visit(document);
|
|
74
88
|
return routes;
|
|
75
89
|
}
|
|
76
90
|
|
|
91
|
+
function extractScriptBindings(code) {
|
|
92
|
+
const bindings = { routes: [], events: [], schedules: [] };
|
|
93
|
+
if (!code) return bindings;
|
|
94
|
+
let match;
|
|
95
|
+
const routeRe = /@route\(\s*["']([A-Za-z]+)\s+([^"']+)["']\s*\)/g;
|
|
96
|
+
while ((match = routeRe.exec(code))) bindings.routes.push(`${match[1].toUpperCase()} ${match[2]}`);
|
|
97
|
+
const eventRe = /@on\(\s*["']([^"']+)["']\s*\)/g;
|
|
98
|
+
while ((match = eventRe.exec(code))) bindings.events.push(match[1]);
|
|
99
|
+
const scheduledRe = /@scheduled\(\s*["']([^"']+)["']\s*\)/g;
|
|
100
|
+
while ((match = scheduledRe.exec(code))) bindings.schedules.push(match[1]);
|
|
101
|
+
const goRouteRe = /\.Route\(\s*["']([A-Za-z]+)["']\s*,\s*["']([^"']+)["']/g;
|
|
102
|
+
while ((match = goRouteRe.exec(code))) bindings.routes.push(`${match[1].toUpperCase()} ${match[2]}`);
|
|
103
|
+
const goEventRe = /\.On\(\s*["']([^"']+)["']/g;
|
|
104
|
+
while ((match = goEventRe.exec(code))) bindings.events.push(match[1]);
|
|
105
|
+
const goScheduleRe = /\.Schedule\(\s*["']([^"']+)["']/g;
|
|
106
|
+
while ((match = goScheduleRe.exec(code))) bindings.schedules.push(match[1]);
|
|
107
|
+
return bindings;
|
|
108
|
+
}
|
|
109
|
+
|
|
77
110
|
function pageKey(page) {
|
|
78
111
|
if (page && page.id != null) return `page:${page.id}`;
|
|
79
112
|
return `page:${normalizeRoute(page && page.route) || page && page.title || 'new'}`;
|
|
@@ -86,6 +119,42 @@ function addRouteRefs(refs, route, source) {
|
|
|
86
119
|
refs.get(clean).add(source);
|
|
87
120
|
}
|
|
88
121
|
|
|
122
|
+
function classifyPageArea(item) {
|
|
123
|
+
const route = normalizeRoute(item && item.route);
|
|
124
|
+
const title = String((item && item.title) || '');
|
|
125
|
+
const tag = String((item && item.tag) || '');
|
|
126
|
+
const text = `${route} ${title} ${tag}`;
|
|
127
|
+
|
|
128
|
+
if (!route) return 'unknown';
|
|
129
|
+
if (route === '/' || /(^|[\s/])(home|index|首页|主页)([\s/]|$)/i.test(text)) return 'home';
|
|
130
|
+
if (/^\/(login|setup|install|register)(\/|$)/i.test(route) || /(系统|内置|平台配置|基础配置)/.test(text)) return 'system';
|
|
131
|
+
if (/^\/admin(\/|$)/i.test(route) || /(管理|后台|运营|审核|权限|控制台|配置)/.test(text)) return 'admin';
|
|
132
|
+
if (/(业务|内容|案例|产品|新闻|订单|客户|会员|课程|活动|资料|下载|预约|表单|详情|列表|列表页|工作台|中心|门户|商城|服务)/.test(text)) return 'business';
|
|
133
|
+
return 'unknown';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeHtmlSkeleton(html) {
|
|
137
|
+
if (!html) return '';
|
|
138
|
+
let out = html;
|
|
139
|
+
out = out.replace(/<!--([\s\S]*?)-->/g, '');
|
|
140
|
+
out = out.replace(/<script\b[\s\S]*?<\/script>/gi, '<script></script>');
|
|
141
|
+
out = out.replace(/<style\b[\s\S]*?<\/style>/gi, '<style></style>');
|
|
142
|
+
out = out.replace(/\b(data-page-route|href|id|for|aria-label|aria-labelledby|aria-describedby|title|value|name|placeholder)\s*=\s*("[^"]*"|'[^']*')/gi, '$1=""');
|
|
143
|
+
out = out.replace(/\b(class|style)\s*=\s*("[^"]*"|'[^']*')/gi, '$1=""');
|
|
144
|
+
out = out.replace(/>[^<]*</g, '><');
|
|
145
|
+
out = out.replace(/\s+/g, ' ');
|
|
146
|
+
return out.trim();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function collectPageGroups(pages) {
|
|
150
|
+
const groups = { home: [], admin: [], business: [], system: [], unknown: [] };
|
|
151
|
+
for (const page of pages) {
|
|
152
|
+
const key = groups[page.area] ? page.area : 'unknown';
|
|
153
|
+
groups[key].push(page);
|
|
154
|
+
}
|
|
155
|
+
return groups;
|
|
156
|
+
}
|
|
157
|
+
|
|
89
158
|
function summarizePage(projectDir, item) {
|
|
90
159
|
const file = itemFile(projectDir, 'pages', item);
|
|
91
160
|
return {
|
|
@@ -95,6 +164,7 @@ function summarizePage(projectDir, item) {
|
|
|
95
164
|
permission: item.permission || null,
|
|
96
165
|
tag: item.tag || '',
|
|
97
166
|
html_file: file,
|
|
167
|
+
area: classifyPageArea(item),
|
|
98
168
|
};
|
|
99
169
|
}
|
|
100
170
|
|
|
@@ -198,19 +268,6 @@ function summarizeAIHub(item) {
|
|
|
198
268
|
return base;
|
|
199
269
|
}
|
|
200
270
|
|
|
201
|
-
function summarizeExternalAPI(item) {
|
|
202
|
-
return {
|
|
203
|
-
id: item.id,
|
|
204
|
-
code: item.code || '',
|
|
205
|
-
name: item.name || '',
|
|
206
|
-
method: item.method || 'GET',
|
|
207
|
-
path: item.path || '',
|
|
208
|
-
base_url: item.base_url || '',
|
|
209
|
-
status: item.status,
|
|
210
|
-
tags: Array.isArray(item.tags) ? item.tags : [],
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
271
|
function summarizeDoc(item) {
|
|
215
272
|
return {
|
|
216
273
|
id: item.id,
|
|
@@ -250,7 +307,6 @@ function buildProjectMap(projectDir) {
|
|
|
250
307
|
db_meta: readJsonSafe(projectDir, '.draftgo/db_meta/index.json'),
|
|
251
308
|
custom_scripts: readJsonSafe(projectDir, '.draftgo/custom_scripts/index.json'),
|
|
252
309
|
aihub: readJsonSafe(projectDir, '.draftgo/aihub/index.json'),
|
|
253
|
-
external_apis: readJsonSafe(projectDir, '.draftgo/external_apis/index.json'),
|
|
254
310
|
docs: readJsonSafe(projectDir, '.draftgo/docs/articles/index.json'),
|
|
255
311
|
doc_categories: readJsonSafe(projectDir, '.draftgo/doc_categories/index.json'),
|
|
256
312
|
system_config: readJsonSafe(projectDir, '.draftgo/system_config/index.json'),
|
|
@@ -261,6 +317,7 @@ function buildProjectMap(projectDir) {
|
|
|
261
317
|
const pages = indexes.pages.items.map((p) => summarizePage(projectDir, p));
|
|
262
318
|
const navigations = indexes.navigations.items.map((n) => summarizeNav(projectDir, n));
|
|
263
319
|
const routeRefs = new Map();
|
|
320
|
+
const pageGroups = collectPageGroups(pages);
|
|
264
321
|
|
|
265
322
|
for (const nav of navigations) {
|
|
266
323
|
const html = nav.html_file ? readTextSafe(relToAbs(projectDir, nav.html_file)) : '';
|
|
@@ -283,25 +340,135 @@ function buildProjectMap(projectDir) {
|
|
|
283
340
|
label: m.label || '',
|
|
284
341
|
fields: m.schema && m.schema.properties ? Object.keys(m.schema.properties) : [],
|
|
285
342
|
})),
|
|
286
|
-
custom_scripts: indexes.custom_scripts.items.map((s) =>
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
343
|
+
custom_scripts: indexes.custom_scripts.items.map((s) => {
|
|
344
|
+
const codeFile = itemFile(projectDir, 'custom_scripts', s);
|
|
345
|
+
const bindings = extractScriptBindings(codeFile ? readTextSafe(relToAbs(projectDir, codeFile)) : '');
|
|
346
|
+
return {
|
|
347
|
+
id: s.id,
|
|
348
|
+
name: s.name || '',
|
|
349
|
+
slug: s.slug || '',
|
|
350
|
+
mode: s.mode || '',
|
|
351
|
+
status: s.status,
|
|
352
|
+
code_file: codeFile,
|
|
353
|
+
routes: bindings.routes,
|
|
354
|
+
events: bindings.events,
|
|
355
|
+
schedules: bindings.schedules,
|
|
356
|
+
};
|
|
357
|
+
}),
|
|
294
358
|
aihub: indexes.aihub.items.map(summarizeAIHub),
|
|
295
|
-
external_apis: indexes.external_apis.items.map(summarizeExternalAPI),
|
|
296
359
|
docs: indexes.docs.items.map(summarizeDoc),
|
|
297
360
|
doc_categories: indexes.doc_categories.items.map(summarizeDocCategory),
|
|
298
361
|
system_config: indexes.system_config.items.map(summarizeSystemConfig),
|
|
299
362
|
roles: indexes.roles.items.map((r) => ({ id: r.id, code: r.code || '', name: r.name || '', status: r.status })),
|
|
300
363
|
users_count: indexes.users.items.length,
|
|
364
|
+
pageGroups,
|
|
301
365
|
routeRefs: Object.fromEntries([...routeRefs.entries()].map(([route, sources]) => [route, [...sources]])),
|
|
302
366
|
};
|
|
303
367
|
}
|
|
304
368
|
|
|
369
|
+
function htmlParseErrors(html) {
|
|
370
|
+
const errors = [];
|
|
371
|
+
const ignored = new Set(['missing-doctype']);
|
|
372
|
+
try {
|
|
373
|
+
parse5.parse(html, {
|
|
374
|
+
onParseError(error) {
|
|
375
|
+
if (ignored.has(error.code)) return;
|
|
376
|
+
errors.push({
|
|
377
|
+
code: error.code || 'html-parse-error',
|
|
378
|
+
line: error.startLine || null,
|
|
379
|
+
column: error.startCol || null,
|
|
380
|
+
});
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
} catch (error) {
|
|
384
|
+
errors.push({ code: error.message || 'html-parse-error', line: null, column: null });
|
|
385
|
+
}
|
|
386
|
+
return errors;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function extractStyleText(html) {
|
|
390
|
+
const parts = [];
|
|
391
|
+
if (!html) return '';
|
|
392
|
+
|
|
393
|
+
const styleAttr = /\bstyle\s*=\s*(["'])([\s\S]*?)\1/gi;
|
|
394
|
+
let m;
|
|
395
|
+
while ((m = styleAttr.exec(html))) parts.push(m[2]);
|
|
396
|
+
|
|
397
|
+
const styleBlock = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
|
|
398
|
+
while ((m = styleBlock.exec(html))) parts.push(m[1]);
|
|
399
|
+
|
|
400
|
+
return parts.join('\n');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function maskDgVarFunctions(css) {
|
|
404
|
+
let out = '';
|
|
405
|
+
for (let i = 0; i < css.length; i += 1) {
|
|
406
|
+
const rest = css.slice(i).toLowerCase();
|
|
407
|
+
const fnStart = rest.startsWith('var(') ? 'var(' : rest.startsWith('color-mix(') ? 'color-mix(' : null;
|
|
408
|
+
if (!fnStart) {
|
|
409
|
+
out += css[i];
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
let j = i + fnStart.length;
|
|
414
|
+
let depth = 1;
|
|
415
|
+
while (j < css.length && depth > 0) {
|
|
416
|
+
if (css[j] === '(') depth += 1;
|
|
417
|
+
else if (css[j] === ')') depth -= 1;
|
|
418
|
+
j += 1;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const fn = css.slice(i, j);
|
|
422
|
+
if (/^var\(\s*--dg-/i.test(fn) || /^color-mix\([\s\S]*var\(\s*--dg-/i.test(fn)) {
|
|
423
|
+
out += ' '.repeat(fn.length);
|
|
424
|
+
}
|
|
425
|
+
else out += fn;
|
|
426
|
+
i = j - 1;
|
|
427
|
+
}
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function hasHardcodedColor(css) {
|
|
432
|
+
const masked = maskDgVarFunctions(css);
|
|
433
|
+
return /#[0-9a-f]{3,8}\b/i.test(masked)
|
|
434
|
+
|| /\b(?:rgb|rgba|hsl|hsla|color-mix)\s*\(/i.test(masked);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function hasDarkThemeCoverage(html) {
|
|
438
|
+
return /\[data-theme\s*=\s*["']dark["']\]/i.test(html)
|
|
439
|
+
|| /\[data-theme\s*~=\s*["']dark["']\]/i.test(html)
|
|
440
|
+
|| /\bdata-theme\s*=\s*["']dark["']/i.test(html);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function reportColorThemeRisk(label, html, addWarning) {
|
|
444
|
+
const css = extractStyleText(html);
|
|
445
|
+
if (!css || !hasHardcodedColor(css)) return;
|
|
446
|
+
if (hasDarkThemeCoverage(html)) return;
|
|
447
|
+
addWarning('DG-COLOR-001', 'low', `${label} 发现硬编码配色且未看到深色主题覆盖;优先使用系统 var(--dg-*) token,自主配色需兼容浅色与深色。`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function collectOrphanFiles(projectDir, relDir, indexRel, fileField, prefix, exts) {
|
|
451
|
+
const dir = path.join(projectDir, relDir);
|
|
452
|
+
if (!exists(dir)) return [];
|
|
453
|
+
const index = readJsonSafe(projectDir, indexRel);
|
|
454
|
+
const indexed = new Set(index.items.map((it) => String(it[fileField] || '').replace(/\\/g, '/')));
|
|
455
|
+
return fs.readdirSync(dir)
|
|
456
|
+
.filter((name) => name.startsWith(`${prefix}_`) && exts.includes(path.extname(name)))
|
|
457
|
+
.map((name) => path.join(relDir, name).replace(/\\/g, '/'))
|
|
458
|
+
.filter((rel) => !indexed.has(rel));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function reportPaginationHack(label, html, addWarning) {
|
|
462
|
+
if (/\bApp\.get\s*\(\s*['"]db\/[^'"]+['"]\s*,\s*\{[\s\S]{0,240}\bpage_size\s*:\s*9999\b/i.test(html)) {
|
|
463
|
+
addWarning('DG-DATA-001', 'high', `${label} 使用 page_size: 9999 拉取 DB 数据;DraftGo 规范是不传 page/page_size 即返回全量。`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function needsAdminCompanion(page) {
|
|
468
|
+
const text = `${page.route || ''} ${page.title || ''} ${page.tag || ''}`;
|
|
469
|
+
return /(案例|产品|新闻|订单|客户|会员|课程|活动|资料|下载|预约|表单|内容|招聘|发布|审核|上下架|商城)/.test(text);
|
|
470
|
+
}
|
|
471
|
+
|
|
305
472
|
function isProbablySystemPage(page) {
|
|
306
473
|
const tag = String(page.tag || '');
|
|
307
474
|
const title = String(page.title || '');
|
|
@@ -313,7 +480,13 @@ function analyzeProject(projectDir) {
|
|
|
313
480
|
const map = buildProjectMap(projectDir);
|
|
314
481
|
const errors = [];
|
|
315
482
|
const warnings = [];
|
|
483
|
+
const warningDetails = [];
|
|
484
|
+
const addWarning = (code, confidence, message) => {
|
|
485
|
+
warnings.push(message);
|
|
486
|
+
warningDetails.push({ code, confidence, message });
|
|
487
|
+
};
|
|
316
488
|
const routeSeen = new Map();
|
|
489
|
+
const signatureSeen = new Map();
|
|
317
490
|
|
|
318
491
|
if (!exists(path.join(projectDir, '.draftgo'))) {
|
|
319
492
|
errors.push('未找到 .draftgo/,请先运行 draftgo init 或 draftgo connect。');
|
|
@@ -322,7 +495,7 @@ function analyzeProject(projectDir) {
|
|
|
322
495
|
for (const page of map.pages) {
|
|
323
496
|
const label = `${page.title || '未命名页面'}${page.id != null ? `#${page.id}` : ''}`;
|
|
324
497
|
if (!page.route) errors.push(`${label} 缺少 route。`);
|
|
325
|
-
if (!page.title)
|
|
498
|
+
if (!page.title) addWarning('DG-PAGE-001', 'high', `${label} 缺少 title。`);
|
|
326
499
|
if (!page.html_file) errors.push(`${label} 找不到 html_file 或 page_${page.id}_*.html。`);
|
|
327
500
|
|
|
328
501
|
if (page.route) {
|
|
@@ -337,7 +510,7 @@ function analyzeProject(projectDir) {
|
|
|
337
510
|
const own = pageKey(page);
|
|
338
511
|
const externalRefs = refs.filter((s) => s !== own);
|
|
339
512
|
if (externalRefs.length === 0) {
|
|
340
|
-
|
|
513
|
+
addWarning('DG-ROUTE-001', 'medium', `${label} (${page.route}) 未在导航、首页或其他页面入口中发现绑定引用。`);
|
|
341
514
|
}
|
|
342
515
|
}
|
|
343
516
|
}
|
|
@@ -345,19 +518,80 @@ function analyzeProject(projectDir) {
|
|
|
345
518
|
if (page.html_file) {
|
|
346
519
|
const html = readTextSafe(relToAbs(projectDir, page.html_file));
|
|
347
520
|
if (/\b(mockData|demoData|fakeData|sampleData|staticData)\b/i.test(html)) {
|
|
348
|
-
|
|
521
|
+
addWarning('DG-MOCK-001', 'medium', `${label} 疑似包含 mock/demo/fake/staticData,确认是否为用户明确要求的静态/demo。`);
|
|
349
522
|
}
|
|
350
523
|
if (/\b(onclick|addEventListener)\b[\s\S]{0,120}\b(toast|alert)\b/i.test(html) && !/\b(App\.(post|put|delete|get)|fetch\s*\()/i.test(html)) {
|
|
351
|
-
|
|
524
|
+
addWarning('DG-CLOSURE-001', 'medium', `${label} 疑似只有反馈提示、缺少真实数据读写或 API 调用。`);
|
|
525
|
+
}
|
|
526
|
+
reportPaginationHack(label, html, addWarning);
|
|
527
|
+
reportColorThemeRisk(label, html, addWarning);
|
|
528
|
+
const signature = normalizeHtmlSkeleton(html);
|
|
529
|
+
if (signature) {
|
|
530
|
+
if (!signatureSeen.has(signature)) signatureSeen.set(signature, []);
|
|
531
|
+
signatureSeen.get(signature).push({ label, area: page.area });
|
|
532
|
+
}
|
|
533
|
+
const parseErrors = htmlParseErrors(html);
|
|
534
|
+
for (const parseError of parseErrors.slice(0, 5)) {
|
|
535
|
+
const at = parseError.line ? `(行 ${parseError.line}${parseError.column ? `:${parseError.column}` : ''})` : '';
|
|
536
|
+
addWarning('DG-HTML-001', 'high', `${label} HTML 解析提醒:${parseError.code}${at}。`);
|
|
352
537
|
}
|
|
353
538
|
}
|
|
354
539
|
}
|
|
355
540
|
|
|
541
|
+
const adminCount = map.pageGroups.admin.length;
|
|
542
|
+
const maintainableBusinessPages = map.pageGroups.business.filter(needsAdminCompanion);
|
|
543
|
+
if (maintainableBusinessPages.length > 0 && adminCount === 0) {
|
|
544
|
+
addWarning(
|
|
545
|
+
'DG-ADMIN-001',
|
|
546
|
+
'medium',
|
|
547
|
+
`检测到 ${maintainableBusinessPages.length} 个具有可维护内容信号的业务页面,但未发现管理端页面;若内容需要运营维护,建议补管理端页面或明确说明不需要。`,
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
for (const labels of signatureSeen.values()) {
|
|
552
|
+
const nonSystem = labels.filter((item) => item.area !== 'system');
|
|
553
|
+
if (nonSystem.length < 3) continue;
|
|
554
|
+
addWarning(
|
|
555
|
+
'DG-DUPLICATE-001',
|
|
556
|
+
'low',
|
|
557
|
+
`发现 ${nonSystem.length} 个结构几乎一致的页面,疑似工作台副本:${nonSystem.map((item) => item.label).join('、')}。建议复刻内置导航/侧栏后按业务差异拆分,不要整页复制。`,
|
|
558
|
+
);
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
for (const nav of map.navigations) {
|
|
563
|
+
if (!nav.html_file) continue;
|
|
564
|
+
const navLabel = `导航${nav.name || nav.code || nav.id || ''}`;
|
|
565
|
+
const html = readTextSafe(relToAbs(projectDir, nav.html_file));
|
|
566
|
+
reportColorThemeRisk(navLabel, html, addWarning);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const orphanGroups = [
|
|
570
|
+
['页面', '.draftgo/pages', '.draftgo/pages/index.json', 'html_file', 'page', ['.html']],
|
|
571
|
+
['导航', '.draftgo/navigations', '.draftgo/navigations/index.json', 'html_file', 'nav', ['.html']],
|
|
572
|
+
['文档', '.draftgo/docs/articles', '.draftgo/docs/articles/index.json', 'content_file', 'article', ['.html', '.md']],
|
|
573
|
+
['自定义脚本', '.draftgo/custom_scripts', '.draftgo/custom_scripts/index.json', 'code_file', 'script', ['.py', '.js', '.ts', '.sh', '.go', '.txt']],
|
|
574
|
+
];
|
|
575
|
+
for (const [label, relDir, indexRel, fileField, prefix, exts] of orphanGroups) {
|
|
576
|
+
const orphans = collectOrphanFiles(projectDir, relDir, indexRel, fileField, prefix, exts);
|
|
577
|
+
if (orphans.length) {
|
|
578
|
+
addWarning('DG-ORPHAN-001', 'high', `${label}目录存在 ${orphans.length} 个未登记到 index.json 的文件:${orphans.slice(0, 5).join('、')}。push 不会上传这些文件。`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
for (const script of map.custom_scripts) {
|
|
583
|
+
if (String(script.mode || '').toLowerCase() !== 'route' || !script.code_file) continue;
|
|
584
|
+
const code = readTextSafe(relToAbs(projectDir, script.code_file));
|
|
585
|
+
if (/\bdef\s+handle\s*\(/.test(code) && !/@route\s*\(/.test(code)) {
|
|
586
|
+
errors.push(`自定义脚本${script.name || script.slug || script.id}#${script.id} 是 route 模式但只有 handle(),缺少 @route("METHOD /path") 注册,HTTP 端点不会命中。`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
356
590
|
if (map.pages.length === 0 && exists(path.join(projectDir, '.draftgo'))) {
|
|
357
|
-
|
|
591
|
+
addWarning('DG-CACHE-001', 'low', '未发现 pages/index.json 页面缓存;开发前建议先拉取页面。');
|
|
358
592
|
}
|
|
359
593
|
|
|
360
|
-
return { map, errors, warnings };
|
|
594
|
+
return { map, errors, warnings, warningDetails };
|
|
361
595
|
}
|
|
362
596
|
|
|
363
597
|
module.exports = {
|
|
@@ -365,4 +599,5 @@ module.exports = {
|
|
|
365
599
|
analyzeProject,
|
|
366
600
|
normalizeRoute,
|
|
367
601
|
extractRoutes,
|
|
602
|
+
extractScriptBindings,
|
|
368
603
|
};
|
package/src/skill.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
// Per-platform skill renderer.
|
|
4
4
|
//
|
|
5
|
-
// For every AI-tool target (see src/platforms.js) we render
|
|
6
|
-
//
|
|
7
|
-
// .draftgo/skill
|
|
8
|
-
// natively, which dramatically improves command compliance.
|
|
5
|
+
// For every AI-tool target (see src/platforms.js) we render the complete
|
|
6
|
+
// instruction body into that tool's own directory. The large OpenAPI snapshot
|
|
7
|
+
// is shared once under .draftgo/skill-shared to avoid redundant copies.
|
|
9
8
|
//
|
|
10
9
|
// Templating in .md files:
|
|
11
10
|
// {{SKILL_DIR}} → platform's project-relative skill dir (forward slashes)
|
|
12
11
|
// {{SKILL_SCRIPTS}} → {{SKILL_DIR}}/scripts
|
|
12
|
+
// {{SKILL_SHARED}} → .draftgo/skill-shared
|
|
13
13
|
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const fs = require('fs');
|
|
@@ -21,6 +21,8 @@ const {
|
|
|
21
21
|
} = require('./fsx');
|
|
22
22
|
|
|
23
23
|
const SKILL_SOURCE_DIR = path.join(RESOURCES_DIR, 'skill');
|
|
24
|
+
const SHARED_RESOURCE_DIR = path.join('.draftgo', 'skill-shared');
|
|
25
|
+
const SHARED_FILES = new Set(['quickref/api.json']);
|
|
24
26
|
|
|
25
27
|
function getPackageVersion() {
|
|
26
28
|
try {
|
|
@@ -54,12 +56,32 @@ function renderFrontmatter(fm) {
|
|
|
54
56
|
return lines.join('\n');
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
function splitFrontmatter(text) {
|
|
60
|
+
const normalized = String(text || '').replace(/\r\n/g, '\n');
|
|
61
|
+
if (!normalized.startsWith('---\n')) return { frontmatter: {}, body: normalized };
|
|
62
|
+
const end = normalized.indexOf('\n---\n', 4);
|
|
63
|
+
if (end < 0) return { frontmatter: {}, body: normalized };
|
|
64
|
+
const frontmatter = {};
|
|
65
|
+
for (const line of normalized.slice(4, end).split('\n')) {
|
|
66
|
+
const colon = line.indexOf(':');
|
|
67
|
+
if (colon <= 0) continue;
|
|
68
|
+
const key = line.slice(0, colon).trim();
|
|
69
|
+
let value = line.slice(colon + 1).trim();
|
|
70
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
71
|
+
value = value.slice(1, -1).replace(/\\"/g, '"');
|
|
72
|
+
}
|
|
73
|
+
frontmatter[key] = value;
|
|
74
|
+
}
|
|
75
|
+
return { frontmatter, body: normalized.slice(end + 5) };
|
|
76
|
+
}
|
|
77
|
+
|
|
57
78
|
function substitute(text, platform) {
|
|
58
79
|
const skillDir = platform.skillDir;
|
|
59
80
|
const scriptsDir = `${skillDir}/scripts`;
|
|
60
81
|
return text
|
|
61
82
|
.replace(/\{\{SKILL_DIR\}\}/g, skillDir)
|
|
62
|
-
.replace(/\{\{SKILL_SCRIPTS\}\}/g, scriptsDir)
|
|
83
|
+
.replace(/\{\{SKILL_SCRIPTS\}\}/g, scriptsDir)
|
|
84
|
+
.replace(/\{\{SKILL_SHARED\}\}/g, SHARED_RESOURCE_DIR.replace(/\\/g, '/'));
|
|
63
85
|
}
|
|
64
86
|
|
|
65
87
|
function walk(root, onFile) {
|
|
@@ -76,27 +98,51 @@ function renderInto(destDir, platform) {
|
|
|
76
98
|
const srcRoot = SKILL_SOURCE_DIR;
|
|
77
99
|
walk(srcRoot, (absSrc) => {
|
|
78
100
|
const rel = path.relative(srcRoot, absSrc);
|
|
101
|
+
if (SHARED_FILES.has(rel.replace(/\\/g, '/'))) return;
|
|
79
102
|
const absDst = path.join(destDir, rel);
|
|
80
103
|
ensureDir(path.dirname(absDst));
|
|
81
104
|
if (absSrc.endsWith('.md')) {
|
|
82
105
|
let body = readText(absSrc);
|
|
83
|
-
body = substitute(body, platform);
|
|
84
106
|
if (rel === 'SKILL.md') {
|
|
85
|
-
|
|
107
|
+
const parsed = splitFrontmatter(body);
|
|
108
|
+
body = renderFrontmatter({ ...(platform.frontmatter || {}), ...parsed.frontmatter }) + parsed.body;
|
|
86
109
|
}
|
|
110
|
+
body = substitute(body, platform);
|
|
87
111
|
writeText(absDst, body);
|
|
88
112
|
} else {
|
|
89
113
|
copyFile(absSrc, absDst);
|
|
90
114
|
}
|
|
91
115
|
});
|
|
92
116
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function ensureSharedResources(projectDir) {
|
|
120
|
+
const sharedRoot = path.join(projectDir, SHARED_RESOURCE_DIR);
|
|
121
|
+
for (const rel of SHARED_FILES) {
|
|
122
|
+
const src = path.join(SKILL_SOURCE_DIR, rel);
|
|
123
|
+
const dest = path.join(sharedRoot, rel);
|
|
124
|
+
if (exists(dest)) {
|
|
125
|
+
const sourceStat = fs.statSync(src);
|
|
126
|
+
const destStat = fs.statSync(dest);
|
|
127
|
+
if (sourceStat.size === destStat.size && destStat.mtimeMs >= sourceStat.mtimeMs) continue;
|
|
128
|
+
}
|
|
129
|
+
copyFile(src, dest);
|
|
130
|
+
}
|
|
131
|
+
return sharedRoot;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function validateRenderedSkill(assetDir) {
|
|
135
|
+
const main = path.join(assetDir, 'SKILL.md');
|
|
136
|
+
if (!exists(main)) throw new Error('渲染结果缺少 SKILL.md');
|
|
137
|
+
const text = readText(main);
|
|
138
|
+
const firstEnd = text.indexOf('\n---\n', 4);
|
|
139
|
+
const body = firstEnd >= 0 ? text.slice(firstEnd + 5) : '';
|
|
140
|
+
if (!text.startsWith('---\n') || firstEnd < 0 || body.startsWith('---\n')) {
|
|
141
|
+
throw new Error('SKILL.md frontmatter 必须且只能有一个');
|
|
142
|
+
}
|
|
143
|
+
if (/\{\{(?:SKILL_DIR|SKILL_SCRIPTS|SKILL_SHARED)\}\}/.test(text)) {
|
|
144
|
+
throw new Error('SKILL.md 仍包含未替换占位符');
|
|
145
|
+
}
|
|
100
146
|
}
|
|
101
147
|
|
|
102
148
|
function ensureRuntime(projectDir) {
|
|
@@ -111,9 +157,10 @@ function ensureRuntime(projectDir) {
|
|
|
111
157
|
}
|
|
112
158
|
appendGitignoreLine(projectDir, '.draftgo/config.json');
|
|
113
159
|
appendGitignoreLine(projectDir, '.draftgo/token');
|
|
160
|
+
ensureSharedResources(projectDir);
|
|
114
161
|
}
|
|
115
162
|
|
|
116
|
-
function installPlatform(projectDir, platform
|
|
163
|
+
function installPlatform(projectDir, platform, opts = {}) {
|
|
117
164
|
if (!exists(SKILL_SOURCE_DIR)) {
|
|
118
165
|
throw new Error(
|
|
119
166
|
`Skill source missing: ${SKILL_SOURCE_DIR}. The CLI package may be broken.`
|
|
@@ -121,24 +168,59 @@ function installPlatform(projectDir, platform /*, { force } */) {
|
|
|
121
168
|
}
|
|
122
169
|
const destAsset = path.join(projectDir, platform.assetDir);
|
|
123
170
|
const destMain = path.join(projectDir, platform.mainFile);
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
// If mainFile lives outside assetDir, clean it too.
|
|
128
|
-
if (path.relative(destAsset, destMain).startsWith('..')) {
|
|
129
|
-
if (exists(destMain)) removePath(destMain);
|
|
171
|
+
const mainOutsideAsset = path.relative(destAsset, destMain).startsWith('..');
|
|
172
|
+
if ((exists(destAsset) || exists(destMain)) && !opts.force) {
|
|
173
|
+
return { path: platform.mainFile, skipped: true };
|
|
130
174
|
}
|
|
131
175
|
|
|
132
|
-
|
|
176
|
+
ensureSharedResources(projectDir);
|
|
177
|
+
const token = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
178
|
+
const stageAsset = `${destAsset}.tmp-${token}`;
|
|
179
|
+
const backupAsset = `${destAsset}.bak-${token}`;
|
|
180
|
+
const stageMain = mainOutsideAsset ? `${destMain}.tmp-${token}` : null;
|
|
181
|
+
const backupMain = mainOutsideAsset ? `${destMain}.bak-${token}` : null;
|
|
182
|
+
let assetBackedUp = false;
|
|
183
|
+
let mainBackedUp = false;
|
|
184
|
+
let assetInstalled = false;
|
|
185
|
+
let mainInstalled = false;
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
renderInto(stageAsset, platform);
|
|
189
|
+
validateRenderedSkill(stageAsset);
|
|
190
|
+
if (mainOutsideAsset) copyFile(path.join(stageAsset, 'SKILL.md'), stageMain);
|
|
133
191
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
192
|
+
ensureDir(path.dirname(destAsset));
|
|
193
|
+
if (exists(destAsset)) {
|
|
194
|
+
fs.renameSync(destAsset, backupAsset);
|
|
195
|
+
assetBackedUp = true;
|
|
196
|
+
}
|
|
197
|
+
if (mainOutsideAsset && exists(destMain)) {
|
|
198
|
+
ensureDir(path.dirname(destMain));
|
|
199
|
+
fs.renameSync(destMain, backupMain);
|
|
200
|
+
mainBackedUp = true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
fs.renameSync(stageAsset, destAsset);
|
|
204
|
+
assetInstalled = true;
|
|
205
|
+
if (mainOutsideAsset) {
|
|
206
|
+
fs.renameSync(stageMain, destMain);
|
|
207
|
+
mainInstalled = true;
|
|
208
|
+
}
|
|
209
|
+
} catch (err) {
|
|
210
|
+
if (mainInstalled && exists(destMain)) removePath(destMain);
|
|
211
|
+
if (assetInstalled && exists(destAsset)) removePath(destAsset);
|
|
212
|
+
if (mainBackedUp && exists(backupMain)) fs.renameSync(backupMain, destMain);
|
|
213
|
+
if (assetBackedUp && exists(backupAsset)) fs.renameSync(backupAsset, destAsset);
|
|
214
|
+
throw err;
|
|
215
|
+
} finally {
|
|
216
|
+
if (exists(stageAsset)) removePath(stageAsset);
|
|
217
|
+
if (stageMain && exists(stageMain)) removePath(stageMain);
|
|
140
218
|
}
|
|
141
219
|
|
|
220
|
+
if (exists(backupAsset)) removePath(backupAsset);
|
|
221
|
+
if (backupMain && exists(backupMain)) removePath(backupMain);
|
|
222
|
+
if (!exists(destMain)) throw new Error(`安装后缺少入口文件:${platform.mainFile}`);
|
|
223
|
+
|
|
142
224
|
return { path: platform.mainFile };
|
|
143
225
|
}
|
|
144
226
|
|
|
@@ -163,7 +245,7 @@ function installAll(projectDir, platforms, opts = {}) {
|
|
|
163
245
|
for (const p of platforms) {
|
|
164
246
|
results.push({ platform: p, result: installPlatform(projectDir, p, opts) });
|
|
165
247
|
}
|
|
166
|
-
writeInstalledVersion(projectDir);
|
|
248
|
+
if (results.every((item) => !item.result.skipped)) writeInstalledVersion(projectDir);
|
|
167
249
|
return results;
|
|
168
250
|
}
|
|
169
251
|
|
|
@@ -177,4 +259,6 @@ module.exports = {
|
|
|
177
259
|
statusPlatform,
|
|
178
260
|
installAll,
|
|
179
261
|
ensureRuntime,
|
|
262
|
+
ensureSharedResources,
|
|
263
|
+
splitFrontmatter,
|
|
180
264
|
};
|