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.
Files changed (108) hide show
  1. super_aide/__init__.py +8 -0
  2. super_aide/__main__.py +3 -0
  3. super_aide/_release/constraints.txt +173 -0
  4. super_aide/_release/release.json +5 -0
  5. super_aide/app.py +414 -0
  6. super_aide/autostart.py +49 -0
  7. super_aide/config.py +150 -0
  8. super_aide/cu/__init__.py +5 -0
  9. super_aide/cu/actions.py +555 -0
  10. super_aide/cu/browser.py +91 -0
  11. super_aide/cu/elements.py +227 -0
  12. super_aide/cu/guard.py +198 -0
  13. super_aide/cu/guide.py +206 -0
  14. super_aide/cu/inputs.py +223 -0
  15. super_aide/cu/keys.py +45 -0
  16. super_aide/cu/observe.py +200 -0
  17. super_aide/cu/ocr.py +186 -0
  18. super_aide/cu/server.py +425 -0
  19. super_aide/cu/signals.py +37 -0
  20. super_aide/engines/__init__.py +5 -0
  21. super_aide/engines/base.py +56 -0
  22. super_aide/engines/browser_use.py +89 -0
  23. super_aide/engines/claude.py +386 -0
  24. super_aide/engines/codex.py +277 -0
  25. super_aide/engines/codex_hook.py +33 -0
  26. super_aide/engines/computer.py +131 -0
  27. super_aide/engines/dsh.py +539 -0
  28. super_aide/engines/experience.py +48 -0
  29. super_aide/engines/hub.py +664 -0
  30. super_aide/engines/prompts.py +66 -0
  31. super_aide/experience/__init__.py +6 -0
  32. super_aide/experience/codex_hook.py +60 -0
  33. super_aide/experience/embed.py +198 -0
  34. super_aide/experience/learn.py +145 -0
  35. super_aide/experience/recall.py +210 -0
  36. super_aide/experience/semantic.py +118 -0
  37. super_aide/experience/server.py +44 -0
  38. super_aide/experience/store.py +184 -0
  39. super_aide/history.py +104 -0
  40. super_aide/hotkeys.py +105 -0
  41. super_aide/i18n.py +68 -0
  42. super_aide/locales/__init__.py +1 -0
  43. super_aide/locales/en_core.py +136 -0
  44. super_aide/locales/en_engines.py +65 -0
  45. super_aide/paths.py +26 -0
  46. super_aide/router/__init__.py +5 -0
  47. super_aide/router/catalog.py +179 -0
  48. super_aide/router/evidence.py +212 -0
  49. super_aide/router/judge.py +82 -0
  50. super_aide/router/policy.py +630 -0
  51. super_aide/safety.py +122 -0
  52. super_aide/setup/__init__.py +1 -0
  53. super_aide/setup/configure.py +282 -0
  54. super_aide/setup/detect.py +323 -0
  55. super_aide/setup/flow.py +91 -0
  56. super_aide/setup/install.py +333 -0
  57. super_aide/setup/winenv.py +54 -0
  58. super_aide/shell/__init__.py +1 -0
  59. super_aide/shell/controller.py +431 -0
  60. super_aide/shell/history_window.py +56 -0
  61. super_aide/shell/killswitch.py +72 -0
  62. super_aide/shell/overlay.py +78 -0
  63. super_aide/shell/panel.py +140 -0
  64. super_aide/shell/screens.py +76 -0
  65. super_aide/shell/settings.py +158 -0
  66. super_aide/shell/setup_window.py +304 -0
  67. super_aide/shell/tray.py +72 -0
  68. super_aide/shell/webwindow.py +89 -0
  69. super_aide/snapshot/__init__.py +1 -0
  70. super_aide/snapshot/chromium.py +40 -0
  71. super_aide/snapshot/collector.py +443 -0
  72. super_aide/snapshot/content.py +59 -0
  73. super_aide/snapshot/redact.py +31 -0
  74. super_aide/snapshot/system.py +143 -0
  75. super_aide/snapshot/uia.py +475 -0
  76. super_aide/snapshot/uiawalk.py +170 -0
  77. super_aide/snapshot/win32.py +163 -0
  78. super_aide/ui/super_aide.ico +0 -0
  79. super_aide/ui/web/app.js +518 -0
  80. super_aide/ui/web/history.css +138 -0
  81. super_aide/ui/web/history.html +34 -0
  82. super_aide/ui/web/history.js +240 -0
  83. super_aide/ui/web/i18n-en.js +200 -0
  84. super_aide/ui/web/i18n.js +45 -0
  85. super_aide/ui/web/index.html +101 -0
  86. super_aide/ui/web/settings.css +513 -0
  87. super_aide/ui/web/settings.html +127 -0
  88. super_aide/ui/web/settings.js +705 -0
  89. super_aide/ui/web/setup.css +176 -0
  90. super_aide/ui/web/setup.html +51 -0
  91. super_aide/ui/web/setup.js +349 -0
  92. super_aide/ui/web/style.css +596 -0
  93. super_aide/ui/web/vendor/marked.min.js +69 -0
  94. super_aide/update.py +155 -0
  95. super_aide/workspace/AGENTS.md +44 -0
  96. super_aide/workspace/CLAUDE.md +1 -0
  97. super_aide/workspace/plugin/.claude-plugin/plugin.json +5 -0
  98. super_aide/workspace/plugin/skills/disk-cleanup/SKILL.md +69 -0
  99. super_aide/workspace/plugin/skills/jupyter-debug/SKILL.md +42 -0
  100. super_aide/workspace/plugin/skills/network-proxy/SKILL.md +61 -0
  101. super_aide/workspace/plugin/skills/office-automation/SKILL.md +48 -0
  102. super_aide/workspace/plugin/skills/python-env/SKILL.md +37 -0
  103. super_aide/workspace/plugin/skills/uninstall-software/SKILL.md +57 -0
  104. super_aide-0.1.1.dist-info/METADATA +158 -0
  105. super_aide-0.1.1.dist-info/RECORD +108 -0
  106. super_aide-0.1.1.dist-info/WHEEL +4 -0
  107. super_aide-0.1.1.dist-info/entry_points.txt +2 -0
  108. super_aide-0.1.1.dist-info/licenses/LICENSE +21 -0
super_aide/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Super Aide:Windows 桌面超级助手。"""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("super-aide")
7
+ except PackageNotFoundError: # 没装、直接从源码跑
8
+ __version__ = "0.0.0"
super_aide/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .app import main
2
+
3
+ raise SystemExit(main())
@@ -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
@@ -0,0 +1,5 @@
1
+ {
2
+ "version": "0.1.1",
3
+ "repo": "typing233/super-aide",
4
+ "built": "2026-10-09T05:40:11+00:00"
5
+ }
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())
@@ -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