@coclaw/openclaw-coclaw 0.2.3 → 0.3.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/index.js +6 -2
- package/package.json +1 -1
- package/src/device-identity.js +190 -0
- package/src/realtime-bridge.js +120 -55
package/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { registerCoclawCli } from './src/cli-registrar.js';
|
|
|
3
3
|
import { resolveErrorMessage } from './src/common/errors.js';
|
|
4
4
|
import { notBound, bindOk, unbindOk } from './src/common/messages.js';
|
|
5
5
|
import { coclawChannelPlugin } from './src/channel-plugin.js';
|
|
6
|
-
import { refreshRealtimeBridge, startRealtimeBridge, stopRealtimeBridge } from './src/realtime-bridge.js';
|
|
6
|
+
import { ensureAgentSession, refreshRealtimeBridge, startRealtimeBridge, stopRealtimeBridge } from './src/realtime-bridge.js';
|
|
7
7
|
import { setRuntime } from './src/runtime.js';
|
|
8
8
|
import { createSessionManager } from './src/session-manager/manager.js';
|
|
9
9
|
import { AutoUpgradeScheduler } from './src/auto-upgrade/updater.js';
|
|
@@ -88,8 +88,12 @@ const plugin = {
|
|
|
88
88
|
}
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
-
api.registerGatewayMethod('nativeui.sessions.listAll', ({ params, respond }) => {
|
|
91
|
+
api.registerGatewayMethod('nativeui.sessions.listAll', async ({ params, respond }) => {
|
|
92
92
|
try {
|
|
93
|
+
const agentId = params?.agentId?.trim?.() || 'main';
|
|
94
|
+
// best-effort ensure:失败不阻断 listAll
|
|
95
|
+
try { await ensureAgentSession(agentId); }
|
|
96
|
+
catch {}
|
|
93
97
|
respond(true, manager.listAll(params ?? {}));
|
|
94
98
|
}
|
|
95
99
|
catch (err) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import nodePath from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { getRuntime } from './runtime.js';
|
|
7
|
+
|
|
8
|
+
const CHANNEL_ID = 'coclaw';
|
|
9
|
+
const IDENTITY_FILENAME = 'device-identity.json';
|
|
10
|
+
|
|
11
|
+
// Ed25519 SPKI 前缀(固定 12 字节),公钥裸字节从 SPKI DER 中截取
|
|
12
|
+
const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
|
|
13
|
+
|
|
14
|
+
function base64UrlEncode(buf) {
|
|
15
|
+
return buf.toString('base64').replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/g, '');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 仅处理 ASCII 范围的大写→小写,保持跨运行时确定性
|
|
19
|
+
function toLowerAscii(input) {
|
|
20
|
+
return input.replace(/[A-Z]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) + 32));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeMetadataForAuth(value) {
|
|
24
|
+
if (typeof value !== 'string') return '';
|
|
25
|
+
const trimmed = value.trim();
|
|
26
|
+
return trimmed ? toLowerAscii(trimmed) : '';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveStateDir() {
|
|
30
|
+
const rt = getRuntime();
|
|
31
|
+
if (rt?.state?.resolveStateDir) {
|
|
32
|
+
return rt.state.resolveStateDir();
|
|
33
|
+
}
|
|
34
|
+
return process.env.OPENCLAW_STATE_DIR
|
|
35
|
+
? nodePath.resolve(process.env.OPENCLAW_STATE_DIR)
|
|
36
|
+
: nodePath.join(os.homedir(), '.openclaw');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 获取身份文件路径
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
export function getIdentityPath() {
|
|
44
|
+
return nodePath.join(resolveStateDir(), CHANNEL_ID, IDENTITY_FILENAME);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 从 PEM 公钥提取裸字节
|
|
49
|
+
* @param {string} publicKeyPem
|
|
50
|
+
* @returns {Buffer}
|
|
51
|
+
*/
|
|
52
|
+
function derivePublicKeyRaw(publicKeyPem) {
|
|
53
|
+
const key = crypto.createPublicKey(publicKeyPem);
|
|
54
|
+
const spki = key.export({ type: 'spki', format: 'der' });
|
|
55
|
+
if (
|
|
56
|
+
spki.length === ED25519_SPKI_PREFIX.length + 32
|
|
57
|
+
&& spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)
|
|
58
|
+
) {
|
|
59
|
+
return spki.subarray(ED25519_SPKI_PREFIX.length);
|
|
60
|
+
}
|
|
61
|
+
/* c8 ignore next -- Ed25519 密钥 SPKI 格式固定,此分支仅防御未知密钥格式 */
|
|
62
|
+
return spki;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 公钥指纹 = SHA256(裸公钥字节) 的十六进制
|
|
67
|
+
* @param {string} publicKeyPem
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
function fingerprintPublicKey(publicKeyPem) {
|
|
71
|
+
const raw = derivePublicKeyRaw(publicKeyPem);
|
|
72
|
+
return crypto.createHash('sha256').update(raw).digest('hex');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 生成新的 Ed25519 密钥对
|
|
77
|
+
* @returns {{ deviceId: string, publicKeyPem: string, privateKeyPem: string }}
|
|
78
|
+
*/
|
|
79
|
+
function generateIdentity() {
|
|
80
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
|
|
81
|
+
const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }).toString();
|
|
82
|
+
const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }).toString();
|
|
83
|
+
const deviceId = fingerprintPublicKey(publicKeyPem);
|
|
84
|
+
return { deviceId, publicKeyPem, privateKeyPem };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 加载或创建设备身份(Ed25519 密钥对)
|
|
89
|
+
*
|
|
90
|
+
* 存储格式与 OpenClaw device-identity.ts 保持一致。
|
|
91
|
+
* @param {string} [filePath] - 自定义路径,默认 ~/.openclaw/coclaw/device-identity.json
|
|
92
|
+
* @returns {{ deviceId: string, publicKeyPem: string, privateKeyPem: string }}
|
|
93
|
+
*/
|
|
94
|
+
export function loadOrCreateDeviceIdentity(filePath) {
|
|
95
|
+
const fp = filePath ?? getIdentityPath();
|
|
96
|
+
try {
|
|
97
|
+
if (fs.existsSync(fp)) {
|
|
98
|
+
const raw = fs.readFileSync(fp, 'utf8');
|
|
99
|
+
const parsed = JSON.parse(raw);
|
|
100
|
+
if (
|
|
101
|
+
parsed?.version === 1
|
|
102
|
+
&& typeof parsed.deviceId === 'string'
|
|
103
|
+
&& typeof parsed.publicKeyPem === 'string'
|
|
104
|
+
&& typeof parsed.privateKeyPem === 'string'
|
|
105
|
+
) {
|
|
106
|
+
// 校验 deviceId 一致性
|
|
107
|
+
const derivedId = fingerprintPublicKey(parsed.publicKeyPem);
|
|
108
|
+
if (derivedId && derivedId !== parsed.deviceId) {
|
|
109
|
+
const updated = { ...parsed, deviceId: derivedId };
|
|
110
|
+
fs.writeFileSync(fp, `${JSON.stringify(updated, null, 2)}\n`, { mode: 0o600 });
|
|
111
|
+
try { fs.chmodSync(fp, 0o600); } catch { /* best-effort */ }
|
|
112
|
+
return { deviceId: derivedId, publicKeyPem: parsed.publicKeyPem, privateKeyPem: parsed.privateKeyPem };
|
|
113
|
+
}
|
|
114
|
+
return { deviceId: parsed.deviceId, publicKeyPem: parsed.publicKeyPem, privateKeyPem: parsed.privateKeyPem };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// 读取/解析失败时重新生成
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const identity = generateIdentity();
|
|
123
|
+
fs.mkdirSync(nodePath.dirname(fp), { recursive: true });
|
|
124
|
+
const stored = {
|
|
125
|
+
version: 1,
|
|
126
|
+
deviceId: identity.deviceId,
|
|
127
|
+
publicKeyPem: identity.publicKeyPem,
|
|
128
|
+
privateKeyPem: identity.privateKeyPem,
|
|
129
|
+
createdAtMs: Date.now(),
|
|
130
|
+
};
|
|
131
|
+
fs.writeFileSync(fp, `${JSON.stringify(stored, null, 2)}\n`, { mode: 0o600 });
|
|
132
|
+
try { fs.chmodSync(fp, 0o600); } catch { /* best-effort */ }
|
|
133
|
+
return identity;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 签名设备认证载荷
|
|
138
|
+
* @param {string} privateKeyPem
|
|
139
|
+
* @param {string} payload
|
|
140
|
+
* @returns {string} base64url 编码签名
|
|
141
|
+
*/
|
|
142
|
+
export function signDevicePayload(privateKeyPem, payload) {
|
|
143
|
+
const key = crypto.createPrivateKey(privateKeyPem);
|
|
144
|
+
const sig = crypto.sign(null, Buffer.from(payload, 'utf8'), key);
|
|
145
|
+
return base64UrlEncode(sig);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 公钥 PEM → base64url 裸字节
|
|
150
|
+
* @param {string} publicKeyPem
|
|
151
|
+
* @returns {string}
|
|
152
|
+
*/
|
|
153
|
+
export function publicKeyRawBase64Url(publicKeyPem) {
|
|
154
|
+
return base64UrlEncode(derivePublicKeyRaw(publicKeyPem));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 构建 v3 版本的设备认证载荷字符串
|
|
159
|
+
* @param {object} params
|
|
160
|
+
* @param {string} params.deviceId
|
|
161
|
+
* @param {string} params.clientId
|
|
162
|
+
* @param {string} params.clientMode
|
|
163
|
+
* @param {string} params.role
|
|
164
|
+
* @param {string[]} params.scopes
|
|
165
|
+
* @param {number} params.signedAtMs
|
|
166
|
+
* @param {string} [params.token]
|
|
167
|
+
* @param {string} params.nonce
|
|
168
|
+
* @param {string} [params.platform]
|
|
169
|
+
* @param {string} [params.deviceFamily]
|
|
170
|
+
* @returns {string}
|
|
171
|
+
*/
|
|
172
|
+
export function buildDeviceAuthPayloadV3(params) {
|
|
173
|
+
const scopes = params.scopes.join(',');
|
|
174
|
+
const token = params.token ?? '';
|
|
175
|
+
const platform = normalizeMetadataForAuth(params.platform);
|
|
176
|
+
const deviceFamily = normalizeMetadataForAuth(params.deviceFamily);
|
|
177
|
+
return [
|
|
178
|
+
'v3',
|
|
179
|
+
params.deviceId,
|
|
180
|
+
params.clientId,
|
|
181
|
+
params.clientMode,
|
|
182
|
+
params.role,
|
|
183
|
+
scopes,
|
|
184
|
+
String(params.signedAtMs),
|
|
185
|
+
token,
|
|
186
|
+
params.nonce,
|
|
187
|
+
platform,
|
|
188
|
+
deviceFamily,
|
|
189
|
+
].join('|');
|
|
190
|
+
}
|
package/src/realtime-bridge.js
CHANGED
|
@@ -3,6 +3,12 @@ import os from 'node:os';
|
|
|
3
3
|
import nodePath from 'node:path';
|
|
4
4
|
|
|
5
5
|
import { clearConfig, getBindingsPath, readConfig } from './config.js';
|
|
6
|
+
import {
|
|
7
|
+
loadOrCreateDeviceIdentity,
|
|
8
|
+
signDevicePayload,
|
|
9
|
+
publicKeyRawBase64Url,
|
|
10
|
+
buildDeviceAuthPayloadV3,
|
|
11
|
+
} from './device-identity.js';
|
|
6
12
|
import { getRuntime } from './runtime.js';
|
|
7
13
|
|
|
8
14
|
const DEFAULT_GATEWAY_WS_URL = 'ws://127.0.0.1:18789';
|
|
@@ -65,12 +71,14 @@ export class RealtimeBridge {
|
|
|
65
71
|
* @param {Function} [deps.clearConfig] - 清除绑定配置
|
|
66
72
|
* @param {Function} [deps.getBindingsPath] - 获取绑定文件路径
|
|
67
73
|
* @param {Function} [deps.resolveGatewayAuthToken] - 获取 gateway 认证 token
|
|
74
|
+
* @param {Function} [deps.loadDeviceIdentity] - 加载设备身份
|
|
68
75
|
*/
|
|
69
76
|
constructor(deps = {}) {
|
|
70
77
|
this.__readConfig = deps.readConfig ?? readConfig;
|
|
71
78
|
this.__clearConfig = deps.clearConfig ?? clearConfig;
|
|
72
79
|
this.__getBindingsPath = deps.getBindingsPath ?? getBindingsPath;
|
|
73
80
|
this.__resolveGatewayAuthToken = deps.resolveGatewayAuthToken ?? defaultResolveGatewayAuthToken;
|
|
81
|
+
this.__loadDeviceIdentity = deps.loadDeviceIdentity ?? loadOrCreateDeviceIdentity;
|
|
74
82
|
this.__WebSocket = deps.WebSocket ?? null;
|
|
75
83
|
|
|
76
84
|
this.serverWs = null;
|
|
@@ -82,14 +90,13 @@ export class RealtimeBridge {
|
|
|
82
90
|
this.gatewayConnectReqId = null;
|
|
83
91
|
this.gatewayRpcSeq = 0;
|
|
84
92
|
this.gatewayPendingRequests = new Map();
|
|
85
|
-
this.mainSessionEnsurePromise = null;
|
|
86
|
-
this.mainSessionEnsured = false;
|
|
87
93
|
this.logger = console;
|
|
88
94
|
this.pluginConfig = {};
|
|
89
95
|
this.intentionallyClosed = false;
|
|
90
96
|
this.serverHbInterval = null;
|
|
91
97
|
this.serverHbTimer = null;
|
|
92
98
|
this.__serverHbMissCount = 0;
|
|
99
|
+
this.__deviceIdentity = null;
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
__resolveWebSocket() {
|
|
@@ -239,67 +246,118 @@ export class RealtimeBridge {
|
|
|
239
246
|
});
|
|
240
247
|
}
|
|
241
248
|
|
|
242
|
-
|
|
243
|
-
|
|
249
|
+
/**
|
|
250
|
+
* 确保指定 agent 的主 session 存在(sessions.resolve + 条件 sessions.reset)
|
|
251
|
+
* @param {string} [agentId] - agent ID,默认 'main'
|
|
252
|
+
* @returns {Promise<{ok: boolean, state?: string, error?: string}>}
|
|
253
|
+
*/
|
|
254
|
+
async ensureAgentSession(agentId) {
|
|
255
|
+
const aid = typeof agentId === 'string' && agentId.trim() ? agentId.trim() : 'main';
|
|
256
|
+
const key = `agent:${aid}:main`;
|
|
257
|
+
const resolved = await this.__gatewayRpc('sessions.resolve', { key }, { timeoutMs: 2000 });
|
|
258
|
+
if (resolved?.ok === true) {
|
|
259
|
+
this.__logDebug(`ensure agent session: ready agentId=${aid}`);
|
|
244
260
|
return { ok: true, state: 'ready' };
|
|
245
261
|
}
|
|
246
|
-
|
|
247
|
-
if (
|
|
248
|
-
return
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
+
// 仅当网关真实响应 "不存在" 时才创建;超时/网关未就绪等瞬态错误不触发 reset
|
|
263
|
+
if (!resolved?.response) {
|
|
264
|
+
return { ok: false, error: resolved?.error ?? 'resolve_transient_failure' };
|
|
265
|
+
}
|
|
266
|
+
// session key 不存在,通过 sessions.reset 创建
|
|
267
|
+
const reset = await this.__gatewayRpc('sessions.reset', { key, reason: 'new' }, { timeoutMs: 2500 });
|
|
268
|
+
if (reset?.ok !== true) {
|
|
269
|
+
return { ok: false, error: reset?.error ?? 'sessions_reset_failed' };
|
|
270
|
+
}
|
|
271
|
+
this.__logDebug(`ensure agent session: created agentId=${aid}`);
|
|
272
|
+
return { ok: true, state: 'created' };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async __ensureAllAgentSessions() {
|
|
276
|
+
try {
|
|
277
|
+
const listResult = await this.__gatewayRpc('agents.list', {}, { timeoutMs: 3000 });
|
|
278
|
+
let agentIds = ['main'];
|
|
279
|
+
if (listResult?.ok === true && Array.isArray(listResult?.response?.payload?.agents)) {
|
|
280
|
+
const ids = listResult.response.payload.agents
|
|
281
|
+
.map((a) => a?.id)
|
|
282
|
+
.filter((id) => typeof id === 'string' && id.trim());
|
|
283
|
+
if (ids.length > 0) agentIds = ids;
|
|
262
284
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (reset?.ok !== true) {
|
|
266
|
-
return { ok: false, error: reset?.error ?? 'sessions_reset_failed' };
|
|
285
|
+
else {
|
|
286
|
+
this.logger.warn?.(`[coclaw] agents.list failed, falling back to main: ${listResult?.error ?? 'unknown'}`);
|
|
267
287
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
this.logger.warn?.(`[coclaw] ensure
|
|
288
|
+
const results = await Promise.allSettled(
|
|
289
|
+
agentIds.map((id) => this.ensureAgentSession(id)),
|
|
290
|
+
);
|
|
291
|
+
for (let i = 0; i < results.length; i++) {
|
|
292
|
+
const r = results[i];
|
|
293
|
+
if (r.status === 'fulfilled' && r.value?.ok) continue;
|
|
294
|
+
const err = r.status === 'fulfilled' ? r.value?.error : String(r.reason);
|
|
295
|
+
this.logger.warn?.(`[coclaw] ensure agent session failed: agentId=${agentIds[i]} error=${err ?? 'unknown'}`);
|
|
276
296
|
}
|
|
277
|
-
return result;
|
|
278
297
|
}
|
|
279
|
-
|
|
280
|
-
|
|
298
|
+
/* c8 ignore next 3 -- 防御性兜底,__gatewayRpc 内部已有完整错误处理 */
|
|
299
|
+
catch (err) {
|
|
300
|
+
this.logger.warn?.(`[coclaw] ensureAllAgentSessions unexpected error: ${String(err?.message ?? err)}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
__ensureDeviceIdentity() {
|
|
305
|
+
if (!this.__deviceIdentity) {
|
|
306
|
+
this.__deviceIdentity = this.__loadDeviceIdentity();
|
|
281
307
|
}
|
|
308
|
+
return this.__deviceIdentity;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
__buildDeviceField(nonce, authToken) {
|
|
312
|
+
const identity = this.__ensureDeviceIdentity();
|
|
313
|
+
const clientId = 'gateway-client';
|
|
314
|
+
const clientMode = 'backend';
|
|
315
|
+
const role = 'operator';
|
|
316
|
+
const scopes = ['operator.admin'];
|
|
317
|
+
const signedAtMs = Date.now();
|
|
318
|
+
const payload = buildDeviceAuthPayloadV3({
|
|
319
|
+
deviceId: identity.deviceId,
|
|
320
|
+
clientId,
|
|
321
|
+
clientMode,
|
|
322
|
+
role,
|
|
323
|
+
scopes,
|
|
324
|
+
signedAtMs,
|
|
325
|
+
token: authToken ?? '',
|
|
326
|
+
nonce: nonce ?? '',
|
|
327
|
+
platform: process.platform,
|
|
328
|
+
deviceFamily: '',
|
|
329
|
+
});
|
|
330
|
+
const signature = signDevicePayload(identity.privateKeyPem, payload);
|
|
331
|
+
return {
|
|
332
|
+
id: identity.deviceId,
|
|
333
|
+
publicKey: publicKeyRawBase64Url(identity.publicKeyPem),
|
|
334
|
+
signature,
|
|
335
|
+
signedAt: signedAtMs,
|
|
336
|
+
nonce: nonce ?? '',
|
|
337
|
+
};
|
|
282
338
|
}
|
|
283
339
|
|
|
284
|
-
__sendGatewayConnectRequest(ws) {
|
|
340
|
+
__sendGatewayConnectRequest(ws, nonce) {
|
|
285
341
|
this.gatewayConnectReqId = `coclaw-connect-${Date.now()}`;
|
|
286
342
|
this.__logDebug(`gateway connect request -> id=${this.gatewayConnectReqId}`);
|
|
287
|
-
const authToken = this.__resolveGatewayAuthToken();
|
|
288
|
-
const params = {
|
|
289
|
-
minProtocol: 3,
|
|
290
|
-
maxProtocol: 3,
|
|
291
|
-
client: {
|
|
292
|
-
id: 'gateway-client',
|
|
293
|
-
version: 'dev',
|
|
294
|
-
platform: process.platform,
|
|
295
|
-
mode: 'backend',
|
|
296
|
-
},
|
|
297
|
-
caps: [],
|
|
298
|
-
role: 'operator',
|
|
299
|
-
scopes: ['operator.admin'],
|
|
300
|
-
auth: authToken ? { token: authToken } : undefined,
|
|
301
|
-
};
|
|
302
343
|
try {
|
|
344
|
+
const authToken = this.__resolveGatewayAuthToken();
|
|
345
|
+
const device = this.__buildDeviceField(nonce, authToken);
|
|
346
|
+
const params = {
|
|
347
|
+
minProtocol: 3,
|
|
348
|
+
maxProtocol: 3,
|
|
349
|
+
client: {
|
|
350
|
+
id: 'gateway-client',
|
|
351
|
+
version: 'dev',
|
|
352
|
+
platform: process.platform,
|
|
353
|
+
mode: 'backend',
|
|
354
|
+
},
|
|
355
|
+
caps: [],
|
|
356
|
+
role: 'operator',
|
|
357
|
+
scopes: ['operator.admin'],
|
|
358
|
+
auth: authToken ? { token: authToken } : undefined,
|
|
359
|
+
device,
|
|
360
|
+
};
|
|
303
361
|
ws.send(JSON.stringify({
|
|
304
362
|
type: 'req',
|
|
305
363
|
id: this.gatewayConnectReqId,
|
|
@@ -338,8 +396,9 @@ export class RealtimeBridge {
|
|
|
338
396
|
return;
|
|
339
397
|
}
|
|
340
398
|
if (payload.type === 'event' && payload.event === 'connect.challenge') {
|
|
399
|
+
const nonce = payload?.payload?.nonce ?? '';
|
|
341
400
|
this.__logDebug('gateway event <- connect.challenge');
|
|
342
|
-
this.__sendGatewayConnectRequest(ws);
|
|
401
|
+
this.__sendGatewayConnectRequest(ws, nonce);
|
|
343
402
|
return;
|
|
344
403
|
}
|
|
345
404
|
if (payload.type === 'res' && this.gatewayConnectReqId && payload.id === this.gatewayConnectReqId) {
|
|
@@ -347,7 +406,7 @@ export class RealtimeBridge {
|
|
|
347
406
|
this.gatewayReady = true;
|
|
348
407
|
this.__logDebug(`gateway connect ok <- id=${payload.id}`);
|
|
349
408
|
this.gatewayConnectReqId = null;
|
|
350
|
-
void this.
|
|
409
|
+
void this.__ensureAllAgentSessions();
|
|
351
410
|
}
|
|
352
411
|
else {
|
|
353
412
|
this.gatewayReady = false;
|
|
@@ -624,8 +683,6 @@ export class RealtimeBridge {
|
|
|
624
683
|
|
|
625
684
|
async stop() {
|
|
626
685
|
this.started = false;
|
|
627
|
-
this.mainSessionEnsured = false;
|
|
628
|
-
this.mainSessionEnsurePromise = null;
|
|
629
686
|
this.__clearServerHeartbeat();
|
|
630
687
|
this.__clearConnectTimer();
|
|
631
688
|
if (this.reconnectTimer) {
|
|
@@ -676,4 +733,12 @@ export async function stopRealtimeBridge() {
|
|
|
676
733
|
return;
|
|
677
734
|
}
|
|
678
735
|
await singleton.stop();
|
|
736
|
+
singleton = null;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
export async function ensureAgentSession(agentId) {
|
|
740
|
+
if (!singleton) {
|
|
741
|
+
return { ok: false, error: 'bridge_not_started' };
|
|
742
|
+
}
|
|
743
|
+
return singleton.ensureAgentSession(agentId);
|
|
679
744
|
}
|