@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.
- package/README.md +3 -3
- package/manual/README.md +3 -3
- package/manual/docs/modules.md +2 -0
- package/manual/docs/plans/2026-10-13-effective-inputs-and-tables.md +39 -0
- package/manual/docs/plans/2026-10-13-plugin-defaults.md +45 -0
- package/manual/docs/plans/2026-10-14-issue7-recovery.md +33 -0
- package/manual/docs/plans/2026-10-14-issue8-10-workflows.md +26 -0
- package/manual/docs/plans/2026-10-14-issue9-completion.md +47 -0
- package/manual/docs/plans/2026-10-14-managed-execution.md +58 -0
- package/manual/docs/plans/2026-10-14-plugin-discovery-history.md +41 -0
- package/manual/docs/plans/2026-10-14-structured-reports.md +77 -0
- package/manual/docs/plans/2026-10-14-unreleased-review.md +47 -0
- package/manual/docs/plugin-development.md +161 -8
- package/manual/docs/releases.md +25 -6
- package/manual/docs/superpowers/plans/2026-10-13-tui-readable-sessions.md +2 -2
- package/manual/docs/superpowers/reviews/2026-10-13-tui-sessions-review.md +2 -0
- package/manual/docs/superpowers/reviews/2026-10-14-0.6.0-release-review.md +19 -0
- package/manual/docs/verification.md +17 -0
- package/manual/packages/core/README.md +6 -0
- package/manual/packages/core/src/application.js +15 -9
- package/manual/packages/core/src/plugin-catalog.js +20 -2
- package/manual/packages/core/src/plugin-loading.js +24 -0
- package/manual/packages/core/src/version.js +1 -1
- package/manual/packages/fst-cli/README.md +5 -1
- package/manual/packages/fst-cli/src/terminal/command-menu.js +3 -2
- package/manual/packages/fst-cli/src/terminal/file-autocomplete.js +106 -0
- package/manual/packages/fst-cli/src/terminal/log-viewer.js +4 -3
- package/manual/packages/fst-cli/src/terminal/navigation.js +23 -1
- package/manual/packages/fst-cli/src/terminal/presentation.js +2 -1
- package/manual/packages/fst-cli/src/terminal/report-presentation.js +38 -0
- package/manual/packages/fst-cli/src/terminal/result-table.js +66 -0
- package/manual/packages/fst-cli/src/terminal/run-feed.js +42 -17
- package/manual/packages/fst-cli/src/terminal/run-presentation.js +51 -17
- package/manual/packages/fst-cli/src/terminal/run-viewer.js +15 -12
- package/manual/packages/fst-cli/src/terminal/schema.js +114 -4
- package/manual/packages/fst-cli/src/terminal/session-history.js +2 -2
- package/manual/packages/fst-cli/src/terminal/session.js +40 -15
- package/manual/packages/fst-cli/src/terminal/view.js +80 -68
- package/manual/packages/fst-cli/src/terminal/workflow-menu.js +3 -1
- package/manual/packages/fst-devtools/README.md +2 -0
- package/manual/packages/fst-devtools/src/plugin-quality.js +4 -1
- package/manual/packages/fst-devtools/src/plugin-tools.js +1 -0
- package/manual/packages/fst-devtools/src/process-usage.js +62 -0
- package/manual/packages/fst-platform/README.md +5 -0
- package/manual/packages/fst-platform/package.json +1 -0
- package/manual/packages/fst-platform/src/managed-process.js +36 -0
- package/manual/packages/fst-platform/src/process.js +7 -2
- package/manual/packages/fst-plugin/README.md +4 -0
- package/manual/packages/fst-plugin/src/contracts.js +3 -0
- package/manual/packages/fst-plugin/src/host.js +38 -23
- package/manual/packages/fst-plugin/src/runtime.js +14 -8
- package/manual/packages/fst-workflow/src/engine.js +1 -1
- package/manual/packages/fst-workspace/README.md +4 -0
- package/manual/packages/fst-workspace/src/plugin-registry.js +1 -1
- package/manual/packages/fst-workspace/src/plugins.js +5 -3
- package/manual/packages/fst-workspace/src/workflows.js +27 -9
- package/manual/packages/sdk/README.md +10 -0
- package/manual/packages/sdk/inputs.js +53 -0
- package/manual/packages/sdk/package.json +2 -0
- package/manual/packages/sdk/reports.js +53 -0
- package/node_modules/@mytools/core/README.md +6 -0
- package/node_modules/@mytools/core/src/application.js +15 -9
- package/node_modules/@mytools/core/src/plugin-catalog.js +20 -2
- package/node_modules/@mytools/core/src/plugin-loading.js +24 -0
- package/node_modules/@mytools/core/src/version.js +1 -1
- package/node_modules/@mytools/fst-cli/README.md +5 -1
- package/node_modules/@mytools/fst-cli/src/terminal/command-menu.js +3 -2
- package/node_modules/@mytools/fst-cli/src/terminal/file-autocomplete.js +106 -0
- package/node_modules/@mytools/fst-cli/src/terminal/log-viewer.js +4 -3
- package/node_modules/@mytools/fst-cli/src/terminal/navigation.js +23 -1
- package/node_modules/@mytools/fst-cli/src/terminal/presentation.js +2 -1
- package/node_modules/@mytools/fst-cli/src/terminal/report-presentation.js +38 -0
- package/node_modules/@mytools/fst-cli/src/terminal/result-table.js +66 -0
- package/node_modules/@mytools/fst-cli/src/terminal/run-feed.js +42 -17
- package/node_modules/@mytools/fst-cli/src/terminal/run-presentation.js +51 -17
- package/node_modules/@mytools/fst-cli/src/terminal/run-viewer.js +15 -12
- package/node_modules/@mytools/fst-cli/src/terminal/schema.js +114 -4
- package/node_modules/@mytools/fst-cli/src/terminal/session-history.js +2 -2
- package/node_modules/@mytools/fst-cli/src/terminal/session.js +40 -15
- package/node_modules/@mytools/fst-cli/src/terminal/view.js +80 -68
- package/node_modules/@mytools/fst-cli/src/terminal/workflow-menu.js +3 -1
- package/node_modules/@mytools/fst-devtools/README.md +2 -0
- package/node_modules/@mytools/fst-devtools/src/plugin-quality.js +4 -1
- package/node_modules/@mytools/fst-devtools/src/plugin-tools.js +1 -0
- package/node_modules/@mytools/fst-devtools/src/process-usage.js +62 -0
- package/node_modules/@mytools/fst-platform/README.md +5 -0
- package/node_modules/@mytools/fst-platform/package.json +1 -0
- package/node_modules/@mytools/fst-platform/src/managed-process.js +36 -0
- package/node_modules/@mytools/fst-platform/src/process.js +7 -2
- package/node_modules/@mytools/fst-plugin/README.md +4 -0
- package/node_modules/@mytools/fst-plugin/src/contracts.js +3 -0
- package/node_modules/@mytools/fst-plugin/src/host.js +38 -23
- package/node_modules/@mytools/fst-plugin/src/runtime.js +14 -8
- package/node_modules/@mytools/fst-workflow/src/engine.js +1 -1
- package/node_modules/@mytools/fst-workspace/README.md +4 -0
- package/node_modules/@mytools/fst-workspace/src/plugin-registry.js +1 -1
- package/node_modules/@mytools/fst-workspace/src/plugins.js +5 -3
- package/node_modules/@mytools/fst-workspace/src/workflows.js +27 -9
- package/node_modules/@mytools/sdk/README.md +10 -0
- package/node_modules/@mytools/sdk/inputs.js +53 -0
- package/node_modules/@mytools/sdk/package.json +2 -0
- package/node_modules/@mytools/sdk/reports.js +53 -0
- package/node_modules/@mytools/web/public/THIRD_PARTY_NOTICES.txt +1032 -1032
- 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=(
|
|
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=
|
|
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=
|
|
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
|
-
//
|
|
223
|
-
export function
|
|
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(
|
|
230
|
-
|
|
231
|
-
for(const
|
|
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())
|
|
239
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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=>
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|