流程包台面重做:版本收进包内部,启停是运行时开关

按你定的六条做:

## ① 台面三页签:总览 · 流程包 · 触发器
「调起规则」改名触发器(键不动,深链不断)。

## ② 版本收在流程内部,不平铺台面
货架一流程一卡:名字 + 标识 + 「生效 vN / 已下线」+ 版本数。
从前一版本一张卡,三个版本占三格 —— 版本是流程的内部事,不该摊在台面上。

## ③ 详情页布局照你说的排
左 1/3 版本列表(恰一个生效,点谁看谁的图),右 2/3 流程图;
图下是节点属性(点画布或点节点条,就地填);再往下才是声明式契约细节
(边映射校验 · 调用接口 · 触发器)。属性区从右侧窄栏挪到图下整行。

## ④ 新建流程
「新建流程」→ 填名称与标识 → 建出一张「开始 → 结束」的最小图,v1 即刻生效,直接进详情。
最小图天然过闭合校验 —— 不闭合存不进来这条闸对新建同样成立。
标识收紧为小写字母开头、只许小写/数字/连字符(它进 URL 和触发器规则)。

## ⑤ 历史包袱文案清掉
「工作流不设负责人,责任只落在节点上:此处不声明岗位,起单时就解析不出任务负责人」
→「这个节点还没指定岗位 —— 不指定,起单时就不知道任务该派给谁」。
同类:「未声明 · 发布前须补齐」→「未指定 —— 保存前选一个岗位」;
「发布前须先安装并过跨包契约相容核对」→「先去活动库安装;保存时会核对契约相容」;
边映射页的「边不闭合不得发布」→「不得保存」。

## ⑥ 版本可激活/停用,一次只能激活一个,切换不动在途
运行时模型从「最新版生效」改为**显式生效指针**:
  · `setPackageState` 激活即独占(同流其余版本一并停用)
  · 停用生效版 = 整条流下线,新起单被拒(报「无生效版本 —— 去流程包里激活一个」)
  · 起单取生效版;在途工作流按启动时钉住的版本号读定义,启停对它们零影响
  · 保存新版仍即刻生效(旧生效版自动停用)—— 回退就是把生效切回旧版,不用改图
store 的 setPackageState 把列上与 definition(jsonb)里的两份 state 一起改(教训同 0012);
迁移 0013 把存量多版同 active 归一为「每条流只留最高版生效」。

实测:激活 v1 → v3 自动停用,新起单钉 v1;停用 v1(零生效)→ 起单 500 且明说原因;
在途 v1 工作流照跑;恢复激活 v3。UI:切版看图、只读提示、启停确认卡、新建一条龙全通。

typecheck ✓ lint ✓ depcruise 376 模块 ✓ 498 测试 ✓

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-28 14:35:54 +08:00
co-authored by Claude Opus 5
parent 2b1d983678
commit 710946e729
17 changed files with 1019 additions and 467 deletions
@@ -47,20 +47,17 @@ export function createProcessService(deps: ProcessServiceDeps): ProcessService {
}
/**
* 起单取版:未指定版本时取**最新的一版**。
* 起单取版:取这条流的**生效版本** —— 一条流恰一个(或零个)生效版本。
*
* 从前这里取「最新已发布」,因为草稿是编排中的半成品、运行时不该看见它。
* 现在没有草稿了 —— 「严格检查 + 保存生效」,不闭合根本存不进来,
* 所以库里的每一版都是能起单的。生效就是这个意思:**新任务起单时取到最新版**,
* 在途的按启动时快照的版本号读定义、跑完旧编排。
* `retired` 的版本拒起单(留给「这一版不再用了」)。
* 「生效」是运行时开关,不是内容属性:保存新版即生效(旧生效版随之停用),
* 也可以手动把生效切回旧版本、或全部停用让整条流下线。
* 在途工作流按启动时钉住的版本号读定义,启停对它们零影响 —— 这正是
* 「切换版本不影响旧实例」的机制保证:实例上钉的是版本号,不是「当前生效」指针。
*/
async function latest(id: string): Promise<ProcessPackage> {
async function activeVersion(id: string): Promise<ProcessPackage> {
const all = await deps.store.listPackages();
const hit = all
.filter((pkg) => pkg.id === id && pkg.state !== 'retired')
.sort((a, b) => b.version - a.version)[0];
if (hit === undefined) throw new Error(`流程包无可用版本,不得起单:${id}`);
const hit = all.find((pkg) => pkg.id === id && pkg.state === 'active');
if (hit === undefined) throw new Error(`流程包无生效版本,不得起单:${id} —— 去流程包里激活一个版本`);
return hit;
}
@@ -295,9 +292,33 @@ export function createProcessService(deps: ProcessServiceDeps): ProcessService {
if (!report.closed) throw new NotClosedError(report);
const saved: ProcessPackage = { ...pkg, version: await nextVersion(pkg.id), state: 'active' };
await deps.store.savePackage(saved);
// 新版即生效 → 旧生效版随之停用:一条流恰一个生效版本(要回退就去版本列表切)
const siblings = await deps.store.listPackages();
for (const sib of siblings) {
if (sib.id === saved.id && sib.version !== saved.version && sib.state === 'active') {
await deps.store.setPackageState(sib.id, sib.version, 'retired');
}
}
return saved;
},
async setPackageState(id, version, state) {
const all = await deps.store.listPackages();
const target = all.find((p) => p.id === id && p.version === version);
if (target === undefined) throw new Error(`流程包版本不存在:${id}@${version}`);
if (state === 'active') {
// 激活即独占:同流其余生效版一并停用 —— 「一次只能激活一个版本」
for (const sib of all) {
if (sib.id === id && sib.version !== version && sib.state === 'active') {
await deps.store.setPackageState(id, sib.version, 'retired');
}
}
}
await deps.store.setPackageState(id, version, state);
const after = await deps.store.listPackages();
return after.filter((p) => p.id === id).sort((a, b) => b.version - a.version);
},
async listPackages() {
return deps.store.listPackages();
},
@@ -320,9 +341,10 @@ export function createProcessService(deps: ProcessServiceDeps): ProcessService {
async start(input: StartWorkflowInput) {
const pkg =
input.packageVersion === undefined
? await latest(input.packageId)
? await activeVersion(input.packageId)
: await requirePackage(input.packageId, input.packageVersion);
if (pkg.state === 'retired') throw new Error(`该版本已停用,不得起单:${pkg.id}@${pkg.version}`);
// 指名道姓要某一版也得是生效版 —— 「一次只能激活一个版本」对新起单是硬约束
if (pkg.state !== 'active') throw new Error(`该版本未生效,不得起单:${pkg.id}@${pkg.version}`);
// 先校验后建单:不合调用接口契约的输入**一条工作流都不留**。
// (校验若放在建单之后,失败就会留下一条永远推不动的在途单,污染在途口径。)