news 2026/9/18 12:20:18

.NET 运行时中 EnC/Hot Reload 可变类型系统的调试契约(RuntimeMutableTypeSystem)深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
.NET 运行时中 EnC/Hot Reload 可变类型系统的调试契约(RuntimeMutableTypeSystem)深度解析

.NET 运行时中 EnC/Hot Reload 可变类型系统的调试契约(RuntimeMutableTypeSystem)深度解析

【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime

导读

本文基于 dotnet/runtime 仓库中 docs/design/datacontracts/RuntimeMutableTypeSystem.md 这一契约文档,深入剖析 .NET 运行时如何通过 Data Contract(数据契约)机制,向调试器、SOS 及 cDAC(Diagnostics Data Contract Reader)暴露"类型加载之后发生的类型系统变化"——即 Edit and Continue(EnC)与 Hot Reload 动态添加字段(EnCAddedField)的底层数据布局与读取协议。读完本文,你将掌握:RuntimeMutableTypeSystem契约的 5 个 API 语义、EnCFieldDesc/EnCEEClassData/EnCSyncBlockInfo等核心数据结构的布局,以及从目标进程内存中枚举 EnC 新增字段、解析静态/实例字段实际存储地址的完整算法,并可对照 src/native/managed/cdac 中的 cDAC 参考实现与 src/coreclr/vm/encee.h 的运行时侧结构定义逐行验证。

契约定位:为什么需要"可变类型系统"契约

从不可变快照到可变类型系统

在 .NET 运行时(CoreCLR)中,常规的类型系统契约RuntimeTypeSystem描述的是静态的、类型加载完成之后固定不变的类型信息:FieldDesc、MethodTable、EEClass 等在首次加载后基本不再变化。调试器与诊断工具(如 SOS、dotnet-dump 分析器)可以通过该契约安全地读取类型布局。

然而,Edit and Continue(EnC)与 Hot Reload打破了这一静态假设:当开发者在调试会话中"编辑并继续"时,编译器会向正在运行的进程应用元数据更新(ApplyEditAndContinue),其中可能新增字段(通常仅限私有字段,参见 src/coreclr/vm/encee.h 中EnCEEClassData::AddField的注释)。新增字段无法像普通字段那样被"粘贴"到既有对象实例上(对象的内存布局在分配时已固化),于是运行时设计了一套旁挂(side-table)机制来存放这些字段的值。

RuntimeMutableTypeSystem契约正是这套机制的"只读视图":它专门暴露"初始类型加载之后发生的变化",让诊断工具在进程外也能准确定位 EnC 新增字段的存储位置,从而支持对加了字段的对象进行正确的内存读取、字段值查看与表达式求值。

契约在 cDAC 体系中的位置

该契约注册在 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs,版本标识为"c1",并被Validate<IRuntimeMutableTypeSystem>(registry)强制校验。注释明确指出其消费方是DacDbiImpl.cs——即调试器 DBI(Debugger Interface)层在处理 edit-and-continue 可变类型系统时使用。

契约的依赖关系(见文档"Contracts used"一节)为:

依赖契约用途
GC读取 dependent handle 的 extra info(GetHandleExtraInfo),获取 EnC helper 对象地址
Loader通过ModuleHandle查询模块的ModuleFlags.EditAndContinue标志与模块指针
Object从对象头获取 SyncBlock 地址(GetSyncBlockAddress)、读取数组数据(GetArrayData
RuntimeTypeSystem查询字段描述符的类型(GetFieldDescType)、获取模块指针

契约 API 全景

文档给出了契约的 5 个公开 API,以下逐一给出语义与使用场景:

IEnumerable<TargetPointer> EnumerateAddedFieldDescs(ITypeHandle typeHandle, bool staticFields); bool IsFieldDescEnCNew(TargetPointer fieldDescPointer); bool DoesEnCFieldDescNeedFixup(TargetPointer encFieldDescPointer); TargetPointer GetEnCStaticFieldDataAddress(TargetPointer encFieldDescPointer); TargetPointer GetEnCInstanceFieldAddress(TargetPointer objectAddress, TargetPointer encFieldDescPointer);
API作用典型使用方
EnumerateAddedFieldDescs枚举某个类型(MethodTable)通过 EnC 新增的全部字段描述符,可分别枚举实例字段与静态字段调试器在类型上浏览所有字段时,需把 EnC 字段也纳入
IsFieldDescEnCNew判断一个普通 FieldDesc 是否实际上是由 EnC 新增的(通过 offset 哨兵值识别)字段迭代器区分普通字段与 EnC 字段
DoesEnCFieldDescNeedFixup判断该 EnCFieldDesc 是否仍处于"未完成初始化"(NeedsFixup)状态调试器在字段尚未 Fixup 时避免错误读取
GetEnCStaticFieldDataAddress获取 EnC 新增静态字段实际存储值的地址查看静态字段值
GetEnCInstanceFieldAddress获取某对象实例上 EnC 新增实例字段实际存储值的地址查看实例字段值、表达式求值

核心数据结构与内存布局

契约文档的"Data descriptors used"一节给出了全部涉及的数据描述符。这些描述符与运行时侧的 C++ 结构一一对应,我们结合 src/coreclr/vm/encee.h 逐一对照。

EnCFieldDesc:EnC 新增字段的描述符

EnCFieldDesc继承自FieldDesc(见 src/coreclr/vm/encee.h),额外携带两个字段:

Data Descriptor 字段类型含义运行时成员(encee.h)
NeedsFixupint32非零表示该描述符尚未完成初始化(Fixup()未调用)m_bNeedsFixup
StaticFieldDatapointer指向承载该静态字段存储的EnCAddedStaticField,未分配存储前为 NULLm_pStaticFieldData

从 src/coreclr/vm/encee.h 可见,NeedsFixup()返回m_bNeedsFixupFixup(token)会调用EEClass::FixupFieldDescForEnC完成真正的 FieldDesc 初始化(可能触发类型加载与 GC,因此只能在进程恢复执行后进行),随后清除标志。m_pStaticFieldDatacdac_data偏移在 src/coreclr/vm/encee.h 中通过offsetof静态导出。

EnCAddedFieldElement 与 EnCEEClassData:按类组织的链表

  • EnCAddedFieldElement(src/coreclr/vm/encee.h)是链表节点:Next指向下一个元素,内嵌一个EnCFieldDesc m_fieldDesc(契约中FieldDesc字段即该内嵌描述符的地址,与 FieldDesc 布局兼容)。一个类新增的每个字段对应一个节点。
  • EnCEEClassData(src/coreclr/vm/encee.h)按类(MethodTable)组织 EnC 数据:
字段类型含义运行时成员
MethodTablepointer本条目对应的 MethodTable 指针m_pMT
AddedInstanceFieldspointer新增实例字段链表头m_pAddedInstanceFields
AddedStaticFieldspointer新增静态字段链表头m_pAddedStaticFields

运行时还维护m_dwNumAddedInstanceFields/m_dwNumAddedStaticFields计数(GetAddedInstanceFields/GetAddedStaticFields),文档注释强调:EnC 只能添加私有字段,因此这些字段对其他类型不可见。

Module::EnCClassList:模块级的查找表

所有EnCEEClassData条目存放在模块的EnCClassList中,这是一个UnorderedArrayBase(无序数组):

字段类型含义
Countuint32数组中有效条目数
Tablepointer指向存放条目的后备存储

EnCSyncBlockInfo / EnCAddedField:实例字段值旁挂于 SyncBlock

EnC 新增实例字段的值不是存在对象本体中,而是懒加载地挂在对象的 SyncBlock 上(见 src/coreclr/vm/encee.h):

  • EnCSyncBlockInfoSyncBlock::EnCInfo):挂在一个对象的 SyncBlock 上,List字段指向 EnC 新增实例字段链表头。SyncBlock 的EnCInfo是可选项(未配置 EnC 时不存在)。
  • EnCAddedField是链表节点:
字段类型含义
FieldDescpointer指向对应的EnCFieldDesc(区分这是哪个字段)
FieldDataObjectHandle一个dependent handle 对:primary OBJECTREF 是依赖锚点(被修改的对象实例),secondary OBJECTREF 是System.Diagnostics.EditAndContinueHelper实例
Nextpointer链表下一个节点

关键设计点(见 encee.h 注释):每个字段只存在一个EnCFieldDesc,但该对象的每个实例都会懒创建各自的EnCAddedField节点FieldData使用 dependent handle 的目的在于:当 primary(对象实例)被 GC 回收时,secondary(helper 对象)也随之释放,从而避免内存泄漏;EnCSyncBlockInfo::Cleanup在对象被回收后负责清理。

SyncBlock 与 Object 描述符

契约还引用了两个跨契约的数据描述符:

  • SyncBlock::EnCInfo(pointer):指向 Edit-and-Continue 新增字段信息,未配置 EnC 时该字段可缺省
  • Object(type size)(uint32):固定 Object 头部(到 MethodTable 指针为止)的字节大小——用于对装箱值类型执行"unbox"操作定位数据区(Object::Data偏移)。

FieldDesc::DWord2 与哨兵偏移

契约依赖FieldDescDWord2(uint32)打包字段偏移。在 src/coreclr/vm/field.h 中,FieldDesc的第二双字是一个 union:低 27 位m_dwOffset(字段偏移),高 5 位m_typeEnC 新增字段没有真实对象内偏移(它们未被放置在对象中),因此使用哨兵值FIELD_OFFSET_NEW_ENC(定义于 src/coreclr/vm/field.h,取值为FIELD_OFFSET_MAX - 4 = (1<<27)-1-4):

// Offset to indicate an EnC added field. They don't have offsets as aren't placed in the object. #define FIELD_OFFSET_NEW_ENC (FIELD_OFFSET_MAX-4)

该值通过 cDAC 全局变量FieldOffsetNewEnc暴露给契约读取方(src/coreclr/vm/datadescriptor/datadescriptor.inc):CDAC_GLOBAL(FieldOffsetNewEnc, T_UINT32, FIELD_OFFSET_NEW_ENC)。契约中的FieldDescFlags2.OffsetMask = 0x07ffffff即 27 位偏移掩码(在 RuntimeMutableTypeSystem_1.cs 中定义),IsFieldDescEnCNew正是把DWord2 & 0x07ffffffFieldOffsetNewEnc全局值比较来判定。

静态字段的存储:EnCAddedStaticField

EnCAddedStaticField(src/coreclr/vm/encee.h)承载 EnC 新增静态字段的值:

字段类型含义
FieldDatapointer内联存储:基本类型字段直接是值的起始地址;class/值类型字段则是指向 GC 跟踪存储(对象引用)的指针

注意:FieldData结构体的最后一个字段,因为它是变长的(BYTE m_FieldData,参见 encee.h 注释"the actual size of this type is variable")。EnCFieldDesc::StaticFieldData在存储分配前为 NULL,GetOrAllocateStaticFieldData负责按需分配(可能抛 OOM)。

五步读取算法:从目标进程内存解析 EnC 字段

文档给出了契约的完整伪代码(C# 风格),cDAC 参考实现位于 RuntimeMutableTypeSystem_1.cs。以下按逻辑拆解为五个算法步骤。

步骤一:枚举类型的新增字段(EnumerateAddedFieldDescs)

流程:

  1. 获取模块指针与模块句柄:通过RuntimeTypeSystem.GetModule(typeHandle)取得模块地址;通过Loader.GetModuleHandleFromModulePtr取得模块句柄。
  2. 快速失败检查(cDAC 实现新增的健壮性检查,文档伪代码中亦体现"没有 EnC 数据则 yield break"):
    • 只有 MethodTable 类型句柄可能有 EnC 新增字段,TypeDesc(TypeVar、FnPtr 等)直接yield break(见 RuntimeMutableTypeSystem_1.cs);
    • 模块必须带ModuleFlags.EditAndContinue标志(L43-L44);
    • Module::EnCClassList字段只在启用FEATURE_METADATA_UPDATER的构建中存在,缺省则不可能有 EnC 数据(L48-L50)。
  3. 线性搜索 EnCEEClassData 条目:遍历UnorderedArrayBaseCount个元素、Table为基址),读取每个entryMethodTable字段,与typeHandle.Address比对,找到匹配项。
  4. 沿链表产出字段描述符:按staticFields选择AddedStaticFieldsAddedInstanceFields头节点,循环读取EnCAddedFieldElement::FieldDesc(产出的是内嵌 EnCFieldDesc 的地址)并沿Next前进,直到 NULL。

注意:此处迭代的是每个类型只维护一份的字段描述符链表(EnCEEClassData 层面),与后面实例层面挂在 SyncBlock 上的EnCAddedField链表是两套结构,不要混淆。

步骤二:判定字段是否为 EnC 新增(IsFieldDescEnCNew)

uint DWord2 = target.Read<uint>(fieldDescPointer + FieldDesc::DWord2); uint offset = DWord2 & FieldDescFlags2.OffsetMask; // 0x07ffffff return offset == target.ReadGlobal<uint>("FieldOffsetNewEnc");

读取 FieldDesc 的DWord2,用 27 位掩码取出 offset,与全局哨兵值FieldOffsetNewEnc(=FIELD_OFFSET_NEW_ENC)比较。这一判定在运行时侧有对应实现FieldDesc::IsEnCNew()(src/coreclr/vm/field.h),返回m_dwOffset == FIELD_OFFSET_NEW_ENC

步骤三:判断是否需要 Fixup(DoesEnCFieldDescNeedFixup)

int needsFixup = target.Read<int>(encFieldDescPointer + EnCFieldDesc::NeedsFixup); return needsFixup != 0;

对应运行时EnCFieldDesc::NeedsFixup()(返回m_bNeedsFixup)。一个 EnC 字段在添加时先用Init做最小初始化(m_bNeedsFixup = TRUE),待进程恢复执行后才由Fixup完成全部初始化并清除标志。

步骤四:解析静态字段存储地址(GetEnCStaticFieldDataAddress)

TargetPointer staticFieldData = target.ReadPointer(encFieldDescPointer + EnCFieldDesc::StaticFieldData); if (staticFieldData == TargetPointer.Null) return TargetPointer.Null; // 存储尚未分配 TargetPointer fieldDataAddress = staticFieldData + EnCAddedStaticField::FieldData; CorElementType fieldType = target.Contracts.RuntimeTypeSystem.GetFieldDescType(encFieldDescPointer); return fieldType is ValueType or Class ? target.ReadPointer(fieldDataAddress) // 引用类型/值类型:再解引用一次 : fieldDataAddress; // 基本类型:内联值起始地址

要点:EnCAddedStaticField::FieldData对于基本类型直接是值的位置(返回其地址即可读值);对于 class 与值类型则是"指向 GC 跟踪存储的指针",需要再解引用一层才能得到对象地址。这正对应 encee.h 中对m_FieldData的注释:"For primitive types, this is the beginning of the actual value. For reference types and user-defined value types, it's the beginning of a pointer to the object."

步骤五:解析实例字段存储地址(GetEnCInstanceFieldAddress)

这是最复杂的算法,完整流程如下:

  1. 取 SyncBlockObject.GetSyncBlockAddress(objectAddress)从对象头获取 SyncBlock 地址;为 NULL 则对象没有 SyncBlock,直接返回 NULL(不可能有 EnC 字段)。
  2. 读 EnCInfoSyncBlock::EnCInfo是可选字段,为 NULL 说明该对象没有 EnC 新增字段。
  3. 遍历 EnCAddedField 链表:从EnCSyncBlockInfo::List头开始,逐个比对EnCAddedField::FieldDesc是否等于目标encFieldDescPointer;找不到则返回 NULL。
  4. 通过 dependent handle 取 helper 对象FieldData是 dependent handle,先ReadObjectHandle得到句柄地址,再通过GC.GetHandleExtraInfo(handleAddress)取出 secondary OBJECTREF——即System.Diagnostics.EditAndContinueHelper实例的地址。
  5. 读取 _objectReference 槽位:helper 对象上唯一的字段_objectReference(src/coreclr/System.Private.CoreLib/src/System/Diagnostics/EditAndContinueHelper.cs 中的private object? _objectReference)持有字段的实际负载,读取该地址得到fieldObject
  6. 按字段类型换算最终地址(与运行时EnCSyncBlockInfo::GetEnCFieldAddrFromHelperFieldDesc一一对应,见 src/coreclr/vm/encee.cpp):
    • 值类型(ValueType):以装箱对象存储((*pOR)->UnBox()),返回fieldObject + Object::Data(unbox 后数据区起始);
    • 引用类型(Class):OBJECTREF 槽位本身就是字段值位置,返回_objectReference地址;
    • 其他(基本类型):存储在 1 元素数组中(运行时侧是I1ARRAYREF),通过Object.GetArrayData返回首元素地址。

这套按类型分派的内存布局正是 EnC 字段值存储的完整真相:值类型装箱、引用类型直接存引用、基本类型包在单元素数组中

运行时侧实现佐证:EditAndContinueModule 的 Resolve 路径

在运行时侧,字段值解析的入口是EditAndContinueModule(src/coreclr/vm/encee.h),它是Module的 EnC 特化子类,IsEditAndContinueCapable()返回 TRUE,并记录m_applyChangesCount(版本号,等于 ApplyChanges 被调用的次数)。

  • ResolveField(thisPointer, pFD)(src/coreclr/vm/encee.cpp):从对象的 SyncBlock 取EnCSyncBlockInfo,调用其ResolveField(L1336)——沿链表查找匹配m_pFieldDesc == pFD的条目,通过 dependent handle 的 secondary 取 helper 对象(GetDependentHandleSecondary/IGCHandleManager::GetDependentHandleSecondary),再调用GetEnCFieldAddrFromHelperFieldDesc完成按类型分派的地址换算;找不到条目返回 NULL。
  • ResolveOrAllocateField(L1020):解析失败(字段值尚未创建)时按需分配EnCAddedField并挂到链表上,仅在 OOM 时失败。
  • EnCFieldDesc::GetAddress(void *o)(L1148)则是字段访问器(JIT 编译出的 EnC 字段访问代码)最终调用的入口:pModule->ResolveOrAllocateField(ObjectToOBJECTREF((Object *)o), this)

调试器契约侧与运行时侧的分工非常清晰:运行时侧负责"写"(创建/分配/挂链表),cDAC 契约侧负责"只读地还原",两侧通过cdac_data<T>模板导出的offsetof偏移(如 src/coreclr/vm/encee.h)与 datadescriptor.inc 中的CDAC_GLOBAL保持布局一致。

全局变量与数据描述符速查

契约文档给出了完整的全局变量与数据描述符清单,整理如下便于查阅:

全局变量(Global variables)

全局类型含义
FieldOffsetNewEncuint32哨兵偏移值,存于FieldDesc::DWord2,标记尚未分配存储的 EnC 新增字段(源码出处:field.h → datadescriptor.inc)

数据描述符汇总(Data descriptors)

描述符字段类型含义
EnCAddedFieldFieldDataObjectHandledependent handle 对:primary 为依赖锚点,secondary 为System.Diagnostics.EditAndContinueHelper实例
EnCAddedFieldFieldDescpointer指向该新增实例字段的EnCFieldDesc
EnCAddedFieldNextpointer指向 SyncBlock 上链表的下一个条目
EnCAddedFieldElementFieldDescpointer内嵌 EnCFieldDesc(与 FieldDesc 布局兼容)的地址
EnCAddedFieldElementNextpointer链表下一节点
EnCAddedStaticFieldFieldDatapointer基本类型字段的内联存储地址,或 class/值类型字段的 GC 跟踪存储指针地址
EnCEEClassDataAddedInstanceFieldspointer新增实例字段链表头
EnCEEClassDataAddedStaticFieldspointer新增静态字段链表头
EnCEEClassDataMethodTablepointer本条目承载 EnC 数据的 MethodTable 指针
EnCFieldDescNeedsFixupint32非零表示仍需 fixup(尚未完全初始化)
EnCFieldDescStaticFieldDatapointer支撑该静态字段的EnCAddedStaticField指针(分配存储前为 NULL)
EnCSyncBlockInfoListpointer与对象关联的 EnC 新增实例字段链表头
FieldDescDWord2uint32打包的标志与偏移字;FieldOffsetNewEnc哨兵标识尚无存储的 EnC 新增字段
ModuleEnCClassListpointerEnC 新增类列表指针
Object(type size)uint32固定 Object 头部(至 MethodTable 指针)字节数
SyncBlockEnCInfopointer对象的 EnC 新增字段信息;未配置 EnC 时可选
System.Diagnostics.EditAndContinueHelper_objectReferencepointerEnC 新增实例字段的每字段存储
UnorderedArrayBaseCountuint32数组当前有效条目数
UnorderedArrayBaseTablepointer数组后备存储指针

其中System.Diagnostics.EditAndContinueHelper在 cDAC 侧对应 EditAndContinueHelperObject.cs,其ObjectReferenceAddress通过[FieldAddress("_objectReference")]属性直接映射到托管字段。

版本演进与扩展阅读

  • 契约当前版本为Version 1"c1")。文档中的契约使用清单由BEGIN/END GENERATED: usage contract=RuntimeMutableTypeSystem version=c1标记自动生成,确保文档与数据描述符、全局变量注册保持同步。类似的生成式契约文档还有 RuntimeTypeSystem.md(静态类型系统)、SyncBlock.md(SyncBlock 与锁状态,其中SyncBlock::EnCInfo字段的完整上下文在此)、Object.md(对象头与GetSyncBlockAddress/GetArrayData的契约定义)。
  • 数据契约体系的整体设计见 datacontracts_design.md 与 contract-descriptor.md;cdac_data<T>模板与全局变量注册机制分别在 encee.h 与 datadescriptor.inc 中。
  • 若要观察运行时如何"应用"一次 EnC 编辑(含新增字段),可追踪EditAndContinueModule::ApplyEditAndContinueAddField(mdFieldDef token)(encee.h)的调用链。

小结

RuntimeMutableTypeSystem契约回答了一个关键问题:EnC/Hot Reload 动态添加的字段到底存在哪里、如何从进程外读取。答案可以浓缩为三句话:

  1. 字段描述符层面:每个 EnC 新增字段有一个EnCFieldDesc,按类型挂在EnCEEClassData的实例/静态两条链表中,通过FieldDesc::DWord2中的FieldOffsetNewEnc哨兵偏移与普通字段区分;
  2. 静态字段值层面:存储在EnCAddedStaticField的变长FieldData区域,按字段类型决定是否需要再解引用一层;
  3. 实例字段值层面:每个对象实例在其 SyncBlock 上懒加载EnCSyncBlockInfo链表,节点通过 dependent handle 关联到EditAndContinueHelper托管对象,最终按"值类型装箱 / 引用类型直接存引用 / 基本类型单元素数组"三种策略定位真实存储地址。

掌握了这套布局与算法,再结合 RuntimeMutableTypeSystem_1.cs 的参考实现,读者便能在自己的诊断工具或调试器扩展中,正确地枚举和读取 EnC 新增字段——这正是调试器在"编辑并继续"场景下保持字段视图、表达式求值一致性的底层保障。

【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 12:18:22

Excel列宽与像素映射原理及跨平台实测方法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 12:16:54

辽宁专升本计算机真题PDF的结构化解析与知识图谱构建

简介&#xff1a;本资源为2022年辽宁专升本考试《计算机基础》科目真题试卷及权威参考答案&#xff0c;面向备考专升本的高职高专学生及计算机基础教学辅导教师&#xff0c;精准覆盖升学选拔核心考点与能力要求。试卷结构完整&#xff0c;含单选题&#xff08;20题&#xff09;…

作者头像 李华
网站建设 2026/9/18 12:15:20

CC Switch 指向 TaoToken:Kimi K2.7 Code 与 MiniMax M3 切换预设怎么存

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/18 12:14:35

做 WorkBuddy 的 Skill 自动化,TaoToken 只提供 Key

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华