synthos 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +215 -65
- package/default-pages/application.json +1 -0
- package/default-pages/json_tools.json +1 -1
- package/default-pages/oregon_trail.html +321 -0
- package/default-pages/oregon_trail.json +12 -0
- package/default-pages/sidebar_page.json +1 -0
- package/default-pages/solar_explorer.html +10 -18
- package/default-pages/solar_explorer.json +2 -2
- package/default-pages/two-panel_page.json +1 -0
- package/default-pages/us_map.html +192 -0
- package/default-pages/us_map.json +12 -0
- package/default-pages/us_map_1850.html +325 -0
- package/default-pages/us_map_1850.json +12 -0
- package/default-pages/western_cities_1850.html +526 -0
- package/default-pages/western_cities_1850.json +12 -0
- package/default-themes/{nebula-dawn.css → nebula-dawn.v2.css} +24 -0
- package/default-themes/{nebula-dusk.css → nebula-dusk.v2.css} +24 -0
- package/dist/agents/a2a/a2aProvider.d.ts +3 -0
- package/dist/agents/a2a/a2aProvider.d.ts.map +1 -0
- package/dist/agents/a2a/a2aProvider.js +126 -0
- package/dist/agents/a2a/a2aProvider.js.map +1 -0
- package/dist/agents/discovery.d.ts +30 -0
- package/dist/agents/discovery.d.ts.map +1 -0
- package/dist/agents/discovery.js +52 -0
- package/dist/agents/discovery.js.map +1 -0
- package/dist/agents/index.d.ts +7 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +19 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/openclaw/gatewayManager.d.ts +113 -0
- package/dist/agents/openclaw/gatewayManager.d.ts.map +1 -0
- package/dist/agents/openclaw/gatewayManager.js +470 -0
- package/dist/agents/openclaw/gatewayManager.js.map +1 -0
- package/dist/agents/openclaw/openclawProvider.d.ts +3 -0
- package/dist/agents/openclaw/openclawProvider.d.ts.map +1 -0
- package/dist/agents/openclaw/openclawProvider.js +239 -0
- package/dist/agents/openclaw/openclawProvider.js.map +1 -0
- package/dist/agents/openclaw/sshTunnelManager.d.ts +23 -0
- package/dist/agents/openclaw/sshTunnelManager.d.ts.map +1 -0
- package/dist/agents/openclaw/sshTunnelManager.js +340 -0
- package/dist/agents/openclaw/sshTunnelManager.js.map +1 -0
- package/dist/agents/types.d.ts +64 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +6 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/connectors/airtable/connector.json +27 -0
- package/dist/connectors/alpha-vantage/connector.json +26 -0
- package/dist/connectors/brave-search/connector.json +26 -0
- package/dist/connectors/cloudinary/connector.json +27 -0
- package/dist/connectors/deepl/connector.json +28 -0
- package/dist/connectors/elevenlabs/connector.json +30 -0
- package/dist/connectors/giphy/connector.json +27 -0
- package/dist/connectors/github/connector.json +29 -0
- package/dist/connectors/huggingface/connector.json +27 -0
- package/dist/connectors/imgur/connector.json +29 -0
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.d.ts.map +1 -1
- package/dist/connectors/instagram/connector.json +43 -0
- package/dist/connectors/jira/connector.json +28 -0
- package/dist/connectors/mapbox/connector.json +26 -0
- package/dist/connectors/nasa/connector.json +27 -0
- package/dist/connectors/newsapi/connector.json +27 -0
- package/dist/connectors/notion/connector.json +28 -0
- package/dist/connectors/open-exchange-rates/connector.json +27 -0
- package/dist/connectors/openweathermap/connector.json +26 -0
- package/dist/connectors/pexels/connector.json +27 -0
- package/dist/connectors/registry.d.ts.map +1 -1
- package/dist/connectors/registry.js +42 -96
- package/dist/connectors/registry.js.map +1 -1
- package/dist/connectors/resend/connector.json +29 -0
- package/dist/connectors/rss2json/connector.json +27 -0
- package/dist/connectors/sendgrid/connector.json +27 -0
- package/dist/connectors/spoonacular/connector.json +28 -0
- package/dist/connectors/stability-ai/connector.json +27 -0
- package/dist/connectors/twilio/connector.json +28 -0
- package/dist/connectors/types.d.ts +23 -0
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/connectors/unsplash/connector.json +27 -0
- package/dist/connectors/wolfram-alpha/connector.json +26 -0
- package/dist/connectors/youtube-data/connector.json +30 -0
- package/dist/files.d.ts +1 -0
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +16 -1
- package/dist/files.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +28 -0
- package/dist/init.js.map +1 -1
- package/dist/migrations.d.ts +3 -2
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +122 -138
- package/dist/migrations.js.map +1 -1
- package/dist/models/anthropic.d.ts +22 -0
- package/dist/models/anthropic.d.ts.map +1 -0
- package/dist/models/anthropic.js +76 -0
- package/dist/models/anthropic.js.map +1 -0
- package/dist/models/chainOfThought.d.ts +12 -0
- package/dist/models/chainOfThought.d.ts.map +1 -0
- package/dist/models/chainOfThought.js +45 -0
- package/dist/models/chainOfThought.js.map +1 -0
- package/dist/models/fireworksai.d.ts +30 -0
- package/dist/models/fireworksai.d.ts.map +1 -0
- package/dist/models/fireworksai.js +133 -0
- package/dist/models/fireworksai.js.map +1 -0
- package/dist/models/index.d.ts +7 -1
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/index.js +19 -1
- package/dist/models/index.js.map +1 -1
- package/dist/models/logCompletePrompt.d.ts +3 -0
- package/dist/models/logCompletePrompt.d.ts.map +1 -0
- package/dist/models/logCompletePrompt.js +23 -0
- package/dist/models/logCompletePrompt.js.map +1 -0
- package/dist/models/openai.d.ts +24 -0
- package/dist/models/openai.d.ts.map +1 -0
- package/dist/models/openai.js +80 -0
- package/dist/models/openai.js.map +1 -0
- package/dist/models/providers.d.ts +1 -0
- package/dist/models/providers.d.ts.map +1 -1
- package/dist/models/providers.js +12 -4
- package/dist/models/providers.js.map +1 -1
- package/dist/models/types.d.ts +34 -2
- package/dist/models/types.d.ts.map +1 -1
- package/dist/models/types.js +16 -0
- package/dist/models/types.js.map +1 -1
- package/dist/models/utils.d.ts +6 -0
- package/dist/models/utils.d.ts.map +1 -0
- package/dist/models/utils.js +21 -0
- package/dist/models/utils.js.map +1 -0
- package/dist/scripts.d.ts +2 -1
- package/dist/scripts.d.ts.map +1 -1
- package/dist/scripts.js +4 -3
- package/dist/scripts.js.map +1 -1
- package/dist/service/createCompletePrompt.d.ts +1 -1
- package/dist/service/createCompletePrompt.d.ts.map +1 -1
- package/dist/service/createCompletePrompt.js +9 -6
- package/dist/service/createCompletePrompt.js.map +1 -1
- package/dist/service/generateImage.d.ts +1 -1
- package/dist/service/generateImage.d.ts.map +1 -1
- package/dist/service/generateImage.js +3 -3
- package/dist/service/generateImage.js.map +1 -1
- package/dist/service/server.d.ts.map +1 -1
- package/dist/service/server.js +3 -0
- package/dist/service/server.js.map +1 -1
- package/dist/service/transformPage.d.ts +4 -2
- package/dist/service/transformPage.d.ts.map +1 -1
- package/dist/service/transformPage.js +74 -6
- package/dist/service/transformPage.js.map +1 -1
- package/dist/service/useAgentRoutes.d.ts +4 -0
- package/dist/service/useAgentRoutes.d.ts.map +1 -0
- package/dist/service/useAgentRoutes.js +389 -0
- package/dist/service/useAgentRoutes.js.map +1 -0
- package/dist/service/useApiRoutes.d.ts.map +1 -1
- package/dist/service/useApiRoutes.js +157 -16
- package/dist/service/useApiRoutes.js.map +1 -1
- package/dist/service/useConnectorRoutes.d.ts.map +1 -1
- package/dist/service/useConnectorRoutes.js +14 -3
- package/dist/service/useConnectorRoutes.js.map +1 -1
- package/dist/service/useGatewayRoutes.d.ts +4 -0
- package/dist/service/useGatewayRoutes.d.ts.map +1 -0
- package/dist/service/useGatewayRoutes.js +168 -0
- package/dist/service/useGatewayRoutes.js.map +1 -0
- package/dist/service/usePageRoutes.d.ts.map +1 -1
- package/dist/service/usePageRoutes.js +16 -5
- package/dist/service/usePageRoutes.js.map +1 -1
- package/dist/settings.d.ts +2 -1
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +4 -8
- package/dist/settings.js.map +1 -1
- package/dist/themes.d.ts +14 -0
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +86 -13
- package/dist/themes.js.map +1 -1
- package/package.json +10 -5
- package/page-scripts/helpers-v2.js +222 -0
- package/page-scripts/page-v2.js +656 -0
- package/required-pages/builder.html +1 -27
- package/required-pages/pages.html +745 -22
- package/required-pages/settings.html +819 -21
- package/required-pages/synthos_apis.html +56 -1
- package/src/agents/a2a/a2aProvider.ts +110 -0
- package/src/agents/discovery.ts +74 -0
- package/src/agents/index.ts +6 -0
- package/src/agents/openclaw/gatewayManager.ts +559 -0
- package/src/agents/openclaw/openclawProvider.ts +261 -0
- package/src/agents/openclaw/sshTunnelManager.ts +385 -0
- package/src/agents/types.ts +82 -0
- package/src/connectors/airtable/connector.json +27 -0
- package/src/connectors/alpha-vantage/connector.json +26 -0
- package/src/connectors/brave-search/connector.json +26 -0
- package/src/connectors/cloudinary/connector.json +27 -0
- package/src/connectors/deepl/connector.json +28 -0
- package/src/connectors/elevenlabs/connector.json +30 -0
- package/src/connectors/giphy/connector.json +27 -0
- package/src/connectors/github/connector.json +29 -0
- package/src/connectors/huggingface/connector.json +27 -0
- package/src/connectors/imgur/connector.json +29 -0
- package/src/connectors/index.ts +2 -0
- package/src/connectors/instagram/connector.json +43 -0
- package/src/connectors/jira/connector.json +28 -0
- package/src/connectors/mapbox/connector.json +26 -0
- package/src/connectors/nasa/connector.json +27 -0
- package/src/connectors/newsapi/connector.json +27 -0
- package/src/connectors/notion/connector.json +28 -0
- package/src/connectors/open-exchange-rates/connector.json +27 -0
- package/src/connectors/openweathermap/connector.json +26 -0
- package/src/connectors/pexels/connector.json +27 -0
- package/src/connectors/registry.ts +21 -97
- package/src/connectors/resend/connector.json +29 -0
- package/src/connectors/rss2json/connector.json +27 -0
- package/src/connectors/sendgrid/connector.json +27 -0
- package/src/connectors/spoonacular/connector.json +28 -0
- package/src/connectors/stability-ai/connector.json +27 -0
- package/src/connectors/twilio/connector.json +28 -0
- package/src/connectors/types.ts +25 -0
- package/src/connectors/unsplash/connector.json +27 -0
- package/src/connectors/wolfram-alpha/connector.json +26 -0
- package/src/connectors/youtube-data/connector.json +30 -0
- package/src/files.ts +14 -0
- package/src/init.ts +27 -0
- package/src/migrations.ts +121 -138
- package/src/models/anthropic.ts +89 -0
- package/src/models/chainOfThought.ts +56 -0
- package/src/models/fireworksai.ts +136 -0
- package/src/models/index.ts +7 -1
- package/src/models/logCompletePrompt.ts +25 -0
- package/src/models/openai.ts +90 -0
- package/src/models/providers.ts +12 -3
- package/src/models/types.ts +67 -2
- package/src/models/utils.ts +16 -0
- package/src/scripts.ts +2 -2
- package/src/service/createCompletePrompt.ts +3 -1
- package/src/service/generateImage.ts +2 -2
- package/src/service/server.ts +4 -0
- package/src/service/transformPage.ts +81 -8
- package/src/service/useAgentRoutes.ts +423 -0
- package/src/service/useApiRoutes.ts +173 -18
- package/src/service/useConnectorRoutes.ts +14 -3
- package/src/service/usePageRoutes.ts +20 -6
- package/src/settings.ts +6 -10
- package/src/themes.ts +84 -12
- package/tests/README.md +12 -0
- package/tests/anthropic.spec.ts +84 -0
- package/tests/chainOfThought.spec.ts +108 -0
- package/tests/ensureScripts.spec.ts +82 -0
- package/tests/files.spec.ts +233 -0
- package/tests/fireworksai.spec.ts +92 -0
- package/tests/logCompletePrompt.spec.ts +74 -0
- package/tests/migrations.spec.ts +169 -0
- package/tests/openai.spec.ts +71 -0
- package/tests/pages.spec.ts +328 -0
- package/tests/providers.spec.ts +144 -0
- package/tests/scripts.spec.ts +209 -0
- package/tests/transformPage.spec.ts +931 -0
- package/tests/types.spec.ts +23 -0
- package/default-pages/app_builder.json +0 -1
- package/default-pages/sidebar_builder.json +0 -1
- package/default-pages/two-panel_builder.json +0 -1
- package/images/home.png +0 -0
- package/images/page-management.png +0 -0
- package/images/settings.png +0 -0
- package/images/synthos-square.png +0 -0
- /package/default-pages/{app_builder.html → application.html} +0 -0
- /package/default-pages/{sidebar_builder.html → sidebar_page.html} +0 -0
- /package/default-pages/{two-panel_builder.html → two-panel_page.html} +0 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
import { startTunnel, stopTunnel, SshTunnelConfig } from './sshTunnelManager';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Types
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
/** Internal gateway config for WebSocket connections (not exported). */
|
|
8
|
+
export interface GatewayConfig {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
url: string;
|
|
12
|
+
token: string;
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
role: 'operator';
|
|
15
|
+
scopes: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface PendingRequest {
|
|
19
|
+
resolve: (payload: unknown) => void;
|
|
20
|
+
reject: (err: Error) => void;
|
|
21
|
+
timer: ReturnType<typeof setTimeout>;
|
|
22
|
+
/** When true, skip resolution on intermediate { status: "accepted" } responses. */
|
|
23
|
+
expectFinal?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface GatewayConnection {
|
|
27
|
+
gatewayId: string;
|
|
28
|
+
ws: WebSocket | null;
|
|
29
|
+
config: GatewayConfig;
|
|
30
|
+
connected: boolean;
|
|
31
|
+
authenticated: boolean;
|
|
32
|
+
/** Granted scopes and role from the hello-ok response */
|
|
33
|
+
grantedAuth?: { role: string; scopes: string[]; deviceToken?: string };
|
|
34
|
+
pendingRequests: Map<string, PendingRequest>;
|
|
35
|
+
eventListeners: Map<string, Array<(data: unknown) => void>>;
|
|
36
|
+
lastTick: number;
|
|
37
|
+
tickIntervalMs: number;
|
|
38
|
+
tickTimer: ReturnType<typeof setInterval> | null;
|
|
39
|
+
reconnectTimer: ReturnType<typeof setTimeout> | null;
|
|
40
|
+
reconnectAttempts: number;
|
|
41
|
+
/** Set to true when disconnect() is called explicitly — suppresses auto-reconnect */
|
|
42
|
+
intentionalClose: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const RPC_TIMEOUT_MS = 30_000;
|
|
46
|
+
const AUTH_TIMEOUT_MS = 15_000;
|
|
47
|
+
const MAX_RECONNECT_DELAY_MS = 60_000;
|
|
48
|
+
const BASE_RECONNECT_DELAY_MS = 1_000;
|
|
49
|
+
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Module-level connection pool
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
const connections = new Map<string, GatewayConnection>();
|
|
55
|
+
|
|
56
|
+
let _nextId = 1;
|
|
57
|
+
function nextRequestId(): string {
|
|
58
|
+
return `req-${_nextId++}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Public API
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Connect to an OpenClaw gateway. Resolves once the auth handshake completes.
|
|
67
|
+
*
|
|
68
|
+
* Protocol (from https://docs.openclaw.ai/gateway/protocol):
|
|
69
|
+
* 1. Client opens WebSocket
|
|
70
|
+
* 2. Server sends {type:"event", event:"connect.challenge", payload:{nonce,ts}}
|
|
71
|
+
* 3. Client sends {type:"req", id, method:"connect", params:{auth:{token}, role, scopes, ...}}
|
|
72
|
+
* 4. Server sends {type:"res", id, ok:true, payload:{type:"hello-ok", policy:{tickIntervalMs}, auth:{...}}}
|
|
73
|
+
*/
|
|
74
|
+
export async function connect(config: GatewayConfig): Promise<GatewayConnection> {
|
|
75
|
+
const existing = connections.get(config.id);
|
|
76
|
+
if (existing?.connected && existing.authenticated) {
|
|
77
|
+
return existing;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const conn: GatewayConnection = existing ?? {
|
|
81
|
+
gatewayId: config.id,
|
|
82
|
+
ws: null,
|
|
83
|
+
config,
|
|
84
|
+
connected: false,
|
|
85
|
+
authenticated: false,
|
|
86
|
+
pendingRequests: new Map(),
|
|
87
|
+
eventListeners: new Map(),
|
|
88
|
+
lastTick: Date.now(),
|
|
89
|
+
tickIntervalMs: 0,
|
|
90
|
+
tickTimer: null,
|
|
91
|
+
reconnectTimer: null,
|
|
92
|
+
reconnectAttempts: 0,
|
|
93
|
+
intentionalClose: false,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Update config in case it changed
|
|
97
|
+
conn.config = config;
|
|
98
|
+
conn.intentionalClose = false;
|
|
99
|
+
connections.set(config.id, conn);
|
|
100
|
+
|
|
101
|
+
return new Promise<GatewayConnection>((resolve, reject) => {
|
|
102
|
+
let settled = false;
|
|
103
|
+
|
|
104
|
+
function settle(err?: Error): void {
|
|
105
|
+
if (settled) return;
|
|
106
|
+
settled = true;
|
|
107
|
+
clearTimeout(authTimeout);
|
|
108
|
+
if (err) reject(err);
|
|
109
|
+
else resolve(conn);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const authTimeout = setTimeout(() => {
|
|
113
|
+
settle(new Error(`Gateway auth timed out after ${AUTH_TIMEOUT_MS}ms: ${config.url}`));
|
|
114
|
+
conn.ws?.close();
|
|
115
|
+
}, AUTH_TIMEOUT_MS);
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const ws = new WebSocket(config.url);
|
|
119
|
+
conn.ws = ws;
|
|
120
|
+
|
|
121
|
+
ws.addEventListener('open', () => {
|
|
122
|
+
conn.connected = true;
|
|
123
|
+
conn.reconnectAttempts = 0;
|
|
124
|
+
// Wait for connect.challenge event from server
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
ws.addEventListener('message', (rawEvent) => {
|
|
128
|
+
const data = typeof rawEvent.data === 'string' ? rawEvent.data : rawEvent.data.toString();
|
|
129
|
+
let msg: Record<string, unknown>;
|
|
130
|
+
try {
|
|
131
|
+
msg = JSON.parse(data);
|
|
132
|
+
} catch {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
handleMessage(conn, msg, () => settle());
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
ws.addEventListener('close', (ev) => {
|
|
140
|
+
const wasAuthenticated = conn.authenticated;
|
|
141
|
+
conn.connected = false;
|
|
142
|
+
conn.authenticated = false;
|
|
143
|
+
stopTickWatch(conn);
|
|
144
|
+
|
|
145
|
+
// Reject all pending requests
|
|
146
|
+
for (const [, pending] of conn.pendingRequests) {
|
|
147
|
+
clearTimeout(pending.timer);
|
|
148
|
+
pending.reject(new Error('Gateway connection closed'));
|
|
149
|
+
}
|
|
150
|
+
conn.pendingRequests.clear();
|
|
151
|
+
|
|
152
|
+
// If we never finished auth, reject the connect promise
|
|
153
|
+
settle(new Error(`Gateway WebSocket closed (code ${ev.code}) before auth completed`));
|
|
154
|
+
|
|
155
|
+
// Auto-reconnect only if: was previously authenticated, not intentionally closed, and config is enabled
|
|
156
|
+
if (wasAuthenticated && !conn.intentionalClose && config.enabled) {
|
|
157
|
+
scheduleReconnect(conn);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
ws.addEventListener('error', () => {
|
|
162
|
+
// The close event will fire after this, so we don't settle here.
|
|
163
|
+
// Just log for diagnostics.
|
|
164
|
+
console.error(`[OpenClaw] WebSocket error on gateway ${config.name}`);
|
|
165
|
+
});
|
|
166
|
+
} catch (err) {
|
|
167
|
+
settle(err instanceof Error ? err : new Error(String(err)));
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Disconnect from a gateway.
|
|
174
|
+
*/
|
|
175
|
+
export function disconnect(gatewayId: string): void {
|
|
176
|
+
const conn = connections.get(gatewayId);
|
|
177
|
+
if (!conn) return;
|
|
178
|
+
|
|
179
|
+
conn.intentionalClose = true;
|
|
180
|
+
|
|
181
|
+
if (conn.reconnectTimer) {
|
|
182
|
+
clearTimeout(conn.reconnectTimer);
|
|
183
|
+
conn.reconnectTimer = null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
stopTickWatch(conn);
|
|
187
|
+
|
|
188
|
+
if (conn.ws) {
|
|
189
|
+
conn.ws.close();
|
|
190
|
+
conn.ws = null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
conn.connected = false;
|
|
194
|
+
conn.authenticated = false;
|
|
195
|
+
connections.delete(gatewayId);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get an existing connection (connects on demand if not yet connected).
|
|
200
|
+
*/
|
|
201
|
+
export async function getConnection(config: GatewayConfig): Promise<GatewayConnection> {
|
|
202
|
+
const existing = connections.get(config.id);
|
|
203
|
+
if (existing?.connected && existing.authenticated) {
|
|
204
|
+
return existing;
|
|
205
|
+
}
|
|
206
|
+
return connect(config);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get connection status without connecting.
|
|
211
|
+
*/
|
|
212
|
+
export function getConnectionStatus(gatewayId: string): { connected: boolean; authenticated: boolean } {
|
|
213
|
+
const conn = connections.get(gatewayId);
|
|
214
|
+
return {
|
|
215
|
+
connected: conn?.connected ?? false,
|
|
216
|
+
authenticated: conn?.authenticated ?? false,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Send an RPC request to the gateway and wait for the correlated response.
|
|
222
|
+
*/
|
|
223
|
+
export function request(conn: GatewayConnection, method: string, params?: unknown, opts?: { expectFinal?: boolean }): Promise<unknown> {
|
|
224
|
+
return new Promise<unknown>((resolve, reject) => {
|
|
225
|
+
if (!conn.ws || !conn.connected || !conn.authenticated) {
|
|
226
|
+
reject(new Error('Gateway not connected or not authenticated'));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const id = nextRequestId();
|
|
231
|
+
|
|
232
|
+
const timer = setTimeout(() => {
|
|
233
|
+
conn.pendingRequests.delete(id);
|
|
234
|
+
reject(new Error(`RPC timeout for method "${method}" (id: ${id})`));
|
|
235
|
+
}, RPC_TIMEOUT_MS);
|
|
236
|
+
|
|
237
|
+
conn.pendingRequests.set(id, { resolve, reject, timer, expectFinal: opts?.expectFinal });
|
|
238
|
+
|
|
239
|
+
const msg: Record<string, unknown> = { type: 'req', id, method };
|
|
240
|
+
if (params !== undefined) {
|
|
241
|
+
msg.params = params;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
console.log(`[OpenClaw] → req: ${id} ${method}`);
|
|
245
|
+
conn.ws.send(JSON.stringify(msg));
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Subscribe to gateway events by event type.
|
|
251
|
+
*/
|
|
252
|
+
export function onEvent(conn: GatewayConnection, eventType: string, listener: (data: unknown) => void): void {
|
|
253
|
+
if (!conn.eventListeners.has(eventType)) {
|
|
254
|
+
conn.eventListeners.set(eventType, []);
|
|
255
|
+
}
|
|
256
|
+
conn.eventListeners.get(eventType)!.push(listener);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Remove a specific event listener.
|
|
261
|
+
*/
|
|
262
|
+
export function offEvent(conn: GatewayConnection, eventType: string, listener: (data: unknown) => void): void {
|
|
263
|
+
const listeners = conn.eventListeners.get(eventType);
|
|
264
|
+
if (!listeners) return;
|
|
265
|
+
const idx = listeners.indexOf(listener);
|
|
266
|
+
if (idx !== -1) listeners.splice(idx, 1);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ---------------------------------------------------------------------------
|
|
270
|
+
// Agent-level helpers (bridge from AgentConfig to internal GatewayConfig)
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Convert an http(s) URL to a ws(s) URL for WebSocket connections.
|
|
275
|
+
*/
|
|
276
|
+
function wsUrl(url: string): string {
|
|
277
|
+
return url
|
|
278
|
+
.replace(/^https:\/\//, 'wss://')
|
|
279
|
+
.replace(/^http:\/\//, 'ws://')
|
|
280
|
+
.replace(/\/+$/, '');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Connect an OpenClaw agent via WebSocket.
|
|
285
|
+
* Converts the agent's HTTP URL to a WS URL and uses default operator role/scopes.
|
|
286
|
+
*/
|
|
287
|
+
export async function connectAgent(agent: {
|
|
288
|
+
id: string;
|
|
289
|
+
name: string;
|
|
290
|
+
url: string;
|
|
291
|
+
token: string;
|
|
292
|
+
sshTunnel?: { enabled: boolean; command: string; password: string };
|
|
293
|
+
}): Promise<GatewayConnection> {
|
|
294
|
+
// Start SSH tunnel first if configured
|
|
295
|
+
if (agent.sshTunnel?.enabled && agent.sshTunnel.command) {
|
|
296
|
+
const tunnelConfig: SshTunnelConfig = {
|
|
297
|
+
command: agent.sshTunnel.command,
|
|
298
|
+
password: agent.sshTunnel.password,
|
|
299
|
+
};
|
|
300
|
+
await startTunnel(agent.id, tunnelConfig);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const gwConfig: GatewayConfig = {
|
|
304
|
+
id: agent.id,
|
|
305
|
+
name: agent.name,
|
|
306
|
+
url: wsUrl(agent.url),
|
|
307
|
+
token: agent.token,
|
|
308
|
+
enabled: true,
|
|
309
|
+
role: 'operator',
|
|
310
|
+
scopes: ['operator.read', 'operator.write', 'operator.approvals'],
|
|
311
|
+
};
|
|
312
|
+
return connect(gwConfig);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Disconnect an agent's WebSocket connection.
|
|
317
|
+
*/
|
|
318
|
+
export function disconnectAgent(agentId: string): void {
|
|
319
|
+
disconnect(agentId);
|
|
320
|
+
stopTunnel(agentId);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Get an agent's WebSocket connection status without connecting.
|
|
325
|
+
*/
|
|
326
|
+
export function getAgentStatus(agentId: string): { connected: boolean; authenticated: boolean } {
|
|
327
|
+
return getConnectionStatus(agentId);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
// Derive the HTTP base URL from the WebSocket URL
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Convert a ws:// or wss:// URL to http:// or https:// for the HTTP API.
|
|
336
|
+
* Also passes through http/https URLs unchanged.
|
|
337
|
+
*/
|
|
338
|
+
export function httpBaseUrl(config: { url: string }): string {
|
|
339
|
+
return config.url
|
|
340
|
+
.replace(/^wss:\/\//, 'https://')
|
|
341
|
+
.replace(/^ws:\/\//, 'http://')
|
|
342
|
+
.replace(/\/+$/, '');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
// Internal helpers
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
function handleMessage(
|
|
350
|
+
conn: GatewayConnection,
|
|
351
|
+
msg: Record<string, unknown>,
|
|
352
|
+
onAuthenticated: () => void
|
|
353
|
+
): void {
|
|
354
|
+
const type = msg.type as string;
|
|
355
|
+
|
|
356
|
+
switch (type) {
|
|
357
|
+
// --- Events from the server ---
|
|
358
|
+
case 'event': {
|
|
359
|
+
const eventName = msg.event as string;
|
|
360
|
+
|
|
361
|
+
if (eventName === 'tick' || eventName === 'health') {
|
|
362
|
+
if (eventName === 'tick') conn.lastTick = Date.now();
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Skip noisy per-token agent events
|
|
367
|
+
if (eventName === 'agent') {
|
|
368
|
+
// Still dispatch to listeners below, just don't log
|
|
369
|
+
} else {
|
|
370
|
+
console.log(`[OpenClaw] ← event: ${eventName}`);
|
|
371
|
+
if (eventName === 'chat') {
|
|
372
|
+
console.log(`[OpenClaw] ← ${eventName} payload:\n${JSON.stringify(msg.payload, null, 2)}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (eventName === 'connect.challenge') {
|
|
377
|
+
sendConnectRequest(conn);
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Dispatch to registered event listeners
|
|
382
|
+
const listeners = conn.eventListeners.get(eventName);
|
|
383
|
+
if (listeners) {
|
|
384
|
+
for (const listener of listeners) {
|
|
385
|
+
try { listener(msg.payload); } catch (err) {
|
|
386
|
+
console.error(`[OpenClaw] Event listener error (${eventName}):`, err);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// Wildcard listeners
|
|
391
|
+
const wildcardListeners = conn.eventListeners.get('*');
|
|
392
|
+
if (wildcardListeners) {
|
|
393
|
+
for (const listener of wildcardListeners) {
|
|
394
|
+
try { listener(msg); } catch (err) {
|
|
395
|
+
console.error(`[OpenClaw] Wildcard listener error:`, err);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// --- RPC responses (including connect handshake) ---
|
|
403
|
+
case 'res': {
|
|
404
|
+
const id = msg.id as string;
|
|
405
|
+
const ok = msg.ok as boolean;
|
|
406
|
+
const payload = (msg.payload ?? msg.result ?? msg.data ?? {}) as Record<string, unknown>;
|
|
407
|
+
const error = msg.error as Record<string, unknown> | string | undefined;
|
|
408
|
+
|
|
409
|
+
const payloadType = (payload as Record<string, unknown>).type as string | undefined;
|
|
410
|
+
console.log(`[OpenClaw] ← res: ${id} ${ok ? (payloadType ?? 'ok') : 'ERROR'}`);
|
|
411
|
+
|
|
412
|
+
if (!ok || error) {
|
|
413
|
+
console.error(`[OpenClaw] ← error (id: ${id}):\n${JSON.stringify(msg, null, 2)}`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Check if this is the hello-ok response to our connect request
|
|
417
|
+
if (payload.type === 'hello-ok') {
|
|
418
|
+
conn.authenticated = true;
|
|
419
|
+
|
|
420
|
+
const policy = payload.policy as Record<string, unknown> | undefined;
|
|
421
|
+
if (policy?.tickIntervalMs && typeof policy.tickIntervalMs === 'number') {
|
|
422
|
+
conn.tickIntervalMs = policy.tickIntervalMs;
|
|
423
|
+
conn.lastTick = Date.now();
|
|
424
|
+
startTickWatch(conn);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const auth = payload.auth as Record<string, unknown> | undefined;
|
|
428
|
+
if (auth) {
|
|
429
|
+
conn.grantedAuth = {
|
|
430
|
+
role: (auth.role as string) ?? conn.config.role,
|
|
431
|
+
scopes: (auth.scopes as string[]) ?? conn.config.scopes,
|
|
432
|
+
deviceToken: auth.deviceToken as string | undefined,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
onAuthenticated();
|
|
437
|
+
|
|
438
|
+
// Also resolve the pending request if tracked
|
|
439
|
+
const pending = conn.pendingRequests.get(id);
|
|
440
|
+
if (pending) {
|
|
441
|
+
clearTimeout(pending.timer);
|
|
442
|
+
conn.pendingRequests.delete(id);
|
|
443
|
+
pending.resolve(payload);
|
|
444
|
+
}
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Regular RPC response
|
|
449
|
+
const pending = conn.pendingRequests.get(id);
|
|
450
|
+
if (pending) {
|
|
451
|
+
// Skip intermediate "accepted" acks for long-running requests
|
|
452
|
+
if (pending.expectFinal && payload.status === 'accepted') {
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
clearTimeout(pending.timer);
|
|
456
|
+
conn.pendingRequests.delete(id);
|
|
457
|
+
if (!ok || error) {
|
|
458
|
+
const errMsg = typeof error === 'string' ? error
|
|
459
|
+
: (error as Record<string, unknown>)?.message as string ?? JSON.stringify(error ?? 'Unknown error');
|
|
460
|
+
pending.reject(new Error(errMsg));
|
|
461
|
+
} else {
|
|
462
|
+
pending.resolve(payload);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Send the `connect` RPC request after receiving the server challenge.
|
|
472
|
+
*
|
|
473
|
+
* OpenClaw protocol requires:
|
|
474
|
+
* - client: { id: 'gateway-client', version, platform, mode: 'backend' }
|
|
475
|
+
* - device: { id, publicKey, signature, signedAt, nonce }
|
|
476
|
+
* - auth: { token }
|
|
477
|
+
*
|
|
478
|
+
* Device signature payload (pipe-delimited):
|
|
479
|
+
* v2 (with nonce): "v2|deviceId|clientId|clientMode|role|scopes|signedAt|token|nonce"
|
|
480
|
+
* v1 (no nonce): "v1|deviceId|clientId|clientMode|role|scopes|signedAt|token"
|
|
481
|
+
*/
|
|
482
|
+
function sendConnectRequest(conn: GatewayConnection): void {
|
|
483
|
+
const id = nextRequestId();
|
|
484
|
+
|
|
485
|
+
const connectMsg: Record<string, unknown> = {
|
|
486
|
+
type: 'req',
|
|
487
|
+
id,
|
|
488
|
+
method: 'connect',
|
|
489
|
+
params: {
|
|
490
|
+
minProtocol: 3,
|
|
491
|
+
maxProtocol: 3,
|
|
492
|
+
client: {
|
|
493
|
+
id: 'gateway-client',
|
|
494
|
+
version: '1.0.0',
|
|
495
|
+
platform: process.platform,
|
|
496
|
+
mode: 'backend',
|
|
497
|
+
},
|
|
498
|
+
role: conn.config.role,
|
|
499
|
+
scopes: conn.config.scopes,
|
|
500
|
+
caps: [],
|
|
501
|
+
commands: [],
|
|
502
|
+
permissions: {},
|
|
503
|
+
auth: { token: conn.config.token },
|
|
504
|
+
locale: 'en-US',
|
|
505
|
+
userAgent: 'SynthOS/1.0',
|
|
506
|
+
},
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
// Track as a pending request so we can correlate the response
|
|
510
|
+
conn.pendingRequests.set(id, {
|
|
511
|
+
resolve: () => { /* handled in handleMessage via hello-ok check */ },
|
|
512
|
+
reject: (err: Error) => {
|
|
513
|
+
console.error(`[OpenClaw] Connect request failed for ${conn.config.name}: ${err.message}`);
|
|
514
|
+
},
|
|
515
|
+
timer: setTimeout(() => {
|
|
516
|
+
conn.pendingRequests.delete(id);
|
|
517
|
+
}, AUTH_TIMEOUT_MS),
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
conn.ws?.send(JSON.stringify(connectMsg));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** Monitor server tick events — close if stalled (2x interval with no tick). */
|
|
524
|
+
function startTickWatch(conn: GatewayConnection): void {
|
|
525
|
+
stopTickWatch(conn);
|
|
526
|
+
if (conn.tickIntervalMs <= 0) return;
|
|
527
|
+
conn.tickTimer = setInterval(() => {
|
|
528
|
+
const elapsed = Date.now() - conn.lastTick;
|
|
529
|
+
if (elapsed > conn.tickIntervalMs * 2) {
|
|
530
|
+
conn.ws?.close(4000, 'tick stall');
|
|
531
|
+
}
|
|
532
|
+
}, conn.tickIntervalMs);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function stopTickWatch(conn: GatewayConnection): void {
|
|
536
|
+
if (conn.tickTimer) {
|
|
537
|
+
clearInterval(conn.tickTimer);
|
|
538
|
+
conn.tickTimer = null;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function scheduleReconnect(conn: GatewayConnection): void {
|
|
543
|
+
if (conn.reconnectTimer || conn.intentionalClose) return;
|
|
544
|
+
|
|
545
|
+
conn.reconnectAttempts++;
|
|
546
|
+
const delay = Math.min(
|
|
547
|
+
BASE_RECONNECT_DELAY_MS * Math.pow(2, conn.reconnectAttempts - 1),
|
|
548
|
+
MAX_RECONNECT_DELAY_MS
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
conn.reconnectTimer = setTimeout(async () => {
|
|
552
|
+
conn.reconnectTimer = null;
|
|
553
|
+
try {
|
|
554
|
+
await connect(conn.config);
|
|
555
|
+
} catch (err) {
|
|
556
|
+
console.error(`[OpenClaw] Reconnect failed for ${conn.config.name}:`, err instanceof Error ? err.message : err);
|
|
557
|
+
}
|
|
558
|
+
}, delay);
|
|
559
|
+
}
|