658 lines
28 KiB
PHP
658 lines
28 KiB
PHP
<?php
|
||
/**
|
||
* Shared, side-effect-free policy helpers for submission guidance.
|
||
*
|
||
* Keep this file independent from the database so the policy can be tested by
|
||
* the PHP CLI without loading the HUSTOJ runtime.
|
||
*/
|
||
|
||
if (!function_exists('llm_guidance_is_terminal_error')) {
|
||
function llm_guidance_is_terminal_error($result_code) {
|
||
$result_code = intval($result_code);
|
||
return $result_code >= 5 && $result_code <= 11;
|
||
}
|
||
}
|
||
|
||
if (!function_exists('llm_guidance_source_metrics')) {
|
||
function llm_guidance_source_metrics($source) {
|
||
$source = str_replace(array("\r\n", "\r"), "\n", strval($source));
|
||
$clean = preg_replace('!/\*.*?\*/!s', ' ', $source);
|
||
$clean = preg_replace('/\/\/[^\n]*/', ' ', $clean);
|
||
if ($clean === null) $clean = $source;
|
||
|
||
$lines = explode("\n", $clean);
|
||
$meaningful_lines = 0;
|
||
$activity_lines = 0;
|
||
$metric_lines = array();
|
||
|
||
foreach ($lines as $line) {
|
||
$line = trim($line);
|
||
if ($line === '') continue;
|
||
// Preprocessor directives and # comments are not student logic.
|
||
// This also prevents Python/Ruby/shell comment-only submissions
|
||
// from inflating completion metrics.
|
||
if (strpos($line, '#') === 0) continue;
|
||
if (preg_match('/^using\s+namespace\b.*;$/i', $line)) continue;
|
||
if (preg_match('/^(?:package|import)\s+[\w.*]+;?$/i', $line)) continue;
|
||
if (preg_match('/^[{};]+$/', $line)) continue;
|
||
|
||
$meaningful_lines++;
|
||
$metric_lines[] = $line;
|
||
|
||
$activity = preg_replace('/\breturn\s+(?:0|null|none)\s*;?$/i', '', $line);
|
||
$activity = preg_replace('/\b(?:int|void)\s+main\s*\([^)]*\)\s*\{?$/i', '', $activity);
|
||
$activity = trim($activity);
|
||
if ($activity !== '' && !preg_match('/^(?:public|private|protected)?\s*(?:class|struct)\s+\w+\s*\{?$/i', $activity)) {
|
||
$activity_lines++;
|
||
}
|
||
}
|
||
|
||
$metric_source = implode("\n", $metric_lines);
|
||
$non_space = preg_replace('/\s+/', '', $metric_source);
|
||
$non_space_chars = strlen($non_space === null ? '' : $non_space);
|
||
$analysis_source = preg_replace('/"(?:\\\\.|[^"\\\\])*"/s', '""', $metric_source);
|
||
$analysis_source = preg_replace("/'(?:\\\\.|[^'\\\\])*'/s", "''", $analysis_source);
|
||
if ($analysis_source === null) $analysis_source = $metric_source;
|
||
$signal_count = preg_match_all(
|
||
'/\b(?:if|else|for|while|do|switch|case|return|break|continue|cin|cout|scanf|printf|input|print|read|readln|readline|read_line|write|writeln|writeline|readFileSync)\b|(?<![=!<>])=(?!=)|:=|\+\+|--/i',
|
||
$analysis_source,
|
||
$unused
|
||
);
|
||
if ($signal_count === false) $signal_count = 0;
|
||
|
||
// Completion is based on observable program stages, not source length.
|
||
// This keeps legitimate one-line Python/C++ programs out of the
|
||
// "blank submission" bucket while still catching templates and
|
||
// constant-answer fishing.
|
||
$has_input = preg_match(
|
||
'/\b(?:cin|scanf|fscanf|getchar|gets|fgets|input|read(?:ln|_?line)?|readfilesync|prompt|next(?:int|long|double|float|line)?|scanner|stdin|fmt\s*\.\s*scan\w*)\b/i',
|
||
$analysis_source
|
||
) === 1;
|
||
$has_output = preg_match(
|
||
'/\b(?:cout|cerr|printf|fprintf|puts|putchar|print|println|write(?:ln|line)?|console\s*\.\s*log|system\s*\.\s*out|fmt\s*\.\s*print\w*|echo)\b/i',
|
||
$analysis_source
|
||
) === 1;
|
||
$has_processing = preg_match(
|
||
'/\b(?:if|else|for|while|do|switch|case|sort|sum|max|min|count|reverse|reduce|filter|map)\b|'
|
||
. '(?:\+\+|--|:=|==|!=|<=|>=|&&|\|\|)|'
|
||
. '(?:[\w\]\)][ \t]*(?:\+|-|\*|\/|%)[ \t]*[\w\[\(])|'
|
||
. '(?:(?<!<)<(?![<=])|(?<!>)>(?![>=])|(?<!\|)\|(?!\|)|[&^~]|\?[^:\r\n]+:)|'
|
||
. '(?:\[[^\]\r\n]*:[^\]\r\n]*\])/i',
|
||
$analysis_source
|
||
) === 1;
|
||
$stage_count = intval($has_input) + intval($has_processing) + intval($has_output);
|
||
$constant_output_only = $has_output && !$has_input && !$has_processing;
|
||
$template_only = !$has_input
|
||
&& !$has_output
|
||
&& !$has_processing
|
||
&& ($non_space_chars < 120 || $signal_count <= 1);
|
||
$trivial = $non_space_chars < 5 || $constant_output_only || $template_only;
|
||
|
||
$source_score = 0.12
|
||
+ 0.22 * intval($has_input)
|
||
+ 0.30 * intval($has_processing)
|
||
+ 0.22 * intval($has_output)
|
||
+ min(0.14, $activity_lines * 0.02);
|
||
if ($trivial) $source_score = min($source_score, 0.20);
|
||
|
||
return array(
|
||
'non_space_chars' => $non_space_chars,
|
||
'meaningful_lines' => $meaningful_lines,
|
||
'activity_lines' => $activity_lines,
|
||
'signal_count' => $signal_count,
|
||
'has_input' => $has_input,
|
||
'has_processing' => $has_processing,
|
||
'has_output' => $has_output,
|
||
'stage_count' => $stage_count,
|
||
'constant_output_only' => $constant_output_only,
|
||
'template_only' => $template_only,
|
||
'source_score' => max(0, min(1, $source_score)),
|
||
'trivial' => $trivial,
|
||
);
|
||
}
|
||
}
|
||
|
||
if (!function_exists('llm_guidance_assess')) {
|
||
/**
|
||
* Return the guidance mode without consulting a reference solution.
|
||
* pass_rate accepts either 0..1 (database form) or 0..100.
|
||
*/
|
||
function llm_guidance_assess($source, $result_code, $pass_rate) {
|
||
$result_code = intval($result_code);
|
||
$metrics = llm_guidance_source_metrics($source);
|
||
$pass_fraction = floatval($pass_rate);
|
||
if ($pass_fraction > 1) $pass_fraction /= 100;
|
||
$pass_fraction = max(0, min(1, $pass_fraction));
|
||
|
||
if (!llm_guidance_is_terminal_error($result_code)) {
|
||
return array_merge($metrics, array(
|
||
'score' => 0,
|
||
'level' => 'none',
|
||
'mode' => 'hidden',
|
||
'judge_pass_rate' => $pass_fraction,
|
||
'auto_fetch' => false,
|
||
'show_output_diff' => false,
|
||
));
|
||
}
|
||
|
||
// CE locations come only from the compiler and the student's own
|
||
// source. They are useful and non-spoiling even for very short code.
|
||
if ($result_code === 11) {
|
||
$level = $metrics['trivial'] ? 'low' : 'medium';
|
||
$combined = $metrics['trivial'] ? 0.20 : max(0.45, $metrics['source_score']);
|
||
$mode = 'compile_location';
|
||
} elseif ($result_code === 5 && $metrics['has_output']) {
|
||
// PE is judge-proven evidence that meaningful output exists. Even
|
||
// a compact program should go straight to the format comparison.
|
||
$level = 'high';
|
||
$combined = 0.72;
|
||
$mode = 'output_diff';
|
||
} elseif ($metrics['trivial']) {
|
||
$level = 'low';
|
||
$combined = min(0.20, $metrics['source_score']);
|
||
$mode = 'flowchart';
|
||
} else {
|
||
// The judge stops at the first failed testcase, so pass_rate=0 is
|
||
// not negative evidence. A complete input/process/output pipeline
|
||
// is enough to treat the submission as a real attempt.
|
||
$combined = max($metrics['source_score'], min(0.85, 0.45 + 0.40 * $pass_fraction));
|
||
$complete_pipeline = intval($metrics['stage_count']) === 3;
|
||
$positive_judge_evidence = $pass_fraction > 0
|
||
&& (intval($metrics['stage_count']) >= 2 || $pass_fraction >= 0.20);
|
||
|
||
if (($result_code === 5 || $result_code === 6)
|
||
&& ($complete_pipeline || $positive_judge_evidence)) {
|
||
$level = 'high';
|
||
$combined = max($combined, 0.72);
|
||
} else {
|
||
$level = 'medium';
|
||
}
|
||
|
||
if ($level === 'medium') $combined = min($combined, 0.69);
|
||
if ($level === 'high') $combined = max($combined, 0.70);
|
||
|
||
if ($level === 'low') {
|
||
$mode = 'flowchart';
|
||
} elseif ($level === 'high' && ($result_code === 5 || $result_code === 6)) {
|
||
$mode = 'output_diff';
|
||
} else {
|
||
$mode = 'focused_hint';
|
||
}
|
||
}
|
||
|
||
return array_merge($metrics, array(
|
||
'score' => intval(round(max(0, min(1, $combined)) * 100)),
|
||
'level' => $level,
|
||
'mode' => $mode,
|
||
'judge_pass_rate' => $pass_fraction,
|
||
'auto_fetch' => true,
|
||
'show_output_diff' => $mode === 'output_diff',
|
||
));
|
||
}
|
||
}
|
||
|
||
if (!function_exists('llm_guidance_mode_label')) {
|
||
function llm_guidance_mode_label($mode) {
|
||
$labels = array(
|
||
'flowchart' => '流程图引导',
|
||
'focused_hint' => '聚焦排错',
|
||
'output_diff' => '输出差异定位',
|
||
'judge_output' => '评测输出定位',
|
||
'compile_location' => '编译错误定位',
|
||
);
|
||
return isset($labels[$mode]) ? $labels[$mode] : 'AI 指导';
|
||
}
|
||
}
|
||
|
||
if (!function_exists('llm_guidance_parse_diff_line')) {
|
||
/** Parse one judge-generated Expected/Yours row. */
|
||
function llm_guidance_parse_diff_line($line) {
|
||
$line = rtrim(strval($line), "\r\n");
|
||
if ($line === '') return null;
|
||
|
||
if (isset($line[0]) && $line[0] === '|') {
|
||
$rest = substr($line, 1);
|
||
// judge_client normalizes pipes in the student's output but not in
|
||
// the expected output, so the final pipe is the field delimiter.
|
||
$separator = strrpos($rest, '|');
|
||
if ($separator !== false) {
|
||
return array(
|
||
substr($rest, 0, $separator),
|
||
substr($rest, $separator + 1),
|
||
);
|
||
}
|
||
}
|
||
|
||
// diff -y format: greedily select the last spaced pipe delimiter.
|
||
if (preg_match('/^(.*)\s+\|\s*(.*)$/', $line, $match)) {
|
||
return array(trim($match[1]), trim($match[2]));
|
||
}
|
||
// Legacy diff -y output can use a whitespace-surrounded backslash as
|
||
// the changed-line delimiter.
|
||
if (preg_match('/^(.*)\s+\\\\\s+(.*)$/', $line, $match)) {
|
||
return array(trim($match[1]), trim($match[2]));
|
||
}
|
||
if (preg_match('/^(.*?)\s+([<>])\s+(.*)$/', $line, $match)) {
|
||
return array($match[1], $match[3]);
|
||
}
|
||
return null;
|
||
}
|
||
}
|
||
|
||
if (!function_exists('llm_guidance_parse_diff_rows')) {
|
||
/** Parse the rows after a judge Expected/Yours header. */
|
||
function llm_guidance_parse_diff_rows($body) {
|
||
$body = str_replace(array("\r\n", "\r"), "\n", strval($body));
|
||
$lines = explode("\n", $body);
|
||
$expected = array();
|
||
$yours = array();
|
||
$found_header = false;
|
||
|
||
foreach ($lines as $line) {
|
||
if (!$found_header && preg_match('/^\|?\s*Expected\s*\|\s*Yours\s*\|?\s*$/i', trim($line))) {
|
||
$found_header = true;
|
||
continue;
|
||
}
|
||
// The Markdown separator is metadata even after the header flag
|
||
// has been set; never display it as a fake first diff row.
|
||
if (preg_match('/^\s*\|?\s*-+\s*\|\s*-+\s*\|?\s*$/', $line)) continue;
|
||
if (!$found_header || $line === '') continue;
|
||
|
||
$parsed = llm_guidance_parse_diff_line($line);
|
||
if ($parsed === null) continue;
|
||
$expected[] = $parsed[0];
|
||
$yours[] = $parsed[1];
|
||
}
|
||
return array('expected' => $expected, 'yours' => $yours);
|
||
}
|
||
}
|
||
|
||
if (!function_exists('llm_guidance_parse_diff_blocks')) {
|
||
/**
|
||
* Parse both current simple Markdown blocks and legacy =====[file]=====
|
||
* blocks. Full diff mode is rejected before any content is interpreted.
|
||
*/
|
||
function llm_guidance_parse_diff_blocks($text) {
|
||
$text = str_replace(array("\r\n", "\r"), "\n", strval($text));
|
||
$blocks = array();
|
||
$seen = array();
|
||
if (trim($text) === '' || llm_guidance_has_full_diff_sections($text)) return $blocks;
|
||
|
||
$append_block = function($name, $body) use (&$blocks, &$seen) {
|
||
$name = trim(strval($name));
|
||
if (llm_guidance_testcase_base($name) === null) return;
|
||
$rows = llm_guidance_parse_diff_rows($body);
|
||
if (empty($rows['expected'])) return;
|
||
$key = hash('sha256', $name . "\0" . serialize($rows));
|
||
if (isset($seen[$key])) return;
|
||
$seen[$key] = true;
|
||
$blocks[] = array(
|
||
'name' => $name,
|
||
'expected' => $rows['expected'],
|
||
'yours' => $rows['yours'],
|
||
'full_mode' => false,
|
||
);
|
||
};
|
||
|
||
// A blank line before the closing ===== line is valid legacy output,
|
||
// so do not split these blocks on blank lines first.
|
||
if (preg_match_all(
|
||
'/^=+\[([^\]\r\n]+)\]=+[ \t]*\n(.*?)^[ \t]*=+[ \t]*$/ms',
|
||
$text,
|
||
$legacy_blocks,
|
||
PREG_SET_ORDER
|
||
)) {
|
||
foreach ($legacy_blocks as $legacy_block) {
|
||
$append_block($legacy_block[1], trim($legacy_block[2], "\n\r"));
|
||
}
|
||
}
|
||
|
||
// Current simple mode separates testcase blocks with a blank line.
|
||
$chunks = preg_split('/\n\s*\n/', $text);
|
||
foreach ($chunks as $chunk) {
|
||
$chunk = trim($chunk, "\r\n");
|
||
if ($chunk === '') continue;
|
||
if (preg_match('/^([^\n=|]+)\n--\n([\s\S]+)$/', $chunk, $simple_block)) {
|
||
$append_block($simple_block[1], $simple_block[2]);
|
||
}
|
||
}
|
||
|
||
return $blocks;
|
||
}
|
||
}
|
||
|
||
if (!function_exists('llm_guidance_output_attempt')) {
|
||
/**
|
||
* Inspect judge-generated Expected/Yours rows without returning either
|
||
* value to callers. This catches padded/template submissions that produced
|
||
* no meaningful output even when their source text is long.
|
||
*/
|
||
function llm_guidance_output_attempt($runtime_info) {
|
||
$lines = preg_split('/\r\n|\r|\n/', strval($runtime_info));
|
||
$after_header = false;
|
||
$pairs = 0;
|
||
$expected_chars = 0;
|
||
$yours_chars = 0;
|
||
$expected_nonempty_lines = 0;
|
||
$yours_nonempty_lines = 0;
|
||
|
||
foreach ($lines as $line) {
|
||
$line = rtrim($line, "\r\n");
|
||
if (preg_match('/Expected.*\|.*Yours/i', $line)) {
|
||
$after_header = true;
|
||
continue;
|
||
}
|
||
if (!$after_header || trim($line) === '') continue;
|
||
if (preg_match('/^\s*=+\s*$/', $line)) {
|
||
$after_header = false;
|
||
continue;
|
||
}
|
||
if (preg_match('/^\s*\|?\s*-+\s*\|\s*-+\s*\|?\s*$/', $line)) continue;
|
||
|
||
$parsed = llm_guidance_parse_diff_line($line);
|
||
if ($parsed === null) continue;
|
||
$expected = $parsed[0];
|
||
$yours = $parsed[1];
|
||
$expected_compact = preg_replace('/\s+/', '', $expected);
|
||
$yours_compact = preg_replace('/\s+/', '', $yours);
|
||
$expected_length = strlen($expected_compact === null ? '' : $expected_compact);
|
||
$yours_length = strlen($yours_compact === null ? '' : $yours_compact);
|
||
|
||
$pairs++;
|
||
$expected_chars += $expected_length;
|
||
$yours_chars += $yours_length;
|
||
if ($expected_length > 0) $expected_nonempty_lines++;
|
||
if ($yours_length > 0) $yours_nonempty_lines++;
|
||
}
|
||
|
||
$ratio = $expected_chars > 0
|
||
? min(1, $yours_chars / $expected_chars)
|
||
: ($yours_chars > 0 ? 1 : 0);
|
||
|
||
return array(
|
||
'available' => $pairs > 0,
|
||
'pairs' => $pairs,
|
||
'expected_chars' => $expected_chars,
|
||
'yours_chars' => $yours_chars,
|
||
'expected_nonempty_lines' => $expected_nonempty_lines,
|
||
'yours_nonempty_lines' => $yours_nonempty_lines,
|
||
'ratio' => $ratio,
|
||
);
|
||
}
|
||
}
|
||
|
||
if (!function_exists('llm_guidance_testcase_base')) {
|
||
/**
|
||
* Normalize a judge testcase filename/request to its safe basename.
|
||
* Only conservative ASCII names are accepted because the value is later
|
||
* used to open files below OJ_DATA.
|
||
*/
|
||
function llm_guidance_testcase_base($candidate) {
|
||
$candidate = trim(strval($candidate));
|
||
if (preg_match('/\.out$/i', $candidate)) {
|
||
$candidate = substr($candidate, 0, -4);
|
||
}
|
||
if ($candidate === ''
|
||
|| strpos($candidate, '..') !== false
|
||
|| !preg_match('/^[A-Za-z0-9][A-Za-z0-9_.-]*$/', $candidate)) {
|
||
return null;
|
||
}
|
||
return $candidate;
|
||
}
|
||
}
|
||
|
||
if (!function_exists('llm_guidance_diff_testcase_names')) {
|
||
/**
|
||
* Return only testcase basenames that the judge recorded for this SID.
|
||
* Expected/user output values are intentionally not returned.
|
||
*/
|
||
function llm_guidance_diff_testcase_names($runtime_info) {
|
||
$text = str_replace(array("\r\n", "\r"), "\n", strval($runtime_info));
|
||
$lines = explode("\n", $text);
|
||
$names = array();
|
||
$seen = array();
|
||
$count = count($lines);
|
||
|
||
for ($i = 0; $i < $count; $i++) {
|
||
$line = rtrim($lines[$i]);
|
||
$candidate = null;
|
||
|
||
// OJ_FULL_DIFF header. The caller separately decides whether full
|
||
// mode is safe to expose; recognizing its name is still useful for
|
||
// diagnostics and tests.
|
||
if (preg_match('/^=+\[([^\]\r\n]+\.out)\]=+\s*$/i', $line, $match)) {
|
||
$candidate = $match[1];
|
||
} elseif ($i + 2 < $count
|
||
&& trim($lines[$i + 1]) === '--'
|
||
&& preg_match('/^\|?\s*Expected\s*\|\s*Yours\s*\|?\s*$/i', trim($lines[$i + 2]))) {
|
||
// Simple diff blocks begin at the start of the runtime text or
|
||
// after the blank separator written by judge_client.
|
||
$at_boundary = $i === 0 || trim($lines[$i - 1]) === '';
|
||
if ($at_boundary && preg_match('/\.out\s*$/i', $line)) {
|
||
$candidate = trim($line);
|
||
}
|
||
}
|
||
|
||
if ($candidate === null || !preg_match('/\.out$/i', $candidate)) continue;
|
||
$base = llm_guidance_testcase_base($candidate);
|
||
if ($base === null || isset($seen[$base])) continue;
|
||
$seen[$base] = true;
|
||
$names[] = $base;
|
||
}
|
||
return $names;
|
||
}
|
||
}
|
||
|
||
if (!function_exists('llm_guidance_has_full_diff_sections')) {
|
||
/** Full mode contains hidden test input and must not reach normal users. */
|
||
function llm_guidance_has_full_diff_sections($runtime_info) {
|
||
$text = strval($runtime_info);
|
||
// runtimeinfo is capped, so later sections may be truncated. One
|
||
// distinctive full-mode marker is enough to fail closed. Accept both
|
||
// a real newline and shells that preserve the literal "\n" prefix.
|
||
return preg_match(
|
||
'/(?:^|[\r\n]|\\\\n)\s*-{3,}\s*(?:(?:test in|test out|user out) top|diff out) \d+ lines\s*-{3,}/i',
|
||
$text
|
||
) === 1;
|
||
}
|
||
}
|
||
|
||
if (!function_exists('llm_guidance_redact_full_diff')) {
|
||
/**
|
||
* Fail closed for full mode. Its separator-looking lines can also occur
|
||
* inside testcase content, so parsing the same untrusted blob cannot safely
|
||
* recover only the student's section.
|
||
*/
|
||
function llm_guidance_redact_full_diff($runtime_info) {
|
||
return '评测器生成了完整差异记录,其中包含隐藏测试输入和期望输出,'
|
||
. '已按当前账号权限省略。管理员或源码浏览权限账号可查看原文。';
|
||
}
|
||
}
|
||
|
||
if (!function_exists('llm_guidance_refine_with_output_attempt')) {
|
||
function llm_guidance_refine_with_output_attempt($assessment, $result_code, $attempt) {
|
||
$result_code = intval($result_code);
|
||
if ($result_code !== 5 && $result_code !== 6) {
|
||
return $assessment;
|
||
}
|
||
|
||
if (empty($attempt['available'])) {
|
||
if ($assessment['mode'] === 'output_diff') {
|
||
$assessment['mode'] = 'judge_output';
|
||
$assessment['show_output_diff'] = false;
|
||
}
|
||
return $assessment;
|
||
}
|
||
|
||
// A single failing testcase can legitimately produce no output (for
|
||
// example, an early return or runtime fault). It is evidence about that
|
||
// testcase, not proof that the whole submission is a blank answer.
|
||
return $assessment;
|
||
}
|
||
}
|
||
|
||
if (!function_exists('llm_guidance_render_focus_payload')) {
|
||
/**
|
||
* Convert an allowlisted model classification into fixed server wording.
|
||
* No free-form model prose is ever returned to the browser.
|
||
*/
|
||
function llm_guidance_render_focus_payload($payload, $source) {
|
||
$data = json_decode(trim(strval($payload)), true);
|
||
if (!is_array($data)
|
||
|| !isset($data['category'])
|
||
|| !isset($data['confidence'])) {
|
||
return null;
|
||
}
|
||
|
||
$templates = array(
|
||
'input' => array('输入读取', '核对读取顺序、数量和数据类型,再用样例逐项对应。'),
|
||
'output' => array('输出生成', '检查是否多输出文字、少输出内容,或格式与题目要求不一致。'),
|
||
'condition' => array('条件判断', '用一组边界输入分别判断条件为真和为假时会走到哪里。'),
|
||
'loop' => array('循环控制', '只检查循环的起点、终点和每轮更新,手算前两轮与最后一轮。'),
|
||
'boundary' => array('边界处理', '分别代入最小值、最大值和临界值,观察下标与条件是否越界。'),
|
||
'initialization' => array('初值与重置', '检查变量第一次使用前的值,以及多轮处理之间是否需要重新初始化。'),
|
||
'type' => array('数据类型', '估算变量可能达到的范围,并检查运算中的类型转换。'),
|
||
'runtime' => array('运行时位置', '用最小输入复现,查看数组下标、除数和对象状态。'),
|
||
'complexity' => array('执行次数', '按最大数据范围估算最内层语句会执行多少次。'),
|
||
'memory' => array('内存使用', '核对数组或容器的规模、作用域和重复分配。'),
|
||
'state' => array('变量状态', '逐步记录关键变量,找到第一次与手算结果不同的位置。'),
|
||
'unknown' => array('最小复现', '选一组最小输入逐步手算,定位第一次偏离预期的位置。'),
|
||
);
|
||
|
||
$category = strval($data['category']);
|
||
if (!isset($templates[$category])) return null;
|
||
$confidence = strval($data['confidence']);
|
||
if (!in_array($confidence, array('low', 'medium', 'high'), true)) return null;
|
||
$line = isset($data['line']) ? intval($data['line']) : 0;
|
||
$source_lines = preg_split('/\r\n|\r|\n/', strval($source));
|
||
if ($line < 1 || $line > count($source_lines)) $line = 0;
|
||
if ($confidence !== 'high') $line = 0;
|
||
|
||
$where = $line > 0
|
||
? '第 ' . $line . ' 行附近:' . $templates[$category][0]
|
||
: $templates[$category][0];
|
||
return "### 建议检查\n" . $where . ':' . $templates[$category][1];
|
||
}
|
||
}
|
||
|
||
if (!function_exists('llm_guidance_limit_text')) {
|
||
function llm_guidance_limit_text($text, $max_length) {
|
||
$text = trim(strval($text));
|
||
$max_length = max(1, intval($max_length));
|
||
if (function_exists('mb_strlen') && function_exists('mb_substr')) {
|
||
return mb_strlen($text, 'UTF-8') > $max_length
|
||
? mb_substr($text, 0, $max_length, 'UTF-8') . '…'
|
||
: $text;
|
||
}
|
||
return strlen($text) > $max_length
|
||
? substr($text, 0, $max_length) . '...'
|
||
: $text;
|
||
}
|
||
}
|
||
|
||
if (!function_exists('llm_guidance_extract_compile_locations')) {
|
||
/**
|
||
* Parse common GCC/Clang/Javac/MSVC/Python locations and return excerpts
|
||
* from the student's own source only. No corrected/reference code exists
|
||
* in this data structure.
|
||
*/
|
||
function llm_guidance_extract_compile_locations($error_text, $source, $context_lines, $max_locations) {
|
||
$context_lines = max(0, intval($context_lines));
|
||
$max_locations = max(1, intval($max_locations));
|
||
$source = str_replace(array("\r\n", "\r"), "\n", strval($source));
|
||
$source_lines = explode("\n", $source);
|
||
$error_lines = preg_split('/\r\n|\r|\n/', strval($error_text));
|
||
$locations = array();
|
||
$seen = array();
|
||
$pending_message = '编译错误';
|
||
|
||
foreach ($error_lines as $error_line) {
|
||
$line_number = 0;
|
||
$column_number = 0;
|
||
$message = '';
|
||
|
||
if (preg_match('/^\s*(?:fatal\s+)?error(?:\[[^\]]+\])?:\s*(.+)$/i', $error_line, $headline)) {
|
||
$pending_message = $headline[1];
|
||
}
|
||
|
||
// GCC, Clang and javac: file.cpp:12:7: error: message
|
||
if (preg_match('/^.*?:(\d+):(?:(\d+):)?\s*(?:fatal\s+)?error:\s*(.+)$/i', $error_line, $match)) {
|
||
$line_number = intval($match[1]);
|
||
$column_number = isset($match[2]) ? intval($match[2]) : 0;
|
||
$message = $match[3];
|
||
}
|
||
// Free Pascal: main.pas(12,7) Error: message
|
||
elseif (preg_match('/^.*?\((\d+)(?:,(\d+))?\)\s*(?:Fatal|Error):\s*(.+)$/i', $error_line, $match)) {
|
||
$line_number = intval($match[1]);
|
||
$column_number = isset($match[2]) ? intval($match[2]) : 0;
|
||
$message = $match[3];
|
||
}
|
||
// MSVC: file.cpp(12,7): error C2143: message
|
||
elseif (preg_match('/^.*?\((\d+)(?:,(\d+))?\)\s*:\s*(?:fatal\s+)?error[^:]*:\s*(.+)$/i', $error_line, $match)) {
|
||
$line_number = intval($match[1]);
|
||
$column_number = isset($match[2]) ? intval($match[2]) : 0;
|
||
$message = $match[3];
|
||
}
|
||
// Rust: " --> src/main.rs:12:7" follows an error headline.
|
||
elseif (preg_match('/^\s*-->\s*.*?:(\d+):(\d+)\s*$/', $error_line, $match)) {
|
||
$line_number = intval($match[1]);
|
||
$column_number = intval($match[2]);
|
||
$message = $pending_message;
|
||
}
|
||
// Python-style parser location: File "main.py", line 12
|
||
elseif (preg_match('/File\s+"[^"]+",\s*line\s+(\d+)/i', $error_line, $match)) {
|
||
$line_number = intval($match[1]);
|
||
$message = '编译器在此行附近报告语法错误';
|
||
}
|
||
// PHP: "... in Main.php on line 12"
|
||
elseif (preg_match('/\bin\s+\S+\s+on\s+line\s+(\d+)\b/i', $error_line, $match)) {
|
||
$line_number = intval($match[1]);
|
||
$message = '解释器在此行附近报告语法错误';
|
||
}
|
||
// Bash: "Main.sh: line 12: syntax error"
|
||
elseif (preg_match('/:\s*line\s+(\d+):/i', $error_line, $match)) {
|
||
$line_number = intval($match[1]);
|
||
$message = '解释器在此行附近报告语法错误';
|
||
}
|
||
// Ruby and other single-column diagnostics: Main.rb:12: syntax error
|
||
elseif (preg_match('/^.*?:(\d+):\s*(?:(?:syntax|parse)\s+error.*|.+Error:.*)$/i', $error_line, $match)) {
|
||
$line_number = intval($match[1]);
|
||
$message = '解释器在此行附近报告语法错误';
|
||
}
|
||
// Go and similar compilers: main.go:12:7: undefined: name
|
||
elseif (preg_match('/^.*?:(\d+):(\d+):\s*(.+)$/', $error_line, $match)
|
||
&& !preg_match('/^\s*(?:warning|note):/i', $match[3])) {
|
||
$line_number = intval($match[1]);
|
||
$column_number = intval($match[2]);
|
||
$message = $match[3];
|
||
}
|
||
|
||
if ($line_number < 1 || $line_number > count($source_lines)) continue;
|
||
$key = $line_number . ':' . $column_number;
|
||
if (isset($seen[$key])) continue;
|
||
$seen[$key] = true;
|
||
|
||
$start = max(1, $line_number - $context_lines);
|
||
$end = min(count($source_lines), $line_number + $context_lines);
|
||
$excerpt = array();
|
||
for ($line = $start; $line <= $end; $line++) {
|
||
$excerpt[] = array(
|
||
'number' => $line,
|
||
'text' => $source_lines[$line - 1],
|
||
'is_error' => $line === $line_number,
|
||
);
|
||
}
|
||
|
||
$locations[] = array(
|
||
'line' => $line_number,
|
||
'column' => $column_number,
|
||
'message' => '编译错误',
|
||
'excerpt' => $excerpt,
|
||
);
|
||
if (count($locations) >= $max_locations) break;
|
||
}
|
||
|
||
return $locations;
|
||
}
|
||
}
|