@yanglaofish/fishtools 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) 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/examples/script-plugin/LICENSE +21 -0
  6. package/manual/docs/examples/script-plugin/README.md +64 -0
  7. package/manual/docs/examples/script-plugin/index.js +39 -0
  8. package/manual/docs/examples/script-plugin/package.json +9 -0
  9. package/manual/docs/examples/script-plugin/scripts/benchmark.py +23 -0
  10. package/manual/docs/kernel-generation.md +97 -0
  11. package/manual/docs/modules.md +57 -0
  12. package/manual/docs/plugin-development.md +163 -12
  13. package/manual/docs/releases.md +23 -0
  14. package/manual/docs/research/2026-10-11-plugin-kernel-capabilities.md +6 -6
  15. package/manual/docs/roadmap.md +16 -3
  16. package/manual/docs/superpowers/plans/2026-10-09-workflow-mvp.md +5 -5
  17. package/manual/docs/superpowers/plans/2026-10-11-public-plugin-fixes.md +2 -2
  18. package/manual/docs/superpowers/plans/2026-10-12-kernel-generation.md +22 -0
  19. package/manual/docs/superpowers/plans/2026-10-12-modular-kernel.md +40 -0
  20. package/manual/docs/superpowers/plans/2026-10-12-plugin-developer-experience.md +50 -0
  21. package/manual/docs/superpowers/reviews/2026-10-12-kernel-generation-review.md +27 -0
  22. package/manual/docs/superpowers/reviews/2026-10-12-modular-kernel-review.md +32 -0
  23. package/manual/docs/superpowers/reviews/2026-10-12-plugin-release-review.md +20 -0
  24. package/manual/docs/superpowers/reviews/2026-10-13-kernel-runtime-release-review.md +19 -0
  25. package/manual/docs/superpowers/specs/2026-10-12-action-network-policy-design.md +52 -0
  26. package/manual/docs/superpowers/specs/2026-10-12-kernel-generation-design.md +38 -0
  27. package/manual/docs/superpowers/specs/2026-10-12-modular-kernel-design.md +70 -0
  28. package/manual/docs/superpowers/specs/2026-10-12-plugin-developer-experience.md +22 -0
  29. package/manual/docs/superpowers/specs/2026-10-12-plugin-readiness-design.md +21 -0
  30. package/manual/docs/superpowers/specs/2026-10-12-runtime-maintenance-granularity-design.md +35 -0
  31. package/manual/docs/superpowers/specs/2026-10-12-workflow-variation-design.md +19 -0
  32. package/manual/docs/verification.md +58 -1
  33. package/manual/packages/core/README.md +44 -0
  34. package/manual/packages/core/package.json +38 -0
  35. package/manual/packages/core/src/application.js +268 -87
  36. package/manual/packages/core/src/bundle.js +2 -0
  37. package/manual/packages/core/src/development-watch-worker.js +69 -0
  38. package/manual/packages/core/src/development.js +146 -0
  39. package/manual/packages/core/src/environments.js +2 -0
  40. package/manual/packages/core/src/export-snapshot.js +42 -0
  41. package/manual/packages/core/src/plugin-update.js +108 -0
  42. package/manual/packages/core/src/process.js +2 -0
  43. package/manual/packages/core/src/resources.js +31 -0
  44. package/manual/packages/core/src/version.js +2 -0
  45. package/manual/packages/fst-cli/LICENSE +21 -0
  46. package/manual/packages/fst-cli/README.md +54 -0
  47. package/manual/packages/fst-cli/bin/fst.js +124 -0
  48. package/manual/packages/fst-cli/package.json +49 -0
  49. package/manual/packages/fst-cli/src/development.js +53 -0
  50. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/model.js +1 -1
  51. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/navigation.js +1 -1
  52. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/presentation.js +9 -1
  53. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-feed.js +31 -5
  54. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-viewer.js +9 -7
  55. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/schema.js +1 -1
  56. package/manual/packages/{core → fst-cli}/src/terminal/session.js +137 -9
  57. package/manual/packages/{core → fst-cli}/src/terminal/view.js +20 -3
  58. package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/workflow-menu.js +17 -2
  59. package/manual/packages/fst-devtools/LICENSE +21 -0
  60. package/manual/packages/fst-devtools/README.md +28 -0
  61. package/manual/packages/fst-devtools/package.json +29 -0
  62. package/{node_modules/@mytools/core → manual/packages/fst-devtools}/src/documentation.js +3 -1
  63. package/manual/packages/fst-devtools/src/plugin-quality.js +136 -0
  64. package/manual/packages/fst-devtools/src/plugin-tools.js +152 -0
  65. package/manual/packages/fst-logs/LICENSE +21 -0
  66. package/manual/packages/fst-logs/README.md +56 -0
  67. package/manual/packages/fst-logs/package.json +24 -0
  68. package/manual/packages/fst-logs/src/log-bounds.js +91 -0
  69. package/manual/packages/fst-logs/src/run-logs.js +239 -0
  70. package/manual/packages/fst-platform/LICENSE +21 -0
  71. package/manual/packages/fst-platform/README.md +67 -0
  72. package/manual/packages/fst-platform/package.json +33 -0
  73. package/manual/packages/fst-platform/src/bundle.js +17 -0
  74. package/manual/packages/fst-platform/src/environments.js +67 -0
  75. package/manual/packages/fst-platform/src/file-probe-engine.js +183 -0
  76. package/manual/packages/fst-platform/src/file-probe-worker.js +43 -0
  77. package/manual/packages/fst-platform/src/file-probe.js +6 -0
  78. package/manual/packages/fst-platform/src/json.js +5 -0
  79. package/manual/packages/fst-platform/src/network.js +199 -0
  80. package/manual/packages/fst-platform/src/npm.js +11 -0
  81. package/manual/packages/fst-platform/src/process.js +110 -0
  82. package/manual/packages/fst-plugin/LICENSE +21 -0
  83. package/manual/packages/fst-plugin/README.md +29 -0
  84. package/manual/packages/fst-plugin/package.json +29 -0
  85. package/manual/packages/fst-plugin/src/contracts.js +86 -0
  86. package/manual/packages/fst-plugin/src/host.js +232 -0
  87. package/manual/packages/fst-plugin/src/loader.js +21 -0
  88. package/manual/packages/fst-plugin/src/privileges.js +91 -0
  89. package/manual/packages/{core → fst-plugin}/src/runtime.js +20 -4
  90. package/manual/packages/fst-workflow/LICENSE +21 -0
  91. package/manual/packages/fst-workflow/README.md +28 -0
  92. package/manual/packages/fst-workflow/package.json +24 -0
  93. package/{node_modules/@mytools/core → manual/packages/fst-workflow}/src/control.js +3 -2
  94. package/manual/packages/{core → fst-workflow}/src/engine.js +2 -2
  95. package/manual/packages/fst-workspace/LICENSE +21 -0
  96. package/manual/packages/fst-workspace/README.md +32 -0
  97. package/manual/packages/fst-workspace/package.json +34 -0
  98. package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/bundle-workspace.js +1 -1
  99. package/manual/packages/fst-workspace/src/index.js +3 -0
  100. package/manual/packages/fst-workspace/src/plugin-installations.js +84 -0
  101. package/manual/packages/fst-workspace/src/plugins.js +35 -0
  102. package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/workflows.js +41 -21
  103. package/manual/packages/pack/LICENSE +21 -0
  104. package/manual/packages/pack/README.md +31 -0
  105. package/manual/packages/pack/archive.js +15 -0
  106. package/manual/packages/pack/dependencies.js +105 -0
  107. package/manual/packages/pack/environments.js +50 -0
  108. package/manual/packages/pack/index.js +103 -0
  109. package/manual/packages/pack/package.json +29 -0
  110. package/manual/packages/pack/runtime.js +61 -0
  111. package/manual/packages/sdk/README.md +31 -0
  112. package/manual/packages/sdk/conditions.js +1 -0
  113. package/manual/packages/sdk/package.json +23 -1
  114. package/manual/packages/sdk/schema-path.js +31 -0
  115. package/{node_modules/@mytools/core/src → manual/packages/sdk}/validation.js +22 -9
  116. package/manual/plugins/web/README.md +29 -0
  117. package/manual/tests/application.test.js +6 -4
  118. package/node_modules/@mytools/core/README.md +44 -0
  119. package/node_modules/@mytools/core/package.json +17 -15
  120. package/node_modules/@mytools/core/src/application.js +268 -87
  121. package/node_modules/@mytools/core/src/bundle.js +2 -17
  122. package/node_modules/@mytools/core/src/development-watch-worker.js +69 -0
  123. package/node_modules/@mytools/core/src/development.js +146 -0
  124. package/node_modules/@mytools/core/src/environments.js +2 -29
  125. package/node_modules/@mytools/core/src/export-snapshot.js +42 -0
  126. package/node_modules/@mytools/core/src/plugin-update.js +108 -0
  127. package/node_modules/@mytools/core/src/process.js +2 -71
  128. package/node_modules/@mytools/core/src/version.js +1 -1
  129. package/node_modules/@mytools/fst-cli/LICENSE +21 -0
  130. package/node_modules/@mytools/fst-cli/README.md +54 -0
  131. package/node_modules/@mytools/fst-cli/bin/fst.js +124 -0
  132. package/node_modules/@mytools/fst-cli/package.json +49 -0
  133. package/node_modules/@mytools/fst-cli/src/cli-run.js +12 -0
  134. package/node_modules/@mytools/fst-cli/src/development.js +53 -0
  135. package/node_modules/@mytools/fst-cli/src/terminal/conditions.js +32 -0
  136. package/node_modules/@mytools/fst-cli/src/terminal/log-viewer.js +45 -0
  137. package/node_modules/@mytools/fst-cli/src/terminal/model.js +104 -0
  138. package/node_modules/@mytools/fst-cli/src/terminal/navigation.js +17 -0
  139. package/node_modules/@mytools/fst-cli/src/terminal/presentation.js +50 -0
  140. package/{manual/packages/core → node_modules/@mytools/fst-cli}/src/terminal/run-feed.js +31 -5
  141. package/node_modules/@mytools/fst-cli/src/terminal/run-presentation.js +60 -0
  142. package/node_modules/@mytools/fst-cli/src/terminal/run-viewer.js +106 -0
  143. package/node_modules/@mytools/fst-cli/src/terminal/schema.js +52 -0
  144. package/node_modules/@mytools/{core → fst-cli}/src/terminal/session.js +137 -9
  145. package/node_modules/@mytools/fst-cli/src/terminal/theme.js +29 -0
  146. package/node_modules/@mytools/{core → fst-cli}/src/terminal/view.js +20 -3
  147. package/node_modules/@mytools/fst-cli/src/terminal/workflow-menu.js +65 -0
  148. package/node_modules/@mytools/fst-devtools/LICENSE +21 -0
  149. package/node_modules/@mytools/fst-devtools/README.md +28 -0
  150. package/node_modules/@mytools/fst-devtools/package.json +29 -0
  151. package/node_modules/@mytools/fst-devtools/src/documentation.js +67 -0
  152. package/node_modules/@mytools/fst-devtools/src/plugin-quality.js +136 -0
  153. package/node_modules/@mytools/fst-devtools/src/plugin-tools.js +152 -0
  154. package/node_modules/@mytools/fst-logs/LICENSE +21 -0
  155. package/node_modules/@mytools/fst-logs/README.md +56 -0
  156. package/node_modules/@mytools/fst-logs/package.json +24 -0
  157. package/node_modules/@mytools/fst-logs/src/log-bounds.js +91 -0
  158. package/node_modules/@mytools/fst-logs/src/markdown-log.js +55 -0
  159. package/node_modules/@mytools/fst-logs/src/run-logs.js +239 -0
  160. package/node_modules/@mytools/fst-platform/LICENSE +21 -0
  161. package/node_modules/@mytools/fst-platform/README.md +67 -0
  162. package/node_modules/@mytools/fst-platform/package.json +33 -0
  163. package/node_modules/@mytools/fst-platform/src/bundle.js +17 -0
  164. package/node_modules/@mytools/fst-platform/src/environments.js +67 -0
  165. package/node_modules/@mytools/fst-platform/src/file-probe-engine.js +183 -0
  166. package/node_modules/@mytools/fst-platform/src/file-probe-worker.js +43 -0
  167. package/node_modules/@mytools/fst-platform/src/file-probe.js +6 -0
  168. package/node_modules/@mytools/fst-platform/src/json.js +5 -0
  169. package/node_modules/@mytools/fst-platform/src/network.js +199 -0
  170. package/node_modules/@mytools/fst-platform/src/npm.js +11 -0
  171. package/node_modules/@mytools/fst-platform/src/process.js +110 -0
  172. package/node_modules/@mytools/fst-plugin/LICENSE +21 -0
  173. package/node_modules/@mytools/fst-plugin/README.md +29 -0
  174. package/node_modules/@mytools/fst-plugin/package.json +29 -0
  175. package/node_modules/@mytools/fst-plugin/src/contracts.js +86 -0
  176. package/node_modules/@mytools/fst-plugin/src/host.js +232 -0
  177. package/node_modules/@mytools/fst-plugin/src/loader.js +21 -0
  178. package/node_modules/@mytools/fst-plugin/src/privileges.js +91 -0
  179. package/node_modules/@mytools/{core → fst-plugin}/src/runtime.js +20 -4
  180. package/node_modules/@mytools/fst-workflow/LICENSE +21 -0
  181. package/node_modules/@mytools/fst-workflow/README.md +28 -0
  182. package/node_modules/@mytools/fst-workflow/package.json +24 -0
  183. package/node_modules/@mytools/fst-workflow/src/control.js +64 -0
  184. package/node_modules/@mytools/{core → fst-workflow}/src/engine.js +2 -2
  185. package/node_modules/@mytools/fst-workspace/LICENSE +21 -0
  186. package/node_modules/@mytools/fst-workspace/README.md +32 -0
  187. package/node_modules/@mytools/fst-workspace/package.json +34 -0
  188. package/node_modules/@mytools/fst-workspace/src/bundle-workspace.js +39 -0
  189. package/node_modules/@mytools/fst-workspace/src/index.js +3 -0
  190. package/node_modules/@mytools/fst-workspace/src/plugin-installations.js +84 -0
  191. package/node_modules/@mytools/fst-workspace/src/plugins.js +35 -0
  192. package/node_modules/@mytools/fst-workspace/src/project.js +29 -0
  193. package/node_modules/@mytools/fst-workspace/src/workflows.js +150 -0
  194. package/node_modules/@mytools/fst-workspace/templates/control-demo.yaml +47 -0
  195. package/node_modules/@mytools/fst-workspace/templates/hello.yaml +35 -0
  196. package/node_modules/@mytools/fst-workspace/templates/python-check.yaml +26 -0
  197. package/node_modules/@mytools/fst-workspace/templates/system-check.yaml +11 -0
  198. package/node_modules/@mytools/pack/README.md +31 -0
  199. package/node_modules/@mytools/pack/dependencies.js +15 -3
  200. package/node_modules/@mytools/pack/environments.js +1 -1
  201. package/node_modules/@mytools/pack/index.js +33 -25
  202. package/node_modules/@mytools/pack/package.json +29 -1
  203. package/node_modules/@mytools/sdk/README.md +31 -0
  204. package/node_modules/@mytools/sdk/conditions.js +1 -0
  205. package/node_modules/@mytools/sdk/diagnostics.js +27 -0
  206. package/node_modules/@mytools/sdk/package.json +23 -1
  207. package/node_modules/@mytools/sdk/schema-path.js +31 -0
  208. package/node_modules/@mytools/sdk/validation.js +78 -0
  209. package/node_modules/@mytools/web/README.md +29 -0
  210. package/node_modules/@mytools/web/package.json +7 -0
  211. package/node_modules/@mytools/web/public/THIRD_PARTY_NOTICES.txt +1275 -1076
  212. package/node_modules/@mytools/web/public/assets/index-BaVzHycP.js +70 -0
  213. package/node_modules/@mytools/web/public/assets/index-DA8Ae92t.css +1 -0
  214. package/node_modules/@mytools/web/public/index.html +2 -2
  215. package/node_modules/@mytools/web/server.js +24 -17
  216. package/node_modules/@mytools/web/src/model.js +84 -0
  217. package/node_modules/acorn/CHANGELOG.md +1012 -0
  218. package/node_modules/acorn/LICENSE +21 -0
  219. package/node_modules/acorn/README.md +309 -0
  220. package/node_modules/acorn/bin/acorn +4 -0
  221. package/node_modules/acorn/dist/acorn.d.mts +897 -0
  222. package/node_modules/acorn/dist/acorn.d.ts +897 -0
  223. package/node_modules/acorn/dist/acorn.js +6370 -0
  224. package/node_modules/acorn/dist/acorn.mjs +6341 -0
  225. package/node_modules/acorn/dist/bin.js +90 -0
  226. package/node_modules/acorn/package.json +50 -0
  227. package/package.json +117 -101
  228. package/manual/packages/core/src/host.js +0 -124
  229. package/node_modules/@mytools/core/bin/fst.js +0 -70
  230. package/node_modules/@mytools/core/src/contracts.js +0 -41
  231. package/node_modules/@mytools/core/src/host.js +0 -124
  232. package/node_modules/@mytools/core/src/plugins.js +0 -55
  233. package/node_modules/@mytools/core/src/run-logs.js +0 -63
  234. package/node_modules/@mytools/web/public/assets/index-DT2B4tMV.js +0 -70
  235. package/node_modules/@mytools/web/public/assets/index-vuZvrUTT.css +0 -1
  236. /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/cli-run.js +0 -0
  237. /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/conditions.js +0 -0
  238. /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/log-viewer.js +0 -0
  239. /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-presentation.js +0 -0
  240. /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/theme.js +0 -0
  241. /package/{node_modules/@mytools/core → manual/packages/fst-logs}/src/markdown-log.js +0 -0
  242. /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/project.js +0 -0
  243. /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/control-demo.yaml +0 -0
  244. /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/hello.yaml +0 -0
  245. /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/python-check.yaml +0 -0
  246. /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/system-check.yaml +0 -0
  247. /package/{node_modules/@mytools/core/src → manual/packages/sdk}/diagnostics.js +0 -0
@@ -1,75 +1,93 @@
1
- import { mkdir, appendFile, writeFile, rename, readFile } from 'node:fs/promises';
1
+ import { mkdir, readFile, writeFile, rename } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import { EventEmitter } from 'node:events';
5
- import { createHost } from './host.js';
6
- import { WorkflowStore } from './workflows.js';
7
- import { runWorkflow } from './engine.js';
8
- import { runProcess } from './process.js';
9
- import { readJson, loadPlugin, installGeneration } from './plugins.js';
5
+ import {isDeepStrictEqual} from 'node:util';
6
+ import { createHost } from '@mytools/fst-plugin';
7
+ import { WorkflowStore } from '@mytools/fst-workspace/workflows';
8
+ import { runWorkflow } from '@mytools/fst-workflow';
9
+ import { runProcess } from '@mytools/fst-platform';
10
+ import {createFileProbe} from '@mytools/fst-platform/fs';
11
+ import {readJson} from '@mytools/fst-platform/json';
12
+ import {loadPlugin} from '@mytools/fst-plugin/loader';
13
+ import {installGeneration} from '@mytools/fst-workspace/plugins';
10
14
  import { FlowError } from '@mytools/sdk';
11
- import {prepareBundleWorkspace} from './bundle-workspace.js';
12
- import {validate} from './validation.js';
15
+ import {prepareBundleWorkspace} from '@mytools/fst-workspace/bundles';
16
+ import {validate} from '@mytools/sdk/schema';
13
17
  import {createResources} from './resources.js';
14
- import {errorRecord} from './diagnostics.js';
15
- import {markdownHeader,markdownEvent,markdownResult} from './markdown-log.js';
16
- import {createRunLogs,validateKeepRuns} from './run-logs.js';
18
+ import {errorRecord} from '@mytools/sdk/diagnostics';
19
+ import {createExportSnapshot} from './export-snapshot.js';
20
+ import {createRunLogs,validateKeepRuns} from '@mytools/fst-logs';
21
+ import {openPluginState,withPluginMutation,assertPluginState,savePluginState,leaseInstallation} from '@mytools/fst-workspace/installations';
22
+ import {planPluginUpdate,changedPluginSources} from './plugin-update.js';
23
+ import {validateNetworkPolicy,inspectNetworkPolicy} from '@mytools/fst-platform/network';
24
+ import {resolvePrivilegeRequirement,checkStaticPrivilege} from '@mytools/fst-plugin/privileges';
17
25
 
18
- export async function createApplication({projectDir=process.cwd(),workspace,outputDir,plugins,config={},maintenance=false}={}) {
26
+ export async function createApplication({projectDir=process.cwd(),workspace,outputDir,plugins,config={},maintenance=false,omitInstalledPlugin,allowUnknownPrivileges=false}={}) {
19
27
  projectDir=await prepareBundleWorkspace(path.resolve(projectDir),workspace);
20
28
  const project=await readJson(path.join(projectDir,'flow.project.json'),{plugins:[]});
21
29
  const output=path.resolve(outputDir??process.env.FLOW_OUTPUT_DIR??(project.readOnly?path.join(process.cwd(),'flow-results'):path.join(projectDir,'.flow','runs')));
22
30
  const store=new WorkflowStore(path.join(projectDir,'workflows'));
23
- const emitter=new EventEmitter(), runs=new Map();
31
+ const emitter=new EventEmitter(), runs=new Map(),journals=new WeakMap();
24
32
  const resources=createResources(),operations=new Set();let closing,launching=0;
25
- const runLogs=createRunLogs(output,{projectDir,allowLegacy:output===path.join(projectDir,'.flow','runs')}),writingRuns=new Set();
33
+ const runLogs=createRunLogs(output,{...project.logs,projectDir,allowLegacy:output===path.join(projectDir,'.flow','runs')}),writingRuns=new Set();
34
+ const maxCachedRuns=project.logs?.maxCachedRuns??50;
35
+ if(!Number.isSafeInteger(maxCachedRuns)||maxCachedRuns<1||maxCachedRuns>1000)throw new FlowError('maxCachedRuns 必须为 1 到 1000 的整数');
36
+ const fileProbe=createFileProbe(project.fileAccess);
26
37
  validateKeepRuns(project.logs?.keepRuns??null);
27
- let host,state=await readJson(path.join(projectDir,'.flow','plugins.json'),{}),busy=false;
38
+ const opened=await openPluginState(projectDir,{readOnly:project.readOnly});
39
+ let host,state=opened.state,busy=false;
40
+ const installationLeases=new Map();
41
+ if(state.root)installationLeases.set(path.resolve(state.root),opened.release);
28
42
  const app={
29
43
  projectDir,project,outputDir:output,store,
30
44
  get host(){return host;},
31
45
  get maintenance(){return maintenance;},
46
+ developmentStatus:()=>({running:launching+writingRuns.size+operations.size,busy,closing:!!closing}),
32
47
  catalog:()=>host.catalog(),
33
- listPlugins:()=>[...host.definitions.values()].map(p=>({id:p.id,version:p.version,title:p.title,description:p.description,platforms:p.platforms,architectures:p.architectures,environments:p.environments,config:p.config,runtime:host.runtimeStatus().find(r=>r.id===p.id),removable:!!state.dependencies?.[p.id]})),
48
+ listPlugins:()=>[...host.definitions.values()].map(p=>({id:p.id,version:p.version,title:p.title,description:p.description,platforms:p.platforms,architectures:p.architectures,environments:p.environments,config:p.config,requirements:p.requirements,privileges:pluginPrivileges(p),runtime:{...host.runtimeStatus().find(r=>r.id===p.id),...(p.runtime?{updatePolicy:p.runtime.updatePolicy??'maintenance'}:{})},removable:!!(state.dependencies?.[p.id]||state.links?.[p.id]),development:!!state.links?.[p.id],warnings:host.warnings.filter(item=>item.plugin===p.id).map(item=>item.message)})),
34
49
  listCommands:()=>host.commands(),
35
50
  listWorkflows:()=>store.list(),
36
- async saveWorkflow(w){if(project.readOnly)throw new FlowError('此分发包仅用于运行;请在开发项目中编辑');return store.save(w);},
37
- async removeWorkflow(id){if(project.readOnly)throw new FlowError('只读项目不能删除工作流');return store.remove(id);},
38
- checkWorkflow:id=>store.resolve([id],host),
51
+ async saveWorkflow(w){return mutateWorkflow(()=>store.save(w));},
52
+ async duplicateWorkflow(definition,options){return mutateWorkflow(()=>store.duplicate(definition,options));},
53
+ async removeWorkflow(id){return mutateWorkflow(()=>store.remove(id));},
54
+ async checkWorkflow(id){
55
+ host.assertHealthy();
56
+ const closure=await store.resolve([id],host);
57
+ return {...closure,privileges:workflowPrivileges(closure,id)};
58
+ },
39
59
  subscribe(fn){emitter.on('event',fn);return ()=>emitter.off('event',fn);},
40
- async startRun(id,input={}) {
60
+ async startRun(id,input={},options={}) {
41
61
  if(busy) throw new FlowError('插件更新中,请稍后运行');
42
- const closure=await store.resolve([id],host);
62
+ const closure=await app.checkWorkflow(id);
63
+ const blocked=closure.privileges.filter(item=>item.mandatory&&item.static.status==='blocked');
43
64
  const snapshotStore={resolve:async()=>structuredClone(closure),childId:ref=>store.childId(ref)};
44
- return app.executeTracked(id,(activeHost,context,event)=>runWorkflow({store:snapshotStore,host:activeHost,id,input,signal:context.signal,onEvent:event,runId:context.runId,context}));
65
+ const networkPolicy=structuredClone(project.network??{});
66
+ return app.executeTracked(id,(activeHost,context,event)=>{
67
+ if(blocked.length)throw new FlowError(`工作流缺少运行权限:${blocked.map(item=>item.action).join('、')}`,{code:'INSUFFICIENT_PRIVILEGE',details:{actions:blocked}});
68
+ return runWorkflow({store:snapshotStore,host:activeHost,id,input,signal:context.signal,onEvent:event,runId:context.runId,context:{...context,allowUnknownPrivileges:!!options.allowUnknownPrivileges,networkPolicy}});
69
+ });
45
70
  },
46
71
  async startCommand(name,input={},context={}){
47
72
  if(busy)throw new FlowError('插件更新中,请稍后调用');
48
- return app.executeTracked(`/${name}`,(activeHost,runContext)=>activeHost.invokeCommand(name,input,{...context,...runContext}));
73
+ const networkPolicy=structuredClone(project.network??{});
74
+ return app.executeTracked(`/${name}`,(activeHost,runContext)=>activeHost.invokeCommand(name,input,{...context,...runContext,networkPolicy}));
49
75
  },
50
76
  async executeTracked(id,execute){
51
- const runId=randomUUID(),dir=path.join(output,runId),controller=new AbortController();
52
- await mkdir(dir,{recursive:true});
53
- const run={id:runId,workflowId:id,logOwner:runLogs.owner,status:'running',startedAt:new Date().toISOString(),events:[],controller};
77
+ const runId=randomUUID(),controller=new AbortController();
78
+ const run={id:runId,workflowId:id,logOwner:runLogs.owner,status:'running',startedAt:new Date().toISOString(),controller};
79
+ const journal=await runLogs.start({id:runId,workflowId:id,status:run.status,startedAt:run.startedAt},{onEvent:event=>emitter.emit('event',event)});
80
+ journals.set(run,journal);
81
+ // 兼容运行句柄的事件读取;外部修改不影响日志模块的缓存。
82
+ Object.defineProperty(run,'events',{get:()=>journal.snapshot().events});
54
83
  runs.set(runId,run);
55
84
  writingRuns.add(runId);
56
- const markdownFile=path.join(dir,'run.md');
57
- let writes=writeFile(markdownFile,markdownHeader(run));
58
- const event=e=>{
59
- const full={time:new Date().toISOString(),runId,...e};run.events.push(full);
60
- emitter.emit('event',full);
61
- const {output:ignored,...record}=full;
62
- writes=writes.then(async()=>{
63
- await appendFile(path.join(dir,'events.jsonl'),JSON.stringify(record)+'\n');
64
- await appendFile(markdownFile,markdownEvent(full));
65
- });
66
- return writes;
67
- };
85
+ const event=e=>journal.append(e);
68
86
  const activeHost=host;
69
87
  run.done=(async()=>{
70
88
  try {
71
89
  await event({type:'run.started'});
72
- run.output=await execute(activeHost,{outputDir:dir,signal:controller.signal,runId,log:(message,data,meta={})=>event({...meta,type:'log',message,data})},event);
90
+ run.output=await execute(activeHost,{outputDir:journal.directory,signal:controller.signal,runId,log:(message,data,meta={})=>event({...meta,type:'log',message,data})},event);
73
91
  run.status='succeeded';await event({type:'run.succeeded'});
74
92
  } catch(error) {
75
93
  if(error?.code==='WORKFLOW_STOPPED'){
@@ -82,23 +100,48 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
82
100
  }
83
101
  } finally {
84
102
  run.finishedAt=new Date().toISOString();
85
- await writes;
86
- await appendFile(markdownFile,markdownResult(app.getRun(runId)));
87
- await writeFile(path.join(dir,'result.json'),JSON.stringify(snapshotRun(run),null,2));
88
- writingRuns.delete(runId);
103
+ try{await journal.finish(runState(run));}finally{writingRuns.delete(runId);}
89
104
  try{await pruneLogs();}catch(error){
90
- await event({type:'log',message:`日志自动清理失败:${error.message}`});
91
- await writeFile(path.join(dir,'result.json'),JSON.stringify(snapshotRun(run),null,2));
105
+ run.warnings=[{code:'LOG_PRUNE_FAILED',message:`日志自动清理失败:${error.message}`}];
106
+ await journal.save(runState(run));
92
107
  }
93
108
  }
94
- return snapshotRun(run);
109
+ const result=snapshotRun(run);trimRuns();return result;
95
110
  })();
111
+ // Web 等调用方只保留运行 ID;立即观察拒绝,但不替换供 CLI/TUI 等待的原 Promise。
112
+ run.done.catch(error=>{
113
+ run.status='failed';
114
+ run.error=errorRecord(new FlowError(`运行日志保存失败:${error.message}`,{code:'LOG_WRITE_FAILED',details:errorRecord(error),cause:run.error}));
115
+ trimRuns();
116
+ });
96
117
  return run;
97
118
  },
98
119
  getRun(id){const r=runs.get(id);return r?snapshotRun(r):null;},
99
- async readRun(id){if(runs.has(id))return app.getRun(id);if(!/^[a-f0-9-]{36}$/.test(id))throw new FlowError('运行 ID 无效');return JSON.parse(await readFile(path.join(output,id,'result.json'),'utf8'));},
120
+ async readRun(id){if(runs.has(id))return app.getRun(id);return runLogs.read(id);},
121
+ readLogPage:(id,options)=>runLogs.page(id,options),
100
122
  listRuns:()=>[...runs.keys()].map(id=>{const {events,...r}=app.getRun(id);return r;}),
101
123
  logSettings:()=>({keepRuns:project.logs?.keepRuns??null}),
124
+ networkPolicy:()=>structuredClone(project.network??{}),
125
+ networkReport:()=>inspectNetworkPolicy(project.network??{},{plugins:[...host.definitions.keys()],actions:[...host.actions.keys()]}),
126
+ async configureNetworkPolicy(next){
127
+ validateNetworkPolicy(next);
128
+ if(project.readOnly)throw new FlowError('只读运行包不能修改代理设置');
129
+ if(busy)throw new FlowError('项目正在更新,请稍后修改代理设置');
130
+ busy=true;let backup;
131
+ try{
132
+ await withPluginMutation(projectDir,async()=>{
133
+ const file=path.join(projectDir,'flow.project.json'),latest=await readJson(file,project);
134
+ if(latest.readOnly)throw new FlowError('只读运行包不能修改代理设置');
135
+ if(!isDeepStrictEqual(latest.network??{},project.network??{}))throw new FlowError('代理设置已被另一实例修改,请重新读取后再保存',{code:'NETWORK_CONFLICT'});
136
+ backup=await backupProjectFile(file,latest,'network');
137
+ const nextProject={...latest,network:structuredClone(next)},temp=`${file}.${randomUUID()}.tmp`;
138
+ await writeFile(temp,JSON.stringify(nextProject,null,2));await rename(temp,file);
139
+ for(const key of Object.keys(project))delete project[key];Object.assign(project,nextProject);
140
+ });
141
+ }finally{busy=false;}
142
+ return {policy:app.networkPolicy(),backup};
143
+ },
144
+ logUsage:()=>runLogs.usage(),
102
145
  async listRunHistory(){
103
146
  const records=new Map((await runLogs.list()).map(r=>[r.id,r]));
104
147
  for(const id of runs.keys())records.set(id,app.getRun(id));
@@ -106,39 +149,65 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
106
149
  },
107
150
  async readLog(id){
108
151
  const run=runs.get(id);
109
- if(run&&writingRuns.has(id))return markdownHeader(run)+run.events.map(markdownEvent).join('');
152
+ if(run&&writingRuns.has(id))return journals.get(run).markdown(runState(run));
110
153
  return runLogs.markdown(id);
111
154
  },
112
155
  async configureLogs(keepRuns){
113
156
  validateKeepRuns(keepRuns);if(project.readOnly)throw new FlowError('只读运行包不能保存日志设置');
114
157
  if(busy)throw new FlowError('项目正在更新,请稍后修改日志设置');
115
- busy=true;
158
+ busy=true;let backup,warning;
116
159
  try{
117
- const logs={...project.logs,keepRuns},file=path.join(projectDir,'flow.project.json'),temp=`${file}.${randomUUID()}.tmp`;
118
- await mkdir(projectDir,{recursive:true});await writeFile(temp,JSON.stringify({...project,logs},null,2));await rename(temp,file);project.logs=logs;
119
- await pruneLogs();
160
+ await withPluginMutation(projectDir,async()=>{
161
+ const file=path.join(projectDir,'flow.project.json'),latest=await readJson(file,project);
162
+ if(latest.readOnly)throw new FlowError('只读运行包不能保存日志设置');
163
+ backup=await backupProjectFile(file,latest,'logs');
164
+ const nextProject={...latest,logs:{...latest.logs,keepRuns}},temp=`${file}.${randomUUID()}.tmp`;
165
+ await writeFile(temp,JSON.stringify(nextProject,null,2));await rename(temp,file);
166
+ for(const key of Object.keys(project))delete project[key];Object.assign(project,nextProject);
167
+ try{await pruneLogs();}
168
+ catch(error){warning={code:'LOG_PRUNE_FAILED',message:`日志保留设置已保存,但旧日志清理失败:${error.message}`};emitter.emit('event',{type:'logs.prune.warning',...warning});}
169
+ });
120
170
  }finally{busy=false;}
171
+ return {keepRuns,backup,...(warning?{warning}:{})};
121
172
  },
122
173
  cancelRun(id){const r=runs.get(id);if(r?.status==='running')r.controller.abort();return !!r;},
123
- async installPlugin(source){return change({add:source});},
124
- async removePlugin(name){if(resources.active(name))throw new FlowError('请先停止此插件提供的 Web');if(!state.dependencies?.[name])throw new FlowError('此插件来自发行配置,不能在此移除');return change({remove:name});},
125
- async pack(options){const {packApplication}=await import('@mytools/pack');return packApplication(app,options);},
126
- async packPlan(options){const {planApplication}=await import('@mytools/pack');return planApplication(app,options);},
127
- async configurePlugin(id,value){
174
+ async installPlugin(source,options){return change({add:source},options);},
175
+ async linkPlugin(source,options){return change({link:path.resolve(projectDir,source)},options);},
176
+ async removePlugin(name,options={}){
177
+ return change({remove:name},options);
178
+ },
179
+ exportSnapshot:options=>createExportSnapshot({store,host,project},options),
180
+ async pack(options){if(project.readOnly)throw new FlowError('只读运行包不能重新打包');const {packApplication}=await import('@mytools/pack');return packApplication(await app.exportSnapshot(options),options);},
181
+ async packPlan(options){const {planApplication}=await import('@mytools/pack');return planApplication(await app.exportSnapshot(options),options);},
182
+ async configurePlugin(id,value,options={}){
128
183
  if(project.readOnly)throw new FlowError('只读项目不能保存配置');
129
- assertMutable();
130
- if(resources.active(id))throw new FlowError('请先停止此插件提供的 Web,再修改配置');
131
184
  if(busy||launching||app.listRuns().some(r=>r.status==='running'))throw new FlowError('请等待运行或安装结束');
132
185
  if(!host.definitions.has(id))throw new FlowError(`插件不存在:${id}`);
133
186
  value=validate(host.definitions.get(id).config,value,`${id} 配置`);
134
- busy=true;let nextHost;
187
+ busy=true;let backup;
135
188
  try{
136
- const nextConfig={...project.config,[id]:value};
137
- nextHost=await createHost({plugins:[...host.definitions.values()].map(p=>({plugin:p,root:p.root})),config:{...nextConfig,...config},services,environmentOverrides:project.environments,runtimeEnabled:!maintenance});
138
- const nextProject={...project,config:nextConfig},file=path.join(projectDir,'flow.project.json'),temp=`${file}.${randomUUID()}.tmp`;
139
- await writeFile(temp,JSON.stringify(nextProject,null,2));await rename(temp,file);
140
- const old=host;host=nextHost;nextHost=null;project.config=nextConfig;await old.close();
141
- }finally{try{if(nextHost)await nextHost.close();}finally{busy=false;}}
189
+ await withPluginMutation(projectDir,async()=>{
190
+ await assertPluginState(projectDir,state);
191
+ const file=path.join(projectDir,'flow.project.json'),latest=await readJson(file,project);
192
+ if(latest.readOnly)throw new FlowError('只读项目不能保存配置');
193
+ const nextConfig={...latest.config,[id]:value};
194
+ const oldConfig={...project.config,...config},effectiveNext={...nextConfig,...config};
195
+ const entries=[...host.definitions.values()].map(p=>({plugin:p,root:p.root}));
196
+ const plan=planPluginUpdate(host.definitions,host.definitions,oldConfig,effectiveNext,new Set([id]));
197
+ assertMaintenance(plan,options);
198
+ backup=await backupProjectFile(file,latest,'config');
199
+ if(plan.affected.length)await host.replace({plugins:entries,config:effectiveNext,affected:plan.affected});
200
+ const nextProject={...latest,config:nextConfig},temp=`${file}.${randomUUID()}.tmp`;
201
+ try{await writeFile(temp,JSON.stringify(nextProject,null,2));await rename(temp,file);}
202
+ catch(error){
203
+ if(plan.affected.length)try{await host.replace({plugins:entries,config:oldConfig,affected:plan.affected});}
204
+ catch(recovery){host.invalidate();throw new AggregateError([error,recovery],`配置保存失败且插件恢复失败:${error.message};${recovery.message}`);}
205
+ throw error;
206
+ }
207
+ for(const key of Object.keys(project))delete project[key];Object.assign(project,nextProject);
208
+ });
209
+ }finally{busy=false;}
210
+ return {backup};
142
211
  },
143
212
  async setMaintenance(enabled){
144
213
  if(typeof enabled!=='boolean')throw new FlowError('维护模式必须为布尔值');
@@ -149,7 +218,7 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
149
218
  try{
150
219
  const entries=[...host.definitions.values()].map(p=>({plugin:p,root:p.root}));
151
220
  if(enabled){maintenance=true;await host.close();}
152
- const next=await createHost({plugins:entries,config:{...project.config,...config},services,environmentOverrides:project.environments,runtimeEnabled:!enabled});
221
+ const next=await createHost({plugins:entries,config:{...project.config,...config},services,project:hostProject(),network:()=>project.network,environmentOverrides:project.environments,runtimeEnabled:!enabled,allowUnknownPrivileges});
153
222
  const old=host;host=next;maintenance=enabled;if(!enabled)await old.close();
154
223
  }finally{busy=false;}
155
224
  },
@@ -167,17 +236,24 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
167
236
  const errors=[];
168
237
  try{await resources.close();}catch(error){errors.push(error);}
169
238
  try{await host.close();}catch(error){errors.push(error);}
239
+ try{const report=await fileProbe.close();if(report.incomplete)errors.push(new FlowError('文件探测进程尚未全部回收',{code:'FS_CLEANUP_INCOMPLETE',details:report}));}catch(error){errors.push(error);}
240
+ for(const release of installationLeases.values())try{await release();}catch(error){errors.push(error);}
170
241
  if(errors.length)throw new AggregateError(errors,`应用关闭失败:${errors.map(e=>e.message).join(';')}`);
171
242
  });return closing;
172
243
  },
173
244
  };
174
- const initial=plugins??await Promise.all((project.plugins??[]).map(spec=>loadPlugin(spec,project.baseBundle?.root??projectDir)));
175
- const services={application:app,runProcess};
176
- const build=extra=>createHost({plugins:[...initial,...extra],config:{...project.config,...config},services,environmentOverrides:project.environments,runtimeEnabled:!maintenance});
177
- const external=[];
178
- for(const name of Object.keys(state.dependencies??{}))external.push(await loadPlugin(name,state.root));
179
- host=await build(external);
180
- for(const name of ['startRun','startCommand','startWeb','saveWorkflow','removeWorkflow','installPlugin','removePlugin','configurePlugin','configureLogs','setMaintenance','pack']){
245
+ let initial;
246
+ const services={application:app,runProcess,fs:fileProbe};
247
+ const hostProject=()=>({directory:projectDir,readOnly:!!project.readOnly});
248
+ const build=extra=>createHost({plugins:[...initial,...extra],config:{...project.config,...config},services,project:hostProject(),network:()=>project.network,environmentOverrides:project.environments,runtimeEnabled:!maintenance,allowUnknownPrivileges});
249
+ try{
250
+ initial=plugins??await Promise.all((project.plugins??[]).map(spec=>loadPlugin(spec,project.baseBundle?.root??projectDir)));
251
+ const external=[];
252
+ for(const name of Object.keys(state.dependencies??{}))if(!maintenance||name!==omitInstalledPlugin)external.push(await loadPlugin(name,state.root));
253
+ for(const [name,source] of Object.entries(state.links??{}))if(!maintenance||name!==omitInstalledPlugin)external.push(await loadPlugin(source,projectDir));
254
+ host=await build(external);
255
+ }catch(error){await fileProbe.close();await opened.release();throw error;}
256
+ for(const name of ['startRun','startCommand','startWeb','saveWorkflow','duplicateWorkflow','removeWorkflow','installPlugin','linkPlugin','removePlugin','configurePlugin','configureNetworkPolicy','configureLogs','setMaintenance','pack']){
181
257
  const original=app[name],isRun=['startRun','startCommand'].includes(name);
182
258
  app[name]=async(...args)=>{
183
259
  if(closing)throw new FlowError('应用已关闭');
@@ -186,26 +262,131 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
186
262
  try{return await task;}finally{operations.delete(task);if(isRun)launching--;}
187
263
  };
188
264
  }
189
- function assertMutable(){if(!maintenance&&host.hasRuntime)throw new FlowError('运行时插件已启动,请先进入维护模式再修改插件或配置');}
190
- function snapshotRun(run){const {done,controller,...result}=run;return structuredClone(result);}
265
+ async function mutateWorkflow(operation){
266
+ if(project.readOnly)throw new FlowError('只读运行包不能修改工作流');
267
+ if(busy)throw new FlowError('插件变更期间暂不能修改工作流',{code:'PLUGIN_BUSY'});
268
+ return withPluginMutation(projectDir,async()=>{
269
+ await assertPluginState(projectDir,state);
270
+ return operation();
271
+ });
272
+ }
273
+ async function backupProjectFile(file,latest,label){
274
+ const directory=path.join(projectDir,'.flow','backups');
275
+ await mkdir(directory,{recursive:true,mode:0o700});
276
+ const backup=path.join(directory,`flow.project.${label}-${randomUUID()}.json`);
277
+ let original;
278
+ try{original=await readFile(file,'utf8');}
279
+ catch(error){if(error.code!=='ENOENT')throw error;original=JSON.stringify(latest,null,2);}
280
+ await writeFile(backup,original,{flag:'wx',mode:0o600});
281
+ return backup;
282
+ }
283
+ function pluginPrivileges(plugin){
284
+ const report=action=>{
285
+ const requirement=resolvePrivilegeRequirement(plugin,action);
286
+ return {action:action===undefined?null:`${plugin.id}:${action}`,requirement,static:checkStaticPrivilege(requirement)};
287
+ };
288
+ return [report(),...Object.keys(plugin.actions??{}).map(report)];
289
+ }
290
+ function assertMaintenance(plan,options={}){
291
+ if(!maintenance&&plan.maintenance.length&&!options.allowMaintenance)throw new FlowError(`更新将重启插件 ${plan.maintenance.join('、')},请确认本次定向维护`,{code:'MAINTENANCE_REQUIRED',details:{plugins:plan.maintenance,affected:plan.affected}});
292
+ const active=plan.affected.filter(id=>resources.active(id));
293
+ if(active.length)throw new FlowError(`请先停止受影响插件提供的 Web:${active.join('、')}`,{code:'WEB_ACTIVE',details:{plugins:active}});
294
+ }
295
+ function workflowPrivileges(closure,entryId){
296
+ const flows=new Map(closure.workflows.map(w=>[w.id,w])),report=[];
297
+ const visit=(id,mandatory)=>{
298
+ const workflow=flows.get(id);
299
+ if(!workflow)return;
300
+ const steps=(list,required)=>{
301
+ let available=required;
302
+ for(const step of list){
303
+ const needed=available&&!step.when;
304
+ if(step.action){
305
+ const pluginId=step.action.slice(0,step.action.lastIndexOf(':')),plugin=host.definitions.get(pluginId);
306
+ const requirement=resolvePrivilegeRequirement(plugin,step.action.slice(pluginId.length+1));
307
+ report.push({workflowId:id,stepId:step.id,action:step.action,mandatory:needed,requirement,static:checkStaticPrivilege(requirement)});
308
+ }
309
+ if(step.workflow)visit(store.childId(step.workflow),needed);
310
+ if(step.branch){steps(step.branch.then,false);steps(step.branch.else??[],false);}
311
+ if(step.parallel)steps(step.parallel.steps,needed);
312
+ if(step.stop)available=false;
313
+ }
314
+ };
315
+ steps(workflow.steps,mandatory);
316
+ };
317
+ visit(entryId,true);return report;
318
+ }
319
+ async function findPluginWorkflowReferences(id){
320
+ const refs=[];
321
+ const collect=(steps,found)=>{for(const step of steps??[]){
322
+ if(typeof step.action==='string'&&step.action.startsWith(`${id}:`))found.add(step.action);
323
+ if(step.branch){collect(step.branch.then,found);collect(step.branch.else,found);}
324
+ if(step.parallel)collect(step.parallel.steps,found);
325
+ }};
326
+ for(const workflow of await store.list()){
327
+ const found=new Set();collect(workflow.steps,found);
328
+ if(found.size)refs.push({id:workflow.id,title:workflow.title??workflow.id,actions:[...found]});
329
+ }
330
+ return refs;
331
+ }
332
+ function runState(run){const {done,controller,...result}=run;return result;}
333
+ function snapshotRun(run){return journals.get(run).snapshot(runState(run));}
334
+ function trimRuns(){
335
+ const completed=[...runs.values()].filter(run=>!writingRuns.has(run.id)&&run.status!=='running');
336
+ for(const run of completed.slice(0,Math.max(0,completed.length-maxCachedRuns)))runs.delete(run.id);
337
+ // 超大业务结果交给 done 的调用方;宿主历史改读磁盘摘要,避免长期持有大对象。
338
+ for(const run of completed){
339
+ try{if(run.output!==undefined&&Buffer.byteLength(JSON.stringify(run.output))>(project.logs?.maxMemoryBytes??1048576)){
340
+ if(run.error?.code==='LOG_WRITE_FAILED'){delete run.output;run.outputTruncated=true;}else runs.delete(run.id);
341
+ }}
342
+ catch{delete run.output;run.outputTruncated=true;}
343
+ }
344
+ }
191
345
  async function pruneLogs(){for(const id of await runLogs.prune(project.logs?.keepRuns??null,writingRuns))runs.delete(id);}
192
- async function change(operation){
346
+ async function change(operation,options={}){
193
347
  if(project.readOnly)throw new FlowError('运行包不支持安装或移除插件');
194
- assertMutable();
195
348
  if(busy || launching || [...runs.values()].some(r=>r.status==='running'))throw new FlowError('请等待当前运行或安装结束');
196
- busy=true;let nextHost;
349
+ busy=true;let candidate,nextRelease;
197
350
  try {
351
+ return await withPluginMutation(projectDir,async()=>{
352
+ await assertPluginState(projectDir,state);
353
+ const latest=await readJson(path.join(projectDir,'flow.project.json'),project);
354
+ if(latest.readOnly)throw new FlowError('运行包不支持安装或移除插件');
355
+ if(operation.remove){
356
+ const name=operation.remove;
357
+ if(resources.active(name))throw new FlowError('请先停止此插件提供的 Web');
358
+ if(!state.dependencies?.[name]&&!state.links?.[name])throw new FlowError('此插件来自发行配置,不能在此移除');
359
+ const dependents=[...host.definitions.values()].filter(p=>p.id!==name&&Object.hasOwn(p.dependencies??{},name)).map(p=>p.id);
360
+ if(dependents.length)throw new FlowError(`插件仍被其他插件依赖:${dependents.join('、')}`,{code:'PLUGIN_IN_USE',details:{dependencies:dependents}});
361
+ const workflows=await findPluginWorkflowReferences(name);
362
+ if(workflows.length&&!options.force)throw new FlowError(`插件 ${name} 仍被工作流引用:${workflows.map(w=>w.id).join('、')}`,{code:'PLUGIN_IN_USE',details:{workflows}});
363
+ }
198
364
  const next=await installGeneration(projectDir,{...operation,state});
199
- for(const p of host.definitions.values())if(resources.active(p.id)){
200
- const candidate=[...initial,...next.loaded].find(e=>(e.plugin??e).id===p.id);
201
- if(!candidate||candidate.root!==p.root||(candidate.plugin??candidate).version!==p.version)throw new FlowError('请先停止活动 Web,再替换其提供者');
365
+ const nextEntries=[...initial,...next.loaded],activeConfig={...project.config,...config},effectiveConfig={...latest.config,...config};
366
+ candidate=await createHost({plugins:nextEntries,config:effectiveConfig,services,project:hostProject(),network:()=>project.network,environmentOverrides:project.environments,runtimeEnabled:false,allowUnknownPrivileges});
367
+ const sourceChanges=await changedPluginSources(host.definitions,candidate.definitions,{oldInstallationRoot:state.root,newInstallationRoot:next.state.root});
368
+ const plan=planPluginUpdate(host.definitions,candidate.definitions,activeConfig,effectiveConfig,sourceChanges);
369
+ assertMaintenance(plan,options);
370
+ await candidate.close();candidate=null;
371
+ nextRelease=await leaseInstallation(projectDir,next.state.root);
372
+ const oldEntries=[...host.definitions.values()].map(p=>({plugin:p,root:p.root}));
373
+ if(plan.affected.length)await host.replace({plugins:nextEntries,config:effectiveConfig,affected:plan.affected});
374
+ try{await savePluginState(projectDir,next.state,state);}
375
+ catch(error){
376
+ if(plan.affected.length)try{await host.replace({plugins:oldEntries,config:activeConfig,affected:plan.affected});}
377
+ catch(recovery){host.invalidate();throw new AggregateError([error,recovery],`插件状态保存失败且恢复失败:${error.message};${recovery.message}`);}
378
+ throw error;
379
+ }
380
+ state=next.state;
381
+ for(const key of Object.keys(project))delete project[key];Object.assign(project,latest);
382
+ installationLeases.set(path.resolve(state.root),nextRelease);nextRelease=null;
383
+ for(const [root,release] of installationLeases)if(root!==path.resolve(state.root)&&![...host.definitions.values()].some(p=>p.root&&path.resolve(p.root).startsWith(root+path.sep))){
384
+ try{await release();installationLeases.delete(root);}
385
+ catch(error){emitter.emit('event',{type:'plugin.cleanup.warning',message:`旧安装批次租约暂未清理:${error.message}`,root});}
202
386
  }
203
- nextHost=await build(next.loaded);
204
- const file=path.join(projectDir,'.flow','plugins.json'),temp=`${file}.tmp`;
205
- await writeFile(temp,JSON.stringify(next.state,null,2));await rename(temp,file);
206
- const old=host;host=nextHost;nextHost=null;state=next.state;await old.close();
207
387
  return app.listPlugins();
208
- } finally {try{if(nextHost)await nextHost.close();}finally{busy=false;}}
388
+ });
389
+ } finally {try{if(candidate)await candidate.close();}finally{try{if(nextRelease)await nextRelease();}finally{busy=false;}}}
209
390
  }
210
391
  return app;
211
392
  }
@@ -1,17 +1,2 @@
1
- import { createHash } from 'node:crypto';
2
- import { readFile, realpath } from 'node:fs/promises';
3
- import path from 'node:path';
4
-
5
- export async function verifyBundle(root){
6
- root=await realpath(root);
7
- const manifest=JSON.parse(await readFile(path.join(root,'manifest.json'),'utf8'));
8
- const errors=[];
9
- for(const [relative,expected] of Object.entries(manifest.files??{})){
10
- const file=path.resolve(root,relative);
11
- try{
12
- if(!file.startsWith(root+path.sep)||!(await realpath(file)).startsWith(root+path.sep))throw new Error('路径越界');
13
- if(createHash('sha256').update(await readFile(file)).digest('hex')!==expected)errors.push(relative);
14
- }catch{errors.push(relative);}
15
- }
16
- return {ok:errors.length===0,errors};
17
- }
1
+ // 兼容既有 core 公开入口,实际能力由 platform 模块维护。
2
+ export * from '@mytools/fst-platform/bundle';
@@ -0,0 +1,69 @@
1
+ import {watch} from 'node:fs';
2
+ import {readFile,opendir,lstat} from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ // 所有目录 I/O 都留在可回收进程中;扫描限制同时约束监听句柄和遍历工作量。
6
+ const options=JSON.parse(process.env.FISHTOOLS_WATCH_OPTIONS),handles=new Map();
7
+ const ignored=new Set(['node_modules','.git','.flow','dist']);
8
+ let stopped=false,scanning=false,again=false,first=true,links=[];
9
+ const send=message=>{if(process.connected)process.send(message);};
10
+ const remote=value=>/^\\\\|^\/\//.test(value);
11
+ async function json(file,fallback){try{return JSON.parse(await readFile(file,'utf8'));}catch(error){if(error.code==='ENOENT')return fallback;throw error;}}
12
+ function change(file){send({type:'changed',file});}
13
+ function addWatch(key,directory,callback,wanted){
14
+ wanted.add(key);if(handles.has(key))return;
15
+ try{const handle=watch(directory,callback);handle.on('error',error=>send({type:'warning',message:`监听 ${directory} 失败:${error.message}`}));handles.set(key,handle);}
16
+ catch(error){send({type:'warning',message:`无法监听 ${directory}:${error.message}`});}
17
+ }
18
+ async function scan(){
19
+ if(stopped)return;if(scanning){again=true;return;}scanning=true;
20
+ send({type:'scanning'});const wanted=new Set();let directories=0,entries=0,incomplete=false;
21
+ try{
22
+ addWatch('project',options.projectDir,(_event,name)=>{
23
+ if(name===null||['flow.project.json','.flow'].includes(String(name))){
24
+ if(name===null||String(name)==='flow.project.json')change(path.join(options.projectDir,String(name??'')));
25
+ void scan();
26
+ }
27
+ },wanted);
28
+ const stateDirectory=path.join(options.projectDir,'.flow');
29
+ addWatch('state',stateDirectory,(_event,name)=>{if(name===null||String(name)==='plugins.json'){change(path.join(stateDirectory,'plugins.json'));void scan();}},wanted);
30
+ if(options.configFile)addWatch('config',path.dirname(options.configFile),(_event,name)=>{
31
+ if(name===null||String(name)===path.basename(options.configFile)){change(options.configFile);void scan();}
32
+ },wanted);
33
+ let project={};
34
+ try{
35
+ project=await json(path.join(options.projectDir,'flow.project.json'),{});
36
+ const state=await json(path.join(stateDirectory,'plugins.json'),{});
37
+ links=Object.values(state.links??{}).filter(value=>typeof value==='string').map(value=>path.resolve(options.projectDir,value));
38
+ }catch(error){send({type:'warning',message:`开发配置暂不可读,保留已有源码监听:${error.message}`});}
39
+ incomplete=links.length>options.maxDirectories;
40
+ const queue=links.slice(0,options.maxDirectories).map(directory=>({directory,depth:0})),seen=new Set();
41
+ while(queue.length&&!stopped){
42
+ const {directory,depth}=queue.shift();if(seen.has(directory))continue;seen.add(directory);
43
+ if(remote(directory)){send({type:'warning',message:`跳过远程开发目录:${directory}`});continue;}
44
+ if(directories>=options.maxDirectories||entries>=options.maxEntries){incomplete=true;break;}
45
+ let stat;try{stat=await lstat(directory);}catch{continue;}
46
+ if(!stat.isDirectory()||stat.isSymbolicLink())continue;directories++;
47
+ addWatch(`source:${directory}`,directory,(event,name)=>{
48
+ const relative=String(name??'');if(relative.split(/[\\/]/).some(part=>ignored.has(part)))return;
49
+ change(path.join(directory,relative));if(event==='rename')void scan();
50
+ },wanted);
51
+ if(depth>=options.maxDepth){incomplete=true;continue;}
52
+ let listing;
53
+ try{
54
+ listing=await opendir(directory);
55
+ for await(const entry of listing){
56
+ entries++;if(entries>=options.maxEntries){incomplete=true;break;}
57
+ if(entry.isDirectory()&&!ignored.has(entry.name))queue.push({directory:path.join(directory,entry.name),depth:depth+1});
58
+ }
59
+ }catch(error){send({type:'warning',message:`扫描 ${directory} 失败:${error.message}`});}
60
+ }
61
+ for(const [key,handle] of handles)if(!wanted.has(key)){handle.close();handles.delete(key);}
62
+ if(incomplete)send({type:'warning',message:'开发源码扫描达到目录、深度或条目预算,超出部分未监听'});
63
+ send({type:first?'ready':'snapshot',readOnly:!!project.readOnly,directories,watchers:handles.size,incomplete});first=false;
64
+ }finally{scanning=false;if(again){again=false;void scan();}}
65
+ }
66
+ function close(){stopped=true;for(const handle of handles.values())handle.close();handles.clear();process.disconnect?.();}
67
+ process.on('message',message=>{if(message?.type==='close')close();});
68
+ process.on('disconnect',close);
69
+ void scan().catch(error=>{send({type:'fatal',message:error.message});close();});