forked from ChaiHao/BPOS-MVP
**运营 · 数据(活动记忆四层)整个撤掉** —— 记忆的放行口还没成形,摆一个空区不如
不摆;真要做时随「数据资产」一并重开。区表、面归属、shell 映射、KnowledgeHome /
MemPage、活动详情里那枚入口一并清干净;旧深链 `#/gov/memory/layers` 落运营首页
(测试跟着改口径,不留死链)。运营中心自此六区。
**流程包首页**:
· 「编排要素:四种」这枚计数删了 —— 网关早废,它是句没人用的口号;
· 卡片重做。从前是「头 + 芯片行 + 一大片空 + 脚注」,158 高全靠留白撑;
现在两段:头(图标 · 名 · 标识 · 生效版 · ⋯)+ **数据板**(活动/审批/边闭合/版本
四格等宽、竖线分隔)。段段有内容、段间有线,高矮由内容说了算 —— 实测 111 高;
· 卡上 **⋯ 菜单**:编辑(就地改名,落在卡面那一版上)/ 删除(整条流,菜单原地翻成
确认,不另弹窗);
· **新建按钮变幽灵卡**:虚线卡住在货架末位,与真卡同宽同高(111/111)。
删整条流是新能力:`DELETE /api/gov/process/:id` + `process.deleteFlow`。
生效版这道闸不设(明示动作,不是手滑),在途单那道照设(PinnedVersionError → 409);
**「还有调起规则指着它」由路由层反查** —— process 是 L7,不该知道 trigger 的存在。
实测(丁子乔账号):改名往返、删除被 2 条调起规则如实挡回、幽灵卡建 `tmp-drop`
再删掉整条流,库里回到只剩 unified-quote v4,试跑数据已清。五闸绿。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
342 lines
13 KiB
TypeScript
342 lines
13 KiB
TypeScript
/**
|
||
* feature `gov-activity` —— 活动库:两维分类 × 包版本 × 资产版本
|
||
*
|
||
* 自 `~/coding/JCAI_PRD/governance/_components/Governance.tsx` 平移:
|
||
* DOM 结构、类名(`g-*` + 色调 `t-*`)与文案原样保留,mock 常量换成服务端投影(README 平移规则 1/2/4)。
|
||
* 区首页只负责导航,治理动作一律落在下钻详情页的**就地确认带**(影响预览三必答)。
|
||
*/
|
||
import { Ico } from '@web-shared/Ico.tsx';
|
||
import { useState } from 'react';
|
||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||
import { installActivityRoute } from '@contracts';
|
||
import { ApiCallError, call } from '@web-shared/typed-fetch.ts';
|
||
import {
|
||
Acts,
|
||
Card,
|
||
Chip,
|
||
Clear,
|
||
Field,
|
||
Note,
|
||
PageHead,
|
||
Prop,
|
||
QueryGate,
|
||
St,
|
||
} from '@web-shared/gov/primitives.tsx';
|
||
|
||
/** 装入回参 —— 与 `installActivityRoute.output` 同形 */
|
||
type InstallReply = {
|
||
accepted: boolean;
|
||
activityId: string;
|
||
packageVersion: string;
|
||
staticFindings: ReadonlyArray<{ code: string; severity: string; message: string }>;
|
||
replay: {
|
||
oldPassStillPass: boolean;
|
||
checked: number;
|
||
skippedNoHistory: boolean;
|
||
regressions: ReadonlyArray<{ taskId: string; detail: string }>;
|
||
};
|
||
};
|
||
import { PARA } from '@web-shared/gov/tone.ts';
|
||
import type { Nav } from '@web-shared/gov/gov-route.ts';
|
||
import { useGovActivities } 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>:编排引用一个活动,运行时就长出一个任务。业务分类 × 活动类型两维平级;
|
||
每活动两条版本序列——包版本走安装两审,资产版本走进化管线。
|
||
<b>审批点与网关不在这里</b>:它们是流程层的系统内置要素,零开发、不入库。
|
||
</Note>
|
||
<InstallCard />
|
||
{activities.length === 0 ? (
|
||
<Clear>货架是空的 —— 装一个活动进来,编排里才引用得到它。</Clear>
|
||
) : (
|
||
<div className="g-shelf">
|
||
{activities.map((a) => {
|
||
const para = PARA[a.paradigm];
|
||
const latest = a.packageVersions[a.packageVersions.length - 1];
|
||
const effective = a.assetVersions.filter((v) => v.effective).length;
|
||
return (
|
||
<button
|
||
key={a.activityId}
|
||
className={'g-tile t-' + para.tone}
|
||
onClick={() => nav.push({ k: 'act', id: a.activityId })}
|
||
>
|
||
<span className="g-tile-h">
|
||
<span className="g-tile-i">
|
||
<Ico name={para.ico} s={16} />
|
||
</span>
|
||
<span className="g-tile-t">
|
||
<b>{a.name}</b>
|
||
<em>{a.activityId}</em>
|
||
</span>
|
||
<Chip tone={para.tone}>{para.label}</Chip>
|
||
</span>
|
||
<span className="g-tile-m">
|
||
<Chip ico="boxes">{a.category}</Chip>
|
||
<Chip ico="git-commit">包 v{latest?.version ?? '—'}</Chip>
|
||
</span>
|
||
<span className="g-tile-f">
|
||
{/*
|
||
只报查得到的事实:资产生效版数与上架状态。
|
||
「实现到没到位」这一层,清单里没有可靠依据 —— 不在卡上编一个章。
|
||
*/}
|
||
<St tone={a.enabled ? 'ok' : 'mute'} ico={a.enabled ? 'circle-check' : 'power'}>
|
||
{a.enabled ? '已上架' : '已停用'}
|
||
</St>
|
||
<span className="r">资产生效 {effective}</span>
|
||
</span>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</>
|
||
</QueryGate>
|
||
);
|
||
}
|
||
|
||
/* ---------------- ⑥ 工具管理 ---------------- */
|
||
|
||
export function ActPage({ id }: { id: string }) {
|
||
const q = useGovActivities();
|
||
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="file-text" title="契约面 · 开发它要照着的" extra={`调人预算 ${a.contract.humanCallBudget} 次`}>
|
||
<Prop ico="arrow-left-right" title="入参" extra={`${a.contract.inputFields.length} 个字段`}>
|
||
{a.contract.inputFields.length === 0 ? (
|
||
<Clear>不吃结构化入参 —— 只凭委托消息启动。</Clear>
|
||
) : (
|
||
<div className="g-rows">
|
||
{a.contract.inputFields.map((f) => (
|
||
<div key={f.name}>
|
||
<em>
|
||
{f.name}
|
||
{f.required && <Chip tone="warn">必填</Chip>}
|
||
</em>
|
||
<b className="mono">{f.type}</b>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</Prop>
|
||
<Prop ico="send" title="输出口" extra="口名 + 载荷字段即下游可衔接的全部">
|
||
<div className="g-rows">
|
||
{a.contract.outputPorts.map((port) => (
|
||
<div key={port.name}>
|
||
<em>{port.name}</em>
|
||
<b className="mono">{port.fields.join(' · ') || '(无字段)'}</b>
|
||
</div>
|
||
))}
|
||
{a.contract.outputPorts.length === 0 && <Clear>没有声明输出口 —— 下游无从衔接,发布会被拦。</Clear>}
|
||
</div>
|
||
</Prop>
|
||
<Prop ico="wrench" title="声明的依赖" extra="授权在白名单与契约,不在活动代码">
|
||
<div className="g-rows">
|
||
<div>
|
||
<em>工具</em>
|
||
<b className="mono">{a.contract.tools.join(' · ') || '(不调工具)'}</b>
|
||
</div>
|
||
<div>
|
||
<em>模型</em>
|
||
<b className="mono">{a.contract.models.join(' · ') || '(不调模型)'}</b>
|
||
</div>
|
||
<div>
|
||
<em>岗位</em>
|
||
<b className="mono">{a.contract.roles.join(' · ') || '(未声明)'}</b>
|
||
</div>
|
||
<div>
|
||
<em>域对象</em>
|
||
<b className="mono">{a.contract.objects.join(' · ') || '(不读写台账)'}</b>
|
||
</div>
|
||
</div>
|
||
</Prop>
|
||
</Card>
|
||
<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>
|
||
</>
|
||
);
|
||
}
|
||
|
||
/* ---------------- 装入 / 换版(安装通道) ---------------- */
|
||
|
||
/**
|
||
* 开发完一个活动,从这里装进来 —— **无代码通道**(ADR-004):交的是清单 + .js 产物,都是数据。
|
||
*
|
||
* 两审在服务端跑:静态审读清单,回放审重演历史(首装无历史即免)。
|
||
* 恒为新增、版本并存:装入不覆盖旧版本,在途任务按其启动时版本跑完。
|
||
*/
|
||
function InstallCard() {
|
||
const queryClient = useQueryClient();
|
||
const [open, setOpen] = useState(false);
|
||
const [manifestText, setManifestText] = useState('');
|
||
const [artifact, setArtifact] = useState('');
|
||
|
||
const install = useMutation({
|
||
mutationFn: () =>
|
||
call<InstallReply>(installActivityRoute, {
|
||
body: { manifest: JSON.parse(manifestText) as unknown, artifact },
|
||
}),
|
||
onSuccess: (out) => {
|
||
if (out.accepted) {
|
||
void queryClient.invalidateQueries({ queryKey: ['gov', 'activities'] });
|
||
setOpen(false);
|
||
}
|
||
},
|
||
});
|
||
|
||
const pick = (setter: (text: string) => void) => async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const file = e.target.files?.[0];
|
||
if (file !== undefined) setter(await file.text());
|
||
};
|
||
|
||
/** 两审未过的结论:412 走的是 ApiCallError,详情在 detail 里 */
|
||
const rejected =
|
||
install.error instanceof ApiCallError && install.error.status === 412
|
||
? (install.error.detail as InstallReply | undefined)
|
||
: install.data?.accepted === false
|
||
? install.data
|
||
: undefined;
|
||
|
||
if (!open) {
|
||
return (
|
||
<Card
|
||
ico="package"
|
||
title="装入 / 换版"
|
||
extra={
|
||
<button className="g-btn primary" onClick={() => setOpen(true)}>
|
||
装一个活动
|
||
</button>
|
||
}
|
||
>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<Card ico="package" title="装入 / 换版">
|
||
<Prop ico="file-text" title="清单 manifest.json" extra="身份 / 七件契约 / 声明 / 资产初始版 / 样例集 / 代码入口">
|
||
<Field k="选文件" hint="活动包 dist/manifest.json">
|
||
<input className="g-input" type="file" accept=".json,application/json" onChange={pick(setManifestText)} />
|
||
</Field>
|
||
<Field k="或直接粘贴">
|
||
<textarea
|
||
className="g-input"
|
||
rows={4}
|
||
value={manifestText}
|
||
placeholder='{ "identity": { "activityId": "rfq-intake", … } }'
|
||
onChange={(e) => setManifestText(e.target.value)}
|
||
/>
|
||
</Field>
|
||
</Prop>
|
||
|
||
<Prop ico="terminal" title="产物 index.js" extra="只当数据存 —— 不 import、不 eval">
|
||
<Field k="选文件" hint="活动包 dist/index.js">
|
||
<input className="g-input" type="file" accept=".js,.mjs,text/javascript" onChange={pick(setArtifact)} />
|
||
</Field>
|
||
<Field k="或直接粘贴">
|
||
<textarea
|
||
className="g-input"
|
||
rows={3}
|
||
value={artifact}
|
||
placeholder="export default defineActivity({ … })"
|
||
onChange={(e) => setArtifact(e.target.value)}
|
||
/>
|
||
</Field>
|
||
</Prop>
|
||
|
||
{rejected !== undefined && (
|
||
<Note tone="warn">
|
||
<b>两审未过,没有上架。</b>
|
||
{rejected.staticFindings.length > 0 && (
|
||
<> 静态审:{rejected.staticFindings.map((f) => f.message).join(';')}。</>
|
||
)}
|
||
{rejected.replay.regressions.length > 0 && (
|
||
<> 回放审:{rejected.replay.regressions.map((r) => `${r.taskId} ${r.detail}`).join(';')}。</>
|
||
)}
|
||
</Note>
|
||
)}
|
||
{install.data?.accepted === true && (
|
||
<Note tone="ok">
|
||
已装入 {install.data.activityId} v{install.data.packageVersion} —— 静态审{' '}
|
||
{install.data.staticFindings.length} 条提示;回放审
|
||
{install.data.replay.skippedNoHistory ? '免(无历史)' : `查了 ${install.data.replay.checked} 单`}。
|
||
</Note>
|
||
)}
|
||
|
||
<Acts
|
||
align="end"
|
||
items={[
|
||
{ label: '取消', ico: 'x', onClick: () => setOpen(false) },
|
||
{
|
||
label: install.isPending ? '两审中…' : '装入并送审',
|
||
ico: 'circle-check',
|
||
primary: true,
|
||
disabled: install.isPending || manifestText.trim() === '' || artifact.trim() === '',
|
||
ask: {
|
||
title: '经安装通道装入活动包',
|
||
note: <Note>无代码通道:交的是数据。两审过了才上架;不过审会把是哪一条拦的说清楚。</Note>,
|
||
impact: {
|
||
who: '此后新起的任务用新包版本;在途任务按启动时版本跑完',
|
||
undo: '版本并存,可停用回退',
|
||
when: '确认后当场跑两审',
|
||
},
|
||
okLabel: '确认装入',
|
||
onOk: () => install.mutate(),
|
||
},
|
||
},
|
||
]}
|
||
/>
|
||
</Card>
|
||
);
|
||
}
|