Skip to content

Folium 模组开发与贡献指南 ​

这份指南面向想为 Folia 写模组的开发者:从零写出第一个模组、在本地调试,到把它发布到 模组市场 并获得官方认证。

三份文档分工如下:

文档内容
本指南怎么做:开发环境、示例、编写要点、发布流程
Folium API 参考查什么:每个接口、成员和类型(由契约自动生成)
Folium 规范规则是什么:清单字段、权限、注册表语义、事件、安全模型、版本策略

1. 开始之前 ​

  • 先确认接口边界。 Folium 1.3 是首个稳定版本,目前仅开放 UI 相关的稳定扩展接口。其余部分,尤其是 omni.providers 等 Omni provider 接口,仍属实验接口,能力尚不能与应用内置 Omni provider 对等。在 Folium v2 发布前,请绝对不要尝试制作超出当前实验接口能力的完整音源模组;这类实现必然遇到接口缺口,浪费开发者和维护者的时间。
  • 原始音频导出模组不得上架。 任何尝试导出 provider 的原始音频流或解密之后的原始音频文件的模组,都有极高的平台法律风险。无论采用何种实现方式,此类模组都不会上架官方模组市场。
  • 模组是可信代码,不是沙箱。 client 入口运行在主界面的渲染进程里,main 入口运行在主进程里、拥有完整的 Node.js 权限。mod.json 里的权限是功能开关,不是安全边界。
  • 只在桌面版可用。 网页版和 PWA 没有模组系统。
  • 默认关闭。 需要在「设置 → 实验室 → 模组系统」里开启;开启后每个模组仍默认禁用,启用时会弹出主进程的原生确认窗口。
  • 确认绑定到文件内容。 模组目录里任何文件变化都会让确认失效,模组被自动禁用,需要重新确认。唯一的例外是开发版里仓库 mods/ 目录中的模组,见调试循环。
  • 签名只是标识。 发布到模组市场的模组带有 Folium 签名,安装后显示「官方认证」,但启用时同样要确认,权限也不会因此变少。

当前契约版本是 Folium 1.3,运行时通过 folium.host.folium.minor 判断宿主支持哪些功能(见兼容与版本)。

2. 准备开发环境 ​

用源码运行桌面版(推荐)

bash
npm install
npm run dev:electron

开发版会先扫描仓库根目录的 mods/,再扫描用户目录下的 mods/,所以把模组放进仓库的 mods/<你的模组>/ 就能直接加载。仓库里的示范模组也在这里,可以对照着看。

用已安装的桌面版

把模组放进用户模组目录:打开模组面板,点右上角的「打开模组目录」(Windows 上是 %APPDATA%\Folia\mods)。也可以把模组的 zip 包拖到模组面板上安装。手动解压 zip 时多出来的一层文件夹不影响加载,直接把解压结果放进模组目录即可。

打开模组面板

开启「设置 → 实验室 → 模组系统」后,在命令面板里执行「模组」命令。面板列出所有发现的模组、它们的状态、签名标识、权限、设置分区和命令,出错时也会在这里显示。

调试循环 ​

在开发版里(npm run dev:electron),模组放在仓库的 mods/ 目录中:

  1. 第一次启用时照常确认(确认窗口会注明「开发模式」)。
  2. 修改模组文件后,在模组面板里点「重载」即可,不需要重新确认。

这是只给开发者源码目录的豁免:加载器发现文件变了,会把确认直接更新到新内容。面板里这类模组会标注「开发目录中的模组」。

在安装版里,或模组放在用户模组目录时:

  1. 修改模组文件。
  2. 在模组面板里点「重载」。内容指纹变了,加载器会撤销之前的确认并把模组禁用(面板提示「文件在你上次确认之后发生了变化」)。
  3. 点模组行上的电源按钮重新启用,并确认。

client 入口按内容指纹带版本号导入,所以重载后加载的一定是最新代码。修改已签名的模组(比如仓库里的示范模组)后,它会显示「签名不匹配」,这是预期的。调试渲染端代码时可以直接打开开发者工具;模组调用 folium.log.error() 或抛出的错误会显示在模组面板里该模组的「界面代码出错」下。

3. 第一个模组:一个歌词动画 ​

新建目录 mods/hello-lyrics/,放三个文件。

mod.json:

json
{
  "folium": 1,
  "id": "hello-lyrics",
  "name": "你好歌词",
  "version": "1.0.0",
  "author": "你的名字",
  "description": "居中显示当前歌词,跟随主题与歌词字号。",
  "client": "client.mjs",
  "preview": "preview.png"
}

client.mjs:

js
// mods/hello-lyrics/client.mjs
// 一个最小的歌词动画:居中显示当前行,跟随主题、字体与用户的歌词字号。

export default function activate(folium) {
  folium.registries.visualizers.register({
    id: 'hello',
    label: { 'zh-CN': '你好歌词', en: 'Hello lyrics' },
    mount: (container, ctx) => mountHello(folium, container, ctx),
  });
}

// 挂载一次;换行、暂停、换主题、改显示设置都通过 ctx.subscribe 得知,不需要重新挂载。
function mountHello(folium, container, ctx) {
  const box = document.createElement('div');
  box.style.cssText = 'position:absolute;inset:0;display:grid;place-items:center;padding:0 8%;text-align:center;line-height:1.3;';
  container.appendChild(box);

  const render = () => {
    const theme = ctx.getTheme();
    const index = ctx.staticMode ? ctx.staticLineIndex : ctx.getLineIndex();
    const line = index !== null && index >= 0 ? ctx.lines[index] : null;
    box.textContent = line ? line.fullText : '';
    box.style.color = theme.primaryColor;
    box.style.fontFamily = folium.theme.resolveFontStack(theme);
    box.style.fontWeight = String(folium.theme.resolveFontWeight(theme, 600));
    box.style.fontSize = `${48 * ctx.getDisplay().lyricsFontScale}px`;
  };

  render();
  const unsubscribe = ctx.subscribe(render);
  return () => {
    unsubscribe();
    box.remove();
  };
}

preview.png:一张 1280×720 的截图,发布到模组市场时必需,本地开发可以先随便放一张。

然后:在模组面板点「重载」,启用「你好歌词」并确认,再到歌词样式设置的「歌词动画」里选择「你好歌词」。

这个例子用到的接口:FoliumVisualizerDef、FoliumStageContext、FoliumThemeHelpers、FoliumDisplay。完整的示范见主仓库的模组目录里的 mods/sample-aurora-visualizer(逐字扫光)和 mods/visualizer52hz(PixiJS、音频分析、设置、思索教程)。

4. 能做什么:速查 ​

想做的事用什么需要的权限参考
新的歌词动画模式registries.visualizers—FoliumVisualizerDef,sample-aurora-visualizer、visualizer52hz
新的背景类型registries.backgrounds—FoliumBackgroundDef
在播放页上叠加内容registries.stageLayersui.stageFoliumStageLayerDef,sample-rickroll
给内置模式加调参项registries.tunings—FoliumTuningDef,k3panel
模组自己的设置registries.settingsSections—FoliumSettingsSectionDef
命令(模组面板与命令面板)registries.commands—FoliumCommandDef
播放器面板里的标签页registries.playerPanelTabs—FoliumPlayerPanelTabDef
进度条旁的按钮、轨道上的标记registries.controlButtons / progressLayers—FoliumProgressContext,sample-progress-bar
改宿主外观registries.styles(只针对公开 part)—FoliumStyleDef,规范里的「styles 与公开 part」
改写歌词、拦截播放events.on('lyrics.transform' / 'playback.beforePlay')—FoliumHookEvents
读取或控制播放、点击歌词跳转folium.playback控制需 playback.controlFoliumPlaybackService
访问网络(不受 CORS 限制)folium.net.fetchnet.fetchFoliumNetService
嵌入外部网页folium.ui.embednet.embed + embedOriginsFoliumUiService
保存数据folium.storage(与 main 共用)filesystem.dataFoliumStorage
让用户选择本地文件folium.ui.pickFile / restoreFile—FoliumFileHandle
使用宿主图标folium.ui.icon—FoliumIconOptions
需要 Node 的工作(文件、进程、ffmpeg)main 入口 + folium.rpc视用到的 api 而定main 入口,sample-transparent-mov-export

5. 写好一个模组 ​

生命周期 ​

  • 所有界面类条目都是 mount(container, ctx) => dispose。只在宿主给的容器里操作,不要查询或修改容器以外的宿主 DOM。
  • dispose 里释放你创建的一切:动画帧、定时器、事件订阅、WebGL 上下文。activate 也可以返回一个清理函数。
  • 模组被禁用、重载或卸载时,宿主会先调用你的清理函数,再撤下你注册的所有条目和事件处理器。

时间与性能 ​

  • 歌词动画只在歌词、歌曲、staticMode 或静态预览行变化时重新挂载;换行不会。当前行从 ctx.getLineIndex() 读,连续时间用 ctx.currentTime.on('change') 或在自己的帧循环里 ctx.currentTime.get()。
  • 暂停时时钟不走,行号、主题、设置、显示设置的变化靠 ctx.subscribe 得知。
  • ctx.audio.getBands() 每次返回同一个对象、原地刷新,要保留读数就复制一份。
  • 各个 getter 在帧循环里调用没有问题;不要为了读取它们触发 React 或 DOM 布局。

预览、静态与导出 ​

  • ctx.staticMode 为真时只画 ctx.staticLineIndex 这一帧,不要启动动画。ctx.isPreview 表示在设置里的预览中。
  • folium.env.context === 'export' 时运行在透明视频导出窗口:只属于界面的注册表照常可以调用但不生效,服务调用会抛 *-unavailable-in-export-context(ui.icon 除外)。同一份代码两边都能跑,只要不在导出窗口里调用服务。
  • ctx.getSurface().transparent 为真时(OBS、透明导出)不要画不透明的底;hostBackground 为真时宿主在你下面画背景,画满整屏的模组应该让位。

歌词与主题 ​

歌词行和主题与内置歌词动画收到的结构同名同义,内置模式的写法可以直接照搬:

  • line.endTime 是歌词原始结束时间;这一行何时离开屏幕看 line.renderHints.renderEndTime,或用 folium.lyrics.getLineRenderEndTime(line)。
  • 分词用 folium.lyrics.segmentWords(line),它会优先使用用户保存的分词(line.wordSegments)。
  • 关键词配色用 folium.lyrics.buildWordColorRanges(line.fullText, theme.wordColors)。
  • 字体用 folium.theme.resolveFontStack(theme),字号乘上 ctx.getDisplay().lyricsFontScale。
  • 和声在 line.backgroundVocals,翻译与罗马音在 translation / romanization / alternateTexts。

设置 ​

  • 用 FoliumParam 声明 settings,宿主负责渲染表单、校验、持久化,并把值带进视觉配置的导入导出和导出窗口。
  • 想要自己的面板外观时用 settingsPanel(container, panelCtx),但仍然必须声明 settings:键、默认值和校验由 schema 决定,面板通过 panelCtx.params 读写。

第三方库 ​

client 只能用相对路径 import 模组目录里的 .mjs / .js 文件,不能 import 裸模块名,也拿不到宿主打包的库。要用第三方库,把它的 ESM 构建放进模组目录(例如 vendor/pixi.min.mjs)并附上许可证,见 visualizer52hz。

兼容与版本 ​

  • 用 folium.host.folium.minor 做功能探测,例如 if (folium.host.folium.minor >= 3) 再使用 1.3 新增的接口。
  • 实验接口(omni.providers、omni.hooks、ponder.targets)需要在清单的 experimental 里选用,任何 minor 版本都可能变化。
  • folium.internals 没有兼容承诺,使用时必须在清单里用 folia 固定宿主版本范围;社区模组应尽量不用。
  • 模组更新时提升 mod.json 的 version(MAJOR.MINOR.PATCH)。

6. 发布到模组市场 ​

官方模组和通过审查的社区模组收录在 folium-compound,发布在 模组市场。收录的模组都带有 Folium 签名,用户安装后显示「官方认证」。

不需要 fork 任何仓库:你只需要把模组放在自己的公开源码仓库里,再开一个 issue。

准备 ​

  • 公开的 https 仓库(GitHub、GitLab、Codeberg 等)。模组可以在仓库根目录,也可以在某个子目录。
  • mod.json 通过 Folia 的清单校验,id 与收录时的目录名相同、没有被占用。
  • 介绍图片:"preview" 指向模组目录里的 PNG / JPG / WebP。推荐 1280×720(接近 16:9,至少 640×360),不超过 1 MB。它显示在模组市场的卡片顶部,最好是模组运行时的真实截图。
  • 为要发布的版本打一个 tag。
  • 以 . 开头的文件和目录(.git、.github、编辑器配置)和 node_modules 不会被收录,其余文件原样收录;不要包含 folium.sig.json;不能有符号链接;最多 300 个文件、16 MB。
  • 包含第三方库时,把它们的许可证文件放在模组目录里。

提交 ​

  1. 在 folium-compound 开一个 「模组提交」issue,填写模组 id、源码仓库、版本(tag、分支或完整 commit)、模组目录、模组说明、逐条的权限说明和许可证。
  2. CI 会把版本解析成具体的 commit,只取回这一个提交,按收录规则整理并检查,然后在 issue 里回复:
    • 通过:「格式检查通过,请等待维护者审查」,附源码链接、将要签名的完整文件清单和签名摘要;
    • 未通过:列出问题。修改后打新 tag 并编辑 issue 里的版本,会自动重新检查。

审查与签名 ​

维护者审查的是那个 commit 的模组目录,至少会确认:

  • 通读 main 与 client 的全部代码,以及它们能加载的所有文件,包括 vendor/ 里的第三方库;
  • 声明的权限、embedOrigins、实验接口与 issue 里的说明、与实际用途相符,没有多余的权限;
  • 不上传用户数据,不下载并执行远程代码,不读写模组目录和数据目录以外的文件(除非这正是它声明的功能)。

审查通过后,维护者在 issue 里评论 /sign <commit>。CI 会再次取回同一个 commit、重跑检查,把文件复制进 folium-compound,签名,登记你为这个模组的维护者,然后发布。几分钟后模组就会出现在模组市场,issue 自动关闭。

签名绑定的是 commit,不是 tag:审查之后如果 tag 被移动或 issue 改成了别的版本,签名命令会被拒绝,需要按新的提交重新审查。

更新 ​

发布新版本时:

  1. 提升 mod.json 的 version,打新 tag。
  2. 开一个 「模组更新」issue,填模组 id、新版本和更新说明。源码仓库和目录沿用登记的那一个;只接受登记维护者提交的更新。
  3. CI 检查并附上与上一个签名版本的对比链接,维护者审查差异后签名,新版本替换旧版本。

需要更换源码仓库或目录、增减维护者时,在 issue 里说明,由维护者处理。

用户看到的签名状态 ​

状态含义
官方认证签名有效,文件自签名后没有改动
未验证没有签名文件:没有经过 Folium 审查的第三方模组
签名不匹配有签名但校验失败,最常见的原因是签名后文件被改动过(包括用户在本地修改了模组)

没有签名的模组照样可以安装和使用,只是显示「未验证」。签名规则和密钥管理见规范里的「签名与官方认证」。

7. 常见问题 ​

改了代码没有生效? 先在模组面板点「重载」并重新启用。还不行的话,确认用户模组目录里没有同 id 的旧副本:同一个 id 只认最先扫描到的那一份(开发版的扫描顺序是仓库 mods/、用户目录 mods/、安装目录 resources/mods)。

为什么改完代码要重新确认? 启用确认绑定到模组目录的内容摘要,任何文件变化都会让它失效,这是为了防止别人替换已启用模组的文件。开发时把模组放进仓库 mods/ 并用 npm run dev:electron 运行,就只需要第一次确认。

模组出错会影响应用吗? 不会。activate、每个事件处理器、每次挂载都在各自的错误边界里,出错只影响这个模组,错误显示在模组面板里。

网页版能用模组吗? 不能,模组系统只在桌面版里可用。

怎样让我的模组显示「官方认证」? 按第 6 节提交到 folium-compound,审查通过后由 CI 签名。本地自己签名不会被认可:只有 Folium 的密钥签出的签名会被宿主信任。

Released under AGPL-3.0