按你定的六条做: ## ① 台面三页签:总览 · 流程包 · 触发器 「调起规则」改名触发器(键不动,深链不断)。 ## ② 版本收在流程内部,不平铺台面 货架一流程一卡:名字 + 标识 + 「生效 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
142 lines
5.2 KiB
TypeScript
142 lines
5.2 KiB
TypeScript
/**
|
|
* module: process —— 本模块要外界提供的能力(编码规范 §10.1 / §5.3)。
|
|
*/
|
|
import type { ProcessPackage, StaffId, TerminalState, WorkflowState } from '@contracts';
|
|
import type { NodeInstance } from './internal/domain/owner-resolution.ts';
|
|
import type { ActivityContractFace } from './internal/domain/closure.ts';
|
|
export type Clock = {
|
|
now(): Date;
|
|
};
|
|
export type IdGenerator = {
|
|
next(): string;
|
|
};
|
|
/** 工作流实例的持久面 —— 崩溃后可续跑 */
|
|
export type WorkflowRecord = {
|
|
readonly id: string;
|
|
readonly packageId: string;
|
|
readonly packageVersion: number;
|
|
readonly subject: string;
|
|
readonly state: WorkflowState;
|
|
/** 来源:凡单必有来源(触发器 / 登记人 / 上游系统) */
|
|
readonly source: {
|
|
kind: 'periodic' | 'event' | 'registration';
|
|
ref: string;
|
|
by?: string;
|
|
};
|
|
readonly initiation: Record<string, unknown>;
|
|
/**
|
|
* 工作组绑定的**快照**(ADR-011):起单时按来源人装载,之后改组不影响这条单。
|
|
* 留痕要能解释「当时为什么派给他」,而不是被今天的组织调整改写(同 AM 9.1 版本信封)。
|
|
*/
|
|
readonly workGroup?: Record<string, string>;
|
|
/** 各节点输出口载荷 —— 边映射装配的来源 */
|
|
readonly ports: Record<string, Record<string, unknown>>;
|
|
readonly vars: Record<string, unknown>;
|
|
readonly startedAt: string;
|
|
readonly finishedAt?: string;
|
|
};
|
|
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>;
|
|
getWorkflow(id: string): Promise<WorkflowRecord | undefined>;
|
|
listWorkflows(limit: number): Promise<WorkflowRecord[]>;
|
|
updateWorkflow(id: string, patch: Partial<WorkflowRecord>): Promise<WorkflowRecord>;
|
|
listInstances(workflowId: string): Promise<NodeInstance[]>;
|
|
insertInstance(workflowId: string, instance: NodeInstance & {
|
|
taskId?: string;
|
|
approvalId?: string;
|
|
}): Promise<void>;
|
|
updateInstance(workflowId: string, nodeId: string, instance: number, patch: Partial<NodeInstance> & {
|
|
taskId?: string;
|
|
}): Promise<void>;
|
|
findInstanceByTask(taskId: string): Promise<{
|
|
workflowId: string;
|
|
nodeId: string;
|
|
instance: number;
|
|
} | undefined>;
|
|
};
|
|
/** 活动契约面 —— app 注入 `activity-registry`(L4):编排校验与起单都要它 */
|
|
export type ActivityDirectory = {
|
|
faces(): Promise<Map<string, ActivityContractFace>>;
|
|
};
|
|
/** 岗位与在岗人员 —— app 注入 `org`(L1):负责人四策略解析的候选集 */
|
|
export type OrgDirectory = {
|
|
positions(): Promise<Set<string>>;
|
|
candidates(position: string): Promise<Array<{
|
|
staffId: string;
|
|
name: string;
|
|
openTasks: number;
|
|
}>>;
|
|
/** 某人的工作组绑定(ADR-011)—— 起单时按来源人取,取不到即不装载 */
|
|
workGroupBinding(staffId: string): Promise<Readonly<Record<string, string>> | undefined>;
|
|
/** 在岗与否 —— 绑定的人已离岗就不硬用,退回四策略 */
|
|
isActive(staffId: string): Promise<boolean>;
|
|
};
|
|
/** 起单与终态 —— app 注入 `task`(L6) */
|
|
export type TaskGateway = {
|
|
start(input: {
|
|
activityId: string;
|
|
ownerId: StaffId;
|
|
title: string;
|
|
workflowId: string;
|
|
nodeId: string;
|
|
data: Record<string, unknown>;
|
|
message?: string;
|
|
}): Promise<{
|
|
id: string;
|
|
}>;
|
|
};
|
|
/** 审批点 —— app 注入 `human-tool`(L3):审批单卡到审批人工位 */
|
|
export type ApprovalGateway = {
|
|
raise(input: {
|
|
workflowId: string;
|
|
nodeId: string;
|
|
approverId: StaffId;
|
|
subject: string;
|
|
basis: ReadonlyArray<{
|
|
k: string;
|
|
v: string;
|
|
}>;
|
|
}): Promise<{
|
|
id: string;
|
|
}>;
|
|
};
|
|
/**
|
|
* 调用接口的入参校验 —— app 注入 ajv。
|
|
* 「凡单必有来源」的另一半是「凡单必合契约」:不合契约**不得起单**,
|
|
* 且必须在**建工作流之前**判掉,否则会留下一条永远推不动的在途单。
|
|
*/
|
|
export type SchemaValidator = {
|
|
validate(schema: Record<string, unknown>, value: unknown): {
|
|
ok: boolean;
|
|
errors: string[];
|
|
};
|
|
};
|
|
/** S-FEEL —— app 注入 `@sfeel`:流转零 LLM */
|
|
export type Sfeel = {
|
|
condition(expression: string, scope: Readonly<Record<string, unknown>>): boolean;
|
|
expression(expression: string, scope: Readonly<Record<string, unknown>>): unknown;
|
|
};
|
|
/** 流转记入事件 —— app 注入 `event`(L1) */
|
|
export type FlowEventPublisher = {
|
|
publish(input: {
|
|
workflowId: string;
|
|
kind: string;
|
|
detail: Record<string, unknown>;
|
|
}): Promise<void>;
|
|
};
|
|
/** 终态回执 —— 登记人收消息 */
|
|
export type ReceiptNotifier = {
|
|
notify(input: {
|
|
to: StaffId;
|
|
workflowId: string;
|
|
subject: string;
|
|
state: WorkflowState;
|
|
}): Promise<void>;
|
|
};
|
|
export type { TerminalState };
|