一个纯本地的浏览器插件(Manifest V3,Chrome / Edge / Firefox 通用):在各企业招聘网站 / ATS 上一键填充申请表单——自动识别中英文字段、处理多段经历、操作 React 下拉与日历、注入简历 PDF 附件。数据全部存在浏览器本地,零网络请求。
设计底线:提交永远由你自己点。 插件不点提交按钮、不勾选任何协议、不碰验证码。 更重要的是:宁可留空,绝不瞎填——填错比不填危害大得多,招聘方看到的是错误信息,而空白只是空白。
同一份代码在三个浏览器都能跑,不需要开发环境、不用装任何依赖。
Chrome / Edge
- 打开
chrome://extensions(Edge 为edge://extensions); - 右上角打开开发者模式;
- 点加载已解压的扩展程序,选择本目录(含
manifest.json的那一层); - 把插件图标固定到工具栏。
Chrome 每次启动可能提示「请停用以开发者模式运行的扩展程序」,点「取消」即可。
Firefox(需 115+)
- 打开
about:debugging#/runtime/this-firefox; - 点临时载入附加组件,选择本目录里的
manifest.json; - 首次点「一键填充」时会弹出权限请求,必须点允许 —— Firefox 把「访问所有网站」当作可选权限,不给就没法注入脚本。
若误点了拒绝,去
about:addons→ 本插件 → 权限,打开「访问所有网站的数据」。
⚠️ Firefox 的临时载入在重启后会消失,档案数据也会一并清掉。长期使用请先在档案页 导出备份 JSON,或者把插件签名后正式安装(web-ext sign,需要 Mozilla 账号)。 Chrome 的开发者模式加载不受此影响,重启后仍在。
插件本身不含任何数据。三条路,选一条:
把 profile-template.json 和你的简历一起交给任意大模型,用这段提示词:
这是一个简历自动填表插件的档案模板(JSON)。请阅读我的简历,严格按模板的字段结构填充,输出一个完整的 JSON 文件。
要求:
1. 保持顶层 "profile" 键和所有字段名不变,不要增删字段名;
2. 教育/工作/项目/论文/竞赛/荣誉等列表按时间倒序,第一条是最新或最高的那条;
3. 时间统一写成 YYYY-MM;在职/在读的结束时间写「至今」;
4. 简历里没有的信息一律留空字符串,不要臆造 —— 尤其是日期、成绩、证件号;
5. 以 _ 开头的注释键可以删掉;
6. 只输出 JSON,不要解释。
模板:
<粘贴 profile-template.json 的内容>
我的简历:
<粘贴简历文本,或上传简历文件>
存成文件 → 插件 ⚙️ 档案 →「导入备份」→ 选中它。
导入后务必逐项核对。 大模型可能读错日期或职位;第 4 条要求是关键——投递表上编造的日期在背调时就是硬伤。
直接在 ⚙️ 档案 页逐栏填写。字段较多,但只填一次,之后一直复用。
Moka、百度等网站上传简历后会自动解析填表。可以先让它解析,再点插件补齐剩下的——插件不覆盖已有内容,两者不冲突。
回到 ⚙️ 档案 页底部上传简历 PDF。它存在插件本地,遇到「上传简历」控件时自动注入。
- 打开招聘页的申请表单;
- 点插件图标 → ⚡ 一键填充当前页面;
- 页面右上角出现进度条(「正在填充 12/48 · 学校名称」),填完自动收起;
- 弹窗列出「已填充 / 已跳过 / 未识别」三类,填过的输入框会短暂显示绿色描边;
- 「未识别」的字段用弹窗下方的快速复制按钮点一下再粘贴;
- 自己核对后提交。
| 能力 | 说明 | |
|---|---|---|
| ✅ | 中英双语字段识别 | 60+ 类字段,带排除词防误填 |
| ✅ | 多段经历 | 教育 / 工作 / 项目 / 论文 / 竞赛 / 荣誉 / 外语 / 编程语言,自动分段对应 |
| ✅ | React 受控表单 | 原生 value setter + 事件派发,兼容 Moka、北森、百度、Workday 等 |
| ✅ | 自定义下拉 | 点击展开 → 语义匹配 → 点选(档案「硕士」能选中选项「硕士研究生」) |
| ✅ | 日历控件 | 按日期格子的 title 精确命中,不翻日历 |
| ✅ | 城市级联 | 逐级走完整路径 |
| ✅ | 拆分日期 | 「年」「月」两个独立文本框 |
| ✅ | 简历附件注入 | 自动避开头像 / 照片上传框 |
| ✅ | iframe 与 open shadow DOM | 所有 frame 逐一注入 |
| ✅ | 可重复区块 | 档案段数多于页面时,自动点「+ 添加」补足(带误点防护) |
| ✅ | 备份提醒 | 档案改过没备份、或超 30 天没备份时提示,可在弹窗就地导出 |
| ✅ | 页面内进度提示 | 右上角实时显示填到第几个字段、正在点哪个下拉,不挡点击 |
| ✅ | 已填内容不覆盖 | 文本 / 下拉 / 单选组 / 已上传附件全都不动,重复点安全 |
| 「至今」写进日期框 | 浏览器会静默丢弃,插件显式跳过并提示;配套的「至今」勾选框请自行勾 | |
| ❌ | 自定义单选组 / 富文本 | 选中态无法可靠判断,不碰 |
| ✅ | 竞赛/奖学金合并列表 | 有的表单把两类合成一个列表(OPPO),自动拼流并抽出「级别 / 等级」 |
| ✅ | 紧急联系人区块禁填 | 「紧急联系人」「亲属信息」等区块整段跳过,绝不填成本人信息 |
| ❌ | 提交、协议勾选、验证码 | 刻意不做 |
完整字段见 profile-template.json。要点:
八类经历都是数组,按时间倒序——第一条即最高学历 / 最近一段实习:
education work projects papers competitions awards languages progLangs
(另有 patents / softwares,默认空)
为什么按结构化存:中兴、百度会把论文拆成「名称 / 类型 / 作者顺序 / 发表时间 / 链接 / 描述」六个独立字段。结构化数据可以随时拼成整段文字喂给只有一个文本框的表单,反过来做不到。
几个特殊字段:
| 字段 | 用途 |
|---|---|
endTime: "至今" |
在职 / 在读 |
cityPath / expectedCityPath |
城市级联专用,写完整路径 上海/上海市/浦东新区;普通文本框仍用 city |
projects[].role / .duty |
百度把「项目职务」和「项目职责」拆成两栏;只有一栏的表单会自动互相兜底 |
custom |
自定义问答,优先级高于内置规则 |
自定义问答是覆盖内置行为的唯一手段。触发关键词逗号分隔,字段标签命中任一关键词即填入对应内容。典型场景:研究生填「本科院校」时内置规则会命中「院校」二字而填成研究生院校——加一条关键词为「本科院校, 第一学历」的问答即可纠正。
弹窗里有两个只读的诊断按钮,先分清是「插件问题」还是「档案没数据」:
- 🩺 档案自检 —— 摊开插件里实际存了什么:每类几段、每段缺哪些字段、简历附件在不在。若显示「工作/实习:0 段」,再改规则也填不出来。
- 🔍 复制诊断报告 —— 摊开页面上有什么字段:每个字段命中哪条规则、落在哪个区块、标签定位是否可疑(标签疑似是值/占位提示时给出祖先 class 链),外加档案概况(仅段数,不含内容)与识别到的区块标题。对未命中或可疑的字段,报告尾部附脱敏 DOM 骨架(往上两层祖先的结构,
value一律替换成[值]、超过 4 字或含数字的文本替换成[文])——「把这个字段的 HTML 复制给我」这一步不用再去 DevTools 人肉抠了。整份报告不含任何个人信息,可直接贴给 AI 排查。
两份对照着看,漏填只有四种原因:
| 清单里的表现 | 原因 | 怎么办 |
|---|---|---|
控件正常,规则列是 — |
规则没认出这个标签 | 加正则,或用自定义问答兜底 |
| 控件正常,规则列有值 | 档案里没这项数据 | 去 ⚙️ 档案 补上 |
控件是 自定义单选 / 富文本(需手动) |
引擎刻意不碰 | 手动选 |
| 字段压根没出现在清单里 | 在未展开的区块或未打开的弹层里 | 先展开再扫描 |
「字段标签」一列显示的是真正命中的那条候选文本,不一定等于肉眼看到的标签。如果这列出现的是字段的值(如「某某大学」)或占位提示(如「项未填写」),说明标签定位落在了错误的 DOM 层级——那才是需要改
labelCands的信号。
清单表头会打印引擎版本。与本文档不符,说明扩展没刷新,那份清单是旧代码产出的。
插件是纯静态文件,打包 zip 发过去即可(约 48K):
cd tools && zip -r resume-quickfill.zip resume-quickfill -x '*/test/*' '*.DS_Store'对方解压后按上面的安装步骤加载。
别把自己的档案 JSON 一起发出去——里面有手机号、邮箱、简历 PDF,甚至可能有证件号。插件目录本身不含任何个人数据。
放到 GitHub 的话,加一条 .gitignore 挡住个人档案:
resume-quickfill-*.json
- 所有数据存于
chrome.storage.local,只在你本机; - 代码中没有任何
fetch/XMLHttpRequest/ 外部资源,manifest 不含远程脚本; - 证件号码是可选项。它以明文存在本地存储中,填不填由你决定——不填只是每次手动敲这一栏,其余字段照常自动填;
- 导出的备份 JSON 内含简历 PDF 与全部个人信息,当作身份文件保管,别放进公开仓库或聊天群;
- 插件不自动提交,也不勾选协议——既避免误投,也不触碰各平台对自动化投递的限制。
content.js(那一千行识别与填充引擎)不依赖任何浏览器扩展 API,只操作 DOM,天然通用。
需要抹平的差异只有两处,都收在 api.js 里:
| 差异 | 处理 |
|---|---|
| 命名空间 | Firefox 的 browser.* 返回 Promise,chrome.* 是回调风格;Chrome MV3 只有 chrome.* 且返回 Promise。统一优先取 browser |
| 主机权限 | Chrome 安装即授予 host_permissions;Firefox MV3 把它当作可选权限,须由用户手势触发 permissions.request() |
manifest.json 里的 browser_specific_settings.gecko 是 Firefox 安装所必需的;Chrome 会忽略这个键(扩展页可能显示一条无害的「无法识别的清单键」警告)。
有一个坑值得记:scripting.executeScript 的 func 体运行在页面的内容脚本环境,拿不到弹窗里的适配层,必须就地解析命名空间——曾经批量替换时把这里也改了,导致填充直接失效。
每个输入框按优先级采集语义线索:<label> → aria-label(ledby) → placeholder → 兄弟文本(父容器文字减去通往输入框的那一支)→ 整段祖先文字 → name / id。
「兄弟文本」是关键:antd 的结构是 <span>某某大学</span><input 空/>,只取整段祖先文字会把值当成标签,真正的「学校名称」还在更上层——所以要逐层上溯并减去输入框所在分支。同理,自定义组件的标签候选会剔除组件自己的当前值。
判据是「本段内已出现过的字段再次出现,即进入下一段」,只有锚点字段(学校名称 / 公司名称 / 项目名称 / 论文名称…)和时间区间参与计数。
这样两种相反的真实布局都能正确分段:中兴把「就读时间」排在「学校名称」之前,Moka 则把「开始/结束时间」放在区块末尾。通配字段(「起止时间」「描述」)按前后最近的明确域归属,若上一区块的该字段已填过,就归下一区块。
「本科院校」「第一学历」这类带学历限定的字段不参与计数,直接按学历定位到对应那段。
拆成「年」「月」框的日期还有三条铁律(全部来自真实 Shopee 页的诊断报告):
- 已填的年/月框要认出来占位。 Moka 的日期框填过值后,标签会退化成值本身(「2024」),规则认不出,不参与年月推进——空着的「结束时间」对就被当成「开始时间」填,结束年月被写成入学年月。引擎按「值长得像年份/月份」把它们补回序列:只推进状态,绝不写入;
- 区块外的年/月对不猜归属。「预计毕业时间」「出生日期」这类独立日期不在任何经历区块里,靠前后邻居猜会把它填成某段经历的时间。猜错是脏数据,留空只是留空;
- 单日期域一段只配一对年/月。 获奖、论文的条目只有一个日期,第二对年/月出现就是下一段,不是本段的「结束时间」。
Shopee 逼出了两条补充规则:
- 非锚点字段也能分段,但要以区块标题为闸门。 Shopee 把「学历 / 学习形式」排在锚点「学校名称」之前——第二段的这些字段出现时锚点还没轮到,只认锚点会取到第一段的值。所以放开限制:同区块内已填过的字段再次出现,即进入下一段。闸门有两道:字段必须落在该域自己的区块标题下(基本信息区的「学历」不算),且此前那次占用也发生在区块内(基本信息区的「预计毕业时间」先占了
edu.endTime,不能因为它,把教育区块里第一个「结束时间」顶到下一段去)。 - 「最高学历毕业院校」是基本信息键,不是教育区块字段。 它若命中锚点
edu.school,会把教育区块计数器提前消耗一格,后面整个教育列表错位一段。
「意向工作城市是否可以调剂」「请问你是否填写了本科学历」——标签里带着「意向工作城市」「学历」,规则会命中,然后把城市名、学历词填进一个该答「是/否」的框里。这类问句一律排除(是否 / 请问 / 调剂),留给用户自己答;真想自动填,用自定义问答加一条「调剂 → 是」即可(自定义问答优先于内置规则,也不受排除词影响)。
有些表单的整个区块只有一个泛化字段:大疆的「获奖经历」只有一栏叫**「描述」**,「语言能力」只有「语言证书及成绩」,「论文/期刊」是「名称/描述/成果」。这些标签本身没有任何信息量——只有区块标题能区分它属于哪一类,靠前后邻居猜必然出错。
所以引擎会先扫一遍页面上的区块标题(h1~h6 / legend / 含 title·header·section 的 class),把「教育背景→edu」「赛事经验→comp」「获奖经历→award」这类映射排成 DOM 顺序,任一字段都能查到自己落在哪个区块下。这个信号的优先级高于前后邻居距离。
在此基础上,标签只是「名称 / 描述 / 成果」这种孤零零的词时,由区块决定它的归属:论文区的「名称」是 paper.name,获奖区的「描述」是 award.desc。
两条约束是踩坑换来的:
- 泛化角色的模式必须全部锚定(
^描述$而非描述),且候选文本要短(≤8 字)。曾经写了个没锚定的描述及成果,结果匹配到整块祖先文字「公司名称 职位名称 实习使用技能 经历描述及成果」,把「起止时间」判成了描述; 编程语言必须排在语言能力之前——「编程语言能力」同时含后者。
同一批信息,各家表单的切法完全相反:
- 大疆把「赛事经验」和「获奖经历」拆成两个独立区块;
- OPPO把它们合并成一个「竞赛/获奖经历」列表,每条再用类别 / 级别 / 等级三个维度描述。
档案里按拆开存(competitions 与 awards),遇到合并型表单时自动拼成一条流——竞赛在前、荣誉在后。理由和论文那条一样:拆开的数据能合并,合并的数据拆不回去。
麻烦的是合并型表单要求把一条荣誉拆成三个互不相干的维度,而它们通常是三个独立下拉:
| 维度 | 问的是 | 典型选项 |
|---|---|---|
| 类别 | 哪一类荣誉 | 学科竞赛 / 科技竞赛 / 奖学金 / 荣誉称号 |
| 级别 | 多大范围 | 国家级 / 省级 / 市级 / 校级 |
| 等级 | 第几等 | 一等 / 二等 / 优秀奖 |
档案里 type 常写成「校级奖学金」这种混合值,所以要先把级别词摘走给「级别」,剩下的才是类别。
若 type 只写了级别(比如就写「国家级」),剩不下东西,分两种情况:
- 奖学金 / 荣誉:拿这个词本身当类别——它就是选项表里的一项,精确命中;
- 竞赛:从竞赛名称查表。「数学建模」→ 学科竞赛,「互联网+」→ 创业竞赛。
第二条要和「瞎猜」分清楚。不能拿「竞赛」二字去撞选项:学科竞赛 / 科技竞赛 / 文体竞赛 / 创业竞赛四个都含这两个字,子串匹配只会选中排在最前面的那个,选中什么纯看页面怎么排。而从竞赛名称查表是知识——数学建模确实属于学科竞赛。表里只收归属无争议的;像「挑战杯」那样大挑是课外学术科技、小挑是创业计划的,不收,留空让用户自己选。
查不到的竞赛类别留空,报「档案中未填写」。想一劳永逸的话,把竞赛的「类型 / 级别」栏两个维度都写上,如 学科竞赛 国家级,就不走查表了。
两套词,中文表单分得很清:
| 问的是 | 用词 | |
|---|---|---|
| 学历 | 读到哪一级 | 专科 / 本科 / 硕士研究生 / 博士研究生 |
| 学位 | 拿到什么学位 | (专科无) / 学士 / 硕士 / 博士 |
同一段本科经历,「学历」栏填本科、「学位」栏填学士。档案里只存一个 degree,所以按层级换算成本栏该用的说法。
下拉和单选一直是对的——它们按层级匹配选项,档案写「学士」也能选中「本科」。出错的是文本框:没有选项可对齐,写什么就是什么,于是本科那段的「学历」栏被原样填成了「学士」。
两个边界:
- 认不出层级的原样保留(如 MBA、中专),不擅自改写用户填的内容;
- 专科的学位栏留空——专科确实没有学位,这不是缺数据。
表单末尾常有「紧急联系人」,里面的字段就叫「姓名」「联系电话」。仅靠排除词拦不住——排除词要求标签里出现「紧急」二字,而这里标签是干净的。
所以引擎按区块拦:标题匹配「紧急联系人 / 亲属信息 / 担保人 / 监护人 / 家庭成员」时,整段字段一律跳过并说明原因。把本人电话填进紧急联系人栏,是那种不会报错、但一眼就露怯的错误。
表单字段多、下拉控件多的时候(OPPO 要点开十几个),整个填充要好几秒,期间没有任何反馈就像卡死了。
提示条画在页面右上角,不画在弹窗里,有两个理由:弹窗一点页面就关掉了;而用户的视线本来就在表单上。实现上是一段纯 DOM,不引入任何扩展 API——content.js 的跨浏览器通用性靠的就是这条底线。
正在识别页面字段… → 展开「教育」区块 2/2… → 正在填充 4/9 · 性别
→ 正在选择「性别」… → ✅ 已填 6 项 · 跳过 4 项
三个约束:
pointer-events: none。提示条盖在页面右上角,绝不能挡住用户点击下面的控件;- 同步循环必须主动 yield。填充主体是一个同步 for 循环,不交还控制权的话浏览器一帧都不重绘,进度条会在最后一次性跳到头——和「卡住」看起来毫无区别。所以每隔几个字段、以及每次操作下拉之前,都
await一次requestAnimationFrame; - 标签页不可见时直接跳过 yield。那时既没人看,rAF 也压根不触发,而
setTimeout会被浏览器钳到 1 秒一次——白等一轮反而真把填充拖慢了。(这一条是被自己的测试套件逼出来的:测试页跑在隐藏的 iframe 里,加上进度条后整套测试从 11 秒变成两分多钟。)
出错时提示条会换成 ⚠️ 并照常收起。永远挂着一条「正在填充…」,比没有提示条更像卡死。
标签定位的兜底之一是兄弟文本——父容器的文字减去通往输入框的那一支。绝大多数表单里这就是标签,但当一个区块里每栏的标签都很短时,它会退化成灾难:
区块「竞赛/获奖经历」的六栏:竞赛/奖学金 · 类别 · 名称 · 获奖时间 · 级别 · 等级
「类别」那一栏爬到区块层拿到的兄弟文本 = 「竞赛 奖学金 名称 获奖时间 级别 等级」
↑ 恰好是本块其它所有标签,即正确答案的补集
这段文字里含「获奖时间」,于是整块六个字段全部命中 award.date,全被填成日期。40 字的长度上限拦不住它——六个中文短标签加起来才 20 字。
判据因此改成:短标签上的完全匹配,优先于长文本里的部分匹配。区块标题已经确定这栏属于哪一类时,^类别$ 这种锚定的精确命中是强证据;而规则在一段 8 字以上的文字里命中,是弱证据。前者赢。
这条对所有控件类型都成立,重复点「一键填充」是安全幂等的:
| 控件 | 判据 | 跳过时的说明 |
|---|---|---|
| 文本框 / 多行文本 | value 非空 |
已有内容,未覆盖 |
原生 <select> |
已选中非首项 | 已有选择,未覆盖 |
| 自定义下拉 / 日历 / 级联 | 组件显示区已有值 | 已有选择,未覆盖 |
| 单选组 | 组内任一项已选中 | 已有选择,未覆盖 |
| 简历附件 | 该上传框已有文件 | 已上传「xxx.pdf」,未覆盖 |
最后两行是后补的——早期版本会改写已选中的单选组,也会拿存档里的简历顶掉页面上已上传的那一份。后者尤其糟:很可能你刚传了针对这家公司改过的版本。
网站解析简历后预填的内容同样受这条保护,所以「先让 ATS 解析、再点插件补齐」是可行的用法。
同一个「时间」在 Moka 上有三种长相,处理方式完全不同:
| 长相 | 例子 | 处理 |
|---|---|---|
month-range-select 四联 |
就读/实习/项目的起止时间 | 组内按位置改写成 起年/起月/止年/止月,套用年月状态机 |
| 相邻两个下拉命中同一条日期规则 | 预计毕业时间、获奖时间 | 前者取年、后者取月;月那半不参与分段推进(否则第二段获奖会被多推一段) |
| 单个日期下拉 | 出生日期 | pickOption 兜底:值是 YYYY-MM 而选项是纯年份/纯月份列表时,取对应片段精确匹配,绝不拿整串去子串碰(「2027-06」会撞上选项「2」) |
另一个假已填陷阱:「必填项未填写」这类占位提示被当成组件的值,引擎以为已填就跳过——学习形式、英语等级证书全军覆没的原因。占位词剥干净才算空。注意 clean() 会先把「必填」当标签噪声剥掉,占位词模式必须容忍剥剩的「项未填写」——这个残缺形态是真实页面上实际出现的。
拿到真实页面完整 DOM 之后才看清的三条:
- 选中值渲染在
<span class="sd-Input-display-value">,内部 input 的value恒为空(它只是搜索框)。读错位置有两个后果:已填的下拉被当成空的重新点选(会覆盖 ATS 解析好的内容),点选之后回读又永远拿不到值,于是成功的点选也被报成「点选未生效」; - 校验提示
必填项未填写在<label>内部、紧挨着 input,DOM 距离比真标题<div class="title-">更近,权重压过真标签——整片字段的标签都变成了「必填项未填写」; - 单位字「年」「月」在组件内部(addon / 内部 input 的 placeholder),从父级向上找标签永远看不到,必须单独补进候选;而「请选择日期」这种纯提示语要整条丢掉——剥掉「请选择」剩下的「日期」是残渣不是标签。
原来只会「往该域最后一个字段之后找最近的按钮」,而 Moka 把 添加 放在 blockTitle 里——在该区块所有字段之前,位置法永远够不着。表现就是教育经历只有一段时死活不会自动加第二段。
改成按区块归属认领:标题元素本身包着按钮,compareDocumentPosition 对被包含元素同样置 FOLLOWING 位,所以现成的 sectionDomOf 就能认出按钮属于哪个区块。诊断接口 __RQF.addButtonDomain(el) 可以直接验证这一步——「不会自己加一段」到底是按钮没找到还是点了没反应,光看填充报告分不出来。
Moka 的在线简历页把这两者做成两个独立区块,而档案里只有一个 work 数组。早期两个标题都映射到同一个域,结果学生的实习被填进了「工作经历」——这是实打实的错。
分流规则:
- 档案里每段经历可以填一栏 性质(
type):实习/全职。留空则从职位名称推断(含「实习」二字即算实习),推断只在没有显式声明时兜底; - 页面同时存在两种标题时才拆:实习进
intern,全职进work; - 页面只有其中一种标题时,那一种收下全部经历。只有「工作经历」的表单若因为条目都是实习就一条不填,那是帮倒忙;
- 合并型标题(「实习与工作经历」)仍归
work,所以它的正则要排在最前面拦掉,免得被「实习经历」误伤。
retarget 对这一对有特例:两者条目结构完全相同,区块标题说了算——哪怕那一侧一条都没有,也该报「档案里没有第 N 段实习」,而不是拿全职经历去顶。
学校名称、专业名称这类字段背后是远程搜索:点击不触发任何请求,菜单也就永远不出现。诊断报告里的表现是 下拉未能展开(试过容器/输入框/父层)——这句话很关键,它把「点不开」和「选项里没有」区分开了,两者的修法完全不同。
所以展开策略是三段:
- 依次点容器 / 内部输入框 / 父层 / 祖父层(Moka 的点击处理器不一定绑在组件容器上);
- 还是不出选项,就往搜索框打字——先用完整值,再退到前 4 字、前 2 字(远程搜索对前缀更友好);
- 选项出来了但没有目标,再打字过滤一次。
探测失败必须把搜索框擦干净,不能把半截关键词留在页面上。
结束时间写「至今」时,日期格填不进去,配套的勾选框得勾上。早期版本一律不碰勾选框,那是能力缺失,不是设计——「不碰勾选框」的本意是不替用户同意协议。
现在只勾标签恰好是「至今 / 在读 / 在职 / present」的框,而且只在同一段经历的容器内找(爬出去就停手)。协议勾选框的标签是「我已阅读并同意…」,永远不会被误勾——测试里有这条反例断言。
期望薪资 旁边就是 当前薪资。排除词若拿全部候选去判,邻居拼接文本里的「当前」会把 expectedSalary 整条规则作废,期望薪资就永远填不上。判定范围必须和匹配范围一致——都用「本字段自己的标签」那一组。
本科院校 落在「其他信息」这个没有映射的标题下,sectionDomOf 会顺延到上一个区块(项目经验)。所以「规则域和区块域不一致就跳过」这种通用规则会误伤它。
真正需要互斥保护的只有语言能力 vs 编程语言这一对——两者的字段名高度重合(掌握程度 / 熟练程度),规则极易蹭错区块,把编程语言的熟练度填进外语栏。做成 pair 专用规则,而不是通用规则。
这条最隐蔽:「不覆盖已有内容」这条保护规则,把插件自己挡在了外面。
Moka 的日期是「年」「月」两个独立下拉。填完年份,组件立刻把空着的月自动补成 1;等轮到月份格时它已经「有值」,于是被跳过。表现是年份全对、月份全是 1——看起来像选错了选项,实际上是根本没去选。
判据:同一个日期的年和月是一个整体。年是插件写的,月就必须由插件写完。只在紧跟着年份的那一格放开覆盖,不波及任何别的字段;若那一格的现值恰好就是要写的值,直接认账,不做无谓点击。
这个 bug 的根因(已有选择,未覆盖)一直明明白白写在填充报告里,但那份报告在弹窗里,一关就没了;拿去排查的是扫描报告,两者对不上,于是排查等于瞎猜——绕了好几轮才找到。
现在 fill() 结束时把结果留在页面上,诊断报告把它按原因归并列出:
## 上次填充结果:已填 34 项,跳过 12 项
- **已有选择,未覆盖**(8 项):月、月、月、月 …
- **选项里没有「2/30(实验班)」,请手动选**(2 项):专业排名、专业排名
「8 个字段都是同一个原因」这种系统性问题,一眼就能看出来。
「至今」勾选框紧挨着起止时间。把它计入「容器里有几个控件」,实习/项目的起止时间就是 4 格 + 1 勾选 = 5,超过闸门 → 够不着自己的标题「起止时间」→ 已填的起始格不占位 → 空着的结束格被当成起始格,入职年月被写进离职年月。所以计数时排除 checkbox / radio / file:它们是字段的修饰,不是独立字段。
同一类错误反复出现,每次表现都不同:
| 现场 | 拼接文本 | 后果 |
|---|---|---|
| OPPO 合并区块 | 「竞赛 奖学金 名称 获奖时间 级别 等级」 | 整块六个字段全填成日期 |
| Shopee 姓名框 | 「性别 女 出生日期 年龄 最高学历毕业院校」 | 含「院校」→ 把 fullName 规则整条作废,姓名被填成「女」 |
| Shopee 空年份框 | 「学历 学习形式 … 学院 专业名称 …」 | 含「学院」→ 年份框命中 edu.college |
三道防线,缺一不可:
- 结构闸门(最有效):向上找标签时,一旦容器里的表单控件超过 4 个,就停止——它装的是「一整块字段」,文字必然是多个标签拼的。阈值取 4 是因为一个「起止时间」最多拆成 年/月/年/月 四个框共用一个标签;
- 排除词不连坐:排除词只在「像标签」的短候选(≤12 字)上判定,才能全局作废一条规则;拼接文本里出现的词跟本字段无关。但命中排除词的那一条候选自身永远作废——「意向工作城市是否可以调剂」既含「意向工作城市」也含「调剂」;
- 短标签的完全匹配优先:区块标题已确定归属时,
^类别$这种锚定精确命中,压过在长文本里的部分命中; - 有自己的标签就不看邻居(最后加的一道,也是最狠的一道):候选分成「本字段自己的」(
<label>/aria/placeholder/ 只装一个控件的容器的文字)和「上下文的」。只要存在前者,后者一概不参与匹配——自己的标签匹配不上,答案就是「没匹配上」,而不是拿上下文瞎猜。 「请问你是否填写了本科学历」曾被区块文字「自我评价 请问你是否…」带着命中intro,把整段自我介绍填进一个该答是/否的下拉,就是缺这一道。 注意name/id不算「自己的标签」——它们常是e1-t1这种没语义的串,算进去会让共用一个标签的两个输入框(中兴的「就读时间 []-[]」)因为有 id 就不再去看那个共用标签。
原生 value setter + input / change 事件,兼容 React / Vue 受控组件。只填空白字段,已有内容一律跳过,重复点击安全幂等。
写完不算完:Moka 这类站点的「年/月」框其实是下拉组件的输入口,React 受控状态若只接受点选,会在 onChange 后把写进去的值吐回来——不校验的话,报告说「已填」,表单数据里却是空的,这是最阴险的失败方式。所以所有文本写入在循环结束后统一回读校验:值还在 → 已填;被清空 → 报「写入后被页面组件丢弃,这个框可能要点选」;被改写 → 报「被页面改成 xx,请核对」。
这类组件内部的 input 只是搜索框,直接写值在失焦后会被丢弃,必须走真实交互:
- 派发
pointerdown / mousedown / mouseup / click(antd 在 mousedown 阶段展开,只发 click 打不开); - 轮询等待选项层渲染(通常 portal 到
<body>),只认点开之后才出现的选项——Moka 的左侧锚点导航与选项行同 class(sd-Menu-container),不做快照差分的话「教育背景」这类导航项会混进选项列表; - 按语义匹配选项文本(学历、性别有专门归一化);目标不在已渲染选项里时(年份列表常虚拟滚动),往搜索框打字过滤再从过滤结果里挑;
- 点中并轮询确认值已写入。
安全约束:已有选择不覆盖;匹配不到就按 Esc 关掉浮层并报「选项里没有 XX」,绝不乱选;被组件包住的原生 input 排除在直接写值之外。
日历不去翻——antd 的格子带 title="2027-06-15" / title="2027-06",按 日 → 月 → 年 的精度逐个精确命中;都命中不了才退回「往输入框打字 + 回车」。
级联逐列展开并按档案路径走。档案只写「上海」而级联要求选到区县时,引擎会停下报错而不是随手挑一个区——在求职表上编造具体地址,比留空危害大得多。只有该级唯一选项、或某选项与上一级同名(上海 → 上海市)才自动推进。
因为要等渲染,fill() 是异步的。实测一次填充约 15ms。
你有 3 篇论文而表单默认只给 1 块时,引擎会先把「+ 添加」点出来再填。这是插件唯一主动改变页面结构的操作,约束比别处都严:
- 只点文字明确是「添加 / 新增 / +」的元素,任何含提交 / 保存 / 删除 / 上传 / 搜索字样的一律排除;
- 每点一次都校验该域的区块数确实增加了,没增加就立刻停手,绝不连点;
- 每域最多补 6 段,且只在「页面上已存在该域字段」时才动——页面没有论文区就不会凭空去找按钮;
- 按钮文字没写明类别(只写「添加」或「+」)时,取该域最后一个字段之后最近的那个,且中间不能夹着别的域的字段。
找不到可用按钮时会在报告里说明「档案有 N 段但页面只有 M 段」,提示手动点开。
档案只存在 chrome.storage.local,清一次浏览数据就全没了(包括几百 KB 的简历 PDF)。判据不只看天数——「改过但没备份」比「30 天没备份」更值得提醒,前者丢的是你刚花时间填的内容:
| 状态 | 提示 |
|---|---|
| 从未备份过 | 红色警告 |
| 档案在上次备份后改过 | 红色警告 |
| 超过 30 天没备份 | 黄色提示 |
| 刚备份过 | 不打扰 |
弹窗里可直接导出,不必先进档案页。
可靠地从 PDF 抽结构化字段需要打包 pdf.js(约 1MB),而简历排版千差万别(双栏、表格、图片型 PDF、中英混排),规则抽取的错误率会直接变成投递表单上的错误信息。更稳妥的是一次性解析成档案 JSON、人工核对、之后长期复用——档案里的信息本就比 PDF 版式稳定得多。
Chrome 会缓存扩展文件。改了 content.js 之后必须去 chrome://extensions 点插件卡片上的刷新按钮(圆形箭头)——重开页面、重启浏览器都不行。
当前引擎版本 1.7.0(content.js 顶部 VERSION,与 manifest.json 保持一致)。
manifest.json 扩展清单(含 Firefox 的 gecko 配置)
content.js 识别与填充引擎(全部逻辑都在这里,零浏览器 API)
api.js 浏览器 API 适配层(Chrome / Firefox 命名空间与权限)
popup.html/js 弹窗:一键填充、扫描清单、档案自检
options.html/js 档案编辑页
styles.css 弹窗与档案页共用样式
backup.js 备份状态判据(弹窗与档案页共用)
profile-template.json 空白档案模板(发给新用户的起点)
test/ 回归测试(见下)
在 content.js 的 RULES 表里加一行。顺序即优先级,具体规则必须排在笼统规则之前,ex 是排除词——任一候选文本命中即整条规则作废:
{ k: 'edu.school', a: 1, re: /学校名称|毕业院校|学校|院校/i, ex: /高中|初中|小学/i },
// ↑ 域.字段 ↑ 锚点(参与分段计数) ↑ 排除词改完必须跑一遍回归——这套规则里排除词彼此牵连,单看一条很难判断会不会误伤。
python3 -m http.server 8642 --directory tools/resume-quickfill浏览器打开 http://localhost:8642/test/index.html,点「▶︎ 运行全部测试」。
当前基线:21 张页面 / 475 项断言全部通过。
| 测试页 | 覆盖场景 |
|---|---|
testform.html |
各种表单写法混排,含「紧急联系人电话」「应聘职位」等不该被填的反例;学历/学位用词区分 |
moka-style.html |
Moka 布局:锚点在前,起止时间在区块末尾 |
zte-style.html |
中兴校招:时间区间在锚点之前,论文/竞赛/荣誉均为结构化区块 |
moka-ym.html |
日期拆成「年」「月」两个文本框,一段经历四个框 |
moka-antd.html |
antd DOM 结构 + 四类填错陷阱(职位搜索框、校园经历、导师姓名、专利软著) |
baidu-style.html |
百度校招:奖项说明多框、项目职务/职责分列、会议期刊 |
antd-live.html |
可交互 antd 下拉:真实展开 / 点选,验证语义匹配与「不覆盖已有值」 |
antd-datetime.html |
可交互日历与城市级联 |
expand-blocks.html |
自动点「+ 添加」展开区块,并验证提交/删除/上传/搜索按钮一次都没被点 |
backup-status.html |
备份提醒的四种判据 |
dji-style.html |
大疆校招:赛事/获奖两个独立区块,靠区块标题区分只有「描述」的泛化字段 |
oppo-style.html |
OPPO 校招:竞赛/奖学金合并成一个列表;紧急联系人区块禁填 |
moka-live-snapshot.html |
真实页面 DOM 快照(用户从 Shopee 申请页复制、只替换个人信息):只验识别——标签定位 / 规则命中 / 区块归属 / 「添加」按钮认领 |
moka-real.html |
Moka 真实 DOM 逐字复刻(class 名与嵌套层级照搬):值在 display-value、校验提示不当标签、组件内 placeholder |
moka-sd.html |
Moka sd-Dropdown 全家桶:点选提交、打字过滤、month-range 四联、日期年月对、占位词假已填、DOM 骨架采样 |
shopee-style.html |
Shopee 校招:基本信息区「最高学历毕业院校」不占教育锚点;「学历」排在锚点前;问句字段不作答 |
oppo-widgets.html |
OPPO 合并区块的真实控件构成(类别/级别/类型三个下拉),验证三个维度不互相串 |
oppo-short.html |
合并区块的短标签写法;并验证已填内容(含单选组与已上传附件)一律不覆盖 |
progress.html |
页面内进度条:逐字段推进、慢路径单独提示、收尾摘要、出错也能收场 |
cross-browser.html |
适配层:模拟 Chrome / Firefox 两种命名空间与四种权限状态 |
custom-widgets.html |
诊断:自定义控件能否被扫描看见 |
单页调试:直接打开 页面.html?run=1,结果显示在页面顶部横幅。
测试档案在 test/run.js 的 RQF_TEST_PROFILE,断言在同文件的 CHECKS,全部是合成数据(本目录在同步网盘的 git 仓库内,不放真实个人信息)。
这三条都实际踩过,改测试页时注意:
- iframe 不能用
display:none—— 那样内部所有元素尺寸为 0,引擎的可见性判断会把整页字段判掉。test/index.html用的是移到屏幕外但保留布局; - 模拟 antd 时
.ant-select-selection-search必须绝对定位铺满 —— 写成零宽 flex 子元素的话,内部 input 宽度为 0,同样会被判成不可见; getComputedStyle返回的是活对象 —— 下一次填充会把进度条节点摘掉重建,到那时再读属性就全是空值。断言里要当场取成快照。