Archived
用户要的是运行时机制:新建 + 编辑,存下去就生效,旧任务走完旧版本, 新任务调用时才加载新编排。所以「草稿 → 发布」两态整套去掉。 ## 闸从「发布」挪到「保存」 `savePackage` 现在先跑闭合校验,不闭合抛 `NotClosedError`(路由 412 带报告)—— **一个字节都不落库**。存进来的每一版都能起单,所以不需要「已发布」这个标记。 `publish` 用例、路由、`publishPackage` 存储方法一并删掉。 实测:把进 A6 的边映射清空后保存 → 412,逐条点名「边 e-g3-a6 未完成载荷映射; 节点「报价议价」的必填字段未映射:rows」。 ## 保存恒新增版本号,服务端说了算 这不是可选项,是「旧任务走完旧版本」的前提:工作流按 (包id, 版本) 去读定义, 就地覆盖 v1 就等于改掉所有在途 v1 任务正在读的编排 —— 它们会中途换了规则却毫不知情。 版本号由用例层算(当前最大 + 1),**不采信入参**。实测:入参故意填 version: 999, 落的是 v2。store 层因此改成**只插不改**:撞键宁可让唯一约束报错,也不静默覆盖。 起单取版从「最新已发布」改为「最新一版」(retired 除外);触发器的 requiredInputs 同口径。 实测:改完描述存成 v2 后,新起的单用 v2,早先那单仍挂 v1。 ## 迁移:旧状态值不归一就是一读就炸 状态枚举从 draft/published/retired 收成 active/retired。列上和 definition 这个 jsonb 里 各存着一份 state,两处都要改 —— 不改,`processPackageSchema.parse` 读一行就抛。 这不是开发期小麻烦,任何已部署实例都会中招。加了迁移 0012。 ## 界面:恒为 0 的假指标撤掉,不换个说法留着 - 编排面板:「已发布/草稿」芯片 → 「生效中/历史版本」(真正的区别是哪一版会被新起单取到); 发布按钮撤掉,保存按钮改成「保存并生效(存为 vN)」并在确认卡里说清影响面 - 历史版本只读,但就地指路:「去 v(N-1) 改(生效中)」,不让人自己找 - 蓝图面:「草稿 N · 未闭合 M」两个计数在新机制下**结构上恒为 0**(不闭合存不进来), 换成「共 N 个版本,保存即生效」 - 驾驶舱:「未闭合草稿」卡整张撤掉 —— 同理恒为 0。天然的第三张是「死路」 (route-dead-end 事件),但那需要一条按事件类型聚合的路由,没有之前不摆(同死信卡的规矩) ## 顺手修:种子的幂等判断本来是坏的 比对用 `JSON.stringify` 认键序,而 jsonb 从 PG 读回来键是重排过的 —— 永远判不等, 每播一次种就多存一版一模一样的定义(实测一口气堆到 v3)。改成递归按键名排序后比对。 连播三次,只第一次存。 ## 契约变更(破坏性,一步到位) `publish` 删除、`NotPublishableError` → `NotClosedError`、`publishable` → `closed`、 状态枚举收窄。按规矩破坏性变更该走三步弃用 —— 这里一步改完,因为: ① depcruise 反查下游全在仓内(374 模块无违规),同批改完; ② 要弃用的是一个**产品决定不再存在**的机制,留弃用窗口没有意义。 CONTRACT.md 已同步,快照单独说明。 typecheck ✓ lint ✓ depcruise 374 模块 ✓ 495 测试 ✓ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
245 lines
9.7 KiB
TypeScript
245 lines
9.7 KiB
TypeScript
/**
|
||
* feature `gov-memory` —— 运营中心的两处:**经营驾驶舱**(度量八指标)与**知识库**(活动记忆四层)。
|
||
*
|
||
* 自 `~/coding/JCAI_PRD/governance/_components/Governance.tsx` 平移:
|
||
* DOM 结构、类名(`g-*` + 色调 `t-*`)与文案原样保留,mock 常量换成服务端投影(README 平移规则 1/2/4)。
|
||
*
|
||
* 原本这两件同挂在「记忆管理」一区,是两种心智硬挤在一起:
|
||
* 看指标问的是「今天跑得怎样」,管沉淀问的是「学到了什么、放不放行」。三面拆分后各自成页。
|
||
*/
|
||
import { useState } from 'react';
|
||
import { TERMINAL_STATE_LABEL } from '@contracts';
|
||
import type { TerminalState } from '@contracts';
|
||
import { Ico } from '@web-shared/Ico.tsx';
|
||
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,
|
||
useGovTasks,
|
||
} from '@web-shared/gov/gov-api.ts';
|
||
|
||
export type HomeProps = { tab: string; nav: Nav };
|
||
|
||
const pct = (v: number | undefined) => Math.round((v ?? 0) * 100);
|
||
|
||
/** 终态五名的呈现色 —— 放行绿、驳回琥珀、停滞灰、耗尽红、终止墨,与状态章同族 */
|
||
const TERMINAL_TONE: Readonly<Record<string, string>> = {
|
||
released: 'var(--ok-accent)',
|
||
blocked: 'var(--warn-accent)',
|
||
stalled: '#8f959e',
|
||
exhausted: 'var(--danger-accent)',
|
||
terminated: '#41464e',
|
||
};
|
||
|
||
/* ---------------- 经营驾驶舱(运营首页) ---------------- */
|
||
|
||
/**
|
||
* 看企业今天跑得怎样 —— 先看**要不要管**(异常三卡),再看**跑得好不好**(度量八指标)。
|
||
* 异常在上:开屏第一眼该回答的是「有没有事要我处置」,不是「自主率几个点」。
|
||
*/
|
||
export function CockpitHome({ nav }: HomeProps) {
|
||
const metrics = useGovMetrics();
|
||
const tasks = useGovTasks();
|
||
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 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="失败走向未消化,待人工处置"
|
||
/>
|
||
{/*
|
||
* 「未闭合草稿」这张卡撤了 —— 严格检查 + 保存即生效之后,不闭合的包**存不进来**,
|
||
* 这个数结构上恒为 0。摆一个永远是 0 的卡位就是在占地方装样子。
|
||
* 天然的第三张是「死路」(网关一条边都判不中而卡住的流,已有 route-dead-end 事件),
|
||
* 但那需要一条按事件类型聚合的查询路由 —— 没有之前不摆(同死信卡的规矩)。
|
||
*/}
|
||
<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)} 次`}
|
||
/>
|
||
{/* 终态分布:比例即信息 —— 五终态五色一条,不把五个百分比塞进一行小字 */}
|
||
<div className="g-kpi">
|
||
<span className="g-kpi-l">
|
||
<Ico name="flag" s={12} />
|
||
终态分布
|
||
</span>
|
||
{mixTop.length === 0 ? (
|
||
<>
|
||
<span className="g-kpi-v">—</span>
|
||
<span className="g-kpi-s">还没有任务落终态</span>
|
||
</>
|
||
) : (
|
||
<>
|
||
<span className="g-mixbar">
|
||
{mixTop.map(([k, v]) => (
|
||
<i key={k} style={{ width: `${pct(v)}%`, background: TERMINAL_TONE[k] ?? '#c9d2dd' }} />
|
||
))}
|
||
</span>
|
||
<span className="g-mixbar-legend">
|
||
{mixTop.map(([k, v]) => (
|
||
<span key={k}>
|
||
<i style={{ background: TERMINAL_TONE[k] ?? '#c9d2dd' }} />
|
||
{TERMINAL_STATE_LABEL[k as TerminalState] ?? k} <b>{pct(v)}%</b>
|
||
</span>
|
||
))}
|
||
</span>
|
||
</>
|
||
)}
|
||
</div>
|
||
</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();
|
||
const first = (activities.data?.activities ?? [])[0]?.activityId;
|
||
const memory = useGovMemory(first);
|
||
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="写入唯蒸馏,条目必挂证据链;记忆不载权力——强制力只经候选增量 + 回放 + 人放行进入业务模型。"
|
||
/>
|
||
);
|
||
}
|