fix: improve cross-device evidence and exploration lifecycle
Align Responses cache affinity across execution and summary calls, preserve required checks and evidence uncertainty, and allow safe deletion of inactive explorations while retaining independent reruns. Validation: backend suite passed except intentional prompt golden drift, then all 17 transcript tests passed after snapshot refresh; 268 frontend tests and production build passed; Ruff, quality ratchet and core type checks passed.
This commit is contained in:
1 parent
f1bb5d02aa
commit
2328a4df0c
32 files changed
+748
-79
No files matched your search
@@ -283,9 +283,18 @@ async def cleanup_history_endpoint():
|
||||
|
||||
@router.post("/api/sessions/{session_id}/delete")
|
||||
async def delete_session_endpoint(session_id: str):
|
||||
from artemis.exploration.store import ExplorationConflict, explorations
|
||||
|
||||
try:
|
||||
from artemis.data_engine.storage import StorageManager
|
||||
|
||||
session_id = str(UUID(session_id))
|
||||
run = explorations.run(session_id)
|
||||
if run and run["kind"] == "exploration":
|
||||
from apps.admin_console.services.exploration_deletion import delete_exploration
|
||||
|
||||
deleted = delete_exploration(explorations, run["exploration_id"])
|
||||
return {"status": "success", "deleted_session_ids": deleted}
|
||||
storage = StorageManager(DB_PATH, TRACES_PATH)
|
||||
storage.delete_session(UUID(session_id))
|
||||
from apps.admin_console.services.automation_store import store
|
||||
@@ -294,6 +303,11 @@ async def delete_session_endpoint(session_id: str):
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Session {session_id} deleted successfully",
|
||||
"deleted_session_ids": [session_id],
|
||||
}
|
||||
except ExplorationConflict as e:
|
||||
raise HTTPException(status_code=409, detail=str(e)) from e
|
||||
except (ValueError, KeyError) as e:
|
||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Delete an inactive conversation while keeping independently executed reruns."""
|
||||
|
||||
import shutil
|
||||
from uuid import UUID
|
||||
|
||||
from apps.admin_console.core.state import state
|
||||
from apps.admin_console.services import report_export
|
||||
from artemis.data_engine.storage import StorageManager
|
||||
from artemis.exploration.store import ExplorationConflict, ExplorationStore
|
||||
from artemis.runtime.process_probe import pid_is_alive
|
||||
from artemis.runtime.task_control import read_state
|
||||
|
||||
|
||||
def _check_runs(runs: list[dict]) -> None:
|
||||
queued = {
|
||||
item.get("session_id")
|
||||
for item in state.queue_items
|
||||
if item.get("status") in {"pending", "queued", "running"}
|
||||
}
|
||||
for run in runs:
|
||||
run_id = run["id"]
|
||||
if (
|
||||
run_id in state.active_runs
|
||||
or run_id in queued
|
||||
or pid_is_alive(read_state(run_id).get("pid"))
|
||||
):
|
||||
raise ExplorationConflict("探索工作进程尚未结束,请停止任务并稍后重试删除")
|
||||
|
||||
|
||||
def delete_exploration(repository: ExplorationStore, exploration_id: str) -> list[str]:
|
||||
runs = repository.begin_delete(exploration_id, _check_runs)
|
||||
# Validation sessions and batch copies remain ordinary Flash/Pro history.
|
||||
deleted = [run["id"] for run in runs if run["kind"] == "exploration"]
|
||||
storage = StorageManager(repository.database.path, report_export.TRACES_PATH)
|
||||
with report_export.EXPORT_LOCK:
|
||||
for run_id in deleted:
|
||||
safe_id = str(UUID(run_id))
|
||||
storage.delete_session(UUID(safe_id))
|
||||
repository.database.delete_history(safe_id)
|
||||
for root in (
|
||||
report_export.EXPORT_ROOT,
|
||||
report_export.EXPORT_ROOT.parent / "mcp_exports",
|
||||
):
|
||||
folder = (root / "sessions" / safe_id).resolve()
|
||||
if folder.is_relative_to(root.resolve()) and folder.is_dir():
|
||||
shutil.rmtree(folder)
|
||||
repository.finish_delete(exploration_id)
|
||||
return deleted
|
||||
@@ -62,6 +62,7 @@ limitations under the License.
|
||||
<span>任务队列</span>
|
||||
</div>
|
||||
<app-task-batch-filter />
|
||||
@if (deleteError()) { <p class="item-error" role="alert">{{ deleteError() }}</p> }
|
||||
<div class="dropdown-section-label">
|
||||
<span class="material-symbols-outlined icon-bolt">bolt</span>
|
||||
<span>当前队列 ({{ activeQueue().length }})</span>
|
||||
@@ -101,7 +102,7 @@ limitations under the License.
|
||||
<span class="item-time-divider">·</span>
|
||||
}
|
||||
<span class="item-time">{{ session.start_time * 1000 | date:'shortTime' }}</span>
|
||||
@if (['running', 'paused', 'task_paused', 'resuming'].includes(getTaskStatus(session))) {
|
||||
@if (!agentService.canDeleteSession(session) && ['running', 'paused', 'task_paused', 'resuming'].includes(getTaskStatus(session))) {
|
||||
<button
|
||||
type="button"
|
||||
class="btn-stop-card"
|
||||
@@ -112,7 +113,7 @@ limitations under the License.
|
||||
<span class="material-symbols-outlined">stop_circle</span>
|
||||
</button>
|
||||
}
|
||||
@if (!['running', 'paused', 'task_paused', 'resuming'].includes(getTaskStatus(session))) {
|
||||
@if (agentService.canDeleteSession(session)) {
|
||||
<button
|
||||
type="button"
|
||||
class="btn-delete-card"
|
||||
|
||||
@@ -294,6 +294,7 @@ export class AgentStreamComponent implements AfterViewInit {
|
||||
public isTaskDropdownOpen = signal<boolean>(false);
|
||||
public isNotesDropdownOpen = signal<boolean>(false);
|
||||
public isAutomationOpen = signal(false);
|
||||
public deleteError = signal('');
|
||||
|
||||
// Run info popover (opened from the Flash / Pro chip): elapsed time, token
|
||||
// usage, executor context share and the Pro tuning the run started with.
|
||||
@@ -997,12 +998,13 @@ export class AgentStreamComponent implements AfterViewInit {
|
||||
|
||||
public deleteTask(sessionId: string, event?: Event): void {
|
||||
if (event) event.stopPropagation();
|
||||
if (!confirm("确定删除此任务?此操作无法撤销。")) {
|
||||
if (!confirm(this.agentService.deletionPrompt(sessionId))) {
|
||||
return;
|
||||
}
|
||||
this.deleteError.set('');
|
||||
this.agentService.deleteSession(sessionId).subscribe({
|
||||
error: (err) => {
|
||||
console.error(`删除会话失败:${sessionId}:`, err);
|
||||
this.deleteError.set(err.error?.detail || "删除失败,请稍后重试");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ limitations under the License.
|
||||
<!-- Tasks Tab Content -->
|
||||
@if (agentService.activeTab() === 'tasks') {
|
||||
<app-task-batch-filter />
|
||||
@if (errorMessage()) { <p class="error-message" role="alert">{{ errorMessage() }}</p> }
|
||||
<!-- Active Queue Section -->
|
||||
<div class="queue-section-header">
|
||||
<span class="material-symbols-outlined section-icon">bolt</span> 当前队列 ({{ activeQueue().length }})
|
||||
@@ -99,7 +100,7 @@ limitations under the License.
|
||||
<span class="task-time-divider">·</span>
|
||||
}
|
||||
<span class="task-time">{{ session.start_time * 1000 | date:'shortTime' }}</span>
|
||||
@if (getTaskStatus(session) === 'running' || getTaskStatus(session) === 'paused') {
|
||||
@if (!agentService.canDeleteSession(session) && (getTaskStatus(session) === 'running' || getTaskStatus(session) === 'paused')) {
|
||||
<button
|
||||
type="button"
|
||||
class="btn-stop-card"
|
||||
@@ -110,7 +111,7 @@ limitations under the License.
|
||||
<span class="material-symbols-outlined">stop_circle</span>
|
||||
</button>
|
||||
}
|
||||
@if (getTaskStatus(session) !== 'running' && getTaskStatus(session) !== 'paused') {
|
||||
@if (agentService.canDeleteSession(session)) {
|
||||
<button
|
||||
type="button"
|
||||
class="btn-delete-card"
|
||||
|
||||
@@ -162,7 +162,7 @@ export class ChatInterfaceComponent {
|
||||
*/
|
||||
public deleteTask(sessionId: string, event: MouseEvent): void {
|
||||
event.stopPropagation();
|
||||
if (!confirm("确定删除此任务?此操作无法撤销。")) {
|
||||
if (!confirm(this.agentService.deletionPrompt(sessionId))) {
|
||||
return;
|
||||
}
|
||||
this.isSubmitting.set(true);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { of, throwError } from 'rxjs';
|
||||
import { Clipboard } from '@angular/cdk/clipboard';
|
||||
import { fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
@@ -10,11 +11,46 @@ describe('历史任务检索', () => {
|
||||
let http: HttpTestingController;
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({providers: [provideHttpClient(), provideHttpClientTesting(),
|
||||
{provide: AgentService, useValue: {currentSessionId: signal(null), batchFilter: signal(''), resultRevision: signal(0), selectSession: jasmine.createSpy()}}]});
|
||||
{provide: AgentService, useValue: {currentSessionId: signal(null), batchFilter: signal(''), resultRevision: signal(0), selectSession: jasmine.createSpy(), deleteSession: jasmine.createSpy().and.returnValue(of({})), canDeleteSession: AgentService.prototype.canDeleteSession, deletionPrompt: (_id: string, kind: string) => kind === 'exploration' ? '删除探索及全部探索记录,复跑与批次保留' : '删除历史任务'}}]});
|
||||
http = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
afterEach(() => http.verify());
|
||||
|
||||
it('已终止探索可删除,确认范围后刷新,取消不发请求', () => {
|
||||
const fixture = TestBed.createComponent(TaskHistoryComponent);
|
||||
const row = {session_id:'stopped', status:'cancelled', exploration_kind:'exploration', exploration_status:'paused', start_time:100};
|
||||
http.expectOne(r => r.url === '/api/automation/history').flush({total:1,items:[row]});
|
||||
fixture.detectChanges();
|
||||
const button = fixture.nativeElement.querySelector('.delete-history');
|
||||
expect(button).not.toBeNull();
|
||||
const confirm = spyOn(window, 'confirm').and.returnValue(false);
|
||||
button.click();
|
||||
expect(TestBed.inject(AgentService).deleteSession).not.toHaveBeenCalled();
|
||||
expect(confirm.calls.mostRecent().args[0]).toContain('复跑与批次保留');
|
||||
confirm.and.returnValue(true);
|
||||
button.click();
|
||||
expect(TestBed.inject(AgentService).deleteSession).toHaveBeenCalledOnceWith('stopped');
|
||||
http.expectOne(r => r.url === '/api/automation/history').flush({total:0,items:[]});
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.querySelector('.delete-history')).toBeNull();
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
it('服务端拒绝删除时保留记录并显示具体原因', () => {
|
||||
const fixture = TestBed.createComponent(TaskHistoryComponent);
|
||||
http.expectOne(r => r.url === '/api/automation/history').flush({total:1,items:[
|
||||
{session_id:'stopped',status:'cancelled',exploration_kind:'exploration',start_time:100}
|
||||
]});
|
||||
spyOn(window, 'confirm').and.returnValue(true);
|
||||
(TestBed.inject(AgentService).deleteSession as jasmine.Spy).and.returnValue(throwError(() => ({error:{detail:'探索正在分析,请稍后删除'}})));
|
||||
fixture.detectChanges();
|
||||
fixture.nativeElement.querySelector('.delete-history').click();
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.querySelector('[role="alert"]').textContent).toContain('探索正在分析');
|
||||
expect(fixture.nativeElement.querySelector('.delete-history')).not.toBeNull();
|
||||
fixture.destroy();
|
||||
});
|
||||
|
||||
it('完成任务分别显示探索、Flash、Pro,模式不受通过或失败颜色影响', () => {
|
||||
const fixture = TestBed.createComponent(TaskHistoryComponent);
|
||||
http.expectOne(r => r.url === '/api/automation/history').flush({total:3,items:[
|
||||
|
||||
@@ -14,7 +14,7 @@ import { AgentService } from '../../services/agent.service';
|
||||
<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>}
|
||||
@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"><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><app-task-id-copy [taskId]="row.session_id" />@if(row.exploration_kind !== "exploration"){<button class="delete-history" (click)="remove(row.session_id)" 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"><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><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>}
|
||||
@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>
|
||||
@@ -38,7 +38,7 @@ export class TaskHistoryComponent {
|
||||
});
|
||||
const timer=setInterval(()=>this.load(true),15000);inject(DestroyRef).onDestroy(()=>{this.revision++;clearInterval(timer);clearTimeout(this.debounce);});
|
||||
}
|
||||
remove(id:string){if(!confirm("确定删除此历史任务?此操作无法撤销。"))return;this.agent.deleteSession(id).subscribe({next:()=>this.load(),error:()=>this.error.set("删除失败,请稍后重试")});}
|
||||
remove(id:string, kind?:string){if(!confirm(this.agent.deletionPrompt(id,kind)))return;this.agent.deleteSession(id).subscribe({next:()=>this.load(),error:e=>this.error.set(e.error?.detail || "删除失败,请稍后重试")});}
|
||||
pages(){return Math.max(1,Math.ceil(this.total()/30));}
|
||||
label(s:string){return ({completed:'执行完成',success:'执行完成',failed:'失败',cancelled:'已终止',interrupted:'中断',skipped:'跳过'} as Record<string,string>)[s] || s;}
|
||||
duration(start:number,end?:number){if(!start||!end)return '—';const seconds=Math.max(0,Math.floor(end-start));const hours=Math.floor(seconds/3600);const minutes=Math.floor((seconds%3600)/60);return hours?`${hours}时${minutes}分`:minutes?`${minutes}分${seconds%60}秒`:`${seconds}秒`;}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { signal, computed } from '@angular/core';
|
||||
import { of } from 'rxjs';
|
||||
import { of, Subject } from 'rxjs';
|
||||
|
||||
import { AgentService } from './agent.service';
|
||||
|
||||
@@ -531,3 +531,67 @@ describe('Queued exploration identity',()=>{
|
||||
expect(fields.exploration_id).toBe('conversation');expect(fields.model_info?.architecture).toBe('explore');expect(fields.exploration_latest_run_id).toBe('new');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Exploration deletion', () => {
|
||||
function createService() {
|
||||
const service = Object.create(AgentService.prototype) as AgentService;
|
||||
const records: any[] = [
|
||||
{session_id:'old',status:'completed',exploration_kind:'exploration'},
|
||||
{session_id:'stopped',status:'cancelled',exploration_status:'paused',exploration_kind:'exploration'},
|
||||
{session_id:'validation',status:'completed',exploration_kind:'validation'}
|
||||
];
|
||||
(service as any).rawSessions = signal(records);
|
||||
(service as any).activeSessionTracking = new Map(records.map(row => [row.session_id,row]));
|
||||
(service as any).pendingQueue = signal([]);
|
||||
service.currentSessionId = signal('stopped');
|
||||
service.userPinnedSessionId = signal('stopped');
|
||||
service.isVideoWindowOpen = signal(false);
|
||||
service.runningSessionId = signal(null);
|
||||
service.resultRevision = signal(0);
|
||||
(service as any).invalidateStatusSignatures = jasmine.createSpy();
|
||||
(service as any).persistSessionsCache = jasmine.createSpy();
|
||||
service.selectSession = jasmine.createSpy();
|
||||
(service as any).fetchSessions = jasmine.createSpy();
|
||||
(service as any).fetchStatus = jasmine.createSpy();
|
||||
const response = new Subject<any>();
|
||||
(service as any).http = {post:jasmine.createSpy().and.returnValue(response)};
|
||||
return {service,response};
|
||||
}
|
||||
|
||||
it('shows delete for ended resumable conversations but not live paused workers', () => {
|
||||
const {service} = createService();
|
||||
expect(service.canDeleteSession({status:'cancelled'})).toBeTrue();
|
||||
expect(service.canDeleteSession({status:'completed'})).toBeTrue();
|
||||
for(const status of ['running','paused','pending','queued','resuming']) {
|
||||
expect(service.canDeleteSession({status})).toBeFalse();
|
||||
}
|
||||
expect(service.deletionPrompt('stopped')).toContain('全部探索执行记录');
|
||||
expect(service.deletionPrompt('stopped')).toContain('复跑与批次任务将保留');
|
||||
});
|
||||
|
||||
it('waits for success and removes all deleted runs without removing reruns', () => {
|
||||
const {service,response} = createService();
|
||||
service.deleteSession('stopped').subscribe();
|
||||
expect((service as any).rawSessions().length).toBe(3);
|
||||
expect(service.selectSession).not.toHaveBeenCalled();
|
||||
response.next({deleted_session_ids:['old','stopped']});
|
||||
expect((service as any).rawSessions().map((s:any)=>s.session_id)).toEqual(['validation']);
|
||||
expect((service as any).activeSessionTracking.has('old')).toBeFalse();
|
||||
expect(service.selectSession).toHaveBeenCalledWith('',false);
|
||||
expect(service.userPinnedSessionId()).toBeNull();
|
||||
expect(service.resultRevision()).toBe(1);
|
||||
});
|
||||
|
||||
it('keeps selection and history when server refuses deletion', () => {
|
||||
const {service,response} = createService();
|
||||
const error = jasmine.createSpy();
|
||||
service.deleteSession('stopped').subscribe({error});
|
||||
response.error({status:409,error:{detail:'探索仍在执行'}});
|
||||
expect(error).toHaveBeenCalled();
|
||||
expect((service as any).rawSessions().length).toBe(3);
|
||||
expect(service.selectSession).not.toHaveBeenCalled();
|
||||
expect(service.userPinnedSessionId()).toBe('stopped');
|
||||
expect(service.resultRevision()).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import { Injectable, signal, inject, computed, DestroyRef, NgZone } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, tap } from 'rxjs';
|
||||
|
||||
import { Session, ModelInfo, TaskQueueItem, AgentStatusResponse, SessionUsage, CaseReviewResponse } from '../core/models/session.model';
|
||||
import { ProTuningDefaults, ProTuningOptions } from '../core/models/pro-tuning.model';
|
||||
@@ -718,45 +718,39 @@ export class AgentService {
|
||||
/**
|
||||
* Delete an individual session / task
|
||||
*/
|
||||
public canDeleteSession(session: Pick<Session, 'status'>): boolean {
|
||||
return ['completed', 'success', 'failed', 'cancelled', 'interrupted', 'skipped'].includes(session.status || '');
|
||||
}
|
||||
|
||||
public deletionPrompt(sessionId: string, explorationKind?: string): string {
|
||||
const kind = explorationKind || this.rawSessions().find(s => s.session_id === sessionId)?.exploration_kind;
|
||||
return kind === 'exploration'
|
||||
? '确定删除此探索的对话、用例版本及全部探索执行记录和文件?已生成的复跑与批次任务将保留。此操作无法撤销。'
|
||||
: '确定删除此历史任务?此操作无法撤销。';
|
||||
}
|
||||
|
||||
public deleteSession(sessionId: string): Observable<any> {
|
||||
// 1. Optimistically update local session state immediately
|
||||
this.invalidateStatusSignatures();
|
||||
this.rawSessions.update((list) => list.filter((s) => s.session_id !== sessionId));
|
||||
this.persistSessionsCache(this.rawSessions());
|
||||
this.pendingQueue.update((list) => list.filter((s) => s.session_id !== sessionId));
|
||||
|
||||
if (this.userPinnedSessionId() === sessionId) {
|
||||
this.userPinnedSessionId.set(null);
|
||||
}
|
||||
|
||||
if (this.currentSessionId() === sessionId) {
|
||||
this.selectSession('', false);
|
||||
if (this.isVideoWindowOpen()) {
|
||||
this.closeVideoPlayer();
|
||||
}
|
||||
const runningId = this.runningSessionId();
|
||||
if (runningId && this.agentStatus() === 'running') {
|
||||
this.selectSession(runningId, false);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Send API request
|
||||
return new Observable((obs) => {
|
||||
this.http.post<any>(`/api/sessions/${sessionId}/delete`, {}).subscribe({
|
||||
next: (res) => {
|
||||
this.fetchSessions();
|
||||
this.fetchStatus();
|
||||
obs.next(res);
|
||||
obs.complete();
|
||||
},
|
||||
error: (err) => {
|
||||
console.error(`删除会话失败:${sessionId}:`, err);
|
||||
this.fetchSessions();
|
||||
this.fetchStatus();
|
||||
obs.error(err);
|
||||
// Keep the task selected until the server confirms the entire deletion.
|
||||
return this.http.post<any>(`/api/sessions/${sessionId}/delete`, {}).pipe(tap(res => {
|
||||
const deleted = new Set<string>(res.deleted_session_ids || [sessionId]);
|
||||
this.invalidateStatusSignatures();
|
||||
for (const id of deleted) this.activeSessionTracking.delete(id);
|
||||
this.rawSessions.update(list => list.filter(s => !deleted.has(s.session_id)));
|
||||
this.pendingQueue.update(list => list.filter(s => !deleted.has(s.session_id)));
|
||||
this.persistSessionsCache(this.rawSessions());
|
||||
if (deleted.has(this.userPinnedSessionId() || '')) this.userPinnedSessionId.set(null);
|
||||
if (deleted.has(this.currentSessionId() || '')) {
|
||||
this.selectSession('', false);
|
||||
if (this.isVideoWindowOpen()) this.closeVideoPlayer();
|
||||
const runningId = this.runningSessionId();
|
||||
if (runningId && !deleted.has(runningId) && this.agentStatus() === 'running') {
|
||||
this.selectSession(runningId, false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
this.resultRevision.update(value => value + 1);
|
||||
this.fetchSessions();
|
||||
this.fetchStatus();
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user