@yanglaofish/fishtools 0.4.0 → 0.5.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.
Files changed (262) hide show
  1. package/README.md +33 -5
  2. package/bin/fst.js +1 -1
  3. package/manual/README.md +33 -5
  4. package/manual/docs/deployment.md +3 -3
  5. package/manual/docs/kernel-generation.md +97 -0
  6. package/manual/docs/modules.md +57 -0
  7. package/manual/docs/plugin-development.md +120 -10
  8. package/manual/docs/releases.md +24 -0
  9. package/manual/docs/research/2026-10-11-plugin-kernel-capabilities.md +6 -6
  10. package/manual/docs/roadmap.md +14 -3
  11. package/manual/docs/superpowers/plans/2026-10-09-workflow-mvp.md +5 -5
  12. package/manual/docs/superpowers/plans/2026-10-11-public-plugin-fixes.md +2 -2
  13. package/manual/docs/superpowers/plans/2026-10-12-kernel-generation.md +22 -0
  14. package/manual/docs/superpowers/plans/2026-10-12-modular-kernel.md +40 -0
  15. package/manual/docs/superpowers/reviews/2026-10-12-kernel-generation-review.md +27 -0
  16. package/manual/docs/superpowers/reviews/2026-10-12-modular-kernel-review.md +32 -0
  17. package/manual/docs/superpowers/reviews/2026-10-13-0.5.0-followup-review.md +67 -0
  18. package/manual/docs/superpowers/reviews/2026-10-13-kernel-runtime-release-review.md +19 -0
  19. package/manual/docs/superpowers/specs/2026-10-12-action-network-policy-design.md +52 -0
  20. package/manual/docs/superpowers/specs/2026-10-12-kernel-generation-design.md +38 -0
  21. package/manual/docs/superpowers/specs/2026-10-12-modular-kernel-design.md +70 -0
  22. package/manual/docs/superpowers/specs/2026-10-12-plugin-readiness-design.md +21 -0
  23. package/manual/docs/superpowers/specs/2026-10-12-runtime-maintenance-granularity-design.md +35 -0
  24. package/manual/docs/superpowers/specs/2026-10-12-workflow-variation-design.md +19 -0
  25. package/manual/docs/verification.md +55 -2
  26. package/manual/packages/core/README.md +44 -0
  27. package/manual/packages/core/package.json +38 -0
  28. package/manual/packages/core/src/application.js +251 -85
  29. package/manual/packages/core/src/bundle.js +2 -0
  30. package/manual/packages/core/src/development-watch-worker.js +69 -0
  31. package/manual/packages/core/src/development.js +146 -0
  32. package/manual/packages/core/src/environments.js +2 -0
  33. package/manual/packages/core/src/export-snapshot.js +42 -0
  34. package/manual/packages/core/src/plugin-update.js +108 -0
  35. package/manual/packages/core/src/process.js +2 -0
  36. package/manual/packages/core/src/resources.js +31 -0
  37. package/manual/packages/core/src/version.js +2 -0
  38. package/manual/packages/fst-cli/LICENSE +21 -0
  39. package/manual/packages/fst-cli/README.md +59 -0
  40. package/{node_modules/@mytools/core → manual/packages/fst-cli}/bin/fst.js +55 -23
  41. package/manual/packages/fst-cli/package.json +50 -0
  42. package/manual/packages/fst-cli/src/development.js +53 -0
  43. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/model.js +1 -1
  44. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/navigation.js +1 -1
  45. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/presentation.js +9 -1
  46. package/manual/packages/fst-cli/src/terminal/run-feed.js +119 -0
  47. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-viewer.js +9 -7
  48. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/schema.js +1 -1
  49. package/manual/packages/{core → fst-cli}/src/terminal/session.js +137 -9
  50. package/manual/packages/{core → fst-cli}/src/terminal/view.js +20 -3
  51. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/workflow-menu.js +17 -2
  52. package/manual/packages/fst-devtools/LICENSE +21 -0
  53. package/manual/packages/fst-devtools/README.md +28 -0
  54. package/manual/packages/fst-devtools/package.json +29 -0
  55. package/{node_modules/@mytools/core → manual/packages/fst-devtools}/src/documentation.js +3 -1
  56. package/manual/packages/fst-devtools/src/plugin-quality.js +136 -0
  57. package/{node_modules/@mytools/core → manual/packages/fst-devtools}/src/plugin-tools.js +24 -5
  58. package/manual/packages/fst-logs/LICENSE +21 -0
  59. package/manual/packages/fst-logs/README.md +67 -0
  60. package/manual/packages/fst-logs/package.json +26 -0
  61. package/manual/packages/fst-logs/src/log-bounds.js +91 -0
  62. package/manual/packages/fst-logs/src/run-logs.js +224 -0
  63. package/manual/packages/fst-logs/src/run-preview.js +18 -0
  64. package/manual/packages/fst-logs/src/step-summaries.js +28 -0
  65. package/manual/packages/fst-platform/LICENSE +21 -0
  66. package/manual/packages/fst-platform/README.md +73 -0
  67. package/manual/packages/fst-platform/package.json +33 -0
  68. package/manual/packages/fst-platform/src/bundle.js +17 -0
  69. package/manual/packages/fst-platform/src/environments.js +67 -0
  70. package/manual/packages/fst-platform/src/file-probe-engine.js +183 -0
  71. package/manual/packages/fst-platform/src/file-probe-worker.js +43 -0
  72. package/manual/packages/fst-platform/src/file-probe.js +6 -0
  73. package/manual/packages/fst-platform/src/json.js +5 -0
  74. package/manual/packages/fst-platform/src/network.js +199 -0
  75. package/manual/packages/fst-platform/src/npm.js +11 -0
  76. package/manual/packages/fst-platform/src/process.js +110 -0
  77. package/manual/packages/fst-plugin/LICENSE +21 -0
  78. package/manual/packages/fst-plugin/README.md +29 -0
  79. package/manual/packages/fst-plugin/package.json +29 -0
  80. package/{node_modules/@mytools/core → manual/packages/fst-plugin}/src/contracts.js +30 -3
  81. package/manual/packages/fst-plugin/src/host.js +235 -0
  82. package/manual/packages/fst-plugin/src/loader.js +21 -0
  83. package/manual/packages/fst-plugin/src/privileges.js +91 -0
  84. package/{node_modules/@mytools/core → manual/packages/fst-plugin}/src/runtime.js +27 -8
  85. package/manual/packages/fst-workflow/LICENSE +21 -0
  86. package/manual/packages/fst-workflow/README.md +28 -0
  87. package/manual/packages/fst-workflow/package.json +24 -0
  88. package/{node_modules/@mytools/core → manual/packages/fst-workflow}/src/control.js +3 -2
  89. package/manual/packages/{core → fst-workflow}/src/engine.js +2 -2
  90. package/manual/packages/fst-workspace/LICENSE +21 -0
  91. package/manual/packages/fst-workspace/README.md +32 -0
  92. package/manual/packages/fst-workspace/package.json +34 -0
  93. package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/bundle-workspace.js +1 -1
  94. package/manual/packages/fst-workspace/src/index.js +3 -0
  95. package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/plugins.js +6 -36
  96. package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/workflows.js +40 -7
  97. package/manual/packages/pack/LICENSE +21 -0
  98. package/manual/packages/pack/README.md +31 -0
  99. package/manual/packages/pack/archive.js +15 -0
  100. package/manual/packages/pack/dependencies.js +105 -0
  101. package/manual/packages/pack/environments.js +50 -0
  102. package/manual/packages/pack/index.js +103 -0
  103. package/manual/packages/pack/package.json +29 -0
  104. package/manual/packages/pack/runtime.js +61 -0
  105. package/manual/packages/sdk/README.md +31 -0
  106. package/manual/packages/sdk/conditions.js +1 -0
  107. package/manual/packages/sdk/package.json +23 -1
  108. package/manual/plugins/web/README.md +29 -0
  109. package/manual/tests/application.test.js +6 -4
  110. package/node_modules/@mytools/core/README.md +44 -0
  111. package/node_modules/@mytools/core/package.json +17 -17
  112. package/node_modules/@mytools/core/src/application.js +251 -85
  113. package/node_modules/@mytools/core/src/bundle.js +2 -17
  114. package/node_modules/@mytools/core/src/development-watch-worker.js +69 -0
  115. package/node_modules/@mytools/core/src/development.js +146 -0
  116. package/node_modules/@mytools/core/src/environments.js +2 -67
  117. package/node_modules/@mytools/core/src/export-snapshot.js +42 -0
  118. package/node_modules/@mytools/core/src/plugin-update.js +108 -0
  119. package/node_modules/@mytools/core/src/process.js +2 -80
  120. package/node_modules/@mytools/core/src/version.js +1 -1
  121. package/node_modules/@mytools/fst-cli/LICENSE +21 -0
  122. package/node_modules/@mytools/fst-cli/README.md +59 -0
  123. package/node_modules/@mytools/fst-cli/bin/fst.js +124 -0
  124. package/node_modules/@mytools/fst-cli/package.json +50 -0
  125. package/node_modules/@mytools/fst-cli/src/cli-run.js +12 -0
  126. package/node_modules/@mytools/fst-cli/src/development.js +53 -0
  127. package/node_modules/@mytools/fst-cli/src/terminal/conditions.js +32 -0
  128. package/node_modules/@mytools/fst-cli/src/terminal/log-viewer.js +45 -0
  129. package/node_modules/@mytools/fst-cli/src/terminal/model.js +104 -0
  130. package/node_modules/@mytools/fst-cli/src/terminal/navigation.js +17 -0
  131. package/node_modules/@mytools/fst-cli/src/terminal/presentation.js +50 -0
  132. package/node_modules/@mytools/fst-cli/src/terminal/run-feed.js +119 -0
  133. package/node_modules/@mytools/fst-cli/src/terminal/run-presentation.js +60 -0
  134. package/node_modules/@mytools/fst-cli/src/terminal/run-viewer.js +106 -0
  135. package/node_modules/@mytools/fst-cli/src/terminal/schema.js +52 -0
  136. package/node_modules/@mytools/{core → fst-cli}/src/terminal/session.js +137 -9
  137. package/node_modules/@mytools/fst-cli/src/terminal/theme.js +29 -0
  138. package/node_modules/@mytools/{core → fst-cli}/src/terminal/view.js +20 -3
  139. package/node_modules/@mytools/fst-cli/src/terminal/workflow-menu.js +65 -0
  140. package/node_modules/@mytools/fst-devtools/LICENSE +21 -0
  141. package/node_modules/@mytools/fst-devtools/README.md +28 -0
  142. package/node_modules/@mytools/fst-devtools/package.json +29 -0
  143. package/node_modules/@mytools/fst-devtools/src/documentation.js +67 -0
  144. package/node_modules/@mytools/fst-devtools/src/plugin-quality.js +136 -0
  145. package/node_modules/@mytools/fst-devtools/src/plugin-tools.js +152 -0
  146. package/node_modules/@mytools/fst-logs/LICENSE +21 -0
  147. package/node_modules/@mytools/fst-logs/README.md +67 -0
  148. package/node_modules/@mytools/fst-logs/package.json +26 -0
  149. package/node_modules/@mytools/fst-logs/src/log-bounds.js +91 -0
  150. package/node_modules/@mytools/fst-logs/src/markdown-log.js +55 -0
  151. package/node_modules/@mytools/fst-logs/src/run-logs.js +224 -0
  152. package/node_modules/@mytools/fst-logs/src/run-preview.js +18 -0
  153. package/node_modules/@mytools/fst-logs/src/step-summaries.js +28 -0
  154. package/node_modules/@mytools/fst-platform/LICENSE +21 -0
  155. package/node_modules/@mytools/fst-platform/README.md +73 -0
  156. package/node_modules/@mytools/fst-platform/package.json +33 -0
  157. package/node_modules/@mytools/fst-platform/src/bundle.js +17 -0
  158. package/node_modules/@mytools/fst-platform/src/environments.js +67 -0
  159. package/node_modules/@mytools/fst-platform/src/file-probe-engine.js +183 -0
  160. package/node_modules/@mytools/fst-platform/src/file-probe-worker.js +43 -0
  161. package/node_modules/@mytools/fst-platform/src/file-probe.js +6 -0
  162. package/node_modules/@mytools/fst-platform/src/json.js +5 -0
  163. package/node_modules/@mytools/fst-platform/src/network.js +199 -0
  164. package/node_modules/@mytools/fst-platform/src/npm.js +11 -0
  165. package/node_modules/@mytools/fst-platform/src/process.js +110 -0
  166. package/node_modules/@mytools/fst-plugin/LICENSE +21 -0
  167. package/node_modules/@mytools/fst-plugin/README.md +29 -0
  168. package/node_modules/@mytools/fst-plugin/package.json +29 -0
  169. package/node_modules/@mytools/fst-plugin/src/contracts.js +86 -0
  170. package/node_modules/@mytools/fst-plugin/src/host.js +235 -0
  171. package/node_modules/@mytools/fst-plugin/src/loader.js +21 -0
  172. package/node_modules/@mytools/fst-plugin/src/privileges.js +91 -0
  173. package/{manual/packages/core → node_modules/@mytools/fst-plugin}/src/runtime.js +27 -8
  174. package/node_modules/@mytools/fst-workflow/LICENSE +21 -0
  175. package/node_modules/@mytools/fst-workflow/README.md +28 -0
  176. package/node_modules/@mytools/fst-workflow/package.json +24 -0
  177. package/node_modules/@mytools/fst-workflow/src/control.js +64 -0
  178. package/node_modules/@mytools/{core → fst-workflow}/src/engine.js +2 -2
  179. package/node_modules/@mytools/fst-workspace/LICENSE +21 -0
  180. package/node_modules/@mytools/fst-workspace/README.md +32 -0
  181. package/node_modules/@mytools/fst-workspace/package.json +34 -0
  182. package/node_modules/@mytools/fst-workspace/src/bundle-workspace.js +39 -0
  183. package/node_modules/@mytools/fst-workspace/src/index.js +3 -0
  184. package/node_modules/@mytools/fst-workspace/src/plugin-installations.js +84 -0
  185. package/node_modules/@mytools/fst-workspace/src/plugins.js +35 -0
  186. package/node_modules/@mytools/fst-workspace/src/project.js +29 -0
  187. package/node_modules/@mytools/fst-workspace/src/workflows.js +150 -0
  188. package/node_modules/@mytools/fst-workspace/templates/control-demo.yaml +47 -0
  189. package/node_modules/@mytools/fst-workspace/templates/hello.yaml +35 -0
  190. package/node_modules/@mytools/fst-workspace/templates/python-check.yaml +26 -0
  191. package/node_modules/@mytools/fst-workspace/templates/system-check.yaml +11 -0
  192. package/node_modules/@mytools/pack/README.md +31 -0
  193. package/node_modules/@mytools/pack/dependencies.js +7 -2
  194. package/node_modules/@mytools/pack/environments.js +1 -1
  195. package/node_modules/@mytools/pack/index.js +33 -25
  196. package/node_modules/@mytools/pack/package.json +29 -1
  197. package/node_modules/@mytools/sdk/README.md +31 -0
  198. package/node_modules/@mytools/sdk/conditions.js +1 -0
  199. package/node_modules/@mytools/sdk/diagnostics.js +27 -0
  200. package/node_modules/@mytools/sdk/package.json +23 -1
  201. package/node_modules/@mytools/sdk/schema-path.js +31 -0
  202. package/node_modules/@mytools/sdk/validation.js +78 -0
  203. package/node_modules/@mytools/web/README.md +29 -0
  204. package/node_modules/@mytools/web/package.json +7 -0
  205. package/node_modules/@mytools/web/public/THIRD_PARTY_NOTICES.txt +1440 -1240
  206. package/node_modules/@mytools/web/public/assets/index-BaVzHycP.js +70 -0
  207. package/node_modules/@mytools/web/public/assets/index-DA8Ae92t.css +1 -0
  208. package/node_modules/@mytools/web/public/index.html +2 -2
  209. package/node_modules/@mytools/web/server.js +24 -17
  210. package/node_modules/@mytools/web/src/model.js +84 -0
  211. package/node_modules/acorn/CHANGELOG.md +1012 -0
  212. package/node_modules/acorn/LICENSE +21 -0
  213. package/node_modules/acorn/README.md +309 -0
  214. package/node_modules/acorn/bin/acorn +4 -0
  215. package/node_modules/acorn/dist/acorn.d.mts +897 -0
  216. package/node_modules/acorn/dist/acorn.d.ts +897 -0
  217. package/node_modules/acorn/dist/acorn.js +6370 -0
  218. package/node_modules/acorn/dist/acorn.mjs +6341 -0
  219. package/node_modules/acorn/dist/bin.js +90 -0
  220. package/node_modules/acorn/package.json +50 -0
  221. package/node_modules/{minipass-flush/node_modules/minipass → tar}/node_modules/yallist/README.md +8 -7
  222. package/node_modules/tar/node_modules/yallist/package.json +68 -0
  223. package/node_modules/yallist/README.md +7 -8
  224. package/node_modules/yallist/package.json +12 -51
  225. package/package.json +131 -115
  226. package/manual/packages/core/src/host.js +0 -139
  227. package/manual/packages/core/src/terminal/run-feed.js +0 -78
  228. package/node_modules/@mytools/core/src/host.js +0 -139
  229. package/node_modules/@mytools/core/src/run-logs.js +0 -63
  230. package/node_modules/@mytools/core/src/terminal/run-feed.js +0 -78
  231. package/node_modules/@mytools/web/public/assets/index-DT2B4tMV.js +0 -70
  232. package/node_modules/@mytools/web/public/assets/index-vuZvrUTT.css +0 -1
  233. package/node_modules/minipass-flush/node_modules/minipass/node_modules/yallist/package.json +0 -29
  234. /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/cli-run.js +0 -0
  235. /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/conditions.js +0 -0
  236. /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/log-viewer.js +0 -0
  237. /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-presentation.js +0 -0
  238. /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/theme.js +0 -0
  239. /package/{node_modules/@mytools/core → manual/packages/fst-logs}/src/markdown-log.js +0 -0
  240. /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/plugin-installations.js +0 -0
  241. /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/project.js +0 -0
  242. /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/control-demo.yaml +0 -0
  243. /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/hello.yaml +0 -0
  244. /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/python-check.yaml +0 -0
  245. /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/system-check.yaml +0 -0
  246. /package/{node_modules/@mytools/core/src → manual/packages/sdk}/diagnostics.js +0 -0
  247. /package/{node_modules/@mytools/core/src → manual/packages/sdk}/schema-path.js +0 -0
  248. /package/{node_modules/@mytools/core/src → manual/packages/sdk}/validation.js +0 -0
  249. /package/node_modules/{yallist → tar/node_modules/yallist}/LICENSE.md +0 -0
  250. /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/index.d.ts +0 -0
  251. /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/index.d.ts.map +0 -0
  252. /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/index.js +0 -0
  253. /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/index.js.map +0 -0
  254. /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/package.json +0 -0
  255. /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/index.d.ts +0 -0
  256. /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/index.d.ts.map +0 -0
  257. /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/index.js +0 -0
  258. /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/index.js.map +0 -0
  259. /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/package.json +0 -0
  260. /package/node_modules/{minipass-flush/node_modules/minipass/node_modules/yallist → yallist}/LICENSE +0 -0
  261. /package/node_modules/{minipass-flush/node_modules/minipass/node_modules/yallist → yallist}/iterator.js +0 -0
  262. /package/node_modules/{minipass-flush/node_modules/minipass/node_modules/yallist → yallist}/yallist.js +0 -0
@@ -0,0 +1,64 @@
1
+ import {isDeepStrictEqual} from 'node:util';
2
+ import {FlowError} from '@mytools/sdk';
3
+ import {bind,references} from '@mytools/sdk/schema';
4
+
5
+ import {conditionOperators} from '@mytools/sdk/conditions';
6
+ export {conditionOperators};
7
+ const invalid=message=>{throw new FlowError(message,{code:'VALIDATION'});};
8
+ export function validateCondition(condition,visit=()=>{},depth=0){
9
+ if(depth>32||!condition||typeof condition!=='object'||Array.isArray(condition))invalid('条件必须是有限深度的结构化对象');
10
+ const groups=['all','any','not'].filter(key=>Object.hasOwn(condition,key));
11
+ if(groups.length){
12
+ if(groups.length!==1||Object.keys(condition).length!==1)invalid('组合条件只能包含 all、any 或 not 中的一种');
13
+ const key=groups[0],children=key==='not'?[condition.not]:condition[key];
14
+ if(!Array.isArray(children)||!children.length)invalid('组合条件不能为空');
15
+ children.forEach(child=>validateCondition(child,visit,depth+1));return;
16
+ }
17
+ if(!conditionOperators.includes(condition.op)||!Object.hasOwn(condition,'left')||(condition.op!=='exists'&&!Object.hasOwn(condition,'right'))||Object.keys(condition).some(k=>!['left','op','right'].includes(k)))invalid('条件比较格式错误');
18
+ references(condition.left,visit);if(condition.op!=='exists')references(condition.right,visit);
19
+ }
20
+
21
+ export function evaluateCondition(condition,state){
22
+ if(condition.all)return condition.all.every(c=>evaluateCondition(c,state));
23
+ if(condition.any)return condition.any.some(c=>evaluateCondition(c,state));
24
+ if(condition.not)return !evaluateCondition(condition.not,state);
25
+ let left,right;
26
+ try{left=bind(condition.left,state);if(condition.op!=='exists')right=bind(condition.right,state);}
27
+ catch(error){if(error.code==='MISSING_REFERENCE')return false;throw error;}
28
+ switch(condition.op){
29
+ case 'exists':return left!==undefined;
30
+ case 'eq':return isDeepStrictEqual(left,right);
31
+ case 'ne':return !isDeepStrictEqual(left,right);
32
+ case 'contains':return typeof left==='string'&&typeof right==='string'?left.includes(right):Array.isArray(left)&&left.some(v=>isDeepStrictEqual(v,right));
33
+ default:
34
+ if(typeof left!=='number'||typeof right!=='number'||!Number.isFinite(left)||!Number.isFinite(right))return false;
35
+ return {gt:()=>left>right,gte:()=>left>=right,lt:()=>left<right,lte:()=>left<=right}[condition.op]();
36
+ }
37
+ }
38
+
39
+ export function childLists(step){
40
+ if(step.branch)return [step.branch.then??[],step.branch.else??[]];
41
+ if(step.parallel)return [step.parallel.steps];
42
+ return [];
43
+ }
44
+
45
+ export function validateStepList(steps,assertId,ids=new Set(),depth=0){
46
+ if(depth>32||!Array.isArray(steps))invalid('步骤列表必须是数组,嵌套深度不得超过 32');
47
+ for(const step of steps){
48
+ assertId(step?.id);if(ids.has(step.id))invalid(`步骤 ID 重复:${step.id}`);ids.add(step.id);
49
+ if(['constructor','prototype'].includes(step.id))invalid(`步骤 ID 不能使用保留名称:${step.id}`);
50
+ const kinds=['action','workflow','branch','parallel','stop'].filter(k=>Object.hasOwn(step,k));
51
+ if(kinds.length!==1)invalid(`${step.id} 必须选择一个动作、子工作流或控制节点`);
52
+ if(step.when!==undefined)validateCondition(step.when);
53
+ if(kinds[0]==='action'||kinds[0]==='workflow'){
54
+ if(typeof step[kinds[0]]!=='string'||!step[kinds[0]])invalid(`${step.id} 的目标必须为字符串`);
55
+ }else{
56
+ const value=step[kinds[0]];
57
+ if(!value||typeof value!=='object'||Array.isArray(value))invalid(`${step.id} 控制节点格式错误`);
58
+ if(step.branch){validateCondition(value.condition);if(!Array.isArray(value.then)||value.else!==undefined&&!Array.isArray(value.else))invalid('分支需要 then/else 步骤数组');}
59
+ if(step.parallel){const n=value.concurrency??4;if(!Number.isInteger(n)||n<1||n>32)invalid('并发数必须是 1 到 32 的整数');}
60
+ if(step.stop&&(typeof value.reason!=='string'||!value.reason.trim()))invalid('提前结束必须填写原因');
61
+ }
62
+ for(const list of childLists(step))validateStepList(list,assertId,ids,depth+1);
63
+ }
64
+ }
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { FlowError } from '@mytools/sdk';
3
- import { bind, validate, emptyObject } from './validation.js';
4
- import {errorRecord} from './diagnostics.js';
3
+ import { bind, validate, emptyObject } from '@mytools/sdk/schema';
4
+ import {errorRecord} from '@mytools/sdk/diagnostics';
5
5
  import {evaluateCondition,childLists} from './control.js';
6
6
 
7
7
  export async function runWorkflow({store,host,id,input={},signal,onEvent=()=>{},runId=randomUUID(),context={}}) {
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mytools contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,32 @@
1
+ # 工作区模块
2
+
3
+ 包名:`@mytools/fst-workspace`。内部 workspace 包,随 `@yanglaofish/fishtools` 统一交付。
4
+
5
+ ## 职责与边界
6
+
7
+ 项目初始化、模板、工作流文件、插件安装批次、租约、开发引用及便携工作区。插件执行由 plugin 负责,系统调用由 platform 负责。
8
+
9
+ 内部依赖:`@mytools/sdk`、`@mytools/fst-platform`、`@mytools/fst-plugin`、`@mytools/fst-workflow`。
10
+ ## 公开入口
11
+
12
+ | 导入路径 | 实现位置 |
13
+ | --- | --- |
14
+ | `@mytools/fst-workspace` | `./src/index.js` |
15
+ | `@mytools/fst-workspace/workflows` | `./src/workflows.js` |
16
+ | `@mytools/fst-workspace/project` | `./src/project.js` |
17
+ | `@mytools/fst-workspace/plugins` | `./src/plugins.js` |
18
+ | `@mytools/fst-workspace/installations` | `./src/plugin-installations.js` |
19
+ | `@mytools/fst-workspace/bundles` | `./src/bundle-workspace.js` |
20
+ | `@mytools/fst-workspace/package.json` | `./package.json` |
21
+
22
+ 仅依赖 exports 声明的入口,禁止消费者跨包读取 src/bin 私有文件。入口路径由本模块维护,修改协议时同步消费方回归。
23
+
24
+ ## 维护与验证
25
+
26
+ 本模块负责上述职责的实现、文档与回归;跨模块接口调整需要同时检查生产者与消费者。具体维护人员尚未指定。
27
+
28
+ 在仓库根目录执行:
29
+
30
+ ```sh
31
+ node --test tests/modules.test.js tests/bundle-workspace.test.js tests/plugin-development-cycle.test.js tests/project-config-concurrency.test.js
32
+ ```
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@mytools/fst-workspace",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./workflows": "./src/workflows.js",
10
+ "./project": "./src/project.js",
11
+ "./plugins": "./src/plugins.js",
12
+ "./installations": "./src/plugin-installations.js",
13
+ "./bundles": "./src/bundle-workspace.js",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "src",
18
+ "templates",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "license": "MIT",
23
+ "engines": {
24
+ "node": ">=24.15.0"
25
+ },
26
+ "dependencies": {
27
+ "@mytools/sdk": "0.1.0",
28
+ "@mytools/fst-platform": "0.1.0",
29
+ "@mytools/fst-plugin": "0.1.0",
30
+ "@mytools/fst-workflow": "0.1.0",
31
+ "tar": "7.5.22",
32
+ "yaml": "2.9.1"
33
+ }
34
+ }
@@ -0,0 +1,39 @@
1
+ import path from 'node:path';
2
+ import os from 'node:os';
3
+ import {mkdir,readFile,readdir,writeFile,copyFile,rename,rmdir} from 'node:fs/promises';
4
+ import {constants} from 'node:fs';
5
+ import {randomUUID} from 'node:crypto';
6
+ import {setTimeout} from 'node:timers/promises';
7
+ import {FlowError} from '@mytools/sdk';
8
+ import {readJson} from '@mytools/fst-platform/json';
9
+
10
+ export async function prepareBundleWorkspace(bundleRoot,requested){
11
+ bundleRoot=path.resolve(bundleRoot);
12
+ const bundle=await readJson(path.join(bundleRoot,'bundle.json'),null);
13
+ if(!bundle?.editable)return bundleRoot;
14
+ if(!/^[\w-]+$/.test(bundle.id))throw new FlowError('运行包 ID 无效');
15
+ const workspace=path.resolve(requested??process.env.FISHTOOLS_WORKSPACE??path.join(process.env.FISHTOOLS_HOME??path.join(os.homedir(),'.fishtools'),'workspaces',bundle.id));
16
+ if(workspace===bundleRoot||workspace.startsWith(bundleRoot+path.sep))throw new FlowError('可写工作区必须位于分发包目录之外');
17
+ await mkdir(workspace,{recursive:true});
18
+ const lock=path.join(workspace,'.fst-init-lock');let locked=false;
19
+ for(let attempt=0;attempt<100;attempt++){
20
+ try{await mkdir(lock);locked=true;break;}catch(e){if(e.code!=='EEXIST')throw e;await setTimeout(100);}
21
+ }
22
+ if(!locked)throw new FlowError('工作区正在初始化,请稍后重试;若进程意外终止,请检查 .fst-init-lock');
23
+ try{
24
+ const file=path.join(workspace,'flow.project.json');
25
+ let project=await readJson(file,null);
26
+ if(project&&project.baseBundle?.id!==bundle.id)throw new FlowError('指定工作区已属于其他项目或分发包');
27
+ if(!project){
28
+ project=JSON.parse(await readFile(path.join(bundleRoot,'flow.project.json'),'utf8'));
29
+ await mkdir(path.join(workspace,'workflows'),{recursive:true});
30
+ for(const name of await readdir(path.join(bundleRoot,'workflows'))){
31
+ if(!name.endsWith('.yaml'))continue;
32
+ try{await copyFile(path.join(bundleRoot,'workflows',name),path.join(workspace,'workflows',name),constants.COPYFILE_EXCL);}catch(e){if(e.code!=='EEXIST')throw e;}
33
+ }
34
+ }
35
+ project.readOnly=false;project.baseBundle={id:bundle.id,root:bundleRoot};
36
+ const temp=`${file}.${randomUUID()}.tmp`;await writeFile(temp,JSON.stringify(project,null,2));await rename(temp,file);
37
+ return workspace;
38
+ }finally{await rmdir(lock);}
39
+ }
@@ -0,0 +1,3 @@
1
+ export {WorkflowStore} from './workflows.js';
2
+ export {initializeProject,resolveProject} from './project.js';
3
+ export {prepareBundleWorkspace} from './bundle-workspace.js';
@@ -0,0 +1,84 @@
1
+ import path from 'node:path';
2
+ import {mkdir,readFile,writeFile,rename,readdir,realpath,lstat,rm,unlink,rmdir} from 'node:fs/promises';
3
+ import {randomUUID} from 'node:crypto';
4
+ import {isDeepStrictEqual} from 'node:util';
5
+ import {create as archive} from 'tar';
6
+ import {FlowError} from '@mytools/sdk';
7
+
8
+ async function json(file,fallback){try{return JSON.parse(await readFile(file,'utf8'));}catch(error){if(error.code==='ENOENT')return fallback;throw error;}}
9
+ const flow=project=>path.join(project,'.flow');
10
+ const stateFile=project=>path.join(flow(project),'plugins.json');
11
+ export const readPluginState=project=>json(stateFile(project),{});
12
+
13
+ // 所有安装/引用/回收操作共用工作区锁,防止其他 fst 进程同时切换批次。
14
+ export async function withPluginMutation(project,callback){
15
+ await mkdir(flow(project),{recursive:true});const lock=path.join(flow(project),'plugin-mutation.lock');
16
+ try{await mkdir(lock);}catch(error){if(error.code==='EEXIST')throw new FlowError('插件状态正在被其他进程修改;若上次异常退出,请确认没有活动 fst 后移除 .flow/plugin-mutation.lock',{code:'PLUGIN_BUSY'});throw error;}
17
+ try{return await callback();}finally{await rmdir(lock);}
18
+ }
19
+
20
+ export async function leaseInstallation(project,root){
21
+ if(!root)return async()=>{};
22
+ const directory=path.join(flow(project),'installation-leases');await mkdir(directory,{recursive:true});
23
+ const file=path.join(directory,`${randomUUID()}.json`);
24
+ await writeFile(file,JSON.stringify({pid:process.pid,root:path.resolve(root)}));
25
+ return async()=>{try{await unlink(file);}catch(error){if(error.code!=='ENOENT')throw error;}};
26
+ }
27
+
28
+ export async function openPluginState(project,{readOnly=false}={}){
29
+ const existing=await readPluginState(project);
30
+ if(readOnly||!existing.root)return {state:existing,release:async()=>{}};
31
+ return withPluginMutation(project,async()=>{const state=await readPluginState(project);return {state,release:await leaseInstallation(project,state.root)};});
32
+ }
33
+
34
+ export async function assertPluginState(project,expected){
35
+ if(!isDeepStrictEqual(await readPluginState(project),expected))throw new FlowError('插件状态已被其他进程修改,请重启当前宿主后再操作',{code:'PLUGIN_STATE_CHANGED'});
36
+ }
37
+
38
+ export async function savePluginState(project,next,previous){
39
+ const backup=path.join(flow(project),'plugins.previous.json'),id=randomUUID();
40
+ await writeFile(`${backup}.${id}.tmp`,JSON.stringify(previous,null,2));await rename(`${backup}.${id}.tmp`,backup);
41
+ const file=stateFile(project);await writeFile(`${file}.${id}.tmp`,JSON.stringify(next,null,2));await rename(`${file}.${id}.tmp`,file);
42
+ }
43
+
44
+ export async function collectInstallations(project,{apply=false}={}){
45
+ // 预览仅读取,不创建锁或备份目录;执行时在锁内重新计算,不能复用旧预览。
46
+ const collect=async()=>{
47
+ const base=path.join(flow(project),'installations'),state=await readPluginState(project),previous=await json(path.join(flow(project),'plugins.previous.json'),{});
48
+ const protectedRoots=new Set([state.root,state.previousRoot,previous.root].filter(Boolean).map(root=>path.resolve(root)));
49
+ let leases=[];try{leases=await readdir(path.join(flow(project),'installation-leases'));}catch(error){if(error.code!=='ENOENT')throw error;}
50
+ for(const file of leases){
51
+ const lease=await json(path.join(flow(project),'installation-leases',file),null);if(!lease)continue;
52
+ // 无法确认进程已退出时宁可保留;PID 重用只会保守地多保留一个批次。
53
+ try{process.kill(lease.pid,0);protectedRoots.add(path.resolve(lease.root));}catch(error){if(error.code!=='ESRCH')protectedRoots.add(path.resolve(lease.root));}
54
+ }
55
+ let entries=[];try{entries=await readdir(base,{withFileTypes:true});}catch(error){if(error.code!=='ENOENT')throw error;}
56
+ const candidates=[],kept=[],removed=[];
57
+ const canonicalBase=entries.length?await realpath(base):base;
58
+ if(entries.length){
59
+ const projectRoot=await realpath(project),relative=path.relative(projectRoot,canonicalBase);
60
+ if(relative==='..'||relative.startsWith('..'+path.sep)||path.isAbsolute(relative)||(await lstat(base)).isSymbolicLink())throw new FlowError('安装目录越界或是目录链接,拒绝清理');
61
+ for(const root of [...protectedRoots]){try{protectedRoots.add(await realpath(root));}catch(error){if(error.code!=='ENOENT')throw error;}}
62
+ }
63
+ for(const entry of entries){
64
+ const directory=path.join(base,entry.name);
65
+ if(!entry.isDirectory()||!/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/.test(entry.name)){kept.push({directory,reason:'非受管批次'});continue;}
66
+ const physical=await realpath(directory);
67
+ if(path.dirname(physical)!==canonicalBase||(await lstat(directory)).isSymbolicLink()||protectedRoots.has(physical)){kept.push({directory,reason:'当前、恢复、活动批次或链接'});continue;}
68
+ const pkg=await json(path.join(directory,'package.json'),{});
69
+ if(pkg.name!=='flow-project-plugins'){kept.push({directory,reason:'无法确认批次归属'});continue;}
70
+ candidates.push({directory});
71
+ if(apply){
72
+ const backups=path.join(flow(project),'installation-backups');await mkdir(backups,{recursive:true});
73
+ const backup=path.join(backups,`${entry.name}-${randomUUID()}.tgz`),temporary=`${backup}.tmp`;
74
+ await archive({cwd:canonicalBase,file:temporary,gzip:true,follow:false,portable:true},[entry.name]);
75
+ await rename(temporary,backup);
76
+ // 备份完成后再次确认真实绝对路径仍是本项目的直接子目录,才允许递归删除。
77
+ if(await realpath(directory)!==physical||path.dirname(physical)!==canonicalBase)throw new FlowError('安装批次路径在清理期间改变,已停止');
78
+ await rm(physical,{recursive:true,maxRetries:5,retryDelay:200});removed.push({directory,backup});
79
+ }
80
+ }
81
+ return {candidates,kept,removed};
82
+ };
83
+ return apply?withPluginMutation(project,collect):collect();
84
+ }
@@ -0,0 +1,35 @@
1
+ import path from 'node:path';
2
+ import {mkdir,writeFile,access} from 'node:fs/promises';
3
+ import {randomUUID} from 'node:crypto';
4
+ import {FlowError} from '@mytools/sdk';
5
+ import {readJson} from '@mytools/fst-platform/json';
6
+ import {npm} from '@mytools/fst-platform/npm';
7
+ import {loadPlugin} from '@mytools/fst-plugin/loader';
8
+ export async function installGeneration(projectDir,{add,remove,link,state}) {
9
+ const root=path.join(projectDir,'.flow','installations',randomUUID());
10
+ await mkdir(root,{recursive:true});
11
+ const dependencies={...(state?.dependencies??{})};
12
+ const links={...(state?.links??{})};
13
+ if(remove){delete dependencies[remove];delete links[remove];}
14
+ if(link){
15
+ const source=path.resolve(link),pkg=await readJson(path.join(source,'package.json'));
16
+ const loaded=await loadPlugin(source,projectDir);
17
+ if(!pkg.name||pkg.name!==loaded.plugin?.id||pkg.version!==loaded.plugin?.version)throw new FlowError('开发插件的包名/版本必须与插件 id/version 一致',{code:'VALIDATION'});
18
+ links[pkg.name]=source;delete dependencies[pkg.name];
19
+ }
20
+ await writeFile(path.join(root,'package.json'),JSON.stringify({name:'flow-project-plugins',private:true,dependencies},null,2));
21
+ let source=add;
22
+ if(add) {
23
+ try {await access(path.resolve(projectDir,add));source=path.resolve(projectDir,add);} catch {}
24
+ if(source.startsWith('-')) throw new FlowError('插件来源不能以选项符号开头',{code:'VALIDATION'});
25
+ }
26
+ if(source||Object.keys(dependencies).length){
27
+ const result=await npm(['install',...(source?[source]:[]),'--save-exact','--ignore-scripts','--install-links','--no-audit','--no-fund'],{cwd:root});
28
+ if(result.exitCode!==0) throw new FlowError(`插件安装失败:${result.stderr}`,{code:'INSTALL_FAILED'});
29
+ }
30
+ const pkg=await readJson(path.join(root,'package.json'));
31
+ const loaded=[];
32
+ for(const name of Object.keys(pkg.dependencies??{})){delete links[name];loaded.push(await loadPlugin(name,root));}
33
+ for(const source of Object.values(links))loaded.push(await loadPlugin(source,projectDir));
34
+ return {state:{root,dependencies:pkg.dependencies??{},links,...(state?.root?{previousRoot:state.root}:{})},loaded};
35
+ }
@@ -0,0 +1,29 @@
1
+ import { mkdir, copyFile, writeFile, access } from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { fileURLToPath } from 'node:url';
6
+ import {prepareBundleWorkspace} from './bundle-workspace.js';
7
+
8
+ const templates=fileURLToPath(new URL('../templates/',import.meta.url));
9
+ async function exists(file){try{await access(file);return true;}catch(error){if(error.code==='ENOENT')return false;throw error;}}
10
+
11
+ export async function initializeProject(directory){
12
+ directory=path.resolve(directory);
13
+ if(await exists(path.join(directory,'flow.project.json')))return directory;
14
+ await mkdir(path.join(directory,'workflows'),{recursive:true});
15
+ for(const name of ['hello','system-check','python-check','control-demo']){
16
+ try{await copyFile(path.join(templates,`${name}.yaml`),path.join(directory,'workflows',`${name}.yaml`),constants.COPYFILE_EXCL);}
17
+ catch(error){if(error.code!=='EEXIST')throw error;}
18
+ }
19
+ // npm 全局安装目录只保存程序,用户数据始终写入独立项目。
20
+ try{await writeFile(path.join(directory,'flow.project.json'),JSON.stringify({plugins:['@mytools/environment','@mytools/examples','@mytools/web'],config:{}},null,2),{flag:'wx'});}
21
+ catch(error){if(error.code!=='EEXIST')throw error;}
22
+ return directory;
23
+ }
24
+
25
+ export async function resolveProject(explicit,workspace){
26
+ if(explicit)return prepareBundleWorkspace(path.resolve(explicit),workspace);
27
+ if(await exists(path.join(process.cwd(),'flow.project.json')))return prepareBundleWorkspace(process.cwd(),workspace);
28
+ return initializeProject(process.env.FISHTOOLS_HOME??path.join(os.homedir(),'.fishtools'));
29
+ }
@@ -0,0 +1,150 @@
1
+ import { mkdir, readFile, readdir, realpath, writeFile, rename, open, unlink } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import YAML from 'yaml';
5
+ import { FlowError } from '@mytools/sdk';
6
+ import { assertId, assertSchema, references, referenceParts, emptyObject } from '@mytools/sdk/schema';
7
+ import {resolveSchemaPath} from '@mytools/sdk/schema-path';
8
+ import {validateCondition,validateStepList} from '@mytools/fst-workflow/control';
9
+
10
+ export class WorkflowStore {
11
+ constructor(root,{openFile=open}={}) { this.root = path.resolve(root);this.openFile=openFile; }
12
+ file(id) { assertId(id); return path.join(this.root,`${id}.yaml`); }
13
+ async get(id) {
14
+ const file=this.file(id);
15
+ const physical=await realpath(file);
16
+ const root=await realpath(this.root);
17
+ if (!physical.startsWith(root+path.sep)) throw new FlowError('工作流越界,必须位于工作流目录内',{code:'VALIDATION'});
18
+ const definition=YAML.parse(await readFile(file,'utf8'));
19
+ if (definition?.id !== id) throw new FlowError(`工作流 ID 与文件名不一致:${id}`,{code:'VALIDATION'});
20
+ return this.validateShape(definition);
21
+ }
22
+ validateShape(w) {
23
+ assertId(w?.id);
24
+ if (w.schemaVersion!==1 || !Array.isArray(w.steps)) throw new FlowError('工作流格式错误:需要 schemaVersion: 1 与 steps 列表',{code:'VALIDATION'});
25
+ validateStepList(w.steps,assertId);
26
+ return structuredClone(w);
27
+ }
28
+ async save(definition) {
29
+ const w=this.validateShape(definition);
30
+ await mkdir(this.root,{recursive:true});
31
+ const file=this.file(w.id), temporary=`${file}.${randomUUID()}.tmp`;
32
+ let previous;
33
+ try{
34
+ const physical=await realpath(file),root=await realpath(this.root);
35
+ if(!physical.startsWith(root+path.sep))throw new FlowError('工作流越界,不能覆盖工作流目录之外的文件',{code:'VALIDATION'});
36
+ previous=await readFile(file,'utf8');
37
+ }catch(error){if(error.code!=='ENOENT')throw error;}
38
+ if(previous!==undefined){
39
+ const directory=path.join(path.dirname(this.root),'.flow','workflow-backups');
40
+ await mkdir(directory,{recursive:true,mode:0o700});
41
+ await writeFile(path.join(directory,`${w.id}-${randomUUID()}.yaml`),previous,{flag:'wx',mode:0o600});
42
+ }
43
+ try{await writeFile(temporary,YAML.stringify(w),'utf8');await rename(temporary,file);}
44
+ catch(error){try{await unlink(temporary);}catch(cleanup){if(cleanup.code!=='ENOENT')throw new AggregateError([error,cleanup],`工作流保存失败且临时文件无法清理:${temporary}`);}throw error;}
45
+ return w;
46
+ }
47
+ async duplicate(definition,{id,title}={}) {
48
+ const source=this.validateShape(definition);
49
+ const copy=this.validateShape({...source,id:id??`flow-${randomUUID()}`,title:title??`${source.title??source.id} 副本`});
50
+ const file=this.file(copy.id),content=YAML.stringify(copy);
51
+ if(typeof copy.title!=='string'||!copy.title.trim())throw new FlowError('工作流名称不能为空',{code:'VALIDATION'});
52
+ await mkdir(this.root,{recursive:true});
53
+ let handle;
54
+ try{
55
+ // 排他创建保证并发请求也不能覆盖同名工作流。
56
+ handle=await this.openFile(file,'wx');
57
+ await handle.writeFile(content,'utf8');
58
+ await handle.close();handle=null;
59
+ return {definition:copy,path:file};
60
+ }catch(error){
61
+ if(handle){
62
+ try{await handle.close();}catch{}
63
+ try{await unlink(file);}catch(cleanupError){if(cleanupError.code!=='ENOENT')throw new AggregateError([error,cleanupError],`复制失败且无法清理新文件:${file}`);}
64
+ }
65
+ if(error.code==='EEXIST')throw new FlowError(`工作流已存在:${copy.id}`,{code:'VALIDATION',cause:error});
66
+ throw error;
67
+ }
68
+ }
69
+ async list() {
70
+ await mkdir(this.root,{recursive:true});
71
+ const files=await readdir(this.root);
72
+ return Promise.all(files.filter(f=>f.endsWith('.yaml')).sort().map(f=>this.get(f.slice(0,-5))));
73
+ }
74
+ async remove(id){
75
+ await this.get(id);
76
+ const refers=steps=>steps.some(step=>step.workflow&&this.childId(step.workflow)===id||step.branch&&(refers(step.branch.then)||refers(step.branch.else??[]))||step.parallel&&refers(step.parallel.steps));
77
+ const dependents=(await this.list()).filter(flow=>flow.id!==id&&refers(flow.steps));
78
+ if(dependents.length)throw new FlowError(`工作流仍被引用:${dependents.map(flow=>flow.title??flow.id).join('、')}`);
79
+ const directory=path.join(path.dirname(this.root),'.flow','deleted-workflows');
80
+ await mkdir(directory,{recursive:true});
81
+ const backup=path.join(directory,`${id}-${randomUUID()}.yaml`);
82
+ // 同项目内原子移动,备份保留完整原文;复制回原文件名即可恢复。
83
+ await rename(this.file(id),backup);return backup;
84
+ }
85
+ childId(reference) {
86
+ if(typeof reference!=='string' || !/^(\.\/)?[A-Za-z0-9][A-Za-z0-9_-]*\.yaml$/.test(reference)) throw new FlowError(`子流程必须位于工作流目录内:${reference}`,{code:'VALIDATION'});
87
+ return reference.replace(/^\.\//,'').slice(0,-5);
88
+ }
89
+ async resolve(entries,host) {
90
+ const workflows=new Map(), outputs=new Map(), plugins=new Set(), visiting=new Set();
91
+ const visit=async id=>{
92
+ if(visiting.has(id)) throw new FlowError(`工作流循环引用:${[...visiting,id].join(' → ')}`,{code:'VALIDATION'});
93
+ if(workflows.has(id)) return;
94
+ visiting.add(id);
95
+ const w=await this.get(id);
96
+ const checker=previous=>ref=>{
97
+ const p=referenceParts(ref);
98
+ if(p[0]==='steps' && !previous.has(p[1])) throw new FlowError(`${id} 引用必须指向前序步骤:${ref}`,{code:'VALIDATION'});
99
+ return resolveSchemaPath(p[0]==='inputs'?(w.inputs??emptyObject):previous.get(p[1]),p.slice(p[0]==='inputs'?1:3),ref);
100
+ };
101
+ const resolveList=async(steps,inherited=new Map(),parallel=false)=>{
102
+ const previous=new Map(inherited),own={};
103
+ for(const s of steps){
104
+ const check=checker(parallel?inherited:previous);
105
+ if(s.when)validateCondition(s.when,check);
106
+ let output;
107
+ if(s.action){
108
+ references(s.with??{},check);
109
+ const action=host.actions.get(s.action);
110
+ if(!action)throw new FlowError(`${id} → ${s.id} 缺少插件动作 ${s.action}`,{code:'VALIDATION'});
111
+ if(action.input===undefined||action.output===undefined)throw new FlowError(`可编排动作 ${s.action} 必须声明 input 和 output schema`,{code:'VALIDATION'});
112
+ assertSchema(action.input,`${s.action} input schema`);assertSchema(action.output,`${s.action} output schema`);
113
+ plugins.add(action.plugin);output=action.output;
114
+ }else if(s.workflow){references(s.with??{},check);const child=this.childId(s.workflow);await visit(child);output=outputs.get(child);}
115
+ else if(s.branch){
116
+ validateCondition(s.branch.condition,check);
117
+ const left=await resolveList(s.branch.then,parallel?inherited:previous),right=await resolveList(s.branch.else??[],parallel?inherited:previous);
118
+ output={type:'object',additionalProperties:false,properties:{...left.own,...right.own}};
119
+ }else if(s.parallel){const group=await resolveList(s.parallel.steps,parallel?inherited:previous,true);output={type:'object',additionalProperties:false,properties:group.own};}
120
+ else output=emptyObject;
121
+ previous.set(s.id,output);own[s.id]=output;
122
+ }
123
+ return {previous,own};
124
+ };
125
+ const {previous}=await resolveList(w.steps),check=checker(previous);
126
+ references(w.outputs??{},check);
127
+ const infer=value=>{
128
+ if(value?.$ref)return check(value.$ref);
129
+ if(Array.isArray(value))return {type:'array'};
130
+ if(value&&typeof value==='object')return {type:'object',additionalProperties:false,properties:Object.fromEntries(Object.entries(value).map(([key,v])=>[key,infer(v)]))};
131
+ return {type:value===null?'null':typeof value};
132
+ };
133
+ outputs.set(id,infer(w.outputs??{}));
134
+ workflows.set(id,w); visiting.delete(id);
135
+ };
136
+ for(const id of entries) await visit(id);
137
+ const resolved=new Set(), visitingPlugins=new Set();
138
+ const addPlugin=id=>{
139
+ if(visitingPlugins.has(id)) throw new FlowError(`插件循环依赖:${id}`,{code:'VALIDATION'});
140
+ if(resolved.has(id)) return;
141
+ const p=host.definitions.get(id);
142
+ if(!p) throw new FlowError(`插件依赖缺失:${id}`,{code:'VALIDATION'});
143
+ visitingPlugins.add(id);
144
+ Object.keys(p.dependencies??{}).forEach(addPlugin);
145
+ visitingPlugins.delete(id); resolved.add(id);
146
+ };
147
+ plugins.forEach(addPlugin);
148
+ return {workflows:[...workflows.values()],plugins:[...resolved]};
149
+ }
150
+ }
@@ -0,0 +1,47 @@
1
+ schemaVersion: 1
2
+ id: control-demo
3
+ title: 条件与并行入门
4
+ description: 检查系统后,根据你的选择继续或提前结束;继续时并行处理两段文本。运行中可切换步骤和查看日志。
5
+ inputs:
6
+ type: object
7
+ properties:
8
+ proceed:
9
+ type: boolean
10
+ title: 继续执行文本处理
11
+ default: true
12
+ required: [proceed]
13
+ steps:
14
+ - id: environment
15
+ action: "@mytools/environment:report"
16
+ with: {}
17
+ - id: guard
18
+ when:
19
+ left: {$ref: steps.environment.output.ok}
20
+ op: eq
21
+ right: false
22
+ stop:
23
+ reason: 环境检查未通过,已停止后续步骤。
24
+ - id: choose
25
+ branch:
26
+ condition:
27
+ left: {$ref: inputs.proceed}
28
+ op: eq
29
+ right: true
30
+ then:
31
+ - id: batch
32
+ parallel:
33
+ concurrency: 2
34
+ steps:
35
+ - id: first
36
+ action: "@mytools/examples:uppercase"
37
+ with: {text: "Hello, Fishtools!"}
38
+ - id: second
39
+ action: "@mytools/examples:uppercase"
40
+ with: {text: Parallel workflow}
41
+ else:
42
+ - id: finish
43
+ stop:
44
+ reason: 本次选择提前结束,没有执行文本处理。
45
+ outputs:
46
+ first: {$ref: steps.choose.output.batch.first.text}
47
+ second: {$ref: steps.choose.output.batch.second.text}
@@ -0,0 +1,35 @@
1
+ schemaVersion: 1
2
+ id: hello
3
+ title: 我的第一个工作流
4
+ description: 将输入文本转为大写,追加完成标记,再保存到文件。所有步骤都可在界面中调整。
5
+ inputs:
6
+ type: object
7
+ required:
8
+ - text
9
+ properties:
10
+ text:
11
+ type: string
12
+ title: 输入文本
13
+ default: Hello, workflow!
14
+ steps:
15
+ - id: uppercase
16
+ action: "@mytools/examples:uppercase"
17
+ with:
18
+ text:
19
+ $ref: inputs.text
20
+ - id: append
21
+ action: "@mytools/examples:append"
22
+ with:
23
+ text:
24
+ $ref: steps.uppercase.output.text
25
+ suffix: " ✓"
26
+ - id: save
27
+ action: "@mytools/examples:save"
28
+ with:
29
+ text:
30
+ $ref: steps.append.output.text
31
+ outputs:
32
+ text:
33
+ $ref: steps.append.output.text
34
+ file:
35
+ $ref: steps.save.output.path
@@ -0,0 +1,26 @@
1
+ schemaVersion: 1
2
+ id: python-check
3
+ title: 检查 Python 并执行脚本
4
+ description: 检查指定的目标 Python,再用同一个解释器运行插件提供的脚本。检查失败会停止。
5
+ inputs:
6
+ type: object
7
+ required: [executable, text]
8
+ properties:
9
+ executable: { type: string, title: 目标 Python 解释器路径 }
10
+ text: { type: string, title: 输入文本, default: Hello Python }
11
+ steps:
12
+ - id: check
13
+ action: '@mytools/environment:check'
14
+ with:
15
+ python:
16
+ executable: { $ref: inputs.executable }
17
+ version: '>=3.9'
18
+ pip: false
19
+ - id: python
20
+ action: '@mytools/examples:pythonEcho'
21
+ with:
22
+ executable: { $ref: steps.check.output.python.executable }
23
+ text: { $ref: inputs.text }
24
+ outputs:
25
+ text: { $ref: steps.python.output.text }
26
+ executable: { $ref: steps.python.output.executable }
@@ -0,0 +1,11 @@
1
+ schemaVersion: 1
2
+ id: system-check
3
+ title: 查看系统环境
4
+ description: 通过官方环境检查插件查看目标机器的操作系统、架构和内核,无需安装 Python。
5
+ inputs: { type: object, properties: {} }
6
+ steps:
7
+ - id: check
8
+ action: '@mytools/environment:check'
9
+ with: {}
10
+ outputs:
11
+ system: { $ref: steps.check.output.system }