@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,86 @@
1
+ import {FlowError} from '@mytools/sdk';
2
+ import {conditionOperators} from '@mytools/sdk/conditions';
3
+ import {resolveSchemaPath} from '@mytools/sdk/schema-path';
4
+ import {inside} from '@mytools/fst-platform/environments';
5
+ import path from 'node:path';
6
+
7
+ export const builtinCommands=[
8
+ ['workflows','选择已有工作流'],['new','新建工作流'],['add','添加插件能力或子工作流'],
9
+ ['steps','设置步骤参数、排序或删除'],['outputs','设置最终输出'],['save','保存工作流'],
10
+ ['run','运行工作流,可附带 @参数.json'],['runs','运行记录:选择任意一次执行,查看进度、结果和日志'],['logs','查看运行日志'],['plugin','管理插件'],['plugins','管理插件(兼容旧入口)'],['config','配置 Fishtools'],['tools','Web 与离线文档'],
11
+ ['pack','选择工作流打包'],['web','启动 Web 工作台'],['docs','阅读离线开发与使用文档'],['help','查看操作说明'],['exit','退出终端工作台'],
12
+ ].map(([name,description])=>({name,description}));
13
+
14
+ function validatePrivilegeDeclarations(plugin){
15
+ const validObject=value=>Boolean(value)&&typeof value==='object'&&!Array.isArray(value);
16
+ const invalid=(path,message)=>{throw new FlowError(message,{code:'VALIDATION',details:{path}});};
17
+ const entries=[['plugin',plugin,undefined],...Object.entries(plugin.actions??{}).map(([name,action])=>[`plugin.actions.${name}`,action,plugin])];
18
+ for(const [location,owner,parent] of entries){
19
+ if(owner.preflight!==undefined&&typeof owner.preflight!=='function')invalid(`${location}.preflight`,`${location} 的 preflight 必须为函数`);
20
+ if(owner.requirements===undefined)continue;
21
+ if(!validObject(owner.requirements))invalid(`${location}.requirements`,`${location} 的 requirements 必须为对象`);
22
+ const privilege=owner.requirements.privilege;
23
+ if(privilege===undefined)continue;
24
+ if(!validObject(privilege))invalid(`${location}.requirements.privilege`,`${location} 的 privilege 必须为对象`);
25
+ for(const [platform,requirement] of Object.entries(privilege)){
26
+ const at=`${location}.requirements.privilege.${platform}`;
27
+ if(!['linux','win32'].includes(platform))invalid(at,`不支持的权限声明平台:${platform}`);
28
+ if(!validObject(requirement))invalid(at,`${at} 必须为对象`);
29
+ const levels=platform==='linux'?['user','root','resource']:['user','administrator','resource'];
30
+ if(!levels.includes(requirement.level))invalid(`${at}.level`,`${at}.level 无效`);
31
+ for(const field of ['reason','remediation']){
32
+ if((requirement.level!=='user'||requirement[field]!==undefined)&&(typeof requirement[field]!=='string'||!requirement[field].trim()))invalid(`${at}.${field}`,`${at}.${field} 必须为非空字符串`);
33
+ }
34
+ if((requirement.level==='resource'||requirement.level==='administrator')&&typeof (owner.preflight??parent?.preflight)!=='function')invalid(at,`${at} 必须配套 preflight`);
35
+ }
36
+ }
37
+ }
38
+
39
+ export function validateDeclarations(plugin){
40
+ for(const [name,action] of Object.entries(plugin.actions??{})){
41
+ if(!action||typeof action!=='object'||Array.isArray(action))throw new FlowError(`动作 ${name} 的声明必须为对象`,{code:'VALIDATION'});
42
+ if(action.conditions!==undefined&&(!action.conditions||typeof action.conditions!=='object'||Array.isArray(action.conditions)))throw new FlowError(`动作 ${name} 的 conditions 必须为对象`,{code:'VALIDATION'});
43
+ for(const [key,preset] of Object.entries(action.conditions??{})){
44
+ if(!preset||typeof preset.title!=='string'||!preset.title.trim()||typeof preset.path!=='string'||!preset.path||preset.path.split('.').some(p=>!p||['__proto__','constructor','prototype'].includes(p))||!conditionOperators.includes(preset.op)||(preset.op!=='exists'&&!Object.hasOwn(preset,'value')))throw new FlowError(`动作 ${name} 的判定预设 ${key} 无效`,{code:'VALIDATION'});
45
+ try{resolveSchemaPath(action.output,preset.path);}
46
+ catch{throw new FlowError(`判定预设 ${key} 引用了未声明的输出字段 ${preset.path}`,{code:'VALIDATION'});}
47
+ }
48
+ }
49
+ for(const key of ['platforms','architectures'])if(plugin[key]!==undefined&&(!Array.isArray(plugin[key])||!plugin[key].length||!plugin[key].every(x=>typeof x==='string'&&x.trim())||new Set(plugin[key]).size!==plugin[key].length))throw new FlowError(`${plugin.id} 的 ${key} 必须是非空且不重复的字符串数组`,{code:'VALIDATION'});
50
+ validatePrivilegeDeclarations(plugin);
51
+ if(plugin.runtime?.updatePolicy!==undefined&&!['automatic','maintenance'].includes(plugin.runtime.updatePolicy))throw new FlowError('运行时 updatePolicy 必须为 automatic 或 maintenance',{code:'VALIDATION',details:{path:'plugin.runtime.updatePolicy'}});
52
+ for(const [owner,location,declarations] of [['插件','plugin.environments',plugin.environments],...Object.entries(plugin.actions??{}).map(([name,action])=>[`动作 ${name}`,`plugin.actions.${name}.environments`,action.environments])]){
53
+ if(declarations!==undefined&&(!declarations||typeof declarations!=='object'||Array.isArray(declarations)))throw new FlowError(`${owner} 的 environments 必须为对象`,{code:'VALIDATION'});
54
+ for(const [id,e] of Object.entries(declarations??{})){
55
+ const at=`${location}.${id}`;
56
+ const invalid=(field,message)=>{throw new FlowError(message,{code:'VALIDATION',details:{path:`${at}.${field}`}});};
57
+ if(!/^[\w-]+$/.test(id)||!e||!['host','bundled','tool'].includes(e.source))throw new FlowError(`环境声明无效:${plugin.id}/${id}`);
58
+ const validString=value=>typeof value==='string'&&Boolean(value.trim());
59
+ if((e.source==='host'||e.executable!==undefined)&&!(e.source==='host'?(validString(e.executable)||Array.isArray(e.executable)&&e.executable.length>0&&e.executable.every(validString)&&new Set(e.executable).size===e.executable.length):validString(e.executable)))throw new FlowError(`环境 executable 声明无效:${plugin.id}/${id}`,{code:'VALIDATION'});
60
+ if(e.allowedSources&&(!Array.isArray(e.allowedSources)||!e.allowedSources.includes(e.source)||!e.allowedSources.every(x=>['host','bundled','tool'].includes(x))))throw new FlowError(`环境来源范围无效:${id}`);
61
+ if(e.source==='tool'&&id!=='node')throw new FlowError(`工具尚未提供环境:${id}`);
62
+ if(e.source==='bundled'||e.artifacts!==undefined){
63
+ const artifacts=e.artifacts;
64
+ if(!artifacts||typeof artifacts!=='object'||Array.isArray(artifacts)||e.source==='bundled'&&!Object.keys(artifacts).length)invalid('artifacts',`环境 ${plugin.id}/${id} 的 artifacts 必须为非空目标字典`);
65
+ for(const [target,artifact] of Object.entries(artifacts)){
66
+ if(!target.trim()||!artifact||typeof artifact!=='object'||Array.isArray(artifact))invalid(`artifacts.${target}`,`环境 ${plugin.id}/${id} 的目标资源无效:${target}`);
67
+ // 与运行时使用同一包内路径规则;此处只查声明,不读取目标平台文件。
68
+ let directory;
69
+ try{if(typeof artifact.directory!=='string'||!artifact.directory.trim())throw new Error();directory=inside(process.cwd(),artifact.directory);}
70
+ catch{invalid(`artifacts.${target}.directory`,`环境 ${plugin.id}/${id} 的资源目录必须是包内相对路径`);}
71
+ try{if(typeof artifact.executable!=='string'||!artifact.executable.trim()||path.normalize(artifact.executable)==='.')throw new Error();inside(directory,artifact.executable);}
72
+ catch{invalid(`artifacts.${target}.executable`,`环境 ${plugin.id}/${id} 的程序路径必须是包内相对路径`);}
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ export function validateCommands(plugin,registered){
80
+ for(const [name,c] of Object.entries(plugin.commands??{})){
81
+ if(!/^[a-zA-Z][\w.-]*$/.test(name)||builtinCommands.some(x=>x.name===name)||registered.has(name))throw new FlowError(`命令名称无效或冲突:/${name}`);
82
+ if(!c||Boolean(c.action)===Boolean(c.run)||c.run&&typeof c.run!=='function')throw new FlowError(`命令 /${name} 必须声明 action 或 run`);
83
+ if(c.action&&!plugin.actions?.[c.action])throw new FlowError(`命令 /${name} 指向不存在的动作:${c.action}`);
84
+ registered.add(name);
85
+ }
86
+ }
@@ -0,0 +1,235 @@
1
+ import { Context } from 'cordis';
2
+ import semver from 'semver';
3
+ import { SDK_VERSION, FlowError } from '@mytools/sdk';
4
+ import { validate, emptyObject } from '@mytools/sdk/schema';
5
+ import {validateDeclarations,validateCommands} from './contracts.js';
6
+ import {prepareEnvironments,platformWarnings} from '@mytools/fst-platform/environments';
7
+ import path from 'node:path';
8
+ import {mkdir} from 'node:fs/promises';
9
+ import {createHash} from 'node:crypto';
10
+ import {createRuntime} from './runtime.js';
11
+ import {pluginError} from '@mytools/sdk/diagnostics';
12
+ import {validateNetworkPolicy,resolveNetworkPolicy,createHttpClient,buildProcessEnv} from '@mytools/fst-platform/network';
13
+ import {resolvePrivilegeRequirement,resolvePrivilegePreflight,checkStaticPrivilege,runPrivilegePreflight} from './privileges.js';
14
+
15
+ export async function createHost({ plugins = [], config = {}, services = {}, project,network,environmentOverrides={},runtimeEnabled=true,allowUnknownPrivileges=false } = {}) {
16
+ const currentNetwork=()=>typeof network==='function'?network():network;
17
+ validateNetworkPolicy(currentNetwork());
18
+ const ctx = new Context();
19
+ const actions = new Map();
20
+ const definitions = new Map();
21
+ const extensions = new Map();
22
+ const serviceNames = new Set();
23
+ const commandNames=new Set(),commands=new Map();
24
+ function bindContext(context){
25
+ const {application:ignored,...capabilities}=services;
26
+ const bound={...capabilities,...context},execute=bound.runProcess;
27
+ // 面向用户的输出与诊断日志共用持久化通道,只区分默认可见性。
28
+ bound.print=(message,data)=>Promise.resolve(bound.log?.(message,data,{audience:'user'}));
29
+ if(typeof execute==='function')bound.runProcess=(file,args,options={})=>{
30
+ const signal=context.signal&&options.signal?AbortSignal.any([context.signal,options.signal]):context.signal??options.signal;
31
+ return execute(file,args,{...options,signal,onOutput:async({stream,message})=>{
32
+ await bound.log?.(message,undefined,{stream});
33
+ await options.onOutput?.({stream,message});
34
+ }});
35
+ };
36
+ return bound;
37
+ }
38
+ async function pluginContext(plugin,action,context){
39
+ const bound=bindContext(context);
40
+ const projectDir=project?.directory??services.application?.projectDir;
41
+ if(bound.fs){
42
+ const probe=bound.fs,resolve=file=>typeof file==='string'&&!/^[/\\]{2}/.test(file)?path.resolve(projectDir??process.cwd(),file):file;
43
+ const options=value=>({...value,signal:context.signal&&value?.signal?AbortSignal.any([context.signal,value.signal]):context.signal??value?.signal});
44
+ bound.fs={stat:(file,value)=>probe.stat(resolve(file),options(value)),readDirectory:(file,value)=>probe.readDirectory(resolve(file),options(value))};
45
+ }
46
+ // 固定长度摘要避免 scoped 包名分隔符、大小写碰撞及合法长包名超出文件名上限。
47
+ const readOnly=project?.readOnly??services.application?.project?.readOnly;
48
+ const stateDir=projectDir&&!readOnly?path.join(projectDir,'.flow','plugin-state',createHash('sha256').update(plugin.id).digest('hex')):undefined;
49
+ if(stateDir)await mkdir(stateDir,{recursive:true});
50
+ for(const message of platformWarnings(plugin))await bound.log?.(message,{code:'PLATFORM_MISMATCH'},{audience:'user'});
51
+ const environments=await prepareEnvironments(plugin,action,environmentOverrides[plugin.id],{cwd:projectDir??process.cwd()});
52
+ context.signal?.throwIfAborted();
53
+ const policy=context.networkPolicy??currentNetwork();
54
+ const actionName=action?.id?.slice(plugin.id.length+1);
55
+ const resolved=resolveNetworkPolicy(policy,{pluginId:plugin.id,actionId:actionName});
56
+ if(policy?.default||policy?.plugins?.[plugin.id])await bound.log?.('网络代理规则',{mode:resolved.mode,...(resolved.profile?{profile:resolved.profile}:{})});
57
+ const http=createHttpClient(resolved,{signal:context.signal});
58
+ if(typeof bound.runProcess==='function'){
59
+ const execute=bound.runProcess;
60
+ // 已构造完整环境;禁止进程层再次合并宿主代理,恢复被策略移除的变量。
61
+ bound.runProcess=(file,args,options={})=>execute(file,args,{...options,env:buildProcessEnv(resolved,{overrides:options.env}),extendEnv:false});
62
+ }
63
+ bound.http={fetch:(input,options)=>http.fetch(input,options)};
64
+ return {capabilities:{...bound,projectDir,stateDir,environments},close:()=>http.close()};
65
+ }
66
+ async function checkPrivilege(plugin,actionName,input,context,fs){
67
+ const requirement=resolvePrivilegeRequirement(plugin,actionName);
68
+ if(!requirement.declared)return requirement;
69
+ const staticResult=checkStaticPrivilege(requirement);
70
+ let result=staticResult;
71
+ if(staticResult.status==='unknown')result=await runPrivilegePreflight(resolvePrivilegePreflight(plugin,actionName),input,{signal:context.signal,fs});
72
+ context.signal?.throwIfAborted();
73
+ if(result.status==='blocked')throw new FlowError(`权限不足:${result.reason??requirement.reason??plugin.id}`,{code:'INSUFFICIENT_PRIVILEGE',details:{plugin:plugin.id,action:actionName,requirement,result}});
74
+ if(result.status==='unknown'&&!context.allowUnknownPrivileges)throw new FlowError(`权限尚未确认:${result.reason??requirement.reason??plugin.id}`,{code:'PRIVILEGE_UNVERIFIED',details:{plugin:plugin.id,action:actionName,requirement,result}});
75
+ if(result.status==='unknown')await context.log?.('权限状态未确认,按本次选择继续运行',{plugin:plugin.id,action:actionName,reason:result.reason},{audience:'user'});
76
+ return requirement;
77
+ }
78
+ for (const entry of plugins) {
79
+ const plugin = entry.plugin ?? entry;
80
+ validateDeclarations(plugin);validateCommands(plugin,commandNames);
81
+ if (!plugin.id || definitions.has(plugin.id)) throw new FlowError(`插件标识缺失或重复:${plugin.id}`, {code:'VALIDATION'});
82
+ if (!semver.satisfies(SDK_VERSION, plugin.sdk ?? '^0.1.0')) throw new FlowError(`插件 ${plugin.id} 与 SDK ${SDK_VERSION} 不兼容`, {code:'VALIDATION'});
83
+ for(const [name,action] of Object.entries(plugin.actions??{})){
84
+ if(typeof action?.run!=='function')throw new FlowError(`动作 ${plugin.id}:${name} 缺少执行函数`);
85
+ }
86
+ for(const [name,extension] of Object.entries(plugin.services??{})){
87
+ if(serviceNames.has(name))throw new FlowError(`服务重复:${name}`);
88
+ if(typeof extension!=='function')throw new FlowError(`服务 ${name} 缺少执行函数`);
89
+ serviceNames.add(name);
90
+ }
91
+ definitions.set(plugin.id, {...plugin, root:entry.root});
92
+ }
93
+ const settingsById=new Map();
94
+ for (const plugin of definitions.values()) {
95
+ for (const [id, range] of Object.entries(plugin.dependencies ?? {})) {
96
+ if (!definitions.has(id) || !semver.satisfies(definitions.get(id).version,range)) throw new FlowError(`插件 ${plugin.id} 缺少兼容依赖 ${id}@${range}`,{code:'VALIDATION'});
97
+ }
98
+ const settings = validate(plugin.config, config[plugin.id] ?? {}, `${plugin.id} 配置`);
99
+ settingsById.set(plugin.id,settings);
100
+ }
101
+ const runtime=createRuntime(ctx,definitions,{enabled:runtimeEnabled});
102
+ let unusable=false;
103
+ const assertReadable=()=>{
104
+ if(unusable)throw new FlowError('插件运行状态无法确认,请重启应用',{code:'PLUGIN_RUNTIME_UNUSABLE'});
105
+ };
106
+ const assertHealthy=()=>{runtime.assertOpen();assertReadable();};
107
+ function registerPlugin(plugin,settings){
108
+ const registered=plugin.runtime?{...plugin,runtime:{...plugin.runtime,apply:async(scope,value)=>{
109
+ await checkPrivilege(plugin,undefined,{}, {allowUnknownPrivileges},services.fs);
110
+ return plugin.runtime.apply(scope,value);
111
+ }}}:plugin;
112
+ runtime.register(registered,settings,scope=>{
113
+ for(const [name,command] of Object.entries(plugin.commands??{})){
114
+ const action=command.action?plugin.actions[command.action]:command;
115
+ commands.set(name,{...command,name,plugin:plugin.id,input:action.input,output:action.output,description:command.description??action.description??action.title??name,config:settings});
116
+ scope.on('dispose',()=>commands.delete(name));
117
+ }
118
+ for (const [name, action] of Object.entries(plugin.actions ?? {})) {
119
+ const id = `${plugin.id}:${name}`;
120
+ if (typeof action.run !== 'function') throw new FlowError(`动作 ${id} 缺少执行函数`);
121
+ actions.set(id, {...action, id, plugin:plugin.id, config:settings, root:plugin.root});
122
+ scope.on('dispose', () => actions.delete(id));
123
+ }
124
+ for (const [name, extension] of Object.entries(plugin.services ?? {})) {
125
+ if (extensions.has(name)) throw new FlowError(`服务重复:${name}`);
126
+ extensions.set(name, {run:extension,plugin:plugin.id});
127
+ scope.on('dispose',()=>extensions.delete(name));
128
+ }
129
+ });
130
+ }
131
+ try{
132
+ for (const plugin of definitions.values())registerPlugin(plugin,settingsById.get(plugin.id));
133
+ await runtime.start();
134
+ }catch(error){
135
+ try{await runtime.close();}catch(cleanup){if(cleanup.message!==error.message)throw new AggregateError([error,cleanup],`${error.message};${cleanup.message}`);}
136
+ throw error;
137
+ }
138
+ return {
139
+ definitions, actions,
140
+ assertHealthy,
141
+ invalidate(){unusable=true;},
142
+ get warnings(){return [...definitions.values()].flatMap(plugin=>platformWarnings(plugin).map(message=>({plugin:plugin.id,message})));},
143
+ get hasRuntime(){return runtime.hasRuntime;},
144
+ runtimeStatus:()=>runtime.status(),
145
+ async replace({plugins:nextEntries,config:nextConfig={},affected=[]}){
146
+ assertHealthy();
147
+ const selected=new Set(affected);
148
+ // 候选宿主只做声明、依赖、配置和服务图校验,不激活候选 runtime。
149
+ const candidate=await createHost({plugins:nextEntries,config:nextConfig,services,project,network,environmentOverrides,runtimeEnabled:false,allowUnknownPrivileges});
150
+ const next=new Map(candidate.definitions);
151
+ await candidate.close();
152
+ const oldDefinitions=new Map([...definitions].filter(([id])=>selected.has(id)));
153
+ const oldSettings=new Map([...settingsById].filter(([id])=>selected.has(id)));
154
+ const switchDefinitions=async(targets,settings)=>{
155
+ try{await runtime.remove(selected);}
156
+ catch(error){unusable=true;throw error;}
157
+ for(const id of selected){definitions.delete(id);settingsById.delete(id);}
158
+ for(const [id,plugin] of targets)if(selected.has(id)){
159
+ definitions.set(id,plugin);
160
+ settingsById.set(id,settings.get(id));
161
+ }
162
+ runtime.reindex();
163
+ for(const [id,plugin] of definitions)if(selected.has(id))registerPlugin(plugin,settingsById.get(id));
164
+ await runtime.ready();
165
+ };
166
+ const nextSettings=new Map();
167
+ for(const [id,plugin] of next)if(selected.has(id))nextSettings.set(id,validate(plugin.config,nextConfig[id]??{},`${id} 配置`));
168
+ try{await switchDefinitions(next,nextSettings);}
169
+ catch(error){
170
+ if(unusable)throw error;
171
+ try{
172
+ runtime.clearActivationErrors();
173
+ await switchDefinitions(oldDefinitions,oldSettings);
174
+ }catch(recovery){unusable=true;throw new AggregateError([error,recovery],`插件更新失败且恢复失败:${error.message};${recovery.message}`);}
175
+ throw error;
176
+ }
177
+ },
178
+ commands(){assertReadable();return [...commands.values()].map(({run,preflight,config,...metadata})=>structuredClone(metadata));},
179
+ async invokeCommand(name,input,context={}){
180
+ assertHealthy();
181
+ const command=commands.get(name);if(!command)throw new FlowError(`命令不存在:/${name}`);
182
+ if(command.action)return this.invoke(`${command.plugin}:${command.action}`,input,context);
183
+ if(!context.ui)throw new FlowError(`交互命令 /${name} 需要在 TUI 中调用`);
184
+ const plugin=definitions.get(command.plugin);
185
+ try{
186
+ const getService=runtime.bindServices(plugin.id);
187
+ context.signal?.throwIfAborted();
188
+ const checked=validate(command.input??emptyObject,input,`/${name} 输入`);
189
+ const bound=await pluginContext(plugin,undefined,context);
190
+ let result;
191
+ try{
192
+ await checkPrivilege(plugin,undefined,checked,context,bound.capabilities.fs);
193
+ result=await command.run(checked,{...bound.capabilities,getService,config:command.config,resourceDir:plugin.root});
194
+ }finally{await bound.close();}
195
+ context.signal?.throwIfAborted();
196
+ return validate(command.output,JSON.parse(JSON.stringify(result??{})),`/${name} 输出`,{useDefaults:false});
197
+ }catch(cause){throw pluginError(cause,{plugin:plugin.id,command:`/${name}`});}
198
+ },
199
+ catalog() { assertReadable();return [...actions.values()].map(({run,preflight,config,root,...metadata})=>structuredClone(metadata)); },
200
+ async invoke(id, input, context = {}) {
201
+ assertHealthy();
202
+ const action = actions.get(id);
203
+ if (!action) throw new FlowError(`动作不存在:${id}`, {code:'VALIDATION'});
204
+ try{
205
+ const getService=runtime.bindServices(action.plugin);
206
+ context.signal?.throwIfAborted();
207
+ const checked=validate(action.input ?? emptyObject,input,`${id} 输入`);
208
+ const plugin=definitions.get(action.plugin),bound=await pluginContext(plugin,action,context);
209
+ let result;
210
+ try{
211
+ await checkPrivilege(plugin,id.slice(plugin.id.length+1),checked,context,bound.capabilities.fs);
212
+ result=await action.run(checked,{...bound.capabilities,getService, config:action.config, resourceDir:action.root});
213
+ }finally{await bound.close();}
214
+ context.signal?.throwIfAborted();
215
+ // 通过 JSON 边界验证返回值,避免函数、循环引用等进入工作流状态。
216
+ const encoded = JSON.stringify(result);
217
+ if (encoded === undefined) throw new FlowError(`${id} 输出必须可 JSON 序列化`);
218
+ return validate(action.output, JSON.parse(encoded),`${id} 输出`,{useDefaults:false});
219
+ }catch(cause){throw pluginError(cause,{plugin:action.plugin,action:id});}
220
+ },
221
+ async service(name,options) {
222
+ assertHealthy();
223
+ if (!extensions.has(name)) throw new FlowError(`未安装提供 ${name} 的插件`);
224
+ const extension=extensions.get(name);runtime.assertActive(extension.plugin);
225
+ const plugin=definitions.get(extension.plugin),bound=await pluginContext(plugin,undefined,{});
226
+ try{
227
+ await checkPrivilege(plugin,undefined,{}, {allowUnknownPrivileges},bound.capabilities.fs);
228
+ const result=await extension.run({...services,...bound.capabilities,application:services.application},options);
229
+ if(typeof result?.close!=='function'){await bound.close();return result;}
230
+ return {...result,close:async()=>{try{await result.close();}finally{await bound.close();}}};
231
+ }catch(error){await bound.close();throw error;}
232
+ },
233
+ close:()=>runtime.close(),
234
+ };
235
+ }
@@ -0,0 +1,21 @@
1
+ import {createRequire} from 'node:module';
2
+ import {pathToFileURL} from 'node:url';
3
+ import {access} from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import {readJson} from '@mytools/fst-platform/json';
6
+ export async function loadPlugin(spec,base) {
7
+ const req=createRequire(path.join(base,'package.json'));
8
+ let entry;
9
+ if(path.isAbsolute(spec) || spec.startsWith('.')) {
10
+ const root=path.resolve(base,spec), pkg=await readJson(path.join(root,'package.json'));
11
+ entry=path.resolve(root,pkg.main??'index.js');
12
+ } else {
13
+ try {entry=req.resolve(spec);} catch {entry=createRequire(import.meta.url).resolve(spec);}
14
+ }
15
+ let root=path.dirname(entry);
16
+ while(root!==path.dirname(root)) {
17
+ try {await access(path.join(root,'package.json'));break;} catch {root=path.dirname(root);}
18
+ }
19
+ const module=await import(pathToFileURL(entry).href);
20
+ return {plugin:module.default,root};
21
+ }
@@ -0,0 +1,91 @@
1
+ // 权限声明按平台逐项覆盖;返回纯数据,避免把插件函数带入元数据。
2
+ export function resolvePrivilegeRequirement(plugin,actionName,platform=process.platform){
3
+ const action=actionName===undefined?undefined:plugin.actions?.[actionName];
4
+ const actionRequirement=action?.requirements?.privilege?.[platform];
5
+ const pluginRequirement=plugin.requirements?.privilege?.[platform];
6
+ const requirement=actionRequirement??pluginRequirement;
7
+ if(!requirement)return {declared:false,platform,source:null,level:null};
8
+ const result={declared:true,platform,source:actionRequirement?'action':'plugin',level:requirement.level};
9
+ if(requirement.reason!==undefined)result.reason=requirement.reason;
10
+ if(requirement.remediation!==undefined)result.remediation=requirement.remediation;
11
+ return result;
12
+ }
13
+
14
+ // 动作探针优先,插件探针可为动作提供默认实现。
15
+ export function resolvePrivilegePreflight(plugin,actionName){
16
+ return (actionName===undefined?undefined:plugin.actions?.[actionName]?.preflight)??plugin.preflight;
17
+ }
18
+
19
+ export function checkStaticPrivilege(requirement,options={}){
20
+ if(!requirement?.declared)return {status:'unknown',reason:'未声明权限要求'};
21
+ if(requirement.level==='user')return {status:'ok'};
22
+ if(requirement.level==='root'&&requirement.platform==='linux'){
23
+ const uid=Object.hasOwn(options,'uid')?options.uid:process.getuid?.();
24
+ if(uid===0)return {status:'ok'};
25
+ if(Number.isInteger(uid)&&uid>0)return {status:'blocked',reason:requirement.reason,remediation:requirement.remediation};
26
+ }
27
+ if(requirement.level==='administrator'&&requirement.platform==='win32'){
28
+ if(options.isAdministrator===true)return {status:'ok'};
29
+ if(options.isAdministrator===false)return {status:'blocked',reason:requirement.reason,remediation:requirement.remediation};
30
+ }
31
+ return {status:'unknown',reason:requirement.reason,remediation:requirement.remediation};
32
+ }
33
+
34
+ function normalizeResult(result){
35
+ if(!result||typeof result!=='object'||Array.isArray(result)||!['ok','blocked','unknown'].includes(result.status))return {status:'unknown',reason:'权限预检返回无效结果'};
36
+ const normalized={status:result.status};
37
+ for(const field of ['reason','remediation']){
38
+ if(result[field]===undefined)continue;
39
+ if(typeof result[field]!=='string'||!result[field].trim())return {status:'unknown',reason:'权限预检返回无效结果'};
40
+ normalized[field]=result[field];
41
+ }
42
+ return normalized;
43
+ }
44
+
45
+ function readOnlyProbe(fs,signal){
46
+ if(!fs)return undefined;
47
+ const result={};
48
+ for(const name of ['stat','readDirectory']){
49
+ if(typeof fs[name]!=='function')continue;
50
+ result[name]=(file,options={})=>{
51
+ const combined=options?.signal?AbortSignal.any([signal,options.signal]):signal;
52
+ return fs[name](file,{...options,signal:combined});
53
+ };
54
+ }
55
+ return Object.freeze(result);
56
+ }
57
+
58
+ function preflightFailure(error){
59
+ let detail;
60
+ try{detail=String(error?.message??error);}catch{detail='未知错误';}
61
+ return {status:'unknown',reason:`权限预检失败:${detail}`};
62
+ }
63
+
64
+ // 探针错误与超时都收敛为待确认;同步死循环无法由计时器强制中断。
65
+ export async function runPrivilegePreflight(preflight,input,{platform=process.platform,signal,fs,timeoutMs=5000}={}){
66
+ if(typeof preflight!=='function')return {status:'unknown',reason:'未提供权限预检'};
67
+ if(signal?.aborted)return {status:'unknown',reason:'权限预检已取消'};
68
+ const controller=new AbortController();
69
+ const duration=Number.isFinite(timeoutMs)&&timeoutMs>0?timeoutMs:5000;
70
+ let timer,abortHandler;
71
+ const interrupted=new Promise(resolve=>{
72
+ timer=setTimeout(()=>{
73
+ controller.abort(new Error('权限预检超时'));
74
+ resolve({status:'unknown',reason:'权限预检超时'});
75
+ },duration);
76
+ if(signal){
77
+ abortHandler=()=>{
78
+ controller.abort(signal.reason);
79
+ resolve({status:'unknown',reason:'权限预检已取消'});
80
+ };
81
+ signal.addEventListener('abort',abortHandler,{once:true});
82
+ if(signal.aborted)abortHandler();
83
+ }
84
+ });
85
+ const work=Promise.resolve().then(()=>preflight(input,{platform,signal:controller.signal,fs:readOnlyProbe(fs,controller.signal)})).then(normalizeResult).catch(preflightFailure);
86
+ try{return await Promise.race([work,interrupted]);}
87
+ finally{
88
+ clearTimeout(timer);
89
+ if(signal&&abortHandler)signal.removeEventListener('abort',abortHandler);
90
+ }
91
+ }
@@ -32,13 +32,15 @@ export function validateRuntimes(definitions,ctx){
32
32
  }
33
33
 
34
34
  export function createRuntime(ctx,definitions,{enabled=true}={}){
35
- const {providers,ordered}=validateRuntimes(definitions,ctx);
35
+ // 动态更新时 ctx 已含运行服务;声明检查始终以 Cordis 原生名称为准。
36
+ const reservedContext=new ctx.constructor();
37
+ let graph=validateRuntimes(definitions,reservedContext);
36
38
  const forks=new Map(),scopes=new Map(),errors=[],pending=new Set();let closing;
37
- const record=(id,error)=>errors.push({plugin:id,message:error?.message??String(error)});
39
+ const record=(id,error,phase='activation')=>errors.push({plugin:id,phase,message:error?.message??String(error)});
38
40
  const track=(id,callback)=>{
39
41
  // 同步撤销注册,只有异步尾部进入等待集合,避免服务重激活时旧清理删掉新注册。
40
- let result;try{result=callback();}catch(error){record(id,error);return;}
41
- const task=Promise.resolve(result).catch(e=>record(id,e)).finally(()=>pending.delete(task));
42
+ let result;try{result=callback();}catch(error){record(id,error,'dispose');return;}
43
+ const task=Promise.resolve(result).catch(e=>record(id,e,'dispose')).finally(()=>pending.delete(task));
42
44
  pending.add(task);return task;
43
45
  };
44
46
  // 3.18.1 的 reset 会调用 dispose,但不等待 Promise;此适配只补齐等待和错误传播。
@@ -52,7 +54,7 @@ export function createRuntime(ctx,definitions,{enabled=true}={}){
52
54
  const drain=async()=>{while(pending.size)await Promise.all([...pending]);};
53
55
  const failure=()=>new FlowError(`插件生命周期失败:${errors.map(e=>`${e.plugin}: ${e.message}`).join(';')}`,{code:'PLUGIN_RUNTIME',details:{errors:structuredClone(errors)}});
54
56
  const runtime={
55
- hasRuntime:[...definitions.values()].some(p=>p.runtime),
57
+ get hasRuntime(){return [...definitions.values()].some(p=>p.runtime);},
56
58
  register(plugin,settings,register){
57
59
  if(!enabled&&plugin.runtime)return;
58
60
  const fork=ctx.plugin({name:plugin.id,inject:plugin.runtime?.inject??[],apply(scope){
@@ -70,7 +72,7 @@ export function createRuntime(ctx,definitions,{enabled=true}={}){
70
72
  // 初始化可能仍在取得资源;先等其结束,再执行本次激活登记的清理。
71
73
  while(activation.tasks.size)await Promise.allSettled([...activation.tasks]);
72
74
  for(const dispose of activation.disposers.splice(0)){
73
- try{await dispose();}catch(error){record(plugin.id,error);}
75
+ try{await dispose();}catch(error){record(plugin.id,error,'dispose');}
74
76
  }
75
77
  })();
76
78
  });
@@ -95,10 +97,27 @@ export function createRuntime(ctx,definitions,{enabled=true}={}){
95
97
  forks.set(plugin.id,fork);
96
98
  },
97
99
  async start(){
100
+ await ctx.start();await runtime.ready();
101
+ },
102
+ reindex(){graph=validateRuntimes(definitions,reservedContext);},
103
+ clearActivationErrors(){
104
+ // 启动失败可以在清理成功后恢复;清理失败必须保留,不能假定资源已释放。
105
+ for(let index=errors.length-1;index>=0;index--)if(errors[index].phase!=='dispose')errors.splice(index,1);
106
+ },
107
+ async remove(ids){
108
+ // 先撤销消费者再撤销提供者;旧作用域清理结束后才能注册新作用域。
109
+ const selected=new Set(ids);
110
+ for(const id of [...graph.ordered].reverse())if(selected.has(id)){
111
+ forks.get(id)?.dispose();forks.delete(id);await drain();
112
+ }
113
+ if(errors.length)throw failure();
114
+ },
115
+ async ready(){
116
+ // 动态注册后的 ready 监听尚未消费;Cordis start 会取出新监听并等待任务。
98
117
  await ctx.start();await drain();
99
118
  if(errors.length)throw failure();
100
119
  if(!enabled)return;
101
- for(const [name,id] of providers)if(ctx.get(name)==null)throw new FlowError(`${id} 未提供声明的服务:${name}`,{code:'PLUGIN_RUNTIME'});
120
+ for(const [name,id] of graph.providers)if(ctx.get(name)==null)throw new FlowError(`${id} 未提供声明的服务:${name}`,{code:'PLUGIN_RUNTIME'});
102
121
  for(const p of runtime.status())if(p.status!=='ready')throw new FlowError(`${p.id} 服务尚未就绪:${p.missing.join(', ')}`,{code:'PLUGIN_RUNTIME'});
103
122
  },
104
123
  status(){return [...definitions.values()].map(p=>{
@@ -127,7 +146,7 @@ export function createRuntime(ctx,definitions,{enabled=true}={}){
127
146
  close(){
128
147
  closing??=Promise.resolve().then(async()=>{
129
148
  // 消费者先关闭,等待其使用完服务;注册撤销和依赖停用由 Cordis 执行。
130
- for(const id of [...ordered].reverse()){forks.get(id)?.dispose();await drain();}
149
+ for(const id of [...graph.ordered].reverse()){forks.get(id)?.dispose();await drain();}
131
150
  await ctx.stop();await drain();if(errors.length)throw failure();
132
151
  });return closing;
133
152
  },
@@ -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,28 @@
1
+ # 工作流模块
2
+
3
+ 包名:`@mytools/fst-workflow`。内部 workspace 包,随 `@yanglaofish/fishtools` 统一交付。
4
+
5
+ ## 职责与边界
6
+
7
+ 步骤执行、控制流和条件语义。工作流文件和闭包解析由 workspace 负责,运行管理由 core 负责。
8
+
9
+ 内部依赖:`@mytools/sdk`。
10
+ ## 公开入口
11
+
12
+ | 导入路径 | 实现位置 |
13
+ | --- | --- |
14
+ | `@mytools/fst-workflow` | `./src/engine.js` |
15
+ | `@mytools/fst-workflow/control` | `./src/control.js` |
16
+ | `@mytools/fst-workflow/package.json` | `./package.json` |
17
+
18
+ 仅依赖 exports 声明的入口,禁止消费者跨包读取 src/bin 私有文件。入口路径由本模块维护,修改协议时同步消费方回归。
19
+
20
+ ## 维护与验证
21
+
22
+ 本模块负责上述职责的实现、文档与回归;跨模块接口调整需要同时检查生产者与消费者。具体维护人员尚未指定。
23
+
24
+ 在仓库根目录执行:
25
+
26
+ ```sh
27
+ node --test tests/modules.test.js tests/workflows.test.js tests/control-flow.test.js
28
+ ```
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@mytools/fst-workflow",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "src/engine.js",
7
+ "exports": {
8
+ ".": "./src/engine.js",
9
+ "./control": "./src/control.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "src",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "license": "MIT",
18
+ "engines": {
19
+ "node": ">=24.15.0"
20
+ },
21
+ "dependencies": {
22
+ "@mytools/sdk": "0.1.0"
23
+ }
24
+ }