minimal-workflow 0.2.0

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 (37) hide show
  1. package/index.html +12 -0
  2. package/package.json +44 -0
  3. package/public/manifest.fallback.json +482 -0
  4. package/scripts/cli.ts +318 -0
  5. package/scripts/server.ts +131 -0
  6. package/src/App.tsx +119 -0
  7. package/src/api/files.ts +32 -0
  8. package/src/api/manifest.ts +17 -0
  9. package/src/canvas/WorkflowCanvas.tsx +161 -0
  10. package/src/canvas/edges/SequentialEdge.tsx +14 -0
  11. package/src/canvas/nodes/AssertNode.tsx +17 -0
  12. package/src/canvas/nodes/BaseNode.tsx +55 -0
  13. package/src/canvas/nodes/BranchNode.tsx +21 -0
  14. package/src/canvas/nodes/LlmNode.tsx +29 -0
  15. package/src/canvas/nodes/LoopNode.tsx +22 -0
  16. package/src/canvas/nodes/PauseNode.tsx +17 -0
  17. package/src/canvas/nodes/SkillNode.tsx +27 -0
  18. package/src/canvas/nodes/ToolNode.tsx +27 -0
  19. package/src/index.css +379 -0
  20. package/src/inspector/Inspector.tsx +224 -0
  21. package/src/inspector/LlmNodeForm.tsx +59 -0
  22. package/src/inspector/SkillNodeForm.tsx +62 -0
  23. package/src/inspector/ToolNodeForm.tsx +120 -0
  24. package/src/main.tsx +10 -0
  25. package/src/palette/ControlFlowSection.tsx +45 -0
  26. package/src/palette/Palette.tsx +65 -0
  27. package/src/palette/SkillPaletteSection.tsx +31 -0
  28. package/src/palette/ToolPaletteSection.tsx +31 -0
  29. package/src/store/filesStore.ts +29 -0
  30. package/src/store/manifestStore.ts +25 -0
  31. package/src/store/workflowStore.ts +140 -0
  32. package/src/types.ts +83 -0
  33. package/src/utils/safePath.ts +49 -0
  34. package/src/yaml-view/YamlEditor.tsx +71 -0
  35. package/src/yaml-view/sync.ts +246 -0
  36. package/tsconfig.json +24 -0
  37. package/vite.config.ts +21 -0
@@ -0,0 +1,71 @@
1
+ /**
2
+ * YAML 视图(MVP):纯 textarea + 工具栏。
3
+ *
4
+ * 设计要点:
5
+ * - 打开时从 store.toWorkflowDef() 拉一次快照,序列化成 yaml 文本
6
+ * - 用户编辑只改本地 state;点"应用到图"才回写 store
7
+ * - 应用前先 parse + validate,任一报错 → 红条提示,不污染图
8
+ * - 不引 Monaco,等 P6 升级
9
+ */
10
+
11
+ import { useState } from 'react';
12
+ import { useWorkflowStore } from '../store/workflowStore.ts';
13
+ import {
14
+ parseWorkflowYaml,
15
+ stringifyWorkflowYaml,
16
+ validateWorkflow,
17
+ } from './sync.ts';
18
+
19
+ interface YamlEditorProps {
20
+ onClose: () => void;
21
+ }
22
+
23
+ export function YamlEditor({ onClose }: YamlEditorProps) {
24
+ const def = useWorkflowStore((s) => s.toWorkflowDef());
25
+ const loadWorkflow = useWorkflowStore((s) => s.loadWorkflow);
26
+
27
+ // 初始化一次快照;之后由 textarea 自己持有
28
+ const [text, setText] = useState<string>(() => {
29
+ try {
30
+ return stringifyWorkflowYaml(def);
31
+ } catch (e) {
32
+ return `# 序列化失败: ${(e as Error).message}\n`;
33
+ }
34
+ });
35
+ const [error, setError] = useState<string | null>(null);
36
+
37
+ const applyToGraph = () => {
38
+ try {
39
+ const parsed = parseWorkflowYaml(text);
40
+ const errs = validateWorkflow(parsed);
41
+ if (errs.length > 0) {
42
+ setError(errs.join('\n'));
43
+ return;
44
+ }
45
+ loadWorkflow(parsed);
46
+ setError(null);
47
+ onClose();
48
+ } catch (e) {
49
+ setError((e as Error).message);
50
+ }
51
+ };
52
+
53
+ return (
54
+ <div className="yaml-view">
55
+ <div className="yaml-toolbar">
56
+ <span>YAML 视图</span>
57
+ <span style={{ flex: 1 }} />
58
+ <button onClick={applyToGraph} className="primary">
59
+ 应用到图
60
+ </button>
61
+ <button onClick={onClose}>取消</button>
62
+ </div>
63
+ {error && <div className="yaml-error">{error}</div>}
64
+ <textarea
65
+ value={text}
66
+ onChange={(e) => setText(e.target.value)}
67
+ spellCheck={false}
68
+ />
69
+ </div>
70
+ );
71
+ }
@@ -0,0 +1,246 @@
1
+ /**
2
+ * 图模型 ↔ YAML 文本的纯函数式互转。
3
+ *
4
+ * 设计要点(参见 docs/workflow-editor-design.md §7):
5
+ * - 纯函数,无副作用,可在浏览器 / Bun / Node 任意环境跑(不 import path/fs)
6
+ * - 不 import minimal-agent/src/* 任何代码(D2 解耦红线)
7
+ * - 用 `yaml` 包做 AST 级别的 parse / stringify,给将来 CST 模式留余地
8
+ * - 与 plugins/workflow-runner/src/runner.ts::stepKind() 保持同义
9
+ */
10
+
11
+ import YAML from 'yaml';
12
+ import type { StepDef, StepKind, WorkflowDef } from '../types.ts';
13
+
14
+ /* ─────────────────────────────────────────────────────────────────────────
15
+ * parseWorkflowYaml
16
+ * ──────────────────────────────────────────────────────────────────────── */
17
+
18
+ /** 把 yaml 字符串解析为 WorkflowDef;失败抛 Error(含人类可读 message)。 */
19
+ export function parseWorkflowYaml(text: string): WorkflowDef {
20
+ let raw: unknown;
21
+ try {
22
+ raw = YAML.parse(text);
23
+ } catch (e) {
24
+ throw new Error(`YAML 解析失败: ${(e as Error).message}`);
25
+ }
26
+
27
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
28
+ throw new Error('Invalid workflow: 顶层必须是对象');
29
+ }
30
+
31
+ const obj = raw as Record<string, unknown>;
32
+ if (typeof obj.name !== 'string' || !obj.name.trim()) {
33
+ throw new Error('Invalid workflow: missing "name" or "steps"');
34
+ }
35
+ if (!Array.isArray(obj.steps)) {
36
+ throw new Error('Invalid workflow: missing "name" or "steps"');
37
+ }
38
+
39
+ // 每个 step 至少要有 id(其余字段交给 validateWorkflow 做深检)
40
+ for (let i = 0; i < obj.steps.length; i++) {
41
+ const s = obj.steps[i] as unknown;
42
+ if (!s || typeof s !== 'object' || Array.isArray(s)) {
43
+ throw new Error(`Invalid workflow: steps[${i}] 不是对象`);
44
+ }
45
+ const sid = (s as Record<string, unknown>).id;
46
+ if (typeof sid !== 'string' || !sid.trim()) {
47
+ throw new Error(`Invalid workflow: steps[${i}] 缺少 id`);
48
+ }
49
+ }
50
+
51
+ return obj as unknown as WorkflowDef;
52
+ }
53
+
54
+ /* ─────────────────────────────────────────────────────────────────────────
55
+ * stringifyWorkflowYaml
56
+ * ──────────────────────────────────────────────────────────────────────── */
57
+
58
+ /** 字段顺序:name → description → version → inputs → steps → __meta */
59
+ const TOP_LEVEL_ORDER = [
60
+ 'name',
61
+ 'description',
62
+ 'version',
63
+ 'inputs',
64
+ 'steps',
65
+ '__meta',
66
+ ] as const;
67
+
68
+ /** 把 WorkflowDef 序列化为 yaml 字符串;保证可被 minimal-agent runtime 解析。 */
69
+ export function stringifyWorkflowYaml(def: WorkflowDef): string {
70
+ // 1) 把 def 拷成一个字段顺序受控的 plain object
71
+ const ordered: Record<string, unknown> = {};
72
+ for (const key of TOP_LEVEL_ORDER) {
73
+ const v = (def as unknown as Record<string, unknown>)[key];
74
+ if (v === undefined || v === null) continue;
75
+ // 空的 inputs 数组省略,避免 yaml 多一行 `inputs: []`
76
+ if (key === 'inputs' && Array.isArray(v) && v.length === 0) continue;
77
+ ordered[key] = v;
78
+ }
79
+ // 透传其它非标准字段(防止丢字段)
80
+ for (const k of Object.keys(def)) {
81
+ if (!(k in ordered) && !TOP_LEVEL_ORDER.includes(k as never)) {
82
+ ordered[k] = (def as unknown as Record<string, unknown>)[k];
83
+ }
84
+ }
85
+
86
+ // 2) __meta.layout 的 x/y 保证整数
87
+ const meta = ordered.__meta as
88
+ | { layout?: { id: string; x: number; y: number }[] }
89
+ | undefined;
90
+ if (meta?.layout && Array.isArray(meta.layout)) {
91
+ meta.layout = meta.layout.map((l) => ({
92
+ id: l.id,
93
+ x: Math.round(l.x),
94
+ y: Math.round(l.y),
95
+ }));
96
+ }
97
+
98
+ // 3) 用 Document 构造,给将来 CST 模式留余地
99
+ const doc = new YAML.Document();
100
+ doc.contents = doc.createNode(ordered);
101
+
102
+ const out = doc.toString({
103
+ indent: 2,
104
+ lineWidth: 0, // 不强制换行,保留长 prompt 字符串
105
+ defaultStringType: 'PLAIN', // 默认 plain,含特殊字符自动升 quoted
106
+ defaultKeyType: 'PLAIN',
107
+ minContentWidth: 0,
108
+ });
109
+
110
+ return out.endsWith('\n') ? out : out + '\n';
111
+ }
112
+
113
+ /* ─────────────────────────────────────────────────────────────────────────
114
+ * validateWorkflow
115
+ * ──────────────────────────────────────────────────────────────────────── */
116
+
117
+ /**
118
+ * 校验 WorkflowDef 结构;返回 errors 数组(空 = OK)。
119
+ *
120
+ * MVP 阶段检查:
121
+ * - step 缺 id
122
+ * - step 没有可执行字段(tool / llm / skill / type)
123
+ * - step id 重复
124
+ * - `${var}` 引用未在前序 step 的 capture values / inputs 里出现
125
+ */
126
+ export function validateWorkflow(def: WorkflowDef): string[] {
127
+ const errors: string[] = [];
128
+
129
+ if (!def || typeof def !== 'object') {
130
+ return ['Workflow 不是对象'];
131
+ }
132
+ if (typeof def.name !== 'string' || !def.name.trim()) {
133
+ errors.push('Workflow 缺少 name');
134
+ }
135
+ if (!Array.isArray(def.steps)) {
136
+ errors.push('Workflow 缺少 steps 数组');
137
+ return errors;
138
+ }
139
+
140
+ // 已知变量集合:先放 inputs.*
141
+ const knownVars = new Set<string>();
142
+ for (const inp of def.inputs ?? []) {
143
+ if (inp?.name) {
144
+ knownVars.add(`inputs.${inp.name}`);
145
+ }
146
+ }
147
+
148
+ const seenIds = new Set<string>();
149
+
150
+ for (const step of def.steps) {
151
+ const sid = step?.id ?? '<no-id>';
152
+
153
+ if (!step?.id || typeof step.id !== 'string') {
154
+ errors.push(`Step "${sid}" 缺少 id`);
155
+ continue;
156
+ }
157
+ if (seenIds.has(step.id)) {
158
+ errors.push(`Step id 重复: "${step.id}"`);
159
+ } else {
160
+ seenIds.add(step.id);
161
+ }
162
+
163
+ // 执行字段检测:tool / llm / skill / type 至少有一个
164
+ const hasExec =
165
+ !!step.tool ||
166
+ !!step.llm ||
167
+ !!step.skill ||
168
+ !!step.type;
169
+ if (!hasExec) {
170
+ errors.push(
171
+ `Step "${step.id}" 没有可执行字段(tool / llm / skill / 或 type)`,
172
+ );
173
+ }
174
+
175
+ // 变量引用检查(只查 ${xxx},inputs.* 和已知 capture)
176
+ const refs = collectVarRefs(step);
177
+ for (const ref of refs) {
178
+ // 把 inputs.book 这种 dotted 也接受为已知;变量名首段或全名匹配都行
179
+ if (knownVars.has(ref)) continue;
180
+ const head = ref.split('.')[0];
181
+ if (knownVars.has(head)) continue;
182
+ errors.push(
183
+ `Step "${step.id}" 引用了未在 capture 里出现的变量: \${${ref}}`,
184
+ );
185
+ }
186
+
187
+ // 把本 step 的 capture values 注入已知集合,供后续 step 引用
188
+ if (step.capture && typeof step.capture === 'object') {
189
+ for (const v of Object.values(step.capture)) {
190
+ if (typeof v === 'string' && v.trim()) {
191
+ knownVars.add(v);
192
+ }
193
+ }
194
+ }
195
+ }
196
+
197
+ return errors;
198
+ }
199
+
200
+ /** 递归遍历 step 所有字符串字段,收集 `${...}` 内的变量名。 */
201
+ function collectVarRefs(step: StepDef): string[] {
202
+ const refs: string[] = [];
203
+ const re = /\$\{([^}]+)\}/g;
204
+
205
+ const visit = (v: unknown): void => {
206
+ if (typeof v === 'string') {
207
+ let m: RegExpExecArray | null;
208
+ re.lastIndex = 0;
209
+ while ((m = re.exec(v)) !== null) {
210
+ refs.push(m[1].trim());
211
+ }
212
+ } else if (Array.isArray(v)) {
213
+ for (const x of v) visit(x);
214
+ } else if (v && typeof v === 'object') {
215
+ for (const x of Object.values(v as Record<string, unknown>)) visit(x);
216
+ }
217
+ };
218
+
219
+ // 不扫 id 自己 / capture 的 values(capture value 是变量定义而非引用)
220
+ for (const [k, v] of Object.entries(step)) {
221
+ if (k === 'id' || k === 'capture') continue;
222
+ visit(v);
223
+ }
224
+ return refs;
225
+ }
226
+
227
+ /* ─────────────────────────────────────────────────────────────────────────
228
+ * detectStepKind
229
+ * ──────────────────────────────────────────────────────────────────────── */
230
+
231
+ /**
232
+ * 探测每个 step 的 kind。语义与 plugins/workflow-runner/src/runner.ts::stepKind()
233
+ * 完全一致:
234
+ * if (step.type) return step.type;
235
+ * if (step.tool) return 'tool';
236
+ * if (step.skill) return 'skill';
237
+ * if (step.llm) return 'llm';
238
+ * return 'assert';
239
+ */
240
+ export function detectStepKind(step: StepDef): StepKind {
241
+ if (step.type) return step.type;
242
+ if (step.tool) return 'tool';
243
+ if (step.skill) return 'skill';
244
+ if (step.llm) return 'llm';
245
+ return 'assert';
246
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "moduleDetection": "force",
13
+ "noEmit": true,
14
+ "jsx": "react-jsx",
15
+ "strict": true,
16
+ "noUnusedLocals": false,
17
+ "noUnusedParameters": false,
18
+ "noFallthroughCasesInSwitch": true,
19
+ "esModuleInterop": true,
20
+ "allowSyntheticDefaultImports": true,
21
+ "types": ["bun-types", "vite/client"]
22
+ },
23
+ "include": ["src", "scripts", "test", "vite.config.ts"]
24
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react-swc';
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ host: '127.0.0.1',
8
+ port: 5173,
9
+ strictPort: true,
10
+ proxy: {
11
+ '/api': {
12
+ target: 'http://127.0.0.1:5174',
13
+ changeOrigin: false,
14
+ },
15
+ },
16
+ },
17
+ build: {
18
+ outDir: 'dist',
19
+ sourcemap: true,
20
+ },
21
+ });