Archived
M5(2/3): process 用例与流转 —— 流程引擎的确定性调度
- routing domain:选边(XOR 恰取一条 + 默认分支兜底;AND 全走)、边映射装配(四类来源 + S-FEEL 转换)、AND 汇合回齐(容忍分支不阻汇合)、审批点驳回走向声明式 - process-service:起单 → 调度节点 → 负责人四策略解析(结果与依据留痕)→ 活动起单 / 审批点卡工位 / 终点收束按来源回执;任务终态到达继续流转或走失败走向四选一; 审批驳回按声明回指定节点重开新实例;状态每次变动后重算聚合投影 - adapter:流程包(id × version)、工作流实例、节点实例三表持久 —— 崩溃后可续跑 ADR 待补项登记为 debt M5-1:§6.1 的 Mastra workflow 编译器作为可替换 adapter 后补, 当前由自持确定性状态机承担,语义等价、usecase 不动 325 个测试通过,门禁五道全绿 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
co-authored by
Claude Opus 5
parent
291fc39179
commit
a8f6eef610
@@ -6,11 +6,16 @@
|
||||
画布的绘制(见 apps/web 的 gov-process)
|
||||
|
||||
公开出口: index.ts
|
||||
- 类型:`ClosureInput` `ClosureReport` `ClosureViolation` `ActivityContractFace`
|
||||
`Candidate` `Resolution` `NodeInstance` `WorkflowState`
|
||||
- 类型:`ProcessService` `StartWorkflowInput` `WorkflowRecord` `ClosureReport` `ClosureViolation`
|
||||
`ActivityContractFace` `Candidate` `Resolution` `NodeInstance` `WorkflowState` `Dispatch`
|
||||
- 错误:`NotPublishableError` `PositionVacantError`
|
||||
- 端口类型:`ProcessStore` `ActivityDirectory` `OrgDirectory` `TaskGateway` `ApprovalGateway`
|
||||
`Sfeel` `FlowEventPublisher` `ReceiptNotifier`
|
||||
- 用例工厂:`createProcessService(deps): ProcessService`
|
||||
- 纯函数:`checkClosure` `publishable` `resolveOwner` `aggregateWorkflowState`
|
||||
`isDigested` `nextInstanceNumber`
|
||||
`isDigested` `nextInstanceNumber` `selectEdges` `assemble` `joinReady`
|
||||
`rejectRouteOf` `dispatchOf` `startNodeOf`
|
||||
- adapter:`createProcessStore`;Drizzle 表:`processPackageTable` `workflowTable` `nodeInstanceTable`
|
||||
|
||||
层级: L7(编码规范 §4)
|
||||
依赖: @contracts;task(L6)、@sfeel
|
||||
@@ -26,6 +31,17 @@
|
||||
- **失败走向的消化**:改道 / 容忍 / 升审批一经生效即消化;终止流待人工处置中算异常
|
||||
- **流级重做一律重开新实例** —— 同节点、新任务 ID、留痕关联,不复活旧任务
|
||||
|
||||
## M5 待补(docs/PROGRESS.md)
|
||||
port.ts 与 usecase:流程包的存取与发布、工作流实例的起单与流转
|
||||
(Mastra `createWorkflow` 编译在 `internal/adapter/workflow-compiler.ts`,编码规范 §6.1)。
|
||||
其它不变量:
|
||||
- **流转零 LLM**:选边与条件求值全部经 `@sfeel`;判定依据入事件记录
|
||||
- **XOR 恰取一条**:条件命中优先,无条件的边作默认分支兜底
|
||||
- **AND 汇合要回齐**:未回齐就不进汇合节点;容忍分支以「已消化」计,不阻汇合
|
||||
- **流程层不自动重试活动**(过程内重试归 Harness);
|
||||
流级重做一律人裁决后**重开新任务实例**
|
||||
- **驳回走向声明式**:审批点的驳回按 `reject` 声明回指定节点重开新实例,不由临场决定
|
||||
- **凡单必有来源**:`source` 必填;终点按输出声明交付、按来源回执
|
||||
|
||||
## M5 待补(docs/PROGRESS.md · docs/debt.md M5-1)
|
||||
`internal/adapter/workflow-compiler.ts` —— 编码规范 §6.1 规定流程包动态编译为 Mastra
|
||||
`createWorkflow`/`createStep`(XOR → `.branch()`,AND → `.parallel()`,节点 `suspend()`/`resume()`)。
|
||||
当前流转由本模块自持的**确定性状态机 + `sys.node_instance` 持久**承担,语义等价且已可测;
|
||||
Mastra 编译器作为可替换的 adapter 后补,替换时 usecase 不动。
|
||||
|
||||
@@ -7,24 +7,89 @@
|
||||
* M5 进行中:domain 已落(闭合校验 / 四策略解析 / 状态聚合),
|
||||
* 用例与 Mastra 工作流编译随后(编码规范 §6.1)。
|
||||
*/
|
||||
import type { ClosureReport, ClosureViolation, OwnerSpec, ProcessPackage, WorkflowState } from '@contracts';
|
||||
import type {
|
||||
ClosureReport,
|
||||
ClosureViolation,
|
||||
OwnerSpec,
|
||||
ProcessNode,
|
||||
ProcessPackage,
|
||||
TerminalState,
|
||||
WorkflowState,
|
||||
} from '@contracts';
|
||||
import type { ActivityContractFace, ClosureInput } from './internal/domain/closure.ts';
|
||||
import type { Candidate, NodeInstance, ResolveContext, Resolution } from './internal/domain/owner-resolution.ts';
|
||||
import type { AssembleScope, Dispatch, RejectRoute } from './internal/domain/routing.ts';
|
||||
import type { WorkflowRecord } from './port.ts';
|
||||
|
||||
export type {
|
||||
ActivityContractFace,
|
||||
AssembleScope,
|
||||
Candidate,
|
||||
ClosureInput,
|
||||
Dispatch,
|
||||
RejectRoute,
|
||||
WorkflowRecord,
|
||||
ClosureReport,
|
||||
ClosureViolation,
|
||||
NodeInstance,
|
||||
OwnerSpec,
|
||||
ProcessPackage,
|
||||
ProcessNode,
|
||||
ResolveContext,
|
||||
Resolution,
|
||||
TerminalState,
|
||||
WorkflowState,
|
||||
};
|
||||
|
||||
/** 起单入参 —— 触发器与上游系统一律经**调用接口**调起(OS 03) */
|
||||
export type StartWorkflowInput = {
|
||||
readonly packageId: string;
|
||||
readonly packageVersion?: number;
|
||||
readonly subject: string;
|
||||
/** 凡单必有来源:触发器即来源;纪要登记起单的,登记人即来源 */
|
||||
readonly source: { kind: 'periodic' | 'event' | 'registration'; ref: string; by?: string };
|
||||
readonly data: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type ProcessService = {
|
||||
savePackage(pkg: ProcessPackage): Promise<ProcessPackage>;
|
||||
listPackages(): Promise<ProcessPackage[]>;
|
||||
getPackage(id: string, version?: number): Promise<ProcessPackage | undefined>;
|
||||
/** 画图即校验 —— 与前端画布同源 */
|
||||
validate(pkg: ProcessPackage): Promise<ClosureReport>;
|
||||
/** 不闭合不得发布 */
|
||||
publish(id: string, version: number): Promise<ClosureReport>;
|
||||
|
||||
start(input: StartWorkflowInput): Promise<WorkflowRecord>;
|
||||
getWorkflow(id: string): Promise<WorkflowRecord>;
|
||||
listWorkflows(limit?: number): Promise<WorkflowRecord[]>;
|
||||
instancesOf(workflowId: string): Promise<NodeInstance[]>;
|
||||
|
||||
/** 任务终态到达:完成即继续流转;非完成即走声明的失败走向 */
|
||||
onTaskTerminal(
|
||||
taskId: string,
|
||||
terminal: TerminalState,
|
||||
ports: Record<string, Record<string, unknown>>,
|
||||
): Promise<WorkflowState | undefined>;
|
||||
applyFailure(
|
||||
workflowId: string,
|
||||
nodeId: string,
|
||||
instance: number,
|
||||
node: Extract<ProcessNode, { kind: 'activity' }>,
|
||||
terminal: TerminalState,
|
||||
): Promise<WorkflowState>;
|
||||
/** 审批裁决:通过继续;驳回按声明走向回指定节点**重开新实例** */
|
||||
onApprovalVerdict(
|
||||
workflowId: string,
|
||||
nodeId: string,
|
||||
verdict: 'pass' | 'reject',
|
||||
payload: Record<string, unknown>,
|
||||
): Promise<WorkflowState>;
|
||||
/** 状态是聚合投影 —— 每次变动后重算 */
|
||||
refreshState(workflowId: string): Promise<WorkflowState>;
|
||||
terminate(workflowId: string, reason: string): Promise<WorkflowState>;
|
||||
};
|
||||
|
||||
export class NotPublishableError extends Error {
|
||||
constructor(readonly report: ClosureReport) {
|
||||
super(`流程包不闭合,不得发布:${report.violations.map((v) => v.message).join('; ')}`);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
export type {
|
||||
ActivityContractFace,
|
||||
AssembleScope,
|
||||
Candidate,
|
||||
ClosureInput,
|
||||
ClosureReport,
|
||||
@@ -10,12 +11,40 @@ export type {
|
||||
NodeInstance,
|
||||
OwnerSpec,
|
||||
ProcessPackage,
|
||||
ProcessService,
|
||||
ResolveContext,
|
||||
Resolution,
|
||||
StartWorkflowInput,
|
||||
WorkflowRecord,
|
||||
WorkflowState,
|
||||
} from './contract.ts';
|
||||
export { NotPublishableError, PositionVacantError } from './contract.ts';
|
||||
|
||||
export type {
|
||||
ActivityDirectory,
|
||||
ApprovalGateway,
|
||||
Clock,
|
||||
FlowEventPublisher,
|
||||
IdGenerator,
|
||||
OrgDirectory,
|
||||
ProcessStore,
|
||||
ReceiptNotifier,
|
||||
Sfeel,
|
||||
TaskGateway,
|
||||
} from './port.ts';
|
||||
|
||||
export type { ProcessServiceDeps } from './internal/usecase/process-service.ts';
|
||||
export { createProcessService } from './internal/usecase/process-service.ts';
|
||||
|
||||
export {
|
||||
assemble,
|
||||
dispatchOf,
|
||||
joinReady,
|
||||
rejectRouteOf,
|
||||
selectEdges,
|
||||
startNodeOf,
|
||||
} from './internal/domain/routing.ts';
|
||||
|
||||
export { checkClosure, publishable } from './internal/domain/closure.ts';
|
||||
export {
|
||||
aggregateWorkflowState,
|
||||
@@ -24,3 +53,6 @@ export {
|
||||
nextInstanceNumber,
|
||||
resolveOwner,
|
||||
} from './internal/domain/owner-resolution.ts';
|
||||
|
||||
export { createProcessStore } from './internal/adapter/process-store.ts';
|
||||
export { nodeInstanceTable, processPackageTable, workflowTable } from './internal/adapter/process-schema.ts';
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* process 的 sys schema 表(编码规范 §7 / ADR-002)。
|
||||
* 流程包定义与版本 + 工作流实例 + 节点实例(重开产生新实例,旧实例不再入算)。
|
||||
*/
|
||||
import { boolean, index, integer, jsonb, primaryKey, text, timestamp } from 'drizzle-orm/pg-core';
|
||||
import { sys } from '@shared/db.ts';
|
||||
|
||||
export const processPackageTable = sys.table(
|
||||
'process_package',
|
||||
{
|
||||
id: text('id').notNull(),
|
||||
version: integer('version').notNull(),
|
||||
name: text('name').notNull(),
|
||||
state: text('state').notNull().default('draft'),
|
||||
definition: jsonb('definition').notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
publishedAt: timestamp('published_at', { withTimezone: true }),
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.id, t.version] }), index('process_package_state_idx').on(t.state)],
|
||||
);
|
||||
|
||||
export const workflowTable = sys.table(
|
||||
'workflow',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
packageId: text('package_id').notNull(),
|
||||
packageVersion: integer('package_version').notNull(),
|
||||
subject: text('subject').notNull(),
|
||||
state: text('state').notNull().default('running'),
|
||||
/** 凡单必有来源 */
|
||||
source: jsonb('source').notNull(),
|
||||
initiation: jsonb('initiation').notNull(),
|
||||
ports: jsonb('ports').notNull().default({}),
|
||||
vars: jsonb('vars').notNull().default({}),
|
||||
startedAt: timestamp('started_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
finishedAt: timestamp('finished_at', { withTimezone: true }),
|
||||
},
|
||||
(t) => [index('workflow_state_idx').on(t.state, t.startedAt)],
|
||||
);
|
||||
|
||||
export const nodeInstanceTable = sys.table(
|
||||
'node_instance',
|
||||
{
|
||||
workflowId: text('workflow_id').notNull(),
|
||||
nodeId: text('node_id').notNull(),
|
||||
instance: integer('instance').notNull(),
|
||||
running: boolean('running').notNull().default(false),
|
||||
waiting: boolean('waiting').notNull().default(false),
|
||||
activated: boolean('activated').notNull().default(true),
|
||||
terminal: text('terminal'),
|
||||
failureDigested: boolean('failure_digested'),
|
||||
taskId: text('task_id'),
|
||||
approvalId: text('approval_id'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [
|
||||
primaryKey({ columns: [t.workflowId, t.nodeId, t.instance] }),
|
||||
index('node_instance_task_idx').on(t.taskId),
|
||||
],
|
||||
);
|
||||
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* process 存储 adapter —— Drizzle 只准出现在 adapter(编码规范 §5.3)。
|
||||
*/
|
||||
import { and, asc, desc, eq } from 'drizzle-orm';
|
||||
import type { ProcessPackage, TerminalState, WorkflowState } from '@contracts';
|
||||
import { processPackageSchema } from '@contracts';
|
||||
import type { Db } from '@shared/db.ts';
|
||||
import type { ProcessStore, WorkflowRecord } from '../../port.ts';
|
||||
import type { NodeInstance } from '../domain/owner-resolution.ts';
|
||||
import { nodeInstanceTable, processPackageTable, workflowTable } from './process-schema.ts';
|
||||
|
||||
type WorkflowRow = typeof workflowTable.$inferSelect;
|
||||
|
||||
function toWorkflow(row: WorkflowRow): WorkflowRecord {
|
||||
return {
|
||||
id: row.id,
|
||||
packageId: row.packageId,
|
||||
packageVersion: row.packageVersion,
|
||||
subject: row.subject,
|
||||
state: row.state as WorkflowState,
|
||||
source: row.source as WorkflowRecord['source'],
|
||||
initiation: row.initiation as Record<string, unknown>,
|
||||
ports: row.ports as Record<string, Record<string, unknown>>,
|
||||
vars: row.vars as Record<string, unknown>,
|
||||
startedAt: row.startedAt.toISOString(),
|
||||
...(row.finishedAt === null ? {} : { finishedAt: row.finishedAt.toISOString() }),
|
||||
};
|
||||
}
|
||||
|
||||
export function createProcessStore(db: Db): ProcessStore {
|
||||
return {
|
||||
async savePackage(pkg: ProcessPackage) {
|
||||
await db
|
||||
.insert(processPackageTable)
|
||||
.values({ id: pkg.id, version: pkg.version, name: pkg.name, state: pkg.state, definition: pkg })
|
||||
.onConflictDoUpdate({
|
||||
target: [processPackageTable.id, processPackageTable.version],
|
||||
set: { name: pkg.name, state: pkg.state, definition: pkg },
|
||||
});
|
||||
},
|
||||
|
||||
async getPackage(id, version) {
|
||||
const conditions = [eq(processPackageTable.id, id)];
|
||||
if (version !== undefined) conditions.push(eq(processPackageTable.version, version));
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(processPackageTable)
|
||||
.where(and(...conditions))
|
||||
.orderBy(desc(processPackageTable.version))
|
||||
.limit(1);
|
||||
return rows[0] === undefined ? undefined : processPackageSchema.parse(rows[0].definition);
|
||||
},
|
||||
|
||||
async listPackages() {
|
||||
const rows = await db.select().from(processPackageTable).orderBy(asc(processPackageTable.name));
|
||||
return rows.map((r) => processPackageSchema.parse(r.definition));
|
||||
},
|
||||
|
||||
async publishPackage(id, version) {
|
||||
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;
|
||||
const definition = { ...(row.definition as Record<string, unknown>), state: 'published' };
|
||||
await db
|
||||
.update(processPackageTable)
|
||||
.set({ state: 'published', publishedAt: new Date(), definition })
|
||||
.where(and(eq(processPackageTable.id, id), eq(processPackageTable.version, version)));
|
||||
},
|
||||
|
||||
async insertWorkflow(record) {
|
||||
await db.insert(workflowTable).values({
|
||||
id: record.id,
|
||||
packageId: record.packageId,
|
||||
packageVersion: record.packageVersion,
|
||||
subject: record.subject,
|
||||
state: record.state,
|
||||
source: record.source,
|
||||
initiation: record.initiation,
|
||||
ports: record.ports,
|
||||
vars: record.vars,
|
||||
startedAt: new Date(record.startedAt),
|
||||
});
|
||||
},
|
||||
|
||||
async getWorkflow(id) {
|
||||
const rows = await db.select().from(workflowTable).where(eq(workflowTable.id, id)).limit(1);
|
||||
return rows[0] === undefined ? undefined : toWorkflow(rows[0]);
|
||||
},
|
||||
|
||||
async listWorkflows(limit) {
|
||||
const rows = await db.select().from(workflowTable).orderBy(desc(workflowTable.startedAt)).limit(limit);
|
||||
return rows.map(toWorkflow);
|
||||
},
|
||||
|
||||
async updateWorkflow(id, patch) {
|
||||
const values: Record<string, unknown> = {};
|
||||
if (patch.state !== undefined) values['state'] = patch.state;
|
||||
if (patch.ports !== undefined) values['ports'] = patch.ports;
|
||||
if (patch.vars !== undefined) values['vars'] = patch.vars;
|
||||
if (patch.finishedAt !== undefined) values['finishedAt'] = new Date(patch.finishedAt);
|
||||
const [row] = await db.update(workflowTable).set(values).where(eq(workflowTable.id, id)).returning();
|
||||
if (row === undefined) throw new Error(`工作流不存在:${id}`);
|
||||
return toWorkflow(row);
|
||||
},
|
||||
|
||||
async listInstances(workflowId) {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(nodeInstanceTable)
|
||||
.where(eq(nodeInstanceTable.workflowId, workflowId))
|
||||
.orderBy(asc(nodeInstanceTable.createdAt));
|
||||
return rows.map(
|
||||
(r): NodeInstance => ({
|
||||
nodeId: r.nodeId,
|
||||
instance: r.instance,
|
||||
running: r.running,
|
||||
waiting: r.waiting,
|
||||
activated: r.activated,
|
||||
...(r.terminal === null ? {} : { terminal: r.terminal as TerminalState }),
|
||||
...(r.failureDigested === null ? {} : { failureDigested: r.failureDigested }),
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
async insertInstance(workflowId, instance) {
|
||||
await db.insert(nodeInstanceTable).values({
|
||||
workflowId,
|
||||
nodeId: instance.nodeId,
|
||||
instance: instance.instance,
|
||||
running: instance.running,
|
||||
waiting: instance.waiting,
|
||||
activated: instance.activated,
|
||||
terminal: instance.terminal ?? null,
|
||||
failureDigested: instance.failureDigested ?? null,
|
||||
taskId: instance.taskId ?? null,
|
||||
approvalId: instance.approvalId ?? null,
|
||||
});
|
||||
},
|
||||
|
||||
async updateInstance(workflowId, nodeId, instance, patch) {
|
||||
const values: Record<string, unknown> = {};
|
||||
if (patch.running !== undefined) values['running'] = patch.running;
|
||||
if (patch.waiting !== undefined) values['waiting'] = patch.waiting;
|
||||
if (patch.terminal !== undefined) values['terminal'] = patch.terminal;
|
||||
if (patch.failureDigested !== undefined) values['failureDigested'] = patch.failureDigested;
|
||||
if (patch.taskId !== undefined) values['taskId'] = patch.taskId;
|
||||
if (Object.keys(values).length === 0) return;
|
||||
await db
|
||||
.update(nodeInstanceTable)
|
||||
.set(values)
|
||||
.where(
|
||||
and(
|
||||
eq(nodeInstanceTable.workflowId, workflowId),
|
||||
eq(nodeInstanceTable.nodeId, nodeId),
|
||||
eq(nodeInstanceTable.instance, instance),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
async findInstanceByTask(taskId) {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(nodeInstanceTable)
|
||||
.where(eq(nodeInstanceTable.taskId, taskId))
|
||||
.limit(1);
|
||||
const row = rows[0];
|
||||
return row === undefined ? undefined : { workflowId: row.workflowId, nodeId: row.nodeId, instance: row.instance };
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* 流程引擎的确定性流转 —— 纯逻辑,零 IO、**零 LLM**(编码规范 §5.3;语义出处 OS 04)。
|
||||
*
|
||||
* 「流程引擎 —— 确定性流转:接收调用接口的起单、按结构调度节点、把审批点卡到人的工位、
|
||||
* 执行网关的分合、把每次流转记入事件。**零 LLM、零临场**。」
|
||||
*/
|
||||
import type { Edge, ProcessNode, ProcessPackage } from '@contracts';
|
||||
|
||||
export type PredicateEvaluator = (expression: string, scope: Readonly<Record<string, unknown>>) => boolean;
|
||||
|
||||
/** 一次流转要做的事 */
|
||||
export type Dispatch =
|
||||
/** 起一单任务 */
|
||||
| { readonly kind: 'start-task'; readonly node: Extract<ProcessNode, { kind: 'activity' }>; readonly payload: Record<string, unknown> }
|
||||
/** 把审批点卡到人的工位 */
|
||||
| { readonly kind: 'raise-approval'; readonly node: Extract<ProcessNode, { kind: 'approval' }>; readonly payload: Record<string, unknown> }
|
||||
/** 流程包显式终点:按输出声明交付、按来源回执 */
|
||||
| { readonly kind: 'finish'; readonly node: Extract<ProcessNode, { kind: 'end' }>; readonly payload: Record<string, unknown> };
|
||||
|
||||
/** 边映射装配:上游输出口载荷 / 流程变量 / 起单载荷 / 常量 → 下游 data 字段 */
|
||||
export type AssembleScope = {
|
||||
readonly ports: Readonly<Record<string, Record<string, unknown>>>;
|
||||
readonly vars: Readonly<Record<string, unknown>>;
|
||||
readonly initiation: Readonly<Record<string, unknown>>;
|
||||
};
|
||||
|
||||
export function assemble(
|
||||
edge: Edge,
|
||||
scope: AssembleScope,
|
||||
evaluateExpression: (sfeel: string, scope: Readonly<Record<string, unknown>>) => unknown,
|
||||
): Record<string, unknown> {
|
||||
const out: Record<string, unknown> = {};
|
||||
const flat: Record<string, unknown> = { ...scope.initiation, ...scope.vars, ...scope.ports };
|
||||
for (const mapping of edge.mapping) {
|
||||
switch (mapping.source.from) {
|
||||
case 'output-port':
|
||||
out[mapping.target] = pick(scope.ports[mapping.source.port], mapping.source.path);
|
||||
break;
|
||||
case 'workflow-var':
|
||||
out[mapping.target] = scope.vars[mapping.source.name];
|
||||
break;
|
||||
case 'initiation':
|
||||
out[mapping.target] = pick(scope.initiation, mapping.source.path);
|
||||
break;
|
||||
case 'constant':
|
||||
out[mapping.target] = mapping.source.value;
|
||||
break;
|
||||
case 'expression':
|
||||
out[mapping.target] = evaluateExpression(mapping.source.sfeel, flat);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function pick(source: Readonly<Record<string, unknown>> | undefined, path: string): unknown {
|
||||
if (source === undefined) return undefined;
|
||||
return path.split('.').reduce<unknown>((acc, seg) => {
|
||||
if (acc === null || acc === undefined || typeof acc !== 'object') return undefined;
|
||||
return (acc as Record<string, unknown>)[seg];
|
||||
}, source);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从某节点出发,算出接下来该走的边。
|
||||
* XOR:条件求值,**恰取一条**;AND:全部出边同时走(分叉)。
|
||||
* 条件声明式、静态可检,**不得内嵌自由逻辑**。
|
||||
*/
|
||||
export function selectEdges(
|
||||
pkg: ProcessPackage,
|
||||
fromNodeId: string,
|
||||
scope: Readonly<Record<string, unknown>>,
|
||||
evaluate: PredicateEvaluator,
|
||||
): Edge[] {
|
||||
const node = pkg.nodes.find((n) => n.id === fromNodeId);
|
||||
const outgoing = pkg.edges.filter((e) => e.from === fromNodeId);
|
||||
|
||||
if (node?.kind === 'gateway' && node.gateway === 'exclusive' && node.mode === 'fork') {
|
||||
// 判定依据入事件记录;无条件的边作默认分支兜底
|
||||
const hit = outgoing.find((e) => e.condition !== undefined && evaluate(e.condition, scope));
|
||||
const fallback = outgoing.find((e) => e.condition === undefined);
|
||||
const chosen = hit ?? fallback;
|
||||
return chosen === undefined ? [] : [chosen];
|
||||
}
|
||||
|
||||
// 并行网关分叉、活动节点、审批点:全部出边
|
||||
return outgoing;
|
||||
}
|
||||
|
||||
/** AND 汇合是否回齐:全部入边的来源分支都已收敛 */
|
||||
export function joinReady(
|
||||
pkg: ProcessPackage,
|
||||
joinNodeId: string,
|
||||
converged: ReadonlySet<string>,
|
||||
tolerated: ReadonlySet<string>,
|
||||
): boolean {
|
||||
const inbound = pkg.edges.filter((e) => e.to === joinNodeId);
|
||||
// 容忍分支以「已消化」计,不阻汇合
|
||||
return inbound.every((e) => converged.has(e.from) || tolerated.has(e.from));
|
||||
}
|
||||
|
||||
/** 节点该被怎么调度 */
|
||||
export function dispatchOf(node: ProcessNode, payload: Record<string, unknown>): Dispatch | undefined {
|
||||
if (node.kind === 'activity') return { kind: 'start-task', node, payload };
|
||||
if (node.kind === 'approval') return { kind: 'raise-approval', node, payload };
|
||||
if (node.kind === 'end') return { kind: 'finish', node, payload };
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** 审批点的驳回走向:声明式,**不由临场决定** */
|
||||
export type RejectRoute =
|
||||
| { readonly kind: 'reopen'; readonly nodeId: string; readonly payload: Record<string, unknown> }
|
||||
| { readonly kind: 'terminate' };
|
||||
|
||||
export function rejectRouteOf(
|
||||
node: Extract<ProcessNode, { kind: 'approval' }>,
|
||||
scope: AssembleScope,
|
||||
evaluateExpression: (sfeel: string, scope: Readonly<Record<string, unknown>>) => unknown,
|
||||
): RejectRoute {
|
||||
if (node.reject.kind === 'terminate') return { kind: 'terminate' };
|
||||
return {
|
||||
kind: 'reopen',
|
||||
nodeId: node.reject.nodeId,
|
||||
payload: assemble(
|
||||
{ id: `${node.id}-reject`, from: node.id, to: node.reject.nodeId, mapping: node.reject.mapping, reopen: true },
|
||||
scope,
|
||||
evaluateExpression,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/** 起点:找出流程包的入口节点 */
|
||||
export function startNodeOf(pkg: ProcessPackage): ProcessNode | undefined {
|
||||
return pkg.nodes.find((n) => n.kind === 'start');
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
/**
|
||||
* process 用例 —— 流程引擎:确定性流转,**零 LLM、零临场**(编码规范 §5.3;语义出处 OS 03 / 04)。
|
||||
*
|
||||
* 起单 · 调度节点 · 审批点卡工位 · 网关分合 · 每次流转记入事件。
|
||||
*/
|
||||
import type { ProcessNode, ProcessPackage, StaffId, TerminalState, WorkflowState } from '@contracts';
|
||||
import type { ProcessService, StartWorkflowInput } from '../../contract.ts';
|
||||
import { NotPublishableError, PositionVacantError } from '../../contract.ts';
|
||||
import type {
|
||||
ActivityDirectory,
|
||||
ApprovalGateway,
|
||||
Clock,
|
||||
FlowEventPublisher,
|
||||
IdGenerator,
|
||||
OrgDirectory,
|
||||
ProcessStore,
|
||||
ReceiptNotifier,
|
||||
Sfeel,
|
||||
TaskGateway,
|
||||
WorkflowRecord,
|
||||
} from '../../port.ts';
|
||||
import { checkClosure } from '../domain/closure.ts';
|
||||
import { aggregateWorkflowState, isDigested, nextInstanceNumber, resolveOwner } from '../domain/owner-resolution.ts';
|
||||
import type { NodeInstance } from '../domain/owner-resolution.ts';
|
||||
import { assemble, dispatchOf, joinReady, rejectRouteOf, selectEdges, startNodeOf } from '../domain/routing.ts';
|
||||
|
||||
export type ProcessServiceDeps = {
|
||||
readonly store: ProcessStore;
|
||||
readonly activities: ActivityDirectory;
|
||||
readonly org: OrgDirectory;
|
||||
readonly tasks: TaskGateway;
|
||||
readonly approvals: ApprovalGateway;
|
||||
readonly sfeel: Sfeel;
|
||||
readonly events: FlowEventPublisher;
|
||||
readonly receipts: ReceiptNotifier;
|
||||
readonly clock: Clock;
|
||||
readonly ids: IdGenerator;
|
||||
};
|
||||
|
||||
export function createProcessService(deps: ProcessServiceDeps): ProcessService {
|
||||
async function requirePackage(id: string, version?: number): Promise<ProcessPackage> {
|
||||
const pkg = await deps.store.getPackage(id, version);
|
||||
if (pkg === undefined) throw new Error(`流程包不存在:${id}`);
|
||||
return pkg;
|
||||
}
|
||||
|
||||
async function requireWorkflow(id: string): Promise<WorkflowRecord> {
|
||||
const record = await deps.store.getWorkflow(id);
|
||||
if (record === undefined) throw new Error(`工作流不存在:${id}`);
|
||||
return record;
|
||||
}
|
||||
|
||||
function scopeOf(record: WorkflowRecord) {
|
||||
return { ports: record.ports, vars: record.vars, initiation: record.initiation };
|
||||
}
|
||||
|
||||
/** 起一个节点:活动 → 起单;审批点 → 卡工位;终点 → 收束 */
|
||||
async function enter(
|
||||
record: WorkflowRecord,
|
||||
pkg: ProcessPackage,
|
||||
node: ProcessNode,
|
||||
payload: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
const dispatch = dispatchOf(node, payload);
|
||||
|
||||
if (dispatch === undefined) {
|
||||
// 网关本身不是责任单元:直接继续流转
|
||||
await advanceFrom(record, pkg, node.id, payload);
|
||||
return;
|
||||
}
|
||||
|
||||
const instances = await deps.store.listInstances(record.id);
|
||||
const instance = nextInstanceNumber(instances, node.id);
|
||||
|
||||
if (dispatch.kind === 'finish') {
|
||||
await deps.store.insertInstance(record.id, {
|
||||
nodeId: node.id,
|
||||
instance,
|
||||
running: false,
|
||||
waiting: false,
|
||||
activated: true,
|
||||
terminal: 'released',
|
||||
});
|
||||
const finished = await deps.store.updateWorkflow(record.id, {
|
||||
state: 'completed',
|
||||
finishedAt: deps.clock.now().toISOString(),
|
||||
ports: { ...record.ports, __output: payload },
|
||||
});
|
||||
await deps.events.publish({ workflowId: record.id, kind: 'workflow-completed', detail: payload });
|
||||
// 凡单必有来源 —— 登记人收消息回执
|
||||
if (record.source.by !== undefined) {
|
||||
await deps.receipts.notify({
|
||||
to: record.source.by as StaffId,
|
||||
workflowId: record.id,
|
||||
subject: record.subject,
|
||||
state: finished.state,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 负责人 / 审批人:编排声明岗位,起单时确定性解析;结果与依据留痕
|
||||
const spec = dispatch.kind === 'start-task' ? dispatch.node.owner : dispatch.node.approver;
|
||||
const candidates = await deps.org.candidates(spec.position);
|
||||
const resolution = resolveOwner(spec, { candidates });
|
||||
if (resolution.kind === 'vacant') throw new PositionVacantError(spec.position);
|
||||
|
||||
await deps.events.publish({
|
||||
workflowId: record.id,
|
||||
kind: 'owner-resolved',
|
||||
detail: { nodeId: node.id, position: spec.position, staffId: resolution.staffId, by: resolution.by, rationale: resolution.rationale },
|
||||
});
|
||||
|
||||
if (dispatch.kind === 'start-task') {
|
||||
const task = await deps.tasks.start({
|
||||
activityId: dispatch.node.activityId,
|
||||
ownerId: resolution.staffId as StaffId,
|
||||
title: dispatch.node.label,
|
||||
workflowId: record.id,
|
||||
nodeId: node.id,
|
||||
data: payload,
|
||||
...(dispatch.node.message === undefined ? {} : { message: dispatch.node.message }),
|
||||
});
|
||||
await deps.store.insertInstance(record.id, {
|
||||
nodeId: node.id,
|
||||
instance,
|
||||
running: true,
|
||||
waiting: false,
|
||||
activated: true,
|
||||
taskId: task.id,
|
||||
});
|
||||
await deps.events.publish({ workflowId: record.id, kind: 'task-started', detail: { nodeId: node.id, taskId: task.id } });
|
||||
return;
|
||||
}
|
||||
|
||||
const approval = await deps.approvals.raise({
|
||||
workflowId: record.id,
|
||||
nodeId: node.id,
|
||||
approverId: resolution.staffId as StaffId,
|
||||
subject: dispatch.node.label,
|
||||
basis: Object.entries(payload).map(([k, v]) => ({ k, v: String(v) })),
|
||||
});
|
||||
await deps.store.insertInstance(record.id, {
|
||||
nodeId: node.id,
|
||||
instance,
|
||||
running: false,
|
||||
waiting: true,
|
||||
activated: true,
|
||||
approvalId: approval.id,
|
||||
});
|
||||
await deps.events.publish({ workflowId: record.id, kind: 'approval-raised', detail: { nodeId: node.id, approvalId: approval.id } });
|
||||
}
|
||||
|
||||
/** 从某节点继续流转:选边 → 装配 → 进下一节点(AND 汇合要回齐) */
|
||||
async function advanceFrom(
|
||||
record: WorkflowRecord,
|
||||
pkg: ProcessPackage,
|
||||
fromNodeId: string,
|
||||
payload: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
const scope = scopeOf(record);
|
||||
const flat = { ...scope.initiation, ...scope.vars, ...payload };
|
||||
const edges = selectEdges(pkg, fromNodeId, flat, deps.sfeel.condition);
|
||||
|
||||
for (const edge of edges) {
|
||||
const target = pkg.nodes.find((n) => n.id === edge.to);
|
||||
if (target === undefined) continue;
|
||||
|
||||
if (target.kind === 'gateway' && target.mode === 'join') {
|
||||
const instances = await deps.store.listInstances(record.id);
|
||||
const converged = new Set(instances.filter((i) => i.terminal === 'released').map((i) => i.nodeId));
|
||||
const tolerated = new Set(instances.filter((i) => i.failureDigested === true).map((i) => i.nodeId));
|
||||
// 未回齐就等着 —— 别的分支到了会再来一次
|
||||
if (!joinReady(pkg, target.id, converged, tolerated)) continue;
|
||||
}
|
||||
|
||||
const assembled = assemble(edge, { ...scope, ports: { ...scope.ports, [fromNodeId]: payload } }, deps.sfeel.expression);
|
||||
await enter(record, pkg, target, assembled);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
async savePackage(pkg) {
|
||||
await deps.store.savePackage(pkg);
|
||||
return pkg;
|
||||
},
|
||||
|
||||
async listPackages() {
|
||||
return deps.store.listPackages();
|
||||
},
|
||||
|
||||
async getPackage(id, version) {
|
||||
return deps.store.getPackage(id, version);
|
||||
},
|
||||
|
||||
/** 画图即校验:发布前跑闭合校验 */
|
||||
async validate(pkg) {
|
||||
return checkClosure({
|
||||
pkg,
|
||||
contracts: await deps.activities.faces(),
|
||||
positions: await deps.org.positions(),
|
||||
});
|
||||
},
|
||||
|
||||
/** 不闭合不得发布 */
|
||||
async publish(id, version) {
|
||||
const pkg = await requirePackage(id, version);
|
||||
const report = await this.validate(pkg);
|
||||
if (!report.publishable) throw new NotPublishableError(report);
|
||||
await deps.store.publishPackage(id, version);
|
||||
return report;
|
||||
},
|
||||
|
||||
/** 起单:经调用接口调起流程包,生成一条工作流 */
|
||||
async start(input: StartWorkflowInput) {
|
||||
const pkg = await requirePackage(input.packageId, input.packageVersion);
|
||||
if (pkg.state !== 'published') throw new Error(`流程包未发布,不得起单:${pkg.id}@${pkg.version}`);
|
||||
|
||||
const record: WorkflowRecord = {
|
||||
id: deps.ids.next(),
|
||||
packageId: pkg.id,
|
||||
packageVersion: pkg.version,
|
||||
subject: input.subject,
|
||||
state: 'running',
|
||||
source: input.source,
|
||||
initiation: input.data,
|
||||
ports: {},
|
||||
vars: {},
|
||||
startedAt: deps.clock.now().toISOString(),
|
||||
};
|
||||
await deps.store.insertWorkflow(record);
|
||||
await deps.events.publish({ workflowId: record.id, kind: 'workflow-started', detail: { source: input.source } });
|
||||
|
||||
const start = startNodeOf(pkg);
|
||||
if (start !== undefined) await advanceFrom(record, pkg, start.id, input.data);
|
||||
return record;
|
||||
},
|
||||
|
||||
async getWorkflow(id) {
|
||||
return requireWorkflow(id);
|
||||
},
|
||||
|
||||
async listWorkflows(limit = 50) {
|
||||
return deps.store.listWorkflows(limit);
|
||||
},
|
||||
|
||||
async instancesOf(workflowId) {
|
||||
return deps.store.listInstances(workflowId);
|
||||
},
|
||||
|
||||
/** 任务终态到达:按终态决定继续流转还是走失败走向 */
|
||||
async onTaskTerminal(taskId, terminal: TerminalState, ports: Record<string, Record<string, unknown>>) {
|
||||
const located = await deps.store.findInstanceByTask(taskId);
|
||||
if (located === undefined) return undefined;
|
||||
|
||||
const record = await requireWorkflow(located.workflowId);
|
||||
const pkg = await requirePackage(record.packageId, record.packageVersion);
|
||||
const node = pkg.nodes.find((n) => n.id === located.nodeId);
|
||||
|
||||
await deps.store.updateInstance(record.id, located.nodeId, located.instance, {
|
||||
running: false,
|
||||
waiting: false,
|
||||
terminal,
|
||||
});
|
||||
await deps.events.publish({ workflowId: record.id, kind: 'task-terminal', detail: { taskId, terminal } });
|
||||
|
||||
const merged = await deps.store.updateWorkflow(record.id, { ports: { ...record.ports, ...ports } });
|
||||
|
||||
if (terminal === 'released') {
|
||||
// 落地收敛后才发射输出口 —— 下游只见落地完成的任务
|
||||
const payload = Object.values(ports)[0] ?? {};
|
||||
await advanceFrom(merged, pkg, located.nodeId, payload);
|
||||
} else if (node?.kind === 'activity') {
|
||||
// 非完成终态触发**声明的失败走向**;流程层不自动重试
|
||||
await this.applyFailure(record.id, located.nodeId, located.instance, node, terminal);
|
||||
}
|
||||
|
||||
return this.refreshState(record.id);
|
||||
},
|
||||
|
||||
/** 失败走向四选一:终止流(默认)/ 改道 / 升审批 / 容忍 */
|
||||
async applyFailure(workflowId, nodeId, instance, node, terminal) {
|
||||
const record = await requireWorkflow(workflowId);
|
||||
const pkg = await requirePackage(record.packageId, record.packageVersion);
|
||||
const disposition = node.failure;
|
||||
const digested = isDigested(disposition.kind);
|
||||
|
||||
await deps.store.updateInstance(workflowId, nodeId, instance, { failureDigested: digested });
|
||||
await deps.events.publish({
|
||||
workflowId,
|
||||
kind: 'failure-disposition',
|
||||
detail: { nodeId, terminal, disposition: disposition.kind },
|
||||
});
|
||||
|
||||
if (disposition.kind === 'reroute') {
|
||||
const edge = pkg.edges.find((e) => e.id === disposition.edgeId);
|
||||
const target = edge === undefined ? undefined : pkg.nodes.find((n) => n.id === edge.to);
|
||||
if (edge !== undefined && target !== undefined) {
|
||||
// 失败载荷 = 终态、原因码与说明
|
||||
const payload = assemble(edge, scopeOf(record), deps.sfeel.expression);
|
||||
await enter(record, pkg, target, { ...payload, __terminal: terminal });
|
||||
}
|
||||
}
|
||||
|
||||
if (disposition.kind === 'escalate') {
|
||||
const target = pkg.nodes.find((n) => n.id === disposition.approvalNodeId);
|
||||
if (target !== undefined) await enter(record, pkg, target, { __terminal: terminal, __from: nodeId });
|
||||
}
|
||||
|
||||
return this.refreshState(workflowId);
|
||||
},
|
||||
|
||||
/** 审批裁决:通过继续流转;驳回按**声明的走向**回指定节点重开新实例 */
|
||||
async onApprovalVerdict(workflowId, nodeId, verdict, payload) {
|
||||
const record = await requireWorkflow(workflowId);
|
||||
const pkg = await requirePackage(record.packageId, record.packageVersion);
|
||||
const node = pkg.nodes.find((n) => n.id === nodeId);
|
||||
if (node?.kind !== 'approval') throw new Error(`节点不是审批点:${nodeId}`);
|
||||
|
||||
const instances = await deps.store.listInstances(workflowId);
|
||||
const current = instances.filter((i) => i.nodeId === nodeId).at(-1);
|
||||
if (current !== undefined) {
|
||||
await deps.store.updateInstance(workflowId, nodeId, current.instance, {
|
||||
waiting: false,
|
||||
terminal: 'released',
|
||||
});
|
||||
}
|
||||
await deps.events.publish({ workflowId, kind: 'approval-verdict', detail: { nodeId, verdict } });
|
||||
|
||||
if (verdict === 'pass') {
|
||||
await advanceFrom(record, pkg, nodeId, payload);
|
||||
} else {
|
||||
const route = rejectRouteOf(node, scopeOf(record), deps.sfeel.expression);
|
||||
if (route.kind === 'terminate') {
|
||||
await deps.store.updateWorkflow(workflowId, { state: 'terminated', finishedAt: deps.clock.now().toISOString() });
|
||||
} else {
|
||||
// 回指定节点**重开新实例**(同节点、新任务 ID、留痕关联),不复活旧任务
|
||||
const target = pkg.nodes.find((n) => n.id === route.nodeId);
|
||||
if (target !== undefined) await enter(record, pkg, target, { ...route.payload, ...payload });
|
||||
}
|
||||
}
|
||||
return this.refreshState(workflowId);
|
||||
},
|
||||
|
||||
/** 工作流状态是聚合投影 —— 每次变动后重算 */
|
||||
async refreshState(workflowId): Promise<WorkflowState> {
|
||||
const record = await requireWorkflow(workflowId);
|
||||
if (record.state === 'terminated') return 'terminated';
|
||||
const instances = await deps.store.listInstances(workflowId);
|
||||
const state = aggregateWorkflowState(instances as NodeInstance[], false);
|
||||
if (state !== record.state) {
|
||||
await deps.store.updateWorkflow(workflowId, {
|
||||
state,
|
||||
...(state === 'completed' ? { finishedAt: deps.clock.now().toISOString() } : {}),
|
||||
});
|
||||
}
|
||||
return state;
|
||||
},
|
||||
|
||||
async terminate(workflowId, reason) {
|
||||
await deps.store.updateWorkflow(workflowId, {
|
||||
state: 'terminated',
|
||||
finishedAt: deps.clock.now().toISOString(),
|
||||
});
|
||||
// 工作流终止级联:在途任务终止、待答卡与征答消息作废、审批点撤卡
|
||||
await deps.events.publish({ workflowId, kind: 'workflow-terminated', detail: { reason } });
|
||||
return 'terminated';
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* 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>;
|
||||
/** 各节点输出口载荷 —— 边映射装配的来源 */
|
||||
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>;
|
||||
getPackage(id: string, version?: number): Promise<ProcessPackage | undefined>;
|
||||
listPackages(): Promise<ProcessPackage[]>;
|
||||
publishPackage(id: string, version: number): Promise<void>;
|
||||
|
||||
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 }>>;
|
||||
};
|
||||
|
||||
/** 起单与终态 —— 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 }>;
|
||||
};
|
||||
|
||||
/** 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 };
|
||||
Reference in New Issue
Block a user