illusion-code 0.1.0__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 (214) hide show
  1. illusion/__init__.py +24 -0
  2. illusion/__main__.py +15 -0
  3. illusion/_frontend/dist/index.mjs +39208 -0
  4. illusion/_frontend/package.json +27 -0
  5. illusion/_frontend/src/App.tsx +624 -0
  6. illusion/_frontend/src/components/CommandPicker.tsx +98 -0
  7. illusion/_frontend/src/components/Composer.tsx +55 -0
  8. illusion/_frontend/src/components/ComposerController.tsx +128 -0
  9. illusion/_frontend/src/components/ConversationView.tsx +750 -0
  10. illusion/_frontend/src/components/Footer.tsx +25 -0
  11. illusion/_frontend/src/components/MarkdownContent.tsx +537 -0
  12. illusion/_frontend/src/components/MarkdownTable.tsx +245 -0
  13. illusion/_frontend/src/components/ModalHost.tsx +425 -0
  14. illusion/_frontend/src/components/MultilineTextInput.tsx +250 -0
  15. illusion/_frontend/src/components/PromptInput.tsx +64 -0
  16. illusion/_frontend/src/components/SelectModal.tsx +78 -0
  17. illusion/_frontend/src/components/SidePanel.tsx +175 -0
  18. illusion/_frontend/src/components/Spinner.tsx +77 -0
  19. illusion/_frontend/src/components/StatusBar.tsx +142 -0
  20. illusion/_frontend/src/components/SwarmPanel.tsx +141 -0
  21. illusion/_frontend/src/components/TodoPanel.tsx +126 -0
  22. illusion/_frontend/src/components/ToolCallDisplay.tsx +202 -0
  23. illusion/_frontend/src/components/TranscriptPane.tsx +79 -0
  24. illusion/_frontend/src/components/WelcomeBanner.tsx +37 -0
  25. illusion/_frontend/src/hooks/useBackendSession.ts +468 -0
  26. illusion/_frontend/src/hooks/useTerminalSize.ts +9 -0
  27. illusion/_frontend/src/i18n.ts +78 -0
  28. illusion/_frontend/src/index.tsx +42 -0
  29. illusion/_frontend/src/theme/ThemeContext.tsx +19 -0
  30. illusion/_frontend/src/theme/builtinThemes.ts +89 -0
  31. illusion/_frontend/src/types.ts +110 -0
  32. illusion/_frontend/src/utils/markdown.ts +33 -0
  33. illusion/_frontend/src/utils/thinking.ts +191 -0
  34. illusion/_frontend/tsconfig.json +13 -0
  35. illusion/_web_dist/assets/index-BseIw-ik.css +10 -0
  36. illusion/_web_dist/assets/index-C_0ZWMuW.js +82 -0
  37. illusion/_web_dist/index.html +16 -0
  38. illusion/api/__init__.py +36 -0
  39. illusion/api/client.py +568 -0
  40. illusion/api/codex_client.py +563 -0
  41. illusion/api/compat.py +138 -0
  42. illusion/api/effort.py +128 -0
  43. illusion/api/errors.py +57 -0
  44. illusion/api/openai_client.py +819 -0
  45. illusion/api/provider.py +148 -0
  46. illusion/api/registry.py +479 -0
  47. illusion/api/usage.py +45 -0
  48. illusion/auth/__init__.py +50 -0
  49. illusion/auth/copilot.py +419 -0
  50. illusion/auth/external.py +612 -0
  51. illusion/auth/flows.py +58 -0
  52. illusion/auth/manager.py +214 -0
  53. illusion/auth/storage.py +372 -0
  54. illusion/bridge/__init__.py +38 -0
  55. illusion/bridge/manager.py +190 -0
  56. illusion/bridge/session_runner.py +84 -0
  57. illusion/bridge/types.py +113 -0
  58. illusion/bridge/work_secret.py +131 -0
  59. illusion/cli.py +1228 -0
  60. illusion/commands/__init__.py +32 -0
  61. illusion/commands/registry.py +1934 -0
  62. illusion/config/__init__.py +39 -0
  63. illusion/config/i18n.py +522 -0
  64. illusion/config/paths.py +259 -0
  65. illusion/config/settings.py +564 -0
  66. illusion/coordinator/__init__.py +41 -0
  67. illusion/coordinator/agent_definitions.py +1093 -0
  68. illusion/coordinator/coordinator_mode.py +127 -0
  69. illusion/engine/__init__.py +95 -0
  70. illusion/engine/cost_tracker.py +55 -0
  71. illusion/engine/messages.py +369 -0
  72. illusion/engine/query.py +632 -0
  73. illusion/engine/query_engine.py +343 -0
  74. illusion/engine/stream_events.py +169 -0
  75. illusion/hooks/__init__.py +67 -0
  76. illusion/hooks/events.py +43 -0
  77. illusion/hooks/executor.py +397 -0
  78. illusion/hooks/hot_reload.py +74 -0
  79. illusion/hooks/loader.py +133 -0
  80. illusion/hooks/schemas.py +121 -0
  81. illusion/hooks/types.py +86 -0
  82. illusion/mcp/__init__.py +104 -0
  83. illusion/mcp/client.py +377 -0
  84. illusion/mcp/config.py +140 -0
  85. illusion/mcp/types.py +175 -0
  86. illusion/memory/__init__.py +36 -0
  87. illusion/memory/manager.py +94 -0
  88. illusion/memory/memdir.py +58 -0
  89. illusion/memory/paths.py +57 -0
  90. illusion/memory/scan.py +120 -0
  91. illusion/memory/search.py +83 -0
  92. illusion/memory/types.py +43 -0
  93. illusion/output_styles/__init__.py +15 -0
  94. illusion/output_styles/loader.py +64 -0
  95. illusion/permissions/__init__.py +39 -0
  96. illusion/permissions/checker.py +174 -0
  97. illusion/permissions/modes.py +38 -0
  98. illusion/platforms.py +148 -0
  99. illusion/plugins/__init__.py +71 -0
  100. illusion/plugins/bundled/__init__.py +0 -0
  101. illusion/plugins/installer.py +59 -0
  102. illusion/plugins/loader.py +301 -0
  103. illusion/plugins/schemas.py +51 -0
  104. illusion/plugins/types.py +56 -0
  105. illusion/prompts/__init__.py +29 -0
  106. illusion/prompts/claudemd.py +74 -0
  107. illusion/prompts/context.py +187 -0
  108. illusion/prompts/environment.py +189 -0
  109. illusion/prompts/system_prompt.py +155 -0
  110. illusion/py.typed +0 -0
  111. illusion/sandbox/__init__.py +29 -0
  112. illusion/sandbox/adapter.py +174 -0
  113. illusion/services/__init__.py +59 -0
  114. illusion/services/compact/__init__.py +1015 -0
  115. illusion/services/cron.py +338 -0
  116. illusion/services/cron_scheduler.py +715 -0
  117. illusion/services/file_history.py +258 -0
  118. illusion/services/lsp/__init__.py +455 -0
  119. illusion/services/session_storage.py +237 -0
  120. illusion/services/token_estimation.py +72 -0
  121. illusion/skills/__init__.py +60 -0
  122. illusion/skills/bundled/__init__.py +110 -0
  123. illusion/skills/bundled/content/batch.md +86 -0
  124. illusion/skills/bundled/content/coding-guidelines.md +70 -0
  125. illusion/skills/bundled/content/debug.md +38 -0
  126. illusion/skills/bundled/content/loop.md +82 -0
  127. illusion/skills/bundled/content/remember.md +105 -0
  128. illusion/skills/bundled/content/simplify.md +53 -0
  129. illusion/skills/bundled/content/skillify.md +113 -0
  130. illusion/skills/bundled/content/stuck.md +54 -0
  131. illusion/skills/bundled/content/update-config.md +329 -0
  132. illusion/skills/bundled/content/verify.md +74 -0
  133. illusion/skills/loader.py +219 -0
  134. illusion/skills/registry.py +40 -0
  135. illusion/skills/types.py +24 -0
  136. illusion/state/__init__.py +18 -0
  137. illusion/state/app_state.py +67 -0
  138. illusion/state/store.py +93 -0
  139. illusion/swarm/__init__.py +71 -0
  140. illusion/swarm/agent_executor.py +857 -0
  141. illusion/swarm/in_process.py +259 -0
  142. illusion/swarm/subprocess_backend.py +136 -0
  143. illusion/swarm/team_helpers.py +123 -0
  144. illusion/swarm/types.py +159 -0
  145. illusion/swarm/worktree.py +347 -0
  146. illusion/tasks/__init__.py +33 -0
  147. illusion/tasks/local_agent_task.py +42 -0
  148. illusion/tasks/local_shell_task.py +27 -0
  149. illusion/tasks/manager.py +377 -0
  150. illusion/tasks/stop_task.py +21 -0
  151. illusion/tasks/types.py +88 -0
  152. illusion/tools/__init__.py +126 -0
  153. illusion/tools/agent_tool.py +388 -0
  154. illusion/tools/ask_user_question_tool.py +186 -0
  155. illusion/tools/base.py +149 -0
  156. illusion/tools/bash_tool.py +413 -0
  157. illusion/tools/config_tool.py +90 -0
  158. illusion/tools/cron_tool.py +473 -0
  159. illusion/tools/enter_plan_mode_tool.py +147 -0
  160. illusion/tools/enter_worktree_tool.py +188 -0
  161. illusion/tools/exit_plan_mode_tool.py +69 -0
  162. illusion/tools/exit_worktree_tool.py +225 -0
  163. illusion/tools/file_edit_tool.py +283 -0
  164. illusion/tools/file_read_tool.py +294 -0
  165. illusion/tools/file_write_tool.py +184 -0
  166. illusion/tools/glob_tool.py +165 -0
  167. illusion/tools/grep_tool.py +190 -0
  168. illusion/tools/list_mcp_resources_tool.py +80 -0
  169. illusion/tools/lsp_tool.py +333 -0
  170. illusion/tools/mcp_auth_tool.py +100 -0
  171. illusion/tools/mcp_tool.py +75 -0
  172. illusion/tools/notebook_edit_tool.py +242 -0
  173. illusion/tools/powershell_tool.py +334 -0
  174. illusion/tools/read_mcp_resource_tool.py +63 -0
  175. illusion/tools/repl_tool.py +100 -0
  176. illusion/tools/send_message_tool.py +112 -0
  177. illusion/tools/shell_common.py +187 -0
  178. illusion/tools/skill_tool.py +86 -0
  179. illusion/tools/sleep_tool.py +62 -0
  180. illusion/tools/structured_output_tool.py +58 -0
  181. illusion/tools/task_create_tool.py +98 -0
  182. illusion/tools/task_get_tool.py +94 -0
  183. illusion/tools/task_list_tool.py +94 -0
  184. illusion/tools/task_output_tool.py +55 -0
  185. illusion/tools/task_stop_tool.py +52 -0
  186. illusion/tools/task_update_tool.py +224 -0
  187. illusion/tools/team_create_tool.py +236 -0
  188. illusion/tools/team_delete_tool.py +104 -0
  189. illusion/tools/todo_write_tool.py +198 -0
  190. illusion/tools/tool_search_tool.py +156 -0
  191. illusion/tools/web_fetch_tool.py +264 -0
  192. illusion/tools/web_search_tool.py +186 -0
  193. illusion/ui/__init__.py +23 -0
  194. illusion/ui/app.py +258 -0
  195. illusion/ui/backend_host.py +1180 -0
  196. illusion/ui/input.py +86 -0
  197. illusion/ui/output.py +363 -0
  198. illusion/ui/permission_dialog.py +47 -0
  199. illusion/ui/permission_store.py +99 -0
  200. illusion/ui/protocol.py +384 -0
  201. illusion/ui/react_launcher.py +280 -0
  202. illusion/ui/runtime.py +787 -0
  203. illusion/ui/textual_app.py +603 -0
  204. illusion/ui/web/__init__.py +10 -0
  205. illusion/ui/web/server.py +87 -0
  206. illusion/ui/web/ws_host.py +1197 -0
  207. illusion/utils/__init__.py +0 -0
  208. illusion/utils/ripgrep.py +299 -0
  209. illusion/utils/shell.py +248 -0
  210. illusion_code-0.1.0.dist-info/METADATA +1159 -0
  211. illusion_code-0.1.0.dist-info/RECORD +214 -0
  212. illusion_code-0.1.0.dist-info/WHEEL +4 -0
  213. illusion_code-0.1.0.dist-info/entry_points.txt +2 -0
  214. illusion_code-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,50 @@
1
+ """
2
+ 认证模块
3
+ ========
4
+
5
+ 本模块提供 IllusionCode 统一的认证管理功能。
6
+
7
+ 主要组件:
8
+ - AuthManager: 认证管理器
9
+ - ApiKeyFlow: API 密钥认证流程
10
+ - store_credential/load_credential: 凭据存储/加载(按 provider)
11
+ - store_env_credential/load_env_credential: 凭据存储/加载(按 env_N)
12
+ - store_external_binding/load_external_binding: 外部绑定存储/加载
13
+ - encrypt/decrypt: 加密/解密功能
14
+
15
+ 使用示例:
16
+ >>> from illusion.auth import AuthManager, ApiKeyFlow
17
+ >>> manager = AuthManager()
18
+ >>> flow = ApiKeyFlow(prompt_text="输入 API 密钥")
19
+ >>> key = flow.run()
20
+ """
21
+
22
+ from illusion.auth.flows import ApiKeyFlow
23
+ from illusion.auth.manager import AuthManager
24
+ from illusion.auth.storage import (
25
+ clear_env_credentials,
26
+ clear_provider_credentials,
27
+ decrypt,
28
+ encrypt,
29
+ load_credential,
30
+ load_env_credential,
31
+ load_external_binding,
32
+ store_credential,
33
+ store_env_credential,
34
+ store_external_binding,
35
+ )
36
+
37
+ __all__ = [
38
+ "AuthManager",
39
+ "ApiKeyFlow",
40
+ "store_credential",
41
+ "load_credential",
42
+ "store_env_credential",
43
+ "load_env_credential",
44
+ "clear_env_credentials",
45
+ "store_external_binding",
46
+ "load_external_binding",
47
+ "clear_provider_credentials",
48
+ "encrypt",
49
+ "decrypt",
50
+ ]
@@ -0,0 +1,419 @@
1
+ """
2
+ GitHub Copilot OAuth 认证模块
3
+ ============================
4
+
5
+ 本模块提供 GitHub Copilot OAuth 设备码认证流程。
6
+
7
+ 认证流程:
8
+ 1. 启动设备码流程,获取 device_code 和 user_code
9
+ 2. 用户在浏览器中完成 GitHub 授权
10
+ 3. 轮询获取 GitHub access_token
11
+ 4. 使用 GitHub token 获取 Copilot token
12
+ 5. 自动刷新 Copilot token(到期前刷新)
13
+
14
+ 存储格式:
15
+ ~/.illusion/copilot_auth.json
16
+
17
+ 使用示例:
18
+ >>> from illusion.auth.copilot import CopilotAuth
19
+ >>> auth = CopilotAuth()
20
+ >>> flow = auth.start_device_flow()
21
+ >>> print(f"请在浏览器中访问: {flow['verification_uri']}")
22
+ >>> print(f"输入代码: {flow['user_code']}")
23
+ >>> auth.poll_for_token(flow['device_code'])
24
+ >>> token = auth.get_valid_token()
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import json
30
+ import logging
31
+ import time
32
+ import urllib.parse
33
+ import urllib.request
34
+ from dataclasses import asdict, dataclass
35
+ from pathlib import Path
36
+ from typing import Any
37
+
38
+ from illusion.config.paths import get_config_dir
39
+
40
+ # 模块级日志记录器
41
+ log = logging.getLogger(__name__)
42
+
43
+ # GitHub OAuth 客户端 ID(VS Code Copilot 扩展使用的公开 ID)
44
+ GITHUB_CLIENT_ID = "Iv1.b507a08c87ecfe98"
45
+
46
+ # Copilot API 常量
47
+ COPILOT_API_BASE = "https://api.githubcopilot.com"
48
+ COPILOT_EDITOR_VERSION = "vscode/1.110.1"
49
+ COPILOT_PLUGIN_VERSION = "copilot-chat/0.38.2"
50
+ COPILOT_USER_AGENT = "GitHubCopilotChat/0.38.2"
51
+ COPILOT_API_VERSION = "2025-10-01"
52
+ COPILOT_INTEGRATION_ID = "vscode-chat"
53
+
54
+ # GitHub 端点
55
+ _GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code"
56
+ _GITHUB_OAUTH_TOKEN_URL = "https://github.com/login/oauth/access_token"
57
+ _GITHUB_USER_URL = "https://api.github.com/user"
58
+ _COPILOT_TOKEN_URL = "https://api.github.com/copilot_internal/v2/token"
59
+
60
+ # Token 刷新提前量(秒)
61
+ _TOKEN_REFRESH_BUFFER = 60
62
+
63
+ # 轮询默认间隔和超时
64
+ _POLL_INTERVAL = 5
65
+ _POLL_TIMEOUT = 900 # 15 分钟
66
+
67
+
68
+ def _request_json(
69
+ url: str,
70
+ *,
71
+ method: str = "GET",
72
+ data: dict[str, str] | None = None,
73
+ headers: dict[str, str] | None = None,
74
+ timeout: int = 15,
75
+ ) -> dict[str, Any]:
76
+ """发送 HTTP 请求并返回 JSON 响应
77
+
78
+ Args:
79
+ url: 请求 URL
80
+ method: HTTP 方法
81
+ data: 表单数据
82
+ headers: 请求头
83
+ timeout: 超时时间(秒)
84
+
85
+ Returns:
86
+ dict[str, Any]: JSON 响应
87
+ """
88
+ req_headers = {
89
+ "Accept": "application/json",
90
+ "User-Agent": COPILOT_USER_AGENT,
91
+ }
92
+ if headers:
93
+ req_headers.update(headers)
94
+
95
+ body = None
96
+ if data:
97
+ body = urllib.parse.urlencode(data).encode("utf-8")
98
+ req_headers["Content-Type"] = "application/x-www-form-urlencoded"
99
+
100
+ request = urllib.request.Request(url, data=body, headers=req_headers, method=method)
101
+ with urllib.request.urlopen(request, timeout=timeout) as response:
102
+ return json.loads(response.read().decode("utf-8"))
103
+
104
+
105
+ def copilot_api_headers(token: str) -> dict[str, str]:
106
+ """返回 Copilot API 请求所需的特殊请求头
107
+
108
+ Args:
109
+ token: Copilot token
110
+
111
+ Returns:
112
+ dict[str, str]: 请求头字典
113
+ """
114
+ return {
115
+ "Authorization": f"Bearer {token}",
116
+ "Content-Type": "application/json",
117
+ "copilot-integration-id": COPILOT_INTEGRATION_ID,
118
+ "editor-version": COPILOT_EDITOR_VERSION,
119
+ "editor-plugin-version": COPILOT_PLUGIN_VERSION,
120
+ "user-agent": COPILOT_USER_AGENT,
121
+ "x-github-api-version": COPILOT_API_VERSION,
122
+ }
123
+
124
+
125
+ def copilot_extra_headers() -> dict[str, str]:
126
+ """返回创建 OpenAI 客户端时需注入的额外请求头(不含 Authorization)
127
+
128
+ Returns:
129
+ dict[str, str]: 额外请求头字典
130
+ """
131
+ return {
132
+ "copilot-integration-id": COPILOT_INTEGRATION_ID,
133
+ "editor-version": COPILOT_EDITOR_VERSION,
134
+ "editor-plugin-version": COPILOT_PLUGIN_VERSION,
135
+ "user-agent": COPILOT_USER_AGENT,
136
+ "x-github-api-version": COPILOT_API_VERSION,
137
+ }
138
+
139
+
140
+ @dataclass
141
+ class CopilotAuthData:
142
+ """Copilot 认证数据
143
+
144
+ Attributes:
145
+ github_token: GitHub OAuth access token
146
+ copilot_token: Copilot token(JWT)
147
+ copilot_token_expires_at: Copilot token 过期时间(Unix 秒)
148
+ user_login: GitHub 用户名
149
+ user_id: GitHub 用户 ID
150
+ authenticated_at: 认证时间戳
151
+ """
152
+
153
+ github_token: str = ""
154
+ copilot_token: str = ""
155
+ copilot_token_expires_at: int = 0
156
+ user_login: str = ""
157
+ user_id: int = 0
158
+ authenticated_at: int = 0
159
+
160
+
161
+ class CopilotAuth:
162
+ """GitHub Copilot OAuth 认证管理器
163
+
164
+ 管理 GitHub OAuth 设备码流程和 Copilot token 的获取、刷新、持久化。
165
+ """
166
+
167
+ def __init__(self) -> None:
168
+ self._storage_path = get_config_dir() / "copilot_auth.json"
169
+ self._data = self._load()
170
+
171
+ @property
172
+ def storage_path(self) -> Path:
173
+ """返回存储文件路径"""
174
+ return self._storage_path
175
+
176
+ def _load(self) -> CopilotAuthData:
177
+ """从磁盘加载认证数据
178
+
179
+ Returns:
180
+ CopilotAuthData: 认证数据
181
+ """
182
+ if not self._storage_path.exists():
183
+ return CopilotAuthData()
184
+ try:
185
+ raw = json.loads(self._storage_path.read_text(encoding="utf-8"))
186
+ return CopilotAuthData(
187
+ github_token=raw.get("github_token", ""),
188
+ copilot_token=raw.get("copilot_token", ""),
189
+ copilot_token_expires_at=raw.get("copilot_token_expires_at", 0),
190
+ user_login=raw.get("user_login", ""),
191
+ user_id=raw.get("user_id", 0),
192
+ authenticated_at=raw.get("authenticated_at", 0),
193
+ )
194
+ except (json.JSONDecodeError, OSError) as exc:
195
+ log.warning("加载 Copilot 认证数据失败: %s", exc)
196
+ return CopilotAuthData()
197
+
198
+ def _save(self) -> None:
199
+ """保存认证数据到磁盘"""
200
+ self._storage_path.parent.mkdir(parents=True, exist_ok=True)
201
+ data = asdict(self._data)
202
+ self._storage_path.write_text(
203
+ json.dumps(data, indent=2) + "\n",
204
+ encoding="utf-8",
205
+ )
206
+ try:
207
+ self._storage_path.chmod(0o600)
208
+ except OSError:
209
+ pass
210
+
211
+ def is_authenticated(self) -> bool:
212
+ """检查是否已认证
213
+
214
+ Returns:
215
+ bool: 是否已认证
216
+ """
217
+ return bool(self._data.github_token)
218
+
219
+ def get_status(self) -> dict[str, Any]:
220
+ """获取认证状态
221
+
222
+ Returns:
223
+ dict[str, Any]: 认证状态信息
224
+ """
225
+ return {
226
+ "authenticated": self.is_authenticated(),
227
+ "username": self._data.user_login or None,
228
+ "expires_at": self._data.copilot_token_expires_at or None,
229
+ }
230
+
231
+ def start_device_flow(self) -> dict[str, Any]:
232
+ """启动 GitHub OAuth 设备码流程
233
+
234
+ Returns:
235
+ dict[str, Any]: 包含 device_code、user_code、verification_uri 的字典
236
+
237
+ Raises:
238
+ RuntimeError: 设备码请求失败
239
+ """
240
+ log.info("启动 Copilot 设备码流程")
241
+ data = _request_json(
242
+ _GITHUB_DEVICE_CODE_URL,
243
+ method="POST",
244
+ data={
245
+ "client_id": GITHUB_CLIENT_ID,
246
+ "scope": "read:user",
247
+ },
248
+ )
249
+ log.info("获取设备码成功,user_code: %s", data.get("user_code"))
250
+ return {
251
+ "device_code": data["device_code"],
252
+ "user_code": data["user_code"],
253
+ "verification_uri": data["verification_uri"],
254
+ "expires_in": data.get("expires_in", 900),
255
+ "interval": data.get("interval", 5),
256
+ }
257
+
258
+ def poll_for_token(self, device_code: str) -> bool:
259
+ """轮询 GitHub OAuth Token,等待用户完成授权
260
+
261
+ Args:
262
+ device_code: 设备码
263
+
264
+ Returns:
265
+ bool: 是否授权成功
266
+
267
+ Raises:
268
+ RuntimeError: 授权被拒绝或设备码过期
269
+ """
270
+ log.info("开始轮询 OAuth Token")
271
+ start_time = time.time()
272
+
273
+ while time.time() - start_time < _POLL_TIMEOUT:
274
+ try:
275
+ data = _request_json(
276
+ _GITHUB_OAUTH_TOKEN_URL,
277
+ method="POST",
278
+ data={
279
+ "client_id": GITHUB_CLIENT_ID,
280
+ "device_code": device_code,
281
+ "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
282
+ },
283
+ )
284
+ except Exception as exc:
285
+ log.warning("轮询请求失败: %s", exc)
286
+ time.sleep(_POLL_INTERVAL)
287
+ continue
288
+
289
+ error = data.get("error")
290
+ if error == "authorization_pending":
291
+ time.sleep(_POLL_INTERVAL)
292
+ continue
293
+ if error == "slow_down":
294
+ time.sleep(_POLL_INTERVAL + 5)
295
+ continue
296
+ if error == "expired_token":
297
+ raise RuntimeError("设备码已过期,请重新运行登录")
298
+ if error == "access_denied":
299
+ raise RuntimeError("授权被拒绝")
300
+ if error:
301
+ raise RuntimeError(f"OAuth 错误: {error}")
302
+
303
+ access_token = data.get("access_token", "")
304
+ if not access_token:
305
+ time.sleep(_POLL_INTERVAL)
306
+ continue
307
+
308
+ log.info("GitHub OAuth Token 获取成功")
309
+ self._complete_auth(access_token)
310
+ return True
311
+
312
+ raise RuntimeError("设备码轮询超时")
313
+
314
+ def _complete_auth(self, github_token: str) -> None:
315
+ """使用 GitHub token 完成认证流程
316
+
317
+ Args:
318
+ github_token: GitHub OAuth access token
319
+
320
+ Raises:
321
+ RuntimeError: Copilot 订阅验证失败
322
+ """
323
+ # 获取用户信息
324
+ user_data = _request_json(
325
+ _GITHUB_USER_URL,
326
+ headers={
327
+ "Authorization": f"token {github_token}",
328
+ "Editor-Version": COPILOT_EDITOR_VERSION,
329
+ "Editor-Plugin-Version": COPILOT_PLUGIN_VERSION,
330
+ },
331
+ )
332
+
333
+ # 获取 Copilot token
334
+ self._fetch_copilot_token(github_token)
335
+
336
+ self._data.github_token = github_token
337
+ self._data.user_login = user_data.get("login", "")
338
+ self._data.user_id = user_data.get("id", 0)
339
+ self._data.authenticated_at = int(time.time())
340
+ self._save()
341
+ log.info("Copilot 认证完成,用户: %s", self._data.user_login)
342
+
343
+ def _fetch_copilot_token(self, github_token: str | None = None) -> str:
344
+ """使用 GitHub token 获取 Copilot token
345
+
346
+ Args:
347
+ github_token: GitHub token,默认使用已存储的
348
+
349
+ Returns:
350
+ str: Copilot token
351
+
352
+ Raises:
353
+ RuntimeError: 获取失败
354
+ """
355
+ token = github_token or self._data.github_token
356
+ if not token:
357
+ raise RuntimeError("无 GitHub token")
358
+
359
+ try:
360
+ data = _request_json(
361
+ _COPILOT_TOKEN_URL,
362
+ headers={
363
+ "Authorization": f"token {token}",
364
+ "User-Agent": COPILOT_USER_AGENT,
365
+ "Editor-Version": COPILOT_EDITOR_VERSION,
366
+ "Editor-Plugin-Version": COPILOT_PLUGIN_VERSION,
367
+ },
368
+ )
369
+ except urllib.error.HTTPError as exc:
370
+ if exc.code == 401:
371
+ raise RuntimeError("GitHub token 无效或已过期") from exc
372
+ if exc.code == 403:
373
+ raise RuntimeError("未订阅 GitHub Copilot") from exc
374
+ raise RuntimeError(f"获取 Copilot token 失败: {exc}") from exc
375
+
376
+ copilot_token = data.get("token", "")
377
+ expires_at = data.get("expires_at", 0)
378
+ if not copilot_token:
379
+ raise RuntimeError("Copilot token 响应为空")
380
+
381
+ self._data.copilot_token = copilot_token
382
+ self._data.copilot_token_expires_at = expires_at
383
+ if github_token:
384
+ self._data.github_token = github_token
385
+ self._save()
386
+ log.info("Copilot Token 获取成功,过期时间: %s", expires_at)
387
+ return copilot_token
388
+
389
+ def get_valid_token(self) -> str:
390
+ """获取有效的 Copilot token,自动刷新
391
+
392
+ Returns:
393
+ str: 有效的 Copilot token
394
+
395
+ Raises:
396
+ RuntimeError: 未认证或刷新失败
397
+ """
398
+ if not self._data.github_token:
399
+ raise RuntimeError("未认证,请先运行 'illusion auth login' 选择 GitHub Copilot")
400
+
401
+ now = int(time.time())
402
+ if (
403
+ self._data.copilot_token
404
+ and self._data.copilot_token_expires_at - now > _TOKEN_REFRESH_BUFFER
405
+ ):
406
+ return self._data.copilot_token
407
+
408
+ log.info("Copilot Token 需要刷新")
409
+ return self._fetch_copilot_token()
410
+
411
+ def clear_auth(self) -> None:
412
+ """清除所有认证数据"""
413
+ self._data = CopilotAuthData()
414
+ if self._storage_path.exists():
415
+ try:
416
+ self._storage_path.unlink()
417
+ except OSError:
418
+ pass
419
+ log.info("Copilot 认证已清除")