Script 命令是介于 View 与 No-View 之间的一种形态:
- 不出现在命令面板中,用户看不到这条命令;
- 仅供 View 命令内部调用,用于在 Node Worker 中执行业务逻辑(如读写文件、处理数据等);
- 通过
@sofastapp/api中的Backend.run()调用,并且支持拿到脚本返回结果。
本页介绍 Script 的清单约定、目录结构以及在 View 中调用的方式。
1. 清单声明:mode: "script"
在插件根目录的 package.json 中,Script 命令与其它命令一样声明在顶层 commands 数组中,只是 mode 设置为 "script":
jsonc
{
"name": "hello-world-plugin",
"version": "0.0.1",
"commands": [
{
"name": "api-backend",
"title": "API: Backend (Script)",
"mode": "view"
},
{
"name": "backend-demo-script",
"title": "Backend Demo Script",
"mode": "script"
}
]
}约定:
mode: "script":- 不会出现在 Sofast 的命令搜索 / 列表中;
- 不会被当作独立命令执行;
- 仅通过
Backend.run('<name>')从 View 内部调用。
commands[].name必须与构建后的脚本文件名一致(见下文构建规则)。
2. 目录与构建结构
Script 的代码结构与 No-View 基本一致,推荐:
- 源码目录:
src/no-view/<command>.ts - 构建输出:使用独立的
vite.worker.config.ts构建为dist/<command>.mjs,再根据你的发布目录布局,将产物放到插件根目录下。
宿主在运行 Script 或 No-View 命令时,会在插件根目录按以下顺序查找入口脚本:
<pluginRoot>/<command>.mjs<pluginRoot>/<command>.js<pluginRoot>/workers/<command>.mjs<pluginRoot>/workers/<command>.js
因此,发布插件时需要保证最终安装到 Sofast 的插件根目录中,脚本入口文件位于上述路径之一(例如:直接把 dist/backend-demo-script.mjs 放在插件根目录)。
构建配置示例(与 No-View 相同):
ts
// vite.worker.config.ts
import { defineConfig } from 'vite';
import fs from 'node:fs';
import path from 'node:path';
function discoverNoViewInputs() {
const inputs: Record<string, string> = {};
const dir = path.resolve(__dirname, 'src/no-view');
try {
const items = fs.readdirSync(dir, { withFileTypes: true });
for (const it of items) {
if (it.isFile() && it.name.endsWith('.ts')) {
const name = it.name.replace(/\.ts$/, '');
inputs[name] = path.join(dir, it.name);
}
}
} catch {}
return inputs;
}
export default defineConfig({
build: {
outDir: 'dist',
emptyOutDir: false,
rollupOptions: {
external: ['worker_threads', /^node:.*/],
input: discoverNoViewInputs(),
output: {
entryFileNames: '[name].mjs',
chunkFileNames: 'assets/[name]-[hash].mjs',
manualChunks: undefined,
},
},
},
});3. Script 入口代码示例
Script 运行在 Node Worker 中,推荐使用 @sofastapp/api/node 提供的 runtime API:
ts
// src/no-view/backend-demo-script.ts
import { ctx, done, log, onError, progress } from '@sofastapp/api/node';
onError();
(async () => {
const { args, command, pluginPath } = ctx();
const value =
typeof args?.value === 'number' ? args.value : Number(args?.value) || 0;
log('backend-demo-script: start', { command, pluginPath, value });
progress(0.3);
await new Promise((r) => setTimeout(r, 150));
progress(0.9);
const doubled = value * 2;
log('backend-demo-script: done', { value, doubled });
done({
ok: true,
value,
doubled,
ts: Date.now(),
});
})();关键点:
ctx():获取当前命令名、插件根目录、调用时传入的args等。log()/progress():发送日志、进度信息(会显示在宿主日志中)。done(result):结束脚本并返回结果给调用方。onError():自动捕获未处理的异常并转换为错误事件。
4. 在 View 中调用 Script
在 View 命令的 UI 代码中,通过 @sofastapp/api 暴露的 Backend.run 调用 Script:
ts
import { Backend } from '@sofastapp/api';
async function runScript() {
const res = await Backend.run<{
ok: boolean;
value: number;
doubled: number;
ts: number;
}>('backend-demo-script', { value: 2 }, { timeoutMs: 10_000 });
console.log('script result', res);
}说明:
- 第一个参数必须是
package.json.commands[].name,且对应的命令mode: "script"。 - 第二个参数是传给脚本的
args,会通过ctx().args传入 Worker。 - 第三个参数可选,用于设置超时时间
timeoutMs(实际执行有上限保护)。 - 返回值就是脚本中
done(result)的result。
5. Script vs No-View 的区别
两者都运行在 Node Worker 中、使用相同的 runtime API,但定位不同:
No-View (
mode: "no-view")- 作为独立命令出现在命令面板;
- 适合由用户主动触发的「无界面任务」;
- 也可以被宿主其他逻辑调用。
Script (
mode: "script")- 不出现在命令面板,用户看不到;
- 仅供 View 内部通过
Backend.run调用; - 更适合作为「View 的后端函数」,实现需要 Node 能力的局部逻辑(例如读写本地文件、调用第三方 SDK 等)。
推荐实践:
- 将需要被用户直接调用的无界面任务设计为
no-view命令; - 将只服务于某个 UI 页面、不会单独出现的后台逻辑设计为
script命令,并通过Backend.run调用。