Files
Open-AutoGLM/dashboard/static/js/dashboard.js
let5sne.win10 3552df23d6 Add Web Dashboard with multi-device control and callback hooks
Features:
- Web Dashboard: FastAPI-based dashboard with Vue.js frontend
  - Multi-device support (ADB, HDC, iOS)
  - Real-time WebSocket updates for task progress
  - Device management with status tracking
  - Task queue with execution controls (start/stop/re-execute)
  - Detailed task information display (thinking, actions, completion messages)
  - Screenshot viewing per device
  - LAN deployment support with configurable CORS

- Callback Hooks: Interrupt and modify task execution
  - step_callback: Called after each step with StepResult
  - before_action_callback: Called before executing action
  - Support for task interruption and dynamic task switching
  - Example scripts demonstrating callback usage

- Configuration: Environment-based configuration
  - .env file support for all settings
  - .env.example template with documentation
  - Model API configuration (base URL, model name, API key)
  - Dashboard configuration (host, port, CORS, device type)
  - Phone agent configuration (delays, max steps, language)

Technical improvements:
- Fixed forward reference issue with StepResult
- Added package exports for callback types and configs
- Enhanced dependencies with FastAPI, WebSocket support
- Thread-safe task execution with device locking
- Async WebSocket broadcasting from sync thread pool

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-09 02:20:06 +08:00

405 lines
13 KiB
JavaScript

/**
* AutoGLM Dashboard - Vue.js Application
*/
const { createApp } = Vue;
createApp({
data() {
return {
devices: [],
tasks: [],
ws: null,
wsConnected: false,
refreshing: false,
toasts: [],
toastIdCounter: 0,
reconnectAttempts: 0,
maxReconnectAttempts: 5,
reconnectDelay: 2000,
};
},
computed: {
activeTasks() {
return this.tasks.filter(t => t.status === 'running');
},
connectedDeviceCount() {
return this.devices.filter(d => d.is_connected).length;
},
},
methods: {
/* API Methods */
async loadDevices() {
try {
const response = await axios.get('/api/devices');
this.devices = response.data.map(d => ({
...d,
taskInput: '',
screenshot: null,
}));
} catch (error) {
this.showToast('Failed to load devices', 'error');
console.error('Error loading devices:', error);
}
},
async loadTasks() {
try {
const response = await axios.get('/api/tasks');
this.tasks = response.data;
} catch (error) {
console.error('Error loading tasks:', error);
}
},
async refreshDevices() {
this.refreshing = true;
try {
await axios.get('/api/devices/refresh');
await this.loadDevices();
this.showToast('Devices refreshed', 'success');
} catch (error) {
this.showToast('Failed to refresh devices', 'error');
} finally {
this.refreshing = false;
}
},
async executeTask(device) {
if (!device.taskInput || !device.taskInput.trim()) {
return;
}
const task = device.taskInput.trim();
device.taskInput = '';
try {
const response = await axios.post('/api/tasks/execute', {
device_id: device.device_id,
task: task,
max_steps: 100,
lang: 'cn',
// Backend will use config from environment
});
this.showToast(`Task started: ${task}`, 'success');
await this.loadTasks();
} catch (error) {
this.showToast('Failed to start task', 'error');
console.error('Error executing task:', error);
}
},
async stopTask(task) {
try {
await axios.post(`/api/tasks/${task.task_id}/stop`);
this.showToast('Stopping task...', 'info');
} catch (error) {
this.showToast('Failed to stop task', 'error');
}
},
async reExecuteTask(task) {
const device = this.devices.find(d => d.device_id === task.device_id);
if (!device) {
this.showToast('Device not found', 'error');
return;
}
if (!device.is_connected) {
this.showToast('Device is not connected', 'error');
return;
}
if (device.status === 'busy') {
this.showToast('Device is busy', 'error');
return;
}
try {
const response = await axios.post('/api/tasks/execute', {
device_id: task.device_id,
task: task.task,
max_steps: task.max_steps || 100,
lang: 'cn',
});
this.showToast(`Task re-executed: ${task.task}`, 'success');
await this.loadTasks();
} catch (error) {
this.showToast('Failed to re-execute task', 'error');
console.error('Error re-executing task:', error);
}
},
async captureScreenshot(deviceId) {
try {
const response = await axios.get(`/api/devices/${deviceId}/screenshot`);
const device = this.devices.find(d => d.device_id === deviceId);
if (device) {
device.screenshot = response.data.screenshot;
}
} catch (error) {
console.error('Error capturing screenshot:', error);
}
},
/* WebSocket Methods */
connectWebSocket() {
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${location.host}/ws`;
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => {
this.wsConnected = true;
this.reconnectAttempts = 0;
console.log('WebSocket connected');
// Subscribe to all devices updates
this.sendWebSocketMessage({
type: 'subscribe',
device_id: '*', // Subscribe to all devices
});
};
this.ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
this.handleWSMessage(msg);
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
};
this.ws.onclose = () => {
this.wsConnected = false;
console.log('WebSocket disconnected');
// Attempt to reconnect
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
console.log(`Reconnecting... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
setTimeout(() => this.connectWebSocket(), this.reconnectDelay);
} else {
this.showToast('WebSocket connection lost', 'error');
}
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
},
sendWebSocketMessage(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
},
handleWSMessage(msg) {
switch (msg.type) {
case 'device_update':
this.updateDevice(msg.data);
break;
case 'task_started':
this.handleTaskStarted(msg.data);
break;
case 'task_step':
this.handleTaskStep(msg.data);
break;
case 'task_completed':
this.handleTaskCompleted(msg.data);
break;
case 'task_failed':
this.handleTaskFailed(msg.data);
break;
case 'task_stopped':
this.handleTaskStopped(msg.data);
break;
case 'screenshot':
this.handleScreenshot(msg.data);
break;
case 'error':
this.showToast(msg.data.error || 'An error occurred', 'error');
break;
case 'pong':
// Ping response, ignore
break;
default:
console.log('Unknown WebSocket message type:', msg.type);
}
},
/* Message Handlers */
updateDevice(data) {
const device = this.devices.find(d => d.device_id === data.device_id);
if (device) {
Object.assign(device, {
status: data.status,
is_connected: data.is_connected,
model: data.model,
current_app: data.current_app,
});
}
},
handleTaskStarted(data) {
this.tasks.unshift({
task_id: data.task_id,
device_id: data.device_id,
task: data.task,
status: 'running',
current_step: 0,
max_steps: data.max_steps || 100,
current_action: null,
thinking: null,
started_at: new Date(),
});
// Update device status
const device = this.devices.find(d => d.device_id === data.device_id);
if (device) {
device.status = 'busy';
}
},
handleTaskStep(data) {
const task = this.tasks.find(t => t.task_id === data.task_id);
if (task) {
task.current_step = data.step;
task.current_action = data.action;
task.thinking = data.thinking;
// Store the completion message
if (data.message && data.finished) {
task.completion_message = data.message;
}
if (data.finished) {
task.status = data.success ? 'completed' : 'failed';
this.releaseDevice(data.device_id);
}
}
},
handleTaskCompleted(data) {
const task = this.tasks.find(t => t.task_id === data.task_id);
if (task) {
task.status = 'completed';
task.finished_at = new Date();
// Store completion message
if (data.message) {
task.completion_message = data.message;
}
this.releaseDevice(data.device_id);
this.showToast(`Task completed: ${task.task}`, 'success');
}
},
handleTaskFailed(data) {
const task = this.tasks.find(t => t.task_id === data.task_id);
if (task) {
task.status = 'failed';
task.error = data.error;
task.finished_at = new Date();
this.releaseDevice(data.device_id);
this.showToast(`Task failed: ${data.error}`, 'error');
}
},
handleTaskStopped(data) {
const task = this.tasks.find(t => t.task_id === data.task_id);
if (task) {
task.status = 'stopped';
task.finished_at = new Date();
this.releaseDevice(data.device_id);
this.showToast('Task stopped', 'info');
}
},
handleScreenshot(data) {
const device = this.devices.find(d => d.device_id === data.device_id);
if (device) {
device.screenshot = data.screenshot;
}
},
releaseDevice(deviceId) {
const device = this.devices.find(d => d.device_id === deviceId);
if (device && device.status === 'busy') {
device.status = 'online';
}
},
/* Utility Methods */
formatAppName(packageName) {
if (!packageName) return '';
// Format package name for display
const parts = packageName.split('.');
return parts[parts.length - 1] || packageName;
},
showToast(message, type = 'info') {
const id = this.toastIdCounter++;
this.toasts.push({ id, message, type });
// Auto-remove after 3 seconds
setTimeout(() => {
this.toasts = this.toasts.filter(t => t.id !== id);
}, 3000);
},
/* Heartbeat */
startHeartbeat() {
setInterval(() => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.sendWebSocketMessage({ type: 'ping' });
}
}, 30000); // Ping every 30 seconds
},
},
mounted() {
// Load initial data
this.loadDevices();
this.loadTasks();
// Connect WebSocket
this.connectWebSocket();
// Start heartbeat
this.startHeartbeat();
// Refresh devices periodically
setInterval(() => {
if (!this.refreshing) {
this.loadDevices();
}
}, 10000); // Every 10 seconds
// Refresh tasks periodically
setInterval(() => {
this.loadTasks();
}, 5000); // Every 5 seconds
},
beforeUnmount() {
// Close WebSocket on unmount
if (this.ws) {
this.ws.close();
}
},
}).mount('#app');