feat(ui): 重新设计判题结果页与提交页

- 判题结果页引入友好错误解释面板,按 verdict 类型给出提示与改进建议
- 新增 dedup_runtimeinfo() 函数将重复 RE 测试点折叠为区间汇总
- 区分管理员/源浏览者与普通用户视图,原始错误默认折叠
- 错误模式匹配扩展至 28 条,覆盖 glibc/Java/Python/Go 等常见运行时异常
- 提交页改用卡片式布局,编辑器工具栏与测试运行区交互优化
- 中英文语言文件补充相关文案与快捷操作标签
- 切换 LLM 点评 API 至 newapi.klarkxy.xyz,模型改为 minimax-m3
- 新增 .codegraph/.gitignore 忽略本地调试数据
This commit is contained in:
2026-07-08 20:56:17 +08:00
parent c4e0f104b4
commit 6c462655b9
7 changed files with 1236 additions and 419 deletions

View File

@@ -24,9 +24,10 @@ if(!isset($_GET['sid'])){
function is_valid($str2){
global $_SESSION,$OJ_NAME,$OJ_FRIENDLY_LEVEL;
if(isset($_SESSION[$OJ_NAME.'_'.'source_browser'])) return true;
return true; // 如果希望能让任何人都查看对比和RE,放开行首注释,并设定$OJ_SHOW_DIFF=true; if you fail to view diff , try remove the // at beginning of this line.
if($OJ_FRIENDLY_LEVEL>3) return true;
// 如果希望能让任何人都查看对比和RE,放开行首注释,并设定$OJ_SHOW_DIFF=true;
// return true;
$n = strlen($str2);
$str = str_split($str2);
$m = 1;
@@ -65,39 +66,190 @@ if(!empty($spj)&&$spj[0][0]==2 && $OJ_HIDE_RIGHT_ANSWER && !isset($_SESSION[$OJ_
$view_errors = "<h1>$MSG_MARK:$mark</h1><br>";
$ok = false;
}
/**
* 把 N 个相同测试点的 RE 折叠为 "1.in~5.in: Aborted (共 5 个测试点)"
*/
function dedup_runtimeinfo($text, &$dedup_count) {
$lines = explode("\n", $text);
$buckets = array(); // reason => [testcase, ...]
$other = array();
foreach ($lines as $line) {
$line = trim($line);
if ($line === "") continue;
// 形如 "data/1001/1.in:Aborted" 或 "1.in: Segmentation fault"
if (preg_match('/^(?:data\/\d+\/)?([^\s:]+(?:[.:][^\s:]+)*?):\s*(.+)$/', $line, $m)) {
$tc = $m[1];
$reason = trim($m[2]);
$buckets[$reason][] = $tc;
} else {
$other[] = $line;
}
}
$out = array();
foreach ($other as $o) $out[] = $o;
foreach ($buckets as $reason => $tcs) {
$count = count($tcs);
if ($count == 1) {
$out[] = $tcs[0] . ": " . $reason;
} else {
$first = $tcs[0];
$last = $tcs[$count - 1];
$range = ($first === $last) ? $first : ($first . "~" . $last);
$out[] = $range . ": " . $reason . " (共 " . $count . " 个测试点)";
$dedup_count += ($count - 1);
}
}
return implode("\n", $out);
}
$view_reinfo = "";
$view_reinfo_raw = ""; // 原始(管理员/有权限者看)
$view_reinfo_summary = ""; // 折叠后(普通用户看)
$view_reinfo_dedup_count = 0; // 折叠掉的测试点数
$verdict_color_class = "blue"; // semantic ui color: green/red/orange/yellow/grey
$result_icon = "info circle";
$verdict_tip = "";
$verdict_hints = array();
if($ok){
if($row['user_id']!=$_SESSION[$OJ_NAME.'_'.'user_id']){
$view_mail_link= "<a href='mail.php?to_user=".htmlentities($row['user_id'],ENT_QUOTES,"UTF-8")."&title=$MSG_SUBMIT $id'>Mail the auther</a>";
}
$sql = "SELECT `error` FROM `runtimeinfo` WHERE `solution_id`=?";
$result = pdo_query($sql,$id);
if(isset($result[0])){
$row = $result[0];
$view_reinfo = str_replace("\n\r","\n",$row['error']);
// $view_reinfo=htmlentities($view_reinfo,ENT_QUOTES,"UTF-8");
$rt = pdo_query($sql,$id);
$raw_error = "";
if(isset($rt[0])){
$raw_error = str_replace("\n\r","\n",$rt[0]['error']);
}
if(strpos($row['error'],"judge/")!==false&&!isset($_SESSION[$OJ_NAME."_administrator"])) $view_reinfo = "潜在的数组或指针越界,请检查代码。";
else if(strpos($row['error'],"php")!==false) $view_reinfo = "error2";
else if(strpos($row['error'],"PASS")!==false) $view_reinfo = "error3";
else if($OJ_SHOW_DIFF && $row && ($ok||$isRE) && ($OJ_TEST_RUN||is_valid($row['error'])||$ok)){
$view_reinfo = str_replace("\n\r","\n",$row['error']);
$view_reinfo = htmlspecialchars_decode($view_reinfo,ENT_QUOTES);
// $view_reinfo = htmlentities($view_reinfo,ENT_QUOTES,"UTF-8");
// $view_reinfo = "```\n" . $view_reinfo. "```\n";
$view_reinfo .="<br>$MSG_MARK:$mark";
// === 阶段 1基于 solution.result 给"主解释"(永远非空)===
$result_code = intval($row['result']);
$is_admin_session = isset($_SESSION[$OJ_NAME."_administrator"]);
$is_source_browser = isset($_SESSION[$OJ_NAME.'_'.'source_browser']);
// 颜色 + 图标
switch ($result_code) {
case 4: $verdict_color_class = "green"; $result_icon = "check circle"; break;
case 5: // PE
case 6: $verdict_color_class = "red"; $result_icon = "times circle"; break;
case 7: // TLE
case 8: // MLE
case 9: $verdict_color_class = "yellow";$result_icon = "hourglass end"; break;
case 10: $verdict_color_class = "red"; $result_icon = "bug"; break;
case 11: $verdict_color_class = "grey"; $result_icon = "code"; break;
case 13: $verdict_color_class = "blue"; $result_icon = "play circle"; break;
default: $verdict_color_class = "blue"; $result_icon = "info circle";
}
else{
$view_errors = $MSG_WARNING_ACCESS_DENIED;
//$view_reinfo = "出于数据保密原因,当前错误提示不可查看,如果希望能让任何人都查看对比和运行错误,请管理员配置\$OJ_SHOW_DIFF=true;<br>然后编辑本文件开放18行首注释令is_valid总是返回true。 <br>\n Sorry , not available (RE:".$isRE.",OJ_SHOW_DIFF:".$OJ_SHOW_DIFF.",TR:".$OJ_TEST_RUN.",valid:".is_valid($row['error']).")";
// 友好提示
switch ($result_code) {
case 4: // AC
$verdict_tip = "本次提交通过了所有测试点";
$verdict_hints[] = "注意时间/内存是否接近限制,考虑是否有更优的算法";
$verdict_hints[] = "可以继续挑战更高难度或查看其他人的标程";
break;
case 5: // PE
$verdict_tip = "输出与预期基本一致但格式不符Presentation Error";
$verdict_hints[] = "对照样例:每行末尾是否多/少换行";
$verdict_hints[] = "检查空格、大小写、标点符号";
$verdict_hints[] = "PE 通常是细微的格式问题,请逐字符对比";
break;
case 6: // WA
$verdict_tip = "输出与预期不符Wrong Answer";
$verdict_hints[] = "对照样例手动模拟程序执行过程";
$verdict_hints[] = "检查边界条件n=0、n=1、极值、空输入";
$verdict_hints[] = "注意 long long 溢出、int 范围";
break;
case 7: // TLE
$verdict_tip = $MSG_HINT_TLE;
$verdict_hints[] = "检查是否有死循环(循环变量未更新)";
$verdict_hints[] = "优化算法复杂度(如 O(n²) → O(n log n)";
$verdict_hints[] = "递归层数过深?改为迭代或加深栈";
break;
case 8: // MLE
$verdict_tip = $MSG_HINT_MLE;
$verdict_hints[] = "大数组开在函数内(栈)?改为全局或 new";
$verdict_hints[] = "Java: 减小内存使用或调整 JVM 参数";
$verdict_hints[] = "递归太深未释放,考虑显式清理";
break;
case 9: // OLE
$verdict_tip = $MSG_HINT_OLE;
$verdict_hints[] = "是否有死循环输出";
$verdict_hints[] = "调试用的 print/cout 是否忘记删除";
break;
case 10: // RE
$verdict_tip = "程序运行中发生错误Runtime Error";
$verdict_hints[] = "数组下标是否越界i<n 还是 i<=n";
$verdict_hints[] = "除以零、空指针、栈溢出";
$verdict_hints[] = "递归爆栈:转迭代或加尾递归";
break;
case 11: // CE
$verdict_tip = "编译错误Compile Error。请查看编译信息页。";
break;
case 13: // TR (test run)
$verdict_tip = "测试运行完成Test Run";
$verdict_hints[] = "你输入的测试数据已与程序输出对比";
break;
default:
$verdict_tip = "正在评判,请稍候刷新";
}
// === 阶段 2syscall vs 真正越界 的区分 ===
if ($result_code === 10) {
if (preg_match('/Forbidden system call.*CALLID:(\d+)/', $raw_error, $m)) {
$callid = $m[1];
$verdict_hints[] = sprintf("系统调用 ID=%d 被判题器禁止,常见于直接操作文件/网络/进程", $callid);
}
// Java / Python / Go 的细化提示
if (preg_match('/java\.lang\.(\w+Exception|\w+Error)/', $raw_error, $m2)) {
$verdict_hints[] = "Java 异常类型:" . $m2[1];
} elseif (preg_match('/Traceback \(most recent call last\)/', $raw_error)) {
$verdict_hints[] = "Python 抛出异常,下方 traceback 从下往上读就是异常源头";
} elseif (preg_match('/panic:.*?(index out of range|invalid memory address|nil pointer)/i', $raw_error)) {
$verdict_hints[] = "Go 运行时报错:通常是越界访问或 nil 指针";
}
}
// === 阶段 3原始错误处理syscall 提示 vs 占位符修复)===
$view_reinfo_raw = $raw_error;
if (trim($raw_error) === "") {
// TLE/MLE/OLE 时 runtimeinfo 通常为空
$view_reinfo_summary = "";
} else {
// 含 "PASS" 不再显示 "error3" 占位
// 含 "php" 不再显示 "error2" 占位(用 generic 提示)
if (strpos($raw_error, "php") !== false) {
$view_reinfo_summary = $MSG_RUNTIME_GENERIC_ERROR;
$view_reinfo_raw = $raw_error; // 管理员仍能看
}
// 含 "judge/" 区分:是否是 syscall 错误
elseif (strpos($raw_error, "judge/") !== false
&& strpos($raw_error, "CALLID") === false
&& strpos($raw_error, "Forbidden system call") === false) {
$view_reinfo_summary = "潜在的数组或指针越界,请检查代码。";
}
// 默认:原文 + 去重
else {
$view_reinfo_summary = $raw_error;
// 多测试点去重:按 "infile: error" 模式分组
$view_reinfo_summary = dedup_runtimeinfo($view_reinfo_summary, $view_reinfo_dedup_count);
}
}
$view_reinfo = $view_reinfo_summary;
// 管理员或 source_browser 永远看完整原文
$show_raw = $is_admin_session || $is_source_browser;
if ($show_raw) {
$view_reinfo = $view_reinfo_raw;
} else {
$view_reinfo = $view_reinfo_summary;
}
}
// 不是本人的提交,且不是 source_browser
else{
if($spj[0][0]!=2) $view_errors = $MSG_WARNING_ACCESS_DENIED;
require("template/".$OJ_TEMPLATE."/error.php");