From 5f6ec303e7d6e4356a50beb3a405c8bd8e9b2cce Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 12:19:41 +0000 Subject: [PATCH] =?UTF-8?q?fix(pack-update):=20DRY/OCP=20=E6=94=B6?= =?UTF-8?q?=E6=95=9B=20provider=20=E8=A7=A3=E6=9E=90=E4=B8=8E=E6=B5=81?= =?UTF-8?q?=E5=BC=8F=E4=B8=8B=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 审查 main@c9a58ea(#63 合入后):入口四处写死 curseforge/needKey、 i18n 写死 cf:、CF 下载整包进内存、进度环路与 BDS 更新重复。 - resolveConfiguredPackProvider / providerShortLabel 注册表化 - bindOk/probeAutoBind 使用 {provider} 占位(LSP) - CF download 改走 bds-tools/httpDownload + 导出 ./http - SDK bindByteProgressToBar 供 BDS/CF 共用;无 Content-Length 也回调 - CLAUDE.md 去掉已删除的 panel/;补充 config 契约测试 Co-authored-by: Shiroha --- CLAUDE.md | 21 ++--- bds-tools/package.json | 1 + bds-tools/src/check-update.ts | 51 +++++------- bds-tools/src/http.ts | 3 +- modules/sdk/@sfmc-sdk/src/logs/index.ts | 2 + .../@sfmc-sdk/src/logs/terminal-progress.ts | 42 ++++++++++ sfmc/pack-update-config.test.mjs | 77 +++++++++++++++++++ sfmc/package.json | 2 +- sfmc/src/i18n/locales/en.ts | 4 +- sfmc/src/i18n/locales/zh-CN.ts | 4 +- sfmc/src/pack-update/providers/curseforge.ts | 48 +++--------- sfmc/src/pack-update/providers/index.ts | 32 ++++++++ sfmc/src/pack-update/service.ts | 48 +++++++----- 13 files changed, 231 insertions(+), 104 deletions(-) create mode 100644 sfmc/pack-update-config.test.mjs diff --git a/CLAUDE.md b/CLAUDE.md index 812237f2..4ad94ae7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,14 +4,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -ScriptsForMinecraftServer is a Minecraft Bedrock Script API (SAPI) plugin with five runtime components sharing one git repo: +ScriptsForMinecraftServer is a Minecraft Bedrock Script API (SAPI) plugin with runtime components sharing one git repo: | Path | Role | Runtime | |------|------|---------| | `db-server/` | SQLite HTTP REST API (port 3001) | Node.js 22.13+ | | `qq-bridge/` | QQ bridge via LLBot OneBot 11 (WS 3002 only) | Node.js | -| `panel/` | TUI management dashboard | Node.js (Ink) | -| `BDSTools/` | BDS auto-updater + behavior-pack assembler | Node.js | +| `bds-tools/` | BDS auto-updater + behavior-pack assembler | Node.js | | `sfmc/` | REPL / CLI supervisor / BP build pipeline | Node.js | | `modules/packages//` | Per-module packages; each one a first-class citizen | Node.js + SAPI | | `modules/sdk/@sfmc-sdk/` | Shared SDK (SAPI/Node umbrella) | consumed by modules | @@ -38,16 +37,18 @@ and bundles them in one go — there is no manual entry.ts to edit. ```bash cd db-server -node inedx.js # starts on 127.0.0.1:3001 -DB_PORT=4000 node inedx.js # override port +npm run start # node dist/index.js → 127.0.0.1:3001 +DB_PORT=4000 npm run start # override port ``` -### Management panel +### sfmc CLI(原 panel/ TUI 已移除) ```bash -node panel/index.js # TUI mode (requires interactive terminal) -node panel/index.js --cli # print status and exit (pipe-friendly) -node panel/index.js --no-tui # keep services running, no TUI +node index.js # REPL(交互) +node index.js status # 打印状态后退出 +node index.js start # 启动服务(db / qq / update / manager) +node index.js stop +node index.js init # 安装向导 ``` ### Dev tools (run from repo root) @@ -89,7 +90,7 @@ Truth source: `modules/catalog.json`(本地 mirror,由已装包投影) + `module 2. `catalog-sync` / install 会把 manifest 投影进 `modules/catalog.json` 3. Run `sfmc behavior-pack build && sfmc behavior-pack deploy`, restart BDS -Module enable/disable at runtime goes through Panel → `POST /api/sfmc/modules/:id/{enable|disable}` → db-server writes `module-lock.json` → SAPI calls `ConfigManager.refreshModules()`. **No hot-reload: restart BDS for changes to take effect.** +Module enable/disable at runtime goes through CLI → `POST /api/sfmc/modules/:id/{enable|disable}` → db-server writes `module-lock.json` → SAPI calls `ConfigManager.refreshModules()`. **No hot-reload: restart BDS for changes to take effect.** ### Configuration Model diff --git a/bds-tools/package.json b/bds-tools/package.json index 03b5fce6..1bff63b9 100644 --- a/bds-tools/package.json +++ b/bds-tools/package.json @@ -33,6 +33,7 @@ "./pack-manager": "./dist/cli-pack-manager.js", "./pack-manager-lib": "./dist/pack-manager.js", "./world-packs": "./dist/world-packs.js", + "./http": "./dist/http.js", "./taskbar": "./dist/taskbar.js", "./update-result": "./dist/update-result.js", "./zipx": "./dist/zipx.js", diff --git a/bds-tools/src/check-update.ts b/bds-tools/src/check-update.ts index dd9d387e..6bb9a54b 100644 --- a/bds-tools/src/check-update.ts +++ b/bds-tools/src/check-update.ts @@ -10,11 +10,11 @@ */ import { + bindByteProgressToBar, createFileSink, createLogger, createStdoutSink, createTerminalProgress, - formatDownloadSpeed, } from "@sfmc-bds/sdk/logs"; import fs from "node:fs"; import os from "node:os"; @@ -297,44 +297,30 @@ export async function runUpdate(): Promise { let lastErr: Error | null = null; const downloadTimeoutMs = (cfg.download_timeout ?? 120) * 1000; /* stderr 被 pipe 时不画 bar;用文本进度给 sfmc REPL 等父进程 */ - const progressBar = createTerminalProgress({ - stream: process.stderr, - logger: (msg) => log.info(msg.startsWith("进度") ? `下载${msg.slice(2)}` : msg), - format: "下载进度 | {bar} | {percentage}% | {value}/{total} MB | 速度: {speed}", - }); - - let lastTime = Date.now(); - let lastLoaded = 0; - let barStarted = false; if (isTaskbarSupported() && process.stdout.isTTY) { log.info("检测到 Windows Terminal,任务栏进度已启用 (OSC 9;4)"); } for (const url of downloadUrls) { + /* 每次尝试新建 bar + binder,避免失败重试时 started 状态残留(LSP) */ + const progressBar = createTerminalProgress({ + stream: process.stderr, + logger: (msg) => log.info(msg.startsWith("进度") ? `下载${msg.slice(2)}` : msg), + format: "下载进度 | {bar} | {percentage}% | {value}/{total} MB | 速度: {speed}", + }); + const onByteProgress = bindByteProgressToBar(progressBar, { + speedSampleMs: 0, + onProgress: (dl, total) => { + const pct = total > 0 ? (dl / total) * 100 : 0; + setTaskbarProgress(pct); + }, + }); try { + setTaskbarProgress(0); await httpDownload(url, zipPath, { totalTimeoutMs: Math.max(downloadTimeoutMs, 600_000), // 不少于 10 分钟 - onProgress: (dl, total) => { - const now = Date.now(); - const timeDelta = (now - lastTime) / 1000; // 秒 - const bytesDelta = dl - lastLoaded; - const speed = timeDelta > 0 ? bytesDelta / timeDelta : 0; - const speedStr = formatDownloadSpeed(speed); - const totalMb = total > 0 ? total / (1024 * 1024) : 0; - const dlMb = dl / (1024 * 1024); - const pct = total > 0 ? (dl / total) * 100 : 0; - - if (!barStarted) { - progressBar.start(totalMb || 1, 0, { speed: "0 KB/s" }); - barStarted = true; - setTaskbarProgress(0); - } - progressBar.update(dlMb, { speed: speedStr }); - setTaskbarProgress(pct); - lastLoaded = dl; - lastTime = now; - }, + onProgress: onByteProgress, }); - if (barStarted) progressBar.stop(); + if (progressBar.active) progressBar.stop(); // 下载完成,任务栏收到 100% 绿条后清掉 setTaskbarProgress(100); clearTaskbarProgress(); @@ -342,8 +328,7 @@ export async function runUpdate(): Promise { lastErr = null; break; } catch (e) { - if (barStarted) progressBar.stop(); - barStarted = false; + if (progressBar.active) progressBar.stop(); // 下载失败,任务栏亮红 setTaskbarProgress(100, "error"); clearTaskbarProgress(); diff --git a/bds-tools/src/http.ts b/bds-tools/src/http.ts index fc7e4ab5..13c6e7d2 100644 --- a/bds-tools/src/http.ts +++ b/bds-tools/src/http.ts @@ -198,10 +198,11 @@ export async function httpDownload( const stream: Readable = res; stream.on("data", (chunk: Buffer) => { downloaded += chunk.length; - if (opts.onProgress && total) { + if (opts.onProgress) { const now = Date.now(); if (now - lastProgressAt >= PROGRESS_INTERVAL_MS) { lastProgressAt = now; + /* total=0(无 Content-Length)时仍回调,调用方可用已下载字节更新 UI */ opts.onProgress(downloaded, total); } } diff --git a/modules/sdk/@sfmc-sdk/src/logs/index.ts b/modules/sdk/@sfmc-sdk/src/logs/index.ts index 17b5ca81..67c3495a 100644 --- a/modules/sdk/@sfmc-sdk/src/logs/index.ts +++ b/modules/sdk/@sfmc-sdk/src/logs/index.ts @@ -52,9 +52,11 @@ export { withProgressPaused, hasActiveProgress, formatDownloadSpeed, + bindByteProgressToBar, } from "./terminal-progress.js"; export type { TerminalProgressOptions, ProgressHandle, ProgressLogFn, + DownloadProgressBinderOptions, } from "./terminal-progress.js"; diff --git a/modules/sdk/@sfmc-sdk/src/logs/terminal-progress.ts b/modules/sdk/@sfmc-sdk/src/logs/terminal-progress.ts index c0d2ad93..5fcc656c 100644 --- a/modules/sdk/@sfmc-sdk/src/logs/terminal-progress.ts +++ b/modules/sdk/@sfmc-sdk/src/logs/terminal-progress.ts @@ -100,6 +100,48 @@ export function formatDownloadSpeed(bytesPerSec: number): string { return `${speed.toFixed(0)} B/s`; } +export interface DownloadProgressBinderOptions { + /** + * 速度采样间隔(毫秒)。 + * >0 时按间隔刷新 lastLoaded;0 表示每次回调都重采样(与旧 BDS 更新行为一致)。 + */ + speedSampleMs?: number; + /** 额外进度钩子(如任务栏 OSC),不替代 bar 更新 */ + onProgress?: ((downloaded: number, total: number) => void) | undefined; +} + +/** + * 把字节进度接到 TerminalProgress(MB 刻度 + 速度文案)。 + * BDS 更新与 CF 包下载共用,避免两处复制 lastTime/lastLoaded 环路(DRY)。 + */ +export function bindByteProgressToBar( + bar: ProgressHandle, + opts: DownloadProgressBinderOptions = {} +): (downloaded: number, total: number) => void { + const speedSampleMs = opts.speedSampleMs ?? 250; + let lastTime = Date.now(); + let lastLoaded = 0; + let started = false; + + return (downloaded: number, total: number) => { + const totalMb = total > 0 ? total / (1024 * 1024) : 1; + const dlMb = downloaded / (1024 * 1024); + if (!started) { + bar.start(totalMb, 0, { speed: "0 KB/s" }); + started = true; + } + const now = Date.now(); + const dt = (now - lastTime) / 1000; + const speed = dt > 0 ? (downloaded - lastLoaded) / dt : 0; + bar.update(dlMb, { speed: formatDownloadSpeed(speed) }); + if (speedSampleMs <= 0 || now - lastTime >= speedSampleMs) { + lastTime = now; + lastLoaded = downloaded; + } + opts.onProgress?.(downloaded, total); + }; +} + function renderBarLine( value: number, total: number, diff --git a/sfmc/pack-update-config.test.mjs b/sfmc/pack-update-config.test.mjs new file mode 100644 index 00000000..ef998aed --- /dev/null +++ b/sfmc/pack-update-config.test.mjs @@ -0,0 +1,77 @@ +/** + * pack-update 配置 / provider 解析契约测试 + * 从 dist 导入权威实现(DRY / LSP)。 + * 需先 `npm run build -w @sfmc-bds/cli`。 + */ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { after, before, describe, it } from "node:test"; +import { pathToFileURL } from "node:url"; + +const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "sfmc-pack-upd-cfg-")); +process.env.SFMC_ROOT = tmpRoot; +fs.mkdirSync(path.join(tmpRoot, "configs"), { recursive: true }); + +const { loadPackUpdateConfig, getPackMatchConfig } = await import( + pathToFileURL(path.resolve("dist/pack-update/config.js")).href +); +const { providerShortLabel, resolveConfiguredPackProvider, createPackSourceProvider } = await import( + pathToFileURL(path.resolve("dist/pack-update/providers/index.js")).href +); + +describe("pack-update config + provider resolve", () => { + before(() => { + fs.mkdirSync(path.join(tmpRoot, "configs"), { recursive: true }); + }); + + after(() => { + try { + fs.rmSync(tmpRoot, { recursive: true, force: true }); + } catch { + /* ignore */ + } + }); + + it("defaultBindingEnabled 默认 false,且出现在类型化配置上", () => { + const cfgPath = path.join(tmpRoot, "configs", "pack-update.json"); + if (fs.existsSync(cfgPath)) fs.unlinkSync(cfgPath); + const cfg = loadPackUpdateConfig(); + assert.equal(cfg.defaultBindingEnabled, false); + assert.equal(typeof cfg.match.nameMinScore, "number"); + }); + + it("旧版 providers.curseforge.match 提升到顶层 match", () => { + const cfgPath = path.join(tmpRoot, "configs", "pack-update.json"); + fs.writeFileSync( + cfgPath, + JSON.stringify({ + providers: { + curseforge: { + enabled: true, + apiKey: "", + match: { nameMinScore: 0.91, stripFolderTags: false }, + }, + }, + }), + "utf8" + ); + const cfg = loadPackUpdateConfig(); + assert.equal(cfg.match.nameMinScore, 0.91); + assert.equal(cfg.match.stripFolderTags, false); + assert.equal(getPackMatchConfig(cfg).nameMinScore, 0.91); + assert.equal("match" in cfg.providers.curseforge, false); + }); + + it("providerShortLabel / resolveConfiguredPackProvider 契约", () => { + assert.equal(providerShortLabel("curseforge"), "cf"); + const cfg = loadPackUpdateConfig(); + assert.equal(resolveConfiguredPackProvider(cfg), null); + cfg.providers.curseforge.apiKey = "test-key-not-real"; + const p = resolveConfiguredPackProvider(cfg); + assert.ok(p); + assert.equal(p.id, "curseforge"); + assert.equal(createPackSourceProvider(cfg, "curseforge").id, "curseforge"); + }); +}); diff --git a/sfmc/package.json b/sfmc/package.json index 349d6a00..22f1aa8b 100644 --- a/sfmc/package.json +++ b/sfmc/package.json @@ -45,7 +45,7 @@ "start": "node ./dist/main.js", "build": "node ../scripts/esbuild-transpile.mjs --dts", "typecheck": "tsc7 --noEmit -p tsconfig.json", - "test": "npm run build && node --test pack-update-policy.test.mjs terminal-progress.test.mjs", + "test": "npm run build && node --test pack-update-policy.test.mjs pack-update-config.test.mjs terminal-progress.test.mjs", "prepublishOnly": "npm run build" }, "dependencies": { diff --git a/sfmc/src/i18n/locales/en.ts b/sfmc/src/i18n/locales/en.ts index a69a23bd..8eb84814 100644 --- a/sfmc/src/i18n/locales/en.ts +++ b/sfmc/src/i18n/locales/en.ts @@ -383,10 +383,10 @@ export const en = { "packUpdate.probeHit": "Possible update source: {name} → {hit} ({score})\n {url}", "packUpdate.editHint": "You can later edit {path} to change/disable sources (enabled: false)", "packUpdate.confirmBind": "Bind this CurseForge project as update source? {hit}", - "packUpdate.probeAutoBind": "Non-interactive: auto-binding → cf:{slug}\n {path}", + "packUpdate.probeAutoBind": "Non-interactive: auto-binding → {provider}:{slug}\n {path}", "packUpdate.probeNoAutoBind": "Non-interactive: not auto-binding; use packs bind", "packUpdate.bindSkipped": "Bind skipped", - "packUpdate.bindOk": "Bound {uuid} → cf:{slug} [{enabled}]\n file: {path}", + "packUpdate.bindOk": "Bound {uuid} → {provider}:{slug} [{enabled}]\n file: {path}", "packUpdate.bindBpOnly": "Only behavior packs (BP) can be bound", "packUpdate.resolveFail": "Cannot resolve CurseForge project: {ref}", "packUpdate.searchEmpty": "No CurseForge results: {query}", diff --git a/sfmc/src/i18n/locales/zh-CN.ts b/sfmc/src/i18n/locales/zh-CN.ts index 4f7ad0f1..246739ef 100644 --- a/sfmc/src/i18n/locales/zh-CN.ts +++ b/sfmc/src/i18n/locales/zh-CN.ts @@ -356,10 +356,10 @@ export const zhCN: Record = { "packUpdate.probeHit": "找到可能的更新源: {name} → {hit} ({score})\n {url}", "packUpdate.editHint": "稍后可编辑 {path} 修改/关闭对应源(enabled: false)", "packUpdate.confirmBind": "将此 CurseForge 项目绑定为更新源? {hit}", - "packUpdate.probeAutoBind": "非交互环境:自动绑定 → cf:{slug}\n {path}", + "packUpdate.probeAutoBind": "非交互环境:自动绑定 → {provider}:{slug}\n {path}", "packUpdate.probeNoAutoBind": "非交互环境不自动绑定;可用 packs bind 手动绑定", "packUpdate.bindSkipped": "已跳过绑定", - "packUpdate.bindOk": "已绑定 {uuid} → cf:{slug} [{enabled}]\n 文件: {path}", + "packUpdate.bindOk": "已绑定 {uuid} → {provider}:{slug} [{enabled}]\n 文件: {path}", "packUpdate.bindBpOnly": "只能为行为包(BP)绑定更新源", "packUpdate.resolveFail": "无法解析 CurseForge 项目: {ref}", "packUpdate.searchEmpty": "CurseForge 无结果: {query}", diff --git a/sfmc/src/pack-update/providers/curseforge.ts b/sfmc/src/pack-update/providers/curseforge.ts index c1e644ff..873e1fcb 100644 --- a/sfmc/src/pack-update/providers/curseforge.ts +++ b/sfmc/src/pack-update/providers/curseforge.ts @@ -4,10 +4,10 @@ * - 官方 api.curseforge.com:getMod / files / download(需 Studios x-api-key) * - 搜索:优先官方;若 403 则回退 api.curse.tools(部分 key 对 /v1/mods/search 被拒) * - Bedrock gameId = 78022,Addons classId = 4984 + * - 下载走 bds-tools httpDownload(流式落盘,与 BDS 更新同一权威 — DRY/DIP) */ -import { createTerminalProgress, formatDownloadSpeed } from "@sfmc-bds/sdk/logs"; -import fs from "node:fs"; -import path from "node:path"; +import { httpDownload } from "@sfmc-bds/bds-tools/http"; +import { bindByteProgressToBar, createTerminalProgress } from "@sfmc-bds/sdk/logs"; import type { CurseForgeProviderConfig, PackReleaseType, @@ -266,43 +266,17 @@ export class CurseForgeBedrockProvider implements PackSourceProvider { stream: process.stderr, format: `下载 ${file.fileName} | {bar} | {percentage}% | {value}/{total} MB | {speed}`, }); - const res = await fetch(file.downloadUrl, { - headers: this.headers(true), - redirect: "follow", + const onByteProgress = bindByteProgressToBar(bar, { + speedSampleMs: 250, + ...(onProgress ? { onProgress } : {}), }); - if (!res.ok || !res.body) { - throw new Error(`下载失败 HTTP ${res.status}`); - } - const total = Number(res.headers.get("content-length") ?? 0); - const totalMb = total > 0 ? total / (1024 * 1024) : 1; - bar.start(totalMb, 0, { speed: "0 KB/s" }); - const reader = res.body.getReader(); - const chunks: Uint8Array[] = []; - let loaded = 0; - let lastTime = Date.now(); - let lastLoaded = 0; try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - chunks.push(value); - loaded += value.byteLength; - const now = Date.now(); - const dt = (now - lastTime) / 1000; - const speed = dt > 0 ? (loaded - lastLoaded) / dt : 0; - const speedStr = formatDownloadSpeed(speed); - bar.update(loaded / (1024 * 1024), { speed: speedStr }); - onProgress?.(loaded, total); - if (dt >= 0.25) { - lastTime = now; - lastLoaded = loaded; - } - } + await httpDownload(file.downloadUrl, destPath, { + headers: this.headers(true), + onProgress: onByteProgress, + }); } finally { - bar.stop(); + if (bar.active) bar.stop(); } - const buf = Buffer.concat(chunks.map((c) => Buffer.from(c))); - fs.mkdirSync(path.dirname(destPath), { recursive: true }); - fs.writeFileSync(destPath, buf); } } diff --git a/sfmc/src/pack-update/providers/index.ts b/sfmc/src/pack-update/providers/index.ts index 0f837ab9..7da29e6b 100644 --- a/sfmc/src/pack-update/providers/index.ts +++ b/sfmc/src/pack-update/providers/index.ts @@ -18,4 +18,36 @@ export function createPackSourceProvider( } } +/** 短标签(列表 / i18n 共用;新源只改此处 — OCP) */ +export function providerShortLabel(id: PackProviderId): string { + switch (id) { + case "curseforge": + return "cf"; + default: { + const _exhaustive: never = id; + return String(_exhaustive); + } + } +} + +/** + * 解析已配置完备的源。 + * - 指定 id:只检查该源 + * - 未指定:按 providers 键顺序取第一个 isConfigured() 的源(入口勿再写死 curseforge — OCP/LSP) + */ +export function resolveConfiguredPackProvider( + cfg: PackUpdateConfig, + id?: PackProviderId +): PackSourceProvider | null { + const candidates: PackProviderId[] = id + ? [id] + : (Object.keys(cfg.providers) as PackProviderId[]); + for (const pid of candidates) { + if (!(pid in cfg.providers)) continue; + const provider = createPackSourceProvider(cfg, pid); + if (provider.isConfigured()) return provider; + } + return null; +} + export { CurseForgeBedrockProvider }; diff --git a/sfmc/src/pack-update/service.ts b/sfmc/src/pack-update/service.ts index 72b280d9..8955a600 100644 --- a/sfmc/src/pack-update/service.ts +++ b/sfmc/src/pack-update/service.ts @@ -30,9 +30,12 @@ import { loadPackUpdateConfig, packUpdateConfigPath, } from "./config.js"; -import { createPackSourceProvider } from "./providers/index.js"; +import { + createPackSourceProvider, + providerShortLabel, + resolveConfiguredPackProvider, +} from "./providers/index.js"; import type { - PackProviderId, PackSourceBinding, PackSourceProvider, PackUpdateConfig, @@ -58,8 +61,9 @@ function bindingEnabledLabel(enabled: boolean): string { return enabled ? "on" : "off"; } -function providerShortLabel(id: PackProviderId): string { - return id === "curseforge" ? "cf" : id; +/** 未配置源时的统一文案(DRY:入口勿各自拼 needKey) */ +function needKeyText(): string { + return t("packUpdate.needKey", { path: packUpdateConfigPath() }); } /** 多查询搜索 + 综合打分排序(probe / searchRemote 共用) */ @@ -119,9 +123,9 @@ export async function probeSourceAfterInstall(opts: { if (opts.info.kind !== "behavior") return; ensurePackUpdateConfigFile(); - const provider = createPackSourceProvider(cfg, "curseforge"); - if (!provider.isConfigured()) { - logPack(t("packUpdate.needKey", { path: packUpdateConfigPath() }), "warn"); + const provider = resolveConfiguredPackProvider(cfg); + if (!provider) { + logPack(needKeyText(), "warn"); return; } @@ -203,7 +207,14 @@ export async function probeSourceAfterInstall(opts: { } else { /* 非 TTY / BDS 收件箱 / askConfirmOnBind=false:命中即写入,避免探测成功却无 pack-sources.json */ accept = true; - logPack(t("packUpdate.probeAutoBind", { slug: best.hit.slug, path: packSourcesPath() }), "info"); + logPack( + t("packUpdate.probeAutoBind", { + provider: providerShortLabel(best.hit.provider), + slug: best.hit.slug, + path: packSourcesPath(), + }), + "info" + ); } if (!accept) { @@ -217,6 +228,7 @@ export async function probeSourceAfterInstall(opts: { logPack( t("packUpdate.bindOk", { uuid: opts.info.uuid, + provider: providerShortLabel(best.hit.provider), slug: best.hit.slug, enabled: bindingEnabledLabel(binding.enabled), path: packSourcesPath(), @@ -227,9 +239,9 @@ export async function probeSourceAfterInstall(opts: { export async function searchRemote(query: string): Promise { const cfg = loadPackUpdateConfig(); - const provider = createPackSourceProvider(cfg, "curseforge"); - if (!provider.isConfigured()) { - return c.yellow(t("packUpdate.needKey", { path: packUpdateConfigPath() })); + const provider = resolveConfiguredPackProvider(cfg); + if (!provider) { + return c.yellow(needKeyText()); } const match = getPackMatchConfig(cfg); const queries = buildSearchQueries(query, match.stripFolderTags); @@ -245,9 +257,9 @@ export async function searchRemote(query: string): Promise { export async function bindPackSource(packId: string, ref: string): Promise { const cfg = loadPackUpdateConfig(); - const provider = createPackSourceProvider(cfg, "curseforge"); - if (!provider.isConfigured()) { - return c.yellow(t("packUpdate.needKey", { path: packUpdateConfigPath() })); + const provider = resolveConfiguredPackProvider(cfg); + if (!provider) { + return c.yellow(needKeyText()); } const { bdsRoot, levelName } = resolveBdsContext(); const packs = listInstalledWorldPacks(bdsRoot, levelName); @@ -267,6 +279,7 @@ export async function bindPackSource(packId: string, ref: string): Promise { const cfg = loadPackUpdateConfig(); if (!cfg.enabled) return c.dim(t("packUpdate.disabled")); - /* 入口预检:当前仅 curseforge;逐 binding 时仍按 binding.provider 分派(LSP) */ - const gate = createPackSourceProvider(cfg, "curseforge"); - if (!gate.isConfigured()) { - return c.yellow(t("packUpdate.needKey", { path: packUpdateConfigPath() })); + /* 入口预检:任一已配置源即可;逐 binding 仍按 binding.provider 分派(LSP/OCP) */ + if (!resolveConfiguredPackProvider(cfg)) { + return c.yellow(needKeyText()); } const { bdsRoot, levelName } = resolveBdsContext();