Windows Terminal(OpenConsole)特征开关机制:til::feature 的 XML 声明、优先级裁决与源码实现
Windows Terminal 仓库(OpenConsole)使用一套名为 til::feature 的特征开关(feature flag)体系,来决定不同代码分支、不同产品品牌(Branding)下哪些功能应被编译进去或运行时启用。其特征声明集中在 src/features.xml,由构建系统自动生成预处理器宏与 C++ 访问接口。读完本文,你将理解该文档定义的完整 XML 结构、六级优先级裁决规则,以及从 XML 到生成头文件、再到源码消费的全链路实现细节。
一、机制总览:XML 声明,构建期裁决
按照 doc/feature_flags.md 的定义,特征开关由存放在 src/features.xml 的 XML 文档控制。这条链路的核心环节是:
- 声明:开发者在 src/features.xml 中用
<feature>元素描述一个开关的名称、默认状态、分支/品牌例外; - 生成:build/rules/GenerateFeatureFlags.proj 在 MSBuild 构建时调用 tools/Generate-FeatureStagingHeader.ps1,结合当前 Git 分支与产品品牌(Branding)裁决出每个开关的最终状态;
- 消费:生成的头文件
TilFeatureStaging.h被强制包含进所有 C++ 编译单元,源码中既可用#if TIL_FEATURE_XXX_ENABLED做编译期裁剪,也可用Feature_XXX::IsEnabled()做运行时判断。
这套机制解决的实际问题在仓库中随处可见:例如 conhost 主程序 src/host/exe/exemain.cpp 用 #if TIL_FEATURE_LEGACYCONHOST_ENABLED 决定是否加载旧版 conhostv1.dll,而 Windows Terminal 应用 src/cascadia/TerminalApp/AppActionHandlers.cpp 在处理“打开草稿板”操作时用 Feature_ScratchpadPane::IsEnabled() 判断是否允许创建草稿板面板。
二、特征声明 XML 的完整结构
官方文档给出了标准的示例文档,一个 <featureStaging> 根节点下包含若干 <feature>:
<?xml version="1.0" encoding="utf-8"?>
<featureStaging xmlns="http://microsoft.com/TilFeatureStaging-Schema.xsd">
<feature>
<!-- This will produce Feature_XYZ::IsEnabled() and TIL_FEATURE_XYZ_ENABLED (preprocessor) -->
<name>Feature_XYZ</name>
<description>Does a cool thing</description>
<!-- GitHub deliverable number; optional -->
<id>1234</id>
<!-- Whether the feature defaults to enabled or disabled -->
<stage>AlwaysEnabled|AlwaysDisabled</stage>
<!-- Branch wildcards where the feature should be *DISABLED* -->
<alwaysDisabledBranchTokens>
<branchToken>branch/with/wildcard/*</branchToken>
<!-- ... more branchTokens ... -->
</alwaysDisabledBranchTokens>
<!-- Just like alwaysDisabledBranchTokens, but for *ENABLING* the feature. -->
<alwaysEnabledBranchTokens>
<brandingToken>...</brandingToken>
</alwaysEnabledBranchTokens>
<!-- Brandings where the feature should be *DISABLED* -->
<alwaysDisabledBrandingTokens>
<!-- Valid brandings include Dev, Preview, Release, WindowsInbox -->
<brandingToken>Release</brandingToken>
<!-- ... more brandingTokens ... -->
</alwaysDisabledBrandingTokens>
<!-- Just like alwaysDisabledBrandingTokens, but for *ENABLING* the feature -->
<alwaysEnabledBrandingTokens>
<brandingToken>...</brandingToken>
</alwaysEnabledBrandingTokens>
<!-- Unequivocally disable this feature in Release -->
<alwaysDisabledReleaseTokens />
</feature>
</featureStaging>
各字段的语义如下(结合 tools/FeatureStagingSchema.xsd 的约束):
| 字段 | 必填 | 说明 | XSD 约束 |
|---|---|---|---|
name |
是 | 开关名称,同时决定生成的预处理器宏与 C++ 结构体名 | 必须匹配 ^Feature_[a-zA-Z0-9_]+ |
description |
是 | 开关用途的文本描述 | 非空字符串 |
id |
否 | 对应的 GitHub 交付项(deliverable)编号 | 正整数 |
stage |
是 | 默认状态 | 仅允许 AlwaysEnabled 或 AlwaysDisabled |
alwaysDisabledBranchTokens |
否 | 在这些分支(支持通配符)上强制禁用 | branchToken 列表,同一 feature 内不可重复 |
alwaysEnabledBranchTokens |
否 | 在这些分支上强制启用 | 同上 |
alwaysDisabledBrandingTokens |
否 | 在这些品牌下强制禁用 | brandingToken 列表,同一 feature 内不可重复 |
alwaysEnabledBrandingTokens |
否 | 在这些品牌下强制启用 | 同上 |
alwaysDisabledReleaseTokens |
否 | 空元素即表示“在 Release 中无条件禁用” | 可为空 |
需要指出两处文档与实现的细微差异:
- 文档注释列出的有效品牌为
Dev、Preview、Release、WindowsInbox,但 XSD 的brandingType实际允许五种取值:Dev|Canary|Preview|Release|WindowsInbox。src/features.xml 中Feature_ScratchpadPane与Feature_MarkdownPane就启用了Canary品牌; - 文档示例中
alwaysEnabledBranchTokens/alwaysEnabledBrandingTokens内部误写成了<branchToken>标签,按 XSD 定义,品牌列表内的子元素应为<brandingToken>。
XSD 还通过 xs:key(featureBranchOverridesKey、featureBrandingOverridesKey)约束同一个 feature 内的分支 token 与品牌 token 不得重复,防止声明歧义。
三、优先级规则:六级裁决顺序
原文档的 Notes 部分强调了一条关键规则:凡是使用 alwaysDisabledReleaseTokens 标记为 Release 禁用的特征,在 Release 构建中始终禁用,即使它来自一个本应被通配符启用的分支(“WindowsInbox 视为 Release 的一种”这一事实可由生成脚本印证,见下文)。
文档给出的完整优先级从高到低为:
alwaysDisabledReleaseTokens(Release 无条件禁用)- 启用型分支 token
- 禁用型分支 token —— 若多个分支 token 同时命中,最长匹配的 token 胜出(最具体者胜)
- 启用型品牌 token
- 禁用型品牌 token
- 特征的默认状态(
stage)
这套顺序在 tools/Generate-FeatureStagingHeader.ps1 的 Resolve-FinalFeatureStage 函数中得到了逐字实现:
- 第一步检查
$Branding -In @("Release", "WindowsInbox")且DisabledReleaseToken为真,直接返回AlwaysDisabled; - 第二步遍历所有分支 token,用
$Branch -Like $branchToken做通配匹配,并只保留长度最长的匹配项; - 第三步查询品牌 token 字典;
- 全部未命中才落到
stage默认值。
另外两条实现层面的细节值得注意:
- 在
Feature类的构造函数中(tools/Generate-FeatureStagingHeader.ps1),启用的分支/品牌 token 会后写入并覆盖禁用的同名 token(注释明确写着 “AlwaysEnabled branches win over AlwaysDisabled branches”),这与“启用型优先级高于禁用型”的规则一致; DisabledReleaseToken的判定是$Null -Ne $entry.alwaysDisabledReleaseTokens,即只要 XML 中出现了空的<alwaysDisabledReleaseTokens/>元素就算命中。
四、生成脚本:从 XML 到头文件
tools/Generate-FeatureStagingHeader.ps1 的调用参数为:
| 参数 | 说明 |
|---|---|
-Path(位置参数 1,必填) |
特征 XML 文件路径(即 src/features.xml) |
-Branding |
取值 Dev、Canary、Preview、Release、WindowsInbox,默认 Dev |
-BranchOverride |
手动覆盖分支名;缺省时依次尝试 git branch --show-current 与 git rev-parse --abbrev-ref HEAD,再失败则跳过分支校验 |
-OutputPath |
指定后写入文件,否则将生成内容直接输出到 stdout |
脚本先加载 XSD 并对 XML 做 Validate(不合法文档直接报错终止),随后把所有 feature 按名称排序,逐一裁决最终状态,最后生成两类产物:
// 编译期:每个开关一个宏
#define TIL_FEATURE_XYZ_ENABLED 1
// 运行时(C++):每个开关一个结构体
struct Feature_XYZ
{
static constexpr bool IsEnabled() { return TIL_FEATURE_XYZ_ENABLED == 1; }
};
生成的头文件头部带有 “THIS FILE IS AUTOMATICALLY GENERATED; DO NOT EDIT IT” 声明,并且每个结构体上都挂了 __pragma(detect_mismatch("ODR_violation_..._mismatch", ...)),用于检测同一翻译单元集合内因头文件版本不一致导致的 ODR(单一定义规则)违规——这一点在脚本的 CODE GENERATION 段(tools/Generate-FeatureStagingHeader.ps1)中可见。
宏名规则是 TIL_ + 全大写特征名 + _ENABLED(PreprocessorName 方法,tools/Generate-FeatureStagingHeader.ps1),因此 Feature_XYZ 生成 TIL_FEATURE_XYZ_ENABLED。
五、构建系统集成:谁在何时触发生成
5.1 Windows Terminal 侧(MSBuild)
build/rules/GenerateFeatureFlags.proj 是一个独立于解决方案的工程,完成三件事:
- 品牌推导:由 MSBuild 属性
WindowsTerminalBranding映射出_WTBrandingName(Canary / Preview / Release),未设置时回落到Dev(build/rules/GenerateFeatureFlags.proj); - 分支缓存:
_GenerateBranchAndBrandingCache目标先用git.exe rev-parse --abbrev-ref HEAD取当前分支,与品牌名一起写入branch_branding_cache.txt,作为 MSBuild 增量构建的输入依赖之一; - 生成头文件:
_RunFeatureFlagScript目标以features.xml与缓存文件为输入,运行 PowerShell 脚本,把 stdout 收集到 MSBuild 项后,用WriteOnlyWhenDifferent="true"写出到bin\$(Configuration)\inc\TilFeatureStaging.h。
这个设计刻意避免了“分支一变就全量重编”:正如工程内注释所述,只有当新生成头文件的内容确实变化时才会触发下游重编译。
头文件如何进入每个编译单元?由 src/common.build.post.props 的 ForcedIncludeFiles 全局强制包含 $(SolutionDir)\bin\$(Configuration)\inc\TilFeatureStaging.h,因此源码中无需显式 #include,#if TIL_FEATURE_... 与 Feature_...::IsEnabled() 随处可用。
5.2 conhost 侧(WindowsInbox 品牌)
在面向 Windows 自带控制台(conhost/ConPTY)的构建中,src/staging/makefile.inc 以 -Branding WindowsInbox -Branch $(BUILDBRANCH) 调用同一脚本,输出 TilFeatureStaging.h 到目标目录;src/staging/sources 声明该头文件是独立构建产物(TARGETTYPE=NOTARGET),src/project.inc 则通过 /FI(forced include)把它注入 conhost 各编译单元。这解释了为什么 features.xml 里有大量 WindowsInbox 品牌例外——同一份源码在“商店版 Windows Terminal”与“系统内置 conhost”两种身份下需要呈现不同的功能集合。
六、仓库当前的特征开关清单
以 src/features.xml 为准,仓库当前声明了如下开关(alwaysDisabledReleaseTokens 列以 ✓ 表示存在空元素):
| 特征名 | 默认 stage | 品牌例外 | 说明 |
|---|---|---|---|
Feature_ReceiveIncomingHandoff |
启用 | WindowsInbox 禁用 | conhost 可接收来自 OpenConsole 的会话移交 |
Feature_EditableUnfocusedAppearance |
启用 | Release 无条件禁用 | 设置 UI 中编辑非聚焦外观 |
Feature_AttemptHandoff |
禁用 | WindowsInbox 启用 | conhost 尝试把会话交给 OpenConsole |
Feature_ConhostAtlasEngineCustomShaders |
启用 | WindowsInbox 禁用 | conhost 的 Atlas 渲染引擎支持自定义着色器 |
Feature_UseNumpadEventsForClipboardInput |
禁用 | WindowsInbox 启用 | 剪贴板转换/输入状态机改用 Numpad 事件(注释说明为降低 Windows 内兼容性风险而保留旧版 GetQuickCharWidth) |
Feature_LegacyConhost |
禁用 | WindowsInbox 启用 | conhost 支持 ForceV2=false 并尝试加载 conhostv1.dll |
Feature_AtlasEnginePresentFallback |
禁用 | Release、WindowsInbox 启用 | Present1 失败时回退到 Present |
Feature_AtlasEngineLoudErrors |
启用 | Release 无条件禁用 | Atlas 引擎向消费者上报每次提交失败,仅非 Release 需要 |
Feature_NearbyFontLoading |
启用 | WindowsInbox 禁用 | 渲染时加载可执行文件同目录下的字体(conhost 避免遍历整个 system32) |
Feature_AdjustIndistinguishableText |
启用 | WindowsInbox 禁用 | 前景色自动调整为更可见 |
Feature_ScrollbarMarks |
启用 | WindowsInbox 禁用 | 实验性滚动条标记 |
Feature_DynamicSSHProfiles(id 9031) |
启用 | Release 无条件禁用 | OpenSSH 配置文件的动态 profile 生成 |
Feature_ShellCompletions(id 3121) |
启用 | Release 无条件禁用 | 供客户端请求显示建议列表的实验性转义序列 |
Feature_VtChecksumReport(id 14974) |
启用 | WindowsInbox 禁用 | DECRQCRA 校验和上报(Terminal 内已有开关,conhost 无关闭途径故整体裁剪) |
Feature_ScratchpadPane(id 997) |
禁用 | Dev、Canary 启用 | 草稿板面板,用于验证非终端面板 |
Feature_MarkdownPane(id 16495) |
禁用 | Dev、Canary 启用 | Markdown 面板,验证 Markdown 解析 |
Feature_KeypadModeEnabled(id 16654) |
禁用 | Dev 启用 | 使 DECKPAM/DECKPNM 序列按预期工作 |
Feature_SaveSnippet(id 9971) |
启用 | Release 无条件禁用 | 保存片段(Save Snippet) |
Feature_QuickFix(id 16599) |
启用 | Release 无条件禁用 | 快速修复(Quick Fix)菜单 |
Feature_DebugModeUI |
启用 | Release 无条件禁用 | 调试模式设置的 UI 入口 |
Feature_WarnOnInvalidSettingsMediaResources |
启用 | Release 无条件禁用 | 图标/背景图/着色器等媒体资源缺失时弹警告 |
从这张清单可以直观看到三种典型用法:实验性功能用“默认禁用 + Dev/Canary 品牌启用”(如 Feature_KeypadModeEnabled);商店版与系统内置版差异化用“品牌禁用 WindowsInbox”或“仅 WindowsInbox 启用”(如 Feature_LegacyConhost);已稳定但尚未进入正式发布的用 alwaysDisabledReleaseTokens 在 Release 中兜底禁用(如 Feature_QuickFix)。
七、源码中的两种消费方式
编译期裁剪——功能连同代码一起被排除出产物:
- src/host/exe/exemain.cpp 在
TIL_FEATURE_RECEIVEINCOMINGHANDOFF_ENABLED、TIL_FEATURE_LEGACYCONHOST_ENABLED宏下分支,决定进程是否具备接收移交、回退旧版 conhost 的能力; - src/server/IoDispatchers.cpp 用
#if !TIL_FEATURE_ATTEMPTHANDOFF_ENABLED区分是否执行移交路径; - src/renderer/atlas/BackendD3D.cpp 在
TIL_FEATURE_CONHOSTATLASENGINECUSTOMSHADERS_ENABLED下编译自定义着色器支持。
运行时判断——代码保留在产物中,按构建期裁决结果决定是否生效:
// src/cascadia/TerminalApp/AppActionHandlers.cpp
void TerminalPage::_HandleOpenScratchpad(const IInspectable& sender,
const ActionEventArgs& args)
{
if (Feature_ScratchpadPane::IsEnabled())
{
const auto& scratchPane{ winrt::make_self<ScratchpadContent>() };
...
两者最终读的都是同一个生成宏,区别仅在于是否允许产物二进制本身携带这段代码。
八、给贡献者的实操指引:新增一个特征开关
按文档与构建链的约定,新增开关的完整步骤是:
- 在 src/features.xml 中追加一个
<feature>元素,name以Feature_开头(XSD 会强制校验命名模式),填好description与stage,按需追加<id>、分支/品牌 token 或空的<alwaysDisabledReleaseTokens/>; - 在 C++ 代码中二选一消费:编译期用
#if TIL_FEATURE_XXX_ENABLED(特征名全大写、前缀TIL_、后缀_ENABLED),运行时用Feature_XXX::IsEnabled();无需手动 include 头文件,因为TilFeatureStaging.h已被全局强制包含(见 src/common.build.post.props); - 本地构建时 MSBuild 会自动运行生成脚本(build/rules/GenerateFeatureFlags.proj),当前 Git 分支与
WindowsTerminalBranding属性决定你构建出的二进制里哪些开关生效;如需验证其他分支的效果,可手动运行脚本并传-BranchOverride,例如:
powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command `
"&'tools\Generate-FeatureStagingHeader.ps1' -Path 'src\features.xml' -Branding Dev -BranchOverride 'rel/1.0' | Out-File -Encoding UTF8 feature_flags_preview.h'"
- 提交前可用 XSD(tools/FeatureStagingSchema.xsd)自查 XML 合法性;脚本本身在加载时即执行
Validate,非法文档会直接报错中断生成。
需要注意的是,分支 token 是通配匹配而非精确匹配(PowerShell 的 -Like 语义),编写 branchToken 时应参考仓库实际的分支命名规范;并且“启用型 token 覆盖禁用型 token、最长匹配胜出”这两条规则意味着,当多个 token 都可能命中时,最终状态以优先级表中更靠前的那条为准。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00