Windows Terminal TerminalSettingsModel 设计详解:级联设置、继承 DAG 与 WinRT 对象模型(Spec 885)

发布时间:2026/9/7 3:43:47
Windows Terminal TerminalSettingsModel 设计详解:级联设置、继承 DAG 与 WinRT 对象模型(Spec 885) Windows Terminal TerminalSettingsModel 设计详解级联设置、继承 DAG 与 WinRT 对象模型Spec #885【免费下载链接】terminalThe new Windows Terminal and the original Windows console host, all in the same place!项目地址: https://gitcode.com/GitHub_Trending/term/terminal本文基于 Windows Terminal 仓库中的设计文档 Spec #885 - Terminal Settings Model完整拆解 Windows Terminal 设置模型TerminalSettingsModel的设计动机、核心对象与继承机制从CreateChild()对象模型继承、profiles.defaults回退链、继承 DAG 的深拷贝算法到序列化错误处理与 Settings UI 的读写流程并结合 src/cascadia/TerminalSettingsModel 中的实际源码印证设计落地情况。读完本文你能理解 Terminal 的 settings.json 是如何被分层加载、继承解析并暴露为 WinRT 对象的。一、设计动机为什么设置模型必须成为 WinRT 对象Spec #885 的摘要Abstract开宗明义该提案将原有的TerminalSettings项目做一次大规模重构更名为TerminalSettingsModel由它负责将 Windows Terminal 的设置以WinRT 对象的形式暴露、序列化与反序列化。这样一来TerminalApp、TerminalControl、TerminalCore 等既有组件可以直接以 WinRT 对象访问设置Settings UI设置界面可以修改这些设置对象Shell Extension如 jumplist 跳转列表可以引用这些设置对象。设计驱动Inspiration非常明确首要驱动力是 Settings UI。撰写该 spec 时Terminal 的设置对象是在 TerminalApp 项目中序列化的Settings UI 若要经由 XAML 访问这些对象它们必须是 WinRT 对象。除此之外未来的 shell 扩展如 jumplist同样依赖这一点。在当前仓库中这一重构已经落地src/cascadia/TerminalSettingsModel/目录下存在完整的CascadiaSettings.h、GlobalAppSettings.h、Profile.h、ColorScheme.h、ActionMap.h等 WinRT runtimeclass 实现源码命名空间为winrt::Microsoft::Terminal::Settings::Model见 CascadiaSettings.h与 spec 中规划的根命名空间Microsoft.Terminal.Settings.Model一致。二、对象迁移四个设置对象成为 WinRT 对象Spec 的“Solution Design”部分列出了从 TerminalApp 迁入 TerminalSettingsModel原 TerminalSettings 项目的四个对象ColorScheme配色方案Profile单个配置文件标签页/窗格的一套终端设置GlobalAppSettings全局应用级设置CascadiaSettings容纳上述所有对象的设置容器同时IControlSettings与ICoreSettings这两个接口被移到Microsoft.Terminal.TerminalControl命名空间下目的是让 TerminalControl 一层能更直接地消费设置模型——这一消费路径在第五节的 TerminalControl 部分展开。从源码看这一规划与实现是对应的GlobalAppSettings.h 中GlobalAppSettings实现IInheritableGlobalAppSettings并暴露ColorSchemes()、ActionMap()、Themes()等成员与 spec 所述“全局设置包含配色方案、键绑定动作等”一致CascadiaSettings.h 头文件注释写明其职责“本类是所有应用设置的容器由两部分组成Globals应用级设置与 Profiles作用于单个终端实例的设置集同时包含该对象的序列化/反序列化逻辑”。三、拆分动作模型AppKeyBindings 与 KeyMappingWindows Terminal 在引入 TerminalSettingsModel 之前用三类对象表示“动作action”AppKeyBindings所有已定义键绑定及其对应动作的映射ActionAndArgs可反序列化的动作内部还嵌套更多对象ShortcutActionDispatch负责分发给定ActionAndArgs的事件TerminalPage处理这些被分发的任何事件。Spec 提出用KeyMapping类拆分AppKeyBindings职责边界如下代码出自 spec 原文namespace TerminalApp { [default_interface] runtimeclass AppKeyBindings : Microsoft.Terminal.TerminalControl.IKeyBindings { AppKeyBindings(); // NOTE: It may be possible to move both of these to the constructor instead void SetDispatch(ShortcutActionDispatch dispatch); void SetKeyMap(KeyMapping keymap); } } namespace TerminalSettingsModel { [default_interface] runtimeclass KeyMapping { void SetKeyBinding(ActionAndArgs actionAndArgs, Microsoft.Terminal.TerminalControl.KeyChord chord); void ClearKeyBinding(Microsoft.Terminal.TerminalControl.KeyChord chord); Microsoft.Terminal.TerminalControl.KeyChord GetKeyBindingForAction(ShortcutAction action); Microsoft.Terminal.TerminalControl.KeyChord GetKeyBindingForActionWithArgs(ActionAndArgs actionAndArgs); } }拆分后AppKeyBindings只负责“检测并分发动作”而KeyMapping负责键绑定的反序列化与查询——这正是设置模型要承担的那一半。同目录补充规范Actions Addendum 的三步演进与主 spec 同目录的 Actions Addendum.md 进一步解决了设置模型中动作存储的表示问题。当时的痛点是JSON 把命令与键绑定写成一条组合项{ name: ..., command: copy, keys: ctrlc }而设置模型却把它们拆成KeyMappingKeyChord → ActionAndArgs映射和Command带图标/名称的动作包装导致两个问题序列化无法判断某命令与某键绑定是否指向同一动作无法决定何时写 name也不知道 name 是自动生成还是用户设置JSON 会膨胀重复处理同一动作可绑定多个 key chord命令面板只是按相同名字把它们合并成了一个条目。Addendum 的解决方案分三步Step 1合并动作Consolidating actions——把KeyChord提升进Command类使键绑定与命令面板动作归入同一个类runtimeclass Command { // The path to the icon (or icon itself, if its an emoji) String IconPath; // The associated name. If none is defined, one is auto-generated. String Name; // The key binding that can be used to invoke this action. // NOTE: Were actually holding the KeyChord instead of just the text. // KeyChordText just serializes the relevant keychord Microsoft.Terminal.Control.KeyChord Keys; String KeyChordText; // The action itself. ActionAndArgs ActionAndArgs; // NOTE: nested and iterable command logic will still be here, // But they are omitted to make this section seem cleaner. }配套改动包括Command::LayerJson需合并原KeyMapping::LayerJson与Command::LayerJson的逻辑内部用vectorKeyChord _keyMappings记录该动作的全部键位Keys()返回最近注册的一个嵌套/可迭代命令HasNestedCommands、NestedCommands、IterateOn继续保留。Step 2查询动作Querying actions——引入统一的ActionMap查询接口runtimeclass ActionMap { ActionAndArgs GetActionByKeyChord(KeyChord keys); KeyChord GetKeyBindingForAction(ShortcutAction action); KeyChord GetKeyBindingForAction(ShortcutAction action, IActionArgs actionArgs); IMapViewString, Command NameMap { get; }; }内部存储为两张表std::mapKeyChord, InternalActionID _KeyMap; std::mapInternalActionID, Command _ActionMap;其中InternalActionID是ActionAndArgs的哈希两个ShortcutAction与IActionArgs相同的ActionAndArgs会产出同一个哈希值。当前仓库的 ActionMap.h 中保留了这一设计的直接痕迹using InternalActionID size_t;L33以及GetActionByKeyChord、NameMap()、KeyBindings()、AllKeyBindingsForAction等查询接口L66–L78并实现了IInheritableActionMap以支持层级继承与 Addendum 中“给ActionMap引入 parent 机制”一致。Step 3Settings UI 需求——ActionMap必须承担所有对Command的修改Command只暴露 getter例如SetKeyChord/SetName/SetIcon/SetAction修改冲突的 key chord 时要同步更新冲突方。Addendum 还专门处理了“解绑unbinding”场景向ActionMap::AddAction传入ActionAndArgs为Invalid/nullptr、Keys为指定 key chord 的Command即可在 JSON 中输出{ command: unbound, keys: ctrlc }表示该键位必须透传、且该条目必须从命令面板中移除。对于“父层绑定了 ctrlc 与 ctrlshiftc、子层只解绑 ctrlc”的复杂情况Addendum 引入_ConsolidatedActions表——它把跨当前层与父层的Command数据合并为一条完整视图查询顺序固定为_ConsolidatedActions→_ActionMap→ 逐层向上父层从而保证GetKeyChordForAction返回的是“全链路合并后”的正确键位。四、回退值Fallback Value把级联设置变成对象模型继承Windows Terminal 的级联设置cascading settings允许设置模型分层构建settings.json的值覆盖defaults.json的值。随着 Settings UI 与序列化引入系统必须知道每个设置值的来源。Spec 给出了一个经典场景// profile: color scheme value defaults: Solarized, // profiles.defaults A: Raspberry, // profile A B: Tango, // profile B C: Solarized // profile C假如用户通过 Settings UI 把profiles.defaults改成Tango那么 profile C 的颜色方案该不该跟着变无法判断——因为 profile C 的Solarized到底是继承自 defaults还是用户显式设置模型自己不知道。因此每个 profile 必须记录“我的值是继承来的还是显式设置的”。对象模型继承CreateChild() 与 Has/Clear 三元 APISpec 的解法是把级联逻辑从“纯 JSON 概念”下沉到对象模型中。每个设置对象新增CreateChild()函数例如GlobalAppSettingsGlobalAppSettings GlobalAppSettings::CreateChild() const { GlobalAppSettings child {}; child._parents.append(this); return child; }std::vectorT _parents记录了“当用户没有提供值时该问谁”。以LaunchMode为例getter/setter 变成spec 原文示例// _LaunchMode will now be a std::optionalLaunchMode instead of a LaunchMode // - std::nullopt will mean that there is no user-set value // - otherwise, the value was explicitly set by the user // returns the resolved value for this setting LaunchMode GlobalAppSettings::LaunchMode() { // fallback tree: // - user set value // - inherited value // - system set value return til::coalesce_value(_LaunchMode, _parents[0].LaunchMode(), _parents[1].LaunchMode(), ..., LaunchMode::DefaultMode); } // explicitly set the user-set value void GlobalAppSettings::LaunchMode(LaunchMode val) { _LaunchMode val; } // check if there is a user-set value // NOTE: This is important for the Settings UI to identify whether the user explicitly or implicitly set the presented value bool GlobalAppSettings::HasLaunchMode() { return _LaunchMode.has_value(); } // explicitly unset the user-set value (we want the inherited value) void GlobalAppSettings::ClearLaunchMode() { return _LaunchMode std::nullopt; }三个 API 各司其职getter 返回解析后的值用户设置 → 继承值 → 系统默认值的级联Has...()回答“用户是否显式设置了”Settings UI 正是靠它区分显示值来自用户还是继承Clear...()显式清除用户设置让值回落到继承链。这套设计在仓库源码中已完整落地且实现方式比 spec 草图更进一步IInheritable.h 定义了IInheritableT模板CreateChild()创建子实例并把this设为 parentL29–L42同时提供AddLeastImportantParent/AddMostImportantParent两个方法区分插入位置越靠前优先级越高以及ClearParents()、Parents()访问器同文件中的INHERITABLE_SETTING/INHERITABLE_SETTING_WITH_LOGGING/INHERITABLE_NULLABLE_SETTING宏L194 起批量生成了 spec 描述的Has##Name()、Clear##Name()、getter先查_##name为空则遍历_parents递归找值最后落到系统默认值与 setter。宏注释中也明确写有回退链语义“fallback: user set value -- inherited value -- system set value”coalesce.h 提供了 spec 示例中引用的til::coalesce_value工具返回第一个有值的std::optional否则返回基线值L30–L38并带有编译期断言要求必须以非 optional 基值结尾。可空设置Nullable Settings有些设置明确允许为 null例如Profile的Foregroundnull 是合法值含义是“继承”而不是“未设置”。Spec 为此设计了专用结构templatetypename T struct NullableSetting { IReferenceT setting{ nullptr }; bool set{ false }; };set表示该值是否被用户显式设置false 时应回退继承setting记录用户实际设置的值nullptr表示显式设置为 null。API 面的相应变化getter/setter 输入输出IReferenceT而非THas...()与Clear...()读写set标志。源码中的最终形态略有调整IInheritable.h 第 78–79 行用using NullableSetting std::optionalstd::optionalT;表达了同样的三态语义——外层 optional 的“有无”等价于 spec 中的set是否用户设置内层 optional 的“有无”区分“显式设为 null”与“有值”。INHERITABLE_NULLABLE_SETTING宏的 setter 也精确实现了这一区分传入nullptr时设置“内层 nullopt”显式 null传入值时设置内层具体值。从源码结构看这是把 spec 中IReference bool的两字段设计压缩成了嵌套 optional 的等价实现。五、CascadiaSettings 的分层加载LayerJson 与四步构建流程CascadiaSettings负责加载整个设置模型。Spec 描述了其LayerJson的工作方式为每个组件创建 child 并把新值叠加在上层——void CascadiaSettings::LayerJson(const Json::Value json) { _globals _globals.CreateChild(); _globals-LayerJson(json); // repeat the same for Profiles... }效果是加载defaults.json后_globals持有 defaults.json 中的全部值被省略的项回退到其父一个纯系统默认值的GlobalAppSettings加载settings.json后_globals只持有 settings.json 中的值被省略的项回退到 defaults.json 构建出来的父对象。Profile的继承回退顺序更复杂spec 明确列出了四级顺序settings.json中的 profilesettings.json的profiles.defaults仅对动态 profile动态 profile 生成器中的硬编码值defaults.json中的 profile。对应地CascadiaSettings必须按以下四步执行spec 原文步骤加载 defaults.json把新建 profile 追加进_profiles行为不变加载动态 profile把新建 profile 追加进_profiles行为不变加载 settings.json 的profiles.defaults由profiles.defaults构建Profile保存为Profile _profileDefaults对每个已存在 profile 执行CreateChild()把_profileDefaults作为第一个 parent 加到每个 child_parents[_profileDefaults, generator/defaults.json 的值]用 child 替换_profiles中的原 profile加载 settings.json 的profiles.list若存在匹配的 profile从匹配项CreateChild并把 json 叠加到 child 上。注意此处不再把_profileDefaults加为 parent因为它已经是祖先节点否则从_profileDefaultsCreateChild()并叠加 json与之前一样_profiles必须更新以移除被替换的父节点。此外_profileDefaults通过Profile CascadiaSettings::ProfileDefaults()对外暴露——spec 指出这能支撑 issue #7414 的实现让命令行启动的标签页使用名为 “Default” 的 profile而不是“默认 profile”。当前仓库的实现印证了这一流程CascadiaSettings.h 中的SettingsLoader结构体L83–L143持有inboxSettingsinbox 即 defaults.json与userSettings两组ParsedSettings并提供GenerateProfiles()动态 profile 生成、MergeInboxIntoUserSettings()、_addUserProfileParent()为 profile 挂接用户层父节点等与上述四步一一对应的方法CascadiaSettings类同样暴露ProfileDefaults()L177静态加载入口LoadDefaults()/LoadAll()。动态 profile 生成器也真实存在于源码中如 WslDistroGenerator.h、AzureCloudShellGenerator.h对应回退链中的第 3 级“生成器硬编码值”。Profile 继承 DAG 示例图底部为 settings.json 的 profile.list 与 profile.defaults箭头向上指向中间层 profile 对象再指向 defaults.json 的 profile 或 std::nullopt六、CreateChild() 与 Copy()继承 DAG 的深拷贝Spec 明确区分了两个方法CreateChild()创建一个继承父对象未定义值的新设置对象。只在反序列化期间使用用于正确解读和更新 JSON它不是取值的必要手段但可支撑更深的继承层级Copy()重建设置对象的内容包括对一个“复制出来的 parent”的引用而非原 parent。Settings UI 会用Copy()拿到CascadiaSettings的深拷贝并把 UI 数据绑定到该拷贝上因此Copy()必须在 IDL 中暴露。_parents在深拷贝时有两个典型陷阱引用原_parents→ 从一棵“过时的对象树”继承简单复制_parents→ 丢失引用语义。例如profile.defaults是所有展示中 profile 的 parent对它的修改应当影响所有profile不恰当的拷贝可能只把修改应用到其中一个。由于“多个 profile 共享同一个_profileDefaults父节点”整个继承结构从树演化成了有向无环图DAG——上方示意图展示了 profile 层级settings.json 的profile.defaults与profile.list在底部每个 profile 向上指向其 parentdefaults.jsonprofile、动态生成器最终指向std::nullopt表示无值可继承。Spec 给出的 DAG 克隆算法Python 示意原文保留# Function to clone a graph. To do this, we start # reading the original graph depth-wise, recursively # If we encounter an unvisited node in original graph, # we initialize a new instance of Node for # cloned graph with key of original node def cloneGraph(oldSource, newSource, visited): clone None if visited[oldSource.key] is False and oldSource.adj is not None: for old in oldSource.adj: # Below check is for backtracking, so new # nodes dont get initialized every time if clone is None or(clone is not None and clone.key ! old.key): clone Node(old.key, []) newSource.adj.append(clone) cloneGraph(old, clone, visited) # Once, all neighbors for that particular node # are created in cloned graph, code backtracks # and exits from that node, mark the node as # visited in original graph, and traverse the # next unvisited visited[old.key] True return newSource该算法时间/空间复杂度为 O(n)n 为展示的 profile 数。Spec 指出实现时有两处微调在CascadiaSettings的克隆体中单独保留一份对profile.defaults的Profile引用visited是一张“原 Profile 指针 → 克隆 Profile”的映射表保证各 profile 引用的是同一个克隆出来的 Profile而不是各拷一份。这与源码实现高度吻合Profile.h 第 93–94 行声明了CopyInheritanceGraphs(std::unordered_mapconst Profile*, winrt::com_ptrProfile visited, ...)与CopyInheritanceGraph(...)visited参数正是 spec 中“指针 → 克隆体”映射的直接体现CascadiaSettings::Copy()CascadiaSettings.h L165与ActionMap::Copy()则分别完成了设置模型整体与动作表的深拷贝。七、序列化、警告与错误处理JsonUtils 与 ConversionTrait引入Microsoft.Terminal.Settings.Model的 WinRT 对象后序列化/反序列化逻辑从 TerminalApp 移入 TerminalSettingsModel现仓库中的 JsonUtils.h。序列化是既有ConversionTrait结构模板的扩展ConversionTrait已包含FromJson与CanConvert序列化则由ToJson函数承担。在源码中可以看到这一模式贯穿各对象例如Profile同时声明了FromJson、LayerJson、ToJsonProfile.h L97–L99。WinRT 不支持异常错误与警告的内部记录Spec 特别指出当时CascadiaSettings反序列化遇到任何错误会抛出异常由调用方捕获并回退到一个简单的CascadiaSettings对象但WinRT 不支持异常。解决方案是CascadiaSettings遇到序列化错误时内部记录该错误的相关信息并“若无其事”地返回一个简单CascadiaSettings消费者随后必须调用CascadiaSettings::GetErrors()与CascadiaSettings::GetWarnings()来理解是否出错、以及如何向用户呈现。当前源码中这一机制演化为 CascadiaSettings.h L186–L189 的接口// load errors winrt::Windows::Foundation::Collections::IVectorViewModel::SettingsLoadWarnings Warnings() const; winrt::Windows::Foundation::IReferenceModel::SettingsLoadErrors GetLoadingError() const; winrt::hstring GetSerializationErrorMessage() const;配套的警告类型定义在 TerminalWarnings.h / TerminalWarnings.idl 中。可以推断这是 spec 中GetErrors()/GetWarnings()二接口方案经演进后的最终形态区分“加载级错误”与“警告列表”。八、TerminalApp 与 TerminalControl加载、热重载与设置的应用TerminalApp加载与重新加载Spec 规定 TerminalApp 构造并引用CascadiaSettings的方式持有 “settings.json” 文件路径的全局引用通过CascadiaSettings(settings.json)构造设置对象先用已编译为字符串字面量的defaults.json数据构建CascadiaSettings再把 settings.json 数据叠加其上检查错误/警告并妥善处理。这与此前“settings.json 路径硬编码、整个LoadAll()调用包在错误处理器里”的旧模型不同。Spec 还特别备注该模型允许在未来把更多设置文件叠加到 Terminal Settings Model 之上例如从 marketplace 等外部位置导入设置文件。当 TerminalApp 检测到 settings.json 变化时重复上述步骤可以缓存由 “defaults.json” 构建的CascadiaSettings结果以提升性能。TerminalControl获取并应用设置Spec 撰写时TerminalApp 会构造TerminalControl.TerminalSettingsWinRT 对象以向宿主终端暴露IControlSettings和ICoreSettings。把这两个接口下沉到 TerminalControl 层后TerminalApp 对“如何向某个 TerminalControl 实例暴露相关设置”有了更好的控制权。TerminalSettings实现IControlSettings与ICoreSettings移入 TerminalApp充当CascadiaSettings与 TerminalControl 之间的桥在 TerminalControl 构造或热重载时TerminalSettings通过复制CascadiaSettings的相关值来构造随后传递给 TermControl进而传递给 TermCore。未来考量Future considerationsSpec 最后列出了三条前瞻性设计均值得记录1. TerminalSettings 传引用passing by referenceTermApp 通过复制CascadiaSettings的相关值合成TerminalSettings交给 Terminal Control但 ctrl滚轮、ctrlshift滚轮这类交互是直接修改实例化TerminalSettings的值的设置重载会丢失这些实例级修改。改进方案是让TerminalSettings成为引用而非复制CascadiaSettings相关值的 WinRT 对象预览类命令如setColorScheme需要现有TerminalSettings的克隆可给TerminalSettings增加CloneAPI。传引用时“覆盖值”变得更复杂——需要覆盖对常量值如snapOnInputtrue或引用值如colorScheme的引用。2. 层叠更多设置Layering Additional Settings随着扩展或主题带来新的设置来源可以暴露LayerSettings(String path)把新设置文件叠加到现有CascadiaSettings上内部已实现只需经 C/WinRT 暴露runtimeclass CascadiaSettings { // Load a settings file, and layer those changes on top of the existing CascadiaSettings void LayerSettings(String path); }3. Settings UI修改与应用设置DRAFTruntimeclass CascadiaSettings { // Create a copy of the existing CascadiaSettings CascadiaSettings Clone(); // Compares object to source and applies changes to // the settings file at outPath void Save(String outPath); }整体流程Settings UI 持有 TerminalApp 的CascadiaSettings引用settingsSourceUI 打开时执行settingsClone settingsSource.Clone()得到自己的克隆用户导航 UI 时读取settingsClone的相应内容XAML 数据绑定把用户修改写回settingsClone点击保存/应用时调用settingsClone.Save(settings.json)——比较settingsClone与settingsSource的差异并把改动注入 settings.jsonTerminalApp 随后因文件变化而自我更新settingsSource也随之同步。当用户同时在直接编辑 settings.json 与操作 Settings UI 时可以比较settingsSource与settingsClone确保三方一致。另注用户若想导出当前配置Save同样可以写到新文件。4. 再序列化ReserializationDRAFT反序列化后把新 JSON 注入 settings.json不应破坏已有的注释与格式。再序列化发生在比较settingsSource与settingsClone之后对 diff 中的每个设置定位 JSON 中相应位置——若 key 已存在则更新为settingsClone的值否则把 key/value 追加到该节末尾与动态 profile 追加到profiles的方式类似。九、验证路径在仓库中继续阅读围绕本 spec 的落地情况可以在当前仓库中沿以下路径验证设计文档本体Spec #885 - Terminal Settings Model 与 Actions Addendum及配图 Inheritance-DAG.png继承基础设施IInheritable.hCreateChild()、_parents、INHERITABLE_SETTING宏族、NullableSetting、coalesce.htil::coalesce_value设置对象实现CascadiaSettings.h、GlobalAppSettings.h、Profile.h、ActionMap.h、JsonUtils.h默认设置数据源defaults.json即 spec 中“编译为字符串字面量”的 inbox 设置单元测试src/cascadia/UnitTests_SettingsModel源码中ActionMap、Profile等类均前向声明了SettingsModelUnitTests下的DeserializationTests、KeyBindingsTests、ProfileTests等测试类覆盖反序列化与键绑定场景。需要说明的是本 spec 成文于 2020 年Actions Addendum 更新于 2021 年其中的代码片段是设计阶段的接口草图当前仓库源码在细节上已有演进例如继承机制从std::vectorT _parents加coalesce_value的手写 getter/setter演化为IInheritableT模板 宏生成的通用实现NullableSetting从两字段结构演化为嵌套 optional。阅读时应以“spec 定方向、源码定现状”为原则对照理解。【免费下载链接】terminalThe new Windows Terminal and the original Windows console host, all in the same place!项目地址: https://gitcode.com/GitHub_Trending/term/terminal创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考