draftgo-cli 3.0.35 → 3.0.38
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 +220 -272
- package/package.json +6 -2
- package/resources/skill/SKILL.md +114 -55
- package/resources/skill/init/SKILL.md +29 -15
- package/resources/skill/manifest.json +5 -4
- package/resources/skill/push/SKILL.md +41 -29
- package/resources/skill/references/aihub.md +8 -5
- package/resources/skill/references/api-endpoints.md +5 -3
- package/resources/skill/references/architecture.md +1 -1
- package/resources/skill/references/checkout.md +116 -0
- package/resources/skill/references/custom-services.md +9 -10
- package/resources/skill/references/data.md +4 -2
- package/resources/skill/references/frontend.md +1 -1
- package/resources/skill/references/mcp.md +101 -0
- package/resources/skill/references/modules.md +8 -8
- package/resources/skill/references/parallel.md +6 -3
- package/resources/skill/references/runtime.md +7 -10
- package/resources/skill/scripts/README.md +8 -0
- package/resources/skill/story/SKILL.md +8 -8
- package/src/cli.js +5 -0
- package/src/commandRegistry.js +7 -1
- package/src/commands/api.js +24 -187
- package/src/commands/autoPush.js +48 -17
- package/src/commands/check.js +17 -47
- package/src/commands/checkout.js +18 -0
- package/src/commands/commit.js +21 -0
- package/src/commands/conflict.js +30 -0
- package/src/commands/conflicts.js +16 -0
- package/src/commands/connect.js +60 -48
- package/src/commands/delete.js +79 -64
- package/src/commands/deploy.js +18 -10
- package/src/commands/diff.js +23 -0
- package/src/commands/help.js +99 -75
- package/src/commands/init.js +4 -10
- package/src/commands/local.js +23 -6
- package/src/commands/map.js +89 -89
- package/src/commands/mcp.js +126 -0
- package/src/commands/sync.js +28 -43
- package/src/commands/verifyUi.js +3 -2
- package/src/localdev/index.js +37 -7
- package/src/localdev/mysqlClient.js +1 -1
- package/src/mcp/client.js +275 -0
- package/src/mcp/hosts.js +520 -0
- package/src/mcp/protocol.js +173 -0
- package/src/mcp/stdio.js +300 -0
- package/src/mcp/tools.js +37 -0
- package/src/platforms.js +3 -4
- package/src/projectConfig.js +91 -49
- package/src/projectMap.js +123 -460
- package/src/skill.js +6 -28
- package/src/worktree/backend.js +250 -0
- package/src/worktree/errors.js +28 -0
- package/src/worktree/index.js +461 -0
- package/src/worktree/manifest.js +75 -0
- package/src/worktree/streams.js +200 -0
- package/src/worktree/types.js +103 -0
- package/src/worktree/validate.js +37 -0
- package/resources/skill/pull/SKILL.md +0 -33
- package/resources/skill/references/api.json +0 -20248
- package/resources/skill/scripts/draftgo_delete.py +0 -149
- package/resources/skill/scripts/draftgo_init.py +0 -80
- package/resources/skill/scripts/draftgo_pull.py +0 -427
- package/resources/skill/scripts/draftgo_push.py +0 -1022
- package/src/python.js +0 -27
package/src/projectMap.js
CHANGED
|
@@ -1,86 +1,30 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const crypto = require('crypto');
|
|
3
4
|
const fs = require('fs');
|
|
4
5
|
const path = require('path');
|
|
5
6
|
const parse5 = require('parse5');
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
try { fs.accessSync(p); return true; } catch { return false; }
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function readTextSafe(p) {
|
|
12
|
-
try { return fs.readFileSync(p, 'utf8'); } catch { return ''; }
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function readJsonSafe(projectDir, rel) {
|
|
16
|
-
const abs = path.join(projectDir, rel);
|
|
17
|
-
try {
|
|
18
|
-
const raw = fs.readFileSync(abs, 'utf8');
|
|
19
|
-
const parsed = JSON.parse(raw);
|
|
20
|
-
return { ok: true, path: rel, items: Array.isArray(parsed) ? parsed : [], raw: parsed };
|
|
21
|
-
} catch (err) {
|
|
22
|
-
return { ok: false, path: rel, items: [], error: err.message };
|
|
23
|
-
}
|
|
24
|
-
}
|
|
7
|
+
const { loadManifest, absolutePath } = require('./worktree/manifest');
|
|
8
|
+
const { mediaType } = require('./worktree/types');
|
|
25
9
|
|
|
26
10
|
function normalizeRoute(route) {
|
|
27
11
|
if (!route || typeof route !== 'string') return '';
|
|
28
12
|
const clean = route.trim();
|
|
29
|
-
if (!clean) return '';
|
|
30
|
-
if (/^https?:\/\//i.test(clean) || clean.startsWith('#') || clean.startsWith('mailto:')) return '';
|
|
13
|
+
if (!clean || /^https?:\/\//i.test(clean) || clean.startsWith('#') || clean.startsWith('mailto:')) return '';
|
|
31
14
|
return clean.startsWith('/') ? clean : `/${clean}`;
|
|
32
15
|
}
|
|
33
16
|
|
|
34
|
-
function relToAbs(projectDir, rel) {
|
|
35
|
-
if (!rel || typeof rel !== 'string') return null;
|
|
36
|
-
const clean = rel.replace(/\\/g, '/');
|
|
37
|
-
return path.isAbsolute(clean) ? clean : path.join(projectDir, clean);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function findGeneratedFile(projectDir, dirRel, prefix, exts) {
|
|
41
|
-
const dir = path.join(projectDir, dirRel);
|
|
42
|
-
if (!exists(dir)) return null;
|
|
43
|
-
const files = fs.readdirSync(dir).filter((f) => {
|
|
44
|
-
if (!f.startsWith(prefix)) return false;
|
|
45
|
-
return exts.some((ext) => f.endsWith(ext));
|
|
46
|
-
});
|
|
47
|
-
return files.length ? path.join(dirRel, files[0]).replace(/\\/g, '/') : null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function itemFile(projectDir, section, item) {
|
|
51
|
-
if (!item || typeof item !== 'object') return null;
|
|
52
|
-
const direct = item.html_file || item.code_file || item.file;
|
|
53
|
-
if (direct && exists(relToAbs(projectDir, direct))) return direct.replace(/\\/g, '/');
|
|
54
|
-
const id = item.id == null ? '' : String(item.id);
|
|
55
|
-
if (!id) return null;
|
|
56
|
-
if (section === 'pages') return findGeneratedFile(projectDir, '.draftgo/pages', `page_${id}_`, ['.html']);
|
|
57
|
-
if (section === 'navigations') return findGeneratedFile(projectDir, '.draftgo/navigations', `nav_${id}_`, ['.html']);
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
17
|
function extractRoutes(html) {
|
|
62
18
|
const routes = new Set();
|
|
63
19
|
if (!html) return routes;
|
|
64
20
|
let document;
|
|
65
|
-
try {
|
|
66
|
-
document = parse5.parse(html);
|
|
67
|
-
} catch {
|
|
68
|
-
return routes;
|
|
69
|
-
}
|
|
21
|
+
try { document = parse5.parse(html); } catch { return routes; }
|
|
70
22
|
const visit = (node) => {
|
|
71
|
-
for (const
|
|
72
|
-
if (
|
|
73
|
-
const route = normalizeRoute(
|
|
23
|
+
for (const attribute of node.attrs || []) {
|
|
24
|
+
if (attribute.name === 'href' || attribute.name === 'data-page-route') {
|
|
25
|
+
const route = normalizeRoute(attribute.value);
|
|
74
26
|
if (route) routes.add(route);
|
|
75
27
|
}
|
|
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
28
|
}
|
|
85
29
|
for (const child of node.childNodes || []) visit(child);
|
|
86
30
|
};
|
|
@@ -88,392 +32,93 @@ function extractRoutes(html) {
|
|
|
88
32
|
return routes;
|
|
89
33
|
}
|
|
90
34
|
|
|
91
|
-
function extractScriptBindings(
|
|
92
|
-
|
|
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;
|
|
35
|
+
function extractScriptBindings() {
|
|
36
|
+
return { routes: [], events: [], schedules: [] };
|
|
108
37
|
}
|
|
109
38
|
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
|
|
39
|
+
function htmlParseErrors(html) {
|
|
40
|
+
const errors = [];
|
|
41
|
+
try {
|
|
42
|
+
parse5.parse(html, { onParseError(error) {
|
|
43
|
+
if (error.code !== 'missing-doctype') errors.push({
|
|
44
|
+
code: error.code || 'html-parse-error', line: error.startLine || null, column: error.startCol || null,
|
|
45
|
+
});
|
|
46
|
+
} });
|
|
47
|
+
} catch (error) {
|
|
48
|
+
errors.push({ code: error.message || 'html-parse-error', line: null, column: null });
|
|
49
|
+
}
|
|
50
|
+
return errors;
|
|
113
51
|
}
|
|
114
52
|
|
|
115
|
-
function
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
refs.get(clean).add(source);
|
|
53
|
+
function sha256File(file) {
|
|
54
|
+
const hash = crypto.createHash('sha256');
|
|
55
|
+
hash.update(fs.readFileSync(file));
|
|
56
|
+
return hash.digest('hex');
|
|
120
57
|
}
|
|
121
58
|
|
|
122
|
-
function
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
|
|
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';
|
|
59
|
+
function decodeTextFile(file, contentType) {
|
|
60
|
+
const match = String(contentType || '').match(/;\s*charset\s*=\s*[']?([^;'\s]+)/i);
|
|
61
|
+
const charset = match ? match[1] : 'utf-8';
|
|
62
|
+
return new TextDecoder(charset, { fatal: true }).decode(fs.readFileSync(file));
|
|
134
63
|
}
|
|
135
64
|
|
|
136
|
-
function
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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();
|
|
65
|
+
function classifyPageArea(entry) {
|
|
66
|
+
const route = normalizeRoute(entry.route);
|
|
67
|
+
if (route === '/') return 'home';
|
|
68
|
+
if (/^\/(login|setup|install|register)(\/|$)/i.test(route)) return 'system';
|
|
69
|
+
if (/^\/admin(\/|$)/i.test(route)) return 'admin';
|
|
70
|
+
return route ? 'business' : 'unknown';
|
|
147
71
|
}
|
|
148
72
|
|
|
149
|
-
function
|
|
73
|
+
function collectGroups(pages) {
|
|
150
74
|
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
|
-
}
|
|
75
|
+
for (const page of pages) groups[page.area || 'unknown'].push(page);
|
|
155
76
|
return groups;
|
|
156
77
|
}
|
|
157
78
|
|
|
158
|
-
function summarizePage(projectDir, item) {
|
|
159
|
-
const file = itemFile(projectDir, 'pages', item);
|
|
160
|
-
return {
|
|
161
|
-
id: item.id,
|
|
162
|
-
title: item.title || '',
|
|
163
|
-
route: normalizeRoute(item.route),
|
|
164
|
-
permission: item.permission || null,
|
|
165
|
-
tag: item.tag || '',
|
|
166
|
-
html_file: file,
|
|
167
|
-
area: classifyPageArea(item),
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function summarizeNav(projectDir, item) {
|
|
172
|
-
const file = itemFile(projectDir, 'navigations', item);
|
|
173
|
-
return {
|
|
174
|
-
id: item.id,
|
|
175
|
-
code: item.code || '',
|
|
176
|
-
name: item.name || '',
|
|
177
|
-
status: item.status,
|
|
178
|
-
html_file: file,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function summarizeAIHub(item) {
|
|
183
|
-
const data = item && typeof item.data === 'object' && item.data ? item.data : {};
|
|
184
|
-
const spec = data.spec && typeof data.spec === 'object' ? data.spec : {};
|
|
185
|
-
const base = {
|
|
186
|
-
id: item.id,
|
|
187
|
-
name: item.name || '',
|
|
188
|
-
type: item.type || '',
|
|
189
|
-
status: item.status,
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
if (item.type === 'agent') {
|
|
193
|
-
const tools = spec.tools && typeof spec.tools === 'object' ? spec.tools : {};
|
|
194
|
-
const outputFormat = spec.output_format && typeof spec.output_format === 'object' ? spec.output_format : {};
|
|
195
|
-
const modelSelection = spec.model_selection && typeof spec.model_selection === 'object' ? spec.model_selection : {};
|
|
196
|
-
return {
|
|
197
|
-
...base,
|
|
198
|
-
mode: spec.mode || 'chat',
|
|
199
|
-
model_id: spec.model_id || null,
|
|
200
|
-
model: spec.model || data.model || '',
|
|
201
|
-
fallback_models: Array.isArray(spec.fallback_models) ? spec.fallback_models : [],
|
|
202
|
-
output_format: {
|
|
203
|
-
mode: outputFormat.mode || 'native',
|
|
204
|
-
json_strategy: outputFormat.json && outputFormat.json.strategy || null,
|
|
205
|
-
has_schema: Boolean(outputFormat.json && outputFormat.json.schema),
|
|
206
|
-
},
|
|
207
|
-
model_selection: {
|
|
208
|
-
user_selectable: Boolean(modelSelection.user_selectable),
|
|
209
|
-
on_invalid: modelSelection.on_invalid || 'ignore',
|
|
210
|
-
},
|
|
211
|
-
tools: {
|
|
212
|
-
enabled: Boolean(tools.enabled && Array.isArray(tools.sources) && tools.sources.length),
|
|
213
|
-
sources: Array.isArray(tools.sources)
|
|
214
|
-
? tools.sources.map((s) => ({
|
|
215
|
-
type: s.type || '',
|
|
216
|
-
id: s.id,
|
|
217
|
-
name: s.name || '',
|
|
218
|
-
tool_filter: Array.isArray(s.tool_filter) ? s.tool_filter : [],
|
|
219
|
-
}))
|
|
220
|
-
: [],
|
|
221
|
-
},
|
|
222
|
-
skills_count: Array.isArray(spec.skills) ? spec.skills.length : 0,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (item.type === 'model') {
|
|
227
|
-
return {
|
|
228
|
-
...base,
|
|
229
|
-
provider: data.provider || 'openai-compatible',
|
|
230
|
-
models_count: Array.isArray(data.models) ? data.models.length : 0,
|
|
231
|
-
supports_response_format: data.supports_response_format,
|
|
232
|
-
supports_json_schema: data.supports_json_schema,
|
|
233
|
-
image_generation_probe: data.image_generation_probe
|
|
234
|
-
? {
|
|
235
|
-
ok: Boolean(data.image_generation_probe.ok),
|
|
236
|
-
model: data.image_generation_probe.model || '',
|
|
237
|
-
tested_at: data.image_generation_probe.tested_at || '',
|
|
238
|
-
}
|
|
239
|
-
: null,
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (item.type === 'mcp') {
|
|
244
|
-
return {
|
|
245
|
-
...base,
|
|
246
|
-
transport: data.transport || '',
|
|
247
|
-
tools_count: Array.isArray(data.tools) ? data.tools.length : 0,
|
|
248
|
-
enabled: data.enabled !== false,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (item.type === 'skill') {
|
|
253
|
-
return {
|
|
254
|
-
...base,
|
|
255
|
-
enabled: data.enabled !== false,
|
|
256
|
-
content_length: typeof data.content === 'string' ? data.content.length : 0,
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (item.type === 'prompt') {
|
|
261
|
-
return {
|
|
262
|
-
...base,
|
|
263
|
-
tags: Array.isArray(item.tags) ? item.tags : [],
|
|
264
|
-
content_length: typeof data.content === 'string' ? data.content.length : 0,
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
return base;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function summarizeDoc(item) {
|
|
272
|
-
return {
|
|
273
|
-
id: item.id,
|
|
274
|
-
title: item.title || '',
|
|
275
|
-
slug: item.slug || '',
|
|
276
|
-
category_id: item.category_id == null ? null : item.category_id,
|
|
277
|
-
status: item.status,
|
|
278
|
-
content_file: item.content_file || null,
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function summarizeDocCategory(item) {
|
|
283
|
-
return {
|
|
284
|
-
id: item.id,
|
|
285
|
-
name: item.name || '',
|
|
286
|
-
slug: item.slug || '',
|
|
287
|
-
parent_id: item.parent_id == null ? null : item.parent_id,
|
|
288
|
-
sort_order: item.sort_order == null ? 0 : item.sort_order,
|
|
289
|
-
status: item.status,
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
function summarizeSystemConfig(item) {
|
|
294
|
-
return {
|
|
295
|
-
config_key: item.config_key || '',
|
|
296
|
-
category: item.category || '',
|
|
297
|
-
value_type: item.value_type || '',
|
|
298
|
-
is_sensitive: Boolean(item.is_sensitive),
|
|
299
|
-
status: item.status,
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
|
|
303
79
|
function buildProjectMap(projectDir) {
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
};
|
|
80
|
+
const manifest = loadManifest(projectDir);
|
|
81
|
+
const checkouts = Object.values(manifest.entries).map((entry) => ({ ...entry }));
|
|
82
|
+
const pages = checkouts.filter((entry) => entry.resource_type === 'pages').map((entry) => ({
|
|
83
|
+
...entry, id: entry.resource_id, html_file: entry.local_path, area: classifyPageArea(entry),
|
|
84
|
+
}));
|
|
85
|
+
const navigations = checkouts.filter((entry) => entry.resource_type === 'navigations').map((entry) => ({
|
|
86
|
+
...entry, id: entry.resource_id, html_file: entry.local_path,
|
|
87
|
+
}));
|
|
88
|
+
const docs = checkouts.filter((entry) => entry.resource_type === 'docs').map((entry) => ({
|
|
89
|
+
...entry, id: entry.resource_id, content_file: entry.local_path,
|
|
90
|
+
}));
|
|
316
91
|
|
|
317
|
-
const pages = indexes.pages.items.map((p) => summarizePage(projectDir, p));
|
|
318
|
-
const navigations = indexes.navigations.items.map((n) => summarizeNav(projectDir, n));
|
|
319
92
|
const routeRefs = new Map();
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
for (const route of extractRoutes(html)) addRouteRefs(routeRefs, route, pageKey(page));
|
|
93
|
+
for (const entry of [...pages, ...navigations]) {
|
|
94
|
+
const file = absolutePath(projectDir, entry.local_path);
|
|
95
|
+
if (!fs.existsSync(file)) continue;
|
|
96
|
+
let source;
|
|
97
|
+
try { source = decodeTextFile(file, entry.content_type); } catch { continue; }
|
|
98
|
+
for (const route of extractRoutes(source)) {
|
|
99
|
+
if (!routeRefs.has(route)) routeRefs.set(route, new Set());
|
|
100
|
+
routeRefs.get(route).add(`${entry.resource_type}:${entry.resource_id}`);
|
|
101
|
+
}
|
|
330
102
|
}
|
|
331
|
-
|
|
332
103
|
return {
|
|
333
104
|
projectDir,
|
|
334
|
-
|
|
105
|
+
manifest_path: '.draftgo/worktree/manifest.json',
|
|
106
|
+
checkouts,
|
|
335
107
|
pages,
|
|
336
108
|
navigations,
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
fields: m.schema && m.schema.properties ? Object.keys(m.schema.properties) : [],
|
|
342
|
-
})),
|
|
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
|
-
}),
|
|
358
|
-
aihub: indexes.aihub.items.map(summarizeAIHub),
|
|
359
|
-
docs: indexes.docs.items.map(summarizeDoc),
|
|
360
|
-
doc_categories: indexes.doc_categories.items.map(summarizeDocCategory),
|
|
361
|
-
system_config: indexes.system_config.items.map(summarizeSystemConfig),
|
|
362
|
-
roles: indexes.roles.items.map((r) => ({ id: r.id, code: r.code || '', name: r.name || '', status: r.status })),
|
|
363
|
-
users_count: indexes.users.items.length,
|
|
364
|
-
pageGroups,
|
|
365
|
-
routeRefs: Object.fromEntries([...routeRefs.entries()].map(([route, sources]) => [route, [...sources]])),
|
|
109
|
+
docs,
|
|
110
|
+
pageGroups: collectGroups(pages),
|
|
111
|
+
routeRefs: Object.fromEntries([...routeRefs].map(([route, sources]) => [route, [...sources]])),
|
|
112
|
+
remote_only: ['db_meta', 'aihub', 'system_config', 'roles', 'users', 'doc_categories', 'custom_services'],
|
|
366
113
|
};
|
|
367
114
|
}
|
|
368
115
|
|
|
369
|
-
function
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
472
|
-
function isProbablySystemPage(page) {
|
|
473
|
-
const tag = String(page.tag || '');
|
|
474
|
-
const title = String(page.title || '');
|
|
475
|
-
const route = normalizeRoute(page.route);
|
|
476
|
-
return tag.includes('系统') || title.includes('系统') || ['/login', '/setup'].includes(route);
|
|
116
|
+
function worktreeFiles(projectDir, directory) {
|
|
117
|
+
const root = path.join(projectDir, '.draftgo', 'worktree', directory);
|
|
118
|
+
if (!fs.existsSync(root)) return [];
|
|
119
|
+
return fs.readdirSync(root, { withFileTypes: true })
|
|
120
|
+
.filter((entry) => entry.isFile())
|
|
121
|
+
.map((entry) => path.join(root, entry.name));
|
|
477
122
|
}
|
|
478
123
|
|
|
479
124
|
function analyzeProject(projectDir) {
|
|
@@ -485,49 +130,66 @@ function analyzeProject(projectDir) {
|
|
|
485
130
|
warnings.push(message);
|
|
486
131
|
warningDetails.push({ code, confidence, message });
|
|
487
132
|
};
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
133
|
+
const registered = new Set();
|
|
134
|
+
|
|
135
|
+
for (const entry of map.checkouts) {
|
|
136
|
+
let local;
|
|
137
|
+
let base;
|
|
138
|
+
try {
|
|
139
|
+
local = absolutePath(projectDir, entry.local_path);
|
|
140
|
+
base = absolutePath(projectDir, entry.base_path);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
errors.push(`${entry.resource_type} ${entry.resource_id}: ${error.message}`);
|
|
143
|
+
continue;
|
|
497
144
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
145
|
+
registered.add(path.resolve(local).toLowerCase());
|
|
146
|
+
if (!fs.existsSync(local)) {
|
|
147
|
+
errors.push(`${entry.resource_type} ${entry.resource_id}: local content is missing (${entry.local_path}).`);
|
|
148
|
+
}
|
|
149
|
+
if (!fs.existsSync(base)) {
|
|
150
|
+
errors.push(`${entry.resource_type} ${entry.resource_id}: checkout base is missing (${entry.base_path}).`);
|
|
151
|
+
} else if (sha256File(base) !== entry.base_hash) {
|
|
152
|
+
errors.push(`${entry.resource_type} ${entry.resource_id}: checkout base hash does not match the manifest.`);
|
|
504
153
|
}
|
|
505
|
-
}
|
|
506
154
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
155
|
+
const type = mediaType(entry.content_type);
|
|
156
|
+
let expectedExtension = entry.file_extension;
|
|
157
|
+
if (type === 'text/html' || type === 'application/xhtml+xml') expectedExtension = '.html';
|
|
158
|
+
else if (type === 'text/markdown' || type === 'text/x-markdown') expectedExtension = '.md';
|
|
159
|
+
else if (type === 'text/plain') expectedExtension = '.txt';
|
|
160
|
+
if (path.extname(entry.local_path).toLowerCase() !== String(expectedExtension).toLowerCase()) {
|
|
161
|
+
errors.push(`${entry.resource_type} ${entry.resource_id}: local extension does not preserve ${entry.content_type}.`);
|
|
162
|
+
}
|
|
163
|
+
if (fs.existsSync(local) && (type === 'text/html' || type === 'application/xhtml+xml')) {
|
|
164
|
+
let source;
|
|
165
|
+
try { source = decodeTextFile(local, entry.content_type); }
|
|
166
|
+
catch (error) {
|
|
167
|
+
errors.push(`${entry.resource_type} ${entry.resource_id}: ${error.message}`);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
for (const issue of htmlParseErrors(source).slice(0, 20)) {
|
|
171
|
+
const position = issue.line ? ` at ${issue.line}:${issue.column || 1}` : '';
|
|
172
|
+
addWarning('DG-HTML-001', 'high', `${entry.resource_type} ${entry.resource_id}: ${issue.code}${position}.`);
|
|
173
|
+
}
|
|
515
174
|
}
|
|
516
175
|
}
|
|
517
176
|
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
177
|
+
for (const directory of ['pages', 'navigations', 'docs']) {
|
|
178
|
+
for (const file of worktreeFiles(projectDir, directory)) {
|
|
179
|
+
if (!registered.has(path.resolve(file).toLowerCase())) {
|
|
180
|
+
const relative = path.relative(projectDir, file).replace(/\\/g, '/');
|
|
181
|
+
addWarning('DG-WORKTREE-ORPHAN', 'high', `Unregistered worktree file: ${relative}.`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const page of map.pages) {
|
|
186
|
+
if (!page.route || page.route === '/' || page.area === 'system') continue;
|
|
187
|
+
const sources = map.routeRefs[normalizeRoute(page.route)] || [];
|
|
188
|
+
if (!sources.some((source) => source.startsWith('navigations:'))) {
|
|
189
|
+
addWarning('DG-ROUTE-001', 'medium',
|
|
190
|
+
`pages ${page.resource_id}: route ${page.route} is not referenced by a checked-out navigation.`);
|
|
528
191
|
}
|
|
529
192
|
}
|
|
530
|
-
|
|
531
193
|
return { map, errors, warnings, warningDetails };
|
|
532
194
|
}
|
|
533
195
|
|
|
@@ -537,4 +199,5 @@ module.exports = {
|
|
|
537
199
|
normalizeRoute,
|
|
538
200
|
extractRoutes,
|
|
539
201
|
extractScriptBindings,
|
|
202
|
+
htmlParseErrors,
|
|
540
203
|
};
|