这件事的起点其实特别简单:桌上有一台 Kindle Paperwhite,吃灰很久了。
扔了舍不得,继续放抽屉里又有点浪费。它屏幕不大,性能也不强,但 e-ink 常显、不刺眼、耗电低,这几个特点放在今天反而挺稀缺。
前阵子我刚折腾过 Codex 桌面宠物,让一个小宠物在电脑里显示 Agent 的运行状态。那次有个判断后来越想越成立:桌面宠物不是装饰品,它更像一个轻量状态层。于是很自然地冒出一个想法:既然 Codex 里可以养桌面宠物,那吃灰的 Kindle 能不能变成一块实体桌宠屏?
一开始真没想做什么严肃的监控系统,只是想把这台 Kindle 用起来,让它在桌面上有点存在感。后来才发现,Claude Code 和 Codex 的状态刚好很适合放上去:谁在跑,谁闲着,在哪个项目里,抬头看一眼就知道。
实拍:Kindle 上单独显示 Codex 当前状态和最近任务。
所以这篇不是“我为了效率发明了一个看板”的故事。
更准确地说,是我想让一台吃灰 Kindle 重新回到桌面上,顺手把它做成了 AI 桌宠的第二块屏。
一、先有一台吃灰 Kindle
Kindle 最尴尬的地方是:它还挺好,但你就是不怎么用。
看书有手机、iPad、微信读书;拿来当主力屏幕又太慢。它很难再成为一个中心设备。
但如果换个思路,不让它当中心,只让它承担一个很窄的任务:常显、低干扰、放在桌角。这个位置反而很适合 Kindle。
它不像第二屏那样不断吸引你点开消息,也不像手机那样一亮就把注意力拽走。它慢、黑白、刷新不频繁,天然适合展示那种“不急,但一直有用”的信息。
这和桌面宠物的思路其实是一脉相承的。
桌面宠物的价值不在于多一个可爱的东西,而是把 Agent 的状态从日志、终端、IDE 插件里拿出来,变成余光就能感知到的小反馈。Kindle 只是把这个状态层从电脑屏幕里拿出来,放到了一块真实的 e-ink 屏上。
很多旧设备不是没用了,只是它不该再承担主力设备的任务。给它一个足够窄的位置,它就能重新变得好用。
这个判断在旧手机、旧平板、树莓派小屏上也成立。关键是别指望它重新变成生产力中心,而是找一个足够克制的单点任务。
二、把状态搬到实体屏上
既然起点是“桌宠的延伸”,第一版设计就不想做成那种枯燥的监控面板:一堆数字、一堆进度条、再配几条日志。
Kindle 的纸质感屏幕,适合放点更轻、更有陪伴感的东西。
最后定下来的是:两只像素小宠物,一只代表 Claude,一只代表 Codex。
实拍:Claude 和 Codex 两只像素宠物,加上下面的系统状态。
它们头顶各有一个小气泡,显示当前状态,比如:
runningidlesleeping- 当前项目名
- 最近任务摘要
后来发现这俩工具的社区里本来就有很适合像素化的形象。我从社区项目 rullerzhou-afk/clawd-on-desk 里拿到了两套风格很接近的 GIF,再用 Python Pillow 自动转成 32x32 黑白像素图,直接喂给前端。
这一步比想象中重要。
如果只是写 Claude running / Codex idle,看两天就会腻。但换成两个小宠物之后,这块屏幕突然有了点桌面摆件的感觉。它不是强提醒,不制造压力,只是在那里安静地告诉你:谁还醒着,谁已经睡了。
这里有一个很小但很实际的工程判断:状态展示不是越详细越好。
如果一块屏幕放在桌角,用户不会认真阅读它。它更像路边的红绿灯,靠形状、位置、少量文字完成信息传递。信息密度太高,反而会把它从“环境感知”拖回“需要阅读的界面”。
我最后保留的状态层级大概是这样:
| 层级 | 内容 | 作用 |
|---|---|---|
| 第一层 | 宠物状态 | 一眼知道 Agent 是否活跃 |
| 第二层 | 项目名 / 最近任务 | 知道它在忙什么 |
| 第三层 | Mac 系统状态 | 顺手看资源占用 |
| 放弃项 | 详细日志 / token / 长文本 | 太重,不适合 Kindle |
这也是整个方案的基调:它不是控制台,也不是监控大屏,只是一块低干扰状态屏。
三、真正的坑在 Kindle 浏览器
我原本以为,现在的 Kindle 浏览器再差,也应该是个能用的现代浏览器。
真机一跑才发现,想多了。
它基本像 2014 年左右的 WebKit。JavaScript 很多时候跑不动,CSS Grid 不支持,连一些继承样式都不稳定。
给 Kindle 做前端,最重要的心法是:假装你回到了 2010 年。
SVG 很漂亮,但 Kindle 直接白屏
第一版宠物是用 SVG 一个个像素拼出来的,需要 JS 在客户端循环渲染。在 Mac 浏览器里看完美,推到 Kindle 上,一片空白。
这个问题并不复杂,但很典型:现代浏览器能跑,不代表老设备能跑。
最后的解法很朴素:服务端直接生成 PNG。
我用 Pillow 把 32x32 sprite 用 Image.NEAREST 放大到 256x256,Flask 通过 /pet/claude.png 直接返回图片字节。Kindle 只需要会 `` 标签就行。
示意代码大概是这样:
from io import BytesIO
from flask import Flask, send_file
from PIL import Image
app = Flask(__name__)
def render_pet_png(path: str, scale: int = 8):
img = Image.open(path).convert("L")
# 转成黑白,避免 Kindle 上灰阶表现不可控
img = img.point(lambda p: 255 if p > 128 else 0)
w, h = img.size
img = img.resize((w * scale, h * scale), Image.NEAREST)
buf = BytesIO()
img.save(buf, format="PNG")
buf.seek(0)
return buf
@app.route("/pet/claude.png")
def claude_pet():
return send_file(
render_pet_png("assets/claude_32.png"),
mimetype="image/png",
max_age=0
)
这类方案不优雅,但稳定。
很多时候,给弱设备做页面,最好的优化不是压缩 JS,也不是 polyfill,而是别让它执行复杂逻辑。能在服务端算掉的,就在服务端算掉。
CSS Grid 很现代,但 Kindle 不认
期望的布局是两只宠物左右并排。我一开始用了 display: grid,Mac 上看好端端的。Kindle 上,两只宠物纵向堆成一列,每只占满整行。
第一反应是退回 table 布局。
我知道,2020 年之后还用 table 做布局多少有点逆时代。但在 Kindle 浏览器上,table 一度是唯一能保证横排的方案。
这种场景里,技术洁癖没什么意义。目标设备不支持,规范再漂亮也没用。
table 能救命,也会制造新坑
修完前两个坑之后,又发现宠物卡片的右边和下面 SYSTEM 卡片的右边对不齐,差了 17 像素。
debug 半天才发现:Kindle 上 table 的宽度计算有点诡异,不完全按父容器内容区宽度算,而是更接近按外层 border-box 处理,相当于忽略了父容器的 padding。于是 table 比旁边的 div 宽出一截。
最后的解法是彻底放弃 table,改成:
- 外层容器固定宽度
- 子元素
float:left - 两个宠物卡片各占
50% - 全局使用
box-sizing:border-box - 中间共享一根边框,避免重复边框导致宽度误差
核心 CSS 大概这样:
* {
box-sizing: border-box;
}
.container {
width: 560px;
margin: 0 auto;
padding: 16px;
}
.pet-row {
width: 100%;
overflow: hidden; /* 清除 float */
border: 2px solid #000;
}
.pet-card {
float: left;
width: 50%;
height: 300px;
text-align: center;
padding: 12px;
}
.pet-card.left {
border-right: 2px solid #000;
}
这事挺讽刺:你以为你在做一个 AI 状态看板,最后真正让你破防的是老浏览器布局。
越是跑在老设备上的东西,越不能相信“现代前端理所当然能用”。
四、状态数据不用 API
我没有调用 Claude Code 或 Codex 的 API,也没有从 CLI 里硬解析输出。
原因很简单:状态看板要稳定,依赖越少越好。
Claude Code 和 Codex 本来就会把会话写到本地 JSONL,我直接读这些文件。它们不一定是官方承诺的稳定接口,但相比模拟终端输出、hook CLI 进程、或者依赖一堆外部 API,本地文件扫描在这个场景下反而是更稳的选择。
大致链路是这样:

这里的关键不是“技术先进”,而是链路短。
Kindle 只是展示层,它不负责复杂计算;后台服务只读本地文件,做轻量解析;前端尽量是静态 HTML PNG 图片。整个系统没有登录态,没有第三方服务,没有 WebSocket,也不需要 Kindle 跑复杂 JS。
示意代码可以很简单:
import json
import glob
from pathlib import Path
from datetime import datetime
def read_last_jsonl_line(pattern: str):
files = sorted(glob.glob(pattern), key=lambda p: Path(p).stat().st_mtime, reverse=True)
if not files:
return None
latest = files[0]
last_line = None
with open(latest, "r", encoding="utf-8") as f:
for line in f:
if line.strip():
last_line = line
if not last_line:
return None
try:
return json.loads(last_line)
except json.JSONDecodeError:
return None
def detect_agent_status(pattern: str):
event = read_last_jsonl_line(pattern)
if not event:
return {
"status": "idle",
"project": "-",
"updated_at": "-"
}
ts = event.get("timestamp") or event.get("created_at")
project = event.get("cwd") or event.get("project") or "-"
return {
"status": "running",
"project": Path(project).name if project != "-" else "-",
"updated_at": ts or datetime.now().strftime("%H:%M")
}
实际实现里还要做一些兜底:
- JSONL 文件不存在时显示
idle - 文件被写入中时避免解析半行
- 路径字段不稳定时做多字段兼容
- 最近更新时间太久时自动降级为
sleeping - 任务摘要过长时截断,避免 Kindle 页面撑开
我没做的是“Claude Code / Codex 使用额度”。
这个功能一开始其实很想放。Kindle 上能直接看到还剩多少额度,确实很实用。但折腾了一圈发现,Claude Code 和 Codex 的 CLI 目前都没有一个我能放心长期依赖的稳定接口。
Claude Code 里的 /usage 更像 CLI 内部能力,不适合作为外部服务长期依赖的公开接口;Codex 这边也没有看到足够稳定的额度 API。
所以 v1 只能放弃额度显示。
这个取舍有点可惜,但我觉得是对的。状态看板最怕变成另一个需要维护的系统。为了一个不稳定数字,引入 fragile parser、定时命令、异常重试,最后很可能比它带来的价值还重。
对这块屏幕来说,核心问题不是“我还能跑多久”,而是“它们现在有没有在跑”。
工具越轻,越适合常驻。
五、顺手塞进 Mac 状态
宠物显示完之后,Kindle 屏幕下半部还有一大块空白。索性把 Mac 的系统状态也放上去:
| 指标 | 读取方式 |
|---|---|
| CPU | psutil.cpu_percent() |
| 内存 | psutil.virtual_memory().percent |
| 磁盘 | (total - free) / total |
| 温度 | smctemp -c,主要用于 Apple Silicon |
| 网络 | psutil.net_io_counters() 两次采样差 |
| 开机时长 | time.time() - psutil.boot_time() |
这部分没有做得很复杂,因为 Kindle 的刷新频率也不适合做实时监控。
我最后采用的是定时刷新页面,而不是 WebSocket 或长连接。比如每 30 秒刷新一次:
很土,但在 Kindle 上很好用。
这也是一个典型的工程权衡:
| 方案 | 优点 | 问题 | 是否适合 Kindle |
|---|---|---|---|
| WebSocket | 实时性好 | 老浏览器支持不稳,连接维护复杂 | 不适合 |
| AJAX 轮询 | 局部刷新,体验好 | 依赖 JS,兼容性风险高 | 勉强 |
| Meta Refresh | 极其简单 | 整页刷新,不够优雅 | 适合 |
| 手动刷新 | 最省电 | 需要人操作 | 不适合常显 |
如果是现代浏览器,我大概率会用 SSE 或 WebSocket。但 Kindle 这个环境下,简单比优雅重要。
系统状态代码也没必要写成复杂监控 agent,一个 Python 函数就够:
import time
import shutil
import psutil
import subprocess
_last_net = None
_last_time = None
def get_temperature():
try:
output = subprocess.check_output(["smctemp", "-c"], timeout=1)
return output.decode("utf-8").strip()
except Exception:
return "-"
def get_system_status():
global _last_net, _last_time
cpu = psutil.cpu_percent(interval=0.1)
memory = psutil.virtual_memory().percent
disk = shutil.disk_usage("/")
disk_percent = round((disk.total - disk.free) / disk.total * 100, 1)
now = time.time()
net = psutil.net_io_counters()
if _last_net and _last_time:
seconds = max(now - _last_time, 1)
up = (net.bytes_sent - _last_net.bytes_sent) / seconds
down = (net.bytes_recv - _last_net.bytes_recv) / seconds
else:
up = down = 0
_last_net = net
_last_time = now
uptime_seconds = int(time.time() - psutil.boot_time())
return {
"cpu": cpu,
"memory": memory,
"disk": disk_percent,
"temperature": get_temperature(),
"network_up": format_speed(up),
"network_down": format_speed(down),
"uptime": format_uptime(uptime_seconds),
}
def format_speed(value):
if value > 1024 * 1024:
return f"{value / 1024 / 1024:.1f} MB/s"
if value > 1024:
return f"{value / 1024:.1f} KB/s"
return f"{value:.0f} B/s"
def format_uptime(seconds):
hours = seconds // 3600
minutes = seconds % 3600 // 60
return f"{hours}h {minutes}m"
这里的状态不是为了排障,而是为了“桌面余光感知”。
CPU 飙高、温度异常、网络在跑,一眼能看到就够了。真要排查问题,还是回到 Activity Monitor、htop、日志系统这些工具里。
别把小屏做成大屏,这是这类项目里很容易踩的坑。
六、架构越简单越能活
最后整个方案其实很朴素:

技术栈没有什么花活:
- Python
- Flask
- Pillow
- psutil
- 一点非常保守的 HTML/CSS
- Kindle 自带浏览器
如果非要总结设计原则,我会把它压成几条:
- 让 Kindle 只负责展示
- 让服务端承担渲染和兼容性
- 前端不用现代能力,能不用 JS 就不用
- 状态来源尽量本地化
- 信息密度控制住,不要做成监控大屏
这类东西最怕一开始就奔着“平台化”去。
比如加用户系统、加插件机制、加主题市场、加远程同步、加移动端控制。听起来都挺合理,但会把一个本来半天能跑起来的小工具,变成一个需要长期维护的产品。
个人项目尤其要警惕这种膨胀。
很多工具不是死在难做,而是死在做太多。
七、旧设备复用的关键是降级预期
这次折腾 Kindle,最大的感受不是“电子墨水屏多适合 AI”,而是旧设备复用需要先降级预期。
Kindle 的屏幕很好,但浏览器很老;它适合常显,但不适合交互;它省电,但刷新慢;它有实体存在感,但显示能力很有限。
这些限制听起来像缺点,但如果任务选对了,反而会变成产品约束。
AI 工具现在越来越多,状态也越来越碎。Claude Code 在跑一个任务,Codex 在另一个项目里改代码,终端里还有别的命令。很多时候你不需要一个完整 dashboard,只需要知道:它们是不是还在工作。
把这个状态放在 Kindle 上,有点像给 AI Agent 一个实体化的呼吸灯。
它不抢注意力,也不要求你处理。你抬头看一眼,心里有数,然后继续干自己的事。
这可能就是这类小工具最舒服的地方:它没有试图改变工作流,只是在原来的工作流旁边补了一层很轻的反馈。
如果手上也有一台吃灰 Kindle,这个方向值得试试。但别一上来就做复杂系统。先让它显示一个状态、一张图、一个刷新中的小宠物。只要它重新回到桌面上,而不是躺在抽屉里,这个项目就已经赢了一半。



























































