- 桌面应用
- 即时通讯
【免费下载链接】teams-for-linux
Unofficial Microsoft Teams for Linux client
本文是 teams-for-linux(非官方 Microsoft Teams Linux 客户端)配置体系的一项核心架构决策(ADR-025)的实践指南。它系统梳理了客户端从"扁平 yargs 配置"演进为"嵌套命名空间配置"的完整约定:新选项如何命名、55 个既有扁平选项如何映射到嵌套目标、四个取反重命名的陷阱、双拼写弃用窗口的机制,以及用户侧迁移工具(Settings > Show Updated Config…)的用法。读完本文,你将掌握为该项目新增配置项的正确姿势,也能安全地把自己的config.json迁移到新命名体系。
一、背景:扁平配置的命名混乱与增量演进决策
ADR-025 源自一项配置组织研究(2025-11-09 启动,原研究文档已删除,可查阅 git 历史)。该研究对基于 yargs 的扁平配置做了全面审计,发现三类典型问题:
- 相关选项散落在不同类别:同属一个功能的选项在顶层命名空间中彼此隔离,难以发现;
- 负向与正向命名混用:
disableNotifications(负向)旁边就是trayIconEnabled(正向),风格完全不统一; - 缩写不一致:
customBGServiceBaseUrl与isCustomBackgroundEnabled并排出现,一个缩写BG,一个全拼Background; - 缺少"何时该变成嵌套对象"的规则:嵌套与否全凭当时提交者的直觉。
研究于 2026-01-18 得出结论:拒绝一次性大爆炸迁移(big-bang migration),改为增量演进——新功能从第一天起就使用嵌套键;既有扁平选项在其所属模块被重构时机会主义地迁移。这一策略已运行数月,研究跟踪的部分已弃用选项甚至已被直接移除,确立了硬删除而非长期别名的先例(见 app/intune/README.md:ssoInTuneEnabled及其兄弟选项已整体移除,改用auth.intune.enabled/auth.intune.user)。
截至 ADR 编写时,app/config/options.js 声明了 72 个顶层选项,其中 17 个是带fields元数据的对象类型,55 个仍是扁平选项;docs-site/static/config-schema.json 是由scripts/generateConfigDocs.js生成的事实来源(该文件随项目演进,当前已声明 85 个选项,optionCount字段可见)。
二、命名规范:规则零与嵌套触发条件
规则零:扁平顶层命名空间对新增关闭
新选项永远嵌套。新增配置项的第一步不是起名,而是先查 docs-site/static/config-schema.json(或 app/config/options.js)里是否已有"属于你的功能领域"的命名空间,把叶子键加进去;只有确实没有任何现成命名空间合适时,才创建新的父级对象。
满足任一条件即嵌套
当以下任一条件成立时,选项必须嵌套到对象下:
- 三个及以上选项与同一功能相关;
- 多个选项共享公共前缀;
- 某个选项只有在兄弟 gate 开启时才有意义(依赖项);
- 一组选项描述同一个概念性功能。
依赖项永远跟随其 gate 嵌套:例如mqtt.brokerUrl必须位于mqtt.enabled之下,因为 broker URL 只在 MQTT 集成开启时有意义。
主功能 gate 使用正向命名
- gate 叶子键一律是
enabled,绝不用disable*名字; - 只有当 gate 拥有依赖兄弟时,它才配拥有自己的对象;独立布尔值用正向
<feature>Enabled风格的叶子。
两种形态今天都在代码中并存,可对照 app/config/options.js:
// gate 拥有 mode/width/height 三个依赖兄弟 → 独立对象 + enabled "media": { "camera": { "resolution": { "enabled": false, "mode": "remove", "width": 0, "height": 0 } }, // 无依赖兄弟的独立布尔 → <feature>Enabled 叶子 "video": { "menuEnabled": false } }逃生舱(escape hatch)保留负向名
纯粹用于"关闭某个平台行为"的选项保留负向名,因为把它们反过来会掩盖"变通方案"的语义。典型代表:
disableGpu(→performance.disableGpu):GPU 出问题时的变通;disableTimestampOnCopy(→platform.disableTimestampOnCopy);network.disableQuic:为绕过 Chromium QUIC 协议错误(issue #2518)而存在。
例外:media.microphone.disableAutogain、media.preventDeviceSwitching携带负向名,但早于本 ADR 就已发布,不构成先例。
全拼不缩写
单词完整拼出,绝不缩写:用customBackground而不是customBG。同时依靠嵌套结构让全拼名字保持简短:tray.enabled优于trayIconEnabled——嵌套本身承担了此前必须写进选项名的类别信息。
三、完整的扁平 → 嵌套重命名映射表
下表将每个扁平选项映射到其嵌套目标,与 ADR-025 原表完全一致。其中四个重命名取反布尔值(Inverted 列为 Yes),工具必须对这些值取反而不是复制;空单元格表示原样复制。例如disableNotifications: true应变为notifications.enabled: false,且默认值在取反下保持行为不变(disableNotifications默认为false,所以notifications.enabled默认为true)。
| 扁平选项 | 嵌套目标 | 取反 |
|---|---|---|
appTitle | app.title | |
url | app.url | |
partition | app.partition | |
frame | window.frame | |
menubar | window.menubar | |
minimized | window.minimized | |
closeAppOnCross | window.closeOnCross | |
minimizeOnClose | window.minimizeOnClose | |
alwaysOnTop | window.alwaysOnTop | |
class | window.class | |
customCSSName | appearance.cssName | |
customCSSLocation | appearance.cssLocation | |
followSystemTheme | appearance.followSystemTheme | |
trayIconEnabled | tray.enabled | |
appIcon | tray.icon | |
appIconType | tray.iconType | |
useMutationTitleLogic | tray.useMutationTitleLogic | |
disableNotifications | notifications.enabled | Yes |
disableNotificationSound | notifications.sound.enabled | Yes |
disableNotificationSoundIfNotAvailable | notifications.sound.onlyWhenAvailable | |
disableNotificationWindowFlash | notifications.windowFlash | Yes |
disableBadgeCount | notifications.badgeCountEnabled | Yes |
notificationMethod | notifications.method | |
defaultNotificationUrgency | notifications.urgency | |
enableIncomingCallToast | incomingCalls.toast | |
incomingCallCommand | incomingCalls.command | |
incomingCallCommandArgs | incomingCalls.commandArgs | |
awayOnSystemIdle | idleDetection.setAwayOnIdle | |
appIdleTimeout | idleDetection.timeout | |
appIdleTimeoutCheckInterval | idleDetection.checkInterval.detectIdle | |
appActiveCheckInterval | idleDetection.checkInterval.detectActive | |
authServerWhitelist | auth.serverWhitelist | |
ssoBasicAuthUser | auth.basic.user | |
ssoBasicAuthPasswordCommand | auth.basic.passwordCommand | |
clientCertPath | auth.clientCertificate.path | |
clientCertPassword | auth.clientCertificate.password | |
customCACertsFingerprints | auth.customCACertificateFingerprints | |
proxyServer | network.proxyServer | |
isCustomBackgroundEnabled | customBackground.enabled | |
customBGServiceBaseUrl | customBackground.serviceBaseUrl | |
customBGServiceConfigFetchInterval | customBackground.configFetchInterval | |
defaultURLHandler | urlHandling.defaultHandler | |
meetupJoinRegEx | urlHandling.meetupJoinRegEx | |
onNewWindowOpenMeetupJoinUrlInApp | urlHandling.openMeetupJoinInApp | |
globalShortcuts | shortcuts.global | |
disableGlobalShortcuts | shortcuts.disableWhileFocused | |
disableGpu | performance.disableGpu | |
electronCLIFlags | performance.electronCLIFlags | |
clearStorageData | storage.clearData | |
webDebug | development.webDebug | |
watchConfigFile | development.watchConfigFile | |
chromeUserAgent | platform.chromeUserAgent | |
emulateWinChromiumPlatform | platform.emulateWindowsChromium | |
spellCheckerLanguages | platform.spellCheckerLanguages | |
disableTimestampOnCopy | platform.disableTimestampOnCopy |
注意:notifications、idleDetection、network、auth四个目标对象是已经发布的、已持有无关叶子的命名空间,把重命名选项并进去是刻意为之;上表每个叶子键都已与已发布字段核对过,不存在冲突(例如notifications已含timeoutType、electron.clickAction等)。
四、特殊行的设计细节
disableGlobalShortcuts是数组而非布尔
它是要在应用聚焦时禁用的加速键数组(见 app/config/options.js),重命名为shortcuts.disableWhileFocused是澄清语义而非取反。
disableNotificationSoundIfNotAvailable不取反
true原本就表示"仅在状态为 Available 时播放声音",与onlyWhenAvailable: true语义完全一致,因此无需取反。
idleDetection.checkInterval的语义化命名
idleDetection.checkInterval的两个叶子按"各自轮询检测什么"命名:
detectIdle:活跃期间使用(appIdleTimeoutCheckInterval),用于观察空闲何时来临;detectActive:空闲期间使用(appActiveCheckInterval),用于观察活跃状态的回归。
在 app/idle/monitor.js 中可看到这对调用关系。ADR 特别指出:如果起名.idle和.active,极易被反向接错线——app/idle/README.md 此前就把这对语义写反了,直到随本 ADR 一并修正。
awayOnSystemIdle为何不是enabledgate
它映射到idleDetection.setAwayOnIdle而非idleDetection.enabled,因为idleDetection作为常开对象发布,没有主开关(它已持有forceState、stateFile等叶子)。
disableBadgeCount与minimizeOnClose的裁定
这两个选项在原始研究映射中没有目标,ADR 在此裁定:
disableBadgeCount→notifications.badgeCountEnabled(取反,加Enabled后缀是因为裸badgeCount布尔读起来像个数字);minimizeOnClose→window.minimizeOnClose。
minimizeOnClose与closeAppOnCross语义重叠(都改变关闭按钮 X 的行为),且当closeAppOnCross为true时minimizeOnClose被忽略(见 app/config/options.js 的 describe)。这只是观察记录,合并两者不在本 ADR 范围内。
storage.clearData对应 Electron session API
storage.clearData对应 Electron 的session.clearStorageDataAPI(https://www.electronjs.org/docs/latest/api/session#sesclearstoragedataoptions);命名空间本身提供了 "storage" 这个词,避免叶子名再重复一次(否则会变成storage.clearStorageData的叠词)。
auth命名空间的几个关键裁定
authServerWhitelist落在auth.serverWhitelist,不在auth.basic下——它是 Negotiate/NTLM 服务器白名单,属于凭据体系而非凭据本身;clientCertPath/clientCertPassword目标是auth.clientCertificate.*而非研究稿的auth.certificate.*,因为auth.clientCertificate.pinDialog.enabled已经随 ADR-024 发布,相邻放两个 certificate 命名空间会更糟;auth.basic.*与已发布的auth.webLogin.user/auth.webLogin.passwordCommand叶子名相似但用途不同,两者并存:auth.basic.*喂给 Electron 的 HTTP Basic/NTLMlogin对话框(代理与内网挑战),auth.webLogin.*用于预填微软 Web 登录表单。
五、弃用窗口机制:双名共存、告警与硬移除
issue #2841 敲定了重命名发布后的行为契约:
- 双名长期共存:扁平名与嵌套名在一段较长的弃用窗口内都可用;
deprecated标记:扁平选项在 app/config/options.js 中标为deprecated(如appTitle标记为"use app.title instead"),yargs 据此在每次启动时读取,对配置文件中出现的任何弃用键产生一条聚合的启动告警(不会每个键一条)——聚合逻辑见 app/config/deprecation.js,app/config/index.js 中的checkUsedDeprecatedValues负责在每次启动时触发;- 2.30.0 硬移除:扁平声明与投影层一起删除(见 issue #2842 的迁移跟踪);
- 嵌套名优先:配置文件中同时设置两个拼写时,嵌套名获胜;
- 告警指向可操作动作:告警会提示Settings > Show Updated Config…(issue #2913),该菜单项会把配置按新名复制一份,绝不动用户自己的
config.json——告警给的是行动而非仅仅一个问题。
分批发布:映射表按批次发布,每批在 release notes 中公告。shortcuts与storage重命名落在 2.17.0;其余目标命名空间全新的重命名随后整体落地(app/config/renames.js 中标注 Batch 1 与 Batch 2)。
两个重要限制:
- 告警只在"Settings 菜单可达"时指向迁移入口——
isMigrationMenuAvailable(app/config/deprecation.js)要求menubar !== "hidden"或托盘图标开启,两者满足其一即可(菜单栏与托盘都携带同一 App 子菜单); - 告警只检查配置文件。通过命令行 flag 或环境变量传入的弃用选项不会产生告警,这些入口在扁平名移除后将静默失效(app/config/deprecation.js 中记录了这一已知限制)。
事实来源的实时性:迁移按命名空间逐个推进,docs-site/static/config-schema.json 是"哪些目标已落地"的实时记录。要了解应用今天实际接受什么,读它而非本文的映射表。
六、运行时投影:renames.js让两个拼写同时工作
投影方向与存在性判定
弃用窗口期内,扁平名仍是所有模块实际读取的键。核心机制在 app/config/renames.js:机器可读表RENAMES把嵌套值"投影"到扁平键上(applyRenamedOptions,L152-L162),因此没有任何功能代码被扫荡改动——它们继续读扁平键,投影层负责翻译。
关键设计:投影的输入只来自配置文件(configFile),"用户是否设置过"以原始配置文件为判据,而非解析后的 config。若用解析后 config 判断"叶子是否为 undefined",会依赖 yargs 整体替换对象选项这一行为而偶然成立——一旦默认值深合并修复(issue #2842 的 gate A),未设置的叶子都会解析成默认值,不再 undefined,就会被投影去覆盖用户实际设置的值。
CLI / 环境变量的已知限制
由于投影不读取 argv,通过命令行 flag 或环境变量设置的嵌套名会被静默忽略——这些入口在移除前必须继续使用扁平名。yargs 以.env(true)运行,环境变量同理。
coerce:取反与数组包装
嵌套叶子不是 yargs 声明的选项,享受不到扁平名的免费强转。coerce函数(app/config/renames.js)补齐两件事:
- 取反:
toBoolean先把字符串"false"/"true"转成布尔再取反——直接对原始值取反会把"false"(truthy 字符串)反成false,与用户意图相反,且布尔叶子不会触发校验告警,构成静默翻转; - 数组包装:
type: "array"的选项(如globalShortcuts)在扁平名收到标量时会被 yargs 包成数组,嵌套叶子则不会——shortcuts.global: "Control+Shift+M"会以裸字符串到达,使 app/globalShortcuts/index.js 的Array.isArray检查失败,对disableGlobalShortcuts的for..of甚至会按字符迭代或抛错。RENAMES表中为数组型选项显式标注了type: "array"。
系统与用户配置合并中的双拼写
mergeConfigFiles(app/config/mergeDefaults.js)合并/etc/teams-for-linux/config.json系统配置与用户配置(用户优先、深合并)时,会先把系统文件中两个拼写都删除,再合并用户文件——否则系统文件的urlHandling.defaultHandler会覆盖用户的defaultURLHandler,因为投影会让嵌套名获胜。
isOptionSetByUser与disableGpu的 Wayland 特例
isOptionSetByUser(app/config/renames.js)回答"用户是否显式设置过某选项",当前只服务disableGpu:如果用户没说任何话,Wayland 下会默认强制禁用 GPU(见 app/startup/commandLine.js)。因此用户通过新名performance.disableGpu选择保留 GPU,必须被计入"已经表态"。它同时检查配置文件两个拼写与命令行各种拼写(--disableGpu、--disable-gpu、--no-disableGpu、带=false形式),但不把命令行的嵌套名算作表态——因为投影从不读取 argv,--performance.disableGpu=true的值从未被投影,若算作表态会让 Wayland 分支尊重一个从未生效的值。
gate A:yargs 整体替换对象选项与mergeDefaults.js
yargs 对对象选项是整体替换而非深合并:配置文件设置了某个命名空间的任一叶子,就会丢掉其余兄弟叶子的声明默认值。对全新命名空间无害(弃用窗口内没人读它),但对已发布叶子的命名空间是静默回归:把proxyServer移进network会让network.disableQuic解析为 undefined 从而重新启用 QUIC(#2518),把任何 auth 选项移进去会关掉auth.keepMsalCacheEncryptionCookie——那正是让用户跨重启保持登录的机制。app/config/mergeDefaults.js 的applyObjectDefaults在解析后恢复未设置叶子的声明默认值(issue #3011 修复),解除了这个阻塞——这就是四个已占用命名空间最后才迁移的原因。
测试保障:表不再无声漂移
app/config/renames.js 的注释说这张表是手工维护的,但它与 app/config/options.js 之间不再能无声漂移:tests/unit/configRenames.test.js中的 "config renames - table integrity" 套件把每个条目钉死在 options.js 上——每个扁平名必须是已声明且标记deprecated的选项、每个弃用消息必须点名嵌套替代名、每个嵌套目标必须存在于 schema、无重复名。另有 "gate A boundary" 套件确保任何重命名不得进入已发布叶子的四个命名空间(network、auth、idleDetection、notifications),直到 gate A 落地。
七、用户侧迁移工具:Settings > Show Updated Config…
ADR-029 明确否决了运行时自动改写config.json——那是用户手编辑、常放版本控制的文件,静默重写风险过高。取而代之的是用户主动调用的 codemod,随 v2.19.0 以 Settings 菜单的 "Show Updated Config…" 条目发布(issue #2913),菜单定义见 app/menus/appMenu.js。
工作流
- 在 Settings 菜单点击 "Show Updated Config…";
writeMigratedConfig(app/config/migrateFile.js)只读取用户的config.json(刻意不读系统+用户的合并——否则会把/etc的值抄进用户文件,把按键管理的管理员策略变成整个命名空间的用户覆盖);toNestedConfigFile(app/config/renames.js)把扁平键改写到嵌套目标,从不修改输入对象;取反与数组包装与运行时coerce对称(取反是对称的),两拼写并存时保留嵌套名(与运行时优先级一致);- 结果无条件写入
config.migrated.json(与原始文件同目录、沿用原始文件权限,回退为0o600属主可读写),原始config.json不被触碰; - 随后弹出对话框列出被重命名的键——审查发生在写入之后而非差异预览,且迁移文件会先跑一遍 app/config/validator.js 的 schema 校验,把类型问题展示给用户;
- 用户审阅
config.migrated.json后自行覆盖config.json。
writeMigratedConfig的返回状态机覆盖各种边界:no-config(没有配置文件)、nothing-to-migrate(已用新名)、blocked(命名空间被非对象占用导致无法迁移)、invalid-json(不可解析或非对象根)、write-failed、written。返回值只含选项名、绝不含配置值,整包结果可安全上屏与入日志(V8 的 JSON 解析错误会内嵌源文本片段,因此刻意不透传)。
手工迁移提示
- 迁移不是完全等价:扁平选项声明了类型,yargs 会把配置文件里的值强转(如
"disableGpu": "false"字符串变为布尔false),而嵌套叶子未声明、保持字符串"false"(truthy)。迁移工具应校验输出,不要假设两个文件解析结果相同; - 建议把迁移后的
config.migrated.json内容对照第三节映射表人工过一遍,特别是四个取反项; - 迁移按命名空间推进(issue #2842),且四个已占用命名空间最后落地——若你的配置用到了
notifications、idleDetection、network、auth中的新叶子,请以 docs-site/static/config-schema.json 为准确认当前接受的拼写。
八、被否决的方案与后果
否决:重新挂载已发布的嵌套对象
研究映射还建议把三个已嵌套对象改挂新父级:cacheManagement→storage.cacheManagement、logConfig→development.logConfig、msTeamsProtocols→urlHandling.msTeamsProtocols。否决:它们已是可发现、分组正确的对象,重新挂载一个正常工作的命名空间只会为零收益破坏每个现有用户配置,是纯粹的折腾。因此storage、development命名空间起步比设想更小(storage起初只有clearData),这是可接受的。
否决:旧名的运行时永久别名
原始研究否决了让扁平名永远工作的别名层,本 ADR 维持否决;先例是弃用窗口后的硬删除(ssoInTuneEnabled及其兄弟)。该否决针对的是永久层;而"时间盒回退窗口内读旧键"是独立问题,issue #2841 的回答是"可以"——双名并存直至 2.30.0 一起删除扁平声明与投影层。
后果评估
正面:贡献者得到一个关于"新选项如何命名、既有扁平选项将落位何处"的权威答案,无需阅读 1600 行的研究文档;映射稳定到可被工具消费;机制落地在 yargs 自身的deprecated字段上。
负面:四个取反意味着"朴素键复制"式迁移会静默翻转用户意图,工具必须参考 Inverted 列(coerce双向处理);迁移非完全等价(类型强转差异);映射表本身手工维护,但已被测试套件钉住。
中性:永久别名层保持否决;codemod 不再被否决;重命名按命名空间推进,四个已占用命名空间混合长期已发布与新到的叶子,它们最后走是因为 yargs 整体替换对象选项的问题已由 app/config/mergeDefaults.js 修复。
九、关联文档与延伸阅读
- ADR-024:Smartcard PKCS#11 PIN Dialog:发布了本 ADR 对齐的
auth.clientCertificate命名空间; - ADR-029:Config Schema as Single Source of Truth:在本文约定之上构建的"一 schema 三消费方"决策(生成文档、docs 配置浏览器、启动校验);
- Roadmap:Config Schema as Single Source of Truth(issue #2597),应用内设置窗口在此跟踪;
- 实时清单:app/config/options.js 与 docs-site/static/config-schema.json;
- 迁移与投影实现:app/config/renames.js、app/config/mergeDefaults.js、app/config/migrateFile.js、app/config/index.js;
- 弃用告警:app/config/deprecation.js;
- 测试保障:tests/unit/configRenames.test.js、tests/unit/configDeprecationWarning.test.js、tests/unit/configMigrateFile.test.js;
- 研究历史:见 git 历史中的
docs-site/docs/development/research/configuration-organization-research.md。
- 桌面应用
- 即时通讯
【免费下载链接】teams-for-linux
Unofficial Microsoft Teams for Linux client
相关推荐
Unleash 前端组件命名规范:以 ADR 驱动的 PascalCase 组件与文件命名实践
Unleash 前端组件命名规范:以 ADR 驱动的 PascalCase 组件与文件命名实践 本篇指南以 Unleash 开源功能管理平台(feature m
后端ADR 文件命名规范实战:architecture-decision-record 项目推荐的命令式 kebab-case 命名法
ADR 文件命名规范实战:architecture decision record 项目推荐的命令式 kebab case 命名法 本文以 architectu
Thunderbird for Android 接口与实现类命名规范:ADR-0004 决策解读与仓库实践
Thunderbird for Android 接口与实现类命名规范:ADR 0004 决策解读与仓库实践 Thunderbird for Android(前身
移动开发企业应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考