112 lines
11 KiB
HTML
112 lines
11 KiB
HTML
<nav class="tabs" aria-label="自动化工具">
|
||
<button [class.selected]="tab() === 'reports'" (click)="tab.set('reports')">结论报告</button>
|
||
<button [class.selected]="tab() === 'batches'" (click)="tab.set('batches')">批量任务</button>
|
||
</nav>
|
||
@if (error()) { <p class="error" role="alert">{{error()}}</p> }
|
||
@if (notice()) { <p class="notice" role="status">{{notice()}}</p> }
|
||
@if (tab() === 'reports') {
|
||
@if(batch(); as b){<button type="button" class="back-to-batch" (click)="tab.set('batches')">返回批次:{{b.name}}</button>}
|
||
<section><h3>默认报告格式</h3><p>任务结束后自动生成。批次报告包同时包含汇总和每个任务的报告。</p>
|
||
<div class="format-options"><label><input type="checkbox" [(ngModel)]="word"> Word 文档</label>
|
||
<label><input type="checkbox" [(ngModel)]="excel"> Excel 表格</label></div>
|
||
<button [disabled]="busy()" (click)="savePreferences()">保存默认设置</button>
|
||
</section>
|
||
<section><h3>当前任务报告</h3>
|
||
@if (report(); as r) {
|
||
<div class="report-overview">
|
||
<strong class="report-title">{{r.name}}</strong>
|
||
<div class="report-badges"><span [class.failed]="r.status === 'failed'">{{label(r.status)}}</span><app-test-result-badge [result]="r.test_result" [executionStatus]="r.status" /></div>
|
||
@if(r.test_result; as result){<app-test-result-card [result]="result" [sessionId]="r.id" [executionStatus]="r.status" (reviewSaved)="reviewSaved($event)" />}
|
||
@if (r.error) { <div class="report-error" role="status"><strong>异常原因</strong><p>{{r.error}}</p></div> }
|
||
</div>
|
||
<div class="report-meta"><div><strong>任务目标与预期</strong><p>{{r.goal || '未记录'}}</p></div>@if(r.expected){<div><strong>预期结果</strong><p>{{r.expected}}</p></div>}</div>
|
||
<div class="actions"><a [href]="'/api/automation/reports/'+r.id+'/docx'">下载 Word</a><a [href]="'/api/automation/reports/'+r.id+'/xlsx'">下载 Excel</a>
|
||
<a [href]="'/api/automation/reports/'+r.id+'/screenshots'">步骤截图 ZIP</a>
|
||
<a [href]="'/api/automation/reports/'+r.id+'/video'">分段录屏 ZIP</a>
|
||
<a [href]="'/api/automation/reports/'+r.id+'/all'">下载全部资料</a></div>
|
||
<p class="muted">报告包含目标、预期、实际结论、步骤与截图证据。执行中的下载为当前快照。</p>
|
||
<div class="report-content"><h3>结论与发现</h3><div class="report-body" [innerHTML]="renderReport(r.content)"></div></div>
|
||
} @else { <p>从任务队列或历史记录中选择任务,即可下载报告。</p> }
|
||
@if (liveTask() && agent.currentSessionId()) {
|
||
<div class="task-guidance">
|
||
<h3>任务指导 · {{label(taskStatus())}}</h3>
|
||
<p>{{ taskStatus() === 'paused' ? '模型请求已暂停;提交指导后请在执行日志中恢复模型调用。' : '运行中提交会立即打断模型思考并重新观察设备;正在执行的设备动作会先安全结束。' }}</p>
|
||
<label for="task-guidance-text">补充信息或调整方向</label>
|
||
<textarea id="task-guidance-text" [(ngModel)]="guidance" maxlength="10000" rows="3" placeholder="描述已观察到的情况和下一步方向"></textarea>
|
||
<div class="actions">
|
||
@if(taskStatus() === 'running'){<button [disabled]="busy()" (click)="pauseTask(agent.currentSessionId()!)">暂停当前任务</button>}
|
||
<button [disabled]="busy() || !guidance.trim()" (click)="guideTask(agent.currentSessionId()!)">提交指导{{taskStatus() === 'task_paused' ? '并恢复' : ''}}</button>
|
||
@if(taskStatus() === 'task_paused'){<button [disabled]="busy()" (click)="continueTask(agent.currentSessionId()!)">直接继续</button>}
|
||
</div>
|
||
</div>
|
||
}
|
||
</section>
|
||
}
|
||
@if (tab() !== 'reports') {
|
||
<section class="device"><label>执行设备<select [(ngModel)]="device"><option value="">请选择设备</option>@for(d of system.connectedDevices(); track d.serial){<option [value]="d.serial">{{d.serial}}</option>}</select></label></section>
|
||
}
|
||
@if (tab() === 'batches') {
|
||
<section><h3>导入任务</h3><p>每行一个用例,仅「Artemis提示词」作为任务正文;「任务配置(JSON)」可设置每项模型、模式、超时和录屏。「前置约束提取」仅供查看与搜索。示例在模板的「填写说明」页。</p>
|
||
<div class="actions"><a href="/api/automation/template">下载 Excel 模板</a><label class="upload">导入 Excel<input type="file" accept=".xlsx" [disabled]="busy()" (change)="upload($event)"></label></div>
|
||
@if(preview(); as p) {
|
||
<div class="selection-toolbar"><span>已选 <strong>{{selectedCount()}}</strong> / {{p.tasks.length}} 个用例 · 错误 {{p.errors.length}} 条</span><button type="button" [disabled]="busy() || !p.tasks.length" (click)="toggleAllRows()">{{selectedCount() === p.tasks.length ? '全不选' : '全选'}}</button></div>
|
||
@for(e of p.errors; track e.row){<p class="error">第 {{e.row}} 行:{{e.message}}</p>}
|
||
<div class="case-preview-list" aria-label="选择导入用例">
|
||
@for(t of p.tasks; track t.row){
|
||
<div class="case-preview" [class.unselected]="!selectedRows().has(t.row)">
|
||
<span class="selection-column"><input type="checkbox" [checked]="selectedRows().has(t.row)" [disabled]="busy()" (change)="toggleRow(t.row)" [attr.aria-label]="'导入用例:' + (t.case_id ? t.case_id + ' ' : '') + t.name"></span>
|
||
<div class="case-preview-content"><strong>{{t.case_id ? t.case_id + ' ' : ''}}{{t.name}}</strong><small>第 {{t.row}} 行</small>
|
||
<p class="case-goal" [title]="t.goal">{{t.goal}}</p>
|
||
@if(caseConfig(t.task_config); as config){<details><summary>自定义参数</summary><pre>{{config}}</pre></details>}@else{<small>使用批次 / 全局参数</small>}
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
<label>批次名称<input [(ngModel)]="name" maxlength="200"></label>
|
||
<div class="fields"><label>执行模式<select [(ngModel)]="profile"><option value="flash">Flash</option><option value="pro">Pro</option></select></label><label>单任务时限(秒)<input type="number" min="30" max="86400" [(ngModel)]="timeout"></label></div>
|
||
<label for="batch-repeat-count">整批循环次数<input id="batch-repeat-count" type="number" min="1" [max]="maxExecutions" step="1" [(ngModel)]="repeatCount"></label>
|
||
<p class="execution-summary">{{selectedCount()}} 个用例 × {{repeatCount || 0}} 轮,共 {{selectedCount() * (repeatCount || 0)}} 次执行</p>
|
||
<p class="muted">每轮按表格顺序执行所选用例,分别保存结果。未勾选用例不会导入。</p>
|
||
@if(!validRepetitions()){<p class="error">循环次数须为正整数,执行总数不超过 {{maxExecutions}}。</p>}
|
||
<label><input type="checkbox" [(ngModel)]="stopOnFailure"> 失败即停止后续任务(默认继续)</label>
|
||
<label><input type="checkbox" [(ngModel)]="word"> Word</label><label><input type="checkbox" [(ngModel)]="excel"> Excel</label>
|
||
<button class="primary" [disabled]="busy() || !p.valid || !selectedCount() || !validRepetitions() || !device || !name.trim() || !formats().length" (click)="startBatch()">确认导入并开始执行</button>
|
||
}
|
||
</section>
|
||
<section><h3>批次记录</h3>
|
||
@for(b of batches(); track b.id){
|
||
<div class="batch-record">
|
||
<button class="record" [class.selected]="batch()?.id === b.id" (click)="selectBatch(b.id)"><strong>{{b.name}}</strong><span>{{label(b.status)}} · {{b.created_at*1000 | date:'MM-dd HH:mm'}} · {{b.repeat_count || 1}} 轮@if(b.total_executions){ · {{b.total_executions}} 次执行}</span></button>
|
||
<button class="delete-batch" type="button" [disabled]="busy() || b.status === 'running'" (click)="deleteBatch(b)" [attr.aria-label]="'删除批次:' + b.name" [title]="b.status === 'running' ? '执行中的批次不能删除' : '删除批次及其任务、报告和文件'"><span class="material-symbols-outlined">delete_outline</span></button>
|
||
</div>
|
||
}
|
||
@if(!batches().length){<p>暂无批次。</p>}
|
||
</section>
|
||
@if(batch(); as b){<section><h3>{{b.name}}</h3><p>{{label(b.status)}} · {{b.reason}}</p>
|
||
<p class="execution-summary">{{b.case_count || b.jobs?.length || 0}} 个用例 · {{b.repeat_count || 1}} 轮 · 共 {{b.total_executions || b.jobs?.length || 0}} 次执行</p>
|
||
<div class="actions">
|
||
<button type="button" (click)="showBatchQueue(b.id)">在任务队列查看此批次</button>
|
||
@if(agent.currentSessionId()){<button type="button" (click)="tab.set('reports')">查看当前任务报告</button>}
|
||
@if(b.status === 'running'){<button [disabled]="busy()" (click)="batchAction('pause')">暂停后续任务</button>}
|
||
@if(b.status === 'draft' || b.status === 'paused'){<button [disabled]="busy()" (click)="batchAction('resume')">{{b.status === 'draft' ? '开始执行' : '继续未开始任务'}}</button>}
|
||
@if(b.status !== 'running' && b.status !== 'deleting'){<button [disabled]="busy()" (click)="batchAction('retry_failed')">复制失败项为新批次</button>}
|
||
<button type="button" [disabled]="archiveBusy()" (click)="downloadBatchArchive(b.id)">{{archiveBusy() && archiveBatchId() === b.id ? '正在准备报告包…' : '下载批次报告 ZIP'}}</button>
|
||
</div><p class="muted">服务重启后等待手动继续。失败项不会自动重试;暂停不打断当前任务。</p>
|
||
@if(archiveBatchId() === b.id && archiveProgress(); as progress){
|
||
<div class="archive-progress" role="status" aria-live="polite" [class.archive-failed]="progress.status === 'failed'">
|
||
<div class="archive-progress-heading">
|
||
@if(progress.status === 'preparing'){<span class="archive-spinner" aria-hidden="true"></span>}
|
||
<strong>{{progress.stage}}</strong>
|
||
</div>
|
||
@if(progress.status === 'preparing'){
|
||
<div class="archive-track" role="progressbar" [attr.aria-valuenow]="progress.total ? progress.completed : null" [attr.aria-valuemin]="progress.total ? 0 : null" [attr.aria-valuemax]="progress.total || null" [attr.aria-label]="'批次报告打包进度:' + progress.stage"><span [style.width.%]="progress.total ? progress.completed / progress.total * 100 : 8"></span></div>
|
||
<p>已处理 {{progress.completed}} / {{progress.total}} 个任务 · 已用 {{duration(progress.elapsed_seconds)}}@if(progress.eta_seconds !== null){ · 预计还需约 {{duration(progress.eta_seconds)}}}</p>
|
||
} @else if(progress.status === 'failed'){<p>{{progress.error}}。可点击下载按钮重试。</p>}
|
||
@else if(progress.status === 'ready'){<p>打包完成,下载已开始。</p>}
|
||
</div>
|
||
}
|
||
@if(b.report_error){<p class="error">报告生成失败:{{b.report_error}}</p>}
|
||
@for(j of b.jobs; track j.session_id){<div class="job" [class.selected]="agent.currentSessionId() === j.session_id"><button (click)="openBatchTask(j.session_id)" [title]="j.case.goal" [attr.aria-pressed]="agent.currentSessionId() === j.session_id">{{j.case.case_id ? j.case.case_id + ' ' : ''}}{{j.case.name}}@if(j.repeat_count > 1){ · 第 {{j.iteration}} / {{j.repeat_count}} 轮}</button><span>{{label(j.status)}}</span><app-test-result-badge [result]="j.test_result" [executionStatus]="j.status" [compact]="true" />@if(j.error){<small>{{j.error}}</small>}</div>}
|
||
</section>}
|
||
}
|