Skip to content

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 调用。