@yanglaofish/fishtools 0.5.3 → 0.6.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 (104) hide show
  1. package/README.md +3 -3
  2. package/manual/README.md +3 -3
  3. package/manual/docs/modules.md +2 -0
  4. package/manual/docs/plans/2026-10-13-effective-inputs-and-tables.md +39 -0
  5. package/manual/docs/plans/2026-10-13-plugin-defaults.md +45 -0
  6. package/manual/docs/plans/2026-10-14-issue7-recovery.md +33 -0
  7. package/manual/docs/plans/2026-10-14-issue8-10-workflows.md +26 -0
  8. package/manual/docs/plans/2026-10-14-issue9-completion.md +47 -0
  9. package/manual/docs/plans/2026-10-14-managed-execution.md +58 -0
  10. package/manual/docs/plans/2026-10-14-plugin-discovery-history.md +41 -0
  11. package/manual/docs/plans/2026-10-14-structured-reports.md +77 -0
  12. package/manual/docs/plans/2026-10-14-unreleased-review.md +47 -0
  13. package/manual/docs/plugin-development.md +161 -8
  14. package/manual/docs/releases.md +25 -6
  15. package/manual/docs/superpowers/plans/2026-10-13-tui-readable-sessions.md +2 -2
  16. package/manual/docs/superpowers/reviews/2026-10-13-tui-sessions-review.md +2 -0
  17. package/manual/docs/superpowers/reviews/2026-10-14-0.6.0-release-review.md +19 -0
  18. package/manual/docs/verification.md +17 -0
  19. package/manual/packages/core/README.md +6 -0
  20. package/manual/packages/core/src/application.js +15 -9
  21. package/manual/packages/core/src/plugin-catalog.js +20 -2
  22. package/manual/packages/core/src/plugin-loading.js +24 -0
  23. package/manual/packages/core/src/version.js +1 -1
  24. package/manual/packages/fst-cli/README.md +5 -1
  25. package/manual/packages/fst-cli/src/terminal/command-menu.js +3 -2
  26. package/manual/packages/fst-cli/src/terminal/file-autocomplete.js +106 -0
  27. package/manual/packages/fst-cli/src/terminal/log-viewer.js +4 -3
  28. package/manual/packages/fst-cli/src/terminal/navigation.js +23 -1
  29. package/manual/packages/fst-cli/src/terminal/presentation.js +2 -1
  30. package/manual/packages/fst-cli/src/terminal/report-presentation.js +38 -0
  31. package/manual/packages/fst-cli/src/terminal/result-table.js +66 -0
  32. package/manual/packages/fst-cli/src/terminal/run-feed.js +42 -17
  33. package/manual/packages/fst-cli/src/terminal/run-presentation.js +51 -17
  34. package/manual/packages/fst-cli/src/terminal/run-viewer.js +15 -12
  35. package/manual/packages/fst-cli/src/terminal/schema.js +114 -4
  36. package/manual/packages/fst-cli/src/terminal/session-history.js +2 -2
  37. package/manual/packages/fst-cli/src/terminal/session.js +40 -15
  38. package/manual/packages/fst-cli/src/terminal/view.js +80 -68
  39. package/manual/packages/fst-cli/src/terminal/workflow-menu.js +3 -1
  40. package/manual/packages/fst-devtools/README.md +2 -0
  41. package/manual/packages/fst-devtools/src/plugin-quality.js +4 -1
  42. package/manual/packages/fst-devtools/src/plugin-tools.js +1 -0
  43. package/manual/packages/fst-devtools/src/process-usage.js +62 -0
  44. package/manual/packages/fst-platform/README.md +5 -0
  45. package/manual/packages/fst-platform/package.json +1 -0
  46. package/manual/packages/fst-platform/src/managed-process.js +36 -0
  47. package/manual/packages/fst-platform/src/process.js +7 -2
  48. package/manual/packages/fst-plugin/README.md +4 -0
  49. package/manual/packages/fst-plugin/src/contracts.js +3 -0
  50. package/manual/packages/fst-plugin/src/host.js +38 -23
  51. package/manual/packages/fst-plugin/src/runtime.js +14 -8
  52. package/manual/packages/fst-workflow/src/engine.js +1 -1
  53. package/manual/packages/fst-workspace/README.md +4 -0
  54. package/manual/packages/fst-workspace/src/plugin-registry.js +1 -1
  55. package/manual/packages/fst-workspace/src/plugins.js +5 -3
  56. package/manual/packages/fst-workspace/src/workflows.js +27 -9
  57. package/manual/packages/sdk/README.md +10 -0
  58. package/manual/packages/sdk/inputs.js +53 -0
  59. package/manual/packages/sdk/package.json +2 -0
  60. package/manual/packages/sdk/reports.js +53 -0
  61. package/node_modules/@mytools/core/README.md +6 -0
  62. package/node_modules/@mytools/core/src/application.js +15 -9
  63. package/node_modules/@mytools/core/src/plugin-catalog.js +20 -2
  64. package/node_modules/@mytools/core/src/plugin-loading.js +24 -0
  65. package/node_modules/@mytools/core/src/version.js +1 -1
  66. package/node_modules/@mytools/fst-cli/README.md +5 -1
  67. package/node_modules/@mytools/fst-cli/src/terminal/command-menu.js +3 -2
  68. package/node_modules/@mytools/fst-cli/src/terminal/file-autocomplete.js +106 -0
  69. package/node_modules/@mytools/fst-cli/src/terminal/log-viewer.js +4 -3
  70. package/node_modules/@mytools/fst-cli/src/terminal/navigation.js +23 -1
  71. package/node_modules/@mytools/fst-cli/src/terminal/presentation.js +2 -1
  72. package/node_modules/@mytools/fst-cli/src/terminal/report-presentation.js +38 -0
  73. package/node_modules/@mytools/fst-cli/src/terminal/result-table.js +66 -0
  74. package/node_modules/@mytools/fst-cli/src/terminal/run-feed.js +42 -17
  75. package/node_modules/@mytools/fst-cli/src/terminal/run-presentation.js +51 -17
  76. package/node_modules/@mytools/fst-cli/src/terminal/run-viewer.js +15 -12
  77. package/node_modules/@mytools/fst-cli/src/terminal/schema.js +114 -4
  78. package/node_modules/@mytools/fst-cli/src/terminal/session-history.js +2 -2
  79. package/node_modules/@mytools/fst-cli/src/terminal/session.js +40 -15
  80. package/node_modules/@mytools/fst-cli/src/terminal/view.js +80 -68
  81. package/node_modules/@mytools/fst-cli/src/terminal/workflow-menu.js +3 -1
  82. package/node_modules/@mytools/fst-devtools/README.md +2 -0
  83. package/node_modules/@mytools/fst-devtools/src/plugin-quality.js +4 -1
  84. package/node_modules/@mytools/fst-devtools/src/plugin-tools.js +1 -0
  85. package/node_modules/@mytools/fst-devtools/src/process-usage.js +62 -0
  86. package/node_modules/@mytools/fst-platform/README.md +5 -0
  87. package/node_modules/@mytools/fst-platform/package.json +1 -0
  88. package/node_modules/@mytools/fst-platform/src/managed-process.js +36 -0
  89. package/node_modules/@mytools/fst-platform/src/process.js +7 -2
  90. package/node_modules/@mytools/fst-plugin/README.md +4 -0
  91. package/node_modules/@mytools/fst-plugin/src/contracts.js +3 -0
  92. package/node_modules/@mytools/fst-plugin/src/host.js +38 -23
  93. package/node_modules/@mytools/fst-plugin/src/runtime.js +14 -8
  94. package/node_modules/@mytools/fst-workflow/src/engine.js +1 -1
  95. package/node_modules/@mytools/fst-workspace/README.md +4 -0
  96. package/node_modules/@mytools/fst-workspace/src/plugin-registry.js +1 -1
  97. package/node_modules/@mytools/fst-workspace/src/plugins.js +5 -3
  98. package/node_modules/@mytools/fst-workspace/src/workflows.js +27 -9
  99. package/node_modules/@mytools/sdk/README.md +10 -0
  100. package/node_modules/@mytools/sdk/inputs.js +53 -0
  101. package/node_modules/@mytools/sdk/package.json +2 -0
  102. package/node_modules/@mytools/sdk/reports.js +53 -0
  103. package/node_modules/@mytools/web/public/THIRD_PARTY_NOTICES.txt +1032 -1032
  104. package/package.json +1 -1
@@ -20,6 +20,10 @@
20
20
 
21
21
  ## 维护与验证
22
22
 
23
+ 动作及命令的上下文 runProcess 使用 platform/managed-process 的共用包装,统一指令、输出关联与取消信号;代理仍由宿主按动作构造。权限预检发生在动作主体之前,不自动提权。共享包装不串行化调用;宿主同进程信任模型不变,开发约束不能替代安全隔离。
24
+
25
+ 动作、动作别名命令及独立交互命令共用 SDK 输入解析。顶层输入字段的 `x-fst.fromConfig` 声明先取实际插件配置,再回退到输入 schema 默认值;显式输入优先。`host.previewInput(target, input)` 返回有效值及来源,管理界面无需读取整份宿主配置。声明由 `validateDeclarations` 检查,插件加载和开发工具使用同一规则。
26
+
23
27
  本模块负责上述职责的实现、文档与回归;跨模块接口调整需要同时检查生产者与消费者。具体维护人员尚未指定。
24
28
 
25
29
  在仓库根目录执行:
@@ -3,6 +3,7 @@ import {conditionOperators} from '@mytools/sdk/conditions';
3
3
  import {resolveSchemaPath} from '@mytools/sdk/schema-path';
4
4
  import {inside} from '@mytools/fst-platform/environments';
5
5
  import path from 'node:path';
6
+ import {validateInputRules} from '@mytools/sdk/inputs';
6
7
 
7
8
  export const builtinCommands=[
8
9
  ['workflows','选择已有工作流'],['new','新建工作流'],['add','添加插件能力或子工作流'],
@@ -37,6 +38,8 @@ function validatePrivilegeDeclarations(plugin){
37
38
  }
38
39
 
39
40
  export function validateDeclarations(plugin){
41
+ for(const [name,owner] of Object.entries(plugin.actions??{}))validateInputRules(owner?.input,plugin.config,`plugin.actions.${name}.input`);
42
+ for(const [name,owner] of Object.entries(plugin.commands??{}))if(!owner?.action)validateInputRules(owner?.input,plugin.config,`plugin.commands.${name}.input`);
40
43
  for(const [name,action] of Object.entries(plugin.actions??{})){
41
44
  if(!action||typeof action!=='object'||Array.isArray(action))throw new FlowError(`动作 ${name} 的声明必须为对象`,{code:'VALIDATION'});
42
45
  if(action.conditions!==undefined&&(!action.conditions||typeof action.conditions!=='object'||Array.isArray(action.conditions)))throw new FlowError(`动作 ${name} 的 conditions 必须为对象`,{code:'VALIDATION'});
@@ -2,15 +2,17 @@ import { Context } from 'cordis';
2
2
  import semver from 'semver';
3
3
  import { SDK_VERSION, FlowError } from '@mytools/sdk';
4
4
  import { validate, emptyObject } from '@mytools/sdk/schema';
5
+ import {resolveInput} from '@mytools/sdk/inputs';
5
6
  import {validateDeclarations,validateCommands} from './contracts.js';
6
7
  import {prepareEnvironments,platformWarnings} from '@mytools/fst-platform/environments';
7
8
  import path from 'node:path';
8
9
  import {mkdir} from 'node:fs/promises';
9
10
  import {createHash} from 'node:crypto';
10
- import {createRuntime,validateRuntimes} from './runtime.js';
11
+ import {createRuntime,validateRuntimes,validateRuntimeDeclaration} from './runtime.js';
11
12
  import {pluginError} from '@mytools/sdk/diagnostics';
12
13
  import {validateNetworkPolicy,resolveNetworkPolicy,createHttpClient,buildProcessEnv} from '@mytools/fst-platform/network';
13
14
  import {resolvePrivilegeRequirement,resolvePrivilegePreflight,checkStaticPrivilege,runPrivilegePreflight} from './privileges.js';
15
+ import {createProcessRunner} from '@mytools/fst-platform/managed-process';
14
16
 
15
17
  export async function createHost({ plugins = [], config = {}, services = {}, project,network,environmentOverrides={},runtimeEnabled=true,allowUnknownPrivileges=false } = {}) {
16
18
  const currentNetwork=()=>typeof network==='function'?network():network;
@@ -25,16 +27,7 @@ export async function createHost({ plugins = [], config = {}, services = {}, pro
25
27
  const bound={...capabilities,...context},execute=bound.runProcess;
26
28
  // 面向用户的输出与诊断日志共用持久化通道,只区分默认可见性。
27
29
  bound.print=(message,data)=>Promise.resolve(bound.log?.(message,data,{audience:'user'}));
28
- if(typeof execute==='function')bound.runProcess=(file,args,options={})=>{
29
- const signal=context.signal&&options.signal?AbortSignal.any([context.signal,options.signal]):context.signal??options.signal;
30
- return execute(file,args,{...options,signal,onStart:async invocation=>{
31
- await bound.log?.('执行程序',invocation,{kind:'process'});
32
- await options.onStart?.(invocation);
33
- },onOutput:async({stream,message})=>{
34
- await bound.log?.(message,undefined,{stream});
35
- await options.onOutput?.({stream,message});
36
- }});
37
- };
30
+ if(typeof execute==='function')bound.runProcess=createProcessRunner({execute,signal:context.signal,log:bound.log});
38
31
  return bound;
39
32
  }
40
33
  async function pluginContext(plugin,action,context){
@@ -161,6 +154,12 @@ export async function createHost({ plugins = [], config = {}, services = {}, pro
161
154
  }
162
155
  },
163
156
  commands(){assertReadable();return [...commands.values()].map(({run,preflight,config,...metadata})=>structuredClone(metadata));},
157
+ previewInput(target,input={}){
158
+ assertReadable();
159
+ const owner=target.startsWith('/')?commands.get(target.slice(1)):actions.get(target);
160
+ if(!owner)throw new FlowError(`参数目标不存在:${target}`,{code:'VALIDATION'});
161
+ return resolveInput(owner.input??emptyObject,input,owner.config,{check:false,label:`${target} 输入`});
162
+ },
164
163
  async invokeCommand(name,input,context={}){
165
164
  assertHealthy();
166
165
  const command=commands.get(name);if(!command)throw new FlowError(`命令不存在:/${name}`);
@@ -170,7 +169,7 @@ export async function createHost({ plugins = [], config = {}, services = {}, pro
170
169
  try{
171
170
  const getService=runtime.bindServices(plugin.id);
172
171
  context.signal?.throwIfAborted();
173
- const checked=validate(command.input??emptyObject,input,`/${name} 输入`);
172
+ const checked=resolveInput(command.input??emptyObject,input,command.config,{label:`/${name} 输入`}).value;
174
173
  const bound=await pluginContext(plugin,undefined,context);
175
174
  let result;
176
175
  try{
@@ -189,7 +188,7 @@ export async function createHost({ plugins = [], config = {}, services = {}, pro
189
188
  try{
190
189
  const getService=runtime.bindServices(action.plugin);
191
190
  context.signal?.throwIfAborted();
192
- const checked=validate(action.input ?? emptyObject,input,`${id} 输入`);
191
+ const checked=resolveInput(action.input??emptyObject,input,action.config,{label:`${id} 输入`}).value;
193
192
  const plugin=definitions.get(action.plugin),bound=await pluginContext(plugin,action,context);
194
193
  let result;
195
194
  try{
@@ -219,25 +218,41 @@ export async function createHost({ plugins = [], config = {}, services = {}, pro
219
218
  };
220
219
  }
221
220
 
222
- // 可用和待配置的插件共用声明检查;配置不足不会掩盖 SDK、依赖或代码错误。
223
- export function validatePluginDefinitions(entries){
221
+ // 单插件契约检查不依赖其他插件,供启动恢复路径精确归属故障。
222
+ export function validatePluginDefinition(entry){
223
+ const plugin=entry?.plugin??entry;
224
+ if(!plugin||typeof plugin!=='object'||Array.isArray(plugin)||typeof plugin.id!=='string'||!plugin.id)throw new FlowError('插件默认导出必须是包含 id 的对象',{code:'VALIDATION'});
225
+ validateDeclarations(plugin);validateCommands(plugin,new Set());
226
+ if(!semver.satisfies(SDK_VERSION,plugin.sdk??'^0.1.0'))throw new FlowError(`插件 ${plugin.id} 与 SDK ${SDK_VERSION} 不兼容`,{code:'VALIDATION'});
227
+ for(const [name,action] of Object.entries(plugin.actions??{}))if(typeof action?.run!=='function')throw new FlowError(`动作 ${plugin.id}:${name} 缺少执行函数`,{code:'VALIDATION'});
228
+ for(const [name,extension] of Object.entries(plugin.services??{}))if(typeof extension!=='function')throw new FlowError(`服务 ${name} 缺少执行函数`,{code:'VALIDATION'});
229
+ validateRuntimeDeclaration(plugin,new Context());
230
+ return plugin;
231
+ }
232
+
233
+ // 恢复启动只豁免已确认故障的节点;其他可确定的声明和依赖错误仍明确拒绝。
234
+ export function validatePluginDefinitions(entries,{unavailable=new Set(),blocked=new Set()}={}){
224
235
  const definitions=new Map(),commandNames=new Set(),serviceNames=new Set();
225
236
  for(const entry of entries){
226
237
  const plugin=entry.plugin??entry;
227
- validateDeclarations(plugin);validateCommands(plugin,commandNames);
228
238
  if(!plugin.id||definitions.has(plugin.id))throw new FlowError(`插件标识缺失或重复:${plugin.id}`,{code:'VALIDATION'});
229
- if(!semver.satisfies(SDK_VERSION,plugin.sdk??'^0.1.0'))throw new FlowError(`插件 ${plugin.id} 与 SDK ${SDK_VERSION} 不兼容`,{code:'VALIDATION'});
230
- for(const [name,action] of Object.entries(plugin.actions??{}))if(typeof action?.run!=='function')throw new FlowError(`动作 ${plugin.id}:${name} 缺少执行函数`,{code:'VALIDATION'});
231
- for(const [name,extension] of Object.entries(plugin.services??{})){
239
+ if(unavailable.has(plugin.id)){definitions.set(plugin.id,{...plugin,root:entry.root});continue;}
240
+ validatePluginDefinition(entry);validateCommands(plugin,commandNames);
241
+ for(const name of Object.keys(plugin.services??{})){
232
242
  if(serviceNames.has(name))throw new FlowError(`服务重复:${name}`,{code:'VALIDATION'});
233
- if(typeof extension!=='function')throw new FlowError(`服务 ${name} 缺少执行函数`,{code:'VALIDATION'});
234
243
  serviceNames.add(name);
235
244
  }
236
245
  definitions.set(plugin.id,{...plugin,root:entry.root});
237
246
  }
238
- for(const plugin of definitions.values())for(const [id,range] of Object.entries(plugin.dependencies??{})){
239
- if(!definitions.has(id)||!semver.satisfies(definitions.get(id).version,range))throw new FlowError(`插件 ${plugin.id} 缺少兼容依赖 ${id}@${range}`,{code:'VALIDATION'});
247
+ for(const plugin of definitions.values()){
248
+ // 此节点已隔离且不会执行;它的损坏依赖不能阻断其他健康插件恢复。
249
+ if(unavailable.has(plugin.id))continue;
250
+ for(const [id,range] of Object.entries(plugin.dependencies??{})){
251
+ // 无法读取目标版本时保留恢复入口;已知版本仍须满足健康消费者的声明。
252
+ if(unavailable.has(id)&&!semver.valid(definitions.get(id)?.version))continue;
253
+ if(!definitions.has(id)||!semver.satisfies(definitions.get(id).version,range))throw new FlowError(`插件 ${plugin.id} 缺少兼容依赖 ${id}@${range}`,{code:'VALIDATION'});
254
+ }
240
255
  }
241
- validateRuntimes(definitions,new Context());
256
+ validateRuntimes(new Map([...definitions].filter(([id])=>!unavailable.has(id))),new Context(),{blocked});
242
257
  return definitions;
243
258
  }
@@ -1,16 +1,20 @@
1
1
  import {FlowError} from '@mytools/sdk';
2
2
 
3
+ export function validateRuntimeDeclaration(p,ctx){
4
+ const r=p.runtime;if(r===undefined)return;
5
+ if(!r||typeof r.apply!=='function')throw new FlowError(`${p.id} runtime 必须提供 apply`);
6
+ for(const key of ['provides','inject']){
7
+ const names=r[key]??[];
8
+ if(!Array.isArray(names)||names.some(n=>typeof n!=='string'||!/^\w[\w.-]*$/.test(n)||n in ctx)||new Set(names).size!==names.length)throw new FlowError(`${p.id} runtime.${key} 服务名称无效或重复`);
9
+ }
10
+ }
11
+
3
12
  // 静态声明用于诊断与打包,运行时激活仍由 Cordis inject 驱动。
4
- export function validateRuntimes(definitions,ctx){
13
+ export function validateRuntimes(definitions,ctx,{blocked=new Set()}={}){
5
14
  const providers=new Map(),ordered=[],visiting=new Set(),visited=new Set();
6
15
  for(const p of definitions.values()){
7
- const r=p.runtime;if(r===undefined)continue;
8
- if(!r||typeof r.apply!=='function')throw new FlowError(`${p.id} runtime 必须提供 apply`);
9
- for(const key of ['provides','inject']){
10
- const names=r[key]??[];
11
- if(!Array.isArray(names)||names.some(n=>typeof n!=='string'||!/^\w[\w.-]*$/.test(n)||n in ctx)||new Set(names).size!==names.length)throw new FlowError(`${p.id} runtime.${key} 服务名称无效或重复`);
12
- }
13
- for(const name of r.provides??[]){
16
+ validateRuntimeDeclaration(p,ctx);
17
+ for(const name of p.runtime?.provides??[]){
14
18
  if(providers.has(name))throw new FlowError(`服务重复:${name}(${providers.get(name)}、${p.id})`);
15
19
  providers.set(name,p.id);
16
20
  }
@@ -21,6 +25,8 @@ export function validateRuntimes(definitions,ctx){
21
25
  visiting.add(id);const p=definitions.get(id);
22
26
  for(const name of p.runtime?.inject??[]){
23
27
  const owner=providers.get(name);
28
+ // 故障依赖的声明不可读取时,只跳过无法确认归属的服务;已知冲突与环仍检查。
29
+ if(!owner&&blocked.has(id))continue;
24
30
  if(!owner)throw new FlowError(`${id} 缺少服务提供者:${name}`);
25
31
  if(owner!==id&&!Object.hasOwn(p.dependencies??{},owner))throw new FlowError(`${id} 的 dependencies 必须声明服务 ${name} 的提供者 ${owner}`);
26
32
  visit(owner);
@@ -57,7 +57,7 @@ export async function runWorkflow({store,host,id,input={},signal,onEvent=()=>{},
57
57
  let output;
58
58
  if(step.action)output=await host.invoke(step.action,args,{
59
59
  ...context,signal:currentSignal,runId,stepId:step.id,callPath,workflow:w,
60
- log:(message,data,meta={})=>emit({type:'log',stream:meta.stream,...(meta.audience==='user'?{audience:'user'}:{}),...(['process','source'].includes(meta.kind)?{kind:meta.kind}:{}),...base,message,data}),
60
+ log:(message,data,meta={})=>emit({type:'log',stream:meta.stream,...(typeof meta.processId==='string'?{processId:meta.processId}:{}),...(meta.audience==='user'?{audience:'user'}:{}),...(['process','source'].includes(meta.kind)?{kind:meta.kind}:{}),...base,message,data}),
61
61
  });
62
62
  else if(step.workflow)output=await execute(store.childId(step.workflow),args,callPath,currentSignal,reportFailure);
63
63
  else if(step.branch){
@@ -28,6 +28,10 @@
28
28
 
29
29
  `/shortcuts` 提供 `validateShortcuts` 校验、`bindShortcutCommands` 在用户配置时补齐命令提供者身份,以及 `describeShortcuts` 生成可用或失效状态。绑定函数可传入 previous 保留旧项:未绑定命令修改参数或重命名仍保持失效,需重新选择目标。命令项使用 command 与 plugin 同时匹配,动作项使用稳定 action ID;不执行其他插件的同名能力。实际保存、备份与宿主装配由 core 协调。
30
30
 
31
+ `WorkflowStore.list()` 保留正常工作流,同时返回 `status: 'invalid'` 的损坏文件诊断条目,`diagnostic` 含文件路径、错误消息以及可获得的 YAML 行列。诊断条目不能保存或复制。`list({strict:true})` 遇错误即拒绝,删除和卸载依赖扫描必须使用此模式;`get/check/run` 保持严格读取。`resolve()` 对根及全部子流程的输入 Schema 提前编译,打包与执行共用闭包预检,不会先执行前置业务步骤才发现子流程声明错误。
32
+
33
+ `installGeneration` 的可选 `recoverInstalled` 回调仅用于纯卸载时重新读取剩余旧插件,交由 core 提供恢复条目;默认调用、新安装和链接保持严格加载。
34
+
31
35
  本模块负责上述职责的实现、文档与回归;跨模块接口调整需要同时检查生产者与消费者。具体维护人员尚未指定。
32
36
 
33
37
  在仓库根目录执行:
@@ -33,7 +33,7 @@ function referencedActions(steps,id,result=new Set()){
33
33
  export async function registryWorkflowReferences(registryDir,id){
34
34
  const projects=await readJson(path.join(registryDir,'.flow','workspaces.json'),[]),references=[];
35
35
  for(const projectDir of projects){
36
- for(const workflow of await new WorkflowStore(path.join(projectDir,'workflows')).list()){
36
+ for(const workflow of await new WorkflowStore(path.join(projectDir,'workflows')).list({strict:true})){
37
37
  const actions=referencedActions(workflow.steps,id);
38
38
  if(actions.length)references.push({projectDir,id:workflow.id,title:workflow.title??workflow.id,actions});
39
39
  }
@@ -5,7 +5,7 @@ import {FlowError} from '@mytools/sdk';
5
5
  import {readJson} from '@mytools/fst-platform/json';
6
6
  import {npm} from '@mytools/fst-platform/npm';
7
7
  import {loadPlugin} from '@mytools/fst-plugin/loader';
8
- export async function installGeneration(projectDir,{add,remove,link,state,sourceDir=projectDir}) {
8
+ export async function installGeneration(projectDir,{add,remove,link,state,sourceDir=projectDir,recoverInstalled}) {
9
9
  const root=path.join(projectDir,'.flow','installations',randomUUID());
10
10
  await mkdir(root,{recursive:true});
11
11
  const dependencies={...(state?.dependencies??{})};
@@ -32,7 +32,9 @@ export async function installGeneration(projectDir,{add,remove,link,state,source
32
32
  }
33
33
  const pkg=await readJson(path.join(root,'package.json'));
34
34
  const loaded=[];
35
- for(const name of Object.keys(pkg.dependencies??{})){delete links[name];disabledLegacy.delete(name);loaded.push(await loadPlugin(name,root));}
36
- for(const source of Object.values(links))loaded.push(await loadPlugin(source,projectDir));
35
+ // 卸载故障插件时允许保留其他旧故障;安装/链接操作仍严格校验新批次。
36
+ const load=remove&&!add&&!link&&recoverInstalled?recoverInstalled:(_id,spec,base)=>loadPlugin(spec,base);
37
+ for(const name of Object.keys(pkg.dependencies??{})){delete links[name];disabledLegacy.delete(name);loaded.push(await load(name,name,root));}
38
+ for(const [name,source] of Object.entries(links))loaded.push(await load(name,source,projectDir,{linked:true}));
37
39
  return {state:{root,dependencies:pkg.dependencies??{},links,...(state?.global?{global:true,disabledLegacy:[...disabledLegacy]}:{}),...(state?.root?{previousRoot:state.root}:{})},loaded};
38
40
  }
@@ -12,14 +12,21 @@ export class WorkflowStore {
12
12
  file(id) { assertId(id); return path.join(this.root,`${id}.yaml`); }
13
13
  async get(id) {
14
14
  const file=this.file(id);
15
- const physical=await realpath(file);
16
- const root=await realpath(this.root);
17
- if (!physical.startsWith(root+path.sep)) throw new FlowError('工作流越界,必须位于工作流目录内',{code:'VALIDATION'});
18
- const definition=YAML.parse(await readFile(file,'utf8'));
19
- if (definition?.id !== id) throw new FlowError(`工作流 ID 与文件名不一致:${id}`,{code:'VALIDATION'});
20
- return this.validateShape(definition);
15
+ try{
16
+ const physical=await realpath(file);
17
+ const root=await realpath(this.root);
18
+ if (!physical.startsWith(root+path.sep)) throw new FlowError('工作流越界,必须位于工作流目录内',{code:'VALIDATION'});
19
+ const definition=YAML.parse(await readFile(file,'utf8'));
20
+ if (definition?.id !== id) throw new FlowError(`工作流 ID 与文件名不一致:${id}`,{code:'VALIDATION'});
21
+ return this.validateShape(definition);
22
+ }catch(error){
23
+ const position=error.linePos?.[0];
24
+ const details={file,...(position?{line:position.line,column:position.col}:{}),reason:error.code};
25
+ throw new FlowError(`工作流读取失败:${file}${position?`:${position.line}:${position.col}`:''}\n${error.message}`,{code:'VALIDATION',details,cause:error});
26
+ }
21
27
  }
22
28
  validateShape(w) {
29
+ if(w?.status==='invalid')throw new FlowError(w.diagnostic?.message??'损坏工作流的诊断条目不能保存或复制,请先修复原文件',{code:'VALIDATION'});
23
30
  assertId(w?.id);
24
31
  if (w.schemaVersion!==1 || !Array.isArray(w.steps)) throw new FlowError('工作流格式错误:需要 schemaVersion: 1 与 steps 列表',{code:'VALIDATION'});
25
32
  validateStepList(w.steps,assertId);
@@ -66,16 +73,26 @@ export class WorkflowStore {
66
73
  throw error;
67
74
  }
68
75
  }
69
- async list() {
76
+ async list({strict=false}={}) {
70
77
  await mkdir(this.root,{recursive:true});
71
78
  const files=await readdir(this.root);
72
- return Promise.all(files.filter(f=>f.endsWith('.yaml')).sort().map(f=>this.get(f.slice(0,-5))));
79
+ return Promise.all(files.filter(f=>f.endsWith('.yaml')).sort().map(async f=>{
80
+ const id=f.slice(0,-5);
81
+ try{return await this.get(id);}
82
+ catch(error){
83
+ if(strict)throw error;
84
+ const file=path.join(this.root,f),diagnostic={...error.details,file,message:error.message};
85
+ // 保留数组协议和已有 Web 展示字段;此条目仅供诊断,不是可保存的定义。
86
+ return {id,title:`不可用:${f}`,description:`${file}\n${error.message}`,status:'invalid',diagnostic,inputs:false,steps:[],outputs:{}};
87
+ }
88
+ }));
73
89
  }
74
90
  async remove(id,{backup=true}={}){
75
91
  if(typeof backup!=='boolean')throw new FlowError('backup 必须是布尔值',{code:'VALIDATION'});
76
92
  await this.get(id);
77
93
  const refers=steps=>steps.some(step=>step.workflow&&this.childId(step.workflow)===id||step.branch&&(refers(step.branch.then)||refers(step.branch.else??[]))||step.parallel&&refers(step.parallel.steps));
78
- const dependents=(await this.list()).filter(flow=>flow.id!==id&&refers(flow.steps));
94
+ // 文件无法读取时不能证明不存在引用,必须阻止删除。
95
+ const dependents=(await this.list({strict:true})).filter(flow=>flow.id!==id&&refers(flow.steps));
79
96
  if(dependents.length)throw new FlowError(`工作流仍被引用:${dependents.map(flow=>flow.title??flow.id).join('、')}`);
80
97
  if(!backup){await unlink(this.file(id));return null;}
81
98
  const directory=path.join(path.dirname(this.root),'.flow','deleted-workflows');
@@ -95,6 +112,7 @@ export class WorkflowStore {
95
112
  if(workflows.has(id)) return;
96
113
  visiting.add(id);
97
114
  const w=await this.get(id);
115
+ assertSchema(w.inputs??emptyObject,`${this.file(id)} inputs schema`);
98
116
  const checker=previous=>ref=>{
99
117
  const p=referenceParts(ref);
100
118
  if(p[0]==='steps' && !previous.has(p[1])) throw new FlowError(`${id} 引用必须指向前序步骤:${ref}`,{code:'VALIDATION'});
@@ -13,6 +13,8 @@
13
13
  | --- | --- |
14
14
  | `@mytools/sdk` | `./index.js` |
15
15
  | `@mytools/sdk/schema` | `./validation.js` |
16
+ | `@mytools/sdk/inputs` | `./inputs.js` |
17
+ | `@mytools/sdk/reports` | `./reports.js` |
16
18
  | `@mytools/sdk/schema-path` | `./schema-path.js` |
17
19
  | `@mytools/sdk/diagnostics` | `./diagnostics.js` |
18
20
  | `@mytools/sdk/conditions` | `./conditions.js` |
@@ -29,3 +31,11 @@
29
31
  ```sh
30
32
  node --test tests/modules.test.js tests/plugin-contract.test.js tests/schema-diagnostics.test.js
31
33
  ```
34
+
35
+ ## 输入解析
36
+
37
+ `@mytools/sdk/inputs` 提供 `validateInputRules`、`hasInputRules`、`resolveInput`。输入字段 `x-fst` 支持 `fromConfig`、`unsetLabel` 和 `unit`,由插件宿主与 TUI 共用。`resolveInput` 返回有效输入和字段来源,始终复制输入;配置继承优先于输入固定默认值,显式输入优先于继承。声明范围与示例见插件开发文档。
38
+
39
+ ## 业务报告
40
+
41
+ `@mytools/sdk/reports` 提供 `reportSchema`、`createReport(content)`、`inspectReport(value)`。v1 支持标题、摘要、分级提醒、建议、业务分组及推荐列;SDK 不负责界面渲染。helper 校验并复制 JSON 内容,错误码为 `REPORT_INVALID`。检查接口返回 `none` / `valid` / `invalid` / `unsupported` 状态,合法时 `report` 指向原值(只读使用),非法时包含 `message` 和可选 `details`。普通输出无需接入;已有插件可将报告放入 `output.report`,保留原数据字段。完整示例见 `docs/plugin-development.md`。
@@ -0,0 +1,53 @@
1
+ import {FlowError} from '@mytools/sdk';
2
+ import {assertSchema,validate,emptyObject} from './validation.js';
3
+
4
+ const object=value=>value!==null&&typeof value==='object'&&!Array.isArray(value);
5
+ const complex=['$ref','oneOf','anyOf','allOf','if','then','else','dependencies','patternProperties'];
6
+ export const hasInputRules=schema=>Object.values(schema?.properties??{}).some(field=>field?.['x-fst']!==undefined);
7
+
8
+ // 仅解释已声明的顶层输入规则,不分析插件执行代码或按同名字段猜测继承关系。
9
+ export function validateInputRules(schema,configSchema,label='input'){
10
+ const fail=(at,message)=>{throw new FlowError(`${at} x-fst:${message}`,{code:'VALIDATION',details:{path:at}});};
11
+ const visit=(node,at,allowed=false)=>{
12
+ if(!object(node))return;
13
+ const rule=node['x-fst'];
14
+ if(rule!==undefined){
15
+ if(!allowed||schema.type!=='object'||complex.some(key=>schema[key]!==undefined||node[key]!==undefined))fail(at,'只支持简单输入对象的顶层字段');
16
+ if(!object(rule)||Object.keys(rule).some(key=>!['fromConfig','unsetLabel','unit'].includes(key)))fail(at,'仅支持 fromConfig、unsetLabel、unit');
17
+ for(const [key,value] of Object.entries(rule))if(typeof value!=='string'||!value.trim())fail(at,`${key} 必须为非空字符串`);
18
+ if(rule.fromConfig!==undefined){
19
+ let current=configSchema;
20
+ for(const part of rule.fromConfig.split('.')){
21
+ if(!part||['__proto__','constructor','prototype'].includes(part)||!Object.hasOwn(current?.properties??{},part))fail(at,`配置路径未声明:${rule.fromConfig}`);
22
+ current=current.properties[part];
23
+ }
24
+ }
25
+ }
26
+ for(const key of ['properties','patternProperties','$defs','definitions','dependencies','dependentSchemas'])for(const [name,child] of Object.entries(node[key]??{}))visit(child,`${at}.${key}.${name}`,node===schema&&key==='properties');
27
+ for(const key of ['items','additionalItems','additionalProperties','contains','not','if','then','else','propertyNames']){
28
+ if(Array.isArray(node[key]))node[key].forEach((child,index)=>visit(child,`${at}.${key}.${index}`));
29
+ else visit(node[key],`${at}.${key}`);
30
+ }
31
+ for(const key of ['allOf','anyOf','oneOf','prefixItems'])node[key]?.forEach(child=>visit(child,`${at}.${key}`));
32
+ };
33
+ visit(schema,label);
34
+ }
35
+
36
+ export function resolveInput(schema=emptyObject,input={},config={}, {check=true,label='参数'}={}){
37
+ const value=structuredClone(input),fields={};
38
+ if(object(value))for(const [key,field] of Object.entries(schema?.properties??{})){
39
+ const rule=field?.['x-fst']??{};
40
+ let source=Object.hasOwn(value,key)&&value[key]!==undefined?'input':'omitted';
41
+ if(source==='omitted'&&rule.fromConfig){
42
+ let inherited=config;
43
+ for(const part of rule.fromConfig.split('.'))inherited=inherited!=null&&Object.hasOwn(inherited,part)?inherited[part]:undefined;
44
+ if(inherited!==undefined){Object.defineProperty(value,key,{value:structuredClone(inherited),enumerable:true,writable:true,configurable:true});source='config';}
45
+ }
46
+ Object.defineProperty(fields,key,{value:{source,...(source==='config'?{path:rule.fromConfig}:{}),...(rule.unit?{unit:rule.unit}:{}),...(rule.unsetLabel?{unsetLabel:rule.unsetLabel}:{})},enumerable:true});
47
+ }
48
+ // 预览允许缺少必填项;校验器仍与执行共用,默认值只作用于副本。
49
+ const effective=check?validate(schema,value,label):value;
50
+ if(!check&&schema!=null)assertSchema(schema,label)(effective);
51
+ for(const [key,field] of Object.entries(fields))if(field.source==='omitted'&&effective?.[key]!==undefined)field.source='default';
52
+ return {value:effective,fields};
53
+ }
@@ -6,6 +6,8 @@
6
6
  "exports": {
7
7
  ".": "./index.js",
8
8
  "./schema": "./validation.js",
9
+ "./inputs": "./inputs.js",
10
+ "./reports": "./reports.js",
9
11
  "./schema-path": "./schema-path.js",
10
12
  "./diagnostics": "./diagnostics.js",
11
13
  "./conditions": "./conditions.js",
@@ -0,0 +1,53 @@
1
+ import {FlowError} from './index.js';
2
+ import {assertSchema,validate} from './validation.js';
3
+
4
+ const text={type:'string',pattern:'\\S'};
5
+ const alerts={type:'array',items:{type:'object',properties:{level:{enum:['info','warning','error']},message:text},required:['level','message'],additionalProperties:false}};
6
+ const conclusions={summary:text,alerts,recommendations:{type:'array',items:text}};
7
+ export const reportSchema={
8
+ type:'object',
9
+ properties:{
10
+ kind:{const:'fst-report'},version:{const:1},title:text,...conclusions,
11
+ sections:{type:'array',items:{type:'object',properties:{title:text,...conclusions,data:true,columns:{type:'array',items:text,minItems:1,uniqueItems:true},titles:{type:'object',additionalProperties:text}},required:['title'],additionalProperties:false}},
12
+ },
13
+ required:['kind','version','title'],additionalProperties:false,
14
+ };
15
+
16
+ // 与落盘 JSON 边界一致,提前拒绝会被 JSON.stringify 静默改值的内容。
17
+ function assertJson(value,ancestors=new Set()){
18
+ if(value===null||typeof value==='string'||typeof value==='boolean'||typeof value==='number'&&Number.isFinite(value))return;
19
+ if(typeof value!=='object'||!value||ancestors.has(value)||(!Array.isArray(value)&&![Object.prototype,null].includes(Object.getPrototypeOf(value))))throw new Error('报告必须为无循环引用的 JSON 数据');
20
+ ancestors.add(value);
21
+ try{
22
+ const list=Array.isArray(value),keys=Reflect.ownKeys(value);
23
+ if(list&&keys.length!==value.length+1)throw new Error('报告数组不能含空槽或附加属性');
24
+ for(const key of keys){
25
+ if(list&&key==='length')continue;
26
+ const field=Object.getOwnPropertyDescriptor(value,key);
27
+ if(typeof key!=='string'||!field.enumerable||!Object.hasOwn(field,'value')||list&&(!Number.isInteger(Number(key))||Number(key)<0||Number(key)>=value.length||String(Number(key))!==key))throw new Error('报告只能包含 JSON 可保留的普通数据属性');
28
+ assertJson(field.value,ancestors);
29
+ }
30
+ }finally{ancestors.delete(value);}
31
+ }
32
+
33
+ export function inspectReport(value){
34
+ try{
35
+ if(!value||typeof value!=='object'||Array.isArray(value))return {status:'none'};
36
+ const kind=Object.getOwnPropertyDescriptor(value,'kind');
37
+ if(kind&&!Object.hasOwn(kind,'value'))throw new Error('报告标记不能为访问器');
38
+ if(kind?.value!=='fst-report')return {status:'none'};
39
+ assertJson(value);
40
+ if(Number.isInteger(value.version)&&value.version!==1)return {status:'unsupported',message:`不支持报告版本 ${value.version},已回退字段阅读。`};
41
+ const check=assertSchema(reportSchema,'报告',{useDefaults:false});
42
+ if(!check(value))return {status:'invalid',message:'报告格式无效,已回退字段阅读。',details:structuredClone(check.errors)};
43
+ return {status:'valid',report:value};
44
+ }catch(error){return {status:'invalid',message:`报告格式无效:${error.message},已回退字段阅读。`};}
45
+ }
46
+
47
+ export function createReport(content){
48
+ try{assertJson(content);}catch(error){throw new FlowError(`报告格式无效:${error.message}`,{code:'REPORT_INVALID'});}
49
+ const value={kind:'fst-report',version:1,...content};
50
+ const result=inspectReport(value);
51
+ if(result.status!=='valid')throw new FlowError(result.message??'报告格式无效',{code:'REPORT_INVALID',details:result.details});
52
+ return validate(reportSchema,value,'报告',{useDefaults:false});
53
+ }