@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.
Files changed (97) hide show
  1. package/README.md +79 -25
  2. package/manual/LICENSE +21 -0
  3. package/manual/README.md +199 -0
  4. package/manual/THIRD_PARTY_NOTICES.md +32 -0
  5. package/manual/docs/examples/action-plugin/LICENSE +21 -0
  6. package/manual/docs/examples/action-plugin/README.md +38 -0
  7. package/manual/docs/examples/action-plugin/index.js +18 -0
  8. package/manual/docs/examples/action-plugin/input.json +1 -0
  9. package/manual/docs/examples/action-plugin/package.json +9 -0
  10. package/manual/docs/examples/pipeline-reader/LICENSE +21 -0
  11. package/manual/docs/examples/pipeline-reader/index.js +14 -0
  12. package/manual/docs/examples/pipeline-reader/package.json +7 -0
  13. package/manual/docs/examples/pipeline-writer/LICENSE +21 -0
  14. package/manual/docs/examples/pipeline-writer/index.js +17 -0
  15. package/manual/docs/examples/pipeline-writer/package.json +7 -0
  16. package/manual/docs/plans/2026-10-10-workbench-ui.md +27 -0
  17. package/{docs → manual/docs}/plugin-development.md +172 -20
  18. package/{docs → manual/docs}/releases.md +32 -2
  19. package/manual/docs/research/2026-10-11-plugin-kernel-capabilities.md +167 -0
  20. package/manual/docs/roadmap.md +90 -0
  21. package/manual/docs/superpowers/plans/2026-10-09-workflow-mvp.md +115 -0
  22. package/manual/docs/superpowers/plans/2026-10-10-cordis-services.md +34 -0
  23. package/manual/docs/superpowers/plans/2026-10-10-fst-terminal.md +33 -0
  24. package/manual/docs/superpowers/plans/2026-10-10-portable-plugin-contract.md +35 -0
  25. package/manual/docs/superpowers/plans/2026-10-11-control-observation.md +77 -0
  26. package/manual/docs/superpowers/plans/2026-10-11-public-plugin-fixes.md +40 -0
  27. package/manual/docs/superpowers/plans/2026-10-11-terminal-hierarchy.md +40 -0
  28. package/manual/docs/superpowers/specs/2026-10-09-workflow-cli-design.md +377 -0
  29. package/manual/docs/superpowers/specs/2026-10-10-cordis-services-design.md +88 -0
  30. package/manual/docs/superpowers/specs/2026-10-10-portable-plugin-contract.md +31 -0
  31. package/manual/docs/superpowers/specs/2026-10-10-run-logs.md +28 -0
  32. package/manual/docs/superpowers/specs/2026-10-11-collapsed-plugin-output.md +23 -0
  33. package/manual/docs/superpowers/specs/2026-10-11-control-observation.md +37 -0
  34. package/{docs → manual/docs}/verification.md +97 -1
  35. package/manual/examples/service-plugins/README.md +20 -0
  36. package/manual/examples/service-plugins/consumer/LICENSE +21 -0
  37. package/manual/examples/service-plugins/consumer/index.js +14 -0
  38. package/manual/examples/service-plugins/consumer/package.json +1 -0
  39. package/manual/examples/service-plugins/provider/LICENSE +21 -0
  40. package/manual/examples/service-plugins/provider/index.js +19 -0
  41. package/manual/examples/service-plugins/provider/package.json +1 -0
  42. package/manual/packages/core/LICENSE +21 -0
  43. package/manual/packages/core/src/application.js +211 -0
  44. package/manual/packages/core/src/engine.js +93 -0
  45. package/manual/packages/core/src/host.js +124 -0
  46. package/manual/packages/core/src/runtime.js +135 -0
  47. package/manual/packages/core/src/terminal/run-feed.js +78 -0
  48. package/manual/packages/core/src/terminal/session.js +438 -0
  49. package/manual/packages/core/src/terminal/view.js +283 -0
  50. package/manual/packages/sdk/LICENSE +21 -0
  51. package/manual/packages/sdk/index.js +14 -0
  52. package/manual/packages/sdk/package.json +1 -0
  53. package/manual/plugins/environment/LICENSE +21 -0
  54. package/manual/plugins/environment/index.js +92 -0
  55. package/manual/plugins/environment/package.json +1 -0
  56. package/manual/plugins/examples/LICENSE +21 -0
  57. package/manual/plugins/examples/index.js +23 -0
  58. package/manual/plugins/examples/package.json +1 -0
  59. package/manual/plugins/examples/scripts/echo.py +5 -0
  60. package/manual/tests/application.test.js +75 -0
  61. package/node_modules/@mytools/core/bin/fst.js +12 -4
  62. package/node_modules/@mytools/core/src/application.js +11 -5
  63. package/node_modules/@mytools/core/src/cli-run.js +6 -0
  64. package/node_modules/@mytools/core/src/contracts.js +15 -2
  65. package/node_modules/@mytools/core/src/control.js +63 -0
  66. package/node_modules/@mytools/core/src/documentation.js +65 -0
  67. package/node_modules/@mytools/core/src/engine.js +80 -26
  68. package/node_modules/@mytools/core/src/host.js +15 -2
  69. package/node_modules/@mytools/core/src/markdown-log.js +9 -6
  70. package/node_modules/@mytools/core/src/process.js +49 -15
  71. package/node_modules/@mytools/core/src/project.js +1 -1
  72. package/node_modules/@mytools/core/src/run-logs.js +1 -1
  73. package/node_modules/@mytools/core/src/terminal/conditions.js +32 -0
  74. package/node_modules/@mytools/core/src/terminal/log-viewer.js +7 -8
  75. package/node_modules/@mytools/core/src/terminal/model.js +33 -4
  76. package/node_modules/@mytools/core/src/terminal/navigation.js +17 -0
  77. package/node_modules/@mytools/core/src/terminal/presentation.js +8 -10
  78. package/node_modules/@mytools/core/src/terminal/run-feed.js +78 -0
  79. package/node_modules/@mytools/core/src/terminal/run-presentation.js +60 -0
  80. package/node_modules/@mytools/core/src/terminal/run-viewer.js +104 -0
  81. package/node_modules/@mytools/core/src/terminal/session.js +212 -57
  82. package/node_modules/@mytools/core/src/terminal/theme.js +29 -0
  83. package/node_modules/@mytools/core/src/terminal/view.js +185 -33
  84. package/node_modules/@mytools/core/src/terminal/workflow-menu.js +50 -0
  85. package/node_modules/@mytools/core/src/validation.js +17 -4
  86. package/node_modules/@mytools/core/src/version.js +1 -1
  87. package/node_modules/@mytools/core/src/workflows.js +41 -19
  88. package/node_modules/@mytools/core/templates/control-demo.yaml +47 -0
  89. package/node_modules/@mytools/environment/index.js +14 -2
  90. package/node_modules/@mytools/pack/index.js +6 -2
  91. package/node_modules/@mytools/web/public/THIRD_PARTY_NOTICES.txt +780 -780
  92. package/node_modules/@mytools/web/public/assets/index-DT2B4tMV.js +70 -0
  93. package/node_modules/@mytools/web/public/assets/{index-BeV6IqTW.css → index-vuZvrUTT.css} +1 -1
  94. package/node_modules/@mytools/web/public/index.html +2 -2
  95. package/package.json +2 -2
  96. package/node_modules/@mytools/web/public/assets/index-TgsBnZgG.js +0 -70
  97. /package/{docs → manual/docs}/deployment.md +0 -0
@@ -0,0 +1,124 @@
1
+ import { Context } from 'cordis';
2
+ import semver from 'semver';
3
+ import { SDK_VERSION, FlowError } from '@mytools/sdk';
4
+ import { validate, emptyObject } from './validation.js';
5
+ import {validateDeclarations,validateCommands} from './contracts.js';
6
+ import {resolveEnvironments} from './environments.js';
7
+ import {createRuntime} from './runtime.js';
8
+ import {pluginError} from './diagnostics.js';
9
+
10
+ export async function createHost({ plugins = [], config = {}, services = {}, environmentOverrides={},runtimeEnabled=true } = {}) {
11
+ const ctx = new Context();
12
+ const actions = new Map();
13
+ const definitions = new Map();
14
+ const extensions = new Map();
15
+ const serviceNames = new Set();
16
+ const commandNames=new Set(),commands=new Map();
17
+ function bindContext(context){
18
+ const bound={...services,...context},execute=bound.runProcess;
19
+ // 面向用户的输出与诊断日志共用持久化通道,只区分默认可见性。
20
+ bound.print=(message,data)=>Promise.resolve(bound.log?.(message,data,{audience:'user'}));
21
+ if(typeof execute==='function')bound.runProcess=(file,args,options={})=>{
22
+ const signal=context.signal&&options.signal?AbortSignal.any([context.signal,options.signal]):context.signal??options.signal;
23
+ return execute(file,args,{...options,signal,onOutput:async({stream,message})=>{
24
+ await bound.log?.(message,undefined,{stream});
25
+ await options.onOutput?.({stream,message});
26
+ }});
27
+ };
28
+ return bound;
29
+ }
30
+ for (const entry of plugins) {
31
+ const plugin = entry.plugin ?? entry;
32
+ validateDeclarations(plugin);validateCommands(plugin,commandNames);
33
+ if (!plugin.id || definitions.has(plugin.id)) throw new FlowError(`插件标识缺失或重复:${plugin.id}`, {code:'VALIDATION'});
34
+ if (!semver.satisfies(SDK_VERSION, plugin.sdk ?? '^0.1.0')) throw new FlowError(`插件 ${plugin.id} 与 SDK ${SDK_VERSION} 不兼容`, {code:'VALIDATION'});
35
+ for(const [name,action] of Object.entries(plugin.actions??{})){
36
+ if(typeof action?.run!=='function')throw new FlowError(`动作 ${plugin.id}:${name} 缺少执行函数`);
37
+ }
38
+ for(const [name,extension] of Object.entries(plugin.services??{})){
39
+ if(serviceNames.has(name))throw new FlowError(`服务重复:${name}`);
40
+ if(typeof extension!=='function')throw new FlowError(`服务 ${name} 缺少执行函数`);
41
+ serviceNames.add(name);
42
+ }
43
+ definitions.set(plugin.id, {...plugin, root:entry.root});
44
+ }
45
+ const settingsById=new Map();
46
+ for (const plugin of definitions.values()) {
47
+ for (const [id, range] of Object.entries(plugin.dependencies ?? {})) {
48
+ if (!definitions.has(id) || !semver.satisfies(definitions.get(id).version,range)) throw new FlowError(`插件 ${plugin.id} 缺少兼容依赖 ${id}@${range}`,{code:'VALIDATION'});
49
+ }
50
+ const settings = validate(plugin.config, config[plugin.id] ?? {}, `${plugin.id} 配置`);
51
+ settingsById.set(plugin.id,settings);
52
+ }
53
+ const runtime=createRuntime(ctx,definitions,{enabled:runtimeEnabled});
54
+ try{
55
+ for (const plugin of definitions.values()) {
56
+ const settings=settingsById.get(plugin.id);
57
+ runtime.register(plugin,settings,scope=>{
58
+ for(const [name,command] of Object.entries(plugin.commands??{})){
59
+ const action=command.action?plugin.actions[command.action]:command;
60
+ commands.set(name,{...command,name,plugin:plugin.id,input:action.input,output:action.output,description:command.description??action.description??action.title??name,config:settings});
61
+ scope.on('dispose',()=>commands.delete(name));
62
+ }
63
+ for (const [name, action] of Object.entries(plugin.actions ?? {})) {
64
+ const id = `${plugin.id}:${name}`;
65
+ if (typeof action.run !== 'function') throw new FlowError(`动作 ${id} 缺少执行函数`);
66
+ actions.set(id, {...action, id, plugin:plugin.id, config:settings, root:plugin.root});
67
+ scope.on('dispose', () => actions.delete(id));
68
+ }
69
+ for (const [name, extension] of Object.entries(plugin.services ?? {})) {
70
+ if (extensions.has(name)) throw new FlowError(`服务重复:${name}`);
71
+ extensions.set(name, {run:extension,plugin:plugin.id});
72
+ scope.on('dispose',()=>extensions.delete(name));
73
+ }
74
+ });
75
+ }
76
+ await runtime.start();
77
+ }catch(error){
78
+ try{await runtime.close();}catch(cleanup){if(cleanup.message!==error.message)throw new AggregateError([error,cleanup],`${error.message};${cleanup.message}`);}
79
+ throw error;
80
+ }
81
+ return {
82
+ definitions, actions,
83
+ hasRuntime:runtime.hasRuntime,
84
+ runtimeStatus:()=>runtime.status(),
85
+ commands(){return [...commands.values()].map(({run,config,...metadata})=>structuredClone(metadata));},
86
+ async invokeCommand(name,input,context={}){
87
+ runtime.assertOpen();
88
+ const command=commands.get(name);if(!command)throw new FlowError(`命令不存在:/${name}`);
89
+ if(command.action)return this.invoke(`${command.plugin}:${command.action}`,input,context);
90
+ if(!context.ui)throw new FlowError(`交互命令 /${name} 需要在 TUI 中调用`);
91
+ const plugin=definitions.get(command.plugin);
92
+ try{
93
+ const getService=runtime.bindServices(plugin.id);
94
+ context.signal?.throwIfAborted();
95
+ const result=await command.run(validate(command.input??emptyObject,input,`/${name} 输入`),{...bindContext(context),getService,config:command.config,resourceDir:plugin.root,environments:resolveEnvironments(plugin,undefined,environmentOverrides[plugin.id])});
96
+ context.signal?.throwIfAborted();
97
+ return validate(command.output,JSON.parse(JSON.stringify(result??{})),`/${name} 输出`);
98
+ }catch(cause){throw pluginError(cause,{plugin:plugin.id,command:`/${name}`});}
99
+ },
100
+ catalog() { return [...actions.values()].map(({run,config,root,...metadata})=>structuredClone(metadata)); },
101
+ async invoke(id, input, context = {}) {
102
+ runtime.assertOpen();
103
+ const action = actions.get(id);
104
+ if (!action) throw new FlowError(`动作不存在:${id}`, {code:'VALIDATION'});
105
+ try{
106
+ const getService=runtime.bindServices(action.plugin);
107
+ context.signal?.throwIfAborted();
108
+ const result = await action.run(validate(action.input ?? emptyObject,input,`${id} 输入`), {...bindContext(context),getService, config:action.config, resourceDir:action.root,environments:resolveEnvironments(definitions.get(action.plugin),action,environmentOverrides[action.plugin])});
109
+ context.signal?.throwIfAborted();
110
+ // 通过 JSON 边界验证返回值,避免函数、循环引用等进入工作流状态。
111
+ const encoded = JSON.stringify(result);
112
+ if (encoded === undefined) throw new FlowError(`${id} 输出必须可 JSON 序列化`);
113
+ return validate(action.output, JSON.parse(encoded),`${id} 输出`);
114
+ }catch(cause){throw pluginError(cause,{plugin:action.plugin,action:id});}
115
+ },
116
+ async service(name,options) {
117
+ runtime.assertOpen();
118
+ if (!extensions.has(name)) throw new FlowError(`未安装提供 ${name} 的插件`);
119
+ const extension=extensions.get(name);runtime.assertActive(extension.plugin);
120
+ return extension.run(services,options);
121
+ },
122
+ close:()=>runtime.close(),
123
+ };
124
+ }
@@ -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
+ }