1. 从"写代码没人陪"说起:桌宠这个需求是怎么冒出来的
用 Codex 写代码这件事,时间一长会有个很微妙的心理变化:你面对的是一块终端、一个对话框、一堆 diff,整个过程安静得有点过分。尤其是深夜跑一个重构任务,Codex 在那边一行行吐代码,你在这边盯着屏幕等结果,中间那几十秒到几分钟的空白期,人是有点悬着的——不知道它跑到哪了、有没有卡住、下一步该不该催。
我一开始的解法很土:开个终端窗口跑top,或者干脆盯着 Codex CLI 的输出滚动。但这样有个问题,视线被绑死在终端上,反而没法去干别的事。后来我想,既然桌面上已经有各种桌宠(Linux 桌宠、Windows 桌宠都玩过一圈),为什么不干脆给 Codex 养一只?让它替我看状态,状态好的时候它自己玩,Codex 忙的时候它给个反馈,出错了它提醒我一声。
这就是"给 Codex 养了只龙娘"这个项目的起点。说白了,它是一个Codex 桌宠:一个常驻桌面的小角色,通过读取 Codex 的运行状态(CLI 输出、日志、进程状态),把"Codex 现在在干嘛"翻译成一个可视化的、有情绪的小生命。它不替代 Codex,也不改 Codex 的任何逻辑,纯粹是个"搭子"——你写代码,它在旁边陪着,顺便帮你盯状态。
适合谁看?三类人。第一类是把 Codex CLI 当日常主力、想找个轻量方式监控任务状态的开发者;第二类是想入门桌宠开发(Python 桌宠是最好上手的路径)但不知道从哪下手的人;第三类是对 Codex 配置、日志、进程这些底层细节好奇,想借这个项目把 Codex 的运行机制摸一遍的人。下面我会把整个思路、选型、实现、踩坑全部摊开讲,代码能抄就抄,配置能复制就复制。
2. 先搞清楚 Codex 到底吐出了什么:状态源盘点
在动手写桌宠之前,必须先回答一个根本问题:桌宠的状态从哪来?如果连 Codex 在干什么都读不到,桌宠就只是个会动的贴图,没有任何"搭子"价值。所以第一步是把 Codex 所有可观测的状态源盘一遍。
2.1 Codex CLI 的三种可观测输出
Codex CLI 运行时,能拿到的信息大致分三层。
第一层是标准输出流。Codex CLI 在跑任务时会往 stdout 打东西,包括当前正在执行的步骤、工具调用、diff 预览等。这层信息最实时,但格式不稳定,不同版本、不同任务类型输出差异很大,直接解析容易翻车。
第二层是会话日志文件。Codex 会把会话记录落到本地目录,通常是~/.codex/下面(Windows 是%USERPROFILE%\.codex\)。这里面有结构化的会话数据,包括消息、工具调用记录、token 消耗等。这层信息最完整,适合做"历史状态"和"任务进度"的读取。
第三层是进程状态。Codex 作为一个进程跑着,它的 CPU、内存、是否存活、有没有卡死,这些用系统调用就能拿到。这层信息最粗糙,但最可靠——不管 Codex 内部怎么变,进程在不在是骗不了人的。
我的桌宠设计里,三层都用上了:进程状态决定"龙娘醒着还是睡着",日志文件决定"龙娘在做什么表情",stdout 流决定"龙娘要不要冒个气泡说句话"。
2.2 为什么优先读日志而不是解析 stdout
这里有个选型上的关键判断,值得单独说。很多人第一反应是去解析 Codex CLI 的 stdout,因为最直接。但我实测下来,优先读日志文件比解析 stdout 稳得多,原因有三个。
一是 stdout 是给人看的,不是给程序看的。它的格式会随版本变,加个颜色、改个前缀,你的解析就废了。日志文件相对稳定,字段是结构化的。
二是 stdout 是流式的,你得一直挂着读,进程一断就丢。日志文件是落盘的,桌宠启动时可以先读一遍历史,知道"上次 Codex 干到哪了",体验连贯。
三是 stdout 解析容易和 Codex 自己的输出缓冲打架。Codex 有些输出是带缓冲的,你读到的时机和实际执行时机对不上,桌宠就会显示"正在执行"但其实早跑完了。
提示:日志文件路径和格式会随 Codex 版本变化,写代码时不要把路径写死,做成可配置项,后面 Codex 升级了改配置就行,不用改代码。
2.3 状态到表情的映射表
把状态读出来之后,得有个映射规则,把冷冰冰的数据翻译成龙娘的表情和动作。我用的映射表大概是这样:
| Codex 状态 | 判定依据 | 龙娘表现 |
|---|---|---|
| 空闲 | 进程存活但无活跃会话 | 待机动画,偶尔眨眼 |
| 思考中 | 日志有请求但无响应 | 头顶冒省略号,轻微晃动 |
| 执行中 | 有工具调用记录 | 快速敲键盘动作 |
| 等待确认 | 日志出现确认类事件 | 举牌子,气泡提示 |
| 出错 | 日志有 error 级别记录 | 表情变沮丧,气泡显示错误摘要 |
| 进程退出 | 进程不存在 | 睡觉动画 |
这张表是整个桌宠的"灵魂"。没有它,桌宠就只是个装饰;有了它,龙娘才真的在"陪"你写代码。映射规则不用一次做全,先做"空闲/执行中/出错"三个状态就能跑起来,后面慢慢加。
3. 桌宠技术选型:为什么是 Python + 透明窗口
确定状态源之后,下一个问题是用什么技术画这只龙娘。桌宠开发看着简单,其实坑不少,选型错了后面全是麻烦。
3.1 主流桌宠方案的横向对比
我把常见的几种方案都试了一遍,结论如下。
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| Electron | 生态好,动画强 | 内存占用大,启动慢 | 复杂交互桌宠 |
| Python + PyQt/PySide | 透明窗口成熟,跨平台 | 打包体积偏大 | 中小型桌宠,首选 |
| Python + tkinter | 零依赖,轻 | 透明和动画支持差 | 极简桌宠 |
| Godot/Unity | 动画表现力最强 | 学习成本高,重 | 游戏化桌宠 |
| 纯 Web + 浏览器窗口 | 开发快 | 无法真正贴桌面 | 网页挂件 |
我最终选的是Python + PyQt(PySide6)。理由很实在:Codex 本身是命令行工具,桌宠要和它做进程级交互,Python 在这块最顺手;PyQt 的Qt.FramelessWindowHint+Qt.WA_TranslucentBackground组合能做出真正的无边框透明窗口,龙娘可以"贴"在桌面上而不是待在一个方框里;跨平台也够用,Linux 桌宠和 Windows 桌宠都能跑。
3.2 透明窗口和点击穿透的两个关键设置
透明窗口这块有两个设置必须做对,否则龙娘要么带个白底,要么挡住你点桌面图标。
第一个是窗口标志。核心是这三行:
self.setWindowFlags( Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool ) self.setAttribute(Qt.WA_TranslucentBackground)FramelessWindowHint去掉标题栏,WindowStaysOnTopHint让它一直在最上层,Tool标志让它不出现在任务栏里(桌宠不该占任务栏位置)。WA_TranslucentBackground是透明背景的关键,没有它,你的 PNG 透明通道会被填成黑色或白色。
第二个是点击穿透。桌宠占着桌面一块区域,如果它拦截鼠标事件,你点它覆盖的图标就点不到。解决办法是给窗口加Qt.WindowTransparentForInput标志,或者在需要交互的区域(比如龙娘身体)单独处理鼠标事件,其余区域穿透。我采用的是后者:龙娘身体可点击(点一下弹个菜单),周围透明区域穿透。
3.3 动画帧的组织方式
龙娘的动作我用的是逐帧 PNG 序列,而不是 GIF 或视频。原因很简单:逐帧序列在 PyQt 里用QTimer控制切换最可控,想加速减速、想根据状态跳帧都方便。GIF 播放控制弱,视频更重。
目录结构大概这样:
assets/ dragon/ idle/ frame_00.png frame_01.png working/ frame_00.png error/ frame_00.png每个状态一个文件夹,QTimer按固定间隔切帧。状态切换时换文件夹、重置帧索引就行。帧率我定在 8 帧/秒,太快显得焦躁,太慢显得卡顿,8 帧对桌宠这种小尺寸角色刚好。
注意:帧图一定要用带 alpha 通道的 PNG,尺寸别太大,单帧控制在 256x256 以内。桌宠是常驻的,帧图太大内存和 GPU 都吃不消,尤其是你开着一堆窗口的时候。
4. 让龙娘"看见"Codex:进程监控与日志解析的落地
技术栈定了,接下来是核心:怎么让龙娘实时知道 Codex 在干嘛。这部分是整个项目里最容易出 bug 的地方,我拆成进程监控和日志解析两块讲。
4.1 进程监控:怎么判断 Codex 活着
判断 Codex 进程是否存活,跨平台最稳的方式是用psutil。它不用你去记tasklist还是ps aux的命令差异,一个 API 搞定。
import psutil def find_codex_process(): for proc in psutil.process_iter(['pid', 'name', 'cmdline']): try: cmdline = proc.info.get('cmdline') or [] name = proc.info.get('name') or '' # Codex CLI 进程名可能是 codex 或 node(如果是 npm 装的) if 'codex' in name.lower(): return proc if any('codex' in str(c).lower() for c in cmdline): return proc except (psutil.NoSuchProcess, psutil.AccessDenied): continue return None这里有个坑要提前说:Codex 的进程名不一定是codex。如果你是通过 npm 全局装的,实际跑起来的可能是node进程,命令行参数里才带 codex 路径。所以判断逻辑要同时看进程名和 cmdline,只看名字会漏。
进程监控的轮询间隔我设的是 2 秒。太频繁没必要,Codex 状态变化没那么快;太慢桌宠反应迟钝。2 秒是个平衡点,CPU 占用几乎可以忽略。
4.2 日志解析:从会话文件里读出"进度"
日志解析是重头戏。Codex 的会话日志一般是 JSON 或 JSONL 格式,一行一条记录。解析思路是:读最后 N 行,找最新的一条有效记录,判断它属于哪类事件。
import json import os from pathlib import Path def read_latest_events(log_path, tail_lines=50): if not os.path.exists(log_path): return [] events = [] with open(log_path, 'r', encoding='utf-8', errors='ignore') as f: lines = f.readlines()[-tail_lines:] for line in lines: line = line.strip() if not line: continue try: events.append(json.loads(line)) except json.JSONDecodeError: continue return events拿到事件列表后,从后往前找第一条能识别的事件,映射成桌宠状态。识别逻辑要写得宽容一点,因为 Codex 的日志字段名可能变。我的做法是:不硬编码字段名,而是做关键词匹配——记录里出现tool_call就当执行中,出现error就当出错,出现assistant且无后续就当思考中。
4.3 日志路径的自动发现
日志路径不能写死,得自动发现。Codex 的配置目录一般遵循这个规律:
- Linux/macOS:
~/.codex/ - Windows:
%USERPROFILE%\.codex\
但用户可能改过配置,所以我的策略是:先查环境变量(如果有CODEX_HOME之类),再查默认路径,最后让用户在桌宠配置里手动指定。三层兜底,基本不会找不到。
def discover_codex_home(): env_home = os.environ.get('CODEX_HOME') if env_home and os.path.isdir(env_home): return Path(env_home) default = Path.home() / '.codex' if default.is_dir(): return default return None提示:如果桌宠一直显示"找不到 Codex",先手动确认
~/.codex/目录存不存在、里面有没有会话文件。很多时候不是代码问题,是 Codex 压根没在这个用户下跑过,目录还没生成。
4.4 状态机的设计:避免状态抖动
把进程状态和日志状态合起来,需要一个状态机来输出最终状态。这里有个容易忽略的问题:状态抖动。比如 Codex 在思考时日志可能几秒没更新,如果直接按"无更新=空闲"判断,龙娘就会在"思考中"和"空闲"之间反复横跳,看着很傻。
我的解法是加一个状态保持时间:新状态必须持续超过 3 秒才真正切换。这样短暂的空档不会触发状态跳变,龙娘的表现就稳了。
class StateMachine: def __init__(self, hold_seconds=3): self.current = 'idle' self.pending = None self.pending_since = 0 self.hold = hold_seconds def update(self, new_state, now): if new_state == self.current: self.pending = None return self.current if new_state != self.pending: self.pending = new_state self.pending_since = now return self.current if now - self.pending_since >= self.hold: self.current = new_state self.pending = None return self.current这个状态机是整个桌宠"看起来聪明"的关键。没有它,龙娘就是个神经质的贴图;有了它,龙娘的反应才像有思考。
5. 龙娘的表情系统与交互设计
状态读出来了,接下来是"演"出来。这部分决定了桌宠是"能用"还是"好用"。
5.1 表情切换的触发逻辑
表情切换不能只靠状态机输出,还要叠加一些临时事件。比如 Codex 报错了,龙娘应该立刻切到沮丧表情并冒个气泡,而不是等状态机慢慢切。所以我的设计是:状态机管基础表情,事件管临时表情。
基础表情跟着状态机走,稳定、慢变。临时表情由事件触发,比如:
- 检测到 error 事件 → 切沮丧表情,持续 5 秒后回基础表情
- 检测到任务完成 → 切开心表情,冒个"搞定"气泡
- 检测到等待确认 → 举牌表情,直到确认事件出现
临时表情用优先级覆盖基础表情,事件结束后自动回落。这样龙娘既有稳定的"底色",又有灵动的"反应"。
5.2 气泡提示的设计取舍
气泡提示(就是龙娘头顶冒的那句话)看着简单,其实设计上要做取舍。我踩过的坑是:气泡信息太多,反而没人看。
一开始我把 Codex 的完整错误信息塞进气泡,结果气泡又长又丑,还挡住代码。后来改成只显示错误类型 + 一句话摘要,完整信息放到右键菜单里。比如气泡只显示"请求失败,点我看详情",用户真想知道细节再点开。
气泡的显示时长也要控制。我设的是 4 秒自动消失,鼠标悬停时暂停计时。这样既不打扰,又不会错过。
5.3 右键菜单:把控制权还给用户
桌宠不能只是个花瓶,得能操作。我给它加了个右键菜单,包含几个实用项:
- 暂停/恢复监控:有时候不想让桌宠读日志,一键暂停
- 切换角色:龙娘只是默认,可以换别的形象
- 查看最近事件:把最近几条 Codex 事件列出来
- 打开日志目录:直接跳到 Codex 日志文件夹
- 退出:干净退出,不留后台进程
菜单用QMenu实现,挂在龙娘的鼠标右键事件上。这块没什么技术难点,但体验提升很大——用户感觉自己在"养"它,而不是被它烦。
5.4 拖拽与位置记忆
桌宠得能拖。用户想把它放屏幕哪个角落就放哪。实现上用mousePressEvent和mouseMoveEvent记录偏移量,移动窗口。
def mousePressEvent(self, event): if event.button() == Qt.LeftButton: self.drag_pos = event.globalPosition().toPoint() - self.frameGeometry().topLeft() event.accept() def mouseMoveEvent(self, event): if event.buttons() == Qt.LeftButton and self.drag_pos: self.move(event.globalPosition().toPoint() - self.drag_pos) event.accept()位置要记忆,下次启动回到原位。存到配置文件里,退出时写、启动时读。这个细节很小,但没有它,每次开机龙娘都跑到屏幕中间,很烦。
6. 打包、开机自启与跨平台的那些坑
代码跑通了,最后一步是让它"像个正经软件"一样用起来。这部分坑最多,我按平台说。
6.1 打包成可执行文件
Python 桌宠打包首选 PyInstaller。命令大概这样:
pyinstaller --noconfirm --windowed --name CodexDragon \ --add-data "assets:assets" \ main.py--windowed是关键,去掉控制台窗口,否则桌宠旁边会挂个黑框。--add-data把素材目录打进去,注意 Windows 上用分号;分隔,Linux/macOS 用冒号:,这是跨平台打包最常见的翻车点。
打包后体积会偏大(PyQt 本身就大),一两百 MB 很正常。如果在意体积,可以换 PySide6 的轻量裁剪,或者用 Nuitka 编译,但配置更复杂,看需求取舍。
6.2 开机自启的三种做法
开机自启我试过三种方式,各有适用场景。
| 方式 | 平台 | 优点 | 缺点 |
|---|---|---|---|
| 启动文件夹快捷方式 | Windows | 简单直观 | 用户容易误删 |
| systemd user service | Linux | 规范,可管理 | 要写 unit 文件 |
| 桌面环境自启配置 | Linux | 图形化 | 依赖具体 DE |
Windows 上最省事的是把快捷方式丢进启动文件夹(shell:startup)。Linux 上我推荐 systemd user service,写个 unit 文件放~/.config/systemd/user/,systemctl --user enable一下就行,比改桌面环境配置通用。
注意:开机自启的桌宠要处理"启动时机"问题。如果桌宠比 Codex 先起来,一开始会找不到进程,显示"睡觉"。这没问题,等 Codex 起来它会自动醒。但别在启动时就报错弹窗,那样很烦。
6.3 跨平台差异的集中处理
跨平台差异主要集中在这几块,我做了统一封装:
- 进程查找:psutil 已经抹平了大部分差异,但进程名匹配规则要放宽
- 日志路径:用
Path.home()而不是硬编码/home/xxx - 透明窗口:Linux 下某些合成器对透明支持不好,需要检测并降级(比如加个半透明背景代替全透明)
- 字体:中文字体在不同系统不一样,气泡文字要指定 fallback 字体链
Linux 桌宠这块特别要提一句:如果你用的是比较老的桌面环境,透明窗口可能显示异常。我的做法是启动时检测合成器是否可用,不可用就自动切到"带边框模式",虽然丑点但能用。
6.4 资源占用与长时间运行的稳定性
桌宠是常驻程序,稳定性比功能更重要。我实测下来,几个影响稳定性的点:
一是内存泄漏。PyQt 里如果频繁创建 QPixmap 不释放,内存会慢慢涨。解决办法是帧图预加载一次,复用而不是每次重读。
二是日志文件句柄。读日志要用with open,别让句柄悬着。我一开始图省事用了全局文件对象,跑一天后句柄耗尽,桌宠直接卡死。
三是异常兜底。桌宠主循环里所有可能抛异常的地方都要 try 住,尤其是日志解析——Codex 日志格式一变,解析就可能抛异常,不兜住整个桌宠就崩了。
def safe_tick(self): try: self.update_state() self.update_animation() except Exception as e: # 记录但不崩,桌宠要能扛住各种意外 self.logger.warning(f"tick error: {e}")这个safe_tick是我踩了无数次崩溃之后加的,强烈建议一开始就写上。
7. 几个真实踩坑记录与排查链路
前面讲的都是"应该怎么做",这一节讲"我实际怎么翻车的",以及怎么一步步排查出来。这些经验比文档值钱。
7.1 龙娘一直显示"睡觉",但 Codex 明明在跑
这是最早遇到的坑。现象是:Codex CLI 在终端里跑得好好的,桌宠却一直显示睡觉。
排查链路是这样的。第一步,确认进程查找逻辑。我手动跑了一遍find_codex_process(),返回 None。说明进程没找到。第二步,看实际进程名。ps aux | grep codex一看,进程名是node,cmdline 里才有 codex 路径。原来我是 npm 装的,进程名根本不是 codex。第三步,改匹配逻辑,同时看 name 和 cmdline,问题解决。
这个坑的教训是:别假设进程名等于工具名。尤其是 Node、Python 这类解释器跑的工具,进程名往往是解释器本身。
7.2 状态在"思考中"和"空闲"之间疯狂横跳
第二个坑是状态抖动。龙娘的表情一秒变好几次,看着像抽风。
排查下来,根因是日志更新有间隔。Codex 思考时可能几秒不写日志,我的逻辑一看"最近没新日志"就判空闲,一有新日志又判思考中,来回切。
解法就是前面说的状态保持时间。加了 3 秒保持之后,抖动消失。这个坑让我意识到:实时性和稳定性是要权衡的,桌宠不需要毫秒级响应,稳定比快更重要。
7.3 打包后在别人电脑上找不到素材
第三个坑是打包。我自己电脑上跑得好好的,打包发给朋友,龙娘变成一片空白。
排查发现是--add-data的路径分隔符问题。我在 Linux 上打包测试没问题,但朋友是 Windows,PyInstaller 在 Windows 上要用分号分隔。而且打包后素材路径要用sys._MEIPASS动态获取,不能再用相对路径。
import sys from pathlib import Path def resource_path(relative): if hasattr(sys, '_MEIPASS'): return Path(sys._MEIPASS) / relative return Path(__file__).parent / relative这个resource_path函数是打包桌宠的标配,一定要用。
7.4 长时间运行后桌宠卡死
第四个坑最隐蔽。桌宠跑几个小时后就卡死,重启又好了。
排查花了很久,最后定位到日志文件句柄泄漏。我早期代码里有个全局的文件对象一直开着读日志,跑久了句柄耗尽。改成每次读都with open之后,问题消失。
这个坑的教训是:常驻程序的资源管理要格外小心。文件、网络连接、定时器,用完就关,别图省事。
7.5 排查这类问题的通用思路
把这几个坑串起来,我总结出一套排查桌宠问题的通用思路:
- 先确认数据源:桌宠显示不对,先手动确认进程在不在、日志有没有更新。数据源没问题再查代码。
- 加日志:桌宠自己的运行日志要记,状态切换、异常都要打出来。出问题时看日志比猜快得多。
- 隔离变量:跨平台问题就分平台测,打包问题就对比打包前后,别一次改一堆。
- 兜底优先:任何可能抛异常的地方都兜住,桌宠崩了比显示错更糟。
8. 还能怎么玩:从桌宠到 Codex 的"可视化外壳"
这个项目做到现在,基本功能已经稳了。但它的想象空间不止于此,我列几个后续可以扩展的方向,给想继续折腾的人参考。
第一个方向是多角色多状态。现在只有龙娘一个角色,可以做成角色包,用户自己换。不同角色对应不同 Codex 使用场景,比如写代码用龙娘,跑测试用别的形象。
第二个方向是任务进度可视化。现在桌宠只显示"在干嘛",可以进一步显示"干到哪了"。比如 Codex 跑一个多步任务,龙娘头顶显示进度条,或者用不同动作表示不同阶段。
第三个方向是轻量交互。现在桌宠只能看,可以加一点交互,比如点一下龙娘让它"催一下" Codex,或者长按弹出快捷命令。注意别做太重,桌宠的核心价值是"陪",不是"管"。
第四个方向是跨工具通用。现在只监控 Codex,其实这套架构可以抽象成"监控任意 CLI 工具",把进程匹配和日志解析做成插件,就能给别的工具也养桌宠。
我个人最看好的是第一个和第四个方向。角色包让桌宠有生命力,通用化让这套代码能复用。至于进度可视化和交互,属于锦上添花,做之前先想清楚会不会让桌宠变"吵"——桌宠一旦开始频繁打扰,用户第一反应就是关掉它。
最后分享一个我自己的使用习惯:我把龙娘放在屏幕右下角,大小调到最小,平时几乎不占注意力。只有 Codex 报错或者任务完成时它才冒气泡,这时候我才会看它一眼。桌宠最好的状态是"存在感刚好"——你需要它的时候它在,不需要的时候它不烦你。这个度,得自己调。