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
6 changes: 6 additions & 0 deletions .changeset/module-update-skip-retired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@sfmc-bds/cli": patch
"@sfmc-bds/desktop": patch
---

模块更新跳过已收编至平台的模块,例如 monitor。
2 changes: 1 addition & 1 deletion docs/zh/api/monitoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ SDK 的 `@sfmc-bds/sdk/sapi/runtime` 导出 `getRuntimeMetrics()` 和 `getRuntim

## monitor 弃用与兼容

`monitor` 已收编至平台。新行为包构建和 SDK 生命周期跳过旧模块;CLI 不再允许安装或启用,桌面与模块索引移除安装入口。无需手动删除旧包目录、`monitor.json`、启用记录或 `sfmc_monitor_*` 历史表。
`monitor` 已收编至平台。新行为包构建、SDK 生命周期和模块更新跳过旧模块;CLI 不再允许安装、启用或更新,桌面与模块索引移除安装入口。无需手动删除旧包目录、`monitor.json`、启用记录或 `sfmc_monitor_*` 历史表。

平台保留 `/c:status`(高级管理员权限)、`tps.current`、`tps.status` 和 `monitor.metrics`。跨模块服务仍要求有效调用方及 `services.requires` 声明。既有 `requires: ["monitor"]` 视为已由平台满足,不能让任意模块声明 `_platform` 身份或抢占平台服务。

Expand Down
23 changes: 21 additions & 2 deletions packages/cli/scripts/module-install/lib/module-update.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
* 安装目录若是 symlink / junction,只记 local-source 跳过,不换包。
* 下载与落盘由调用方注入的 install,本文件不复制第二套安装逻辑。
*/
import { isRetiredPlatformModule, RETIRED_PLATFORM_MODULES } from "@sfmc-bds/sdk/contracts";
import { randomBytes } from "node:crypto";
import fs from "node:fs";
import fsp from "node:fs/promises";
import path from "node:path";
import { randomBytes } from "node:crypto";
import { createRequire } from "node:module";
import path from "node:path";
import { upsertCatalogEntry } from "./catalog.mjs";
import { readJson } from "./io.mjs";
import { loadModuleUpdateConfig } from "./module-update-config.mjs";
Expand Down Expand Up @@ -54,6 +55,15 @@ import { compareSemver, isMajorBump, satisfiesSdk } from "./semver.mjs";
/** 这些跳过原因在开服日志里不展开,避免每轮刷屏。 */
export const QUIET_SKIP_REASONS = new Set(["auto-off", "local-source", "up-to-date"]);

/**
* 目录名或 manifest.id 是否已收编至平台。
* @param {string | null | undefined} id
*/
function isRetiredModuleId(id) {
const raw = String(id ?? "");
return isRetiredPlatformModule(raw) || isRetiredPlatformModule(normalizeModuleId(raw));
}

/**
* 读取本机 @sfmc-bds/sdk 版本,用来对照索引里的 sdk 范围。
* @param {string} [cliPkgDir]
Expand Down Expand Up @@ -368,6 +378,7 @@ export async function planModuleUpdates(options = {}) {
installedNorm.add(normalizeModuleId(folder));
if (pin.manifestId) installedNorm.add(normalizeModuleId(pin.manifestId));
}
for (const id of RETIRED_PLATFORM_MODULES) installedNorm.add(normalizeModuleId(id));

/** @type {ModuleUpgrade[]} */
const upgrades = [];
Expand All @@ -390,6 +401,10 @@ export async function planModuleUpdates(options = {}) {
if (wanted.size > 0 && !keys.some((key) => wanted.has(key))) continue;
const dir = packageDirOf(root, folder);
const installedVersion = readPackageVersion(dir) ?? pin.installedVersion;
if ([folder, manifestId].some((id) => isRetiredModuleId(id))) {
skipped.push({ id: folder, reason: "retired", fromVersion: installedVersion, toVersion: null, detail: "" });
continue;
}
const indexEntry = index ? index[folder] || index[normalizeModuleId(manifestId || "")] || null : null;
/* 目录本身是开发链接时先跳过,避免 pin 写成 npm 后仍去查 dist-tag 并换包。 */
const linkSkip = skipWhenDevLink(
Expand Down Expand Up @@ -538,6 +553,10 @@ export async function applyModuleUpgrades(upgrades, deps) {
/** @type {{ id: string, message: string }[]} */
const failed = [];
for (const item of upgrades) {
if (isRetiredModuleId(item.id)) {
skipped.push({ id: item.id, reason: "retired", fromVersion: item.fromVersion, toVersion: item.toVersion, detail: "" });
continue;
}
const linkSkip = skipWhenDevLink(item.id, packageDirOf(deps.root, item.id), item.fromVersion, item.toVersion);
if (linkSkip) {
skipped.push(linkSkip);
Expand Down
94 changes: 94 additions & 0 deletions packages/cli/scripts/module-install/test/module-update.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -529,3 +529,97 @@ test("新播种的更新配置在开服时不自动应用,已有文件保持
assert.equal(keptPlan.applyOnStart, true);
assert.equal(fs.readFileSync(keptFile, "utf8"), before);
});

test("弃用模块不进入升级,依赖它的模块仍可更新", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "sfmc-mod-retired-"));
const monitor = path.join(root, "modules", "packages", "monitor");
fs.mkdirSync(path.join(monitor, "sapi"), { recursive: true });
fs.writeFileSync(path.join(monitor, "package.json"), JSON.stringify({ name: "@sfmc-bds/module-monitor", version: "0.2.1" }));
fs.writeFileSync(path.join(monitor, "sapi", "manifest.json"), JSON.stringify({ id: "monitor", configKey: "monitor", requires: [] }));
const chat = path.join(root, "modules", "packages", "chat");
fs.mkdirSync(path.join(chat, "sapi"), { recursive: true });
fs.writeFileSync(path.join(chat, "package.json"), JSON.stringify({ name: "@sfmc-bds/module-chat", version: "1.0.0" }));
fs.writeFileSync(
path.join(chat, "sapi", "manifest.json"),
JSON.stringify({ id: "chat", configKey: "chat", requires: ["monitor"] })
);
const plan = await planModuleUpdates({
root,
hostSdk: "0.2.5",
ids: ["monitor"],
fetchIndex: async () => ({
index: {
monitor: { npm: "@sfmc-bds/module-monitor", version: "0.5.0", sdk: ">=0.2.0" },
chat: { npm: "@sfmc-bds/module-chat", version: "1.1.0", sdk: ">=0.2.2" },
},
offline: false,
}),
fetchDistTag: async () => {
throw new Error("retired module should not query dist-tag");
},
});
assert.equal(plan.upgrades.length, 0);
const skipped = plan.skipped.find((item) => item.id === "monitor");
assert.equal(skipped?.reason, "retired");
assert.equal(skipped?.fromVersion, "0.2.1");
assert.equal(skipped?.toVersion, null);

const withChat = await planModuleUpdates({
root,
hostSdk: "0.2.5",
fetchIndex: async () => ({
index: {
monitor: { npm: "@sfmc-bds/module-monitor", version: "0.5.0", sdk: ">=0.2.0" },
chat: { npm: "@sfmc-bds/module-chat", version: "1.1.0", sdk: ">=0.2.2" },
},
offline: false,
}),
fetchDistTag: async () => {
throw new Error("indexed modules should not query dist-tag");
},
});
assert.deepEqual(withChat.upgrades.map((item) => item.id), ["chat"]);
assert.equal(withChat.skipped.some((item) => item.reason === "missing-dependency"), false);
assert.equal(withChat.skipped.find((item) => item.id === "monitor")?.reason, "retired");

const dependent = fs.mkdtempSync(path.join(os.tmpdir(), "sfmc-mod-retired-dep-"));
const onlyChat = path.join(dependent, "modules", "packages", "chat");
fs.mkdirSync(path.join(onlyChat, "sapi"), { recursive: true });
fs.writeFileSync(path.join(onlyChat, "package.json"), JSON.stringify({ name: "@sfmc-bds/module-chat", version: "1.0.0" }));
fs.writeFileSync(
path.join(onlyChat, "sapi", "manifest.json"),
JSON.stringify({ id: "chat", configKey: "chat", requires: ["monitor"] })
);
const dependentPlan = await planModuleUpdates({
root: dependent,
hostSdk: "0.2.5",
fetchIndex: async () => ({
index: { chat: { npm: "@sfmc-bds/module-chat", version: "1.1.0", sdk: ">=0.2.2" } },
offline: false,
}),
fetchDistTag: async () => null,
});
assert.deepEqual(dependentPlan.upgrades.map((item) => item.id), ["chat"]);
assert.equal(dependentPlan.skipped.some((item) => item.reason === "missing-dependency"), false);
});

test("换包阶段仍拒绝弃用模块", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "sfmc-mod-retired-apply-"));
const pkg = path.join(root, "modules", "packages", "monitor");
fs.mkdirSync(pkg, { recursive: true });
fs.writeFileSync(path.join(pkg, "marker.txt"), "old");
const result = await applyModuleUpgrades(
[{ id: "monitor", fromVersion: "0.2.1", toVersion: "0.5.0", spec: "npm:@sfmc-bds/module-monitor@0.5.0", requires: [] }],
{
root,
failMode: "continue",
install: async () => {
throw new Error("retired module must not be installed");
},
}
);
assert.equal(result.applied.length, 0);
assert.equal(result.failed.length, 0);
assert.equal(result.skipped[0]?.reason, "retired");
assert.equal(fs.readFileSync(path.join(pkg, "marker.txt"), "utf8"), "old");
});
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ export const en = {
"mod.update.reason.missingModule": "not installed",
"mod.update.reason.missingDependency": "required module is not installed",
"mod.update.reason.badVersion": "version is not semver",
"mod.update.reason.retired": "included in the platform",
"mod.pin.usage": "Usage: mod pin <id> --auto on|off",
"mod.pin.ok": "{id} auto-update {state}",
"mod.pin.on": "on",
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/i18n/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ export const zhCN: Record<MessageKey, string> = {
"mod.update.reason.missingModule": "未安装",
"mod.update.reason.missingDependency": "依赖模块未安装",
"mod.update.reason.badVersion": "版本号不是 semver",
"mod.update.reason.retired": "已收编至平台",
"mod.pin.usage": "用法: mod pin <id> --auto on|off",
"mod.pin.ok": "{id} 自动更新{state}",
"mod.pin.on": "已打开",
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/module-update/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ const REASON_KEYS: Record<string, MessageKey> = {
"missing-module": "mod.update.reason.missingModule",
"missing-dependency": "mod.update.reason.missingDependency",
"bad-version": "mod.update.reason.badVersion",
retired: "mod.update.reason.retired",
};

/** 定位 fetch-module.mjs。找不到时返回 null,调用方给出安装提示。 */
Expand Down
2 changes: 1 addition & 1 deletion packages/desktop/src/renderer/dev/mock-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ export function installMockBridge() {
platform: id === "local-main"
? { currentVersion: "0.2.4", targetVersion: "0.2.4", releaseNotes: platformNotes, development: false, upgradeRequired: false, externalServices: ["llbot"], steps: [] }
: { currentVersion: "0.2.3", targetVersion: "0.2.4", releaseNotes: platformNotes, development: false, upgradeRequired: true, externalServices: [], steps: ["下载固定版本的平台依赖组合", "停止受影响服务并确认进程退出", "备份配置、模块、数据库与世界", "切换平台并验证", "恢复此前运行的服务"] },
modules: { failMode: "continue", applyOnStart: false, upgrades: [{ id: "sfmc-module-land", fromVersion: "1.4.2", toVersion: "1.5.0", spec: "npm:@sfmc-bds/module-land@1.5.0", requires: [] }], skipped: [{ id: "sfmc-module-activity-log", reason: "dev-link", fromVersion: "0.2.0-dev", toVersion: null, detail: "" }, { id: "sfmc-module-economy", reason: "up-to-date", fromVersion: "2.0.1", toVersion: "2.0.1", detail: "" }] },
modules: { failMode: "continue", applyOnStart: false, upgrades: [{ id: "sfmc-module-land", fromVersion: "1.4.2", toVersion: "1.5.0", spec: "npm:@sfmc-bds/module-land@1.5.0", requires: [] }], skipped: [{ id: "monitor", reason: "retired", fromVersion: "0.5.3", toVersion: null, detail: "" }, { id: "sfmc-module-activity-log", reason: "dev-link", fromVersion: "0.2.0-dev", toVersion: null, detail: "" }, { id: "sfmc-module-economy", reason: "up-to-date", fromVersion: "2.0.1", toVersion: "2.0.1", detail: "" }] },
bds: { checked: true, updated: false, currentVersion: "1.26.51.01", latestVersion: "1.26.60.02", result: "check-only" },
};
case "updates.run": return runTask(id, method, maintenance);
Expand Down
1 change: 1 addition & 0 deletions packages/desktop/src/renderer/pages/UpdatesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const SKIP_REASONS: Record<string, string> = {
"dependency-failed": "依赖更新失败",
"github-manual": "需手动更新",
"no-target": "无目标版本",
retired: "已收编至平台",
};

const failed = (value: unknown): value is Failed => Boolean(value && typeof value === "object" && "error" in value);
Expand Down
Loading