浏览器到 API 的强制演进
本 skill 只要以浏览器执行远端业务动作,就必须先读并遵守 浏览器到 API 强制演进。登录、验证码、2FA、纯视觉验收可保留浏览器;认证后的可接口化动作仍须抓取、复现、独立回验,并在同一任务自更新本 skill 与脚本。
第三方平台扫码登录
前置规范:API 优先,自我演进
执行本 skill 前先读本节,本节优先于下文的浏览器步骤。
- 有接口先用接口:已确认的接口或脚本(本 skill 的 scripts、下文或“API 化进度”里记录的)优先调用。浏览器只用于四种情况:登录、扫码/验证码/2FA 交给用户、接口未知的步骤、最终页面核验。
- 没有接口就边做边研究:某一步仍只能点页面时,先开
Network.enable,再用page.events()抓写请求,只记 URL、方法和 body,不记 Cookie、Token 或密码,存到任务临时目录。做完后自己分析出能复现的接口,用只读接口或回读页面验证,再写进脚本或本 skill;浏览器路径保留作兜底。 - 维护进度:本 skill 里维护“API 化进度”:已确认接口(用途、方法路径、body 模板、验证方法)和待抓步骤。每次执行至少推进一项,回复里写一句进度;全部改成接口后,把本节改为维护说明。
- 安全边界:写接口只在本次任务的目标对象上验证,不拿无关数据试错;会触发写入的 GET 不预取;凭据只从环境变量或私有配置读取,不打印。
用 ego-lite 打开目标站登录页,抓取标准微信 OAuth 码、公众号二维码、微信小程序码或站内二维码并推送给用户,用户手机扫码确认后接管验证登录结果。
全程在 ego 隔离空间后台完成:不 open_application、不激活窗口、不弹桌面通知。OAuth 可能使用 popup 或当前 tab,公众号码在站内页面,CSDN 使用站内 iframe;始终保留平台创建的真实上下文,不能猜或复制 OAuth URL。
站点矩阵(2026-08-29 实测)
“二维码确认”只表示已真实点击并看到可扫描二维码;“完整登录确认”表示已经完成用户扫码和站内登录态复核。
| 平台 | 结果 | 登录页与微信入口 | 二维码形态 / 限制 |
|---|---|---|---|
| 掘金 juejin | 完整登录确认 | https://juejin.cn/;可见 .login-button → .oauth-box .oauth-bg 第 2 个 | open.weixin.qq.com 标准图片;首页 .login-button 消失才算成功 |
| 知乎 zhihu | 完整登录确认 | https://www.zhihu.com/signin;.Login-socialButton 或 button[class*="socialButton"] 第 1 个 | open.weixin.qq.com 标准图片;访问 /signin 自动跳走、头像可见、登录控件消失才算成功 |
| 简书 jianshu | 完整登录确认 | https://www.jianshu.com/sign_in;a#weixin.weixin | ego 可能抑制 _blank;在同 task tab 打开该元素的官方 href=/users/auth/wechat,随后进入标准微信 OAuth |
| 今日头条 | 完整登录确认 | https://www.toutiao.com/;.toutiao-header .header-right a.login-button | 保留默认头条 App 二维码;确认刷新遮罩隐藏后截取并发送完整 .web-login-union__login__scan-code 卡片,不切换微信、不只抓内部二维码图片;使用今日头条 App 扫码 |
| 博客园 | 完整登录确认 | https://account.cnblogs.com/signin;app-external-sign-in-providers .providers button:first-of-type(子图 WeChat.png) | 站内 popup 的 app-wechat-mp-qr img[alt="微信二维码"],扫码会关注博客园服务号并登录 |
| 51CTO | 完整登录确认 | https://home.51cto.com/index?reback=https%3A%2F%2Fwww.51cto.com%2F;.login-type-switch:nth-child(1) | 站内 #login-wechat img.qr-img,图片来自 mp.weixin.qq.com/cgi-bin/showqrcode |
| CSDN | 完整登录确认 | https://www.csdn.net/;a.toolbar-btn-loginfun → iframe #passportbox iframe[name="passport_iframe"] → “微信登录” tab | https://passport.csdn.net/login?code=applets 内 .login-code-wechat .public-code img;280×280 JPEG data URI 微信小程序码 |
| SegmentFault 思否 | 完整登录确认 | https://segmentfault.com/user/login;button.login-mode[data-mode="weixin"] | #weixinQrCode iframe 内嵌标准微信 OAuth;iframe target 类型为 iframe、回调 /user/oauth/weixin |
| 小红书 xiaohongshu | 主站、创作平台完整登录确认 | 主站 https://www.xiaohongshu.com/explore;创作平台 https://creator.xiaohongshu.com/publish/publish?from=menu&target=article 被 401 重定向后,真实点击短信登录卡右上角 64×64 模式图标 | 主站 .login-modal img.qrcode-img,小红书 App 或微信可扫;创作平台为 160×160 PNG data URI,页面要求小红书 App 扫码 |
| 少数派 sspai | 完整登录确认 | https://sspai.com/write → /login;.ssCommunityIcon__weixin | 当前 tab 进入 open.weixin.qq.com/connect/qrconnect;切换后 img.js_qrcode_img.web_qrcode_img 160×160 标准图片;扫码后回调 /callback/weixin |
掘金、知乎默认码不是微信,必须依据矩阵进入微信入口,不能看到二维码就抓。今日头条例外:保留默认 App 码并发送完整扫码卡片。当前 skill 只支持表中十个平台,其余平台不要自动套用。
与文章发布扩展的边界
- 本 skill 的登录态只保存在
qr-login-<平台>对应的 ego task space;不会自动写入 Chrome 扩展所在的用户目录,也不会向业务后端同步 Cookie。 - 用户要求“给 ArtiPub/文章发布助手登录”时,先确认目标是 ego 中继续操作,还是给 Chrome 扩展准备登录态。后者必须在扩展所在的 Chrome 用户目录完成官方登录并由扩展同步 Cookie;不得因 ego 登录成功就宣称发布助手已经可用。
- 两者仅复用平台入口、人工扫码和成功判定知识,不复制二维码、OAuth URL、Cookie 或账号密码;除非已验证为同一浏览器用户目录,否则一律按两套独立登录态处理。
主流程
const task = await useOrCreateTaskSpace('qr-login-<平台>')
-
只接受矩阵中的十个平台;用一个
ego-browser nodejsheredoc 完成整个浏览器阶段,不逐步探测已记录选择器。SegmentFault 走 OAuth iframe 分支,博客园/51CTO 走公众号二维码分支,CSDN 走小程序码分支,小红书走站内二维码分支,今日头条走 App 二维码分支,其余四站走标准 OAuth 分支。 -
gotoAndWait(<登录页>)强制刷新首页;按「登录成功判定」检查。未登录时按站点表打开登录弹窗。需要微信 OAuth 的站点记录微信点击前的Target.getTargets,并取可见入口中心点后调用 egoclick({ x, y })真实鼠标点击;DOMel.click()没有用户激活,Chromium 会拦截 popup,造成空等。今日头条打开弹窗后直接走 App 二维码分支。 -
标准 OAuth 站点按矩阵进入真实 OAuth:掘金/知乎从 targetId 差集取新 popup;简书在当前 task tab 打开
a#weixin的官方站内 href。最终 target 都必须匹配open.weixin.qq.com/connect/qrconnect。今日头条不执行本步,直接走下方 App 二维码分支。 -
保留原始 OAuth target,禁止关闭后复制 URL 到新 tab。 微信 OAuth 的 state 与 opener 上下文有关,复制 URL 会让掘金回调报
error_code: 4 / 参数错误。popup 用Target.attachToTarget({ targetId, flatten: false })附着;简书直接使用当前 tab。 -
在 OAuth target 中只读取
.js_switchToNormal的可见 rect,再连续发送真实Input.dispatchMouseEvent。禁止元素.click()。点击后必须同时确认切换按钮不可见,且img.js_qrcode_img的offsetParent存在、可见宽高都不少于 100px;两项缺一即失败。然后在同一 target 抓二维码:const { mkdir, writeFile } = await import('node:fs/promises') const { tmpdir } = await import('node:os') const { join } = await import('node:path') // 若当前工作区规定了任务产物根目录,优先把 qrDir 指向其中的 qr-login/;无规定才用系统临时目录。 const qrDir = join(tmpdir(), 'vft-kit', 'qr-login') await mkdir(qrDir, { recursive: true }) const imageFile = join(qrDir, '<平台>-qr.jpg') // sendPopup(method, params) 封装 Target.sendMessageToTarget,并按 id 等待 // Target.receivedMessageFromTarget;不要改用当前 tab 的 js()。 const result = await sendPopup('Runtime.evaluate', { awaitPromise: true, returnByValue: true, expression: String.raw`(async () => { const visibleQr = el => { const r = el.getBoundingClientRect() return el.offsetParent && r.width >= 100 && r.height >= 100 && /connect\/qrcode/.test(el.currentSrc || el.src) } const img = [...document.querySelectorAll('img.js_qrcode_img')].find(visibleQr) || [...document.images].find(visibleQr) const src = img?.currentSrc || img?.src if (!src) throw new Error('找不到微信二维码') const response = await fetch(src, { credentials: 'include' }) if (!response.ok) throw new Error('二维码请求失败: ' + response.status) const bytes = new Uint8Array(await response.arrayBuffer()) let binary = '' for (const byte of bytes) binary += String.fromCharCode(byte) return btoa(binary) })()` }) const b64 = result.result.value await writeFile(imageFile, Buffer.from(b64, 'base64'))不要输出二维码内容、OAuth URL 或图床链接。个别站点二维码是 canvas 且被污染时,用
cdp('Page.captureScreenshot', { clip })按元素 rect 截区域图兜底。 -
推送前先在当前对话说明三项:正在执行的动作、操作对象、扫码后的实际效果(例:「扫码后登录掘金账号,不会发布任何内容」),格式
正在执行:{action};对象:{subject};扫码后:{effect}。然后调用本 skill 的脚本(secrets 由调用方按供给桥注入 env,见下):python3 "<本 SKILL.md 所在目录的绝对路径>/scripts/notify-qr.py" \ --image-file "<本地二维码绝对路径>" --action '登录' --subject '掘金'脚本要求
PUSHPLUS_TOKEN环境变量;缺失时不上传公网,改为把本地二维码作为当前对话附件交给用户。只有输出同时含upload: ok与 PushPluscode: 200才算交付成功;ntfy字段ok/failed/skipped为附加提醒,failed 只报告不阻塞。 -
告诉用户按平台提示使用微信或对应 App 扫码/确认后停止浏览器操作,不轮询,也不
handOffTaskSpace(用户无需碰浏览器窗口)。用户回复「继续」后复用同一 task space;只有用户明确要求在浏览器内接管时才 handoff。 -
验证成功:用户回复后直接重新接管同一 task space。OAuth 分支检查原 target 已离开微信域名并跳到平台回调,再刷新首页按矩阵复核登录态;站内二维码分支按各自章节复核。二维码过期时从平台登录页重新取得新 state,不能刷新或复制旧 OAuth URL。
-
结束删除
imageFile,completeTaskSpace(task.id, { keep: false })。
今日头条 App 二维码分支
- 打开首页并真实点击
.toutiao-header .header-right a.login-button,等待.web-login-union__login__scan-code可见。不要勾协议、不要点击“微信登录”;目标就是弹窗默认展示的头条 App 码。 - 在该容器内同时确认标题文本为“扫码登录”、说明含“今日头条App”,
img.web-login-scan-code__content__qrcode-wrapper__qrcode可见宽高都不少于 100px,且.web-login-scan-code__content__qrcode-wrapper__mask不可见。若遮罩显示“点击刷新”,必须点击刷新并等待二维码源变化;不发送空白、旧码或刷新遮罩状态。 - 用
Page.captureScreenshot按.web-login-union__login__scan-code的可见 rect 截取完整卡片并写入imageFile。必须包含“扫码登录”标题、二维码和“请使用今日头条App扫码登录”说明;禁止只下载或截取内部img。 - 回到主流程第 6 步推送。用户扫码确认后复用原 task space,刷新首页;顶部
a.login-button和侧栏“立即登录”入口消失、已登录头像/账号入口可见才算成功。
SegmentFault 微信 OAuth iframe 分支
- 打开
https://segmentfault.com/user/login,真实点击button.login-mode[data-mode="weixin"];等待#weixinQrCode iframe可见。它不会创建 page popup,而会创建 URL 为open.weixin.qq.com/connect/qrconnect、type 为iframe的 OOPIF target。 - 通过
Target.attachToTarget({ targetId, flatten: false })附着该 iframe。微信记忆账号时,iframe 内可见.js_switchToNormal位于内部 y≈160,但站点 iframe 默认只有 160px 高,会把按钮裁掉。先把父页 iframe、#weixinQrCode和最近的.qrCode高度临时扩至 204px,再把 iframe 内按钮中心按父 iframe rect 映射到根页面坐标,用 ego 真实鼠标点击。 - 切换可能延迟数秒;最多等待 5 秒,必须确认所有可见
.js_switchToNormal消失,并且 iframe 内可见img.js_qrcode_img宽高不少于 100px。页面含多个隐藏同名节点,禁止用querySelector单节点判断。 - 从父页读取 iframe rect,仅截取顶部 160×160 二维码区域。实测 iframe session 内
fetch(img.currentSrc)可能被跨域策略拒绝;失败后直接用父 page target 的Page.captureScreenshot({ clip })截图,不重复请求。如果出现模糊二维码和刷新图标,说明已过期,重新加载 SegmentFault 登录页生成新 iframe,不能推送过期截图。 - 用户扫码后验证时,把导航和 DOM 检查拆成两个 heredoc:第一个只接管并
gotoAndWait('/user/login'),等待已登录重定向完成;第二个重新选择稳定 tab 后检查。实测在导航完成的同一 heredoc 立即js(),ego 可能因 target 切换瞬间误报用户控制。最终必须同时满足/user/login跳到首页、登录入口消失、实际/u/<账号>链接和头像出现;不能只凭 iframe 内容变化判定。
站内公众号二维码分支
博客园、51CTO 不走 open.weixin.qq.com/connect/qrconnect:
- 按矩阵真实点击微信入口。博客园会创建站内 popup;51CTO 在当前登录页切换。
- 等待对应
<img>可见、宽高不少于 100px且完成加载。图片来自跨域mp.weixin.qq.com/cgi-bin/showqrcode,不要依赖页面fetch或 canvastoDataURL。 - 读取二维码元素 rect,用其 page target 的
Page.captureScreenshot({ clip })截取区域;不要截整个登录页。 - 博客园推送前必须说明“扫码会关注博客园服务号并登录”;51CTO只说明登录,不能扩大效果。
- 用户回复后按平台页面跳转、登录入口消失、头像或昵称出现复核;未实扫的平台不得只凭二维码消失宣称成功。
CSDN 微信小程序码分支
-
打开 CSDN 首页;若
a.toolbar-btn-loginfun已不可见且头像/昵称可见,直接报告已登录。否则真实点击登录按钮,等待#passportbox iframe[name="passport_iframe"]。 -
读取 iframe 的官方
src,在同一 task space 自有 tab 打开;最终页应为https://passport.csdn.net/login?code=applets。点击文案为“微信登录”的.login-box-tabs-items span,确认其含tabs-active。 -
等待
.login-code-wechat .public-code img同时满足complete、naturalWidth > 0、currentSrc.startsWith('data:image/jpeg;base64,');不要用容器的loadingclass 判定,它在二维码已出现时仍可能保留。 -
从
currentSrc取逗号后的 base64,直接解码写 JPEG;图片原始尺寸 280×280。若跨域 iframe 无法读取,才按可见 rect 用 CDP 区域截图兜底:const dataUri = await js(`(() => { const img = document.querySelector('.login-code-wechat .public-code img') if (!img?.complete || !img.naturalWidth || !img.currentSrc.startsWith('data:image/jpeg;base64,')) { throw new Error('CSDN 微信小程序码尚未就绪') } return img.currentSrc })()`) await writeFile(imageFile, Buffer.from(dataUri.slice(dataUri.indexOf(',') + 1), 'base64')) -
推送前说明“扫码后通过 CSDN 微信小程序登录或注册 CSDN”;这不是开放平台二维码,不要等待
open.weixin.qq.comtarget。 -
用户回复后回到原 CSDN 页刷新:登录 iframe/遮罩关闭、
a.toolbar-btn-loginfun消失、顶部出现实际头像或昵称,三项满足才算成功。
小红书站内二维码分支
先按用户目标选择分支,两套登录态不互相证明:主站已登录仍可能被创作平台以 redirectReason=401 拒绝。
主站
- 复用
qr-login-xiaohongshutask space,打开https://www.xiaohongshu.com/explore,先按第 5 项检查已有登录态,成立就直接报告已登录。未登录时登录弹窗通常自动出现;若已关闭,读取可见button#login-btn.login-btn中心点并用 ego 真实鼠标点击重开。 - 等待
.login-modal.reds-modal-open img.qrcode-img同时满足complete、naturalWidth >= 100、可见宽高均不少于 100px、currentSrc.startsWith('data:image/png;base64,'),并严格确认window.__INITIAL_STATE__.login.qrData.status === 'un_scanned'。它是小红书站内码,无 popup、iframe 或open.weixin.qq.comtarget。 - 从
currentSrc取逗号后的 base64 直接解码写 PNG;不要输出二维码内容。推送前说明“小红书 App 或微信均可扫码,扫码后登录小红书主站账号,不会发布内容”。 - 页面通过
/api/sns/web/v1/login/qrcode/create创建二维码并轮询/api/qrcode/userinfo;un_scanned表示等待扫码,invalid表示过期。过期后重新打开登录弹窗取得新码,不推送旧 data URI。 - 用户回复后复用同一 task space,刷新首页并同时确认
window.__INITIAL_STATE__.user.loggedIn.value === true、user.userInfo.value非空、.login-modal.reds-modal-open消失、可见button#login-btn消失、li.user.side-bar-component a.link-wrapper中可见文案“我”出现;五项满足才报告完整登录成功。刷新后login.qrData.status可能重新变成un_scanned,不得用它推翻已成立的页面登录态。
创作服务平台
- 在同一
qr-login-xiaohongshutask space 打开用户要访问的创作页。若跳到creator.xiaohongshu.com/login?...redirectReason=401,不要停在短信验证码:查找登录卡内可见、cursor: pointer、显示尺寸 64×64 的<img>,取中心点用 ego 真实鼠标点击。不要依赖构建生成的 CSS 类名。 - 必须回读到文案“APP扫一扫登录”,再选择可见宽高均不少于 150px、
naturalWidth >= 100、currentSrc.startsWith('data:image/png;base64,')的二维码<img>。从 data URI 解码写 PNG;64×64 的模式图标不是二维码,禁止误抓。 - 推送前说明“请用小红书 App 扫码;扫码后登录小红书创作服务平台,不会发布内容”。页面出现过期提示或大图消失时,重新加载创作页并重新切换扫码模式,不推送旧码。
- 用户回复后复用同一 task space,重新打开原创作页;必须确认 URL 不在
/login、未再次出现redirectReason=401,且目标编辑器或创作后台可访问,才报告创作平台登录成功。主站五项登录态不能替代这项验证。
未实扫的分支只能写“二维码确认”。
少数派微信 OAuth 分支
- 复用
qr-login-sspaitask space,从https://sspai.com/write开始;未登录时页面会跳到/login。先检查登录态,不能把写作页跳转本身当作失败。 - 在
/login读取可见.ssCommunityIcon__weixin中心点并用 ego 真实鼠标点击;不要调用 DOM.click()。该入口在当前 tab 导航到官方open.weixin.qq.com/connect/qrconnect,不创建 popup,必须保留原始 OAuth target 和 state。 - 在原 OAuth target 中按通用规则真实点击可见
.js_switchToNormal;确认该按钮消失后,等待可见且已加载的img.js_qrcode_img.web_qrcode_img(宽高均不少于 100px,currentSrc匹配open.weixin.qq.com/connect/qrcode),从图片响应抓取二维码,不输出 OAuth URL 或二维码内容。 - 推送前说明“扫码后登录少数派账号,不会发布文章”。二维码过期时从
https://sspai.com/write重新进入登录并取得新 state,不能刷新或复制旧 OAuth URL。 - 用户回复后复用同一 task space,等待原 tab 离开微信域名并回调到
sspai.com/callback/weixin,再回到https://sspai.com/write复核:登录入口消失、账号信息/头像出现且写作编辑器可访问;三项满足才报告完整登录成功。
notify-qr.py 的 env 供给桥
脚本本身不读任何密钥文件;调用方负责 export:
| 环境变量 | 含义 | 默认 |
|---|---|---|
| PUSHPLUS_TOKEN | PushPlus 消息 token(必需) | 无 |
| PUSHPLUS_TOPIC | 群组编码 | me |
| PUSHPLUS_SEND_URL | 发送接口 | https://www.pushplus.plus/send |
| BEEIMG_TOKEN / BEEIMG_UPLOAD_URL / BEEIMG_STORAGE_ID | 主图床 | 无 token 时回退 Uguu |
| UGUU_UPLOAD_URL | 匿名兜底图床 | https://uguu.se/upload |
| NTFY_BASE_URL / NTFY_TOPIC / NTFY_TOKEN / NTFY_ALLOW_SHORT_TOPIC | 附加提醒,可全缺 | 缺则 skipped |
--title-prefix 公众号 可复用公众号流程的标题格式。调用方负责从自己的凭据存储导出这些 env;公共脚本不读取仓库内的凭据文件。
ntfy 的通用发送、macOS 接收端、自启和订阅看门狗由 vft-kit:notify 维护;本 skill 只保留“PushPlus 成功后发送二维码附件、ntfy 失败不阻塞”的扫码业务语义。
微信扫一扫