夜雨聆风学习资料网

ARTICLE · 1078936

告别切换!Python 操控AI大模型,打造桌面级“悬浮 AI”助手(软件运行效果看视频号)

告别切换!Python 操控AI大模型,打造桌面级“悬浮 AI”助手(软件运行效果看视频号)
⚡ 

深度工作流重构:频繁在 Word/IDE 与浏览器 AI 网页之间复制粘贴,正在严重打断你的专注力。本文带你用 Python + PySide6 打通 Win32 消息管道,实现系统级选中文本一键捕获、SSE 流式渲染与原位零延迟文本覆写。


💡 为什么你需要桌面级“悬浮 AI”?

在日常开发、文档撰写与长文阅读中,传统的 AI 交互需要经历 8 步繁琐切屏。而基于 Win32 底层管道打造的桌面助手,能将其压缩至极致:

维度对比
传统 Web 端 AI 交互
桌面级悬浮 AI 助手
操作链路
8 步(复制 ➔ 切屏 ➔ 粘贴 ➔ 切回...)
2 步(选中文本 ➔ 原位唤醒替换)
专注度
频繁打断,上下文体验割裂
原位沉浸,无缝衔接当前工作
渲染体验
标签页易淹没,加载僵硬
极简暗黑卡片 + SSE 毫秒级打字机
通用性
依赖特定浏览器插件
Win32 全局支持(Word / IDE / 浏览器通用)

🛠️ 系统全景架构设计

项目采用 分层解耦架构,确保云端 LLM 的流式 HTTP 请求不会阻塞主界面,实现毫秒级响应。

1️⃣ Win32 捕获层 ➔ 发送 Ctrl+C 指令,零延迟读取系统剪贴板文本管道2️⃣ PySide6 交互层 ➔ 唤醒无边框悬浮窗,展示深色极简 UI 界面3️⃣ QThread 调度层 ➔ 后台异步请求大模型 API,基于 Qt Signal 进行 Token 级推流4️⃣ Win32 写回层 ➔ 自动交接焦点,模拟 Ctrl+V 将 AI 结果原位覆写


🔑 核心技术拆解:“原地替换”时序控制

“原地替换”的最核心难点在于:当用户点击悬浮窗上的按钮时,Windows 的焦点在悬浮窗本身。如果直接粘贴,内容会错发到悬浮窗内部。

必须通过精确的硬件级时序控制来恢复焦点:

Step 1: pyperclip.copy(ai_text)      ➔ 将 AI 结果写入系统剪贴板Step 2: self.hide()                   ➔ 隐藏悬浮窗,释放系统焦点Step 3: time.sleep(0.08)              ➔ 线程让步,等待 Windows 交接 HWND 句柄Step 4: send_key_combo_paste()        ➔ 模拟全局 Ctrl+V,覆盖高亮目标文本

💻 生产级 Python 完整源代码

下面是包含底层 Win32 按键管道、QThread 异步 SSE 线程与 UI 交互的完整源码:

import sysimport timeimport ctypesimport pyperclipfrom PySide6.QtCore import Qt, QThread, Signal, QPointfrom PySide6.QtWidgets import (    QApplication, QWidget, QVBoxLayout, QHBoxLayout,     QTextEdit, QPushButton, QCheckBox, QLabel)from zhipuai import ZhipuAI# -------------------------------------------------------------------# 1. Win32 API 底层模拟与虚拟键码定义# -------------------------------------------------------------------VK_CONTROL = 0x11VK_KEY_C = 0x43VK_KEY_V = 0x56KEYEVENTF_KEYUP = 0x0002defsend_key_combo_copy():"""模拟底层硬件按键: Ctrl + C"""    ctypes.windll.user32.keybd_event(VK_CONTROL, 0, 0, 0)    ctypes.windll.user32.keybd_event(VK_KEY_C, 0, 0, 0)    time.sleep(0.05)    ctypes.windll.user32.keybd_event(VK_KEY_C, 0, KEYEVENTF_KEYUP, 0)    ctypes.windll.user32.keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYUP, 0)defsend_key_combo_paste():"""模拟底层硬件按键: Ctrl + V"""    ctypes.windll.user32.keybd_event(VK_CONTROL, 0, 0, 0)    ctypes.windll.user32.keybd_event(VK_KEY_V, 0, 0, 0)    time.sleep(0.05)    ctypes.windll.user32.keybd_event(VK_KEY_V, 0, KEYEVENTF_KEYUP, 0)    ctypes.windll.user32.keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYUP, 0)# -------------------------------------------------------------------# 2. LLM 异步流式 Worker 线程# -------------------------------------------------------------------classStreamLLMThread(QThread):    token_stream = Signal(str)    stream_finished = Signal()def__init__(self, api_key, system_prompt, user_content):        super().__init__()        self.api_key = api_key        self.system_prompt = system_prompt        self.user_content = user_contentdefrun(self):try:            client = ZhipuAI(api_key=self.api_key)            response = client.chat.completions.create(                model="glm-4-flash",  # 可无缝替换为 deepseek-chat                messages=[                    {"role": "system", "content": self.system_prompt},                    {"role": "user", "content": self.user_content}                ],                stream=True            )for chunk in response:if chunk.choices[0].delta.content:                    self.token_stream.emit(chunk.choices[0].delta.content)            self.stream_finished.emit()except Exception as e:            self.token_stream.emit(f"\n[错误]: {str(e)}")            self.stream_finished.emit()# -------------------------------------------------------------------# 3. 桌面 AI 悬浮窗主界面# -------------------------------------------------------------------classAIFloatingWindow(QWidget):def__init__(self):        super().__init__()        self.api_key = "YOUR_API_KEY_HERE"# 替换为你的 API Key        self.drag_position = QPoint()        self.last_ai_response = ""        self.init_ui()definit_ui(self):        self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool)        self.setAttribute(Qt.WA_TranslucentBackground)        self.resize(560, 390)# 现代科技暗黑 QSS 注入        self.setStyleSheet("""            QWidget#MainContainer {                background-color: #1E1E24;                border: 1px solid #3A3A45;                border-radius: 12px;            }            QLabel {                color: #A0A0B0;                font-size: 12px;            }            QTextEdit {                background-color: #141419;                color: #E5E7EB;                border: 1px solid #2A2A35;                border-radius: 8px;                padding: 10px;                font-size: 13px;            }            QPushButton {                background-color: #2D2D38;                color: #E0E0E0;                border: None;                border-radius: 6px;                padding: 6px 12px;                font-weight: bold;            }            QPushButton:hover { background-color: #3E3E4D; }            QPushButton#PrimaryBtn { background-color: #2563EB; color: #FFFFFF; }            QPushButton#PrimaryBtn:hover { background-color: #1D4ED8; }            QPushButton#ActionBtn { background-color: #6366F1; color: #FFFFFF; }            QPushButton#ActionBtn:hover { background-color: #4F46E5; }        """)        main_layout = QVBoxLayout(self)        main_layout.setContentsMargins(0, 0, 0, 0)        container = QWidget()        container.setObjectName("MainContainer")        container_layout = QVBoxLayout(container)        container_layout.setContentsMargins(14, 14, 14, 14)# 顶部快捷指令栏        top_bar = QHBoxLayout()        self.btn_polish = QPushButton("✨ 原地润色")        self.btn_summary = QPushButton("📝 3点总结")        self.btn_translate = QPushButton("🌐 中英互译")        self.btn_fetch = QPushButton("🔄 载入选中")        self.btn_polish.setObjectName("ActionBtn")        self.btn_summary.setObjectName("ActionBtn")        self.btn_translate.setObjectName("ActionBtn")        top_bar.addWidget(self.btn_polish)        top_bar.addWidget(self.btn_summary)        top_bar.addWidget(self.btn_translate)        top_bar.addWidget(self.btn_fetch)# 中央交互编辑框        self.text_editor = QTextEdit()        self.text_editor.setPlaceholderText("在外部选中文字后点击【载入选中】...")        self.status_label = QLabel("状态: 准备就绪")# 底部控制栏        bottom_bar = QHBoxLayout()        self.chk_autostart = QCheckBox("开机自启")        self.btn_clear = QPushButton("🧹 清空")        self.btn_submit = QPushButton("⚡ AI 快答")        self.btn_replace = QPushButton("📋 原地替换")        self.btn_submit.setObjectName("PrimaryBtn")        self.btn_replace.setObjectName("PrimaryBtn")        bottom_bar.addWidget(self.chk_autostart)        bottom_bar.addWidget(self.btn_clear)        bottom_bar.addStretch()        bottom_bar.addWidget(self.btn_submit)        bottom_bar.addWidget(self.btn_replace)        container_layout.addLayout(top_bar)        container_layout.addWidget(self.text_editor)        container_layout.addWidget(self.status_label)        container_layout.addLayout(bottom_bar)        main_layout.addWidget(container)# 绑定信号        self.btn_fetch.clicked.connect(self.capture_selected_text)        self.btn_clear.clicked.connect(self.clear_text)        self.btn_submit.clicked.connect(lambda: self.execute_prompt("请专业且简练地回答:"))        self.btn_polish.clicked.connect(lambda: self.execute_prompt("请对以下文本进行润色与修改:"))        self.btn_summary.clicked.connect(lambda: self.execute_prompt("请用3个核心要点归纳总结:"))        self.btn_translate.clicked.connect(lambda: self.execute_prompt("中英互译以下内容:"))        self.btn_replace.clicked.connect(self.replace_selected_text)defmousePressEvent(self, event):if event.button() == Qt.LeftButton:            self.drag_position = event.globalPosition().toPoint() - self.frameGeometry().topLeft()defmouseMoveEvent(self, event):if event.buttons() == Qt.LeftButton:            self.move(event.globalPosition().toPoint() - self.drag_position)defcapture_selected_text(self):        self.status_label.setText("状态: 正在捕获选中内容...")        self.hide()        time.sleep(0.05)        send_key_combo_copy()        time.sleep(0.05)        captured_text = pyperclip.paste().strip()        self.show()        self.activateWindow()if captured_text:            self.text_editor.setText(f"【待处理文本】:\n{captured_text}\n\n")            self.status_label.setText("状态: 文本载入成功")defexecute_prompt(self, system_prompt):        user_text = self.text_editor.toPlainText().strip()ifnot user_text: return        self.last_ai_response = ""        self.text_editor.append(f"\n【AI 回复】:\n")        self.status_label.setText("状态: AI 思考中...")        self.worker = StreamLLMThread(self.api_key, system_prompt, user_text)        self.worker.token_stream.connect(self.on_token_received)        self.worker.stream_finished.connect(lambda: self.status_label.setText("状态: 生成完毕"))        self.worker.start()defon_token_received(self, token):        self.last_ai_response += token        cursor = self.text_editor.textCursor()        cursor.movePosition(cursor.MoveOperation.End)        cursor.insertText(token)        self.text_editor.setTextCursor(cursor)defreplace_selected_text(self):ifnot self.last_ai_response: return        pyperclip.copy(self.last_ai_response)        self.status_label.setText("状态: 正在原地替换...")        self.hide()        time.sleep(0.08)        send_key_combo_paste()defclear_text(self):        self.text_editor.clear()        self.last_ai_response = ""        self.status_label.setText("状态: 已清空")if __name__ == "__main__":    app = QApplication(sys.argv)    window = AIFloatingWindow()    window.show()    sys.exit(app.exec())

📦 打包与独立运行

使用 PyInstaller 即可将其打包为单文件绿色可执行程序(.exe):

pyinstaller --noconsole --onefile --clean --name="AIAssistant" main.py

🎯 总结:通过打通系统底层消息管道与 UI 渲染,我们实现了无缝的桌面级 AI 沉浸体验。可在此基础上进一步绑定全局热键 Alt+Q,开启真正的极致生产力!运行效果可看本视频号!

相关学习资料