项目地址:https://github.com/CanFlyhang/mindtrack-desk

一、为什么我需要一个「屏幕记忆助手」?

作为开发者、产品人、独立创作者,你是不是也有过这样的瞬间:

  • 一整天在电脑前忙忙碌碌,下班时却说不清「今天到底干了啥」;

  • 早上还在看一篇很重要的技术文章,下午想回顾时已经完全找不到网页;

  • 排查 Bug 一顿操作猛如虎,过两天再遇到同类问题,竟然连当时的操作路径都不记得了;

  • 想做周报 / 复盘,只能凭印象「脑补」工作内容。

我们的大脑擅长当下的「聚焦」,却很不擅长完整、客观地记录自己的工作轨迹。

而在 LLM 多模态能力越来越强的今天,一个显而易见的问题是:

 能不能让 AI 帮我自动记住「我在屏幕前都做了什么」,而且可以随时回放和检索?

带着这个问题,我做了一个小而美的桌面项目 —— MindTrack Desk · 灵迹桌面

这篇文章,想和你分享:

  • 我是怎么用 Python + PySide6 + 豆包多模态,实现一个「Windows 屏幕记忆助手」的;

  • 在这个过程中,我踩过哪些坑、做了哪些设计取舍;

- 更重要的是:这种「AI + 时间线」的方式,对我们的工作与思考可能意味着什么。

二、MindTrack Desk 是个什么东西?

一句话概括:

一个运行在 Windows 上的悬浮窗工具,自动监听当前活动窗口变化,截屏发给豆包多模态模型,总结你正在做的事情,并按时间线保存下来。

它做了几件很简单但组合起来很有意思的事:

1. 悬浮窗 UI

  • 半透明深色风格、圆角、无边框、可拖拽;

  • 始终置顶,可以随手点「开始监控 / 暂停监控」;

  • 支持一键打开「历史记录」窗口。

2. 窗口监听

  • 使用 pywin32 实时获取当前前台窗口句柄和标题;

  • 做了「防抖动」逻辑:只有在一个窗口稳定停留一段时间(例如 1.5 秒)才会触发;

  • 避免你在多个窗口来回切换时产生大量垃圾记录。

3. 自动截屏 + 多模态总结

  • 使用 mss 截取当前前台窗口区域,必要时退化为全屏截图;

  • 将图片压缩为 JPEG,转为 Base64;

  • 通过火山引擎 Ark Runtime 调用豆包多模态模型(如 doubao-seed-2-0-pro-260215 或 vision 模型),让模型总结当前屏幕内容。

4. 本地时间线记录

  • 使用 SQLite 记录:时间戳 / 窗口标题 / AI 总结 / 截图路径;

  • 截图保存到 logs/images/

  • 提供一个历史记录窗口,左侧列表 + 右侧大图 + 文本摘要,方便回顾。

你可以把它当成一个自动生成「工作流水账」的 AI 小伙伴


三、从想法到实现:整体架构一图流

项目整体架构很简单,也尽量保持「工程可读性」:

mindtrack-desk/
├── main.py               # 程序入口,初始化 Qt 应用与控制器
├── src/
│   ├── ui/
│   │   ├── floating_window.py  # 悬浮窗 UI
│   │   ├── history_window.py   # 历史记录窗口 UI
│   │   └── styles.qss          # 全局样式(深色主题)
│   ├── services/
│   │   ├── monitor.py          # WindowWatcher:监控当前活动窗口的 QThread
│   │   ├── capture.py          # ScreenCapture:截图 + Base64 编码
│   │   ├── ai_client.py        # ArkClient:调用豆包多模态 API
│   │   ├── storage.py          # DataManager:SQLite 存储
│   │   └── worker.py           # AnalysisWorker:截图 + 调用 AI + 落地的后台线程
│   └── app_controller.py       # AppController:业务中枢,串联 UI 与服务
└── logs/
    └── images/                 # 截图目录

用一句话概括:

> UI 做交互,服务做能力,控制器做编排。

下面我挑几个关键模块展开讲讲。

四、关键技术细节拆解

4.1 PySide6 悬浮窗:既要「酷」,又要好用

悬浮窗的定位是「安静地存在,但随时可用」。在 UI 设计上我做了几个小决定:

  • 无边框、圆角、深色 + 半透明背景;

  • 始终置顶,不会被其他窗口遮挡;

  • 支持拖拽,用户想放哪就放哪;

  • 只保留几个核心信息:当前状态 / 操作按钮 / 历史入口。

核心代码(部分)如下:

class FloatingWindow(QWidget):
    # 定义信号,供控制器连接
    start_monitoring_signal = Signal()  # 开始监控信号
    stop_monitoring_signal = Signal()   # 停止监控信号
    open_history_signal = Signal()      # 打开历史记录信号

    def __init__(self):
        super().__init__()
        self.is_monitoring = False  # 当前是否在监控
        self._init_ui()

    def _init_ui(self):
        # 去边框 + 置顶 + 工具窗
        self.setWindowFlags(
            Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
        )
        # 启用透明背景,用于自绘圆角
        self.setAttribute(Qt.WA_TranslucentBackground)
        self.resize(300, 150)

        # 省略布局代码...

    def toggle_monitoring(self):
        """切换监控状态"""
        self.is_monitoring = not self.is_monitoring
        if self.is_monitoring:
            self.start_monitoring_signal.emit()
        else:
            self.stop_monitoring_signal.emit()

    # 重写鼠标事件,实现窗口拖拽
    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self._drag_pos = event.globalPosition().toPoint() - self.frameGeometry().topLeft()

    def mouseMoveEvent(self, event):
        if event.buttons() & Qt.LeftButton:
            self.move(event.globalPosition().toPoint() - self._drag_pos)

样式统一放在 styles.qss 中,方便后续更换主题 / 做皮肤系统。


4.2 活动窗口监听:别太「敏感」

刚开始我天真地以为:

不就是 GetForegroundWindow 轮询一下就完了吗?

现实是:人会「来回切窗口」,还会「手抖点错再切回来」。如果不做防抖,几分钟就能刷出几十条日志,非常噪音。

最终我在 WindowWatcher 里做了三件事:

  1. 轮询当前前台窗口句柄;

  2. 如果句柄变化,开始计时;

  3. 只有保持稳定超过 debounce_seconds(默认 1.5s)才真正触发事件。

简化后的逻辑:

class WindowWatcher(QThread):
    # 发出句柄和标题
    window_changed = Signal(int, str)

    def __init__(self, debounce_seconds=1.5):
        super().__init__()
        self._running = False
        self._debounce_seconds = debounce_seconds
        self._last_hwnd = 0
        self._pending_hwnd = 0
        self._pending_start = 0.0

    def run(self):
        self._running = True
        while self._running:
            try:
                hwnd = win32gui.GetForegroundWindow()
                # 省略防抖逻辑实现...
            except Exception as e:
                print(f"WindowWatcher 异常: {e}")
            time.sleep(0.2)  # 轮询间隔

    def stop(self):
        self._running = False
        self.wait()  # 等待线程退出

为了避免程序退出时出现 QThread: Destroyed while thread is still running 的经典警告,我在 AppController 里统一做了线程清理(包括监控线程和工作线程),保证优雅关闭。


4.3 截图与多屏适配:mss 的小坑与小惊喜

截图我选了 mss,原因很简单:

  • 跨平台、性能不错;

  • 对多屏支持较好,可以直接拿到各个显示器的坐标。

在本项目里,一般用的是「按窗口坐标截取」:

class ScreenCapture:
    def capture_active_window(self, hwnd):
        """根据窗口句柄截取当前窗口区域"""
        try:
            left, top, right, bottom = win32gui.GetWindowRect(hwnd)
            width = right - left
            height = bottom - top
            if width <= 0 or height <= 0:
                return self.capture_full_screen()

            with mss.mss() as sct:
                monitor = {
                    "top": top,
                    "left": left,
                    "width": width,
                    "height": height,
                }
                sct_img = sct.grab(monitor)
                img = Image.frombytes(
                    "RGB",
                    sct_img.size,
                    sct_img.bgra,
                    "raw",
                    "BGRX",
                )
                return img
        except Exception as e:
            print(f"窗口截图失败,使用全屏截图兜底: {e}")
            return self.capture_full_screen()

这里特意留了一个兜底策略:窗口截取失败时会退化为全屏截图,避免因为某些奇怪窗口导致整个流程中断。


4.4 调用豆包多模态:请求设计与错误兜底

Ark 客户端我采用官方推荐的 Ark Runtime SDK,核心关注点有两个:

1. 不要把 API Key 写死在代码里:通过 `.env` + `python-dotenv` 管理;

2. 确保没配 Key 时不会直接程序崩溃:返回友好的错误提示。

简化版的 ArkClient 如下:

class ArkClient:
    def __init__(self):
        load_dotenv()  # 加载 .env 文件
        self._api_key = os.getenv("ARK_API_KEY")
        self._model = os.getenv("ARK_MODEL_NAME", "doubao-seed-2-0-pro-260215")
        self._base_url = "https://ark.cn-beijing.volces.com/api/v3"
        self._client = None

        if self._api_key:
            # 有 Key 时才初始化客户端
            self._client = Ark(
                base_url=self._base_url,
                api_key=self._api_key,
            )
        else:
            print("警告:未配置 ARK_API_KEY,AI 分析功能将不可用。")

    def analyze_image(self, base64_image: str, prompt: str) -> str:
        """调用多模态模型分析图片内容"""
        if not self._client:
            return "错误:未配置 ARK_API_KEY,无法调用多模态模型。"

        try:
            # 这里根据 Ark / 豆包具体接口形态构造请求
            # 示例:以图文混合消息的形式发送
            resp = self._client.chat.completions.create(
                model=self._model,
                messages=[
                    {
                        "role": "user",
                        "content": [
                            {"type": "text", "text": prompt},
                            {
                                "type": "image_url",
                                "image_url": {
                                    "url": f"data:image/jpeg;base64,{base64_image}"
                                },
                            },
                        ],
                    }
                ],
            )
            return resp.choices[0].message.content
        except Exception as e:
            print(f"调用豆包多模态失败: {e}")
            return f"分析失败:{e}"

4.5 本地存储与历史回放:数字记忆的一块「硬盘」

这里刻意没引入复杂的 ORM,而是用最朴素的 SQLite:

class DataManager:
    def __init__(self, db_path="logs/history.db"):
        self._db_path = db_path
        self._init_db()

    def _init_db(self):
        """初始化数据库表结构"""
        conn = sqlite3.connect(self._db_path)
        cursor = conn.cursor()
        cursor.execute(
            """
            CREATE TABLE IF NOT EXISTS history (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                timestamp TEXT,
                window_title TEXT,
                summary TEXT,
                image_path TEXT
            )
            """
        )
        conn.commit()
        conn.close()

历史记录窗口 HistoryWindow 用的是非常直观的布局:

  • 左侧 QListWidget 做时间线列表;

  • 右侧上方显示截图、下方显示 AI 总结;

  • 支持滚动、文本选择复制。

这块的实现对 PySide6 初学者也比较友好,推荐你直接看源码体验一下。


五、这个小项目想引发哪些思考?

写这个项目的时候,我一直在想几个问题,也想抛出来看看有没有人有共鸣:

5.1 数字时代的「第二大脑」,需要什么形态?

现在很多人都在用:

  • Obsidian / Notion / 思源 / Logseq / 飞书文档……

手动写笔记、整理知识体系。

这些工具的前提是:我得愿意思考 &愿意记录。

但很多时候,我们只是想:

「我不想自己记录,但希望有一份还算靠谱的『工作轨迹』备份在那里,必要时能翻出来。」

MindTrack Desk 想做的,就是这一块。

它记录的不是「你思考的结论」,而是:

  • 你看过哪些页面;

  • 你写过哪些代码;

  • 你打开过哪些工具;

  • 你在某一段时间里,大概在干什么。

这是一个更底层的「行为时间线」,可能是未来各种第二大脑系统的重要输入。

5.2 多模态 LLM 能不能变成一种「被动记录器」?

传统的时间追踪工具(比如 Toggl、RescueTime)更多是统计「在哪个软件上花了多久时间」。

而多模态 LLM 可以做的事情是:

  • 不仅知道你在「Chrome」里待了 30 分钟;

  • 还可以知道你在看「某篇论文 / 某篇文档 / 某段代码」;

  • 甚至可以帮你做:

    • 摘要;

    • 任务推断(例如「你在修一个后端接口 Bug」);

    • TODO 抽取(例如「似乎还有两个 TODO 没完成」)。

MindTrack Desk 只是一个非常粗糙的第一步:把屏幕画面送给模型,让模型自己说说「你在干什么」。

后面完全可以进一步探索:

  • 结合键鼠事件、窗口标题、进程名做更精准的任务识别;

  • 把一天的屏幕事件聚合成一个「日记摘要」;

  • 回放一段时间线时,自动提示你「这里曾经解决过一个 Bug,要不要转成知识库条目?」。

5.3 隐私与边界:什么不该被记录?

任何涉及自动截屏的项目,都绕不开隐私问题。

所以在设计 MindTrack Desk 的时候,我刻意做了几个限制与预留

- 所有截图 & 结构化数据 仅保存在本地,不主动上传任何第三方存储;

  • 只有在调用多模态 API 时,当前这张图会发送到豆包服务端做一次推理;

  • 未来可以增加:

    • 黑名单应用(例如密码管理器、网银、隐私聊天软件等);

    • 局部涂抹 / 模糊敏感区域;

    • 一键清空某个时间段的所有记录。

这块其实很值得认真设计:

什么样的数据是对自己有帮助的?什么又是你不希望任何系统触碰的?

欢迎在 Issue / 评论区一起讨论。


六、如何参与共建?

项目地址:

Github地址点击进入

你可以从这些方向入手:

- 🎨 UI / UX 改进

  • 更酷的主题 / 动画效果;

  • 桌面小挂件模式;

  • 支持不同布局的历史回放。

- 🧠 多模态能力增强

  • 针对不同场景设计不同 Prompt(写代码 / 看文档 / 刷 B 站);

  • 为某些特定软件做「定制化总结模板」;

  • 引入更多模型后端,做「多模型路由」。

- 🛡 隐私与控制

  • 黑名单窗口(不截图 / 不记录);

  • 敏感字段自动模糊处理;

  • 数据导出 / 加密存储。

- 📈 知识化与时间线重构

  • 把每天的记录聚合成「日报 / 周报」;

  • 和 Obsidian / Logseq 等知识库做联动;

  • 做成一个「AI 驱动的时光轴」。

非常欢迎:

  • 提 Issue 讨论新的想法;

  • 提 PR 一起迭代代码;

  • Fork 后玩出自己的一套改造版本。


七、写在最后

MindTrack Desk 只是一个非常早期的探索版本,它没有解决所有问题,甚至还有不少粗糙的地方。

但我很喜欢它的出发点:

在 AI 时代,让机器帮我们记住那些「容易被遗忘的过程」,

然后把有限的注意力,留给真正值得思考的事情。

如果你也对这种「屏幕记忆 + 多模态理解 + 时间线回放」的方向感兴趣,

欢迎来 GitHub 或 CSDN 找我,一起把这个小项目打磨得更有趣一点。

也欢迎你顺手点一个 Star ✨,

让更多人看到这个关于「数字记忆」的小实验。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐