feat: release 1.4.0 with isolated upgrades and device discovery
This commit is contained in:
1 parent
a6a3ee2eb0
commit
df66897ff4
41 files changed
+1564
-126
No files matched your search
@@ -274,7 +274,12 @@ async def get_run_defaults():
|
||||
async def list_devices():
|
||||
"""List all connected Android devices with their busy / idle status."""
|
||||
devices = await device_pool.list_devices_async()
|
||||
return {"devices": [d.to_dict() for d in devices]}
|
||||
from artemis.runtime.adb_discovery import adb_device_discovery
|
||||
|
||||
return {
|
||||
"devices": [d.to_dict() for d in devices],
|
||||
"automatic_connections": adb_device_discovery.results.copy(),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/api/stop")
|
||||
|
||||
@@ -166,6 +166,13 @@ def persist_environment(values: dict[str, str]) -> None:
|
||||
|
||||
|
||||
class CapabilitySettings(BaseModel):
|
||||
adb_auto_connect: bool = Field(default_factory=lambda: settings.ARTEMIS_ADB_AUTO_CONNECT)
|
||||
adb_discovery_interval_seconds: float = Field(
|
||||
default_factory=lambda: settings.ARTEMIS_ADB_DISCOVERY_INTERVAL_SECONDS, ge=1, le=300
|
||||
)
|
||||
adb_discovery_timeout_seconds: float = Field(
|
||||
default_factory=lambda: settings.ARTEMIS_ADB_DISCOVERY_TIMEOUT_SECONDS, gt=0, le=30
|
||||
)
|
||||
video_enabled: bool = False
|
||||
recording_enabled: bool = False
|
||||
outputter_enabled: bool = True
|
||||
@@ -206,6 +213,9 @@ def get_capability_settings() -> CapabilitySettings:
|
||||
|
||||
def save_capability_settings(update: CapabilitySettings) -> CapabilitySettings:
|
||||
values = {
|
||||
"ARTEMIS_ADB_AUTO_CONNECT": update.adb_auto_connect,
|
||||
"ARTEMIS_ADB_DISCOVERY_INTERVAL_SECONDS": update.adb_discovery_interval_seconds,
|
||||
"ARTEMIS_ADB_DISCOVERY_TIMEOUT_SECONDS": update.adb_discovery_timeout_seconds,
|
||||
"ARTEMIS_VIDEO_ENABLED": update.video_enabled,
|
||||
"ARTEMIS_RECORDING_ENABLED": update.recording_enabled,
|
||||
"ARTEMIS_OUTPUTTER_ENABLED": update.outputter_enabled,
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "artemis-showcase-ui",
|
||||
"version": "1.3.3",
|
||||
"version": "1.4.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "artemis-showcase-ui",
|
||||
"version": "1.3.3",
|
||||
"version": "1.4.0",
|
||||
"dependencies": {
|
||||
"@angular/cdk": "^22.1.5",
|
||||
"@angular/common": "^22.1.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "artemis-showcase-ui",
|
||||
"version": "1.3.3",
|
||||
"version": "1.4.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve --proxy-config proxy.conf.json",
|
||||
|
||||
+11
-2
@@ -4,6 +4,9 @@ import { HttpClient } from '@angular/common/http';
|
||||
import { finalize } from 'rxjs';
|
||||
|
||||
interface Capabilities {
|
||||
adb_auto_connect: boolean;
|
||||
adb_discovery_interval_seconds: number;
|
||||
adb_discovery_timeout_seconds: number;
|
||||
video_enabled: boolean;
|
||||
recording_enabled: boolean;
|
||||
outputter_enabled: boolean;
|
||||
@@ -24,6 +27,8 @@ interface Capabilities {
|
||||
<section aria-label="功能开关">
|
||||
<h3>功能开关</h3>
|
||||
<p>按需启用,保存后对新任务生效。</p>
|
||||
<label><input type="checkbox" role="switch" [(ngModel)]="config.adb_auto_connect" [disabled]="busy() || loading() || !loaded()" />
|
||||
<span>自动连接设备<small>刷新设备时发现本机常见模拟器,并重连手动连接过的 TCP 设备;重启 ADB 后也会重连。不扫描局域网,不自动选择任务设备。保存后立即生效。</small></span></label>
|
||||
<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()" />
|
||||
@@ -44,6 +49,8 @@ interface Capabilities {
|
||||
<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>
|
||||
<label>设备自动发现间隔(秒)<input type="number" min="1" max="300" [(ngModel)]="config.adb_discovery_interval_seconds" [disabled]="busy() || !loaded()" /></label>
|
||||
<label>每轮自动连接预算(秒)<input type="number" min="1" max="30" [(ngModel)]="config.adb_discovery_timeout_seconds" [disabled]="busy() || !loaded()" /></label>
|
||||
</div>
|
||||
</details>
|
||||
<button type="button" (click)="save()" [disabled]="busy() || loading() || !loaded() || !valid()">{{ busy() ? '正在保存…' : '保存功能设置' }}</button>
|
||||
@@ -67,7 +74,7 @@ interface Capabilities {
|
||||
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 };
|
||||
config: Capabilities = { adb_auto_connect: true, adb_discovery_interval_seconds: 10, adb_discovery_timeout_seconds: 5, 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({
|
||||
@@ -89,6 +96,8 @@ export class CapabilitySettingsComponent implements OnInit {
|
||||
&& 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;
|
||||
&& c.worker_cleanup_timeout_seconds > 0 && c.worker_cleanup_timeout_seconds <= 300
|
||||
&& c.adb_discovery_interval_seconds >= 1 && c.adb_discovery_interval_seconds <= 300
|
||||
&& c.adb_discovery_timeout_seconds > 0 && c.adb_discovery_timeout_seconds <= 30;
|
||||
}
|
||||
}
|
||||
+24
-2
@@ -138,7 +138,7 @@ describe('持久化模型与功能设置', () => {
|
||||
});
|
||||
|
||||
it('录屏与视频分析独立保存,重新打开后保留各自状态', () => {
|
||||
const capabilities = { video_enabled: true, recording_enabled: false, outputter_enabled: true, step_summarizer_enabled: false, 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 };
|
||||
const capabilities = { adb_auto_connect: true, adb_discovery_interval_seconds: 10, adb_discovery_timeout_seconds: 5, video_enabled: true, recording_enabled: false, outputter_enabled: true, step_summarizer_enabled: false, 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 };
|
||||
const fixture = TestBed.createComponent(CapabilitySettingsComponent);
|
||||
fixture.detectChanges();
|
||||
http.expectOne('/api/system/capability-settings').flush(capabilities);
|
||||
@@ -162,7 +162,7 @@ describe('持久化模型与功能设置', () => {
|
||||
});
|
||||
|
||||
it('执行保护默认折叠,修改时限可保存且拒绝无效数值', async () => {
|
||||
const 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 };
|
||||
const capabilities = { adb_auto_connect: true, adb_discovery_interval_seconds: 10, adb_discovery_timeout_seconds: 5, 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 };
|
||||
const fixture = TestBed.createComponent(CapabilitySettingsComponent);
|
||||
fixture.detectChanges();
|
||||
http.expectOne('/api/system/capability-settings').flush(capabilities);
|
||||
@@ -190,4 +190,26 @@ describe('持久化模型与功能设置', () => {
|
||||
fixture.componentInstance.save();
|
||||
http.expectNone('/api/system/capability-settings');
|
||||
});
|
||||
|
||||
it('自动连接默认开启,可独立关闭并保存发现预算', () => {
|
||||
const fixture = TestBed.createComponent(CapabilitySettingsComponent);
|
||||
const capabilities = { ...fixture.componentInstance.config };
|
||||
fixture.detectChanges();
|
||||
http.expectOne('/api/system/capability-settings').flush(capabilities);
|
||||
fixture.detectChanges();
|
||||
const root: HTMLElement = fixture.nativeElement;
|
||||
expect(root.textContent).toContain('自动连接设备');
|
||||
expect(fixture.componentInstance.config.adb_auto_connect).toBeTrue();
|
||||
fixture.componentInstance.config.adb_auto_connect = false;
|
||||
fixture.componentInstance.config.adb_discovery_timeout_seconds = 8;
|
||||
fixture.componentInstance.save();
|
||||
const request = http.expectOne('/api/system/capability-settings');
|
||||
expect(request.request.body.adb_auto_connect).toBeFalse();
|
||||
expect(request.request.body.adb_discovery_timeout_seconds).toBe(8);
|
||||
expect(request.request.body.recording_enabled).toBeFalse();
|
||||
request.flush(request.request.body);
|
||||
fixture.componentInstance.config.adb_discovery_timeout_seconds = 0;
|
||||
fixture.componentInstance.save();
|
||||
http.expectNone('/api/system/capability-settings');
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,7 @@ import { Component, signal, computed, inject, OnInit, OnDestroy, ChangeDetection
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { AgentService } from '../../services/agent.service';
|
||||
import { SystemService } from '../../services/system.service';
|
||||
import { SystemService, formatAdbRestartFeedback } from '../../services/system.service';
|
||||
import {
|
||||
AdbServerConnectionResult,
|
||||
AdbServerDevice,
|
||||
@@ -548,10 +548,8 @@ export class HomeComponent implements OnInit, OnDestroy {
|
||||
this.adbRestartFeedback.set(null);
|
||||
this.systemService.restartAdb().subscribe({
|
||||
next: (res) => {
|
||||
this.adbRestartFeedback.set(
|
||||
res?.restart_result?.skipped ? "设备已刷新 ✓" : "ADB 已刷新 ✓"
|
||||
);
|
||||
setTimeout(() => this.adbRestartFeedback.set(null), 2500);
|
||||
this.adbRestartFeedback.set(formatAdbRestartFeedback(res?.restart_result));
|
||||
setTimeout(() => this.adbRestartFeedback.set(null), 6000);
|
||||
},
|
||||
error: () => {
|
||||
this.adbRestartFeedback.set("重启失败");
|
||||
|
||||
@@ -3,7 +3,23 @@ import { HttpTestingController, provideHttpClientTesting } from '@angular/common
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SystemReadinessReport } from '../core/models/system.model';
|
||||
import { SystemService } from './system.service';
|
||||
import { SystemService, formatAdbRestartFeedback } from './system.service';
|
||||
|
||||
describe('ADB restart feedback', () => {
|
||||
it('区分已连接与正在连接,不把等待就绪显示为失败', () => {
|
||||
const message = formatAdbRestartFeedback({ success: true, automatic_connections: [{ success: true }, { success: false, pending: true }] });
|
||||
expect(message).toContain('已连接 1 台设备');
|
||||
expect(message).toContain('1 台设备连接中');
|
||||
expect(message).not.toContain('失败');
|
||||
});
|
||||
it('显示连接失败数量,提示查看诊断', () => {
|
||||
expect(formatAdbRestartFeedback({ success: true, automatic_connections: [{ success: false }] })).toContain('1 项连接失败,请查看诊断');
|
||||
});
|
||||
it('服务器重启失败不会显示成功,远程端点明确显示仅刷新', () => {
|
||||
expect(formatAdbRestartFeedback({ success: false, message: '端口被占用' })).toBe('端口被占用');
|
||||
expect(formatAdbRestartFeedback({ success: true, skipped: true })).toBe('设备已刷新 ✓');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SystemService readiness polling', () => {
|
||||
let service: SystemService;
|
||||
|
||||
@@ -27,6 +27,19 @@ import {
|
||||
EmulatorLaunchStage
|
||||
} from '../core/models/system.model';
|
||||
|
||||
export function formatAdbRestartFeedback(result?: {
|
||||
success?: boolean; skipped?: boolean; message?: string;
|
||||
automatic_connections?: { success?: boolean; pending?: boolean }[];
|
||||
}): string {
|
||||
if (!result?.success) return result?.message || '重启失败';
|
||||
if (result.skipped) return '设备已刷新 ✓';
|
||||
const connections = result.automatic_connections ?? [];
|
||||
const connected = connections.filter(item => item.success).length;
|
||||
const pending = connections.filter(item => item.pending).length;
|
||||
const failed = connections.filter(item => !item.success && !item.pending).length;
|
||||
return `ADB 已重启${connected ? `,已连接 ${connected} 台设备` : ''}${pending ? `,${pending} 台设备连接中,将自动重试` : ''}${failed ? `,${failed} 项连接失败,请查看诊断` : ' ✓'}`;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user