career-ops 中的 clean-markers:为 AI 生成的求职文本加装不可见 Unicode 审计与清理关卡
2026/9/6 19:41:22 网站建设 项目流程

career-ops 中的 clean-markers:为 AI 生成的求职文本加装不可见 Unicode 审计与清理关卡

【免费下载链接】career-opsOpen-source AI job search: scan job portals, evaluate listings into a structured A-H report with a global 1-5 score, tailor your CV, track applications — runs locally in your AI coding CLI (Claude Code, Codex, OpenCode, Antigravity…)项目地址: https://gitcode.com/GitHub_Trending/ca/career-ops

在 career-ops 的工作流中,职位描述、申请表单和猎头邮件都属于不可信数据——它们可能携带肉眼不可见的 Unicode 控制字符(零宽空格、bidi 方向控制符、Unicode 标签字符等),而 AI Agent 在改写简历、生成求职信或填写申请答案时,很可能把这些隐藏字符原样复制进最终产物。clean-markers.mjs是 career-ops 内置的一个零依赖审计/清理工具:它能对生成的文本文件做不可见字符扫描(audit),或在清理后重新验证(clean),并以退出码作为"发送前关卡"(pre-send gate)接入脚本或 CI。读完本文,你将掌握该工具的完整命令用法、它拦截的 20 余类隐藏字符清单、退出码契约,以及 HTML 场景下保护<style>/<script>块不被误改的源码级实现机制。

一、问题背景:不可信输入如何污染生成的求职文档

按 docs/CLEAN_MARKERS.md 的描述,问题的链路是:

  1. 招聘网站、ATS 表单、猎头邮件中粘贴的网页文本可能含有不可见 Unicode(例如用于隐藏标记的 bidi 控制符、零宽连接符);
  2. Agent 基于这些输入生成简历(CV)、求职信(cover letter)或申请答案(application answers);
  3. 用户看到的是"干净"的文档,但字节层面携带了隐藏字符,可能在 ATS 解析、邮件渲染或 PDF 转换中造成难以排查的异常。

clean-markers.mjs的定位就是在这条链路末端加一道关卡:在生成的文本文件离开你的机器、或在 HTML/Markdown 被渲染成最终文档之前,先审计或清除这些用户从未主动输入过的字符

工具边界同样在文档中明确声明:

  • 只处理文本文件,不编辑 PDF 元数据;
  • 运行时不安装任何包(never runsnpm install),仅使用 Node 标准库;
  • 除非显式传入--ascii,否则不改动任何可见标点

二、使用方式:audit / clean / --ascii 三档操作

文档给出的完整用法如下(均可从仓库根目录直接执行):

node clean-markers.mjs audit output/acme-cv.html # report only, never modifies node clean-markers.mjs clean output/acme-cover-letter.md node clean-markers.mjs clean --ascii output/cover-letter.txt # also normalize smart quotes / dashes node clean-markers.mjs audit output/*.html output/*.md # globs OK

三个要点:

  • audit永不修改文件。它的用途是"证明"一份文档是干净的。只要有一个文件 FAIL,进程退出码就是1,因此可以直接用作脚本或 CI 步骤中的发送前关卡。
  • clean会剥离不可见 Unicode。注意:不换行空格(non-breaking space,U+00A0)被视为正常排版字符,既不会被标记也不会被替换——这是有意为之,避免破坏文档原有的排版意图。
  • --ascii(仅对clean的文本模式有效)额外把弯引号转直引号、em/en 破折号转连字符、省略号转...,适合纯文本质求职信或邮件场景。

当生成的文件即将对外发出或被渲染时,典型调用序列是:

node clean-markers.mjs audit output/acme-cv.html output/acme-cover-letter.md node clean-markers.mjs clean output/acme-cv.html output/acme-cover-letter.md

三、拦截范围:被标记的不可见字符全集

结合 clean-markers.mjs 的源码,label()函数定义了工具的判定规则,可分为四类:

类别码点名称(源码中的标注)
零宽标记0x200BZERO-WIDTH SPACE
零宽标记0x200CZWNJ(零宽非连接符)
零宽标记0x200DZWJ(零宽连接符)
零宽标记0x2060WORD JOINER
零宽标记0xFEFFBOM/ZWNBSP
软连字符0x00ADSOFT HYPHEN
分隔符0x180EMONGOLIAN VOWEL SEP
bidi 控制0x200E/0x200FLRM / RLM
bidi 控制0x202A0x202ELRE / RLE / PDF(bidi) / LRO / RLO
bidi 隔离0x20660x2069LRI / RLI / FSI / PDI
Unicode 标签0xE00000xE007FTAG(区间判定,报告中按TAG U+XXXXX输出)
变体选择符0xFE000xFE0F0xE01000xE01EFVARIATION-SEL(区间判定)

从源码结构看,标签字符和变体选择符不是逐一点名,而是用区间判断isTagisVS)覆盖,报告输出为TAG U+xxxxxVARIATION-SEL U+xxxxx这样的动态标签,因此能兜住整个区段而无需穷举。

扫描函数scanText()按 code point 遍历全文,把每个命中的标签名计次,最终产出一个{标签: 数量}的对象;audit模式把它以 JSON 形式打印在 FAIL 行里,便于定位污染分布:

❌ FAIL output/acme-cv.html: hidden chars: {"ZWJ":3,"BOM/ZWNBSP":1}

四、退出码契约:把它接成 CI 里的发送前关卡

clean-markers.mjs 末尾的参数解析与退出逻辑约定了三档退出码:

  • 未提供任何文件时打印用法并以退出码 2结束;
  • 任一文件 FAIL(audit 发现隐藏字符,或 clean 后复扫仍不干净)时退出码 1
  • 全部干净时退出码 0

这意味着它可以直接嵌入 shell 脚本或 CI 步骤:退出码非零即阻断发送,无需解析日志文本。这也是文档中"Exit code 1 if any file FAILS, so it works as a pre-send gate"承诺的底层依据。

五、实现解析:clean 模式的去污与复扫验证

cleanText(t, ascii)的清理策略是逐字符删除(clean-markers.mjs):

  • 软连字符(0x00AD)直接跳过丢弃;
  • 命中NAMED映射、标签区、变体选择符区间的字符直接跳过丢弃;
  • 其余字符原样保留。

随后若传了--ascii,再做四组替换:

out.replace(/[‘’‚‛]/g, "'") // 单引号族 → 直单引号 .replace(/[“”„‟]/g, '"') // 双引号族 → 直双引号 .replace(/[–—―]/g, '-') // en/em/水平破折号 → 连字符 .replace(/…/g, '...') // 省略号 → 三个点

清理完成后,工具会重新读盘、重新扫描(re-scan),以复扫结果作为本次操作的最终判定:

const after = readFileSync(file,'utf8'); const re = scanText(isHtml ? maskCode(after).masked : after); const ok = Object.keys(re).length===0;

只有复扫结果为空才输出✅ cleaned text,否则输出⚠️——这种"写完再验"的闭环保证了clean的可靠性不依赖单次替换的假设。

另一个值得注意的设计:clean模式下,如果文件本身没有隐藏字符且没有--ascii,工具不会写盘Object.keys(found).length || opts.ascii为假时直接跳过写操作),避免无谓地改变文件 mtime 或触发下游 diff。

六、HTML 的特殊处理:<style><script>永不进入扫描与清理

简历类 HTML 产物(如 career-ops 的 templates/cv-template.html 渲染结果)必然内嵌 CSS 与 JS。如果盲目对全文做替换,可能破坏样式表或脚本内容。因此对.html/.htm文件,工具会先用正则把整块<style>…</style><script>…</script>掩码(mask)成占位 token__CLEAN_MARKERS_MASK_n__,只对外部正文做扫描与清理,之后再还原(clean-markers.mjs):

// Never scan or transform inside <style>/<script> — marker removal and --ascii must not touch CSS/JS. // (same approach as generate-pdf.mjs) const CODE_BLOCK = /<(style|script)\b[^>]*>[\s\S]*?<\/\1>/gi;

源码注释明确说明这与 generate-pdf.mjs 中 HTML 净化阶段"先掩码、后还原"的做法一致——从源码结构看,该文件同样会把<style>/<script>等区块替换为占位符再处理正文,两处实现共享同一套防御思路:对"文本之外"的内容保持字节级不动。

七、支持的文件类型与跳过的边界情况

TEXT_EXT白名单(clean-markers.mjs)限定为:

.html .htm .md .txt .json .csv .svg .xml .tex

边界行为:

  • 文件不存在:打印⚠️ <file>: not found,且该文件不阻断整体判定;
  • 扩展名不在白名单(如.pdf、图片):打印➖ <file>: unsupported type (…) — skipped,同样视为通过;
  • .tex在列,配合 career-ops 的 LaTeX 简历链路(generate-latex.mjs、modes/latex.md)生成的.tex中间产物也纳入了保护范围;
  • PDF 一律不处理,这与文档"does not edit PDF metadata"的声明吻合。

八、在 career-ops 升级体系中的位置

clean-markers.mjs 被列在 update-system.mjs 的系统文件清单中,即它属于升级流程会维护的系统层脚本,而非用户本地自定义文件;CHANGELOG.md 中亦记录了该工具作为特性引入:"clean-markers — audit/strip hidden Unicode from generated text outputs"。

一个实用的组合方式:在批量投递脚本里,先对output/下所有生成物执行audit作为发送前关卡,失败(退出码 1)则暂停并向用户展示隐藏字符分布,再决定是否用clean(加--ascii视下游渠道而定)处理后重发。整个流程无需任何第三方依赖,只需 Node 运行时即可在本地完成。

小结

clean-markers.mjs用不到百行的零依赖实现,解决了"不可信网页输入 → AI 生成文档"链路中最隐蔽的一类污染:不可见 Unicode。它以audit/clean两种模式、三档退出码、20 余类隐藏字符的精确判定(含标签区与变体选择符区间)、以及 HTML 中<style>/<script>块的掩码保护,构成了 career-ops 生成文本离开本机前的一道可脚本化、可进 CI 的质量关卡。

【免费下载链接】career-opsOpen-source AI job search: scan job portals, evaluate listings into a structured A-H report with a global 1-5 score, tailor your CV, track applications — runs locally in your AI coding CLI (Claude Code, Codex, OpenCode, Antigravity…)项目地址: https://gitcode.com/GitHub_Trending/ca/career-ops

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询