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 PyQtPySide6。理由很实在Codex 本身是命令行工具桌宠要和它做进程级交互Python 在这块最顺手PyQt 的Qt.FramelessWindowHintQt.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_lines50): if not os.path.exists(log_path): return [] events [] with open(log_path, r, encodingutf-8, errorsignore) 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_seconds3): 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图形化依赖具体 DEWindows 上最省事的是把快捷方式丢进启动文件夹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(ftick error: {e})这个safe_tick是我踩了无数次崩溃之后加的强烈建议一开始就写上。7. 几个真实踩坑记录与排查链路前面讲的都是应该怎么做这一节讲我实际怎么翻车的以及怎么一步步排查出来。这些经验比文档值钱。7.1 龙娘一直显示睡觉但 Codex 明明在跑这是最早遇到的坑。现象是Codex CLI 在终端里跑得好好的桌宠却一直显示睡觉。排查链路是这样的。第一步确认进程查找逻辑。我手动跑了一遍find_codex_process()返回 None。说明进程没找到。第二步看实际进程名。ps aux | grep codex一看进程名是nodecmdline 里才有 codex 路径。原来我是 npm 装的进程名根本不是 codex。第三步改匹配逻辑同时看 name 和 cmdline问题解决。这个坑的教训是别假设进程名等于工具名。尤其是 Node、Python 这类解释器跑的工具进程名往往是解释器本身。7.2 状态在思考中和空闲之间疯狂横跳第二个坑是状态抖动。龙娘的表情一秒变好几次看着像抽风。排查下来根因是日志更新有间隔。Codex 思考时可能几秒不写日志我的逻辑一看最近没新日志就判空闲一有新日志又判思考中来回切。解法就是前面说的状态保持时间。加了 3 秒保持之后抖动消失。这个坑让我意识到实时性和稳定性是要权衡的桌宠不需要毫秒级响应稳定比快更重要。7.3 打包后在别人电脑上找不到素材第三个坑是打包。我自己电脑上跑得好好的打包发给朋友龙娘变成一片空白。排查发现是--add-data的路径分隔符问题。我在 Linux 上打包测试没问题但朋友是 WindowsPyInstaller 在 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 报错或者任务完成时它才冒气泡这时候我才会看它一眼。桌宠最好的状态是存在感刚好——你需要它的时候它在不需要的时候它不烦你。这个度得自己调。