三面拆分⑯:流程包详情改三栏 · 字段只声明 · 版本可删可改名
**字段只声明,不做任何要求**(用户定):编排层不再碰字段 ——
· 激活闸只剩两面:结构闭合(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:
@@ -54,6 +54,10 @@ export type ProcessService = {
|
|||||||
getWorkflow(id: string): Promise<WorkflowRecord>;
|
getWorkflow(id: string): Promise<WorkflowRecord>;
|
||||||
listWorkflows(limit?: number): Promise<WorkflowRecord[]>;
|
listWorkflows(limit?: number): Promise<WorkflowRecord[]>;
|
||||||
instancesOf(workflowId: string): Promise<NodeInstance[]>;
|
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>>;
|
nodeOwners(packageId: string): Promise<Record<string, string>>;
|
||||||
setNodeOwner(packageId: string, nodeId: string, staffId: string | undefined): Promise<void>;
|
setNodeOwner(packageId: string, nodeId: string, staffId: string | undefined): Promise<void>;
|
||||||
/** 任务终态到达:完成即继续流转;非完成即走声明的失败走向 */
|
/** 任务终态到达:完成即继续流转;非完成即走声明的失败走向 */
|
||||||
@@ -68,6 +72,12 @@ export type ProcessService = {
|
|||||||
terminate(workflowId: string, reason: string): Promise<WorkflowState>;
|
terminate(workflowId: string, reason: string): Promise<WorkflowState>;
|
||||||
};
|
};
|
||||||
/** 起单入参不合调用接口契约 —— 在建工作流之前判掉,不留半条在途单 */
|
/** 起单入参不合调用接口契约 —— 在建工作流之前判掉,不留半条在途单 */
|
||||||
|
/** 生效版不可删 —— 先停用:这道闸挡的是「手滑把正在起单的定义删了」 */
|
||||||
|
export declare class ActiveVersionError extends Error {
|
||||||
|
readonly id: string;
|
||||||
|
readonly version: number;
|
||||||
|
constructor(id: string, version: number);
|
||||||
|
}
|
||||||
export declare class CallInterfaceViolationError extends Error {
|
export declare class CallInterfaceViolationError extends Error {
|
||||||
readonly packageId: string;
|
readonly packageId: string;
|
||||||
readonly errors: readonly string[];
|
readonly errors: readonly string[];
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
* module: process —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。
|
* module: process —— 唯一出口。只做 re-export,不写逻辑(编码规范 §10.1)。
|
||||||
*/
|
*/
|
||||||
export type { ActivityContractFace, AssembleScope, Candidate, ClosureInput, ClosureReport, ClosureViolation, NodeInstance, OwnerSpec, ProcessPackage, ProcessService, ResolveContext, Resolution, StartWorkflowInput, WorkflowRecord, WorkflowState, } from './contract.ts';
|
export type { ActivityContractFace, AssembleScope, Candidate, ClosureInput, ClosureReport, ClosureViolation, NodeInstance, OwnerSpec, ProcessPackage, ProcessService, ResolveContext, Resolution, StartWorkflowInput, WorkflowRecord, WorkflowState, } from './contract.ts';
|
||||||
export { CallInterfaceViolationError, NotClosedError, PinnedVersionError, PositionVacantError, StaleVersionError } from './contract.ts';
|
export { ActiveVersionError, CallInterfaceViolationError, NotClosedError, PinnedVersionError, PositionVacantError, StaleVersionError } from './contract.ts';
|
||||||
export type { FlowRuntime, ActivityDirectory, ApprovalGateway, Clock, FlowEventPublisher, IdGenerator, OrgDirectory, ProcessStore, ReceiptNotifier, Sfeel, TaskGateway, } from './port.ts';
|
export type { FlowRuntime, ActivityDirectory, ApprovalGateway, Clock, FlowEventPublisher, IdGenerator, OrgDirectory, ProcessStore, ReceiptNotifier, Sfeel, TaskGateway, } from './port.ts';
|
||||||
export type { ProcessServiceDeps } from './internal/usecase/process-service.ts';
|
export type { ProcessServiceDeps } from './internal/usecase/process-service.ts';
|
||||||
export { createProcessService } from './internal/usecase/process-service.ts';
|
export { createProcessService } from './internal/usecase/process-service.ts';
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ export type ProcessStore = {
|
|||||||
countPinnedInFlight(id: string, version: number): Promise<number>;
|
countPinnedInFlight(id: string, version: number): Promise<number>;
|
||||||
/** 只改状态,不动定义 —— state 在列上与 definition 里各存一份,两处要一起改 */
|
/** 只改状态,不动定义 —— state 在列上与 definition 里各存一份,两处要一起改 */
|
||||||
setPackageState(id: string, version: number, state: 'active' | 'retired'): Promise<void>;
|
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>;
|
getPackage(id: string, version?: number): Promise<ProcessPackage | undefined>;
|
||||||
listPackages(): Promise<ProcessPackage[]>;
|
listPackages(): Promise<ProcessPackage[]>;
|
||||||
insertWorkflow(record: WorkflowRecord): Promise<void>;
|
insertWorkflow(record: WorkflowRecord): Promise<void>;
|
||||||
|
|||||||
+255
-5
@@ -813,10 +813,8 @@ export declare const saveProcessRoute: {
|
|||||||
closed: z.ZodBoolean;
|
closed: z.ZodBoolean;
|
||||||
violations: z.ZodArray<z.ZodObject<{
|
violations: z.ZodArray<z.ZodObject<{
|
||||||
kind: z.ZodEnum<{
|
kind: z.ZodEnum<{
|
||||||
"type-conflict": "type-conflict";
|
|
||||||
"dangling-node": "dangling-node";
|
"dangling-node": "dangling-node";
|
||||||
"multi-default-edge": "multi-default-edge";
|
"multi-default-edge": "multi-default-edge";
|
||||||
"tolerate-required-dependency": "tolerate-required-dependency";
|
|
||||||
"unknown-activity": "unknown-activity";
|
"unknown-activity": "unknown-activity";
|
||||||
}>;
|
}>;
|
||||||
nodeId: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "NodeId", "out">>;
|
nodeId: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "NodeId", "out">>;
|
||||||
@@ -959,6 +957,257 @@ export declare const setProcessStateRoute: {
|
|||||||
}, z.core.$strip>>;
|
}, z.core.$strip>>;
|
||||||
}, z.core.$strip>;
|
}, z.core.$strip>;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* 删一版 —— 两道闸(2026-07-30):
|
||||||
|
* · **生效版不可删**:先停用,免得手滑把正在起单的定义删了;
|
||||||
|
* · **在途钉版不可删**:那些单要按它跑完,定义没了就再也推不动。
|
||||||
|
*/
|
||||||
|
export declare const deleteProcessVersionRoute: {
|
||||||
|
readonly method: "DELETE";
|
||||||
|
readonly path: "/api/gov/process/:id/:version";
|
||||||
|
readonly params: z.ZodObject<{
|
||||||
|
id: z.ZodString;
|
||||||
|
version: z.ZodString;
|
||||||
|
}, 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>;
|
||||||
|
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">;
|
||||||
|
reject: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
||||||
|
kind: z.ZodLiteral<"reopen">;
|
||||||
|
nodeId: z.core.$ZodBranded<z.ZodString, "NodeId", "out">;
|
||||||
|
}, 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>], "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>;
|
||||||
|
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>;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 改名 —— 名字是**这条流的身份**,不是某一版的属性:一次改及于它的所有版本。
|
||||||
|
* (版本列表上只显示 v1/v2…,若只改一版的名,列表上什么也看不见,人会以为没生效。)
|
||||||
|
*/
|
||||||
|
export declare const renameProcessRoute: {
|
||||||
|
readonly method: "POST";
|
||||||
|
readonly path: "/api/gov/process/:id/rename";
|
||||||
|
readonly params: z.ZodObject<{
|
||||||
|
id: z.ZodString;
|
||||||
|
}, z.core.$strip>;
|
||||||
|
readonly body: z.ZodObject<{
|
||||||
|
name: z.ZodString;
|
||||||
|
}, 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>;
|
||||||
|
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">;
|
||||||
|
reject: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
||||||
|
kind: z.ZodLiteral<"reopen">;
|
||||||
|
nodeId: z.core.$ZodBranded<z.ZodString, "NodeId", "out">;
|
||||||
|
}, 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>], "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>;
|
||||||
|
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: {
|
export declare const validateProcessRoute: {
|
||||||
readonly method: "POST";
|
readonly method: "POST";
|
||||||
@@ -1079,10 +1328,8 @@ export declare const validateProcessRoute: {
|
|||||||
closed: z.ZodBoolean;
|
closed: z.ZodBoolean;
|
||||||
violations: z.ZodArray<z.ZodObject<{
|
violations: z.ZodArray<z.ZodObject<{
|
||||||
kind: z.ZodEnum<{
|
kind: z.ZodEnum<{
|
||||||
"type-conflict": "type-conflict";
|
|
||||||
"dangling-node": "dangling-node";
|
"dangling-node": "dangling-node";
|
||||||
"multi-default-edge": "multi-default-edge";
|
"multi-default-edge": "multi-default-edge";
|
||||||
"tolerate-required-dependency": "tolerate-required-dependency";
|
|
||||||
"unknown-activity": "unknown-activity";
|
"unknown-activity": "unknown-activity";
|
||||||
}>;
|
}>;
|
||||||
nodeId: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "NodeId", "out">>;
|
nodeId: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "NodeId", "out">>;
|
||||||
@@ -1798,7 +2045,10 @@ export declare const listActivitiesRoute: {
|
|||||||
outputPorts: z.ZodArray<z.ZodObject<{
|
outputPorts: z.ZodArray<z.ZodObject<{
|
||||||
name: z.ZodString;
|
name: z.ZodString;
|
||||||
description: z.ZodOptional<z.ZodString>;
|
description: z.ZodOptional<z.ZodString>;
|
||||||
fields: z.ZodArray<z.ZodString>;
|
fields: z.ZodArray<z.ZodObject<{
|
||||||
|
name: z.ZodString;
|
||||||
|
type: z.ZodString;
|
||||||
|
}, z.core.$strip>>;
|
||||||
}, z.core.$strip>>;
|
}, z.core.$strip>>;
|
||||||
tools: z.ZodArray<z.ZodString>;
|
tools: z.ZodArray<z.ZodString>;
|
||||||
models: z.ZodArray<z.ZodString>;
|
models: z.ZodArray<z.ZodString>;
|
||||||
|
|||||||
+6
-7
@@ -322,21 +322,22 @@ export declare const processPackageSchema: z.ZodObject<{
|
|||||||
diagram: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
diagram: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
||||||
}, z.core.$strip>;
|
}, z.core.$strip>;
|
||||||
export type ProcessPackage = z.infer<typeof processPackageSchema>;
|
export type ProcessPackage = z.infer<typeof processPackageSchema>;
|
||||||
export declare const CLOSURE_VIOLATION_KINDS: readonly ["type-conflict", "dangling-node", "multi-default-edge", "tolerate-required-dependency", "unknown-activity"];
|
/**
|
||||||
|
* 激活闸只有两面(2026-07-30 用户定「字段只声明,不做任何要求」):
|
||||||
|
* **结构闭合**(dangling-node / multi-default-edge)与**活动装载**(unknown-activity)。
|
||||||
|
* 字段类的检查全部退役 —— 要什么、够不够、类型对不对归活动自己,不在编排层现形。
|
||||||
|
*/
|
||||||
|
export declare const CLOSURE_VIOLATION_KINDS: readonly ["dangling-node", "multi-default-edge", "unknown-activity"];
|
||||||
export declare const closureViolationKindSchema: z.ZodEnum<{
|
export declare const closureViolationKindSchema: z.ZodEnum<{
|
||||||
"type-conflict": "type-conflict";
|
|
||||||
"dangling-node": "dangling-node";
|
"dangling-node": "dangling-node";
|
||||||
"multi-default-edge": "multi-default-edge";
|
"multi-default-edge": "multi-default-edge";
|
||||||
"tolerate-required-dependency": "tolerate-required-dependency";
|
|
||||||
"unknown-activity": "unknown-activity";
|
"unknown-activity": "unknown-activity";
|
||||||
}>;
|
}>;
|
||||||
export type ClosureViolationKind = z.infer<typeof closureViolationKindSchema>;
|
export type ClosureViolationKind = z.infer<typeof closureViolationKindSchema>;
|
||||||
export declare const closureViolationSchema: z.ZodObject<{
|
export declare const closureViolationSchema: z.ZodObject<{
|
||||||
kind: z.ZodEnum<{
|
kind: z.ZodEnum<{
|
||||||
"type-conflict": "type-conflict";
|
|
||||||
"dangling-node": "dangling-node";
|
"dangling-node": "dangling-node";
|
||||||
"multi-default-edge": "multi-default-edge";
|
"multi-default-edge": "multi-default-edge";
|
||||||
"tolerate-required-dependency": "tolerate-required-dependency";
|
|
||||||
"unknown-activity": "unknown-activity";
|
"unknown-activity": "unknown-activity";
|
||||||
}>;
|
}>;
|
||||||
nodeId: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "NodeId", "out">>;
|
nodeId: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "NodeId", "out">>;
|
||||||
@@ -349,10 +350,8 @@ export declare const closureReportSchema: z.ZodObject<{
|
|||||||
closed: z.ZodBoolean;
|
closed: z.ZodBoolean;
|
||||||
violations: z.ZodArray<z.ZodObject<{
|
violations: z.ZodArray<z.ZodObject<{
|
||||||
kind: z.ZodEnum<{
|
kind: z.ZodEnum<{
|
||||||
"type-conflict": "type-conflict";
|
|
||||||
"dangling-node": "dangling-node";
|
"dangling-node": "dangling-node";
|
||||||
"multi-default-edge": "multi-default-edge";
|
"multi-default-edge": "multi-default-edge";
|
||||||
"tolerate-required-dependency": "tolerate-required-dependency";
|
|
||||||
"unknown-activity": "unknown-activity";
|
"unknown-activity": "unknown-activity";
|
||||||
}>;
|
}>;
|
||||||
nodeId: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "NodeId", "out">>;
|
nodeId: z.ZodOptional<z.core.$ZodBranded<z.ZodString, "NodeId", "out">>;
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ import {
|
|||||||
resumeTaskRoute,
|
resumeTaskRoute,
|
||||||
staffIdSchema,
|
staffIdSchema,
|
||||||
taskIdSchema,
|
taskIdSchema,
|
||||||
|
deleteProcessVersionRoute,
|
||||||
|
renameProcessRoute,
|
||||||
setNodeOwnerRoute,
|
setNodeOwnerRoute,
|
||||||
setTriggerConfirmRoute,
|
setTriggerConfirmRoute,
|
||||||
listNodeOwnersRoute,
|
listNodeOwnersRoute,
|
||||||
@@ -47,7 +49,7 @@ import { extractFields } from '@modules/channel';
|
|||||||
import { assembleInput, scopeOf } from '@modules/trigger';
|
import { assembleInput, scopeOf } from '@modules/trigger';
|
||||||
import { InstallRejectedError } from '@modules/activity-registry';
|
import { InstallRejectedError } from '@modules/activity-registry';
|
||||||
import type { InstallVerdict } 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 { Container } from '../container.ts';
|
||||||
import type { AppEnv } from '../http-env.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) => {
|
app.post(setProcessStateRoute.path, async (c) => {
|
||||||
const denied = await governProcess(c.get('account'));
|
const denied = await governProcess(c.get('account'));
|
||||||
if (denied) return c.json(denied, denyStatus(denied));
|
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(
|
const faces = await Promise.all(
|
||||||
cards.map(async (card) => {
|
cards.map(async (card) => {
|
||||||
const manifest = await container.registry.manifestOf(card.activityId);
|
const manifest = await container.registry.manifestOf(card.activityId);
|
||||||
|
type FieldSpec = { type?: string; items?: { type?: string } };
|
||||||
const data = manifest.contract.input.data as {
|
const data = manifest.contract.input.data as {
|
||||||
required?: string[];
|
required?: string[];
|
||||||
properties?: Record<string, { type?: string }>;
|
properties?: Record<string, FieldSpec>;
|
||||||
};
|
};
|
||||||
const required = data.required ?? [];
|
const required = data.required ?? [];
|
||||||
const props = data.properties ?? {};
|
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 portFields = (payload: unknown) => {
|
||||||
const p = payload as { properties?: Record<string, unknown> } | undefined;
|
const p = payload as { properties?: Record<string, FieldSpec> } | undefined;
|
||||||
return Object.keys(p?.properties ?? {});
|
return Object.entries(p?.properties ?? {}).map(([name, spec]) => ({ name, type: typeOf(spec) }));
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
requiredInputs: required,
|
requiredInputs: required,
|
||||||
inputFields: Object.entries(props).map(([name, spec]) => ({
|
inputFields: Object.entries(props).map(([name, spec]) => ({
|
||||||
name,
|
name,
|
||||||
type: spec.type ?? 'unknown',
|
type: typeOf(spec),
|
||||||
required: required.includes(name),
|
required: required.includes(name),
|
||||||
})),
|
})),
|
||||||
// 必发口与选择组一并列出:调用方只凭口名与 schema 就能衔接下游
|
// 必发口与选择组一并列出:调用方只凭口名与 schema 就能衔接下游
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# module: process
|
# module: process
|
||||||
|
|
||||||
职责: 流程层:流程包定义与版本、**闭合校验**(边只表达顺序与条件 · 画图不检查 · 不闭合不得激活)、
|
职责: 流程层:流程包定义与版本(建/克隆/启停/**删/改名**)、**闭合校验**(边只表达顺序与条件 · 画图不检查 · 不闭合不得激活)、
|
||||||
负责人四策略的确定性解析、工作流状态的聚合投影、失败走向的消化判定、节点实例重开
|
负责人四策略的确定性解析、工作流状态的聚合投影、失败走向的消化判定、节点实例重开
|
||||||
不负责: 活动的执行(见 task 与范式引擎)、触发器(与流程包**平级**,见 trigger)、
|
不负责: 活动的执行(见 task 与范式引擎)、触发器(与流程包**平级**,见 trigger)、
|
||||||
画布的绘制(见 apps/web 的 gov-process)
|
画布的绘制(见 apps/web 的 gov-process)
|
||||||
@@ -25,6 +25,12 @@
|
|||||||
被依赖: trigger, app(治理台 · 工作流管理)
|
被依赖: trigger, app(治理台 · 工作流管理)
|
||||||
|
|
||||||
不变量(OS 03 / 3.2):
|
不变量(OS 03 / 3.2):
|
||||||
|
- **字段只声明,不做任何要求**(2026-07-30 用户定):编排层不校验字段 —— 要什么、够不够、
|
||||||
|
类型对不对全归活动自己。激活闸只剩两面:**结构闭合**(dangling-node / multi-default-edge)
|
||||||
|
与**活动装载**(unknown-activity);字段类的三条检查(边未映射 / 必填未映射 / 类型相抵)全退役。
|
||||||
|
- **版本可删,两道闸**:生效版不可删(先停用,`ActiveVersionError`)、在途钉版不可删
|
||||||
|
(那些单要按它跑完,`PinnedVersionError`)。**改名及于整条流的所有版本** ——
|
||||||
|
名字是流的身份,不是某一版的属性。
|
||||||
- **数据按字段名流动**(2026-07-30 对齐 Mastra,替代「连线即映射」):边不带映射,
|
- **数据按字段名流动**(2026-07-30 对齐 Mastra,替代「连线即映射」):边不带映射,
|
||||||
只表达顺序与条件(条件读上游输出字段)。节点入参 = 铺平作用域
|
只表达顺序与条件(条件读上游输出字段)。节点入参 = 铺平作用域
|
||||||
(起单入参 ← 沿途各输出口字段 ← 刚发射的载荷,后者赢)按下游契约取;
|
(起单入参 ← 沿途各输出口字段 ← 刚发射的载荷,后者赢)按下游契约取;
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ describe('checkClosure —— 不闭合不得激活', () => {
|
|||||||
expect(report.closed).toBe(true);
|
expect(report.closed).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('同名字段类型相抵即拦 —— 名字即管道,名同型异必坏', () => {
|
it('字段一概不查(2026-07-30「只声明,不做任何要求」)—— 名同型异也不拦激活', () => {
|
||||||
// 上游 done 口发 quote: number …下游若声明 quote: string 就是硬伤
|
// 上游 done 口发 quote: number,下游声明 quote: string:从前是 type-conflict,现在归活动自己管
|
||||||
const clash = new Map([
|
const clash = new Map([
|
||||||
['a4', face({ inputTypes: { pn: 'string', quote: 'string' }, requiredInputs: ['pn'] })],
|
['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 },
|
{ id: 'e2', from: 'A4' as never, to: 'end' as never, reopen: false },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const violations = checkClosure({ pkg: chain, contracts: clash }).violations;
|
expect(checkClosure({ pkg: chain, contracts: clash }).closed, '结构闭合、活动在库 —— 就该激活得了').toBe(true);
|
||||||
expect(violations.filter((v) => v.kind === 'type-conflict').map((v) => v.field)).toEqual(['quote']);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('引用未安装的活动即拦', () => {
|
it('引用未安装的活动即拦', () => {
|
||||||
@@ -124,40 +123,6 @@ describe('checkClosure —— 不闭合不得激活', () => {
|
|||||||
).toEqual([]);
|
).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 —— 四策略确定性解析', () => {
|
describe('resolveOwner —— 四策略确定性解析', () => {
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ function harness(seed: ProcessPackage[], pinned = 0) {
|
|||||||
insertInstance: async () => undefined,
|
insertInstance: async () => undefined,
|
||||||
updateInstance: async () => undefined,
|
updateInstance: async () => undefined,
|
||||||
findInstanceByTask: async () => undefined,
|
findInstanceByTask: async () => undefined,
|
||||||
|
deletePackage: async () => undefined,
|
||||||
|
renameFlow: async () => undefined,
|
||||||
nodeOwners: async () => ({}),
|
nodeOwners: async () => ({}),
|
||||||
setNodeOwner: async () => undefined,
|
setNodeOwner: async () => undefined,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ function harness(validationErrors: readonly string[]): Harness {
|
|||||||
},
|
},
|
||||||
updateInstance: async () => undefined,
|
updateInstance: async () => undefined,
|
||||||
findInstanceByTask: async () => undefined,
|
findInstanceByTask: async () => undefined,
|
||||||
|
deletePackage: async () => undefined,
|
||||||
|
renameFlow: async () => undefined,
|
||||||
nodeOwners: async () => ({}),
|
nodeOwners: async () => ({}),
|
||||||
setNodeOwner: async () => undefined,
|
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(/不存在/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -79,6 +79,11 @@ export type ProcessService = {
|
|||||||
listWorkflows(limit?: number): Promise<WorkflowRecord[]>;
|
listWorkflows(limit?: number): Promise<WorkflowRecord[]>;
|
||||||
instancesOf(workflowId: string): Promise<NodeInstance[]>;
|
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>>;
|
nodeOwners(packageId: string): Promise<Record<string, string>>;
|
||||||
setNodeOwner(packageId: string, nodeId: string, staffId: string | undefined): Promise<void>;
|
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 {
|
export class CallInterfaceViolationError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
readonly packageId: string,
|
readonly packageId: string,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export type {
|
|||||||
WorkflowRecord,
|
WorkflowRecord,
|
||||||
WorkflowState,
|
WorkflowState,
|
||||||
} from './contract.ts';
|
} from './contract.ts';
|
||||||
export { CallInterfaceViolationError, NotClosedError,
|
export { ActiveVersionError, CallInterfaceViolationError, NotClosedError,
|
||||||
PinnedVersionError, PositionVacantError, StaleVersionError } from './contract.ts';
|
PinnedVersionError, PositionVacantError, StaleVersionError } from './contract.ts';
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
|
|||||||
@@ -58,6 +58,24 @@ export function createProcessStore(db: Db): ProcessStore {
|
|||||||
.where(and(eq(processPackageTable.id, id), eq(processPackageTable.version, version)));
|
.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) {
|
async getPackage(id, version) {
|
||||||
const conditions = [eq(processPackageTable.id, id)];
|
const conditions = [eq(processPackageTable.id, id)];
|
||||||
if (version !== undefined) conditions.push(eq(processPackageTable.version, version));
|
if (version !== undefined) conditions.push(eq(processPackageTable.version, version));
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export function checkClosure(input: ClosureInput): ClosureReport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (node.kind === 'activity') {
|
if (node.kind === 'activity') {
|
||||||
violations.push(...checkActivityNode(node, incoming.get(node.id) ?? [], contracts, byId));
|
violations.push(...checkActivityNode(node, contracts));
|
||||||
}
|
}
|
||||||
// 岗位不入编排(活动点与审批点同一口径,2026-07-28 第五轮):没有岗位可核
|
// 岗位不入编排(活动点与审批点同一口径,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 };
|
return { closed: violations.length === 0, violations };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 活动点只查一件事:**引用的活动装没装**(判据二);字段一概不查(只声明,不要求) */
|
||||||
function checkActivityNode(
|
function checkActivityNode(
|
||||||
node: Extract<ProcessNode, { kind: 'activity' }>,
|
node: Extract<ProcessNode, { kind: 'activity' }>,
|
||||||
inbound: readonly Edge[],
|
|
||||||
contracts: ReadonlyMap<string, ActivityContractFace>,
|
contracts: ReadonlyMap<string, ActivityContractFace>,
|
||||||
byId: ReadonlyMap<string, ProcessNode>,
|
|
||||||
): ClosureViolation[] {
|
): ClosureViolation[] {
|
||||||
const out: ClosureViolation[] = [];
|
const out: ClosureViolation[] = [];
|
||||||
const face = contracts.get(node.activityId);
|
const face = contracts.get(node.activityId);
|
||||||
@@ -101,30 +98,10 @@ function checkActivityNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* 数据按字段名流动(2026-07-30 对齐 Mastra):边不带映射,「必填有没有着落」
|
* **字段只声明,不做任何要求**(2026-07-30 用户定):编排层不校验字段 ——
|
||||||
* 不再是激活阻断 —— 宽进纪律下,缺的字段运行时由受理补全单找人收齐。
|
* 要什么、够不够、类型对不对,全归活动自己;缺了由活动内的实现去补,不在这一层现形。
|
||||||
* 激活闸只留 schema 咬合这一条硬伤:**上游产出与下游输入同名却不同型**,
|
* 于是激活闸只剩两面:**结构闭合**(下面几条)与**活动装载**(上面的 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:网关去节点化)。
|
* 多入边**不再判「隐式合并」**(产品决策 2026-07-28:网关去节点化)。
|
||||||
@@ -136,46 +113,6 @@ function checkActivityNode(
|
|||||||
return out;
|
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 {
|
export function closed(report: ClosureReport): boolean {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
import type { ProcessPackage } from '@contracts';
|
import type { ProcessPackage } from '@contracts';
|
||||||
import type { ProcessService, StartWorkflowInput } from '../../contract.ts';
|
import type { ProcessService, StartWorkflowInput } from '../../contract.ts';
|
||||||
import { NotClosedError, PinnedVersionError, StaleVersionError } from '../../contract.ts';
|
import { ActiveVersionError, NotClosedError, PinnedVersionError, StaleVersionError } from '../../contract.ts';
|
||||||
import type {
|
import type {
|
||||||
ActivityDirectory,
|
ActivityDirectory,
|
||||||
ApprovalGateway,
|
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);
|
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() {
|
async listPackages() {
|
||||||
return deps.store.listPackages();
|
return deps.store.listPackages();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ export type ProcessStore = {
|
|||||||
countPinnedInFlight(id: string, version: number): Promise<number>;
|
countPinnedInFlight(id: string, version: number): Promise<number>;
|
||||||
/** 只改状态,不动定义 —— state 在列上与 definition 里各存一份,两处要一起改 */
|
/** 只改状态,不动定义 —— state 在列上与 definition 里各存一份,两处要一起改 */
|
||||||
setPackageState(id: string, version: number, state: 'active' | 'retired'): Promise<void>;
|
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>;
|
getPackage(id: string, version?: number): Promise<ProcessPackage | undefined>;
|
||||||
listPackages(): Promise<ProcessPackage[]>;
|
listPackages(): Promise<ProcessPackage[]>;
|
||||||
|
|
||||||
|
|||||||
@@ -341,10 +341,9 @@
|
|||||||
/* 图自身按 aspect-ratio 缩放,整张一定看得全——纸面不再限高、不再内部滚动 */
|
/* 图自身按 aspect-ratio 缩放,整张一定看得全——纸面不再限高、不再内部滚动 */
|
||||||
.g-canvas .process-desk { flex: 1; min-height: 0; height: auto; max-height: none; overflow: visible; padding: 0; background: transparent; display: flex; flex-direction: column; }
|
.g-canvas .process-desk { flex: 1; min-height: 0; height: auto; max-height: none; overflow: visible; padding: 0; background: transparent; display: flex; flex-direction: column; }
|
||||||
.g-canvas .process-doc { flex: 1; min-height: 0; box-sizing: border-box; max-width: none; padding: 14px 18px 18px; border-radius: 10px; box-shadow: var(--cshadow); display: flex; flex-direction: column; }
|
.g-canvas .process-doc { flex: 1; min-height: 0; box-sizing: border-box; max-width: none; padding: 14px 18px 18px; border-radius: 10px; box-shadow: var(--cshadow); display: flex; flex-direction: column; }
|
||||||
/* 图框吃满纸面,并在下方空出**恰好一条底栏的高度** —— 图框下沿与左列版本卡底栏
|
/* 图框吃满纸面(纸被栅格拉高时跟着长,不会浮回上面);下沿留白与纸面左右内距同为 18,
|
||||||
(.g-vercard-f,净高 38)的上沿连成同一条横线。20 = 38(底栏净高) − 18(纸面下内距)。
|
四边等距 —— 三栏改版后左列不再有底栏,原先那条「对齐底栏顶线」的 20px 就没有对齐对象了。 */
|
||||||
纸面被栅格拉高时(版本多、左列更长)图框跟着长,才不会又浮回上面去。 */
|
.g-canvas .process-doc > .hero { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; margin-bottom: 0; }
|
||||||
.g-canvas .process-doc > .hero { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; margin-bottom: 20px; }
|
|
||||||
.g-canvas .process-doc > .hero > .fig-body { flex: 1 1 auto; min-height: 0; }
|
.g-canvas .process-doc > .hero > .fig-body { flex: 1 1 auto; min-height: 0; }
|
||||||
/* 画布上的选中态(外挂类,_flow 复刻件未改):命中层描边 = 选中框 */
|
/* 画布上的选中态(外挂类,_flow 复刻件未改):命中层描边 = 选中框 */
|
||||||
.g-canvas .flow-node-sel .djs-hit { fill: rgba(51, 112, 255, .09) !important; stroke: #3370ff !important; stroke-width: 2.5px !important; stroke-opacity: 1 !important; rx: 7px; ry: 7px; }
|
.g-canvas .flow-node-sel .djs-hit { fill: rgba(51, 112, 255, .09) !important; stroke: #3370ff !important; stroke-width: 2.5px !important; stroke-opacity: 1 !important; rx: 7px; ry: 7px; }
|
||||||
@@ -520,7 +519,11 @@
|
|||||||
/* ── 流程详情:左版本列 + 右画布(补充)──────────────────────────────
|
/* ── 流程详情:左版本列 + 右画布(补充)──────────────────────────────
|
||||||
版本收在流程内部:左列是这条流的全部版本(恰一个生效),点谁看谁的图;
|
版本收在流程内部:左列是这条流的全部版本(恰一个生效),点谁看谁的图;
|
||||||
属性区从右侧窄栏挪到图下整行(g-wide),声明式契约细节再往下。 */
|
属性区从右侧窄栏挪到图下整行(g-wide),声明式契约细节再往下。 */
|
||||||
.g-flowlab { display: grid; grid-template-columns: minmax(220px, 1fr) minmax(0, 2.2fr); gap: 12px; align-items: stretch; }
|
/* 三栏(2026-07-30):左**窄**版本条 · 中流程图 · 右节点属性。
|
||||||
|
右栏用包壳绝对定位 —— 行高由中栏的图定,属性长了在卡内滚,不把图拉长。 */
|
||||||
|
.g-flowlab { display: grid; grid-template-columns: 132px minmax(0, 2.5fr) minmax(320px, 1.25fr); gap: 12px; align-items: stretch; }
|
||||||
|
.g-nodewrap { position: relative; min-height: 0; }
|
||||||
|
.g-nodewrap > .g-nodecard { position: absolute; inset: 0; height: auto; }
|
||||||
/* 版本卡:与右侧的图**等高**(grid stretch),版本多了在卡内滚,不把页面撑长。
|
/* 版本卡:与右侧的图**等高**(grid stretch),版本多了在卡内滚,不把页面撑长。
|
||||||
形制取**图纸风**,与右边的流程图同一套语言(flow-canvas.css 的 process-doc):
|
形制取**图纸风**,与右边的流程图同一套语言(flow-canvas.css 的 process-doc):
|
||||||
白纸同影同角、mono 小字距大写题头(VER — 前缀对 FIG — 前缀)、钢青/黄铜色系、
|
白纸同影同角、mono 小字距大写题头(VER — 前缀对 FIG — 前缀)、钢青/黄铜色系、
|
||||||
@@ -541,6 +544,27 @@
|
|||||||
.g-ver-new:hover { border-color: #4e7daf; box-shadow: 0 0 0 2px rgba(78, 125, 175, .14); }
|
.g-ver-new:hover { border-color: #4e7daf; box-shadow: 0 0 0 2px rgba(78, 125, 175, .14); }
|
||||||
.g-ver-new:disabled { opacity: .5; cursor: default; box-shadow: none; }
|
.g-ver-new:disabled { opacity: .5; cursor: default; box-shadow: none; }
|
||||||
.g-vercard-list { flex: 1; min-height: 0; overflow-y: auto; padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; }
|
.g-vercard-list { flex: 1; min-height: 0; overflow-y: auto; padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
|
||||||
|
/* ── 窄版本条:一版一行(v几 + 状态 + 计数),动作只在**看着的那一版**上展开 ── */
|
||||||
|
.g-verslim .g-vercard-h { padding: 30px 10px 16px; gap: 4px; }
|
||||||
|
.g-verslim .g-vercard-list { padding: 10px 8px; gap: 6px; }
|
||||||
|
.g-verrow { display: flex; flex-direction: column; border: 1px solid var(--sh-line); border-left: 3px solid var(--sh-steel); background: #fff; }
|
||||||
|
.g-verrow.on { border-color: #9fb0c0; background: #f2f5f8; }
|
||||||
|
.g-verrow.live { border-left-color: var(--sh-brass); background: var(--sh-brass-wash); }
|
||||||
|
.g-verrow.live.on { border-color: #cbb98d; }
|
||||||
|
.g-verrow-h { display: flex; flex-wrap: wrap; align-items: baseline; gap: 3px 6px; padding: 7px 8px; border: 0; background: transparent; font-family: var(--sh-mono); font-size: 10px; color: #3a4452; text-align: left; cursor: pointer; }
|
||||||
|
.g-verrow-h > b { font-size: 12.5px; font-weight: 700; color: var(--sh-ink); }
|
||||||
|
.g-verrow-h .st { font-size: 9px; letter-spacing: .1em; color: var(--sh-muted); }
|
||||||
|
.g-verrow-h .st.live { color: var(--sh-brass); font-weight: 700; }
|
||||||
|
.g-verrow-h .cnt { flex-basis: 100%; font-size: 9px; color: var(--sh-muted); }
|
||||||
|
.g-verrow-a { display: flex; flex-wrap: wrap; gap: 4px; padding: 0 8px 8px; }
|
||||||
|
.g-verrow-a .g-ver-act { flex: 1 1 auto; text-align: center; padding: 3px 6px; letter-spacing: .06em; }
|
||||||
|
.g-ver-act.t-bad { color: #b0413e; border-color: #e0bcba; }
|
||||||
|
.g-ver-act:disabled { opacity: .45; cursor: default; }
|
||||||
|
/* 改名条:名字改的是整条流,所以落在卡底、不挂在某一行上 */
|
||||||
|
.g-verrename { flex: none; display: flex; flex-direction: column; gap: 5px; padding: 9px 10px; border-top: 1px solid var(--sh-line); background: #f9fafc; }
|
||||||
|
.g-verrename .g-input { height: 26px; font-size: 12px; }
|
||||||
|
.g-verrename .t { font-family: var(--sh-mono); font-size: 8.5px; line-height: 1.7; color: var(--sh-muted); }
|
||||||
.g-ver { display: flex; align-items: center; gap: 9px; padding: 9px 11px; border: 1px solid var(--sh-line); border-left: 3px solid var(--sh-steel); background: #fff; cursor: pointer; font-family: var(--sh-mono); font-size: 11px; color: #3a4452; }
|
.g-ver { display: flex; align-items: center; gap: 9px; padding: 9px 11px; border: 1px solid var(--sh-line); border-left: 3px solid var(--sh-steel); background: #fff; cursor: pointer; font-family: var(--sh-mono); font-size: 11px; color: #3a4452; }
|
||||||
.g-ver:hover { border-color: #aeb9c6; }
|
.g-ver:hover { border-color: #aeb9c6; }
|
||||||
.g-ver.on { background: #f2f5f8; border-color: #9fb0c0; }
|
.g-ver.on { background: #f2f5f8; border-color: #9fb0c0; }
|
||||||
@@ -554,10 +578,6 @@
|
|||||||
.g-ver-act { border: 1px solid var(--sh-line); background: #fff; padding: 2px 8px; font-family: inherit; font-size: 9.5px; letter-spacing: .1em; color: #3a4452; cursor: pointer; }
|
.g-ver-act { border: 1px solid var(--sh-line); background: #fff; padding: 2px 8px; font-family: inherit; font-size: 9.5px; letter-spacing: .1em; color: #3a4452; cursor: pointer; }
|
||||||
.g-ver-act:hover { border-color: #aeb9c6; }
|
.g-ver-act:hover { border-color: #aeb9c6; }
|
||||||
.g-ver-act.t-go { color: var(--sh-brass); border-color: #d9c9a4; }
|
.g-ver-act.t-go { color: var(--sh-brass); border-color: #d9c9a4; }
|
||||||
/* 卡底栏 —— 它的净高(1 顶线 + 10 + 17 行 + 10 = 38)是**两列共用的基线**:
|
|
||||||
右侧画布的图框下沿要落在这条顶线上(见上面 .g-canvas .process-doc > .hero 的下外距)。
|
|
||||||
文案改长到换行、或改了内距,那边的 20px 要跟着算。 */
|
|
||||||
.g-vercard-f { flex: none; min-height: 38px; box-sizing: border-box; padding: 10px 16px; border-top: 1px solid var(--sh-line); font-family: var(--sh-mono); font-size: 9.5px; line-height: 1.8; color: var(--sh-muted); }
|
|
||||||
.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 { 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:hover { border-color: #c9d6e6; }
|
||||||
.g-ver.on { border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-wash); }
|
.g-ver.on { border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-wash); }
|
||||||
@@ -572,35 +592,16 @@
|
|||||||
检验章(判据面全清的合格章)是唯一的戏眼,其余一律细线纪律。 */
|
检验章(判据面全清的合格章)是唯一的戏眼,其余一律细线纪律。 */
|
||||||
|
|
||||||
/* ── 激活 · 双判据 → 检验单:上下两格判据面均分卡高,面头一行、面体列违规 ── */
|
/* ── 激活 · 双判据 → 检验单:上下两格判据面均分卡高,面头一行、面体列违规 ── */
|
||||||
.g-exboard { min-height: 0; }
|
|
||||||
.g-exboard .g-vercard-h { height: auto; padding: 13px 18px 10px; }
|
|
||||||
.g-vercard-list.g-exsheet { padding: 0; gap: 0; overflow: hidden; }
|
|
||||||
.g-exface { flex: 1 1 0; min-height: 0; display: flex; flex-direction: column; }
|
|
||||||
.g-exface + .g-exface { border-top: 1px solid var(--sh-line); }
|
|
||||||
.g-exfacet { flex: none; display: flex; align-items: center; gap: 7px; padding: 10px 16px 9px; font: 600 9.5px/1 var(--sh-mono, var(--mono)); letter-spacing: 0.14em; text-transform: uppercase; color: var(--sh-steel, #46586a); border-bottom: 1px dashed #e4e9ef; }
|
|
||||||
.g-exfacet.ok { color: #2e7d4f; }
|
|
||||||
.g-exfacet .st { margin-left: auto; letter-spacing: .1em; text-align: right; }
|
|
||||||
.g-exfacet:not(.ok) .st { color: #b0413e; }
|
|
||||||
.g-exfacet:not(.ok) svg { color: #b0413e; }
|
|
||||||
.g-exface-b { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 7px; padding: 10px 14px 12px; }
|
|
||||||
/* 违规行:方角细线 + 红左线(压回全局 .g-ver 的圆角白卡版);点一条定位到节点属性 */
|
/* 违规行:方角细线 + 红左线(压回全局 .g-ver 的圆角白卡版);点一条定位到节点属性 */
|
||||||
.g-exface-b .g-ver { border-radius: 0; border: 1px solid var(--sh-line); border-left: 3px solid #b0413e; padding: 7px 10px; font-family: var(--sh-mono); box-shadow: none; }
|
|
||||||
.g-exface-b .g-ver:hover { border-color: #aeb9c6; border-left-color: #b0413e; }
|
|
||||||
.g-exface-b .g-ver:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
|
|
||||||
.g-ex { cursor: pointer; border-left-color: #b0413e; }
|
.g-ex { cursor: pointer; border-left-color: #b0413e; }
|
||||||
.g-ex > b { font-size: 11px; }
|
.g-ex > b { font-size: 11px; }
|
||||||
.g-ex .msg { flex: 1; min-width: 0; font-size: 10.5px; line-height: 1.55; letter-spacing: 0; text-transform: none; color: #3a4452; }
|
.g-ex .msg { flex: 1; min-width: 0; font-size: 10.5px; line-height: 1.55; letter-spacing: 0; text-transform: none; color: #3a4452; }
|
||||||
/* 检验章:该面清零即盖 —— 双线方框微旋,检验单的通过凭证;半透边线是印泥感 */
|
/* 检验章:该面清零即盖 —— 双线方框微旋,检验单的通过凭证;半透边线是印泥感 */
|
||||||
.g-exstamp { margin: auto; display: flex; flex-direction: column; align-items: center; gap: 5px; padding: 11px 20px 10px; border: 1.5px solid rgba(46, 125, 79, .55); outline: 1px solid rgba(46, 125, 79, .3); outline-offset: 3px; color: #2e7d4f; transform: rotate(-4deg); font: 700 12.5px/1.2 var(--sh-mono); letter-spacing: .34em; text-indent: .34em; }
|
|
||||||
.g-exstamp small { font-size: 7.5px; font-weight: 600; letter-spacing: .18em; text-indent: .18em; opacity: .7; }
|
|
||||||
/* 第二面的章反向微旋 —— 两枚章不该是同一手势盖出来的 */
|
/* 第二面的章反向微旋 —— 两枚章不该是同一手势盖出来的 */
|
||||||
.g-exface + .g-exface .g-exstamp { transform: rotate(2.5deg); }
|
|
||||||
/* 裁定带:两面全清才出 —— 黄铜是「可生效」的颜色 */
|
/* 裁定带:两面全清才出 —— 黄铜是「可生效」的颜色 */
|
||||||
.g-exverdict { flex: none; padding: 9px 16px 9px 13px; border-top: 1px solid var(--sh-line); border-left: 3px solid var(--sh-brass); background: var(--sh-brass-wash); font-family: var(--sh-mono); font-size: 10px; line-height: 1.8; color: var(--sh-brass); }
|
|
||||||
.g-exboard-empty { padding: 16px 18px; font-family: var(--sh-mono); font-size: 10px; line-height: 1.9; color: var(--sh-muted); }
|
|
||||||
|
|
||||||
/* ── 节点 · 属性 → 索引栏(固定)+ 明细区(滚动)────────────────────── */
|
/* ── 节点 · 属性 → 索引栏(固定)+ 明细区(滚动)────────────────────── */
|
||||||
.g-nodecard { height: 560px; min-height: 0; }
|
.g-nodecard { min-height: 0; }
|
||||||
.g-nodecard .g-vercard-h { height: auto; padding: 13px 18px 10px; }
|
.g-nodecard .g-vercard-h { height: auto; padding: 13px 18px 10px; }
|
||||||
.g-nodecard .g-vercard-h .n { margin-left: auto; letter-spacing: .1em; }
|
.g-nodecard .g-vercard-h .n { margin-left: auto; letter-spacing: .1em; }
|
||||||
.g-nodecard .g-vercard-h .n.dirty { color: var(--sh-brass); }
|
.g-nodecard .g-vercard-h .n.dirty { color: var(--sh-brass); }
|
||||||
@@ -609,15 +610,6 @@
|
|||||||
.g-nodecard-b > .g-note { flex: none; margin: 10px 14px 0; }
|
.g-nodecard-b > .g-note { flex: none; margin: 10px 14px 0; }
|
||||||
|
|
||||||
/* 索引栏 = 图框分度带:浅底压一层,行首竖签(节点/边 + 计数),芯片缩成分度块 */
|
/* 索引栏 = 图框分度带:浅底压一层,行首竖签(节点/边 + 计数),芯片缩成分度块 */
|
||||||
.g-pickrail { flex: none; background: #f9fafc; border-bottom: 1px solid var(--sh-line); }
|
|
||||||
.g-pickrow { display: flex; align-items: flex-start; gap: 10px; padding: 8px 14px; }
|
|
||||||
.g-pickrow + .g-pickrow { border-top: 1px dashed #e4e9ef; }
|
|
||||||
.g-picklbl { flex: none; display: inline-flex; align-items: baseline; gap: 4px; min-width: 42px; padding-top: 6px; font: 600 9px/1 var(--sh-mono); letter-spacing: .14em; text-transform: uppercase; color: var(--sh-steel); }
|
|
||||||
.g-picklbl b { font-weight: 600; font-size: 9px; color: var(--sh-muted); }
|
|
||||||
.g-pickrow .g-picker { gap: 4px; }
|
|
||||||
.g-nodecard .g-pick { height: 22px; padding: 0 8px; gap: 5px; border-radius: 0; border-color: var(--sh-line); background: #fff; font-size: 11px; color: #3a4452; }
|
|
||||||
.g-nodecard .g-pick b { font-size: 9px; font-family: var(--sh-mono); }
|
|
||||||
.g-nodecard .g-pick:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
|
|
||||||
|
|
||||||
/* 明细区:卡内唯一的滚动层 —— 索引在上钉住,拉表单不丢定位 */
|
/* 明细区:卡内唯一的滚动层 —— 索引在上钉住,拉表单不丢定位 */
|
||||||
.g-propsheet { flex: 1; min-height: 0; overflow-y: auto; padding: 12px 14px 14px; }
|
.g-propsheet { flex: 1; min-height: 0; overflow-y: auto; padding: 12px 14px 14px; }
|
||||||
@@ -628,9 +620,19 @@
|
|||||||
.g-sheetempty b { font-size: 12px; font-weight: 700; color: var(--sh-steel); }
|
.g-sheetempty b { font-size: 12px; font-weight: 700; color: var(--sh-steel); }
|
||||||
.g-sheetempty p { margin: 0; font-size: 11px; line-height: 1.9; color: var(--sh-muted); }
|
.g-sheetempty p { margin: 0; font-size: 11px; line-height: 1.9; color: var(--sh-muted); }
|
||||||
|
|
||||||
/* 属性小卡:两列手排、按内容自高 —— 空白不再被 250 定高摊出来,长卡也不内滚;
|
/* 缺项清单:没选节点时占住空态 —— 原「激活判据」看板的活儿收进这里,点一条定位过去 */
|
||||||
左列 = 身份与数据口,右列 = 类型与装载(JSX 里 .g-propcol 手排,语义成组、高度可控) */
|
.g-lack { width: 100%; display: flex; flex-direction: column; gap: 6px; text-align: left; }
|
||||||
.g-nodecard .g-props { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; gap: 12px; }
|
.g-lack-h { display: flex; align-items: center; gap: 7px; font: 600 9.5px/1 var(--sh-mono); letter-spacing: .14em; text-transform: uppercase; color: #b0413e; padding-bottom: 2px; }
|
||||||
|
.g-lack-r { display: flex; align-items: baseline; gap: 8px; width: 100%; padding: 7px 10px; border: 1px solid var(--sh-line); border-left: 3px solid #b0413e; background: #fff; font: inherit; font-size: 11px; line-height: 1.7; color: #3a4452; text-align: left; cursor: pointer; }
|
||||||
|
.g-lack-r:hover:not(:disabled) { border-color: #aeb9c6; border-left-color: #b0413e; }
|
||||||
|
.g-lack-r:disabled { cursor: default; opacity: .75; }
|
||||||
|
.g-lack-r > b { flex: none; font-family: var(--sh-mono); font-size: 10px; color: var(--sh-ink); }
|
||||||
|
/* 两面都齐:一枚小检验章,别再占一整张卡 */
|
||||||
|
.g-okstamp { display: inline-flex; align-items: center; gap: 6px; margin-top: 4px; padding: 5px 11px; border: 1px solid rgba(46, 125, 79, .45); font: 600 10px/1 var(--sh-mono); letter-spacing: .08em; color: #2e7d4f; }
|
||||||
|
|
||||||
|
/* 属性小卡:三栏改版后右栏是**窄轨**(≈335),两列摊不开 —— 改单列竖排,
|
||||||
|
顺序即阅读次序:身份 → 数据接口 → 类型与责任 → 装载 → 出边。长了在明细区自己滚。 */
|
||||||
|
.g-nodecard .g-props { display: flex; flex-direction: column; align-items: stretch; gap: 12px; }
|
||||||
.g-propcol { min-width: 0; display: flex; flex-direction: column; gap: 12px; }
|
.g-propcol { min-width: 0; display: flex; flex-direction: column; gap: 12px; }
|
||||||
.g-nodecard .g-props.g-eprops { display: flex; flex-direction: column; align-items: stretch; gap: 12px; }
|
.g-nodecard .g-props.g-eprops { display: flex; flex-direction: column; align-items: stretch; gap: 12px; }
|
||||||
.g-nodecard .g-prop { border: 1px solid var(--sh-line); border-radius: 0; box-shadow: none; }
|
.g-nodecard .g-prop { border: 1px solid var(--sh-line); border-radius: 0; box-shadow: none; }
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
* feature `gov-process` —— 流程包(设计态):**一条流一个包,版本收在包内部**。
|
* feature `gov-process` —— 流程包(设计态):**一条流一个包,版本收在包内部**。
|
||||||
*
|
*
|
||||||
* 一区一内容:三枚计数摆在货架顶端,底下就是包货架(触发器已独立成区)。
|
* 一区一内容:三枚计数摆在货架顶端,底下就是包货架(触发器已独立成区)。
|
||||||
* 货架一流程一卡(不按版本平铺);进详情后左 1/3 是版本列表、右 2/3 是流程图,
|
* 货架一流程一卡(不按版本平铺);进详情是**三栏**:左窄轨=版本(选中那版才出动作:
|
||||||
* 图下是节点属性(身份 · 数据接口 · 装载 · 出边条件),再往下是调用接口与触发器。
|
* 激活/停用 · 改名 · 删除)、中=流程图、右=节点属性(点图选节点,内容长了卡内滚,不拉长图)。
|
||||||
|
* 「激活判据」不再单占一卡 —— 没选节点时,属性栏就地列出还差哪几条,点一条定位过去。
|
||||||
*
|
*
|
||||||
* 版本是**运行时开关**,不是发布流程:
|
* 版本是**运行时开关**,不是发布流程:
|
||||||
* · 编辑即其身:保存**写回当前版本**(号不变);新版本只出自「新建」
|
* · 编辑即其身:保存**写回当前版本**(号不变);新版本只出自「新建」
|
||||||
@@ -14,7 +15,14 @@
|
|||||||
import { useRef, useState, useEffect } from 'react';
|
import { useRef, useState, useEffect } from 'react';
|
||||||
import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
|
||||||
import type { ClosureReport, ProcessPackage, ProcessNode } from '@contracts';
|
import type { ClosureReport, ProcessPackage, ProcessNode } from '@contracts';
|
||||||
import { processPackageSchema, saveProcessRoute, setProcessStateRoute, validateProcessRoute } from '@contracts';
|
import {
|
||||||
|
deleteProcessVersionRoute,
|
||||||
|
processPackageSchema,
|
||||||
|
renameProcessRoute,
|
||||||
|
saveProcessRoute,
|
||||||
|
setProcessStateRoute,
|
||||||
|
validateProcessRoute,
|
||||||
|
} from '@contracts';
|
||||||
import { call } from '@web-shared/typed-fetch.ts';
|
import { call } from '@web-shared/typed-fetch.ts';
|
||||||
import { Ico } from '@web-shared/Ico.tsx';
|
import { Ico } from '@web-shared/Ico.tsx';
|
||||||
import type { AskSpec } from '@web-shared/gov/primitives.tsx';
|
import type { AskSpec } from '@web-shared/gov/primitives.tsx';
|
||||||
@@ -351,6 +359,8 @@ function FlowLab({
|
|||||||
const [dirty, setDirty] = useState(false);
|
const [dirty, setDirty] = useState(false);
|
||||||
const [report, setReport] = useState<ClosureReport | undefined>(undefined);
|
const [report, setReport] = useState<ClosureReport | undefined>(undefined);
|
||||||
const [ask, setAsk] = useState<{ spec: AskSpec; danger?: boolean } | undefined>(undefined);
|
const [ask, setAsk] = useState<{ spec: AskSpec; danger?: boolean } | undefined>(undefined);
|
||||||
|
/** 改名就地开一个输入框(名字改的是整条流,不是某一版) */
|
||||||
|
const [renaming, setRenaming] = useState(false);
|
||||||
const canvasRef = useRef<HTMLDivElement>(null);
|
const canvasRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const nodes = graph?.nodes ?? [];
|
const nodes = graph?.nodes ?? [];
|
||||||
@@ -365,49 +375,6 @@ function FlowLab({
|
|||||||
(node.act === undefined && semanticNode?.kind === 'activity' && a.activityId === semanticNode.activityId),
|
(node.act === undefined && semanticNode?.kind === 'activity' && a.activityId === semanticNode.activityId),
|
||||||
);
|
);
|
||||||
|
|
||||||
/*
|
|
||||||
* 数据接口的来源推导(对齐 Mastra 名流动):这个节点的每个输入字段,运行时会从哪来 ——
|
|
||||||
* ① 起单入参里有同名字段 → 起单带进;② 某个上游(沿边回溯)的输出口发同名字段 → 上游发;
|
|
||||||
* ③ 都没有 → 受理归一时派补全单找人收。设计期看得见,不必等跑起来猜。
|
|
||||||
*/
|
|
||||||
const fieldSources = (() => {
|
|
||||||
if (node === null) return new Map<string, string>();
|
|
||||||
const initFields = new Set(
|
|
||||||
Object.keys(((pkg.callInterface.input as { properties?: Record<string, unknown> }).properties ?? {})),
|
|
||||||
);
|
|
||||||
// 祖先集:沿边回溯(含回边,访问集防环)
|
|
||||||
const ancestors = new Set<string>();
|
|
||||||
const queue = [node.id];
|
|
||||||
while (queue.length > 0) {
|
|
||||||
const cur = queue.pop()!;
|
|
||||||
for (const e of graph?.edges ?? []) {
|
|
||||||
if (e.to !== cur || ancestors.has(e.from)) continue;
|
|
||||||
ancestors.add(e.from);
|
|
||||||
queue.push(e.from);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const actOf = (nid: string): string | undefined => {
|
|
||||||
const g = nodes.find((x) => x.id === nid);
|
|
||||||
if (g?.act !== undefined) return g.act;
|
|
||||||
const sem = pkg.nodes.find((x) => x.id === nid);
|
|
||||||
return sem?.kind === 'activity' ? sem.activityId : undefined;
|
|
||||||
};
|
|
||||||
const out = new Map<string, string>();
|
|
||||||
for (const f of refAct?.contract.inputFields ?? []) {
|
|
||||||
if (initFields.has(f.name)) {
|
|
||||||
out.set(f.name, '起单入参');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const from = [...ancestors].find((nid) =>
|
|
||||||
activities
|
|
||||||
.find((a) => a.activityId === actOf(nid))
|
|
||||||
?.contract.outputPorts.some((port) => port.fields.includes(f.name)),
|
|
||||||
);
|
|
||||||
out.set(f.name, from !== undefined ? `上游 ${from}` : '补全单');
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
})();
|
|
||||||
|
|
||||||
// 每次进页问一次闭合 —— 画图即校验(与后端裁决同源);落笔保存后用返回的报告
|
// 每次进页问一次闭合 —— 画图即校验(与后端裁决同源);落笔保存后用返回的报告
|
||||||
const liveReport = useQuery({
|
const liveReport = useQuery({
|
||||||
queryKey: ['gov', 'process-report', pkg.id, pkg.version],
|
queryKey: ['gov', 'process-report', pkg.id, pkg.version],
|
||||||
@@ -436,7 +403,6 @@ function FlowLab({
|
|||||||
|
|
||||||
const kindLabel = (n: FlowNode) =>
|
const kindLabel = (n: FlowNode) =>
|
||||||
n.kind === 'activity' ? (n.flavor === 'approval' ? '审批点' : '活动点') : n.kind === 'start' ? '调用接口' : '显式终点';
|
n.kind === 'activity' ? (n.flavor === 'approval' ? '审批点' : '活动点') : n.kind === 'start' ? '调用接口' : '显式终点';
|
||||||
const kindTone = (n: FlowNode) => (n.kind === 'activity' ? (n.flavor === 'approval' ? 'warn' : 'auto') : 'mute');
|
|
||||||
|
|
||||||
// 画布上的选中态:diagram-js 给每个形状挂了 data-element-id,外挂一个类,不改画布件
|
// 画布上的选中态:diagram-js 给每个形状挂了 data-element-id,外挂一个类,不改画布件
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -477,6 +443,34 @@ function FlowLab({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 删一版:生效版与在途钉版都会被服务端挡回来(409),判词照抄 */
|
||||||
|
const dropVersion = useMutation({
|
||||||
|
mutationFn: (version: number) =>
|
||||||
|
call<{ packages: ProcessPackage[] }>(deleteProcessVersionRoute, {
|
||||||
|
params: { id: pkg.id, version: String(version) },
|
||||||
|
}),
|
||||||
|
onSuccess: (out) => {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ['gov', 'process'] });
|
||||||
|
setAsk(undefined);
|
||||||
|
// 删掉的正是在看的这一版 → 落到还剩的最新一版;一版不剩就回货架
|
||||||
|
const rest = out.packages;
|
||||||
|
if (rest.length === 0) nav.jump('workflows', 'pkgs');
|
||||||
|
else if (rest[0] !== undefined) onSaved(rest[0].version);
|
||||||
|
},
|
||||||
|
onError: (e: unknown) => setSaveErr((e as { message?: string }).message ?? '删不掉'),
|
||||||
|
});
|
||||||
|
|
||||||
|
/** 改名:名字是这条流的身份 —— 一次改及于所有版本 */
|
||||||
|
const rename = useMutation({
|
||||||
|
mutationFn: (name: string) =>
|
||||||
|
call<{ packages: ProcessPackage[] }>(renameProcessRoute, { params: { id: pkg.id }, body: { name } }),
|
||||||
|
onSuccess: () => {
|
||||||
|
void queryClient.invalidateQueries({ queryKey: ['gov', 'process'] });
|
||||||
|
setRenaming(false);
|
||||||
|
},
|
||||||
|
onError: (e: unknown) => setSaveErr((e as { message?: string }).message ?? '改名失败'),
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 新建版本 = **以当前看的版本为底克隆一版**(服务端定版号,**不生效**)。
|
* 新建版本 = **以当前看的版本为底克隆一版**(服务端定版号,**不生效**)。
|
||||||
* 与「激活」分工明确:激活只是把生效指针切过去,不产生新版本;
|
* 与「激活」分工明确:激活只是把生效指针切过去,不产生新版本;
|
||||||
@@ -538,6 +532,23 @@ function FlowLab({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const askDrop = (v: ProcessPackage) =>
|
||||||
|
setAsk({
|
||||||
|
danger: true,
|
||||||
|
spec: {
|
||||||
|
title: `删除 v${v.version}:${pkg.name}`,
|
||||||
|
note: (
|
||||||
|
<Note tone="warn">
|
||||||
|
版本删了**不可撤销**。生效版与**在途钉版**删不掉 —— 后者那些单要按这一版跑完,
|
||||||
|
定义没了就再也推不动。
|
||||||
|
</Note>
|
||||||
|
),
|
||||||
|
impact: { who: '这一版的定义不再存在', undo: '不可撤销', when: '确认即删' },
|
||||||
|
okLabel: `删掉 v${v.version}`,
|
||||||
|
onOk: () => dropVersion.mutate(v.version),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const askRetire = (v: ProcessPackage) =>
|
const askRetire = (v: ProcessPackage) =>
|
||||||
setAsk({
|
setAsk({
|
||||||
danger: true,
|
danger: true,
|
||||||
@@ -652,12 +663,12 @@ function FlowLab({
|
|||||||
{/* 编排工作区:左 1/3 版本列表 + 右 2/3 画布 —— 版本收在流程内部,不平铺台面 */}
|
{/* 编排工作区:左 1/3 版本列表 + 右 2/3 画布 —— 版本收在流程内部,不平铺台面 */}
|
||||||
<Sec ico="route" title="编排工作区">
|
<Sec ico="route" title="编排工作区">
|
||||||
<div className="g-flowlab">
|
<div className="g-flowlab">
|
||||||
<aside className="g-vercard">
|
{/* 左栏窄:一版一行只放「v几 + 状态」;动作(激活/停用/改名/删除)只在选中行展开 */}
|
||||||
{/* 题头与右侧图题成对:「流程包 · 版本」对「流程图 · 顺序」;新建与编辑同款芯片 */}
|
<aside className="g-vercard g-verslim">
|
||||||
<div className="g-vercard-h">
|
<div className="g-vercard-h">
|
||||||
流程包 · 版本
|
版本
|
||||||
<button className="g-ver-new" onClick={askClone} disabled={cloneVersion.isPending}>
|
<button className="g-ver-new" onClick={askClone} disabled={cloneVersion.isPending} title="以当前版为底克隆一版">
|
||||||
<Ico name="plus" s={11} /> 新建
|
<Ico name="plus" s={11} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="g-vercard-list">
|
<div className="g-vercard-list">
|
||||||
@@ -665,52 +676,64 @@ function FlowLab({
|
|||||||
const isActive = v.version === activeVersion;
|
const isActive = v.version === activeVersion;
|
||||||
const isShown = v.version === pkg.version;
|
const isShown = v.version === pkg.version;
|
||||||
return (
|
return (
|
||||||
<div
|
<div key={v.version} className={'g-verrow' + (isShown ? ' on' : '') + (isActive ? ' live' : '')}>
|
||||||
key={v.version}
|
<button className="g-verrow-h" onClick={() => onPick(v.version)}>
|
||||||
className={'g-ver' + (isShown ? ' on' : '') + (isActive ? ' live' : '')}
|
<b>v{v.version}</b>
|
||||||
role="button"
|
<span className={'st' + (isActive ? ' live' : '')}>{isActive ? '生效' : '停用'}</span>
|
||||||
tabIndex={0}
|
|
||||||
onClick={() => onPick(v.version)}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter') onPick(v.version);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<b>v{v.version}</b>
|
|
||||||
<span className={'st' + (isActive ? ' live' : '')}>{isActive ? '生效中' : '停用'}</span>
|
|
||||||
<span className="m">
|
|
||||||
<span className="cnt">
|
<span className="cnt">
|
||||||
{v.nodes.length}节点·{v.edges.length}边
|
{v.nodes.length}点·{v.edges.length}边
|
||||||
</span>
|
</span>
|
||||||
{isActive ? (
|
</button>
|
||||||
<button
|
{isShown && (
|
||||||
className="g-ver-act"
|
/* 动作只在**看着的那一版**上出现:窄栏放不下每行一排钮,也不该让人误点别版 */
|
||||||
onClick={(e) => {
|
<div className="g-verrow-a">
|
||||||
e.stopPropagation();
|
{isActive ? (
|
||||||
askRetire(v);
|
<button className="g-ver-act" onClick={() => askRetire(v)}>
|
||||||
}}
|
停用
|
||||||
>
|
</button>
|
||||||
停用
|
) : (
|
||||||
|
<button
|
||||||
|
className="g-ver-act t-go"
|
||||||
|
/* 不闭合点不了:结构闭合 + 活动装载两面清零才亮 */
|
||||||
|
disabled={closedOf(v.version) !== true}
|
||||||
|
title={closedOf(v.version) === true ? `激活 v${v.version}` : '还不闭合 —— 属性面会列出差哪几条'}
|
||||||
|
onClick={() => askActivate(v)}
|
||||||
|
>
|
||||||
|
激活
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button className="g-ver-act" onClick={() => setRenaming(true)} title="改这条流的名字(所有版本同名)">
|
||||||
|
改名
|
||||||
</button>
|
</button>
|
||||||
) : (
|
|
||||||
<button
|
<button
|
||||||
className="g-ver-act t-go"
|
className="g-ver-act t-bad"
|
||||||
/* 不闭合点不了:异常清零才亮 —— 完整性闸在激活,不在画图 */
|
disabled={isActive || dropVersion.isPending}
|
||||||
disabled={closedOf(v.version) !== true}
|
title={isActive ? '生效版删不掉 —— 先停用' : `删掉 v${v.version}`}
|
||||||
title={closedOf(v.version) === true ? `激活 v${v.version}` : '有异常,先看异常看板'}
|
onClick={() => askDrop(v)}
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
askActivate(v);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
激活
|
删除
|
||||||
</button>
|
</button>
|
||||||
)}
|
</div>
|
||||||
</span>
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="g-vercard-f">恰一个生效版本;在途工作流按启动时的版本跑完,启停与新建都不影响它们。</div>
|
{renaming && (
|
||||||
|
<div className="g-verrename">
|
||||||
|
<input
|
||||||
|
className="g-input"
|
||||||
|
autoFocus
|
||||||
|
defaultValue={pkg.name}
|
||||||
|
placeholder="流程名"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') rename.mutate((e.target as HTMLInputElement).value.trim());
|
||||||
|
if (e.key === 'Escape') setRenaming(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="t">回车改名 · Esc 取消 —— 名字是这条流的身份,各版本同名</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<div className="g-canvas" ref={canvasRef}>
|
<div className="g-canvas" ref={canvasRef}>
|
||||||
@@ -750,90 +773,10 @@ function FlowLab({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
* 左下:异常看板 —— 与版本卡同列同风(图纸式)。
|
* 右栏:节点属性。外面这层包壳是**为了不撑高整行** —— 卡绝对定位撑满行高,
|
||||||
* 画图不检查,这里**替**你盯着:哪个节点缺什么,点一条定位到属性去补;
|
* 行高由中栏的图定;属性长了在卡内滚,不把图拉长(用户要的「卡片高度跟图一致」)。
|
||||||
* 清零了激活按钮才亮 —— 完整性闸在激活,不在画图。
|
|
||||||
*/}
|
*/}
|
||||||
<aside className="g-vercard g-exboard">
|
<div className="g-nodewrap">
|
||||||
<div className="g-vercard-h">激活 · 双判据</div>
|
|
||||||
{activeReport === undefined ? (
|
|
||||||
<div className="g-vercard-list g-exsheet">
|
|
||||||
<div className="g-exboard-empty">校验中…</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
(() => {
|
|
||||||
/* 能不能激活看两面:①图编排闭合完善 ②活动装载。unknown-activity 归装载,其余归编排。 */
|
|
||||||
const loading = activeReport.violations.filter((v) => v.kind === 'unknown-activity');
|
|
||||||
const drawing = activeReport.violations.filter((v) => v.kind !== 'unknown-activity');
|
|
||||||
const row = (v: (typeof activeReport.violations)[number], i: number) => {
|
|
||||||
const nid = v.nodeId ?? pkg.edges.find((e) => e.id === v.edgeId)?.to;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`${v.kind}-${i}`}
|
|
||||||
className="g-ver g-ex"
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
onClick={() => {
|
|
||||||
if (nid !== undefined) setSel(String(nid));
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' && nid !== undefined) setSel(String(nid));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{nid !== undefined && <b>{String(nid)}</b>}
|
|
||||||
<span className="msg">{v.message}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
/* 检验单式:两格判据面均分卡高,面体列违规行;全清的面盖检验章,两面全清出裁定带 */
|
|
||||||
return (
|
|
||||||
<div className="g-vercard-list g-exsheet">
|
|
||||||
<section className="g-exface">
|
|
||||||
<div className={'g-exfacet' + (drawing.length === 0 ? ' ok' : '')}>
|
|
||||||
<Ico name={drawing.length === 0 ? 'circle-check' : 'triangle-alert'} s={12} />
|
|
||||||
判据一 · 图编排
|
|
||||||
<span className="st">{drawing.length === 0 ? '闭合' : `${drawing.length} 项待补`}</span>
|
|
||||||
</div>
|
|
||||||
<div className="g-exface-b">
|
|
||||||
{drawing.length === 0 ? (
|
|
||||||
<div className="g-exstamp">
|
|
||||||
编排闭合<small>CHK·1 — GRAPH CLOSED</small>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
drawing.map(row)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section className="g-exface">
|
|
||||||
<div className={'g-exfacet' + (loading.length === 0 ? ' ok' : '')}>
|
|
||||||
<Ico name={loading.length === 0 ? 'circle-check' : 'triangle-alert'} s={12} />
|
|
||||||
判据二 · 活动装载
|
|
||||||
<span className="st">{loading.length === 0 ? '齐' : `${loading.length} 个未链接实现`}</span>
|
|
||||||
</div>
|
|
||||||
<div className="g-exface-b">
|
|
||||||
{loading.length === 0 ? (
|
|
||||||
<div className="g-exstamp">
|
|
||||||
装载齐备<small>CHK·2 — IMPL LINKED</small>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
loading.map(row)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{activeReport.violations.length === 0 && (
|
|
||||||
<div className="g-exverdict">
|
|
||||||
两面都齐 —— v{pkg.version} 可运行。
|
|
||||||
{pkg.version === activeVersion ? '这一版正在生效。' : '版本行的「激活」已亮,点它即可上线。'}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()
|
|
||||||
)}
|
|
||||||
<div className="g-vercard-f">点一条定位到属性去补;两面清零,激活才亮。</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
{/* 右下:节点属性(图纸卡,与流程图同宽)—— 闭环(岗位/判据/映射)在这里决定 */}
|
|
||||||
<div className="g-vercard g-nodecard">
|
<div className="g-vercard g-nodecard">
|
||||||
<div className="g-vercard-h">
|
<div className="g-vercard-h">
|
||||||
节点 · 属性
|
节点 · 属性
|
||||||
@@ -855,39 +798,53 @@ function FlowLab({
|
|||||||
{saveErr}
|
{saveErr}
|
||||||
</Note>
|
</Note>
|
||||||
)}
|
)}
|
||||||
{/* 索引栏 = 图框分度带:节点行 + 边行,行首竖签;固定不滚,属性区在下面自己滚 */}
|
|
||||||
<div className="g-pickrail">
|
|
||||||
<div className="g-pickrow">
|
|
||||||
<span className="g-picklbl">
|
|
||||||
节点<b>{nodes.filter((n) => n.kind !== 'start' && n.kind !== 'end' && n.kind !== 'end-bad').length}</b>
|
|
||||||
</span>
|
|
||||||
<div className="g-picker">
|
|
||||||
{nodes
|
|
||||||
.filter((n) => n.kind !== 'start' && n.kind !== 'end' && n.kind !== 'end-bad')
|
|
||||||
.map((n) => (
|
|
||||||
<button
|
|
||||||
key={n.id}
|
|
||||||
className={`g-pick t-${kindTone(n)}` + (sel === n.id ? ' on' : '')}
|
|
||||||
onClick={() => setSel(sel === n.id ? null : n.id)}
|
|
||||||
>
|
|
||||||
<b className="g-mono">{n.id}</b>
|
|
||||||
{n.label}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* key 随选中对象换 —— 换一件看,明细区滚回顶,不带上一件的滚动位 */}
|
{/* key 随选中对象换 —— 换一件看,明细区滚回顶,不带上一件的滚动位 */}
|
||||||
<div className="g-propsheet" key={node !== null ? `n:${node.id}` : 'blank'}>
|
<div className="g-propsheet" key={node !== null ? `n:${node.id}` : 'blank'}>
|
||||||
{node === null ? (
|
{node === null ? (
|
||||||
|
/*
|
||||||
|
* 没选节点时,这块地方替原来的「激活判据」看板:还差哪几条就列在这儿,点一条定位过去。
|
||||||
|
* 激活闸只剩两面 —— **结构闭合**与**活动装载**;字段一概不查(只声明,不要求)。
|
||||||
|
*/
|
||||||
<div className="g-sheetempty">
|
<div className="g-sheetempty">
|
||||||
<Ico name="target" s={20} />
|
{(activeReport?.violations.length ?? 0) === 0 ? (
|
||||||
<b>点画布或拣选条,选一个节点</b>
|
<>
|
||||||
<p>
|
<Ico name="target" s={20} />
|
||||||
节点填名称、链接实现、往哪走(出边条件)。边不是可编辑对象 —— 它只表达顺序;
|
<b>在图上点一个节点</b>
|
||||||
点画布上的线会选中发起它的那个节点。负责人运营时分配,不在这儿;
|
<p>
|
||||||
审批裁决(verdict)与放行选口(outcome)由系统注入判据,不用你声明。
|
节点填名称、链接实现、往哪走(出边条件)。边不是可编辑对象 —— 它只表达顺序;
|
||||||
</p>
|
点线会选中发起它的那个节点。负责人运营时分配,不在这儿。
|
||||||
|
</p>
|
||||||
|
{activeReport !== undefined && (
|
||||||
|
<span className="g-okstamp">
|
||||||
|
<Ico name="circle-check" s={13} />
|
||||||
|
结构闭合 · 活动装载齐备 —— v{pkg.version} 可运行
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="g-lack">
|
||||||
|
<div className="g-lack-h">
|
||||||
|
<Ico name="triangle-alert" s={13} />
|
||||||
|
还差 {activeReport!.violations.length} 条才能激活
|
||||||
|
</div>
|
||||||
|
{activeReport!.violations.map((v, i) => {
|
||||||
|
const nid = v.nodeId ?? pkg.edges.find((e) => e.id === v.edgeId)?.to;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={`${v.kind}-${i}`}
|
||||||
|
className="g-lack-r"
|
||||||
|
disabled={nid === undefined}
|
||||||
|
onClick={() => {
|
||||||
|
if (nid !== undefined) setSel(String(nid));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{nid !== undefined && <b>{String(nid)}</b>}
|
||||||
|
<span>{v.message}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -923,43 +880,39 @@ function FlowLab({
|
|||||||
|
|
||||||
{node.kind === 'activity' && node.flavor !== 'approval' && (
|
{node.kind === 'activity' && node.flavor !== 'approval' && (
|
||||||
/*
|
/*
|
||||||
* 数据接口(对齐 Mastra):边不传数据,约束力全在输入/输出字段 ——
|
* 数据接口 —— **只声明,不做任何要求**(2026-07-30 用户定):
|
||||||
* 这里不填写、只**看清**:每个输入字段运行时从哪来(起单 / 上游同名 / 补全单),
|
* 这里照着活动契约把「吃什么、吐什么」列出来,不标必填、不推来源、不校验类型。
|
||||||
* 每个输出口发什么名。要改字段,改的是活动契约,不是这张卡。
|
* 字段够不够、缺了怎么补,是**活动内实现**的事,不在编排层现形。
|
||||||
|
* 数组照实写成 `T[]` —— 声明得看得出是一个还是一串。
|
||||||
*/
|
*/
|
||||||
<Prop
|
<Prop
|
||||||
ico="git-merge"
|
ico="git-merge"
|
||||||
title="数据接口 · 按字段名流动"
|
title="数据接口 · 声明"
|
||||||
extra={refAct === undefined ? '先链接活动实现' : '契约即管道:名字对上,数据就到'}
|
extra={refAct === undefined ? '先链接活动实现' : '只声明,不做任何要求'}
|
||||||
>
|
>
|
||||||
{refAct === undefined ? (
|
{refAct === undefined ? (
|
||||||
<Note>链接活动实现后,这里列出它吃什么、吐什么、每个字段从哪来。</Note>
|
<Note>链接活动实现后,这里照它的契约列出吃什么、吐什么。</Note>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="g-dif">
|
<div className="g-dif">
|
||||||
<em>输入 · {refAct.contract.inputFields.length} 字段</em>
|
<em>输入 · {refAct.contract.inputFields.length} 字段</em>
|
||||||
{refAct.contract.inputFields.map((f) => {
|
{refAct.contract.inputFields.map((f) => (
|
||||||
const src = fieldSources.get(f.name) ?? '补全单';
|
<div key={f.name} className="g-dif-r">
|
||||||
return (
|
<span className="k">{f.name}</span>
|
||||||
<div key={f.name} className="g-dif-r">
|
<span className="t">{f.type}</span>
|
||||||
<span className="k">
|
</div>
|
||||||
{f.name}
|
))}
|
||||||
{f.required && <i>必填</i>}
|
|
||||||
</span>
|
|
||||||
<span className="t">{f.type}</span>
|
|
||||||
<span className={'s' + (src === '补全单' ? ' ask' : '')}>
|
|
||||||
{src === '补全单' ? '← 补全单(受理归一找人收)' : `← ${src}`}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="g-dif">
|
<div className="g-dif">
|
||||||
<em>输出 · {refAct.contract.outputPorts.length} 口</em>
|
<em>输出 · {refAct.contract.outputPorts.length} 口</em>
|
||||||
{refAct.contract.outputPorts.map((port) => (
|
{refAct.contract.outputPorts.map((port) => (
|
||||||
<div key={port.name} className="g-dif-r">
|
<div key={port.name} className="g-dif-r">
|
||||||
<span className="k">{port.name}</span>
|
<span className="k">{port.name}</span>
|
||||||
<span className="f">{port.fields.length > 0 ? port.fields.join(' · ') : '(整包事实)'}</span>
|
<span className="f">
|
||||||
|
{port.fields.length > 0
|
||||||
|
? port.fields.map((f) => `${f.name} ${f.type}`).join(' · ')
|
||||||
|
: '(整包事实)'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -1094,6 +1047,7 @@ function FlowLab({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Sec>
|
</Sec>
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -63,7 +63,7 @@
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **蓝图** | 全局 —— 定义完了没有 | 四库因果链 + 待补清单(见上) |
|
| **蓝图** | 全局 —— 定义完了没有 | 四库因果链 + 待补清单(见上) |
|
||||||
| **触发器** | 谁调起流程 | 事件 / 登记 / 周期三类调起规则的定义;定完就地投一封信演练,看哪条规则接住(归属解析 → 三级消费的真链路)。**启停开关在运营中心** |
|
| **触发器** | 谁调起流程 | 事件 / 登记 / 周期三类调起规则的定义;定完就地投一封信演练,看哪条规则接住(归属解析 → 三级消费的真链路)。**启停开关在运营中心** |
|
||||||
| **流程包** | 战略 —— 企业以哪些流转运转 | 编排工作区(画布 + 属性卡)、版本谱系、导出依赖文件。**边只表达顺序与条件**(对齐 Mastra):数据按输入/输出字段名流动,约束力在两端活动契约上;节点属性卡的「数据接口」列出每个输入字段运行时从哪来(起单 / 上游同名 / 补全单) |
|
| **流程包** | 战略 —— 企业以哪些流转运转 | 编排工作区**三栏**:左窄轨版本(激活/停用 · 改名 · 删除)、中流程图、右节点属性。**边只表达顺序与条件**(对齐 Mastra),数据按字段名流动;**字段只声明,不做任何要求** —— 激活闸只剩结构闭合与活动装载两面,字段够不够是活动自己的事 |
|
||||||
| **活动库** | 方法 —— 每件事怎么干 | 货架、契约面、范式、资产版本谱系、安装两审 |
|
| **活动库** | 方法 —— 每件事怎么干 | 货架、契约面、范式、资产版本谱系、安装两审 |
|
||||||
| **工具箱** | 能力 —— 系统能对世界做什么 | 功能 / LLM / 人类三类工具目录、白名单授权、幂等口径、引用反查。**一律主显示中文名**,逻辑名退居副标 |
|
| **工具箱** | 能力 —— 系统能对世界做什么 | 功能 / LLM / 人类三类工具目录、白名单授权、幂等口径、引用反查。**一律主显示中文名**,逻辑名退居副标 |
|
||||||
| **数据域** | 对象 —— 企业运营什么 | 域与对象 schema、字段、放行规则、被谁引用。**行数据不在这里** —— 那是运营的事 |
|
| **数据域** | 对象 —— 企业运营什么 | 域与对象 schema、字段、放行规则、被谁引用。**行数据不在这里** —— 那是运营的事 |
|
||||||
|
|||||||
@@ -226,6 +226,30 @@ export const setProcessStateRoute = {
|
|||||||
output: z.object({ packages: z.array(processPackageSchema) }),
|
output: z.object({ packages: z.array(processPackageSchema) }),
|
||||||
} as const satisfies RouteContract;
|
} as const satisfies RouteContract;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删一版 —— 两道闸(2026-07-30):
|
||||||
|
* · **生效版不可删**:先停用,免得手滑把正在起单的定义删了;
|
||||||
|
* · **在途钉版不可删**:那些单要按它跑完,定义没了就再也推不动。
|
||||||
|
*/
|
||||||
|
export const deleteProcessVersionRoute = {
|
||||||
|
method: 'DELETE',
|
||||||
|
path: '/api/gov/process/:id/:version',
|
||||||
|
params: z.object({ id: z.string(), version: z.string() }),
|
||||||
|
output: z.object({ packages: z.array(processPackageSchema) }),
|
||||||
|
} as const satisfies RouteContract;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 改名 —— 名字是**这条流的身份**,不是某一版的属性:一次改及于它的所有版本。
|
||||||
|
* (版本列表上只显示 v1/v2…,若只改一版的名,列表上什么也看不见,人会以为没生效。)
|
||||||
|
*/
|
||||||
|
export const renameProcessRoute = {
|
||||||
|
method: 'POST',
|
||||||
|
path: '/api/gov/process/:id/rename',
|
||||||
|
params: z.object({ id: z.string() }),
|
||||||
|
body: z.object({ name: z.string().min(1).max(40) }),
|
||||||
|
output: z.object({ packages: z.array(processPackageSchema) }),
|
||||||
|
} as const satisfies RouteContract;
|
||||||
|
|
||||||
/** 画图即校验 —— 与前端画布同源;保存前随手看还差几条,不改任何状态 */
|
/** 画图即校验 —— 与前端画布同源;保存前随手看还差几条,不改任何状态 */
|
||||||
|
|
||||||
export const validateProcessRoute = {
|
export const validateProcessRoute = {
|
||||||
@@ -388,7 +412,14 @@ export const listActivitiesRoute = {
|
|||||||
contract: z.object({
|
contract: z.object({
|
||||||
requiredInputs: z.array(z.string()),
|
requiredInputs: z.array(z.string()),
|
||||||
inputFields: z.array(z.object({ name: z.string(), type: z.string(), required: z.boolean() })),
|
inputFields: z.array(z.object({ name: z.string(), type: z.string(), required: z.boolean() })),
|
||||||
outputPorts: z.array(z.object({ name: z.string(), description: z.string().optional(), fields: z.array(z.string()) })),
|
outputPorts: z.array(
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
/** 字段名 + 类型;**数组写成 `T[]`**(如 `object[]`)—— 声明得看得出是不是一串 */
|
||||||
|
fields: z.array(z.object({ name: z.string(), type: z.string() })),
|
||||||
|
}),
|
||||||
|
),
|
||||||
tools: z.array(z.string()),
|
tools: z.array(z.string()),
|
||||||
models: z.array(z.string()),
|
models: z.array(z.string()),
|
||||||
objects: z.array(z.string()),
|
objects: z.array(z.string()),
|
||||||
|
|||||||
@@ -232,14 +232,15 @@ export type ProcessPackage = z.infer<typeof processPackageSchema>;
|
|||||||
|
|
||||||
/* ── 编排校验的报告形状(前端画布即时预览与后端裁决同源) ───────────── */
|
/* ── 编排校验的报告形状(前端画布即时预览与后端裁决同源) ───────────── */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 激活闸只有两面(2026-07-30 用户定「字段只声明,不做任何要求」):
|
||||||
|
* **结构闭合**(dangling-node / multi-default-edge)与**活动装载**(unknown-activity)。
|
||||||
|
* 字段类的检查全部退役 —— 要什么、够不够、类型对不对归活动自己,不在编排层现形。
|
||||||
|
*/
|
||||||
export const CLOSURE_VIOLATION_KINDS = [
|
export const CLOSURE_VIOLATION_KINDS = [
|
||||||
/** 相邻两活动同名字段类型相抵 —— schema 咬合是唯一的数据约束,名同型异必坏 */
|
|
||||||
'type-conflict',
|
|
||||||
'dangling-node',
|
'dangling-node',
|
||||||
/** 多出边的无条件默认兜底超过一条 —— 第二条默认永远走不到 */
|
/** 多出边的无条件默认兜底超过一条 —— 第二条默认永远走不到 */
|
||||||
'multi-default-edge',
|
'multi-default-edge',
|
||||||
/** 容忍分支的产出被下游必填依赖 —— 分支可空,依赖它的必填就会空 */
|
|
||||||
'tolerate-required-dependency',
|
|
||||||
'unknown-activity',
|
'unknown-activity',
|
||||||
] as const;
|
] as const;
|
||||||
export const closureViolationKindSchema = z.enum(CLOSURE_VIOLATION_KINDS);
|
export const closureViolationKindSchema = z.enum(CLOSURE_VIOLATION_KINDS);
|
||||||
|
|||||||
Reference in New Issue
Block a user