@meiyukichan/dsh-sandore 0.1.0-rc.6
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/LICENSE +201 -0
- package/cordis.patch.yml +39 -0
- package/dsh.plugin.json +16 -0
- package/lib/client/CommandEditor.d.ts +19 -0
- package/lib/client/DagEditor.d.ts +22 -0
- package/lib/client/DefinitionDetail.d.ts +13 -0
- package/lib/client/DefinitionList.d.ts +19 -0
- package/lib/client/DiscussionInput.d.ts +63 -0
- package/lib/client/DragHandle.d.ts +16 -0
- package/lib/client/EditorPane.d.ts +13 -0
- package/lib/client/FlowEditor.d.ts +31 -0
- package/lib/client/InstanceList.d.ts +24 -0
- package/lib/client/NewInstanceDialog.d.ts +19 -0
- package/lib/client/PreviewPanel.d.ts +13 -0
- package/lib/client/RunView.d.ts +79 -0
- package/lib/client/StepChatPanel.d.ts +34 -0
- package/lib/client/TreeNav.d.ts +27 -0
- package/lib/client/UnifiedTimeline.d.ts +11 -0
- package/lib/client/WorkflowButton.d.ts +11 -0
- package/lib/client/WorkflowList.d.ts +18 -0
- package/lib/client/WorkflowOverlay.d.ts +6 -0
- package/lib/client/chat/AssistantMarkdown.d.ts +22 -0
- package/lib/client/chat/CompactionItem.d.ts +24 -0
- package/lib/client/chat/ContextBody.d.ts +136 -0
- package/lib/client/chat/ContextInjectionRow.d.ts +25 -0
- package/lib/client/chat/GenericCommandCard.d.ts +10 -0
- package/lib/client/chat/MessageIconActions.d.ts +41 -0
- package/lib/client/chat/MessageItem.d.ts +65 -0
- package/lib/client/chat/ReasoningRow.d.ts +13 -0
- package/lib/client/chat/TurnTailNodeView.d.ts +29 -0
- package/lib/client/chat/UserMessageRow.d.ts +18 -0
- package/lib/client/chat/attachment/ImageLightbox.d.ts +26 -0
- package/lib/client/chat/attachment/MessageImage.d.ts +56 -0
- package/lib/client/chat/attachment/image-attachment.d.ts +41 -0
- package/lib/client/chat/attachment/image-labels.d.ts +21 -0
- package/lib/client/chat/message-chrome.d.ts +51 -0
- package/lib/client/chat/reference/ReferenceIcon.d.ts +20 -0
- package/lib/client/chat/turn-assistant.d.ts +12 -0
- package/lib/client/chat/turn-metrics.d.ts +42 -0
- package/lib/client/chat/use-calendar-day.d.ts +5 -0
- package/lib/client/chat/use-throttled-visual-update.d.ts +7 -0
- package/lib/client/chat-flow.d.ts +110 -0
- package/lib/client/composer-selects.d.ts +17 -0
- package/lib/client/dag-editor-model.d.ts +141 -0
- package/lib/client/describe-error.d.ts +9 -0
- package/lib/client/index.d.ts +6 -0
- package/lib/client/locales.d.ts +777 -0
- package/lib/client/model-faces.d.ts +156 -0
- package/lib/client/open-discussion.d.ts +24 -0
- package/lib/client/panel-model.d.ts +34 -0
- package/lib/client/remote.d.ts +34 -0
- package/lib/client/run-view-model.d.ts +304 -0
- package/lib/client/sandore-save.d.ts +41 -0
- package/lib/client/sandore-sse.d.ts +15 -0
- package/lib/client/step-chat-retry.d.ts +44 -0
- package/lib/client/tool/GenericToolCard.d.ts +28 -0
- package/lib/client/tool/ToolRow.d.ts +95 -0
- package/lib/client/tool/diff-card-model.d.ts +63 -0
- package/lib/client/tool/read-card-model.d.ts +67 -0
- package/lib/client/tool/search-card-model.d.ts +94 -0
- package/lib/client/tool/terminal-card-model.d.ts +86 -0
- package/lib/client/tool/tool-call-model.d.ts +74 -0
- package/lib/client/tool/web-card-model.d.ts +44 -0
- package/lib/client/use-focus-refresh.d.ts +1 -0
- package/lib/client/workflow-face.d.ts +68 -0
- package/lib/client/workflow-store.d.ts +80 -0
- package/lib/client/workspace-path.d.ts +15 -0
- package/lib/client.js +38389 -0
- package/lib/index.js +8854 -0
- package/lib/microflow/branch-evaluator.d.ts +290 -0
- package/lib/microflow/concurrency-limiter.d.ts +39 -0
- package/lib/microflow/errors.d.ts +39 -0
- package/lib/microflow/file-utils.d.ts +210 -0
- package/lib/microflow/finalize-manager.d.ts +414 -0
- package/lib/microflow/fork-session.d.ts +151 -0
- package/lib/microflow/index.d.ts +17 -0
- package/lib/microflow/internal.d.ts +56 -0
- package/lib/microflow/loop-manager.d.ts +127 -0
- package/lib/microflow/rules-resolver.d.ts +28 -0
- package/lib/microflow/stage-executor.d.ts +362 -0
- package/lib/microflow/step-navigator.d.ts +108 -0
- package/lib/microflow/step-runner.d.ts +190 -0
- package/lib/microflow/subagents.d.ts +380 -0
- package/lib/tool/index.d.ts +65 -0
- package/lib/typert.host.d.ts +16 -0
- package/lib/typert.host.js +25 -0
- package/lib/typert.remote-client.d.ts +114 -0
- package/lib/typert.remote-client.js +399 -0
- package/lib/workflow/ask-tool.d.ts +78 -0
- package/lib/workflow/dag-navigator.d.ts +136 -0
- package/lib/workflow/domain.d.ts +304 -0
- package/lib/workflow/engine-surface.d.ts +70 -0
- package/lib/workflow/errors.d.ts +13 -0
- package/lib/workflow/host-agent.d.ts +53 -0
- package/lib/workflow/index.d.ts +23 -0
- package/lib/workflow/interaction-manager.d.ts +170 -0
- package/lib/workflow/lifecycle-manager.d.ts +111 -0
- package/lib/workflow/persistence.d.ts +35 -0
- package/lib/workflow/plugin.d.ts +27 -0
- package/lib/workflow/reactive-state.d.ts +27 -0
- package/lib/workflow/recovery-manager.d.ts +158 -0
- package/lib/workflow/remote.d.ts +139 -0
- package/lib/workflow/schema.d.ts +1917 -0
- package/lib/workflow/seam.d.ts +254 -0
- package/lib/workflow/shell.d.ts +127 -0
- package/lib/workflow/sse.d.ts +16 -0
- package/lib/workflow/typert/typert.host.d.ts +16 -0
- package/lib/workflow/typert/typert.remote-client.d.ts +114 -0
- package/lib/workflow/types.d.ts +187 -0
- package/lib/workflow/wire.d.ts +256 -0
- package/lib/workflow/workflow-engine.d.ts +373 -0
- package/lib/workflow/yaml-error.d.ts +22 -0
- package/lib/workflow/yaml-parser.d.ts +23 -0
- package/package.json +140 -0
- package/packages/client/CommandEditor.tsx +73 -0
- package/packages/client/DagEditor.module.css +711 -0
- package/packages/client/DagEditor.tsx +1203 -0
- package/packages/client/DefinitionDetail.module.css +110 -0
- package/packages/client/DefinitionDetail.tsx +126 -0
- package/packages/client/DefinitionList.tsx +173 -0
- package/packages/client/DiscussionInput.tsx +763 -0
- package/packages/client/DragHandle.tsx +92 -0
- package/packages/client/EditorPane.tsx +70 -0
- package/packages/client/FlowEditor.tsx +227 -0
- package/packages/client/InstanceList.tsx +250 -0
- package/packages/client/NewInstanceDialog.module.css +244 -0
- package/packages/client/NewInstanceDialog.tsx +478 -0
- package/packages/client/PreviewPanel.tsx +55 -0
- package/packages/client/RunView.module.css +246 -0
- package/packages/client/RunView.tsx +314 -0
- package/packages/client/StepChatPanel.tsx +486 -0
- package/packages/client/TreeNav.tsx +294 -0
- package/packages/client/UnifiedTimeline.tsx +484 -0
- package/packages/client/WorkflowButton.module.css +72 -0
- package/packages/client/WorkflowButton.tsx +50 -0
- package/packages/client/WorkflowList.tsx +158 -0
- package/packages/client/WorkflowOverlay.module.css +390 -0
- package/packages/client/WorkflowOverlay.tsx +136 -0
- package/packages/client/WorkflowPanel.module.css +247 -0
- package/packages/client/chat/AssistantMarkdown.module.css +38 -0
- package/packages/client/chat/AssistantMarkdown.tsx +120 -0
- package/packages/client/chat/ChatView.module.css +198 -0
- package/packages/client/chat/CompactionItem.tsx +85 -0
- package/packages/client/chat/ContextBody.module.css +162 -0
- package/packages/client/chat/ContextBody.tsx +598 -0
- package/packages/client/chat/ContextInjectionRow.module.css +63 -0
- package/packages/client/chat/ContextInjectionRow.tsx +78 -0
- package/packages/client/chat/GenericCommandCard.module.css +87 -0
- package/packages/client/chat/GenericCommandCard.tsx +76 -0
- package/packages/client/chat/MessageIconActions.module.css +84 -0
- package/packages/client/chat/MessageIconActions.tsx +147 -0
- package/packages/client/chat/MessageItem.module.css +293 -0
- package/packages/client/chat/MessageItem.tsx +384 -0
- package/packages/client/chat/ReasoningRow.module.css +82 -0
- package/packages/client/chat/ReasoningRow.tsx +70 -0
- package/packages/client/chat/TurnTailNodeView.module.css +10 -0
- package/packages/client/chat/TurnTailNodeView.tsx +63 -0
- package/packages/client/chat/UserMessageRow.module.css +75 -0
- package/packages/client/chat/UserMessageRow.tsx +92 -0
- package/packages/client/chat/accessibility.module.css +9 -0
- package/packages/client/chat/attachment/ImageLightbox.module.css +45 -0
- package/packages/client/chat/attachment/ImageLightbox.tsx +70 -0
- package/packages/client/chat/attachment/MessageImage.module.css +70 -0
- package/packages/client/chat/attachment/MessageImage.tsx +136 -0
- package/packages/client/chat/attachment/image-attachment.ts +98 -0
- package/packages/client/chat/attachment/image-labels.ts +34 -0
- package/packages/client/chat/message-chrome.ts +96 -0
- package/packages/client/chat/reference/ReferenceIcon.tsx +40 -0
- package/packages/client/chat/turn-assistant.ts +15 -0
- package/packages/client/chat/turn-metrics.ts +102 -0
- package/packages/client/chat/use-calendar-day.ts +27 -0
- package/packages/client/chat/use-throttled-visual-update.ts +44 -0
- package/packages/client/chat-flow.spec.ts +357 -0
- package/packages/client/chat-flow.ts +278 -0
- package/packages/client/composer-selects.module.css +376 -0
- package/packages/client/composer-selects.tsx +531 -0
- package/packages/client/dag-editor-model.spec.ts +173 -0
- package/packages/client/dag-editor-model.ts +359 -0
- package/packages/client/describe-error.spec.ts +18 -0
- package/packages/client/describe-error.ts +12 -0
- package/packages/client/index.ts +144 -0
- package/packages/client/input.module.css +854 -0
- package/packages/client/is-conditional-check.spec.ts +34 -0
- package/packages/client/locales.ts +789 -0
- package/packages/client/model-faces.ts +225 -0
- package/packages/client/open-discussion.spec.ts +39 -0
- package/packages/client/open-discussion.ts +32 -0
- package/packages/client/panel-model.spec.ts +152 -0
- package/packages/client/panel-model.ts +72 -0
- package/packages/client/preview.module.css +95 -0
- package/packages/client/react-dom.d.ts +11 -0
- package/packages/client/reactflow-theme.css +91 -0
- package/packages/client/remote.spec.ts +41 -0
- package/packages/client/remote.ts +46 -0
- package/packages/client/run-view-model.spec.ts +641 -0
- package/packages/client/run-view-model.ts +636 -0
- package/packages/client/sandore-save.spec.ts +146 -0
- package/packages/client/sandore-save.ts +97 -0
- package/packages/client/sandore-sse.ts +59 -0
- package/packages/client/step-chat-retry.spec.ts +70 -0
- package/packages/client/step-chat-retry.ts +65 -0
- package/packages/client/timeline.module.css +503 -0
- package/packages/client/tool/GenericToolCard.tsx +99 -0
- package/packages/client/tool/ToolRow.module.css +308 -0
- package/packages/client/tool/ToolRow.tsx +328 -0
- package/packages/client/tool/diff-card-model.ts +103 -0
- package/packages/client/tool/read-card-model.ts +86 -0
- package/packages/client/tool/search-card-model.ts +166 -0
- package/packages/client/tool/terminal-card-model.ts +225 -0
- package/packages/client/tool/tool-call-model.ts +267 -0
- package/packages/client/tool/web-card-model.ts +80 -0
- package/packages/client/tree-nav.module.css +282 -0
- package/packages/client/use-focus-refresh.ts +23 -0
- package/packages/client/workflow-face.ts +72 -0
- package/packages/client/workflow-store.spec.ts +49 -0
- package/packages/client/workflow-store.ts +161 -0
- package/packages/client/workspace-path.ts +27 -0
- package/packages/css-modules.d.ts +6 -0
- package/packages/microflow/branch-evaluator.spec.ts +745 -0
- package/packages/microflow/branch-evaluator.ts +1037 -0
- package/packages/microflow/concurrency-limiter.ts +70 -0
- package/packages/microflow/errors.ts +64 -0
- package/packages/microflow/file-utils.spec.ts +604 -0
- package/packages/microflow/file-utils.ts +521 -0
- package/packages/microflow/finalize-manager.spec.ts +1977 -0
- package/packages/microflow/finalize-manager.ts +1464 -0
- package/packages/microflow/fork-session.spec.ts +184 -0
- package/packages/microflow/fork-session.ts +316 -0
- package/packages/microflow/index.ts +17 -0
- package/packages/microflow/internal.ts +166 -0
- package/packages/microflow/loop-manager.spec.ts +294 -0
- package/packages/microflow/loop-manager.ts +255 -0
- package/packages/microflow/rules-resolver.spec.ts +147 -0
- package/packages/microflow/rules-resolver.ts +58 -0
- package/packages/microflow/stage-executor.spec.ts +1577 -0
- package/packages/microflow/stage-executor.ts +1478 -0
- package/packages/microflow/step-navigator.spec.ts +301 -0
- package/packages/microflow/step-navigator.ts +300 -0
- package/packages/microflow/step-runner.spec.ts +1132 -0
- package/packages/microflow/step-runner.ts +752 -0
- package/packages/microflow/subagents.spec.ts +215 -0
- package/packages/microflow/subagents.ts +422 -0
- package/packages/tests/fixtures/test-workflow.yaml +128 -0
- package/packages/tests/mocks/agents.ts +206 -0
- package/packages/tests/mocks/llm.ts +46 -0
- package/packages/tests/mocks/session-title.ts +31 -0
- package/packages/tests/mocks/sessions.ts +131 -0
- package/packages/tests/mocks/shell.ts +60 -0
- package/packages/tests/mocks/storage-domain.ts +101 -0
- package/packages/tests/mocks/subagents.ts +197 -0
- package/packages/tests/mocks/subprocess.ts +59 -0
- package/packages/tests/mocks/user-questions.ts +33 -0
- package/packages/tests/mocks/workspace.ts +52 -0
- package/packages/tool/index.spec.ts +287 -0
- package/packages/tool/index.ts +348 -0
- package/packages/workflow/ask-tool.ts +173 -0
- package/packages/workflow/dag-navigator.spec.ts +236 -0
- package/packages/workflow/dag-navigator.ts +425 -0
- package/packages/workflow/domain.ts +26 -0
- package/packages/workflow/engine-surface.ts +71 -0
- package/packages/workflow/errors.ts +13 -0
- package/packages/workflow/host-agent.spec.ts +92 -0
- package/packages/workflow/host-agent.ts +62 -0
- package/packages/workflow/index.ts +25 -0
- package/packages/workflow/interaction-manager.spec.ts +390 -0
- package/packages/workflow/interaction-manager.ts +397 -0
- package/packages/workflow/lifecycle-manager.spec.ts +683 -0
- package/packages/workflow/lifecycle-manager.ts +288 -0
- package/packages/workflow/persistence.spec.ts +109 -0
- package/packages/workflow/persistence.ts +64 -0
- package/packages/workflow/plugin.ts +39 -0
- package/packages/workflow/reactive-state.spec.ts +69 -0
- package/packages/workflow/reactive-state.ts +52 -0
- package/packages/workflow/recovery-manager.spec.ts +606 -0
- package/packages/workflow/recovery-manager.ts +471 -0
- package/packages/workflow/remote.spec.ts +1045 -0
- package/packages/workflow/remote.ts +479 -0
- package/packages/workflow/schema.spec.ts +303 -0
- package/packages/workflow/schema.ts +562 -0
- package/packages/workflow/seam.ts +293 -0
- package/packages/workflow/shell.ts +142 -0
- package/packages/workflow/sse.ts +110 -0
- package/packages/workflow/typert/typert.host.d.ts +16 -0
- package/packages/workflow/typert/typert.host.js +25 -0
- package/packages/workflow/typert/typert.remote-client.d.ts +114 -0
- package/packages/workflow/typert/typert.remote-client.js +399 -0
- package/packages/workflow/types.ts +258 -0
- package/packages/workflow/wire.ts +267 -0
- package/packages/workflow/workflow-engine-e2e.spec.ts +262 -0
- package/packages/workflow/workflow-engine-real-e2e.spec.ts +922 -0
- package/packages/workflow/workflow-engine.spec.ts +4389 -0
- package/packages/workflow/workflow-engine.ts +2133 -0
- package/packages/workflow/yaml-error.ts +29 -0
- package/packages/workflow/yaml-parser.spec.ts +757 -0
- package/packages/workflow/yaml-parser.ts +287 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dag-editor-model.ts 的编辑模型纯函数单元测试:
|
|
3
|
+
* 最小合法定义骨架 round-trip、五类型 command 工厂。
|
|
4
|
+
* @module @deepseek-ai/dsh-client-ui-sandore/src/client/dag-editor-model.spec
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from 'vitest'
|
|
7
|
+
|
|
8
|
+
import { parseWorkflowYaml, serializeWorkflowYaml } from 'dsh-sandore/yaml'
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
createBlankDefinition, createBranch, createCommand, createInputFromItem, createOutputToItem,
|
|
12
|
+
createPassThroughVar, createStage, newId, parseOptionalNumber, trimToUndefined, updateCommandText, withDefinitionId,
|
|
13
|
+
} from './dag-editor-model.ts'
|
|
14
|
+
|
|
15
|
+
describe('createBlankDefinition', () => {
|
|
16
|
+
it('produces a minimal valid definition that round-trips through YAML', () => {
|
|
17
|
+
const blank = createBlankDefinition()
|
|
18
|
+
|
|
19
|
+
const roundTripped = parseWorkflowYaml(serializeWorkflowYaml(blank))
|
|
20
|
+
|
|
21
|
+
expect(roundTripped).toEqual(blank)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('defaults to a single empty stage', () => {
|
|
25
|
+
const blank = createBlankDefinition()
|
|
26
|
+
|
|
27
|
+
expect(blank.stages).toHaveLength(1)
|
|
28
|
+
expect(blank.stages[0]?.name).toBe('阶段 1')
|
|
29
|
+
expect(blank.stages[0]?.steps).toEqual([])
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('withDefinitionId', () => {
|
|
34
|
+
it('replaces the id with the saved id and keeps the rest of the model intact', () => {
|
|
35
|
+
const blank = createBlankDefinition()
|
|
36
|
+
|
|
37
|
+
const updated = withDefinitionId(blank, 'wfl-abc')
|
|
38
|
+
|
|
39
|
+
expect(updated.id).toBe('wfl-abc')
|
|
40
|
+
expect(updated.name).toBe(blank.name)
|
|
41
|
+
expect(updated.stages).toEqual(blank.stages)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('does not mutate the source definition', () => {
|
|
45
|
+
const blank = createBlankDefinition()
|
|
46
|
+
|
|
47
|
+
withDefinitionId(blank, 'wfl-abc')
|
|
48
|
+
|
|
49
|
+
expect(blank.id).toBe('wfl-new')
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
describe('updateCommandText', () => {
|
|
54
|
+
it('updates a shell command text', () => {
|
|
55
|
+
const next = updateCommandText({ type: 'shell', value: { command: 'a' } }, 'command', 'b')
|
|
56
|
+
|
|
57
|
+
expect(next).toEqual({ type: 'shell', value: { command: 'b' } })
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('updates a prompt promptText', () => {
|
|
61
|
+
const next = updateCommandText({ type: 'prompt', value: { promptText: 'a' } }, 'promptText', 'b')
|
|
62
|
+
|
|
63
|
+
expect(next).toEqual({ type: 'prompt', value: { promptText: 'b' } })
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('clears an optional field when given an empty string', () => {
|
|
67
|
+
const next = updateCommandText({ type: 'prompt', value: { promptText: 'a', modelId: 'm' } }, 'modelId', ' ')
|
|
68
|
+
|
|
69
|
+
expect(next).toEqual({ type: 'prompt', value: { promptText: 'a', modelId: undefined } })
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('returns the unchanged command for a field not applicable to its type', () => {
|
|
73
|
+
const command = createCommand('mcp')
|
|
74
|
+
|
|
75
|
+
const next = updateCommandText(command, 'command', 'b')
|
|
76
|
+
|
|
77
|
+
expect(next).toBe(command)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('trimToUndefined', () => {
|
|
82
|
+
it('returns undefined for a blank string', () => {
|
|
83
|
+
expect(trimToUndefined(' ')).toBeUndefined()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('trims surrounding whitespace otherwise', () => {
|
|
87
|
+
expect(trimToUndefined(' x ')).toBe('x')
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('parseOptionalNumber', () => {
|
|
92
|
+
it('returns undefined for a blank or non-numeric string', () => {
|
|
93
|
+
expect(parseOptionalNumber('')).toBeUndefined()
|
|
94
|
+
expect(parseOptionalNumber('abc')).toBeUndefined()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('parses a valid number', () => {
|
|
98
|
+
expect(parseOptionalNumber('42')).toBe(42)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('createCommand', () => {
|
|
103
|
+
it('creates a shell command', () => {
|
|
104
|
+
expect(createCommand('shell')).toEqual({ type: 'shell', value: { command: '' } })
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('creates a prompt command', () => {
|
|
108
|
+
expect(createCommand('prompt')).toEqual({ type: 'prompt', value: { promptText: '' } })
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('creates an agent command', () => {
|
|
112
|
+
expect(createCommand('agent')).toEqual({ type: 'agent', value: { promptText: '' } })
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('creates a skill command', () => {
|
|
116
|
+
expect(createCommand('skill')).toEqual({ type: 'skill', value: { promptText: '' } })
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('creates an mcp command', () => {
|
|
120
|
+
expect(createCommand('mcp')).toEqual({ type: 'mcp', value: { promptText: '' } })
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe('newId', () => {
|
|
125
|
+
it('generates a unique prefixed id', () => {
|
|
126
|
+
const first = newId('step')
|
|
127
|
+
const second = newId('step')
|
|
128
|
+
|
|
129
|
+
expect(first).toMatch(/^step-/)
|
|
130
|
+
expect(second).toMatch(/^step-/)
|
|
131
|
+
expect(first).not.toBe(second)
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
describe('createStage', () => {
|
|
136
|
+
it('generates a unique stg- prefixed id with the given name', () => {
|
|
137
|
+
const first = createStage('阶段 1')
|
|
138
|
+
const second = createStage('阶段 2')
|
|
139
|
+
|
|
140
|
+
expect(first).toEqual({ id: first.id, name: '阶段 1', steps: [] })
|
|
141
|
+
expect(first.id).toMatch(/^stg-/)
|
|
142
|
+
expect(second.id).toMatch(/^stg-/)
|
|
143
|
+
expect(first.id).not.toBe(second.id)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('mapping item factories', () => {
|
|
148
|
+
it('creates an input-from item with a unique ipt- id', () => {
|
|
149
|
+
const first = createInputFromItem()
|
|
150
|
+
const second = createInputFromItem()
|
|
151
|
+
|
|
152
|
+
expect(first.id).toMatch(/^ipt-/)
|
|
153
|
+
expect(second.id).toMatch(/^ipt-/)
|
|
154
|
+
expect(first.id).not.toBe(second.id)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('creates an output-to item with a unique opt- id', () => {
|
|
158
|
+
const first = createOutputToItem()
|
|
159
|
+
const second = createOutputToItem()
|
|
160
|
+
|
|
161
|
+
expect(first.id).toMatch(/^opt-/)
|
|
162
|
+
expect(second.id).toMatch(/^opt-/)
|
|
163
|
+
expect(first.id).not.toBe(second.id)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('creates an empty pass-through var', () => {
|
|
167
|
+
expect(createPassThroughVar()).toEqual({ name: '', value: '', description: '' })
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('creates an empty branch', () => {
|
|
171
|
+
expect(createBranch()).toEqual({ next: '' })
|
|
172
|
+
})
|
|
173
|
+
})
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DagEditor 的编辑态模型纯函数(无 React 依赖,可单测)。
|
|
3
|
+
*
|
|
4
|
+
* 定义经 `dsh-sandore/yaml`(浏览器安全子路径)解析/序列化,
|
|
5
|
+
* 编辑态模型即 `WorkflowDefinition`。此处提供新建骨架与五类型 command 工厂,
|
|
6
|
+
* 供组件初始化与 Step 字段编辑复用,避免在 JSX 中散落结构常量。
|
|
7
|
+
* @module @deepseek-ai/dsh-client-ui-sandore/src/client/dag-editor-model
|
|
8
|
+
*/
|
|
9
|
+
import type {
|
|
10
|
+
CommandConfig, DagBranch, InputFromItem, OutputToItem, PassThroughVar, WorkflowDefinition, WorkflowStage, WorkflowStep,
|
|
11
|
+
} from 'dsh-sandore/schema'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 合并视图 Step(V2 分离式):Stage 流控位(`{id, branches?}`)与顶层定义
|
|
15
|
+
* (`definition.steps[id]` 全字段)按 id 合并,供画布/编辑器读取完整字段。
|
|
16
|
+
*/
|
|
17
|
+
export type MergedStep = WorkflowStep & { branches?: DagBranch[] }
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 构造某 Stage 的合并 Step 视图:`definition.steps[id]` 定义字段 + `stage.steps[id].branches`
|
|
21
|
+
* 流控字段。顶层缺定义(旧数据/未同步)时退回 `{id}` 骨架。
|
|
22
|
+
* @param definition - 工作流定义(含顶层 steps[])。
|
|
23
|
+
* @param stage - 目标 Stage(含流控位 steps[])。
|
|
24
|
+
* @returns 合并 Step 列表(顺序 = Stage 流控位顺序)。
|
|
25
|
+
*/
|
|
26
|
+
export function mergedStepsOf(definition: WorkflowDefinition, stage: WorkflowStage): MergedStep[] {
|
|
27
|
+
const byId = new Map((definition.steps ?? []).map(step => [step.id, step] as const))
|
|
28
|
+
return stage.steps.map(slot => ({
|
|
29
|
+
...(byId.get(slot.id) ?? { id: slot.id }),
|
|
30
|
+
...(slot.branches === undefined ? {} : { branches: slot.branches }),
|
|
31
|
+
}))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 五类型命令判别字段(对齐 `CommandConfig` discriminated union)。 */
|
|
35
|
+
export type CommandType = CommandConfig['type']
|
|
36
|
+
|
|
37
|
+
/** 全部五类型命令判别字段(编辑表单下拉项)。 */
|
|
38
|
+
export const COMMAND_TYPES: readonly CommandType[] = ['shell', 'prompt', 'agent', 'skill', 'mcp']
|
|
39
|
+
|
|
40
|
+
/** 生成带前缀的唯一 id(`{prefix}-{uuid}`),供 Stage/Step/ipt/opt 节点复用。 */
|
|
41
|
+
export function newId(prefix: string): string {
|
|
42
|
+
return `${prefix}-${globalThis.crypto.randomUUID()}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 构造一个新的空 Stage 节点(唯一 `stg-{uuid}` id + 空 steps)。 */
|
|
46
|
+
export function createStage(name: string): WorkflowStage {
|
|
47
|
+
return { id: newId('stg'), name, steps: [] }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 构造一个新的输入绑定项(唯一 `ipt-{uuid}` id)。 */
|
|
51
|
+
export function createInputFromItem(): InputFromItem {
|
|
52
|
+
return { id: newId('ipt') }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 构造一个新的输出绑定项(唯一 `opt-{uuid}` id)。 */
|
|
56
|
+
export function createOutputToItem(): OutputToItem {
|
|
57
|
+
return { id: newId('opt') }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 构造一个空的透传变量项(`name`/`value`/`description` 待填)。 */
|
|
61
|
+
export function createPassThroughVar(): PassThroughVar {
|
|
62
|
+
return { name: '', value: '', description: '' }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 构造一个空的分支项(`next` 待填)。 */
|
|
66
|
+
export function createBranch(): DagBranch {
|
|
67
|
+
return { next: '' }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 去掉首尾空白,空串归 `undefined`(用于可选字段清除)。 */
|
|
71
|
+
export function trimToUndefined(value: string): string | undefined {
|
|
72
|
+
const trimmed = value.trim()
|
|
73
|
+
return trimmed === '' ? undefined : trimmed
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 解析可选的数字输入:空串/非法值归 `undefined`。 */
|
|
77
|
+
export function parseOptionalNumber(value: string): number | undefined {
|
|
78
|
+
if (value.trim() === '') return undefined
|
|
79
|
+
const number = Number(value)
|
|
80
|
+
return Number.isFinite(number) ? number : undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 可编辑的命令文本字段(按类型收窄到 value 内的字段)。 */
|
|
84
|
+
export type CommandTextField = 'command' | 'promptText' | 'agentParamsTemplate' | 'providerId' | 'modelId'
|
|
85
|
+
|
|
86
|
+
/** 更新某类型命令的单个文本字段,返回新命令(可选字段空串 → 清除)。 */
|
|
87
|
+
export function updateCommandText(command: CommandConfig, field: CommandTextField, value: string): CommandConfig {
|
|
88
|
+
const optional = trimToUndefined(value)
|
|
89
|
+
switch (command.type) {
|
|
90
|
+
case 'shell':
|
|
91
|
+
return field === 'command' ? { type: 'shell', value: { command: value } } : command
|
|
92
|
+
case 'prompt':
|
|
93
|
+
if (field === 'promptText') return { type: 'prompt', value: { ...command.value, promptText: value } }
|
|
94
|
+
if (field === 'providerId') return { type: 'prompt', value: { ...command.value, providerId: optional } }
|
|
95
|
+
if (field === 'modelId') return { type: 'prompt', value: { ...command.value, modelId: optional } }
|
|
96
|
+
return command
|
|
97
|
+
case 'agent':
|
|
98
|
+
if (field === 'promptText') return { type: 'agent', value: { ...command.value, promptText: value } }
|
|
99
|
+
if (field === 'agentParamsTemplate') return { type: 'agent', value: { ...command.value, agentParamsTemplate: optional } }
|
|
100
|
+
if (field === 'providerId') return { type: 'agent', value: { ...command.value, providerId: optional } }
|
|
101
|
+
if (field === 'modelId') return { type: 'agent', value: { ...command.value, modelId: optional } }
|
|
102
|
+
return command
|
|
103
|
+
case 'skill':
|
|
104
|
+
if (field === 'promptText') return { type: 'skill', value: { ...command.value, promptText: value } }
|
|
105
|
+
if (field === 'providerId') return { type: 'skill', value: { ...command.value, providerId: optional } }
|
|
106
|
+
return command
|
|
107
|
+
case 'mcp':
|
|
108
|
+
return field === 'promptText' ? { type: 'mcp', value: { promptText: value } } : command
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 将保存后的真实 id 回填进定义模型(新建哨兵 `wfl-new` → `wfl-{uuid}`),
|
|
114
|
+
* 使 YAML 预览基线反映已保存内容。
|
|
115
|
+
* @param definition - 当前编辑态定义模型。
|
|
116
|
+
* @param id - 保存回调返回的持久化 id。
|
|
117
|
+
* @returns id 已更新的定义模型(其余字段不变)。
|
|
118
|
+
*/
|
|
119
|
+
export function withDefinitionId(definition: WorkflowDefinition, id: string): WorkflowDefinition {
|
|
120
|
+
return { ...definition, id }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 构造某类型的最小合法命令(值为空串,待用户填写)。
|
|
125
|
+
* @param type - 命令类型。
|
|
126
|
+
* @returns 该类型的空命令配置。
|
|
127
|
+
*/
|
|
128
|
+
export function createCommand(type: CommandType): CommandConfig {
|
|
129
|
+
switch (type) {
|
|
130
|
+
case 'shell':
|
|
131
|
+
return { type: 'shell', value: { command: '' } }
|
|
132
|
+
case 'prompt':
|
|
133
|
+
return { type: 'prompt', value: { promptText: '' } }
|
|
134
|
+
case 'agent':
|
|
135
|
+
return { type: 'agent', value: { promptText: '' } }
|
|
136
|
+
case 'skill':
|
|
137
|
+
return { type: 'skill', value: { promptText: '' } }
|
|
138
|
+
case 'mcp':
|
|
139
|
+
return { type: 'mcp', value: { promptText: '' } }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 新建定义的默认 id(首次保存由 Host `createDefinition` 生成 `wfl-{uuid}` 记录键)。 */
|
|
144
|
+
export const NEW_DEFINITION_ID = 'wfl-new'
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 构造最小合法定义骨架(单个空 Stage),供「新建定义」进入空白 DagEditor。
|
|
148
|
+
*
|
|
149
|
+
* `skills`/`mcps`/`agents`/`settings` 显式给出默认值以对齐 `WorkflowDefinitionSchema`
|
|
150
|
+
* 的 `.default()` 输出类型;`serializeWorkflowYaml` → `parseWorkflowYaml` 可深层相等往返。
|
|
151
|
+
* @param id - 定义 id(新建默认 `wfl-new`)。
|
|
152
|
+
* @returns 最小合法定义。
|
|
153
|
+
*/
|
|
154
|
+
export function createBlankDefinition(id: string = NEW_DEFINITION_ID): WorkflowDefinition {
|
|
155
|
+
return {
|
|
156
|
+
id,
|
|
157
|
+
name: '未命名工作流',
|
|
158
|
+
version: '1.0.0',
|
|
159
|
+
skills: [],
|
|
160
|
+
mcps: [],
|
|
161
|
+
agents: [],
|
|
162
|
+
settings: { defaultTimeout: 120, maxConcurrentAgents: 4 },
|
|
163
|
+
// 骨架 Stage 用 uuid(与 createStage 的 `stg-{uuid}` 一致,避免 stg-1 与后续添加
|
|
164
|
+
// 的 uuid 格式混杂)。
|
|
165
|
+
stages: [{ id: newId('stg'), name: '阶段 1', steps: [] }],
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── DAG 画布(React Flow + Dagre 布局) ─────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
/** Stage 节点高度:随 Step 行数增长(封顶 6 行)。 */
|
|
172
|
+
export function stageNodeHeight(stepCount: number): number {
|
|
173
|
+
return 48 + Math.min(stepCount, 6) * 20 + 12
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** 单个 Step 的流出目标(branches[].next + branches[].loop.next,去空)。 */
|
|
177
|
+
export function stepTargetsOf(step: MergedStep): string[] {
|
|
178
|
+
const targets: string[] = []
|
|
179
|
+
for (const branch of step.branches ?? []) {
|
|
180
|
+
if (branch.next !== undefined && branch.next !== '') targets.push(branch.next)
|
|
181
|
+
if (branch.loop?.next !== undefined && branch.loop.next !== '') targets.push(branch.loop.next)
|
|
182
|
+
}
|
|
183
|
+
return targets
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** 流控边类型:串行 / 并行分支 / 条件分支 / 循环回边。 */
|
|
187
|
+
export type FlowEdgeKind = 'serial' | 'parallel' | 'conditional' | 'loop'
|
|
188
|
+
|
|
189
|
+
/** 流控边规格(供 DAG 画布渲染与 dagre 布局)。 */
|
|
190
|
+
export interface FlowEdgeSpec {
|
|
191
|
+
readonly source: string
|
|
192
|
+
readonly target: string
|
|
193
|
+
readonly kind: FlowEdgeKind
|
|
194
|
+
/** 边标签(条件分支 label / 循环标识)。 */
|
|
195
|
+
readonly label?: string | undefined
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 分支是否为条件分支:有 `condition`(布尔判定命令)才算。无 `condition` = 并行分支
|
|
200
|
+
* (始终纳入下一批);有 `loop` 的分支是循环回边(其 `next` 指向更早节点)。
|
|
201
|
+
*/
|
|
202
|
+
export function isConditionalBranch(branch: DagBranch): boolean {
|
|
203
|
+
return branch.condition !== undefined
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 判断在 Stage 间新增连线 `source → target` 是否会形成回环(Stage DAG 不允许循环)。
|
|
208
|
+
* 从 target 出发沿既有出边(branches[].next + branches[].loop.next)做可达性搜索:
|
|
209
|
+
* 若可回到 source,则新增这条边会构成环。自环(source === target)也视为回环。
|
|
210
|
+
* @param stages - 当前 Stage 列表(不含待加边)。
|
|
211
|
+
* @param source - 连线起点。
|
|
212
|
+
* @param target - 连线终点。
|
|
213
|
+
* @returns 是否构成回环。
|
|
214
|
+
*/
|
|
215
|
+
export function wouldCreateStageCycle(stages: readonly WorkflowStage[], source: string, target: string): boolean {
|
|
216
|
+
if (source === target) return true
|
|
217
|
+
const outgoing = new Map<string, string[]>()
|
|
218
|
+
for (const stage of stages) {
|
|
219
|
+
const targets: string[] = []
|
|
220
|
+
for (const branch of stage.branches ?? []) {
|
|
221
|
+
if (branch.next !== undefined && branch.next !== '') targets.push(branch.next)
|
|
222
|
+
if (branch.loop?.next !== undefined && branch.loop.next !== '') targets.push(branch.loop.next)
|
|
223
|
+
}
|
|
224
|
+
outgoing.set(stage.id, targets)
|
|
225
|
+
}
|
|
226
|
+
const visited = new Set<string>([target])
|
|
227
|
+
const queue = [target]
|
|
228
|
+
while (queue.length > 0) {
|
|
229
|
+
const current = queue.shift()!
|
|
230
|
+
for (const next of outgoing.get(current) ?? []) {
|
|
231
|
+
if (next === source) return true
|
|
232
|
+
if (!visited.has(next)) {
|
|
233
|
+
visited.add(next)
|
|
234
|
+
queue.push(next)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Stage 层流控边(branches 是唯一出边单元,**无循环**):有 `condition` 的条件分支
|
|
243
|
+
* 琥珀虚线(带 label),无 `condition` 的并行分支默认灰实线。未声明流控的孤立 Stage
|
|
244
|
+
* 按定义顺序串行兜底(画布不画边,仅引擎推进)。
|
|
245
|
+
* @param stages - 定义内的 Stage 列表。
|
|
246
|
+
* @returns 去重后的流控边。
|
|
247
|
+
*/
|
|
248
|
+
export function stageFlowEdges(stages: readonly WorkflowStage[]): FlowEdgeSpec[] {
|
|
249
|
+
const edges: FlowEdgeSpec[] = []
|
|
250
|
+
const emitted = new Set<string>()
|
|
251
|
+
const push = (edge: FlowEdgeSpec): void => {
|
|
252
|
+
const key = `${edge.source}->${edge.target}:${edge.kind}`
|
|
253
|
+
if (emitted.has(key)) return
|
|
254
|
+
emitted.add(key)
|
|
255
|
+
edges.push(edge)
|
|
256
|
+
}
|
|
257
|
+
for (const stage of stages) {
|
|
258
|
+
for (const branch of stage.branches ?? []) {
|
|
259
|
+
if (branch.next === undefined || branch.next === '') continue
|
|
260
|
+
const conditional = isConditionalBranch(branch)
|
|
261
|
+
push({
|
|
262
|
+
source: stage.id,
|
|
263
|
+
target: branch.next,
|
|
264
|
+
kind: conditional ? 'conditional' : 'parallel',
|
|
265
|
+
...(branch.label !== undefined ? { label: branch.label } : {}),
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return edges
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Step 层流控边(微流程:并行/条件 branches + **循环回边分支 loop**)。
|
|
274
|
+
* 条件分支(有 condition)琥珀虚线带 label;循环分支(有 loop)`loop.next` 红色虚线
|
|
275
|
+
* 回边;无 condition/loop 的并行分支默认灰实线。
|
|
276
|
+
* @param steps - 某 Stage 内的合并 Step 列表(含 branches 流控)。
|
|
277
|
+
* @returns 去重后的流控边。
|
|
278
|
+
*/
|
|
279
|
+
export function stepFlowEdges(steps: readonly MergedStep[]): FlowEdgeSpec[] {
|
|
280
|
+
const byId = new Set(steps.map(step => step.id))
|
|
281
|
+
const edges: FlowEdgeSpec[] = []
|
|
282
|
+
const emitted = new Set<string>()
|
|
283
|
+
const push = (edge: FlowEdgeSpec): void => {
|
|
284
|
+
const key = `${edge.source}->${edge.target}:${edge.kind}`
|
|
285
|
+
if (emitted.has(key)) return
|
|
286
|
+
emitted.add(key)
|
|
287
|
+
edges.push(edge)
|
|
288
|
+
}
|
|
289
|
+
for (const step of steps) {
|
|
290
|
+
for (const branch of step.branches ?? []) {
|
|
291
|
+
if (branch.next === undefined || branch.next === '' || !byId.has(branch.next)) continue
|
|
292
|
+
const conditional = isConditionalBranch(branch)
|
|
293
|
+
push({
|
|
294
|
+
source: step.id,
|
|
295
|
+
target: branch.next,
|
|
296
|
+
kind: conditional ? 'conditional' : 'parallel',
|
|
297
|
+
...(branch.label !== undefined ? { label: branch.label } : {}),
|
|
298
|
+
})
|
|
299
|
+
if (branch.loop?.next !== undefined && branch.loop.next !== '') {
|
|
300
|
+
if (byId.has(branch.loop.next)) {
|
|
301
|
+
push({ source: step.id, target: branch.loop.next, kind: 'loop', ...(branch.loop.next === step.id ? { label: 'loop' } : {}) })
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return edges
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── Dagre 自动布局(@dagrejs/dagre,React Flow 画布坐标) ───────────────────
|
|
310
|
+
|
|
311
|
+
import dagre from '@dagrejs/dagre'
|
|
312
|
+
|
|
313
|
+
/** 参与 dagre 布局的节点尺寸。 */
|
|
314
|
+
export interface DagreNodeSpec {
|
|
315
|
+
readonly id: string
|
|
316
|
+
readonly width: number
|
|
317
|
+
readonly height: number
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** dagre 有向边。 */
|
|
321
|
+
export interface DagreEdgeSpec {
|
|
322
|
+
readonly source: string
|
|
323
|
+
readonly target: string
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* 用 Dagre 计算分层布局(TB = 自上而下垂直流,官方 dagre 示例默认方向)。
|
|
328
|
+
* 返回节点 id → 左上角坐标(React Flow 的 position 语义,dagre 输出的是中心点,
|
|
329
|
+
* 需减半宽/高)。
|
|
330
|
+
* @param nodes - 参与布局的节点。
|
|
331
|
+
* @param edges - 有向边(自动去重)。
|
|
332
|
+
* @returns 节点 id → 左上角坐标。
|
|
333
|
+
*/
|
|
334
|
+
export function dagreLayout(
|
|
335
|
+
nodes: readonly DagreNodeSpec[],
|
|
336
|
+
edges: readonly DagreEdgeSpec[],
|
|
337
|
+
): Record<string, { x: number; y: number }> {
|
|
338
|
+
const graph = new dagre.graphlib.Graph()
|
|
339
|
+
graph.setDefaultEdgeLabel(() => ({}))
|
|
340
|
+
// TB 垂直流:同层节点横向间距 56、层间纵向间距 110,画布留白 24(参考官方示例)。
|
|
341
|
+
graph.setGraph({ rankdir: 'TB', nodesep: 56, ranksep: 110, marginx: 24, marginy: 24 })
|
|
342
|
+
for (const node of nodes) {
|
|
343
|
+
graph.setNode(node.id, { width: node.width, height: node.height })
|
|
344
|
+
}
|
|
345
|
+
const seen = new Set<string>()
|
|
346
|
+
for (const edge of edges) {
|
|
347
|
+
const key = `${edge.source}->${edge.target}`
|
|
348
|
+
if (seen.has(key)) continue
|
|
349
|
+
seen.add(key)
|
|
350
|
+
graph.setEdge(edge.source, edge.target)
|
|
351
|
+
}
|
|
352
|
+
dagre.layout(graph)
|
|
353
|
+
const positions: Record<string, { x: number; y: number }> = {}
|
|
354
|
+
for (const node of nodes) {
|
|
355
|
+
const meta = graph.node(node.id)
|
|
356
|
+
positions[node.id] = { x: meta.x - node.width / 2, y: meta.y - node.height / 2 }
|
|
357
|
+
}
|
|
358
|
+
return positions
|
|
359
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* describe-error.ts(错误消息归一化)的单元测试:`Error` 取 `.message`、
|
|
3
|
+
* 非 Error 抛错值回落 `String`。
|
|
4
|
+
* @module @deepseek-ai/dsh-client-ui-sandore/src/client/describe-error.spec
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from 'vitest'
|
|
7
|
+
|
|
8
|
+
import { describeError } from './describe-error.ts'
|
|
9
|
+
|
|
10
|
+
describe('describeError', () => {
|
|
11
|
+
it('returns the message of an Error instance', () => {
|
|
12
|
+
expect(describeError(new Error('boom'))).toBe('boom')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('returns the string form of a non-Error thrown value', () => {
|
|
16
|
+
expect(describeError('nope')).toBe('nope')
|
|
17
|
+
})
|
|
18
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 错误消息归一化(无 React 依赖,可单测)。
|
|
3
|
+
*
|
|
4
|
+
* 渲染任意抛出的值为可读消息:`Error` 取 `.message`,其余回落 `String(value)`。
|
|
5
|
+
* 仅供诊断展示,调用方永不按消息内容路由控制流。
|
|
6
|
+
* @module @deepseek-ai/dsh-client-ui-sandore/src/client/describe-error
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** 渲染任意抛出的值为可读消息(诊断用,永不按消息路由)。 */
|
|
10
|
+
export function describeError(error: unknown): string {
|
|
11
|
+
return error instanceof Error ? error.message : String(error)
|
|
12
|
+
}
|