← 文章 / AI技术
cevior 7小时前 · 2026-09-26 10:39:50 · 6 阅读

告别切换!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 sys
import time
import ctypes
import pyperclip
from PySide6.QtCore import Qt, QThread, Signal, QPoint
from PySide6.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QHBoxLayout, 
    QTextEdit, QPushButton, QCheckBox, QLabel
)
from zhipuai import ZhipuAI

# -------------------------------------------------------------------
# 1. Win32 API 底层模拟与虚拟键码定义
# -------------------------------------------------------------------
VK_CONTROL = 0x11
VK_KEY_C = 0x43
VK_KEY_V = 0x56
KEYEVENTF_KEYUP = 0x0002

def send_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)

def send_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 线程
# -------------------------------------------------------------------
class StreamLLMThread(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_content

    def run(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 悬浮窗主界面
# -------------------------------------------------------------------
class AIFloatingWindow(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()

    def init_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)

    def mousePressEvent(self, event):
        if event.button() == Qt.LeftButton:
            self.drag_position = event.globalPosition().toPoint() - self.frameGeometry().topLeft()

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

    def capture_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("状态: 文本载入成功")

    def execute_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()

    def on_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)

    def replace_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()

    def clear_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,开启真正的极致生产力!运行效果可看本视频号!


原始来源: cevior

评论 (0)