L2文档三大板块重构+评分细则不公开化;系统对齐:一键三轮评审、受验者注册表映射、提交状态探测工具

- 文档重构为 考核目的(1.x)/提交要求(2.x,含成果物需求)/考核内容(3.x) 板块编号体系
- 删除公开评分细则(7维表/合格线/难度赋分/功能拆分),保留验收基准与提交要求
- 自选题取消事前登记,改由 AGENTS.md 记录核心内容;前端同步移除登记字段
- 新增 l2-participants 注册表服务与 teams-config L2 URL 自动映射
- 新增 startReviewRounds 一键N轮评审(自动续跑+快照聚合中位数),前端「重新评审×3」
- 新增 check-repos.mjs 提交状态探测(API四态判定:已提交/空仓/未创建/未授权)
- 文档措辞与实现对齐(AI辅助评审)、修复引用/残片/格式问题
This commit is contained in:
hangshuo652
2026-08-26 10:52:23 +08:00
parent 6a2419d4a4
commit 040808ef6b
10 changed files with 510 additions and 554 deletions
+172
View File
@@ -0,0 +1,172 @@
/**
* 参赛/受验者仓库提交状态探测工具
* 用法:node server/scripts/check-repos.mjs [赛道一|赛道二|L2考核|all]
*
* 通过 Gitea API 快速判定四种状态(无需克隆):
* SUBMITTED 仓库存在且非空(已提交)
* EMPTY 仓库存在但为空(建仓未推送)
* NOT_CREATED 仓库不存在(或全局评审账号无权限且无备用凭据可区分)
* NO_ACCESS 仓库存在(队伍自身凭据可见)但评审账号未被授权协作者
* AUTH_FAIL 队伍自身凭据失效
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..', '..');
// ---------- 加载 .env ----------
function loadEnv() {
const p = path.join(ROOT, 'server', '.env');
const out = {};
for (const line of fs.readFileSync(p, 'utf8').split(/\r?\n/)) {
const m = line.match(/^([A-Z_]+)=(.*)$/);
if (m && !line.trim().startsWith('#')) out[m[1]] = m[2].trim();
}
return out;
}
const env = loadEnv();
const G_USER = env.GITEA_USERNAME || '';
const G_TOKEN = env.GITEA_TOKEN || '';
if (!G_USER || !G_TOKEN) {
console.error('缺少 GITEA_USERNAME / GITEA_TOKENserver/.env');
process.exit(1);
}
const filterArg = process.argv[2] || 'all';
// ---------- 组装探测目标 ----------
const targets = [];
if (filterArg === 'all' || filterArg === '赛道一' || filterArg === '赛道二') {
const teams = JSON.parse(fs.readFileSync(path.join(ROOT, 'config', 'teams.json'), 'utf8')).teams || [];
for (const t of teams) {
if (filterArg !== 'all' && t.track !== filterArg) continue;
targets.push({
kind: t.track,
label: `${t.team || t.dept || ''}${t.leader ? '/' + t.leader : ''}`,
user: t.gittea.user,
repo: t.gittea.repo,
// 备用凭据:队伍自己的 token/密码(用于区分"未创建"与"未授权"
altAuth: t.gittea.token
? { type: 'token', value: t.gittea.token }
: (t.gittea.password ? { type: 'basic', value: t.gittea.password } : null),
});
}
}
if (filterArg === 'all' || filterArg === 'L2考核') {
try {
const l2 = JSON.parse(fs.readFileSync(path.join(ROOT, 'config', 'l2-participants.json'), 'utf8'));
for (const p of l2.participants || []) {
targets.push({
kind: 'L2考核',
label: `${p.no}${p.name ? '/' + p.name : ''}`,
user: p.gitteaUser,
repo: p.repo || 'L2-assessment',
altAuth: null, // 自注册模式无个人 token404 无法与"未授权"区分
});
}
} catch { /* 注册表不存在则跳过 */ }
}
if (targets.length === 0) {
console.error('没有匹配的探测目标。用法: node server/scripts/check-repos.mjs [赛道一|赛道二|L2考核|all]');
process.exit(1);
}
// ---------- 探测 ----------
async function probeRepo(base, user, repo, auth) {
const url = `${base.replace(/\/+$/, '')}/api/v1/repos/${encodeURIComponent(user)}/${encodeURIComponent(repo)}`;
const headers = {};
if (auth?.type === 'token') headers.Authorization = `token ${auth.value}`;
else if (auth?.type === 'basic') headers.Authorization = 'Basic ' + Buffer.from(`${G_USER}:${auth.value}`).toString('base64');
else headers.Authorization = 'Basic ' + Buffer.from(`${G_USER}:${G_TOKEN}`).toString('base64');
for (let attempt = 1; attempt <= 2; attempt++) {
try {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 12000);
const res = await fetch(url, { headers, signal: ctrl.signal });
clearTimeout(timer);
if (res.status === 200) {
const j = await res.json();
return { code: 'ok', empty: !!j.empty, defaultBranch: j.default_branch || '', updatedAt: j.updated_at || '' };
}
if (res.status === 404) return { code: 'miss' };
if (res.status === 401 || res.status === 403) return { code: 'denied' };
return { code: 'http', detail: res.status };
} catch (e) {
if (attempt === 2) return { code: 'net', detail: (e.message || '').slice(0, 60) };
await new Promise(r => setTimeout(r, 800));
}
}
}
async function classify(t, base) {
const g = await probeRepo(base, t.user, t.repo, null);
if (g.code === 'ok') {
if (g.empty) return { state: 'EMPTY', icon: '🟡', text: '空仓库(已建仓,未推送任何内容)' };
const branchWarn = g.defaultBranch && g.defaultBranch !== 'main' ? `,注意默认分支=${g.defaultBranch}` : '';
return { state: 'SUBMITTED', icon: '✅', text: `已提交(${g.defaultBranch}${branchWarn},更新 ${String(g.updatedAt).slice(0, 10)}` };
}
if (g.code === 'denied') return { state: 'NO_ACCESS', icon: '⚠️ ', text: '评审账号无权限(403' };
if (g.code === 'miss') {
if (t.altAuth) {
const a = await probeRepo(base, t.user, t.repo, t.altAuth);
if (a.code === 'ok') {
const extra = a.empty ? '(且为空仓库)' : '';
return { state: 'NO_ACCESS', icon: '⚠️ ', text: `仓库存在${extra},但评审账号未被授权协作者` };
}
if (a.code === 'miss') return { state: 'NOT_CREATED', icon: '🔴', text: '仓库未创建(两队凭据均确认 404)' };
if (a.code === 'denied') return { state: 'AUTH_FAIL', icon: '🔴', text: '队伍自身凭据失效(token/密码不可用)' };
return { state: 'ERR', icon: '❓', text: '队伍凭据探测异常' };
}
return { state: 'NOT_CREATED', icon: '🔴', text: '未创建 或 未授权(无备用凭据,无法区分)' };
}
return { state: 'ERR', icon: '❓', text: `探测失败 ${g.detail || ''}` };
}
// 小并发池
async function pool(items, worker, size = 6) {
const results = new Array(items.length);
let i = 0;
async function run() {
while (i < items.length) {
const idx = i++;
results[idx] = await worker(items[idx]);
}
}
await Promise.all(Array.from({ length: Math.min(size, items.length) }, run));
return results;
}
// ---------- 主流程 ----------
const l2cfg = (() => { try { return JSON.parse(fs.readFileSync(path.join(ROOT, 'config', 'l2-participants.json'), 'utf8')); } catch { return {}; } })();
const BASE = l2cfg.gitteaUrl || 'https://gittea.dev';
console.log(`探测目标 ${targets.length} 个(base=${BASE}, 评审账号=${G_USER}`);
console.log('='.repeat(100));
const results = await pool(targets, async (t) => ({ t, r: await classify(t, BASE) }));
const ORDER = ['SUBMITTED', 'EMPTY', 'NO_ACCESS', 'NOT_CREATED', 'AUTH_FAIL', 'ERR'];
const STATE_TEXT = {
SUBMITTED: '已提交', EMPTY: '空仓库', NO_ACCESS: '未授权',
NOT_CREATED: '未创建', AUTH_FAIL: '凭据失效', ERR: '探测异常',
};
const counts = {};
for (const { t, r } of results) {
counts[r.state] = (counts[r.state] || 0) + 1;
console.log(`[${t.kind}] ${t.label.padEnd(24)} ${r.icon} ${STATE_TEXT[r.state].padEnd(6)} ${r.text}`);
}
console.log('='.repeat(100));
console.log('汇总:');
for (const s of ORDER) {
if (counts[s]) console.log(` ${STATE_TEXT[s]}: ${counts[s]}`);
}
const notDone = (counts.EMPTY || 0) + (counts.NO_ACCESS || 0) + (counts.NOT_CREATED || 0) + (counts.AUTH_FAIL || 0);
console.log(`\n结论:已提交 ${counts.SUBMITTED || 0} / 总数 ${results.length};未完成 ${notDone} 个(见上方明细)。`);