95 lines
7.5 KiB
TypeScript
95 lines
7.5 KiB
TypeScript
import { Component, inject, OnInit, signal, output } from '@angular/core';
|
|
import { FormsModule } from '@angular/forms';
|
|
import { HttpClient } from '@angular/common/http';
|
|
import { finalize } from 'rxjs';
|
|
|
|
interface Capabilities {
|
|
video_enabled: boolean;
|
|
recording_enabled: boolean;
|
|
outputter_enabled: boolean;
|
|
step_summarizer_enabled: boolean;
|
|
screenshot_limit_enabled: boolean;
|
|
screenshot_max_long_edge: number;
|
|
task_timeout_seconds: number;
|
|
observation_timeout_seconds: number;
|
|
observation_max_attempts: number;
|
|
device_command_timeout_seconds: number;
|
|
action_failure_attempts: number;
|
|
worker_cleanup_timeout_seconds: number;
|
|
}
|
|
|
|
@Component({
|
|
selector: 'app-capability-settings', standalone: true, imports: [FormsModule],
|
|
template: `
|
|
<section aria-label="功能开关">
|
|
<h3>功能开关</h3>
|
|
<p>按需启用,保存后对新任务生效。</p>
|
|
<label><input type="checkbox" role="switch" [(ngModel)]="config.video_enabled" [disabled]="busy() || loading() || !loaded()" />
|
|
<span>视频分析<small>允许智能体分析任务画面。运行期间会临时录制,完成后不保留录像(除非开启录屏)。</small></span></label>
|
|
<label><input type="checkbox" role="switch" [(ngModel)]="config.recording_enabled" [disabled]="busy() || loading() || !loaded()" />
|
|
<span>保存录屏<small>保留任务录像以便复盘,可单独开启,不要求启用视频分析。</small></span></label>
|
|
<label><input type="checkbox" role="switch" [(ngModel)]="config.outputter_enabled" [disabled]="busy() || loading() || !loaded()" />
|
|
<span>最终报告生成<small>根据执行记录生成任务结果报告。</small></span></label>
|
|
<label><input type="checkbox" role="switch" [(ngModel)]="config.step_summarizer_enabled" [disabled]="busy() || loading() || !loaded()" />
|
|
<span>步骤摘要压缩<small>Flash 模式下在后台生成摘要,减少长任务的上下文占用。</small></span></label>
|
|
<label><input type="checkbox" role="switch" [(ngModel)]="config.screenshot_limit_enabled" [disabled]="busy() || loading() || !loaded()" />
|
|
<span>限制截图分辨率<small>开启时长边最多 {{ config.screenshot_max_long_edge }} 像素;关闭后使用设备原始分辨率。JSON 配置可覆盖开关和上限。</small></span></label>
|
|
<details class="execution-settings">
|
|
<summary>执行保护</summary>
|
|
<p>限制故障等待与反复失败;保存后对新任务生效,任务 JSON 可单独覆盖。</p>
|
|
<div class="number-fields">
|
|
<label>任务总超时(秒)<input type="number" min="30" max="86400" step="1" [(ngModel)]="config.task_timeout_seconds" [disabled]="busy() || !loaded()" /><small>不包含排队和用户暂停时间。</small></label>
|
|
<label>屏幕观测超时(秒)<input type="number" min="1" max="300" [(ngModel)]="config.observation_timeout_seconds" [disabled]="busy() || !loaded()" /></label>
|
|
<label>观测恢复最多尝试次数<input type="number" min="1" max="5" step="1" [(ngModel)]="config.observation_max_attempts" [disabled]="busy() || !loaded()" /></label>
|
|
<label>ADB 命令超时(秒)<input type="number" min="1" max="60" [(ngModel)]="config.device_command_timeout_seconds" [disabled]="busy() || !loaded()" /></label>
|
|
<label>连续动作失败次数<input type="number" min="1" max="10" step="1" [(ngModel)]="config.action_failure_attempts" [disabled]="busy() || !loaded()" /><small>先重新观察,再让兜底模型处理当前步骤。</small></label>
|
|
<label>任务收尾超时(秒)<input type="number" min="1" max="300" [(ngModel)]="config.worker_cleanup_timeout_seconds" [disabled]="busy() || !loaded()" /><small>已有最终结果但进程未退出时,回收进程以释放队列。</small></label>
|
|
</div>
|
|
</details>
|
|
<button type="button" (click)="save()" [disabled]="busy() || loading() || !loaded() || !valid()">{{ busy() ? '正在保存…' : '保存功能设置' }}</button>
|
|
@if (message()) { <p role="status">{{ message() }}</p> }
|
|
@if (error()) { <p role="alert" class="error">{{ error() }}</p> }
|
|
</section>
|
|
`,
|
|
styles: [`section { margin-top: 24px; padding-top: 20px; border-top: 1px solid #64748b55; }
|
|
h3 { margin: 0 0 8px; } p, small { color: #64748b; line-height: 1.6; }
|
|
label { display: flex; gap: 12px; align-items: flex-start; padding: 14px 0; cursor: pointer; }
|
|
input { width: 20px; height: 20px; accent-color: #0284c7; flex-shrink: 0; }
|
|
small { display: block; margin-top: 4px; } button { padding: 10px 18px; border-radius: 10px; border: 1px solid #38bdf877; background: #0284c7; color: white; font: inherit; cursor: pointer; }
|
|
button:disabled { opacity: .5; cursor: default; } .error { color: #be123c; }
|
|
.execution-settings { margin: 12px 0 20px; padding: 12px 14px; border: 1px solid #cbd5e1; border-radius: 10px; background: #f8fafc; }
|
|
summary { cursor: pointer; font-weight: 600; color: #334155; }
|
|
.number-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 16px; }
|
|
.number-fields label { display: block; padding: 0; color: #334155; }
|
|
.number-fields input { box-sizing: border-box; display: block; width: 100%; height: auto; margin-top: 8px; padding: 9px; border: 1px solid #cbd5e1; border-radius: 8px; font: inherit; }
|
|
@media(max-width: 480px) { .number-fields { grid-template-columns: 1fr; } }`]
|
|
})
|
|
export class CapabilitySettingsComponent implements OnInit {
|
|
changed = output<Capabilities>();
|
|
private http = inject(HttpClient);
|
|
config: Capabilities = { video_enabled: false, recording_enabled: false, outputter_enabled: true, step_summarizer_enabled: true, screenshot_limit_enabled: true, screenshot_max_long_edge: 2048, task_timeout_seconds: 1800, observation_timeout_seconds: 60, observation_max_attempts: 3, device_command_timeout_seconds: 10, action_failure_attempts: 3, worker_cleanup_timeout_seconds: 30 };
|
|
busy = signal(false); loading = signal(true); loaded = signal(false); message = signal(''); error = signal('');
|
|
ngOnInit(): void {
|
|
this.http.get<Capabilities>('/api/system/capability-settings').pipe(finalize(() => this.loading.set(false))).subscribe({
|
|
next: config => { this.config = config; this.loaded.set(true); this.changed.emit(config); }, error: () => this.error.set('功能配置加载失败,请刷新页面重试。')
|
|
});
|
|
}
|
|
save(): void {
|
|
if (this.busy() || this.loading() || !this.loaded() || !this.valid()) return;
|
|
this.busy.set(true); this.message.set(''); this.error.set('');
|
|
this.http.post<Capabilities>('/api/system/capability-settings', this.config).pipe(finalize(() => this.busy.set(false))).subscribe({
|
|
next: config => { this.config = config; this.changed.emit(config); this.message.set('功能设置已保存,新任务将使用此配置。'); },
|
|
error: () => this.error.set('保存失败,请检查服务器连接和 .env 文件权限。')
|
|
});
|
|
}
|
|
valid(): boolean {
|
|
const c = this.config;
|
|
return Number.isInteger(c.task_timeout_seconds) && c.task_timeout_seconds >= 30 && c.task_timeout_seconds <= 86400
|
|
&& c.observation_timeout_seconds > 0 && c.observation_timeout_seconds <= 300
|
|
&& Number.isInteger(c.observation_max_attempts) && c.observation_max_attempts >= 1 && c.observation_max_attempts <= 5
|
|
&& c.device_command_timeout_seconds > 0 && c.device_command_timeout_seconds <= 60
|
|
&& Number.isInteger(c.action_failure_attempts) && c.action_failure_attempts >= 1 && c.action_failure_attempts <= 10
|
|
&& c.worker_cleanup_timeout_seconds > 0 && c.worker_cleanup_timeout_seconds <= 300;
|
|
}
|
|
}
|