@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
@@ -0,0 +1,52 @@
1
+ import { validate } from '@mytools/sdk/schema';
2
+ import { fileReference } from './model.js';
3
+
4
+ export const needsJsonEditor=schema=>['oneOf','anyOf','allOf','if','$ref','patternProperties'].some(key=>schema?.[key]!==undefined);
5
+
6
+ // 值只在整个编辑过程完成后返回,Escape 不会留下半个修改。
7
+ export async function editValue(ui,schema={},value,{baseDir,required=false,label=schema.title??'值'}={}){
8
+ if(value===undefined&&schema.default!==undefined)value=structuredClone(schema.default);
9
+ if(schema.enum&&!needsJsonEditor(schema)){
10
+ const index=await ui.select(label,schema.enum.map((v,i)=>({value:String(i),label:JSON.stringify(v)})));
11
+ return schema.enum[Number(index)];
12
+ }
13
+ if(schema.type==='boolean'&&!needsJsonEditor(schema))return (await ui.select(label,[{value:'true',label:'是 / true'},{value:'false',label:'否 / false'}]))==='true';
14
+ if(schema.type==='object'&&schema.properties&&!needsJsonEditor(schema)){
15
+ const result=structuredClone(value??{}),props=Object.entries(schema.properties);
16
+ while(true){
17
+ const key=await ui.select(`${label} · 选择字段`,[
18
+ ...props.map(([key,s])=>({value:`field:${key}`,label:`${s.title??key}${schema.required?.includes(key)?' *':''}`,description:JSON.stringify(result[key])??'未填写'})),
19
+ {value:'__done',label:'完成填写'},
20
+ ]);
21
+ if(key==='__done'){
22
+ try{return validate(schema,result,label);}catch(e){ui.show(e.message);continue;}
23
+ }
24
+ const field=key.slice(6);
25
+ const next=await editValue(ui,schema.properties[field],result[field],{baseDir,required:schema.required?.includes(field),label:schema.properties[field].title??field});
26
+ if(next===undefined)delete result[field];else result[field]=next;
27
+ }
28
+ }
29
+ if(schema.type==='array'&&schema.items&&!Array.isArray(schema.items)&&!needsJsonEditor(schema)){
30
+ const result=structuredClone(value??[]);
31
+ while(true){
32
+ const choice=await ui.select(label,[...result.map((v,i)=>({value:String(i),label:`${i+1}. ${JSON.stringify(v)}`})),{value:'add',label:'添加一项'},{value:'done',label:'完成列表'}]);
33
+ if(choice==='done'){try{return validate(schema,result,label);}catch(e){ui.show(e.message);continue;}}
34
+ if(choice==='add'){const item=await editValue(ui,schema.items,undefined,{baseDir,required:true,label:'新项'});result.push(item);}
35
+ else{
36
+ const index=Number(choice),action=await ui.select('修改列表项',[{value:'edit',label:'编辑'},{value:'remove',label:'移除'}]);
37
+ if(action==='remove')result.splice(index,1);else result[index]=await editValue(ui,schema.items,result[index],{baseDir,required:true,label:`第 ${index+1} 项`});
38
+ }
39
+ }
40
+ }
41
+ while(true){
42
+ const string=schema.type==='string';
43
+ const initial=value===undefined?'':string?String(value):JSON.stringify(value);
44
+ const raw=await ui.input(`${label}${required?'(必填)':''}${string?' · @文件引用路径,@@表示字面量@':' · 输入 JSON 值'}${schema.description?'\n'+schema.description:''}`,{value:string&&initial.startsWith('@')?'@'+initial:initial,files:string});
45
+ try{
46
+ if(!raw&&!required&&schema.type!=='string')return undefined;
47
+ if(!raw&&required&&schema.type!=='string')throw new Error('请填写必填值');
48
+ const result=string?fileReference(raw,baseDir):JSON.parse(raw);
49
+ return validate(schema,result,label);
50
+ }catch(e){ui.show(`输入无效:${e.message}`);}
51
+ }
52
+ }
@@ -9,10 +9,20 @@ import {gettingStarted,pluginStatus,pluginDetails} from './presentation.js';
9
9
  import {readableValue,readableError} from './run-presentation.js';
10
10
  import {commandMenu} from './navigation.js';
11
11
  import {workflowMenu} from './workflow-menu.js';
12
+ import {resolvePrivilegeRequirement,checkStaticPrivilege} from '@mytools/fst-plugin/privileges';
12
13
 
13
14
  export class TerminalSession{
14
15
  constructor(app,ui,{baseDir=process.cwd(),webOptions={}}={}){this.app=app;this.ui=ui;this.baseDir=baseDir;this.webOptions=webOptions;this.flow=null;this.dirty=false;this.exiting=false;this.web=null;ui.activeRuns=()=>app.listRuns().filter(run=>run.status==='running').length;}
15
16
  writable(){if(this.app.project.readOnly)throw new Error('这是只读运行包,请在完整工作区中编排、安装或打包');}
17
+ async withMaintenance(operation,options={}){
18
+ try{return await operation(options);}
19
+ catch(error){
20
+ if(error.code!=='MAINTENANCE_REQUIRED')throw error;
21
+ const names=error.details?.plugins?.join('、')??'相关插件';
22
+ if(!await this.ui.confirm(`本次修改会短暂停止并重启 ${names}。现在继续吗?`))return null;
23
+ return operation({...options,allowMaintenance:true});
24
+ }
25
+ }
16
26
  current(){if(!this.flow)throw new Error('先用 /workflows 打开或新建工作流');return this.flow;}
17
27
  commands(){return commandMenu(this.app.listPlugins?.()??[],this.app.listCommands());}
18
28
  async discard(){return !this.dirty||await this.ui.confirm('当前修改未保存,是否放弃修改?');}
@@ -71,7 +81,7 @@ export class TerminalSession{
71
81
  '## 全部操作',this.commands().map(c=>`- /${c.name}:${c.description}`).join('\n'),
72
82
  ].join('\n\n'));return;
73
83
  case 'docs':{
74
- const {listDocumentation,readDocumentation}=await import('../documentation.js');
84
+ const {listDocumentation,readDocumentation}=await import('@mytools/fst-devtools/documentation');
75
85
  const topics=await listDocumentation();
76
86
  const topic=await this.ui.select('离线文档', [...topics.map(item=>({value:item.id,label:item.title})),{value:'back',label:'返回工作台'}]);
77
87
  if(topic!=='back'){const document=await readDocumentation(topic);await this.ui.viewMarkdown(document.title,document.text);}
@@ -110,7 +120,12 @@ export class TerminalSession{
110
120
  if(args.length>1)throw new Error(`用法:/${command.name} [@参数.json]`);
111
121
  const input=args.length?await readRunInput(args[0],this.baseDir):command.input?await editValue(this.ui,command.input,{}, {baseDir:this.baseDir,required:true,label:command.description}):{};
112
122
  const ui=Object.fromEntries(['input','select','show','confirm'].map(name=>[name,this.ui[name].bind(this.ui)]));
113
- const run=await this.app.startCommand(command.name,input,{ui});
123
+ const definition=this.app.host.definitions.get(command.plugin);
124
+ const requirement=resolvePrivilegeRequirement(definition,command.action);
125
+ const unknown=requirement.declared&&checkStaticPrivilege(requirement).status==='unknown';
126
+ const allowUnknownPrivileges=unknown?await this.ui.confirm(`/${command.name} 的权限需要运行时核查。若检查结果仍无法确认,是否允许本次继续?\n${requirement.reason??''}\n${requirement.remediation??''}`):false;
127
+ if(unknown&&!allowUnknownPrivileges)return;
128
+ const run=await this.app.startCommand(command.name,input,{ui,allowUnknownPrivileges});
114
129
  if(this.ui.trackRun){
115
130
  this.ui.clearMessages?.();this.ui.trackRun({app:this.app,runId:run.id,title:command.description??command.name,input});
116
131
  if(command.action){void run.done.catch(error=>{if(!this.ui.closed)this.ui.show(`运行记录保存失败:${error.message}`);});return;}
@@ -127,19 +142,22 @@ export class TerminalSession{
127
142
  const id=selectedId??await this.ui.select('选择要配置的插件',plugins.map(p=>({value:p.id,label:p.title??p.id})));
128
143
  const plugin=plugins.find(p=>p.id===id);
129
144
  const value=await editValue(this.ui,plugin.config,this.app.project.config?.[id]??{}, {baseDir:this.baseDir,required:true,label:plugin.title??id});
130
- await this.app.configurePlugin(id,value);this.ui.show('插件配置已保存到当前工作区。');
145
+ const result=await this.withMaintenance(options=>this.app.configurePlugin(id,value,options));
146
+ this.ui.show(result===null?'已取消修改。':`插件配置已保存到当前工作区。原项目配置备份:${result.backup}`);
131
147
  }
132
148
  async logs(){
133
149
  const statuses=runStatuses;
134
150
  while(true){
135
151
  const history=(await this.app.listRunHistory()).sort((a,b)=>(b.status==='running')-(a.status==='running'));
136
- if(!history.length){this.ui.show('还没有执行记录。运行工作流后,可在 /logs 回看。');return;}
152
+ if(!history.length)this.ui.show('还没有执行记录。运行工作流后,可在 /logs 回看。');
137
153
  const titles=new Map((await this.app.listWorkflows()).map(flow=>[flow.id,flow.title??flow.id]));
138
154
  const choice=await this.ui.select('执行记录 · 选择一次执行,查看步骤、输入输出与日志',[
139
155
  ...history.map(r=>({value:`run:${r.id}`,label:`${statuses[r.status]??r.status} · ${titles.get(r.workflowId)??r.workflowId}`,description:`${new Date(r.startedAt).toLocaleString()} · ${r.id.slice(0,8)}`})),
156
+ {value:'usage',label:'查看日志体积',description:'按需统计,不清理文件'},
140
157
  {value:'back',label:'返回'},
141
158
  ]);
142
159
  if(choice==='back')return;
160
+ if(choice==='usage'){await this.logUsage();continue;}
143
161
  const id=choice.slice(4),record=history.find(r=>r.id===id);
144
162
  try{
145
163
  if(this.ui.observeRun)await this.openRun(record,{initialView:2});
@@ -285,9 +303,12 @@ export class TerminalSession{
285
303
  if(choice==='saved')flow=saved;
286
304
  else await this.execute('/save');
287
305
  }
288
- await this.app.checkWorkflow(flow.id);
306
+ const report=await this.app.checkWorkflow(flow.id);
307
+ const unknown=report.privileges.filter(item=>item.requirement.declared&&item.static.status==='unknown');
308
+ const allowUnknownPrivileges=unknown.length?await this.ui.confirm(`这些动作需要在运行时核查权限:${unknown.map(item=>item.action).join('、')}。若仍无法确认,是否允许本次继续?`):false;
309
+ if(unknown.length&&!allowUnknownPrivileges)return false;
289
310
  const input=reference?await readRunInput(reference,this.baseDir):(needsJsonEditor(flow.inputs)||Object.keys(flow.inputs?.properties??{}).length)?await editValue(this.ui,flow.inputs,{}, {baseDir:this.baseDir,label:'本次运行参数',required:true}):{};
290
- const run=await this.app.startRun(flow.id,input);
311
+ const run=await this.app.startRun(flow.id,input,{allowUnknownPrivileges});
291
312
  if(this.ui.trackRun){
292
313
  this.ui.clearMessages?.();this.ui.trackRun({app:this.app,runId:run.id,title:flow.title??flow.id,input});
293
314
  void run.done.catch(error=>{if(!this.ui.closed)this.ui.show(`运行记录保存失败:${error.message}`);});return true;
@@ -319,20 +340,116 @@ export class TerminalSession{
319
340
  const keep=this.app.logSettings().keepRuns;
320
341
  const choice=await this.ui.select('Fishtools 配置',[
321
342
  {value:'info',label:'查看工作区信息'},
343
+ {value:'network',label:'代理设置',description:'分别设置整个工作区、插件或某个动作'},
344
+ {value:'usage',label:'查看日志体积'},
322
345
  ...(!this.app.project.readOnly?[{value:'logs',label:'日志保留次数',description:keep===null?'不自动清理':`最近 ${keep} 次`}]:[]),
323
346
  {value:'back',label:'返回主面板'},
324
347
  ]);
325
348
  if(choice==='back')return;
326
349
  if(choice==='info')this.ui.show(`工作区:${this.app.projectDir}\n日志目录:${this.app.outputDir}\n模式:${this.app.project.readOnly?'只读运行包':'可编辑工作区'}`);
350
+ else if(choice==='network')await this.networkSettings();
351
+ else if(choice==='usage')await this.logUsage();
327
352
  else await this.logSettings();
328
353
  }
329
354
  }
355
+ async chooseNetworkRule(label){
356
+ const profiles=Object.keys(this.app.networkPolicy().profiles??{});
357
+ const value=await this.ui.select(label,[
358
+ {value:'inherit',label:'沿用系统代理环境'},
359
+ {value:'direct',label:'直连,不使用代理'},
360
+ ...profiles.map(name=>({value:`profile:${name}`,label:`使用代理配置:${name}`})),
361
+ {value:'back',label:'返回'},
362
+ ]);
363
+ if(value==='back')return null;
364
+ return value.startsWith('profile:')?{mode:'profile',profile:value.slice(8)}:{mode:value};
365
+ }
366
+ async networkProfiles(){
367
+ while(true){
368
+ const policy=this.app.networkPolicy(),profiles=policy.profiles??{};
369
+ const choice=await this.ui.select('代理配置(地址可用环境变量引用,凭据请勿写入项目)',[
370
+ ...Object.keys(profiles).map(name=>({value:`edit:${name}`,label:`修改 ${name}`})),
371
+ ...(!this.app.project.readOnly?[{value:'create',label:'+ 新建代理配置'},...Object.keys(profiles).map(name=>({value:`delete:${name}`,label:`删除 ${name}`}))]:[]),
372
+ {value:'back',label:'返回'},
373
+ ]);
374
+ if(choice==='back')return;
375
+ this.writable();
376
+ if(choice.startsWith('delete:')){
377
+ const name=choice.slice(7);
378
+ if(await this.ui.confirm(`删除代理配置 ${name}?引用它的规则需要先改为其他模式。`)){
379
+ delete profiles[name];const saved=await this.app.configureNetworkPolicy({...policy,profiles});
380
+ this.ui.show(`代理配置 ${name} 已删除。原项目配置备份:${saved.backup}`);
381
+ }
382
+ continue;
383
+ }
384
+ const name=choice==='create'?(await this.ui.input('代理配置名称(字母开头)',{files:false})).trim():choice.slice(5);
385
+ if(!name)throw new Error('代理配置名称不能为空');
386
+ const current=profiles[name]??{},next={};
387
+ for(const [field,label] of [['http','HTTP 请求'],['https','HTTPS 请求'],['noProxy','不走代理的目标(例如 localhost,127.0.0.1)']]){
388
+ const direct=field==='noProxy'?'noProxy':`${field}Url`,reference=`${field}Env`;
389
+ const source=await this.ui.select(`${name} · ${label}`,[{value:'none',label:'不单独设置'},{value:'url',label:field==='noProxy'?'直接填写目标列表':'直接填写代理地址(不能带凭据)'},{value:'env',label:'读取环境变量(可用于有凭据的代理)'}]);
390
+ if(source!=='none'){
391
+ const key=source==='env'?reference:direct;
392
+ next[key]=(await this.ui.input(source==='env'?'环境变量名称':label,{value:current[key]??'',files:false})).trim();
393
+ }
394
+ }
395
+ const saved=await this.app.configureNetworkPolicy({...policy,profiles:{...profiles,[name]:next}});
396
+ this.ui.show(`代理配置 ${name} 已保存。原项目配置备份:${saved.backup}`);
397
+ }
398
+ }
399
+ async networkSettings(){
400
+ while(true){
401
+ const policy=this.app.networkPolicy(),summary=JSON.stringify(policy,null,2);
402
+ const choice=await this.ui.select('代理设置 · 动作规则优先于插件,插件优先于工作区',[
403
+ {value:'view',label:'查看当前规则'},
404
+ ...(!this.app.project.readOnly?[{value:'default',label:'设置工作区默认模式'},{value:'plugin',label:'设置某个插件的默认模式'},{value:'action',label:'设置某个动作的模式'}]:[]),
405
+ {value:'profiles',label:'管理代理配置'},
406
+ {value:'back',label:'返回'},
407
+ ]);
408
+ if(choice==='back')return;
409
+ if(choice==='view'){
410
+ const report=this.app.networkReport();
411
+ const mode=item=>item.mode==='profile'?`代理配置 ${item.profile}`:item.mode==='direct'?'直连':'继承系统环境';
412
+ const effective=report.entries.map(item=>`${item.id}:${mode(item)}(来源:${{action:'动作',plugin:'插件',workspace:'工作区',system:'系统默认'}[item.source]})`);
413
+ const stale=[...report.stalePlugins.map(id=>`插件 ${id}`),...report.staleActions.map(id=>`动作 ${id}`)];
414
+ this.ui.show(`已保存规则:\n${summary}\n\n最终生效模式:\n${effective.join('\n')||'暂无已安装插件'}\n\n失效引用:\n${stale.join('\n')||'无'}`);
415
+ continue;
416
+ }
417
+ if(choice==='profiles'){await this.networkProfiles();continue;}
418
+ const next=structuredClone(policy);
419
+ if(choice==='default'){
420
+ const rule=await this.chooseNetworkRule('工作区默认模式');if(!rule)continue;
421
+ next.default=rule;
422
+ }else{
423
+ const installed=this.app.listPlugins();
424
+ if(!installed.length){this.ui.show('当前没有已安装插件。');continue;}
425
+ const id=await this.ui.select('选择插件',installed.map(p=>({value:p.id,label:p.title??p.id})));
426
+ next.plugins??={};next.plugins[id]??={};
427
+ if(choice==='plugin'){
428
+ const rule=await this.chooseNetworkRule(`${id} 默认模式`);if(!rule)continue;
429
+ next.plugins[id].default=rule;
430
+ }else{
431
+ const actions=this.app.catalog().filter(a=>a.plugin===id);
432
+ if(!actions.length){this.ui.show('此插件没有可编排动作。');continue;}
433
+ const action=await this.ui.select('选择动作',actions.map(a=>({value:a.id.slice(id.length+1),label:a.title??a.id})));
434
+ const rule=await this.chooseNetworkRule(`${id}:${action} 模式`);if(!rule)continue;
435
+ next.plugins[id].actions??={};next.plugins[id].actions[action]=rule;
436
+ }
437
+ }
438
+ const saved=await this.app.configureNetworkPolicy(next);this.ui.show(`代理规则已保存。原项目配置备份:${saved.backup}`);
439
+ }
440
+ }
441
+ async logUsage(){
442
+ const usage=await this.app.logUsage(),keep=this.app.logSettings().keepRuns;
443
+ const units=['B','KiB','MiB','GiB','TiB'];let size=usage.bytes,index=0;
444
+ while(size>=1024&&index<units.length-1){size/=1024;index++;}
445
+ this.ui.show(`日志目录:${usage.directory}\n日志文件体积:${index?size.toFixed(2):size} ${units[index]}(${usage.bytes} 字节,${usage.files} 个文件)${usage.incomplete?',部分路径无法读取,统计不完整':''}\n统计该目录中所有运行的 run.md / events.jsonl / result.json,包含进行中的运行;不含业务文件,不跟随链接。共享目录可能包含其他项目。\n这是文件长度快照,运行中可能继续增长;并非磁盘实际分配空间。\n本项目保留策略:${keep===null?'不自动清理':`最近 ${keep} 次已结束运行`};没有单次运行体积上限。`);
446
+ }
330
447
  async logSettings(){
331
448
  const keep=this.app.logSettings().keepRuns;
332
449
  const mode=await this.ui.select('保留策略(整个项目合计,仅清理日志,保留业务文件)',[{value:'count',label:'仅保留最近 N 次已结束运行'},{value:'all',label:'不自动清理'},{value:'back',label:'返回'}]);
333
450
  if(mode==='back')return;
334
451
  const count=mode==='all'?null:Number(await this.ui.input('保留次数(例如 10)',{value:String(keep??10),files:false}));
335
- await this.app.configureLogs(count);this.ui.show('日志保留设置已保存。');
452
+ const saved=await this.app.configureLogs(count);this.ui.show(`日志保留设置已保存。原项目配置备份:${saved.backup}${saved.warning?`\n${saved.warning.message}`:''}`);
336
453
  }
337
454
  async openRun(entry,{initialView}={}){
338
455
  const record=this.app.getRun(entry.id)??await this.app.readRun(entry.id),workflows=await this.app.listWorkflows();
@@ -358,7 +475,8 @@ export class TerminalSession{
358
475
  const text=await this.ui.input(action==='npm'?'npm 包名(支持 @scope/name)':'输入 @ 并选择插件目录或 .tgz',{files:action==='local'});
359
476
  const source=action==='local'?fileReference(text,this.baseDir):text.trim();if(!source)throw new Error('插件来源不能为空');
360
477
  if(!await this.ui.confirm(`安装并加载可信插件:${source}?`))return;
361
- const done=this.ui.busy('正在安装插件…');try{await this.app.installPlugin(source);}finally{done();}
478
+ const done=this.ui.busy('正在安装插件…');let result;try{result=await this.withMaintenance(options=>this.app.installPlugin(source,options));}finally{done();}
479
+ if(result===null){this.ui.show('已取消安装。');continue;}
362
480
  }
363
481
  this.ui.show('插件列表已更新。');
364
482
  }
@@ -383,7 +501,17 @@ export class TerminalSession{
383
501
  else if(choice==='configure')await this.configure(id);
384
502
  else if(choice.startsWith('command:'))await this.pluginCommand(commands.find(c=>c.name===choice.slice(8)),[]);
385
503
  else if(choice==='remove'){
386
- this.writable();if(await this.ui.confirm(`移除 ${id}?已有工作流可能依赖它。`)){await this.app.removePlugin(id);this.ui.show('插件已移除。');return;}
504
+ this.writable();if(await this.ui.confirm(`移除 ${id}?`)){
505
+ let removed;
506
+ try{removed=await this.withMaintenance(options=>this.app.removePlugin(id,options));}
507
+ catch(error){
508
+ if(error.code!=='PLUGIN_IN_USE'||!error.details?.workflows?.length)throw error;
509
+ const names=error.details.workflows.map(w=>w.title??w.id).join('、');
510
+ if(!await this.ui.confirm(`以下工作流仍使用 ${id}:${names}。移除后这些工作流将无法运行,仍要移除吗?`))continue;
511
+ removed=await this.withMaintenance(options=>this.app.removePlugin(id,options),{force:true});
512
+ }
513
+ if(removed){this.ui.show('插件已移除。');return;}
514
+ }
387
515
  }
388
516
  }
389
517
  }
@@ -0,0 +1,29 @@
1
+ import {stripVTControlCharacters} from 'node:util';
2
+ import {truncateToWidth,visibleWidth} from '@mariozechner/pi-tui';
3
+
4
+ // 先清理外部文本,再由受控主题函数添加颜色;不接受插件传入的 ANSI。
5
+ export const cleanText=value=>stripVTControlCharacters(String(value)).replace(/[\x00-\x08\x0b-\x1f\x7f]/g,'');
6
+ const color=(rgb,text)=>Object.hasOwn(process.env,'NO_COLOR')?text:`\x1b[38;2;${rgb}m${text}\x1b[39m`;
7
+ export const palette={
8
+ brand:text=>color('78;201;176',text),
9
+ error:text=>color('244;117;117',text),
10
+ text:text=>color('229;233;237',text),
11
+ muted:text=>color('139;149;158',text),
12
+ border:text=>color('74;101;99',text),
13
+ };
14
+ export const menuTheme={selectedPrefix:palette.brand,selectedText:palette.brand,description:palette.muted,scrollInfo:palette.muted,noMatch:palette.muted};
15
+ export const fitLine=(text,width)=>truncateToWidth(text,Math.max(1,width),'…');
16
+
17
+ export function shortenPath(value,width){
18
+ if(visibleWidth(value)<=width)return value;
19
+ // 中间省略保留盘符与项目名;完整目录仍在帮助页中展示。
20
+ const chars=[...value],head=chars.splice(0,Math.min(3,Math.max(0,width-2))).join('');
21
+ while(chars.length&&visibleWidth(head+'…'+chars.join(''))>width)chars.shift();
22
+ return fitLine(head+'…'+chars.join(''),width);
23
+ }
24
+
25
+ export function markdownTheme(){
26
+ const plain=text=>text;
27
+ return {heading:palette.brand,link:palette.brand,linkUrl:palette.muted,code:palette.brand,codeBlock:plain,codeBlockBorder:palette.border,quote:plain,quoteBorder:palette.border,hr:palette.border,listBullet:palette.brand,
28
+ bold:palette.text,italic:plain,strikethrough:plain,underline:plain};
29
+ }
@@ -6,7 +6,7 @@ import { MarkdownLogViewer } from './log-viewer.js';
6
6
  import { RunViewer, mouseOn, mouseOff, mouseWheel } from './run-viewer.js';
7
7
  import {RunFeed} from './run-feed.js';
8
8
  import { logo } from './presentation.js';
9
- import { VERSION } from '../version.js';
9
+ import { VERSION } from '@mytools/core/version';
10
10
  import {cleanText as safe,palette,menuTheme as theme,fitLine,shortenPath} from './theme.js';
11
11
 
12
12
  export const cancelled=()=>Object.assign(new Error('已返回主菜单'),{code:'PROMPT_CANCELLED'});
@@ -191,13 +191,13 @@ export class TerminalView{
191
191
  }
192
192
  }
193
193
  async observeRun({app,runId,record,flow,workflows=[],initialView}){
194
- const previousPending=this.pending??null,children=this.tui.children;let overlay,unsubscribe;
194
+ const previousPending=this.pending??null,children=this.tui.children;let overlay,unsubscribe,readPages=false;
195
195
  const initial=app.getRun(runId)??record??await app.readRun(runId);
196
196
  if(!initial)throw new Error('这次运行记录已不存在');
197
197
  try{
198
198
  await new Promise((resolve,reject)=>{
199
199
  this.pending={resolve,reject};
200
- this.runViewer=new RunViewer({record:initial,flow:initial.status==='running'?flow:undefined,workflows,catalog:app.catalog?.()??[],terminal:this.terminal,onClose:resolve,onCancel:()=>app.cancelRun(runId),requestRender:()=>this.tui.requestRender()});
200
+ this.runViewer=new RunViewer({record:initial,flow:initial.status==='running'?flow:undefined,workflows,catalog:app.catalog?.()??[],terminal:this.terminal,onClose:resolve,onCancel:()=>app.cancelRun(runId),onReadLogs:app.readLogPage?()=>{readPages=true;resolve();}:undefined,requestRender:()=>this.tui.requestRender()});
201
201
  Object.assign(this.runViewer,this.runViews.get(runId)??{});
202
202
  if(initialView!==undefined){this.runViewer.view=initialView;this.runViewer.offset=0;}
203
203
  unsubscribe=app.subscribe(event=>{if(event.runId===runId)this.runViewer?.update(app.getRun(runId));});
@@ -209,6 +209,23 @@ export class TerminalView{
209
209
  if(this.runViews.size>30)this.runViews.delete(this.runViews.keys().next().value);
210
210
  unsubscribe?.();this.runViewer=null;this.tui.children=children;this.pending=previousPending;overlay?.hide();this.terminal.write(mouseOff);this.tui.requestRender(true);
211
211
  }
212
+ if(readPages)await this.viewLogPages(app,runId);
213
+ }
214
+ async viewLogPages(app,id){
215
+ let cursor=0;const previous=[];
216
+ for(;;){
217
+ const page=await app.readLogPage(id,{cursor,limit:65536});
218
+ await this.viewMarkdown(`完整日志 · ${cursor}–${page.nextCursor} / ${page.totalBytes} 字节`,page.text||'本页暂无内容');
219
+ const choice=await this.select('完整日志分页',[
220
+ ...(page.hasMore?[{value:'next',label:'下一页'}]:[]),
221
+ ...(previous.length?[{value:'previous',label:'上一页'}]:[]),
222
+ {value:'refresh',label:'刷新本页'},{value:'first',label:'回到首页'},{value:'back',label:'返回'},
223
+ ]);
224
+ if(choice==='back')return;
225
+ if(choice==='next'){previous.push(cursor);if(previous.length>1000)previous.shift();cursor=page.nextCursor;}
226
+ if(choice==='previous')cursor=previous.pop()??0;
227
+ if(choice==='first'){cursor=0;previous.length=0;}
228
+ }
212
229
  }
213
230
  async ask(message,component){
214
231
  fit(component);
@@ -0,0 +1,65 @@
1
+ export async function workflowMenu(session){
2
+ const {app,ui}=session;
3
+ while(true){
4
+ const workflows=await app.listWorkflows();
5
+ const choice=await ui.select('工作流 · 选择后编辑或执行',[
6
+ ...workflows.map(flow=>({value:`workflow:${flow.id}`,label:flow.title??flow.id,description:`${flow.id} · ${flow.steps.length} 步`})),
7
+ ...(!app.project.readOnly?workflows.map(flow=>({value:`duplicate:${flow.id}`,label:`复制 ${flow.title??flow.id} 为新工作流`,description:`从已保存版本 ${flow.id} 创建副本` })):[]),
8
+ ...(!app.project.readOnly?[{value:'new',label:'+ 新建工作流'}]:[]),
9
+ ...(session.dirty?[{value:'draft',label:`继续编辑草稿:${session.flow.title}`}]:[]),
10
+ {value:'back',label:'返回主面板'},
11
+ ]);
12
+ if(choice==='back')return;
13
+ try{
14
+ const id=choice.startsWith('workflow:')?choice.slice(9):null;
15
+ if(choice.startsWith('duplicate:')){
16
+ if(!await session.discard())continue;
17
+ await duplicateIntoSession(session,workflows.find(flow=>flow.id===choice.slice(10)));
18
+ }
19
+ else if(choice==='new'){if(await session.execute('/new')===false)continue;}
20
+ else if(choice!=='draft'&&session.flow?.id!==id){
21
+ if(!await session.discard())continue;
22
+ session.flow=structuredClone(workflows.find(flow=>flow.id===id));session.dirty=false;
23
+ }
24
+ if(await editWorkflow(session)==='running')return;
25
+ }catch(error){if(error.code!=='PROMPT_CANCELLED')throw error;}
26
+ }
27
+ }
28
+
29
+ async function editWorkflow(session){
30
+ const {app,ui}=session;
31
+ while(true){
32
+ const flow=session.current();
33
+ const choice=await ui.select(`${flow.title??flow.id} · ${flow.steps.length} 步 · ${session.dirty?'未保存':'已保存'}`, [
34
+ {value:'run',label:'运行工作流'},
35
+ ...(!app.project.readOnly?[
36
+ {value:'add',label:'添加步骤 / 控制流'}, {value:'steps',label:'编辑步骤、参数与顺序'},
37
+ {value:'outputs',label:'设置最终输出'},{value:'save',label:'保存工作流'},
38
+ {value:'duplicate',label:'复制为新工作流(包含当前未保存修改)'},
39
+ {value:'delete',label:'删除工作流(保留可恢复备份)'},
40
+ ]:[{value:'steps',label:'查看步骤'}]),
41
+ {value:'back',label:'返回工作流列表(保留草稿)'},
42
+ ]);
43
+ if(choice==='back')return;
44
+ try{
45
+ if(choice==='delete'){
46
+ if(!await ui.confirm(`删除 ${flow.title??flow.id}?已保存文件会移入备份,未保存修改将放弃。`))continue;
47
+ const exists=(await app.listWorkflows()).some(w=>w.id===flow.id);
48
+ const backup=exists?await app.removeWorkflow(flow.id):null;
49
+ session.flow=null;session.dirty=false;ui.show(backup?`工作流已移入备份:${backup}`:'未保存的草稿已删除。');return;
50
+ }
51
+ if(choice==='run'){
52
+ if(await session.startRun([],{draft:true}))return 'running';
53
+ }else if(choice==='duplicate')await duplicateIntoSession(session,flow);
54
+ else await session.execute('/'+choice);
55
+ }catch(error){if(error.code!=='PROMPT_CANCELLED')throw error;}
56
+ }
57
+ }
58
+
59
+ async function duplicateIntoSession(session,source){
60
+ const title=(await session.ui.input('新工作流名称',{value:`${source.title??source.id} 副本`,files:false})).trim();
61
+ if(!title)throw new Error('名称不能为空');
62
+ const {definition}=await session.app.duplicateWorkflow(source,{title});
63
+ session.flow=structuredClone(definition);session.dirty=false;
64
+ session.ui.show(`已创建副本:${definition.title}(${definition.id})。原工作流 ${source.id} 未修改。可在父工作流中依次加入原版和副本两个子流程步骤,比较输出和耗时。`);
65
+ }
@@ -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-devtools`。内部 workspace 包,随 `@yanglaofish/fishtools` 统一交付。
4
+
5
+ ## 职责与边界
6
+
7
+ 插件脚手架、validate、lint、离线文档发现与导出。不管理常驻宿主或用户运行状态。
8
+
9
+ 内部依赖:`@mytools/sdk`、`@mytools/fst-platform`、`@mytools/fst-plugin`。
10
+ ## 公开入口
11
+
12
+ | 导入路径 | 实现位置 |
13
+ | --- | --- |
14
+ | `@mytools/fst-devtools` | `./src/plugin-tools.js` |
15
+ | `@mytools/fst-devtools/documentation` | `./src/documentation.js` |
16
+ | `@mytools/fst-devtools/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/plugin-tools.test.js tests/plugin-lint.test.js tests/documentation.test.js
28
+ ```
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@mytools/fst-devtools",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "src/plugin-tools.js",
7
+ "exports": {
8
+ ".": "./src/plugin-tools.js",
9
+ "./documentation": "./src/documentation.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
+ "@mytools/fst-platform": "0.1.0",
24
+ "@mytools/fst-plugin": "0.1.0",
25
+ "semver": "7.7.4",
26
+ "validate-npm-package-name": "8.0.0",
27
+ "acorn": "8.19.0"
28
+ }
29
+ }
@@ -0,0 +1,67 @@
1
+ import path from 'node:path';
2
+ import {fileURLToPath} from 'node:url';
3
+ import {access,readFile,readdir,lstat,mkdir,copyFile,realpath} from 'node:fs/promises';
4
+
5
+ const topics=[
6
+ ['plugins','插件开发规范','docs/plugin-development.md'],
7
+ ['deployment','安装与部署','docs/deployment.md'],
8
+ ['control','控制流与运行观察规范','docs/superpowers/specs/2026-10-11-control-observation.md'],
9
+ ['services','服务插件示例','examples/service-plugins/README.md'],
10
+ ['example','可安装的零依赖动作插件','docs/examples/action-plugin/README.md'],
11
+ ['roadmap','路线图','docs/roadmap.md'],
12
+ ['modules','模块架构与维护','docs/modules.md'],
13
+ ['generation','大日志、开发重载与隔离文件探测','docs/kernel-generation.md'],
14
+ ['readme','Fishtools 使用说明','README.md'],
15
+ ];
16
+ // 保留文档链接所需的目录布局;参考代码仅供阅读,不是第二份运行时。
17
+ const assets=['README.md','LICENSE','THIRD_PARTY_NOTICES.md','docs','examples/service-plugins','plugins/examples','plugins/environment','plugins/web/README.md','packages/sdk','packages/core','packages/pack','packages/fst-logs','packages/fst-platform','packages/fst-workflow','packages/fst-plugin','packages/fst-workspace','packages/fst-devtools','packages/fst-cli','tests/application.test.js'];
18
+ const excluded=new Set(['node_modules','dist','.git','.cache','__pycache__','.pytest_cache','coverage','public']);
19
+ const inside=(parent,child)=>{const relative=path.relative(parent,child);return relative===''||(!relative.startsWith(`..${path.sep}`)&&relative!=='..'&&!path.isAbsolute(relative));};
20
+
21
+ export async function findDocumentationRoot(){
22
+ let directory=path.dirname(fileURLToPath(import.meta.url));
23
+ for(;;){
24
+ for(const candidate of [directory,path.join(directory,'manual')]){
25
+ try{await access(path.join(candidate,'docs/plugin-development.md'));await access(path.join(candidate,'LICENSE'));return candidate;}
26
+ catch(error){if(error.code!=='ENOENT'&&error.code!=='ENOTDIR')throw error;}
27
+ }
28
+ const parent=path.dirname(directory);if(parent===directory)break;directory=parent;
29
+ }
30
+ throw new Error('当前安装缺少离线文档,请重新安装完整的 Fishtools 发行包');
31
+ }
32
+
33
+ export async function listDocumentation(){
34
+ const root=await findDocumentationRoot();
35
+ return topics.map(([id,title,file])=>({id,title,path:path.join(root,file)}));
36
+ }
37
+
38
+ export async function readDocumentation(topic='plugins'){
39
+ const entry=(await listDocumentation()).find(item=>item.id===topic);
40
+ if(!entry)throw new Error(`未知文档主题:${topic}。运行 fst docs 查看可用主题`);
41
+ return {...entry,text:await readFile(entry.path,'utf8')};
42
+ }
43
+
44
+ export async function copyDocumentation(destination,{sourceRoot}={}){
45
+ const source=await realpath(sourceRoot??await findDocumentationRoot());
46
+ const target=path.resolve(destination);
47
+ try{await lstat(target);throw new Error(`文档导出目标已存在:${target}`);}catch(error){if(error.code!=='ENOENT')throw error;}
48
+ // 目标可位于仓库 dist 下,但不能嵌入即将复制的资料目录。
49
+ await mkdir(path.dirname(target),{recursive:true});
50
+ const canonicalTarget=path.join(await realpath(path.dirname(target)),path.basename(target));
51
+ for(const asset of assets)if(inside(path.join(source,asset),canonicalTarget))throw new Error('文档导出目标不能位于原资料目录内');
52
+ await mkdir(target);
53
+ const copy=async(from,to)=>{
54
+ const info=await lstat(from);
55
+ if(info.isSymbolicLink())throw new Error(`离线文档不允许符号链接:${from}`);
56
+ if(info.isDirectory()){
57
+ await mkdir(to,{recursive:true});
58
+ for(const entry of await readdir(from))if(!excluded.has(entry))await copy(path.join(from,entry),path.join(to,entry));
59
+ }else if(info.isFile()){
60
+ await mkdir(path.dirname(to),{recursive:true});await copyFile(from,to);
61
+ }else throw new Error(`离线文档包含不支持的文件:${from}`);
62
+ };
63
+ for(const asset of assets)await copy(path.join(source,asset),path.join(target,asset));
64
+ return target;
65
+ }
66
+
67
+ export async function exportDocumentation(destination){return copyDocumentation(destination);}