@yanglaofish/fishtools 0.1.4 → 0.3.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 +79 -25
- package/manual/LICENSE +21 -0
- package/manual/README.md +199 -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/examples/pipeline-reader/LICENSE +21 -0
- package/manual/docs/examples/pipeline-reader/index.js +14 -0
- package/manual/docs/examples/pipeline-reader/package.json +7 -0
- package/manual/docs/examples/pipeline-writer/LICENSE +21 -0
- package/manual/docs/examples/pipeline-writer/index.js +17 -0
- package/manual/docs/examples/pipeline-writer/package.json +7 -0
- package/manual/docs/plans/2026-10-10-workbench-ui.md +27 -0
- package/{docs → manual/docs}/plugin-development.md +172 -20
- package/{docs → manual/docs}/releases.md +32 -2
- package/manual/docs/research/2026-10-11-plugin-kernel-capabilities.md +167 -0
- package/manual/docs/roadmap.md +90 -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/plans/2026-10-11-public-plugin-fixes.md +40 -0
- package/manual/docs/superpowers/plans/2026-10-11-terminal-hierarchy.md +40 -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-collapsed-plugin-output.md +23 -0
- package/manual/docs/superpowers/specs/2026-10-11-control-observation.md +37 -0
- package/{docs → manual/docs}/verification.md +97 -1
- 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/application.js +211 -0
- package/manual/packages/core/src/engine.js +93 -0
- package/manual/packages/core/src/host.js +124 -0
- package/manual/packages/core/src/runtime.js +135 -0
- package/manual/packages/core/src/terminal/run-feed.js +78 -0
- package/manual/packages/core/src/terminal/session.js +438 -0
- package/manual/packages/core/src/terminal/view.js +283 -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 +75 -0
- package/node_modules/@mytools/core/bin/fst.js +12 -4
- package/node_modules/@mytools/core/src/application.js +11 -5
- 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 +15 -2
- package/node_modules/@mytools/core/src/markdown-log.js +9 -6
- package/node_modules/@mytools/core/src/process.js +49 -15
- 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 +33 -4
- package/node_modules/@mytools/core/src/terminal/navigation.js +17 -0
- package/node_modules/@mytools/core/src/terminal/presentation.js +8 -10
- package/node_modules/@mytools/core/src/terminal/run-feed.js +78 -0
- package/node_modules/@mytools/core/src/terminal/run-presentation.js +60 -0
- package/node_modules/@mytools/core/src/terminal/run-viewer.js +104 -0
- package/node_modules/@mytools/core/src/terminal/session.js +212 -57
- package/node_modules/@mytools/core/src/terminal/theme.js +29 -0
- package/node_modules/@mytools/core/src/terminal/view.js +185 -33
- package/node_modules/@mytools/core/src/terminal/workflow-menu.js +50 -0
- package/node_modules/@mytools/core/src/validation.js +17 -4
- package/node_modules/@mytools/core/src/version.js +1 -1
- package/node_modules/@mytools/core/src/workflows.js +41 -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 +6 -2
- package/node_modules/@mytools/web/public/THIRD_PARTY_NOTICES.txt +780 -780
- 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
|
@@ -15,7 +15,7 @@ export function parseCommand(text){
|
|
|
15
15
|
}
|
|
16
16
|
if(quote)throw new Error('文件路径的引号未闭合');
|
|
17
17
|
if(started)args.push(token);
|
|
18
|
-
if(!args[0]?.startsWith('/'))throw new Error('
|
|
18
|
+
if(!args[0]?.startsWith('/'))throw Object.assign(new Error('这不是斜杠命令。输入 / 选择操作;执行工作流请用 /run 工作流ID。'),{code:'COMMAND_INPUT'});
|
|
19
19
|
args[0]=args[0].slice(1);return args;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -38,8 +38,21 @@ export async function readRunInput(reference,baseDir){
|
|
|
38
38
|
|
|
39
39
|
export const newWorkflow=(title,description)=>({schemaVersion:1,id:`flow-${randomUUID().slice(0,8)}`,title,description,inputs:{type:'object',properties:{}},steps:[],outputs:{}});
|
|
40
40
|
|
|
41
|
+
// 路径从根 steps 开始,例如 [1, 'branch', 'then', 0]。
|
|
42
|
+
export const stepAt=(flow,index)=>[...(Array.isArray(index)?index:[index])].reduce((node,key)=>node[key],flow.steps);
|
|
43
|
+
export const sequenceAt=(flow,parts=[])=>parts.reduce((node,key)=>node[key],flow.steps);
|
|
44
|
+
export function precedingSteps(flow,index){
|
|
45
|
+
const parts=Array.isArray(index)?index:[index];let sequence=flow.steps;const result=[];
|
|
46
|
+
for(let i=0;i<parts.length;i+=3){
|
|
47
|
+
// 并行子项同属一批,不能把前面的兄弟当成已完成步骤。
|
|
48
|
+
if(i===0||parts[i-2]!=='parallel')result.push(...sequence.slice(0,parts[i]));
|
|
49
|
+
if(i+1<parts.length)sequence=sequence[parts[i]][parts[i+1]][parts[i+2]];
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
41
54
|
export function setBinding(flow,index,key,mode,schema,value){
|
|
42
|
-
const step=flow
|
|
55
|
+
const step=stepAt(flow,index),old=step.with?.[key];step.with??={};
|
|
43
56
|
if(mode==='runtime'){
|
|
44
57
|
flow.inputs??={type:'object',properties:{}};flow.inputs.properties??={};
|
|
45
58
|
const name=`fst_${step.id}_${key}`;
|
|
@@ -60,15 +73,31 @@ export function setBinding(flow,index,key,mode,schema,value){
|
|
|
60
73
|
}
|
|
61
74
|
}
|
|
62
75
|
|
|
76
|
+
export function pruneRuntimeInputs(flow){
|
|
77
|
+
const refs=new Set();
|
|
78
|
+
const scan=value=>{if(value&&typeof value==='object'){if(typeof value.$ref==='string')refs.add(value.$ref);Object.values(value).forEach(scan);}};
|
|
79
|
+
scan(flow.steps);scan(flow.outputs);
|
|
80
|
+
for(const name of Object.keys(flow.inputs?.properties??{})){
|
|
81
|
+
if(name.startsWith('fst_')&&!refs.has(`inputs.${name}`)){
|
|
82
|
+
delete flow.inputs.properties[name];flow.inputs.required=(flow.inputs.required??[]).filter(key=>key!==name);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
63
87
|
export function stepMetadata(step,catalog,workflows){
|
|
88
|
+
if(step.branch||step.parallel){
|
|
89
|
+
const children=step.parallel?.steps??[...(step.branch.then??[]),...(step.branch.else??[])];
|
|
90
|
+
return {title:step.title??(step.branch?'条件分支':'并行组'),output:{type:'object',properties:Object.fromEntries(children.map(child=>[child.id,stepMetadata(child,catalog,workflows)?.output??{}]))}};
|
|
91
|
+
}
|
|
92
|
+
if(step.stop)return {title:step.title??'提前结束',output:{type:'object',properties:{}}};
|
|
64
93
|
if(step.action)return catalog.find(a=>a.id===step.action);
|
|
65
|
-
const flow=workflows.find(w
|
|
94
|
+
const flow=workflows.find(w=>`${w.id}.yaml`===step.workflow?.replace(/^\.\//,''));
|
|
66
95
|
return flow?{title:flow.title??flow.id,description:flow.description,input:flow.inputs,output:{type:'object',properties:Object.fromEntries(Object.keys(flow.outputs??{}).map(k=>[k,{}]))}}:null;
|
|
67
96
|
}
|
|
68
97
|
|
|
69
98
|
export function outputSources(flow,index,catalog,workflows){
|
|
70
99
|
const flatten=(schema,prefix='')=>Object.entries(schema?.properties??{}).flatMap(([key,s])=>{const name=prefix?`${prefix}.${key}`:key;return [{name,schema:s},...flatten(s,name)];});
|
|
71
|
-
return flow
|
|
100
|
+
return precedingSteps(flow,index).flatMap((step,i)=>{
|
|
72
101
|
const meta=stepMetadata(step,catalog,workflows),base=`steps.${step.id}.output`,label=`${i+1}. ${meta?.title??step.id}`;
|
|
73
102
|
return [{value:base,label:`${label} → 完整输出`,schema:meta?.output},...flatten(meta?.output).map(f=>({value:`${base}.${f.name}`,label:`${label} → ${f.schema.title??f.name}`,schema:f.schema}))];
|
|
74
103
|
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {builtinCommands} from '../contracts.js';
|
|
2
|
+
|
|
3
|
+
const visible=['workflows','run','pack','logs','plugin','config','tools','help','exit'];
|
|
4
|
+
// 入口由插件 ID 推导;冲突时加前缀,不能覆盖内置或作者注册的命令。
|
|
5
|
+
export function commandMenu(plugins,commands){
|
|
6
|
+
const entries=builtinCommands.filter(command=>visible.includes(command.name)).map(command=>({...command,description:`[内置] ${command.description}`}));
|
|
7
|
+
const used=new Set([...builtinCommands.map(c=>c.name),...commands.map(c=>c.name)]);
|
|
8
|
+
const groups=plugins.filter(plugin=>commands.some(c=>c.plugin===plugin.id)).sort((a,b)=>a.id.localeCompare(b.id));
|
|
9
|
+
for(const plugin of groups){
|
|
10
|
+
const leaf=plugin.id.split('/').at(-1).replace(/[^\w.-]/g,'-');
|
|
11
|
+
let name=/^[a-zA-Z]/.test(leaf)?leaf:`plugin-${leaf}`;
|
|
12
|
+
if(groups.filter(p=>p.id.split('/').at(-1)===leaf).length>1)name=plugin.id.replace(/^@/,'').replace(/[^\w.-]/g,'-');
|
|
13
|
+
while(used.has(name))name='plugin-'+name;
|
|
14
|
+
used.add(name);entries.push({name,plugin:plugin.id,description:`[插件] ${plugin.title??plugin.id} · ${plugin.id}`});
|
|
15
|
+
}
|
|
16
|
+
return entries;
|
|
17
|
+
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
export const logo=[
|
|
2
|
-
'
|
|
3
|
-
'
|
|
4
|
-
'
|
|
5
|
-
'F I S H H t o o o o l ss',
|
|
6
|
-
'F IIIII SSSS H H tt oo oo l sss',
|
|
2
|
+
'█▀▀ ▀█▀ █▀▀ █ █',
|
|
3
|
+
'█▀ █ ▀▀█ █▀█',
|
|
4
|
+
'▀ ▀▀▀ ▀▀▀ ▀ ▀',
|
|
7
5
|
].join('\n');
|
|
8
6
|
|
|
9
7
|
export const pluginStatus=plugin=>({ready:'可用',pending:'等待服务',failed:'失败',stopped:'已停止'})[plugin.runtime?.status]??'未知';
|
|
@@ -11,10 +9,10 @@ export const pluginStatus=plugin=>({ready:'可用',pending:'等待服务',failed
|
|
|
11
9
|
export function gettingStarted(flows,{readOnly=false}={}){
|
|
12
10
|
const sample=flows.find(w=>w.id==='hello')??flows[0];
|
|
13
11
|
return [
|
|
14
|
-
sample
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
sample?`试运行 /run ${sample.id}\n${sample.description??sample.title??'按提示填写参数,执行后查看结果。'}`:'当前工作区还没有工作流。',
|
|
13
|
+
'',
|
|
14
|
+
`/workflows ${readOnly?'浏览工作流':'管理工作流'} /plugin 插件 /config 工具配置`,
|
|
15
|
+
`/logs 回看执行记录、输入输出与日志 /help 帮助`,
|
|
18
16
|
].join('\n');
|
|
19
17
|
}
|
|
20
18
|
|
|
@@ -39,6 +37,6 @@ export function pluginDetails(plugin,actions,commands){
|
|
|
39
37
|
'## 环境声明',
|
|
40
38
|
...[plugin,...actions].flatMap(item=>Object.entries(item.environments??{}).map(([name,e])=>`- ${item.id} / ${name}:${({host:'使用宿主机环境',bundled:'随包携带',tool:'使用 Fishtools 自带环境'})[e.source]??e.source}`)),
|
|
41
39
|
'环境与平台由插件作者声明;具体执行结果请在 /logs 查看。',
|
|
42
|
-
'编排用法:/
|
|
40
|
+
'编排用法:/workflows 新建工作流 → 添加步骤并选择上述能力 → 填写参数 → 保存 → 运行。也可在主面板用 /run 选择已保存的工作流。',
|
|
43
41
|
].join('\n\n');
|
|
44
42
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {readableValue,readableError,readableTimeline} from './run-presentation.js';
|
|
2
|
+
import {runStatuses} from './run-viewer.js';
|
|
3
|
+
|
|
4
|
+
// 主面板按事件到达顺序追加;运行快照另存,配置菜单不会清掉执行记录。
|
|
5
|
+
export class RunFeed{
|
|
6
|
+
constructor(refresh){this.refresh=refresh;this.runs=new Map();this.entries=[];this.subscriptions=new Map();this.expanded=false;}
|
|
7
|
+
track({app,runId,title,input}){
|
|
8
|
+
if(this.runs.has(runId))return;
|
|
9
|
+
const record=app.getRun(runId),item={app,record,title,input,count:0,ended:false,steps:new Map(),catalog:new Map((app.catalog?.()??[]).map(action=>[action.id,action]))};
|
|
10
|
+
this.runs.set(runId,item);
|
|
11
|
+
this.entries.push({type:'start',item});
|
|
12
|
+
if(!this.subscriptions.has(app))this.subscriptions.set(app,app.subscribe(event=>{
|
|
13
|
+
const item=this.runs.get(event.runId);if(!item||item.app!==app)return;
|
|
14
|
+
this.update(item,event);this.release(app);
|
|
15
|
+
if(event.type==='log'&&!item.ended)this.scheduleRefresh();
|
|
16
|
+
else this.flushRefresh();
|
|
17
|
+
}));
|
|
18
|
+
this.update(item);this.release(app);this.refresh();
|
|
19
|
+
}
|
|
20
|
+
scheduleRefresh(){
|
|
21
|
+
if(this.refreshTimer)return;
|
|
22
|
+
this.refreshTimer=setTimeout(()=>{this.refreshTimer=null;this.refresh();},100);
|
|
23
|
+
}
|
|
24
|
+
flushRefresh(){
|
|
25
|
+
if(this.refreshTimer){clearTimeout(this.refreshTimer);this.refreshTimer=null;}
|
|
26
|
+
this.refresh();
|
|
27
|
+
}
|
|
28
|
+
update(item,event){
|
|
29
|
+
if(event?.type&&!['run.succeeded','run.failed','run.cancelled','run.stopped'].includes(event.type))item.record.events.push(event);
|
|
30
|
+
else{
|
|
31
|
+
const record=item.app.getRun(item.record.id);if(!record)return;
|
|
32
|
+
item.record={...record,events:[...(record.events??[])]};
|
|
33
|
+
}
|
|
34
|
+
const {record}=item,events=record.events.slice(item.count);item.count=record.events.length;
|
|
35
|
+
for(const event of events)if(event.type.startsWith('step.'))item.steps.set(event.callPath,{...item.steps.get(event.callPath),...event});
|
|
36
|
+
if(events.length){
|
|
37
|
+
const last=this.entries.at(-1);
|
|
38
|
+
if(last?.type==='events'&&last.item===item)last.events.push(...events);
|
|
39
|
+
else this.entries.push({type:'events',item,events});
|
|
40
|
+
}
|
|
41
|
+
if(record.status!=='running'&&!item.ended){item.ended=true;this.entries.push({type:'result',item});}
|
|
42
|
+
}
|
|
43
|
+
release(app){
|
|
44
|
+
if([...this.runs.values()].some(item=>item.app===app&&!item.ended))return;
|
|
45
|
+
this.subscriptions.get(app)?.();this.subscriptions.delete(app);
|
|
46
|
+
}
|
|
47
|
+
message(text){this.entries.push({type:'message',text});this.refresh();}
|
|
48
|
+
compactEvents(entry,seen){
|
|
49
|
+
const {item}=entry,rows=[];
|
|
50
|
+
for(const event of entry.events){
|
|
51
|
+
const key=`${item.record.id}\0${event.callPath}`;
|
|
52
|
+
if(event.type.startsWith('step.')&&!seen.has(key)){
|
|
53
|
+
seen.add(key);const latest=item.steps.get(event.callPath),action=item.catalog.get(latest.action);
|
|
54
|
+
const status=latest.type.slice(5),symbols={started:'●',succeeded:'✓',failed:'✗',cancelled:'×',skipped:'–',stopped:'■'};
|
|
55
|
+
const name=action?.title??latest.action??latest.stepId??latest.callPath;
|
|
56
|
+
rows.push(`${symbols[status]??'·'} ${name} · ${status==='started'?'执行中':runStatuses[status]??status}${latest.durationMs!==undefined?' · '+latest.durationMs+' ms':''}${action?.plugin?' · '+action.plugin:''} · ${latest.callPath}`);
|
|
57
|
+
}
|
|
58
|
+
if(event.type==='log'&&event.audience==='user')rows.push(`[${event.callPath??item.record.workflowId}] ${event.message??''}${event.data!==undefined?'\n'+readableValue(event.data):''}`);
|
|
59
|
+
if(event.type==='step.failed'&&![...item.steps.values()].some(step=>step.type==='step.failed'&&step.callPath?.startsWith(event.callPath+'/')))rows.push(`✗ ${event.callPath}\n${readableError(event.error??{message:event.message})}`);
|
|
60
|
+
if(event.type==='step.stopped'||event.type==='step.skipped')rows.push(`${event.callPath}:${event.reason??event.message??''}`);
|
|
61
|
+
}
|
|
62
|
+
return rows.length?`${item.title??item.record.workflowId} · ${item.record.id.slice(0,8)}\n${rows.join('\n')}`:'';
|
|
63
|
+
}
|
|
64
|
+
text(){const seen=new Set();return this.entries.map(entry=>{
|
|
65
|
+
if(entry.type==='message')return entry.text;
|
|
66
|
+
const {record,title,input}=entry.item,label=`${title??record.workflowId} · ${record.id.slice(0,8)}`;
|
|
67
|
+
if(entry.type==='start')return [
|
|
68
|
+
`› ${record.workflowId.startsWith('/')?record.workflowId:'/run '+record.workflowId} · ${label}`,
|
|
69
|
+
`状态:${runStatuses[record.status]??record.status}`,
|
|
70
|
+
this.expanded&&input&&Object.keys(input).length?'输入\n'+readableValue(input):'',
|
|
71
|
+
].filter(Boolean).join('\n');
|
|
72
|
+
if(entry.type==='events')return this.expanded?`${label}\n${readableTimeline(entry.events,{values:true})}`:this.compactEvents(entry,seen);
|
|
73
|
+
return [`结果 · ${label} · ${runStatuses[record.status]??record.status}`,
|
|
74
|
+
this.expanded&&record.output!==undefined?readableValue(record.output):'',record.error&&(this.expanded||![...entry.item.steps.values()].some(step=>step.type==='step.failed'))?readableError(record.error,{details:this.expanded}):'',record.reason??'',
|
|
75
|
+
].filter(Boolean).join('\n');
|
|
76
|
+
}).filter(Boolean).join('\n\n');}
|
|
77
|
+
close(){if(this.refreshTimer)clearTimeout(this.refreshTimer);this.refreshTimer=null;for(const unsubscribe of this.subscriptions.values())unsubscribe();this.subscriptions.clear();}
|
|
78
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// 阅读层只处理展示;原始事件、输入输出与错误仍保留在运行记录中。
|
|
2
|
+
export function readableValue(value,indent=''){
|
|
3
|
+
if(value===undefined)return '暂无记录';
|
|
4
|
+
if(value===null)return 'null';
|
|
5
|
+
if(typeof value!=='object')return String(value);
|
|
6
|
+
const entries=Array.isArray(value)?value.map((v,i)=>[`[${i+1}]`,v]):Object.entries(value);
|
|
7
|
+
if(!entries.length)return Array.isArray(value)?'(空列表)':'(空对象)';
|
|
8
|
+
return entries.map(([key,item])=>{
|
|
9
|
+
const nested=item!==null&&typeof item==='object';
|
|
10
|
+
return `${indent}${key}:${nested?'\n'+readableValue(item,indent+' '):String(item).replaceAll('\n','\n'+indent+' ')}`;
|
|
11
|
+
}).join('\n');
|
|
12
|
+
}
|
|
13
|
+
export function readableError(error,{details=false}={}){
|
|
14
|
+
if(!error)return '';
|
|
15
|
+
const chain=[];for(let current=error;current;current=current.cause)chain.push(current);
|
|
16
|
+
const program=chain.find(e=>e.details?.executable)?.details.executable;
|
|
17
|
+
const code=chain.find(e=>e.code==='ENOENT')?.code??error.code;
|
|
18
|
+
const summary=[`原因:${code==='ENOENT'&&program?'未找到所需程序':error.message??'未知错误'}`,error.plugin&&`插件:${error.plugin}`,code&&`错误码:${code}`,program&&`程序:${program}`,code==='ENOENT'&&'请检查程序是否安装、路径是否正确以及 PATH 配置。'].filter(Boolean).join('\n');
|
|
19
|
+
if(!details)return summary;
|
|
20
|
+
const diagnostic=chain.flatMap((item,index)=>[
|
|
21
|
+
`${index===0?'错误':'上游原因 '+index}:${item.message??'未知错误'}`,
|
|
22
|
+
item.code&&`错误码:${item.code}`,
|
|
23
|
+
item.details!==undefined&&`细节:\n${readableValue(item.details)}`,
|
|
24
|
+
item.stack&&`调用栈:\n${item.stack}`,
|
|
25
|
+
].filter(Boolean));
|
|
26
|
+
return `${summary}\n\n${diagnostic.join('\n')}`;
|
|
27
|
+
}
|
|
28
|
+
const eventNames={'step.started':'步骤开始','step.succeeded':'步骤成功','step.failed':'步骤失败','step.cancelled':'步骤取消','step.skipped':'步骤跳过','step.stopped':'步骤停止','run.started':'运行开始','run.succeeded':'运行成功','run.failed':'运行失败','run.cancelled':'运行取消','run.stopped':'提前结束'};
|
|
29
|
+
const time=value=>{const date=new Date(value);return Number.isFinite(+date)?date.toLocaleTimeString('zh-CN',{hour12:false}):'--:--:--';};
|
|
30
|
+
export function readableTimeline(events,{callPath,values=false}={}){
|
|
31
|
+
const rows=[];let pending;
|
|
32
|
+
const push=(event,message)=>{
|
|
33
|
+
const prefix=`${time(event.time)} [${event.callPath??'运行'}${event.stream?' · '+event.stream:''}]`;
|
|
34
|
+
const lines=String(message).replaceAll('\r\n','\n').replaceAll('\r','\n').split('\n');
|
|
35
|
+
if(lines.at(-1)==='')lines.pop();
|
|
36
|
+
for(const line of lines)rows.push(`${prefix} ${line}`);
|
|
37
|
+
};
|
|
38
|
+
const flush=()=>{if(pending){if(pending.text)push(pending.event,pending.text);pending=null;}};
|
|
39
|
+
for(const event of events??[]){
|
|
40
|
+
if(callPath&&event.callPath!==callPath)continue;
|
|
41
|
+
if(event.type==='log'&&event.stream){
|
|
42
|
+
const key=`${event.callPath}\0${event.stream}`;
|
|
43
|
+
if(pending?.key!==key){flush();pending={key,event,text:''};}
|
|
44
|
+
pending.text+=event.message??'';
|
|
45
|
+
const end=pending.text.lastIndexOf('\n');
|
|
46
|
+
if(end>=0){push(pending.event,pending.text.slice(0,end+1));pending.text=pending.text.slice(end+1);pending.event=event;}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if(event.type==='log'){flush();push(event,`${event.message??''}${event.data!==undefined?'\n'+readableValue(event.data):''}`);continue;}
|
|
50
|
+
if(eventNames[event.type]){
|
|
51
|
+
const detail=event.error?readableError(event.error,{details:values}):event.reason??event.message;
|
|
52
|
+
flush();push(event,`${eventNames[event.type]}${event.action?' · '+event.action:''}${event.durationMs!==undefined?` · ${event.durationMs} ms`:''}${detail?'\n'+detail:''}`);
|
|
53
|
+
if(values&&event.type.startsWith('step.')){
|
|
54
|
+
if(event.input!==undefined&&Object.keys(event.input??{}).length)push(event,'输入\n'+readableValue(event.input));
|
|
55
|
+
if(event.output!==undefined)push(event,'输出\n'+readableValue(event.output));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
flush();return rows.join('\n')||'等待执行记录。插件通过 log 或 runProcess 输出的内容会显示在这里。';
|
|
60
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
2
|
+
import { Text, matchesKey, truncateToWidth } from '@mariozechner/pi-tui';
|
|
3
|
+
import {palette} from './theme.js';
|
|
4
|
+
import {readableValue,readableError,readableTimeline} from './run-presentation.js';
|
|
5
|
+
|
|
6
|
+
export const runStatuses={pending:'待执行',running:'运行中',succeeded:'成功',failed:'失败',cancelled:'已取消',skipped:'已跳过',stopped:'已停止'};
|
|
7
|
+
const safe=value=>stripVTControlCharacters(String(value)).replace(/[\x00-\x08\x0b-\x1f\x7f]/g,'');
|
|
8
|
+
export const mouseWheel=data=>{const match=/^\x1b\[<(\d+);\d+;\d+[Mm]$/.exec(data);return match&&(Number(match[1])&64)?(Number(match[1])&1?1:-1):0;};
|
|
9
|
+
export const mouseOn='\x1b[?1000h\x1b[?1006h';
|
|
10
|
+
export const mouseOff='\x1b[?1006l\x1b[?1000l';
|
|
11
|
+
|
|
12
|
+
export class RunViewer{
|
|
13
|
+
constructor({record,flow,workflows=[],catalog=[],terminal,onClose,onCancel,requestRender}){
|
|
14
|
+
Object.assign(this,{terminal,onClose,onCancel,requestRender,catalog});this.view=0;this.selected=0;this.offset=0;this.follow=true;this.allLogs=true;this.raw=false;this.confirmCancel=false;
|
|
15
|
+
this.planned=[];
|
|
16
|
+
const walk=(steps,prefix,ancestors)=>{
|
|
17
|
+
for(const step of steps??[]){
|
|
18
|
+
const callPath=`${prefix}/${step.id}`;this.planned.push({callPath,stepId:step.id,action:step.action,title:step.title,status:'pending'});
|
|
19
|
+
if(step.branch){walk(step.branch.then,callPath,ancestors);walk(step.branch.else,callPath,ancestors);}
|
|
20
|
+
if(step.parallel)walk(step.parallel.steps,callPath,ancestors);
|
|
21
|
+
if(step.workflow){const child=workflows.find(w=>`${w.id}.yaml`===step.workflow.replace(/^\.\//,''));if(child&&!ancestors.has(child.id))walk(child.steps,callPath,new Set([...ancestors,child.id]));}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
if(flow)walk(flow.steps,flow.id,new Set([flow.id]));
|
|
25
|
+
this.update(record);
|
|
26
|
+
}
|
|
27
|
+
invalidate(){}
|
|
28
|
+
update(record){
|
|
29
|
+
if(!record)return;
|
|
30
|
+
this.record=record;const nodes=new Map(this.planned.map(node=>[node.callPath,{...node}]));
|
|
31
|
+
for(const event of record.events??[]){
|
|
32
|
+
if(!event.type.startsWith('step.')||!event.callPath)continue;
|
|
33
|
+
const node=nodes.get(event.callPath)??{callPath:event.callPath,stepId:event.stepId};
|
|
34
|
+
Object.assign(node,event,{status:event.type==='step.started'?'running':event.type.slice(5)});
|
|
35
|
+
node.title??=this.catalog.find(action=>action.id===node.action)?.title;nodes.set(event.callPath,node);
|
|
36
|
+
}
|
|
37
|
+
this.nodes=[...nodes.values()];this.selected=Math.max(0,Math.min(this.selected,this.nodes.length-1));
|
|
38
|
+
this.requestRender();
|
|
39
|
+
}
|
|
40
|
+
content(){
|
|
41
|
+
const node=this.nodes[this.selected],status=runStatuses[this.record.status]??this.record.status;
|
|
42
|
+
if(this.view===0)return [
|
|
43
|
+
`运行:${status}${this.record.reason?` · ${this.record.reason}`:''}`,
|
|
44
|
+
this.record.error?readableError(this.record.error):'',
|
|
45
|
+
this.record.output!==undefined?'\n最终输出\n'+readableValue(this.record.output):'',
|
|
46
|
+
'\n步骤 · Enter 查看详情',
|
|
47
|
+
...this.nodes.map((n,i)=>`${i===this.selected?'›':' '} ${runStatuses[n.status]??n.status} · ${n.title??n.callPath}${n.durationMs!==undefined?` · ${n.durationMs} ms`:''}${n.reason?' · '+n.reason:''}`),
|
|
48
|
+
!this.nodes.length?'此运行没有工作流步骤,可在日志页查看插件输出。':'',
|
|
49
|
+
].filter(Boolean).join('\n');
|
|
50
|
+
if(this.view===1){
|
|
51
|
+
const format=value=>this.raw?JSON.stringify(value??null,null,2):readableValue(value);
|
|
52
|
+
const error=node?node.error:this.record.error;
|
|
53
|
+
return [`步骤:${node?.callPath??'整个运行'}`,node?.action?'能力:'+node.action:'',error?'\n错误\n'+(this.raw?format(error):readableError(error)):'',
|
|
54
|
+
node?'\n输入\n'+format(node.input):'', '\n输出\n'+format(node?node.output:this.record.output),
|
|
55
|
+
!error&&(node?.reason||node?.message)?'\n说明:'+(node.reason??node.message):'',this.raw?'\n原始 JSON · 包含完整错误详情与调用栈':'\nj 查看原始 JSON 与完整诊断',
|
|
56
|
+
].filter(Boolean).join('\n');
|
|
57
|
+
}
|
|
58
|
+
return readableTimeline(this.record.events,{callPath:this.allLogs?undefined:node?.callPath});
|
|
59
|
+
}
|
|
60
|
+
layout(width){
|
|
61
|
+
const height=Math.max(1,this.terminal.rows),pageSize=Math.max(1,height-4);
|
|
62
|
+
const lines=new Text(safe(this.content()),0,0).render(Math.max(1,width));
|
|
63
|
+
const maxOffset=Math.max(0,lines.length-pageSize);
|
|
64
|
+
this.offset=this.follow&&this.view===2?maxOffset:Math.max(0,Math.min(this.offset,maxOffset));
|
|
65
|
+
return {height,pageSize,lines,maxOffset};
|
|
66
|
+
}
|
|
67
|
+
render(width){
|
|
68
|
+
const {height,pageSize,lines}=this.layout(width),node=this.nodes[this.selected];
|
|
69
|
+
const body=lines.slice(this.offset,this.offset+pageSize);while(body.length<pageSize)body.push('');
|
|
70
|
+
const tabs=`${['1 总览','2 输入输出','3 日志'].map((s,i)=>this.view===i?palette.brand(`[${s}]`):palette.muted(s)).join(' ')} ${palette.muted('·')} ${palette.text(safe(runStatuses[this.record.status]??this.record.status))} ${palette.muted('· '+safe(this.record.workflowId??'运行'))}`;
|
|
71
|
+
const selection=`[ / ] 切换步骤:${node?.callPath??'暂无步骤'}${this.view===2?` · ${this.allLogs?'全部日志':'本步骤日志'} · ${this.follow?'跟随尾部':'暂停跟随'}`:''}`;
|
|
72
|
+
const footer=this.confirmCancel?'取消整个运行?y 确认 · 任意其他键返回':'q/Esc 返回 · x 取消 · 1/2/3 切换';
|
|
73
|
+
const shortcuts=width<60?'↑↓/滚轮 · [ ] 步骤 · j 原始数据':`↑↓/滚轮 翻页 · [ ] 步骤 · j 原始数据 · a 全部/本步骤 · End 跟随 · ${this.offset+1}–${Math.min(this.offset+pageSize,lines.length)}/${lines.length}`;
|
|
74
|
+
const styledBody=body.map(line=>line.startsWith('›')?palette.brand(line):line);
|
|
75
|
+
const styledFooter=palette.muted(safe(footer));
|
|
76
|
+
const screen=height===1?styledBody:height===2?[styledBody[0],styledFooter]:height===3?[tabs,styledBody[0],styledFooter]:height===4?[tabs,styledBody[0],styledFooter,palette.muted(shortcuts)]:[tabs,palette.muted(safe(selection)),...styledBody,styledFooter,palette.muted(shortcuts)];
|
|
77
|
+
return screen.map(line=>truncateToWidth(line,width));
|
|
78
|
+
}
|
|
79
|
+
handleInput(data){
|
|
80
|
+
if(this.confirmCancel){this.confirmCancel=false;if(data==='y')this.onCancel();this.requestRender();return;}
|
|
81
|
+
if(['escape','q'].some(key=>matchesKey(data,key))){this.onClose();return;}
|
|
82
|
+
if(matchesKey(data,'enter')){if(this.view===0){this.view=1;this.offset=0;this.requestRender();}return;}
|
|
83
|
+
if(data==='j'){this.raw=!this.raw;this.view=1;this.offset=0;this.requestRender();return;}
|
|
84
|
+
if(data==='x'&&this.record.status==='running'){this.confirmCancel=true;this.requestRender();return;}
|
|
85
|
+
if(['1','2','3'].includes(data)){this.view=Number(data)-1;this.offset=0;this.follow=true;this.requestRender();return;}
|
|
86
|
+
if(data==='['||data===']'){
|
|
87
|
+
this.selected=Math.max(0,Math.min(this.nodes.length-1,this.selected+(data===']'?1:-1)));this.offset=0;
|
|
88
|
+
if(this.view===0){const {lines,pageSize,maxOffset}=this.layout(this.terminal.columns);const row=lines.findIndex(line=>line.startsWith('›'));this.offset=Math.min(maxOffset,Math.max(0,row-Math.floor(pageSize/2)));}
|
|
89
|
+
this.requestRender();return;
|
|
90
|
+
}
|
|
91
|
+
if(data==='a'){this.allLogs=!this.allLogs;this.offset=0;this.requestRender();return;}
|
|
92
|
+
if(data==='f'){this.follow=!this.follow;this.requestRender();return;}
|
|
93
|
+
const {pageSize,maxOffset}=this.layout(this.terminal.columns),wheel=mouseWheel(data);
|
|
94
|
+
if(wheel)this.offset+=wheel*3;
|
|
95
|
+
else if(matchesKey(data,'up'))this.offset--;
|
|
96
|
+
else if(matchesKey(data,'down'))this.offset++;
|
|
97
|
+
else if(matchesKey(data,'pageUp'))this.offset-=pageSize;
|
|
98
|
+
else if(matchesKey(data,'pageDown'))this.offset+=pageSize;
|
|
99
|
+
else if(matchesKey(data,'home'))this.offset=0;
|
|
100
|
+
else if(matchesKey(data,'end')){this.offset=maxOffset;this.follow=true;this.requestRender();return;}
|
|
101
|
+
else return;
|
|
102
|
+
this.follow=false;this.offset=Math.max(0,Math.min(maxOffset,this.offset));this.requestRender();
|
|
103
|
+
}
|
|
104
|
+
}
|