| name | mod-workflow |
| description | 网易基岩版 MOD Python 开发强制工作流。未知项目必须先 scan_py2_mod_architecture 建立认知;改动已有模块必须先 scan_py2_import_chain 分析引用链;禁止单文件堆积;禁止跨端侧 import;事件监听优先注册表分发避免重复注册;mcmodsdk 环境默认按 str 字节串处理,非必要不转 unicode;游戏业务非必要不 try 且不吞异常;关键决策点必须确认用户意图。 |
MOD 开发工作流约束
本规范是强制性工作流,适用于网易基岩版 MC MOD 的 Python 业务开发。以下所有"必须"均为硬性门控,未通过则不得进入下一阶段。
阶段零:项目认知门控
触发条件:首次接触一个 MOD 项目,或对项目结构不确定时。
硬性规则:在写出任何业务代码之前,必须完成以下步骤,不得跳过。
- 调用
scan_py2_mod_architecture 扫描行为包根目录
- 参数建议:
ignore_third_party_analysis=true(聚焦业务,避免框架噪音)
- 必须从输出中确认:入口模块位置、模块分层、核心子系统、依赖链
- 阅读架构输出,识别当前任务涉及的子系统和模块边界
- 如果任务涉及特定子系统,对该子系统目录追加调用
scan_py2_import_chain 做定点深挖
禁止:
- 禁止在未完成架构扫描的情况下创建新文件或修改已有文件
- 禁止凭猜测判断模块职责或依赖关系
通过条件:能明确回答"这个项目有哪些子系统、当前任务应该改哪些模块、这些模块之间的依赖关系是什么"。
阶段一:改动前引用链分析
触发条件:需要修改、重构、拆分、合并任何已有模块时。
硬性规则:动刀之前,必须对目标文件/目录调用 scan_py2_import_chain。
- 从输出中提取:
- 直接引用方(谁 import 了它)
- 间接依赖方(谁通过其他模块间接依赖它)
- 它的内部依赖(它 import 了谁)
- 它在架构中的层级位置
- 根据引用链列出所有受影响的文件清单
- 制定改动方案时,方案中必须包含对每个受影响文件的处理说明
禁止:
- 禁止在未分析引用链的情况下重命名、移动、删除或拆分模块
- 禁止改动一个模块后不检查其引用方是否需要同步更新
通过条件:改动方案中已覆盖所有引用方的处理方式(无需修改 / 需同步改接口 / 需更新 import 路径)。
阶段二:代码编写约束
通过阶段零和阶段一后,进入编码阶段。以下规则在整个编码过程中持续生效。
2.1 客户端/服务端 import 边界约束
MOD 业务分为服务端(Server)和客户端(Client)两侧,Python 的 import 具有初始化语义,不只是取符号。
硬性规则:
- 不得在客户端模块中 import 服务端业务模块,反之亦然
- 不得在
__init__.py 中混合导入客户端和服务端业务模块——__init__.py 包内导入必须严格确认目标模块属于哪一侧
- 不得在错误线程上初始化对应侧的对象(PyModSdk 基于多线程 GIL 轮换,客户端与服务端对象不是独立副本)
例外——跳板封装库:
以下导入本身是合法的,因为它们只在实际调用内部接口时才进入对应侧初始化逻辑:
import mod.server.extraServerApi as serverApi
import mod.client.extraClientApi as clientApi
但前提是:不得在错误线程或错误侧继续调用其内部能力。仅导入无影响,调用才触发初始化。
决策规则:
- 当你准备写
import 时,先确认:
- 当前文件运行在哪一侧(客户端 / 服务端)?
- 被导入的模块属于哪一侧?
- 两者是否一致? → 不一致则禁止导入(跳板封装库除外)
- 当你编辑
__init__.py 时,逐行检查每个导入目标的归属侧
禁止:
- 禁止客户端文件 import 服务端业务模块(跳板封装库除外)
- 禁止服务端文件 import 客户端业务模块(跳板封装库除外)
- 禁止在
__init__.py 中混合导入两侧业务模块
- 禁止为了"方便共用"把客户端和服务端逻辑写在同一个文件中
2.2 项目架构与命名风格约束
硬性规则:
- 优先以当前用户项目已有的架构风格和命名风格进行开发和维护
- 不得擅自引入与现有结构冲突的新组织模式或命名约定
- 仅在用户明确要求重构时才可切换架构风格
决策规则:
- 阶段零扫描完成后,从架构输出和已有代码中识别项目采用的:
- 目录组织风格(按功能板块 / 按端侧 / 其他)
- 命名约定(文件名、类名、函数名、变量名的风格)
- 模块划分方式
- 新增代码必须遵循已识别的全部风格,保持一致性
- 如果项目无明确风格(如新项目),可参考以下两种推荐风格,根据项目规模和需求选择
命名风格约束:
- 阶段零完成后,必须从已有代码中识别项目的命名风格并遵循
- 常见风格包括但不限于:大驼峰(
GetPlayerPos)、小驼峰(getPlayerPos)、下划线(get_player_pos)
- 不同项目可能混用多种风格(如类名大驼峰、函数名下划线),以项目实际为准
- 示例代码中的命名仅为展示逻辑,实际编码时必须适配项目风格
推荐风格 A——按功能板块划分,端侧文件平铺:
<Feature>/
<业务板块>/
Server.py # 服务端逻辑
Client.py # 客户端逻辑
Utils.py # 该板块的工具函数
Config.py # 该板块的配置/常量
适合:功能模块较多、各板块相对独立的中大型项目。
推荐风格 B——按端侧划分,业务文件内聚:
ServerSide/
__init__.py # 加载服务端全部业务
xxx.py
yyy.py
ClientSide/
__init__.py # 加载客户端全部业务
xxx.py
yyy.py
适合:端侧职责清晰、业务量适中的项目。由 __init__.py 直接加载该端侧全部业务模块。
注意:以上目录结构和文件命名仅为推荐参考,具体情况应具体分析。已有项目的风格优先级始终高于推荐风格。所有示例中的命名(包括文件名、函数名)均为示意,实际开发必须以项目已有命名风格为准。
2.3 文件职责约束
硬性规则:
- 每个文件必须有单一明确的职责,不得将不相关的功能塞入同一文件
- 新增功能时,如果与目标文件的现有职责不同,必须新建文件
- 公共工具函数、常量、配置必须抽离到专用模块,不得内联在业务文件中
- 新增子系统必须建立独立目录
决策规则:
- 当你准备向一个文件添加代码时,先判断:这段代码的职责是否与该文件现有内容一致?
- 一致 → 写入
- 不一致 → 新建文件,放到职责匹配的位置
- 当你发现一个文件已经承担了多个不相关职责 → 在改动方案中标注"建议拆分",但当前任务只做必要改动,不擅自大规模重构
禁止:
- 禁止在单个文件中堆积超过两个不相关的功能模块
- 禁止为了"方便"把所有新增逻辑都追加到一个已有文件末尾
2.4 代码结构约束
硬性规则:
- 使用早返回(guard clause)模式避免嵌套地狱:当条件不满足时尽早
return,而非层层嵌套 if
- 函数嵌套层级不应超过 3 层,超过时必须通过早返回或拆分子函数来降低复杂度
决策规则:
- 当你准备写
if xxx: 且内部有大段逻辑时,考虑是否可以反转为 if not xxx: return
- 多个前置条件检查应逐个早返回,而非嵌套
反例:
def OnPlayerAttack(args):
playerId = args.get("playerId")
if playerId:
targetId = args.get("targetId")
if targetId:
comp = CreateComponent(playerId, "Minecraft", "attack")
if comp:
damage = comp.GetAttr()
if damage is not None:
ApplyDamage(targetId, damage)
推荐:
def OnPlayerAttack(args):
playerId = args.get("playerId")
if not playerId:
return
targetId = args.get("targetId")
if not targetId:
return
comp = CreateComponent(playerId, "Minecraft", "attack")
if not comp:
return
damage = comp.GetAttr()
if damage is None:
return
ApplyDamage(targetId, damage)
2.5 事件监听约束
事件监听(ListenForEvent / UnListenForEvent 或框架等价物)是系统级行为,每次注册都会增加引擎调度开销。
核心原则:同一个系统事件应尽量只注册一次监听,在内部通过注册表按业务 key 分发到对应的 handler,而非让多个业务板块各自注册同一事件。
硬性规则:
- 当多个业务板块需要响应同一事件时,必须优先考虑注册表分发架构,而非每个板块各自注册一次该事件
- 事件监听的注册和注销必须成对出现,生命周期必须明确
- 不得在可能多次执行的代码路径(循环、条件分支、动态创建)中注册事件监听,除非有防重复机制
决策规则:
- 当你准备为某个事件写
ListenForEvent | Listen 时,先检查:
- 项目中是否已有该事件的监听? → 有 → 不要再注册一次,应该把新的处理逻辑挂到已有的分发机制上
- 项目中是否已有事件注册表或统一分发入口? → 有 → 通过已有机制注册 handler,不要绕过
- 没有统一机制且多个板块需要同一事件? → 建议向用户提出建立注册表分发架构的方案
- 注销时机:
- 实体/对象销毁时,必须注销其关联的 handler
- 模块卸载时,必须清理该模块注册的所有 handler
注册表分发设计思路(仅为参考,具体实现应适配项目架构和框架):
_ITEM_USE_HANDLERS = {}
def RegisterItemUseHandler(itemName, handler):
"""业务板块调用此函数注册处理逻辑,而非直接 ListenForEvent"""
handlerSet = _ITEM_USE_HANDLERS.get(itemName)
if handlerSet is None:
handlerSet = set()
_ITEM_USE_HANDLERS[itemName] = handlerSet
handlerSet.add(handler)
def UnregisterItemUseHandler(itemName, handler):
handlerSet = _ITEM_USE_HANDLERS.get(itemName)
if handlerSet is not None:
handlerSet.discard(handler)
def _OnItemUse(args):
"""唯一的事件监听回调,负责按 key 分发"""
import traceback
itemName = args.get("itemName")
handlers = _ITEM_USE_HANDLERS.get(itemName)
if not handlers:
return
for handler in list(handlers):
try:
handler(args)
except Exception:
traceback.print_exc()
注意:分发循环中的 try/except 属于基础设施级别的异常隔离,不违反 2.6 异常处理约束。2.6 约束的是游戏业务代码中用 try 代替显式判断或吞掉异常的行为。分发器作为基础设施,必须保证单个 handler 崩溃不影响其他 handler 执行,且必须保留完整 traceback 日志。
核心思路:系统事件只注册一次 → 回调内部按业务 key(如 itemName、blockName、entityType 等)查注册表 → 分发到对应 handler。具体 key 的选择和注册表结构取决于业务场景,不必拘泥于上述示例。
如果项目使用 QuModLibs 等框架且框架已有事件管理机制,应优先使用框架能力。
优先原则:
- 多个业务板块响应同一事件时,优先考虑注册表分发架构,但不强制——复杂项目中确实存在合理的多处注册同一事件的场景
- 注册表化是推荐的优化方向,不是所有事件都必须注册表化,具体情况具体分析
禁止:
- 禁止在可能多次执行的代码路径中注册事件监听且无防重复机制
- 禁止只注册不注销(除非该监听确实贯穿整个生命周期且不会重复注册)
- 禁止绕过项目已有的事件注册/分发机制
2.6 异常处理约束
硬性规则:
- 游戏业务代码(tick、事件回调、循环、组件调用)中,非必要不得使用
try/except
- 优先使用显式判断(
if x is not None、dict.get()、hasattr())代替异常捕获
- 如确需使用
try,必须同时满足以下全部条件:
- 捕获明确的异常类型(禁止裸
except: 和 except Exception: pass)
- 异常处理块中必须保留错误信息(
traceback.print_exc() 或 logging)
- 该异常确实是不可预期的外部错误(文件IO、网络、第三方库)
决策规则:
- 当你准备写
try 时,先问自己:
- 这个错误能否通过前置判断避免? → 能 → 用
if 判断,不用 try
- 这是热路径(tick/循环/高频事件)吗? → 是 → 除非绝对必要,不用
try
- 如果必须用
try:是否指定了明确异常类型?是否保留了错误日志? → 两者都满足才可以写
禁止:
- 禁止
except: pass 或 except Exception: pass(吞异常)
- 禁止用
try/except KeyError 代替 dict.get()
- 禁止用
try/except AttributeError 代替 hasattr() 或 is not None 检查
- 禁止用
try/except StopIteration 代替 next(it, default) 或 for 循环
2.7 字符串类型约束
mcmodsdk / 网易 Py2 MOD 运行环境中,游戏 API 与业务数据大多以 str 字节串形式传递,而不是 unicode。
硬性规则:
- 非必要场景不得主动把普通业务字符串转为
unicode
- 不得为所有字符串处理添加通用的
unicode 兜底转换
- 只有在确实需要按字符语义处理时,才允许临时转换为
unicode
允许临时转换的典型场景:
- 需要明确获取中文字符数量,而不是字节长度
- 需要按字符进行切片、截断、对齐或逐字遍历
- 需要与明确要求
unicode 的第三方库或接口交互
决策规则:
- 如果只是传递 identifier、事件参数、配置 key、资源路径、命令参数、英文/命名空间 ID,保持
str,不要转换
- 如果只是比较、字典查找、拼接、格式化日志,优先保持
str
- 如果必须按中文字符数处理,转换范围必须尽量小:用完即回到项目原有字符串约定
禁止:
- 禁止把
unicode(value, "utf-8") 或等价转换作为所有字符串入口的默认兜底
- 禁止在没有字符级处理需求时,为了“保险”同时兼容
str / unicode
- 禁止引入大量编码兜底逻辑掩盖真实数据约定
2.8 资料检索约束
硬性规则:
- 编写涉及 ModAPI 接口、事件、枚举、JSON 字段、资源路径的代码前,必须先通过 MCP 工具检索确认
- 不得凭记忆或猜测编写 API 调用、事件名、枚举值
搜索技巧:
- 网易侧工具(
search_api、search_event、search_enum、search_all、search_netease_guide、search_qumod)支持中文模糊匹配
- 单次搜索关键词应精简聚焦,建议一次只查一个概念;塞入过多内容会降低准确性
- 搜索结果不理想时,换关键词或拆分为多次搜索
- 国际版通用内容用
search_wiki、search_bedrock_dev(英文关键词)
- 原版资源文件用
search_game_assets
工具对照表:
| 查什么 | 用什么工具 |
|---|
| ModAPI 接口 | search_api |
| ModAPI 事件 | search_event |
| ModAPI 枚举 | search_enum |
| 以上混合搜索 | search_all |
| 网易独占教程 | search_netease_guide |
| QuModLibs 框架 | search_qumod |
| Bedrock Wiki | search_wiki |
| JSON 组件格式 | search_bedrock_dev |
| 原版资源文件 | search_game_assets |
| 网易版差异 | get_netease_diff |
| 网易 JSON UI 组件库 | get_netease_jsonui |
禁止:
- 禁止在未检索的情况下编写包含 API 名、事件名、枚举值的代码
- 禁止在资料不足时硬猜字段名或参数格式,应明确告知用户"资料中未找到,需要确认"
阶段三:改动后验证
触发条件:完成任何代码编写或修改后。
硬性规则:改完代码后,必须执行以下验证步骤,不得跳过。
- import 链完整性:对修改过的文件重新调用
scan_py2_import_chain,确认:
- 没有引入新的跨端侧 import
- 没有产生循环依赖
- 所有引用方的 import 路径仍然有效(尤其是重命名/移动/拆分后)
- 端侧边界检查:确认新增/修改的文件没有混入错误端侧的导入或初始化
- 事件监听配对:如果新增了事件监听注册,确认有对应的注销逻辑
- 代码风格一致性:确认新增代码的命名风格、目录位置与项目已有风格一致
注意:如果验证发现问题,必须修复后再提交,不得带着已知问题交付。
何时停下来问用户
以下场景中,AI 必须停下来向用户确认,不得自行决定:
- 架构风格不明确:项目中存在多种组织风格,无法判断应该遵循哪一种
- 重构范围超出任务:当前任务的最佳实现方式需要大范围重构已有代码
- 新增子系统的放置位置:项目结构中没有明确的位置适合放置新功能
- 事件监听架构决策:需要建立新的注册表分发机制,或需要改变已有的事件管理方式
- 端侧归属不确定:某个模块或功能无法明确判断属于客户端还是服务端
- 资料库中未找到所需信息:API、事件、字段等在资料库中查不到,需要用户确认
- 破坏性改动:改动可能影响已有功能的行为(如修改公共接口的签名、改变事件参数结构)
原则:宁可多问一次,不可做错一步。错误的架构决策比多一轮对话的成本高得多。
工作流总览
收到任务
│
├─ 1. 项目是否已知?
│ ├─ 否 → 【阶段零】scan_py2_mod_architecture → 建立认知 → 识别架构风格
│ └─ 是 → 继续
│
├─ 2. 是否涉及已有模块改动?
│ ├─ 是 → 【阶段一】scan_py2_import_chain → 分析引用链 → 制定方案
│ └─ 否(纯新增)→ 确认新文件放置位置是否符合架构风格
│
├─ 3. 是否涉及 ModAPI / 事件 / 资源?
│ ├─ 是 → 【2.8】先检索资料库
│ └─ 否 → 继续
│
├─ 4. 【阶段二】编码(持续生效的约束)
│ ├─ import 不跨端侧边界 【2.1】
│ ├─ 遵循项目已有架构与命名风格 【2.2】
│ ├─ 每个文件单一职责 【2.3】
│ ├─ 早返回避免嵌套地狱 【2.4】
│ ├─ 事件监听优先注册表分发 【2.5】
│ ├─ 非必要不 try,不吞异常 【2.6】
│ └─ 非必要不转 unicode,默认保持 str 字节串 【2.7】
│
├─ 5. 【阶段三】改动后验证(用工具确认)
│ ├─ scan_py2_import_chain 确认 import 链完整、无循环依赖
│ ├─ 端侧边界无混入
│ ├─ 事件监听注册/注销配对
│ └─ 代码风格与项目一致
│
└─ 任何阶段遇到不确定的决策 → 停下来问用户,不要自行决定