mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-16 18:55:50 +08:00
feat(feishu): persistent session watches and completion pushes
- /watch resolves the target READ-ONLY: sessions are validated against registered workspace listings (current or any other) without binding the conversation or switching workspaces. /unwatch and /watchlist round out the command set; the watchlist card supports button and number-reply unwatching. - Watches persist in the state store (sessionId + title + chatId + lastSeq) and resume at runtime start: the bridge starts the global event-mux watcher in its constructor when the harness supports it. - Completion pushes fire on turn/end, deduped by sessionId + turn id, with the seq watermark persisted per watch. After a mux reconnect the bridge replays each watched session's recent history (session.history) through the normal handler, so missed turn/end events are compensated without duplicates. - Bound-session push targets are persisted (chatTargets) instead of living only in memory, and refresh on every accepted message. - Watch failures map to safe user-facing messages.
This commit is contained in:
parent
0579c24b13
commit
beb269dc0e
6 changed files with 746 additions and 132 deletions
|
|
@ -1223,6 +1223,85 @@ export class HarnessClient {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch the global Harness event mux (all sessions) until `signal`
|
||||
* aborts, reconnecting on drop. The Desktop host serves the mux as a
|
||||
* WebSocket downlink; frames are `server-request` envelopes whose payload
|
||||
* is a `session/event` — only those are forwarded. `onReconnect` (when
|
||||
* provided) fires after every (re)connection so callers can compensate
|
||||
* for events missed while offline.
|
||||
*/
|
||||
watchHarnessEvents({ signal, onSessionEvent, onReconnect }) {
|
||||
if (typeof onSessionEvent !== 'function') {
|
||||
return Promise.reject(new TypeError('watchHarnessEvents requires onSessionEvent'));
|
||||
}
|
||||
const url = new URL('/api/events.mux', this.#baseUrl);
|
||||
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
let stopped = false;
|
||||
const connect = () => {
|
||||
if (stopped || signal.aborted) return Promise.resolve();
|
||||
return new Promise((resolve) => {
|
||||
const socket = this.#createWebSocket(url.toString());
|
||||
const settle = () => {
|
||||
socket.removeEventListener('open', handleOpen);
|
||||
socket.removeEventListener('message', handleMessage);
|
||||
socket.removeEventListener('close', handleClose);
|
||||
socket.removeEventListener('error', handleError);
|
||||
signal.removeEventListener('abort', handleAbort);
|
||||
resolve();
|
||||
};
|
||||
const handleOpen = () => {
|
||||
try {
|
||||
onReconnect?.();
|
||||
} catch (error) {
|
||||
console.warn(`[${this.#logPrefix}] mux reconnect hook failed:`, error.message);
|
||||
}
|
||||
};
|
||||
const handleMessage = (event) => {
|
||||
try {
|
||||
if (typeof event.data !== 'string') return;
|
||||
const envelope = JSON.parse(event.data);
|
||||
const payload = envelope?.payload;
|
||||
if (envelope?.type !== 'server-request' || !payload || typeof payload !== 'object') return;
|
||||
if (payload.type !== 'session/event') return;
|
||||
if (typeof payload.sessionId !== 'string' || !payload.event || typeof payload.event !== 'object') return;
|
||||
onSessionEvent({ sessionId: payload.sessionId, event: payload.event });
|
||||
} catch (error) {
|
||||
console.warn(`[${this.#logPrefix}] ignored a malformed global mux frame:`, error.message);
|
||||
}
|
||||
};
|
||||
const handleClose = () => {
|
||||
settle();
|
||||
if (!stopped && !signal.aborted) {
|
||||
setTimeout(() => { void connect(); }, 2000);
|
||||
}
|
||||
};
|
||||
const handleError = () => {
|
||||
try {
|
||||
socket.close();
|
||||
} catch {
|
||||
// The close event drives reconnection; nothing to do here.
|
||||
}
|
||||
};
|
||||
const handleAbort = () => {
|
||||
stopped = true;
|
||||
try {
|
||||
socket.close();
|
||||
} catch {
|
||||
// Already closed.
|
||||
}
|
||||
};
|
||||
socket.addEventListener('open', handleOpen);
|
||||
socket.addEventListener('message', handleMessage);
|
||||
socket.addEventListener('close', handleClose, { once: true });
|
||||
socket.addEventListener('error', handleError, { once: true });
|
||||
signal.addEventListener('abort', handleAbort, { once: true });
|
||||
if (signal.aborted) handleAbort();
|
||||
});
|
||||
};
|
||||
return connect();
|
||||
}
|
||||
|
||||
stopManagedProcess() {
|
||||
if (this.#managedProcess?.exitCode === null) this.#managedProcess.kill('SIGTERM');
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue