fix: release 2.4.4 with collapsible task outcome details
This commit is contained in:
1 parent
0c3ca69c1d
commit
c6d2050ff0
18 files changed
+204
-20
No files matched your search
@@ -155,6 +155,7 @@ Flash 的最终 AI 判定与分析独立配置,默认关闭;首页功能配
|
|||||||
- 通常以 4、6、8、12、16、24px 组织内部间距;沿用组件现有网格和断点。子容器允许收缩,长内容使用 `min-width: 0` 及合适换行。
|
- 通常以 4、6、8、12、16、24px 组织内部间距;沿用组件现有网格和断点。子容器允许收缩,长内容使用 `min-width: 0` 及合适换行。
|
||||||
- 探索面板在 600px 以下收紧外边距并允许操作换行;模型选择控件宽度跟随容器。窄屏仍可辨认状态、选择来源和发送指导。
|
- 探索面板在 600px 以下收紧外边距并允许操作换行;模型选择控件宽度跟随容器。窄屏仍可辨认状态、选择来源和发送指导。
|
||||||
- 列表后台刷新保留滚动、选择和折叠状态;首次加载与静默更新分开,避免周期性加载提示挤动内容。实时连接复用现有页面可见性管理。
|
- 列表后台刷新保留滚动、选择和折叠状态;首次加载与静默更新分开,避免周期性加载提示挤动内容。实时连接复用现有页面可见性管理。
|
||||||
|
- 结束任务的错误与阻塞说明默认只占一行,点击展开安全 Markdown 正文与已有依据,长正文限高滚动。摘要优先采用已保存的有效结论原因,否则取执行错误首行;不新增 AI 复盘请求,不改写原始记录。展开说明不触发任务切换,刷新保留展开状态。
|
||||||
|
|
||||||
## Elevation
|
## Elevation
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
基于 [ARTEMIS](https://github.com/google/artemis) 扩展的 Android AI 自动化测试平台。用自然语言描述任务,即可通过简体中文网页或 MCP 驱动真机、模拟器执行操作并收集结果。模型统一使用 **OpenAI Responses API**。
|
基于 [ARTEMIS](https://github.com/google/artemis) 扩展的 Android AI 自动化测试平台。用自然语言描述任务,即可通过简体中文网页或 MCP 驱动真机、模拟器执行操作并收集结果。模型统一使用 **OpenAI Responses API**。
|
||||||
|
|
||||||
**2.4.3**:异常模型输出耗尽重试后只失败当前任务,避免触发全局暂停;补充请求关联和有界参数摘要,便于定位流式异常。见 [2.4.3 发行说明](docs/releases/2.4.3.md)。人工评审插件保持 1.1.0;列表优化见 [2.4.2](docs/releases/2.4.2.md),安装修复见 [2.4.1](docs/releases/2.4.1.md)。
|
**2.4.4**:任务结束说明默认显示一行摘要,点击展开 Markdown 正文和已记录依据,避免失败长文本占满列表;不新增 AI 复盘请求。见 [2.4.4 发行说明](docs/releases/2.4.4.md)。人工评审插件保持 1.1.0;批次异常隔离见 [2.4.3](docs/releases/2.4.3.md),列表优化见 [2.4.2](docs/releases/2.4.2.md),安装修复见 [2.4.1](docs/releases/2.4.1.md)。
|
||||||
|
|
||||||
## 主要功能
|
## 主要功能
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "artemis-showcase-ui",
|
"name": "artemis-showcase-ui",
|
||||||
"version": "2.4.3",
|
"version": "2.4.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "artemis-showcase-ui",
|
"name": "artemis-showcase-ui",
|
||||||
"version": "2.4.3",
|
"version": "2.4.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/cdk": "^22.2.4",
|
"@angular/cdk": "^22.2.4",
|
||||||
"@angular/common": "^22.2.3",
|
"@angular/common": "^22.2.3",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "artemis-showcase-ui",
|
"name": "artemis-showcase-ui",
|
||||||
"version": "2.4.3",
|
"version": "2.4.4",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve --proxy-config proxy.conf.json",
|
"start": "ng serve --proxy-config proxy.conf.json",
|
||||||
|
|||||||
@@ -129,9 +129,7 @@ limitations under the License.
|
|||||||
<div class="item-goal" [title]="session.initial_goal || ''">
|
<div class="item-goal" [title]="session.initial_goal || ''">
|
||||||
{{ session.display_name || session.initial_goal || '未命名任务' }}
|
{{ session.display_name || session.initial_goal || '未命名任务' }}
|
||||||
</div>
|
</div>
|
||||||
@if (session.error && getTaskStatus(session) === 'failed') {
|
<app-task-outcome [error]="session.error" [result]="session.test_result" [status]="getTaskStatus(session)" />
|
||||||
<div class="item-error" [title]="session.error">{{session.error}}</div>
|
|
||||||
}
|
|
||||||
<div class="item-footer">
|
<div class="item-footer">
|
||||||
<app-task-id-copy [taskId]="session.session_id" />
|
<app-task-id-copy [taskId]="session.session_id" />
|
||||||
<span class="item-date">{{ session.start_time * 1000 | date:'MM/dd/yy' }} · 已用 {{ formatTaskDuration(session.start_time, session.end_time) }}</span>
|
<span class="item-date">{{ session.start_time * 1000 | date:'MM/dd/yy' }} · 已用 {{ formatTaskDuration(session.start_time, session.end_time) }}</span>
|
||||||
|
|||||||
@@ -3694,7 +3694,6 @@ $stream-row-height: 25px;
|
|||||||
&.failed { border-left: 3.5px solid #dc2626; background: var(--app-error-soft, #fff7f7); }
|
&.failed { border-left: 3.5px solid #dc2626; background: var(--app-error-soft, #fff7f7); }
|
||||||
&.completed { border-left: 3.5px solid #16a34a; }
|
&.completed { border-left: 3.5px solid #16a34a; }
|
||||||
&.cancelled, &.interrupted { border-left: 3.5px solid #ea580c; }
|
&.cancelled, &.interrupted { border-left: 3.5px solid #ea580c; }
|
||||||
.item-error { color: var(--app-error-text, #b91c1c); font-size: 11px; line-height: 1.4; overflow-wrap: anywhere; }
|
|
||||||
|
|
||||||
.item-header {
|
.item-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { TaskOutcomeComponent } from '../task-outcome/task-outcome.component';
|
||||||
import { ExplorationMessage, Demonstration } from '../../services/exploration.service';
|
import { ExplorationMessage, Demonstration } from '../../services/exploration.service';
|
||||||
import { mergeConversation } from '../../utils/exploration-timeline.util';
|
import { mergeConversation } from '../../utils/exploration-timeline.util';
|
||||||
import { TaskIdCopyComponent } from '../task-id-copy/task-id-copy.component';
|
import { TaskIdCopyComponent } from '../task-id-copy/task-id-copy.component';
|
||||||
@@ -251,7 +252,7 @@ export type { MarkdownSegment, MarkdownLine, NoteMilestone, ParsedNote, StreamRe
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-agent-stream',
|
selector: 'app-agent-stream',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [ExpandableValueComponent, PluginToolResultComponent, ReportMarkdownComponent, LlmRequestStatusComponent, TaskLifecycleComponent, TaskIdCopyComponent, UiLabelPipe, CommonModule, FormsModule, OverlayModule, TaskHistoryComponent, AutomationPanelComponent, TestResultBadgeComponent, TestResultCardComponent, TaskBatchFilterComponent],
|
imports: [TaskOutcomeComponent,ExpandableValueComponent, PluginToolResultComponent, ReportMarkdownComponent, LlmRequestStatusComponent, TaskLifecycleComponent, TaskIdCopyComponent, UiLabelPipe, CommonModule, FormsModule, OverlayModule, TaskHistoryComponent, AutomationPanelComponent, TestResultBadgeComponent, TestResultCardComponent, TaskBatchFilterComponent],
|
||||||
templateUrl: './agent-stream.component.html',
|
templateUrl: './agent-stream.component.html',
|
||||||
styleUrl: './agent-stream.component.scss',
|
styleUrl: './agent-stream.component.scss',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
|||||||
+2
-2
@@ -17,7 +17,7 @@
|
|||||||
<div class="report-heading"><app-favorite-button [sourceId]="r.id" /><strong class="report-title">{{r.name}}</strong></div>
|
<div class="report-heading"><app-favorite-button [sourceId]="r.id" /><strong class="report-title">{{r.name}}</strong></div>
|
||||||
<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>
|
<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.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> }
|
@if (r.error) { <app-task-outcome [error]="r.error" [status]="r.status" /> }
|
||||||
</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="report-meta"><div><strong>任务目标与预期</strong><p>{{r.goal || '未记录'}}</p></div>@if(r.expected){<div><strong>预期结果</strong><p>{{r.expected}}</p></div>}</div>
|
||||||
<app-evidence-sources [value]="r.evidence_sources || null" />
|
<app-evidence-sources [value]="r.evidence_sources || null" />
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
<app-test-result-badge [result]="j.test_result" [executionStatus]="j.status" [compact]="true" />
|
<app-test-result-badge [result]="j.test_result" [executionStatus]="j.status" [compact]="true" />
|
||||||
@if(j.human_reviewed){<span class="human-review-badge" title="人工评审已有保存内容,不代表测试通过">人工已审</span>}
|
@if(j.human_reviewed){<span class="human-review-badge" title="人工评审已有保存内容,不代表测试通过">人工已审</span>}
|
||||||
</div>
|
</div>
|
||||||
@if(j.error){<small>{{j.error}}</small>}
|
<app-task-outcome [error]="j.error" [result]="j.test_result" [status]="j.status" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</section>}
|
</section>}
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ details{margin:14px 0;}summary{color:var(--app-text-secondary, #475569);cursor:p
|
|||||||
.report-heading{display:flex;align-items:flex-start;gap:8px;}.report-heading .report-title{flex:1;min-width:0;}
|
.report-heading{display:flex;align-items:flex-start;gap:8px;}.report-heading .report-title{flex:1;min-width:0;}
|
||||||
.report-title{display:block;font-size:14px;line-height:1.5;color:var(--app-text-strong, #0f172a);overflow-wrap:anywhere;}
|
.report-title{display:block;font-size:14px;line-height:1.5;color:var(--app-text-strong, #0f172a);overflow-wrap:anywhere;}
|
||||||
.report-badges{display:flex;gap:7px;flex-wrap:wrap;margin-top:9px;span{padding:3px 8px;border:1px solid var(--app-primary-border, #bfdbfe);border-radius:20px;background:var(--app-primary-soft, #eff6ff);color:var(--app-primary-text, #1e40af);font-size:12px;&.failed{background:var(--app-error-soft, #fff1f2);border-color:var(--app-error-border, #fecdd3);color:var(--app-error-text, #be123c);}}}
|
.report-badges{display:flex;gap:7px;flex-wrap:wrap;margin-top:9px;span{padding:3px 8px;border:1px solid var(--app-primary-border, #bfdbfe);border-radius:20px;background:var(--app-primary-soft, #eff6ff);color:var(--app-primary-text, #1e40af);font-size:12px;&.failed{background:var(--app-error-soft, #fff1f2);border-color:var(--app-error-border, #fecdd3);color:var(--app-error-text, #be123c);}}}
|
||||||
.report-error{margin-top:11px;padding:10px 12px;background:var(--app-error-soft, #fff1f2);border-left:3px solid #e11d48;border-radius:6px;color:var(--app-error-text, #9f1239);strong{font-size:12px;}p{margin:4px 0 0;color:var(--app-error-text, #9f1239);white-space:pre-wrap;}}
|
|
||||||
.report-meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:10px;margin:12px 0;div{min-width:0;padding:10px 12px;border:1px solid var(--app-border, #e2e8f0);border-radius:8px;background:var(--app-surface, #fff);}strong{font-size:12px;color:var(--app-text-secondary, #475569);}p{margin:5px 0 0;white-space:pre-wrap;color:var(--app-text-control, #1e293b);}}
|
.report-meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:10px;margin:12px 0;div{min-width:0;padding:10px 12px;border:1px solid var(--app-border, #e2e8f0);border-radius:8px;background:var(--app-surface, #fff);}strong{font-size:12px;color:var(--app-text-secondary, #475569);}p{margin:5px 0 0;white-space:pre-wrap;color:var(--app-text-control, #1e293b);}}
|
||||||
.report-content{margin-top:14px;padding:14px;border:1px solid var(--app-border, #e2e8f0);border-radius:10px;background:var(--app-surface, #fff);h3{margin-bottom:12px;}}
|
.report-content{margin-top:14px;padding:14px;border:1px solid var(--app-border, #e2e8f0);border-radius:10px;background:var(--app-surface, #fff);h3{margin-bottom:12px;}}
|
||||||
.report-body{overflow-x:auto;overflow-wrap:anywhere;color:var(--app-text-control, #1e293b);line-height:1.75;}
|
.report-body{overflow-x:auto;overflow-wrap:anywhere;color:var(--app-text-control, #1e293b);line-height:1.75;}
|
||||||
|
|||||||
+6
-1
@@ -176,7 +176,12 @@ describe('AutomationPanelComponent batch deletion', () => {
|
|||||||
});
|
});
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
const root = fixture.nativeElement as HTMLElement;
|
const root = fixture.nativeElement as HTMLElement;
|
||||||
expect(root.querySelector('.report-error')?.textContent).toContain('连续 3 次');
|
const explanation = root.querySelector<HTMLDetailsElement>('app-task-outcome details')!;
|
||||||
|
expect(explanation.open).toBeFalse();
|
||||||
|
expect(explanation.querySelector('summary')?.textContent).toContain('连续 3 次');
|
||||||
|
explanation.querySelector('summary')!.click();
|
||||||
|
expect(explanation.open).toBeTrue();
|
||||||
|
expect(explanation.querySelector('.body')?.textContent).toContain('连续 3 次');
|
||||||
expect(root.querySelector('.report-body h2')?.textContent).toBe('结果');
|
expect(root.querySelector('.report-body h2')?.textContent).toBe('结果');
|
||||||
expect(root.querySelector('.report-body')?.textContent).toContain('第一行');
|
expect(root.querySelector('.report-body')?.textContent).toContain('第一行');
|
||||||
expect(root.querySelector('.report-body')?.textContent).toContain('第二行');
|
expect(root.querySelector('.report-body')?.textContent).toContain('第二行');
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { TaskOutcomeComponent } from '../task-outcome/task-outcome.component';
|
||||||
import { Component, DestroyRef, computed, effect, inject, output, signal } from '@angular/core';
|
import { Component, DestroyRef, computed, effect, inject, output, signal } from '@angular/core';
|
||||||
import { FavoriteButtonComponent } from '../favorite-button/favorite-button.component';
|
import { FavoriteButtonComponent } from '../favorite-button/favorite-button.component';
|
||||||
import { ReportMarkdownComponent } from '../report-markdown/report-markdown.component';
|
import { ReportMarkdownComponent } from '../report-markdown/report-markdown.component';
|
||||||
@@ -24,7 +25,7 @@ interface ArchiveProgress {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-automation-panel', standalone: true, imports: [FavoriteButtonComponent, ReportMarkdownComponent, CommonModule, FormsModule, TestResultBadgeComponent, TestResultCardComponent, TaskPluginsComponent, EvidenceSourcesComponent],
|
selector: 'app-automation-panel', standalone: true, imports: [TaskOutcomeComponent,FavoriteButtonComponent, ReportMarkdownComponent, CommonModule, FormsModule, TestResultBadgeComponent, TestResultCardComponent, TaskPluginsComponent, EvidenceSourcesComponent],
|
||||||
templateUrl: './automation-panel.component.html', styleUrl: './automation-panel.component.scss'
|
templateUrl: './automation-panel.component.html', styleUrl: './automation-panel.component.scss'
|
||||||
})
|
})
|
||||||
export class AutomationPanelComponent {
|
export class AutomationPanelComponent {
|
||||||
|
|||||||
@@ -18,6 +18,28 @@ describe('历史任务检索', () => {
|
|||||||
});
|
});
|
||||||
afterEach(() => http.verify());
|
afterEach(() => http.verify());
|
||||||
|
|
||||||
|
it('历史错误独立折叠,展开不选择任务且后台刷新保留展开状态', () => {
|
||||||
|
const fixture = TestBed.createComponent(TaskHistoryComponent);
|
||||||
|
const row = {session_id:'blocked', name:'前置条件缺失', status:'failed', start_time:100,
|
||||||
|
error:'[web-blocked] FlashRunner failed: **目标未绑定**\n\n- 未执行删除'};
|
||||||
|
http.expectOne(r => r.url === '/api/automation/history').flush({total:1,items:[row]});
|
||||||
|
fixture.detectChanges();
|
||||||
|
const host = fixture.nativeElement as HTMLElement;
|
||||||
|
const details = host.querySelector<HTMLDetailsElement>('app-task-outcome details')!;
|
||||||
|
expect(details.closest('button')).toBeNull();
|
||||||
|
expect(details.open).toBeFalse();
|
||||||
|
details.querySelector('summary')!.click();
|
||||||
|
expect(details.open).toBeTrue();
|
||||||
|
expect(TestBed.inject(AgentService).selectSession).not.toHaveBeenCalled();
|
||||||
|
fixture.componentInstance.load(true);
|
||||||
|
http.expectOne(r => r.url === '/api/automation/history').flush({total:1,items:[{...row}]});
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(host.querySelector('app-task-outcome details')).toBe(details);
|
||||||
|
expect(details.open).toBeTrue();
|
||||||
|
expect(details.querySelector('strong')!.textContent).toBe('目标未绑定');
|
||||||
|
fixture.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
it('已终止探索可删除,确认范围后刷新,取消不发请求', () => {
|
it('已终止探索可删除,确认范围后刷新,取消不发请求', () => {
|
||||||
const fixture = TestBed.createComponent(TaskHistoryComponent);
|
const fixture = TestBed.createComponent(TaskHistoryComponent);
|
||||||
const row = {session_id:'stopped', status:'cancelled', exploration_kind:'exploration', exploration_status:'paused', start_time:100};
|
const row = {session_id:'stopped', status:'cancelled', exploration_kind:'exploration', exploration_status:'paused', start_time:100};
|
||||||
@@ -268,7 +290,7 @@ describe('历史任务检索', () => {
|
|||||||
expect(row.getAttribute('title')).toBe('请打开设置首页并核对两个真实分类');
|
expect(row.getAttribute('title')).toBe('请打开设置首页并核对两个真实分类');
|
||||||
expect(row.textContent).toContain('1.1 打开并核对设置');
|
expect(row.textContent).toContain('1.1 打开并核对设置');
|
||||||
expect(row.textContent).toContain('总耗时 1分5秒');
|
expect(row.textContent).toContain('总耗时 1分5秒');
|
||||||
expect(row.textContent).toContain('任务执行超时');
|
expect(fixture.nativeElement.querySelector('app-task-outcome summary').textContent).toContain('任务执行超时');
|
||||||
expect(fixture.nativeElement.querySelector('.status-failed .history-status').textContent).toContain('失败');
|
expect(fixture.nativeElement.querySelector('.status-failed .history-status').textContent).toContain('失败');
|
||||||
fixture.destroy();
|
fixture.destroy();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { TaskOutcomeComponent } from '../task-outcome/task-outcome.component';
|
||||||
import { TaskIdCopyComponent } from '../task-id-copy/task-id-copy.component';
|
import { TaskIdCopyComponent } from '../task-id-copy/task-id-copy.component';
|
||||||
import { FavoriteButtonComponent } from '../favorite-button/favorite-button.component';
|
import { FavoriteButtonComponent } from '../favorite-button/favorite-button.component';
|
||||||
import { TaskModeBadgeComponent } from '../task-mode-badge/task-mode-badge.component';
|
import { TaskModeBadgeComponent } from '../task-mode-badge/task-mode-badge.component';
|
||||||
@@ -9,13 +10,13 @@ import { HttpClient, HttpParams } from '@angular/common/http';
|
|||||||
import { AgentService } from '../../services/agent.service';
|
import { AgentService } from '../../services/agent.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector:'app-task-history', standalone:true, imports:[CommonModule,FormsModule,TaskIdCopyComponent,TestResultBadgeComponent,TaskModeBadgeComponent,FavoriteButtonComponent],
|
selector:'app-task-history', standalone:true, imports:[TaskOutcomeComponent,CommonModule,FormsModule,TaskIdCopyComponent,TestResultBadgeComponent,TaskModeBadgeComponent,FavoriteButtonComponent],
|
||||||
template:`<section aria-label="历史任务"><h3>历史任务 <small>{{total()}} 条</small><small class="query-status" role="status">{{loading() ? '正在查询…' : ''}}</small></h3>
|
template:`<section aria-label="历史任务"><h3>历史任务 <small>{{total()}} 条</small><small class="query-status" role="status">{{loading() ? '正在查询…' : ''}}</small></h3>
|
||||||
<input aria-label="搜索历史任务" placeholder="搜索名称、目标、任务编号或标签" [(ngModel)]="query" (ngModelChange)="search()">
|
<input aria-label="搜索历史任务" placeholder="搜索名称、目标、任务编号或标签" [(ngModel)]="query" (ngModelChange)="search()">
|
||||||
<div class="filters"><label>起始日期<input type="date" [(ngModel)]="start" (ngModelChange)="search()"></label><label>结束日期<input type="date" [(ngModel)]="end" (ngModelChange)="search()"></label><label>状态<select [(ngModel)]="status" (ngModelChange)="search()"><option value="">全部状态</option><option value="completed">执行完成</option><option value="failed">失败</option><option value="cancelled">已取消</option><option value="interrupted">中断</option><option value="skipped">跳过</option></select></label></div>
|
<div class="filters"><label>起始日期<input type="date" [(ngModel)]="start" (ngModelChange)="search()"></label><label>结束日期<input type="date" [(ngModel)]="end" (ngModelChange)="search()"></label><label>状态<select [(ngModel)]="status" (ngModelChange)="search()"><option value="">全部状态</option><option value="completed">执行完成</option><option value="failed">失败</option><option value="cancelled">已取消</option><option value="interrupted">中断</option><option value="skipped">跳过</option></select></label></div>
|
||||||
@if(error()){<p role="alert">{{error()}}</p>}
|
@if(error()){<p role="alert">{{error()}}</p>}
|
||||||
@for(group of groups(); track group.key){<details [open]="!collapsed.has(group.key)" (toggle)="toggle(group.key,$event)"><summary>{{group.title}} <small>本页 {{group.rows.length}} 条</small></summary>
|
@for(group of groups(); track group.key){<details [open]="!collapsed.has(group.key)" (toggle)="toggle(group.key,$event)"><summary>{{group.title}} <small>本页 {{group.rows.length}} 条</small></summary>
|
||||||
@for(row of group.rows; track row.session_id){<div class="history-item" [class]="'history-item status-'+row.status"><div class="history-heading"><app-favorite-button [sourceId]="row.session_id" /><button class="row" [class.selected]="agent.currentSessionId() === row.session_id" (click)="agent.selectSession(row.session_id,true)" [title]="row.initial_goal || ''"><strong>{{row.name || row.initial_goal}}</strong><span class="history-meta"><app-task-mode-badge [session]="row" />@if(row.exploration_kind === "validation"){<span>用例复跑 · v{{row.case_version}}</span>}<b class="history-status">{{label(row.status)}}</b><app-test-result-badge [result]="row.test_result" [executionStatus]="row.status" [compact]="true" /> <span>{{row.end_time ? '完成于 '+(row.end_time*1000 | date:'MM-dd HH:mm:ss') : '开始于 '+(row.start_time*1000 | date:'MM-dd HH:mm:ss')}}</span><span>总耗时 {{duration(row.start_time,row.end_time)}}</span></span>@if(row.error){<span class="history-error" [title]="row.error">{{row.error}}</span>}</button></div><app-task-id-copy [taskId]="row.session_id" />@if(agent.canDeleteSession(row)){<button class="delete-history" (click)="remove(row.session_id, row.exploration_kind)" aria-label="删除此历史任务"><span class="material-symbols-outlined">delete</span></button>}</div>}
|
@for(row of group.rows; track row.session_id){<div class="history-item" [class]="'history-item status-'+row.status"><div class="history-heading"><app-favorite-button [sourceId]="row.session_id" /><button class="row" [class.selected]="agent.currentSessionId() === row.session_id" (click)="agent.selectSession(row.session_id,true)" [title]="row.initial_goal || ''"><strong>{{row.name || row.initial_goal}}</strong><span class="history-meta"><app-task-mode-badge [session]="row" />@if(row.exploration_kind === "validation"){<span>用例复跑 · v{{row.case_version}}</span>}<b class="history-status">{{label(row.status)}}</b><app-test-result-badge [result]="row.test_result" [executionStatus]="row.status" [compact]="true" /> <span>{{row.end_time ? '完成于 '+(row.end_time*1000 | date:'MM-dd HH:mm:ss') : '开始于 '+(row.start_time*1000 | date:'MM-dd HH:mm:ss')}}</span><span>总耗时 {{duration(row.start_time,row.end_time)}}</span></span></button></div><app-task-outcome [error]="row.error" [result]="row.test_result" [status]="row.status" /><app-task-id-copy [taskId]="row.session_id" />@if(agent.canDeleteSession(row)){<button class="delete-history" (click)="remove(row.session_id, row.exploration_kind)" aria-label="删除此历史任务"><span class="material-symbols-outlined">delete</span></button>}</div>}
|
||||||
</details>}
|
</details>}
|
||||||
@if(!loading() && !total()){<p>没有符合条件的历史任务。</p>}
|
@if(!loading() && !total()){<p>没有符合条件的历史任务。</p>}
|
||||||
<div class="pager"><button [disabled]="page === 1 || loading()" (click)="page=page-1;load()">上一页</button><span>{{page}} / {{pages()}}</span><button [disabled]="page >= pages() || loading()" (click)="page=page+1;load()">下一页</button></div>
|
<div class="pager"><button [disabled]="page === 1 || loading()" (click)="page=page-1;load()">上一页</button><span>{{page}} / {{pages()}}</span><button [disabled]="page >= pages() || loading()" (click)="page=page+1;load()">下一页</button></div>
|
||||||
@@ -51,7 +52,6 @@ import { AgentService } from '../../services/agent.service';
|
|||||||
.status-completed .history-status,.status-success .history-status{background:var(--app-success-soft, #dcfce7);color:var(--app-success-text, #166534)}
|
.status-completed .history-status,.status-success .history-status{background:var(--app-success-soft, #dcfce7);color:var(--app-success-text, #166534)}
|
||||||
.status-failed .history-status{background:var(--app-error-soft, #fee2e2);color:var(--app-error-text, #991b1b)}
|
.status-failed .history-status{background:var(--app-error-soft, #fee2e2);color:var(--app-error-text, #991b1b)}
|
||||||
.status-cancelled .history-status,.status-interrupted .history-status{background:var(--app-warning-soft, #fef3c7);color:var(--app-warning-text, #92400e)}
|
.status-cancelled .history-status,.status-interrupted .history-status{background:var(--app-warning-soft, #fef3c7);color:var(--app-warning-text, #92400e)}
|
||||||
.history-error{font-size:11px;color:var(--app-error-text, #b91c1c);line-height:1.4}
|
|
||||||
.delete-history{position:absolute;right:5px;top:8px;border:0;padding:3px;color:var(--app-text-muted, #64748b);cursor:pointer;font-size:12px}
|
.delete-history{position:absolute;right:5px;top:8px;border:0;padding:3px;color:var(--app-text-muted, #64748b);cursor:pointer;font-size:12px}
|
||||||
.delete-history .material-symbols-outlined{font-size:16px}
|
.delete-history .material-symbols-outlined{font-size:16px}
|
||||||
.delete-history:hover{color:var(--app-error-text, #b91c1c)}
|
.delete-history:hover{color:var(--app-error-text, #b91c1c)}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { TaskOutcomeComponent } from './task-outcome.component';
|
||||||
|
|
||||||
|
describe('任务结束说明', () => {
|
||||||
|
function create(status = 'failed', error = '[web-example] FlashRunner failed: **前置受阻**\n\n- 未找到目标设备\n- 未执行删除') {
|
||||||
|
const fixture = TestBed.createComponent(TaskOutcomeComponent);
|
||||||
|
fixture.componentRef.setInput('status', status);
|
||||||
|
fixture.componentRef.setInput('error', error);
|
||||||
|
fixture.detectChanges();
|
||||||
|
return fixture;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('默认只显示一行摘要,展开后渲染完整 Markdown,不选择外层任务', () => {
|
||||||
|
const fixture = create();
|
||||||
|
const host = fixture.nativeElement as HTMLElement;
|
||||||
|
host.style.width = '280px';
|
||||||
|
const details = host.querySelector('details')!;
|
||||||
|
expect(details.open).toBeFalse();
|
||||||
|
expect(host.querySelector('.summary-text')!.textContent).toBe('前置受阻');
|
||||||
|
expect(host.textContent).not.toContain('FlashRunner failed');
|
||||||
|
expect(host.querySelector('button details')).toBeNull();
|
||||||
|
const select = jasmine.createSpy('外层任务选择');
|
||||||
|
host.addEventListener('click', select);
|
||||||
|
host.querySelector('summary')!.click();
|
||||||
|
expect(details.open).toBeTrue();
|
||||||
|
expect(select).not.toHaveBeenCalled();
|
||||||
|
expect(host.querySelector('.body strong')!.textContent).toBe('前置受阻');
|
||||||
|
expect(host.querySelectorAll('.body li').length).toBe(2);
|
||||||
|
fixture.componentRef.setInput('error', '[web-example] FlashRunner failed: **更新说明**\n\n- 已有记录');
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(host.querySelector('details')).toBe(details);
|
||||||
|
expect(details.open).toBeTrue();
|
||||||
|
host.querySelector('summary')!.click();
|
||||||
|
expect(details.open).toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('优先采用已有结论,同时保留不同的执行错误和依据', () => {
|
||||||
|
const fixture = create();
|
||||||
|
fixture.componentRef.setInput('result', {status:'blocked', reason:'目标设备未绑定', evidence:['截图中仅有 **添加设备**']});
|
||||||
|
fixture.detectChanges();
|
||||||
|
const host = fixture.nativeElement as HTMLElement;
|
||||||
|
expect(host.querySelector('.summary-text')!.textContent).toBe('目标设备未绑定');
|
||||||
|
expect(host.textContent).toContain('未执行删除');
|
||||||
|
expect(host.textContent).toContain('已记录依据');
|
||||||
|
expect(host.textContent).toContain('添加设备');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('完成但结论阻塞也展示说明,运行中和无内容时不展示', () => {
|
||||||
|
const fixture = create('running');
|
||||||
|
expect(fixture.nativeElement.querySelector('details')).toBeNull();
|
||||||
|
fixture.componentRef.setInput('status', 'completed');
|
||||||
|
fixture.componentRef.setInput('error', '');
|
||||||
|
fixture.componentRef.setInput('result', {status:'blocked',reason:'缺少预置数据',evidence:[]});
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(fixture.nativeElement.querySelector('.summary-text').textContent).toBe('缺少预置数据');
|
||||||
|
fixture.componentRef.setInput('result', null);
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(fixture.nativeElement.querySelector('details')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('不重复相同说明,不执行错误正文中的 HTML 或危险链接', () => {
|
||||||
|
const error = '**失败**\n<script>alert(1)</script>\n[打开](javascript:alert(1))';
|
||||||
|
const fixture = create('failed', error);
|
||||||
|
fixture.componentRef.setInput('result', {status:'blocked',reason:error,evidence:[]});
|
||||||
|
fixture.detectChanges();
|
||||||
|
const host = fixture.nativeElement as HTMLElement;
|
||||||
|
expect(host.querySelectorAll('app-report-markdown').length).toBe(1);
|
||||||
|
expect(host.querySelector('script')).toBeNull();
|
||||||
|
expect(host.querySelector('a[href^="javascript:"]')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { Component, computed, input } from '@angular/core';
|
||||||
|
import { CaseTestResult } from '../../core/models/session.model';
|
||||||
|
import { ReportMarkdownComponent } from '../report-markdown/report-markdown.component';
|
||||||
|
|
||||||
|
/** Read-only presentation of persisted results; opening it never requests a model. */
|
||||||
|
@Component({
|
||||||
|
selector: 'app-task-outcome',
|
||||||
|
standalone: true,
|
||||||
|
imports: [ReportMarkdownComponent],
|
||||||
|
template: `@if (visible()) {
|
||||||
|
<details (click)="$event.stopPropagation()" (keydown)="$event.stopPropagation()">
|
||||||
|
<summary aria-label="任务说明,展开或收起">
|
||||||
|
<span class="chevron" aria-hidden="true">›</span>
|
||||||
|
<span class="summary-text">{{summary()}}</span>
|
||||||
|
<span class="hint">详情</span>
|
||||||
|
</summary>
|
||||||
|
<div class="body">
|
||||||
|
@if (reason()) { <app-report-markdown [content]="reason()" /> }
|
||||||
|
@if (message() && message() !== reason()) {
|
||||||
|
@if (reason()) { <p class="label">执行记录</p> }
|
||||||
|
<app-report-markdown [content]="message()" />
|
||||||
|
}
|
||||||
|
@if (result()?.evidence?.length) {
|
||||||
|
<p class="label">已记录依据</p>
|
||||||
|
<ul>@for (item of result()!.evidence; track $index) {<li><app-report-markdown [content]="item" /></li>}</ul>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
}`,
|
||||||
|
styles: [`
|
||||||
|
:host { display: block; min-width: 0; max-width: 100%; }
|
||||||
|
details { margin: 4px 0; font-size: 12px; color: var(--app-text-secondary, #475569); }
|
||||||
|
summary { display: flex; align-items: center; gap: 6px; min-width: 0; min-height: 26px; cursor: pointer; list-style: none; border-radius: 4px; }
|
||||||
|
summary::-webkit-details-marker { display: none; }
|
||||||
|
summary:focus-visible { outline: 2px solid var(--app-primary-text, #1a73e8); outline-offset: 2px; }
|
||||||
|
.summary-text { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||||
|
.chevron { flex: none; font-size: 18px; line-height: 20px; }
|
||||||
|
details[open] .chevron { transform: rotate(90deg); }
|
||||||
|
.hint { flex: none; font-size: 11px; color: var(--app-text-muted, #64748b); }
|
||||||
|
.body { max-height: 280px; overflow: auto; padding: 8px 10px; margin-top: 4px; border: 1px solid var(--app-border, #e2e8f0); border-radius: 6px; background: var(--app-surface, #fff); cursor: text; }
|
||||||
|
.label { font-size: 11px; color: var(--app-text-muted, #64748b); margin: 10px 0 4px; }
|
||||||
|
ul { margin: 4px 0; padding-left: 20px; }
|
||||||
|
`]
|
||||||
|
})
|
||||||
|
export class TaskOutcomeComponent {
|
||||||
|
error = input<string | null>();
|
||||||
|
result = input<CaseTestResult | null>();
|
||||||
|
status = input('');
|
||||||
|
// Strip only the known SDK envelope, preserving the actual explanation and diagnostics.
|
||||||
|
private explanation(value: string | null | undefined): string {
|
||||||
|
return (value || '').replace(/^\[[^\]\r\n]+\]\s*FlashRunner failed:\s*/, '').trim();
|
||||||
|
}
|
||||||
|
message = computed(() => this.explanation(this.error()));
|
||||||
|
reason = computed(() => this.explanation(this.result()?.reason));
|
||||||
|
visible = computed(() => ['completed', 'success', 'failed', 'cancelled', 'interrupted', 'skipped'].includes(this.status())
|
||||||
|
&& !!(this.message() || (this.result()?.status !== 'passed' && this.reason())));
|
||||||
|
summary = computed(() => {
|
||||||
|
const line = (this.reason() || this.message()).split(/\r?\n/).find(part => part.trim()) || '';
|
||||||
|
return line.replace(/^\s{0,3}(?:#{1,6}\s+|[-*+]\s+|>\s*)/, '')
|
||||||
|
.replace(/!?\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/(\*\*|__|~~|`)/g, '');
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# ARTEMIS 2.4.4
|
||||||
|
|
||||||
|
本版改善任务结束后错误和阻塞说明的阅读体验,基于 2.4.3。人工评审插件保持 **1.1.0**,SDK、其他插件及第三方依赖版本不变。
|
||||||
|
|
||||||
|
## 更新
|
||||||
|
|
||||||
|
- 历史任务、任务切换列表和批次任务默认只显示一行说明,超长内容省略,点击可展开;报告异常说明采用相同折叠展示。
|
||||||
|
- 展开后安全渲染 Markdown 正文、列表和表格,并展示已有结论及已记录依据;长正文限高滚动。
|
||||||
|
- 列表摘要优先使用已保存的结论原因,否则使用执行错误首行;展示时移除 FlashRunner 固定技术前缀,原始记录保持不变。
|
||||||
|
- 仅展示已结束任务的已有信息,不新增 AI 复盘或模型请求。展开说明不触发任务切换,列表后台刷新保留展开状态。
|
||||||
|
|
||||||
|
本版不改变任务执行、模型重试或批次停止策略,继续保留 2.4.3 的异常输出隔离与参数保护。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- 前端回归:353 项通过,生产构建通过。
|
||||||
|
- 内置浏览器使用隔离样例验证默认折叠、Markdown 展开、长内容、无错误完成任务、批次布局、浅色/深色与窄视口。
|
||||||
|
- 最终源码 ZIP 的隔离解压、独立空 npm 缓存、官方源与国内镜像安装和生产构建结果随附件 `INSTALL-VERIFICATION.md` 提供。
|
||||||
|
- 未复跑用户业务任务、调用真实模型或进行高并发测试;本次没有 Python 行为变更,未重跑 Python 全量回归。
|
||||||
|
|
||||||
|
## 升级
|
||||||
|
|
||||||
|
已有安装请等待任务结束,运行 `update-cn.bat` 或 `update.bat` 后重启。新安装下载 `artemis-2.4.4-source.zip`,按 `SHA256SUMS.txt` 校验后启动。请勿直接覆盖旧安装目录。
|
||||||
|
|
||||||
|
人工评审插件 1.1.0 无需更新。本次发布不构建 Docker,不重启用户服务。
|
||||||
+1
-1
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "artemis"
|
name = "artemis"
|
||||||
version = "2.4.3"
|
version = "2.4.4"
|
||||||
description = "Autonomous AI Assistant for Mobile Automation"
|
description = "Autonomous AI Assistant for Mobile Automation"
|
||||||
|
|
||||||
authors = [
|
authors = [
|
||||||
|
|||||||
Reference in new issue
Block a user