chore: import original project baseline

Import the pre-repair source tree as the history baseline.
Runtime data (data/), virtualenvs, bytecode caches and logs are
gitignored so local secrets and user state stay out of the repo.
This commit is contained in:
2026-09-17 16:40:01 +08:00
commit a7412824e0
124 changed files with 26747 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
"""
系统级工具集合 (system_tools)
- screen_capture: 屏幕截图覆盖层
- global_hotkey: 全局热键监听线程
- file_reader: 文本/代码文件读取(编码探测 + 二进制探测)
"""
+71
View File
@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
"""
文本/代码文件读取工具 (file_reader)
编码探测 + 二进制探测 + 大小守卫,供附件系统(拖拽/粘贴/上传文本与代码文件)复用。
用法:read_text_file(path) -> (content, encoding, size_kb, lines),失败抛 ValueError。
说明:纯函数模块,不依赖 Qt,便于单元测试。
"""
import os
# 明确拒绝的二进制/文档格式(Word/Excel/PPT/PDF/压缩包/可执行/媒体等)
BINARY_EXTS = frozenset({
# Office 文档(明确排除 Word/Excel
'.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
'.odt', '.ods', '.odp', '.rtf',
# 电子书(.pdf 已交由 tools/builtin_tools/pdf_reader.py 专门解析,不在此拦截)
'.epub', '.mobi',
# 压缩包 / 磁盘镜像
'.zip', '.rar', '.7z', '.tar', '.gz', '.bz2', '.xz', '.iso', '.dmg',
# 可执行 / 编译产物
'.exe', '.dll', '.so', '.dylib', '.msi', '.bin', '.apk', '.jar',
'.class', '.pyc', '.pyd', '.o', '.a',
# 音视频
'.mp3', '.wav', '.flac', '.m4a', '.aac', '.ogg',
'.mp4', '.avi', '.mov', '.mkv', '.webm', '.wmv',
# 字体
'.ttf', '.otf', '.woff', '.woff2', '.eot',
# 数据库
'.db', '.sqlite', '.sqlite3', '.mdb',
# 设计稿
'.psd', '.ai', '.sketch', '.fig',
})
# 单文件大小上限(字节):超过则拒绝,避免内存与上下文爆炸
MAX_ATTACH_FILE_BYTES = 2 * 1024 * 1024 # 2 MB
# 编码降级链:UTF-8(含BOM) → GB18030(⊇GBK/GB2312) → latin-1(永不失败)
_ENCODINGS = ('utf-8-sig', 'gb18030', 'latin-1')
def read_text_file(path, max_bytes=MAX_ATTACH_FILE_BYTES):
"""读取一个文本/代码文件。
返回 (content, encoding, size_kb, lines)。
文件过大或判定为二进制时抛出 ValueError(附带可读原因)。
"""
size = os.path.getsize(path)
if size > max_bytes:
raise ValueError(
f"文件过大({size / 1024 / 1024:.1f} MB > {max_bytes / 1024 / 1024:.0f} MB"
)
with open(path, 'rb') as f:
raw = f.read()
# 二进制探测:前 8KB 含 NUL 字节即判定为二进制(git 同款启发式)
if b'\x00' in raw[:8192]:
raise ValueError("二进制文件,无法作为文本读取")
text, used_enc = None, 'utf-8'
for enc in _ENCODINGS:
try:
text = raw.decode(enc)
used_enc = enc
break
except UnicodeDecodeError:
continue
if text is None: # 理论上 latin-1 兜底永不失败
raise ValueError("无法识别文件编码")
lines = text.count('\n') + 1
return text, used_enc, round(size / 1024, 2), lines
+105
View File
@@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
"""
全局热键监听器 (Windows)
独立线程 + Win32 RegisterHotKey 消息循环,实现主窗口失焦也能触发的系统级全局快捷键。
用法:
hotkey = GlobalHotkeyThread() # 默认 Alt+S
hotkey.triggered.connect(on_triggered) # 跨线程信号,槽函数在主线程执行
hotkey.start() # 启动线程
hotkey.stop() # 注销热键并退出线程
"""
import ctypes
import sys
import threading
from ctypes import wintypes
from PyQt6 import QtCore
# Win32 常量
WM_HOTKEY = 0x0312
WM_QUIT = 0x0012
MOD_ALT = 0x0001
MOD_CONTROL = 0x0002
MOD_SHIFT = 0x0004
MOD_NOREPEAT = 0x4000 # 按住不重复触发 (Windows 7+)
VK_S = 0x53
_is_windows = sys.platform == "win32"
if _is_windows:
_user32 = ctypes.windll.user32
_kernel32 = ctypes.windll.kernel32
# 显式声明函数签名,避免 ctypes 默认 int 截断指针/句柄
_user32.RegisterHotKey.argtypes = [wintypes.HWND, ctypes.c_int, wintypes.UINT, wintypes.UINT]
_user32.RegisterHotKey.restype = wintypes.BOOL
_user32.UnregisterHotKey.argtypes = [wintypes.HWND, ctypes.c_int]
_user32.UnregisterHotKey.restype = wintypes.BOOL
_user32.GetMessageW.argtypes = [
ctypes.POINTER(wintypes.MSG), wintypes.HWND, wintypes.UINT, wintypes.UINT
]
_user32.GetMessageW.restype = wintypes.BOOL # >0 正常 / 0 WM_QUIT / -1 出错
_user32.PostThreadMessageW.argtypes = [
wintypes.DWORD, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM
]
_user32.PostThreadMessageW.restype = wintypes.BOOL
_kernel32.GetCurrentThreadId.restype = wintypes.DWORD
class GlobalHotkeyThread(QtCore.QThread):
"""在独立线程注册系统级全局热键并运行 Win32 消息循环。
收到 WM_HOTKEY 后通过 Qt 信号 triggered 通知主线程,
信号槽机制保证槽函数在主线程执行,操作 Qt 控件安全。
"""
triggered = QtCore.pyqtSignal()
def __init__(self, hotkey_id=9001, mod=MOD_ALT, vk=VK_S, parent=None):
super().__init__(parent)
self._hotkey_id = hotkey_id
self._mod = mod
self._vk = vk
self._thread_id = 0
self._ready = threading.Event() # run() 记录线程 ID 后置位
self._registered = False
def run(self):
"""线程入口:注册热键 -> 消息循环 -> 退出时注销"""
if not _is_windows:
self._ready.set()
return
self._thread_id = _kernel32.GetCurrentThreadId()
ok = _user32.RegisterHotKey(
None, self._hotkey_id, self._mod | MOD_NOREPEAT, self._vk
)
self._registered = bool(ok)
self._ready.set()
if not ok:
print(
f"[GlobalHotkey] 注册热键失败 (id={self._hotkey_id}, mod={self._mod:#x}, "
f"vk={self._vk:#x}),可能已被其他程序占用"
)
return
# Win32 消息循环:hwnd=None 取本线程所有消息
msg = wintypes.MSG()
while _user32.GetMessageW(ctypes.byref(msg), None, 0, 0) > 0:
if msg.message == WM_HOTKEY and msg.wParam == self._hotkey_id:
self.triggered.emit()
# 收到 WM_QUIT 退出循环 -> 注销热键
_user32.UnregisterHotKey(None, self._hotkey_id)
self._registered = False
def stop(self):
"""请求线程退出:向线程消息队列投递 WM_QUIT,然后等待结束"""
if not _is_windows:
return
# 等 run() 至少记录好线程 ID(注册成功或失败都行)
self._ready.wait(timeout=2.0)
if self._thread_id:
_user32.PostThreadMessageW(self._thread_id, WM_QUIT, 0, 0)
self.wait(2000)
+193
View File
@@ -0,0 +1,193 @@
# -*- coding: utf-8 -*-
"""
屏幕截图覆盖层 (ScreenCaptureOverlay)
全屏半透明遮罩 + 鼠标拖拽选区 + 确认/取消工具条 + 截图完成发射 QImage 信号
用法:调用 start() 启动截图,监听 screenshot_captured 信号获取结果
"""
from PyQt6 import QtCore, QtGui, QtWidgets
import os
class ScreenCaptureOverlay(QtWidgets.QWidget):
"""全屏截图覆盖层:半透明遮罩 + 鼠标拖拽选区 + 确认/取消按钮"""
screenshot_captured = QtCore.pyqtSignal(QtGui.QImage)
# 按钮尺寸
BTN_W = 36
BTN_H = 30
BTN_GAP = 4
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowFlags(
QtCore.Qt.WindowType.FramelessWindowHint
| QtCore.Qt.WindowType.WindowStaysOnTopHint
| QtCore.Qt.WindowType.Tool
)
self.setCursor(QtCore.Qt.CursorShape.CrossCursor)
self._full_pixmap = None
self._start_pos = QtCore.QPoint()
self._end_pos = QtCore.QPoint()
self._is_drawing = False
self._has_selection = False
self._current_rect = QtCore.QRect()
# SVG 图标路径(项目根目录下 svg/ 文件夹)
root_dir = os.path.abspath(
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "..")
)
# 确认 / 取消按钮(使用 SVG 图标,灰白简约风背景)
self._btn_confirm = QtWidgets.QPushButton(self)
self._btn_cancel = QtWidgets.QPushButton(self)
self._btn_confirm.setIcon(QtGui.QIcon(os.path.join(root_dir, "svg", "check.svg")))
self._btn_cancel.setIcon(QtGui.QIcon(os.path.join(root_dir, "svg", "cross.svg")))
btn_style = (
"QPushButton { background-color: #f9f9f9; border: 1px solid #ddd; border-radius: 4px; }"
"QPushButton:hover { background-color: #eee; border-color: #ccc; }"
)
for btn in (self._btn_confirm, self._btn_cancel):
btn.setFixedSize(self.BTN_W, self.BTN_H)
btn.setIconSize(QtCore.QSize(20, 20))
btn.setCursor(QtCore.Qt.CursorShape.PointingHandCursor)
btn.setStyleSheet(btn_style)
btn.hide()
self._btn_confirm.clicked.connect(self._on_confirm)
self._btn_cancel.clicked.connect(self._on_cancel)
def start(self):
"""开始截图:抓取屏幕全图并显示覆盖层"""
screen = QtWidgets.QApplication.primaryScreen()
if not screen:
return
self._full_pixmap = screen.grabWindow(0)
self.setGeometry(screen.geometry())
self.show()
self.activateWindow()
self.raise_()
def paintEvent(self, event):
if not self._full_pixmap:
return
painter = QtGui.QPainter(self)
# 1. 绘制屏幕截图作为背景
painter.drawPixmap(0, 0, self._full_pixmap)
# 2. 半透明遮罩
painter.fillRect(self.rect(), QtGui.QColor(0, 0, 0, 100))
# 绘制中 或 已有选区 时,绘制选区高亮
if self._is_drawing or self._has_selection:
if self._is_drawing:
rect = QtCore.QRect(self._start_pos, self._end_pos).normalized()
else:
rect = self._current_rect
if rect.width() > 0 and rect.height() > 0:
# 3. 选区内重绘原图(去掉遮罩,形成高亮效果)
painter.drawPixmap(rect, self._full_pixmap, rect)
# 4. 蓝色边框
pen = QtGui.QPen(QtGui.QColor(0, 120, 215), 2)
painter.setPen(pen)
painter.setBrush(QtCore.Qt.BrushStyle.NoBrush)
painter.drawRect(rect)
# 5. 尺寸标注
size_text = f"{rect.width()} x {rect.height()}"
font = painter.font()
font.setPointSize(9)
painter.setFont(font)
fm = painter.fontMetrics()
text_w = fm.horizontalAdvance(size_text)
text_h = fm.height()
text_x = rect.x()
text_y = rect.y() - text_h - 2
if text_y < 0:
text_y = rect.bottom() + 2
painter.fillRect(text_x, text_y, text_w + 10, text_h, QtGui.QColor(0, 120, 215))
painter.setPen(QtGui.QColor(255, 255, 255))
painter.drawText(text_x + 5, text_y + fm.ascent(), size_text)
painter.end()
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.MouseButton.LeftButton:
# 开始新选区,隐藏按钮
self._hide_buttons()
self._has_selection = False
self._start_pos = event.position().toPoint()
self._end_pos = self._start_pos
self._is_drawing = True
self.update()
elif event.button() == QtCore.Qt.MouseButton.RightButton:
self.close()
def mouseMoveEvent(self, event):
if self._is_drawing:
self._end_pos = event.position().toPoint()
self.update()
def mouseReleaseEvent(self, event):
if event.button() == QtCore.Qt.MouseButton.LeftButton and self._is_drawing:
self._is_drawing = False
rect = QtCore.QRect(self._start_pos, self._end_pos).normalized()
if rect.width() > 5 and rect.height() > 5:
# 保留选区,显示确认/取消按钮
self._current_rect = rect
self._has_selection = True
self._position_buttons(rect)
self._btn_confirm.show()
self._btn_cancel.show()
self.update()
else:
self.close()
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key.Key_Escape:
self.close()
elif event.key() in (QtCore.Qt.Key.Key_Return, QtCore.Qt.Key.Key_Enter):
if self._has_selection:
self._on_confirm()
def _position_buttons(self, rect):
"""将确认/取消按钮定位到选区右下角"""
total_w = self.BTN_W * 2 + self.BTN_GAP
# 默认放在选区右下角外侧
x = rect.right() - total_w
y = rect.bottom() + 4
# 边界检测:超出屏幕底部时翻到选区内侧
if y + self.BTN_H > self.height():
y = rect.bottom() - self.BTN_H - 4
if x < 0:
x = 0
self._btn_confirm.move(x, y)
self._btn_cancel.move(x + self.BTN_W + self.BTN_GAP, y)
def _hide_buttons(self):
self._btn_confirm.hide()
self._btn_cancel.hide()
def _on_confirm(self):
"""确认截图:裁剪并发射信号"""
if self._full_pixmap and self._current_rect.width() > 5 and self._current_rect.height() > 5:
dpr = self._full_pixmap.devicePixelRatio()
phys_rect = QtCore.QRect(
int(self._current_rect.x() * dpr),
int(self._current_rect.y() * dpr),
int(self._current_rect.width() * dpr),
int(self._current_rect.height() * dpr),
)
captured = self._full_pixmap.toImage().copy(phys_rect)
self.screenshot_captured.emit(captured)
self.close()
def _on_cancel(self):
"""取消截图:直接关闭"""
self.close()
def closeEvent(self, event):
self._is_drawing = False
self._has_selection = False
self._hide_buttons()
self._full_pixmap = None
super().closeEvent(event)