/** * 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 ( <> 活动是任务的定义:编排引用一个活动,运行时就长出一个任务。业务分类 × 活动类型两维平级; 每活动两条版本序列——包版本走安装两审,资产版本走进化管线。 审批点与网关不在这里:它们是流程层的系统内置要素,零开发、不入库。 {activities.length === 0 ? ( 货架是空的 —— 装一个活动进来,编排里才引用得到它。 ) : ( {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 ( nav.push({ k: 'act', id: a.activityId })} > {a.name} {a.activityId} {para.label} {a.category} 包 v{latest?.version ?? '—'} {/* 只报查得到的事实:资产生效版数与上架状态。 「实现到没到位」这一层,清单里没有可靠依据 —— 不在卡上编一个章。 */} {a.enabled ? '已上架' : '已停用'} 资产生效 {effective} ); })} )} > ); } /* ---------------- ⑥ 工具管理 ---------------- */ export function ActPage({ id }: { id: string }) { const q = useGovActivities(); const a = (q.data?.activities ?? []).find((x) => x.activityId === id); if (a === undefined) return 活动不存在。; return ( <> {PARA[a.paradigm].label} {a.category} {a.enabled ? '已启用' : '已停用'} > } desc={PARA[a.paradigm].full + ' —— 活动是系统外开发、经此安装的外装件。'} /> {/* 契约面 —— 开发这个活动要照着的东西:吃什么、吐什么、用谁的工具、动哪些域对象。 它排在版本谱系前面:先看清契约,再看装了哪几版。 */} {a.contract.inputFields.length === 0 ? ( 不吃结构化入参 —— 只凭委托消息启动。 ) : ( {a.contract.inputFields.map((f) => ( {f.name} {f.required && 必填} {f.type} ))} )} {a.contract.outputPorts.map((port) => ( {port.name} {port.fields.join(' · ') || '(无字段)'} ))} {a.contract.outputPorts.length === 0 && 没有声明输出口 —— 下游无从衔接,发布会被拦。} 工具 {a.contract.tools.join(' · ') || '(不调工具)'} 模型 {a.contract.models.join(' · ') || '(不调模型)'} 岗位 {a.contract.roles.join(' · ') || '(未声明)'} 域对象 {a.contract.objects.join(' · ') || '(不读写台账)'} {a.packageVersions.map((p) => ( v{p.version} {p.state} · {p.installedAt.slice(0, 16).replace('T', ' ')} ))} {a.assetVersions.map((v, i) => ( {v.kind} v{v.version} {v.effective ? '生效中' : '历史'} ))} {a.assetVersions.length === 0 && 尚无资产版本。} > ); } /* ---------------- 装入 / 换版(安装通道) ---------------- */ /** * 开发完一个活动,从这里装进来 —— **无代码通道**(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(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) => { 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 ( setOpen(true)}> 装一个活动 } > ); } return ( setManifestText(e.target.value)} /> setArtifact(e.target.value)} /> {rejected !== undefined && ( 两审未过,没有上架。 {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(';')}。> )} )} {install.data?.accepted === true && ( 已装入 {install.data.activityId} v{install.data.packageVersion} —— 静态审{' '} {install.data.staticFindings.length} 条提示;回放审 {install.data.replay.skippedNoHistory ? '免(无历史)' : `查了 ${install.data.replay.checked} 单`}。 )} setOpen(false) }, { label: install.isPending ? '两审中…' : '装入并送审', ico: 'circle-check', primary: true, disabled: install.isPending || manifestText.trim() === '' || artifact.trim() === '', ask: { title: '经安装通道装入活动包', note: 无代码通道:交的是数据。两审过了才上架;不过审会把是哪一条拦的说清楚。, impact: { who: '此后新起的任务用新包版本;在途任务按启动时版本跑完', undo: '版本并存,可停用回退', when: '确认后当场跑两审', }, okLabel: '确认装入', onOk: () => install.mutate(), }, }, ]} /> ); }