Initial DSHWatch DSHWatch_Server scaffold

This commit is contained in:
2026-09-01 14:25:35 +08:00
commit 132b274768
8 changed files with 947 additions and 0 deletions
+257
View File
@@ -0,0 +1,257 @@
// DSHWatch relay broker
// Bridges DeepSeek Harness instances (DSH plugins) with phone/watch clients over WebSocket.
//
// Roles:
// dsh - the DeepSeek Harness plugin (one per userId)
// client - phone app (and through the phone app, the watch)
//
// Protocol is a tiny JSON envelope. See README.md.
import { createServer } from 'node:http';
import { WebSocketServer } from 'ws';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import process from 'node:process';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
function loadConfig() {
const configPath = process.env.DSHWATCH_SERVER_CONFIG || path.join(__dirname, 'config.json');
let config = {};
try {
config = JSON.parse(readFileSync(configPath, 'utf8'));
} catch {
// use defaults
}
return {
port: Number(config.port || process.env.PORT || 8787),
host: config.host || process.env.HOST || '0.0.0.0',
token: config.token || process.env.DSHWATCH_TOKEN || 'change-me',
heartbeatMs: Number(config.heartbeatMs || 30000)
};
}
const config = loadConfig();
const harnesses = new Map();
const clients = new Map();
const pending = new Map();
const lastStatus = new Map();
const wsUserId = new Map();
function bindUserId(ws, userId) { wsUserId.set(ws, userId); }
function userIdOf(ws) { return wsUserId.get(ws); }
function clientsFor(userId) {
let set = clients.get(userId);
if (!set) {
set = new Set();
clients.set(userId, set);
}
return set;
}
function send(ws, obj) {
if (ws.readyState === ws.OPEN) {
try { ws.send(JSON.stringify(obj)); } catch { /* ignore */ }
}
}
function broadcastToClients(userId, obj) {
for (const ws of clientsFor(userId)) send(ws, obj);
}
function replyError(clientWs, requestId, message, code) {
send(clientWs, { type: 'response', requestId, ok: false, error: { code: code || 'unreachable', message } });
}
function registerHarness(userId, ws) {
const prev = harnesses.get(userId);
if (prev && prev !== ws) {
send(prev, { type: 'notice', code: 'replaced', message: 'A newer harness connection took over this userId.' });
try { prev.close(); } catch {}
}
harnesses.set(userId, ws);
const status = lastStatus.get(userId) || null;
send(ws, { type: 'hello', role: 'dsh', online: true, status: status ? { userId: status.userId } : {} });
broadcastToClients(userId, { type: 'harness', online: true });
}
function unregisterHarness(userId, ws) {
if (harnesses.get(userId) === ws) {
harnesses.delete(userId);
broadcastToClients(userId, { type: 'harness', online: false });
}
}
function handleDshHello(userId, ws, msg) {
bindUserId(ws, userId);
if (msg.token !== config.token) {
send(ws, { type: 'auth-error', message: 'invalid token' });
ws.close();
return;
}
ws.role = 'dsh';
registerHarness(userId, ws);
}
function handleClientHello(userId, ws, msg) {
bindUserId(ws, userId);
if (msg.token !== config.token) {
send(ws, { type: 'auth-error', message: 'invalid token' });
ws.close();
return;
}
ws.role = 'client';
clientsFor(userId).add(ws);
const harness = harnesses.get(userId);
const status = lastStatus.get(userId) || null;
send(ws, {
type: 'hello',
role: 'client',
online: Boolean(harness),
harness: Boolean(harness),
status
});
}
function handleHarnessMessage(userId, ws, msg) {
if (msg.type === 'response') {
const req = pending.get(msg.requestId);
if (req) {
clearTimeout(req.timer);
pending.delete(msg.requestId);
send(req.clientWs, {
type: 'response',
requestId: req.clientRequestId,
ok: msg.ok !== false,
result: msg.result,
error: msg.error
});
}
} else if (msg.type === 'event') {
broadcastToClients(userId, { type: 'event', event: msg.event });
} else if (msg.type === 'status') {
lastStatus.set(userId, msg.status || null);
broadcastToClients(userId, { type: 'status', status: msg.status });
} else if (msg.type === 'ping') {
send(ws, { type: 'pong' });
}
}
function handleClientMessage(userId, ws, msg) {
if (msg.type === 'request') {
const harness = harnesses.get(userId);
if (!harness) {
replyError(ws, msg.requestId, 'harness offline');
return;
}
const requestId = msg.requestId || ('req-' + Date.now() + '-' + Math.random().toString(16).slice(2));
if (pending.has(requestId)) {
replyError(ws, msg.requestId, 'duplicate requestId', 'bad-request');
return;
}
const timer = setTimeout(() => {
pending.delete(requestId);
replyError(ws, msg.requestId, 'harness timeout');
}, Number(msg.timeoutMs) || 30000);
pending.set(requestId, { userId, clientWs: ws, clientRequestId: msg.requestId, timer });
send(harness, { type: 'request', requestId, method: msg.method, params: msg.params || {} });
} else if (msg.type === 'ping') {
send(ws, { type: 'pong' });
}
}
function handleMessage(ws, raw) {
let msg;
try { msg = JSON.parse(String(raw)); } catch {
send(ws, { type: 'error', message: 'invalid JSON' });
return;
}
if (!msg || typeof msg !== 'object') return;
const userId = msg.userId;
if (typeof userId !== 'string' || userId.length === 0) {
send(ws, { type: 'auth-error', message: 'missing userId' });
ws.close();
return;
}
if (msg.type === 'hello' && !ws.role) {
if (msg.role === 'dsh') handleDshHello(userId, ws, msg);
else handleClientHello(userId, ws, msg);
return;
}
if (ws.role === 'dsh') {
handleHarnessMessage(userId, ws, msg);
} else if (ws.role === 'client') {
handleClientMessage(userId, ws, msg);
} else {
send(ws, { type: 'auth-error', message: 'hello required' });
ws.close();
}
}
function handleConnection(ws, req) {
const url = new URL(req.url || '/', 'http://localhost');
if (url.pathname !== '/ws' && url.pathname !== '/') {
ws.close(4001, 'unknown path');
return;
}
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
ws.on('message', (raw) => {
try { handleMessage(ws, raw); } catch (err) {
send(ws, { type: 'error', message: String(err && err.message || err) });
ws.close();
}
});
ws.on('close', () => {
const userId = userIdOf(ws);
if (userId) {
if (ws.role === 'dsh') unregisterHarness(userId, ws);
else {
const set = clients.get(userId);
if (set) { set.delete(ws); if (set.size === 0) clients.delete(userId); }
}
for (const [id, req] of pending) {
if (req.clientWs === ws) {
clearTimeout(req.timer);
pending.delete(id);
}
}
}
wsUserId.delete(ws);
});
ws.on('error', () => {});
}
const server = createServer((req, res) => {
const url = new URL(req.url || '/', 'http://localhost');
if (url.pathname === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
ok: true,
harnesses: Array.from(harnesses.entries()).map(([userId]) => ({ userId })),
clients: Array.from(clients.entries()).map(([userId, set]) => ({ userId, count: set.size }))
}));
return;
}
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('DSHWatch relay broker running. Connect clients to /ws\n');
});
const wss = new WebSocketServer({ server });
wss.on('connection', handleConnection);
setInterval(() => {
for (const ws of wss.clients) {
if (ws.isAlive === false) { ws.terminate(); continue; }
ws.isAlive = false;
ws.ping();
}
}, config.heartbeatMs);
server.listen(config.port, config.host, () => {
console.log(String('[dshwatch-server] listening on ws://' + config.host + ':' + config.port + '/ws'));
console.log(String('[dshwatch-server] health: http://' + config.host + ':' + config.port + '/health'));
});