- Move core agent engine to packages/core/ - Add packages/types/ for shared TypeScript types - Add packages/utils/ for utility functions - Add apps/cli/ for command-line interface - Add apps/gateway/ for NestJS WebSocket gateway - Add apps/server/ for REST API server - Restructure desktop app (electron/ → src/main/, src/preload/) - Update pnpm workspace configuration - Remove legacy src/ directory Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
211 lines
9 KiB
HTML
211 lines
9 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Demo Client</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace; background: #0a0a0a; color: #e0e0e0; padding: 2rem; }
|
|
h1 { font-size: 1.4rem; margin-bottom: 0.3rem; color: #fff; }
|
|
.badge { display: inline-block; font-size: 0.7rem; background: #3a2a00; color: #ffaa33; border: 1px solid #554400; border-radius: 3px; padding: 0.1rem 0.4rem; margin-left: 0.5rem; vertical-align: middle; }
|
|
.subtitle { color: #555; font-size: 0.8rem; margin-bottom: 1.5rem; }
|
|
.card { background: #161616; border: 1px solid #2a2a2a; border-radius: 8px; padding: 1.2rem; margin-bottom: 1.2rem; }
|
|
label { display: block; color: #888; font-size: 0.8rem; margin-bottom: 0.3rem; }
|
|
input { width: 100%; background: #0e0e0e; border: 1px solid #333; border-radius: 4px; padding: 0.5rem 0.7rem; color: #e0e0e0; font-family: monospace; font-size: 0.85rem; margin-bottom: 0.8rem; }
|
|
input:focus { outline: none; border-color: #555; }
|
|
input:disabled { opacity: 0.5; }
|
|
button { background: #2a2a2a; color: #e0e0e0; border: 1px solid #444; border-radius: 4px; padding: 0.5rem 1rem; cursor: pointer; font-size: 0.85rem; }
|
|
button:hover { background: #3a3a3a; }
|
|
button:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
.btn-connect { background: #1a3a1a; border-color: #2a5a2a; }
|
|
.btn-connect:hover { background: #2a4a2a; }
|
|
.btn-disconnect { background: #3a1a1a; border-color: #5a2a2a; }
|
|
.btn-disconnect:hover { background: #4a2a2a; }
|
|
.btn-send { background: #1a2a3a; border-color: #2a4a5a; }
|
|
.btn-send:hover { background: #2a3a4a; }
|
|
.status { font-size: 0.8rem; margin-bottom: 0.8rem; }
|
|
.status .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 0.4rem; vertical-align: middle; }
|
|
.dot-off { background: #555; }
|
|
.dot-on { background: #4a4; }
|
|
.dot-connecting { background: #aa4; }
|
|
.device-id { font-size: 0.75rem; color: #555; margin-bottom: 1rem; word-break: break-all; }
|
|
.chat { display: none; }
|
|
.chat.active { display: block; }
|
|
.messages { background: #0e0e0e; border: 1px solid #222; border-radius: 4px; height: 300px; overflow-y: auto; padding: 0.6rem; margin-bottom: 0.8rem; font-size: 0.8rem; font-family: monospace; }
|
|
.messages .msg { padding: 0.3rem 0; border-bottom: 1px solid #1a1a1a; }
|
|
.messages .msg:last-child { border-bottom: none; }
|
|
.msg-from { color: #888; }
|
|
.msg-self { color: #6a8faa; }
|
|
.msg-in { color: #6aaa6a; }
|
|
.msg-system { color: #aa8833; font-style: italic; }
|
|
.send-row { display: flex; gap: 0.5rem; }
|
|
.send-row input { margin-bottom: 0; flex: 1; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Demo Client <span class="badge">DEMO</span></h1>
|
|
<p class="subtitle">Test client for sending messages to agents via Gateway</p>
|
|
|
|
<div class="card">
|
|
<div class="device-id">Device ID: <span id="device-id"></span></div>
|
|
<div class="status">
|
|
Status: <span class="dot dot-off" id="status-dot"></span><span id="status-text">Disconnected</span>
|
|
</div>
|
|
|
|
<div id="connect-form">
|
|
<label>Gateway URL</label>
|
|
<input type="text" id="gateway-url" placeholder="http://localhost:3000" />
|
|
<button class="btn-connect" id="btn-connect" onclick="doConnect()">Connect</button>
|
|
</div>
|
|
|
|
<div id="connected-form" style="display:none;">
|
|
<button class="btn-disconnect" onclick="doDisconnect()">Disconnect</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card chat" id="chat">
|
|
<label>Messages</label>
|
|
<div class="messages" id="messages"></div>
|
|
<div style="display:flex; gap:0.5rem; margin-bottom:0.5rem;">
|
|
<div style="flex:1;">
|
|
<label>Target Device ID</label>
|
|
<input type="text" id="target-device-id" placeholder="Hub device ID" />
|
|
</div>
|
|
<div style="flex:1;">
|
|
<label>Agent ID</label>
|
|
<input type="text" id="target-agent-id" placeholder="Agent ID on that device" />
|
|
</div>
|
|
</div>
|
|
<div class="send-row">
|
|
<input type="text" id="msg-input" placeholder="Type a message..." onkeydown="if(event.key==='Enter')doSend()" />
|
|
<button class="btn-send" onclick="doSend()">Send</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.socket.io/4.8.3/socket.io.min.js"></script>
|
|
<script>
|
|
// Device ID: persist in localStorage
|
|
const STORAGE_KEY = 'demo-client-device-id';
|
|
let deviceId = localStorage.getItem(STORAGE_KEY);
|
|
if (!deviceId) {
|
|
// Simple UUID v4 for demo (no uuid lib in browser)
|
|
deviceId = crypto.randomUUID();
|
|
localStorage.setItem(STORAGE_KEY, deviceId);
|
|
}
|
|
document.getElementById('device-id').textContent = deviceId;
|
|
|
|
// Restore last used values
|
|
document.getElementById('gateway-url').value = localStorage.getItem('demo-client-gateway-url') || 'http://localhost:3000';
|
|
|
|
let socket = null;
|
|
|
|
function setStatus(state) {
|
|
const dot = document.getElementById('status-dot');
|
|
const text = document.getElementById('status-text');
|
|
dot.className = 'dot ' + ({ disconnected: 'dot-off', connecting: 'dot-connecting', connected: 'dot-on', registered: 'dot-on' }[state] || 'dot-off');
|
|
text.textContent = state.charAt(0).toUpperCase() + state.slice(1);
|
|
}
|
|
|
|
function appendMsg(type, text) {
|
|
const el = document.getElementById('messages');
|
|
const div = document.createElement('div');
|
|
div.className = 'msg msg-' + type;
|
|
div.textContent = text;
|
|
el.appendChild(div);
|
|
el.scrollTop = el.scrollHeight;
|
|
}
|
|
|
|
function doConnect() {
|
|
const url = document.getElementById('gateway-url').value.trim();
|
|
if (!url) { alert('Please fill in Gateway URL'); return; }
|
|
|
|
localStorage.setItem('demo-client-gateway-url', url);
|
|
|
|
setStatus('connecting');
|
|
document.getElementById('btn-connect').disabled = true;
|
|
|
|
socket = io(url, {
|
|
path: '/ws',
|
|
query: { deviceId, deviceType: 'client' },
|
|
reconnection: true,
|
|
reconnectionDelay: 1000,
|
|
});
|
|
|
|
socket.on('connect', () => {
|
|
setStatus('connected');
|
|
// 服务端从 query 自动注册,等待 registered 事件
|
|
});
|
|
|
|
socket.on('registered', (res) => {
|
|
if (res.success) {
|
|
setStatus('registered');
|
|
document.getElementById('connect-form').style.display = 'none';
|
|
document.getElementById('connected-form').style.display = 'block';
|
|
document.getElementById('chat').classList.add('active');
|
|
document.getElementById('target-device-id').value = localStorage.getItem('demo-client-target-device-id') || '';
|
|
document.getElementById('target-agent-id').value = localStorage.getItem('demo-client-target-agent-id') || '';
|
|
appendMsg('system', `Connected as ${deviceId}`);
|
|
} else {
|
|
appendMsg('system', `Registration failed: ${res.error}`);
|
|
setStatus('disconnected');
|
|
document.getElementById('btn-connect').disabled = false;
|
|
}
|
|
});
|
|
|
|
socket.on('receive', (msg) => {
|
|
appendMsg('in', `[${msg.from}] ${typeof msg.payload === 'string' ? msg.payload : JSON.stringify(msg.payload)}`);
|
|
});
|
|
|
|
socket.on('send_error', (err) => {
|
|
appendMsg('system', `Send error: ${err.error} (${err.code})`);
|
|
});
|
|
|
|
socket.on('disconnect', (reason) => {
|
|
setStatus('disconnected');
|
|
appendMsg('system', `Disconnected: ${reason}`);
|
|
});
|
|
|
|
socket.on('connect_error', (err) => {
|
|
appendMsg('system', `Connection error: ${err.message}`);
|
|
setStatus('disconnected');
|
|
document.getElementById('btn-connect').disabled = false;
|
|
});
|
|
}
|
|
|
|
function doDisconnect() {
|
|
if (socket) { socket.disconnect(); socket = null; }
|
|
setStatus('disconnected');
|
|
document.getElementById('connect-form').style.display = 'block';
|
|
document.getElementById('connected-form').style.display = 'none';
|
|
document.getElementById('chat').classList.remove('active');
|
|
document.getElementById('btn-connect').disabled = false;
|
|
}
|
|
|
|
function doSend() {
|
|
const input = document.getElementById('msg-input');
|
|
const text = input.value.trim();
|
|
const targetDeviceId = document.getElementById('target-device-id').value.trim();
|
|
const targetAgentId = document.getElementById('target-agent-id').value.trim();
|
|
if (!text || !socket) return;
|
|
if (!targetDeviceId || !targetAgentId) { alert('Please fill in Target Device ID and Agent ID'); return; }
|
|
|
|
localStorage.setItem('demo-client-target-device-id', targetDeviceId);
|
|
localStorage.setItem('demo-client-target-agent-id', targetAgentId);
|
|
|
|
const msgId = crypto.randomUUID();
|
|
socket.emit('send', {
|
|
id: msgId,
|
|
uid: null,
|
|
from: deviceId,
|
|
to: targetDeviceId,
|
|
action: 'message',
|
|
payload: { agentId: targetAgentId, content: text },
|
|
});
|
|
|
|
appendMsg('self', `[you -> ${targetDeviceId}/${targetAgentId}] ${text}`);
|
|
input.value = '';
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|