import { Component, OnInit, inject, signal, output } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; import { finalize, timeout } from 'rxjs'; import { UI_READ_TIMEOUT_MS } from '../../core/http-timeouts'; import { SettingSliderComponent, REASONING_VALUES, REASONING_LABELS } from '../setting-slider/setting-slider.component'; import { ModelSelectorComponent } from '../model-selector/model-selector.component'; import { MODEL_PRESETS } from '../../core/model-presets'; interface ModelSettings { base_url: string; model: string; api_key_configured: boolean; timeout_seconds: number; max_retries: number; retry_interval_seconds: number; rate_limit_retry_seconds: number; tool_fallback_model: string; tool_fallback_reasoning_effort: string; tool_fallback_max_retries: number; tool_fallback_no_progress_attempts: number; reasoning_effort: string; model_presets: string[]; env_path: string; message?: string; } @Component({ selector: 'app-model-settings', standalone: true, imports: [FormsModule, SettingSliderComponent, ModelSelectorComponent], template: `

统一使用 OpenAI Responses。保存后新任务立即生效,重启后自动加载。

填写接口根地址,通常包含 /v1,无需添加 /responses。 选择预设模型,或选择“自定义模型”填写名称。
稳定性与兜底模型
首选模型连续 3 次未形成有效工具调用后,本步骤使用兜底模型;下一步恢复首选模型。
@if (loading()) {

正在加载配置…

} @if (message()) {

{{ message() }}

} @if (error()) { } @if (envPath()) { 保存位置:{{ envPath() }} } `, styles: [` .model-settings { display: grid; gap: 10px; color: #1e293b; } p, small { margin: 0; line-height: 1.6; color: #64748b; } label { font-weight: 600; } label span { font-weight: 400; font-size: 12px; } input, select { box-sizing: border-box; width: 100%; padding: 12px; border-radius: 10px; border: 1px solid #64748b66; background: #f8fafc; color: inherit; font: inherit; } input:focus, select:focus { outline: 2px solid #38bdf8; } .number-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-top: 8px; } .number-fields input { margin-top: 8px; } .recovery-settings { border: 1px solid #cbd5e1; border-radius: 10px; background: #f8fafc; padding: 10px 14px; } .recovery-settings summary { cursor: pointer; font-weight: 600; color: #334155; } .recovery-content { display: grid; gap: 10px; padding-top: 12px; } .recovery-content label input, .recovery-content label select { margin-top: 8px; } .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; } button:focus-visible { outline: 2px solid #0284c7; outline-offset: 2px; } small { overflow-wrap: anywhere; } @media(max-width: 480px) { .number-fields { grid-template-columns: 1fr; } } .model-settings {gap:12px;font-size:13px} label {margin-top:6px}input,select {background:#fff;border-color:#dbe3ec;padding:11px 12px} input:focus,select:focus {outline:2px solid #a5b4fc;outline-offset:2px} .actions {border-top:1px solid #e2e8f0;padding-top:16px;align-items:center}.actions button {min-height:40px;background:#4f46e5;border-color:#4f46e5;font-weight:600;border-radius:12px}.actions button[type=button] {background:#fff;border-color:#dbe3ec;color:#475569} .recovery-settings {border-color:#e2e8f0;border-radius:14px;padding:14px 16px}.recovery-settings summary {font-size:13px} `] }) export class ModelSettingsComponent implements OnInit { readonly reasoningValues = REASONING_VALUES; readonly reasoningLabels = REASONING_LABELS; private http = inject(HttpClient); saved = output(); modelPresets: string[] = MODEL_PRESETS; baseUrl = ''; model = ''; apiKey = ''; timeout = 300; retries = 5; retryInterval = 10; reasoning = 'high'; rateLimitRetry = 60; toolFallbackModel = 'gpt-6-luna'; toolFallbackReasoning = 'high'; toolFallbackRetries = 3; toolFallbackNoProgress = 3; busy = signal<'save' | 'test' | null>(null); loading = signal(true); keyConfigured = signal(false); envPath = signal(''); message = signal(''); error = signal(''); ngOnInit(): void { this.http.get('/api/system/model-settings').pipe(timeout(UI_READ_TIMEOUT_MS), finalize(() => this.loading.set(false))).subscribe({ next: config => this.apply(config), error: () => this.error.set('配置加载失败,请刷新页面重试。') }); } valid(): boolean { return !!this.baseUrl.trim() && !!this.model.trim() && (!!this.apiKey.trim() || this.keyConfigured()) && this.timeout > 0 && this.timeout <= 3600 && Number.isInteger(this.retries) && this.retries >= 0 && this.retries <= 10 && this.retryInterval >= 0 && this.retryInterval <= 300 && this.rateLimitRetry >= 0 && this.rateLimitRetry <= 3600 && !!this.toolFallbackModel.trim() && Number.isInteger(this.toolFallbackRetries) && this.toolFallbackRetries >= 0 && this.toolFallbackRetries <= 10 && Number.isInteger(this.toolFallbackNoProgress) && this.toolFallbackNoProgress >= 1 && this.toolFallbackNoProgress <= 10; } private payload() { return { base_url: this.baseUrl.trim(), model: this.model.trim(), api_key: this.apiKey.trim() || undefined, timeout_seconds: this.timeout, max_retries: this.retries, retry_interval_seconds: this.retryInterval, reasoning_effort: this.reasoning, rate_limit_retry_seconds: this.rateLimitRetry, tool_fallback_model: this.toolFallbackModel.trim(), tool_fallback_reasoning_effort: this.toolFallbackReasoning, tool_fallback_max_retries: this.toolFallbackRetries, tool_fallback_no_progress_attempts: this.toolFallbackNoProgress }; } private apply(config: ModelSettings): void { this.baseUrl = config.base_url; this.model = config.model; this.timeout = config.timeout_seconds; 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; this.toolFallbackModel = config.tool_fallback_model ?? 'gpt-6-luna'; this.toolFallbackReasoning = config.tool_fallback_reasoning_effort ?? 'high'; this.toolFallbackRetries = config.tool_fallback_max_retries ?? 3; this.toolFallbackNoProgress = config.tool_fallback_no_progress_attempts ?? 3; } save(): void { if (this.busy() || !this.valid()) return; this.busy.set('save'); this.message.set(''); this.error.set(''); this.http.post('/api/system/model-settings', this.payload()).pipe(finalize(() => this.busy.set(null))).subscribe({ next: config => { this.apply(config); this.apiKey = ''; this.message.set(config.message || '配置已保存。'); this.saved.emit(); }, error: err => this.error.set(typeof err.error?.detail === 'string' ? err.error.detail : '保存失败,请检查配置内容。') }); } test(): void { if (this.busy() || !this.valid()) return; this.busy.set('test'); this.message.set(''); this.error.set(''); this.http.post<{ message: string }>('/api/system/model-settings/test', this.payload()).pipe(finalize(() => this.busy.set(null))).subscribe({ next: result => this.message.set(result.message), error: err => this.error.set(typeof err.error?.detail === 'string' ? err.error.detail : '连接测试失败,请检查配置。') }); } }