forked from ChaiHao/BPOS-MVP
三面拆分④:驾驶舱首页 —— 度量改接 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:
+30
-1
@@ -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>;
|
||||||
|
|||||||
@@ -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 });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/* ── 系统管理 · 通道实例 ────────────────────────────────────────── */
|
/* ── 系统管理 · 通道实例 ────────────────────────────────────────── */
|
||||||
|
|||||||
@@ -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。
|
||||||
|
「治理台与工作台汇报同源同口径」这一条,从此在代码里也成立。
|
||||||
|
|||||||
@@ -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} />;
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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) });
|
||||||
|
|
||||||
|
|||||||
@@ -53,3 +53,10 @@
|
|||||||
- 缩短页签文字(沟通 / 成果 / 详情 / 日志)—— 省 ~100px,但文案变短
|
- 缩短页签文字(沟通 / 成果 / 详情 / 日志)—— 省 ~100px,但文案变短
|
||||||
- 翻页囊只留箭头、页码收进 title —— 省 ~50px,但看不见「第几件/共几件」
|
- 翻页囊只留箭头、页码收进 title —— 省 ~50px,但看不见「第几件/共几件」
|
||||||
- 页签条换行两排 —— 全都放得下,但工作区正文少一行高
|
- 页签条换行两排 —— 全都放得下,但工作区正文少一行高
|
||||||
|
|
||||||
|
## 驾驶舱 · 死信卡(2026-07-27)
|
||||||
|
|
||||||
|
运营驾驶舱首屏收了「异常工作流 / 未闭合草稿 / 在途任务」三卡,**没有死信卡**。
|
||||||
|
死信目前只以治理者消息要素送出(`deadLetters.report` → `message`),没有一个按 kind 计数的
|
||||||
|
查询面 —— 要补这一卡,得给 system_message 加一条按 kind 聚合的治理路由。
|
||||||
|
在那之前,驾驶舱不摆一个查不出数的卡位。
|
||||||
|
|||||||
@@ -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` ──────────────────────── */
|
||||||
|
|||||||
@@ -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" /></>),
|
||||||
|
|||||||
Reference in New Issue
Block a user