三面拆分④:驾驶舱首页 —— 度量改接 report 真八指标,纠正率/DMN 不再恒为 0

治理台原先调 memory 的五指标版,还把 submissions/corrections/judgements
硬塞 0 —— 仪表在说谎。八指标本就在 report 里算好,这里接上,与工作台汇报同源同口径。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-27 21:08:42 +08:00
co-authored by Claude Fable 5
parent bb6760d7a4
commit 9fce0fdaa0
11 changed files with 292 additions and 127 deletions
+1 -1
View File
@@ -2,4 +2,4 @@
* feature `gov-memory` 唯一出口(编码规范 §9.1)。
* 记忆管理:度量仪表、活动记忆四层
*/
export { MemoryHome, MemPage } from './internal/panel.tsx';
export { CockpitHome, KnowledgeHome, MemPage } from './internal/panel.tsx';
@@ -1,91 +1,200 @@
/**
* feature `gov-memory` —— 记忆管理:度量仪表、活动记忆四层
* feature `gov-memory` —— 运营中心的两处:**经营驾驶舱**(度量八指标)与**知识库**(活动记忆四层)。
*
* 自 `~/coding/JCAI_PRD/governance/_components/Governance.tsx` 平移:
* DOM 结构、类名(`g-*` + 色调 `t-*`)与文案原样保留,mock 常量换成服务端投影(README 平移规则 1/2/4)。
* 区首页只负责导航,治理动作一律落在下钻详情页的**就地确认带**(影响预览三必答)。
*
* 原本这两件同挂在「记忆管理」一区,是两种心智硬挤在一起:
* 看指标问的是「今天跑得怎样」,管沉淀问的是「学到了什么、放不放行」。三面拆分后各自成页。
*/
import { useState } from 'react';
import { Chip, Clear, Kpi, NList, NRow, Note, PageHead, QueryGate } from '@web-shared/gov/primitives.tsx';
import { TERMINAL_STATE_LABEL } from '@contracts';
import type { TerminalState } from '@contracts';
import { Chip, Clear, Kpi, NList, NRow, Note, PageHead, QueryGate, Todo } from '@web-shared/gov/primitives.tsx';
import type { Nav } from '@web-shared/gov/gov-route.ts';
import {
useGovActivities,
useGovMemory,
useGovMetrics,
useGovPackages,
useGovPkgReports,
useGovTasks,
} from '@web-shared/gov/gov-api.ts';
export type HomeProps = { tab: string; nav: Nav };
export function MemoryHome({ tab, nav }: HomeProps) {
const pct = (v: number | undefined) => Math.round((v ?? 0) * 100);
/* ---------------- 经营驾驶舱(运营首页) ---------------- */
/**
* 看企业今天跑得怎样 —— 先看**要不要管**(异常三卡),再看**跑得好不好**(度量八指标)。
* 异常在上:开屏第一眼该回答的是「有没有事要我处置」,不是「自主率几个点」。
*/
export function CockpitHome({ nav }: HomeProps) {
const metrics = useGovMetrics();
const activities = useGovActivities();
const [picked, setPicked] = useState<string | undefined>(undefined);
const memory = useGovMemory(picked);
if (tab === 'layers') {
return (
<>
<Note>
活动记忆四层:<b>事实层 / 模式层 / 裁决层 / 例外层</b>。写入唯蒸馏,条目必挂证据链;
记忆不载权力——强制力只经候选增量 + 回放 + 人放行进入业务模型。
</Note>
<div className="g-picker">
{(activities.data?.activities ?? []).map((a) => (
<button key={a.activityId} className={'g-pick' + (picked === a.activityId ? ' on' : '')} onClick={() => setPicked(a.activityId)}>
{a.name}
</button>
))}
</div>
<NList>
{(memory.data?.entries ?? []).map((e) => (
<NRow
key={e.id}
tone={e.incrementState === undefined ? 'brand' : 'warn'}
ico="layers"
title={e.statement}
sub={`${e.layer} · ${e.createdAt.slice(0, 16).replace('T', ' ')}`}
cols={[{ k: '证据', v: e.evidenceCount }]}
right={e.incrementState !== undefined ? <Chip tone="warn">候选 · {e.incrementState}</Chip> : <Chip tone="ok">已成条目</Chip>}
onClick={() => nav.push({ k: 'mem', id: e.id })}
/>
))}
{picked !== undefined && (memory.data?.entries ?? []).length === 0 && (
<Clear>该活动尚无记忆条目——终态封卷触发蒸馏钩子后产出。</Clear>
)}
</NList>
</>
);
}
const tasks = useGovTasks();
const packages = useGovPackages();
const reports = useGovPkgReports(packages.data?.packages ?? []);
const m = metrics.data;
const pct = (v: number | undefined) => Math.round((v ?? 0) * 100);
const workflows = tasks.data?.workflows ?? [];
const taskRows = tasks.data?.tasks ?? [];
const abnormal = workflows.filter((w) => w.state === 'abnormal').length;
const running = taskRows.filter((t) => t.terminal === undefined).length;
const draftBad = (packages.data?.packages ?? []).filter(
(p) => p.state !== 'published' && reports.data?.get(`${p.id}@${p.version}`)?.publishable === false,
).length;
const mix = m?.terminalMix ?? {};
const mixTop = Object.entries(mix)
.filter(([, v]) => v > 0)
.sort((a, b) => b[1] - a[1]);
return (
<QueryGate query={metrics}>
<>
<div className="g-kpis">
<Kpi ico="bot" tone="auto" label="自主率" value={pct(m?.autonomy)} unit="%" dial={pct(m?.autonomy)} sub="机器产出 ÷(机器 + 人)" />
<Kpi ico="rotate-ccw" tone="warn" label="纠正率" value={pct(m?.correction)} unit="%" dial={pct(m?.correction)} sub="被纠正批次 ÷ 提交批次" />
<Kpi
ico="gauge"
tone="brand"
label="有效自主率"
value={pct(m?.effectiveAutonomy)}
unit="%"
dial={pct(m?.effectiveAutonomy)}
sub="自主率 ×(1 − 纠正率)"
/>
<Kpi ico="hand" label="打扰率" value={(m?.interruption ?? 0).toFixed(1)} sub="每单任务调用人类工具次数" />
<Kpi ico="git-commit" tone="ok" label="DMN 覆盖率" value={pct(m?.dmnCoverage)} unit="%" sub="确定性求值占判断次数" />
</div>
<Note>
自主率与纠正率<b>成对读</b>,复合为有效自主率——单看自主率会奖励「猛冲后返工」。度量只是仪表,不载权力。
</Note>
</>
<>
<div className="g-kpis">
<Kpi
ico="alert-triangle"
tone="danger"
label="异常工作流"
value={abnormal}
sub="失败走向未消化,待人工处置"
/>
<Kpi ico="git-merge" tone="warn" label="未闭合草稿" value={draftBad} sub="边不闭合不得发布" />
<Kpi ico="activity" tone="brand" label="在途任务" value={running} sub="尚未落终态的任务" />
</div>
{abnormal > 0 ? (
<div className="g-todo">
<Todo
tone="danger"
ico="alert-triangle"
title={`${abnormal} 条工作流异常`}
desc="失败走向未消化 —— 去需处置逐条看它卡在哪个节点、要不要改派或叫停。"
acts={[{ label: '去处置', ico: 'flag', onClick: () => nav.jump('tasks', 'todo') }]}
/>
</div>
) : (
<Clear>没有异常工作流 —— 在途的都还在正常走。</Clear>
)}
<div className="g-kpis">
<Kpi
ico="bot"
tone="auto"
label="自主率"
value={pct(m?.autonomy)}
unit="%"
dial={pct(m?.autonomy)}
sub="机器产出 ÷(机器 + 人)"
/>
<Kpi
ico="rotate-ccw"
tone="warn"
label="纠正率"
value={pct(m?.correction)}
unit="%"
dial={pct(m?.correction)}
sub="被纠正批次 ÷ 提交批次"
/>
<Kpi
ico="gauge"
tone="brand"
label="有效自主率"
value={pct(m?.effectiveAutonomy)}
unit="%"
dial={pct(m?.effectiveAutonomy)}
sub="自主率 ×(1 − 纠正率)"
/>
<Kpi ico="hand" label="打扰率" value={(m?.interruption ?? 0).toFixed(1)} sub="每单任务调用人类工具次数" />
</div>
<div className="g-kpis">
<Kpi ico="unplug" tone="danger" label="断点率" value={pct(m?.breakpoint)} unit="%" sub="当下可为 ∩ 目标所需 = 空" />
<Kpi ico="git-commit" tone="ok" label="DMN 覆盖率" value={pct(m?.dmnCoverage)} unit="%" sub="确定性求值占判断次数" />
<Kpi
ico="coins"
label="单位成本"
value={Math.round(m?.unitCost.tokens ?? 0)}
unit="tok"
sub={`每放行一单:${(m?.unitCost.minutes ?? 0).toFixed(1)} 分钟 · 调人 ${(m?.unitCost.humanCalls ?? 0).toFixed(1)} 次`}
/>
<Kpi
ico="flag"
label="终态分布"
value={mixTop.length === 0 ? '—' : `${pct(mixTop[0]![1])}%`}
sub={
mixTop.length === 0
? '还没有任务落终态'
: mixTop
.map(([k, v]) => `${TERMINAL_STATE_LABEL[k as TerminalState] ?? k} ${pct(v)}%`)
.join(' · ')
}
/>
</div>
<Note>
自主率与纠正率<b>成对读</b>,复合为有效自主率——单看自主率会奖励「猛冲后返工」。度量只是仪表,不载权力。
八指标与工作台的汇报<b>同源同口径</b>,统计窗口:近一{m?.period === 'day' ? '日' : m?.period === 'week' ? '周' : m?.period === 'year' ? '年' : '月'}。
</Note>
</>
</QueryGate>
);
}
/* ---------------- ⑨ 系统管理 ---------------- */
/* ---------------- 知识库 ---------------- */
/** 活动记忆四层 + 候选增量的放行口 —— 记忆的终点是成为模型,那道门在这里 */
export function KnowledgeHome({ nav }: HomeProps) {
const activities = useGovActivities();
const [picked, setPicked] = useState<string | undefined>(undefined);
const memory = useGovMemory(picked);
return (
<>
<Note>
活动记忆四层:<b>事实层 / 模式层 / 裁决层 / 例外层</b>。写入唯蒸馏,条目必挂证据链;
记忆不载权力——强制力只经候选增量 + 回放 + 人放行进入业务模型。
</Note>
<div className="g-picker">
{(activities.data?.activities ?? []).map((a) => (
<button
key={a.activityId}
className={'g-pick' + (picked === a.activityId ? ' on' : '')}
onClick={() => setPicked(a.activityId)}
>
{a.name}
</button>
))}
</div>
<NList>
{(memory.data?.entries ?? []).map((e) => (
<NRow
key={e.id}
tone={e.incrementState === undefined ? 'brand' : 'warn'}
ico="layers"
title={e.statement}
sub={`${e.layer} · ${e.createdAt.slice(0, 16).replace('T', ' ')}`}
cols={[{ k: '证据', v: e.evidenceCount }]}
right={
e.incrementState !== undefined ? (
<Chip tone="warn">候选 · {e.incrementState}</Chip>
) : (
<Chip tone="ok">已成条目</Chip>
)
}
onClick={() => nav.push({ k: 'mem', id: e.id })}
/>
))}
{picked === undefined && <Clear>选一个活动,看它这些年学到了什么。</Clear>}
{picked !== undefined && (memory.data?.entries ?? []).length === 0 && (
<Clear>这个活动还没有记忆条目 —— 任务落终态封卷、蒸馏钩子跑过之后,沉淀会出现在这里。</Clear>
)}
</NList>
</>
);
}
/* ---------------- 记忆条目详情 ---------------- */
export function MemPage({ id }: { id: string }) {
const activities = useGovActivities();
@@ -94,22 +203,17 @@ export function MemPage({ id }: { id: string }) {
const e = (memory.data?.entries ?? []).find((x) => x.id === id);
if (e === undefined) return <Note>记忆条目需从对应活动进入。</Note>;
return (
<>
<PageHead
ico="layers"
kicker={`活动记忆 · ${e.layer}`}
title={e.statement}
chips={
<>
<Chip ico="link">{e.evidenceCount} 条证据</Chip>
{e.incrementState !== undefined && <Chip tone="warn">候选增量 · {e.incrementState}</Chip>}
</>
}
desc="写入唯蒸馏,条目必挂证据链;记忆不载权力——强制力只经候选增量 + 回放 + 人放行进入业务模型。"
/>
</>
<PageHead
ico="layers"
kicker={`活动记忆 · ${e.layer}`}
title={e.statement}
chips={
<>
<Chip ico="link">{e.evidenceCount} 条证据</Chip>
{e.incrementState !== undefined && <Chip tone="warn">候选增量 · {e.incrementState}</Chip>}
</>
}
desc="写入唯蒸馏,条目必挂证据链;记忆不载权力——强制力只经候选增量 + 回放 + 人放行进入业务模型。"
/>
);
}
/* ---------------- 系统 ---------------- */