DeskPet 是一款原生 macOS 桌面宠物应用(Swift / AppKit + WKWebView)。Live2D 角色以透明窗口常驻桌面,具备待机动作、视线跟随、随音乐节拍摆动与头顶对话气泡;对话内容与动作切换由大语言模型(LLM)以结构化 JSON 生成。应用另集成作息提醒、天气变化提醒、记忆库、每日任务简报,以及支持文献、新闻、行情与天气检索并渲染 Markdown 与 LaTeX 的科研模式。源代码以 MIT 协议开源:Yu-Xiang-Yang/deskpet。本文全部截图均取自本机实际运行的 DeskPet(角色为 Live2D 官方示例模型 Tororo),未经合成或模拟数据渲染。
1 背景与动机
现有桌面宠物应用多为预设动画的循环播放,与用户的交互局限于点击触发固定动作;另一方面,LLM 对话工具以独立窗口形式存在,调用时需切换上下文。DeskPet 的设计目标是将二者合并:
- 常驻且不干扰:角色窗口透明,仅角色本体的不透明像素响应鼠标,其余区域点击穿透至下层窗口;
- 语言与动作一致:模型在同一次回复中给出台词与动作标签,角色的表情、动作与台词语气保持一致;
- 面向科研工作流:同一角色可切换至科研模式,回答以 Markdown 与 LaTeX 渲染,并可调用 arXiv、Semantic Scholar 等检索工具;
- 轻量构建:仅依赖 Xcode 命令行工具(
swiftc),不使用 Xcode 工程。
2 功能概述

图 1 DeskPet 在 macOS 桌面上的运行状态(实机截图,2026-10-14)。角色为 Tororo,名字设为“墨笃”,模型服务为 DeepSeek(deepseek-flash,OpenAI 兼容接口)。头顶气泡为模型对“下午要改论文,半小时后提醒我起来活动一下”的回复,同一回复的 remind 字段创建了一条 30 min 后的提醒,motion 字段触发舔爪动作;底部为对话输入框。
| 模块 | 说明 |
|---|---|
| 角色渲染 | Live2D 模型(Cubism 3/4 运行时格式),以 PixiJS WebGL 渲染;可在设置中切换形象 |
| 交互 | 拖动移动并记忆位置;透明区域点击穿透;单击头部触发摸头反应,单击身体打开对话输入框;全局快捷键(默认 ⌥Space)呼出输入框 |
| 对话气泡 | 至多堆叠 3 条,新气泡位于底部,较早气泡上移并降低不透明度,各自计时后淡出;指针悬停时暂停计时 |
| 结构化回复 | 模型返回 reply、motion、remind、weather_city、remember、forget 六个字段,分别驱动台词、动作、提醒、天气查询与记忆库读写 |
| 音乐律动 | 检测其它应用的音频输出,经系统音频监听估计节拍,驱动角色左右摆动 |
| 主动行为 | 定时闲聊、自由走动、随机待机动作;用户离开 5 min 后进入睡眠状态 |
| 提醒 | 吃饭、上下班、久坐、喝水、25 min 专注计时,以气泡、动作与系统通知同时呈现 |
| 天气变化提醒 | 每 20 min 获取 Open-Meteo 逐小时预报,按规则提示带伞、恶劣天气与次日气温突变 |
| 记忆库 | 模型自动写入长期有效的用户信息,并附加于后续对话的系统提示词 |
| 科研模式 | 回答以 Markdown 与 KaTeX 渲染,可调用 6 个客户端检索工具 |
| 每日任务 | 每日首次使用时依次执行预设任务,结果保存为“每日简报” |
3 交互界面
3.1 对话
单击角色身体或按下 ⌥Space 打开对话输入框。用户消息与角色回复均以气泡形式堆叠于头顶,回复到达时角色同步播放模型选定的动作。
图 2 一次对话的三帧(实机截图,时刻自点击发送起计)。左:输入消息;中:1.5 s,用户消息以紫色气泡显示于上方,回复已开始流式写入下方气泡,角色播放动作;右:7.5 s,回复完整呈现,较早的用户气泡上移并降低不透明度。
模型返回的 JSON 中,reply 显示为气泡,motion 经 pet-map.json 映射为 Live2D 动作,remind 交由提醒调度器;三者在同一次请求中完成,不需要额外的意图识别步骤。
3.2 菜单栏菜单与右键菜单
菜单栏图标(爪印)与角色右键菜单由同一函数 addItems(to:) 生成,条目相同:和我聊天、科研模式、执行每日任务、消息列表、专注计时、提醒设置、设置、显示层级等。科研模式开启时另出现“开始新的科研话题”与“联网检索(较慢)”,并隐藏“去别处转转”。在刘海屏上菜单栏图标可能被遮挡,右键菜单因此保留全部入口。
图 3 菜单栏菜单,科研模式处于开启状态(实机截图)。
显示层级提供三种选项:
| 层级 | 窗口级别 | 行为 |
|---|---|---|
| 始终置顶 | NSWindow.Level.floating |
位于所有普通窗口之上 |
| 普通窗口 | NSWindow.Level.normal |
可被其它窗口遮挡 |
| 仅在桌面 | 桌面图标层级 + 1 | 位于桌面图标之上、全部应用窗口之下;气泡亦随之被遮挡,提醒改由系统通知送达 |
3.3 科研模式
科研模式是角色的一种状态,开启后输入框中的问题交由科研助手回答。回答气泡支持 Markdown、KaTeX 公式与代码高亮,可滚动;单击气泡打开完整对话窗口。
图 4 科研模式下的回答气泡(实机截图)。问题为“简要说明 PINN 求解 Biot 固结问题时损失函数的构成,给出公式”,未开启联网检索。发送后 12 s 时气泡仍显示“思考中.”,18 s 时完整回答已呈现。气泡内以 KaTeX 渲染残差项 $\mathcal{L}_r$、边界项 $\mathcal{L}_b$ 与初始项 $\mathcal{L}_0$,内容超出气泡高度时可滚动,右上角“×”可立即关闭,底部输入框提示处于科研模式。

图 5 消息列表窗口(实机截图)。左栏按“日常对话”与“科研对话”分组,显示标题、时间与消息数,含每日任务生成的“每日简报”;右栏为图 4 所示对话的完整记录,标题旁标注所用模型,可重命名、删除或继续提问,并可勾选“联网检索文献”。
3.4 设置
设置窗口分为通用、主人信息、模型、提醒、每日任务、记忆库六个标签页。图 6 给出其中四页;“主人信息”与“记忆库”含个人资料,未予展示。
图 6 设置窗口(实机截图)。左上:通用页,含名字、称呼、人设与形象选择,形象下方显示模型要求的版权标注;右上:模型页,可在 Claude(Anthropic)与 OpenAI 兼容接口之间切换,API key 以掩码显示并保存于钥匙串;左下:提醒页,含吃饭、上下班、久坐与喝水提醒;右下:每日任务页,每行一项任务,可立即执行一次。
4 系统架构
源代码共 27 个 Swift 文件(5 192 行)与 7 个前端文件(1 428 行),由 build.sh 直接调用 swiftc 编译,不含 Swift Package 或 Xcode 工程。代码分为三层:
| 目录 | 内容 |
|---|---|
Sources/Core/ |
与界面无关的逻辑:JSON、SSE 解析、宠物回复解析、提醒规则、天气提醒规则、本地化 |
Sources/LLM/ |
LLMProvider 协议、Anthropic 与 OpenAI 兼容两种实现、检索工具与天气服务 |
Sources/App/ |
AppKit / SwiftUI:宠物窗口、对话输入框、消息列表、科研引擎、设置、提醒、记忆库、天气监测、音乐监测 |
Resources/web/ |
pet.html/js/css(角色与气泡)、research.html/js/css(消息列表)、md.js(Markdown 与公式) |
数据流如下:
用户输入(输入框 / ⌥Space) ─▶ PetBrain ──┬─ 系统提示词:人设 + 主人信息 + 记忆库 + 回复格式
├─ 上下文:当前时间 + 所在地天气(缓存 30 min)
▼
LLMProvider(Anthropic / OpenAI 兼容,SSE 流式)
▼
PetReply.parse ─▶ reply → 气泡(pet.js)
├─ motion → pet-map.json → Live2D 动作组 / 表情
├─ remind → ReminderScheduler
├─ weather_city → 查询天气后再次请求
└─ remember / forget → MemoryStore
角色渲染与气泡位于 WKWebView 中,原生层与网页层经 WKScriptMessageHandler 与 evaluateJavaScript 双向通信。原生层负责窗口、鼠标穿透、视线坐标、提醒与模型调用;网页层负责 Live2D 绘制、命中测试与气泡排版。
5 关键实现
5.1 结构化回复
宠物对话要求模型只输出一个 JSON 对象,其 JSON Schema 由 PetReply.schema 给出,所有对象均设 additionalProperties: false。motion 取自 11 种语义动作:idle、happy、wave、nod、shake、think、surprised、sleepy、shy、eat、sad。Claude 以结构化输出(structured outputs)保证格式;OpenAI 兼容接口使用 JSON 模式。
解析器对模型输出采取容错策略:依次尝试整段文本、Markdown 代码块内文本,以及首个 { 至末个 } 之间的子串;未知动作名回退为 idle;全部失败时以整段文本作为台词。该策略使不支持严格结构化输出的模型(如部分 OpenAI 兼容服务)同样可用。
5.2 语义动作到模型动作的映射
不同 Live2D 模型的动作组与表情命名各异。每个模型目录下的 pet-map.json 将 11 种语义动作映射至该模型的动作组(group)、组内序号(index)与表情(expression);条目可含 params,在动作期间平滑覆盖模型参数,用于模型未提供的表情。以示例模型 Tororo 的 shake 为例:
"shake": {
"group": "Idle", "index": 1,
"params": { "PARAM_EAR_L": -0.7, "PARAM_EAR_R": -0.7, "PARAM_TAIL_ANGRY": 1 }
}
该条目在播放待机动作的同时将双耳下压、尾部置为生气姿态。说话期间,应用逐帧设置 LipSync 组参数(缺省为 ParamMouthOpenY)以驱动口型。
5.3 透明区域的点击穿透
macOS 的 NSPanel 只能整体设置 ignoresMouseEvents,无法按像素区分。DeskPet 采用“网页端采样 + 原生端查询”的方式:
- 网页端每 200 ms 将 WebGL 画布(
preserveDrawingBuffer: true)缩绘至探测画布,以 10 × 10 CSS 像素为单元读取 alpha 通道,alpha > 24 的单元标记为实心,所得位图经消息通道传给原生层; - 原生层以 30 Hz 的频率读取指针位置,换算为页面坐标后查询位图;指针位于实心单元时令窗口接收鼠标事件,否则令其穿透。
同一定时器将指针坐标传给网页端,驱动角色的视线跟随。
5.4 音乐节拍跟随
音乐律动分为播放检测与节拍估计两步:
- 播放检测:每 0.5 s 经 Core Audio 进程对象查询其它应用是否在输出音频,该查询无需授权;输出持续 ≥ 1.5 s 视为开始播放(以排除通知音),静默 ≥ 2 s 视为停止;
- 节拍估计:macOS 14.2 及以上版本经 Core Audio process tap 获取系统音频(需“系统音频录制”授权),计算约 150 Hz 低通频段的能量包络并检测起音,以节拍间隔的中位数估计速度,驱动角色以半拍速左右摆动。未获授权时,以约 112 BPM 的固定节奏代替。
音频仅在本机实时分析,不录制、不保存、不上传。
5.5 记忆库
对话中出现的长期有效信息(偏好、过敏、重要日期、长期计划等)由模型经 remember 字段写入,界面以“📝 记下了”气泡提示;forget 字段按关键词删除。条目按时间由新到旧附加于闲聊、科研与每日任务的系统提示词,上限 2 400 字符。去重规则如下:
| 情形 | 处理 |
|---|---|
| 规范化后相同,或互相包含 | 以新表述替换旧条目 |
| 仅存在少量字符插入(较短文本按序保留于较长文本,且长度比 ≥ 0.7) | 以新表述替换旧条目 |
| 词语替换 | 视为不同事实,均保留 |
5.6 科研模式的工具循环
科研引擎单轮至多调用模型 3 次:前 2 次允许工具调用,第 3 次禁止工具、强制作答。同一轮的全部工具调用并行执行,结果合并为一条消息回传。可用的客户端工具如下:
| 工具 | 数据来源 | 说明 |
|---|---|---|
search_arxiv |
arXiv API | 预印本检索 |
search_semantic_scholar |
Semantic Scholar API | 已发表论文检索 |
search_news |
Bing News RSS,失败时 Google News RSS | 新闻按时间倒序 |
get_stock_quotes |
Yahoo Finance | 股票、指数与 ETF 行情,6 位 A 股代码自动换算为 .SS / .SZ |
get_weather |
Open-Meteo | 当前天气与 1–7 天预报,支持中文地名 |
fetch_webpage |
公网网页 | 仅允许公网 http/https 地址,正文截取至约 6 000 字符 |
稳定性措施:单次请求超时 12 s;HTTP 429 / 503 仅重试一次,同一主机此后 60 s 内直接返回错误,使模型改用其它来源;相同检索 10 min 内复用缓存。使用 Claude 时另可调用服务端 web_search;Claude 的助手消息以原始内容块保存并原样回传,使对话历史保持只追加,以利于提示缓存。
部分 OpenAI 兼容模型(如 DeepSeek)偶尔将工具调用以其原生标记写入正文。应用在流式输出中隐藏此类标记,并将其解析为工具调用执行。
5.7 天气变化提醒
依据“主人信息”中的城市,每 20 min 获取一次逐小时预报并与当前天气比较:
| 类别 | 触发条件 | 频率 |
|---|---|---|
| 带伞 | 当前无降水,而 3 h 内降水概率 ≥ 60 %(或出现降水天气代码且概率 ≥ 40 %) | 6 h 内至多一次 |
| 恶劣天气 | 6 h 内出现雷暴、大雨、大雪,或风速 ≥ 45 km/h | 6 h 内至多一次 |
| 次日变化 | 17:00 后,明日最高气温较今日变化 ≥ 6 °C,或明日降水概率 ≥ 70 % | 每日一次 |
6 模型服务
| 服务 | 配置项 | 说明 |
|---|---|---|
| Claude(Anthropic) | API key、模型名(默认 claude-opus-5-5)、拒答回退开关 |
Messages API 流式调用;宠物对话使用 effort: low 与结构化输出,科研对话使用 effort: high |
| OpenAI 兼容接口 | Base URL、模型名、API key | 预设 OpenAI、DeepSeek、通义千问、Moonshot 与 Ollama;本地服务可不填 key |
API key 保存于 macOS 钥匙串(service 为 local.deskpet),不以明文写入磁盘或 UserDefaults。
7 构建与安装
7.1 构建
需 macOS 14 及以上(节拍跟随需 14.2 及以上)与 Xcode 命令行工具:
git clone https://github.com/Yu-Xiang-Yang/deskpet.git
cd deskpet
./build.sh install
首次构建时,scripts/fetch-vendor.sh 下载固定版本的前端库(pixi.js 6.5.10、pixi-live2d-display 0.4.0、marked 12.0.2、DOMPurify 3.1.6、KaTeX 0.16.11、highlight.js 11.10.0)并逐一校验 SHA-256,运行时不访问任何 CDN。
未配置签名证书时应用以 ad-hoc 方式签名,每次重新构建后签名改变,系统将再次请求钥匙串与“系统音频录制”授权。在“钥匙串访问”中创建名为 DeskPet Code Signing 的自签名代码签名证书后,build.sh 即以其签名,授权在重新构建后保持有效。
7.2 Live2D 组件
Live2D Cubism Core 与示例模型均禁止再分发,仓库因此不含任何 Live2D 文件,需使用者自行从 Live2D 官网下载 Cubism SDK for Web 与 Tororo & Hijiki 示例模型,再执行:
scripts/install-live2d.sh ~/Downloads/CubismSdkForWeb-5-r.5.zip ~/Downloads/tororo_hijiki_ja.zip
脚本将 Cubism Core 与两个模型复制至 ~/Library/Application Support/DeskPet/models/,并附上对应的 pet-map.json。Cubism Core 5.1 起绘制顺序的接口发生变化,pet.js 在加载 Core 后注册兼容映射,使 pixi-live2d-display 0.4.0 可与 Cubism SDK for Web 5-r.5(Core 6.0.1)配合使用。
8 测试
| 脚本 | 内容 |
|---|---|
scripts/selftest.sh |
编译 Core、LLM 与记忆库源码及 Tests/main.swift,共 88 项检查,覆盖 SSE 解析、两种流式累加器、请求体字段、宠物回复解析、提醒规则、记忆去重与持久化、天气提醒规则及检索工具辅助函数 |
scripts/integration-test.sh |
启动 Tests/mock_server.py,以 7 字节分片写出两种 API 的流式响应,经真实 HTTP 流验证工具循环、thinking 块回放、回退请求头、HTTP 错误信息与 JSON 模式;检索工具调用真实接口 |
9 数据存储与网络访问
| 内容 | 位置 |
|---|---|
| 对话记录 | ~/Library/Application Support/DeskPet/conversations/*.json |
| 记忆库 | ~/Library/Application Support/DeskPet/memories.json |
| Live2D 模型与 Cubism Core | ~/Library/Application Support/DeskPet/models/ |
| 设置、窗口位置、提醒状态 | UserDefaults(域 io.github.yu-xiang-yang.deskpet) |
| API key | 钥匙串(service local.deskpet) |
应用仅访问所选模型服务的 API 地址、上述检索工具的数据源,以及模型经 fetch_webpage 指定的公网网页。
10 设计取舍
| 设计 | 依据 |
|---|---|
| 原生窗口 + WKWebView 渲染 | Live2D 的成熟 Web 运行时可直接复用;窗口层级、穿透、快捷键与通知由 AppKit 处理 |
语义动作 + pet-map.json |
模型只需选择 11 种语义动作之一,与具体 Live2D 模型解耦;更换模型时仅需编写映射文件 |
| 基于 alpha 位图的点击穿透 | NSPanel 不支持按像素穿透;200 ms 采样、30 Hz 查询的开销可忽略 |
| 宠物对话低 effort、科研对话高 effort | 闲聊要求低延迟,科研回答要求推理深度 |
| 工具循环上限 3 次调用 | 限制检索带来的延迟与费用,并保证必有回答 |
| 不随仓库分发 Live2D 文件 | 遵守 Live2D 的许可条款;模型由使用者自行获取 |
| API key 存于钥匙串 | 避免明文写入磁盘 |
11 局限与注意事项
- Live2D 组件须由使用者自行获取;公开分发允许加载任意模型的预编译应用前,须确认 Live2D 的发布许可条款(可能属于“可扩展型应用”);
- ad-hoc 签名下,每次重新构建需重新授权钥匙串与系统音频录制;
- 新闻与行情依赖非官方接口,可能随服务方调整而失效;
- “仅在桌面”层级下气泡被应用窗口遮挡,提醒仅以系统通知可见。
12 项目地址
GitHub:https://github.com/Yu-Xiang-Yang/deskpet(MIT 协议,含中英文 README 与第三方许可说明)。
本文截图使用 Live2D 示例模型 Tororo。This content uses sample data owned and copyrighted by Live2D Inc. Live2D 及 Cubism 为 Live2D Inc. 的商标或注册商标;本项目与 Live2D Inc. 无关联。