RunJS 插件扩展点(ctx 文档 / snippets / 场景映射)

当插件新增或扩展 RunJS 能力时,建议通过 官方扩展点把“上下文映射 / ctx 文档 / 示例代码”一并注册进去,让:

  • CodeEditor 能自动补全 ctx.xxx.yyy
  • AI coding 能获得结构化的 ctx API reference + 示例

本章介绍两个扩展点:

  • registerRunJSContextContribution(...)
  • registerRunJSSnippet(...)

1. registerRunJSContextContribution

用于注册 RunJS 的“贡献”(contribution),典型用途:

  • 新增/覆盖 RunJSContextRegistry 映射(modelClass -> RunJSContext,含 scenes
  • FlowRunJSContext 或自定义 RunJSContext 扩展 RunJSDocMetactx API 的说明/示例/补全模板)

行为说明

  • contribution 会在 setupRunJSContexts() 阶段统一执行;
  • 如果 setupRunJSContexts() 已完成,晚注册会立即执行一次(无需重跑 setup);
  • 每个 contribution 对每个 RunJSVersion 最多只会执行一次

示例:新增一个可编写 JS 的模型上下文

import { registerRunJSContextContribution, FlowRunJSContext, RunJSContextRegistry } from '@nocobase/flow-engine';

registerRunJSContextContribution(({ version, FlowRunJSContext: BaseCtx, RunJSContextRegistry: Registry }) => {
  if (version !== 'v1') return;

  class MyPluginRunJSContext extends BaseCtx {}

  // 1) ctx 文档/补全(RunJSDocMeta)
  MyPluginRunJSContext.define({
    label: 'MyPlugin RunJS context',
    properties: {
      myPlugin: {
        description: 'My plugin namespace',
        detail: 'object',
        properties: {
          hello: {
            description: 'Say hello',
            detail: '(name: string) => string',
            completion: { insertText: `ctx.myPlugin.hello('World')` },
          },
        },
      },
    },
  });

  // 2) model -> context 映射(scene 影响编辑器补全/snippets 筛选)
  Registry.register('v1', 'MyPluginJSModel', MyPluginRunJSContext, { scenes: ['block'] });
});

2. registerRunJSSnippet

用于注册 RunJS 的示例代码片段(snippets),用于:

  • CodeEditor snippet 补全
  • 作为 AI coding 的示例/参考素材(按场景/版本/locale 可裁剪)

推荐 ref 命名

建议使用:plugin/<pluginName>/<topic>,例如:

  • plugin/plugin-my/foo
  • plugin/plugin-my/api-request-example

避免与 core 的 global/*scene/* 冲突。

冲突策略

  • 默认不覆盖已有 ref(返回 false,但不抛错)
  • 需要覆盖时显式传入 { override: true }

示例:注册一个 snippet

import { registerRunJSSnippet } from '@nocobase/flow-engine';

registerRunJSSnippet('plugin/plugin-my/hello', async () => ({
  default: {
    label: 'Hello (My Plugin)',
    description: 'Minimal example for my plugin',
    prefix: 'my-hello',
    versions: ['v1'],
    scenes: ['block'],
    contexts: ['*'],
    content: `
// My plugin snippet
ctx.message.success('Hello from plugin');
`,
  },
}));

3. 最佳实践

  • 文档 + snippets 分层维护
    • RunJSDocMeta:描述/补全模板(短、结构化)
    • snippets:长示例(可复用、可按 scene/version 筛选)
  • 避免 prompt 过长:示例不宜过多,优先收敛到“最小可运行模板”。
  • 场景优先级:若你的 JS 代码主要运行在表单/表格等场景,请尽量填对 scenes,提升补全与示例相关性。

4. 基于实际 ctx 隐藏补全:hidden(ctx)

某些 ctx API 具有强场景性(例如 ctx.popup 仅在弹窗/抽屉打开时可用)。当你希望在补全时隐藏这些不可用 API,可以在 RunJSDocMeta 中为对应条目定义 hidden(ctx)

  • 返回 true:隐藏当前节点及其子树
  • 返回 string[]:隐藏当前节点下的指定子路径(支持一次返回多个路径;路径为相对路径;按前缀匹配隐藏子树)

hidden(ctx) 支持 async:你可以使用 await ctx.getVar('ctx.xxx') 来判断(由使用者自行决断)。建议尽量快速、无副作用(不要发网络请求)。

示例:仅当存在 popup.uid 时展示 ctx.popup.* 补全

FlowRunJSContext.define({
  properties: {
    popup: {
      description: 'Popup context (async)',
      hidden: async (ctx) => !(await ctx.getVar('ctx.popup'))?.uid,
      properties: {
        uid: 'Popup uid',
      },
    },
  },
});

示例:popup 可用但隐藏部分子路径(仅相对路径;例如隐藏 recordparent.record

FlowRunJSContext.define({
  properties: {
    popup: {
      description: 'Popup context (async)',
      hidden: async (ctx) => {
        const popup = await ctx.getVar('ctx.popup');
        if (!popup?.uid) return true;
        const hidden: string[] = [];
        if (!popup?.record) hidden.push('record');
        if (!popup?.parent?.record) hidden.push('parent.record');
        return hidden;
      },
      properties: {
        uid: 'Popup uid',
        record: 'Popup record',
        parent: {
          properties: {
            record: 'Parent record',
          },
        },
      },
    },
  },
});

说明:CodeEditor 始终启用基于实际 ctx 的补全过滤(fail-open,不抛错)。

5. 运行时 info/meta 与上下文信息 API(面向补全与大模型)

除了通过 FlowRunJSContext.define()(静态)维护 ctx 文档外,你也可以在运行时通过 FlowContext.defineProperty/defineMethod 注入 info/meta,并通过以下 API 输出可序列化的上下文信息,供 CodeEditor/大模型使用:

  • await ctx.getApiInfos(options?):静态 API 信息
  • await ctx.getVarInfos(options?):变量结构信息(来源 meta,支持 path/maxDepth 展开)
  • await ctx.getEnvInfos():运行时环境快照

5.1 defineMethod(name, fn, info?)

info 支持(均可选):

  • description / detail / examples
  • ref: string | { url: string; title?: string }
  • params / returns(JSDoc-like)

注意:getApiInfos() 输出为静态 API 文档,不会包含 deprecated / disabled / disabledReason 等字段。

示例:为 ctx.refreshTargets() 提供文档链接

ctx.defineMethod('refreshTargets', async () => {
  // ...
}, {
  description: '刷新目标区块的数据',
  detail: '() => Promise<void>',
  ref: { url: 'https://docs.nocobase.com/', title: 'Docs' },
});

5.2 defineProperty(key, { meta?, info? })

  • meta:用于变量选择器 UI(getPropertyMetaTree / FlowContextSelector),决定是否展示、树结构、禁用等(支持函数/async)。
    • 常用字段:title / type / properties / sort / hidden / disabled / disabledReason / buildVariablesParams
  • info:用于静态 API 文档(getApiInfos)与面向大模型的描述,不影响变量选择器 UI(支持函数/async)。
    • 常用字段:title / type / interface / description / examples / ref / params / returns

当仅提供 meta(未提供 info)时:

  • getApiInfos() 不会返回该 key(因为静态 API 文档不从 meta 推断)
  • getVarInfos() 会基于 meta 构建变量结构(用于变量选择器/动态变量树)

5.3 上下文信息 API

用于输出“可用的上下文能力信息”,返回形态为方案 A(不再包 { apis/envs/... } 一层)。

type FlowContextInfosEnvNode = {
  description?: string;
  getVar?: string; // 可直接用于 await ctx.getVar(getVar),推荐以 "ctx." 开头
  value?: any; // 已解析的静态值(可序列化,仅在能推断到时返回)
  properties?: Record<string, FlowContextInfosEnvNode>;
};

type FlowContextApiInfos = Record<string, any>; // 静态文档(顶层一层)
type FlowContextVarInfos = Record<string, any>; // 变量结构(可按 path/maxDepth 展开)
type FlowContextEnvInfos = {
  popup?: FlowContextInfosEnvNode;
  block?: FlowContextInfosEnvNode;
  flowModel?: FlowContextInfosEnvNode;
  resource?: FlowContextInfosEnvNode;
  record?: FlowContextInfosEnvNode;
  currentViewBlocks?: FlowContextInfosEnvNode;
};

常用参数:

  • getApiInfos({ version }):RunJS 文档版本(默认 v1
  • getVarInfos({ path, maxDepth }):剪裁与最大展开层级(默认 3)

注意:以上 API 的返回结果都不包含函数,适合直接序列化传给大模型。

5.4 await ctx.getVar(path)

当你只有一个“变量路径字符串”(例如来自配置/用户输入),希望直接拿到该变量的运行时值时,可以使用 getVar

  • 示例:const v = await ctx.getVar('ctx.record.roles.id')
  • path 为以 ctx. 开头的表达式路径(例如 ctx.record.id / ctx.record.roles[0].id

另外:以下划线 _ 开头的方法/属性会被视为私有成员,不会出现在 getApiInfos() / getVarInfos() 的输出中。