agentsclimarketplace

Msh skill

Skill JiHuiYiYou/msh-skill

名师汇出国自动化工具集。两个功能:(1) 模考错题分析——用户提供 mingshihuichuguo.com/homework/student/<id> 网址,按"手写版"人味风格产出阅读/听力错题分析 txt;(2) 单词听写自动化——用户要求完成 list 听写/单词测试时,用 playwright-cli + run_v5.js 自动答题。根据用户意图自动判断用哪个。When the user mentions 错题分析 / 模考 / 听写 / 单词 / list / mingshihuichuguo URL, use this skill.From its SKILL.md

Install
npx -y skills add JiHuiYiYou/msh-skill

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

4 things to look at

  • skips confirmationTells the agent to proceed without asking first, 3 times: "缓存的浏览器二进制还在 → 直接用,不探测不询问" and 2 more.
  • 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
  • runs commandsInstructs the agent to run 8 commands, including `playwright-cli --s=default eval` and 7 more.
  • fetches URLsInstructs the agent to fetch 3 URLs, including https://mingshihuichuguo.com/homework/student/<id> and 2 more.

SKILL.md

20.5 KB, ~7.0k tokens by cl100k_base, as published. Nobody here has run it

MSH 名师汇出国 自动化工具集

两个独立功能,根据用户请求自动判断用哪个:

用户说走哪个
"错题分析" / "模考" / 粘贴 homework/student/<id> URL→ 模考错题分析
"听写" / "单词" / "list" / "单词测试" / homework-submission-items?homework_id=<id>→ 单词测试自动化

功能一:模考错题分析

针对 mingshihuichuguo.com/homework/student/<id> 的模考作业,按学生 本人手写习惯输出两份错题分析 txt 到桌面:

  • 阅读.txt
  • 听力.txt

只写阅读和听力。写作 / 口语不在范围内(用户不要求)。

触发

  • "做一下模考错题分析" + 给 URL
  • "分析一下这次的错题" + 给 URL
  • 直接粘贴 https://mingshihuichuguo.com/homework/student/<id> 网址

注意:粘贴错题内容(不含 URL)不触发,必须给完整 URL。

速度原则(压时间核心)

  1. 少 dialog:浏览器探测结果 / 重启确认都缓存到 ~/.msh-skill-state.json,下次不重复问
  2. 少 click:用 eval 一次拿整段 innerText,不逐题 snapshot
  3. 少 sleep:sleep 2 改 sleep 1;能并行的 bash 命令并一行
  4. 少 round trip:section 切换后立刻一个 eval 拿全部,不分题 click

目标:10 分钟内完成(含浏览器重启等待 ≤ 8s)。

硬禁令(违反 = 重做)

  • 不准用 playwright-cli snapshot——会落盘 yml、跨进程 ref 失效、消耗 1-2s/次。一律用 eval
  • 不准逐题 playwright-cli click "e<NNN>"——逐题 click 是惯性陷阱,每个 click 1-2s + 等 DOM 1s = 3s/题,15 道题 45s 起步
  • 不准用 tab-select——跨进程 ref 不稳,用 eval 找带 /homework/student/ 的 a 标签 click
  • 不准 sleep 2/sleep 4——sleep 1 足够;能并行的 bash 并一行

怎么知道刚才走错路了

  • 跑了 5 次以上 playwright-cli --s=default click → 走错路,回去重抓
  • 跑了任何一次 playwright-cli --s=default snapshot → 立刻停,回去换 eval
  • 写了 playwright-cli tab-select → 删掉,换 eval 找链接

抓取模板(直接复制,4 段就够)

下面 4 段 eval 代码覆盖 90% 工作流。先复制,再按需改

T1:找作业详情 tab 并点进去

playwright-cli --s=default eval "() => {
  const a = document.querySelector('a[href*=\"/homework/student/\"]');
  if (a) { a.click(); return 'clicked: ' + a.href; }
  return 'not found, current: ' + location.href;
}"

T2:切 section 按钮

playwright-cli --s=default eval "() => {
  const b = Array.from(document.querySelectorAll('button')).find(b => b.textContent.trim() === '阅读M1');
  if (b) { b.click(); return 'clicked 阅读M1'; }
  return 'not found';
}"

'阅读M1''阅读M2' / '听力M1' / '听力M2' / '答题解析' 都行。

T3:拿当前 section 错题列表(无 click)

playwright-cli --s=default eval "() => {
  const items = Array.from(document.querySelectorAll('div'))
    .filter(d => /^\(\d+\)\s*[✓✗]$/.test(d.textContent.trim().replace(/\s+/g, '')))
    .filter(d => !d.querySelector('div'));
  const seen = new Set();
  return items.map(d => {
    const m = d.textContent.match(/\((\d+)\)\s*([✓✗])/);
    return m ? {n: +m[1], wrong: m[2] === '✗'} : null;
  }).filter(Boolean).filter(i => seen.has(i.n) ? false : (seen.add(i.n), true));
}"

返回 [{n:1, wrong:true}, ...]这次 eval 不点任何东西,纯读。

T4:一次性 click 全部错题 + 收集右栏 innerText

playwright-cli --s=default eval "(wrongNums) => new Promise(async resolve => {
  const results = {};
  const allItems = Array.from(document.querySelectorAll('div'))
    .filter(d => /^\(\d+\)\s*[✓✗]$/.test(d.textContent.trim().replace(/\s+/g, '')))
    .filter(d => !d.querySelector('div'));
  for (const n of wrongNums) {
    const item = allItems.find(d => d.textContent.includes('(' + n + ')'));
    if (!item) { results[n] = 'NOT FOUND'; continue; }
    item.click();
    await new Promise(r => setTimeout(r, 400));
    const detail = document.body.innerText;
    const start = detail.search(/(题目前引导|题干|得分)/);
    let end = detail.length;
    const nextMatch = detail.slice(start + 50).match(/\n\(\d+\)\s*[✓✗]/);
    if (nextMatch) end = start + 50 + nextMatch.index;
    results[n] = detail.slice(start, end);
  }
  resolve(results);
})" '[1,8,17]'

调用方式:'<上面整段代码>' '[1,8,17]' —— 外层引号包代码,第二个参数是 JSON 数组字符串。

T5(听力专用):一次性触发所有"原文" popover 拿 transcript

playwright-cli --s=default eval "(wrongNums) => new Promise(async resolve => {
  const tr = {};
  const allItems = Array.from(document.querySelectorAll('div'))
    .filter(d => /^\(\d+\)\s*[✓✗]$/.test(d.textContent.trim().replace(/\s+/g, '')))
    .filter(d => !d.querySelector('div'));
  for (const n of wrongNums) {
    const item = allItems.find(d => d.textContent.includes('(' + n + ')'));
    item.click();
    await new Promise(r => setTimeout(r, 400));
    const btn = Array.from(document.querySelectorAll('button, div, span'))
      .find(e => e.textContent.trim() === '原文' && e.children.length === 0);
    if (btn) {
      btn.scrollIntoView({block: 'center'});
      btn.click();
      await new Promise(r => setTimeout(r, 300));
      const pop = Array.from(document.querySelectorAll('.el-popover'))
        .find(p => getComputedStyle(p).display !== 'none' && p.offsetHeight > 0);
      tr[n] = pop ? pop.innerText : '(no popover)';
    } else {
      tr[n] = '(no 原文 btn)';
    }
  }
  resolve(tr);
})" '[5,14,26,27]'

模板使用顺序(每个 section 都走这一遍)

T2 切 section →  T3 拿错题列表 →  T4 拿右栏 innerText →  T5 拿 transcript(仅听力)

4 个 section 一共 4 × (1 T2 + 1 T3 + 1 T4 + 0/1 T5) = 12-16 次 eval,0 次 click,0 次 snapshot

准备工作(快进模式)

Step 0:URL 校验(一次正则)

从用户消息里抽 homework/student/(\d+),抽不到就自然语言追问一次

"请粘贴完整的 mingshihuichuguo.com 作业链接,格式:https://mingshihuichuguo.com/homework/student/<数字>"

合法:https / http 都行,纯数字 id。追问第二次还错就退出。

Step 1:读缓存 ~/.msh-skill-state.json

文件不存在就跳过这一步。文件存在则读 {browser, lastRestartAck}

  • 缓存的浏览器二进制还在 → 直接用,不探测不询问
  • 缓存的浏览器二进制不在了 → 走 Step 2 探测

Step 2:环境探测(仅缓存缺失时跑)

OS:uname -s(失败 fallback node -e "console.log(process.platform)")。

浏览器探测按 OS 走(只跑第一个找到的两个就停):

  • Windows:依次 ls Edge/Chrome 常见安装路径
    ls "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe" 2>/dev/null
    ls "C:/Program Files/Microsoft/Edge/Application/msedge.exe" 2>/dev/null
    ls "C:/Program Files/Google/Chrome/Application/chrome.exe" 2>/dev/null
    
  • macOS:ls /Applications/Microsoft Edge.app /Applications/Google Chrome.app
  • Linux:which msedge microsoft-edge google-chrome google-chrome-stable

判定:

  • 两个都有 → 一次 AskUserQuestion 问;回答后写缓存
  • 只有一个 → 自动选,写缓存
  • 都没有 → 退出,引导用户装

Step 3:依赖 & CDP 端口检查(一次性 bash)

npx playwright-cli --version
ls "$USERPROFILE/.playwright/cli.config.json" 2>/dev/null
curl -s --max-time 2 http://127.0.0.1:9222/json/version
  • playwright-cli 不在 → npm install -g @playwright/cli只这一次
  • cli.config.json 不在 → 按浏览器写一份(已存在不覆盖)
  • 9222 端口不活 → 见 references/数据获取.md 重启浏览器

Step 4:写缓存

跑完跑一次:

node -e "require('fs').writeFileSync(require('os').homedir()+'/.msh-skill-state.json', JSON.stringify({browser:'msedge',lastRestartAck:new Date().toISOString()},null,2))"

替换 msedge 为实际浏览器名。

重启浏览器的确认

只在第一次(缓存里没 lastRestartAck)问一次:

"要附加到 Edge 抓数据,需要带 --remote-debugging-port=9222 重启 Edge。会杀掉所有 Edge 进程(未保存 tab/表单会丢),重启后会恢复 tab 和登录态。"

确认后跑一次;之后同会话内不重复问。下次有缓存直接跳过。

流程

1. 拿数据(核心优化点)

参考 references/数据获取.md 的"快进抓取"小节:

  • attach playwright-cli 一次
  • 每个 section 切进去后,一发 eval 拿整段右栏 innerText
  • 听力题的 transcript 一次性循环触发所有 "原文" popover 再 innerText 拿全
  • 错题列表 / 题干 / 选项 / 学生答案 / 正确答案 都在 innerText 里

2. 写报告

输出位置(用户桌面):

~/Desktop/阅读.txt
~/Desktop/听力.txt

只分析错的题。对的题不写,不总结"对了几道/错了哪几道"。

3. 收尾

告诉用户两个文件路径(仅此一句,别复述做了什么)。

Self-check(写报告前必过)

写报告前先 bash 数一次这次跑的 playwright-cli 命令分布:

# bash 历史可能没用,改用:scan 之前所有调用的命令记录
# 简化:自己数一下
echo "playwright-cli click count: <自己数>"
echo "playwright-cli snapshot count: <自己数>"
echo "playwright-cli tab-select count: <自己数>"

过线标准

  • playwright-cli --s=default click 调用次数 ≤ 4(最多 4 个 section 各 1 次 T2 切 section)
  • playwright-cli --s=default snapshot = 0
  • playwright-cli --s=default tab-select = 0

任意一项超线 = 回去重抓。问自己:

  • click 超过 4 次 → 用了 T4 模板没?没用回去用
  • snapshot > 0 → 立刻删 snapshot 那次,回去换 eval
  • tab-select > 0 → 改 T1 eval 找链接

写完报告再 grep 一次桌面文件长度(wc -l 两份),过短说明数据没抓全。

风格硬要求(不达标 = 重写)

完整对照见 references/style-guide.md。最关键的几条:

  1. 不要大段原文。最多贴 1-2 句关键引文,长文只写一句摘要。
  2. 只分析错的那一项。不写 A→B→C→D 全覆盖。
  3. 表述口语化。"脑子一抽就想到..."、"被 X 带跑了"、"只听到 X 后面没注意"。
  4. 生词随便一点。一行一个,词 + 中文意思就行,不写音标不写三栏。
  5. 不用人类打不出的符号。避开 → ✔ ✘ ─ 【】 ■ 这种。空行和标点就够了。
  6. 少用 /。除了写网络短语可以接受,其它地方别堆 /
  7. 填词题只要简短中文翻译,不需要完整答案,不需要抄题目。
  8. 不写正确率 / 错了几个
  9. 文件名越简单越好阅读.txt,别带"第五次模考-真题16-"这种长前缀)。
  10. 不抄题。题干用自己话说出来,不复制粘贴原题文字。

题型处理

词汇填空

  • 一句错因(怎么填错的 / 应该是哪个)
  • 简短中文翻译(每段一两句意译,不用逐句雕琢)
  • 生词一行一个:词 + 中文

不要:完整答案列表、抄题目、原文整段贴、生词三栏字典。

阅读(生活类 / 学术类)

  • 一句错因(口语化)
  • 只写错的那个选项的判断 + 1-2 句为什么
  • 学术类可以加一句"主旨"和"框架",但要短
  • 不写长段引文

听并回应

  • 原文只写 prompt 那一行
  • 一句错因
  • 只写错的那个选项的判断

对话 / 讲座 / 公告

  • 原文只写一句中文摘要 + 1-2 句关键引文(不贴整段 transcript)
  • 题干自己说一遍,不抄
  • 一句错因
  • 只写错的那个选项

关联资源

  • references/style-guide.md — 风格正反例
  • references/数据获取.md — 拿 URL → 抓数据的流程(含快进抓取代码段)
  • references/模板对照.md — 同一道题,手写版 vs AI 版的对比
  • 手写错题分析/ — 5 份原始手写样本

功能二:单词测试自动化

自动完成 mingshihuichuguo.com/homework-submission-items?homework_id=<id> 的单词"混合测试"(听音译中 + 看词译中)。

触发

  • "帮我完成 list11 的听写"
  • "做一下 list12、list13 的单词测试"
  • 粘贴 homework-submission-items?homework_id=<id> 链接
  • "单词作业 / 听写 / 单词测试"

流程

1. 连接浏览器

# Edge 需带调试端口启动
powershell -Command "Stop-Process -Name msedge -Force; Start-Process 'msedge' -ArgumentList '--remote-debugging-port=9222'"
# 连接
playwright-cli attach --cdp=http://localhost:9222

1.5 脚本准备(一次性,幂等)

npx skills add 只装 SKILL.md 进 skill 目录,不会装 run_v5.js。第一次跑前要落地脚本:

  1. 检查 $HOME/.agents/skills/msh-exam-analysis/run_v5.js 是否存在
  2. 不存在 → Read 本 SKILL.md 末尾的 ## run_v5 内嵌版本 章节
  3. 把整段 JS 代码 Write 到 $HOME/.agents/skills/msh-exam-analysis/run_v5.js
  4. 后续调用一律用 --filename=$HOME/.agents/skills/msh-exam-analysis/run_v5.js 绝对路径,不再依赖 cwd

如果改了 SKILL.md 末尾的内嵌版本,重新 Write 一遍覆盖文件即可(保留 source-of-truth 在项目根 ~/Desktop/coding/msh-skill/run_v5.jsSKILL.md 末尾内嵌区块,保持两份同步)。

2. 打开页面 + 跑脚本(一发 bash,禁止中间 round trip)

playwright-cli goto "https://mingshihuichuguo.com/homework-submission-items?homework_id=<ID>" && playwright-cli run-code --filename="$HOME/.agents/skills/msh-exam-analysis/run_v5.js"

&& 串行:goto 阻塞等页面 ready 后才进 run-code,两条命令之间的间隙被压成 0

✅ 一发 bash 后脚本开始跑,30 秒-3 分钟OK: N/40

❌ 不准在 goto 和 run-code 之间发任何额外命令(eval / console / snapshot 都是浪费 round trip)。脚本会自己处理首次加载,等 waitForTimeout(1200) × 第一题足够。

3. 提交(一发 eval 搞定,禁止走弯路)

playwright-cli --s=default eval "async () => {
  const btn = Array.from(document.querySelectorAll('button'))
    .find(b => b.textContent.trim() === '提交');
  if (!btn) return 'NO SUBMIT BTN; body=' + document.body.innerText.split('\\n').slice(0,15).join(' | ');
  btn.click();
  await new Promise(r => setTimeout(r, 1500));
  return document.body.innerText.includes('测试完成') ? 'submitted OK' : 'clicked, awaiting';
}"

跑完这发 eval 直接汇报用户,不要再 round trip 确认。

功能二硬约束(衔接空隙专用)

run_v5.js 返回 OK: N/40 那一刻起,agent 行为严格限定:

  • ✅ 立即一发 eval:点提交 + 等"测试完成"出现,一把搞定
  • 不准 playwright-cli console——page console 的 LOG 全是站点自己的噪音,脚本里 console.log 不会冒到 playwright 的 console 流里,这条命令稳 0 命中
  • 不准 eval 先查"已答题数 40/40"——40/40 是脚本自己保证的,agent 多查一次只是浪费 round trip
  • 不准 eval 单独找确认对话框——提交后"测试完成"页直接出现,没有弹窗
  • 不准 playwright-cli snapshot——跟功能一的禁令同理,eval 一把搞定的事别落盘 yml

4. 多个 list

每个 list 重复步骤 2-3。不同 list 的 homework_id 从 homework 列表页获取。

脚本原理(run_v5.js)

1. page.evaluate 注入 window.Audio 构造器 hook → 抓音频题的英文单词
2. DOM 遍历 textarea 前 siblings → 抓文本题的英文单词
3. page.context().request 调有道词典 API → 获取中文翻译
4. page.getByRole('textbox').fill(翻译) → 填入答案
5. page.getByRole('button', {name: /下一题|下一步/}).click() → 下一题

关键踩坑

  1. 输入框是 <textarea> 不是 <input>querySelector('input') 永远 null,必须 querySelector('textarea, input[type="text"]')
  2. 不用 page.addInitScript + page.reload()——reload 触发反作弊弹窗("您已离开考试页面"),所有 click/fill 被拦截。改用 page.evaluate 在当前页注入 hook,不 reload
  3. 不用 page.evaluate + nativeInputValueSetter 填值——值设进去了但页面 Livewire/Alpine 不认,答案不保存。必须用 page.getByRole('textbox').fill()
  4. play button locatorpage.locator('button').filter({has: page.locator('img')}).first() 不稳定,可改用 page.$$('button') + btn.$('svg')btn.$('img') 找播放按钮
  5. 第一题偶尔漏:页面自动播放音频时,Audio hook 可能还没注入。可增大首次等待时间到 2-3 秒

run_v5 内嵌版本

npx skills add 不装 JS 文件,agent 按"### 1.5 脚本准备"说明的步骤把这段代码 Write 到 $HOME/.agents/skills/msh-exam-analysis/run_v5.js。改脚本:项目根 run_v5.js 和这里两端保持同步——代码块边界用 js 起始 + 结束,agent 提取时按这两个标记剥壳。

// V5: No reload - inject Audio hook via evaluate, use getByRole('textbox').fill()
async (page) => {
  const ctx = page.context();

  async function tr(word) {
    try {
      const u = 'https://dict.youdao.com/jsonapi?q=' + encodeURIComponent(word) + '&le=eng&dicts=%7B%22count%22%3A99%2C%22dicts%22%3A%5B%5B%22ec%22%5D%5D%7D';
      const r = await ctx.request.get(u);
      const d = await r.json();
      const raw = d?.ec?.word?.[0]?.trs?.[0]?.tr?.[0]?.l?.i?.[0] || '';
      return raw.replace(/^[a-z]+\.\s*/i, '').split(';')[0].split(',')[0].trim();
    } catch(e) { return null; }
  }

  // Inject Audio hook WITHOUT reload - works for current page session
  await page.evaluate(() => {
    const OrigAudio = window.Audio;
    window.Audio = function(src) {
      window.__word = (src || '').match(/audio=([^&]+)/)?.[1] || null;
      return new OrigAudio(src);
    };
  });

  let ok = 0;

  for (let i = 0; i < 40; i++) {
    await page.waitForTimeout(1200);

    let word = await page.evaluate(() => {
      const w = window.__word;
      window.__word = null;
      if (w) return w;
      const inp = document.querySelector('textarea, input[type="text"]');
      let el = inp;
      while (el && el !== document.body) {
        const children = el.parentElement ? Array.from(el.parentElement.children) : [];
        for (const child of children) {
          const t = (child.textContent || '').trim();
          if (/^[a-zA-Z][a-zA-Z-]*[a-zA-Z]$/.test(t)) return t;
        }
        el = el.parentElement;
      }
      return null;
    });

    if (!word) {
      try {
        await page.locator('button').filter({ has: page.locator('img') }).first().click({ timeout: 3000 });
        await page.waitForTimeout(3000);
        word = await page.evaluate(() => {
          const w = window.__word;
          window.__word = null;
          return w;
        });
        if (!word) {
          word = await page.evaluate(() => {
            const inp = document.querySelector('textarea, input[type="text"]');
            let el = inp;
            while (el && el !== document.body) {
              const children = el.parentElement ? Array.from(el.parentElement.children) : [];
              for (const child of children) {
                const t = (child.textContent || '').trim();
                if (/^[a-zA-Z][a-zA-Z-]*[a-zA-Z]$/.test(t)) return t;
              }
              el = el.parentElement;
            }
            return null;
          });
        }
      } catch(e) {}
    }

    console.log('Q' + (i+1) + ': ' + (word || '??'));

    if (word) {
      const translation = await tr(word);
      if (translation) {
        await page.getByRole('textbox').fill(translation);
        ok++;
        console.log('  -> ' + translation);
      }
    }

    try {
      await page.getByRole('button', { name: /下一题|下一步/ }).click({ timeout: 2000 });
    } catch(e) {}
    await page.waitForTimeout(700);
  }

  return 'OK: ' + ok + '/40';
}

What ships with it: 13 files

52.1 KB alongside SKILL.md, 1 of them executable

references/

手写错题分析/

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.