@yanglaofish/fishtools 0.5.0 → 0.5.1
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/manual/docs/releases.md +12 -0
- package/manual/docs/superpowers/reviews/2026-10-13-0.5.0-followup-review.md +67 -0
- package/manual/docs/verification.md +14 -1
- package/manual/packages/core/src/application.js +12 -3
- package/manual/packages/core/src/version.js +1 -1
- package/manual/packages/fst-cli/README.md +6 -1
- package/manual/packages/fst-cli/package.json +1 -0
- package/manual/packages/fst-cli/src/terminal/run-feed.js +24 -9
- package/manual/packages/fst-logs/README.md +11 -0
- package/manual/packages/fst-logs/package.json +2 -0
- package/manual/packages/fst-logs/src/run-logs.js +8 -23
- package/manual/packages/fst-logs/src/run-preview.js +18 -0
- package/manual/packages/fst-logs/src/step-summaries.js +28 -0
- package/manual/packages/fst-platform/README.md +6 -0
- package/manual/packages/fst-platform/src/process.js +2 -2
- package/manual/packages/fst-plugin/src/host.js +7 -4
- package/manual/packages/fst-plugin/src/runtime.js +8 -5
- package/node_modules/@mytools/core/src/application.js +12 -3
- package/node_modules/@mytools/core/src/version.js +1 -1
- package/node_modules/@mytools/fst-cli/README.md +6 -1
- package/node_modules/@mytools/fst-cli/package.json +1 -0
- package/node_modules/@mytools/fst-cli/src/terminal/run-feed.js +24 -9
- package/node_modules/@mytools/fst-logs/README.md +11 -0
- package/node_modules/@mytools/fst-logs/package.json +2 -0
- package/node_modules/@mytools/fst-logs/src/run-logs.js +8 -23
- package/node_modules/@mytools/fst-logs/src/run-preview.js +18 -0
- package/node_modules/@mytools/fst-logs/src/step-summaries.js +28 -0
- package/node_modules/@mytools/fst-platform/README.md +6 -0
- package/node_modules/@mytools/fst-platform/src/process.js +2 -2
- package/node_modules/@mytools/fst-plugin/src/host.js +7 -4
- package/node_modules/@mytools/fst-plugin/src/runtime.js +8 -5
- package/node_modules/@mytools/web/public/THIRD_PARTY_NOTICES.txt +1416 -1415
- package/package.json +1 -1
package/manual/docs/releases.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
产品名称 Fishtools,公共 npm 包名 `@yanglaofish/fishtools`,启动命令 `fst`。`0.x` 定位为早期可用版本。内部 `@mytools/*` 包随主包捆绑,不单独发布;内部插件 API 版本独立维护。
|
|
4
4
|
|
|
5
|
+
## 0.5.1 复核修正
|
|
6
|
+
|
|
7
|
+
- 动作选择直连时,外部子进程不再重新继承系统代理;所选代理配置也不会被宿主残留变量干扰。
|
|
8
|
+
- 插件更新遇到旧资源或失败的新资源清理失败时,不再重新启动另一份资源;应用明确要求重启。
|
|
9
|
+
- TUI 大型并行或子流程的步骤摘要淘汰后可安全显示,不再读取缺失状态报错。
|
|
10
|
+
步骤缓存与最终结果、输入、错误均使用 logs 模块的有界预览;完整日志和业务返回值保持完整。
|
|
11
|
+
- 非 root 用户可正常执行会提前退出的工作流,分支、并行和子流程内的 stop 不再引起后续权限误拦。
|
|
12
|
+
真正到达需要 root 的动作时仍会在执行前阻止。
|
|
13
|
+
|
|
14
|
+
工作流和插件格式保持兼容。本轮基于 0.4.0 至 0.5.0 的独立双轴复核,详见
|
|
15
|
+
[审查记录](superpowers/reviews/2026-10-13-0.5.0-followup-review.md)。发布结果以 GitHub Release 与[验收记录](verification.md)为准。
|
|
16
|
+
|
|
5
17
|
## 0.5.0 内核能力与日常操作
|
|
6
18
|
|
|
7
19
|
- 大量进程输出可以完整落盘、内存只保留有限摘要;CLI、TUI、Web 支持分页查看完整日志。开发引用插件可用 `--watch` 在业务空闲后重启完整进程;网络挂载文件探测在隔离进程中执行并设置超时与资源预算。
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# 0.5.0 全代际代码复核与修正
|
|
2
|
+
|
|
3
|
+
用户要求使用 `code-review` 再次审查并立即修正问题,沿用验证后推送和发包的授权。
|
|
4
|
+
固定基线为 0.4.0 提交 `f5292449afcb28adfa98f19ce6586e0a72846080`,审查版本为
|
|
5
|
+
`d26a0eecc791e50211f8a5cf89c6235413659688`,独立分支 `codex/review-0.5.0-followup`。
|
|
6
|
+
比较命令:`git diff f5292449afcb28adfa98f19ce6586e0a72846080...d26a0eecc791e50211f8a5cf89c6235413659688`。
|
|
7
|
+
|
|
8
|
+
此范围覆盖 0.4.0 之后的十一模块拆分、完整日志、开发重载、隔离探测、代理、权限、复制删除与定向维护。
|
|
9
|
+
两个独立只读审查分别依据仓库模块规范与已批准设计;没有用测试通过代替代码阅读。
|
|
10
|
+
|
|
11
|
+
## Standards
|
|
12
|
+
|
|
13
|
+
原审查报告 2 项,修补复审另确认 1 项,共 3 项,最高 P1;没有确认需要单独处理的新增 code smell。
|
|
14
|
+
|
|
15
|
+
1. **P1:TUI 主面板在摘要淘汰后崩溃。** 固定版本 `fst-cli/src/terminal/run-feed.js:38/62/78`
|
|
16
|
+
更新旧步骤时不刷新 Map 顺序。`a.started、b.started、a.succeeded、c.started` 在两条预算下
|
|
17
|
+
淘汰了 a,但事件窗口仍有 a;渲染读取不存在的 `latest.action` 抛出 TypeError。
|
|
18
|
+
违反 `docs/kernel-generation.md:41` 与 `fst-cli/README.md:54` 的有界显示和省略状态规则。
|
|
19
|
+
2. **P2:TUI 步骤缓存绕过字节预算。** 同一实现缓存整条事件和 input/output,仅限制条数。
|
|
20
|
+
50 条约 60 KB 输出会形成约 3 MB 步骤缓存,而事件窗口已限到约 1 MB;历史运行会继续累计。
|
|
21
|
+
违反 `docs/kernel-generation.md:40` 与 `fst-logs/README.md:36` 的轻量摘要及双预算规则。
|
|
22
|
+
3. **P2:TUI 完成后仍保存完整大结果。** 修补复审通过真实应用确认,动作返回 2 MiB 字符串,
|
|
23
|
+
core 已释放大结果缓存,磁盘已保存摘要,但主面板 record.output 仍保存 2,097,163 字节,超过 1 MiB 预算。
|
|
24
|
+
同样违反有界主面板缓存要求;运行输入与错误等字段也应使用 logs 提供的结果投影。
|
|
25
|
+
|
|
26
|
+
## Spec
|
|
27
|
+
|
|
28
|
+
原审查报告共 3 项,最高 P1;没有发现明确的无需求范围扩大。
|
|
29
|
+
|
|
30
|
+
1. **P1:直连子进程重新继承宿主代理。** 固定版本 `fst-platform/src/process.js:37` 使用 Execa
|
|
31
|
+
默认环境合并,将网络规则已删除的 HTTP_PROXY 等重新引入;Linux 小写变量也可能干扰 profile。
|
|
32
|
+
违反 `action-network-policy-design.md:42` 的 direct 清理与 profile 注入约定。
|
|
33
|
+
2. **P1:旧 runtime 清理失败仍被重新启动。** `fst-plugin/src/host.js:169/170` 清空生命周期错误后
|
|
34
|
+
恢复旧作用域。清理抛错而未释放资源时,starts/live 从 1/1 增为 2/2,动作仍可调用。
|
|
35
|
+
违反 `runtime-maintenance-granularity-design.md:27/29` 的等待清理及半更新不可运行约定。
|
|
36
|
+
3. **P2:提前结束后的 root 动作误判为必经。** `core/src/application.js:309-313` 不传播嵌套分支、
|
|
37
|
+
并行和子流程的 stop。普通 Linux 用户的流程本应安全停止,却在开始前报权限不足。
|
|
38
|
+
违反 `plugin-readiness-design.md:11`:可能跳过的动作提示,实际执行前检查。
|
|
39
|
+
|
|
40
|
+
## 修正与回归
|
|
41
|
+
|
|
42
|
+
- 代理:增加明确的 `extendEnv` 控制,普通 runProcess 保留默认继承;插件宿主构造完整环境后禁止再次扩展。
|
|
43
|
+
真实子进程验证 direct、profile、inherit 并发隔离,宿主环境不变;修复前 direct 回归确实失败。
|
|
44
|
+
- runtime:启动错误与清理错误分开记录,只允许清理成功后的启动失败恢复。
|
|
45
|
+
旧资源或失败的新资源清理失败时,停止恢复,宿主标记不可运行。
|
|
46
|
+
两个真实生命周期回归修复前都出现二次启动,修复后保持一次;正常配置恢复仍通过。
|
|
47
|
+
- TUI:由 logs 的公开 `/steps` 入口统一提供轻量、按最近更新排序且有双预算的缓存,日志写入器也复用。
|
|
48
|
+
缺失摘要显示省略提示,保留事件中可查询的错误;主面板不在步骤缓存中复制业务正文。
|
|
49
|
+
交错步骤与大输出两个回归在修复前分别报 TypeError、越过预算。
|
|
50
|
+
追加 `/preview` 入口,历史元数据与主面板共用有界结果投影;裁去不使用的步骤快照副本。
|
|
51
|
+
真实 2 MiB 输入/输出回归在修复前确认主面板超预算,修复后只留摘要;业务 done 与 JSONL run.result 仍保留全文。
|
|
52
|
+
- 权限:嵌套 stop 传播到后续步骤;可能被 stop 取消的并行兄弟也不当作必经。
|
|
53
|
+
隔离测试进程模拟 Linux UID 1000,验证分支、并行和子流程正常停止,以及真正执行时仍拒绝 root 动作。
|
|
54
|
+
无条件 root 动作保持启动前拦截;修复前嵌套步骤的 mandatory 断言失败。
|
|
55
|
+
|
|
56
|
+
## 修补后的独立复审与完整验证
|
|
57
|
+
|
|
58
|
+
- Spec 独立复审确认 3 项修正符合设计:完整代理环境、清理失败不可恢复、stop 的权限传播;没有遗留或新增阻断问题。
|
|
59
|
+
- Standards 独立复审确认步骤缓存与结果投影都已受限,新 exports、依赖声明和锁文件同步;没有遗留或新增阻断问题。
|
|
60
|
+
- 定向验证覆盖真实子进程、真实插件生命周期、真实 2 MiB 业务结果与完整 JSONL,以及隔离普通用户工作流。
|
|
61
|
+
每项主修复都先执行失败回归,再修正并通过;不是仅增加会通过的测试。
|
|
62
|
+
- `npm run dist:npm` 成功,0.5.1 本地归档 9,757,761 字节、284 个生产包。
|
|
63
|
+
使用该实际归档设置 `FISHTOOLS_TEST_NPM_ARCHIVE` 后,完整 `npm test` 382 项:381 通过、0 失败、1 跳过,166.0 秒。
|
|
64
|
+
跳过项为 Windows 当前无文件 symlink 权限,目录 junction 测试实际执行;离线安装、脱离源码运行与再次导出通过。
|
|
65
|
+
- 发布 tag 与 CLI/package 版本同步检查通过。远端双平台、便携包和 npm 发布结果在[验收记录](../../verification.md)中补充。
|
|
66
|
+
|
|
67
|
+
汇总:Standards 3 项(最高 P1),Spec 3 项(最高 P1),6 项均已修正;修补复审无剩余阻断问题。
|
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
# 验收记录
|
|
2
2
|
|
|
3
|
+
## 0.5.1 全代际复核修正(2026-10-13)
|
|
4
|
+
|
|
5
|
+
- 分支 `codex/review-0.5.0-followup`,以 0.4.0 `f529244` 至 0.5.0 后验收提交 `d26a0ee` 为固定范围执行独立 Standards / Spec 审查。
|
|
6
|
+
共确认 6 项问题:直连子进程重新继承代理、清理失败后重复启动资源、嵌套 stop 的权限误拦、TUI 摘要淘汰崩溃、步骤缓存超预算、最终结果缓存超预算。
|
|
7
|
+
均先失败复现再修正;修补后的两个独立复审无剩余阻断问题,详见[审查记录](superpowers/reviews/2026-10-13-0.5.0-followup-review.md)。
|
|
8
|
+
- logs 提供统一步骤缓存与结果投影,TUI 不重新持有完整大正文;真实 2 MiB 输入/输出验证业务 done 与完整 JSONL 不受影响。
|
|
9
|
+
实际子进程验证三种代理模式、正常/失败清理恢复与 Linux 普通用户嵌套 stop 的执行边界。
|
|
10
|
+
- `npm run dist:npm` 成功,0.5.1 本地归档 9,757,761 字节、284 个生产包;用该归档执行完整 `npm test` **382 项:381 通过、0 失败、1 跳过**,166.0 秒。
|
|
11
|
+
包括离线全局安装、脱离源码运行与再次导出。Windows 文件 symlink 权限导致一项跳过,目录 junction 回归执行。
|
|
12
|
+
- 产品和 CLI 同步为 0.5.1,保持工作流与内部插件 API 格式兼容。远端验证和发布尚待执行,完成后追加记录。
|
|
13
|
+
|
|
3
14
|
## 0.5.0 发布前验证(2026-10-13)
|
|
4
15
|
|
|
5
16
|
- 分支 `codex/kernel-runtime-readiness`,固定基线 `002260b`。完成动作级代理、权限预检、工作流复制与删除、引用保护、定向插件更新及恢复;双轴审查与修复见[审查记录](superpowers/reviews/2026-10-13-kernel-runtime-release-review.md)。
|
|
6
17
|
- 审查补齐项目配置及工作流覆盖保存的备份、卸载期间的并发保护、恢复失败后的停止运行、代理实际生效模式和失效引用提示,以及权限不足时的失败日志。原有宿主关闭后能力列表行为已恢复。
|
|
7
18
|
- Windows Node 24.15.0 上 `npm run dist:npm` 成功;最终 npm 归档 `dist/yanglaofish-fishtools-0.5.0.tgz` 为 9,750,617 字节,收集 284 个生产包。Web 构建成功;仅有第三方 `use client` 指令提示。
|
|
8
19
|
- 设置 `FISHTOOLS_TEST_NPM_ARCHIVE` 指向最终归档后运行完整 `npm test`,**374 项:373 通过、0 失败、1 跳过**,164.2 秒。覆盖 npm 离线全局安装、脱离源码运行、再次导出、代理规则、权限、备份与并发卸载。跳过的是本机未开放文件符号链接权限的 Windows 用例;目录 junction 仍执行。
|
|
9
|
-
-
|
|
20
|
+
- [v0.5.0 发布工作流](https://github.com/yanglaofish/Fishtools/actions/runs/38282818908)最终全部成功:Windows **374/374**,Ubuntu **373/374**、1 项跳过;两平台便携包与无 Node 的断网只读 Linux 容器验收通过。
|
|
21
|
+
- npm 官方源已公开 `@yanglaofish/fishtools@0.5.0`,`latest` 指向 0.5.0;[GitHub Release](https://github.com/yanglaofish/Fishtools/releases/tag/v0.5.0)已公开,包含 npm、Windows、Linux 归档及 `SHA256SUMS`。
|
|
22
|
+
- npm 首次接受上传后处理超过发布脚本的 5 分钟等待上限;注册表可见后仅重试失败的发布任务,归档完整性匹配,随后完成 Release,未重复创建版本。
|
|
10
23
|
|
|
11
24
|
## 完整内核能力演进(2026-10-12,本地未发布)
|
|
12
25
|
|
|
@@ -293,7 +293,15 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
|
|
|
293
293
|
if(active.length)throw new FlowError(`请先停止受影响插件提供的 Web:${active.join('、')}`,{code:'WEB_ACTIVE',details:{plugins:active}});
|
|
294
294
|
}
|
|
295
295
|
function workflowPrivileges(closure,entryId){
|
|
296
|
-
const flows=new Map(closure.workflows.map(w=>[w.id,w])),report=[];
|
|
296
|
+
const flows=new Map(closure.workflows.map(w=>[w.id,w])),report=[],stoppingFlows=new Map();
|
|
297
|
+
const mayStopFlow=id=>{
|
|
298
|
+
if(!stoppingFlows.has(id))stoppingFlows.set(id,(flows.get(id)?.steps??[]).some(mayStopStep));
|
|
299
|
+
return stoppingFlows.get(id);
|
|
300
|
+
};
|
|
301
|
+
const mayStopStep=step=>!!step.stop
|
|
302
|
+
||!!step.workflow&&mayStopFlow(store.childId(step.workflow))
|
|
303
|
+
||!!step.branch&&[...step.branch.then,...(step.branch.else??[])].some(mayStopStep)
|
|
304
|
+
||!!step.parallel&&step.parallel.steps.some(mayStopStep);
|
|
297
305
|
const visit=(id,mandatory)=>{
|
|
298
306
|
const workflow=flows.get(id);
|
|
299
307
|
if(!workflow)return;
|
|
@@ -308,8 +316,9 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
|
|
|
308
316
|
}
|
|
309
317
|
if(step.workflow)visit(store.childId(step.workflow),needed);
|
|
310
318
|
if(step.branch){steps(step.branch.then,false);steps(step.branch.else??[],false);}
|
|
311
|
-
|
|
312
|
-
if(step.
|
|
319
|
+
// 并行兄弟可能在动作开始前停止整组;嵌套 stop 也会传播到后续步骤。
|
|
320
|
+
if(step.parallel)steps(step.parallel.steps,needed&&!step.parallel.steps.some(mayStopStep));
|
|
321
|
+
if(mayStopStep(step))available=false;
|
|
313
322
|
}
|
|
314
323
|
};
|
|
315
324
|
steps(workflow.steps,mandatory);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// 由 npm version 的生命周期脚本同步,内部插件 API 版本独立维护。
|
|
2
|
-
export const VERSION="0.5.
|
|
2
|
+
export const VERSION="0.5.1";
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
fst 命令入口、交互终端、输入呈现和运行观察。通过公开应用和模块接口工作,业务执行不在界面中重复实现。
|
|
8
8
|
|
|
9
|
-
内部依赖:`@mytools/sdk`、`@mytools/core`、`@mytools/fst-workspace`、`@mytools/fst-platform`、`@mytools/fst-plugin`、`@mytools/fst-devtools`。
|
|
9
|
+
内部依赖:`@mytools/sdk`、`@mytools/core`、`@mytools/fst-workspace`、`@mytools/fst-platform`、`@mytools/fst-plugin`、`@mytools/fst-devtools`、`@mytools/fst-logs`。
|
|
10
10
|
## 公开入口
|
|
11
11
|
|
|
12
12
|
| 导入路径 | 实现位置 |
|
|
@@ -52,3 +52,8 @@ node --test tests/modules.test.js tests/cli.test.js tests/terminal.test.js
|
|
|
52
52
|
## 完整日志分页
|
|
53
53
|
|
|
54
54
|
`fst logs` 列出历史,`fst logs <id> --cursor 0 --limit 65536 --json` 返回正文和下一页字节游标。TUI 运行详情按 `p` 打开完整日志,每页读取后可前后翻页或刷新。主面板保留有界近期摘要;日志清理失败显示维护警告。细节见 `fst docs generation`。
|
|
55
|
+
|
|
56
|
+
主面板通过 `fst-logs/steps` 共用步骤摘要缓存,按最近更新顺序淘汰,同时限制条数和字节量。
|
|
57
|
+
摘要不保存业务 input/output;缺失的状态显示“摘要已省略”,已有错误仍可查看。
|
|
58
|
+
运行输入、最终结果与错误通过 `fst-logs/preview` 保存有界预览,避免业务完成后又缓存完整大结果。
|
|
59
|
+
展开时明确提示结果为摘要,完整结果仍在日志中;不会替换业务调用方取得的完整输出。
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import {readableValue,readableError,readableTimeline} from './run-presentation.js';
|
|
2
2
|
import {runStatuses} from './run-viewer.js';
|
|
3
|
+
import {createStepSummaries} from '@mytools/fst-logs/steps';
|
|
4
|
+
import {previewRun} from '@mytools/fst-logs/preview';
|
|
5
|
+
|
|
6
|
+
const feedRecord=(record,maxBytes)=>{
|
|
7
|
+
const {stepSummaries,stepSummaryBytes,stepsTruncated,...state}=record;
|
|
8
|
+
return previewRun({...state,events:[...(state.events??[])]},{maxValueBytes:Math.min(maxBytes,65536)});
|
|
9
|
+
};
|
|
3
10
|
|
|
4
11
|
// 主面板按事件到达顺序追加;运行快照另存,配置菜单不会清掉执行记录。
|
|
5
12
|
export class RunFeed{
|
|
6
13
|
constructor(refresh,{maxEvents=1000,maxBytes=1048576,maxRuns=50}={}){Object.assign(this,{refresh,maxEvents,maxBytes,maxRuns});this.runs=new Map();this.entries=[];this.subscriptions=new Map();this.expanded=false;this.dropped=0;}
|
|
7
14
|
track({app,runId,title,input}){
|
|
8
15
|
if(this.runs.has(runId))return;
|
|
9
|
-
const
|
|
16
|
+
const raw=app.getRun(runId),record=feedRecord(raw,this.maxBytes),bounded=previewRun({input},{maxValueBytes:Math.min(this.maxBytes,65536)});
|
|
17
|
+
const item={app,record,title,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]))};
|
|
18
|
+
for(const summary of raw.stepSummaries??[])item.steps.append(summary);
|
|
10
19
|
this.runs.set(runId,item);
|
|
11
20
|
this.entries.push({type:'start',item});
|
|
12
21
|
if(!this.subscriptions.has(app))this.subscriptions.set(app,app.subscribe(event=>{
|
|
@@ -30,12 +39,13 @@ export class RunFeed{
|
|
|
30
39
|
if(event?.type&&!['run.succeeded','run.failed','run.cancelled','run.stopped'].includes(event.type)){item.record.events.push(event);events=[event];item.count++;}
|
|
31
40
|
else{
|
|
32
41
|
const record=item.app.getRun(item.record.id);if(!record)return;
|
|
33
|
-
item.record=
|
|
42
|
+
item.record=feedRecord(record,this.maxBytes);
|
|
43
|
+
for(const summary of record.stepSummaries??[])item.steps.append(summary);
|
|
34
44
|
events=record.events.filter((e,index)=>e.seq!==undefined?e.seq>item.count:index>=item.count);
|
|
35
45
|
item.count=record.eventCount??record.events.length;
|
|
36
46
|
}
|
|
37
47
|
const {record}=item;
|
|
38
|
-
for(const event of events)
|
|
48
|
+
for(const event of events)item.steps.append(event);
|
|
39
49
|
if(events.length){
|
|
40
50
|
const last=this.entries.at(-1);
|
|
41
51
|
if(last?.type==='events'&&last.item===item)last.events.push(...events);
|
|
@@ -59,7 +69,6 @@ export class RunFeed{
|
|
|
59
69
|
this.entries=this.entries.filter(entry=>!entry.events||entry.events.length);
|
|
60
70
|
for(const item of this.runs.values()){
|
|
61
71
|
item.record.events=this.entries.filter(e=>e.item===item&&e.events).flatMap(e=>e.events);
|
|
62
|
-
while(item.steps.size>this.maxEvents)item.steps.delete(item.steps.keys().next().value);
|
|
63
72
|
}
|
|
64
73
|
const ended=[...this.runs.values()].filter(item=>item.ended);
|
|
65
74
|
for(const item of ended.slice(0,Math.max(0,ended.length-this.maxRuns))){this.runs.delete(item.record.id);this.entries=this.entries.filter(e=>e.item!==item);this.dropped++;}
|
|
@@ -75,10 +84,14 @@ export class RunFeed{
|
|
|
75
84
|
for(const event of entry.events){
|
|
76
85
|
const key=`${item.record.id}\0${event.callPath}`;
|
|
77
86
|
if(event.type.startsWith('step.')&&!seen.has(key)){
|
|
78
|
-
seen.add(key);const latest=item.steps.get(event.callPath)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
87
|
+
seen.add(key);const latest=item.steps.get(event.callPath);
|
|
88
|
+
if(!latest)rows.push(`· ${event.action??event.stepId??event.callPath} · 摘要已省略 · ${event.callPath}`);
|
|
89
|
+
else{
|
|
90
|
+
const action=item.catalog.get(latest.action);
|
|
91
|
+
const status=latest.type.slice(5),symbols={started:'●',succeeded:'✓',failed:'✗',cancelled:'×',skipped:'–',stopped:'■'};
|
|
92
|
+
const name=action?.title??latest.action??latest.stepId??latest.callPath;
|
|
93
|
+
rows.push(`${symbols[status]??'·'} ${name} · ${status==='started'?'执行中':runStatuses[status]??status}${latest.durationMs!==undefined?' · '+latest.durationMs+' ms':''}${action?.plugin?' · '+action.plugin:''} · ${latest.callPath}`);
|
|
94
|
+
}
|
|
82
95
|
}
|
|
83
96
|
if(event.type==='log'&&event.audience==='user')rows.push(`[${event.callPath??item.record.workflowId}] ${event.message??''}${event.data!==undefined?'\n'+readableValue(event.data):''}`);
|
|
84
97
|
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})}`);
|
|
@@ -86,18 +99,20 @@ export class RunFeed{
|
|
|
86
99
|
}
|
|
87
100
|
return rows.length?`${item.title??item.record.workflowId} · ${item.record.id.slice(0,8)}\n${rows.join('\n')}`:'';
|
|
88
101
|
}
|
|
89
|
-
text(){const seen=new Set();return (this.dropped?'
|
|
102
|
+
text(){const seen=new Set();return (this.dropped?'主面板仅保留近期摘要;超出预算的事件和步骤摘要已省略。完整日志可在 /logs 中按 p 分页阅读。\n\n':'')+this.entries.map(entry=>{
|
|
90
103
|
if(entry.type==='message')return entry.text;
|
|
91
104
|
const {record,title,input}=entry.item,label=`${title??record.workflowId} · ${record.id.slice(0,8)}`;
|
|
92
105
|
if(entry.type==='start')return [
|
|
93
106
|
`› ${record.workflowId.startsWith('/')?record.workflowId:'/run '+record.workflowId} · ${label}`,
|
|
94
107
|
`状态:${runStatuses[record.status]??record.status}`,
|
|
95
108
|
this.expanded&&input&&Object.keys(input).length?'输入\n'+readableValue(input):'',
|
|
109
|
+
this.expanded&&entry.item.inputTruncated?'输入较大,主面板仅显示摘要。':'',
|
|
96
110
|
].filter(Boolean).join('\n');
|
|
97
111
|
if(entry.type==='events')return this.expanded?`${label}\n${readableTimeline(entry.events,{values:true})}`:this.compactEvents(entry,seen);
|
|
98
112
|
return [`结果 · ${label} · ${runStatuses[record.status]??record.status}`,
|
|
99
113
|
...(record.warnings??[]).map(warning=>`警告:${warning.message}`),
|
|
100
114
|
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??'',
|
|
115
|
+
this.expanded&&record.outputTruncated?'结果较大,主面板仅显示摘要;完整结果请在 /logs 中按 p 分页阅读。':'',
|
|
101
116
|
].filter(Boolean).join('\n');
|
|
102
117
|
}).filter(Boolean).join('\n\n');}
|
|
103
118
|
close(){if(this.refreshTimer)clearTimeout(this.refreshTimer);this.refreshTimer=null;for(const unsubscribe of this.subscriptions.values())unsubscribe();this.subscriptions.clear();}
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
| --- | --- |
|
|
14
14
|
| `@mytools/fst-logs` | `./src/run-logs.js` |
|
|
15
15
|
| `@mytools/fst-logs/markdown` | `./src/markdown-log.js` |
|
|
16
|
+
| `@mytools/fst-logs/steps` | `./src/step-summaries.js` |
|
|
17
|
+
| `@mytools/fst-logs/preview` | `./src/run-preview.js` |
|
|
16
18
|
| `@mytools/fst-logs/package.json` | `./package.json` |
|
|
17
19
|
|
|
18
20
|
仅依赖 exports 声明的入口,禁止消费者跨包读取 src/bin 私有文件。入口路径由本模块维护,修改协议时同步消费方回归。
|
|
@@ -35,6 +37,15 @@
|
|
|
35
37
|
|
|
36
38
|
快照与历史元数据另有 `stepSummaries`、`stepSummaryBytes`、`stepsTruncated`。步骤摘要按 callPath(缺少时按 stepId)保留最新状态,仅包含类型、定位、动作、时间、序号、耗时及有界原因/错误,不缓存 input/output。步骤摘要使用独立的 maxEvents/maxMemoryBytes 预算,因此两份缓存合计最多为两倍 maxMemoryBytes;单条步骤摘要仍受 maxEventBytes 限制。普通日志淘汰不影响步骤状态;步骤本身超预算淘汰时 stepsTruncated 为 true,展示端应把无法确认的步骤显示为未知状态。
|
|
37
39
|
|
|
40
|
+
`createStepSummaries(policy)` 从 `/steps` 入口提供同一套摘要缓存,日志写入器与 TUI 共用。
|
|
41
|
+
`append(event)` 更新状态并刷新淘汰顺序;`get(callPath)`、`values()` 和 `snapshot()` 返回副本。
|
|
42
|
+
`snapshot()` 包含上述三个步骤摘要字段,`size` 为保留条数。
|
|
43
|
+
|
|
44
|
+
`previewRun(record,{maxValueBytes=65536})` 从 `/preview` 入口提供展示与历史元数据共用的结果投影。
|
|
45
|
+
input/output、error、reason 等字段转为有界 JSON 预览,过大时标记 `<字段>Truncated:true`;业务数据对象不变。
|
|
46
|
+
定位字段与 events/stepSummaries 保留,后两者由各自缓存预算负责;展示端应裁去不使用的缓存副本。
|
|
47
|
+
maxValueBytes 至少为 2,连预览本身也容不下时省略字段值并保留截断标记。
|
|
48
|
+
|
|
38
49
|
队列预算在追加时检查。未等待 append 的插件同样受到预算约束;超限返回 `LOG_QUEUE_LIMIT` 拒绝,并使 finish 失败。拒绝已安装内部处理,调用方仍可 await 观察失败,不会产生未处理拒绝。`LOG_DISK_LIMIT` 表示可选磁盘预算不足,拒绝完整追加而不静默截断。磁盘预算与队列预算都计算 JSONL 和 Markdown。建议插件等待 append,以便磁盘写入形成背压。
|
|
39
50
|
|
|
40
51
|
`read`/`list` 仅读取受限的 result.json 元数据,运行中的 read 回退到归属元数据;不加载 JSONL 或 Markdown 正文。巨大旧 result.json 超过读取预算时,read 明确返回 `LOG_METADATA_LIMIT`,list 跳过该记录。旧版本缺少归属信息时仅在 `allowLegacy:true` 下可读。
|
|
@@ -2,7 +2,9 @@ import path from 'node:path';
|
|
|
2
2
|
import {readdir,readFile,lstat,unlink,rmdir,mkdir,writeFile,appendFile,open} from 'node:fs/promises';
|
|
3
3
|
import {FlowError} from '@mytools/sdk';
|
|
4
4
|
import {markdownHeader,markdownEvent,markdownResult} from './markdown-log.js';
|
|
5
|
-
import {logPolicy,jsonSize,
|
|
5
|
+
import {logPolicy,jsonSize,eventPreview} from './log-bounds.js';
|
|
6
|
+
import {createStepSummaries} from './step-summaries.js';
|
|
7
|
+
import {previewRun} from './run-preview.js';
|
|
6
8
|
|
|
7
9
|
const validId=id=>typeof id==='string'&&/^[a-f0-9-]{36}$/.test(id);
|
|
8
10
|
const logFiles=['run.md','events.jsonl','result.json','run.meta.json'];
|
|
@@ -68,15 +70,15 @@ export function createRunLogs(directory,{projectDir,allowLegacy=false,...options
|
|
|
68
70
|
return {
|
|
69
71
|
list,read,owner,policy:Object.freeze({...policy}),
|
|
70
72
|
async start(initial,{onEvent=()=>{}}={}){
|
|
71
|
-
const directory=location(initial.id),events=[],sizes=[],steps=
|
|
73
|
+
const directory=location(initial.id),events=[],sizes=[],steps=createStepSummaries(policy);
|
|
72
74
|
await mkdir(root,{recursive:true});
|
|
73
75
|
const rootStat=await lstat(root);if(!rootStat.isDirectory()||rootStat.isSymbolicLink())throw new FlowError('日志根目录无效');
|
|
74
76
|
await mkdir(directory,{recursive:true});
|
|
75
77
|
await safeDirectory(initial.id);
|
|
76
78
|
for(const name of logFiles){try{await lstat(path.join(directory,name));throw new FlowError('运行日志已存在,拒绝覆盖');}catch(error){if(error.code!=='ENOENT')throw error;}}
|
|
77
79
|
const markdownFile=path.join(directory,'run.md');
|
|
78
|
-
let writes=Promise.resolve(),failure=null,pendingBytes=0,diskBytes=0,resultBytes=0,eventCount=0,eventBytes=2,eventsTruncated=false,
|
|
79
|
-
const snapshot=state=>structuredClone({...initial,...state,logOwner:owner,events,eventCount,eventBytes,eventsTruncated
|
|
80
|
+
let writes=Promise.resolve(),failure=null,pendingBytes=0,diskBytes=0,resultBytes=0,eventCount=0,eventBytes=2,eventsTruncated=false,finished=false;
|
|
81
|
+
const snapshot=state=>structuredClone({...initial,...state,logOwner:owner,events,eventCount,eventBytes,eventsTruncated,...steps.snapshot()});
|
|
80
82
|
const reject=error=>{failure??=error;const promise=Promise.reject(failure);promise.catch(()=>{});return promise;};
|
|
81
83
|
function diskCheck(extra){if(policy.maxDiskBytes!==null&&diskBytes+extra>policy.maxDiskBytes)throw new FlowError('日志磁盘预算已满',{code:'LOG_DISK_LIMIT',details:{maxDiskBytes:policy.maxDiskBytes}});}
|
|
82
84
|
const queue=(bytes,operation)=>{
|
|
@@ -95,28 +97,11 @@ export function createRunLogs(directory,{projectDir,allowLegacy=false,...options
|
|
|
95
97
|
while(events.length>policy.maxEvents||eventBytes>policy.maxMemoryBytes){
|
|
96
98
|
const removed=sizes.shift();events.shift();eventBytes-=removed+(events.length?1:0);eventsTruncated=true;
|
|
97
99
|
}
|
|
98
|
-
|
|
99
|
-
const lightweight={};
|
|
100
|
-
for(const key of ['type','callPath','stepId','action','time','seq','durationMs','reason','error'])if(full[key]!==undefined)lightweight[key]=full[key];
|
|
101
|
-
const summary=eventPreview(lightweight,policy.maxEventBytes),key=summary.callPath??summary.stepId;
|
|
102
|
-
if(key!==undefined&&summary.callPath===full.callPath&&summary.stepId===full.stepId){
|
|
103
|
-
const previous=steps.get(key);
|
|
104
|
-
if(previous){steps.delete(key);stepSummaryBytes-=previous.bytes+(steps.size?1:0);}
|
|
105
|
-
const bytes=Buffer.byteLength(JSON.stringify(summary));steps.set(key,{event:summary,bytes});stepSummaryBytes+=bytes+(steps.size>1?1:0);
|
|
106
|
-
while(steps.size>policy.maxEvents||stepSummaryBytes>policy.maxMemoryBytes){
|
|
107
|
-
const oldest=steps.keys().next().value,removed=steps.get(oldest);steps.delete(oldest);stepSummaryBytes-=removed.bytes+(steps.size?1:0);stepsTruncated=true;
|
|
108
|
-
}
|
|
109
|
-
}else stepsTruncated=true;
|
|
110
|
-
}
|
|
100
|
+
steps.append(full);
|
|
111
101
|
onEvent(structuredClone(event));
|
|
112
102
|
}
|
|
113
103
|
function metadata(state){
|
|
114
|
-
const run={...initial,...state,logOwner:owner,events,eventCount,eventBytes,eventsTruncated
|
|
115
|
-
for(const key of Object.keys(run)){
|
|
116
|
-
if(['events','stepSummaries','id','workflowId','logOwner','status','startedAt','finishedAt'].includes(key))continue;
|
|
117
|
-
if(run[key]===undefined)continue;
|
|
118
|
-
const bounded=preview(run[key],policy.maxEventBytes);run[key]=bounded.value;if(bounded.truncated)run[`${key}Truncated`]=true;
|
|
119
|
-
}
|
|
104
|
+
const run=previewRun({...initial,...state,logOwner:owner,events,eventCount,eventBytes,eventsTruncated,...steps.snapshot()},{maxValueBytes:policy.maxEventBytes});
|
|
120
105
|
const text=JSON.stringify(run);
|
|
121
106
|
if(Buffer.byteLength(text)>metadataLimit)throw new FlowError('运行记录元数据超过写入预算',{code:'LOG_METADATA_LIMIT'});
|
|
122
107
|
return text;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {FlowError} from '@mytools/sdk';
|
|
2
|
+
import {defaults,preview,jsonSize} from './log-bounds.js';
|
|
3
|
+
|
|
4
|
+
const retainedFields=new Set(['events','stepSummaries','id','workflowId','logOwner','status','startedAt','finishedAt']);
|
|
5
|
+
|
|
6
|
+
// 路由信息与已有独立预算的缓存保留;业务正文和诊断字段只投影有界预览。
|
|
7
|
+
export function previewRun(record,{maxValueBytes=defaults.maxEventBytes}={}){
|
|
8
|
+
if(!Number.isSafeInteger(maxValueBytes)||maxValueBytes<2)throw new FlowError('结果预览 maxValueBytes 必须为至少 2 的整数字节数');
|
|
9
|
+
const run={...record};
|
|
10
|
+
for(const key of Object.keys(run)){
|
|
11
|
+
if(retainedFields.has(key)||run[key]===undefined)continue;
|
|
12
|
+
const bounded=preview(run[key],maxValueBytes);
|
|
13
|
+
if(jsonSize(bounded.value,maxValueBytes)>maxValueBytes)delete run[key];
|
|
14
|
+
else run[key]=bounded.value;
|
|
15
|
+
if(bounded.truncated)run[`${key}Truncated`]=true;
|
|
16
|
+
}
|
|
17
|
+
return run;
|
|
18
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {logPolicy,eventPreview} from './log-bounds.js';
|
|
2
|
+
|
|
3
|
+
// 日志写入器与展示端共用轻量摘要,避免各端重新缓存业务 input/output。
|
|
4
|
+
export function createStepSummaries(options={}){
|
|
5
|
+
const policy=logPolicy(options),steps=new Map();
|
|
6
|
+
let bytes=2,truncated=false;
|
|
7
|
+
return {
|
|
8
|
+
append(full){
|
|
9
|
+
if(typeof full.type!=='string'||!full.type.startsWith('step.')||(full.callPath===undefined&&full.stepId===undefined))return;
|
|
10
|
+
const lightweight={};
|
|
11
|
+
for(const key of ['type','callPath','stepId','action','time','seq','durationMs','reason','error'])if(full[key]!==undefined)lightweight[key]=full[key];
|
|
12
|
+
const summary=eventPreview(lightweight,policy.maxEventBytes),key=summary.callPath??summary.stepId;
|
|
13
|
+
if(key===undefined||summary.callPath!==full.callPath||summary.stepId!==full.stepId){truncated=true;return;}
|
|
14
|
+
const previous=steps.get(key);
|
|
15
|
+
if(previous){steps.delete(key);bytes-=previous.bytes+(steps.size?1:0);}
|
|
16
|
+
const size=Buffer.byteLength(JSON.stringify(summary));
|
|
17
|
+
steps.set(key,{event:summary,bytes:size});bytes+=size+(steps.size>1?1:0);
|
|
18
|
+
while(steps.size>policy.maxEvents||bytes>policy.maxMemoryBytes){
|
|
19
|
+
const oldest=steps.keys().next().value,removed=steps.get(oldest);
|
|
20
|
+
steps.delete(oldest);bytes-=removed.bytes+(steps.size?1:0);truncated=true;
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
get size(){return steps.size;},
|
|
24
|
+
get(key){return structuredClone(steps.get(key)?.event);},
|
|
25
|
+
values(){return [...steps.values()].map(entry=>structuredClone(entry.event));},
|
|
26
|
+
snapshot(){return {stepSummaries:this.values(),stepSummaryBytes:bytes,stepsTruncated:truncated};},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
| `@mytools/fst-platform/bundle` | `./src/bundle.js` |
|
|
17
17
|
| `@mytools/fst-platform/json` | `./src/json.js` |
|
|
18
18
|
| `@mytools/fst-platform/npm` | `./src/npm.js` |
|
|
19
|
+
| `@mytools/fst-platform/network` | `./src/network.js` |
|
|
19
20
|
| `@mytools/fst-platform/fs` | `./src/file-probe.js` |
|
|
20
21
|
| `@mytools/fst-platform/package.json` | `./package.json` |
|
|
21
22
|
|
|
@@ -31,6 +32,11 @@
|
|
|
31
32
|
node --test tests/modules.test.js tests/process.test.js tests/environments.test.js
|
|
32
33
|
```
|
|
33
34
|
|
|
35
|
+
## 子进程环境
|
|
36
|
+
|
|
37
|
+
`runProcess(file,args,{env,extendEnv=true})` 默认把 `env` 覆盖到宿主环境上。
|
|
38
|
+
当调用方已按动作代理规则构造完整环境时,传 `extendEnv:false`,防止已移除的代理变量被重新继承。
|
|
39
|
+
|
|
34
40
|
## 隔离文件探测
|
|
35
41
|
|
|
36
42
|
`createFileProbe({timeout=2000,maxConcurrent=2,maxPending=16,allowRemote=false})` 创建只读探测器。
|
|
@@ -7,7 +7,7 @@ import path from 'node:path';
|
|
|
7
7
|
const outputText=bytes=>bytes??new Uint8Array();
|
|
8
8
|
const stripFinalNewline=text=>text.endsWith('\r\n')?text.slice(0,-2):text.endsWith('\n')?text.slice(0,-1):text;
|
|
9
9
|
|
|
10
|
-
export async function runProcess(file,args=[],{cwd,env,signal,timeout=30000,maxBuffer=4*1024*1024,encoding='utf8',onOutput,outputMode='buffer',summaryBytes=65536}={}) {
|
|
10
|
+
export async function runProcess(file,args=[],{cwd,env,extendEnv=true,signal,timeout=30000,maxBuffer=4*1024*1024,encoding='utf8',onOutput,outputMode='buffer',summaryBytes=65536}={}) {
|
|
11
11
|
if(!['buffer','stream'].includes(outputMode))throw new TypeError('outputMode 必须为 buffer 或 stream');
|
|
12
12
|
const streaming=outputMode==='stream';
|
|
13
13
|
if(streaming&&typeof onOutput!=='function')throw new TypeError('stream 模式必须提供 onOutput 消费输出');
|
|
@@ -34,7 +34,7 @@ export async function runProcess(file,args=[],{cwd,env,signal,timeout=30000,maxB
|
|
|
34
34
|
}
|
|
35
35
|
catch(cause){throw new FlowError(`未找到程序:${file}`,{code:'ENOENT',details:{executable:file},cause});}
|
|
36
36
|
// Execa 收集原始字节;文本只在这里按同一编码解码,避免返回值被默认 UTF-8 解码。
|
|
37
|
-
const child=execa(executable,args,{cwd,env,reject:false,windowsHide:true,maxBuffer,buffer:!streaming,timeout:0,shell:false,encoding:'buffer',stripFinalNewline:false});
|
|
37
|
+
const child=execa(executable,args,{cwd,env,extendEnv,reject:false,windowsHide:true,maxBuffer,buffer:!streaming,timeout:0,shell:false,encoding:'buffer',stripFinalNewline:false});
|
|
38
38
|
let reason,killing,outputError,outputFailed=false,outputWrites=Promise.resolve();
|
|
39
39
|
let markStopped;const stopped=new Promise(resolve=>{markStopped=resolve;});
|
|
40
40
|
const outputBytes={stdout:0,stderr:0},tails={stdout:'',stderr:''};let outputTruncated=false;
|
|
@@ -57,7 +57,8 @@ export async function createHost({ plugins = [], config = {}, services = {}, pro
|
|
|
57
57
|
const http=createHttpClient(resolved,{signal:context.signal});
|
|
58
58
|
if(typeof bound.runProcess==='function'){
|
|
59
59
|
const execute=bound.runProcess;
|
|
60
|
-
|
|
60
|
+
// 已构造完整环境;禁止进程层再次合并宿主代理,恢复被策略移除的变量。
|
|
61
|
+
bound.runProcess=(file,args,options={})=>execute(file,args,{...options,env:buildProcessEnv(resolved,{overrides:options.env}),extendEnv:false});
|
|
61
62
|
}
|
|
62
63
|
bound.http={fetch:(input,options)=>http.fetch(input,options)};
|
|
63
64
|
return {capabilities:{...bound,projectDir,stateDir,environments},close:()=>http.close()};
|
|
@@ -100,7 +101,7 @@ export async function createHost({ plugins = [], config = {}, services = {}, pro
|
|
|
100
101
|
const runtime=createRuntime(ctx,definitions,{enabled:runtimeEnabled});
|
|
101
102
|
let unusable=false;
|
|
102
103
|
const assertReadable=()=>{
|
|
103
|
-
if(unusable)throw new FlowError('
|
|
104
|
+
if(unusable)throw new FlowError('插件运行状态无法确认,请重启应用',{code:'PLUGIN_RUNTIME_UNUSABLE'});
|
|
104
105
|
};
|
|
105
106
|
const assertHealthy=()=>{runtime.assertOpen();assertReadable();};
|
|
106
107
|
function registerPlugin(plugin,settings){
|
|
@@ -151,7 +152,8 @@ export async function createHost({ plugins = [], config = {}, services = {}, pro
|
|
|
151
152
|
const oldDefinitions=new Map([...definitions].filter(([id])=>selected.has(id)));
|
|
152
153
|
const oldSettings=new Map([...settingsById].filter(([id])=>selected.has(id)));
|
|
153
154
|
const switchDefinitions=async(targets,settings)=>{
|
|
154
|
-
await runtime.remove(selected);
|
|
155
|
+
try{await runtime.remove(selected);}
|
|
156
|
+
catch(error){unusable=true;throw error;}
|
|
155
157
|
for(const id of selected){definitions.delete(id);settingsById.delete(id);}
|
|
156
158
|
for(const [id,plugin] of targets)if(selected.has(id)){
|
|
157
159
|
definitions.set(id,plugin);
|
|
@@ -165,8 +167,9 @@ export async function createHost({ plugins = [], config = {}, services = {}, pro
|
|
|
165
167
|
for(const [id,plugin] of next)if(selected.has(id))nextSettings.set(id,validate(plugin.config,nextConfig[id]??{},`${id} 配置`));
|
|
166
168
|
try{await switchDefinitions(next,nextSettings);}
|
|
167
169
|
catch(error){
|
|
170
|
+
if(unusable)throw error;
|
|
168
171
|
try{
|
|
169
|
-
runtime.
|
|
172
|
+
runtime.clearActivationErrors();
|
|
170
173
|
await switchDefinitions(oldDefinitions,oldSettings);
|
|
171
174
|
}catch(recovery){unusable=true;throw new AggregateError([error,recovery],`插件更新失败且恢复失败:${error.message};${recovery.message}`);}
|
|
172
175
|
throw error;
|
|
@@ -36,11 +36,11 @@ export function createRuntime(ctx,definitions,{enabled=true}={}){
|
|
|
36
36
|
const reservedContext=new ctx.constructor();
|
|
37
37
|
let graph=validateRuntimes(definitions,reservedContext);
|
|
38
38
|
const forks=new Map(),scopes=new Map(),errors=[],pending=new Set();let closing;
|
|
39
|
-
const record=(id,error)=>errors.push({plugin:id,message:error?.message??String(error)});
|
|
39
|
+
const record=(id,error,phase='activation')=>errors.push({plugin:id,phase,message:error?.message??String(error)});
|
|
40
40
|
const track=(id,callback)=>{
|
|
41
41
|
// 同步撤销注册,只有异步尾部进入等待集合,避免服务重激活时旧清理删掉新注册。
|
|
42
|
-
let result;try{result=callback();}catch(error){record(id,error);return;}
|
|
43
|
-
const task=Promise.resolve(result).catch(e=>record(id,e)).finally(()=>pending.delete(task));
|
|
42
|
+
let result;try{result=callback();}catch(error){record(id,error,'dispose');return;}
|
|
43
|
+
const task=Promise.resolve(result).catch(e=>record(id,e,'dispose')).finally(()=>pending.delete(task));
|
|
44
44
|
pending.add(task);return task;
|
|
45
45
|
};
|
|
46
46
|
// 3.18.1 的 reset 会调用 dispose,但不等待 Promise;此适配只补齐等待和错误传播。
|
|
@@ -72,7 +72,7 @@ export function createRuntime(ctx,definitions,{enabled=true}={}){
|
|
|
72
72
|
// 初始化可能仍在取得资源;先等其结束,再执行本次激活登记的清理。
|
|
73
73
|
while(activation.tasks.size)await Promise.allSettled([...activation.tasks]);
|
|
74
74
|
for(const dispose of activation.disposers.splice(0)){
|
|
75
|
-
try{await dispose();}catch(error){record(plugin.id,error);}
|
|
75
|
+
try{await dispose();}catch(error){record(plugin.id,error,'dispose');}
|
|
76
76
|
}
|
|
77
77
|
})();
|
|
78
78
|
});
|
|
@@ -100,7 +100,10 @@ export function createRuntime(ctx,definitions,{enabled=true}={}){
|
|
|
100
100
|
await ctx.start();await runtime.ready();
|
|
101
101
|
},
|
|
102
102
|
reindex(){graph=validateRuntimes(definitions,reservedContext);},
|
|
103
|
-
|
|
103
|
+
clearActivationErrors(){
|
|
104
|
+
// 启动失败可以在清理成功后恢复;清理失败必须保留,不能假定资源已释放。
|
|
105
|
+
for(let index=errors.length-1;index>=0;index--)if(errors[index].phase!=='dispose')errors.splice(index,1);
|
|
106
|
+
},
|
|
104
107
|
async remove(ids){
|
|
105
108
|
// 先撤销消费者再撤销提供者;旧作用域清理结束后才能注册新作用域。
|
|
106
109
|
const selected=new Set(ids);
|
|
@@ -293,7 +293,15 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
|
|
|
293
293
|
if(active.length)throw new FlowError(`请先停止受影响插件提供的 Web:${active.join('、')}`,{code:'WEB_ACTIVE',details:{plugins:active}});
|
|
294
294
|
}
|
|
295
295
|
function workflowPrivileges(closure,entryId){
|
|
296
|
-
const flows=new Map(closure.workflows.map(w=>[w.id,w])),report=[];
|
|
296
|
+
const flows=new Map(closure.workflows.map(w=>[w.id,w])),report=[],stoppingFlows=new Map();
|
|
297
|
+
const mayStopFlow=id=>{
|
|
298
|
+
if(!stoppingFlows.has(id))stoppingFlows.set(id,(flows.get(id)?.steps??[]).some(mayStopStep));
|
|
299
|
+
return stoppingFlows.get(id);
|
|
300
|
+
};
|
|
301
|
+
const mayStopStep=step=>!!step.stop
|
|
302
|
+
||!!step.workflow&&mayStopFlow(store.childId(step.workflow))
|
|
303
|
+
||!!step.branch&&[...step.branch.then,...(step.branch.else??[])].some(mayStopStep)
|
|
304
|
+
||!!step.parallel&&step.parallel.steps.some(mayStopStep);
|
|
297
305
|
const visit=(id,mandatory)=>{
|
|
298
306
|
const workflow=flows.get(id);
|
|
299
307
|
if(!workflow)return;
|
|
@@ -308,8 +316,9 @@ export async function createApplication({projectDir=process.cwd(),workspace,outp
|
|
|
308
316
|
}
|
|
309
317
|
if(step.workflow)visit(store.childId(step.workflow),needed);
|
|
310
318
|
if(step.branch){steps(step.branch.then,false);steps(step.branch.else??[],false);}
|
|
311
|
-
|
|
312
|
-
if(step.
|
|
319
|
+
// 并行兄弟可能在动作开始前停止整组;嵌套 stop 也会传播到后续步骤。
|
|
320
|
+
if(step.parallel)steps(step.parallel.steps,needed&&!step.parallel.steps.some(mayStopStep));
|
|
321
|
+
if(mayStopStep(step))available=false;
|
|
313
322
|
}
|
|
314
323
|
};
|
|
315
324
|
steps(workflow.steps,mandatory);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// 由 npm version 的生命周期脚本同步,内部插件 API 版本独立维护。
|
|
2
|
-
export const VERSION="0.5.
|
|
2
|
+
export const VERSION="0.5.1";
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
fst 命令入口、交互终端、输入呈现和运行观察。通过公开应用和模块接口工作,业务执行不在界面中重复实现。
|
|
8
8
|
|
|
9
|
-
内部依赖:`@mytools/sdk`、`@mytools/core`、`@mytools/fst-workspace`、`@mytools/fst-platform`、`@mytools/fst-plugin`、`@mytools/fst-devtools`。
|
|
9
|
+
内部依赖:`@mytools/sdk`、`@mytools/core`、`@mytools/fst-workspace`、`@mytools/fst-platform`、`@mytools/fst-plugin`、`@mytools/fst-devtools`、`@mytools/fst-logs`。
|
|
10
10
|
## 公开入口
|
|
11
11
|
|
|
12
12
|
| 导入路径 | 实现位置 |
|
|
@@ -52,3 +52,8 @@ node --test tests/modules.test.js tests/cli.test.js tests/terminal.test.js
|
|
|
52
52
|
## 完整日志分页
|
|
53
53
|
|
|
54
54
|
`fst logs` 列出历史,`fst logs <id> --cursor 0 --limit 65536 --json` 返回正文和下一页字节游标。TUI 运行详情按 `p` 打开完整日志,每页读取后可前后翻页或刷新。主面板保留有界近期摘要;日志清理失败显示维护警告。细节见 `fst docs generation`。
|
|
55
|
+
|
|
56
|
+
主面板通过 `fst-logs/steps` 共用步骤摘要缓存,按最近更新顺序淘汰,同时限制条数和字节量。
|
|
57
|
+
摘要不保存业务 input/output;缺失的状态显示“摘要已省略”,已有错误仍可查看。
|
|
58
|
+
运行输入、最终结果与错误通过 `fst-logs/preview` 保存有界预览,避免业务完成后又缓存完整大结果。
|
|
59
|
+
展开时明确提示结果为摘要,完整结果仍在日志中;不会替换业务调用方取得的完整输出。
|