ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

给Codex养只龙娘:Python桌宠开发实战与状态监控

给Codex养只龙娘:Python桌宠开发实战与状态监控

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 serviceLinux规范,可管理要写 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 排查这类问题的通用思路

把这几个坑串起来,我总结出一套排查桌宠问题的通用思路:

  1. 先确认数据源:桌宠显示不对,先手动确认进程在不在、日志有没有更新。数据源没问题再查代码。
  2. 加日志:桌宠自己的运行日志要记,状态切换、异常都要打出来。出问题时看日志比猜快得多。
  3. 隔离变量:跨平台问题就分平台测,打包问题就对比打包前后,别一次改一堆。
  4. 兜底优先:任何可能抛异常的地方都兜住,桌宠崩了比显示错更糟。

8. 还能怎么玩:从桌宠到 Codex 的"可视化外壳"

这个项目做到现在,基本功能已经稳了。但它的想象空间不止于此,我列几个后续可以扩展的方向,给想继续折腾的人参考。

第一个方向是多角色多状态。现在只有龙娘一个角色,可以做成角色包,用户自己换。不同角色对应不同 Codex 使用场景,比如写代码用龙娘,跑测试用别的形象。

第二个方向是任务进度可视化。现在桌宠只显示"在干嘛",可以进一步显示"干到哪了"。比如 Codex 跑一个多步任务,龙娘头顶显示进度条,或者用不同动作表示不同阶段。

第三个方向是轻量交互。现在桌宠只能看,可以加一点交互,比如点一下龙娘让它"催一下" Codex,或者长按弹出快捷命令。注意别做太重,桌宠的核心价值是"陪",不是"管"。

第四个方向是跨工具通用。现在只监控 Codex,其实这套架构可以抽象成"监控任意 CLI 工具",把进程匹配和日志解析做成插件,就能给别的工具也养桌宠。

我个人最看好的是第一个和第四个方向。角色包让桌宠有生命力,通用化让这套代码能复用。至于进度可视化和交互,属于锦上添花,做之前先想清楚会不会让桌宠变"吵"——桌宠一旦开始频繁打扰,用户第一反应就是关掉它。

最后分享一个我自己的使用习惯:我把龙娘放在屏幕右下角,大小调到最小,平时几乎不占注意力。只有 Codex 报错或者任务完成时它才冒气泡,这时候我才会看它一眼。桌宠最好的状态是"存在感刚好"——你需要它的时候它在,不需要的时候它不烦你。这个度,得自己调。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进