From 2ec563c07943d4d651a5a2875314a2e4f3ca1126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E9=93=AD=E8=BD=A9?= Date: Wed, 23 Sep 2026 00:24:35 +0800 Subject: [PATCH] fix: support max reasoning and custom model selection for 1.0.0 --- README.md | 2 +- .../services/model_settings_service.py | 6 +-- .../model-settings.component.spec.ts | 41 +++++++++++++++++++ .../model-settings.component.ts | 20 +++++++-- artemis/config/llm.py | 2 +- docs/ai-automation.md | 2 +- docs/releases/1.0.0.md | 26 ++++++++++++ mcp_server/tools/automation.py | 2 +- pyproject.toml | 3 +- skills/artemis-automation/SKILL.md | 2 +- .../unit/admin_console/test_model_settings.py | 20 +++++++-- tests/unit/test_openai_responses.py | 9 ++++ 12 files changed, 119 insertions(+), 16 deletions(-) create mode 100644 docs/releases/1.0.0.md diff --git a/README.md b/README.md index 0a1402d..73c291b 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ cd Artemis-Eapil 3. 输入任务目标,选择 Flash 或 Pro 后执行;在工作区查看步骤、截图和结果。 4. 通过“报告与批量”导出报告或导入 Excel 用例;任务队列支持历史搜索。 -预设模型为 `gpt-5.6-luna`、`mimo-v2.6-flash`,也可填写自定义名称。默认模型是 `mimo-v2.6-flash`,思考强度 `high`,单次请求超时 **300 秒**,失败后最多重试 **5 次**,间隔 **10 秒**;视频分析与录屏默认关闭。模型及思考参数是否可用取决于所配置网关;不支持时会明确报错。 +预设模型为 `gpt-5.6-luna`、`mimo-v2.6-flash`,也可填写自定义名称。默认模型是 `mimo-v2.6-flash`,思考强度默认 `high`(最高可选 `max`),单次请求超时 **300 秒**,失败后最多重试 **5 次**,间隔 **10 秒**;视频分析与录屏默认关闭。模型及思考参数是否可用取决于所配置网关;不支持时会明确报错。 也可在项目 `.env` 中设置 `OPENAI_API_KEY`、`OPENAI_BASE_URL` 和 `OPENAI_MODEL`,其他参数见 [.env.example](.env.example)。直接编辑环境文件后需重启相应服务。不要提交自己的密钥。 diff --git a/apps/admin_console/services/model_settings_service.py b/apps/admin_console/services/model_settings_service.py index 5ca57df..fa761dc 100644 --- a/apps/admin_console/services/model_settings_service.py +++ b/apps/admin_console/services/model_settings_service.py @@ -22,9 +22,9 @@ class ModelSettingsUpdate(BaseModel): timeout_seconds: float = Field(default=300, gt=0, le=3600) max_retries: int = Field(default=5, ge=0, le=10) retry_interval_seconds: float = Field(default=10, ge=0, le=300) - reasoning_effort: Literal["default", "none", "minimal", "low", "medium", "high", "xhigh"] = ( - "high" - ) + reasoning_effort: Literal[ + "default", "none", "minimal", "low", "medium", "high", "xhigh", "max" + ] = "high" @field_validator("base_url") @classmethod diff --git a/apps/showcase_ui/src/app/components/model-settings/model-settings.component.spec.ts b/apps/showcase_ui/src/app/components/model-settings/model-settings.component.spec.ts index 82d6282..c4f567c 100644 --- a/apps/showcase_ui/src/app/components/model-settings/model-settings.component.spec.ts +++ b/apps/showcase_ui/src/app/components/model-settings/model-settings.component.spec.ts @@ -36,6 +36,47 @@ describe('持久化模型与功能设置', () => { expect(component.message()).toContain('保存'); }); + it('预设按钮填入名称,自定义模型与 max 可以保存并重新加载', async () => { + const fixture = TestBed.createComponent(ModelSettingsComponent); + fixture.detectChanges(); + http.expectOne('/api/system/model-settings').flush(config); + fixture.detectChanges(); + await fixture.whenStable(); + const root: HTMLElement = fixture.nativeElement; + const presets = root.querySelectorAll('.model-presets button'); + expect(presets.length).toBe(2); + presets[0].click(); + fixture.detectChanges(); + await fixture.whenStable(); + const input = root.querySelector('#model-name')!; + expect(input.value).toBe('gpt-5.6-luna'); + expect(presets[0].getAttribute('aria-pressed')).toBe('true'); + expect(input.getAttribute('list')).toBeNull(); + expect(input.autocomplete).toBe('off'); + input.value = 'custom/model-v2'; + input.dispatchEvent(new Event('input')); + const reasoning = root.querySelector('#model-reasoning')!; + reasoning.value = 'max'; + reasoning.dispatchEvent(new Event('change')); + fixture.detectChanges(); + await fixture.whenStable(); + expect(presets[0].getAttribute('aria-pressed')).toBe('false'); + root.querySelector('form')!.dispatchEvent(new Event('submit', { cancelable: true })); + const request = http.expectOne('/api/system/model-settings'); + expect(request.request.body.model).toBe('custom/model-v2'); + expect(request.request.body.reasoning_effort).toBe('max'); + const saved = { ...config, model: 'custom/model-v2', reasoning_effort: 'max' }; + request.flush(saved); + fixture.destroy(); + const reopened = TestBed.createComponent(ModelSettingsComponent); + reopened.detectChanges(); + http.expectOne('/api/system/model-settings').flush(saved); + reopened.detectChanges(); + await reopened.whenStable(); + expect(reopened.nativeElement.querySelector('#model-name').value).toBe('custom/model-v2'); + expect(reopened.nativeElement.querySelector('#model-reasoning').value).toBe('max'); + }); + it('连接测试使用当前输入而不写入保存接口', () => { const fixture = TestBed.createComponent(ModelSettingsComponent); fixture.detectChanges(); diff --git a/apps/showcase_ui/src/app/components/model-settings/model-settings.component.ts b/apps/showcase_ui/src/app/components/model-settings/model-settings.component.ts index 36ed7d9..a34b6de 100644 --- a/apps/showcase_ui/src/app/components/model-settings/model-settings.component.ts +++ b/apps/showcase_ui/src/app/components/model-settings/model-settings.component.ts @@ -27,14 +27,22 @@ interface ModelSettings { placeholder="https://api.openai.com/v1" [disabled]="busy() || loading()" /> 填写接口根地址,通常包含 /v1,无需添加 /responses。 - + @for (preset of modelPresets; track preset) { + + } + + - + 点击预设填入,或直接输入自定义模型名称;保存后可继续使用。 接口需支持所选强度;不支持时会明确报错,可手动选择“由模型默认”。 @@ -71,12 +79,18 @@ interface ModelSettings { .actions { display: flex; gap: 12px; margin-top: 8px; } button { padding: 11px 20px; border-radius: 10px; border: 1px solid #38bdf877; background: #0284c7; color: white; cursor: pointer; font: inherit; } button:disabled { opacity: .5; cursor: default; } .success { color: #047857; } .error { color: #be123c; } + .model-presets { display: flex; flex-wrap: wrap; gap: 8px; } + .model-presets button { padding: 8px 12px; background: #f8fafc; color: #475569; border-color: #cbd5e1; font-size: 14px; } + .model-presets button:hover:not(:disabled) { background: #e0f2fe; border-color: #38bdf8; } + .model-presets button[aria-pressed="true"] { background: #e0f2fe; border-color: #0284c7; color: #075985; } + button:focus-visible { outline: 2px solid #0284c7; outline-offset: 2px; } small { overflow-wrap: anywhere; } @media(max-width: 480px) { .number-fields { grid-template-columns: 1fr; } } `] }) export class ModelSettingsComponent implements OnInit { private http = inject(HttpClient); saved = output(); + readonly modelPresets = ['gpt-5.6-luna', 'mimo-v2.6-flash']; baseUrl = ''; model = ''; apiKey = ''; timeout = 300; retries = 5; retryInterval = 10; reasoning = 'high'; busy = signal<'save' | 'test' | null>(null); loading = signal(true); keyConfigured = signal(false); envPath = signal(''); diff --git a/artemis/config/llm.py b/artemis/config/llm.py index 7858f85..47c7293 100644 --- a/artemis/config/llm.py +++ b/artemis/config/llm.py @@ -67,7 +67,7 @@ class LLM(BaseModel): max_tokens: int | None = Field(default=None, gt=0) temperature: float | None = None reasoning_effort: ( - Literal["default", "none", "minimal", "low", "medium", "high", "xhigh"] | None + Literal["default", "none", "minimal", "low", "medium", "high", "xhigh", "max"] | None ) = Field(default_factory=lambda: settings.OPENAI_REASONING_EFFORT) enable_grounding: bool | None = None diff --git a/docs/ai-automation.md b/docs/ai-automation.md index fffcd41..0b17c6c 100644 --- a/docs/ai-automation.md +++ b/docs/ai-automation.md @@ -42,6 +42,6 @@ MCP 自动刷新只处理已存在且指向当前项目的标准 Python 模块 报告导出到 `artifacts/mcp_exports/`,包含本地路径及服务下载链接。运行中导出是快照;执行完成不等于验证通过,报告会保留未验证、失败、跳过与中断状态。 -模型默认使用 `mimo-v2.6-flash`,也预设 `gpt-5.6-luna` 并支持自定义。思考强度默认 `high`,选“由模型默认”不发送该参数;接口不支持时明确报错。单次请求 300 秒,首次失败后最多再试 5 次,每次等待 10 秒,SDK 不叠加重试。耗尽后沿用任务暂停/恢复机制,鉴权或非法参数立即失败。视频分析与录屏默认关闭。 +模型默认使用 `mimo-v2.6-flash`,也预设 `gpt-5.6-luna` 并支持自定义。思考强度最高支持 `max`,默认 `high`,选“由模型默认”不发送该参数;接口不支持时明确报错。单次请求 300 秒,首次失败后最多再试 5 次,每次等待 10 秒,SDK 不叠加重试。耗尽后沿用任务暂停/恢复机制,鉴权或非法参数立即失败。视频分析与录屏默认关闭。 保存全局配置要求用户明确指令;只读配置不包含密钥,MCP 不接收密钥。密钥仍在网页或 `.env` 中填写。设置对新任务生效,模型请求重试不会触发测试用例自动重跑。历史步骤证据保留,设备动作回放已移除。 diff --git a/docs/releases/1.0.0.md b/docs/releases/1.0.0.md new file mode 100644 index 0000000..051b99b --- /dev/null +++ b/docs/releases/1.0.0.md @@ -0,0 +1,26 @@ +# ARTEMIS-Eapil 1.0.0 + +## 功能 + +- 简体中文网页,支持自然语言 Android 任务、设备选择与 Flash/Pro 执行模式。 +- 统一使用 OpenAI Responses;预设 gpt-5.6-luna、mimo-v2.6-flash,也可输入并保存自定义模型。 +- 思考强度最高可选 max,默认 high;单次请求超时 300 秒,失败后最多重试 5 次,间隔 10 秒。 +- Excel 用例批量导入、执行与队列管理,历史按日期分组并支持搜索。 +- Word、Excel 及批次 ZIP 报告,区分执行状态和实际验证结论。 +- 提供完整流程 MCP 工具、可分发 Skill 和仓库 AGENTS.md 开发指南。 +- Windows 一键安装支持国内镜像、已有环境复用、Git 快进更新、依赖检查及已有 MCP 配置刷新。 + +## 本次修复 + +- 模型名称不再使用浏览器原生建议下拉,改用页面内预设按钮和独立文本输入,关闭输入框自动填充。 +- 前端、配置保存及模型请求均支持 max 思考强度;自定义模型名称可保存并重新加载。 +- 移除设备动作回放,保留历史步骤和截图;视频分析与录屏默认关闭。 +- 精简中文 README,移除 GitHub Actions 工作流。 + +## 安装与升级 + +Windows 解压源码后运行 start.bat,国内网络运行 start-cn.bat。首次运行需要联网安装依赖并构建网页。配置模型接口和密钥后即可使用。 + +Git 安装用户在无本地冲突的情况下通过启动脚本检查更新;ZIP 安装保留本地目录继续运行,需要自动源码更新时改用 Git 克隆。升级后在 AI 客户端重新加载 MCP,Skill 按客户端方式安装或更新。 + +源码包不包含虚拟环境、依赖目录、编译产物、密钥和任务数据。模型与 max 参数是否可用取决于所配置的 Responses 网关;不支持时会明确报错,不自动降级。 diff --git a/mcp_server/tools/automation.py b/mcp_server/tools/automation.py index ad8ba17..469604d 100644 --- a/mcp_server/tools/automation.py +++ b/mcp_server/tools/automation.py @@ -114,7 +114,7 @@ async def mobile_config( Only set persist_defaults=true when the user requests saved/global configuration. model changes: model (preset or custom), base_url, reasoning_effort - (default/none/minimal/low/medium/high/xhigh), timeout_seconds, max_retries, + (default/none/minimal/low/medium/high/xhigh/max), timeout_seconds, max_retries, retry_interval_seconds. 'default' omits reasoning; unsupported values fail visibly. capabilities: video_enabled (recording + analysis), outputter_enabled, step_summarizer_enabled. reports: formats=['docx','xlsx'] (at least one). diff --git a/pyproject.toml b/pyproject.toml index 4547f64..2499e89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,7 @@ [project] name = "artemis" -version = "1.0" +version = "1.0.0" description = "Autonomous AI Assistant for Mobile Automation" authors = [ @@ -254,4 +254,3 @@ dev = [ "pyright>=1.1.389", "pre-commit>=3.5.0", ] - diff --git a/skills/artemis-automation/SKILL.md b/skills/artemis-automation/SKILL.md index fea30d3..441b016 100644 --- a/skills/artemis-automation/SKILL.md +++ b/skills/artemis-automation/SKILL.md @@ -27,7 +27,7 @@ description: 使用 ARTEMIS MCP 在 Android 真机或模拟器上完成自然语 ## 配置和恢复边界 - `mobile_config` 读取非敏感设置。只有用户明确要求保存或修改默认配置时,才传 `changes` 与 `persist_defaults=true`;这些设置影响后续任务,不能为一次临时需求擅自改变以后所有任务。 -- 模型名称支持预设 `gpt-5.6-luna`、`mimo-v2.6-flash` 和自定义名称,取决于用户配置的 Responses 网关。默认思考强度 `high`、单次请求 300 秒、失败后最多重试 5 次、固定间隔 10 秒;默认关闭视频分析与录屏。不根据模型名称猜测网关能力。 +- 模型名称支持预设 `gpt-5.6-luna`、`mimo-v2.6-flash` 和自定义名称,取决于用户配置的 Responses 网关。思考强度最高支持 `max`,默认 `high`、单次请求 300 秒、失败后最多重试 5 次、固定间隔 10 秒;默认关闭视频分析与录屏。不根据模型名称猜测网关能力。 - 强度选 `default` 表示不发送该参数,和 `none`(显式要求关闭)不同。接口拒绝强度参数时明确呈现失败,先询问用户是否改用 `default`,不能静默降级或换模型。 - API Key 只通过网页或 `.env` 配置。不要请求用户把 key 发到聊天中,不读取/回显已有 key。保存后新任务生效;直接编辑环境文件后需重启相应服务,运行中的任务保留自己的配置。 - 当前视频开关仅提供保存的全局设置,没有单任务覆盖或独立视频文件分析接口。用户只要求临时分析且默认关闭时,明确说明这个限制;不能用“先开全局、执行后再关”影响同期任务。只有用户改为要求保存启用后,才修改配置并提交新任务。 diff --git a/tests/unit/admin_console/test_model_settings.py b/tests/unit/admin_console/test_model_settings.py index 9d323c6..4940dd9 100644 --- a/tests/unit/admin_console/test_model_settings.py +++ b/tests/unit/admin_console/test_model_settings.py @@ -125,16 +125,30 @@ def test_reasoning_retry_settings_roundtrip(isolated_settings): assert parse_llm_config().operator.reasoning_effort == "default" -def test_responses_payload_has_reasoning_and_no_sdk_retry(monkeypatch): +def test_max_reasoning_and_custom_model_survive_reload(isolated_settings): + result = service.save_model_settings( + service.ModelSettingsUpdate(model="custom/model-v2", reasoning_effort="max") + ) + assert result["model"] == "custom/model-v2" + assert result["reasoning_effort"] == "max" + assert dotenv_values(isolated_settings)["OPENAI_REASONING_EFFORT"] == "max" + reloaded = Settings(_env_file=isolated_settings) + assert reloaded.OPENAI_REASONING_EFFORT == "max" + assert reloaded.OPENAI_MODEL == "custom/model-v2" + assert parse_llm_config().operator.reasoning_effort == "max" + + +@pytest.mark.parametrize("effort", ["high", "max"]) +def test_responses_payload_has_reasoning_and_no_sdk_retry(monkeypatch, effort): from artemis.llm.router import ModelEndpoint, ModelFactory from unittest.mock import Mock factory = Mock() monkeypatch.setattr("langchain_openai.ChatOpenAI", factory) ModelFactory.create_model( - ModelEndpoint(api_key="test-only", reasoning_effort="high", max_retries=5) + ModelEndpoint(api_key="test-only", reasoning_effort=effort, max_retries=5) ) - assert factory.call_args.kwargs["reasoning_effort"] == "high" + assert factory.call_args.kwargs["reasoning_effort"] == effort assert factory.call_args.kwargs["max_retries"] == 0 assert factory.call_args.kwargs["use_responses_api"] is True ModelFactory.create_model(ModelEndpoint(api_key="test-only", reasoning_effort="default")) diff --git a/tests/unit/test_openai_responses.py b/tests/unit/test_openai_responses.py index b4ab466..8e351e9 100644 --- a/tests/unit/test_openai_responses.py +++ b/tests/unit/test_openai_responses.py @@ -85,6 +85,15 @@ async def test_text_and_image_use_responses_transport(transport_model): assert result.usage_metadata["total_tokens"] == 13 +@pytest.mark.asyncio +async def test_max_reasoning_is_sent_unchanged_to_responses(transport_model): + model, requests, _ = transport_model + model.reasoning_effort = "max" + await model.ainvoke([HumanMessage(content="Check")]) + assert requests[0].url.path == "/v1/responses" + assert json.loads(requests[0].content)["reasoning"]["effort"] == "max" + + @pytest.mark.asyncio async def test_function_call_roundtrip_uses_call_id(transport_model): model, requests, outputs = transport_model