@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.
Files changed (104) hide show
  1. package/README.md +3 -3
  2. package/manual/README.md +3 -3
  3. package/manual/docs/modules.md +2 -0
  4. package/manual/docs/plans/2026-10-13-effective-inputs-and-tables.md +39 -0
  5. package/manual/docs/plans/2026-10-13-plugin-defaults.md +45 -0
  6. package/manual/docs/plans/2026-10-14-issue7-recovery.md +33 -0
  7. package/manual/docs/plans/2026-10-14-issue8-10-workflows.md +26 -0
  8. package/manual/docs/plans/2026-10-14-issue9-completion.md +47 -0
  9. package/manual/docs/plans/2026-10-14-managed-execution.md +58 -0
  10. package/manual/docs/plans/2026-10-14-plugin-discovery-history.md +41 -0
  11. package/manual/docs/plans/2026-10-14-structured-reports.md +77 -0
  12. package/manual/docs/plans/2026-10-14-unreleased-review.md +47 -0
  13. package/manual/docs/plugin-development.md +161 -8
  14. package/manual/docs/releases.md +25 -6
  15. package/manual/docs/superpowers/plans/2026-10-13-tui-readable-sessions.md +2 -2
  16. package/manual/docs/superpowers/reviews/2026-10-13-tui-sessions-review.md +2 -0
  17. package/manual/docs/superpowers/reviews/2026-10-14-0.6.0-release-review.md +19 -0
  18. package/manual/docs/verification.md +17 -0
  19. package/manual/packages/core/README.md +6 -0
  20. package/manual/packages/core/src/application.js +15 -9
  21. package/manual/packages/core/src/plugin-catalog.js +20 -2
  22. package/manual/packages/core/src/plugin-loading.js +24 -0
  23. package/manual/packages/core/src/version.js +1 -1
  24. package/manual/packages/fst-cli/README.md +5 -1
  25. package/manual/packages/fst-cli/src/terminal/command-menu.js +3 -2
  26. package/manual/packages/fst-cli/src/terminal/file-autocomplete.js +106 -0
  27. package/manual/packages/fst-cli/src/terminal/log-viewer.js +4 -3
  28. package/manual/packages/fst-cli/src/terminal/navigation.js +23 -1
  29. package/manual/packages/fst-cli/src/terminal/presentation.js +2 -1
  30. package/manual/packages/fst-cli/src/terminal/report-presentation.js +38 -0
  31. package/manual/packages/fst-cli/src/terminal/result-table.js +66 -0
  32. package/manual/packages/fst-cli/src/terminal/run-feed.js +42 -17
  33. package/manual/packages/fst-cli/src/terminal/run-presentation.js +51 -17
  34. package/manual/packages/fst-cli/src/terminal/run-viewer.js +15 -12
  35. package/manual/packages/fst-cli/src/terminal/schema.js +114 -4
  36. package/manual/packages/fst-cli/src/terminal/session-history.js +2 -2
  37. package/manual/packages/fst-cli/src/terminal/session.js +40 -15
  38. package/manual/packages/fst-cli/src/terminal/view.js +80 -68
  39. package/manual/packages/fst-cli/src/terminal/workflow-menu.js +3 -1
  40. package/manual/packages/fst-devtools/README.md +2 -0
  41. package/manual/packages/fst-devtools/src/plugin-quality.js +4 -1
  42. package/manual/packages/fst-devtools/src/plugin-tools.js +1 -0
  43. package/manual/packages/fst-devtools/src/process-usage.js +62 -0
  44. package/manual/packages/fst-platform/README.md +5 -0
  45. package/manual/packages/fst-platform/package.json +1 -0
  46. package/manual/packages/fst-platform/src/managed-process.js +36 -0
  47. package/manual/packages/fst-platform/src/process.js +7 -2
  48. package/manual/packages/fst-plugin/README.md +4 -0
  49. package/manual/packages/fst-plugin/src/contracts.js +3 -0
  50. package/manual/packages/fst-plugin/src/host.js +38 -23
  51. package/manual/packages/fst-plugin/src/runtime.js +14 -8
  52. package/manual/packages/fst-workflow/src/engine.js +1 -1
  53. package/manual/packages/fst-workspace/README.md +4 -0
  54. package/manual/packages/fst-workspace/src/plugin-registry.js +1 -1
  55. package/manual/packages/fst-workspace/src/plugins.js +5 -3
  56. package/manual/packages/fst-workspace/src/workflows.js +27 -9
  57. package/manual/packages/sdk/README.md +10 -0
  58. package/manual/packages/sdk/inputs.js +53 -0
  59. package/manual/packages/sdk/package.json +2 -0
  60. package/manual/packages/sdk/reports.js +53 -0
  61. package/node_modules/@mytools/core/README.md +6 -0
  62. package/node_modules/@mytools/core/src/application.js +15 -9
  63. package/node_modules/@mytools/core/src/plugin-catalog.js +20 -2
  64. package/node_modules/@mytools/core/src/plugin-loading.js +24 -0
  65. package/node_modules/@mytools/core/src/version.js +1 -1
  66. package/node_modules/@mytools/fst-cli/README.md +5 -1
  67. package/node_modules/@mytools/fst-cli/src/terminal/command-menu.js +3 -2
  68. package/node_modules/@mytools/fst-cli/src/terminal/file-autocomplete.js +106 -0
  69. package/node_modules/@mytools/fst-cli/src/terminal/log-viewer.js +4 -3
  70. package/node_modules/@mytools/fst-cli/src/terminal/navigation.js +23 -1
  71. package/node_modules/@mytools/fst-cli/src/terminal/presentation.js +2 -1
  72. package/node_modules/@mytools/fst-cli/src/terminal/report-presentation.js +38 -0
  73. package/node_modules/@mytools/fst-cli/src/terminal/result-table.js +66 -0
  74. package/node_modules/@mytools/fst-cli/src/terminal/run-feed.js +42 -17
  75. package/node_modules/@mytools/fst-cli/src/terminal/run-presentation.js +51 -17
  76. package/node_modules/@mytools/fst-cli/src/terminal/run-viewer.js +15 -12
  77. package/node_modules/@mytools/fst-cli/src/terminal/schema.js +114 -4
  78. package/node_modules/@mytools/fst-cli/src/terminal/session-history.js +2 -2
  79. package/node_modules/@mytools/fst-cli/src/terminal/session.js +40 -15
  80. package/node_modules/@mytools/fst-cli/src/terminal/view.js +80 -68
  81. package/node_modules/@mytools/fst-cli/src/terminal/workflow-menu.js +3 -1
  82. package/node_modules/@mytools/fst-devtools/README.md +2 -0
  83. package/node_modules/@mytools/fst-devtools/src/plugin-quality.js +4 -1
  84. package/node_modules/@mytools/fst-devtools/src/plugin-tools.js +1 -0
  85. package/node_modules/@mytools/fst-devtools/src/process-usage.js +62 -0
  86. package/node_modules/@mytools/fst-platform/README.md +5 -0
  87. package/node_modules/@mytools/fst-platform/package.json +1 -0
  88. package/node_modules/@mytools/fst-platform/src/managed-process.js +36 -0
  89. package/node_modules/@mytools/fst-platform/src/process.js +7 -2
  90. package/node_modules/@mytools/fst-plugin/README.md +4 -0
  91. package/node_modules/@mytools/fst-plugin/src/contracts.js +3 -0
  92. package/node_modules/@mytools/fst-plugin/src/host.js +38 -23
  93. package/node_modules/@mytools/fst-plugin/src/runtime.js +14 -8
  94. package/node_modules/@mytools/fst-workflow/src/engine.js +1 -1
  95. package/node_modules/@mytools/fst-workspace/README.md +4 -0
  96. package/node_modules/@mytools/fst-workspace/src/plugin-registry.js +1 -1
  97. package/node_modules/@mytools/fst-workspace/src/plugins.js +5 -3
  98. package/node_modules/@mytools/fst-workspace/src/workflows.js +27 -9
  99. package/node_modules/@mytools/sdk/README.md +10 -0
  100. package/node_modules/@mytools/sdk/inputs.js +53 -0
  101. package/node_modules/@mytools/sdk/package.json +2 -0
  102. package/node_modules/@mytools/sdk/reports.js +53 -0
  103. package/node_modules/@mytools/web/public/THIRD_PARTY_NOTICES.txt +1032 -1032
  104. package/package.json +1 -1
@@ -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),bounded=previewRun({input},{maxValueBytes:Math.min(this.maxBytes,65536)});
22
- const item={app,record,title,done,input:bounded.input,inputTruncated:bounded.inputTruncated,count:0,ended:false,steps:createStepSummaries({maxEvents:this.maxEvents,maxMemoryBytes:this.maxBytes}),catalog:new Map((app.catalog?.()??[]).map(action=>[action.id,action]))};
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(latest.action);
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??latest.action??latest.stepId??latest.callPath;
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 查看指令、输入与结果 · /logs 选择步骤`);
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==='log'&&event.audience==='user')rows.push(`[${event.callPath??item.record.workflowId}] ${event.message??''}${event.data!==undefined?'\n'+readableValue(event.data):''}`);
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 查看指令、文件与结果 · /logs 查看单步详情',
162
- this.expanded&&input&&Object.keys(input).length?'输入\n'+readableValue(input):'',
163
- this.expanded&&entry.item.inputTruncated?'输入较大,主面板仅显示摘要。':'',
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
- 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??'',
171
- this.expanded&&record.outputTruncated?'结果较大,主面板仅显示摘要;完整结果请在 /logs 中按 p 分页阅读。':'',
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
- function valueText(value,indent,schema,depth,seen){
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
- return entries.map(([key,item])=>{
20
- const child=list?(schema?.prefixItems?.[key]??(Array.isArray(schema?.items)?schema.items[key]:schema?.items)):schema?.properties?.[key];
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
- return `${indent}${label}:${nested?'\n'+text:text.replaceAll('\r\n','\n').replaceAll('\r','\n').replaceAll('\n','\n'+indent+' ')}`;
26
- }).join('\n');
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 processDetails(data){
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.callPath??'运行'}${event.stream?' · '+event.stream:''}]`;
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'?processDetails(event.data):readableValue(event.data);
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&&Object.keys(event.input??{}).length)push(event,'输入\n'+readableValue(event.input,'',action?.input));
89
- if(event.output!==undefined)push(event,'输出\n'+readableValue(event.output,'',action?.output));
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
  }
@@ -1,10 +1,9 @@
1
- import { stripVTControlCharacters } from 'node:util';
2
1
  import { Text, matchesKey, truncateToWidth } from '@mariozechner/pi-tui';
3
- import {palette} from './theme.js';
2
+ import {palette,cleanText as safe} from './theme.js';
4
3
  import {readableValue,readableError,readableTimeline,elapsedTime} from './run-presentation.js';
4
+ import {outputReport} from './report-presentation.js';
5
5
 
6
6
  export const runStatuses={pending:'待执行',unknown:'摘要已省略',running:'运行中',succeeded:'成功',failed:'失败',cancelled:'已取消',skipped:'已跳过',stopped:'已停止'};
7
- const safe=value=>stripVTControlCharacters(String(value)).replace(/[\x00-\x08\x0b-\x1f\x7f]/g,'');
8
7
  export const mouseWheel=data=>{const match=/^\x1b\[<(\d+);\d+;\d+[Mm]$/.exec(data);return match&&(Number(match[1])&64)?(Number(match[1])&1?1:-1):0;};
9
8
  export const mouseOn='\x1b[?1000h\x1b[?1006h';
10
9
  export const mouseOff='\x1b[?1006l\x1b[?1000l';
@@ -39,15 +38,18 @@ export class RunViewer{
39
38
  this.requestRender();
40
39
  }
41
40
  outputSchema(node){return this.catalog.find(action=>action.id===(node?.action??this.record.action??this.record.workflowId))?.output;}
42
- getRenderedOutput(){
41
+ getRenderedOutput(width=this.terminal.columns,{markdown=false}={}){
43
42
  const node=this.view===0?undefined:this.nodes[this.selected],output=node?node.output:this.record.output,schema=this.outputSchema(node);
44
- if(output&&typeof output==='object'){
43
+ if(output&&typeof output==='object'&&!outputReport(output)){
45
44
  if(typeof output.markdown==='string')return output.markdown;
46
45
  for(const [key,value] of Object.entries(output))if(typeof value==='string'&&(schema?.properties?.[key]?.format==='markdown'||schema?.properties?.[key]?.contentMediaType==='text/markdown'))return value;
47
46
  }
48
- return readableValue(output,'',schema);
47
+ const text=readableValue(output,'',schema,{compact:true,width:markdown?Math.max(1,width-2):width,truncated:node?node.eventTruncated:this.record.outputTruncated});
48
+ if(!markdown)return text;
49
+ const fence='`'.repeat(Math.max(3,...[...text.matchAll(/`+/g)].map(match=>match[0].length+1)));
50
+ return `${fence}text\n${text}\n${fence}`;
49
51
  }
50
- content(){
52
+ content(width=this.terminal.columns){
51
53
  const node=this.nodes[this.selected],status=runStatuses[this.record.status]??this.record.status;
52
54
  const elapsed=elapsedTime(this.record,this.now());
53
55
  if(this.view===0)return [
@@ -55,7 +57,7 @@ export class RunViewer{
55
57
  elapsed!==undefined?`耗时:${elapsed}`:'',
56
58
  this.record.error?readableError(this.record.error):'',
57
59
  ...(this.record.warnings??[]).map(warning=>`警告:${warning.message}`),
58
- this.record.output!==undefined?'\n最终输出\n'+readableValue(this.record.output,'',this.outputSchema()):'',
60
+ this.record.output!==undefined?'\n最终输出(紧凑显示)\n'+readableValue(this.record.output,'',this.outputSchema(),{compact:true,width,truncated:this.record.outputTruncated}):'',
59
61
  this.record.resultDir?'\n生成文件:'+this.record.resultDir:'',
60
62
  '\n步骤 · Enter 查看详情',
61
63
  ...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:''}`),
@@ -63,19 +65,20 @@ export class RunViewer{
63
65
  ].filter(Boolean).join('\n');
64
66
  if(this.view===1){
65
67
  const action=this.catalog.find(action=>action.id===(node?.action??this.record.action??this.record.workflowId));
66
- const format=(value,schema)=>this.raw?JSON.stringify(value??null,null,2):readableValue(value,'',schema);
68
+ const format=(value,schema,{compact=false,decodeStrings=true}={})=>this.raw?JSON.stringify(value??null,null,2):readableValue(value,'',schema,{compact,decodeStrings,width,truncated:node?node.eventTruncated:compact?this.record.outputTruncated:this.record.inputTruncated});
67
69
  const error=node?node.error:this.record.error;
70
+ const input=node?node.input:this.record.input!==undefined?this.record.input:this.record.events?.find(event=>event.type==='run.input')?.input;
68
71
  return [`步骤:${node?.title??node?.callPath??'整个运行'}`,node?.title&&node.callPath?'调用路径:'+node.callPath:'',node?.action?'能力:'+node.action:'',error?'\n错误\n'+(this.raw?format(error):readableError(error)):'',
69
- '\n输入\n'+format(node?node.input:this.record.input??this.record.events?.find(event=>event.type==='run.input')?.input,action?.input), '\n输出\n'+format(node?node.output:this.record.output,action?.output),
72
+ '\n输入\n'+format(input,action?.input,{decodeStrings:false}), '\n输出\n'+format(node?node.output:this.record.output,action?.output,{compact:true}),
70
73
  !error&&(node?.reason||node?.message)?'\n说明:'+(node.reason??node.message):'',this.raw?'\n原始 JSON · 包含完整错误详情与调用栈':'\nj 查看原始 JSON 与完整诊断',
71
74
  this.onReadOutput?'m 格式化阅读输出':'',
72
75
  ].filter(Boolean).join('\n');
73
76
  }
74
- return (this.record.eventsTruncated?'当前为近期日志摘要,按 p 分页查看完整落盘日志。\n\n':'')+readableTimeline(this.record.events,{callPath:this.allLogs?undefined:node?.callPath,catalog:this.catalog});
77
+ return (this.record.eventsTruncated?'当前为近期日志摘要,按 p 分页查看完整落盘日志。\n\n':'')+readableTimeline(this.record.events,{callPath:this.allLogs?undefined:node?.callPath,catalog:this.catalog,width});
75
78
  }
76
79
  layout(width){
77
80
  const height=Math.max(1,this.terminal.rows),pageSize=Math.max(1,height-4);
78
- const lines=new Text(safe(this.content()),0,0).render(Math.max(1,width));
81
+ const lines=new Text(safe(this.content(width)),0,0).render(Math.max(1,width));
79
82
  const maxOffset=Math.max(0,lines.length-pageSize);
80
83
  this.offset=this.follow&&this.view===2?maxOffset:Math.max(0,Math.min(this.offset,maxOffset));
81
84
  return {height,pageSize,lines,maxOffset};
@@ -1,18 +1,128 @@
1
- import { validate } from '@mytools/sdk/schema';
1
+ import { validate, assertSchema } from '@mytools/sdk/schema';
2
+ import {isDeepStrictEqual} from 'node:util';
2
3
  import { fileReference } from './model.js';
4
+ import {readableValue} from './run-presentation.js';
5
+ import {cleanText,fitLine} from './theme.js';
6
+ import {visibleWidth} from '@mariozechner/pi-tui';
7
+ import {hasInputRules,resolveInput} from '@mytools/sdk/inputs';
3
8
 
4
9
  export const needsJsonEditor=schema=>['oneOf','anyOf','allOf','if','$ref','patternProperties'].some(key=>schema?.[key]!==undefined);
5
10
 
11
+ // 与执行时共用校验器;即使尚缺必填字段,也保留已能确定的默认值供编辑。
12
+ function editableDefaults(schema,value){
13
+ const copy=structuredClone(value);
14
+ if(schema!=null)assertSchema(schema,'参数')(copy);
15
+ return copy;
16
+ }
17
+
18
+ export function inputEntries(schema,value,fields={}){
19
+ if(schema?.type!=='object'||!schema.properties||needsJsonEditor(schema))return [readableValue(value,'',schema,{decodeStrings:false})];
20
+ const names=new Set([...Object.keys(schema.properties),...Object.keys(value??{})]);
21
+ return [...names].map(name=>{
22
+ const field=fields[name],title=schema.properties[name]?.title??name;
23
+ if(value?.[name]===undefined)return `${title}:${schema.required?.includes(name)?'未填写(必填)':field?.unsetLabel??'未提供(插件未声明缺省行为)'}`;
24
+ const text=readableValue({[name]:value[name]},'',schema,{decodeStrings:false});
25
+ if(!field)return text;
26
+ const source=field?.source==='config'?'继承插件配置:':field?.source==='default'?'默认值:':field?.source==='input'?'本次输入:':'';
27
+ return `${title}:${source}${readableValue(value[name],'',schema.properties[name],{decodeStrings:false})}${field?.unit?' '+field.unit:''}${field?.path?'('+field.path+')':''}`;
28
+ });
29
+ }
30
+
31
+ function parameterPreview(entries,label,screenWidth=80,screenHeight=24){
32
+ const limit=Math.max(1,Math.min(4,screenHeight-10)),width=Math.max(8,screenWidth-6);
33
+ const values=entries.map(entry=>cleanText(entry).replace(/[\r\n\t]/g,' '));
34
+ let columns=width>=110?3:width>=60?2:1;
35
+ // 来源说明较长时减少列数,优先显示完整含义,仍保持预览行数有界。
36
+ while(columns>1&&values.some(value=>visibleWidth(value)>Math.floor((width-(columns-1)*3)/columns)))columns--;
37
+ const cellWidth=Math.floor((width-(columns-1)*3)/columns);
38
+ const cells=values.slice(0,limit*columns).map(entry=>fitLine(entry,cellWidth));
39
+ const rows=[];
40
+ for(let index=0;index<cells.length;index+=columns){
41
+ rows.push(cells.slice(index,index+columns).map((cell,i)=>i<columns-1?cell+' '.repeat(cellWidth-visibleWidth(cell)):cell).join(' '));
42
+ }
43
+ const preview=rows.join('\n')||'无需参数';
44
+ return {text:`${fitLine(cleanText(label??'插件能力').replace(/\s+/g,' '),width)}\n当前参数(含默认值):\n${preview}`,truncated:values.length>cells.length||values.some(value=>visibleWidth(value)>cellWidth)||entries.some(entry=>/[\r\n\t]/.test(entry))};
45
+ }
46
+
47
+ export async function commandInput(ui,schema,value,{baseDir,label,preview}={}){
48
+ const rules=hasInputRules(schema),project=preview??(value=>resolveInput(schema,value,{}, {check:false}));
49
+ let current=rules?structuredClone(value):editableDefaults(schema,value);
50
+ while(true){
51
+ const resolved=rules?project(current):{value:current};
52
+ const entries=inputEntries(schema,resolved.value,resolved.fields),summary=entries.join('\n')||'无需参数';
53
+ const layout=()=>parameterPreview(entries,label,ui.terminal?.columns,ui.terminal?.rows);
54
+ const message=(width,height)=>parameterPreview(entries,label,width,height).text;
55
+ const choice=await ui.select(ui.terminal?message:message(),[
56
+ {value:'run',label:'执行'},
57
+ {value:'edit',label:'配置参数'},
58
+ ...(ui.viewMarkdown?[{value:'view',label:'展开未显示完整的参数',description:`共 ${entries.length} 项`,hidden:()=>!layout().truncated}]:[]),
59
+ {value:'cancel',label:'返回'},
60
+ ]);
61
+ if(choice==='cancel')return {cancelled:true};
62
+ if(choice==='view'){
63
+ // 保留换行和引号,参数里的 Markdown 符号不能改变显示含义。
64
+ let fenceLength=3;for(const match of summary.matchAll(/`+/g))fenceLength=Math.max(fenceLength,match[0].length+1);
65
+ const fence='`'.repeat(fenceLength);
66
+ await ui.viewMarkdown('当前参数(含默认值)',`${fence}text\n${summary}\n${fence}`);continue;
67
+ }
68
+ if(choice==='edit')current=await editInput(ui,schema,current,{baseDir,label,preview:project,required:true});
69
+ else if(choice==='run'){
70
+ try{
71
+ if(rules){validate(schema,project(current).value,label??'插件参数',{useDefaults:false});return {value:structuredClone(current)};}
72
+ return {value:validate(schema,current,label??'插件参数')};
73
+ }
74
+ catch(error){if(error.code!=='VALIDATION')throw error;ui.show(`暂不能执行,请配置参数:${error.message}`);}
75
+ }
76
+ }
77
+ }
78
+
79
+ // 能力调用和快捷名称保存使用同一编辑入口,省略值不因打开表单而固化。
80
+ export async function editInput(ui,schema,value,options={}){
81
+ if(!hasInputRules(schema))return editValue(ui,schema,value,options);
82
+ return editInputRules(ui,schema,value,{...options,preview:options.preview??(value=>resolveInput(schema,value,{}, {check:false}))});
83
+ }
84
+
85
+ async function editInputRules(ui,schema,value,{baseDir,label,preview}){
86
+ const current=structuredClone(value);
87
+ while(true){
88
+ const resolved=preview(current),entries=inputEntries(schema,resolved.value,resolved.fields);
89
+ const names=Object.keys(schema.properties);
90
+ const choice=await ui.select(`${label??'参数'} · 选择字段`,[
91
+ ...names.map((name,index)=>({value:`field:${name}`,label:schema.properties[name].title??name,description:entries[index]})),
92
+ {value:'__done',label:'完成填写'},
93
+ ]);
94
+ if(choice==='__done'){
95
+ try{validate(schema,preview(current).value,label??'参数',{useDefaults:false});return current;}
96
+ catch(error){ui.show(error.message);continue;}
97
+ }
98
+ const name=choice.slice(6),field=schema.properties[name],rule=field['x-fst'];
99
+ if(rule){
100
+ const action=await ui.select(field.title??name,[{value:'set',label:'设置本次输入'},{value:'inherit',label:rule.fromConfig?'恢复继承插件配置':'恢复省略参数'},{value:'back',label:'返回'}]);
101
+ if(action==='back')continue;
102
+ if(action==='inherit'){delete current[name];continue;}
103
+ }
104
+ const next=await editValue(ui,field,resolved.value[name],{baseDir,label:field.title??name,required:schema.required?.includes(name)});
105
+ if(next===undefined)delete current[name];else Object.defineProperty(current,name,{value:next,enumerable:true,writable:true,configurable:true});
106
+ }
107
+ }
108
+
6
109
  // 值只在整个编辑过程完成后返回,Escape 不会留下半个修改。
7
110
  export async function editValue(ui,schema={},value,{baseDir,required=false,label=schema.title??'值'}={}){
8
111
  if(value===undefined&&schema.default!==undefined)value=structuredClone(schema.default);
9
112
  if(schema.enum&&!needsJsonEditor(schema)){
10
- const index=await ui.select(label,schema.enum.map((v,i)=>({value:String(i),label:JSON.stringify(v)})));
113
+ const choices=schema.enum.map((v,i)=>({value:String(i),label:JSON.stringify(v)}));
114
+ const selected=schema.enum.findIndex(item=>isDeepStrictEqual(item,value));
115
+ if(selected>0)choices.unshift(...choices.splice(selected,1));
116
+ const index=await ui.select(label,choices);
11
117
  return schema.enum[Number(index)];
12
118
  }
13
- if(schema.type==='boolean'&&!needsJsonEditor(schema))return (await ui.select(label,[{value:'true',label:'是 / true'},{value:'false',label:'否 / false'}]))==='true';
119
+ if(schema.type==='boolean'&&!needsJsonEditor(schema)){
120
+ const choices=[{value:'true',label:'是 / true'},{value:'false',label:'否 / false'}];
121
+ if(value===false)choices.reverse();
122
+ return (await ui.select(label,choices))==='true';
123
+ }
14
124
  if(schema.type==='object'&&schema.properties&&!needsJsonEditor(schema)){
15
- const result=structuredClone(value??{}),props=Object.entries(schema.properties);
125
+ const result=editableDefaults(schema,value??{}),props=Object.entries(schema.properties);
16
126
  while(true){
17
127
  const key=await ui.select(`${label} · 选择字段`,[
18
128
  ...props.map(([key,s])=>({value:`field:${key}`,label:`${s.title??key}${schema.required?.includes(key)?' *':''}`,description:JSON.stringify(result[key])??'未填写'})),
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import {randomUUID} from 'node:crypto';
3
3
  import {createSessionHistory} from '@mytools/fst-logs/sessions';
4
- import {readableValue,readableError,runCategory,runCategoryNames} from './run-presentation.js';
4
+ import {readableValue,readableError,runCategory,runCategoryNames,presentationCatalog} from './run-presentation.js';
5
5
 
6
6
  // 完整交互追加落盘,快照只保存可立即恢复的近期窗口,不重复保存运行日志全文。
7
7
  export class TerminalHistory{
@@ -59,7 +59,7 @@ export class TerminalHistory{
59
59
  if(this.app.listRuns().some(run=>run.status==='running'))throw new Error('当前有运行中的任务,完成后再恢复历史会话。');
60
60
  await this.flush();this.resumedFrom=record.id;
61
61
  this.ui.commandHistory=[...record.commandHistory];this.ui.commandHistoryIndex=null;
62
- this.ui.feed.restoreState(record.feed,{catalog:this.app.catalog?.()??[]});
62
+ this.ui.feed.restoreState(record.feed,{catalog:presentationCatalog(this.app)});
63
63
  this.append({type:'restored',sessionId:record.id});await this.flush();
64
64
  }
65
65
  async close(){await this.flush();this.closed=true;this.ui.feed.onEntry=null;this.ui.onHistoryChange=null;}