docs: 添加涉密文件自检工具实施计划

This commit is contained in:
2026-06-08 13:53:24 +08:00
commit 31161d9a5f
1838 changed files with 455407 additions and 0 deletions

View File

View File

@@ -0,0 +1,64 @@
# 软件渲染选项
from PySide2.QtGui import QGuiApplication, QOpenGLContext
from PySide2.QtCore import Qt
import os
from umi_log import logger
from . import pre_configs
from ..platform import Platform
_GLDict = {
"AA_UseDesktopOpenGL": Qt.AA_UseDesktopOpenGL,
"AA_UseOpenGLES": Qt.AA_UseOpenGLES,
"AA_UseSoftwareOpenGL": Qt.AA_UseSoftwareOpenGL,
}
_Opt = ""
def initOpengl():
global _Opt
opt = getOpengl()
if opt not in _GLDict:
opt = Platform.getOpenGLUse()
setOpengl(opt)
QGuiApplication.setAttribute(_GLDict[opt], True)
_Opt = opt
def checkOpengl():
global _Opt
if _Opt == "AA_UseOpenGLES": # GLES需要检查有些win7不支持
if not QOpenGLContext.openGLModuleType() == QOpenGLContext.LibGLES:
QGuiApplication.setAttribute(Qt.AA_UseOpenGLES, False)
_Opt = "AA_UseSoftwareOpenGL" # 既然不支持opengl那就软渲染吧
setOpengl(_Opt)
msg = "当前系统不支持OpenGLES已禁用此渲染器。\n若本次运行中程序崩溃或报错,请重新启动程序。\n\n"
msg += "The current system does not support OpenGLES and has disabled the program from using this renderer. \nIf there are crashes or errors during this run, please restarting the program."
logger.warning(msg)
os.MessageBox(msg, type_="warning")
def setOpengl(opt):
if opt not in _GLDict:
raise ValueError
pre_configs.setValue("opengl", opt)
def getOpengl():
return pre_configs.getValue("opengl")
# OpenGL渲染模式
# 启用 OpenGL 上下文之间的资源共享
# QGuiApplication.setAttribute(Qt.AA_ShareOpenGLContexts, True)
# 渲染模式,【减少窗口调整大小时内容的抖动】
# 方式一启用OpenGL软件渲染。性能最差CPU占用率大幅提升效果最好。
# QGuiApplication.setAttribute(Qt.AA_UseSoftwareOpenGL, True)
# 方式二:使用 桌面 OpenGL例如 opengl32.dll 或 libGL.so。性能最好效果较差。
# QGuiApplication.setAttribute(Qt.AA_UseDesktopOpenGL, True)
# 方式三:使用 OpenGL ES 2.0 或更高版本用d3d接口抽象成Opengl。性能和效果都很好。但兼容性很差
# 1. ColorOverlay必须开启cache否则无法渲染透明层。
# 2. 需要系统安装dx9和OpenGL3。虚拟机中可能无法使用。需要检查兼容性
# 必须做兼容性判定兼容时才启用AA_UseOpenGLES。
# QGuiApplication.setAttribute(Qt.AA_UseOpenGLES, True)

View File

@@ -0,0 +1,63 @@
# 提供在主线程中调用指定函数
from PySide2.QtCore import QObject, Slot, Signal, QTimer, QMutex
from uuid import uuid4 # 唯一ID
class __CallFunc(QObject):
def __init__(self):
super().__init__()
# 信号 在主线程中调用函数
self._callFuncSignal = self.cSignal()
self._callFuncSignal.signal.connect(self._cFunc)
# 计时器停止字典
self._timerStopDict = {}
self._timerLock = QMutex()
# ========================= 【接口】 =========================
# 立刻在主线程中调用python函数
def now(self, func, *args):
self._callFuncSignal.signal.emit((func, args))
# 延时在主线程中调用python函数。返回计时器ID
def delay(self, func, time, *args):
timerID = str(uuid4())
def go():
timer = QTimer(self)
timer.setSingleShot(True) # 单次运行
timer.timeout.connect(lambda: self._timerFunc(timerID, func, args))
timer.start(time * 1000)
self.now(go)
return timerID
# 取消已启用的延时
def delayStop(self, timerID):
self._timerLock.lock()
self._timerStopDict[timerID] = True # 记录停止
self._timerLock.unlock()
# ==================================================
# 计时器调用的函数
def _timerFunc(self, timerID, func, args):
self._timerLock.lock()
if timerID in self._timerStopDict:
del self._timerStopDict[timerID]
self._timerLock.unlock()
return
self._timerLock.unlock()
func(*args)
# 异步调用的槽函数
@Slot("QVariant")
def _cFunc(self, args):
args[0](*args[1])
# 信号类
class cSignal(QObject):
signal = Signal("QVariant")
CallFunc = __CallFunc()

View File

@@ -0,0 +1,131 @@
# ============================================
# =============== 文件查找/加载 ===============
# ============================================
# 从指定路径中,查找符合的文件
import re
import os
import time
from PySide2.QtQml import QJSValue
from typing import List
from ..event_bus.pubsub_service import PubSubService # 发布事件
from ..mission.mission_doc import MissionDOC, DocSuf
from ..mission.mission_ocr import ImageSuf
from umi_log import logger
FileSuf = { # 合法文件后缀
"image": ImageSuf,
"doc": DocSuf,
}
# 同步从路径中搜索后缀符合要求的文件,返回路径列表。
def findFiles(
paths: List, # 初始路径列表
sufType: str, # 后缀类型FileSuf的key
isRecurrence: bool, # 若为True则递归搜索
):
if isinstance(paths, QJSValue):
paths = paths.toVariant()
if not isinstance(paths, list):
logger.error(f"不合法的路径列表:{paths}, {type(paths)}")
return []
sufs = FileSuf.get(sufType, "")
if not sufs:
logger.error(f"不合法的后缀类型:{sufs}")
return []
def _sufMatching(path):
return os.path.splitext(path)[-1].lower() in sufs
filePaths = []
for p in paths:
if os.path.isfile(p) and _sufMatching(p): # 是文件,直接判断
filePaths.append(os.path.abspath(p))
elif os.path.isdir(p): # 是目录
if isRecurrence: # 需要递归
for root, dirs, files in os.walk(p):
for file in files:
if _sufMatching(file): # 收集子文件
filePaths.append(
os.path.abspath(os.path.join(root, file))
) # 将路径转换为绝对路径
else: # 不递归读取子文件夹
for file in os.listdir(p):
if os.path.isfile(os.path.join(p, file)) and _sufMatching(file):
filePaths.append(os.path.abspath(os.path.join(p, file)))
for i, p in enumerate(filePaths): # 规范化正斜杠
filePaths[i] = p.replace("\\", "/")
return filePaths
# 异步从路径中搜索后缀符合要求的文件并定时刷新UI。
# image: 返回路径列表
# doc: 返回 MissionDOC.getDocInfo 的信息字典列表
def asynFindFiles(
paths: List, # 初始路径列表
sufType: str, # 后缀类型FileSuf的key
isRecurrence: bool, # 若为True则递归搜索
completeKey: str, # 全部完成后的事件key。向事件传入合法路径列表。
updateKey: str, # UI刷新进度的事件key。填""则不刷新。向事件传入 (已完成的路径数量, 最近一条路径)
updateTime: float, # UI刷新进度的间距
):
if isinstance(paths, QJSValue):
paths = paths.toVariant()
if not isinstance(paths, list):
logger.error(f"不合法的路径列表:{paths}, {type(paths)}")
PubSubService.publish(completeKey, [])
return
sufs = FileSuf.get(sufType, "")
if not sufs:
logger.error(f"不合法的后缀类型:{sufs}")
PubSubService.publish(completeKey, [])
return
def _sufMatching(path):
return os.path.splitext(path)[-1].lower() in sufs
if not updateKey: # 如果没有刷新事件,则刷新间隔为无穷大
updateTime = float("inf")
filePaths = []
lastTime = 0 # 上一次update事件的时间
def updateEvent(fp):
nonlocal lastTime
now = time.time()
if now - lastTime > updateTime:
PubSubService.publish(updateKey, len(filePaths), fp)
lastTime = now
def addFile(fp):
fp = fp.replace("\\", "/") # 规范化正斜杠
if sufType == "doc": # 文档读取信息
info = MissionDOC.getDocInfo(fp)
if "error" in info:
logger.warning(f'读入文档失败:{fp}, {info["error"]}')
else:
filePaths.append(info)
else:
filePaths.append(fp)
updateEvent(fp)
for p in paths:
if os.path.isfile(p) and _sufMatching(p): # 是文件,直接判断
addFile(os.path.abspath(p))
elif os.path.isdir(p): # 是目录
if isRecurrence: # 需要递归
for root, dirs, files in os.walk(p):
for file in files:
if _sufMatching(file): # 收集子文件
# 转换为绝对路径
fp = os.path.abspath(os.path.join(root, file))
addFile(fp)
else: # 不递归读取子文件夹
for file in os.listdir(p):
if os.path.isfile(os.path.join(p, file)) and _sufMatching(file):
fp = os.path.abspath(os.path.join(p, file))
addFile(fp)
PubSubService.publish(completeKey, filePaths)

View File

@@ -0,0 +1,86 @@
# 全局设置连接器
import os
from PySide2.QtCore import QObject, Slot
from . import app_opengl
from .i18n_configs import I18n
from ..platform import Platform
from .pre_configs import getErrorStr
from ..server import web_server
from ..server.cmd_server import CmdActuator
from umi_log import change_save_log_level, open_logs_dir
class GlobalConfigsConnector(QObject):
def __init__(self):
super().__init__()
# 创建快捷方式
@Slot(str, result=str)
def createShortcut(self, position):
return Platform.Shortcut.createShortcut(position)
# 删除快捷方式
@Slot(str, result=int)
def deleteShortcut(self, position):
return Platform.Shortcut.deleteShortcut(position)
# 获取UI语言信息
@Slot(result="QVariant")
def i18nGetInfos(self):
return I18n.getInfos()
# 设置UI语言
@Slot(str, result=bool)
def i18nSetLanguage(self, lang):
return I18n.setLanguage(lang)
# 获取Opengl渲染器选项
@Slot(result=str)
def getOpengl(self):
return app_opengl.getOpengl()
# 设置Opengl渲染器选项
@Slot(str)
def setOpengl(self, opt):
app_opengl.setOpengl(opt)
# 修改日志级别成功返回T
@Slot(str, result=bool)
def change_save_log_level(self, levelname):
return change_save_log_level(levelname)
# 打开日志保存目录
@Slot()
def open_logs_dir(self):
open_logs_dir()
# 启动web服务器传入qml对象及回调函数名。
@Slot("QVariant", str, str, result=int)
def runUmiWeb(self, qmlObj, callback, host):
web_server.runUmiWeb(qmlObj, callback, host)
# 设置服务端口号
@Slot(int)
def setServerPort(self, port):
web_server.setPort(port)
# 将qml模块字典传入cmd执行器
@Slot("QVariant")
def setQmlToCmd(self, moduleDict):
CmdActuator.initCollect(moduleDict)
# 检查权限,返回检查结果
@Slot(result=str)
def checkAccess(self):
cwd = os.getcwd() # 当前工作路径
err = getErrorStr() # 读写异常情况
if not err: # 没有异常,则再检查一遍权限
if not os.access(cwd, os.R_OK):
err += "在当前路径不具有可读权限。\nDo not have read permission on the current path."
if not os.access(cwd, os.W_OK):
err += "在当前路径不具有可写权限。\nDo not have write permission on the current path."
if err:
err = cwd + "\n" + err
return err

View File

@@ -0,0 +1,146 @@
# UI语言设置
import os
from PySide2.QtCore import QTranslator
from . import pre_configs
from plugin_i18n import setLangCode
from umi_log import logger
I18nDir = "i18n" # 翻译文件 目录
DefaultLang = "zh_CN" # 默认语言项目中qsTr()标记的原生语言,无翻译文件。
# 语言表。每个语种只有第一个语言代码是有效的(对应到翻译文件.qm
# 其余的语言代码会映射到第一个。如zh_HK会映射到zh_TWen_CA映射到en_US。
# https://www.science.co.il/language/Locale-codes.php
LanguageCodes = {
# ===== 简中 =====
"zh_CN": "简体中文",
"zh": "简体中文",
# ===== 繁中 =====
"zh_TW": "繁體中文", # 中国台湾
"zh_HK": "繁體中文", # 中国香港
"zh_MO": "繁體中文", # 中国澳门
"zh_SG": "繁體中文", # 新加坡
# ===== 英语 =====
"en_US": "English", # 美国
"en": "English",
"en_GB": "English", # 英国
"en_AU": "English", # 澳大利亚
"en_CA": "English", # 加拿大
# ===== 日语 =====
"ja_JP": "日本語", # 日本
# ===== 俄语 =====
"ru_RU": "Русский", # 俄罗斯
"ru": "Русский",
# ===== 葡萄牙语 =====
"pt": "Português",
"pt_BR": "Português", # 巴西
"pt_PT": "Português", # 葡萄牙
# ===== 泰米尔语 =====
"ta": "தமிழ்",
"ta_TA": "தமிழ்",
}
""" 暂未启用的语言
# ===== 韩语 =====
"ko_KR": "한국어", # 韩国
# ===== 法语 =====
"fr_FR": "Français", # 法国
"fr": "Français",
"fr_CA": "Français", # 加拿大(魁北克)
"fr_BE": "Français", # 比利时
# ===== 意大利语 =====
"it_IT": "Italiano",
# ===== 挪威语 =====
"nb_NO": "norsk",
# ===== 德语 =====
"de_DE": "Deutsch",
"de": "Deutsch",
"de_AT": "Deutsch",
"de_CH": "Deutsch",
# ===== 西班牙语 Spanish =====
"es_ES": "Español",
"es_MX": "Español",
"""
class _I18n:
def init(self, qtApp):
translator = QTranslator()
qtApp.translators = [translator]
self.langCode = ""
self.langDict = {}
# 获取信息
self._getLangPath()
text, path = self.langDict[self.langCode]
setLangCode(self.langCode) # 设置插件翻译
if not path:
logger.debug("使用默认文本,未加载翻译。")
return
if not translator.load(path):
msg = f"无法加载UI语言\n[Error] Unable to load UI language: {path}"
logger.warning(msg)
os.MessageBox(msg, type_="warning")
return
if not qtApp.installTranslator(translator): # 安装翻译器
msg = f"无法加载翻译模块!\n[Error] Unable to installTranslator: {path}"
logger.warning(msg)
os.MessageBox(msg, type_="warning")
return
logger.info(f"i18n file loaded successfully. {self.langCode} - {text}")
# 切换语言
def setLanguage(self, code):
if code in self.langDict:
self.langCode = code
pre_configs.setValue("i18n", code) # 写入预配置项
return True
return False
# 获取语言参数
def getInfos(self):
return [self.langCode, self.langDict]
# 获取当前翻译文件路径,如果没有配置文件则初始化
def _getLangPath(self):
self.langDict = {}
self.langCode = ""
# 搜索本地翻译文件
for file in os.listdir(I18nDir):
if file.endswith(".qm"):
code = os.path.splitext(file)[0]
path = os.path.join(I18nDir, file)
text = LanguageCodes.get(code, code)
self.langDict[code] = [text, path]
if DefaultLang not in self.langDict:
text = LanguageCodes[DefaultLang]
self.langDict[DefaultLang] = [text, ""]
# 加载预配置项
code = pre_configs.getValue("i18n")
if code in self.langDict:
self.langCode = code
# 未能加载,则初始化预配置
if not self.langCode:
import locale
# 取得当前系统语言
code, encoding = locale.getdefaultlocale()
# 映射首位代号
if code in LanguageCodes:
langStr = LanguageCodes[code]
for c, l in LanguageCodes.items():
if l == langStr:
code = c
break
# 尝试写入配置
if not self.setLanguage(code):
# 写入配置失败,则使用默认语言
self.setLanguage(DefaultLang)
logger.warning(
f"The current system language is {code} and there is no corresponding i18n file. The default language used is {DefaultLang}."
)
I18n = _I18n()

View File

@@ -0,0 +1,76 @@
# 程序的配置分为两部分一部分是由qml引擎控制的主配置必须启动qml才能访问。
# 而这里是第二部分的配置项单独存放少量关键配置可以在未启动qml之前访问。
import os
import json
_FileName = "./.pre_settings"
_Configs = {
"i18n": "", # 界面语言
"opengl": "", # 界面OpenGL渲染类型
"server_port": 1224, # 服务端口号
"last_pid": -1, # 最后一次运行时的进程号
"last_ptime": -1, # 最后一次运行时的进程创建时间
}
_Errors = {} # 记录读写预配置文件的异常情况
def getValue(key):
if key in _Configs:
return _Configs[key]
else:
raise ValueError
def setValue(key, value):
if key in _Configs:
_Configs[key] = value
writeConfigs()
else:
raise ValueError
def writeConfigs():
global _Errors
try:
with open(_FileName, "w", encoding="utf-8") as file:
json.dump(_Configs, file, ensure_ascii=False, indent=4)
except PermissionError:
_Errors[
"Write PermissionError"
] = "权限不足,无法写入配置文件。\nInsufficient permissions, unable to write to the configuration file."
except Exception as e:
_Errors[
"Write Error"
] = f"无法写入配置文件。\nUnable to write to the configuration file: {e}"
def readConfigs():
global _Errors
if not os.path.exists(_FileName):
return
try:
with open(_FileName, "r") as file:
data = json.load(file)
for key in _Configs:
_Configs[key] = data[key]
except PermissionError:
_Errors[
"Write PermissionError"
] = "权限不足,无法读取配置文件。\nInsufficient permissions, unable to read to the configuration file."
except Exception as e:
_Errors[
"Write Error"
] = f"无法读取配置文件。\nUnable to read to the configuration file: {e}"
# 返回异常情况字符串
def getErrorStr():
global _Errors
err = ""
if _Errors:
for e in _Errors.values():
err += e + "\n"
return err

View File

@@ -0,0 +1,33 @@
# =========================================
# =============== 主题连接器 ===============
# =========================================
from PySide2.QtCore import QObject, Slot
from umi_log import logger
ThemePath = "themes.json"
class ThemeConnector(QObject):
# 读取主题
@Slot(result=str)
def loadThemeStr(self):
try:
with open(ThemePath, "r", encoding="utf-8") as f:
r = f.read()
return r
except FileNotFoundError:
pass
except Exception:
logger.warning("读取主题文件失败。", exc_info=True)
return ""
# 保存主题
@Slot(str)
def saveThemeStr(self, tstr):
try:
with open(ThemePath, "w", encoding="utf-8") as f:
f.write(tstr)
except Exception:
logger.warning("写入主题文件失败。", exc_info=True)

View File

@@ -0,0 +1,42 @@
# =====================================================
# =============== 全局线程池 异步任务接口 ===============
# =====================================================
from PySide2.QtCore import QThreadPool, QRunnable
from umi_log import logger
# 全局线程池
GlobalThreadPool = QThreadPool.globalInstance()
# 异步类
class Runnable(QRunnable):
def __init__(self, taskFunc, *args, **kwargs):
super().__init__()
self._taskFunc = taskFunc
self._args = args
self._kwargs = kwargs
def run(self):
try:
self._taskFunc(*self._args, **self._kwargs)
except Exception:
logger.error("异步运行发生错误。", exc_info=True, stack_info=True)
# 启动异步类
def threadPoolStart(runnable: QRunnable):
# 检查线程池是否满,并扩充
activeThreadCount = GlobalThreadPool.activeThreadCount()
if activeThreadCount >= GlobalThreadPool.maxThreadCount():
logger.debug(f"线程池已满 {activeThreadCount} !自动扩充+1。")
GlobalThreadPool.setMaxThreadCount(activeThreadCount + 1)
GlobalThreadPool.start(runnable)
# 快捷接口:异步运行函数,返回异步类的对象
def threadRun(taskFunc, *args, **kwargs):
runnable = Runnable(taskFunc, *args, **kwargs)
threadPoolStart(runnable)
return runnable

View File

@@ -0,0 +1,104 @@
# =======================================
# =============== 通用工具 ===============
# =======================================
import re
import os
from PySide2.QtGui import QClipboard
from PySide2.QtCore import QFileInfo
from PySide2.QtQml import QJSValue
from urllib.parse import unquote # 路径解码
from umi_log import logger
Clipboard = QClipboard() # 剪贴板
# 传入文件名检测是否含非法字符。没问题返回True
def allowedFileName(fn):
pattern = r'[\\/:*?"<>|]'
if re.search(pattern, fn):
return False # 转布尔值
else:
return True
# 复制文本到剪贴板
def copyText(text):
Clipboard.setText(text)
# QUrl列表 转 String列表
def QUrl2String(urls):
resList = []
for url in urls:
if url.isLocalFile():
u = unquote(url.toLocalFile()) # 解码路径
if QFileInfo(u).exists(): # 检查路径是否真的存在
resList.append(u)
return resList
# 初始化配置项字典数值,等同于 Configs.qml 的 function initConfigDict
# 主要是为了补充type和default
def initConfigDict(dic):
toDict = {}
def handleConfigItem(config, key): # 处理一个配置项
# 类型指定type
if not config["type"] == "":
if config["type"] == "file": # 文件选择
config["default"] = "" if not config["default"] is None else None
elif config["type"] == "var" and config["default"] is None: # 任意类型
config["default"] = ""
# 类型省略type
else:
if isinstance(config["default"], bool): # 布尔
config["type"] = "boolean"
elif "optionsList" in config: # 枚举
config["type"] = "enum"
if len(config["optionsList"]) == 0:
logger.error(f"处理配置项异常:{key}枚举列表为空。")
return
if config["default"] is None:
config["default"] = config["optionsList"][0][0]
elif isinstance(config["default"], str): # 文本
config["type"] = "text"
elif isinstance(config["default"], (int, float)): # 数字
config["type"] = "number"
elif "btnsList" in config: # 按钮组
config["type"] = "buttons"
return
else:
logger.error(f"未知类型的配置项:{key}")
return
def handleConfigGroup(group, prefix=""): # 处理一个配置组
for key in group:
config = group[key]
if not isinstance(config, dict):
continue
# 补充空白参数
if "type" not in config: # 类型
config["type"] = ""
if "default" not in config: # 默认值
config["default"] = None
if "advanced" not in config: # 是否为高级选项
config["advanced"] = False
# 记录完整key
fullKey = prefix + key
if config["type"] == "group": # 若是配置项组,递归遍历
handleConfigGroup(config, fullKey + ".") # 前缀加深一层
else: # 若是配置项
toDict[fullKey] = config
handleConfigItem(config, fullKey)
handleConfigGroup(dic)
return toDict
# 整理 argd 参数字典,将 float 恢复 int 类型,如 12.0 → 12
def argdIntConvert(argd):
for k, v in argd.items():
if isinstance(v, float) and v.is_integer():
argd[k] = int(v)

View File

@@ -0,0 +1,63 @@
# 通用工具连接器
from typing import List
from PySide2.QtCore import QObject, Slot
from . import utils
from . import file_finder # 文件搜索器
from ..platform import Platform # 跨平台
from .thread_pool import threadRun # 异步执行函数
class UtilsConnector(QObject):
def __init__(self):
super().__init__()
# 将文本写入剪贴板
@Slot(str)
def copyText(self, text):
utils.copyText(text)
# 用系统应用打开文件或目录
@Slot(str)
def startfile(self, path):
Platform.startfile(path)
# 硬件控制
@Slot(str)
def hardwareCtrl(self, key):
if key == "shutdown": # 关机
Platform.HardwareCtrl.shutdown()
elif key == "hibernate": # 休眠
Platform.HardwareCtrl.hibernate()
# 同步搜索文件,返回合法的文件路径列表
@Slot("QVariant", bool, str, result="QVariant")
def findFiles(self, paths, sufType, isRecurrence):
return file_finder.findFiles(paths, sufType, isRecurrence)
# 异步搜索文件
@Slot("QVariant", str, bool, str, str, float)
def asynFindFiles(
self,
paths: List, # 初始路径列表
sufType: str, # 后缀类型FileSuf的key
isRecurrence: bool, # 若为True则递归搜索
completeKey: str, # 全部完成后的事件key。向事件传入合法路径列表。
updateKey: str, # 加载中刷新进度的key不填则无。向事件传入 (已完成的路径数量, 最近一条路径)
updateTime: float, # 刷新进度的间距
):
threadRun(
file_finder.asynFindFiles,
paths,
sufType,
isRecurrence,
completeKey,
updateKey,
updateTime,
)
# QUrl列表 转 String列表
@Slot("QVariant", result="QVariant")
def QUrl2String(self, fileUrls):
return utils.QUrl2String(fileUrls)