143 lines
6.1 KiB
JavaScript
143 lines
6.1 KiB
JavaScript
// GenericAgent Web2 browser bridge adapter.
|
|
// HTTP is the command/data channel. WebSocket only carries small state events.
|
|
(() => {
|
|
'use strict';
|
|
|
|
const listeners = new Map();
|
|
let ws = null;
|
|
let cachedBridgeReady = null;
|
|
const bridgeBase = `${location.protocol}//${location.hostname}:14168`;
|
|
const wsUrl = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.hostname}:14168/ws`;
|
|
|
|
function on(channel, cb) {
|
|
if (typeof cb !== 'function') return () => {};
|
|
if (!listeners.has(channel)) listeners.set(channel, new Set());
|
|
listeners.get(channel).add(cb);
|
|
if (channel === 'bridge-ready' && cachedBridgeReady) {
|
|
try { cb(cachedBridgeReady); } catch (err) { console.error('[ga-web2 listener] replay bridge-ready', err); }
|
|
}
|
|
return () => listeners.get(channel)?.delete(cb);
|
|
}
|
|
|
|
function emit(channel, payload) {
|
|
if (channel === 'bridge-ready') cachedBridgeReady = payload;
|
|
const set = listeners.get(channel);
|
|
if (!set) return;
|
|
for (const cb of Array.from(set)) {
|
|
try { cb(payload); } catch (err) { console.error('[ga-web2 listener]', channel, err); }
|
|
}
|
|
}
|
|
|
|
async function http(path, options = {}) {
|
|
const headers = Object.assign({}, options.headers || {});
|
|
const init = Object.assign({}, options, { headers });
|
|
if (init.body && typeof init.body !== 'string') {
|
|
headers['Content-Type'] = headers['Content-Type'] || 'application/json';
|
|
init.body = JSON.stringify(init.body);
|
|
}
|
|
const res = await fetch(`${bridgeBase}${path}`, init);
|
|
const text = await res.text();
|
|
let data = null;
|
|
try { data = text ? JSON.parse(text) : {}; } catch (_) { data = { raw: text }; }
|
|
if (!res.ok) {
|
|
const err = new Error((data && (data.error || data.message)) || `${res.status} ${res.statusText}`);
|
|
err.status = res.status;
|
|
err.data = data;
|
|
throw err;
|
|
}
|
|
return data;
|
|
}
|
|
|
|
function connectWs() {
|
|
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
|
|
try {
|
|
ws = new WebSocket(wsUrl);
|
|
ws.addEventListener('open', () => emit('bridge-log', 'WS state channel connected'));
|
|
ws.addEventListener('message', (ev) => {
|
|
let msg;
|
|
try { msg = JSON.parse(ev.data); } catch (_) { return; }
|
|
if (msg.type === 'bridge-ready') {
|
|
emit('bridge-ready', msg);
|
|
} else if (msg.type === 'session-state') {
|
|
emit('bridge-notification', msg);
|
|
} else if (msg.type === 'bridge-log') {
|
|
emit('bridge-log', msg.payload || msg);
|
|
} else if (msg.type === 'bridge-error') {
|
|
emit('bridge-error', msg.payload || msg);
|
|
}
|
|
});
|
|
ws.addEventListener('close', () => emit('bridge-closed', { reason: 'ws-closed' }));
|
|
ws.addEventListener('error', () => emit('bridge-error', { type: 'ws-error', message: 'WebSocket state channel error' }));
|
|
} catch (err) {
|
|
emit('bridge-error', { type: 'ws-error', message: err.message || String(err) });
|
|
}
|
|
}
|
|
|
|
async function rpc(method, params = {}) {
|
|
switch (method) {
|
|
case 'app/status':
|
|
return http('/status');
|
|
case 'app/config/get':
|
|
return http('/config');
|
|
case 'app/config/save':
|
|
return http('/config', { method: 'POST', body: params || {} });
|
|
case 'get/model-profiles':
|
|
return http('/model-profiles');
|
|
case 'session/new':
|
|
return http('/session/new', { method: 'POST', body: params || {} });
|
|
case 'session/prompt': {
|
|
const sid = params.sessionId || params.id || params.bridgeSessionId;
|
|
if (!sid) throw new Error('session/prompt missing sessionId');
|
|
return http(`/session/${encodeURIComponent(sid)}/prompt`, { method: 'POST', body: params || {} });
|
|
}
|
|
case 'session/poll': {
|
|
const sid = params.sessionId || params.id || params.bridgeSessionId;
|
|
if (!sid) throw new Error('session/poll missing sessionId');
|
|
const after = params.afterId ?? params.after ?? 0;
|
|
const limit = params.limit ?? 200;
|
|
return http(`/session/${encodeURIComponent(sid)}/messages?after=${encodeURIComponent(after)}&limit=${encodeURIComponent(limit)}`);
|
|
}
|
|
case 'session/cancel': {
|
|
const sid = params.sessionId || params.id || params.bridgeSessionId;
|
|
if (!sid) throw new Error('session/cancel missing sessionId');
|
|
return http(`/session/${encodeURIComponent(sid)}/cancel`, { method: 'POST', body: params || {} });
|
|
}
|
|
case 'app/path/open':
|
|
return http('/path/open', { method: 'POST', body: params || {} });
|
|
case 'app/path/selectGaRoot':
|
|
return http('/config');
|
|
case 'list_continuable_sessions':
|
|
return { sessions: [] };
|
|
case 'restore_session':
|
|
throw new Error('restore_session is not implemented in web2 bridge');
|
|
default:
|
|
throw new Error(`Unknown RPC method: ${method}`);
|
|
}
|
|
}
|
|
|
|
window.ga = {
|
|
platform: navigator.platform.toLowerCase().includes('mac') ? 'darwin' : 'win32',
|
|
startBridge: async () => { connectWs(); return http('/status'); },
|
|
stopBridge: async () => ({ ok: true }),
|
|
checkStatus: () => rpc('app/status', {}),
|
|
getConfig: () => rpc('app/config/get', {}),
|
|
saveConfig: (cfg) => rpc('app/config/save', cfg || {}),
|
|
getModelProfiles: () => rpc('get/model-profiles', {}),
|
|
selectGaRoot: () => rpc('app/path/selectGaRoot', {}),
|
|
openMykeyTemplate: () => rpc('app/path/open', { kind: 'mykeyTemplate' }),
|
|
openMykey: () => rpc('app/path/open', { kind: 'mykey' }),
|
|
pollSession: (sessionId, afterId = 0) => rpc('session/poll', { sessionId, afterId }),
|
|
rpc,
|
|
onBridgeMessage: (cb) => on('bridge-message', cb),
|
|
onBridgeNotification: (cb) => on('bridge-notification', cb),
|
|
onBridgeError: (cb) => on('bridge-error', cb),
|
|
onBridgeClosed: (cb) => on('bridge-closed', cb),
|
|
onBridgeReady: (cb) => on('bridge-ready', cb),
|
|
onBridgeLog: (cb) => on('bridge-log', cb),
|
|
onOpenSearch: (cb) => on('open-search', cb)
|
|
};
|
|
|
|
connectWs();
|
|
http('/status').then(status => emit('bridge-ready', status)).catch(err => emit('bridge-error', { type: 'http-error', message: err.message || String(err) }));
|
|
})();
|