OpenMAIC 专业课程编辑指南:用 pro-editing 技能对外部 Agent 的既有课件做外科手术式精修
【免费下载链接】OpenMAICOpen Multi-Agent Interactive Classroom — Get an immersive, multi-agent learning experience in just one click项目地址: https://gitcode.com/GitHub_Trending/op/OpenMAIC
本指南讲解 OpenMAIC(Open Multi-Agent Interactive Classroom)中pro-editingAgent 技能(skills/agent-runtime/pro-editing/SKILL.md)的完整用法:它面向已经存在并被持久化(persisted)的课程,为 Agent 提供一套"先盘点、再精读、后最小修改、最后一致性校验"的专业编辑工作流。读完本文,你将掌握list_scenes/read_stage/patch_stage/edit_deck/generate_tts/render_scene_preview这组工具的调用纪律,理解 JSON Pointer 寻址规则与身份不变约束,并能借助slide-dsl与slide-craft两个配套技能,把"改一个词、修一处重叠、重排一页"这类请求做成不会留下编辑痕迹的高质量修改。
技能定位:编辑一门已存在的课程,而不是重新生成它
pro-editing的元规则写在技能描述里:它只处理已持久化的课程——页面已经存在于存储中,会话挂接在一个既有课程上,或课堂已被构建。它的全部工作方式是"surgical"(外科手术式):读出现有内容、只改被要求的部分、其余一律不动。
这决定了它与仓库中其他技能的分工:
pro-editing决定"改哪一页、用什么操作、如何校验"(编辑流程);- skills/agent-runtime/slide-dsl/SKILL.md 是页面 JSON 的字段参考手册——告诉 Agent 每个字段叫什么、合法值是什么、渲染器实际读取哪个字段;
- skills/agent-runtime/slide-craft/SKILL.md 是设计法则——画布几何、字号高度表、类型层级、对比度与间距标准,即"什么样才算改得好";
stage-design与curriculum-planner仍在构建阶段负责页面生成与课程大纲规划,不参与编辑。
一个值得注意的实现细节:pro-editing目录下的 outline-constraints.json刻意不携带任何约束字段。原因写在文件的$comment中:pro-editing 的编辑回路只读取和修补已持久化页面,不运行大纲生成,因此没有大纲结构需要约束检查器强制,任何"结构性下限"反而会错误地约束用户并未要求的重建。该文件只是这一决策的书面记录。从源码看,约束文件是可选的兄弟文件,lib/server/agent-runtime/skills.ts 在加载技能时若发现同目录存在outline-constraints.json才会读取并注入大纲生成器的teacherContext槽位。
第一步:进入课程之前,先盘点全局
技能规定:任何编辑动作之前,必须先调用list_scenes对整个课程做一次盘点。这一步得到的不是假设,而是编辑所依据的唯一地图:
- 列出每个已持久化页面的完整清单:页码、顺序、类型;
- 不要规划替换性 stage——课程已有结构,你不是在重新规划它;
- 如果请求很宽泛(如"让它更好看"),把它转化为针对页面清单的具体计划,并在动手前说明将触及哪些页面。
从实现上看,list_scenes是基础课程工具之一(见 lib/server/agent-runtime/generation-tools.ts),它的提示语明确要求"每页使用一次 generate_scene,每次成功调用都是一个持久化检查点";edit_deck则负责retitle/insert/delete/reorder这类整页级操作(lib/server/agent-runtime/course-edit/tools.ts)。盘点页数、顺序、标题正是这一层的职责。
第二步:每次修改前,用 read_stage 精读目标页
read_stage以path:/scenes/<order|id>定位页面,紧邻每次编辑之前调用。返回的清单——元素 id、题目 id、动作 id、widget 配置——是你唯一可以编辑的地址空间,同时也揭示页面的真实类型。过期的 id 会导致失败的编辑;对页面内容的过期假设会导致错误的编辑。如果一次编辑落在了错误的页面类型上,这是"重新读取"的信号,而不是"强行执行操作"的信号。
read_stage提供三档深度(detail参数),三者不可互换:
detail | 返回内容 | 适用场景 |
|---|---|---|
tree(默认) | 每个元素一行紧凑信息:id、type、纯文本text、left、top、width、height、src | 定位元素。绝不能作为补丁来源——它剥离了样式 |
source | 页面精确持久化 JSON——所有样式字段、带内联标记与换行的原始 content HTML、精确几何、z 序 | 每次补丁的根。写入后再读一次验证 |
text | 每个含文本元素以{ path, id, type, text }输出,外加整页combinedText | 证明旧文案没有残留 |
关键规则:幻灯片写入必须用detail:"source"。tree只是紧凑地图,足够找到元素,却永远不够编辑它。source返回整页 JSON(含type、schemaVersion与完整 canvas),因此数组索引不会在读取与写入之间漂移——你在/content/canvas/elements/2/left读到的字段,就通过同一路径写回。text档则是残留检查的有力工具:每个path指向该元素自己的/content/canvas/elements/N,检查结果直接交给你要修补的指针;注意latex元素返回的是 LaTeX 源码、code是换行连接的代码行、table是|连接的单元格。
从源码看,read_stage(lib/server/agent-runtime/dsl-tools.ts)对source档会省略大块内联媒体字节(omitReadSceneMediaBytes),避免超大字符串阻塞事件循环;source与text档在超过 12000 字符后分页,通过nextOffset继续读取。
第三步:选择最小的操作——patch_stage 操作矩阵
一次幻灯片编辑不外乎三种操作中的一种,而第一种就是针对detail:"source"刚返回内容的单次 JSON Pointer 写入。技能的决策矩阵如下:
| 用户意图 | 工具 | 操作 |
|---|---|---|
| 修复或改写幻灯片上的文字 | patch_stage | 对该元素内容路径执行set或str_replace——/content/canvas/elements/N/content、…/text/content、…/data/0/0/text、…/lines/1/content、…/latex |
| 移动 / 缩放 / 旋转元素、修复重叠 | patch_stage | 对/content/canvas/elements/N/left(或top/width/height/rotate)执行set,每次操作一个数字 |
| 重新着色或重设元素样式 | patch_stage | 对渲染器持有的样式路径执行set——…/defaultColor、…/fill、…/text/defaultColor、…/color |
| 删除可选字段 | patch_stage | 对该路径执行remove |
| 替换图片或媒体源 | patch_stage | 对/content/canvas/elements/N/src执行set |
| 重排幻灯片元素层级 | patch_stage | 对整个/content/canvas/elements数组执行set——整体重排,id 与类型保持一致 |
| 新增或删除幻灯片元素 | patch_stage | add_element(完整 JSON,无id)/delete_element |
| 修复测验题、选项、答案、评分 | patch_stage | 对/content/questions/...执行set/remove;增删需重写整个数组 |
| 修改交互页 | patch_stage | 对/content/widgetConfig/...或/content/html执行set/remove/str_replace |
| 改写 / 插入 / 删除 / 重排旁白 | patch_stage | 指向/actions/...的 scene 根级指针;插入/重排需重写完整数组 |
| 编辑 PBL 简报、角色、里程碑、微任务 | patch_stage | 对/content/projectV2/...执行set/remove |
| 重命名 / 插入 / 删除 / 重排整页 | edit_deck | retitle/insert/delete/reorder |
| 从零重写一页 | generate_scene+instruction | 仅限用户明确要求整页重写时 |
矩阵背后的硬性规则(原文档的"Rules of the matrix"):
- 一个意图 → 一个操作。不要捆绑你无法逐一命名的变更。
- 寻址叶子。隔离你的变更所需的最小路径才是正确的路径;把整个对象写回去,正是相邻样式字段消失的原因。
patch_stage无法改变身份——不能改 canvas id、不能改元素 id 集合、不能改元素的type。这些必须走add_element/delete_element;类型变更等于"删除+新增"。内容本身不受检查——它按原样存储,所以slide-dsl是挡在你写的标记与坏页面之间的唯一防线。- 被拒绝的补丁什么都没改。坏路径、越界索引、未知字段、错误类型或非法结果页,都会带着原样页面响亮地失败。重新读取、重新提交,绝不硬来。
add_element接收一个完整的元素 JSON 但不含id——服务端校验与补丁相同的结构契约,分配 id,并按afterId或index(二选一)插入。- 没有任何东西会规范化你的值。颜色、字体、主题都不会被重写成"家风格",所以看起来不对的编辑是你的值的问题,不是工具的问题。
edit_deck insert创建空桩;用generate_scene配合该新页的显式type与brief填充它,或修补一个已合法的 scene。generate_scene带instruction会丢弃整页并重新生成。这是重写,不是编辑:留待用户明确要求重建页面时使用,绝不可作为绕过精细编辑的捷径。- 如果会话已附加资料或网络访问权限,用它们来夯实编辑内容——绝不是重建无人要求的页面的借口。
源码级印证:patch_stage 的原子批处理
lib/server/agent-runtime/dsl-tools.ts 中patch_stage的实现揭示了文档背后的真实机制:
- 它原子性地修补
/scenes/<order|sceneId>下的单个 scene,ops是操作数组,任意一个操作失败(applyPatchOp返回ok: false)都会拒绝整个批次并报告失败的操作序号; - 幻灯片路径的
set/remove/str_replace最终被归一化为{ op: 'patch', action, path, value }交给applySlideEdit,其中path剥去/content前缀后才是/canvas/...部分——这与文档中"从read_stage detail:"source"返回的精确 scene 根寻址"一致; slideOperation会拒绝以/actions/...之外的路径冒充幻灯片指针的调用,并明确报错slide content pointer must start /content/canvas/;- 一批补丁提交前还有最终态占位符守卫:逐操作检查是隔离的,模型可以用多个
str_replace各携带一个片段,拼凑出一个完整的只读媒体占位符从而绕过逐操作检查;因此整个批次在序列化最终 scene 后会重新整体校验,命中即整体拒绝、绝不落盘(绝不进入putScene)。
寻址规则速查
path是锚定在read_stage detail:"source"返回的精确 scene 上的 JSON Pointer,幻灯片字段以/content/canvas/开头:
- 寻址叶子而非分支:给出隔离变更所需的最小路径。把整个对象写回,相邻样式字段会被悄悄丢弃。
- 数组索引必须规范且有界:
0、1、2合法;03、-1、+1和越界索引一律被拒。 - 最后一段之前的每一段都必须已存在;最后一段可以不存在——
set一个对象尚无的键会新增该可选字段(如给文本元素加fill),remove则删除它;对不存在的路径remove会失败。 - 路径不得穿越标量:
/content/canvas/elements/0/content/0会失败,因为content是字符串而非容器。 - 对数组索引
remove会缩短数组;除整数组重写外,没有"按索引插入"。 ~1与~0转义键内的/与~;裸~或~2被拒。- 值在进入时深拷贝,你发送的嵌套对象会作为自己的树存储。
工作中的示例(每次调用一个):
| 变更 | path | value |
|---|---|---|
| 标题的富文本 | /content/canvas/elements/0/content | <p><span style="color:#00a870">新标题</span></p> |
| 单个表格单元格 | /content/canvas/elements/5/data/0/0/text | "净利润" |
| 一行代码 | /content/canvas/elements/9/lines/1/content | "total = price * count" |
| 形状标签 | /content/canvas/elements/3/text/content | <p>第二阶段</p> |
| 字形默认色 | /content/canvas/elements/2/defaultColor | "#1f4e79" |
| 单个图表标签 | /content/canvas/elements/6/data/labels/2 | "Q3" |
| 元素位置 | /content/canvas/elements/2/left | 120 |
| 页面背景 | /content/canvas/background/color | "#f7f7f5" |
| 删除可选字段 | /content/canvas/elements/4/shadow | (op: 'remove') |
| 层级重排 | /content/canvas/elements | 整个数组,重排后返回 |
z 序没有叶子可寻址,因为绘制顺序就是数组位置:索引 0 最先绘制(底层),最后绘制顶层;渲染器直接以数组索引分配 CSSz-index。要重排层级,就整体重写/content/canvas/elements——id 集合与每个 id→type 配对必须原样返回。这就是"唯一的没有叶子可寻址的变更"。
最小编辑纪律(Minimum-edit discipline)
- 只改被要求的部分。不重设未触碰元素的样式,不改写没人抱怨的旁白,不在修一页的同时"顺手改进"相邻页面。
- 保持课程自身的声音:匹配其既有术语、语调和视觉语言。一次编辑不应被看出来是编辑。
- 只要目标比整页更窄,优先用细粒度的逐字段
patch_stage,而不是generate_scene。 - 任何涉及几何、颜色、文本长度或富文本结构的幻灯片编辑,都要加载
slide-craft——它承载了页面被绘制时所依据的设计法则:文本高度表、类型层级、对比度配对、间距节奏,以及哪个字段在哪个元素类型上真正到达屏幕。 - 当你需要字段本身——它的名字、合法值、寻址它的路径、渲染器读取两个字段中的哪一个——那就是
slide-dsl。
slide-craft 的关键设计法则(编辑时遵守)
skills/agent-runtime/slide-craft/SKILL.md 定义了页面设计约束,编辑时这些数字是"修复"与"凹陷"的分界线:
- 画布 1000 × 562.5 px,所有元素遵守50px 边距:
left ∈ [50, 950]、top ∈ [50, 512.5],右边缘left + width ≤ 950、下边缘 ≤ 512.5; - 文本按表定高,不靠肉眼:文本元素四周有 10px 内边距,可用区为
(width - 20) × (height - 20),高度来自字号表(line-height 1.5、含内边距)。例如 16px 字号:1 行 46、2 行 70、3 行 94、4 行 118、5 行 142;24px:58 / 94 / 130 / 166 / 202;32px:70 / 118 / 166 / 214 / 262。替换文字后重新推导高度:characters_per_line = (width - 20) / font_size,最长行保持在≤ 75%,按内容中最大字号查表; - 高度是容器,不是夹子:溢出文本会溢出盒子而非收缩,页面会展示溢出。修复顺序是先缩短文字、再跳到下一档表行、最后加宽盒子;
- 对比度是一对,不是一个颜色:正文 ≥ 4.5:1,24px 及以上标题 ≥ 3:1。
#333333配白约 12:1 安全;强调色#5b9bd5配白约 3:1,只够 32px 标题。稳妥模式是"浅色调填充 + 同色相深色文字":#1e40af配#dbeafe、#166534配#dcfce7、#92400e配#fef3c7,各约 6–7:1; - 形状上的文字是一个对象:
text.width = shape.width - 40、text.left = shape.left + (shape.width - text.width) / 2、text.top = shape.top + (shape.height - text.height) / 2,中心点应相差 ≤ 2px——移动形状永远是"移动形状 + 重推导标签"两步; - 平行元素共享完全相同的数字:一行三张卡片共用一个
width、height、top和间距;标题→副标题 30–40px、标题→正文 35–50px、段落块之间 20–30px、多栏间距 40–60px、被箭头跨越的间距需要 60–80px; - 派生元素跟随锚点:标题下划线、分隔线、高亮条都是依据被装饰文本计算的形状,文本移动后必须重算;
- 内容类型与元素类型一一对应:任何数学都是
latex元素;表格单元格是纯文本;代码元素取纯文本行(默认 14px,高度要覆盖 32px 头部加每行);line的width是描边粗细而非长度;图片保持宽高比,改width/height要一起改。
Look → edit → look:预览预算
对于布局敏感的变更(位置、密度、新元素、对齐),在render_scene_preview可用时用它验证:看渲染、编辑、再看一次。每页最多两轮预览——每次渲染都耗费一次工具调用,而整个运行有硬性的调用上限。不要预览未触碰的页面,不要超过预算:两轮未收敛就停下来,告诉用户剩余事项,而不是把整次运行烧在一页上。
从实现看,render_scene_preview(lib/server/agent-runtime/scene-preview.ts)是一个按能力注册的独立工具,import-pptx的提示语也建议导入后用它检查每个页面。
旁白音频跟随文本:generate_tts
用patch_stage修改语音动作的/actions/N/text会清除该行改写后的音频,而插入的行天然没有音频。因此任何语音措辞变更后,都必须在进入下一页前对该页调用generate_tts——它的默认模式只合成缺失音频的行,正好是所需行为。改写文本却不重新生成音频,交付的是一页静音。
源码印证(lib/server/agent-runtime/course-edit/tools.ts):generate_tts默认force: false,只补齐没有音频的语音动作;force: true才强制重合成所有语音音频。成功变更会落盘并打持久化检查点,返回Narration audio: X generated, Y skipped, Z failed.的汇总。另外,dsl-tools.ts中str_replace与非幻灯片指针的set/remove路径在写入后会调用clearStaleSpeechAudio——这是源码层面"音频跟随文本"的自动保障:文本一改,对应音频即被判定过期。
收尾:一致性检查(Consistency pass)
在告诉用户"完成"之前,必须过一遍:
list_scenes——确认页面清单与意图一致:数量、顺序、标题;- 每个编辑结果已经携带了该页的最新清单;对照验证,仅当结果留下疑问时才用
read_stage重读; - 扫一遍你触碰过的页面,检查你的编辑可能引入的跨页漂移:同一概念命名是否一致、单位与术语是否统一、你引入的样式是否应用于所有该用的地方;
- 被触碰页面上每个语音动作都必须有
audioId(read_stage会显示);任何缺音频的页面用generate_tts补齐。
与人类编辑共享课程:last-write-wins 协作纪律
保存按页采用最后写入者胜出(last-write-wins),而同时在浏览器里编辑的人类作者可能覆盖你,就像你覆盖他们一样。把课程当作共享土地:
- 先读新,再写。紧邻编辑前的
read_stage不是形式——它正是避免覆盖用户自你上次查看以来所做更改的方式; - 小而增量地写。每次工具调用在落地的瞬间自动持久化其变更;一小串小写入相比一次大重写,冲突损失更小。绝不要积攒"已准备但未写入"的批次——边写边走;
- 用户正在编辑时让位。如果用户说他们正在改同一页,或一次新读取显示了你没写过的内容,停下来、重读、先协调再写。永远不要用你的陈旧快照覆盖人类更新的版本。
这与patch_stage实现中"每次操作前structuredClone当前 scene、逐操作应用、整体校验后落盘"的原子语义相呼应:拒绝的批次不产生任何部分写入。
预算管理:把调用花在编辑上,而不是仪式上
运行时没有硬性的工具调用上限,但每一次额外调用都是用户等待的延迟。把调用花在编辑本身:入口一次list_scenes、每页编辑前一次read_stage、编辑本身、措辞变化处generate_tts、仅在布局真正有风险时的一轮预览。如果请求很大,先做最高价值的编辑,并告诉用户剩余事项。
为什么这样编辑是安全的:结构校验的唯一边界
pro-editing之所以能大胆做最小编辑,是因为写入路径的守卫只有一个:DSL 结构 schema。写入的内容按字节存储,没有标记允许列表、没有清理器、没有颜色或字体规范化、没有几何钳制、没有家风格。patch_stage只检查一件事——结果文档仍符合 DSL 结构契约(字段名、类型、必填字段、封闭对象)——然后逐字节持久化你的值。
因此文档强调(skills/agent-runtime/slide-dsl/SKILL.md):
- 拒绝清单是穷尽的:非
/content/canvas/开头的幻灯片路径、畸形~转义、非规范或越界数组索引、缺失中间段或穿越非容器的路径、对不存在键的remove、缺value的set或带value的remove,以及身份类拒绝(改/content/canvas/id、增删改重复元素 id、改 id 的type、add_element带 id 或同时带afterId与index),还有 schema 类拒绝(未知字段、错误类型、封闭联合外的值、移除必填字段、错误元数元组、错误类型上的字段); - 但不在清单上的更多:没有标签检查、没有 CSS 检查、没有颜色格式检查、没有长度限制、没有几何边界、没有对比度规则、没有
html与latex一致性检查、没有图表series与labels匹配检查——这些都靠你(和slide-craft)来保证; - 服务端唯一自动重写:当补丁改变元素的
latex时,服务端从新源码重渲染该元素的html快照(KaTeX,display 模式,错误渲染而不抛出)并存储;若渲染无结果则移除html。除此之外文档的任何部分都不会被触碰。
这也是"被拒绝的补丁什么都没改"的完整含义:错误是信息,不是损坏——每条检查都在任何内容存储之前运行,错误消息会点名违规路径。
快速参考:编辑流程全景
| 阶段 | 工具调用 | 目的 |
|---|---|---|
| 入口 | list_scenes | 盘点整门课:页数、顺序、类型,制定编辑计划 |
| 每次编辑前 | read_stage path:/scenes/<order|id> detail:"source" | 拿精确持久化 JSON,作为补丁路径的唯一地址空间 |
| 编辑 | patch_stage(set/remove/str_replace/add_element/delete_element)或edit_deck(retitle/insert/delete/reorder) | 最小操作、寻址叶子、不碰身份 |
| 布局验证 | render_scene_preview | 每页至多两轮,只对有布局风险的页面 |
| 音频 | generate_tts | 每次语音措辞变更后补齐缺失音频 |
| 收尾 | list_scenes+ 逐页核对 + 跨页一致性扫描 | 数量/顺序/标题正确、术语统一、无静音页 |
配合技能三件套:字段含义查 slide-dsl,设计标准查 slide-craft,编辑流程遵循本文的pro-editing。整条链路在 lib/server/agent-runtime/dsl-tools.ts 与 lib/server/agent-runtime/course-edit/tools.ts 中均有完整的原子实现支撑。
【免费下载链接】OpenMAICOpen Multi-Agent Interactive Classroom — Get an immersive, multi-agent learning experience in just one click项目地址: https://gitcode.com/GitHub_Trending/op/OpenMAIC
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考