@yanglaofish/fishtools 0.5.3 → 0.6.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 +3 -3
- package/manual/README.md +3 -3
- package/manual/docs/modules.md +2 -0
- package/manual/docs/plans/2026-10-13-effective-inputs-and-tables.md +39 -0
- package/manual/docs/plans/2026-10-13-plugin-defaults.md +45 -0
- package/manual/docs/plans/2026-10-14-issue7-recovery.md +33 -0
- package/manual/docs/plans/2026-10-14-issue8-10-workflows.md +26 -0
- package/manual/docs/plans/2026-10-14-issue9-completion.md +47 -0
- package/manual/docs/plans/2026-10-14-managed-execution.md +58 -0
- package/manual/docs/plans/2026-10-14-plugin-discovery-history.md +41 -0
- package/manual/docs/plans/2026-10-14-structured-reports.md +77 -0
- package/manual/docs/plans/2026-10-14-unreleased-review.md +47 -0
- package/manual/docs/plugin-development.md +161 -8
- package/manual/docs/releases.md +25 -6
- package/manual/docs/superpowers/plans/2026-10-13-tui-readable-sessions.md +2 -2
- package/manual/docs/superpowers/reviews/2026-10-13-tui-sessions-review.md +2 -0
- package/manual/docs/superpowers/reviews/2026-10-14-0.6.0-release-review.md +19 -0
- package/manual/docs/verification.md +17 -0
- package/manual/packages/core/README.md +6 -0
- package/manual/packages/core/src/application.js +15 -9
- package/manual/packages/core/src/plugin-catalog.js +20 -2
- package/manual/packages/core/src/plugin-loading.js +24 -0
- package/manual/packages/core/src/version.js +1 -1
- package/manual/packages/fst-cli/README.md +5 -1
- package/manual/packages/fst-cli/src/terminal/command-menu.js +3 -2
- package/manual/packages/fst-cli/src/terminal/file-autocomplete.js +106 -0
- package/manual/packages/fst-cli/src/terminal/log-viewer.js +4 -3
- package/manual/packages/fst-cli/src/terminal/navigation.js +23 -1
- package/manual/packages/fst-cli/src/terminal/presentation.js +2 -1
- package/manual/packages/fst-cli/src/terminal/report-presentation.js +38 -0
- package/manual/packages/fst-cli/src/terminal/result-table.js +66 -0
- package/manual/packages/fst-cli/src/terminal/run-feed.js +42 -17
- package/manual/packages/fst-cli/src/terminal/run-presentation.js +51 -17
- package/manual/packages/fst-cli/src/terminal/run-viewer.js +15 -12
- package/manual/packages/fst-cli/src/terminal/schema.js +114 -4
- package/manual/packages/fst-cli/src/terminal/session-history.js +2 -2
- package/manual/packages/fst-cli/src/terminal/session.js +40 -15
- package/manual/packages/fst-cli/src/terminal/view.js +80 -68
- package/manual/packages/fst-cli/src/terminal/workflow-menu.js +3 -1
- package/manual/packages/fst-devtools/README.md +2 -0
- package/manual/packages/fst-devtools/src/plugin-quality.js +4 -1
- package/manual/packages/fst-devtools/src/plugin-tools.js +1 -0
- package/manual/packages/fst-devtools/src/process-usage.js +62 -0
- package/manual/packages/fst-platform/README.md +5 -0
- package/manual/packages/fst-platform/package.json +1 -0
- package/manual/packages/fst-platform/src/managed-process.js +36 -0
- package/manual/packages/fst-platform/src/process.js +7 -2
- package/manual/packages/fst-plugin/README.md +4 -0
- package/manual/packages/fst-plugin/src/contracts.js +3 -0
- package/manual/packages/fst-plugin/src/host.js +38 -23
- package/manual/packages/fst-plugin/src/runtime.js +14 -8
- package/manual/packages/fst-workflow/src/engine.js +1 -1
- package/manual/packages/fst-workspace/README.md +4 -0
- package/manual/packages/fst-workspace/src/plugin-registry.js +1 -1
- package/manual/packages/fst-workspace/src/plugins.js +5 -3
- package/manual/packages/fst-workspace/src/workflows.js +27 -9
- package/manual/packages/sdk/README.md +10 -0
- package/manual/packages/sdk/inputs.js +53 -0
- package/manual/packages/sdk/package.json +2 -0
- package/manual/packages/sdk/reports.js +53 -0
- package/node_modules/@mytools/core/README.md +6 -0
- package/node_modules/@mytools/core/src/application.js +15 -9
- package/node_modules/@mytools/core/src/plugin-catalog.js +20 -2
- package/node_modules/@mytools/core/src/plugin-loading.js +24 -0
- package/node_modules/@mytools/core/src/version.js +1 -1
- package/node_modules/@mytools/fst-cli/README.md +5 -1
- package/node_modules/@mytools/fst-cli/src/terminal/command-menu.js +3 -2
- package/node_modules/@mytools/fst-cli/src/terminal/file-autocomplete.js +106 -0
- package/node_modules/@mytools/fst-cli/src/terminal/log-viewer.js +4 -3
- package/node_modules/@mytools/fst-cli/src/terminal/navigation.js +23 -1
- package/node_modules/@mytools/fst-cli/src/terminal/presentation.js +2 -1
- package/node_modules/@mytools/fst-cli/src/terminal/report-presentation.js +38 -0
- package/node_modules/@mytools/fst-cli/src/terminal/result-table.js +66 -0
- package/node_modules/@mytools/fst-cli/src/terminal/run-feed.js +42 -17
- package/node_modules/@mytools/fst-cli/src/terminal/run-presentation.js +51 -17
- package/node_modules/@mytools/fst-cli/src/terminal/run-viewer.js +15 -12
- package/node_modules/@mytools/fst-cli/src/terminal/schema.js +114 -4
- package/node_modules/@mytools/fst-cli/src/terminal/session-history.js +2 -2
- package/node_modules/@mytools/fst-cli/src/terminal/session.js +40 -15
- package/node_modules/@mytools/fst-cli/src/terminal/view.js +80 -68
- package/node_modules/@mytools/fst-cli/src/terminal/workflow-menu.js +3 -1
- package/node_modules/@mytools/fst-devtools/README.md +2 -0
- package/node_modules/@mytools/fst-devtools/src/plugin-quality.js +4 -1
- package/node_modules/@mytools/fst-devtools/src/plugin-tools.js +1 -0
- package/node_modules/@mytools/fst-devtools/src/process-usage.js +62 -0
- package/node_modules/@mytools/fst-platform/README.md +5 -0
- package/node_modules/@mytools/fst-platform/package.json +1 -0
- package/node_modules/@mytools/fst-platform/src/managed-process.js +36 -0
- package/node_modules/@mytools/fst-platform/src/process.js +7 -2
- package/node_modules/@mytools/fst-plugin/README.md +4 -0
- package/node_modules/@mytools/fst-plugin/src/contracts.js +3 -0
- package/node_modules/@mytools/fst-plugin/src/host.js +38 -23
- package/node_modules/@mytools/fst-plugin/src/runtime.js +14 -8
- package/node_modules/@mytools/fst-workflow/src/engine.js +1 -1
- package/node_modules/@mytools/fst-workspace/README.md +4 -0
- package/node_modules/@mytools/fst-workspace/src/plugin-registry.js +1 -1
- package/node_modules/@mytools/fst-workspace/src/plugins.js +5 -3
- package/node_modules/@mytools/fst-workspace/src/workflows.js +27 -9
- package/node_modules/@mytools/sdk/README.md +10 -0
- package/node_modules/@mytools/sdk/inputs.js +53 -0
- package/node_modules/@mytools/sdk/package.json +2 -0
- package/node_modules/@mytools/sdk/reports.js +53 -0
- package/node_modules/@mytools/web/public/THIRD_PARTY_NOTICES.txt +1032 -1032
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {randomUUID} from 'node:crypto';
|
|
2
2
|
import {isDeepStrictEqual} from 'node:util';
|
|
3
3
|
import {isCommandName} from '@mytools/fst-plugin/contracts';
|
|
4
|
-
import {editValue} from './schema.js';
|
|
4
|
+
import {editValue,editInput} from './schema.js';
|
|
5
5
|
|
|
6
6
|
export async function runShortcut(session,entry,args=[]){
|
|
7
7
|
if(!entry.available)throw new Error(`快捷名称 /${entry.name} 已失效:${entry.reason??'目标不存在'}`);
|
|
@@ -153,7 +153,8 @@ async function editShortcut(session,oldName){
|
|
|
153
153
|
}else if(choice==='input'){
|
|
154
154
|
const metadata=target.action?app.catalog().find(action=>action.id===target.action):app.listCommands().find(command=>command.name===target.command&&command.plugin===target.plugin);
|
|
155
155
|
if(!metadata){ui.show('目标已失效,请先重新选择目标。');continue;}
|
|
156
|
-
|
|
156
|
+
const preview=app.previewInput?value=>app.previewInput(target.action??`/${target.command}`,value):undefined;
|
|
157
|
+
target.input=await editInput(ui,metadata.input??{type:'object',properties:{},additionalProperties:false},target.input??{},{baseDir:session.baseDir,required:true,label:'快捷名称默认参数',preview});
|
|
157
158
|
}else if(choice==='save'){
|
|
158
159
|
const next=structuredClone(app.project.shortcuts??{});if(oldName)delete next[oldName];
|
|
159
160
|
if(Object.hasOwn(next,name)){ui.show('快捷名称已经存在,请选择其他名称。');continue;}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {homedir} from 'node:os';
|
|
3
|
+
import {CombinedAutocompleteProvider} from '@mariozechner/pi-tui';
|
|
4
|
+
import {createFileProbe} from '@mytools/fst-platform/fs';
|
|
5
|
+
import {cleanText as safe} from './theme.js';
|
|
6
|
+
|
|
7
|
+
function budget(value,fallback,max){return Number.isSafeInteger(value)&&value>0?Math.min(value,max):fallback;}
|
|
8
|
+
export function createAutocompleteProbe(options={}){
|
|
9
|
+
return options.probe??createFileProbe({timeout:budget(options.timeout,1200,10000),maxConcurrent:1,maxPending:1,allowRemote:options.allowRemote===true});
|
|
10
|
+
}
|
|
11
|
+
function remote(value){
|
|
12
|
+
const normalized=value.replaceAll('/','\\');
|
|
13
|
+
if(/^\\\\[?.]\\/u.test(normalized))return /^\\\\[?.]\\UNC(?:\\|$)/iu.test(normalized)||/^\\\\[?.]\\GLOBALROOT\\Device\\(?:Mup|LanmanRedirector|WebDavRedirector)(?:\\|$)/iu.test(normalized);
|
|
14
|
+
return /^\\\\/u.test(normalized)||/^\\(?:\?\?\\UNC|Device\\(?:Mup|LanmanRedirector|WebDavRedirector))(?:\\|$)/iu.test(normalized);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 同步接口只查有界缓存;所有文件 I/O 交给可取消的隔离探测进程。
|
|
18
|
+
export class FileAutocomplete extends CombinedAutocompleteProvider{
|
|
19
|
+
constructor(commands=[],baseDir=process.cwd(),workflows=[],options={}){
|
|
20
|
+
super(commands,baseDir);this.baseDir=baseDir;this.workflows=workflows;
|
|
21
|
+
this.defaultCommands=new CombinedAutocompleteProvider(commands.filter(command=>!command.searchOnly),baseDir);
|
|
22
|
+
this.files=options.files!==false;this.allowRemote=options.allowRemote===true;
|
|
23
|
+
this.entryLimit=budget(options.entryLimit,1000,10000);this.maxItems=budget(options.maxItems,100,100);
|
|
24
|
+
this.cacheLimit=budget(options.cacheLimit,16,64);this.cacheTtl=budget(options.cacheTtl,5000,60000);
|
|
25
|
+
this.timeout=budget(options.timeout,1200,10000);this.debounceMs=options.debounceMs===0?0:budget(options.debounceMs,40,500);
|
|
26
|
+
this.probe=createAutocompleteProbe(options);this.ownsProbe=options.ownsProbe!==false;
|
|
27
|
+
this.onUpdate=options.onUpdate;this.cache=new Map();this.closed=false;
|
|
28
|
+
}
|
|
29
|
+
cancelPending(){
|
|
30
|
+
const pending=this.pending;this.pending=null;
|
|
31
|
+
if(pending){clearTimeout(pending.timer);pending.controller.abort();}
|
|
32
|
+
}
|
|
33
|
+
close(){
|
|
34
|
+
if(this.closing)return this.closing;
|
|
35
|
+
this.closed=true;this.cancelPending();this.cache.clear();this.onUpdate=null;
|
|
36
|
+
return this.closing=Promise.resolve(this.ownsProbe?this.probe.close():undefined);
|
|
37
|
+
}
|
|
38
|
+
schedule(directory,snapshot){
|
|
39
|
+
if(this.pending?.directory===directory){this.pending.snapshot=snapshot;return;}
|
|
40
|
+
this.cancelPending();
|
|
41
|
+
const pending={directory,snapshot,controller:new AbortController()};this.pending=pending;
|
|
42
|
+
pending.timer=setTimeout(async()=>{
|
|
43
|
+
let result;
|
|
44
|
+
try{result=await this.probe.readDirectory(directory,{limit:this.entryLimit,timeout:this.timeout,signal:pending.controller.signal});}
|
|
45
|
+
catch{result={status:'error',incomplete:true,reason:'probeFailed'};}
|
|
46
|
+
if(this.closed||this.pending!==pending||pending.controller.signal.aborted)return;
|
|
47
|
+
this.pending=null;
|
|
48
|
+
// 错误和超时也短暂缓存,避免按键触发无限重试。
|
|
49
|
+
const cached={status:result.status,incomplete:result.incomplete,reason:result.reason,
|
|
50
|
+
value:result.status==='ok'?result.value.slice(0,this.entryLimit):[],expires:Date.now()+this.cacheTtl};
|
|
51
|
+
this.cache.delete(directory);this.cache.set(directory,cached);
|
|
52
|
+
while(this.cache.size>this.cacheLimit)this.cache.delete(this.cache.keys().next().value);
|
|
53
|
+
this.lastResult={status:cached.status,incomplete:cached.incomplete,reason:cached.reason};
|
|
54
|
+
this.onUpdate?.(pending.snapshot);
|
|
55
|
+
},this.debounceMs);
|
|
56
|
+
}
|
|
57
|
+
getSuggestions(lines,line,col){
|
|
58
|
+
if(this.closed)return null;
|
|
59
|
+
const text=(lines[line]??'').slice(0,col);
|
|
60
|
+
const prefix=text.match(/(?:^|\s)(@"[^"\n]*|@[^\s]*)$/)?.[1];
|
|
61
|
+
if(!prefix){
|
|
62
|
+
if(text==='/'){this.cancelPending();return this.defaultCommands.getSuggestions(lines,line,col);}
|
|
63
|
+
const run=text.match(/^\/run\s+([^@"\s]*)$/);
|
|
64
|
+
if(run){
|
|
65
|
+
this.cancelPending();
|
|
66
|
+
const query=run[1].toLowerCase(),items=this.workflows.filter(flow=>`${flow.id} ${flow.title??''}`.toLowerCase().includes(query)).slice(0,this.maxItems).map(flow=>({value:flow.id,label:flow.title??flow.id,description:flow.id,workflow:true}));
|
|
67
|
+
return items.length?{prefix:run[1],items}:null;
|
|
68
|
+
}
|
|
69
|
+
// 只把命令交给上游,不能回落到它的同步文件系统实现。
|
|
70
|
+
if(/^\/[^\s/]*(?:\s.*)?$/.test(text)){this.cancelPending();return super.getSuggestions(lines,line,col);}
|
|
71
|
+
}
|
|
72
|
+
if(!this.files)return null;
|
|
73
|
+
const filePrefix=prefix??this.extractPathPrefix(text,false);
|
|
74
|
+
if(filePrefix===null){this.cancelPending();return null;}
|
|
75
|
+
const raw=filePrefix.replace(/^@/,'').replace(/^"/,'').replaceAll('\\','/');
|
|
76
|
+
// 解析前判定,避免 POSIX resolve 抹掉 UNC 的双斜杠或 Windows 设备前缀。
|
|
77
|
+
if(!this.allowRemote&&(remote(raw)||remote(this.baseDir))){
|
|
78
|
+
this.cancelPending();this.lastResult={status:'skipped',incomplete:true,reason:'remotePath'};return null;
|
|
79
|
+
}
|
|
80
|
+
const expanded=raw.startsWith('~/')?path.join(homedir(),raw.slice(2)):raw==='~'?homedir():raw;
|
|
81
|
+
const absolute=path.resolve(this.baseDir,expanded||'.');
|
|
82
|
+
const directory=!raw||raw==='~'||raw.endsWith('/')?absolute:path.dirname(absolute);
|
|
83
|
+
const query=!raw||raw==='~'||raw.endsWith('/')?'':path.basename(absolute).toLowerCase();
|
|
84
|
+
const cached=this.cache.get(directory);
|
|
85
|
+
if(!cached||cached.expires<=Date.now()){
|
|
86
|
+
this.schedule(directory,{lines:[...lines],line,col});return null;
|
|
87
|
+
}
|
|
88
|
+
if(this.pending?.directory!==directory)this.cancelPending();
|
|
89
|
+
this.lastResult={status:cached.status,incomplete:cached.incomplete,reason:cached.reason};
|
|
90
|
+
const items=cached.value.filter(entry=>entry.name.toLowerCase().startsWith(query)).map(entry=>{
|
|
91
|
+
// 链接只作为链接候选,不逐个 stat 目标。用户显式输入 / 后才读取其内容。
|
|
92
|
+
const dir=entry.type==='directory',head=raw==='~'?'~/':raw.slice(0,raw.lastIndexOf('/')+1),file=head+entry.name+(dir?'/':'');
|
|
93
|
+
return {value:`${prefix?'@':''}${/[\s"]/.test(file)?JSON.stringify(file):file}`,label:safe(entry.name)+(dir?'/':''),description:dir?'目录':entry.type==='symlink'?'符号链接':'文件',directory:dir,file:true};
|
|
94
|
+
}).sort((a,b)=>Number(b.directory)-Number(a.directory)||a.label.localeCompare(b.label)).slice(0,this.maxItems);
|
|
95
|
+
return items.length?{prefix:filePrefix,items,incomplete:cached.incomplete}:null;
|
|
96
|
+
}
|
|
97
|
+
getForceFileSuggestions(lines,line,col){return this.getSuggestions(lines,line,col);}
|
|
98
|
+
applyCompletion(lines,line,col,item,prefix){
|
|
99
|
+
if(item.workflow){const next=[...lines],before=lines[line].slice(0,col-prefix.length),text=item.value+' ';next[line]=before+text+lines[line].slice(col);return {lines:next,cursorLine:line,cursorCol:before.length+text.length};}
|
|
100
|
+
if(!item.file&&!prefix.startsWith('@'))return super.applyCompletion(lines,line,col,item,prefix);
|
|
101
|
+
const before=lines[line].slice(0,col-prefix.length);
|
|
102
|
+
let after=lines[line].slice(col);if(after.startsWith('"'))after=after.slice(1);
|
|
103
|
+
const completed=item.value+(item.directory?'':' '),next=[...lines];next[line]=before+completed+after;
|
|
104
|
+
return {lines:next,cursorLine:line,cursorCol:before.length+completed.length-(item.directory&&item.value.endsWith('"')?1:0)};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { Markdown, matchesKey, truncateToWidth } from '@mariozechner/pi-tui';
|
|
2
2
|
import { mouseWheel } from './run-viewer.js';
|
|
3
|
-
import {palette,markdownTheme} from './theme.js';
|
|
3
|
+
import {cleanText,palette,markdownTheme} from './theme.js';
|
|
4
4
|
|
|
5
5
|
const theme=markdownTheme();
|
|
6
6
|
|
|
7
|
-
//
|
|
7
|
+
// 每次只向 Overlay 提交当前窗口内的行;动态结果在消费时清理控制字符。
|
|
8
8
|
export class MarkdownLogViewer{
|
|
9
9
|
constructor({title,text,terminal,onClose,requestRender}){
|
|
10
10
|
this.title=title.replace(/\s+/g,' ');this.terminal=terminal;this.onClose=onClose;this.requestRender=requestRender;
|
|
11
|
-
this.markdown=new Markdown(text,0,0,theme);this.offset=0;
|
|
11
|
+
this.source=text;this.markdown=new Markdown(typeof text==='function'?'':cleanText(text),0,0,theme);this.offset=0;
|
|
12
12
|
}
|
|
13
13
|
invalidate(){this.markdown.invalidate();}
|
|
14
14
|
layout(width){
|
|
15
|
+
if(typeof this.source==='function'&&this.sourceWidth!==width){this.markdown.setText(cleanText(this.source(width)));this.sourceWidth=width;}
|
|
15
16
|
const height=Math.max(1,this.terminal.rows);
|
|
16
17
|
const pageSize=Math.max(1,height-2);
|
|
17
18
|
// Markdown 自身缓存同一宽度的完整渲染,滚动时不重新解析日志。
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {builtinCommands} from '@mytools/fst-plugin/contracts';
|
|
1
|
+
import {builtinCommands,isCommandName} from '@mytools/fst-plugin/contracts';
|
|
2
2
|
|
|
3
3
|
const visible=['workflows','run','pack','logs','plugin','commands','shortcuts','config','tools','help','exit'];
|
|
4
4
|
// 入口由插件 ID 推导;冲突时加前缀,不能覆盖内置或作者注册的命令。
|
|
@@ -18,3 +18,25 @@ export function commandMenu(plugins,commands,shortcuts=[]){
|
|
|
18
18
|
}
|
|
19
19
|
return entries;
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
// 空斜杠保持精简;输入前缀后可搜索所有可执行命令及无歧义的插件命名空间。
|
|
23
|
+
export function commandCompletions(plugins,commands,shortcuts=[]){
|
|
24
|
+
const entries=commandMenu(plugins,commands,shortcuts),used=new Set(entries.map(entry=>entry.name));
|
|
25
|
+
const menuLength=entries.length,aliases=[];
|
|
26
|
+
for(const command of [...builtinCommands,...commands])if(!used.has(command.name)){
|
|
27
|
+
used.add(command.name);entries.push({name:command.name,description:command.description??command.name,searchOnly:true});
|
|
28
|
+
}
|
|
29
|
+
const namespaces=new Map();
|
|
30
|
+
for(const command of commands){
|
|
31
|
+
if(!command.name.includes('.'))continue;
|
|
32
|
+
const name=command.name.split('.')[0];
|
|
33
|
+
if(!namespaces.has(name))namespaces.set(name,new Set());
|
|
34
|
+
namespaces.get(name).add(command.plugin);
|
|
35
|
+
}
|
|
36
|
+
for(const [name,owners] of namespaces)if(owners.size===1&&!used.has(name)&&isCommandName(name)){
|
|
37
|
+
const id=[...owners][0],plugin=plugins.find(plugin=>plugin.id===id);
|
|
38
|
+
if(!plugin)continue;
|
|
39
|
+
used.add(name);aliases.push({name,plugin:id,searchOnly:true,description:`[插件] ${plugin.title??id} · 选择能力`});
|
|
40
|
+
}
|
|
41
|
+
return [...entries.slice(0,menuLength),...aliases,...entries.slice(menuLength)];
|
|
42
|
+
}
|
|
@@ -4,7 +4,7 @@ export const logo=[
|
|
|
4
4
|
'▀ ▀▀▀ ▀▀▀ ▀ ▀',
|
|
5
5
|
].join('\n');
|
|
6
6
|
|
|
7
|
-
export const pluginStatus=plugin=>({ready:'可用',pending:'等待服务',failed:'失败',stopped:'已停止',unconfigured:'待配置'})[plugin.runtime?.status]??'未知';
|
|
7
|
+
export const pluginStatus=plugin=>({ready:'可用',pending:'等待服务',failed:'失败',stopped:'已停止',unconfigured:'待配置',unavailable:'不可用'})[plugin.runtime?.status]??'未知';
|
|
8
8
|
|
|
9
9
|
export function gettingStarted(flows,{readOnly=false}={}){
|
|
10
10
|
const sample=flows.find(w=>w.id==='hello')??flows[0];
|
|
@@ -36,6 +36,7 @@ export function pluginDetails(plugin,actions,commands){
|
|
|
36
36
|
plugin.description??'作者尚未提供用途说明。',
|
|
37
37
|
`标识:${plugin.id}\n\n版本:${plugin.version}\n\n来源:${({global:'用户级全局安装或链接',workspace:'工作区安装或链接(兼容旧版)',preset:'发行包内置 / 项目预置'})[plugin.scope]??(plugin.removable?'工作区安装或链接':'发行包内置 / 项目预置')}${plugin.source?`\n\n代码位置:${plugin.source}`:''}\n\n状态:${pluginStatus(plugin)}`,
|
|
38
38
|
...(plugin.configurationError?[`配置状态:${plugin.configurationError}\n\n在当前工作区设置插件配置后启用。`]:[]),
|
|
39
|
+
...(plugin.loadError?[`不可用原因:${plugin.loadError.message}\n\n修复上述插件来源后重启,或从插件管理移除此插件。`]:[]),
|
|
39
40
|
...(plugin.runtime?.missing?.length?[`缺少服务:${plugin.runtime.missing.join(', ')}`]:[]),
|
|
40
41
|
`更新方式:${!plugin.runtime?.updatePolicy?'无运行时服务':plugin.runtime.updatePolicy==='automatic'?'可自动短暂停止并重启':'需要本次维护确认'}\n\n插件权限:${privilege(plugin,plugin)}`,
|
|
41
42
|
`声明平台:${plugin.platforms?.join(', ')??'作者未声明'}\n\n声明架构:${plugin.architectures?.join(', ')??'作者未声明'}`,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {inspectReport} from '@mytools/sdk/reports';
|
|
2
|
+
import {cleanText as clean} from './theme.js';
|
|
3
|
+
import {isDeepStrictEqual} from 'node:util';
|
|
4
|
+
|
|
5
|
+
const levels={info:'提示',warning:'警告',error:'错误'};
|
|
6
|
+
function conclusions(value){
|
|
7
|
+
return [value.summary&&`摘要:${value.summary}`,...(value.alerts??[]).map(alert=>`${levels[alert.level]}:${alert.message}`),
|
|
8
|
+
...(value.recommendations?.length?['建议:',...value.recommendations.map((text,index)=>` ${index+1}. ${text}`)]:[])].filter(Boolean);
|
|
9
|
+
}
|
|
10
|
+
function dataSchema(section){
|
|
11
|
+
const properties=Object.fromEntries(Object.entries(section.titles??{}).map(([key,title])=>[key,{title}]));
|
|
12
|
+
return {'x-fst-table':section.columns,properties,items:{properties},additionalProperties:{properties}};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// 整个输出或 report 字段均可承载报告,不猜测其他同名业务字段的含义。
|
|
16
|
+
export function outputReport(value){
|
|
17
|
+
if(value?.kind==='fst-report')return value;
|
|
18
|
+
if(value?.report?.kind==='fst-report')return value.report;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function forwardedReport(output,finalOutput){
|
|
22
|
+
const report=outputReport(output);
|
|
23
|
+
return report&&isDeepStrictEqual(report,outputReport(finalOutput))?report:undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function renderReport(value,{indent='',render,truncated=false}){
|
|
27
|
+
if(truncated&&value?.kind==='fst-report')return clean(`${indent}报告内容为摘要,完整结果请在 /logs 中按 p 分页阅读。\n${render(value,indent,undefined,{raw:true})}`);
|
|
28
|
+
const result=inspectReport(value);
|
|
29
|
+
if(result.status==='none')return;
|
|
30
|
+
if(result.status!=='valid')return clean(`${indent}${result.message}\n${render(value,indent,undefined,{raw:true})}`);
|
|
31
|
+
const inset=(text,padding)=>String(text).split('\n').map(line=>' '.repeat(padding)+line).join('\n');
|
|
32
|
+
const lines=[`报告:${value.title}`,...conclusions(value).map(text=>inset(text,2))];
|
|
33
|
+
for(const section of value.sections??[]){
|
|
34
|
+
lines.push('',inset(`── ${section.title} ──`,2),...conclusions(section).map(text=>inset(text,4)));
|
|
35
|
+
if(Object.hasOwn(section,'data'))lines.push(inset(render(section.data,'',dataSchema(section),{inset:4}),4));
|
|
36
|
+
}
|
|
37
|
+
return clean(lines.join('\n')).split('\n').map(line=>indent+line).join('\n');
|
|
38
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {visibleWidth} from '@mariozechner/pi-tui';
|
|
2
|
+
import {cleanText as clean} from './theme.js';
|
|
3
|
+
|
|
4
|
+
const heading=value=>clean(String(value).replace(/[\r\n\t]+/g,' '));
|
|
5
|
+
const record=value=>value!==null&&typeof value==='object'&&!Array.isArray(value);
|
|
6
|
+
const scalar=value=>value===null||['string','number','boolean'].includes(typeof value);
|
|
7
|
+
const empty=value=>value==null||value===''||typeof value==='object'&&Object.keys(value).length===0;
|
|
8
|
+
const text=value=>value===undefined?'未提供':value===null?'空值':value===''?'空字符串':typeof value==='boolean'?(value?'是':'否'):clean(value);
|
|
9
|
+
const sameKeys=(a,b)=>a.length===b.length&&a.every(key=>b.includes(key));
|
|
10
|
+
|
|
11
|
+
// 表格只选择阅读列;完整的长值、复杂值和其余字段在表后按行号展开。
|
|
12
|
+
export function resultTable(value,{schema,width,indent,label,render}){
|
|
13
|
+
const available=width-visibleWidth(indent);
|
|
14
|
+
if(!Number.isFinite(available)||available<40)return;
|
|
15
|
+
const list=Array.isArray(value),entries=list?value.map((item,index)=>[String(index+1),item]):Object.entries(value);
|
|
16
|
+
if(entries.length<2||entries.some(([,item])=>!record(item)))return;
|
|
17
|
+
const firstKeys=Object.keys(entries[0][1]),declared=schema?.['x-fst-table'];
|
|
18
|
+
const explicit=Array.isArray(declared)&&declared.length>0&&declared.every(key=>typeof key==='string')&&new Set(declared).size===declared.length;
|
|
19
|
+
if(!explicit&&!entries.every(([,item])=>sameKeys(firstKeys,Object.keys(item))))return;
|
|
20
|
+
const rowSchema=list?(Array.isArray(schema?.items)?undefined:schema?.items):schema?.additionalProperties;
|
|
21
|
+
const candidates=(explicit?declared:firstKeys).filter(key=>entries.every(([,item])=>item[key]===undefined||scalar(item[key]))&&entries.some(([,item])=>item[key]!==undefined&&(explicit||item[key]!==null&&item[key]!=='')));
|
|
22
|
+
if(candidates.length<(explicit?1:2))return;
|
|
23
|
+
const duplicate=!explicit&&!list?candidates.find(name=>entries.every(([key,item])=>item[name]===key)):undefined;
|
|
24
|
+
const common=!explicit?candidates.filter(key=>key!==duplicate&&entries.every(([,item])=>item[key]===entries[0][1][key])&&visibleWidth(text(entries[0][1][key]))<=24&&!/[\r\n\t]/.test(text(entries[0][1][key]))):[];
|
|
25
|
+
const ordered=candidates.filter(key=>key!==duplicate&&!common.includes(key));
|
|
26
|
+
if(!explicit)ordered.sort((a,b)=>Number(entries.some(([,item])=>/[\r\n\t]/.test(text(item[a]))||visibleWidth(text(item[a]))>24))-Number(entries.some(([,item])=>/[\r\n\t]/.test(text(item[b]))||visibleWidth(text(item[b]))>24)));
|
|
27
|
+
// 条目键也可能很长,序号始终可定位;完整键在表后的对应条目显示。
|
|
28
|
+
const identity={key:null,title:list?'序号':duplicate?heading(label(duplicate,rowSchema?.properties?.[duplicate])):'条目',values:entries.map(([key])=>list?key:clean(key)),width:0};
|
|
29
|
+
const columns=[identity,...ordered.map(key=>({key,title:heading(label(key,rowSchema?.properties?.[key])),values:entries.map(([,item])=>text(item[key]))}))];
|
|
30
|
+
for(const column of columns)column.width=column.values.reduce((size,value)=>Math.max(size,/[\r\n\t]/.test(value)||visibleWidth(value)>24?Math.max(6,Math.min(24,visibleWidth(value))):visibleWidth(value)),visibleWidth(column.title));
|
|
31
|
+
const selected=[];let used=1;
|
|
32
|
+
for(const column of columns){
|
|
33
|
+
if(used+column.width+3>available||selected.length>=7){if(column===identity)return;continue;}
|
|
34
|
+
selected.push(column);used+=column.width+3;
|
|
35
|
+
}
|
|
36
|
+
if(selected.length<2&&!common.length)return;
|
|
37
|
+
let folded=false;
|
|
38
|
+
const detailRows=[],border=(left,middle,right)=>indent+left+selected.map(column=>'─'.repeat(column.width+2)).join(middle)+right;
|
|
39
|
+
const line=values=>indent+'│'+values.map((value,index)=>' '+value+' '.repeat(selected[index].width-visibleWidth(value)+1)).join('│')+'│';
|
|
40
|
+
const lines=[border('┌','┬','┐'),line(selected.map(column=>column.title)),border('├','┼','┤')];
|
|
41
|
+
entries.forEach(([key,item],index)=>{
|
|
42
|
+
const details=Object.create(null);let longKey=false;
|
|
43
|
+
const values=selected.map(column=>{
|
|
44
|
+
const value=column.values[index];
|
|
45
|
+
if(/[\r\n\t]/.test(value)||visibleWidth(value)>column.width){
|
|
46
|
+
if(column.key===null)longKey=true;else details[column.key]=item[column.key];
|
|
47
|
+
return '见下文';
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
});
|
|
51
|
+
for(const [name,value] of Object.entries(item))if(name!==duplicate&&!common.includes(name)&&!selected.some(column=>column.key===name)){
|
|
52
|
+
if(empty(value))folded=true;else details[name]=value;
|
|
53
|
+
}
|
|
54
|
+
lines.push(line(values));
|
|
55
|
+
if(longKey||Object.keys(details).length){
|
|
56
|
+
const identityText=list?`[${index+1}]`:`[${index+1}] ${clean(key)}`;
|
|
57
|
+
const body=render(details,indent+' ',rowSchema);
|
|
58
|
+
detailRows.push(`${indent}${identityText} · 补充字段${longKey?'(完整条目键)':''}${body.includes('\n')?'\n'+body:':'+body.trimStart()}`);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
lines.push(border('└','┴','┘'));
|
|
62
|
+
if(common.length)lines.push(`${indent}共有字段:${common.map(key=>`${label(key,rowSchema?.properties?.[key])}:${text(entries[0][1][key])}`).join(' · ')}`);
|
|
63
|
+
if(folded)lines.push(`${indent}空字段已折叠;/logs → j 查看原始数据。`);
|
|
64
|
+
if(detailRows.length)lines.push(`${indent}补充字段(对应表格行,保留全文):`,...detailRows);
|
|
65
|
+
return lines.join('\n');
|
|
66
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {readableValue,readableError,readableTimeline,runCategory,runCategoryNames,elapsedTime} from './run-presentation.js';
|
|
1
|
+
import {readableValue,readableError,readableTimeline,readableProcess,runCategory,runCategoryNames,elapsedTime,presentationCatalog} from './run-presentation.js';
|
|
2
2
|
import {runStatuses} from './run-viewer.js';
|
|
3
3
|
import {createStepSummaries} from '@mytools/fst-logs/steps';
|
|
4
4
|
import {previewRun} from '@mytools/fst-logs/preview';
|
|
5
|
+
import {outputReport,forwardedReport} from './report-presentation.js';
|
|
5
6
|
|
|
6
7
|
const feedRecord=(record,maxBytes)=>{
|
|
7
8
|
const {stepSummaries,stepSummaryBytes,stepsTruncated,...state}=record;
|
|
@@ -18,8 +19,15 @@ export class RunFeed{
|
|
|
18
19
|
}
|
|
19
20
|
track({app,runId,title,input,done}){
|
|
20
21
|
if(this.runs.has(runId))return;
|
|
21
|
-
const raw=app.getRun(runId),record=feedRecord(raw,this.maxBytes)
|
|
22
|
-
|
|
22
|
+
const raw=app.getRun(runId),record=feedRecord(raw,this.maxBytes);
|
|
23
|
+
let inputTruncated=false;
|
|
24
|
+
if(input===undefined){
|
|
25
|
+
const inputEvent=raw.events?.find(event=>event.type==='run.input');
|
|
26
|
+
input=raw.input!==undefined?raw.input:inputEvent?.input;
|
|
27
|
+
inputTruncated=raw.input!==undefined?raw.inputTruncated:inputEvent?.eventTruncated;
|
|
28
|
+
}
|
|
29
|
+
const bounded=previewRun({input},{maxValueBytes:Math.min(this.maxBytes,65536)});
|
|
30
|
+
const item={app,record,title,done,input:bounded.input,inputTruncated:bounded.inputTruncated||inputTruncated,count:0,ended:false,steps:createStepSummaries({maxEvents:this.maxEvents,maxMemoryBytes:this.maxBytes}),catalog:new Map(presentationCatalog(app).map(action=>[action.id,action]))};
|
|
23
31
|
for(const summary of raw.stepSummaries??[])item.steps.append(summary);
|
|
24
32
|
this.runs.set(runId,item);
|
|
25
33
|
this.entries.push({type:'start',item});
|
|
@@ -43,7 +51,7 @@ export class RunFeed{
|
|
|
43
51
|
};
|
|
44
52
|
if(done)void done.then(finish,()=>finish());
|
|
45
53
|
}
|
|
46
|
-
savedRun(item,maxValueBytes=8192){const {events,stepSummaries,stepSummaryBytes,...state}=item.record;return {...previewRun({...state,input:item.input},{maxValueBytes}),title:item.title};}
|
|
54
|
+
savedRun(item,maxValueBytes=8192){const {events,stepSummaries,stepSummaryBytes,...state}=item.record;return {...previewRun({...state,input:item.input,inputTruncated:item.inputTruncated||state.inputTruncated},{maxValueBytes}),title:item.title};}
|
|
47
55
|
elapsed(){
|
|
48
56
|
const running=[...this.runs.values()].some(item=>!item.ended);
|
|
49
57
|
if(running&&!this.elapsedTimer){this.elapsedTimer=setInterval(()=>this.refresh(),1000);this.elapsedTimer.unref?.();}
|
|
@@ -68,6 +76,10 @@ export class RunFeed{
|
|
|
68
76
|
item.count=record.eventCount??record.events.length;
|
|
69
77
|
}
|
|
70
78
|
const {record}=item;
|
|
79
|
+
if(item.input===undefined){
|
|
80
|
+
const inputEvent=events.find(event=>event.type==='run.input');
|
|
81
|
+
if(inputEvent){const bounded=previewRun({input:inputEvent.input},{maxValueBytes:Math.min(this.maxBytes,65536)});item.input=bounded.input;item.inputTruncated=bounded.inputTruncated||inputEvent.eventTruncated;}
|
|
82
|
+
}
|
|
71
83
|
for(const event of events)item.steps.append(event);
|
|
72
84
|
if(events.length){
|
|
73
85
|
const last=this.entries.at(-1);
|
|
@@ -121,7 +133,7 @@ export class RunFeed{
|
|
|
121
133
|
const record={...entry.record,events:[]};
|
|
122
134
|
if(record.status==='running'){record.status='unknown';record.reason='上次退出时执行中,未记录最终状态';}
|
|
123
135
|
const steps=createStepSummaries({maxEvents:this.maxEvents,maxMemoryBytes:this.maxBytes});for(const step of entry.steps??[])steps.append(step);
|
|
124
|
-
this.runs.set(record.id,{record,input:entry.input,title:entry.title,endedAt:entry.endedAt,ended:true,steps,catalog:new Map(catalog.map(action=>[action.id,action]))});
|
|
136
|
+
this.runs.set(record.id,{record,input:entry.input,inputTruncated:record.inputTruncated,title:entry.title,endedAt:entry.endedAt,ended:true,steps,catalog:new Map(catalog.map(action=>[action.id,action]))});
|
|
125
137
|
}else if(entry.type==='message'&&typeof entry.text==='string')this.entries.push({type:'message',text:entry.text});
|
|
126
138
|
else if(['start','events','result'].includes(entry.type)&&this.runs.has(entry.runId)){
|
|
127
139
|
const item=this.runs.get(entry.runId);this.entries.push({type:entry.type,item,...(entry.type==='events'?{events:entry.events??[]}:{})});
|
|
@@ -130,7 +142,7 @@ export class RunFeed{
|
|
|
130
142
|
}
|
|
131
143
|
this.trim();this.elapsed();this.refresh();
|
|
132
144
|
}
|
|
133
|
-
compactEvents(entry,seen){
|
|
145
|
+
compactEvents(entry,seen,width){
|
|
134
146
|
const {item}=entry,rows=[];
|
|
135
147
|
for(const event of entry.events){
|
|
136
148
|
const key=`${item.record.id}\0${event.callPath}`;
|
|
@@ -138,19 +150,29 @@ export class RunFeed{
|
|
|
138
150
|
seen.add(key);const latest=item.steps.get(event.callPath);
|
|
139
151
|
if(!latest)rows.push(`· ${event.action??event.stepId??event.callPath} · 摘要已省略 · ${event.callPath}`);
|
|
140
152
|
else{
|
|
141
|
-
const action=item.catalog.get(
|
|
153
|
+
const actionId=latest.action??event.action,action=item.catalog.get(actionId);
|
|
142
154
|
const status=latest.type.slice(5),symbols={started:'●',succeeded:'✓',failed:'✗',cancelled:'×',skipped:'–',stopped:'■'};
|
|
143
|
-
const name=action?.title??
|
|
144
|
-
rows.push(`${symbols[status]??'·'} ${name} · ${status==='started'?'执行中':runStatuses[status]??status}${latest.durationMs!==undefined?' · '+latest.durationMs+' ms':''}${action?.plugin?' · '+action.plugin:''} · ${latest.callPath}\n Ctrl+O
|
|
155
|
+
const name=action?.title??actionId??latest.stepId??latest.callPath;
|
|
156
|
+
rows.push(`${symbols[status]??'·'} ${name}${actionId&&name!==actionId?' · '+actionId:''} · ${status==='started'?'执行中':runStatuses[status]??status}${latest.durationMs!==undefined?' · '+latest.durationMs+' ms':''}${action?.plugin?' · '+action.plugin:''} · ${latest.callPath}\n Ctrl+O 展开原始输出与诊断 · /logs 选择步骤`);
|
|
145
157
|
}
|
|
146
158
|
}
|
|
147
|
-
if(event.type==='
|
|
159
|
+
if(event.type==='step.started'&&event.input!==undefined)rows.push(`输入 · ${event.callPath}\n${readableValue(event.input,' ',item.catalog.get(event.action)?.input,{compact:true,decodeStrings:false,width,truncated:event.eventTruncated})}`);
|
|
160
|
+
if(event.type==='log'&&event.kind==='process')rows.push(`执行指令 · ${event.callPath??item.record.workflowId}${typeof event.processId==='string'&&event.processId?' · #'+event.processId.slice(0,8):''}\n${readableProcess(event.data)}`);
|
|
161
|
+
if(event.type==='step.succeeded'&&!seen.has(`${key}\0report`)){
|
|
162
|
+
seen.add(`${key}\0report`);const report=outputReport(event.output);
|
|
163
|
+
// 从已有的有界事件读取正文;最终输出转发同一报告时展示一次即可。
|
|
164
|
+
if(report&&!forwardedReport(event.output,item.record.output))rows.push(readableValue(report,' ',undefined,{compact:true,width,truncated:event.eventTruncated}));
|
|
165
|
+
}
|
|
166
|
+
if(event.type==='log'&&event.audience==='user'&&!event.stream&&event.kind!=='process'){
|
|
167
|
+
const data=event.data!==undefined?'\n'+readableValue(event.data,'',undefined,{compact:true,width:Number.isFinite(width)?Math.max(1,width-2):width,truncated:event.eventTruncated}):'';
|
|
168
|
+
rows.push(`[${event.callPath??item.record.workflowId}] ${event.message??''}${data}`.replaceAll('\r\n','\n').replaceAll('\r','\n').split('\n').map(line=>' '+line).join('\n'));
|
|
169
|
+
}
|
|
148
170
|
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})}`);
|
|
149
171
|
if(event.type==='step.stopped'||event.type==='step.skipped')rows.push(`${event.callPath}:${event.reason??event.message??''}`);
|
|
150
172
|
}
|
|
151
173
|
return rows.length?`${item.title??item.record.workflowId} · ${item.record.id.slice(0,8)}\n${rows.join('\n')}`:'';
|
|
152
174
|
}
|
|
153
|
-
text(){const seen=new Set();return (this.dropped?'主面板仅保留近期摘要;超出预算的事件和步骤摘要已省略。完整日志可在 /logs 中按 p 分页阅读。\n\n':'')+this.entries.map(entry=>{
|
|
175
|
+
text(width){const seen=new Set();return (this.dropped?'主面板仅保留近期摘要;超出预算的事件和步骤摘要已省略。完整日志可在 /logs 中按 p 分页阅读。\n\n':'')+this.entries.map(entry=>{
|
|
154
176
|
if(entry.type==='message')return entry.text;
|
|
155
177
|
const {record,title,input}=entry.item,label=`${title??record.workflowId} · ${record.id.slice(0,8)}`,duration=elapsedTime({...record,...(entry.item.ended&&!record.finishedAt?{finishedAt:entry.item.endedAt}:{})},this.now());
|
|
156
178
|
if(entry.type==='start')return [
|
|
@@ -158,17 +180,20 @@ export class RunFeed{
|
|
|
158
180
|
`状态:${runStatuses[record.status]??record.status}`,
|
|
159
181
|
record.reason??'',
|
|
160
182
|
duration&&`耗时:${duration}`,
|
|
161
|
-
'Ctrl+O
|
|
162
|
-
|
|
163
|
-
|
|
183
|
+
'Ctrl+O 展开原始输出与诊断 · /logs 查看单步详情',
|
|
184
|
+
input!==undefined?'输入\n'+readableValue(input,' ',entry.item.catalog.get(record.action??record.workflowId)?.input,{compact:true,decodeStrings:false,width}):'',
|
|
185
|
+
entry.item.inputTruncated?'输入较大,主面板仅显示摘要。':'',
|
|
164
186
|
].filter(Boolean).join('\n');
|
|
165
|
-
if(entry.type==='events')return this.expanded?`${label}\n${readableTimeline(entry.events,{values:true,catalog:[...entry.item.catalog.values()]})}`:this.compactEvents(entry,seen);
|
|
187
|
+
if(entry.type==='events')return this.expanded?`${label}\n${readableTimeline(entry.events,{values:true,width,catalog:[...entry.item.catalog.values()],finalOutput:record.output})}`:this.compactEvents(entry,seen,width);
|
|
188
|
+
const report=outputReport(record.output);
|
|
189
|
+
const output=!this.expanded||report===record.output?undefined:report?Object.fromEntries(Object.entries(record.output).filter(([key])=>key!=='report')):record.output;
|
|
166
190
|
return [`结果 · ${label} · ${runStatuses[record.status]??record.status}`,
|
|
167
191
|
duration&&`耗时:${duration}`,
|
|
168
192
|
record.resultDir?`生成文件:${record.resultDir}`:'',
|
|
169
193
|
...(record.warnings??[]).map(warning=>`警告:${warning.message}`),
|
|
170
|
-
|
|
171
|
-
|
|
194
|
+
report?readableValue(report,' ',undefined,{compact:true,width,truncated:record.outputTruncated}):'',
|
|
195
|
+
output!==undefined&&(!report||Object.keys(output).length)?'原始输出(/logs → j 查看原始数据)\n'+readableValue(output,'',entry.item.catalog.get(record.action??record.workflowId)?.output,{compact:true,width,truncated:record.outputTruncated}):'',record.error&&(this.expanded||![...entry.item.steps.values()].some(step=>step.type==='step.failed'))?readableError(record.error,{details:this.expanded}):'',record.reason??'',
|
|
196
|
+
record.outputTruncated?'结果较大,主面板仅显示摘要;完整结果请在 /logs 中按 p 分页阅读。':'',
|
|
172
197
|
].filter(Boolean).join('\n');
|
|
173
198
|
}).filter(Boolean).join('\n\n');}
|
|
174
199
|
close(){this.closed=true;if(this.refreshTimer)clearTimeout(this.refreshTimer);this.refreshTimer=null;if(this.elapsedTimer)clearInterval(this.elapsedTimer);this.elapsedTimer=null;for(const unsubscribe of this.subscriptions.values())unsubscribe();this.subscriptions.clear();this.finishedListeners.clear();}
|
|
@@ -1,33 +1,60 @@
|
|
|
1
|
+
import {visibleWidth} from '@mariozechner/pi-tui';
|
|
2
|
+
import {resultTable} from './result-table.js';
|
|
3
|
+
import {renderReport,forwardedReport} from './report-presentation.js';
|
|
1
4
|
// 阅读层只处理展示;原始事件、输入输出与错误仍保留在运行记录中。
|
|
2
|
-
const fieldNames={stdout:'标准输出',stderr:'标准错误',stdin:'标准输入',exitCode:'退出码',path:'路径',file:'文件',directory:'文件目录',cwd:'执行目录',args:'参数',script:'原始脚本',executable:'程序',ok:'是否成功',success:'是否成功',plugin:'插件',durationMs:'耗时(毫秒)'};
|
|
5
|
+
const fieldNames={stdout:'标准输出',stderr:'标准错误',stdin:'标准输入',exitCode:'退出码',path:'路径',file:'文件',directory:'文件目录',cwd:'执行目录',args:'参数',script:'原始脚本',executable:'程序',ok:'是否成功',success:'是否成功',plugin:'插件',durationMs:'耗时(毫秒)',id:'标识',title:'名称',group:'分组',found:'已找到',version:'版本',source:'来源',error:'错误',note:'说明'};
|
|
3
6
|
const jsonTextLimit=65536;
|
|
7
|
+
// 斜杠命令与工作流动作共用阅读元信息,避免直接调用时丢失字段标题。
|
|
8
|
+
export function presentationCatalog(app){
|
|
9
|
+
return [...(app.catalog?.()??[]),...(app.listCommands?.()??[]).map(command=>({...command,id:`/${command.name}`}))];
|
|
10
|
+
}
|
|
4
11
|
function decodedValue(value){
|
|
5
12
|
if(typeof value!=='string'||value.length>jsonTextLimit||!/^\s*(?:[\[{"\d-]|true\b|false\b|null\b)/.test(value)||Buffer.byteLength(value)>jsonTextLimit)return value;
|
|
6
13
|
try{return JSON.parse(value);}catch{return value;}
|
|
7
14
|
}
|
|
8
|
-
|
|
15
|
+
const emptyField=value=>value==null||value===''||typeof value==='object'&&Object.keys(value).length===0;
|
|
16
|
+
function valueText(value,indent,schema,depth,seen,decodeStrings,compact,width,truncated){
|
|
9
17
|
if(depth>32)return `${indent}(结构较深,请按 j 查看原始数据)`;
|
|
10
18
|
if(value===undefined)return '暂无记录';
|
|
11
19
|
if(value===null)return '空值';
|
|
12
20
|
if(typeof value==='boolean')return value?'是':'否';
|
|
21
|
+
if(typeof value==='string'&&!decodeStrings)return JSON.stringify(value);
|
|
13
22
|
if(typeof value!=='object')return String(value);
|
|
14
23
|
if(seen.has(value))return `${indent}(循环引用)`;
|
|
24
|
+
if(decodeStrings){
|
|
25
|
+
const report=renderReport(value,{indent,truncated,render:(item,prefix,child,{raw=false,inset=0}={})=>valueText(item,prefix,child,depth+1,seen,!raw,raw?false:compact,raw?width:width-visibleWidth(indent)-inset,truncated)});
|
|
26
|
+
if(report!==undefined)return report;
|
|
27
|
+
}
|
|
15
28
|
seen.add(value);
|
|
16
29
|
try{
|
|
17
30
|
const list=Array.isArray(value),entries=list?value.map((v,i)=>[i,v]):Object.entries(value);
|
|
18
31
|
if(!entries.length)return list?'(空列表)':'(空对象)';
|
|
19
|
-
|
|
20
|
-
|
|
32
|
+
const table=compact&&decodeStrings?resultTable(value,{schema,width,indent,label:(key,child)=>(typeof child?.title==='string'&&child.title.trim())||fieldNames[key]||key,render:(item,prefix,child)=>valueText(item,prefix,child,depth+1,seen,decodeStrings,compact,undefined,truncated)}):undefined;
|
|
33
|
+
if(table!==undefined)return table;
|
|
34
|
+
const rows=[];let inline=[];
|
|
35
|
+
const flush=()=>{if(inline.length){rows.push(indent+inline.join(' · '));inline=[];}};
|
|
36
|
+
for(let [key,item] of entries){
|
|
37
|
+
// 输入采用原值阅读,显式空值也可能有意义;仅结果紧凑模式折叠空字段。
|
|
38
|
+
if(compact&&decodeStrings&&!list&&emptyField(item))continue;
|
|
39
|
+
const child=list?(schema?.prefixItems?.[key]??(Array.isArray(schema?.items)?schema.items[key]:schema?.items)):(schema?.properties?.[key]??(typeof schema?.additionalProperties==='object'?schema.additionalProperties:undefined));
|
|
21
40
|
const title=typeof child?.title==='string'&&child.title.trim()?child.title.trim():undefined;
|
|
22
41
|
const label=list?`[${key+1}]`:title??fieldNames[key]??key;
|
|
23
|
-
item=decodedValue(item);const nested=item!==null&&typeof item==='object';
|
|
24
|
-
const text=valueText(item,nested?indent+' ':'',child,depth+1,seen);
|
|
25
|
-
|
|
26
|
-
|
|
42
|
+
if(decodeStrings)item=decodedValue(item);const nested=item!==null&&typeof item==='object';
|
|
43
|
+
const text=valueText(item,nested?indent+' ':'',child,depth+1,seen,decodeStrings,compact,width,truncated);
|
|
44
|
+
const scalar=`${label}:${text}`;
|
|
45
|
+
if(compact&&!nested&&!/[\r\n]/.test(scalar)&&scalar.length<=100){
|
|
46
|
+
if(inline.join(' · ').length+scalar.length>120)flush();
|
|
47
|
+
inline.push(scalar);
|
|
48
|
+
}else{
|
|
49
|
+
flush();
|
|
50
|
+
rows.push(`${indent}${label}:${nested?(compact&&!text.includes('\n')?text.trimStart():'\n'+text):text.replaceAll('\r\n','\n').replaceAll('\r','\n').replaceAll('\n','\n'+indent+' ')}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
flush();return rows.join('\n')||(compact?'(空字段已折叠)':'');
|
|
27
54
|
}finally{seen.delete(value);}
|
|
28
55
|
}
|
|
29
|
-
export function readableValue(value,indent='',schema){
|
|
30
|
-
return valueText(decodedValue(value),indent,schema,0,new Set());
|
|
56
|
+
export function readableValue(value,indent='',schema,{decodeStrings=true,compact=false,width,truncated=false}={}){
|
|
57
|
+
return valueText(decodeStrings?decodedValue(value):value,indent,schema,0,new Set(),decodeStrings,compact,width,truncated);
|
|
31
58
|
}
|
|
32
59
|
export function readableError(error,{details=false}={}){
|
|
33
60
|
if(!error)return '';
|
|
@@ -47,7 +74,7 @@ export function readableError(error,{details=false}={}){
|
|
|
47
74
|
const eventNames={'step.started':'步骤开始','step.succeeded':'步骤成功','step.failed':'步骤失败','step.cancelled':'步骤取消','step.skipped':'步骤跳过','step.stopped':'步骤停止','run.started':'运行开始','run.succeeded':'运行成功','run.failed':'运行失败','run.cancelled':'运行取消','run.stopped':'提前结束'};
|
|
48
75
|
const time=value=>{const date=new Date(value);return Number.isFinite(+date)?date.toLocaleTimeString('zh-CN',{hour12:false}):'--:--:--';};
|
|
49
76
|
const actionFrom=(catalog,id)=>catalog instanceof Map?catalog.get(id):catalog?.find(action=>action.id===id);
|
|
50
|
-
function
|
|
77
|
+
export function readableProcess(data){
|
|
51
78
|
if(!data||typeof data!=='object')return readableValue(data);
|
|
52
79
|
const rows=[];
|
|
53
80
|
if(data.file!==undefined||data.executable!==undefined)rows.push(`程序:${data.file??data.executable}`);
|
|
@@ -56,11 +83,13 @@ function processDetails(data){
|
|
|
56
83
|
if(data.script!==undefined)rows.push(`原始脚本:\n${data.script}`);
|
|
57
84
|
return rows.join('\n')||readableValue(data);
|
|
58
85
|
}
|
|
59
|
-
export function readableTimeline(events,{callPath,values=false,catalog=[]}={}){
|
|
86
|
+
export function readableTimeline(events,{callPath,values=false,catalog=[],width,finalOutput}={}){
|
|
60
87
|
const actions=new Map();for(const event of events??[])if(event.callPath&&event.action)actions.set(event.callPath,event.action);
|
|
61
88
|
const rows=[];let pending;
|
|
89
|
+
const identity=event=>`${event.callPath??'运行'}${typeof event.processId==='string'&&event.processId?' · #'+event.processId.slice(0,8):''}${event.stream?' · '+event.stream:''}`;
|
|
90
|
+
const contentWidth=event=>Number.isFinite(width)?Math.max(1,width-visibleWidth(`00:00:00 [${identity(event)}] `)):undefined;
|
|
62
91
|
const push=(event,message)=>{
|
|
63
|
-
const prefix=`${time(event.time)} [${event
|
|
92
|
+
const prefix=`${time(event.time)} [${identity(event)}]`;
|
|
64
93
|
const lines=String(message).replaceAll('\r\n','\n').replaceAll('\r','\n').split('\n');
|
|
65
94
|
if(lines.at(-1)==='')lines.pop();
|
|
66
95
|
for(const line of lines)rows.push(`${prefix} ${line}`);
|
|
@@ -69,7 +98,7 @@ export function readableTimeline(events,{callPath,values=false,catalog=[]}={}){
|
|
|
69
98
|
for(const event of events??[]){
|
|
70
99
|
if(callPath&&event.callPath!==callPath)continue;
|
|
71
100
|
if(event.type==='log'&&event.stream){
|
|
72
|
-
const key=`${event.callPath}\0${event.stream}`;
|
|
101
|
+
const key=`${event.callPath}\0${event.processId??''}\0${event.stream}`;
|
|
73
102
|
if(pending?.key!==key){flush();pending={key,event,text:''};}
|
|
74
103
|
pending.text+=event.message??'';
|
|
75
104
|
const end=pending.text.lastIndexOf('\n');
|
|
@@ -77,7 +106,7 @@ export function readableTimeline(events,{callPath,values=false,catalog=[]}={}){
|
|
|
77
106
|
continue;
|
|
78
107
|
}
|
|
79
108
|
if(event.type==='log'){
|
|
80
|
-
flush();const data=event.kind==='process'?
|
|
109
|
+
flush();const data=event.kind==='process'?readableProcess(event.data):readableValue(event.data,'',undefined,{compact:true,width:contentWidth(event),truncated:event.eventTruncated});
|
|
81
110
|
push(event,`${event.message??''}${event.data!==undefined?'\n'+data:''}`);continue;
|
|
82
111
|
}
|
|
83
112
|
if(eventNames[event.type]){
|
|
@@ -85,8 +114,13 @@ export function readableTimeline(events,{callPath,values=false,catalog=[]}={}){
|
|
|
85
114
|
flush();push(event,`${eventNames[event.type]}${event.action?' · '+event.action:''}${event.durationMs!==undefined?` · ${event.durationMs} ms`:''}${detail?'\n'+detail:''}`);
|
|
86
115
|
if(values&&event.type.startsWith('step.')){
|
|
87
116
|
const action=actionFrom(catalog,event.action??actions.get(event.callPath));
|
|
88
|
-
if(event.input!==undefined
|
|
89
|
-
if(event.output!==undefined)
|
|
117
|
+
if(event.input!==undefined)push(event,'输入\n'+readableValue(event.input,'',action?.input,{decodeStrings:false,compact:true,width:contentWidth(event)}));
|
|
118
|
+
if(event.output!==undefined){
|
|
119
|
+
const forwarded=forwardedReport(event.output,finalOutput);
|
|
120
|
+
const output=forwarded?(forwarded===event.output?undefined:Object.fromEntries(Object.entries(event.output).filter(([key])=>key!=='report'))):event.output;
|
|
121
|
+
if(forwarded)push(event,'报告已展示在本次运行的最终输出。');
|
|
122
|
+
if(output!==undefined&&(!forwarded||Object.keys(output).length))push(event,'输出(紧凑显示)\n'+readableValue(output,'',action?.output,{compact:true,width:contentWidth(event),truncated:event.eventTruncated}));
|
|
123
|
+
}
|
|
90
124
|
}
|
|
91
125
|
}
|
|
92
126
|
}
|