Skip to content

Web 端扩展接入

CoreLib 的 Web Console 前端支持由业务插件动态扩展。插件可以声明自己的配置入口、GUI/物品/自定义资源编辑器、字段说明、列表模板、物品预览兜底和翻译文本。当前实现采用两层接入:

  1. 服务端声明式注册:插件在 src/main/resources/web-console.yml 中声明模块、文件入口、前端脚本和少量节点元信息。
  2. 前端 IIFE 扩展:插件在 web-console/src/main.tsx 中使用 emaki-web-console 导出的注册函数,打包到 src/main/resources/web-extensions/*.js,由 Web Console 动态加载。

这种方式可以让插件的 Java 启动入口保持很薄,同时把复杂 UI、字段说明和本地预览逻辑放在前端扩展中迭代。

接入总览

最小接入需要 4 个部分:

text
YourPlugin/
├── src/main/java/.../YourPlugin.java
├── src/main/resources/
│   ├── plugin.yml
│   ├── web-console.yml
│   └── web-extensions/
│       └── yourplugin-extension.js      # 构建产物
└── web-console/
    ├── package.json
    ├── vite.config.ts
    └── src/main.tsx

启用阶段注册:

java
@Override
public void onEnable() {
    WebConsoleRegistry.registerFromYaml(this);
}

关闭阶段反注册:

java
@Override
public void onDisable() {
    WebConsoleRegistry.unregisterModule(this);
}

业务模块 reload 时也建议先反注册再重新注册,避免旧文件入口、旧 editor 或旧扩展资源残留。

服务端注册文件

在插件资源目录创建 web-console.yml

yaml
# 仅声明结构信息与资源入口,展示文案建议放在前端 i18n 中。
module:
  tone: custom
  icon: '<svg viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg"><path d="M8 8h22v22H8z" fill="none" stroke="currentColor"/></svg>'

files:
  - path: "config.yml"
    kind: CONFIG
  - path: "gui/**/*.yml"
    kind: GUI
    editor: "yourplugin:gui"
  - path: "items/**/*.yml"
    kind: ITEM
    editor: "yourplugin:item"
  - path: "sets/**/*.yml"
    kind: SET
    editor: "yourplugin:set"

extensions:
  - id: "yourplugin:editor"
    resource: "web-extensions/yourplugin-extension.js"

nodes:
  - path: "custom_templates"
    type: dynamic_map
    creatableChildren: true

module

字段说明
name模块显示名。可以省略,省略时使用插件名;当前内置扩展通常交给前端 i18n 覆盖。
summary模块摘要。可以省略,前端扩展可通过翻译覆盖。
tone模块视觉色调标识。前端可用它区分模块类型。
iconSVG 字符串。建议使用纯 pathstroke="currentColor",避免外链资源。

files

字段必填说明
path相对插件数据目录的文件路径。支持固定文件和 glob,例如 recipes/**/*.yml
kind文件类型。内置 CONFIGGUIITEMSCRIPT,其他值会作为自定义资源类型注册。
title文件入口标题。不写时前端可通过 i18n key 覆盖。
comment文件入口说明。不写时前端可通过 i18n key 覆盖。
editor专属 editor ID,例如 emakigem:gem。前端按 editor ID 匹配最高优先级 surface。

CONFIG 固定文件会被结构化展开。CONFIG glob 目录不会一次性读取所有文件,点击子文件时通过 /api/registry/file 懒加载节点。GUIITEM 和自定义 kind 默认走源码读写接口,再由前端 surface 决定是否提供可视编辑。

extensions

字段说明
id扩展唯一 ID。建议使用 插件名:功能,例如 emakigem:item-surface
resource插件 JAR 内资源路径,例如 web-extensions/yourplugin-extension.js

资源路径会被规范化,不能以 / 开头,不能包含 ..。注册成功后 /api/registry 会返回:

json
{
  "moduleId": "YourPlugin",
  "id": "yourplugin:editor",
  "url": "/extensions/YourPlugin/web-extensions/yourplugin-extension.js?v=插件版本",
  "apiVersion": "1.1.0"
}

前端用 <script> 加载扩展脚本,因此扩展资源本身不带 Authorization header。服务端只会返回已经注册过的插件资源路径,不会开放任意文件读取。

nodes

nodes 用于补充服务端结构化配置节点元信息:

yaml
nodes:
  - path: "rewards"
    label: "奖励池"
    comment: "按权重抽取的奖励配置。"
    type: dynamic_map
    creatableChildren: true

常用类型:

类型说明
text单行文本。
number数字输入。
boolean开关。
list通用列表。
stringList字符串列表编辑体验。
numberList数字列表编辑体验。
actionsCoreLib Action 列表。
object分组节点。
dynamic_map对象节点作为可动态增删键值的 map 编辑,不递归展开。
enum:A,B,C静态枚举。
dynamic_enum:目录扫描该目录下 YAML 的 id 或文件名作为选项。

前端扩展也可以用 registerPluginConfig 覆盖或补充这些节点信息。复杂字段说明建议放在前端扩展中,这样可以同时提供中英文、多字段模板和列表 schema。

前端工作区

在插件目录下创建 web-console/package.json

json
{
  "name": "yourplugin-web-console-extension",
  "private": true,
  "version": "0.1.0",
  "type": "module",
  "scripts": {
    "build": "tsc && vite build"
  },
  "dependencies": {
    "emaki-web-console": "0.1.0"
  }
}

根目录 package.json 使用 npm workspaces 管理各模块 Web 扩展。新增插件时,把工作区加入根项目:

json
{
  "workspaces": [
    "EmakiCoreLib/web-console",
    "YourPlugin/web-console"
  ]
}

如果希望根命令能单独构建你的扩展,再添加脚本:

json
{
  "scripts": {
    "build:web:yourplugin": "npm run build -w yourplugin-web-console-extension"
  }
}

Vite 构建配置

业务模块扩展应构建成 IIFE,并把 React 与 CoreLib Web Console API 作为外部全局变量:

ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react({ jsxRuntime: 'classic' })],
  build: {
    outDir: '../src/main/resources/web-extensions',
    emptyOutDir: false,
    lib: {
      entry: 'src/main.tsx',
      name: 'YourPluginWebConsoleExtension',
      formats: ['iife'],
      fileName: () => 'yourplugin-extension.js'
    },
    rollupOptions: {
      external: ['react', 'emaki-web-console'],
      output: {
        globals: {
          react: 'React',
          'emaki-web-console': 'EmakiWebConsole'
        }
      }
    }
  }
});

关键点:

  • outDir 必须指向 src/main/resources/web-extensions,确保 Maven 打包时进入 JAR。
  • emptyOutDir: false 避免清空同目录下其他扩展资源。
  • formats: ['iife'] 让浏览器通过 <script> 直接执行。
  • react external 到全局 React,由 CoreLib 前端壳安装。
  • emaki-web-console external 到全局 EmakiWebConsole,由 installWebConsoleHost() 安装。
  • jsxRuntime: 'classic' 可避免自动 JSX runtime 被打进扩展或产生额外全局依赖。

Maven 构建接入

可以在插件 pom.xml 中加入 exec-maven-plugin,让 Maven 在 generate-resources 阶段构建前端扩展:

xml
<properties>
    <skip.web.extension>false</skip.web.extension>
</properties>

<build>
    <plugins>
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>exec-maven-plugin</artifactId>
            <version>3.5.0</version>
            <executions>
                <execution>
                    <id>install-web-extension-deps</id>
                    <phase>generate-resources</phase>
                    <goals>
                        <goal>exec</goal>
                    </goals>
                    <configuration>
                        <workingDirectory>${project.basedir}/web-console</workingDirectory>
                        <executable>npm</executable>
                        <arguments>
                            <argument>install</argument>
                        </arguments>
                        <skip>${skip.web.extension}</skip>
                    </configuration>
                </execution>
                <execution>
                    <id>build-web-extension</id>
                    <phase>generate-resources</phase>
                    <goals>
                        <goal>exec</goal>
                    </goals>
                    <configuration>
                        <workingDirectory>${project.basedir}/web-console</workingDirectory>
                        <executable>npm</executable>
                        <arguments>
                            <argument>run</argument>
                            <argument>build</argument>
                        </arguments>
                        <skip>${skip.web.extension}</skip>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

当前 Gem、Skills、Item、Cooking 等模块已经使用类似方式构建扩展。若临时跳过前端构建,可以传入 -Dskip.web.extension=true,但要确认已有 web-extensions/*.js 产物仍是最新的。

扩展入口文件

最小 web-console/src/main.tsx

tsx
import { registerModuleLocale, registerPluginConfig, registerPluginGuiEditor } from 'emaki-web-console';

const MODULE = 'YourPlugin';

registerModuleLocale(MODULE, 'zh-CN', {
  'yourplugin.module.name': 'YourPlugin',
  'yourplugin.module.summary': '自定义配置、GUI 与物品编辑',
  'yourplugin.file.config.title': '主配置',
  'yourplugin.file.config.comment': '插件主配置文件。',
  'yourplugin.surface.gui': 'YourPlugin GUI'
});

registerModuleLocale(MODULE, 'en-US', {
  'yourplugin.module.name': 'YourPlugin',
  'yourplugin.module.summary': 'Custom config, GUI, and item editing',
  'yourplugin.file.config.title': 'Main Config',
  'yourplugin.file.config.comment': 'Main plugin configuration.',
  'yourplugin.surface.gui': 'YourPlugin GUI'
});

registerPluginConfig({
  moduleId: MODULE,
  metaFields: [
    ['feature.enabled', '启用功能', '控制该功能是否启用。', 'boolean'],
    ['feature.cooldown', '冷却时间', '功能触发后的冷却时间,单位 tick。', 'number']
  ],
  ruleFields: {
    item_sources: ['物品来源', '支持 minecraft、CraftEngine、ItemsAdder、Nexo 等来源格式。', 'stringList'],
    actions: ['动作列表', '按顺序执行的 CoreLib Action。', 'actions']
  }
});

registerPluginGuiEditor({
  moduleId: MODULE,
  editorId: 'yourplugin:gui',
  label: 'YourPlugin GUI',
  fields: [
    ['confirm', '确认按钮', '执行核心操作的按钮槽位。', 'text'],
    ['preview', '预览槽位', '展示当前配置结果的槽位。', 'text']
  ]
});

扩展脚本执行时,CoreLib 已经把 window.Reactwindow.EmakiWebConsole 安装好。通过 emaki-web-console 导入的函数会在构建时映射到这个全局对象。

Surface 匹配规则

Web Console 使用 surface registry 决定文件打开时渲染哪个页面。匹配顺序:

  1. editorId 精确匹配。
  2. moduleId + kind 匹配。
  3. kind 匹配。

优先级高的注册会覆盖低优先级注册。通用 GUI/ITEM surface 由 CoreLib 注册,插件可以用更高优先级覆盖指定模块或指定 editor。

注册自定义 surface

tsx
import type { SurfaceProps } from 'emaki-web-console';
import { registerSurface } from 'emaki-web-console';

function SetEditorSurface(props: SurfaceProps) {
  return <div>{props.module.name} / {props.file.path}</div>;
}

registerSurface({
  kind: 'SET',
  moduleId: 'YourPlugin',
  editorId: 'yourplugin:set',
  component: SetEditorSurface,
  label: '套装',
  priority: 120
});

SurfaceProps 提供:

属性说明
module当前模块注册信息。
file当前文件入口。
apiApiClient 实例,可读写配置、GUI、物品、资源和预览。
childPathglob 子文件路径。
refreshKey注册表刷新标记。
editor当前 editor 描述。
onReload请求父级重新加载当前文件。
setToolbar设置顶部工具栏状态、保存按钮、源码内容和变更提示。
showLocalChrome是否需要 surface 自己显示本地外壳。

Editor 描述与字段

专属 editor 可以声明字段元信息,供通用 GuiEditorSurfaceItemEditorSurface 渲染:

tsx
import { registerEditorDescriptor, registerEditorField } from 'emaki-web-console';

registerEditorDescriptor('YourPlugin', 'yourplugin:item', {
  id: 'yourplugin:item',
  moduleId: 'YourPlugin',
  title: '自定义物品',
  kindLabel: '物品',
  baseName: '<white>预览物品</white>',
  baseLore: ['<gray>默认 Lore</gray>']
});

registerEditorField('YourPlugin', 'yourplugin:item', {
  path: 'display_name',
  label: '显示名',
  comment: '物品展示名称,支持 MiniMessage。',
  type: 'text'
});

常用字段类型:

type说明
text单行文本。
number数字。
boolean开关。
textarea多行文本。
stringList字符串列表。
numberList数字列表。
enum下拉选项,需要 options
jsonJSON/对象编辑。
actionsCoreLib Action 列表编辑。

配置节点增强

registerPluginConfig 是配置页最常用的批量注册入口:

tsx
registerPluginConfig({
  moduleId: 'YourPlugin',
  metaFields: [
    ['quality.tiers', '品质池', '按权重抽取的品质列表。', 'list']
  ],
  ruleFields: {
    enabled: ['启用', '是否启用当前功能或条目。', 'boolean'],
    chance: ['概率', '成功率、触发率或抽取概率。', 'number']
  },
  createTemplates: [
    ['quality.item_meta.tiers', {
      id: 'quality-tier-display',
      label: '品质显示规则',
      fields: [
        { path: 'name_actions', label: '名称动作链', type: 'list', defaultValue: [] },
        { path: 'lore_actions', label: 'Lore 动作链', type: 'list', defaultValue: [] }
      ]
    }]
  ],
  listItemSchemas: [
    ['quality.tiers', [
      { path: 'name', label: '品质名', type: 'text', defaultValue: '普通' },
      { path: 'weight', label: '权重', type: 'number', defaultValue: 1 },
      { path: 'multiplier', label: '倍率', type: 'number', defaultValue: 1 }
    ], { uniqueBy: 'name' }]
  ]
});

可拆开使用的低层函数包括:

函数用途
registerConfigNodeMeta(moduleId, path, meta)精确覆盖某个配置路径的标签、说明、类型、选项等。
registerConfigNodeRule(moduleId, matcher, meta)按 key、prefix、suffix、contains 或函数匹配一批节点。
registerConfigCreateTemplate(moduleId, nodePath, template)给 dynamic map 或可创建节点添加新建模板。
registerConfigListItemSchema(moduleId, listPath, fields, options)给列表项提供字段 schema。
registerUniqueListField(moduleId, listPath, fieldPath)指定列表项内唯一字段,避免重复键。

物品字段渲染器

通用物品编辑器会按字段 type 查找自定义渲染器。适合成本、效果、插槽、套装部位等复杂结构。

tsx
import { registerItemFieldRenderer, type ItemFieldRendererContext } from 'emaki-web-console';

function CostEditor({ context }: { context: ItemFieldRendererContext }) {
  return (
    <div>
      <strong>{context.field.label}</strong>
      <button onClick={() => context.setField(context.field.path, { currencies: [], materials: [] })}>
        重置费用
      </button>
      {context.renderDefault()}
    </div>
  );
}

registerItemFieldRenderer(
  'cost',
  context => <CostEditor context={context} />,
  { moduleId: 'YourPlugin', editorId: 'yourplugin:item', priority: 100 }
);

ItemFieldRendererContext 中常用属性:

属性说明
data当前编辑中的完整 YAML 数据。
originalData初始数据,用于比较变更。
field当前字段描述。
value当前字段值。
changed该字段是否变更。
actionTypesResult服务端返回的 name/lore action 类型。
economyProviders当前经济提供器选项。
editorFields当前 editor 的全部字段描述。
setField(path, value)更新字段值。
renderDefault()渲染默认字段编辑器。

物品预览兜底

服务端 /api/items/preview 会生成真实结构的预览。前端扩展还可以注册本地兜底,在服务端预览失败或需要即时展示时使用:

tsx
import { registerItemPreviewFallback, type ItemPreviewFallbackContext } from 'emaki-web-console';

function localPreview(context: ItemPreviewFallbackContext) {
  return {
    kind: 'yourplugin:item',
    id: String(context.data.id ?? ''),
    displayName: String(context.data.display_name ?? context.baseName),
    lore: Array.isArray(context.data.lore) ? context.data.lore.map(String) : context.baseLore
  };
}

registerItemPreviewFallback(localPreview, {
  moduleId: 'YourPlugin',
  editorId: 'yourplugin:item',
  priority: 100
});

兜底预览应只做展示辅助,不应假装完成 PDC 写入、外部物品解析或服务端实际生成逻辑。

SourceDocumentAdapter

默认源码读写会根据 kind 分流到配置、脚本、GUI、物品或自定义资源接口。特殊格式可以注册自己的源码适配器:

tsx
import { registerSourceDocumentAdapter } from 'emaki-web-console';

registerSourceDocumentAdapter({
  kind: 'SET',
  moduleId: 'YourPlugin',
  editorId: 'yourplugin:set',
  priority: 100,
  adapter: {
    language: 'yaml',
    read: (api, context) => api.readTextDocument({ kind: context.file.kind, moduleId: context.module.id, path: context.path }),
    save: (api, context, content, revision) => api.saveTextDocument({ kind: context.file.kind, moduleId: context.module.id, path: context.path }, content, revision),
    defaultContent: ({ name }) => `id: ${name}\ndisplay_name: "${name}"\n`
  }
});

适配器可控制:

字段说明
read读取源码。
save保存源码。
parse把源码解析为对象。
serialize把对象序列化为源码。
defaultContent新建文件时生成默认内容。
language编辑器语言,例如 yamljavascripttext

翻译接入

使用 registerModuleLocale 注册模块自己的文本:

tsx
registerModuleLocale('YourPlugin', 'zh-CN', {
  'yourplugin.module.name': 'YourPlugin',
  'yourplugin.module.summary': '自定义模块',
  'yourplugin.file.config.title': '主配置',
  'yourplugin.field.feature.enabled': '启用功能'
});

推荐 key 约定:

key 形态用途
yourplugin.module.name模块显示名。
yourplugin.module.summary模块摘要。
yourplugin.file.<name>.title文件入口标题。
yourplugin.file.<name>.comment文件入口说明。
yourplugin.field.<path>配置或 editor 字段标签。
yourplugin.comment.<path>配置或 editor 字段说明。
yourplugin.surface.<name>自定义 surface 标签。

CoreLib 前端内置 getLocale()t()registerLocale()registerModuleLocale()。内置模块通常同时注册 zh-CNen-US

ApiClient 常用方法

扩展 surface 可以通过 props.api 调用接口:

方法说明
registry()重新获取模块注册表。
registryFileNodes(moduleId, path)获取 glob 子文件结构化节点。
saveRegistryValue(moduleId, filePath, path, value, revision)保存结构化配置节点。
createFile(moduleId, fileId, name, content?)创建 glob 子文件。
deleteFile(moduleId, fileId, path, confirmPath)删除子文件。
readTextDocument(target) / saveTextDocument(target, content, revision?)按 kind 读写源码。
readGui() / saveGui()读写 GUI YAML。
readItem() / saveItem()读写物品 YAML。
readResource() / saveResource()读写自定义 kind 资源。
previewItem(content, previewLevel, baseName?, baseLore?)请求服务端物品预览。
actionTypes()获取 name/lore action 类型。
economyProviders()获取经济提供器选项。

写入类接口受 web_console.security.allow_config_write 控制,并使用 revision 检测并发修改。

当前内置模块示例

模块扩展 ID主要接入点
EmakiAttributeemakiattribute:locale配置字段说明、DamageCause 运行时枚举、属性/伤害/条件/lore format 元信息。
EmakiForgeemakiforge:gui-surface品质配置 schema、锻造 GUI editor、配方与 item adjustment 文件入口说明。
EmakiStrengthenemakistrengthen:gui-surface强化配置字段、配方字段、强化 GUI editor。
EmakiCookingemakicooking:gui-surface工位配置、展示实体配置、Cooking GUI editor。
EmakiGememakigem:item-surface宝石 editor、插槽物品 editor、升级/成本/效果自定义字段、本地预览兜底、GUI editor。
EmakiSkillsemakiskills:gui-surface技能配置、资源配置、触发器字段、技能 GUI editor。
EmakiItememakiitem:localeSET 文件类型、EmakiItem 物品 editor、套装 editor、套装字段渲染、源码文档适配。

调试流程

  1. 运行根目录或模块目录的 Web 构建:npm run build:web:yourplugin 或在 YourPlugin/web-console 下执行 npm run build
  2. 确认 src/main/resources/web-extensions/yourplugin-extension.js 已更新。
  3. 构建插件 JAR,确认 JAR 内包含 web-console.ymlweb-extensions/yourplugin-extension.js
  4. 启动服务端,开启 web_console.enabled=true
  5. 执行 /emakicorelib web 获取访问地址。
  6. 如果模块树没有出现插件,检查插件是否启用、allowed_modules 是否放行、控制台是否有 web-console.yml 解析警告。
  7. 如果扩展脚本没有生效,打开浏览器控制台,检查 /api/registryextensions 列表和 /extensions/...js 请求状态。
  8. 临时执行 /emakicorelib webdebug frontend/emakicorelib webdebug backend 查看请求日志,排查完成后再次执行关闭。

兼容与约束

  • CoreLib 当前前端 API 版本为 1.1.0,注册表返回的扩展项会带上 apiVersion
  • 扩展脚本应只依赖 ReactEmakiWebConsole 全局,不要假设页面中存在其他全局库。
  • 服务端 web-console.yml 负责结构入口,前端扩展负责展示语言和复杂 UI;不要把大量展示文案塞进 YAML 注册文件。
  • extensions[].resource 必须是插件 JAR 内资源路径,不能指向外部 URL。
  • kind 自定义值会按大写匹配;注册 surface、文件类型标签和 source adapter 时保持同一个 kind 名称。
  • 前端本地预览只作为编辑辅助,最终运行效果仍以服务端加载、PDC 写入和实际业务逻辑为准。