oomi-ai 0.2.3 → 0.2.5
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/bin/oomi-ai.js +571 -98
- package/bin/sessionBridgeState.js +51 -0
- package/openclaw.extension.js +21 -9
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -2
package/bin/oomi-ai.js
CHANGED
|
@@ -4,7 +4,10 @@ import os from 'os';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { spawn } from 'child_process';
|
|
6
6
|
import { randomUUID } from 'crypto';
|
|
7
|
+
import net from 'net';
|
|
8
|
+
import { lookup as dnsLookup } from 'dns/promises';
|
|
7
9
|
import WebSocket from 'ws';
|
|
10
|
+
import { ensureSessionBridge, forwardFrameToSession, flushSessionQueue } from './sessionBridgeState.js';
|
|
8
11
|
|
|
9
12
|
const MARKER_START = '<oomi-agent-instructions>';
|
|
10
13
|
const MARKER_END = '</oomi-agent-instructions>';
|
|
@@ -13,6 +16,8 @@ const PACKAGE_ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname
|
|
|
13
16
|
const UPDATE_STATE_FILE = path.join(os.homedir(), '.openclaw', 'oomi-ai-update-check.json');
|
|
14
17
|
const DEFAULT_UPDATE_CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000;
|
|
15
18
|
const DEFAULT_UPDATE_CHECK_TIMEOUT_MS = 1200;
|
|
19
|
+
const BRIDGE_RECONNECT_BASE_MS = 2000;
|
|
20
|
+
const BRIDGE_RECONNECT_MAX_MS = 60000;
|
|
16
21
|
|
|
17
22
|
function parsePositiveInteger(value, fallback) {
|
|
18
23
|
const num = Number(value);
|
|
@@ -148,6 +153,9 @@ Commands:
|
|
|
148
153
|
openclaw plugin
|
|
149
154
|
Print OpenClaw extension install/config guidance for Oomi channel plugin.
|
|
150
155
|
|
|
156
|
+
openclaw status
|
|
157
|
+
Show bridge state + runtime health from local status files.
|
|
158
|
+
|
|
151
159
|
personas sync
|
|
152
160
|
Sync personas from the repo into the Oomi backend registry.
|
|
153
161
|
|
|
@@ -161,7 +169,7 @@ Common flags:
|
|
|
161
169
|
--broker-http URL Managed broker HTTPS URL (for pair claim)
|
|
162
170
|
--broker-ws URL Managed broker device WS URL (wss://.../cable)
|
|
163
171
|
--pair-code CODE One-time pairing code from Oomi
|
|
164
|
-
--app-url URL Oomi app URL used for pairing APIs
|
|
172
|
+
--app-url URL Oomi app URL used for pairing APIs; bridge can also refresh managed broker URLs from it
|
|
165
173
|
--label TEXT Pairing label shown in broker logs
|
|
166
174
|
--session-key KEY Session key used in generated connect URL
|
|
167
175
|
--detach Start bridge in background and exit
|
|
@@ -536,6 +544,10 @@ function resolveBridgeStatePath() {
|
|
|
536
544
|
return path.join(os.homedir(), '.openclaw', 'oomi-bridge.json');
|
|
537
545
|
}
|
|
538
546
|
|
|
547
|
+
function resolveBridgeStatusPath() {
|
|
548
|
+
return path.join(os.homedir(), '.openclaw', 'oomi-bridge-status.json');
|
|
549
|
+
}
|
|
550
|
+
|
|
539
551
|
function defaultDeviceId() {
|
|
540
552
|
const host = os.hostname().toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 24) || 'openclaw';
|
|
541
553
|
return `oomi-${host}-${randomUUID().slice(0, 8)}`;
|
|
@@ -565,6 +577,27 @@ function writeBridgeState(nextState) {
|
|
|
565
577
|
writeFile(statePath, JSON.stringify(nextState, null, 2) + '\n');
|
|
566
578
|
}
|
|
567
579
|
|
|
580
|
+
function readBridgeStatus() {
|
|
581
|
+
return readJsonSafe(resolveBridgeStatusPath()) || {};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function writeBridgeStatus(nextStatus) {
|
|
585
|
+
const statusPath = resolveBridgeStatusPath();
|
|
586
|
+
ensureDir(path.dirname(statusPath));
|
|
587
|
+
writeFile(statusPath, JSON.stringify(nextStatus, null, 2) + '\n');
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function updateBridgeStatus(partial) {
|
|
591
|
+
const current = readBridgeStatus();
|
|
592
|
+
const next = {
|
|
593
|
+
...current,
|
|
594
|
+
...partial,
|
|
595
|
+
updatedAt: new Date().toISOString(),
|
|
596
|
+
};
|
|
597
|
+
writeBridgeStatus(next);
|
|
598
|
+
return next;
|
|
599
|
+
}
|
|
600
|
+
|
|
568
601
|
async function claimBridgeDeviceToken({ brokerHttp, pairCode, deviceId }) {
|
|
569
602
|
const response = await fetch(`${brokerHttp.replace(/\/$/, '')}/v1/pair/claim`, {
|
|
570
603
|
method: 'POST',
|
|
@@ -673,29 +706,219 @@ function parseJsonPayload(raw) {
|
|
|
673
706
|
}
|
|
674
707
|
}
|
|
675
708
|
|
|
709
|
+
function bridgeNowIso() {
|
|
710
|
+
return new Date().toISOString();
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function extractErrorCode(err) {
|
|
714
|
+
if (!err || typeof err !== 'object') return '';
|
|
715
|
+
const value = err.code;
|
|
716
|
+
return typeof value === 'string' ? value.trim().toUpperCase() : '';
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function extractErrorMessage(err) {
|
|
720
|
+
if (!err) return '';
|
|
721
|
+
if (typeof err === 'string') return err.trim();
|
|
722
|
+
if (err instanceof Error) return err.message.trim();
|
|
723
|
+
return String(err).trim();
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function classifyBridgeFailure({ err, reason = '', code = '', forceClass = '' } = {}) {
|
|
727
|
+
const errorCode = (code || extractErrorCode(err)).toUpperCase();
|
|
728
|
+
const message = [extractErrorMessage(err), String(reason || '').trim()].filter(Boolean).join(' | ');
|
|
729
|
+
const text = `${errorCode} ${message}`.toLowerCase();
|
|
730
|
+
|
|
731
|
+
let failureClass = forceClass || 'unknown';
|
|
732
|
+
let retryable = true;
|
|
733
|
+
let hint = 'Check broker URL, network path, and bridge token.';
|
|
734
|
+
let baseDelayMs = BRIDGE_RECONNECT_BASE_MS;
|
|
735
|
+
|
|
736
|
+
if (
|
|
737
|
+
errorCode === 'ENOTFOUND' ||
|
|
738
|
+
errorCode === 'EAI_AGAIN' ||
|
|
739
|
+
text.includes('enotfound') ||
|
|
740
|
+
text.includes('name resolution') ||
|
|
741
|
+
text.includes('dns')
|
|
742
|
+
) {
|
|
743
|
+
failureClass = 'dns_resolution';
|
|
744
|
+
hint = 'Host resolution failed. Verify DNS/network access to the broker host.';
|
|
745
|
+
baseDelayMs = 5000;
|
|
746
|
+
} else if (
|
|
747
|
+
text.includes('unauthorized') ||
|
|
748
|
+
text.includes('forbidden') ||
|
|
749
|
+
text.includes('invalid token') ||
|
|
750
|
+
text.includes('token expired') ||
|
|
751
|
+
text.includes('broker rejected')
|
|
752
|
+
) {
|
|
753
|
+
failureClass = 'auth_rejected';
|
|
754
|
+
retryable = false;
|
|
755
|
+
hint = 'Bridge token is invalid/expired. Re-run: oomi openclaw pair --app-url <url>.';
|
|
756
|
+
baseDelayMs = BRIDGE_RECONNECT_MAX_MS;
|
|
757
|
+
} else if (
|
|
758
|
+
errorCode === 'ECONNREFUSED' ||
|
|
759
|
+
errorCode === 'ENETUNREACH' ||
|
|
760
|
+
errorCode === 'EHOSTUNREACH' ||
|
|
761
|
+
errorCode === 'ETIMEDOUT' ||
|
|
762
|
+
text.includes('socket hang up') ||
|
|
763
|
+
text.includes('abnormal closure')
|
|
764
|
+
) {
|
|
765
|
+
failureClass = 'network';
|
|
766
|
+
hint = 'Broker network path is unavailable. Check connectivity/firewall/proxy settings.';
|
|
767
|
+
baseDelayMs = 3000;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return {
|
|
771
|
+
errorCode: errorCode || 'UNKNOWN',
|
|
772
|
+
message: message || 'unknown bridge error',
|
|
773
|
+
failureClass,
|
|
774
|
+
retryable,
|
|
775
|
+
hint,
|
|
776
|
+
baseDelayMs,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function computeReconnectDelayMs(attempt, baseDelayMs) {
|
|
781
|
+
const growth = Math.min(BRIDGE_RECONNECT_MAX_MS, Math.round(baseDelayMs * (2 ** Math.max(0, attempt - 1))));
|
|
782
|
+
const jitter = Math.floor(growth * (Math.random() * 0.25));
|
|
783
|
+
return Math.min(BRIDGE_RECONNECT_MAX_MS, growth + jitter);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
async function assertTcpReachable(urlValue, timeoutMs = 1500) {
|
|
787
|
+
const url = new URL(urlValue);
|
|
788
|
+
const host = url.hostname || '127.0.0.1';
|
|
789
|
+
const port = Number(url.port || (url.protocol === 'wss:' || url.protocol === 'https:' ? 443 : 80));
|
|
790
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
791
|
+
throw new Error(`Invalid port in URL: ${urlValue}`);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
await new Promise((resolve, reject) => {
|
|
795
|
+
const socket = net.connect({ host, port });
|
|
796
|
+
let finished = false;
|
|
797
|
+
const done = (fn) => (value) => {
|
|
798
|
+
if (finished) return;
|
|
799
|
+
finished = true;
|
|
800
|
+
clearTimeout(timer);
|
|
801
|
+
socket.destroy();
|
|
802
|
+
fn(value);
|
|
803
|
+
};
|
|
804
|
+
const timer = setTimeout(done(reject), timeoutMs, new Error(`TCP connect timeout (${host}:${port})`));
|
|
805
|
+
socket.once('connect', done(resolve));
|
|
806
|
+
socket.once('error', done(reject));
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
async function runBridgePreflight({ brokerWs, gatewayUrl, gatewayConfigPath }) {
|
|
811
|
+
let brokerUrl;
|
|
812
|
+
try {
|
|
813
|
+
brokerUrl = new URL(brokerWs);
|
|
814
|
+
} catch {
|
|
815
|
+
throw new Error(`Invalid broker WS URL: ${brokerWs}`);
|
|
816
|
+
}
|
|
817
|
+
if (brokerUrl.protocol !== 'ws:' && brokerUrl.protocol !== 'wss:') {
|
|
818
|
+
throw new Error(`Broker WS URL must use ws:// or wss:// (received ${brokerUrl.protocol})`);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const brokerHost = brokerUrl.hostname;
|
|
822
|
+
if (!brokerHost) {
|
|
823
|
+
throw new Error('Broker WS URL is missing hostname.');
|
|
824
|
+
}
|
|
825
|
+
await dnsLookup(brokerHost);
|
|
826
|
+
|
|
827
|
+
let parsedGatewayUrl;
|
|
828
|
+
try {
|
|
829
|
+
parsedGatewayUrl = new URL(gatewayUrl);
|
|
830
|
+
} catch {
|
|
831
|
+
throw new Error(`Invalid local gateway URL (${gatewayUrl}) from ${gatewayConfigPath}`);
|
|
832
|
+
}
|
|
833
|
+
if (parsedGatewayUrl.protocol !== 'ws:') {
|
|
834
|
+
throw new Error(`Local gateway URL must use ws:// (received ${parsedGatewayUrl.protocol})`);
|
|
835
|
+
}
|
|
836
|
+
await assertTcpReachable(parsedGatewayUrl.toString());
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
function buildBridgeDetachArgs(rawFlags = {}) {
|
|
840
|
+
const orderedKeys = [
|
|
841
|
+
'broker-http',
|
|
842
|
+
'broker-ws',
|
|
843
|
+
'pair-code',
|
|
844
|
+
'app-url',
|
|
845
|
+
'device-id',
|
|
846
|
+
'device-token',
|
|
847
|
+
];
|
|
848
|
+
const args = [process.argv[1], 'openclaw', 'bridge'];
|
|
849
|
+
|
|
850
|
+
for (const key of orderedKeys) {
|
|
851
|
+
const value = rawFlags[key];
|
|
852
|
+
if (value === undefined || value === null || value === false) continue;
|
|
853
|
+
if (value === true) {
|
|
854
|
+
args.push(`--${key}`);
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
const text = String(value).trim();
|
|
858
|
+
if (!text) continue;
|
|
859
|
+
args.push(`--${key}`, text);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return args;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function startBridgeDetachedProcess(rawFlags = {}) {
|
|
866
|
+
const args = buildBridgeDetachArgs(rawFlags);
|
|
867
|
+
const child = spawn(process.execPath, args, {
|
|
868
|
+
detached: true,
|
|
869
|
+
stdio: 'ignore',
|
|
870
|
+
});
|
|
871
|
+
child.unref();
|
|
872
|
+
return child.pid;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
async function resolveBridgeRuntimeConfig(flags, bridgeState) {
|
|
876
|
+
const explicitBrokerHttp = String(flags['broker-http'] || '').trim();
|
|
877
|
+
const explicitBrokerWs = String(flags['broker-ws'] || '').trim();
|
|
878
|
+
const appUrl = String(flags['app-url'] || process.env.OOMI_APP_URL || '').trim();
|
|
879
|
+
|
|
880
|
+
let brokerHttp = String(explicitBrokerHttp || process.env.OOMI_CHAT_BROKER_HTTP_URL || bridgeState.brokerHttp || '').trim();
|
|
881
|
+
let brokerWs = String(explicitBrokerWs || process.env.OOMI_CHAT_BROKER_DEVICE_WS_URL || bridgeState.brokerWs || '').trim();
|
|
882
|
+
let managedConfigUsed = false;
|
|
883
|
+
let managedConfigError = '';
|
|
884
|
+
|
|
885
|
+
if (appUrl && (!explicitBrokerHttp || !explicitBrokerWs)) {
|
|
886
|
+
try {
|
|
887
|
+
const managedConfig = await fetchManagedGatewayConfig({ appUrl });
|
|
888
|
+
managedConfigUsed = true;
|
|
889
|
+
if (!explicitBrokerHttp) {
|
|
890
|
+
brokerHttp = String(managedConfig.brokerHttpUrl || '').trim();
|
|
891
|
+
}
|
|
892
|
+
if (!explicitBrokerWs) {
|
|
893
|
+
brokerWs = String(managedConfig.brokerDeviceWsUrl || '').trim();
|
|
894
|
+
}
|
|
895
|
+
} catch (err) {
|
|
896
|
+
managedConfigError = extractErrorMessage(err);
|
|
897
|
+
if (!brokerWs) {
|
|
898
|
+
throw err;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
return {
|
|
904
|
+
appUrl,
|
|
905
|
+
brokerHttp,
|
|
906
|
+
brokerWs,
|
|
907
|
+
managedConfigUsed,
|
|
908
|
+
managedConfigError,
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
|
|
676
912
|
async function startOpenclawBridge(flags) {
|
|
677
913
|
const bridgeState = readBridgeState();
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
bridgeState.brokerHttp ||
|
|
682
|
-
''
|
|
683
|
-
).trim();
|
|
684
|
-
const brokerWs = String(
|
|
685
|
-
flags['broker-ws'] ||
|
|
686
|
-
process.env.OOMI_CHAT_BROKER_DEVICE_WS_URL ||
|
|
687
|
-
bridgeState.brokerWs ||
|
|
688
|
-
''
|
|
689
|
-
).trim();
|
|
914
|
+
const runtimeConfig = await resolveBridgeRuntimeConfig(flags, bridgeState);
|
|
915
|
+
const brokerHttp = runtimeConfig.brokerHttp;
|
|
916
|
+
const brokerWs = runtimeConfig.brokerWs;
|
|
690
917
|
const deviceId = resolveDeviceId(flags, bridgeState);
|
|
691
918
|
const pairCode = String(flags['pair-code'] || '').trim().toUpperCase();
|
|
692
919
|
const explicitDeviceToken = String(flags['device-token'] || '').trim();
|
|
693
|
-
const canReuseStateToken =
|
|
694
|
-
String(bridgeState.deviceId || '').trim() === deviceId &&
|
|
695
|
-
String(bridgeState.brokerWs || '').trim() === brokerWs &&
|
|
696
|
-
String(bridgeState.brokerHttp || '').trim() === brokerHttp;
|
|
697
920
|
let deviceToken = explicitDeviceToken;
|
|
698
|
-
if (!deviceToken &&
|
|
921
|
+
if (!deviceToken && String(bridgeState.deviceId || '').trim() === deviceId) {
|
|
699
922
|
deviceToken = String(bridgeState.deviceToken || '').trim();
|
|
700
923
|
}
|
|
701
924
|
|
|
@@ -730,11 +953,64 @@ async function startOpenclawBridge(flags) {
|
|
|
730
953
|
throw new Error(`Gateway auth token/password not found in ${gateway.configPath}.`);
|
|
731
954
|
}
|
|
732
955
|
|
|
956
|
+
if (runtimeConfig.managedConfigUsed && runtimeConfig.appUrl) {
|
|
957
|
+
console.log(`[bridge] refreshed broker URLs from ${runtimeConfig.appUrl}`);
|
|
958
|
+
} else if (runtimeConfig.managedConfigError) {
|
|
959
|
+
console.warn(
|
|
960
|
+
`[bridge] failed to refresh broker URLs from app URL; using local/state broker config (${runtimeConfig.managedConfigError})`
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
try {
|
|
965
|
+
await runBridgePreflight({
|
|
966
|
+
brokerWs,
|
|
967
|
+
gatewayUrl: gateway.gatewayUrl,
|
|
968
|
+
gatewayConfigPath: gateway.configPath,
|
|
969
|
+
});
|
|
970
|
+
} catch (err) {
|
|
971
|
+
const failure = classifyBridgeFailure({ err });
|
|
972
|
+
updateBridgeStatus({
|
|
973
|
+
status: 'error',
|
|
974
|
+
deviceId,
|
|
975
|
+
brokerWs,
|
|
976
|
+
brokerHttp,
|
|
977
|
+
gatewayUrl: gateway.gatewayUrl,
|
|
978
|
+
lastDisconnectAt: bridgeNowIso(),
|
|
979
|
+
lastErrorCode: failure.errorCode,
|
|
980
|
+
lastErrorClass: failure.failureClass,
|
|
981
|
+
lastErrorMessage: failure.message,
|
|
982
|
+
hint: failure.hint,
|
|
983
|
+
consecutiveFailures: 0,
|
|
984
|
+
pid: process.pid,
|
|
985
|
+
});
|
|
986
|
+
throw new Error(`Bridge preflight failed (${failure.failureClass}): ${failure.message}. ${failure.hint}`);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
updateBridgeStatus({
|
|
990
|
+
status: 'starting',
|
|
991
|
+
deviceId,
|
|
992
|
+
brokerWs,
|
|
993
|
+
brokerHttp,
|
|
994
|
+
gatewayUrl: gateway.gatewayUrl,
|
|
995
|
+
lastErrorCode: '',
|
|
996
|
+
lastErrorClass: '',
|
|
997
|
+
lastErrorMessage: '',
|
|
998
|
+
consecutiveFailures: 0,
|
|
999
|
+
pid: process.pid,
|
|
1000
|
+
startedAt: bridgeNowIso(),
|
|
1001
|
+
});
|
|
1002
|
+
|
|
733
1003
|
console.log(`Starting OpenClaw bridge: device=${deviceId}`);
|
|
734
1004
|
console.log(`Local gateway: ${gateway.gatewayUrl}`);
|
|
735
1005
|
console.log(`Broker WS: ${brokerWs}`);
|
|
736
1006
|
|
|
737
1007
|
const activeGatewaySockets = new Map();
|
|
1008
|
+
const reconnectState = {
|
|
1009
|
+
attempt: 0,
|
|
1010
|
+
timer: null,
|
|
1011
|
+
stopped: false,
|
|
1012
|
+
lastFailure: null,
|
|
1013
|
+
};
|
|
738
1014
|
const brokerPath = (() => {
|
|
739
1015
|
try {
|
|
740
1016
|
return new URL(brokerWs).pathname || '';
|
|
@@ -784,15 +1060,102 @@ async function startOpenclawBridge(flags) {
|
|
|
784
1060
|
return null;
|
|
785
1061
|
};
|
|
786
1062
|
|
|
1063
|
+
const scheduleReconnect = () => {
|
|
1064
|
+
if (reconnectState.stopped || reconnectState.timer) return;
|
|
1065
|
+
reconnectState.attempt += 1;
|
|
1066
|
+
const failure =
|
|
1067
|
+
reconnectState.lastFailure ||
|
|
1068
|
+
classifyBridgeFailure({ reason: 'connection closed without classified error' });
|
|
1069
|
+
const delayMs = computeReconnectDelayMs(reconnectState.attempt, failure.baseDelayMs);
|
|
1070
|
+
|
|
1071
|
+
console.warn(
|
|
1072
|
+
`[bridge] reconnect scheduled in ${delayMs}ms (attempt ${reconnectState.attempt}, class=${failure.failureClass}, code=${failure.errorCode})`
|
|
1073
|
+
);
|
|
1074
|
+
|
|
1075
|
+
updateBridgeStatus({
|
|
1076
|
+
status: 'reconnecting',
|
|
1077
|
+
deviceId,
|
|
1078
|
+
brokerWs,
|
|
1079
|
+
brokerHttp,
|
|
1080
|
+
gatewayUrl: gateway.gatewayUrl,
|
|
1081
|
+
lastDisconnectAt: bridgeNowIso(),
|
|
1082
|
+
lastErrorCode: failure.errorCode,
|
|
1083
|
+
lastErrorClass: failure.failureClass,
|
|
1084
|
+
lastErrorMessage: failure.message,
|
|
1085
|
+
hint: failure.hint,
|
|
1086
|
+
consecutiveFailures: reconnectState.attempt,
|
|
1087
|
+
pid: process.pid,
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
reconnectState.timer = setTimeout(() => {
|
|
1091
|
+
reconnectState.timer = null;
|
|
1092
|
+
connectBroker();
|
|
1093
|
+
}, delayMs);
|
|
1094
|
+
};
|
|
1095
|
+
|
|
787
1096
|
const connectBroker = () => {
|
|
788
1097
|
const wsUrl = new URL(brokerWs);
|
|
789
1098
|
wsUrl.searchParams.set('token', deviceToken);
|
|
790
1099
|
|
|
791
1100
|
const brokerSocket = new WebSocket(wsUrl.toString());
|
|
792
1101
|
let actionCableHeartbeat = null;
|
|
1102
|
+
const setupGatewaySession = (sessionId, sessionBridge) => {
|
|
1103
|
+
if (!sessionBridge || !sessionBridge.socket) return;
|
|
1104
|
+
const gatewaySocket = sessionBridge.socket;
|
|
1105
|
+
|
|
1106
|
+
gatewaySocket.on('open', () => {
|
|
1107
|
+
console.log(`[bridge] gateway.open ${sessionId}`);
|
|
1108
|
+
flushSessionQueue(sessionBridge);
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
gatewaySocket.on('message', (gatewayRaw) => {
|
|
1112
|
+
const frame = typeof gatewayRaw === 'string' ? gatewayRaw : gatewayRaw.toString();
|
|
1113
|
+
sendBrokerPayload(brokerSocket, { action: 'gateway_frame', type: 'gateway.frame', sessionId, frame });
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
gatewaySocket.on('close', (code, reason) => {
|
|
1117
|
+
const reasonText = reason ? reason.toString() : '';
|
|
1118
|
+
console.log(
|
|
1119
|
+
`[bridge] gateway.close ${sessionId} code=${String(code)}${reasonText ? ` reason=${reasonText}` : ''}`
|
|
1120
|
+
);
|
|
1121
|
+
activeGatewaySockets.delete(sessionId);
|
|
1122
|
+
sendBrokerPayload(brokerSocket, {
|
|
1123
|
+
action: 'gateway_closed',
|
|
1124
|
+
type: 'gateway.closed',
|
|
1125
|
+
sessionId,
|
|
1126
|
+
code,
|
|
1127
|
+
reason: reasonText,
|
|
1128
|
+
});
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
gatewaySocket.on('error', (err) => {
|
|
1132
|
+
console.error(`[bridge] gateway.error ${sessionId}: ${String(err)}`);
|
|
1133
|
+
sendBrokerPayload(brokerSocket, {
|
|
1134
|
+
action: 'log',
|
|
1135
|
+
type: 'log',
|
|
1136
|
+
sessionId,
|
|
1137
|
+
level: 'error',
|
|
1138
|
+
message: `Gateway socket error (${sessionId}): ${String(err)}`,
|
|
1139
|
+
});
|
|
1140
|
+
});
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
const getOrCreateGatewaySession = (sessionId) => {
|
|
1144
|
+
const existing = activeGatewaySockets.get(sessionId);
|
|
1145
|
+
if (existing) return existing;
|
|
1146
|
+
const sessionBridge = ensureSessionBridge({
|
|
1147
|
+
sessions: activeGatewaySockets,
|
|
1148
|
+
sessionId,
|
|
1149
|
+
createSocket: () => new WebSocket(gateway.gatewayUrl),
|
|
1150
|
+
});
|
|
1151
|
+
if (sessionBridge) setupGatewaySession(sessionId, sessionBridge);
|
|
1152
|
+
return sessionBridge;
|
|
1153
|
+
};
|
|
793
1154
|
|
|
794
1155
|
brokerSocket.on('open', () => {
|
|
795
1156
|
console.log('[bridge] Connected to managed broker.');
|
|
1157
|
+
reconnectState.attempt = 0;
|
|
1158
|
+
reconnectState.lastFailure = null;
|
|
796
1159
|
if (!actionCableMode) return;
|
|
797
1160
|
brokerSocket.send(
|
|
798
1161
|
JSON.stringify({
|
|
@@ -803,6 +1166,20 @@ async function startOpenclawBridge(flags) {
|
|
|
803
1166
|
actionCableHeartbeat = setInterval(() => {
|
|
804
1167
|
sendBrokerPayload(brokerSocket, { action: 'heartbeat' });
|
|
805
1168
|
}, 15000);
|
|
1169
|
+
updateBridgeStatus({
|
|
1170
|
+
status: 'connected',
|
|
1171
|
+
deviceId,
|
|
1172
|
+
brokerWs,
|
|
1173
|
+
brokerHttp,
|
|
1174
|
+
gatewayUrl: gateway.gatewayUrl,
|
|
1175
|
+
lastConnectedAt: bridgeNowIso(),
|
|
1176
|
+
lastErrorCode: '',
|
|
1177
|
+
lastErrorClass: '',
|
|
1178
|
+
lastErrorMessage: '',
|
|
1179
|
+
hint: '',
|
|
1180
|
+
consecutiveFailures: 0,
|
|
1181
|
+
pid: process.pid,
|
|
1182
|
+
});
|
|
806
1183
|
});
|
|
807
1184
|
|
|
808
1185
|
brokerSocket.on('message', (rawData) => {
|
|
@@ -815,12 +1192,61 @@ async function startOpenclawBridge(flags) {
|
|
|
815
1192
|
}
|
|
816
1193
|
|
|
817
1194
|
if (payload.type === 'broker.disconnect') {
|
|
1195
|
+
reconnectState.lastFailure = classifyBridgeFailure({
|
|
1196
|
+
reason: String(payload.reason || 'unauthorized'),
|
|
1197
|
+
forceClass: 'auth_rejected',
|
|
1198
|
+
});
|
|
1199
|
+
reconnectState.stopped = true;
|
|
818
1200
|
console.error(`[bridge] Broker rejected connection: ${String(payload.reason || 'unauthorized')}`);
|
|
1201
|
+
console.error(`[bridge] ${reconnectState.lastFailure.hint}`);
|
|
1202
|
+
updateBridgeStatus({
|
|
1203
|
+
status: 'error',
|
|
1204
|
+
deviceId,
|
|
1205
|
+
brokerWs,
|
|
1206
|
+
brokerHttp,
|
|
1207
|
+
gatewayUrl: gateway.gatewayUrl,
|
|
1208
|
+
lastDisconnectAt: bridgeNowIso(),
|
|
1209
|
+
lastErrorCode: reconnectState.lastFailure.errorCode,
|
|
1210
|
+
lastErrorClass: reconnectState.lastFailure.failureClass,
|
|
1211
|
+
lastErrorMessage: reconnectState.lastFailure.message,
|
|
1212
|
+
hint: reconnectState.lastFailure.hint,
|
|
1213
|
+
consecutiveFailures: reconnectState.attempt + 1,
|
|
1214
|
+
pid: process.pid,
|
|
1215
|
+
});
|
|
1216
|
+
try {
|
|
1217
|
+
brokerSocket.close(4001, 'auth_rejected');
|
|
1218
|
+
} catch {
|
|
1219
|
+
// no-op
|
|
1220
|
+
}
|
|
819
1221
|
return;
|
|
820
1222
|
}
|
|
821
1223
|
|
|
822
1224
|
if (payload.type === 'broker.reject_subscription') {
|
|
823
1225
|
console.error('[bridge] Broker rejected DeviceChannel subscription.');
|
|
1226
|
+
reconnectState.lastFailure = classifyBridgeFailure({
|
|
1227
|
+
reason: 'broker rejected DeviceChannel subscription',
|
|
1228
|
+
forceClass: 'auth_rejected',
|
|
1229
|
+
});
|
|
1230
|
+
reconnectState.stopped = true;
|
|
1231
|
+
updateBridgeStatus({
|
|
1232
|
+
status: 'error',
|
|
1233
|
+
deviceId,
|
|
1234
|
+
brokerWs,
|
|
1235
|
+
brokerHttp,
|
|
1236
|
+
gatewayUrl: gateway.gatewayUrl,
|
|
1237
|
+
lastDisconnectAt: bridgeNowIso(),
|
|
1238
|
+
lastErrorCode: reconnectState.lastFailure.errorCode,
|
|
1239
|
+
lastErrorClass: reconnectState.lastFailure.failureClass,
|
|
1240
|
+
lastErrorMessage: reconnectState.lastFailure.message,
|
|
1241
|
+
hint: reconnectState.lastFailure.hint,
|
|
1242
|
+
consecutiveFailures: reconnectState.attempt + 1,
|
|
1243
|
+
pid: process.pid,
|
|
1244
|
+
});
|
|
1245
|
+
try {
|
|
1246
|
+
brokerSocket.close(4002, 'subscription_rejected');
|
|
1247
|
+
} catch {
|
|
1248
|
+
// no-op
|
|
1249
|
+
}
|
|
824
1250
|
return;
|
|
825
1251
|
}
|
|
826
1252
|
|
|
@@ -831,55 +1257,9 @@ async function startOpenclawBridge(flags) {
|
|
|
831
1257
|
|
|
832
1258
|
if (payload.type === 'client.open') {
|
|
833
1259
|
const sessionId = String(payload.sessionId || '').trim();
|
|
834
|
-
if (!sessionId
|
|
1260
|
+
if (!sessionId) return;
|
|
835
1261
|
console.log(`[bridge] client.open ${sessionId}`);
|
|
836
|
-
|
|
837
|
-
const sessionBridge = {
|
|
838
|
-
socket: gatewaySocket,
|
|
839
|
-
queue: [],
|
|
840
|
-
};
|
|
841
|
-
activeGatewaySockets.set(sessionId, sessionBridge);
|
|
842
|
-
|
|
843
|
-
gatewaySocket.on('open', () => {
|
|
844
|
-
console.log(`[bridge] gateway.open ${sessionId}`);
|
|
845
|
-
while (sessionBridge.queue.length > 0 && gatewaySocket.readyState === WebSocket.OPEN) {
|
|
846
|
-
const nextFrame = sessionBridge.queue.shift();
|
|
847
|
-
if (typeof nextFrame === 'string' && nextFrame) {
|
|
848
|
-
gatewaySocket.send(nextFrame);
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
});
|
|
852
|
-
|
|
853
|
-
gatewaySocket.on('message', (gatewayRaw) => {
|
|
854
|
-
const frame = typeof gatewayRaw === 'string' ? gatewayRaw : gatewayRaw.toString();
|
|
855
|
-
sendBrokerPayload(brokerSocket, { action: 'gateway_frame', type: 'gateway.frame', sessionId, frame });
|
|
856
|
-
});
|
|
857
|
-
|
|
858
|
-
gatewaySocket.on('close', (code, reason) => {
|
|
859
|
-
const reasonText = reason ? reason.toString() : '';
|
|
860
|
-
console.log(
|
|
861
|
-
`[bridge] gateway.close ${sessionId} code=${String(code)}${reasonText ? ` reason=${reasonText}` : ''}`
|
|
862
|
-
);
|
|
863
|
-
activeGatewaySockets.delete(sessionId);
|
|
864
|
-
sendBrokerPayload(brokerSocket, {
|
|
865
|
-
action: 'gateway_closed',
|
|
866
|
-
type: 'gateway.closed',
|
|
867
|
-
sessionId,
|
|
868
|
-
code,
|
|
869
|
-
reason: reasonText,
|
|
870
|
-
});
|
|
871
|
-
});
|
|
872
|
-
|
|
873
|
-
gatewaySocket.on('error', (err) => {
|
|
874
|
-
console.error(`[bridge] gateway.error ${sessionId}: ${String(err)}`);
|
|
875
|
-
sendBrokerPayload(brokerSocket, {
|
|
876
|
-
action: 'log',
|
|
877
|
-
type: 'log',
|
|
878
|
-
sessionId,
|
|
879
|
-
level: 'error',
|
|
880
|
-
message: `Gateway socket error (${sessionId}): ${String(err)}`,
|
|
881
|
-
});
|
|
882
|
-
});
|
|
1262
|
+
getOrCreateGatewaySession(sessionId);
|
|
883
1263
|
return;
|
|
884
1264
|
}
|
|
885
1265
|
|
|
@@ -888,19 +1268,13 @@ async function startOpenclawBridge(flags) {
|
|
|
888
1268
|
const frame = typeof payload.frame === 'string' ? payload.frame : '';
|
|
889
1269
|
if (!sessionId || !frame) return;
|
|
890
1270
|
console.log(`[bridge] client.frame ${sessionId}`);
|
|
891
|
-
const sessionBridge =
|
|
892
|
-
if (!sessionBridge
|
|
893
|
-
console.log(`[bridge] client.frame dropped (no session) ${sessionId}`);
|
|
894
|
-
return;
|
|
895
|
-
}
|
|
896
|
-
const gatewaySocket = sessionBridge.socket;
|
|
1271
|
+
const sessionBridge = getOrCreateGatewaySession(sessionId);
|
|
1272
|
+
if (!sessionBridge) return;
|
|
897
1273
|
const frameWithAuth = injectGatewayAuth(frame, gateway);
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
} else if (gatewaySocket.readyState === WebSocket.CONNECTING) {
|
|
1274
|
+
const result = forwardFrameToSession(sessionBridge, frameWithAuth);
|
|
1275
|
+
if (result === 'queued') {
|
|
901
1276
|
console.log(`[bridge] client.frame queued ${sessionId}`);
|
|
902
|
-
|
|
903
|
-
} else {
|
|
1277
|
+
} else if (result === 'dropped') {
|
|
904
1278
|
console.log(`[bridge] client.frame dropped (socket not open) ${sessionId}`);
|
|
905
1279
|
}
|
|
906
1280
|
return;
|
|
@@ -923,7 +1297,8 @@ async function startOpenclawBridge(flags) {
|
|
|
923
1297
|
clearInterval(actionCableHeartbeat);
|
|
924
1298
|
actionCableHeartbeat = null;
|
|
925
1299
|
}
|
|
926
|
-
|
|
1300
|
+
const reasonText = reason ? reason.toString() : '';
|
|
1301
|
+
console.log(`[bridge] Broker disconnected (${code}) ${reasonText}`);
|
|
927
1302
|
for (const [sessionId, sessionBridge] of activeGatewaySockets.entries()) {
|
|
928
1303
|
activeGatewaySockets.delete(sessionId);
|
|
929
1304
|
try {
|
|
@@ -932,14 +1307,49 @@ async function startOpenclawBridge(flags) {
|
|
|
932
1307
|
// no-op
|
|
933
1308
|
}
|
|
934
1309
|
}
|
|
935
|
-
|
|
1310
|
+
|
|
1311
|
+
if (reconnectState.stopped) {
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
if (!reconnectState.lastFailure) {
|
|
1316
|
+
reconnectState.lastFailure = classifyBridgeFailure({
|
|
1317
|
+
reason: `socket closed code=${String(code)}${reasonText ? ` reason=${reasonText}` : ''}`,
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
scheduleReconnect();
|
|
936
1321
|
});
|
|
937
1322
|
|
|
938
1323
|
brokerSocket.on('error', (err) => {
|
|
939
|
-
|
|
1324
|
+
reconnectState.lastFailure = classifyBridgeFailure({ err });
|
|
1325
|
+
console.error(
|
|
1326
|
+
`[bridge] Broker socket error [${reconnectState.lastFailure.failureClass}/${reconnectState.lastFailure.errorCode}]: ${reconnectState.lastFailure.message}`
|
|
1327
|
+
);
|
|
1328
|
+
console.error(`[bridge] ${reconnectState.lastFailure.hint}`);
|
|
940
1329
|
});
|
|
941
1330
|
};
|
|
942
1331
|
|
|
1332
|
+
const markStopped = (signal) => {
|
|
1333
|
+
reconnectState.stopped = true;
|
|
1334
|
+
if (reconnectState.timer) {
|
|
1335
|
+
clearTimeout(reconnectState.timer);
|
|
1336
|
+
reconnectState.timer = null;
|
|
1337
|
+
}
|
|
1338
|
+
updateBridgeStatus({
|
|
1339
|
+
status: 'stopped',
|
|
1340
|
+
deviceId,
|
|
1341
|
+
brokerWs,
|
|
1342
|
+
brokerHttp,
|
|
1343
|
+
gatewayUrl: gateway.gatewayUrl,
|
|
1344
|
+
lastDisconnectAt: bridgeNowIso(),
|
|
1345
|
+
stopSignal: signal,
|
|
1346
|
+
pid: process.pid,
|
|
1347
|
+
});
|
|
1348
|
+
process.exit(0);
|
|
1349
|
+
};
|
|
1350
|
+
process.once('SIGINT', () => markStopped('SIGINT'));
|
|
1351
|
+
process.once('SIGTERM', () => markStopped('SIGTERM'));
|
|
1352
|
+
|
|
943
1353
|
connectBroker();
|
|
944
1354
|
await new Promise(() => {});
|
|
945
1355
|
}
|
|
@@ -1029,25 +1439,13 @@ async function pairAndStartOpenclawBridge(flags) {
|
|
|
1029
1439
|
}
|
|
1030
1440
|
|
|
1031
1441
|
if (detach) {
|
|
1032
|
-
const
|
|
1033
|
-
|
|
1034
|
-
'
|
|
1035
|
-
'
|
|
1036
|
-
'
|
|
1037
|
-
managedConfig.brokerHttpUrl,
|
|
1038
|
-
'--broker-ws',
|
|
1039
|
-
brokerWs,
|
|
1040
|
-
'--device-id',
|
|
1041
|
-
deviceId,
|
|
1042
|
-
'--device-token',
|
|
1043
|
-
deviceToken,
|
|
1044
|
-
];
|
|
1045
|
-
const child = spawn(process.execPath, args, {
|
|
1046
|
-
detached: true,
|
|
1047
|
-
stdio: 'ignore',
|
|
1442
|
+
const pid = startBridgeDetachedProcess({
|
|
1443
|
+
'broker-http': managedConfig.brokerHttpUrl,
|
|
1444
|
+
'broker-ws': brokerWs,
|
|
1445
|
+
'device-id': deviceId,
|
|
1446
|
+
'device-token': deviceToken,
|
|
1048
1447
|
});
|
|
1049
|
-
|
|
1050
|
-
console.log(`Bridge started in background (pid: ${child.pid}).`);
|
|
1448
|
+
console.log(`Bridge started in background (pid: ${pid}).`);
|
|
1051
1449
|
return;
|
|
1052
1450
|
}
|
|
1053
1451
|
|
|
@@ -1115,6 +1513,69 @@ async function createOpenclawInviteLink(flags) {
|
|
|
1115
1513
|
}
|
|
1116
1514
|
}
|
|
1117
1515
|
|
|
1516
|
+
function printOpenclawBridgeStatus(flags) {
|
|
1517
|
+
const bridgeState = readBridgeState();
|
|
1518
|
+
const runtimeStatus = readBridgeStatus();
|
|
1519
|
+
const jsonOutput = isTruthyFlag(flags.json);
|
|
1520
|
+
const redactToken = (value) => {
|
|
1521
|
+
const text = String(value || '').trim();
|
|
1522
|
+
if (!text) return '';
|
|
1523
|
+
if (text.length <= 12) return '***';
|
|
1524
|
+
return `${text.slice(0, 6)}...${text.slice(-6)}`;
|
|
1525
|
+
};
|
|
1526
|
+
|
|
1527
|
+
const payload = {
|
|
1528
|
+
bridgeStatePath: resolveBridgeStatePath(),
|
|
1529
|
+
bridgeStatusPath: resolveBridgeStatusPath(),
|
|
1530
|
+
bridgeState: {
|
|
1531
|
+
brokerHttp: String(bridgeState.brokerHttp || ''),
|
|
1532
|
+
brokerWs: String(bridgeState.brokerWs || ''),
|
|
1533
|
+
deviceId: String(bridgeState.deviceId || ''),
|
|
1534
|
+
deviceToken: redactToken(bridgeState.deviceToken),
|
|
1535
|
+
claimedAt: bridgeState.claimedAt || null,
|
|
1536
|
+
expiresAt: bridgeState.expiresAt || null,
|
|
1537
|
+
},
|
|
1538
|
+
runtime: runtimeStatus,
|
|
1539
|
+
};
|
|
1540
|
+
|
|
1541
|
+
if (jsonOutput) {
|
|
1542
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
console.log('Oomi Bridge Status');
|
|
1547
|
+
console.log('------------------');
|
|
1548
|
+
console.log(`Bridge state: ${payload.bridgeStatePath}`);
|
|
1549
|
+
console.log(`Runtime status: ${payload.bridgeStatusPath}`);
|
|
1550
|
+
console.log(`Device: ${payload.bridgeState.deviceId || 'not paired'}`);
|
|
1551
|
+
console.log(`Broker HTTP: ${payload.bridgeState.brokerHttp || 'not configured'}`);
|
|
1552
|
+
console.log(`Broker WS: ${payload.bridgeState.brokerWs || 'not configured'}`);
|
|
1553
|
+
if (payload.bridgeState.deviceToken) {
|
|
1554
|
+
console.log(`Device token: ${payload.bridgeState.deviceToken}`);
|
|
1555
|
+
}
|
|
1556
|
+
if (payload.runtime && typeof payload.runtime === 'object' && Object.keys(payload.runtime).length > 0) {
|
|
1557
|
+
console.log(`Runtime state: ${String(payload.runtime.status || 'unknown')}`);
|
|
1558
|
+
if (payload.runtime.lastConnectedAt) {
|
|
1559
|
+
console.log(`Last connected: ${payload.runtime.lastConnectedAt}`);
|
|
1560
|
+
}
|
|
1561
|
+
if (payload.runtime.lastDisconnectAt) {
|
|
1562
|
+
console.log(`Last disconnected: ${payload.runtime.lastDisconnectAt}`);
|
|
1563
|
+
}
|
|
1564
|
+
if (payload.runtime.lastErrorClass || payload.runtime.lastErrorCode || payload.runtime.lastErrorMessage) {
|
|
1565
|
+
console.log(
|
|
1566
|
+
`Last error: ${String(payload.runtime.lastErrorClass || 'unknown')}/${String(payload.runtime.lastErrorCode || 'UNKNOWN')} ${String(payload.runtime.lastErrorMessage || '').trim()}`
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
if (payload.runtime.hint) {
|
|
1570
|
+
console.log(`Hint: ${payload.runtime.hint}`);
|
|
1571
|
+
}
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
console.log('Runtime state: no bridge runtime status recorded yet.');
|
|
1576
|
+
console.log('Run: oomi openclaw bridge --app-url https://www.oomi.ai');
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1118
1579
|
function printOpenclawPluginSetup(flags) {
|
|
1119
1580
|
const bridgeState = readBridgeState();
|
|
1120
1581
|
const backendUrl = String(
|
|
@@ -1214,6 +1675,13 @@ async function main() {
|
|
|
1214
1675
|
}
|
|
1215
1676
|
|
|
1216
1677
|
if (command === 'openclaw' && subcommand === 'bridge') {
|
|
1678
|
+
if (Boolean(args.flags.detach)) {
|
|
1679
|
+
const detachedFlags = { ...args.flags };
|
|
1680
|
+
delete detachedFlags.detach;
|
|
1681
|
+
const pid = startBridgeDetachedProcess(detachedFlags);
|
|
1682
|
+
console.log(`Bridge started in background (pid: ${pid}).`);
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1217
1685
|
await startOpenclawBridge(args.flags);
|
|
1218
1686
|
return;
|
|
1219
1687
|
}
|
|
@@ -1228,6 +1696,11 @@ async function main() {
|
|
|
1228
1696
|
return;
|
|
1229
1697
|
}
|
|
1230
1698
|
|
|
1699
|
+
if (command === 'openclaw' && subcommand === 'status') {
|
|
1700
|
+
printOpenclawBridgeStatus(args.flags);
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1231
1704
|
if (command === 'openclaw' && subcommand === 'plugin') {
|
|
1232
1705
|
printOpenclawPluginSetup(args.flags);
|
|
1233
1706
|
return;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const WS_CONNECTING = 0;
|
|
2
|
+
const WS_OPEN = 1;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Ensure session state exists so client frames can be buffered before client.open arrives.
|
|
6
|
+
*/
|
|
7
|
+
export function ensureSessionBridge({ sessions, sessionId, createSocket }) {
|
|
8
|
+
const id = String(sessionId || '').trim();
|
|
9
|
+
if (!id) return null;
|
|
10
|
+
|
|
11
|
+
const existing = sessions.get(id);
|
|
12
|
+
if (existing) return existing;
|
|
13
|
+
|
|
14
|
+
const socket = createSocket(id);
|
|
15
|
+
const next = { socket, queue: [] };
|
|
16
|
+
sessions.set(id, next);
|
|
17
|
+
return next;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Forward a frame to the gateway socket or queue it while connecting.
|
|
22
|
+
*/
|
|
23
|
+
export function forwardFrameToSession(sessionBridge, frameText) {
|
|
24
|
+
if (!sessionBridge || !sessionBridge.socket || typeof frameText !== 'string' || !frameText) {
|
|
25
|
+
return 'dropped';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { socket } = sessionBridge;
|
|
29
|
+
if (socket.readyState === WS_OPEN) {
|
|
30
|
+
socket.send(frameText);
|
|
31
|
+
return 'sent';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (socket.readyState === WS_CONNECTING) {
|
|
35
|
+
sessionBridge.queue.push(frameText);
|
|
36
|
+
return 'queued';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return 'dropped';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function flushSessionQueue(sessionBridge) {
|
|
43
|
+
if (!sessionBridge || !sessionBridge.socket) return;
|
|
44
|
+
const socket = sessionBridge.socket;
|
|
45
|
+
while (sessionBridge.queue.length > 0 && socket.readyState === WS_OPEN) {
|
|
46
|
+
const nextFrame = sessionBridge.queue.shift();
|
|
47
|
+
if (typeof nextFrame === 'string' && nextFrame) {
|
|
48
|
+
socket.send(nextFrame);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
package/openclaw.extension.js
CHANGED
|
@@ -154,7 +154,12 @@ async function postJson({ url, token, body, timeoutMs }) {
|
|
|
154
154
|
const oomiChannelPlugin = {
|
|
155
155
|
id: CHANNEL_ID,
|
|
156
156
|
meta: {
|
|
157
|
-
|
|
157
|
+
label: 'Oomi',
|
|
158
|
+
selectionLabel: 'Oomi (Managed)',
|
|
159
|
+
docsPath: '/channels/oomi',
|
|
160
|
+
docsLabel: 'oomi',
|
|
161
|
+
blurb: 'Managed channel transport for Oomi chat.',
|
|
162
|
+
aliases: ['oomi-ai'],
|
|
158
163
|
description: 'Managed Oomi channel plugin.',
|
|
159
164
|
},
|
|
160
165
|
capabilities: {
|
|
@@ -167,15 +172,22 @@ const oomiChannelPlugin = {
|
|
|
167
172
|
threads: true,
|
|
168
173
|
},
|
|
169
174
|
|
|
170
|
-
config
|
|
171
|
-
|
|
172
|
-
|
|
175
|
+
config: {
|
|
176
|
+
listAccountIds(cfg) {
|
|
177
|
+
const normalized = normalizeConfig(cfg);
|
|
178
|
+
return Object.entries(normalized.accounts)
|
|
179
|
+
.filter(([, account]) => account.enabled !== false)
|
|
180
|
+
.map(([accountId]) => accountId);
|
|
181
|
+
},
|
|
173
182
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
183
|
+
resolveAccount(cfg, accountId) {
|
|
184
|
+
const { accountId: resolvedAccountId, account } = resolveAccount(cfg, accountId);
|
|
185
|
+
if (!account) return null;
|
|
186
|
+
return {
|
|
187
|
+
id: resolvedAccountId,
|
|
188
|
+
...account,
|
|
189
|
+
};
|
|
190
|
+
},
|
|
179
191
|
},
|
|
180
192
|
|
|
181
193
|
outbound: {
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oomi-ai",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Oomi CLI for OpenClaw setup",
|
|
5
5
|
"bin": {
|
|
6
6
|
"oomi": "bin/oomi-ai.js"
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
"url": "https://github.com/crispcode-io/oomi/issues"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
|
-
"check": "node --check bin/oomi-ai.js"
|
|
49
|
+
"check": "node --check bin/oomi-ai.js",
|
|
50
|
+
"test": "node --test test/sessionBridgeState.test.mjs"
|
|
50
51
|
},
|
|
51
52
|
"dependencies": {
|
|
52
53
|
"ws": "^8.19.0"
|
|
@@ -54,6 +55,7 @@
|
|
|
54
55
|
"license": "MIT",
|
|
55
56
|
"files": [
|
|
56
57
|
"bin/oomi-ai.js",
|
|
58
|
+
"bin/sessionBridgeState.js",
|
|
57
59
|
"openclaw.plugin.json",
|
|
58
60
|
"openclaw.extension.js",
|
|
59
61
|
"agent_instructions.md",
|