feat: add guided exploration and share user-started MCP backend

This commit is contained in:
chenmingxuan committed 2026-10-16 18:32:46 +08:00
1 parent f86db1ced6
commit ee256ad7ce
144 files changed
+8625 -2075

No files matched your search

@@ -84,6 +84,12 @@ interface Capabilities {
.number-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 16px; }
.number-fields label { display: block; padding: 0; color: #334155; }
.number-fields input, .number-fields select { box-sizing: border-box; display: block; width: 100%; height: auto; margin-top: 8px; padding: 9px; border: 1px solid #cbd5e1; border-radius: 8px; font: inherit; }
section {font-size:13px;border-color:#e2e8f0}h3 {font-size:15px;color:#334155}
section > label {align-items:center;flex-direction:row-reverse;justify-content:space-between;border-bottom:1px solid #f1f5f9;gap:20px}section > label > span {flex:1}small {font-size:12px}
input[role=switch] {appearance:none;position:relative;width:36px;height:22px;border:1px solid #cbd5e1;border-radius:20px;background:#e2e8f0;cursor:pointer;transition:background .15s}
input[role=switch]::after {content:'';position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:50%;background:#fff;box-shadow:0 1px 3px #0f172a25;transition:transform .15s}
input[role=switch]:checked {background:#6366f1;border-color:#6366f1}input[role=switch]:checked::after {transform:translateX(14px)}input[role=switch]:focus-visible {outline:2px solid #a5b4fc;outline-offset:3px}input:disabled {opacity:.5;cursor:default}
button {background:#4f46e5;border-color:#4f46e5;border-radius:12px;font-weight:600}.execution-settings {border-color:#e2e8f0;border-radius:14px;padding:14px 16px}.number-fields input,.number-fields select {background:#fff;border-color:#dbe3ec}
@media(max-width: 480px) { .number-fields { grid-template-columns: 1fr; } }`]
})
export class CapabilitySettingsComponent implements OnInit {
@@ -71,9 +71,9 @@ describe('持久化模型与功能设置', () => {
expect(input.autocomplete).toBe('off');
input.value = 'custom/model-v2';
input.dispatchEvent(new Event('input'));
const reasoning = root.querySelector<HTMLSelectElement>('#model-reasoning')!;
reasoning.value = 'max';
reasoning.dispatchEvent(new Event('change'));
const reasoning = root.querySelector<HTMLInputElement>('input[aria-label="思考强度"]')!;
reasoning.value = '7';
reasoning.dispatchEvent(new Event('input'));
fixture.detectChanges();
await fixture.whenStable();
expect(presets[0].getAttribute('aria-pressed')).toBe('false');
@@ -90,7 +90,7 @@ describe('持久化模型与功能设置', () => {
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');
expect(reopened.nativeElement.querySelector('input[aria-label="思考强度"]').getAttribute('aria-valuetext')).toBe('最高 · max');
});
it('高级稳定性设置可编辑并保存', async () => {
@@ -2,6 +2,7 @@ import { Component, OnInit, inject, signal, output } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { finalize } from 'rxjs';
import { SettingSliderComponent, REASONING_VALUES, REASONING_LABELS } from '../setting-slider/setting-slider.component';
interface ModelSettings {
base_url: string;
@@ -24,7 +25,7 @@ interface ModelSettings {
@Component({
selector: 'app-model-settings',
standalone: true,
imports: [FormsModule],
imports: [FormsModule, SettingSliderComponent],
template: `
<form class="model-settings" (ngSubmit)="save()">
<p>统一使用 OpenAI Responses。保存后新任务立即生效,重启后自动加载。</p>
@@ -43,14 +44,7 @@ interface ModelSettings {
[(ngModel)]="model" required placeholder="输入接口支持的模型名" aria-describedby="model-name-help"
[disabled]="busy() || loading()" />
<small id="model-name-help">点击预设填入,或直接输入自定义模型名称;保存后可继续使用。</small>
<label for="model-reasoning">思考强度</label>
<select id="model-reasoning" name="reasoning" [(ngModel)]="reasoning" [disabled]="busy() || loading()">
<option value="default">由模型默认</option><option value="none">关闭(none)</option>
<option value="minimal">最低(minimal)</option><option value="low">低(low)</option>
<option value="medium">中(medium)</option><option value="high">高(high)</option>
<option value="xhigh">极高(xhigh)</option><option value="max">最高(max)</option>
</select>
<small>接口需支持所选强度;不支持时会明确报错,可手动选择“由模型默认”。</small>
<app-setting-slider label="思考强度" [(value)]="reasoning" [values]="reasoningValues" [labels]="reasoningLabels" [disabled]="!!busy() || loading()" hint="更高的强度适合复杂推理;需接口支持,也可选择模型默认。" />
<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()" />
@@ -69,13 +63,7 @@ interface ModelSettings {
<label>兜底模型名称<input name="toolFallbackModel" type="text" required autocomplete="off"
autocapitalize="off" spellcheck="false" [(ngModel)]="toolFallbackModel"
[disabled]="busy() || loading()" /></label>
<label>兜底思考强度<select name="toolFallbackReasoning" [(ngModel)]="toolFallbackReasoning"
[disabled]="busy() || loading()">
<option value="default">由模型默认</option><option value="none">关闭(none)</option>
<option value="minimal">最低(minimal)</option><option value="low">低(low)</option>
<option value="medium">中(medium)</option><option value="high">高(high)</option>
<option value="xhigh">极高(xhigh)</option><option value="max">最高(max)</option>
</select></label>
<app-setting-slider label="兜底思考强度" [(value)]="toolFallbackReasoning" [values]="reasoningValues" [labels]="reasoningLabels" [disabled]="!!busy() || loading()" />
<div class="number-fields">
<label>兜底请求重试次数<input name="toolFallbackRetries" type="number" min="0" max="10" required
[(ngModel)]="toolFallbackRetries" [disabled]="busy() || loading()" /></label>
@@ -119,9 +107,16 @@ interface ModelSettings {
.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; } }
.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}
.model-presets button {border-radius:20px;font-size:12px}.model-presets button[aria-pressed=true] {background:#eef2ff;border-color:#a5b4fc;color:#4338ca}
.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<void>();
modelPresets: string[] = [];