【免费下载链接】skills
Get production-ready images, video, audio, and 3D from any AI agent: skills that pick the right model, price before spending, and keep characters and brands consistent through the Scenario MCP, plus expert teams that drive Blender, Maya, ZBrush, Unreal, and Unity.
本指南以 scenario-maya-expert 技能中的可靠性参考文档为骨架,系统梳理让 Agent 生成的 Maya Python 代码"正确、可重复执行、不破坏用户场景"的工程约束。读完后你将掌握:cmds 与 OpenMaya 2.0 的选型判据、返回值与查询的真实行为、撤销与主线程的正确用法、11 段可直接改写的标准流程(P1–P11),以及运行前的 lint 规则与运行时的验收闸门。
事实边界:该参考文档明确标注 文档中的 Maya 代码尚未在 Maya 中实际运行,标记
[verify]的条目由探针测试tests/code/maya-expert/test_00_probe.py负责确认,标记[added]的是该工具包自己补充的规则。本文严格保留这一状态说明,不把"未验证"写成"已验证"。
为什么 Agent 生成的 Maya 代码特别容易坏
人类写 Maya 脚本时,靠 Script Editor 的回显、视口里的一次旋转、模型面板里的一次选择来纠错。Agent 没有这些反馈通道,于是三类错误会被放大:依赖选择状态的隐式操作(select 之后再执行命令)、对返回值结构的错误假设(以为 polyCube 返回一个名字)、以及在 GUI 会话里做出无法撤销的写入。
参考文档给出一条贯穿全文的判断标准:先用数字证明,再用眼睛确认。文档引用了 Fragapane 对匹配类工具的标准——把期望矩阵的逆乘以计算矩阵,结果应当等于单位矩阵,而不是"看起来对";也引用了 dcc-mcp showcase 用 0.0 的偏差和最多 1/255 的色差来举证的做法。这套"数字优先"的思路,正是本文所有流程的设计前提。
第一层:25 条紧凑清单
原文档的第 1 节是一份 25 条的 checklist,这里按主题重新组织,但保留全部条目与原始依据。
导入、命名与选择
- 使用
import maya.cmds as cmds;绝不要from maya.cmds import *,否则会遮蔽help之类的内置函数(依据:Maya 2027 Help, Using Python)。 - 每条命令都要显式传入节点参数;不要"先 select 再操作",批处理代码里不要出现
ls(sl=True)。 - 捕获创建类命令的返回值并真正使用它们:
polyCube返回[transform, historyNode];name="crate#"中的#会展开为唯一编号;名字冲突时 Maya 会自动改名。 - 只要两个节点可能共享短名,就使用长名(
long=True、fullPath=True)或 UUID(cmds.ls(n, uuid=True))。 - 在
ls、listRelatives、listConnections、listHistory、polyInfo之后加or []——这些接口在无匹配时可能返回None而非空列表。
第 5 条在仓库源码里有直接对应物。mx_audit.py 的 list_meshes() 通篇使用 cmds.listRelatives(...) or []、cmds.ls(type="mesh", long=True) or [];mx_validate.py 的 run_checks() 里同样写成 cmds.polyUVSet(m, q=True, allUVSets=True) or []。这不是风格偏好,而是让循环不会因为 None 抛 TypeError。参见 mx_audit.py 与 mx_validate.py。
返回值、参数与单位
- 复合
getAttr返回[(x, y, z)],取值要用[0]。 - 固定位置的参数不能被元组打散:应写
cmds.move(x, y, z, node)或cmds.move(*vec, node)。 - 与 Python 关键字同名的 flag 会失去原名(
is短旗标要用internalSet;break长旗标要用b);时间范围是元组;单位是字符串(如"10pal")。 - 在
addAttr之前先判断:objExists(node + ".attr")或attributeQuery(..., exists=True)。 - 按名字做"取或建"(get-or-create),给自己创建的节点打标记,并按标记清理,绝不用可能命中用户节点的通配符。
- 除非任务本身就是修复,否则不要修改或删除自己没创建的节点;必须改动时逐条报告(即
mx_validate的 fix log)。 - GUI 编辑要包在撤销块里,并在
finally中关闭;一个 Agent 步骤对应一个块。Maya 2027 Help 明确警告:误用的撤销块"会让撤销队列处于损坏状态"。
撤销、主线程与桥接
- OpenMaya 2.0 的写入对撤销不可见;在 GUI 会话中要么用 cmds 写入,要么只在拥有
MDGModifier的MPxCommand内部使用 OM2。 - 绝不要在同一个可撤销操作里"用 cmds 创建节点、再用 OM2 删除它":撤销时会试图删除一个已经不存在的节点。
- 用类型检查代替异常:
obj.hasFn(om.MFn.kDagNode);在findPlug之前用MFnDependencyNode.hasAttribute();使用最窄的 function set,它会顺便完成输入校验。 - 使用具名的旋转顺序常量;
MTransformationMatrix的顺序值与MEulerRotation相差一位;MEulerRotation接受弧度。 - OpenMaya 在内部单位下工作(厘米、弧度),cmds 读写的是 UI 单位(用户可能设成米或度)。
MFnMesh在空网格上会抛异常(devkit 2022.1),而polyEvaluate在计无可计时返回的是消息字符串而不是 0,所以if not polyEvaluate(...)不能当守卫用。- 只有主线程能与 Maya 通信:cmds 在其它线程会抛异常,OpenMaya 则有"不可预见的副作用"。
- 通过桥接调用时,绝不使用
input()、pdb、confirmDialog、promptDialog、fileDialog2:它们会打开模态 UI 并阻塞 Maya。 - 在执行
file(new=True)或file(open=True, force=True)之前,必须先确认cmds.file(q=True, modified=True)为False,或已保存副本。 - 打开不可信场景时使用
executeScriptNodes=False;保存到新路径,绝不覆盖源文件。 - UI 回调传可调用对象而不是字符串;回调签名接受
*args;循环中用functools.partial而不是 lambda。 - 每份报告都要记录
cmds.about(version=True)、cmds.about(apiVersion=True)、sys.version、cmds.about(batch=True)。 - 算术优先使用节点网络(2024 的 math 节点、2025 的 matrix 节点)而不是表达式:带
getAttr的表达式会向 Evaluation Manager 隐藏依赖关系;只有没有等价节点的公式才保留表达式。
清单第 18、21、22 三条在仓库里都有可运行的实现,值得逐个展开。
空网格守卫:mx_audit.py 提供了两个被文档称为"工具包与技能代码包装非自建网格的唯一许可方式"的函数。is_empty_mesh(shape) 先用 polyEvaluate 计数;mfn_mesh(shape) 先计数再捕获构造函数的 RuntimeError,形成双重保护。配套的 _count() 处理了 polyEvaluate 返回非整数的情形——凡不是 int 的一律当作 0,bool 也当 0。这正是清单第 18 条的工程落地:
def is_empty_mesh(shape):
c = mesh_counts(shape)
return c["verts"] == 0 or c["faces"] == 0
def mfn_mesh(shape):
import maya.api.OpenMaya as om
if is_empty_mesh(shape):
return None
sl = om.MSelectionList()
sl.add(shape)
try:
return om.MFnMesh(sl.getDagPath(0))
except RuntimeError:
return None
场景替换守卫:清单第 21 条由 mx_bridge.lint() 强制。它用正则匹配 cmds.file 的长短旗标(new/n、open/o、force/f),也匹配 mel.eval("file -f -new;") 与 PyMEL 的 newFile/openFile。测试 test_mx_bridge_lint.py 枚举了 10 种"必须拒绝"的写法与 6 种"必须放行"的写法,包括 cmds.file(save=True, force=True) 这种合法保存,以及 print("file -o is the MEL flag") 这种不应误伤的情形。放行需要显式传 allow_scene_replace=True。
第二层:cmds 还是 OpenMaya 2.0
原文档给出一张决策表,其核心逻辑是"查询用 cmds,海量数据用 OM2,用户可能撤销的编辑用 cmds"。
| 工作内容 | 选用 | 原因 |
|---|---|---|
一次性查询、objExists、ls、listConnections | cmds | Fragapane 指出 cmds 查询常常快于 API 循环,有时甚至快于等价的 C++ |
| 用户可能撤销的实时会话编辑 | 撤销块内的 cmds | OM2 对撤销毫无贡献 |
| 数千个点或组件、拓扑遍历、矩阵 | OM2(MFnMesh.getPoints、getVertices、MItMeshEdge、MDagPath.inclusiveMatrix) | 一次调用代替数千次;"写起来长,执行起来快" |
| 回调 | OM2 的 MMessage 类,回调体要轻,id 要保存并可移除 | 先把回调设计成一张小图 |
| 永不需要撤销的批处理 | OM2 写入没问题 | 无头模式没有可撤销的东西 |
| GUI 中需要可撤销的 OM2 操作 | 带 MDGModifier 的 API 2.0 MPxCommand | Autodesk 的撤销模型:doIt 一次、redoIt、undoIt |
这张表在仓库的 mx_run.py 中有对应取舍:批处理路径直接使用 maya.standalone,不关心撤销;而 mx_bridge_server.py 的 handle() 明确为每次调用开启撤销块,并在 finally 里无条件关闭:
if undo and cmds is not None:
cmds.undoInfo(openChunk=True, chunkName="mx:%s" % job_id)
chunk = True
...
finally:
if chunk:
try:
cmds.undoInfo(closeChunk=True) # always close, an open chunk corrupts undo
except Exception:
pass
注意注释用词——"open chunk corrupts undo",这与清单第 12 条引用 Maya 帮助的措辞一致,说明这不是桥接作者的偏好,而是来自官方文档的风险描述。
第三层:评估后数据与原始数据
这一节是很多 Agent 脚本翻车的地方,原文给出五组要点。
- 形变网格有两个 shape:可见的那个(形变结果)和中间对象
...ShapeOrig(intermediateObject为 1),后者供给变形器链。listRelatives(x, shapes=True, noIntermediate=True)取可见 shape,在它上面用MFnMesh读到的是形变后的点;要读静止位置得读 Orig shape(原文标[verify])。 - Maya 的依赖图是拉取式的:查询一个脏 plug(
getAttr、xform(q=True, ws=True)、MFnMesh.getPoints)本身就会触发求值,setAttr之后不需要任何刷新调用。要读另一帧而不移动时间轴,用cmds.getAttr(plug, time=t);要步进则用cmds.currentTime(t, update=True)(Cached Playback 与脚本步进的交互待验证)。 - 局部 vs 世界:
getAttr(".translate")是局部的;世界值来自xform(q=True, ws=True, ...)(UI 单位)或MDagPath.inclusiveMatrix()(厘米)。 - 历史:
listHistory(shape, pruneDagObjects=True)列出上游节点;变形器是那些nodeType(n, inherited=True)里含geometryFilter的节点。用delete(n, constructionHistory=True)删除建模历史;蒙皮网格上改用bakePartialHistory(n, prePostDeformers=True)(即 Delete Non-Deformer History)。 - 实例与引用:实例是多个 DAG 路径共享一个 shape,用
MDagPath.isInstanced()与listRelatives(shape, allParents=True)判断;被引用节点用referenceQuery(n, isNodeReferenced=True),其编辑记录在父文件的 reference node 中,按名字和路径回放,因此永远不要重命名或重新父化引用文件内部的节点。
上面"历史"那条在仓库里被实现得比文档更细:mx_audit.split_history() 把上游节点分成三组——真正的建模历史、geometryFilter 变形器、以及"只喂给变形器非几何输入"的节点(bind pose、权重驱动曲线、单位转换、表达式)。源码注释记录了一个真实缺陷:曾经把 dagPose 当作历史,导致"每个蒙皮网格看起来都是脏的",这就是 DEFORMER_SUPPORT_TYPES 里出现 "dagPose" 的原因。判断"是否为变形器输入"的规则是:该节点的所有出边都落在变形器的非 input/originalGeometry 属性、另一个变形器输入、或编辑器簿记节点上,并迭代到不动点。
第四层:11 个可复用流程
原文档第 4 节给出 P1–P11,全部标注"尚未在 Maya 中运行"。下面完整保留代码,并标注它们与工具包其它模块的接口关系。
P1. 会话探针
import sys, platform
import maya.cmds as cmds
def session_info():
return {"maya": cmds.about(version=True), "api": cmds.about(apiVersion=True),
"python": sys.version.split()[0], "machine": platform.machine(),
"batch": cmds.about(batch=True), "scene": cmds.file(q=True, sceneName=True),
"modified": cmds.file(q=True, modified=True),
"units": (cmds.currentUnit(q=True, linear=True), cmds.currentUnit(q=True, angle=True),
cmds.currentUnit(q=True, time=True), cmds.upAxis(q=True, axis=True))}
无头版本是 mx_run.session_info()。仓库实现比文档片段多记录了三项:sys.executable(确认跑在哪个 mayapy)、cutIdentifier(区分 2027.0/2027.1/2027.2 的点版本)、以及 batch_env(子进程实际拿到的环境变量快照)。最后一项很实用——文档说 [verify] 尚未确认 "warning" 级别是否也放行 error,所以直接把环境打出来比争论更可靠。
P2. 幂等取或建、打标记、按标记清理 [added]
TAG = "mxAgent"
def get_or_create(node_type, name, parent=None):
"""Long name of our node `name`, created if missing. Refuses a user node of that name."""
if cmds.objExists(name):
n = cmds.ls(name, long=True)[0]
if cmds.attributeQuery(TAG, node=n, exists=True):
return n
raise RuntimeError("%s exists and is not ours: choose another name" % name)
n = cmds.createNode(node_type, name=name, parent=parent) if parent else cmds.createNode(node_type, name=name)
cmds.addAttr(n, longName=TAG, attributeType="bool", defaultValue=True)
return cmds.ls(n, long=True)[0]
def our_nodes():
return [n for n in cmds.ls(long=True) or [] if cmds.attributeQuery(TAG, node=n, exists=True)]
两个容易漏掉的点:创建 shape 类型节点而不给 parent 时,Maya 会额外建一个 transform,两个都要打标记;除了自定义属性,也可以用命名空间(mx_review 就是在 mxReview 命名空间里建东西,最后连同内容一起删掉)或 object set 作为替代方案。
P3. 撤销块
from contextlib import contextmanager
@contextmanager
def undo_chunk(name="agent"):
cmds.undoInfo(openChunk=True, chunkName=name)
try:
yield
finally:
cmds.undoInfo(closeChunk=True) # always close
桥接服务器已经为每次调用包了一个块,名字是 mx:<job id>。这意味着 Agent 侧不必重复包裹——重复包裹会让撤销层级变深,用户一次 Ctrl+Z 撤不干净。
P4. 以撤销往返作为验收测试
def undo_round_trip(fn, *a, **kw):
before = sorted(cmds.ls(long=True))
with undo_chunk("probe"):
fn(*a, **kw)
after = sorted(cmds.ls(long=True))
cmds.undo()
undone = sorted(cmds.ls(long=True)) == before
cmds.redo()
redone = sorted(cmds.ls(long=True)) == after
return undone and redone
在 mayapy 里要先检查 cmds.undoInfo(q=True, state=True),因为无头模式下撤销是否开启本身待验证。
P5. OpenMaya 2.0 读取
import maya.api.OpenMaya as om
def dag(name):
sl = om.MSelectionList(); sl.add(name); return sl.getDagPath(0)
def mobject(name):
sl = om.MSelectionList(); sl.add(name); return sl.getDependNode(0)
def world_points(mesh): # one call, centimeters; [] on an empty mesh
import mx_audit # scenario-maya-expert/scripts on sys.path
fn = mx_audit.mfn_mesh(mesh) # MFnMesh raises on an empty mesh (2022.1)
return fn.getPoints(om.MSpace.kWorld) if fn else []
def world_matrix(name): # [added] instance-aware, shortest route
return dag(name).inclusiveMatrix()
def world_matrix_from_plug(obj): # Fragapane's plug route; MDataHandle.asMatrix() returns garbage
if not obj.hasFn(om.MFn.kDagNode):
return None
p = om.MFnDagNode(obj).findPlug("worldMatrix", False).elementByLogicalIndex(0)
return om.MFnMatrixData(p.asMObject()).matrix()
def matrices_match(expected, computed, tol=1e-5): # the rigorous check
return (expected.inverse() * computed).isEquivalent(om.MMatrix(), tol) # [verify signature]
world_points 是清单第 18 条与第 15 条的合体示范:先 mfn_mesh 拿到可能为 None 的 function set,再用条件表达式返回空列表,调用方拿到的永远是列表,不需要 try/except。world_matrix_from_plug 则演示了"最窄 function set"原则——先 hasFn(kDagNode) 判断,避免对非 DAG 节点构造 MFnDagNode 时抛异常。
mx_audit.extract() 是这套读取路径的生产级版本:resolve_mesh() 解析 transform 或 shape 长名并拒绝歧义短名;fn.getPoints(om.MSpace.kWorld) 取世界坐标;fn.getVertices() 一次拿到面顶点数与连接表;fn.getUVSetNames() 遍历 UV 集;硬边统计走 MItMeshEdge 并在边数超过 hard_edge_limit(默认 500000)时跳过,避免在超大网格上退化。
P6. 单位安全
UI_TO_CM = {"mm": 0.1, "cm": 1.0, "m": 100.0, "in": 2.54, "ft": 30.48, "yd": 91.44}
def ui_to_cm(v):
return v * UI_TO_CM[cmds.currentUnit(q=True, linear=True)]
不要为了脚本能跑而修改场景单位,要换算。文档额外提醒:Nucleus 与 Bifrost 无论工作单位是什么,都假定 1 单位 = 1 米。仓库里的换算表更完整(mx_audit.UI_TO_CM 含 km、mi,UNIT_PER_CM 是其倒数方向的映射),并且 transform_info() 会先用 cmds.currentUnit(q=True, linear=True) 查当前单位再换算 pivot,而不是硬编码厘米。
P7. 不依赖选择的属性操作
if not cmds.attributeQuery("expansion", node=ctrl, exists=True):
cmds.addAttr(ctrl, longName="expansion", attributeType="double", minValue=0, maxValue=100,
defaultValue=100, keyable=True)
cmds.addAttr(node, longName="fkBufferMsg", attributeType="message") # metadata link
cmds.connectAttr(buffer + ".message", node + ".fkBufferMsg")
cmds.keyTangent(obj, attribute="rotateY", time=(s, e), inTangentType="linear", outTangentType="linear")
cmds.cutKey(obj, attribute="rotateY", time=(None, None)) # all keys since 2022
约束的权重属性名要通过 weightAliasList=True 查询,绝不硬编码 W0(该 flag 待验证)。这一条和清单第 10 条是同一思想的两面:不要假设名字,去查询名字。
P8. 给 mx_run 的无头作业
# job_export.py run: python3 mx_run.py --scene in.ma --plugins fbx job_export.py -- /abs/out/asset.fbx
import maya.cmds as cmds
import maya.mel as mel
def main(argv):
out = argv[0]
roots = cmds.ls("|*_GRP", long=True) or [] # explicit roots, no selection state kept
mel.eval("FBXPushSettings")
try:
mel.eval("FBXResetExport")
mel.eval("FBXExportSmoothingGroups -v true")
cmds.select(roots, replace=True) # FBXExport -s needs a selection
mel.eval('FBXExport -f "%s" -s' % out)
finally:
mel.eval("FBXPopSettings")
return {"fbx": out, "roots": roots}
FBX 设置是全局状态:先 push、再 reset、逐项设置、最后 pop,并且用 -q 把设置记进日志。完整的 Unreal 预设放在 scenario-maya-pipeline-scripting 技能里——该技能的 mx_pipeline.fbx_commands() 就是把"reset → 逐项设置 → 清空 take 累加器 → 逐条 take"生成成有序 MEL 行列表的纯函数,并对动画预设强制要求显式传入 bake=(start, end),因为 Bake Start/End/Step 来自时间轴、从不存进预设。
mx_run 这条通道的契约值得完整记住:
- 命令行:
python3 mx_run.py [--scene f.ma] [--plugins mtoa,fbx,abc,usd] job.py -- args; - 一个场景一个子进程:崩溃或挂死只损失一个作业;
- 最后一行 stdout 永远是
MX_RESULT {json}; - 退出码:0 成功、1 作业抛错、2 用法错误、3 Maya 初始化失败、4 超时、5 找不到 mayapy;
- 子进程环境固定为
MAYA_DISABLE_ADP=1、MAYA_BATCH_STDOUT_LOGGING_LEVEL=none、MAYA_BATCH_STDERR_LOGGING_LEVEL=warning、MAYA_SKIP_USERSETUP_PY=1、ARNOLD_FORCE_ABORT_ON_LICENSE_FAIL=0、PYTHONUNBUFFERED=1; - 脚本节点默认不执行,
--save-as保存新版本并拒绝覆盖源文件。
其中 ARNOLD_FORCE_ABORT_ON_LICENSE_FAIL=0 的来历在源码注释里写得很清楚:Arnold 7.3 起批渲染默认在授权失败时直接中止,设为 0 则改为带水印继续渲染。
P9. 可撤销的 OpenMaya 命令
import maya.api.OpenMaya as om
def maya_useNewAPI():
pass
class AgentMakeGroup(om.MPxCommand):
kName = "agentMakeGroup"
def __init__(self):
om.MPxCommand.__init__(self)
self.mod = om.MDagModifier()
def isUndoable(self):
return True
def doIt(self, args): # runs once: set up, then redoIt
node = self.mod.createNode("transform")
self.mod.renameNode(node, "agentGrp#") # [verify] '#' expansion
self.redoIt()
def redoIt(self):
self.mod.doIt()
def undoIt(self):
self.mod.undoIt()
def initializePlugin(obj):
om.MFnPlugin(obj).registerCommand(AgentMakeGroup.kName, AgentMakeGroup)
def uninitializePlugin(obj):
om.MFnPlugin(obj).deregisterCommand(AgentMakeGroup.kName)
两个细节:MDGModifier 的删除操作要先调 doIt() 再调 deleteNode()、然后紧接一次 doIt()(API 2.0 帮助);内部插件的节点 ID 范围是 0x00000 到 0x7ffff。
P10. 插件重载
def reload_plugin(path, name):
cmds.flushUndo() # no instance of the old command left on the undo stack
if cmds.pluginInfo(name, q=True, loaded=True):
cmds.unloadPlugin(name)
cmds.loadPlugin(path)
flushUndo() 是必需的:撤销栈上只要还留着旧命令的实例,卸载就会失败或留下悬空引用。
P11. 带注册表的场景回调
_CALLBACKS = []
def watch_attr(node, attr, fn):
obj = mobject(node)
def cb(msg, plug, other, client):
if msg & om.MNodeMessage.kAttributeSet and plug.partialName(useLongNames=True) == attr:
fn(plug)
_CALLBACKS.append(om.MNodeMessage.addAttributeChangedCallback(obj, cb))
def clear_callbacks():
for cid in _CALLBACKS:
om.MMessage.removeCallback(cid)
del _CALLBACKS[:]
先把回调体写成普通函数跑通再接上——回调不会被保存进场景,所以"重启后失效"不是 bug 而是设计。这与 Fragapane 的建议一致:先把回调设计成一张小图,再考虑注册。
第五层:运行前先 lint
原文档第 5 节列出一份"拒绝或改写"清单:
from maya.cmds import *;import pymel;PySide2、shiboken2、QtWidgets.QAction、dockControl;- Python 2 语法;
- API 2.0 代码里的
OpenMayaMPx或MScriptUtil; noEvaluation、computeModifies、time=();addDoubleLinear、multiplyDoubleLinear、pointMatrixMult;- 批处理代码里的
ls(sl=True); - 桥接场景下的
input(、pdb、confirmDialog、promptDialog、fileDialog2; - 字符串形式的 UI 回调;
- 未做 modified 检查的
file(new=True, force=True)。
最后两组由 mx_bridge.lint() 自动完成。它的模态模式表是这样的(_MODAL_PATTERNS):
(r"\binput\s*\(", "input() opens a modal dialog in GUI Maya"),
(r"\braw_input\s*\(", "raw_input is Python 2 and modal"),
(r"\bpdb\b|\bbreakpoint\s*\(", "pdb opens input dialogs in GUI Maya"),
(r"\bconfirmDialog\b", "confirmDialog waits for a click"),
(r"\bpromptDialog\b", "promptDialog waits for typing"),
(r"\blayoutDialog\b", "layoutDialog is modal"),
(r"\bfileDialog2?\b", "file dialogs wait for a click"),
(r"\bshowWindow\b.*modal|\bmodal\s*=\s*True", "modal windows block the main thread"),
raw_input 与 breakpoint( 是文档清单之外的两个补充,说明 lint 层的价值不只是"照抄文档",而是把同类风险一次覆盖。每条拒绝都带一句人话解释,Agent 拿到 error 字段就能自我修正,而不是盲目重试。
被拒绝的调用返回 ok: False 且 error 以 refused by lint: 开头,代码根本不会被发送到 Maya——这意味着安全检查发生在 Agent 一侧,不会在用户会话里留下半成品。
第六层:运行时闸门
原文档第 6 节列出六类必须通过的运行时检查:
- 版本探针已记录;所需插件已加载(
pluginInfo(p, q=True, loaded=True))。 - 每个会改场景的工具都通过撤销往返测试(P4)。
- 任何吸附、匹配或绑定数学都要过
matrices_match矩阵闸门。 - 每个 OM2 辅助函数在类型错误的节点上要干净返回(
None或中性值)。 - 批处理:子进程退出码 0、
MX_RESULT被解析、uninitialize到达、每场景一个子进程。 - 桥接:端口只监听回环(
lsof -nP -iTCP:7001 -sTCP:LISTEN)、每条回复都被解析、设置了超时、结束时关闭端口。
在仓库里,这些闸门不是清单口号,而是验收函数。mx_audit.verdict(report, profile="game"|"film"|"subd") 把检查结果转成 error: / warn: / info: 字符串列表,空列表才算干净;mx_audit.passed() 只判断有没有 error。判定阈值按 profile 分档,源码里为每一行标了出处,例如:
PROFILES = {
# game film subd
"ngons": ("error", "error", "error"),
"tris": (None, None, "warn"),
"history": ("error", "error", "warn"),
"not_frozen": ("error", "error", "warn"),
"uv_overlap": ("warn", "error", "warn"),
"locked_normals": ("warn", "warn", "warn"),
}
同一份报告,在 game 档案下 uv_overlap 只是警告(优化过的游戏 UV 常常故意叠壳),在 film 档案下就是错误(唯一切图与烘焙需要唯一 UV)。这种"同一事实、不同结论"的设计,正是把专家判断编码进工具的方式。
另一层闸门是 mx_validate.validate(profile="model"|"rig"|"shot", roots=None, rules=None, fix=())。它输出 {ok, profile, summary, checks, fixes, before_summary, roots, roots_after, roots_lost, seconds},每个 check 形如 {id, status, message, count, items, fix},status 取 pass|warn|fail|skip|info。
三个档案的差别体现在 SEV 表里:model 对 geometry、uvs、history、transforms 全判 fail;rig 把 geometry 和 uvs 降为 warn(绑定阶段允许变形器历史),但 history 仍是 fail;shot 干脆对 geometry、uvs、history、transforms 判 None(跳过),因为资产应在各自交付点检查,而把 references、file_paths、frame_rate 升为 fail。
修复策略同样有闸门意识。SAFE_FIXES = ("history", "smooth_preview", "unknown_dead") 只包含默认安全的项;history_deformed、freeze、unlock_normals、empty_groups 等属于按需可选;而非流形、lamina、n-gon、单位、帧率、引用、命名、脚本节点永远不自动修——源码注释引用 FlippedNormals 的观点:n-gon 必须手工修,Cleanup 的自动结果不可预测。_safe_to_freeze() 还会拒绝冻结被引用、被锁定、被连接、下方有 joint、下方有蒙皮网格或实例化 shape 的 transform。
一个容易忽略的工程细节:所有 fix 各自包在独立撤销块中,并且 roots 在修复前后按 UUID 持有(root_handles / live_roots)。源码注释记录了这个问题的原貌——命名空间合并把 chr:body_GRP 改成 body_GRP 之后,早先的按名字查找会抛 "root does not exist",于是校验范围直接丢失。
第七层:把 GUI 习惯翻译成 Agent 动作
原文档最后给出一张对照表,这是整篇文档最实用的部分之一:人类靠界面获得的反馈,Agent 必须找到等价物。
| 人类习惯 | Agent 的替代动作 |
|---|---|
| 从 Script Editor 回显里学 MEL | cmds.help("<command>") 加 CommandsPython 参考;绝不猜 flag |
| 旋转视口 | mx_review.review 联系表(无头)或 mx_review.playblast(GUI) |
| 插件管理器 | loadPlugin、pluginInfo(q=True, loaded=True)、mx_run --plugins |
| Connection Editor、Add Attribute 窗口 | connectAttr、addAttr(message、matrix、数组类型是窗口造不出来的) |
| Mesh > Cleanup(选匹配项) | polyInfo(nonManifoldEdges=, nonManifoldVertices=, laminaFaces=) 加 mx_audit |
| 关掉 DAG Objects Only 的 Outliner | 带类型的 ls();mx_validate 的 unused、unknown、sets、layers 检查 |
| Ctrl+Z | cmds.undo();一次桥接调用就是一个块 |
| Render View 的 IPR | MtoA 5+ 下不可用;mx_review 无头出静帧,GUI 里用 Arnold RenderView |
这张表背后的原则与技能主文档的"专家循环"完全一致:没有看过渲染就不要宣布完成。"联系表"这个比喻尤其贴切——专家在建模时会不停旋转模型,Agent 的等价物就是把前、侧、后、四分之三、顶、低角度一次性拍成带标签的图,然后自己读图。
安全边界:桥接端口必须按"无认证"对待
文档对 mx_bridge 的安全姿态说得很直白:端口以 127.0.0.1:<port> 打开(需用 lsof 验证没有监听在 *),没有任何认证。Maya 帮助警告:INET 端口不需要用户识别,会以 Maya 用户的权限执行任何命令,包括 system()。因此规则是:只在个人机器上用,绝不转发端口,会话结束就 stop()。
这条警告在 mx_bridge_server.py 的模块文档里被重复了一次,并补充了 UNIX socket 选项(start(unix_socket="...")),因为文件权限可以把它限制到单个用户(Maya 实际设置的权限待验证)。交换目录默认是 $MX_BRIDGE_DIR,否则 /tmp/mx_bridge_<uid>,权限 0700,且如果属主不是当前用户就拒绝启动。
还有一条容易误解的设计:为什么桥接要用文件交换而不是直接把代码发过去?因为 commandPort 默认是 MEL、缓冲区 4096 字符(更长的命令会断开连接,更长的返回值会被替换成错误),而且 Python 送进去只返回单条语句的结果。所以 mx_bridge 只发一行短命令 _mx_bridge_run("<id>"),代码和结果都走文件。当代码超过 3900 字符时,send_line() 会主动抛 BridgeError,而不是等 Maya 那边静默断连。
交付前的最低自检
把上面七层压缩成 Agent 可以立即执行的顺序:
- 跑 P1 记录版本、API 版本、Python 版本、批处理标志与单位;
- 用 P2 的 get-or-create 建自己的节点并打标记,不碰用户节点;
- 需要撤销的编辑进 P3 的块,并用 P4 做撤销往返验收;
- 重读取走 P5,空网格先用
mx_audit.mfn_mesh()挡掉; - 涉及长度和角度时用 P6 换算,不改场景单位;
- 运行前过一遍第 5 节的 lint 清单,桥接场景直接依赖
mx_bridge.lint(); - 交付前跑
mx_audit.verdict()与mx_validate.validate(),把 JSON 报告、联系表和"未验证项清单"一起交出去。
其中第 7 步的"未验证项清单"不是客套。整份参考文档本身就以"所有代码尚未在 Maya 中运行"开头,并承诺在 Maya 装上后先跑探针测试、再修正所有 [verify] 条目。一个把不确定性写在明处的工具包,比一个假装什么都知道的工具包更值得信任——这也是本文愿意完整保留 [verify] 标记的原因。
相关文件索引
- 可靠性参考原文:cmds-reliability.md
- Maya 2027 破坏性变更全表:maya-2027-traps.md
- 资料来源与最佳用途:sources.md
- 技能主协议与执行通道:SKILL.md
- 网格客观检查与闸门:mx_audit.py
- 场景校验与安全修复:mx_validate.py
- 无头作业入口:mx_run.py
- 桥接客户端与 lint:mx_bridge.py
- 桥接服务器(运行在 Maya 内):mx_bridge_server.py
- 视觉评审与联系表:mx_review.py
- 场景替换守卫测试:test_mx_bridge_lint.py
- 家族脚本导入与命名守卫测试:test_family_imports.py
【免费下载链接】skills
Get production-ready images, video, audio, and 3D from any AI agent: skills that pick the right model, price before spending, and keep characters and brands consistent through the Scenario MCP, plus expert teams that drive Blender, Maya, ZBrush, Unreal, and Unity.
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



