@quantacenter/quanta-problem-cli 0.0.0-stage → 1.0.0
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 +337 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4134 -0
- package/dist/index.js.map +1 -0
- package/dist/program.d.ts +134 -0
- package/dist/program.js +125 -0
- package/dist/program.js.map +1 -0
- package/package.json +60 -5
- package/templates/backend_service/README.md +55 -0
- package/templates/backend_service/answer/package.json +9 -0
- package/templates/backend_service/answer/pnpm-lock.yaml +9 -0
- package/templates/backend_service/answer/server.js +61 -0
- package/templates/backend_service/detail.md +30 -0
- package/templates/backend_service/gitignore +5 -0
- package/templates/backend_service/judge.js +43 -0
- package/templates/backend_service/problem.config.ts +37 -0
- package/templates/backend_service/template/package.json +9 -0
- package/templates/backend_service/template/pnpm-lock.yaml +9 -0
- package/templates/backend_service/template/server.js +28 -0
- package/templates/frontend/README.md +34 -0
- package/templates/frontend/answer/index.html +97 -0
- package/templates/frontend/gitignore +5 -0
- package/templates/frontend/judge.js +41 -0
- package/templates/frontend/problem.config.ts +34 -0
- package/templates/frontend/template/index.html +74 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { CommanderError, Command } from 'commander';
|
|
3
|
+
import { resolve, relative, basename, join, dirname, isAbsolute, sep, extname } from 'path';
|
|
4
|
+
import { readFileSync, existsSync, constants } from 'fs';
|
|
5
|
+
import { readdir, writeFile, readFile, stat, mkdir, access, chmod, rename, rm } from 'fs/promises';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import sharp from 'sharp';
|
|
9
|
+
import { createInterface } from 'readline';
|
|
10
|
+
import { select as select$1, isCancel, multiselect as multiselect$1 } from '@clack/prompts';
|
|
11
|
+
import { homedir } from 'os';
|
|
12
|
+
import { spawn } from 'child_process';
|
|
13
|
+
import { createColors } from 'picocolors';
|
|
14
|
+
|
|
15
|
+
// src/core/exit-codes.ts
|
|
16
|
+
var EXIT = {
|
|
17
|
+
/** 全部成功 */
|
|
18
|
+
OK: 0,
|
|
19
|
+
/** 未分类的运行时错误 */
|
|
20
|
+
FAILURE: 1,
|
|
21
|
+
/** 参数/配置用法错误(用户输入问题,不是 bug) */
|
|
22
|
+
USAGE: 2,
|
|
23
|
+
/** 预检或审计未通过(题目本身不合格,需要改题目而不是改脚本) */
|
|
24
|
+
PRECHECK_FAILED: 3,
|
|
25
|
+
/** 网络或服务端 5xx */
|
|
26
|
+
NETWORK: 4,
|
|
27
|
+
/** 未登录 / 令牌失效 / 权限不足 */
|
|
28
|
+
AUTH: 5
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/core/errors.ts
|
|
32
|
+
var CliError = class extends Error {
|
|
33
|
+
hint;
|
|
34
|
+
details;
|
|
35
|
+
exitCode;
|
|
36
|
+
constructor(message, options = {}) {
|
|
37
|
+
super(message, { cause: options.cause });
|
|
38
|
+
this.name = new.target.name;
|
|
39
|
+
this.hint = options.hint;
|
|
40
|
+
this.details = options.details;
|
|
41
|
+
this.exitCode = options.exitCode ?? EXIT.FAILURE;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var UsageError = class extends CliError {
|
|
45
|
+
constructor(message, options = {}) {
|
|
46
|
+
super(message, { ...options, exitCode: EXIT.USAGE });
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var ConfigError = class extends CliError {
|
|
50
|
+
constructor(message, options = {}) {
|
|
51
|
+
super(message, { ...options, exitCode: EXIT.USAGE });
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var PrecheckError = class extends CliError {
|
|
55
|
+
constructor(message, options = {}) {
|
|
56
|
+
super(message, { ...options, exitCode: EXIT.PRECHECK_FAILED });
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
var AuthError = class extends CliError {
|
|
60
|
+
constructor(message, options = {}) {
|
|
61
|
+
super(message, { ...options, exitCode: EXIT.AUTH });
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
var NetworkError = class extends CliError {
|
|
65
|
+
constructor(message, options = {}) {
|
|
66
|
+
super(message, { ...options, exitCode: EXIT.NETWORK });
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
var ApiError = class extends CliError {
|
|
70
|
+
status;
|
|
71
|
+
path;
|
|
72
|
+
constructor(message, options) {
|
|
73
|
+
const { status, path, ...rest } = options;
|
|
74
|
+
super(message, {
|
|
75
|
+
...rest,
|
|
76
|
+
exitCode: status === 401 || status === 403 ? EXIT.AUTH : EXIT.FAILURE,
|
|
77
|
+
details: { ...rest.details, status, path }
|
|
78
|
+
});
|
|
79
|
+
this.status = status;
|
|
80
|
+
this.path = path;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var CanceledError = class extends CliError {
|
|
84
|
+
constructor(message = "\u5DF2\u53D6\u6D88") {
|
|
85
|
+
super(message, { exitCode: EXIT.FAILURE });
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
var isAbortError = (error) => error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
89
|
+
var toCliError = (error) => {
|
|
90
|
+
if (error instanceof CliError) return error;
|
|
91
|
+
if (isAbortError(error)) {
|
|
92
|
+
return new CliError("\u8BF7\u6C42\u5DF2\u53D6\u6D88\u6216\u8D85\u65F6", {
|
|
93
|
+
cause: error,
|
|
94
|
+
hint: "\u7F51\u7EDC\u8F83\u6162\u65F6\u53EF\u8C03\u5927 QUANTA_HTTP_TIMEOUT\uFF0C\u6216\u68C0\u67E5 API \u5730\u5740\u662F\u5426\u53EF\u8FBE\u3002"
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (error instanceof Error) {
|
|
98
|
+
return new CliError(error.message, { cause: error });
|
|
99
|
+
}
|
|
100
|
+
return new CliError(String(error));
|
|
101
|
+
};
|
|
102
|
+
var pathExists = async (path) => {
|
|
103
|
+
try {
|
|
104
|
+
await access(path, constants.F_OK);
|
|
105
|
+
return true;
|
|
106
|
+
} catch {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
var ensureDir = async (dir) => {
|
|
111
|
+
await mkdir(dir, { recursive: true });
|
|
112
|
+
};
|
|
113
|
+
var readText = async (path) => {
|
|
114
|
+
try {
|
|
115
|
+
return await readFile(path, "utf8");
|
|
116
|
+
} catch (error) {
|
|
117
|
+
throw new CliError(`\u65E0\u6CD5\u8BFB\u53D6\u6587\u4EF6\uFF1A${path}`, {
|
|
118
|
+
cause: error,
|
|
119
|
+
details: { path }
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
var writeText = async (path, content) => {
|
|
124
|
+
await ensureDir(dirname(path));
|
|
125
|
+
await writeFile(path, content, "utf8");
|
|
126
|
+
};
|
|
127
|
+
var writeTextAtomic = async (path, content) => {
|
|
128
|
+
const dir = dirname(path);
|
|
129
|
+
await ensureDir(dir);
|
|
130
|
+
const temp = join(dir, `.${basename(path)}.${process.pid}.tmp`);
|
|
131
|
+
const mode = await stat(path).then((info) => info.mode).catch(() => void 0);
|
|
132
|
+
try {
|
|
133
|
+
await writeFile(temp, content, "utf8");
|
|
134
|
+
if (mode !== void 0) await chmod(temp, mode);
|
|
135
|
+
await rename(temp, path);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
await rm(temp, { force: true }).catch(() => {
|
|
138
|
+
});
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
var writeTextWithMode = async (path, content, mode) => {
|
|
143
|
+
await writeText(path, content);
|
|
144
|
+
if (mode !== void 0) await chmod(path, mode);
|
|
145
|
+
};
|
|
146
|
+
var writeBinary = async (path, content) => {
|
|
147
|
+
await ensureDir(dirname(path));
|
|
148
|
+
await writeFile(path, content);
|
|
149
|
+
};
|
|
150
|
+
var listDir = async (dir) => {
|
|
151
|
+
try {
|
|
152
|
+
return await readdir(dir);
|
|
153
|
+
} catch {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
var walkFiles = async (root, options = {}) => {
|
|
158
|
+
const results = [];
|
|
159
|
+
const walk = async (dir, prefix) => {
|
|
160
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(
|
|
161
|
+
() => []
|
|
162
|
+
);
|
|
163
|
+
for (const entry of entries.sort(
|
|
164
|
+
(a, b) => a.name.localeCompare(b.name)
|
|
165
|
+
)) {
|
|
166
|
+
if (entry.isSymbolicLink()) continue;
|
|
167
|
+
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
168
|
+
if (options.ignore?.(entry.name, relativePath)) continue;
|
|
169
|
+
const absolutePath = join(dir, entry.name);
|
|
170
|
+
if (entry.isDirectory()) {
|
|
171
|
+
await walk(absolutePath, relativePath);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (entry.isFile()) results.push(relativePath);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
await walk(root, "");
|
|
178
|
+
return results.sort();
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// ../shared/service/backend-service-image.ts
|
|
182
|
+
var PNPM_VERSION = "9.15.9";
|
|
183
|
+
var BACKEND_SERVICE_IMAGES = {
|
|
184
|
+
"node-24": {
|
|
185
|
+
id: "node-24",
|
|
186
|
+
label: "Node.js 24",
|
|
187
|
+
image: "challenge-backend-service-agent",
|
|
188
|
+
editorBootCommand: (mountPath) => [
|
|
189
|
+
`[install-pnpm] npm install --global pnpm@${PNPM_VERSION}`,
|
|
190
|
+
`[install] pnpm --dir ./${mountPath} install`
|
|
191
|
+
].join("\n"),
|
|
192
|
+
// 入口由题目的 package.json 决定;npm 在没有 scripts.start 时回退到 node server.js。
|
|
193
|
+
initCommand: (mountPath) => `[dev-server] cd ./${mountPath} && npm start`,
|
|
194
|
+
defaultPort: 3e3
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
var BACKEND_SERVICE_IMAGE_IDS = Object.keys(
|
|
198
|
+
BACKEND_SERVICE_IMAGES
|
|
199
|
+
);
|
|
200
|
+
var DEFAULT_BACKEND_SERVICE_IMAGE_ID = "node-24";
|
|
201
|
+
var isBackendServiceImageId = (value) => typeof value === "string" && Object.prototype.hasOwnProperty.call(BACKEND_SERVICE_IMAGES, value);
|
|
202
|
+
var normalizeBackendServiceImageId = (id) => isBackendServiceImageId(id) ? id : DEFAULT_BACKEND_SERVICE_IMAGE_ID;
|
|
203
|
+
var getBackendServiceImage = (id) => BACKEND_SERVICE_IMAGES[normalizeBackendServiceImageId(id)];
|
|
204
|
+
var buildBackendBootCommand = (id, mountPath) => getBackendServiceImage(id).editorBootCommand(mountPath);
|
|
205
|
+
var buildBackendInitCommand = (id, mountPath) => getBackendServiceImage(id).initCommand(mountPath);
|
|
206
|
+
var getBackendServiceDefaultPort = (id) => getBackendServiceImage(id).defaultPort;
|
|
207
|
+
Object.values(
|
|
208
|
+
BACKEND_SERVICE_IMAGES
|
|
209
|
+
).map((item) => item.image);
|
|
210
|
+
|
|
211
|
+
// src/domain/cover.ts
|
|
212
|
+
var COVER_DEFAULTS = {
|
|
213
|
+
bg: "#0F172A",
|
|
214
|
+
accent: "#A6FB1D",
|
|
215
|
+
text: "#F8FAFC",
|
|
216
|
+
footer: "Quanta Challenge",
|
|
217
|
+
width: 1200,
|
|
218
|
+
height: 675
|
|
219
|
+
};
|
|
220
|
+
var COVER_SUGGESTED_BG_TO = "#14432A";
|
|
221
|
+
var HEX_COLOR = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
222
|
+
var isHexColor = (value) => HEX_COLOR.test(value);
|
|
223
|
+
var assertHexColor = (value, field) => {
|
|
224
|
+
if (!isHexColor(value)) {
|
|
225
|
+
throw new UsageError(
|
|
226
|
+
`${field} \u5FC5\u987B\u662F\u5341\u516D\u8FDB\u5236\u989C\u8272\uFF08#rgb / #rrggbb\uFF09\uFF0C\u5F53\u524D\u4E3A "${value}"`,
|
|
227
|
+
{ hint: '\u4F8B\u5982 --bg "#0F172A" --accent "#A6FB1D"\u3002' }
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
return value;
|
|
231
|
+
};
|
|
232
|
+
var XML_ESCAPES = {
|
|
233
|
+
"&": "&",
|
|
234
|
+
"<": "<",
|
|
235
|
+
">": ">",
|
|
236
|
+
'"': """,
|
|
237
|
+
"'": "'"
|
|
238
|
+
};
|
|
239
|
+
var escapeXml = (value) => value.replace(/[&<>"']/g, (char) => XML_ESCAPES[char] ?? char);
|
|
240
|
+
var isWideChar = (code) => code >= 4352 && code <= 4447 || code >= 11904 && code <= 42191 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65072 && code <= 65135 || code >= 65280 && code <= 65376 || code >= 65504 && code <= 65510;
|
|
241
|
+
var charWidthEm = (char, bold) => {
|
|
242
|
+
const code = char.codePointAt(0) ?? 0;
|
|
243
|
+
if (isWideChar(code)) return 1;
|
|
244
|
+
if (char === " ") return 0.28;
|
|
245
|
+
if ("ilj.,:;'|!".includes(char)) return 0.3;
|
|
246
|
+
if (/[A-Z]/.test(char)) return bold ? 0.68 : 0.66;
|
|
247
|
+
if (/[0-9]/.test(char)) return 0.58;
|
|
248
|
+
return bold ? 0.6 : 0.56;
|
|
249
|
+
};
|
|
250
|
+
var textWidthEm = (text, bold) => [...text].reduce((sum, char) => sum + charWidthEm(char, bold), 0);
|
|
251
|
+
var isBreakAfter = (char) => char === " " || isWideChar(char.codePointAt(0) ?? 0);
|
|
252
|
+
var wrapByWidth = (text, maxWidthEm, bold = true) => {
|
|
253
|
+
const lines = [];
|
|
254
|
+
let current = "";
|
|
255
|
+
let currentEm = 0;
|
|
256
|
+
let breakAt = -1;
|
|
257
|
+
let breakEm = 0;
|
|
258
|
+
const flush = (at, em) => {
|
|
259
|
+
const line = (at >= 0 ? current.slice(0, at) : current).trim();
|
|
260
|
+
if (line) lines.push(line);
|
|
261
|
+
current = at >= 0 ? current.slice(at).trimStart() : "";
|
|
262
|
+
currentEm = at >= 0 ? currentEm - em : 0;
|
|
263
|
+
breakAt = -1;
|
|
264
|
+
breakEm = 0;
|
|
265
|
+
};
|
|
266
|
+
for (const char of text) {
|
|
267
|
+
const width = charWidthEm(char, bold);
|
|
268
|
+
if (currentEm + width > maxWidthEm && current) {
|
|
269
|
+
if (breakAt > 0 && breakEm > maxWidthEm * 0.4) {
|
|
270
|
+
flush(breakAt, breakEm);
|
|
271
|
+
} else {
|
|
272
|
+
flush(-1, currentEm);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
current += char;
|
|
276
|
+
currentEm += width;
|
|
277
|
+
if (isBreakAfter(char)) {
|
|
278
|
+
breakAt = current.length;
|
|
279
|
+
breakEm = currentEm;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (current.trim()) lines.push(current.trim());
|
|
283
|
+
return lines.length > 0 ? lines : [""];
|
|
284
|
+
};
|
|
285
|
+
var layoutCover = (spec) => {
|
|
286
|
+
const scale = spec.width / COVER_DEFAULTS.width;
|
|
287
|
+
const pad = 72 * scale;
|
|
288
|
+
const barWidth = 6 * scale;
|
|
289
|
+
const textX = pad + barWidth + 26 * scale;
|
|
290
|
+
const contentWidth = spec.width - textX - pad;
|
|
291
|
+
const baseFontSize = 58 * scale;
|
|
292
|
+
const minFontSize = Math.max(20, baseFontSize * 0.5);
|
|
293
|
+
const maxLines = 3;
|
|
294
|
+
let fontSize = baseFontSize;
|
|
295
|
+
let lines = wrapByWidth(spec.title, contentWidth / fontSize, true);
|
|
296
|
+
for (let size = baseFontSize; size >= minFontSize; size -= 2) {
|
|
297
|
+
const candidate = wrapByWidth(spec.title, contentWidth / size, true);
|
|
298
|
+
const fits = candidate.every(
|
|
299
|
+
(line) => textWidthEm(line, true) * size <= contentWidth + 0.5
|
|
300
|
+
);
|
|
301
|
+
if (fits && candidate.length <= maxLines) {
|
|
302
|
+
fontSize = size;
|
|
303
|
+
lines = candidate;
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
fontSize = size;
|
|
307
|
+
lines = candidate;
|
|
308
|
+
}
|
|
309
|
+
const lineHeight = fontSize * 1.22;
|
|
310
|
+
const blockTop = pad + 24 * scale;
|
|
311
|
+
const textBaseline = blockTop + fontSize;
|
|
312
|
+
const barHeight = lineHeight * (lines.length - 1) + fontSize * 1.02;
|
|
313
|
+
const subtitleFontSize = 26 * scale;
|
|
314
|
+
const subtitleBaseline = blockTop + lineHeight * (lines.length - 1) + fontSize * 0.62 + 72 * scale;
|
|
315
|
+
return {
|
|
316
|
+
lines,
|
|
317
|
+
fontSize,
|
|
318
|
+
lineHeight,
|
|
319
|
+
barX: pad,
|
|
320
|
+
barY: blockTop + fontSize * 0.06,
|
|
321
|
+
barWidth,
|
|
322
|
+
barHeight,
|
|
323
|
+
textX,
|
|
324
|
+
firstBaseline: textBaseline,
|
|
325
|
+
subtitleBaseline,
|
|
326
|
+
subtitleFontSize,
|
|
327
|
+
footerBaseline: spec.height - 60 * scale,
|
|
328
|
+
footerFontSize: 24 * scale
|
|
329
|
+
};
|
|
330
|
+
};
|
|
331
|
+
var fontStack = (mono) => mono ? "'Menlo','Monaco','Courier New',monospace" : "'PingFang SC','Hiragino Sans GB','Microsoft YaHei','Noto Sans CJK SC','Source Han Sans SC',sans-serif";
|
|
332
|
+
var buildCoverSvg = (spec) => {
|
|
333
|
+
assertHexColor(spec.bg, "\u80CC\u666F\u8272");
|
|
334
|
+
assertHexColor(spec.accent, "\u5F3A\u8C03\u8272");
|
|
335
|
+
assertHexColor(spec.text, "\u6807\u9898\u989C\u8272");
|
|
336
|
+
if (spec.bgTo !== void 0) assertHexColor(spec.bgTo, "\u6E10\u53D8\u7EC8\u6B62\u8272");
|
|
337
|
+
const layout = layoutCover(spec);
|
|
338
|
+
const background = spec.bgTo === void 0 ? `<rect width="${spec.width}" height="${spec.height}" fill="${spec.bg}"/>` : `<defs><linearGradient id="bg" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="${spec.bg}"/><stop offset="100%" stop-color="${spec.bgTo}"/></linearGradient></defs><rect width="${spec.width}" height="${spec.height}" fill="url(#bg)"/>`;
|
|
339
|
+
const titleLines = layout.lines.map(
|
|
340
|
+
(line, index) => `<text x="${layout.textX}" y="${round(layout.firstBaseline + index * layout.lineHeight)}" font-family="${fontStack(false)}" font-size="${round(layout.fontSize)}" font-weight="700" fill="${spec.text}">${escapeXml(line)}</text>`
|
|
341
|
+
).join("\n");
|
|
342
|
+
const subtitle = spec.subtitle ? `<text x="${layout.textX}" y="${round(layout.subtitleBaseline)}" font-family="${fontStack(true)}" font-size="${round(layout.subtitleFontSize)}" font-weight="700" fill="${spec.accent}">${escapeXml(spec.subtitle)}</text>` : "";
|
|
343
|
+
const footer = `<text x="${layout.barX}" y="${round(layout.footerBaseline)}" font-family="${fontStack(false)}" font-size="${round(layout.footerFontSize)}" fill="#64748B">${escapeXml(spec.footer ?? COVER_DEFAULTS.footer)}</text>`;
|
|
344
|
+
return [
|
|
345
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${spec.width}" height="${spec.height}" viewBox="0 0 ${spec.width} ${spec.height}">`,
|
|
346
|
+
background,
|
|
347
|
+
`<rect x="${layout.barX}" y="${round(layout.barY)}" width="${layout.barWidth}" height="${round(layout.barHeight)}" rx="${layout.barWidth / 2}" fill="${spec.accent}"/>`,
|
|
348
|
+
titleLines,
|
|
349
|
+
subtitle,
|
|
350
|
+
footer,
|
|
351
|
+
"</svg>"
|
|
352
|
+
].filter(Boolean).join("\n");
|
|
353
|
+
};
|
|
354
|
+
var round = (value) => Math.round(value * 100) / 100;
|
|
355
|
+
|
|
356
|
+
// src/domain/problem-config-schema.ts
|
|
357
|
+
var DIFFICULTIES = ["easy", "medium", "hard", "very_hard"];
|
|
358
|
+
var DEFAULT_CONFIG_FILES = [
|
|
359
|
+
"problem.config.ts",
|
|
360
|
+
"problem.config.mts",
|
|
361
|
+
"problem.config.js",
|
|
362
|
+
"problem.config.mjs",
|
|
363
|
+
"problem.config.json"
|
|
364
|
+
];
|
|
365
|
+
var DEFAULT_JUDGE_UPLOAD_PATH = "project";
|
|
366
|
+
var DEFAULT_INIT_COMMAND = "npx serve -l 3000 project";
|
|
367
|
+
var PROBLEM_TYPES = ["frontend", "backend_service"];
|
|
368
|
+
var coverGenerateSchema = z.object({
|
|
369
|
+
/** 背景色(纯色;配合 bgTo 变成对角线渐变) */
|
|
370
|
+
bg: z.string().default(COVER_DEFAULTS.bg),
|
|
371
|
+
/** 渐变终止色,留空 = 纯色背景 */
|
|
372
|
+
bgTo: z.string().optional(),
|
|
373
|
+
/** 强调色:左侧竖条 + 副标题 */
|
|
374
|
+
accent: z.string().default(COVER_DEFAULTS.accent),
|
|
375
|
+
/** 主标题颜色 */
|
|
376
|
+
text: z.string().default(COVER_DEFAULTS.text),
|
|
377
|
+
/** 副标题(建议写接口清单或一句话简介),留空则不渲染 */
|
|
378
|
+
subtitle: z.string().optional(),
|
|
379
|
+
/** 左下角页脚,默认 “Quanta Challenge” */
|
|
380
|
+
footer: z.string().optional(),
|
|
381
|
+
width: z.number().int().min(200).max(4096).default(COVER_DEFAULTS.width),
|
|
382
|
+
height: z.number().int().min(200).max(4096).default(COVER_DEFAULTS.height),
|
|
383
|
+
/** 输出文件(相对题目目录) */
|
|
384
|
+
out: z.string().default("cover.png")
|
|
385
|
+
});
|
|
386
|
+
var coverSchema = z.union([
|
|
387
|
+
z.object({ mode: z.literal("default") }),
|
|
388
|
+
z.object({
|
|
389
|
+
mode: z.literal("custom"),
|
|
390
|
+
/** 平台上的图片 id;`qpc cover --upload --write` 会把生成结果写回这里 */
|
|
391
|
+
imageId: z.string().min(1).optional(),
|
|
392
|
+
/** 配置了就能用 `qpc cover` 一键生成(后端题型必须自定义封面) */
|
|
393
|
+
generate: coverGenerateSchema.optional()
|
|
394
|
+
})
|
|
395
|
+
]);
|
|
396
|
+
var problemConfigSchema = z.object({
|
|
397
|
+
title: z.string().min(1, "title \u4E0D\u80FD\u4E3A\u7A7A"),
|
|
398
|
+
/** 平台题目 id(qpc upload 返回的 pid):填了它就在该题目下追加新版本,否则每次新建 */
|
|
399
|
+
problemId: z.number().int().positive().optional(),
|
|
400
|
+
/** 题型;缺省为前端,老题目不需要改配置 */
|
|
401
|
+
type: z.enum(PROBLEM_TYPES).default("frontend"),
|
|
402
|
+
/** 题面正文;与 detailFile 二选一 */
|
|
403
|
+
detail: z.string().min(1).optional(),
|
|
404
|
+
/** 题面文件(相对题目目录),便于把长题面单独放一个 markdown */
|
|
405
|
+
detailFile: z.string().min(1).optional(),
|
|
406
|
+
difficulty: z.enum(DIFFICULTIES),
|
|
407
|
+
tagIds: z.array(z.number().int().positive()).min(1, "tagIds \u81F3\u5C11\u9700\u8981\u4E00\u4E2A\u6807\u7B7E\uFF08\u7528 `qpc tag` \u67E5\u770B\u53EF\u7528 id\uFF09"),
|
|
408
|
+
/** 留空表示"以判题脚本里各检查点之和为准";填写则必须与之和相等 */
|
|
409
|
+
totalScore: z.number().int().positive().optional(),
|
|
410
|
+
cover: coverSchema.default({ mode: "default" }),
|
|
411
|
+
runtime: z.object({
|
|
412
|
+
/** 判题时打包上传的目录,同时决定快照挂载路径(快照键为 `/<judgeUploadPath>/...`) */
|
|
413
|
+
judgeUploadPath: z.string().min(1).default(DEFAULT_JUDGE_UPLOAD_PATH),
|
|
414
|
+
initCommand: z.string().min(1).default(DEFAULT_INIT_COMMAND),
|
|
415
|
+
buildCommand: z.string().min(1).optional(),
|
|
416
|
+
bootCommand: z.string().min(1).optional(),
|
|
417
|
+
/** 后端服务:初始镜像 id(不是 docker 镜像名,取值见 @challenge/shared/backend-service-image) */
|
|
418
|
+
serviceImage: z.enum(BACKEND_SERVICE_IMAGE_IDS).optional(),
|
|
419
|
+
/** 后端服务:服务监听端口(也是判题机请求的端口) */
|
|
420
|
+
servicePort: z.number().int().positive().max(65535).default(3e3)
|
|
421
|
+
}).default({
|
|
422
|
+
judgeUploadPath: DEFAULT_JUDGE_UPLOAD_PATH,
|
|
423
|
+
initCommand: DEFAULT_INIT_COMMAND,
|
|
424
|
+
servicePort: 3e3
|
|
425
|
+
}),
|
|
426
|
+
paths: z.object({
|
|
427
|
+
template: z.string().min(1).default("template"),
|
|
428
|
+
answer: z.string().min(1).default("answer"),
|
|
429
|
+
judge: z.string().min(1).default("judge.js")
|
|
430
|
+
}).default({ template: "template", answer: "answer", judge: "judge.js" })
|
|
431
|
+
});
|
|
432
|
+
var applyBackendServiceImageDefaults = (config) => {
|
|
433
|
+
if (config.type !== "backend_service") {
|
|
434
|
+
return { config, overridden: [] };
|
|
435
|
+
}
|
|
436
|
+
const mountPath = config.runtime.judgeUploadPath || DEFAULT_JUDGE_UPLOAD_PATH;
|
|
437
|
+
const serviceImage = normalizeBackendServiceImageId(
|
|
438
|
+
config.runtime.serviceImage
|
|
439
|
+
);
|
|
440
|
+
const bootCommand = buildBackendBootCommand(serviceImage, mountPath);
|
|
441
|
+
const defaultInitCommand = buildBackendInitCommand(serviceImage, mountPath);
|
|
442
|
+
const overridden = [];
|
|
443
|
+
if (config.runtime.bootCommand && config.runtime.bootCommand !== bootCommand) {
|
|
444
|
+
overridden.push("runtime.bootCommand");
|
|
445
|
+
}
|
|
446
|
+
const initCommand = !config.runtime.initCommand || config.runtime.initCommand === DEFAULT_INIT_COMMAND ? defaultInitCommand : config.runtime.initCommand;
|
|
447
|
+
return {
|
|
448
|
+
config: {
|
|
449
|
+
...config,
|
|
450
|
+
runtime: {
|
|
451
|
+
...config.runtime,
|
|
452
|
+
serviceImage,
|
|
453
|
+
servicePort: config.runtime.servicePort ?? getBackendServiceDefaultPort(serviceImage),
|
|
454
|
+
bootCommand,
|
|
455
|
+
initCommand
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
overridden
|
|
459
|
+
};
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
// src/domain/problem-config.ts
|
|
463
|
+
var loadProblemConfig = async (rootDir, options = {}) => {
|
|
464
|
+
const configFile = await findConfigFile(rootDir, options.file);
|
|
465
|
+
const module = await importConfig(configFile);
|
|
466
|
+
const parsed = problemConfigSchema.safeParse(module);
|
|
467
|
+
if (!parsed.success) {
|
|
468
|
+
const issues = parsed.error.issues.map(
|
|
469
|
+
(issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`
|
|
470
|
+
);
|
|
471
|
+
throw new ConfigError(
|
|
472
|
+
`\u9898\u76EE\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A${configFile}
|
|
473
|
+
- ${issues.join("\n - ")}`,
|
|
474
|
+
{
|
|
475
|
+
details: { issues },
|
|
476
|
+
hint: "\u5BF9\u7167 examples/hello-total/problem.config.ts \u7684\u5B57\u6BB5\u8BF4\u660E\u4FEE\u6B63\u3002"
|
|
477
|
+
}
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
const parsedConfig = applyBackendServiceImageDefaults(parsed.data);
|
|
481
|
+
const config = parsedConfig.config;
|
|
482
|
+
const detail = await resolveDetail(config, rootDir, configFile);
|
|
483
|
+
return {
|
|
484
|
+
...config,
|
|
485
|
+
detail,
|
|
486
|
+
rootDir,
|
|
487
|
+
configFile,
|
|
488
|
+
overriddenByImage: parsedConfig.overridden,
|
|
489
|
+
paths: {
|
|
490
|
+
template: resolve(rootDir, config.paths.template),
|
|
491
|
+
answer: resolve(rootDir, config.paths.answer),
|
|
492
|
+
judge: resolve(rootDir, config.paths.judge)
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
};
|
|
496
|
+
var findConfigFile = async (rootDir, explicit) => {
|
|
497
|
+
if (explicit) {
|
|
498
|
+
const candidate = isAbsolute(explicit) ? explicit : resolve(rootDir, explicit);
|
|
499
|
+
if (!await pathExists(candidate)) {
|
|
500
|
+
throw new ConfigError(`\u6307\u5B9A\u7684\u914D\u7F6E\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${candidate}`);
|
|
501
|
+
}
|
|
502
|
+
return candidate;
|
|
503
|
+
}
|
|
504
|
+
for (const name of DEFAULT_CONFIG_FILES) {
|
|
505
|
+
const candidate = resolve(rootDir, name);
|
|
506
|
+
if (await pathExists(candidate)) return candidate;
|
|
507
|
+
}
|
|
508
|
+
throw new ConfigError(
|
|
509
|
+
`\u5728 ${rootDir} \u4E0B\u627E\u4E0D\u5230\u9898\u76EE\u914D\u7F6E\uFF08\u671F\u671B ${DEFAULT_CONFIG_FILES.join(" / ")} \u4E4B\u4E00\uFF09`,
|
|
510
|
+
{
|
|
511
|
+
hint: `\u521B\u5EFA ${DEFAULT_CONFIG_FILES[0]}\uFF0C\u6216\u5148\u8FD0\u884C \`qpc init <\u76EE\u5F55>\`\u3002`
|
|
512
|
+
}
|
|
513
|
+
);
|
|
514
|
+
};
|
|
515
|
+
var importConfig = async (file) => {
|
|
516
|
+
const { createJiti } = await import('jiti');
|
|
517
|
+
const jiti = createJiti(import.meta.url, {
|
|
518
|
+
interopDefault: true,
|
|
519
|
+
moduleCache: false,
|
|
520
|
+
alias: {
|
|
521
|
+
// 让配置文件里的 `import { defineProblemConfig } from '@challenge/problem-creator-cli'`
|
|
522
|
+
// 无论 CLI 是从源码跑(tsx)还是从 dist 跑都能解析到同一个模块。
|
|
523
|
+
"@challenge/problem-creator-cli": programModulePath()
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
try {
|
|
527
|
+
return await jiti.import(file);
|
|
528
|
+
} catch (error) {
|
|
529
|
+
throw new ConfigError(`\u65E0\u6CD5\u52A0\u8F7D\u914D\u7F6E\u6587\u4EF6\uFF1A${file}`, {
|
|
530
|
+
cause: error,
|
|
531
|
+
details: {
|
|
532
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
var programModulePath = () => {
|
|
538
|
+
const candidates = [
|
|
539
|
+
"./program.js",
|
|
540
|
+
"../program.js",
|
|
541
|
+
"../program.ts",
|
|
542
|
+
"./program.ts"
|
|
543
|
+
];
|
|
544
|
+
for (const candidate of candidates) {
|
|
545
|
+
const path = fileURLToPath(new URL(candidate, import.meta.url));
|
|
546
|
+
if (existsSync(path)) return path;
|
|
547
|
+
}
|
|
548
|
+
throw new ConfigError(
|
|
549
|
+
"\u5185\u90E8\u9519\u8BEF\uFF1A\u627E\u4E0D\u5230 program \u6A21\u5757\uFF08\u65E0\u6CD5\u89E3\u6790 defineProblemConfig\uFF09",
|
|
550
|
+
{
|
|
551
|
+
details: { from: import.meta.url, candidates }
|
|
552
|
+
}
|
|
553
|
+
);
|
|
554
|
+
};
|
|
555
|
+
var resolveDetail = async (config, rootDir, configFile) => {
|
|
556
|
+
if (config.detail) return config.detail;
|
|
557
|
+
if (!config.detailFile) {
|
|
558
|
+
throw new ConfigError(
|
|
559
|
+
`\u9898\u76EE\u914D\u7F6E\u7F3A\u5C11\u9898\u9762\uFF1A${configFile} \u91CC\u9700\u8981 detail \u6216 detailFile`,
|
|
560
|
+
{
|
|
561
|
+
hint: "\u63D0\u4F9B `detail`\uFF08\u6B63\u6587\uFF09\u6216 `detailFile`\uFF08\u76F8\u5BF9\u9898\u76EE\u76EE\u5F55\u7684\u6587\u4EF6\u8DEF\u5F84\uFF09\u3002"
|
|
562
|
+
}
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
const file = resolve(rootDir, config.detailFile);
|
|
566
|
+
if (!await pathExists(file)) {
|
|
567
|
+
throw new ConfigError(`detailFile \u6307\u5411\u7684\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${file}`);
|
|
568
|
+
}
|
|
569
|
+
return readFile(file, "utf8");
|
|
570
|
+
};
|
|
571
|
+
var toPosix = (path) => path.split(sep).join("/");
|
|
572
|
+
var displayPath = (cwd, target) => {
|
|
573
|
+
const rel = relative(cwd, target);
|
|
574
|
+
return rel.startsWith("..") ? target : toPosix(rel);
|
|
575
|
+
};
|
|
576
|
+
var normalizeSnapshotKey = (key) => {
|
|
577
|
+
const segments = toPosix(key).split("/").filter((s) => s && s !== ".");
|
|
578
|
+
return `/${segments.join("/")}`;
|
|
579
|
+
};
|
|
580
|
+
var joinSnapshotKey = (mountPath, relativePath) => normalizeSnapshotKey(`${mountPath}/${relativePath}`);
|
|
581
|
+
var formatBytes = (bytes) => {
|
|
582
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
583
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
584
|
+
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// src/domain/judge-script.ts
|
|
588
|
+
var IDENT = /[A-Za-z0-9_$]/;
|
|
589
|
+
var isRegexStart = (code, index) => {
|
|
590
|
+
let i = index - 1;
|
|
591
|
+
while (i >= 0 && /\s/.test(code[i])) i -= 1;
|
|
592
|
+
if (i < 0) return true;
|
|
593
|
+
return /[([{,;:=!&|?+\-*%~^<>]/.test(code[i]);
|
|
594
|
+
};
|
|
595
|
+
var skipQuoted = (code, start, quote) => {
|
|
596
|
+
let i = start + 1;
|
|
597
|
+
while (i < code.length) {
|
|
598
|
+
const ch = code[i];
|
|
599
|
+
if (ch === "\\") {
|
|
600
|
+
i += 2;
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (ch === quote) return i + 1;
|
|
604
|
+
if (ch === "\n") return i;
|
|
605
|
+
i += 1;
|
|
606
|
+
}
|
|
607
|
+
return code.length;
|
|
608
|
+
};
|
|
609
|
+
var skipTemplate = (code, start) => {
|
|
610
|
+
let i = start + 1;
|
|
611
|
+
while (i < code.length) {
|
|
612
|
+
const ch = code[i];
|
|
613
|
+
if (ch === "\\") {
|
|
614
|
+
i += 2;
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
if (ch === "`") return i + 1;
|
|
618
|
+
if (ch === "$" && code[i + 1] === "{") {
|
|
619
|
+
i = skipBalanced(code, i + 1, "{", "}");
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
i += 1;
|
|
623
|
+
}
|
|
624
|
+
return code.length;
|
|
625
|
+
};
|
|
626
|
+
var skipRegex = (code, start) => {
|
|
627
|
+
let i = start + 1;
|
|
628
|
+
let inClass = false;
|
|
629
|
+
while (i < code.length) {
|
|
630
|
+
const ch = code[i];
|
|
631
|
+
if (ch === "\\") {
|
|
632
|
+
i += 2;
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
if (ch === "[") inClass = true;
|
|
636
|
+
else if (ch === "]") inClass = false;
|
|
637
|
+
else if (ch === "/" && !inClass) return i + 1;
|
|
638
|
+
else if (ch === "\n") return i;
|
|
639
|
+
i += 1;
|
|
640
|
+
}
|
|
641
|
+
return code.length;
|
|
642
|
+
};
|
|
643
|
+
var skipBalanced = (code, openIndex, open, close) => {
|
|
644
|
+
let depth = 0;
|
|
645
|
+
let i = openIndex;
|
|
646
|
+
while (i < code.length) {
|
|
647
|
+
const ch = code[i];
|
|
648
|
+
if (ch === "/" && code[i + 1] === "/") {
|
|
649
|
+
const end = code.indexOf("\n", i);
|
|
650
|
+
i = end === -1 ? code.length : end;
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
if (ch === "/" && code[i + 1] === "*") {
|
|
654
|
+
const end = code.indexOf("*/", i + 2);
|
|
655
|
+
i = end === -1 ? code.length : end + 2;
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (ch === '"' || ch === "'") {
|
|
659
|
+
i = skipQuoted(code, i, ch);
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if (ch === "`") {
|
|
663
|
+
i = skipTemplate(code, i);
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
if (ch === "/" && isRegexStart(code, i)) {
|
|
667
|
+
i = skipRegex(code, i);
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
if (ch === open) depth += 1;
|
|
671
|
+
else if (ch === close) {
|
|
672
|
+
depth -= 1;
|
|
673
|
+
if (depth === 0) return i + 1;
|
|
674
|
+
}
|
|
675
|
+
i += 1;
|
|
676
|
+
}
|
|
677
|
+
return code.length;
|
|
678
|
+
};
|
|
679
|
+
var maskNonCode = (code) => {
|
|
680
|
+
const chars = code.split("");
|
|
681
|
+
const blank = (from, to) => {
|
|
682
|
+
for (let i2 = from; i2 < to; i2 += 1) {
|
|
683
|
+
if (chars[i2] !== "\n") chars[i2] = " ";
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
let i = 0;
|
|
687
|
+
while (i < code.length) {
|
|
688
|
+
const ch = code[i];
|
|
689
|
+
const next = code[i + 1];
|
|
690
|
+
if (ch === "/" && next === "/") {
|
|
691
|
+
const end = code.indexOf("\n", i);
|
|
692
|
+
const to = end === -1 ? code.length : end;
|
|
693
|
+
blank(i, to);
|
|
694
|
+
i = to;
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
if (ch === "/" && next === "*") {
|
|
698
|
+
const end = code.indexOf("*/", i + 2);
|
|
699
|
+
const to = end === -1 ? code.length : end + 2;
|
|
700
|
+
blank(i, to);
|
|
701
|
+
i = to;
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
if (ch === '"' || ch === "'") {
|
|
705
|
+
const to = skipQuoted(code, i, ch);
|
|
706
|
+
blank(i, to);
|
|
707
|
+
i = to;
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
if (ch === "`") {
|
|
711
|
+
const to = skipTemplate(code, i);
|
|
712
|
+
blank(i, to);
|
|
713
|
+
i = to;
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
if (ch === "/" && isRegexStart(code, i)) {
|
|
717
|
+
const to = skipRegex(code, i);
|
|
718
|
+
blank(i, to);
|
|
719
|
+
i = to;
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
i += 1;
|
|
723
|
+
}
|
|
724
|
+
return chars.join("");
|
|
725
|
+
};
|
|
726
|
+
var splitArgs = (code, mask, from, to) => {
|
|
727
|
+
const args = [];
|
|
728
|
+
let depth = 0;
|
|
729
|
+
let start = from;
|
|
730
|
+
for (let i = from; i < to; i += 1) {
|
|
731
|
+
const ch = mask[i];
|
|
732
|
+
if (ch === "(" || ch === "[" || ch === "{") depth += 1;
|
|
733
|
+
else if (ch === ")" || ch === "]" || ch === "}") depth -= 1;
|
|
734
|
+
else if (ch === "," && depth === 0) {
|
|
735
|
+
args.push(code.slice(start, i));
|
|
736
|
+
start = i + 1;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (to > from) args.push(code.slice(start, to));
|
|
740
|
+
return args.map((arg) => arg.trim()).filter((arg) => arg.length > 0);
|
|
741
|
+
};
|
|
742
|
+
var findCalls = (code, mask, callee) => {
|
|
743
|
+
const sites = [];
|
|
744
|
+
let i = 0;
|
|
745
|
+
while (i < mask.length) {
|
|
746
|
+
const index = mask.indexOf(callee, i);
|
|
747
|
+
if (index === -1) break;
|
|
748
|
+
const before = index === 0 ? "" : mask[index - 1];
|
|
749
|
+
const afterIndex = index + callee.length;
|
|
750
|
+
const after = afterIndex >= mask.length ? "" : mask[afterIndex];
|
|
751
|
+
if (IDENT.test(callee[0]) && before && IDENT.test(before)) {
|
|
752
|
+
i = index + 1;
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
if (after && IDENT.test(after)) {
|
|
756
|
+
i = index + 1;
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
let cursor = afterIndex;
|
|
760
|
+
while (cursor < mask.length && /\s/.test(mask[cursor]))
|
|
761
|
+
cursor += 1;
|
|
762
|
+
if (mask[cursor] !== "(") {
|
|
763
|
+
i = index + 1;
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
const end = skipBalanced(mask, cursor, "(", ")");
|
|
767
|
+
sites.push({
|
|
768
|
+
start: index,
|
|
769
|
+
end,
|
|
770
|
+
args: splitArgs(code, mask, cursor + 1, end - 1)
|
|
771
|
+
});
|
|
772
|
+
i = end;
|
|
773
|
+
}
|
|
774
|
+
return sites;
|
|
775
|
+
};
|
|
776
|
+
var lineOf = (code, index) => {
|
|
777
|
+
let line = 1;
|
|
778
|
+
for (let i = 0; i < index && i < code.length; i += 1) {
|
|
779
|
+
if (code[i] === "\n") line += 1;
|
|
780
|
+
}
|
|
781
|
+
return line;
|
|
782
|
+
};
|
|
783
|
+
var parseStringLiteral = (text) => {
|
|
784
|
+
const trimmed = text.trim();
|
|
785
|
+
const quote = trimmed[0];
|
|
786
|
+
if (quote !== '"' && quote !== "'" && quote !== "`") return void 0;
|
|
787
|
+
if (trimmed.length < 2 || trimmed[trimmed.length - 1] !== quote)
|
|
788
|
+
return void 0;
|
|
789
|
+
const body = trimmed.slice(1, -1);
|
|
790
|
+
if (quote === "`" && body.includes("${")) return void 0;
|
|
791
|
+
return body.replace(/\\(.)/g, (_, char) => {
|
|
792
|
+
const escapes = {
|
|
793
|
+
n: "\n",
|
|
794
|
+
t: " ",
|
|
795
|
+
r: "\r",
|
|
796
|
+
"\\": "\\",
|
|
797
|
+
'"': '"',
|
|
798
|
+
"'": "'",
|
|
799
|
+
"`": "`"
|
|
800
|
+
};
|
|
801
|
+
return escapes[char] ?? char;
|
|
802
|
+
});
|
|
803
|
+
};
|
|
804
|
+
var analyzeJudgeScript = (code) => {
|
|
805
|
+
const mask = maskNonCode(code);
|
|
806
|
+
const handlerCalls = findCalls(code, mask, "defineTestHandler");
|
|
807
|
+
const checkpointCalls = findCalls(code, mask, "defineCheckPoint");
|
|
808
|
+
const expectCalls = findCalls(code, mask, "expect");
|
|
809
|
+
const checkpoints = [];
|
|
810
|
+
const unparsedCheckpoints = [];
|
|
811
|
+
for (const call of checkpointCalls) {
|
|
812
|
+
const [nameArg = "", scoreArg = "", handlerArg = ""] = call.args;
|
|
813
|
+
const name = parseStringLiteral(nameArg) ?? nameArg.replace(/\s+/g, " ").trim();
|
|
814
|
+
const score = Number.parseFloat(scoreArg.trim());
|
|
815
|
+
const line = lineOf(code, call.start);
|
|
816
|
+
if (!Number.isFinite(score)) {
|
|
817
|
+
unparsedCheckpoints.push({
|
|
818
|
+
line,
|
|
819
|
+
value: scoreArg.trim() || "<missing>"
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
const returnsValue = /\breturn\b\s*[^;\s}]/.test(maskNonCode(handlerArg));
|
|
823
|
+
checkpoints.push({
|
|
824
|
+
name: name || "<unnamed>",
|
|
825
|
+
score: Number.isFinite(score) ? score : 0,
|
|
826
|
+
line,
|
|
827
|
+
returnsValue
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
const clickWithoutTimeout = [];
|
|
831
|
+
for (const call of findCalls(code, mask, ".click")) {
|
|
832
|
+
const snippet = code.slice(Math.max(0, call.start - 16), call.end).replace(/\s+/g, " ").trim();
|
|
833
|
+
if (/\btimeout\b/.test(mask.slice(call.start, call.end))) continue;
|
|
834
|
+
clickWithoutTimeout.push({
|
|
835
|
+
line: lineOf(code, call.start),
|
|
836
|
+
value: snippet
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
const vagueExpectations = [];
|
|
840
|
+
for (const call of expectCalls) {
|
|
841
|
+
const message = call.args[1];
|
|
842
|
+
if (message === void 0) continue;
|
|
843
|
+
if (/\$\{|\+|实际|expected|Received/i.test(message)) continue;
|
|
844
|
+
vagueExpectations.push({
|
|
845
|
+
line: lineOf(code, call.start),
|
|
846
|
+
value: parseStringLiteral(message) ?? message.replace(/\s+/g, " ").trim()
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
const namedExports = [];
|
|
850
|
+
const exportRe = /\bexport\s+(?!default\b)(?:const|let|var|function|class|async)\s+([A-Za-z0-9_$]+)/g;
|
|
851
|
+
for (const match of mask.matchAll(exportRe)) {
|
|
852
|
+
if (match[1]) namedExports.push(match[1]);
|
|
853
|
+
}
|
|
854
|
+
return {
|
|
855
|
+
hasDefaultExport: /\bexport\s+default\b/.test(mask),
|
|
856
|
+
hasDefineTestHandler: handlerCalls.length > 0,
|
|
857
|
+
namedExports,
|
|
858
|
+
checkpoints,
|
|
859
|
+
checkpointTotal: checkpoints.reduce((sum, cp) => sum + cp.score, 0),
|
|
860
|
+
unparsedCheckpoints,
|
|
861
|
+
clickWithoutTimeout,
|
|
862
|
+
vagueExpectations,
|
|
863
|
+
expectationCount: expectCalls.length,
|
|
864
|
+
lineCount: code.split("\n").length
|
|
865
|
+
};
|
|
866
|
+
};
|
|
867
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
868
|
+
"png",
|
|
869
|
+
"jpg",
|
|
870
|
+
"jpeg",
|
|
871
|
+
"gif",
|
|
872
|
+
"bmp",
|
|
873
|
+
"tiff",
|
|
874
|
+
"webp",
|
|
875
|
+
"ico",
|
|
876
|
+
"cur"
|
|
877
|
+
]);
|
|
878
|
+
var DEFAULT_IGNORE = /* @__PURE__ */ new Set([
|
|
879
|
+
"node_modules",
|
|
880
|
+
".git",
|
|
881
|
+
".turbo",
|
|
882
|
+
".DS_Store",
|
|
883
|
+
"dist",
|
|
884
|
+
"coverage"
|
|
885
|
+
]);
|
|
886
|
+
var collectSnapshot = async (root, options = {}) => {
|
|
887
|
+
const mountPath = options.mountPath ?? "project";
|
|
888
|
+
const ignore = (name) => DEFAULT_IGNORE.has(name) || (options.ignore?.(name) ?? false);
|
|
889
|
+
const ignored = /* @__PURE__ */ new Set();
|
|
890
|
+
const relativePaths = await walkFiles(root, {
|
|
891
|
+
ignore: (name) => {
|
|
892
|
+
if (!ignore(name)) return false;
|
|
893
|
+
ignored.add(name);
|
|
894
|
+
return true;
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
const files = {};
|
|
898
|
+
let totalBytes = 0;
|
|
899
|
+
for (const relativePath of relativePaths) {
|
|
900
|
+
const absolutePath = join(root, relativePath);
|
|
901
|
+
let buffer;
|
|
902
|
+
try {
|
|
903
|
+
buffer = await readFile(absolutePath);
|
|
904
|
+
} catch (error) {
|
|
905
|
+
throw new CliError(`\u5FEB\u7167\u8BFB\u53D6\u5931\u8D25\uFF1A${absolutePath}`, { cause: error });
|
|
906
|
+
}
|
|
907
|
+
const extension = extname(relativePath).slice(1).toLowerCase();
|
|
908
|
+
files[joinSnapshotKey(mountPath, relativePath)] = BINARY_EXTENSIONS.has(
|
|
909
|
+
extension
|
|
910
|
+
) ? buffer.toString("base64") : buffer.toString("utf8");
|
|
911
|
+
totalBytes += buffer.byteLength;
|
|
912
|
+
}
|
|
913
|
+
return {
|
|
914
|
+
files,
|
|
915
|
+
fileCount: Object.keys(files).length,
|
|
916
|
+
totalBytes,
|
|
917
|
+
ignored: [...ignored].sort()
|
|
918
|
+
};
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
// src/domain/validate.ts
|
|
922
|
+
var validateProblem = async (config, options = {}) => {
|
|
923
|
+
const findings = [];
|
|
924
|
+
const push = (finding) => {
|
|
925
|
+
findings.push(finding);
|
|
926
|
+
};
|
|
927
|
+
const rel = (path) => displayPath(config.rootDir, path);
|
|
928
|
+
const judgeExists = await pathExists(config.paths.judge);
|
|
929
|
+
if (!judgeExists) {
|
|
930
|
+
push({
|
|
931
|
+
rule: "JUD001",
|
|
932
|
+
severity: "error",
|
|
933
|
+
message: `\u5224\u9898\u811A\u672C\u4E0D\u5B58\u5728\uFF1A${rel(config.paths.judge)}`,
|
|
934
|
+
file: rel(config.paths.judge),
|
|
935
|
+
hint: "\u5728\u9898\u76EE\u76EE\u5F55\u4E0B\u521B\u5EFA judge.js\uFF0C\u53C2\u8003 examples/hello-total/judge.js\u3002"
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
const judgeCode = judgeExists ? await readText(config.paths.judge) : "";
|
|
939
|
+
const analysis = analyzeJudgeScript(judgeCode);
|
|
940
|
+
if (judgeExists && !analysis.hasDefaultExport) {
|
|
941
|
+
push({
|
|
942
|
+
rule: "JUD002",
|
|
943
|
+
severity: "error",
|
|
944
|
+
message: "\u5224\u9898\u811A\u672C\u7F3A\u5C11 `export default defineTestHandler(...)`",
|
|
945
|
+
file: rel(config.paths.judge),
|
|
946
|
+
hint: "\u5224\u9898\u673A\u628A\u6587\u672C `export default ` \u66FF\u6362\u6210 `const run = `\uFF1B\u5199\u6210\u88F8\u51FD\u6570\u58F0\u660E\u4E0D\u4F1A\u751F\u6548\u3002"
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
if (judgeExists && !analysis.hasDefineTestHandler) {
|
|
950
|
+
push({
|
|
951
|
+
rule: "JUD003",
|
|
952
|
+
severity: "error",
|
|
953
|
+
message: "\u5224\u9898\u811A\u672C\u6CA1\u6709\u8C03\u7528 defineTestHandler",
|
|
954
|
+
file: rel(config.paths.judge)
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
for (const checkpoint of analysis.checkpoints) {
|
|
958
|
+
if (!checkpoint.returnsValue) {
|
|
959
|
+
push({
|
|
960
|
+
rule: "JUD004",
|
|
961
|
+
severity: "error",
|
|
962
|
+
message: `\u68C0\u67E5\u70B9\u300C${checkpoint.name}\u300D\u7684 handler \u6CA1\u6709 return \u5206\u6570`,
|
|
963
|
+
file: rel(config.paths.judge),
|
|
964
|
+
line: checkpoint.line,
|
|
965
|
+
hint: "handler \u672B\u5C3E\u8865 `return ${checkpoint.score};`\u3002\u4E0D return \u65F6\u5373\u4F7F\u65AD\u8A00\u5168\u8FC7\u4E5F\u8BB0 0 \u5206\u3002"
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
if (checkpoint.score <= 0) {
|
|
969
|
+
push({
|
|
970
|
+
rule: "JUD005",
|
|
971
|
+
severity: "warn",
|
|
972
|
+
message: `\u68C0\u67E5\u70B9\u300C${checkpoint.name}\u300D\u5206\u503C\u4E3A ${checkpoint.score}`,
|
|
973
|
+
file: rel(config.paths.judge),
|
|
974
|
+
line: checkpoint.line
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
for (const unparsed of analysis.unparsedCheckpoints) {
|
|
979
|
+
push({
|
|
980
|
+
rule: "JUD006",
|
|
981
|
+
severity: "warn",
|
|
982
|
+
message: `\u68C0\u67E5\u70B9\u5206\u503C\u4E0D\u662F\u5B57\u9762\u91CF\uFF0C\u65E0\u6CD5\u5728\u672C\u5730\u7D2F\u52A0\uFF1A${unparsed.value}`,
|
|
983
|
+
file: rel(config.paths.judge),
|
|
984
|
+
line: unparsed.line,
|
|
985
|
+
hint: "\u8BF7\u7528\u6570\u5B57\u5B57\u9762\u91CF\u5199\u5206\u503C\uFF0C\u672C\u5730\u6821\u9A8C\u4E0E\u7EBF\u4E0A\u5BA1\u8BA1\u624D\u80FD\u5BF9\u4E0A\u3002"
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
const duplicated = duplicateNames(analysis.checkpoints.map((c) => c.name));
|
|
989
|
+
for (const name of duplicated) {
|
|
990
|
+
push({
|
|
991
|
+
rule: "JUD007",
|
|
992
|
+
severity: "warn",
|
|
993
|
+
message: `\u68C0\u67E5\u70B9\u540D\u79F0\u91CD\u590D\uFF1A${name}`,
|
|
994
|
+
file: rel(config.paths.judge),
|
|
995
|
+
hint: "\u540C\u540D\u68C0\u67E5\u70B9\u5728\u5224\u9898\u8BE6\u60C5\u91CC\u65E0\u6CD5\u533A\u5206\uFF0C\u5EFA\u8BAE\u6539\u6210\u552F\u4E00\u7684\u63CF\u8FF0\u3002"
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
for (const click of analysis.clickWithoutTimeout) {
|
|
999
|
+
push({
|
|
1000
|
+
rule: "JUD008",
|
|
1001
|
+
severity: "warn",
|
|
1002
|
+
message: `click \u672A\u8BBE\u7F6E timeout\uFF1A${click.value}`,
|
|
1003
|
+
file: rel(config.paths.judge),
|
|
1004
|
+
line: click.line,
|
|
1005
|
+
hint: "\u76EE\u6807\u53EF\u80FD\u662F disabled \u6309\u94AE\uFF0CPlaywright \u4F1A\u91CD\u8BD5\u5230 30 \u79D2\u8D85\u65F6\uFF1B\u6539\u6D3E\u53D1\u5408\u6210\u4E8B\u4EF6\u6216\u52A0 `{ timeout: 3000 }`\u3002"
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
for (const vague of analysis.vagueExpectations) {
|
|
1009
|
+
push({
|
|
1010
|
+
rule: "JUD009",
|
|
1011
|
+
severity: "info",
|
|
1012
|
+
message: `\u65AD\u8A00\u6D88\u606F\u6CA1\u6709\u8BF4\u660E"\u671F\u671B vs \u5B9E\u9645"\uFF1A${vague.value}`,
|
|
1013
|
+
file: rel(config.paths.judge),
|
|
1014
|
+
line: vague.line,
|
|
1015
|
+
hint: '\u6D88\u606F\u4F1A\u539F\u6837\u5C55\u793A\u7ED9\u5B66\u751F\uFF0C\u5EFA\u8BAE\u62FC\u63A5\u5B9E\u9645\u503C\uFF0C\u4F8B\u5982 `\u5408\u8BA1\u5E94\u4E3A 10.50\uFF0C\u5B9E\u9645\u4E3A "${actual}"`\u3002'
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
if (judgeExists && analysis.expectationCount === 0) {
|
|
1019
|
+
push({
|
|
1020
|
+
rule: "JUD010",
|
|
1021
|
+
severity: "warn",
|
|
1022
|
+
message: "\u5224\u9898\u811A\u672C\u91CC\u6CA1\u6709\u4EFB\u4F55 $.expect \u65AD\u8A00",
|
|
1023
|
+
file: rel(config.paths.judge)
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
for (const name of analysis.namedExports) {
|
|
1027
|
+
push({
|
|
1028
|
+
rule: "JUD011",
|
|
1029
|
+
severity: "warn",
|
|
1030
|
+
message: `\u5B58\u5728 default \u4E4B\u5916\u7684\u5BFC\u51FA\uFF1A${name}`,
|
|
1031
|
+
file: rel(config.paths.judge),
|
|
1032
|
+
hint: "\u6A21\u5757\u88AB\u66FF\u6362\u540E\u8FD9\u4E9B\u5BFC\u51FA\u4ECD\u4F1A\u6B8B\u7559\uFF0C\u53EF\u80FD\u5BFC\u81F4\u6C99\u7BB1\u91CC\u51FA\u73B0\u610F\u6599\u4E4B\u5916\u7684\u7ED1\u5B9A\u3002"
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
const totalScore = config.totalScore ?? analysis.checkpointTotal;
|
|
1036
|
+
if (config.totalScore !== void 0 && config.totalScore !== analysis.checkpointTotal) {
|
|
1037
|
+
push({
|
|
1038
|
+
rule: "CFG001",
|
|
1039
|
+
severity: "error",
|
|
1040
|
+
message: `totalScore=${config.totalScore} \u4E0E\u68C0\u67E5\u70B9\u4E4B\u548C ${analysis.checkpointTotal} \u4E0D\u4E00\u81F4`,
|
|
1041
|
+
file: rel(config.configFile),
|
|
1042
|
+
hint: "\u5BA1\u8BA1\u901A\u8FC7\u7684\u6761\u4EF6\u662F score === totalScore\uFF0C\u4E24\u8005\u4E0D\u7B49\u65F6\u7EBF\u4E0A\u4F1A\u5224\u5931\u8D25\u3002"
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
if (config.totalScore === void 0) {
|
|
1046
|
+
push({
|
|
1047
|
+
rule: "CFG002",
|
|
1048
|
+
severity: "info",
|
|
1049
|
+
message: `\u672A\u6307\u5B9A totalScore\uFF0C\u6309\u68C0\u67E5\u70B9\u4E4B\u548C\u53D6 ${analysis.checkpointTotal}`,
|
|
1050
|
+
file: rel(config.configFile)
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
const isBackend = config.type === "backend_service";
|
|
1054
|
+
const mountPath = config.runtime.judgeUploadPath;
|
|
1055
|
+
const initCommand = config.runtime.initCommand;
|
|
1056
|
+
if (!isBackend && initCommand && !initCommand.includes(mountPath)) {
|
|
1057
|
+
push({
|
|
1058
|
+
rule: "RUN001",
|
|
1059
|
+
severity: "warn",
|
|
1060
|
+
message: `initCommand \u672A\u5305\u542B\u4E0A\u4F20\u76EE\u5F55 "${mountPath}"\uFF1A${initCommand}`,
|
|
1061
|
+
file: rel(config.configFile),
|
|
1062
|
+
hint: "\u7AD9\u70B9\u6839\u5FC5\u987B\u7B49\u4E8E\u5224\u9898\u6253\u5305\u76EE\u5F55\uFF0C\u5426\u5219\u5224\u9898\u673A\u4F1A\u5728\u9519\u8BEF\u7684\u76EE\u5F55\u4E0B\u627E index.html \u76F4\u5230\u8D85\u65F6\u3002"
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
if (config.cover.mode === "custom" && !("imageId" in config.cover)) {
|
|
1066
|
+
push({
|
|
1067
|
+
rule: "IMG001",
|
|
1068
|
+
severity: "error",
|
|
1069
|
+
message: "cover.mode=custom \u65F6\u5FC5\u987B\u63D0\u4F9B imageId",
|
|
1070
|
+
file: rel(config.configFile),
|
|
1071
|
+
hint: "\u914D\u7F6E\u4E86 cover.generate \u7684\u8BDD\uFF0C\u8DD1 `qpc cover --upload --write` \u4E00\u952E\u751F\u6210\u5E76\u5199\u56DE\uFF1B\u5426\u5219\u628A\u5E73\u53F0\u4E0A\u7684\u56FE\u7247 id \u586B\u8FDB imageId\u3002"
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
if (isBackend) {
|
|
1075
|
+
for (const field of config.overriddenByImage) {
|
|
1076
|
+
push({
|
|
1077
|
+
rule: "RUN002",
|
|
1078
|
+
severity: "warn",
|
|
1079
|
+
message: `${field} \u4F1A\u88AB\u521D\u59CB\u955C\u50CF ${config.runtime.serviceImage} \u89C4\u5B9A\u7684\u547D\u4EE4\u8986\u76D6\uFF0C\u4E0D\u4F1A\u751F\u6548`,
|
|
1080
|
+
file: rel(config.configFile),
|
|
1081
|
+
hint: "\u7F16\u8F91\u5668\u91CC\u7684\u4E00\u6B21\u6027\u51C6\u5907\u6B65\u9AA4\u7531\u955C\u50CF\u51B3\u5B9A\uFF08\u4E0E\u5BB9\u5668\u5185\u7684\u79BB\u7EBF\u5B89\u88C5\u4FDD\u6301\u4E00\u81F4\uFF09\uFF1B\u670D\u52A1\u542F\u52A8\u547D\u4EE4\u8BF7\u5199\u5728 runtime.initCommand\u3002"
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
if (config.cover.mode === "default") {
|
|
1085
|
+
push({
|
|
1086
|
+
rule: "IMG002",
|
|
1087
|
+
severity: "error",
|
|
1088
|
+
message: "\u540E\u7AEF\u670D\u52A1\u9898\u578B\u5FC5\u987B\u4F7F\u7528\u81EA\u5B9A\u4E49\u5C01\u9762\uFF08\u6CA1\u6709\u9996\u5C4F\u53EF\u622A\u56FE\uFF09",
|
|
1089
|
+
file: rel(config.configFile),
|
|
1090
|
+
hint: '\u628A cover \u6539\u6210 { mode: "custom", generate: { bg: "#0F172A", bgTo: "#14432A" } }\uFF0C\u518D\u8DD1 `qpc cover --upload --write`\u3002'
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
const template = await collectSnapshot(config.paths.template, { mountPath });
|
|
1095
|
+
const answer = await collectSnapshot(config.paths.answer, { mountPath });
|
|
1096
|
+
const checkArtifact = async (label, dir, snapshot, rulePrefix, requireIndex) => {
|
|
1097
|
+
const relative4 = rel(dir);
|
|
1098
|
+
if (!await pathExists(dir)) {
|
|
1099
|
+
push({
|
|
1100
|
+
rule: `${rulePrefix}001`,
|
|
1101
|
+
severity: "error",
|
|
1102
|
+
message: `${label} \u76EE\u5F55\u4E0D\u5B58\u5728\uFF1A${relative4}`,
|
|
1103
|
+
file: relative4,
|
|
1104
|
+
hint: "\u9898\u76EE\u5FC5\u987B\u6709\u7B54\u6848\u6A21\u677F\uFF08\u5B66\u751F\u8D77\u70B9\uFF09\u4E0E\u53C2\u8003\u89E3\uFF08\u62FF\u6EE1\u5206\u7684\u7B54\u6848\uFF09\u3002"
|
|
1105
|
+
});
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
if (snapshot.fileCount === 0) {
|
|
1109
|
+
push({
|
|
1110
|
+
rule: `${rulePrefix}002`,
|
|
1111
|
+
severity: "error",
|
|
1112
|
+
message: `${label} \u76EE\u5F55\u4E3A\u7A7A\uFF1A${relative4}`,
|
|
1113
|
+
file: relative4
|
|
1114
|
+
});
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
const hasIndex = Object.keys(snapshot.files).some(
|
|
1118
|
+
(key) => key.endsWith("/index.html")
|
|
1119
|
+
);
|
|
1120
|
+
if (requireIndex && label === "template" && !hasIndex) {
|
|
1121
|
+
push({
|
|
1122
|
+
rule: "TPL003",
|
|
1123
|
+
severity: "error",
|
|
1124
|
+
message: `\u7B54\u6848\u6A21\u677F\u7F3A\u5C11 index.html\uFF08\u7AD9\u70B9\u6839 = ${mountPath}\uFF09`,
|
|
1125
|
+
file: relative4,
|
|
1126
|
+
hint: "initCommand \u4EE5\u8BE5\u76EE\u5F55\u4E3A\u7AD9\u70B9\u6839\u542F\u52A8\u9759\u6001\u670D\u52A1\u5668\uFF0C\u6CA1\u6709 index.html \u65F6\u5224\u9898\u4F1A\u4E00\u76F4\u7B49\u9009\u62E9\u5668\u3002"
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
if (isBackend && label === "answer") {
|
|
1130
|
+
const hasLockfile = Object.keys(snapshot.files).some(
|
|
1131
|
+
(key) => key.endsWith("/pnpm-lock.yaml")
|
|
1132
|
+
);
|
|
1133
|
+
if (!hasLockfile) {
|
|
1134
|
+
push({
|
|
1135
|
+
rule: "RUN003",
|
|
1136
|
+
severity: "error",
|
|
1137
|
+
message: `\u540E\u7AEF\u670D\u52A1\u53C2\u8003\u7B54\u6848\u7F3A\u5C11 pnpm-lock.yaml\uFF08\u5E94\u5728 ${mountPath}/ \u4E0B\uFF09`,
|
|
1138
|
+
file: relative4,
|
|
1139
|
+
hint: "\u79BB\u7EBF\u5B89\u88C5\u4F9D\u8D56\u9501\u6587\u4EF6\uFF1B\u6CA1\u6709\u5B83\u5BB9\u5668\u65E0\u6CD5\u5728\u65AD\u7F51\u73AF\u5883\u4E0B\u5B89\u88C5\u4F9D\u8D56\u3002\u5148\u5728\u53C2\u8003\u7B54\u6848\u76EE\u5F55\u6267\u884C\u4E00\u6B21 pnpm install \u751F\u6210\u3002"
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
for (const ignored of snapshot.ignored) {
|
|
1144
|
+
push({
|
|
1145
|
+
rule: "SNAP003",
|
|
1146
|
+
severity: "info",
|
|
1147
|
+
message: `${relative4} \u4E0B\u5DF2\u8DF3\u8FC7 ${ignored}/\uFF08\u6784\u5EFA\u4EA7\u7269\u6216\u4F9D\u8D56\uFF09`,
|
|
1148
|
+
file: relative4
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
};
|
|
1152
|
+
await checkArtifact(
|
|
1153
|
+
"template",
|
|
1154
|
+
config.paths.template,
|
|
1155
|
+
template,
|
|
1156
|
+
"TPL",
|
|
1157
|
+
!isBackend
|
|
1158
|
+
);
|
|
1159
|
+
await checkArtifact(
|
|
1160
|
+
"answer",
|
|
1161
|
+
config.paths.answer,
|
|
1162
|
+
answer,
|
|
1163
|
+
"ANS",
|
|
1164
|
+
!isBackend
|
|
1165
|
+
);
|
|
1166
|
+
const maxUploadBytes = options.maxUploadBytes ?? Number.POSITIVE_INFINITY;
|
|
1167
|
+
const maxFileCount = options.maxFileCount ?? 500;
|
|
1168
|
+
for (const [label, snapshot] of [
|
|
1169
|
+
["template", template],
|
|
1170
|
+
["answer", answer]
|
|
1171
|
+
]) {
|
|
1172
|
+
if (snapshot.totalBytes > maxUploadBytes) {
|
|
1173
|
+
push({
|
|
1174
|
+
rule: "SNAP001",
|
|
1175
|
+
severity: "error",
|
|
1176
|
+
message: `${label} \u5FEB\u7167\u4F53\u79EF ${formatBytes(snapshot.totalBytes)} \u8D85\u8FC7\u4E0A\u9650 ${formatBytes(maxUploadBytes)}`,
|
|
1177
|
+
hint: "\u786E\u8BA4\u6CA1\u6709\u628A dist/\u3001node_modules/ \u6216\u5927\u56FE\u6253\u8FDB\u9898\u76EE\u76EE\u5F55\uFF1B\u5FC5\u8981\u65F6\u8C03\u6574 QUANTA_MAX_UPLOAD_BYTES\u3002"
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
if (snapshot.fileCount > maxFileCount) {
|
|
1181
|
+
push({
|
|
1182
|
+
rule: "SNAP002",
|
|
1183
|
+
severity: "error",
|
|
1184
|
+
message: `${label} \u5FEB\u7167\u6587\u4EF6\u6570 ${snapshot.fileCount} \u8D85\u8FC7\u4E0A\u9650 ${maxFileCount}`
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
const errors = findings.filter((f) => f.severity === "error").length;
|
|
1189
|
+
const warnings = findings.filter((f) => f.severity === "warn").length;
|
|
1190
|
+
return {
|
|
1191
|
+
findings,
|
|
1192
|
+
errors,
|
|
1193
|
+
warnings,
|
|
1194
|
+
ok: errors === 0,
|
|
1195
|
+
totalScore,
|
|
1196
|
+
stats: {
|
|
1197
|
+
checkpointCount: analysis.checkpoints.length,
|
|
1198
|
+
judgeLines: analysis.lineCount,
|
|
1199
|
+
template: toStats(template),
|
|
1200
|
+
answer: toStats(answer)
|
|
1201
|
+
},
|
|
1202
|
+
analysis,
|
|
1203
|
+
templateSnapshot: template.files,
|
|
1204
|
+
answerSnapshot: answer.files
|
|
1205
|
+
};
|
|
1206
|
+
};
|
|
1207
|
+
var toStats = (snapshot) => ({
|
|
1208
|
+
fileCount: snapshot.fileCount,
|
|
1209
|
+
totalBytes: snapshot.totalBytes,
|
|
1210
|
+
ignored: snapshot.ignored
|
|
1211
|
+
});
|
|
1212
|
+
var duplicateNames = (names) => {
|
|
1213
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1214
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
1215
|
+
for (const name of names) {
|
|
1216
|
+
if (seen.has(name)) duplicates.add(name);
|
|
1217
|
+
seen.add(name);
|
|
1218
|
+
}
|
|
1219
|
+
return [...duplicates];
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1222
|
+
// src/services/admin-api.ts
|
|
1223
|
+
var createAdminApi = (client) => ({
|
|
1224
|
+
login: async (input) => client.call(
|
|
1225
|
+
"auth.login.email",
|
|
1226
|
+
{
|
|
1227
|
+
input,
|
|
1228
|
+
method: "POST"
|
|
1229
|
+
}
|
|
1230
|
+
),
|
|
1231
|
+
getCurrentUser: async () => {
|
|
1232
|
+
const result = await client.call(
|
|
1233
|
+
"auth.login.getUser",
|
|
1234
|
+
{
|
|
1235
|
+
method: "GET"
|
|
1236
|
+
}
|
|
1237
|
+
);
|
|
1238
|
+
return result.user;
|
|
1239
|
+
},
|
|
1240
|
+
uploadProblem: (input) => client.call(
|
|
1241
|
+
"admin.problem.upload",
|
|
1242
|
+
{
|
|
1243
|
+
input,
|
|
1244
|
+
method: "POST"
|
|
1245
|
+
}
|
|
1246
|
+
),
|
|
1247
|
+
reuploadProblem: (input) => client.call(
|
|
1248
|
+
"admin.problem.reupload",
|
|
1249
|
+
{
|
|
1250
|
+
input,
|
|
1251
|
+
method: "POST"
|
|
1252
|
+
}
|
|
1253
|
+
),
|
|
1254
|
+
uploadImage: (input) => client.call("admin.image.upload", {
|
|
1255
|
+
input,
|
|
1256
|
+
method: "POST"
|
|
1257
|
+
}),
|
|
1258
|
+
getAuditDetail: (problemId) => client.call("admin.problem.getAuditDetail", {
|
|
1259
|
+
input: { problemId },
|
|
1260
|
+
method: "GET"
|
|
1261
|
+
}),
|
|
1262
|
+
setStatus: (problemId, publish) => client.call("admin.problem.setStatus", {
|
|
1263
|
+
input: { problemId, publish },
|
|
1264
|
+
method: "POST"
|
|
1265
|
+
}),
|
|
1266
|
+
listProblems: async (tagIds = []) => {
|
|
1267
|
+
const result = await client.call("admin.problem.list", { input: { tids: tagIds }, method: "GET" });
|
|
1268
|
+
if (Array.isArray(result)) return result;
|
|
1269
|
+
return result.problems ?? [];
|
|
1270
|
+
},
|
|
1271
|
+
listTags: async () => {
|
|
1272
|
+
const tags = await client.call("public.tag.list", {
|
|
1273
|
+
method: "GET"
|
|
1274
|
+
});
|
|
1275
|
+
return tags.map((tag) => ({
|
|
1276
|
+
tid: tag.tid,
|
|
1277
|
+
name: tag.name,
|
|
1278
|
+
color: tag.color ?? null,
|
|
1279
|
+
description: tag.description ?? null
|
|
1280
|
+
}));
|
|
1281
|
+
}
|
|
1282
|
+
});
|
|
1283
|
+
var auditRecordOf = (detail) => detail.TemplateJudgeRecord?.[0]?.judgeRecord;
|
|
1284
|
+
var isAuditSettled = (detail) => {
|
|
1285
|
+
if (detail.status !== "draft") return true;
|
|
1286
|
+
const record = auditRecordOf(detail);
|
|
1287
|
+
return record !== void 0 && record.result !== "pending";
|
|
1288
|
+
};
|
|
1289
|
+
var readAuditResults = (record) => {
|
|
1290
|
+
if (!record) return { results: [] };
|
|
1291
|
+
if (Array.isArray(record.info)) return { results: record.info };
|
|
1292
|
+
const errorMessage = record.info.errorMessage;
|
|
1293
|
+
return { results: [], errorMessage };
|
|
1294
|
+
};
|
|
1295
|
+
|
|
1296
|
+
// src/ui/report.ts
|
|
1297
|
+
var SEVERITY_LABEL = {
|
|
1298
|
+
error: "\u9519\u8BEF",
|
|
1299
|
+
warn: "\u8B66\u544A",
|
|
1300
|
+
info: "\u63D0\u793A"
|
|
1301
|
+
};
|
|
1302
|
+
var SEVERITY_ORDER = ["error", "warn", "info"];
|
|
1303
|
+
var renderFindings = (logger, findings, options = {}) => {
|
|
1304
|
+
const { colors } = logger;
|
|
1305
|
+
for (const severity of SEVERITY_ORDER) {
|
|
1306
|
+
const group = findings.filter((finding) => finding.severity === severity);
|
|
1307
|
+
if (group.length === 0) continue;
|
|
1308
|
+
if (severity === "info" && !options.includeInfo) continue;
|
|
1309
|
+
const color = severity === "error" ? colors.red : severity === "warn" ? colors.yellow : colors.dim;
|
|
1310
|
+
logger.info(`${color(`${SEVERITY_LABEL[severity]} \xD7 ${group.length}`)}`);
|
|
1311
|
+
for (const finding of group) {
|
|
1312
|
+
const location = finding.file ? `${finding.file}${finding.line ? `:${finding.line}` : ""} ` : "";
|
|
1313
|
+
logger.info(
|
|
1314
|
+
` ${colors.bold(finding.rule)} ${location}${finding.message}`
|
|
1315
|
+
);
|
|
1316
|
+
if (finding.hint) logger.detail(finding.hint);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1320
|
+
var renderValidationSummary = (logger, report, options = {}) => {
|
|
1321
|
+
const { colors } = logger;
|
|
1322
|
+
logger.step(options.title ?? "\u9884\u68C0\u7ED3\u679C");
|
|
1323
|
+
logger.table([
|
|
1324
|
+
[
|
|
1325
|
+
"\u68C0\u67E5\u70B9",
|
|
1326
|
+
`${report.stats.checkpointCount} \u4E2A\uFF0C\u5408\u8BA1 ${report.totalScore} \u5206`
|
|
1327
|
+
],
|
|
1328
|
+
[
|
|
1329
|
+
"\u7B54\u6848\u6A21\u677F",
|
|
1330
|
+
`${report.stats.template.fileCount} \u4E2A\u6587\u4EF6\uFF0C${formatBytes(report.stats.template.totalBytes)}`
|
|
1331
|
+
],
|
|
1332
|
+
[
|
|
1333
|
+
"\u53C2\u8003\u89E3",
|
|
1334
|
+
`${report.stats.answer.fileCount} \u4E2A\u6587\u4EF6\uFF0C${formatBytes(report.stats.answer.totalBytes)}`
|
|
1335
|
+
],
|
|
1336
|
+
["\u5224\u9898\u811A\u672C", `${report.stats.judgeLines} \u884C`],
|
|
1337
|
+
[
|
|
1338
|
+
"\u7ED3\u8BBA",
|
|
1339
|
+
report.ok ? colors.green(`\u901A\u8FC7\uFF08${report.warnings} \u6761\u8B66\u544A\uFF09`) : colors.red(`\u672A\u901A\u8FC7\uFF08${report.errors} \u4E2A\u9519\u8BEF\uFF09`)
|
|
1340
|
+
]
|
|
1341
|
+
]);
|
|
1342
|
+
};
|
|
1343
|
+
var renderAuditDetail = (logger, detail, options = {}) => {
|
|
1344
|
+
const { colors } = logger;
|
|
1345
|
+
const record = detail.TemplateJudgeRecord?.[0]?.judgeRecord;
|
|
1346
|
+
const { results, errorMessage } = readAuditResults(record);
|
|
1347
|
+
const score = record?.score ?? 0;
|
|
1348
|
+
logger.step(`\u9898\u76EE #${detail.pid} ${detail.title}`);
|
|
1349
|
+
logger.table([
|
|
1350
|
+
["\u72B6\u6001", detail.status],
|
|
1351
|
+
["\u96BE\u5EA6", detail.difficulty],
|
|
1352
|
+
["\u5BA1\u8BA1\u5F97\u5206", `${score} / ${detail.totalScore}`],
|
|
1353
|
+
["\u5BA1\u8BA1\u7ED3\u679C", record?.result ?? "unknown"]
|
|
1354
|
+
]);
|
|
1355
|
+
for (const [index, item] of results.entries()) {
|
|
1356
|
+
const mark = item.status === "pass" ? colors.green("\u2714") : colors.red("\u2716");
|
|
1357
|
+
logger.info(
|
|
1358
|
+
` ${mark} [${index + 1}] ${item.score}/${item.totalScore} ${item.details ?? ""}`
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
if (errorMessage) logger.error(`\u5BA1\u8BA1\u6267\u884C\u5931\u8D25\uFF1A${errorMessage}`);
|
|
1362
|
+
if (options.verbose && detail.JudgeFile?.[0]?.judgeScript) {
|
|
1363
|
+
logger.debug(`\u5224\u9898\u811A\u672C ${detail.JudgeFile[0].judgeScript.length} \u5B57\u7B26`);
|
|
1364
|
+
}
|
|
1365
|
+
return record?.result === "success" && detail.totalScore > 0 && score === detail.totalScore;
|
|
1366
|
+
};
|
|
1367
|
+
var validationToJson = (report) => ({
|
|
1368
|
+
ok: report.ok,
|
|
1369
|
+
totalScore: report.totalScore,
|
|
1370
|
+
errors: report.errors,
|
|
1371
|
+
warnings: report.warnings,
|
|
1372
|
+
stats: report.stats,
|
|
1373
|
+
checkpoints: report.analysis.checkpoints,
|
|
1374
|
+
findings: report.findings
|
|
1375
|
+
});
|
|
1376
|
+
var auditToJson = (detail) => {
|
|
1377
|
+
const record = detail.TemplateJudgeRecord?.[0]?.judgeRecord;
|
|
1378
|
+
return {
|
|
1379
|
+
problemId: detail.pid,
|
|
1380
|
+
title: detail.title,
|
|
1381
|
+
status: detail.status,
|
|
1382
|
+
totalScore: detail.totalScore,
|
|
1383
|
+
audit: record ? {
|
|
1384
|
+
result: record.result,
|
|
1385
|
+
score: record.score,
|
|
1386
|
+
...readAuditResults(record)
|
|
1387
|
+
} : null
|
|
1388
|
+
};
|
|
1389
|
+
};
|
|
1390
|
+
|
|
1391
|
+
// src/commands/check.ts
|
|
1392
|
+
var registerCheckCommand = (registry) => {
|
|
1393
|
+
const { program, context } = registry;
|
|
1394
|
+
program.command("check").description("\u79BB\u7EBF\u9884\u68C0\u9898\u76EE\uFF08\u5224\u9898\u811A\u672C\u3001\u5FEB\u7167\u3001\u5206\u503C\u3001\u8FD0\u884C\u914D\u7F6E\uFF09").argument("[dir]", "\u9898\u76EE\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09", ".").option("--strict", "\u8B66\u544A\u4E5F\u89C6\u4E3A\u5931\u8D25").action(async (dir, options) => {
|
|
1395
|
+
const ctx = await context();
|
|
1396
|
+
await runCheck(ctx, { dir, ...options });
|
|
1397
|
+
});
|
|
1398
|
+
};
|
|
1399
|
+
var runCheck = async (ctx, options) => {
|
|
1400
|
+
const rootDir = resolve(ctx.cwd, options.dir);
|
|
1401
|
+
ctx.logger.step(`\u8BFB\u53D6\u9898\u76EE\u914D\u7F6E \u2192 ${rootDir}`);
|
|
1402
|
+
const config = await loadProblemConfig(rootDir);
|
|
1403
|
+
ctx.logger.detail(`title: ${config.title}`);
|
|
1404
|
+
const report = await validateProblem(config, {
|
|
1405
|
+
maxUploadBytes: ctx.env.maxUploadBytes
|
|
1406
|
+
});
|
|
1407
|
+
renderFindings(ctx.logger, report.findings, {
|
|
1408
|
+
includeInfo: ctx.logger.level === "debug"
|
|
1409
|
+
});
|
|
1410
|
+
renderValidationSummary(ctx.logger, report);
|
|
1411
|
+
if (ctx.logger.json) {
|
|
1412
|
+
ctx.logger.result(validationToJson(report));
|
|
1413
|
+
}
|
|
1414
|
+
if (!report.ok) {
|
|
1415
|
+
throw new PrecheckError(
|
|
1416
|
+
`\u9884\u68C0\u672A\u901A\u8FC7\uFF1A${report.errors} \u4E2A\u9519\u8BEF\u3001${report.warnings} \u6761\u8B66\u544A`,
|
|
1417
|
+
{
|
|
1418
|
+
hint: "\u6309\u4E0A\u9762\u7684\u89C4\u5219\u53F7\u9010\u6761\u4FEE\u590D\u540E\u91CD\u8DD1 `qpc check`\u3002"
|
|
1419
|
+
}
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
if (options.strict && report.warnings > 0) {
|
|
1423
|
+
throw new PrecheckError(`--strict\uFF1A\u5B58\u5728 ${report.warnings} \u6761\u8B66\u544A`, {
|
|
1424
|
+
hint: "\u53BB\u6389 --strict \u53EF\u53EA\u62E6\u622A\u9519\u8BEF\uFF1B\u8B66\u544A\u4E0D\u5F71\u54CD\u7EBF\u4E0A\u5BA1\u8BA1\u3002"
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
ctx.logger.success("\u9884\u68C0\u901A\u8FC7");
|
|
1428
|
+
return report;
|
|
1429
|
+
};
|
|
1430
|
+
|
|
1431
|
+
// src/domain/config-writer.ts
|
|
1432
|
+
var applyCoverUpdate = (content, update, file) => {
|
|
1433
|
+
if (file.endsWith(".json")) {
|
|
1434
|
+
return applyToJson(content, update);
|
|
1435
|
+
}
|
|
1436
|
+
return applyToSource(content, update, file);
|
|
1437
|
+
};
|
|
1438
|
+
var applyToJson = (content, update) => {
|
|
1439
|
+
let parsed;
|
|
1440
|
+
try {
|
|
1441
|
+
parsed = JSON.parse(content);
|
|
1442
|
+
} catch (error) {
|
|
1443
|
+
throw new ConfigError("\u9898\u76EE\u914D\u7F6E\u4E0D\u662F\u5408\u6CD5 JSON\uFF0C\u65E0\u6CD5\u5199\u56DE\u5C01\u9762", {
|
|
1444
|
+
cause: error,
|
|
1445
|
+
hint: "\u624B\u52A8\u628A imageId \u4E0E\u914D\u8272\u586B\u8FDB cover\u3002"
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
const root = parsed;
|
|
1449
|
+
const cover = root.cover;
|
|
1450
|
+
if (cover?.mode !== "custom") {
|
|
1451
|
+
throw new ConfigError(
|
|
1452
|
+
'\u914D\u7F6E\u91CC\u6CA1\u6709 cover: { mode: "custom" }\uFF0C\u65E0\u6CD5\u5199\u56DE\u5C01\u9762',
|
|
1453
|
+
{ hint: "\u5148\u6309\u6587\u6863\u628A\u5C01\u9762\u6539\u6210\u81EA\u5B9A\u4E49\u6A21\u5F0F\uFF0C\u518D\u91CD\u8DD1\u3002" }
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
cover.imageId = update.imageId;
|
|
1457
|
+
if (update.colors) {
|
|
1458
|
+
const previous = cover.generate && typeof cover.generate === "object" ? cover.generate : {};
|
|
1459
|
+
const generate = { ...previous };
|
|
1460
|
+
generate.bg = update.colors.bg;
|
|
1461
|
+
generate.accent = update.colors.accent;
|
|
1462
|
+
generate.text = update.colors.text;
|
|
1463
|
+
if (update.colors.bgTo === void 0) {
|
|
1464
|
+
delete generate.bgTo;
|
|
1465
|
+
} else {
|
|
1466
|
+
generate.bgTo = update.colors.bgTo;
|
|
1467
|
+
}
|
|
1468
|
+
cover.generate = generate;
|
|
1469
|
+
}
|
|
1470
|
+
return `${JSON.stringify(parsed, null, 3)}
|
|
1471
|
+
`;
|
|
1472
|
+
};
|
|
1473
|
+
var applyToSource = (content, update, file) => {
|
|
1474
|
+
const coverMatch = /cover\s*:\s*\{/.exec(content);
|
|
1475
|
+
if (!coverMatch) {
|
|
1476
|
+
throw new ConfigError(
|
|
1477
|
+
`\u5728 ${file} \u91CC\u627E\u4E0D\u5230 cover: { ... }\uFF0C\u65E0\u6CD5\u5199\u56DE\u5C01\u9762`,
|
|
1478
|
+
{ hint: "\u628A imageId \u4E0E\u914D\u8272\u624B\u52A8\u586B\u8FDB cover\u3002" }
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
const openBrace = content.indexOf("{", coverMatch.index);
|
|
1482
|
+
const closeBrace = findObjectEnd(content, openBrace);
|
|
1483
|
+
if (closeBrace < 0) {
|
|
1484
|
+
throw new ConfigError(`\u5728 ${file} \u91CC\u65E0\u6CD5\u89E3\u6790 cover \u5BF9\u8C61\uFF08\u62EC\u53F7\u4E0D\u914D\u5BF9\uFF09`, {
|
|
1485
|
+
hint: "\u628A imageId \u4E0E\u914D\u8272\u624B\u52A8\u586B\u8FDB cover\u3002"
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
const before = content.slice(0, openBrace);
|
|
1489
|
+
const after = content.slice(closeBrace + 1);
|
|
1490
|
+
const coverLineStart = content.lastIndexOf("\n", coverMatch.index) + 1;
|
|
1491
|
+
const coverLinePrefix = content.slice(coverLineStart, coverMatch.index);
|
|
1492
|
+
const indent = /^[ \t]*$/.test(coverLinePrefix) ? `${coverLinePrefix} ` : `${/\n([ \t]+)\S/.exec(before)?.[1] ?? " "} `;
|
|
1493
|
+
let body = content.slice(openBrace, closeBrace + 1);
|
|
1494
|
+
if (update.colors) {
|
|
1495
|
+
body = writeColors(body, update.colors, indent, file);
|
|
1496
|
+
}
|
|
1497
|
+
body = writeImageId(body, update.imageId, file, indent);
|
|
1498
|
+
return `${before}${body}${after}`;
|
|
1499
|
+
};
|
|
1500
|
+
var applyProblemIdUpdate = (content, problemId, file) => {
|
|
1501
|
+
if (file.endsWith(".json")) {
|
|
1502
|
+
return applyProblemIdToJson(content, problemId, file);
|
|
1503
|
+
}
|
|
1504
|
+
return applyProblemIdToSource(content, problemId, file);
|
|
1505
|
+
};
|
|
1506
|
+
var applyProblemIdToJson = (content, problemId, file) => {
|
|
1507
|
+
let parsed;
|
|
1508
|
+
try {
|
|
1509
|
+
parsed = JSON.parse(content);
|
|
1510
|
+
} catch (error) {
|
|
1511
|
+
throw new ConfigError("\u9898\u76EE\u914D\u7F6E\u4E0D\u662F\u5408\u6CD5 JSON\uFF0C\u65E0\u6CD5\u5199\u56DE problemId", {
|
|
1512
|
+
cause: error,
|
|
1513
|
+
hint: `\u624B\u52A8\u628A problemId: ${problemId} \u586B\u8FDB\u914D\u7F6E\u3002`
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1517
|
+
throw new ConfigError(`${file} \u7684\u9876\u5C42\u4E0D\u662F\u4E00\u4E2A\u5BF9\u8C61\uFF0C\u65E0\u6CD5\u5199\u56DE problemId`, {
|
|
1518
|
+
hint: `\u624B\u52A8\u628A problemId: ${problemId} \u586B\u8FDB\u914D\u7F6E\u3002`
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
parsed.problemId = problemId;
|
|
1522
|
+
return `${JSON.stringify(parsed, null, 3)}
|
|
1523
|
+
`;
|
|
1524
|
+
};
|
|
1525
|
+
var applyProblemIdToSource = (content, problemId, file) => {
|
|
1526
|
+
const existing = /\bproblemId\s*:\s*\d+/.exec(content);
|
|
1527
|
+
if (existing) return content.replace(existing[0], `problemId: ${problemId}`);
|
|
1528
|
+
const openBrace = findConfigObjectStart(content);
|
|
1529
|
+
if (openBrace < 0) {
|
|
1530
|
+
throw new ConfigError(
|
|
1531
|
+
`\u5728 ${file} \u91CC\u627E\u4E0D\u5230\u9898\u76EE\u914D\u7F6E\u5BF9\u8C61\uFF0C\u65E0\u6CD5\u5199\u56DE problemId`,
|
|
1532
|
+
{ hint: `\u624B\u52A8\u628A problemId: ${problemId} \u586B\u8FDB\u914D\u7F6E\u3002` }
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
const after = content.slice(openBrace + 1);
|
|
1536
|
+
const firstNewline = after.indexOf("\n");
|
|
1537
|
+
const sameLine = after.slice(0, firstNewline < 0 ? void 0 : firstNewline);
|
|
1538
|
+
const insertion = /^[ \t]*$/.test(sameLine) ? `
|
|
1539
|
+
${/\n([ \t]+)\S/.exec(after)?.[1] ?? " "}problemId: ${problemId},` : ` problemId: ${problemId},`;
|
|
1540
|
+
return `${content.slice(0, openBrace + 1)}${insertion}${after}`;
|
|
1541
|
+
};
|
|
1542
|
+
var applyTagIdsUpdate = (content, tagIds, file) => {
|
|
1543
|
+
if (file.endsWith(".json")) {
|
|
1544
|
+
return applyTagIdsToJson(content, tagIds, file);
|
|
1545
|
+
}
|
|
1546
|
+
return applyTagIdsToSource(content, tagIds, file);
|
|
1547
|
+
};
|
|
1548
|
+
var applyTagIdsToJson = (content, tagIds, file) => {
|
|
1549
|
+
let parsed;
|
|
1550
|
+
try {
|
|
1551
|
+
parsed = JSON.parse(content);
|
|
1552
|
+
} catch (error) {
|
|
1553
|
+
throw new ConfigError("\u9898\u76EE\u914D\u7F6E\u4E0D\u662F\u5408\u6CD5 JSON\uFF0C\u65E0\u6CD5\u5199\u56DE tagIds", {
|
|
1554
|
+
cause: error,
|
|
1555
|
+
hint: `\u624B\u52A8\u628A tagIds: [${tagIds.join(", ")}] \u586B\u8FDB\u914D\u7F6E\u3002`
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1559
|
+
throw new ConfigError(`${file} \u7684\u9876\u5C42\u4E0D\u662F\u4E00\u4E2A\u5BF9\u8C61\uFF0C\u65E0\u6CD5\u5199\u56DE tagIds`, {
|
|
1560
|
+
hint: `\u624B\u52A8\u628A tagIds: [${tagIds.join(", ")}] \u586B\u8FDB\u914D\u7F6E\u3002`
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
parsed.tagIds = tagIds;
|
|
1564
|
+
return `${JSON.stringify(parsed, null, 3)}
|
|
1565
|
+
`;
|
|
1566
|
+
};
|
|
1567
|
+
var applyTagIdsToSource = (content, tagIds, file) => {
|
|
1568
|
+
const literal = `[${tagIds.join(", ")}]`;
|
|
1569
|
+
const existing = /\btagIds\s*:\s*\[[^\]]*\]/.exec(content);
|
|
1570
|
+
if (existing) return content.replace(existing[0], `tagIds: ${literal}`);
|
|
1571
|
+
for (const anchor of ["difficulty", "title"]) {
|
|
1572
|
+
const match = new RegExp(
|
|
1573
|
+
`\\b${anchor}\\s*:\\s*(['"\`])[^'"\`]*\\1,?`
|
|
1574
|
+
).exec(content);
|
|
1575
|
+
if (!match) continue;
|
|
1576
|
+
const lineStart = content.lastIndexOf("\n", match.index) + 1;
|
|
1577
|
+
const prefix = content.slice(lineStart, match.index);
|
|
1578
|
+
const indent = /^([ \t]*)$/.exec(prefix)?.[1] ?? " ";
|
|
1579
|
+
const sep2 = /,\s*$/.test(match[0]) ? "" : ",";
|
|
1580
|
+
const insertAt = match.index + match[0].length;
|
|
1581
|
+
const next = /^[ \t]*$/.test(prefix) ? `${sep2}
|
|
1582
|
+
${indent}tagIds: ${literal},` : `${sep2} tagIds: ${literal},`;
|
|
1583
|
+
return `${content.slice(0, insertAt)}${next}${content.slice(insertAt)}`;
|
|
1584
|
+
}
|
|
1585
|
+
const openBrace = findConfigObjectStart(content);
|
|
1586
|
+
if (openBrace < 0) {
|
|
1587
|
+
throw new ConfigError(
|
|
1588
|
+
`\u5728 ${file} \u91CC\u627E\u4E0D\u5230\u9898\u76EE\u914D\u7F6E\u5BF9\u8C61\uFF0C\u65E0\u6CD5\u5199\u56DE tagIds`,
|
|
1589
|
+
{ hint: `\u624B\u52A8\u628A tagIds: ${literal} \u586B\u8FDB\u914D\u7F6E\u3002` }
|
|
1590
|
+
);
|
|
1591
|
+
}
|
|
1592
|
+
const after = content.slice(openBrace + 1);
|
|
1593
|
+
const firstNewline = after.indexOf("\n");
|
|
1594
|
+
const sameLine = after.slice(0, firstNewline < 0 ? void 0 : firstNewline);
|
|
1595
|
+
const insertion = /^[ \t]*$/.test(sameLine) ? `
|
|
1596
|
+
${/\n([ \t]+)\S/.exec(after)?.[1] ?? " "}tagIds: ${literal},` : ` tagIds: ${literal},`;
|
|
1597
|
+
return `${content.slice(0, openBrace + 1)}${insertion}${after}`;
|
|
1598
|
+
};
|
|
1599
|
+
var findConfigObjectStart = (content) => {
|
|
1600
|
+
const wrapper = /defineProblemConfig\s*\(\s*\{/.exec(content);
|
|
1601
|
+
if (wrapper) return wrapper.index + wrapper[0].lastIndexOf("{");
|
|
1602
|
+
const fallback = /export\s+default\s*\{/.exec(content);
|
|
1603
|
+
if (fallback) return fallback.index + fallback[0].lastIndexOf("{");
|
|
1604
|
+
return content.indexOf("{");
|
|
1605
|
+
};
|
|
1606
|
+
var writeColors = (body, colors, coverIndent, file) => {
|
|
1607
|
+
const generateMatch = /generate\s*:\s*\{/.exec(body);
|
|
1608
|
+
if (!generateMatch) {
|
|
1609
|
+
return insertGenerate(body, colors, coverIndent, file);
|
|
1610
|
+
}
|
|
1611
|
+
const openBrace = body.indexOf("{", generateMatch.index);
|
|
1612
|
+
const closeBrace = findObjectEnd(body, openBrace);
|
|
1613
|
+
if (closeBrace < 0) {
|
|
1614
|
+
throw new ConfigError(
|
|
1615
|
+
`\u5728 ${file} \u91CC\u65E0\u6CD5\u89E3\u6790 cover.generate\uFF08\u62EC\u53F7\u4E0D\u914D\u5BF9\uFF09`,
|
|
1616
|
+
{
|
|
1617
|
+
hint: "\u628A\u914D\u8272\u624B\u52A8\u586B\u8FDB cover.generate\u3002"
|
|
1618
|
+
}
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
const generatePrefix = body.slice(openBrace, closeBrace + 1);
|
|
1622
|
+
const generateIndent = detectIndent(generatePrefix) ?? `${coverIndent} `;
|
|
1623
|
+
let generateBody = generatePrefix;
|
|
1624
|
+
generateBody = upsertColor(generateBody, "bg", colors.bg, generateIndent);
|
|
1625
|
+
generateBody = upsertColor(
|
|
1626
|
+
generateBody,
|
|
1627
|
+
"bgTo",
|
|
1628
|
+
colors.bgTo,
|
|
1629
|
+
generateIndent
|
|
1630
|
+
);
|
|
1631
|
+
generateBody = upsertColor(
|
|
1632
|
+
generateBody,
|
|
1633
|
+
"accent",
|
|
1634
|
+
colors.accent,
|
|
1635
|
+
generateIndent
|
|
1636
|
+
);
|
|
1637
|
+
generateBody = upsertColor(
|
|
1638
|
+
generateBody,
|
|
1639
|
+
"text",
|
|
1640
|
+
colors.text,
|
|
1641
|
+
generateIndent
|
|
1642
|
+
);
|
|
1643
|
+
return body.slice(0, openBrace) + generateBody + body.slice(closeBrace + 1);
|
|
1644
|
+
};
|
|
1645
|
+
var insertGenerate = (body, colors, indent, file) => {
|
|
1646
|
+
const modeMatch = /mode\s*:\s*(['"`])custom\1,?/.exec(body);
|
|
1647
|
+
if (!modeMatch) {
|
|
1648
|
+
throw new ConfigError(
|
|
1649
|
+
`\u5728 ${file} \u7684 cover \u5BF9\u8C61\u91CC\u627E\u4E0D\u5230 mode: 'custom'\uFF0C\u65E0\u6CD5\u5199\u5165 generate`,
|
|
1650
|
+
{ hint: "\u628A\u914D\u8272\u624B\u52A8\u586B\u8FDB cover.generate\u3002" }
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
const literal = formatColors(colors);
|
|
1654
|
+
const matched = modeMatch[0];
|
|
1655
|
+
const hasComma = /,\s*$/.test(matched);
|
|
1656
|
+
const linePrefix = body.slice(
|
|
1657
|
+
body.lastIndexOf("\n", modeMatch.index) + 1,
|
|
1658
|
+
modeMatch.index
|
|
1659
|
+
);
|
|
1660
|
+
const inline = !/^[ \t]*$/.test(linePrefix);
|
|
1661
|
+
const separator = hasComma ? " " : ", ";
|
|
1662
|
+
const replacement = inline ? `${matched}${separator}generate: { ${literal} },` : `${matched}${hasComma ? "" : ","}
|
|
1663
|
+
${indent}generate: { ${literal} },`;
|
|
1664
|
+
return body.replace(matched, replacement);
|
|
1665
|
+
};
|
|
1666
|
+
var formatColors = (colors) => {
|
|
1667
|
+
const parts = [
|
|
1668
|
+
`bg: '${colors.bg}'`,
|
|
1669
|
+
colors.bgTo === void 0 ? void 0 : `bgTo: '${colors.bgTo}'`,
|
|
1670
|
+
`accent: '${colors.accent}'`,
|
|
1671
|
+
`text: '${colors.text}'`
|
|
1672
|
+
].filter((part) => part !== void 0);
|
|
1673
|
+
return parts.join(", ");
|
|
1674
|
+
};
|
|
1675
|
+
var writeImageId = (body, imageId, file, indent) => {
|
|
1676
|
+
const existing = /imageId\s*:\s*(['"`])(?:[^'"`]*)\1/.exec(body);
|
|
1677
|
+
return existing ? body.replace(existing[0], `imageId: '${imageId}'`) : insertImageId(body, imageId, file, indent);
|
|
1678
|
+
};
|
|
1679
|
+
var insertImageId = (body, imageId, file, indent) => {
|
|
1680
|
+
const modeMatch = /mode\s*:\s*(['"`])custom\1,?/.exec(body);
|
|
1681
|
+
if (!modeMatch) {
|
|
1682
|
+
throw new ConfigError(
|
|
1683
|
+
`\u5728 ${file} \u7684 cover \u5BF9\u8C61\u91CC\u627E\u4E0D\u5230 mode: 'custom'\uFF0C\u65E0\u6CD5\u5199\u56DE imageId`,
|
|
1684
|
+
{ hint: "\u628A imageId \u624B\u52A8\u586B\u8FDB cover.imageId\u3002" }
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1687
|
+
const matched = modeMatch[0];
|
|
1688
|
+
const hasComma = /,\s*$/.test(matched);
|
|
1689
|
+
const linePrefix = body.slice(
|
|
1690
|
+
body.lastIndexOf("\n", modeMatch.index) + 1,
|
|
1691
|
+
modeMatch.index
|
|
1692
|
+
);
|
|
1693
|
+
const inline = !/^[ \t]*$/.test(linePrefix);
|
|
1694
|
+
const separator = hasComma ? " " : ", ";
|
|
1695
|
+
const replacement = inline ? `${matched}${separator}imageId: '${imageId}',` : `${matched}${hasComma ? "" : ","}
|
|
1696
|
+
${indent}imageId: '${imageId}',`;
|
|
1697
|
+
return body.replace(matched, replacement);
|
|
1698
|
+
};
|
|
1699
|
+
var keyPattern = (key) => `\\b${key}\\s*:\\s*(['"\`])(?:[^'"\`]*)\\1`;
|
|
1700
|
+
var detectIndent = (body) => /\n([ \t]+)\S/.exec(body)?.[1];
|
|
1701
|
+
var upsertColor = (body, key, value, indent) => {
|
|
1702
|
+
const existing = new RegExp(keyPattern(key)).exec(body);
|
|
1703
|
+
if (value === void 0) {
|
|
1704
|
+
return existing ? removeKey(body, key) : body;
|
|
1705
|
+
}
|
|
1706
|
+
if (existing) {
|
|
1707
|
+
return body.replace(existing[0], `${key}: '${value}'`);
|
|
1708
|
+
}
|
|
1709
|
+
return insertKey(body, key, value, indent);
|
|
1710
|
+
};
|
|
1711
|
+
var insertKey = (body, key, value, indent) => {
|
|
1712
|
+
const inner = body.slice(1, -1);
|
|
1713
|
+
if (inner.trim() === "") return `{ ${key}: '${value}' }`;
|
|
1714
|
+
if (!inner.includes("\n")) return body.replace("{", `{ ${key}: '${value}',`);
|
|
1715
|
+
const entryIndent = detectIndent(body) ?? indent;
|
|
1716
|
+
return body.replace("{", `{
|
|
1717
|
+
${entryIndent}${key}: '${value}',`);
|
|
1718
|
+
};
|
|
1719
|
+
var removeKey = (body, key) => {
|
|
1720
|
+
const pattern = keyPattern(key);
|
|
1721
|
+
const trailing = new RegExp(`${pattern}\\s*,\\s*`);
|
|
1722
|
+
if (trailing.test(body)) return body.replace(trailing, "");
|
|
1723
|
+
const leading = new RegExp(`\\s*,\\s*${pattern}`);
|
|
1724
|
+
if (leading.test(body)) return body.replace(leading, "");
|
|
1725
|
+
return body.replace(new RegExp(pattern), "");
|
|
1726
|
+
};
|
|
1727
|
+
var findObjectEnd = (content, openBrace) => {
|
|
1728
|
+
let depth = 0;
|
|
1729
|
+
let quote = null;
|
|
1730
|
+
for (let i = openBrace; i < content.length; i += 1) {
|
|
1731
|
+
const char = content[i];
|
|
1732
|
+
const previous = content[i - 1];
|
|
1733
|
+
if (quote) {
|
|
1734
|
+
if (char === quote && previous !== "\\") quote = null;
|
|
1735
|
+
continue;
|
|
1736
|
+
}
|
|
1737
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
1738
|
+
quote = char;
|
|
1739
|
+
continue;
|
|
1740
|
+
}
|
|
1741
|
+
if (char === "{") depth += 1;
|
|
1742
|
+
if (char === "}") {
|
|
1743
|
+
depth -= 1;
|
|
1744
|
+
if (depth === 0) return i;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
return -1;
|
|
1748
|
+
};
|
|
1749
|
+
|
|
1750
|
+
// src/domain/cover-palette.ts
|
|
1751
|
+
var TEXT_CONTRAST = 7;
|
|
1752
|
+
var ACCENT_CONTRAST = 4.5;
|
|
1753
|
+
var BG_ANCHORS = [
|
|
1754
|
+
{ name: "\u6DF1\u84DD", hue: 216 },
|
|
1755
|
+
{ name: "\u975B\u84DD", hue: 232 },
|
|
1756
|
+
{ name: "\u9752\u84DD", hue: 198 },
|
|
1757
|
+
{ name: "\u5E7D\u7EFF", hue: 168 },
|
|
1758
|
+
{ name: "\u677E\u7EFF", hue: 148 },
|
|
1759
|
+
{ name: "\u7D2B\u7F57\u5170", hue: 262 },
|
|
1760
|
+
{ name: "\u6697\u7D2B", hue: 280 },
|
|
1761
|
+
{ name: "\u9152\u7EA2", hue: 348 },
|
|
1762
|
+
{ name: "\u6697\u7EA2", hue: 10 },
|
|
1763
|
+
{ name: "\u6696\u68D5", hue: 26 }
|
|
1764
|
+
];
|
|
1765
|
+
var clamp01 = (value) => Math.min(1, Math.max(0, value));
|
|
1766
|
+
var createRandom = (seed) => {
|
|
1767
|
+
let state = seed >>> 0;
|
|
1768
|
+
return () => {
|
|
1769
|
+
state = state + 1831565813 >>> 0;
|
|
1770
|
+
let value = state;
|
|
1771
|
+
value = Math.imul(value ^ value >>> 15, value | 1);
|
|
1772
|
+
value ^= value + Math.imul(value ^ value >>> 7, value | 61);
|
|
1773
|
+
return ((value ^ value >>> 14) >>> 0) / 4294967296;
|
|
1774
|
+
};
|
|
1775
|
+
};
|
|
1776
|
+
var hueToRgb = (hue, chroma, second) => {
|
|
1777
|
+
if (hue < 60) return [chroma, second, 0];
|
|
1778
|
+
if (hue < 120) return [second, chroma, 0];
|
|
1779
|
+
if (hue < 180) return [0, chroma, second];
|
|
1780
|
+
if (hue < 240) return [0, second, chroma];
|
|
1781
|
+
if (hue < 300) return [second, 0, chroma];
|
|
1782
|
+
return [chroma, 0, second];
|
|
1783
|
+
};
|
|
1784
|
+
var toHexByte = (value) => Math.round(clamp01(value) * 255).toString(16).padStart(2, "0").toUpperCase();
|
|
1785
|
+
var hslToHex = (hue, sat, light) => {
|
|
1786
|
+
const h = (hue % 360 + 360) % 360;
|
|
1787
|
+
const s = clamp01(sat);
|
|
1788
|
+
const l = clamp01(light);
|
|
1789
|
+
const chroma = (1 - Math.abs(2 * l - 1)) * s;
|
|
1790
|
+
const second = chroma * (1 - Math.abs(h / 60 % 2 - 1));
|
|
1791
|
+
const offset = l - chroma / 2;
|
|
1792
|
+
const [r, g, b] = hueToRgb(h, chroma, second);
|
|
1793
|
+
return `#${toHexByte(r + offset)}${toHexByte(g + offset)}${toHexByte(b + offset)}`;
|
|
1794
|
+
};
|
|
1795
|
+
var parseHex = (hex) => {
|
|
1796
|
+
const value = hex.slice(1);
|
|
1797
|
+
const full = value.length === 3 ? [...value].map((char) => char + char).join("") : value.slice(0, 6);
|
|
1798
|
+
return [
|
|
1799
|
+
Number.parseInt(full.slice(0, 2), 16),
|
|
1800
|
+
Number.parseInt(full.slice(2, 4), 16),
|
|
1801
|
+
Number.parseInt(full.slice(4, 6), 16)
|
|
1802
|
+
];
|
|
1803
|
+
};
|
|
1804
|
+
var toLinear = (channel) => {
|
|
1805
|
+
const value = channel / 255;
|
|
1806
|
+
return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
1807
|
+
};
|
|
1808
|
+
var relativeLuminance = (hex) => {
|
|
1809
|
+
const [r, g, b] = parseHex(hex);
|
|
1810
|
+
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
|
|
1811
|
+
};
|
|
1812
|
+
var contrastRatio = (a, b) => {
|
|
1813
|
+
const [high, low] = [relativeLuminance(a), relativeLuminance(b)].sort(
|
|
1814
|
+
(x, y) => y - x
|
|
1815
|
+
);
|
|
1816
|
+
return (high + 0.05) / (low + 0.05);
|
|
1817
|
+
};
|
|
1818
|
+
var describePaletteProblem = (palette) => {
|
|
1819
|
+
const backgrounds = palette.bgTo === void 0 ? [palette.bg] : [palette.bg, palette.bgTo];
|
|
1820
|
+
const requirements = [
|
|
1821
|
+
[palette.text, "\u4E3B\u6807\u9898\u8272", TEXT_CONTRAST],
|
|
1822
|
+
[palette.accent, "\u5F3A\u8C03\u8272", ACCENT_CONTRAST]
|
|
1823
|
+
];
|
|
1824
|
+
for (const [color, label, minimum] of requirements) {
|
|
1825
|
+
for (const bg of backgrounds) {
|
|
1826
|
+
const ratio = contrastRatio(color, bg);
|
|
1827
|
+
if (ratio < minimum) {
|
|
1828
|
+
return `${label} ${color} \u5BF9\u80CC\u666F ${bg} \u7684\u5BF9\u6BD4\u5EA6\u53EA\u6709 ${ratio.toFixed(2)}:1\uFF08\u8981\u6C42 \u2265 ${minimum}:1\uFF09`;
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
return void 0;
|
|
1833
|
+
};
|
|
1834
|
+
var pickRelation = (rand) => {
|
|
1835
|
+
const sign = rand() < 0.5 ? -1 : 1;
|
|
1836
|
+
const roll = rand();
|
|
1837
|
+
if (roll < 0.22) return { name: "\u540C\u8272\u7CFB", offset: sign * (6 + rand() * 20) };
|
|
1838
|
+
if (roll < 0.45)
|
|
1839
|
+
return { name: "\u90BB\u8FD1\u8272", offset: sign * (30 + rand() * 25) };
|
|
1840
|
+
if (roll < 0.7) {
|
|
1841
|
+
return { name: "\u5206\u88C2\u4E92\u8865", offset: sign * (138 + rand() * 24) };
|
|
1842
|
+
}
|
|
1843
|
+
if (roll < 0.88) {
|
|
1844
|
+
return { name: "\u4E92\u8865\u8272", offset: sign * (172 + rand() * 16) };
|
|
1845
|
+
}
|
|
1846
|
+
return { name: "\u4E09\u89D2\u8272", offset: sign * (108 + rand() * 24) };
|
|
1847
|
+
};
|
|
1848
|
+
var fitAccent = (hue, sat, backgrounds, rand) => {
|
|
1849
|
+
const meetsContrast = (light) => {
|
|
1850
|
+
const candidate = hslToHex(hue, sat, light);
|
|
1851
|
+
return backgrounds.every(
|
|
1852
|
+
(bg) => contrastRatio(candidate, bg) >= ACCENT_CONTRAST
|
|
1853
|
+
);
|
|
1854
|
+
};
|
|
1855
|
+
let minimum;
|
|
1856
|
+
for (let light = 0.94; light >= 0.5; light -= 5e-3) {
|
|
1857
|
+
if (meetsContrast(light)) {
|
|
1858
|
+
minimum = light;
|
|
1859
|
+
} else if (minimum !== void 0) {
|
|
1860
|
+
break;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
if (minimum === void 0) return COVER_DEFAULTS.accent;
|
|
1864
|
+
const preferred = minimum + 0.05 + rand() * 0.06;
|
|
1865
|
+
return hslToHex(hue, sat, Math.max(minimum, Math.min(0.9, preferred)));
|
|
1866
|
+
};
|
|
1867
|
+
var randomCoverPalette = (seed) => {
|
|
1868
|
+
const resolvedSeed = (seed ?? Math.floor(Math.random() * 4294967295)) >>> 0;
|
|
1869
|
+
const rand = createRandom(resolvedSeed);
|
|
1870
|
+
const anchor = BG_ANCHORS[Math.floor(rand() * BG_ANCHORS.length)];
|
|
1871
|
+
const bgHue = anchor.hue + (rand() - 0.5) * 16;
|
|
1872
|
+
const bgSat = 0.34 + rand() * 0.2;
|
|
1873
|
+
const bgLight = 0.095 + rand() * 0.07;
|
|
1874
|
+
const bg = hslToHex(bgHue, bgSat, bgLight);
|
|
1875
|
+
const text = hslToHex(
|
|
1876
|
+
bgHue + (rand() - 0.5) * 24,
|
|
1877
|
+
0.06 + rand() * 0.1,
|
|
1878
|
+
0.94 + rand() * 0.03
|
|
1879
|
+
);
|
|
1880
|
+
let bgTo;
|
|
1881
|
+
if (rand() < 0.6) {
|
|
1882
|
+
const toHue = bgHue + (rand() - 0.5) * 40;
|
|
1883
|
+
const toSat = clamp01(bgSat + (rand() - 0.5) * 0.12);
|
|
1884
|
+
let toLight = bgLight + 0.03 + rand() * 0.06;
|
|
1885
|
+
while (toLight > bgLight && contrastRatio(text, hslToHex(toHue, toSat, toLight)) < TEXT_CONTRAST) {
|
|
1886
|
+
toLight -= 0.01;
|
|
1887
|
+
}
|
|
1888
|
+
if (toLight > bgLight + 5e-3) {
|
|
1889
|
+
bgTo = hslToHex(toHue, toSat, toLight);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
const relation = pickRelation(rand);
|
|
1893
|
+
const accentSat = relation.name === "\u540C\u8272\u7CFB" ? 0.6 + rand() * 0.3 : 0.72 + rand() * 0.28;
|
|
1894
|
+
const accent = fitAccent(
|
|
1895
|
+
bgHue + relation.offset,
|
|
1896
|
+
accentSat,
|
|
1897
|
+
bgTo === void 0 ? [bg] : [bg, bgTo],
|
|
1898
|
+
rand
|
|
1899
|
+
);
|
|
1900
|
+
return {
|
|
1901
|
+
palette: { bg, bgTo, accent, text },
|
|
1902
|
+
seed: resolvedSeed,
|
|
1903
|
+
scheme: `${anchor.name} \xB7 ${relation.name}`
|
|
1904
|
+
};
|
|
1905
|
+
};
|
|
1906
|
+
var renderCoverPng = async (spec) => sharp(Buffer.from(buildCoverSvg(spec), "utf8")).png().toBuffer();
|
|
1907
|
+
var isInteractive = (stdin) => Boolean(stdin.isTTY) && typeof stdin.setRawMode === "function";
|
|
1908
|
+
var promptHidden = async (question, io) => {
|
|
1909
|
+
const { stdin, stderr } = io;
|
|
1910
|
+
if (!isInteractive(stdin)) {
|
|
1911
|
+
const rl = createInterface({ input: stdin, terminal: false });
|
|
1912
|
+
const answer = await new Promise((resolve8) => {
|
|
1913
|
+
rl.question(question, resolve8);
|
|
1914
|
+
});
|
|
1915
|
+
rl.close();
|
|
1916
|
+
return answer.trim();
|
|
1917
|
+
}
|
|
1918
|
+
stderr(question);
|
|
1919
|
+
const wasRaw = Boolean(stdin.isRaw);
|
|
1920
|
+
stdin.setRawMode?.(true);
|
|
1921
|
+
stdin.resume();
|
|
1922
|
+
return new Promise((resolve8, reject) => {
|
|
1923
|
+
const chars = [];
|
|
1924
|
+
const cleanup = () => {
|
|
1925
|
+
stdin.setRawMode?.(wasRaw);
|
|
1926
|
+
stdin.removeListener("data", onData);
|
|
1927
|
+
stdin.pause();
|
|
1928
|
+
stderr("\n");
|
|
1929
|
+
};
|
|
1930
|
+
const onData = (chunk) => {
|
|
1931
|
+
for (const char of chunk.toString("utf8")) {
|
|
1932
|
+
if (char === "\r" || char === "\n") {
|
|
1933
|
+
cleanup();
|
|
1934
|
+
resolve8(chars.join(""));
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
if (char === "") {
|
|
1938
|
+
cleanup();
|
|
1939
|
+
reject(new CanceledError());
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
if (char === "\x7F" || char === "\b") {
|
|
1943
|
+
chars.pop();
|
|
1944
|
+
continue;
|
|
1945
|
+
}
|
|
1946
|
+
chars.push(char);
|
|
1947
|
+
}
|
|
1948
|
+
};
|
|
1949
|
+
stdin.on("data", onData);
|
|
1950
|
+
});
|
|
1951
|
+
};
|
|
1952
|
+
var promptVisible = async (question, io) => {
|
|
1953
|
+
const rl = createInterface({ input: io.stdin, terminal: false });
|
|
1954
|
+
const answer = await new Promise((resolve8) => {
|
|
1955
|
+
rl.question(question, resolve8);
|
|
1956
|
+
});
|
|
1957
|
+
rl.close();
|
|
1958
|
+
return answer.trim();
|
|
1959
|
+
};
|
|
1960
|
+
var readAllStdin = async (stdin) => {
|
|
1961
|
+
const chunks = [];
|
|
1962
|
+
for await (const chunk of stdin) {
|
|
1963
|
+
chunks.push(Buffer.from(chunk));
|
|
1964
|
+
}
|
|
1965
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
1966
|
+
};
|
|
1967
|
+
var confirm = async (question, io, defaultValue = false) => {
|
|
1968
|
+
if (!io.stdin.isTTY) {
|
|
1969
|
+
throw new UsageError("\u5F53\u524D\u4E0D\u662F\u4EA4\u4E92\u5F0F\u7EC8\u7AEF\uFF0C\u65E0\u6CD5\u786E\u8BA4", {
|
|
1970
|
+
hint: "\u8BF7\u5728\u547D\u4EE4\u4E2D\u663E\u5F0F\u52A0\u4E0A --yes \u8868\u793A\u786E\u8BA4\uFF0C\u6216\u4F7F\u7528 --dry-run \u9884\u89C8\u3002"
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
const rl = createInterface({ input: io.stdin, terminal: false });
|
|
1974
|
+
const answer = await new Promise((resolve8) => {
|
|
1975
|
+
rl.question(`${question} ${defaultValue ? "[Y/n]" : "[y/N]"} `, resolve8);
|
|
1976
|
+
});
|
|
1977
|
+
rl.close();
|
|
1978
|
+
const normalized = answer.trim().toLowerCase();
|
|
1979
|
+
if (normalized === "") return defaultValue;
|
|
1980
|
+
return normalized === "y" || normalized === "yes";
|
|
1981
|
+
};
|
|
1982
|
+
var select = async (message, choices, io, options = {}) => {
|
|
1983
|
+
if (!isInteractive(io.stdin)) {
|
|
1984
|
+
throw new UsageError("\u5F53\u524D\u4E0D\u662F\u4EA4\u4E92\u5F0F\u7EC8\u7AEF\uFF0C\u65E0\u6CD5\u9009\u62E9", {
|
|
1985
|
+
hint: "\u8BF7\u5728\u547D\u4EE4\u91CC\u663E\u5F0F\u6307\u5B9A\uFF0C\u4F8B\u5982 `qpc init --type backend_service`\u3002"
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
const answer = await select$1({
|
|
1989
|
+
message,
|
|
1990
|
+
options: choices.map(
|
|
1991
|
+
(choice) => ({
|
|
1992
|
+
value: choice.value,
|
|
1993
|
+
label: choice.label,
|
|
1994
|
+
...choice.hint === void 0 ? {} : { hint: choice.hint }
|
|
1995
|
+
})
|
|
1996
|
+
),
|
|
1997
|
+
...options.initialValue === void 0 ? {} : { initialValue: options.initialValue },
|
|
1998
|
+
input: io.stdin,
|
|
1999
|
+
output: io.stderrStream ?? process.stderr
|
|
2000
|
+
});
|
|
2001
|
+
if (isCancel(answer)) {
|
|
2002
|
+
throw new CanceledError("\u5DF2\u53D6\u6D88\u9009\u62E9");
|
|
2003
|
+
}
|
|
2004
|
+
return answer;
|
|
2005
|
+
};
|
|
2006
|
+
var multiselect = async (message, choices, io, options = {}) => {
|
|
2007
|
+
if (!isInteractive(io.stdin)) {
|
|
2008
|
+
throw new UsageError("\u5F53\u524D\u4E0D\u662F\u4EA4\u4E92\u5F0F\u7EC8\u7AEF\uFF0C\u65E0\u6CD5\u591A\u9009", {
|
|
2009
|
+
hint: "\u8BF7\u5728\u547D\u4EE4\u91CC\u663E\u5F0F\u6307\u5B9A\uFF0C\u4F8B\u5982 `qpc tag --set 1,2`\u3002"
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
const answer = await multiselect$1({
|
|
2013
|
+
message,
|
|
2014
|
+
options: choices.map(
|
|
2015
|
+
(choice) => ({
|
|
2016
|
+
value: choice.value,
|
|
2017
|
+
label: choice.label,
|
|
2018
|
+
...choice.hint === void 0 ? {} : { hint: choice.hint }
|
|
2019
|
+
})
|
|
2020
|
+
),
|
|
2021
|
+
...options.initialValues === void 0 ? {} : { initialValues: options.initialValues },
|
|
2022
|
+
...options.required === void 0 ? {} : { required: options.required },
|
|
2023
|
+
input: io.stdin,
|
|
2024
|
+
output: io.stderrStream ?? process.stderr
|
|
2025
|
+
});
|
|
2026
|
+
if (isCancel(answer)) {
|
|
2027
|
+
throw new CanceledError("\u5DF2\u53D6\u6D88\u9009\u62E9");
|
|
2028
|
+
}
|
|
2029
|
+
return answer;
|
|
2030
|
+
};
|
|
2031
|
+
|
|
2032
|
+
// src/commands/support.ts
|
|
2033
|
+
var requireSession = (ctx) => {
|
|
2034
|
+
if (!ctx.client.hasSession) {
|
|
2035
|
+
throw new AuthError("\u5C1A\u672A\u767B\u5F55", {
|
|
2036
|
+
hint: `\u5148\u6267\u884C \`qpc login --api ${ctx.env.apiUrl}\`\uFF08\u6216\u8BBE\u7F6E QUANTA_TOKEN\uFF09\u3002`
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2039
|
+
};
|
|
2040
|
+
var summarizeUser = (user) => [user.nickname, user.email, user.role].filter(Boolean).join(" \xB7 ") || "<unknown>";
|
|
2041
|
+
var confirmOrSkip = async (ctx, question) => {
|
|
2042
|
+
if (ctx.assumeYes) return true;
|
|
2043
|
+
if (!ctx.runtime.stdin.isTTY) {
|
|
2044
|
+
ctx.logger.detail("\u975E\u4EA4\u4E92\u73AF\u5883\uFF0C\u5DF2\u8DF3\u8FC7\u786E\u8BA4\uFF08--yes \u53EF\u663E\u5F0F\u58F0\u660E\uFF09");
|
|
2045
|
+
return true;
|
|
2046
|
+
}
|
|
2047
|
+
return confirm(question, {
|
|
2048
|
+
stdin: ctx.runtime.stdin,
|
|
2049
|
+
stderr: ctx.runtime.stderr
|
|
2050
|
+
});
|
|
2051
|
+
};
|
|
2052
|
+
|
|
2053
|
+
// src/commands/cover.ts
|
|
2054
|
+
var registerCoverCommand = (registry) => {
|
|
2055
|
+
const { program, context } = registry;
|
|
2056
|
+
program.command("cover").description("\u751F\u6210\u5C01\u9762\u56FE\uFF08\u53EF\u4E0A\u4F20\u3001\u5199\u56DE\u914D\u7F6E\uFF09").argument("[dir]", "\u9898\u76EE\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09", ".").option("--out <file>", "\u8F93\u51FA\u6587\u4EF6\uFF08\u9ED8\u8BA4\u53D6\u914D\u7F6E\u91CC\u7684 cover.generate.out\uFF09").option("--bg <color>", `\u80CC\u666F\u8272\uFF0C\u9ED8\u8BA4 ${COVER_DEFAULTS.bg}`).option(
|
|
2057
|
+
"--bg-to <color>",
|
|
2058
|
+
`\u6E10\u53D8\u7EC8\u6B62\u8272\uFF08\u7ED9\u4E86\u5C31\u753B\u6E10\u53D8\uFF09\uFF0C\u63A8\u8350 ${COVER_SUGGESTED_BG_TO}`
|
|
2059
|
+
).option("--accent <color>", `\u5F3A\u8C03\u8272\uFF0C\u9ED8\u8BA4 ${COVER_DEFAULTS.accent}`).option("--text <color>", `\u4E3B\u6807\u9898\u989C\u8272\uFF0C\u9ED8\u8BA4 ${COVER_DEFAULTS.text}`).option("--subtitle <text>", "\u526F\u6807\u9898\uFF08\u7559\u7A7A\u5219\u4E0D\u6E32\u67D3\uFF09").option("--footer <text>", `\u5DE6\u4E0B\u89D2\u9875\u811A\uFF0C\u9ED8\u8BA4 ${COVER_DEFAULTS.footer}`).option("--width <px>", `\u753B\u5E03\u5BBD\u5EA6\uFF0C\u9ED8\u8BA4 ${COVER_DEFAULTS.width}`).option("--height <px>", `\u753B\u5E03\u9AD8\u5EA6\uFF0C\u9ED8\u8BA4 ${COVER_DEFAULTS.height}`).option("--random", "\u968F\u673A\u751F\u6210\u4E00\u5957\u5408\u7406\u914D\u8272\uFF08\u53EF\u590D\u73B0\uFF09").option("--seed <n>", "\u914D\u5408 --random\uFF1A\u56FA\u5B9A\u79CD\u5B50").option("--upload", "\u4E0A\u4F20\u5C01\u9762\uFF0C\u8F93\u51FA imageId").option("--write", "\u628A imageId \u4E0E\u914D\u8272\u5199\u56DE\u914D\u7F6E\uFF08\u9690\u542B --upload\uFF09").action(async (dir, options) => {
|
|
2060
|
+
const ctx = await context();
|
|
2061
|
+
await runCover(ctx, { dir, ...options });
|
|
2062
|
+
});
|
|
2063
|
+
};
|
|
2064
|
+
var runCover = async (ctx, options) => {
|
|
2065
|
+
const rootDir = resolve(ctx.cwd, options.dir);
|
|
2066
|
+
const config = await loadProblemConfig(rootDir);
|
|
2067
|
+
if (config.cover.mode !== "custom") {
|
|
2068
|
+
throw new ConfigError(
|
|
2069
|
+
"\u5F53\u524D\u9898\u76EE\u7528\u7684\u662F\u300C\u9996\u5C4F\u622A\u56FE\u300D\u5C01\u9762\uFF0C\u4E0D\u9700\u8981\u4E5F\u4E0D\u5E94\u8BE5\u751F\u6210\u5C01\u9762\u56FE",
|
|
2070
|
+
{
|
|
2071
|
+
hint: "\u6539\u6210 cover: { mode: 'custom', generate: { ... } } \u518D\u8DD1 `qpc cover`\u3002"
|
|
2072
|
+
}
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2075
|
+
const generate = config.cover.generate;
|
|
2076
|
+
const outFile = resolve(
|
|
2077
|
+
rootDir,
|
|
2078
|
+
options.out ?? generate?.out ?? "cover.png"
|
|
2079
|
+
);
|
|
2080
|
+
const randomized = resolveRandom(options);
|
|
2081
|
+
const spec = resolveSpec(
|
|
2082
|
+
config.title,
|
|
2083
|
+
generate,
|
|
2084
|
+
options,
|
|
2085
|
+
randomized?.palette
|
|
2086
|
+
);
|
|
2087
|
+
if (randomized) {
|
|
2088
|
+
const problem = describePaletteProblem({
|
|
2089
|
+
bg: spec.bg,
|
|
2090
|
+
bgTo: spec.bgTo,
|
|
2091
|
+
accent: spec.accent,
|
|
2092
|
+
text: spec.text
|
|
2093
|
+
});
|
|
2094
|
+
if (problem) {
|
|
2095
|
+
throw new UsageError(`\u968F\u673A\u914D\u8272\u4E0E\u663E\u5F0F\u989C\u8272\u53C2\u6570\u51B2\u7A81\uFF1A${problem}`, {
|
|
2096
|
+
hint: "\u53BB\u6389\u989C\u8272\u53C2\u6570\u8BA9 --random \u6574\u7EC4\u751F\u6210\uFF0C\u6216\u6574\u7EC4\u624B\u52A8\u6307\u5B9A\u3002"
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
const colors = randomized ? {
|
|
2101
|
+
bg: spec.bg,
|
|
2102
|
+
bgTo: spec.bgTo,
|
|
2103
|
+
accent: spec.accent,
|
|
2104
|
+
text: spec.text
|
|
2105
|
+
} : void 0;
|
|
2106
|
+
ctx.logger.step(`\u6E32\u67D3\u5C01\u9762 \u2192 ${relative(ctx.cwd, outFile) || outFile}`);
|
|
2107
|
+
const png = await renderCoverPng(spec);
|
|
2108
|
+
try {
|
|
2109
|
+
await writeBinary(outFile, png);
|
|
2110
|
+
} catch (error) {
|
|
2111
|
+
throw new ConfigError(`\u65E0\u6CD5\u5199\u5165\u5C01\u9762\u6587\u4EF6\uFF1A${outFile}`, { cause: error });
|
|
2112
|
+
}
|
|
2113
|
+
const rows = [
|
|
2114
|
+
["\u6807\u9898", spec.title],
|
|
2115
|
+
["\u526F\u6807\u9898", spec.subtitle || "<\u65E0>"],
|
|
2116
|
+
["\u5C3A\u5BF8", `${spec.width} \xD7 ${spec.height}`],
|
|
2117
|
+
[
|
|
2118
|
+
"\u914D\u8272",
|
|
2119
|
+
`${spec.bg}${spec.bgTo ? ` \u2192 ${spec.bgTo}` : ""} / ${spec.accent}`
|
|
2120
|
+
]
|
|
2121
|
+
];
|
|
2122
|
+
if (randomized) {
|
|
2123
|
+
rows.push(["\u968F\u673A", `${randomized.scheme}\uFF08seed=${randomized.seed}\uFF09`]);
|
|
2124
|
+
}
|
|
2125
|
+
rows.push(["\u6587\u4EF6", `${outFile}\uFF08${formatBytes(png.byteLength)}\uFF09`]);
|
|
2126
|
+
ctx.logger.table(rows);
|
|
2127
|
+
ctx.logger.success("\u5C01\u9762\u5DF2\u751F\u6210");
|
|
2128
|
+
const shouldUpload = options.upload === true || options.write === true;
|
|
2129
|
+
if (!shouldUpload) {
|
|
2130
|
+
if (ctx.logger.json) {
|
|
2131
|
+
ctx.logger.result({
|
|
2132
|
+
out: outFile,
|
|
2133
|
+
bytes: png.byteLength,
|
|
2134
|
+
spec,
|
|
2135
|
+
random: randomized ? { seed: randomized.seed, scheme: randomized.scheme } : void 0
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
requireSession(ctx);
|
|
2141
|
+
const api = createAdminApi(ctx.client);
|
|
2142
|
+
ctx.logger.step("\u4E0A\u4F20\u5C01\u9762\uFF08admin.image.upload\uFF09");
|
|
2143
|
+
const image = await api.uploadImage({
|
|
2144
|
+
fileBase64: png.toString("base64"),
|
|
2145
|
+
fileName: basename(outFile)
|
|
2146
|
+
});
|
|
2147
|
+
ctx.logger.success(`\u5C01\u9762\u5DF2\u4E0A\u4F20\uFF1A${image.id}`);
|
|
2148
|
+
if (options.write) {
|
|
2149
|
+
const written = await writeBackCover(config.configFile, image.id, colors);
|
|
2150
|
+
ctx.logger.success(
|
|
2151
|
+
`${colors ? "imageId \u4E0E\u914D\u8272" : "imageId"} \u5DF2\u5199\u56DE ${relative(ctx.cwd, written) || written}`
|
|
2152
|
+
);
|
|
2153
|
+
} else {
|
|
2154
|
+
ctx.logger.info(" \u52A0 --write \u53EF\u5199\u56DE\u914D\u7F6E");
|
|
2155
|
+
}
|
|
2156
|
+
if (ctx.logger.json) {
|
|
2157
|
+
ctx.logger.result({
|
|
2158
|
+
out: outFile,
|
|
2159
|
+
bytes: png.byteLength,
|
|
2160
|
+
imageId: image.id,
|
|
2161
|
+
imageName: image.name,
|
|
2162
|
+
spec,
|
|
2163
|
+
random: randomized ? { seed: randomized.seed, scheme: randomized.scheme } : void 0,
|
|
2164
|
+
wroteColors: options.write === true && colors !== void 0,
|
|
2165
|
+
configFile: options.write ? config.configFile : void 0
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
};
|
|
2169
|
+
var writeBackCover = async (configFile, imageId, colors) => {
|
|
2170
|
+
const original = await readText(configFile);
|
|
2171
|
+
const patched = applyCoverUpdate(original, { imageId, colors }, configFile);
|
|
2172
|
+
await writeTextAtomic(configFile, patched);
|
|
2173
|
+
try {
|
|
2174
|
+
const reloaded = await loadProblemConfig(resolve(configFile, ".."));
|
|
2175
|
+
if (reloaded.cover.mode !== "custom" || reloaded.cover.imageId !== imageId) {
|
|
2176
|
+
throw new Error("\u5199\u56DE\u540E\u8BFB\u53D6\u5230\u7684 imageId \u4E0E\u9884\u671F\u4E0D\u4E00\u81F4");
|
|
2177
|
+
}
|
|
2178
|
+
if (colors) {
|
|
2179
|
+
const generate = reloaded.cover.generate;
|
|
2180
|
+
const matches = generate !== void 0 && generate.bg === colors.bg && generate.bgTo === colors.bgTo && generate.accent === colors.accent && generate.text === colors.text;
|
|
2181
|
+
if (!matches) throw new Error("\u5199\u56DE\u540E\u8BFB\u53D6\u5230\u7684\u914D\u8272\u4E0E\u9884\u671F\u4E0D\u4E00\u81F4");
|
|
2182
|
+
}
|
|
2183
|
+
} catch (error) {
|
|
2184
|
+
await writeTextAtomic(configFile, original);
|
|
2185
|
+
throw new ConfigError("\u5199\u56DE\u5C01\u9762\u4FE1\u606F\u540E\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF0C\u5DF2\u56DE\u6EDA\u539F\u6587\u4EF6", {
|
|
2186
|
+
cause: error,
|
|
2187
|
+
hint: `\u624B\u52A8\u628A imageId \u586B\u6210 ${imageId}\uFF0C\u5E76\u6284\u56DE\u914D\u8272\u3002`
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
return configFile;
|
|
2191
|
+
};
|
|
2192
|
+
var resolveRandom = (options) => {
|
|
2193
|
+
if (!options.random) {
|
|
2194
|
+
if (options.seed !== void 0) {
|
|
2195
|
+
throw new UsageError("--seed \u53EA\u6709\u5728 --random \u65F6\u624D\u6709\u610F\u4E49", {
|
|
2196
|
+
hint: "\u4F8B\u5982 `qpc cover --random --seed 42`\u3002"
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
return void 0;
|
|
2200
|
+
}
|
|
2201
|
+
return randomCoverPalette(parseSeed(options.seed));
|
|
2202
|
+
};
|
|
2203
|
+
var parseSeed = (value) => {
|
|
2204
|
+
if (value === void 0) return void 0;
|
|
2205
|
+
if (!/^\d+$/.test(value)) {
|
|
2206
|
+
throw new UsageError(`--seed \u5FC5\u987B\u662F\u975E\u8D1F\u6574\u6570\uFF1A${value}`, {
|
|
2207
|
+
hint: "\u4F8B\u5982 `qpc cover --random --seed 42`\u3002"
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
const seed = Number.parseInt(value, 10);
|
|
2211
|
+
if (!Number.isSafeInteger(seed) || seed > 4294967295) {
|
|
2212
|
+
throw new UsageError(`--seed \u8D85\u51FA\u8303\u56F4\uFF080~4294967295\uFF09\uFF1A${value}`);
|
|
2213
|
+
}
|
|
2214
|
+
return seed;
|
|
2215
|
+
};
|
|
2216
|
+
var resolveSpec = (title, generate, options, palette) => {
|
|
2217
|
+
const pick = (flag, fromConfig) => flag ?? fromConfig;
|
|
2218
|
+
const color = (value, fallback, field) => assertHexColor(value ?? fallback, field);
|
|
2219
|
+
const colorFrom = (flag, fromRandom, fromConfig) => flag ?? (palette === void 0 ? fromConfig : fromRandom);
|
|
2220
|
+
const bgTo = colorFrom(options.bgTo, palette?.bgTo, generate?.bgTo);
|
|
2221
|
+
return {
|
|
2222
|
+
title,
|
|
2223
|
+
subtitle: pick(options.subtitle, generate?.subtitle),
|
|
2224
|
+
footer: pick(options.footer, generate?.footer),
|
|
2225
|
+
bg: color(
|
|
2226
|
+
colorFrom(options.bg, palette?.bg, generate?.bg),
|
|
2227
|
+
COVER_DEFAULTS.bg,
|
|
2228
|
+
"--bg"
|
|
2229
|
+
),
|
|
2230
|
+
bgTo: bgTo === void 0 ? void 0 : color(bgTo, COVER_DEFAULTS.bg, "--bg-to"),
|
|
2231
|
+
accent: color(
|
|
2232
|
+
colorFrom(options.accent, palette?.accent, generate?.accent),
|
|
2233
|
+
COVER_DEFAULTS.accent,
|
|
2234
|
+
"--accent"
|
|
2235
|
+
),
|
|
2236
|
+
text: color(
|
|
2237
|
+
colorFrom(options.text, palette?.text, generate?.text),
|
|
2238
|
+
COVER_DEFAULTS.text,
|
|
2239
|
+
"--text"
|
|
2240
|
+
),
|
|
2241
|
+
width: parseSize(
|
|
2242
|
+
options.width,
|
|
2243
|
+
generate?.width,
|
|
2244
|
+
COVER_DEFAULTS.width,
|
|
2245
|
+
"--width"
|
|
2246
|
+
),
|
|
2247
|
+
height: parseSize(
|
|
2248
|
+
options.height,
|
|
2249
|
+
generate?.height,
|
|
2250
|
+
COVER_DEFAULTS.height,
|
|
2251
|
+
"--height"
|
|
2252
|
+
)
|
|
2253
|
+
};
|
|
2254
|
+
};
|
|
2255
|
+
var parseSize = (flag, fromConfig, fallback, field) => {
|
|
2256
|
+
if (flag === void 0) return fromConfig ?? fallback;
|
|
2257
|
+
const value = Number.parseInt(flag, 10);
|
|
2258
|
+
if (!Number.isFinite(value) || value < 200 || value > 4096) {
|
|
2259
|
+
throw new UsageError(`${field} \u5FC5\u987B\u662F 200~4096 \u4E4B\u95F4\u7684\u6574\u6570\uFF1A${flag}`);
|
|
2260
|
+
}
|
|
2261
|
+
return value;
|
|
2262
|
+
};
|
|
2263
|
+
|
|
2264
|
+
// src/utils/jwt.ts
|
|
2265
|
+
var decodeJwt = (token) => {
|
|
2266
|
+
const segments = token.split(".");
|
|
2267
|
+
if (segments.length < 2 || !segments[1]) return void 0;
|
|
2268
|
+
try {
|
|
2269
|
+
const payload = Buffer.from(
|
|
2270
|
+
segments[1].replace(/-/g, "+").replace(/_/g, "/"),
|
|
2271
|
+
"base64"
|
|
2272
|
+
).toString("utf8");
|
|
2273
|
+
const parsed = JSON.parse(payload);
|
|
2274
|
+
return typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
2275
|
+
} catch {
|
|
2276
|
+
return void 0;
|
|
2277
|
+
}
|
|
2278
|
+
};
|
|
2279
|
+
var describeTokenExpiry = (token, now = Date.now()) => {
|
|
2280
|
+
if (!token) return "\u65E0";
|
|
2281
|
+
const claims = decodeJwt(token);
|
|
2282
|
+
if (!claims?.exp) return "\u672A\u77E5\uFF08\u65E0\u6CD5\u89E3\u6790 exp\uFF09";
|
|
2283
|
+
const expiresAt = claims.exp * 1e3;
|
|
2284
|
+
const remainingMs = expiresAt - now;
|
|
2285
|
+
const stamp = new Date(expiresAt).toISOString();
|
|
2286
|
+
if (remainingMs <= 0) return `\u5DF2\u8FC7\u671F\uFF08${stamp}\uFF09`;
|
|
2287
|
+
if (remainingMs < 6e4)
|
|
2288
|
+
return `${Math.round(remainingMs / 1e3)} \u79D2\u540E\u8FC7\u671F\uFF08${stamp}\uFF09`;
|
|
2289
|
+
return `${Math.round(remainingMs / 6e4)} \u5206\u949F\u540E\u8FC7\u671F\uFF08${stamp}\uFF09`;
|
|
2290
|
+
};
|
|
2291
|
+
|
|
2292
|
+
// src/commands/doctor.ts
|
|
2293
|
+
var MIN_NODE_MAJOR = 20;
|
|
2294
|
+
var registerDoctorCommand = (registry) => {
|
|
2295
|
+
const { program, context } = registry;
|
|
2296
|
+
program.command("doctor").description("\u81EA\u68C0\uFF1A\u8FD0\u884C\u73AF\u5883\u3001API \u53EF\u8FBE\u6027\u3001\u767B\u5F55\u72B6\u6001").action(async () => {
|
|
2297
|
+
const ctx = await context();
|
|
2298
|
+
await runDoctor(ctx);
|
|
2299
|
+
});
|
|
2300
|
+
};
|
|
2301
|
+
var runDoctor = async (ctx) => {
|
|
2302
|
+
const results = [];
|
|
2303
|
+
results.push(checkNodeVersion());
|
|
2304
|
+
const credentials = await checkCredentialsFile(ctx);
|
|
2305
|
+
results.push(credentials);
|
|
2306
|
+
results.push(await checkApiReachable(ctx));
|
|
2307
|
+
results.push(await checkSession(ctx));
|
|
2308
|
+
const configFile = DEFAULT_CONFIG_FILES.map(
|
|
2309
|
+
(name) => existsSync(join(ctx.cwd, name))
|
|
2310
|
+
).some(Boolean);
|
|
2311
|
+
results.push({
|
|
2312
|
+
name: "\u5F53\u524D\u76EE\u5F55\u7684\u9898\u76EE\u914D\u7F6E",
|
|
2313
|
+
status: configFile ? "ok" : "skip",
|
|
2314
|
+
detail: configFile ? "\u627E\u5230 problem.config.*" : `\u5728 ${ctx.cwd} \u672A\u627E\u5230\uFF08\u7528 --cwd \u6307\u5B9A\u9898\u76EE\u76EE\u5F55\uFF09`
|
|
2315
|
+
});
|
|
2316
|
+
ctx.logger.step("qpc doctor");
|
|
2317
|
+
const symbols = {
|
|
2318
|
+
ok: ctx.logger.colors.green("\u2714"),
|
|
2319
|
+
warn: ctx.logger.colors.yellow("\u25B2"),
|
|
2320
|
+
fail: ctx.logger.colors.red("\u2716"),
|
|
2321
|
+
skip: ctx.logger.colors.dim("\u2013")
|
|
2322
|
+
};
|
|
2323
|
+
for (const result of results) {
|
|
2324
|
+
ctx.logger.info(
|
|
2325
|
+
` ${symbols[result.status]} ${result.name}\uFF1A${result.detail}`
|
|
2326
|
+
);
|
|
2327
|
+
if (result.fix && result.status !== "ok")
|
|
2328
|
+
ctx.logger.detail(`\u4FEE\u590D\uFF1A${result.fix}`);
|
|
2329
|
+
}
|
|
2330
|
+
const failures = results.filter((result) => result.status === "fail");
|
|
2331
|
+
if (ctx.logger.json) {
|
|
2332
|
+
ctx.logger.result({
|
|
2333
|
+
ok: failures.length === 0,
|
|
2334
|
+
checks: results,
|
|
2335
|
+
apiUrl: ctx.env.apiUrl,
|
|
2336
|
+
credentialsFile: ctx.credentialsFile
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
if (failures.length > 0) {
|
|
2340
|
+
throw new CliError(`${failures.length} \u9879\u68C0\u67E5\u5931\u8D25`, {
|
|
2341
|
+
hint: '\u6309\u4E0A\u9762\u7684"\u4FEE\u590D"\u9010\u9879\u5904\u7406\uFF1B\u53EA\u5173\u5FC3\u9898\u76EE\u672C\u8EAB\u65F6\u53EF\u4EE5\u5148\u8DD1 `qpc check`\u3002'
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
ctx.logger.success("\u5168\u90E8\u68C0\u67E5\u901A\u8FC7");
|
|
2345
|
+
};
|
|
2346
|
+
var checkNodeVersion = () => {
|
|
2347
|
+
const major = Number.parseInt(
|
|
2348
|
+
process.versions.node.split(".")[0] ?? "0",
|
|
2349
|
+
10
|
|
2350
|
+
);
|
|
2351
|
+
return {
|
|
2352
|
+
name: "Node \u7248\u672C",
|
|
2353
|
+
status: major >= MIN_NODE_MAJOR ? "ok" : "fail",
|
|
2354
|
+
detail: `v${process.versions.node}`,
|
|
2355
|
+
fix: `\u5347\u7EA7\u5230 Node ${MIN_NODE_MAJOR} \u53CA\u4EE5\u4E0A`
|
|
2356
|
+
};
|
|
2357
|
+
};
|
|
2358
|
+
var checkCredentialsFile = async (ctx) => {
|
|
2359
|
+
if (!existsSync(ctx.credentialsFile)) {
|
|
2360
|
+
return {
|
|
2361
|
+
name: "\u51ED\u636E\u6587\u4EF6",
|
|
2362
|
+
status: ctx.client.hasSession ? "ok" : "warn",
|
|
2363
|
+
detail: ctx.client.hasSession ? "\u4E0D\u5B58\u5728\uFF0C\u4F46\u5DF2\u901A\u8FC7\u73AF\u5883\u53D8\u91CF\u63D0\u4F9B\u4EE4\u724C" : `\u4E0D\u5B58\u5728\uFF1A${ctx.credentialsFile}`,
|
|
2364
|
+
fix: "qpc login"
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
const info = await stat(ctx.credentialsFile).catch(() => void 0);
|
|
2368
|
+
if (!info) {
|
|
2369
|
+
return {
|
|
2370
|
+
name: "\u51ED\u636E\u6587\u4EF6",
|
|
2371
|
+
status: "warn",
|
|
2372
|
+
detail: `\u65E0\u6CD5\u8BFB\u53D6\uFF1A${ctx.credentialsFile}`,
|
|
2373
|
+
fix: "qpc login"
|
|
2374
|
+
};
|
|
2375
|
+
}
|
|
2376
|
+
const mode = (info.mode & 511).toString(8);
|
|
2377
|
+
const tooOpen = (info.mode & 63) !== 0;
|
|
2378
|
+
return {
|
|
2379
|
+
name: "\u51ED\u636E\u6587\u4EF6",
|
|
2380
|
+
status: tooOpen ? "warn" : "ok",
|
|
2381
|
+
detail: `${ctx.credentialsFile}\uFF08\u6743\u9650 ${mode}\uFF09`,
|
|
2382
|
+
fix: tooOpen ? `chmod 600 ${ctx.credentialsFile}` : void 0
|
|
2383
|
+
};
|
|
2384
|
+
};
|
|
2385
|
+
var checkApiReachable = async (ctx) => {
|
|
2386
|
+
try {
|
|
2387
|
+
await ctx.client.call("auth.login.getUser", { method: "GET" });
|
|
2388
|
+
return {
|
|
2389
|
+
name: "API \u53EF\u8FBE",
|
|
2390
|
+
status: "ok",
|
|
2391
|
+
detail: `${ctx.env.apiUrl}\uFF08\u5DF2\u767B\u5F55\uFF09`
|
|
2392
|
+
};
|
|
2393
|
+
} catch (error) {
|
|
2394
|
+
if (error instanceof ApiError && (error.status === 401 || error.status === 403)) {
|
|
2395
|
+
return {
|
|
2396
|
+
name: "API \u53EF\u8FBE",
|
|
2397
|
+
status: "ok",
|
|
2398
|
+
detail: `${ctx.env.apiUrl}\uFF08\u672A\u767B\u5F55/\u65E0\u6743\u9650\uFF0C\u4F46\u670D\u52A1\u6B63\u5E38\uFF09`
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
return {
|
|
2402
|
+
name: "API \u53EF\u8FBE",
|
|
2403
|
+
status: "fail",
|
|
2404
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
2405
|
+
fix: "\u786E\u8BA4 Web \u5E94\u7528\u5DF2\u542F\u52A8\uFF08docker compose up -d\uFF09\uFF0C\u6216\u7528 --api \u6307\u5B9A\u5730\u5740"
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
};
|
|
2409
|
+
var checkSession = async (ctx) => {
|
|
2410
|
+
if (!ctx.client.hasSession) {
|
|
2411
|
+
return {
|
|
2412
|
+
name: "\u767B\u5F55\u72B6\u6001",
|
|
2413
|
+
status: "warn",
|
|
2414
|
+
detail: "\u672A\u767B\u5F55",
|
|
2415
|
+
fix: `qpc login --api ${ctx.env.apiUrl}`
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
try {
|
|
2419
|
+
const user = await ctx.client.call(
|
|
2420
|
+
"auth.login.getUser",
|
|
2421
|
+
{ method: "GET" }
|
|
2422
|
+
);
|
|
2423
|
+
const role = user.user.role;
|
|
2424
|
+
const admin = role === "ADMIN" || role === "SUPER_ADMIN";
|
|
2425
|
+
return {
|
|
2426
|
+
name: "\u767B\u5F55\u72B6\u6001",
|
|
2427
|
+
status: admin ? "ok" : "warn",
|
|
2428
|
+
detail: `${summarizeUser(user.user)}\uFF1Baccess token ${describeTokenExpiry(ctx.credentials.cookies.access)}`,
|
|
2429
|
+
fix: admin ? void 0 : "\u51FA\u9898\u63A5\u53E3\u8981\u6C42 ADMIN \u89D2\u8272\uFF0C\u8BF7\u7528\u7BA1\u7406\u5458\u8D26\u53F7\u767B\u5F55"
|
|
2430
|
+
};
|
|
2431
|
+
} catch (error) {
|
|
2432
|
+
return {
|
|
2433
|
+
name: "\u767B\u5F55\u72B6\u6001",
|
|
2434
|
+
status: "fail",
|
|
2435
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
2436
|
+
fix: "qpc login\uFF08\u51ED\u636E\u53EF\u80FD\u5DF2\u8FC7\u671F\u6216\u88AB\u670D\u52A1\u7AEF\u5BC6\u94A5\u8F6E\u6362\u5931\u6548\uFF09"
|
|
2437
|
+
};
|
|
2438
|
+
}
|
|
2439
|
+
};
|
|
2440
|
+
var GITIGNORE_TEMPLATE = "gitignore";
|
|
2441
|
+
var PLACEHOLDER = /\{\{(\w+)\}\}/g;
|
|
2442
|
+
var TEMPLATE_DIR_CANDIDATES = ["../../templates", "../templates"];
|
|
2443
|
+
var resolveTemplateDir = (type) => {
|
|
2444
|
+
for (const candidate of TEMPLATE_DIR_CANDIDATES) {
|
|
2445
|
+
const dir = fileURLToPath(
|
|
2446
|
+
new URL(`${candidate}/${type}`, import.meta.url)
|
|
2447
|
+
);
|
|
2448
|
+
if (existsSync(dir)) return dir;
|
|
2449
|
+
}
|
|
2450
|
+
throw new ConfigError("\u5185\u90E8\u9519\u8BEF\uFF1A\u627E\u4E0D\u5230\u811A\u624B\u67B6\u6A21\u677F\u76EE\u5F55", {
|
|
2451
|
+
details: { from: import.meta.url, type }
|
|
2452
|
+
});
|
|
2453
|
+
};
|
|
2454
|
+
var buildProblemScaffold = async (vars) => {
|
|
2455
|
+
const templateDir = resolveTemplateDir(vars.type);
|
|
2456
|
+
const context = buildContext(vars);
|
|
2457
|
+
const relativePaths = await walkFiles(templateDir, {
|
|
2458
|
+
ignore: (name) => name === ".DS_Store"
|
|
2459
|
+
});
|
|
2460
|
+
const files = await Promise.all(
|
|
2461
|
+
relativePaths.map(async (relativePath) => {
|
|
2462
|
+
const raw = await readText(join(templateDir, relativePath));
|
|
2463
|
+
return {
|
|
2464
|
+
path: relativePath === GITIGNORE_TEMPLATE ? ".gitignore" : relativePath,
|
|
2465
|
+
content: renderTemplate(raw, context, relativePath)
|
|
2466
|
+
};
|
|
2467
|
+
})
|
|
2468
|
+
);
|
|
2469
|
+
return {
|
|
2470
|
+
files,
|
|
2471
|
+
cover: vars.type === "backend_service" ? { path: "cover.png", spec: buildBackendCoverSpec(vars.title) } : void 0
|
|
2472
|
+
};
|
|
2473
|
+
};
|
|
2474
|
+
var renderTemplate = (content, context, file) => content.replace(PLACEHOLDER, (match, key) => {
|
|
2475
|
+
const value = context[key];
|
|
2476
|
+
if (value === void 0) {
|
|
2477
|
+
throw new ConfigError(`\u6A21\u677F ${file} \u5F15\u7528\u4E86\u672A\u77E5\u5360\u4F4D\u7B26 ${match}`, {
|
|
2478
|
+
details: {
|
|
2479
|
+
file,
|
|
2480
|
+
placeholder: key,
|
|
2481
|
+
available: Object.keys(context).sort()
|
|
2482
|
+
}
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2485
|
+
return value;
|
|
2486
|
+
});
|
|
2487
|
+
var buildContext = (vars) => {
|
|
2488
|
+
const { title, type, difficulty, tagIds, mountPath } = vars;
|
|
2489
|
+
const context = {
|
|
2490
|
+
title,
|
|
2491
|
+
difficulty,
|
|
2492
|
+
tagIds: tagIds.join(", "),
|
|
2493
|
+
mountPath,
|
|
2494
|
+
initCommand: type === "backend_service" ? buildBackendInitCommand(
|
|
2495
|
+
DEFAULT_BACKEND_SERVICE_IMAGE_ID,
|
|
2496
|
+
mountPath
|
|
2497
|
+
) : DEFAULT_INIT_COMMAND.replace(/\sproject$/, ` ${mountPath}`)
|
|
2498
|
+
};
|
|
2499
|
+
if (type === "backend_service") {
|
|
2500
|
+
context.serviceImage = DEFAULT_BACKEND_SERVICE_IMAGE_ID;
|
|
2501
|
+
context.servicePort = String(
|
|
2502
|
+
getBackendServiceDefaultPort(DEFAULT_BACKEND_SERVICE_IMAGE_ID)
|
|
2503
|
+
);
|
|
2504
|
+
context.coverBg = BACKEND_COVER.bg;
|
|
2505
|
+
context.coverBgTo = BACKEND_COVER.bgTo;
|
|
2506
|
+
context.coverAccent = BACKEND_COVER.accent;
|
|
2507
|
+
context.coverSubtitle = BACKEND_COVER.subtitle;
|
|
2508
|
+
}
|
|
2509
|
+
return context;
|
|
2510
|
+
};
|
|
2511
|
+
var BACKEND_COVER = {
|
|
2512
|
+
bg: COVER_DEFAULTS.bg,
|
|
2513
|
+
bgTo: COVER_SUGGESTED_BG_TO,
|
|
2514
|
+
accent: COVER_DEFAULTS.accent,
|
|
2515
|
+
subtitle: "GET /health"
|
|
2516
|
+
};
|
|
2517
|
+
var buildBackendCoverSpec = (title) => ({
|
|
2518
|
+
title,
|
|
2519
|
+
subtitle: BACKEND_COVER.subtitle,
|
|
2520
|
+
bg: BACKEND_COVER.bg,
|
|
2521
|
+
bgTo: BACKEND_COVER.bgTo,
|
|
2522
|
+
accent: BACKEND_COVER.accent,
|
|
2523
|
+
text: COVER_DEFAULTS.text,
|
|
2524
|
+
width: COVER_DEFAULTS.width,
|
|
2525
|
+
height: COVER_DEFAULTS.height
|
|
2526
|
+
});
|
|
2527
|
+
|
|
2528
|
+
// src/commands/init.ts
|
|
2529
|
+
var registerInitCommand = (registry) => {
|
|
2530
|
+
const { program, context } = registry;
|
|
2531
|
+
program.command("init").description(
|
|
2532
|
+
"\u751F\u6210\u9898\u76EE\u9AA8\u67B6\uFF1Aproblem.config.ts / judge.js / template / answer"
|
|
2533
|
+
).argument("[dir]", "\u76EE\u6807\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09", ".").option("--name <title>", "\u9898\u76EE\u6807\u9898\uFF08\u9ED8\u8BA4\u53D6\u76EE\u5F55\u540D\uFF09").option(
|
|
2534
|
+
"--type <type>",
|
|
2535
|
+
`\u9898\u578B\uFF1A${PROBLEM_TYPES.join(" | ")}\uFF08\u4E0D\u4F20\u5219\u65B9\u5411\u952E\u9009\u62E9\uFF1B\u975E\u4EA4\u4E92\u73AF\u5883\u9ED8\u8BA4 frontend\uFF09`
|
|
2536
|
+
).option(
|
|
2537
|
+
"--difficulty <level>",
|
|
2538
|
+
`\u96BE\u5EA6\uFF1A${DIFFICULTIES.join(" | ")}`,
|
|
2539
|
+
"easy"
|
|
2540
|
+
).option("--tag <id...>", "\u6807\u7B7E id\uFF0C\u53EF\u7ED9\u591A\u4E2A\uFF08\u7528 `qpc tag` \u67E5\u770B\uFF09", ["1"]).option(
|
|
2541
|
+
"--mount <path>",
|
|
2542
|
+
"\u5224\u9898\u6253\u5305\u76EE\u5F55 / \u5FEB\u7167\u6302\u8F7D\u8DEF\u5F84",
|
|
2543
|
+
DEFAULT_JUDGE_UPLOAD_PATH
|
|
2544
|
+
).option("--force", "\u76EE\u5F55\u975E\u7A7A\u65F6\u4ECD\u7136\u5199\u5165\uFF08\u53EA\u8986\u76D6\u540C\u540D\u6587\u4EF6\uFF0C\u4E0D\u5220\u9664\u5176\u5B83\u6587\u4EF6\uFF09").action(async (dir, options) => {
|
|
2545
|
+
const ctx = await context();
|
|
2546
|
+
await runInit(ctx, dir, options);
|
|
2547
|
+
});
|
|
2548
|
+
};
|
|
2549
|
+
var runInit = async (ctx, dir, options) => {
|
|
2550
|
+
const target = resolve(ctx.cwd, dir);
|
|
2551
|
+
const difficulty = assertDifficulty(options.difficulty);
|
|
2552
|
+
const tagIds = assertTagIds(options.tag);
|
|
2553
|
+
const mountPath = options.mount ?? DEFAULT_JUDGE_UPLOAD_PATH;
|
|
2554
|
+
const title = options.name?.trim() || defaultTitle(target);
|
|
2555
|
+
if (!options.force) {
|
|
2556
|
+
const existing = (await listDir(target)).filter(
|
|
2557
|
+
(name) => name !== ".git"
|
|
2558
|
+
);
|
|
2559
|
+
if (existing.length > 0) {
|
|
2560
|
+
throw new UsageError(`\u76EE\u5F55\u975E\u7A7A\uFF1A${displayPath(ctx.cwd, target)}`, {
|
|
2561
|
+
hint: "\u6362\u4E00\u4E2A\u7A7A\u76EE\u5F55\uFF0C\u6216\u52A0 --force \u8986\u76D6\u540C\u540D\u6587\u4EF6\uFF08\u5DF2\u6709\u6587\u4EF6\u4E0D\u4F1A\u88AB\u5220\u9664\uFF09\u3002"
|
|
2562
|
+
});
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
const type = await resolveProblemType(ctx, options.type);
|
|
2566
|
+
const scaffold = await buildProblemScaffold({
|
|
2567
|
+
title,
|
|
2568
|
+
type,
|
|
2569
|
+
difficulty,
|
|
2570
|
+
tagIds,
|
|
2571
|
+
mountPath
|
|
2572
|
+
});
|
|
2573
|
+
ctx.logger.step(
|
|
2574
|
+
`\u751F\u6210${type === "backend_service" ? "\u540E\u7AEF\u670D\u52A1\u9898" : "\u524D\u7AEF\u9898"}\u9AA8\u67B6 \u2192 ${displayPath(ctx.cwd, target)}`
|
|
2575
|
+
);
|
|
2576
|
+
for (const file of scaffold.files) {
|
|
2577
|
+
await writeTextWithMode(resolve(target, file.path), file.content);
|
|
2578
|
+
ctx.logger.success(
|
|
2579
|
+
`${file.path} ${ctx.logger.colors.dim(`${file.content.length} \u5B57\u7B26`)}`
|
|
2580
|
+
);
|
|
2581
|
+
}
|
|
2582
|
+
if (scaffold.cover) {
|
|
2583
|
+
const outFile = resolve(target, scaffold.cover.path);
|
|
2584
|
+
try {
|
|
2585
|
+
const png = await renderCoverPng(scaffold.cover.spec);
|
|
2586
|
+
await writeBinary(outFile, png);
|
|
2587
|
+
ctx.logger.success(
|
|
2588
|
+
`${scaffold.cover.path} ${ctx.logger.colors.dim(formatBytes(png.byteLength))}`
|
|
2589
|
+
);
|
|
2590
|
+
} catch (error) {
|
|
2591
|
+
ctx.logger.warn(
|
|
2592
|
+
`\u5C01\u9762\u6E32\u67D3\u5931\u8D25\uFF08${error instanceof Error ? error.message : String(error)}\uFF09\uFF1A\u914D\u7F6E\u91CC\u5DF2\u6709 cover.generate\uFF0C\u8DD1 \`qpc cover\` \u53EF\u91CD\u8BD5`
|
|
2593
|
+
);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
const cd = dir === "." ? "qpc" : `cd ${displayPath(ctx.cwd, target)} && qpc`;
|
|
2597
|
+
ctx.logger.blank();
|
|
2598
|
+
ctx.logger.step("\u4E0B\u4E00\u6B65");
|
|
2599
|
+
if (type === "backend_service") {
|
|
2600
|
+
ctx.logger.info(
|
|
2601
|
+
` ${cd} cover --upload --write # \u540E\u7AEF\u9898\u5FC5\u987B\u81EA\u5B9A\u4E49\u5C01\u9762\uFF1A\u751F\u6210 \u2192 \u4E0A\u4F20 \u2192 \u5199\u56DE imageId`
|
|
2602
|
+
);
|
|
2603
|
+
}
|
|
2604
|
+
ctx.logger.info(` ${cd} check # \u6BEB\u79D2\u7EA7\u9759\u6001\u9884\u68C0`);
|
|
2605
|
+
ctx.logger.info(` ${cd} upload --wait # \u4E0A\u4F20\u5E76\u7B49\u5F85\u5BA1\u8BA1\uFF08\u7EA6 20 \u79D2\uFF09`);
|
|
2606
|
+
if (ctx.logger.json) {
|
|
2607
|
+
ctx.logger.result({
|
|
2608
|
+
dir: target,
|
|
2609
|
+
type,
|
|
2610
|
+
files: scaffold.files.map((file) => file.path),
|
|
2611
|
+
cover: scaffold.cover?.path,
|
|
2612
|
+
difficulty,
|
|
2613
|
+
tagIds
|
|
2614
|
+
});
|
|
2615
|
+
}
|
|
2616
|
+
};
|
|
2617
|
+
var defaultTitle = (target) => target.split(/[\\/]/).filter(Boolean).pop() ?? "\u672A\u547D\u540D\u9898\u76EE";
|
|
2618
|
+
var assertProblemType = (value) => {
|
|
2619
|
+
const candidate = value ?? "frontend";
|
|
2620
|
+
if (!PROBLEM_TYPES.includes(candidate)) {
|
|
2621
|
+
throw new UsageError(`\u9898\u578B\u4E0D\u5408\u6CD5\uFF1A${value}`, {
|
|
2622
|
+
hint: `\u53EF\u9009\u503C\uFF1A${PROBLEM_TYPES.join(" | ")}`
|
|
2623
|
+
});
|
|
2624
|
+
}
|
|
2625
|
+
return candidate;
|
|
2626
|
+
};
|
|
2627
|
+
var resolveProblemType = async (ctx, value) => {
|
|
2628
|
+
if (value !== void 0) return assertProblemType(value);
|
|
2629
|
+
if (ctx.assumeYes || ctx.logger.json || !ctx.runtime.stdin.isTTY) {
|
|
2630
|
+
return "frontend";
|
|
2631
|
+
}
|
|
2632
|
+
return select(
|
|
2633
|
+
"\u8BF7\u9009\u62E9\u9898\u578B",
|
|
2634
|
+
[
|
|
2635
|
+
{
|
|
2636
|
+
value: "frontend",
|
|
2637
|
+
label: "\u524D\u7AEF\u9898\uFF08frontend\uFF09",
|
|
2638
|
+
hint: "\u9759\u6001\u7AD9\u70B9\uFF0C\u7528 Playwright \u5224\u9898"
|
|
2639
|
+
},
|
|
2640
|
+
{
|
|
2641
|
+
value: "backend_service",
|
|
2642
|
+
label: "\u540E\u7AEF\u670D\u52A1\u9898\uFF08backend_service\uFF09",
|
|
2643
|
+
hint: "\u5E38\u9A7B\u8FDB\u7A0B\uFF0C\u7528 HTTP \u5224\u9898\uFF0C\u6A21\u677F\u542B\u5C01\u9762\u56FE"
|
|
2644
|
+
}
|
|
2645
|
+
],
|
|
2646
|
+
{
|
|
2647
|
+
stdin: ctx.runtime.stdin,
|
|
2648
|
+
stderr: ctx.runtime.stderr,
|
|
2649
|
+
stderrStream: ctx.runtime.stderrStream
|
|
2650
|
+
},
|
|
2651
|
+
{ initialValue: "frontend" }
|
|
2652
|
+
);
|
|
2653
|
+
};
|
|
2654
|
+
var assertDifficulty = (value) => {
|
|
2655
|
+
const candidate = value ?? "easy";
|
|
2656
|
+
if (!DIFFICULTIES.includes(candidate)) {
|
|
2657
|
+
throw new UsageError(`\u96BE\u5EA6\u4E0D\u5408\u6CD5\uFF1A${value}`, {
|
|
2658
|
+
hint: `\u53EF\u9009\u503C\uFF1A${DIFFICULTIES.join(" | ")}`
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
return candidate;
|
|
2662
|
+
};
|
|
2663
|
+
var assertTagIds = (values) => {
|
|
2664
|
+
const list = values && values.length > 0 ? values : ["1"];
|
|
2665
|
+
const parts = list.flatMap(
|
|
2666
|
+
(value) => value.split(",").map((part) => part.trim()).filter(Boolean)
|
|
2667
|
+
);
|
|
2668
|
+
const parsed = parts.map((part) => Number.parseInt(part, 10));
|
|
2669
|
+
if (parsed.some((id) => !Number.isInteger(id) || id <= 0)) {
|
|
2670
|
+
throw new UsageError(`\u6807\u7B7E id \u5FC5\u987B\u662F\u6B63\u6574\u6570\uFF1A${parts.join(", ")}`, {
|
|
2671
|
+
hint: "\u8FD0\u884C `qpc tag` \u67E5\u770B\u53EF\u7528\u6807\u7B7E id\uFF0C\u6216\u7528 `qpc tag --write` \u4EA4\u4E92\u9009\u62E9\u3002"
|
|
2672
|
+
});
|
|
2673
|
+
}
|
|
2674
|
+
return parsed;
|
|
2675
|
+
};
|
|
2676
|
+
var credentialsSchema = z.object({
|
|
2677
|
+
version: z.literal(1),
|
|
2678
|
+
apiUrl: z.string().optional(),
|
|
2679
|
+
cookies: z.object({
|
|
2680
|
+
access: z.string().optional(),
|
|
2681
|
+
refresh: z.string().optional(),
|
|
2682
|
+
csrf: z.string().optional()
|
|
2683
|
+
}),
|
|
2684
|
+
user: z.object({
|
|
2685
|
+
id: z.string(),
|
|
2686
|
+
nickname: z.string().optional(),
|
|
2687
|
+
email: z.string().optional(),
|
|
2688
|
+
role: z.string().optional()
|
|
2689
|
+
}).optional(),
|
|
2690
|
+
updatedAt: z.string().optional()
|
|
2691
|
+
});
|
|
2692
|
+
var emptyCredentials = () => ({
|
|
2693
|
+
version: 1,
|
|
2694
|
+
cookies: {}
|
|
2695
|
+
});
|
|
2696
|
+
var resolveConfigDir = (env = process.env) => {
|
|
2697
|
+
const explicit = env.QUANTA_CONFIG_DIR;
|
|
2698
|
+
if (explicit) return explicit;
|
|
2699
|
+
const xdg = env.XDG_CONFIG_HOME;
|
|
2700
|
+
if (xdg) return xdg;
|
|
2701
|
+
return join(homedir(), ".config");
|
|
2702
|
+
};
|
|
2703
|
+
var credentialsPath = (configDir) => join(configDir, "quanta", "credentials.json");
|
|
2704
|
+
var readCredentials = async (file) => {
|
|
2705
|
+
if (!await pathExists(file)) return emptyCredentials();
|
|
2706
|
+
const raw = await readText(file);
|
|
2707
|
+
let parsed;
|
|
2708
|
+
try {
|
|
2709
|
+
parsed = JSON.parse(raw);
|
|
2710
|
+
} catch (error) {
|
|
2711
|
+
throw new ConfigError(`\u51ED\u636E\u6587\u4EF6\u4E0D\u662F\u5408\u6CD5 JSON\uFF1A${file}`, {
|
|
2712
|
+
cause: error,
|
|
2713
|
+
hint: "\u6267\u884C `qpc logout` \u540E\u91CD\u65B0 `qpc login`\u3002"
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2716
|
+
const result = credentialsSchema.safeParse(parsed);
|
|
2717
|
+
if (!result.success) {
|
|
2718
|
+
throw new ConfigError(`\u51ED\u636E\u6587\u4EF6\u7ED3\u6784\u65E0\u6CD5\u8BC6\u522B\uFF1A${file}`, {
|
|
2719
|
+
details: { issues: result.error.issues.map((i) => i.message) },
|
|
2720
|
+
hint: "\u6267\u884C `qpc logout` \u540E\u91CD\u65B0 `qpc login`\u3002"
|
|
2721
|
+
});
|
|
2722
|
+
}
|
|
2723
|
+
return result.data;
|
|
2724
|
+
};
|
|
2725
|
+
var writeCredentials = async (file, credentials) => {
|
|
2726
|
+
await writeTextWithMode(
|
|
2727
|
+
file,
|
|
2728
|
+
`${JSON.stringify({ ...credentials, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 3)}
|
|
2729
|
+
`,
|
|
2730
|
+
384
|
|
2731
|
+
);
|
|
2732
|
+
};
|
|
2733
|
+
var removeCredentials = async (file) => {
|
|
2734
|
+
const { rm: rm2 } = await import('fs/promises');
|
|
2735
|
+
if (!await pathExists(file)) return false;
|
|
2736
|
+
await rm2(file, { force: true });
|
|
2737
|
+
return true;
|
|
2738
|
+
};
|
|
2739
|
+
|
|
2740
|
+
// ../shared/service/oauth/device-flow.ts
|
|
2741
|
+
var DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
2742
|
+
var DEVICE_CODE_SLOW_DOWN_SECONDS = 5;
|
|
2743
|
+
var DEVICE_FLOW_CLIENT_ID = "quanta-problem-creator-cli";
|
|
2744
|
+
var DEVICE_FLOW_SCOPE = "problem:write";
|
|
2745
|
+
var DEVICE_FLOW_ERRORS = {
|
|
2746
|
+
/** 用户还没批准,继续按 interval 轮询 */
|
|
2747
|
+
authorizationPending: "authorization_pending",
|
|
2748
|
+
/** 轮询太快:interval 必须 +5s */
|
|
2749
|
+
slowDown: "slow_down",
|
|
2750
|
+
/** 用户明确拒绝:立即停止轮询 */
|
|
2751
|
+
accessDenied: "access_denied",
|
|
2752
|
+
/** device_code 过期,会话结束 */
|
|
2753
|
+
expiredToken: "expired_token"
|
|
2754
|
+
};
|
|
2755
|
+
var DEVICE_FLOW_PATHS = {
|
|
2756
|
+
deviceAuthorization: "/api/oauth/device/authorize",
|
|
2757
|
+
deviceToken: "/api/oauth/device/token",
|
|
2758
|
+
/** 用户在浏览器里确认授权的人机页面 */
|
|
2759
|
+
verification: "/auth/device"
|
|
2760
|
+
};
|
|
2761
|
+
|
|
2762
|
+
// src/utils/async.ts
|
|
2763
|
+
var sleep = (ms, signal) => new Promise((resolve8, reject) => {
|
|
2764
|
+
if (signal?.aborted) {
|
|
2765
|
+
reject(
|
|
2766
|
+
signal.reason instanceof Error ? signal.reason : new Error("aborted")
|
|
2767
|
+
);
|
|
2768
|
+
return;
|
|
2769
|
+
}
|
|
2770
|
+
const timer = setTimeout(() => {
|
|
2771
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2772
|
+
resolve8();
|
|
2773
|
+
}, ms);
|
|
2774
|
+
const onAbort = () => {
|
|
2775
|
+
clearTimeout(timer);
|
|
2776
|
+
reject(
|
|
2777
|
+
signal?.reason instanceof Error ? signal.reason : new Error("aborted")
|
|
2778
|
+
);
|
|
2779
|
+
};
|
|
2780
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2781
|
+
});
|
|
2782
|
+
var poll = async (options) => {
|
|
2783
|
+
const intervalMs = options.intervalMs ?? 2e3;
|
|
2784
|
+
const timeoutMs = options.timeoutMs ?? 12e4;
|
|
2785
|
+
const startedAt = Date.now();
|
|
2786
|
+
let attempt = 0;
|
|
2787
|
+
for (; ; ) {
|
|
2788
|
+
attempt += 1;
|
|
2789
|
+
const value = await options.probe(attempt);
|
|
2790
|
+
if (value !== void 0) return value;
|
|
2791
|
+
const elapsed = Date.now() - startedAt;
|
|
2792
|
+
if (elapsed + intervalMs > timeoutMs) return void 0;
|
|
2793
|
+
options.onTick?.(elapsed, attempt);
|
|
2794
|
+
await sleep(intervalMs, options.signal);
|
|
2795
|
+
}
|
|
2796
|
+
};
|
|
2797
|
+
var CLI_NAME = "qpc";
|
|
2798
|
+
var CLI_DESCRIPTION = "Quanta \u51FA\u9898\u5DE5\u5177\uFF1A\u811A\u624B\u67B6\u3001\u79BB\u7EBF\u9884\u68C0\u3001\u4E0A\u4F20\u4E0E\u53D1\u5E03";
|
|
2799
|
+
var readVersion = () => {
|
|
2800
|
+
try {
|
|
2801
|
+
const raw = readFileSync(
|
|
2802
|
+
new URL("../package.json", import.meta.url),
|
|
2803
|
+
"utf8"
|
|
2804
|
+
);
|
|
2805
|
+
const parsed = JSON.parse(raw);
|
|
2806
|
+
return parsed.version ?? "0.0.0";
|
|
2807
|
+
} catch {
|
|
2808
|
+
return "0.0.0";
|
|
2809
|
+
}
|
|
2810
|
+
};
|
|
2811
|
+
var CLI_VERSION = readVersion();
|
|
2812
|
+
|
|
2813
|
+
// src/services/device-flow.ts
|
|
2814
|
+
var postForm = async (options, path, body) => {
|
|
2815
|
+
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
2816
|
+
const signal = options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(timeoutMs)]) : AbortSignal.timeout(timeoutMs);
|
|
2817
|
+
let response;
|
|
2818
|
+
try {
|
|
2819
|
+
response = await options.fetch(`${options.baseUrl}${path}`, {
|
|
2820
|
+
method: "POST",
|
|
2821
|
+
headers: {
|
|
2822
|
+
// RFC 8628 §3.1 要求 x-www-form-urlencoded;服务端为兼容也收 JSON,
|
|
2823
|
+
// 但这里按规范发送,避免依赖服务端的宽松解析。
|
|
2824
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
2825
|
+
accept: "application/json",
|
|
2826
|
+
"user-agent": `qpc/${CLI_VERSION} (quanta-problem-creator)`
|
|
2827
|
+
},
|
|
2828
|
+
body: new URLSearchParams(body).toString(),
|
|
2829
|
+
signal
|
|
2830
|
+
});
|
|
2831
|
+
} catch (error) {
|
|
2832
|
+
throw new CliError(`\u65E0\u6CD5\u8FDE\u63A5 ${options.baseUrl}${path}`, {
|
|
2833
|
+
cause: error,
|
|
2834
|
+
hint: "\u786E\u8BA4 API \u5730\u5740\u6B63\u786E\u4E14\u670D\u52A1\u5DF2\u542F\u52A8\uFF1Aqpc doctor"
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
const text = await response.text();
|
|
2838
|
+
let json;
|
|
2839
|
+
try {
|
|
2840
|
+
json = text ? JSON.parse(text) : void 0;
|
|
2841
|
+
} catch {
|
|
2842
|
+
json = void 0;
|
|
2843
|
+
}
|
|
2844
|
+
return { status: response.status, json };
|
|
2845
|
+
};
|
|
2846
|
+
var createDeviceFlow = (options) => {
|
|
2847
|
+
const requestCodes = async () => {
|
|
2848
|
+
const { status, json } = await postForm(
|
|
2849
|
+
options,
|
|
2850
|
+
DEVICE_FLOW_PATHS.deviceAuthorization,
|
|
2851
|
+
{ client_id: DEVICE_FLOW_CLIENT_ID, scope: DEVICE_FLOW_SCOPE }
|
|
2852
|
+
);
|
|
2853
|
+
const body = json;
|
|
2854
|
+
if (status !== 200 || !body?.device_code || !body.user_code) {
|
|
2855
|
+
throw new CliError(`\u670D\u52A1\u7AEF\u62D2\u7EDD\u4E86\u8BBE\u5907\u6388\u6743\u8BF7\u6C42\uFF08HTTP ${status}\uFF09`, {
|
|
2856
|
+
details: { body: body ?? null },
|
|
2857
|
+
hint: body?.error === "invalid_client" ? "\u670D\u52A1\u7AEF\u4E0D\u8BA4\u8BC6\u672C\u5BA2\u6237\u7AEF\u7684 client_id\uFF0C\u786E\u8BA4 CLI \u4E0E\u670D\u52A1\u7AEF\u7248\u672C\u4E00\u81F4\u3002" : "\u68C0\u67E5 API \u5730\u5740\u662F\u5426\u6307\u5411 quanta-challenge \u5E94\u7528\uFF08\u800C\u975E\u5176\u5B83\u670D\u52A1\uFF09\u3002"
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
return {
|
|
2861
|
+
device_code: body.device_code,
|
|
2862
|
+
user_code: body.user_code,
|
|
2863
|
+
verification_uri: body.verification_uri ?? "",
|
|
2864
|
+
verification_uri_complete: body.verification_uri_complete ?? "",
|
|
2865
|
+
// RFC §3.2:interval 缺省时客户端必须按 5 秒处理
|
|
2866
|
+
interval: body.interval ?? 5,
|
|
2867
|
+
expires_in: body.expires_in ?? 600
|
|
2868
|
+
};
|
|
2869
|
+
};
|
|
2870
|
+
const pollForToken = async (auth, onPending) => {
|
|
2871
|
+
let intervalMs = (options.overrideIntervalSeconds ?? auth.interval) * 1e3;
|
|
2872
|
+
const deadline = Date.now() + auth.expires_in * 1e3;
|
|
2873
|
+
const startedAt = Date.now();
|
|
2874
|
+
let consecutiveNetworkErrors = 0;
|
|
2875
|
+
for (; ; ) {
|
|
2876
|
+
if (Date.now() >= deadline) {
|
|
2877
|
+
throw new CliError("\u8BBE\u5907\u6388\u6743\u7801\u5DF2\u8FC7\u671F", {
|
|
2878
|
+
hint: "\u91CD\u65B0\u6267\u884C `qpc login` \u4F1A\u751F\u6210\u65B0\u7684\u9A8C\u8BC1\u7801\u3002"
|
|
2879
|
+
});
|
|
2880
|
+
}
|
|
2881
|
+
let status;
|
|
2882
|
+
let body;
|
|
2883
|
+
try {
|
|
2884
|
+
const result = await postForm(
|
|
2885
|
+
options,
|
|
2886
|
+
DEVICE_FLOW_PATHS.deviceToken,
|
|
2887
|
+
{
|
|
2888
|
+
grant_type: DEVICE_CODE_GRANT_TYPE,
|
|
2889
|
+
device_code: auth.device_code,
|
|
2890
|
+
client_id: DEVICE_FLOW_CLIENT_ID
|
|
2891
|
+
}
|
|
2892
|
+
);
|
|
2893
|
+
status = result.status;
|
|
2894
|
+
body = result.json;
|
|
2895
|
+
consecutiveNetworkErrors = 0;
|
|
2896
|
+
} catch (error2) {
|
|
2897
|
+
consecutiveNetworkErrors += 1;
|
|
2898
|
+
intervalMs *= 2;
|
|
2899
|
+
options.logger.warn(
|
|
2900
|
+
`\u8F6E\u8BE2\u5931\u8D25\uFF08\u7B2C ${consecutiveNetworkErrors} \u6B21\uFF09\uFF0C\u95F4\u9694\u8C03\u6574\u4E3A ${Math.round(intervalMs / 1e3)} \u79D2`
|
|
2901
|
+
);
|
|
2902
|
+
if (consecutiveNetworkErrors >= 5) throw error2;
|
|
2903
|
+
await sleep(intervalMs, options.signal);
|
|
2904
|
+
continue;
|
|
2905
|
+
}
|
|
2906
|
+
if (status === 200 && body?.access_token && body.refresh_token) {
|
|
2907
|
+
return {
|
|
2908
|
+
access_token: body.access_token,
|
|
2909
|
+
refresh_token: body.refresh_token,
|
|
2910
|
+
token_type: "Bearer",
|
|
2911
|
+
expires_in: body.expires_in ?? 900,
|
|
2912
|
+
scope: body.scope ?? DEVICE_FLOW_SCOPE
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2915
|
+
const error = body?.error;
|
|
2916
|
+
if (error === DEVICE_FLOW_ERRORS.authorizationPending) {
|
|
2917
|
+
onPending?.(Date.now() - startedAt);
|
|
2918
|
+
} else if (error === DEVICE_FLOW_ERRORS.slowDown) {
|
|
2919
|
+
intervalMs += DEVICE_CODE_SLOW_DOWN_SECONDS * 1e3;
|
|
2920
|
+
options.logger.detail(
|
|
2921
|
+
`\u670D\u52A1\u7AEF\u8981\u6C42\u964D\u4F4E\u8F6E\u8BE2\u9891\u7387\uFF0C\u95F4\u9694\u8C03\u6574\u4E3A ${Math.round(intervalMs / 1e3)} \u79D2`
|
|
2922
|
+
);
|
|
2923
|
+
} else if (error === DEVICE_FLOW_ERRORS.accessDenied) {
|
|
2924
|
+
throw new PrecheckError("\u6388\u6743\u88AB\u62D2\u7EDD", {
|
|
2925
|
+
hint: "\u4F60\u5728\u786E\u8BA4\u9875\u70B9\u51FB\u4E86\u62D2\u7EDD\uFF1B\u91CD\u65B0\u6267\u884C `qpc login` \u53EF\u518D\u8BD5\u4E00\u6B21\u3002"
|
|
2926
|
+
});
|
|
2927
|
+
} else if (error === DEVICE_FLOW_ERRORS.expiredToken) {
|
|
2928
|
+
throw new CliError("\u8BBE\u5907\u6388\u6743\u7801\u5DF2\u5931\u6548\uFF08\u53EF\u80FD\u5DF2\u88AB\u4F7F\u7528\uFF09", {
|
|
2929
|
+
hint: "\u91CD\u65B0\u6267\u884C `qpc login`\u3002"
|
|
2930
|
+
});
|
|
2931
|
+
} else {
|
|
2932
|
+
throw new CliError(
|
|
2933
|
+
`\u6362\u4EE4\u724C\u5931\u8D25${error ? `\uFF1A${error}` : `\uFF08HTTP ${status}\uFF09`}`,
|
|
2934
|
+
{
|
|
2935
|
+
details: {
|
|
2936
|
+
status,
|
|
2937
|
+
error: body?.error,
|
|
2938
|
+
description: body?.error_description
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
);
|
|
2942
|
+
}
|
|
2943
|
+
await sleep(intervalMs, options.signal);
|
|
2944
|
+
}
|
|
2945
|
+
};
|
|
2946
|
+
return { requestCodes, pollForToken };
|
|
2947
|
+
};
|
|
2948
|
+
var openBrowser = async (url) => {
|
|
2949
|
+
const platform = process.platform;
|
|
2950
|
+
const command = platform === "darwin" ? { file: "open", args: [url] } : platform === "win32" ? (
|
|
2951
|
+
// cmd 的 start 是内置命令;第一个空参数是为了让 start 把带引号的 URL
|
|
2952
|
+
// 当作标题之外的参数(Windows 上 start 会把第一个引号参数当窗口标题)。
|
|
2953
|
+
{ file: "cmd", args: ["/c", "start", "", url] }
|
|
2954
|
+
) : { file: "xdg-open", args: [url] };
|
|
2955
|
+
return new Promise((resolve8) => {
|
|
2956
|
+
try {
|
|
2957
|
+
const child = spawn(command.file, command.args, {
|
|
2958
|
+
stdio: "ignore",
|
|
2959
|
+
detached: true
|
|
2960
|
+
});
|
|
2961
|
+
child.on("error", () => resolve8(false));
|
|
2962
|
+
child.on("spawn", () => {
|
|
2963
|
+
child.unref();
|
|
2964
|
+
resolve8(true);
|
|
2965
|
+
});
|
|
2966
|
+
} catch {
|
|
2967
|
+
resolve8(false);
|
|
2968
|
+
}
|
|
2969
|
+
});
|
|
2970
|
+
};
|
|
2971
|
+
|
|
2972
|
+
// src/commands/login.ts
|
|
2973
|
+
var registerLoginCommand = (registry) => {
|
|
2974
|
+
const { program, context } = registry;
|
|
2975
|
+
program.command("login").description(
|
|
2976
|
+
"\u767B\u5F55\u5E76\u4FDD\u5B58\u4F1A\u8BDD\u51ED\u636E\uFF08\u9ED8\u8BA4 ~/.config/quanta/credentials.json\uFF09"
|
|
2977
|
+
).option(
|
|
2978
|
+
"--email <email>",
|
|
2979
|
+
"\u8D26\u53F7\u90AE\u7BB1\uFF08\u663E\u5F0F\u8D70\u5BC6\u7801\u767B\u5F55\uFF1B\u4E5F\u53EF\u7528 QUANTA_EMAIL\uFF09"
|
|
2980
|
+
).option(
|
|
2981
|
+
"--password-stdin",
|
|
2982
|
+
"\u4ECE\u6807\u51C6\u8F93\u5165\u8BFB\u53D6\u5BC6\u7801\uFF08\u663E\u5F0F\u8D70\u5BC6\u7801\u767B\u5F55\uFF0C\u63A8\u8350\u7528\u4E8E CI\uFF09"
|
|
2983
|
+
).option("--show", "\u53EA\u663E\u793A\u5F53\u524D\u767B\u5F55\u72B6\u6001\uFF0C\u4E0D\u505A\u767B\u5F55").option("--logout", "\u5220\u9664\u672C\u5730\u51ED\u636E").option("--device", "\u7528\u8BBE\u5907\u7801\u5728\u6D4F\u89C8\u5668\u91CC\u6388\u6743\u767B\u5F55\uFF08\u9ED8\u8BA4\u884C\u4E3A\uFF09").option(
|
|
2984
|
+
"--no-browser",
|
|
2985
|
+
"\u8BBE\u5907\u7801\u6D41\u7A0B\u4E0B\u53EA\u6253\u5370\u5730\u5740\u4E0E\u9A8C\u8BC1\u7801\uFF0C\u4E0D\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668"
|
|
2986
|
+
).action(async (options) => {
|
|
2987
|
+
const ctx = await context();
|
|
2988
|
+
await runLogin(ctx, options);
|
|
2989
|
+
});
|
|
2990
|
+
};
|
|
2991
|
+
var wantsPasswordFlow = (options) => Boolean(options.email || options.passwordStdin);
|
|
2992
|
+
var runLogin = async (ctx, options) => {
|
|
2993
|
+
if (options.logout) {
|
|
2994
|
+
const removed = await removeCredentials(ctx.credentialsFile);
|
|
2995
|
+
ctx.logger.info(
|
|
2996
|
+
removed ? `\u5DF2\u5220\u9664\u51ED\u636E\uFF1A${ctx.credentialsFile}` : `\u672C\u5730\u6CA1\u6709\u51ED\u636E\uFF1A${ctx.credentialsFile}`
|
|
2997
|
+
);
|
|
2998
|
+
if (ctx.logger.json) ctx.logger.result({ loggedOut: true, removed });
|
|
2999
|
+
return;
|
|
3000
|
+
}
|
|
3001
|
+
if (options.show) {
|
|
3002
|
+
const stored = await readCredentials(ctx.credentialsFile);
|
|
3003
|
+
const api2 = createAdminApi(ctx.client);
|
|
3004
|
+
let user = stored.user;
|
|
3005
|
+
if (ctx.client.hasSession) {
|
|
3006
|
+
user = await api2.getCurrentUser().catch(() => stored.user);
|
|
3007
|
+
}
|
|
3008
|
+
ctx.logger.table([
|
|
3009
|
+
["\u51ED\u636E\u6587\u4EF6", ctx.credentialsFile],
|
|
3010
|
+
["API", ctx.client.baseUrl],
|
|
3011
|
+
["\u7528\u6237", user ? summarizeUser(user) : "<\u672A\u767B\u5F55>"],
|
|
3012
|
+
["access token", describeTokenExpiry(stored.cookies.access)],
|
|
3013
|
+
["refresh token", stored.cookies.refresh ? "\u5DF2\u4FDD\u5B58" : "\u65E0"],
|
|
3014
|
+
[
|
|
3015
|
+
"csrf token",
|
|
3016
|
+
stored.cookies.csrf ? "\u5DF2\u4FDD\u5B58" : "\u65E0\uFF08\u5C06\u4F7F\u7528 x-ssr \u901A\u9053\uFF09"
|
|
3017
|
+
]
|
|
3018
|
+
]);
|
|
3019
|
+
if (ctx.logger.json) {
|
|
3020
|
+
ctx.logger.result({
|
|
3021
|
+
credentialsFile: ctx.credentialsFile,
|
|
3022
|
+
apiUrl: ctx.client.baseUrl,
|
|
3023
|
+
user: user ?? null,
|
|
3024
|
+
hasSession: ctx.client.hasSession
|
|
3025
|
+
});
|
|
3026
|
+
}
|
|
3027
|
+
return;
|
|
3028
|
+
}
|
|
3029
|
+
if (options.device || !wantsPasswordFlow(options)) {
|
|
3030
|
+
await runDeviceLogin(ctx, options);
|
|
3031
|
+
return;
|
|
3032
|
+
}
|
|
3033
|
+
const email = options.email?.trim() || ctx.runtime.env.QUANTA_EMAIL?.trim() || await promptVisible("\u90AE\u7BB1\uFF1A", {
|
|
3034
|
+
stdin: ctx.runtime.stdin,
|
|
3035
|
+
stderr: ctx.runtime.stderr
|
|
3036
|
+
});
|
|
3037
|
+
if (!email)
|
|
3038
|
+
throw new UsageError("\u7F3A\u5C11\u90AE\u7BB1\uFF1A\u7528 --email \u6216 QUANTA_EMAIL \u63D0\u4F9B");
|
|
3039
|
+
const password = await resolvePassword(ctx, options);
|
|
3040
|
+
const api = createAdminApi(ctx.client);
|
|
3041
|
+
ctx.logger.step(`\u767B\u5F55 ${ctx.client.baseUrl}`);
|
|
3042
|
+
const result = await api.login({ email, password });
|
|
3043
|
+
if (result.csrfToken && !ctx.client.cookies.csrf) {
|
|
3044
|
+
ctx.client.cookies.csrf = result.csrfToken;
|
|
3045
|
+
}
|
|
3046
|
+
await writeCredentials(ctx.credentialsFile, {
|
|
3047
|
+
version: 1,
|
|
3048
|
+
apiUrl: ctx.env.apiUrl,
|
|
3049
|
+
cookies: { ...ctx.client.cookies },
|
|
3050
|
+
user: result.user
|
|
3051
|
+
});
|
|
3052
|
+
ctx.logger.success(`\u5DF2\u767B\u5F55\uFF1A${summarizeUser(result.user)}`);
|
|
3053
|
+
ctx.logger.detail(`\u51ED\u636E\u5DF2\u5199\u5165 ${ctx.credentialsFile}\uFF08\u6743\u9650 0600\uFF09`);
|
|
3054
|
+
if (result.user.role !== "ADMIN" && result.user.role !== "SUPER_ADMIN") {
|
|
3055
|
+
ctx.logger.warn(`\u5F53\u524D\u89D2\u8272\u662F ${result.user.role}\uFF0C\u51FA\u9898\u63A5\u53E3\u8981\u6C42 ADMIN`);
|
|
3056
|
+
}
|
|
3057
|
+
if (ctx.logger.json) {
|
|
3058
|
+
ctx.logger.result({
|
|
3059
|
+
credentialsFile: ctx.credentialsFile,
|
|
3060
|
+
apiUrl: ctx.env.apiUrl,
|
|
3061
|
+
user: result.user
|
|
3062
|
+
});
|
|
3063
|
+
}
|
|
3064
|
+
};
|
|
3065
|
+
var runDeviceLogin = async (ctx, options) => {
|
|
3066
|
+
const flow = createDeviceFlow({
|
|
3067
|
+
baseUrl: ctx.env.apiUrl,
|
|
3068
|
+
fetch: ctx.runtime.fetch,
|
|
3069
|
+
logger: ctx.logger,
|
|
3070
|
+
...ctx.runtime.signal ? { signal: ctx.runtime.signal } : {},
|
|
3071
|
+
timeoutMs: ctx.env.timeoutMs
|
|
3072
|
+
});
|
|
3073
|
+
ctx.logger.step(`\u5411 ${ctx.env.apiUrl} \u7533\u8BF7\u8BBE\u5907\u6388\u6743\u7801`);
|
|
3074
|
+
const auth = await flow.requestCodes();
|
|
3075
|
+
ctx.logger.blank();
|
|
3076
|
+
ctx.logger.info("\u8BF7\u5728\u6D4F\u89C8\u5668\u4E2D\u5B8C\u6210\u6388\u6743\uFF1A");
|
|
3077
|
+
ctx.logger.info(` \u5730\u5740\uFF1A${auth.verification_uri}`);
|
|
3078
|
+
ctx.logger.info(` \u9A8C\u8BC1\u7801\uFF1A${ctx.logger.colors.bold(auth.user_code)}`);
|
|
3079
|
+
ctx.logger.detail(
|
|
3080
|
+
`\u9A8C\u8BC1\u7801 ${Math.round(auth.expires_in / 60)} \u5206\u949F\u5185\u6709\u6548\uFF0C\u8BF7\u5728\u786E\u8BA4\u9875\u6838\u5BF9\u4E0E\u672C\u7EC8\u7AEF\u4E00\u81F4`
|
|
3081
|
+
);
|
|
3082
|
+
ctx.logger.blank();
|
|
3083
|
+
if (!options.noBrowser) {
|
|
3084
|
+
const target = auth.verification_uri_complete || auth.verification_uri;
|
|
3085
|
+
const opened = await openBrowser(target);
|
|
3086
|
+
ctx.logger.detail(
|
|
3087
|
+
opened ? "\u5DF2\u5C1D\u8BD5\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00\u6388\u6743\u9875\u9762" : "\u65E0\u6CD5\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668\uFF0C\u8BF7\u624B\u52A8\u8BBF\u95EE\u4E0A\u9762\u7684\u5730\u5740"
|
|
3088
|
+
);
|
|
3089
|
+
}
|
|
3090
|
+
ctx.logger.step("\u7B49\u5F85\u6388\u6743\u5B8C\u6210\uFF08Ctrl-C \u53D6\u6D88\uFF09");
|
|
3091
|
+
const tokens = await flow.pollForToken(auth, (elapsedMs) => {
|
|
3092
|
+
if (ctx.logger.level === "debug") {
|
|
3093
|
+
ctx.logger.detail(
|
|
3094
|
+
`\u4ECD\u5728\u7B49\u5F85\u6388\u6743\u2026 \u5DF2\u7B49\u5F85 ${Math.round(elapsedMs / 1e3)} \u79D2`
|
|
3095
|
+
);
|
|
3096
|
+
}
|
|
3097
|
+
});
|
|
3098
|
+
ctx.client.cookies.access = tokens.access_token;
|
|
3099
|
+
ctx.client.cookies.refresh = tokens.refresh_token;
|
|
3100
|
+
await writeCredentials(ctx.credentialsFile, {
|
|
3101
|
+
version: 1,
|
|
3102
|
+
apiUrl: ctx.env.apiUrl,
|
|
3103
|
+
cookies: { ...ctx.client.cookies }
|
|
3104
|
+
});
|
|
3105
|
+
const api = createAdminApi(ctx.client);
|
|
3106
|
+
let summary = "\u5DF2\u767B\u5F55";
|
|
3107
|
+
try {
|
|
3108
|
+
const user = await api.getCurrentUser();
|
|
3109
|
+
summary = `\u5DF2\u767B\u5F55\uFF1A${summarizeUser(user)}`;
|
|
3110
|
+
await writeCredentials(ctx.credentialsFile, {
|
|
3111
|
+
version: 1,
|
|
3112
|
+
apiUrl: ctx.env.apiUrl,
|
|
3113
|
+
cookies: { ...ctx.client.cookies },
|
|
3114
|
+
user
|
|
3115
|
+
});
|
|
3116
|
+
if (user.role !== "ADMIN" && user.role !== "SUPER_ADMIN") {
|
|
3117
|
+
ctx.logger.warn(
|
|
3118
|
+
`\u5F53\u524D\u89D2\u8272\u662F ${user.role}\uFF0C\u51FA\u9898\u63A5\u53E3\u8981\u6C42 ADMIN\uFF0Cupload/publish \u4F1A\u88AB\u62D2\u7EDD`
|
|
3119
|
+
);
|
|
3120
|
+
}
|
|
3121
|
+
} catch (error) {
|
|
3122
|
+
ctx.logger.warn(
|
|
3123
|
+
`\u767B\u5F55\u6210\u529F\u4F46\u8BFB\u53D6\u7528\u6237\u4FE1\u606F\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`
|
|
3124
|
+
);
|
|
3125
|
+
}
|
|
3126
|
+
ctx.logger.success(summary);
|
|
3127
|
+
ctx.logger.detail(`\u51ED\u636E\u5DF2\u5199\u5165 ${ctx.credentialsFile}\uFF08\u6743\u9650 0600\uFF09`);
|
|
3128
|
+
if (ctx.logger.json) {
|
|
3129
|
+
ctx.logger.result({
|
|
3130
|
+
loginMethod: "device_code",
|
|
3131
|
+
credentialsFile: ctx.credentialsFile,
|
|
3132
|
+
apiUrl: ctx.env.apiUrl,
|
|
3133
|
+
scope: tokens.scope
|
|
3134
|
+
});
|
|
3135
|
+
}
|
|
3136
|
+
};
|
|
3137
|
+
var resolvePassword = async (ctx, options) => {
|
|
3138
|
+
const fromEnv = ctx.runtime.env.QUANTA_PASSWORD;
|
|
3139
|
+
if (fromEnv) return fromEnv;
|
|
3140
|
+
if (options.passwordStdin) {
|
|
3141
|
+
const value2 = await readAllStdin(ctx.runtime.stdin);
|
|
3142
|
+
if (!value2) throw new UsageError("--password-stdin \u6CA1\u6709\u8BFB\u5230\u5185\u5BB9");
|
|
3143
|
+
return value2;
|
|
3144
|
+
}
|
|
3145
|
+
if (!ctx.runtime.stdin.isTTY) {
|
|
3146
|
+
throw new UsageError("\u975E\u4EA4\u4E92\u73AF\u5883\u4E0B\u65E0\u6CD5\u63D0\u793A\u8F93\u5165\u5BC6\u7801", {
|
|
3147
|
+
hint: '\u7528 `echo "$PASSWORD" | qpc login --password-stdin`\uFF0C\u6216\u8BBE\u7F6E QUANTA_PASSWORD\u3002'
|
|
3148
|
+
});
|
|
3149
|
+
}
|
|
3150
|
+
const value = await promptHidden("\u5BC6\u7801\uFF08\u4E0D\u4F1A\u56DE\u663E\uFF09\uFF1A", {
|
|
3151
|
+
stdin: ctx.runtime.stdin,
|
|
3152
|
+
stderr: ctx.runtime.stderr
|
|
3153
|
+
});
|
|
3154
|
+
if (!value) throw new UsageError("\u5BC6\u7801\u4E3A\u7A7A");
|
|
3155
|
+
return value;
|
|
3156
|
+
};
|
|
3157
|
+
|
|
3158
|
+
// src/commands/status.ts
|
|
3159
|
+
var registerStatusCommand = (registry) => {
|
|
3160
|
+
const { program, context } = registry;
|
|
3161
|
+
program.command("status").description("\u67E5\u770B\u9898\u76EE\u72B6\u6001\u4E0E\u6700\u8FD1\u4E00\u6B21\u5BA1\u8BA1\u7684\u68C0\u67E5\u70B9\u660E\u7EC6").argument("<problemId>", "\u9898\u76EE\u7248\u672C id\uFF08upload \u8FD4\u56DE\u7684 pid\uFF09").option("--watch", "\u6301\u7EED\u5237\u65B0\uFF0C\u76F4\u5230\u5BA1\u8BA1\u51FA\u7ED3\u679C\uFF08Ctrl-C \u9000\u51FA\uFF09").option("--interval <seconds>", "\u5237\u65B0\u95F4\u9694\uFF08\u79D2\uFF09", "3").action(
|
|
3162
|
+
async (problemId, options) => {
|
|
3163
|
+
const ctx = await context();
|
|
3164
|
+
await runStatus(ctx, { problemId, ...options });
|
|
3165
|
+
}
|
|
3166
|
+
);
|
|
3167
|
+
};
|
|
3168
|
+
var runStatus = async (ctx, options) => {
|
|
3169
|
+
const problemId = parseProblemId(options.problemId);
|
|
3170
|
+
requireSession(ctx);
|
|
3171
|
+
const api = createAdminApi(ctx.client);
|
|
3172
|
+
if (!options.watch) {
|
|
3173
|
+
const detail = await api.getAuditDetail(problemId);
|
|
3174
|
+
renderAuditDetail(ctx.logger, detail, {
|
|
3175
|
+
verbose: ctx.logger.level === "debug"
|
|
3176
|
+
});
|
|
3177
|
+
if (ctx.logger.json) ctx.logger.result(auditToJson(detail));
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
3180
|
+
const intervalMs = parseInterval(options.interval);
|
|
3181
|
+
let lastSignature = "";
|
|
3182
|
+
for (; ; ) {
|
|
3183
|
+
if (ctx.runtime.signal?.aborted) return;
|
|
3184
|
+
const detail = await api.getAuditDetail(problemId);
|
|
3185
|
+
const record = auditRecordOf(detail);
|
|
3186
|
+
const signature = `${detail.status}|${record?.result ?? "none"}|${record?.score ?? 0}`;
|
|
3187
|
+
if (signature !== lastSignature) {
|
|
3188
|
+
lastSignature = signature;
|
|
3189
|
+
ctx.logger.blank();
|
|
3190
|
+
renderAuditDetail(ctx.logger, detail, {
|
|
3191
|
+
verbose: ctx.logger.level === "debug"
|
|
3192
|
+
});
|
|
3193
|
+
} else {
|
|
3194
|
+
ctx.logger.detail(`\u72B6\u6001\u672A\u53D8\u5316\uFF08${signature}\uFF09`);
|
|
3195
|
+
}
|
|
3196
|
+
if (isAuditSettled(detail)) {
|
|
3197
|
+
if (ctx.logger.json) ctx.logger.result(auditToJson(detail));
|
|
3198
|
+
ctx.logger.success("\u5BA1\u8BA1\u5DF2\u51FA\u7ED3\u679C\uFF0C\u505C\u6B62\u5237\u65B0");
|
|
3199
|
+
return;
|
|
3200
|
+
}
|
|
3201
|
+
await sleep(intervalMs, ctx.runtime.signal);
|
|
3202
|
+
}
|
|
3203
|
+
};
|
|
3204
|
+
var parseProblemId = (value) => {
|
|
3205
|
+
const parsed = Number.parseInt(value, 10);
|
|
3206
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
3207
|
+
throw new UsageError(`\u9898\u76EE id \u5FC5\u987B\u662F\u6B63\u6574\u6570\uFF1A${value}`, {
|
|
3208
|
+
hint: "id \u6765\u81EA `qpc upload` \u7684\u8F93\u51FA\uFF0C\u6216\u7BA1\u7406\u7AEF\u9898\u76EE\u5217\u8868\u3002"
|
|
3209
|
+
});
|
|
3210
|
+
}
|
|
3211
|
+
return parsed;
|
|
3212
|
+
};
|
|
3213
|
+
var parseInterval = (value) => {
|
|
3214
|
+
const seconds = Number.parseFloat(value ?? "3");
|
|
3215
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
3216
|
+
throw new UsageError(`--interval \u5FC5\u987B\u662F\u6B63\u6570\uFF08\u79D2\uFF09\uFF1A${value}`);
|
|
3217
|
+
}
|
|
3218
|
+
return seconds * 1e3;
|
|
3219
|
+
};
|
|
3220
|
+
|
|
3221
|
+
// src/commands/publish.ts
|
|
3222
|
+
var registerPublishCommand = (registry) => {
|
|
3223
|
+
const { program, context } = registry;
|
|
3224
|
+
program.command("publish").description("\u53D1\u5E03\u9898\u76EE\uFF08status: ready \u2192 published\uFF09\uFF0C\u6216 --unpublish \u4E0B\u67B6").argument("<problemId>", "\u9898\u76EE\u7248\u672C id").option("--unpublish", "\u4E0B\u67B6\uFF08published \u2192 ready\uFF09").action(
|
|
3225
|
+
async (problemId, options) => {
|
|
3226
|
+
const ctx = await context();
|
|
3227
|
+
await runPublish(ctx, { problemId, ...options });
|
|
3228
|
+
}
|
|
3229
|
+
);
|
|
3230
|
+
};
|
|
3231
|
+
var runPublish = async (ctx, options) => {
|
|
3232
|
+
const problemId = parseProblemId(options.problemId);
|
|
3233
|
+
requireSession(ctx);
|
|
3234
|
+
const api = createAdminApi(ctx.client);
|
|
3235
|
+
const detail = await api.getAuditDetail(problemId);
|
|
3236
|
+
const publish = !options.unpublish;
|
|
3237
|
+
if (publish && !["ready", "published"].includes(detail.status)) {
|
|
3238
|
+
throw new UsageError(
|
|
3239
|
+
`\u9898\u76EE #${problemId} \u5F53\u524D\u72B6\u6001\u662F ${detail.status}\uFF0C\u4E0D\u80FD\u53D1\u5E03`,
|
|
3240
|
+
{
|
|
3241
|
+
hint: "\u53EA\u6709\u5BA1\u8BA1\u901A\u8FC7\uFF08status=ready\uFF09\u7684\u7248\u672C\u624D\u80FD\u53D1\u5E03\uFF1B\u5148 `qpc status <id> --watch` \u770B\u5BA1\u8BA1\u7ED3\u679C\u3002"
|
|
3242
|
+
}
|
|
3243
|
+
);
|
|
3244
|
+
}
|
|
3245
|
+
if (!publish && detail.status !== "published") {
|
|
3246
|
+
throw new UsageError(
|
|
3247
|
+
`\u9898\u76EE #${problemId} \u5F53\u524D\u72B6\u6001\u662F ${detail.status}\uFF0C\u65E0\u9700\u4E0B\u67B6`
|
|
3248
|
+
);
|
|
3249
|
+
}
|
|
3250
|
+
ctx.logger.step(
|
|
3251
|
+
`${publish ? "\u53D1\u5E03" : "\u4E0B\u67B6"}\u9898\u76EE #${problemId}\uFF08${detail.title}\uFF09`
|
|
3252
|
+
);
|
|
3253
|
+
const result = await api.setStatus(problemId, publish);
|
|
3254
|
+
ctx.logger.success(result.message);
|
|
3255
|
+
if (ctx.logger.json) {
|
|
3256
|
+
ctx.logger.result({
|
|
3257
|
+
problemId,
|
|
3258
|
+
publish,
|
|
3259
|
+
status: publish ? "published" : "ready",
|
|
3260
|
+
message: result.message
|
|
3261
|
+
});
|
|
3262
|
+
}
|
|
3263
|
+
};
|
|
3264
|
+
var registerTagCommand = (registry) => {
|
|
3265
|
+
const { program, context } = registry;
|
|
3266
|
+
program.command("tag").description("\u67E5\u770B\u5E73\u53F0\u6807\u7B7E\uFF1B--write \u4EA4\u4E92\u591A\u9009\u540E\u628A tagIds \u5199\u56DE\u914D\u7F6E").argument("[dir]", "\u9898\u76EE\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09", ".").option("--write", "\u4EA4\u4E92\u5F0F\u591A\u9009\u6807\u7B7E\u5E76\u5199\u56DE\u914D\u7F6E").option("--set <id...>", "\u76F4\u63A5\u6307\u5B9A\u6807\u7B7E id\uFF08\u9017\u53F7\u6216\u7A7A\u683C\u5206\u9694\uFF09\uFF0C\u5199\u56DE\u914D\u7F6E").action(async (dir, options) => {
|
|
3267
|
+
const ctx = await context();
|
|
3268
|
+
await runTag(ctx, { dir, ...options });
|
|
3269
|
+
});
|
|
3270
|
+
};
|
|
3271
|
+
var runTag = async (ctx, options) => {
|
|
3272
|
+
if (options.write && options.set !== void 0) {
|
|
3273
|
+
throw new UsageError("--write \u4E0E --set \u4E0D\u80FD\u540C\u65F6\u4F7F\u7528", {
|
|
3274
|
+
hint: "--write \u662F\u4EA4\u4E92\u591A\u9009\uFF0C--set \u662F\u76F4\u63A5\u6307\u5B9A id\uFF0C\u4E8C\u9009\u4E00\u3002"
|
|
3275
|
+
});
|
|
3276
|
+
}
|
|
3277
|
+
const api = createAdminApi(ctx.client);
|
|
3278
|
+
ctx.logger.step("\u8BFB\u53D6\u5E73\u53F0\u6807\u7B7E\uFF08public.tag.list\uFF09");
|
|
3279
|
+
const tags = await api.listTags();
|
|
3280
|
+
if (!options.write && options.set === void 0) {
|
|
3281
|
+
renderTags(ctx, tags);
|
|
3282
|
+
if (ctx.logger.json) ctx.logger.result({ tags });
|
|
3283
|
+
return;
|
|
3284
|
+
}
|
|
3285
|
+
if (tags.length === 0) {
|
|
3286
|
+
throw new UsageError("\u5E73\u53F0\u4E0A\u8FD8\u6CA1\u6709\u6807\u7B7E\uFF0C\u65E0\u6CD5\u914D\u7F6E", {
|
|
3287
|
+
hint: "\u5148\u5728\u7BA1\u7406\u7AEF\u300C\u6807\u7B7E\u7BA1\u7406\u300D\u521B\u5EFA\u6807\u7B7E\uFF0C\u518D\u91CD\u8DD1\u3002"
|
|
3288
|
+
});
|
|
3289
|
+
}
|
|
3290
|
+
const rootDir = resolve(ctx.cwd, options.dir);
|
|
3291
|
+
const configFile = await findConfigFile(rootDir);
|
|
3292
|
+
const tagIds = options.set !== void 0 ? assertTagsExist(parseTagIds(options.set), tags) : await pickTags(ctx, tags, await readCurrentTagIds(configFile));
|
|
3293
|
+
const written = await writeBackTagIds(configFile, tagIds);
|
|
3294
|
+
ctx.logger.blank();
|
|
3295
|
+
renderTags(ctx, tags, new Set(tagIds));
|
|
3296
|
+
ctx.logger.success(
|
|
3297
|
+
`tagIds \u5DF2\u5199\u56DE ${displayPath(ctx.cwd, written)}\uFF1A[${tagIds.join(", ")}]`
|
|
3298
|
+
);
|
|
3299
|
+
if (ctx.logger.json) {
|
|
3300
|
+
ctx.logger.result({
|
|
3301
|
+
tagIds,
|
|
3302
|
+
tags,
|
|
3303
|
+
configFile
|
|
3304
|
+
});
|
|
3305
|
+
}
|
|
3306
|
+
};
|
|
3307
|
+
var renderTags = (ctx, tags, selected) => {
|
|
3308
|
+
if (tags.length === 0) {
|
|
3309
|
+
ctx.logger.warn("\u5E73\u53F0\u4E0A\u8FD8\u6CA1\u6709\u6807\u7B7E");
|
|
3310
|
+
return;
|
|
3311
|
+
}
|
|
3312
|
+
const { colors } = ctx.logger;
|
|
3313
|
+
ctx.logger.step(`\u5E73\u53F0\u6807\u7B7E\uFF08${tags.length} \u4E2A\uFF09`);
|
|
3314
|
+
for (const tag of tags) {
|
|
3315
|
+
const mark = selected ? selected.has(tag.tid) ? colors.green("\u25CF") : colors.dim("\u25CB") : colors.dim("\xB7");
|
|
3316
|
+
const color = tag.color ? colors.dim(tag.color) : "";
|
|
3317
|
+
ctx.logger.info(` ${mark} #${tag.tid} ${tag.name} ${color}`);
|
|
3318
|
+
if (tag.description) ctx.logger.detail(tag.description);
|
|
3319
|
+
}
|
|
3320
|
+
};
|
|
3321
|
+
var pickTags = async (ctx, tags, current) => {
|
|
3322
|
+
const chosen = await multiselect(
|
|
3323
|
+
"\u9009\u62E9\u672C\u9898\u7684\u6807\u7B7E\uFF08\u7A7A\u683C\u5207\u6362\uFF0C\u56DE\u8F66\u786E\u8BA4\uFF09",
|
|
3324
|
+
tags.map((tag) => ({
|
|
3325
|
+
value: String(tag.tid),
|
|
3326
|
+
label: `#${tag.tid} ${tag.name}`,
|
|
3327
|
+
...tag.color ? { hint: tag.color } : {}
|
|
3328
|
+
})),
|
|
3329
|
+
{
|
|
3330
|
+
stdin: ctx.runtime.stdin,
|
|
3331
|
+
stderr: ctx.runtime.stderr,
|
|
3332
|
+
stderrStream: ctx.runtime.stderrStream
|
|
3333
|
+
},
|
|
3334
|
+
{ initialValues: current.map(String), required: true }
|
|
3335
|
+
);
|
|
3336
|
+
return chosen.map((value) => Number.parseInt(value, 10));
|
|
3337
|
+
};
|
|
3338
|
+
var readCurrentTagIds = async (configFile) => {
|
|
3339
|
+
const content = await readText(configFile);
|
|
3340
|
+
const match = /\btagIds\s*:\s*\[([^\]]*)\]/.exec(content);
|
|
3341
|
+
if (!match) return [];
|
|
3342
|
+
return (match[1] ?? "").split(",").map((part) => Number.parseInt(part.trim(), 10)).filter((id) => Number.isInteger(id) && id > 0);
|
|
3343
|
+
};
|
|
3344
|
+
var parseTagIds = (values) => {
|
|
3345
|
+
const parts = values.flatMap((value) => value.split(",")).map((part) => part.trim()).filter(Boolean);
|
|
3346
|
+
const parsed = parts.map((part) => Number.parseInt(part, 10));
|
|
3347
|
+
if (parsed.length === 0 || parsed.some((id) => !Number.isInteger(id) || id <= 0)) {
|
|
3348
|
+
throw new UsageError(`\u6807\u7B7E id \u5FC5\u987B\u662F\u6B63\u6574\u6570\uFF1A${parts.join(", ")}`, {
|
|
3349
|
+
hint: "\u8FD0\u884C `qpc tag` \u67E5\u770B\u53EF\u7528 id\u3002"
|
|
3350
|
+
});
|
|
3351
|
+
}
|
|
3352
|
+
return parsed;
|
|
3353
|
+
};
|
|
3354
|
+
var assertTagsExist = (tagIds, tags) => {
|
|
3355
|
+
const known = new Set(tags.map((tag) => tag.tid));
|
|
3356
|
+
const missing = tagIds.filter((id) => !known.has(id));
|
|
3357
|
+
if (missing.length > 0) {
|
|
3358
|
+
throw new UsageError(`\u6807\u7B7E\u4E0D\u5B58\u5728\uFF1A${missing.join(", ")}`, {
|
|
3359
|
+
hint: `\u53EF\u7528 id\uFF1A${tags.map((tag) => tag.tid).join(", ")}\uFF08\`qpc tag\` \u770B\u540D\u79F0\uFF09\u3002`
|
|
3360
|
+
});
|
|
3361
|
+
}
|
|
3362
|
+
return tagIds;
|
|
3363
|
+
};
|
|
3364
|
+
var writeBackTagIds = async (configFile, tagIds) => {
|
|
3365
|
+
const original = await readText(configFile);
|
|
3366
|
+
const patched = applyTagIdsUpdate(original, tagIds, configFile);
|
|
3367
|
+
await writeTextAtomic(configFile, patched);
|
|
3368
|
+
try {
|
|
3369
|
+
const reloaded = await loadProblemConfig(resolve(configFile, ".."));
|
|
3370
|
+
const same = reloaded.tagIds.length === tagIds.length && reloaded.tagIds.every((id, index) => id === tagIds[index]);
|
|
3371
|
+
if (!same) throw new Error("\u5199\u56DE\u540E\u8BFB\u53D6\u5230\u7684 tagIds \u4E0E\u9884\u671F\u4E0D\u4E00\u81F4");
|
|
3372
|
+
} catch (error) {
|
|
3373
|
+
await writeTextAtomic(configFile, original);
|
|
3374
|
+
throw new ConfigError("\u5199\u56DE tagIds \u540E\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF0C\u5DF2\u56DE\u6EDA\u539F\u6587\u4EF6", {
|
|
3375
|
+
cause: error,
|
|
3376
|
+
hint: `\u624B\u52A8\u628A tagIds \u586B\u6210 [${tagIds.join(", ")}]\u3002`
|
|
3377
|
+
});
|
|
3378
|
+
}
|
|
3379
|
+
return configFile;
|
|
3380
|
+
};
|
|
3381
|
+
var registerUploadCommand = (registry) => {
|
|
3382
|
+
const { program, context } = registry;
|
|
3383
|
+
program.command("upload").description("\u4E0A\u4F20\u9898\u76EE\uFF08\u65B0\u5EFA\u6216\u8FFD\u52A0\u7248\u672C\uFF09\u5E76\u53EF\u7B49\u5F85\u7EBF\u4E0A\u5BA1\u8BA1\u7ED3\u679C").argument("[dir]", "\u9898\u76EE\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09", ".").option("--dry-run", "\u53EA\u505A\u9884\u68C0\u5E76\u6253\u5370\u5C06\u8981\u63D0\u4EA4\u7684\u5185\u5BB9").option("--skip-check", "\u8DF3\u8FC7\u9884\u68C0\uFF08\u4E0D\u5EFA\u8BAE\uFF09").option("--no-wait", "\u4E0A\u4F20\u540E\u4E0D\u7B49\u5F85\u5BA1\u8BA1").option(
|
|
3384
|
+
"--problem <pid>",
|
|
3385
|
+
"\u5728\u8BE5\u9898\u76EE\u4E0B\u8FFD\u52A0\u65B0\u7248\u672C\uFF08\u8986\u76D6\u914D\u7F6E\u91CC\u7684 problemId\uFF09"
|
|
3386
|
+
).option("--new", "\u5F3A\u5236\u65B0\u5EFA\u9898\u76EE\uFF08\u5FFD\u7565\u914D\u7F6E\u91CC\u7684 problemId\uFF09").option("--write", "\u628A\u672C\u6B21\u4F7F\u7528\u7684\u9898\u76EE id \u5199\u56DE\u914D\u7F6E").option("--timeout <seconds>", "\u5BA1\u8BA1\u7B49\u5F85\u8D85\u65F6\uFF08\u79D2\uFF09", "180").option("--interval <seconds>", "\u5BA1\u8BA1\u72B6\u6001\u8F6E\u8BE2\u95F4\u9694\uFF08\u79D2\uFF09", "2").action(async (dir, options) => {
|
|
3387
|
+
const ctx = await context();
|
|
3388
|
+
await runUpload(ctx, { dir, ...options });
|
|
3389
|
+
});
|
|
3390
|
+
};
|
|
3391
|
+
var runUpload = async (ctx, options) => {
|
|
3392
|
+
const rootDir = resolve(ctx.cwd, options.dir);
|
|
3393
|
+
const config = await loadProblemConfig(rootDir);
|
|
3394
|
+
if (options.new && options.problem !== void 0) {
|
|
3395
|
+
throw new UsageError("--new \u4E0E --problem \u4E0D\u80FD\u540C\u65F6\u4F7F\u7528", {
|
|
3396
|
+
hint: "\u8981\u65B0\u5EFA\u9898\u76EE\u5C31\u522B\u6307\u5B9A --problem\uFF1B\u8981\u8FFD\u52A0\u7248\u672C\u5C31\u522B\u52A0 --new\u3002"
|
|
3397
|
+
});
|
|
3398
|
+
}
|
|
3399
|
+
const targetProblemId = resolveTargetProblemId(options, config.problemId);
|
|
3400
|
+
let report;
|
|
3401
|
+
if (options.skipCheck) {
|
|
3402
|
+
ctx.logger.warn(
|
|
3403
|
+
"\u5DF2\u8DF3\u8FC7\u9884\u68C0\uFF08--skip-check\uFF09\uFF1A\u9759\u6001\u9519\u8BEF\u4F1A\u7559\u5230\u7EBF\u4E0A\u5BA1\u8BA1\u624D\u66B4\u9732"
|
|
3404
|
+
);
|
|
3405
|
+
report = await validateProblem(config, {
|
|
3406
|
+
maxUploadBytes: ctx.env.maxUploadBytes
|
|
3407
|
+
});
|
|
3408
|
+
} else {
|
|
3409
|
+
report = await validateProblem(config, {
|
|
3410
|
+
maxUploadBytes: ctx.env.maxUploadBytes
|
|
3411
|
+
});
|
|
3412
|
+
renderFindings(ctx.logger, report.findings);
|
|
3413
|
+
if (!report.ok) {
|
|
3414
|
+
throw new PrecheckError(
|
|
3415
|
+
`\u9884\u68C0\u672A\u901A\u8FC7\uFF1A${report.errors} \u4E2A\u9519\u8BEF\uFF0C\u5DF2\u4E2D\u6B62\u4E0A\u4F20`,
|
|
3416
|
+
{
|
|
3417
|
+
hint: "\u4FEE\u590D\u540E\u91CD\u8DD1\uFF1B\u786E\u5B9E\u8981\u5F3A\u884C\u4E0A\u4F20\u65F6\u7528 --skip-check\u3002"
|
|
3418
|
+
}
|
|
3419
|
+
);
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
const judgeScript = await readText(config.paths.judge);
|
|
3423
|
+
const payload = {
|
|
3424
|
+
title: config.title,
|
|
3425
|
+
detail: config.detail,
|
|
3426
|
+
tagIds: config.tagIds,
|
|
3427
|
+
difficulty: config.difficulty,
|
|
3428
|
+
totalScore: report.totalScore,
|
|
3429
|
+
judgeScript,
|
|
3430
|
+
answerTemplateSnapshot: report.templateSnapshot,
|
|
3431
|
+
referenceAnswerSnapshot: report.answerSnapshot,
|
|
3432
|
+
coverMode: config.cover.mode,
|
|
3433
|
+
...config.cover.mode === "custom" ? { coverImageId: config.cover.imageId } : {},
|
|
3434
|
+
...config.runtime.bootCommand ? { bootCommand: config.runtime.bootCommand } : {},
|
|
3435
|
+
...config.runtime.buildCommand ? { buildCommand: config.runtime.buildCommand } : {},
|
|
3436
|
+
initCommand: config.runtime.initCommand,
|
|
3437
|
+
judgeUploadPath: config.runtime.judgeUploadPath,
|
|
3438
|
+
problemType: config.type,
|
|
3439
|
+
...config.runtime.serviceImage ? { serviceImage: config.runtime.serviceImage } : {},
|
|
3440
|
+
servicePort: config.runtime.servicePort
|
|
3441
|
+
};
|
|
3442
|
+
const payloadBytes = Buffer.byteLength(judgeScript, "utf8") + report.stats.template.totalBytes + report.stats.answer.totalBytes;
|
|
3443
|
+
ctx.logger.step("\u4E0A\u4F20\u5185\u5BB9\u6458\u8981");
|
|
3444
|
+
ctx.logger.table([
|
|
3445
|
+
["\u6807\u9898", config.title],
|
|
3446
|
+
["\u96BE\u5EA6 / \u603B\u5206", `${config.difficulty} / ${report.totalScore}`],
|
|
3447
|
+
["\u6807\u7B7E", config.tagIds.join(", ")],
|
|
3448
|
+
["\u5224\u9898\u6253\u5305\u76EE\u5F55", config.runtime.judgeUploadPath],
|
|
3449
|
+
["\u9898\u578B", config.type],
|
|
3450
|
+
["\u542F\u52A8\u547D\u4EE4", config.runtime.initCommand ?? "<\u9ED8\u8BA4>"],
|
|
3451
|
+
["\u7B54\u6848\u6A21\u677F", `${report.stats.template.fileCount} \u4E2A\u6587\u4EF6`],
|
|
3452
|
+
["\u53C2\u8003\u89E3", `${report.stats.answer.fileCount} \u4E2A\u6587\u4EF6`],
|
|
3453
|
+
["\u5224\u9898\u811A\u672C", `${Buffer.byteLength(judgeScript, "utf8")} \u5B57\u8282`],
|
|
3454
|
+
["\u8BF7\u6C42\u4F53", formatBytes(payloadBytes)],
|
|
3455
|
+
[
|
|
3456
|
+
"\u76EE\u6807",
|
|
3457
|
+
targetProblemId === void 0 ? "\u65B0\u5EFA\u9898\u76EE" : `\u9898\u76EE #${targetProblemId} \u7684\u65B0\u7248\u672C`
|
|
3458
|
+
],
|
|
3459
|
+
[
|
|
3460
|
+
"\u63A5\u53E3",
|
|
3461
|
+
`${ctx.env.apiUrl} \u2192 ${targetProblemId === void 0 ? "admin.problem.upload" : "admin.problem.reupload"}`
|
|
3462
|
+
]
|
|
3463
|
+
]);
|
|
3464
|
+
if (options.dryRun) {
|
|
3465
|
+
ctx.logger.success("--dry-run\uFF1A\u672A\u53D1\u8D77\u4EFB\u4F55\u8BF7\u6C42");
|
|
3466
|
+
if (ctx.logger.json) {
|
|
3467
|
+
ctx.logger.result({
|
|
3468
|
+
dryRun: true,
|
|
3469
|
+
mode: targetProblemId === void 0 ? "create" : "version",
|
|
3470
|
+
targetProblemId,
|
|
3471
|
+
payload: {
|
|
3472
|
+
...payload,
|
|
3473
|
+
judgeScript: `<${judgeScript.length} \u5B57\u7B26>`,
|
|
3474
|
+
answerTemplateSnapshot: `<${report.stats.template.fileCount} \u4E2A\u6587\u4EF6>`,
|
|
3475
|
+
referenceAnswerSnapshot: `<${report.stats.answer.fileCount} \u4E2A\u6587\u4EF6>`
|
|
3476
|
+
},
|
|
3477
|
+
validation: validationToJson(report)
|
|
3478
|
+
});
|
|
3479
|
+
}
|
|
3480
|
+
return;
|
|
3481
|
+
}
|
|
3482
|
+
requireSession(ctx);
|
|
3483
|
+
const api = createAdminApi(ctx.client);
|
|
3484
|
+
let baseId;
|
|
3485
|
+
if (targetProblemId !== void 0) {
|
|
3486
|
+
ctx.logger.step(`\u67E5\u627E\u9898\u76EE #${targetProblemId} \u7684\u57FA\u7840\u9898 id`);
|
|
3487
|
+
const target = await api.getAuditDetail(targetProblemId);
|
|
3488
|
+
baseId = target.BaseProblem?.id;
|
|
3489
|
+
if (baseId === void 0) {
|
|
3490
|
+
throw new PrecheckError(
|
|
3491
|
+
`\u9898\u76EE #${targetProblemId} \u6CA1\u6709\u5173\u8054\u7684\u57FA\u7840\u9898\uFF0C\u65E0\u6CD5\u8FFD\u52A0\u7248\u672C`,
|
|
3492
|
+
{
|
|
3493
|
+
hint: "\u786E\u8BA4\u8FD9\u4E2A id \u6765\u81EA `qpc upload` / \u7BA1\u7406\u7AEF\uFF0C\u4E14\u5C5E\u4E8E\u5F53\u524D\u8D26\u53F7\u3002"
|
|
3494
|
+
}
|
|
3495
|
+
);
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
await confirmOrSkip(
|
|
3499
|
+
ctx,
|
|
3500
|
+
targetProblemId === void 0 ? "\u786E\u8BA4\u521B\u5EFA\u65B0\u9898\u76EE\uFF1F" : `\u786E\u8BA4\u5728\u9898\u76EE #${targetProblemId} \u4E0B\u521B\u5EFA\u65B0\u7248\u672C\uFF1F`
|
|
3501
|
+
);
|
|
3502
|
+
ctx.logger.step(
|
|
3503
|
+
baseId === void 0 ? "\u63D0\u4EA4 upload \u8BF7\u6C42" : "\u63D0\u4EA4 reupload \u8BF7\u6C42"
|
|
3504
|
+
);
|
|
3505
|
+
const created = baseId === void 0 ? await api.uploadProblem(payload) : await api.reuploadProblem({ ...payload, baseId });
|
|
3506
|
+
const problemId = created.problemId;
|
|
3507
|
+
ctx.logger.success(
|
|
3508
|
+
baseId === void 0 ? `\u9898\u76EE\u5DF2\u521B\u5EFA\uFF1A#${problemId}\uFF08${created.message}\uFF09` : `\u65B0\u7248\u672C\u5DF2\u521B\u5EFA\uFF1A#${problemId}\uFF08${created.message}\uFF09`
|
|
3509
|
+
);
|
|
3510
|
+
const writeBackIfRequested = async () => {
|
|
3511
|
+
if (!options.write) return;
|
|
3512
|
+
const written = await writeBackProblemId(config.configFile, problemId);
|
|
3513
|
+
ctx.logger.success(
|
|
3514
|
+
`problemId \u5DF2\u5199\u56DE ${relative(ctx.cwd, written) || written}`
|
|
3515
|
+
);
|
|
3516
|
+
};
|
|
3517
|
+
if (!options.write) {
|
|
3518
|
+
if (targetProblemId === void 0 && config.problemId === void 0) {
|
|
3519
|
+
ctx.logger.info(
|
|
3520
|
+
` \u52A0 --write \u53EF\u628A problemId: ${problemId} \u5199\u56DE\u914D\u7F6E\uFF0C\u4E4B\u540E\u4E0A\u4F20\u9ED8\u8BA4\u5728\u8BE5\u9898\u76EE\u4E0B\u8FFD\u52A0\u7248\u672C`
|
|
3521
|
+
);
|
|
3522
|
+
} else if (options.new && config.problemId !== void 0) {
|
|
3523
|
+
ctx.logger.warn(
|
|
3524
|
+
` \u914D\u7F6E\u91CC\u7684 problemId: ${config.problemId} \u4ECD\u6307\u5411\u65E7\u9898\u76EE\uFF1B\u8981\u6539\u7528\u65B0\u9898\u76EE\u8BF7\u52A0 --write`
|
|
3525
|
+
);
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
if (options.wait === false) {
|
|
3529
|
+
await writeBackIfRequested();
|
|
3530
|
+
ctx.logger.info(` \u5BA1\u8BA1\u72B6\u6001\uFF1Aqpc status ${problemId} --watch`);
|
|
3531
|
+
if (ctx.logger.json) ctx.logger.result({ problemId, waited: false });
|
|
3532
|
+
return;
|
|
3533
|
+
}
|
|
3534
|
+
const detail = await waitForAudit(
|
|
3535
|
+
ctx,
|
|
3536
|
+
problemId,
|
|
3537
|
+
options.timeout,
|
|
3538
|
+
options.interval
|
|
3539
|
+
);
|
|
3540
|
+
const passed = renderAuditDetail(ctx.logger, detail, {
|
|
3541
|
+
verbose: ctx.logger.level === "debug"
|
|
3542
|
+
});
|
|
3543
|
+
if (ctx.logger.json) ctx.logger.result({ problemId, waited: true, detail });
|
|
3544
|
+
if (!passed) {
|
|
3545
|
+
const hasRecord = auditRecordOf(detail) !== void 0;
|
|
3546
|
+
const reason = hasRecord ? '\u770B\u4E0A\u9762\u7684\u68C0\u67E5\u70B9\u660E\u7EC6\uFF1A\u5206\u6570\u4E0D\u7B49\u901A\u5E38\u662F"\u53C2\u8003\u89E3\u62FF\u4E0D\u5230\u6EE1\u5206"\u6216"totalScore \u4E0E\u68C0\u67E5\u70B9\u4E4B\u548C\u4E0D\u4E00\u81F4"\u3002' : "\u9898\u76EE\u72B6\u6001\u5DF2\u7F6E\u4E3A invalid\uFF0C\u4F46\u5224\u9898\u8BB0\u5F55\u6CA1\u6709\u5199\u56DE\uFF1A\u591A\u534A\u662F\u5224\u9898\u673A/\u5BB9\u5668\u51FA\u7ED3\u679C\u524D\u5C31\u5D29\u4E86\uFF0C\u770B\u8C03\u5EA6\u5668\u65E5\u5FD7\u3002";
|
|
3547
|
+
throw new PrecheckError(`\u9898\u76EE #${problemId} \u5BA1\u8BA1\u672A\u901A\u8FC7`, {
|
|
3548
|
+
hint: options.write ? `${reason}\uFF08--write \u5DF2\u8DF3\u8FC7\uFF1A\u5BA1\u8BA1\u672A\u901A\u8FC7\u4E0D\u5199\u56DE problemId\uFF09` : reason
|
|
3549
|
+
});
|
|
3550
|
+
}
|
|
3551
|
+
await writeBackIfRequested();
|
|
3552
|
+
ctx.logger.success(`\u5BA1\u8BA1\u901A\u8FC7\uFF1A${detail.totalScore} \u5206\u6EE1\u5206`);
|
|
3553
|
+
ctx.logger.info(` \u53D1\u5E03\uFF1Aqpc publish ${problemId}`);
|
|
3554
|
+
};
|
|
3555
|
+
var waitForAudit = async (ctx, problemId, timeoutSeconds, intervalSeconds) => {
|
|
3556
|
+
const api = createAdminApi(ctx.client);
|
|
3557
|
+
const timeoutMs = parsePositiveSeconds(timeoutSeconds, 180) * 1e3;
|
|
3558
|
+
const intervalMs = parsePositiveSeconds(intervalSeconds, 2) * 1e3;
|
|
3559
|
+
ctx.logger.step(`\u7B49\u5F85\u5BA1\u8BA1\u7ED3\u679C\uFF08\u6700\u591A ${Math.round(timeoutMs / 1e3)} \u79D2\uFF09`);
|
|
3560
|
+
const detail = await poll({
|
|
3561
|
+
intervalMs,
|
|
3562
|
+
timeoutMs,
|
|
3563
|
+
signal: ctx.runtime.signal,
|
|
3564
|
+
probe: async () => {
|
|
3565
|
+
const current = await api.getAuditDetail(problemId);
|
|
3566
|
+
if (isAuditSettled(current)) return current;
|
|
3567
|
+
ctx.logger.detail(`\u5BA1\u8BA1\u8FDB\u884C\u4E2D\u2026\uFF08status=${current.status}\uFF09`);
|
|
3568
|
+
return void 0;
|
|
3569
|
+
}
|
|
3570
|
+
});
|
|
3571
|
+
if (!detail) {
|
|
3572
|
+
throw new PrecheckError(`\u7B49\u5F85\u5BA1\u8BA1\u8D85\u65F6\uFF08\u9898\u76EE #${problemId}\uFF09`, {
|
|
3573
|
+
hint: `\u53EF\u80FD\u539F\u56E0\uFF1A\u8C03\u5EA6\u5668\u672A\u8FD0\u884C / \u5224\u9898\u673A\u5BB9\u5668\u8D77\u4E0D\u6765\u3002\u68C0\u67E5 qpc status ${problemId}\uFF0C\u5E76\u770B\u8C03\u5EA6\u5668\u65E5\u5FD7\u3002`
|
|
3574
|
+
});
|
|
3575
|
+
}
|
|
3576
|
+
return detail;
|
|
3577
|
+
};
|
|
3578
|
+
var parsePositiveSeconds = (value, fallback) => {
|
|
3579
|
+
if (value === void 0) return fallback;
|
|
3580
|
+
const seconds = Number.parseFloat(value);
|
|
3581
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
3582
|
+
throw new UsageError(`\u65F6\u95F4\u53C2\u6570\u5FC5\u987B\u662F\u6B63\u6570\uFF08\u79D2\uFF09\uFF1A${value}`);
|
|
3583
|
+
}
|
|
3584
|
+
return seconds;
|
|
3585
|
+
};
|
|
3586
|
+
var resolveTargetProblemId = (options, configuredProblemId) => {
|
|
3587
|
+
if (options.new) return void 0;
|
|
3588
|
+
if (options.problem !== void 0) return parseProblemId(options.problem);
|
|
3589
|
+
return configuredProblemId;
|
|
3590
|
+
};
|
|
3591
|
+
var writeBackProblemId = async (configFile, problemId) => {
|
|
3592
|
+
const original = await readText(configFile);
|
|
3593
|
+
const patched = applyProblemIdUpdate(original, problemId, configFile);
|
|
3594
|
+
await writeTextAtomic(configFile, patched);
|
|
3595
|
+
try {
|
|
3596
|
+
const reloaded = await loadProblemConfig(resolve(configFile, ".."));
|
|
3597
|
+
if (reloaded.problemId !== problemId) {
|
|
3598
|
+
throw new Error("\u5199\u56DE\u540E\u8BFB\u53D6\u5230\u7684 problemId \u4E0E\u9884\u671F\u4E0D\u4E00\u81F4");
|
|
3599
|
+
}
|
|
3600
|
+
} catch (error) {
|
|
3601
|
+
await writeTextAtomic(configFile, original);
|
|
3602
|
+
throw new ConfigError("\u5199\u56DE problemId \u540E\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF0C\u5DF2\u56DE\u6EDA\u539F\u6587\u4EF6", {
|
|
3603
|
+
cause: error,
|
|
3604
|
+
hint: `\u624B\u52A8\u628A problemId: ${problemId} \u586B\u8FDB\u914D\u7F6E\u3002`
|
|
3605
|
+
});
|
|
3606
|
+
}
|
|
3607
|
+
return configFile;
|
|
3608
|
+
};
|
|
3609
|
+
|
|
3610
|
+
// src/commands/index.ts
|
|
3611
|
+
var registerCommands = (registry) => {
|
|
3612
|
+
registerInitCommand(registry);
|
|
3613
|
+
registerCheckCommand(registry);
|
|
3614
|
+
registerCoverCommand(registry);
|
|
3615
|
+
registerTagCommand(registry);
|
|
3616
|
+
registerUploadCommand(registry);
|
|
3617
|
+
registerStatusCommand(registry);
|
|
3618
|
+
registerPublishCommand(registry);
|
|
3619
|
+
registerLoginCommand(registry);
|
|
3620
|
+
registerDoctorCommand(registry);
|
|
3621
|
+
};
|
|
3622
|
+
|
|
3623
|
+
// src/services/http.ts
|
|
3624
|
+
var COOKIE_NAMES = {
|
|
3625
|
+
access: "quanta_access_token",
|
|
3626
|
+
refresh: "quanta_refresh_token",
|
|
3627
|
+
csrf: "quanta_csrf_token"
|
|
3628
|
+
};
|
|
3629
|
+
var HttpClient = class {
|
|
3630
|
+
baseUrl;
|
|
3631
|
+
cookies;
|
|
3632
|
+
options;
|
|
3633
|
+
refreshing;
|
|
3634
|
+
constructor(options) {
|
|
3635
|
+
this.options = options;
|
|
3636
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
3637
|
+
this.cookies = { ...options.cookies };
|
|
3638
|
+
}
|
|
3639
|
+
get hasSession() {
|
|
3640
|
+
return Boolean(this.cookies.access || this.cookies.refresh);
|
|
3641
|
+
}
|
|
3642
|
+
url(path) {
|
|
3643
|
+
if (/^https?:\/\//i.test(path)) return path;
|
|
3644
|
+
return `${this.baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
|
|
3645
|
+
}
|
|
3646
|
+
headers(extra = {}) {
|
|
3647
|
+
const headers = {
|
|
3648
|
+
accept: "application/json",
|
|
3649
|
+
"user-agent": `qpc/${CLI_VERSION} (quanta-problem-creator)`,
|
|
3650
|
+
...extra
|
|
3651
|
+
};
|
|
3652
|
+
const cookie = Object.entries(COOKIE_NAMES).map(([key, name]) => {
|
|
3653
|
+
const value = this.cookies[key];
|
|
3654
|
+
return value ? `${name}=${value}` : void 0;
|
|
3655
|
+
}).filter((entry) => Boolean(entry)).join("; ");
|
|
3656
|
+
if (cookie) headers.cookie = cookie;
|
|
3657
|
+
if (this.cookies.csrf) {
|
|
3658
|
+
headers["x-csrf-token"] = this.cookies.csrf;
|
|
3659
|
+
} else {
|
|
3660
|
+
headers["x-ssr"] = "1";
|
|
3661
|
+
}
|
|
3662
|
+
return headers;
|
|
3663
|
+
}
|
|
3664
|
+
/** 解析 set-cookie 并并入 cookie jar,返回是否有变化 */
|
|
3665
|
+
absorbCookies(response) {
|
|
3666
|
+
const raw = response.headers.getSetCookie?.() ?? [];
|
|
3667
|
+
if (raw.length === 0) return false;
|
|
3668
|
+
let changed = false;
|
|
3669
|
+
for (const entry of raw) {
|
|
3670
|
+
const [pair] = entry.split(";");
|
|
3671
|
+
if (!pair) continue;
|
|
3672
|
+
const index = pair.indexOf("=");
|
|
3673
|
+
if (index <= 0) continue;
|
|
3674
|
+
const name = pair.slice(0, index).trim();
|
|
3675
|
+
const value = pair.slice(index + 1).trim();
|
|
3676
|
+
for (const [key, cookieName] of Object.entries(COOKIE_NAMES)) {
|
|
3677
|
+
if (name !== cookieName) continue;
|
|
3678
|
+
const slot = key;
|
|
3679
|
+
if (value === "") {
|
|
3680
|
+
delete this.cookies[slot];
|
|
3681
|
+
} else if (this.cookies[slot] !== value) {
|
|
3682
|
+
this.cookies[slot] = value;
|
|
3683
|
+
} else {
|
|
3684
|
+
continue;
|
|
3685
|
+
}
|
|
3686
|
+
changed = true;
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
return changed;
|
|
3690
|
+
}
|
|
3691
|
+
async persistCookies() {
|
|
3692
|
+
await this.options.onCookiesChanged?.(this.cookies);
|
|
3693
|
+
}
|
|
3694
|
+
/** 低层请求:负责超时、错误分类与 cookie 吸收,不做 tRPC 解码 */
|
|
3695
|
+
async request(path, init = {}) {
|
|
3696
|
+
const url = this.url(path);
|
|
3697
|
+
const timeoutMs = init.timeoutMs ?? this.options.timeoutMs ?? 15e3;
|
|
3698
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
3699
|
+
const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
|
|
3700
|
+
const headers = {
|
|
3701
|
+
...this.headers(),
|
|
3702
|
+
...init.headers ?? {}
|
|
3703
|
+
};
|
|
3704
|
+
this.options.logger.debug(`${init.method ?? "GET"} ${url}`);
|
|
3705
|
+
let response;
|
|
3706
|
+
try {
|
|
3707
|
+
response = await this.options.fetch(url, { ...init, headers, signal });
|
|
3708
|
+
} catch (error) {
|
|
3709
|
+
if (isAbortError(error)) {
|
|
3710
|
+
throw new NetworkError(`\u8BF7\u6C42\u8D85\u65F6\u6216\u5DF2\u53D6\u6D88\uFF1A${url}`, {
|
|
3711
|
+
cause: error,
|
|
3712
|
+
details: { url, timeoutMs },
|
|
3713
|
+
hint: "\u786E\u8BA4\u670D\u52A1\u5DF2\u542F\u52A8\uFF08docker compose up -d\uFF09\uFF0C\u6216\u8C03\u5927 QUANTA_HTTP_TIMEOUT\u3002"
|
|
3714
|
+
});
|
|
3715
|
+
}
|
|
3716
|
+
throw new NetworkError(`\u65E0\u6CD5\u8FDE\u63A5 ${url}`, {
|
|
3717
|
+
cause: error,
|
|
3718
|
+
details: { url },
|
|
3719
|
+
hint: "\u786E\u8BA4 API \u5730\u5740\u6B63\u786E\u4E14\u670D\u52A1\u5DF2\u542F\u52A8\uFF1Aqpc doctor"
|
|
3720
|
+
});
|
|
3721
|
+
}
|
|
3722
|
+
if (this.absorbCookies(response)) await this.persistCookies();
|
|
3723
|
+
return response;
|
|
3724
|
+
}
|
|
3725
|
+
/** 调用 tRPC procedure。path 形如 `admin.problem.upload` */
|
|
3726
|
+
async call(path, options = {}) {
|
|
3727
|
+
const method = options.method ?? "POST";
|
|
3728
|
+
const endpoint = `/api/trpc/${path}`;
|
|
3729
|
+
const send = async (retried = false) => {
|
|
3730
|
+
const response = method === "GET" ? await this.request(
|
|
3731
|
+
`${endpoint}?input=${encodeURIComponent(JSON.stringify(options.input ?? null))}`,
|
|
3732
|
+
{ method: "GET", signal: options.signal }
|
|
3733
|
+
) : await this.request(endpoint, {
|
|
3734
|
+
method: "POST",
|
|
3735
|
+
headers: { "content-type": "application/json" },
|
|
3736
|
+
body: JSON.stringify(options.input ?? null),
|
|
3737
|
+
signal: options.signal
|
|
3738
|
+
});
|
|
3739
|
+
const text = await response.text();
|
|
3740
|
+
const payload = parseJson(text);
|
|
3741
|
+
if (response.ok && isTrpcSuccess(payload))
|
|
3742
|
+
return payload.result.data;
|
|
3743
|
+
if (response.status === 401 && !retried && await this.refreshSession()) {
|
|
3744
|
+
return send(true);
|
|
3745
|
+
}
|
|
3746
|
+
throw this.toApiError(response, payload, endpoint);
|
|
3747
|
+
};
|
|
3748
|
+
return send();
|
|
3749
|
+
}
|
|
3750
|
+
/** 用 refresh token 换新的 access token(GET /api/refresh);token 仅 15 分钟,刷新是必需路径。 */
|
|
3751
|
+
async refreshSession() {
|
|
3752
|
+
if (this.refreshing) return this.refreshing;
|
|
3753
|
+
if (!this.cookies.refresh) return false;
|
|
3754
|
+
this.refreshing = (async () => {
|
|
3755
|
+
try {
|
|
3756
|
+
const response = await this.request("/api/refresh", {
|
|
3757
|
+
method: "GET"
|
|
3758
|
+
});
|
|
3759
|
+
if (!response.ok) {
|
|
3760
|
+
this.options.logger.warn(
|
|
3761
|
+
"\u5237\u65B0\u767B\u5F55\u72B6\u6001\u5931\u8D25\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C `qpc login`"
|
|
3762
|
+
);
|
|
3763
|
+
return false;
|
|
3764
|
+
}
|
|
3765
|
+
await response.arrayBuffer();
|
|
3766
|
+
this.options.logger.debug("access token \u5DF2\u5237\u65B0");
|
|
3767
|
+
return Boolean(this.cookies.access);
|
|
3768
|
+
} catch {
|
|
3769
|
+
return false;
|
|
3770
|
+
} finally {
|
|
3771
|
+
this.refreshing = void 0;
|
|
3772
|
+
}
|
|
3773
|
+
})();
|
|
3774
|
+
return this.refreshing;
|
|
3775
|
+
}
|
|
3776
|
+
toApiError(response, payload, path) {
|
|
3777
|
+
const failure = payload;
|
|
3778
|
+
const json = failure?.error?.json;
|
|
3779
|
+
const message = json?.message ?? `HTTP ${response.status}`;
|
|
3780
|
+
const status = json?.data?.httpStatus ?? response.status;
|
|
3781
|
+
if (status === 401 || status === 403) {
|
|
3782
|
+
return new ApiError(message, {
|
|
3783
|
+
status,
|
|
3784
|
+
path,
|
|
3785
|
+
details: { code: json?.data?.code, traceId: json?.data?.traceId },
|
|
3786
|
+
hint: status === 401 ? "\u767B\u5F55\u72B6\u6001\u5DF2\u5931\u6548\uFF1Aqpc login --force \u91CD\u65B0\u767B\u5F55\u3002" : "\u5F53\u524D\u8D26\u53F7\u6CA1\u6709\u7BA1\u7406\u5458\u6743\u9650\uFF08\u51FA\u9898\u63A5\u53E3\u8981\u6C42 role=ADMIN\uFF09\u3002"
|
|
3787
|
+
});
|
|
3788
|
+
}
|
|
3789
|
+
return new ApiError(message, {
|
|
3790
|
+
status,
|
|
3791
|
+
path,
|
|
3792
|
+
details: {
|
|
3793
|
+
code: json?.data?.code,
|
|
3794
|
+
traceId: json?.data?.traceId,
|
|
3795
|
+
zodError: json?.data?.zodError
|
|
3796
|
+
}
|
|
3797
|
+
});
|
|
3798
|
+
}
|
|
3799
|
+
};
|
|
3800
|
+
var parseJson = (text) => {
|
|
3801
|
+
if (!text) return void 0;
|
|
3802
|
+
try {
|
|
3803
|
+
return JSON.parse(text);
|
|
3804
|
+
} catch {
|
|
3805
|
+
return void 0;
|
|
3806
|
+
}
|
|
3807
|
+
};
|
|
3808
|
+
var isTrpcSuccess = (payload) => typeof payload === "object" && payload !== null && "result" in payload && typeof payload.result?.data !== "undefined";
|
|
3809
|
+
|
|
3810
|
+
// src/core/env.ts
|
|
3811
|
+
var DEFAULT_API_URL = "http://localhost:3000";
|
|
3812
|
+
var DEFAULT_HTTP_TIMEOUT_MS = 15e3;
|
|
3813
|
+
var DEFAULT_MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
|
|
3814
|
+
var parsePositiveInt = (value, fallback) => {
|
|
3815
|
+
if (!value) return fallback;
|
|
3816
|
+
const parsed = Number.parseInt(value, 10);
|
|
3817
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
3818
|
+
};
|
|
3819
|
+
var resolveEnvConfig = (env = process.env) => {
|
|
3820
|
+
const apiUrlFromEnv = env.QUANTA_API_URL?.trim() || void 0;
|
|
3821
|
+
return {
|
|
3822
|
+
apiUrl: apiUrlFromEnv || DEFAULT_API_URL,
|
|
3823
|
+
apiUrlFromEnv,
|
|
3824
|
+
token: env.QUANTA_TOKEN?.trim() || void 0,
|
|
3825
|
+
timeoutMs: parsePositiveInt(
|
|
3826
|
+
env.QUANTA_HTTP_TIMEOUT,
|
|
3827
|
+
DEFAULT_HTTP_TIMEOUT_MS
|
|
3828
|
+
),
|
|
3829
|
+
maxUploadBytes: parsePositiveInt(
|
|
3830
|
+
env.QUANTA_MAX_UPLOAD_BYTES,
|
|
3831
|
+
DEFAULT_MAX_UPLOAD_BYTES
|
|
3832
|
+
),
|
|
3833
|
+
configDir: resolveConfigDir(env)
|
|
3834
|
+
};
|
|
3835
|
+
};
|
|
3836
|
+
var OTEL_SEVERITY = {
|
|
3837
|
+
error: { number: 17, text: "ERROR" },
|
|
3838
|
+
warn: { number: 13, text: "WARN" },
|
|
3839
|
+
info: { number: 9, text: "INFO" },
|
|
3840
|
+
debug: { number: 5, text: "DEBUG" }
|
|
3841
|
+
};
|
|
3842
|
+
var LOG_LEVEL_RANK = {
|
|
3843
|
+
silent: 0,
|
|
3844
|
+
error: 1,
|
|
3845
|
+
warn: 2,
|
|
3846
|
+
info: 3,
|
|
3847
|
+
debug: 4
|
|
3848
|
+
};
|
|
3849
|
+
var write = (target, line) => target(`${line}
|
|
3850
|
+
`);
|
|
3851
|
+
var createLogger = (options = {}) => {
|
|
3852
|
+
const level = options.level ?? "info";
|
|
3853
|
+
const json = options.json ?? false;
|
|
3854
|
+
const colors = createColors(json ? false : options.color ?? true);
|
|
3855
|
+
const stdout = options.stdout ?? ((chunk) => process.stdout.write(chunk));
|
|
3856
|
+
const stderr = options.stderr ?? ((chunk) => process.stderr.write(chunk));
|
|
3857
|
+
const enabled = (candidate) => LOG_LEVEL_RANK[candidate] <= LOG_LEVEL_RANK[level] && level !== "silent";
|
|
3858
|
+
const emit = (candidate, symbol, message, meta, stream = "stderr") => {
|
|
3859
|
+
if (!enabled(candidate)) return;
|
|
3860
|
+
if (json) {
|
|
3861
|
+
const severity = OTEL_SEVERITY[candidate];
|
|
3862
|
+
const attributes = meta && Object.keys(meta).length > 0 ? meta : void 0;
|
|
3863
|
+
write(
|
|
3864
|
+
stderr,
|
|
3865
|
+
JSON.stringify({
|
|
3866
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3867
|
+
severity_number: severity.number,
|
|
3868
|
+
severity_text: severity.text,
|
|
3869
|
+
body: message,
|
|
3870
|
+
...attributes ? { attributes } : {},
|
|
3871
|
+
resource: { "service.name": "challenge-problem-creator-cli" }
|
|
3872
|
+
})
|
|
3873
|
+
);
|
|
3874
|
+
return;
|
|
3875
|
+
}
|
|
3876
|
+
const target = stream === "stdout" ? stdout : stderr;
|
|
3877
|
+
const label = symbol ? `${symbol} ` : "";
|
|
3878
|
+
write(target, `${label}${message}`);
|
|
3879
|
+
if (meta && Object.keys(meta).length > 0) {
|
|
3880
|
+
write(target, colors.dim(` ${JSON.stringify(meta)}`));
|
|
3881
|
+
}
|
|
3882
|
+
};
|
|
3883
|
+
const symbols = colors.isColorSupported ? {
|
|
3884
|
+
info: colors.blue("\u2139"),
|
|
3885
|
+
warn: colors.yellow("\u25B2"),
|
|
3886
|
+
error: colors.red("\u2716"),
|
|
3887
|
+
step: colors.cyan("\u25B8"),
|
|
3888
|
+
success: colors.green("\u2714"),
|
|
3889
|
+
fail: colors.red("\u2716")
|
|
3890
|
+
} : {
|
|
3891
|
+
info: "[i]",
|
|
3892
|
+
warn: "[!]",
|
|
3893
|
+
error: "[x]",
|
|
3894
|
+
step: ">",
|
|
3895
|
+
success: "[ok]",
|
|
3896
|
+
fail: "[x]"
|
|
3897
|
+
};
|
|
3898
|
+
return {
|
|
3899
|
+
level,
|
|
3900
|
+
json,
|
|
3901
|
+
colors,
|
|
3902
|
+
error: (message, meta) => emit("error", symbols.error, message, meta),
|
|
3903
|
+
warn: (message, meta) => emit("warn", symbols.warn, colors.yellow(message), meta),
|
|
3904
|
+
info: (message, meta) => emit("info", symbols.info, message, meta, "stdout"),
|
|
3905
|
+
debug: (message, meta) => emit("debug", colors.dim("\xB7"), colors.dim(message), meta),
|
|
3906
|
+
step: (message) => emit("info", symbols.step, colors.bold(message), void 0, "stdout"),
|
|
3907
|
+
success: (message) => emit(
|
|
3908
|
+
"info",
|
|
3909
|
+
symbols.success,
|
|
3910
|
+
colors.green(message),
|
|
3911
|
+
void 0,
|
|
3912
|
+
"stdout"
|
|
3913
|
+
),
|
|
3914
|
+
fail: (message) => emit("error", symbols.fail, colors.red(message)),
|
|
3915
|
+
detail: (message) => emit("info", "", colors.dim(` ${message}`), void 0, "stdout"),
|
|
3916
|
+
blank: () => {
|
|
3917
|
+
if (json || level === "silent") return;
|
|
3918
|
+
write(stderr, "");
|
|
3919
|
+
},
|
|
3920
|
+
result: (payload) => {
|
|
3921
|
+
write(stdout, json ? JSON.stringify(payload) : String(payload));
|
|
3922
|
+
},
|
|
3923
|
+
table: (rows) => {
|
|
3924
|
+
if (json || rows.length === 0) return;
|
|
3925
|
+
const width = Math.max(...rows.map(([key]) => displayWidth(key)));
|
|
3926
|
+
for (const [key, value] of rows) {
|
|
3927
|
+
const pad = " ".repeat(Math.max(0, width - displayWidth(key)));
|
|
3928
|
+
write(stdout, ` ${key}${pad} ${value}`);
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
};
|
|
3932
|
+
};
|
|
3933
|
+
var displayWidth = (text) => {
|
|
3934
|
+
let width = 0;
|
|
3935
|
+
for (const char of text) {
|
|
3936
|
+
const code = char.codePointAt(0) ?? 0;
|
|
3937
|
+
width += code >= 4352 && isWide(code) ? 2 : 1;
|
|
3938
|
+
}
|
|
3939
|
+
return width;
|
|
3940
|
+
};
|
|
3941
|
+
var isWide = (code) => code >= 4352 && code <= 4447 || code >= 11904 && code <= 42191 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65072 && code <= 65135 || code >= 65280 && code <= 65376 || code >= 65504 && code <= 65510;
|
|
3942
|
+
|
|
3943
|
+
// src/core/context.ts
|
|
3944
|
+
var resolveLogLevel = (options) => {
|
|
3945
|
+
if (options.verbose) return "debug";
|
|
3946
|
+
if (options.quiet) return "error";
|
|
3947
|
+
return "info";
|
|
3948
|
+
};
|
|
3949
|
+
var resolveColor = (options, runtime2) => {
|
|
3950
|
+
if (options.color !== void 0) return options.color;
|
|
3951
|
+
if (runtime2.env.NO_COLOR) return false;
|
|
3952
|
+
return runtime2.isTTY ?? false;
|
|
3953
|
+
};
|
|
3954
|
+
var createCommandContext = async (options, runtime2) => {
|
|
3955
|
+
const env = resolveEnvConfig(runtime2.env);
|
|
3956
|
+
const logger = createLogger({
|
|
3957
|
+
level: resolveLogLevel(options),
|
|
3958
|
+
json: options.json ?? false,
|
|
3959
|
+
color: resolveColor(options, runtime2),
|
|
3960
|
+
stdout: runtime2.stdout,
|
|
3961
|
+
stderr: runtime2.stderr
|
|
3962
|
+
});
|
|
3963
|
+
const baseDir = options.cwd ?? runtime2.env.INIT_CWD ?? runtime2.cwd;
|
|
3964
|
+
const cwd = resolve(runtime2.cwd, baseDir);
|
|
3965
|
+
const credentialsFile = credentialsPath(env.configDir);
|
|
3966
|
+
const stored = await readCredentials(credentialsFile);
|
|
3967
|
+
const apiUrl = (options.api ?? env.apiUrlFromEnv ?? stored.apiUrl ?? env.apiUrl).replace(/\/+$/, "");
|
|
3968
|
+
const credentials = {
|
|
3969
|
+
...stored,
|
|
3970
|
+
apiUrl,
|
|
3971
|
+
cookies: { ...stored.cookies }
|
|
3972
|
+
};
|
|
3973
|
+
const tokenFromEnv = Boolean(options.token ?? env.token);
|
|
3974
|
+
if (tokenFromEnv) credentials.cookies.access = options.token ?? env.token;
|
|
3975
|
+
const persistCredentials = async (next) => {
|
|
3976
|
+
if (tokenFromEnv && !next) return;
|
|
3977
|
+
await writeCredentials(credentialsFile, next ?? credentials);
|
|
3978
|
+
};
|
|
3979
|
+
const client = new HttpClient({
|
|
3980
|
+
baseUrl: apiUrl,
|
|
3981
|
+
fetch: runtime2.fetch,
|
|
3982
|
+
cookies: credentials.cookies,
|
|
3983
|
+
logger,
|
|
3984
|
+
timeoutMs: env.timeoutMs,
|
|
3985
|
+
signal: runtime2.signal,
|
|
3986
|
+
onCookiesChanged: (cookies) => {
|
|
3987
|
+
if (tokenFromEnv) return;
|
|
3988
|
+
return persistCredentials({ ...credentials, cookies });
|
|
3989
|
+
}
|
|
3990
|
+
});
|
|
3991
|
+
return {
|
|
3992
|
+
runtime: runtime2,
|
|
3993
|
+
logger,
|
|
3994
|
+
cwd,
|
|
3995
|
+
env: { ...env, apiUrl },
|
|
3996
|
+
credentialsFile,
|
|
3997
|
+
credentials,
|
|
3998
|
+
client,
|
|
3999
|
+
assumeYes: options.yes ?? false,
|
|
4000
|
+
persistCredentials
|
|
4001
|
+
};
|
|
4002
|
+
};
|
|
4003
|
+
|
|
4004
|
+
// src/core/runtime.ts
|
|
4005
|
+
var createProcessRuntime = (overrides = {}) => ({
|
|
4006
|
+
cwd: process.cwd(),
|
|
4007
|
+
env: process.env,
|
|
4008
|
+
stdin: process.stdin,
|
|
4009
|
+
stdout: (chunk) => process.stdout.write(chunk),
|
|
4010
|
+
stderr: (chunk) => process.stderr.write(chunk),
|
|
4011
|
+
stderrStream: process.stderr,
|
|
4012
|
+
fetch: ((input, init) => globalThis.fetch(input, init)),
|
|
4013
|
+
now: () => Date.now(),
|
|
4014
|
+
isTTY: Boolean(process.stdout.isTTY),
|
|
4015
|
+
...overrides
|
|
4016
|
+
});
|
|
4017
|
+
var installSignalHandlers = (runtime2) => {
|
|
4018
|
+
const controller2 = new AbortController();
|
|
4019
|
+
let interrupts = 0;
|
|
4020
|
+
const onSignal = () => {
|
|
4021
|
+
interrupts += 1;
|
|
4022
|
+
if (interrupts > 1) {
|
|
4023
|
+
runtime2.stderr("\n\u518D\u6B21\u6536\u5230\u4E2D\u65AD\u4FE1\u53F7\uFF0C\u7ACB\u5373\u9000\u51FA\u3002\n");
|
|
4024
|
+
process.exit(130);
|
|
4025
|
+
}
|
|
4026
|
+
runtime2.stderr("\n\u6B63\u5728\u53D6\u6D88\u2026\uFF08\u518D\u6309\u4E00\u6B21 Ctrl-C \u5F3A\u5236\u9000\u51FA\uFF09\n");
|
|
4027
|
+
controller2.abort(new CanceledError("\u5DF2\u53D6\u6D88\uFF08Ctrl-C\uFF09"));
|
|
4028
|
+
};
|
|
4029
|
+
process.on("SIGINT", onSignal);
|
|
4030
|
+
process.on("SIGTERM", onSignal);
|
|
4031
|
+
return {
|
|
4032
|
+
controller: controller2,
|
|
4033
|
+
dispose: () => {
|
|
4034
|
+
process.removeListener("SIGINT", onSignal);
|
|
4035
|
+
process.removeListener("SIGTERM", onSignal);
|
|
4036
|
+
}
|
|
4037
|
+
};
|
|
4038
|
+
};
|
|
4039
|
+
|
|
4040
|
+
// src/cli.ts
|
|
4041
|
+
var createProgram = (runtime2) => {
|
|
4042
|
+
const program = new Command();
|
|
4043
|
+
program.name(CLI_NAME).description(CLI_DESCRIPTION).version(CLI_VERSION, "-v, --version", "\u67E5\u770B\u7248\u672C\u53F7").option(
|
|
4044
|
+
"--api <url>",
|
|
4045
|
+
"Web \u5E94\u7528\u5730\u5740\uFF08\u9ED8\u8BA4 QUANTA_API_URL \u6216 http://localhost:3000\uFF09"
|
|
4046
|
+
).option(
|
|
4047
|
+
"--token <token>",
|
|
4048
|
+
"\u76F4\u63A5\u4F7F\u7528\u8BBF\u95EE\u4EE4\u724C\uFF08\u9ED8\u8BA4 QUANTA_TOKEN\uFF1B\u4E0D\u4F1A\u5199\u5165\u51ED\u636E\u6587\u4EF6\uFF09"
|
|
4049
|
+
).option(
|
|
4050
|
+
"--cwd <dir>",
|
|
4051
|
+
"\u5DE5\u4F5C\u76EE\u5F55\u57FA\u51C6\uFF08\u89E3\u6790\u987A\u5E8F\uFF1A--cwd > INIT_CWD > \u8FDB\u7A0B cwd\uFF09"
|
|
4052
|
+
).option("--json", "\u8F93\u51FA\u673A\u5668\u53EF\u8BFB JSON\uFF08stdout \u53EA\u6709\u7ED3\u679C\uFF0C\u65E5\u5FD7\u8D70 stderr\uFF09").option("--quiet", "\u9690\u85CF\u8FDB\u5EA6\u4FE1\u606F\uFF0C\u53EA\u4FDD\u7559\u9519\u8BEF\u4E0E\u547D\u4EE4\u7ED3\u679C").option("--verbose", "\u8F93\u51FA\u8C03\u8BD5\u4FE1\u606F").option("--no-color", "\u7981\u7528\u5F69\u8272\u8F93\u51FA").option("-y, --yes", "\u8DF3\u8FC7\u4EA4\u4E92\u786E\u8BA4\uFF08CI \u5FC5\u7ED9\uFF09").showHelpAfterError("\uFF08\u7528 `qpc --help` \u67E5\u770B\u5168\u90E8\u7528\u6CD5\uFF09").configureOutput({
|
|
4053
|
+
writeOut: (chunk) => runtime2.stdout(chunk),
|
|
4054
|
+
writeErr: (chunk) => runtime2.stderr(chunk)
|
|
4055
|
+
}).exitOverride();
|
|
4056
|
+
const globals = () => {
|
|
4057
|
+
const options = program.opts();
|
|
4058
|
+
const colorExplicit = program.getOptionValueSource("color") === "cli";
|
|
4059
|
+
return {
|
|
4060
|
+
api: asString(options.api),
|
|
4061
|
+
token: asString(options.token),
|
|
4062
|
+
cwd: asString(options.cwd),
|
|
4063
|
+
json: Boolean(options.json),
|
|
4064
|
+
quiet: Boolean(options.quiet),
|
|
4065
|
+
verbose: Boolean(options.verbose),
|
|
4066
|
+
yes: Boolean(options.yes),
|
|
4067
|
+
color: colorExplicit ? Boolean(options.color) : void 0
|
|
4068
|
+
};
|
|
4069
|
+
};
|
|
4070
|
+
const registry = {
|
|
4071
|
+
program,
|
|
4072
|
+
context: () => createCommandContext(globals(), runtime2)
|
|
4073
|
+
};
|
|
4074
|
+
registerCommands(registry);
|
|
4075
|
+
return program;
|
|
4076
|
+
};
|
|
4077
|
+
var runCli = async (argv, runtime2 = createProcessRuntime()) => {
|
|
4078
|
+
const program = createProgram(runtime2);
|
|
4079
|
+
try {
|
|
4080
|
+
await program.parseAsync(argv, { from: "node" });
|
|
4081
|
+
return EXIT.OK;
|
|
4082
|
+
} catch (error) {
|
|
4083
|
+
if (error instanceof CommanderError) {
|
|
4084
|
+
return error.exitCode === 0 ? EXIT.OK : EXIT.USAGE;
|
|
4085
|
+
}
|
|
4086
|
+
const cliError = toCliError(error);
|
|
4087
|
+
const json = argv.includes("--json");
|
|
4088
|
+
const logger = createLogger({
|
|
4089
|
+
level: "debug",
|
|
4090
|
+
json,
|
|
4091
|
+
color: runtime2.isTTY ?? false,
|
|
4092
|
+
stdout: runtime2.stdout,
|
|
4093
|
+
stderr: runtime2.stderr
|
|
4094
|
+
});
|
|
4095
|
+
if (json) {
|
|
4096
|
+
logger.result({
|
|
4097
|
+
ok: false,
|
|
4098
|
+
error: {
|
|
4099
|
+
name: cliError.name,
|
|
4100
|
+
message: cliError.message,
|
|
4101
|
+
hint: cliError.hint,
|
|
4102
|
+
details: cliError.details,
|
|
4103
|
+
exitCode: cliError.exitCode
|
|
4104
|
+
}
|
|
4105
|
+
});
|
|
4106
|
+
} else {
|
|
4107
|
+
logger.error(cliError.message);
|
|
4108
|
+
if (cliError.hint) logger.info(` ${cliError.hint}`);
|
|
4109
|
+
if (cliError.details && argv.includes("--verbose")) {
|
|
4110
|
+
logger.detail(JSON.stringify(cliError.details, null, 3));
|
|
4111
|
+
}
|
|
4112
|
+
if (!(error instanceof Error) || !(error instanceof CommanderError)) {
|
|
4113
|
+
const cause = cliError.cause;
|
|
4114
|
+
if (argv.includes("--verbose") && cause instanceof Error && cause.stack) {
|
|
4115
|
+
logger.detail(cause.stack);
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
return cliError.exitCode;
|
|
4120
|
+
}
|
|
4121
|
+
};
|
|
4122
|
+
var asString = (value) => typeof value === "string" && value.length > 0 ? value : void 0;
|
|
4123
|
+
|
|
4124
|
+
// src/index.ts
|
|
4125
|
+
var runtime = createProcessRuntime();
|
|
4126
|
+
var { controller, dispose } = installSignalHandlers(runtime);
|
|
4127
|
+
var exitCode = await runCli(process.argv, {
|
|
4128
|
+
...runtime,
|
|
4129
|
+
signal: controller.signal
|
|
4130
|
+
});
|
|
4131
|
+
dispose();
|
|
4132
|
+
process.exitCode = exitCode;
|
|
4133
|
+
//# sourceMappingURL=index.js.map
|
|
4134
|
+
//# sourceMappingURL=index.js.map
|