feat(config): default to WebSocket and migrate saved HTTP settings

This commit is contained in:
chenmingxuan committed 2026-11-04 17:45:52 +08:00
1 parent 2a3f43fb60
commit 04022f2eaf
18 files changed
+244 -58

No files matched your search

@@ -1,20 +1,18 @@
"""Persist the single Responses endpoint without returning credentials to the browser."""
import os
import tempfile
import threading
from pathlib import Path
from typing import Literal
from urllib.parse import urlsplit
from pydantic import BaseModel, Field, SecretStr, field_validator
from artemis.config.agent import load_agent_config
from artemis.config.environment import (
persist_environment as write_environment,
TRANSPORT_DEFAULT_VERSION_KEY,
)
from artemis.config.paths import get_env_file
from artemis.config.settings import settings
_write_lock = threading.Lock()
class ModelSettingsUpdate(BaseModel):
base_url: str = "https://api-n-cd.playones.com/v1"
@@ -118,6 +116,8 @@ def save_model_settings(update: ModelSettingsUpdate) -> dict:
value = getattr(update, field_name)
if value is not None:
values[env_name] = str(value)
if update.transport is not None:
values[TRANSPORT_DEFAULT_VERSION_KEY] = "1"
if update.api_key and update.api_key.get_secret_value().strip():
values["OPENAI_API_KEY"] = update.api_key.get_secret_value().strip()
persist_environment(values)
@@ -142,31 +142,7 @@ def save_model_settings(update: ModelSettingsUpdate) -> dict:
def persist_environment(values: dict[str, str]) -> None:
with _write_lock:
env_path = get_env_file()
env_path.parent.mkdir(parents=True, exist_ok=True)
original = env_path.read_text(encoding="utf-8") if env_path.exists() else ""
# Preserve comments and unrelated variables; quote values to avoid dotenv interpolation.
kept = [
line
for line in original.splitlines()
if line.removeprefix("export ").split("=", 1)[0].strip() not in values
]
for key, value in values.items():
escaped = value.replace("\\", "\\\\").replace("'", "\\'")
kept.append(f"{key}='{escaped}'")
content = "\n".join(kept) + "\n"
fd, temp_name = tempfile.mkstemp(prefix=".env-", dir=env_path.parent)
try:
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as stream:
stream.write(content)
stream.flush()
os.fsync(stream.fileno())
os.replace(temp_name, env_path)
finally:
Path(temp_name).unlink(missing_ok=True)
for key, value in values.items():
os.environ[key] = value
write_environment(get_env_file(), values)
class CapabilitySettings(BaseModel):
@@ -30,29 +30,29 @@ describe('持久化模型与功能设置', () => {
http.expectNone('/api/system/model-settings');
}));
it('加载、测试和保存 WebSocket 选择,旧配置默认 HTTP', fakeAsync(() => {
it('缺少协议的配置默认 WebSocket,允许手动切回 HTTP', fakeAsync(() => {
const fixture = TestBed.createComponent(ModelSettingsComponent);
fixture.detectChanges();
http.expectOne('/api/system/model-settings').flush(config);
tick();
fixture.detectChanges();
const component = fixture.componentInstance;
expect(component.transport).toBe('http');
expect(component.transport).toBe('websocket');
const selector: HTMLSelectElement = fixture.nativeElement.querySelector('#model-transport');
selector.value = 'websocket';
selector.value = 'http';
selector.dispatchEvent(new Event('change'));
tick();
fixture.detectChanges();
component.test();
const check = http.expectOne('/api/system/model-settings/test');
expect(check.request.body.transport).toBe('websocket');
check.flush({message: 'Responses WEBSOCKET 连接测试成功'});
expect(check.request.body.transport).toBe('http');
check.flush({message: 'Responses HTTP 连接测试成功'});
component.save();
const save = http.expectOne('/api/system/model-settings');
expect(save.request.body.transport).toBe('websocket');
expect(save.request.body.transport).toBe('http');
expect(save.request.body.api_key).toBeUndefined();
save.flush({...config, transport: 'websocket'});
expect(component.transport).toBe('websocket');
save.flush({...config, transport: 'http'});
expect(component.transport).toBe('http');
}));
it('加载配置后保留密钥,保存新模型且不发送空密钥', () => {
@@ -109,7 +109,7 @@ interface ModelSettings {
`]
})
export class ModelSettingsComponent implements OnInit {
transport: 'http' | 'websocket' = 'http';
transport: 'http' | 'websocket' = 'websocket';
readonly reasoningValues = REASONING_VALUES;
readonly reasoningLabels = REASONING_LABELS;
private http = inject(HttpClient);
@@ -144,7 +144,7 @@ export class ModelSettingsComponent implements OnInit {
}
private apply(config: ModelSettings): void {
this.baseUrl = config.base_url; this.model = config.model; this.timeout = config.timeout_seconds;
this.transport = config.transport ?? 'http';
this.transport = config.transport ?? 'websocket';
this.modelPresets = config.model_presets;
this.retries = config.max_retries; this.retryInterval = config.retry_interval_seconds ?? 10; this.reasoning = config.reasoning_effort ?? 'high'; this.keyConfigured.set(config.api_key_configured); this.envPath.set(config.env_path);
this.rateLimitRetry = config.rate_limit_retry_seconds ?? 60;