@yanglaofish/fishtools 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +53 -20
  2. package/manual/LICENSE +21 -0
  3. package/manual/README.md +173 -0
  4. package/manual/THIRD_PARTY_NOTICES.md +32 -0
  5. package/manual/docs/examples/action-plugin/LICENSE +21 -0
  6. package/manual/docs/examples/action-plugin/README.md +38 -0
  7. package/manual/docs/examples/action-plugin/index.js +18 -0
  8. package/manual/docs/examples/action-plugin/input.json +1 -0
  9. package/manual/docs/examples/action-plugin/package.json +9 -0
  10. package/manual/docs/plans/2026-10-10-workbench-ui.md +27 -0
  11. package/{docs → manual/docs}/plugin-development.md +151 -19
  12. package/{docs → manual/docs}/releases.md +18 -1
  13. package/manual/docs/roadmap.md +85 -0
  14. package/manual/docs/superpowers/plans/2026-10-09-workflow-mvp.md +115 -0
  15. package/manual/docs/superpowers/plans/2026-10-10-cordis-services.md +34 -0
  16. package/manual/docs/superpowers/plans/2026-10-10-fst-terminal.md +33 -0
  17. package/manual/docs/superpowers/plans/2026-10-10-portable-plugin-contract.md +35 -0
  18. package/manual/docs/superpowers/plans/2026-10-11-control-observation.md +77 -0
  19. package/manual/docs/superpowers/specs/2026-10-09-workflow-cli-design.md +377 -0
  20. package/manual/docs/superpowers/specs/2026-10-10-cordis-services-design.md +88 -0
  21. package/manual/docs/superpowers/specs/2026-10-10-portable-plugin-contract.md +31 -0
  22. package/manual/docs/superpowers/specs/2026-10-10-run-logs.md +28 -0
  23. package/manual/docs/superpowers/specs/2026-10-11-control-observation.md +37 -0
  24. package/{docs → manual/docs}/verification.md +20 -0
  25. package/manual/examples/service-plugins/README.md +20 -0
  26. package/manual/examples/service-plugins/consumer/LICENSE +21 -0
  27. package/manual/examples/service-plugins/consumer/index.js +14 -0
  28. package/manual/examples/service-plugins/consumer/package.json +1 -0
  29. package/manual/examples/service-plugins/provider/LICENSE +21 -0
  30. package/manual/examples/service-plugins/provider/index.js +19 -0
  31. package/manual/examples/service-plugins/provider/package.json +1 -0
  32. package/manual/packages/core/LICENSE +21 -0
  33. package/manual/packages/core/src/host.js +122 -0
  34. package/manual/packages/sdk/LICENSE +21 -0
  35. package/manual/packages/sdk/index.js +14 -0
  36. package/manual/packages/sdk/package.json +1 -0
  37. package/manual/plugins/environment/LICENSE +21 -0
  38. package/manual/plugins/environment/index.js +92 -0
  39. package/manual/plugins/environment/package.json +1 -0
  40. package/manual/plugins/examples/LICENSE +21 -0
  41. package/manual/plugins/examples/index.js +23 -0
  42. package/manual/plugins/examples/package.json +1 -0
  43. package/manual/plugins/examples/scripts/echo.py +5 -0
  44. package/manual/tests/application.test.js +45 -0
  45. package/node_modules/@mytools/core/bin/fst.js +12 -4
  46. package/node_modules/@mytools/core/src/application.js +9 -4
  47. package/node_modules/@mytools/core/src/cli-run.js +6 -0
  48. package/node_modules/@mytools/core/src/contracts.js +15 -2
  49. package/node_modules/@mytools/core/src/control.js +63 -0
  50. package/node_modules/@mytools/core/src/documentation.js +65 -0
  51. package/node_modules/@mytools/core/src/engine.js +80 -26
  52. package/node_modules/@mytools/core/src/host.js +13 -2
  53. package/node_modules/@mytools/core/src/markdown-log.js +8 -5
  54. package/node_modules/@mytools/core/src/process.js +35 -14
  55. package/node_modules/@mytools/core/src/project.js +1 -1
  56. package/node_modules/@mytools/core/src/run-logs.js +1 -1
  57. package/node_modules/@mytools/core/src/terminal/conditions.js +32 -0
  58. package/node_modules/@mytools/core/src/terminal/log-viewer.js +7 -8
  59. package/node_modules/@mytools/core/src/terminal/model.js +32 -3
  60. package/node_modules/@mytools/core/src/terminal/presentation.js +42 -0
  61. package/node_modules/@mytools/core/src/terminal/run-viewer.js +86 -0
  62. package/node_modules/@mytools/core/src/terminal/session.js +150 -31
  63. package/node_modules/@mytools/core/src/terminal/theme.js +28 -0
  64. package/node_modules/@mytools/core/src/terminal/view.js +92 -13
  65. package/node_modules/@mytools/core/src/validation.js +15 -4
  66. package/node_modules/@mytools/core/src/version.js +1 -1
  67. package/node_modules/@mytools/core/src/workflows.js +30 -19
  68. package/node_modules/@mytools/core/templates/control-demo.yaml +47 -0
  69. package/node_modules/@mytools/environment/index.js +14 -2
  70. package/node_modules/@mytools/pack/index.js +2 -0
  71. package/node_modules/@mytools/web/public/THIRD_PARTY_NOTICES.txt +1094 -1095
  72. package/node_modules/@mytools/web/public/assets/index-DT2B4tMV.js +70 -0
  73. package/node_modules/@mytools/web/public/assets/{index-BeV6IqTW.css → index-vuZvrUTT.css} +1 -1
  74. package/node_modules/@mytools/web/public/index.html +2 -2
  75. package/package.json +2 -2
  76. package/node_modules/@mytools/web/public/assets/index-TgsBnZgG.js +0 -70
  77. /package/{docs → manual/docs}/deployment.md +0 -0
@@ -0,0 +1,37 @@
1
+ # 条件控制、并行编排与运行观察
2
+
3
+ ## 已确认目标
4
+
5
+ 插件负责完成能力并返回符合声明的结果;正常返回表示执行成功,业务结果是否符合要求由工作流判断。提供可编排动作必须声明 input/output(无参数或无输出使用空对象 schema);业务检测返回 passed/ok/count 等结构化字段。判定预设可选,普通大小写转换无需重复声明“执行成功”。
6
+
7
+ 用户在 TUI 与 Web 的列表中添加条件分支、并行组、提前结束,也可给步骤设置执行条件。运行中可在步骤总览、输入输出、实时日志间切换,按步骤调用路径区分同一插件的多次调用。支持键盘翻页和鼠标滚轮,离开观察页面不终止任务,停止操作显式取消。
8
+
9
+ ## 控制流协议(兼容 schemaVersion: 1 的既有顺序步骤)
10
+
11
+ 每个步骤有 id、可选 title、可选 when,以下五种执行目标必须恰有一种:
12
+
13
+ - `{action, with}`:调用插件。
14
+ - `{workflow, with}`:调用子工作流。
15
+ - `{branch: {condition, then: [步骤], else: [步骤]}}`:只执行命中的分支。
16
+ - `{parallel: {steps: [步骤], concurrency: 2}}`:并发执行独立子步骤;concurrency 为 1–32 整数,默认 4。
17
+ - `{stop: {reason: '环境不满足'}}`:正常提前结束整个顶层运行,状态 stopped,保留原因和之前的步骤事件。可以带 when 决定是否触发。
18
+
19
+ 条件为 `{left: {$ref: 'steps.check.output.ok'}, op: 'eq', right: true}`;支持 eq/ne/gt/gte/lt/lte/contains/exists,以及 `{all:[条件]}`、`{any:[条件]}`、`{not:条件}`。严格按类型比较,不执行 JS。缺失字段在 exists 中为 false,其他比较也为 false;非法引用在执行前拒绝。when 为 false 记 step.skipped,不调用插件。
20
+
21
+ 判定预设:动作上的 `conditions: {passed: {title:'检查通过', path:'ok', op:'eq', value:true}}`,path 相对于本动作输出,界面将其展开为条件节点。核心检查格式与已声明输出字段。检查插件增加独立 report 动作返回 ok:false;保留旧 check 动作失败即抛错以兼容旧工作流。
22
+
23
+ 嵌套序列可读取外层已完成步骤及本层前序步骤;并行兄弟之间不可互相引用。节点 id 在同一工作流内唯一。分支/并行组输出为实际执行完成的子步骤 id → 输出对象,外部通过 `steps.group.output.child.field` 引用。跳过/未选中节点无输出;普通绑定读取缺失字段仍明确报错,作者可通过条件/分支避免读取。并行组必须等待所有启动任务清理完成;首个失败或提前结束会停止调度,取消已启动兄弟,保留首个原因。不保证回滚业务副作用。嵌套停止传播到顶层。
24
+
25
+ ## 运行事件与界面合同
26
+
27
+ - 保留现有事件,增加 step.skipped、step.stopped、workflow.stopped、run.stopped。
28
+ - step.started 携带解析后的 input、action(如有)、callPath;事件以 callPath 归属步骤。
29
+ - runProcess 输出产生 `{type:'log', stream:'stdout'|'stderr', message, callPath, stepId}` 事件;仍返回 exitCode/stdout/stderr,保留超时、取消、进程树回收和输出上限。UTF-8 分块正确解码。
30
+ - app.getRun/readRun 返回 status、events、output/error、可选 reason;stopped 无 error。CLI stopped 正常退出并打印原因。
31
+ - TUI 新增 /runs,运行后打开可切换视图的观察面板;可关闭面板返回工作台,后台运行继续。/logs 保留已结束日志。滚轮仅在可滚动面板接管,并在关闭后恢复终端鼠标模式。
32
+ - Web 运行面板使用同一事件显示待执行/运行中/已跳过/成功/失败/停止;详情按调用路径过滤日志,滚动离开底部时暂停自动跟随。
33
+ - 所有条件与分组编辑提供表单,不要求手写 JSON/YAML;只读包允许观察和运行,不允许改流程。
34
+
35
+ ## 验收与范围
36
+
37
+ 覆盖条件分支、正常结束、并发上限、取消清理、嵌套依赖闭包导出、实时 stdout/stderr、鼠标与键盘浏览以及 TUI/Web 编辑保存再运行。Windows/Linux 保持一致。暂停恢复、持久化断点、多机分布式执行、自动回滚不在本次范围。
@@ -165,3 +165,23 @@ MiB = 1,048,576 字节。以下为本机单次测量,不是性能保证;运
165
165
  ## 分层便携包阶段远端结果(历史)
166
166
 
167
167
  代码与测试提交 e55075e 的 [Windows / Ubuntu CI](https://github.com/yanglaofish/Fishtools/actions/runs/38053011826) 均通过;Windows 5 分 13 秒、Ubuntu 1 分 27 秒。验收文档归档提交不改变已验证代码。
168
+ # 2026-10-11 TUI 上手体验修正
169
+
170
+ - 修正首页缺少产品介绍、示例入口以及插件清单不显眼的问题:显示 FISHtools 字符标识、欢迎语、当前工作区、数量和真实工作流示例。
171
+ - `/plugins` 直接列出已有插件,详情展示声明的动作、输入输出、环境、配置及命令;维护模式仍保留说明,仅隐藏不可执行的命令入口。
172
+ - 完整页面按终端高度分配空间;80×24、52×24、40×20 自动化渲染检查覆盖首页和可滚动插件列表。
173
+ - 本地终端与 Markdown 阅读器共 26 项测试通过;Windows 与 WSL 真实 PTY 检查首页、插件列表和详情返回。本地 npm 安装、再次导出及包内插件调用验证通过。
174
+ - 提交 `4c6bdc5` 的 [Windows/Linux 全量 CI](https://github.com/yanglaofish/Fishtools/actions/runs/38071024230) 各 91 项通过;[v0.1.4 发布验收](https://github.com/yanglaofish/Fishtools/actions/runs/38071614758) 的双平台测试、自包含归档解压运行、无 Node Docker 检查及发布全部成功。npm 官方注册表已提供 `@yanglaofish/fishtools@0.1.4`,[Release](https://github.com/yanglaofish/Fishtools/releases/tag/v0.1.4) 已附三个归档及 SHA256SUMS。
175
+ - 修复 npm 发布预演依赖远端版本状态的问题:仅预演采用离线独立空缓存,实际发布继续检查注册表版本和归档校验值。
176
+
177
+ ## 2026-10-11 0.2.0 控制流、运行观察与离线开发资料
178
+
179
+ - 本地最终 `npm test` **143/143** 通过,180.2 秒;涵盖真实 npm 离线全局安装、再次导出、资料定位和归档完整性。Web 构建通过,无新增运行时依赖。
180
+ - 独立审查发现并修复:嵌套并行首因可能被取消覆盖、空分组与最后跳过节点漏检取消、null schema 绕过声明、失败后用户取消覆盖最终状态;分别有失败复现和回归测试。服务示例补齐严格输出声明。
181
+ - WSL Linux 运行控制流、实时输出和旧工作流专项 **29/29** 通过。Windows 完整便携 ZIP 解压后清空 PATH,使用包内 Node 执行 hello、control-demo、正常停止、离线手册阅读及导出成功。
182
+ - Windows 导出的同一 Linux tar.gz 在 WSL Ubuntu 24.04 中解压,通过 80×24 真实 PTY 验证彩色首页、/docs 阅读/翻页/鼠标滚轮、control-demo 运行、输入输出与日志视图、关闭面板、/runs 重新观察及正常退出。Windows 真实 PTY 已验证文档阅读、插件详情和退出;终端专项 **44/44** 通过,包含 40×20 窄屏、NO_COLOR 与安全文本渲染。
183
+ - Linux 包只读挂载到 Debian 12 slim 容器;容器断网、根文件系统只读、移除 capabilities、基础环境无 Node/npm。手册导出后实际安装零依赖示例,offline.greet 返回“你好,离线用户”;条件并行与提前结束、配置保存、参数修改、完整性检查及再次导出全部通过。复现入口 `scripts/portable-smoke.sh`。
184
+ - Chrome 真实浏览器 1440 与 390 宽度验证嵌套控制节点编辑保存、输入引用、执行结果、日志暂停/恢复跟随、关闭面板后台继续、Esc 焦点及只读管理;无横向溢出或 console error。终态 cancelled / stopped 与未完成状态分别显示;Web 模型 **6/6** 通过。
185
+ - npm 与便携包包含 `manual/`;`fst docs`、`fst docs plugins`、`fst docs --export <新目录>` 与 TUI `/docs` 均可离线访问。文档测试 **4/4**,验证相对 Markdown 链接、拒绝覆盖、脱离源码定位和再次导出。SDK 尚未独立发布;示例使用标准对象,不依赖内部 SDK 的在线安装。
186
+ - 验收脚本复用旧临时工作区曾被项目归属校验拒绝;脚本现为每次解压生成独立工作区,重新验收通过。没有放宽归属或完整性检查。
187
+ - 本记录为本地验收结果;远端 CI 与 npm/tag/Release 结果在发布完成后追加。
@@ -0,0 +1,20 @@
1
+ # 可独立安装的服务插件示例
2
+
3
+ `provider` 提供 `demo.connection`,`consumer` 提供 `/demo.read` 和可编排动作。两者是标准 npm 插件目录,无需修改核心代码;MIT 许可证随目录保留。
4
+
5
+ 已安装 fst 后,使用 `fst docs --export ./fishtools-manual` 导出资料,再进入导出目录执行以下命令。源码开发也可直接从仓库根目录执行:
6
+
7
+ ```sh
8
+ fst --maintenance plugin add ./examples/service-plugins/provider
9
+ fst --maintenance plugin add ./examples/service-plugins/consumer
10
+ fst command demo.read
11
+ fst
12
+ ```
13
+
14
+ 在 TUI 输入 `/demo.read`,或将“使用共享连接”添加到工作流。两个动作共享同一个连接实例;每次启动应用创建新实例,调用计数不会跨进程保留。
15
+
16
+ `dependencies` 声明提供者的插件 ID 和版本,使导出消费者工作流时自动包含提供者;`runtime.inject` 声明运行时服务就绪要求。它们与 package.json 的 npm 库依赖承担不同职责。
17
+
18
+ 示例不连接真实网络。实际 SSH/数据库插件应在 ready 中完成连接后再 `ctx.set()`,在 `ctx.on('dispose', async () => ...)` 中关闭;不要提前发布未就绪的连接。
19
+
20
+ 修改有状态插件前,在 TUI `/plugins` 进入维护模式,修改完成后选择恢复服务;原子 CLI 使用 `--maintenance`。维护模式是本进程选项,不写入项目文件。按默认方式再次启动即恢复服务。
@@ -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.
@@ -0,0 +1,14 @@
1
+ export default {
2
+ id:'fishtools-demo-consumer',version:'1.0.0',title:'共享服务调用示例',
3
+ dependencies:{'fishtools-demo-connection':'^1.0.0'},
4
+ runtime:{inject:['demo.connection'],apply(){}},
5
+ commands:{'demo.read':{action:'read',description:'使用共享连接读取文本'}},
6
+ actions:{read:{
7
+ title:'使用共享连接',
8
+ input:{type:'object',properties:{text:{type:'string',default:'hello'}},additionalProperties:false},
9
+ output:{type:'object',properties:{text:{type:'string'},uses:{type:'integer',minimum:1}},required:['text','uses'],additionalProperties:false},
10
+ async run(input,{getService,signal}){
11
+ signal?.throwIfAborted();return getService('demo.connection').read(input.text);
12
+ },
13
+ }},
14
+ };
@@ -0,0 +1 @@
1
+ {"name":"fishtools-demo-consumer","version":"1.0.0","type":"module","main":"index.js","files":["index.js","LICENSE"],"license":"MIT"}
@@ -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.
@@ -0,0 +1,19 @@
1
+ export default {
2
+ id:'fishtools-demo-connection',version:'1.0.0',title:'共享连接示例',
3
+ runtime:{
4
+ provides:['demo.connection'],
5
+ apply(ctx){
6
+ let connection;
7
+ ctx.on('dispose',async()=>{await connection?.close();});
8
+ ctx.on('ready',async()=>{
9
+ // 实际插件可在此连接 SSH 或数据库;就绪后才发布服务。
10
+ let uses=0,closed=false;
11
+ connection={
12
+ read(text){if(closed)throw Error('连接已关闭');return {text:`connection:${text}`,uses:++uses};},
13
+ async close(){closed=true;},
14
+ };
15
+ ctx.set('demo.connection',connection);
16
+ });
17
+ },
18
+ },
19
+ };
@@ -0,0 +1 @@
1
+ {"name":"fishtools-demo-connection","version":"1.0.0","type":"module","main":"index.js","files":["index.js","LICENSE"],"license":"MIT"}
@@ -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.
@@ -0,0 +1,122 @@
1
+ import { Context } from 'cordis';
2
+ import semver from 'semver';
3
+ import { SDK_VERSION, FlowError } from '@mytools/sdk';
4
+ import { validate, emptyObject } from './validation.js';
5
+ import {validateDeclarations,validateCommands} from './contracts.js';
6
+ import {resolveEnvironments} from './environments.js';
7
+ import {createRuntime} from './runtime.js';
8
+ import {pluginError} from './diagnostics.js';
9
+
10
+ export async function createHost({ plugins = [], config = {}, services = {}, environmentOverrides={},runtimeEnabled=true } = {}) {
11
+ const ctx = new Context();
12
+ const actions = new Map();
13
+ const definitions = new Map();
14
+ const extensions = new Map();
15
+ const serviceNames = new Set();
16
+ const commandNames=new Set(),commands=new Map();
17
+ function bindContext(context){
18
+ const bound={...services,...context},execute=bound.runProcess;
19
+ if(typeof execute==='function')bound.runProcess=(file,args,options={})=>{
20
+ const signal=context.signal&&options.signal?AbortSignal.any([context.signal,options.signal]):context.signal??options.signal;
21
+ return execute(file,args,{...options,signal,onOutput:async({stream,message})=>{
22
+ await bound.log?.(message,undefined,{stream});
23
+ await options.onOutput?.({stream,message});
24
+ }});
25
+ };
26
+ return bound;
27
+ }
28
+ for (const entry of plugins) {
29
+ const plugin = entry.plugin ?? entry;
30
+ validateDeclarations(plugin);validateCommands(plugin,commandNames);
31
+ if (!plugin.id || definitions.has(plugin.id)) throw new FlowError(`插件标识缺失或重复:${plugin.id}`, {code:'VALIDATION'});
32
+ if (!semver.satisfies(SDK_VERSION, plugin.sdk ?? '^0.1.0')) throw new FlowError(`插件 ${plugin.id} 与 SDK ${SDK_VERSION} 不兼容`, {code:'VALIDATION'});
33
+ for(const [name,action] of Object.entries(plugin.actions??{})){
34
+ if(typeof action?.run!=='function')throw new FlowError(`动作 ${plugin.id}:${name} 缺少执行函数`);
35
+ }
36
+ for(const [name,extension] of Object.entries(plugin.services??{})){
37
+ if(serviceNames.has(name))throw new FlowError(`服务重复:${name}`);
38
+ if(typeof extension!=='function')throw new FlowError(`服务 ${name} 缺少执行函数`);
39
+ serviceNames.add(name);
40
+ }
41
+ definitions.set(plugin.id, {...plugin, root:entry.root});
42
+ }
43
+ const settingsById=new Map();
44
+ for (const plugin of definitions.values()) {
45
+ for (const [id, range] of Object.entries(plugin.dependencies ?? {})) {
46
+ if (!definitions.has(id) || !semver.satisfies(definitions.get(id).version,range)) throw new FlowError(`插件 ${plugin.id} 缺少兼容依赖 ${id}@${range}`,{code:'VALIDATION'});
47
+ }
48
+ const settings = validate(plugin.config, config[plugin.id] ?? {}, `${plugin.id} 配置`);
49
+ settingsById.set(plugin.id,settings);
50
+ }
51
+ const runtime=createRuntime(ctx,definitions,{enabled:runtimeEnabled});
52
+ try{
53
+ for (const plugin of definitions.values()) {
54
+ const settings=settingsById.get(plugin.id);
55
+ runtime.register(plugin,settings,scope=>{
56
+ for(const [name,command] of Object.entries(plugin.commands??{})){
57
+ const action=command.action?plugin.actions[command.action]:command;
58
+ commands.set(name,{...command,name,plugin:plugin.id,input:action.input,output:action.output,description:command.description??action.description??action.title??name,config:settings});
59
+ scope.on('dispose',()=>commands.delete(name));
60
+ }
61
+ for (const [name, action] of Object.entries(plugin.actions ?? {})) {
62
+ const id = `${plugin.id}:${name}`;
63
+ if (typeof action.run !== 'function') throw new FlowError(`动作 ${id} 缺少执行函数`);
64
+ actions.set(id, {...action, id, plugin:plugin.id, config:settings, root:plugin.root});
65
+ scope.on('dispose', () => actions.delete(id));
66
+ }
67
+ for (const [name, extension] of Object.entries(plugin.services ?? {})) {
68
+ if (extensions.has(name)) throw new FlowError(`服务重复:${name}`);
69
+ extensions.set(name, {run:extension,plugin:plugin.id});
70
+ scope.on('dispose',()=>extensions.delete(name));
71
+ }
72
+ });
73
+ }
74
+ await runtime.start();
75
+ }catch(error){
76
+ try{await runtime.close();}catch(cleanup){if(cleanup.message!==error.message)throw new AggregateError([error,cleanup],`${error.message};${cleanup.message}`);}
77
+ throw error;
78
+ }
79
+ return {
80
+ definitions, actions,
81
+ hasRuntime:runtime.hasRuntime,
82
+ runtimeStatus:()=>runtime.status(),
83
+ commands(){return [...commands.values()].map(({run,config,...metadata})=>structuredClone(metadata));},
84
+ async invokeCommand(name,input,context={}){
85
+ runtime.assertOpen();
86
+ const command=commands.get(name);if(!command)throw new FlowError(`命令不存在:/${name}`);
87
+ if(command.action)return this.invoke(`${command.plugin}:${command.action}`,input,context);
88
+ if(!context.ui)throw new FlowError(`交互命令 /${name} 需要在 TUI 中调用`);
89
+ const plugin=definitions.get(command.plugin);
90
+ try{
91
+ const getService=runtime.bindServices(plugin.id);
92
+ context.signal?.throwIfAborted();
93
+ const result=await command.run(validate(command.input??emptyObject,input,`/${name} 输入`),{...bindContext(context),getService,config:command.config,resourceDir:plugin.root,environments:resolveEnvironments(plugin,undefined,environmentOverrides[plugin.id])});
94
+ context.signal?.throwIfAborted();
95
+ return validate(command.output,JSON.parse(JSON.stringify(result??{})),`/${name} 输出`);
96
+ }catch(cause){throw pluginError(cause,{plugin:plugin.id,command:`/${name}`});}
97
+ },
98
+ catalog() { return [...actions.values()].map(({run,config,root,...metadata})=>structuredClone(metadata)); },
99
+ async invoke(id, input, context = {}) {
100
+ runtime.assertOpen();
101
+ const action = actions.get(id);
102
+ if (!action) throw new FlowError(`动作不存在:${id}`, {code:'VALIDATION'});
103
+ try{
104
+ const getService=runtime.bindServices(action.plugin);
105
+ context.signal?.throwIfAborted();
106
+ const result = await action.run(validate(action.input ?? emptyObject,input,`${id} 输入`), {...bindContext(context),getService, config:action.config, resourceDir:action.root,environments:resolveEnvironments(definitions.get(action.plugin),action,environmentOverrides[action.plugin])});
107
+ context.signal?.throwIfAborted();
108
+ // 通过 JSON 边界验证返回值,避免函数、循环引用等进入工作流状态。
109
+ const encoded = JSON.stringify(result);
110
+ if (encoded === undefined) throw new FlowError(`${id} 输出必须可 JSON 序列化`);
111
+ return validate(action.output, JSON.parse(encoded),`${id} 输出`);
112
+ }catch(cause){throw pluginError(cause,{plugin:action.plugin,action:id});}
113
+ },
114
+ async service(name,options) {
115
+ runtime.assertOpen();
116
+ if (!extensions.has(name)) throw new FlowError(`未安装提供 ${name} 的插件`);
117
+ const extension=extensions.get(name);runtime.assertActive(extension.plugin);
118
+ return extension.run(services,options);
119
+ },
120
+ close:()=>runtime.close(),
121
+ };
122
+ }
@@ -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.
@@ -0,0 +1,14 @@
1
+ export const SDK_VERSION = '0.1.0';
2
+
3
+ export function definePlugin(plugin) {
4
+ return Object.freeze({ sdk: '^0.1.0', actions: {}, ...plugin });
5
+ }
6
+
7
+ export class FlowError extends Error {
8
+ constructor(message, { code = 'EXECUTION_FAILED', details, cause } = {}) {
9
+ super(message, { cause });
10
+ this.name = 'FlowError';
11
+ this.code = code;
12
+ this.details = details;
13
+ }
14
+ }
@@ -0,0 +1 @@
1
+ {"name":"@mytools/sdk","version":"0.1.0","type":"module","main":"index.js","exports":"./index.js","files":["index.js","README.md"],"license":"MIT"}
@@ -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.
@@ -0,0 +1,92 @@
1
+ import os from 'node:os';
2
+ import { readFile } from 'node:fs/promises';
3
+ import semver from 'semver';
4
+ import { definePlugin, FlowError } from '@mytools/sdk';
5
+
6
+ const pythonProbe=String.raw`import sys,json,importlib.metadata,importlib.util
7
+ names=json.loads(sys.argv[1])
8
+ packages=[]
9
+ for item in names:
10
+ name=item['name']
11
+ try:
12
+ version=importlib.metadata.version(name)
13
+ module=item.get('module')
14
+ if module: __import__(module)
15
+ packages.append({'name':name,'version':version,'ok':True})
16
+ except Exception as e:
17
+ packages.append({'name':name,'ok':False,'error':str(e)})
18
+ print(json.dumps({'executable':sys.executable,'version':sys.version.split()[0],'prefix':sys.prefix,'packages':packages}))`;
19
+
20
+ const plugin={
21
+ id:'@mytools/environment',version:'0.1.0',title:'环境检查',description:'检查目标机器的系统、Python/pip 与昇腾 NPU 环境,缺失必需条件时停止工作流。',
22
+ platforms:['win32','linux'],commands:{'env.check':{description:'检查系统、Python/pip 或 NPU',action:'check'}},
23
+ actions:{
24
+ check:{
25
+ title:'检查目标环境',description:'始终报告系统信息;按需检查指定 Python 及其软件包、pip 和 NPU。只检查,不安装或修改环境。',
26
+ input:{type:'object',properties:{
27
+ python:{type:'object',title:'Python 环境',required:['executable'],properties:{
28
+ executable:{type:'string',title:'解释器路径',description:'填写目标环境的 python/python.exe 绝对路径'},
29
+ version:{type:'string',title:'Python 版本要求',default:'>=3.9'},
30
+ pip:{type:'boolean',title:'要求 pip 可用',default:false},
31
+ packages:{type:'array',title:'要求的软件包',default:[],items:{type:'object',required:['name'],properties:{name:{type:'string',title:'包名'},version:{type:'string',title:'版本范围'},module:{type:'string',title:'需要尝试导入的模块(可选)'}}}},
32
+ }},
33
+ npu:{type:'boolean',title:'检查昇腾 NPU',default:false},
34
+ npuCommand:{type:'string',title:'npu-smi 路径',default:'npu-smi'},
35
+ },additionalProperties:false},
36
+ output:{type:'object',properties:{ok:{type:'boolean',title:'检查通过'},system:{type:'object',title:'系统信息'},python:{type:'object',title:'Python 环境',properties:{executable:{type:'string',title:'已检查的解释器'},version:{type:'string',title:'Python 版本'}}},checks:{type:'array',title:'检查明细',items:{type:'object'}}},required:['ok','system','checks']},
37
+ async run(input,{runProcess,signal}) {
38
+ const report={ok:true,system:{platform:os.platform(),architecture:os.arch(),kernel:os.release()},checks:[]};
39
+ if(process.platform==='linux'){
40
+ try{report.system.distribution=await readFile('/etc/os-release','utf8');}catch{}
41
+ }
42
+ const add=(name,ok,actual,status='error',expected)=>{report.checks.push({name,status:ok?'available':status,actual,expected});if(!ok)report.ok=false;};
43
+ const probe=async(file,args)=>runProcess(file,args,{signal,timeout:15000});
44
+ if(input.python){
45
+ const requested=input.python;
46
+ try {
47
+ const result=await probe(requested.executable,['-c',pythonProbe,JSON.stringify(requested.packages??[])]);
48
+ if(result.exitCode!==0)throw new Error(result.stderr||'Python 查询失败');
49
+ report.python=JSON.parse(result.stdout);
50
+ const parsed=semver.coerce(report.python.version);
51
+ const compatible=!!parsed && semver.satisfies(parsed,requested.version??'>=3.9');
52
+ add('python',compatible,report.python.version,'incompatible',requested.version??'>=3.9');
53
+ for(const pkg of report.python.packages){
54
+ const range=requested.packages.find(p=>p.name===pkg.name)?.version;
55
+ const version=semver.coerce(pkg.version??'');
56
+ add(`python.package:${pkg.name}`,pkg.ok&&(!range||(version&&semver.satisfies(version,range))),pkg.version??pkg.error,pkg.ok?'incompatible':'not_found',range);
57
+ }
58
+ if(requested.pip){
59
+ const pip=await probe(report.python.executable,['-m','pip','--version']);
60
+ add('pip',pip.exitCode===0,pip.stdout||pip.stderr,'not_found');
61
+ }
62
+ } catch(error){
63
+ if(signal?.aborted)throw error;
64
+ add('python',false,error.message,error.code==='ENOENT'?'not_found':'error','目标机可用解释器');
65
+ }
66
+ }
67
+ if(input.npu){
68
+ if(process.platform!=='linux')add('npu',false,'当前 NPU 适配仅支持 Linux','unsupported');
69
+ else try{
70
+ const r=await probe(input.npuCommand??'npu-smi',['info','-l']);
71
+ const count=r.stdout.match(/Total\s+NPU\s+count\s*:\s*(\d+)/i);
72
+ add('npu',r.exitCode===0&&Number(count?.[1])>0,r.stdout||r.stderr,r.exitCode===0?'not_found':'error','当前进程可见的昇腾 NPU');
73
+ }catch(error){if(signal?.aborted)throw error;add('npu',false,error.message,error.code==='ENOENT'?'not_found':'error');}
74
+ }
75
+ if(!report.ok)throw new FlowError('目标环境检查未通过,请根据检查明细处理后重试',{code:'ENVIRONMENT_FAILED',details:report});
76
+ return report;
77
+ },
78
+ },
79
+ },
80
+ };
81
+ // 报告模式将“检测完成”与“检测通过”分开;旧 check 保持失败即终止的兼容语义。
82
+ plugin.actions.report={
83
+ ...plugin.actions.check,
84
+ title:'生成环境报告',description:'返回结构化检测结果;未通过时返回 ok:false,由工作流条件决定是否继续。',
85
+ conditions:{passed:{title:'环境检查通过',path:'ok',op:'eq',value:true}},
86
+ async run(input,context){
87
+ try{return await plugin.actions.check.run(input,context);}
88
+ catch(error){if(error.code==='ENVIRONMENT_FAILED')return error.details;throw error;}
89
+ },
90
+ };
91
+ plugin.commands['env.report']={description:'检测环境并返回报告(不因检查未通过抛错)',action:'report'};
92
+ export default definePlugin(plugin);
@@ -0,0 +1 @@
1
+ {"name":"@mytools/environment","version":"0.1.0","type":"module","main":"index.js","files":["index.js"],"license":"MIT","dependencies":{"@mytools/sdk":"0.1.0","semver":"7.7.4"}}
@@ -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.
@@ -0,0 +1,23 @@
1
+ import { definePlugin, FlowError } from '@mytools/sdk';
2
+ import { writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ const text={type:'string',title:'文本'};
6
+ const textInput={type:'object',required:['text'],properties:{text},additionalProperties:false};
7
+ export default definePlugin({
8
+ id:'@mytools/examples',version:'0.1.0',title:'基础示例',description:'文本处理、文件输出和目标 Python 调用示例,用于组合第一个工作流。',
9
+ platforms:['win32','linux'],architectures:['x64','arm64'],
10
+ commands:{
11
+ 'text.upper':{description:'将文本转为大写',action:'uppercase'},
12
+ 'text.ask':{description:'交互输入文本并转大写',async run(_input,{ui,signal}){const text=await ui.input('输入文本');signal.throwIfAborted();return {text:text.toUpperCase()};}},
13
+ },
14
+ actions:{
15
+ uppercase:{title:'文本转大写',description:'将输入文本中的英文字母转成大写,输出可以继续传给其他步骤。',input:textInput,output:textInput,async run({text}){return {text:text.toUpperCase()};}},
16
+ append:{title:'追加文本',description:'在一段文本末尾追加内容。',input:{type:'object',required:['text','suffix'],properties:{text,suffix:{type:'string',title:'追加内容',default:' ✓'}},additionalProperties:false},output:textInput,async run({text,suffix}){return {text:text+suffix};}},
17
+ save:{title:'保存文本文件',description:'把文本保存到本次运行的结果目录,输出文件路径。',input:textInput,output:{type:'object',properties:{path:{type:'string',title:'结果文件路径'}},required:['path']},async run({text},{outputDir}){const file=path.join(outputDir,'result.txt');await writeFile(file,text,'utf8');return {path:file};}},
18
+ pythonEcho:{title:'调用 Python 脚本',description:'使用目标环境解释器执行随插件分发的 Python 脚本,返回解释器和处理结果。',input:{type:'object',required:['executable','text'],properties:{executable:{type:'string',title:'Python 解释器',description:'建议选择环境检查步骤输出的 python.executable'},text},additionalProperties:false},output:{type:'object',properties:{text,executable:{type:'string',title:'实际解释器'},version:{type:'string',title:'Python 版本'}},required:['text','executable','version']},async run({executable,text},{runProcess,signal,resourceDir}){
19
+ const r=await runProcess(executable,[path.join(resourceDir,'scripts','echo.py'),text],{signal});
20
+ if(r.exitCode!==0)throw new FlowError(`Python 脚本失败:${r.stderr}`);return JSON.parse(r.stdout);
21
+ }},
22
+ },
23
+ });
@@ -0,0 +1 @@
1
+ {"name":"@mytools/examples","version":"0.1.0","type":"module","main":"index.js","files":["index.js","scripts"],"license":"MIT","dependencies":{"@mytools/sdk":"0.1.0"}}
@@ -0,0 +1,5 @@
1
+ import json
2
+ import sys
3
+
4
+ # 由插件显式指定目标环境解释器;脚本只使用 Python 标准库。
5
+ print(json.dumps({"text": sys.argv[1].upper(), "executable": sys.executable, "version": sys.version.split()[0]}))
@@ -0,0 +1,45 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { mkdir, writeFile, readFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { createApplication } from '../packages/core/src/application.js';
6
+ import { temporary, testPlugin, object } from './helpers.js';
7
+
8
+ test('运行快照独立于之后保存,状态与日志持久化',async t=>{
9
+ const projectDir=await temporary(t);
10
+ const app=await createApplication({projectDir,plugins:[testPlugin()]}); t.after(()=>app.close());
11
+ const w={id:'hello',schemaVersion:1,title:'示例',inputs:object(),steps:[{id:'convert',action:'@test/text:upper',with:{text:'hello'}}],outputs:{text:{$ref:'steps.convert.output.text'}}};
12
+ await app.saveWorkflow(w);
13
+ const run=await app.startRun('hello',{});
14
+ await app.saveWorkflow({...w,steps:[{...w.steps[0],with:{text:'changed'}}]});
15
+ await run.done;
16
+ assert.equal(app.getRun(run.id).status,'succeeded');
17
+ assert.deepEqual(app.getRun(run.id).output,{text:'HELLO'});
18
+ const events=(await readFile(path.join(app.outputDir,run.id,'events.jsonl'),'utf8')).trim().split('\n').map(JSON.parse);
19
+ assert.ok(events.some(e=>e.type==='run.succeeded'));
20
+ assert.ok(!events.some(e=>Object.hasOwn(e,'output')));
21
+ });
22
+
23
+ test('仓库外本地插件可以安装、加载并执行,移除后目录同步',async t=>{
24
+ const root=await temporary(t), pluginDir=path.join(root,'外部插件'),projectDir=path.join(root,'project');
25
+ await mkdir(pluginDir,{recursive:true});
26
+ await writeFile(path.join(pluginDir,'package.json'),JSON.stringify({name:'external-flow-test',version:'1.0.0',type:'module',main:'index.js'}));
27
+ await writeFile(path.join(pluginDir,'index.js'),`export default {id:'external-flow-test',version:'1.0.0',sdk:'^0.1.0',actions:{hello:{title:'外部动作',input:{type:'object'},output:{type:'object'},run:async()=>({value:42})}}};`);
28
+ const app=await createApplication({projectDir,plugins:[]});t.after(()=>app.close());
29
+ await app.installPlugin(pluginDir);
30
+ assert.ok(app.catalog().some(a=>a.id==='external-flow-test:hello'));
31
+ await app.saveWorkflow({id:'external',schemaVersion:1,inputs:object(),steps:[{id:'hello',action:'external-flow-test:hello',with:{}}],outputs:{answer:{$ref:'steps.hello.output.value'}}});
32
+ const run=await app.startRun('external',{});await run.done;
33
+ assert.deepEqual(app.getRun(run.id).output,{answer:42});
34
+ await app.removePlugin('external-flow-test');
35
+ assert.ok(!app.catalog().some(a=>a.id==='external-flow-test:hello'));
36
+ });
37
+
38
+ test('外部配置覆盖项目值并在调用前验证',async t=>{
39
+ const projectDir=await temporary(t);
40
+ await writeFile(path.join(projectDir,'flow.project.json'),JSON.stringify({config:{configured:{prefix:'项目'}}}));
41
+ const plugin={id:'configured',version:'1.0.0',config:{type:'object',properties:{prefix:{type:'string'}},required:['prefix'],additionalProperties:false},actions:{show:{input:object(),output:object({prefix:{type:'string'}},['prefix']),run:async(_input,{config})=>config}}};
42
+ const app=await createApplication({projectDir,plugins:[plugin],config:{configured:{prefix:'目标'}}});t.after(()=>app.close());
43
+ assert.deepEqual(await app.host.invoke('configured:show',{}),{prefix:'目标'});
44
+ await assert.rejects(createApplication({projectDir,plugins:[plugin],config:{configured:{prefix:42}}}),/配置/);
45
+ });