ascript 1.3.27__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ascript/__init__.py +8 -0
- ascript/hooks/__init__.py +0 -0
- ascript/hooks/hook-ascript.py +2 -0
- ascript/windows/__init__.py +29 -0
- ascript/windows/daos/__init__.py +0 -0
- ascript/windows/daos/screen.py +143 -0
- ascript/windows/hardware/__init__.py +5 -0
- ascript/windows/hardware/keyboard.py +279 -0
- ascript/windows/hardware/logitech_driver.py +179 -0
- ascript/windows/hardware/mouse.py +308 -0
- ascript/windows/license/__init__.py +16 -0
- ascript/windows/license/achttp.cp310-win_amd64.pyd +0 -0
- ascript/windows/license/achttp.cp311-win_amd64.pyd +0 -0
- ascript/windows/license/achttp.cp312-win_amd64.pyd +0 -0
- ascript/windows/license/achttp.cp313-win_amd64.pyd +0 -0
- ascript/windows/license/achttp.cp314-win_amd64.pyd +0 -0
- ascript/windows/license/achttp.cp38-win_amd64.pyd +0 -0
- ascript/windows/license/achttp.cp39-win_amd64.pyd +0 -0
- ascript/windows/media/__init__.py +0 -0
- ascript/windows/resources/__init__.py +0 -0
- ascript/windows/resources/libs/logitech/x64/LogitechGkeyEnginesWrapper.dll +0 -0
- ascript/windows/resources/libs/logitech/x86/LogitechGkeyEnginesWrapper.dll +0 -0
- ascript/windows/resources/templates/__init__.py +0 -0
- ascript/windows/resources/templates/__init__py.win.template +51 -0
- ascript/windows/resources/templates/build.as.win.template +14 -0
- ascript/windows/resources/templates/build.py +71 -0
- ascript/windows/resources/templates/logo_windows.ico +0 -0
- ascript/windows/resources/templates/windows/main.py +50 -0
- ascript/windows/resources/templates/windows/pyproject.toml +44 -0
- ascript/windows/resources/templates/windows/res/img/logo.ico +0 -0
- ascript/windows/screen/__init__.py +13 -0
- ascript/windows/screen/as_cv.py +382 -0
- ascript/windows/screen/canny.py +128 -0
- ascript/windows/screen/code_scanner.py +72 -0
- ascript/windows/screen/color_algorithms.py +277 -0
- ascript/windows/screen/find_colors.py +251 -0
- ascript/windows/screen/find_images.py +217 -0
- ascript/windows/screen/ocr/__init__.py +4 -0
- ascript/windows/screen/ocr/ocr.py +223 -0
- ascript/windows/screen/ocr/rapidocr_hook.py +301 -0
- ascript/windows/screen/ocrx.py +169 -0
- ascript/windows/screen/screen_core.py +139 -0
- ascript/windows/store/__init__.py +21 -0
- ascript/windows/system/__init__.py +8 -0
- ascript/windows/system/device.py +355 -0
- ascript/windows/system/easy_print.py +115 -0
- ascript/windows/system/env.py +492 -0
- ascript/windows/system/exception/__init__.py +1 -0
- ascript/windows/system/key_value.py +145 -0
- ascript/windows/system/pip.py +175 -0
- ascript/windows/system/r.py +165 -0
- ascript/windows/system/runtime.py +62 -0
- ascript/windows/tools/__init__.py +27 -0
- ascript/windows/tools/__main__.py +21 -0
- ascript/windows/tools/action.json +33 -0
- ascript/windows/tools/action.py +168 -0
- ascript/windows/tools/colors_tool.py +391 -0
- ascript/windows/tools/config.py +26 -0
- ascript/windows/tools/dumptool.py +18 -0
- ascript/windows/tools/eel_api.py +495 -0
- ascript/windows/tools/gpplug/__init__.py +0 -0
- ascript/windows/tools/gpplug/plug.py +70 -0
- ascript/windows/tools/mouse_catch.py +168 -0
- ascript/windows/tools/plug_manager.py +38 -0
- ascript/windows/tools/tool_api.py +906 -0
- ascript/windows/tools/tool_window.py +56 -0
- ascript/windows/tools/uia_worker.py +365 -0
- ascript/windows/tools/web/app_store_window.html +481 -0
- ascript/windows/tools/web/colors.html +532 -0
- ascript/windows/tools/web/css/screen.css +1342 -0
- ascript/windows/tools/web/css/viewtree.css +357 -0
- ascript/windows/tools/web/flot_control.html +195 -0
- ascript/windows/tools/web/img/ico/favicon.png +0 -0
- ascript/windows/tools/web/img/ico/paintpalette.png +0 -0
- ascript/windows/tools/web/img/svg/find_colors.svg +1 -0
- ascript/windows/tools/web/img/svg/find_images.svg +1 -0
- ascript/windows/tools/web/img/svg/illustration-6.svg +89 -0
- ascript/windows/tools/web/img/svg/img_default.svg +1 -0
- ascript/windows/tools/web/img/svg/ocr.svg +1 -0
- ascript/windows/tools/web/js/screen/colors.js +466 -0
- ascript/windows/tools/web/js/screen/gp_plug.js +108 -0
- ascript/windows/tools/web/js/screen/image_mouse.js +464 -0
- ascript/windows/tools/web/js/screen/img_view.js +558 -0
- ascript/windows/tools/web/js/screen/plug.js +246 -0
- ascript/windows/tools/web/js/screen/screen.js +690 -0
- ascript/windows/tools/web/js/screen/screen_api.js +778 -0
- ascript/windows/tools/web/js/screen/screen_maker.js +10 -0
- ascript/windows/tools/web/js/utils/map.js +73 -0
- ascript/windows/tools/web/js/utils/upload.js +157 -0
- ascript/windows/tools/web/js/viewtree/niv_js.js +27 -0
- ascript/windows/tools/web/js/viewtree/vtree.js +1510 -0
- ascript/windows/tools/web/libs/bootstrap/css/bootstrap.min.css +6 -0
- ascript/windows/tools/web/libs/bootstrap/js/bootstrap.bundle.min.js +7 -0
- ascript/windows/tools/web/libs/bootstrap-icons/0-circle.svg +4 -0
- ascript/windows/tools/web/libs/bootstrap-icons/0-square.svg +4 -0
- ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.css +2078 -0
- ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.json +2052 -0
- ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.min.css +5 -0
- ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.scss +2090 -0
- ascript/windows/tools/web/libs/bootstrap-icons/font/fonts/bootstrap-icons.woff +0 -0
- ascript/windows/tools/web/libs/bootstrap-icons/font/fonts/bootstrap-icons.woff2 +0 -0
- ascript/windows/tools/web/libs/feather-icons/dist/feather.min.js +13 -0
- ascript/windows/tools/web/libs/feather-icons/dist/icons.json +1 -0
- ascript/windows/tools/web/libs/jquery/core.js +399 -0
- ascript/windows/tools/web/libs/jquery/jquery.js +10598 -0
- ascript/windows/tools/web/libs/jquery/jquery.min.js +4 -0
- ascript/windows/tools/web/libs/jquery/jquery.min.js.zip +0 -0
- ascript/windows/tools/web/libs/jquery/jquery.slim.js +8495 -0
- ascript/windows/tools/web/libs/jquery/jquery.slim.min.js +2 -0
- ascript/windows/tools/web/libs/jquery-menu/jquery.contextMenu.js +2134 -0
- ascript/windows/tools/web/libs/jquery-menu/jquery.contextMenu.min.css +16 -0
- ascript/windows/tools/web/libs/jquery-zTree/css/zTreeStyle.css +97 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/1_close.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/1_open.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/2.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/3.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/4.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/5.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/6.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/7.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/8.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/9.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/line_conn.gif +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/loading.gif +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/tree-node-btn.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/zTreeStandard.gif +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/zTreeStandard.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/js/jquery.ztree.all.min.js +3 -0
- ascript/windows/tools/web/libs/treeview/css/jquery.treeview.css +79 -0
- ascript/windows/tools/web/libs/treeview/img/file.png +0 -0
- ascript/windows/tools/web/libs/treeview/img/ico_loading.png +0 -0
- ascript/windows/tools/web/libs/treeview/img/icon4.png +0 -0
- ascript/windows/tools/web/libs/treeview/img/icon5.png +0 -0
- ascript/windows/tools/web/libs/treeview/img/icon6.png +0 -0
- ascript/windows/tools/web/libs/treeview/img/py.png +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-black-line.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-black.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-default-line.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-default.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-famfamfam-line.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-famfamfam.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-gray-line.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-gray.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-red-line.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-red.gif +0 -0
- ascript/windows/tools/web/libs/treeview/js/jquery.treeview.edit.js +37 -0
- ascript/windows/tools/web/libs/treeview/js/jquery.treeview.js +250 -0
- ascript/windows/tools/web/license_window.html +255 -0
- ascript/windows/tools/web/plugs/a.html +653 -0
- ascript/windows/tools/web/plugs/demo.html +52 -0
- ascript/windows/tools/web/plugs/find_colors.html +629 -0
- ascript/windows/tools/web/plugs/find_images.html +699 -0
- ascript/windows/tools/web/plugs/rapidocr.html +954 -0
- ascript/windows/tools/web/vtree.html +496 -0
- ascript/windows/tools/webview/assets/index-HymBPl-I.css +1 -0
- ascript/windows/tools/webview/assets/index-PqA_m8tL.js +17 -0
- ascript/windows/tools/webview/index.html +13 -0
- ascript/windows/ui/__init__.py +6 -0
- ascript/windows/ui/app_store_window.py +272 -0
- ascript/windows/ui/draw_window.py +231 -0
- ascript/windows/ui/flot_test.py +297 -0
- ascript/windows/ui/flot_window.py +488 -0
- ascript/windows/ui/license_window.py +111 -0
- ascript/windows/ui/web_window.py +466 -0
- ascript/windows/utils/__init__.py +0 -0
- ascript/windows/utils/node_utils.py +20 -0
- ascript/windows/utils/screen_utils.py +280 -0
- ascript/windows/window/Selector.py +1015 -0
- ascript/windows/window/Window.py +747 -0
- ascript/windows/window/__init__.py +5 -0
- ascript/windows/window/launcher.py +213 -0
- ascript/windows/window/uia_runtime.py +26 -0
- ascript-1.3.27.dist-info/METADATA +41 -0
- ascript-1.3.27.dist-info/RECORD +177 -0
- ascript-1.3.27.dist-info/WHEEL +5 -0
- ascript-1.3.27.dist-info/entry_points.txt +2 -0
- ascript-1.3.27.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import threading
|
|
4
|
+
import base64
|
|
5
|
+
import ctypes
|
|
6
|
+
import time
|
|
7
|
+
import multiprocessing
|
|
8
|
+
import importlib
|
|
9
|
+
import importlib.machinery
|
|
10
|
+
import inspect
|
|
11
|
+
import types
|
|
12
|
+
from typing import Optional, Callable, Dict
|
|
13
|
+
from PIL import Image
|
|
14
|
+
import pystray
|
|
15
|
+
from .web_window import WebWindow
|
|
16
|
+
from ..system import R
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class QueueWriter:
|
|
20
|
+
"""队列写入器,将标准输出重定向到多进程队列。
|
|
21
|
+
|
|
22
|
+
用于子进程中捕获 print 输出,通过队列传递给主进程进行显示。
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, queue):
|
|
26
|
+
"""初始化队列写入器。
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
queue: multiprocessing.Queue 实例,用于接收输出消息。
|
|
30
|
+
"""
|
|
31
|
+
self.queue = queue
|
|
32
|
+
|
|
33
|
+
def write(self, message):
|
|
34
|
+
"""将非空消息写入队列。
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
message: 要写入的消息字符串。
|
|
38
|
+
"""
|
|
39
|
+
if message.strip():
|
|
40
|
+
self.queue.put(message)
|
|
41
|
+
|
|
42
|
+
def flush(self):
|
|
43
|
+
"""刷新操作(无实际操作,仅为兼容标准输出接口)。"""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class FloatScriptWindow(WebWindow):
|
|
48
|
+
"""浮动脚本控制窗口。
|
|
49
|
+
|
|
50
|
+
提供一个无边框置顶的迷你控制面板,支持脚本的启动/停止、
|
|
51
|
+
日志实时输出、系统托盘集成,以及通过多进程隔离运行用户脚本。
|
|
52
|
+
继承自 WebWindow。
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, icon_path: str, title: str = "AScript", use_process: bool = True, auto_start: bool = False):
|
|
56
|
+
"""初始化浮动脚本控制窗口。
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
icon_path: 图标文件路径,用于窗口显示和系统托盘图标。
|
|
60
|
+
title: 窗口标题,默认为 "AScript"。
|
|
61
|
+
use_process: 是否使用独立进程运行脚本,默认为 True。
|
|
62
|
+
auto_start: 是否在窗口就绪后自动启动脚本,默认为 False。
|
|
63
|
+
"""
|
|
64
|
+
self.icon_path = os.path.abspath(icon_path)
|
|
65
|
+
self.ui_title = title
|
|
66
|
+
self._is_hidden = False
|
|
67
|
+
self._callback = None
|
|
68
|
+
|
|
69
|
+
self.use_process = use_process
|
|
70
|
+
self.auto_start = auto_start
|
|
71
|
+
self.active_worker = None
|
|
72
|
+
self.events = {}
|
|
73
|
+
self._is_ready = False
|
|
74
|
+
self._is_switching = False
|
|
75
|
+
|
|
76
|
+
self.stop_event = multiprocessing.Event()
|
|
77
|
+
self.msg_queue = multiprocessing.Queue()
|
|
78
|
+
|
|
79
|
+
# _root = paths.get_path("ascript/windows/tools/web/")
|
|
80
|
+
# _filepath = paths.get_path("ascript/windows/tools/web/flot_control.html")
|
|
81
|
+
|
|
82
|
+
_root = R.internal("windows", "tools", "web") # paths.get_path("ascript/windows/tools/web/")
|
|
83
|
+
_filepath = R.internal("windows", "tools", "web",
|
|
84
|
+
"flot_control.html") # paths.get_path("ascript/windows/tools/web/license_window.html")
|
|
85
|
+
super().__init__(_filepath, project_root=_root)
|
|
86
|
+
|
|
87
|
+
self.expose(self.get_init_data)
|
|
88
|
+
self.expose(self.on_toggle_click)
|
|
89
|
+
self.expose(self.drag_window)
|
|
90
|
+
self.expose(self.hide_window)
|
|
91
|
+
self.expose(self.resize_ui)
|
|
92
|
+
|
|
93
|
+
self._setup_logging()
|
|
94
|
+
threading.Thread(target=self._queue_listener, daemon=True).start()
|
|
95
|
+
|
|
96
|
+
def on(self, event_name: str):
|
|
97
|
+
"""注册事件监听的装饰器。
|
|
98
|
+
|
|
99
|
+
支持的事件名: "start"(脚本启动)、"stop"(脚本停止)。
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
event_name: 事件名称。
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Callable: 装饰器函数。
|
|
106
|
+
"""
|
|
107
|
+
def wrapper(func: Callable):
|
|
108
|
+
self.events[event_name] = func
|
|
109
|
+
return func
|
|
110
|
+
|
|
111
|
+
return wrapper
|
|
112
|
+
|
|
113
|
+
def _setup_logging(self):
|
|
114
|
+
"""设置日志重定向。
|
|
115
|
+
|
|
116
|
+
将 sys.stdout 替换为当前实例,使 print 输出同时写入控制台和 WebView 界面。
|
|
117
|
+
"""
|
|
118
|
+
self._original_stdout = sys.stdout
|
|
119
|
+
sys.stdout = self
|
|
120
|
+
|
|
121
|
+
def _queue_listener(self):
|
|
122
|
+
"""队列消息监听线程。
|
|
123
|
+
|
|
124
|
+
持续从子进程消息队列中读取日志并打印到主进程标准输出(触发 WebView 显示)。
|
|
125
|
+
"""
|
|
126
|
+
while True:
|
|
127
|
+
try:
|
|
128
|
+
msg = self.msg_queue.get()
|
|
129
|
+
print(msg)
|
|
130
|
+
except:
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
def write(self, message):
|
|
134
|
+
"""写入消息到控制台和 WebView 界面。
|
|
135
|
+
|
|
136
|
+
同时将消息输出到原始标准输出和 WebView 的日志面板。
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
message: 要写入的消息字符串。
|
|
140
|
+
"""
|
|
141
|
+
# 1. 尝试写回原始控制台,增加出错捕获
|
|
142
|
+
try:
|
|
143
|
+
self._original_stdout.write(message)
|
|
144
|
+
except:
|
|
145
|
+
# 出错时不进行任何处理,直接跳过,防止程序崩溃
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
# 2. 推送到 WebView UI
|
|
149
|
+
if self._is_ready and message.strip() and self.webview_instance:
|
|
150
|
+
try:
|
|
151
|
+
# 保持你原始的字符替换逻辑
|
|
152
|
+
safe_msg = message.replace("\\", "\\\\").replace("'", "\\'").replace('"', '\\"').replace("\n",
|
|
153
|
+
"").replace(
|
|
154
|
+
"\r", "")
|
|
155
|
+
self.webview_instance.evaluate_js(f"if(window.addLog){{window.addLog('{safe_msg}');}}")
|
|
156
|
+
except:
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
def flush(self):
|
|
160
|
+
"""刷新原始标准输出缓冲区。"""
|
|
161
|
+
try:
|
|
162
|
+
self._original_stdout.flush()
|
|
163
|
+
except:
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def _callback_argument(callback):
|
|
168
|
+
"""Return a spawn-safe callback reference for imported project modules."""
|
|
169
|
+
if callback is None or callback.__module__ in {"__main__", "__mp_main__"}:
|
|
170
|
+
return callback
|
|
171
|
+
return {
|
|
172
|
+
"module": callback.__module__,
|
|
173
|
+
"qualname": callback.__qualname__,
|
|
174
|
+
"source": inspect.getsourcefile(callback) or "",
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def _resolve_callback(callback):
|
|
179
|
+
"""Resolve a callback reference inside a Windows spawned process."""
|
|
180
|
+
if callback is None or callable(callback):
|
|
181
|
+
return callback
|
|
182
|
+
if not isinstance(callback, dict):
|
|
183
|
+
raise TypeError("无效的脚本回调")
|
|
184
|
+
|
|
185
|
+
module_name = callback.get("module", "")
|
|
186
|
+
qualname = callback.get("qualname", "")
|
|
187
|
+
source = os.path.abspath(callback.get("source", ""))
|
|
188
|
+
if not module_name or not qualname:
|
|
189
|
+
raise ValueError("脚本回调信息不完整")
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
module = importlib.import_module(module_name)
|
|
193
|
+
except ModuleNotFoundError as error:
|
|
194
|
+
root_name = module_name.split(".", 1)[0]
|
|
195
|
+
if error.name != root_name or not source or not os.path.isfile(source):
|
|
196
|
+
raise
|
|
197
|
+
|
|
198
|
+
module_parts = module_name.split(".")
|
|
199
|
+
package_root = os.path.dirname(source)
|
|
200
|
+
for _ in module_parts[1:-1]:
|
|
201
|
+
package_root = os.path.dirname(package_root)
|
|
202
|
+
|
|
203
|
+
package = types.ModuleType(root_name)
|
|
204
|
+
package.__path__ = [package_root]
|
|
205
|
+
package.__package__ = root_name
|
|
206
|
+
package.__spec__ = importlib.machinery.ModuleSpec(root_name, loader=None, is_package=True)
|
|
207
|
+
package.__spec__.submodule_search_locations = [package_root]
|
|
208
|
+
sys.modules[root_name] = package
|
|
209
|
+
module = importlib.import_module(module_name)
|
|
210
|
+
|
|
211
|
+
resolved = module
|
|
212
|
+
for part in qualname.split("."):
|
|
213
|
+
if part == "<locals>":
|
|
214
|
+
raise ValueError("多进程回调必须定义在模块顶层")
|
|
215
|
+
resolved = getattr(resolved, part)
|
|
216
|
+
return resolved
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
def _start_process(process):
|
|
220
|
+
"""Start a worker without letting spawn re-import a synthetic host package."""
|
|
221
|
+
main_module = sys.modules.get("__main__")
|
|
222
|
+
main_spec = getattr(main_module, "__spec__", None)
|
|
223
|
+
spec_name = getattr(main_spec, "name", None)
|
|
224
|
+
restore_name = None
|
|
225
|
+
if (
|
|
226
|
+
main_spec is not None
|
|
227
|
+
and spec_name
|
|
228
|
+
and spec_name != "__main__"
|
|
229
|
+
and not spec_name.endswith(".__main__")
|
|
230
|
+
and getattr(main_spec, "loader", None) is None
|
|
231
|
+
):
|
|
232
|
+
restore_name = spec_name
|
|
233
|
+
main_spec.name = "__main__"
|
|
234
|
+
try:
|
|
235
|
+
process.start()
|
|
236
|
+
finally:
|
|
237
|
+
if restore_name is not None:
|
|
238
|
+
main_spec.name = restore_name
|
|
239
|
+
|
|
240
|
+
@staticmethod
|
|
241
|
+
def _worker_wrapper(start_func, stop_func, queue, stop_event):
|
|
242
|
+
"""子进程中的工作函数包装器。
|
|
243
|
+
|
|
244
|
+
在独立进程中运行用户脚本,重定向输出到队列,并监听停止信号。
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
start_func: 用户注册的启动函数。
|
|
248
|
+
stop_func: 用户注册的停止函数(可选,可为 None)。
|
|
249
|
+
queue: multiprocessing.Queue,用于传递日志消息。
|
|
250
|
+
stop_event: multiprocessing.Event,用于接收停止信号。
|
|
251
|
+
"""
|
|
252
|
+
sys.stdout = QueueWriter(queue)
|
|
253
|
+
sys.stderr = QueueWriter(queue)
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
start_func = FloatScriptWindow._resolve_callback(start_func)
|
|
257
|
+
stop_func = FloatScriptWindow._resolve_callback(stop_func)
|
|
258
|
+
except Exception as error:
|
|
259
|
+
queue.put(f">>> [系统] 无法加载任务回调: {error}")
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
worker_thread = threading.Thread(target=start_func, daemon=True)
|
|
263
|
+
worker_thread.start()
|
|
264
|
+
|
|
265
|
+
def safe_cleanup():
|
|
266
|
+
if stop_func:
|
|
267
|
+
t = threading.Thread(target=stop_func, daemon=True)
|
|
268
|
+
t.start()
|
|
269
|
+
t.join(timeout=2.5)
|
|
270
|
+
if t.is_alive():
|
|
271
|
+
pass
|
|
272
|
+
|
|
273
|
+
while True:
|
|
274
|
+
if stop_event.is_set():
|
|
275
|
+
safe_cleanup()
|
|
276
|
+
break
|
|
277
|
+
|
|
278
|
+
if not worker_thread.is_alive():
|
|
279
|
+
safe_cleanup()
|
|
280
|
+
break
|
|
281
|
+
|
|
282
|
+
time.sleep(0.1)
|
|
283
|
+
|
|
284
|
+
def _monitor_worker(self, worker):
|
|
285
|
+
"""监控工作进程状态。
|
|
286
|
+
|
|
287
|
+
等待工作进程结束后,更新 UI 状态为已停止。
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
worker: 要监控的 multiprocessing.Process 实例。
|
|
291
|
+
"""
|
|
292
|
+
worker.join()
|
|
293
|
+
if self.active_worker == worker:
|
|
294
|
+
self.active_worker = None
|
|
295
|
+
self.stop_event.clear()
|
|
296
|
+
if self.webview_instance:
|
|
297
|
+
self.webview_instance.evaluate_js("if(window.syncStatus){window.syncStatus(false);}")
|
|
298
|
+
|
|
299
|
+
def on_toggle_click(self, status):
|
|
300
|
+
"""处理前端播放/停止按钮点击事件。
|
|
301
|
+
|
|
302
|
+
根据状态启动或停止工作进程,防止重复操作。
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
status: True 表示启动,False 表示停止。
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
dict: 操作结果,如 {"status": "success"} 或 {"status": "error", "msg": "BUSY"}。
|
|
309
|
+
"""
|
|
310
|
+
if self._is_switching:
|
|
311
|
+
return {"status": "error", "msg": "BUSY"}
|
|
312
|
+
self._is_switching = True
|
|
313
|
+
try:
|
|
314
|
+
if status:
|
|
315
|
+
if 'start' in self.events:
|
|
316
|
+
start_func = self.events['start']
|
|
317
|
+
stop_func = self.events.get('stop')
|
|
318
|
+
if self.use_process:
|
|
319
|
+
if self.active_worker and self.active_worker.is_alive():
|
|
320
|
+
self._force_stop_worker()
|
|
321
|
+
self.stop_event.clear()
|
|
322
|
+
self.active_worker = multiprocessing.Process(
|
|
323
|
+
target=self._worker_wrapper,
|
|
324
|
+
args=(
|
|
325
|
+
self._callback_argument(start_func),
|
|
326
|
+
self._callback_argument(stop_func),
|
|
327
|
+
self.msg_queue,
|
|
328
|
+
self.stop_event,
|
|
329
|
+
),
|
|
330
|
+
daemon=True
|
|
331
|
+
)
|
|
332
|
+
self._start_process(self.active_worker)
|
|
333
|
+
threading.Thread(target=self._monitor_worker, args=(self.active_worker,), daemon=True).start()
|
|
334
|
+
else:
|
|
335
|
+
self._force_stop_worker()
|
|
336
|
+
return {"status": "success"}
|
|
337
|
+
finally:
|
|
338
|
+
self._is_switching = False
|
|
339
|
+
|
|
340
|
+
def _force_stop_worker(self):
|
|
341
|
+
"""强制停止工作进程。
|
|
342
|
+
|
|
343
|
+
依次尝试: 发送停止事件 -> 等待退出 -> terminate -> kill,确保进程被终止。
|
|
344
|
+
"""
|
|
345
|
+
if self.active_worker:
|
|
346
|
+
worker_to_stop = self.active_worker
|
|
347
|
+
self.active_worker = None
|
|
348
|
+
if self.use_process:
|
|
349
|
+
if worker_to_stop.is_alive():
|
|
350
|
+
self.stop_event.set()
|
|
351
|
+
worker_to_stop.join(timeout=3.0)
|
|
352
|
+
|
|
353
|
+
if worker_to_stop.is_alive():
|
|
354
|
+
worker_to_stop.terminate()
|
|
355
|
+
worker_to_stop.join(timeout=1.0)
|
|
356
|
+
if worker_to_stop.is_alive():
|
|
357
|
+
worker_to_stop.kill()
|
|
358
|
+
worker_to_stop.join()
|
|
359
|
+
self.stop_event.clear()
|
|
360
|
+
|
|
361
|
+
def drag_window(self):
|
|
362
|
+
"""启动窗口拖拽操作,供前端鼠标拖拽事件调用。"""
|
|
363
|
+
if self.webview_instance:
|
|
364
|
+
try:
|
|
365
|
+
self.webview_instance.start_drag()
|
|
366
|
+
except:
|
|
367
|
+
try:
|
|
368
|
+
self.webview_instance.move_window()
|
|
369
|
+
except:
|
|
370
|
+
pass
|
|
371
|
+
|
|
372
|
+
def hide_window(self):
|
|
373
|
+
"""隐藏或显示窗口,供前端隐藏按钮调用。"""
|
|
374
|
+
self.toggle_visible()
|
|
375
|
+
|
|
376
|
+
def resize_ui(self, expand):
|
|
377
|
+
"""调整窗口大小。
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
expand: True 展开为大窗口 (300x450),False 收缩为小窗口 (240x76)。
|
|
381
|
+
"""
|
|
382
|
+
if self.webview_instance:
|
|
383
|
+
self.webview_instance.resize(300 if expand else 240, 450 if expand else 76)
|
|
384
|
+
|
|
385
|
+
def get_init_data(self):
|
|
386
|
+
"""获取窗口初始化数据。
|
|
387
|
+
|
|
388
|
+
标记窗口为就绪状态,并返回标题和图标的 Base64 数据。
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
dict: 包含 title(窗口标题)和 icon_b64(图标 Base64 编码)的字典。
|
|
392
|
+
"""
|
|
393
|
+
self._is_ready = True
|
|
394
|
+
extension = os.path.splitext(self.icon_path)[1].lower()
|
|
395
|
+
icon_mime = {
|
|
396
|
+
".ico": "image/x-icon",
|
|
397
|
+
".jpg": "image/jpeg",
|
|
398
|
+
".jpeg": "image/jpeg",
|
|
399
|
+
".gif": "image/gif",
|
|
400
|
+
".svg": "image/svg+xml",
|
|
401
|
+
".webp": "image/webp",
|
|
402
|
+
}.get(extension, "image/png")
|
|
403
|
+
try:
|
|
404
|
+
if not os.path.exists(self.icon_path):
|
|
405
|
+
return {"title": self.ui_title, "icon_b64": "", "icon_mime": icon_mime}
|
|
406
|
+
with open(self.icon_path, "rb") as f:
|
|
407
|
+
b64 = base64.b64encode(f.read()).decode('utf-8')
|
|
408
|
+
return {"title": self.ui_title, "icon_b64": b64, "icon_mime": icon_mime}
|
|
409
|
+
except:
|
|
410
|
+
return {"title": self.ui_title, "icon_b64": "", "icon_mime": icon_mime}
|
|
411
|
+
|
|
412
|
+
def _fix_window_once(self):
|
|
413
|
+
"""通过 Win32 API 修复窗口样式。
|
|
414
|
+
|
|
415
|
+
移除标题栏、边框和系统菜单,设置为工具窗口样式以隐藏任务栏图标。
|
|
416
|
+
"""
|
|
417
|
+
GWL_STYLE, GWL_EXSTYLE = -16, -20
|
|
418
|
+
WS_CAPTION, WS_THICKFRAME, WS_SYSMENU = 0x00C00000, 0x00040000, 0x00080000
|
|
419
|
+
WS_EX_TOOLWINDOW, WS_EX_APPWINDOW = 0x00000080, 0x00040000
|
|
420
|
+
hwnd = ctypes.windll.user32.FindWindowW(None, 'AScriptControlPanel')
|
|
421
|
+
if hwnd:
|
|
422
|
+
style = ctypes.windll.user32.GetWindowLongW(hwnd, GWL_STYLE)
|
|
423
|
+
ctypes.windll.user32.SetWindowLongW(hwnd, GWL_STYLE, style & ~WS_CAPTION & ~WS_THICKFRAME & ~WS_SYSMENU)
|
|
424
|
+
ex_style = ctypes.windll.user32.GetWindowLongW(hwnd, GWL_EXSTYLE)
|
|
425
|
+
ctypes.windll.user32.SetWindowLongW(hwnd, GWL_EXSTYLE, (ex_style | WS_EX_TOOLWINDOW) & ~WS_EX_APPWINDOW)
|
|
426
|
+
ctypes.windll.user32.SetWindowPos(hwnd, 0, 0, 0, 0, 0, 0x0020 | 0x0002 | 0x0001 | 0x0004)
|
|
427
|
+
|
|
428
|
+
def toggle_visible(self, icon=None, item=None):
|
|
429
|
+
"""切换窗口的显示/隐藏状态。
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
icon: 系统托盘图标对象(由 pystray 回调传入,可选)。
|
|
433
|
+
item: 菜单项对象(由 pystray 回调传入,可选)。
|
|
434
|
+
"""
|
|
435
|
+
if self.webview_instance:
|
|
436
|
+
if self._is_hidden:
|
|
437
|
+
self.webview_instance.show()
|
|
438
|
+
self._is_hidden = False
|
|
439
|
+
else:
|
|
440
|
+
self.webview_instance.hide()
|
|
441
|
+
self._is_hidden = True
|
|
442
|
+
|
|
443
|
+
def _setup_tray(self):
|
|
444
|
+
"""初始化并启动系统托盘图标。
|
|
445
|
+
|
|
446
|
+
创建包含"显示/隐藏"和"彻底退出"菜单项的系统托盘图标。
|
|
447
|
+
"""
|
|
448
|
+
try:
|
|
449
|
+
img = Image.open(self.icon_path)
|
|
450
|
+
menu = pystray.Menu(pystray.MenuItem("显示/隐藏", self.toggle_visible, default=True),
|
|
451
|
+
pystray.MenuItem("彻底退出", lambda: os._exit(0)))
|
|
452
|
+
tray = pystray.Icon("AScriptTray", img, self.ui_title, menu)
|
|
453
|
+
tray.run()
|
|
454
|
+
except:
|
|
455
|
+
pass
|
|
456
|
+
|
|
457
|
+
def _auto_start_logic(self):
|
|
458
|
+
"""自动启动逻辑。
|
|
459
|
+
|
|
460
|
+
等待 WebView 就绪后,自动触发脚本启动,用于 auto_start=True 的场景。
|
|
461
|
+
"""
|
|
462
|
+
while not self._is_ready:
|
|
463
|
+
time.sleep(0.1)
|
|
464
|
+
if self.webview_instance:
|
|
465
|
+
self.webview_instance.evaluate_js("if(window.syncStatus){window.syncStatus(true);}")
|
|
466
|
+
self.on_toggle_click(True)
|
|
467
|
+
|
|
468
|
+
def show(self, title: str = "AScriptControlPanel", debug=False, width: int = 240, height: int = 76, **kwargs):
|
|
469
|
+
"""显示浮动控制窗口并进入事件循环。
|
|
470
|
+
|
|
471
|
+
启动系统托盘、窗口样式修复和可选的自动启动逻辑,然后调用父类 show 方法。
|
|
472
|
+
仅在主进程中有效,子进程调用会直接返回。
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
title: 窗口标题,默认为 "AScriptControlPanel"。
|
|
476
|
+
debug: 是否启用 WebView 调试模式,默认为 False。
|
|
477
|
+
width: 窗口宽度(像素),默认为 240。
|
|
478
|
+
height: 窗口高度(像素),默认为 76。
|
|
479
|
+
**kwargs: 传递给 WebWindow.show 的额外参数。
|
|
480
|
+
"""
|
|
481
|
+
if multiprocessing.current_process().name != 'MainProcess':
|
|
482
|
+
return
|
|
483
|
+
threading.Thread(target=self._setup_tray, daemon=True).start()
|
|
484
|
+
threading.Timer(1.5, self._fix_window_once).start()
|
|
485
|
+
if self.auto_start:
|
|
486
|
+
threading.Thread(target=self._auto_start_logic, daemon=True).start()
|
|
487
|
+
kwargs.update({'frameless': True, 'on_top': True, 'resizable': False, 'background_color': '#1a1a1a'})
|
|
488
|
+
super().show(title=title, debug=debug, width=width, height=height, **kwargs)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
|
|
2
|
+
from .web_window import WebWindow
|
|
3
|
+
from .. import license
|
|
4
|
+
from ..system import KeyValue,R
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LicenseWindow(WebWindow):
|
|
8
|
+
"""授权验证窗口类。
|
|
9
|
+
|
|
10
|
+
提供应用授权验证的 Web 窗口界面,支持查询应用信息、激活码验证和启动授权检查。
|
|
11
|
+
继承自 WebWindow,窗口关闭后返回验证结果(True/False)。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, app_id: str):
|
|
15
|
+
"""初始化授权验证窗口。
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
app_id: 需要验证授权的应用唯一标识符。
|
|
19
|
+
"""
|
|
20
|
+
self.app_id = app_id
|
|
21
|
+
self.license_data = None
|
|
22
|
+
self.app_info = None
|
|
23
|
+
# 默认返回 False,确保任何非正常退出都视为失败
|
|
24
|
+
self._return_value = False
|
|
25
|
+
|
|
26
|
+
_root = R.internal("windows","tools","web") #paths.get_path("ascript/windows/tools/web/")
|
|
27
|
+
_filepath = R.internal("windows","tools","web","license_window.html") #paths.get_path("ascript/windows/tools/web/license_window.html")
|
|
28
|
+
print(_filepath)
|
|
29
|
+
super().__init__(_filepath, project_root=_root)
|
|
30
|
+
|
|
31
|
+
self.expose(self.get_app_info)
|
|
32
|
+
self.expose(self.get_cached_key)
|
|
33
|
+
self.expose(self.activate_key)
|
|
34
|
+
self.expose(self.start_app)
|
|
35
|
+
|
|
36
|
+
def get_app_info(self):
|
|
37
|
+
"""获取当前应用的详细信息。
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
dict: 应用信息字典;未找到时返回 {'name': '没有找到该程序'}。
|
|
41
|
+
"""
|
|
42
|
+
res = license.achttp.get_apps(self.app_id)
|
|
43
|
+
if res and len(res) > 0:
|
|
44
|
+
self.app_info = res[0]
|
|
45
|
+
return self.app_info
|
|
46
|
+
return {'name': '没有找到该程序'}
|
|
47
|
+
|
|
48
|
+
def activate_key(self, key: str):
|
|
49
|
+
"""使用激活码进行授权验证。
|
|
50
|
+
|
|
51
|
+
验证成功后将激活码缓存到本地,失败则清空授权数据。
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
key: 用户输入的激活码字符串。
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
dict: 服务端返回的验证结果,成功时 code 为 1 并包含 data 字段。
|
|
58
|
+
"""
|
|
59
|
+
# 这里的 res 结构需包含 {code: 1, data: {status: 1...}}
|
|
60
|
+
res = license.achttp.get_card(self.app_id, key)
|
|
61
|
+
if res and res.get('code') == 1:
|
|
62
|
+
self.license_data = res.get('data')
|
|
63
|
+
KeyValue.save("license_cache_no", key)
|
|
64
|
+
else:
|
|
65
|
+
self.license_data = None # 验证失败必须清空旧数据
|
|
66
|
+
return res
|
|
67
|
+
|
|
68
|
+
def start_app(self):
|
|
69
|
+
"""执行最终的授权校验并决定是否允许启动。
|
|
70
|
+
|
|
71
|
+
依次检查应用信息加载状态、免费版直接放行、授权版验证 license 状态。
|
|
72
|
+
验证通过后关闭窗口并设置返回值为 True。
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
bool|dict: 验证通过返回 True;失败返回包含 error 信息的字典。
|
|
76
|
+
"""
|
|
77
|
+
# print("Final security check before launch...")
|
|
78
|
+
|
|
79
|
+
# 1. 验证 app_info
|
|
80
|
+
if not self.app_info:
|
|
81
|
+
print("Error: App info not loaded.")
|
|
82
|
+
return {"error": "初始化未完成"}
|
|
83
|
+
|
|
84
|
+
# 2. 免费版直接放行
|
|
85
|
+
if self.app_info.get('is_free') == 1:
|
|
86
|
+
# print("Mode: Free version. Access granted.")
|
|
87
|
+
self._return_value = True
|
|
88
|
+
self.close()
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
# 3. 授权版严格校验
|
|
92
|
+
# 核心:必须 status 存在且明确等于 1
|
|
93
|
+
if self.license_data and self.license_data.get('status') == 1:
|
|
94
|
+
# print("Mode: Professional. License verified.")
|
|
95
|
+
self._return_value = True
|
|
96
|
+
self.close()
|
|
97
|
+
return True
|
|
98
|
+
else:
|
|
99
|
+
# 如果 status 是 2 (过期) 或其他,拦截并告知前端
|
|
100
|
+
current_status = self.license_data.get('status') if self.license_data else "None"
|
|
101
|
+
print(f"Access Denied: License status is {current_status}")
|
|
102
|
+
self._return_value = False
|
|
103
|
+
return {"error": "授权已过期或无效,请重新激活"}
|
|
104
|
+
|
|
105
|
+
def get_cached_key(self):
|
|
106
|
+
"""获取本地缓存的激活码。
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
str: 缓存的激活码字符串,若无缓存则返回空字符串。
|
|
110
|
+
"""
|
|
111
|
+
return KeyValue.get("license_cache_no", "")
|