Archived
M6(2/n): 治理台九功能 + SSE 订阅层 + 读/治分权
治理台按 §9.1 拆成九个 gov-* feature,共用件落 shared/gov:
· 外壳(左轨/顶栏/面包屑/页签/整页下钻)自原型 Governance.tsx 平移,
DOM 与 g-* 类名原样;路由状态即应用状态,区/页签/下钻栈全编码进 hash 可深链
· 编排画布 diagram-js 画布件 1:1 平移(flow-layout/flow-node-paint/FlowDiagram),
新增 pkg-to-graph 把 ProcessPackage 转成画布图模型;统一报价 v1 全图 12 节点 15 边渲染通过
· 后端补四路由:审批单 / 工作流下钻 / 通道实例 / 度量仪表
SSE 订阅层(§9.2):app 层单一连接,按信封目标失效缓存;
组件不再自行轮询(去掉 refetchInterval)。广播只说「哪个目标动了」,不带业务载荷,
且在投递已落库已送达之后才发 —— 不是第二条投递链路。
两处实质修正:
· 治理路由一律要 govern → 读路由改要 read。客体三类 × 动作三种的分法就是为此:
BD 有 task:read 能看在途、动不了控制事件;一律 govern 等于把三种动作压成一种
· 控制事件/恢复/改派原记为 engine-transition(归机器产出)→ 新增 control 条目种类,
归人裁决侧。归侧是按动作类型的纯推导,所以「谁干的」必须体现在 kind 上;
否则人的叫停会被算进自主率的机器分子
判据 ⑩ 治理台一半跑通:在途监控 / 叫停 / 恢复,治理动作留痕可查。
无权查看不再画成空清单,按岗位说明缺哪一项。
gates: tsc / eslint / depcruise(286 模块)/ api-report / 360 测试 全绿
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* feature `gov-activity` 唯一出口(编码规范 §9.1)。
|
||||
* 活动库:两维分类 × 包版本 × 资产版本
|
||||
*/
|
||||
export { LibraryHome, ActPage } from './internal/panel.tsx';
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* feature `gov-activity` —— 活动库:两维分类 × 包版本 × 资产版本
|
||||
*
|
||||
* 自 `~/coding/JCAI_PRD/governance/_components/Governance.tsx` 平移:
|
||||
* DOM 结构、类名(`g-*` + 色调 `t-*`)与文案原样保留,mock 常量换成服务端投影(README 平移规则 1/2/4)。
|
||||
* 区首页只负责导航,治理动作一律落在下钻详情页的**就地确认带**(影响预览三必答)。
|
||||
*/
|
||||
import { Card, Chip, Clear, Entry, NList, NRow, Note, PageHead, QueryGate, St } from '@web-shared/gov/primitives.tsx';
|
||||
import { PARA } from '@web-shared/gov/tone.ts';
|
||||
import type { Nav } from '@web-shared/gov/gov-route.ts';
|
||||
import {
|
||||
useGovActivities,
|
||||
useGovMemory,
|
||||
} from '@web-shared/gov/gov-api.ts';
|
||||
|
||||
export type HomeProps = { tab: string; nav: Nav };
|
||||
|
||||
export function LibraryHome({ nav }: HomeProps) {
|
||||
const q = useGovActivities();
|
||||
const activities = q.data?.activities ?? [];
|
||||
return (
|
||||
<QueryGate query={q}>
|
||||
<>
|
||||
<Note>
|
||||
活动是任务的<b>定义</b>:编排引用一个活动,运行时就长出一个任务。业务分类 × 活动类型两维平级;
|
||||
每活动两条版本序列——包版本走安装两审,资产版本走进化管线。
|
||||
</Note>
|
||||
<NList>
|
||||
{activities.map((a) => {
|
||||
const effective = a.assetVersions.filter((v) => v.effective);
|
||||
return (
|
||||
<NRow
|
||||
key={a.activityId}
|
||||
tone={PARA[a.paradigm].tone}
|
||||
ico={PARA[a.paradigm].ico}
|
||||
title={a.name}
|
||||
sub={`${a.activityId} · ${a.category}`}
|
||||
cols={[
|
||||
{ k: '类型', v: PARA[a.paradigm].label },
|
||||
{ k: '包版本', v: <span className="g-mono">{a.packageVersions.map((p) => p.version).join(', ')}</span> },
|
||||
{ k: '资产生效版', v: <span className="g-mono">{effective.length}</span> },
|
||||
]}
|
||||
right={
|
||||
<St tone={a.enabled ? 'ok' : 'mute'} ico={a.enabled ? 'circle-check' : 'power'}>
|
||||
{a.enabled ? '已启用' : '已停用'}
|
||||
</St>
|
||||
}
|
||||
onClick={() => nav.push({ k: 'act', id: a.activityId })}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</NList>
|
||||
</>
|
||||
</QueryGate>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- ⑥ 工具管理 ---------------- */
|
||||
|
||||
export function ActPage({ id, nav }: { id: string; nav: Nav }) {
|
||||
const q = useGovActivities();
|
||||
const memory = useGovMemory(id);
|
||||
const a = (q.data?.activities ?? []).find((x) => x.activityId === id);
|
||||
if (a === undefined) return <Note>活动不存在。</Note>;
|
||||
return (
|
||||
<>
|
||||
<PageHead
|
||||
ico={PARA[a.paradigm].ico}
|
||||
tone={PARA[a.paradigm].tone}
|
||||
kicker={`活动 · ${a.activityId}`}
|
||||
title={a.name}
|
||||
chips={
|
||||
<>
|
||||
<Chip tone={PARA[a.paradigm].tone}>{PARA[a.paradigm].label}</Chip>
|
||||
<Chip ico="tag">{a.category}</Chip>
|
||||
<Chip tone={a.enabled ? 'ok' : 'mute'}>{a.enabled ? '已启用' : '已停用'}</Chip>
|
||||
</>
|
||||
}
|
||||
desc={PARA[a.paradigm].full + ' —— 活动是系统外开发、经此安装的外装件。'}
|
||||
/>
|
||||
<Card ico="package" title="包版本 · 安装两审">
|
||||
<div className="g-rows">
|
||||
{a.packageVersions.map((p) => (
|
||||
<div key={p.version}>
|
||||
<em>v{p.version}</em>
|
||||
<b className="mono">
|
||||
{p.state} · {p.installedAt.slice(0, 16).replace('T', ' ')}
|
||||
</b>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<Card ico="trending-up" title="资产版本 · 进化管线">
|
||||
<div className="g-rows">
|
||||
{a.assetVersions.map((v, i) => (
|
||||
<div key={i}>
|
||||
<em>
|
||||
{v.kind} v{v.version}
|
||||
</em>
|
||||
<b className="mono">{v.effective ? '生效中' : '历史'}</b>
|
||||
</div>
|
||||
))}
|
||||
{a.assetVersions.length === 0 && <Clear>尚无资产版本。</Clear>}
|
||||
</div>
|
||||
</Card>
|
||||
<Entry
|
||||
ico="layers"
|
||||
title="活动记忆"
|
||||
desc={`四层条目 · 当前 ${(memory.data?.entries ?? []).length} 条`}
|
||||
onClick={() => nav.jump('memory', 'layers')}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* feature `gov-data` 唯一出口(编码规范 §9.1)。
|
||||
* 数据管理:域 / 对象目录、schema 视图、记录浏览
|
||||
*/
|
||||
export { DataHome, ObjPage } from './internal/panel.tsx';
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* feature `gov-data` —— 数据管理:域 / 对象目录、schema 视图、记录浏览
|
||||
*
|
||||
* 自 `~/coding/JCAI_PRD/governance/_components/Governance.tsx` 平移:
|
||||
* DOM 结构、类名(`g-*` + 色调 `t-*`)与文案原样保留,mock 常量换成服务端投影(README 平移规则 1/2/4)。
|
||||
* 区首页只负责导航,治理动作一律落在下钻详情页的**就地确认带**(影响预览三必答)。
|
||||
*/
|
||||
import { Ico } from '@web-shared/Ico.tsx';
|
||||
import { Card, Chip, Clear, NList, NRow, Note, PageHead, QueryGate, Sec } from '@web-shared/gov/primitives.tsx';
|
||||
import type { Nav } from '@web-shared/gov/gov-route.ts';
|
||||
import {
|
||||
useGovLedger,
|
||||
useGovObjects,
|
||||
} from '@web-shared/gov/gov-api.ts';
|
||||
|
||||
export type HomeProps = { tab: string; nav: Nav };
|
||||
|
||||
export function DataHome({ nav }: HomeProps) {
|
||||
const q = useGovObjects();
|
||||
const objects = q.data?.objects ?? [];
|
||||
const domains = [...new Set(objects.map((o) => o.domain))];
|
||||
return (
|
||||
<QueryGate query={q}>
|
||||
<>
|
||||
<Note>
|
||||
数据<b>不设箱、靠域</b>:域是承载的划分,对象由活动声明物化——最大化合并、只增不破坏。
|
||||
数据可以自生长,权力不可以自生长。
|
||||
</Note>
|
||||
{domains.map((d) => (
|
||||
<div key={d} className="g-sec">
|
||||
<div className="g-band">
|
||||
<Ico name="database" s={13} />
|
||||
{d}
|
||||
<span className="x">{objects.filter((o) => o.domain === d).length} 个对象</span>
|
||||
</div>
|
||||
<NList>
|
||||
{objects
|
||||
.filter((o) => o.domain === d)
|
||||
.map((o) => (
|
||||
<NRow
|
||||
key={`${o.domain}.${o.object}`}
|
||||
tone="brand"
|
||||
ico="table"
|
||||
title={o.object}
|
||||
sub={`${o.domain}.${o.object}`}
|
||||
onClick={() => nav.push({ k: 'obj', id: `${o.domain}.${o.object}` })}
|
||||
/>
|
||||
))}
|
||||
</NList>
|
||||
</div>
|
||||
))}
|
||||
{objects.length === 0 && <Clear>尚无物化对象——活动首次写入时按声明物化。</Clear>}
|
||||
</>
|
||||
</QueryGate>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- ⑧ 记忆管理 ---------------- */
|
||||
|
||||
export function ObjPage({ id }: { id: string }) {
|
||||
const [domain, object] = id.split('.');
|
||||
const objects = useGovObjects();
|
||||
const page = useGovLedger(domain, object);
|
||||
const known = (objects.data?.objects ?? []).some((o) => o.domain === domain && o.object === object);
|
||||
if (!known) return <Note>对象未物化。</Note>;
|
||||
const columns = page.data?.columns ?? [];
|
||||
return (
|
||||
<>
|
||||
<PageHead
|
||||
ico="table"
|
||||
kicker={`数据对象 · ${domain}`}
|
||||
title={object ?? ''}
|
||||
chips={
|
||||
<>
|
||||
<Chip ico="columns">{columns.length} 列</Chip>
|
||||
<Chip ico="hash">{page.data?.total ?? 0} 行</Chip>
|
||||
<Chip ico="key">主键 {(page.data?.primaryKey ?? []).join(', ') || '—'}</Chip>
|
||||
</>
|
||||
}
|
||||
desc="声明物化:最大化合并、只增不破坏——不删列、不改窄、不换主键。此处只读正式版,跨任务只见过门的事实。"
|
||||
/>
|
||||
<Card ico="columns" title="模式">
|
||||
<div className="g-rows">
|
||||
{columns.map((c) => (
|
||||
<div key={c.name}>
|
||||
<em>{c.name}</em>
|
||||
<b className="mono">{c.type}</b>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
<Sec ico="table" title="记录浏览" extra={`前 ${(page.data?.rows ?? []).length} 行`}>
|
||||
<div className="g-dbwrap">
|
||||
<table className="g-dbtable">
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map((c) => (
|
||||
<th key={c.name}>{c.name}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(page.data?.rows ?? []).map((row, i) => (
|
||||
<tr key={i}>
|
||||
{columns.map((c) => (
|
||||
<td key={c.name} className="g-mono">
|
||||
{String(row[c.name] ?? '')}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Sec>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- 记忆条目 ---------------- */
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* feature `gov-memory` 唯一出口(编码规范 §9.1)。
|
||||
* 记忆管理:度量仪表、活动记忆四层
|
||||
*/
|
||||
export { MemoryHome, MemPage } from './internal/panel.tsx';
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* 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 type { Nav } from '@web-shared/gov/gov-route.ts';
|
||||
import {
|
||||
useGovActivities,
|
||||
useGovMemory,
|
||||
useGovMetrics,
|
||||
} from '@web-shared/gov/gov-api.ts';
|
||||
|
||||
export type HomeProps = { tab: string; nav: Nav };
|
||||
|
||||
export function MemoryHome({ tab, 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 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 }) {
|
||||
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="写入唯蒸馏,条目必挂证据链;记忆不载权力——强制力只经候选增量 + 回放 + 人放行进入业务模型。"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- 系统 ---------------- */
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* feature `gov-process` 唯一出口(编码规范 §9.1)。
|
||||
* 流程包编排画布(diagram-js 画布件平移):连线即映射、画图即校验、不闭合不得发布
|
||||
*/
|
||||
export { WorkflowsHome, PkgPage, EdgesPage } from './internal/panel.tsx';
|
||||
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* feature `gov-process` —— 流程包编排画布(diagram-js 画布件平移):连线即映射、画图即校验、不闭合不得发布
|
||||
*
|
||||
* 自 `~/coding/JCAI_PRD/governance/_components/Governance.tsx` 平移:
|
||||
* DOM 结构、类名(`g-*` + 色调 `t-*`)与文案原样保留,mock 常量换成服务端投影(README 平移规则 1/2/4)。
|
||||
* 区首页只负责导航,治理动作一律落在下钻详情页的**就地确认带**(影响预览三必答)。
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
publishProcessRoute,
|
||||
} from '@contracts';
|
||||
import type { ClosureReport } from '@contracts';
|
||||
import { call } from '@web-shared/typed-fetch.ts';
|
||||
import { Chip, Entry, NList, NRow, Note, PageHead, QueryGate, Sec, St } from '@web-shared/gov/primitives.tsx';
|
||||
import type { Nav } from '@web-shared/gov/gov-route.ts';
|
||||
import {
|
||||
useGovPackages,
|
||||
} from '@web-shared/gov/gov-api.ts';
|
||||
import { FlowFigure } from '@web-shared/gov/flow/FlowFigure.tsx';
|
||||
|
||||
export type HomeProps = { tab: string; nav: Nav };
|
||||
|
||||
export function WorkflowsHome({ nav }: HomeProps) {
|
||||
const q = useGovPackages();
|
||||
const packages = q.data?.packages ?? [];
|
||||
return (
|
||||
<QueryGate query={q}>
|
||||
<>
|
||||
<Note>
|
||||
编排即定契约——<b>连线即映射、画图即校验</b>,边不闭合不得发布。流程包是设计态的定义,工作流是它的运行时实例。
|
||||
</Note>
|
||||
<NList>
|
||||
{packages.map((p) => {
|
||||
const published = p.state === 'published';
|
||||
return (
|
||||
<NRow
|
||||
key={`${p.id}@${p.version}`}
|
||||
tone={published ? 'ok' : 'warn'}
|
||||
ico="workflow"
|
||||
title={p.name}
|
||||
sub={`${p.id} · v${p.version}`}
|
||||
cols={[
|
||||
{ k: '节点', v: p.nodes.length },
|
||||
{ k: '边', v: p.edges.length },
|
||||
{ k: '活动', v: p.nodes.filter((n) => n.kind === 'activity').length },
|
||||
]}
|
||||
right={
|
||||
<St tone={published ? 'ok' : 'warn'} ico={published ? 'circle-check' : 'file-pen'}>
|
||||
{published ? '已发布' : '草稿'}
|
||||
</St>
|
||||
}
|
||||
onClick={() => nav.push({ k: 'pkg', id: `${p.id}@${p.version}` })}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</NList>
|
||||
</>
|
||||
</QueryGate>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- ④ 触发器配置 ---------------- */
|
||||
|
||||
/** 治理写动作统一入口:成功即失效相关查询,画面由服务端重投影 */
|
||||
function useGovAction<TInput>(mutate: (input: TInput) => Promise<unknown>, keys: readonly string[]) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: mutate,
|
||||
onSuccess: () => {
|
||||
for (const key of keys) void queryClient.invalidateQueries({ queryKey: ['gov', key] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------- 工作流 ---------------- */
|
||||
|
||||
export function PkgPage({ id, nav }: { id: string; nav: Nav }) {
|
||||
const [pid, versionText] = id.split('@');
|
||||
const q = useGovPackages();
|
||||
const [report, setReport] = useState<ClosureReport | undefined>(undefined);
|
||||
const pkg = (q.data?.packages ?? []).find((p) => p.id === pid && String(p.version) === versionText);
|
||||
|
||||
const publish = useGovAction(
|
||||
() => call<ClosureReport>(publishProcessRoute, { params: { id: pid!, version: versionText! } }).then((r) => setReport(r)),
|
||||
['process'],
|
||||
);
|
||||
|
||||
if (pkg === undefined) return <Note>流程包不存在。</Note>;
|
||||
const published = pkg.state === 'published';
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead
|
||||
ico="workflow"
|
||||
tone={published ? 'ok' : 'warn'}
|
||||
kicker={`流程包 · ${pkg.id} v${pkg.version}`}
|
||||
title={pkg.name}
|
||||
chips={
|
||||
<>
|
||||
<Chip tone={published ? 'ok' : 'warn'}>{published ? '已发布' : '草稿'}</Chip>
|
||||
<Chip ico="circle-dot">{pkg.nodes.length} 节点</Chip>
|
||||
<Chip ico="git-branch">{pkg.edges.length} 边</Chip>
|
||||
</>
|
||||
}
|
||||
desc="编排即定契约——连线即映射、画图即校验,边不闭合不得发布。"
|
||||
acts={[
|
||||
{
|
||||
label: '发布',
|
||||
ico: 'upload',
|
||||
primary: true,
|
||||
disabled: published,
|
||||
ask: {
|
||||
title: `发布流程包:${pkg.name} v${pkg.version}`,
|
||||
note: <Note tone="warn">发布前跑闭合校验:任一违规即拒绝,不存在「带病发布」。</Note>,
|
||||
impact: { who: '此后新起单按此版本执行;在途工作流仍按其起单时的版本', undo: '可发新版本覆盖,不可回改本版', when: '校验通过后即时' },
|
||||
okLabel: '确认发布',
|
||||
onOk: () => publish.mutate(undefined),
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{report !== undefined && (
|
||||
<Note tone={report.publishable ? 'ok' : 'warn'}>
|
||||
{report.publishable ? '闭合校验通过 —— 已发布。' : `闭合校验未过:${report.violations.map((v) => v.message).join(';')}`}
|
||||
</Note>
|
||||
)}
|
||||
|
||||
<Sec ico="git-branch" title="编排画布">
|
||||
<FlowFigure pkg={pkg} instances={[]} />
|
||||
</Sec>
|
||||
|
||||
<Entry ico="arrow-left-right" title="边映射" desc="连线即映射:每条边把上游输出口装配成下游输入" onClick={() => nav.push({ k: 'edges', id })} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
if (pkg === undefined) return <Note>流程包不存在。</Note>;
|
||||
const labelOf = (nodeId: string) => pkg.nodes.find((n) => n.id === nodeId)?.label ?? nodeId;
|
||||
return (
|
||||
<>
|
||||
<PageHead ico="arrow-left-right" kicker={`边映射 · ${pkg.id} v${pkg.version}`} title={pkg.name} desc="边上的字段装配就是下游活动的输入契约:装配不上,校验期就拦下。" />
|
||||
<NList>
|
||||
{pkg.edges.map((e, i) => (
|
||||
<NRow
|
||||
key={i}
|
||||
tone={e.reopen ? 'warn' : 'brand'}
|
||||
ico={e.reopen ? 'rotate-ccw' : 'arrow-right'}
|
||||
title={`${labelOf(e.from)} → ${labelOf(e.to)}`}
|
||||
sub={e.condition === undefined ? undefined : `条件 · ${e.condition}`}
|
||||
cols={[
|
||||
{ k: '类别', v: e.reopen ? '回边 · 重开新实例' : '正向' },
|
||||
{ k: '字段装配', v: <span className="g-mono">{e.mapping.length}</span> },
|
||||
]}
|
||||
onClick={() => undefined}
|
||||
/>
|
||||
))}
|
||||
</NList>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- 触发器 ---------------- */
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* feature `gov-staff` 唯一出口(编码规范 §9.1)。
|
||||
* 员工管理:账号、岗位授权、停用前置检查
|
||||
*/
|
||||
export { PeopleHome, EmpPage, RolePage } from './internal/panel.tsx';
|
||||
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* feature `gov-staff` —— 员工管理:账号、岗位授权、停用前置检查
|
||||
*
|
||||
* 自 `~/coding/JCAI_PRD/governance/_components/Governance.tsx` 平移:
|
||||
* DOM 结构、类名(`g-*` + 色调 `t-*`)与文案原样保留,mock 常量换成服务端投影(README 平移规则 1/2/4)。
|
||||
* 区首页只负责导航,治理动作一律落在下钻详情页的**就地确认带**(影响预览三必答)。
|
||||
*/
|
||||
import {
|
||||
GOVERNANCE_FUNCTION_LABEL,
|
||||
GRANT_ACTION_LABEL,
|
||||
} from '@contracts';
|
||||
import { Card, Chip, Clear, Entry, NList, NRow, Note, PageHead, QueryGate, St } from '@web-shared/gov/primitives.tsx';
|
||||
import type { Nav } from '@web-shared/gov/gov-route.ts';
|
||||
import {
|
||||
useGovGrants,
|
||||
useGovStaff,
|
||||
useGovTasks,
|
||||
} from '@web-shared/gov/gov-api.ts';
|
||||
|
||||
export type HomeProps = { tab: string; nav: Nav };
|
||||
|
||||
export function PeopleHome({ tab, nav }: HomeProps) {
|
||||
const q = useGovStaff();
|
||||
const tasks = useGovTasks();
|
||||
const staff = q.data?.staff ?? [];
|
||||
const positions = q.data?.positions ?? [];
|
||||
|
||||
if (tab === 'roles') {
|
||||
return (
|
||||
<>
|
||||
<Note>
|
||||
权限按<b>岗位</b>授予,不按人:客体三类(数据 / 工具 / 治理功能)× 动作三种(读 / 写 / 治理)。
|
||||
人换岗即换权,权限随岗位走。
|
||||
</Note>
|
||||
<NList>
|
||||
{positions.map((p) => (
|
||||
<NRow
|
||||
key={p.id}
|
||||
tone="brand"
|
||||
ico="key"
|
||||
title={p.name}
|
||||
sub={`岗位编码 ${p.code}`}
|
||||
cols={[{ k: '在岗', v: staff.filter((s) => s.position.id === p.id).length }]}
|
||||
onClick={() => nav.push({ k: 'role', id: p.id })}
|
||||
/>
|
||||
))}
|
||||
</NList>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryGate query={q}>
|
||||
<>
|
||||
<Note>
|
||||
停用前置检查:在途任务与待答卡须先<b>改派完毕</b>——停用不得让在途工作无人认领。
|
||||
</Note>
|
||||
<NList>
|
||||
{staff.map((s) => {
|
||||
const mine = (tasks.data?.tasks ?? []).filter((t) => t.ownerName === s.name && t.terminal === undefined);
|
||||
return (
|
||||
<NRow
|
||||
key={s.id}
|
||||
tone={s.state === 'active' ? 'brand' : 'mute'}
|
||||
ico="user-round"
|
||||
title={s.name}
|
||||
sub={`${s.account} · ${s.position.name}(${s.position.code})`}
|
||||
cols={[{ k: '在途任务', v: mine.length }]}
|
||||
right={
|
||||
<St tone={s.state === 'active' ? 'ok' : 'mute'} ico={s.state === 'active' ? 'circle-check' : 'power'}>
|
||||
{s.state === 'active' ? '在职' : '已停用'}
|
||||
</St>
|
||||
}
|
||||
onClick={() => nav.push({ k: 'emp', id: s.id })}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</NList>
|
||||
</>
|
||||
</QueryGate>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- ③ 工作流管理 ---------------- */
|
||||
|
||||
export function EmpPage({ id, nav }: { id: string; nav: Nav }) {
|
||||
const q = useGovStaff();
|
||||
const tasks = useGovTasks();
|
||||
const s = (q.data?.staff ?? []).find((x) => x.id === id);
|
||||
if (s === undefined) return <Note>员工不存在。</Note>;
|
||||
const mine = (tasks.data?.tasks ?? []).filter((t) => t.ownerName === s.name);
|
||||
const live = mine.filter((t) => t.terminal === undefined);
|
||||
return (
|
||||
<>
|
||||
<PageHead
|
||||
ico="user-round"
|
||||
kicker={`员工 · ${s.account}`}
|
||||
title={s.name}
|
||||
chips={
|
||||
<>
|
||||
<Chip ico="key">
|
||||
{s.position.name} · {s.position.code}
|
||||
</Chip>
|
||||
<Chip tone={s.state === 'active' ? 'ok' : 'mute'}>{s.state === 'active' ? '在职' : '已停用'}</Chip>
|
||||
</>
|
||||
}
|
||||
desc="停用前置检查:在途任务与待答卡须先改派完毕——停用不得让在途工作无人认领。"
|
||||
/>
|
||||
<Card ico="list-checks" title="在途任务" extra={<span className="g-mono">{live.length}</span>}>
|
||||
<NList>
|
||||
{live.map((t) => (
|
||||
<NRow key={t.id} tone="brand" ico="list-checks" title={t.title} onClick={() => nav.push({ k: 'task', id: t.id })} />
|
||||
))}
|
||||
{live.length === 0 && <Clear>无在途任务——可安全停用。</Clear>}
|
||||
</NList>
|
||||
</Card>
|
||||
<Entry ico="key" title="岗位授权" desc={`查看 ${s.position.name} 的授权清单`} onClick={() => nav.push({ k: 'role', id: s.position.id })} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function RolePage({ id }: { id: string }) {
|
||||
const staff = useGovStaff();
|
||||
const grants = useGovGrants(id);
|
||||
const position = (staff.data?.positions ?? []).find((p) => p.id === id);
|
||||
return (
|
||||
<>
|
||||
<PageHead
|
||||
ico="key"
|
||||
kicker="岗位授权"
|
||||
title={position?.name ?? id}
|
||||
chips={<Chip ico="hash">{position?.code ?? '—'}</Chip>}
|
||||
desc="权限按岗位授予,不按人:客体三类(数据 / 工具 / 治理功能)× 动作三种(读 / 写 / 治理)。人换岗即换权。"
|
||||
/>
|
||||
<div className="g-rows">
|
||||
{(grants.data?.grants ?? []).map((g, i) => (
|
||||
<div key={i}>
|
||||
<em>
|
||||
{g.object.kind === 'data'
|
||||
? `数据 · ${g.object.domain}.${g.object.object}`
|
||||
: g.object.kind === 'governance'
|
||||
? `治理功能 · ${GOVERNANCE_FUNCTION_LABEL[g.object.function]}`
|
||||
: `任务 · ${g.object.activityId ?? '全部活动'}`}
|
||||
</em>
|
||||
<b className="mono">{GRANT_ACTION_LABEL[g.action]}</b>
|
||||
</div>
|
||||
))}
|
||||
{(grants.data?.grants ?? []).length === 0 && <Clear>未授予任何权限。</Clear>}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- 流程包 ---------------- */
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* feature `gov-system` 唯一出口(编码规范 §9.1)。
|
||||
* 系统管理:LLM 实例四元绑定、通道实例
|
||||
*/
|
||||
export { SystemHome, LlmPage, ChanPage } from './internal/panel.tsx';
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* feature `gov-system` —— 系统管理:LLM 实例四元绑定、通道实例
|
||||
*
|
||||
* 自 `~/coding/JCAI_PRD/governance/_components/Governance.tsx` 平移:
|
||||
* DOM 结构、类名(`g-*` + 色调 `t-*`)与文案原样保留,mock 常量换成服务端投影(README 平移规则 1/2/4)。
|
||||
* 区首页只负责导航,治理动作一律落在下钻详情页的**就地确认带**(影响预览三必答)。
|
||||
*/
|
||||
import { Card, Chip, NList, NRow, Note, PageHead, QueryGate, Rows, St } from '@web-shared/gov/primitives.tsx';
|
||||
import type { Nav } from '@web-shared/gov/gov-route.ts';
|
||||
import {
|
||||
useGovChannels,
|
||||
useGovLlm,
|
||||
} from '@web-shared/gov/gov-api.ts';
|
||||
|
||||
export type HomeProps = { tab: string; nav: Nav };
|
||||
|
||||
export function SystemHome({ tab, nav }: HomeProps) {
|
||||
const llm = useGovLlm();
|
||||
const channels = useGovChannels();
|
||||
|
||||
if (tab === 'chan') {
|
||||
return (
|
||||
<>
|
||||
<Note>
|
||||
通道实例声明<b>归属键规则</b>与默认认领岗位。归属解析三级逐级短路:会话线程 → 监听键 → 地址窗口;
|
||||
歧义或失败一律判无主——<b>宁交人,不错投</b>。
|
||||
</Note>
|
||||
<NList>
|
||||
{(channels.data?.channels ?? []).map((ch) => (
|
||||
<NRow
|
||||
key={ch.id}
|
||||
tone={ch.enabled ? 'brand' : 'mute'}
|
||||
ico="mail"
|
||||
title={ch.name}
|
||||
sub={ch.address}
|
||||
cols={[
|
||||
{ k: '归属键规则', v: ch.extractCount },
|
||||
{ k: '载荷映射', v: ch.mappingCount },
|
||||
{ k: '地址窗口', v: <span className="g-mono">{Math.round(ch.addressWindowMs / 3600_000)}h</span> },
|
||||
{ k: '默认认领岗位', v: ch.fallbackPosition ?? '—' },
|
||||
]}
|
||||
right={
|
||||
<St tone={ch.enabled ? 'ok' : 'mute'} ico={ch.enabled ? 'circle-check' : 'power'}>
|
||||
{ch.enabled ? '已启用' : '已停用'}
|
||||
</St>
|
||||
}
|
||||
onClick={() => nav.push({ k: 'chan', id: ch.id })}
|
||||
/>
|
||||
))}
|
||||
</NList>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryGate query={llm}>
|
||||
<>
|
||||
<Note>
|
||||
LLM 实例是<b>名称间接层</b>:活动只见自定义模型名,Provider / URL / API-Key / Model 四元在系统侧绑定。
|
||||
密钥与 endpoint 值只存治理台——不入包、不入活动、不入留痕明文。
|
||||
</Note>
|
||||
<NList>
|
||||
{(llm.data?.instances ?? []).map((i) => (
|
||||
<NRow
|
||||
key={i.id}
|
||||
tone={i.enabled ? 'brand' : 'mute'}
|
||||
ico="cpu"
|
||||
title={i.name}
|
||||
sub={`${i.provider} · ${i.model}`}
|
||||
cols={[
|
||||
{ k: 'Base URL', v: <span className="g-mono">{i.baseUrl}</span> },
|
||||
{ k: '系统用途', v: i.systemUse ? '是' : '—' },
|
||||
]}
|
||||
right={
|
||||
<St tone={i.apiKeyBound ? 'ok' : 'warn'} ico={i.apiKeyBound ? 'shield-check' : 'alert-triangle'}>
|
||||
{i.apiKeyBound ? '密钥已绑定' : '密钥未绑定'}
|
||||
</St>
|
||||
}
|
||||
onClick={() => nav.push({ k: 'llm', id: i.id })}
|
||||
/>
|
||||
))}
|
||||
</NList>
|
||||
</>
|
||||
</QueryGate>
|
||||
);
|
||||
}
|
||||
|
||||
export function LlmPage({ id }: { id: string }) {
|
||||
const q = useGovLlm();
|
||||
const i = (q.data?.instances ?? []).find((x) => x.id === id);
|
||||
if (i === undefined) return <Note>实例不存在。</Note>;
|
||||
return (
|
||||
<>
|
||||
<PageHead
|
||||
ico="cpu"
|
||||
kicker={`LLM 实例 · ${i.provider}`}
|
||||
title={i.name}
|
||||
chips={
|
||||
<>
|
||||
<Chip ico="hash">{i.model}</Chip>
|
||||
<Chip tone={i.apiKeyBound ? 'ok' : 'warn'}>{i.apiKeyBound ? '密钥已绑定' : '密钥未绑定'}</Chip>
|
||||
{i.systemUse && <Chip tone="brand">系统用途</Chip>}
|
||||
</>
|
||||
}
|
||||
desc="名称间接层:活动只声明自定义模型名,四元绑定在系统侧。换厂商、换端点、换密钥都不动活动一行代码。"
|
||||
/>
|
||||
<Card ico="settings" title="四元绑定">
|
||||
<Rows
|
||||
items={[
|
||||
{ k: 'Provider', v: i.provider },
|
||||
{ k: 'Base URL', v: i.baseUrl, mono: true },
|
||||
{ k: 'Model', v: i.model, mono: true },
|
||||
{ k: 'API-Key', v: i.apiKeyBound ? '已绑定(密文存储,永不回显)' : '未绑定' },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChanPage({ id }: { id: string }) {
|
||||
const q = useGovChannels();
|
||||
const ch = (q.data?.channels ?? []).find((x) => x.id === id);
|
||||
if (ch === undefined) return <Note>通道不存在。</Note>;
|
||||
return (
|
||||
<>
|
||||
<PageHead
|
||||
ico="mail"
|
||||
kicker={`通道实例 · ${ch.kind}`}
|
||||
title={ch.name}
|
||||
chips={
|
||||
<>
|
||||
<Chip ico="at-sign">{ch.address}</Chip>
|
||||
<Chip tone={ch.enabled ? 'ok' : 'mute'}>{ch.enabled ? '已启用' : '已停用'}</Chip>
|
||||
</>
|
||||
}
|
||||
desc="归属解析三级逐级短路:会话线程 → 监听键 → 地址窗口;歧义或失败一律判无主——宁交人,不错投。"
|
||||
/>
|
||||
<Card ico="settings" title="归属与映射">
|
||||
<Rows
|
||||
items={[
|
||||
{ k: '归属键规则', v: `${ch.extractCount} 条` },
|
||||
{ k: '载荷映射', v: `${ch.mappingCount} 条` },
|
||||
{ k: '地址窗口', v: `${Math.round(ch.addressWindowMs / 3600_000)} 小时`, mono: true },
|
||||
{ k: '默认认领岗位', v: ch.fallbackPosition ?? '—' },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* feature `gov-task` 唯一出口(编码规范 §9.1)。
|
||||
* 任务管理:在途监控、控制事件(暂停/叫停/终止)、恢复、改派、任务日志查看
|
||||
*/
|
||||
export { TasksHome, WfPage, TaskPage, TaskLogPage } from './internal/panel.tsx';
|
||||
@@ -0,0 +1,506 @@
|
||||
/**
|
||||
* feature `gov-task` —— 任务管理:在途监控、控制事件(暂停/叫停/终止)、恢复、改派、任务日志查看
|
||||
*
|
||||
* 自 `~/coding/JCAI_PRD/governance/_components/Governance.tsx` 平移:
|
||||
* DOM 结构、类名(`g-*` + 色调 `t-*`)与文案原样保留,mock 常量换成服务端投影(README 平移规则 1/2/4)。
|
||||
* 区首页只负责导航,治理动作一律落在下钻详情页的**就地确认带**(影响预览三必答)。
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
CONTROL_SEMANTIC_LABEL,
|
||||
TASK_RUNTIME_STATE_LABEL,
|
||||
TERMINAL_STATE_LABEL,
|
||||
WORKFLOW_STATE_LABEL,
|
||||
controlTaskRoute,
|
||||
reassignTaskRoute,
|
||||
resumeTaskRoute,
|
||||
} from '@contracts';
|
||||
import type { ControlSemantic } from '@contracts';
|
||||
import { Ico } from '@web-shared/Ico.tsx';
|
||||
import { call } from '@web-shared/typed-fetch.ts';
|
||||
import { Ava, Card, Chip, Clear, Entry, Facts, Kpi, NList, NRow, Note, PageHead, QueryGate, Sec, St } from '@web-shared/gov/primitives.tsx';
|
||||
import type { ActSpec } from '@web-shared/gov/primitives.tsx';
|
||||
import { APR_ST, PARA, RUNTIME_ST, TERMINAL_ST, WF_ST } from '@web-shared/gov/tone.ts';
|
||||
import type { Nav } from '@web-shared/gov/gov-route.ts';
|
||||
import { FlowFigure } from '@web-shared/gov/flow/FlowFigure.tsx';
|
||||
import {
|
||||
useGovApprovals,
|
||||
useGovStaff,
|
||||
useGovTaskLog,
|
||||
useGovTasks,
|
||||
useGovWorkflow,
|
||||
} from '@web-shared/gov/gov-api.ts';
|
||||
|
||||
export type HomeProps = { tab: string; nav: Nav };
|
||||
|
||||
export function TasksHome({ tab, nav }: HomeProps) {
|
||||
const q = useGovTasks();
|
||||
const approvals = useGovApprovals();
|
||||
const tasks = q.data?.tasks ?? [];
|
||||
const workflows = q.data?.workflows ?? [];
|
||||
|
||||
/** 工作流不设负责人:要看「这条流现在落在谁身上」,读它当前节点的任务负责人 */
|
||||
const holderOf = (workflowId: string) => {
|
||||
const live = tasks.find((t) => t.workflowId === workflowId && t.terminal === undefined);
|
||||
if (live) return { name: live.ownerName, label: '当前任务负责人' };
|
||||
const stuck = tasks.find((t) => t.workflowId === workflowId && t.terminal !== undefined && t.terminal !== 'released');
|
||||
return stuck ? { name: stuck.ownerName, label: '最后经手 · 待处置' } : undefined;
|
||||
};
|
||||
|
||||
if (tab === 'board') {
|
||||
const lanes = [
|
||||
{ k: 'running', tone: 'brand' },
|
||||
{ k: 'waiting', tone: 'warn' },
|
||||
{ k: 'abnormal', tone: 'danger' },
|
||||
{ k: 'completed', tone: 'ok' },
|
||||
] as const;
|
||||
return (
|
||||
<div className="g-board">
|
||||
{lanes.map((ln) => {
|
||||
const items = workflows.filter((w) => w.state === ln.k);
|
||||
return (
|
||||
<div key={ln.k} className={`g-lane t-${ln.tone}`}>
|
||||
<div className="g-lane-h">
|
||||
<i />
|
||||
{WORKFLOW_STATE_LABEL[ln.k]}
|
||||
<b>{items.length}</b>
|
||||
</div>
|
||||
{items.map((w) => {
|
||||
const mine = tasks.filter((t) => t.workflowId === w.id);
|
||||
const done = mine.filter((t) => t.terminal === 'released').length;
|
||||
const total = Math.max(mine.length, 1);
|
||||
const holder = holderOf(w.id);
|
||||
const cur = mine.find((t) => t.terminal === undefined)?.title ?? '—';
|
||||
return (
|
||||
<button key={w.id} className={`g-wf t-${ln.tone}`} onClick={() => nav.push({ k: 'wf', id: w.id })}>
|
||||
<span className="g-wf-top">
|
||||
{w.id.slice(0, 8)}
|
||||
<span className="r">{w.startedAt.slice(5, 16).replace('T', ' ')}</span>
|
||||
</span>
|
||||
<span className="g-wf-t">{w.subject}</span>
|
||||
<span className="g-wf-m">
|
||||
<Ico name="boxes" s={12} />
|
||||
{w.packageId}
|
||||
</span>
|
||||
<span className="g-wf-track">
|
||||
<i style={{ width: `${(done / total) * 100}%` }} />
|
||||
</span>
|
||||
<span className="g-wf-m">
|
||||
<Ico name="circle-dot" s={12} />
|
||||
{cur}
|
||||
<span style={{ marginLeft: 'auto' }} className="g-mono">
|
||||
{done}/{total}
|
||||
</span>
|
||||
</span>
|
||||
<span className="g-wf-m g-wf-hold">
|
||||
{holder ? (
|
||||
<>
|
||||
<Ava name={holder.name} />
|
||||
{holder.name}
|
||||
<em>{holder.label}</em>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Ico name="users" s={12} />
|
||||
无在途任务
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{items.length === 0 && <div className="g-lane-e">—</div>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tab === 'apr') {
|
||||
const rows = approvals.data?.approvals ?? [];
|
||||
return (
|
||||
<>
|
||||
<Note>
|
||||
<b>审批点</b>是流程层的通用人裁节点,与活动、网关、触发器、流程包并列为编排层五要素——它是
|
||||
<b>系统内置要素</b>,声明即用、零开发、不进活动库。它卡到审批人工位产出<b>审批单</b>:通过 /
|
||||
驳回,附依据;没有预算、没有 Harness、没有任务日志,所以不是任务。
|
||||
</Note>
|
||||
<NList>
|
||||
{rows.map((a) => (
|
||||
<NRow
|
||||
key={a.id}
|
||||
tone={APR_ST[a.state].tone}
|
||||
ico="shield-check"
|
||||
title={a.title}
|
||||
sub={`${a.workflowSubject} · ${a.id}`}
|
||||
cols={[
|
||||
{ k: '审批岗位', v: a.approverPosition },
|
||||
{ k: '审批人', v: a.approverName },
|
||||
]}
|
||||
right={
|
||||
<St tone={APR_ST[a.state].tone} ico={APR_ST[a.state].ico}>
|
||||
{APR_ST[a.state].label}
|
||||
</St>
|
||||
}
|
||||
onClick={() => nav.push({ k: 'wf', id: a.workflowId })}
|
||||
/>
|
||||
))}
|
||||
{rows.length === 0 && <Clear>暂无审批单——流上还没有走到审批点。</Clear>}
|
||||
</NList>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (tab === 'list') {
|
||||
return (
|
||||
<>
|
||||
<Note>
|
||||
任务 = 工作流上的节点,一个活动 = 一个任务;它派进员工工作台的<b>任务清单</b>,负责人由节点声明的岗位解析而来。
|
||||
</Note>
|
||||
<NList>
|
||||
{tasks.map((t) => {
|
||||
const st = t.terminal !== undefined ? TERMINAL_ST[t.terminal] : RUNTIME_ST[t.runtime];
|
||||
const label = t.terminal !== undefined ? TERMINAL_STATE_LABEL[t.terminal] : TASK_RUNTIME_STATE_LABEL[t.runtime];
|
||||
return (
|
||||
<NRow
|
||||
key={t.id}
|
||||
tone={PARA[t.paradigm].tone}
|
||||
ico={PARA[t.paradigm].ico}
|
||||
title={t.title}
|
||||
sub={`${t.id.slice(0, 8)} · 活动 ${t.activityId}`}
|
||||
cols={[
|
||||
{ k: '负责人', v: t.ownerName },
|
||||
{
|
||||
k: '步数占用',
|
||||
v: (
|
||||
<span className="g-mono">
|
||||
{t.usedSteps}/{t.capSteps}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{ k: '起始', v: <span className="g-mono">{t.startedAt.slice(5, 16).replace('T', ' ')}</span> },
|
||||
]}
|
||||
right={
|
||||
<St tone={st.tone} ico={st.ico}>
|
||||
{label}
|
||||
</St>
|
||||
}
|
||||
onClick={() => nav.push({ k: 'task', id: t.id })}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{tasks.length === 0 && <Clear>暂无任务——起单后这里会出现。</Clear>}
|
||||
</NList>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const abnormal = workflows.filter((w) => w.state === 'abnormal');
|
||||
const stuck = tasks.filter((t) => t.terminal === 'blocked' || t.terminal === 'exhausted' || t.terminal === 'stalled');
|
||||
return (
|
||||
<QueryGate query={q}>
|
||||
<div className="g-todo">
|
||||
<div className="g-kpis">
|
||||
<Kpi
|
||||
ico="activity"
|
||||
tone="brand"
|
||||
label="在途工作流"
|
||||
value={workflows.filter((w) => w.state === 'running' || w.state === 'waiting').length}
|
||||
sub="运行中 + 等待中"
|
||||
/>
|
||||
<Kpi ico="alert-triangle" tone="danger" label="异常工作流" value={abnormal.length} sub="失败走向未消化,待人工处置" />
|
||||
<Kpi ico="alert-circle" tone="warn" label="任务需处置" value={stuck.length} sub="受阻待人 + 预算耗尽 + 停滞熔断" />
|
||||
<Kpi
|
||||
ico="circle-check"
|
||||
tone="ok"
|
||||
label="放行完成"
|
||||
value={tasks.filter((t) => t.terminal === 'released').length}
|
||||
sub="唯一成功终态"
|
||||
/>
|
||||
</div>
|
||||
{abnormal.map((w) => (
|
||||
<NRow
|
||||
key={w.id}
|
||||
tone="danger"
|
||||
ico="alert-triangle"
|
||||
title={`${w.subject} · 异常`}
|
||||
sub="节点以非完成终态结束且失败走向未消化,按声明为「终止流」,需人工处置。"
|
||||
onClick={() => nav.push({ k: 'wf', id: w.id })}
|
||||
/>
|
||||
))}
|
||||
{stuck.map((t) => (
|
||||
<NRow
|
||||
key={t.id}
|
||||
tone={t.terminal === 'exhausted' ? 'warn' : 'danger'}
|
||||
ico={TERMINAL_ST[t.terminal!].ico}
|
||||
title={`${t.title} · ${TERMINAL_STATE_LABEL[t.terminal!]}`}
|
||||
sub={`${t.id.slice(0, 8)} · 处置 = 改派 / 重开新实例 / 终止`}
|
||||
onClick={() => nav.push({ k: 'task', id: t.id })}
|
||||
/>
|
||||
))}
|
||||
{abnormal.length + stuck.length === 0 && <Clear>无待处置——在途实例全部正常推进。</Clear>}
|
||||
</div>
|
||||
</QueryGate>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- ② 员工管理 ---------------- */
|
||||
|
||||
/** 治理写动作统一入口:成功即失效相关查询,画面由服务端重投影 */
|
||||
function useGovAction<TInput>(mutate: (input: TInput) => Promise<unknown>, keys: readonly string[]) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: mutate,
|
||||
onSuccess: () => {
|
||||
for (const key of keys) void queryClient.invalidateQueries({ queryKey: ['gov', key] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------- 工作流 ---------------- */
|
||||
|
||||
export function WfPage({ id, nav }: { id: string; nav: Nav }) {
|
||||
const workflowId = id.includes(':') ? id.split(':')[0]! : id;
|
||||
const q = useGovWorkflow(workflowId);
|
||||
if (q.data === undefined) return <Note>载入中…</Note>;
|
||||
const { workflow, pkg, instances, tasks } = q.data;
|
||||
const st = WF_ST[workflow.state];
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead
|
||||
ico="workflow"
|
||||
tone={st.tone}
|
||||
kicker={`工作流 · ${workflow.id.slice(0, 8)}`}
|
||||
title={workflow.subject}
|
||||
chips={
|
||||
<>
|
||||
<Chip tone={st.tone} ico={st.ico}>
|
||||
{WORKFLOW_STATE_LABEL[workflow.state]}
|
||||
</Chip>
|
||||
<Chip ico="boxes">
|
||||
{workflow.packageId} v{workflow.packageVersion}
|
||||
</Chip>
|
||||
<Chip ico="zap">
|
||||
来源 · {workflow.source.kind} · {workflow.source.ref}
|
||||
</Chip>
|
||||
</>
|
||||
}
|
||||
desc="工作流状态由它上面的节点(任务与审批单)聚合推导,工作流层不另设状态机;重开产生新的一条工作流,旧的已终、不再入算。"
|
||||
/>
|
||||
|
||||
<Sec ico="git-branch" title="流程图 · 节点实例落位">
|
||||
<FlowFigure pkg={pkg} instances={instances} />
|
||||
</Sec>
|
||||
|
||||
<Sec ico="list-checks" title="流内任务" extra={`${tasks.length} 个`}>
|
||||
<NList>
|
||||
{tasks.map((t) => (
|
||||
<NRow
|
||||
key={t.id}
|
||||
tone="brand"
|
||||
ico="list-checks"
|
||||
title={t.title}
|
||||
sub={`节点 ${t.nodeId} · ${t.id.slice(0, 8)}`}
|
||||
cols={[{ k: '负责人', v: t.ownerName }]}
|
||||
onClick={() => nav.push({ k: 'task', id: t.id })}
|
||||
/>
|
||||
))}
|
||||
{tasks.length === 0 && <Clear>暂无任务。</Clear>}
|
||||
</NList>
|
||||
</Sec>
|
||||
|
||||
<Sec ico="circle-dot" title="节点实例" extra={`${instances.length} 条`}>
|
||||
<div className="g-rows">
|
||||
{instances.map((i) => (
|
||||
<div key={`${i.nodeId}#${i.instance}`}>
|
||||
<em>
|
||||
{pkg.nodes.find((n) => n.id === i.nodeId)?.label ?? i.nodeId}
|
||||
{i.instance > 1 ? ` #${i.instance}` : ''}
|
||||
</em>
|
||||
<b className="mono">
|
||||
{i.terminal !== undefined
|
||||
? TERMINAL_STATE_LABEL[i.terminal]
|
||||
: i.running
|
||||
? '运行中'
|
||||
: i.waiting
|
||||
? '等待中'
|
||||
: i.activated
|
||||
? '已激活'
|
||||
: '未激活'}
|
||||
</b>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Sec>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- 任务:在途监控 · 控制 · 改派 ---------------- */
|
||||
|
||||
export function TaskPage({ id, nav }: { id: string; nav: Nav }) {
|
||||
const q = useGovTasks();
|
||||
const staff = useGovStaff();
|
||||
const task = (q.data?.tasks ?? []).find((t) => t.id === id);
|
||||
const [assignee, setAssignee] = useState<string>('');
|
||||
|
||||
const control = useGovAction(
|
||||
(semantic: ControlSemantic) =>
|
||||
call(controlTaskRoute, { params: { taskId: id }, body: { semantic, reason: '治理台干预' } }),
|
||||
['tasks', 'workflow', 'tasklog'],
|
||||
);
|
||||
const resume = useGovAction(() => call(resumeTaskRoute, { params: { taskId: id } }), ['tasks', 'workflow', 'tasklog']);
|
||||
const reassign = useGovAction(
|
||||
(to: string) => call(reassignTaskRoute, { params: { taskId: id }, body: { to } }),
|
||||
['tasks', 'staff'],
|
||||
);
|
||||
|
||||
if (task === undefined) return <Note>任务不存在或已不在近期窗口内。</Note>;
|
||||
const st = task.terminal !== undefined ? TERMINAL_ST[task.terminal] : RUNTIME_ST[task.runtime];
|
||||
const label = task.terminal !== undefined ? TERMINAL_STATE_LABEL[task.terminal] : TASK_RUNTIME_STATE_LABEL[task.runtime];
|
||||
const live = task.terminal === undefined;
|
||||
const suspended = task.runtime === 'suspended';
|
||||
|
||||
/** 控制三语义:暂停可恢复、叫停归负责人、终止级联作废 */
|
||||
const controlAct = (semantic: ControlSemantic, danger: boolean): ActSpec => ({
|
||||
label: CONTROL_SEMANTIC_LABEL[semantic],
|
||||
ico: semantic === 'terminate' ? 'power' : semantic === 'halt' ? 'hand' : 'pause',
|
||||
danger,
|
||||
disabled: !live,
|
||||
ask: {
|
||||
title: `控制事件 · ${CONTROL_SEMANTIC_LABEL[semantic]}:${task.title}`,
|
||||
note: <Note>控制事件走统一事件信封(四类事件之控制事件),先持久后投递。</Note>,
|
||||
impact: {
|
||||
who:
|
||||
semantic === 'terminate'
|
||||
? '在途任务终止、待答卡与征答消息作废、审批点撤卡(级联)'
|
||||
: semantic === 'halt'
|
||||
? `负责人 ${task.ownerName} —— 挂起并记最重档纠正信号`
|
||||
: '任务挂起,监听与预算冻结',
|
||||
undo: semantic === 'terminate' ? '不可恢复' : '可恢复',
|
||||
when: '确认后即时',
|
||||
},
|
||||
okLabel: `确认${CONTROL_SEMANTIC_LABEL[semantic]}`,
|
||||
onOk: () => control.mutate(semantic),
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead
|
||||
ico={PARA[task.paradigm].ico}
|
||||
tone={st.tone}
|
||||
kicker={`任务 · ${task.id.slice(0, 8)}`}
|
||||
title={task.title}
|
||||
chips={
|
||||
<>
|
||||
<Chip tone={st.tone} ico={st.ico}>
|
||||
{label}
|
||||
</Chip>
|
||||
<Chip tone={PARA[task.paradigm].tone} ico={PARA[task.paradigm].ico}>
|
||||
{PARA[task.paradigm].label}
|
||||
</Chip>
|
||||
<Chip ico="user-round">{task.ownerName}</Chip>
|
||||
</>
|
||||
}
|
||||
desc={PARA[task.paradigm].full + ' —— 治理不改定义:控制只作用于这一次运行。'}
|
||||
acts={[
|
||||
controlAct('pause', false),
|
||||
controlAct('halt', true),
|
||||
controlAct('terminate', true),
|
||||
{
|
||||
label: '恢复',
|
||||
ico: 'play',
|
||||
primary: true,
|
||||
disabled: !suspended,
|
||||
ask: {
|
||||
title: `恢复任务:${task.title}`,
|
||||
note: <Note tone="ok">恢复 = 从挂起点继续;预算与监听解冻,任务记忆只追加、不回滚。</Note>,
|
||||
impact: { who: `负责人 ${task.ownerName}`, undo: '可再次控制', when: '确认后即时' },
|
||||
okLabel: '确认恢复',
|
||||
onOk: () => resume.mutate(undefined),
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Card ico="gauge" title="预算占用" extra={<span className="g-mono">{`${task.usedSteps}/${task.capSteps} 步`}</span>}>
|
||||
<Facts
|
||||
items={[
|
||||
{ k: '步数', v: `${task.usedSteps}/${task.capSteps}`, ratio: task.usedSteps / Math.max(task.capSteps, 1) },
|
||||
{ k: '活动', v: task.activityId },
|
||||
{ k: '范式', v: PARA[task.paradigm].label },
|
||||
{ k: '起始', v: task.startedAt.slice(0, 16).replace('T', ' ') },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Sec ico="user-plus" title="改派">
|
||||
<Note>改派 = 负责人变更;改派留痕并通知双方,任务记忆与草稿层不动。</Note>
|
||||
<div className="g-field">
|
||||
<em>新负责人</em>
|
||||
<div className="g-seg">
|
||||
{(staff.data?.staff ?? [])
|
||||
.filter((s) => s.state === 'active' && s.name !== task.ownerName)
|
||||
.map((s) => (
|
||||
<button key={s.id} className={'g-seg-b' + (assignee === s.id ? ' on' : '')} onClick={() => setAssignee(s.id)}>
|
||||
{s.name} · {s.position.code}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="g-acts-r">
|
||||
<button className="g-btn primary" disabled={assignee === '' || !live} onClick={() => reassign.mutate(assignee)}>
|
||||
<Ico name="user-plus" s={13} />
|
||||
确认改派
|
||||
</button>
|
||||
</div>
|
||||
</Sec>
|
||||
|
||||
<Entry
|
||||
ico="scroll-text"
|
||||
title="任务日志"
|
||||
desc="只追加、信封盖戳、归侧标注 —— 过程即证据"
|
||||
onClick={() => nav.push({ k: 'tasklog', id: task.id })}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function TaskLogPage({ id }: { id: string }) {
|
||||
const q = useGovTaskLog(id);
|
||||
const lines = q.data?.log ?? [];
|
||||
return (
|
||||
<>
|
||||
<PageHead
|
||||
ico="scroll-text"
|
||||
kicker={`任务日志 · ${id.slice(0, 8)}`}
|
||||
title="过程留痕"
|
||||
desc="任务记忆只追加、不修改:每条都带信封戳与归侧标注(机器产出 / 人产出 / 人裁决 / 世界等待)。"
|
||||
/>
|
||||
<div className="g-entries">
|
||||
{lines.map((l) => (
|
||||
<div key={l.id} className="g-entry">
|
||||
<span className="g-entry-i">
|
||||
<Ico name="circle-dot" s={14} />
|
||||
</span>
|
||||
<span className="g-entry-t">
|
||||
<b>{l.title}</b>
|
||||
<em>{l.at.slice(0, 19).replace('T', ' ')}</em>
|
||||
</span>
|
||||
<span className="g-entry-m">
|
||||
<Chip>{l.sideLabel}</Chip>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{lines.length === 0 && <Clear>暂无留痕。</Clear>}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- 员工 / 岗位 ---------------- */
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* feature `gov-tool` 唯一出口(编码规范 §9.1)。
|
||||
* 工具管理:注册、白名单、凭证绑定(只回键,永不回值)
|
||||
*/
|
||||
export { ToolsHome, ToolPage } from './internal/panel.tsx';
|
||||
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* feature `gov-tool` —— 工具管理:注册、白名单、凭证绑定(只回键,永不回值)
|
||||
*
|
||||
* 自 `~/coding/JCAI_PRD/governance/_components/Governance.tsx` 平移:
|
||||
* DOM 结构、类名(`g-*` + 色调 `t-*`)与文案原样保留,mock 常量换成服务端投影(README 平移规则 1/2/4)。
|
||||
* 区首页只负责导航,治理动作一律落在下钻详情页的**就地确认带**(影响预览三必答)。
|
||||
*/
|
||||
import { Ico } from '@web-shared/Ico.tsx';
|
||||
import { Card, Chip, Clear, NList, NRow, Note, PageHead, QueryGate } from '@web-shared/gov/primitives.tsx';
|
||||
import type { Nav } from '@web-shared/gov/gov-route.ts';
|
||||
import {
|
||||
useGovTools,
|
||||
} from '@web-shared/gov/gov-api.ts';
|
||||
|
||||
export type HomeProps = { tab: string; nav: Nav };
|
||||
|
||||
export function ToolsHome({ nav }: HomeProps) {
|
||||
const q = useGovTools();
|
||||
const tools = q.data?.tools ?? [];
|
||||
const reads = tools.filter((t) => t.effect === 'read');
|
||||
const writes = tools.filter((t) => t.effect === 'write');
|
||||
return (
|
||||
<QueryGate query={q}>
|
||||
<>
|
||||
<Note>
|
||||
按<b>效应等级</b>分列:读类过程中自由调用;写类只登记<b>动作意向</b>,放行后统一落地。密钥只存治理台,不入包、不入活动。
|
||||
</Note>
|
||||
{[
|
||||
{ label: '读类 · 过程中自由调用', items: reads, tone: 'brand', ico: 'search' },
|
||||
{ label: '写类 · 登记意向,放行后落地', items: writes, tone: 'danger', ico: 'pen-line' },
|
||||
].map((group) => (
|
||||
<div key={group.label} className="g-sec">
|
||||
<div className="g-band">
|
||||
<Ico name={group.ico} s={13} />
|
||||
{group.label}
|
||||
<span className="x">{group.items.length}</span>
|
||||
</div>
|
||||
<NList>
|
||||
{group.items.map((t) => (
|
||||
<NRow
|
||||
key={t.name}
|
||||
tone={group.tone}
|
||||
ico="wrench"
|
||||
title={t.name}
|
||||
sub={t.description}
|
||||
cols={[
|
||||
{ k: '版本', v: <span className="g-mono">{t.version}</span> },
|
||||
{ k: '连接配置键', v: <span className="g-mono">{t.connectionKeys.join(', ') || '—'}</span> },
|
||||
]}
|
||||
right={t.pure ? <Chip tone="ok">纯工具</Chip> : <Chip tone={group.tone}>{t.effect === 'read' ? '读' : '写'}</Chip>}
|
||||
onClick={() => nav.push({ k: 'tool', id: t.name })}
|
||||
/>
|
||||
))}
|
||||
</NList>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
</QueryGate>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- ⑦ 数据管理 ---------------- */
|
||||
|
||||
export function ToolPage({ id }: { id: string }) {
|
||||
const q = useGovTools();
|
||||
const t = (q.data?.tools ?? []).find((x) => x.name === id);
|
||||
if (t === undefined) return <Note>工具不存在。</Note>;
|
||||
const write = t.effect === 'write';
|
||||
return (
|
||||
<>
|
||||
<PageHead
|
||||
ico="wrench"
|
||||
tone={write ? 'danger' : 'brand'}
|
||||
kicker={`工具 · ${t.name}`}
|
||||
title={t.name}
|
||||
chips={
|
||||
<>
|
||||
<Chip tone={write ? 'danger' : 'brand'}>{write ? '写类' : '读类'}</Chip>
|
||||
{t.pure && <Chip tone="ok">纯工具</Chip>}
|
||||
<Chip ico="hash">v{t.version}</Chip>
|
||||
</>
|
||||
}
|
||||
desc={
|
||||
write
|
||||
? '写类工具在过程中只登记动作意向,不改世界;放行通过后由落地收敛统一执行。'
|
||||
: '读类工具过程中自由调用 —— 读不改世界,故不需放行。'
|
||||
}
|
||||
/>
|
||||
<Card ico="info" title="说明">
|
||||
<p className="g-desc">{t.description}</p>
|
||||
</Card>
|
||||
<Card ico="shield-check" title="连接配置">
|
||||
<Note>凭证隔离:治理台只回<b>配置键</b>,永不回值;密钥不入包、不入活动、不入留痕明文。</Note>
|
||||
<div className="g-rows">
|
||||
{t.connectionKeys.map((k) => (
|
||||
<div key={k}>
|
||||
<em>{k}</em>
|
||||
<b className="mono">已在治理台绑定</b>
|
||||
</div>
|
||||
))}
|
||||
{t.connectionKeys.length === 0 && <Clear>此工具无需外部连接。</Clear>}
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- 数据对象 ---------------- */
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
/**
|
||||
* feature `gov-trigger` 唯一出口(编码规范 §9.1)。
|
||||
* 触发器配置(周期 / 事件 / 登记三类):与流程包平级,启停不改任何版本
|
||||
*/
|
||||
export { TriggersHome, TriggerPage } from './internal/panel.tsx';
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* feature `gov-trigger` —— 触发器配置(周期 / 事件 / 登记三类):与流程包平级,启停不改任何版本
|
||||
*
|
||||
* 自 `~/coding/JCAI_PRD/governance/_components/Governance.tsx` 平移:
|
||||
* DOM 结构、类名(`g-*` + 色调 `t-*`)与文案原样保留,mock 常量换成服务端投影(README 平移规则 1/2/4)。
|
||||
* 区首页只负责导航,治理动作一律落在下钻详情页的**就地确认带**(影响预览三必答)。
|
||||
*/
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import {
|
||||
TRIGGER_KIND_LABEL,
|
||||
toggleTriggerRoute,
|
||||
triggerKindOf,
|
||||
} from '@contracts';
|
||||
import { call } from '@web-shared/typed-fetch.ts';
|
||||
import { Card, Chip, NList, NRow, Note, PageHead, QueryGate, Rows, St } from '@web-shared/gov/primitives.tsx';
|
||||
import type { Nav } from '@web-shared/gov/gov-route.ts';
|
||||
import {
|
||||
useGovTriggers,
|
||||
} from '@web-shared/gov/gov-api.ts';
|
||||
|
||||
export type HomeProps = { tab: string; nav: Nav };
|
||||
|
||||
export function TriggersHome({ nav }: HomeProps) {
|
||||
const q = useGovTriggers();
|
||||
const triggers = q.data?.triggers ?? [];
|
||||
return (
|
||||
<QueryGate query={q}>
|
||||
<>
|
||||
<Note>
|
||||
触发器与流程包<b>平级</b>——不入包结构、不随包版本,增删启停不改任何版本。周期 / 事件 / 登记三类。
|
||||
</Note>
|
||||
<NList>
|
||||
{triggers.map((t) => {
|
||||
const kind = triggerKindOf(t.spec);
|
||||
return (
|
||||
<NRow
|
||||
key={t.id}
|
||||
tone={t.enabled ? 'brand' : 'mute'}
|
||||
ico="zap"
|
||||
title={t.name}
|
||||
sub={`调起 ${t.processPackageId} · ${t.mapping.length} 条起单映射`}
|
||||
cols={[{ k: '类别', v: <Chip tone="brand">{TRIGGER_KIND_LABEL[kind]}</Chip> }]}
|
||||
right={
|
||||
<St tone={t.enabled ? 'ok' : 'mute'} ico={t.enabled ? 'circle-check' : 'power'}>
|
||||
{t.enabled ? '已启用' : '已停用'}
|
||||
</St>
|
||||
}
|
||||
onClick={() => nav.push({ k: 'trigger', id: t.id })}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</NList>
|
||||
</>
|
||||
</QueryGate>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- ⑤ 活动库管理 ---------------- */
|
||||
|
||||
/** 治理写动作统一入口:成功即失效相关查询,画面由服务端重投影 */
|
||||
function useGovAction<TInput>(mutate: (input: TInput) => Promise<unknown>, keys: readonly string[]) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: mutate,
|
||||
onSuccess: () => {
|
||||
for (const key of keys) void queryClient.invalidateQueries({ queryKey: ['gov', key] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------- 工作流 ---------------- */
|
||||
|
||||
export function TriggerPage({ id }: { id: string }) {
|
||||
const q = useGovTriggers();
|
||||
const t = (q.data?.triggers ?? []).find((x) => x.id === id);
|
||||
const toggle = useGovAction(
|
||||
(enabled: boolean) => call(toggleTriggerRoute, { params: { id }, body: { enabled } }),
|
||||
['triggers'],
|
||||
);
|
||||
if (t === undefined) return <Note>触发器不存在。</Note>;
|
||||
return (
|
||||
<>
|
||||
<PageHead
|
||||
ico="zap"
|
||||
tone={t.enabled ? 'brand' : 'mute'}
|
||||
kicker={`触发器 · ${t.id}`}
|
||||
title={t.name}
|
||||
chips={
|
||||
<>
|
||||
<Chip tone="brand">{TRIGGER_KIND_LABEL[triggerKindOf(t.spec)]}</Chip>
|
||||
<Chip ico="boxes">调起 {t.processPackageId}</Chip>
|
||||
</>
|
||||
}
|
||||
desc="触发器与流程包平级——不入包结构、不随包版本,增删启停不改任何版本。"
|
||||
acts={[
|
||||
{
|
||||
label: t.enabled ? '停用' : '启用',
|
||||
ico: 'power',
|
||||
danger: t.enabled,
|
||||
ask: {
|
||||
title: `${t.enabled ? '停用' : '启用'}触发器:${t.name}`,
|
||||
note: <Note>启停不改任何版本;已起的单不受影响。</Note>,
|
||||
impact: {
|
||||
who: t.enabled ? '此后不再自动起单,已在途工作流不受影响' : '此后按规则自动起单',
|
||||
undo: '可随时反向切换',
|
||||
when: '确认后即时',
|
||||
},
|
||||
okLabel: t.enabled ? '确认停用' : '确认启用',
|
||||
onOk: () => toggle.mutate(!t.enabled),
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Card ico="settings" title="起单规则">
|
||||
<Rows
|
||||
items={[
|
||||
{ k: '类别', v: TRIGGER_KIND_LABEL[triggerKindOf(t.spec)] },
|
||||
{ k: '规格', v: JSON.stringify(t.spec), mono: true },
|
||||
{ k: '流程包', v: t.processPackageId, mono: true },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
<Card ico="arrow-left-right" title="起单映射" extra={<span className="g-mono">{t.mapping.length}</span>}>
|
||||
<div className="g-rows">
|
||||
{t.mapping.map((m, i) => (
|
||||
<div key={i}>
|
||||
<em>{m.target}</em>
|
||||
<b className="mono">{JSON.stringify(m.source)}</b>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- 活动 / 工具 ---------------- */
|
||||
|
||||
Reference in New Issue
Block a user