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 的描述问题的链路是招聘网站、ATS 表单、猎头邮件中粘贴的网页文本可能含有不可见 Unicode例如用于隐藏标记的 bidi 控制符、零宽连接符Agent 基于这些输入生成简历CV、求职信cover letter或申请答案application answers用户看到的是干净的文档但字节层面携带了隐藏字符可能在 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 spaceU00A0被视为正常排版字符既不会被标记也不会被替换——这是有意为之避免破坏文档原有的排版意图。--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 SEPbidi 控制0x200E/0x200FLRM / RLMbidi 控制0x202A–0x202ELRE / RLE / PDF(bidi) / LRO / RLObidi 隔离0x2066–0x2069LRI / RLI / FSI / PDIUnicode 标签0xE0000–0xE007FTAG区间判定报告中按TAG UXXXXX输出变体选择符0xFE00–0xFE0F、0xE0100–0xE01EFVARIATION-SEL区间判定从源码结构看标签字符和变体选择符不是逐一点名而是用区间判断isTag、isVS覆盖报告输出为TAG Uxxxxx、VARIATION-SEL Uxxxxx这样的动态标签因此能兜住整个区段而无需穷举。扫描函数scanText()按 code point 遍历全文把每个命中的标签名计次最终产出一个{标签: 数量}的对象audit模式把它以 JSON 形式打印在 FAIL 行里便于定位污染分布❌ FAIL output/acme-cv.html: hidden chars: {ZWJ:3,BOM/ZWNBSP:1}四、退出码契约把它接成 CI 里的发送前关卡clean-markers.mjs 末尾的参数解析与退出逻辑约定了三档退出码未提供任何文件时打印用法并以退出码 2结束任一文件 FAILaudit 发现隐藏字符或 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).length0;只有复扫结果为空才输出✅ 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),仅供参考