super-aide 0.1.1__py3-none-any.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.
- super_aide/__init__.py +8 -0
- super_aide/__main__.py +3 -0
- super_aide/_release/constraints.txt +173 -0
- super_aide/_release/release.json +5 -0
- super_aide/app.py +414 -0
- super_aide/autostart.py +49 -0
- super_aide/config.py +150 -0
- super_aide/cu/__init__.py +5 -0
- super_aide/cu/actions.py +555 -0
- super_aide/cu/browser.py +91 -0
- super_aide/cu/elements.py +227 -0
- super_aide/cu/guard.py +198 -0
- super_aide/cu/guide.py +206 -0
- super_aide/cu/inputs.py +223 -0
- super_aide/cu/keys.py +45 -0
- super_aide/cu/observe.py +200 -0
- super_aide/cu/ocr.py +186 -0
- super_aide/cu/server.py +425 -0
- super_aide/cu/signals.py +37 -0
- super_aide/engines/__init__.py +5 -0
- super_aide/engines/base.py +56 -0
- super_aide/engines/browser_use.py +89 -0
- super_aide/engines/claude.py +386 -0
- super_aide/engines/codex.py +277 -0
- super_aide/engines/codex_hook.py +33 -0
- super_aide/engines/computer.py +131 -0
- super_aide/engines/dsh.py +539 -0
- super_aide/engines/experience.py +48 -0
- super_aide/engines/hub.py +664 -0
- super_aide/engines/prompts.py +66 -0
- super_aide/experience/__init__.py +6 -0
- super_aide/experience/codex_hook.py +60 -0
- super_aide/experience/embed.py +198 -0
- super_aide/experience/learn.py +145 -0
- super_aide/experience/recall.py +210 -0
- super_aide/experience/semantic.py +118 -0
- super_aide/experience/server.py +44 -0
- super_aide/experience/store.py +184 -0
- super_aide/history.py +104 -0
- super_aide/hotkeys.py +105 -0
- super_aide/i18n.py +68 -0
- super_aide/locales/__init__.py +1 -0
- super_aide/locales/en_core.py +136 -0
- super_aide/locales/en_engines.py +65 -0
- super_aide/paths.py +26 -0
- super_aide/router/__init__.py +5 -0
- super_aide/router/catalog.py +179 -0
- super_aide/router/evidence.py +212 -0
- super_aide/router/judge.py +82 -0
- super_aide/router/policy.py +630 -0
- super_aide/safety.py +122 -0
- super_aide/setup/__init__.py +1 -0
- super_aide/setup/configure.py +282 -0
- super_aide/setup/detect.py +323 -0
- super_aide/setup/flow.py +91 -0
- super_aide/setup/install.py +333 -0
- super_aide/setup/winenv.py +54 -0
- super_aide/shell/__init__.py +1 -0
- super_aide/shell/controller.py +431 -0
- super_aide/shell/history_window.py +56 -0
- super_aide/shell/killswitch.py +72 -0
- super_aide/shell/overlay.py +78 -0
- super_aide/shell/panel.py +140 -0
- super_aide/shell/screens.py +76 -0
- super_aide/shell/settings.py +158 -0
- super_aide/shell/setup_window.py +304 -0
- super_aide/shell/tray.py +72 -0
- super_aide/shell/webwindow.py +89 -0
- super_aide/snapshot/__init__.py +1 -0
- super_aide/snapshot/chromium.py +40 -0
- super_aide/snapshot/collector.py +443 -0
- super_aide/snapshot/content.py +59 -0
- super_aide/snapshot/redact.py +31 -0
- super_aide/snapshot/system.py +143 -0
- super_aide/snapshot/uia.py +475 -0
- super_aide/snapshot/uiawalk.py +170 -0
- super_aide/snapshot/win32.py +163 -0
- super_aide/ui/super_aide.ico +0 -0
- super_aide/ui/web/app.js +518 -0
- super_aide/ui/web/history.css +138 -0
- super_aide/ui/web/history.html +34 -0
- super_aide/ui/web/history.js +240 -0
- super_aide/ui/web/i18n-en.js +200 -0
- super_aide/ui/web/i18n.js +45 -0
- super_aide/ui/web/index.html +101 -0
- super_aide/ui/web/settings.css +513 -0
- super_aide/ui/web/settings.html +127 -0
- super_aide/ui/web/settings.js +705 -0
- super_aide/ui/web/setup.css +176 -0
- super_aide/ui/web/setup.html +51 -0
- super_aide/ui/web/setup.js +349 -0
- super_aide/ui/web/style.css +596 -0
- super_aide/ui/web/vendor/marked.min.js +69 -0
- super_aide/update.py +155 -0
- super_aide/workspace/AGENTS.md +44 -0
- super_aide/workspace/CLAUDE.md +1 -0
- super_aide/workspace/plugin/.claude-plugin/plugin.json +5 -0
- super_aide/workspace/plugin/skills/disk-cleanup/SKILL.md +69 -0
- super_aide/workspace/plugin/skills/jupyter-debug/SKILL.md +42 -0
- super_aide/workspace/plugin/skills/network-proxy/SKILL.md +61 -0
- super_aide/workspace/plugin/skills/office-automation/SKILL.md +48 -0
- super_aide/workspace/plugin/skills/python-env/SKILL.md +37 -0
- super_aide/workspace/plugin/skills/uninstall-software/SKILL.md +57 -0
- super_aide-0.1.1.dist-info/METADATA +158 -0
- super_aide-0.1.1.dist-info/RECORD +108 -0
- super_aide-0.1.1.dist-info/WHEEL +4 -0
- super_aide-0.1.1.dist-info/entry_points.txt +2 -0
- super_aide-0.1.1.dist-info/licenses/LICENSE +21 -0
super_aide/__init__.py
ADDED
super_aide/__main__.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# This file was autogenerated by uv via the following command:
|
|
2
|
+
# uv export --project D:\a\super-aide\super-aide --frozen --no-dev --no-hashes --no-emit-project --output-file D:\a\super-aide\super-aide\src\super_aide\_release\constraints.txt
|
|
3
|
+
annotated-types==0.8.0
|
|
4
|
+
# via pydantic
|
|
5
|
+
anyio==4.15.1
|
|
6
|
+
# via
|
|
7
|
+
# claude-agent-sdk
|
|
8
|
+
# httpx2
|
|
9
|
+
# mcp
|
|
10
|
+
# sse-starlette
|
|
11
|
+
# starlette
|
|
12
|
+
attrs==26.1.0
|
|
13
|
+
# via
|
|
14
|
+
# jsonschema
|
|
15
|
+
# referencing
|
|
16
|
+
cffi==2.1.1 ; platform_python_implementation != 'PyPy'
|
|
17
|
+
# via cryptography
|
|
18
|
+
claude-agent-sdk==0.2.164
|
|
19
|
+
# via super-aide
|
|
20
|
+
click==8.5.0 ; sys_platform != 'emscripten'
|
|
21
|
+
# via uvicorn
|
|
22
|
+
comtypes==1.4.17
|
|
23
|
+
# via uiautomation
|
|
24
|
+
cryptography==50.0.2
|
|
25
|
+
# via pyjwt
|
|
26
|
+
flatbuffers==25.12.19
|
|
27
|
+
# via onnxruntime
|
|
28
|
+
h11==0.16.0 ; sys_platform != 'emscripten'
|
|
29
|
+
# via
|
|
30
|
+
# httpcore2
|
|
31
|
+
# uvicorn
|
|
32
|
+
httpcore2==2.13.1 ; sys_platform != 'emscripten'
|
|
33
|
+
# via httpx2
|
|
34
|
+
httpx2==2.13.1
|
|
35
|
+
# via mcp
|
|
36
|
+
httpx2-jsfetch==1.0 ; sys_platform == 'emscripten'
|
|
37
|
+
# via httpx2
|
|
38
|
+
idna==3.20
|
|
39
|
+
# via
|
|
40
|
+
# anyio
|
|
41
|
+
# httpx2
|
|
42
|
+
jsonschema==4.26.0
|
|
43
|
+
# via
|
|
44
|
+
# claude-agent-sdk
|
|
45
|
+
# mcp
|
|
46
|
+
jsonschema-specifications==2025.9.1
|
|
47
|
+
# via jsonschema
|
|
48
|
+
mcp==2.3.0
|
|
49
|
+
# via
|
|
50
|
+
# claude-agent-sdk
|
|
51
|
+
# super-aide
|
|
52
|
+
mcp-types==2.3.0
|
|
53
|
+
# via mcp
|
|
54
|
+
mss==10.2.0
|
|
55
|
+
# via super-aide
|
|
56
|
+
numpy==2.5.3
|
|
57
|
+
# via
|
|
58
|
+
# onnxruntime
|
|
59
|
+
# super-aide
|
|
60
|
+
onnxruntime==1.31.0
|
|
61
|
+
# via super-aide
|
|
62
|
+
openai-codex==0.160.1
|
|
63
|
+
# via super-aide
|
|
64
|
+
openai-codex-cli-bin==0.160.1
|
|
65
|
+
# via openai-codex
|
|
66
|
+
opentelemetry-api==1.45.1
|
|
67
|
+
# via mcp
|
|
68
|
+
packaging==26.3
|
|
69
|
+
# via
|
|
70
|
+
# onnxruntime
|
|
71
|
+
# openai-codex
|
|
72
|
+
pillow==12.3.0
|
|
73
|
+
# via super-aide
|
|
74
|
+
protobuf==7.36.2
|
|
75
|
+
# via onnxruntime
|
|
76
|
+
psutil==7.2.2
|
|
77
|
+
# via super-aide
|
|
78
|
+
pycparser==3.0 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy'
|
|
79
|
+
# via cffi
|
|
80
|
+
pydantic==2.13.5
|
|
81
|
+
# via
|
|
82
|
+
# mcp
|
|
83
|
+
# mcp-types
|
|
84
|
+
# openai-codex
|
|
85
|
+
pydantic-core==2.46.5
|
|
86
|
+
# via pydantic
|
|
87
|
+
pyjwt==2.15.1
|
|
88
|
+
# via mcp
|
|
89
|
+
pyside6==6.11.2
|
|
90
|
+
# via super-aide
|
|
91
|
+
pyside6-addons==6.11.2
|
|
92
|
+
# via pyside6
|
|
93
|
+
pyside6-essentials==6.11.2
|
|
94
|
+
# via
|
|
95
|
+
# pyside6
|
|
96
|
+
# pyside6-addons
|
|
97
|
+
python-multipart==0.0.32
|
|
98
|
+
# via mcp
|
|
99
|
+
pywin32==312
|
|
100
|
+
# via
|
|
101
|
+
# mcp
|
|
102
|
+
# super-aide
|
|
103
|
+
referencing==0.37.0
|
|
104
|
+
# via
|
|
105
|
+
# jsonschema
|
|
106
|
+
# jsonschema-specifications
|
|
107
|
+
rpds-py==2026.9.1
|
|
108
|
+
# via
|
|
109
|
+
# jsonschema
|
|
110
|
+
# referencing
|
|
111
|
+
sentencepiece==0.2.2
|
|
112
|
+
# via super-aide
|
|
113
|
+
shiboken6==6.11.2
|
|
114
|
+
# via
|
|
115
|
+
# pyside6
|
|
116
|
+
# pyside6-addons
|
|
117
|
+
# pyside6-essentials
|
|
118
|
+
sniffio==1.3.1
|
|
119
|
+
# via claude-agent-sdk
|
|
120
|
+
sse-starlette==3.5.0
|
|
121
|
+
# via mcp
|
|
122
|
+
starlette==1.7.0
|
|
123
|
+
# via
|
|
124
|
+
# mcp
|
|
125
|
+
# sse-starlette
|
|
126
|
+
tomli-w==1.2.0
|
|
127
|
+
# via super-aide
|
|
128
|
+
truststore==0.10.4 ; sys_platform != 'emscripten'
|
|
129
|
+
# via
|
|
130
|
+
# httpcore2
|
|
131
|
+
# httpx2
|
|
132
|
+
typing-extensions==4.16.0
|
|
133
|
+
# via
|
|
134
|
+
# anyio
|
|
135
|
+
# httpx2
|
|
136
|
+
# mcp
|
|
137
|
+
# mcp-types
|
|
138
|
+
# opentelemetry-api
|
|
139
|
+
# pydantic
|
|
140
|
+
# pydantic-core
|
|
141
|
+
# referencing
|
|
142
|
+
# starlette
|
|
143
|
+
# typing-inspection
|
|
144
|
+
# winrt-runtime
|
|
145
|
+
typing-inspection==0.4.4
|
|
146
|
+
# via
|
|
147
|
+
# mcp
|
|
148
|
+
# pydantic
|
|
149
|
+
uiautomation==2.0.29
|
|
150
|
+
# via super-aide
|
|
151
|
+
uvicorn==0.54.0 ; sys_platform != 'emscripten'
|
|
152
|
+
# via mcp
|
|
153
|
+
winrt-runtime==3.2.1
|
|
154
|
+
# via
|
|
155
|
+
# super-aide
|
|
156
|
+
# winrt-windows-foundation
|
|
157
|
+
# winrt-windows-foundation-collections
|
|
158
|
+
# winrt-windows-globalization
|
|
159
|
+
# winrt-windows-graphics-imaging
|
|
160
|
+
# winrt-windows-media-ocr
|
|
161
|
+
# winrt-windows-storage-streams
|
|
162
|
+
winrt-windows-foundation==3.2.1
|
|
163
|
+
# via super-aide
|
|
164
|
+
winrt-windows-foundation-collections==3.2.1
|
|
165
|
+
# via super-aide
|
|
166
|
+
winrt-windows-globalization==3.2.1
|
|
167
|
+
# via super-aide
|
|
168
|
+
winrt-windows-graphics-imaging==3.2.1
|
|
169
|
+
# via super-aide
|
|
170
|
+
winrt-windows-media-ocr==3.2.1
|
|
171
|
+
# via super-aide
|
|
172
|
+
winrt-windows-storage-streams==3.2.1
|
|
173
|
+
# via super-aide
|
super_aide/app.py
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
"""入口:单实例、前置检查(准备窗口)、托盘、全局快捷键、引擎预热、模型发现,以及设置的实时生效。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
import logging
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
from logging.handlers import RotatingFileHandler
|
|
12
|
+
|
|
13
|
+
from PySide6.QtCore import QObject, QTimer, Signal
|
|
14
|
+
from PySide6.QtWidgets import QApplication, QSystemTrayIcon
|
|
15
|
+
|
|
16
|
+
from . import __version__, autostart, config, history, hotkeys, i18n, paths, update
|
|
17
|
+
from .engines import prompts
|
|
18
|
+
from .engines.hub import EngineHub
|
|
19
|
+
from .i18n import app_name, tr
|
|
20
|
+
from .router import EngineRouter
|
|
21
|
+
from .setup import detect, flow
|
|
22
|
+
from .shell.controller import Controller
|
|
23
|
+
from .shell.history_window import HistoryWindow
|
|
24
|
+
from .shell.settings import SettingsWindow
|
|
25
|
+
from .shell.setup_window import SetupResult, SetupWindow
|
|
26
|
+
from .shell.tray import Tray, orb_icon
|
|
27
|
+
from .snapshot.collector import Collector
|
|
28
|
+
from .snapshot.uia import UiaWorker
|
|
29
|
+
|
|
30
|
+
log = logging.getLogger("super_aide")
|
|
31
|
+
ERROR_ALREADY_EXISTS = 183
|
|
32
|
+
WAKE_EVENT = "Local\\SuperAideWake" # 再打开一次时,叫醒正在运行的那个
|
|
33
|
+
EVENT_MODIFY_STATE, INFINITE, WAIT_OBJECT_0 = 0x0002, 0xFFFFFFFF, 0
|
|
34
|
+
kernel32 = ctypes.windll.kernel32
|
|
35
|
+
kernel32.CreateEventW.restype = ctypes.c_void_p
|
|
36
|
+
kernel32.CreateEventW.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_int, ctypes.c_wchar_p]
|
|
37
|
+
kernel32.OpenEventW.restype = ctypes.c_void_p
|
|
38
|
+
kernel32.OpenEventW.argtypes = [ctypes.c_uint, ctypes.c_int, ctypes.c_wchar_p]
|
|
39
|
+
kernel32.SetEvent.argtypes = [ctypes.c_void_p]
|
|
40
|
+
kernel32.CloseHandle.argtypes = [ctypes.c_void_p]
|
|
41
|
+
kernel32.WaitForSingleObject.argtypes = [ctypes.c_void_p, ctypes.c_uint]
|
|
42
|
+
kernel32.WaitForSingleObject.restype = ctypes.c_uint
|
|
43
|
+
CREATE_NO_WINDOW, CREATE_NEW_CONSOLE, DETACHED_PROCESS = 0x08000000, 0x00000010, 0x00000008
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _has_console() -> bool:
|
|
47
|
+
return bool(ctypes.windll.kernel32.GetConsoleWindow())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _setup_logging() -> None:
|
|
51
|
+
handler = RotatingFileHandler(paths.LOGS / "super_aide.log", maxBytes=2 * 1024 * 1024,
|
|
52
|
+
backupCount=3, encoding="utf-8")
|
|
53
|
+
handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s"))
|
|
54
|
+
root = logging.getLogger()
|
|
55
|
+
root.setLevel(logging.INFO)
|
|
56
|
+
root.addHandler(handler)
|
|
57
|
+
if sys.stderr is not None: # pythonw(开机自启)下没有 stderr
|
|
58
|
+
root.addHandler(logging.StreamHandler())
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _hide_child_consoles() -> None:
|
|
62
|
+
"""没有控制台时(开机自启用 pythonw),两边 SDK 启动的 CLI 和它们再启动的命令行程序
|
|
63
|
+
会各弹出一个黑窗口:统一加上 CREATE_NO_WINDOW。"""
|
|
64
|
+
if _has_console():
|
|
65
|
+
return
|
|
66
|
+
original = subprocess.Popen.__init__
|
|
67
|
+
|
|
68
|
+
def init(self, *args, creationflags=0, **kwargs):
|
|
69
|
+
if not creationflags & (CREATE_NEW_CONSOLE | DETACHED_PROCESS):
|
|
70
|
+
creationflags |= CREATE_NO_WINDOW
|
|
71
|
+
original(self, *args, creationflags=creationflags, **kwargs)
|
|
72
|
+
|
|
73
|
+
subprocess.Popen.__init__ = init
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _sync_workspace() -> None:
|
|
77
|
+
"""把仓库里的扩展包模板(共享指令、技能)同步到 ~/.super_aide/workspace。"""
|
|
78
|
+
src = paths.WORKSPACE_TEMPLATE
|
|
79
|
+
if not src.exists():
|
|
80
|
+
return
|
|
81
|
+
for path in src.rglob("*"):
|
|
82
|
+
if path.is_file():
|
|
83
|
+
target = paths.WORKSPACE / path.relative_to(src)
|
|
84
|
+
if not target.exists() or path.stat().st_mtime > target.stat().st_mtime:
|
|
85
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
shutil.copy2(path, target)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _single_instance() -> bool:
|
|
90
|
+
ctypes.windll.kernel32.CreateMutexW(None, False, "Local\\SuperAideSingleton")
|
|
91
|
+
return ctypes.windll.kernel32.GetLastError() != ERROR_ALREADY_EXISTS
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _wake_running() -> None:
|
|
95
|
+
"""已经在运行了(比如你又从开始菜单点了一次):叫醒它,让它提示一下自己在哪。"""
|
|
96
|
+
handle = kernel32.OpenEventW(EVENT_MODIFY_STATE, False, WAKE_EVENT)
|
|
97
|
+
if handle:
|
|
98
|
+
kernel32.SetEvent(handle)
|
|
99
|
+
kernel32.CloseHandle(handle)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Wakeup(QObject):
|
|
103
|
+
"""等别的副本来叫醒(Windows 命名事件)。从后台线程发出的信号,Qt 自动排队到主线程。"""
|
|
104
|
+
|
|
105
|
+
woken = Signal()
|
|
106
|
+
|
|
107
|
+
def __init__(self) -> None:
|
|
108
|
+
super().__init__()
|
|
109
|
+
self.handle = kernel32.CreateEventW(None, False, False, WAKE_EVENT) # 自动复位:叫一次响一次
|
|
110
|
+
if self.handle:
|
|
111
|
+
threading.Thread(target=self._wait, name="wakeup", daemon=True).start()
|
|
112
|
+
|
|
113
|
+
def _wait(self) -> None:
|
|
114
|
+
while kernel32.WaitForSingleObject(self.handle, INFINITE) == WAIT_OBJECT_0:
|
|
115
|
+
self.woken.emit()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _displays(app: QApplication) -> list[dict]:
|
|
119
|
+
return [{"name": s.name(), "size": [round(s.size().width() * s.devicePixelRatio()),
|
|
120
|
+
round(s.size().height() * s.devicePixelRatio())],
|
|
121
|
+
"scale": s.devicePixelRatio(), "primary": s == app.primaryScreen()} for s in app.screens()]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _routing(cfg: config.Config) -> tuple:
|
|
125
|
+
return config.to_dict(cfg)["engines"], cfg.pin_engine
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def apply_choices(cfg: config.Config, result: SetupResult) -> None:
|
|
129
|
+
"""准备窗口里的选择落到配置上:要用 DSH 就打开它(只开不关:关掉在设置里),
|
|
130
|
+
你填的模型(填了就只用它,其它的在设置里还能再打开)。"""
|
|
131
|
+
cfg.setup_done = True
|
|
132
|
+
if result.use_dsh:
|
|
133
|
+
cfg.dsh.enabled = True
|
|
134
|
+
for name, model in result.models.items():
|
|
135
|
+
flow.only_model(cfg.engine(name), model)
|
|
136
|
+
config.save(cfg)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class SuperAide:
|
|
140
|
+
"""把各部分连起来。设置窗口里的改动经这里落到各部分上,立即生效。"""
|
|
141
|
+
|
|
142
|
+
def __init__(self, app: QApplication, cfg: config.Config, report: detect.Report) -> None:
|
|
143
|
+
self.app, self.cfg = app, cfg
|
|
144
|
+
self.collector = Collector(UiaWorker(), cfg.blocked_apps, _displays(app))
|
|
145
|
+
self.router = EngineRouter(cfg)
|
|
146
|
+
self.router.set_ready(report.usable(cfg.dsh.enabled)) # 没配好模型的引擎不参与
|
|
147
|
+
self.hub = EngineHub(self.router, prompts.shared_instructions(), cfg.blocklist_enabled)
|
|
148
|
+
self.ctrl = Controller(cfg, self.collector, self.hub)
|
|
149
|
+
self.hotkeys = hotkeys.HotkeyManager(self.ctrl.on_hotkey)
|
|
150
|
+
app.installNativeEventFilter(self.hotkeys)
|
|
151
|
+
self.tray = Tray(self.ctrl, app, cfg, self.open_settings, self.open_history)
|
|
152
|
+
self.settings: SettingsWindow | None = None # 第一次打开时再创建
|
|
153
|
+
self.history: HistoryWindow | None = None
|
|
154
|
+
self.setup: SetupWindow | None = None
|
|
155
|
+
#: 刚在准备窗口里配好的引擎 → 你填的模型(空 = 等发现模型时挑一个它的服务确实提供的)
|
|
156
|
+
self.fresh: dict[str, str] = {}
|
|
157
|
+
self.warm_pending = False # 设置里改了模型:关窗口时再预热,免得每拨一下开关都预热一次
|
|
158
|
+
self.hub.models_found.connect(self._on_models_found)
|
|
159
|
+
|
|
160
|
+
def start(self, announce: bool = False, updated: bool = False) -> None:
|
|
161
|
+
failed = self._register_hotkeys(self.cfg)
|
|
162
|
+
if failed:
|
|
163
|
+
self.tray.showMessage(app_name(), tr("快捷键 {keys} 注册失败(可能被其它程序占用),可以在设置里换一个。",
|
|
164
|
+
keys=tr("、").join(failed)), QSystemTrayIcon.Warning, 8000)
|
|
165
|
+
elif announce or updated:
|
|
166
|
+
self.announce(updated=updated)
|
|
167
|
+
try:
|
|
168
|
+
autostart.upgrade()
|
|
169
|
+
except OSError as e:
|
|
170
|
+
log.warning("更新开机自启的命令失败:%s", e)
|
|
171
|
+
self.hub.warm_up()
|
|
172
|
+
self.hub.discover_models()
|
|
173
|
+
threading.Thread(target=self._prune_sessions, name="prune", daemon=True).start()
|
|
174
|
+
log.info("Super Aide 已启动:%s 问答 / %s 操作", self.cfg.hotkey_ask, self.cfg.hotkey_act)
|
|
175
|
+
|
|
176
|
+
def announce(self, again: bool = False, updated: bool = False) -> None:
|
|
177
|
+
"""你自己打开 Super Aide 时(开机自启不提示):它只在托盘里,没有窗口,告诉你它在哪、怎么用。"""
|
|
178
|
+
if updated:
|
|
179
|
+
title = tr("{app}已更新到 {version}", app=app_name(), version=__version__)
|
|
180
|
+
else:
|
|
181
|
+
title = tr("{app}已经在运行", app=app_name()) if again else tr("{app}已在后台运行", app=app_name())
|
|
182
|
+
self.tray.showMessage(title, tr("按 {ask} 问答,{act} 操作。右键托盘图标可以打开设置。",
|
|
183
|
+
ask=self.cfg.hotkey_ask, act=self.cfg.hotkey_act),
|
|
184
|
+
QSystemTrayIcon.Information, 6000)
|
|
185
|
+
|
|
186
|
+
def apply_update(self, release: update.Release | None) -> str:
|
|
187
|
+
"""设置里点了更新:交给安装器(它等 Super Aide 退出后再装),自己退出。返回没能开始的原因。"""
|
|
188
|
+
try:
|
|
189
|
+
update.start_update(release)
|
|
190
|
+
except (update.UpdateError, OSError) as e:
|
|
191
|
+
log.warning("没能开始更新:%s", e)
|
|
192
|
+
return str(e)
|
|
193
|
+
log.info("开始更新到 %s,先退出", release.version if release else "最新版本")
|
|
194
|
+
QTimer.singleShot(300, self.app.quit)
|
|
195
|
+
return ""
|
|
196
|
+
|
|
197
|
+
def shutdown(self) -> None:
|
|
198
|
+
self.hotkeys.unregister_all()
|
|
199
|
+
self.ctrl.esc.stop()
|
|
200
|
+
self.hub.shutdown()
|
|
201
|
+
self.collector.close()
|
|
202
|
+
|
|
203
|
+
@staticmethod
|
|
204
|
+
def _prune_sessions() -> None:
|
|
205
|
+
cleaned = history.prune()
|
|
206
|
+
if cleaned:
|
|
207
|
+
log.info("清理了 %d 个 %d 天前的会话(只留下问答记录)", cleaned, history.KEEP_DAYS)
|
|
208
|
+
|
|
209
|
+
def open_history(self) -> None:
|
|
210
|
+
if self.history is None:
|
|
211
|
+
self.history = HistoryWindow(self.ctrl.theme)
|
|
212
|
+
self.history.open()
|
|
213
|
+
|
|
214
|
+
# ---------- 快捷键 ----------
|
|
215
|
+
|
|
216
|
+
def _register_hotkeys(self, cfg: config.Config) -> list[str]:
|
|
217
|
+
"""按 cfg 重新注册,返回没注册上的。"""
|
|
218
|
+
self.hotkeys.unregister_all()
|
|
219
|
+
failed = []
|
|
220
|
+
for name, spec in (("ask", cfg.hotkey_ask), ("act", cfg.hotkey_act)):
|
|
221
|
+
try:
|
|
222
|
+
if not self.hotkeys.register(name, spec):
|
|
223
|
+
failed.append(spec)
|
|
224
|
+
except ValueError as e:
|
|
225
|
+
failed.append(tr("{spec}({reason})", spec=spec, reason=e))
|
|
226
|
+
return failed
|
|
227
|
+
|
|
228
|
+
def check_hotkey(self, spec: str) -> str:
|
|
229
|
+
try:
|
|
230
|
+
return "" if self.hotkeys.available(spec) else tr("已被其它程序占用")
|
|
231
|
+
except ValueError as e:
|
|
232
|
+
return str(e)
|
|
233
|
+
|
|
234
|
+
def pause_hotkeys(self) -> None:
|
|
235
|
+
"""设置里录新快捷键时先放开,不然按下现在的快捷键会直接召唤出界面。"""
|
|
236
|
+
self.hotkeys.unregister_all()
|
|
237
|
+
|
|
238
|
+
def resume_hotkeys(self) -> None:
|
|
239
|
+
if not self.hotkeys.active:
|
|
240
|
+
self._register_hotkeys(self.cfg)
|
|
241
|
+
|
|
242
|
+
# ---------- 设置 ----------
|
|
243
|
+
|
|
244
|
+
def open_settings(self) -> None:
|
|
245
|
+
if self.settings is None:
|
|
246
|
+
self.settings = SettingsWindow(self)
|
|
247
|
+
self.settings.open()
|
|
248
|
+
|
|
249
|
+
def resolved_theme(self) -> str:
|
|
250
|
+
return self.ctrl.theme()
|
|
251
|
+
|
|
252
|
+
def apply_settings(self, new: config.Config) -> list[str]:
|
|
253
|
+
"""返回没能生效的项。快捷键注册不上时保留原来的。"""
|
|
254
|
+
errors = []
|
|
255
|
+
old = self.cfg
|
|
256
|
+
if (new.hotkey_ask, new.hotkey_act) != (old.hotkey_ask, old.hotkey_act) or not self.hotkeys.active:
|
|
257
|
+
failed = self._register_hotkeys(new)
|
|
258
|
+
if failed:
|
|
259
|
+
errors.append(tr("快捷键 {keys} 注册不上(可能被其它程序占用),还用原来的", keys=tr("、").join(failed)))
|
|
260
|
+
new.hotkey_ask, new.hotkey_act = old.hotkey_ask, old.hotkey_act
|
|
261
|
+
self._register_hotkeys(old)
|
|
262
|
+
routing_changed = _routing(new) != _routing(old)
|
|
263
|
+
language_changed = new.language != old.language
|
|
264
|
+
old.update_from(new)
|
|
265
|
+
self.collector.blocked = {a.lower() for a in old.blocked_apps}
|
|
266
|
+
if routing_changed:
|
|
267
|
+
self.hub.reconfigure()
|
|
268
|
+
self.warm_pending = True
|
|
269
|
+
config.save(old)
|
|
270
|
+
if language_changed:
|
|
271
|
+
self.apply_language()
|
|
272
|
+
self.tray.refresh(old)
|
|
273
|
+
self.ctrl.panel.call("setTheme", self.ctrl.theme())
|
|
274
|
+
return errors
|
|
275
|
+
|
|
276
|
+
def apply_language(self) -> None:
|
|
277
|
+
"""设置里换了语言:托盘、回答界面、各个窗口立刻换(设置窗口自己按保存结果重画)。"""
|
|
278
|
+
i18n.set_language(i18n.resolve(self.cfg.language))
|
|
279
|
+
self.ctrl.panel.call("setLang", i18n.lang())
|
|
280
|
+
for window in (self.settings, self.history, self.setup):
|
|
281
|
+
if window is not None:
|
|
282
|
+
window.retitle()
|
|
283
|
+
if self.history is not None and self.history.isVisible():
|
|
284
|
+
self.history.reopened()
|
|
285
|
+
if self.setup is not None:
|
|
286
|
+
self.setup.rescan() # 检查结果里的说明是按当时的语言写的:重新查一遍
|
|
287
|
+
|
|
288
|
+
def settings_closed(self) -> None:
|
|
289
|
+
self.resume_hotkeys() # 正在录快捷键时关掉窗口,也要把快捷键恢复
|
|
290
|
+
if self.warm_pending:
|
|
291
|
+
self.warm_pending = False
|
|
292
|
+
self.hub.warm_up()
|
|
293
|
+
|
|
294
|
+
def refresh_models(self) -> None:
|
|
295
|
+
self.hub.discover_models()
|
|
296
|
+
|
|
297
|
+
def _on_models_found(self, result: dict) -> None:
|
|
298
|
+
added, fitted = 0, False
|
|
299
|
+
for name, ids in result["found"].items():
|
|
300
|
+
engine = self.cfg.engine(name)
|
|
301
|
+
added += engine.merge_discovered(ids)
|
|
302
|
+
if name in self.fresh:
|
|
303
|
+
fitted |= flow.fit_models(engine, name, ids, self.fresh.pop(name))
|
|
304
|
+
if added or fitted:
|
|
305
|
+
config.save(self.cfg)
|
|
306
|
+
log.info("发现 %d 个新模型,已加入配置(默认关闭)", added)
|
|
307
|
+
if fitted:
|
|
308
|
+
self.hub.reconfigure()
|
|
309
|
+
self.hub.warm_up()
|
|
310
|
+
if self.settings is not None and self.settings.isVisible():
|
|
311
|
+
self.settings.reopened()
|
|
312
|
+
if self.settings is not None:
|
|
313
|
+
self.settings.models_refreshed({"added": added, "errors": result["errors"]})
|
|
314
|
+
|
|
315
|
+
# ---------- 准备窗口(环境检查) ----------
|
|
316
|
+
|
|
317
|
+
def adopt_setup(self, window: SetupWindow, result: SetupResult) -> None:
|
|
318
|
+
"""第一次启动时的准备窗口:以后在设置里再打开的就是它。"""
|
|
319
|
+
self.setup = window
|
|
320
|
+
window.on_done, window.on_quit = self.apply_setup, None
|
|
321
|
+
self.fresh.update({name: result.models.get(name, "") for name in result.fresh})
|
|
322
|
+
|
|
323
|
+
def open_setup(self) -> None:
|
|
324
|
+
if self.setup is None:
|
|
325
|
+
self.setup = SetupWindow(self.cfg, detect.scan(), first_run=False, on_done=self.apply_setup)
|
|
326
|
+
else:
|
|
327
|
+
self.setup.show_again()
|
|
328
|
+
|
|
329
|
+
def apply_setup(self, result: SetupResult) -> None:
|
|
330
|
+
"""准备窗口点了完成(或关掉):路由只用配好的引擎;改过登录或服务地址的引擎按新配置重连、重新发现模型。
|
|
331
|
+
什么都没变就到此为止。"""
|
|
332
|
+
before = (self.cfg.dsh.enabled, set(self.router.ready or ()))
|
|
333
|
+
apply_choices(self.cfg, result)
|
|
334
|
+
ready = result.report.usable(self.cfg.dsh.enabled)
|
|
335
|
+
if not result.fresh and before == (self.cfg.dsh.enabled, set(ready)):
|
|
336
|
+
return
|
|
337
|
+
self.fresh.update({name: result.models.get(name, "") for name in result.fresh})
|
|
338
|
+
self.hub.reconfigure(ready=ready)
|
|
339
|
+
if result.fresh:
|
|
340
|
+
self.hub.refresh(result.fresh)
|
|
341
|
+
self.hub.warm_up()
|
|
342
|
+
self.hub.discover_models()
|
|
343
|
+
if self.settings is not None and self.settings.isVisible():
|
|
344
|
+
self.settings.reopened()
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class Startup:
|
|
348
|
+
"""先过准备窗口(需要时:第一次启动,或者一个能用的引擎都没有),再启动 Super Aide。"""
|
|
349
|
+
|
|
350
|
+
def __init__(self, app: QApplication, cfg: config.Config, quiet: bool = False) -> None:
|
|
351
|
+
self.app, self.cfg = app, cfg
|
|
352
|
+
self.quiet = quiet # 开机自启:不提示
|
|
353
|
+
self.aide: SuperAide | None = None
|
|
354
|
+
self.window: SetupWindow | None = None
|
|
355
|
+
self.wakeup = Wakeup()
|
|
356
|
+
self.wakeup.woken.connect(self._woken)
|
|
357
|
+
|
|
358
|
+
def _woken(self) -> None:
|
|
359
|
+
"""又打开了一次:准备窗口开着就把它叫到前面,否则提示一下在后台运行。"""
|
|
360
|
+
if self.window is not None and (self.aide is None or self.window.isVisible()):
|
|
361
|
+
self.window.open()
|
|
362
|
+
elif self.aide is not None:
|
|
363
|
+
self.aide.announce(again=True)
|
|
364
|
+
|
|
365
|
+
def run(self) -> None:
|
|
366
|
+
report = detect.scan()
|
|
367
|
+
if flow.needs_setup(report, self.cfg.dsh.enabled, self.cfg.setup_done):
|
|
368
|
+
log.info("先做准备:%s", ";".join(f"{t.title}{'' if t.installed else '没装'}"
|
|
369
|
+
f"{'' if t.configured or not t.installed else '没配模型'}"
|
|
370
|
+
for t in (report.claude, report.codex, report.dsh)))
|
|
371
|
+
self.window = SetupWindow(self.cfg, report, first_run=True, on_done=self._done, on_quit=self.app.quit)
|
|
372
|
+
return
|
|
373
|
+
if not self.cfg.setup_done:
|
|
374
|
+
self.cfg.setup_done = True # 本来就都装好、配好了
|
|
375
|
+
config.save(self.cfg)
|
|
376
|
+
self._launch(report)
|
|
377
|
+
|
|
378
|
+
def _done(self, result: SetupResult) -> None:
|
|
379
|
+
apply_choices(self.cfg, result)
|
|
380
|
+
self._launch(result.report, result)
|
|
381
|
+
|
|
382
|
+
def _launch(self, report: detect.Report, result: SetupResult | None = None) -> None:
|
|
383
|
+
self.aide = SuperAide(self.app, self.cfg, report)
|
|
384
|
+
if result is not None:
|
|
385
|
+
self.aide.adopt_setup(self.window, result)
|
|
386
|
+
self.app.aboutToQuit.connect(self.aide.shutdown)
|
|
387
|
+
# 刚准备完也提示:告诉你怎么用。安装器更新完重新打开时带 --updated
|
|
388
|
+
self.aide.start(announce=result is not None or not self.quiet, updated="--updated" in sys.argv)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def main() -> int:
|
|
392
|
+
paths.ensure()
|
|
393
|
+
_setup_logging()
|
|
394
|
+
_hide_child_consoles()
|
|
395
|
+
_sync_workspace()
|
|
396
|
+
if not _single_instance():
|
|
397
|
+
log.info("已经在运行了")
|
|
398
|
+
_wake_running()
|
|
399
|
+
return 0
|
|
400
|
+
cfg = config.load()
|
|
401
|
+
i18n.set_language(i18n.resolve(cfg.language))
|
|
402
|
+
|
|
403
|
+
app = QApplication(sys.argv)
|
|
404
|
+
app.setQuitOnLastWindowClosed(False)
|
|
405
|
+
app.setApplicationName("Super Aide")
|
|
406
|
+
app.setWindowIcon(orb_icon())
|
|
407
|
+
|
|
408
|
+
startup = Startup(app, cfg, quiet=autostart.FLAG in sys.argv)
|
|
409
|
+
startup.run()
|
|
410
|
+
return app.exec()
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
if __name__ == "__main__":
|
|
414
|
+
sys.exit(main())
|
super_aide/autostart.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""开机自启:写 HKCU 的 Run 键,用虚拟环境里的 pythonw 启动(不弹控制台窗口)。
|
|
2
|
+
|
|
3
|
+
自启的命令带 --autostart:开机时安静地在后台起来;你自己打开时才提示一下它在哪、怎么用。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import winreg
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
RUN_KEY = r"Software\Microsoft\Windows\CurrentVersion\Run"
|
|
13
|
+
NAME = "SuperAide"
|
|
14
|
+
FLAG = "--autostart"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def command() -> str:
|
|
18
|
+
exe = Path(sys.executable)
|
|
19
|
+
windowless = exe.with_name("pythonw.exe")
|
|
20
|
+
return f'"{windowless if windowless.exists() else exe}" -m super_aide {FLAG}'
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _stored() -> str:
|
|
24
|
+
try:
|
|
25
|
+
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, RUN_KEY) as key:
|
|
26
|
+
return str(winreg.QueryValueEx(key, NAME)[0])
|
|
27
|
+
except OSError:
|
|
28
|
+
return ""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_enabled() -> bool:
|
|
32
|
+
return bool(_stored())
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def upgrade() -> None:
|
|
36
|
+
"""以前写的自启命令没带 --autostart:是这个 Python 写的就补上(别的副本写的不动,免得被开发版抢走自启)。"""
|
|
37
|
+
if _stored() == command().removesuffix(f" {FLAG}"):
|
|
38
|
+
set_enabled(True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def set_enabled(on: bool) -> None:
|
|
42
|
+
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, RUN_KEY, 0, winreg.KEY_SET_VALUE) as key:
|
|
43
|
+
if on:
|
|
44
|
+
winreg.SetValueEx(key, NAME, 0, winreg.REG_SZ, command())
|
|
45
|
+
else:
|
|
46
|
+
try:
|
|
47
|
+
winreg.DeleteValue(key, NAME)
|
|
48
|
+
except FileNotFoundError:
|
|
49
|
+
pass
|