fix: harden service websocket reconnect flows

Stabilize the service console and callback-host websocket paths so idle disconnects and mid-task client drops no longer wedge task execution or spam repeated commands.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
木炎
2026-04-09 10:34:34 +08:00
parent 96c3bf1dee
commit 57b9be733d
8 changed files with 353 additions and 55 deletions

View File

@@ -896,36 +896,66 @@ window.sgclawOnEval = sgclawOnEval;
window.callBackJsToCpp = callBackJsToCpp;
document.getElementById('wi').textContent = SGCLAW_BROWSER_WS_URL;
_log('Connecting to browser WebSocket\u2026');
const sgclawSocket = new WebSocket(SGCLAW_BROWSER_WS_URL);
sgclawSocket.addEventListener('open', async () => {{
document.getElementById('sd').classList.add('on');
document.getElementById('stx').textContent = 'Connected';
_log('<span class="ok">\u2713</span> WebSocket connected');
_task('Connected to browser');
sgclawSocket.send(JSON.stringify({{ type: 'register', role: 'web' }}));
await sgclawReady();
_log('<span class="ok">\u2713</span> Ready signal sent');
_task('Ready \u2014 waiting for commands');
}});
let sgclawSocket = null;
let sgclawReconnectTimer = null;
let sgclawDeferredCommandLogged = false;
sgclawSocket.addEventListener('close', () => {{
document.getElementById('sd').classList.remove('on');
document.getElementById('stx').textContent = 'Disconnected';
_log('<span class="er">\u2717</span> WebSocket disconnected');
_task('Disconnected');
}});
sgclawSocket.addEventListener('message', (event) => {{
console.debug('sgclaw helper received browser frame', event.data);
try {{
var data = String(event.data || '');
if (data.indexOf('@_@') !== -1) {{
sgclawEmitCallback('callBackJsToCpp', {{ raw: data }});
function connectSocket() {{
if (sgclawSocket && (sgclawSocket.readyState === WebSocket.OPEN || sgclawSocket.readyState === WebSocket.CONNECTING)) {{
return;
}}
_log('Connecting to browser WebSocket\u2026');
document.getElementById('stx').textContent = 'Connecting…';
_task('Connecting to browser');
const socket = new WebSocket(SGCLAW_BROWSER_WS_URL);
sgclawSocket = socket;
socket.addEventListener('open', async () => {{
if (sgclawSocket !== socket) {{
return;
}}
}} catch (_e) {{}}
}});
if (sgclawReconnectTimer) {{
clearTimeout(sgclawReconnectTimer);
sgclawReconnectTimer = null;
}}
sgclawDeferredCommandLogged = false;
document.getElementById('sd').classList.add('on');
document.getElementById('stx').textContent = 'Connected';
_log('<span class="ok">\u2713</span> WebSocket connected');
_task('Connected to browser');
socket.send(JSON.stringify({{ type: 'register', role: 'web' }}));
await sgclawReady();
_log('<span class="ok">\u2713</span> Ready signal sent');
_task('Ready \u2014 waiting for commands');
}});
socket.addEventListener('close', () => {{
if (sgclawSocket !== socket) {{
return;
}}
sgclawSocket = null;
document.getElementById('sd').classList.remove('on');
document.getElementById('stx').textContent = 'Disconnected';
_log('<span class="er">\u2717</span> WebSocket disconnected');
_task('Disconnected — reconnecting');
if (!sgclawReconnectTimer) {{
sgclawReconnectTimer = setTimeout(connectSocket, 1000);
}}
}});
socket.addEventListener('message', (event) => {{
if (sgclawSocket !== socket) {{
return;
}}
console.debug('sgclaw helper received browser frame', event.data);
try {{
var data = String(event.data || '');
if (data.indexOf('@_@') !== -1) {{
sgclawEmitCallback('callBackJsToCpp', {{ raw: data }});
}}
}} catch (_e) {{}}
}});
}}
async function sgclawPollCommands() {{
try {{
@@ -936,22 +966,29 @@ async function sgclawPollCommands() {{
const envelope = await response.json();
const command = envelope && envelope.command;
if (!command || !command.action) {{
sgclawDeferredCommandLogged = false;
return;
}}
if (!sgclawSocket || sgclawSocket.readyState !== WebSocket.OPEN) {{
if (!sgclawDeferredCommandLogged) {{
_log('<span class="er">!</span> Browser connection lost — command deferred');
sgclawDeferredCommandLogged = true;
}}
return;
}}
sgclawDeferredCommandLogged = false;
_nc++;
const args = Array.isArray(command.args) ? command.args : [];
_lastCmd=Date.now();_setIdle(false);
_log('<span class="a">\u2192</span> execute <span class="a">'+command.action+'</span>'+(args.length>1?' <span class="u">'+String(args[1]||'').substring(0,50)+'</span>':''));
_task('Executing: '+command.action);
if (sgclawSocket.readyState !== WebSocket.OPEN) {{
return;
}}
sgclawSocket.send(JSON.stringify([window.location.href || SGCLAW_HELPER_URL, command.action, ...args]));
await sgclawPostJson(SGCLAW_COMMAND_ACK_ENDPOINT, {{ type: 'command_ack' }});
}} catch (_error) {{
}}
}}
connectSocket();
setInterval(sgclawPollCommands, 250);
_log('sgClaw Runtime Console initialized');
</script>
@@ -1110,6 +1147,11 @@ mod tests {
assert!(html.contains("ws://127.0.0.1:12345"));
assert!(html.contains(r#"JSON.stringify({ type: 'register', role: 'web' })"#));
assert!(html.contains("sgclawReady"));
assert!(html.contains("connectSocket()"));
assert!(html.contains("setTimeout(connectSocket, 1000)"));
assert!(html.contains("if (!sgclawSocket || sgclawSocket.readyState !== WebSocket.OPEN)"));
assert!(html.contains("Browser connection lost — command deferred"));
assert!(html.contains("sgclawSocket = null;"));
assert!(html.contains("sgclawOnLoaded"));
assert!(html.contains("sgclawOnClickProbe"));
assert!(html.contains("sgclawOnClick"));