@yanglaofish/fishtools 0.2.0 → 0.4.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 +43 -17
- package/manual/README.md +43 -17
- 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/examples/script-plugin/LICENSE +21 -0
- package/manual/docs/examples/script-plugin/README.md +64 -0
- package/manual/docs/examples/script-plugin/index.js +39 -0
- package/manual/docs/examples/script-plugin/package.json +9 -0
- package/manual/docs/examples/script-plugin/scripts/benchmark.py +23 -0
- package/manual/docs/plugin-development.md +75 -14
- package/manual/docs/releases.md +30 -1
- package/manual/docs/research/2026-10-11-plugin-kernel-capabilities.md +167 -0
- package/manual/docs/roadmap.md +8 -1
- package/manual/docs/superpowers/plans/2026-10-11-control-observation.md +1 -1
- 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/plans/2026-10-12-plugin-developer-experience.md +50 -0
- package/manual/docs/superpowers/reviews/2026-10-12-plugin-release-review.md +20 -0
- package/manual/docs/superpowers/specs/2026-10-11-collapsed-plugin-output.md +23 -0
- package/manual/docs/superpowers/specs/2026-10-12-plugin-developer-experience.md +22 -0
- package/manual/docs/verification.md +100 -0
- package/manual/packages/core/src/application.js +235 -0
- package/manual/packages/core/src/engine.js +93 -0
- package/manual/packages/core/src/host.js +22 -5
- 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/tests/application.test.js +30 -0
- package/node_modules/@mytools/core/bin/fst.js +27 -5
- package/node_modules/@mytools/core/package.json +2 -0
- package/node_modules/@mytools/core/src/application.js +46 -21
- package/node_modules/@mytools/core/src/contracts.js +27 -9
- package/node_modules/@mytools/core/src/documentation.js +1 -1
- package/node_modules/@mytools/core/src/engine.js +1 -1
- package/node_modules/@mytools/core/src/environments.js +39 -1
- package/node_modules/@mytools/core/src/host.js +22 -5
- package/node_modules/@mytools/core/src/markdown-log.js +1 -1
- package/node_modules/@mytools/core/src/plugin-installations.js +84 -0
- package/node_modules/@mytools/core/src/plugin-tools.js +133 -0
- package/node_modules/@mytools/core/src/plugins.js +16 -6
- package/node_modules/@mytools/core/src/process.js +30 -8
- package/node_modules/@mytools/core/src/schema-path.js +31 -0
- package/node_modules/@mytools/core/src/terminal/model.js +1 -1
- package/node_modules/@mytools/core/src/terminal/navigation.js +17 -0
- package/node_modules/@mytools/core/src/terminal/presentation.js +3 -3
- 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 +30 -12
- package/node_modules/@mytools/core/src/terminal/session.js +118 -50
- package/node_modules/@mytools/core/src/terminal/theme.js +1 -0
- package/node_modules/@mytools/core/src/terminal/view.js +125 -28
- package/node_modules/@mytools/core/src/terminal/workflow-menu.js +50 -0
- package/node_modules/@mytools/core/src/validation.js +24 -9
- package/node_modules/@mytools/core/src/version.js +1 -1
- package/node_modules/@mytools/core/src/workflows.js +13 -15
- package/node_modules/@mytools/pack/dependencies.js +8 -1
- package/node_modules/@mytools/pack/index.js +4 -2
- package/node_modules/@mytools/web/public/THIRD_PARTY_NOTICES.txt +871 -870
- package/node_modules/{tar → minipass-flush/node_modules/minipass}/node_modules/yallist/README.md +7 -8
- package/node_modules/minipass-flush/node_modules/minipass/node_modules/yallist/package.json +29 -0
- package/node_modules/yallist/README.md +8 -7
- package/node_modules/yallist/package.json +51 -12
- package/package.json +15 -15
- package/node_modules/tar/node_modules/yallist/package.json +0 -68
- /package/node_modules/{yallist → minipass-flush/node_modules/minipass/node_modules/yallist}/LICENSE +0 -0
- /package/node_modules/{yallist → minipass-flush/node_modules/minipass/node_modules/yallist}/iterator.js +0 -0
- /package/node_modules/{yallist → minipass-flush/node_modules/minipass/node_modules/yallist}/yallist.js +0 -0
- /package/node_modules/{tar/node_modules/yallist → yallist}/LICENSE.md +0 -0
- /package/node_modules/{tar/node_modules/yallist → yallist}/dist/commonjs/index.d.ts +0 -0
- /package/node_modules/{tar/node_modules/yallist → yallist}/dist/commonjs/index.d.ts.map +0 -0
- /package/node_modules/{tar/node_modules/yallist → yallist}/dist/commonjs/index.js +0 -0
- /package/node_modules/{tar/node_modules/yallist → yallist}/dist/commonjs/index.js.map +0 -0
- /package/node_modules/{tar/node_modules/yallist → yallist}/dist/commonjs/package.json +0 -0
- /package/node_modules/{tar/node_modules/yallist → yallist}/dist/esm/index.d.ts +0 -0
- /package/node_modules/{tar/node_modules/yallist → yallist}/dist/esm/index.d.ts.map +0 -0
- /package/node_modules/{tar/node_modules/yallist → yallist}/dist/esm/index.js +0 -0
- /package/node_modules/{tar/node_modules/yallist → yallist}/dist/esm/index.js.map +0 -0
- /package/node_modules/{tar/node_modules/yallist → yallist}/dist/esm/package.json +0 -0
|
@@ -3,7 +3,10 @@ import semver from 'semver';
|
|
|
3
3
|
import { SDK_VERSION, FlowError } from '@mytools/sdk';
|
|
4
4
|
import { validate, emptyObject } from './validation.js';
|
|
5
5
|
import {validateDeclarations,validateCommands} from './contracts.js';
|
|
6
|
-
import {
|
|
6
|
+
import {prepareEnvironments,platformWarnings} from './environments.js';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import {mkdir} from 'node:fs/promises';
|
|
9
|
+
import {createHash} from 'node:crypto';
|
|
7
10
|
import {createRuntime} from './runtime.js';
|
|
8
11
|
import {pluginError} from './diagnostics.js';
|
|
9
12
|
|
|
@@ -16,6 +19,8 @@ export async function createHost({ plugins = [], config = {}, services = {}, env
|
|
|
16
19
|
const commandNames=new Set(),commands=new Map();
|
|
17
20
|
function bindContext(context){
|
|
18
21
|
const bound={...services,...context},execute=bound.runProcess;
|
|
22
|
+
// 面向用户的输出与诊断日志共用持久化通道,只区分默认可见性。
|
|
23
|
+
bound.print=(message,data)=>Promise.resolve(bound.log?.(message,data,{audience:'user'}));
|
|
19
24
|
if(typeof execute==='function')bound.runProcess=(file,args,options={})=>{
|
|
20
25
|
const signal=context.signal&&options.signal?AbortSignal.any([context.signal,options.signal]):context.signal??options.signal;
|
|
21
26
|
return execute(file,args,{...options,signal,onOutput:async({stream,message})=>{
|
|
@@ -25,6 +30,17 @@ export async function createHost({ plugins = [], config = {}, services = {}, env
|
|
|
25
30
|
};
|
|
26
31
|
return bound;
|
|
27
32
|
}
|
|
33
|
+
async function pluginContext(plugin,action,context){
|
|
34
|
+
const bound=bindContext(context),application=services.application;
|
|
35
|
+
const projectDir=application?.projectDir;
|
|
36
|
+
// 固定长度摘要避免 scoped 包名分隔符、大小写碰撞及合法长包名超出文件名上限。
|
|
37
|
+
const stateDir=projectDir&&!application.project.readOnly?path.join(projectDir,'.flow','plugin-state',createHash('sha256').update(plugin.id).digest('hex')):undefined;
|
|
38
|
+
if(stateDir)await mkdir(stateDir,{recursive:true});
|
|
39
|
+
for(const message of platformWarnings(plugin))await bound.log?.(message,{code:'PLATFORM_MISMATCH'},{audience:'user'});
|
|
40
|
+
const environments=await prepareEnvironments(plugin,action,environmentOverrides[plugin.id],{cwd:projectDir??process.cwd()});
|
|
41
|
+
context.signal?.throwIfAborted();
|
|
42
|
+
return {...bound,projectDir,stateDir,environments};
|
|
43
|
+
}
|
|
28
44
|
for (const entry of plugins) {
|
|
29
45
|
const plugin = entry.plugin ?? entry;
|
|
30
46
|
validateDeclarations(plugin);validateCommands(plugin,commandNames);
|
|
@@ -78,6 +94,7 @@ export async function createHost({ plugins = [], config = {}, services = {}, env
|
|
|
78
94
|
}
|
|
79
95
|
return {
|
|
80
96
|
definitions, actions,
|
|
97
|
+
warnings:[...definitions.values()].flatMap(plugin=>platformWarnings(plugin).map(message=>({plugin:plugin.id,message}))),
|
|
81
98
|
hasRuntime:runtime.hasRuntime,
|
|
82
99
|
runtimeStatus:()=>runtime.status(),
|
|
83
100
|
commands(){return [...commands.values()].map(({run,config,...metadata})=>structuredClone(metadata));},
|
|
@@ -90,9 +107,9 @@ export async function createHost({ plugins = [], config = {}, services = {}, env
|
|
|
90
107
|
try{
|
|
91
108
|
const getService=runtime.bindServices(plugin.id);
|
|
92
109
|
context.signal?.throwIfAborted();
|
|
93
|
-
const result=await command.run(validate(command.input??emptyObject,input,`/${name} 输入`),{...
|
|
110
|
+
const result=await command.run(validate(command.input??emptyObject,input,`/${name} 输入`),{...await pluginContext(plugin,undefined,context),getService,config:command.config,resourceDir:plugin.root});
|
|
94
111
|
context.signal?.throwIfAborted();
|
|
95
|
-
return validate(command.output,JSON.parse(JSON.stringify(result??{})),`/${name}
|
|
112
|
+
return validate(command.output,JSON.parse(JSON.stringify(result??{})),`/${name} 输出`,{useDefaults:false});
|
|
96
113
|
}catch(cause){throw pluginError(cause,{plugin:plugin.id,command:`/${name}`});}
|
|
97
114
|
},
|
|
98
115
|
catalog() { return [...actions.values()].map(({run,config,root,...metadata})=>structuredClone(metadata)); },
|
|
@@ -103,12 +120,12 @@ export async function createHost({ plugins = [], config = {}, services = {}, env
|
|
|
103
120
|
try{
|
|
104
121
|
const getService=runtime.bindServices(action.plugin);
|
|
105
122
|
context.signal?.throwIfAborted();
|
|
106
|
-
const result = await action.run(validate(action.input ?? emptyObject,input,`${id} 输入`), {...
|
|
123
|
+
const result = await action.run(validate(action.input ?? emptyObject,input,`${id} 输入`), {...await pluginContext(definitions.get(action.plugin),action,context),getService, config:action.config, resourceDir:action.root});
|
|
107
124
|
context.signal?.throwIfAborted();
|
|
108
125
|
// 通过 JSON 边界验证返回值,避免函数、循环引用等进入工作流状态。
|
|
109
126
|
const encoded = JSON.stringify(result);
|
|
110
127
|
if (encoded === undefined) throw new FlowError(`${id} 输出必须可 JSON 序列化`);
|
|
111
|
-
return validate(action.output, JSON.parse(encoded),`${id}
|
|
128
|
+
return validate(action.output, JSON.parse(encoded),`${id} 输出`,{useDefaults:false});
|
|
112
129
|
}catch(cause){throw pluginError(cause,{plugin:action.plugin,action:id});}
|
|
113
130
|
},
|
|
114
131
|
async service(name,options) {
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import {FlowError} from '@mytools/sdk';
|
|
2
|
+
|
|
3
|
+
// 静态声明用于诊断与打包,运行时激活仍由 Cordis inject 驱动。
|
|
4
|
+
export function validateRuntimes(definitions,ctx){
|
|
5
|
+
const providers=new Map(),ordered=[],visiting=new Set(),visited=new Set();
|
|
6
|
+
for(const p of definitions.values()){
|
|
7
|
+
const r=p.runtime;if(r===undefined)continue;
|
|
8
|
+
if(!r||typeof r.apply!=='function')throw new FlowError(`${p.id} runtime 必须提供 apply`);
|
|
9
|
+
for(const key of ['provides','inject']){
|
|
10
|
+
const names=r[key]??[];
|
|
11
|
+
if(!Array.isArray(names)||names.some(n=>typeof n!=='string'||!/^\w[\w.-]*$/.test(n)||n in ctx)||new Set(names).size!==names.length)throw new FlowError(`${p.id} runtime.${key} 服务名称无效或重复`);
|
|
12
|
+
}
|
|
13
|
+
for(const name of r.provides??[]){
|
|
14
|
+
if(providers.has(name))throw new FlowError(`服务重复:${name}(${providers.get(name)}、${p.id})`);
|
|
15
|
+
providers.set(name,p.id);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function visit(id){
|
|
19
|
+
if(visiting.has(id))throw new FlowError(`服务循环依赖:${[...visiting,id].join(' → ')}`);
|
|
20
|
+
if(visited.has(id))return;
|
|
21
|
+
visiting.add(id);const p=definitions.get(id);
|
|
22
|
+
for(const name of p.runtime?.inject??[]){
|
|
23
|
+
const owner=providers.get(name);
|
|
24
|
+
if(!owner)throw new FlowError(`${id} 缺少服务提供者:${name}`);
|
|
25
|
+
if(owner!==id&&!Object.hasOwn(p.dependencies??{},owner))throw new FlowError(`${id} 的 dependencies 必须声明服务 ${name} 的提供者 ${owner}`);
|
|
26
|
+
visit(owner);
|
|
27
|
+
}
|
|
28
|
+
visiting.delete(id);visited.add(id);ordered.push(id);
|
|
29
|
+
}
|
|
30
|
+
for(const id of definitions.keys())visit(id);
|
|
31
|
+
return {providers,ordered};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createRuntime(ctx,definitions,{enabled=true}={}){
|
|
35
|
+
const {providers,ordered}=validateRuntimes(definitions,ctx);
|
|
36
|
+
const forks=new Map(),scopes=new Map(),errors=[],pending=new Set();let closing;
|
|
37
|
+
const record=(id,error)=>errors.push({plugin:id,message:error?.message??String(error)});
|
|
38
|
+
const track=(id,callback)=>{
|
|
39
|
+
// 同步撤销注册,只有异步尾部进入等待集合,避免服务重激活时旧清理删掉新注册。
|
|
40
|
+
let result;try{result=callback();}catch(error){record(id,error);return;}
|
|
41
|
+
const task=Promise.resolve(result).catch(e=>record(id,e)).finally(()=>pending.delete(task));
|
|
42
|
+
pending.add(task);return task;
|
|
43
|
+
};
|
|
44
|
+
// 3.18.1 的 reset 会调用 dispose,但不等待 Promise;此适配只补齐等待和错误传播。
|
|
45
|
+
ctx.on('internal/listener',function(name,listener,options){
|
|
46
|
+
if(name!=='dispose')return;
|
|
47
|
+
const wrapped=()=>track(this.runtime.name??'host',listener),list=this.scope.disposables;
|
|
48
|
+
list[options.prepend?'unshift':'push'](wrapped);
|
|
49
|
+
return ()=>{const i=list.indexOf(wrapped);if(i>=0)list.splice(i,1);};
|
|
50
|
+
},{prepend:true,global:true});
|
|
51
|
+
ctx.on('internal/error',function(error){record(this?.runtime?.name??'host',error);},{global:true});
|
|
52
|
+
const drain=async()=>{while(pending.size)await Promise.all([...pending]);};
|
|
53
|
+
const failure=()=>new FlowError(`插件生命周期失败:${errors.map(e=>`${e.plugin}: ${e.message}`).join(';')}`,{code:'PLUGIN_RUNTIME',details:{errors:structuredClone(errors)}});
|
|
54
|
+
const runtime={
|
|
55
|
+
hasRuntime:[...definitions.values()].some(p=>p.runtime),
|
|
56
|
+
register(plugin,settings,register){
|
|
57
|
+
if(!enabled&&plugin.runtime)return;
|
|
58
|
+
const fork=ctx.plugin({name:plugin.id,inject:plugin.runtime?.inject??[],apply(scope){
|
|
59
|
+
const activation={scope,active:true,tasks:new Set(),disposers:[]};
|
|
60
|
+
scopes.set(plugin.id,activation);
|
|
61
|
+
const initialize=callback=>{
|
|
62
|
+
const task=Promise.resolve().then(callback);
|
|
63
|
+
activation.tasks.add(task);
|
|
64
|
+
return task.finally(()=>activation.tasks.delete(task));
|
|
65
|
+
};
|
|
66
|
+
scope.on('dispose',()=>{
|
|
67
|
+
activation.active=false;
|
|
68
|
+
if(scopes.get(plugin.id)===activation)scopes.delete(plugin.id);
|
|
69
|
+
return (async()=>{
|
|
70
|
+
// 初始化可能仍在取得资源;先等其结束,再执行本次激活登记的清理。
|
|
71
|
+
while(activation.tasks.size)await Promise.allSettled([...activation.tasks]);
|
|
72
|
+
for(const dispose of activation.disposers.splice(0)){
|
|
73
|
+
try{await dispose();}catch(error){record(plugin.id,error);}
|
|
74
|
+
}
|
|
75
|
+
})();
|
|
76
|
+
});
|
|
77
|
+
register(scope);
|
|
78
|
+
const managed=new Proxy(scope,{get(target,name){
|
|
79
|
+
if(name==='on')return(event,listener,options={})=>{
|
|
80
|
+
if(event==='dispose'){
|
|
81
|
+
activation.disposers[options.prepend?'unshift':'push'](listener);
|
|
82
|
+
return()=>{const i=activation.disposers.indexOf(listener);if(i>=0)activation.disposers.splice(i,1);};
|
|
83
|
+
}
|
|
84
|
+
if(event==='ready')return scope.on(event,()=>initialize(listener),options);
|
|
85
|
+
return scope.on(event,listener,options);
|
|
86
|
+
};
|
|
87
|
+
if(name==='set')return(...args)=>{
|
|
88
|
+
if(!activation.active||closing)throw new FlowError(`${plugin.id} 激活作用域已失效`);
|
|
89
|
+
return scope.set(...args);
|
|
90
|
+
};
|
|
91
|
+
return Reflect.get(target,name);
|
|
92
|
+
}});
|
|
93
|
+
if(plugin.runtime)scope.on('ready',()=>initialize(()=>plugin.runtime.apply(managed,settings)));
|
|
94
|
+
}});
|
|
95
|
+
forks.set(plugin.id,fork);
|
|
96
|
+
},
|
|
97
|
+
async start(){
|
|
98
|
+
await ctx.start();await drain();
|
|
99
|
+
if(errors.length)throw failure();
|
|
100
|
+
if(!enabled)return;
|
|
101
|
+
for(const [name,id] of providers)if(ctx.get(name)==null)throw new FlowError(`${id} 未提供声明的服务:${name}`,{code:'PLUGIN_RUNTIME'});
|
|
102
|
+
for(const p of runtime.status())if(p.status!=='ready')throw new FlowError(`${p.id} 服务尚未就绪:${p.missing.join(', ')}`,{code:'PLUGIN_RUNTIME'});
|
|
103
|
+
},
|
|
104
|
+
status(){return [...definitions.values()].map(p=>{
|
|
105
|
+
const state=forks.get(p.id)?.runtime.status,missing=(p.runtime?.inject??[]).filter(n=>ctx.get(n)==null);
|
|
106
|
+
const error=errors.findLast(e=>e.plugin===p.id);
|
|
107
|
+
return {id:p.id,status:closing||!enabled&&p.runtime?'stopped':state===3?'failed':state===2&&forks.get(p.id)?.runtime.isActive&&!missing.length?'ready':'pending',missing,...(error?{error:error.message}:{})};
|
|
108
|
+
});},
|
|
109
|
+
assertActive(id){
|
|
110
|
+
if(closing)throw new FlowError('插件宿主已停止');
|
|
111
|
+
if(runtime.status().find(p=>p.id===id)?.status!=='ready'||!scopes.has(id))throw new FlowError(`插件 ${id} 当前不可用`,{code:'PLUGIN_RUNTIME',details:runtime.status().find(p=>p.id===id)});
|
|
112
|
+
},
|
|
113
|
+
bindServices(id){
|
|
114
|
+
runtime.assertActive(id);const activation=scopes.get(id);
|
|
115
|
+
return name=>runtime.getService(id,name,activation);
|
|
116
|
+
},
|
|
117
|
+
getService(id,name,activation){
|
|
118
|
+
if(!activation?.active||scopes.get(id)!==activation)throw new FlowError(`${id} 激活作用域已失效`);
|
|
119
|
+
runtime.assertActive(id);
|
|
120
|
+
const p=definitions.get(id);
|
|
121
|
+
if(![...(p.runtime?.inject??[]),...(p.runtime?.provides??[])].includes(name))throw new FlowError(`${id} 未声明服务:${name}`);
|
|
122
|
+
const value=activation.scope.get(name);
|
|
123
|
+
if(value==null)throw new FlowError(`${id} 服务不可用:${name}`);
|
|
124
|
+
return value;
|
|
125
|
+
},
|
|
126
|
+
assertOpen(){if(closing)throw new FlowError('插件宿主已停止');},
|
|
127
|
+
close(){
|
|
128
|
+
closing??=Promise.resolve().then(async()=>{
|
|
129
|
+
// 消费者先关闭,等待其使用完服务;注册撤销和依赖停用由 Cordis 执行。
|
|
130
|
+
for(const id of [...ordered].reverse()){forks.get(id)?.dispose();await drain();}
|
|
131
|
+
await ctx.stop();await drain();if(errors.length)throw failure();
|
|
132
|
+
});return closing;
|
|
133
|
+
},
|
|
134
|
+
};return runtime;
|
|
135
|
+
}
|
|
@@ -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
|
+
}
|