77 lines
3.3 KiB
TypeScript
77 lines
3.3 KiB
TypeScript
import { CAPABILITY_IDS } from "@umt/contracts";
|
|
|
|
// 汇报格式的机器核对(AG-5 评测用):fake 模型评测与真实模型评测共用。
|
|
// 只查能机械判断的项;"结论在第一句"按开头的套话与是否含状态词近似判断。
|
|
|
|
export interface ReportLintIssue {
|
|
rule:
|
|
| "too_long"
|
|
| "uuid"
|
|
| "hash"
|
|
| "raw_bytes"
|
|
| "iso_time"
|
|
| "internal_field"
|
|
| "capability_id"
|
|
| "heading"
|
|
| "no_conclusion_first"
|
|
| "asks_user_for_id"
|
|
| "lecture";
|
|
detail: string;
|
|
}
|
|
|
|
export interface ReportLintOptions {
|
|
/** 最多几行(非空行);缺省 8 */
|
|
maxLines?: number;
|
|
}
|
|
|
|
const UUID = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i;
|
|
const HASH = /\b[0-9a-f]{40}(?:[0-9a-f]{24})?\b/i;
|
|
/** 7 位以上的裸数字(原始字节数、原始计数);年份、日期、带千分位的不算 */
|
|
const RAW_NUMBER = /(?<![\d.:\-/])\d{7,}(?![\d.:\-/])/;
|
|
const ISO_TIME = /\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
|
|
/** 内部字段名:snake_case、camelCase 与已知的内部词 */
|
|
const INTERNAL_WORDS =
|
|
/\b(?:adapter|healthStatus|health_status|mediaEntityId|slotId|planId|runId|taskId|downloadTaskId|sourceReleaseId|payloadHash|idempotency|capabilityId|invocationId)\b/;
|
|
const SNAKE = /\b[a-z]+_[a-z_]+\b/;
|
|
const CAMEL = /\b[a-z]+[A-Z][A-Za-z]+\b/;
|
|
/** 开头套话(结论没放第一句) */
|
|
const WEAK_OPENERS = /^(?:#|\*\*?检查结果|检查结果|结论[::]|以下|下面|好的|当然|首先|根据|经过|我(?:已|先|查)|让我)/;
|
|
const ASK_ID = /(?:请|麻烦)?(?:提供|给(?:我)?(?:一个)?)[^。\n]{0,12}(?:ID|hash|编号|哈希)/i;
|
|
const LECTURE = /(?:不等于|并不代表|不能证明|无证据|建议定期|值得注意的是|需要注意的是)/;
|
|
|
|
/** 允许出现在汇报里的英文词(技术词保留原文) */
|
|
const ALLOWED_TOKENS = new Set(["qBittorrent", "iPhone", "macOS"]);
|
|
|
|
/** 核对一段汇报;返回空数组 = 通过 */
|
|
export function lintReport(text: string, opts: ReportLintOptions = {}): ReportLintIssue[] {
|
|
const maxLines = opts.maxLines ?? 8;
|
|
const issues: ReportLintIssue[] = [];
|
|
const lines = text
|
|
.split("\n")
|
|
.map((l) => l.trim())
|
|
.filter((l) => l !== "" && !/^\|?\s*:?-{3,}/.test(l));
|
|
if (lines.length > maxLines) issues.push({ rule: "too_long", detail: `${lines.length} 行,超过 ${maxLines} 行` });
|
|
const find = (re: RegExp) => text.match(re)?.[0];
|
|
const hit = (rule: ReportLintIssue["rule"], re: RegExp) => {
|
|
const m = find(re);
|
|
if (m) issues.push({ rule, detail: m });
|
|
};
|
|
hit("uuid", UUID);
|
|
hit("hash", HASH);
|
|
hit("raw_bytes", RAW_NUMBER);
|
|
hit("iso_time", ISO_TIME);
|
|
hit("internal_field", INTERNAL_WORDS);
|
|
const snake = find(SNAKE);
|
|
if (snake) issues.push({ rule: "internal_field", detail: snake });
|
|
const camel = text.match(new RegExp(CAMEL, "g"))?.find((w) => !ALLOWED_TOKENS.has(w));
|
|
if (camel) issues.push({ rule: "internal_field", detail: camel });
|
|
const cap = CAPABILITY_IDS.find((id) => text.includes(id));
|
|
if (cap) issues.push({ rule: "capability_id", detail: cap });
|
|
if (/^#{1,6} /m.test(text)) issues.push({ rule: "heading", detail: "用了 Markdown 标题" });
|
|
const first = lines[0] ?? "";
|
|
if (!first || WEAK_OPENERS.test(first)) issues.push({ rule: "no_conclusion_first", detail: first.slice(0, 40) });
|
|
hit("asks_user_for_id", ASK_ID);
|
|
hit("lecture", LECTURE);
|
|
return issues;
|
|
}
|