@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
|
@@ -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
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { stripVTControlCharacters } from 'node:util';
|
|
2
2
|
import { Text, matchesKey, truncateToWidth } from '@mariozechner/pi-tui';
|
|
3
3
|
import {palette} from './theme.js';
|
|
4
|
+
import {readableValue,readableError,readableTimeline} from './run-presentation.js';
|
|
4
5
|
|
|
5
6
|
export const runStatuses={pending:'待执行',running:'运行中',succeeded:'成功',failed:'失败',cancelled:'已取消',skipped:'已跳过',stopped:'已停止'};
|
|
6
7
|
const safe=value=>stripVTControlCharacters(String(value)).replace(/[\x00-\x08\x0b-\x1f\x7f]/g,'');
|
|
@@ -9,8 +10,8 @@ export const mouseOn='\x1b[?1000h\x1b[?1006h';
|
|
|
9
10
|
export const mouseOff='\x1b[?1006l\x1b[?1000l';
|
|
10
11
|
|
|
11
12
|
export class RunViewer{
|
|
12
|
-
constructor({record,flow,workflows=[],terminal,onClose,onCancel,requestRender}){
|
|
13
|
-
Object.assign(this,{terminal,onClose,onCancel,requestRender});this.view=0;this.selected=0;this.offset=0;this.follow=true;this.allLogs=false;this.confirmCancel=false;
|
|
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;
|
|
14
15
|
this.planned=[];
|
|
15
16
|
const walk=(steps,prefix,ancestors)=>{
|
|
16
17
|
for(const step of steps??[]){
|
|
@@ -30,20 +31,34 @@ export class RunViewer{
|
|
|
30
31
|
for(const event of record.events??[]){
|
|
31
32
|
if(!event.type.startsWith('step.')||!event.callPath)continue;
|
|
32
33
|
const node=nodes.get(event.callPath)??{callPath:event.callPath,stepId:event.stepId};
|
|
33
|
-
Object.assign(node,event,{status:event.type==='step.started'?'running':event.type.slice(5)});
|
|
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);
|
|
34
36
|
}
|
|
35
37
|
this.nodes=[...nodes.values()];this.selected=Math.max(0,Math.min(this.selected,this.nodes.length-1));
|
|
36
38
|
this.requestRender();
|
|
37
39
|
}
|
|
38
40
|
content(){
|
|
39
41
|
const node=this.nodes[this.selected],status=runStatuses[this.record.status]??this.record.status;
|
|
40
|
-
if(this.view===0)return [
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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});
|
|
44
59
|
}
|
|
45
60
|
layout(width){
|
|
46
|
-
const height=Math.max(1,this.terminal.rows),pageSize=Math.max(1,height-
|
|
61
|
+
const height=Math.max(1,this.terminal.rows),pageSize=Math.max(1,height-4);
|
|
47
62
|
const lines=new Text(safe(this.content()),0,0).render(Math.max(1,width));
|
|
48
63
|
const maxOffset=Math.max(0,lines.length-pageSize);
|
|
49
64
|
this.offset=this.follow&&this.view===2?maxOffset:Math.max(0,Math.min(this.offset,maxOffset));
|
|
@@ -54,20 +69,23 @@ export class RunViewer{
|
|
|
54
69
|
const body=lines.slice(this.offset,this.offset+pageSize);while(body.length<pageSize)body.push('');
|
|
55
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??'运行'))}`;
|
|
56
71
|
const selection=`[ / ] 切换步骤:${node?.callPath??'暂无步骤'}${this.view===2?` · ${this.allLogs?'全部日志':'本步骤日志'} · ${this.follow?'跟随尾部':'暂停跟随'}`:''}`;
|
|
57
|
-
const footer=this.confirmCancel?'取消整个运行?y 确认 · 任意其他键返回'
|
|
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}`;
|
|
58
74
|
const styledBody=body.map(line=>line.startsWith('›')?palette.brand(line):line);
|
|
59
75
|
const styledFooter=palette.muted(safe(footer));
|
|
60
|
-
const screen=height===1?styledBody:height===2?[styledBody[0],styledFooter]:height===3?[tabs,styledBody[0],styledFooter]:[tabs,palette.muted(safe(selection)),...styledBody,styledFooter];
|
|
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)];
|
|
61
77
|
return screen.map(line=>truncateToWidth(line,width));
|
|
62
78
|
}
|
|
63
79
|
handleInput(data){
|
|
64
80
|
if(this.confirmCancel){this.confirmCancel=false;if(data==='y')this.onCancel();this.requestRender();return;}
|
|
65
|
-
if(['escape','q'
|
|
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;}
|
|
66
84
|
if(data==='x'&&this.record.status==='running'){this.confirmCancel=true;this.requestRender();return;}
|
|
67
85
|
if(['1','2','3'].includes(data)){this.view=Number(data)-1;this.offset=0;this.follow=true;this.requestRender();return;}
|
|
68
86
|
if(data==='['||data===']'){
|
|
69
87
|
this.selected=Math.max(0,Math.min(this.nodes.length-1,this.selected+(data===']'?1:-1)));this.offset=0;
|
|
70
|
-
if(this.view===0)this.offset=Math.max(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)));}
|
|
71
89
|
this.requestRender();return;
|
|
72
90
|
}
|
|
73
91
|
if(data==='a'){this.allLogs=!this.allLogs;this.offset=0;this.requestRender();return;}
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
2
3
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
+
import { parseCommand, fileReference, readRunInput, newWorkflow, setBinding, stepMetadata, outputSources, stepAt, sequenceAt, pruneRuntimeInputs } from './model.js';
|
|
4
5
|
import { editCondition } from './conditions.js';
|
|
5
6
|
import { runStatuses } from './run-viewer.js';
|
|
6
7
|
import { editValue, needsJsonEditor } from './schema.js';
|
|
7
8
|
import {gettingStarted,pluginStatus,pluginDetails} from './presentation.js';
|
|
9
|
+
import {readableValue,readableError} from './run-presentation.js';
|
|
10
|
+
import {commandMenu} from './navigation.js';
|
|
11
|
+
import {workflowMenu} from './workflow-menu.js';
|
|
8
12
|
|
|
9
13
|
export class TerminalSession{
|
|
10
|
-
constructor(app,ui,{baseDir=process.cwd(),webOptions={}}={}){this.app=app;this.ui=ui;this.baseDir=baseDir;this.webOptions=webOptions;this.flow=null;this.dirty=false;this.exiting=false;this.web=null;}
|
|
14
|
+
constructor(app,ui,{baseDir=process.cwd(),webOptions={}}={}){this.app=app;this.ui=ui;this.baseDir=baseDir;this.webOptions=webOptions;this.flow=null;this.dirty=false;this.exiting=false;this.web=null;ui.activeRuns=()=>app.listRuns().filter(run=>run.status==='running').length;}
|
|
11
15
|
writable(){if(this.app.project.readOnly)throw new Error('这是只读运行包,请在完整工作区中编排、安装或打包');}
|
|
12
|
-
current(){if(!this.flow)throw new Error('先用 /workflows
|
|
13
|
-
commands(){return [
|
|
16
|
+
current(){if(!this.flow)throw new Error('先用 /workflows 打开或新建工作流');return this.flow;}
|
|
17
|
+
commands(){return commandMenu(this.app.listPlugins?.()??[],this.app.listCommands());}
|
|
14
18
|
async discard(){return !this.dirty||await this.ui.confirm('当前修改未保存,是否放弃修改?');}
|
|
15
19
|
async mayExit(){
|
|
16
20
|
if(!await this.discard())return false;
|
|
@@ -27,15 +31,25 @@ export class TerminalSession{
|
|
|
27
31
|
while(!this.exiting){
|
|
28
32
|
this.ui.summary(`欢迎使用 Fishtools · 插件编排工作台\n当前工作区:${this.app.projectDir}\n${this.app.listPlugins().length} 个插件 · ${(await this.app.listWorkflows()).length} 个工作流${this.flow?` · ${this.flow.title??this.flow.id} · ${this.dirty?'未保存':'已保存'}`:''}`);
|
|
29
33
|
let text;
|
|
30
|
-
try{text=await this.ui.input('fst > 输入 / 选择操作,@ 引用文件',{commands:this.commands()});}
|
|
34
|
+
try{text=await this.ui.input('fst > 输入 / 选择操作,@ 引用文件',{commands:this.commands(),workflows:await this.app.listWorkflows()});}
|
|
31
35
|
catch(e){if(e.code!=='PROMPT_CANCELLED')throw e;try{if(await this.mayExit())break;}catch{}continue;}
|
|
32
36
|
if(!text.trim())continue;
|
|
33
|
-
try{await this.execute(text);}catch(e){this.ui.show(e.code==='PROMPT_CANCELLED'?'
|
|
37
|
+
try{await this.execute(text);}catch(e){if(e.code!=='PROMPT_CANCELLED'||!this.ui.endCommand)this.ui.show(e.code==='PROMPT_CANCELLED'?'已返回主面板。':`${e.code==='COMMAND_INPUT'?'输入提示':'操作失败'}:${e.message}`);}
|
|
34
38
|
}
|
|
35
39
|
}finally{if(this.web)await this.web.close();}
|
|
36
40
|
}
|
|
37
41
|
async execute(text){
|
|
42
|
+
// 只记录用户提交的入口;菜单内部调用 /save 等动作不制造伪命令。
|
|
43
|
+
const outer=!this.commandDepth;
|
|
44
|
+
if(outer)this.ui.beginCommand?.(text);
|
|
45
|
+
this.commandDepth=(this.commandDepth??0)+1;
|
|
46
|
+
try{return await this.executeCommand(text);}
|
|
47
|
+
finally{this.commandDepth--;if(outer&&!this.exiting)this.ui.endCommand?.();}
|
|
48
|
+
}
|
|
49
|
+
async executeCommand(text){
|
|
38
50
|
const [command,...args]=parseCommand(text);
|
|
51
|
+
const entry=this.commands().find(c=>c.name===command&&c.plugin);
|
|
52
|
+
if(entry){if(args.length)throw new Error(`/${command} 请从二级菜单选择能力`);return this.pluginSkills(entry.plugin);}
|
|
39
53
|
const pluginCommand=this.app.listCommands().find(c=>c.name===command);
|
|
40
54
|
if(pluginCommand)return this.pluginCommand(pluginCommand,args);
|
|
41
55
|
if(command!=='run'&&args.length)throw new Error(`/${command} 使用交互选择,请不要附加参数`);
|
|
@@ -46,8 +60,11 @@ export class TerminalSession{
|
|
|
46
60
|
`当前工作区:${this.app.projectDir}`,
|
|
47
61
|
`文件路径起点:${this.baseDir}`,
|
|
48
62
|
'## 常用操作',gettingStarted(await this.app.listWorkflows(),this.app.project),
|
|
49
|
-
'## 编排工作流','/
|
|
50
|
-
'##
|
|
63
|
+
'## 编排工作流','/workflows 浏览、新建、打开工作流,进入后添加步骤、编辑参数、设置输出、保存或删除。删除会保留备份,仍被其他流程引用时不能删除。/run 选择已保存的工作流,或补全工作流 ID/中文标题后执行已保存版本,不会要求保存编辑草稿。要执行未保存修改,在工作流编辑菜单选择运行,再选择保存并运行;返回编辑会保留草稿。',
|
|
64
|
+
'## 主面板','运行期间可以继续输入命令,打开配置、管理插件或执行其他工作流。↑/↓ 回看本次启动已提交的命令,回到底部恢复草稿;补全菜单打开时仍用于选择候选。Ctrl+O 展开或收起执行详情,PgUp/PgDn 或滚轮浏览执行过程;/logs 回看各次执行的步骤、输入输出和日志。关闭菜单不会清空运行内容。',
|
|
65
|
+
'## 命令分层','/config 配置 Fishtools,/plugin 管理插件及各插件配置。输入 /插件入口名 后进入该插件的能力菜单;补全列表标明 [内置] 或 [插件]。/tools 包含 Web 与离线文档。旧 /new、/add、/steps、/outputs、/save、/plugins 以及插件的原始命令仍可直接输入。',
|
|
66
|
+
'## 观察运行','输入 /logs 打开执行记录。列表包含多个工作流的每次执行,运行中任务排在前面;选择后查看进度、结果和日志。从详情返回后可选择另一条记录,本次启动期间每条记录分别保留阅读位置。重启后仍可查看保留下来的执行记录。',
|
|
67
|
+
'1 / 2 / 3 切换总览、输入输出和日志,Enter 查看步骤详情,[ / ] 切换步骤,j 查看原始诊断数据。↑↓、PgUp/PgDn 或滚轮浏览;End 恢复尾部跟随。q / Esc 返回,正在执行的任务在后台继续,x 后 y 明确取消。',
|
|
51
68
|
'## 文件与运行参数','输入 @ 后按 Tab 选择文件;@@ 表示字面量 @。运行命令的 @参数.json 会读取 JSON 对象作为输入。',
|
|
52
69
|
'/run 工作流ID @"参数 文件.json"',
|
|
53
70
|
'## 离线文档','输入 /docs 阅读插件开发、部署、控制流与服务文档。独立命令 fst docs --path 查看资料位置,fst docs --export 目标目录 导出资料。',
|
|
@@ -61,28 +78,26 @@ export class TerminalSession{
|
|
|
61
78
|
return;
|
|
62
79
|
}
|
|
63
80
|
case 'exit':if(await this.mayExit())this.exiting=true;return;
|
|
64
|
-
case 'workflows':
|
|
65
|
-
const flows=await this.app.listWorkflows();
|
|
66
|
-
if(!flows.length){this.ui.show('没有工作流,输入 /new 新建。');return;}
|
|
67
|
-
if(!await this.discard())return;
|
|
68
|
-
const id=await this.ui.select('打开工作流',flows.map(w=>({value:w.id,label:w.title??w.id,description:`${w.steps.length} 步 · ${w.description??''}`})));
|
|
69
|
-
this.flow=structuredClone(flows.find(w=>w.id===id));this.dirty=false;this.ui.show(`已打开:${this.flow.title}`);return;
|
|
70
|
-
}
|
|
81
|
+
case 'workflows':return workflowMenu(this);
|
|
71
82
|
case 'new':{
|
|
72
|
-
this.writable();if(!await this.discard())return;
|
|
83
|
+
this.writable();if(!await this.discard())return false;
|
|
73
84
|
const title=(await this.ui.input('工作流名称',{files:false})).trim();if(!title)throw new Error('名称不能为空');
|
|
74
85
|
const description=await this.ui.input('用途说明(可留空)',{files:false});
|
|
75
|
-
this.flow=newWorkflow(title,description);this.dirty=true;this.ui.show('
|
|
86
|
+
this.flow=newWorkflow(title,description);this.dirty=true;this.ui.show('工作流草稿已创建,可以在编辑菜单添加步骤。');return true;
|
|
76
87
|
}
|
|
77
88
|
case 'add':return this.add();
|
|
78
89
|
case 'steps':return this.steps();
|
|
79
90
|
case 'outputs':return this.outputs();
|
|
80
91
|
case 'save':this.writable();await this.app.saveWorkflow(this.current());this.dirty=false;this.ui.show(`已保存:${this.flow.title}`);return;
|
|
81
92
|
case 'run':return this.startRun(args);
|
|
82
|
-
case 'runs':return this.
|
|
93
|
+
case 'runs':return this.logs();
|
|
83
94
|
case 'logs':return this.logs();
|
|
84
|
-
case 'plugins':return this.plugins();
|
|
85
|
-
case 'config':return this.
|
|
95
|
+
case 'plugin':case 'plugins':return this.plugins();
|
|
96
|
+
case 'config':return this.toolConfig();
|
|
97
|
+
case 'tools':{
|
|
98
|
+
const choice=await this.ui.select('工具',[{value:'web',label:'启动 Web 工作台'},{value:'docs',label:'阅读离线文档'},{value:'back',label:'返回主面板'}]);
|
|
99
|
+
if(choice!=='back')await this.execute('/'+choice);return;
|
|
100
|
+
}
|
|
86
101
|
case 'pack':return this.pack();
|
|
87
102
|
case 'web':{
|
|
88
103
|
if(!this.web)this.web=await this.app.startWeb(this.webOptions);
|
|
@@ -96,9 +111,13 @@ export class TerminalSession{
|
|
|
96
111
|
const input=args.length?await readRunInput(args[0],this.baseDir):command.input?await editValue(this.ui,command.input,{}, {baseDir:this.baseDir,required:true,label:command.description}):{};
|
|
97
112
|
const ui=Object.fromEntries(['input','select','show','confirm'].map(name=>[name,this.ui[name].bind(this.ui)]));
|
|
98
113
|
const run=await this.app.startCommand(command.name,input,{ui});
|
|
99
|
-
|
|
114
|
+
if(this.ui.trackRun){
|
|
115
|
+
this.ui.clearMessages?.();this.ui.trackRun({app:this.app,runId:run.id,title:command.description??command.name,input});
|
|
116
|
+
if(command.action){void run.done.catch(error=>{if(!this.ui.closed)this.ui.show(`运行记录保存失败:${error.message}`);});return;}
|
|
117
|
+
}
|
|
118
|
+
const unsubscribe=this.ui.trackRun?()=>{}:this.app.subscribe(e=>{if(e.runId===run.id&&e.type==='log')this.ui.show(e.message);});
|
|
100
119
|
const done=this.ui.busy(`正在执行 /${command.name} · Ctrl+C 取消`,()=>this.app.cancelRun(run.id));
|
|
101
|
-
try{const result=await run.done;this.ui.show(
|
|
120
|
+
try{const result=await run.done;if(!this.ui.trackRun)this.ui.show(`插件执行${runStatuses[result.status]??result.status}\n${result.error?readableError(result.error):readableValue(result.output)}\n/logs 查看执行详情与历史日志`);}
|
|
102
121
|
finally{unsubscribe();done();}
|
|
103
122
|
}
|
|
104
123
|
async configure(selectedId){
|
|
@@ -113,27 +132,26 @@ export class TerminalSession{
|
|
|
113
132
|
async logs(){
|
|
114
133
|
const statuses=runStatuses;
|
|
115
134
|
while(true){
|
|
116
|
-
const history=await this.app.listRunHistory(),
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
135
|
+
const history=(await this.app.listRunHistory()).sort((a,b)=>(b.status==='running')-(a.status==='running'));
|
|
136
|
+
if(!history.length){this.ui.show('还没有执行记录。运行工作流后,可在 /logs 回看。');return;}
|
|
137
|
+
const titles=new Map((await this.app.listWorkflows()).map(flow=>[flow.id,flow.title??flow.id]));
|
|
138
|
+
const choice=await this.ui.select('执行记录 · 选择一次执行,查看步骤、输入输出与日志',[
|
|
139
|
+
...history.map(r=>({value:`run:${r.id}`,label:`${statuses[r.status]??r.status} · ${titles.get(r.workflowId)??r.workflowId}`,description:`${new Date(r.startedAt).toLocaleString()} · ${r.id.slice(0,8)}`})),
|
|
140
|
+
{value:'back',label:'返回'},
|
|
120
141
|
]);
|
|
121
142
|
if(choice==='back')return;
|
|
122
|
-
if(choice==='settings'){
|
|
123
|
-
const mode=await this.ui.select('保留策略(整个项目合计,仅清理日志,保留业务文件)',[{value:'count',label:'仅保留最近 N 次已结束运行'},{value:'all',label:'不自动清理'},{value:'back',label:'返回'}]);
|
|
124
|
-
if(mode==='back')continue;
|
|
125
|
-
const count=mode==='all'?null:Number(await this.ui.input('保留次数(例如 10)',{value:String(keep??10),files:false}));
|
|
126
|
-
await this.app.configureLogs(count);this.ui.show('日志保留设置已保存。');continue;
|
|
127
|
-
}
|
|
128
143
|
const id=choice.slice(4),record=history.find(r=>r.id===id);
|
|
129
|
-
try{
|
|
144
|
+
try{
|
|
145
|
+
if(this.ui.observeRun)await this.openRun(record,{initialView:2});
|
|
146
|
+
else await this.ui.viewMarkdown(`${record.workflowId} · ${statuses[record.status]??record.status}`,await this.app.readLog(id));
|
|
147
|
+
}
|
|
130
148
|
catch(error){if(error.code!=='PROMPT_CANCELLED')throw error;}
|
|
131
149
|
}
|
|
132
150
|
}
|
|
133
151
|
async add(){
|
|
134
152
|
this.writable();const flow=structuredClone(this.current()),catalog=this.app.catalog(),workflows=await this.app.listWorkflows();
|
|
135
153
|
await this.addNode(flow,[],catalog,workflows);
|
|
136
|
-
this.flow=flow;this.dirty=true;this.ui.show('
|
|
154
|
+
this.flow=flow;this.dirty=true;this.ui.show('步骤已添加,可在工作流菜单继续调整或保存。');
|
|
137
155
|
}
|
|
138
156
|
async addNode(flow,parent,catalog,workflows){
|
|
139
157
|
this.writable();
|
|
@@ -226,6 +244,7 @@ export class TerminalSession{
|
|
|
226
244
|
const index=flow.steps.findIndex(s=>s.id===id);
|
|
227
245
|
if(this.app.project.readOnly){this.ui.show(JSON.stringify(flow.steps[index],null,2));return;}
|
|
228
246
|
await this.stepOperation(flow,[index],catalog,workflows);
|
|
247
|
+
if(isDeepStrictEqual(flow,this.flow))return;
|
|
229
248
|
this.flow=flow;this.dirty=true;this.ui.show('步骤已更新,保存前可继续调整。');
|
|
230
249
|
}
|
|
231
250
|
async outputs(){
|
|
@@ -241,22 +260,42 @@ export class TerminalSession{
|
|
|
241
260
|
}else delete flow.outputs[action.slice(7)];
|
|
242
261
|
this.flow=flow;this.dirty=true;this.ui.show('汇总输出已更新。');
|
|
243
262
|
}
|
|
244
|
-
async startRun(args){
|
|
263
|
+
async startRun(args,{draft=false}={}){
|
|
245
264
|
if(args.length>2)throw new Error('用法:/run [工作流ID] [@参数.json]');
|
|
246
265
|
const [first,second]=args;let reference=first?.startsWith('@')?first:second;
|
|
247
266
|
if(first?.startsWith('@')&&second)throw new Error('参数文件后不能再附加参数');
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
267
|
+
const workflows=await this.app.listWorkflows();
|
|
268
|
+
let flow=draft?this.current():null;
|
|
269
|
+
if(first&&!first.startsWith('@')){
|
|
270
|
+
flow=workflows.find(w=>w.id===first);if(!flow)throw new Error(`工作流不存在:${first}`);
|
|
271
|
+
}
|
|
272
|
+
if(!flow){
|
|
273
|
+
if(!workflows.length){this.ui.show('还没有已保存的工作流,请在 /workflows 中新建并保存。');return false;}
|
|
274
|
+
const id=await this.ui.select('运行 · 选择已保存的工作流',[...workflows.map(w=>({value:`workflow:${w.id}`,label:w.title??w.id,description:w.id})),{value:'back',label:'返回主面板'}]);
|
|
275
|
+
if(id==='back')return false;flow=workflows.find(w=>`workflow:${w.id}`===id);
|
|
276
|
+
}
|
|
277
|
+
if(flow===this.flow&&this.dirty){
|
|
278
|
+
const saved=workflows.find(w=>w.id===flow.id);
|
|
279
|
+
const choice=await this.ui.select(`${flow.title??flow.id} · 有未保存的修改`,[
|
|
280
|
+
{value:'cancel',label:'返回编辑',description:'保留草稿,本次不运行'},
|
|
281
|
+
{value:'save',label:'保存并运行',description:'将当前修改保存到工作流文件'},
|
|
282
|
+
...(saved?[{value:'saved',label:'运行已保存版本',description:'保留当前草稿,不执行未保存修改'}]:[]),
|
|
283
|
+
]);
|
|
284
|
+
if(choice==='cancel'){this.ui.show('已取消本次运行,草稿保留,返回工作流编辑。');return false;}
|
|
285
|
+
if(choice==='saved')flow=saved;
|
|
286
|
+
else await this.execute('/save');
|
|
251
287
|
}
|
|
252
|
-
if(!flow)throw new Error('先用 /workflows 选择工作流,或使用 /run 工作流ID');
|
|
253
|
-
if(flow===this.flow&&this.dirty){if(!await this.ui.confirm('保存当前修改后运行?'))return;await this.execute('/save');}
|
|
254
288
|
await this.app.checkWorkflow(flow.id);
|
|
255
289
|
const input=reference?await readRunInput(reference,this.baseDir):(needsJsonEditor(flow.inputs)||Object.keys(flow.inputs?.properties??{}).length)?await editValue(this.ui,flow.inputs,{}, {baseDir:this.baseDir,label:'本次运行参数',required:true}):{};
|
|
256
290
|
const run=await this.app.startRun(flow.id,input);
|
|
291
|
+
if(this.ui.trackRun){
|
|
292
|
+
this.ui.clearMessages?.();this.ui.trackRun({app:this.app,runId:run.id,title:flow.title??flow.id,input});
|
|
293
|
+
void run.done.catch(error=>{if(!this.ui.closed)this.ui.show(`运行记录保存失败:${error.message}`);});return true;
|
|
294
|
+
}
|
|
257
295
|
if(this.ui.observeRun){
|
|
258
296
|
await this.ui.observeRun({app:this.app,runId:run.id,flow,workflows:await this.app.listWorkflows()});
|
|
259
|
-
this.
|
|
297
|
+
const status=this.app.getRun(run.id)?.status;
|
|
298
|
+
this.ui.clearMessages?.();this.ui.show(`${status==='running'?'已返回工作台,任务在后台继续执行。':`本次运行${runStatuses[status]??'已结束'}。`}\n输入 /logs 查看任意一次执行的进度、结果和日志。`);return true;
|
|
260
299
|
}
|
|
261
300
|
const unsubscribe=this.app.subscribe(e=>{if(e.runId===run.id&&e.type.startsWith('step.'))this.ui.show(`${e.type} · ${e.callPath}${e.message?' · '+e.message:''}`);});
|
|
262
301
|
const done=this.ui.busy('运行中 · Ctrl+C 取消当前运行',()=>this.app.cancelRun(run.id));
|
|
@@ -266,14 +305,38 @@ export class TerminalSession{
|
|
|
266
305
|
if(result.status==='succeeded'&&!Object.keys(result.output??{}).length)this.ui.show(JSON.stringify(result.events.filter(e=>e.type==='step.succeeded').map(e=>({step:e.callPath,output:e.output})),null,2));
|
|
267
306
|
this.ui.show(`输入 /logs 在终端内查看日志\n可读日志:${path.join(this.app.outputDir,run.id,'run.md')}\n结果目录:${path.join(this.app.outputDir,run.id)}`);
|
|
268
307
|
}finally{unsubscribe();done();}
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
async pluginSkills(id){
|
|
311
|
+
const plugin=this.app.listPlugins().find(p=>p.id===id),commands=this.app.listCommands().filter(c=>c.plugin===id);
|
|
312
|
+
const choice=await this.ui.select(`[插件] ${plugin?.title??id} · 选择能力`,[
|
|
313
|
+
...commands.map(c=>({value:`command:${c.name}`,label:c.description??c.name,description:`/${c.name}`})),{value:'back',label:'返回主面板'},
|
|
314
|
+
]);
|
|
315
|
+
if(choice!=='back')await this.pluginCommand(commands.find(c=>`command:${c.name}`===choice),[]);
|
|
316
|
+
}
|
|
317
|
+
async toolConfig(){
|
|
318
|
+
while(true){
|
|
319
|
+
const keep=this.app.logSettings().keepRuns;
|
|
320
|
+
const choice=await this.ui.select('Fishtools 配置',[
|
|
321
|
+
{value:'info',label:'查看工作区信息'},
|
|
322
|
+
...(!this.app.project.readOnly?[{value:'logs',label:'日志保留次数',description:keep===null?'不自动清理':`最近 ${keep} 次`}]:[]),
|
|
323
|
+
{value:'back',label:'返回主面板'},
|
|
324
|
+
]);
|
|
325
|
+
if(choice==='back')return;
|
|
326
|
+
if(choice==='info')this.ui.show(`工作区:${this.app.projectDir}\n日志目录:${this.app.outputDir}\n模式:${this.app.project.readOnly?'只读运行包':'可编辑工作区'}`);
|
|
327
|
+
else await this.logSettings();
|
|
328
|
+
}
|
|
269
329
|
}
|
|
270
|
-
async
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
330
|
+
async logSettings(){
|
|
331
|
+
const keep=this.app.logSettings().keepRuns;
|
|
332
|
+
const mode=await this.ui.select('保留策略(整个项目合计,仅清理日志,保留业务文件)',[{value:'count',label:'仅保留最近 N 次已结束运行'},{value:'all',label:'不自动清理'},{value:'back',label:'返回'}]);
|
|
333
|
+
if(mode==='back')return;
|
|
334
|
+
const count=mode==='all'?null:Number(await this.ui.input('保留次数(例如 10)',{value:String(keep??10),files:false}));
|
|
335
|
+
await this.app.configureLogs(count);this.ui.show('日志保留设置已保存。');
|
|
336
|
+
}
|
|
337
|
+
async openRun(entry,{initialView}={}){
|
|
338
|
+
const record=this.app.getRun(entry.id)??await this.app.readRun(entry.id),workflows=await this.app.listWorkflows();
|
|
339
|
+
await this.ui.observeRun({app:this.app,runId:record.id,record,flow:workflows.find(flow=>flow.id===record.workflowId),workflows,initialView});
|
|
277
340
|
}
|
|
278
341
|
async plugins(){
|
|
279
342
|
this.ui.clearMessages?.();
|
|
@@ -339,7 +402,12 @@ export class TerminalSession{
|
|
|
339
402
|
const closure=await this.app.checkWorkflow([...entries][0]);
|
|
340
403
|
const ids=new Set(closure.plugins);
|
|
341
404
|
for(const id of [...entries].slice(1))for(const plugin of (await this.app.checkWorkflow(id)).plugins)ids.add(plugin);
|
|
342
|
-
if(includeWeb)
|
|
405
|
+
if(includeWeb){
|
|
406
|
+
const provider=[...this.app.host.definitions.values()].find(plugin=>typeof plugin.services?.web==='function');
|
|
407
|
+
if(!provider)throw new Error('未安装提供 Web 服务的插件');
|
|
408
|
+
ids.add(provider.id);
|
|
409
|
+
}
|
|
410
|
+
for(const id of ids)for(const dependency of Object.keys(this.app.host.definitions.get(id)?.dependencies??{}))ids.add(dependency);
|
|
343
411
|
const environments={};
|
|
344
412
|
for(const id of ids){
|
|
345
413
|
const plugin=this.app.host.definitions.get(id),declarations=new Map();
|
|
@@ -363,7 +431,7 @@ export class TerminalSession{
|
|
|
363
431
|
|
|
364
432
|
export async function startTerminal(app,options={}){
|
|
365
433
|
const {TerminalView}=await import('./view.js');
|
|
366
|
-
const ui=new TerminalView({baseDir:process.cwd()}),session=new TerminalSession(app,ui,options);
|
|
434
|
+
const ui=new TerminalView({baseDir:options.baseDir??process.cwd(),previewLabel:options.previewLabel}),session=new TerminalSession(app,ui,options);
|
|
367
435
|
const terminate=()=>{session.exiting=true;ui.pending?.reject(Object.assign(new Error('退出'),{code:'PROMPT_CANCELLED'}));for(const r of app.listRuns())if(r.status==='running')app.cancelRun(r.id);};
|
|
368
436
|
process.once('SIGTERM',terminate);
|
|
369
437
|
try{ui.start();await session.run();}finally{process.removeListener('SIGTERM',terminate);await ui.close();}
|
|
@@ -6,6 +6,7 @@ export const cleanText=value=>stripVTControlCharacters(String(value)).replace(/[
|
|
|
6
6
|
const color=(rgb,text)=>Object.hasOwn(process.env,'NO_COLOR')?text:`\x1b[38;2;${rgb}m${text}\x1b[39m`;
|
|
7
7
|
export const palette={
|
|
8
8
|
brand:text=>color('78;201;176',text),
|
|
9
|
+
error:text=>color('244;117;117',text),
|
|
9
10
|
text:text=>color('229;233;237',text),
|
|
10
11
|
muted:text=>color('139;149;158',text),
|
|
11
12
|
border:text=>color('74;101;99',text),
|