插件规范(AI / 人类可执行契约)
本文是 TransBox 扩展的权威实现说明。所有字段、签名、文件名以仓库源码为准(manifest.py、base.py、registry.py、glossary.py、plugins_mixin.py 及 extensions/ 示例)。
阅读顺序建议:mod.json → 选 type → 抄最小模板 → 本地热调 → 导出。
1. 目录结构
每个 MOD 是一个独立文件夹,根目录必须有清单:
my_mod/
├── mod.json # 必选(或 manifest.json)
├── icon.svg # 推荐;也会自动探测 icon.png/webp/jpg
├── README.md # 可选,工坊展示说明
├── theme.json # type=theme
├── terms.json # type=glossary(推荐)
├── terms.txt # type=glossary(= 或 Tab 分隔)
├── ja.json # type=i18n
├── my_provider.py # Python 类入口
└── CustomBackground.qml # ui_extensions 引用的组件2. mod.json 字段表
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
id | string | 是 | — | 无 id 整包拒绝。建议 作者.类型.名称,勿用 builtin.* |
name | string | 建议 | "未命名 MOD" | 工坊显示名 |
version | string | 建议 | "1.0.0" | 无强制 semver 校验 |
author | string | 建议 | "未知作者" | — |
description | string | 否 | "" | 简介 |
type | string | 建议 | "glossary" | 见 §3 |
entrypoint | string | 视类型 | "" | 见 §4 |
icon | string|null | 否 | null | 相对路径 |
preview_image | string|null | 否 | null | 工坊预览图 |
steam_item_id | int|null | 否 | null | 可读入;当前无额外运行时逻辑 |
config_schema | array | 否 | [] | 见 §6 |
tags | string[] | 否 | [] | — |
supported_langs | string[] | 否 | [] | — |
min_app_version | string | 否 | "1.0.0" | 当前无运行时校验 |
tier | string | 否 | "free" | 无强制校验 |
enabled | bool | 否 | 类型相关 | i18n/theme 默认 false,其余默认 true |
has_settings_tab | bool | 否 | false | 设置中心独立 Tab |
tab_id | string|null | 否 | null | 缺省用 plugin id |
tab_title | string|null | 否 | null | 缺省用 name |
style_layer | string | 否 | "full" | 仅主题:full/material/typography/effects |
ui_extensions | object | 否 | {} | 插槽名 → QML 相对路径 |
language_code | string|null | 否 | null | 仅 i18n,如 ja-JP |
language_name | string|null | 否 | null | 仅 i18n,如 日本語 |
不要写在 mod.json 里的:
window_backdrop— 会被忽略。窗口材质写在theme.json里。source_path/is_builtin/icon_url/category_tier— 运行时注入。
安全分级(只读衍生):theme/glossary/i18n → data;其余 → script。
3. 支持的 type
| type | 基类/实现 | 说明 |
|---|---|---|
glossary | 内置 GlossaryMod | 词表 JSON/TXT |
theme | 数据 + ui_extensions | 主题与视觉 |
i18n | 纯 JSON | 界面语言包 |
middleware | BasePipelineMiddleware | 流水线拦截 |
asr | BaseASRProvider | 语音识别引擎 |
mt | BaseMTEngineProvider | 机器翻译引擎 |
tts | BaseTTSProvider | 语音合成引擎 |
caption_bridge | 内置 | 第三方一般不用 |
不要使用不存在的历史类型名(如 audio_filter、bundle)——代码不会处理。
4. entrypoint 规则
| 类型 | 格式 | 示例 |
|---|---|---|
| Python(asr/mt/tts/middleware) | 相对文件.py:类名 | my_translator.py:CustomDemoMTProvider |
| Python(省略类名) | 相对文件.py | 类名默认 Plugin |
| glossary | 词表文件 | terms.json(默认)/ terms.txt / terms.tsv |
| theme | 主题 JSON | theme.json(默认) |
| i18n | 字典 JSON | ja.json(默认 locale.json) |
真实示例(仓库 extensions/):
example_custom_mt/my_translator.py:CustomDemoMTProvider
example_text_filter_mod/filter_middleware.py:TextFilterMiddleware
example_glossary_gaming/terms.json
example_i18n_japanese/ja.json
example_theme_cyberpunk/theme.json5. 零代码类型:怎么做
5.1 Glossary(术语库)
推荐 terms.json:
{
"Elden Ring": "艾尔登法环",
"Malenia": "玛莲妮亚"
}或:
[{ "src": "Elden Ring", "tgt": "艾尔登法环" }]terms.txt / terms.tsv:
# 注释行
Elden Ring=艾尔登法环
Malenia 玛莲妮亚- 分隔符:第一个
=,否则 Tab #注释、空行跳过- 不支持
->、=>(整行丢弃)
{
"id": "workshop.glossary.elden_ring",
"name": "《艾尔登法环》游戏专有术语库",
"version": "1.0.0",
"author": "TransBox",
"type": "glossary",
"entrypoint": "terms.json",
"description": "游戏专有名词对照"
}5.2 Theme(主题)
mod.json:
{
"id": "workshop.theme.cyberpunk",
"name": "赛博朋克霓虹",
"version": "1.0.0",
"author": "TransBox",
"type": "theme",
"style_layer": "full",
"entrypoint": "theme.json",
"enabled": false,
"ui_extensions": {
"overlay_background": "CyberDeepNeonBackground.qml",
"audio_level": "CyberDeepNeonAudioLevel.qml"
}
}theme.json(window_backdrop 只写在这里):
{
"theme_id": "workshop.theme.cyberpunk",
"name": "赛博朋克霓虹",
"style_layer": "full",
"window_backdrop": "none",
"tokens": {
"primary": "#00f0ff",
"primaryHover": "#38f9d7",
"primaryGlow": "rgba(0, 240, 255, 0.45)",
"glassSurface": "rgba(7, 11, 25, 0.75)",
"glassBorder": "rgba(0, 240, 255, 0.35)"
},
"style_overrides": {
"bgColor": "#070b19",
"textColor": "#00f0ff",
"sourceColor": "#ff2a85",
"fontSize": 28,
"fontFamily": "MiSans VF",
"bgOpacity": 0.65,
"fontOpacity": 1.0
}
}style_overrides 也接受 snake_case 别名(如 text_color、font_size)。window_backdrop 有效值:acrylic | mica | mica_alt | none | blur。
5.3 i18n(语言包)
{
"id": "example.i18n.japanese",
"name": "日本語 UI 言語パック",
"version": "1.0.0",
"type": "i18n",
"author": "TransBox",
"language_code": "ja-JP",
"language_name": "日本語",
"entrypoint": "ja.json"
}字典为扁平 "key": "译文"。可保留 _meta(含 code/name/short_label)。缺失键回退 en-US → zh-CN。
6. config_schema
数组元素必须同时含 key 与 label,否则该项被跳过。
| 键 | 类型 | 必填 | 说明 |
|---|---|---|---|
key | string | 是 | 读 self._config["key"] |
label | string | 是 | 界面标签 |
type | string | 否 | string(默认)/ password / number / boolean / select(file 仅在清单注释中出现,界面会落成文本框,不推荐) |
default | any | 否 | 默认值 |
description | string | 否 | 说明 |
choices | string[] | 否 | select 的选项 |
is_secret | bool | 否 | 密文显示 |
实际控件:
select→ 下拉boolean→ 开关number→ 数字输入(校验)password或is_secret: true→ 密码框- 其它 → 文本框
Python 侧:
def initialize(self, config=None):
super().initialize(config)
self.api_key = self._config.get("api_key", "")UI 修改会持久化并调用 update_config。
7. Python SPI 基类(签名)
7.1 BasePlugin
class BasePlugin(ABC):
def __init__(self, manifest: ModManifest) -> None: ...
@property
def id(self) -> str: ...
@property
def name(self) -> str: ...
@property
def plugin_type(self) -> str: ...
def initialize(self, config: dict | None = None) -> None: ...
def shutdown(self) -> None: ...
def update_config(self, new_config: dict) -> None: ...
def handle_action(self, action: str, params: dict) -> Any: ...
# handle_action 调用 action_<action>(**params)7.2 数据类型
@dataclass
class WordTime:
word: str
start: float
end: float
@dataclass
class Transcript:
text: str
language: str | None = None
words: list[WordTime] | None = None
@dataclass
class Translation:
text: str
source_prefix: str | None = None
complete: bool = True # False 表示增量片段7.3 BaseASRProvider
@dataclass
class ASRCapabilities:
has_native_vad: bool = False
supports_streaming: bool = False
supports_word_timestamps: bool = False
supports_language_detection: bool = True
class BaseASRProvider(BasePlugin):
@property
def capabilities(self) -> ASRCapabilities: ...
@property
def is_streaming_capable(self) -> bool: ... # default False
def ensure_loaded(self, progress_callback=None) -> None: ...
@abstractmethod
def transcribe(
self,
audio: np.ndarray, # float32 mono 16 kHz, [-1, 1]
sample_rate: int = 16000,
language: str = "auto",
stream_id: str = "default",
with_alignment: bool = False,
on_token=None,
) -> Transcript: ...
# 可选流式:
def stream_open(self, stream_id, language="auto"): ...
def stream_feed(self, stream_id, pcm_chunk): ...
def stream_events(self, stream_id): ... # yield (text, is_final)
def stream_close(self, stream_id) -> str: ...
def close_all_streams(self) -> None: ...7.4 BaseMTEngineProvider
class BaseMTEngineProvider(BasePlugin):
def ensure_loaded(self) -> None: ...
@abstractmethod
def translate(
self,
text: str,
source_lang: str = "en",
target_lang: str = "zh",
glossary_context: str | None = None,
target: str | None = None,
source: str | None = None,
) -> Translation: ...
def translate_partial(
self, text, source_lang="en", target_lang="zh",
committed_prefix="", glossary_context=None,
target=None, source=None,
) -> Translation: ... # 默认回退整句 translate7.5 BaseTTSProvider
class BaseTTSProvider(BasePlugin):
@property
def sample_rate(self) -> int: ... # default 24000
@property
def is_streaming_capable(self) -> bool: ...
def ensure_loaded(self) -> None: ...
@abstractmethod
def synthesize(
self,
text: str,
voice_id: str = "",
voice: str | None = None,
language: str = "zh",
) -> tuple[np.ndarray, int]: ... # float32 mono
def stream_tts(self, text, voice_id="", voice=None, language="zh"):
...
def list_voices(self) -> list[dict]: ... # [{'id','name','lang'}]
def register_voice(self, voice_name, audio_samples,
reference_text="", sample_rate=24000) -> bool: ...7.6 BasePipelineMiddleware
@dataclass
class PipelineContext:
session_id: str = ""
sample_rate: int = 16000
source: str = "peer" # 'peer' | 'self'
source_lang: str = "auto"
target_lang: str = "zh"
extra: dict = field(default_factory=dict)
class BasePipelineMiddleware(BasePlugin):
@property
def priority(self) -> int: ... # default 100,越小越先
def process_audio(self, audio: np.ndarray, context) -> np.ndarray: ...
def process_transcript(self, transcript: Transcript, context) -> Transcript: ...
def process_translation(self, translation: Translation, context) -> Translation: ...
def process_tts_audio(self, pcm_audio, sample_rate, context) -> tuple[np.ndarray, int]: ...8. 最小可运行模板
8.1 自定义 MT(可直接改名使用)
mod.json:
{
"id": "myteam.mt.demo",
"name": "我的示范翻译引擎",
"version": "1.0.0",
"author": "Me",
"type": "mt",
"entrypoint": "my_translator.py:MyMTProvider",
"config_schema": [
{
"key": "prefix_tag",
"label": "译文前缀",
"type": "string",
"default": "【Demo】"
}
]
}my_translator.py:
from __future__ import annotations
from typing import Any, Dict, Optional
from transbox.plugins.base import BaseMTEngineProvider, Translation
from transbox.plugins.manifest import ModManifest
class MyMTProvider(BaseMTEngineProvider):
def __init__(self, manifest: ModManifest) -> None:
super().__init__(manifest)
self.prefix = "【Demo】"
def initialize(self, config: Optional[Dict[str, Any]] = None) -> None:
super().initialize(config)
self.prefix = self._config.get("prefix_tag", "【Demo】")
def translate(
self,
text: str,
source_lang: str = "en",
target_lang: str = "zh",
glossary_context: Optional[str] = None,
target: Optional[str] = None,
source: Optional[str] = None,
) -> Translation:
tgt = target or target_lang
clean = (text or "").strip()
return Translation(text=f"{self.prefix} {clean} → {tgt}", complete=True)启用后:设置 → 机器翻译 → 引擎下拉中选中该引擎。
8.2 中间件
from transbox.plugins.base import BasePipelineMiddleware, Transcript, Translation
class TextFilterMiddleware(BasePipelineMiddleware):
@property
def priority(self) -> int:
return 50
def process_transcript(self, transcript: Transcript, context) -> Transcript:
text = transcript.text.replace("那个...", "")
return Transcript(text=text, language=transcript.language)
def process_translation(self, translation: Translation, context) -> Translation:
text = translation.text.replace("坏词", "***")
return Translation(text=text, complete=translation.complete)entrypoint: "filter_middleware.py:TextFilterMiddleware"。
9. 本地热调试
- 启动 TransBox → 设置 → 扩展工坊
- 打开本地 MOD 目录,放入你的文件夹
- 点 刷新
- 开关启用;引擎类再在对应设置 Tab 选中
- 改代码后:停用 → 刷新/重载 → 再启用(或重启客户端)
10. 导出与发布
- 扩展工坊中对你的 MOD 点 导出
- 生成
transbox_mod_<id>_v<version>.zip - 上传 Steam 创意工坊(AppID 1316080)
事实澄清: 当前客户端没有python -m transbox.plugins.steam_ugc --upload
这一 CLI。steam_ugc 模块提供发现工坊路径与 export_mod_for_workshop 打包;上传请用 Steam 客户端/创意工坊网页流程。详见 工坊打包发布。
11. 运行环境与红线
| 项 | 事实 |
|---|---|
| Python | 宿主 3.11–3.12 |
| 常见可用 | numpy、PySide6、sounddevice、requests |
| 勿假设可用 | websockets(非保证依赖) |
| 运行时 pip | 禁止 os.system("pip install ...") |
| 沙箱 | 无,与主程序同权限 |
| 隐私 | 不得私自上传麦克风/字幕数据 |
| 配置 | 只读写本插件配置,勿改主程序全局设置 |
需要重模型:独立进程 + 本地 HTTP,插件内只做请求。
12. 官方示例索引
| 目录 | type | 演示 |
|---|---|---|
example_glossary_gaming | glossary | terms.json |
example_theme_cyberpunk | theme | 全景 QML + theme.json |
community_theme_glassmorphism | theme | 多窗口毛玻璃 |
example_custom_waveform_ui | theme | audio_level |
example_cyberpunk_caption_ui | theme | 背景插槽 |
example_text_filter_mod | middleware | 文本过滤 |
example_custom_mt | mt | 自定义翻译 + config_schema |
example_i18n_japanese | i18n | 日语包 |
13. 给 AI 代理的实现检查清单
实现前逐项核对:
- [ ]
mod.json含非空唯一id - [ ]
type∈ - [ ]
entrypoint路径与类名在包内真实存在 - [ ] Python 类继承正确基类,实现全部
@abstractmethod - [ ] glossary 文本分隔符为
=或 Tab(不用->) - [ ]
window_backdrop只出现在theme.json - [ ] UI 插槽名使用下表已消费名称(§14)
- [ ] 不依赖未文档化的第三方包;不在运行时 pip
- [ ]
config_schema每项都有key与label - [ ] 本地放入
extensions/后可在工坊列表看到并启用
14. UI 插槽(摘要)
已消费、可替换:
overlay_background · caption_view · audio_level · alert_capsule · overlay_decorations · style_bar_actions · settings_background · history_background · dialog_background · window_background
声明了但当前无 Loader 消费(勿承诺可替换):overlay_root · style_bar
特殊键 settings_view(不在标准插槽列表):给插件设置页自定义 QML,注入 pluginId / bridge / rootWin / pluginConfig。
完整属性见 UI 插槽与 HostContext。