@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,136 @@
1
+ import path from 'node:path';
2
+ import {readFile,realpath} from 'node:fs/promises';
3
+ import {isBuiltin,createRequire} from 'node:module';
4
+ import {fileURLToPath,pathToFileURL} from 'node:url';
5
+ import {parse} from 'acorn';
6
+
7
+ const nonempty=value=>typeof value==='string'&&value.trim().length>0;
8
+ function inspectSchema(schema,location,warning,seen=new WeakSet()){
9
+ if(typeof schema==='boolean'){
10
+ warning('BOOLEAN_SCHEMA','布尔 schema 合法,但无法提供字段级表单提示;建议按业务需要描述字段',location);return;
11
+ }
12
+ if(!schema||typeof schema!=='object'||seen.has(schema))return;
13
+ seen.add(schema);
14
+ if(Array.isArray(schema)){schema.forEach((child,index)=>inspectSchema(child,`${location}[${index}]`,warning,seen));return;}
15
+ for(const key of ['properties','patternProperties','$defs','definitions','dependencies']){
16
+ for(const [name,child] of Object.entries(schema[key]??{})){
17
+ const at=`${location}.${key}.${name}`;
18
+ if(['properties','patternProperties'].includes(key)&&!nonempty(child?.title))warning('MISSING_TITLE','字段缺少非空 title',`${at}.title`);
19
+ inspectSchema(child,at,warning,seen);
20
+ }
21
+ }
22
+ for(const key of ['items','additionalItems','contains','propertyNames','additionalProperties','not','if','then','else']){
23
+ // additionalProperties 等布尔约束是正常用法,不作为表单质量提示。
24
+ if(typeof schema[key]==='object')inspectSchema(schema[key],`${location}.${key}`,warning,seen);
25
+ }
26
+ for(const key of ['allOf','anyOf','oneOf','prefixItems']){
27
+ if(Array.isArray(schema[key]))schema[key].forEach((child,index)=>inspectSchema(child,`${location}.${key}[${index}]`,warning,seen));
28
+ }
29
+ }
30
+
31
+ export function inspectMetadata(plugin,warning){
32
+ for(const collection of ['actions','commands']){
33
+ for(const [name,item] of Object.entries(plugin[collection]??{})){
34
+ if(!item||typeof item!=='object')continue;
35
+ const location=`plugin.${collection}.${name}`;
36
+ // 动作别名命令可以复用动作元信息。
37
+ const target=collection==='commands'&&item.action?plugin.actions?.[item.action]:undefined;
38
+ for(const key of ['title','description']){
39
+ if(!nonempty(item[key]??target?.[key]))warning(`MISSING_${key.toUpperCase()}`,`缺少非空 ${key}`,`${location}.${key}`);
40
+ }
41
+ if(!target)for(const key of ['input','output'])inspectSchema(item[key],`${location}.${key}`,warning);
42
+ }
43
+ }
44
+ }
45
+
46
+ function walk(node,visit){
47
+ if(!node||typeof node!=='object')return;
48
+ if(typeof node.type==='string')visit(node);
49
+ for(const value of Object.values(node)){
50
+ if(Array.isArray(value))for(const child of value)walk(child,visit);
51
+ else if(value&&typeof value==='object')walk(value,visit);
52
+ }
53
+ }
54
+
55
+ function bindingNames(pattern,result){
56
+ if(!pattern)return;
57
+ if(pattern.type==='Identifier')result.set(pattern.name,(result.get(pattern.name)??0)+1);
58
+ else if(pattern.type==='RestElement')bindingNames(pattern.argument,result);
59
+ else if(pattern.type==='AssignmentPattern')bindingNames(pattern.left,result);
60
+ else if(pattern.type==='ObjectPattern')for(const p of pattern.properties)bindingNames(p.type==='RestElement'?p.argument:p.value,result);
61
+ else if(pattern.type==='ArrayPattern')for(const p of pattern.elements)bindingNames(p,result);
62
+ }
63
+
64
+ function sourceReferences(ast,commonjs,warning,location){
65
+ const bindings=new Map(),references=[],factories=new Set(),loaders=new Set();
66
+ walk(ast,node=>{
67
+ if(node.type==='VariableDeclarator')bindingNames(node.id,bindings);
68
+ if(['FunctionDeclaration','FunctionExpression','ArrowFunctionExpression'].includes(node.type)){
69
+ bindingNames(node.id,bindings);for(const p of node.params)bindingNames(p,bindings);
70
+ }
71
+ if(node.type==='ClassDeclaration'||node.type==='ClassExpression')bindingNames(node.id,bindings);
72
+ if(node.type==='CatchClause')bindingNames(node.param,bindings);
73
+ if(node.type==='ImportDeclaration')for(const p of node.specifiers){
74
+ bindingNames(p.local,bindings);
75
+ if(['node:module','module'].includes(node.source.value)&&p.imported?.name==='createRequire')factories.add(p.local.name);
76
+ }
77
+ // 重赋值也跳过,避免把可变的局部函数当作 require。
78
+ if(node.type==='AssignmentExpression')bindingNames(node.left,bindings);
79
+ });
80
+ walk(ast,node=>{
81
+ const base=node.init?.arguments?.[0];
82
+ if(node.type==='VariableDeclarator'&&node.id.type==='Identifier'&&bindings.get(node.id.name)===1&&node.init?.type==='CallExpression'&&
83
+ node.init.callee.type==='Identifier'&&factories.has(node.init.callee.name)&&bindings.get(node.init.callee.name)===1&&
84
+ base?.type==='MemberExpression'&&!base.computed&&base.property.name==='url'&&base.object.type==='MetaProperty'&&base.object.meta.name==='import')loaders.add(node.id.name);
85
+ });
86
+ // 保守跳过文件内重名绑定;不把局部对象当作 Node 全局对象。
87
+ walk(ast,node=>{
88
+ const at=`${location}:${node.loc.start.line}:${node.loc.start.column+1}`;
89
+ if(['ImportDeclaration','ExportNamedDeclaration','ExportAllDeclaration','ImportExpression'].includes(node.type)&&typeof node.source?.value==='string'){
90
+ references.push({specifier:node.source.value,at,commonjs:false});
91
+ }
92
+ if(node.type!=='CallExpression')return;
93
+ const callee=node.callee;
94
+ if(callee.type==='Identifier'&&((commonjs&&!bindings.has('require')&&callee.name==='require')||loaders.has(callee.name))&&typeof node.arguments[0]?.value==='string'){
95
+ references.push({specifier:node.arguments[0].value,at,commonjs:true});
96
+ }
97
+ if(!bindings.has('console')&&callee.type==='MemberExpression'&&callee.object.type==='Identifier'&&callee.object.name==='console'&&
98
+ (callee.computed?callee.property.value:callee.property.name)==='log')warning('CONSOLE_LOG','直接 console.log 可能干扰终端或 JSON 输出;动作和命令内请使用上下文 log / print',at);
99
+ });
100
+ return references;
101
+ }
102
+
103
+ export async function inspectSources(directory,pkg,warning){
104
+ const root=await realpath(directory),seen=new Set(),declared=new Set([pkg.name,...Object.keys(pkg.dependencies??{}),...Object.keys(pkg.peerDependencies??{}),...Object.keys(pkg.optionalDependencies??{})]);
105
+ const within=file=>{const relative=path.relative(root,file);return relative!== '..'&&!relative.startsWith(`..${path.sep}`)&&!path.isAbsolute(relative)&&!relative.split(path.sep).includes('node_modules');};
106
+ async function scan(file){
107
+ let actual;
108
+ try{actual=await realpath(file);}catch(cause){warning('SOURCE_UNREADABLE',`无法扫描本地模块:${cause.message}`,path.relative(root,file));return;}
109
+ if(!within(actual)){warning('SOURCE_OUTSIDE_PACKAGE','跳过包目录外或 node_modules 中的源码',path.relative(root,file));return;}
110
+ if(seen.has(actual))return;seen.add(actual);
111
+ const extension=path.extname(actual);
112
+ if(extension==='.json'||extension==='.node')return;
113
+ if(!['.js','.mjs','.cjs'].includes(extension)){warning('SOURCE_UNSUPPORTED','仅扫描 .js/.mjs/.cjs 源码',path.relative(root,actual));return;}
114
+ let ast;
115
+ const location=path.relative(root,actual),commonjs=extension==='.cjs';
116
+ try{ast=parse(await readFile(actual,'utf8'),{ecmaVersion:'latest',sourceType:commonjs?'commonjs':'module',locations:true});}
117
+ catch(cause){warning('SOURCE_PARSE_FAILED',`无法完成源码检查:${cause.message}`,location);return;}
118
+ for(const ref of sourceReferences(ast,commonjs,warning,location)){
119
+ const spec=ref.specifier;
120
+ if(isBuiltin(spec))continue;
121
+ if(spec.startsWith('.')){
122
+ try{
123
+ const target=ref.commonjs?createRequire(actual).resolve(spec):fileURLToPath(new URL(spec,pathToFileURL(actual)));
124
+ await scan(target);
125
+ }catch(cause){warning('SOURCE_UNREADABLE',`无法解析本地模块 ${spec}:${cause.message}`,ref.at);}
126
+ }else if(spec.startsWith('#')||spec.includes(':')||path.isAbsolute(spec)){
127
+ warning('DEPENDENCY_UNCHECKED',`未检查特殊模块引用 ${spec}`,ref.at);
128
+ }else{
129
+ const name=spec.startsWith('@')?spec.split('/').slice(0,2).join('/'):spec.split('/')[0];
130
+ if(!declared.has(name))warning('UNDECLARED_DEPENDENCY',`运行时引用 ${name},但 package.json 的 dependencies / optionalDependencies / peerDependencies 未声明它`,ref.at);
131
+ }
132
+ }
133
+ }
134
+ if(typeof (pkg.main??'index.js')!=='string'){warning('SOURCE_UNREADABLE','package.json.main 必须是入口路径','package.json.main');return;}
135
+ await scan(path.resolve(root,pkg.main??'index.js'));
136
+ }
@@ -0,0 +1,152 @@
1
+ import path from 'node:path';
2
+ import {readFile,writeFile,mkdir,mkdtemp,rename,rm,access} from 'node:fs/promises';
3
+ import semver from 'semver';
4
+ import validateNpmPackageName from 'validate-npm-package-name';
5
+ import {SDK_VERSION} from '@mytools/sdk';
6
+ import {loadPlugin} from '@mytools/fst-plugin/loader';
7
+ import {validateDeclarations,validateCommands} from '@mytools/fst-plugin/contracts';
8
+ import {assertSchema} from '@mytools/sdk/schema';
9
+ import {platformWarnings} from '@mytools/fst-platform/environments';
10
+
11
+ const validNpmName=name=>typeof name==='string'&&validateNpmPackageName(name).validForNewPackages===true;
12
+ const hasOwn=(value,key)=>Object.hasOwn(value,key);
13
+
14
+ function inspectRequired(schema,location,warning,seen=new WeakSet()){
15
+ if(!schema||typeof schema!=='object'||seen.has(schema))return;
16
+ seen.add(schema);
17
+ if(Array.isArray(schema.required)){
18
+ schema.required.forEach((name,index)=>{
19
+ if(typeof name==='string'&&!hasOwn(schema.properties??{},name))warning('REQUIRED_UNDECLARED',`必填字段 ${name} 未在同层 properties 声明;请检查 required 所在层级`,`${location}.required[${index}]`);
20
+ });
21
+ }
22
+ for(const key of ['properties','patternProperties','$defs','definitions']){
23
+ for(const [name,child] of Object.entries(schema[key]??{}))inspectRequired(child,`${location}.${key}.${name}`,warning,seen);
24
+ }
25
+ for(const key of ['items','additionalProperties','not','if','then','else'])inspectRequired(schema[key],`${location}.${key}`,warning,seen);
26
+ for(const key of ['allOf','anyOf','oneOf','prefixItems']){
27
+ if(Array.isArray(schema[key]))schema[key].forEach((child,index)=>inspectRequired(child,`${location}.${key}[${index}]`,warning,seen));
28
+ }
29
+ }
30
+
31
+ const validationNotes=()=>[{code:'RUNTIME_OUTPUT_UNVERIFIED',message:'输出 required 字段是否实际返回无法静态确认;请按发布清单执行一次工作流验证。'}];
32
+
33
+ export async function validatePluginDirectory(directory){
34
+ return {...await inspectPluginDirectory(directory),notes:validationNotes()};
35
+ }
36
+
37
+ export async function lintPluginDirectory(directory){
38
+ const quality=await import('./plugin-quality.js');
39
+ return {...await inspectPluginDirectory(directory,quality),notes:[...validationNotes(),
40
+ {code:'STATIC_SCAN_LIMITS',message:'源码检查仅覆盖入口可达的本地 JavaScript 和字面量依赖;动态加载、别名调用和局部重名可能无法识别。质量警告不会修改文件,也不代表运行结果或安全保证。'},
41
+ ]};
42
+ }
43
+
44
+ async function inspectPluginDirectory(directory,quality){
45
+ const root=path.resolve(directory),diagnostics=[];
46
+ const report=(severity,code,message,location)=>diagnostics.push({severity,code,message,...(location?{path:location}:{})});
47
+ const error=(code,message,location)=>report('error',code,message,location);
48
+ const warning=(code,message,location)=>report('warning',code,message,location);
49
+ let pkg,plugin;
50
+ try{pkg=JSON.parse(await readFile(path.join(root,'package.json'),'utf8'));}
51
+ catch(cause){error('PACKAGE_READ_FAILED',`无法读取 package.json:${cause.message}`,'package.json');return {valid:false,plugin:{},diagnostics};}
52
+ if(!pkg||typeof pkg!=='object'||Array.isArray(pkg)){error('INVALID_PACKAGE','package.json 必须是对象','package.json');return {valid:false,plugin:{},diagnostics};}
53
+ if(!validNpmName(pkg.name))error('INVALID_PACKAGE_NAME','package.json 的 name 必须是合法 npm 包名','package.json.name');
54
+ if(!semver.valid(pkg.version))error('INVALID_PACKAGE_VERSION','package.json 的 version 必须是 semver 版本','package.json.version');
55
+ if(pkg.type!=='module')error('ESM_REQUIRED','插件必须声明 type: module','package.json.type');
56
+ if(quality)await quality.inspectSources(root,pkg,warning);
57
+ try{({plugin}=await loadPlugin('.',root));}
58
+ catch(cause){error('PLUGIN_LOAD_FAILED',`无法导入插件:${cause.message}`,'package.json.main');return {valid:false,plugin:{},diagnostics};}
59
+ if(!plugin||typeof plugin!=='object'||Array.isArray(plugin)){error('INVALID_PLUGIN','默认导出必须是插件定义对象','plugin');return {valid:false,plugin:{},diagnostics};}
60
+ const identity={id:plugin.id,version:plugin.version};
61
+ if(quality)quality.inspectMetadata(plugin,warning);
62
+ if(!validNpmName(plugin.id))error('INVALID_PLUGIN_ID','插件 id 必须是合法 npm 包名','plugin.id');
63
+ else if(plugin.id!==pkg.name)error('PLUGIN_ID_MISMATCH',`插件 id ${plugin.id} 与包名 ${pkg.name} 不一致`,'plugin.id');
64
+ if(!semver.valid(plugin.version))error('INVALID_PLUGIN_VERSION','插件 version 必须是 semver 版本','plugin.version');
65
+ else if(plugin.version!==pkg.version)error('PLUGIN_VERSION_MISMATCH',`插件 version ${plugin.version} 与包版本 ${pkg.version} 不一致`,'plugin.version');
66
+ if(typeof plugin.sdk!=='string'||!semver.validRange(plugin.sdk))error('INVALID_SDK_RANGE','插件 sdk 必须明确声明有效的 semver 范围','plugin.sdk');
67
+ else if(!semver.satisfies(SDK_VERSION,plugin.sdk))error('SDK_INCOMPATIBLE',`插件 SDK 范围 ${plugin.sdk} 不包含当前 ${SDK_VERSION}`,'plugin.sdk');
68
+ for(const [key,location] of [['platforms','plugin.platforms'],['architectures','plugin.architectures']]){
69
+ if(plugin[key]===undefined)warning(`${key.toUpperCase()}_UNDECLARED`,`${key} 未声明,请明确插件支持范围`,location);
70
+ }
71
+ try{validateDeclarations(plugin);}catch(cause){error('INVALID_DECLARATION',cause.message,cause.details?.path??'plugin');}
72
+ if((plugin.runtime||Object.keys(plugin.services??{}).length||Object.values(plugin.commands??{}).some(command=>!command.action))&&!plugin.requirements?.privilege)
73
+ warning('MISSING_PRIVILEGE_DECLARATION','插件服务或独立命令未声明权限要求;请说明普通用户是否可运行','plugin.requirements.privilege');
74
+ for(const [name,action] of Object.entries(plugin.actions??{}))if(!action?.requirements?.privilege&&!plugin.requirements?.privilege)
75
+ warning('MISSING_PRIVILEGE_DECLARATION',`动作 ${name} 未声明权限要求;请说明普通用户是否可运行`,`plugin.actions.${name}.requirements.privilege`);
76
+ if(['platforms','architectures'].every(key=>plugin[key]===undefined||Array.isArray(plugin[key]))) {
77
+ for(const message of platformWarnings(plugin))warning('PLATFORM_MISMATCH',message,message.includes('架构')?'plugin.architectures':'plugin.platforms');
78
+ }
79
+ try{validateCommands(plugin,new Set());}catch(cause){error('INVALID_COMMAND',cause.message,'plugin.commands');}
80
+ for(const [location,schema] of [['plugin.config',plugin.config]]){
81
+ if(schema!==undefined){try{assertSchema(schema,location);inspectRequired(schema,location,warning);}catch(cause){error('INVALID_SCHEMA',cause.message,location);}}
82
+ }
83
+ if(plugin.actions!==undefined&&(!plugin.actions||typeof plugin.actions!=='object'||Array.isArray(plugin.actions)))error('INVALID_ACTIONS','actions 必须是动作字典','plugin.actions');
84
+ for(const [name,action] of Object.entries(plugin.actions??{})){
85
+ const location=`plugin.actions.${name}`;
86
+ if(!action||typeof action!=='object'){error('INVALID_ACTION','动作必须是对象',location);continue;}
87
+ if(typeof action.run!=='function')error('MISSING_RUN','动作必须声明 run 函数',`${location}.run`);
88
+ for(const key of ['input','output']){
89
+ const schema=action[key],at=`${location}.${key}`;
90
+ if(schema===undefined){error('MISSING_SCHEMA',`动作 ${name} 必须明确声明 ${key} schema`,at);continue;}
91
+ try{assertSchema(schema,at);inspectRequired(schema,at,warning);}catch(cause){error('INVALID_SCHEMA',cause.message,at);}
92
+ }
93
+ }
94
+ if(plugin.commands!==undefined&&(!plugin.commands||typeof plugin.commands!=='object'||Array.isArray(plugin.commands)))error('INVALID_COMMANDS','commands 必须是命令字典','plugin.commands');
95
+ for(const [name,command] of Object.entries(plugin.commands??{})){
96
+ if(!command||typeof command!=='object'||command.action)continue;
97
+ for(const key of ['input','output']){
98
+ const schema=command[key],at=`plugin.commands.${name}.${key}`;
99
+ if(schema===undefined){error('MISSING_SCHEMA',`命令 ${name} 必须明确声明 ${key} schema`,at);continue;}
100
+ try{assertSchema(schema,at);inspectRequired(schema,at,warning);}catch(cause){error('INVALID_SCHEMA',cause.message,at);}
101
+ }
102
+ }
103
+ if(plugin.dependencies!==undefined){
104
+ if(!plugin.dependencies||typeof plugin.dependencies!=='object'||Array.isArray(plugin.dependencies))error('INVALID_DEPENDENCIES','dependencies 必须是插件 ID 到版本范围的字典','plugin.dependencies');
105
+ else for(const [name,range] of Object.entries(plugin.dependencies)){
106
+ if(!validNpmName(name))error('INVALID_DEPENDENCY_ID',`依赖插件 ID 无效:${name}`,`plugin.dependencies.${name}`);
107
+ if(typeof range!=='string'||!semver.validRange(range))error('INVALID_DEPENDENCY_RANGE',`依赖 ${name} 的版本范围无效`,`plugin.dependencies.${name}`);
108
+ }
109
+ }
110
+ if(plugin.runtime!==undefined){
111
+ const runtime=plugin.runtime;
112
+ if(!runtime||typeof runtime!=='object'||Array.isArray(runtime))error('INVALID_RUNTIME','runtime 必须是声明对象','plugin.runtime');
113
+ else{
114
+ if(typeof runtime.apply!=='function')error('INVALID_RUNTIME','runtime 必须提供 apply 函数','plugin.runtime.apply');
115
+ for(const key of ['provides','inject']){
116
+ const names=runtime[key]??[];
117
+ if(!Array.isArray(names)||names.some(name=>typeof name!=='string'||!/^\w[\w.-]*$/.test(name))||new Set(names).size!==names.length)
118
+ error('INVALID_RUNTIME',`runtime.${key} 必须是不重复的有效服务名数组`,`plugin.runtime.${key}`);
119
+ }
120
+ if(Array.isArray(runtime.provides)&&Array.isArray(runtime.inject)&&runtime.inject.some(name=>runtime.provides.includes(name)))
121
+ error('RUNTIME_SELF_DEPENDENCY','runtime.inject 不能依赖本插件提供的服务','plugin.runtime.inject');
122
+ }
123
+ }
124
+ return {valid:!diagnostics.some(item=>item.severity==='error'),plugin:identity,diagnostics};
125
+ }
126
+
127
+ function scaffold(name){
128
+ const packageJson={name,version:'1.0.0',type:'module',main:'index.js',files:['index.js','test','README.md','LICENSE'],scripts:{test:'node --test'},license:'MIT',engines:{node:'>=24.15.0'}};
129
+ const plugin=`// 直接导出插件定义,无需安装内部 SDK。\nexport default {\n id:${JSON.stringify(name)},version:'1.0.0',sdk:'^0.1.0',\n title:'嵌套输入示例',description:'演示业务结果、嵌套输入和判定预设。',\n platforms:['win32','linux','darwin'],architectures:['x64','arm64'],\n actions:{\n check:{\n title:'检查请求',description:'根据嵌套请求生成业务结果。',\n input:{type:'object',properties:{request:{type:'object',properties:{name:{type:'string',minLength:1,title:'姓名'},enabled:{type:'boolean',title:'启用'}},required:['name','enabled'],additionalProperties:false}},required:['request'],additionalProperties:false},\n output:{type:'object',properties:{ok:{type:'boolean'},message:{type:'string'}},required:['ok','message'],additionalProperties:false},\n conditions:{passed:{title:'业务通过',path:'ok',op:'eq',value:true}},\n requirements:{privilege:{win32:{level:'user'},linux:{level:'user'}}},\n async run({request},{signal}={}){\n signal?.throwIfAborted?.();\n return request.enabled?{ok:true,message:\`你好,\${request.name}\`}:{ok:false,message:'已禁用'};\n },\n },\n },\n};\n`;
130
+ const testSource=`import test from 'node:test';\nimport assert from 'node:assert/strict';\nimport plugin from '../index.js';\n\ntest('嵌套参数和业务判定',async()=>{\n assert.deepEqual(await plugin.actions.check.run({request:{name:'阿青',enabled:true}}),{ok:true,message:'你好,阿青'});\n assert.deepEqual(await plugin.actions.check.run({request:{name:'阿青',enabled:false}}),{ok:false,message:'已禁用'});\n});\n`;
131
+ const readme=`# ${name}\n\n可独立安装的 Fishtools 插件示例,不依赖尚未公开发布的 SDK。\n\n## 使用\n\n在本目录运行 \`npm test\`,并运行 \`fst plugin validate .\` 检查声明。安装后可将 \`${name}:check\` 加入工作流。\n\n输入示例:\n\n\`\`\`json\n{"request":{"name":"阿青","enabled":true}}\n\`\`\`\n\n输出含 \`ok\` 与 \`message\`。\`ok:false\` 表示业务未通过,但动作仍正常返回;\`conditions.passed\` 可供工作流分支使用。请同步修改 package.json 和 index.js 中的名称、版本,并根据实际支持情况调整 platforms / architectures。\n`;
132
+ const license=`MIT License\n\nCopyright (c) ${new Date().getFullYear()} ${name}\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the "Software"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n`;
133
+ return {'package.json':`${JSON.stringify(packageJson,null,2)}\n`,'index.js':plugin,'README.md':readme,'LICENSE':license,'test/plugin.test.js':testSource};
134
+ }
135
+
136
+ export async function createPlugin(name,directory=path.resolve(name)){
137
+ if(!validNpmName(name))throw new Error('插件名称必须是合法 npm 包名(可带 @scope)');
138
+ const target=path.resolve(directory),parent=path.dirname(target);
139
+ await mkdir(parent,{recursive:true});
140
+ try{await access(target);throw new Error(`目标目录已存在:${target}`);}catch(cause){if(cause.code!=='ENOENT')throw cause;}
141
+ const staging=await mkdtemp(path.join(parent,'.plugin-create-'));
142
+ try{
143
+ for(const [file,contents] of Object.entries(scaffold(name))){
144
+ const destination=path.join(staging,file);
145
+ await mkdir(path.dirname(destination),{recursive:true});
146
+ await writeFile(destination,contents,{flag:'wx'});
147
+ }
148
+ try{await access(target);throw new Error(`目标目录已存在:${target}`);}catch(cause){if(cause.code!=='ENOENT')throw cause;}
149
+ await rename(staging,target);
150
+ }catch(cause){await rm(staging,{recursive:true,force:true});throw cause;}
151
+ return {directory:target};
152
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mytools contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,56 @@
1
+ # 日志模块
2
+
3
+ 包名:`@mytools/fst-logs`。内部 workspace 包,随 `@yanglaofish/fishtools` 统一交付。
4
+
5
+ ## 职责与边界
6
+
7
+ 运行日志写入器、串行落盘、有界事件缓存、快照、历史分页、保留和体积统计。运行调度由 core 负责。
8
+
9
+ 内部依赖:`@mytools/sdk`。
10
+ ## 公开入口
11
+
12
+ | 导入路径 | 实现位置 |
13
+ | --- | --- |
14
+ | `@mytools/fst-logs` | `./src/run-logs.js` |
15
+ | `@mytools/fst-logs/markdown` | `./src/markdown-log.js` |
16
+ | `@mytools/fst-logs/package.json` | `./package.json` |
17
+
18
+ 仅依赖 exports 声明的入口,禁止消费者跨包读取 src/bin 私有文件。入口路径由本模块维护,修改协议时同步消费方回归。
19
+
20
+ ## 日志策略与完整记录
21
+
22
+ `createRunLogs(directory,{projectDir,allowLegacy=false,...policy})` 接受以下字节预算:
23
+
24
+ | 字段 | 默认值 | 含义 |
25
+ | --- | --- | --- |
26
+ | maxEvents | 1000 | 内存尾部事件条数 |
27
+ | maxMemoryBytes | 1048576 | 尾部事件 JSON 数组总字节,至少 2 |
28
+ | maxEventBytes | 65536 | 单条预览 JSON 字节,至少 256 |
29
+ | maxPendingBytes | 8388608 | 尚未完成的写入任务字节预算 |
30
+ | maxDiskBytes | null | 每次运行所有约定日志文件总字节;null 不主动限制磁盘 |
31
+
32
+ `start(initial,{onEvent})` 返回 `append(event)`、`finish(state)`、`save(state)`、`snapshot(state)` 与原有 `markdown(state)`。完整事件(含 output)持续顺序写入 `events.jsonl`,完整 Markdown 持续追加到 `run.md`。完成时额外写入完整可解析的 `run.result` JSONL 记录;此记录不重复广播或加入缓存。运行 ID 的归属在开始时写入 `run.meta.json`,结束信息写入 `result.json`。
33
+
34
+ `onEvent` 与 `snapshot().events` 提供有界预览,巨大字段带 `eventTruncated:true`。每条追加事件有递增 `seq`,缓存被淘汰后消费者仍可识别新事件。快照增加 `eventCount`(包括最终 run.result)、`eventBytes` 和 `eventsTruncated`。快照中的业务 input/output 保持完整,不能以事件摘要替代工作流数据。历史 result.json 对巨大字段保存摘要及对应 `outputTruncated`、`inputTruncated` 等标记;完整最终 output 可从 JSONL 的 run.result 或 Markdown 取得。小字段保持原结构。
35
+
36
+ 快照与历史元数据另有 `stepSummaries`、`stepSummaryBytes`、`stepsTruncated`。步骤摘要按 callPath(缺少时按 stepId)保留最新状态,仅包含类型、定位、动作、时间、序号、耗时及有界原因/错误,不缓存 input/output。步骤摘要使用独立的 maxEvents/maxMemoryBytes 预算,因此两份缓存合计最多为两倍 maxMemoryBytes;单条步骤摘要仍受 maxEventBytes 限制。普通日志淘汰不影响步骤状态;步骤本身超预算淘汰时 stepsTruncated 为 true,展示端应把无法确认的步骤显示为未知状态。
37
+
38
+ 队列预算在追加时检查。未等待 append 的插件同样受到预算约束;超限返回 `LOG_QUEUE_LIMIT` 拒绝,并使 finish 失败。拒绝已安装内部处理,调用方仍可 await 观察失败,不会产生未处理拒绝。`LOG_DISK_LIMIT` 表示可选磁盘预算不足,拒绝完整追加而不静默截断。磁盘预算与队列预算都计算 JSONL 和 Markdown。建议插件等待 append,以便磁盘写入形成背压。
39
+
40
+ `read`/`list` 仅读取受限的 result.json 元数据,运行中的 read 回退到归属元数据;不加载 JSONL 或 Markdown 正文。巨大旧 result.json 超过读取预算时,read 明确返回 `LOG_METADATA_LIMIT`,list 跳过该记录。旧版本缺少归属信息时仅在 `allowLegacy:true` 下可读。
41
+
42
+ ## 分页与文件保护
43
+
44
+ `page(id,{cursor=0,limit=65536})` 返回 `{text,cursor,nextCursor,hasMore,totalBytes}`,cursor 为 UTF-8 字节位置。limit 至少 4,最大 262144;页尾退回完整字符边界,下一页使用 nextCursor。落在字符内部、负数或超出正文长度的 cursor 返回 `LOG_CURSOR_INVALID`。运行中的分页同样验证项目归属;旧日志没有 Markdown 时使用有界元数据生成正文后分页。分页只读取当前页,原 `markdown(id)` 保留完整读取兼容接口,查看巨大日志应使用 page。
45
+
46
+ 拒绝日志根目录、运行目录或正文文件链接,已有同 ID 日志拒绝覆盖。体积统计和保留策略仅涉及 `run.md`、`events.jsonl`、`result.json`、`run.meta.json` 四个约定文件;业务文件和子目录保持保留。统计不递归、不读取正文。普通文件系统检查不能提供针对恶意并发路径替换的操作系统安全沙箱。
47
+
48
+ ## 维护与验证
49
+
50
+ 本模块负责上述职责的实现、文档与回归;跨模块接口调整需要同时检查生产者与消费者。具体维护人员尚未指定。
51
+
52
+ 在仓库根目录执行:
53
+
54
+ ```sh
55
+ node --test tests/log-streaming.test.js tests/modules.test.js tests/module-interfaces.test.js tests/run-logs.test.js tests/live-output.test.js
56
+ ```
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@mytools/fst-logs",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "src/run-logs.js",
7
+ "exports": {
8
+ ".": "./src/run-logs.js",
9
+ "./markdown": "./src/markdown-log.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "src",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "license": "MIT",
18
+ "engines": {
19
+ "node": ">=24.15.0"
20
+ },
21
+ "dependencies": {
22
+ "@mytools/sdk": "0.1.0"
23
+ }
24
+ }
@@ -0,0 +1,91 @@
1
+ import {FlowError} from '@mytools/sdk';
2
+
3
+ export const defaults={maxEvents:1000,maxMemoryBytes:1024*1024,maxEventBytes:64*1024,maxPendingBytes:8*1024*1024,maxDiskBytes:null};
4
+ export function logPolicy(options){
5
+ const policy={};
6
+ for(const [key,value] of Object.entries(defaults)){
7
+ policy[key]=options[key]??value;
8
+ if(key==='maxDiskBytes'&&policy[key]===null)continue;
9
+ if(!Number.isSafeInteger(policy[key])||policy[key]<1)throw new FlowError(`日志 ${key} 必须为正整数`);
10
+ }
11
+ if(policy.maxEventBytes<256)throw new FlowError('日志 maxEventBytes 至少为 256 字节');
12
+ if(policy.maxMemoryBytes<2)throw new FlowError('日志 maxMemoryBytes 至少为 2 字节');
13
+ return policy;
14
+ }
15
+
16
+ export function utf8Prefix(text,bytes){
17
+ // 先按字符截取,使临时 Buffer 也受预算限制。
18
+ const buffer=Buffer.from(text.slice(0,bytes));
19
+ let end=Math.min(bytes,buffer.length);
20
+ if(end<buffer.length)while(end>0&&(buffer[end]&0xc0)===0x80)end--;
21
+ return buffer.subarray(0,end).toString('utf8');
22
+ }
23
+
24
+ // 估算完整 JSON 的字节量,超预算立即停止;避免为超大消息先复制整份字符串。
25
+ export function jsonSize(value,limit){
26
+ const seen=new Set();let bytes=0;
27
+ function count(value){
28
+ if(bytes>limit)return;
29
+ if(typeof value==='string'){
30
+ bytes+=2;
31
+ for(let i=0;i<value.length&&bytes<=limit;i++){
32
+ const code=value.charCodeAt(i);
33
+ if(code<32)bytes+=([8,9,10,12,13].includes(code)?2:6);
34
+ else if(code===34||code===92)bytes+=2;
35
+ else if(code<128)bytes++;
36
+ else if(code<2048)bytes+=2;
37
+ else if(code>=0xd800&&code<=0xdbff&&value.charCodeAt(i+1)>=0xdc00&&value.charCodeAt(i+1)<=0xdfff){bytes+=4;i++;}
38
+ else bytes+=(code>=0xd800&&code<=0xdfff)?6:3;
39
+ }
40
+ }else if(value===null)bytes+=4;
41
+ else if(typeof value==='number')bytes+=String(value).length;
42
+ else if(typeof value==='boolean')bytes+=value?4:5;
43
+ else if(typeof value==='bigint')throw new TypeError('日志不能序列化 BigInt');
44
+ else if(value&&typeof value==='object'){
45
+ if(seen.has(value))throw new TypeError('日志不能序列化循环引用');
46
+ seen.add(value);bytes+=2;
47
+ if(Array.isArray(value)){for(const item of value){count(item===undefined?null:item);bytes++;if(bytes>limit)break;}}
48
+ else for(const key of Object.keys(value)){
49
+ if(value[key]===undefined||typeof value[key]==='function'||typeof value[key]==='symbol')continue;
50
+ count(key);bytes+=2;count(value[key]);if(bytes>limit)break;
51
+ }
52
+ seen.delete(value);
53
+ }
54
+ }
55
+ count(value);return bytes;
56
+ }
57
+
58
+ export function preview(value,maxBytes){
59
+ if(jsonSize(value,maxBytes)<=maxBytes){const text=JSON.stringify(value);if(Buffer.byteLength(text)<=maxBytes)return {value:JSON.parse(text),truncated:false};}
60
+ let remaining=Math.max(0,maxBytes-64);
61
+ function copy(item,depth=0){
62
+ if(remaining<16||depth>12)return undefined;
63
+ if(typeof item==='string'){
64
+ const size=jsonSize(item,remaining);
65
+ if(size<=remaining){remaining-=size;return item;}
66
+ const text=utf8Prefix(item,Math.max(0,Math.floor((remaining-2)/6)));
67
+ remaining-=Buffer.byteLength(JSON.stringify(text));return text;
68
+ }
69
+ if(item===null||typeof item==='number'||typeof item==='boolean'){remaining-=16;return item;}
70
+ if(!item||typeof item!=='object')return undefined;
71
+ remaining-=2;const result=Array.isArray(item)?[]:{};
72
+ for(const key of Object.keys(item)){
73
+ const cost=Buffer.byteLength(JSON.stringify(key))+2;
74
+ if(cost+16>remaining)break;remaining-=cost;
75
+ const child=copy(item[key],depth+1);if(child!==undefined)result[key]=child;
76
+ }
77
+ return result;
78
+ }
79
+ return {value:copy(value)??null,truncated:true};
80
+ }
81
+
82
+ export function eventPreview(event,maxBytes){
83
+ // 增量序号、类型和归属优先,消息随后;巨大正文不能挤掉路由信息。
84
+ const ordered={seq:event.seq,type:event.type,runId:event.runId,time:event.time};
85
+ for(const key of ['callPath','stepId','action','stream'])if(event[key]!==undefined)ordered[key]=event[key];
86
+ if(event.message!==undefined)ordered.message=event.message;
87
+ for(const key of Object.keys(event))if(!(key in ordered))ordered[key]=event[key];
88
+ const result=preview(ordered,maxBytes);
89
+ if(result.truncated)result.value.eventTruncated=true;
90
+ return result.value;
91
+ }
@@ -0,0 +1,55 @@
1
+ const names={
2
+ 'run.started':'运行开始','run.succeeded':'运行成功','run.failed':'运行失败','run.cancelled':'运行取消','run.stopped':'运行停止',
3
+ 'workflow.started':'工作流开始','workflow.succeeded':'工作流完成','workflow.stopped':'工作流停止',
4
+ 'step.started':'步骤开始','step.succeeded':'步骤成功','step.failed':'步骤失败','step.cancelled':'步骤取消','step.skipped':'步骤跳过','step.stopped':'步骤停止',log:'插件日志',
5
+ };
6
+ const statuses={running:'运行中',succeeded:'成功',failed:'失败',cancelled:'已取消',stopped:'已停止'};
7
+
8
+ // 用长于正文的围栏包裹插件文本,避免错误消息中的 Markdown 改写日志结构。
9
+ function block(value,language='text'){
10
+ const text=typeof value==='string'?value:JSON.stringify(value,null,2)??String(value);
11
+ let longest=2;for(const match of text.matchAll(/`+/g))longest=Math.max(longest,match[0].length);
12
+ const fence='`'.repeat(longest+1);
13
+ return `${fence}${language}\n${text}\n${fence}\n\n`;
14
+ }
15
+
16
+ function errorText(error){
17
+ let output='',depth=0;
18
+ for(let current=error;current;current=current.cause){
19
+ const lines=[];
20
+ for(const [key,label] of [['plugin','插件'],['action','动作'],['command','命令'],['code','错误码'],['message','原因']]){
21
+ if(current[key]!==undefined)lines.push(`${label}:${current[key]}`);
22
+ }
23
+ output+=`#### ${depth++?'原始错误':'错误信息'}\n\n`+block(lines.join('\n'));
24
+ if(current.details!==undefined)output+='详情:\n\n'+block(current.details,'json');
25
+ if(current.stack)output+='调用栈:\n\n'+block(current.stack);
26
+ }
27
+ return output;
28
+ }
29
+
30
+ export function markdownHeader(run){
31
+ return '# Fishtools 运行日志\n\n'+block(`运行 ID:${run.id}\n工作流或命令:${run.workflowId}\n开始时间:${run.startedAt}`)+'## 执行过程\n\n';
32
+ }
33
+
34
+ export function markdownEvent(event){
35
+ let output=`### ${event.type==='log'&&event.audience==='user'?'用户输出':names[event.type]??'事件'}\n\n`;
36
+ const lines=[`时间:${event.time}`];
37
+ for(const [key,label] of [['callPath','调用路径'],['action','动作'],['plugin','插件'],['command','命令'],['stream','输出流'],['durationMs','耗时(毫秒)']]){
38
+ if(event[key]!==undefined)lines.push(`${label}:${event[key]}`);
39
+ }
40
+ output+=block(lines.join('\n'));
41
+ if(event.message)output+=block(event.message);
42
+ if(event.reason!==undefined)output+='停止或跳过原因:\n\n'+block(event.reason);
43
+ if(event.data!==undefined)output+='附加信息:\n\n'+block(event.data,'json');
44
+ if(event.input!==undefined)output+='步骤输入:\n\n'+block(event.input,'json');
45
+ if(event.output!==undefined)output+='步骤输出:\n\n'+block(event.output,'json');
46
+ if(event.error)output+=errorText(event.error);
47
+ return output;
48
+ }
49
+
50
+ export function markdownResult(run){
51
+ let output='## 最终结果\n\n'+block(`状态:${statuses[run.status]??run.status}\n结束时间:${run.finishedAt}`);
52
+ if(run.reason!==undefined)output+='停止原因:\n\n'+block(run.reason);
53
+ if(run.output!==undefined)output+=block(run.output,'json');
54
+ return output;
55
+ }