@lokvis/ui-react 0.1.1-beta.0 → 0.2.1
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 +21 -0
- package/dist/components/AssetPanel.d.ts +2 -2
- package/dist/components/AssetPanel.d.ts.map +1 -1
- package/dist/components/AssetPanel.js +79 -7
- package/dist/components/AssetPanel.js.map +1 -1
- package/dist/components/Canvas.d.ts +9 -1
- package/dist/components/Canvas.d.ts.map +1 -1
- package/dist/components/Canvas.js +39 -2
- package/dist/components/Canvas.js.map +1 -1
- package/dist/components/CommandPalette.d.ts +48 -0
- package/dist/components/CommandPalette.d.ts.map +1 -0
- package/dist/components/CommandPalette.js +154 -0
- package/dist/components/CommandPalette.js.map +1 -0
- package/dist/components/CompareSlider.d.ts +25 -0
- package/dist/components/CompareSlider.d.ts.map +1 -0
- package/dist/components/CompareSlider.js +106 -0
- package/dist/components/CompareSlider.js.map +1 -0
- package/dist/components/DownloadPanel.d.ts +20 -0
- package/dist/components/DownloadPanel.d.ts.map +1 -0
- package/dist/components/DownloadPanel.js +169 -0
- package/dist/components/DownloadPanel.js.map +1 -0
- package/dist/components/ErrorBanner.d.ts +22 -0
- package/dist/components/ErrorBanner.d.ts.map +1 -0
- package/dist/components/ErrorBanner.js +47 -0
- package/dist/components/ErrorBanner.js.map +1 -0
- package/dist/components/ExifPanel.d.ts +26 -0
- package/dist/components/ExifPanel.d.ts.map +1 -0
- package/dist/components/ExifPanel.js +104 -0
- package/dist/components/ExifPanel.js.map +1 -0
- package/dist/components/GlobalDropzone.d.ts +37 -0
- package/dist/components/GlobalDropzone.d.ts.map +1 -0
- package/dist/components/GlobalDropzone.js +154 -0
- package/dist/components/GlobalDropzone.js.map +1 -0
- package/dist/components/HistoryPanel.d.ts +17 -7
- package/dist/components/HistoryPanel.d.ts.map +1 -1
- package/dist/components/HistoryPanel.js +74 -62
- package/dist/components/HistoryPanel.js.map +1 -1
- package/dist/components/Inspector.d.ts.map +1 -1
- package/dist/components/Inspector.js +2 -1
- package/dist/components/Inspector.js.map +1 -1
- package/dist/components/ProgressBar.d.ts +18 -0
- package/dist/components/ProgressBar.d.ts.map +1 -0
- package/dist/components/ProgressBar.js +35 -0
- package/dist/components/ProgressBar.js.map +1 -0
- package/dist/components/StatusBar.d.ts +8 -2
- package/dist/components/StatusBar.d.ts.map +1 -1
- package/dist/components/StatusBar.js +59 -4
- package/dist/components/StatusBar.js.map +1 -1
- package/dist/components/ThemeToggle.d.ts +20 -0
- package/dist/components/ThemeToggle.d.ts.map +1 -0
- package/dist/components/ThemeToggle.js +60 -0
- package/dist/components/ThemeToggle.js.map +1 -0
- package/dist/components/WorkflowEditor.d.ts +24 -0
- package/dist/components/WorkflowEditor.d.ts.map +1 -0
- package/dist/components/WorkflowEditor.js +160 -0
- package/dist/components/WorkflowEditor.js.map +1 -0
- package/dist/components/WorkflowTemplates.d.ts +23 -0
- package/dist/components/WorkflowTemplates.d.ts.map +1 -0
- package/dist/components/WorkflowTemplates.js +32 -0
- package/dist/components/WorkflowTemplates.js.map +1 -0
- package/dist/components/Workspace.d.ts +48 -2
- package/dist/components/Workspace.d.ts.map +1 -1
- package/dist/components/Workspace.js +78 -4
- package/dist/components/Workspace.js.map +1 -1
- package/dist/data/workflow-templates.d.ts +42 -0
- package/dist/data/workflow-templates.d.ts.map +1 -0
- package/dist/data/workflow-templates.js +112 -0
- package/dist/data/workflow-templates.js.map +1 -0
- package/dist/hooks/useDebouncedRun.d.ts +49 -0
- package/dist/hooks/useDebouncedRun.d.ts.map +1 -0
- package/dist/hooks/useDebouncedRun.js +117 -0
- package/dist/hooks/useDebouncedRun.js.map +1 -0
- package/dist/hooks/useLokvis.js +1 -1
- package/dist/hooks/useMediaQuery.d.ts +27 -0
- package/dist/hooks/useMediaQuery.d.ts.map +1 -0
- package/dist/hooks/useMediaQuery.js +41 -0
- package/dist/hooks/useMediaQuery.js.map +1 -0
- package/dist/hooks/useShareLink.d.ts +58 -0
- package/dist/hooks/useShareLink.d.ts.map +1 -0
- package/dist/hooks/useShareLink.js +120 -0
- package/dist/hooks/useShareLink.js.map +1 -0
- package/dist/hooks/useTheme.d.ts +30 -0
- package/dist/hooks/useTheme.d.ts.map +1 -0
- package/dist/hooks/useTheme.js +105 -0
- package/dist/hooks/useTheme.js.map +1 -0
- package/dist/hooks/useWorkflowProgress.d.ts +28 -0
- package/dist/hooks/useWorkflowProgress.d.ts.map +1 -0
- package/dist/hooks/useWorkflowProgress.js +25 -0
- package/dist/hooks/useWorkflowProgress.js.map +1 -0
- package/dist/hooks/useWorkflows.d.ts +86 -0
- package/dist/hooks/useWorkflows.d.ts.map +1 -0
- package/dist/hooks/useWorkflows.js +247 -0
- package/dist/hooks/useWorkflows.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -1
- package/dist/store/assets-slice.d.ts.map +1 -1
- package/dist/store/assets-slice.js +2 -0
- package/dist/store/assets-slice.js.map +1 -1
- package/dist/store/history-slice.d.ts +18 -0
- package/dist/store/history-slice.d.ts.map +1 -0
- package/dist/store/history-slice.js +42 -0
- package/dist/store/history-slice.js.map +1 -0
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +3 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/runtime-slice.d.ts +10 -2
- package/dist/store/runtime-slice.d.ts.map +1 -1
- package/dist/store/runtime-slice.js +77 -28
- package/dist/store/runtime-slice.js.map +1 -1
- package/dist/store/types.d.ts +70 -1
- package/dist/store/types.d.ts.map +1 -1
- package/dist/store/types.js +10 -0
- package/dist/store/types.js.map +1 -1
- package/dist/store/workflow-slice.d.ts +1 -1
- package/dist/store/workflow-slice.d.ts.map +1 -1
- package/dist/store/workflow-slice.js +128 -9
- package/dist/store/workflow-slice.js.map +1 -1
- package/package.json +9 -6
- package/src/components/AssetPanel.tsx +149 -8
- package/src/components/Canvas.tsx +79 -2
- package/src/components/CommandPalette.tsx +300 -0
- package/src/components/CompareSlider.tsx +186 -0
- package/src/components/DownloadPanel.tsx +252 -0
- package/src/components/ErrorBanner.tsx +95 -0
- package/src/components/ExifPanel.tsx +187 -0
- package/src/components/GlobalDropzone.tsx +220 -0
- package/src/components/HistoryPanel.tsx +231 -104
- package/src/components/Inspector.tsx +4 -0
- package/src/components/ProgressBar.tsx +104 -0
- package/src/components/StatusBar.tsx +124 -5
- package/src/components/ThemeToggle.tsx +122 -0
- package/src/components/WorkflowEditor.tsx +382 -0
- package/src/components/WorkflowTemplates.tsx +101 -0
- package/src/components/Workspace.tsx +205 -8
- package/src/data/workflow-templates.ts +137 -0
- package/src/hooks/useDebouncedRun.ts +151 -0
- package/src/hooks/useLokvis.ts +1 -1
- package/src/hooks/useMediaQuery.ts +53 -0
- package/src/hooks/useShareLink.ts +158 -0
- package/src/hooks/useTheme.ts +116 -0
- package/src/hooks/useWorkflowProgress.ts +41 -0
- package/src/hooks/useWorkflows.ts +326 -0
- package/src/index.ts +32 -0
- package/src/store/assets-slice.ts +2 -0
- package/src/store/history-slice.ts +65 -0
- package/src/store/index.ts +3 -0
- package/src/store/runtime-slice.ts +89 -32
- package/src/store/types.ts +71 -1
- package/src/store/workflow-slice.ts +139 -10
|
@@ -36,6 +36,7 @@ export const createAssetsSlice: StateCreator<
|
|
|
36
36
|
await runtime.importAsset({ kind: 'file', file });
|
|
37
37
|
}
|
|
38
38
|
await get().refreshAssets();
|
|
39
|
+
await get().refreshStorageUsage();
|
|
39
40
|
set({ statusMessage: `Imported ${files.length} file(s)` });
|
|
40
41
|
},
|
|
41
42
|
|
|
@@ -52,6 +53,7 @@ export const createAssetsSlice: StateCreator<
|
|
|
52
53
|
if (!runtime) return;
|
|
53
54
|
await runtime.removeAsset(id);
|
|
54
55
|
await get().refreshAssets();
|
|
56
|
+
await get().refreshStorageUsage();
|
|
55
57
|
if (get().selectedAssetId === id) set({ selectedAssetId: null });
|
|
56
58
|
|
|
57
59
|
// 修复 review 报告:原实现删除 asset 后未 revoke 缩略图 ObjectURL,导致
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* History slice —— 历史栈状态与撤销/重做/跳转(W7.1)
|
|
3
|
+
*
|
|
4
|
+
* 负责:
|
|
5
|
+
* - 跟踪当前活跃工作流的历史条目与游标(供 HistoryPanel 高亮当前步骤)
|
|
6
|
+
* - undo / redo / jumpTo 委托给 runtime,history:changed 事件回流自动更新 state
|
|
7
|
+
*
|
|
8
|
+
* 事件桥接:runtime-slice.init 订阅 history:changed,回调里 set 本 slice 的
|
|
9
|
+
* state,保证 store 是历史状态的单一来源,组件无需各自订阅。
|
|
10
|
+
*/
|
|
11
|
+
import type { StateCreator } from 'zustand';
|
|
12
|
+
import type { WorkspaceStore, WorkspaceState, WorkspaceActions } from './types.js';
|
|
13
|
+
|
|
14
|
+
export interface HistorySlice
|
|
15
|
+
extends Pick<
|
|
16
|
+
WorkspaceState,
|
|
17
|
+
'historyEntries' | 'historyCursor' | 'historyWorkflowId'
|
|
18
|
+
>,
|
|
19
|
+
Pick<WorkspaceActions, 'refreshHistory' | 'undo' | 'redo' | 'jumpToHistory'> {}
|
|
20
|
+
|
|
21
|
+
export const createHistorySlice: StateCreator<
|
|
22
|
+
WorkspaceStore,
|
|
23
|
+
[],
|
|
24
|
+
[],
|
|
25
|
+
HistorySlice
|
|
26
|
+
> = (set, get) => ({
|
|
27
|
+
historyEntries: [],
|
|
28
|
+
historyCursor: -1,
|
|
29
|
+
historyWorkflowId: null,
|
|
30
|
+
|
|
31
|
+
async refreshHistory(workflowId) {
|
|
32
|
+
const { runtime } = get();
|
|
33
|
+
if (!runtime) return;
|
|
34
|
+
try {
|
|
35
|
+
const { entries, cursor } = await runtime.getHistoryState(workflowId);
|
|
36
|
+
set({
|
|
37
|
+
historyEntries: entries,
|
|
38
|
+
historyCursor: cursor,
|
|
39
|
+
historyWorkflowId: workflowId,
|
|
40
|
+
});
|
|
41
|
+
} catch {
|
|
42
|
+
// 查询失败保持上次值,不阻断
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
async undo() {
|
|
47
|
+
const { runtime, historyWorkflowId } = get();
|
|
48
|
+
if (!runtime || !historyWorkflowId) return;
|
|
49
|
+
await runtime.undo(historyWorkflowId);
|
|
50
|
+
// history:changed 事件会触发 runtime-slice 的订阅回调更新 state,
|
|
51
|
+
// 此处无需手动 refresh,避免双发
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
async redo() {
|
|
55
|
+
const { runtime, historyWorkflowId } = get();
|
|
56
|
+
if (!runtime || !historyWorkflowId) return;
|
|
57
|
+
await runtime.redo(historyWorkflowId);
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
async jumpToHistory(index) {
|
|
61
|
+
const { runtime, historyWorkflowId } = get();
|
|
62
|
+
if (!runtime || !historyWorkflowId) return;
|
|
63
|
+
await runtime.jumpTo(historyWorkflowId, index);
|
|
64
|
+
},
|
|
65
|
+
});
|
package/src/store/index.ts
CHANGED
|
@@ -16,12 +16,15 @@ import { createRuntimeSlice } from './runtime-slice.js';
|
|
|
16
16
|
import { createAssetsSlice } from './assets-slice.js';
|
|
17
17
|
import { createCapabilitiesSlice } from './capabilities-slice.js';
|
|
18
18
|
import { createWorkflowSlice } from './workflow-slice.js';
|
|
19
|
+
import { createHistorySlice } from './history-slice.js';
|
|
19
20
|
|
|
20
21
|
export const useWorkspaceStore = create<WorkspaceStore>()((...a) => ({
|
|
21
22
|
...createRuntimeSlice(...a),
|
|
22
23
|
...createAssetsSlice(...a),
|
|
23
24
|
...createCapabilitiesSlice(...a),
|
|
24
25
|
...createWorkflowSlice(...a),
|
|
26
|
+
...createHistorySlice(...a),
|
|
25
27
|
}));
|
|
26
28
|
|
|
27
29
|
export type { WorkspaceState, WorkspaceActions, WorkspaceStore } from './types.js';
|
|
30
|
+
export { MAX_WORKFLOW_STEPS } from './types.js';
|
|
@@ -1,53 +1,110 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Runtime slice —— Runtime 初始化与状态消息
|
|
3
3
|
*
|
|
4
|
-
* 负责 runtime
|
|
4
|
+
* 负责 runtime 实例注入、初始化、全局状态消息与错误、存储配额(W6.7)、
|
|
5
|
+
* 历史栈事件桥接(W7.1)。
|
|
5
6
|
*/
|
|
6
7
|
import type { StateCreator } from 'zustand';
|
|
8
|
+
import type { LokvisEvent } from '@lokvis/schema';
|
|
7
9
|
import type { WorkspaceStore, WorkspaceState, WorkspaceActions } from './types.js';
|
|
8
10
|
|
|
9
11
|
export interface RuntimeSlice
|
|
10
12
|
extends Pick<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
WorkspaceState,
|
|
14
|
+
| 'runtime'
|
|
15
|
+
| 'initializing'
|
|
16
|
+
| 'initError'
|
|
17
|
+
| 'running'
|
|
18
|
+
| 'statusMessage'
|
|
19
|
+
| 'error'
|
|
20
|
+
| 'errorSeq'
|
|
21
|
+
| 'storageUsage'
|
|
22
|
+
>,
|
|
14
23
|
Pick<
|
|
15
24
|
WorkspaceActions,
|
|
16
|
-
'init' | 'setStatus' | 'setError'
|
|
25
|
+
'init' | 'setStatus' | 'setError' | 'refreshStorageUsage'
|
|
17
26
|
> {}
|
|
18
27
|
|
|
28
|
+
/**
|
|
29
|
+
* 历史事件订阅解除函数。
|
|
30
|
+
*
|
|
31
|
+
* 修复 review 报告:原为模块级 let,多个 store 实例(测试 / 多 Workspace)
|
|
32
|
+
* 共用同一变量,后创建的 store 会覆盖前者,先前的订阅无法被正确清理 → 泄漏。
|
|
33
|
+
* 现迁到 createRuntime slice 工厂闭包内,每个 store 实例持有独立 offFn。
|
|
34
|
+
*/
|
|
19
35
|
export const createRuntimeSlice: StateCreator<
|
|
20
36
|
WorkspaceStore,
|
|
21
37
|
[],
|
|
22
38
|
[],
|
|
23
39
|
RuntimeSlice
|
|
24
|
-
> = (set, get) =>
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
> = (set, get) => {
|
|
41
|
+
/** 当前 store 实例的历史订阅解除函数(闭包私有,实例间隔离) */
|
|
42
|
+
let offHistoryChanged: (() => void) | null = null;
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
runtime: null,
|
|
46
|
+
initializing: false,
|
|
47
|
+
initError: null,
|
|
48
|
+
running: false,
|
|
49
|
+
statusMessage: 'Idle',
|
|
50
|
+
error: null,
|
|
51
|
+
errorSeq: 0,
|
|
52
|
+
storageUsage: null,
|
|
53
|
+
|
|
54
|
+
async init(runtime) {
|
|
55
|
+
// 重入 init 时先清理上一次的历史订阅(避免重复订阅泄漏)
|
|
56
|
+
if (offHistoryChanged) {
|
|
57
|
+
offHistoryChanged();
|
|
58
|
+
offHistoryChanged = null;
|
|
59
|
+
}
|
|
60
|
+
set({ runtime, initializing: true, initError: null });
|
|
61
|
+
try {
|
|
62
|
+
// W7.1: 订阅 history:changed,把 runtime 历史事件桥接到 store,
|
|
63
|
+
// 使 HistoryPanel 无需各自订阅,store 成为历史状态单一来源。
|
|
64
|
+
// workspace 单工作流场景下直接采用事件携带的 workflowId/entries/cursor
|
|
65
|
+
// 覆盖 store,无需额外过滤。
|
|
66
|
+
offHistoryChanged = runtime.eventBus.on('history:changed', (e: LokvisEvent) => {
|
|
67
|
+
if (e.type !== 'history:changed') return;
|
|
68
|
+
set({
|
|
69
|
+
historyEntries: e.entries,
|
|
70
|
+
historyCursor: e.currentIndex,
|
|
71
|
+
historyWorkflowId: e.workflowId,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
await get().refreshAssets();
|
|
75
|
+
await get().refreshCapabilities();
|
|
76
|
+
await get().refreshStorageUsage();
|
|
77
|
+
set({ initializing: false, statusMessage: 'Ready' });
|
|
78
|
+
} catch (err) {
|
|
79
|
+
set({
|
|
80
|
+
initializing: false,
|
|
81
|
+
initError: err instanceof Error ? err.message : String(err),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
},
|
|
31
85
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
await get().refreshAssets();
|
|
36
|
-
await get().refreshCapabilities();
|
|
37
|
-
set({ initializing: false, statusMessage: 'Ready' });
|
|
38
|
-
} catch (err) {
|
|
39
|
-
set({
|
|
40
|
-
initializing: false,
|
|
41
|
-
initError: err instanceof Error ? err.message : String(err),
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
},
|
|
86
|
+
setStatus(message) {
|
|
87
|
+
set({ statusMessage: message });
|
|
88
|
+
},
|
|
45
89
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
90
|
+
setError(error) {
|
|
91
|
+
// errorSeq 在设置非 null error 时递增,使 ErrorBanner 能感知新错误事件
|
|
92
|
+
// 重新弹出(避免相同错误消息重复出现时 banner 因引用相等不触发 useEffect)
|
|
93
|
+
set((state) => ({
|
|
94
|
+
error,
|
|
95
|
+
errorSeq: error !== null ? state.errorSeq + 1 : state.errorSeq,
|
|
96
|
+
}));
|
|
97
|
+
},
|
|
49
98
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
99
|
+
async refreshStorageUsage() {
|
|
100
|
+
const { runtime } = get();
|
|
101
|
+
if (!runtime) return;
|
|
102
|
+
try {
|
|
103
|
+
const usage = await runtime.getStorageUsage();
|
|
104
|
+
set({ storageUsage: usage });
|
|
105
|
+
} catch {
|
|
106
|
+
// 查询失败不阻断,保持上次值或 null
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
};
|
package/src/store/types.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 把原 store.ts 顶部的 WorkspaceState / WorkspaceActions 抽出,
|
|
5
5
|
* 供各 slice 引用,避免循环依赖。
|
|
6
6
|
*/
|
|
7
|
-
import type { Asset, Capability } from '@lokvis/schema';
|
|
7
|
+
import type { Asset, Capability, HistoryEntry } from '@lokvis/schema';
|
|
8
8
|
import type { LokvisRuntime } from '@lokvis/runtime';
|
|
9
9
|
import type { CapabilityMap, NodeStatus, WorkspaceNode } from '../types.js';
|
|
10
10
|
|
|
@@ -40,6 +40,30 @@ export interface WorkspaceState {
|
|
|
40
40
|
statusMessage: string;
|
|
41
41
|
/** 错误信息 */
|
|
42
42
|
error: string | null;
|
|
43
|
+
/**
|
|
44
|
+
* 错误事件序号(单调递增)。每次 setError(非 null) 都递增,使
|
|
45
|
+
* ErrorBanner 能在相同错误消息重复出现时仍感知到"新错误事件"重新弹出。
|
|
46
|
+
* null error 不递增。0 表示初始无错误状态。
|
|
47
|
+
*/
|
|
48
|
+
errorSeq: number;
|
|
49
|
+
|
|
50
|
+
/** 存储配额使用情况(W6.7):{ usage, quota } 字节,null 表示未查询 */
|
|
51
|
+
storageUsage: { usage: number; quota: number } | null;
|
|
52
|
+
|
|
53
|
+
/** 历史栈条目(W7.1):当前活跃工作流的执行历史 */
|
|
54
|
+
historyEntries: HistoryEntry[];
|
|
55
|
+
/** 历史栈游标(W7.1):-1 表示无已应用条目(初始);i 表示第 i 条已应用 */
|
|
56
|
+
historyCursor: number;
|
|
57
|
+
/** 当前历史所属的工作流 ID(W7.1) */
|
|
58
|
+
historyWorkflowId: string | null;
|
|
59
|
+
|
|
60
|
+
/** 上次工作流执行的输出 Asset ID 列表(W9.4/W9.5):用于 before/after 对比与下载管理 */
|
|
61
|
+
lastOutputIds: string[];
|
|
62
|
+
/** 当前选中的输出 Asset ID(W9.4):用于多输出场景选择 */
|
|
63
|
+
selectedOutputId: string | null;
|
|
64
|
+
|
|
65
|
+
/** 当前正在运行的工作流 ID(W11.6:用于 cancel) */
|
|
66
|
+
currentRunId: string | null;
|
|
43
67
|
}
|
|
44
68
|
|
|
45
69
|
/** 工作台操作 */
|
|
@@ -69,19 +93,65 @@ export interface WorkspaceActions {
|
|
|
69
93
|
selectNode(id: string | null): void;
|
|
70
94
|
/** 设置节点状态 */
|
|
71
95
|
setNodeStatus(id: string, status: NodeStatus, error?: string, duration?: number): void;
|
|
96
|
+
/**
|
|
97
|
+
* 移动节点到新位置(线性链重排,W10.4)。
|
|
98
|
+
* @param from 源索引(0-based)
|
|
99
|
+
* @param to 目标索引(0-based,移动后该节点的新位置)
|
|
100
|
+
*/
|
|
101
|
+
moveNode(from: number, to: number): void;
|
|
102
|
+
/**
|
|
103
|
+
* 在指定位置插入节点(W11.1)。
|
|
104
|
+
* @param index 目标位置(0-based;越界时自动 clamp 到 [0, length])
|
|
105
|
+
* @param capability 能力名
|
|
106
|
+
*/
|
|
107
|
+
insertNodeAt(index: number, capability: string): void;
|
|
72
108
|
|
|
73
109
|
/** 执行工作流 */
|
|
74
110
|
run(): Promise<Asset[]>;
|
|
111
|
+
/** 取消当前运行(W11.6) */
|
|
112
|
+
cancelRun(): Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* 应用工作流模板(W11.4):替换当前 nodes 为模板节点序列。
|
|
115
|
+
* @param templateNodes 模板节点(capability + 默认 params)
|
|
116
|
+
*/
|
|
117
|
+
loadWorkflowTemplate(
|
|
118
|
+
templateNodes: Array<{ capability: string; params: Record<string, unknown> }>
|
|
119
|
+
): void;
|
|
75
120
|
/** 设置状态消息 */
|
|
76
121
|
setStatus(message: string): void;
|
|
77
122
|
/** 设置错误 */
|
|
78
123
|
setError(error: string | null): void;
|
|
124
|
+
/** 刷新存储配额使用情况(W6.7) */
|
|
125
|
+
refreshStorageUsage(): Promise<void>;
|
|
126
|
+
/** 刷新历史栈(W7.1) */
|
|
127
|
+
refreshHistory(workflowId: string): Promise<void>;
|
|
128
|
+
/** 撤销一步(W7.1) */
|
|
129
|
+
undo(): Promise<void>;
|
|
130
|
+
/** 重做一步(W7.1) */
|
|
131
|
+
redo(): Promise<void>;
|
|
132
|
+
/** 跳转到指定历史条目(W7.1) */
|
|
133
|
+
jumpToHistory(index: number): Promise<void>;
|
|
79
134
|
/** 清空工作流 */
|
|
80
135
|
clearWorkflow(): void;
|
|
136
|
+
/** 选择输出资产(W9.4) */
|
|
137
|
+
selectOutput(id: string | null): void;
|
|
138
|
+
/** 清空上次输出列表(W9.4/W9.5) */
|
|
139
|
+
clearOutputs(): void;
|
|
81
140
|
}
|
|
82
141
|
|
|
83
142
|
export type WorkspaceStore = WorkspaceState & WorkspaceActions;
|
|
84
143
|
|
|
144
|
+
/**
|
|
145
|
+
* 工作流最大节点数(M1 MVP 约束)。
|
|
146
|
+
*
|
|
147
|
+
* UI 层独立定义此常量而非 import runtime 包的 MAX_WORKFLOW_STEPS,
|
|
148
|
+
* 因为 ui-react → runtime 的方向依赖违反五层架构约束(UI → Workflow → Runtime)。
|
|
149
|
+
* 两层各自维护同值常量,通过测试 + 文档约定保持同步
|
|
150
|
+
* (runtime 测试断言 MAX_WORKFLOW_STEPS === 5,UI 层此常量也为 5)。
|
|
151
|
+
* 调整上限时需同步修改两处 + 相关注释。
|
|
152
|
+
*/
|
|
153
|
+
export const MAX_WORKFLOW_STEPS = 5;
|
|
154
|
+
|
|
85
155
|
/** 生成节点 ID */
|
|
86
156
|
export function genNodeId(): string {
|
|
87
157
|
return `node_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
@@ -12,10 +12,10 @@ import type { StateCreator } from 'zustand';
|
|
|
12
12
|
import type { Asset, Workflow, WorkflowEdge, WorkflowNode } from '@lokvis/schema';
|
|
13
13
|
import type { WorkspaceNode } from '../types.js';
|
|
14
14
|
import type { WorkspaceStore, WorkspaceState, WorkspaceActions } from './types.js';
|
|
15
|
-
import { genNodeId } from './types.js';
|
|
15
|
+
import { genNodeId, MAX_WORKFLOW_STEPS } from './types.js';
|
|
16
16
|
|
|
17
17
|
export interface WorkflowSlice
|
|
18
|
-
extends Pick<WorkspaceState, 'nodes' | 'selectedNodeId'>,
|
|
18
|
+
extends Pick<WorkspaceState, 'nodes' | 'selectedNodeId' | 'lastOutputIds' | 'selectedOutputId' | 'currentRunId'>,
|
|
19
19
|
Pick<
|
|
20
20
|
WorkspaceActions,
|
|
21
21
|
| 'addNode'
|
|
@@ -23,8 +23,14 @@ export interface WorkflowSlice
|
|
|
23
23
|
| 'removeNode'
|
|
24
24
|
| 'selectNode'
|
|
25
25
|
| 'setNodeStatus'
|
|
26
|
+
| 'moveNode'
|
|
27
|
+
| 'insertNodeAt'
|
|
26
28
|
| 'run'
|
|
29
|
+
| 'cancelRun'
|
|
30
|
+
| 'loadWorkflowTemplate'
|
|
27
31
|
| 'clearWorkflow'
|
|
32
|
+
| 'selectOutput'
|
|
33
|
+
| 'clearOutputs'
|
|
28
34
|
> {}
|
|
29
35
|
|
|
30
36
|
export const createWorkflowSlice: StateCreator<
|
|
@@ -35,8 +41,16 @@ export const createWorkflowSlice: StateCreator<
|
|
|
35
41
|
> = (set, get) => ({
|
|
36
42
|
nodes: [],
|
|
37
43
|
selectedNodeId: null,
|
|
44
|
+
lastOutputIds: [],
|
|
45
|
+
selectedOutputId: null,
|
|
46
|
+
currentRunId: null,
|
|
38
47
|
|
|
39
48
|
addNode(capability) {
|
|
49
|
+
// W10.1/W10.3: 节点数上限(与 runtime MAX_WORKFLOW_STEPS 对齐,UI 层独立常量)
|
|
50
|
+
if (get().nodes.length >= MAX_WORKFLOW_STEPS) {
|
|
51
|
+
get().setError(`工作流最多 ${MAX_WORKFLOW_STEPS} 个节点(M1 MVP 限制),请先删除不需要的节点`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
40
54
|
const node: WorkspaceNode = {
|
|
41
55
|
id: genNodeId(),
|
|
42
56
|
capability,
|
|
@@ -74,13 +88,96 @@ export const createWorkflowSlice: StateCreator<
|
|
|
74
88
|
}));
|
|
75
89
|
},
|
|
76
90
|
|
|
91
|
+
moveNode(from, to) {
|
|
92
|
+
set((state) => {
|
|
93
|
+
if (
|
|
94
|
+
from < 0 || from >= state.nodes.length ||
|
|
95
|
+
to < 0 || to >= state.nodes.length ||
|
|
96
|
+
from === to
|
|
97
|
+
) {
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
const next = [...state.nodes];
|
|
101
|
+
const [moved] = next.splice(from, 1);
|
|
102
|
+
if (!moved) return {};
|
|
103
|
+
next.splice(to, 0, moved);
|
|
104
|
+
return { nodes: next };
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
insertNodeAt(index, capability) {
|
|
109
|
+
// W11.1: 在指定位置插入节点(与节点数上限对齐)
|
|
110
|
+
if (get().nodes.length >= MAX_WORKFLOW_STEPS) {
|
|
111
|
+
get().setError(`工作流最多 ${MAX_WORKFLOW_STEPS} 个节点(M1 MVP 限制),请先删除不需要的节点`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (!capability) return;
|
|
115
|
+
const node: WorkspaceNode = {
|
|
116
|
+
id: genNodeId(),
|
|
117
|
+
capability,
|
|
118
|
+
params: {},
|
|
119
|
+
status: 'idle',
|
|
120
|
+
};
|
|
121
|
+
set((state) => {
|
|
122
|
+
const next = [...state.nodes];
|
|
123
|
+
const at = Math.max(0, Math.min(index, next.length));
|
|
124
|
+
next.splice(at, 0, node);
|
|
125
|
+
return { nodes: next, selectedNodeId: node.id };
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
loadWorkflowTemplate(templateNodes) {
|
|
130
|
+
// W11.4: 用模板节点序列替换当前工作流,清空上次输出
|
|
131
|
+
const nodes: WorkspaceNode[] = templateNodes.map(({ capability, params }) => ({
|
|
132
|
+
id: genNodeId(),
|
|
133
|
+
capability,
|
|
134
|
+
params,
|
|
135
|
+
status: 'idle' as const,
|
|
136
|
+
}));
|
|
137
|
+
set({
|
|
138
|
+
nodes,
|
|
139
|
+
selectedNodeId: nodes[0]?.id ?? null,
|
|
140
|
+
lastOutputIds: [],
|
|
141
|
+
selectedOutputId: null,
|
|
142
|
+
error: null,
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
async cancelRun() {
|
|
147
|
+
// W11.6: 取消当前运行。本函数仅负责向 runtime 发起 cancel 信号;
|
|
148
|
+
// running / currentRunId / 节点状态的归位由 run() 的 finally 统一处理,
|
|
149
|
+
// 避免 cancel 与 run() 的 finally 同时写状态造成竞态
|
|
150
|
+
// (review 反馈:若 cancel() 抛错,旧实现仍会 set running: false 伪造
|
|
151
|
+
// "已停止",但底层工作流仍在运行,UI 状态与实际不符)。
|
|
152
|
+
//
|
|
153
|
+
// 时序:cancel() 成功 → runtime.run() 返回 status='cancelled'
|
|
154
|
+
// → run() 兜底把 pending/running 节点标 cancelled + finally 归位。
|
|
155
|
+
// cancel() 抛错:不伪造停止状态,仅记录错误让用户感知"取消失败,仍在运行"。
|
|
156
|
+
const { runtime, currentRunId } = get();
|
|
157
|
+
if (!runtime || !currentRunId) return;
|
|
158
|
+
set({ statusMessage: 'Cancelling...' });
|
|
159
|
+
try {
|
|
160
|
+
await runtime.cancel(currentRunId);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
163
|
+
console.warn('[lokvis] cancelRun failed:', message);
|
|
164
|
+
get().setError(`取消失败,工作流仍在运行:${message}`);
|
|
165
|
+
}
|
|
166
|
+
// 不在此设置 running / currentRunId / 节点状态;
|
|
167
|
+
// 等 run() 的 await runtime.run() 返回后,由 run() 统一归位。
|
|
168
|
+
},
|
|
169
|
+
|
|
77
170
|
async run() {
|
|
78
|
-
const { runtime, nodes, selectedAssetId } = get();
|
|
171
|
+
const { runtime, nodes, selectedAssetId, running } = get();
|
|
79
172
|
if (!runtime) throw new Error('Runtime not initialized');
|
|
80
173
|
if (nodes.length === 0) throw new Error('Workflow is empty');
|
|
81
174
|
if (!selectedAssetId) throw new Error('No asset selected');
|
|
175
|
+
// 并发保护:正在运行时再次调用直接拒绝(避免事件订阅泄漏 + currentRunId 覆盖)
|
|
176
|
+
if (running) {
|
|
177
|
+
throw new Error('Workflow is already running');
|
|
178
|
+
}
|
|
82
179
|
|
|
83
|
-
set({ running: true, error: null, statusMessage: 'Running...' });
|
|
180
|
+
set({ running: true, error: null, statusMessage: 'Running...', currentRunId: null });
|
|
84
181
|
|
|
85
182
|
// 重置所有节点状态
|
|
86
183
|
set((state) => ({
|
|
@@ -110,6 +207,8 @@ export const createWorkflowSlice: StateCreator<
|
|
|
110
207
|
try {
|
|
111
208
|
// 构建 Workflow
|
|
112
209
|
const workflow = buildLinearWorkflow(nodes);
|
|
210
|
+
// W11.6: 记录当前 workflowId 供 cancelRun() 使用
|
|
211
|
+
set({ currentRunId: workflow.id });
|
|
113
212
|
const input = await runtime.getAsset(selectedAssetId);
|
|
114
213
|
|
|
115
214
|
set({ statusMessage: 'Running workflow...' });
|
|
@@ -127,15 +226,30 @@ export const createWorkflowSlice: StateCreator<
|
|
|
127
226
|
}
|
|
128
227
|
}
|
|
129
228
|
|
|
130
|
-
//
|
|
229
|
+
// 加载输出资产。失败时收集失败 id 并设置 error 让用户感知(不静默丢弃)
|
|
131
230
|
const outputs: Asset[] = [];
|
|
231
|
+
const failedOutputIds: string[] = [];
|
|
132
232
|
for (const id of result.outputs) {
|
|
133
233
|
try {
|
|
134
234
|
outputs.push(await runtime.getAsset(id));
|
|
135
|
-
} catch {
|
|
136
|
-
|
|
235
|
+
} catch (err) {
|
|
236
|
+
console.warn(`[lokvis] getAsset(${id}) failed:`, err);
|
|
237
|
+
failedOutputIds.push(id);
|
|
137
238
|
}
|
|
138
239
|
}
|
|
240
|
+
if (failedOutputIds.length > 0) {
|
|
241
|
+
get().setError(`${failedOutputIds.length} 个输出资产加载失败,可能存储损坏`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// W9.4/W9.5: 记录输出 Asset ID,供 before/after 对比与下载管理使用。
|
|
245
|
+
// 仅在 status === 'completed' 时记录,失败/取消的输出无意义。
|
|
246
|
+
if (result.status === 'completed' && outputs.length > 0) {
|
|
247
|
+
const firstOutput = outputs[0];
|
|
248
|
+
set({
|
|
249
|
+
lastOutputIds: outputs.map((o) => o.id),
|
|
250
|
+
selectedOutputId: firstOutput ? firstOutput.id : null,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
139
253
|
|
|
140
254
|
set({
|
|
141
255
|
running: false,
|
|
@@ -143,18 +257,20 @@ export const createWorkflowSlice: StateCreator<
|
|
|
143
257
|
});
|
|
144
258
|
|
|
145
259
|
if (result.status === 'failed') {
|
|
146
|
-
|
|
260
|
+
get().setError(result.error ?? 'Workflow failed');
|
|
147
261
|
}
|
|
148
262
|
|
|
149
263
|
await get().refreshAssets();
|
|
264
|
+
await get().refreshStorageUsage();
|
|
150
265
|
return outputs;
|
|
151
266
|
} catch (err) {
|
|
152
267
|
const message = err instanceof Error ? err.message : String(err);
|
|
153
268
|
set({
|
|
154
269
|
running: false,
|
|
155
|
-
|
|
270
|
+
currentRunId: null,
|
|
156
271
|
statusMessage: 'Failed',
|
|
157
272
|
});
|
|
273
|
+
get().setError(message);
|
|
158
274
|
// 异常退出时把仍在 pending/running 的节点标 failed
|
|
159
275
|
for (const node of get().nodes) {
|
|
160
276
|
if (node.status === 'pending' || node.status === 'running') {
|
|
@@ -163,6 +279,8 @@ export const createWorkflowSlice: StateCreator<
|
|
|
163
279
|
}
|
|
164
280
|
throw err;
|
|
165
281
|
} finally {
|
|
282
|
+
// W11.6: 清除 currentRunId(成功/失败/取消都应清除)
|
|
283
|
+
set({ currentRunId: null });
|
|
166
284
|
offStarted();
|
|
167
285
|
offFinished();
|
|
168
286
|
offNodeFailed();
|
|
@@ -172,6 +290,14 @@ export const createWorkflowSlice: StateCreator<
|
|
|
172
290
|
clearWorkflow() {
|
|
173
291
|
set({ nodes: [], selectedNodeId: null });
|
|
174
292
|
},
|
|
293
|
+
|
|
294
|
+
selectOutput(id) {
|
|
295
|
+
set({ selectedOutputId: id });
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
clearOutputs() {
|
|
299
|
+
set({ lastOutputIds: [], selectedOutputId: null });
|
|
300
|
+
},
|
|
175
301
|
});
|
|
176
302
|
|
|
177
303
|
/** 把工作台节点序列构建为线性 Workflow */
|
|
@@ -187,7 +313,10 @@ function buildLinearWorkflow(nodes: WorkspaceNode[]): Workflow {
|
|
|
187
313
|
}));
|
|
188
314
|
const edges: WorkflowEdge[] = [];
|
|
189
315
|
for (let i = 0; i < workflowNodes.length - 1; i++) {
|
|
190
|
-
|
|
316
|
+
const fromNode = workflowNodes[i];
|
|
317
|
+
const toNode = workflowNodes[i + 1];
|
|
318
|
+
if (!fromNode || !toNode) continue;
|
|
319
|
+
edges.push({ from: fromNode.id, to: toNode.id });
|
|
191
320
|
}
|
|
192
321
|
return {
|
|
193
322
|
id: `wf_${Date.now().toString(36)}`,
|