@gadmin2n/schematics 0.0.72 → 0.0.74

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.
Files changed (173) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +2 -0
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/job.prisma +62 -0
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/system.prisma +0 -21
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +171 -0
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/AgendaJob.ts +60 -0
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/Event.ts +1 -1
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/WorkflowEventOutbox.ts +62 -0
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/WorkflowNodeInstance.ts +62 -0
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/WorkflowNodeType.ts +62 -0
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/.env +5 -0
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +5 -4
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/prisma.config.ts +14 -7
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/index.ts +4 -0
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permissions.ts +49 -3
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +746 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflows.ts +786 -0
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.controller.ts +6 -0
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agenda/agenda.service.ts +79 -0
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agendaJob/agendaJob.controller.spec.ts +20 -0
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agendaJob/agendaJob.controller.ts +145 -0
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agendaJob/agendaJob.module.ts +10 -0
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/{canvas/canvas.service.spec.ts → agendaJob/agendaJob.service.spec.ts} +71 -65
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agendaJob/agendaJob.service.ts +83 -0
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/index.ts +2 -1
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/temporal.module.ts +9 -0
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/temporal.service.ts +100 -0
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution.dto.ts +19 -0
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-export.dto.ts +43 -0
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-export.service.ts +317 -0
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-node-type.controller.ts +16 -0
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-node-type.service.ts +13 -0
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +220 -0
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.dto.ts +82 -0
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.module.ts +16 -0
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +505 -0
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowEventOutbox/workflowEventOutbox.controller.spec.ts +22 -0
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowEventOutbox/workflowEventOutbox.controller.ts +147 -0
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowEventOutbox/workflowEventOutbox.module.ts +10 -0
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowEventOutbox/workflowEventOutbox.service.spec.ts +356 -0
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowEventOutbox/workflowEventOutbox.service.ts +110 -0
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeInstance/workflowNodeInstance.controller.spec.ts +22 -0
  43. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeInstance/workflowNodeInstance.controller.ts +216 -0
  44. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeInstance/workflowNodeInstance.module.ts +10 -0
  45. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeInstance/workflowNodeInstance.service.spec.ts +356 -0
  46. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeInstance/workflowNodeInstance.service.ts +168 -0
  47. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeType/workflowNodeType.controller.spec.ts +22 -0
  48. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeType/workflowNodeType.controller.ts +199 -0
  49. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeType/workflowNodeType.module.ts +10 -0
  50. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeType/workflowNodeType.service.spec.ts +348 -0
  51. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeType/workflowNodeType.service.ts +106 -0
  52. package/dist/lib/application/files/gadmin2-game-angle-demo/server/yarn.lock +579 -1082
  53. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/README.md +278 -0
  54. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/config/development-sql.yaml +5 -0
  55. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/docker-compose.yml +25 -0
  56. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/package.json +13 -0
  57. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/sql/create-event-trigger.sql +87 -0
  58. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/.env +7 -0
  59. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/SANDBOX.md +122 -0
  60. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/package-lock.json +4285 -0
  61. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/package.json +28 -0
  62. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/__tests__/activities/code-execute.test.ts +44 -0
  63. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/__tests__/activities/http-request.test.ts +87 -0
  64. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/__tests__/helpers.test.ts +225 -0
  65. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/__tests__/node-type-consistency.test.ts +101 -0
  66. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/code-execute.ts +51 -0
  67. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/db-execute.ts +85 -0
  68. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/db-query.ts +35 -0
  69. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/http-request.ts +54 -0
  70. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/index.ts +6 -0
  71. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/reporting.ts +62 -0
  72. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/send-notification.ts +47 -0
  73. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/config.ts +13 -0
  74. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/condition.ts +101 -0
  75. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/context.ts +58 -0
  76. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/graph.ts +184 -0
  77. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/helpers.ts +133 -0
  78. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/node-types.ts +57 -0
  79. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/types.ts +77 -0
  80. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/index.ts +36 -0
  81. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/outbox-poller.ts +226 -0
  82. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/workflows/dsl-workflow.ts +411 -0
  83. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/tsconfig.json +19 -0
  84. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/vitest.config.ts +8 -0
  85. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/yarn.lock +1905 -0
  86. package/dist/lib/application/files/gadmin2-game-angle-demo/web/package-lock.json +17555 -0
  87. package/dist/lib/application/files/gadmin2-game-angle-demo/web/package.json +5 -2
  88. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/App.tsx +1 -0
  89. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/sider.tsx +5 -1
  90. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/title.tsx +1 -1
  91. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +63 -0
  92. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/dev-shell/DevShell.tsx +91 -2
  93. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/list.tsx +48 -2
  94. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/show.tsx +43 -2
  95. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +14 -9
  96. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +14 -9
  97. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agenda/index.tsx +309 -56
  98. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agenda/show.tsx +1 -3
  99. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agendaJob/create.tsx +108 -0
  100. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agendaJob/edit.tsx +124 -0
  101. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agendaJob/index.tsx +4 -0
  102. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agendaJob/list.tsx +245 -0
  103. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/agendaJob/show.tsx +70 -0
  104. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasListPage.tsx +0 -1
  105. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasPage.tsx +160 -2
  106. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasToolbar.tsx +120 -148
  107. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CodeFloatWindow.tsx +74 -181
  108. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/LivePreview.tsx +15 -13
  109. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasConfigRegistry.tsx +2 -2
  110. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasContextMenuRegistry.tsx +338 -3
  111. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasDefaults.ts +18 -17
  112. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/BarChartDataSourceModal.tsx +10 -4
  113. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/LineChartDataSourceModal.tsx +10 -4
  114. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/{ChartViewerConfigModal.tsx → MultiChartConfigModal.tsx} +30 -18
  115. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/MultiChartDataSourceModal.tsx +427 -0
  116. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/NumCardDataSourceModal.tsx +10 -4
  117. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/PromptModal.tsx +6 -14
  118. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/RadarChartDataSourceModal.tsx +10 -4
  119. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/TableDataSourceModal.tsx +10 -4
  120. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/canvasModalProps.ts +24 -0
  121. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/demos.ts +45 -63
  122. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CustomNode.tsx +99 -0
  123. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/ExportModal.tsx +87 -0
  124. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/FlowRenderer.tsx +322 -0
  125. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/ImportModal.tsx +175 -0
  126. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodeEditModal.tsx +60 -0
  127. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.tsx +1150 -0
  128. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/RunWorkflowModal.tsx +101 -0
  129. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/StatusCards.tsx +198 -0
  130. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/VersionPanel.tsx +81 -0
  131. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx +566 -0
  132. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/hooks/useWorkflowAgent.ts +224 -0
  133. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/index.tsx +524 -0
  134. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +343 -0
  135. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instances.tsx +243 -0
  136. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/components/CreateNodeInstanceModal.tsx +363 -0
  137. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/components/DynamicConfigForm.tsx +154 -0
  138. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/components/NodeInstanceForm.tsx +176 -0
  139. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/create.tsx +77 -0
  140. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/edit.tsx +112 -0
  141. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/index.tsx +305 -0
  142. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/node-instances/show.tsx +282 -0
  143. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/show.tsx +469 -0
  144. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +92 -0
  145. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflowEventOutbox/create.tsx +111 -0
  146. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflowEventOutbox/edit.tsx +127 -0
  147. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflowEventOutbox/index.tsx +4 -0
  148. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflowEventOutbox/list.tsx +254 -0
  149. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflowEventOutbox/show.tsx +74 -0
  150. package/dist/lib/application/files/gadmin2-game-angle-demo/web/yarn.lock +1501 -1199
  151. package/package.json +1 -1
  152. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/app.controller.spec.ts +0 -22
  153. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/canvas/BarChart/index.tsx +0 -896
  154. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/canvas/ChartSwitcher/index.tsx +0 -219
  155. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/canvas/ChartViewer/index.tsx +0 -159
  156. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/canvas/Filter/index.tsx +0 -192
  157. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/canvas/LineChart/index.tsx +0 -1034
  158. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/canvas/NumCard/NumCard.module.css +0 -8
  159. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/canvas/NumCard/index.tsx +0 -509
  160. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/canvas/NumLineCard/index.tsx +0 -66
  161. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/canvas/PieChart/index.tsx +0 -552
  162. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/canvas/RadarChart/index.tsx +0 -263
  163. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/canvas/Section/index.tsx +0 -35
  164. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/canvas/Table/index.tsx +0 -207
  165. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/canvas/TreemapChart/index.tsx +0 -382
  166. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/canvas/WorldMap/index.tsx +0 -135
  167. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/canvas/chart-constants.ts +0 -53
  168. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/canvas/icon/InfoIcon.tsx +0 -8
  169. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/canvas/icon/index.ts +0 -1
  170. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/canvas/map/config.ts +0 -31
  171. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/canvas/map/nameMap.json +0 -9
  172. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/canvas/map/world.geo.json +0 -39349
  173. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/canvas/metric-info-tooltip/index.tsx +0 -19
@@ -0,0 +1,1150 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import {
3
+ Button,
4
+ Checkbox,
5
+ Input,
6
+ InputNumber,
7
+ message,
8
+ Segmented,
9
+ Select,
10
+ Space,
11
+ Spin,
12
+ Switch,
13
+ Tag,
14
+ Tooltip,
15
+ Typography,
16
+ } from 'antd';
17
+ import {
18
+ DeleteOutlined,
19
+ DisconnectOutlined,
20
+ LinkOutlined,
21
+ MinusCircleOutlined,
22
+ PlusOutlined,
23
+ SendOutlined,
24
+ } from '@ant-design/icons';
25
+ import { customRequest } from 'helpers/http';
26
+ import type { WorkflowNode, WorkflowNodeType } from '../types';
27
+
28
+ const { Text, Title } = Typography;
29
+ const { TextArea } = Input;
30
+
31
+ const CATEGORY_COLORS: Record<string, { bg: string; border: string }> = {
32
+ TRIGGER: { bg: '#e6f7ff', border: '#91caff' },
33
+ ACTION: { bg: '#f6ffed', border: '#b7eb8f' },
34
+ CONDITION: { bg: '#fff7e6', border: '#ffd591' },
35
+ LOOP: { bg: '#f9f0ff', border: '#d3adf7' },
36
+ FLOW_CONTROL: { bg: '#f0f5ff', border: '#adc6ff' },
37
+ APPROVAL: { bg: '#fff1f0', border: '#ffa39e' },
38
+ SUB_WORKFLOW: { bg: '#f0f5ff', border: '#adc6ff' },
39
+ };
40
+
41
+ const CATEGORY_LABELS: Record<string, string> = {
42
+ TRIGGER: '触发器',
43
+ ACTION: '动作',
44
+ CONDITION: '条件',
45
+ LOOP: '循环',
46
+ FLOW_CONTROL: '流程控制',
47
+ APPROVAL: '审批',
48
+ SUB_WORKFLOW: '子流程',
49
+ };
50
+
51
+ // Fields that should use TextArea for longer input
52
+ const TEXTAREA_FIELDS = new Set([
53
+ 'query',
54
+ 'message',
55
+ 'condition',
56
+ 'body',
57
+ 'headers',
58
+ 'inputMapping',
59
+ 'script',
60
+ 'rawSql',
61
+ 'data',
62
+ 'responseBody',
63
+ 'fallbackExpression',
64
+ ]);
65
+
66
+ // Rich tooltip content for node types (hardcoded)
67
+ const NODE_TOOLTIPS: Record<string, string> = {
68
+ cron_trigger:
69
+ '定时触发工作流\n' +
70
+ '━━━━━━━━━━━━━━━━━━━━\n' +
71
+ '示例: 每天零点生成日报、每5分钟检查队列、每月1号结算\n' +
72
+ 'Temporal: ScheduleClient 创建定时 Schedule,到点启动 Workflow',
73
+ event_trigger:
74
+ '监听内部业务事件,事件发生时自动触发\n' +
75
+ '━━━━━━━━━━━━━━━━━━━━\n' +
76
+ '事件类别:\n' +
77
+ '• 数据变更: record.created, record.updated\n' +
78
+ '• 状态变更: order.paid, ticket.escalated\n' +
79
+ '• 用户行为: user.registered, login_failed_3x\n' +
80
+ '• 系统事件: deployment.completed, health_check.failed\n' +
81
+ '━━━━━━━━━━━━━━━━━━━━\n' +
82
+ 'Temporal: 事件总线收到事件后调用 WorkflowClient.start() 启动新 Workflow,或通过 Signal 唤醒等待中的 Workflow\n\n' +
83
+ '通常后端会有一个事件总线(如 Redis Pub/Sub、NATS、Kafka、或简单的内存 EventEmitter),当业务代码 emit("order.paid", payload)时,事件中间件查找所有订阅了该事件的 active workflow 定义,并启动对应的 Temporal Workflow。',
84
+ webhook_trigger:
85
+ '通过 HTTP 请求触发工作流\n' +
86
+ '━━━━━━━━━━━━━━━━━━━━\n' +
87
+ '示例: 第三方支付回调、GitHub Webhook、表单提交\n' +
88
+ 'Temporal: HTTP 网关接收请求后调用 WorkflowClient.start(),支持同步等待结果或异步返回',
89
+ manual_trigger:
90
+ '手动触发,用于调试或一次性执行\n' +
91
+ '━━━━━━━━━━━━━━━━━━━━\n' +
92
+ '示例: 手动跑数据迁移、调试新 Workflow、一次性批处理\n' +
93
+ 'Temporal: 直接调用 WorkflowClient.start() 并传入自定义 input',
94
+ http_request:
95
+ '向外部 API 发送 HTTP 请求\n' +
96
+ '━━━━━━━━━━━━━━━━━━━━\n' +
97
+ '示例: 调用第三方 API、推送数据到下游系统、获取外部数据\n' +
98
+ 'Temporal: 封装为 Activity,自带超时和重试策略',
99
+ db_query:
100
+ '执行数据库查询(读操作)\n' +
101
+ '━━━━━━━━━━━━━━━━━━━━\n' +
102
+ '示例: 查询订单状态、获取用户列表、统计聚合\n' +
103
+ 'Temporal: 封装为 Activity,支持参数化查询防注入',
104
+ db_execute:
105
+ '执行数据库写操作\n' +
106
+ '━━━━━━━━━━━━━━━━━━━━\n' +
107
+ '示例: 插入审批记录、更新订单状态、批量删除过期数据\n' +
108
+ 'Temporal: 封装为 Activity,失败时 Workflow 可选择重试或补偿',
109
+ send_notification:
110
+ '发送通知消息\n' +
111
+ '━━━━━━━━━━━━━━━━━━━━\n' +
112
+ '示例: 审批结果邮件、Slack 告警、企微/钉钉群消息\n' +
113
+ 'Temporal: 封装为 Activity,支持模板变量和多渠道',
114
+ code:
115
+ '执行自定义 JavaScript 代码\n' +
116
+ '━━━━━━━━━━━━━━━━━━━━\n' +
117
+ '示例: 数据格式转换、复杂计算、JSON 结构重组、条件逻辑\n' +
118
+ 'Temporal: 封装为 Activity,在 Worker 中通过 VM2 沙箱执行',
119
+ set_variable:
120
+ '设置、转换或重命名工作流变量\n' +
121
+ '━━━━━━━━━━━━━━━━━━━━\n' +
122
+ '示例: 提取嵌套字段、重命名 API 返回字段、构造下游所需结构\n' +
123
+ 'Temporal: 在 Workflow 代码中直接操作 context 对象',
124
+ if_else:
125
+ '根据条件走不同分支\n' +
126
+ '━━━━━━━━━━━━━━━━━━━━\n' +
127
+ '示例: 金额>1万走人工审批、VIP用户走快速通道\n' +
128
+ 'Temporal: Workflow 代码中的 if/else 控制流',
129
+ switch:
130
+ '根据字段值走多路分支\n' +
131
+ '━━━━━━━━━━━━━━━━━━━━\n' +
132
+ '示例: 按订单类型分发、按地区选择处理逻辑\n' +
133
+ 'Temporal: Workflow 代码中的 switch/case 控制流',
134
+ for_each:
135
+ '遍历数组,对每个元素执行子流程\n' +
136
+ '━━━━━━━━━━━━━━━━━━━━\n' +
137
+ '示例: 批量发送通知、逐个处理订单项、并行调用多个 API\n' +
138
+ 'Temporal: 循环内启动 Activity 或 Child Workflow,支持并行控制',
139
+ delay:
140
+ '暂停工作流执行,等待指定时间或信号\n' +
141
+ '━━━━━━━━━━━━━━━━━━━━\n' +
142
+ '示例: 等待30分钟后发提醒、等到指定时间执行、等待外部回调\n' +
143
+ 'Temporal: workflow.sleep()(持久化定时器,服务重启不丢失)',
144
+ parallel:
145
+ '并行执行多个分支,等待完成后合并\n' +
146
+ '━━━━━━━━━━━━━━━━━━━━\n' +
147
+ '示例: 同时调用多个 API 聚合结果、并行审批多个部门\n' +
148
+ 'Temporal: Promise.all() 并行启动多个 Activity/Child Workflow',
149
+ error_handler:
150
+ '捕获上游节点错误,执行降级逻辑\n' +
151
+ '━━━━━━━━━━━━━━━━━━━━\n' +
152
+ '示例: API 失败后走备用方案、超时后发告警通知\n' +
153
+ 'Temporal: try/catch + RetryPolicy(支持固定/指数退避重试)',
154
+ approval:
155
+ '暂停流程等待人工审批\n' +
156
+ '━━━━━━━━━━━━━━━━━━━━\n' +
157
+ '示例: 费用报销审批、上线发布审批、权限申请审批\n' +
158
+ 'Temporal: workflow.condition() 等待 Signal,审批人通过 SignalWorkflow 发送结果',
159
+ sub_workflow:
160
+ '调用另一个已定义的工作流作为子流程\n' +
161
+ '━━━━━━━━━━━━━━━━━━━━\n' +
162
+ '示例: 复用通用审批流程、嵌套调用数据处理 Pipeline\n' +
163
+ 'Temporal: executeChild() 启动 Child Workflow,支持同步等待或异步触发',
164
+ };
165
+
166
+ // Placeholder hints for specific fields
167
+ const FIELD_PLACEHOLDERS: Record<string, string> = {
168
+ cron: '0 0 * * * (每天零点)',
169
+ timezone: 'Asia/Shanghai',
170
+ eventName: 'order.created',
171
+ path: '/api/webhook/my-hook',
172
+ url: 'https://api.example.com/endpoint',
173
+ query: 'SELECT * FROM table WHERE id = $1',
174
+ rawSql: 'INSERT INTO table (col) VALUES ($1)',
175
+ condition: 'data.amount > 1000',
176
+ fallbackExpression: 'data.status === "active"',
177
+ iteratorField: 'data.items',
178
+ message: '通知内容,支持 {{variable}} 模板变量',
179
+ subject: '邮件主题',
180
+ field: 'data.status',
181
+ timeout: '超时时间(秒)',
182
+ duration: '延迟秒数',
183
+ script: '// 在此编写 JavaScript 代码\nreturn { result: input.data };',
184
+ table: '表名',
185
+ where: 'id = $1',
186
+ datasource: 'default',
187
+ signalName: 'approval_signal',
188
+ gotoNodeId: '目标节点 ID',
189
+ };
190
+
191
+ interface Props {
192
+ node: WorkflowNode | null;
193
+ nodeTypes: WorkflowNodeType[];
194
+ currentWorkflowId?: string;
195
+ onSave: (
196
+ nodeId: string,
197
+ updates: {
198
+ label?: string;
199
+ config?: Record<string, any>;
200
+ instanceRef?: WorkflowNode['instanceRef'] | null;
201
+ },
202
+ ) => void;
203
+ onLabelChange: (nodeId: string, label: string) => void;
204
+ onAiEdit: (nodeId: string, prompt: string) => void;
205
+ onDelete?: (nodeId: string) => void;
206
+ onAbort?: () => void;
207
+ aiLoading: boolean;
208
+ }
209
+
210
+ export function NodePropertyPanel({
211
+ node,
212
+ nodeTypes,
213
+ currentWorkflowId,
214
+ onSave,
215
+ onLabelChange,
216
+ onAiEdit,
217
+ onDelete,
218
+ onAbort,
219
+ aiLoading,
220
+ }: Props) {
221
+ const [label, setLabel] = useState('');
222
+ const [config, setConfig] = useState<Record<string, any>>({});
223
+ const [prompt, setPrompt] = useState('');
224
+ const [dirty, setDirty] = useState(false);
225
+ const [activeTab, setActiveTab] = useState<'config' | 'ai'>('config');
226
+ const [workflowOptions, setWorkflowOptions] = useState<
227
+ { label: string; value: string }[]
228
+ >([]);
229
+ const [instances, setInstances] = useState<
230
+ {
231
+ id: string;
232
+ name: string;
233
+ description: string | null;
234
+ config: Record<string, any>;
235
+ }[]
236
+ >([]);
237
+ const [selectedInstanceId, setSelectedInstanceId] = useState<string | null>(
238
+ null,
239
+ );
240
+ const [instanceLoading, setInstanceLoading] = useState(false);
241
+ const [newInstanceName, setNewInstanceName] = useState('');
242
+ const [newInstanceDesc, setNewInstanceDesc] = useState('');
243
+ const [saveAsLoading, setSaveAsLoading] = useState(false);
244
+ const [alsoSaveAsInstance, setAlsoSaveAsInstance] = useState(false);
245
+
246
+ // Find configSchema for current node type
247
+ const currentNodeType = nodeTypes.find((nt) => nt.type === node?.type);
248
+ const configSchema = (currentNodeType?.configSchema || {}) as any;
249
+ const requiredFields: string[] = configSchema.required || [];
250
+ const properties: Record<string, any> = configSchema.properties || {};
251
+
252
+ useEffect(() => {
253
+ if (node) {
254
+ setLabel(node.label);
255
+ // Merge existing config with schema defaults
256
+ const mergedConfig: Record<string, any> = {};
257
+ for (const [key, prop] of Object.entries<any>(properties)) {
258
+ const existingValue = node.config?.[key];
259
+ if (existingValue !== undefined) {
260
+ mergedConfig[key] = existingValue;
261
+ } else if (prop.type === 'string') {
262
+ mergedConfig[key] = '';
263
+ } else if (prop.type === 'number') {
264
+ mergedConfig[key] = undefined;
265
+ } else if (prop.type === 'boolean') {
266
+ mergedConfig[key] = false;
267
+ } else if (prop.type === 'array') {
268
+ mergedConfig[key] = [];
269
+ } else if (prop.type === 'object') {
270
+ mergedConfig[key] = {};
271
+ }
272
+ }
273
+ setConfig(mergedConfig);
274
+ setDirty(false);
275
+ setPrompt('');
276
+ }
277
+ }, [node?.id, currentNodeType]);
278
+
279
+ // Sync form when node config/label updates externally (e.g. AI edit)
280
+ useEffect(() => {
281
+ if (!node || dirty) return;
282
+ setLabel(node.label);
283
+ const mergedConfig: Record<string, any> = {};
284
+ for (const [key, prop] of Object.entries<any>(properties)) {
285
+ const existingValue = node.config?.[key];
286
+ if (existingValue !== undefined) {
287
+ mergedConfig[key] = existingValue;
288
+ } else if (prop.type === 'string') {
289
+ mergedConfig[key] = '';
290
+ } else if (prop.type === 'number') {
291
+ mergedConfig[key] = undefined;
292
+ } else if (prop.type === 'boolean') {
293
+ mergedConfig[key] = false;
294
+ } else if (prop.type === 'array') {
295
+ mergedConfig[key] = [];
296
+ } else if (prop.type === 'object') {
297
+ mergedConfig[key] = {};
298
+ }
299
+ }
300
+ setConfig(mergedConfig);
301
+ // eslint-disable-next-line react-hooks/exhaustive-deps
302
+ }, [node?.label, JSON.stringify(node?.config)]);
303
+
304
+ // Load workflow list for Sub Workflow node
305
+ useEffect(() => {
306
+ if (node?.type === 'sub_workflow') {
307
+ customRequest<{ data: any[] }>('workflow', 'GET')
308
+ .then((res) => {
309
+ const list = Array.isArray(res) ? res : res?.data || [];
310
+ setWorkflowOptions(
311
+ list
312
+ .filter((w: any) => String(w.id) !== currentWorkflowId)
313
+ .map((w: any) => ({ label: w.name, value: String(w.id) })),
314
+ );
315
+ })
316
+ .catch(() => {});
317
+ }
318
+ }, [node?.type, currentWorkflowId]);
319
+
320
+ // Load instances for current node type
321
+ useEffect(() => {
322
+ if (!currentNodeType?.id) {
323
+ setInstances([]);
324
+ return;
325
+ }
326
+ setInstanceLoading(true);
327
+ customRequest<{ data: any[] }>('workflowNodeInstance/findMany', 'POST', {
328
+ where: { nodeTypeId: Number(currentNodeType.id), isEnabled: true },
329
+ take: 100,
330
+ })
331
+ .then((res) => {
332
+ const list = Array.isArray(res) ? res : res?.data || [];
333
+ setInstances(
334
+ list.map((item: any) => ({
335
+ id: String(item.id),
336
+ name: item.name,
337
+ description: item.description,
338
+ config: item.config ?? {},
339
+ })),
340
+ );
341
+ })
342
+ .catch(() => setInstances([]))
343
+ .finally(() => setInstanceLoading(false));
344
+ }, [currentNodeType?.id]);
345
+
346
+ // Reset selection when node changes
347
+ useEffect(() => {
348
+ setSelectedInstanceId(null);
349
+ setAlsoSaveAsInstance(false);
350
+ }, [node?.id]);
351
+
352
+ if (!node) {
353
+ // Panel state: generating
354
+ if (aiLoading) {
355
+ return (
356
+ <div
357
+ style={{
358
+ width: 320,
359
+ display: 'flex',
360
+ flexDirection: 'column',
361
+ alignItems: 'center',
362
+ justifyContent: 'center',
363
+ borderLeft: '1px solid #f0f0f0',
364
+ background: '#fff',
365
+ gap: 16,
366
+ padding: 32,
367
+ }}
368
+ >
369
+ <Spin size="large" />
370
+ <Title level={5} style={{ margin: 0, color: '#595959' }}>
371
+ AI is generating workflow…
372
+ </Title>
373
+ <Text type="secondary" style={{ textAlign: 'center', fontSize: 13 }}>
374
+ Please wait, the workflow will be updated on the canvas
375
+ automatically.
376
+ </Text>
377
+ <Button onClick={() => onAbort?.()}>Cancel</Button>
378
+ </div>
379
+ );
380
+ }
381
+
382
+ return (
383
+ <div
384
+ style={{
385
+ width: 320,
386
+ display: 'flex',
387
+ flexDirection: 'column',
388
+ borderLeft: '1px solid #f0f0f0',
389
+ background: '#fff',
390
+ overflow: 'hidden',
391
+ }}
392
+ >
393
+ {/* Node palette */}
394
+ <div style={{ flex: 1, overflow: 'auto', padding: 16 }}>
395
+ <Text
396
+ type="secondary"
397
+ style={{ fontSize: 13, display: 'block', marginBottom: 12 }}
398
+ >
399
+ 拖拽节点到左侧画布
400
+ </Text>
401
+ {Object.entries(
402
+ nodeTypes.reduce<Record<string, WorkflowNodeType[]>>((acc, nt) => {
403
+ const cat = nt.category || 'ACTION';
404
+ if (!acc[cat]) acc[cat] = [];
405
+ acc[cat].push(nt);
406
+ return acc;
407
+ }, {}),
408
+ ).map(([category, types]) => {
409
+ const colors = CATEGORY_COLORS[category] || CATEGORY_COLORS.ACTION;
410
+ return (
411
+ <div key={category} style={{ marginBottom: 12 }}>
412
+ <Text
413
+ strong
414
+ style={{
415
+ fontSize: 12,
416
+ color: '#595959',
417
+ display: 'block',
418
+ marginBottom: 6,
419
+ }}
420
+ >
421
+ {CATEGORY_LABELS[category] || category}
422
+ </Text>
423
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
424
+ {types.map((nt) => (
425
+ <Tooltip
426
+ key={nt.type}
427
+ title={
428
+ <div
429
+ style={{
430
+ whiteSpace: 'pre-line',
431
+ fontSize: 12,
432
+ lineHeight: 1.6,
433
+ }}
434
+ >
435
+ {NODE_TOOLTIPS[nt.type] || nt.description || nt.label}
436
+ </div>
437
+ }
438
+ placement="left"
439
+ mouseEnterDelay={0.3}
440
+ overlayStyle={{ maxWidth: 360 }}
441
+ >
442
+ <div
443
+ draggable
444
+ onDragStart={(e) => {
445
+ e.dataTransfer.setData(
446
+ 'application/workflow-node-type',
447
+ JSON.stringify(nt),
448
+ );
449
+ e.dataTransfer.effectAllowed = 'move';
450
+ }}
451
+ style={{
452
+ cursor: 'grab',
453
+ padding: '5px 10px',
454
+ background: colors.bg,
455
+ border: `1px solid ${colors.border}`,
456
+ borderRadius: 6,
457
+ fontSize: 12,
458
+ userSelect: 'none',
459
+ }}
460
+ >
461
+ {nt.label}
462
+ </div>
463
+ </Tooltip>
464
+ ))}
465
+ </div>
466
+ </div>
467
+ );
468
+ })}
469
+ </div>
470
+ </div>
471
+ );
472
+ }
473
+
474
+ function handleConfigChange(key: string, value: any) {
475
+ setConfig((prev) => ({ ...prev, [key]: value }));
476
+ setDirty(true);
477
+ }
478
+
479
+ function handleLabelChange(value: string) {
480
+ setLabel(value);
481
+ if (node) {
482
+ onLabelChange(node.id, value);
483
+ }
484
+ }
485
+
486
+ async function handleSave() {
487
+ if (!node) return;
488
+ // Clean undefined values
489
+ const cleanConfig: Record<string, any> = {};
490
+ for (const [k, v] of Object.entries(config)) {
491
+ if (v !== undefined && v !== '') cleanConfig[k] = v;
492
+ }
493
+
494
+ if (alsoSaveAsInstance && currentNodeType && newInstanceName.trim()) {
495
+ // Save node AND create a new instance, then reference it
496
+ setSaveAsLoading(true);
497
+ try {
498
+ const res = await customRequest<any>(
499
+ 'workflowNodeInstance/createOne',
500
+ 'POST',
501
+ {
502
+ nodeTypeId: Number(currentNodeType.id),
503
+ name: newInstanceName.trim(),
504
+ description: newInstanceDesc.trim() || null,
505
+ config: cleanConfig,
506
+ isEnabled: true,
507
+ },
508
+ );
509
+ const newId = String(res.id);
510
+ onSave(node.id, {
511
+ label,
512
+ config: {},
513
+ instanceRef: {
514
+ instanceId: newId,
515
+ instanceName: newInstanceName.trim(),
516
+ },
517
+ });
518
+ setInstances((prev) => [
519
+ ...prev,
520
+ {
521
+ id: newId,
522
+ name: newInstanceName.trim(),
523
+ description: newInstanceDesc.trim() || null,
524
+ config: cleanConfig,
525
+ },
526
+ ]);
527
+ setAlsoSaveAsInstance(false);
528
+ setNewInstanceName('');
529
+ setNewInstanceDesc('');
530
+ message.success('已保存并创建新实例');
531
+ } catch (e: any) {
532
+ message.error(e?.message ?? '创建实例失败');
533
+ } finally {
534
+ setSaveAsLoading(false);
535
+ }
536
+ } else {
537
+ onSave(node.id, { label, config: cleanConfig });
538
+ message.success('节点已保存');
539
+ }
540
+ setDirty(false);
541
+ }
542
+
543
+ function handleAiSubmit() {
544
+ if (!node || !prompt.trim()) return;
545
+ onAiEdit(node.id, prompt.trim());
546
+ setPrompt('');
547
+ }
548
+
549
+ function handleReferenceInstance() {
550
+ if (!node || !selectedInstanceId) return;
551
+ const inst = instances.find((i) => i.id === selectedInstanceId);
552
+ if (!inst) return;
553
+ onSave(node.id, {
554
+ label: inst.name,
555
+ config: {},
556
+ instanceRef: { instanceId: inst.id, instanceName: inst.name },
557
+ });
558
+ setLabel(inst.name);
559
+ setSelectedInstanceId(null);
560
+ }
561
+
562
+ function handleCopyAsTemplate() {
563
+ if (!node || !selectedInstanceId) return;
564
+ const inst = instances.find((i) => i.id === selectedInstanceId);
565
+ if (!inst) return;
566
+ setConfig(inst.config);
567
+ setLabel(inst.name);
568
+ setDirty(true);
569
+ setSelectedInstanceId(null);
570
+ if (node) {
571
+ onLabelChange(node.id, inst.name);
572
+ }
573
+ message.success('已加载实例配置,可自由修改');
574
+ }
575
+
576
+ function handleDetachReference() {
577
+ if (!node) return;
578
+ const inst = instances.find((i) => i.id === node.instanceRef?.instanceId);
579
+ const resolvedConfig = inst?.config ?? {};
580
+ onSave(node.id, { config: resolvedConfig, instanceRef: null });
581
+ }
582
+
583
+ // Render a config field based on its schema
584
+ function renderConfigField(key: string, schema: any) {
585
+ const isRequired = requiredFields.includes(key);
586
+ const value = config[key];
587
+ const placeholder = FIELD_PLACEHOLDERS[key] || '';
588
+
589
+ // Special: workflowId → Select with workflow list
590
+ if (key === 'workflowId') {
591
+ return (
592
+ <div key={key}>
593
+ <Text
594
+ type="secondary"
595
+ style={{ fontSize: 13, display: 'block', marginBottom: 2 }}
596
+ >
597
+ {isRequired && <span style={{ color: '#ff4d4f' }}>* </span>}
598
+ {key}
599
+ </Text>
600
+ <Select
601
+ value={value || undefined}
602
+ onChange={(v) => handleConfigChange(key, v)}
603
+ placeholder="选择子流程"
604
+ options={workflowOptions}
605
+ style={{ width: '100%' }}
606
+ allowClear
607
+ showSearch
608
+ filterOption={(input, option) =>
609
+ (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
610
+ }
611
+ />
612
+ </div>
613
+ );
614
+ }
615
+
616
+ // String with enum → Select
617
+ if (schema.type === 'string' && schema.enum) {
618
+ return (
619
+ <div key={key}>
620
+ <Text
621
+ type="secondary"
622
+ style={{ fontSize: 13, display: 'block', marginBottom: 2 }}
623
+ >
624
+ {isRequired && <span style={{ color: '#ff4d4f' }}>* </span>}
625
+ {key}
626
+ </Text>
627
+ <Select
628
+ value={value || undefined}
629
+ onChange={(v) => handleConfigChange(key, v)}
630
+ placeholder={`选择 ${key}`}
631
+ options={schema.enum.map((e: string) => ({ label: e, value: e }))}
632
+ style={{ width: '100%' }}
633
+ />
634
+ </div>
635
+ );
636
+ }
637
+
638
+ // String → Input or TextArea
639
+ if (schema.type === 'string') {
640
+ const useTextArea = TEXTAREA_FIELDS.has(key);
641
+ return (
642
+ <div key={key}>
643
+ <Text
644
+ type="secondary"
645
+ style={{ fontSize: 13, display: 'block', marginBottom: 2 }}
646
+ >
647
+ {isRequired && <span style={{ color: '#ff4d4f' }}>* </span>}
648
+ {key}
649
+ </Text>
650
+ {useTextArea ? (
651
+ <TextArea
652
+ value={value ?? ''}
653
+ onChange={(e) => handleConfigChange(key, e.target.value)}
654
+ placeholder={placeholder}
655
+ autoSize={{ minRows: 2, maxRows: 6 }}
656
+ />
657
+ ) : (
658
+ <Input
659
+ value={value ?? ''}
660
+ onChange={(e) => handleConfigChange(key, e.target.value)}
661
+ placeholder={placeholder}
662
+ />
663
+ )}
664
+ </div>
665
+ );
666
+ }
667
+
668
+ // Number → InputNumber
669
+ if (schema.type === 'number') {
670
+ return (
671
+ <div key={key}>
672
+ <Text
673
+ type="secondary"
674
+ style={{ fontSize: 13, display: 'block', marginBottom: 2 }}
675
+ >
676
+ {isRequired && <span style={{ color: '#ff4d4f' }}>* </span>}
677
+ {key}
678
+ </Text>
679
+ <InputNumber
680
+ value={value}
681
+ onChange={(v) => handleConfigChange(key, v)}
682
+ placeholder={placeholder}
683
+ style={{ width: '100%' }}
684
+ min={0}
685
+ />
686
+ </div>
687
+ );
688
+ }
689
+
690
+ // Boolean → Switch
691
+ if (schema.type === 'boolean') {
692
+ return (
693
+ <div
694
+ key={key}
695
+ style={{
696
+ display: 'flex',
697
+ justifyContent: 'space-between',
698
+ alignItems: 'center',
699
+ }}
700
+ >
701
+ <Text type="secondary" style={{ fontSize: 13 }}>
702
+ {isRequired && <span style={{ color: '#ff4d4f' }}>* </span>}
703
+ {key}
704
+ </Text>
705
+ <Switch
706
+ checked={!!value}
707
+ onChange={(checked) => handleConfigChange(key, checked)}
708
+ size="small"
709
+ />
710
+ </div>
711
+ );
712
+ }
713
+
714
+ // Array of strings → Select mode="tags"
715
+ if (schema.type === 'array' && schema.items?.type === 'string') {
716
+ return (
717
+ <div key={key}>
718
+ <Text
719
+ type="secondary"
720
+ style={{ fontSize: 13, display: 'block', marginBottom: 2 }}
721
+ >
722
+ {isRequired && <span style={{ color: '#ff4d4f' }}>* </span>}
723
+ {key}
724
+ </Text>
725
+ <Select
726
+ mode="tags"
727
+ value={Array.isArray(value) ? value : []}
728
+ onChange={(v) => handleConfigChange(key, v)}
729
+ placeholder={`输入后按 Enter 添加`}
730
+ style={{ width: '100%' }}
731
+ tokenSeparators={[',']}
732
+ />
733
+ </div>
734
+ );
735
+ }
736
+
737
+ // Array of objects (e.g. switch cases) → dynamic list
738
+ if (schema.type === 'array' && schema.items?.type === 'object') {
739
+ const items: any[] = Array.isArray(value) ? value : [];
740
+ const itemProps = schema.items.properties || {};
741
+ return (
742
+ <div key={key}>
743
+ <Text
744
+ type="secondary"
745
+ style={{ fontSize: 13, display: 'block', marginBottom: 4 }}
746
+ >
747
+ {isRequired && <span style={{ color: '#ff4d4f' }}>* </span>}
748
+ {key}
749
+ </Text>
750
+ {items.map((item, idx) => (
751
+ <div
752
+ key={idx}
753
+ style={{
754
+ display: 'flex',
755
+ gap: 4,
756
+ marginBottom: 4,
757
+ alignItems: 'center',
758
+ }}
759
+ >
760
+ {Object.keys(itemProps).map((propKey) => (
761
+ <Input
762
+ key={propKey}
763
+ value={item[propKey] ?? ''}
764
+ onChange={(e) => {
765
+ const newItems = [...items];
766
+ newItems[idx] = {
767
+ ...newItems[idx],
768
+ [propKey]: e.target.value,
769
+ };
770
+ handleConfigChange(key, newItems);
771
+ }}
772
+ placeholder={propKey}
773
+ size="small"
774
+ style={{ flex: 1 }}
775
+ />
776
+ ))}
777
+ <MinusCircleOutlined
778
+ style={{ color: '#ff4d4f', cursor: 'pointer', fontSize: 14 }}
779
+ onClick={() => {
780
+ const newItems = items.filter((_, i) => i !== idx);
781
+ handleConfigChange(key, newItems);
782
+ }}
783
+ />
784
+ </div>
785
+ ))}
786
+ <Button
787
+ type="dashed"
788
+ size="small"
789
+ icon={<PlusOutlined />}
790
+ onClick={() => {
791
+ const emptyItem: Record<string, string> = {};
792
+ for (const propKey of Object.keys(itemProps)) {
793
+ emptyItem[propKey] = '';
794
+ }
795
+ handleConfigChange(key, [...items, emptyItem]);
796
+ }}
797
+ block
798
+ >
799
+ 添加
800
+ </Button>
801
+ </div>
802
+ );
803
+ }
804
+
805
+ // Object → JSON TextArea
806
+ if (schema.type === 'object') {
807
+ const displayValue =
808
+ typeof value === 'object'
809
+ ? JSON.stringify(value, null, 2)
810
+ : (value ?? '');
811
+ return (
812
+ <div key={key}>
813
+ <Text
814
+ type="secondary"
815
+ style={{ fontSize: 13, display: 'block', marginBottom: 2 }}
816
+ >
817
+ {isRequired && <span style={{ color: '#ff4d4f' }}>* </span>}
818
+ {key}
819
+ <Text type="secondary" style={{ fontSize: 11 }}>
820
+ {' '}
821
+ (JSON)
822
+ </Text>
823
+ </Text>
824
+ <TextArea
825
+ value={displayValue === '{}' ? '' : displayValue}
826
+ onChange={(e) => {
827
+ const raw = e.target.value;
828
+ try {
829
+ handleConfigChange(key, JSON.parse(raw));
830
+ } catch {
831
+ // Keep as string if not valid JSON yet (user is typing)
832
+ handleConfigChange(key, raw);
833
+ }
834
+ }}
835
+ placeholder={`{"key": "value"}`}
836
+ autoSize={{ minRows: 2, maxRows: 5 }}
837
+ />
838
+ </div>
839
+ );
840
+ }
841
+
842
+ // Fallback: plain input
843
+ return (
844
+ <div key={key}>
845
+ <Text
846
+ type="secondary"
847
+ style={{ fontSize: 13, display: 'block', marginBottom: 2 }}
848
+ >
849
+ {isRequired && <span style={{ color: '#ff4d4f' }}>* </span>}
850
+ {key}
851
+ </Text>
852
+ <Input
853
+ value={
854
+ typeof value === 'string' ? value : JSON.stringify(value ?? '')
855
+ }
856
+ onChange={(e) => handleConfigChange(key, e.target.value)}
857
+ placeholder={placeholder}
858
+ />
859
+ </div>
860
+ );
861
+ }
862
+
863
+ return (
864
+ <div
865
+ style={{
866
+ width: 320,
867
+ borderLeft: '1px solid #f0f0f0',
868
+ background: '#fff',
869
+ display: 'flex',
870
+ flexDirection: 'column',
871
+ overflow: 'hidden',
872
+ }}
873
+ >
874
+ {/* Header */}
875
+ <div
876
+ style={{
877
+ padding: '16px 16px 12px',
878
+ borderBottom: '1px solid #f0f0f0',
879
+ display: 'flex',
880
+ justifyContent: 'space-between',
881
+ alignItems: 'flex-start',
882
+ }}
883
+ >
884
+ <Space direction="vertical" size={4}>
885
+ <Title level={5} style={{ margin: 0, fontSize: 14 }}>
886
+ 节点属性
887
+ </Title>
888
+ <Tag color="blue">{node.type}</Tag>
889
+ </Space>
890
+ <Button
891
+ type="text"
892
+ danger
893
+ icon={<DeleteOutlined />}
894
+ onClick={() => onDelete?.(node.id)}
895
+ title="删除节点"
896
+ />
897
+ </div>
898
+
899
+ {/* Tab Switcher */}
900
+ <div
901
+ style={{ padding: '12px 16px 0', borderBottom: '1px solid #f0f0f0' }}
902
+ >
903
+ <Segmented
904
+ value={activeTab}
905
+ onChange={(val) => setActiveTab(val as 'config' | 'ai')}
906
+ options={[
907
+ { label: '节点配置', value: 'config' },
908
+ { label: 'AI 生成', value: 'ai' },
909
+ ]}
910
+ block
911
+ style={{ marginBottom: 12 }}
912
+ />
913
+ </div>
914
+
915
+ {/* Form */}
916
+ <div style={{ flex: 1, overflow: 'auto', padding: 16 }}>
917
+ {activeTab === 'config' && (
918
+ <Space direction="vertical" size={12} style={{ width: '100%' }}>
919
+ {/* Instance Selector / Reference Banner */}
920
+ {!node.instanceRef && (
921
+ <div
922
+ style={{
923
+ padding: 12,
924
+ background: '#fafafa',
925
+ borderRadius: 6,
926
+ }}
927
+ >
928
+ <Text
929
+ type="secondary"
930
+ style={{ fontSize: 12, display: 'block', marginBottom: 6 }}
931
+ >
932
+ 复用已有实例
933
+ </Text>
934
+ <Select
935
+ value={selectedInstanceId ?? undefined}
936
+ onChange={(v) => setSelectedInstanceId(v)}
937
+ placeholder={
938
+ instanceLoading
939
+ ? '加载中...'
940
+ : instances.length === 0
941
+ ? '暂无可用实例'
942
+ : '选择实例...'
943
+ }
944
+ options={instances.map((i) => ({
945
+ label: i.name,
946
+ value: i.id,
947
+ }))}
948
+ style={{ width: '100%', marginBottom: 8 }}
949
+ allowClear
950
+ showSearch
951
+ loading={instanceLoading}
952
+ disabled={instances.length === 0 && !instanceLoading}
953
+ filterOption={(input, option) =>
954
+ (option?.label ?? '')
955
+ .toLowerCase()
956
+ .includes(input.toLowerCase())
957
+ }
958
+ />
959
+ {selectedInstanceId && (
960
+ <Space size={8}>
961
+ <Button
962
+ size="small"
963
+ icon={<LinkOutlined />}
964
+ onClick={handleReferenceInstance}
965
+ >
966
+ 引用(动态)
967
+ </Button>
968
+ <Button size="small" onClick={handleCopyAsTemplate}>
969
+ 复制为模板
970
+ </Button>
971
+ </Space>
972
+ )}
973
+ </div>
974
+ )}
975
+ {node.instanceRef && (
976
+ <div
977
+ style={{
978
+ padding: '10px 12px',
979
+ background: '#e6f7ff',
980
+ borderRadius: 6,
981
+ display: 'flex',
982
+ alignItems: 'center',
983
+ justifyContent: 'space-between',
984
+ }}
985
+ >
986
+ <Text style={{ fontSize: 13 }}>
987
+ 🔗 引用: {node.instanceRef.instanceName}
988
+ </Text>
989
+ <Button
990
+ size="small"
991
+ icon={<DisconnectOutlined />}
992
+ onClick={handleDetachReference}
993
+ >
994
+ 解除引用
995
+ </Button>
996
+ </div>
997
+ )}
998
+
999
+ {/* Label */}
1000
+ <div>
1001
+ <Text
1002
+ strong
1003
+ style={{ fontSize: 14, display: 'block', marginBottom: 4 }}
1004
+ >
1005
+ Label
1006
+ </Text>
1007
+ <Input
1008
+ value={label}
1009
+ onChange={(e) => handleLabelChange(e.target.value)}
1010
+ />
1011
+ </div>
1012
+
1013
+ {/* Schema-driven config fields */}
1014
+ {Object.keys(properties).length > 0 && (
1015
+ <div>
1016
+ <Text
1017
+ strong
1018
+ style={{ fontSize: 14, display: 'block', marginBottom: 8 }}
1019
+ >
1020
+ Config
1021
+ </Text>
1022
+ {node.instanceRef ? (
1023
+ <Space
1024
+ direction="vertical"
1025
+ size={8}
1026
+ style={{ width: '100%' }}
1027
+ >
1028
+ {(() => {
1029
+ const inst = instances.find(
1030
+ (i) => i.id === node.instanceRef?.instanceId,
1031
+ );
1032
+ const refConfig = inst?.config ?? {};
1033
+ return Object.entries(properties).map(([key]) => (
1034
+ <div key={key}>
1035
+ <Text
1036
+ type="secondary"
1037
+ style={{
1038
+ fontSize: 13,
1039
+ display: 'block',
1040
+ marginBottom: 2,
1041
+ }}
1042
+ >
1043
+ {key}
1044
+ </Text>
1045
+ <Text style={{ fontSize: 13, color: '#595959' }}>
1046
+ {refConfig[key] !== undefined &&
1047
+ refConfig[key] !== ''
1048
+ ? typeof refConfig[key] === 'object'
1049
+ ? JSON.stringify(refConfig[key])
1050
+ : String(refConfig[key])
1051
+ : '—'}
1052
+ </Text>
1053
+ </div>
1054
+ ));
1055
+ })()}
1056
+ </Space>
1057
+ ) : (
1058
+ <Space
1059
+ direction="vertical"
1060
+ size={10}
1061
+ style={{ width: '100%' }}
1062
+ >
1063
+ {Object.entries(properties).map(([key, propSchema]) =>
1064
+ renderConfigField(key, propSchema),
1065
+ )}
1066
+ </Space>
1067
+ )}
1068
+ </div>
1069
+ )}
1070
+
1071
+ {/* Save section */}
1072
+ {!node.instanceRef && (
1073
+ <div>
1074
+ <Checkbox
1075
+ checked={alsoSaveAsInstance}
1076
+ onChange={(e) => setAlsoSaveAsInstance(e.target.checked)}
1077
+ style={{ marginBottom: 8 }}
1078
+ >
1079
+ <Text style={{ fontSize: 13 }}>同时保存为实例(模版)</Text>
1080
+ </Checkbox>
1081
+ {alsoSaveAsInstance && (
1082
+ <Space
1083
+ direction="vertical"
1084
+ size={6}
1085
+ style={{ width: '100%', marginBottom: 8 }}
1086
+ >
1087
+ <Input
1088
+ value={newInstanceName}
1089
+ onChange={(e) => setNewInstanceName(e.target.value)}
1090
+ placeholder="实例名称(必填)"
1091
+ size="small"
1092
+ />
1093
+ <Input
1094
+ value={newInstanceDesc}
1095
+ onChange={(e) => setNewInstanceDesc(e.target.value)}
1096
+ placeholder="描述(可选)"
1097
+ size="small"
1098
+ />
1099
+ </Space>
1100
+ )}
1101
+ <Button
1102
+ type="primary"
1103
+ block
1104
+ loading={saveAsLoading}
1105
+ disabled={alsoSaveAsInstance && !newInstanceName.trim()}
1106
+ onClick={handleSave}
1107
+ >
1108
+ 保存修改
1109
+ </Button>
1110
+ </div>
1111
+ )}
1112
+ </Space>
1113
+ )}
1114
+
1115
+ {activeTab === 'ai' && (
1116
+ <div>
1117
+ <Text
1118
+ type="secondary"
1119
+ style={{ fontSize: 13, display: 'block', marginBottom: 8 }}
1120
+ >
1121
+ 描述复杂修改,AI 将更新此节点
1122
+ </Text>
1123
+ <div style={{ display: 'flex', gap: 4 }}>
1124
+ <TextArea
1125
+ value={prompt}
1126
+ onChange={(e) => setPrompt(e.target.value)}
1127
+ placeholder="例: 将超时改为30分钟..."
1128
+ autoSize={{ minRows: 4, maxRows: 8 }}
1129
+ onPressEnter={(e) => {
1130
+ if (e.ctrlKey) handleAiSubmit();
1131
+ }}
1132
+ />
1133
+ </div>
1134
+ <Button
1135
+ type="default"
1136
+ icon={<SendOutlined />}
1137
+ loading={aiLoading}
1138
+ onClick={handleAiSubmit}
1139
+ disabled={!prompt.trim()}
1140
+ style={{ marginTop: 8 }}
1141
+ block
1142
+ >
1143
+ 发送
1144
+ </Button>
1145
+ </div>
1146
+ )}
1147
+ </div>
1148
+ </div>
1149
+ );
1150
+ }