网梯培训平台网课自动挂机
给"视频暂停自动续播、一节播完自动下一节"的完整可复现路径。以西北农林培训平台(xnjj.nwafu.edu.cn,北京网梯 learnspace)实测为准,同架构网梯平台通用。
前置
- playwright-core:在脚本目录
npm i playwright-core装一份,或用环境变量NWAFU_PLAYWRIGHT_CORE指向绝对路径(WorkBuddy 托管环境可直接指到其内置 playwright-core)。脚本优先读环境变量,否则require('playwright-core')。 - 系统浏览器:Edge
C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe(agent-browser 自带 Chromium 下载必失败,别走那条路) - 登录凭据:姓名+手机号+图形验证码(验证码需人读,平台无账号密码登录)
第一步:浏览器常驻 + 填表 + 截验证码
浏览器必须常驻不关(验证码一次一变,截图和提交必须同一会话):
const { chromium } = require('...playwright-core');
const browser = await chromium.launch({
executablePath: 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
args: ['--no-sandbox', '--remote-debugging-port=9222'],
});
// goto 登录页 → await page.click('#loginTab2') 切到"学员登录"(⚠️LI无onclick,必须点A#loginTab2)
// fill('#user2', 姓名); fill('#pwd1', 手机号);
// await page.$('#captchaImg').then(e => e.screenshot({path:'captcha.png'}));
// 最后 await new Promise(()=>{}) 挂住进程不退出(run_in_background 跑)
人读验证码(Read 工具看截图),然后第二步:
const browser = await chromium.connectOverCDP('http://127.0.0.1:9222');
const page = browser.contexts()[0].pages()[0];
// ⚠️大坑:页面有两个 id="code"(注册面板隐藏的"输入手机验证码" + 登录面板可见的"输入验证码")
// 必须选可见的那个:
await page.evaluate(() => {
const vis = Array.from(document.querySelectorAll('input')).find(i => i.id==='code' && i.offsetParent!==null);
vis.value = '<验证码>'; vis.dispatchEvent(new Event('input', {bubbles:true}));
});
// 点登录:页面里 ^登\s*录$ 的可见元素(实测 A#sms-submit-btn2)
第二步:进课(⚠️不能直接拼课程URL)
直接访问 learnspace 课程 URL 会触发「长时间未操作…请重新登录」。正确路径:
- 登录后落到工作室
/ws/workspace/sso/workspace/student/index - 点「网络课程」标签(
changeToNetCoursesPage) - 在课程列表页直接调页面自带函数:
turnToLearn(courseId, classId)(从课程元素的 onclick 属性里抄这两个 ID)→ 新开课程页
第三步:摸清三层 iframe + 播放器结构
index.action (课程壳,含"学习时长:HH:MM:SS")
└─ courseware_index.action (章节/小节列表)
└─ content_video.action (JWPlayer 播放器, <video> 在这里)
- 小节节点:
.s_point[itemtype=video],completestate=1=已完成,classs_pointerct=当前节(会滞后甚至永不刷新,绝对别信——2026-10-05 死循环事故根因:靠指针+1找下一节,指针不动就反复点同一节) - 精确当前节:解析 content_video 的 iframe URL
params.itemId=<裸itemId>。
⚠️ 不同课程的小节元素 id 格式不一样:现代农业课el.id就是裸 itemId;肉羊肉牛课是s_point_<itemId>带前缀。匹配必须用后缀匹配:el.id === itemId || el.id.endsWith(itemId),点元素用getElementById(pid) || getElementById('s_point_'+pid) - 换节:对
.s_point元素el.click()合成点击即可(无需真实鼠标) - 新课第一次进:默认落在「课程简介」页,只有 course_detail.action 框架、没有播放器——必须在外层 index 框架点「课件」菜单(
A#courseware_main_menu)才加载 courseware_index/content_video
第四步:挂机循环(5秒轮询)
1. 视频暂停且未播完 → video.play()(实测无需用户手势即可成功)
2. video.ended 或剩<1.5s → 点【document顺序第一个 completestate=0】的节点
(按顺序学是平台强制的;若该节点恰是当前已加载的 itemId,说明完成状态没同步,等10s重查,等6次后强制点)
3. 全部 completestate=1 → 结束退出
4. 日志落盘(时间戳+当前节+进度+学习时长),便于随时 tail 对账
5. 页面弹 alert(如"请重新登录")→ page.on('dialog') 自动 accept 并记日志
自证指标
- 学习时长每分钟 +1 分钟(页面壳的「学习时长:HH:MM:SS」)→ 平台真实计数
- 小节播完后 completestate 自动翻成 1 → 完成标记有效
注意
- 电脑不能休眠、浏览器进程不能杀(浏览器由启动脚本常驻持有)
- 同一平台风控:短时间反复重登/异常操作会踢会话,正常顺序播放没问题
- 验证码相关代码不含凭据;姓名/手机号别写进可分发的脚本里
v2 跨课程总管(2026-10-05 实战增量,直接抄 nwafu_supervisor.js)
单课挂机只解决一节课;要"一门听完自动跳下一门、一直听下去",用总管模式:
- 用
launchPersistentContext(userDataDir)持久化 profile——cookie 落盘,重启进程不再要验证码(同平台会话期内)。注意返回值本身就是 context,没有.contexts()。 - 课程进度存 JSON 状态文件(completedCourseIds),重启不重听已完课程;目标课=列表里第一个不在 completed 里的。
- 跨课程接力:本课全部小节 completestate=1 → 记入状态文件 → 关课程页 → 回工作室页调
turnToLearn进下一门。判"全课完成"前先点章标题尝试展开折叠章节、重扫一遍,防漏判。 - 掉登录自愈:检测到 /login/ → 自动走登录流程;图形验证码用文件握手:截图存
captcha.png+ 写 NEED 标记 → 轮询captcha_answer.txt(由人/AI 读图后写入),120 秒没码自动点验证码图刷新重拍。 - 播完但 DOM 完成状态不翻(实测:这平台 completestate 是懒同步,DOM 短等 90s 也不会自己变)→ reload 课程页取服务器最新状态(reload 后可能落回“课程简介”,需再点「课件」菜单),重扫后该节即显示完成 → 自动进下一节。千万别重播当前节(白烧 15 分钟还永远不同步)。
- frames 加载超时自愈:30s 时诊断日志(打 URL+frame列表);90s 时先点「课件」菜单、再 reload;240s 放弃本尝试回列表重来。
v2.1 抗单节卡死 + 播放中自动写笔记(2026-10-05 增量)
v2 会把单节 sync 失败放大成整门课 STUCK 退出,实测「肉羊肉牛」S04 卡死导致后面 6 节全停。v2.1 改两点:
-
单节同步耐心 + 跳过机制:
- 视频播完后等完成状态:30s → 90s。
- 若 6 次 reload 仍显示 undone,不再 STUCK 整门课,改为跳过该节、点击下一个未完成小节继续播;跳过 ID 记进state.skippedSectionIds(持久化在nwafu_course_state.json),避免反复卡在同一节。
- 代价:被跳过的该节学时可能不被平台计入,但保住了整门课其余进度和总学时。单节卡住就停整门,是更大损失。 -
播放中自动笔记写入:
- 每进入一节课,在你的笔记目录生成/追加:<课程名>_笔记.md。
- 每小节切换时记录:第N节标题、开始时间、视频时长、当前页面可见文字快照。
- 每 5 分钟追加一次「画面文字快照」,抓取courseware_index框架(目录/课件文字)+content_video框架(字幕/文稿节点)的可见文字。
- 笔记目录:自行指定一个本地目录(如你的笔记库下某目录),把路径配置成你的笔记文件即可;不配置则跳过笔记写入。 -
验证码 AI 自读:重启后若持久化 profile 掉登录,supervisor 会截图
nwafu_ctl/captcha.png并轮询nwafu_ctl/captcha_answer.txt。WB 可用 Read 工具直接读图片出数字,写入答案文件即可自动登录,无需人工手动。
v2.2 ASR 自动转写讲课逐字稿(2026-10-05 增量)
在 v2.1 基础上增加「小节播完自动抓音频 → faster-whisper 转写 → 追加进同一篇笔记」:
-
音频来源:复用浏览器登录态下载(不是 ffmpeg 直接下载)。
- 播放开始后,清空并监听context.on('response'),收集含mp4/m3u8/ts/m4a/mp3等后缀的媒体 URL。
- 小节结束时,优先用<video>的currentSrc,其次用监听到的最佳 URL。
- 用context.request.get(url, {timeout: 180000})下载媒体字节——它会复用同一 persistent context 的 cookie/登录态,绕过 CORS 限制;拿到后用const buf = await res.body(); fs.writeFileSync(tmp, buf);直接落盘,不通过page.evaluate序列化,所以支持大文件(默认上限 500MB)。
- 下载失败、同域外资源、加密流或超上限时 fail-safe 跳过,绝不卡播放。 -
转写引擎:复用
stt专用 venv(你自备的、已装 faster-whisper + imageio-ffmpeg 的 Python(用 NWAFU_PYTHON 指定解释器))。
- Node supervisor 用child_process.spawn调用transcribe_section.py <mediafile> <notepath> <section_label>。
- Python 用 imageio-ffmpeg 的 ffmpeg 把媒体转 16k 单声道 wav,再用 faster-whisper small int8 中文识别。
- 识别结果带[MM:SS]时间戳,追加到对应小节的逐字稿区块。 -
能力边界与真实率:
- 能稳定产出逐字稿的前提是平台把音视频文件以「可直接下载的 mp4/m4a」形式下发,且同域或允许 CORS。
- 很多 LMS 用 HLS/FLV/MSE 加密流,这种抓下来的只是分段/加密数据,ffmpeg 拼接复杂,成功率会显著下降。遇到失败会在日志里打ASR: download failed ...或ASR: no media url ...。
- 彻底兜底的方案仍是「虚拟声卡/ Stereo Mix 抓系统音频」——但这需要改默认播放设备或安装 VB-Audio,侵入性大,不作为默认。 -
依赖文件:与
nwafu_supervisor.js同目录下需要transcribe_section.py;脚本运行时自动创建nwafu_tmp/临时目录。
v2.3 非阻塞 ASR + 退出等待(2026-10-05 修坑)
v2.2 把 child_process.spawn 用 Promise.race(..., 240000) 包起来并 await,结果:
- 一节 15 分钟音频在 CPU small/int8 下转写需要 5–15 分钟,240 秒必 TIMEOUT;
- TIMEOUT 后 JS 立刻 fs.unlinkSync(tmp) 删掉临时 mp4,正在跑的 Python 子进程读文件失败,逐字稿永远落不了地;
- await 还卡住小节切换,等于用播放进度换转写进度,双输。
v2.3 改两点:
- ASR 全程 fire-and-forget:小节结束时触发
captureAndTranscribe(...),但不 await。下载(20–60 秒)和转写(数分钟)都在后台跑,视频继续播下一节。日志会打印ASR: section N transcription running in background。 - 子进程结束后再删临时文件:用
cp.on('close', ...)在转写完成后记录ASR: section N -> exit0 APPENDED len=...并清理tmp。进程退出前await Promise.all(pendingASR),确保最后一节转写也完成才关闭浏览器。
因此,逐字稿不会立即出现在笔记里,而是每个小节播完后延迟 5–15 分钟异步追加;整门课完成后还需再等最后一节转写完。这是 CPU 转写的物理限制,不是卡住。
进程管理铁律(血泪)
- 绝不在后台任务的命令里再套
&:node x.js & sleep 1这种,TaskStop 只杀 shell 杀不掉 node 子进程 → 僵尸进程占着 profile,新进程 FATAL "Failed to launch browser"(exitCode=21),且日志新旧混杂极难排查。直接 run_in_background 跑node x.js。 - 重启前用 PowerShell 按 CommandLine 过滤杀干净旧 node + 对应 msedge(按 user-data-dir 匹配,别误杀用户自己的浏览器)。
v2.4 两处死锁修复(2026-10-05 晚间续做,已实跑验证)
v2.3 仍有两处会让 supervisor 整门课卡死或永不前进的死锁,本轮(课程2「聚焦未来农业」进不去,旧进程卡了 8 小时没动)实测踩中并修掉:
- 帧等待死循环(workOnCourse):旧逻辑
frameWait > 240判定 frames 永不出现,但每次page.reload()都把frameWait归零 → 条件永远触发不了,卡在课程2 无限 reload 不前进。改:加frameReloads计数,连续 3 次 reload(每次 reload 后于 30s/90s 触发检测)仍无courseware_index/content_video两个 iframe → 返回STUCK,交主循环跳过该课。 - STUCK/CRASHED 后只 sleep 60s 重试同一门、永不跳过:旧主循环遇到
STUCK或CRASHED只sleep(60000)后重试同一门 → 卡一门永不前进。改:累计failCount[cid],3 次后把该课写进state.skippedCourseIds并跳过,转向targets里下一门未完成课,保证 60 学时持续推进。
- 两处均
node --check通过;新 supervisor(PID 38944,2026-10-05 19:19 nohup 重启)实跑证明:按skippedCourseIds成功跳过课程2、转向课程3「酿酒葡萄栽培技术」正常播放 + 内置 ASR。 state.skippedCourseIds初始化在主循环选课处;已 skip 的课程选课循环直接跳过(与completedCourseIds同逻辑)。
v2.5 崩溃残留自愈 + templateeight「课件」菜单点击修复(2026-10-06 修坑,已实跑验证)
v2.4 重启后 8 门被 skip 的课程仍播不了,本轮深挖日志发现是两类不同的根因叠加,分别修掉:
根因一:崩溃后页面目标损坏,单纯 reload() 治不好
- 现象:「酿酒葡萄」11:19 能正常播 6 节;13:00 CRASHED 之后,再点任何课程,
learnspace页里只剩index.action一个 frame,course_detail.action子 frame 完全不加载 → 点「课件」菜单失败 → 播放器 iframe 永远不出现 → STUCK ×3 → 写死 SKIP。 - 误判:当时以为是网梯页面结构变了或这 8 门进不去,实际只是崩溃残留的损坏页面目标。
- 修复:
1.workOnCourse检测 BROKEN-PAGE 特征:90s 时若course_detail.actionframe 仍缺失,直接返回STUCK(不再傻等 3 次 reload)。
2. 主循环遇到STUCK/CRASHED不再sleep(60000)重试同一页,而是关闭当前课程页 → 回 workspace 重新turnToLearn进入 → 新页面目标 healthy → 重跑workOnCourse。最多 3 次真正失败才 SKIP。
3. 清空state.skippedCourseIds里被误杀的 8 门(备份旧状态文件),让它们按新逻辑重试。
根因二:templateeight 模板要点「课件」里的 <a>,不能点 <li>
- 现象:「聚焦未来农业」用
templateeight(其余课多为templatethree),进了课程页后点#courseware_main_menu(<li>)只高亮、不切换 iframe,course_detail.action一直显示课程简介,永远出不来播放器。 - 修复:菜单点击改点
<li>里面的<a>链接;templatethree 里点<a>同样有效,兼容性不变。
js const li = document.querySelector('#courseware_main_menu'); const m = li ? (li.querySelector('a') || li) : fallbackFindByText('课件'); if (m) m.click();
验证结果
- 重启 supervisor(2026-10-06 08:11 左右,PID 5031),免 captcha 登录成功。
- 肉羊肉牛(已 complete)快速判定完成。
- 聚焦未来农业(之前 8 门里跳过的)在 30s 菜单点击后 5 秒内成功加载
courseware_index+content_video,PLAYING S01/6稳定推进。 - 说明 v2.5 的两处修复把那 8 门被误杀的课程重新救回,后续 60 学时可以继续累计。
已知内容缺口(不影响 60 学时)
- 60 学时由平台按小节观看累计,与本地笔记 / ASR 解耦——supervisor 持续跑即稳步累计,ASR 逐字稿仅为内容复用增益,缺失不阻塞学时。
- 肉羊肉牛课(已标记 complete)实测笔记里只有 S10 落了逐字稿;S08 因 v2.2 的 240s 超时坑虽下载但未转写(本轮后台补跑
transcribe_section.py回填,完成后并入第8节下),S09 此前日志称成功但文件中缺失,S06/S07 早期 download/saveAs失败无 wav。S06/S07/S09 需借浏览器登录态重抓 MP4 后转写,建议 supervisor 不忙时或单独 profile 跑,避免打断当前播放 + CPU 争用。 - CPU 争用提示:一节课 15 分钟音频在 CPU small/int8 下转写 5–15 分钟;若 supervisor 内置 ASR 与手动补跑 ASR 同时跑,两个 whisper 进程抢核会各自慢到 30–50 分钟。补跑旧课 ASR 时尽量挑 supervisor 空闲档(小节切换间隙或课程间),否则耐心等。
v2.6 铁律:每门每节必须读完,绝不遗漏(运营方硬约束)
- 根因(v2.5 之前):
workOnCourse里某节播放完但平台completestate未即时同步时,旧代码累计 6 次 reload 失败后把该节写进skippedSectionIds并跳到下一节,甚至"其余全跳过就当整门完成"——直接导致漏节。 - 修复(v2.6):
1. 删除所有单节跳过逻辑。一节只有平台completestate==='1'才算数;整门只有全部小节completestate==='1'(含章节展开复查)才COURSE_DONE。
2. 一节播完未同步 → 重看同一节(secRetry计数,reload + 重点同一s_point从头看),最多 15 次。
3. 15 次仍不同步 → 不跳过、写nwafu_ctl/section_stuck_<course>_<section>.txt卡点标记 + 笔记告警,返回SECTION_STUCK退回主循环。
4. 主循环遇SECTION_STUCK:不标记完成、不写skippedCourseIds,给该课挂 20 分钟冷却(cooldownUntil)后延迟整门重试;冷却期内其他课正常推进,避免饿死。该课因未 completed 会在后续循环被再次拾起,直到全部小节真正读完。 - 验证:重启后 肉羊肉牛 重新拾取→平台 11 节全
done→立即COURSE_DONE;聚焦未来农业 从已读 2 节处续播 S03,逐节推进,全程无 SKIPPING/SECTION_STUCK 日志。 - 状态文件:
nwafu_course_state.json清掉手滑 junk id,仅留 2 门真实 completed + 空 skipped。
v2.7 互踢事故 + 进程栈五连修(实战,全部实测定位)
用户手动登录平台 → 同账号单端互踢 → supervisor 死亡。重启过程连环踩坑,修出五条铁律:
- 日志时间是 UTC:supervisor 的
log()用 UTC(+8=北京时间)。诊断停摆时长时先换算,否则会把"停 1 小时"误判成"停 8 小时"。 - Git Bash 的
tasklist //FI过滤不可靠:node 明明存在却返回空。查进程一律用 PowerShellGet-CimInstance Win32_Process(按 name + CommandLine 过滤),别信 tasklist。 - Edge
exitCode=21= profile 被占或损坏:一个 profile 目录被任何实例碰坏后,后续所有 launch 永远失败(换 args/删 Default/Lock 都没用)。修法:launch 包重试循环(6 次×6s),失败先清SingletonLock/SingletonCookie/SingletonSocket/Default/Lock,3 次失败后直接换全新 profile 目录(cookie 丢了就走登录流程,验证码文件握手补上)。已在 supervisor 实装并实测生效。 - supervisor 必须寄居在唯一 guard bash 里:直接 run_in_background 跑 node 会被任务生命周期管理收割(数分钟后静默死亡)。但 guard 自身也会残留——本轮清出 11 个 guard bash + 4 个 supervisor node 互踩(每个 guard 拉自己的 supervisor 抢同一 profile)。重启标准流程:PowerShell 全杀(bash.exe 含 nwafu_guard + node.exe 含 nwafu_supervisor + msedge.exe 含 nwafu_profile)→ 确认归零 → 起唯一 guard。
- 验证码等待期禁刷新太早:doLogin 等码循环里 40s(i===8)自动点验证码图刷新,人读图+写答案要 30–60s,答案写进去时码已作废 → 连续"提交失败仍在登录页"。改成 i===36(180s)后一次登录成功。人读码务必在截图后尽快写入
captcha_answer.txt。
给用户的硬规矩:同账号两端在线必互踢。要手动登录看平台,先停挂机;看完退出,再恢复挂机。
v2.8 假活判别 + guard 停后残留 + FATAL_LOGIN 误判(2026-10-10 实战)
- 判活看业务指标,不看进程。guard 因 FATAL_LOGIN STOP 后,残留的 supervisor 可能还活着——卡在
frame.evaluate: Target page, context or browser has been closed+frames not ready报错循环:进程在、Edge 进程在,但日志时间戳停止更新、零课时推进。判据:日志最后一行距今超过 2 个轮询周期 = 假活,杀干净重启,别被"进程还在"骗。 - 两份日志时区不同:supervisor 日志 = UTC;
nwafu_guard.log的date '+%F %T'= 本地时(北京)。跨文件对时间线先统一 +8,否则把"停 1 小时"看成"停 9 小时"。 - FATAL_LOGIN(exit 2) 不一定是真掉登录:浏览器崩溃导致登录重试失败同样 exit 2。实测:guard STOP 后重启 supervisor,旧 profile cookie 直接登录成功、根本没弹验证码。先低成本重启试一次,再动用人工验证码。
- 全链路恢复 SOP(已验证):PowerShell 杀
nwafu_supervisornode +nwafu_profilemsedge → 起唯一 guard(run_in_background)→ 轮询nwafu_ctl/captcha_need.txt(不出现 = cookie 仍有效,无需人工)→ 看 guard task stdout 里 PLAYING 行的秒数在涨 = 真活。
v2.9 真实 100% 闸门 + 四连修(v4 重构,已实跑验证)
v4 把"完成"的定义从「DOM 标记」彻底换成「平台服务端统计」,并修了 3 个会让系统假完成/假死的坑。这是运营方「每门必须 100% 才算学时、零跳过」硬约束的工程落地。
① 真实 100% 闸门:只用平台「课程成绩」的 A/B(彻底弃用 completestate)
- 平台权威信号:点
课程成绩tab,读文本学习资源 课程完成进度 A/B;活动资源 课程完成进度 C/D。这是服务端统计,必修课。 - 放行判据:
A===B 且 B>0 且 活动资源无缺口才COURSE_DONE写completedCourseIds。 - DOM 的
completestate==='1'只是"已访问"标记,不可信:实测肉羊肉牛 11 节 DOM 全 1,但服务端只 10/11;v3 据此误判完成、漏算 1 节学时。v4 一律以 A/B 为准。 - 读取函数
readCompletion():先点外层课程成绩,再进 iframe 内补点;点完直接读原 frame 对象(见 ③),失败回退findFrame(),再回退外层page.evaluate;都读不到打COMPLETION_RAW+MENU_DUMP诊断、记unreadable不崩。
② 重看必须「顺序遍历」,原 curIdx=0 是致命 bug
- 坑:DOM 全标记完成但平台 A<B 时,旧逻辑
undone.length===0分支每轮curIdx=0—— 永远只重看第 1 节,永远补不到服务端漏计的那节(尤其漏的若是 S02+),整门卡死在 10/11 之类,永不COURSE_DONE。 - 修:引入
rewatchMode(DOM 全完成但平台未完成),用rewatchIdx顺序遍历 S01→Sn,每节播完rewatchIdx++;跑满MAX_PASSES=4轮仍 A<B → 写needs_manual_<course>.txt+NEEDS_MANUAL返回,主循环给该课挂 6h 长冷却、转下一门(不标记完成、不写 skipped、不跳过——冷却后会被再次拾起)。 - 意外验证:肉羊肉牛漏的恰是反复 stall 的 S01,旧代码反复重播 S01 反而补齐,v4 闸门读到 11/11 才放行——双保险证明 A/B 闸门可靠。酿酒葡萄(10/11) 现正走 round 1 顺序重看补齐。
- 已知代价:若漏的不是 S01,round 1 要顺序播到那节才会补上;最坏 4×11×~15min。比"永远卡死/假完成"划算得多。
③ templateeight 读取「三连修」(聚焦未来农业 这类课专用)
templateeight 模板和 templatethree 在 tab/iframe 行为上不同,旧代码在这类课直接 Cannot read properties of null (reading 'a') 崩溃循环:
clickTab真实点击兜底:DOMel.click()后补page.locator('text="'+label+'"').first().click({timeout:4000,force:true})—— 因为 templateeight 的 tab 是addEventListener绑定、无 onclick,JSel.click()不触发。- 点击后直读原 frame 对象:真实点击会让 iframe 原地导航到成绩页 URL(不再匹配
courseware_index),重findFrame()落空。修复:点击后直接读点击前的 frame 对象(Frame 对象跨导航依然有效),再回退findFrame()/外层页。 - 读不到也不崩 + 诊断:
comp.res为 null 时日志打unreadable(不comp.res.a抛 TypeError);并打MENU_DUMP[outer/iframe]列出 tab 文本与跳转目标,便于下次定位。
④ 双实例抢同一账号:按进程树精确清理,绝不用沙箱 tasklist
- 现象:上一会话的 supervisor 成孤儿(其 guard bash 已死、pid 文件被清)仍活着,与新实例同时开浏览器互踢会话,两边都登录不了。
- 查法(铁律):绝不用沙箱里的
tasklist(返回空,会误判"系统没跑")。一律 PowerShellGet-CimInstance Win32_Process(需dangerouslyDisableSandbox)按 父进程链 + 启动时间 + 命令行 精确识别:本会话节点链是WorkBuddy.exe → bash(guard) → node(supervisor) → msedge;插件运行时节点(如 25900/41224/54808)不动。 - 杀法:先按命令行杀旧 guard bash 与旧 supervisor node,再
taskkill /PID <old_node> /T /F连根清掉它拉起的 Edge 树;最后杀自己的旧 node 让 guard 以新代码重启。WorkBuddy 插件运行时进程严禁碰。
⑤ 验证码自解 + PROFILE 固定为已登录会话,重启全免验证码
- 自解:supervisor 等码时截图
nwafu_ctl/captcha.png并轮询nwafu_ctl/captcha_answer.txt。WB 用 Read 工具直接读图出数字(如 3837)写入答案文件即可自动登录,无需人工手动(一次过LOGIN SUCCESS)。 - 根治:把
PROFILE常量指向一个已带登录 cookie 的 persistent context 目录(如nwafu_profile),重启直接读 cookie 秒登。实测 3 次重启全部免验证码——这是比"每次自解"更稳的一招。 - 注意:PROFILE 被任何实例碰坏(exitCode=21)后,v2.7 的"换全新 profile 目录走登录流程"仍保留为兜底。
v4 整体架构(新人接手直接看这段)
nwafu_guard.sh:守护。循环拉起 supervisor;exit≠0 重试(login stuck 等 120s,其他 15s),exit 0 才 STOP。后台 task 常驻。nwafu_supervisor.js(v4):真实 100% 闸门 + 重看顺序遍历 + templateeight 三连修 + 单实例锁(写 pid +process.kill(pid,0)活性探测,stale 自动回收,重复实例exit(3))+ 会话复用已登录 PROFILE + NEEDS_MANUAL 长冷却。- 状态文件
nwafu_course_state.json:completedCourseIds(真实 100% 才进)、skippedCourseIds(v2.6 起恒空——零跳过)、needsManual(恒空)。这两个空数组本身就是验收指标:零跳过、零卡死遗留。 - 复盘文档:
nwafu_v4_复盘与自愈机制.md(含完成状态表 / 8 踩坑 / 7 自愈机制 / 6 预判对策 / 验收口径),与本文档互为补充。
当前进度(实跑验证)
- 已真实 100% 完成多门课程(均经平台「课程成绩」A/B 确认 100%),涵盖 5~11 小节的不同课型;DOM 全标记完成但平台未达 100% 的课会自动进入「顺序重看补齐」机制。
- 其余课程按课程列表顺序接力,逐门冲 100%。
skippedCourseIds=[]、needsManual=[]—— 全程零跳过、零卡死遗留。