Mobius 图文教程制作技能

SkillWeb & browsing

Lets your agent create illustrated step-by-step tutorials with annotated screenshots and publish them to a docs site.

Instructions available. Your AI can read the instructions. Execution depends on the setup they require.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

Then ask your AI: use the Mobius 图文教程制作技能 skill

About this skill

Create illustrated Mobius usage tutorials (full workflow: understand user intent → take screenshots with Playwright → add red numbered annotations → upload to image hosting → write markdown → update mkdocs → verify with local build → commit/push). Use when the user says "加教程 / 写教程 / 图文教程 / tutorial"

What this skill tells your AI

The instructions your AI receives, as published by nutshellai-tech/mobius in skills/mobius-write-guide/SKILL.md and read by ahel’s review.

把一个 Mobius 功能做成带标记截图的双语图文教程,发布到 mkdocs 文档站(GitHub Pages)。本技能是 docs/tutorial/04~14 这一批教程的制作经验沉淀,所有命令、常量、坑都是实测过的,照做即可。

全流程总览

0. 理解用户 → 1. Playwright 截图(原始+矩形) → 2. PIL 标注 → 3. 上传图床
→ 4. 写 .md + .en.md → 5. 改 mkdocs.yml + index → 6. mkdocs build 验证
→ 7. commit + push (gitlab 直连, github 走 proxychains)

全程不要污染真实数据:需要演示数据时新建临时项目/示例记忆,做完删掉。 全程绝不能让密钥、token、私钥路径进入截图(见末尾安全红线)。


0. 理解用户、规划

  • 确认主题与边界:要讲哪个功能?从入口到结果完整覆盖,一般 3–6 张图。
  • 确认归类与位置:参考现有 mkdocs.yml 的 nav 分区(I-五分钟精通 / II-高级能力 / III-管理员基础 / IV-自我进化能力 / V-小技巧 / VI-深度研究)。问清放在哪个分区、是否新建分区;用户常会指定"靠前"。新建分区时要同步:nav 加顶层段 + nav_translations 加 section 中文名 + index.md/index.en.md 加 ### 段,三处缺一不可。
  • 确认命名:章节中文名(用户经常随手改名,如「万能捷径:小莫助理」),同步想好英文 nav key(如 Xiaomo: Universal Shortcut)。
  • 先读代码再截:grep 前端 data-tour="..." 选择器、读相关组件,确保步骤文案和真实 UI 一致,也方便定位截图元素。
  • 编号:用下一个可用编号(看 docs/tutorial/ 最大号 +1),文件名 <NN>_<snake_name>.md。

1. Playwright 截图(原始 PNG + 元素矩形)

环境(已装好,无需再 install)

playwright 不在仓库内、在 npx 缓存里,且缓存目录哈希会变,别写死——用一行 find 自动发现:

NP=$(find /home/tianyi/.npm/_npx -maxdepth 3 -type d -name node_modules 2>/dev/null | head -1)
NODE_PATH="$NP" node /tmp/your_script.js
# 验证: NODE_PATH="$NP" node -e "require('playwright');console.log('ok')"

若 find 为空(缓存被清),随便 npx playwright ... 跑一次会重建。Chromium 浏览器缓存在 ~/.cache/ms-playwright。

登录 + 关首登引导(写死,密码免登)

const TOKEN = process.env.MOB_TOKEN; // 或 curl 取: 见下方"关键常量"
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 2 });
await ctx.addInitScript((t) => {
  try { localStorage.setItem('cc-token', t); } catch(e){}                    // 登录态
  try { localStorage.setItem('imac:first-login-tour-seen:v1:fuqingxu', String(Date.now())); } catch(e){} // 关掉首登引导弹窗,否则盖住界面
}, TOKEN);
  • viewport 固定 1440×900 + deviceScaleFactor:2 → 截图 2880×1800,清晰;所有 CSS 坐标 ×2 = 图片像素。
  • waitUntil: 'load',绝不用 'networkidle':Mobius 有 SSE 长连接,networkidle 永远不触发会 30s 超时(能继续但慢且状态不对)。

路由

  • 项目:/u/<user>/p/<project>
  • Issue:/u/<user>/p/<project>/i/<issue>
  • 打开某个会话的聊天:/u/<user>/p/<project>/i/<issue>?session=<session_id>(IssuePage 右栏 ?session= 时显示 ChatArea)
  • 管理中心:点右上头像 → 管理中心([data-tour="top-user-menu"] → 按钮「管理中心」),仅 admin 可见

截图 + 记录矩形(关键)

用整页 viewport 截图 + 元素 boundingBox(),矩形用 CSS 像素(与 annotate 的 SCALE=2 配套):

async function rect(loc){ const b = await loc.boundingBox(); return b && {x:b.x,y:b.y,w:b.width,h:b.height}; }
// 滚动目标入视口再截图
await page.locator('[data-tour="xxx"]').scrollIntoViewIfNeeded();
await page.screenshot({ path: `${RAW}/01.png` });
  • 弹窗/菜单点击用 {force:true}:Mobius 的 div.fixed.inset-0 弹窗,普通 click 经常不触发(被遮罩拦截),force:true 才稳定。
  • 等弹窗用稳定内层元素,别用 div.fixed.inset-0 的 locator visible(Playwright 可见性判定对它不准):
    await page.waitForSelector('input[placeholder="搜索输入内容"]', { state:'visible' });
    
  • 截图前等数据加载:聊天历史/列表是 SSE 拉取的,waitForTimeout(1500~2500) 或 waitForFunction(() => document.body.innerText.length > N)。

把每张图的矩形写进 shots.json(见下一步)。

先探后截(probe → capture → fixup,省大量返工)

真实 UI 的选择器别靠猜——先写个 probe 脚本导航到目标页、dump 出所有可见交互元素(文本/title/aria/rect),看清楚再写正式 capture 脚本。矩形拿不准时,capture 脚本里把每步的 rect console.log 出来 + 先拍一张裸图,对照后再用 fixup 脚本补/改 shots.json 里的 highlight(比反复重跑 capture 快)。

// dump 所有可见 button/a/input/label/select 的文本+矩形(写进 json 供查阅)
function dump(page,root){return page.evaluate((root)=>{const rc=el=>{const r=el.getBoundingClientRect();return{x:Math.round(r.x),y:Math.round(r.y),w:Math.round(r.width),h:Math.round(r.height)};};const vis=e=>{const r=e.getBoundingClientRect();const s=getComputedStyle(e);return r.width>16&&r.height>10&&s.display!=='none'&&s.visibility!=='hidden';};const el=root?document.querySelector(root):document;return Array.from((el||document).querySelectorAll('button,a,[role="button"],[role="tab"],input,label,select')).filter(vis).map(e=>({tag:e.tagName.toLowerCase(),txt:(e.textContent||'').trim().slice(0,30),title:e.getAttribute('title')||'',al:e.getAttribute('aria-label')||'',ph:e.getAttribute('placeholder')||'',checked:!!e.checked,rect:rc(e)}));},root);}

坑① React 受控输入填不进去:直接 inp.value='x' 不会更新 React state(提交时仍是空),必须用原生 setter 触发,或干脆用 Playwright 的 page.fill(selector, value)(最稳):

// page.fill 最稳:
await page.fill('input[placeholder="Research 标题"]','大模型推理加速');
// 或在 evaluate 内手动触发(React 认):
const set=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set;
set.call(inp,'大模型推理加速'); inp.dispatchEvent(new Event('input',{bubbles:true}));

坑② 按文本匹配会撞到同名元素:Mobius 常有「侧栏导航按钮」和「弹窗内标签」同名(如 "Research Graph" 既是研究页侧栏按钮、又是团队弹窗里的 Agent 标签)。.find(x=>x.textContent.includes(...)) 会命中DOM 顺序靠前的那个(侧栏),点下去没选中目标。修法:用更长、唯一的文本匹配(如 /Research Graph 绘制/ 而非 /Research Graph/),或把查询 scope 到弹窗根(先给最高 z 的 div.fixed.inset-0 打 data-topmodal 标签再在其内 query)。

坑③ 定位 select 时分清"包在里面"还是"平级":有的 label 把 <select> 包在里面(<label>形象<select>…</select></label>),有的是平级兄弟。取 select 时 lab.querySelector('select')(包在里面)vs lab.parentElement.querySelector('select')(平级)不一样,搞错会拿到隔壁那个 select(如把"场景"当成"形象")。先用 dump 确认结构,或直接按 options 内容反查:selects.find(s=>[...s.options].some(o=>/企鹅/.test(o.textContent)))。

别截出真实 Agent / 别烧 token:截「创建 Agent / 启动会话」类弹窗时,只展示配置、不要点「创建/启动/提交」(一旦提交就 spawn 真实会话、烧模型 quota、污染数据)。截图停在表单填好未提交的状态即可;真要建演示数据,用最轻的 API(如建 research 只插一行 DB,不起 agent)。


2. PIL 标注(红色编号 + 描边 + 标签)

注意避免字体超出边框!这是标注阶段的常见错误

复用现成标注器 .imac/tmp/tutorial05/annotate.py(红圆角描边 + 编号红圆 badge + 白底红边 pill 标签 + leader line,CJK 自动换行 + 碰撞避让,字体 /usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc)。

它读 shots.json(CSS 像素矩形,SCALE=2 乘回图片像素):

{
  "shot1": {
    "file": "01.png",
    "highlights": [
      {"rect": {"x":350,"y":537,"w":464,"h":96}, "anchor": "tr", "label": "① 这里是 XX 功能"}
    ]
  }
}
  • anchor:badge 从元素的哪个角向外伸(tl tr bl br tc bc cl cr)。

参数化复用(annotate.py 把 RAW/OUT 写死了 tutorial05,复制一份改掉,或用环境变量):

# 把 RAW/OUT 改成可环境变量覆盖
python3 - <<'PY'
src=open('.imac/tmp/tutorial05/annotate.py').read()
src=src.replace("RAW = '/home/tianyi/imac-test/.imac/tmp/tutorial05/raw'",
                "import os\nRAW = os.environ.get('TUT_RAW','/home/tianyi/imac-test/.imac/tmp/tutorial05/raw')")
src=src.replace("OUT = '/home/tianyi/imac-test/.imac/tmp/tutorial05/annotated'",
                "OUT = os.environ.get('TUT_OUT','/home/tianyi/imac-test/.imac/tmp/tutorial05/annotated')")
open('.imac/tmp/<yourdir>/annotate.py','w').write(src)
PY
TUT_RAW=/.../raw TUT_OUT=/.../annotated python3 .imac/tmp/<yourdir>/annotate.py

聚焦裁剪:整页截图里元素太小时,annotated 之后用 PIL 把关键区域裁出来(CSS 坐标 ×2):

from PIL import Image
im=Image.open('annotated/01.png')
im.crop((x0*2,y0*2,x1*2,y1*2)).thumbnail((1600,1600)).save('01_focus.jpg','JPEG',quality=88)

3. 上传图床

curl -X POST https://public.agent-matrix.com/up/v100 \
  -H "Authorization: Bearer iooir13gnwduio_beli882__AUNGLOIUYUG" \
  -F "folder=tutorial" -F "file_name=NN_topic_01.jpg" -F "file=@/path/to.jpg" --no-buffer
# 返回多行流式 JSON, 取 status==success 的 url

硬规矩(都踩过):

  • folder 必须纯 ASCII 字母(不能数字/连字符)→ 用 tutorial。
  • 单文件限 ~500KB。2880×1800 PNG 会 413 → 上传前 Image.thumbnail((1600,1600)) + 存 JPEG q88(~150–250KB)。
  • file_name 扩展名必须与内容一致:存 JPEG 字节就命名 .jpg(写 .png 会让 CDN 按 png 回头,类型不符)。
  • 🔴 图床不覆盖同名:同名重传,返回的 URL 服务的内容字节级不变(老图还在)。所以每次用新文件名(如加 _v2),改完同步改 markdown 里的 URL。这也意味着——一旦截到密钥传上去了,删 markdown 没用,老 URL 仍可下载,必须轮换密钥(见安全红线)。
  • 域名漂移:上传返回的 host 现在是 serve.gptacademic.cn(CDN),但 docs 里必须用 serve.nutshellai.cn(同 bucket 同 path,直接换 host,两域名都 200)。写 markdown 时直接把返回 URL 的 host 换掉:
    echo "$url" | sed 's#serve.gptacademic.cn#serve.nutshellai.cn#'
    

4. 写 markdown(双语)

  • docs/tutorial/NN_topic.md(中文,主)+ NN_topic.en.md(英文)。
  • 风格对齐 01–05:极简。# 标题 + 编号步骤(### 1. ### 2.),每步 1–3 行 bullet + 一张带标记的截图承载信息。
  • 图片用图床绝对 URL:![image](https://serve.nutshellai.cn/publish/auto/tutorial/NN_topic_01.jpg)
  • 末尾可加一句 > 小贴士:... 收尾。‍(零宽字符)当空行分隔,沿用旧教程习惯。

5. 改 mkdocs.yml + docs/index

mkdocs 用 mkdocs-material + mkdocs-static-i18n(docs_structure: suffix:文件名 <name>.md=默认中文,<name>.en.md=英文)。

nav(英文 key + 子项)

nav:
  - Home: index.md
  - 'II. Advanced Capabilities':
      - Add Remote Compute: tutorial/04_add_remote_server.md
  • section/章节名带冒号必须加引号:'Concepts: Skills & Memory': tutorial/...,nav_translations 里同样 "Concepts: Skills & Memory": ...。
  • 分区是顶层 dict + list 值(navigation.sections + navigation.tabs 已开,会渲染成分组/标签)。

nav_translations(英文 key → 中文,zh 是默认语言)

plugins:
  - i18n:
      docs_structure: suffix
      languages:
        - locale: zh
          default: true
          nav_translations:
            Home: 首页
            "II. Advanced Capabilities": II-高级能力
            "Add Remote Compute": 添加远程算力
  • 每个 nav key(section 名 + 条目名)都必须在 nav_translations 里有对应中文,漏一个中文站就显示英文。改完用脚本校验(见下)。

docs/index.md / index.en.md(首页「快速开始」)

同步加一个 ### II-高级能力 标题 + 链接列表,和 nav 分区一致。


6. 本地构建验证(务必做,CI 跑的是 mkdocs build)

仓库已备好专用 venv .venv-docs(mkdocs-material + mkdocs-static-i18n 都装好了),直接用:

.venv-docs/bin/mkdocs build --strict 2>&1 | tail -25

只有 .venv-docs 损坏 / 被删时才重建:python3 -m venv .venv-docs && .venv-docs/bin/pip install -q -r docs/requirements-docs.txt。

  • 看日志有 Translated N navigation elements to 'zh'(N = nav key 总数,新增条目后 N 应 +对应数,无 missing)。
  • 无 error / Exception / Conflicting files。
  • --strict 下任何 WARNING 都会 abort:仓库里有长期存在的「非本次」告警(如未入库的 compute-and-devices/README.*.md 链接、zhipu-key-setup.md 未进 nav),它们与你的教程无关。判断你自己的改动是否干净:grep 构建输出里有没有 tutorial/NN、你的图床 URL、或你改的 nav key;再确认 site/tutorial/ + site/en/tutorial/ 下都生成了你的 NN_* 页面。只要这两条过了,strict 的 abort 就不是你造成的,可放行。
  • i18n 冲突:X.md(默认 zh)和 X.zh.md 同时存在会报 Conflicting files for the default language 'zh'。修法:把英文内容从 X.md 改名到 X.en.md,留 X.zh.md(zh)+ X.en.md(en)。(注:默认 zh 用裸 NN_xxx.md 和显式 NN_xxx.zh.md 都合法,仓库里两种都有,保持和同分区已有教程一致即可。)

一键校验 nav→翻译→文件 三者一致:

import yaml, os
d=yaml.safe_load(open('mkdocs.yml')); zt=d['plugins'][1]['i18n']['languages'][0]['nav_translations']
keys=[]
def walk(e):
    if isinstance(e,dict):
        for k,v in e.items():
            keys.append(k)
            if isinstance(v,list): [walk(x) for x in v]
for it in d['nav']: walk(it)
print('missing zh:', [k for k in keys if k not in zt])
# 每个 nav 条目都要有 .md 或 .zh.md (默认 zh) 源文件

验证完删掉 site/ 和 venv(别提交构建产物)。


7. commit + push

git add -A   # 本仓库是自迭代项目: 按规则连非自己改的文件一并提交
git commit -m "Add tutorial: <英文说明> (中文说明, 含带标记截图; 同步 docs/index 与 mkdocs nav 中英)"
# GitLab(origin,内网,直连)
git push origin main
# GitHub(github,外网,TLS 常崩 → 走 proxychains;禁止用环境变量设代理)
proxychains -q git push github main
  • commit message 格式:英文 (中文),不含人名,邮箱 mobius_os@163.com(本仓库已配好)。
  • 并发自迭代 agent 会 git add -A 把你的改动扫进它的 commit(用它的 message)——你的代码会正确落入 HEAD,但若你要用自己的 message,提交要快。
  • pre-commit hook 的 tsc/frontend 检查:只改 .md/.yml 时会 Skipped;偶尔首次 commit 报 "files were modified by this hook"(格式化),git add -A && git commit 再来一次即可。

🔴 安全红线(最重要)

绝不能让密钥/token/私钥/真实凭据进入截图或教程文本。 真实事故:tutorial 10 把 Claude Code 模型的 channel key(形如 1d293dfd0a554fa381480f8828eba9f2.7f0EB6IaSjLtVi39,是 token 形状)截进了管理中心截图,发布到了公开文档站。

截管理中心 / 模型配置 / 账号类页面前,务必:

  1. 识别敏感字段:模型 key、api_key、token、Anthropic/Codex API Key、SSH 私钥路径、密码框。注意模型 key 字段虽叫"key",但值可能是 token(hex.secret 格式)。
  2. 优先用掩码态截图:表单的 api_key 默认就是掩码(••••XXXX)——别点"显示/揭示"按钮再截。
  3. 不得不截含敏感值的区域 → 上传前用 PIL 马赛克/实色遮蔽该矩形(重度像素化 + 高斯模糊,确保不可逆读)。
  4. 密钥一旦传上图床 = 已泄露:图床不删旧文件、文档站是公开的,必须通知用户轮换密钥,光删 markdown 没用。

同样别写入真实账号密码 token 到 markdown 文本。


关键常量速查

项值
Mobius 本地地址http://127.0.0.1:45616
登录(密码免登)POST /api/auth/login {"username":"fuqingxu"} → .token
登录态 localStoragecc-token
关首登引导 localStorageimac:first-login-tour-seen:v1:fuqingxu
PlaywrightNODE_PATH=$(find /home/tianyi/.npm/_npx -maxdepth 3 -name node_modules | head -1)(npx 缓存,别写死)
标注器(复用源).imac/tmp/tutorial05/annotate.py(复制后参数化 RAW/OUT)
CJK 字体/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc
图床上传POST https://public.agent-matrix.com/up/v100,Authorization: Bearer iooir13gnwduio_beli882__AUNGLOIUYUG
图床 foldertutorial(纯 ASCII 字母)
图床域名(docs 用)serve.nutshellai.cn(返回的是 serve.gptacademic.cn,换掉)
docs 仓库/home/tianyi/imac-test(docs/ + mkdocs.yml + docs/requirements-docs.txt)
远端 originssh://git@gitlab.agent-matrix.com:12340/nutshellai/mobius.git(内网直连)
远端 githubhttps://github.com/mobius-system/mobius.git(外网,push 走 proxychains -q)
commit 邮箱/署名mobius_os@163.com / Mobius OS(已配)

常见坑(别再踩)

  • waitUntil:'networkidle' → SSE 永不空闲,30s 超时。用 'load' + 显式 wait。
  • 弹窗 click 不触发 → 加 {force:true};等弹窗用内层元素 waitForSelector,别用 div.fixed.inset-0 的 visible 判定。
  • 整页截图元素太小 → annotated 后裁剪聚焦(CSS×2)。
  • 图床同名不覆盖 → 永远用新文件名,改 md URL。
  • 图床返回 serve.gptacademic.cn → markdown 里必须换成 serve.nutshellai.cn,否则用户得手动 "discard cdn"。
  • mkdocs nav key 漏翻译 → 中文站显示英文;用上面的校验脚本查 missing。
  • X.md + X.zh.md 并存 → i18n 默认语言冲突,build 报错;把英文挪到 X.en.md。
  • GitHub push TLS 报错/超时 → 外网,走 proxychains -q git push github main;禁止用 http_proxy 环境变量。
  • 截到密钥 → 见安全红线,必须遮蔽 + 通知轮换。
  • 污染真实数据 → 用临时项目/示例记忆演示,做完删掉(DELETE /api/projects/<id> 带 {"confirm":"<id>"})。
  • React 受控输入填不进 → inp.value= 不触发 React state;用 page.fill() 或原生 setter + input 事件(见 §1 坑①)。
  • 按文本匹配撞同名 → 侧栏按钮与弹窗标签常同名(如 "Research Graph");用更长唯一文本或 scope 到弹窗根(见 §1 坑②)。
  • select 取错隔壁 → label 包 select vs 平级,搞错会拿到相邻 select;按 options 内容反查最稳(见 §1 坑③)。
  • 截 Agent/会话创建弹窗误提交 → 只展示配置别点「创建/启动」,否则 spawn 真实会话烧 quota。
  • --strict 被「非本次」老告警 abort → grep 自己的 tutorial/NN + 确认 site/.../tutorial/NN_* 生成即可放行(见 §6)。
  • 新建分区只改了 nav → 必须同步 nav_translations(section 名)+ index.md + index.en.md,三处缺一不可。

Signals

GitHub stars
295
Forks
9
Last commit
Sep 2026
Advanced
Item type
skill
Key
mobius-write-guide-nutshellai-tech
Source
github.com/nutshellai-tech/mobius