三面拆分④:驾驶舱首页 —— 度量改接 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
+30 -1
View File
@@ -1749,17 +1749,46 @@ export declare const govWorkflowRoute: {
}, z.core.$strip>>; }, z.core.$strip>>;
}, z.core.$strip>; }, z.core.$strip>;
}; };
/** 自主率与纠正率**成对读**,复合为有效自主率 —— 单看自主率会奖励「猛冲后返工」 */ /**
* 度量八指标(OS 8.3)—— 运营驾驶舱的仪表读数。
*
* 出参直接复用 `reportMetricsSchema`:**与工作台汇报同源同口径**。
* 治理台若另算一套,同一个组织会读出两个自主率 —— 那时候争的就不是数,是谁的表准。
* 自主率与纠正率**成对读**,复合为有效自主率 —— 单看自主率会奖励「猛冲后返工」。
*/
export declare const govMetricsRoute: { export declare const govMetricsRoute: {
readonly method: "GET"; readonly method: "GET";
readonly path: "/api/gov/metrics"; readonly path: "/api/gov/metrics";
/** 统计窗口 —— 不传按月;驾驶舱可切日/周/月/年 */
readonly query: z.ZodObject<{
period: z.ZodOptional<z.ZodEnum<{
day: "day";
week: "week";
month: "month";
year: "year";
}>>;
}, z.core.$strip>;
/** `period` 原样回声:客户端不传时,由它知道服务端用的是哪个窗口 */
readonly output: z.ZodObject<{ readonly output: z.ZodObject<{
autonomy: z.ZodNumber; autonomy: z.ZodNumber;
correction: z.ZodNumber; correction: z.ZodNumber;
effectiveAutonomy: z.ZodNumber; effectiveAutonomy: z.ZodNumber;
interruption: z.ZodNumber; interruption: z.ZodNumber;
breakpoint: z.ZodNumber;
dmnCoverage: z.ZodNumber; dmnCoverage: z.ZodNumber;
unitCost: z.ZodObject<{
tokens: z.ZodNumber;
minutes: z.ZodNumber;
humanCalls: z.ZodNumber;
}, z.core.$strip>;
terminalMix: z.ZodRecord<z.ZodString, z.ZodNumber>;
taskCount: z.ZodNumber; taskCount: z.ZodNumber;
period: z.ZodEnum<{
day: "day";
week: "week";
month: "month";
year: "year";
}>;
}, z.core.$strip>; }, z.core.$strip>;
}; };
export type GovTasksOutput = z.infer<typeof listGovTasksRoute.output>; export type GovTasksOutput = z.infer<typeof listGovTasksRoute.output>;
+16 -10
View File
@@ -410,21 +410,27 @@ export function registerGovernanceOpsRoutes(app: Hono<AppEnv>, container: Contai
* 度量仪表 —— 自主率与纠正率**成对读**,复合为有效自主率。 * 度量仪表 —— 自主率与纠正率**成对读**,复合为有效自主率。
* 只读近期任务的任务记忆聚合;度量本身不载权力。 * 只读近期任务的任务记忆聚合;度量本身不载权力。
*/ */
/*
* 度量八指标 —— 走 report 模块的**全组织**投影(staffId 传 undefined)。
*
* 此前这里调的是 memory 的五指标版,且把 submissions / corrections /
* judgements 四个入参硬塞 0 —— 纠正率与 DMN 覆盖率于是结构性恒为 0,
* 仪表在说谎。八指标本就在 report 里算好(§report/contract.ts:「治理台记忆区的
* 仪表读数,同源同口径」),这里只是把它接上,不另起一套口径。
*/
app.get(govMetricsRoute.path, async (c) => { app.get(govMetricsRoute.path, async (c) => {
const denied = await readMemory(c.get('account')); const denied = await readMemory(c.get('account'));
if (denied) return c.json(denied, denyStatus(denied)); if (denied) return c.json(denied, denyStatus(denied));
const parsed = govMetricsRoute.query.safeParse(c.req.query());
if (!parsed.success) {
const body: ApiError = { code: 'validation-failed', message: '统计窗口不合法', detail: parsed.error.issues };
return c.json(body, 400);
}
const period = parsed.data.period ?? 'month';
const { staff } = await container.org.listStaff(); const { staff } = await container.org.listStaff();
const nested = await Promise.all(staff.map((s) => container.task.listByOwner(s.id))); const nested = await Promise.all(staff.map((s) => container.task.listByOwner(s.id)));
const taskIds = nested.flat().map((t) => t.id); const metrics = await container.report.metrics(undefined, period);
const metrics = await container.memory.metrics({ return c.json({ ...metrics, taskCount: nested.flat().length, period });
taskIds,
submissions: 0,
corrections: 0,
taskCount: taskIds.length,
deterministicJudgements: 0,
totalJudgements: 0,
});
return c.json({ ...metrics, taskCount: taskIds.length });
}); });
/* ── 系统管理 · 通道实例 ────────────────────────────────────────── */ /* ── 系统管理 · 通道实例 ────────────────────────────────────────── */
+7 -1
View File
@@ -24,5 +24,11 @@
- **口径挂归侧与事件流,零埋点**:输入只有归侧日志行与任务终态,没有第三种埋点 - **口径挂归侧与事件流,零埋点**:输入只有归侧日志行与任务终态,没有第三种埋点
- **自主率与纠正率成对读**,复合为有效自主率 —— 单看任一都会被优化坏 - **自主率与纠正率成对读**,复合为有效自主率 —— 单看任一都会被优化坏
- **单位成本的分母只认放行通过**:跑得多不等于产出多 - **单位成本的分母只认放行通过**:跑得多不等于产出多
- 度量是**只读投影**,不设独立模块(§4 注);治理台记忆区与工作台汇报同源同口径 - 度量是**只读投影**,不设独立模块(§4 注);治理台驾驶舱与工作台汇报同源同口径
- 周期窗口左闭右开、**周一为首日**、按 UTC 切分(单组织单时区) - 周期窗口左闭右开、**周一为首日**、按 UTC 切分(单组织单时区)
近变(2026-07-27):
- 模块面**未变**。app 层的 `GET /api/gov/metrics`(运营驾驶舱)改接本模块的
`metrics(undefined, period)` —— 此前它调 memory 的五指标版并把 submissions /
corrections / judgements 硬塞 0,纠正率与 DMN 覆盖率结构性恒为 0。
「治理台与工作台汇报同源同口径」这一条,从此在代码里也成立。
+3 -3
View File
@@ -30,7 +30,7 @@ import { TriggerPage } from '@features/gov-trigger';
import { ActPage, LibraryHome } from '@features/gov-activity'; import { ActPage, LibraryHome } from '@features/gov-activity';
import { ToolPage, ToolsHome } from '@features/gov-tool'; import { ToolPage, ToolsHome } from '@features/gov-tool';
import { DataHome, ObjPage } from '@features/gov-data'; import { DataHome, ObjPage } from '@features/gov-data';
import { MemPage, MemoryHome } from '@features/gov-memory'; import { CockpitHome, KnowledgeHome, MemPage } from '@features/gov-memory';
import { ChanPage, LlmPage, SystemHome } from '@features/gov-system'; import { ChanPage, LlmPage, SystemHome } from '@features/gov-system';
/* ── URL 编解码:`#/<surface>/<zone>/<tab>[/<k>:<id>…]` ────────────────── */ /* ── URL 编解码:`#/<surface>/<zone>/<tab>[/<k>:<id>…]` ────────────────── */
@@ -341,11 +341,11 @@ function ZoneHome({ zone, tab, nav }: { zone: GovZone; tab: string; nav: Nav })
tools: ToolsHome, tools: ToolsHome,
data: DataHome, data: DataHome,
// 运营中心 // 运营中心
cockpit: MemoryHome, cockpit: CockpitHome,
tasks: TasksHome, tasks: TasksHome,
people: PeopleHome, people: PeopleHome,
ledgers: LedgersPlaceholder, ledgers: LedgersPlaceholder,
knowledge: MemoryHome, knowledge: KnowledgeHome,
system: SystemHome, system: SystemHome,
}[zone]; }[zone];
return <P tab={tab} nav={nav} />; return <P tab={tab} nav={nav} />;
+1 -1
View File
@@ -2,4 +2,4 @@
* feature `gov-memory` 唯一出口(编码规范 §9.1)。 * feature `gov-memory` 唯一出口(编码规范 §9.1)。
* 记忆管理:度量仪表、活动记忆四层 * 记忆管理:度量仪表、活动记忆四层
*/ */
export { MemoryHome, MemPage } from './internal/panel.tsx'; export { CockpitHome, KnowledgeHome, MemPage } from './internal/panel.tsx';
@@ -1,28 +1,154 @@
/** /**
* feature `gov-memory` —— 记忆管理:度量仪表、活动记忆四层 * feature `gov-memory` —— 运营中心的两处:**经营驾驶舱**(度量八指标)与**知识库**(活动记忆四层)。
* *
* 自 `~/coding/JCAI_PRD/governance/_components/Governance.tsx` 平移: * 自 `~/coding/JCAI_PRD/governance/_components/Governance.tsx` 平移:
* DOM 结构、类名(`g-*` + 色调 `t-*`)与文案原样保留,mock 常量换成服务端投影(README 平移规则 1/2/4)。 * DOM 结构、类名(`g-*` + 色调 `t-*`)与文案原样保留,mock 常量换成服务端投影(README 平移规则 1/2/4)。
* 区首页只负责导航,治理动作一律落在下钻详情页的**就地确认带**(影响预览三必答)。 *
* 原本这两件同挂在「记忆管理」一区,是两种心智硬挤在一起:
* 看指标问的是「今天跑得怎样」,管沉淀问的是「学到了什么、放不放行」。三面拆分后各自成页。
*/ */
import { useState } from 'react'; 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 type { Nav } from '@web-shared/gov/gov-route.ts';
import { import {
useGovActivities, useGovActivities,
useGovMemory, useGovMemory,
useGovMetrics, useGovMetrics,
useGovPackages,
useGovPkgReports,
useGovTasks,
} from '@web-shared/gov/gov-api.ts'; } from '@web-shared/gov/gov-api.ts';
export type HomeProps = { tab: string; nav: Nav }; 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 metrics = useGovMetrics();
const tasks = useGovTasks();
const packages = useGovPackages();
const reports = useGovPkgReports(packages.data?.packages ?? []);
const m = metrics.data;
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="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 activities = useGovActivities();
const [picked, setPicked] = useState<string | undefined>(undefined); const [picked, setPicked] = useState<string | undefined>(undefined);
const memory = useGovMemory(picked); const memory = useGovMemory(picked);
if (tab === 'layers') {
return ( return (
<> <>
<Note> <Note>
@@ -31,7 +157,11 @@ export function MemoryHome({ tab, nav }: HomeProps) {
</Note> </Note>
<div className="g-picker"> <div className="g-picker">
{(activities.data?.activities ?? []).map((a) => ( {(activities.data?.activities ?? []).map((a) => (
<button key={a.activityId} className={'g-pick' + (picked === a.activityId ? ' on' : '')} onClick={() => setPicked(a.activityId)}> <button
key={a.activityId}
className={'g-pick' + (picked === a.activityId ? ' on' : '')}
onClick={() => setPicked(a.activityId)}
>
{a.name} {a.name}
</button> </button>
))} ))}
@@ -45,47 +175,26 @@ export function MemoryHome({ tab, nav }: HomeProps) {
title={e.statement} title={e.statement}
sub={`${e.layer} · ${e.createdAt.slice(0, 16).replace('T', ' ')}`} sub={`${e.layer} · ${e.createdAt.slice(0, 16).replace('T', ' ')}`}
cols={[{ k: '证据', v: e.evidenceCount }]} cols={[{ k: '证据', v: e.evidenceCount }]}
right={e.incrementState !== undefined ? <Chip tone="warn">候选 · {e.incrementState}</Chip> : <Chip tone="ok">已成条目</Chip>} right={
e.incrementState !== undefined ? (
<Chip tone="warn">候选 · {e.incrementState}</Chip>
) : (
<Chip tone="ok">已成条目</Chip>
)
}
onClick={() => nav.push({ k: 'mem', id: e.id })} onClick={() => nav.push({ k: 'mem', id: e.id })}
/> />
))} ))}
{picked === undefined && <Clear>选一个活动,看它这些年学到了什么。</Clear>}
{picked !== undefined && (memory.data?.entries ?? []).length === 0 && ( {picked !== undefined && (memory.data?.entries ?? []).length === 0 && (
<Clear>该活动尚无记忆条目——终态封卷触发蒸馏钩子后产出。</Clear> <Clear>这个活动还没有记忆条目 —— 任务落终态封卷、蒸馏钩子跑过之后,沉淀会出现在这里。</Clear>
)} )}
</NList> </NList>
</> </>
); );
} }
const m = metrics.data; /* ---------------- 记忆条目详情 ---------------- */
const pct = (v: number | undefined) => Math.round((v ?? 0) * 100);
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>
</>
</QueryGate>
);
}
/* ---------------- ⑨ 系统管理 ---------------- */
export function MemPage({ id }: { id: string }) { export function MemPage({ id }: { id: string }) {
const activities = useGovActivities(); const activities = useGovActivities();
@@ -94,7 +203,6 @@ export function MemPage({ id }: { id: string }) {
const e = (memory.data?.entries ?? []).find((x) => x.id === id); const e = (memory.data?.entries ?? []).find((x) => x.id === id);
if (e === undefined) return <Note>记忆条目需从对应活动进入。</Note>; if (e === undefined) return <Note>记忆条目需从对应活动进入。</Note>;
return ( return (
<>
<PageHead <PageHead
ico="layers" ico="layers"
kicker={`活动记忆 · ${e.layer}`} kicker={`活动记忆 · ${e.layer}`}
@@ -107,9 +215,5 @@ export function MemPage({ id }: { id: string }) {
} }
desc="写入唯蒸馏,条目必挂证据链;记忆不载权力——强制力只经候选增量 + 回放 + 人放行进入业务模型。" desc="写入唯蒸馏,条目必挂证据链;记忆不载权力——强制力只经候选增量 + 回放 + 人放行进入业务模型。"
/> />
</>
); );
} }
/* ---------------- 系统 ---------------- */
@@ -35,7 +35,7 @@ import {
Todo, Todo,
} from '@web-shared/gov/primitives.tsx'; } from '@web-shared/gov/primitives.tsx';
import type { Nav } from '@web-shared/gov/gov-route.ts'; 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 { FlowFigureBlock } from '@web-shared/gov/flow/FlowFigure.tsx';
import { packageToGraph } from '@web-shared/gov/flow/pkg-to-graph.ts'; import { packageToGraph } from '@web-shared/gov/flow/pkg-to-graph.ts';
import { mergeGraphIntoPackage } from '@web-shared/gov/flow/graph-to-pkg.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) { function edgeCheckOf(pkg: ProcessPackage, report: ClosureReport | undefined) {
const total = pkg.edges.length; const total = pkg.edges.length;
@@ -84,7 +67,7 @@ export function WorkflowsHome({ tab, nav }: HomeProps) {
function PackagesHome({ tab, nav }: HomeProps) { function PackagesHome({ tab, nav }: HomeProps) {
const q = useGovPackages(); const q = useGovPackages();
const packages = q.data?.packages ?? []; 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 reportOf = (p: ProcessPackage) => reports.data?.get(`${p.id}@${p.version}`);
const counts = (p: ProcessPackage) => ({ const counts = (p: ProcessPackage) => ({
activity: p.nodes.filter((n) => n.kind === 'activity').length, 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 [pid, versionText] = id.split('@');
const q = useGovPackages(); const q = useGovPackages();
const pkg = (q.data?.packages ?? []).find((p) => p.id === pid && String(p.version) === versionText); 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) { if (pkg === undefined) {
return ( return (
<QueryGate query={q}> <QueryGate query={q}>
+24
View File
@@ -5,6 +5,7 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import type { import type {
ClosureReport,
GovActivitiesOutput, GovActivitiesOutput,
GovApprovalsOutput, GovApprovalsOutput,
GovChannelsOutput, GovChannelsOutput,
@@ -21,6 +22,7 @@ import type {
GovToolsOutput, GovToolsOutput,
GovTriggersOutput, GovTriggersOutput,
GovWorkflowOutput, GovWorkflowOutput,
ProcessPackage,
} from '@contracts'; } from '@contracts';
import { import {
browseLedgerRoute, browseLedgerRoute,
@@ -39,6 +41,7 @@ import {
listStaffRoute, listStaffRoute,
listToolsRoute, listToolsRoute,
listTriggersRoute, listTriggersRoute,
validateProcessRoute,
} from '@contracts'; } from '@contracts';
import { call } from '@web-shared/typed-fetch.ts'; import { call } from '@web-shared/typed-fetch.ts';
@@ -81,6 +84,27 @@ export const useGovGrants = (positionId: string | undefined): UseQueryResult<Gov
export const useGovPackages = (): UseQueryResult<GovPackagesOutput> => export const useGovPackages = (): UseQueryResult<GovPackagesOutput> =>
useQuery({ queryKey: ['gov', 'process'], queryFn: () => call<GovPackagesOutput>(listProcessPackagesRoute) }); useQuery({ queryKey: ['gov', 'process'], queryFn: () => call<GovPackagesOutput>(listProcessPackagesRoute) });
/**
* 每个包一份闭合报告 —— 磁贴上的「边 n/m」、待补清单、发布门、驾驶舱的未闭合草稿数都读它。
* 一处算、多处读:两张首页各算一遍就会各说各话。
*/
export const useGovPkgReports = (
packages: readonly ProcessPackage[],
): UseQueryResult<Map<string, ClosureReport>> =>
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);
},
});
export const useGovTriggers = (): UseQueryResult<GovTriggersOutput> => export const useGovTriggers = (): UseQueryResult<GovTriggersOutput> =>
useQuery({ queryKey: ['gov', 'triggers'], queryFn: () => call<GovTriggersOutput>(listTriggersRoute) }); useQuery({ queryKey: ['gov', 'triggers'], queryFn: () => call<GovTriggersOutput>(listTriggersRoute) });
+7
View File
@@ -53,3 +53,10 @@
- 缩短页签文字(沟通 / 成果 / 详情 / 日志)—— 省 ~100px,但文案变短 - 缩短页签文字(沟通 / 成果 / 详情 / 日志)—— 省 ~100px,但文案变短
- 翻页囊只留箭头、页码收进 title —— 省 ~50px,但看不见「第几件/共几件」 - 翻页囊只留箭头、页码收进 title —— 省 ~50px,但看不见「第几件/共几件」
- 页签条换行两排 —— 全都放得下,但工作区正文少一行高 - 页签条换行两排 —— 全都放得下,但工作区正文少一行高
## 驾驶舱 · 死信卡(2026-07-27)
运营驾驶舱首屏收了「异常工作流 / 未闭合草稿 / 在途任务」三卡,**没有死信卡**。
死信目前只以治理者消息要素送出(`deadLetters.report` → `message`),没有一个按 kind 计数的
查询面 —— 要补这一卡,得给 system_message 加一条按 kind 聚合的治理路由。
在那之前,驾驶舱不摆一个查不出数的卡位。
+12 -9
View File
@@ -9,6 +9,7 @@ import { positionIdSchema, staffIdSchema, taskIdSchema } from '../id.ts';
import { closureReportSchema, processPackageSchema, triggerConfigSchema } from '../process-package.ts'; import { closureReportSchema, processPackageSchema, triggerConfigSchema } from '../process-package.ts';
import { controlSemanticSchema, taskRuntimeStateSchema, terminalStateSchema, workflowStateSchema } from '../terminal.ts'; import { controlSemanticSchema, taskRuntimeStateSchema, terminalStateSchema, workflowStateSchema } from '../terminal.ts';
import { paradigmSchema } from '../manifest.ts'; import { paradigmSchema } from '../manifest.ts';
import { reportMetricsSchema, reportPeriodSchema } from './workbench.ts';
/* ── 员工管理 ─────────────────────────────────────────────────────────── */ /* ── 员工管理 ─────────────────────────────────────────────────────────── */
@@ -393,18 +394,20 @@ export const govWorkflowRoute = {
/* ── 记忆管理 · 度量仪表 ──────────────────────────────────────────────── */ /* ── 记忆管理 · 度量仪表 ──────────────────────────────────────────────── */
/** 自主率与纠正率**成对读**,复合为有效自主率 —— 单看自主率会奖励「猛冲后返工」 */ /**
* 度量八指标(OS 8.3)—— 运营驾驶舱的仪表读数。
*
* 出参直接复用 `reportMetricsSchema`:**与工作台汇报同源同口径**。
* 治理台若另算一套,同一个组织会读出两个自主率 —— 那时候争的就不是数,是谁的表准。
* 自主率与纠正率**成对读**,复合为有效自主率 —— 单看自主率会奖励「猛冲后返工」。
*/
export const govMetricsRoute = { export const govMetricsRoute = {
method: 'GET', method: 'GET',
path: '/api/gov/metrics', path: '/api/gov/metrics',
output: z.object({ /** 统计窗口 —— 不传按月;驾驶舱可切日/周/月/年 */
autonomy: z.number(), query: z.object({ period: reportPeriodSchema.optional() }),
correction: z.number(), /** `period` 原样回声:客户端不传时,由它知道服务端用的是哪个窗口 */
effectiveAutonomy: z.number(), output: reportMetricsSchema.extend({ taskCount: z.number().int(), period: reportPeriodSchema }),
interruption: z.number(),
dmnCoverage: z.number(),
taskCount: z.number().int(),
}),
} as const satisfies RouteContract; } as const satisfies RouteContract;
/* ── 出参类型:契约自己命名,消费方不再 `z.infer` ──────────────────────── */ /* ── 出参类型:契约自己命名,消费方不再 `z.infer` ──────────────────────── */
+3
View File
@@ -29,6 +29,9 @@ export function Ico({ name, s = 18 }: { name: string; s?: number }) {
"user-round": (<><circle cx="12" cy="8" r="5" /><path d="M20 21a8 8 0 0 0-16 0" /></>), "user-round": (<><circle cx="12" cy="8" r="5" /><path d="M20 21a8 8 0 0 0-16 0" /></>),
bot: (<><path d="M12 8V4H8" /><rect width="16" height="12" x="4" y="8" rx="2" /><path d="M2 14h2" /><path d="M20 14h2" /><path d="M15 13v2" /><path d="M9 13v2" /></>), bot: (<><path d="M12 8V4H8" /><rect width="16" height="12" x="4" y="8" rx="2" /><path d="M2 14h2" /><path d="M20 14h2" /><path d="M15 13v2" /><path d="M9 13v2" /></>),
dashboard: (<><rect width="7" height="9" x="3" y="3" rx="1" /><rect width="7" height="5" x="14" y="3" rx="1" /><rect width="7" height="9" x="14" y="12" rx="1" /><rect width="7" height="5" x="3" y="16" rx="1" /></>), dashboard: (<><rect width="7" height="9" x="3" y="3" rx="1" /><rect width="7" height="5" x="14" y="3" rx="1" /><rect width="7" height="9" x="14" y="12" rx="1" /><rect width="7" height="5" x="3" y="16" rx="1" /></>),
hand: (<><path d="M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2" /><path d="M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v2" /><path d="M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8" /><path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15" /></>),
unplug: (<><path d="m19 5 3-3" /><path d="m2 22 3-3" /><path d="M6.3 20.3a2.4 2.4 0 0 0 3.4 0L12 18l-6-6-2.3 2.3a2.4 2.4 0 0 0 0 3.4Z" /><path d="M7.5 13.5 10 11" /><path d="M10.5 16.5 13 14" /><path d="m12 6 6 6 2.3-2.3a2.4 2.4 0 0 0 0-3.4l-2.6-2.6a2.4 2.4 0 0 0-3.4 0Z" /></>),
coins: (<><circle cx="8" cy="8" r="6" /><path d="M18.09 10.37A6 6 0 1 1 10.34 18" /><path d="M7 6h1v4" /><path d="m16.71 13.88.7.71-2.82 2.82" /></>),
mail: (<><rect width="20" height="16" x="2" y="4" rx="2" /><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" /></>), mail: (<><rect width="20" height="16" x="2" y="4" rx="2" /><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" /></>),
"mail-open": (<><path d="M21.2 8.4c.5.38.8.97.8 1.6v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V10a2 2 0 0 1 .8-1.6l8-6a2 2 0 0 1 2.4 0z" /><path d="m22 10-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 10" /></>), "mail-open": (<><path d="M21.2 8.4c.5.38.8.97.8 1.6v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V10a2 2 0 0 1 .8-1.6l8-6a2 2 0 0 1 2.4 0z" /><path d="m22 10-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 10" /></>),
inbox: (<><polyline points="22 12 16 12 14 15 10 15 8 12 2 12" /><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /></>), inbox: (<><polyline points="22 12 16 12 14 15 10 15 8 12 2 12" /><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /></>),