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

按你定的六条做:

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

## ② 版本收在流程内部,不平铺台面
货架一流程一卡:名字 + 标识 + 「生效 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
+7
View File
@@ -30,10 +30,17 @@ export type ProcessService = {
/**
* 保存 —— **严格检查 + 即刻生效**,不闭合抛 `NotClosedError`。
* 恒产生新版本号(服务端算,不采信入参的 version);返回实际落的那一版。
* 落下来的新版**即为生效版**:同流的旧生效版随之停用(一条流恰一个生效版本)。
*/
savePackage(pkg: ProcessPackage): Promise<ProcessPackage>;
listPackages(): Promise<ProcessPackage[]>;
getPackage(id: string, version?: number): Promise<ProcessPackage | undefined>;
/**
* 版本启停 —— 运行时开关,**不改定义一个字**。
* 激活某版:同流其余版本一并停用(恰一个生效);停用生效版:整条流下线,新起单被拒。
* 在途工作流按启动时钉住的版本跑完,启停对它们零影响。
*/
setPackageState(id: string, version: number, state: 'active' | 'retired'): Promise<ProcessPackage[]>;
/** 画图即校验 —— 与前端画布同源;只算不存 */
validate(pkg: ProcessPackage): Promise<ClosureReport>;
start(input: StartWorkflowInput): Promise<WorkflowRecord>;
+2
View File
@@ -37,6 +37,8 @@ export type WorkflowRecord = {
};
export type ProcessStore = {
savePackage(pkg: ProcessPackage): Promise<void>;
/** 只改状态,不动定义 —— state 在列上与 definition 里各存一份,两处要一起改 */
setPackageState(id: string, version: number, state: 'active' | 'retired'): Promise<void>;
getPackage(id: string, version?: number): Promise<ProcessPackage | undefined>;
listPackages(): Promise<ProcessPackage[]>;
insertWorkflow(record: WorkflowRecord): Promise<void>;
+212
View File
@@ -1065,6 +1065,218 @@ export declare const saveProcessRoute: {
}, z.core.$strip>;
}, z.core.$strip>;
};
/**
* 版本启停 —— 运行时开关,不改定义一个字。
* 激活某版:同流其余版本一并停用(一条流恰一个生效版本);停用生效版:整条流下线,新起单被拒。
* 在途工作流按启动时钉住的版本跑完,启停对它们零影响。
* 返回这条流启停后的全部版本(新→旧),界面直接以它刷新版本列表。
*/
export declare const setProcessStateRoute: {
readonly method: "POST";
readonly path: "/api/gov/process/:id/:version/state";
readonly params: z.ZodObject<{
id: z.ZodString;
version: z.ZodString;
}, z.core.$strip>;
readonly body: z.ZodObject<{
state: z.ZodEnum<{
active: "active";
retired: "retired";
}>;
}, z.core.$strip>;
readonly output: z.ZodObject<{
packages: z.ZodArray<z.ZodObject<{
id: z.core.$ZodBranded<z.ZodString, "ProcessPackageId", "out">;
name: z.ZodString;
version: z.ZodNumber;
state: z.ZodEnum<{
active: "active";
retired: "retired";
}>;
callInterface: z.ZodObject<{
input: z.ZodRecord<z.ZodString, z.ZodUnknown>;
output: z.ZodRecord<z.ZodString, z.ZodUnknown>;
}, z.core.$strip>;
nodes: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
kind: z.ZodLiteral<"start">;
id: z.core.$ZodBranded<z.ZodString, "NodeId", "out">;
label: z.ZodString;
sub: z.ZodOptional<z.ZodString>;
layout: z.ZodOptional<z.ZodObject<{
row: z.ZodNumber;
col: z.ZodNumber;
x: z.ZodOptional<z.ZodNumber>;
y: z.ZodOptional<z.ZodNumber>;
}, z.core.$strip>>;
}, z.core.$strip>, z.ZodObject<{
kind: z.ZodLiteral<"end">;
output: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
id: z.core.$ZodBranded<z.ZodString, "NodeId", "out">;
label: z.ZodString;
sub: z.ZodOptional<z.ZodString>;
layout: z.ZodOptional<z.ZodObject<{
row: z.ZodNumber;
col: z.ZodNumber;
x: z.ZodOptional<z.ZodNumber>;
y: z.ZodOptional<z.ZodNumber>;
}, z.core.$strip>>;
}, z.core.$strip>, z.ZodObject<{
kind: z.ZodLiteral<"activity">;
activityId: z.core.$ZodBranded<z.ZodString, "ActivityId", "out">;
in: z.ZodOptional<z.ZodArray<z.ZodString>>;
out: z.ZodOptional<z.ZodArray<z.ZodString>>;
activityVersion: z.ZodOptional<z.ZodString>;
owner: z.ZodObject<{
position: z.ZodString;
strategy: z.ZodDefault<z.ZodEnum<{
assigned: "assigned";
sole: "sole";
rotation: "rotation";
"follow-source": "follow-source";
}>>;
staffId: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "StaffId", "out">>;
}, z.core.$strip>;
failure: z.ZodDefault<z.ZodDiscriminatedUnion<[z.ZodObject<{
kind: z.ZodLiteral<"terminate">;
}, z.core.$strip>, z.ZodObject<{
kind: z.ZodLiteral<"reroute">;
edgeId: z.ZodString;
}, z.core.$strip>, z.ZodObject<{
kind: z.ZodLiteral<"escalate">;
approvalNodeId: z.core.$ZodBranded<z.ZodString, "NodeId", "out">;
}, z.core.$strip>, z.ZodObject<{
kind: z.ZodLiteral<"tolerate">;
}, z.core.$strip>], "kind">>;
message: z.ZodOptional<z.ZodString>;
id: z.core.$ZodBranded<z.ZodString, "NodeId", "out">;
label: z.ZodString;
sub: z.ZodOptional<z.ZodString>;
layout: z.ZodOptional<z.ZodObject<{
row: z.ZodNumber;
col: z.ZodNumber;
x: z.ZodOptional<z.ZodNumber>;
y: z.ZodOptional<z.ZodNumber>;
}, z.core.$strip>>;
}, z.core.$strip>, z.ZodObject<{
kind: z.ZodLiteral<"approval">;
approver: z.ZodObject<{
position: z.ZodString;
strategy: z.ZodDefault<z.ZodEnum<{
assigned: "assigned";
sole: "sole";
rotation: "rotation";
"follow-source": "follow-source";
}>>;
staffId: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "StaffId", "out">>;
}, z.core.$strip>;
reject: z.ZodDiscriminatedUnion<[z.ZodObject<{
kind: z.ZodLiteral<"reopen">;
nodeId: z.core.$ZodBranded<z.ZodString, "NodeId", "out">;
mapping: z.ZodDefault<z.ZodArray<z.ZodObject<{
target: z.ZodString;
source: z.ZodDiscriminatedUnion<[z.ZodObject<{
from: z.ZodLiteral<"output-port">;
port: z.ZodString;
path: z.ZodString;
}, z.core.$strip>, z.ZodObject<{
from: z.ZodLiteral<"workflow-var">;
name: z.ZodString;
}, z.core.$strip>, z.ZodObject<{
from: z.ZodLiteral<"initiation">;
path: z.ZodString;
}, z.core.$strip>, z.ZodObject<{
from: z.ZodLiteral<"constant">;
value: z.ZodUnknown;
}, z.core.$strip>, z.ZodObject<{
from: z.ZodLiteral<"expression">;
sfeel: z.ZodString;
}, z.core.$strip>], "from">;
}, z.core.$strip>>>;
}, z.core.$strip>, z.ZodObject<{
kind: z.ZodLiteral<"terminate">;
}, z.core.$strip>], "kind">;
nudge: z.ZodOptional<z.ZodObject<{
deadline: z.ZodString;
steps: z.ZodDefault<z.ZodArray<z.ZodObject<{
after: z.ZodString;
action: z.ZodEnum<{
nudge: "nudge";
escalate: "escalate";
}>;
}, z.core.$strip>>>;
}, z.core.$strip>>;
message: z.ZodOptional<z.ZodString>;
id: z.core.$ZodBranded<z.ZodString, "NodeId", "out">;
label: z.ZodString;
sub: z.ZodOptional<z.ZodString>;
layout: z.ZodOptional<z.ZodObject<{
row: z.ZodNumber;
col: z.ZodNumber;
x: z.ZodOptional<z.ZodNumber>;
y: z.ZodOptional<z.ZodNumber>;
}, z.core.$strip>>;
}, z.core.$strip>, z.ZodObject<{
kind: z.ZodLiteral<"gateway">;
gateway: z.ZodEnum<{
parallel: "parallel";
exclusive: "exclusive";
}>;
mode: z.ZodEnum<{
join: "join";
fork: "fork";
}>;
basis: z.ZodOptional<z.ZodString>;
id: z.core.$ZodBranded<z.ZodString, "NodeId", "out">;
label: z.ZodString;
sub: z.ZodOptional<z.ZodString>;
layout: z.ZodOptional<z.ZodObject<{
row: z.ZodNumber;
col: z.ZodNumber;
x: z.ZodOptional<z.ZodNumber>;
y: z.ZodOptional<z.ZodNumber>;
}, z.core.$strip>>;
}, z.core.$strip>], "kind">>;
edges: z.ZodArray<z.ZodObject<{
id: z.ZodString;
from: z.core.$ZodBranded<z.ZodString, "NodeId", "out">;
to: z.core.$ZodBranded<z.ZodString, "NodeId", "out">;
condition: z.ZodOptional<z.ZodString>;
mapping: z.ZodDefault<z.ZodArray<z.ZodObject<{
target: z.ZodString;
source: z.ZodDiscriminatedUnion<[z.ZodObject<{
from: z.ZodLiteral<"output-port">;
port: z.ZodString;
path: z.ZodString;
}, z.core.$strip>, z.ZodObject<{
from: z.ZodLiteral<"workflow-var">;
name: z.ZodString;
}, z.core.$strip>, z.ZodObject<{
from: z.ZodLiteral<"initiation">;
path: z.ZodString;
}, z.core.$strip>, z.ZodObject<{
from: z.ZodLiteral<"constant">;
value: z.ZodUnknown;
}, z.core.$strip>, z.ZodObject<{
from: z.ZodLiteral<"expression">;
sfeel: z.ZodString;
}, z.core.$strip>], "from">;
}, z.core.$strip>>>;
label: z.ZodOptional<z.ZodString>;
tone: z.ZodOptional<z.ZodEnum<{
ok: "ok";
bad: "bad";
}>>;
reopen: z.ZodDefault<z.ZodBoolean>;
}, z.core.$strip>>;
variables: z.ZodDefault<z.ZodArray<z.ZodObject<{
name: z.ZodString;
schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
}, z.core.$strip>>>;
description: z.ZodOptional<z.ZodString>;
diagram: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
}, z.core.$strip>>;
}, z.core.$strip>;
};
/** 画图即校验 —— 与前端画布同源;保存前随手看还差几条,不改任何状态 */
export declare const validateProcessRoute: {
readonly method: "POST";
+18
View File
@@ -25,6 +25,7 @@ import {
listTriggersRoute,
registerToolRoute,
saveProcessRoute,
setProcessStateRoute,
reassignTaskRoute,
resumeTaskRoute,
staffIdSchema,
@@ -290,6 +291,23 @@ export function registerGovernanceOpsRoutes(app: Hono<AppEnv>, container: Contai
}
});
/** 版本启停:激活即独占,停用即下线 —— 在途按启动版本跑完,零影响 */
app.post(setProcessStateRoute.path, async (c) => {
const denied = await governProcess(c.get('account'));
if (denied) return c.json(denied, denyStatus(denied));
const parsed = setProcessStateRoute.body.safeParse(await c.req.json());
if (!parsed.success) {
const body: ApiError = { code: 'validation-failed', message: '启停状态不合协议' };
return c.json(body, 400);
}
const packages = await container.process.setPackageState(
c.req.param('id'),
Number(c.req.param('version')),
parsed.data.state,
);
return c.json({ packages });
});
/** 画图即校验 —— 与前端画布同源 */
app.post(validateProcessRoute.path, async (c) => {
const denied = await governProcess(c.get('account'));
+2 -4
View File
@@ -747,11 +747,9 @@ export function wireEngines(deps: EngineWiringDeps): EngineWiring {
process: {
start: async (input) => process.start(input),
requiredInputs: async (packageId) => {
// 与起单取版同口径:读**最新一版**的调用接口(没有草稿了,存进来的都生效)
// 与起单取版同口径:读**生效版本**的调用接口(一条流恰一个生效版本)
const all = await process.listPackages();
const pkg = all
.filter((x) => x.id === packageId && x.state !== 'retired')
.sort((a, b) => b.version - a.version)[0];
const pkg = all.find((x) => x.id === packageId && x.state === 'active');
const input = pkg?.callInterface.input as { required?: string[] } | undefined;
return input?.required ?? [];
},
+5 -3
View File
@@ -11,7 +11,7 @@
- 错误:`NotClosedError` `PositionVacantError`
- 端口类型:`ProcessStore` `ActivityDirectory` `OrgDirectory` `TaskGateway` `ApprovalGateway`
`Sfeel` `FlowEventPublisher` `ReceiptNotifier`
- 用例工厂:`createProcessService(deps): ProcessService`
- 用例工厂:`createProcessService(deps): ProcessService`(含 `setPackageState` 版本启停)
- 纯函数:`checkClosure` `closed` `resolveOwner` `aggregateWorkflowState`
(`resolveOwner` 的 `bound` 入参 = 已校验的工作组绑定,ADR-011)
`isDigested` `nextInstanceNumber` `selectEdges` `assemble` `joinReady`
@@ -28,8 +28,10 @@
- **不闭合不得保存**:未完成边、必填未映射、类型冲突、孤立节点、异或出边缺条件、
汇合同名字段来源不明、容忍分支被下游必填依赖 —— 任一即 `closed = false`,`savePackage` 抛 `NotClosedError`
- **严格检查 + 保存即生效,没有发布机制**(产品决策 2026-07-28):
没有草稿态、没有发布门。存进来的每一版都能起单;生效与否由**运行时**决定 ——
新工作流起单取最新版,在途的按启动时快照的版本号读定义、跑完旧编排。
没有草稿态、没有发布门。保存 = 存为新版 + 即刻生效。
- **一条流恰一个生效版本**(产品决策 2026-07-28):`setPackageState` 激活即独占
(同流其余版本一并停用);停用生效版 = 整条流下线,新起单被拒。
起单取**生效版**;在途工作流按启动时钉住的版本号读定义 —— 启停对它们零影响。
- **保存恒新增版本号**(服务端算,不采信入参):工作流按 (包id, 版本) 读定义,
就地覆盖会改掉在途任务正在读的编排。store 层因此**只插不改**,撞键宁可报错也不静默覆盖。
- **求值语义前后端同源**:画布的即时校验与后端裁决共用 `checkClosure`
@@ -45,6 +45,7 @@ function harness(validationErrors: readonly string[]): Harness {
const deps: ProcessServiceDeps = {
store: {
savePackage: async () => undefined,
setPackageState: async () => undefined,
getPackage: async () => PKG,
listPackages: async () => [PKG],
insertWorkflow: async (record) => {
+7
View File
@@ -55,10 +55,17 @@ export type ProcessService = {
/**
* 保存 —— **严格检查 + 即刻生效**,不闭合抛 `NotClosedError`。
* 恒产生新版本号(服务端算,不采信入参的 version);返回实际落的那一版。
* 落下来的新版**即为生效版**:同流的旧生效版随之停用(一条流恰一个生效版本)。
*/
savePackage(pkg: ProcessPackage): Promise<ProcessPackage>;
listPackages(): Promise<ProcessPackage[]>;
getPackage(id: string, version?: number): Promise<ProcessPackage | undefined>;
/**
* 版本启停 —— 运行时开关,**不改定义一个字**。
* 激活某版:同流其余版本一并停用(恰一个生效);停用生效版:整条流下线,新起单被拒。
* 在途工作流按启动时钉住的版本跑完,启停对它们零影响。
*/
setPackageState(id: string, version: number, state: 'active' | 'retired'): Promise<ProcessPackage[]>;
/** 画图即校验 —— 与前端画布同源;只算不存 */
validate(pkg: ProcessPackage): Promise<ClosureReport>;
@@ -41,6 +41,22 @@ export function createProcessStore(db: Db): ProcessStore {
.values({ id: pkg.id, version: pkg.version, name: pkg.name, state: pkg.state, definition: pkg });
},
async setPackageState(id, version, state) {
const rows = await db
.select()
.from(processPackageTable)
.where(and(eq(processPackageTable.id, id), eq(processPackageTable.version, version)))
.limit(1);
const row = rows[0];
if (row === undefined) return;
// state 在列上与 definition(jsonb)里各有一份 —— 读包走 definition,列供查询;两处必须一起改
const definition = { ...(row.definition as Record<string, unknown>), state };
await db
.update(processPackageTable)
.set({ state, definition })
.where(and(eq(processPackageTable.id, id), eq(processPackageTable.version, version)));
},
async getPackage(id, version) {
const conditions = [eq(processPackageTable.id, id)];
if (version !== undefined) conditions.push(eq(processPackageTable.version, version));
@@ -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}`);
// 先校验后建单:不合调用接口契约的输入**一条工作流都不留**。
// (校验若放在建单之后,失败就会留下一条永远推不动的在途单,污染在途口径。)
+2
View File
@@ -32,6 +32,8 @@ export type WorkflowRecord = {
export type ProcessStore = {
savePackage(pkg: ProcessPackage): Promise<void>;
/** 只改状态,不动定义 —— state 在列上与 definition 里各存一份,两处要一起改 */
setPackageState(id: string, version: number, state: 'active' | 'retired'): Promise<void>;
getPackage(id: string, version?: number): Promise<ProcessPackage | undefined>;
listPackages(): Promise<ProcessPackage[]>;
+18
View File
@@ -503,3 +503,21 @@
.g-mixbar-legend { display: flex; flex-wrap: wrap; gap: 3px 10px; font-size: 10px; color: var(--ink-3); }
.g-mixbar-legend b { font-family: var(--mono); font-weight: 700; color: var(--ink-2); }
.g-mixbar-legend i { display: inline-block; width: 7px; height: 7px; border-radius: 2px; margin-right: 4px; }
/* ── 流程详情:左版本列 + 右画布(补充)──────────────────────────────
版本收在流程内部:左列是这条流的全部版本(恰一个生效),点谁看谁的图;
属性区从右侧窄栏挪到图下整行(g-wide),声明式契约细节再往下。 */
.g-flowlab { display: grid; grid-template-columns: minmax(220px, 1fr) minmax(0, 2.2fr); gap: 12px; align-items: start; }
.g-vers { position: sticky; top: 2px; display: flex; flex-direction: column; gap: 8px; }
.g-ver { display: flex; align-items: center; gap: 8px; padding: 10px 12px; background: var(--card); border: 1px solid var(--cline); border-radius: 11px; cursor: pointer; font: inherit; text-align: left; transition: border-color var(--t-fast), box-shadow var(--t-fast); }
.g-ver:hover { border-color: #c9d6e6; }
.g-ver.on { border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-wash); }
.g-ver > b { font-family: var(--mono); font-size: 13px; color: var(--ink); }
.g-ver .m { margin-left: auto; display: flex; align-items: center; gap: 8px; }
.g-ver .cnt { font-size: 10.5px; font-family: var(--mono); color: var(--ink-4); white-space: nowrap; }
.g-ver-act { border: 1px solid var(--cline); background: #fff; border-radius: 8px; padding: 3px 9px; font: inherit; font-size: 11px; font-weight: 600; color: var(--ink-2); cursor: pointer; }
.g-ver-act:hover { border-color: #c9d6e6; }
.g-ver-act.t-go { color: var(--ok); border-color: color-mix(in srgb, var(--ok) 35%, var(--cline)); }
/* 属性区整行铺开:g-side 的窄栏约束(单列 grid、sticky)在这里都不要 */
.g-wide { position: static; margin-top: 12px; }
.g-wide .g-props { grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); }
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -103,7 +103,7 @@ export const ZM: Readonly<Record<GovZone, { title: string; desc: string; accent:
title: '流程包管理',
icon: 'workflow',
accent: 'linear-gradient(140deg,#4954e6,#3a44c9)',
desc: '战略——企业以哪些流转运转。五要素编排(活动 · 审批点 · 网关 · 触发器 · 流程包)与版本:编排即定契约,连线即映射、画图即校验,边不闭合不得发布。调起规则(触发器的定义)也在本区:谁调起这条流,与流程包一处维护;启停在运营中心。',
desc: '战略——企业以哪些流转运转。一条流一个包,版本收在包内部:保存即生效、恰一个生效版本、在途按启动版本跑完。编排即定契约:连线即映射、画图即校验,边不闭合存不进来。触发器(谁调起这条流)也在本区定义;启停在运营中心。',
},
library: {
title: '活动库',
@@ -148,7 +148,7 @@ export const TABS: Readonly<Record<GovZone, ReadonlyArray<{ key: string; label:
workflows: [
{ key: 'overview', label: '总览', ico: 'gauge' },
{ key: 'pkgs', label: '流程包', ico: 'boxes' },
{ key: 'rules', label: '调起规则', ico: 'zap' },
{ key: 'rules', label: '触发器', ico: 'zap' },
],
library: [{ key: 'shelf', label: '活动货架', ico: 'boxes' }],
tools: [{ key: 'catalog', label: '能力目录', ico: 'wrench' }],
+13
View File
@@ -0,0 +1,13 @@
-- 一条流恰一个生效版本(产品决策 2026-07-28:版本可激活/停用,一次只能激活一个)
--
-- 此前「保存即生效」按最新版取,库里同一条流可能多版都是 active。
-- 归一:每条流只留版本号最大的那版 active,其余停用。
-- state 在列上与 definition(jsonb)里各存一份,两处一起改(教训同 0012)。
update sys.process_package p
set state = 'retired',
definition = jsonb_set(definition, '{state}', '"retired"')
where p.state = 'active'
and exists (
select 1 from sys.process_package q
where q.id = p.id and q.state = 'active' and q.version > p.version
);
+7
View File
@@ -92,6 +92,13 @@
"when": 1785130053168,
"tag": "0012_process_no_publish",
"breakpoints": true
},
{
"idx": 13,
"version": "7",
"when": 1785130113168,
"tag": "0013_one_active_version",
"breakpoints": true
}
]
}
+14
View File
@@ -207,6 +207,20 @@ export const saveProcessRoute = {
output: z.object({ pkg: processPackageSchema, report: closureReportSchema }),
} as const satisfies RouteContract;
/**
* 版本启停 —— 运行时开关,不改定义一个字。
* 激活某版:同流其余版本一并停用(一条流恰一个生效版本);停用生效版:整条流下线,新起单被拒。
* 在途工作流按启动时钉住的版本跑完,启停对它们零影响。
* 返回这条流启停后的全部版本(新→旧),界面直接以它刷新版本列表。
*/
export const setProcessStateRoute = {
method: 'POST',
path: '/api/gov/process/:id/:version/state',
params: z.object({ id: z.string(), version: z.string() }),
body: z.object({ state: z.enum(['active', 'retired']) }),
output: z.object({ packages: z.array(processPackageSchema) }),
} as const satisfies RouteContract;
/** 画图即校验 —— 与前端画布同源;保存前随手看还差几条,不改任何状态 */
export const validateProcessRoute = {