mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 04:31:58 +00:00
反方审稿审批卡说清「根本没反查」和「到底降不降档」,运行类别取不到时不说出具(前端)
- 服务端给了 unreviewed(核查模型不可用、触顶、数据源不在了等没能反查的句数)就另起一行「N 句没能反查,已记为无法判断」:这几句混在「无法判断」里,分不出是查了数据不够还是一条都没查 - 「出具会降档」只在正式运行、有被推翻的结论、服务端的 degrades 为真时写(老服务端没给时照旧按「不是只标注」算);按降档处置却不在带出具契约的成果节点上游的,说一句为什么不影响出具档位,不再让审核人以为会降档 - 运行类别取不到时一句和出具有关的话都不说:以前写「暂时无法确认运行类别;如为正式运行……会降档」外加重试,卡顶却按审批自带的运行类别写着正式运行,两处打架。窗口重新拿到焦点时照旧再取,取到正式运行后降档提示才出现 - 理由文档的画法(RationaleDoc)导出给双盲复算的审批卡用:没给跳转时有出处的数字不是按钮,只画实线、挂查询编号、读屏念出处 - 检查器帮助里「结论被推翻时」的选项名照下拉写(出具降档 / 只标注 / 报错并停止运行),和服务端报错里点的名字一致 - 预览用例补没能反查、不在出具路径上两种;check-challenge 补这两段、改写取不到运行类别那一段,并新增一节逐项比对 engine/labels.py 的 CHALLENGE_LABEL、CHALLENGE_ON_REFUTED_LABEL 和检查器的字段名、下拉选项
This commit is contained in:
@@ -12,7 +12,9 @@
|
||||
"exploreFlag": "run-chal-explore-flag",
|
||||
"runfail": "run-chal-runfail",
|
||||
"dup": "run-chal-dup",
|
||||
"norecord": "run-chal-norecord"
|
||||
"norecord": "run-chal-norecord",
|
||||
"unreviewed": "run-chal-unreviewed",
|
||||
"offpath": "run-chal-offpath"
|
||||
},
|
||||
"report": {
|
||||
"node_id": "write",
|
||||
|
||||
@@ -22,7 +22,9 @@ import fx from './challenge.json'
|
||||
* - ?syn=challenge-approval-dense:同第一个,摆在 360px 的右栏里
|
||||
* - ?syn=challenge-approval-clear / -clear-flag:正式运行,没有被推翻的结论(降档 / 只标注),两种都不写降档、不写只标注
|
||||
* - ?syn=challenge-approval-explore-flag:探索运行配只标注,同样不说任何和出具有关的话
|
||||
* - ?syn=challenge-approval-runfail:运行类别取不到(检查脚本先答 500),说明并可重试
|
||||
* - ?syn=challenge-approval-runfail:运行类别取不到(检查脚本先答 500),不说任何和出具有关的话
|
||||
* - ?syn=challenge-approval-unreviewed:正式运行,四句里有两句没能反查(unreviewed),被推翻的会降档(degrades)
|
||||
* - ?syn=challenge-approval-offpath:正式运行,按降档处置,但反方审稿不在带出具契约的成果节点上游(degrades 为假)
|
||||
* - ?syn=challenge-approval-dup:同 many,工件里补出来的一句和条目里的一句原文相同、unit 不同(工件由检查脚本合成)
|
||||
* - ?syn=challenge-approval-norecord:同 many,审稿记录没存上(artifact 为 null)
|
||||
*
|
||||
@@ -57,7 +59,9 @@ function Card({ approval, name, dense = false }: { approval: Approval; name: str
|
||||
}
|
||||
|
||||
const RUNS = fx.runs
|
||||
const withChallenge = (c: Partial<ApprovalChallenge>): ApprovalReport => ({ ...report, challenge: { ...challenge, ...c } })
|
||||
/** 期 4 起服务端另带的两项:unreviewed(没能反查的句数)、degrades(被推翻的会不会让出具降档) */
|
||||
type Challenge = ApprovalChallenge & { unreviewed?: number; degrades?: boolean }
|
||||
const withChallenge = (c: Partial<Challenge>): ApprovalReport => ({ ...report, challenge: { ...challenge, ...c } as ApprovalChallenge })
|
||||
const many = fx.many as unknown as ApprovalChallenge
|
||||
/** 没有被推翻的结论:同第一个用例去掉被推翻的那句,计数跟着改 */
|
||||
const clear: Partial<ApprovalChallenge> = {
|
||||
@@ -207,6 +211,14 @@ export const cases: Record<string, () => JSX.Element> = {
|
||||
<Card name="challenge-approval-norecord"
|
||||
approval={approvalOf('ap-chal-13', RUNS.norecord, 'formal', { ...report, challenge: { ...many, artifact: null } })} />
|
||||
),
|
||||
'challenge-approval-unreviewed': () => (
|
||||
<Card name="challenge-approval-unreviewed"
|
||||
approval={approvalOf('ap-chal-14', RUNS.unreviewed, 'formal', withChallenge({ unreviewed: 2, degrades: true }))} />
|
||||
),
|
||||
'challenge-approval-offpath': () => (
|
||||
<Card name="challenge-approval-offpath"
|
||||
approval={approvalOf('ap-chal-15', RUNS.offpath, 'formal', withChallenge({ on_refuted: 'degrade', degrades: false, unreviewed: 0 }))} />
|
||||
),
|
||||
'challenge-inspector': () => <InspectorCase name="challenge-inspector" workflow={WORKFLOW} />,
|
||||
'challenge-inspector-chain': () => <InspectorCase name="challenge-inspector-chain" workflow={CHAIN} />,
|
||||
}
|
||||
|
||||
@@ -3594,10 +3594,10 @@ export const DUAL_CHECK_TEXT = {
|
||||
}
|
||||
|
||||
/**
|
||||
* 反方审稿节点:节点库说明、检查器、节点卡片摘要,以及审批卡上的反方审稿(run/ChallengeSummary.tsx)。
|
||||
* 四种判断全站只有这一种叫法:经受住、被削弱、被推翻、无法判断。判断由反方模型给出,不是系统核对。
|
||||
* 「出具会降档」只在正式运行、有被推翻的结论、按降档处理时才说;探索运行不出具,不说任何和出具有关的话;
|
||||
* 运行类别取不到时照实说取不到
|
||||
* 反方审稿节点:节点库说明、检查器、节点卡片摘要、审批卡上的反方审稿(run/ChallengeSummary.tsx),以及时间线上反方
|
||||
* 审稿的日志(log)。四种判断全站只有这一种叫法:经受住、被削弱、被推翻、无法判断。判断由反方模型给出,不是系统核对。
|
||||
* 「出具会降档」只在正式运行、有被推翻的结论、被推翻的真会让出具降档(服务端的 degrades;老服务端没给时按「没写只标注」
|
||||
* 算)时才说;探索运行不出具、运行类别取不到时,不说任何和出具有关的话
|
||||
*/
|
||||
export const CHALLENGE_TEXT = {
|
||||
description: '从反方立场复核上游报告的关键结论:另一个模型只读查库,逐句判断经受住、被削弱、被推翻还是无法判断',
|
||||
@@ -3628,8 +3628,9 @@ export const CHALLENGE_TEXT = {
|
||||
max_claims: '每份报告最多挑几句关键结论来审。留空则使用「设置 → 偏好设置 → 核查」中的值',
|
||||
max_queries: '复核每句结论时最多执行几次查询。留空则使用「设置 → 偏好设置 → 核查」中的值',
|
||||
timeout_s: '复核每句结论的时长上限,到时按已查到的结果给出判断。留空则使用「设置 → 偏好设置 → 核查」中的值',
|
||||
on_refuted: '降档:被推翻的结论计入出具缺口,正式运行降档出具;只标注:在审批卡和运行记录中标出,不影响出具;'
|
||||
+ '报错:本节点失败,运行停止',
|
||||
// 选项名照下拉里的写(「出具降档」「只标注」「报错并停止运行」),和服务端报错里点的选项名一致
|
||||
on_refuted: '出具降档:被推翻的结论计入出具缺口,正式运行降档出具;只标注:在审批卡和运行记录中标出,不影响出具;'
|
||||
+ '报错并停止运行:本节点失败,运行停止',
|
||||
},
|
||||
onRefuted: { degrade: '出具降档(默认)', flag: '只标注', fail: '报错并停止运行' },
|
||||
unknownValue: (v: string) => `${v}:无法识别的值,请从上方选项中选择`,
|
||||
@@ -3678,8 +3679,11 @@ export const CHALLENGE_TEXT = {
|
||||
degrade: '出具会降档',
|
||||
degradeHint: '被推翻的结论计入出具缺口,本次正式运行将降档出具',
|
||||
flagNote: '按节点设置,被推翻的结论只做标注,不影响出具档位',
|
||||
/** 运行类别没取到:不知道是不是正式运行,照实说,并写出正式运行时的后果(旁边给重试) */
|
||||
runClassFailed: '暂时无法确认运行类别;如为正式运行,被推翻的结论会使出具降档',
|
||||
/** 按降档处置,但节点不在带出具契约的成果节点上游(服务端的 degrades 为假):缺口进不了出具 */
|
||||
degradeOff: '本节点不在带出具契约的成果节点上游,被推翻的结论不影响出具档位',
|
||||
/** 服务端的 unreviewed:核查模型不可用、触顶、数据源不在了等,这几句根本没查成,只是记为无法判断 */
|
||||
unreviewed: (n: number) => `${formatNumber(n)} 句没能反查,已记为无法判断`,
|
||||
unreviewedHint: '核查模型不可用、达到上限或数据源已停用时,这几句没有真正查询数据,原因见运行时间线',
|
||||
/** 审批里没带工件(服务端保存审稿记录失败):每句都看不了理由和查询,整块说一次 */
|
||||
noRecord: '反方审稿记录未保存,无法查看理由和查询',
|
||||
more: (n: number) => `另有 ${formatNumber(n)} 句未列出`,
|
||||
@@ -3698,6 +3702,8 @@ export const CHALLENGE_TEXT = {
|
||||
truncated: '结果已截断,数据库中还有更多数据',
|
||||
/** 理由里有出处的数字:读屏听到的是「数字,出处:查询 Q2 · 第 1 行 · amount」,点一下跳到那次查询 */
|
||||
cited: (text: string, source: string) => `${text},出处:${source},跳到这次查询`,
|
||||
/** 同上,没有可跳的查询时(双盲复算的理由)只念出处 */
|
||||
citedPlain: (source: string) => `(出处:${source})`,
|
||||
|
||||
// ---- 时间线(run/decode.ts explainLog):按日志的 code 和结构化字段说,不从原话里认
|
||||
log: {
|
||||
|
||||
@@ -17,16 +17,24 @@ export interface ChallengeSummaryProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批卡里反方审稿的摘要,紧跟在报告核对概况(ApprovalReports)后面:一行概况(四种判断各几句),逐句列出结论原文和判断,
|
||||
* 点开一句再按 challenge_artifact 取完整内容,看反方的理由和它执行过的查询。审稿记录没存上(没有工件)时只列判断、
|
||||
* 不能点开,整块说一次。
|
||||
* 审批卡里反方审稿的摘要,紧跟在报告核对概况(ApprovalReports)后面:一行概况(四种判断各几句),有句子根本没反查成时
|
||||
* 另起一行说「N 句没能反查」,逐句列出结论原文和判断,点开一句再按 challenge_artifact 取完整内容,看反方的理由和它执行
|
||||
* 过的查询。审稿记录没存上(没有工件)时只列判断、不能点开,整块说一次。
|
||||
*
|
||||
* 判断是反方模型给的,不是系统核对:颜色照结论句裁判的那一套,「经受住」也不用确定性的绿。
|
||||
* 「出具会降档」只在正式运行、有被推翻的结论、按降档处理时才写——只标注的不写降档,探索运行不出具,和出具有关的话一句不说。
|
||||
* 运行类别不由审批卡传进来,按运行 id 从证据 store 取(loadRunClass),取到之前不说;取不到时照实说取不到,可重试。
|
||||
* 「出具会降档」只在正式运行、有被推翻的结论、被推翻的真会让出具降档时才写:服务端给了 degrades 就照它(按降档处置、又接
|
||||
* 在带出具契约的成果节点上游才为真),老服务端没给时按「没写只标注」算。只标注的写一句不影响出具,按降档处置却不在出具
|
||||
* 路径上的写一句为什么不影响;探索运行不出具,和出具有关的话一句不说。运行类别不由审批卡传进来,按运行 id 从证据 store
|
||||
* 取(loadRunClass),取到之前、取不到时都不说和出具有关的话(窗口重新拿到焦点时再取)。
|
||||
*
|
||||
* 审的报告都没有经过反方审稿时什么都不画,也不发任何请求,卡片和以前一样。
|
||||
*/
|
||||
/**
|
||||
* 审批卡上的反方审稿概况,约定之外服务端另带两项(期 4):unreviewed 没能反查的句数,degrades 被推翻的会不会让出具降档。
|
||||
* 老服务端都不给
|
||||
*/
|
||||
type Challenge = ApprovalChallenge & { unreviewed?: number; degrades?: boolean }
|
||||
|
||||
export function ChallengeSummary({ reports, runId }: ChallengeSummaryProps) {
|
||||
const reviewed = useMemo(() => reports.flatMap((r) => {
|
||||
const challenge = challengeOf(r)
|
||||
@@ -37,7 +45,7 @@ export function ChallengeSummary({ reports, runId }: ChallengeSummaryProps) {
|
||||
}
|
||||
|
||||
function ChallengeList({ reviewed, runId }: {
|
||||
reviewed: { report: ApprovalReport; challenge: ApprovalChallenge }[]; runId: string
|
||||
reviewed: { report: ApprovalReport; challenge: Challenge }[]; runId: string
|
||||
}) {
|
||||
const run = useRunClass(runId)
|
||||
return (
|
||||
@@ -74,7 +82,7 @@ const count = (v: unknown) => (typeof v === 'number' && Number.isFinite(v) && v
|
||||
* payload.reports[i].challenge 认不认得:要么有计数、要么有条目。计数缺了按条目数(条目最多 10 条,计数才是全部);
|
||||
* 条目缺了 unit 的留空(行的键带序号,区分得开),不补假的:补出来的键会被当成真的 unit 去和工件里的句子比
|
||||
*/
|
||||
function challengeOf(report: ApprovalReport): ApprovalChallenge | null {
|
||||
function challengeOf(report: ApprovalReport): Challenge | null {
|
||||
const raw = report.challenge as unknown
|
||||
if (!raw || typeof raw !== 'object') return null
|
||||
const c = raw as Record<string, unknown>
|
||||
@@ -86,41 +94,32 @@ function challengeOf(report: ApprovalReport): ApprovalChallenge | null {
|
||||
const counts = Object.fromEntries(VERDICTS.map((v) => [v, given
|
||||
? count(given[v]) : items.filter((it) => it.verdict === v).length])) as ChallengeCounts
|
||||
if (!given && !items.length) return null
|
||||
const unreviewed = typeof c.unreviewed === 'number' && Number.isFinite(c.unreviewed) ? count(c.unreviewed) : undefined
|
||||
return {
|
||||
counts, items, artifact: typeof c.artifact === 'string' && c.artifact ? c.artifact : null,
|
||||
on_refuted: typeof c.on_refuted === 'string' ? c.on_refuted : undefined,
|
||||
...(unreviewed !== undefined ? { unreviewed } : {}),
|
||||
...(typeof c.degrades === 'boolean' ? { degrades: c.degrades } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
interface RunClass {
|
||||
runClass: 'formal' | 'exploratory' | null
|
||||
/** 取失败了、还没取到(重取期间也算:那一行和焦点所在的重试按钮不跟着闪掉) */
|
||||
failed: boolean
|
||||
retry: () => void
|
||||
}
|
||||
|
||||
function useRunClass(runId: string): RunClass {
|
||||
/**
|
||||
* 这次运行的类别:取到之前、取不到时为 null——不知道是不是正式运行,就一句和出具有关的话都不说(卡顶照审批自带的运行
|
||||
* 类别写着,这里再猜一个只会和它对不上)。取失败了不就此作罢:窗口重新拿到焦点时再取一次(store 对出错的槽会重发),
|
||||
* 取到了正式运行,降档的提示随之出现
|
||||
*/
|
||||
function useRunClass(runId: string): 'formal' | 'exploratory' | null {
|
||||
const slot = useEvidence((s) => (runId ? s.runClasses[runId] : undefined))
|
||||
const load = useEvidence((s) => s.loadRunClass)
|
||||
useEffect(() => { if (runId) void load(runId) }, [runId, load])
|
||||
const status = slot?.status
|
||||
const [hadError, setHadError] = useState(false)
|
||||
useEffect(() => {
|
||||
if (status === 'error') setHadError(true)
|
||||
else if (status === 'ok') setHadError(false)
|
||||
}, [status])
|
||||
const failed = status === 'error' || (hadError && status === 'loading')
|
||||
// 取失败了不就此作罢:窗口重新拿到焦点时再取一次(store 对出错的槽会重发)。只取一次就放弃的话,一次网络抖动
|
||||
// 会让「出具会降档」一直不出现,而卡顶按审批自带的运行类别照样写着正式运行
|
||||
useEffect(() => {
|
||||
if (status !== 'error' || !runId) return
|
||||
const again = () => { void load(runId) }
|
||||
window.addEventListener('focus', again)
|
||||
return () => window.removeEventListener('focus', again)
|
||||
}, [status, runId, load])
|
||||
const retry = useCallback(() => { if (runId) void load(runId) }, [runId, load])
|
||||
const runClass = status === 'ok' && (slot?.data === 'formal' || slot?.data === 'exploratory') ? slot.data : null
|
||||
return { runClass, failed, retry }
|
||||
return status === 'ok' && (slot?.data === 'formal' || slot?.data === 'exploratory') ? slot.data : null
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 取工件
|
||||
@@ -213,7 +212,7 @@ const focusLost = () => !document.activeElement || document.activeElement === do
|
||||
// ---------------------------------------------------------------- 卡片
|
||||
|
||||
function ChallengeBlock({ report, challenge, run }: {
|
||||
report: ApprovalReport; challenge: ApprovalChallenge; run: RunClass
|
||||
report: ApprovalReport; challenge: Challenge; run: 'formal' | 'exploratory' | null
|
||||
}) {
|
||||
const [open, setOpen] = useState<Set<string>>(() => new Set())
|
||||
const [all, setAll] = useState(false)
|
||||
@@ -222,9 +221,12 @@ function ChallengeBlock({ report, challenge, run }: {
|
||||
const total = VERDICTS.reduce((n, v) => n + counts[v], 0)
|
||||
// 审批里没带工件:服务端保存审稿记录失败了。每句都看不了理由和查询,不画成能点开的样子,整块说一次
|
||||
const noRecord = !challenge.artifact
|
||||
const refuted = counts.refuted > 0 && run.runClass === 'formal'
|
||||
const refuted = counts.refuted > 0 && run === 'formal'
|
||||
// 没给 on_refuted(老服务端)按降档;报错的节点到不了审批,不会是 fail
|
||||
const flagged = challenge.on_refuted === 'flag'
|
||||
// 被推翻的真会让出具降档:服务端给了就照它,老服务端没给时按「不是只标注」算
|
||||
const degrades = challenge.degrades ?? !flagged
|
||||
const unreviewed = challenge.unreviewed ?? 0
|
||||
// 条目之外的几句(条目最多 10 条):要看全部时从工件里补上,排在后面,顺序照工件
|
||||
const extra = all && detail.data ? detail.data.claims.filter((c) => !items.some((it) => sameClaim(c, it))) : []
|
||||
const rows: ChallengeItem[] = [...items, ...extra]
|
||||
@@ -256,24 +258,13 @@ function ChallengeBlock({ report, challenge, run }: {
|
||||
const showAll = () => { focusMore.current = true; setAll(true) }
|
||||
const retryAll = () => { focusMore.current = true; detail.retry?.() }
|
||||
|
||||
// 运行类别重取成功后,「暂时无法确认」那一行(连同焦点所在的重试按钮)换成降档说明:焦点接到概况那一行上
|
||||
const countsRef = useRef<HTMLDivElement>(null)
|
||||
const runRetried = useRef(false)
|
||||
useEffect(() => {
|
||||
if (run.failed || !runRetried.current) return
|
||||
runRetried.current = false
|
||||
if (focusLost()) countsRef.current?.focus()
|
||||
}, [run.failed])
|
||||
const retryRun = () => { runRetried.current = true; run.retry() }
|
||||
|
||||
return (
|
||||
<div ref={blockRef} className="mb-2 rounded border bg-bg p-2" data-challenge={report.node_id}>
|
||||
<div className="mb-1 flex items-center gap-1 text-2xs font-medium text-faint">
|
||||
<ShieldAlert size={11} aria-hidden /> {CHALLENGE_TEXT.head(report.label || report.node_id)}
|
||||
</div>
|
||||
{total > 0 || items.length ? (
|
||||
<div ref={countsRef} tabIndex={-1} className={`tnum flex flex-wrap items-center gap-x-1.5 gap-y-1 text-2xs ${FOCUS_RING}`}
|
||||
data-challenge-counts="">
|
||||
<div className="tnum flex flex-wrap items-center gap-x-1.5 gap-y-1 text-2xs" data-challenge-counts="">
|
||||
{VERDICTS.map((v, i) => (
|
||||
<Fragment key={v}>
|
||||
{i > 0 && <span className="text-faint" aria-hidden>·</span>}
|
||||
@@ -282,7 +273,7 @@ function ChallengeBlock({ report, challenge, run }: {
|
||||
</span>
|
||||
</Fragment>
|
||||
))}
|
||||
{refuted && !flagged && (
|
||||
{refuted && !flagged && degrades && (
|
||||
<span className="chip shrink-0 font-semibold" data-challenge-degrade=""
|
||||
style={{ color: 'var(--st-failed)', borderColor: 'var(--st-failed)' }}>
|
||||
<AlertTriangle size={11} aria-hidden /> {CHALLENGE_TEXT.degrade}
|
||||
@@ -292,20 +283,23 @@ function ChallengeBlock({ report, challenge, run }: {
|
||||
) : (
|
||||
<div className="text-2xs text-dim" data-challenge-empty="">{CHALLENGE_TEXT.none}</div>
|
||||
)}
|
||||
{/* 根本没反查成的几句:「无法判断」里分不出查了数据不够和一条都没查,单独说一次 */}
|
||||
{unreviewed > 0 && (
|
||||
<p className="mt-0.5 text-2xs leading-relaxed" data-challenge-unreviewed={unreviewed}>
|
||||
<span className="font-medium" style={{ color: 'var(--st-waiting)' }}>{CHALLENGE_TEXT.unreviewed(unreviewed)}</span>
|
||||
<span className="text-dim">。{CHALLENGE_TEXT.unreviewedHint}</span>
|
||||
</p>
|
||||
)}
|
||||
{/* 降档的原因常显,不放在悬停提示里:触屏、键盘用户也得看得到为什么、会怎样(只标注那一档同样常显一行) */}
|
||||
{refuted && !flagged && (
|
||||
{refuted && !flagged && degrades && (
|
||||
<div className="mt-0.5 text-2xs leading-relaxed text-dim" data-challenge-degrade-note="">{CHALLENGE_TEXT.degradeHint}</div>
|
||||
)}
|
||||
{refuted && !flagged && !degrades && (
|
||||
<div className="mt-0.5 text-2xs leading-relaxed text-dim" data-challenge-degrade-off="">{CHALLENGE_TEXT.degradeOff}</div>
|
||||
)}
|
||||
{refuted && flagged && (
|
||||
<div className="mt-0.5 text-2xs leading-relaxed text-dim" data-challenge-flag="">{CHALLENGE_TEXT.flagNote}</div>
|
||||
)}
|
||||
{/* 运行类别取不到:不知道是不是正式运行,不能装作没事(卡顶按审批自带的运行类别照样写着正式运行),照实说并可重试 */}
|
||||
{counts.refuted > 0 && !flagged && run.failed && (
|
||||
<div className="mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-2xs leading-relaxed" data-challenge-runclass="failed">
|
||||
<span style={{ color: 'var(--st-waiting)' }}>{CHALLENGE_TEXT.runClassFailed}</span>
|
||||
<button type="button" className="btn btn-xs" onClick={retryRun}>{CHALLENGE_TEXT.retry}</button>
|
||||
</div>
|
||||
)}
|
||||
{rows.length > 0 && (
|
||||
<ul className="mt-1.5 space-y-0.5" data-challenge-items="">
|
||||
{rows.map((item, i) => {
|
||||
@@ -525,7 +519,11 @@ function Rationale({ claim, onJump }: { claim: ChallengeClaim; onJump: (alias: s
|
||||
/** 列表项从哪一个片段开始:同 EvidenceDoc(行首的「- 」「1. 」是结构片段) */
|
||||
const ITEM_START = /(^|\n)[ \t]*([-*+]|\d+[.)])[ \t]+$/
|
||||
|
||||
function RationaleDoc({ doc, onJump }: { doc: EvidenceDocData; onJump: (alias: string) => void }) {
|
||||
/**
|
||||
* 核查 agent 的理由文档(evidence.compose_doc 组装的,数字带出处、裸数字被标出)。反方审稿、双盲复算的审批卡共用:
|
||||
* onJump 给了的(反方审稿,旁边列着它的查询)有出处的数字是按钮,点一下跳到那次查询;没给的(双盲复算)只画出处
|
||||
*/
|
||||
export function RationaleDoc({ doc, onJump }: { doc: EvidenceDocData; onJump?: (alias: string) => void }) {
|
||||
return (
|
||||
<div className="min-w-0 space-y-1.5 text-[11.5px] leading-relaxed" data-challenge-doc="">
|
||||
{doc.blocks.map((block, b) => <RationaleBlock key={block.id ?? b} block={block} doc={doc} onJump={onJump} />)}
|
||||
@@ -534,7 +532,7 @@ function RationaleDoc({ doc, onJump }: { doc: EvidenceDocData; onJump: (alias: s
|
||||
}
|
||||
|
||||
/** 一块:外观借 Markdown 的 BlockView,和报告文档、普通答案长得一样(块的分法同 EvidenceDoc 的 DocBlock) */
|
||||
function RationaleBlock({ block, doc, onJump }: { block: EvidenceBlock; doc: EvidenceDocData; onJump: (alias: string) => void }) {
|
||||
function RationaleBlock({ block, doc, onJump }: { block: EvidenceBlock; doc: EvidenceDocData; onJump?: (alias: string) => void }) {
|
||||
const units = block.units ?? []
|
||||
const unit = (u: EvidenceUnit, code = false) => <RationaleUnit key={u.id} unit={u} doc={doc} code={code} onJump={onJump} />
|
||||
switch (block.type) {
|
||||
@@ -605,11 +603,12 @@ function cellAlias(seg: EvidenceSegment, doc: EvidenceDocData): string | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* 一句话:文字按行内 Markdown 渲染(整句算一次样式,同 EvidenceDoc 的 UnitView)。有出处的数字是按钮,后面挂查询编号;
|
||||
* 裸数字、疑似不存在的名称画点状线,句末挂「无证据」「疑似不存在的名称」。别的片段(表名、字段名的链接)照普通文字
|
||||
* 一句话:文字按行内 Markdown 渲染(整句算一次样式,同 EvidenceDoc 的 UnitView)。有出处的数字画实线、后面挂查询编号,
|
||||
* 给了 onJump 时是按钮;裸数字、疑似不存在的名称画点状线,句末挂「无证据」「疑似不存在的名称」。别的片段(表名、字段名的
|
||||
* 链接)照普通文字
|
||||
*/
|
||||
function RationaleUnit({ unit, doc, code, onJump }: {
|
||||
unit: EvidenceUnit; doc: EvidenceDocData; code: boolean; onJump: (alias: string) => void
|
||||
unit: EvidenceUnit; doc: EvidenceDocData; code: boolean; onJump?: (alias: string) => void
|
||||
}) {
|
||||
const segs = (unit.segments ?? []).filter((s) => s.kind !== 'structural')
|
||||
const whole = segs.map((s) => s.text).join('')
|
||||
@@ -624,13 +623,22 @@ function RationaleUnit({ unit, doc, code, onJump }: {
|
||||
const body = styles ? <StyledSlice text={whole} styles={styles} from={from} to={pos} links={!state} /> : s.text
|
||||
if (alias) {
|
||||
const source = sourceOf(s, doc)
|
||||
// inline-block:不吃上面那条下划线
|
||||
const tag = <sup className="mono ml-px inline-block select-none text-[9px] font-semibold text-faint" aria-hidden>{alias}</sup>
|
||||
if (!onJump) {
|
||||
return (
|
||||
// 不是按钮:.ev-seg 的手形光标换成问号,悬停看出处
|
||||
<span key={s.id} className="ev-seg" data-ev-state="deterministic" data-ev-line="solid" data-challenge-cite={alias}
|
||||
style={{ ...evStyle('deterministic'), cursor: 'help' }} title={source}>
|
||||
{body}{tag}<span className="sr-only">{CHALLENGE_TEXT.citedPlain(source)}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<button key={s.id} type="button" className="ev-seg" data-ev-state="deterministic" data-ev-line="solid"
|
||||
data-challenge-cite={alias} style={evStyle('deterministic')} title={source}
|
||||
aria-label={CHALLENGE_TEXT.cited(s.text, source)} onClick={() => onJump(alias)}>
|
||||
{body}
|
||||
{/* inline-block:不吃上面那条下划线 */}
|
||||
<sup className="mono ml-px inline-block select-none text-[9px] font-semibold text-faint" aria-hidden>{alias}</sup>
|
||||
{body}{tag}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
+124
-21
@@ -2,19 +2,22 @@
|
||||
// challenge、canvas/verifyFields.tsx)。
|
||||
//
|
||||
// 守的是这些「看着没坏、其实用不了」的地方:概况的句数和条目对不上、有被推翻的结论却不提醒降档(或者原因只藏在悬停里)、
|
||||
// 没有被推翻的结论也写降档、只标注时也写降档、探索运行里说出具、运行类别取不到时降档提示悄悄消失、没经过反方审稿的报告
|
||||
// 也冒出一块空框(或者白发一次请求)、点开一句不取工件(或者每点一句取一次)、理由里的数字看不出引用了哪次查询、
|
||||
// 没有被推翻的结论也写降档、只标注时也写降档、探索运行里说出具、运行类别取不到时说起出具(取不到就不知道是不是正式
|
||||
// 运行,一句和出具有关的话都不说,窗口重新拿到焦点时再取)、反方审稿不在出具路径上(服务端的 degrades 为假)还写降档、
|
||||
// 根本没反查成的几句混在「无法判断」里看不出来、没经过反方审稿的报告也冒出一块空框(或者白发一次请求)、点开一句不取工件(或者每点一句取一次)、理由里的数字看不出引用了哪次查询、
|
||||
// 裸数字没被标出来、理由文档是另一个工件时不去取、查询的 SQL / 行数 / 截断不显示、工件取不到时卡死在加载中、
|
||||
// 审稿记录没存上时每句还能点开、条目只列前十句时看不到其余的(或者补的时候吞掉原文相同的另一句、键盘焦点掉到页面上);
|
||||
// 检查器里的字段写不进节点配置(或者写进去的不是约定的形状:跟随设置要把这一项拿掉,不能写成空字符串)、下拉里列出了
|
||||
// 下游的报告、留空时认的报告和服务端不一样(「初稿 → 终稿 → 反方审稿」审终稿)、选中的报告被删了还看着像「未选择」、
|
||||
// 选中的不是报告撰写节点却说成不在上游、反方模型和撰写报告的模型相同不提醒、画布校验已经报了的又在字段下面说一遍、
|
||||
// 节点卡片的摘要还写着功能开发中;窄屏(390px)被撑出横向滚动、浅色主题下标签看不清。
|
||||
// 节点卡片的摘要还写着功能开发中、检查器上的叫法和服务端报错里的(engine/labels.py 的 CHALLENGE_LABEL、
|
||||
// CHALLENGE_ON_REFUTED_LABEL)对不上;窄屏(390px)被撑出横向滚动、浅色主题下标签看不清。
|
||||
//
|
||||
// 跑在预览页上:/preview.html?syn=challenge-*(用例见 frontend/src/dev/fixtures/challenge.tsx)。夹具在
|
||||
// frontend/src/dev/fixtures/challenge.json,理由文档是后端 run_verify_agent + compose_doc 真跑出来的(虚构的库和数字)。
|
||||
// 运行类别(GET /api/runs/{id})、工件(GET /api/artifacts/{id})、画布校验(两个纯计算的 POST)在这里伪造,别的非 GET
|
||||
// 一律拦掉并记下来,别的 GET 放行。
|
||||
// 一律拦掉并记下来,别的 GET 放行。叫法对照那一节不开浏览器:读 backend/app/engine/labels.py,terms.ts 借 vite 转译后
|
||||
// 直接 import。
|
||||
//
|
||||
// node scripts/check-challenge.mjs
|
||||
// 对别的实例跑时带上地址:AGENTLAB_WEB=http://localhost:<前端端口> node scripts/check-challenge.mjs
|
||||
@@ -42,10 +45,13 @@ const RUN_CLASS = {
|
||||
[RUNS.formal]: 'formal', [RUNS.flag]: 'formal', [RUNS.explore]: 'exploratory', [RUNS.none]: 'formal',
|
||||
[RUNS.many]: 'formal', [RUNS.broken]: 'formal', [RUNS.clear]: 'formal', [RUNS.clearFlag]: 'formal',
|
||||
[RUNS.exploreFlag]: 'exploratory', [RUNS.runfail]: 'formal', [RUNS.dup]: 'formal', [RUNS.norecord]: 'formal',
|
||||
[RUNS.unreviewed]: 'formal', [RUNS.offpath]: 'formal',
|
||||
}
|
||||
const LABEL = { stands: '经受住', weakened: '被削弱', refuted: '被推翻', inconclusive: '无法判断' }
|
||||
const FLAG_NOTE = '按节点设置,被推翻的结论只做标注,不影响出具档位'
|
||||
const DEGRADE_NOTE = '被推翻的结论计入出具缺口,本次正式运行将降档出具'
|
||||
const DEGRADE_OFF = '本节点不在带出具契约的成果节点上游,被推翻的结论不影响出具档位'
|
||||
const UNREVIEWED = (n) => `${n} 句没能反查,已记为无法判断。核查模型不可用、达到上限或数据源已停用时,这几句没有真正查询数据,原因见运行时间线`
|
||||
const FAILED = '无法获取反方审稿的详细内容'
|
||||
// F3a 画布校验(schema._check_challenge)在同样情形下的说法:检查器按 field 挂在字段下面
|
||||
const SERVER_MANY = '「反方审稿」的上游有多个「报告撰写」节点(「写月报」、「写周报」),请在「要审的报告」中选择要审的那一份'
|
||||
@@ -67,6 +73,48 @@ async function section(name, fn) {
|
||||
}
|
||||
}
|
||||
|
||||
// terms.ts 借 vite 转译成 data: URL 再 import(同 check-verify、check-decode):依赖也各自转好替换进去
|
||||
const loaded = new Map()
|
||||
async function load(path) {
|
||||
if (loaded.has(path)) return loaded.get(path)
|
||||
const res = await fetch(`${WEB}${path}?t=${Date.now()}`)
|
||||
if (!res.ok) throw new Error(`${path} → HTTP ${res.status}`)
|
||||
let code = await res.text()
|
||||
const deps = new Set([...code.matchAll(/from\s+["'](\/src\/[^"'?]+)(?:\?[^"']*)?["']/g)].map((m) => m[1]))
|
||||
for (const dep of deps) {
|
||||
const url = await load(dep)
|
||||
const pattern = new RegExp(`(["'])${dep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\?[^"']*)?\\1`, 'g')
|
||||
code = code.replace(pattern, JSON.stringify(url))
|
||||
}
|
||||
const url = 'data:text/javascript;base64,' + Buffer.from(code).toString('base64')
|
||||
loaded.set(path, url)
|
||||
return url
|
||||
}
|
||||
|
||||
await section('检查器的叫法和服务端报错一致', async () => {
|
||||
// 校验和执行报错写的是 engine/labels.py 的 CHALLENGE_LABEL(字段)、CHALLENGE_ON_REFUTED_LABEL(「结论被推翻时」的
|
||||
// 取值),报错让人去改某一项时,检查器上得是同一个名字,下拉里得找得到那个选项(选项只多一个「(默认)」)
|
||||
const { CHALLENGE_TEXT: C } = await import(await load('/src/lib/terms.ts'))
|
||||
const py = readFileSync(`${root}backend/app/engine/labels.py`, 'utf8')
|
||||
const dict = (name) => {
|
||||
const block = py.match(new RegExp(`^${name}: dict\\[str, str\\] = \\{([\\s\\S]*?)\\}$`, 'm'))?.[1]
|
||||
return block == null ? null : Object.fromEntries([...block.matchAll(/"([^"]+)":\s*"([^"]+)"/g)].map((m) => [m[1], m[2]]))
|
||||
}
|
||||
const fields = dict('CHALLENGE_LABEL')
|
||||
const options = dict('CHALLENGE_ON_REFUTED_LABEL')
|
||||
check('从 labels.py 读出了 CHALLENGE_LABEL、CHALLENGE_ON_REFUTED_LABEL', !!fields && !!options
|
||||
&& Object.keys(fields).length === 8 && Object.keys(options).length === 3, `${JSON.stringify(fields)} / ${JSON.stringify(options)}`)
|
||||
const wrong = Object.entries(fields ?? {}).filter(([k, v]) => C.field?.[k] !== v)
|
||||
check('检查器的字段名(CHALLENGE_TEXT.field)和 CHALLENGE_LABEL 一字不差、一项不多', !wrong.length
|
||||
&& Object.keys(C.field ?? {}).length === Object.keys(fields ?? {}).length, wrong.map(([k, v]) => `${k}:${C.field?.[k]} ≠ ${v}`).join(';'))
|
||||
const off = Object.entries(options ?? {}).filter(([k, v]) => (C.onRefuted?.[k] ?? '').replace(/(默认)$/, '') !== v)
|
||||
check('「结论被推翻时」的下拉选项去掉「(默认)」后和 CHALLENGE_ON_REFUTED_LABEL 一字不差', !off.length
|
||||
&& Object.keys(C.onRefuted ?? {}).length === 3, off.map(([k, v]) => `${k}:${C.onRefuted?.[k]} ≠ ${v}`).join(';'))
|
||||
check('……只有缺省的那一项(出具降档)带「(默认)」', Object.entries(C.onRefuted ?? {}).filter(([, v]) => v.endsWith('(默认)'))
|
||||
.map(([k]) => k).join(',') === 'degrade', JSON.stringify(C.onRefuted))
|
||||
check('帮助里提到的选项名就是下拉里的名字', Object.values(options ?? {}).every((v) => C.help?.on_refuted?.includes(`${v}:`)), C.help?.on_refuted)
|
||||
})
|
||||
|
||||
const browser = await chromium.launch({ executablePath: CHROME })
|
||||
|
||||
/**
|
||||
@@ -155,7 +203,8 @@ await section('审批卡:概况与降档提示', async () => {
|
||||
check('降档的原因常显在概况下面,不靠悬停', await note.count() === 1 && await note.isVisible() && (await text(note)) === DEGRADE_NOTE
|
||||
&& (await page.locator('[data-challenge-degrade]').getAttribute('title')) === null, await note.count() ? await text(note) : '没有')
|
||||
check('不是只标注:没有「只做标注」那句', await page.locator('[data-challenge-flag]').count() === 0)
|
||||
check('运行类别取到了:没有「暂时无法确认运行类别」', await page.locator('[data-challenge-runclass]').count() === 0)
|
||||
// 老服务端不给 degrades:按旧规则(不是只标注就降档),不写「不在出具路径上」
|
||||
check('没给 degrades:按旧规则写降档,不写「不影响出具档位」', await page.locator('[data-challenge-degrade-off]').count() === 0)
|
||||
const rows = await block.locator('[data-challenge-item]').evaluateAll((els) => els.map((el) => ({
|
||||
unit: el.getAttribute('data-challenge-item'),
|
||||
tag: el.querySelector('[data-challenge-tag]')?.textContent?.trim(),
|
||||
@@ -304,31 +353,83 @@ await section('正式运行、没有被推翻的结论:不写降档', async ()
|
||||
}
|
||||
})
|
||||
|
||||
await section('运行类别取不到:说明并可重试', async () => {
|
||||
await section('运行类别取不到:不说任何和出具有关的话,重新拿到焦点时再取', async () => {
|
||||
// 取不到就不知道是不是正式运行:以前写「暂时无法确认运行类别;如为正式运行……会降档」外加重试,卡顶却按审批自带的
|
||||
// 运行类别写着正式运行,两处说法打架。期 4 起取不到时和出具有关的话一句不说,窗口重新拿到焦点时再取,取到了正式运行
|
||||
// 降档提示才出现
|
||||
const { ctx, page, hits, state, errors } = await open('challenge-approval-runfail')
|
||||
await page.locator('[data-challenge="write"]').waitFor()
|
||||
const block = page.locator('[data-challenge="write"]')
|
||||
await block.waitFor()
|
||||
await runClassSettled(page, hits, RUNS.runfail)
|
||||
const failedLine = page.locator('[data-challenge-runclass="failed"]')
|
||||
check('取不到:不写降档,也不装作没事——写明暂时无法确认运行类别', await page.locator('[data-challenge-degrade]').count() === 0
|
||||
&& await failedLine.count() === 1 && (await text(failedLine)).startsWith('暂时无法确认运行类别'),
|
||||
await failedLine.count() ? await text(failedLine) : '没有说明')
|
||||
// 窗口重新拿到焦点时再取一次(store 对出错的槽会重发)
|
||||
const quiet = async () => {
|
||||
const all = await text(block)
|
||||
return await page.locator('[data-challenge-degrade], [data-challenge-degrade-note], [data-challenge-degrade-off], '
|
||||
+ '[data-challenge-flag], [data-challenge-runclass]').count() === 0 && !all.includes('出具') && !all.includes('运行类别')
|
||||
}
|
||||
check('取不到:不写降档、不写只标注,也不提运行类别', await quiet(), await text(block))
|
||||
check('取不到:概况和条目照常显示', (await text(page.locator('[data-challenge-count="refuted"]'))) === '被推翻 1 句'
|
||||
&& await page.locator('[data-challenge-item]').count() === 4)
|
||||
check('取不到:不再给「重试」(重试的是运行类别,不是审稿内容)', await block.locator('button', { hasText: '重试' }).count() === 0)
|
||||
// 窗口重新拿到焦点时再取一次(store 对出错的槽会重发):还是取不到,照样不说
|
||||
await page.evaluate(() => window.dispatchEvent(new Event('focus')))
|
||||
for (let i = 0; i < 30 && hits.runs.filter((r) => r === RUNS.runfail).length < 2; i++) await page.waitForTimeout(100)
|
||||
check('窗口重新拿到焦点:再取一次运行类别', hits.runs.filter((r) => r === RUNS.runfail).length === 2, hits.runs.join('、'))
|
||||
await page.waitForTimeout(300)
|
||||
check('……还是取不到:照样不说出具', await quiet(), await text(block))
|
||||
state.runFail = false
|
||||
await failedLine.locator('button', { hasText: '重试' }).focus()
|
||||
await page.keyboard.press('Enter')
|
||||
await page.evaluate(() => window.dispatchEvent(new Event('focus')))
|
||||
await page.waitForSelector('[data-challenge-degrade]')
|
||||
check('按重试:取到正式运行后写「出具会降档」,说明随之消失', await failedLine.count() === 0
|
||||
&& hits.runs.filter((r) => r === RUNS.runfail).length === 3, hits.runs.join('、'))
|
||||
await page.waitForTimeout(100)
|
||||
check('按重试:那一行拿掉后焦点接到概况上,不掉到页面上', await page.evaluate(() =>
|
||||
document.activeElement?.hasAttribute('data-challenge-counts') ?? false), await page.evaluate(() => document.activeElement?.tagName))
|
||||
check('取到了正式运行:写「出具会降档」和原因', (await text(page.locator('[data-challenge-degrade]'))) === '出具会降档'
|
||||
&& (await text(page.locator('[data-challenge-degrade-note]'))) === DEGRADE_NOTE && hits.runs.filter((r) => r === RUNS.runfail).length === 3,
|
||||
hits.runs.join('、'))
|
||||
check('取不到:页面没有报错', errors.length === 0, errors.join(';'))
|
||||
await ctx.close()
|
||||
})
|
||||
|
||||
await section('有句子根本没反查成:单独说一行,不混在「无法判断」里', async () => {
|
||||
// 服务端的 unreviewed:核查模型不可用、触顶、数据源不在了……这几句记为无法判断,可一条查询都没执行。「无法判断 2 句」
|
||||
// 分不出是查了、数据不够,还是根本没查,审批卡单独说
|
||||
const { ctx, page, hits, errors } = await open('challenge-approval-unreviewed')
|
||||
const block = page.locator('[data-challenge="write"]')
|
||||
await block.waitFor()
|
||||
await runClassSettled(page, hits, RUNS.unreviewed)
|
||||
const line = block.locator('[data-challenge-unreviewed]')
|
||||
check('写明几句没能反查、记为无法判断,原因去时间线看', await line.count() === 1 && (await text(line)) === UNREVIEWED(2)
|
||||
&& (await line.getAttribute('data-challenge-unreviewed')) === '2', await line.count() ? await text(line) : '没有')
|
||||
check('……概况照服务端的计数写,不另算', (await text(block.locator('[data-challenge-count="inconclusive"]'))) === '无法判断 1 句')
|
||||
// degrades 为真、正式运行、有被推翻的:照常写降档
|
||||
check('服务端说会降档(degrades 为真):写「出具会降档」和原因', (await text(page.locator('[data-challenge-degrade]'))) === '出具会降档'
|
||||
&& (await text(page.locator('[data-challenge-degrade-note]'))) === DEGRADE_NOTE && await page.locator('[data-challenge-degrade-off]').count() === 0)
|
||||
check('没能反查:页面没有报错', errors.length === 0, errors.join(';'))
|
||||
await ctx.close()
|
||||
// 老服务端不给 unreviewed(第一个用例)、给了 0(offpath 用例):都不写这一行
|
||||
for (const [syn, run] of [['challenge-approval', RUNS.formal], ['challenge-approval-offpath', RUNS.offpath]]) {
|
||||
const other = await open(syn)
|
||||
await other.page.locator('[data-challenge="write"]').waitFor()
|
||||
await runClassSettled(other.page, other.hits, run)
|
||||
check(`${syn}:没有没能反查的句子时不写这一行`, await other.page.locator('[data-challenge-unreviewed]').count() === 0
|
||||
&& !(await text(other.page.locator('[data-challenge="write"]'))).includes('没能反查'))
|
||||
await other.ctx.close()
|
||||
}
|
||||
})
|
||||
|
||||
await section('按降档处置、却不在出具路径上(degrades 为假):不写降档,说清为什么不影响', async () => {
|
||||
// 反方审稿接在侧枝上(或成果节点没带出具契约):被推翻的结论记了缺口也进不了出具。以前照 on_refuted 写「出具会降档」,
|
||||
// 审核人以为这次会降档,实际照常出具
|
||||
const { ctx, page, hits, errors } = await open('challenge-approval-offpath')
|
||||
const block = page.locator('[data-challenge="write"]')
|
||||
await block.waitFor()
|
||||
await runClassSettled(page, hits, RUNS.offpath)
|
||||
check('不写「出具会降档」', await page.locator('[data-challenge-degrade], [data-challenge-degrade-note]').count() === 0
|
||||
&& !(await text(block)).includes('出具会降档'), await text(block))
|
||||
const off = page.locator('[data-challenge-degrade-off]')
|
||||
check('写明本节点不在带出具契约的成果节点上游、不影响出具档位', await off.count() === 1 && (await text(off)) === DEGRADE_OFF,
|
||||
await off.count() ? await text(off) : '没有')
|
||||
check('不是只标注:没有「只做标注」那句', await page.locator('[data-challenge-flag]').count() === 0)
|
||||
check('不在出具路径上:页面没有报错', errors.length === 0, errors.join(';'))
|
||||
await ctx.close()
|
||||
})
|
||||
|
||||
await section('没有反方审稿:什么都不画', async () => {
|
||||
const { ctx, page, hits, errors } = await open('challenge-approval-none')
|
||||
await page.locator('[data-approval-report="write"]').waitFor()
|
||||
@@ -674,11 +775,13 @@ await section('窄屏与深浅色', async () => {
|
||||
check(`390px(${theme}):页面没有报错`, errors.length === 0, errors.join(';'))
|
||||
await ctx.close()
|
||||
}
|
||||
// 这次新加的几行说明(降档原因、运行类别取不到、审稿记录没存上):窄屏不撑宽,深浅色都看得清
|
||||
// 几行说明(降档原因、没能反查、不在出具路径上、审稿记录没存上):窄屏不撑宽,深浅色都看得清
|
||||
for (const theme of ['dark', 'light']) {
|
||||
const notes = [
|
||||
['challenge-approval', '[data-challenge-degrade-note]'],
|
||||
['challenge-approval-runfail', '[data-challenge-runclass="failed"] span'],
|
||||
['challenge-approval-unreviewed', '[data-challenge-unreviewed] > span:first-child'],
|
||||
['challenge-approval-unreviewed', '[data-challenge-unreviewed] > span:last-child'],
|
||||
['challenge-approval-offpath', '[data-challenge-degrade-off]'],
|
||||
['challenge-approval-norecord', '[data-challenge-norecord]'],
|
||||
]
|
||||
for (const [syn, sel] of notes) {
|
||||
|
||||
Reference in New Issue
Block a user