@yanglaofish/fishtools 0.4.0 → 0.5.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 +33 -5
- package/bin/fst.js +1 -1
- package/manual/README.md +33 -5
- package/manual/docs/deployment.md +3 -3
- package/manual/docs/kernel-generation.md +97 -0
- package/manual/docs/modules.md +57 -0
- package/manual/docs/plugin-development.md +120 -10
- package/manual/docs/releases.md +12 -0
- package/manual/docs/research/2026-10-11-plugin-kernel-capabilities.md +6 -6
- package/manual/docs/roadmap.md +14 -3
- package/manual/docs/superpowers/plans/2026-10-09-workflow-mvp.md +5 -5
- package/manual/docs/superpowers/plans/2026-10-11-public-plugin-fixes.md +2 -2
- package/manual/docs/superpowers/plans/2026-10-12-kernel-generation.md +22 -0
- package/manual/docs/superpowers/plans/2026-10-12-modular-kernel.md +40 -0
- package/manual/docs/superpowers/reviews/2026-10-12-kernel-generation-review.md +27 -0
- package/manual/docs/superpowers/reviews/2026-10-12-modular-kernel-review.md +32 -0
- package/manual/docs/superpowers/reviews/2026-10-13-kernel-runtime-release-review.md +19 -0
- package/manual/docs/superpowers/specs/2026-10-12-action-network-policy-design.md +52 -0
- package/manual/docs/superpowers/specs/2026-10-12-kernel-generation-design.md +38 -0
- package/manual/docs/superpowers/specs/2026-10-12-modular-kernel-design.md +70 -0
- package/manual/docs/superpowers/specs/2026-10-12-plugin-readiness-design.md +21 -0
- package/manual/docs/superpowers/specs/2026-10-12-runtime-maintenance-granularity-design.md +35 -0
- package/manual/docs/superpowers/specs/2026-10-12-workflow-variation-design.md +19 -0
- package/manual/docs/verification.md +42 -2
- package/manual/packages/core/README.md +44 -0
- package/manual/packages/core/package.json +38 -0
- package/manual/packages/core/src/application.js +242 -85
- package/manual/packages/core/src/bundle.js +2 -0
- package/manual/packages/core/src/development-watch-worker.js +69 -0
- package/manual/packages/core/src/development.js +146 -0
- package/manual/packages/core/src/environments.js +2 -0
- package/manual/packages/core/src/export-snapshot.js +42 -0
- package/manual/packages/core/src/plugin-update.js +108 -0
- package/manual/packages/core/src/process.js +2 -0
- package/manual/packages/core/src/resources.js +31 -0
- package/manual/packages/core/src/version.js +2 -0
- package/manual/packages/fst-cli/LICENSE +21 -0
- package/manual/packages/fst-cli/README.md +54 -0
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/bin/fst.js +55 -23
- package/manual/packages/fst-cli/package.json +49 -0
- package/manual/packages/fst-cli/src/development.js +53 -0
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/model.js +1 -1
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/navigation.js +1 -1
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/presentation.js +9 -1
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-feed.js +31 -5
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-viewer.js +9 -7
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/schema.js +1 -1
- package/manual/packages/{core → fst-cli}/src/terminal/session.js +137 -9
- package/manual/packages/{core → fst-cli}/src/terminal/view.js +20 -3
- package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/workflow-menu.js +17 -2
- package/manual/packages/fst-devtools/LICENSE +21 -0
- package/manual/packages/fst-devtools/README.md +28 -0
- package/manual/packages/fst-devtools/package.json +29 -0
- package/{node_modules/@mytools/core → manual/packages/fst-devtools}/src/documentation.js +3 -1
- package/manual/packages/fst-devtools/src/plugin-quality.js +136 -0
- package/{node_modules/@mytools/core → manual/packages/fst-devtools}/src/plugin-tools.js +24 -5
- package/manual/packages/fst-logs/LICENSE +21 -0
- package/manual/packages/fst-logs/README.md +56 -0
- package/manual/packages/fst-logs/package.json +24 -0
- package/manual/packages/fst-logs/src/log-bounds.js +91 -0
- package/manual/packages/fst-logs/src/run-logs.js +239 -0
- package/manual/packages/fst-platform/LICENSE +21 -0
- package/manual/packages/fst-platform/README.md +67 -0
- package/manual/packages/fst-platform/package.json +33 -0
- package/manual/packages/fst-platform/src/bundle.js +17 -0
- package/manual/packages/fst-platform/src/environments.js +67 -0
- package/manual/packages/fst-platform/src/file-probe-engine.js +183 -0
- package/manual/packages/fst-platform/src/file-probe-worker.js +43 -0
- package/manual/packages/fst-platform/src/file-probe.js +6 -0
- package/manual/packages/fst-platform/src/json.js +5 -0
- package/manual/packages/fst-platform/src/network.js +199 -0
- package/manual/packages/fst-platform/src/npm.js +11 -0
- package/manual/packages/fst-platform/src/process.js +110 -0
- package/manual/packages/fst-plugin/LICENSE +21 -0
- package/manual/packages/fst-plugin/README.md +29 -0
- package/manual/packages/fst-plugin/package.json +29 -0
- package/{node_modules/@mytools/core → manual/packages/fst-plugin}/src/contracts.js +30 -3
- package/manual/packages/fst-plugin/src/host.js +232 -0
- package/manual/packages/fst-plugin/src/loader.js +21 -0
- package/manual/packages/fst-plugin/src/privileges.js +91 -0
- package/{node_modules/@mytools/core → manual/packages/fst-plugin}/src/runtime.js +20 -4
- package/manual/packages/fst-workflow/LICENSE +21 -0
- package/manual/packages/fst-workflow/README.md +28 -0
- package/manual/packages/fst-workflow/package.json +24 -0
- package/{node_modules/@mytools/core → manual/packages/fst-workflow}/src/control.js +3 -2
- package/manual/packages/{core → fst-workflow}/src/engine.js +2 -2
- package/manual/packages/fst-workspace/LICENSE +21 -0
- package/manual/packages/fst-workspace/README.md +32 -0
- package/manual/packages/fst-workspace/package.json +34 -0
- package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/bundle-workspace.js +1 -1
- package/manual/packages/fst-workspace/src/index.js +3 -0
- package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/plugins.js +6 -36
- package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/workflows.js +40 -7
- package/manual/packages/pack/LICENSE +21 -0
- package/manual/packages/pack/README.md +31 -0
- package/manual/packages/pack/archive.js +15 -0
- package/manual/packages/pack/dependencies.js +105 -0
- package/manual/packages/pack/environments.js +50 -0
- package/manual/packages/pack/index.js +103 -0
- package/manual/packages/pack/package.json +29 -0
- package/manual/packages/pack/runtime.js +61 -0
- package/manual/packages/sdk/README.md +31 -0
- package/manual/packages/sdk/conditions.js +1 -0
- package/manual/packages/sdk/package.json +23 -1
- package/manual/plugins/web/README.md +29 -0
- package/manual/tests/application.test.js +6 -4
- package/node_modules/@mytools/core/README.md +44 -0
- package/node_modules/@mytools/core/package.json +17 -17
- package/node_modules/@mytools/core/src/application.js +242 -85
- package/node_modules/@mytools/core/src/bundle.js +2 -17
- package/node_modules/@mytools/core/src/development-watch-worker.js +69 -0
- package/node_modules/@mytools/core/src/development.js +146 -0
- package/node_modules/@mytools/core/src/environments.js +2 -67
- package/node_modules/@mytools/core/src/export-snapshot.js +42 -0
- package/node_modules/@mytools/core/src/plugin-update.js +108 -0
- package/node_modules/@mytools/core/src/process.js +2 -80
- package/node_modules/@mytools/core/src/version.js +1 -1
- package/node_modules/@mytools/fst-cli/LICENSE +21 -0
- package/node_modules/@mytools/fst-cli/README.md +54 -0
- package/node_modules/@mytools/fst-cli/bin/fst.js +124 -0
- package/node_modules/@mytools/fst-cli/package.json +49 -0
- package/node_modules/@mytools/fst-cli/src/cli-run.js +12 -0
- package/node_modules/@mytools/fst-cli/src/development.js +53 -0
- package/node_modules/@mytools/fst-cli/src/terminal/conditions.js +32 -0
- package/node_modules/@mytools/fst-cli/src/terminal/log-viewer.js +45 -0
- package/node_modules/@mytools/fst-cli/src/terminal/model.js +104 -0
- package/node_modules/@mytools/fst-cli/src/terminal/navigation.js +17 -0
- package/node_modules/@mytools/fst-cli/src/terminal/presentation.js +50 -0
- package/{manual/packages/core → node_modules/@mytools/fst-cli}/src/terminal/run-feed.js +31 -5
- package/node_modules/@mytools/fst-cli/src/terminal/run-presentation.js +60 -0
- package/node_modules/@mytools/fst-cli/src/terminal/run-viewer.js +106 -0
- package/node_modules/@mytools/fst-cli/src/terminal/schema.js +52 -0
- package/node_modules/@mytools/{core → fst-cli}/src/terminal/session.js +137 -9
- package/node_modules/@mytools/fst-cli/src/terminal/theme.js +29 -0
- package/node_modules/@mytools/{core → fst-cli}/src/terminal/view.js +20 -3
- package/node_modules/@mytools/fst-cli/src/terminal/workflow-menu.js +65 -0
- package/node_modules/@mytools/fst-devtools/LICENSE +21 -0
- package/node_modules/@mytools/fst-devtools/README.md +28 -0
- package/node_modules/@mytools/fst-devtools/package.json +29 -0
- package/node_modules/@mytools/fst-devtools/src/documentation.js +67 -0
- package/node_modules/@mytools/fst-devtools/src/plugin-quality.js +136 -0
- package/node_modules/@mytools/fst-devtools/src/plugin-tools.js +152 -0
- package/node_modules/@mytools/fst-logs/LICENSE +21 -0
- package/node_modules/@mytools/fst-logs/README.md +56 -0
- package/node_modules/@mytools/fst-logs/package.json +24 -0
- package/node_modules/@mytools/fst-logs/src/log-bounds.js +91 -0
- package/node_modules/@mytools/fst-logs/src/markdown-log.js +55 -0
- package/node_modules/@mytools/fst-logs/src/run-logs.js +239 -0
- package/node_modules/@mytools/fst-platform/LICENSE +21 -0
- package/node_modules/@mytools/fst-platform/README.md +67 -0
- package/node_modules/@mytools/fst-platform/package.json +33 -0
- package/node_modules/@mytools/fst-platform/src/bundle.js +17 -0
- package/node_modules/@mytools/fst-platform/src/environments.js +67 -0
- package/node_modules/@mytools/fst-platform/src/file-probe-engine.js +183 -0
- package/node_modules/@mytools/fst-platform/src/file-probe-worker.js +43 -0
- package/node_modules/@mytools/fst-platform/src/file-probe.js +6 -0
- package/node_modules/@mytools/fst-platform/src/json.js +5 -0
- package/node_modules/@mytools/fst-platform/src/network.js +199 -0
- package/node_modules/@mytools/fst-platform/src/npm.js +11 -0
- package/node_modules/@mytools/fst-platform/src/process.js +110 -0
- package/node_modules/@mytools/fst-plugin/LICENSE +21 -0
- package/node_modules/@mytools/fst-plugin/README.md +29 -0
- package/node_modules/@mytools/fst-plugin/package.json +29 -0
- package/node_modules/@mytools/fst-plugin/src/contracts.js +86 -0
- package/node_modules/@mytools/fst-plugin/src/host.js +232 -0
- package/node_modules/@mytools/fst-plugin/src/loader.js +21 -0
- package/node_modules/@mytools/fst-plugin/src/privileges.js +91 -0
- package/{manual/packages/core → node_modules/@mytools/fst-plugin}/src/runtime.js +20 -4
- package/node_modules/@mytools/fst-workflow/LICENSE +21 -0
- package/node_modules/@mytools/fst-workflow/README.md +28 -0
- package/node_modules/@mytools/fst-workflow/package.json +24 -0
- package/node_modules/@mytools/fst-workflow/src/control.js +64 -0
- package/node_modules/@mytools/{core → fst-workflow}/src/engine.js +2 -2
- package/node_modules/@mytools/fst-workspace/LICENSE +21 -0
- package/node_modules/@mytools/fst-workspace/README.md +32 -0
- package/node_modules/@mytools/fst-workspace/package.json +34 -0
- package/node_modules/@mytools/fst-workspace/src/bundle-workspace.js +39 -0
- package/node_modules/@mytools/fst-workspace/src/index.js +3 -0
- package/node_modules/@mytools/fst-workspace/src/plugin-installations.js +84 -0
- package/node_modules/@mytools/fst-workspace/src/plugins.js +35 -0
- package/node_modules/@mytools/fst-workspace/src/project.js +29 -0
- package/node_modules/@mytools/fst-workspace/src/workflows.js +150 -0
- package/node_modules/@mytools/fst-workspace/templates/control-demo.yaml +47 -0
- package/node_modules/@mytools/fst-workspace/templates/hello.yaml +35 -0
- package/node_modules/@mytools/fst-workspace/templates/python-check.yaml +26 -0
- package/node_modules/@mytools/fst-workspace/templates/system-check.yaml +11 -0
- package/node_modules/@mytools/pack/README.md +31 -0
- package/node_modules/@mytools/pack/dependencies.js +7 -2
- package/node_modules/@mytools/pack/environments.js +1 -1
- package/node_modules/@mytools/pack/index.js +33 -25
- package/node_modules/@mytools/pack/package.json +29 -1
- package/node_modules/@mytools/sdk/README.md +31 -0
- package/node_modules/@mytools/sdk/conditions.js +1 -0
- package/node_modules/@mytools/sdk/diagnostics.js +27 -0
- package/node_modules/@mytools/sdk/package.json +23 -1
- package/node_modules/@mytools/sdk/schema-path.js +31 -0
- package/node_modules/@mytools/sdk/validation.js +78 -0
- package/node_modules/@mytools/web/README.md +29 -0
- package/node_modules/@mytools/web/package.json +7 -0
- package/node_modules/@mytools/web/public/THIRD_PARTY_NOTICES.txt +1398 -1199
- package/node_modules/@mytools/web/public/assets/index-BaVzHycP.js +70 -0
- package/node_modules/@mytools/web/public/assets/index-DA8Ae92t.css +1 -0
- package/node_modules/@mytools/web/public/index.html +2 -2
- package/node_modules/@mytools/web/server.js +24 -17
- package/node_modules/@mytools/web/src/model.js +84 -0
- package/node_modules/acorn/CHANGELOG.md +1012 -0
- package/node_modules/acorn/LICENSE +21 -0
- package/node_modules/acorn/README.md +309 -0
- package/node_modules/acorn/bin/acorn +4 -0
- package/node_modules/acorn/dist/acorn.d.mts +897 -0
- package/node_modules/acorn/dist/acorn.d.ts +897 -0
- package/node_modules/acorn/dist/acorn.js +6370 -0
- package/node_modules/acorn/dist/acorn.mjs +6341 -0
- package/node_modules/acorn/dist/bin.js +90 -0
- package/node_modules/acorn/package.json +50 -0
- package/node_modules/{minipass-flush/node_modules/minipass → tar}/node_modules/yallist/README.md +8 -7
- package/node_modules/tar/node_modules/yallist/package.json +68 -0
- package/node_modules/yallist/README.md +7 -8
- package/node_modules/yallist/package.json +12 -51
- package/package.json +131 -115
- package/manual/packages/core/src/host.js +0 -139
- package/node_modules/@mytools/core/src/host.js +0 -139
- package/node_modules/@mytools/core/src/run-logs.js +0 -63
- package/node_modules/@mytools/web/public/assets/index-DT2B4tMV.js +0 -70
- package/node_modules/@mytools/web/public/assets/index-vuZvrUTT.css +0 -1
- package/node_modules/minipass-flush/node_modules/minipass/node_modules/yallist/package.json +0 -29
- /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/cli-run.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/conditions.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/log-viewer.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/run-presentation.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-cli}/src/terminal/theme.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-logs}/src/markdown-log.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/plugin-installations.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/src/project.js +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/control-demo.yaml +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/hello.yaml +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/python-check.yaml +0 -0
- /package/{node_modules/@mytools/core → manual/packages/fst-workspace}/templates/system-check.yaml +0 -0
- /package/{node_modules/@mytools/core/src → manual/packages/sdk}/diagnostics.js +0 -0
- /package/{node_modules/@mytools/core/src → manual/packages/sdk}/schema-path.js +0 -0
- /package/{node_modules/@mytools/core/src → manual/packages/sdk}/validation.js +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/LICENSE.md +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/index.d.ts +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/index.d.ts.map +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/index.js +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/index.js.map +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/commonjs/package.json +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/index.d.ts +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/index.d.ts.map +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/index.js +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/index.js.map +0 -0
- /package/node_modules/{yallist → tar/node_modules/yallist}/dist/esm/package.json +0 -0
- /package/node_modules/{minipass-flush/node_modules/minipass/node_modules/yallist → yallist}/LICENSE +0 -0
- /package/node_modules/{minipass-flush/node_modules/minipass/node_modules/yallist → yallist}/iterator.js +0 -0
- /package/node_modules/{minipass-flush/node_modules/minipass/node_modules/yallist → yallist}/yallist.js +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// 阅读层只处理展示;原始事件、输入输出与错误仍保留在运行记录中。
|
|
2
|
+
export function readableValue(value,indent=''){
|
|
3
|
+
if(value===undefined)return '暂无记录';
|
|
4
|
+
if(value===null)return 'null';
|
|
5
|
+
if(typeof value!=='object')return String(value);
|
|
6
|
+
const entries=Array.isArray(value)?value.map((v,i)=>[`[${i+1}]`,v]):Object.entries(value);
|
|
7
|
+
if(!entries.length)return Array.isArray(value)?'(空列表)':'(空对象)';
|
|
8
|
+
return entries.map(([key,item])=>{
|
|
9
|
+
const nested=item!==null&&typeof item==='object';
|
|
10
|
+
return `${indent}${key}:${nested?'\n'+readableValue(item,indent+' '):String(item).replaceAll('\n','\n'+indent+' ')}`;
|
|
11
|
+
}).join('\n');
|
|
12
|
+
}
|
|
13
|
+
export function readableError(error,{details=false}={}){
|
|
14
|
+
if(!error)return '';
|
|
15
|
+
const chain=[];for(let current=error;current;current=current.cause)chain.push(current);
|
|
16
|
+
const program=chain.find(e=>e.details?.executable)?.details.executable;
|
|
17
|
+
const code=chain.find(e=>e.code==='ENOENT')?.code??error.code;
|
|
18
|
+
const summary=[`原因:${code==='ENOENT'&&program?'未找到所需程序':error.message??'未知错误'}`,error.plugin&&`插件:${error.plugin}`,code&&`错误码:${code}`,program&&`程序:${program}`,code==='ENOENT'&&'请检查程序是否安装、路径是否正确以及 PATH 配置。'].filter(Boolean).join('\n');
|
|
19
|
+
if(!details)return summary;
|
|
20
|
+
const diagnostic=chain.flatMap((item,index)=>[
|
|
21
|
+
`${index===0?'错误':'上游原因 '+index}:${item.message??'未知错误'}`,
|
|
22
|
+
item.code&&`错误码:${item.code}`,
|
|
23
|
+
item.details!==undefined&&`细节:\n${readableValue(item.details)}`,
|
|
24
|
+
item.stack&&`调用栈:\n${item.stack}`,
|
|
25
|
+
].filter(Boolean));
|
|
26
|
+
return `${summary}\n\n${diagnostic.join('\n')}`;
|
|
27
|
+
}
|
|
28
|
+
const eventNames={'step.started':'步骤开始','step.succeeded':'步骤成功','step.failed':'步骤失败','step.cancelled':'步骤取消','step.skipped':'步骤跳过','step.stopped':'步骤停止','run.started':'运行开始','run.succeeded':'运行成功','run.failed':'运行失败','run.cancelled':'运行取消','run.stopped':'提前结束'};
|
|
29
|
+
const time=value=>{const date=new Date(value);return Number.isFinite(+date)?date.toLocaleTimeString('zh-CN',{hour12:false}):'--:--:--';};
|
|
30
|
+
export function readableTimeline(events,{callPath,values=false}={}){
|
|
31
|
+
const rows=[];let pending;
|
|
32
|
+
const push=(event,message)=>{
|
|
33
|
+
const prefix=`${time(event.time)} [${event.callPath??'运行'}${event.stream?' · '+event.stream:''}]`;
|
|
34
|
+
const lines=String(message).replaceAll('\r\n','\n').replaceAll('\r','\n').split('\n');
|
|
35
|
+
if(lines.at(-1)==='')lines.pop();
|
|
36
|
+
for(const line of lines)rows.push(`${prefix} ${line}`);
|
|
37
|
+
};
|
|
38
|
+
const flush=()=>{if(pending){if(pending.text)push(pending.event,pending.text);pending=null;}};
|
|
39
|
+
for(const event of events??[]){
|
|
40
|
+
if(callPath&&event.callPath!==callPath)continue;
|
|
41
|
+
if(event.type==='log'&&event.stream){
|
|
42
|
+
const key=`${event.callPath}\0${event.stream}`;
|
|
43
|
+
if(pending?.key!==key){flush();pending={key,event,text:''};}
|
|
44
|
+
pending.text+=event.message??'';
|
|
45
|
+
const end=pending.text.lastIndexOf('\n');
|
|
46
|
+
if(end>=0){push(pending.event,pending.text.slice(0,end+1));pending.text=pending.text.slice(end+1);pending.event=event;}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if(event.type==='log'){flush();push(event,`${event.message??''}${event.data!==undefined?'\n'+readableValue(event.data):''}`);continue;}
|
|
50
|
+
if(eventNames[event.type]){
|
|
51
|
+
const detail=event.error?readableError(event.error,{details:values}):event.reason??event.message;
|
|
52
|
+
flush();push(event,`${eventNames[event.type]}${event.action?' · '+event.action:''}${event.durationMs!==undefined?` · ${event.durationMs} ms`:''}${detail?'\n'+detail:''}`);
|
|
53
|
+
if(values&&event.type.startsWith('step.')){
|
|
54
|
+
if(event.input!==undefined&&Object.keys(event.input??{}).length)push(event,'输入\n'+readableValue(event.input));
|
|
55
|
+
if(event.output!==undefined)push(event,'输出\n'+readableValue(event.output));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
flush();return rows.join('\n')||'等待执行记录。插件通过 log 或 runProcess 输出的内容会显示在这里。';
|
|
60
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
2
|
+
import { Text, matchesKey, truncateToWidth } from '@mariozechner/pi-tui';
|
|
3
|
+
import {palette} from './theme.js';
|
|
4
|
+
import {readableValue,readableError,readableTimeline} from './run-presentation.js';
|
|
5
|
+
|
|
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
|
+
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
|
+
export const mouseOn='\x1b[?1000h\x1b[?1006h';
|
|
10
|
+
export const mouseOff='\x1b[?1006l\x1b[?1000l';
|
|
11
|
+
|
|
12
|
+
export class RunViewer{
|
|
13
|
+
constructor({record,flow,workflows=[],catalog=[],terminal,onClose,onCancel,onReadLogs,requestRender}){
|
|
14
|
+
Object.assign(this,{terminal,onClose,onCancel,onReadLogs,requestRender,catalog});this.view=0;this.selected=0;this.offset=0;this.follow=true;this.allLogs=true;this.raw=false;this.confirmCancel=false;
|
|
15
|
+
this.planned=[];
|
|
16
|
+
const walk=(steps,prefix,ancestors)=>{
|
|
17
|
+
for(const step of steps??[]){
|
|
18
|
+
const callPath=`${prefix}/${step.id}`;this.planned.push({callPath,stepId:step.id,action:step.action,title:step.title,status:'pending'});
|
|
19
|
+
if(step.branch){walk(step.branch.then,callPath,ancestors);walk(step.branch.else,callPath,ancestors);}
|
|
20
|
+
if(step.parallel)walk(step.parallel.steps,callPath,ancestors);
|
|
21
|
+
if(step.workflow){const child=workflows.find(w=>`${w.id}.yaml`===step.workflow.replace(/^\.\//,''));if(child&&!ancestors.has(child.id))walk(child.steps,callPath,new Set([...ancestors,child.id]));}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
if(flow)walk(flow.steps,flow.id,new Set([flow.id]));
|
|
25
|
+
this.update(record);
|
|
26
|
+
}
|
|
27
|
+
invalidate(){}
|
|
28
|
+
update(record){
|
|
29
|
+
if(!record)return;
|
|
30
|
+
this.record=record;const nodes=new Map(this.planned.map(node=>[node.callPath,{...node,...(record.stepsTruncated&&record.status!=='running'?{status:'unknown'}:{})}]));
|
|
31
|
+
for(const event of [...(record.events??[]),...(record.stepSummaries??[])].sort((a,b)=>(a.seq??0)-(b.seq??0))){
|
|
32
|
+
if(!event.type.startsWith('step.')||!event.callPath)continue;
|
|
33
|
+
const node=nodes.get(event.callPath)??{callPath:event.callPath,stepId:event.stepId};
|
|
34
|
+
Object.assign(node,event,{status:event.type==='step.started'?'running':event.type.slice(5)});
|
|
35
|
+
node.title??=this.catalog.find(action=>action.id===node.action)?.title;nodes.set(event.callPath,node);
|
|
36
|
+
}
|
|
37
|
+
this.nodes=[...nodes.values()];this.selected=Math.max(0,Math.min(this.selected,this.nodes.length-1));
|
|
38
|
+
this.requestRender();
|
|
39
|
+
}
|
|
40
|
+
content(){
|
|
41
|
+
const node=this.nodes[this.selected],status=runStatuses[this.record.status]??this.record.status;
|
|
42
|
+
if(this.view===0)return [
|
|
43
|
+
`运行:${status}${this.record.reason?` · ${this.record.reason}`:''}`,
|
|
44
|
+
this.record.error?readableError(this.record.error):'',
|
|
45
|
+
...(this.record.warnings??[]).map(warning=>`警告:${warning.message}`),
|
|
46
|
+
this.record.output!==undefined?'\n最终输出\n'+readableValue(this.record.output):'',
|
|
47
|
+
'\n步骤 · Enter 查看详情',
|
|
48
|
+
...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:''}`),
|
|
49
|
+
!this.nodes.length?'此运行没有工作流步骤,可在日志页查看插件输出。':'',
|
|
50
|
+
].filter(Boolean).join('\n');
|
|
51
|
+
if(this.view===1){
|
|
52
|
+
const format=value=>this.raw?JSON.stringify(value??null,null,2):readableValue(value);
|
|
53
|
+
const error=node?node.error:this.record.error;
|
|
54
|
+
return [`步骤:${node?.callPath??'整个运行'}`,node?.action?'能力:'+node.action:'',error?'\n错误\n'+(this.raw?format(error):readableError(error)):'',
|
|
55
|
+
node?'\n输入\n'+format(node.input):'', '\n输出\n'+format(node?node.output:this.record.output),
|
|
56
|
+
!error&&(node?.reason||node?.message)?'\n说明:'+(node.reason??node.message):'',this.raw?'\n原始 JSON · 包含完整错误详情与调用栈':'\nj 查看原始 JSON 与完整诊断',
|
|
57
|
+
].filter(Boolean).join('\n');
|
|
58
|
+
}
|
|
59
|
+
return (this.record.eventsTruncated?'当前为近期日志摘要,按 p 分页查看完整落盘日志。\n\n':'')+readableTimeline(this.record.events,{callPath:this.allLogs?undefined:node?.callPath});
|
|
60
|
+
}
|
|
61
|
+
layout(width){
|
|
62
|
+
const height=Math.max(1,this.terminal.rows),pageSize=Math.max(1,height-4);
|
|
63
|
+
const lines=new Text(safe(this.content()),0,0).render(Math.max(1,width));
|
|
64
|
+
const maxOffset=Math.max(0,lines.length-pageSize);
|
|
65
|
+
this.offset=this.follow&&this.view===2?maxOffset:Math.max(0,Math.min(this.offset,maxOffset));
|
|
66
|
+
return {height,pageSize,lines,maxOffset};
|
|
67
|
+
}
|
|
68
|
+
render(width){
|
|
69
|
+
const {height,pageSize,lines}=this.layout(width),node=this.nodes[this.selected];
|
|
70
|
+
const body=lines.slice(this.offset,this.offset+pageSize);while(body.length<pageSize)body.push('');
|
|
71
|
+
const tabs=`${['1 总览','2 输入输出','3 日志'].map((s,i)=>this.view===i?palette.brand(`[${s}]`):palette.muted(s)).join(' ')} ${palette.muted('·')} ${palette.text(safe(runStatuses[this.record.status]??this.record.status))} ${palette.muted('· '+safe(this.record.workflowId??'运行'))}`;
|
|
72
|
+
const selection=`[ / ] 切换步骤:${node?.callPath??'暂无步骤'}${this.view===2?` · ${this.allLogs?'全部日志':'本步骤日志'} · ${this.follow?'跟随尾部':'暂停跟随'}`:''}`;
|
|
73
|
+
const footer=this.confirmCancel?'取消整个运行?y 确认 · 任意其他键返回':'q/Esc 返回 · x 取消 · p 完整日志 · 1/2/3 切换';
|
|
74
|
+
const shortcuts=width<60?'↑↓/滚轮 · [ ] 步骤 · j 原始数据':`↑↓/滚轮 翻页 · [ ] 步骤 · j 原始数据 · a 全部/本步骤 · End 跟随 · ${this.offset+1}–${Math.min(this.offset+pageSize,lines.length)}/${lines.length}`;
|
|
75
|
+
const styledBody=body.map(line=>line.startsWith('›')?palette.brand(line):line);
|
|
76
|
+
const styledFooter=palette.muted(safe(footer));
|
|
77
|
+
const screen=height===1?styledBody:height===2?[styledBody[0],styledFooter]:height===3?[tabs,styledBody[0],styledFooter]:height===4?[tabs,styledBody[0],styledFooter,palette.muted(shortcuts)]:[tabs,palette.muted(safe(selection)),...styledBody,styledFooter,palette.muted(shortcuts)];
|
|
78
|
+
return screen.map(line=>truncateToWidth(line,width));
|
|
79
|
+
}
|
|
80
|
+
handleInput(data){
|
|
81
|
+
if(data==='p'&&this.onReadLogs){this.onReadLogs();return;}
|
|
82
|
+
if(this.confirmCancel){this.confirmCancel=false;if(data==='y')this.onCancel();this.requestRender();return;}
|
|
83
|
+
if(['escape','q'].some(key=>matchesKey(data,key))){this.onClose();return;}
|
|
84
|
+
if(matchesKey(data,'enter')){if(this.view===0){this.view=1;this.offset=0;this.requestRender();}return;}
|
|
85
|
+
if(data==='j'){this.raw=!this.raw;this.view=1;this.offset=0;this.requestRender();return;}
|
|
86
|
+
if(data==='x'&&this.record.status==='running'){this.confirmCancel=true;this.requestRender();return;}
|
|
87
|
+
if(['1','2','3'].includes(data)){this.view=Number(data)-1;this.offset=0;this.follow=true;this.requestRender();return;}
|
|
88
|
+
if(data==='['||data===']'){
|
|
89
|
+
this.selected=Math.max(0,Math.min(this.nodes.length-1,this.selected+(data===']'?1:-1)));this.offset=0;
|
|
90
|
+
if(this.view===0){const {lines,pageSize,maxOffset}=this.layout(this.terminal.columns);const row=lines.findIndex(line=>line.startsWith('›'));this.offset=Math.min(maxOffset,Math.max(0,row-Math.floor(pageSize/2)));}
|
|
91
|
+
this.requestRender();return;
|
|
92
|
+
}
|
|
93
|
+
if(data==='a'){this.allLogs=!this.allLogs;this.offset=0;this.requestRender();return;}
|
|
94
|
+
if(data==='f'){this.follow=!this.follow;this.requestRender();return;}
|
|
95
|
+
const {pageSize,maxOffset}=this.layout(this.terminal.columns),wheel=mouseWheel(data);
|
|
96
|
+
if(wheel)this.offset+=wheel*3;
|
|
97
|
+
else if(matchesKey(data,'up'))this.offset--;
|
|
98
|
+
else if(matchesKey(data,'down'))this.offset++;
|
|
99
|
+
else if(matchesKey(data,'pageUp'))this.offset-=pageSize;
|
|
100
|
+
else if(matchesKey(data,'pageDown'))this.offset+=pageSize;
|
|
101
|
+
else if(matchesKey(data,'home'))this.offset=0;
|
|
102
|
+
else if(matchesKey(data,'end')){this.offset=maxOffset;this.follow=true;this.requestRender();return;}
|
|
103
|
+
else return;
|
|
104
|
+
this.follow=false;this.offset=Math.max(0,Math.min(maxOffset,this.offset));this.requestRender();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { validate } from '@mytools/sdk/schema';
|
|
2
|
+
import { fileReference } from './model.js';
|
|
3
|
+
|
|
4
|
+
export const needsJsonEditor=schema=>['oneOf','anyOf','allOf','if','$ref','patternProperties'].some(key=>schema?.[key]!==undefined);
|
|
5
|
+
|
|
6
|
+
// 值只在整个编辑过程完成后返回,Escape 不会留下半个修改。
|
|
7
|
+
export async function editValue(ui,schema={},value,{baseDir,required=false,label=schema.title??'值'}={}){
|
|
8
|
+
if(value===undefined&&schema.default!==undefined)value=structuredClone(schema.default);
|
|
9
|
+
if(schema.enum&&!needsJsonEditor(schema)){
|
|
10
|
+
const index=await ui.select(label,schema.enum.map((v,i)=>({value:String(i),label:JSON.stringify(v)})));
|
|
11
|
+
return schema.enum[Number(index)];
|
|
12
|
+
}
|
|
13
|
+
if(schema.type==='boolean'&&!needsJsonEditor(schema))return (await ui.select(label,[{value:'true',label:'是 / true'},{value:'false',label:'否 / false'}]))==='true';
|
|
14
|
+
if(schema.type==='object'&&schema.properties&&!needsJsonEditor(schema)){
|
|
15
|
+
const result=structuredClone(value??{}),props=Object.entries(schema.properties);
|
|
16
|
+
while(true){
|
|
17
|
+
const key=await ui.select(`${label} · 选择字段`,[
|
|
18
|
+
...props.map(([key,s])=>({value:`field:${key}`,label:`${s.title??key}${schema.required?.includes(key)?' *':''}`,description:JSON.stringify(result[key])??'未填写'})),
|
|
19
|
+
{value:'__done',label:'完成填写'},
|
|
20
|
+
]);
|
|
21
|
+
if(key==='__done'){
|
|
22
|
+
try{return validate(schema,result,label);}catch(e){ui.show(e.message);continue;}
|
|
23
|
+
}
|
|
24
|
+
const field=key.slice(6);
|
|
25
|
+
const next=await editValue(ui,schema.properties[field],result[field],{baseDir,required:schema.required?.includes(field),label:schema.properties[field].title??field});
|
|
26
|
+
if(next===undefined)delete result[field];else result[field]=next;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if(schema.type==='array'&&schema.items&&!Array.isArray(schema.items)&&!needsJsonEditor(schema)){
|
|
30
|
+
const result=structuredClone(value??[]);
|
|
31
|
+
while(true){
|
|
32
|
+
const choice=await ui.select(label,[...result.map((v,i)=>({value:String(i),label:`${i+1}. ${JSON.stringify(v)}`})),{value:'add',label:'添加一项'},{value:'done',label:'完成列表'}]);
|
|
33
|
+
if(choice==='done'){try{return validate(schema,result,label);}catch(e){ui.show(e.message);continue;}}
|
|
34
|
+
if(choice==='add'){const item=await editValue(ui,schema.items,undefined,{baseDir,required:true,label:'新项'});result.push(item);}
|
|
35
|
+
else{
|
|
36
|
+
const index=Number(choice),action=await ui.select('修改列表项',[{value:'edit',label:'编辑'},{value:'remove',label:'移除'}]);
|
|
37
|
+
if(action==='remove')result.splice(index,1);else result[index]=await editValue(ui,schema.items,result[index],{baseDir,required:true,label:`第 ${index+1} 项`});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
while(true){
|
|
42
|
+
const string=schema.type==='string';
|
|
43
|
+
const initial=value===undefined?'':string?String(value):JSON.stringify(value);
|
|
44
|
+
const raw=await ui.input(`${label}${required?'(必填)':''}${string?' · @文件引用路径,@@表示字面量@':' · 输入 JSON 值'}${schema.description?'\n'+schema.description:''}`,{value:string&&initial.startsWith('@')?'@'+initial:initial,files:string});
|
|
45
|
+
try{
|
|
46
|
+
if(!raw&&!required&&schema.type!=='string')return undefined;
|
|
47
|
+
if(!raw&&required&&schema.type!=='string')throw new Error('请填写必填值');
|
|
48
|
+
const result=string?fileReference(raw,baseDir):JSON.parse(raw);
|
|
49
|
+
return validate(schema,result,label);
|
|
50
|
+
}catch(e){ui.show(`输入无效:${e.message}`);}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -9,10 +9,20 @@ import {gettingStarted,pluginStatus,pluginDetails} from './presentation.js';
|
|
|
9
9
|
import {readableValue,readableError} from './run-presentation.js';
|
|
10
10
|
import {commandMenu} from './navigation.js';
|
|
11
11
|
import {workflowMenu} from './workflow-menu.js';
|
|
12
|
+
import {resolvePrivilegeRequirement,checkStaticPrivilege} from '@mytools/fst-plugin/privileges';
|
|
12
13
|
|
|
13
14
|
export class TerminalSession{
|
|
14
15
|
constructor(app,ui,{baseDir=process.cwd(),webOptions={}}={}){this.app=app;this.ui=ui;this.baseDir=baseDir;this.webOptions=webOptions;this.flow=null;this.dirty=false;this.exiting=false;this.web=null;ui.activeRuns=()=>app.listRuns().filter(run=>run.status==='running').length;}
|
|
15
16
|
writable(){if(this.app.project.readOnly)throw new Error('这是只读运行包,请在完整工作区中编排、安装或打包');}
|
|
17
|
+
async withMaintenance(operation,options={}){
|
|
18
|
+
try{return await operation(options);}
|
|
19
|
+
catch(error){
|
|
20
|
+
if(error.code!=='MAINTENANCE_REQUIRED')throw error;
|
|
21
|
+
const names=error.details?.plugins?.join('、')??'相关插件';
|
|
22
|
+
if(!await this.ui.confirm(`本次修改会短暂停止并重启 ${names}。现在继续吗?`))return null;
|
|
23
|
+
return operation({...options,allowMaintenance:true});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
16
26
|
current(){if(!this.flow)throw new Error('先用 /workflows 打开或新建工作流');return this.flow;}
|
|
17
27
|
commands(){return commandMenu(this.app.listPlugins?.()??[],this.app.listCommands());}
|
|
18
28
|
async discard(){return !this.dirty||await this.ui.confirm('当前修改未保存,是否放弃修改?');}
|
|
@@ -71,7 +81,7 @@ export class TerminalSession{
|
|
|
71
81
|
'## 全部操作',this.commands().map(c=>`- /${c.name}:${c.description}`).join('\n'),
|
|
72
82
|
].join('\n\n'));return;
|
|
73
83
|
case 'docs':{
|
|
74
|
-
const {listDocumentation,readDocumentation}=await import('
|
|
84
|
+
const {listDocumentation,readDocumentation}=await import('@mytools/fst-devtools/documentation');
|
|
75
85
|
const topics=await listDocumentation();
|
|
76
86
|
const topic=await this.ui.select('离线文档', [...topics.map(item=>({value:item.id,label:item.title})),{value:'back',label:'返回工作台'}]);
|
|
77
87
|
if(topic!=='back'){const document=await readDocumentation(topic);await this.ui.viewMarkdown(document.title,document.text);}
|
|
@@ -110,7 +120,12 @@ export class TerminalSession{
|
|
|
110
120
|
if(args.length>1)throw new Error(`用法:/${command.name} [@参数.json]`);
|
|
111
121
|
const input=args.length?await readRunInput(args[0],this.baseDir):command.input?await editValue(this.ui,command.input,{}, {baseDir:this.baseDir,required:true,label:command.description}):{};
|
|
112
122
|
const ui=Object.fromEntries(['input','select','show','confirm'].map(name=>[name,this.ui[name].bind(this.ui)]));
|
|
113
|
-
const
|
|
123
|
+
const definition=this.app.host.definitions.get(command.plugin);
|
|
124
|
+
const requirement=resolvePrivilegeRequirement(definition,command.action);
|
|
125
|
+
const unknown=requirement.declared&&checkStaticPrivilege(requirement).status==='unknown';
|
|
126
|
+
const allowUnknownPrivileges=unknown?await this.ui.confirm(`/${command.name} 的权限需要运行时核查。若检查结果仍无法确认,是否允许本次继续?\n${requirement.reason??''}\n${requirement.remediation??''}`):false;
|
|
127
|
+
if(unknown&&!allowUnknownPrivileges)return;
|
|
128
|
+
const run=await this.app.startCommand(command.name,input,{ui,allowUnknownPrivileges});
|
|
114
129
|
if(this.ui.trackRun){
|
|
115
130
|
this.ui.clearMessages?.();this.ui.trackRun({app:this.app,runId:run.id,title:command.description??command.name,input});
|
|
116
131
|
if(command.action){void run.done.catch(error=>{if(!this.ui.closed)this.ui.show(`运行记录保存失败:${error.message}`);});return;}
|
|
@@ -127,19 +142,22 @@ export class TerminalSession{
|
|
|
127
142
|
const id=selectedId??await this.ui.select('选择要配置的插件',plugins.map(p=>({value:p.id,label:p.title??p.id})));
|
|
128
143
|
const plugin=plugins.find(p=>p.id===id);
|
|
129
144
|
const value=await editValue(this.ui,plugin.config,this.app.project.config?.[id]??{}, {baseDir:this.baseDir,required:true,label:plugin.title??id});
|
|
130
|
-
await this.app.configurePlugin(id,value)
|
|
145
|
+
const result=await this.withMaintenance(options=>this.app.configurePlugin(id,value,options));
|
|
146
|
+
this.ui.show(result===null?'已取消修改。':`插件配置已保存到当前工作区。原项目配置备份:${result.backup}`);
|
|
131
147
|
}
|
|
132
148
|
async logs(){
|
|
133
149
|
const statuses=runStatuses;
|
|
134
150
|
while(true){
|
|
135
151
|
const history=(await this.app.listRunHistory()).sort((a,b)=>(b.status==='running')-(a.status==='running'));
|
|
136
|
-
if(!history.length)
|
|
152
|
+
if(!history.length)this.ui.show('还没有执行记录。运行工作流后,可在 /logs 回看。');
|
|
137
153
|
const titles=new Map((await this.app.listWorkflows()).map(flow=>[flow.id,flow.title??flow.id]));
|
|
138
154
|
const choice=await this.ui.select('执行记录 · 选择一次执行,查看步骤、输入输出与日志',[
|
|
139
155
|
...history.map(r=>({value:`run:${r.id}`,label:`${statuses[r.status]??r.status} · ${titles.get(r.workflowId)??r.workflowId}`,description:`${new Date(r.startedAt).toLocaleString()} · ${r.id.slice(0,8)}`})),
|
|
156
|
+
{value:'usage',label:'查看日志体积',description:'按需统计,不清理文件'},
|
|
140
157
|
{value:'back',label:'返回'},
|
|
141
158
|
]);
|
|
142
159
|
if(choice==='back')return;
|
|
160
|
+
if(choice==='usage'){await this.logUsage();continue;}
|
|
143
161
|
const id=choice.slice(4),record=history.find(r=>r.id===id);
|
|
144
162
|
try{
|
|
145
163
|
if(this.ui.observeRun)await this.openRun(record,{initialView:2});
|
|
@@ -285,9 +303,12 @@ export class TerminalSession{
|
|
|
285
303
|
if(choice==='saved')flow=saved;
|
|
286
304
|
else await this.execute('/save');
|
|
287
305
|
}
|
|
288
|
-
await this.app.checkWorkflow(flow.id);
|
|
306
|
+
const report=await this.app.checkWorkflow(flow.id);
|
|
307
|
+
const unknown=report.privileges.filter(item=>item.requirement.declared&&item.static.status==='unknown');
|
|
308
|
+
const allowUnknownPrivileges=unknown.length?await this.ui.confirm(`这些动作需要在运行时核查权限:${unknown.map(item=>item.action).join('、')}。若仍无法确认,是否允许本次继续?`):false;
|
|
309
|
+
if(unknown.length&&!allowUnknownPrivileges)return false;
|
|
289
310
|
const input=reference?await readRunInput(reference,this.baseDir):(needsJsonEditor(flow.inputs)||Object.keys(flow.inputs?.properties??{}).length)?await editValue(this.ui,flow.inputs,{}, {baseDir:this.baseDir,label:'本次运行参数',required:true}):{};
|
|
290
|
-
const run=await this.app.startRun(flow.id,input);
|
|
311
|
+
const run=await this.app.startRun(flow.id,input,{allowUnknownPrivileges});
|
|
291
312
|
if(this.ui.trackRun){
|
|
292
313
|
this.ui.clearMessages?.();this.ui.trackRun({app:this.app,runId:run.id,title:flow.title??flow.id,input});
|
|
293
314
|
void run.done.catch(error=>{if(!this.ui.closed)this.ui.show(`运行记录保存失败:${error.message}`);});return true;
|
|
@@ -319,20 +340,116 @@ export class TerminalSession{
|
|
|
319
340
|
const keep=this.app.logSettings().keepRuns;
|
|
320
341
|
const choice=await this.ui.select('Fishtools 配置',[
|
|
321
342
|
{value:'info',label:'查看工作区信息'},
|
|
343
|
+
{value:'network',label:'代理设置',description:'分别设置整个工作区、插件或某个动作'},
|
|
344
|
+
{value:'usage',label:'查看日志体积'},
|
|
322
345
|
...(!this.app.project.readOnly?[{value:'logs',label:'日志保留次数',description:keep===null?'不自动清理':`最近 ${keep} 次`}]:[]),
|
|
323
346
|
{value:'back',label:'返回主面板'},
|
|
324
347
|
]);
|
|
325
348
|
if(choice==='back')return;
|
|
326
349
|
if(choice==='info')this.ui.show(`工作区:${this.app.projectDir}\n日志目录:${this.app.outputDir}\n模式:${this.app.project.readOnly?'只读运行包':'可编辑工作区'}`);
|
|
350
|
+
else if(choice==='network')await this.networkSettings();
|
|
351
|
+
else if(choice==='usage')await this.logUsage();
|
|
327
352
|
else await this.logSettings();
|
|
328
353
|
}
|
|
329
354
|
}
|
|
355
|
+
async chooseNetworkRule(label){
|
|
356
|
+
const profiles=Object.keys(this.app.networkPolicy().profiles??{});
|
|
357
|
+
const value=await this.ui.select(label,[
|
|
358
|
+
{value:'inherit',label:'沿用系统代理环境'},
|
|
359
|
+
{value:'direct',label:'直连,不使用代理'},
|
|
360
|
+
...profiles.map(name=>({value:`profile:${name}`,label:`使用代理配置:${name}`})),
|
|
361
|
+
{value:'back',label:'返回'},
|
|
362
|
+
]);
|
|
363
|
+
if(value==='back')return null;
|
|
364
|
+
return value.startsWith('profile:')?{mode:'profile',profile:value.slice(8)}:{mode:value};
|
|
365
|
+
}
|
|
366
|
+
async networkProfiles(){
|
|
367
|
+
while(true){
|
|
368
|
+
const policy=this.app.networkPolicy(),profiles=policy.profiles??{};
|
|
369
|
+
const choice=await this.ui.select('代理配置(地址可用环境变量引用,凭据请勿写入项目)',[
|
|
370
|
+
...Object.keys(profiles).map(name=>({value:`edit:${name}`,label:`修改 ${name}`})),
|
|
371
|
+
...(!this.app.project.readOnly?[{value:'create',label:'+ 新建代理配置'},...Object.keys(profiles).map(name=>({value:`delete:${name}`,label:`删除 ${name}`}))]:[]),
|
|
372
|
+
{value:'back',label:'返回'},
|
|
373
|
+
]);
|
|
374
|
+
if(choice==='back')return;
|
|
375
|
+
this.writable();
|
|
376
|
+
if(choice.startsWith('delete:')){
|
|
377
|
+
const name=choice.slice(7);
|
|
378
|
+
if(await this.ui.confirm(`删除代理配置 ${name}?引用它的规则需要先改为其他模式。`)){
|
|
379
|
+
delete profiles[name];const saved=await this.app.configureNetworkPolicy({...policy,profiles});
|
|
380
|
+
this.ui.show(`代理配置 ${name} 已删除。原项目配置备份:${saved.backup}`);
|
|
381
|
+
}
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
const name=choice==='create'?(await this.ui.input('代理配置名称(字母开头)',{files:false})).trim():choice.slice(5);
|
|
385
|
+
if(!name)throw new Error('代理配置名称不能为空');
|
|
386
|
+
const current=profiles[name]??{},next={};
|
|
387
|
+
for(const [field,label] of [['http','HTTP 请求'],['https','HTTPS 请求'],['noProxy','不走代理的目标(例如 localhost,127.0.0.1)']]){
|
|
388
|
+
const direct=field==='noProxy'?'noProxy':`${field}Url`,reference=`${field}Env`;
|
|
389
|
+
const source=await this.ui.select(`${name} · ${label}`,[{value:'none',label:'不单独设置'},{value:'url',label:field==='noProxy'?'直接填写目标列表':'直接填写代理地址(不能带凭据)'},{value:'env',label:'读取环境变量(可用于有凭据的代理)'}]);
|
|
390
|
+
if(source!=='none'){
|
|
391
|
+
const key=source==='env'?reference:direct;
|
|
392
|
+
next[key]=(await this.ui.input(source==='env'?'环境变量名称':label,{value:current[key]??'',files:false})).trim();
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const saved=await this.app.configureNetworkPolicy({...policy,profiles:{...profiles,[name]:next}});
|
|
396
|
+
this.ui.show(`代理配置 ${name} 已保存。原项目配置备份:${saved.backup}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async networkSettings(){
|
|
400
|
+
while(true){
|
|
401
|
+
const policy=this.app.networkPolicy(),summary=JSON.stringify(policy,null,2);
|
|
402
|
+
const choice=await this.ui.select('代理设置 · 动作规则优先于插件,插件优先于工作区',[
|
|
403
|
+
{value:'view',label:'查看当前规则'},
|
|
404
|
+
...(!this.app.project.readOnly?[{value:'default',label:'设置工作区默认模式'},{value:'plugin',label:'设置某个插件的默认模式'},{value:'action',label:'设置某个动作的模式'}]:[]),
|
|
405
|
+
{value:'profiles',label:'管理代理配置'},
|
|
406
|
+
{value:'back',label:'返回'},
|
|
407
|
+
]);
|
|
408
|
+
if(choice==='back')return;
|
|
409
|
+
if(choice==='view'){
|
|
410
|
+
const report=this.app.networkReport();
|
|
411
|
+
const mode=item=>item.mode==='profile'?`代理配置 ${item.profile}`:item.mode==='direct'?'直连':'继承系统环境';
|
|
412
|
+
const effective=report.entries.map(item=>`${item.id}:${mode(item)}(来源:${{action:'动作',plugin:'插件',workspace:'工作区',system:'系统默认'}[item.source]})`);
|
|
413
|
+
const stale=[...report.stalePlugins.map(id=>`插件 ${id}`),...report.staleActions.map(id=>`动作 ${id}`)];
|
|
414
|
+
this.ui.show(`已保存规则:\n${summary}\n\n最终生效模式:\n${effective.join('\n')||'暂无已安装插件'}\n\n失效引用:\n${stale.join('\n')||'无'}`);
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if(choice==='profiles'){await this.networkProfiles();continue;}
|
|
418
|
+
const next=structuredClone(policy);
|
|
419
|
+
if(choice==='default'){
|
|
420
|
+
const rule=await this.chooseNetworkRule('工作区默认模式');if(!rule)continue;
|
|
421
|
+
next.default=rule;
|
|
422
|
+
}else{
|
|
423
|
+
const installed=this.app.listPlugins();
|
|
424
|
+
if(!installed.length){this.ui.show('当前没有已安装插件。');continue;}
|
|
425
|
+
const id=await this.ui.select('选择插件',installed.map(p=>({value:p.id,label:p.title??p.id})));
|
|
426
|
+
next.plugins??={};next.plugins[id]??={};
|
|
427
|
+
if(choice==='plugin'){
|
|
428
|
+
const rule=await this.chooseNetworkRule(`${id} 默认模式`);if(!rule)continue;
|
|
429
|
+
next.plugins[id].default=rule;
|
|
430
|
+
}else{
|
|
431
|
+
const actions=this.app.catalog().filter(a=>a.plugin===id);
|
|
432
|
+
if(!actions.length){this.ui.show('此插件没有可编排动作。');continue;}
|
|
433
|
+
const action=await this.ui.select('选择动作',actions.map(a=>({value:a.id.slice(id.length+1),label:a.title??a.id})));
|
|
434
|
+
const rule=await this.chooseNetworkRule(`${id}:${action} 模式`);if(!rule)continue;
|
|
435
|
+
next.plugins[id].actions??={};next.plugins[id].actions[action]=rule;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const saved=await this.app.configureNetworkPolicy(next);this.ui.show(`代理规则已保存。原项目配置备份:${saved.backup}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
async logUsage(){
|
|
442
|
+
const usage=await this.app.logUsage(),keep=this.app.logSettings().keepRuns;
|
|
443
|
+
const units=['B','KiB','MiB','GiB','TiB'];let size=usage.bytes,index=0;
|
|
444
|
+
while(size>=1024&&index<units.length-1){size/=1024;index++;}
|
|
445
|
+
this.ui.show(`日志目录:${usage.directory}\n日志文件体积:${index?size.toFixed(2):size} ${units[index]}(${usage.bytes} 字节,${usage.files} 个文件)${usage.incomplete?',部分路径无法读取,统计不完整':''}\n统计该目录中所有运行的 run.md / events.jsonl / result.json,包含进行中的运行;不含业务文件,不跟随链接。共享目录可能包含其他项目。\n这是文件长度快照,运行中可能继续增长;并非磁盘实际分配空间。\n本项目保留策略:${keep===null?'不自动清理':`最近 ${keep} 次已结束运行`};没有单次运行体积上限。`);
|
|
446
|
+
}
|
|
330
447
|
async logSettings(){
|
|
331
448
|
const keep=this.app.logSettings().keepRuns;
|
|
332
449
|
const mode=await this.ui.select('保留策略(整个项目合计,仅清理日志,保留业务文件)',[{value:'count',label:'仅保留最近 N 次已结束运行'},{value:'all',label:'不自动清理'},{value:'back',label:'返回'}]);
|
|
333
450
|
if(mode==='back')return;
|
|
334
451
|
const count=mode==='all'?null:Number(await this.ui.input('保留次数(例如 10)',{value:String(keep??10),files:false}));
|
|
335
|
-
await this.app.configureLogs(count);this.ui.show('
|
|
452
|
+
const saved=await this.app.configureLogs(count);this.ui.show(`日志保留设置已保存。原项目配置备份:${saved.backup}${saved.warning?`\n${saved.warning.message}`:''}`);
|
|
336
453
|
}
|
|
337
454
|
async openRun(entry,{initialView}={}){
|
|
338
455
|
const record=this.app.getRun(entry.id)??await this.app.readRun(entry.id),workflows=await this.app.listWorkflows();
|
|
@@ -358,7 +475,8 @@ export class TerminalSession{
|
|
|
358
475
|
const text=await this.ui.input(action==='npm'?'npm 包名(支持 @scope/name)':'输入 @ 并选择插件目录或 .tgz',{files:action==='local'});
|
|
359
476
|
const source=action==='local'?fileReference(text,this.baseDir):text.trim();if(!source)throw new Error('插件来源不能为空');
|
|
360
477
|
if(!await this.ui.confirm(`安装并加载可信插件:${source}?`))return;
|
|
361
|
-
const done=this.ui.busy('正在安装插件…');try{await this.app.installPlugin(source);}finally{done();}
|
|
478
|
+
const done=this.ui.busy('正在安装插件…');let result;try{result=await this.withMaintenance(options=>this.app.installPlugin(source,options));}finally{done();}
|
|
479
|
+
if(result===null){this.ui.show('已取消安装。');continue;}
|
|
362
480
|
}
|
|
363
481
|
this.ui.show('插件列表已更新。');
|
|
364
482
|
}
|
|
@@ -383,7 +501,17 @@ export class TerminalSession{
|
|
|
383
501
|
else if(choice==='configure')await this.configure(id);
|
|
384
502
|
else if(choice.startsWith('command:'))await this.pluginCommand(commands.find(c=>c.name===choice.slice(8)),[]);
|
|
385
503
|
else if(choice==='remove'){
|
|
386
|
-
this.writable();if(await this.ui.confirm(`移除 ${id}
|
|
504
|
+
this.writable();if(await this.ui.confirm(`移除 ${id}?`)){
|
|
505
|
+
let removed;
|
|
506
|
+
try{removed=await this.withMaintenance(options=>this.app.removePlugin(id,options));}
|
|
507
|
+
catch(error){
|
|
508
|
+
if(error.code!=='PLUGIN_IN_USE'||!error.details?.workflows?.length)throw error;
|
|
509
|
+
const names=error.details.workflows.map(w=>w.title??w.id).join('、');
|
|
510
|
+
if(!await this.ui.confirm(`以下工作流仍使用 ${id}:${names}。移除后这些工作流将无法运行,仍要移除吗?`))continue;
|
|
511
|
+
removed=await this.withMaintenance(options=>this.app.removePlugin(id,options),{force:true});
|
|
512
|
+
}
|
|
513
|
+
if(removed){this.ui.show('插件已移除。');return;}
|
|
514
|
+
}
|
|
387
515
|
}
|
|
388
516
|
}
|
|
389
517
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {stripVTControlCharacters} from 'node:util';
|
|
2
|
+
import {truncateToWidth,visibleWidth} from '@mariozechner/pi-tui';
|
|
3
|
+
|
|
4
|
+
// 先清理外部文本,再由受控主题函数添加颜色;不接受插件传入的 ANSI。
|
|
5
|
+
export const cleanText=value=>stripVTControlCharacters(String(value)).replace(/[\x00-\x08\x0b-\x1f\x7f]/g,'');
|
|
6
|
+
const color=(rgb,text)=>Object.hasOwn(process.env,'NO_COLOR')?text:`\x1b[38;2;${rgb}m${text}\x1b[39m`;
|
|
7
|
+
export const palette={
|
|
8
|
+
brand:text=>color('78;201;176',text),
|
|
9
|
+
error:text=>color('244;117;117',text),
|
|
10
|
+
text:text=>color('229;233;237',text),
|
|
11
|
+
muted:text=>color('139;149;158',text),
|
|
12
|
+
border:text=>color('74;101;99',text),
|
|
13
|
+
};
|
|
14
|
+
export const menuTheme={selectedPrefix:palette.brand,selectedText:palette.brand,description:palette.muted,scrollInfo:palette.muted,noMatch:palette.muted};
|
|
15
|
+
export const fitLine=(text,width)=>truncateToWidth(text,Math.max(1,width),'…');
|
|
16
|
+
|
|
17
|
+
export function shortenPath(value,width){
|
|
18
|
+
if(visibleWidth(value)<=width)return value;
|
|
19
|
+
// 中间省略保留盘符与项目名;完整目录仍在帮助页中展示。
|
|
20
|
+
const chars=[...value],head=chars.splice(0,Math.min(3,Math.max(0,width-2))).join('');
|
|
21
|
+
while(chars.length&&visibleWidth(head+'…'+chars.join(''))>width)chars.shift();
|
|
22
|
+
return fitLine(head+'…'+chars.join(''),width);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function markdownTheme(){
|
|
26
|
+
const plain=text=>text;
|
|
27
|
+
return {heading:palette.brand,link:palette.brand,linkUrl:palette.muted,code:palette.brand,codeBlock:plain,codeBlockBorder:palette.border,quote:plain,quoteBorder:palette.border,hr:palette.border,listBullet:palette.brand,
|
|
28
|
+
bold:palette.text,italic:plain,strikethrough:plain,underline:plain};
|
|
29
|
+
}
|
|
@@ -6,7 +6,7 @@ import { MarkdownLogViewer } from './log-viewer.js';
|
|
|
6
6
|
import { RunViewer, mouseOn, mouseOff, mouseWheel } from './run-viewer.js';
|
|
7
7
|
import {RunFeed} from './run-feed.js';
|
|
8
8
|
import { logo } from './presentation.js';
|
|
9
|
-
import { VERSION } from '
|
|
9
|
+
import { VERSION } from '@mytools/core/version';
|
|
10
10
|
import {cleanText as safe,palette,menuTheme as theme,fitLine,shortenPath} from './theme.js';
|
|
11
11
|
|
|
12
12
|
export const cancelled=()=>Object.assign(new Error('已返回主菜单'),{code:'PROMPT_CANCELLED'});
|
|
@@ -191,13 +191,13 @@ export class TerminalView{
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
async observeRun({app,runId,record,flow,workflows=[],initialView}){
|
|
194
|
-
const previousPending=this.pending??null,children=this.tui.children;let overlay,unsubscribe;
|
|
194
|
+
const previousPending=this.pending??null,children=this.tui.children;let overlay,unsubscribe,readPages=false;
|
|
195
195
|
const initial=app.getRun(runId)??record??await app.readRun(runId);
|
|
196
196
|
if(!initial)throw new Error('这次运行记录已不存在');
|
|
197
197
|
try{
|
|
198
198
|
await new Promise((resolve,reject)=>{
|
|
199
199
|
this.pending={resolve,reject};
|
|
200
|
-
this.runViewer=new RunViewer({record:initial,flow:initial.status==='running'?flow:undefined,workflows,catalog:app.catalog?.()??[],terminal:this.terminal,onClose:resolve,onCancel:()=>app.cancelRun(runId),requestRender:()=>this.tui.requestRender()});
|
|
200
|
+
this.runViewer=new RunViewer({record:initial,flow:initial.status==='running'?flow:undefined,workflows,catalog:app.catalog?.()??[],terminal:this.terminal,onClose:resolve,onCancel:()=>app.cancelRun(runId),onReadLogs:app.readLogPage?()=>{readPages=true;resolve();}:undefined,requestRender:()=>this.tui.requestRender()});
|
|
201
201
|
Object.assign(this.runViewer,this.runViews.get(runId)??{});
|
|
202
202
|
if(initialView!==undefined){this.runViewer.view=initialView;this.runViewer.offset=0;}
|
|
203
203
|
unsubscribe=app.subscribe(event=>{if(event.runId===runId)this.runViewer?.update(app.getRun(runId));});
|
|
@@ -209,6 +209,23 @@ export class TerminalView{
|
|
|
209
209
|
if(this.runViews.size>30)this.runViews.delete(this.runViews.keys().next().value);
|
|
210
210
|
unsubscribe?.();this.runViewer=null;this.tui.children=children;this.pending=previousPending;overlay?.hide();this.terminal.write(mouseOff);this.tui.requestRender(true);
|
|
211
211
|
}
|
|
212
|
+
if(readPages)await this.viewLogPages(app,runId);
|
|
213
|
+
}
|
|
214
|
+
async viewLogPages(app,id){
|
|
215
|
+
let cursor=0;const previous=[];
|
|
216
|
+
for(;;){
|
|
217
|
+
const page=await app.readLogPage(id,{cursor,limit:65536});
|
|
218
|
+
await this.viewMarkdown(`完整日志 · ${cursor}–${page.nextCursor} / ${page.totalBytes} 字节`,page.text||'本页暂无内容');
|
|
219
|
+
const choice=await this.select('完整日志分页',[
|
|
220
|
+
...(page.hasMore?[{value:'next',label:'下一页'}]:[]),
|
|
221
|
+
...(previous.length?[{value:'previous',label:'上一页'}]:[]),
|
|
222
|
+
{value:'refresh',label:'刷新本页'},{value:'first',label:'回到首页'},{value:'back',label:'返回'},
|
|
223
|
+
]);
|
|
224
|
+
if(choice==='back')return;
|
|
225
|
+
if(choice==='next'){previous.push(cursor);if(previous.length>1000)previous.shift();cursor=page.nextCursor;}
|
|
226
|
+
if(choice==='previous')cursor=previous.pop()??0;
|
|
227
|
+
if(choice==='first'){cursor=0;previous.length=0;}
|
|
228
|
+
}
|
|
212
229
|
}
|
|
213
230
|
async ask(message,component){
|
|
214
231
|
fit(component);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export async function workflowMenu(session){
|
|
2
|
+
const {app,ui}=session;
|
|
3
|
+
while(true){
|
|
4
|
+
const workflows=await app.listWorkflows();
|
|
5
|
+
const choice=await ui.select('工作流 · 选择后编辑或执行',[
|
|
6
|
+
...workflows.map(flow=>({value:`workflow:${flow.id}`,label:flow.title??flow.id,description:`${flow.id} · ${flow.steps.length} 步`})),
|
|
7
|
+
...(!app.project.readOnly?workflows.map(flow=>({value:`duplicate:${flow.id}`,label:`复制 ${flow.title??flow.id} 为新工作流`,description:`从已保存版本 ${flow.id} 创建副本` })):[]),
|
|
8
|
+
...(!app.project.readOnly?[{value:'new',label:'+ 新建工作流'}]:[]),
|
|
9
|
+
...(session.dirty?[{value:'draft',label:`继续编辑草稿:${session.flow.title}`}]:[]),
|
|
10
|
+
{value:'back',label:'返回主面板'},
|
|
11
|
+
]);
|
|
12
|
+
if(choice==='back')return;
|
|
13
|
+
try{
|
|
14
|
+
const id=choice.startsWith('workflow:')?choice.slice(9):null;
|
|
15
|
+
if(choice.startsWith('duplicate:')){
|
|
16
|
+
if(!await session.discard())continue;
|
|
17
|
+
await duplicateIntoSession(session,workflows.find(flow=>flow.id===choice.slice(10)));
|
|
18
|
+
}
|
|
19
|
+
else if(choice==='new'){if(await session.execute('/new')===false)continue;}
|
|
20
|
+
else if(choice!=='draft'&&session.flow?.id!==id){
|
|
21
|
+
if(!await session.discard())continue;
|
|
22
|
+
session.flow=structuredClone(workflows.find(flow=>flow.id===id));session.dirty=false;
|
|
23
|
+
}
|
|
24
|
+
if(await editWorkflow(session)==='running')return;
|
|
25
|
+
}catch(error){if(error.code!=='PROMPT_CANCELLED')throw error;}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function editWorkflow(session){
|
|
30
|
+
const {app,ui}=session;
|
|
31
|
+
while(true){
|
|
32
|
+
const flow=session.current();
|
|
33
|
+
const choice=await ui.select(`${flow.title??flow.id} · ${flow.steps.length} 步 · ${session.dirty?'未保存':'已保存'}`, [
|
|
34
|
+
{value:'run',label:'运行工作流'},
|
|
35
|
+
...(!app.project.readOnly?[
|
|
36
|
+
{value:'add',label:'添加步骤 / 控制流'}, {value:'steps',label:'编辑步骤、参数与顺序'},
|
|
37
|
+
{value:'outputs',label:'设置最终输出'},{value:'save',label:'保存工作流'},
|
|
38
|
+
{value:'duplicate',label:'复制为新工作流(包含当前未保存修改)'},
|
|
39
|
+
{value:'delete',label:'删除工作流(保留可恢复备份)'},
|
|
40
|
+
]:[{value:'steps',label:'查看步骤'}]),
|
|
41
|
+
{value:'back',label:'返回工作流列表(保留草稿)'},
|
|
42
|
+
]);
|
|
43
|
+
if(choice==='back')return;
|
|
44
|
+
try{
|
|
45
|
+
if(choice==='delete'){
|
|
46
|
+
if(!await ui.confirm(`删除 ${flow.title??flow.id}?已保存文件会移入备份,未保存修改将放弃。`))continue;
|
|
47
|
+
const exists=(await app.listWorkflows()).some(w=>w.id===flow.id);
|
|
48
|
+
const backup=exists?await app.removeWorkflow(flow.id):null;
|
|
49
|
+
session.flow=null;session.dirty=false;ui.show(backup?`工作流已移入备份:${backup}`:'未保存的草稿已删除。');return;
|
|
50
|
+
}
|
|
51
|
+
if(choice==='run'){
|
|
52
|
+
if(await session.startRun([],{draft:true}))return 'running';
|
|
53
|
+
}else if(choice==='duplicate')await duplicateIntoSession(session,flow);
|
|
54
|
+
else await session.execute('/'+choice);
|
|
55
|
+
}catch(error){if(error.code!=='PROMPT_CANCELLED')throw error;}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function duplicateIntoSession(session,source){
|
|
60
|
+
const title=(await session.ui.input('新工作流名称',{value:`${source.title??source.id} 副本`,files:false})).trim();
|
|
61
|
+
if(!title)throw new Error('名称不能为空');
|
|
62
|
+
const {definition}=await session.app.duplicateWorkflow(source,{title});
|
|
63
|
+
session.flow=structuredClone(definition);session.dirty=false;
|
|
64
|
+
session.ui.show(`已创建副本:${definition.title}(${definition.id})。原工作流 ${source.id} 未修改。可在父工作流中依次加入原版和副本两个子流程步骤,比较输出和耗时。`);
|
|
65
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mytools contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|