三面拆分⑯:流程包详情改三栏 · 字段只声明 · 版本可删可改名

**字段只声明,不做任何要求**(用户定):编排层不再碰字段 ——
  · 激活闸只剩两面:结构闭合(dangling-node / multi-default-edge)与活动装载
    (unknown-activity);字段类的检查(同名不同型、容忍分支被必填依赖)全部退役。
  · 节点属性卡的「数据接口」从「推来源」改成**纯声明**:照活动契约列出吃什么、吐什么,
    不标必填、不推来源、不提补全 —— 缺了怎么补是**活动内实现**的事,不在这一层现形。
  · 字段类型支持数组:统一写成 `T[]`(如 `any[]`),出口口的字段也带上类型
    (原来只有字段名)。实测 A2 的 benchmark 口显示「rows any[] · needSourcing boolean」。

**详情页改三栏**:左窄轨版本(132px) | 中流程图 | 右节点属性。
  · 版本条一版一行(v几 · 状态 · 点/边计数),动作只在**看着的那一版**上展开 ——
    窄栏放不下每行一排钮,也不该让人误点别版。
  · 删掉「激活 · 双判据」整卡:没选节点时,属性栏就地列出还差哪几条(点一条定位过去);
    两面都齐时盖一枚小检验章,不再占一整张卡。
  · 删掉节点拣选行 —— 直接在图上点节点。
  · 属性卡用包壳绝对定位:行高由中栏的图定,属性长了在卡内滚,不把图拉长
    (实测卡高 467、明细内容 1039、可滚,图框不受影响)。窄轨里属性小卡改单列竖排。
  · 顺带校正:上一轮那条「图框下沿对齐左列底栏顶线」的 20px 留白,
    三栏改版后左栏已无底栏,没有对齐对象了 —— 改回四边等距(18px)。

**版本可删可改名**(新):
  · 删:生效版不可删(先停用,ActiveVersionError → 409)、在途钉版不可删
    (那些单要按它跑完,PinnedVersionError → 409);删掉正看的那版会落到剩下的最新版,
    一版不剩就回货架。
  · 改名:名字是**这条流的身份**,一次改及于所有版本 —— 版本列表上只显示 v1/v2…,
    若只改一版的名,列表上什么也看不见,人会以为没生效。
  · 5 条单测守两道闸与改名口径;实测走查:改名落库 → 改回;生效版删 409、停用版删 200。

process CONTRACT.md 与产品设计.md 同步。五闸绿。

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-29 16:14:54 +08:00
co-authored by Claude Opus 5
parent f03f23f2ca
commit c9ac058bdf
22 changed files with 775 additions and 396 deletions
+45 -5
View File
@@ -35,6 +35,8 @@ import {
resumeTaskRoute,
staffIdSchema,
taskIdSchema,
deleteProcessVersionRoute,
renameProcessRoute,
setNodeOwnerRoute,
setTriggerConfirmRoute,
listNodeOwnersRoute,
@@ -47,7 +49,7 @@ import { extractFields } from '@modules/channel';
import { assembleInput, scopeOf } from '@modules/trigger';
import { InstallRejectedError } from '@modules/activity-registry';
import type { InstallVerdict } from '@modules/activity-registry';
import { NotClosedError, PinnedVersionError, StaleVersionError } from '@modules/process';
import { ActiveVersionError, NotClosedError, PinnedVersionError, StaleVersionError } from '@modules/process';
import type { Container } from '../container.ts';
import type { AppEnv } from '../http-env.ts';
@@ -486,6 +488,40 @@ export function registerGovernanceOpsRoutes(app: Hono<AppEnv>, container: Contai
});
/** 版本启停:激活即独占,停用即下线 —— 在途按启动版本跑完,零影响 */
/** 删一版:生效版先停用(409),在途钉版不许删(409)—— 那些单要按它跑完 */
app.delete(deleteProcessVersionRoute.path, async (c) => {
const denied = await governProcess(c.get('account'));
if (denied) return c.json(denied, denyStatus(denied));
try {
const packages = await container.process.deleteVersion(c.req.param('id'), Number(c.req.param('version')));
return c.json({ packages });
} catch (error) {
if (error instanceof ActiveVersionError || error instanceof PinnedVersionError) {
const body: ApiError = { code: 'conflict', message: error.message };
return c.json(body, 409);
}
const body: ApiError = { code: 'not-found', message: error instanceof Error ? error.message : String(error) };
return c.json(body, 404);
}
});
/** 改名:名字是这条流的身份 —— 一次改及于所有版本;在途单不受影响(名字不参与流转) */
app.post(renameProcessRoute.path, async (c) => {
const denied = await governProcess(c.get('account'));
if (denied) return c.json(denied, denyStatus(denied));
const parsed = renameProcessRoute.body.safeParse(await c.req.json());
if (!parsed.success) {
const body: ApiError = { code: 'validation-failed', message: '流程名不合协议(1–40 字)' };
return c.json(body, 400);
}
try {
return c.json({ packages: await container.process.rename(c.req.param('id'), parsed.data.name.trim()) });
} catch (error) {
const body: ApiError = { code: 'not-found', message: error instanceof Error ? error.message : String(error) };
return c.json(body, 404);
}
});
app.post(setProcessStateRoute.path, async (c) => {
const denied = await governProcess(c.get('account'));
if (denied) return c.json(denied, denyStatus(denied));
@@ -661,21 +697,25 @@ export function registerGovernanceOpsRoutes(app: Hono<AppEnv>, container: Contai
const faces = await Promise.all(
cards.map(async (card) => {
const manifest = await container.registry.manifestOf(card.activityId);
type FieldSpec = { type?: string; items?: { type?: string } };
const data = manifest.contract.input.data as {
required?: string[];
properties?: Record<string, { type?: string }>;
properties?: Record<string, FieldSpec>;
};
const required = data.required ?? [];
const props = data.properties ?? {};
/** 类型的人读写法 —— **数组写成 `T[]`**:声明得看得出是一个还是一串 */
const typeOf = (spec: FieldSpec | undefined): string =>
spec?.type === 'array' ? `${spec.items?.type ?? 'any'}[]` : (spec?.type ?? 'unknown');
const portFields = (payload: unknown) => {
const p = payload as { properties?: Record<string, unknown> } | undefined;
return Object.keys(p?.properties ?? {});
const p = payload as { properties?: Record<string, FieldSpec> } | undefined;
return Object.entries(p?.properties ?? {}).map(([name, spec]) => ({ name, type: typeOf(spec) }));
};
return {
requiredInputs: required,
inputFields: Object.entries(props).map(([name, spec]) => ({
name,
type: spec.type ?? 'unknown',
type: typeOf(spec),
required: required.includes(name),
})),
// 必发口与选择组一并列出:调用方只凭口名与 schema 就能衔接下游
+7 -1
View File
@@ -1,6 +1,6 @@
# module: process
职责: 流程层:流程包定义与版本、**闭合校验**(边只表达顺序与条件 · 画图不检查 · 不闭合不得激活)、
职责: 流程层:流程包定义与版本(建/克隆/启停/**删/改名**)、**闭合校验**(边只表达顺序与条件 · 画图不检查 · 不闭合不得激活)、
负责人四策略的确定性解析、工作流状态的聚合投影、失败走向的消化判定、节点实例重开
不负责: 活动的执行(见 task 与范式引擎)、触发器(与流程包**平级**,见 trigger)、
画布的绘制(见 apps/web 的 gov-process)
@@ -25,6 +25,12 @@
被依赖: trigger, app(治理台 · 工作流管理)
不变量(OS 03 / 3.2):
- **字段只声明,不做任何要求**(2026-07-30 用户定):编排层不校验字段 —— 要什么、够不够、
类型对不对全归活动自己。激活闸只剩两面:**结构闭合**(dangling-node / multi-default-edge)
与**活动装载**(unknown-activity);字段类的三条检查(边未映射 / 必填未映射 / 类型相抵)全退役。
- **版本可删,两道闸**:生效版不可删(先停用,`ActiveVersionError`)、在途钉版不可删
(那些单要按它跑完,`PinnedVersionError`)。**改名及于整条流的所有版本** ——
名字是流的身份,不是某一版的属性。
- **数据按字段名流动**(2026-07-30 对齐 Mastra,替代「连线即映射」):边不带映射,
只表达顺序与条件(条件读上游输出字段)。节点入参 = 铺平作用域
(起单入参 ← 沿途各输出口字段 ← 刚发射的载荷,后者赢)按下游契约取;
@@ -56,8 +56,8 @@ describe('checkClosure —— 不闭合不得激活', () => {
expect(report.closed).toBe(true);
});
it('同名字段类型相抵即拦 —— 名字即管道,名同型异必坏', () => {
// 上游 done 口发 quote: number …下游若声明 quote: string 就是硬伤
it('字段一概不查(2026-07-30「只声明,不做任何要求」)—— 名同型异也不拦激活', () => {
// 上游 done 口发 quote: number,下游声明 quote: string:从前是 type-conflict,现在归活动自己管
const clash = new Map([
['a4', face({ inputTypes: { pn: 'string', quote: 'string' }, requiredInputs: ['pn'] })],
]);
@@ -74,8 +74,7 @@ describe('checkClosure —— 不闭合不得激活', () => {
{ id: 'e2', from: 'A4' as never, to: 'end' as never, reopen: false },
],
});
const violations = checkClosure({ pkg: chain, contracts: clash }).violations;
expect(violations.filter((v) => v.kind === 'type-conflict').map((v) => v.field)).toEqual(['quote']);
expect(checkClosure({ pkg: chain, contracts: clash }).closed, '结构闭合、活动在库 —— 就该激活得了').toBe(true);
});
it('引用未安装的活动即拦', () => {
@@ -124,40 +123,6 @@ describe('checkClosure —— 不闭合不得激活', () => {
).toEqual([]);
});
it('容忍分支的输出不得被下游必填依赖', () => {
const tolerant = pkg({
nodes: [
{ id: 'start' as never, kind: 'start', label: '起点' },
{
id: 'A3' as never,
kind: 'activity',
label: 'A3 渠道寻源',
activityId: 'a4' as never,
failure: { kind: 'tolerate' },
},
{
id: 'A4' as never,
kind: 'activity',
label: 'A4 成本定价',
activityId: 'a4' as never,
failure: { kind: 'terminate' },
},
{ id: 'end' as never, kind: 'end', label: '终点' },
],
edges: [
{ id: 'e0', from: 'start' as never, to: 'A3' as never, reopen: false },
{
id: 'e1',
from: 'A3' as never,
to: 'A4' as never,
reopen: false,
},
{ id: 'e2', from: 'A4' as never, to: 'end' as never, reopen: false },
],
});
const violations = checkClosure({ pkg: tolerant, contracts }).violations;
expect(violations.map((v) => v.kind)).toContain('tolerate-required-dependency');
});
});
describe('resolveOwner —— 四策略确定性解析', () => {
@@ -58,6 +58,8 @@ function harness(seed: ProcessPackage[], pinned = 0) {
insertInstance: async () => undefined,
updateInstance: async () => undefined,
findInstanceByTask: async () => undefined,
deletePackage: async () => undefined,
renameFlow: async () => undefined,
nodeOwners: async () => ({}),
setNodeOwner: async () => undefined,
},
@@ -68,6 +68,8 @@ function harness(validationErrors: readonly string[]): Harness {
},
updateInstance: async () => undefined,
findInstanceByTask: async () => undefined,
deletePackage: async () => undefined,
renameFlow: async () => undefined,
nodeOwners: async () => ({}),
setNodeOwner: async () => undefined,
},
@@ -0,0 +1,117 @@
/**
* 版本的删与改名(2026-07-30 用户要的两件)—— 守两道闸、一条口径。
*
* · **生效版不可删**:先停用。挡的是「手滑把正在起单的定义删了」;
* · **在途钉版不可删**:那些工作流按启动时钉住的这一版跑完,定义没了就再也推不动
* —— 与「在途钉版不可改」同一条理由、同一个错(PinnedVersionError);
* · **改名及于整条流的所有版本**:名字是流的身份,不是某一版的属性。
* (版本列表上只显示 v1/v2…,若只改一版的名,列表上什么也看不见,人会以为没生效。)
*/
import { describe, expect, it } from 'vitest';
import type { ProcessPackage } from '@contracts';
import { processPackageSchema } from '@contracts';
import { ActiveVersionError, PinnedVersionError } from '../contract.ts';
import { createProcessService } from '../internal/usecase/process-service.ts';
import type { ProcessServiceDeps } from '../internal/usecase/process-service.ts';
const pkg = (version: number, state: 'active' | 'retired'): ProcessPackage =>
processPackageSchema.parse({
id: 'quote',
name: '统一报价',
version,
state,
callInterface: { input: { type: 'object' }, output: { type: 'object' } },
nodes: [
{ kind: 'start', id: 'S', label: '开始' },
{ kind: 'end', id: 'E', label: '结束' },
],
edges: [{ id: 'e1', from: 'S', to: 'E', reopen: false }],
variables: [],
});
type Harness = {
service: ReturnType<typeof createProcessService>;
rows: ProcessPackage[];
};
function harness(rows: ProcessPackage[], pinned = 0): Harness {
const live = [...rows];
const deps: ProcessServiceDeps = {
store: {
savePackage: async () => undefined,
updatePackageDefinition: async () => undefined,
countPinnedInFlight: async () => pinned,
setPackageState: async () => undefined,
deletePackage: async (id: string, version: number) => {
const i = live.findIndex((p) => p.id === id && p.version === version);
if (i >= 0) live.splice(i, 1);
},
renameFlow: async (id: string, name: string) => {
for (let i = 0; i < live.length; i += 1) {
if (live[i]!.id === id) live[i] = { ...live[i]!, name };
}
},
getPackage: async (id: string, version?: number) =>
live.find((p) => p.id === id && (version === undefined || p.version === version)),
listPackages: async () => live,
insertWorkflow: async () => undefined,
getWorkflow: async () => undefined,
updateWorkflow: async () => undefined as never,
listWorkflows: async () => [],
insertInstance: async () => undefined,
updateInstance: async () => undefined,
listInstances: async () => [],
findInstanceByTask: async () => undefined,
nodeOwners: async () => ({}),
setNodeOwner: async () => undefined,
},
activities: { contractFaceOf: async () => undefined },
tasks: { start: async () => 't-1' as never, terminate: async () => undefined },
approvals: { raise: async () => undefined },
org: { resolveOwner: async () => 's-1' as never, workGroupBinding: async () => undefined },
events: { publish: async () => undefined },
receipts: { notify: async () => undefined },
schemas: { validate: () => ({ ok: true, errors: [] }) },
sfeel: { condition: () => true, expression: () => undefined },
trace: {
beginWorkflow: async () => undefined,
beginNode: async () => undefined,
endNode: async () => undefined,
endWorkflow: async () => undefined,
} as never,
clock: { now: () => new Date('2026-07-30T00:00:00.000Z') },
ids: { next: () => 'x' },
} as never;
return { service: createProcessService(deps), rows: live };
}
describe('版本的删与改名', () => {
it('停用版可删 —— 删完列表里就没有它了', async () => {
const h = harness([pkg(2, 'active'), pkg(1, 'retired')]);
const after = await h.service.deleteVersion('quote', 1);
expect(after.map((p) => p.version)).toEqual([2]);
});
it('生效版不可删 —— 先停用', async () => {
const h = harness([pkg(2, 'active'), pkg(1, 'retired')]);
await expect(h.service.deleteVersion('quote', 2)).rejects.toBeInstanceOf(ActiveVersionError);
expect(h.rows.map((p) => p.version), '一行都没动').toEqual([2, 1]);
});
it('在途钉版不可删 —— 那些单要按它跑完', async () => {
const h = harness([pkg(2, 'active'), pkg(1, 'retired')], 3);
await expect(h.service.deleteVersion('quote', 1)).rejects.toBeInstanceOf(PinnedVersionError);
expect(h.rows).toHaveLength(2);
});
it('改名及于所有版本 —— 名字是这条流的身份', async () => {
const h = harness([pkg(2, 'active'), pkg(1, 'retired')]);
const after = await h.service.rename('quote', '统一报价(新)');
expect(after.map((p) => p.name)).toEqual(['统一报价(新)', '统一报价(新)']);
});
it('改不存在的流:如实报,不静默建一条', async () => {
const h = harness([pkg(1, 'retired')]);
await expect(h.service.rename('nope', 'X')).rejects.toThrow(/不存在/);
});
});
+13
View File
@@ -79,6 +79,11 @@ export type ProcessService = {
listWorkflows(limit?: number): Promise<WorkflowRecord[]>;
instancesOf(workflowId: string): Promise<NodeInstance[]>;
/** 删一版:生效版与在途钉版都不许删(见 `ActiveVersionError` / `PinnedVersionError`) */
deleteVersion(id: string, version: number): Promise<ProcessPackage[]>;
/** 改名:名字是这条流的身份,一次改及于所有版本 */
rename(id: string, name: string): Promise<ProcessPackage[]>;
/* ── 节点负责人(运营配置):显式指派 > 这里 > 轮值 ── */
nodeOwners(packageId: string): Promise<Record<string, string>>;
setNodeOwner(packageId: string, nodeId: string, staffId: string | undefined): Promise<void>;
@@ -109,6 +114,14 @@ export type ProcessService = {
};
/** 起单入参不合调用接口契约 —— 在建工作流之前判掉,不留半条在途单 */
/** 生效版不可删 —— 先停用:这道闸挡的是「手滑把正在起单的定义删了」 */
export class ActiveVersionError extends Error {
constructor(readonly id: string, readonly version: number) {
super(`v${version} 正在生效,不能删 —— 先停用它`);
this.name = 'ActiveVersionError';
}
}
export class CallInterfaceViolationError extends Error {
constructor(
readonly packageId: string,
+1 -1
View File
@@ -18,7 +18,7 @@ export type {
WorkflowRecord,
WorkflowState,
} from './contract.ts';
export { CallInterfaceViolationError, NotClosedError,
export { ActiveVersionError, CallInterfaceViolationError, NotClosedError,
PinnedVersionError, PositionVacantError, StaleVersionError } from './contract.ts';
export type {
@@ -58,6 +58,24 @@ export function createProcessStore(db: Db): ProcessStore {
.where(and(eq(processPackageTable.id, id), eq(processPackageTable.version, version)));
},
async deletePackage(id, version) {
await db
.delete(processPackageTable)
.where(and(eq(processPackageTable.id, id), eq(processPackageTable.version, version)));
},
async renameFlow(id, name) {
// 名字在列上与 definition(jsonb)里各有一份 —— 与 setPackageState 同一口径,两处一起改
const rows = await db.select().from(processPackageTable).where(eq(processPackageTable.id, id));
for (const row of rows) {
const definition = { ...(row.definition as Record<string, unknown>), name };
await db
.update(processPackageTable)
.set({ name, definition })
.where(and(eq(processPackageTable.id, id), eq(processPackageTable.version, row.version)));
}
},
async getPackage(id, version) {
const conditions = [eq(processPackageTable.id, id)];
if (version !== undefined) conditions.push(eq(processPackageTable.version, version));
@@ -55,7 +55,7 @@ export function checkClosure(input: ClosureInput): ClosureReport {
}
if (node.kind === 'activity') {
violations.push(...checkActivityNode(node, incoming.get(node.id) ?? [], contracts, byId));
violations.push(...checkActivityNode(node, contracts));
}
// 岗位不入编排(活动点与审批点同一口径,2026-07-28 第五轮):没有岗位可核
/*
@@ -77,16 +77,13 @@ export function checkClosure(input: ClosureInput): ClosureReport {
}
}
violations.push(...checkTolerateDependencies(pkg, contracts, byId));
return { closed: violations.length === 0, violations };
}
/** 活动点只查一件事:**引用的活动装没装**(判据二);字段一概不查(只声明,不要求) */
function checkActivityNode(
node: Extract<ProcessNode, { kind: 'activity' }>,
inbound: readonly Edge[],
contracts: ReadonlyMap<string, ActivityContractFace>,
byId: ReadonlyMap<string, ProcessNode>,
): ClosureViolation[] {
const out: ClosureViolation[] = [];
const face = contracts.get(node.activityId);
@@ -101,30 +98,10 @@ function checkActivityNode(
}
/*
* 数据按字段名流动(2026-07-30 对齐 Mastra):边不带映射,「必填有没有着落」
* 不再是激活阻断 —— 宽进纪律下,缺的字段运行时由受理补全单找人收齐。
* 激活闸只留 schema 咬合这一条硬伤:**上游产出与下游输入同名却不同型**,
* 名字既是管道,名同型异一定坏 —— 这必须在设计期拦下。
* **字段只声明,不做任何要求**(2026-07-30 用户定):编排层不校验字段 ——
* 要什么、够不够、类型对不对,全归活动自己;缺了由活动内的实现去补,不在这一层现形。
* 于是激活闸只剩两面:**结构闭合**(下面几条)与**活动装载**(上面的 unknown-activity)。
*/
for (const edge of inbound) {
const upstream = byId.get(edge.from);
if (upstream?.kind !== 'activity') continue;
const upFace = contracts.get(upstream.activityId);
if (upFace === undefined) continue;
for (const [field, targetType] of Object.entries(face.inputTypes)) {
for (const portFields of Object.values(upFace.outputs)) {
const sourceType = portFields[field];
if (sourceType !== undefined && sourceType !== targetType) {
out.push({
kind: 'type-conflict',
edgeId: edge.id,
field,
message: `边 ${edge.id} 同名字段 ${field} 类型相抵:上游 ${sourceType} → 下游 ${targetType}(名字即管道,名同型异必坏)`,
});
}
}
}
}
/*
* 多入边**不再判「隐式合并」**(产品决策 2026-07-28:网关去节点化)。
@@ -136,46 +113,6 @@ function checkActivityNode(
return out;
}
/**
* 容忍(仅限 AND 分支):静态校验其**下游映射不得依赖该分支的必填字段**(OS 03 失败走向)。
*/
function checkTolerateDependencies(
pkg: ProcessPackage,
contracts: ReadonlyMap<string, ActivityContractFace>,
byId: ReadonlyMap<string, ProcessNode>,
): ClosureViolation[] {
const out: ClosureViolation[] = [];
const tolerated = new Set(
pkg.nodes.filter((n) => n.kind === 'activity' && n.failure.kind === 'tolerate').map((n) => n.id),
);
if (tolerated.size === 0) return out;
for (const edge of pkg.edges) {
if (!tolerated.has(edge.from)) continue;
const target = byId.get(edge.to);
if (target?.kind !== 'activity') continue;
const face = contracts.get(target.activityId);
if (face === undefined) continue;
const source = byId.get(edge.from);
if (source?.kind !== 'activity') continue;
const srcFace = contracts.get(source.activityId);
if (srcFace === undefined) continue;
// 按名判依赖:容忍分支产出的字段名恰是下游的必填 —— 分支可空,依赖它的必填就会空
const produced = new Set(Object.values(srcFace.outputs).flatMap((fields) => Object.keys(fields)));
for (const required of face.requiredInputs) {
if (produced.has(required)) {
out.push({
kind: 'tolerate-required-dependency',
edgeId: edge.id,
nodeId: target.id,
field: required,
message: `容忍分支 ${edge.from} 产出的 ${required} 被下游必填依赖 —— 分支可空,依赖它的必填就会空`,
});
}
}
}
return out;
}
/** 不闭合不得**保存** —— 没有发布这道门,闸落在保存上,这是它的唯一判据 */
export function closed(report: ClosureReport): boolean {
@@ -5,7 +5,7 @@
*/
import type { ProcessPackage } from '@contracts';
import type { ProcessService, StartWorkflowInput } from '../../contract.ts';
import { NotClosedError, PinnedVersionError, StaleVersionError } from '../../contract.ts';
import { ActiveVersionError, NotClosedError, PinnedVersionError, StaleVersionError } from '../../contract.ts';
import type {
ActivityDirectory,
ApprovalGateway,
@@ -154,6 +154,32 @@ export function createProcessService(deps: ProcessServiceDeps): ProcessService {
return after.filter((p) => p.id === id).sort((a, b) => b.version - a.version);
},
async deleteVersion(id, version) {
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 (target.state === 'active') throw new ActiveVersionError(id, version);
/*
* 在途钉版不可删:那些工作流按启动时钉住的这一版跑完,定义没了就再也推不动 ——
* 与「在途钉版不可改」同一条理由、同一个错。
*/
const pinned = await deps.store.countPinnedInFlight(id, version);
if (pinned > 0) throw new PinnedVersionError(id, version, pinned);
await deps.store.deletePackage(id, version);
const after = await deps.store.listPackages();
return after.filter((p) => p.id === id).sort((a, b) => b.version - a.version);
},
async rename(id, name) {
const all = await deps.store.listPackages();
if (!all.some((p) => p.id === id)) throw new Error(`流程包不存在:${id}`);
// 名字是这条流的身份 —— 一次改及于所有版本(在途单不受影响:名字不参与流转)
await deps.store.renameFlow(id, name);
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();
},
+3
View File
@@ -76,6 +76,9 @@ export type ProcessStore = {
countPinnedInFlight(id: string, version: number): Promise<number>;
/** 只改状态,不动定义 —— state 在列上与 definition 里各存一份,两处要一起改 */
setPackageState(id: string, version: number, state: 'active' | 'retired'): Promise<void>;
deletePackage(id: string, version: number): Promise<void>;
/** 改名及于这条流的所有版本 —— 名字是流的身份,不是版本的属性 */
renameFlow(id: string, name: string): Promise<void>;
getPackage(id: string, version?: number): Promise<ProcessPackage | undefined>;
listPackages(): Promise<ProcessPackage[]>;