本文描述 sfmc packs / addon 的第三方世界 BP/RP 远程更新完整设计与实现细节(与 SFMC 业务模块 mod / GitHub registry 无关)。
相关代码:
| 路径 | 职责 |
|---|---|
sfmc/src/pack-update/ |
配置、绑定、CF Provider、探测/检查/应用 |
sfmc/src/world-packs.ts |
CLI 接线、安装后探测钩子 |
sfmc/src/services.ts |
BDS beforeStart 检查/应用 |
bds-tools/src/world-packs.ts |
安装/enable/抬版权威实现 |
modules/sdk/@sfmc-sdk/src/logs/terminal-progress.ts |
进度条与日志共存 |
configs/pack-update.json |
运行时配置(首次由 sfmc ensure 写入内置 DEFAULTS) |
通用收件箱安装见 资源包管理。
目标:为已装进世界目录的第三方 addon(如 OrdinaryWorld 的 Slash Blade BP)找到 CurseForge 更新源,并在启动或手动命令时检查/应用更新。
边界:
- 只管理世界侧
behavior_packs/resource_packs,不管理 SFMCmodules/packages业务模块。 - 多来源预留
SourceProvider抽象;当前唯一实现是 CurseForge Bedrock。 - 下载物仍走既有
installPackDirectory/enableInstalledPack(DRY,无第二套拷贝逻辑)。
验收样例:本地「Slash Blade v4」BP ↔ CF 项目 slug slash-blade-addon。
flowchart TD
install[packs_install] --> postHook[postInstall_sourceProbe]
postHook --> cfgKey{Studios_apiKey}
cfgKey -->|no| logNoKey[log_need_key]
cfgKey -->|yes| multiQ[buildSearchQueries]
multiQ --> cfSearch[CF_search_official_or_mirror]
cfSearch --> score[packSourceScore_name_plus_slug]
score -->|hit| askBind[TTY_confirm_bind]
score -->|miss| logMiss[log_no_source]
askBind -->|yes| bindStore[packs_pack-sources.json]
bdsStart[BDS_beforeStart] --> inbox[scan_inbox]
inbox --> eachBound[for_each_enabled_binding]
eachBound --> cmpBp[compare_remote_BP_vs_local_BP]
cmpBp -->|newer| dl[download_archive]
dl --> apply[force_install_BP_and_RP]
apply --> rpBump[maybe_bump_RP_version]
rpBump --> enable[refresh_world_enable_lists]
| 文件 | 作用 |
|---|---|
configs/pack-update.json |
策略、Provider、启动行为 |
packs/pack-sources.json |
每 BP uuid → CF 绑定(可手改/enabled: false) |
apiKey 也可用环境变量 CURSEFORGE_API_KEY 覆盖(避免把 key 写进可分享配置)。
不再从 configs-default 拷贝。createServices() / ensurePackUpdateConfigFile() 在文件缺失时写入代码内 DEFAULTS,并附带 $schema(见 @sfmc-bds/sdk/schemas/pack_update.schema.json)。
权威实现:sfmc/src/pack-update/config.ts 的 ensurePackUpdateConfigFile。
{
"enabled": true,
"checkOnBdsStart": true,
"applyOnBdsStart": true,
"askConfirmOnBind": true,
"probeSourceAfterInstall": true,
"defaultBindingEnabled": false,
"match": {
"nameMinScore": 0.6,
"stripFolderTags": true
},
"providers": {
"curseforge": {
"enabled": true,
"apiKey": "",
"baseUrl": "https://api.curseforge.com",
"searchBaseUrl": "https://api.curse.tools/v1/cf",
"gameId": 78022,
"classId": 4984,
"pageSize": 10,
"preferredReleaseTypes": ["release", "beta", "alpha"]
}
},
"versionPolicy": {
"authority": "behavior_pack",
"onUpdateOverwriteBoth": true,
"rpBumpWhenSameMajor": true,
"rpBumpComponent": "patch",
"majorHigherSkipRpBump": true
},
"startup": {
"sequential": true,
"delayMsBetweenPacks": 0,
"skipDisabledBindings": true,
"failMode": "continue"
},
"uninstall": {
"recycleBin": true,
"trashRelativeDir": "packs/_trash"
}
}| 字段 | 含义 |
|---|---|
defaultBindingEnabled |
新建绑定的默认 enabled(默认 false)。仍写入 pack-sources.json,但需手改为 true(或改此默认)才参与启动检查/自动更新。 |
uninstall.recycleBin |
packs uninstall 是否移入回收站(默认 true);false 或 CLI --purge 则直接删除 |
uninstall.trashRelativeDir |
回收站相对 SFMC_ROOT 的路径(默认 packs/_trash) |
match.nameMinScore |
安装后自动绑定的最低相似度阈值(源无关,顶层)。 |
match.stripFolderTags |
清洗时是否去掉方括号标签(如 [BP]/[玩法])。 |
gameId |
Minecraft Bedrock = 78022。历史误用 459 无效,加载时会纠正。Java Minecraft 是 432,不要混用。 |
classId |
Bedrock Addons = 4984;null 时用 /v1/categories?classesOnly=true 解析「Addons」。 |
baseUrl |
官方 Core API:getMod / files / download-url。 |
searchBaseUrl |
搜索镜像;官方 search 403 时回退。 |
{
"bindings": {
"3116e462-9838-48bf-be53-354958c1810f": {
"enabled": false,
"provider": "curseforge",
"projectId": 1055810,
"slug": "slash-blade-addon",
"websiteUrl": "https://www.curseforge.com/minecraft-bedrock/addons/slash-blade-addon",
"pairedResourceUuid": "6796f9a6-d1f4-4f90-8737-988dce8b4eaf",
"lastFileId": null,
"lastCheckedAt": null,
"lastAppliedFileId": null
}
}
}关闭某包自动更新:将该条 enabled 设为 false,或 packs unbind <id>。
packs list 文案:开启 src=cf:slug,关闭 src=cf:slug:off(与 sources 的开/关一致,不再丢成单独的 src=off)。
检查/应用:仅在成功写入世界目录后才更新 lastAppliedFileId。同一 fileId 已应用则跳过下载;版本未更高但尚未 apply 过该文件时仍会覆盖安装(同步 CF 内容)。
CurseForge 存在两套完全不同的「API Token」,不可混用。
| Upload / Legacy API Tokens | CurseForge for Studios(本功能需要) | |
|---|---|---|
| 文档 | support 文章 9000197321 | docs.curseforge.com |
| 申请 | 站点「API Tokens」 | console.curseforge.com |
| 形态 | UUID(约 36 字符) | 常见 $2a$10$…(更长) |
| 请求头 | X-Api-Token |
x-api-key |
| 用途 | 上传项目文件等 | api.curseforge.com 搜索/元数据/下载 |
| 对本功能 | 无效 → 403 | 正确 |
本仓库请求官方 API 时只发送 x-api-key。若 key 形如 UUID,403 错误信息会提示换 Studios Key。
JSON 中 $ 无需加倍;仅当把 key 放进 shell / docker-compose 环境变量 时,$ 可能被展开,需按运行环境转义(常见写法是 $$)。
实测(Studios Key):
| 端点 | 结果 |
|---|---|
GET /v1/games、/v1/games/78022 |
200 |
GET /v1/categories?gameId=78022 |
200 |
GET /v1/mods/{id}、/files |
200 |
GET /v1/mods/search?... |
部分 key 恒 403(Forbidden: API Key missing or invalid) |
GET https://api.curse.tools/v1/cf/mods/search?... |
200(社区镜像,路径约定与官方类似) |
实现策略(providers/curseforge.ts):
- 先打官方
baseUrl+/v1/mods/search。 - 若返回 403 → 静默改打
searchBaseUrl(默认https://api.curse.tools/v1/cf)的/mods/search。 - getMod / files / download-url / CDN 下载仍走官方 + Studios Key(下载也带
x-api-key,以应对 CDN 鉴权收紧)。
- 项目 slug 无空格:小写 + 连字符,例如
slash-blade-addon。 - 展示名可有空格/本地化:
Slash Blade Addon。 - 本地文件夹常带标签与版本:
[BA] [玩法] …Slash Blade v4 BP。
因此「只把带空格的清洗名丢进 searchFilter、只比 name」成功率偏低。
探测与 packs search 共用。安装后探测固定两源:manifest.header.name(header)与安装文件夹名(folder),分别派生后再轮询合并,避免某一源占满配额。
每个原始字符串先经 collectQuerySeeds 做多级清洗,再派生 slug。
只要合并结果里已有拉丁字母候选,就丢弃纯中文查询(CF Bedrock 几乎不靠中文命中)。
- 去
§、方括号标签[BP]/[BA]/[玩法]、扩展名 - CJK/拉丁粘连分界:
拔刀剑Slash→拔刀剑 Slash - 去整词包角色:
BP/RP/Addon/行为包… - 去版本尾巴:
v4、1.21.100 - 提取拉丁短语(CF 主路径):
Slash Blade v4 BP→ … →Slash Blade - 对每个种子再生成:
slash-blade、Slash-Blade、slash-blade-addon(大小写不敏感去重,故Slash-Blade与slash-blade只保留一条)
样例文件夹 [BP] [BA] [玩法] 拔刀剑Slash Blade v4 BP 的种子链约等于:
拔刀剑 Slash Blade v4 BP
→ Slash Blade v4 BP
→ Slash Blade v4
→ Slash Blade
→ Slash-Blade / slash-blade / slash-blade-addon
查询列表按「slug-addon > slug > Title-Slug > 拉丁短语 > 含 CJK」排序并截断(默认最多 14 条),避免 API 打爆同时保住核心 slug。
仅读 header 若全是中文/无英文,仍可靠文件夹名命中 CF。
对每个候选 hit,取 max(按 name 相似度, 按 slug 相似度):
| 条件 | 约分 |
|---|---|
| slug 完全相等 | 1.0 |
slug = {q}-addon 或以 {q}- 为前缀 |
0.96 |
| slug 互相包含 | 0.88 |
否则把 - 当空格做 token Jaccard / 包含 |
同 nameSimilarity |
| 纯展示名相等 / 包含 / token 交并比 | 1.0 / 0.85 / 交并 |
安装后自动绑定要求 score >= match.nameMinScore(默认 0.6)。
样例:本地 Slash Blade v4 ↔ CF slash-blade-addon → slug 前缀规则约 0.96,会排在 Terra Blade 等弱相关结果之前。
packs search <q>:多查询 + 按 score 降序;每行显示id/slug/score。packs bind <id> <projectId|slug|url>:手动绑定,不依赖探测分数。- 安装成功后的探测:分数达标后写入
packs/pack-sources.json(enabled取defaultBindingEnabled,默认关)。TTY 且askConfirmOnBind=true时先确认;非 TTY 自动写入绑定。开启自动更新请把对应条改为"enabled": true。
权威:本地 BP header.version vs 远程归档内 BP header.version。
忽略用户曾对手改 RP 小版本号的干扰。
| 情况 | 行为 |
|---|---|
| 远程 BP ≤ 本地 BP,且该 fileId 已成功 apply | 无更新 |
远程 BP ≤ 本地 BP,但 lastAppliedFileId 未记录该文件 |
仍覆盖安装(同步 CF 内容;避免汉化包等同版本号跳过写入) |
| 远程 BP 更大,且 major 相同 | force 覆盖 BP + 配对 RP;再按 nextEnabledVersion = bump(max(新 RP, 旧 RP)) 抬启用版本(默认 patch+1),并同步 world_resource_packs.json,以便玩家进服触发客户端 RP 刷新 |
| 远程 BP major 更高 | 直接覆盖双方,不再额外 bump RP(majorHigherSkipRpBump) |
| BP 自身 | 保持远程原版,不额外 bump |
lastAppliedFileId 仅在成功写入世界目录后更新;不可在「仅下载比较」阶段提前写入。
配对 RP:绑定里的 pairedResourceUuid,或新 BP dependencies[].uuid,或 mcaddon 内与之对齐的 resource 包。
抬版原语(DRY):bds-tools 的 nextEnabledVersion / ensureVersionGreaterThan(写回 manifest)+ writePackHeaderVersion。策略层 sfmc/pack-update/version-policy 再导出同一套纯函数,禁止 while 追赶。
| 命令 | 行为 |
|---|---|
packs check [id] |
下载最新归档,按 BP 版本比较,不安装 |
packs update <id|--all> |
比较并应用(含 RP 抬版策略) |
packs sources |
打印配置路径与全部绑定 |
packs path |
含 pack-sources.json 路径 |
scanAndInstallInbox(收件箱)runPackUpdatesOnBdsStart(若checkOnBdsStart;若applyOnBdsStart则一并应用)ensurePacksReady(SFMC 模块聚合 BP/RP)
逐个 binding 顺序处理(startup.sequential);失败默认 failMode: continue,不阻断开服。
- Provider
getLatestFile(projectId)→ 带进度下载到临时目录。 resolvePackRoots(解压顶层归档 + 展开嵌套.mcpack+ 发现manifest.json包根)。- 读远程 BP 版本 →
decideVersionPolicy。 installPackDirectory(..., force: true)装 BP / RP。- 按需
ensureVersionGreaterThan(内部nextEnabledVersion)抬 RP。 enableInstalledPack刷新世界 enable 列表。- 更新 binding 的
lastAppliedFileId/lastCheckedAt。
进度条:createTerminalProgress(stderr);日志 sink / REPL 写行前 pauseAllProgress、写完 resumeAllProgress(与 BDS 更新器共用,DRY)。
sfmc/src/pack-update/
types.ts # PackSourceProvider 契约、配置/绑定类型
config.ts # load / ensure pack-update.json
bindings.ts # packs/pack-sources.json
version-policy.ts # 版本比较、slug/name 查询与打分
providers/curseforge.ts
service.ts # probe / search / check / apply / BDS 钩子
新增来源(如 Modrinth)时:实现 PackSourceProvider,在配置 providers 注册,不必改核心 switch 链。
| 现象 | 排查 |
|---|---|
启动没有 pack-update.json |
确认跑的是会调用 createServices 的 sfmc;看 SFMC_ROOT 是否指向期望数据根 |
403 + UUID 形 key |
用了 Upload Token;改去 console.curseforge.com 申请 Studios Key |
| Studios Key 仍 search 403 | 正常现象之一;应已自动走 searchBaseUrl。确认配置里 gameId=78022 |
| 仅个别项目(如 MineCars)403 / 无法下载,其它正常 | 作者关闭了 Allow distribution(allowModDistribution=false)→ downloadUrl 为空且 /download-url 恒 403。与 API key 无关。手动网页下载放入 packs/inbox,或联系作者开启分发;也可 enabled: false 跳过该绑定 |
gameId=459 |
无效;改为 78022(新版本会纠正) |
| 搜不到 Slash Blade | 看 slug 是否 slash-blade-addon;用 packs search "slash blade" 看 score;或 packs bind <uuid> slash-blade-addon |
| 更新了但客户端 RP 不刷新 | 同 major 路径应抬 RP;确认 world_resource_packs.json 版本已变并重启 BDS / 重进服 |
| 进度条与日志抢行 | 应使用 SDK createTerminalProgress;勿直接 cli-progress 写 stdout |
# 配置 API Key 后
sfmc packs search "Slash Blade v4"
sfmc packs bind <uuid|folder> slash-blade-addon
sfmc packs sources
sfmc packs check
sfmc packs update --all
sfmc packs path别名:addon ≡ packs。