<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>AI赋能坊</title>
    <link>https://blog.operonai.com</link>
    <description>产品经理记录 Codex / Claude Code / Obsidian 的 AI 工作流实战，分享踩坑、提效方法和可复用配置。</description>
    <language>zh-CN</language>
    <atom:link href="https://blog.operonai.com/feed.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>深夜球场：我做了一个2026世界杯观赛指南</title>
      <link>https://blog.operonai.com/2026-06-12-ca0jey</link>
      <guid isPermaLink="true">https://blog.operonai.com/2026-06-12-ca0jey</guid>
      <description></description>
      <content:encoded><![CDATA[<p>6月12日凌晨3点，2026世界杯揭幕战开球。北京时间深夜到凌晨，时差党的老问题又来了：</p><ul class="tight" data-tight="true"><li><p>今天哪几场？几点开？</p></li><li><p>收藏的比赛怎么提前提醒？</p></li><li><p>想分享观赛计划，手动截图排版太累</p></li></ul><p>我花一个下午做了这个：<a target="_blank" rel="noopener noreferrer nofollow" href="https://wc26.operonai.com/"><strong>深夜球场·我的世界杯观赛指南</strong></a></p><h2>它解决什么</h2><p>不是又一个赛程表。是<strong>从筛选到提醒到分享的完整闭环</strong>：</p><h3>1. 今日赛场</h3><p></p><img src="https://blog.operonai.com/api/images/image/2026/06/fff26b724cbc6e5a-2026-world-cup-guide-1781248420803.webp" alt="" data-align="center" style="display: block; max-width: 100%; margin-left: auto; margin-right: auto;"><p>
黑底首屏，逐秒倒计时到下一场开球，当天所有比赛一眼扫完。</p><p>不用翻日历找今天的赛程，开页面就是「距加拿大 vs 波黑 还有 12:58:27」，直播中的比赛实时标注状态。</p><h3>2. 筛选收藏</h3><p></p><img src="https://blog.operonai.com/api/images/image/2026/06/f8dfc536893908a4-2026-world-cup-guide-1781248437410.webp" alt="" data-align="center" style="display: block; max-width: 100%; margin-left: auto; margin-right: auto;"><p>
12个小组 + 淘汰赛阶段、8座城市、48支球队、104场比赛，点星标收藏。</p><p>可以只看巴西的、只看决赛圈的、只看洛杉矶主办的，筛选逻辑叠加，收藏状态带 hash 参数可分享。</p><h3>3. 日历提醒</h3><p></p><img src="/api/images/image/2026/06/894fcf3ce5ab120c--381x826.webp" alt="|381x826" data-align="center" style="display: block; max-width: 100%; margin-left: auto; margin-right: auto;"><p>
收藏后一键加手机日历，开球前30分钟提醒。</p><p><strong>关键细节</strong>：iOS/Android 点按钮直接唤起日历导入页（不走下载目录），背后是 Cloudflare Pages Function 从静态全量日历里按收藏 ID 实时过滤，返回 <code>Content-Disposition: inline</code>。</p><h3>4. 观赛长图</h3><p></p><img src="https://blog.operonai.com/api/images/image/2026/06/5024b9c7abf9e24f-2026-world-cup-guide-1781248646542.webp" alt="" data-align="center" style="display: block; max-width: 100%; margin-left: auto; margin-right: auto;"><p>
生成带二维码的观赛计划长图，发朋友圈/群聊，朋友扫码一键导入你的收藏。</p><p>长图自动排版、插入站点二维码（QRCode.js 生成），移动端点下载走 Web Share API 直达相册，不进文件夹。</p><p><strong>核心断点</strong>：不是缺赛程信息，是「北京时间」+「选择性订阅」+「可分享」这三件事没有一站式工具。</p><h2>技术实现（vibe coding 验证点）</h2><p><strong>零构建纯静态 + 边缘函数</strong>，全程一个 Claude Code 对话完成。</p><h3>架构选择</h3><pre><code>worldcup-2026/
├── index.html          # 单文件，零依赖，本地 file:// 直接可用
├── data.js             # 唯一数据源：104场赛程+转播平台+站点URL
├── wc26-full.ics       # 全量静态日历（工具生成）
├── functions/cal.js    # Pages Function：按收藏ID过滤日历
└── tools/gen-ics.cjs   # Node脚本：从data.js生成ICS
</code></pre><p><strong>为什么不用 React/Vue</strong>：</p><ul class="tight" data-tight="true"><li><p>不需要状态管理（收藏用 localStorage + URL hash）</p></li><li><p>不需要路由（单页全展示）</p></li><li><p>不需要构建（改完 push 就部署）</p></li></ul><p><strong>为什么需要 Pages Function</strong>：</p><ul class="tight" data-tight="true"><li><p>静态 ICS 是全量 104 场，个性化日历需要运行时过滤</p></li><li><p>Function 无状态边缘计算，从 <code>/wc26-full.ics</code> 读取 → 按 UID 过滤 → 返回 <code>text/calendar inline</code></p></li><li><p>手机才会直接唤起日历导入，不走下载</p></li></ul><h3>数据驱动设计</h3><p><code>data.js</code> 是唯一事实来源：</p><pre><code class="language-js">const MATCHES_RAW = [
  ['06-12','03:00','A','墨西哥','MEX','南非','RSA','墨西哥城'],
  // 字段: [日期, 时间, stage, 队1, 码1, 队2, 码2, 城市]
]

const TV_PLATFORMS = [
  { name:'CCTV5 / 央视频', url:'https://tv.cctv.com/live/cctv5/' },
]

const SITE_URL = 'https://wc26.operonai.com/';
</code></pre><p><strong>日常维护循环</strong>（每天早上，淘汰赛对阵确定后）：</p><pre><code class="language-bash">vim worldcup-2026/data.js  # 把「待定/TBD」替换为真实队名
node worldcup-2026/tools/gen-ics.cjs  # 重新生成全量ICS
git push  # Cloudflare Pages 自动部署，日历订阅用户自动同步
</code></pre><p>改一处，全链路同步：HTML 读 data.js 渲染页面、ICS 生成器读 data.js 写日历、Function 从静态 ICS 过滤。</p><h3>品牌设计</h3><p>2026 美加墨世界杯官方品牌语言：</p><ul class="tight" data-tight="true"><li><p><strong>三国旗色</strong>：墨西哥绿 <code>#00754a</code>、加拿大红 <code>#d2122e</code>、美国深蓝 <code>#15356e</code>、奖杯金 <code>#d4a72c</code></p></li><li><p><strong>顶部色带</strong>：6px 固定高度 ribbon，三国色交替</p></li><li><p><strong>标题红色空心圆环</strong>：间隔号位置（不是句号），垂直对齐字高中线</p></li><li><p><strong>官方图形语言</strong>：方形 + 四分之一圆单元条（<code>.tiles</code> class）</p></li></ul><p>没用世界杯官方会徽和吉祥物素材，球迷自制工具，与 FIFA 无关。</p><h3>vibe coding 全程</h3><p>这正是我一直验证的 <strong>vibe coding</strong>：不写需求文档，边对话边成型，从想法到上线不离开一个会话。</p><p><strong>时间线</strong>：</p><ul class="tight" data-tight="true"><li><p>13:00 提出需求：做个北京时间赛程表</p></li><li><p>13:15 第一版上线：静态 HTML + 数据驱动</p></li><li><p>14:30 加入今日倒计时、筛选收藏、海报生成</p></li><li><p>15:45 边缘函数实现个性化日历</p></li><li><p>16:20 手机端优化：Web Share API 直达相册</p></li><li><p>16:40 最终部署、Git 仓库初始化、CLAUDE.md 文档</p></li></ul><p><strong>Claude Code 做了什么</strong>：</p><ul class="tight" data-tight="true"><li><p>联网核实转播平台（央视/咪咕/小红书持权公告）</p></li><li><p>从多个公开报道汇总 104 场赛程（Sky Sports / NBC Sports）</p></li><li><p>生成符合 2026 官方品牌的视觉设计</p></li><li><p>本地截图验证（Edge 无头浏览器）</p></li><li><p>部署到 Cloudflare Pages（wrangler CLI）</p></li><li><p>实时调试边缘函数（curl 验证 <code>/cal</code> 端点）</p></li></ul><p><strong>人做了什么</strong>：</p><ul class="tight" data-tight="true"><li><p>提需求、视觉反馈（「红点太小了」「链接不对」）</p></li><li><p>真机验证（手机日历导入、微信内打开）</p></li><li><p>最终确认部署</p></li></ul><p>80% 的实现、调试、部署都在对话里完成，我只需要在关键决策点确认方向。</p><h2>为什么值得收藏</h2><p>不只是「好用」，是<strong>可迁移的产品思路</strong>：</p><h3>1. 单文件 HTML 的复兴</h3><p>2026 年了，零构建纯静态仍然是最快的产品验证方式。</p><p>不是「回到过去」，是<strong>现代边缘计算 + 老派静态文件</strong>的组合拳：</p><ul class="tight" data-tight="true"><li><p>静态部分：CDN 全球分发，秒开</p></li><li><p>动态部分：Pages Function 边缘计算，个性化不引入后端</p></li><li><p>开发体验：改完 push 就部署，无构建等待</p></li></ul><p><strong>什么时候该用单文件 HTML</strong>：</p><ul class="tight" data-tight="true"><li><p>数据量 &lt;1000 条（客户端渲染无压力）</p></li><li><p>无复杂状态管理（localStorage + URL hash 够用）</p></li><li><p>快速验证产品假设（一天上线）</p></li></ul><h3>2. 数据驱动的日常维护</h3><p>赛程更新只改一个 <code>data.js</code>，HTML/ICS/Function 自动同步。</p><p>这是「单一事实来源」在小工具里的实践：</p><ul class="tight" data-tight="true"><li><p>HTML 直接 <code>&lt;script src="data.js"&gt;</code> 引入渲染</p></li><li><p>ICS 生成器 <code>require('./data.js')</code> 读取写文件</p></li><li><p>Function 从静态 ICS 过滤（data.js 变化后重新生成 ICS）</p></li></ul><p><strong>对比常见做法</strong>：</p><ul class="tight" data-tight="true"><li><p>❌ 赛程硬编码在 HTML → 改一场要全局搜索替换</p></li><li><p>❌ 赛程存数据库 → 每次改都要上后台、写 SQL</p></li><li><p>✅ 赛程在 data.js → vim 改完 push，三分钟上线</p></li></ul><h3>3. 闭环思维</h3><p>不止给信息，给完整动作链。</p><p><strong>产品设计的闭环</strong>：</p><ul class="tight" data-tight="true"><li><p>筛选 → 收藏（带 hash 可分享）</p></li><li><p>收藏 → 日历提醒（移动端一键导入）</p></li><li><p>收藏 → 观赛长图（带二维码可传播）</p></li></ul><p>每个环节都省一步：</p><ul class="tight" data-tight="true"><li><p>不用手动复制粘贴到日历</p></li><li><p>不用截图拼接海报</p></li><li><p>不用朋友问「你收藏哪几场」再转发</p></li></ul><p><strong>对比常见赛程表</strong>：</p><ul class="tight" data-tight="true"><li><p>只给信息 → 用户要自己订阅、自己记</p></li><li><p>只给提醒 → 没法分享给朋友</p></li><li><p>只给长图 → 没法导入日历</p></li></ul><p>闭环的本质是：<strong>让用户在你的产品里完成完整任务，不跳出去补齐缺失环节</strong>。</p><h2>可复用的技术链路</h2><p>如果你也在做小工具或内部效率产品，这条链路可以直接复用：</p><pre><code>需求 → Claude Code 对话 → 单文件 HTML + data.js → 
Cloudflare Pages 部署 → Pages Function 补个性化 → 
一个下午上线
</code></pre><p><strong>适合场景</strong>：</p><ul class="tight" data-tight="true"><li><p>团队内部工具（OKR 看板、值班表、会议室预订）</p></li><li><p>临时活动页（报名表、投票、日程安排）</p></li><li><p>数据可视化（日报/周报生成、指标 Dashboard）</p></li><li><p>个人效率工具（习惯打卡、账单记录、阅读清单）</p></li></ul><p><strong>核心优势</strong>：</p><ul class="tight" data-tight="true"><li><p>无构建：改完立刻部署</p></li><li><p>无后端：静态 + 边缘函数够用</p></li><li><p>可维护：数据集中在一个文件</p></li><li><p>可验证：vibe coding 全程可回溯</p></li></ul><hr><p><strong>站点</strong>：<a target="_blank" rel="noopener noreferrer nofollow" href="https://wc26.operonai.com/">wc26.operonai.com</a><br><strong>代码</strong>：<a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/jason-create-cmd/wc26-guide">github.com/jason-create-cmd/wc26-guide</a>（private，感兴趣可申请访问）</p><p>6月12日到7月20日，深夜不错过。</p><p>如果你在做类似工具，或想了解 vibe coding 的完整实践，欢迎在评论区交流：</p><ul class="tight" data-tight="true"><li><p>你现在最卡筛选、提醒还是分享？</p></li><li><p>你的工作流里有哪些环节可以「闭环」优化？</p></li><li><p>你试过零构建 + 边缘函数的组合吗？</p></li></ul>]]></content:encoded>
      <category>AI创作</category>
      <pubDate>Fri, 12 Jun 2026 07:18:45 GMT</pubDate>
    </item>
    <item>
      <title>基于 Codex 30 天工作记录识别可封装 workflows</title>
      <link>https://blog.operonai.com/2026-05-28-oC4XHo</link>
      <guid isPermaLink="true">https://blog.operonai.com/2026-05-28-oC4XHo</guid>
      <description>本文介绍如何让 Codex 回顾近 30 天工作记录、sessions、memories、Chronicle 与现有资产，识别高频且可复用的工作流，并按证据封装为 skill、subagent 或 automation，避免重复、过宽和 speculative 创建，适合团队定期盘点与效率优化。</description>
      <content:encoded><![CDATA[<h1>基于 Codex 30 天工作记录识别可封装 workflows</h1>
<p>用途：让 Codex 回看近期 sessions、memories、rollout summaries、Chronicle 和现有 skills/agents/automations，识别值得封装的重复工作流，并只创建高置信、缺失、范围窄的资产。</p>
<p>使用边界：</p>
<ul>
<li>适合周期性做 skills-hub / agent workflow 盘点。</li>
<li>要求先给 compact shortlist，再只创建高置信 missing items。</li>
<li>避免创建 speculative、overlapping、过宽的 skill / subagent / automation。</li>
</ul>
<h2>Prompt</h2>
<pre><code>"Look back over my recent work from the last 30 days, or all available history if shorter, and identify repeated manual workflows worth packaging.

Use available evidence in this order:
- Recent Codex sessions and task summaries.
- Codex Memories and rollout summaries to find patterns repeated across sessions.
- Chronicle, if enabled, to spot repeated work outside Codex. Use Chronicle for discovery only; confirm important details in the relevant source system when possible.
- Existing skills, custom agents, and automations, so you reuse or extend what already exists instead of duplicating it.

Look broadly for work that is repeated, time-consuming, error-prone, context-heavy, or benefits from a consistent process. Include workflows across coding, research, writing, planning, communication, operations, analysis, and personal administration.

Only act on a candidate when it:
- occurred at least twice, or is clearly likely to recur and costly to repeat;
- has stable inputs, a repeatable procedure, and a clear output or stopping condition;
- would materially improve speed, quality, consistency, or reliability;
- is not already adequately covered.

Choose the smallest appropriate form:
- Skill: a reusable workflow or playbook.
- Custom subagent: a bounded specialist role or investigation task suitable for delegation.
- Automation: a scheduled or recurring check, report, reminder, or monitor.
- Skip: work that is too one-off, ambiguous, sensitive, or poorly evidenced to package.

First produce a compact shortlist with:
- repeated workflow
- supporting evidence and dates
- frequency/confidence
- recommended form: skill, subagent, automation, extend existing, or skip
- why it is or is not worth creating

Then create only the high-confidence missing items. Keep them narrow, practical, source-aware, and easy to validate. Do not create speculative, overlapping, or overly broad assets.

Finish with:
- what you created or extended
- what you deliberately skipped
- what needs more evidence before packaging"
</code></pre>
<p>翻译版本</p>
<pre><code>回顾我过去 30 天内的近期工作，若历史记录更短则查看全部可用记录，识别值得打包的重复性手动工作流程。

按以下顺序使用可用证据：
- 最近的 Codex 会话记录及任务摘要。
- Codex 记忆与部署摘要，以发现跨会话重复的模式。
- 启用 Chronicle（编年史）功能，可识别 Codex 之外的重复性工作。仅将 Chronicle 用于发现目的；在可能的情况下，请通过相关源系统确认重要细节。
- 利用现有技能、自定义代理和自动化流程，以便复用或扩展已有成果，避免重复建设。

广泛寻找那些重复性高、耗时、易出错、依赖大量上下文或能从标准化流程中受益的工作。涵盖编码、研究、写作、规划、沟通、运营、分析及个人事务管理等各类工作流。

仅当候选事项满足以下条件时方可采取行动：
- 至少发生过两次，或明显可能再次发生且重复成本高昂；
- 具有稳定的输入、可重复的流程，以及明确的输出或终止条件；
- 能显著提升速度、质量、一致性或可靠性；
- 尚未被充分覆盖。

选择最合适的形式：
- 技能：可复用的工作流程或操作手册。
- 自定义子代理：适合委派的限定专家角色或调查任务。
- 自动化：定时或重复执行的检查、报告、提醒或监控。
- 跳过：过于一次性、模糊、敏感或证据不足而难以整合的工作。

首先制作一份简洁的候选清单，包含：
- 重复的工作流程
- 支持性证据和日期
- 频率/置信度
- 推荐形式：技能、子代理、自动化、扩展现有功能，或跳过
- 判断其是否值得创建的理由

然后仅创建高置信度的缺失项目。保持项目范围明确、实用、基于来源且易于验证。请勿创建推测性、重叠或过于宽泛的资产。

最后以：
- 你创建或扩展的内容
- 你刻意跳过的内容
- 在打包前需要更多证据的内容
</code></pre>
]]></content:encoded>
      <category>AI工具</category>
      <pubDate>Thu, 28 May 2026 14:05:45 GMT</pubDate>
    </item>
    <item>
      <title>Codex 对话日志复盘提示词</title>
      <link>https://blog.operonai.com/2026-05-28-_95sbx</link>
      <guid isPermaLink="true">https://blog.operonai.com/2026-05-28-_95sbx</guid>
      <description>整理 Codex 对话日志复盘提示词，说明如何检索历史 sessions、执行日志与配置，提炼用户偏好、经验规则并接入 .agent、AGENTS.md 等加载链路。</description>
      <content:encoded><![CDATA[<h1>Codex 对话日志复盘提示词</h1>
<p>用途：让 Codex 系统性阅读和检索历史对话记录、执行日志、memory 与项目配置，提炼可复用经验、个人偏好和后续默认规则。</p>
<p>适用场景：</p>
<ul>
<li>需要从大量 Codex sessions / logs 中做复盘。</li>
<li>需要把用户偏好、UI 风格、产品理念沉淀成结构化档案。</li>
<li>需要把历史执行经验转成后续 agent 可直接继承的规则。</li>
<li>需要把产物接入 <code>.agent</code>、<code>AGENTS.md</code>、<code>CLAUDE.md</code> 等加载链路。</li>
</ul>
<h2>Prompt</h2>
<pre><code class="language-text">/goal 阅读并检索我们所有的 Codex 对话记录与执行日志，进行系统性复盘，提炼出可复用的经验文档。

文档需要涵盖以下内容：
一、执行经验总结：记录哪些做法导致了问题、最终正确的执行方式是什么，以及从中得出的教训。

二、我的偏好与理念提炼：从对话中识别并归纳我的 UI 设计偏好、产品设计理念、交互原则等，形成结构化的个人风格档案。

三、可复用规则清单：将上述内容整理为 Codex 未来可直接遵循的行为准则。

完成文档后，将其保存为独立文件，并在 .agent 配置中以地址引用的方式加载该文档，使后续所有 Codex 会话默认继承这些经验，无需重复说明。
</code></pre>
<h2>使用建议</h2>
<ul>
<li>先读取当前项目的 <code>AGENTS.md</code> / <code>CLAUDE.md</code>，确认经验文档应该进入全局 rules 还是项目 <code>.agent</code>。</li>
<li>先盘点日志规模，再用脚本结构化提取用户纠偏、失败工具调用、主题分布和高频路径。</li>
<li>不要把 sessions 原文批量塞进上下文；优先使用索引、抽样、关键词和 rollout summaries。</li>
<li>经验文档要区分三层：全局通用规则、项目经验规则、一次性执行日志。</li>
<li>完成后用 <code>git diff --check</code>、UTF-8 读回和 scoped <code>git status</code> 验证。</li>
</ul>
]]></content:encoded>
      <category>AI工具</category>
      <pubDate>Thu, 28 May 2026 14:04:50 GMT</pubDate>
    </item>
    <item>
      <title>Antigravity 2.0 账号地区不可用</title>
      <link>https://blog.operonai.com/2026-05-28-nj-khq</link>
      <guid isPermaLink="true">https://blog.operonai.com/2026-05-28-nj-khq</guid>
      <description># Antigravity 2.0 账号地区不可用 记录 Antigravity 2.0 登录时出现账号不可用提示的排查与处理。适用于错误： ```text Sorry, this account is ineligible to use Antigravity Authentication failed. ``` !</description>
      <content:encoded><![CDATA[<h1>Antigravity 2.0 账号地区不可用</h1><p>记录 Antigravity 2.0 登录时出现账号不可用提示的排查与处理。适用于错误：</p><pre><code class="language-text">Sorry, this account is ineligible to use Antigravity
Authentication failed.
</code></pre><p></p><img src="https://blog.operonai.com/api/images/image/2026/05/4be72c6cbf879e26-2026-05-28-1779939960399.webp" alt="2026-05-28-1779939960399.webp" data-align="center" style="display: block; max-width: 100%; margin-left: auto; margin-right: auto;"><hr><h2>1. 结论</h2><p>优先排查两类问题：</p><ol class="tight" data-tight="true"><li><p>网络环境不符合要求：VPN 未开启，或未开启 TUN 模式。</p></li><li><p>Google 账号关联地区不符合要求：如果账号地区仍是 China，需要申请改到 United States。</p></li></ol><p>这类问题不一定是 Antigravity 客户端故障。网络出口和 Google 账号地区都会影响 eligibility 判断。</p><h2>2. 快速检查</h2><h3>2.1 检查 VPN 和 TUN</h3><p>先确认：</p><ul class="tight" data-tight="true"><li><p>VPN 已开启。</p></li><li><p>TUN 模式已开启。</p></li><li><p>登录 Antigravity 的浏览器 / IDE 流量确实走代理出口。</p></li></ul><p>如果 VPN 没开，或只开了系统代理但没有 TUN，Antigravity 登录页仍可能拿到不符合要求的地区判断。</p><h3>2.2 检查 Google 账号地区</h3><p>打开 Google Terms 页面确认当前账号关联地区：</p><p><a target="_blank" rel="noopener noreferrer nofollow" href="https://policies.google.com/terms">Google Terms</a></p><p>如果页面或账号关联信息显示地区为 China，就按下面流程申请修改。</p><h2>3. 修改 Google 账号地区</h2><p>进入 Google country association form：</p><p><a target="_blank" rel="noopener noreferrer nofollow" href="https://accounts.google.com/v3/signin/identifier?continue=https%3A%2F%2Fpolicies.google.com%2Fcountry-association-form&amp;dsh=S-837258255%3A1779257931587585&amp;followup=https%3A%2F%2Fpolicies.google.com%2Fcountry-association-form&amp;passive=1209600&amp;flowName=GlifWebSignIn&amp;flowEntry=ServiceLogin&amp;ifkv=AWa2PavLCkvhaCwpqKozV3q-RsX2j0jJ7Md1KppeY7-CpvQMqv282njA69t4CP5QgJJCwZM6t-iz8g">Google Country Association Form</a></p><p>修改原因可以写英文：</p><pre><code class="language-text">I need to use Google AI Ultra for my work, please help me to change to US.
</code></pre><p>提交后一般需要等待 Google 邮件回复。经验上大约 1 小时左右会收到结果邮件。</p><h2>4. 处理顺序</h2><p>建议按这个顺序排查：</p><ol class="tight" data-tight="true"><li><p>开启 VPN。</p></li><li><p>开启 TUN 模式。</p></li><li><p>重新打开 Antigravity 登录流程。</p></li><li><p>如果仍提示账号不可用，检查 Google 账号关联地区。</p></li><li><p>如果关联地区是 China，提交 Google country association form，申请改为 United States。</p></li><li><p>等待 Google 邮件确认后，再重新登录 Antigravity。</p></li></ol><h2>5. 复盘判断</h2><p>这个坑的关键点是：Antigravity 2.0 的登录资格判断不是只看当前客户端，也可能同时受网络出口和 Google 账号地区影响。</p><p>所以不要只在 Antigravity 里反复切账号。先保证网络出口合规，再处理 Google account country association。</p>]]></content:encoded>
      <category>AI工具</category>
      <pubDate>Thu, 28 May 2026 03:57:02 GMT</pubDate>
    </item>
    <item>
      <title>录视频没字幕？我用 Vibe Coding 一个周末造了一个免费字幕生成工具</title>
      <link>https://blog.operonai.com/2026-05-17-frlnfk</link>
      <guid isPermaLink="true">https://blog.operonai.com/2026-05-17-frlnfk</guid>
      <description>作者记录录制技术视频生成字幕的真实痛点，对比飞书妙记、剪映、通义听悟等方案后，用 Vibe Coding 和 Cloudflare 周末自建免费字幕生成工具。</description>
      <content:encoded><![CDATA[<h1>录视频没字幕？我用 Vibe Coding 一个周末造了一个免费字幕生成工具</h1><h2>起因：一个真实的痛点</h2><p>周末录制技术视频的时候，我遇到了一个看似简单但实际上相当恼人的问题——<strong>给视频配字幕</strong>。</p><p>这件事说起来不难，但做起来处处是坑。</p><p>录完一段屏幕操作演示，我心想，字幕嘛，2026 年了，AI 都能写代码了，加个字幕还不是分分钟的事？</p><p>事实证明，我太天真了。</p><h2>踩坑之旅：那些"差一点就好用"的方案</h2><h3>第一站：飞书妙记</h3><p>我最先想到的是飞书。飞书妙记支持语音转文字，而且它的中文识别效果确实不错——<strong>短句拆分自然、口语过滤到位、几乎可以直接拿来当字幕用</strong>。</p><p></p><img src="https://blog.operonai.com/api/images/image/2026/05/c6bd525641e4b3ca-2026-05-17-vibe-coding-caption-tool-1778993237806.webp" alt="飞书妙记字幕效果截图" data-align="center" style="display: block; max-width: 100%; margin-left: auto; margin-right: auto;"><p>
但问题来了：</p><p><strong>免费版只有 15 分钟。</strong></p><p></p><img src="https://blog.operonai.com/api/images/image/2026/05/5790aaf3122e16fa-2026-05-17-vibe-coding-caption-tool-image.webp" alt="飞书妙记语音转文字时长限制截图" data-align="center" style="display: block; max-width: 100%; margin-left: auto; margin-right: auto;"><p>我如果经常要做字幕生成功能的话。15 分钟根本不够用。</p><p>想要更多？升级 AI 会员，连续包月 ¥69/月，年付 ¥699。</p><p></p><img src="https://blog.operonai.com/api/images/image/2026/05/44e2a23bc11be014-2026-05-17-vibe-coding-caption-tool-1778993100862.webp" alt="飞书 AI 会员价格页截图" data-align="center" style="display: block; max-width: 100%; margin-left: auto; margin-right: auto;"><p>
一个字幕功能，要我每年花 700 块？我就偶尔录个视频，用不了那么多。</p><h3>第二站：剪映</h3><p>网上很多人推荐剪映——把视频导入，自动识别字幕，再导出 SRT 文件替换。</p><p>这个方案有几个问题：</p><ol class="tight" data-tight="true"><li><p><strong>也有时长限制</strong>——免费版对视频长度有门槛</p></li><li><p><strong>操作链路长</strong>——导入、识别、导出、再合并，折腾半天</p></li><li><p><strong>我的场景是技术视频</strong>，里面大量英文术语、代码片段、产品名，剪映的识别准确率并不理想</p></li></ol><h3>第三站：通义听悟</h3><p>阿里的通义听悟倒是支持语音转文字，但我实际试了之后发现：</p><ol class="tight" data-tight="true"><li><p><strong>同样有时间限制</strong></p></li><li><p><strong>百分比显示不对</strong>——我说的"百分之十"，它给我转成了一种非常规的格式，不是日常习惯的 <code>10%</code></p></li></ol><p></p><img src="https://blog.operonai.com/api/images/image/2026/05/1f78f2c187621783-2026-05-17-vibe-coding-caption-tool-1778993124449.webp" alt="通义听悟百分比异常截图" data-align="center" style="display: block; max-width: 100%; margin-left: auto; margin-right: auto;"><p>说白了，这些工具有一个共同特点：</p><blockquote><p><strong>免费版让你尝到甜头，但想真正用起来，得掏钱。</strong></p></blockquote><h2>转折：为什么不自己造一个？</h2><p>踩完一圈坑之后，我停下来想了想：</p><p><strong>AI 现在这么强大，语音识别的 API 到处都是，Cloudflare 提供了几乎免费的基础设施——正好周末有时间，我为什么不用 Vibe Coding 的方式，自己造一个？</strong></p><p>这不就是 Vibe Coding 最理想的场景吗？</p><ul class="tight" data-tight="true"><li><p>✅ 痛点真实——我自己每周都要用</p></li><li><p>✅ 需求明确——上传视频 → 生成字幕 → 下载 SRT/ASS</p></li><li><p>✅ 技术路径清晰——语音识别 API + 文件存储 + 简单前端</p></li><li><p>✅ 部署成本接近零——Cloudflare 免费套餐基本够用</p></li></ul><p>于是我决定：<strong>不是去找一个完美的工具，而是自己造一个刚好够用的工具。</strong></p><p>这就是 Vibe Coding 的精神——<strong>与其等待，不如动手；与其凑合，不如精准解决自己的问题。</strong></p><h2>技术选型：为什么选 Cloudflare 全家桶？</h2><p>在动手之前，我调研了一下常规的字幕生成实现方式：</p><h3>方案一：本地跑 Whisper</h3><p>OpenAI 的 Whisper 是目前最主流的开源语音识别模型。可以在本地跑，精度也不错。</p><p>但问题是：</p><ul class="tight" data-tight="true"><li><p>需要 GPU，我的 Mac 跑起来巨慢</p></li><li><p>大文件（2-3 小时视频）内存开销惊人</p></li><li><p>部署成服务需要一台 GPU 服务器，成本不低</p></li></ul><h3>方案二：云端 API</h3><p>各大云厂商都有语音识别 API——Google、Azure、AWS、阿里、腾讯，这些当然也是可以的。</p><h3>我的选择：Soniox + Cloudflare</h3><p>最终我选了一个相对小众但非常适合这个场景的组合：</p><table class="tiptap-table" style="min-width: 75px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>组件</p></th><th colspan="1" rowspan="1"><p>作用</p></th><th colspan="1" rowspan="1"><p>选择理由</p></th></tr><tr><td colspan="1" rowspan="1"><p><strong>Soniox</strong></p></td><td colspan="1" rowspan="1"><p>异步语音转写</p></td><td colspan="1" rowspan="1"><p>专业 ASR，中英混合识别好，<strong>注册送 $200 额度</strong>，足够用很久</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Cloudflare Pages</strong></p></td><td colspan="1" rowspan="1"><p>前端托管</p></td><td colspan="1" rowspan="1"><p>免费，全球 CDN，自动 HTTPS</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Cloudflare Workers</strong></p></td><td colspan="1" rowspan="1"><p>API 后端</p></td><td colspan="1" rowspan="1"><p>免费 10 万次/天请求，Serverless</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Cloudflare R2</strong></p></td><td colspan="1" rowspan="1"><p>文件存储</p></td><td colspan="1" rowspan="1"><p>免费 10GB，无出口流量费</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Cloudflare D1</strong></p></td><td colspan="1" rowspan="1"><p>任务数据库</p></td><td colspan="1" rowspan="1"><p>免费 SQLite，足够存 job 元数据</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>DeepSeek</strong></p></td><td colspan="1" rowspan="1"><p>字幕文本润色（主力）</p></td><td colspan="1" rowspan="1"><p>手上已有 API key，便宜好用</p></td></tr><tr><td colspan="1" rowspan="1"><p><strong>Google Gemini</strong></p></td><td colspan="1" rowspan="1"><p>字幕文本润色（备用）</p></td><td colspan="1" rowspan="1"><p>免费额度充足，作为兜底方案</p></td></tr></tbody></table><p><strong>整套方案的运行成本：几乎为零。</strong></p><p>Cloudflare 的免费套餐完全覆盖我的用量。Soniox 注册送的 $200 额度，按我的使用频率够用很长时间。DeepSeek 的 API 本身我就有在用，单价极低。Gemini 作为备用，免费额度也够。</p><p>这就是 Vibe Coding 的另一个核心理念：<strong>不是花最多的钱买最贵的方案，而是用最巧的方式组合出最适合自己的方案。</strong></p><p></p><img src="https://blog.operonai.com/api/images/image/2026/05/0de2bd51240cd6c8-2026-05-17-vibe-coding-caption-tool-cost-comparison.png" alt="字幕工具年成本对比" data-align="center" style="display: block; max-width: 100%; margin-left: auto; margin-right: auto;"><h2>架构设计：从上传到字幕的完整链路</h2><p>整个系统的数据流是这样的：</p><p></p><img src="https://blog.operonai.com/api/images/image/2026/05/9bd2d7d579e3d9df-2026-05-17-vibe-coding-caption-tool-architecture.png" alt="字幕生成系统架构" data-align="center" style="display: block; max-width: 100%; margin-left: auto; margin-right: auto;"><p>具体步骤：</p><ol class="tight" data-tight="true"><li><p><strong>打开工具页面</strong>，用密码登录（私人工具，不开放注册）</p></li><li><p><strong>上传视频文件</strong>，前端自动读取视频宽高</p></li><li><p><strong>浏览器直传 R2</strong>——视频不经过 Worker，直接通过签名 URL 上传到对象存储，避免 Worker 体积限制</p></li><li><p><strong>Worker 创建任务</strong>，在 D1 记录 job 元数据，调用 Soniox 异步转写</p></li><li><p><strong>Soniox 后台处理</strong>，完成后通过 Webhook 回调 Worker</p></li><li><p><strong>Worker 生成初步字幕</strong>，把 Soniox 返回的 token 按时间分段</p></li><li><p><strong>LLM 润色字幕文本</strong>——这是最关键的一步</p></li><li><p><strong>输出三种格式</strong>：SRT（通用字幕）、ASS（带样式字幕）、Transcript JSON（完整文稿）</p></li></ol><p>一个 8分钟 的视频，从上传到字幕生成完成，大约 <strong>1-2 分钟</strong>。大部分时间花在 Soniox 的异步转写上，Worker 本身的处理几乎是瞬时的。实际效果非常 nice，Soniox 的中英文混合识别能力确实出色。</p><video src="/api/images/video/2026/05/5gNf3I2zUp-demo.mp4" controls="" playsinline="" webkit-playsinline="true" x5-playsinline="true" x5-video-player-type="h5" x-webkit-airplay="true" preload="metadata" style="max-width: 100%; max-height: 600px;">您的浏览器不支持视频播放</video><h2>关键创新：LLM 字幕润色</h2><p>这个项目最有意思的部分，不是语音识别本身，而是<strong>用 LLM 做字幕后处理</strong>。</p><h3>为什么需要 LLM？</h3><p>原始的 ASR（语音识别）输出有几个典型问题：</p><ul class="tight" data-tight="true"><li><p><strong>口语词太多</strong>——"呃"、"嗯"、"那个"、"就是说"满天飞</p></li><li><p><strong>重复表达</strong>——"这个这个"、"然后然后"</p></li><li><p><strong>英文断裂</strong>——<code>D em o</code> 被拆成三个词，<code>A P I</code> 被拆成三个字母</p></li><li><p><strong>数字格式混乱</strong>——"百分之十"到底是写成"10%"还是"百分之十"还是"百分之百10"？</p></li></ul><p>这些问题，传统的文本处理规则很难覆盖。但 LLM 天然擅长——<strong>它理解上下文，知道什么该删、什么该留、什么该合并</strong>。</p><h3>设计原则</h3><p>我给 LLM 定了几条硬规则：</p><ol class="tight" data-tight="true"><li><p><strong>LLM 只处理文本，不碰时间码</strong>——时间轴的准确性由 Worker 保证</p></li><li><p><strong>LLM 输出必须引用原始片段 ID</strong>——不能凭空创造句子</p></li><li><p><strong>可以合并相邻短句，不能删除信息</strong></p></li><li><p><strong>失败时自动回退原始字幕</strong>——宁可粗糙，不能丢内容</p></li></ol><p>默认的润色风格对标<strong>飞书妙记</strong>：</p><blockquote><p>短句优先，8-18 个字一条。删口语词，修断裂英文，数字按场景处理。每条字幕开头结尾不留标点。</p></blockquote><pre><code class="language-text"># 润色前（原始 ASR 输出）
"呃然后这地方可以设置这个预算超过百分之百10的一个处理方法"

# 润色后（LLM 处理）
"这里可以设置预算超过110%的处理方法"
</code></pre><p>一行字幕，从 28 个字压缩到 16 个字。<strong>信息量不变，阅读体验天差地别。</strong></p><h3>双 LLM 策略</h3><p>为了稳定性，我采用了双 LLM 策略：</p><ul class="tight" data-tight="true"><li><p><strong>主力</strong>：DeepSeek——我手上本来就有 API key，中文处理能力强，价格极低</p></li><li><p><strong>备用</strong>：Google Gemini——免费额度充足，DeepSeek 挂了自动切换</p></li><li><p><strong>兜底</strong>：如果两个都挂了，直接用原始字幕，任务照样完成</p></li></ul><p><strong>不会因为 LLM 的问题导致整个任务失败——这是工程思维，不是 demo 思维。</strong></p><p></p><img src="https://blog.operonai.com/api/images/image/2026/05/29f01747cb23c4a1-2026-05-17-vibe-coding-caption-tool-llm-polish.png" alt="LLM 字幕润色前后对比" data-align="center" style="display: block; max-width: 100%; margin-left: auto; margin-right: auto;"><h2>ASS 字幕样式：细节决定体验</h2><p>很多字幕工具生成的 ASS 文件有一个常见 bug：<strong>不写入视频分辨率</strong>。</p><p>这会导致 <code>ffmpeg</code> 在烧录字幕时，把分辨率回退到默认的 <code>384×288</code>，字幕直接被拉大到占满半个屏幕。</p><p>我的方案在前端上传时就读取视频的真实宽高，写入 ASS 的 <code>PlayResX</code> / <code>PlayResY</code>：</p><table class="tiptap-table" style="min-width: 50px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><th colspan="1" rowspan="1"><p>样式项</p></th><th colspan="1" rowspan="1"><p>设置</p></th></tr><tr><td colspan="1" rowspan="1"><p>分辨率</p></td><td colspan="1" rowspan="1"><p>真实视频宽高</p></td></tr><tr><td colspan="1" rowspan="1"><p>字体</p></td><td colspan="1" rowspan="1"><p>PingFang SC</p></td></tr><tr><td colspan="1" rowspan="1"><p>字号（1080p）</p></td><td colspan="1" rowspan="1"><p>44</p></td></tr><tr><td colspan="1" rowspan="1"><p>位置</p></td><td colspan="1" rowspan="1"><p>底部居中</p></td></tr><tr><td colspan="1" rowspan="1"><p>底部间距</p></td><td colspan="1" rowspan="1"><p>视频高度 × 4.5%</p></td></tr><tr><td colspan="1" rowspan="1"><p>背景</p></td><td colspan="1" rowspan="1"><p>半透明黑色底框</p></td></tr><tr><td colspan="1" rowspan="1"><p>布局</p></td><td colspan="1" rowspan="1"><p>单行优先</p></td></tr></tbody></table><p>烧录出来的效果，和专业剪辑软件输出的字幕几乎无差别。</p><p></p><img src="https://blog.operonai.com/api/images/image/2026/05/1e61a2ab9b8bcf5b-2026-05-17-vibe-coding-caption-tool-1778996348598.webp" alt="2026-05-17-vibe-coding-caption-tool-1778996348598.webp" data-align="center" style="display: block; max-width: 100%; margin-left: auto; margin-right: auto;"><h2>开发过程：一个周末，从 0 到上线</h2><p>整个开发过程，从有想法到生产上线，就用了<strong>一个周末</strong>。周末正好有录视频的需求，痛点摆在面前，索性直接动手。</p><h3>周六上午：骨架搭建 + 首次部署</h3><ul class="tight" data-tight="true"><li><p>初始化项目，搭建 Vite + React 前端</p></li><li><p>编写 Worker API：登录、上传签名、任务管理、Soniox 集成</p></li><li><p>配置 Cloudflare 全家桶：Pages、Worker、R2、D1</p></li><li><p>踩了一堆 Cloudflare 部署的坑（Pages 和 Worker 配置冲突、R2 凭证混淆、DNS 生效等待……）</p></li><li><p>首次部署成功，首页返回 200</p></li></ul><h3>周六下午 → 周日：LLM 润色 + 生产验证</h3><ul class="tight" data-tight="true"><li><p>集成 LLM 字幕后处理模块</p></li><li><p>接入我已有的 DeepSeek API 作为主力，Google Gemini 作为备用</p></li><li><p>迭代字幕提示词，对标飞书妙记效果</p></li><li><p>修复 ASS 样式问题（PlayResX/PlayResY、底部间距、字体）</p></li><li><p>前端增加自定义提示词功能</p></li><li><p><strong>生产验证</strong>：7分钟 的视频，几分钟 完成，字幕质量达标</p></li></ul><h3>周日下午：开源发布</h3><ul class="tight" data-tight="true"><li><p>拆分私有仓库和公开仓库</p></li><li><p>脱敏处理：替换真实域名、Account ID、数据库 ID</p></li><li><p>编写 export 脚本，保证后续同步安全</p></li><li><p>配置 GitHub Actions CI、CodeQL、Dependabot</p></li><li><p><code>detect-secrets</code> 扫描 0 发现</p></li><li><p><strong>正式开源：</strong><a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/jason-create-cmd/video-caption-generator"><strong>video-caption-generator</strong></a></p></li></ul><p><strong>一个周末。从痛点到产品，从代码到开源。</strong></p><p>这就是 Vibe Coding 的速度。不是因为代码量少，而是因为——<strong>你知道你要什么，AI 帮你写，Cloudflare 帮你跑，你只需要做决策。</strong></p><p></p><img src="https://blog.operonai.com/api/images/image/2026/05/11ee30253a928503-2026-05-17-vibe-coding-caption-tool-weekend-timeline.png" alt="一个周末从痛点到开源的开发时间线" data-align="center" style="display: block; max-width: 100%; margin-left: auto; margin-right: auto;"><p>Cloudflare 免费套餐：Pages 无限站点、Workers 10 万次/天、R2 免费 10GB、D1 免费 5GB。Soniox 注册送 $200 额度。</p><p>对于个人使用，这些额度远远够用。</p><h2>可扩展性：不只是字幕</h2><p>这个项目的架构天然支持扩展：</p><ul class="tight" data-tight="true"><li><p><strong>多语言</strong>——Soniox 支持 40+ 语言，换个 model 参数就行</p></li><li><p><strong>实时字幕</strong>——Soniox 也有 realtime API，可以扩展为直播字幕</p></li><li><p><strong>批量处理</strong>——前端可以改为队列模式，一次上传多个视频</p></li><li><p><strong>团队使用</strong>——加个用户系统就能变成团队工具</p></li><li><p><strong>自定义 LLM</strong>——支持 <code>LLM_BASE_URL</code> 配置，可以接任何 OpenAI 兼容的模型</p></li></ul><p>这也是我选择开源的原因——<strong>这个工具解决的不只是我一个人的问题。</strong></p><p>每一个录视频的人，都会遇到字幕的烦恼。不是所有人都需要 Adobe Premiere 那样的全功能方案，有时候，一个<strong>够用、免费、可控</strong>的工具，就是最好的工具。</p><h2>写在最后</h2><p>回过头看这个项目，最让我感触的不是技术本身，而是一种做事的方式：</p><p><strong>当你遇到一个真实的痛点，不要在现有工具里反复横跳，不要等待某个完美方案出现。用你手边的技术，用 AI 的能力，用 Vibe Coding 的方式——直接造一个。</strong></p><p>一个周末，0 成本运行，解决了一个每周都会遇到的问题。</p><p>这不是什么了不起的技术壮举。这只是 <strong>2026 年，一个普通开发者应该具备的能力</strong>——看到问题，分析问题，用 AI 解决问题。</p><p><strong>工具的最高境界，是为你量身定做的。</strong></p><hr><blockquote><p>🔗 <strong>开源地址</strong>：<a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/jason-create-cmd/video-caption-generator">github.com/jason-create-cmd/video-caption-generator</a></p><p>🎬 <strong>操作演示</strong>：完整的操作视频和效果展示见评论区</p><p>如果这个项目对你有帮助，欢迎 Star ⭐ 和 Fork 🍴</p></blockquote><hr>]]></content:encoded>
      <category>AI工具</category>
      <pubDate>Sun, 17 May 2026 08:08:28 GMT</pubDate>
    </item>
    <item>
      <title>Codex 周限到底用了多少，其实可以自己查。</title>
      <link>https://blog.operonai.com/2026-05-12-pxd1ac</link>
      <guid isPermaLink="true">https://blog.operonai.com/2026-05-12-pxd1ac</guid>
      <description>一次性使用代码 (() =&gt; { &quot;use strict&quot;; const addStyle = (css) =&gt; { const style = document.createElement(&quot;style&quot;); style.textContent = css; document.head.appendChild(sty</description>
      <content:encoded><![CDATA[<h2>一次性使用代码</h2><pre><code>(() =&gt; {
  "use strict";

  const addStyle = (css) =&gt; {
    const style = document.createElement("style");
    style.textContent = css;
    document.head.appendChild(style);
  };

  const CONFIG = {
    USD_PER_CREDIT: 40 / 1000,
  };

  const fmtNum = (n) =&gt; {
    if (n &gt;= 1e8) return (n / 1e8).toFixed(2) + "亿";
    if (n &gt;= 1e4) return (n / 1e4).toFixed(1) + "万";
    return Number(n || 0).toLocaleString();
  };

  const n = (v) =&gt; (Number.isFinite(Number(v)) ? Number(v) : 0);

  const tokenTotal = (obj = {}) =&gt;
    n(obj.text_total_tokens) ||
    n(obj.cached_text_input_tokens) +
      n(obj.uncached_text_input_tokens) +
      n(obj.text_output_tokens);

  function getPageBearerToken() {
    const bootstrapData =
      document.getElementById("client-bootstrap")?.textContent || "";

    return bootstrapData.match(
      /[\w-]{30,}\.[\w-]{30,}\.[\w-]{30,}/,
    )?.[0];
  }

  document.getElementById("codex-compass-btn")?.remove();
  document.getElementById("codex-compass-root")?.remove();

  addStyle(`
    #codex-compass-root {
      position: fixed; top: 5%; left: 50%; transform: translateX(-50%);
      width: 720px; max-height: 90vh; overflow: auto; background: #fff;
      border-radius: 12px; box-shadow: 0 10px 50px rgba(0,0,0,0.3);
      z-index: 2147483647; padding: 24px; display: none;
      flex-direction: column; border: 1px solid #e5e5e5; color: #333;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
    }
    .compass-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
    .compass-title { font-size: 18px; font-weight: 600; }
    .compass-close { cursor: pointer; font-size: 28px; color: #999; line-height: 1; }
    .compass-note { font-size: 12px; color: #777; line-height: 1.6; margin: -4px 0 16px; }
    .compass-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; }
    .compass-card { background: #f9f9f9; padding: 12px; border-radius: 8px; border: 1px solid #eee; }
    .compass-card.highlight { background: #eefaf5; border-color: #d1f2e1; }
    .card-label { font-size: 12px; color: #666; margin-bottom: 4px; }
    .card-value { font-size: 15px; font-weight: 700; color: #10a37f; white-space: nowrap; }
    .table-container { max-height: 220px; overflow-y: auto; border: 1px solid #eee; border-radius: 6px; margin-bottom: 15px; }
    .compass-table { width: 100%; border-collapse: collapse; font-size: 12px; }
    .compass-table thead { position: sticky; top: 0; background: #f5f5f5; z-index: 1; }
    .compass-table th { text-align: left; padding: 8px; border-bottom: 1px solid #eee; color: #666; }
    .compass-table td { padding: 8px; border-bottom: 1px solid #f0f0f0; }
    .compass-footer-row { background: #fafafa; font-weight: 700; position: sticky; bottom: 0; border-top: 2px solid #eee; }
    #codex-compass-btn {
      position: fixed; bottom: 20px; right: 20px; z-index: 2147483646;
      padding: 10px 20px; background: #10a37f; color: white;
      border: none; border-radius: 8px; cursor: pointer; font-weight: 600;
    }
  `);

  async function apiGet(path, token) {
    if (!token) {
      throw new Error("未找到页面 Bearer token，无法访问 Codex usage 接口。");
    }

    const res = await fetch(path, {
      headers: {
        Authorization: `Bearer ${token}`,
      },
      credentials: "omit",
    });

    const text = await res.text();

    if (!res.ok) {
      throw new Error(`${path} HTTP ${res.status}: ${text.slice(0, 300)}`);
    }

    try {
      return JSON.parse(text);
    } catch {
      throw new Error(`${path} returned non-JSON: ${text.slice(0, 300)}`);
    }
  }

  function getStats(list) {
    const credits = list.reduce((sum, d) =&gt; sum + n(d.totals?.credits), 0);
    const turns = list.reduce((sum, d) =&gt; sum + n(d.totals?.turns), 0);
    const tokens = list.reduce((sum, d) =&gt; sum + tokenTotal(d.totals), 0);
    return { credits, turns, tokens, usd: credits * CONFIG.USD_PER_CREDIT };
  }

  function renderTable(list, stats) {
    return `
      &lt;div class="table-container"&gt;
        &lt;table class="compass-table"&gt;
          &lt;thead&gt;
            &lt;tr&gt;&lt;th&gt;日期&lt;/th&gt;&lt;th&gt;Credits&lt;/th&gt;&lt;th&gt;Tokens&lt;/th&gt;&lt;th&gt;金额&lt;/th&gt;&lt;th&gt;轮数&lt;/th&gt;&lt;/tr&gt;
          &lt;/thead&gt;
          &lt;tbody&gt;
            ${[...list].reverse().map((row) =&gt; `
              &lt;tr&gt;
                &lt;td&gt;${row.date}&lt;/td&gt;
                &lt;td style="font-family:monospace"&gt;${n(row.totals?.credits).toFixed(3)}&lt;/td&gt;
                &lt;td style="font-family:monospace"&gt;${fmtNum(tokenTotal(row.totals))}&lt;/td&gt;
                &lt;td&gt;$ ${(n(row.totals?.credits) * CONFIG.USD_PER_CREDIT).toFixed(2)}&lt;/td&gt;
                &lt;td&gt;${n(row.totals?.turns)}&lt;/td&gt;
              &lt;/tr&gt;
            `).join("")}
          &lt;/tbody&gt;
          &lt;tfoot&gt;
            &lt;tr class="compass-footer-row"&gt;
              &lt;td&gt;合计&lt;/td&gt;
              &lt;td&gt;${stats.credits.toFixed(3)}&lt;/td&gt;
              &lt;td style="font-family:monospace"&gt;${fmtNum(stats.tokens)}&lt;/td&gt;
              &lt;td style="color:#10a37f"&gt;$ ${stats.usd.toFixed(2)}&lt;/td&gt;
              &lt;td&gt;${stats.turns}&lt;/td&gt;
            &lt;/tr&gt;
          &lt;/tfoot&gt;
        &lt;/table&gt;
      &lt;/div&gt;
    `;
  }

  function showPanel(data) {
    const root = document.getElementById("codex-compass-root");
    const { secondary, dailyList, cycleStartDate } = data;

    const currentCycleList = [];
    const historyList = [];

    dailyList.forEach((item) =&gt; {
      if (new Date(item.date) &gt;= new Date(cycleStartDate)) {
        currentCycleList.push(item);
      } else {
        historyList.push(item);
      }
    });

    const currentStats = getStats(currentCycleList);
    const historyStats = getStats(historyList);

    const ratio = n(secondary?.used_percent) / 100;
    const estCredits = ratio &gt; 0 ? currentStats.credits / ratio : 0;

    let historyRangeTitle = "历史记录（本周期外）";
    if (historyList.length &gt; 0) {
      const sortedHistory = [...historyList].sort(
        (a, b) =&gt; new Date(a.date) - new Date(b.date),
      );
      historyRangeTitle =
        `历史记录（本周期外 ${sortedHistory[0].date} 至 ${sortedHistory.at(-1).date}）`;
    }

    root.innerHTML = `
      &lt;div class="compass-header"&gt;
        &lt;div class="compass-title"&gt;Codex 配额分析&lt;/div&gt;
        &lt;div class="compass-close" id="compass-close-btn"&gt;&amp;times;&lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="compass-note"&gt;
        说明：analytics 可能延迟几小时；周期切换通常按美西时间计算。脚本会读取当前 chatgpt.com 页面内已有的 Bearer token，仅用于请求 chatgpt.com 同域 usage / analytics 接口；不读取 cookie 内容，不打印、不保存、不上传 token。
      &lt;/div&gt;

      &lt;div class="compass-grid"&gt;
        &lt;div class="compass-card"&gt;
          &lt;div class="card-label"&gt;已用比例&lt;/div&gt;
          &lt;div class="card-value"&gt;${n(secondary?.used_percent)}%&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="compass-card"&gt;
          &lt;div class="card-label"&gt;本周期已用&lt;/div&gt;
          &lt;div class="card-value"&gt;${currentStats.credits.toFixed(1)} Credits&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="compass-card highlight"&gt;
          &lt;div class="card-label"&gt;推算总额&lt;/div&gt;
          &lt;div class="card-value"&gt;${estCredits.toFixed(1)} Credits&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="compass-card"&gt;
          &lt;div class="card-label"&gt;周期价值估算&lt;/div&gt;
          &lt;div class="card-value"&gt;$ ${(estCredits * CONFIG.USD_PER_CREDIT).toFixed(2)}&lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div style="font-weight:600; margin-bottom:8px; font-size:13px;"&gt;本周期明细（始于 ${cycleStartDate}）&lt;/div&gt;
      ${renderTable(currentCycleList, currentStats)}

      ${
        historyList.length &gt; 0
          ? `&lt;div style="font-weight:600; margin-bottom:8px; font-size:13px; color:#666;"&gt;${historyRangeTitle}&lt;/div&gt;
             ${renderTable(historyList, historyStats)}`
          : ""
      }
    `;

    root.style.display = "flex";
    document.getElementById("compass-close-btn").onclick = () =&gt; {
      root.style.display = "none";
    };
  }

  async function run() {
    const btn = document.getElementById("codex-compass-btn");

    btn.innerText = "分析中...";

    try {
      const token = getPageBearerToken();

      const usage = await apiGet("/backend-api/wham/usage", token);

      const secondary =
        usage?.rate_limit?.secondary_window ||
        usage?.rate_limit?.primary_window;

      const endDate = new Date(Date.now() + 86400000)
        .toISOString()
        .split("T")[0];

      const startDate = new Date(Date.now() - 30 * 86400000)
        .toISOString()
        .split("T")[0];

      const cycleStartDate = secondary
        ? new Date((secondary.reset_at - secondary.limit_window_seconds) * 1000)
            .toISOString()
            .split("T")[0]
        : startDate;

      const dailyData = await apiGet(
        `/backend-api/wham/analytics/daily-workspace-usage-counts?start_date=${startDate}&amp;end_date=${endDate}&amp;group_by=day`,
        token,
      );

      showPanel({
        secondary,
        dailyList: dailyData.data || [],
        cycleStartDate,
      });
    } catch (e) {
      console.error("[Codex Compass]", e);
      alert("错误: " + e.message);
    } finally {
      btn.innerText = "运行用量分析";
    }
  }

  const btn = document.createElement("button");
  btn.id = "codex-compass-btn";
  btn.innerText = "运行用量分析";
  btn.onclick = run;
  document.body.appendChild(btn);

  const root = document.createElement("div");
  root.id = "codex-compass-root";
  document.body.appendChild(root);
})();</code></pre><h2>油猴脚本代码</h2><pre><code>// ==UserScript==
// @name         Codex Quota Compass V2.0
// @namespace    https://chatgpt.com/
// @version      2.0
// @description  展示 Codex 配额、Credits、Tokens、周期估算与历史用量
// @match        https://chatgpt.com/*
// @run-at       document-idle
// @grant        GM_addStyle
// ==/UserScript==

(() =&gt; {
  "use strict";

  const CONFIG = {
    USD_PER_CREDIT: 40 / 1000,
  };

  const TARGET_PATH = "/codex/cloud/settings/analytics";

  const fmtNum = (n) =&gt; {
    if (n &gt;= 1e8) return (n / 1e8).toFixed(2) + "亿";
    if (n &gt;= 1e4) return (n / 1e4).toFixed(1) + "万";
    return Number(n || 0).toLocaleString();
  };

  const n = (v) =&gt; (Number.isFinite(Number(v)) ? Number(v) : 0);

  const tokenTotal = (obj = {}) =&gt;
    n(obj.text_total_tokens) ||
    n(obj.cached_text_input_tokens) +
      n(obj.uncached_text_input_tokens) +
      n(obj.text_output_tokens);

  function getPageBearerToken() {
    const bootstrapData =
      document.getElementById("client-bootstrap")?.textContent || "";

    return bootstrapData.match(
      /[\w-]{30,}\.[\w-]{30,}\.[\w-]{30,}/,
    )?.[0];
  }

  GM_addStyle(`
    #codex-compass-root {
      position: fixed; top: 5%; left: 50%; transform: translateX(-50%);
      width: 720px; max-height: 90vh; overflow: auto; background: #fff;
      border-radius: 12px; box-shadow: 0 10px 50px rgba(0,0,0,0.3);
      z-index: 2147483647; padding: 24px; display: none;
      flex-direction: column; border: 1px solid #e5e5e5; color: #333;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
    }
    .compass-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; }
    .compass-title { font-size: 18px; font-weight: 600; }
    .compass-close { cursor: pointer; font-size: 28px; color: #999; line-height: 1; }
    .compass-note { font-size: 12px; color: #777; line-height: 1.6; margin: -4px 0 16px; }
    .compass-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; }
    .compass-card { background: #f9f9f9; padding: 12px; border-radius: 8px; border: 1px solid #eee; }
    .compass-card.highlight { background: #eefaf5; border-color: #d1f2e1; }
    .card-label { font-size: 12px; color: #666; margin-bottom: 4px; }
    .card-value { font-size: 15px; font-weight: 700; color: #10a37f; white-space: nowrap; }
    .table-container { max-height: 220px; overflow-y: auto; border: 1px solid #eee; border-radius: 6px; margin-bottom: 15px; }
    .compass-table { width: 100%; border-collapse: collapse; font-size: 12px; }
    .compass-table thead { position: sticky; top: 0; background: #f5f5f5; z-index: 1; }
    .compass-table th { text-align: left; padding: 8px; border-bottom: 1px solid #eee; color: #666; }
    .compass-table td { padding: 8px; border-bottom: 1px solid #f0f0f0; }
    .compass-footer-row { background: #fafafa; font-weight: 700; position: sticky; bottom: 0; border-top: 2px solid #eee; }
    #codex-compass-btn {
      position: fixed; bottom: 20px; right: 20px; z-index: 2147483646;
      padding: 10px 20px; background: #10a37f; color: white;
      border: none; border-radius: 8px; cursor: pointer; font-weight: 600;
    }
  `);

  async function apiGet(path, token) {
    if (!token) {
      throw new Error("未找到页面 Bearer token，无法访问 Codex usage 接口。");
    }

    const res = await fetch(path, {
      headers: {
        Authorization: `Bearer ${token}`,
      },
      credentials: "omit",
    });

    const text = await res.text();

    if (!res.ok) {
      throw new Error(`${path} HTTP ${res.status}: ${text.slice(0, 300)}`);
    }

    try {
      return JSON.parse(text);
    } catch {
      throw new Error(`${path} returned non-JSON: ${text.slice(0, 300)}`);
    }
  }

  function getStats(list) {
    const credits = list.reduce((sum, d) =&gt; sum + n(d.totals?.credits), 0);
    const turns = list.reduce((sum, d) =&gt; sum + n(d.totals?.turns), 0);
    const tokens = list.reduce((sum, d) =&gt; sum + tokenTotal(d.totals), 0);
    return { credits, turns, tokens, usd: credits * CONFIG.USD_PER_CREDIT };
  }

  function renderTable(list, stats) {
    return `
      &lt;div class="table-container"&gt;
        &lt;table class="compass-table"&gt;
          &lt;thead&gt;
            &lt;tr&gt;&lt;th&gt;日期&lt;/th&gt;&lt;th&gt;Credits&lt;/th&gt;&lt;th&gt;Tokens&lt;/th&gt;&lt;th&gt;金额&lt;/th&gt;&lt;th&gt;轮数&lt;/th&gt;&lt;/tr&gt;
          &lt;/thead&gt;
          &lt;tbody&gt;
            ${[...list].reverse().map((row) =&gt; `
              &lt;tr&gt;
                &lt;td&gt;${row.date}&lt;/td&gt;
                &lt;td style="font-family:monospace"&gt;${n(row.totals?.credits).toFixed(3)}&lt;/td&gt;
                &lt;td style="font-family:monospace"&gt;${fmtNum(tokenTotal(row.totals))}&lt;/td&gt;
                &lt;td&gt;$ ${(n(row.totals?.credits) * CONFIG.USD_PER_CREDIT).toFixed(2)}&lt;/td&gt;
                &lt;td&gt;${n(row.totals?.turns)}&lt;/td&gt;
              &lt;/tr&gt;
            `).join("")}
          &lt;/tbody&gt;
          &lt;tfoot&gt;
            &lt;tr class="compass-footer-row"&gt;
              &lt;td&gt;合计&lt;/td&gt;
              &lt;td&gt;${stats.credits.toFixed(3)}&lt;/td&gt;
              &lt;td style="font-family:monospace"&gt;${fmtNum(stats.tokens)}&lt;/td&gt;
              &lt;td style="color:#10a37f"&gt;$ ${stats.usd.toFixed(2)}&lt;/td&gt;
              &lt;td&gt;${stats.turns}&lt;/td&gt;
            &lt;/tr&gt;
          &lt;/tfoot&gt;
        &lt;/table&gt;
      &lt;/div&gt;
    `;
  }

  function showPanel(data) {
    const root = document.getElementById("codex-compass-root");
    const { secondary, dailyList, cycleStartDate } = data;

    const currentCycleList = [];
    const historyList = [];

    dailyList.forEach((item) =&gt; {
      if (new Date(item.date) &gt;= new Date(cycleStartDate)) {
        currentCycleList.push(item);
      } else {
        historyList.push(item);
      }
    });

    const currentStats = getStats(currentCycleList);
    const historyStats = getStats(historyList);

    const ratio = n(secondary?.used_percent) / 100;
    const estCredits = ratio &gt; 0 ? currentStats.credits / ratio : 0;

    let historyRangeTitle = "历史记录（本周期外）";
    if (historyList.length &gt; 0) {
      const sortedHistory = [...historyList].sort(
        (a, b) =&gt; new Date(a.date) - new Date(b.date),
      );
      historyRangeTitle =
        `历史记录（本周期外 ${sortedHistory[0].date} 至 ${sortedHistory.at(-1).date}）`;
    }

    root.innerHTML = `
      &lt;div class="compass-header"&gt;
        &lt;div class="compass-title"&gt;Codex 配额分析&lt;/div&gt;
        &lt;div class="compass-close" id="compass-close-btn"&gt;&amp;times;&lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="compass-note"&gt;
        说明：analytics 可能延迟几小时；周期切换通常按美西时间计算。脚本会读取当前 chatgpt.com 页面内已有的 Bearer token，仅用于请求 chatgpt.com 同域 usage / analytics 接口；不读取 cookie 内容，不打印、不保存、不上传 token。
      &lt;/div&gt;

      &lt;div class="compass-grid"&gt;
        &lt;div class="compass-card"&gt;
          &lt;div class="card-label"&gt;已用比例&lt;/div&gt;
          &lt;div class="card-value"&gt;${n(secondary?.used_percent)}%&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="compass-card"&gt;
          &lt;div class="card-label"&gt;本周期已用&lt;/div&gt;
          &lt;div class="card-value"&gt;${currentStats.credits.toFixed(1)} Credits&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="compass-card highlight"&gt;
          &lt;div class="card-label"&gt;推算总额&lt;/div&gt;
          &lt;div class="card-value"&gt;${estCredits.toFixed(1)} Credits&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="compass-card"&gt;
          &lt;div class="card-label"&gt;周期价值估算&lt;/div&gt;
          &lt;div class="card-value"&gt;$ ${(estCredits * CONFIG.USD_PER_CREDIT).toFixed(2)}&lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div style="font-weight:600; margin-bottom:8px; font-size:13px;"&gt;本周期明细（始于 ${cycleStartDate}）&lt;/div&gt;
      ${renderTable(currentCycleList, currentStats)}

      ${
        historyList.length &gt; 0
          ? `&lt;div style="font-weight:600; margin-bottom:8px; font-size:13px; color:#666;"&gt;${historyRangeTitle}&lt;/div&gt;
             ${renderTable(historyList, historyStats)}`
          : ""
      }
    `;

    root.style.display = "flex";
    document.getElementById("compass-close-btn").onclick = () =&gt; {
      root.style.display = "none";
    };
  }

  async function run() {
    const btn = document.getElementById("codex-compass-btn");

    btn.innerText = "分析中...";

    try {
      const token = getPageBearerToken();

      const usage = await apiGet("/backend-api/wham/usage", token);

      const secondary =
        usage?.rate_limit?.secondary_window ||
        usage?.rate_limit?.primary_window;

      const endDate = new Date(Date.now() + 86400000)
        .toISOString()
        .split("T")[0];

      const startDate = new Date(Date.now() - 30 * 86400000)
        .toISOString()
        .split("T")[0];

      const cycleStartDate = secondary
        ? new Date((secondary.reset_at - secondary.limit_window_seconds) * 1000)
            .toISOString()
            .split("T")[0]
        : startDate;

      const dailyData = await apiGet(
        `/backend-api/wham/analytics/daily-workspace-usage-counts?start_date=${startDate}&amp;end_date=${endDate}&amp;group_by=day`,
        token,
      );

      showPanel({
        secondary,
        dailyList: dailyData.data || [],
        cycleStartDate,
      });
    } catch (e) {
      console.error("[Codex Compass]", e);
      alert("错误: " + e.message);
    } finally {
      btn.innerText = "运行用量分析";
    }
  }

  function removePanel() {
    document.getElementById("codex-compass-btn")?.remove();
    document.getElementById("codex-compass-root")?.remove();
  }

  function mount() {
    removePanel();

    const btn = document.createElement("button");
    btn.id = "codex-compass-btn";
    btn.innerText = "运行用量分析";
    btn.onclick = run;
    document.body.appendChild(btn);

    const root = document.createElement("div");
    root.id = "codex-compass-root";
    document.body.appendChild(root);
  }

  function boot() {
    if (location.pathname === TARGET_PATH) {
      if (!document.getElementById("codex-compass-btn")) {
        mount();
      }
    } else {
      removePanel();
    }
  }

  boot();
  setInterval(boot, 1000);
})();</code></pre><p></p><p></p>]]></content:encoded>
      <category>AI工具</category>
      <pubDate>Tue, 12 May 2026 04:22:06 GMT</pubDate>
    </item>
    <item>
      <title>Codex Desktop For Intel Mac 总是默认 Fast 模式：根因和修复方法</title>
      <link>https://blog.operonai.com/2026-04-27-kp3acy</link>
      <guid isPermaLink="true">https://blog.operonai.com/2026-04-27-kp3acy</guid>
      <description>本文分析 Codex Desktop for Intel Mac 重启后总是默认 Fast 模式的原因，梳理 config、全局状态与 watcher 崩溃三层冲突，并给出关闭 Fast、修复持久化状态和 Python 环境的完整处理方法。</description>
      <content:encoded><![CDATA[<img src="/api/images/image/2026/04/d3698dd71273df64-image.webp" alt="image.png" data-align="center" style="display: block; max-width: 100%; margin-left: auto; margin-right: auto;"><p>Codex Desktop For Intel Mac 在</p><p><code>~/.codex/config.toml</code> 写了 <code>fast_default_opt_out = true</code>，重启后 UI 还是 Fast。</p><p>这不是配置写错了，是三层状态同时在打架。</p><hr><h2>为什么要关</h2><p>Fast 模式大约带来 1.5x 速度提升，但 token 消耗放大到约 2.5x。对重量级模型来说这个差值不小。如果只是偶发使用无所谓，但把 Codex 当日常工程工具的话，账单会一直比预期高。</p><h2>更麻烦的是 Intel Mac 上没有稳定的关闭入口，UI 的模式切换背后牵涉三个不同状态层。</h2><img src="https://blog.operonai.com/api/images/image/2026/04/f0c3b950ad0a7119-01-infographic-fast-cost-speed.png" alt="Fast 模式成本与速度对比" data-align="center" style="display: block; max-width: 100%; margin-left: auto; margin-right: auto;"><h2>问题现象</h2><p><code>~/.codex/config.toml</code> 已经写了：</p><pre><code class="language-toml">[notice]
fast_default_opt_out = true</code></pre><p>但 <code>~/.codex/.codex-global-state.json</code> 里还是：</p><pre><code class="language-json">{ "default-service-tier": "fast" }</code></pre><p>只检查 <code>config.toml</code> 会误判。Desktop UI 拿的不是这个文件。</p><hr><h2>根因：三层状态叠加</h2><p><strong>Layer 1 — 账号计划的 managed default</strong></p><p>Codex Desktop <code>26.422.30944</code> 对部分 ChatGPT business / enterprise 账号有 managed Fast default 逻辑（PR <a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/openai/codex/pull/19053">#19053</a>）。官方反馈 issue <a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/openai/codex/issues/19230">#19230</a> 已关闭，标 <code>not planned</code>。</p><p><strong>Layer 2 — Desktop UI persisted state</strong></p><p>Desktop 在 <code>~/.codex/.codex-global-state.json</code> 里保存 UI 级别的状态，独立于 <code>config.toml</code>。只要 <code>default-service-tier</code> 是 <code>"fast"</code>，config 文件写什么都没用。</p><p><strong>Layer 3 — 退出时写回 + watcher crash</strong></p><p>用 launchd 托管的退出后 patch watcher 一直 crash：</p><pre><code>ModuleNotFoundError: No module named 'tomllib'</code></pre><p>launchd 调用的是系统 <code>/usr/bin/python3</code>，没有 <code>tomllib</code>。watcher 启动即挂，没有完成任何 patch。</p><p>实际链路：</p><pre><code>config.toml 已 opt-out
→ App 运行时内存态仍是 fast
→ 退出时写回 .codex-global-state.json
→ watcher crash，没完成补丁
→ 下次打开继续 Fast</code></pre><hr><h2>修复：三件事同时处理</h2><h3>1. 固定 durable config</h3><p><code>~/.codex/config.toml</code>：</p><pre><code class="language-toml">[notice]
fast_default_opt_out = true</code></pre><p><code>[features]<br>fast_mode = false</code></p><p><code>fast_default_opt_out</code> 只是 opt-out 信号，<code>fast_mode = false</code> 才是功能开关，两个都要写。确认顶层没有 <code>service_tier = "fast"</code>。</p><h3>2. 修正 Desktop persisted state</h3><p>修改 <code>~/.codex/.codex-global-state.json</code> 和 <code>.bak</code>，把 <code>electron-persisted-atom-state</code> 里改成：</p><pre><code class="language-json">{
"default-service-tier": null,
"has-user-changed-service-tier": true
}</code></pre><p><code>null</code> 回到 Standard；<code>has-user-changed-service-tier: true</code> 告诉 Desktop 用户已明确选过，不再套用 enterprise_default。</p><h3>3. 退出后再 patch 一次</h3><p>Codex App 退出时有最后一次 state flush，会把改好的状态写回 fast。需要用 <code>launchd submit</code> 托管一个独立 watcher，在 App 完整退出 2 秒后再做 patch。</p><p>关键：<strong>不依赖 </strong><code>tomllib</code>，只用系统 Python 稳定有的模块：<code>json</code>、<code>re</code>、<code>pathlib</code>、<code>shutil</code>。</p><img src="https://blog.operonai.com/api/images/image/2026/04/e1f98949ec656414-02-flowchart-fast-state-loop.png" alt="Codex Fast 状态写回链路" data-align="center" style="display: block; max-width: 100%; margin-left: auto; margin-right: auto;"><hr><h2>验证</h2><pre><code class="language-bash">/usr/bin/python3 - &lt;&lt;'PY'
from pathlib import Path
import json, re, datetime</code></pre><p><code>home = Path.home()<br>config = home / '.codex/config.toml'<br>text = config.read_text()</code></p><p><code>print('config_has_service_tier', bool(re.search(r'^\sservice_tier\s=', text, re.M)))<br>print('config_fast_default_opt_out_true', bool(re.search(r'^\sfast_default_opt_out\s=\strue\s$', text, re.M)))<br>print('config_fast_mode_false', bool(re.search(r'^\sfast_mode\s=\sfalse\s$', text, re.M)))</code></p><p><code>for name in ['.codex-global-state.json', '.codex-global-state.json.bak']:<br>p = home / '.codex' / name<br>atom = json.loads(p.read_text()).get('electron-persisted-atom-state', {})<br>print(name, {<br>'default-service-tier': atom.get('default-service-tier'),<br>'has-user-changed-service-tier': atom.get('has-user-changed-service-tier'),<br>})<br>PY<br></code></p><p>期望输出：</p><pre><code>config_has_service_tier False
config_fast_default_opt_out_true True
config_fast_mode_false True
.codex-global-state.json {'default-service-tier': None, 'has-user-changed-service-tier': True}</code></pre><p>真正的验收是：完整退出 → 等 watcher 执行 → 重开 → UI 不再默认 Fast。不能只看磁盘文件。</p><hr><h2>复发排查顺序</h2><ol class="tight" data-tight="true"><li><p>检查 <code>config.toml</code>：<code>fast_default_opt_out = true</code> + <code>fast_mode = false</code> + 无顶层 <code>service_tier = "fast"</code></p></li><li><p>检查 <code>.codex-global-state.json</code>：<code>default-service-tier = null</code> + <code>has-user-changed-service-tier = true</code></p></li><li><p>看 watcher log：<code>tail -100 ~/.codex/tmp/disable-fast-after-codex-exit-launchd.log</code></p></li><li><p>watcher 没成功：检查是否被 launchctl remove、是否依赖了系统 Python 没有的模块、是否 patch 太早被 Electron flush 覆盖</p></li><li><p>以上都对但仍然 Fast：查 <code>~/Library/Application Support/Codex/Local Storage/leveldb/</code></p></li></ol><hr><h2>结论</h2><p><code>config.toml</code> 是 durable config，但不是 Desktop UI 的唯一状态源。<code>.codex-global-state.json</code> 是 Desktop 自己持久化的 UI 状态，退出时会被写回。退出后修复必须独立于 App 生命周期，launchd 环境很干净，不要假设它有当前 shell 的 Python 或 PATH。</p><p>Fast 模式不是改一个配置项就能关掉的。durable config、Desktop persisted state、退出时写回，三件事缺一个，它就会悄悄回来。</p><p></p><p></p><p></p><p></p>]]></content:encoded>
      <category>AI工具</category>
      <pubDate>Mon, 27 Apr 2026 05:48:03 GMT</pubDate>
    </item>
  </channel>
</rss>