draftgo-cli 2.0.3 → 2.0.9

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/src/index.js CHANGED
@@ -35,6 +35,18 @@ async function run(argv) {
35
35
  return require('./commands/status')(projectDir);
36
36
  case 'doctor':
37
37
  return await require('./commands/doctor')(projectDir, flags);
38
+ case 'map':
39
+ return require('./commands/map')(projectDir, flags);
40
+ case 'check':
41
+ return require('./commands/check')(projectDir, flags);
42
+ case 'dev':
43
+ case 'build':
44
+ return require('./commands/projectScript')(projectDir, command, flags);
45
+ case 'pull':
46
+ case 'push':
47
+ return require('./commands/sync')(projectDir, command, positional, flags);
48
+ case 'local':
49
+ return require('./commands/local')(projectDir, positional, flags);
38
50
  case 'list-targets':
39
51
  case 'targets':
40
52
  return require('./commands/listTargets')();
@@ -101,6 +101,11 @@ function generate(projectDir, opts) {
101
101
  ' image: mysql:8.0',
102
102
  ` container_name: ${projectName}-mysql`,
103
103
  ' restart: unless-stopped',
104
+ ' logging:',
105
+ ' driver: json-file',
106
+ ' options:',
107
+ ' max-size: "10m"',
108
+ ' max-file: "3"',
104
109
  ' environment:',
105
110
  ' MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}',
106
111
  ' MYSQL_DATABASE: ${MYSQL_DATABASE}',
@@ -138,6 +143,11 @@ function generate(projectDir, opts) {
138
143
  ' image: redis:7-alpine',
139
144
  ` container_name: ${projectName}-redis`,
140
145
  ' restart: unless-stopped',
146
+ ' logging:',
147
+ ' driver: json-file',
148
+ ' options:',
149
+ ' max-size: "10m"',
150
+ ' max-file: "3"',
141
151
  ' command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}", "--appendonly", "yes"]',
142
152
  ' volumes:',
143
153
  ' - ./data/redis:/data',
@@ -166,6 +176,11 @@ function generate(projectDir, opts) {
166
176
  ' image: cabinai/draftgo:latest',
167
177
  ` container_name: ${projectName}`,
168
178
  ' restart: unless-stopped',
179
+ ' logging:',
180
+ ' driver: json-file',
181
+ ' options:',
182
+ ' max-size: "10m"',
183
+ ' max-file: "3"',
169
184
  ` ports:`,
170
185
  ` - "${appPort}:3000"`,
171
186
  ' environment:',
@@ -184,6 +199,10 @@ function generate(projectDir, opts) {
184
199
  ' LOG_LEVEL: INFO',
185
200
  ' LOG_MAX_BYTES: "5242880"',
186
201
  ' LOG_BACKUP_COUNT: "5"',
202
+ ' LOG_TO_CONSOLE: "true"',
203
+ ' LOG_CONSOLE_JSON: "true"',
204
+ ' LOG_UVICORN_ACCESS: "true"',
205
+ ' LOG_RUNTIME_HEARTBEAT_INTERVAL_SECONDS: "300"',
187
206
  ' UPLOAD_PROVIDER: local',
188
207
  ' UPLOAD_MAX_SIZE: "10485760"',
189
208
  ' LOCAL_UPLOAD_DIR: /app/backend/storage/uploads',
@@ -0,0 +1,368 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function exists(p) {
7
+ try { fs.accessSync(p); return true; } catch { return false; }
8
+ }
9
+
10
+ function readTextSafe(p) {
11
+ try { return fs.readFileSync(p, 'utf8'); } catch { return ''; }
12
+ }
13
+
14
+ function readJsonSafe(projectDir, rel) {
15
+ const abs = path.join(projectDir, rel);
16
+ try {
17
+ const raw = fs.readFileSync(abs, 'utf8');
18
+ const parsed = JSON.parse(raw);
19
+ return { ok: true, path: rel, items: Array.isArray(parsed) ? parsed : [], raw: parsed };
20
+ } catch (err) {
21
+ return { ok: false, path: rel, items: [], error: err.message };
22
+ }
23
+ }
24
+
25
+ function normalizeRoute(route) {
26
+ if (!route || typeof route !== 'string') return '';
27
+ const clean = route.trim();
28
+ if (!clean) return '';
29
+ if (/^https?:\/\//i.test(clean) || clean.startsWith('#') || clean.startsWith('mailto:')) return '';
30
+ return clean.startsWith('/') ? clean : `/${clean}`;
31
+ }
32
+
33
+ function relToAbs(projectDir, rel) {
34
+ if (!rel || typeof rel !== 'string') return null;
35
+ const clean = rel.replace(/\\/g, '/');
36
+ return path.isAbsolute(clean) ? clean : path.join(projectDir, clean);
37
+ }
38
+
39
+ function findGeneratedFile(projectDir, dirRel, prefix, exts) {
40
+ const dir = path.join(projectDir, dirRel);
41
+ if (!exists(dir)) return null;
42
+ const files = fs.readdirSync(dir).filter((f) => {
43
+ if (!f.startsWith(prefix)) return false;
44
+ return exts.some((ext) => f.endsWith(ext));
45
+ });
46
+ return files.length ? path.join(dirRel, files[0]).replace(/\\/g, '/') : null;
47
+ }
48
+
49
+ function itemFile(projectDir, section, item) {
50
+ if (!item || typeof item !== 'object') return null;
51
+ const direct = item.html_file || item.code_file || item.file;
52
+ if (direct && exists(relToAbs(projectDir, direct))) return direct.replace(/\\/g, '/');
53
+ const id = item.id == null ? '' : String(item.id);
54
+ if (!id) return null;
55
+ if (section === 'pages') return findGeneratedFile(projectDir, '.draftgo/pages', `page_${id}_`, ['.html']);
56
+ if (section === 'navigations') return findGeneratedFile(projectDir, '.draftgo/navigations', `nav_${id}_`, ['.html']);
57
+ return null;
58
+ }
59
+
60
+ function extractRoutes(html) {
61
+ const routes = new Set();
62
+ if (!html) return routes;
63
+ const patterns = [
64
+ /\bdata-page-route\s*=\s*["']([^"']+)["']/gi,
65
+ /\bhref\s*=\s*["']([^"']+)["']/gi,
66
+ ];
67
+ for (const re of patterns) {
68
+ let m;
69
+ while ((m = re.exec(html))) {
70
+ const route = normalizeRoute(m[1]);
71
+ if (route) routes.add(route);
72
+ }
73
+ }
74
+ return routes;
75
+ }
76
+
77
+ function pageKey(page) {
78
+ if (page && page.id != null) return `page:${page.id}`;
79
+ return `page:${normalizeRoute(page && page.route) || page && page.title || 'new'}`;
80
+ }
81
+
82
+ function addRouteRefs(refs, route, source) {
83
+ const clean = normalizeRoute(route);
84
+ if (!clean) return;
85
+ if (!refs.has(clean)) refs.set(clean, new Set());
86
+ refs.get(clean).add(source);
87
+ }
88
+
89
+ function summarizePage(projectDir, item) {
90
+ const file = itemFile(projectDir, 'pages', item);
91
+ return {
92
+ id: item.id,
93
+ title: item.title || '',
94
+ route: normalizeRoute(item.route),
95
+ permission: item.permission || null,
96
+ tag: item.tag || '',
97
+ html_file: file,
98
+ };
99
+ }
100
+
101
+ function summarizeNav(projectDir, item) {
102
+ const file = itemFile(projectDir, 'navigations', item);
103
+ return {
104
+ id: item.id,
105
+ code: item.code || '',
106
+ name: item.name || '',
107
+ status: item.status,
108
+ html_file: file,
109
+ };
110
+ }
111
+
112
+ function summarizeAIHub(item) {
113
+ const data = item && typeof item.data === 'object' && item.data ? item.data : {};
114
+ const spec = data.spec && typeof data.spec === 'object' ? data.spec : {};
115
+ const base = {
116
+ id: item.id,
117
+ name: item.name || '',
118
+ type: item.type || '',
119
+ status: item.status,
120
+ };
121
+
122
+ if (item.type === 'agent') {
123
+ const tools = spec.tools && typeof spec.tools === 'object' ? spec.tools : {};
124
+ const outputFormat = spec.output_format && typeof spec.output_format === 'object' ? spec.output_format : {};
125
+ const modelSelection = spec.model_selection && typeof spec.model_selection === 'object' ? spec.model_selection : {};
126
+ return {
127
+ ...base,
128
+ mode: spec.mode || 'chat',
129
+ model_id: spec.model_id || null,
130
+ model: spec.model || data.model || '',
131
+ fallback_models: Array.isArray(spec.fallback_models) ? spec.fallback_models : [],
132
+ output_format: {
133
+ mode: outputFormat.mode || 'native',
134
+ json_strategy: outputFormat.json && outputFormat.json.strategy || null,
135
+ has_schema: Boolean(outputFormat.json && outputFormat.json.schema),
136
+ },
137
+ model_selection: {
138
+ user_selectable: Boolean(modelSelection.user_selectable),
139
+ on_invalid: modelSelection.on_invalid || 'ignore',
140
+ },
141
+ tools: {
142
+ enabled: Boolean(tools.enabled && Array.isArray(tools.sources) && tools.sources.length),
143
+ sources: Array.isArray(tools.sources)
144
+ ? tools.sources.map((s) => ({
145
+ type: s.type || '',
146
+ id: s.id,
147
+ name: s.name || '',
148
+ tool_filter: Array.isArray(s.tool_filter) ? s.tool_filter : [],
149
+ }))
150
+ : [],
151
+ },
152
+ skills_count: Array.isArray(spec.skills) ? spec.skills.length : 0,
153
+ };
154
+ }
155
+
156
+ if (item.type === 'model') {
157
+ return {
158
+ ...base,
159
+ provider: data.provider || 'openai-compatible',
160
+ models_count: Array.isArray(data.models) ? data.models.length : 0,
161
+ supports_response_format: data.supports_response_format,
162
+ supports_json_schema: data.supports_json_schema,
163
+ image_generation_probe: data.image_generation_probe
164
+ ? {
165
+ ok: Boolean(data.image_generation_probe.ok),
166
+ model: data.image_generation_probe.model || '',
167
+ tested_at: data.image_generation_probe.tested_at || '',
168
+ }
169
+ : null,
170
+ };
171
+ }
172
+
173
+ if (item.type === 'mcp') {
174
+ return {
175
+ ...base,
176
+ transport: data.transport || '',
177
+ tools_count: Array.isArray(data.tools) ? data.tools.length : 0,
178
+ enabled: data.enabled !== false,
179
+ };
180
+ }
181
+
182
+ if (item.type === 'skill') {
183
+ return {
184
+ ...base,
185
+ enabled: data.enabled !== false,
186
+ content_length: typeof data.content === 'string' ? data.content.length : 0,
187
+ };
188
+ }
189
+
190
+ if (item.type === 'prompt') {
191
+ return {
192
+ ...base,
193
+ tags: Array.isArray(item.tags) ? item.tags : [],
194
+ content_length: typeof data.content === 'string' ? data.content.length : 0,
195
+ };
196
+ }
197
+
198
+ return base;
199
+ }
200
+
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
+ function summarizeDoc(item) {
215
+ return {
216
+ id: item.id,
217
+ title: item.title || '',
218
+ slug: item.slug || '',
219
+ category_id: item.category_id == null ? null : item.category_id,
220
+ status: item.status,
221
+ content_file: item.content_file || null,
222
+ };
223
+ }
224
+
225
+ function summarizeDocCategory(item) {
226
+ return {
227
+ id: item.id,
228
+ name: item.name || '',
229
+ slug: item.slug || '',
230
+ parent_id: item.parent_id == null ? null : item.parent_id,
231
+ sort_order: item.sort_order == null ? 0 : item.sort_order,
232
+ status: item.status,
233
+ };
234
+ }
235
+
236
+ function summarizeSystemConfig(item) {
237
+ return {
238
+ config_key: item.config_key || '',
239
+ category: item.category || '',
240
+ value_type: item.value_type || '',
241
+ is_sensitive: Boolean(item.is_sensitive),
242
+ status: item.status,
243
+ };
244
+ }
245
+
246
+ function buildProjectMap(projectDir) {
247
+ const indexes = {
248
+ pages: readJsonSafe(projectDir, '.draftgo/pages/index.json'),
249
+ navigations: readJsonSafe(projectDir, '.draftgo/navigations/index.json'),
250
+ db_meta: readJsonSafe(projectDir, '.draftgo/db_meta/index.json'),
251
+ custom_scripts: readJsonSafe(projectDir, '.draftgo/custom_scripts/index.json'),
252
+ aihub: readJsonSafe(projectDir, '.draftgo/aihub/index.json'),
253
+ external_apis: readJsonSafe(projectDir, '.draftgo/external_apis/index.json'),
254
+ docs: readJsonSafe(projectDir, '.draftgo/docs/articles/index.json'),
255
+ doc_categories: readJsonSafe(projectDir, '.draftgo/doc_categories/index.json'),
256
+ system_config: readJsonSafe(projectDir, '.draftgo/system_config/index.json'),
257
+ roles: readJsonSafe(projectDir, '.draftgo/roles/index.json'),
258
+ users: readJsonSafe(projectDir, '.draftgo/users/index.json'),
259
+ };
260
+
261
+ const pages = indexes.pages.items.map((p) => summarizePage(projectDir, p));
262
+ const navigations = indexes.navigations.items.map((n) => summarizeNav(projectDir, n));
263
+ const routeRefs = new Map();
264
+
265
+ for (const nav of navigations) {
266
+ const html = nav.html_file ? readTextSafe(relToAbs(projectDir, nav.html_file)) : '';
267
+ for (const route of extractRoutes(html)) addRouteRefs(routeRefs, route, `nav:${nav.id || nav.code || nav.name}`);
268
+ }
269
+
270
+ for (const page of pages) {
271
+ const html = page.html_file ? readTextSafe(relToAbs(projectDir, page.html_file)) : '';
272
+ for (const route of extractRoutes(html)) addRouteRefs(routeRefs, route, pageKey(page));
273
+ }
274
+
275
+ return {
276
+ projectDir,
277
+ indexes: Object.fromEntries(Object.entries(indexes).map(([k, v]) => [k, { ok: v.ok, path: v.path, count: v.items.length }])),
278
+ pages,
279
+ navigations,
280
+ db_meta: indexes.db_meta.items.map((m) => ({
281
+ id: m.id,
282
+ type: m.type || '',
283
+ label: m.label || '',
284
+ fields: m.schema && m.schema.properties ? Object.keys(m.schema.properties) : [],
285
+ })),
286
+ custom_scripts: indexes.custom_scripts.items.map((s) => ({
287
+ id: s.id,
288
+ name: s.name || '',
289
+ slug: s.slug || '',
290
+ mode: s.mode || '',
291
+ status: s.status,
292
+ code_file: itemFile(projectDir, 'custom_scripts', s),
293
+ })),
294
+ aihub: indexes.aihub.items.map(summarizeAIHub),
295
+ external_apis: indexes.external_apis.items.map(summarizeExternalAPI),
296
+ docs: indexes.docs.items.map(summarizeDoc),
297
+ doc_categories: indexes.doc_categories.items.map(summarizeDocCategory),
298
+ system_config: indexes.system_config.items.map(summarizeSystemConfig),
299
+ roles: indexes.roles.items.map((r) => ({ id: r.id, code: r.code || '', name: r.name || '', status: r.status })),
300
+ users_count: indexes.users.items.length,
301
+ routeRefs: Object.fromEntries([...routeRefs.entries()].map(([route, sources]) => [route, [...sources]])),
302
+ };
303
+ }
304
+
305
+ function isProbablySystemPage(page) {
306
+ const tag = String(page.tag || '');
307
+ const title = String(page.title || '');
308
+ const route = normalizeRoute(page.route);
309
+ return tag.includes('系统') || title.includes('系统') || ['/login', '/setup'].includes(route);
310
+ }
311
+
312
+ function analyzeProject(projectDir) {
313
+ const map = buildProjectMap(projectDir);
314
+ const errors = [];
315
+ const warnings = [];
316
+ const routeSeen = new Map();
317
+
318
+ if (!exists(path.join(projectDir, '.draftgo'))) {
319
+ errors.push('未找到 .draftgo/,请先运行 draftgo init 或 draftgo connect。');
320
+ }
321
+
322
+ for (const page of map.pages) {
323
+ const label = `${page.title || '未命名页面'}${page.id != null ? `#${page.id}` : ''}`;
324
+ if (!page.route) errors.push(`${label} 缺少 route。`);
325
+ if (!page.title) warnings.push(`${label} 缺少 title。`);
326
+ if (!page.html_file) errors.push(`${label} 找不到 html_file 或 page_${page.id}_*.html。`);
327
+
328
+ if (page.route) {
329
+ if (routeSeen.has(page.route)) {
330
+ errors.push(`route 重复:${page.route}(${routeSeen.get(page.route)} 与 ${label})。`);
331
+ } else {
332
+ routeSeen.set(page.route, label);
333
+ }
334
+
335
+ if (page.route !== '/' && !isProbablySystemPage(page)) {
336
+ const refs = map.routeRefs[page.route] || [];
337
+ const own = pageKey(page);
338
+ const externalRefs = refs.filter((s) => s !== own);
339
+ if (externalRefs.length === 0) {
340
+ warnings.push(`${label} (${page.route}) 未在导航、首页或其他页面入口中发现绑定引用。`);
341
+ }
342
+ }
343
+ }
344
+
345
+ if (page.html_file) {
346
+ const html = readTextSafe(relToAbs(projectDir, page.html_file));
347
+ if (/\b(mockData|demoData|fakeData|sampleData|staticData)\b/i.test(html)) {
348
+ warnings.push(`${label} 疑似包含 mock/demo/fake/staticData,确认是否为用户明确要求的静态/demo。`);
349
+ }
350
+ 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
+ warnings.push(`${label} 疑似只有反馈提示、缺少真实数据读写或 API 调用。`);
352
+ }
353
+ }
354
+ }
355
+
356
+ if (map.pages.length === 0 && exists(path.join(projectDir, '.draftgo'))) {
357
+ warnings.push('未发现 pages/index.json 页面缓存;开发前建议先拉取页面。');
358
+ }
359
+
360
+ return { map, errors, warnings };
361
+ }
362
+
363
+ module.exports = {
364
+ buildProjectMap,
365
+ analyzeProject,
366
+ normalizeRoute,
367
+ extractRoutes,
368
+ };