/** * feature `gov-process` —— 流程包(设计态):**一条流一个包,版本收在包内部**。 * * 台面三页签:总览 · 流程包 · 触发器。 * 货架一流程一卡(不按版本平铺);进详情后左 1/3 是版本列表、右 2/3 是流程图, * 图下是节点属性,再往下才是声明式的契约细节(调用接口 · 边映射 · 触发器)。 * * 版本是**运行时开关**,不是发布流程: * · 画图不检查:保存 = 存为新版,**不生效**;激活才跑闭合校验(异常清零方可) * · 一条流恰一个生效版本;可把生效切回旧版,也可全部停用让整条流下线 * · 在途工作流按启动时钉住的版本跑完 —— 启停对它们零影响 */ import { useRef, useState, useEffect } from 'react'; import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query'; import type { ClosureReport, ProcessPackage, ProcessNode } from '@contracts'; import { processPackageSchema, saveProcessRoute, setProcessStateRoute, validateProcessRoute } from '@contracts'; import { call } from '@web-shared/typed-fetch.ts'; import { Ico } from '@web-shared/Ico.tsx'; import type { AskSpec } from '@web-shared/gov/primitives.tsx'; import { Acts, AskBand, Card, Chip, Field, Kpi, ListEdit, Note, PageHead, Prop, QueryGate, Sec, Seg, St, } from '@web-shared/gov/primitives.tsx'; import type { Nav } from '@web-shared/gov/gov-route.ts'; import { useGovActivities, useGovPackages, useGovPkgReports, useGovStaff } from '@web-shared/gov/gov-api.ts'; import type { GovActivities } from '@web-shared/gov/gov-api.ts'; import { FlowFigureBlock } from '@web-shared/gov/flow/FlowFigure.tsx'; import { packageToGraph } from '@web-shared/gov/flow/pkg-to-graph.ts'; import { mergeGraphIntoPackage } from '@web-shared/gov/flow/graph-to-pkg.ts'; import type { FlowEdge, FlowGraph, FlowNode } from '@web-shared/gov/flow/flow-layout.ts'; import type { SpPosition } from '@web-shared/gov/flow/support.tsx'; import { TriggerRulesHome } from '@features/gov-trigger'; export type HomeProps = { tab: string; nav: Nav }; /* ── 报告:画图即校验,列表与详情同一来源 ─────────────────────────────── */ /** 边闭合数:边总数 − 有违规的边(同一条边多处违规只记一次) */ function edgeCheckOf(pkg: ProcessPackage, report: ClosureReport | undefined) { const total = pkg.edges.length; if (report === undefined) return { closed: total, total, issue: undefined }; const badEdges = new Set(report.violations.filter((v) => v.edgeId !== undefined).map((v) => v.edgeId)); const issue = report.violations[0]?.message; return { closed: total - badEdges.size, total, issue }; } /** 一条流的版本按新→旧;生效版恰一个(或零个 —— 整条流下线) */ function flowsOf(packages: readonly ProcessPackage[]) { const byId = new Map(); for (const p of packages) byId.set(p.id, [...(byId.get(p.id) ?? []), p]); return [...byId.entries()].map(([id, versions]) => { const sorted = [...versions].sort((a, b) => b.version - a.version); return { id, versions: sorted, active: sorted.find((v) => v.state === 'active') }; }); } /* ── 边属性:传递契约(条件 + 映射)──────────────────────────────────── */ /** 卡点 id → 去向节点 id(边属性按语义对端认亲:A5→c_reject 即 A5→A4) */ function calloutToOf(graph: FlowGraph | null): Map | undefined { if (graph === null) return undefined; return new Map( graph.nodes.filter((n) => n.kind === 'callout' && n.to !== undefined).map((n) => [n.id, n.to!]), ); } /** 映射行的工作形状 —— 语义边 mapping 的元素;设计者只见三种来源,其余是系统隐性上下文 */ type MapRow = { target: string; source: Record & { from: string } }; const SOURCE_KINDS = [ { k: 'output-port', label: '上游输出口' }, { k: 'initiation', label: '起单载荷' }, { k: 'constant', label: '常量' }, ] as const; function EdgeProps({ edge, toSem, onPatch, }: { edge: FlowEdge; toSem: string; onPatch: (f: (e: FlowEdge) => FlowEdge) => void; }) { const rows = (edge.mapping ?? []) as MapRow[]; const setRows = (next: MapRow[]) => onPatch((e) => ({ ...e, mapping: next })); const patchRow = (i: number, f: (r: MapRow) => MapRow) => setRows(rows.map((r, j) => (j === i ? f(r) : r))); return (
onPatch((x) => ({ ...x, condition: e.target.value }))} /> onPatch((x) => { const { label: _drop, ...rest } = x; return e.target.value === '' ? rest : { ...rest, label: e.target.value }; }) } /> {rows.map((r, i) => { const known = SOURCE_KINDS.some((k) => k.k === r.source.from); return (
patchRow(i, (x) => ({ ...x, target: e.target.value }))} /> ← {known ? ( ) : ( // workflow-var / expression 等是系统侧来源 —— 保留展示,不给编辑入口 {String(r.source.from)}(系统) )} {r.source.from === 'output-port' && ( <> patchRow(i, (x) => ({ ...x, source: { ...x.source, port: e.target.value } }))} /> patchRow(i, (x) => ({ ...x, source: { ...x.source, path: e.target.value } }))} /> )} {r.source.from === 'initiation' && ( patchRow(i, (x) => ({ ...x, source: { ...x.source, path: e.target.value } }))} /> )} {r.source.from === 'constant' && ( patchRow(i, (x) => ({ ...x, source: { ...x.source, value: e.target.value } }))} /> )}
); })} setRows([...rows, { target: '', source: { from: 'output-port', port: '', path: '' } }]), }, ]} />
); } /* ---------------- 台面:总览 · 流程包 · 触发器 ---------------- */ export function WorkflowsHome({ tab, nav }: HomeProps) { // 触发器的**定义面**:谁调起这条流,与流程包一处维护(启停在运营中心) if (tab === 'rules') return ; if (tab === 'pkgs') return ; return ; } function OverviewHome() { const q = useGovPackages(); const packages = q.data?.packages ?? []; const reports = useGovPkgReports(packages); const flows = flowsOf(packages); const totalEdges = packages.reduce((a, p) => a + p.edges.length, 0); const closedEdges = packages.reduce( (a, p) => a + edgeCheckOf(p, reports.data?.get(`${p.id}@${p.version}`)).closed, 0, ); return (
{/* 网关已废除:分支 = 条件出边,不再是一种要素 */}
画图不检查,激活才查完整:随手画的都存得下(存出来不生效),异常清零后激活, 即刻对新起的工作流生效;在途的按启动时版本跑完。要回退,把生效切回旧版本 —— 一条流一次只有一个生效版本。
); } /* ---------------- 货架:一条流一张卡 + 新建流程 ---------------- */ /** * 新建的流程 = 一张**最小可生效**的图:开始 → 结束。 * 激活要过闭合校验,这张最小图天然闭合,建完即激活 —— 建出来就能起单 * (起了直接走完,没业务意义,但机制是通的);进详情画上活动再存,即成下一版。 */ function minimalFlow(id: string, name: string): ProcessPackage { return processPackageSchema.parse({ id, name, version: 1, state: 'active', callInterface: { input: { type: 'object' }, output: { type: 'object' } }, nodes: [ { id: 'start', kind: 'start', label: '开始', layout: { row: 0, col: 0, x: 30, y: 70 } }, { id: 'end', kind: 'end', label: '结束', layout: { row: 0, col: 1, x: 150, y: 70 } }, ], edges: [{ id: 'e-start-end', from: 'start', to: 'end', mapping: [], reopen: false }], variables: [], }); } function FlowShelf({ nav }: { nav: Nav }) { const q = useGovPackages(); const queryClient = useQueryClient(); const packages = q.data?.packages ?? []; const reports = useGovPkgReports(packages); const flows = flowsOf(packages); const [creating, setCreating] = useState(false); const [nfName, setNfName] = useState(''); const [nfId, setNfId] = useState(''); const [nfError, setNfError] = useState(undefined); // 标识进 URL 和触发器规则里引用,所以收紧:小写字母开头,只许小写/数字/连字符 const idOk = /^[a-z][a-z0-9-]{1,40}$/.test(nfId); const idTaken = flows.some((f) => f.id === nfId); const create = useMutation({ /* * 保存不生效(存出来一律 retired)—— 新建要兑现「建出来就能起单」, * 得跟一记激活。最小图天然闭合,激活闸必过;真有 412 就让它抛,onError 如实报。 */ mutationFn: async () => { const out = await call<{ pkg: ProcessPackage; report: ClosureReport }>(saveProcessRoute, { body: { pkg: minimalFlow(nfId, nfName.trim()) }, }); await call<{ packages: ProcessPackage[] }>(setProcessStateRoute, { params: { id: out.pkg.id, version: String(out.pkg.version) }, body: { state: 'active' }, }); return out; }, onSuccess: (out) => { void queryClient.invalidateQueries({ queryKey: ['gov', 'process'] }); setCreating(false); setNfName(''); setNfId(''); nav.push({ k: 'pkg', id: out.pkg.id }); }, onError: (e: unknown) => setNfError((e as { message?: string }).message ?? '建流程失败'), }); return ( {creating ? (
setNfName(e.target.value)} /> setNfId(e.target.value.trim())} /> {nfId !== '' && !idOk && 标识不合规:小写字母开头,只许小写、数字、连字符,2–41 位。} {idTaken && 这个标识已有流程在用 —— 换一个,或直接进那条流改。} {nfError !== undefined && {nfError}} setCreating(false) }, { label: create.isPending ? '建流程中…' : '建流程并进入', ico: 'plus', primary: true, disabled: create.isPending || !idOk || idTaken || nfName.trim() === '', onClick: () => create.mutate(), }, ]} />
) : ( setCreating(true) }]} /> )}
{flows.map((f) => { /* 卡面统计取生效版;整条流下线时取最新版(看的还是定义,只是没在跑) */ const shown = f.active ?? f.versions[0]!; const ec = edgeCheckOf(shown, reports.data?.get(`${shown.id}@${shown.version}`)); const c = { activity: shown.nodes.filter((n) => n.kind === 'activity').length, approval: shown.nodes.filter((n) => n.kind === 'approval').length, }; return ( ); })}
); } /* ---------------- 流程详情:版本列表 + 画布 + 节点属性 + 契约细节 ---------------- */ export function PkgPage({ id, nav }: { id: string; nav: Nav }) { // 旧深链是 `id@version`;详情现在是**流程级**的,版本在页内切,所以只认 id 段 const flowId = id.split('@')[0] ?? id; const q = useGovPackages(); const acts = useGovActivities(); const staffQ = useGovStaff(); const versions = (q.data?.packages ?? []) .filter((p) => p.id === flowId) .sort((a, b) => b.version - a.version); /* 全部版本的闭合报告 —— 版本行的「激活」按钮据它灰显(不闭合点不了) */ const reports = useGovPkgReports(versions); /* 选中哪一版看图:默认跟着生效版走;手动点过就钉住;保存后钉到**刚存的新版**(它未生效) */ const [pinned, setPinned] = useState(null); const active = versions.find((v) => v.state === 'active'); const shown = (pinned === null ? undefined : versions.find((v) => v.version === pinned)) ?? active ?? versions[0]; if (shown === undefined) { return ( 流程不存在。 ); } return ( ({ id: p.id, name: p.name, abbr: p.code }))} nextVersion={Math.max(...versions.map((x) => x.version)) + 1} closedOf={(v) => reports.data?.get(`${flowId}@${v}`)?.closed} onPick={(v) => setPinned(v)} onSaved={(v) => setPinned(v)} /> ); } /** 本页要用的活动字段面 —— 与 `GovActivitiesOutput` 的一致子集(含契约面,导出依赖文件要它) */ type ActivityRow = { activityId: string; name: string; paradigm: string; packageVersions: ReadonlyArray<{ version: string }>; assetVersions: ReadonlyArray<{ version: number; effective: boolean }>; contract: GovActivities['activities'][number]['contract']; }; function FlowLab({ pkg, versions, activeVersion, nav, activities, positions, nextVersion, closedOf, onPick, onSaved, }: { pkg: ProcessPackage; versions: readonly ProcessPackage[]; activeVersion: number | undefined; nav: Nav; activities: readonly ActivityRow[]; positions: SpPosition[]; nextVersion: number; /** 该版本闭合与否(undefined = 报告还没回来)—— 激活按钮据此灰显 */ closedOf: (version: number) => boolean | undefined; onPick: (version: number) => void; onSaved: (version: number) => void; }) { const queryClient = useQueryClient(); /* * 任何版本都可编辑:保存恒存为**未生效**的新版本,谁也伤不着 —— * 在旧版上改就是「以旧版为底另起一版」。生效切换只在激活那一下。 */ /** 画布工作副本:属性卡与手绘编辑都改它;保存时合并回流程包 */ const [graph, setGraph] = useState(() => packageToGraph(pkg, [])); const [sel, setSel] = useState(null); /** 边选中态:graph.edges 的下标(与节点选中互斥 —— 属性卡一次编辑一件事) */ const [selEdge, setSelEdge] = useState(null); const [dirty, setDirty] = useState(false); const [report, setReport] = useState(undefined); const [ask, setAsk] = useState<{ spec: AskSpec; danger?: boolean } | undefined>(undefined); const canvasRef = useRef(null); const nodes = graph?.nodes ?? []; const node = nodes.find((n) => n.id === sel) ?? null; const semanticNode: ProcessNode | undefined = pkg.nodes.find((n) => n.id === sel); const refAct = node === null ? undefined : activities.find( (a) => (node.act !== undefined && a.activityId === node.act) || (node.act === undefined && semanticNode?.kind === 'activity' && a.activityId === semanticNode.activityId), ); // 每次进页问一次闭合 —— 画图即校验(与后端裁决同源);落笔保存后用返回的报告 const liveReport = useQuery({ queryKey: ['gov', 'process-report', pkg.id, pkg.version], queryFn: () => call(validateProcessRoute, { body: { pkg } }), }); const activeReport = report ?? liveReport.data; const ec = edgeCheckOf(pkg, activeReport); const patch = (f: (n: FlowNode) => FlowNode) => { if (sel === null) return; setGraph((g) => (g === null ? g : { ...g, nodes: g.nodes.map((n) => (n.id === sel ? f(n) : n)) })); setDirty(true); }; const kindLabel = (n: FlowNode) => n.kind === 'activity' ? n.flavor === 'approval' ? '审批点' : '活动点' : n.kind === 'callout' ? '卡点' : n.kind === 'start' ? '调用接口' : '显式终点'; const kindTone = (n: FlowNode) => n.kind === 'activity' ? (n.flavor === 'approval' ? 'warn' : 'auto') : n.kind === 'callout' ? 'danger' : 'mute'; // 画布上的选中态:diagram-js 给每个形状挂了 data-element-id,外挂一个类,不改画布件 useEffect(() => { const root = canvasRef.current; if (root === null) return; root.querySelectorAll('.flow-node-sel').forEach((el) => el.classList.remove('flow-node-sel')); if (sel !== null) root.querySelector(`[data-element-id="${sel}"]`)?.classList.add('flow-node-sel'); }, [sel, graph]); const [saveErr, setSaveErr] = useState(undefined); const save = useMutation({ mutationFn: (input: { graph: FlowGraph; idMap: Record }) => call<{ pkg: ProcessPackage; report: ClosureReport }>(saveProcessRoute, { body: { pkg: mergeGraphIntoPackage(pkg, input.graph, input.idMap) }, }), onSuccess: (out) => { setReport(out.report); setDirty(false); setSaveErr(undefined); void queryClient.invalidateQueries({ queryKey: ['gov', 'process'] }); void queryClient.invalidateQueries({ queryKey: ['gov', 'process-reports'] }); // 存下来的是未生效的新版 —— 钉过去接着画;异常看板会列它还缺什么 onSaved(out.pkg.version); }, onError: (e: unknown) => setSaveErr((e as { message?: string }).message ?? '保存失败'), }); /** 版本启停:运行时开关,不改定义 —— 在途按启动版本跑完,零影响 */ const setState = useMutation({ mutationFn: (input: { version: number; state: 'active' | 'retired' }) => call<{ packages: ProcessPackage[] }>(setProcessStateRoute, { params: { id: pkg.id, version: String(input.version) }, body: { state: input.state }, }), onSuccess: () => { void queryClient.invalidateQueries({ queryKey: ['gov', 'process'] }); setAsk(undefined); }, }); /** * 新建版本 = **以当前看的版本为底克隆一版**(服务端定版号,**不生效**)。 * 与「激活」分工明确:激活只是把生效指针切过去,不产生新版本; * 新建版本才长一版 —— 把旧定义复活成最新版、或在现状上另起一版接着画,都走这儿。 */ const cloneVersion = useMutation({ mutationFn: () => call<{ pkg: ProcessPackage; report: ClosureReport }>(saveProcessRoute, { body: { pkg } }), onSuccess: (out) => { setReport(out.report); void queryClient.invalidateQueries({ queryKey: ['gov', 'process'] }); void queryClient.invalidateQueries({ queryKey: ['gov', 'process-reports'] }); setAsk(undefined); onSaved(out.pkg.version); }, }); const askClone = () => setAsk({ spec: { title: `新建版本:以 v${pkg.version} 为底 → v${nextVersion}`, note: dirty ? ( 画布上有未保存的改动,新建版本不会带上它们(克隆的是已存的 v{pkg.version})。 想连改动一起走,直接点「保存为 v{nextVersion}」。 ) : ( 把 v{pkg.version} 的定义原样复制成 v{nextVersion}。不生效 —— 画完、异常清零后再激活。 ), impact: { who: '只多一版,不动任何在跑的东西', undo: '版本并存,不要就停着别激活', when: '确认即建', }, okLabel: `建 v${nextVersion}`, onOk: () => cloneVersion.mutate(), }, }); const askActivate = (v: ProcessPackage) => setAsk({ spec: { title: `激活 v${v.version}:${pkg.name}`, note: ( 激活即独占:此后新起的工作流用 v{v.version} {activeVersion === undefined ? '' : `,v${activeVersion} 随之停用`}。 在途的按各自启动时的版本跑完,不受影响。 ), impact: { who: `新起单一律走 v${v.version}`, undo: '可再切回任一版本', when: '确认即切换', }, okLabel: `激活 v${v.version}`, onOk: () => setState.mutate({ version: v.version, state: 'active' }), }, }); const askRetire = (v: ProcessPackage) => setAsk({ danger: true, spec: { title: `停用 v${v.version}:${pkg.name}`, note: ( 停用后这条流没有生效版本:新起单会被拒,触发器命中了也起不了。 在途工作流照旧跑完,不受影响。 ), impact: { who: '整条流对新起单下线', undo: '随时激活任一版本即恢复', when: '确认即下线', }, okLabel: '停用,整条流下线', onOk: () => setState.mutate({ version: v.version, state: 'retired' }), }, }); /** * 导出依赖文件 —— 编排定契约:各活动的接口声明、责任岗位、输入/输出声明与上游装配, * 一并成一份 JSON。**依赖文件即包清单骨架**,开发者对着声明在系统外开发活动。 */ const exportDeps = () => { const actContract = new Map(activities.map((a) => [a.activityId, a.contract])); const doc = { package: { id: pkg.id, name: pkg.name, version: pkg.version }, callInterface: pkg.callInterface, activities: pkg.nodes .filter((n) => n.kind === 'activity') .map((n) => ({ nodeId: n.id, label: n.label, activityId: n.activityId, owner: n.owner, // 编排上手写的注记(给人读) declaredIn: n.in ?? [], declaredOut: n.out ?? [], /* * 活动库里那份**真契约**:必填入参、字段、输出口、声明的工具/模型/域对象。 * 开发者照着它写实现;只给注记的话,依赖文件说不清这个活动到底要什么。 */ contract: actContract.get(n.activityId) ?? null, upstreamAssembly: pkg.edges .filter((e) => e.to === n.id) .map((e) => ({ from: e.from, mapping: e.mapping })), })), approvals: pkg.nodes .filter((n) => n.kind === 'approval') .map((n) => ({ nodeId: n.id, label: n.label, approver: n.approver, reject: n.reject })), }; const blob = new Blob([JSON.stringify(doc, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${pkg.id}-v${pkg.version}-依赖文件.json`; a.click(); URL.revokeObjectURL(url); }; return ( <> {activeVersion !== undefined ? ( 生效 v{activeVersion} ) : ( 已下线 —— 没有生效版本 )} {versions.length} 个版本 边 {ec.closed}/{ec.total} } acts={[ { label: '导出依赖文件', ico: 'upload', ask: { title: '导出依赖文件', note: ( 编排定契约:导出各活动的接口声明、责任岗位、输入输出声明与上游装配 —— 依赖文件即包清单骨架,开发者对着声明在系统外开发。 ), impact: { who: '活动开发者', undo: '可重复导出', when: '确认后生成' }, okLabel: '确认导出', onOk: exportDeps, }, }, ]} /> {ask !== undefined && setAsk(undefined)} />} {/* 编排工作区:左 1/3 版本列表 + 右 2/3 画布 —— 版本收在流程内部,不平铺台面 */}
n.kind === 'activity').map((n) => n.id)} /* * 画布只画**编排逻辑**:节点的名字、连线的顺序;边上的字是辅助阅读的注记。 * 图上真正生效可编辑的只有**名字** —— 不传 positions,双击就不出岗位一栏。 * 岗位、判据、映射这些闭环要素在**节点属性**里决定;分配到人是运营的事,这里不碰。 */ canEdit onNodeClick={(nid) => { setSel(nid); setSelEdge(null); }} onEditNodeText={async (nid, p2) => { setGraph((g) => g === null ? g : { ...g, nodes: g.nodes.map((n) => (n.id === nid ? { ...n, label: p2.label } : n)) }, ); setDirty(true); }} onSaveFlowGraph={async (next, idMap) => { setGraph(next); await save.mutateAsync({ graph: next, idMap }); }} />
{/* * 左下:异常看板 —— 与版本卡同列同风(图纸式)。 * 画图不检查,这里**替**你盯着:哪个节点缺什么,点一条定位到属性去补; * 清零了激活按钮才亮 —— 完整性闸在激活,不在画图。 */} {/* 右下:节点属性(图纸卡,与流程图同宽)—— 闭环(岗位/判据/映射)在这里决定 */}
节点 · 属性 {dirty ? '有改动待保存' : '点节点就地填写'}
{saveErr !== undefined && ( 保存失败: {saveErr} )}
{nodes .filter((n) => n.kind !== 'start' && n.kind !== 'end' && n.kind !== 'end-bad') .map((n) => ( ))}
{/* 边拣选条:边属性 = 传递契约(条件 + 映射)—— 图只定顺序,契约在这儿填 */}
{(graph?.edges ?? []).map((e, i) => { const toSem = calloutToOf(graph)?.get(e.to) ?? e.to; const brief = e.condition !== undefined && e.condition !== '' ? ` [${e.condition}]` : ''; return ( ); })}
{node === null && selEdge === null ? ( 点画布或上面的拣选条选一件 —— 节点填岗位与链接实现,边填传递契约(条件 + 映射)。 审批裁决(verdict)与放行选口(outcome)由系统注入判据,不用你声明。 ) : node === null && selEdge !== null && graph !== null ? ( { setGraph((g) => g === null ? g : { ...g, edges: g.edges.map((e, i) => (i === selEdge ? f(e) : e)) }, ); setDirty(true); }} /> ) : node === null ? null : ( <>
patch((n) => ({ ...n, label: e.target.value }))} /> {node.kind === 'activity' && ( patch((n) => ({ ...n, sub: e.target.value === '' ? undefined : e.target.value })) } /> )} {node.kind === 'activity' && ( patch((n) => ({ ...n, flavor: k === 'approval' ? ('approval' as const) : undefined })) } /> )} {node.kind === 'activity' && node.flavor === 'approval' && ( 审批点是系统内置要素,不是活动 —— 声明即用、零开发、不进活动库(一个流程包里需要开发的 只有活动)。功能属性四件:审批岗位(复用负责人四策略解析)· 输入装配映射 · 驳回走向 (声明式:回指定节点重开新实例 / 终止)· 催办阶梯。运行时它产出审批单而非任务。 )} {node.owner === undefined && ( 这个节点还没指定岗位 —— 不指定,起单时就不知道 {node.flavor === 'approval' ? '审批单' : '任务'}该派给谁。 )} {node.kind === 'activity' && ( patch((n) => ({ ...n, in: v.length > 0 ? [...v] : undefined }))} /> )} {node.kind === 'activity' && ( patch((n) => ({ ...n, out: v.length > 0 ? [...v] : undefined }))} /> )} {node.kind === 'callout' && ( )} {node.kind === 'activity' && node.flavor !== 'approval' && ( {refAct !== undefined ? ( <> {/* 实现方式(范式/版本)是装载信息,只读展示 —— 编排面不决定它 */}
{refAct.paradigm === 'loop' ? '实现:案例 loop' : '实现:标准 flow'} v{refAct.packageVersions[0]?.version ?? '—'} × a {refAct.assetVersions.find((x) => x.effective)?.version ?? '—'}
) : ( 还没链接活动库里已安装的实现 —— 图可以照画照存,激活前链上即可。 )}
)}
)}
); }