@yanglaofish/fishtools 0.1.3 → 0.2.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 +53 -20
- package/manual/LICENSE +21 -0
- package/manual/README.md +173 -0
- package/manual/THIRD_PARTY_NOTICES.md +32 -0
- package/manual/docs/examples/action-plugin/LICENSE +21 -0
- package/manual/docs/examples/action-plugin/README.md +38 -0
- package/manual/docs/examples/action-plugin/index.js +18 -0
- package/manual/docs/examples/action-plugin/input.json +1 -0
- package/manual/docs/examples/action-plugin/package.json +9 -0
- package/manual/docs/plans/2026-10-10-workbench-ui.md +27 -0
- package/{docs → manual/docs}/plugin-development.md +151 -19
- package/{docs → manual/docs}/releases.md +18 -1
- package/manual/docs/roadmap.md +85 -0
- package/manual/docs/superpowers/plans/2026-10-09-workflow-mvp.md +115 -0
- package/manual/docs/superpowers/plans/2026-10-10-cordis-services.md +34 -0
- package/manual/docs/superpowers/plans/2026-10-10-fst-terminal.md +33 -0
- package/manual/docs/superpowers/plans/2026-10-10-portable-plugin-contract.md +35 -0
- package/manual/docs/superpowers/plans/2026-10-11-control-observation.md +77 -0
- package/manual/docs/superpowers/specs/2026-10-09-workflow-cli-design.md +377 -0
- package/manual/docs/superpowers/specs/2026-10-10-cordis-services-design.md +88 -0
- package/manual/docs/superpowers/specs/2026-10-10-portable-plugin-contract.md +31 -0
- package/manual/docs/superpowers/specs/2026-10-10-run-logs.md +28 -0
- package/manual/docs/superpowers/specs/2026-10-11-control-observation.md +37 -0
- package/{docs → manual/docs}/verification.md +20 -0
- package/manual/examples/service-plugins/README.md +20 -0
- package/manual/examples/service-plugins/consumer/LICENSE +21 -0
- package/manual/examples/service-plugins/consumer/index.js +14 -0
- package/manual/examples/service-plugins/consumer/package.json +1 -0
- package/manual/examples/service-plugins/provider/LICENSE +21 -0
- package/manual/examples/service-plugins/provider/index.js +19 -0
- package/manual/examples/service-plugins/provider/package.json +1 -0
- package/manual/packages/core/LICENSE +21 -0
- package/manual/packages/core/src/host.js +122 -0
- package/manual/packages/sdk/LICENSE +21 -0
- package/manual/packages/sdk/index.js +14 -0
- package/manual/packages/sdk/package.json +1 -0
- package/manual/plugins/environment/LICENSE +21 -0
- package/manual/plugins/environment/index.js +92 -0
- package/manual/plugins/environment/package.json +1 -0
- package/manual/plugins/examples/LICENSE +21 -0
- package/manual/plugins/examples/index.js +23 -0
- package/manual/plugins/examples/package.json +1 -0
- package/manual/plugins/examples/scripts/echo.py +5 -0
- package/manual/tests/application.test.js +45 -0
- package/node_modules/@mytools/core/bin/fst.js +12 -4
- package/node_modules/@mytools/core/src/application.js +9 -4
- package/node_modules/@mytools/core/src/cli-run.js +6 -0
- package/node_modules/@mytools/core/src/contracts.js +15 -2
- package/node_modules/@mytools/core/src/control.js +63 -0
- package/node_modules/@mytools/core/src/documentation.js +65 -0
- package/node_modules/@mytools/core/src/engine.js +80 -26
- package/node_modules/@mytools/core/src/host.js +13 -2
- package/node_modules/@mytools/core/src/markdown-log.js +8 -5
- package/node_modules/@mytools/core/src/process.js +35 -14
- package/node_modules/@mytools/core/src/project.js +1 -1
- package/node_modules/@mytools/core/src/run-logs.js +1 -1
- package/node_modules/@mytools/core/src/terminal/conditions.js +32 -0
- package/node_modules/@mytools/core/src/terminal/log-viewer.js +7 -8
- package/node_modules/@mytools/core/src/terminal/model.js +32 -3
- package/node_modules/@mytools/core/src/terminal/presentation.js +42 -0
- package/node_modules/@mytools/core/src/terminal/run-viewer.js +86 -0
- package/node_modules/@mytools/core/src/terminal/session.js +150 -31
- package/node_modules/@mytools/core/src/terminal/theme.js +28 -0
- package/node_modules/@mytools/core/src/terminal/view.js +92 -13
- package/node_modules/@mytools/core/src/validation.js +15 -4
- package/node_modules/@mytools/core/src/version.js +1 -1
- package/node_modules/@mytools/core/src/workflows.js +30 -19
- package/node_modules/@mytools/core/templates/control-demo.yaml +47 -0
- package/node_modules/@mytools/environment/index.js +14 -2
- package/node_modules/@mytools/pack/index.js +2 -0
- package/node_modules/@mytools/web/public/THIRD_PARTY_NOTICES.txt +1094 -1095
- package/node_modules/@mytools/web/public/assets/index-DT2B4tMV.js +70 -0
- package/node_modules/@mytools/web/public/assets/{index-BeV6IqTW.css → index-vuZvrUTT.css} +1 -1
- package/node_modules/@mytools/web/public/index.html +2 -2
- package/package.json +2 -2
- package/node_modules/@mytools/web/public/assets/index-TgsBnZgG.js +0 -70
- /package/{docs → manual/docs}/deployment.md +0 -0
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import {
|
|
5
|
-
import { TUI, Text, Container, Editor, SelectList, ProcessTerminal, CombinedAutocompleteProvider, matchesKey, truncateToWidth } from '@mariozechner/pi-tui';
|
|
4
|
+
import { TUI, Text, Container, Editor, SelectList, ProcessTerminal, CombinedAutocompleteProvider, matchesKey, truncateToWidth, visibleWidth } from '@mariozechner/pi-tui';
|
|
6
5
|
import { MarkdownLogViewer } from './log-viewer.js';
|
|
6
|
+
import { RunViewer, mouseOn, mouseOff } from './run-viewer.js';
|
|
7
|
+
import { logo } from './presentation.js';
|
|
8
|
+
import { VERSION } from '../version.js';
|
|
9
|
+
import {cleanText as safe,palette,menuTheme as theme,fitLine,shortenPath} from './theme.js';
|
|
7
10
|
|
|
8
11
|
export const cancelled=()=>Object.assign(new Error('已返回主菜单'),{code:'PROMPT_CANCELLED'});
|
|
9
|
-
const safe=value=>stripVTControlCharacters(String(value)).replace(/[\x00-\x08\x0b-\x1f\x7f]/g,'');
|
|
10
|
-
const theme={selectedPrefix:s=>`\x1b[36m${s}\x1b[0m`,selectedText:s=>`\x1b[36m${s}\x1b[0m`,description:s=>s,scrollInfo:s=>s,noMatch:s=>s};
|
|
11
12
|
// 上游菜单按字符数分配描述宽度,中文双宽字符需要再按终端列宽收口。
|
|
12
13
|
function fit(component){const render=component.render.bind(component);component.render=width=>render(width).map(line=>truncateToWidth(line,width));return component;}
|
|
13
14
|
|
|
@@ -46,15 +47,42 @@ export class TerminalView{
|
|
|
46
47
|
this.baseDir=baseDir;this.terminal=terminal;this.tui=new TUI(terminal);this.logs=[];
|
|
47
48
|
const originalStart=terminal.start.bind(terminal);
|
|
48
49
|
terminal.start=(input,resize)=>originalStart(data=>{
|
|
49
|
-
if(matchesKey(data,'ctrl+c')){if(this.cancelRun)this.cancelRun();this.pending?.reject(cancelled());return;}
|
|
50
|
-
if(matchesKey(data,'escape')&&this.pending&&!this.logViewer){
|
|
50
|
+
if(matchesKey(data,'ctrl+c')){if(this.runViewer){this.runViewer.onClose();return;}if(this.cancelRun)this.cancelRun();this.pending?.reject(cancelled());return;}
|
|
51
|
+
if(matchesKey(data,'escape')&&this.pending&&!this.logViewer&&!this.runViewer){
|
|
51
52
|
// 编辑器自己处理补全菜单 Escape,第二次 Escape 返回上一级。
|
|
52
53
|
if(this.editor?.isShowingAutocomplete?.()){input(data);return;}
|
|
53
54
|
this.pending.reject(cancelled());return;
|
|
54
55
|
}
|
|
55
56
|
input(data);
|
|
56
57
|
},resize);
|
|
57
|
-
this.header=new Text('',1,
|
|
58
|
+
this.header=new Text('',1,0);this.transcript=new Text('',1,0);this.prompt=new Container();this.hint=new Text('',1,0);
|
|
59
|
+
this.hint.render=width=>['',fitLine(' '+palette.muted(width<45?'↑↓ 选择 Enter 确认 Esc 返回':'↑↓ 选择 Enter 确认 Tab 补全 Esc 返回 Ctrl+C 退出'),width)];
|
|
60
|
+
const renderTranscript=this.transcript.render.bind(this.transcript);
|
|
61
|
+
this.headingLines=(width,mark='FISHtools')=>{
|
|
62
|
+
const room=Math.max(1,width-2),lines=[''];
|
|
63
|
+
if(mark===logo){
|
|
64
|
+
lines.push(...logo.split('\n').map((line,i)=>' '+palette.brand(line)+(i===2?' '+palette.text('tools')+' '+palette.muted(`v${VERSION}`):'')));
|
|
65
|
+
}else lines.push(' '+palette.brand('FISH')+palette.text('tools')+' '+palette.muted(`v${VERSION}`));
|
|
66
|
+
for(const [i,line] of (this.summaryText??'').split('\n').entries()){
|
|
67
|
+
if(line.startsWith('当前工作区:')){
|
|
68
|
+
const label='当前工作区:',folder=shortenPath(line.slice(label.length),Math.max(1,room-visibleWidth(label)));
|
|
69
|
+
lines.push(' '+palette.muted(label)+palette.text(folder));
|
|
70
|
+
}else lines.push(' '+(i===0?palette.text:palette.muted)(fitLine(line,room)));
|
|
71
|
+
}
|
|
72
|
+
lines.push('');return lines.map(line=>fitLine(line,width));
|
|
73
|
+
};
|
|
74
|
+
// 先保证输入与菜单可见,空间充足才展示三行字标。
|
|
75
|
+
this.header.render=width=>{
|
|
76
|
+
const full=this.headingLines(width,logo),other=renderTranscript(width).length+this.prompt.render(width).length+this.hint.render(width).length;
|
|
77
|
+
return width>=34&&this.terminal.rows>=18&&full.length+other<=this.terminal.rows?full:this.headingLines(width);
|
|
78
|
+
};
|
|
79
|
+
this.transcript.render=width=>{
|
|
80
|
+
const lines=renderTranscript(width),budget=Math.max(0,this.terminal.rows-this.header.render(width).length-this.prompt.render(width).length-this.hint.render(width).length);
|
|
81
|
+
if(lines.length<=budget)return lines;
|
|
82
|
+
if(!budget)return [];
|
|
83
|
+
const excerpt=this.logs.length<=1?lines.slice(0,budget-1):lines.slice(-(budget-1)||lines.length);
|
|
84
|
+
return [...excerpt,fitLine(' '+palette.muted('… /help 完整帮助 · /logs 运行日志'),width)];
|
|
85
|
+
};
|
|
58
86
|
this.tui.addChild(this.header);this.tui.addChild(this.transcript);this.tui.addChild(this.prompt);this.tui.addChild(this.hint);
|
|
59
87
|
}
|
|
60
88
|
start(){this.tui.start();}
|
|
@@ -63,8 +91,13 @@ export class TerminalView{
|
|
|
63
91
|
await new Promise(resolve=>process.nextTick(resolve));this.tui.stop();
|
|
64
92
|
if(this.terminal instanceof ProcessTerminal)process.stdin.pause();
|
|
65
93
|
}
|
|
66
|
-
summary(text){this.
|
|
67
|
-
|
|
94
|
+
summary(text){this.summaryText=safe(text);this.tui.requestRender();}
|
|
95
|
+
clearMessages(){this.logs=[];this.transcript.setText('');this.tui.requestRender();}
|
|
96
|
+
show(message,{kind='message'}={}){
|
|
97
|
+
const text=safe(message).slice(0,6000);
|
|
98
|
+
const formatted=kind==='welcome'?text.split('\n').map((line,i)=>i===0?palette.text(line):palette.muted(line)).join('\n'):text;
|
|
99
|
+
this.logs.push(formatted);this.logs=this.logs.slice(-6);this.transcript.setText(this.logs.join('\n\n'));this.tui.requestRender();
|
|
100
|
+
}
|
|
68
101
|
async viewMarkdown(title,text){
|
|
69
102
|
const previousPending=this.pending??null,previousViewer=this.logViewer;
|
|
70
103
|
const children=this.tui.children;
|
|
@@ -73,6 +106,7 @@ export class TerminalView{
|
|
|
73
106
|
await new Promise((resolve,reject)=>{
|
|
74
107
|
this.pending={resolve,reject};
|
|
75
108
|
this.logViewer=new MarkdownLogViewer({title:safe(title),text:safe(text),terminal:this.terminal,onClose:resolve,requestRender:()=>this.tui.requestRender()});
|
|
109
|
+
this.terminal.write(mouseOn);
|
|
76
110
|
// Overlay 的基底为空,旧 transcript 不再撑高滚动区域;关闭后原样恢复。
|
|
77
111
|
this.tui.children=[];
|
|
78
112
|
overlay=this.tui.showOverlay(this.logViewer,{width:'100%',maxHeight:'100%',anchor:'top-left',margin:0});
|
|
@@ -81,27 +115,72 @@ export class TerminalView{
|
|
|
81
115
|
}finally{
|
|
82
116
|
this.tui.children=children;this.pending=previousPending;this.logViewer=previousViewer;
|
|
83
117
|
overlay?.hide();this.tui.requestRender(true);
|
|
118
|
+
this.terminal.write(mouseOff);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async observeRun({app,runId,flow,workflows=[]}){
|
|
122
|
+
const previousPending=this.pending??null,children=this.tui.children;let overlay,unsubscribe;
|
|
123
|
+
try{
|
|
124
|
+
await new Promise((resolve,reject)=>{
|
|
125
|
+
this.pending={resolve,reject};
|
|
126
|
+
this.runViewer=new RunViewer({record:app.getRun(runId),flow,workflows,terminal:this.terminal,onClose:resolve,onCancel:()=>app.cancelRun(runId),requestRender:()=>this.tui.requestRender()});
|
|
127
|
+
unsubscribe=app.subscribe(event=>{if(event.runId===runId)this.runViewer?.update(app.getRun(runId));});
|
|
128
|
+
this.tui.children=[];this.terminal.write(mouseOn);
|
|
129
|
+
overlay=this.tui.showOverlay(this.runViewer,{width:'100%',maxHeight:'100%',anchor:'top-left',margin:0});this.tui.requestRender(true);
|
|
130
|
+
});
|
|
131
|
+
}finally{
|
|
132
|
+
unsubscribe?.();this.runViewer=null;this.tui.children=children;this.pending=previousPending;overlay?.hide();this.terminal.write(mouseOff);this.tui.requestRender(true);
|
|
84
133
|
}
|
|
85
134
|
}
|
|
86
135
|
async ask(message,component){
|
|
87
136
|
fit(component);
|
|
88
|
-
|
|
137
|
+
const title=new Text(palette.text(safe(message)),1,0);
|
|
138
|
+
if(message.startsWith('fst >'))title.render=width=>[fitLine(' '+palette.text(width<45?'fst > / 选择操作 · @ 文件':safe(message)),width)];
|
|
139
|
+
this.prompt.clear();this.prompt.addChild(title);this.prompt.addChild(component);this.tui.setFocus(component);this.tui.requestRender();
|
|
89
140
|
try{return await new Promise((resolve,reject)=>{this.pending={resolve,reject};component.onSubmit=resolve;component.onSelect=item=>resolve(item.value);component.onCancel=()=>reject(cancelled());});}
|
|
90
141
|
finally{this.pending=null;this.editor=null;this.prompt.clear();this.tui.requestRender();}
|
|
91
142
|
}
|
|
92
143
|
input(message,{value='',commands=[],files=true}={}){
|
|
93
|
-
const editor=new Editor(this.tui,{borderColor:
|
|
144
|
+
const editor=new Editor(this.tui,{borderColor:palette.border,selectList:theme},{paddingX:1});
|
|
145
|
+
const renderEditor=editor.render.bind(editor);
|
|
146
|
+
editor.render=width=>{
|
|
147
|
+
if(width<8)return renderEditor(width);
|
|
148
|
+
const inner=width-4,lines=renderEditor(inner);let closed=false;
|
|
149
|
+
return lines.map((line,i)=>{
|
|
150
|
+
if(i===0)return ' '+palette.border('╭'+'─'.repeat(inner)+'╮');
|
|
151
|
+
if(!closed&&/^─+$/.test(safe(line))){closed=true;return ' '+palette.border('╰'+'─'.repeat(inner)+'╯');}
|
|
152
|
+
if(closed)return ' '+line;
|
|
153
|
+
return ' '+palette.border('│')+line+' '.repeat(Math.max(0,inner-visibleWidth(line)))+palette.border('│');
|
|
154
|
+
});
|
|
155
|
+
};
|
|
94
156
|
this.editor=editor;editor.setText(value);
|
|
95
157
|
if(files||commands.length)editor.setAutocompleteProvider(new FileAutocomplete(commands,this.baseDir));
|
|
96
158
|
return this.ask(message,editor);
|
|
97
159
|
}
|
|
98
160
|
select(message,items){
|
|
99
161
|
if(!items.length)throw new Error('当前没有可选择的项目');
|
|
100
|
-
|
|
162
|
+
const list=new SelectList(items.map(i=>({...i,label:safe(i.label).replace(/\s+/g,' '),description:i.description?safe(i.description).replace(/\s+/g,' '):undefined})),8,theme);
|
|
163
|
+
list.render=width=>{
|
|
164
|
+
const reserved=this.headingLines(width).length+new Text(palette.text(safe(message)),1,0).render(width).length+this.hint.render(width).length+1;
|
|
165
|
+
list.maxVisible=Math.max(1,Math.min(8,this.terminal.rows-reserved));
|
|
166
|
+
const start=Math.max(0,Math.min(list.selectedIndex-Math.floor(list.maxVisible/2),list.filteredItems.length-list.maxVisible));
|
|
167
|
+
const end=Math.min(start+list.maxVisible,list.filteredItems.length),lines=[];
|
|
168
|
+
for(let i=start;i<end;i++){
|
|
169
|
+
const item=list.filteredItems[i],selected=i===list.selectedIndex,available=Math.max(1,width-4);
|
|
170
|
+
const labelWidth=item.description&&width>=56?Math.min(32,Math.floor(available*0.45)):available;
|
|
171
|
+
const label=fitLine(item.label||item.value,labelWidth),prefix=selected?'› ':' ';
|
|
172
|
+
let line=' '+(selected?palette.brand(prefix+label):palette.text(prefix+label));
|
|
173
|
+
if(item.description&&width>=56)line+=' '.repeat(Math.max(2,labelWidth-visibleWidth(label)+2))+palette.muted(fitLine(item.description,Math.max(1,available-labelWidth-2)));
|
|
174
|
+
lines.push(fitLine(line,width));
|
|
175
|
+
}
|
|
176
|
+
if(start>0||end<list.filteredItems.length)lines.push(palette.muted(` ${list.selectedIndex+1} / ${list.filteredItems.length}`));
|
|
177
|
+
return lines.length?lines:[palette.muted(' 没有匹配项')];
|
|
178
|
+
};
|
|
179
|
+
return this.ask(message,list);
|
|
101
180
|
}
|
|
102
181
|
async confirm(message){return await this.select(message,[{value:'no',label:'否,返回'},{value:'yes',label:'是,继续'}])==='yes';}
|
|
103
182
|
busy(message,cancel){
|
|
104
|
-
this.cancelRun=cancel;this.prompt.clear();this.prompt.addChild(new Text(safe(message),1,
|
|
183
|
+
this.cancelRun=cancel;this.prompt.clear();this.prompt.addChild(new Text(palette.text(safe(message)),1,0));this.tui.setFocus(this.prompt);this.tui.requestRender();
|
|
105
184
|
return ()=>{this.cancelRun=null;this.prompt.clear();this.tui.requestRender();};
|
|
106
185
|
}
|
|
107
186
|
}
|
|
@@ -3,12 +3,23 @@ import { FlowError } from '@mytools/sdk';
|
|
|
3
3
|
|
|
4
4
|
const ajv = new Ajv({ allErrors: true, strict: false, useDefaults: true });
|
|
5
5
|
const validators = new WeakMap();
|
|
6
|
+
const booleanValidators = new Map();
|
|
6
7
|
export const emptyObject = { type: 'object', properties: {}, additionalProperties: false };
|
|
7
8
|
|
|
9
|
+
export function assertSchema(schema,label) {
|
|
10
|
+
if (!(typeof schema==='boolean'||schema&&typeof schema==='object'&&!Array.isArray(schema))) throw new FlowError(`${label} 必须声明有效的 JSON Schema`,{code:'VALIDATION'});
|
|
11
|
+
const cache=typeof schema==='boolean'?booleanValidators:validators;
|
|
12
|
+
let check=cache.get(schema);
|
|
13
|
+
if(!check){
|
|
14
|
+
try{check=ajv.compile(schema);cache.set(schema,check);}
|
|
15
|
+
catch(cause){throw new FlowError(`${label} JSON Schema 无效:${cause.message}`,{code:'VALIDATION',cause});}
|
|
16
|
+
}
|
|
17
|
+
return check;
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
export function validate(schema, value, label) {
|
|
9
|
-
if (
|
|
10
|
-
|
|
11
|
-
if (!check) { check = ajv.compile(schema); validators.set(schema, check); }
|
|
21
|
+
if (schema==null) return structuredClone(value);
|
|
22
|
+
const check = assertSchema(schema,label);
|
|
12
23
|
const copy = structuredClone(value);
|
|
13
24
|
if (!check(copy)) throw new FlowError(`${label}: ${ajv.errorsText(check.errors, { separator: ';' })}`, { code: 'VALIDATION', details: check.errors });
|
|
14
25
|
return copy;
|
|
@@ -42,7 +53,7 @@ export function bind(value, context) {
|
|
|
42
53
|
if (Object.hasOwn(value, '$ref')) {
|
|
43
54
|
let current = context;
|
|
44
55
|
for (const field of referenceParts(value.$ref)) {
|
|
45
|
-
if (current == null || !Object.hasOwn(current, field)) throw new FlowError(`引用的值不存在:${value.$ref}
|
|
56
|
+
if (current == null || !Object.hasOwn(current, field)) throw new FlowError(`引用的值不存在:${value.$ref}`,{code:'MISSING_REFERENCE'});
|
|
46
57
|
current = current[field];
|
|
47
58
|
}
|
|
48
59
|
return structuredClone(current);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// 由 npm version 的生命周期脚本同步,内部插件 API 版本独立维护。
|
|
2
|
-
export const VERSION="0.
|
|
2
|
+
export const VERSION="0.2.0";
|
|
@@ -3,7 +3,8 @@ import path from 'node:path';
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import YAML from 'yaml';
|
|
5
5
|
import { FlowError } from '@mytools/sdk';
|
|
6
|
-
import { assertId, references, referenceParts, emptyObject } from './validation.js';
|
|
6
|
+
import { assertId, assertSchema, references, referenceParts, emptyObject } from './validation.js';
|
|
7
|
+
import {validateCondition,validateStepList} from './control.js';
|
|
7
8
|
|
|
8
9
|
function fieldSchema(schema,fields,ref){
|
|
9
10
|
let current=schema;
|
|
@@ -34,13 +35,7 @@ export class WorkflowStore {
|
|
|
34
35
|
validateShape(w) {
|
|
35
36
|
assertId(w?.id);
|
|
36
37
|
if (w.schemaVersion!==1 || !Array.isArray(w.steps)) throw new FlowError('工作流格式错误:需要 schemaVersion: 1 与 steps 列表',{code:'VALIDATION'});
|
|
37
|
-
|
|
38
|
-
for(const s of w.steps) {
|
|
39
|
-
assertId(s.id);
|
|
40
|
-
if(ids.has(s.id)) throw new FlowError(`步骤 ID 重复:${s.id}`,{code:'VALIDATION'});
|
|
41
|
-
ids.add(s.id);
|
|
42
|
-
if(Boolean(s.action)===Boolean(s.workflow) || (s.action && typeof s.action!=='string') || (s.workflow && typeof s.workflow!=='string')) throw new FlowError(`${s.id} 必须选择一个动作或子工作流`,{code:'VALIDATION'});
|
|
43
|
-
}
|
|
38
|
+
validateStepList(w.steps,assertId);
|
|
44
39
|
return structuredClone(w);
|
|
45
40
|
}
|
|
46
41
|
async save(definition) {
|
|
@@ -66,21 +61,37 @@ export class WorkflowStore {
|
|
|
66
61
|
if(visiting.has(id)) throw new FlowError(`工作流循环引用:${[...visiting,id].join(' → ')}`,{code:'VALIDATION'});
|
|
67
62
|
if(workflows.has(id)) return;
|
|
68
63
|
visiting.add(id);
|
|
69
|
-
const w=await this.get(id)
|
|
70
|
-
const
|
|
64
|
+
const w=await this.get(id);
|
|
65
|
+
const checker=previous=>ref=>{
|
|
71
66
|
const p=referenceParts(ref);
|
|
72
67
|
if(p[0]==='steps' && !previous.has(p[1])) throw new FlowError(`${id} 引用必须指向前序步骤:${ref}`,{code:'VALIDATION'});
|
|
73
68
|
return fieldSchema(p[0]==='inputs'?(w.inputs??emptyObject):previous.get(p[1]),p.slice(p[0]==='inputs'?1:3),ref);
|
|
74
69
|
};
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
if(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
70
|
+
const resolveList=async(steps,inherited=new Map(),parallel=false)=>{
|
|
71
|
+
const previous=new Map(inherited),own={};
|
|
72
|
+
for(const s of steps){
|
|
73
|
+
const check=checker(parallel?inherited:previous);
|
|
74
|
+
if(s.when)validateCondition(s.when,check);
|
|
75
|
+
let output;
|
|
76
|
+
if(s.action){
|
|
77
|
+
references(s.with??{},check);
|
|
78
|
+
const action=host.actions.get(s.action);
|
|
79
|
+
if(!action)throw new FlowError(`${id} → ${s.id} 缺少插件动作 ${s.action}`,{code:'VALIDATION'});
|
|
80
|
+
if(action.input===undefined||action.output===undefined)throw new FlowError(`可编排动作 ${s.action} 必须声明 input 和 output schema`,{code:'VALIDATION'});
|
|
81
|
+
assertSchema(action.input,`${s.action} input schema`);assertSchema(action.output,`${s.action} output schema`);
|
|
82
|
+
plugins.add(action.plugin);output=action.output;
|
|
83
|
+
}else if(s.workflow){references(s.with??{},check);const child=this.childId(s.workflow);await visit(child);output=outputs.get(child);}
|
|
84
|
+
else if(s.branch){
|
|
85
|
+
validateCondition(s.branch.condition,check);
|
|
86
|
+
const left=await resolveList(s.branch.then,parallel?inherited:previous),right=await resolveList(s.branch.else??[],parallel?inherited:previous);
|
|
87
|
+
output={type:'object',additionalProperties:false,properties:{...left.own,...right.own}};
|
|
88
|
+
}else if(s.parallel){const group=await resolveList(s.parallel.steps,parallel?inherited:previous,true);output={type:'object',additionalProperties:false,properties:group.own};}
|
|
89
|
+
else output=emptyObject;
|
|
90
|
+
previous.set(s.id,output);own[s.id]=output;
|
|
91
|
+
}
|
|
92
|
+
return {previous,own};
|
|
93
|
+
};
|
|
94
|
+
const {previous}=await resolveList(w.steps),check=checker(previous);
|
|
84
95
|
references(w.outputs??{},check);
|
|
85
96
|
const infer=value=>{
|
|
86
97
|
if(value?.$ref)return check(value.$ref);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
schemaVersion: 1
|
|
2
|
+
id: control-demo
|
|
3
|
+
title: 条件与并行入门
|
|
4
|
+
description: 检查系统后,根据你的选择继续或提前结束;继续时并行处理两段文本。运行中可切换步骤和查看日志。
|
|
5
|
+
inputs:
|
|
6
|
+
type: object
|
|
7
|
+
properties:
|
|
8
|
+
proceed:
|
|
9
|
+
type: boolean
|
|
10
|
+
title: 继续执行文本处理
|
|
11
|
+
default: true
|
|
12
|
+
required: [proceed]
|
|
13
|
+
steps:
|
|
14
|
+
- id: environment
|
|
15
|
+
action: "@mytools/environment:report"
|
|
16
|
+
with: {}
|
|
17
|
+
- id: guard
|
|
18
|
+
when:
|
|
19
|
+
left: {$ref: steps.environment.output.ok}
|
|
20
|
+
op: eq
|
|
21
|
+
right: false
|
|
22
|
+
stop:
|
|
23
|
+
reason: 环境检查未通过,已停止后续步骤。
|
|
24
|
+
- id: choose
|
|
25
|
+
branch:
|
|
26
|
+
condition:
|
|
27
|
+
left: {$ref: inputs.proceed}
|
|
28
|
+
op: eq
|
|
29
|
+
right: true
|
|
30
|
+
then:
|
|
31
|
+
- id: batch
|
|
32
|
+
parallel:
|
|
33
|
+
concurrency: 2
|
|
34
|
+
steps:
|
|
35
|
+
- id: first
|
|
36
|
+
action: "@mytools/examples:uppercase"
|
|
37
|
+
with: {text: "Hello, Fishtools!"}
|
|
38
|
+
- id: second
|
|
39
|
+
action: "@mytools/examples:uppercase"
|
|
40
|
+
with: {text: Parallel workflow}
|
|
41
|
+
else:
|
|
42
|
+
- id: finish
|
|
43
|
+
stop:
|
|
44
|
+
reason: 本次选择提前结束,没有执行文本处理。
|
|
45
|
+
outputs:
|
|
46
|
+
first: {$ref: steps.choose.output.batch.first.text}
|
|
47
|
+
second: {$ref: steps.choose.output.batch.second.text}
|
|
@@ -17,7 +17,7 @@ for item in names:
|
|
|
17
17
|
packages.append({'name':name,'ok':False,'error':str(e)})
|
|
18
18
|
print(json.dumps({'executable':sys.executable,'version':sys.version.split()[0],'prefix':sys.prefix,'packages':packages}))`;
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
const plugin={
|
|
21
21
|
id:'@mytools/environment',version:'0.1.0',title:'环境检查',description:'检查目标机器的系统、Python/pip 与昇腾 NPU 环境,缺失必需条件时停止工作流。',
|
|
22
22
|
platforms:['win32','linux'],commands:{'env.check':{description:'检查系统、Python/pip 或 NPU',action:'check'}},
|
|
23
23
|
actions:{
|
|
@@ -77,4 +77,16 @@ export default definePlugin({
|
|
|
77
77
|
},
|
|
78
78
|
},
|
|
79
79
|
},
|
|
80
|
-
}
|
|
80
|
+
};
|
|
81
|
+
// 报告模式将“检测完成”与“检测通过”分开;旧 check 保持失败即终止的兼容语义。
|
|
82
|
+
plugin.actions.report={
|
|
83
|
+
...plugin.actions.check,
|
|
84
|
+
title:'生成环境报告',description:'返回结构化检测结果;未通过时返回 ok:false,由工作流条件决定是否继续。',
|
|
85
|
+
conditions:{passed:{title:'环境检查通过',path:'ok',op:'eq',value:true}},
|
|
86
|
+
async run(input,context){
|
|
87
|
+
try{return await plugin.actions.check.run(input,context);}
|
|
88
|
+
catch(error){if(error.code==='ENVIRONMENT_FAILED')return error.details;throw error;}
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
plugin.commands['env.report']={description:'检测环境并返回报告(不因检查未通过抛错)',action:'report'};
|
|
92
|
+
export default definePlugin(plugin);
|
|
@@ -9,6 +9,7 @@ import YAML from 'yaml';
|
|
|
9
9
|
import { verifyBundle } from '@mytools/core/bundle';
|
|
10
10
|
import {planEnvironments} from './environments.js';
|
|
11
11
|
import {archiveDirectory} from './archive.js';
|
|
12
|
+
import {copyDocumentation} from '../core/src/documentation.js';
|
|
12
13
|
export {verifyBundle};
|
|
13
14
|
|
|
14
15
|
async function hashes(root,dir=root,result={}){
|
|
@@ -84,6 +85,7 @@ export async function packApplication(app,{entries,target,outDir='dist',includeW
|
|
|
84
85
|
for(const e of environmentPlan.entries.filter(e=>e.source==='bundled'))for(const p of packages.filter(p=>p.name===e.plugin)){
|
|
85
86
|
const file=['app',p.path,e.executable].join('/');executablePaths.add(file);await chmod(path.join(directory,file),0o755);
|
|
86
87
|
}
|
|
88
|
+
await copyDocumentation(path.join(directory,'manual'));
|
|
87
89
|
const manifest={format:2,bundleId,editable:!readOnly,createdAt:new Date().toISOString(),target,runtime,entries,workflows:closure.workflows.map(w=>w.id),plugins:plugins.map(p=>({id:p.id,version:p.version,platforms:p.platforms,architectures:p.architectures})),environments:environmentPlan.entries,warnings:environmentPlan.warnings,packages,files:await hashes(directory)};
|
|
88
90
|
await writeFile(path.join(directory,'manifest.json'),JSON.stringify(manifest,null,2));
|
|
89
91
|
const archive=await archiveDirectory(directory,target,executablePaths);
|