首页
/ Ghost 私有特性标志实战:在 labs.js 与 Admin UI 中注册并验证一个 Private Feature Flag

Ghost 私有特性标志实战:在 labs.js 与 Admin UI 中注册并验证一个 Private Feature Flag

2026-09-05 12:27:29作者:凌朦慧Richard

本文基于 Ghost 仓库的 Agent 技能文档 add-private-feature-flag,完整讲解如何在 Ghost 中新增一个私有(developer experiments)特性标志:从后端 PRIVATE_FEATURES 注册、Admin 设置页的 UI 开关,到单测与 API 快照更新的全流程。读完后你可以独立为 Ghost 新增一个 Labs 标志,并理解标志值的解析优先级、可写键白名单的强制校验,以及各测试体系中标志默认值的差异。

一、私有标志的定位:它出现在哪里、谁能看到

Ghost 使用特性标志(通常称为 Labs flags)来在功能尚未对所有人开放时先行合并代码、提供 beta 能力,或在不移除代码的前提下关闭某功能。私有标志(private flag)的展示规则由 Admin 的 Labs 设置页决定:

  • 私有标志显示在 Labs 设置的 Private features 标签页下;
  • 该标签页只有在 developer experiments(开发者实验)启用时才会渲染

这一规则可以在 Admin 源码中得到印证。Labs 设置页组件 中,Private featuresTabsTriggerTabsContent 都被 config.enableDeveloperExperiments 条件包裹(见 labs.tsx):

<TabsList>
  <TabsTrigger value="labs-beta-features">Beta features</TabsTrigger>
  {config.enableDeveloperExperiments && (
    <TabsTrigger value="labs-private-features">Private features</TabsTrigger>
  )}
</TabsList>

因此私有标志面向的是开发者实验场景,而非面向最终用户的功能开关。完整的决策依据(何时该用 Labs 标志、标志的三阶段模型、读取方式、提升与清理流程)以仓库中的正式指南 feature flag 指南 为准,技能文档在动手前也明确要求先阅读该指南。

二、新增私有标志的三步流程

技能文档 .agents/skills/add-private-feature-flag/SKILL.md 给出的标准步骤如下。

步骤 1:在 ghost/core/core/shared/labs.js 注册标志

打开 ghost/core/core/shared/labs.js,将标志名(camelCase 字符串)加入 PRIVATE_FEATURES 数组:

// These features are considered private they live in the private tab of the labs settings page
// Which is only visible if the developer experiments flag is enabled
const PRIVATE_FEATURES = [
  'automations',
  'automationRunAnalytics',
  'stripeAutomaticTax',
  // ... 现有标志
  // 'myNewFeature',  ← 在这里新增,注意 camelCase
];

labs.js 是该文件的唯一注册源,数组之外的关键导出还有两个,它们决定了后续所有校验与读取行为:

module.exports.GA_KEYS = [...GA_FEATURES];
module.exports.WRITABLE_KEYS_ALLOWLIST = [...PUBLIC_BETA_FEATURES, ...PRIVATE_FEATURES];
  • GA_FEATURES:始终返回 true 的标志,用于 GA 后的短暂过渡期(当前为 automationAnalyticstagDetailsReact,见 labs.js#L30);
  • PUBLIC_BETA_FEATURES:公开 beta 标志,用户可自由开关(当前含 superEditorseditorExcerptadditionalPaymentMethodsnavigationIcons,见 labs.js#L33-L38);
  • WRITABLE_KEYS_ALLOWLIST:两个可写列表的并集,是判断一个标志是否"已注册"的权威集合。

步骤 2:在 Admin 设置页添加 UI 开关

打开 apps/admin/src/settings/advanced/labs/private-features.tsx,向 features 数组追加一条 Feature 条目,flag 字段必须与步骤 1 中 labs.js 里的字符串完全一致

type Feature = {
  title: string;
  description: string;
  flag: string;
  limitName?: string;
};

const features: Feature[] = [
  // ...
  {
    title: 'My new feature',
    description: 'Enables ...',
    flag: 'myNewFeature',   // 必须与 labs.js 中 PRIVATE_FEATURES 里的字符串一致
  },
];

该文件组件 AlphaFeatures 会为每个条目渲染一个 LabItem + FeatureToggle,并用 limiter 过滤受订阅计划限制的特性(limitName 可选字段即用于此)。开关的实际写入逻辑在 feature-toggle.tsx 中:

  1. 从全局 settings 读出 labs 设置(一个 JSON 字符串),解析为布尔映射;
  2. 切换时调用 editSettings([{ key: 'labs', value: JSON.stringify({ ...labs, [flag]: newValue }) }]) 把新值合并回同一个 labs 设置;
  3. 成功后立即用 react-query 的 setQueriesData 同步更新本地 config 缓存中的 config.labs[flag],保证前端立即反映新状态。

步骤 3:运行测试并更新 config API 快照

技能文档给出的验证命令:

# 单元测试(labs 服务本身)
cd ghost/core && pnpm test:single test/unit/shared/labs.test.js

# 更新并审查两个 API 快照
cd ghost/core && pnpm test:single test/e2e-api/admin/config.test.js -u
cd ghost/core && pnpm test:single test/e2e-api/admin/settings.test.js -u

执行后需要人工审查两份快照 diff,确认只新增了你自己这个标志,没有夹带其他变化。test:single 脚本会按路径自动选择 vitest 配置(test/unit/* 用默认配置,test/e2e-api/* 用带数据库的 vitest.config.db.ts),见 ghost/core/package.json 中的 test:single 定义。

三、为什么快照一定会变:测试体系下的标志默认值

新增一个默认关闭的标志,为什么会导致 config / settings 快照变化?labs.js 文件头部注释和 labs.test.js 都解释了这一机制:

  • Ghost Core 的 integratione2ee2e-api 等数据库后端测试在 fixture 初始化时会执行 enableAllLabsFeatures,把 WRITABLE_KEYS_ALLOWLIST(即 PUBLIC_BETA_FEATURES + PRIVATE_FEATURES全部强制为 true,以便覆盖所有被标志门控的代码路径;
  • 因此只要你的标志进入了 PRIVATE_FEATURES,config API 的响应快照就会多出这个 key,即使它在生产环境默认是关闭的。

各测试体系的 Labs 默认值差异(来自 feature-flags 指南):

测试体系 setup 后的默认值
Ghost Core 单元测试 不强制开启任何标志,测试内自行 stub 需要的值
Ghost Core integration / legacytestUtils.setup() 所有已注册的 private 与 public beta 标志强制开启
Ghost Core e2e / e2e-api / e2e-isolatedfixtureManager.init() 同上,全部强制开启
React Admin 单测与验收测试(共享 test-data fixture) labsDefaults 中 key 默认关闭,需显式传 labs 覆盖
Ember Admin 测试(Mirage) labs 默认为空对象,用 enableLabsFlag / disableLabsFlag 调整
顶层 Playwright(e2e/ 使用新站点自身的值,只有 test.use({labs: ...}) 传入的标志被改变

这也意味着:GA_FEATURES 中的标志在所有运行时(含测试)默认开启,直到被移除或被配置覆盖。

四、源码纵深:标志值如何解析,白名单如何强制

解析优先级

labs.jsgetAll()labs.js#L69-L92)按以下顺序叠加各来源,后者优先:

stored Labs setting(数据库中的 labs 设置)
→ GA default(GA_FEATURES 强制 true)
→ remote override(远程覆盖清单,自托管默认不启用)
→ config.labs(本地配置文件,始终最高优先级)

源码中这段逻辑的注释写得很直白:config.labs > remote > GA > DB。另有两点细节值得注意:

  • labs.members 是特殊值,始终由 members_signup_access 设置重算(!== 'none' 即为 true),不受标志列表控制;
  • 远程覆盖清单是稀疏的:缺失的 key 不发表意见,布尔值对所有使用该清单的实例生效,{value, percent} 条目则按"标志名 + 站点 UUID"的稳定分桶做近似百分比灰度。

isSet(flag) 就是 !!(getAll()[flag] === true) 的严格判断(labs.js#L102-L106);labs.test.js 中的用例逐一验证了上述优先级,例如 config.labs 钉住 false 时,即使数据库设置与远程覆盖都是 trueisSet 仍返回 false

白名单强制:标志名必须在三处完全一致

新增标志必须"在 labs.jsprivate-features.tsx 与快照中同名"这一要求,背后是两层服务端强制校验,二者都引用 WRITABLE_KEYS_ALLOWLIST

  1. Settings 模型校验settings.jslabs 校验器会解析 JSON,任何一个不在白名单内的 key 都直接抛 ValidationError("Settings lab value cannot have value other then ...");
  2. API 输入序列化过滤settings 输入序列化器key === 'labs' 时按白名单过滤,未注册的 key 会被静默丢弃。

也就是说,如果只加了 UI 开关而忘了注册后端标志,开关操作会被服务端拒绝或丢弃;反过来只注册后端标志而不同步 UI,则该标志不会出现在 Labs 页面。标志字符串的一致性由校验器兜底,而不是靠开发者自觉——这也是快照必须同步更新的原因。

读取标志的配套 API

注册完成后,门控代码按运行环境选用对应 API(均出自 feature-flags 指南):

  • Ghost Core 服务端:labs.isSet('myFeature')
  • 需要整条 API 路由在关闭时返回 404:labs.enabledMiddleware('myFeature')(实现见 labs.js#L153-L160,未开启时抛 NotFoundError);
  • 主题模板辅助函数:从 @labs.myFeature 读取计算值;必须报告"功能被禁用"错误的 helper 用 labs.enabledHelper(...)
  • React Admin:useFeatureFlag(来自 @tryghost/admin-x-framework/hooks),它读取服务端在 Admin config 响应中计算好的值,响应缺失或加载中时返回 false
  • 旧版 Ember Admin:feature 服务,this.feature.get('myFeature')

指南特别强调:门控决策要放在"拥有该行为"的边界上——隐藏按钮保护不了服务端端点,拒绝端点也不等于给了 Admin 一个可用的禁用态。

五、备注:无需迁移、命名规范与 beta 变体

技能文档 Notes 部分的要点,结合源码补充说明如下:

  • 无需数据库迁移:所有 Labs 值(private + public beta)存储在同一个 JSON 格式的 labs 设置里,Admin 的 FeatureToggle 写入的也正是这个单一设置项;
  • 命名规范:标志是 camelCase 字符串(如 welcomeEmailDesignCustomizationmachinePayments),且必须在 labs.jsprivate-features.tsx 与快照三处完全一致;
  • 公开 beta 标志走另一条路:如果功能要面向所有用户可见(而非仅限开发者实验),应把 key 加入 labs.jsPUBLIC_BETA_FEATURES,并把开关加到 beta-features.tsx
  • 完整生命周期private 或 public beta → GA → 删除标志与旧分支。GA 阶段把 key 移入 GA_FEATURES(默认开启且不再可写)、移除 Admin 开关、更新快照;确认所有受支持的部署都能运行开启行为后,尽快删除标志、禁用分支以及只为该路径而存在的测试。

六、操作清单(速查)

序号 操作 文件 / 命令
1 将 camelCase 标志名加入 PRIVATE_FEATURES ghost/core/core/shared/labs.js
2 features 数组追加 {title, description, flag} 条目 apps/admin/src/settings/advanced/labs/private-features.tsx
3 运行 labs 单元测试 cd ghost/core && pnpm test:single test/unit/shared/labs.test.js
4 更新 config / settings API 快照并审查 diff cd ghost/core && pnpm test:single test/e2e-api/admin/config.test.js -u && pnpm test:single test/e2e-api/admin/settings.test.js -u
5 门控服务端与浏览器两侧行为,并为开/关两态各补测试 参照 feature-flags 指南
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384