)
1. 为什么我要用 Codex 重写二维码生成器二维码这东西看着简单真做起来坑不少。我最早用现成在线工具给一批商品做标签结果发现三个问题一是批量导入只能一次几十条多了就卡二是想加公司 Logo 上去工具要么收费要么把码点糊掉导致扫不出来三是生成完就完事想改个跳转链接得重新做一批。后来我干脆用 Codex 从零撸了一个把批量生成、Logo 定制、容错等级控制这几件事一次性解决。这篇要交付的是一个能跑起来的二维码生成器项目核心能力包括批量导入文本或链接生成二维码、在二维码中心叠加 Logo 并自动控制容错等级与边距、导出 PNG 打包下载。适合谁看前端有一定基础、想用 AI 辅助写完整项目的同学或者运营/电商同学想自己搞个批量制码工具。全程用自然语言驱动 Codex 生成代码你负责描述需求和验证结果。先说清楚技术选型避免你跟着做的时候版本对不上。前端用 Vue 3 ViteUI 用 Element Plus二维码核心库用qrcode样式渲染走 Canvas API 手绘Logo 合成也用 Canvas。后端这块如果你只做本地批量生成其实纯前端就够了但如果你想要活码管理、扫码统计那就得配 Node.js Express SQLite。本文重点放在前端生成链路后端只给关键配置片段保证你能复现。环境检查先跑一遍版本不对后面全是玄学报错node -v # 需要 v20 及以上 pnpm -v # v8 及以上 codex --version # 确认 Codex CLI 可用我试过用 npm 装依赖better-sqlite3编译经常卡住换 pnpm 之后顺很多。如果你只做前端部分better-sqlite3可以先不装。2. TaoToken 前置配置让 Codex 稳定产出可运行代码Codex 这类编码 Agent 最怕的就是生成到一半断流或者模型换来换去导致代码风格不统一。我的做法是先把模型接入层固定下来用 TaoToken 做统一入口Base URL、Key、Model ID 三件套配好后面所有生成请求都走这一条链路。TaoToken 在这里的角色是模型调用网关它本身不是二维码工具也不碰你的业务数据。你把它理解成一个模型插座Codex 负责理解需求和组织代码TaoToken 负责把请求稳定地送到模型那边。官网入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 注意 API 地址后面不加任何参数。配置分两步。第一步去控制台拿 Key地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 在 API Keys 页面创建一个新 Key复制出来。第二步把 Key 写进 Codex 的配置文件。如果你用的是 Codex CLI配置文件通常在~/.codex/config.toml或者项目根目录的.codex/config.toml具体看你的版本。下面这段是可直接复制的 TOML 片段# ~/.codex/config.toml model claude-sonnet-4-5 model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY wire_api chat然后在终端里导出环境变量别把 Key 硬编码进文件export TAOTOKEN_API_KEYsk-你的Key如果你用的是 Cline 或者 Claude Code 这类工具配置逻辑一样都是 Base URL Key Model ID 三件套。Cline 的 MCP 配置里baseUrl填https://taotoken.net/apiapiKey填你的 Keymodel填claude-sonnet-4-5。Claude Code 的话在settings.json里配ANTHROPIC_BASE_URL指向 TaoToken 的 API 地址ANTHROPIC_API_KEY填 Key。配完之后验证一下跑一个最小请求确认链路通curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-5, messages: [{role: user, content: 回复 ok}], max_tokens: 10 }返回里能看到choices数组就说明通了。这一步别跳过后面 Codex 生成代码如果报 401你至少能确定是 Key 的问题还是 Codex 配置的问题。模型选择上做这种多文件项目我建议用长上下文能力强的模型生成到后面不容易丢上下文。如果你要长期跑编码任务可以考虑 Coding Plan地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 按量或包月看你自己用量。3. 可复制配置项目结构与二维码核心引擎项目初始化我直接让 Codex 生成骨架提示词写清楚目录结构和技术栈它会把client和server分开建好。下面是关键配置片段你可以直接复制。先看前端package.json的依赖部分版本我锁了一下避免qrcode大版本升级导致 API 变化{ name: qrcode-generator-client, private: true, version: 1.0.0, type: module, scripts: { dev: vite, build: vite build, preview: vite preview }, dependencies: { vue: ^3.4.0, vue-router: ^4.3.0, element-plus: ^2.7.0, element-plus/icons-vue: ^2.3.0, qrcode: ^1.5.3, axios: ^1.7.0, jszip: ^3.10.1 }, devDependencies: { vitejs/plugin-vue: ^5.0.0, vite: ^5.2.0, sass: ^1.77.0 } }核心引擎我拆成三个类职责分开后面改样式不会牵一发动全身。第一个是QREngine负责基础生成和内容编码比如 vCard、WiFi 字符串拼接。第二个是QRStyleRenderer负责把二维码矩阵画到 Canvas 上支持圆点、圆角、菱形等码点形状。第三个是QRLogoComposer负责在中心叠加 Logo。先看QREngine的关键部分容错等级推荐逻辑是重点// client/src/utils/qr-engine.js import QRCode from qrcode export class QREngine { static DEFAULT_OPTIONS { width: 400, margin: 2, errorCorrectionLevel: H, color: { dark: #000000, light: #FFFFFF } } static async toDataURL(content, options {}) { const merged { ...this.DEFAULT_OPTIONS, ...options, color: { ...this.DEFAULT_OPTIONS.color, ...(options.color || {}) } } return await QRCode.toDataURL(content, merged) } static recommendErrorLevel(content, hasLogo false) { if (hasLogo) return H const len content.length if (len 50) return H if (len 100) return Q if (len 200) return M return L } }这里有个关键点只要你要叠 Logo容错等级必须用 H30%。因为 Logo 会遮住中心约 20% 到 25% 的码点容错等级低了直接扫不出来。我踩过的坑就是一开始用 M 级加 Logo手机怎么扫都识别不了换成 H 级立刻正常。再看QRStyleRenderer的渲染主循环它不用qrcode自带的toCanvas而是拿到矩阵数据后自己画这样才能控制每个码点的形状// client/src/utils/qr-style-renderer.js import QRCode from qrcode export class QRStyleRenderer { static DOT_SHAPES { SQUARE: square, CIRCLE: circle, ROUNDED: rounded, DIAMOND: diamond } static EYE_SHAPES { SQUARE: square, CIRCLE: circle, ROUNDED: rounded } static async render(canvas, content, styleConfig {}) { const { size 400, margin 2, errorCorrectionLevel H, dotShape square, eyeShape square, foreground #000000, background #FFFFFF } styleConfig const qrData QRCode.create(content, { errorCorrectionLevel }) const modules qrData.modules const moduleCount modules.size const moduleData modules.data const totalModules moduleCount margin * 2 const moduleSize size / totalModules canvas.width size canvas.height size const ctx canvas.getContext(2d) ctx.fillStyle background ctx.fillRect(0, 0, size, size) ctx.fillStyle foreground for (let row 0; row moduleCount; row) { for (let col 0; col moduleCount; col) { if (!moduleData[row * moduleCount col]) continue const x (col margin) * moduleSize const y (row margin) * moduleSize if (this._isFinderPattern(row, col, moduleCount)) { this._drawEyeModule(ctx, x, y, moduleSize, eyeShape) } else { this._drawDotModule(ctx, x, y, moduleSize, dotShape) } } } return canvas } static _isFinderPattern(row, col, moduleCount) { if (row 7 col 7) return true if (row 7 col moduleCount - 7) return true if (row moduleCount - 7 col 7) return true return false } }_isFinderPattern判断的是三个角上的定位图案这三个区域必须保持方形或统一形状不能跟普通码点一样处理否则扫描器找不到定位点。这是很多人自己画二维码时最容易忽略的地方。Logo 合成部分核心是控制 Logo 尺寸不超过二维码的 25%并且加白色衬底// client/src/utils/qr-logo-composer.js export class QRLogoComposer { static async embedLogo(qrCanvas, logoSrc, options {}) { const { sizeRatio 0.25, shape rounded, borderWidth 4, borderColor #FFFFFF, borderRadius 8 } options const ctx qrCanvas.getContext(2d) const canvasSize qrCanvas.width const logoSize canvasSize * Math.min(sizeRatio, 0.28) const logoX (canvasSize - logoSize) / 2 const logoY (canvasSize - logoSize) / 2 const logoImage await this._loadImage(logoSrc) ctx.save() ctx.fillStyle borderColor this._drawShape(ctx, shape, logoX - borderWidth, logoY - borderWidth, logoSize borderWidth * 2, logoSize borderWidth * 2, borderRadius borderWidth) ctx.restore() ctx.save() this._clipShape(ctx, shape, logoX, logoY, logoSize, logoSize, borderRadius) ctx.clip() ctx.drawImage(logoImage, logoX, logoY, logoSize, logoSize) ctx.restore() return qrCanvas } }sizeRatio我默认给 0.25上限卡在 0.28。超过这个比例即使 H 级容错也容易扫不出来。白色衬底的作用是让 Logo 和码点之间有隔离带视觉上更干净扫描识别率也更高。4. 验证请求批量生成与 Logo 定制实测配置写完得跑起来验证。先启动前端cd client pnpm install pnpm dev浏览器打开http://localhost:5173你应该能看到左侧配置面板、右侧预览区。先做单张验证在 URL 输入框填一个链接比如https://taotoken.net/api右侧应该实时出现二维码。用手机扫一下能正常跳转就说明基础链路通了。接着验证 Logo 定制。上传一张 PNG 格式的 Logo观察预览区二维码中心应该出现 Logo周围有白色衬底。这里有个验证动作很关键——用手机扫带 Logo 的码。如果扫不出来先检查容错等级是不是 H再检查 Logo 尺寸是不是超过 28%。我实测下来400px 的二维码配 100px 的 Logo25%识别率最稳。批量生成验证需要准备一个 CSV 文件每行一条内容https://example.com/product/1001 https://example.com/product/1002 https://example.com/product/1003 https://example.com/product/1004 https://example.com/product/1005在批量生成页面拖入这个文件页面会列出所有待生成内容。点击开始批量生成进度条会走完然后自动下载一个 ZIP 包。解压后应该看到qr_0001.png到qr_0005.png五个文件。逐个扫码验证每个都应该跳转到对应链接。批量生成的核心代码在BatchGenerate.vue里它用 JSZip 打包import JSZip from jszip import { QRStyleRenderer } from ../utils/qr-style-renderer async function startBatchGenerate() { const zip new JSZip() const total contentList.value.length for (let i 0; i total; i) { const item contentList.value[i] const canvas document.createElement(canvas) await QRStyleRenderer.render(canvas, item.content, { size: batchConfig.size, dotShape: batchConfig.dotShape, foreground: batchConfig.foreground, errorCorrectionLevel: H }) const blob await new Promise(resolve canvas.toBlob(resolve, image/png)) zip.file(${batchConfig.prefix}${String(i 1).padStart(4, 0)}.png, blob) progress.value Math.round(((i 1) / total) * 100) } const zipBlob await zip.generateAsync({ type: blob }) downloadBlob(zipBlob, qrcodes_batch_${Date.now()}.zip) }如果你要验证活码功能需要启动后端cd server pnpm install node app.js后端跑在http://localhost:3001。创建一个活码curl -X POST http://localhost:3001/api/live-codes \ -H Content-Type: application/json \ -d {name:测试活码,targetUrl:https://example.com}返回里会有qrUrl字段用这个地址生成二维码。之后你修改targetUrl二维码不用换扫码跳转的目标就变了。这就是活码的价值——物料已经印出去了链接还能改。5. 本篇常见错排查401、local proxy failed 与扫码失败做这个项目我遇到最多的报错集中在三类逐个说清楚。第一类401 Unauthorized。这个基本是 Key 的问题。先确认环境变量有没有生效echo $TAOTOKEN_API_KEY如果输出为空说明export没生效重新执行一遍。如果输出正常但 Codex 还是报 401检查config.toml里的env_key字段是不是写成了TAOTOKEN_API_KEY大小写要一致。还有一种情况是 Key 复制的时候带了空格用cat -A看一下有没有隐藏字符。第二类local proxy failed。这个报错通常出现在 Codex 或 Cline 启动时意思是本地代理连接不上。先确认你的 Base URL 是不是https://taotoken.net/api注意结尾不要多加/v1有些工具会自动拼路径多一层就 404。然后检查网络能不能通curl -I https://taotoken.net/api返回 200 或 401 都说明网络通401 只是没带 Key。如果 curl 都超时那就是本地网络环境问题检查一下 DNS 或者换个网络试试。第三类扫码失败报 reading choices 或者直接识别不出。这个分两种情况。如果是带 Logo 的码扫不出九成是容错等级不够或者 Logo 太大。把errorCorrectionLevel改成HsizeRatio降到 0.22 再试。如果是纯二维码也扫不出检查margin是不是设成了 0。二维码四周需要留白quiet zone标准要求至少 4 个模块宽度我默认给 2 其实偏小如果你打印出来扫不出把margin调到 4。还有一个隐蔽的坑Canvas 尺寸和 CSS 尺寸不一致。如果你用 CSS 把 canvas 缩小显示但实际生成的是 400px导出的时候没问题但预览时扫码可能因为屏幕像素密度问题识别困难。解决办法是预览区用image-rendering: pixelated或者直接按 1:1 显示。如果你用的是 Claude Code 接入方式报 OAuth 相关错误检查settings.json里的ANTHROPIC_BASE_URL是不是指向了 TaoToken 的 API 地址ANTHROPIC_API_KEY是不是填的 TaoToken Key 而不是其他平台的 Key。这两个字段填错一个就会认证失败。排查顺序我建议固定成先curl验证 Key 和网络再看工具配置文件最后看代码逻辑。这样能快速定位是接入层问题还是业务层问题。6. 从生成到落地把二维码生成器接进你的工作流项目跑通之后怎么用到实际业务里我分享几个真实场景的接法。电商场景把商品 SKU 和对应链接整理成 CSV用批量生成功能一次出几百个码导出 ZIP 后直接丢给打印店做标签。注意 CSV 里链接别带中文参数有些扫码器对 URL 编码支持不好建议提前用encodeURIComponent处理。活动场景用活码功能做一个统一入口物料上印同一个二维码后台随时改跳转目标。比如上午跳报名页下午跳直播页晚上跳回放页一张码搞定。活码的code字段建议用短 ID太长会导致二维码密度过高影响识别。品牌场景Logo 定制这块建议准备两套 Logo一套深色一套浅色根据二维码前景色自动切换。如果前景色是深色Logo 用浅色衬底反之亦然。这样视觉上更协调识别率也稳定。性能方面批量生成超过 500 张的时候主线程会卡。解决办法是把渲染逻辑放进 Web Worker或者分批用requestIdleCallback处理。我实测 1000 张 400px 的二维码纯主线程大概要 8 到 10 秒放进 Worker 后页面不卡总耗时差不多。最后说下扩展方向。这个项目的基础能力已经够用了如果你想继续加功能可以考虑扫码统计记录每次扫描的时间、设备、地区、二维码有效期控制过期自动失效、A/B 测试同一个码按比例跳不同链接。这些都需要后端配合但核心生成逻辑不用动加接口就行。如果你在接入过程中遇到模型调用问题可以去接入文档查配置细节https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。需要管理多个 Key 或者查看用量去 API Keys 页面https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。想先试试模型对话效果可以走这个入口https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 。代码写到最后最实用的技巧其实是先让 Codex 生成最小可运行版本验证扫码通过再逐步加样式和批量功能。我一开始贪心让 Codex 一次性生成所有功能结果样式渲染和 Logo 合成混在一起出了 bug 很难定位。拆成三步走之后每步都有明确的验证动作效率反而更高。