minimal-workflow 0.5.1 → 0.5.3

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/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # minimal-workflow
2
+
3
+ > minimal-agent **workflow YAML 的可视化编辑器** —— 拖拽节点、连线、属性表单、实时校验,
4
+ > 双向同步到 `workflows/*.yaml`。基于 Bun + Vite + React + [@xyflow/react](https://reactflow.dev)。
5
+
6
+ `v0.4.1` · 独立子项目(**不属于 minimal-agent npm 包发布物**),单独 `package.json` / 依赖 / Bun ≥ 1.1。
7
+
8
+ ---
9
+
10
+ ## 这是什么
11
+
12
+ minimal-agent 的 `workflow-runner` 插件用 YAML DAG 描述工作流(9 种 step:tool / llm / skill /
13
+ assert / branch / loop / pause / parallel / vote)。手写 YAML 容易出错,这个编辑器让你**画图**:
14
+
15
+ - 左侧 **Palette** 拖出节点;中间 **画布** 连线表达顺序与 then/else/do 子步骤;右侧 **Inspector** 编辑属性
16
+ - **实时校验**:每个节点显示错误数(hover / Inspector 看详情),规则镜像后端 `loader.ts`
17
+ - **YAML 视图**双向:图 → YAML 预览,编辑 YAML → 校验通过后回写图
18
+ - **撤销/重做**(Ctrl+Z / Ctrl+Shift+Z)、画布布局与连线持久化到 `__meta`(runner 严格忽略,不影响执行)
19
+
20
+ 工具 / skill 的元数据(名称、参数 schema)由编辑器在启动时从目标项目的 `src/tools` + `skills`
21
+ **自动导出**成 `<project>/.editor-cache/manifest.json`,过期自动重建。
22
+
23
+ ## 运行
24
+
25
+ 需要一个 **minimal-agent 项目目录**(含 `workflows/` 子目录)作为编辑对象。
26
+
27
+ ```bash
28
+ cd editor
29
+ bun install
30
+
31
+ # 一键启动(推荐):起 IO 服务 + Vite + 自动开浏览器
32
+ bun start .. # 用上级目录(minimal-agent 本体)作为项目
33
+ bun start /path/to/your-project # 或指定任意含 workflows/ 的项目
34
+ ```
35
+
36
+ CLI 选项(`bun start --help`):
37
+
38
+ | 选项 | 说明 |
39
+ |---|---|
40
+ | `--port <n>` | 编辑器(Vite)端口,默认 `5173` |
41
+ | `--server-port <n>` | 文件 IO 服务端口,默认 `5174` |
42
+ | `--refresh-manifest` | 强制重新生成 manifest |
43
+ | `--no-manifest-refresh` | 跳过 manifest 新鲜度检查 |
44
+ | `--no-open` | 不自动打开浏览器 |
45
+
46
+ > 纯前端调试用 `bun run dev`,但那样不会起 IO 服务(读写 / manifest 接口会 404)——
47
+ > 日常用 `bun start <project>` 即可。
48
+
49
+ ## 架构
50
+
51
+ ```
52
+ editor/
53
+ ├─ scripts/
54
+ │ ├─ cli.ts # 入口:manifest 新鲜度检查 → spawn server + vite → 开浏览器
55
+ │ └─ server.ts # 本地 IO 服务(Bun.serve,127.0.0.1,仅本地):/api/{list,read,write,manifest}
56
+ └─ src/
57
+ ├─ App.tsx # 三栏布局 + 工具栏(新建 / 保存 / YAML 视图 / 撤销重做)
58
+ ├─ types.ts # 与 workflow-runner 同形的模型(D2 红线:手抄、禁 import 后端)
59
+ ├─ canvas/ # ReactFlow 画布 + 9 种节点组件 + 连线
60
+ ├─ inspector/ # 属性面板(按 kind 分发到各表单)+ inputs 面板
61
+ ├─ palette/ # 左侧节点 / 工具 / skill 拖拽源
62
+ ├─ store/ # zustand:workflow(含 undo/redo + 图↔def 拓扑组装)/ manifest / files
63
+ ├─ yaml-view/ # 图 ↔ YAML 纯函数互转(parse / stringify / validate)
64
+ └─ api/ # 调 server 的 4 个端点
65
+ ```
66
+
67
+ ## 命令
68
+
69
+ ```bash
70
+ bun start [project-dir] # = bun scripts/cli.ts,一键启动
71
+ bun run dev # 仅 Vite 前端(不含 IO 服务)
72
+ bun run server # 仅 IO 服务(需 WORKFLOW_UI_PROJECT_DIR 环境变量)
73
+ bun run build # Vite 生产构建
74
+ bun run typecheck # tsc --noEmit
75
+ bun test # 单元测试(sync / store / undo / 表单 等)
76
+ ```
77
+
78
+ ## 与 minimal-agent 的关系(解耦红线)
79
+
80
+ - **D2 解耦**:编辑器**运行时不 import** minimal-agent / workflow-runner 的任何代码(要能在浏览器跑)。
81
+ step 模型与校验规则是**手抄**后端的(`src/types.ts`、`src/yaml-view/sync.ts`)。
82
+ - **防漂移**:手抄会随后端演进而过时,故主项目用 `test/editorSchemaSync.test.ts` 守护
83
+ —— 后端 `loader.ts` 的 `VALID_STEP_TYPES` / `CONTROL_FLOW_TYPES` 一变该测试就红,提示同步本编辑器。
84
+ - **`__meta`**:保存的 YAML 里 `__meta`(节点坐标、连线)仅供编辑器还原布局,runner 严格忽略,
85
+ 不影响执行语义。
86
+ - **本地 only**:IO 服务仅监听 `127.0.0.1`,绝不暴露外网。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minimal-workflow",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Visual editor for minimal-agent workflow YAML files",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,6 +10,8 @@ import {
10
10
  type EdgeChange,
11
11
  type Connection,
12
12
  addEdge,
13
+ ReactFlowProvider,
14
+ useReactFlow,
13
15
  } from '@xyflow/react';
14
16
  import '@xyflow/react/dist/style.css';
15
17
 
@@ -43,7 +45,8 @@ const edgeTypes = {
43
45
  sequential: SequentialEdge,
44
46
  };
45
47
 
46
- export function WorkflowCanvas() {
48
+ function WorkflowCanvasInner() {
49
+ const { screenToFlowPosition } = useReactFlow();
47
50
  const nodes = useWorkflowStore((s) => s.nodes);
48
51
  const edges = useWorkflowStore((s) => s.edges);
49
52
  const setNodes = useWorkflowStore((s) => s.setNodes);
@@ -117,18 +120,15 @@ export function WorkflowCanvas() {
117
120
  } catch {
118
121
  return;
119
122
  }
120
- const bounds = (e.target as HTMLElement)
121
- .closest('.canvas')!
122
- .getBoundingClientRect();
123
- const position = {
124
- x: e.clientX - bounds.left - 90,
125
- y: e.clientY - bounds.top - 30,
126
- };
123
+ // ReactFlow screenToFlowPosition 把屏幕像素换算成画布(flow)坐标,
124
+ // 自动吃掉当前缩放 / 平移 —— 缩放或平移后拖入的节点也能精确落在光标处
125
+ // (旧实现用 getBoundingClientRect + 硬编码偏移,viewport 一变就落偏)。
126
+ const position = screenToFlowPosition({ x: e.clientX, y: e.clientY });
127
127
  const id = nextId(payload.kind, nodes.map((n) => n.id));
128
128
  const step = makeStepTemplate(payload.kind, id, payload.name);
129
129
  addStep(step, position);
130
130
  },
131
- [nodes, addStep],
131
+ [nodes, addStep, screenToFlowPosition],
132
132
  );
133
133
 
134
134
  /**
@@ -166,6 +166,16 @@ export function WorkflowCanvas() {
166
166
  );
167
167
  }
168
168
 
169
+ export function WorkflowCanvas() {
170
+ // ReactFlowProvider 提供 useReactFlow() 的 context(screenToFlowPosition 等)。
171
+ // 必须包在渲染 <ReactFlow> 的组件之外,Inner 才能在 onDrop 里拿到正确的坐标换算。
172
+ return (
173
+ <ReactFlowProvider>
174
+ <WorkflowCanvasInner />
175
+ </ReactFlowProvider>
176
+ );
177
+ }
178
+
169
179
  function nextId(kind: StepKind, existing: string[]): string {
170
180
  let i = 1;
171
181
  while (existing.includes(`${kind}_${i}`)) i++;
@@ -4,8 +4,7 @@ import type { CanvasNodeData } from '../../types.ts';
4
4
 
5
5
  interface BaseNodeProps extends NodeProps {
6
6
  data: CanvasNodeData;
7
- selected?: boolean;
8
- /** 节点种类 css class(决定边框色) */
7
+ /** 节点种类 css class(决定边框色);selected 继承自 NodeProps(ReactFlow 注入) */
9
8
  variant: string;
10
9
  /** 节点标题(如 "🔧 tool: bash") */
11
10
  title: string;
@@ -34,7 +33,9 @@ export function BaseNode({
34
33
  {body && <div className="node-body">{body}</div>}
35
34
  {errors && errors.length > 0 && (
36
35
  <div style={{ marginTop: 4 }}>
37
- <span className="badge err">{errors.length} 错误</span>
36
+ <span className="badge err" title={errors.join('\n')}>
37
+ {errors.length} 错误
38
+ </span>
38
39
  </div>
39
40
  )}
40
41
  {!outputs || outputs.length === 0 ? (
@@ -27,6 +27,27 @@ export function Inspector() {
27
27
  {kind} · {step.id}
28
28
  </h2>
29
29
 
30
+ {node.data.errors && node.data.errors.length > 0 && (
31
+ <div
32
+ style={{
33
+ background: '#fef2f2',
34
+ border: '1px solid #fca5a5',
35
+ borderRadius: 4,
36
+ padding: '6px 8px',
37
+ marginBottom: 8,
38
+ fontSize: 12,
39
+ color: '#b91c1c',
40
+ }}
41
+ >
42
+ <strong>{node.data.errors.length} 个问题</strong>
43
+ <ul style={{ margin: '4px 0 0', paddingLeft: 16 }}>
44
+ {node.data.errors.map((err, i) => (
45
+ <li key={i}>{err}</li>
46
+ ))}
47
+ </ul>
48
+ </div>
49
+ )}
50
+
30
51
  <CommonStepFields step={step} onPatch={onPatch} />
31
52
 
32
53
  {kind === 'tool' && <ToolNodeForm step={step} onPatch={onPatch} />}
@@ -3,6 +3,7 @@ import type { Edge, Node } from '@xyflow/react';
3
3
  import type {
4
4
  CanvasNodeData,
5
5
  StepDef,
6
+ StepKind,
6
7
  WorkflowDef,
7
8
  WorkflowEdgeMeta,
8
9
  } from '../types.ts';
@@ -368,7 +369,7 @@ function sameErrors(
368
369
  return true;
369
370
  }
370
371
 
371
- function detectKind(step: StepDef): string {
372
+ function detectKind(step: StepDef): StepKind {
372
373
  if (step.type) return step.type;
373
374
  if (step.tool) return 'tool';
374
375
  if (step.skill) return 'skill';
@@ -464,7 +465,7 @@ function assembleWorkflow(
464
465
  visiting.add(cur);
465
466
  claimed.add(cur);
466
467
  chain.push(cur);
467
- const nexts = siblingAdj.get(cur) ?? [];
468
+ const nexts: string[] = siblingAdj.get(cur) ?? [];
468
469
  // 链只走第一个 sibling 后继(DAG 上的"线性"分支)
469
470
  cur = nexts[0];
470
471
  }
package/src/types.ts CHANGED
@@ -104,9 +104,13 @@ export interface Manifest {
104
104
  skills: SkillMeta[];
105
105
  }
106
106
 
107
- /** 图模型节点 data 字段 */
108
- export interface CanvasNodeData {
107
+ /**
108
+ * 图模型节点 data 字段。
109
+ * 交叉 `Record<string, unknown>` 以满足 @xyflow/react `Node<T>` 对 data 的约束
110
+ * (T extends Record<string, unknown>);已知字段仍各自保留类型,访问 step/kind/errors 不受影响。
111
+ */
112
+ export type CanvasNodeData = {
109
113
  step: StepDef;
110
114
  kind: StepKind;
111
115
  errors?: string[];
112
- }
116
+ } & Record<string, unknown>;
@@ -127,7 +127,9 @@ export function stringifyWorkflowYaml(def: WorkflowDef): string {
127
127
 
128
128
  // ADR-05:控制流 type 集合——这些节点的"动作"由 type 自身定义,
129
129
  // 不能附带 tool/skill/llm,也不能配 output_schema / context_files / allowed_tools。
130
- const CONTROL_FLOW_TYPES = new Set<string>([
130
+ // 导出供主项目的 schema 一致性测试比对(test/editorSchemaSync.test.ts)——
131
+ // 这两份是手抄后端 loader.ts 的(D2 红线禁止运行时 import 后端),导出后由测试守护不漂移。
132
+ export const CONTROL_FLOW_TYPES = new Set<string>([
131
133
  'assert',
132
134
  'branch',
133
135
  'loop',
@@ -137,7 +139,7 @@ const CONTROL_FLOW_TYPES = new Set<string>([
137
139
  ]);
138
140
 
139
141
  // 合法的 step.type 枚举(与后端 VALID_STEP_TYPES 同集)。
140
- const VALID_STEP_TYPES = new Set<string>([
142
+ export const VALID_STEP_TYPES = new Set<string>([
141
143
  'assert',
142
144
  'branch',
143
145
  'loop',
@@ -215,7 +217,7 @@ function topLevelErrors(def: WorkflowDef): string[] {
215
217
  errors.push(`inputs[${i}] 必须是对象`);
216
218
  return;
217
219
  }
218
- const r = item as Record<string, unknown>;
220
+ const r = item as unknown as Record<string, unknown>;
219
221
  if (typeof r.name !== 'string' || r.name.length === 0) {
220
222
  errors.push(`inputs[${i}].name 必填(非空 string)`);
221
223
  }