diff --git a/artemis/interfaces/cli/commands/ui.py b/artemis/interfaces/cli/commands/ui.py index 9caa025..e14ec76 100644 --- a/artemis/interfaces/cli/commands/ui.py +++ b/artemis/interfaces/cli/commands/ui.py @@ -15,6 +15,7 @@ """UI Launch and Web Console CLI Command (artemis ui).""" import logging +import os from pathlib import Path import re import shutil @@ -34,6 +35,11 @@ import typer logger = logging.getLogger(__name__) +def _launch_text(english: str, chinese: str) -> str: + """Honor the language selected by the Windows launch wrapper.""" + return chinese if os.environ.get("ARTEMIS_LAUNCH_LANG") == "zh-CN" else english + + def running_via_console_script_shim() -> bool: """Return True when this CLI was launched through a console-script ``.exe`` shim. @@ -308,7 +314,12 @@ def ui_command( ) if restart: - console.print(f" [yellow]🔄 Restart requested. Recycling port {port}...[/yellow]") + console.print( + _launch_text( + f" [yellow]🔄 Restart requested. Recycling port {port}...[/yellow]", + f" [yellow]🔄 已请求重启,正在释放端口 {port}...[/yellow]", + ) + ) load_session_reconciler() stop_server(port=port, timeout=12.0) elif is_port_in_use(port): @@ -316,19 +327,39 @@ def ui_command( pid_str = f" (PID: {', '.join(map(str, pids))})" if pids else "" if sys.stdin.isatty(): console.print( - f"\n [yellow]⚠ Port {port} is already in use by an active Artemis server{pid_str}.[/yellow]" + _launch_text( + f"\n [yellow]⚠ Port {port} is already in use by an active Artemis server{pid_str}.[/yellow]", + f"\n [yellow]⚠ 检测到 ARTEMIS 服务正在运行,占用端口 {port}{pid_str}。[/yellow]", + ) ) - console.print(" [dim]Choose an action:[/dim]") - console.print(" [bold]1[/bold] Open browser (default)") - console.print(" [bold]2[/bold] Restart server (stop existing and start here)") - console.print(" [bold]3[/bold] Cancel") + console.print( + _launch_text(" [dim]Choose an action:[/dim]", " [dim]请选择操作:[/dim]") + ) + console.print( + _launch_text( + " [bold]1[/bold] Open browser (default)", + " [bold]1[/bold] 打开浏览器(默认)", + ) + ) + console.print( + _launch_text( + " [bold]2[/bold] Restart server (stop existing and start here)", + " [bold]2[/bold] 重启服务(停止现有服务并在此启动,会中断正在运行的任务)", + ) + ) + console.print(_launch_text(" [bold]3[/bold] Cancel", " [bold]3[/bold] 取消")) try: - choice = input(" Select [1-3, default 1]: ").strip() + choice = input( + _launch_text(" Select [1-3, default 1]: ", " 请输入选项 [1-3,默认 1]:") + ).strip() except (KeyboardInterrupt, EOFError): choice = "1" if choice == "2": console.print( - f"\n [yellow]🔄 Terminating previous Artemis instance on port {port}...[/yellow]" + _launch_text( + f"\n [yellow]🔄 Terminating previous Artemis instance on port {port}...[/yellow]", + f"\n [yellow]🔄 正在停止端口 {port} 上的现有 ARTEMIS 服务...[/yellow]", + ) ) load_session_reconciler() stop_server(port=port, timeout=12.0) @@ -340,8 +371,12 @@ def ui_command( return else: console.print( - f"\n [yellow]⚠ Port {port} is already in use by Artemis{pid_str}. " - f"Use '[bold]artemis restart[/bold]' to restart.[/yellow]\n" + _launch_text( + f"\n [yellow]⚠ Port {port} is already in use by Artemis{pid_str}. " + "Use '[bold]artemis restart[/bold]' to restart.[/yellow]\n", + f"\n [yellow]⚠ 检测到 ARTEMIS 服务正在运行,占用端口 {port}{pid_str}。" + "如需重启,请运行 '[bold]artemis restart[/bold]'。[/yellow]\n", + ) ) if open_browser: webbrowser.open(local_url) diff --git a/docs/releases/2.4.4.md b/docs/releases/2.4.4.md index 28b82de..a7534c7 100644 --- a/docs/releases/2.4.4.md +++ b/docs/releases/2.4.4.md @@ -8,6 +8,7 @@ - 展开后安全渲染 Markdown 正文、列表和表格,并展示已有结论及已记录依据;长正文限高滚动。 - 列表摘要优先使用已保存的结论原因,否则使用执行错误首行;展示时移除 FlashRunner 固定技术前缀,原始记录保持不变。 - 仅展示已结束任务的已有信息,不新增 AI 复盘或模型请求。展开说明不触发任务切换,列表后台刷新保留展开状态。 +- `start-cn.bat` 检测到正在运行的服务时,服务占用提示、打开浏览器/重启/取消菜单、输入提示及重启进度均使用中文;非交互启动提示也使用中文。默认仍只打开浏览器,选择重启才停止现有服务。 本版不改变任务执行、模型重试或批次停止策略,继续保留 2.4.3 的异常输出隔离与参数保护。 @@ -16,10 +17,13 @@ - 前端回归:353 项通过,生产构建通过。 - 内置浏览器使用隔离样例验证默认折叠、Markdown 展开、长内容、无错误完成任务、批次布局、浅色/深色与窄视口。 - 最终源码 ZIP 的隔离解压、独立空 npm 缓存、官方源与国内镜像安装和生产构建结果随附件 `INSTALL-VERIFICATION.md` 提供。 -- 未复跑用户业务任务、调用真实模型或进行高并发测试;本次没有 Python 行为变更,未重跑 Python 全量回归。 +- 启动语言与 Windows 启动脚本定向回归 31 项通过,Ruff 格式与静态检查、质量基线及核心类型检查通过。Python 默认确定性回归结果随安装验证附件提供。 +- 未复跑用户业务任务、调用真实模型或进行高并发测试。 ## 升级 已有安装请等待任务结束,运行 `update-cn.bat` 或 `update.bat` 后重启。新安装下载 `artemis-2.4.4-source.zip`,按 `SHA256SUMS.txt` 校验后启动。请勿直接覆盖旧安装目录。 +2026-10-29 撤回首次发布的 2.4.4,补充中文启动提示后重新发布。此前下载过 2.4.4 的用户请重新下载并核对最新校验文件,或使用更新脚本获取最新提交。首次发布包的 SHA256 为 `9a2a678c0cf97b5de140479a3f3946171d20d19efaf3ad249c708ed35a06e67a`,该包已撤回。 + 人工评审插件 1.1.0 无需更新。本次发布不构建 Docker,不重启用户服务。 diff --git a/tests/unit/test_ui_command.py b/tests/unit/test_ui_command.py index a00b334..e467a65 100644 --- a/tests/unit/test_ui_command.py +++ b/tests/unit/test_ui_command.py @@ -109,6 +109,82 @@ def _recording_console(): return Console(file=io.StringIO(), force_terminal=False, width=120) +@pytest.mark.parametrize("language", ["zh-CN", "en"]) +@pytest.mark.parametrize("choice", ["", "2", "3"]) +def test_existing_server_prompt_respects_launch_language(monkeypatch, language, choice): + from artemis.runtime import server_lifecycle + + console = _recording_console() + monkeypatch.setenv("ARTEMIS_LAUNCH_LANG", language) + monkeypatch.setattr(ui, "Console", lambda: console) + monkeypatch.setattr(ui.sys.stdin, "isatty", lambda: True) + monkeypatch.setattr(server_lifecycle, "is_port_in_use", lambda port: True) + monkeypatch.setattr(server_lifecycle, "find_server_pids", lambda port: [1234]) + prompts, opened, stopped = [], [], [] + monkeypatch.setattr("builtins.input", lambda prompt: prompts.append(prompt) or choice) + monkeypatch.setattr(ui.webbrowser, "open", opened.append) + monkeypatch.setattr(ui, "load_session_reconciler", lambda: None) + + class RestartSelected(Exception): + pass + + def stop_server(**kwargs): + stopped.append(kwargs) + raise RestartSelected # Never launch or stop a real service. + + monkeypatch.setattr(server_lifecycle, "stop_server", stop_server) + if choice == "2": + with pytest.raises(RestartSelected): + ui.ui_command(port=8123) + else: + ui.ui_command(port=8123) + + output = console.file.getvalue() + assert "8123" in output and "PID: 1234" in output + if language == "zh-CN": + assert all( + text in output + for text in ("服务正在运行", "请选择操作", "打开浏览器(默认)", "重启服务", "取消") + ) + assert "请输入选项" in prompts[0] + assert "Choose an action" not in output and "Restart server" not in output + if choice == "2": + assert "正在停止端口 8123" in output + else: + assert "Choose an action" in output and "Open browser (default)" in output + assert "Select [1-3, default 1]" in prompts[0] + assert opened == (["http://localhost:8123"] if not choice else []) + assert stopped == ([{"port": 8123, "timeout": 12.0}] if choice == "2" else []) + + +@pytest.mark.parametrize("language", ["zh-CN", None]) +@pytest.mark.parametrize("open_browser", [True, False]) +def test_existing_server_noninteractive_does_not_restart(monkeypatch, language, open_browser): + from artemis.runtime import server_lifecycle + + console = _recording_console() + if language: + monkeypatch.setenv("ARTEMIS_LAUNCH_LANG", language) + else: + monkeypatch.delenv("ARTEMIS_LAUNCH_LANG", raising=False) + monkeypatch.setattr(ui, "Console", lambda: console) + monkeypatch.setattr(ui.sys.stdin, "isatty", lambda: False) + monkeypatch.setattr(server_lifecycle, "is_port_in_use", lambda port: True) + monkeypatch.setattr(server_lifecycle, "find_server_pids", lambda port: []) + monkeypatch.setattr( + server_lifecycle, "stop_server", lambda **kwargs: pytest.fail("Unexpected restart") + ) + opened = [] + monkeypatch.setattr(ui.webbrowser, "open", opened.append) + + ui.ui_command(port=8123, open_browser=open_browser) + + output = console.file.getvalue() + assert "artemis restart" in output + assert ("服务正在运行" if language else "already in use") in output + assert opened == (["http://localhost:8123"] if open_browser else []) + + def test_ensure_showcase_built_skips_install_when_deps_current(monkeypatch): steps = [] monkeypatch.setattr(ui, "_showcase_build_required", lambda showcase_dir: True)