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