Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bds-tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
"./bds-manager": "./dist/bds-manager.js",
"./pack-manager": "./dist/cli-pack-manager.js",
"./world-packs": "./dist/world-packs.js",
"./taskbar": "./dist/taskbar.js",
"./update-result": "./dist/update-result.js",
"./package.json": "./package.json",
"./types": {
"types": "./dist/types.d.ts"
Expand Down
12 changes: 7 additions & 5 deletions bds-tools/src/check-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { httpDownload } from "./http.js";
import { isMainModule } from "./is-main.js";
import { loadConfig, LOG_PATH, resolvePaths } from "./paths.js";
import { sendText, sendWithImage } from "./qqutil.js";
import { emitUpdateResult } from "./update-result.js";
import { extractZipFileToDir } from "./zipx.js";
import {
clearRollbackMarker,
getDirSize,
Expand Down Expand Up @@ -133,7 +135,7 @@ async function restorePreserves(bdsPath: string, backupDir: string, preserve: st
}
}

/** 把 srcDir 下的内容移动到 destDir (覆盖) — 经 zipx 安全解压 */
/** 把 zip 解压到 destDir (覆盖) — 经 zipx 安全解压 */
async function extractZipToBds(zipPath: string, destDir: string): Promise<void> {
// 抽出到临时目录,避免旧内容干扰
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bds-extract-"));
Expand Down Expand Up @@ -183,7 +185,7 @@ export async function runUpdate(): Promise<number> {
}
console.log(`CURRENT=${currentVer}`);
console.log(`LATEST=${latestVer}`);
console.log("SFMC_UPDATE_RESULT=uptodate");
emitUpdateResult("uptodate");
return 0;
}
} catch (e) {
Expand All @@ -198,7 +200,7 @@ export async function runUpdate(): Promise<number> {
if (qqNotify) {
await sendText(`⚠️ BDS ${latestVer} 不在兼容性白名单,已跳过升级。请人工确认。`);
}
console.log("SFMC_UPDATE_RESULT=skipped");
emitUpdateResult("skipped");
return 2;
}

Expand All @@ -220,7 +222,7 @@ export async function runUpdate(): Promise<number> {
console.log(`LATEST=${latestVer}`);
console.log(`CHANNEL=${channel}`);
console.log(`URLS=${downloadUrls.length}`);
console.log("SFMC_UPDATE_RESULT=check-only");
emitUpdateResult("check-only");
return 0;
}

Expand Down Expand Up @@ -474,7 +476,7 @@ export async function runUpdate(): Promise<number> {
}
log.info(`===== 更新完成 (${(durationMs / 1000).toFixed(1)}s) =====`);
/* 机器可读结果标记:供 sfmc 等监督器判断「真正完成部署」,勿依赖本地化日志文案。 */
console.log("SFMC_UPDATE_RESULT=deployed");
emitUpdateResult("deployed");
return 0;
}

Expand Down
5 changes: 5 additions & 0 deletions bds-tools/src/taskbar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ function canWriteOsc(): boolean {
return supported && !!process.stdout.isTTY;
}

/** 从日志文本剥离 WT 任务栏 OSC 9;4(与写入侧同一权威,供父进程 DRY)。 */
export function stripTaskbarOsc(s: string): string {
return s.replace(/\x1b\]9;4[^\x07\x1b]*(?:\x07|\x1b\\)/g, "");
}

/**
* 设置任务栏进度。
* - normal: 绿色 (pct 0-100)
Expand Down
20 changes: 20 additions & 0 deletions bds-tools/src/update-result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* update-result.ts — BDS updater ↔ sfmc 监督器机器可读结果契约(DRY / DIP)
*
* 勿依赖本地化日志文案判断「是否真正部署」;一律解析本标记。
*/
export const UPDATE_RESULT_PREFIX = "SFMC_UPDATE_RESULT=";

export type UpdateResultKind = "uptodate" | "skipped" | "check-only" | "deployed";

/** 向 stdout 打印机器标记(供父进程捕获)。 */
export function emitUpdateResult(kind: UpdateResultKind): void {
console.log(`${UPDATE_RESULT_PREFIX}${kind}`);
}

/** 从 updater 合并输出中解析是否完成部署。 */
export function didUpdateDeploy(out: string): boolean {
return new RegExp(
`(?:^|\\n)${UPDATE_RESULT_PREFIX}deployed(?:\\r?\\n|$)`
).test(out);
}
12 changes: 7 additions & 5 deletions sfmc/src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { t } from "./i18n/index.js";
import { spawnService } from "./runtime.js";
import { ROOT, SERVICE_NAMES, services, type ServiceName } from "./services.js";
import { c, DIVIDER, highlightLogLine } from "./theme.js";
import { stripTaskbarOsc } from "@sfmc-bds/bds-tools/taskbar";
import { didUpdateDeploy } from "@sfmc-bds/bds-tools/update-result";

function parseService(raw: string): ServiceName | null {
const s = raw.toLowerCase() as ServiceName;
Expand Down Expand Up @@ -161,9 +163,9 @@ export async function cmdStopAll(): Promise<string> {
return c.dim(t("svc.allStopped"));
}

/** 剥离 Windows Terminal 任务栏 OSC,避免 pipe 日志出现空白行 */
function stripTaskbarOsc(s: string): string {
return s.replace(/\x1b\]9;4[^\x07\x1b]*(?:\x07|\x1b\\)/g, "");
/** 剥离 Windows Terminal 任务栏 OSC,避免 pipe 日志出现空白行(委托 bds-tools/taskbar) */
function stripOsc(s: string): string {
return stripTaskbarOsc(s);
}

/**
Expand All @@ -188,7 +190,7 @@ export async function cmdUpdate(args: string[] = []): Promise<string> {
});
let out = "";
const pushChunk = (raw: string, level: "info" | "error"): void => {
const s = stripTaskbarOsc(raw);
const s = stripOsc(raw);
out += s;
for (const line of s
.split(/\r?\n/)
Expand All @@ -215,7 +217,7 @@ export async function cmdUpdate(args: string[] = []): Promise<string> {

/* 仅在真正完成部署后拉起 BDS;「已是最新」不打扰当前状态。
* 以 updater 输出的 SFMC_UPDATE_RESULT=deployed 机器标记为准(勿匹配本地化日志)。 */
const didDeploy = /(?:^|\n)SFMC_UPDATE_RESULT=deployed(?:\r?\n|$)/.test(result.out);
const didDeploy = didUpdateDeploy(result.out);
if (result.code === 0 && didDeploy && !userNoStart && !checkOnly) {
if (bds.running) {
return result.out;
Expand Down
8 changes: 8 additions & 0 deletions sfmc/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,10 @@ export const en = {
"mod.dbUnreachable":
"Cannot reach db-server at {url}: {message}. Is db-service running? (sfmc> start db)",
"mod.toggleFailed": "{action} {id} failed: {err}",
"mod.action.enabled": "enabled",
"mod.action.disabled": "disabled",
"mod.toggleOk": "{label} {id}",
"mod.toggleBadBody": "{action} {id} returned: {text}",
"mod.enable.usage": "Usage: sfmc module|mod enable <id>",
"mod.disable.usage": "Usage: sfmc module|mod disable <id>",
"mod.install.usage": "Usage: sfmc module|mod install <id> [id2 ...] [--from <source>] [--link]",
Expand Down Expand Up @@ -337,6 +341,10 @@ export const en = {
"packs.installedEnabled": "[packs] Installed & enabled {kind}: {name} v{version}",
"packs.installFailed": "[packs] Install failed {name}: {reason}",
"packs.listEmpty": "(none)",
"packs.list.bpHeader": "Behavior packs:",
"packs.list.rpHeader": "Resource packs:",
"packs.list.on": "on ",
"packs.list.off": "off",
"packs.doctor.parseFail": "{file} could not be parsed",
"packs.doctor.missingDir": "List has uuid but directory missing ({kind}): {uuid}",
"packs.doctor.versionMismatch":
Expand Down
12 changes: 10 additions & 2 deletions sfmc/src/i18n/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,10 @@ export const zhCN: Record<MessageKey, string> = {
"mod.dbUnreachable":
"无法连接 db-server {url}: {message}。db 服务是否在运行?(sfmc> start db)",
"mod.toggleFailed": "{action} {id} 失败: {err}",
"mod.action.enabled": "已启用",
"mod.action.disabled": "已禁用",
"mod.toggleOk": "{label} {id}",
"mod.toggleBadBody": "{action} {id} 返回: {text}",
"mod.enable.usage": "用法: sfmc module|mod enable <id>",
"mod.disable.usage": "用法: sfmc module|mod disable <id>",
"mod.install.usage": "用法: sfmc module|mod install <id> [id2 ...] [--from <source>] [--link]",
Expand Down Expand Up @@ -321,6 +325,10 @@ export const zhCN: Record<MessageKey, string> = {
"packs.installedEnabled": "[packs] 已安装并启用 {kind}: {name} v{version}",
"packs.installFailed": "[packs] 安装失败 {name}: {reason}",
"packs.listEmpty": "(无)",
"packs.list.bpHeader": "行为包:",
"packs.list.rpHeader": "资源包:",
"packs.list.on": "开 ",
"packs.list.off": "关",
"packs.doctor.parseFail": "{file} 无法解析",
"packs.doctor.missingDir": "清单有 uuid 但目录缺失 ({kind}): {uuid}",
"packs.doctor.versionMismatch":
Expand All @@ -342,8 +350,8 @@ export const zhCN: Record<MessageKey, string> = {
"packs.search.usage": "用法: packs search <query>",
"packs.toggle.usage": "用法: packs {verb} <uuid|folder>",
"packs.install.usage": "用法: packs install [path|--inbox] [--force]",
"packs.installInbox": "install inbox: installed={installed} skipped={skipped} failed={failed}",
"packs.scanResult": "scan: installed={installed} skipped={skipped} failed={failed}",
"packs.installInbox": "收件箱安装: 成功={installed} 跳过={skipped} 失败={failed}",
"packs.scanResult": "扫描: 成功={installed} 跳过={skipped} 失败={failed}",
"packs.unknownSub": "未知 packs 子命令: {verb}\n",
"packs.usage.title": "{cmd}(别名 {alias})— 世界侧任意 BP/RP",
"packs.usage.bump": "仅 RP,patch 版本 +1",
Expand Down
8 changes: 5 additions & 3 deletions sfmc/src/module-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,9 +371,11 @@ async function postModuleToggle(id: string, action: "enable" | "disable"): Promi
return failResult(c.red(t("mod.toggleFailed", { action, id, err })));
}
const ok = (body as { success?: boolean })?.success !== false;
return ok
? okResult(c.green(`${action}d ${id}`))
: failResult(c.red(`${action} ${id} returned: ${text}`));
if (ok) {
const label = action === "enable" ? t("mod.action.enabled") : t("mod.action.disabled");
return okResult(c.green(t("mod.toggleOk", { label, id })));
}
return failResult(c.red(t("mod.toggleBadBody", { action, id, text })));
}

export async function cmdModuleEnable(args: string[]): Promise<CliResult> {
Expand Down
8 changes: 4 additions & 4 deletions sfmc/src/world-packs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,16 +409,16 @@ function formatPackList(packs: InstalledWorldPack[]): string {
const bp = packs.filter((p) => p.kind === "behavior");
const rp = packs.filter((p) => p.kind === "resource");
if (bp.length) {
lines.push(c.bold("Behavior packs:"));
lines.push(c.bold(t("packs.list.bpHeader")));
for (const p of bp) {
const en = p.enabled ? c.green("on ") : c.dim("off");
const en = p.enabled ? c.green(t("packs.list.on")) : c.dim(t("packs.list.off"));
lines.push(` [${en}] ${p.folderName} ${p.name} v${fmtVer(p.version)} ${c.dim(p.uuid)}`);
}
}
if (rp.length) {
lines.push(c.bold("Resource packs:"));
lines.push(c.bold(t("packs.list.rpHeader")));
for (const p of rp) {
const en = p.enabled ? c.green("on ") : c.dim("off");
const en = p.enabled ? c.green(t("packs.list.on")) : c.dim(t("packs.list.off"));
lines.push(` [${en}] ${p.folderName} ${p.name} v${fmtVer(p.version)} ${c.dim(p.uuid)}`);
}
}
Expand Down
Loading