import { Component, DestroyRef, computed, effect, inject, signal } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; import { firstValueFrom } from 'rxjs'; import { AgentService } from '../../services/agent.service'; import { SystemService } from '../../services/system.service'; import { renderMarkdownToHtml } from '../../utils/markdown-parser.util'; @Component({ selector: 'app-automation-panel', standalone: true, imports: [CommonModule, FormsModule], templateUrl: './automation-panel.component.html', styleUrl: './automation-panel.component.scss' }) export class AutomationPanelComponent { agent = inject(AgentService); system = inject(SystemService); http = inject(HttpClient); tab = signal('reports'); error = signal(''); notice = signal(''); busy = signal(false); batches = signal([]); preview = signal(null); batch = signal(null); report = signal(null); guidance = ''; taskStatus = computed(() => { const sid = this.agent.currentSessionId(); if (!sid) return ''; return this.agent.sessions().find(s => s.session_id === sid)?.status || (this.agent.runningSessionId() === sid ? this.agent.agentStatus() : '') || ''; }); liveTask = computed(() => ['running', 'paused', 'task_paused', 'resuming'].includes(this.taskStatus())); word = true; excel = true; name = ''; device = ''; profile = 'flash'; stopOnFailure = false; timeout = 1800; private destroyed = false; constructor() { const destroy = inject(DestroyRef); this.http.get('/api/automation/preferences').subscribe(p => { this.word = p.formats.includes('docx'); this.excel = p.formats.includes('xlsx'); this.timeout = p.batch_timeout_seconds || 1800; }); void this.refresh(); const timer = setInterval(() => void this.refresh(), 5000); destroy.onDestroy(() => { this.destroyed = true; clearInterval(timer); }); effect(() => { const sid = this.agent.currentSessionId(); this.report.set(null); this.guidance = ''; if (sid) this.http.get(`/api/automation/reports/${encodeURIComponent(sid)}`).subscribe({ next: r => { if (this.agent.currentSessionId() === sid) this.report.set(r); }, error: () => {} }); }); } formats() { return [ ...(this.word ? ['docx'] : []), ...(this.excel ? ['xlsx'] : []) ]; } label(s: string) { return ({draft:'待开始',running:'执行中',paused:'已暂停',task_paused:'等待指导',resuming:'等待设备恢复',completed:'执行完成',failed:'失败',cancelled:'已取消',pending:'待执行',queued:'排队中',interrupted:'中断',skipped:'跳过',finalizing:'正在生成报告',deleting:'删除中,可重试清理'} as Record)[s] || s; } renderReport(content: string) { return renderMarkdownToHtml(content || ''); } async action(fn: () => Promise) { if (this.busy()) return; this.busy.set(true); this.error.set(''); this.notice.set(''); try { await fn(); } catch (e: any) { const d = e.error?.detail; this.error.set(typeof d === 'string' ? d : d ? JSON.stringify(d) : e.message || '操作失败'); } finally { this.busy.set(false); } } async refresh() { try { const b = await firstValueFrom(this.http.get('/api/automation/batches')); if (this.destroyed) return; this.batches.set(b); const sid = this.agent.currentSessionId(); if (sid && this.tab() === 'reports') { const r = await firstValueFrom(this.http.get(`/api/automation/reports/${encodeURIComponent(sid)}`)); if (this.agent.currentSessionId() === sid) this.report.set(r); } const id = this.batch()?.id; if (id) { const item = await firstValueFrom(this.http.get(`/api/automation/batches/${id}`)); if (this.batch()?.id === id) this.batch.set(item); } } catch { /* User initiated actions expose errors; polling retries on the next interval. */ } } savePreferences() { void this.action(async () => { if (!this.formats().length) throw Error('请至少选择一种报告格式'); await firstValueFrom(this.http.put('/api/automation/preferences',{formats:this.formats()})); this.notice.set('已保存,后续任务和批次使用此默认格式'); }); } upload(event: Event) { const file = (event.target as HTMLInputElement).files?.[0]; if (!file) return; void this.action(async () => { this.preview.set(null); const data = new FormData(); data.append('file',file); this.preview.set(await firstValueFrom(this.http.post('/api/automation/import/preview',data))); this.name = file.name.replace(/\.xlsx$/i,''); }); } startBatch() { void this.action(async () => { if (!this.preview()?.valid) throw Error('请先导入并修正所有错误'); if (!this.device) throw Error('请选择执行设备'); const cases = this.preview().tasks.map(({row, ...c}: any) => c); const created: any = await firstValueFrom(this.http.post('/api/automation/batches',{name:this.name.trim(),cases,device_serial:this.device,profile:this.profile,stop_on_failure:this.stopOnFailure,timeout_seconds:this.timeout,formats:this.formats()})); this.batch.set(created); await firstValueFrom(this.http.post(`/api/automation/batches/${created.id}/resume`,{})); this.preview.set(null); this.notice.set('批次已开始,可暂停或查看每项结果'); await this.refresh(); }); } selectBatch(id: string) { void this.action(async () => { this.batch.set(await firstValueFrom(this.http.get(`/api/automation/batches/${id}`))); }); } deleteBatch(item: { id: string; name: string }) { if (!window.confirm(`删除批次“${item.name}”及其全部任务记录、报告和文件?此操作无法撤销。`)) return; void this.action(async () => { await firstValueFrom(this.http.delete(`/api/automation/batches/${encodeURIComponent(item.id)}`)); if (this.batch()?.id === item.id) this.batch.set(null); await this.refresh(); this.agent.fetchSessions(); this.notice.set('批次及关联任务、报告和文件已删除'); }); } batchAction(action: string) { void this.action(async () => { this.batch.set(await firstValueFrom(this.http.post(`/api/automation/batches/${this.batch().id}/${action}`,{}))); await this.refresh(); }); } pauseTask(id: string) { void this.action(async () => { await firstValueFrom(this.http.post(`/api/tasks/${encodeURIComponent(id)}/pause`,{})); this.notice.set('已请求暂停:当前步骤结束后释放设备'); await this.refresh(); }); } guideTask(id: string) { void this.action(async () => { if (!this.guidance.trim()) throw Error('请先填写指导内容'); const status = this.taskStatus(); await firstValueFrom(this.http.post(`/api/tasks/${encodeURIComponent(id)}/guide`,{instruction:this.guidance.trim()})); this.guidance = ''; this.agent.fetchGuidance(id); if (status === 'paused') this.notice.set('模型请求暂停,请在执行日志中恢复模型调用'); await this.refresh(); }); } continueTask(id: string) { void this.action(async () => { await firstValueFrom(this.http.post(`/api/tasks/${encodeURIComponent(id)}/continue`,{})); this.notice.set('任务已申请恢复;设备忙时等待排队'); await this.refresh(); }); } }