hydro-ai-helper 3.0.2 → 3.0.4
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 +2 -1
- package/README_en.md +2 -1
- package/dist/handlers/testdataGenHandler.js +12 -4
- package/dist/handlers/testdataGenHandler.js.map +1 -1
- package/dist/services/errorReporter.js +12 -2
- package/dist/services/errorReporter.js.map +1 -1
- package/dist/services/goJudgeSandboxService.js +8 -2
- package/dist/services/goJudgeSandboxService.js.map +1 -1
- package/dist/services/testdataGenService.js +431 -60
- package/dist/services/testdataGenService.js.map +1 -1
- package/frontend/generated/localeFallback.ts +8 -2
- package/frontend/testdataGen/TestdataGenPanel.tsx +32 -2
- package/locales/en.yaml +4 -1
- package/locales/zh.yaml +4 -1
- package/package.json +1 -1
|
@@ -18,11 +18,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
18
18
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
19
19
|
};
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
-
exports.TestdataGenService = exports.TEMPLATE_FILENAMES = exports.TESTDATA_GEN_LIMITS = exports.SUPPORTED_TEMPLATE_LANGS = void 0;
|
|
21
|
+
exports.TestdataGenService = exports.TestdataGenerationError = exports.TEMPLATE_FILENAMES = exports.TESTDATA_GEN_LIMITS = exports.SUPPORTED_TEMPLATE_LANGS = void 0;
|
|
22
22
|
exports.isSafeTestdataFilename = isSafeTestdataFilename;
|
|
23
23
|
exports.validateGenerateOptions = validateGenerateOptions;
|
|
24
|
+
exports.buildCoveragePlan = buildCoveragePlan;
|
|
25
|
+
exports.getExistingNumericCases = getExistingNumericCases;
|
|
26
|
+
exports.allocateCaseNumbers = allocateCaseNumbers;
|
|
24
27
|
exports.detectStdFilename = detectStdFilename;
|
|
25
28
|
exports.buildCompileSh = buildCompileSh;
|
|
29
|
+
exports.hasCustomChecker = hasCustomChecker;
|
|
26
30
|
exports.buildConfigYaml = buildConfigYaml;
|
|
27
31
|
exports.buildTestdataSystemPrompt = buildTestdataSystemPrompt;
|
|
28
32
|
exports.buildTestdataUserPrompt = buildTestdataUserPrompt;
|
|
@@ -30,6 +34,7 @@ exports.buildSandboxBlueprintSystemPrompt = buildSandboxBlueprintSystemPrompt;
|
|
|
30
34
|
exports.buildSandboxBlueprintUserPrompt = buildSandboxBlueprintUserPrompt;
|
|
31
35
|
exports.extractJsonObject = extractJsonObject;
|
|
32
36
|
exports.normalizeFileContent = normalizeFileContent;
|
|
37
|
+
exports.normalizeExecutableContent = normalizeExecutableContent;
|
|
33
38
|
exports.normalizeGenerationObject = normalizeGenerationObject;
|
|
34
39
|
exports.parseGenerationResponse = parseGenerationResponse;
|
|
35
40
|
exports.parseSandboxBlueprint = parseSandboxBlueprint;
|
|
@@ -45,6 +50,10 @@ exports.parseTemplateSections = parseTemplateSections;
|
|
|
45
50
|
exports.assemblePlan = assemblePlan;
|
|
46
51
|
exports.isLikelyFunctionProblem = isLikelyFunctionProblem;
|
|
47
52
|
exports.buildSkeletonPlan = buildSkeletonPlan;
|
|
53
|
+
exports.extractTestdataErrorMetadata = extractTestdataErrorMetadata;
|
|
54
|
+
exports.classifySandboxRepairScope = classifySandboxRepairScope;
|
|
55
|
+
exports.buildSandboxRepairPrompt = buildSandboxRepairPrompt;
|
|
56
|
+
exports.mergeSandboxBlueprintRepair = mergeSandboxBlueprintRepair;
|
|
48
57
|
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
49
58
|
const goJudgeSandboxService_1 = require("./goJudgeSandboxService");
|
|
50
59
|
const textTruncate_1 = require("../lib/textTruncate");
|
|
@@ -100,7 +109,7 @@ function validateGenerateOptions(options) {
|
|
|
100
109
|
if (options.fillInMode !== undefined && !['auto', 'yes', 'no'].includes(options.fillInMode)) {
|
|
101
110
|
return 'ai_helper_testdata_err_invalid_fill_in';
|
|
102
111
|
}
|
|
103
|
-
if (options.dataScale !== undefined && !['small', 'medium', 'large'].includes(options.dataScale)) {
|
|
112
|
+
if (options.dataScale !== undefined && !['auto', 'small', 'medium', 'large'].includes(options.dataScale)) {
|
|
104
113
|
return 'ai_helper_testdata_err_invalid_scale';
|
|
105
114
|
}
|
|
106
115
|
if ((options.providedStd || '').length > exports.TESTDATA_GEN_LIMITS.MAX_PROVIDED_STD) {
|
|
@@ -111,6 +120,93 @@ function validateGenerateOptions(options) {
|
|
|
111
120
|
}
|
|
112
121
|
return null;
|
|
113
122
|
}
|
|
123
|
+
const COVERAGE_GUIDANCE = {
|
|
124
|
+
small: '合法最小值、题面样例或可人工验算的简单结构;不得为了取 0/空输入而违反题面下界',
|
|
125
|
+
medium: '约束范围的中间量级,并交叉变化不同约束,避免所有维度同时按同一比例缩放',
|
|
126
|
+
large: '至少一个关键约束接近上下界或临界值;使用可解析结构并控制输出体积与沙箱耗时',
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* 为一次生成建立确定性的规模计划。auto 在 caseCount>=3 时保证三个档位均出现,
|
|
130
|
+
* 其余名额按 30%/40%/30% 的目标比例用最大缺口法分配。
|
|
131
|
+
*/
|
|
132
|
+
function buildCoveragePlan(caseCount, strategy = 'auto') {
|
|
133
|
+
if (!Number.isInteger(caseCount) || caseCount <= 0)
|
|
134
|
+
return [];
|
|
135
|
+
let scales;
|
|
136
|
+
if (strategy !== 'auto') {
|
|
137
|
+
scales = Array.from({ length: caseCount }, () => strategy);
|
|
138
|
+
}
|
|
139
|
+
else if (caseCount === 1) {
|
|
140
|
+
scales = ['small'];
|
|
141
|
+
}
|
|
142
|
+
else if (caseCount === 2) {
|
|
143
|
+
scales = ['small', 'large'];
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
const desired = {
|
|
147
|
+
small: caseCount * 0.3,
|
|
148
|
+
medium: caseCount * 0.4,
|
|
149
|
+
large: caseCount * 0.3,
|
|
150
|
+
};
|
|
151
|
+
const counts = {
|
|
152
|
+
small: Math.max(1, Math.floor(desired.small)),
|
|
153
|
+
medium: Math.max(1, Math.floor(desired.medium)),
|
|
154
|
+
large: Math.max(1, Math.floor(desired.large)),
|
|
155
|
+
};
|
|
156
|
+
const priority = ['medium', 'small', 'large'];
|
|
157
|
+
while (counts.small + counts.medium + counts.large < caseCount) {
|
|
158
|
+
const next = priority.reduce((best, scale) => (desired[scale] - counts[scale] > desired[best] - counts[best] ? scale : best), priority[0]);
|
|
159
|
+
counts[next]++;
|
|
160
|
+
}
|
|
161
|
+
while (counts.small + counts.medium + counts.large > caseCount) {
|
|
162
|
+
const next = [...priority].reverse().reduce((best, scale) => (counts[scale] > 1 && counts[scale] - desired[scale] > counts[best] - desired[best] ? scale : best), counts.large > 1 ? 'large' : counts.medium > 1 ? 'medium' : 'small');
|
|
163
|
+
counts[next]--;
|
|
164
|
+
}
|
|
165
|
+
scales = [
|
|
166
|
+
...Array.from({ length: counts.small }, () => 'small'),
|
|
167
|
+
...Array.from({ length: counts.medium }, () => 'medium'),
|
|
168
|
+
...Array.from({ length: counts.large }, () => 'large'),
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
return scales.map((dataScale, index) => ({
|
|
172
|
+
caseNumber: index + 1,
|
|
173
|
+
dataScale,
|
|
174
|
+
guidance: COVERAGE_GUIDANCE[dataScale],
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
/** 提取数字测试点状态:任一侧存在即保留编号,只有 in/out 成对才进入 config。 */
|
|
178
|
+
function getExistingNumericCases(existingFiles = []) {
|
|
179
|
+
const sides = new Map();
|
|
180
|
+
for (const name of existingFiles) {
|
|
181
|
+
const match = name.match(/^(\d+)\.(in|out)$/i);
|
|
182
|
+
if (!match)
|
|
183
|
+
continue;
|
|
184
|
+
const number = Number(match[1]);
|
|
185
|
+
if (!Number.isSafeInteger(number) || number <= 0)
|
|
186
|
+
continue;
|
|
187
|
+
if (!sides.has(number))
|
|
188
|
+
sides.set(number, new Set());
|
|
189
|
+
sides.get(number)?.add(match[2].toLowerCase());
|
|
190
|
+
}
|
|
191
|
+
const reserved = new Set(sides.keys());
|
|
192
|
+
const complete = [...sides.entries()]
|
|
193
|
+
.filter(([, value]) => value.has('in') && value.has('out'))
|
|
194
|
+
.map(([number]) => number)
|
|
195
|
+
.sort((a, b) => a - b);
|
|
196
|
+
return { reserved, complete };
|
|
197
|
+
}
|
|
198
|
+
/** 分配不与任何现有 .in/.out 冲突的最小正整数编号。 */
|
|
199
|
+
function allocateCaseNumbers(existingFiles = [], count) {
|
|
200
|
+
const { reserved } = getExistingNumericCases(existingFiles);
|
|
201
|
+
const allocated = [];
|
|
202
|
+
for (let candidate = 1; allocated.length < count; candidate++) {
|
|
203
|
+
if (reserved.has(candidate))
|
|
204
|
+
continue;
|
|
205
|
+
allocated.push(candidate);
|
|
206
|
+
reserved.add(candidate);
|
|
207
|
+
}
|
|
208
|
+
return allocated;
|
|
209
|
+
}
|
|
114
210
|
/** 根据标准答案代码猜测 std 文件扩展名(教师多用 Python,启发式足够) */
|
|
115
211
|
function detectStdFilename(code) {
|
|
116
212
|
if (/#include\s*[<"]/.test(code))
|
|
@@ -178,6 +274,27 @@ else
|
|
|
178
274
|
fi
|
|
179
275
|
`;
|
|
180
276
|
}
|
|
277
|
+
function parseExistingProblemConfig(raw) {
|
|
278
|
+
if (!raw?.trim())
|
|
279
|
+
return {};
|
|
280
|
+
try {
|
|
281
|
+
const parsed = js_yaml_1.default.load(raw);
|
|
282
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
283
|
+
? parsed
|
|
284
|
+
: {};
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return {};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/** 判断现有题目是否使用非 default/strict 的自定义 checker。 */
|
|
291
|
+
function hasCustomChecker(raw) {
|
|
292
|
+
const config = parseExistingProblemConfig(raw);
|
|
293
|
+
const checkerType = typeof config.checker_type === 'string' ? config.checker_type.trim().toLowerCase() : '';
|
|
294
|
+
const checker = typeof config.checker === 'string' ? config.checker.trim() : '';
|
|
295
|
+
return (!!checkerType && !['default', 'strict'].includes(checkerType))
|
|
296
|
+
|| (!!checker && !['default', 'strict'].includes(checkerType));
|
|
297
|
+
}
|
|
181
298
|
/**
|
|
182
299
|
* 生成 config.yaml(评测设置)
|
|
183
300
|
*
|
|
@@ -186,17 +303,36 @@ fi
|
|
|
186
303
|
*/
|
|
187
304
|
function buildConfigYaml(options) {
|
|
188
305
|
const { problemType, caseCount, languages } = options;
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
306
|
+
const previous = parseExistingProblemConfig(options.existingConfig);
|
|
307
|
+
const caseNumbers = options.caseNumbers?.length
|
|
308
|
+
? [...new Set(options.caseNumbers)].sort((a, b) => a - b)
|
|
309
|
+
: Array.from({ length: caseCount }, (_, i) => i + 1);
|
|
310
|
+
const cases = caseNumbers.map(number => ({
|
|
311
|
+
input: `${number}.in`,
|
|
312
|
+
output: `${number}.out`,
|
|
192
313
|
}));
|
|
193
|
-
const config = {
|
|
194
|
-
|
|
195
|
-
|
|
314
|
+
const config = {};
|
|
315
|
+
const preservedKeys = [
|
|
316
|
+
'type', 'subType', 'target', 'score', 'time', 'memory', 'filename',
|
|
317
|
+
'checker_type', 'checker', 'interactor', 'manager', 'num_processes',
|
|
318
|
+
'judge_extra_files', 'detail', 'validator', 'time_limit_rate', 'memory_limit_rate',
|
|
319
|
+
];
|
|
320
|
+
for (const key of preservedKeys) {
|
|
321
|
+
if (previous[key] !== undefined)
|
|
322
|
+
config[key] = previous[key];
|
|
323
|
+
}
|
|
324
|
+
if (!config.type)
|
|
325
|
+
config.type = 'default';
|
|
326
|
+
const previousUserExtraFiles = Array.isArray(previous.user_extra_files)
|
|
327
|
+
? previous.user_extra_files.filter((item) => typeof item === 'string')
|
|
328
|
+
: [];
|
|
196
329
|
if (problemType === 'function') {
|
|
197
330
|
const userExtraFiles = languages.map(l => exports.TEMPLATE_FILENAMES[l]);
|
|
198
331
|
userExtraFiles.push('compile.sh');
|
|
199
|
-
config.user_extra_files = userExtraFiles;
|
|
332
|
+
config.user_extra_files = [...new Set([...previousUserExtraFiles, ...userExtraFiles])];
|
|
333
|
+
}
|
|
334
|
+
else if (previousUserExtraFiles.length > 0) {
|
|
335
|
+
config.user_extra_files = previousUserExtraFiles;
|
|
200
336
|
}
|
|
201
337
|
config.subtasks = [{
|
|
202
338
|
score: 100,
|
|
@@ -208,6 +344,9 @@ function buildConfigYaml(options) {
|
|
|
208
344
|
if (problemType === 'function') {
|
|
209
345
|
config.langs = languages.flatMap(l => LANG_FAMILY_CODES[l]);
|
|
210
346
|
}
|
|
347
|
+
else if (Array.isArray(previous.langs)) {
|
|
348
|
+
config.langs = previous.langs;
|
|
349
|
+
}
|
|
211
350
|
return js_yaml_1.default.dump(config, { lineWidth: 120, noRefs: true });
|
|
212
351
|
}
|
|
213
352
|
// ─── 提示词构建 ───────────────────────────────────────────────────────────────
|
|
@@ -343,16 +482,19 @@ function buildTestdataSystemPrompt() {
|
|
|
343
482
|
1. 若题面含示例,优先覆盖示例表达的场景;仍须遵守上面的“一文件一组”默认规则。
|
|
344
483
|
2. 必须包含边界组,并在 label 中写明设计意图:
|
|
345
484
|
- 最小规模:空输入、0、1、单元素(以题面约束允许的最小值为准);
|
|
346
|
-
-
|
|
485
|
+
- 规模上限:对应 CASE 覆盖计划允许的上限附近;
|
|
347
486
|
- 特殊值:相等、重复、负数、临界值(视题意选取,如闰年 2 月 29 日、恰好越界前后);
|
|
348
487
|
- 特殊结构:全相同、已排序、逆序、对称/回文等(视题意选取)。
|
|
349
488
|
3. 其余测试点使用多样化的中间规模数据,避免彼此雷同。
|
|
350
489
|
4. 输入输出必须与题面(或标程)的格式要求严格一致;.in 是评测输入文件内容,.out 是标准输出文件内容。
|
|
351
|
-
5.
|
|
490
|
+
5. 数据规模策略(默认 auto 自动混合):
|
|
491
|
+
- auto:严格遵守用户消息中的逐 CASE 覆盖计划,在同一次生成中同时包含小规模、中等规模和临界规模;
|
|
352
492
|
- small:所有数据保持人工可快速验算的量级(数值一般 ≤ 100,单个 .in ≤ 30 行);
|
|
353
493
|
- medium:在题面约束内取中等量级(如 10^2~10^4,单个 .in ≤ 200 行),仍须保证输出可被可靠推演;
|
|
354
494
|
- large:接近题面约束上限。此档必须使用【可解析构造】:用有规律的数据(全相同、等差、周期、对称等),使正确输出能由公式/推理直接得出,而不是逐条模拟;无法可靠推出输出时,宁可缩小该测试点规模,也绝不允许猜测输出。
|
|
355
|
-
6.
|
|
495
|
+
6. 若题面存在多个独立约束,不得把所有维度一起机械放大。应交叉覆盖,例如“小规模结构 + 临界元素值”“大规模结构 + 简单/稀疏取值”“某一参数取上下界而其余参数取中间值”。
|
|
496
|
+
7. 若题面未给出明确范围,使用保守、可被 VALIDATOR 验证的构造,不得臆造违反题意的 0、空输入或极端值。
|
|
497
|
+
8. 正确性最重要:先确定标程(教师已提供则以其为准),再对每个测试点逐步推演标程的运行得到 .out。宁可数据小,绝不允许输出错误。
|
|
356
498
|
|
|
357
499
|
【输出格式(分节文本,禁止 JSON)】
|
|
358
500
|
代码与数据必须以原文直出,因此使用分节标记格式,不要输出 JSON、不要做任何转义、不要用代码围栏包裹任何内容。标记行独占一行、顶格书写,形如 @@@标记@@@。整体结构如下(不适用的节直接省略):
|
|
@@ -393,6 +535,7 @@ template.cc 原文
|
|
|
393
535
|
- 所有说明性文字(ANALYSIS/NOTES/label)使用简体中文。`;
|
|
394
536
|
}
|
|
395
537
|
const DATA_SCALE_TEXT = {
|
|
538
|
+
auto: 'auto(自动混合:按题面约束一次覆盖小/中/临界规模)',
|
|
396
539
|
small: 'small(小规模,人工可快速验算)',
|
|
397
540
|
medium: 'medium(中等规模,题面约束内取中位量级)',
|
|
398
541
|
large: 'large(接近题面约束上限,必须使用可解析构造保证输出正确)',
|
|
@@ -416,6 +559,7 @@ function buildTestdataUserPrompt(params) {
|
|
|
416
559
|
}[options.fillInMode || 'auto'];
|
|
417
560
|
const langText = options.languages.map(l => LANG_DISPLAY[l]).join('、') || '(无)';
|
|
418
561
|
const requiredTemplateSections = options.languages.map(l => `@@@TEMPLATE:${l}@@@`).join('、');
|
|
562
|
+
const coveragePlan = buildCoveragePlan(options.caseCount, options.dataScale || 'auto');
|
|
419
563
|
const statement = statementMarkdown.length > exports.TESTDATA_GEN_LIMITS.MAX_STATEMENT_LENGTH
|
|
420
564
|
? `${statementMarkdown.slice(0, exports.TESTDATA_GEN_LIMITS.MAX_STATEMENT_LENGTH)}\n...(题面过长已截断)`
|
|
421
565
|
: statementMarkdown;
|
|
@@ -429,9 +573,10 @@ function buildTestdataUserPrompt(params) {
|
|
|
429
573
|
`- 题型:${kindText}`,
|
|
430
574
|
`- 填空题(完善代码):${fillInText}`,
|
|
431
575
|
`- Hydro 测试点数量:${options.caseCount} 个独立的 .in/.out 文件对(这不是单个输入文件首行的 T)`,
|
|
432
|
-
`-
|
|
576
|
+
`- 数据规模策略:${DATA_SCALE_TEXT[options.dataScale || 'auto']}`,
|
|
433
577
|
`- 函数题模板语言:${langText}`,
|
|
434
578
|
];
|
|
579
|
+
lines.push('', '【逐测试点覆盖计划(必须按 CASE 编号执行,并把实际覆盖目标写进 label)】', ...coveragePlan.map(slot => `- CASE ${slot.caseNumber}: ${slot.dataScale} — ${slot.guidance}`));
|
|
435
580
|
if (options.problemKind !== 'traditional') {
|
|
436
581
|
lines.push(`- 若判定/指定为函数题,必须完整输出这些模板节:${requiredTemplateSections}(不得遗漏)`);
|
|
437
582
|
}
|
|
@@ -463,9 +608,10 @@ function buildSandboxBlueprintSystemPrompt() {
|
|
|
463
608
|
6. BRUTE 是与 ORACLE 相互独立的第二实现(对拍用):用最朴素的暴力/枚举/模拟写法,宁慢勿错,只需在生成数据规模内跑完即可;它同样是自包含、读同一 stdin 编码、按题面输出的完整 Python 3 程序。严禁 BRUTE 与 ORACLE 共享核心函数或互相调用——它们的一致性是数据正确性的机器证据。
|
|
464
609
|
7. 函数题必须输出 SOLUTION 节:与学生提交形式完全一致的函数/类定义(只含实现,不含读输入或打印),它将与 template.py 拼接后在沙箱实跑,用于验证模板与输入编码。传统题省略 SOLUTION。
|
|
465
610
|
8. 鼓励输出 VALIDATOR 节:Python 3 程序,从 stdin 读一份 .in,校验格式与题面约束(数量范围、数值边界、结构合法性);合法则静默 exit 0,非法则向 stderr 打印原因并 exit 1(可用 sys.exit(1))。
|
|
466
|
-
9.
|
|
467
|
-
10.
|
|
468
|
-
11.
|
|
611
|
+
9. 数据必须严格遵守用户消息中的逐 CASE 覆盖计划,并根据题面真实约束交叉变化不同维度;所有生成过程必须确定性,固定随机种子。
|
|
612
|
+
10. GENERATOR 必须使用紧凑 JSON(Python json.dumps(..., ensure_ascii=False, separators=(',', ':'))),stdout 总量必须小于 1MB。每个 input 的 UTF-8 内容必须小于 256KB,并确保 ORACLE 对该 input 的 stdout 也小于 256KB;全部 .in/.out 与辅助文件合计必须小于 1MB。若临界输入会导致文件过大或超时,应使用仍能触发复杂度/边界行为的可解析构造并适当缩小,而不是打印海量数据。
|
|
613
|
+
11. 若教师提供标准答案,它是算法和输出格式的唯一权威;ORACLE 必须忠实实现它。
|
|
614
|
+
12. 函数题必须输出用户要求的每一个 TEMPLATE 节:Python 追加到学生代码末尾;Java 为 public class Main 并调用 class Solution;C++ 用 #include "foo.cc"。传统题省略 TEMPLATE。
|
|
469
615
|
|
|
470
616
|
输出必须使用以下原文分节,禁止代码围栏、JSON 外壳或额外说明(不适用的可选节直接省略):
|
|
471
617
|
@@@META@@@
|
|
@@ -524,6 +670,19 @@ function normalizeFileContent(content) {
|
|
|
524
670
|
return '\n';
|
|
525
671
|
return lf.endsWith('\n') ? lf : `${lf}\n`;
|
|
526
672
|
}
|
|
673
|
+
/**
|
|
674
|
+
* 规范化 AI 返回的可执行代码节。
|
|
675
|
+
*
|
|
676
|
+
* 模型偶尔会无视“不要代码围栏”,在每个 @@@ 节内部再次输出
|
|
677
|
+
* ```python ... ```。分节解析器只会移除包裹整个响应的围栏,因此这里
|
|
678
|
+
* 仅剥离完整包裹该代码节的单层围栏;普通数据文件仍走 normalizeFileContent,
|
|
679
|
+
* 不会误删合法输入中的反引号。
|
|
680
|
+
*/
|
|
681
|
+
function normalizeExecutableContent(content) {
|
|
682
|
+
const lf = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim();
|
|
683
|
+
const fenced = lf.match(/^```[^\n]*\n([\s\S]*?)\n```$/);
|
|
684
|
+
return normalizeFileContent(fenced ? fenced[1] : lf);
|
|
685
|
+
}
|
|
527
686
|
/**
|
|
528
687
|
* 校验并规范化「已解析为对象」的生成结果(JSON 与分节文本两条解析路径共用)
|
|
529
688
|
* @throws Error 结构非法时抛出(消息为中文,直接展示给教师)
|
|
@@ -565,7 +724,7 @@ function normalizeGenerationObject(obj, options, parseOptions = {}) {
|
|
|
565
724
|
}
|
|
566
725
|
continue;
|
|
567
726
|
}
|
|
568
|
-
templates[lang] =
|
|
727
|
+
templates[lang] = normalizeExecutableContent(t);
|
|
569
728
|
}
|
|
570
729
|
}
|
|
571
730
|
let stdSolution;
|
|
@@ -573,7 +732,7 @@ function normalizeGenerationObject(obj, options, parseOptions = {}) {
|
|
|
573
732
|
if (rawStd && typeof rawStd.code === 'string' && rawStd.code.trim()) {
|
|
574
733
|
stdSolution = {
|
|
575
734
|
language: typeof rawStd.language === 'string' ? rawStd.language : 'python',
|
|
576
|
-
code:
|
|
735
|
+
code: normalizeExecutableContent(rawStd.code),
|
|
577
736
|
};
|
|
578
737
|
}
|
|
579
738
|
// 填空题判定:用户显式指定时以用户为准,auto 时采纳 AI 结论
|
|
@@ -682,7 +841,7 @@ function parseSandboxBlueprint(raw, options, parseOptions = {}) {
|
|
|
682
841
|
else if (kind === 'TEMPLATE') {
|
|
683
842
|
const lang = (parts[1] || '').trim().toLowerCase();
|
|
684
843
|
if (exports.SUPPORTED_TEMPLATE_LANGS.includes(lang) && content.trim()) {
|
|
685
|
-
templates[lang] =
|
|
844
|
+
templates[lang] = normalizeExecutableContent(content);
|
|
686
845
|
}
|
|
687
846
|
}
|
|
688
847
|
}
|
|
@@ -712,12 +871,12 @@ function parseSandboxBlueprint(raw, options, parseOptions = {}) {
|
|
|
712
871
|
analysis,
|
|
713
872
|
functionName: meta.functionName || undefined,
|
|
714
873
|
templates: problemType === 'function' ? templates : undefined,
|
|
715
|
-
generatorCode:
|
|
716
|
-
oracleCode:
|
|
874
|
+
generatorCode: normalizeExecutableContent(generatorCode),
|
|
875
|
+
oracleCode: normalizeExecutableContent(oracleCode),
|
|
717
876
|
// SOLUTION/BRUTE/VALIDATOR 均可缺失(宽容);缺失后果在 verification 中体现
|
|
718
|
-
solutionCode: solutionCode.trim() ?
|
|
719
|
-
bruteCode: bruteCode.trim() ?
|
|
720
|
-
validatorCode: validatorCode.trim() ?
|
|
877
|
+
solutionCode: solutionCode.trim() ? normalizeExecutableContent(solutionCode) : undefined,
|
|
878
|
+
bruteCode: bruteCode.trim() ? normalizeExecutableContent(bruteCode) : undefined,
|
|
879
|
+
validatorCode: validatorCode.trim() ? normalizeExecutableContent(validatorCode) : undefined,
|
|
721
880
|
notes,
|
|
722
881
|
};
|
|
723
882
|
}
|
|
@@ -923,8 +1082,9 @@ function isCancellation(err) {
|
|
|
923
1082
|
* 双重验证管线(对拍 + 模板实跑 + 输入校验),执行序 a→f。
|
|
924
1083
|
* 各阶段间累计校验总时长预算,避免大批量挤兑沙箱 RAM 盘。
|
|
925
1084
|
*/
|
|
926
|
-
async function materializeSandboxBlueprint(blueprint, options, statementMarkdown, runner, signal) {
|
|
1085
|
+
async function materializeSandboxBlueprint(blueprint, options, statementMarkdown, runner, signal, customChecker = false) {
|
|
927
1086
|
const startedAt = Date.now();
|
|
1087
|
+
const coveragePlan = buildCoveragePlan(options.caseCount, options.dataScale || 'auto');
|
|
928
1088
|
const checkBudget = () => {
|
|
929
1089
|
if (Date.now() - startedAt > goJudgeSandboxService_1.SANDBOX_TOTAL_BUDGET_MS) {
|
|
930
1090
|
throw new Error('沙箱执行总时长超出预算,请减少测试点数量后重试');
|
|
@@ -993,9 +1153,9 @@ async function materializeSandboxBlueprint(blueprint, options, statementMarkdown
|
|
|
993
1153
|
if (Buffer.byteLength(output, 'utf8') > exports.TESTDATA_GEN_LIMITS.MAX_FILE_SIZE) {
|
|
994
1154
|
throw new Error(`ORACLE 为第 ${index + 1} 个测试点生成的 .out 超过 256KB 上限`);
|
|
995
1155
|
}
|
|
996
|
-
return { ...item, output };
|
|
1156
|
+
return { ...item, output, dataScale: coveragePlan[index]?.dataScale };
|
|
997
1157
|
});
|
|
998
|
-
for (let i = 0; i < samples.length; i++) {
|
|
1158
|
+
for (let i = 0; i < samples.length && !customChecker; i++) {
|
|
999
1159
|
const actual = oracleResults[inputs.length + i]?.stdout || '';
|
|
1000
1160
|
if (comparableFileContent(actual) !== comparableFileContent(samples[i].output)) {
|
|
1001
1161
|
throw new Error(`ORACLE 未通过题面样例 ${samples[i].id}:期望 ${JSON.stringify(comparableFileContent(samples[i].output))}`
|
|
@@ -1038,7 +1198,7 @@ async function materializeSandboxBlueprint(blueprint, options, statementMarkdown
|
|
|
1038
1198
|
const oracleIsProvidedStd = !!(providedStd
|
|
1039
1199
|
&& blueprint.problemType === 'traditional'
|
|
1040
1200
|
&& detectStdFilename(providedStd) === 'std.py'
|
|
1041
|
-
&& comparableFileContent(blueprint.oracleCode) === comparableFileContent(
|
|
1201
|
+
&& comparableFileContent(blueprint.oracleCode) === comparableFileContent(normalizeExecutableContent(providedStd)));
|
|
1042
1202
|
let bruteCheck;
|
|
1043
1203
|
if (blueprint.bruteCode) {
|
|
1044
1204
|
checkBudget();
|
|
@@ -1060,8 +1220,8 @@ async function materializeSandboxBlueprint(blueprint, options, statementMarkdown
|
|
|
1060
1220
|
agreed++;
|
|
1061
1221
|
continue;
|
|
1062
1222
|
}
|
|
1063
|
-
//
|
|
1064
|
-
if (oracleIsProvidedStd) {
|
|
1223
|
+
// 教师 std 或自定义 checker 是权威:文本不一致只记录复核,不误判为生成失败。
|
|
1224
|
+
if (oracleIsProvidedStd || customChecker) {
|
|
1065
1225
|
disagreed.push(caseNo);
|
|
1066
1226
|
continue;
|
|
1067
1227
|
}
|
|
@@ -1077,7 +1237,7 @@ async function materializeSandboxBlueprint(blueprint, options, statementMarkdown
|
|
|
1077
1237
|
oracleKind: oracleIsProvidedStd ? 'provided-std' : 'ai-solution',
|
|
1078
1238
|
validator: { ran: validatorRan, casesChecked: validatorRan ? inputs.length : 0 },
|
|
1079
1239
|
};
|
|
1080
|
-
if (blueprint.problemType === 'traditional') {
|
|
1240
|
+
if (blueprint.problemType === 'traditional' && !customChecker) {
|
|
1081
1241
|
// 样例不一致已在上面抛出,走到这里即全部通过
|
|
1082
1242
|
verification.sampleCheck = { total: samples.length, passed: samples.length };
|
|
1083
1243
|
}
|
|
@@ -1090,7 +1250,12 @@ async function materializeSandboxBlueprint(blueprint, options, statementMarkdown
|
|
|
1090
1250
|
'测试输入由生成器产生,所有 .out 已在 Hydro 沙箱中实际运行 Python 标程生成。',
|
|
1091
1251
|
];
|
|
1092
1252
|
if (bruteCheck && bruteCheck.disagreed.length > 0) {
|
|
1093
|
-
noteParts.push(
|
|
1253
|
+
noteParts.push(customChecker
|
|
1254
|
+
? `题目使用自定义 checker;暴力解与标程在测试点 ${bruteCheck.disagreed.join('、')} 的文本输出不同,已保留并请人工复核 checker 语义。`
|
|
1255
|
+
: `暴力解与教师标准答案在测试点 ${bruteCheck.disagreed.join('、')} 不一致,已按教师 std 输出为准,请人工复核。`);
|
|
1256
|
+
}
|
|
1257
|
+
if (customChecker && samples.length > 0) {
|
|
1258
|
+
noteParts.push('题目使用自定义 checker,已验证标程可运行题面样例,但跳过样例输出的纯文本相等检查。');
|
|
1094
1259
|
}
|
|
1095
1260
|
if (bruteCheck && bruteCheck.skippedTimeout.length > 0) {
|
|
1096
1261
|
noteParts.push(`暴力解在测试点 ${bruteCheck.skippedTimeout.join('、')} 超时,已跳过对拍。`);
|
|
@@ -1133,7 +1298,7 @@ function parseTemplateSections(raw) {
|
|
|
1133
1298
|
return;
|
|
1134
1299
|
const content = trimBlankEdges(currentLines);
|
|
1135
1300
|
if (content.trim())
|
|
1136
|
-
templates[currentLang] =
|
|
1301
|
+
templates[currentLang] = normalizeExecutableContent(content);
|
|
1137
1302
|
};
|
|
1138
1303
|
for (const line of text.split(/\r?\n/)) {
|
|
1139
1304
|
const marker = line.match(SECTION_MARKER_RE);
|
|
@@ -1181,11 +1346,16 @@ function assemblePlan(response, options, context = {}) {
|
|
|
1181
1346
|
const dataOrigin = sandbox ? 'executed' : 'ai-only';
|
|
1182
1347
|
const files = [];
|
|
1183
1348
|
const caseCount = response.cases.length;
|
|
1349
|
+
const coveragePlan = buildCoveragePlan(caseCount, options.dataScale || 'auto');
|
|
1350
|
+
const newCaseNumbers = allocateCaseNumbers(context.existingFiles, caseCount);
|
|
1351
|
+
const existingComplete = getExistingNumericCases(context.existingFiles).complete;
|
|
1352
|
+
const configCaseNumbers = [...new Set([...existingComplete, ...newCaseNumbers])].sort((a, b) => a - b);
|
|
1184
1353
|
/** AI 生成代码文件统一入口:文件名只写一处,注释符由文件名推导。 */
|
|
1185
1354
|
const pushCode = (name, code, kind, origin, purpose) => files.push({ name, content: prependPurposeComment(name, code, purpose), kind, origin });
|
|
1186
1355
|
response.cases.forEach((c, i) => {
|
|
1187
|
-
|
|
1188
|
-
files.push({ name: `${
|
|
1356
|
+
const fileNumber = newCaseNumbers[i];
|
|
1357
|
+
files.push({ name: `${fileNumber}.in`, content: c.input, kind: 'case-in', origin: dataOrigin });
|
|
1358
|
+
files.push({ name: `${fileNumber}.out`, content: c.output, kind: 'case-out', origin: dataOrigin });
|
|
1189
1359
|
});
|
|
1190
1360
|
if (response.problemType === 'function') {
|
|
1191
1361
|
for (const lang of options.languages) {
|
|
@@ -1212,14 +1382,15 @@ function assemblePlan(response, options, context = {}) {
|
|
|
1212
1382
|
// 教师提供的标准答案是唯一权威:原样写入(deterministic,非实跑制品)
|
|
1213
1383
|
const providedStd = options.providedStd?.trim();
|
|
1214
1384
|
if (providedStd) {
|
|
1385
|
+
const normalizedProvidedStd = normalizeExecutableContent(providedStd);
|
|
1215
1386
|
files.push({
|
|
1216
1387
|
name: detectStdFilename(providedStd),
|
|
1217
|
-
content:
|
|
1388
|
+
content: normalizedProvidedStd,
|
|
1218
1389
|
kind: 'std',
|
|
1219
1390
|
origin: 'deterministic',
|
|
1220
1391
|
});
|
|
1221
1392
|
if (response.oracleCode?.trim()
|
|
1222
|
-
&&
|
|
1393
|
+
&& normalizeExecutableContent(response.oracleCode) !== normalizedProvidedStd) {
|
|
1223
1394
|
pushCode('oracle.py', response.oracleCode, 'std', sandbox ? 'executed' : 'ai-only', FILE_PURPOSES.oracle);
|
|
1224
1395
|
}
|
|
1225
1396
|
}
|
|
@@ -1231,7 +1402,7 @@ function assemblePlan(response, options, context = {}) {
|
|
|
1231
1402
|
pushCode('std.py', stdContent, 'std', sandbox ? 'executed' : 'ai-only', useSolutionForm ? FILE_PURPOSES.stdSolutionForm : FILE_PURPOSES.stdProgram);
|
|
1232
1403
|
if (sandbox
|
|
1233
1404
|
&& response.oracleCode?.trim()
|
|
1234
|
-
&&
|
|
1405
|
+
&& normalizeExecutableContent(response.oracleCode) !== normalizeExecutableContent(stdContent)) {
|
|
1235
1406
|
pushCode('oracle.py', response.oracleCode, 'std', 'executed', FILE_PURPOSES.oracle);
|
|
1236
1407
|
}
|
|
1237
1408
|
}
|
|
@@ -1240,8 +1411,10 @@ function assemblePlan(response, options, context = {}) {
|
|
|
1240
1411
|
name: 'config.yaml',
|
|
1241
1412
|
content: buildConfigYaml({
|
|
1242
1413
|
problemType: response.problemType,
|
|
1243
|
-
caseCount,
|
|
1414
|
+
caseCount: configCaseNumbers.length,
|
|
1244
1415
|
languages: options.languages,
|
|
1416
|
+
caseNumbers: configCaseNumbers,
|
|
1417
|
+
existingConfig: context.existingConfig,
|
|
1245
1418
|
}),
|
|
1246
1419
|
kind: 'config',
|
|
1247
1420
|
origin: 'deterministic',
|
|
@@ -1253,6 +1426,13 @@ function assemblePlan(response, options, context = {}) {
|
|
|
1253
1426
|
notes: response.notes,
|
|
1254
1427
|
files,
|
|
1255
1428
|
caseCount,
|
|
1429
|
+
totalCaseCount: configCaseNumbers.length,
|
|
1430
|
+
caseCoverage: response.cases.map((item, index) => ({
|
|
1431
|
+
caseNumber: index + 1,
|
|
1432
|
+
fileNumber: newCaseNumbers[index],
|
|
1433
|
+
dataScale: item.dataScale || coveragePlan[index]?.dataScale || 'small',
|
|
1434
|
+
target: item.label || coveragePlan[index]?.guidance || '',
|
|
1435
|
+
})),
|
|
1256
1436
|
...(response.verification ? { verification: response.verification } : {}),
|
|
1257
1437
|
};
|
|
1258
1438
|
}
|
|
@@ -1304,16 +1484,20 @@ function isLikelyFunctionProblem(statementMarkdown) {
|
|
|
1304
1484
|
* 用作 AI 故障时的降级方案——保住最容易出错的 compile.sh / config.yaml /
|
|
1305
1485
|
* 模板机制部分,测试数据内容由教师在预览中手动填写。
|
|
1306
1486
|
*/
|
|
1307
|
-
function buildSkeletonPlan(options, statementMarkdown = '') {
|
|
1487
|
+
function buildSkeletonPlan(options, statementMarkdown = '', existingFiles = [], existingConfig) {
|
|
1308
1488
|
const autoDetectedFunction = options.problemKind === 'auto' && isLikelyFunctionProblem(statementMarkdown);
|
|
1309
1489
|
const problemType = options.problemKind === 'function' || autoDetectedFunction
|
|
1310
1490
|
? 'function'
|
|
1311
1491
|
: 'traditional';
|
|
1312
1492
|
const files = [];
|
|
1493
|
+
const caseNumbers = allocateCaseNumbers(existingFiles, options.caseCount);
|
|
1494
|
+
const existingComplete = getExistingNumericCases(existingFiles).complete;
|
|
1495
|
+
const configCaseNumbers = [...new Set([...existingComplete, ...caseNumbers])].sort((a, b) => a - b);
|
|
1496
|
+
const coveragePlan = buildCoveragePlan(options.caseCount, options.dataScale || 'auto');
|
|
1313
1497
|
// 骨架模式全部为确定性生成/空占位,无沙箱实跑制品
|
|
1314
|
-
for (
|
|
1315
|
-
files.push({ name: `${
|
|
1316
|
-
files.push({ name: `${
|
|
1498
|
+
for (const number of caseNumbers) {
|
|
1499
|
+
files.push({ name: `${number}.in`, content: '\n', kind: 'case-in', origin: 'deterministic' });
|
|
1500
|
+
files.push({ name: `${number}.out`, content: '\n', kind: 'case-out', origin: 'deterministic' });
|
|
1317
1501
|
}
|
|
1318
1502
|
if (problemType === 'function') {
|
|
1319
1503
|
for (const lang of options.languages) {
|
|
@@ -1325,14 +1509,20 @@ function buildSkeletonPlan(options, statementMarkdown = '') {
|
|
|
1325
1509
|
if (providedStd) {
|
|
1326
1510
|
files.push({
|
|
1327
1511
|
name: detectStdFilename(providedStd),
|
|
1328
|
-
content:
|
|
1512
|
+
content: normalizeExecutableContent(providedStd),
|
|
1329
1513
|
kind: 'std',
|
|
1330
1514
|
origin: 'deterministic',
|
|
1331
1515
|
});
|
|
1332
1516
|
}
|
|
1333
1517
|
files.push({
|
|
1334
1518
|
name: 'config.yaml',
|
|
1335
|
-
content: buildConfigYaml({
|
|
1519
|
+
content: buildConfigYaml({
|
|
1520
|
+
problemType,
|
|
1521
|
+
caseCount: configCaseNumbers.length,
|
|
1522
|
+
languages: options.languages,
|
|
1523
|
+
caseNumbers: configCaseNumbers,
|
|
1524
|
+
existingConfig,
|
|
1525
|
+
}),
|
|
1336
1526
|
kind: 'config',
|
|
1337
1527
|
origin: 'deterministic',
|
|
1338
1528
|
});
|
|
@@ -1353,6 +1543,13 @@ function buildSkeletonPlan(options, statementMarkdown = '') {
|
|
|
1353
1543
|
notes: noteParts.join(''),
|
|
1354
1544
|
files,
|
|
1355
1545
|
caseCount: options.caseCount,
|
|
1546
|
+
totalCaseCount: configCaseNumbers.length,
|
|
1547
|
+
caseCoverage: coveragePlan.map((slot, index) => ({
|
|
1548
|
+
caseNumber: slot.caseNumber,
|
|
1549
|
+
fileNumber: caseNumbers[index],
|
|
1550
|
+
dataScale: slot.dataScale,
|
|
1551
|
+
target: slot.guidance,
|
|
1552
|
+
})),
|
|
1356
1553
|
};
|
|
1357
1554
|
}
|
|
1358
1555
|
function buildCaseInputRepairPrompt(issue, options) {
|
|
@@ -1374,21 +1571,143 @@ function buildTemplateRepairPrompt(missing) {
|
|
|
1374
1571
|
3. Java 模板必须是 public class Main,并调用学生提交的 class Solution;C++ 模板通过 #include "foo.cc" 引入学生代码;Python 模板只含驱动代码。
|
|
1375
1572
|
4. 只使用 @@@TEMPLATE:语言@@@ 标记和源码原文,不要输出 JSON、代码围栏或解释文字。`;
|
|
1376
1573
|
}
|
|
1377
|
-
|
|
1574
|
+
/** 携带匿名模型/阶段信息的业务错误,供遥测判断失败是否与模型相关。 */
|
|
1575
|
+
class TestdataGenerationError extends Error {
|
|
1576
|
+
constructor(message, failureStage, results = []) {
|
|
1577
|
+
super(message);
|
|
1578
|
+
this.name = 'TestdataGenerationError';
|
|
1579
|
+
const usedModels = [...new Set(results.map(result => `${result.usedModel.endpointName}/${result.usedModel.modelName}`))];
|
|
1580
|
+
const lastModel = results[results.length - 1]?.usedModel;
|
|
1581
|
+
this.telemetryMetadata = {
|
|
1582
|
+
failureStage,
|
|
1583
|
+
...(lastModel ? {
|
|
1584
|
+
endpointName: lastModel.endpointName,
|
|
1585
|
+
modelName: lastModel.modelName,
|
|
1586
|
+
} : {}),
|
|
1587
|
+
...(usedModels.length > 0 ? { usedModels } : {}),
|
|
1588
|
+
aiAttemptCount: results.length,
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
exports.TestdataGenerationError = TestdataGenerationError;
|
|
1593
|
+
function extractTestdataErrorMetadata(err) {
|
|
1594
|
+
return err instanceof TestdataGenerationError ? err.telemetryMetadata : undefined;
|
|
1595
|
+
}
|
|
1596
|
+
function classifySandboxRepairScope(error) {
|
|
1597
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1598
|
+
if (/未通过输入校验/.test(detail))
|
|
1599
|
+
return 'validator';
|
|
1600
|
+
if (/GENERATOR|\.in 超过|生成\s*\d+\s*个测试点/.test(detail))
|
|
1601
|
+
return 'generator';
|
|
1602
|
+
if (/ORACLE|题面样例/.test(detail))
|
|
1603
|
+
return 'oracle';
|
|
1604
|
+
if (/暴力解/.test(detail))
|
|
1605
|
+
return 'brute';
|
|
1606
|
+
if (/template\.py|模板输出|SOLUTION/.test(detail))
|
|
1607
|
+
return 'template-py';
|
|
1608
|
+
return 'full';
|
|
1609
|
+
}
|
|
1610
|
+
function buildSandboxRepairPrompt(error, options, scope = classifySandboxRepairScope(error)) {
|
|
1378
1611
|
const templates = options.languages.map(lang => `@@@TEMPLATE:${lang}@@@`).join('、') || '(传统题无需模板)';
|
|
1379
1612
|
const detail = (error instanceof Error ? error.message : String(error)).slice(0, 1600);
|
|
1613
|
+
if (scope === 'generator') {
|
|
1614
|
+
return `你上一条蓝图的输入生成阶段未通过 Hydro 沙箱验证:
|
|
1615
|
+
${detail}
|
|
1616
|
+
|
|
1617
|
+
请只输出修复后的 @@@GENERATOR@@@;如果必须同步修改输入约束校验,再额外输出 @@@VALIDATOR@@@。不要重复 META、ORACLE、BRUTE、SOLUTION、TEMPLATE 或说明文字。要求:
|
|
1618
|
+
1. stdout 只能是包含恰好 ${options.caseCount} 个 cases 的紧凑 JSON,使用 json.dumps(..., ensure_ascii=False, separators=(',', ':'))。
|
|
1619
|
+
2. stdout 必须小于 1MB,每个 input 的 UTF-8 内容必须小于 256KB,且全部 .in/.out 与辅助文件合计必须小于 1MB;程序必须在 5 秒内结束,不要打印日志,不要构造超长字符串或无界循环。
|
|
1620
|
+
3. 每个 input 必须合法且符合逐 CASE 覆盖计划;若临界数据过大,使用能保留边界/复杂度特征的可解析构造。
|
|
1621
|
+
4. 只使用请求的分节标记和源码原文,不要代码围栏。`;
|
|
1622
|
+
}
|
|
1623
|
+
if (scope === 'validator') {
|
|
1624
|
+
return `你上一条蓝图的输入校验阶段未通过 Hydro 沙箱验证:
|
|
1625
|
+
${detail}
|
|
1626
|
+
|
|
1627
|
+
请同时输出修复后的 @@@GENERATOR@@@ 与 @@@VALIDATOR@@@。先严格依据题面判断是生成输入非法,还是校验器错误地拒绝了合法输入,再修正对应实现;不要通过放弃题面约束、删除校验器或让校验器无条件成功来绕过验证。每个 input 的 UTF-8 内容必须小于 256KB,全部 .in/.out 与辅助文件合计必须小于 1MB。不要输出其他分节、代码围栏或说明文字。`;
|
|
1628
|
+
}
|
|
1629
|
+
if (scope === 'oracle') {
|
|
1630
|
+
return `你上一条蓝图的标程阶段未通过 Hydro 沙箱验证:
|
|
1631
|
+
${detail}
|
|
1632
|
+
|
|
1633
|
+
请只输出修复后的 @@@ORACLE@@@,并额外输出与其独立的 @@@BRUTE@@@ 以便重新对拍。不要重复 META、GENERATOR、VALIDATOR、SOLUTION、TEMPLATE 或说明文字。ORACLE 必须通过题面样例、处理所有合法边界且在 5 秒内结束,每个测试点的 stdout UTF-8 内容必须小于 256KB;BRUTE 不得调用或复制 ORACLE 的核心实现。`;
|
|
1634
|
+
}
|
|
1635
|
+
if (scope === 'brute') {
|
|
1636
|
+
return `你上一条蓝图的暴力对拍阶段未通过验证:
|
|
1637
|
+
${detail}
|
|
1638
|
+
|
|
1639
|
+
请只输出修复后的 @@@BRUTE@@@。它必须读取原有 GENERATOR 的同一 stdin 编码,独立实现题意,不得调用或复制 ORACLE 的核心函数。不要输出其他分节或代码围栏。`;
|
|
1640
|
+
}
|
|
1641
|
+
if (scope === 'template-py') {
|
|
1642
|
+
return `你上一条蓝图的 Python 学生解与模板组合未通过验证:
|
|
1643
|
+
${detail}
|
|
1644
|
+
|
|
1645
|
+
请只输出同步修复后的 @@@SOLUTION@@@ 与 @@@TEMPLATE:py@@@。两者必须沿用原有 GENERATOR 的 stdin 编码,拼接运行后的输出与 ORACLE 完全一致。不要输出其他分节或代码围栏。`;
|
|
1646
|
+
}
|
|
1380
1647
|
return `你上一条生成蓝图未通过 Hydro 沙箱验证:
|
|
1381
1648
|
${detail}
|
|
1382
1649
|
|
|
1383
1650
|
请重新输出【完整蓝图】(所有节,不得省略上次已有的节),并针对上述失败修正:
|
|
1384
|
-
1. GENERATOR stdout 必须只有合法 JSON,cases 恰好 ${options.caseCount} 个;每个 input 是原始 stdin。
|
|
1651
|
+
1. GENERATOR stdout 必须只有合法 JSON,cases 恰好 ${options.caseCount} 个;每个 input 是原始 stdin、UTF-8 内容小于 256KB,全部 .in/.out 与辅助文件合计小于 1MB。
|
|
1385
1652
|
2. ACM 题若题面有 T,默认每个 input 使用 T=1 并包含恰好一组完整数据;函数题每个 input 只对应一次调用。
|
|
1386
|
-
3. ORACLE 必须是可直接运行的 Python 3
|
|
1653
|
+
3. ORACLE 必须是可直接运行的 Python 3 完整程序,不得硬编码用例答案,并应通过题面样例;每个测试点的 stdout UTF-8 内容必须小于 256KB。
|
|
1387
1654
|
4. BRUTE 必须继续输出,并保持与 ORACLE 相互独立的暴力实现。若失败原因是对拍不一致,先推断 ORACLE 与 BRUTE 谁错并修正错的一方;严禁通过删除 BRUTE 或让两者共享实现来绕过对拍。
|
|
1388
1655
|
5. 上次输出过 VALIDATOR 的必须继续输出;若失败原因是输入未通过校验,修正 GENERATOR 或 VALIDATOR 中错误的一方。
|
|
1389
1656
|
6. 函数题必须完整包含 SOLUTION(学生提交形式)与全部模板:${templates}。
|
|
1390
1657
|
7. 使用 @@@META@@@、@@@GENERATOR@@@、@@@ORACLE@@@、@@@SOLUTION@@@、@@@BRUTE@@@、@@@VALIDATOR@@@、@@@TEMPLATE:语言@@@ 分节原文,不要代码围栏。`;
|
|
1391
1658
|
}
|
|
1659
|
+
function repairSectionContent(sections, header) {
|
|
1660
|
+
const section = sections.find(item => item.header.trim().toUpperCase() === header.toUpperCase());
|
|
1661
|
+
if (!section)
|
|
1662
|
+
return undefined;
|
|
1663
|
+
const content = trimBlankEdges(section.content);
|
|
1664
|
+
return content.trim() ? normalizeExecutableContent(content) : undefined;
|
|
1665
|
+
}
|
|
1666
|
+
/** 将定向修复结果合并进已解析蓝图;缺少必需节时抛错并由调用方回退完整修复。 */
|
|
1667
|
+
function mergeSandboxBlueprintRepair(original, raw, scope) {
|
|
1668
|
+
const sections = splitDelimitedSections(raw);
|
|
1669
|
+
if (sections.length === 0)
|
|
1670
|
+
throw new Error('AI 定向修复未返回分节标记');
|
|
1671
|
+
const merged = {
|
|
1672
|
+
...original,
|
|
1673
|
+
templates: original.templates ? { ...original.templates } : undefined,
|
|
1674
|
+
};
|
|
1675
|
+
if (scope === 'generator' || scope === 'validator') {
|
|
1676
|
+
const generatorCode = repairSectionContent(sections, 'GENERATOR');
|
|
1677
|
+
if (!generatorCode)
|
|
1678
|
+
throw new Error('AI 定向修复未返回 GENERATOR');
|
|
1679
|
+
merged.generatorCode = generatorCode;
|
|
1680
|
+
const validatorCode = repairSectionContent(sections, 'VALIDATOR');
|
|
1681
|
+
if (scope === 'validator' && !validatorCode)
|
|
1682
|
+
throw new Error('AI 输入校验修复未返回 VALIDATOR');
|
|
1683
|
+
if (validatorCode)
|
|
1684
|
+
merged.validatorCode = validatorCode;
|
|
1685
|
+
}
|
|
1686
|
+
else if (scope === 'oracle') {
|
|
1687
|
+
const oracleCode = repairSectionContent(sections, 'ORACLE');
|
|
1688
|
+
if (!oracleCode)
|
|
1689
|
+
throw new Error('AI 定向修复未返回 ORACLE');
|
|
1690
|
+
merged.oracleCode = oracleCode;
|
|
1691
|
+
const bruteCode = repairSectionContent(sections, 'BRUTE');
|
|
1692
|
+
if (bruteCode)
|
|
1693
|
+
merged.bruteCode = bruteCode;
|
|
1694
|
+
}
|
|
1695
|
+
else if (scope === 'brute') {
|
|
1696
|
+
const bruteCode = repairSectionContent(sections, 'BRUTE');
|
|
1697
|
+
if (!bruteCode)
|
|
1698
|
+
throw new Error('AI 定向修复未返回 BRUTE');
|
|
1699
|
+
merged.bruteCode = bruteCode;
|
|
1700
|
+
}
|
|
1701
|
+
else {
|
|
1702
|
+
const solutionCode = repairSectionContent(sections, 'SOLUTION');
|
|
1703
|
+
const templatePy = repairSectionContent(sections, 'TEMPLATE:py');
|
|
1704
|
+
if (!solutionCode || !templatePy)
|
|
1705
|
+
throw new Error('AI 定向修复必须同时返回 SOLUTION 与 TEMPLATE:py');
|
|
1706
|
+
merged.solutionCode = solutionCode;
|
|
1707
|
+
merged.templates = { ...merged.templates, py: templatePy };
|
|
1708
|
+
}
|
|
1709
|
+
return merged;
|
|
1710
|
+
}
|
|
1392
1711
|
function mergeTokenUsage(usages) {
|
|
1393
1712
|
const present = usages.filter((usage) => Boolean(usage));
|
|
1394
1713
|
if (present.length === 0)
|
|
@@ -1441,7 +1760,7 @@ class TestdataGenService {
|
|
|
1441
1760
|
useProvidedPythonOracle(blueprint, options) {
|
|
1442
1761
|
const provided = options.providedStd?.trim();
|
|
1443
1762
|
if (blueprint.problemType === 'traditional' && provided && detectStdFilename(provided) === 'std.py') {
|
|
1444
|
-
return { ...blueprint, oracleCode:
|
|
1763
|
+
return { ...blueprint, oracleCode: normalizeExecutableContent(provided) };
|
|
1445
1764
|
}
|
|
1446
1765
|
return blueprint;
|
|
1447
1766
|
}
|
|
@@ -1511,7 +1830,11 @@ class TestdataGenService {
|
|
|
1511
1830
|
}
|
|
1512
1831
|
}
|
|
1513
1832
|
}
|
|
1514
|
-
const plan = assemblePlan(response, params.options, {
|
|
1833
|
+
const plan = assemblePlan(response, params.options, {
|
|
1834
|
+
mode: 'direct',
|
|
1835
|
+
existingFiles: params.existingFiles,
|
|
1836
|
+
existingConfig: params.existingConfig,
|
|
1837
|
+
});
|
|
1515
1838
|
// 直出模式未经沙箱验证:给出 direct 验证元数据,前端据此渲染「未验证」提示
|
|
1516
1839
|
plan.verification = {
|
|
1517
1840
|
mode: 'direct',
|
|
@@ -1525,18 +1848,40 @@ class TestdataGenService {
|
|
|
1525
1848
|
const callOptions = this.getCallOptions(params.signal);
|
|
1526
1849
|
const initialResult = await this.aiClient.chat([{ role: 'user', content: userPrompt }], systemPrompt, callOptions);
|
|
1527
1850
|
const results = [initialResult];
|
|
1528
|
-
let
|
|
1851
|
+
let blueprintSourceContent = initialResult.content;
|
|
1852
|
+
let blueprint;
|
|
1853
|
+
try {
|
|
1854
|
+
blueprint = this.useProvidedPythonOracle(parseSandboxBlueprint(initialResult.content, params.options, { allowMissingTemplates: true }), params.options);
|
|
1855
|
+
}
|
|
1856
|
+
catch (parseError) {
|
|
1857
|
+
if (isCancellation(parseError))
|
|
1858
|
+
throw parseError;
|
|
1859
|
+
const repairResult = await this.aiClient.chat([
|
|
1860
|
+
{ role: 'user', content: userPrompt },
|
|
1861
|
+
{ role: 'assistant', content: initialResult.content },
|
|
1862
|
+
{ role: 'user', content: buildSandboxRepairPrompt(parseError, params.options, 'full') },
|
|
1863
|
+
], systemPrompt, callOptions);
|
|
1864
|
+
results.push(repairResult);
|
|
1865
|
+
blueprintSourceContent = repairResult.content;
|
|
1866
|
+
try {
|
|
1867
|
+
blueprint = this.useProvidedPythonOracle(parseSandboxBlueprint(repairResult.content, params.options, { allowMissingTemplates: true }), params.options);
|
|
1868
|
+
}
|
|
1869
|
+
catch (repairParseError) {
|
|
1870
|
+
throw new TestdataGenerationError(`AI 自动修复蓝图格式后仍无法解析:${repairParseError instanceof Error ? repairParseError.message : String(repairParseError)}`, 'blueprint_parse', results);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1529
1873
|
if (blueprint.problemType === 'function') {
|
|
1530
1874
|
const missing = params.options.languages.filter(lang => !blueprint.templates?.[lang]?.trim());
|
|
1531
1875
|
if (missing.length > 0) {
|
|
1532
1876
|
const repairResult = await this.aiClient.chat([
|
|
1533
1877
|
{ role: 'user', content: userPrompt },
|
|
1534
|
-
{ role: 'assistant', content:
|
|
1878
|
+
{ role: 'assistant', content: blueprintSourceContent },
|
|
1535
1879
|
{ role: 'user', content: buildTemplateRepairPrompt(missing) },
|
|
1536
1880
|
], systemPrompt, callOptions);
|
|
1537
1881
|
results.push(repairResult);
|
|
1538
1882
|
const repairedTemplates = parseTemplateSections(repairResult.content);
|
|
1539
1883
|
blueprint.templates = { ...blueprint.templates, ...repairedTemplates };
|
|
1884
|
+
blueprintSourceContent = `${blueprintSourceContent}\n${repairResult.content}`;
|
|
1540
1885
|
const stillMissing = params.options.languages.filter(lang => !blueprint.templates?.[lang]?.trim());
|
|
1541
1886
|
if (stillMissing.length > 0) {
|
|
1542
1887
|
throw new Error(`AI 补全后仍缺少 ${stillMissing.map(lang => LANG_DISPLAY[lang]).join('、')}。`);
|
|
@@ -1545,36 +1890,62 @@ class TestdataGenService {
|
|
|
1545
1890
|
}
|
|
1546
1891
|
let response;
|
|
1547
1892
|
try {
|
|
1548
|
-
response = await materializeSandboxBlueprint(blueprint, params.options, params.statementMarkdown, runner, params.signal);
|
|
1893
|
+
response = await materializeSandboxBlueprint(blueprint, params.options, params.statementMarkdown, runner, params.signal, hasCustomChecker(params.existingConfig));
|
|
1549
1894
|
}
|
|
1550
1895
|
catch (firstError) {
|
|
1551
1896
|
if (isCancellation(firstError))
|
|
1552
1897
|
throw firstError;
|
|
1898
|
+
const repairScope = classifySandboxRepairScope(firstError);
|
|
1553
1899
|
let repairResult;
|
|
1554
1900
|
try {
|
|
1555
1901
|
repairResult = await this.aiClient.chat([
|
|
1556
1902
|
{ role: 'user', content: userPrompt },
|
|
1557
|
-
{ role: 'assistant', content:
|
|
1558
|
-
{ role: 'user', content: buildSandboxRepairPrompt(firstError, params.options) },
|
|
1903
|
+
{ role: 'assistant', content: blueprintSourceContent },
|
|
1904
|
+
{ role: 'user', content: buildSandboxRepairPrompt(firstError, params.options, repairScope) },
|
|
1559
1905
|
], systemPrompt, callOptions);
|
|
1560
1906
|
}
|
|
1561
1907
|
catch (err) {
|
|
1562
1908
|
if (isCancellation(err))
|
|
1563
1909
|
throw err;
|
|
1564
|
-
throw new
|
|
1910
|
+
throw new TestdataGenerationError(`AI 生成蓝图未通过 Hydro 沙箱验证,自动修复请求又失败了。技术细节:${err instanceof Error ? err.message : String(err)}`, repairScope, results);
|
|
1565
1911
|
}
|
|
1566
1912
|
results.push(repairResult);
|
|
1567
1913
|
try {
|
|
1568
|
-
|
|
1569
|
-
|
|
1914
|
+
try {
|
|
1915
|
+
blueprint = repairScope === 'full'
|
|
1916
|
+
? parseSandboxBlueprint(repairResult.content, params.options)
|
|
1917
|
+
: mergeSandboxBlueprintRepair(blueprint, repairResult.content, repairScope);
|
|
1918
|
+
blueprintSourceContent = repairScope === 'full' ? repairResult.content : blueprintSourceContent;
|
|
1919
|
+
}
|
|
1920
|
+
catch (targetedParseError) {
|
|
1921
|
+
if (repairScope === 'full')
|
|
1922
|
+
throw targetedParseError;
|
|
1923
|
+
const fullRepairResult = await this.aiClient.chat([
|
|
1924
|
+
{ role: 'user', content: userPrompt },
|
|
1925
|
+
{ role: 'assistant', content: blueprintSourceContent },
|
|
1926
|
+
{
|
|
1927
|
+
role: 'user',
|
|
1928
|
+
content: buildSandboxRepairPrompt(new Error(`定向修复结果不可用:${targetedParseError instanceof Error ? targetedParseError.message : String(targetedParseError)}`), params.options, 'full'),
|
|
1929
|
+
},
|
|
1930
|
+
], systemPrompt, callOptions);
|
|
1931
|
+
results.push(fullRepairResult);
|
|
1932
|
+
blueprint = parseSandboxBlueprint(fullRepairResult.content, params.options);
|
|
1933
|
+
blueprintSourceContent = fullRepairResult.content;
|
|
1934
|
+
}
|
|
1935
|
+
blueprint = this.useProvidedPythonOracle(blueprint, params.options);
|
|
1936
|
+
response = await materializeSandboxBlueprint(blueprint, params.options, params.statementMarkdown, runner, params.signal, hasCustomChecker(params.existingConfig));
|
|
1570
1937
|
}
|
|
1571
1938
|
catch (err) {
|
|
1572
1939
|
if (isCancellation(err))
|
|
1573
1940
|
throw err;
|
|
1574
|
-
throw new
|
|
1941
|
+
throw new TestdataGenerationError(`AI 自动修复后仍未通过 Hydro 沙箱验证。请重试或使用骨架模式。技术细节:${err instanceof Error ? err.message : String(err)}`, classifySandboxRepairScope(err), results);
|
|
1575
1942
|
}
|
|
1576
1943
|
}
|
|
1577
|
-
return this.applyResultMetadata(assemblePlan(response, params.options, {
|
|
1944
|
+
return this.applyResultMetadata(assemblePlan(response, params.options, {
|
|
1945
|
+
mode: 'sandbox',
|
|
1946
|
+
existingFiles: params.existingFiles,
|
|
1947
|
+
existingConfig: params.existingConfig,
|
|
1948
|
+
}), results);
|
|
1578
1949
|
}
|
|
1579
1950
|
}
|
|
1580
1951
|
exports.TestdataGenService = TestdataGenService;
|