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.
- illusion/__init__.py +24 -0
- illusion/__main__.py +15 -0
- illusion/_frontend/dist/index.mjs +39208 -0
- illusion/_frontend/package.json +27 -0
- illusion/_frontend/src/App.tsx +624 -0
- illusion/_frontend/src/components/CommandPicker.tsx +98 -0
- illusion/_frontend/src/components/Composer.tsx +55 -0
- illusion/_frontend/src/components/ComposerController.tsx +128 -0
- illusion/_frontend/src/components/ConversationView.tsx +750 -0
- illusion/_frontend/src/components/Footer.tsx +25 -0
- illusion/_frontend/src/components/MarkdownContent.tsx +537 -0
- illusion/_frontend/src/components/MarkdownTable.tsx +245 -0
- illusion/_frontend/src/components/ModalHost.tsx +425 -0
- illusion/_frontend/src/components/MultilineTextInput.tsx +250 -0
- illusion/_frontend/src/components/PromptInput.tsx +64 -0
- illusion/_frontend/src/components/SelectModal.tsx +78 -0
- illusion/_frontend/src/components/SidePanel.tsx +175 -0
- illusion/_frontend/src/components/Spinner.tsx +77 -0
- illusion/_frontend/src/components/StatusBar.tsx +142 -0
- illusion/_frontend/src/components/SwarmPanel.tsx +141 -0
- illusion/_frontend/src/components/TodoPanel.tsx +126 -0
- illusion/_frontend/src/components/ToolCallDisplay.tsx +202 -0
- illusion/_frontend/src/components/TranscriptPane.tsx +79 -0
- illusion/_frontend/src/components/WelcomeBanner.tsx +37 -0
- illusion/_frontend/src/hooks/useBackendSession.ts +468 -0
- illusion/_frontend/src/hooks/useTerminalSize.ts +9 -0
- illusion/_frontend/src/i18n.ts +78 -0
- illusion/_frontend/src/index.tsx +42 -0
- illusion/_frontend/src/theme/ThemeContext.tsx +19 -0
- illusion/_frontend/src/theme/builtinThemes.ts +89 -0
- illusion/_frontend/src/types.ts +110 -0
- illusion/_frontend/src/utils/markdown.ts +33 -0
- illusion/_frontend/src/utils/thinking.ts +191 -0
- illusion/_frontend/tsconfig.json +13 -0
- illusion/_web_dist/assets/index-BseIw-ik.css +10 -0
- illusion/_web_dist/assets/index-C_0ZWMuW.js +82 -0
- illusion/_web_dist/index.html +16 -0
- illusion/api/__init__.py +36 -0
- illusion/api/client.py +568 -0
- illusion/api/codex_client.py +563 -0
- illusion/api/compat.py +138 -0
- illusion/api/effort.py +128 -0
- illusion/api/errors.py +57 -0
- illusion/api/openai_client.py +819 -0
- illusion/api/provider.py +148 -0
- illusion/api/registry.py +479 -0
- illusion/api/usage.py +45 -0
- illusion/auth/__init__.py +50 -0
- illusion/auth/copilot.py +419 -0
- illusion/auth/external.py +612 -0
- illusion/auth/flows.py +58 -0
- illusion/auth/manager.py +214 -0
- illusion/auth/storage.py +372 -0
- illusion/bridge/__init__.py +38 -0
- illusion/bridge/manager.py +190 -0
- illusion/bridge/session_runner.py +84 -0
- illusion/bridge/types.py +113 -0
- illusion/bridge/work_secret.py +131 -0
- illusion/cli.py +1228 -0
- illusion/commands/__init__.py +32 -0
- illusion/commands/registry.py +1934 -0
- illusion/config/__init__.py +39 -0
- illusion/config/i18n.py +522 -0
- illusion/config/paths.py +259 -0
- illusion/config/settings.py +564 -0
- illusion/coordinator/__init__.py +41 -0
- illusion/coordinator/agent_definitions.py +1093 -0
- illusion/coordinator/coordinator_mode.py +127 -0
- illusion/engine/__init__.py +95 -0
- illusion/engine/cost_tracker.py +55 -0
- illusion/engine/messages.py +369 -0
- illusion/engine/query.py +632 -0
- illusion/engine/query_engine.py +343 -0
- illusion/engine/stream_events.py +169 -0
- illusion/hooks/__init__.py +67 -0
- illusion/hooks/events.py +43 -0
- illusion/hooks/executor.py +397 -0
- illusion/hooks/hot_reload.py +74 -0
- illusion/hooks/loader.py +133 -0
- illusion/hooks/schemas.py +121 -0
- illusion/hooks/types.py +86 -0
- illusion/mcp/__init__.py +104 -0
- illusion/mcp/client.py +377 -0
- illusion/mcp/config.py +140 -0
- illusion/mcp/types.py +175 -0
- illusion/memory/__init__.py +36 -0
- illusion/memory/manager.py +94 -0
- illusion/memory/memdir.py +58 -0
- illusion/memory/paths.py +57 -0
- illusion/memory/scan.py +120 -0
- illusion/memory/search.py +83 -0
- illusion/memory/types.py +43 -0
- illusion/output_styles/__init__.py +15 -0
- illusion/output_styles/loader.py +64 -0
- illusion/permissions/__init__.py +39 -0
- illusion/permissions/checker.py +174 -0
- illusion/permissions/modes.py +38 -0
- illusion/platforms.py +148 -0
- illusion/plugins/__init__.py +71 -0
- illusion/plugins/bundled/__init__.py +0 -0
- illusion/plugins/installer.py +59 -0
- illusion/plugins/loader.py +301 -0
- illusion/plugins/schemas.py +51 -0
- illusion/plugins/types.py +56 -0
- illusion/prompts/__init__.py +29 -0
- illusion/prompts/claudemd.py +74 -0
- illusion/prompts/context.py +187 -0
- illusion/prompts/environment.py +189 -0
- illusion/prompts/system_prompt.py +155 -0
- illusion/py.typed +0 -0
- illusion/sandbox/__init__.py +29 -0
- illusion/sandbox/adapter.py +174 -0
- illusion/services/__init__.py +59 -0
- illusion/services/compact/__init__.py +1015 -0
- illusion/services/cron.py +338 -0
- illusion/services/cron_scheduler.py +715 -0
- illusion/services/file_history.py +258 -0
- illusion/services/lsp/__init__.py +455 -0
- illusion/services/session_storage.py +237 -0
- illusion/services/token_estimation.py +72 -0
- illusion/skills/__init__.py +60 -0
- illusion/skills/bundled/__init__.py +110 -0
- illusion/skills/bundled/content/batch.md +86 -0
- illusion/skills/bundled/content/coding-guidelines.md +70 -0
- illusion/skills/bundled/content/debug.md +38 -0
- illusion/skills/bundled/content/loop.md +82 -0
- illusion/skills/bundled/content/remember.md +105 -0
- illusion/skills/bundled/content/simplify.md +53 -0
- illusion/skills/bundled/content/skillify.md +113 -0
- illusion/skills/bundled/content/stuck.md +54 -0
- illusion/skills/bundled/content/update-config.md +329 -0
- illusion/skills/bundled/content/verify.md +74 -0
- illusion/skills/loader.py +219 -0
- illusion/skills/registry.py +40 -0
- illusion/skills/types.py +24 -0
- illusion/state/__init__.py +18 -0
- illusion/state/app_state.py +67 -0
- illusion/state/store.py +93 -0
- illusion/swarm/__init__.py +71 -0
- illusion/swarm/agent_executor.py +857 -0
- illusion/swarm/in_process.py +259 -0
- illusion/swarm/subprocess_backend.py +136 -0
- illusion/swarm/team_helpers.py +123 -0
- illusion/swarm/types.py +159 -0
- illusion/swarm/worktree.py +347 -0
- illusion/tasks/__init__.py +33 -0
- illusion/tasks/local_agent_task.py +42 -0
- illusion/tasks/local_shell_task.py +27 -0
- illusion/tasks/manager.py +377 -0
- illusion/tasks/stop_task.py +21 -0
- illusion/tasks/types.py +88 -0
- illusion/tools/__init__.py +126 -0
- illusion/tools/agent_tool.py +388 -0
- illusion/tools/ask_user_question_tool.py +186 -0
- illusion/tools/base.py +149 -0
- illusion/tools/bash_tool.py +413 -0
- illusion/tools/config_tool.py +90 -0
- illusion/tools/cron_tool.py +473 -0
- illusion/tools/enter_plan_mode_tool.py +147 -0
- illusion/tools/enter_worktree_tool.py +188 -0
- illusion/tools/exit_plan_mode_tool.py +69 -0
- illusion/tools/exit_worktree_tool.py +225 -0
- illusion/tools/file_edit_tool.py +283 -0
- illusion/tools/file_read_tool.py +294 -0
- illusion/tools/file_write_tool.py +184 -0
- illusion/tools/glob_tool.py +165 -0
- illusion/tools/grep_tool.py +190 -0
- illusion/tools/list_mcp_resources_tool.py +80 -0
- illusion/tools/lsp_tool.py +333 -0
- illusion/tools/mcp_auth_tool.py +100 -0
- illusion/tools/mcp_tool.py +75 -0
- illusion/tools/notebook_edit_tool.py +242 -0
- illusion/tools/powershell_tool.py +334 -0
- illusion/tools/read_mcp_resource_tool.py +63 -0
- illusion/tools/repl_tool.py +100 -0
- illusion/tools/send_message_tool.py +112 -0
- illusion/tools/shell_common.py +187 -0
- illusion/tools/skill_tool.py +86 -0
- illusion/tools/sleep_tool.py +62 -0
- illusion/tools/structured_output_tool.py +58 -0
- illusion/tools/task_create_tool.py +98 -0
- illusion/tools/task_get_tool.py +94 -0
- illusion/tools/task_list_tool.py +94 -0
- illusion/tools/task_output_tool.py +55 -0
- illusion/tools/task_stop_tool.py +52 -0
- illusion/tools/task_update_tool.py +224 -0
- illusion/tools/team_create_tool.py +236 -0
- illusion/tools/team_delete_tool.py +104 -0
- illusion/tools/todo_write_tool.py +198 -0
- illusion/tools/tool_search_tool.py +156 -0
- illusion/tools/web_fetch_tool.py +264 -0
- illusion/tools/web_search_tool.py +186 -0
- illusion/ui/__init__.py +23 -0
- illusion/ui/app.py +258 -0
- illusion/ui/backend_host.py +1180 -0
- illusion/ui/input.py +86 -0
- illusion/ui/output.py +363 -0
- illusion/ui/permission_dialog.py +47 -0
- illusion/ui/permission_store.py +99 -0
- illusion/ui/protocol.py +384 -0
- illusion/ui/react_launcher.py +280 -0
- illusion/ui/runtime.py +787 -0
- illusion/ui/textual_app.py +603 -0
- illusion/ui/web/__init__.py +10 -0
- illusion/ui/web/server.py +87 -0
- illusion/ui/web/ws_host.py +1197 -0
- illusion/utils/__init__.py +0 -0
- illusion/utils/ripgrep.py +299 -0
- illusion/utils/shell.py +248 -0
- illusion_code-0.1.0.dist-info/METADATA +1159 -0
- illusion_code-0.1.0.dist-info/RECORD +214 -0
- illusion_code-0.1.0.dist-info/WHEEL +4 -0
- illusion_code-0.1.0.dist-info/entry_points.txt +2 -0
- illusion_code-0.1.0.dist-info/licenses/LICENSE +21 -0
illusion/cli.py
ADDED
|
@@ -0,0 +1,1228 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IllusionCode CLI 入口模块
|
|
3
|
+
========================
|
|
4
|
+
|
|
5
|
+
本模块提供 IllusionCode 命令行界面,使用 typer 构建。
|
|
6
|
+
|
|
7
|
+
主要功能:
|
|
8
|
+
- 交互式会话模式
|
|
9
|
+
- 非交互式打印模式
|
|
10
|
+
- MCP 服务器管理
|
|
11
|
+
- 插件管理
|
|
12
|
+
- 认证管理
|
|
13
|
+
- Cron 任务调度管理
|
|
14
|
+
|
|
15
|
+
子命令说明:
|
|
16
|
+
- mcp: MCP 服务器管理(list、add、remove)
|
|
17
|
+
- plugin: 插件管理(list、install、uninstall)
|
|
18
|
+
- auth: 认证管理(login、status、logout、switch)
|
|
19
|
+
- cron: Cron 调度管理(start、stop、status、list、toggle、history、logs)
|
|
20
|
+
|
|
21
|
+
使用示例:
|
|
22
|
+
>>> illusion # 启动交互式会话
|
|
23
|
+
>>> illusion -p "你的提示词" # 非交互式打印模式
|
|
24
|
+
>>> illusion auth login # 认证登录
|
|
25
|
+
>>> illusion mcp list # 列出 MCP 服务器
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json # JSON 解析和序列化
|
|
31
|
+
import sys # 系统相关功能
|
|
32
|
+
from pathlib import Path # 路径操作
|
|
33
|
+
from typing import Any, Optional # 类型注解
|
|
34
|
+
|
|
35
|
+
import typer # CLI 框架
|
|
36
|
+
|
|
37
|
+
# 确保 Windows 上 stdout/stderr 使用 UTF-8,防止通过 tsx 继承 stdio 管道时的 UnicodeEncodeError
|
|
38
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
39
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
40
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
41
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
42
|
+
|
|
43
|
+
# 应用程序版本
|
|
44
|
+
__version__ = "0.1.0"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _version_callback(value: bool) -> None:
|
|
48
|
+
"""版本回调函数
|
|
49
|
+
|
|
50
|
+
当用户使用 --version 选项时调用,打印版本号并退出程序。
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
value: 标志位,当前始终为 True
|
|
54
|
+
"""
|
|
55
|
+
if value:
|
|
56
|
+
print(f"illusion {__version__}") # 打印版本信息
|
|
57
|
+
raise typer.Exit() # 退出程序
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# 创建主应用程序
|
|
61
|
+
app = typer.Typer(
|
|
62
|
+
name="illusion",
|
|
63
|
+
help=(
|
|
64
|
+
"Illusion Code - AI 驱动的编程助手\n"
|
|
65
|
+
"默认启动交互式会话,使用 -p/--print 进入非交互模式"
|
|
66
|
+
),
|
|
67
|
+
add_completion=False,
|
|
68
|
+
rich_markup_mode="rich",
|
|
69
|
+
invoke_without_command=True,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# 子命令
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
# 创建子命令应用(mcp、plugin、auth、cron)
|
|
78
|
+
mcp_app = typer.Typer(name="mcp", help="MCP 服务器管理 / Manage MCP servers")
|
|
79
|
+
plugin_app = typer.Typer(name="plugin", help="插件管理 / Manage plugins")
|
|
80
|
+
auth_app = typer.Typer(name="auth", help="认证管理 / Manage authentication")
|
|
81
|
+
cron_app = typer.Typer(name="cron", help="定时任务管理 / Manage cron scheduler and jobs")
|
|
82
|
+
web_app = typer.Typer(name="web", help="启动 Web 界面 / Launch Web UI")
|
|
83
|
+
|
|
84
|
+
# 注册子命令到主应用
|
|
85
|
+
app.add_typer(mcp_app)
|
|
86
|
+
app.add_typer(plugin_app)
|
|
87
|
+
app.add_typer(auth_app)
|
|
88
|
+
app.add_typer(cron_app)
|
|
89
|
+
app.add_typer(web_app)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ---- mcp 子命令 ----
|
|
93
|
+
|
|
94
|
+
@mcp_app.command("list")
|
|
95
|
+
def mcp_list() -> None:
|
|
96
|
+
"""列出已配置的 MCP 服务器
|
|
97
|
+
|
|
98
|
+
加载当前设置和插件,列出所有已配置的 MCP 服务器及其传输类型。
|
|
99
|
+
"""
|
|
100
|
+
from illusion.config import load_settings
|
|
101
|
+
from illusion.mcp.config import load_mcp_server_configs
|
|
102
|
+
from illusion.plugins import load_plugins
|
|
103
|
+
|
|
104
|
+
settings = load_settings()
|
|
105
|
+
cwd = str(Path.cwd())
|
|
106
|
+
plugins = load_plugins(settings, cwd)
|
|
107
|
+
configs = load_mcp_server_configs(settings, plugins, cwd)
|
|
108
|
+
if not configs:
|
|
109
|
+
print(_t("mcp_none"))
|
|
110
|
+
return
|
|
111
|
+
for name, cfg in configs.items():
|
|
112
|
+
if hasattr(cfg, "type"):
|
|
113
|
+
transport = cfg.type
|
|
114
|
+
if transport == "stdio":
|
|
115
|
+
cmd = getattr(cfg, "command", "")
|
|
116
|
+
detail = f" ({cmd})" if cmd else ""
|
|
117
|
+
elif transport in ("http", "ws"):
|
|
118
|
+
url = getattr(cfg, "url", "")
|
|
119
|
+
detail = f" ({url})" if url else ""
|
|
120
|
+
else:
|
|
121
|
+
detail = ""
|
|
122
|
+
else:
|
|
123
|
+
transport = "unknown"
|
|
124
|
+
detail = ""
|
|
125
|
+
print(f" {name}: {transport}{detail}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@mcp_app.command("add")
|
|
129
|
+
def mcp_add(
|
|
130
|
+
name: str = typer.Argument(..., help="Server name"),
|
|
131
|
+
config_json: str = typer.Argument(..., help="Server config as JSON string"),
|
|
132
|
+
) -> None:
|
|
133
|
+
"""添加 MCP 服务器配置
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
name: 服务器名称
|
|
137
|
+
config_json: 服务器配置的 JSON 字符串
|
|
138
|
+
"""
|
|
139
|
+
from illusion.config import load_settings, save_settings
|
|
140
|
+
from illusion.mcp.types import McpServerConfig
|
|
141
|
+
|
|
142
|
+
settings = load_settings()
|
|
143
|
+
try:
|
|
144
|
+
raw = json.loads(config_json)
|
|
145
|
+
except json.JSONDecodeError as exc:
|
|
146
|
+
print(_t("mcp_invalid_json", exc=exc), file=sys.stderr)
|
|
147
|
+
raise typer.Exit(1)
|
|
148
|
+
try:
|
|
149
|
+
cfg = McpServerConfig.model_validate(raw)
|
|
150
|
+
except Exception as exc:
|
|
151
|
+
print(_t("mcp_invalid_config", exc=exc), file=sys.stderr)
|
|
152
|
+
raise typer.Exit(1)
|
|
153
|
+
if not isinstance(settings.mcp_servers, dict):
|
|
154
|
+
settings.mcp_servers = {}
|
|
155
|
+
settings.mcp_servers[name] = cfg
|
|
156
|
+
save_settings(settings)
|
|
157
|
+
print(_t("mcp_added", name=name))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@mcp_app.command("remove")
|
|
161
|
+
def mcp_remove(
|
|
162
|
+
name: str = typer.Argument(..., help="Server name to remove"),
|
|
163
|
+
) -> None:
|
|
164
|
+
"""移除 MCP 服务器配置
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
name: 要移除的服务器名称
|
|
168
|
+
"""
|
|
169
|
+
from illusion.config import load_settings, save_settings
|
|
170
|
+
|
|
171
|
+
settings = load_settings()
|
|
172
|
+
if not isinstance(settings.mcp_servers, dict) or name not in settings.mcp_servers:
|
|
173
|
+
print(_t("mcp_not_found", name=name), file=sys.stderr)
|
|
174
|
+
raise typer.Exit(1)
|
|
175
|
+
del settings.mcp_servers[name]
|
|
176
|
+
save_settings(settings)
|
|
177
|
+
print(_t("mcp_removed", name=name))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ---- plugin 子命令 ----
|
|
181
|
+
|
|
182
|
+
@plugin_app.command("list")
|
|
183
|
+
def plugin_list() -> None:
|
|
184
|
+
"""列出已安装的插件"""
|
|
185
|
+
from illusion.config import load_settings
|
|
186
|
+
from illusion.plugins import load_plugins
|
|
187
|
+
|
|
188
|
+
settings = load_settings()
|
|
189
|
+
plugins = load_plugins(settings, str(Path.cwd()))
|
|
190
|
+
if not plugins:
|
|
191
|
+
print(_t("plugin_none"))
|
|
192
|
+
return
|
|
193
|
+
for plugin in plugins:
|
|
194
|
+
status = _t("plugin_enabled") if plugin.enabled else _t("plugin_disabled")
|
|
195
|
+
print(f" {plugin.name} [{status}] - {plugin.description or ''}")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@plugin_app.command("install")
|
|
199
|
+
def plugin_install(
|
|
200
|
+
source: str = typer.Argument(..., help="Plugin source (path or URL)"),
|
|
201
|
+
) -> None:
|
|
202
|
+
"""从源路径安装插件"""
|
|
203
|
+
from illusion.plugins.installer import install_plugin_from_path
|
|
204
|
+
|
|
205
|
+
result = install_plugin_from_path(source)
|
|
206
|
+
print(_t("plugin_installed", name=result))
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@plugin_app.command("uninstall")
|
|
210
|
+
def plugin_uninstall(
|
|
211
|
+
name: str = typer.Argument(..., help="Plugin name to uninstall"),
|
|
212
|
+
) -> None:
|
|
213
|
+
"""卸载插件"""
|
|
214
|
+
from illusion.plugins.installer import uninstall_plugin
|
|
215
|
+
|
|
216
|
+
uninstall_plugin(name)
|
|
217
|
+
print(_t("plugin_uninstalled", name=name))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ---- cron 子命令(对齐 openclaw cron CLI) ----
|
|
221
|
+
|
|
222
|
+
@cron_app.command("start")
|
|
223
|
+
def cron_start() -> None:
|
|
224
|
+
"""启动 cron 调度器"""
|
|
225
|
+
from illusion.services.cron_scheduler import is_scheduler_running, start_daemon
|
|
226
|
+
|
|
227
|
+
if is_scheduler_running():
|
|
228
|
+
print(_t("cron_already_running"))
|
|
229
|
+
return
|
|
230
|
+
pid = start_daemon()
|
|
231
|
+
print(_t("cron_started", pid=pid))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
@cron_app.command("stop")
|
|
235
|
+
def cron_stop() -> None:
|
|
236
|
+
"""停止 cron 调度器"""
|
|
237
|
+
from illusion.services.cron_scheduler import stop_scheduler
|
|
238
|
+
|
|
239
|
+
if stop_scheduler():
|
|
240
|
+
print(_t("cron_stopped"))
|
|
241
|
+
else:
|
|
242
|
+
print(_t("cron_not_running"))
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@cron_app.command("status")
|
|
246
|
+
def cron_status_cmd() -> None:
|
|
247
|
+
"""显示 cron 调度器状态和任务统计"""
|
|
248
|
+
from illusion.services.cron_scheduler import scheduler_status
|
|
249
|
+
|
|
250
|
+
status = scheduler_status()
|
|
251
|
+
state = _t("cron_state_running") if status["running"] else _t("cron_state_stopped")
|
|
252
|
+
print(f"Scheduler: {state}" + (f" (pid={status['pid']})" if status["pid"] else ""))
|
|
253
|
+
print(f"Jobs: {status['enabled_jobs']} {_t('cron_enabled')} / {status['total_jobs']} total")
|
|
254
|
+
print(f"Log: {status['log_file']}")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@cron_app.command("list")
|
|
258
|
+
def cron_list_cmd() -> None:
|
|
259
|
+
"""列出所有 cron 任务"""
|
|
260
|
+
from illusion.services.cron import load_cron_jobs
|
|
261
|
+
|
|
262
|
+
jobs = load_cron_jobs()
|
|
263
|
+
if not jobs:
|
|
264
|
+
print(_t("cron_jobs_none"))
|
|
265
|
+
return
|
|
266
|
+
never = _t("cron_never")
|
|
267
|
+
na = _t("cron_na")
|
|
268
|
+
for job in jobs:
|
|
269
|
+
enabled = "+" if job.get("enabled", True) else "-"
|
|
270
|
+
name = job.get("name", job.get("id", "?"))
|
|
271
|
+
schedule = job.get("schedule", "?")
|
|
272
|
+
recurring = _t("cron_recurring") if job.get("recurring", True) else _t("cron_oneshot")
|
|
273
|
+
|
|
274
|
+
last = job.get("last_run", never)
|
|
275
|
+
if last != never:
|
|
276
|
+
last = last[:19]
|
|
277
|
+
last_status = job.get("last_status", "")
|
|
278
|
+
status_indicator = f" [{last_status}]" if last_status else ""
|
|
279
|
+
|
|
280
|
+
next_run = job.get("next_run", na)
|
|
281
|
+
if next_run != na:
|
|
282
|
+
next_run = next_run[:19]
|
|
283
|
+
|
|
284
|
+
errors = job.get("consecutive_errors", 0)
|
|
285
|
+
error_str = f" [{_t('cron_errors', n=errors)}]" if errors > 0 else ""
|
|
286
|
+
|
|
287
|
+
print(f" [{enabled}] {name} {schedule} ({recurring})")
|
|
288
|
+
print(f" {_t('cron_prompt_label')}: {job.get('prompt', '?')[:60]}")
|
|
289
|
+
print(f" {_t('cron_last_label')}: {last}{status_indicator} {_t('cron_next_label')}: {next_run}{error_str}")
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@cron_app.command("toggle")
|
|
293
|
+
def cron_toggle_cmd(
|
|
294
|
+
name: str = typer.Argument(..., help="Job name or ID"),
|
|
295
|
+
enabled: bool = typer.Argument(..., help="true to enable, false to disable"),
|
|
296
|
+
) -> None:
|
|
297
|
+
"""启用或禁用 cron 任务"""
|
|
298
|
+
from illusion.services.cron import set_job_enabled
|
|
299
|
+
|
|
300
|
+
if not set_job_enabled(name, enabled):
|
|
301
|
+
print(_t("cron_job_not_found", name=name))
|
|
302
|
+
raise typer.Exit(1)
|
|
303
|
+
state = _t("cron_enabled") if enabled else _t("cron_disabled")
|
|
304
|
+
print(_t("cron_job_state", name=name, state=state))
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@cron_app.command("run")
|
|
308
|
+
def cron_run_cmd(
|
|
309
|
+
name: str = typer.Argument(..., help="Job name or ID"),
|
|
310
|
+
) -> None:
|
|
311
|
+
"""手动触发执行 cron 任务"""
|
|
312
|
+
import asyncio
|
|
313
|
+
|
|
314
|
+
from illusion.services.cron import get_cron_job
|
|
315
|
+
from illusion.services.cron_scheduler import execute_job
|
|
316
|
+
|
|
317
|
+
job = get_cron_job(name)
|
|
318
|
+
if job is None:
|
|
319
|
+
print(_t("cron_job_not_found", name=name))
|
|
320
|
+
raise typer.Exit(1)
|
|
321
|
+
|
|
322
|
+
prompt = job.get("prompt", "")
|
|
323
|
+
if not prompt:
|
|
324
|
+
print(_t("cron_no_prompt", name=name))
|
|
325
|
+
raise typer.Exit(1)
|
|
326
|
+
|
|
327
|
+
print(_t("cron_running_job", name=name))
|
|
328
|
+
entry = asyncio.run(execute_job(job))
|
|
329
|
+
status = entry.get("status", "unknown")
|
|
330
|
+
rc = entry.get("returncode", "?")
|
|
331
|
+
print(_t("cron_finished", status=status, rc=rc))
|
|
332
|
+
|
|
333
|
+
stdout = entry.get("stdout", "").strip()
|
|
334
|
+
stderr = entry.get("stderr", "").strip()
|
|
335
|
+
if stdout:
|
|
336
|
+
print(f"{_t('cron_output')}\n{stdout}")
|
|
337
|
+
if stderr and status != "success":
|
|
338
|
+
print(f"{_t('cron_error')}\n{stderr}")
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
@cron_app.command("history")
|
|
342
|
+
def cron_history_cmd(
|
|
343
|
+
name: str | None = typer.Argument(None, help="Filter by job name"),
|
|
344
|
+
limit: int = typer.Option(20, "--limit", "-n", help="Number of entries"),
|
|
345
|
+
) -> None:
|
|
346
|
+
"""显示 cron 执行历史记录"""
|
|
347
|
+
from illusion.services.cron_scheduler import load_history
|
|
348
|
+
|
|
349
|
+
entries = load_history(limit=limit, job_name=name)
|
|
350
|
+
if not entries:
|
|
351
|
+
print(_t("cron_no_history"))
|
|
352
|
+
return
|
|
353
|
+
for entry in entries:
|
|
354
|
+
ts = entry.get("started_at", "?")[:19]
|
|
355
|
+
status = entry.get("status", "?")
|
|
356
|
+
rc = entry.get("returncode", "?")
|
|
357
|
+
job_name = entry.get("name", "?")
|
|
358
|
+
prompt_preview = entry.get("prompt", "")[:40]
|
|
359
|
+
print(f" {ts} {job_name} {status} (rc={rc})")
|
|
360
|
+
if prompt_preview:
|
|
361
|
+
print(f" {_t('cron_prompt_label')}: {prompt_preview}")
|
|
362
|
+
stderr = entry.get("stderr", "").strip()
|
|
363
|
+
if stderr and status != "success":
|
|
364
|
+
for line in stderr.splitlines()[:3]:
|
|
365
|
+
print(f" {_t('cron_error')} {line}")
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
@cron_app.command("logs")
|
|
369
|
+
def cron_logs_cmd(
|
|
370
|
+
lines: int = typer.Option(30, "--lines", "-n", help="Number of lines"),
|
|
371
|
+
) -> None:
|
|
372
|
+
"""显示 cron 调度器日志"""
|
|
373
|
+
from illusion.config.paths import get_logs_dir
|
|
374
|
+
|
|
375
|
+
log_path = get_logs_dir() / "cron_scheduler.log"
|
|
376
|
+
if not log_path.exists():
|
|
377
|
+
print(_t("cron_no_log"))
|
|
378
|
+
return
|
|
379
|
+
content = log_path.read_text(encoding="utf-8", errors="replace")
|
|
380
|
+
tail = content.splitlines()[-lines:]
|
|
381
|
+
for line in tail:
|
|
382
|
+
print(line)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
# ---- auth 子命令 ----
|
|
386
|
+
|
|
387
|
+
# i18n 从共享模块导入
|
|
388
|
+
from illusion.config.i18n import MESSAGES as _I18N, t as _t # noqa: E402
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _ensure_language() -> str:
|
|
392
|
+
"""确保 ui_language 已设置,未设置时让用户选择
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
str: 当前 ui_language 值
|
|
396
|
+
"""
|
|
397
|
+
from illusion.config import load_settings, save_settings
|
|
398
|
+
settings = load_settings()
|
|
399
|
+
if settings.ui_language:
|
|
400
|
+
return settings.ui_language
|
|
401
|
+
|
|
402
|
+
print(_t("select_language"))
|
|
403
|
+
print(" 1. 中文 (zh-CN)")
|
|
404
|
+
print(" 2. English (en-US)")
|
|
405
|
+
raw = typer.prompt("1/2", default="1")
|
|
406
|
+
lang = "zh-CN" if raw.strip() == "1" else "en-US"
|
|
407
|
+
settings.ui_language = lang
|
|
408
|
+
save_settings(settings)
|
|
409
|
+
return lang
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
_PROVIDER_OPTIONS: list[tuple[str, dict[str, str]]] = [
|
|
413
|
+
("custom", _I18N["custom_provider"]),
|
|
414
|
+
("anthropic", _I18N["anthropic_label"]),
|
|
415
|
+
("openai", _I18N["openai_label"]),
|
|
416
|
+
("copilot", _I18N["copilot_label"]),
|
|
417
|
+
("codex", _I18N["codex_label"]),
|
|
418
|
+
]
|
|
419
|
+
|
|
420
|
+
_API_FORMAT_OPTIONS: list[tuple[str, str]] = [
|
|
421
|
+
("openai", "OpenAI"),
|
|
422
|
+
("anthropic", "Anthropic"),
|
|
423
|
+
]
|
|
424
|
+
|
|
425
|
+
_DEFAULT_ENDPOINTS: dict[str, str] = {
|
|
426
|
+
"anthropic": "https://api.anthropic.com",
|
|
427
|
+
"openai": "https://api.openai.com/v1",
|
|
428
|
+
"copilot": "https://api.githubcopilot.com",
|
|
429
|
+
"codex": "https://chatgpt.com/backend-api",
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
_DEFAULT_MODELS: dict[str, str] = {
|
|
433
|
+
"anthropic": "claude-sonnet-4-6",
|
|
434
|
+
"openai": "gpt-5.4",
|
|
435
|
+
"copilot": "gpt-5.5",
|
|
436
|
+
"codex": "codex-mini",
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
@auth_app.command("login")
|
|
441
|
+
def auth_login() -> None:
|
|
442
|
+
"""交互式配置提供商认证
|
|
443
|
+
|
|
444
|
+
流程:选择提供商 → 认证 → 保存
|
|
445
|
+
Copilot 使用 GitHub OAuth 设备码流程,其他提供商使用 API 密钥。
|
|
446
|
+
"""
|
|
447
|
+
from illusion.auth.flows import ApiKeyFlow
|
|
448
|
+
from illusion.auth.manager import AuthManager
|
|
449
|
+
from illusion.auth.storage import store_env_credential
|
|
450
|
+
|
|
451
|
+
_ensure_language()
|
|
452
|
+
manager = AuthManager()
|
|
453
|
+
|
|
454
|
+
# 1. 选择提供商
|
|
455
|
+
print(_t("select_provider"))
|
|
456
|
+
for i, (key, labels) in enumerate(_PROVIDER_OPTIONS, 1):
|
|
457
|
+
lang = manager.settings.ui_language or "en-US"
|
|
458
|
+
label = labels.get(lang, labels.get("en-US", key))
|
|
459
|
+
print(f" {i}. {label}")
|
|
460
|
+
raw = typer.prompt(_t("enter_number"), default="1")
|
|
461
|
+
try:
|
|
462
|
+
idx = int(raw.strip()) - 1
|
|
463
|
+
if 0 <= idx < len(_PROVIDER_OPTIONS):
|
|
464
|
+
provider_choice = _PROVIDER_OPTIONS[idx][0]
|
|
465
|
+
else:
|
|
466
|
+
print(_t("invalid_selection"), file=sys.stderr)
|
|
467
|
+
raise typer.Exit(1)
|
|
468
|
+
except ValueError:
|
|
469
|
+
print(_t("invalid_selection"), file=sys.stderr)
|
|
470
|
+
raise typer.Exit(1)
|
|
471
|
+
|
|
472
|
+
# --- Copilot 走设备码 OAuth 流程 ---
|
|
473
|
+
if provider_choice == "copilot":
|
|
474
|
+
_copilot_login(manager)
|
|
475
|
+
return
|
|
476
|
+
|
|
477
|
+
# --- Codex 走外部 CLI 凭据读取流程 ---
|
|
478
|
+
if provider_choice == "codex":
|
|
479
|
+
_codex_login(manager)
|
|
480
|
+
return
|
|
481
|
+
|
|
482
|
+
# --- 其他提供商走 API 密钥流程 ---
|
|
483
|
+
|
|
484
|
+
# 2. 确定 API 格式
|
|
485
|
+
if provider_choice == "anthropic":
|
|
486
|
+
api_format = "anthropic"
|
|
487
|
+
elif provider_choice == "openai":
|
|
488
|
+
api_format = "openai"
|
|
489
|
+
else:
|
|
490
|
+
# 自定义提供商:让用户选择 API 格式
|
|
491
|
+
print(_t("select_api_format"))
|
|
492
|
+
for i, (fmt, label) in enumerate(_API_FORMAT_OPTIONS, 1):
|
|
493
|
+
print(f" {i}. {label}")
|
|
494
|
+
raw = typer.prompt(_t("enter_number"), default="1")
|
|
495
|
+
try:
|
|
496
|
+
idx = int(raw.strip()) - 1
|
|
497
|
+
if 0 <= idx < len(_API_FORMAT_OPTIONS):
|
|
498
|
+
api_format = _API_FORMAT_OPTIONS[idx][0]
|
|
499
|
+
else:
|
|
500
|
+
print(_t("invalid_selection"), file=sys.stderr)
|
|
501
|
+
raise typer.Exit(1)
|
|
502
|
+
except ValueError:
|
|
503
|
+
print(_t("invalid_selection"), file=sys.stderr)
|
|
504
|
+
raise typer.Exit(1)
|
|
505
|
+
|
|
506
|
+
# 3. 输入端点
|
|
507
|
+
default_ep = _DEFAULT_ENDPOINTS.get(provider_choice, "")
|
|
508
|
+
if default_ep:
|
|
509
|
+
prompt_text = f"{_t('enter_endpoint')} ({_t('default_endpoint')}: {default_ep}): "
|
|
510
|
+
endpoint = input(prompt_text).strip()
|
|
511
|
+
if not endpoint:
|
|
512
|
+
endpoint = default_ep
|
|
513
|
+
else:
|
|
514
|
+
endpoint = input(f"{_t('enter_endpoint')}: ").strip()
|
|
515
|
+
if not endpoint:
|
|
516
|
+
print(_t("endpoint_required"), file=sys.stderr)
|
|
517
|
+
raise typer.Exit(1)
|
|
518
|
+
|
|
519
|
+
# 4. 输入 API 密钥
|
|
520
|
+
flow = ApiKeyFlow(prompt_text=_t("enter_api_key"))
|
|
521
|
+
try:
|
|
522
|
+
api_key = flow.run()
|
|
523
|
+
except ValueError:
|
|
524
|
+
print(_t("api_key_required"), file=sys.stderr)
|
|
525
|
+
raise typer.Exit(1)
|
|
526
|
+
|
|
527
|
+
# 5. 输入模型名称
|
|
528
|
+
default_model = _DEFAULT_MODELS.get(provider_choice, "")
|
|
529
|
+
if default_model:
|
|
530
|
+
prompt_text = f"{_t('enter_model')} ({_t('default_endpoint')}: {default_model}): "
|
|
531
|
+
model_name = input(prompt_text).strip()
|
|
532
|
+
if not model_name:
|
|
533
|
+
model_name = default_model
|
|
534
|
+
else:
|
|
535
|
+
model_name = input(f"{_t('enter_model')}: ").strip()
|
|
536
|
+
if not model_name:
|
|
537
|
+
print(_t("model_required"), file=sys.stderr)
|
|
538
|
+
raise typer.Exit(1)
|
|
539
|
+
|
|
540
|
+
# 6. 分配 env_N 并保存
|
|
541
|
+
envs = manager.list_envs()
|
|
542
|
+
if envs:
|
|
543
|
+
# 找到下一个可用的 env_N
|
|
544
|
+
existing_nums = []
|
|
545
|
+
for k in envs:
|
|
546
|
+
try:
|
|
547
|
+
existing_nums.append(int(k.split("_")[1]))
|
|
548
|
+
except (ValueError, IndexError):
|
|
549
|
+
pass
|
|
550
|
+
next_num = max(existing_nums, default=0) + 1
|
|
551
|
+
else:
|
|
552
|
+
next_num = 1
|
|
553
|
+
env_key = f"env_{next_num}"
|
|
554
|
+
|
|
555
|
+
# 保存到 settings.json
|
|
556
|
+
env_config = {
|
|
557
|
+
"api_format": api_format,
|
|
558
|
+
"base_url": endpoint,
|
|
559
|
+
"api_key": "", # 不在 settings.json 中存储实际密钥
|
|
560
|
+
"model_1": model_name,
|
|
561
|
+
}
|
|
562
|
+
setattr(manager.settings, env_key, env_config)
|
|
563
|
+
manager.settings.model = f"{env_key}:model_1"
|
|
564
|
+
manager.save_settings()
|
|
565
|
+
|
|
566
|
+
# 保存密钥到 credentials.json
|
|
567
|
+
store_env_credential(env_key, "api_key", api_key)
|
|
568
|
+
|
|
569
|
+
print(_t("env_saved", env_key=env_key))
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _copilot_login(manager: Any) -> None:
|
|
573
|
+
"""Copilot 设备码 OAuth 认证流程
|
|
574
|
+
|
|
575
|
+
Args:
|
|
576
|
+
manager: AuthManager 实例
|
|
577
|
+
"""
|
|
578
|
+
|
|
579
|
+
from illusion.auth.copilot import CopilotAuth
|
|
580
|
+
|
|
581
|
+
copilot = CopilotAuth()
|
|
582
|
+
|
|
583
|
+
# 1. 启动设备码流程
|
|
584
|
+
try:
|
|
585
|
+
flow = copilot.start_device_flow()
|
|
586
|
+
except Exception as exc:
|
|
587
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
588
|
+
raise typer.Exit(1)
|
|
589
|
+
|
|
590
|
+
# 2. 显示用户码和验证 URL
|
|
591
|
+
print(_t("copilot_open_url"))
|
|
592
|
+
print(f" {flow['verification_uri']}")
|
|
593
|
+
print(_t("copilot_enter_code", code=flow["user_code"]))
|
|
594
|
+
print()
|
|
595
|
+
print(_t("copilot_waiting"))
|
|
596
|
+
|
|
597
|
+
# 3. 轮询等待授权
|
|
598
|
+
try:
|
|
599
|
+
success = copilot.poll_for_token(flow["device_code"])
|
|
600
|
+
except RuntimeError as exc:
|
|
601
|
+
msg = str(exc)
|
|
602
|
+
if "过期" in msg or "expired" in msg.lower():
|
|
603
|
+
print(_t("copilot_device_expired"), file=sys.stderr)
|
|
604
|
+
elif "拒绝" in msg or "denied" in msg.lower():
|
|
605
|
+
print(_t("copilot_auth_denied"), file=sys.stderr)
|
|
606
|
+
elif "订阅" in msg or "subscription" in msg.lower():
|
|
607
|
+
print(_t("copilot_no_subscription"), file=sys.stderr)
|
|
608
|
+
else:
|
|
609
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
610
|
+
raise typer.Exit(1)
|
|
611
|
+
|
|
612
|
+
if not success:
|
|
613
|
+
print(_t("copilot_device_expired"), file=sys.stderr)
|
|
614
|
+
raise typer.Exit(1)
|
|
615
|
+
|
|
616
|
+
status = copilot.get_status()
|
|
617
|
+
username = status.get("username") or ""
|
|
618
|
+
print(_t("copilot_auth_success", user=username))
|
|
619
|
+
|
|
620
|
+
# 4. 输入模型名称
|
|
621
|
+
default_model = _DEFAULT_MODELS.get("copilot", "gpt-5.5")
|
|
622
|
+
prompt_text = f"{_t('enter_model')} ({_t('default_endpoint')}: {default_model}): "
|
|
623
|
+
model_name = input(prompt_text).strip()
|
|
624
|
+
if not model_name:
|
|
625
|
+
model_name = default_model
|
|
626
|
+
|
|
627
|
+
# 5. 分配 env_N 并保存
|
|
628
|
+
envs = manager.list_envs()
|
|
629
|
+
if envs:
|
|
630
|
+
existing_nums = []
|
|
631
|
+
for k in envs:
|
|
632
|
+
try:
|
|
633
|
+
existing_nums.append(int(k.split("_")[1]))
|
|
634
|
+
except (ValueError, IndexError):
|
|
635
|
+
pass
|
|
636
|
+
next_num = max(existing_nums, default=0) + 1
|
|
637
|
+
else:
|
|
638
|
+
next_num = 1
|
|
639
|
+
env_key = f"env_{next_num}"
|
|
640
|
+
|
|
641
|
+
env_config = {
|
|
642
|
+
"api_format": "openai",
|
|
643
|
+
"base_url": _DEFAULT_ENDPOINTS["copilot"],
|
|
644
|
+
"api_key": "",
|
|
645
|
+
"model_1": model_name,
|
|
646
|
+
"provider": "copilot",
|
|
647
|
+
}
|
|
648
|
+
setattr(manager.settings, env_key, env_config)
|
|
649
|
+
manager.settings.model = f"{env_key}:model_1"
|
|
650
|
+
manager.save_settings()
|
|
651
|
+
|
|
652
|
+
print(_t("env_saved", env_key=env_key))
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _codex_login(manager: Any) -> None:
|
|
656
|
+
"""Codex 外部 CLI 凭据读取流程
|
|
657
|
+
|
|
658
|
+
从 ~/.codex/auth.json 读取已由 Codex CLI 管理的认证信息。
|
|
659
|
+
|
|
660
|
+
Args:
|
|
661
|
+
manager: AuthManager 实例
|
|
662
|
+
"""
|
|
663
|
+
from illusion.auth.external import default_binding_for_provider, load_external_credential
|
|
664
|
+
|
|
665
|
+
# 1. 检查 Codex CLI 认证是否存在
|
|
666
|
+
binding = default_binding_for_provider("openai_codex")
|
|
667
|
+
try:
|
|
668
|
+
cred = load_external_credential(binding)
|
|
669
|
+
except (ValueError, FileNotFoundError):
|
|
670
|
+
print(_t("codex_not_found"), file=sys.stderr)
|
|
671
|
+
raise typer.Exit(1)
|
|
672
|
+
|
|
673
|
+
username = cred.profile_label or cred.value[:8] + "..."
|
|
674
|
+
print(_t("codex_auth_success", user=username))
|
|
675
|
+
|
|
676
|
+
# 2. 输入模型名称
|
|
677
|
+
default_model = _DEFAULT_MODELS.get("codex", "codex-mini")
|
|
678
|
+
prompt_text = f"{_t('enter_model')} ({_t('default_endpoint')}: {default_model}): "
|
|
679
|
+
model_name = input(prompt_text).strip()
|
|
680
|
+
if not model_name:
|
|
681
|
+
model_name = default_model
|
|
682
|
+
|
|
683
|
+
# 3. 分配 env_N 并保存
|
|
684
|
+
envs = manager.list_envs()
|
|
685
|
+
if envs:
|
|
686
|
+
existing_nums = []
|
|
687
|
+
for k in envs:
|
|
688
|
+
try:
|
|
689
|
+
existing_nums.append(int(k.split("_")[1]))
|
|
690
|
+
except (ValueError, IndexError):
|
|
691
|
+
pass
|
|
692
|
+
next_num = max(existing_nums, default=0) + 1
|
|
693
|
+
else:
|
|
694
|
+
next_num = 1
|
|
695
|
+
env_key = f"env_{next_num}"
|
|
696
|
+
|
|
697
|
+
env_config = {
|
|
698
|
+
"api_format": "openai",
|
|
699
|
+
"base_url": _DEFAULT_ENDPOINTS["codex"],
|
|
700
|
+
"api_key": "",
|
|
701
|
+
"model_1": model_name,
|
|
702
|
+
"provider": "codex",
|
|
703
|
+
}
|
|
704
|
+
setattr(manager.settings, env_key, env_config)
|
|
705
|
+
manager.settings.model = f"{env_key}:model_1"
|
|
706
|
+
manager.save_settings()
|
|
707
|
+
|
|
708
|
+
print(_t("env_saved", env_key=env_key))
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
@auth_app.command("status")
|
|
712
|
+
def auth_status_cmd() -> None:
|
|
713
|
+
"""显示所有环境的认证状态"""
|
|
714
|
+
from illusion.auth.manager import AuthManager
|
|
715
|
+
|
|
716
|
+
_ensure_language()
|
|
717
|
+
manager = AuthManager()
|
|
718
|
+
statuses = manager.get_env_credential_statuses()
|
|
719
|
+
|
|
720
|
+
if not statuses:
|
|
721
|
+
print(_t("no_envs"))
|
|
722
|
+
return
|
|
723
|
+
|
|
724
|
+
print(_t("env_status_title"))
|
|
725
|
+
|
|
726
|
+
# 列宽
|
|
727
|
+
col_env = 10
|
|
728
|
+
col_format = 12
|
|
729
|
+
col_model = 28
|
|
730
|
+
col_endpoint = 36
|
|
731
|
+
col_cred = 10
|
|
732
|
+
|
|
733
|
+
header = (
|
|
734
|
+
f"{_t('col_env'):<{col_env}} "
|
|
735
|
+
f"{_t('col_format'):<{col_format}} "
|
|
736
|
+
f"{_t('col_model'):<{col_model}} "
|
|
737
|
+
f"{_t('col_endpoint'):<{col_endpoint}} "
|
|
738
|
+
f"{_t('col_credential'):<{col_cred}} "
|
|
739
|
+
)
|
|
740
|
+
print(header)
|
|
741
|
+
print("-" * len(header))
|
|
742
|
+
|
|
743
|
+
for name, info in statuses.items():
|
|
744
|
+
cred_str = _t("configured") if info["has_credential"] else _t("missing")
|
|
745
|
+
active_str = f" {_t('active_mark')}" if info["active"] else ""
|
|
746
|
+
ep = info["base_url"] or "-"
|
|
747
|
+
print(
|
|
748
|
+
f"{name:<{col_env}} "
|
|
749
|
+
f"{info['api_format']:<{col_format}} "
|
|
750
|
+
f"{info['model']:<{col_model}} "
|
|
751
|
+
f"{ep:<{col_endpoint}} "
|
|
752
|
+
f"{cred_str:<{col_cred}} "
|
|
753
|
+
f"{active_str}"
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
@auth_app.command("logout")
|
|
758
|
+
def auth_logout(
|
|
759
|
+
env_key: Optional[str] = typer.Argument(None, help="Environment to clear (e.g. env_1)"),
|
|
760
|
+
) -> None:
|
|
761
|
+
"""清除环境的已存储凭据
|
|
762
|
+
|
|
763
|
+
Args:
|
|
764
|
+
env_key: 要清除的环境,默认交互式选择
|
|
765
|
+
"""
|
|
766
|
+
from illusion.auth.manager import AuthManager
|
|
767
|
+
|
|
768
|
+
_ensure_language()
|
|
769
|
+
manager = AuthManager()
|
|
770
|
+
|
|
771
|
+
if env_key is None:
|
|
772
|
+
envs = manager.list_envs()
|
|
773
|
+
if not envs:
|
|
774
|
+
print(_t("no_envs"))
|
|
775
|
+
raise typer.Exit(1)
|
|
776
|
+
print(_t("select_env_to_logout"))
|
|
777
|
+
env_keys = list(envs.keys())
|
|
778
|
+
for i, k in enumerate(env_keys, 1):
|
|
779
|
+
print(f" {i}. {k}")
|
|
780
|
+
raw = typer.prompt(_t("enter_number"), default="1")
|
|
781
|
+
try:
|
|
782
|
+
idx = int(raw.strip()) - 1
|
|
783
|
+
if 0 <= idx < len(env_keys):
|
|
784
|
+
env_key = env_keys[idx]
|
|
785
|
+
else:
|
|
786
|
+
print(_t("invalid_selection"), file=sys.stderr)
|
|
787
|
+
raise typer.Exit(1)
|
|
788
|
+
except ValueError:
|
|
789
|
+
print(_t("invalid_selection"), file=sys.stderr)
|
|
790
|
+
raise typer.Exit(1)
|
|
791
|
+
|
|
792
|
+
manager.clear_env_api_key(env_key)
|
|
793
|
+
print(_t("credential_cleared", env_key=env_key))
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
@auth_app.command("switch")
|
|
797
|
+
def auth_switch(
|
|
798
|
+
env_key: Optional[str] = typer.Argument(None, help="Environment to switch to (e.g. env_1)"),
|
|
799
|
+
) -> None:
|
|
800
|
+
"""切换活动环境
|
|
801
|
+
|
|
802
|
+
Args:
|
|
803
|
+
env_key: 要切换的环境,无参数时交互式选择
|
|
804
|
+
"""
|
|
805
|
+
from illusion.auth.manager import AuthManager
|
|
806
|
+
|
|
807
|
+
_ensure_language()
|
|
808
|
+
manager = AuthManager()
|
|
809
|
+
|
|
810
|
+
if env_key is None:
|
|
811
|
+
envs = manager.list_envs()
|
|
812
|
+
if not envs:
|
|
813
|
+
print(_t("no_envs"))
|
|
814
|
+
raise typer.Exit(1)
|
|
815
|
+
print(_t("select_env_to_switch"))
|
|
816
|
+
env_keys = list(envs.keys())
|
|
817
|
+
for i, k in enumerate(env_keys, 1):
|
|
818
|
+
print(f" {i}. {k}")
|
|
819
|
+
raw = typer.prompt(_t("enter_number"), default="1")
|
|
820
|
+
try:
|
|
821
|
+
idx = int(raw.strip()) - 1
|
|
822
|
+
if 0 <= idx < len(env_keys):
|
|
823
|
+
env_key = env_keys[idx]
|
|
824
|
+
else:
|
|
825
|
+
print(_t("invalid_selection"), file=sys.stderr)
|
|
826
|
+
raise typer.Exit(1)
|
|
827
|
+
except ValueError:
|
|
828
|
+
print(_t("invalid_selection"), file=sys.stderr)
|
|
829
|
+
raise typer.Exit(1)
|
|
830
|
+
|
|
831
|
+
try:
|
|
832
|
+
manager.use_env(env_key)
|
|
833
|
+
except ValueError:
|
|
834
|
+
print(_t("env_not_found", env_key=env_key), file=sys.stderr)
|
|
835
|
+
raise typer.Exit(1)
|
|
836
|
+
print(_t("switched_to", env_key=env_key))
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
@auth_app.command("add-model")
|
|
840
|
+
def auth_add_model(
|
|
841
|
+
env_key: str = typer.Argument(..., help="Environment key (e.g. env_1)"),
|
|
842
|
+
model_name: str = typer.Argument(..., help="Model name to add"),
|
|
843
|
+
) -> None:
|
|
844
|
+
"""在已有的 env_N 中增加模型(model_N)
|
|
845
|
+
|
|
846
|
+
Args:
|
|
847
|
+
env_key: 环境键名,如 env_1
|
|
848
|
+
model_name: 要添加的模型名称
|
|
849
|
+
"""
|
|
850
|
+
from illusion.auth.manager import AuthManager
|
|
851
|
+
|
|
852
|
+
_ensure_language()
|
|
853
|
+
manager = AuthManager()
|
|
854
|
+
|
|
855
|
+
env = manager.settings.get_env(env_key)
|
|
856
|
+
if env is None:
|
|
857
|
+
print(_t("env_not_found", env_key=env_key), file=sys.stderr)
|
|
858
|
+
raise typer.Exit(1)
|
|
859
|
+
|
|
860
|
+
# 找到下一个可用的 model_N 编号
|
|
861
|
+
existing = []
|
|
862
|
+
for k in env.list_models():
|
|
863
|
+
try:
|
|
864
|
+
existing.append(int(k.split("_")[1]))
|
|
865
|
+
except (ValueError, IndexError):
|
|
866
|
+
pass
|
|
867
|
+
next_num = max(existing, default=0) + 1
|
|
868
|
+
model_key = f"model_{next_num}"
|
|
869
|
+
|
|
870
|
+
# 写入配置
|
|
871
|
+
env_config = env.model_dump()
|
|
872
|
+
env_config[model_key] = model_name
|
|
873
|
+
setattr(manager.settings, env_key, env_config)
|
|
874
|
+
manager.save_settings()
|
|
875
|
+
|
|
876
|
+
print(_t("model_added", env_key=env_key, model_key=model_key, model_name=model_name))
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
# ---- web 子命令 ----
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
@web_app.callback(invoke_without_command=True)
|
|
883
|
+
def web_start(
|
|
884
|
+
port: int = typer.Option(3000, "--port", "-p", help="Web 服务端口"),
|
|
885
|
+
host: str = typer.Option("127.0.0.1", "--host", help="监听地址"),
|
|
886
|
+
dev: bool = typer.Option(False, "--dev", help="开发模式(启用 CORS,不 serve 静态文件)"),
|
|
887
|
+
model: Optional[str] = typer.Option(None, "--model", "-m", help="指定模型"),
|
|
888
|
+
prompt: Optional[str] = typer.Option(None, "--prompt", help="初始提示词"),
|
|
889
|
+
) -> None:
|
|
890
|
+
"""启动 Illusion Code Web 界面 / Launch Illusion Code Web UI"""
|
|
891
|
+
import threading
|
|
892
|
+
import uvicorn
|
|
893
|
+
from illusion.ui.web.server import create_app
|
|
894
|
+
from illusion.ui.web.ws_host import WebHostConfig
|
|
895
|
+
|
|
896
|
+
config = WebHostConfig(
|
|
897
|
+
model=model,
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
app = create_app(dev=dev, host_config=config)
|
|
901
|
+
|
|
902
|
+
url = f"http://{host}:{port}"
|
|
903
|
+
typer.echo(f"Illusion Code Web UI: {url}")
|
|
904
|
+
if not dev:
|
|
905
|
+
import webbrowser
|
|
906
|
+
threading.Thread(target=webbrowser.open, args=(url,), daemon=True).start()
|
|
907
|
+
|
|
908
|
+
uvicorn.run(app, host=host, port=port, log_level="warning")
|
|
909
|
+
|
|
910
|
+
|
|
911
|
+
# ---------------------------------------------------------------------------
|
|
912
|
+
# 主命令
|
|
913
|
+
# ---------------------------------------------------------------------------
|
|
914
|
+
|
|
915
|
+
@app.callback(invoke_without_command=True)
|
|
916
|
+
def main(
|
|
917
|
+
ctx: typer.Context,
|
|
918
|
+
version: bool = typer.Option(
|
|
919
|
+
False,
|
|
920
|
+
"--version",
|
|
921
|
+
"-v",
|
|
922
|
+
help="Show version and exit",
|
|
923
|
+
callback=_version_callback,
|
|
924
|
+
is_eager=True,
|
|
925
|
+
),
|
|
926
|
+
# --- Session ---
|
|
927
|
+
continue_session: bool = typer.Option(
|
|
928
|
+
False,
|
|
929
|
+
"--continue",
|
|
930
|
+
"-c",
|
|
931
|
+
help="Continue the most recent conversation in the current directory",
|
|
932
|
+
rich_help_panel="Session",
|
|
933
|
+
),
|
|
934
|
+
resume: str | None = typer.Option(
|
|
935
|
+
None,
|
|
936
|
+
"--resume",
|
|
937
|
+
"-r",
|
|
938
|
+
help="Resume a conversation by session ID, or open picker",
|
|
939
|
+
rich_help_panel="Session",
|
|
940
|
+
),
|
|
941
|
+
name: str | None = typer.Option(
|
|
942
|
+
None,
|
|
943
|
+
"--name",
|
|
944
|
+
"-n",
|
|
945
|
+
help="Set a display name for this session",
|
|
946
|
+
rich_help_panel="Session",
|
|
947
|
+
),
|
|
948
|
+
# --- Model & Effort ---
|
|
949
|
+
model: str | None = typer.Option(
|
|
950
|
+
None,
|
|
951
|
+
"--model",
|
|
952
|
+
"-m",
|
|
953
|
+
help="Model alias (e.g. 'sonnet', 'opus') or full model ID",
|
|
954
|
+
rich_help_panel="Model & Effort",
|
|
955
|
+
),
|
|
956
|
+
effort: str | None = typer.Option(
|
|
957
|
+
None,
|
|
958
|
+
"--effort",
|
|
959
|
+
help="Effort level for the session (low, medium, high, max)",
|
|
960
|
+
rich_help_panel="Model & Effort",
|
|
961
|
+
),
|
|
962
|
+
verbose: bool = typer.Option(
|
|
963
|
+
False,
|
|
964
|
+
"--verbose",
|
|
965
|
+
help="Override verbose mode setting from config",
|
|
966
|
+
rich_help_panel="Model & Effort",
|
|
967
|
+
),
|
|
968
|
+
max_turns: int | None = typer.Option(
|
|
969
|
+
None,
|
|
970
|
+
"--max-turns",
|
|
971
|
+
help="Maximum number of agentic turns (useful with --print)",
|
|
972
|
+
rich_help_panel="Model & Effort",
|
|
973
|
+
),
|
|
974
|
+
# --- Output ---
|
|
975
|
+
print_mode: str | None = typer.Option(
|
|
976
|
+
None,
|
|
977
|
+
"--print",
|
|
978
|
+
"-p",
|
|
979
|
+
help="Print response and exit. Pass your prompt as the value: -p 'your prompt'",
|
|
980
|
+
rich_help_panel="Output",
|
|
981
|
+
),
|
|
982
|
+
output_format: str | None = typer.Option(
|
|
983
|
+
None,
|
|
984
|
+
"--output-format",
|
|
985
|
+
help="Output format with --print: text (default), json, or stream-json",
|
|
986
|
+
rich_help_panel="Output",
|
|
987
|
+
),
|
|
988
|
+
# --- Permissions ---
|
|
989
|
+
permission_mode: str | None = typer.Option(
|
|
990
|
+
None,
|
|
991
|
+
"--permission-mode",
|
|
992
|
+
help="Permission mode: default, plan, or full_auto",
|
|
993
|
+
rich_help_panel="Permissions",
|
|
994
|
+
),
|
|
995
|
+
dangerously_skip_permissions: bool = typer.Option(
|
|
996
|
+
False,
|
|
997
|
+
"--dangerously-skip-permissions",
|
|
998
|
+
help="Bypass all permission checks (only for sandboxed environments)",
|
|
999
|
+
rich_help_panel="Permissions",
|
|
1000
|
+
),
|
|
1001
|
+
allowed_tools: Optional[list[str]] = typer.Option(
|
|
1002
|
+
None,
|
|
1003
|
+
"--allowed-tools",
|
|
1004
|
+
help="Comma or space-separated list of tool names to allow",
|
|
1005
|
+
rich_help_panel="Permissions",
|
|
1006
|
+
),
|
|
1007
|
+
disallowed_tools: Optional[list[str]] = typer.Option(
|
|
1008
|
+
None,
|
|
1009
|
+
"--disallowed-tools",
|
|
1010
|
+
help="Comma or space-separated list of tool names to deny",
|
|
1011
|
+
rich_help_panel="Permissions",
|
|
1012
|
+
),
|
|
1013
|
+
# --- System & Context ---
|
|
1014
|
+
system_prompt: str | None = typer.Option(
|
|
1015
|
+
None,
|
|
1016
|
+
"--system-prompt",
|
|
1017
|
+
"-s",
|
|
1018
|
+
help="Override the default system prompt",
|
|
1019
|
+
rich_help_panel="System & Context",
|
|
1020
|
+
),
|
|
1021
|
+
append_system_prompt: str | None = typer.Option(
|
|
1022
|
+
None,
|
|
1023
|
+
"--append-system-prompt",
|
|
1024
|
+
help="Append text to the default system prompt",
|
|
1025
|
+
rich_help_panel="System & Context",
|
|
1026
|
+
),
|
|
1027
|
+
settings_file: str | None = typer.Option(
|
|
1028
|
+
None,
|
|
1029
|
+
"--settings",
|
|
1030
|
+
help="Path to a JSON settings file or inline JSON string",
|
|
1031
|
+
rich_help_panel="System & Context",
|
|
1032
|
+
),
|
|
1033
|
+
base_url: str | None = typer.Option(
|
|
1034
|
+
None,
|
|
1035
|
+
"--base-url",
|
|
1036
|
+
help="Anthropic-compatible API base URL",
|
|
1037
|
+
rich_help_panel="System & Context",
|
|
1038
|
+
),
|
|
1039
|
+
api_key: str | None = typer.Option(
|
|
1040
|
+
None,
|
|
1041
|
+
"--api-key",
|
|
1042
|
+
"-k",
|
|
1043
|
+
help="API key (overrides config and environment)",
|
|
1044
|
+
rich_help_panel="System & Context",
|
|
1045
|
+
),
|
|
1046
|
+
bare: bool = typer.Option(
|
|
1047
|
+
False,
|
|
1048
|
+
"--bare",
|
|
1049
|
+
help="Minimal mode: skip hooks, plugins, MCP, and auto-discovery",
|
|
1050
|
+
rich_help_panel="System & Context",
|
|
1051
|
+
),
|
|
1052
|
+
api_format: str | None = typer.Option(
|
|
1053
|
+
None,
|
|
1054
|
+
"--api-format",
|
|
1055
|
+
help="API format: 'anthropic' (default) or 'openai' (DashScope, GitHub Models, etc.)",
|
|
1056
|
+
rich_help_panel="System & Context",
|
|
1057
|
+
),
|
|
1058
|
+
# --- Advanced ---
|
|
1059
|
+
debug: bool = typer.Option(
|
|
1060
|
+
False,
|
|
1061
|
+
"--debug",
|
|
1062
|
+
"-d",
|
|
1063
|
+
help="Enable debug logging",
|
|
1064
|
+
rich_help_panel="Advanced",
|
|
1065
|
+
),
|
|
1066
|
+
mcp_config: Optional[list[str]] = typer.Option(
|
|
1067
|
+
None,
|
|
1068
|
+
"--mcp-config",
|
|
1069
|
+
help="Load MCP servers from JSON files or strings",
|
|
1070
|
+
rich_help_panel="Advanced",
|
|
1071
|
+
),
|
|
1072
|
+
cwd: str = typer.Option(
|
|
1073
|
+
str(Path.cwd()),
|
|
1074
|
+
"--cwd",
|
|
1075
|
+
help="Working directory for the session",
|
|
1076
|
+
hidden=True,
|
|
1077
|
+
),
|
|
1078
|
+
backend_only: bool = typer.Option(
|
|
1079
|
+
False,
|
|
1080
|
+
"--backend-only",
|
|
1081
|
+
help="Run the structured backend host for the React terminal UI",
|
|
1082
|
+
hidden=True,
|
|
1083
|
+
),
|
|
1084
|
+
) -> None:
|
|
1085
|
+
"""主入口函数:启动交互式会话或运行单个提示词
|
|
1086
|
+
|
|
1087
|
+
支持多种运行模式:
|
|
1088
|
+
- 交互式会话模式(默认)
|
|
1089
|
+
- 非交互式打印模式(使用 -p/--print)
|
|
1090
|
+
- 继续会话(使用 --continue 或 --resume)
|
|
1091
|
+
|
|
1092
|
+
Args:
|
|
1093
|
+
ctx: Typer 上下文对象
|
|
1094
|
+
version: 显示版本号选项
|
|
1095
|
+
continue_session: 继续最近会话选项
|
|
1096
|
+
resume: 通过会话 ID 恢复会话选项
|
|
1097
|
+
name: 会话显示名称
|
|
1098
|
+
model: 模型别名或完整模型 ID
|
|
1099
|
+
effort: 会话努力级别
|
|
1100
|
+
verbose: 覆盖详细输出模式设置
|
|
1101
|
+
max_turns: 最大代理轮次数
|
|
1102
|
+
print_mode: 打印模式提示词
|
|
1103
|
+
output_format: 输出格式
|
|
1104
|
+
permission_mode: 权限模式
|
|
1105
|
+
dangerously_skip_permissions: 跳过权限检查
|
|
1106
|
+
allowed_tools: 允许的工具列表
|
|
1107
|
+
disallowed_tools: 禁止的工具列表
|
|
1108
|
+
system_prompt: 覆盖默认系统提示词
|
|
1109
|
+
append_system_prompt: 追加到默认系统提示词
|
|
1110
|
+
settings_file: 设置文件路径
|
|
1111
|
+
base_url: Anthropic 兼容 API 基础 URL
|
|
1112
|
+
api_key: API 密钥
|
|
1113
|
+
bare: 最小模式
|
|
1114
|
+
api_format: API 格式
|
|
1115
|
+
debug: 启用调试日志
|
|
1116
|
+
mcp_config: 从 JSON 文件或字符串加载 MCP 服务器
|
|
1117
|
+
cwd: 会话工作目录
|
|
1118
|
+
backend_only: 运行结构化后端主机
|
|
1119
|
+
"""
|
|
1120
|
+
if ctx.invoked_subcommand is not None: # 如果调用了子命令,直接返回
|
|
1121
|
+
return
|
|
1122
|
+
|
|
1123
|
+
import asyncio # 异步编程模块
|
|
1124
|
+
|
|
1125
|
+
if dangerously_skip_permissions: # 如果跳过权限检查
|
|
1126
|
+
permission_mode = "full_auto" # 设置为完全自动模式
|
|
1127
|
+
|
|
1128
|
+
from illusion.ui.app import run_print_mode, run_repl # 导入 UI 模块
|
|
1129
|
+
|
|
1130
|
+
# 处理 --continue 和 --resume 标志
|
|
1131
|
+
if continue_session or resume is not None:
|
|
1132
|
+
from illusion.services.session_storage import ( # 导入会话存储模块
|
|
1133
|
+
list_session_snapshots, # 列出会话快照
|
|
1134
|
+
load_session_by_id, # 按 ID 加载会话
|
|
1135
|
+
load_session_snapshot, # 加载会话快照
|
|
1136
|
+
)
|
|
1137
|
+
|
|
1138
|
+
session_data = None # 会话数据
|
|
1139
|
+
if continue_session:
|
|
1140
|
+
session_data = load_session_snapshot(cwd)
|
|
1141
|
+
if session_data is None:
|
|
1142
|
+
print(_t("session_not_found_prev"), file=sys.stderr)
|
|
1143
|
+
raise typer.Exit(1)
|
|
1144
|
+
print(_t("session_continuing", summary=session_data.get('summary', '(?)')[:60]))
|
|
1145
|
+
elif resume == "" or resume is None:
|
|
1146
|
+
sessions = list_session_snapshots(cwd, limit=10)
|
|
1147
|
+
if not sessions:
|
|
1148
|
+
print(_t("session_no_saved"), file=sys.stderr)
|
|
1149
|
+
raise typer.Exit(1)
|
|
1150
|
+
print(_t("session_saved_list"))
|
|
1151
|
+
for i, s in enumerate(sessions, 1):
|
|
1152
|
+
print(f" {i}. [{s['session_id']}] {s.get('summary', '?')[:50]} ({_t('session_msg_count', n=s['message_count'])})")
|
|
1153
|
+
choice = typer.prompt(_t("session_enter_id"))
|
|
1154
|
+
try:
|
|
1155
|
+
idx = int(choice) - 1
|
|
1156
|
+
if 0 <= idx < len(sessions):
|
|
1157
|
+
session_data = load_session_by_id(cwd, sessions[idx]["session_id"])
|
|
1158
|
+
else:
|
|
1159
|
+
print(_t("invalid_selection"), file=sys.stderr)
|
|
1160
|
+
raise typer.Exit(1)
|
|
1161
|
+
except ValueError:
|
|
1162
|
+
session_data = load_session_by_id(cwd, choice)
|
|
1163
|
+
if session_data is None:
|
|
1164
|
+
print(_t("session_not_found", id=choice), file=sys.stderr)
|
|
1165
|
+
raise typer.Exit(1)
|
|
1166
|
+
else:
|
|
1167
|
+
session_data = load_session_by_id(cwd, resume)
|
|
1168
|
+
if session_data is None:
|
|
1169
|
+
print(_t("session_not_found", id=resume), file=sys.stderr)
|
|
1170
|
+
raise typer.Exit(1)
|
|
1171
|
+
|
|
1172
|
+
# 将会话传递给 REPL
|
|
1173
|
+
asyncio.run(
|
|
1174
|
+
run_repl(
|
|
1175
|
+
prompt=None, # 无提示词,使用恢复的会话
|
|
1176
|
+
cwd=cwd, # 工作目录
|
|
1177
|
+
model=session_data.get("model") or model, # 模型
|
|
1178
|
+
backend_only=backend_only, # 仅后端模式
|
|
1179
|
+
base_url=base_url, # 基础 URL
|
|
1180
|
+
system_prompt=session_data.get("system_prompt") or system_prompt, # 系统提示词
|
|
1181
|
+
api_key=api_key, # API 密钥
|
|
1182
|
+
restore_messages=session_data.get("messages"), # 恢复的消息
|
|
1183
|
+
restore_session_id=session_data.get("session_id"),
|
|
1184
|
+
effort=effort, # 推理强度级别
|
|
1185
|
+
)
|
|
1186
|
+
)
|
|
1187
|
+
return
|
|
1188
|
+
|
|
1189
|
+
# 打印模式处理
|
|
1190
|
+
if print_mode is not None:
|
|
1191
|
+
prompt = print_mode.strip()
|
|
1192
|
+
if not prompt:
|
|
1193
|
+
print(_t("print_requires_prompt"), file=sys.stderr)
|
|
1194
|
+
raise typer.Exit(1)
|
|
1195
|
+
# 运行打印模式
|
|
1196
|
+
asyncio.run(
|
|
1197
|
+
run_print_mode(
|
|
1198
|
+
prompt=prompt, # 提示词
|
|
1199
|
+
output_format=output_format or "text", # 输出格式
|
|
1200
|
+
cwd=cwd, # 工作目录
|
|
1201
|
+
model=model, # 模型
|
|
1202
|
+
base_url=base_url, # 基础 URL
|
|
1203
|
+
system_prompt=system_prompt, # 系统提示词
|
|
1204
|
+
append_system_prompt=append_system_prompt, # 追加系统提示词
|
|
1205
|
+
api_key=api_key, # API 密钥
|
|
1206
|
+
api_format=api_format, # API 格式
|
|
1207
|
+
permission_mode=permission_mode, # 权限模式
|
|
1208
|
+
max_turns=max_turns, # 最大轮次
|
|
1209
|
+
effort=effort, # 推理强度级别
|
|
1210
|
+
)
|
|
1211
|
+
)
|
|
1212
|
+
return
|
|
1213
|
+
|
|
1214
|
+
# 启动交互式 REPL 会话
|
|
1215
|
+
asyncio.run(
|
|
1216
|
+
run_repl(
|
|
1217
|
+
prompt=None, # 无初始提示词
|
|
1218
|
+
cwd=cwd, # 工作目录
|
|
1219
|
+
model=model, # 模型
|
|
1220
|
+
max_turns=max_turns, # 最大轮次
|
|
1221
|
+
backend_only=backend_only, # 仅后端模式
|
|
1222
|
+
base_url=base_url, # 基础 URL
|
|
1223
|
+
system_prompt=system_prompt, # 系统提示词
|
|
1224
|
+
api_key=api_key, # API 密钥
|
|
1225
|
+
api_format=api_format, # API 格式
|
|
1226
|
+
effort=effort, # 推理强度级别
|
|
1227
|
+
)
|
|
1228
|
+
)
|