ET框架 MongoHelper 漏注册 Entity 子类?用 TaoToken 接 Codex 对着静态构造函数查
ET 框架 Server 端的 MongoHelper 静态构造函数一旦把异常吞成一行11111111111111111漏注册的 Entity 子类就只能靠猜。用 TaoToken 接 Codex 对着代码逐条核对会快很多先打开 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 注册并创建一把 API Key再把 Base URL 填成 https://taotoken.net/api剩下的事交给对话去对齐。服务端启动时Main 里那句看起来什么都没干的MongoHelper.init()会触发整个静态构造函数注册 IgnoreExtraElementsConvention、遍历typeof(Game).Assembly.GetTypes()、用IsSubclassOf(typeof(Entity))过滤并跳过IsGenericType、逐个调BsonClassMap.LookupClassMap(type)最后按#if SERVER处理 Vector3 / Vector4 / Vector2Int 的 StructBsonSerialize。这条链路只要有一环没覆盖到某个 Entity 子类就不会进 BsonClassMap存进 Mongo 时字段静默丢失而日志里你只能看到一行数字。下面这套流程是把“猜”换成“对着代码核”。1. 11111111111111111 这条日志为什么定位不出漏的是哪个类1.1 Main 里那句空的 MongoHelper.init() 到底触发了什么很多人第一次读到MongoHelper.init()会以为它是个空方法实际上它是 C# 静态构造函数的一个触发开关。只要第一次访问 MongoHelper 的任何静态成员CLR 就会保证静态构造函数先跑完且只跑一次。所以 ET 把这套注册逻辑塞进静态构造函数靠 Main 里一次调用把它“点火”。这套设计的代价是注册阶段的所有异常都被压在静态构造里任何一处抛错你都看不到完整的调用现场。如果静态构造函数本身抛异常CLR 会把它包成 TypeInitializationException之后再访问 MongoHelper 的任何成员都会持续失败报错信息还停留在这层包装上。你在 Main 里改一行、重启一次服务、翻一遍日志来来回回就是找不到漏的那个类型。1.2 catch 吞异常之后排查缺口在哪原始写法把BsonClassMap.LookupClassMap(type)放在 try 里catch 只做一件事Log.Error(11111111111111111)。这一行唯一的价值是告诉你“扫描循环里出过错”但它丢掉了三样关键信息出错的是哪个类型、这个类型来自哪个程序集、异常的真实类型和堆栈。更要命的是它不区分“跳过”和“失败”。IsGenericType为真直接 continue这是有意跳过不会打日志LookupClassMap抛异常走 catch会打日志但只打一串数字。于是你在日志里看到的报错既可能是真漏了也可能是某个本来该跳过的类型引发的误报靠这一行数字根本分不清。把这个循环整段贴给 Codex让它先帮你把“有意跳过”和“异常吞掉”拆成两条独立路径是整件事的起点。2. 把 MongoHelper 静态构造函数交给 Codex 前先接好通道2.1 在 TaoToken 创建 Key 并确认模型 IDCodex 需要一个能长期稳定调用的 API 通道。打开 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 注册账号进控制台创建一把 Key复制出来先别急着粘进配置文件——后面所有示例里它都是占位符YOUR_API_KEY真实 Key 只存在你自己机器的环境变量里。模型 ID 不要凭记忆写。进 TaoToken 模型广场 看当天的列表把适合长代码上下文的那一个抄下来配置里写YOUR_MODEL_ID占位。ET 这套注册逻辑牵扯 BsonClassMap、ConventionRegistry 和程序集反射上下文长度比“回答快不快”重要得多选模型时优先看上下文窗口而不是单价。2.2 ~/.codex/config.toml 里把 base_url 指向 https://taotoken.net/apiCodex 的供应商配置写在~/.codex/config.toml里和 Claude Code 的环境变量那套完全不是一回事不要互相套用。一个可用的写法是这样# ~/.codex/config.toml model YOUR_MODEL_ID model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY wire_api chatKey 通过环境变量注入不要写死在 toml 里export TAOTOKEN_API_KEYYOUR_API_KEY注意base_url结尾不要带/v1填的就是https://taotoken.net/api。Codex 自己会拼接后续路径你多写一层反而会 404。配置改完重启一次 Codex让它重新读文件。2.3 一次对话要带上的四份材料光把静态构造函数贴过去Codex 只能给你一堆“可能原因”。要让它给出可编译的结论一次至少给四样东西MongoHelper 静态构造函数的完整原文从 ConventionRegistry.Register 那行到#if SERVER结尾中间不要省略Entity基类的定义位置以及typeof(Game)这个类型本身定义在哪个.csproj/ 程序集真实的报错文本或堆栈包括那行11111111111111111前后各 20 行日志一个具体案例哪个 Entity 子类存进去之后字段丢了字段名是什么Mongo 里实际存成了什么。第四项最容易被忽略。没有具体案例Codex 会往“通用反射问题”上跑给了具体类型名它就能反过来推这个类型为什么没被扫到。3. 对着 typeof(Game).Assembly.GetTypes() 核对扫描范围3.1 Entity 子类可能根本不在 Game 所在程序集里这是 ET 项目里最常见的一个坑。typeof(Game).Assembly只返回 Game 这一个类型所属的那一个程序集。ET 的典型结构是 Model / Hotfix / Server 多程序集Entity 的具体子类很可能写在 Hotfix 程序集里而 Game 这个类型恰好在 Model 程序集里。扫描范围从一开始就少了一半剩下的代码写得再对也没用。把这段判断交给 Codex 时直接问“如果某个 Entity 子类定义在 Hotfix 程序集而 Game 定义在 Model 程序集typeof(Game).Assembly.GetTypes()能扫到它吗”它通常会立刻给你一个跨程序集的替代写法var candidates AppDomain.CurrentDomain.GetAssemblies() .SelectMany(a { try { return a.GetTypes(); } catch (ReflectionTypeLoadException ex) { return ex.Types.Where(t t ! null); } }) .Where(t t ! null t.IsClass !t.IsAbstract) .Where(t t ! typeof(Entity) typeof(Entity).IsAssignableFrom(t)) .ToList();这里有两个细节值得让 Codex 展开讲一是ReflectionTypeLoadException必须显式捕获否则一个程序集里有一个类型加载失败整个GetTypes()都会抛而不是返回部分结果二是IsAssignableFrom和IsSubclassOf不等价前者会把 Entity 自身也算进去所以要多加一句t ! typeof(Entity)。3.2 判断条件到底该用 IsSubclassOf 还是 IsAssignableFrom原始代码用的是type.IsSubclassOf(typeof(Entity))。这个 API 的行为是只有当 type 是 Entity 的直接或间接派生类时才返回 truetype 本身等于 Entity 时返回 false接口实现也不计入。放在这个循环里它其实是对的因为 Entity 自身不需要注册 ClassMap。问题出在候选集变化之后。如果你把扫描范围扩到全程序集就可能扫到Entity本身、扫到IEntity这类接口、扫到EntitySystem这类虽然名字像但和序列化无关的类型。这时候就得把过滤条件写得更明确。把两种写法各自的边界列给 Codex让它给出一个带注释的判断函数比直接改一行靠谱得多。4. IsGenericType 与 LookupClassMap 两条路径分开查4.1 泛型实体被 continue 掉是有意还是漏了if (type.IsGenericType) continue;这一行会让所有泛型 Entity 子类直接出局。开放泛型确实不能直接注册 ClassMapMongoDB 驱动也没法为一个未绑定类型参数的类生成序列化器但闭合泛型是可以的。如果你项目里有MyComponentT这类结构并且实际用到的是MyComponentint那么“跳过泛型”就等于把它的序列化也没收了。让 Codex 帮你区分这几个概念IsGenericType、IsGenericTypeDefinition、IsConstructedGenericType。开放泛型定义应当跳过闭合泛型可以考虑注册但要注意闭合泛型的 ClassMap 是按具体类型参数分别注册的注册成本会随类型参数组合增长。先确认你项目里到底有没有泛型 Entity 在使用再决定要不要补这一段。4.2 LookupClassMap 抛异常被 catch 吞掉之后BsonClassMap.LookupClassMap(type)在第一次遇到某个类型时会触发它的 AutoMap把公开属性映射成 BsonMemberMap。这一步抛异常的常见原因有某个属性类型本身没有对应的序列化器属性上有冲突的 Bson 特性类里有循环引用导致 AutoMap 递归过深以及上面提到的泛型问题。排查顺序建议是先把这个 catch 改成带上下文的日志再回头判断哪些是“真的漏了”catch (Exception e) { Log.Error($[MongoHelper] LookupClassMap failed: {type.FullName}, $assembly{type.Assembly.GetName().Name}, $generic{type.IsGenericType}, abstract{type.IsAbstract}, ex{e}); }这一版日志一跑你就能看到漏掉的类型名和真实异常。把这段输出整段贴回 Codex 的对话里让它对照原代码逐条判断这个类型本该被注册吗它是被扫描范围漏掉还是被过滤条件挡掉还是 AutoMap 阶段自己抛的4.3 IgnoreExtraElementsConvention 的注册顺序和影响面ConventionRegistry.Register注册的包会影响之后所有LookupClassMap的 AutoMap 行为。IgnoreExtraElementsConvention(true) 的作用是当 Bson 文档里出现了类上没有的字段时反序列化不报错、直接忽略。这个约定本身没问题但它必须注册在所有 ClassMap 生成之前。原始代码把它放在静态构造函数最前面顺序是对的。但你如果为了排查临时把注册挪到循环之后就会出现前面几个类按旧约定建了 ClassMap、后面几个类按新约定的分裂状态症状会变得非常难解释。让 Codex 帮你确认一件事就够了你现在改的这版代码里ConventionRegistry.Register是不是仍然在所有LookupClassMap调用之前执行。顺便问一句“这个约定对已经生成的 ClassMap 有没有补救作用”答案通常是没有。5. 让 Codex 给出可编译的补丁你在本地跑再贴回结果5.1 打印已注册清单做一次差集改了过滤条件之后别只看日志有没有报错直接对比两个集合更直观一边是“按新规则应该被注册的类型”一边是“BsonClassMap 里实际已注册的类型”。var shouldRegister typeof(Game).Assembly.GetTypes() .Where(t t.IsClass !t.IsAbstract !t.IsGenericType) .Where(t t.IsSubclassOf(typeof(Entity))) .Select(t t.FullName) .ToList(); var registered BsonClassMap.GetRegisteredClassMaps() .Select(m m.ClassType.FullName) .ToHashSet(); foreach (var name in shouldRegister) { Log.Info($[MongoHelper] {(registered.Contains(name) ? OK : MISS)} {name}); }差集里那些标着 MISS 的类型就是你的排查清单。注意这段代码要在所有注册逻辑跑完之后执行可以临时挂在MongoHelper.init()末尾。5.2 补丁由 Codex 写编译和运行必须你本地做这里要划一条线Codex 能做的是读你的静态构造函数、给出一份改好的MongoHelper.cs、解释每一处改动的理由。它不能替你启动服务端、不能替你连生产 Mongo、也不能替你在服务器上跑真实的反序列化验证。正确做法是让 Codex 输出完整方法体你在本地 IDE 里替换、编译、启动一个测试进程把控制台里那段注册清单和LookupClassMap failed日志原样贴回对话再让它判断下一步。如果你把“直接帮我连上数据库看看”这种指令丢给它得到的只会是看起来像那么回事、但你没有验证过的建议。整个排查循环应该是Codex 出补丁 → 你本地编译 → 你本地跑 → 你贴回报错 → Codex 修正。一轮可能两三次但每次都有可验证的输入输出。5.3 #if SERVER 下 StructBsonSerialize 的核对点循环结束之后那段#if SERVER分支负责给 Vector3 / Vector4 / Vector2Int 注册 StructBsonSerialize 相关的序列化配置。这段代码只在 SERVER 宏定义存在时编译进去如果你的构建配置没打开这个宏它在本地调试时会整段消失而线上又正常——这种差异会让人怀疑前面的循环其实是宏的问题。排查时可以问 Codex 两个问题当前 csproj 里DefineConstants有没有包含SERVER这几个结构体的 ClassMap 注册和前面 Entity 循环之间有没有顺序依赖。StructBsonSerialize 一般只影响这些值类型的存储形式和 Entity 的 ClassMap 是两条线但如果顺序反了个别结构体字段的表现会不一样。6. 跑通之后回控制台对一下这次调用6.1 先用模型对话做一次最小验证配置刚改完最省事的验证不是立刻开一个 ET 的完整工程而是用同一把 Key 在 TaoToken 模型对话 里发一条消息确认模型 ID 和 Base URL 这一对没填错。这一步能过的意思很明确Key 有效、通道通、模型 ID 是模型广场里真实存在的那个。如果这一步就失败别急着怀疑 ET 的代码先回 控制台 API Keys 看这把 Key 的状态和调用记录。Key 本身有没有额度、是不是复制时多了空格、base_url是不是手滑写成了https://taotoken.net/api/v1这几项对一遍基本就能定位。6.2 长期拿 Codex 查 ET 的序列化问题怎么安排更顺这类排查不是一次性的。ET 项目里 Entity 子类会随功能不断增加每次新增一个带特殊字段类型的组件就有可能再撞一次 AutoMap 失败。与其每次从日志猜起不如把这套流程固定下来静态构造函数原文、Entity 基类位置、注册清单差集打印这三样存成对话模板新问题来了直接换报错重新跑一遍。如果这类对话量比较大Coding Plan 的按周期计费会比按次调用好算账服务端日常的代码排查如果也想接到 Claude Code 上环境变量和 settings.json 的写法在 Claude Code 接入文档 里有完整对照。配好之后你会发现真正省下来的时间不是“AI 帮你写代码”而是那句只有11111111111111111的日志终于能换成一行带类型名和堆栈的可读报错。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →