Localize the web console and persist custom model and capability settings. Use OpenAI Responses exclusively and move runtime limits into environment configuration. Bootstrap uv and the virtual environment on Windows, support optional China mirrors, and package source distributions without environments, build output, or credentials. Update regression tests and repository quality checks for the new behavior.
109 lines
5.9 KiB
TypeScript
109 lines
5.9 KiB
TypeScript
import { Component, OnInit, inject, signal, output } from '@angular/core';
|
|
import { FormsModule } from '@angular/forms';
|
|
import { HttpClient } from '@angular/common/http';
|
|
import { finalize } from 'rxjs';
|
|
|
|
interface ModelSettings {
|
|
base_url: string;
|
|
model: string;
|
|
api_key_configured: boolean;
|
|
timeout_seconds: number;
|
|
max_retries: number;
|
|
env_path: string;
|
|
message?: string;
|
|
}
|
|
|
|
@Component({
|
|
selector: 'app-model-settings',
|
|
standalone: true,
|
|
imports: [FormsModule],
|
|
template: `
|
|
<form class="model-settings" (ngSubmit)="save()">
|
|
<p>统一使用 OpenAI Responses。保存后新任务立即生效,重启后自动加载。</p>
|
|
<label for="model-base-url">接口地址</label>
|
|
<input id="model-base-url" name="baseUrl" type="url" [(ngModel)]="baseUrl" required
|
|
placeholder="https://api.openai.com/v1" [disabled]="busy() || loading()" />
|
|
<small>填写接口根地址,通常包含 /v1,无需添加 /responses。</small>
|
|
<label for="model-name">模型名称</label>
|
|
<input id="model-name" name="model" [(ngModel)]="model" required placeholder="输入接口支持的模型名"
|
|
[disabled]="busy() || loading()" />
|
|
<label for="model-key">API Key <span>{{ keyConfigured() ? '(已配置,留空保留)' : '(尚未配置)' }}</span></label>
|
|
<input id="model-key" name="apiKey" type="password" [(ngModel)]="apiKey" autocomplete="new-password"
|
|
placeholder="输入新密钥" [disabled]="busy() || loading()" />
|
|
<div class="number-fields">
|
|
<label>请求超时(秒)<input name="timeout" type="number" min="1" max="3600" required
|
|
[(ngModel)]="timeout" [disabled]="busy() || loading()" /></label>
|
|
<label>接口重试次数<input name="retries" type="number" min="0" max="10" required
|
|
[(ngModel)]="retries" [disabled]="busy() || loading()" /></label>
|
|
</div>
|
|
<div class="actions">
|
|
<button type="submit" [disabled]="busy() || loading() || !valid()">{{ busy() === 'save' ? '正在保存…' : '保存配置' }}</button>
|
|
<button type="button" (click)="test()" [disabled]="busy() || loading() || !valid()">
|
|
{{ busy() === 'test' ? '正在测试…' : '测试连接' }}
|
|
</button>
|
|
</div>
|
|
@if (loading()) { <p role="status">正在加载配置…</p> }
|
|
@if (message()) { <p role="status" class="success">{{ message() }}</p> }
|
|
@if (error()) { <p role="alert" class="error">{{ error() }}</p> }
|
|
@if (envPath()) { <small>保存位置:{{ envPath() }}</small> }
|
|
</form>
|
|
`,
|
|
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 { box-sizing: border-box; width: 100%; padding: 12px; border-radius: 10px; border: 1px solid #64748b66;
|
|
background: #f8fafc; color: inherit; font: inherit; }
|
|
input: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; }
|
|
.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; }
|
|
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<void>();
|
|
baseUrl = ''; model = ''; apiKey = ''; timeout = 120; retries = 0;
|
|
busy = signal<'save' | 'test' | null>(null);
|
|
loading = signal(true); keyConfigured = signal(false); envPath = signal('');
|
|
message = signal(''); error = signal('');
|
|
|
|
ngOnInit(): void {
|
|
this.http.get<ModelSettings>('/api/system/model-settings').pipe(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;
|
|
}
|
|
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 };
|
|
}
|
|
private apply(config: ModelSettings): void {
|
|
this.baseUrl = config.base_url; this.model = config.model; this.timeout = config.timeout_seconds;
|
|
this.retries = config.max_retries; this.keyConfigured.set(config.api_key_configured); this.envPath.set(config.env_path);
|
|
}
|
|
save(): void {
|
|
if (this.busy() || !this.valid()) return;
|
|
this.busy.set('save'); this.message.set(''); this.error.set('');
|
|
this.http.post<ModelSettings>('/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 : '连接测试失败,请检查配置。')
|
|
});
|
|
}
|
|
}
|