From 6235118141cc53c7621ba94d85acb54d04ed1ad6 Mon Sep 17 00:00:00 2001 From: KiriAky 107 Date: Sat, 5 Sep 2026 02:09:15 +0800 Subject: [PATCH] =?UTF-8?q?fix(multimodal):=20=E5=86=BB=E7=BB=93=E6=8E=A8?= =?UTF-8?q?=E7=90=86=E7=8E=AF=E5=A2=83=E5=B9=B6=E9=9A=94=E7=A6=BB=E8=BF=9F?= =?UTF-8?q?=E5=88=B0=E5=AF=BC=E5=85=A5=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/local_models/runtime.py | 10 ++++++---- backend/tests/test_local_models.py | 4 ++-- backend/tests/test_multimodal_finalization.py | 2 +- backend/tests/test_runtime_components.py | 5 +++-- .../features/settings/RequestJsonEditor.spec.ts | 17 ++++++++++++++++- .../src/features/settings/RequestJsonEditor.vue | 2 +- 6 files changed, 29 insertions(+), 11 deletions(-) diff --git a/backend/app/local_models/runtime.py b/backend/app/local_models/runtime.py index e362df0..ed86af0 100644 --- a/backend/app/local_models/runtime.py +++ b/backend/app/local_models/runtime.py @@ -69,9 +69,10 @@ def configure(request): return request -def interpreter(): +def interpreter(config=None): from app.local_models import components - if not os.getenv("APP_MODEL_PYTHON") and configuration().device == "cuda" and components.ready(): + requested_device = (config or configuration()).device + if not os.getenv("APP_MODEL_PYTHON") and requested_device == "cuda" and components.ready(): return components.ROOT / "Scripts/python.exe" return Path(os.getenv("APP_MODEL_PYTHON", str(BACKEND_DIR / ".venv-models" / ("Scripts/python.exe" if os.name == "nt" else "bin/python")))) @@ -160,7 +161,8 @@ class Runtime: async def _execute(self, key, operation, payload, config, diagnostics): if read_state(key)["status"] != "installed": raise ProviderError("LOCAL_MODEL_NOT_INSTALLED", "请先下载本地模型。") - if not interpreter().is_file(): + executable = interpreter(config) + if not executable.is_file(): raise ProviderError("LOCAL_RUNTIME_NOT_INSTALLED", "请先安装本地模型运行环境。") from app.services.usage_service import UsageAttempt attempt = UsageAttempt("local-models", CATALOG[key].repository, "local", operation, source="local") @@ -170,7 +172,7 @@ class Runtime: env = {**os.environ, "HF_HUB_OFFLINE": "1", "TRANSFORMERS_OFFLINE": "1", "HF_HUB_DISABLE_TELEMETRY": "1", "OMP_NUM_THREADS": str(config.cpu_threads), "PYTHONIOENCODING": "utf-8"} - args = (str(interpreter()), str(Path(__file__).with_name("worker.py"))) + args = (str(executable), str(Path(__file__).with_name("worker.py"))) options = {"env": env, "limit": 16 * 1024 * 1024, **({"creationflags": 0x08000000} if os.name == "nt" else {})} try: diff --git a/backend/tests/test_local_models.py b/backend/tests/test_local_models.py index 08e9ee7..bbd3a96 100644 --- a/backend/tests/test_local_models.py +++ b/backend/tests/test_local_models.py @@ -47,7 +47,7 @@ def test_local_model_missing_is_explicit(): def test_cancel_reaps_active_model_process(monkeypatch): import app.local_models.runtime as module monkeypatch.setattr(module,'read_state',lambda key:{'status':'installed'}) - monkeypatch.setattr(module,'interpreter',lambda:Path(sys.executable)) + monkeypatch.setattr(module,'interpreter',lambda *_:Path(sys.executable)) class Input: def write(self, value): request = json.loads(value) @@ -92,7 +92,7 @@ def test_subprocess_fallback_runs_and_reaps_real_worker(monkeypatch, tmp_path, c import app.local_models.process as process_module monkeypatch.setattr(module, 'read_state', lambda key: {'status': 'installed'}) - monkeypatch.setattr(module, 'interpreter', lambda: Path(sys.executable)) + monkeypatch.setattr(module, 'interpreter', lambda *_: Path(sys.executable)) worker = tmp_path / 'worker.py' worker.write_text( 'import json,sys,time\n' diff --git a/backend/tests/test_multimodal_finalization.py b/backend/tests/test_multimodal_finalization.py index 002870b..3ddc0b9 100644 --- a/backend/tests/test_multimodal_finalization.py +++ b/backend/tests/test_multimodal_finalization.py @@ -21,7 +21,7 @@ def test_cuda_retries_only_device_failures_in_reaped_process(monkeypatch, code, from app.services.usage_service import connection monkeypatch.setattr(module, 'configuration', lambda: module.RuntimeConfig(device='cuda')) monkeypatch.setattr(module, 'read_state', lambda key: {'status': 'installed'}) - monkeypatch.setattr(module, 'interpreter', lambda: Path(sys.executable)) + monkeypatch.setattr(module, 'interpreter', lambda *_: Path(sys.executable)) events = [] class Process: diff --git a/backend/tests/test_runtime_components.py b/backend/tests/test_runtime_components.py index 6f367e6..b0c7901 100644 --- a/backend/tests/test_runtime_components.py +++ b/backend/tests/test_runtime_components.py @@ -79,7 +79,8 @@ def test_interpreter_keeps_cpu_default_and_respects_explicit_override(monkeypatc (components.ROOT / 'ready.json').write_text('{}') monkeypatch.setattr(runtime, 'configuration', lambda: runtime.RuntimeConfig(device='cpu')) assert runtime.interpreter() != python - monkeypatch.setattr(runtime, 'configuration', lambda: runtime.RuntimeConfig(device='cuda')) - assert runtime.interpreter() == python + # A queued attempt keeps its frozen device even after the saved setting changes. + assert runtime.interpreter(runtime.RuntimeConfig(device='cuda')) == python + assert runtime.interpreter(runtime.RuntimeConfig(device='cpu')) != python monkeypatch.setenv('APP_MODEL_PYTHON', 'explicit-python.exe') assert str(runtime.interpreter()) == 'explicit-python.exe' diff --git a/frontend/src/features/settings/RequestJsonEditor.spec.ts b/frontend/src/features/settings/RequestJsonEditor.spec.ts index 49d8559..51362dc 100644 --- a/frontend/src/features/settings/RequestJsonEditor.spec.ts +++ b/frontend/src/features/settings/RequestJsonEditor.spec.ts @@ -1,5 +1,5 @@ // @vitest-environment happy-dom -import { mount } from '@vue/test-utils' +import { flushPromises, mount } from '@vue/test-utils' import { expect, it, vi } from 'vitest' import RequestJsonEditor from './RequestJsonEditor.vue' import { apiClient } from '@/services/apiClient' @@ -38,6 +38,21 @@ it('ignores an imported configuration that finishes after a newer edit', async ( wrapper.unmount() }) +it('ignores an old import failure after a newer edit', async () => { + let fail!: (reason: Error) => void + vi.mocked(apiClient.post).mockReturnValue(new Promise((_resolve, reject) => { fail = reject })) + const wrapper = mount(RequestJsonEditor, {props:{modelValue:[]}}) + const input = wrapper.get('input[type="file"]') + Object.defineProperty(input.element, 'files', {value:[new File(['{}'], 'old.json')], configurable:true}) + await input.trigger('change') + await wrapper.findAll('button').find(button => button.text() === '添加请求规则')!.trigger('click') + fail(new Error('旧导入失败')) + await flushPromises() + expect(wrapper.text()).not.toContain('旧导入失败') + expect(wrapper.findAll('textarea')).toHaveLength(1) + wrapper.unmount() +}) + it('restores defaults even from an invalid draft and reflects replacement configurations', async () => { const wrapper = mount(RequestJsonEditor, {props:{modelValue:[{capability:'chat', body:{enable_thinking:false}}]}}) await wrapper.get('textarea').setValue('{invalid') diff --git a/frontend/src/features/settings/RequestJsonEditor.vue b/frontend/src/features/settings/RequestJsonEditor.vue index 1040506..a63ca79 100644 --- a/frontend/src/features/settings/RequestJsonEditor.vue +++ b/frontend/src/features/settings/RequestJsonEditor.vue @@ -51,7 +51,7 @@ async function importRules(event: Event) { if (current !== generation) return rules.value = validated.request_overrides.map(rule => ({...rule, draft: JSON.stringify(rule.body, null, 2), error: ''})) publish() - } catch(e) { transferError.value = (e as Error).message } + } catch(e) { if (current === generation) transferError.value = (e as Error).message } } async function exportRules() { transferError.value = ''