news 2026/8/16 2:57:04

:实现微信、QQ提示音接管与 OpenCode 联动桌面宠物开发实录

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
:实现微信、QQ提示音接管与 OpenCode 联动桌面宠物开发实录

桌面宠物开发实录:实现微信、QQ提示音接管与 OpenCode 联动

文章目录

  • 桌面宠物开发实录:实现微信、QQ提示音接管与 OpenCode 联动
    • 摘要
    • 一、场景切入:我想要的不是"挂件",而是嵌入生活的角色
    • 二、总体架构:一个进程,四条线程
    • 三、原理拆解与关键实现
      • 3.1 精灵图动画:一图多动作,配置驱动
      • 3.2 声音引擎:场景 → 文件映射,多通道播放
      • 3.3 核心难点:WASAPI 只读监听,实现"提示音接管"
      • 3.4 定时提醒与日程:调度线程 + SQLite
      • 3.5 OpenCode 联动:内嵌 FastAPI,信号桥接线程
        • 配置提示
        • 关键实现
    • 四、项目结构速览
    • 五、总结

摘要

本文分享一个 PyQt5 桌面宠物的完整实现:不碰任何第三方文件,用 pycaw / WASAPI 对音频会话做只读轮询,配合状态机降误报,在微信响起的瞬间播放自定义语音(叠加 / 软替换双模式);同时内嵌 FastAPI 服务接入 opencode-im-notifier 插件,任务完成时弹窗并播"任务完成"语音。文章围绕精灵动画、声音引擎、音频监听、定时调度、OpenCode 联动五个子技术点展开,以低资源占用、高体验为设计取舍,只保留关键实现与可复用代码。

一、场景切入:我想要的不是"挂件",而是嵌入生活的角色

做桌宠的初衷很简单:把自己喜欢的动漫角色放进电脑里。但是又不希望只是一张会动的图片——真正的期待是,角色**"活"在工作环境里**:来消息时开口提醒,到点催喝水,任务跑完击掌提醒等。

市面上的桌宠,大多是个"挂件":

  1. 用法有限:主要是一个会动的背景板,接不了微信/QQ 提示音,也感知不到你的开发任务,热闹一阵就沦为桌面角落的装饰。
  2. 资源占用:为变现塞满广告,白白吃掉大量内存和 CPU——为了一点可爱付出卡顿的代价,本末倒置。

于是我把目标定为三个:
① 一个可自定义、会卖萌的精灵图桌宠,把喜欢的角色请进电脑;
② 微信/QQ 来消息时播自定义语音(“接管"提示音)——角色真的"会说话”;
③ opencode 任务完成时主动通知——角色见证你的工作,而不是在旁边发呆。

一句话概括这条主线:低资源占用 + 高用户体验,让桌宠从"挂件"变成"嵌入生活的工具"。下面按"原理拆解 → 关键实现"记录这条路。
首先,效果展示:
气泡效果如图所示:

功能详情如图所示:

二、总体架构:一个进程,四条线程

项目是单进程多线程:PyQt5 事件循环为主干,三个守护线程各自干活,通过Qt 信号(Signal)回主线程更新 UI,天然线程安全。

为守住"低资源占用"这条底线,三个守护线程全部做成事件驱动:音频轮询 0.25s 一次、调度每秒一次、OpenCode 服务惰性导入——平时几乎不占 CPU,只在关键时刻向主线程发信号,让资源让位给体验。

线程职责与主线程通信
主线程精灵动画、气泡、提醒 UI、托盘
SchedulerManager每秒扫 SQLite,查到期提醒remind_signal
AudioWatcher0.25s 轮询 WASAPI 会话notify_signal
OpenCodeHook内嵌 FastAPI 服务收通知hook_signal
┌──────────────────── 主线程(UI) ────────────────────┐ │ FloatingBall 精灵动画 / 气泡 / 提醒卡片 / 托盘 │ │ │ │ │ │ │ SoundEngine(声音) SchedulerManager AudioWatcher │ │ │ (定时调度) (音频监听:pycaw) │ │ │ │ │ │ │ OpenCodeHook ◄── 内嵌FastAPI ◄── opencode-im-notifier│ └────────────────────────────────────────────────────┘

代码量分布也说明主线:floating_ball.py是躯干,其余模块各司其职。下面按五个子技术点拆解。

三、原理拆解与关键实现

3.1 精灵图动画:一图多动作,配置驱动

精灵图是精灵表(Sprite Sheet)img/spt.webp共 9 行 × 8 列,每格 192×208 像素,一行一个动作。帧配置在img/spt.txt,格式是"每行动作保留的帧数 # 动作描述",例如:

6 # 眨眼(待机) 8 # 向右跑 4 # 打招呼1 5 # 跳跃

这样加新动作只需改文本配置,不用动代码。加载时按行裁剪,并用keep_cfg截断帧数:

defload_sprite_sheet(self):img=Image.open(self.sprite_path).convert("RGBA")forrowinrange(self.rows):action_frames=[]forcolinrange(self.cols):frame=img.crop((col*self.frame_width,row*self.frame_height,(col+1)*self.frame_width,(row+1)*self.frame_height))# 剔除几乎全透明的帧(动作末尾的空帧)alpha=frame.getdata(3)ifsum(1forainalphaifa>10)<len(alpha)*0.005:continue# RGBA 原始字节 → QImage → QPixmapdata=frame.tobytes("raw","RGBA")qimage=QImage(data,self.frame_width,self.frame_height,QImage.Format_RGBA8888)action_frames.append(QPixmap.fromImage(qimage))ifrowinkeep_cfg:# 按 spt.txt 截断保留帧数action_frames=action_frames[:keep_cfg[row]]self.all_actions.append(action_frames)

播放是QTimer驱动的两态逻辑:switch_action切动作并复位帧号;next_frame在"乒乓(PingPong)"模式下到边界反向,动作更自然(loop模式则直接取模循环):

defnext_frame(self):frames=self.all_actions[self.current_action_index]ifself.play_mode=='loop':# 循环模式self.current_frame_index=(self.current_frame_index+1)%len(frames)returnnext_idx=self.current_frame_index+self.direction# 乒乓模式ifnext_idx<0ornext_idx>=len(frames):# 到边界折返self.direction*=-1next_idx=self.current_frame_index+self.direction self.current_frame_index=next_idx

3.2 声音引擎:场景 → 文件映射,多通道播放

语音文件放在voice_tmp/,命名约定XX-场景序号.wav。启动时扫描目录,按文件名前缀建立场景(Scene)映射

SCENE_ALIASES={"消息":"msg","提醒":"remind","闹钟":"alarm","彩蛋":"easter","任务完成":"done",}

新增语音零代码改动:丢个XX-任务完成.wav进去就自动归入done场景。播放时从场景池里随机选、尽量不与上次重复,并维护多个QMediaPlayer通道实现叠加播放——提醒音不会打断消息音:

defplay_random(self,key):files=self._scene_files.get(key,[])ifnotfiles:returnNone# 场景无语音 → 静默降级pool=[fforfinfilesiff!=self._last_played.get(key)]orfiles chosen=random.choice(pool)self._last_played[key]=chosen self.play(chosen)

3.3 核心难点:WASAPI 只读监听,实现"提示音接管"

这是全文最有含金量的一段。原理:pycaw 封装了 WASAPI,AudioUtilities.GetAllSessions()能拿到每个进程的音频会话,会话状态(State)在响一声时是Inactive → Active → Inactive

难点在于误报:微信播放语音消息、语音通话也会使会话变Active。解决思路是状态机 + 时长阈值 + 冷却:

  • 会话变Active记下时间戳;
  • 若持续超过阈值(默认 2s)→ 判定为"长播放"(语音/通话),本次忽略;
  • 若在阈值内回到Inactive→ 是一次短提示音,触发接管;
  • 同一会话触发后进入冷却(默认 3s),避免连续提示音重复连响。
def_update(self,name,state,session,now):tr=self._track.setdefault(name,{"active_since":None,"decided":False,"last_trigger":0.0})ifstate==_SESSION_ACTIVE:iftr["active_since"]isNone:# 提示音响起的瞬间tr["active_since"]=now tr["decided"]=Falseelifnottr["decided"]andnow-tr["active_since"]>self.sensitivity:tr["decided"]=True# 超过阈值 → 长播放,忽略else:# Inactive → 一次播放结束since=tr["active_since"]ifsinceisnotNoneandnottr["decided"]\andnow-since<=self.sensitivity \andnow-tr["last_trigger"]>=self.cool_down_s:# 冷却防连响tr["last_trigger"]=now self._on_notify(name,session,now)# 通知主线程播自定义语音tr["active_since"]=Nonetr["decided"]=False

接管有两种模式

模式行为
叠加(Overlay)保留微信原声,在其上额外播自定义语音(默认)
软替换(Silent)触发瞬间把该会话音量MasterVolume临时置 0,播完恢复——不碰文件也能"只响自定义音"
def_mute(self,name,session,now):# 软替换:临时静音该应用session.SimpleAudioVolume.MasterVolume=0.0self._silenced[name]=(session,now+self._mute_seconds)def_restore(self,name):# 播完恢复音量volume=self._silenced.pop(name)[0].SimpleAudioVolume volume.MasterVolume=1.0

选 pycaw 而不用 ctypes 强转 COM 指针,是因为 pycaw 内部管理 COM 生命周期,能避免常见的_ctypes.pyd崩溃(这是踩过坑换来的经验)。

3.4 定时提醒与日程:调度线程 + SQLite

提醒和日程存 SQLite(data.db),SchedulerManager线程每秒查一次到期项,命中就发remind_signal

classSchedulerManager(QThread):remind_signal=pyqtSignal(dict)# {"title":..., "sound_key":..., "time":...}defrun(self):whileself._running:self._check(datetime.now())time.sleep(1.0)def_check(self,now):forrinself.db.due_reminders(now):# 查已到期且未触发的项self.remind_signal.emit({"kind":"reminder","title":r["title"],"time":r["start_time"],"sound_key":"alarm"})self._advance_reminder(r,now)# 触发后推进下一次(每日/每周)

主线程收到信号后播remind场景语音、弹提醒卡片、切打招呼动作。提醒支持一次性 / 每日 / 每周,日程有独立日历视图(CalendarView)管理,这里不展开。

3.5 OpenCode 联动:内嵌 FastAPI,信号桥接线程

配置提示

opencode 用opencode-im-notifier插件把任务完成消息推送到 HTTP 端点,桌宠内嵌一个 FastAPI 服务来接。在 opencode-im-notifier 中根据提示配置好文件,将飞书的webhook地址替换成后端的地址(默认http://127.0.0.1:8992/hook)或者见下面的代码复制到~/.config/opencode/opencode-im-notifier.json中:

{"dingtalk":{"enable":false},"feishu":{"enable":true,"webhook":"http://127.0.0.1:8992/hook"},"wecom":{"enable":false,"webhook":"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx"},"notifyOn":["idle","permission","question","error"],"quietHours":{"start":"22:00","end":"08:00"},"title":"我的项目"}
关键实现

OpenCodeHook(QThread)在独立线程里跑uvicorn.Server.run()(自带事件循环,不阻塞 Qt),收到POST /hook后解析消息,通过 Qt 信号交回主线程:

classOpenCodeHook(QThread):hook_signal=pyqtSignal(dict)# 线程 → 主线程的桥defrun(self):app=build_app(self._handle_payload)# POST /hook 端点server=uvicorn.Server(uvicorn.Config(app,host=self.host,port=self.port,log_level="warning"))server.run()def_handle_payload(self,payload):parsed=parse_opencode_payload(payload)# 从卡片消息里提取标题/正文self.hook_signal.emit(parsed)

主线程收到信号就弹完成卡片、播done场景语音(如:银月-任务完成.wav)、切打招呼动作:

def_on_opencode_hook(self,parsed):self.sound.play_random("done")# 任务完成语音self.card.show_card(parsed["title"],subtitle=parsed["body"],anchor=self)idx=self._find_action("打招呼1")orself._find_action("打招呼2")ifidxisnotNone:self.play_action(idx,notify=False)

细节:fastapi/uvicorn惰性导入——没装依赖时线程直接退出并提示,不影响桌宠本体;端口被占用(如调试用的test.py仍开着)也会优雅降级。这是"低侵入"设计哲学的延续。

四、项目结构速览

VoxNotify/ ├── main.py # 入口 ├── floating_ball.py # 桌宠主体:动画/交互/气泡/提醒 ├── audio_watcher.py # WASAPI 音频监听(核心:语音接管) ├── sound_engine.py # 声音引擎:场景映射 + 多通道播放 ├── opencode_hook.py # OpenCode 联动:内嵌 FastAPI ├── schedule_manager.py # 定时调度线程 ├── reminder_dialog.py # 定时提醒 UI ├── calendar_view.py # 日程日历 UI ├── database.py # SQLite 数据层 ├── bubble_widget.py # 气泡 / 提醒卡片(无边框置顶) ├── config.py + config.json ├── logger.py # 统一日志(轮转文件) ├── img/ # 精灵图 spt.webp + 帧配置 spt.txt └── voice_tmp/*.wav # 场景语音(消息/提醒/彩蛋/任务完成)

项目源码地址

五、总结

回头看,这个桌宠在我这里已经不再是一个"挂件"——会提醒、会恭喜、会在角落陪着你干活,真正嵌进了日常的工作流。如果你也在做类似的桌宠 / 通知工具,希望我的实现思路,能给你一定的灵感。

后续可做的方向:打包成单 exe(PyInstaller)、声音文件按主题分类、精灵图增加更多动作帧。增加精灵图和声音替换的用户体验等等,这只是我的一个想法的初步样子,后续还有待探索。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/16 2:55:14

GitHub 双仓库静态部署完整配置手册(适配你的项目)

✨个人主页&#xff1a;编程的一拳超人 ⛺️ 欢迎关注&#xff1a;&#x1f44d;点赞 &#x1f4e2;留言 &#x1f60d;收藏 于高山之巅&#xff0c;方见大河奔涌&#xff1b;于群峰之上&#xff0c;更觉长风浩荡。 GitHub 双仓库静态部署完整配置手册&#xff08;适配你的项目…

作者头像 李华
网站建设 2026/8/16 2:52:53

老人独居看护摄像头怎么选?跌倒检测+一键呼叫,让牵挂落地的技术方案

一、痛点提出&#xff1a;独居老人最怕摔倒没人知道独居老人最怕的就是摔倒没人知道。跌倒已成为老年人意外伤害的首要原因&#xff0c;而“摔倒后无人知晓”是导致严重后果的关键因素。普通摄像头只能事后翻回放&#xff0c;无法在第一时间发现问题。中国电信在云南普洱市落地…

作者头像 李华
网站建设 2026/8/16 2:50:27

C语言浮点数内存表示与IEEE 754标准详解:从原理到实战排查

1. 项目概述&#xff1a;从“看不懂”的十六进制到程序崩溃的元凶如果你写过C语言&#xff0c;大概率遇到过这样的场景&#xff1a;你定义了一个float a 0.1;&#xff0c;然后写了个循环for (; a < 1.0; a 0.1)&#xff0c;满心期待它能循环10次&#xff0c;结果它可能循环…

作者头像 李华
网站建设 2026/8/16 2:49:08

高并发下MySQL数据安全:悲观锁、乐观锁与队列化方案实战解析

1. 项目概述&#xff1a;高并发下的数据安全困境做后端开发或者数据库运维的朋友&#xff0c;肯定都遇到过这样的场景&#xff1a;一个热门商品秒杀&#xff0c;或者一个关键配置项被多个服务节点同时更新&#xff0c;数据库里同一行数据在极短时间内被多次修改。这时候&#x…

作者头像 李华
网站建设 2026/8/16 2:47:48

Vibe Coding 实战指南:构建 AI 辅助开发工作流,提升编程效率

最近在尝试将 AI 融入日常开发流程时&#xff0c;发现了一个非常有趣且高效的新范式——Vibe Coding。这个概念由吴恩达教授&#xff08;Andrew Ng&#xff09;在 DeepLearning.AI 的课程中提出&#xff0c;它并非一个具体的工具&#xff0c;而是一种全新的开发理念和工作流。简…

作者头像 李华
网站建设 2026/8/16 2:42:59

构建AI驱动的自动化运维系统:从根因定位到智能决策

1. 这篇文章真正要解决的问题你是否经历过这样的深夜&#xff1f;线上服务突然告警&#xff0c;日志量瞬间暴涨&#xff0c;你需要在海量的错误信息、监控指标和链路追踪数据中&#xff0c;像侦探一样寻找那个导致系统崩溃的“元凶”。这个过程耗时耗力&#xff0c;高度依赖工程…

作者头像 李华