@gadmin2n/schematics 0.0.152 → 0.0.153
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/application/files/gadmin2-game-angle-demo/CODEBUDDY.local.md +8 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/.seed-timestamp +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/index.ts +2 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permission.seed.ts +23 -44
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +25 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow.seed.ts +74 -64
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/temporal.service.ts +0 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-cancellation.spec.ts +17 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.spec.ts +58 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-compiler.service.ts +66 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution-idempotency.spec.ts +61 -149
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution.dto.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-runtime.controller.spec.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle-runtime.controller.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-lifecycle.service.spec.ts +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.spec.ts +38 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +18 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.dto.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +131 -242
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.test.ts +7 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +6 -36
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/hooks/useDynamicResources.tsx +0 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/workflow.json +29 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/workflow.json +29 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/people-groups/index.tsx +20 -17
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/person-directory/index.tsx +38 -35
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/detail.tsx +6 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/business-events/index.tsx +24 -22
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/EnhancedFlowRenderer.tsx +0 -11
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/StatusCards.tsx +24 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/connections/index.tsx +20 -18
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/index.tsx +7 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +28 -79
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instances.tsx +5 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/components/NodeInstanceForm.tsx +0 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/DbExecuteConfigEditor.tsx +227 -97
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/widgets/SimplifiedNodeEditors.test.tsx +62 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/settings/index.tsx +73 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +21 -46
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/newNodeConfig.ts +1 -0
- package/package.json +1 -1
|
@@ -19,11 +19,7 @@ import {
|
|
|
19
19
|
scopedIdempotencyKey,
|
|
20
20
|
workflowDataHash,
|
|
21
21
|
} from './manual-workflow-execution';
|
|
22
|
-
import {
|
|
23
|
-
TemporalService,
|
|
24
|
-
WorkflowRetryCheckpoint,
|
|
25
|
-
workflowTemporalId,
|
|
26
|
-
} from './temporal.service';
|
|
22
|
+
import { TemporalService, workflowTemporalId } from './temporal.service';
|
|
27
23
|
import { verifyWebhookSignature } from './webhook-signature.util';
|
|
28
24
|
import { describeCallableWorkflow } from './workflow-callable';
|
|
29
25
|
import { WorkflowCompilerService } from './workflow-compiler.service';
|
|
@@ -97,19 +93,22 @@ type CronSchedulePlan =
|
|
|
97
93
|
version: { id: bigint; dsl: unknown };
|
|
98
94
|
};
|
|
99
95
|
const WORKFLOW_PUBLISH_LOCK_KEY = BigInt(734_221_907);
|
|
100
|
-
const RETRYABLE_MAIN_FLOW_NODE_TYPES = new Set([
|
|
101
|
-
'http_request',
|
|
102
|
-
'db_execute',
|
|
103
|
-
'code',
|
|
104
|
-
'delay',
|
|
105
|
-
'sub_workflow',
|
|
106
|
-
]);
|
|
107
|
-
|
|
108
96
|
interface WorkflowExecutionInternalOptions {
|
|
109
97
|
operationScopeId?: string;
|
|
110
|
-
retryCheckpoint?: WorkflowRetryCheckpoint;
|
|
111
98
|
retryOfInstanceId?: bigint;
|
|
112
|
-
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type WorkflowServiceOutcome<T> =
|
|
102
|
+
| { kind: 'ok'; value: T }
|
|
103
|
+
| { kind: 'not_found'; message: string }
|
|
104
|
+
| { kind: 'conflict'; message: string }
|
|
105
|
+
| { kind: 'invalid_input'; message: string };
|
|
106
|
+
|
|
107
|
+
export interface WorkflowExecutionResult {
|
|
108
|
+
instanceId: string;
|
|
109
|
+
temporalRunId: string | null;
|
|
110
|
+
workflowVersionId: string;
|
|
111
|
+
status: string;
|
|
113
112
|
}
|
|
114
113
|
|
|
115
114
|
function dslUsesBusinessEvent(dsl: unknown, registrationCode: string): boolean {
|
|
@@ -644,6 +643,7 @@ export class WorkflowService {
|
|
|
644
643
|
{
|
|
645
644
|
pending: number;
|
|
646
645
|
running: number;
|
|
646
|
+
waiting_retry: number;
|
|
647
647
|
completed: number;
|
|
648
648
|
failed: number;
|
|
649
649
|
cancelled: number;
|
|
@@ -654,6 +654,7 @@ export class WorkflowService {
|
|
|
654
654
|
runsMap.set(row.workflowId, {
|
|
655
655
|
pending: 0,
|
|
656
656
|
running: 0,
|
|
657
|
+
waiting_retry: 0,
|
|
657
658
|
completed: 0,
|
|
658
659
|
failed: 0,
|
|
659
660
|
cancelled: 0,
|
|
@@ -681,6 +682,7 @@ export class WorkflowService {
|
|
|
681
682
|
runs: runsMap.get(item.id) || {
|
|
682
683
|
pending: 0,
|
|
683
684
|
running: 0,
|
|
685
|
+
waiting_retry: 0,
|
|
684
686
|
completed: 0,
|
|
685
687
|
failed: 0,
|
|
686
688
|
cancelled: 0,
|
|
@@ -1407,7 +1409,7 @@ export class WorkflowService {
|
|
|
1407
1409
|
const activeInstances = await tx.workflowInstance.count({
|
|
1408
1410
|
where: {
|
|
1409
1411
|
workflowId,
|
|
1410
|
-
status: { in: ['PENDING', 'RUNNING'] },
|
|
1412
|
+
status: { in: ['PENDING', 'RUNNING', 'WAITING_RETRY'] },
|
|
1411
1413
|
},
|
|
1412
1414
|
});
|
|
1413
1415
|
if (activeInstances > 0)
|
|
@@ -1727,19 +1729,24 @@ export class WorkflowService {
|
|
|
1727
1729
|
}
|
|
1728
1730
|
|
|
1729
1731
|
async getStats(workflowId: bigint) {
|
|
1730
|
-
const [running, failed, completed, total] = await Promise.all(
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1732
|
+
const [running, waitingRetry, failed, completed, total] = await Promise.all(
|
|
1733
|
+
[
|
|
1734
|
+
this.prisma.workflowInstance.count({
|
|
1735
|
+
where: { workflowId, status: 'RUNNING' },
|
|
1736
|
+
}),
|
|
1737
|
+
this.prisma.workflowInstance.count({
|
|
1738
|
+
where: { workflowId, status: 'WAITING_RETRY' },
|
|
1739
|
+
}),
|
|
1740
|
+
this.prisma.workflowInstance.count({
|
|
1741
|
+
where: { workflowId, status: 'FAILED' },
|
|
1742
|
+
}),
|
|
1743
|
+
this.prisma.workflowInstance.count({
|
|
1744
|
+
where: { workflowId, status: 'COMPLETED' },
|
|
1745
|
+
}),
|
|
1746
|
+
this.prisma.workflowInstance.count({ where: { workflowId } }),
|
|
1747
|
+
],
|
|
1748
|
+
);
|
|
1749
|
+
return { running, waitingRetry, failed, completed, total };
|
|
1743
1750
|
}
|
|
1744
1751
|
|
|
1745
1752
|
async getWorkflowStatsByCode(code: string) {
|
|
@@ -1764,6 +1771,7 @@ export class WorkflowService {
|
|
|
1764
1771
|
failed24h,
|
|
1765
1772
|
running,
|
|
1766
1773
|
pending,
|
|
1774
|
+
waitingRetry,
|
|
1767
1775
|
] = await Promise.all([
|
|
1768
1776
|
this.prisma.workflowInstance.count(),
|
|
1769
1777
|
this.prisma.workflowInstance.count({
|
|
@@ -1779,6 +1787,9 @@ export class WorkflowService {
|
|
|
1779
1787
|
}),
|
|
1780
1788
|
this.prisma.workflowInstance.count({ where: { status: 'RUNNING' } }),
|
|
1781
1789
|
this.prisma.workflowInstance.count({ where: { status: 'PENDING' } }),
|
|
1790
|
+
this.prisma.workflowInstance.count({
|
|
1791
|
+
where: { status: 'WAITING_RETRY' },
|
|
1792
|
+
}),
|
|
1782
1793
|
]);
|
|
1783
1794
|
|
|
1784
1795
|
// Success rate calculation
|
|
@@ -1828,6 +1839,7 @@ export class WorkflowService {
|
|
|
1828
1839
|
},
|
|
1829
1840
|
running,
|
|
1830
1841
|
pending,
|
|
1842
|
+
waitingRetry,
|
|
1831
1843
|
};
|
|
1832
1844
|
}
|
|
1833
1845
|
|
|
@@ -1902,7 +1914,6 @@ export class WorkflowService {
|
|
|
1902
1914
|
const executionContext = createWorkflowExecutionState(
|
|
1903
1915
|
dto.workflow,
|
|
1904
1916
|
internal?.operationScopeId,
|
|
1905
|
-
internal?.retryCheckpoint,
|
|
1906
1917
|
);
|
|
1907
1918
|
const idempotencyRequestHash = dto.idempotencyKey
|
|
1908
1919
|
? workflowDataHash(
|
|
@@ -1938,7 +1949,6 @@ export class WorkflowService {
|
|
|
1938
1949
|
}),
|
|
1939
1950
|
...(internal?.retryOfInstanceId && {
|
|
1940
1951
|
retryOfInstanceId: internal.retryOfInstanceId,
|
|
1941
|
-
retryFromNodeId: internal.retryFromNodeId,
|
|
1942
1952
|
}),
|
|
1943
1953
|
creator,
|
|
1944
1954
|
},
|
|
@@ -2011,7 +2021,6 @@ export class WorkflowService {
|
|
|
2011
2021
|
versionId: Number(instanceVersion.id),
|
|
2012
2022
|
dsl: instanceVersion.dsl,
|
|
2013
2023
|
context: instance.context as Record<string, any>,
|
|
2014
|
-
retryCheckpoint: readRetryCheckpoint(instance.context),
|
|
2015
2024
|
});
|
|
2016
2025
|
} catch (error) {
|
|
2017
2026
|
await this.workflowLifecycle.markFailed({
|
|
@@ -2250,7 +2259,6 @@ export class WorkflowService {
|
|
|
2250
2259
|
select: {
|
|
2251
2260
|
id: true,
|
|
2252
2261
|
status: true,
|
|
2253
|
-
retryFromNodeId: true,
|
|
2254
2262
|
createdAt: true,
|
|
2255
2263
|
},
|
|
2256
2264
|
}),
|
|
@@ -2271,16 +2279,10 @@ export class WorkflowService {
|
|
|
2271
2279
|
workflow: { name: instance.workflow.name },
|
|
2272
2280
|
approvableNodeIds: approvableRequests.map((request) => request.nodeId),
|
|
2273
2281
|
retryableNodeIds:
|
|
2274
|
-
instance.status === '
|
|
2275
|
-
?
|
|
2276
|
-
dsl,
|
|
2277
|
-
instance.nodeExecutions,
|
|
2278
|
-
readRetryCheckpoint(instance.context),
|
|
2279
|
-
)
|
|
2282
|
+
instance.status === 'WAITING_RETRY' && dsl
|
|
2283
|
+
? listSameRunRetryableNodeIds(dsl, instance.nodeExecutions)
|
|
2280
2284
|
: [],
|
|
2281
2285
|
retryAttempts,
|
|
2282
|
-
reusedNodeIds:
|
|
2283
|
-
readRetryCheckpoint(instance.context)?.completedNodeIds ?? [],
|
|
2284
2286
|
dsl,
|
|
2285
2287
|
};
|
|
2286
2288
|
}
|
|
@@ -2306,7 +2308,7 @@ export class WorkflowService {
|
|
|
2306
2308
|
...(Object.prototype.hasOwnProperty.call(result, 'output') && {
|
|
2307
2309
|
result: result.output,
|
|
2308
2310
|
}),
|
|
2309
|
-
...(instance.status
|
|
2311
|
+
...(['FAILED', 'WAITING_RETRY'].includes(instance.status) && {
|
|
2310
2312
|
error: {
|
|
2311
2313
|
...(typeof result.code === 'string' ? { code: result.code } : {}),
|
|
2312
2314
|
message:
|
|
@@ -2347,10 +2349,10 @@ export class WorkflowService {
|
|
|
2347
2349
|
if (!instance) throw new NotFoundException('Workflow instance not found');
|
|
2348
2350
|
if (
|
|
2349
2351
|
instance.status !== 'CANCELLED' &&
|
|
2350
|
-
!['RUNNING', 'PENDING'].includes(instance.status)
|
|
2352
|
+
!['RUNNING', 'PENDING', 'WAITING_RETRY'].includes(instance.status)
|
|
2351
2353
|
)
|
|
2352
2354
|
throw new ConflictException(
|
|
2353
|
-
'Only RUNNING or
|
|
2355
|
+
'Only RUNNING, PENDING, or WAITING_RETRY instances can be cancelled',
|
|
2354
2356
|
);
|
|
2355
2357
|
if (
|
|
2356
2358
|
instance.status !== 'CANCELLED' &&
|
|
@@ -2510,7 +2512,7 @@ export class WorkflowService {
|
|
|
2510
2512
|
|
|
2511
2513
|
const reservedInstances = await this.prisma.workflowInstance.findMany({
|
|
2512
2514
|
where: {
|
|
2513
|
-
status: { in: ['PENDING', 'RUNNING'] },
|
|
2515
|
+
status: { in: ['PENDING', 'RUNNING', 'WAITING_RETRY'] },
|
|
2514
2516
|
temporalRunId: { not: null },
|
|
2515
2517
|
updatedAt: { lte: cutoff },
|
|
2516
2518
|
},
|
|
@@ -2529,7 +2531,10 @@ export class WorkflowService {
|
|
|
2529
2531
|
instance.temporalRunId!,
|
|
2530
2532
|
);
|
|
2531
2533
|
if (temporalState.kind === 'MISSING') {
|
|
2532
|
-
if (
|
|
2534
|
+
if (
|
|
2535
|
+
instance.status === 'RUNNING' ||
|
|
2536
|
+
instance.status === 'WAITING_RETRY'
|
|
2537
|
+
) {
|
|
2533
2538
|
await this.failMissingRunningInstance(
|
|
2534
2539
|
instance.id,
|
|
2535
2540
|
instance.temporalRunId!,
|
|
@@ -2555,7 +2560,6 @@ export class WorkflowService {
|
|
|
2555
2560
|
versionId: Number(instance.versionId),
|
|
2556
2561
|
dsl: version.dsl,
|
|
2557
2562
|
context: (instance.context ?? {}) as Record<string, any>,
|
|
2558
|
-
retryCheckpoint: readRetryCheckpoint(instance.context),
|
|
2559
2563
|
});
|
|
2560
2564
|
} catch (error) {
|
|
2561
2565
|
await this.failUnstartedInstance(
|
|
@@ -2599,7 +2603,7 @@ export class WorkflowService {
|
|
|
2599
2603
|
): Promise<void> {
|
|
2600
2604
|
await this.workflowLifecycle.markFailed({
|
|
2601
2605
|
instanceId,
|
|
2602
|
-
expectedStatuses: ['RUNNING'],
|
|
2606
|
+
expectedStatuses: ['RUNNING', 'WAITING_RETRY'],
|
|
2603
2607
|
temporalRunId,
|
|
2604
2608
|
result: {
|
|
2605
2609
|
code: 'TEMPORAL_EXECUTION_MISSING',
|
|
@@ -2685,7 +2689,7 @@ export class WorkflowService {
|
|
|
2685
2689
|
if (state.status !== 'COMPLETED' && state.status !== 'CANCELLED') {
|
|
2686
2690
|
await this.workflowLifecycle.markFailed({
|
|
2687
2691
|
instanceId,
|
|
2688
|
-
expectedStatuses: ['PENDING', 'RUNNING'],
|
|
2692
|
+
expectedStatuses: ['PENDING', 'RUNNING', 'WAITING_RETRY'],
|
|
2689
2693
|
temporalRunId,
|
|
2690
2694
|
result:
|
|
2691
2695
|
state.result === undefined
|
|
@@ -2702,7 +2706,7 @@ export class WorkflowService {
|
|
|
2702
2706
|
await this.prisma.workflowInstance.updateMany({
|
|
2703
2707
|
where: {
|
|
2704
2708
|
id: instanceId,
|
|
2705
|
-
status: { in: ['PENDING', 'RUNNING'] },
|
|
2709
|
+
status: { in: ['PENDING', 'RUNNING', 'WAITING_RETRY'] },
|
|
2706
2710
|
temporalRunId,
|
|
2707
2711
|
},
|
|
2708
2712
|
data: {
|
|
@@ -2785,59 +2789,72 @@ export class WorkflowService {
|
|
|
2785
2789
|
);
|
|
2786
2790
|
}
|
|
2787
2791
|
|
|
2788
|
-
async
|
|
2792
|
+
async retryFailedNode(
|
|
2789
2793
|
instanceId: bigint,
|
|
2790
2794
|
nodeId: string,
|
|
2791
|
-
creator: string,
|
|
2792
2795
|
temporalService: TemporalService,
|
|
2793
|
-
) {
|
|
2796
|
+
): Promise<WorkflowServiceOutcome<WorkflowExecutionResult>> {
|
|
2794
2797
|
const instance = await this.prisma.workflowInstance.findUnique({
|
|
2795
2798
|
where: { id: instanceId },
|
|
2796
2799
|
include: { nodeExecutions: true },
|
|
2797
2800
|
});
|
|
2798
|
-
if (!instance)
|
|
2799
|
-
if (instance.status !== '
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
where: { retryOfInstanceId: instance.id },
|
|
2805
|
-
orderBy: { createdAt: 'desc' },
|
|
2806
|
-
});
|
|
2807
|
-
if (existingRetry) return workflowExecutionResult(existingRetry);
|
|
2801
|
+
if (!instance) return { kind: 'not_found', message: 'Instance not found' };
|
|
2802
|
+
if (instance.status !== 'WAITING_RETRY')
|
|
2803
|
+
return {
|
|
2804
|
+
kind: 'conflict',
|
|
2805
|
+
message: '只有等待重试的 Workflow 才能重试失败节点',
|
|
2806
|
+
};
|
|
2808
2807
|
|
|
2809
2808
|
const version = await this.prisma.workflowVersion.findUnique({
|
|
2810
2809
|
where: { id: instance.versionId },
|
|
2811
2810
|
select: { dsl: true },
|
|
2812
2811
|
});
|
|
2813
|
-
if (!version)
|
|
2812
|
+
if (!version)
|
|
2813
|
+
return { kind: 'not_found', message: 'Workflow version not found' };
|
|
2814
2814
|
|
|
2815
|
-
const
|
|
2815
|
+
const invalidTargetMessage = validateSameRunRetryTarget(
|
|
2816
2816
|
version.dsl,
|
|
2817
2817
|
instance.nodeExecutions,
|
|
2818
2818
|
nodeId,
|
|
2819
|
-
readRetryCheckpoint(instance.context),
|
|
2820
2819
|
);
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
},
|
|
2832
|
-
creator,
|
|
2833
|
-
temporalService,
|
|
2834
|
-
{
|
|
2835
|
-
operationScopeId: String(operationScopeId),
|
|
2836
|
-
retryCheckpoint,
|
|
2837
|
-
retryOfInstanceId: instance.id,
|
|
2838
|
-
retryFromNodeId: nodeId,
|
|
2839
|
-
},
|
|
2820
|
+
if (invalidTargetMessage)
|
|
2821
|
+
return { kind: 'invalid_input', message: invalidTargetMessage };
|
|
2822
|
+
if (!instance.temporalRunId)
|
|
2823
|
+
return {
|
|
2824
|
+
kind: 'conflict',
|
|
2825
|
+
message: '当前 Workflow 没有可继续的 Temporal Run',
|
|
2826
|
+
};
|
|
2827
|
+
const failedExecution = instance.nodeExecutions.find(
|
|
2828
|
+
(execution) =>
|
|
2829
|
+
execution.nodeId === nodeId && execution.status === 'FAILED',
|
|
2840
2830
|
);
|
|
2831
|
+
if (!failedExecution)
|
|
2832
|
+
return { kind: 'invalid_input', message: '只能重试当前失败的节点' };
|
|
2833
|
+
try {
|
|
2834
|
+
await temporalService.signalWorkflow(
|
|
2835
|
+
instance.temporalRunId,
|
|
2836
|
+
'retryNode',
|
|
2837
|
+
{
|
|
2838
|
+
nodeId,
|
|
2839
|
+
requestId: `retry-node:${
|
|
2840
|
+
instance.id
|
|
2841
|
+
}:${nodeId}:${failedExecution.updatedAt.toISOString()}`,
|
|
2842
|
+
},
|
|
2843
|
+
);
|
|
2844
|
+
} catch (error) {
|
|
2845
|
+
this.logger.error(
|
|
2846
|
+
`workflow_node_retry_signal_failed - instanceId: ${
|
|
2847
|
+
instance.id
|
|
2848
|
+
}, nodeId: ${nodeId}, error: ${
|
|
2849
|
+
error instanceof Error ? error.message : String(error)
|
|
2850
|
+
}`,
|
|
2851
|
+
);
|
|
2852
|
+
throw error;
|
|
2853
|
+
}
|
|
2854
|
+
this.logger.log(
|
|
2855
|
+
`workflow_node_retry_signal_sent - instanceId: ${instance.id}, nodeId: ${nodeId}`,
|
|
2856
|
+
);
|
|
2857
|
+
return { kind: 'ok', value: workflowExecutionResult(instance) };
|
|
2841
2858
|
}
|
|
2842
2859
|
|
|
2843
2860
|
async approveInstance(
|
|
@@ -2868,15 +2885,13 @@ export class WorkflowService {
|
|
|
2868
2885
|
function createWorkflowExecutionState(
|
|
2869
2886
|
value: Record<string, any> | undefined,
|
|
2870
2887
|
operationScopeId?: string,
|
|
2871
|
-
retryCheckpoint?: WorkflowRetryCheckpoint,
|
|
2872
2888
|
): Record<string, any> {
|
|
2873
2889
|
return {
|
|
2874
2890
|
workflow: value ?? {},
|
|
2875
2891
|
nodes: {},
|
|
2876
|
-
...(
|
|
2892
|
+
...(operationScopeId && {
|
|
2877
2893
|
system: {
|
|
2878
|
-
|
|
2879
|
-
...(retryCheckpoint && { retryCheckpoint }),
|
|
2894
|
+
operationScopeId,
|
|
2880
2895
|
},
|
|
2881
2896
|
}),
|
|
2882
2897
|
};
|
|
@@ -2887,7 +2902,7 @@ function workflowExecutionResult(instance: {
|
|
|
2887
2902
|
versionId: bigint;
|
|
2888
2903
|
status: string;
|
|
2889
2904
|
temporalRunId: string | null;
|
|
2890
|
-
}) {
|
|
2905
|
+
}): WorkflowExecutionResult {
|
|
2891
2906
|
return {
|
|
2892
2907
|
instanceId: instance.id.toString(),
|
|
2893
2908
|
temporalRunId: instance.temporalRunId,
|
|
@@ -2910,50 +2925,11 @@ function quoteLiteral(value: string): string {
|
|
|
2910
2925
|
return `'${value.replace(/'/g, "''")}'`;
|
|
2911
2926
|
}
|
|
2912
2927
|
|
|
2913
|
-
function
|
|
2914
|
-
value: unknown,
|
|
2915
|
-
): WorkflowRetryCheckpoint | undefined {
|
|
2916
|
-
const checkpoint = asRecord(asRecord(value).system).retryCheckpoint;
|
|
2917
|
-
if (
|
|
2918
|
-
!checkpoint ||
|
|
2919
|
-
typeof checkpoint !== 'object' ||
|
|
2920
|
-
Array.isArray(checkpoint)
|
|
2921
|
-
)
|
|
2922
|
-
return undefined;
|
|
2923
|
-
const record = checkpoint as Record<string, unknown>;
|
|
2924
|
-
if (
|
|
2925
|
-
typeof record.startNodeId !== 'string' ||
|
|
2926
|
-
!Array.isArray(record.completedNodeIds) ||
|
|
2927
|
-
!record.completedNodeIds.every((item) => typeof item === 'string') ||
|
|
2928
|
-
!record.outputs ||
|
|
2929
|
-
typeof record.outputs !== 'object' ||
|
|
2930
|
-
Array.isArray(record.outputs)
|
|
2931
|
-
)
|
|
2932
|
-
return undefined;
|
|
2933
|
-
const outputs = record.outputs as Record<string, unknown>;
|
|
2934
|
-
if (
|
|
2935
|
-
!record.completedNodeIds.every((nodeId) =>
|
|
2936
|
-
Object.prototype.hasOwnProperty.call(outputs, nodeId),
|
|
2937
|
-
)
|
|
2938
|
-
)
|
|
2939
|
-
return undefined;
|
|
2940
|
-
return {
|
|
2941
|
-
startNodeId: record.startNodeId,
|
|
2942
|
-
completedNodeIds: record.completedNodeIds,
|
|
2943
|
-
outputs,
|
|
2944
|
-
};
|
|
2945
|
-
}
|
|
2946
|
-
|
|
2947
|
-
function buildRetryCheckpoint(
|
|
2928
|
+
function validateSameRunRetryTarget(
|
|
2948
2929
|
dslValue: unknown,
|
|
2949
|
-
nodeExecutions: Array<{
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
output: unknown;
|
|
2953
|
-
}>,
|
|
2954
|
-
retryFromNodeId: string,
|
|
2955
|
-
inheritedCheckpoint?: WorkflowRetryCheckpoint,
|
|
2956
|
-
): WorkflowRetryCheckpoint {
|
|
2930
|
+
nodeExecutions: Array<{ nodeId: string; status: string }>,
|
|
2931
|
+
nodeId: string,
|
|
2932
|
+
): string | undefined {
|
|
2957
2933
|
const dsl = asRecord(dslValue);
|
|
2958
2934
|
const nodes = Array.isArray(dsl.nodes)
|
|
2959
2935
|
? (dsl.nodes as Array<Record<string, unknown>>)
|
|
@@ -2961,128 +2937,41 @@ function buildRetryCheckpoint(
|
|
|
2961
2937
|
const edges = Array.isArray(dsl.edges)
|
|
2962
2938
|
? (dsl.edges as Array<Record<string, unknown>>)
|
|
2963
2939
|
: [];
|
|
2964
|
-
const target = nodes.find((node) => node.id ===
|
|
2965
|
-
if (!target)
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2940
|
+
const target = nodes.find((node) => node.id === nodeId);
|
|
2941
|
+
if (!target) return '失败节点不在原发布版本中';
|
|
2942
|
+
const nodeType = String(target.type ?? '');
|
|
2943
|
+
if (nodeType.endsWith('_trigger')) return '触发节点不能重试';
|
|
2944
|
+
const isCompositeOwner = nodeType === 'parallel' || nodeType === 'for_each';
|
|
2969
2945
|
if (
|
|
2946
|
+
!isCompositeOwner &&
|
|
2970
2947
|
edges.some(
|
|
2971
2948
|
(edge) =>
|
|
2972
2949
|
typeof edge.pairId === 'string' &&
|
|
2973
2950
|
edge.pairId.length > 0 &&
|
|
2974
|
-
(edge.source ===
|
|
2951
|
+
(edge.source === nodeId || edge.target === nodeId),
|
|
2975
2952
|
)
|
|
2976
|
-
)
|
|
2977
|
-
|
|
2978
|
-
}
|
|
2979
|
-
|
|
2980
|
-
const executionMap = new Map<
|
|
2981
|
-
string,
|
|
2982
|
-
{ nodeId: string; status: string; output: unknown }
|
|
2983
|
-
>();
|
|
2984
|
-
for (const completedNodeId of inheritedCheckpoint?.completedNodeIds ?? []) {
|
|
2985
|
-
executionMap.set(completedNodeId, {
|
|
2986
|
-
nodeId: completedNodeId,
|
|
2987
|
-
status: 'COMPLETED',
|
|
2988
|
-
output: inheritedCheckpoint?.outputs[completedNodeId],
|
|
2989
|
-
});
|
|
2990
|
-
}
|
|
2991
|
-
for (const execution of nodeExecutions) {
|
|
2992
|
-
executionMap.set(execution.nodeId, execution);
|
|
2993
|
-
}
|
|
2994
|
-
if (executionMap.get(retryFromNodeId)?.status !== 'FAILED') {
|
|
2995
|
-
throw new BadRequestException('只能从实际失败的节点重试');
|
|
2996
|
-
}
|
|
2997
|
-
|
|
2998
|
-
const mainPredecessors = new Map<string, string[]>();
|
|
2999
|
-
for (const edge of edges) {
|
|
3000
|
-
if (
|
|
3001
|
-
(edge.kind === undefined || edge.kind === 'FLOW') &&
|
|
3002
|
-
!edge.pairId &&
|
|
3003
|
-
typeof edge.source === 'string' &&
|
|
3004
|
-
typeof edge.target === 'string'
|
|
3005
|
-
) {
|
|
3006
|
-
const predecessors = mainPredecessors.get(edge.target) ?? [];
|
|
3007
|
-
predecessors.push(edge.source);
|
|
3008
|
-
mainPredecessors.set(edge.target, predecessors);
|
|
3009
|
-
}
|
|
3010
|
-
}
|
|
3011
|
-
|
|
3012
|
-
const nodeMap = new Map(
|
|
3013
|
-
nodes
|
|
3014
|
-
.filter((node) => typeof node.id === 'string')
|
|
3015
|
-
.map((node) => [String(node.id), node]),
|
|
3016
|
-
);
|
|
3017
|
-
const ancestors = new Set<string>();
|
|
3018
|
-
const pending = [...(mainPredecessors.get(retryFromNodeId) ?? [])];
|
|
3019
|
-
while (pending.length > 0) {
|
|
3020
|
-
const current = pending.pop();
|
|
3021
|
-
if (!current) continue;
|
|
3022
|
-
if (ancestors.has(current)) continue;
|
|
3023
|
-
ancestors.add(current);
|
|
3024
|
-
pending.push(...(mainPredecessors.get(current) ?? []));
|
|
3025
|
-
}
|
|
3026
|
-
|
|
3027
|
-
const completedNodeIds: string[] = [];
|
|
3028
|
-
const outputs: Record<string, unknown> = {};
|
|
3029
|
-
for (const node of nodes) {
|
|
3030
|
-
const ancestorId = typeof node.id === 'string' ? node.id : '';
|
|
3031
|
-
if (!ancestorId || !ancestors.has(ancestorId)) continue;
|
|
3032
|
-
if (String(node.type).endsWith('_trigger')) continue;
|
|
3033
|
-
const execution = executionMap.get(ancestorId);
|
|
3034
|
-
if (execution?.status !== 'COMPLETED') {
|
|
3035
|
-
throw new BadRequestException(
|
|
3036
|
-
`节点“${String(
|
|
3037
|
-
node.label || node.type || ancestorId,
|
|
3038
|
-
)}”没有可复用的成功结果`,
|
|
3039
|
-
);
|
|
3040
|
-
}
|
|
3041
|
-
completedNodeIds.push(ancestorId);
|
|
3042
|
-
outputs[ancestorId] = execution.output;
|
|
3043
|
-
}
|
|
3044
|
-
|
|
3045
|
-
const immediatePredecessors = mainPredecessors.get(retryFromNodeId) ?? [];
|
|
3046
|
-
if (immediatePredecessors.length > 1) {
|
|
3047
|
-
throw new BadRequestException('失败节点存在多个上游,暂不支持从这里重试');
|
|
3048
|
-
}
|
|
2953
|
+
)
|
|
2954
|
+
return '请从外层循环或并行节点重试';
|
|
3049
2955
|
if (
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
)
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
}
|
|
3058
|
-
|
|
3059
|
-
return { startNodeId: retryFromNodeId, completedNodeIds, outputs };
|
|
2956
|
+
!nodeExecutions.some(
|
|
2957
|
+
(execution) =>
|
|
2958
|
+
execution.nodeId === nodeId && execution.status === 'FAILED',
|
|
2959
|
+
)
|
|
2960
|
+
)
|
|
2961
|
+
return '只能重试当前失败的节点';
|
|
2962
|
+
return undefined;
|
|
3060
2963
|
}
|
|
3061
2964
|
|
|
3062
|
-
function
|
|
2965
|
+
function listSameRunRetryableNodeIds(
|
|
3063
2966
|
dsl: unknown,
|
|
3064
|
-
nodeExecutions: Array<{
|
|
3065
|
-
nodeId: string;
|
|
3066
|
-
status: string;
|
|
3067
|
-
output: unknown;
|
|
3068
|
-
}>,
|
|
3069
|
-
inheritedCheckpoint?: WorkflowRetryCheckpoint,
|
|
2967
|
+
nodeExecutions: Array<{ nodeId: string; status: string }>,
|
|
3070
2968
|
): string[] {
|
|
3071
2969
|
return nodeExecutions
|
|
3072
2970
|
.filter((execution) => execution.status === 'FAILED')
|
|
3073
|
-
.filter(
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
nodeExecutions,
|
|
3078
|
-
execution.nodeId,
|
|
3079
|
-
inheritedCheckpoint,
|
|
3080
|
-
);
|
|
3081
|
-
return true;
|
|
3082
|
-
} catch {
|
|
3083
|
-
return false;
|
|
3084
|
-
}
|
|
3085
|
-
})
|
|
2971
|
+
.filter(
|
|
2972
|
+
(execution) =>
|
|
2973
|
+
!validateSameRunRetryTarget(dsl, nodeExecutions, execution.nodeId),
|
|
2974
|
+
)
|
|
3086
2975
|
.map((execution) => execution.nodeId);
|
|
3087
2976
|
}
|
|
3088
2977
|
|
package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.test.ts
CHANGED
|
@@ -10,11 +10,13 @@ describe('workflow route placement', () => {
|
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
describe('person and access routes', () => {
|
|
13
|
-
it('
|
|
14
|
-
expect(getRoutePath('
|
|
15
|
-
expect(getRoutePath('
|
|
16
|
-
|
|
17
|
-
);
|
|
13
|
+
it('merges service/connection/people pages into workflow settings', () => {
|
|
14
|
+
expect(getRoutePath('workflow_settings')).toBe('/admin/workflow/settings');
|
|
15
|
+
expect(getRoutePath('person_directory')).toBeNull();
|
|
16
|
+
expect(getRoutePath('people_group')).toBeNull();
|
|
17
|
+
expect(getRoutePath('workflow_temporal_tenant')).toBeNull();
|
|
18
|
+
expect(getRoutePath('workflow_connections')).toBeNull();
|
|
19
|
+
expect(getRoutePath('workflow_event_catalog')).toBeNull();
|
|
18
20
|
expect(getRoutePath('user')).toBe('/admin/permission/user');
|
|
19
21
|
});
|
|
20
22
|
});
|
|
@@ -191,31 +191,13 @@ export const routeRegistry: Record<string, RouteEntry> = {
|
|
|
191
191
|
})),
|
|
192
192
|
),
|
|
193
193
|
},
|
|
194
|
-
//
|
|
195
|
-
person_directory: {
|
|
196
|
-
path: 'people',
|
|
197
|
-
component: lazyWithReload(() =>
|
|
198
|
-
import('routes/person-directory').then((m) => ({
|
|
199
|
-
default: m.default,
|
|
200
|
-
})),
|
|
201
|
-
),
|
|
202
|
-
},
|
|
203
|
-
// 访问授权 — 保留 user code/path,兼容既有页面权限和链接
|
|
194
|
+
// 用户管理 — 保留 user code/path,兼容既有页面权限和链接
|
|
204
195
|
user: {
|
|
205
196
|
path: 'user',
|
|
206
197
|
component: lazyWithReload(() =>
|
|
207
198
|
import('routes/user').then((m) => ({ default: m.UserListPage })),
|
|
208
199
|
),
|
|
209
200
|
},
|
|
210
|
-
// 人员组 — code: people_group
|
|
211
|
-
people_group: {
|
|
212
|
-
path: 'people-groups',
|
|
213
|
-
component: lazyWithReload(() =>
|
|
214
|
-
import('routes/people-groups').then((m) => ({
|
|
215
|
-
default: m.default,
|
|
216
|
-
})),
|
|
217
|
-
),
|
|
218
|
-
},
|
|
219
201
|
// 角色管理
|
|
220
202
|
role: {
|
|
221
203
|
path: 'role-list',
|
|
@@ -379,24 +361,12 @@ export const routeRegistry: Record<string, RouteEntry> = {
|
|
|
379
361
|
() => import('routes/workflow/node-instances/show'),
|
|
380
362
|
),
|
|
381
363
|
},
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
),
|
|
387
|
-
},
|
|
388
|
-
workflow_connections: {
|
|
389
|
-
path: 'connections',
|
|
390
|
-
component: lazyWithReload(
|
|
391
|
-
() => import('routes/workflow/connections'),
|
|
392
|
-
),
|
|
393
|
-
},
|
|
394
|
-
workflow_event_catalog: {
|
|
395
|
-
path: 'business-events',
|
|
396
|
-
component: lazyWithReload(
|
|
397
|
-
() => import('routes/workflow/business-events'),
|
|
398
|
-
),
|
|
364
|
+
// 功能设置 — 合并原服务注册/连接管理/人员目录/人员组四个页面
|
|
365
|
+
workflow_settings: {
|
|
366
|
+
path: 'settings',
|
|
367
|
+
component: lazyWithReload(() => import('routes/workflow/settings')),
|
|
399
368
|
},
|
|
369
|
+
// 业务事件运行记录详情(列表已并入功能设置 tab,此路由保留详情深链)
|
|
400
370
|
businessEventDetail: {
|
|
401
371
|
path: 'business-events/show/:registrationCode',
|
|
402
372
|
component: lazyWithReload(
|