Codex 实战 Skills:用 TaoToken 统一 Key 编写批量防伪加密水印 Skill 1. 保密文档批处理的真实痛点与 Codex Skills 的切入点如果你在企业里负责过文档分发大概率遇到过这种场景法务部要你把一份 100 页的技术白皮书发给 30 个供应商每份都要带上对方公司名的防伪水印还要单独设一个打开密码。手动用 Acrobat 一份份加一天就没了而且中途漏掉一份没加密后果可能比加班更严重。我试过用纯 Python 脚本硬扛结果卡在两个地方一是水印字体和透明度在不同 PDF 上表现不一致二是加密权限的位掩码写错一位打印权限就全开了。后来把整套流程封装成 Codex Skill才算把「输入约定 → 水印生成 → 加密输出 → 校验解密」这条链路固定下来。Codex Skills 在这里的价值不是帮你写代码而是把「批量防伪加密水印」这件事变成一个可复用、可版本管理的原子能力。你只需要约定好输入目录、水印模板、密码策略剩下的扫描、渲染、加密、日志全部自动跑完。适合谁适合需要定期向外部合作方分发保密 PDF 的运维、安全工程师以及想把文档 DLP 流程自动化的后端开发。这一篇我会以 100 份文档为样本拆解 Skill 的输入约定、水印生成与加密输出流程给出可复制的 Skill 配置片段和 TaoToken 统一 Key 接入示例最后附上批量运行后的水印校验与解密验证动作。你跟着做基本能一次跑通。2. TaoToken 统一 Key 前置让 Skill 调用模型时不再散落密钥Codex Skills 在运行过程中如果需要调用大模型来做水印文案生成、文档摘要或异常判断就会涉及 API Key 的管理。传统做法是把 Key 硬编码在脚本里或者每个 Skill 单独配一份环境变量结果就是密钥散落、轮换困难、审计无门。TaoToken 在这里的角色是提供一个统一的 API 入口让你用同一个 Key 驱动多个 Skill 的模型调用。先明确几个地址后面配置会反复用到官网入口https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 基地址https://taotoken.net/api模型对话页https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewriteCoding Plan 页https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewriteAPI Keys 管理https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewriteClaude Code 接入https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_codeutm_campaignrewrite拿到 Key 之后不要直接写进 Skill 的源码。推荐的做法是在项目根目录建一个.env文件把 Key 和 Base URL 放进去Skill 运行时通过环境变量读取。这样你在 Codex 里切换不同 Skill 时只需要维护一份密钥配置。具体操作登录控制台后进入 API Keys 页面创建一个新 Key复制出来。然后在你的 Skill 项目目录下创建.env# .env TAOTOKEN_API_KEYsk-你的实际Key TAOTOKEN_BASE_URLhttps://taotoken.net/api接着在 Skill 的入口脚本里加载import os from dotenv import load_dotenv load_dotenv() API_KEY os.getenv(TAOTOKEN_API_KEY) BASE_URL os.getenv(TAOTOKEN_BASE_URL) if not API_KEY: raise RuntimeError(TAOTOKEN_API_KEY 未设置请检查 .env 文件)这里有个容易踩的坑.env一定要加进.gitignore否则 Key 会跟着代码进仓库。另外如果你在 Codex 的 Skill 配置里直接写env字段注意不要和系统环境变量冲突优先级是 Skill 配置 系统环境变量。对于需要长期跑批量任务的场景比如每天定时处理 100 份文档建议用 Coding Plan 的额度比按次调用更划算而且 Key 的权限可以单独限制避免一个 Key 被所有 Skill 共用导致审计混乱。配置完成后你可以先用模型对话页发一条测试请求确认 Key 和 Base URL 都能通再进入下一步的 Skill 编写。3. 可复制的 Skill 配置输入约定、水印生成与加密输出这一节是核心我会给出完整的 Skill 配置片段和代码结构。先约定输入输出再拆水印生成最后做加密输出。3.1 输入约定与目录结构Skill 的输入约定必须固定否则批量跑的时候文件名一乱就找不到对应关系。我采用的约定是skill_workspace/ ├── input_pdfs/ # 原始 PDF命名规则{接收方}_{文档名}.pdf │ ├── ClientA_Proposal.pdf │ ├── ClientB_Proposal.pdf │ └── ... ├── output_pdfs/ # 加密后的 PDF命名规则secure_{原文件名} ├── config/ │ └── skill_config.json ├── logs/ │ └── watermark_process.log └── skill.pyskill_config.json是 Skill 的配置中心所有可变参数都放这里避免改代码{ input_folder: ./input_pdfs, output_folder: ./output_pdfs, watermark: { text_template: CONFIDENTIAL - {recipient} ONLY, font_path: /System/Library/Fonts/PingFang.ttc, font_size: 42, transparency: 0.15, rotation: 45, color_rgb: [0.5, 0.5, 0.5] }, encryption: { user_password_template: Open_{recipient}_2024, owner_password: OwnerSecure_2024, allow_printing: false, allow_copying: false, allow_modifying: false, algorithm: AES-256 }, model: { base_url: https://taotoken.net/api, model_id: gpt-4o-mini, api_key_env: TAOTOKEN_API_KEY } }注意text_template和user_password_template里的{recipient}占位符Skill 会从文件名里解析出接收方名称动态替换。这样 100 份文档就能自动生成 100 个不同的水印和密码不需要手动改。3.2 水印生成reportlab Canvas 的关键参数水印生成用 reportlab 的 Canvas核心是透明度、旋转和字体注册。下面这段代码可以直接复制from reportlab.lib.pagesizes import A4 from reportlab.pdfgen import canvas from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont def register_font(font_path: str) - str: if font_path and os.path.exists(font_path): pdfmetrics.registerFont(TTFont(CustomFont, font_path)) return CustomFont return Helvetica def create_watermark_pdf(text: str, font_name: str, cfg: dict, out_path: str): c canvas.Canvas(out_path, pagesizeA4) width, height A4 c.setFillAlpha(cfg[transparency]) c.setFont(font_name, cfg[font_size]) c.setFillColorRGB(*cfg[color_rgb]) c.saveState() c.translate(width / 2, height / 2) c.rotate(cfg[rotation]) text_width c.stringWidth(text, font_name, cfg[font_size]) c.drawString(-text_width / 2, -cfg[font_size] / 2, text) c.restoreState() c.save() return out_path这里有几个参数需要你根据实际文档调整。transparency设成 0.15 是比较安全的区间既能看清又不遮挡正文rotation用 45 度是行业惯例裁剪难度高font_size42 在 A4 上大约占页面宽度的三分之一视觉上够醒目。如果你用的是中文字体font_path在 macOS 上可以指向 PingFang.ttcWindows 上指向 simhei.ttfLinux 上如果没有中文字体建议水印文案先用英文避免出现方框乱码。3.3 加密输出权限位掩码与 AES-256加密部分用 pypdf 的encrypt方法关键是权限位掩码别写错。下面是对照表权限位掩码说明打印0x04允许打印文档复制0x08允许复制文本和图形修改0x10允许修改文档内容注释0x20允许添加注释如果你要禁止打印、复制、修改权限码就是 0。代码里这样构建from pypdf import PdfReader, PdfWriter def encrypt_pdf(input_path, output_path, watermark_path, user_pw, owner_pw, cfg): reader PdfReader(input_path) watermark_page PdfReader(watermark_path).pages[0] writer PdfWriter() for page in reader.pages: page.merge_page(watermark_page, overTrue) writer.add_page(page) perm_code 0 if cfg[allow_printing]: perm_code | 0x04 if cfg[allow_copying]: perm_code | 0x08 if cfg[allow_modifying]: perm_code | 0x10 writer.encrypt( user_passworduser_pw, owner_passwordowner_pw, permissions_flagperm_code, algorithmAES-256 ) with open(output_path, wb) as f: writer.write(f)merge_page的overTrue表示水印覆盖在正文之上配合 0.15 的透明度效果就是水印浮在文字上方但不影响阅读。如果你希望水印在文字下方改成overFalse但要注意有些 PDF 的图层顺序会导致水印被完全遮住实测下来overTrue更稳定。3.4 批量调度与日志批量处理用pathlib扫描目录每个文件独立 try-except单个失败不中断整体from pathlib import Path import logging def process_batch(config: dict): input_dir Path(config[input_folder]) output_dir Path(config[output_folder]) output_dir.mkdir(parentsTrue, exist_okTrue) pdf_files list(input_dir.glob(*.pdf)) success, fail 0, 0 for pdf_file in pdf_files: recipient pdf_file.stem.split(_)[0] watermark_text config[watermark][text_template].format(recipientrecipient) user_pw config[encryption][user_password_template].format(recipientrecipient) try: wm_path create_watermark_pdf(watermark_text, font_name, config[watermark], temp_wm.pdf) encrypt_pdf( str(pdf_file), str(output_dir / fsecure_{pdf_file.name}), wm_path, user_pw, config[encryption][owner_password], config[encryption] ) success 1 logging.info(f成功: {pdf_file.name}) except Exception as e: fail 1 logging.error(f失败: {pdf_file.name}, 原因: {e}) logging.info(f批量完成成功 {success}失败 {fail})跑完 100 份文档日志里会清楚记录每一份的状态。如果某一份因为字体缺失或 PDF 损坏失败其他 99 份不受影响。4. 验证请求与成功结果水印校验与解密验证批量跑完之后不能只看日志说成功就完事必须做两步验证水印是否真的盖上去了加密是否真的生效了。4.1 水印校验最直接的方法是用 pypdf 读取输出文件检查页面内容流里是否包含水印文本。但更实用的是用命令行工具快速抽检# 用 pdftotext 提取文本看水印文字是否出现 pdftotext output_pdfs/secure_ClientA_Proposal.pdf - | grep CONFIDENTIAL如果输出里有CONFIDENTIAL - ClientA ONLY说明水印文本已经写入。但文本提取只能验证文字存在验证不了透明度和旋转。要验证视觉效果建议用 Python 渲染第一页为图片from pdf2image import convert_from_path images convert_from_path(output_pdfs/secure_ClientA_Proposal.pdf, first_page1, last_page1) images[0].save(check_watermark.png)打开图片你应该能看到 45 度倾斜的灰色半透明水印覆盖在正文上方。如果水印太淡或太浓回去调transparency参数。4.2 解密验证加密验证要确认两件事用正确密码能打开用错误密码打不开且权限限制生效。from pypdf import PdfReader # 正确密码 reader PdfReader(output_pdfs/secure_ClientA_Proposal.pdf) if reader.is_encrypted: result reader.decrypt(Open_ClientA_2024) print(f解密结果: {result}) # 应该输出 1 或 2表示成功 print(f页数: {len(reader.pages)}) # 错误密码 reader2 PdfReader(output_pdfs/secure_ClientA_Proposal.pdf) if reader2.is_encrypted: result2 reader2.decrypt(WrongPassword) print(f错误密码解密结果: {result2}) # 应该输出 0表示失败如果正确密码返回 1 或 2错误密码返回 0说明加密生效。权限验证可以用reader.user_access_permissions查看确认打印和复制都是 False。4.3 用 TaoToken 模型做异常摘要100 份文档跑完日志可能有几百行。你可以用 TaoToken 的模型对话能力把日志丢给模型做异常摘要。配置如下import requests def summarize_log(log_text: str): resp requests.post( f{BASE_URL}/v1/chat/completions, headers{Authorization: fBearer {API_KEY}}, json{ model: gpt-4o-mini, messages: [ {role: system, content: 你是一个日志分析助手请提取失败项和原因。}, {role: user, content: log_text} ] } ) return resp.json()[choices][0][message][content]这样你不需要逐行翻日志模型会直接告诉你哪几份失败了、可能的原因是什么。注意 Base URL 用https://taotoken.net/api不要加 UTM 参数那是给网页用的。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth批量跑 Skill 的时候报错集中在几个地方。我按真实遇到的顺序列出来你对照排查。5.1 401 Unauthorized这是最常见的通常是 Key 没读到或 Key 无效。检查顺序.env文件是否在 Skill 运行目录下load_dotenv()是否在读取 Key 之前调用。环境变量名是否和代码里一致比如代码读TAOTOKEN_API_KEY.env里写的是TAOTOKEN_KEY那就读不到。Key 是否被复制时带了空格或换行建议用strip()处理。如果用的是 Coding Plan 的 Key确认该 Key 是否有权限调用你指定的模型。修复后重新跑一份文档测试不要直接跑 100 份。5.2 local proxy failed这个报错通常出现在你本地网络环境有代理设置但 Skill 请求 TaoToken API 时走了代理导致连接失败。检查echo $HTTP_PROXY echo $HTTPS_PROXY如果有值在 Skill 里显式禁用代理import os os.environ[HTTP_PROXY] os.environ[HTTPS_PROXY] 或者在 requests 调用时加proxies{http: None, https: None}。注意不要用任何非正规的网络工具直接连 TaoToken 的 API 地址即可。5.3 reading choices 报错这个报错说明模型返回的 JSON 结构里没有choices字段通常是请求体格式不对或模型 ID 写错。检查model字段是否拼写正确比如gpt-4o-mini不要写成gpt4o-mini。请求头Content-Type是否为application/json。如果返回的是错误信息先打印resp.text看完整内容再定位。resp requests.post(...) print(resp.status_code) print(resp.text) # 先看原始返回 data resp.json() if choices in data: content data[choices][0][message][content] else: raise RuntimeError(f模型返回异常: {data})5.4 OAuth 相关报错如果你在 Codex 里配置了 OAuth 方式的接入报错通常是 token 过期或回调地址不匹配。检查回调地址是否和控制台里配置的一致包括端口号。token 是否过期重新走一次授权流程。如果同时配了 API Key 和 OAuth确认 Skill 实际用的是哪一种不要混用。对于批量文档处理这种场景我建议直接用 API Key比 OAuth 简单而且 Key 可以单独限制权限审计更方便。5.5 三件套检查清单如果你用的是 CC Switch、Cline MCP 或 Codex 的auth.json出现连接问题时按这三件套逐项核对配置项正确值常见错误Base URLhttps://taotoken.net/api写成带 UTM 的网页地址API Keysk-开头复制时漏字符或带空格Model IDgpt-4o-mini等拼写错误或用了不存在的模型auth.json的配置示例{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model: gpt-4o-mini }三项都对上基本不会出现连接问题。如果还报错先去模型对话页发一条测试消息确认 Key 本身可用再排查 Skill 侧。6. 语义一致 CTA把 Skill 接入你的文档安全流程到这里你已经有了一个能跑通 100 份文档的防伪加密水印 Skill。接下来要做的是把它接入你日常的文档分发流程。如果你在排障或接入阶段遇到问题先去 API Keys 页面确认 Key 状态再对照接入文档检查 Base URL 和 Model ID。文档里有完整的请求示例和错误码说明比在代码里猜要快。如果你想先验证模型调用是否正常用模型对话页发一条测试消息确认返回结构里有choices字段再回到 Skill 里跑批量任务。如果你打算把这个 Skill 做成每天定时跑的长期任务比如每天早上 8 点自动处理前一天的待分发文档建议用 Coding Plan 的额度Key 的权限可以单独限制在文档处理相关的模型上避免和其他业务混用。最后提醒一个实操细节批量跑之前先用 3 份文档做小样本测试确认水印位置、透明度、密码规则都符合预期再放开到 100 份。我踩过的坑是第一次直接跑全量结果水印字体没注册成功100 份全变成方框只能删掉重来。小样本测试花 5 分钟能省你半小时。