@demogo-cn/shared 0.9.36

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 (2) hide show
  1. package/lib/core.js +437 -0
  2. package/package.json +10 -0
package/lib/core.js ADDED
@@ -0,0 +1,437 @@
1
+ // DemoGo v0.9.6 ? Shared core module for CLI and MCP
2
+ // This module is shared between cli/ and mcp/ packages.
3
+
4
+ import { gzipSync } from "node:zlib";
5
+ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
6
+ import { readFileSync } from "node:fs";
7
+ import path from "node:path";
8
+ // Package-level config ? each package calls configureShared() before using API functions
9
+ let _userAgentPrefix = "demogo-shared";
10
+ let _version = "0.0.0";
11
+ export function configureShared({ userAgentPrefix, version }) {
12
+ if (userAgentPrefix) _userAgentPrefix = userAgentPrefix;
13
+ if (version) _version = version;
14
+ }
15
+ function ua() { return `${_userAgentPrefix}/${_version}`; }
16
+
17
+ export const MAX_FILES = 800;
18
+ export const MAX_BYTES = 50 * 1024 * 1024;
19
+
20
+ export const unsafeProjectDirectoryNames = new Set([
21
+ "desktop",
22
+ "downloads",
23
+ "documents",
24
+ "onedrive",
25
+ "桌面",
26
+ "下载",
27
+ "文档"
28
+ ]);
29
+
30
+ const EXCLUDED_DIRS = new Set([
31
+ ".demogo",
32
+ ".git",
33
+ ".hg",
34
+ ".svn",
35
+ ".next",
36
+ ".nuxt",
37
+ ".output",
38
+ ".cache",
39
+ ".turbo",
40
+ ".vite",
41
+ "node_modules",
42
+ "coverage",
43
+ "tmp",
44
+ "temp"
45
+ ]);
46
+
47
+ const EXCLUDED_FILES = new Set([
48
+ ".env",
49
+ ".env.local",
50
+ ".env.development",
51
+ ".env.production",
52
+ ".npmrc",
53
+ ".yarnrc",
54
+ ".DS_Store",
55
+ "Thumbs.db"
56
+ ]);
57
+
58
+ const SENSITIVE_EXTENSIONS = [
59
+ ".pem",
60
+ ".key",
61
+ ".p12",
62
+ ".pfx",
63
+ ".crt",
64
+ ".cer"
65
+ ];
66
+
67
+ export async function assertDirectory(dir) {
68
+ try {
69
+ const stats = await stat(dir);
70
+ if (!stats.isDirectory()) throw new Error(`不是项目目录:${dir}`);
71
+ } catch {
72
+ throw new Error(`找不到项目目录:${dir}`);
73
+ }
74
+ }
75
+
76
+ export function assertSafeProjectDirectory(dir) {
77
+ const resolved = path.resolve(dir);
78
+ const home = path.resolve(process.env.USERPROFILE || process.env.HOME || "");
79
+ const baseName = path.basename(resolved).toLowerCase();
80
+
81
+ if (home && resolved === home) {
82
+ throw new Error([
83
+ "当前目录像是用户主目录,不适合直接发布。",
84
+ "请新建一个干净文件夹,只放要发布的项目文件,然后在该文件夹里运行发布命令。",
85
+ "也可以使用:npx --yes @demogo-cn/cli deploy --dir <项目目录>"
86
+ ].join("\n"));
87
+ }
88
+
89
+ if (unsafeProjectDirectoryNames.has(baseName)) {
90
+ throw new Error([
91
+ `当前目录是 ${path.basename(resolved)},通常会包含很多无关文件,不适合直接发布。`,
92
+ "请新建一个干净文件夹,只放要发布的项目文件;如果只有一个 HTML 文件,也可以让 AI 先创建临时目录再发布。",
93
+ "也可以使用:npx --yes @demogo-cn/cli deploy --dir <项目目录>"
94
+ ].join("\n"));
95
+ }
96
+ }
97
+
98
+ export async function collectFiles(rootDir) {
99
+ const files = [];
100
+ let totalBytes = 0;
101
+
102
+ async function walk(currentDir, relativeDir = "") {
103
+ const entries = await readdir(currentDir, { withFileTypes: true });
104
+ entries.sort((left, right) => left.name.localeCompare(right.name));
105
+ for (const entry of entries) {
106
+ const fullPath = path.join(currentDir, entry.name);
107
+ const relativePath = toPosixPath(path.join(relativeDir, entry.name));
108
+ if (shouldExclude(entry, relativePath)) continue;
109
+ if (entry.isDirectory()) {
110
+ await walk(fullPath, relativePath);
111
+ continue;
112
+ }
113
+ if (!entry.isFile()) continue;
114
+ const stats = await stat(fullPath);
115
+ totalBytes += stats.size;
116
+ if (files.length >= MAX_FILES) throw new Error(projectTooLargeMessage(`项目文件超过 ${MAX_FILES} 个。`));
117
+ if (totalBytes > MAX_BYTES) throw new Error(projectTooLargeMessage("项目文件超过 50MB。"));
118
+ files.push({ fullPath, relativePath, size: stats.size });
119
+ }
120
+ }
121
+
122
+ await walk(rootDir);
123
+ return files;
124
+ }
125
+
126
+ export function projectTooLargeMessage(reason) {
127
+ return [
128
+ `${reason}你可能在桌面、下载、文档或用户根目录执行了发布。`,
129
+ "请切换到真正的项目目录,或新建一个干净文件夹,只放要发布的项目文件。",
130
+ "如果只有一个 HTML 文件,不需要改名为 index.html,可以把它单独放进干净目录后发布。",
131
+ "也可以使用:npx --yes @demogo-cn/cli deploy --dir <项目目录>"
132
+ ].join("\n");
133
+ }
134
+
135
+ export function summarizeProject(projectDir, files) {
136
+ const totalBytes = files.reduce((sum, file) => sum + file.size, 0);
137
+ const hasReadyPage = files.some((file) => [
138
+ "index.html",
139
+ "dist/index.html",
140
+ "build/index.html",
141
+ "out/index.html",
142
+ "public/index.html"
143
+ ].includes(file.relativePath));
144
+ const singleHtmlEntry = inferSingleHtmlEntry(files)?.relativePath || "";
145
+ const hasPackageJson = files.some((file) => file.relativePath === "package.json");
146
+ return {
147
+ projectDir,
148
+ fileCount: files.length,
149
+ totalBytes,
150
+ hasReadyPage,
151
+ singleHtmlEntry,
152
+ hasPackageJson
153
+ };
154
+ }
155
+
156
+ export async function createProjectArchive(rootDir, fileList, archivePath) {
157
+ const chunks = [];
158
+ const htmlEntry = inferSingleHtmlEntry(fileList);
159
+ for (const file of fileList) {
160
+ const data = await readFile(file.fullPath);
161
+ const archivePathName = htmlEntry && file.relativePath === htmlEntry.relativePath ? "index.html" : file.relativePath;
162
+ chunks.push(...createTarHeaders(archivePathName, data.length, 0o644));
163
+ chunks.push(data);
164
+ chunks.push(Buffer.alloc(padLength(data.length)));
165
+ }
166
+ chunks.push(Buffer.alloc(1024));
167
+ await mkdir(path.dirname(archivePath), { recursive: true });
168
+ await writeFile(archivePath, gzipSync(Buffer.concat(chunks)));
169
+ }
170
+
171
+ function inferSingleHtmlEntry(files) {
172
+ const hasIndex = files.some((file) => file.relativePath === "index.html");
173
+ if (hasIndex) return null;
174
+ const rootHtmlFiles = files.filter((file) => (
175
+ /^[^/]+\.html?$/i.test(file.relativePath) &&
176
+ !/^admin\.html$/i.test(file.relativePath) &&
177
+ !/^login\.html$/i.test(file.relativePath)
178
+ ));
179
+ return rootHtmlFiles.length === 1 ? rootHtmlFiles[0] : null;
180
+ }
181
+
182
+ export async function deployArchive({ apiBase, token, archivePath, projectName, source = "mcp", userAgent = ua() }) {
183
+ const formData = new FormData();
184
+ formData.set("name", projectName);
185
+ formData.set("source", source);
186
+ formData.set(
187
+ "project",
188
+ new Blob([await readFile(archivePath)], { type: "application/gzip" }),
189
+ path.basename(archivePath)
190
+ );
191
+
192
+ let response;
193
+ try {
194
+ response = await fetch(`${normalizeApiBase(apiBase)}/api/agent/deploy`, {
195
+ method: "POST",
196
+ headers: {
197
+ Authorization: `Bearer ${token}`,
198
+ "User-Agent": userAgent,
199
+ "X-DemoGo-Deploy-Source": source
200
+ },
201
+ body: formData
202
+ });
203
+ } catch {
204
+ throw new Error(`无法连接 DemoGo:${normalizeApiBase(apiBase)}。请检查 API 地址是否正确,或稍后再试。`);
205
+ }
206
+
207
+ return parseDeploymentResponse(response);
208
+ }
209
+
210
+ export async function checkApiHealth(apiBase, userAgent = ua()) {
211
+ let response;
212
+ try {
213
+ response = await fetch(`${normalizeApiBase(apiBase)}/api/health`, {
214
+ method: "GET",
215
+ headers: { "User-Agent": userAgent }
216
+ });
217
+ } catch {
218
+ throw new Error(`无法连接 DemoGo:${normalizeApiBase(apiBase)}。请检查平台地址。`);
219
+ }
220
+ const text = await response.text();
221
+ const payload = parseJson(text);
222
+ if (!response.ok || !payload?.ok) {
223
+ throw new Error(`DemoGo 平台地址不可用:HTTP ${response.status}`);
224
+ }
225
+ return payload;
226
+ }
227
+
228
+ export async function checkAgentToken(apiBase, token, userAgent = ua()) {
229
+ let response;
230
+ try {
231
+ response = await fetch(`${normalizeApiBase(apiBase)}/api/agent/token-check`, {
232
+ method: "GET",
233
+ headers: {
234
+ Authorization: `Bearer ${token}`,
235
+ "User-Agent": userAgent
236
+ }
237
+ });
238
+ } catch {
239
+ throw new Error(`无法连接 DemoGo:${normalizeApiBase(apiBase)}。请检查平台地址。`);
240
+ }
241
+ const text = await response.text();
242
+ const payload = parseJson(text);
243
+ if (!response.ok || !payload?.ok) {
244
+ throw new Error(payload?.error || readableHttpError(response.status, text));
245
+ }
246
+ return payload;
247
+ }
248
+
249
+ export async function updateArchive({ apiBase, token, archivePath, demoRef, source = "mcp", userAgent = ua() }) {
250
+ const formData = new FormData();
251
+ formData.set("source", source);
252
+ formData.set("demoId", demoRef);
253
+ formData.set(
254
+ "project",
255
+ new Blob([await readFile(archivePath)], { type: "application/gzip" }),
256
+ path.basename(archivePath)
257
+ );
258
+
259
+ let response;
260
+ try {
261
+ response = await fetch(`${normalizeApiBase(apiBase)}/api/agent/update`, {
262
+ method: "POST",
263
+ headers: {
264
+ Authorization: `Bearer ${token}`,
265
+ "User-Agent": userAgent,
266
+ "X-DemoGo-Deploy-Source": source
267
+ },
268
+ body: formData
269
+ });
270
+ } catch {
271
+ throw new Error(`无法连接 DemoGo:${normalizeApiBase(apiBase)}。请检查 API 地址是否正确,或稍后再试。`);
272
+ }
273
+
274
+ return parseDeploymentResponse(response);
275
+ }
276
+
277
+ async function parseDeploymentResponse(response) {
278
+ const text = await response.text();
279
+ const payload = parseJson(text);
280
+ if (!response.ok) {
281
+ const message = payload?.error || readableHttpError(response.status, text);
282
+ const shouldShowFixPrompt = response.status === 400 || payload?.inspection?.canPublish === false;
283
+ const fixPrompt = shouldShowFixPrompt
284
+ ? payload?.inspection?.fixPrompt || payload?.inspection?.ruleReport?.fixPrompt || ""
285
+ : "";
286
+ const details = fixPrompt ? `\n\n给 AI 工具的修改建议:\n${fixPrompt}` : "";
287
+ throw new Error(`${message}${details}`);
288
+ }
289
+ return payload;
290
+ }
291
+
292
+ export async function getProjectDetails(apiBase, token, demoId, userAgent = ua()) {
293
+ let response;
294
+ try {
295
+ response = await fetch(`${normalizeApiBase(apiBase)}/api/agent/project/${encodeURIComponent(demoId)}`, {
296
+ method: "GET",
297
+ headers: {
298
+ Authorization: `Bearer ${token}`,
299
+ "User-Agent": userAgent
300
+ }
301
+ });
302
+ } catch {
303
+ throw new Error(`无法连接 DemoGo:${normalizeApiBase(apiBase)}。请检查 API 地址是否正确,或稍后再试。`);
304
+ }
305
+ return parseDeploymentResponse(response);
306
+ }
307
+
308
+ export function normalizeApiBase(value) {
309
+ return String(value || "").trim().replace(/\/+$/, "");
310
+ }
311
+
312
+ export function safeArchiveName(value) {
313
+ return String(value || "demogo-project").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "demogo-project";
314
+ }
315
+
316
+ export function formatBytes(bytes) {
317
+ if (bytes < 1024) return `${bytes} B`;
318
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
319
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
320
+ }
321
+
322
+ function shouldExclude(entry, relativePath) {
323
+ const name = entry.name;
324
+ if (entry.isDirectory() && EXCLUDED_DIRS.has(name)) return true;
325
+ if (EXCLUDED_FILES.has(name)) return true;
326
+ if (name.endsWith(".log")) return true;
327
+ if (SENSITIVE_EXTENSIONS.some((extension) => name.toLowerCase().endsWith(extension))) return true;
328
+ if (relativePath.startsWith("dist/") && relativePath.includes("/server-package/")) return true;
329
+ return false;
330
+ }
331
+
332
+ function createTarHeaders(name, size, mode) {
333
+ const normalizedName = toPosixPath(name);
334
+ const ustarPath = splitUstarPath(normalizedName);
335
+ if (ustarPath && isAscii(normalizedName)) {
336
+ return [createTarHeader({ name: ustarPath.name, prefix: ustarPath.prefix, size, mode })];
337
+ }
338
+
339
+ const paxBody = Buffer.from(createPaxRecord("path", normalizedName), "utf8");
340
+ const fallbackName = safeTarName(normalizedName);
341
+ return [
342
+ createTarHeader({ name: `PaxHeaders/${fallbackName}`.slice(0, 100), size: paxBody.length, mode: 0o644, type: "x" }),
343
+ paxBody,
344
+ Buffer.alloc(padLength(paxBody.length)),
345
+ createTarHeader({ name: fallbackName, size, mode })
346
+ ];
347
+ }
348
+
349
+ function createTarHeader({ name, size, mode, type = "0", prefix = "" }) {
350
+ const buffer = Buffer.alloc(512, 0);
351
+ writeString(buffer, name, 0, 100);
352
+ writeOctal(buffer, mode, 100, 8);
353
+ writeOctal(buffer, 0, 108, 8);
354
+ writeOctal(buffer, 0, 116, 8);
355
+ writeOctal(buffer, size, 124, 12);
356
+ writeOctal(buffer, Math.floor(Date.now() / 1000), 136, 12);
357
+ buffer.fill(0x20, 148, 156);
358
+ buffer[156] = type.charCodeAt(0);
359
+ writeString(buffer, "ustar", 257, 6);
360
+ writeString(buffer, "00", 263, 2);
361
+ writeString(buffer, prefix, 345, 155);
362
+ const checksum = buffer.reduce((sum, value) => sum + value, 0);
363
+ writeOctal(buffer, checksum, 148, 8);
364
+ return buffer;
365
+ }
366
+
367
+ function writeString(buffer, value, offset, length) {
368
+ const data = Buffer.from(String(value), "utf8");
369
+ data.subarray(0, length).copy(buffer, offset);
370
+ }
371
+
372
+ function writeOctal(buffer, value, offset, length) {
373
+ const text = value.toString(8).padStart(length - 1, "0").slice(0, length - 1);
374
+ buffer.write(text, offset, length - 1, "ascii");
375
+ buffer[offset + length - 1] = 0;
376
+ }
377
+
378
+ function padLength(size) {
379
+ return (512 - (size % 512)) % 512;
380
+ }
381
+
382
+ function toPosixPath(value) {
383
+ return value.split(path.sep).join("/");
384
+ }
385
+
386
+ function splitUstarPath(name) {
387
+ if (Buffer.byteLength(name, "utf8") <= 100) return { name, prefix: "" };
388
+ const parts = name.split("/");
389
+ for (let index = parts.length - 1; index > 0; index -= 1) {
390
+ const prefix = parts.slice(0, index).join("/");
391
+ const entryName = parts.slice(index).join("/");
392
+ if (Buffer.byteLength(entryName, "utf8") <= 100 && Buffer.byteLength(prefix, "utf8") <= 155) {
393
+ return { name: entryName, prefix };
394
+ }
395
+ }
396
+ return null;
397
+ }
398
+
399
+ function createPaxRecord(key, value) {
400
+ let record = ` ${key}=${value}\n`;
401
+ let length = Buffer.byteLength(record, "utf8") + String(Buffer.byteLength(record, "utf8")).length;
402
+ while (true) {
403
+ const next = `${length}${record}`;
404
+ const nextLength = Buffer.byteLength(next, "utf8");
405
+ if (nextLength === length) return next;
406
+ length = nextLength;
407
+ }
408
+ }
409
+
410
+ function safeTarName(value) {
411
+ const baseName = path.posix.basename(String(value || "file")) || "file";
412
+ const safe = safeArchiveName(baseName) || "file";
413
+ if (Buffer.byteLength(safe, "utf8") <= 100) return safe;
414
+ return safe.slice(0, 80) || "file";
415
+ }
416
+
417
+ function isAscii(value) {
418
+ return /^[\x00-\x7F]*$/.test(value);
419
+ }
420
+
421
+ function parseJson(text) {
422
+ try {
423
+ return JSON.parse(text);
424
+ } catch {
425
+ return null;
426
+ }
427
+ }
428
+
429
+ function readableHttpError(status, text) {
430
+ if (status === 401) return "AI 发布口令无效或已被重置。";
431
+ if (status === 403) return "当前账号额度不足,无法生成新的试用链接。";
432
+ if (status === 413) return "项目包太大,请压缩或删除大文件后重试。";
433
+ if (status === 400) return "项目暂时不能发布,请根据 DemoGo 返回的提示修改后再试。";
434
+ if (text?.trim().startsWith("<")) return "服务器返回了异常页面,本次发布没有完成。";
435
+ return `DemoGo 发布失败:HTTP ${status}`;
436
+ }
437
+
package/package.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": "0.9.36",
3
+ "name": "@demogo-cn/shared",
4
+ "main": "./lib/core.js",
5
+ "description": "DemoGo shared core module for CLI and MCP",
6
+ "exports": {
7
+ ".": "./lib/core.js"
8
+ },
9
+ "type": "module"
10
+ }