@bpmn-nova/studio 0.3.0-preview → 0.3.2-preview

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 (63) hide show
  1. package/README.md +245 -20
  2. package/dist/canvas.js +7 -4
  3. package/dist/context-menu.js +2 -2
  4. package/dist/controller.js +84 -6
  5. package/dist/index.d.ts +138 -38
  6. package/dist/index.js +12 -11
  7. package/dist/interactions.js +1 -1
  8. package/dist/modules/bpmn-model/index.d.ts +11 -0
  9. package/dist/modules/bpmn-model/index.js +703 -0
  10. package/dist/modules/core/containment.js +590 -0
  11. package/dist/modules/core/gateway.js +72 -0
  12. package/dist/modules/core/history.js +32 -0
  13. package/dist/modules/core/index.d.ts +268 -0
  14. package/dist/modules/core/index.js +7 -0
  15. package/dist/modules/core/layout.js +644 -0
  16. package/dist/modules/core/model.js +287 -0
  17. package/dist/modules/core/runtime-transition-route.js +360 -0
  18. package/dist/modules/core/scope.js +99 -0
  19. package/dist/modules/designer/index.d.ts +79 -0
  20. package/dist/modules/designer/index.js +607 -0
  21. package/dist/modules/engine-activiti/index.d.ts +19 -0
  22. package/dist/modules/engine-activiti/index.js +160 -0
  23. package/dist/modules/engine-flowable/index.d.ts +19 -0
  24. package/dist/modules/engine-flowable/index.js +160 -0
  25. package/dist/modules/export-svg/index.d.ts +112 -0
  26. package/dist/modules/export-svg/index.js +2 -0
  27. package/dist/modules/export-svg/preview.js +327 -0
  28. package/dist/modules/export-svg/render.js +718 -0
  29. package/dist/modules/icons/index.d.ts +24 -0
  30. package/dist/modules/icons/index.js +264 -0
  31. package/dist/modules/palette/index.d.ts +74 -0
  32. package/dist/modules/palette/index.js +99 -0
  33. package/dist/modules/palette/panel.js +99 -0
  34. package/dist/modules/properties/index.d.ts +20 -0
  35. package/dist/modules/properties/index.js +19 -0
  36. package/dist/modules/properties-activiti/index.d.ts +3 -0
  37. package/dist/modules/properties-activiti/index.js +97 -0
  38. package/dist/modules/properties-bpmn/index.d.ts +3 -0
  39. package/dist/modules/properties-bpmn/index.js +518 -0
  40. package/dist/modules/properties-core/index.d.ts +124 -0
  41. package/dist/modules/properties-core/index.js +312 -0
  42. package/dist/modules/properties-flowable/index.d.ts +3 -0
  43. package/dist/modules/properties-flowable/index.js +114 -0
  44. package/dist/modules/properties-renderer/index.d.ts +25 -0
  45. package/dist/modules/properties-renderer/index.js +491 -0
  46. package/dist/modules/renderer-svg/index.d.ts +118 -0
  47. package/dist/modules/renderer-svg/index.js +1460 -0
  48. package/dist/modules/runtime/index.d.ts +169 -0
  49. package/dist/modules/runtime/index.js +535 -0
  50. package/dist/modules/theme/index.d.ts +95 -0
  51. package/dist/modules/theme/index.js +368 -0
  52. package/dist/modules/viewer/index.d.ts +265 -0
  53. package/dist/modules/viewer/index.js +1011 -0
  54. package/dist/modules/viewer/runtime-content.js +123 -0
  55. package/dist/modules/viewer/runtime-details-motion.js +228 -0
  56. package/dist/modules/viewer/runtime-trace.js +574 -0
  57. package/dist/modules/viewer/timeline.js +276 -0
  58. package/dist/selection-layout.js +1 -1
  59. package/dist/shell.js +210 -26
  60. package/dist/styles.css +116 -7
  61. package/llms-full.txt +3082 -0
  62. package/llms.txt +225 -0
  63. package/package.json +39 -16
@@ -0,0 +1,535 @@
1
+ const TERMINAL_PROCESS_STATUSES = new Set(['completed', 'terminated']);
2
+ const ABNORMAL_TRANSITION_TYPES = new Set(['reject', 'return']);
3
+ const APPROVAL_ACTION_LABELS = {
4
+ submit: '提交', approve: '通过', reject: '驳回', return: '退回',
5
+ 'add-sign': '加签', transfer: '转办', delegate: '委派', withdraw: '撤回', comment: '备注', skip: '跳过',
6
+ };
7
+ const OUTCOME_ACTION_TYPES = {
8
+ submitted: 'submit', approved: 'approve', rejected: 'reject', returned: 'return', skipped: 'skip',
9
+ };
10
+
11
+ function normalizeParticipant(record = {}) {
12
+ if (record.participant?.name) return { ...record.participant };
13
+ if (record.assignee) return { id: record.assigneeId || undefined, name: String(record.assignee) };
14
+ return null;
15
+ }
16
+
17
+ function recordOrder(record, index) {
18
+ const stamp = record.startTime || record.endTime;
19
+ const parsed = stamp ? Date.parse(stamp.replace(' ', 'T')) : Number.NaN;
20
+ return Number.isFinite(parsed) ? parsed : index;
21
+ }
22
+
23
+ function runtimeOrder(value, index) {
24
+ const stamp = value?.occurredAt || value?.time || value?.endTime || value?.startTime;
25
+ const parsed = stamp ? Date.parse(String(stamp).replace(' ', 'T')) : Number.NaN;
26
+ return Number.isFinite(parsed) ? parsed : index;
27
+ }
28
+
29
+ function normalizeRuntimeAsset(asset) {
30
+ if (!asset?.id) return null;
31
+ return {
32
+ id: String(asset.id),
33
+ name: String(asset.name || asset.id),
34
+ mediaType: String(asset.mediaType || 'application/octet-stream'),
35
+ ...(Number.isFinite(Number(asset.size)) ? { size: Number(asset.size) } : {}),
36
+ ...(Number.isFinite(Number(asset.width)) ? { width: Number(asset.width) } : {}),
37
+ ...(Number.isFinite(Number(asset.height)) ? { height: Number(asset.height) } : {}),
38
+ };
39
+ }
40
+
41
+ function normalizeApprovalContent(content, fallbackText = '') {
42
+ const source = typeof content === 'string' ? { plainText: content } : (content || {});
43
+ const assets = Array.isArray(source.assets) ? source.assets.map(normalizeRuntimeAsset).filter(Boolean) : [];
44
+ const blocks = Array.isArray(source.blocks)
45
+ ? source.blocks.filter((block) => block && typeof block === 'object').map((block) => {
46
+ if (block.type === 'paragraph') return { type: 'paragraph', text: String(block.text || '') };
47
+ if (block.type === 'image') return { type: 'image', assetId: String(block.assetId || ''), ...(block.alt ? { alt: String(block.alt) } : {}) };
48
+ if (block.type === 'file') return { type: 'file', assetId: String(block.assetId || '') };
49
+ return { type: String(block.type || 'unknown') };
50
+ })
51
+ : [];
52
+ const paragraphText = blocks.filter((block) => block.type === 'paragraph').map((block) => block.text.trim()).filter(Boolean).join('\n');
53
+ const plainText = String(source.plainText ?? fallbackText ?? paragraphText).trim() || paragraphText;
54
+ if (!blocks.length && plainText) blocks.push({ type: 'paragraph', text: plainText });
55
+ const referencedAssets = new Set(blocks.filter((block) => ['image', 'file'].includes(block.type)).map((block) => block.assetId));
56
+ assets.forEach((asset) => {
57
+ if (referencedAssets.has(asset.id)) return;
58
+ blocks.push({ type: asset.mediaType.startsWith('image/') ? 'image' : 'file', assetId: asset.id });
59
+ });
60
+ return { plainText, blocks, assets };
61
+ }
62
+
63
+ function normalizeApprovalAction(action, index) {
64
+ const type = String(action?.type || 'comment');
65
+ const actor = action?.actor?.name ? { ...action.actor, name: String(action.actor.name) } : null;
66
+ const targets = Array.isArray(action?.targets)
67
+ ? action.targets.filter((target) => target?.name).map((target) => ({ ...target, name: String(target.name) }))
68
+ : [];
69
+ return {
70
+ id: String(action?.id || `runtime-action-${index + 1}`),
71
+ type,
72
+ ...(action?.label ? { label: String(action.label) } : {}),
73
+ elementId: String(action?.elementId || ''),
74
+ visitId: String(action?.visitId || action?.activityId || action?.id || `runtime-action-${index + 1}`),
75
+ ...(action?.activityId ? { activityId: String(action.activityId) } : {}),
76
+ ...(actor ? { actor } : {}),
77
+ occurredAt: String(action?.occurredAt || action?.time || ''),
78
+ ...(targets.length ? { targets } : {}),
79
+ ...(action?.targetElementId ? { targetElementId: String(action.targetElementId) } : {}),
80
+ content: normalizeApprovalContent(action?.content),
81
+ ...(action?.metadata && typeof action.metadata === 'object' ? { metadata: { ...action.metadata } } : {}),
82
+ };
83
+ }
84
+
85
+ function legacyActionForRecord(record, index) {
86
+ const comment = String(record.comment || '').trim();
87
+ const type = OUTCOME_ACTION_TYPES[record.outcome] || (record.outcome && record.outcome !== 'pending' ? String(record.outcome) : 'comment');
88
+ if (!comment && (!record.outcome || record.outcome === 'pending')) return null;
89
+ return normalizeApprovalAction({
90
+ id: `legacy-activity-action:${record.id || index + 1}`,
91
+ type,
92
+ activityId: record.id || undefined,
93
+ elementId: record.elementId,
94
+ visitId: record.visitId || record.multiInstanceId || record.id || `record-${index + 1}`,
95
+ actor: normalizeParticipant(record) || undefined,
96
+ occurredAt: record.endTime || record.startTime || '',
97
+ content: comment ? { plainText: comment } : undefined,
98
+ }, index);
99
+ }
100
+
101
+ function actionText(action) {
102
+ return String(action?.content?.plainText || '').trim();
103
+ }
104
+
105
+ function approvalActionLabel(action) {
106
+ return action?.label || APPROVAL_ACTION_LABELS[action?.type] || action?.type || '审批操作';
107
+ }
108
+
109
+ function summarizeApprovalActions(actions = []) {
110
+ const latestAction = [...actions].reverse().find((action) => action.plainText || action.assetCount) || actions.at(-1) || null;
111
+ const assetSummary = latestAction
112
+ ? [latestAction.imageCount ? `${latestAction.imageCount} 张图片` : '', latestAction.fileCount ? `${latestAction.fileCount} 个附件` : ''].filter(Boolean).join(' · ')
113
+ : '';
114
+ return {
115
+ latestAction,
116
+ actionText: latestAction?.plainText || '',
117
+ actionSummary: latestAction ? [latestAction.label, latestAction.plainText, assetSummary].filter(Boolean).join(' · ') : '',
118
+ imageCount: actions.reduce((sum, action) => sum + action.imageCount, 0),
119
+ fileCount: actions.reduce((sum, action) => sum + action.fileCount, 0),
120
+ assetCount: actions.reduce((sum, action) => sum + action.assetCount, 0),
121
+ };
122
+ }
123
+
124
+ function groupVisits(records = []) {
125
+ const groups = new Map();
126
+ records.forEach((record, index) => {
127
+ const key = record.visitId
128
+ ? `visit:${record.visitId}`
129
+ : record.multiInstanceId
130
+ ? `multi:${record.multiInstanceId}`
131
+ : `record:${record.id || index}`;
132
+ if (!groups.has(key)) groups.set(key, { id: record.visitId || record.multiInstanceId || record.id || String(index), records: [], order: recordOrder(record, index) });
133
+ const visit = groups.get(key);
134
+ visit.records.push(record);
135
+ visit.order = Math.max(visit.order, recordOrder(record, index));
136
+ });
137
+ return [...groups.values()].sort((a, b) => a.order - b.order).map((visit, index) => ({ ...visit, round: index + 1 }));
138
+ }
139
+
140
+ function resolveVisitStatus(records = []) {
141
+ if (!records.length) return 'idle';
142
+ if (records.some((item) => item.status === 'failed')) return 'failed';
143
+ if (records.some((item) => item.status === 'active')) return 'active';
144
+ if (records.every((item) => item.status === 'skipped')) return 'skipped';
145
+ if (records.some((item) => item.status === 'cancelled') && !records.some((item) => item.status === 'completed')) return 'cancelled';
146
+ return 'completed';
147
+ }
148
+
149
+ function uniqueParticipants(records = []) {
150
+ const seen = new Set();
151
+ const result = [];
152
+ for (const record of records) {
153
+ const participant = normalizeParticipant(record);
154
+ if (!participant?.name) continue;
155
+ const key = participant.id || participant.name;
156
+ if (seen.has(key)) continue;
157
+ seen.add(key);
158
+ result.push(participant);
159
+ }
160
+ return result;
161
+ }
162
+
163
+ function participantSummary(participants) {
164
+ if (!participants.length) return '';
165
+ const visible = participants.slice(0, 2).map((item) => item.name).join('、');
166
+ return participants.length > 2 ? `${visible} +${participants.length - 2}` : visible;
167
+ }
168
+
169
+ function candidateSummary(node) {
170
+ const properties = node?.properties || {};
171
+ const candidate = properties.candidateGroups || properties.candidateUsers;
172
+ if (!candidate) return '';
173
+ const value = Array.isArray(candidate) ? candidate.join('、') : String(candidate);
174
+ return value ? `待领取 · ${value}` : '';
175
+ }
176
+
177
+ function baseStatusLabel(status) {
178
+ return {
179
+ completed: '已完成', active: '处理中', failed: '失败', cancelled: '已取消', skipped: '未经过', idle: '未到达', rejected: '已驳回',
180
+ }[status] || status;
181
+ }
182
+
183
+ function transitionOrder(transition, index) {
184
+ const stamp = transition.occurredAt || transition.time;
185
+ const parsed = stamp ? Date.parse(stamp.replace(' ', 'T')) : Number.NaN;
186
+ return Number.isFinite(parsed) ? parsed : index;
187
+ }
188
+
189
+ function edgeVisitOrder(visit, index) {
190
+ const stamp = visit.occurredAt || visit.time;
191
+ const parsed = stamp ? Date.parse(stamp.replace(' ', 'T')) : Number.NaN;
192
+ return Number.isFinite(parsed) ? parsed : index;
193
+ }
194
+
195
+ function inferInvalidatedPath(model, runtime, transition) {
196
+ const source = model?.nodes?.find((node) => node.id === transition.sourceElementId);
197
+ const target = model?.nodes?.find((node) => node.id === transition.targetElementId);
198
+ if (!source || !target) return { activityIds: [], edgeIds: [], issues: ['驳回来源或目标节点不存在,无法推断失效路径。'] };
199
+ const rootScope = model.id;
200
+ if ((source.scopeId || rootScope) !== (target.scopeId || rootScope)) return { activityIds: [], edgeIds: [], issues: ['驳回跨越流程作用域,未自动重置路径。'] };
201
+ const visited = new Set(runtime.visitedEdges);
202
+ const edges = (model.edges || []).filter((edge) => visited.has(edge.id) && (edge.scopeId || rootScope) === (source.scopeId || rootScope));
203
+ const outgoing = new Map();
204
+ for (const edge of edges) {
205
+ if (!outgoing.has(edge.source)) outgoing.set(edge.source, []);
206
+ outgoing.get(edge.source).push(edge);
207
+ }
208
+ const paths = [];
209
+ const walk = (nodeId, nodeIds, edgeIds) => {
210
+ if (paths.length > 1 || nodeIds.length > (model.nodes?.length || 0) + 1) return;
211
+ if (nodeId === source.id) {
212
+ paths.push({ nodeIds, edgeIds });
213
+ return;
214
+ }
215
+ for (const edge of outgoing.get(nodeId) || []) {
216
+ if (nodeIds.includes(edge.target)) continue;
217
+ walk(edge.target, [...nodeIds, edge.target], [...edgeIds, edge.id]);
218
+ }
219
+ };
220
+ walk(target.id, [target.id], []);
221
+ if (paths.length !== 1) {
222
+ return {
223
+ activityIds: [], edgeIds: [],
224
+ issues: [paths.length ? '驳回路径存在多个候选分支,未自动重置路径。' : '未找到驳回目标到来源的已访问路径。'],
225
+ };
226
+ }
227
+ return { activityIds: paths[0].nodeIds.slice(1), edgeIds: paths[0].edgeIds, issues: [] };
228
+ }
229
+
230
+ export function normalizeRuntime(snapshot = {}) {
231
+ snapshot = snapshot || {};
232
+ const activities = Array.isArray(snapshot.activities)
233
+ ? snapshot.activities.map((record) => ({ ...record, participant: normalizeParticipant(record) || undefined }))
234
+ : [];
235
+ const explicitActions = Array.isArray(snapshot.actions)
236
+ ? snapshot.actions.map(normalizeApprovalAction)
237
+ : [];
238
+ const actions = [...explicitActions];
239
+ const explicitActivityIds = new Set(explicitActions.map((action) => action.activityId).filter(Boolean));
240
+ activities.forEach((record, index) => {
241
+ if (record.id && explicitActivityIds.has(record.id)) return;
242
+ const action = legacyActionForRecord(record, index);
243
+ if (action) actions.push(action);
244
+ });
245
+ const transitions = Array.isArray(snapshot.transitions)
246
+ ? snapshot.transitions.map((transition, index) => ({ id: transition.id || `runtime-transition-${index + 1}`, type: transition.type || 'forward', ...transition }))
247
+ : [];
248
+ transitions.forEach((transition, index) => {
249
+ if (transition.actionId && actions.some((action) => action.id === transition.actionId)) return;
250
+ if (!['reject', 'return', 'skip'].includes(transition.type)) return;
251
+ const comment = String(transition.comment || '').trim();
252
+ const sourceRecord = activities.find((record) => record.id === transition.sourceActivityId);
253
+ const reusable = actions.find((action) => action.activityId && action.activityId === transition.sourceActivityId
254
+ && action.type === transition.type && (!comment || actionText(action) === comment));
255
+ if (reusable) {
256
+ transition.actionId = reusable.id;
257
+ return;
258
+ }
259
+ const action = normalizeApprovalAction({
260
+ id: `legacy-transition-action:${transition.id}`,
261
+ type: transition.type,
262
+ activityId: transition.sourceActivityId,
263
+ elementId: transition.sourceElementId,
264
+ visitId: sourceRecord?.visitId || sourceRecord?.multiInstanceId || sourceRecord?.id || `transition:${transition.id}`,
265
+ actor: transition.operator ? { name: String(transition.operator) } : undefined,
266
+ occurredAt: transition.occurredAt || transition.time || '',
267
+ targetElementId: transition.targetElementId,
268
+ content: comment ? { plainText: comment } : undefined,
269
+ }, activities.length + actions.length + index);
270
+ actions.push(action);
271
+ transition.actionId = action.id;
272
+ });
273
+ const edgeVisits = Array.isArray(snapshot.edgeVisits)
274
+ ? snapshot.edgeVisits.map((visit, index) => ({ id: visit.id || `runtime-edge-visit-${index + 1}`, status: visit.status || 'effective', ...visit }))
275
+ : [];
276
+ const orderedActions = actions
277
+ .map((action, index) => ({ action, order: runtimeOrder(action, index), index }))
278
+ .sort((a, b) => (a.order - b.order) || (a.index - b.index))
279
+ .map(({ action }) => action);
280
+ return {
281
+ ...snapshot,
282
+ processInstanceId: snapshot.processInstanceId || '',
283
+ status: snapshot.status || 'running',
284
+ activities,
285
+ actions: orderedActions,
286
+ visitedEdges: Array.isArray(snapshot.visitedEdges) ? [...snapshot.visitedEdges] : [],
287
+ transitions,
288
+ edgeVisits,
289
+ };
290
+ }
291
+
292
+ export function activityState(runtime, elementId) {
293
+ if (!runtime) return { status: 'idle', records: [], latestRecords: [], visits: [] };
294
+ const records = runtime.activities.filter((item) => item.elementId === elementId);
295
+ const visits = groupVisits(records);
296
+ const latestRecords = visits.at(-1)?.records || [];
297
+ return { status: resolveVisitStatus(latestRecords), records, latestRecords, visits };
298
+ }
299
+
300
+ export function createRuntimePresentation({ model, runtime, appearance = null } = {}) {
301
+ const normalized = normalizeRuntime(runtime);
302
+ const nodes = new Map((model?.nodes || []).map((node) => [node.id, node]));
303
+ const activityStates = new Map((model?.nodes || []).map((node) => [node.id, activityState(normalized, node.id)]));
304
+ const nodePresentations = new Map();
305
+ const actionPresentations = normalized.actions.map((action, index) => {
306
+ const assets = action.content.assets || [];
307
+ const referenced = (action.content.blocks || []).filter((block) => ['image', 'file'].includes(block.type));
308
+ const imageIds = new Set(referenced.filter((block) => block.type === 'image').map((block) => block.assetId));
309
+ const fileIds = new Set(referenced.filter((block) => block.type === 'file').map((block) => block.assetId));
310
+ const images = assets.filter((asset) => imageIds.has(asset.id) || (!referenced.length && asset.mediaType.startsWith('image/')));
311
+ const files = assets.filter((asset) => fileIds.has(asset.id) || (!referenced.length && !asset.mediaType.startsWith('image/')));
312
+ const baseAction = {
313
+ ...action,
314
+ explicitLabel: action.label || null,
315
+ label: approvalActionLabel(action),
316
+ plainText: action.content.plainText || '',
317
+ images,
318
+ files,
319
+ imageCount: images.length,
320
+ fileCount: files.length,
321
+ assetCount: images.length + files.length,
322
+ order: runtimeOrder(action, index),
323
+ };
324
+ const resolvedAppearance = appearance?.resolveAction?.(baseAction);
325
+ return resolvedAppearance
326
+ ? { ...baseAction, label: resolvedAppearance.label, iconId: resolvedAppearance.iconId, tone: resolvedAppearance.tone }
327
+ : baseAction;
328
+ });
329
+ const actionMap = new Map(actionPresentations.map((action) => [action.id, action]));
330
+ const rawTransitions = normalized.transitions
331
+ .map((transition, index) => ({ ...transition, order: transitionOrder(transition, index) }))
332
+ .sort((a, b) => a.order - b.order);
333
+ const transitionPresentations = rawTransitions.map((transition) => {
334
+ const targetName = nodes.get(transition.targetElementId)?.name || transition.targetElementId || '';
335
+ const labelPrefix = transition.type === 'reject' ? '驳回至' : transition.type === 'return' ? '退回至' : transition.type === 'skip' ? '跳过' : '流转至';
336
+ const targetState = activityStates.get(transition.targetElementId);
337
+ const targetRecordsAfter = (targetState?.records || []).filter((record, index) => recordOrder(record, index) > transition.order);
338
+ const targetStatusAfter = resolveVisitStatus(groupVisits(targetRecordsAfter).at(-1)?.records || []);
339
+ const state = transition.state || (ABNORMAL_TRANSITION_TYPES.has(transition.type)
340
+ ? (targetRecordsAfter.length && targetStatusAfter !== 'active' ? 'resolved' : 'active')
341
+ : 'resolved');
342
+ const hasExplicitInvalidation = Array.isArray(transition.invalidatedActivityIds) || Array.isArray(transition.invalidatedEdgeIds);
343
+ const invalidation = hasExplicitInvalidation
344
+ ? { activityIds: [...(transition.invalidatedActivityIds || [])], edgeIds: [...(transition.invalidatedEdgeIds || [])], issues: [] }
345
+ : ABNORMAL_TRANSITION_TYPES.has(transition.type)
346
+ ? inferInvalidatedPath(model, normalized, transition)
347
+ : { activityIds: [], edgeIds: [], issues: [] };
348
+ return {
349
+ ...transition,
350
+ action: actionMap.get(transition.actionId) || null,
351
+ sourceName: nodes.get(transition.sourceElementId)?.name || transition.sourceElementId || '',
352
+ targetName,
353
+ label: targetName ? `${labelPrefix}:${targetName}` : labelPrefix,
354
+ state,
355
+ visible: ABNORMAL_TRANSITION_TYPES.has(transition.type) && state === 'active',
356
+ resolvedAt: transition.resolvedAt || (state === 'resolved' ? targetRecordsAfter.map((record) => record.endTime).filter(Boolean).at(-1) : undefined),
357
+ invalidatedActivityIds: invalidation.activityIds,
358
+ invalidatedEdgeIds: invalidation.edgeIds,
359
+ issues: invalidation.issues,
360
+ };
361
+ });
362
+ const latestAbnormal = [...transitionPresentations].reverse().find((item) => ABNORMAL_TRANSITION_TYPES.has(item.type));
363
+ transitionPresentations.forEach((transition) => { transition.latest = transition === latestAbnormal; });
364
+
365
+ const invalidatedNodes = new Map();
366
+ const invalidatedEdges = new Map();
367
+ const hasActivityAfter = (elementId, transition) => (activityStates.get(elementId)?.records || [])
368
+ .some((record, index) => recordOrder(record, index) > transition.order);
369
+ const hasEdgeVisitAfter = (edgeId, transition) => {
370
+ const edgeVisits = normalized.edgeVisits.filter((visit) => visit.edgeId === edgeId);
371
+ if (edgeVisits.length) return edgeVisits.some((visit, index) => visit.status !== 'superseded' && edgeVisitOrder(visit, index) > transition.order);
372
+ const edge = model?.edges?.find((item) => item.id === edgeId);
373
+ return edge ? hasActivityAfter(edge.target, transition) : false;
374
+ };
375
+ for (const transition of transitionPresentations.filter((item) => ABNORMAL_TRANSITION_TYPES.has(item.type))) {
376
+ for (const elementId of transition.invalidatedActivityIds) if (!hasActivityAfter(elementId, transition)) invalidatedNodes.set(elementId, transition);
377
+ for (const edgeId of transition.invalidatedEdgeIds) if (!hasEdgeVisitAfter(edgeId, transition)) invalidatedEdges.set(edgeId, transition);
378
+ }
379
+
380
+ for (const node of model?.nodes || []) {
381
+ const state = activityStates.get(node.id);
382
+ const actions = actionPresentations.filter((action) => action.elementId === node.id);
383
+ const visits = state.visits.map((visit) => {
384
+ const visitActions = actions.filter((action) => action.visitId === visit.id);
385
+ return { ...visit, actions: visitActions, ...summarizeApprovalActions(visitActions) };
386
+ });
387
+ const latestVisit = visits.at(-1) || null;
388
+ const latestRecords = latestVisit?.records || [];
389
+ const participants = uniqueParticipants(latestRecords);
390
+ const approvalMode = latestRecords.find((item) => item.approvalMode)?.approvalMode || null;
391
+ const multiInstanceMode = latestRecords.find((item) => item.multiInstanceMode)?.multiInstanceMode || null;
392
+ const declaredTotal = latestRecords.reduce((max, item) => Math.max(max, Number(item.totalInstances) || 0), 0);
393
+ const total = Math.max(declaredTotal, latestRecords.length, participants.length);
394
+ const completed = latestRecords.filter((item) => item.status === 'completed').length;
395
+ const required = latestRecords.reduce((max, item) => Math.max(max, Number(item.requiredInstances) || 0), 0) || (approvalMode === 'any' ? 1 : total);
396
+ const inboundReentry = transitionPresentations.some((item) => ABNORMAL_TRANSITION_TYPES.has(item.type) && item.targetElementId === node.id);
397
+ const latestSourceRejection = [...transitionPresentations].reverse().find((item) => item.type === 'reject' && item.sourceElementId === node.id);
398
+ const rejectionSource = latestRecords.some((item) => item.outcome === 'rejected')
399
+ || (latestSourceRejection && (!latestVisit || latestSourceRejection.order >= latestVisit.order));
400
+ const skipped = !state.records.length && transitionPresentations.some((item) => item.type === 'skip' && item.targetElementId === node.id);
401
+ let status = skipped ? 'skipped' : state.status;
402
+ if (rejectionSource && status === 'completed') status = 'rejected';
403
+ const isReentry = inboundReentry && Boolean(latestVisit) && (latestVisit.round > 1 || status === 'active');
404
+ const superseded = invalidatedNodes.has(node.id);
405
+ const pathStatus = superseded || status === 'rejected' ? 'idle' : status;
406
+
407
+ let statusLabel = baseStatusLabel(status);
408
+ if (isReentry && status === 'active') statusLabel = '重新审批';
409
+ else if (status === 'active' || status === 'completed') {
410
+ if (approvalMode === 'all') statusLabel = `${multiInstanceMode === 'sequential' ? '顺序会签' : '会签'} ${completed}/${total || required}`;
411
+ else if (approvalMode === 'any') statusLabel = `或签 ${completed}/${total || required}`;
412
+ else if (participants.length > 1) statusLabel = `多人审批 ${completed}/${total}`;
413
+ }
414
+
415
+ let summary = participantSummary(participants);
416
+ const isApprovalTask = ['userTask', 'callActivity'].includes(node.type);
417
+ const isAutomatedTask = ['serviceTask', 'scriptTask', 'businessRuleTask', 'sendTask', 'receiveTask'].includes(node.type);
418
+ if (!summary && isApprovalTask) {
419
+ if (status === 'active') summary = candidateSummary(node) || '待分配审批人';
420
+ else if (status === 'skipped') summary = '分支未经过';
421
+ else if (!state.records.length) summary = TERMINAL_PROCESS_STATUSES.has(normalized.status) ? '流程未经过' : '尚未流转';
422
+ } else if (!summary && isAutomatedTask) {
423
+ summary = status === 'active' ? '执行中' : status === 'completed' ? '执行完成' : status === 'failed' ? '执行失败' : status === 'skipped' ? '分支未经过' : '等待执行';
424
+ }
425
+
426
+ const relatedTransitions = transitionPresentations.filter((item) => item.sourceElementId === node.id || item.targetElementId === node.id);
427
+ const actionSummary = summarizeApprovalActions(actions);
428
+ nodePresentations.set(node.id, {
429
+ elementId: node.id, status, pathStatus, superseded, statusLabel, summary, fullSummary: participants.map((item) => item.name).join('、') || summary,
430
+ participants, approvalMode, multiInstanceMode, completed, total, required, round: latestVisit?.round || 0,
431
+ records: state.records, latestRecords, visits, transitions: relatedTransitions, actions, ...actionSummary,
432
+ isReentry,
433
+ hasDetails: Boolean(state.records.length || actions.length || relatedTransitions.length),
434
+ });
435
+ }
436
+
437
+ return {
438
+ runtime: normalized,
439
+ getNode(elementId) {
440
+ return nodePresentations.get(elementId) || {
441
+ elementId, status: 'idle', pathStatus: 'idle', superseded: false, statusLabel: '未到达', summary: '', fullSummary: '', participants: [], approvalMode: null,
442
+ multiInstanceMode: null, completed: 0, total: 0, required: 0, round: 0, records: [], latestRecords: [], visits: [], transitions: [], actions: [], latestAction: null,
443
+ actionText: '', actionSummary: '', imageCount: 0, fileCount: 0, assetCount: 0, isReentry: false, hasDetails: false,
444
+ };
445
+ },
446
+ getEdge(edgeId) {
447
+ const edgeVisits = normalized.edgeVisits.filter((visit) => visit.edgeId === edgeId);
448
+ const latestVisit = edgeVisits
449
+ .map((visit, index) => ({ visit, order: edgeVisitOrder(visit, index) }))
450
+ .sort((a, b) => a.order - b.order)
451
+ .at(-1)?.visit;
452
+ const visited = normalized.visitedEdges.includes(edgeId) || edgeVisits.length > 0;
453
+ const superseded = invalidatedEdges.has(edgeId) || latestVisit?.status === 'superseded';
454
+ const status = !visited || superseded ? 'idle' : 'completed';
455
+ return { edgeId, status, visited, historicallyVisited: visited, superseded };
456
+ },
457
+ getAction(actionId) { return actionMap.get(actionId) || null; },
458
+ getActions(elementId = null) { return elementId ? actionPresentations.filter((action) => action.elementId === elementId) : [...actionPresentations]; },
459
+ getTransition(transitionId) { return transitionPresentations.find((item) => item.id === transitionId) || null; },
460
+ getTransitions() { return transitionPresentations; },
461
+ };
462
+ }
463
+
464
+ export function demoRuntime() {
465
+ return normalizeRuntime({
466
+ processInstanceId: 'PI-DEMO-20260824-001',
467
+ status: 'running',
468
+ visitedEdges: ['Flow_Start_Submit', 'Flow_Submit_Manager', 'Flow_Manager_Gateway', 'Flow_To_Director'],
469
+ edgeVisits: [
470
+ { id: 'edge-start-submit-1', edgeId: 'Flow_Start_Submit', occurredAt: '2026-08-24 09:00' },
471
+ { id: 'edge-submit-manager-1', edgeId: 'Flow_Submit_Manager', occurredAt: '2026-08-24 09:08' },
472
+ { id: 'edge-manager-gateway-1', edgeId: 'Flow_Manager_Gateway', occurredAt: '2026-08-24 10:12' },
473
+ { id: 'edge-gateway-director-1', edgeId: 'Flow_To_Director', occurredAt: '2026-08-24 10:12' },
474
+ ],
475
+ activities: [
476
+ { id: 'act-start-1', elementId: 'StartEvent_1', visitId: 'visit-start-1', status: 'completed', startTime: '2026-08-24 09:00', endTime: '2026-08-24 09:00' },
477
+ { id: 'act-submit-1', elementId: 'UserTask_Submit', visitId: 'visit-submit-1', status: 'completed', assignee: '张三', startTime: '2026-08-24 09:00', endTime: '2026-08-24 09:08', outcome: 'submitted', comment: '采购申请已提交。' },
478
+ { id: 'act-manager-1', elementId: 'UserTask_Manager', visitId: 'visit-manager-1', status: 'completed', assignee: '李经理', startTime: '2026-08-24 09:08', endTime: '2026-08-24 10:12', outcome: 'approved', comment: '资料完整,同意提交总经理审批。' },
479
+ { id: 'act-gateway-1', elementId: 'Gateway_Amount', visitId: 'visit-gateway-1', status: 'completed', startTime: '2026-08-24 10:12', endTime: '2026-08-24 10:12' },
480
+ { id: 'act-director-1', elementId: 'UserTask_Director', visitId: 'visit-director-1', status: 'completed', assignee: '王总', startTime: '2026-08-24 10:12', endTime: '2026-08-24 10:30', outcome: 'rejected', comment: '采购金额说明不完整,请补充后重新提交。' },
481
+ { id: 'act-manager-2', elementId: 'UserTask_Manager', visitId: 'visit-manager-2', status: 'active', assignee: '李经理', startTime: '2026-08-24 10:31', outcome: 'pending' },
482
+ ],
483
+ actions: [
484
+ {
485
+ id: 'action-submit-1', type: 'submit', activityId: 'act-submit-1', elementId: 'UserTask_Submit', visitId: 'visit-submit-1',
486
+ actor: { id: 'user-zhang', name: '张三' }, occurredAt: '2026-08-24 09:08',
487
+ content: { plainText: '采购申请已提交。' },
488
+ },
489
+ {
490
+ id: 'action-manager-add-sign-1', type: 'add-sign', activityId: 'act-manager-1', elementId: 'UserTask_Manager', visitId: 'visit-manager-1',
491
+ actor: { id: 'user-manager-li', name: '李经理' }, targets: [{ id: 'user-supervisor-wang', name: '王主管' }], occurredAt: '2026-08-24 09:36',
492
+ content: { plainText: '采购金额较大,请协同核验报价材料。' },
493
+ },
494
+ {
495
+ id: 'action-manager-approve-1', type: 'approve', activityId: 'act-manager-1', elementId: 'UserTask_Manager', visitId: 'visit-manager-1',
496
+ actor: { id: 'user-manager-li', name: '李经理' }, occurredAt: '2026-08-24 10:12',
497
+ content: {
498
+ plainText: '资料完整,同意提交总经理审批。',
499
+ blocks: [
500
+ { type: 'paragraph', text: '资料完整,同意提交总经理审批。现场报价单和采购核验清单见附件。' },
501
+ { type: 'image', assetId: 'asset-quotation-image', alt: '现场报价单预览' },
502
+ { type: 'file', assetId: 'asset-purchase-checklist' },
503
+ ],
504
+ assets: [
505
+ { id: 'asset-quotation-image', name: '现场报价单.svg', mediaType: 'image/svg+xml', size: 2861, width: 960, height: 600 },
506
+ { id: 'asset-purchase-checklist', name: '采购核验清单.txt', mediaType: 'text/plain', size: 248 },
507
+ ],
508
+ },
509
+ },
510
+ {
511
+ id: 'action-director-reject-1', type: 'reject', activityId: 'act-director-1', elementId: 'UserTask_Director', visitId: 'visit-director-1',
512
+ actor: { id: 'user-director-wang', name: '王总' }, occurredAt: '2026-08-24 10:30', targetElementId: 'UserTask_Manager',
513
+ content: { plainText: '采购金额说明不完整,请补充后重新提交。' },
514
+ },
515
+ {
516
+ id: 'action-manager-transfer-2', type: 'transfer', activityId: 'act-manager-2', elementId: 'UserTask_Manager', visitId: 'visit-manager-2',
517
+ actor: { id: 'user-manager-li', name: '李经理' }, targets: [{ id: 'user-supervisor-wang', name: '王主管' }], occurredAt: '2026-08-24 10:34',
518
+ content: { plainText: '材料补充由王主管继续跟进。' },
519
+ },
520
+ ],
521
+ transitions: [{
522
+ id: 'transition-reject-director-manager',
523
+ type: 'reject',
524
+ sourceActivityId: 'act-director-1',
525
+ sourceElementId: 'UserTask_Director',
526
+ targetElementId: 'UserTask_Manager',
527
+ operator: '王总',
528
+ occurredAt: '2026-08-24 10:30',
529
+ actionId: 'action-director-reject-1',
530
+ comment: '采购金额说明不完整,请补充后重新提交。',
531
+ invalidatedActivityIds: ['Gateway_Amount', 'UserTask_Director'],
532
+ invalidatedEdgeIds: ['Flow_Manager_Gateway', 'Flow_To_Director'],
533
+ }],
534
+ });
535
+ }
@@ -0,0 +1,95 @@
1
+ export type NovaThemeMode = 'light' | 'dark' | 'auto'
2
+ export type NovaResolvedTheme = 'light' | 'dark'
3
+
4
+ export interface NovaThemeState {
5
+ mode: NovaThemeMode
6
+ resolvedTheme: NovaResolvedTheme
7
+ }
8
+
9
+ export interface NovaThemeSnapshot {
10
+ resolvedTheme: NovaResolvedTheme
11
+ colors: Readonly<Record<NovaSemanticColorToken, string>>
12
+ tones: Readonly<Record<string, Readonly<Required<NovaThemeTone>>>>
13
+ fontFamily: string
14
+ }
15
+
16
+ export type NovaSemanticColorToken =
17
+ | 'canvas' | 'surface' | 'surfaceRaised' | 'surfaceSubtle' | 'surfaceMuted'
18
+ | 'text' | 'textSecondary' | 'textMuted' | 'textInverse'
19
+ | 'border' | 'borderStrong' | 'divider'
20
+ | 'primary' | 'primaryHover' | 'primarySoft' | 'focusRing'
21
+ | 'edge' | 'gridDot' | 'backdrop'
22
+
23
+ export interface NovaThemeTone {
24
+ foreground?: string
25
+ background?: string
26
+ border?: string
27
+ strong?: string
28
+ }
29
+
30
+ export interface NovaThemePalette {
31
+ colors?: Partial<Record<NovaSemanticColorToken, string>>
32
+ shadows?: Partial<Record<'sm' | 'md' | 'lg', string>>
33
+ tones?: Record<string, NovaThemeTone>
34
+ }
35
+
36
+ export interface NovaThemeOptions {
37
+ mode?: NovaThemeMode
38
+ light?: NovaThemePalette | null
39
+ dark?: NovaThemePalette | null
40
+ }
41
+
42
+ export type NovaThemeInput = NovaThemeMode | NovaThemeOptions
43
+
44
+ export interface RuntimeActionAppearance {
45
+ label?: string
46
+ iconId?: string
47
+ tone?: string
48
+ }
49
+
50
+ export interface RuntimeAppearanceOptions {
51
+ statuses?: Record<string, string>
52
+ actions?: Record<string, RuntimeActionAppearance>
53
+ transitions?: Record<string, { tone?: string }>
54
+ }
55
+
56
+ export interface RuntimeAppearanceActionInput {
57
+ type?: string
58
+ explicitLabel?: string
59
+ label?: string
60
+ }
61
+
62
+ export interface RuntimeAppearanceTransitionInput {
63
+ type?: string
64
+ }
65
+
66
+ export interface RuntimeAppearanceResolver {
67
+ resolveStatus(status: string): string
68
+ resolveAction(action?: RuntimeAppearanceActionInput): Required<RuntimeActionAppearance>
69
+ resolveTransition(transition?: RuntimeAppearanceTransitionInput): { tone: string }
70
+ }
71
+
72
+ export class ThemeController {
73
+ constructor(options: {
74
+ root: HTMLElement
75
+ theme?: NovaThemeInput | null
76
+ onChange?: (state: NovaThemeState) => void
77
+ })
78
+ setTheme(theme: NovaThemeInput | null): NovaThemeState
79
+ setMode(mode: NovaThemeMode): NovaThemeState
80
+ getState(): NovaThemeState
81
+ getSnapshot(theme?: 'current' | NovaResolvedTheme): NovaThemeSnapshot
82
+ subscribe(listener: (state: NovaThemeState) => void): () => void
83
+ destroy(): void
84
+ }
85
+
86
+ export function createThemeController(options: ConstructorParameters<typeof ThemeController>[0]): ThemeController
87
+ export function createRuntimeAppearance(options?: RuntimeAppearanceOptions): RuntimeAppearanceResolver
88
+ export function applyRuntimeTone(element: HTMLElement | SVGElement, tone?: string | null): string
89
+
90
+ export const NOVA_THEME_MODES: readonly NovaThemeMode[]
91
+ export const DEFAULT_RUNTIME_APPEARANCE: Readonly<{
92
+ statuses: Readonly<Record<string, string>>
93
+ actions: Readonly<Record<string, Readonly<Required<RuntimeActionAppearance>>>>
94
+ transitions: Readonly<Record<string, Readonly<{ tone: string }>>>
95
+ }>