Archived
三面拆分④:驾驶舱首页 —— 度量改接 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:
@@ -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="写入唯蒸馏,条目必挂证据链;记忆不载权力——强制力只经候选增量 + 回放 + 人放行进入业务模型。"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- 系统 ---------------- */
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
Todo,
|
||||
} from '@web-shared/gov/primitives.tsx';
|
||||
import type { Nav } from '@web-shared/gov/gov-route.ts';
|
||||
import { useGovActivities, useGovPackages, useGovStaff, useGovTriggers } from '@web-shared/gov/gov-api.ts';
|
||||
import { useGovActivities, useGovPackages, useGovPkgReports, useGovStaff, useGovTriggers } from '@web-shared/gov/gov-api.ts';
|
||||
import { FlowFigureBlock } from '@web-shared/gov/flow/FlowFigure.tsx';
|
||||
import { packageToGraph } from '@web-shared/gov/flow/pkg-to-graph.ts';
|
||||
import { mergeGraphIntoPackage } from '@web-shared/gov/flow/graph-to-pkg.ts';
|
||||
@@ -47,23 +47,6 @@ export type HomeProps = { tab: string; nav: Nav };
|
||||
|
||||
/* ── 报告:画图即校验,列表与详情同一来源 ─────────────────────────────── */
|
||||
|
||||
/** 每个包一份闭合报告 —— 磁贴上的「边 n/m」、待处置清单、发布门都读它 */
|
||||
function usePkgReports(packages: readonly ProcessPackage[]) {
|
||||
return useQuery({
|
||||
queryKey: ['gov', 'process-reports', packages.map((p) => `${p.id}@${p.version}`).join(',')],
|
||||
enabled: packages.length > 0,
|
||||
queryFn: async () => {
|
||||
const entries = await Promise.all(
|
||||
packages.map(
|
||||
async (p) =>
|
||||
[`${p.id}@${p.version}`, await call<ClosureReport>(validateProcessRoute, { body: { pkg: p } })] as const,
|
||||
),
|
||||
);
|
||||
return new Map(entries);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 边闭合数:边总数 − 有违规的边(同一条边多处违规只记一次) */
|
||||
function edgeCheckOf(pkg: ProcessPackage, report: ClosureReport | undefined) {
|
||||
const total = pkg.edges.length;
|
||||
@@ -84,7 +67,7 @@ export function WorkflowsHome({ tab, nav }: HomeProps) {
|
||||
function PackagesHome({ tab, nav }: HomeProps) {
|
||||
const q = useGovPackages();
|
||||
const packages = q.data?.packages ?? [];
|
||||
const reports = usePkgReports(packages);
|
||||
const reports = useGovPkgReports(packages);
|
||||
const reportOf = (p: ProcessPackage) => reports.data?.get(`${p.id}@${p.version}`);
|
||||
const counts = (p: ProcessPackage) => ({
|
||||
activity: p.nodes.filter((n) => n.kind === 'activity').length,
|
||||
@@ -860,7 +843,7 @@ export function EdgesPage({ id }: { id: string }) {
|
||||
const [pid, versionText] = id.split('@');
|
||||
const q = useGovPackages();
|
||||
const pkg = (q.data?.packages ?? []).find((p) => p.id === pid && String(p.version) === versionText);
|
||||
const reports = usePkgReports(pkg === undefined ? [] : [pkg]);
|
||||
const reports = useGovPkgReports(pkg === undefined ? [] : [pkg]);
|
||||
if (pkg === undefined) {
|
||||
return (
|
||||
<QueryGate query={q}>
|
||||
|
||||
Reference in New Issue
Block a user