aicq-openclaw-plugin 1.2.0 → 1.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/dist/index.js +552 -98
- package/openclaw.plugin.json +2 -2
- package/package.json +13 -11
package/dist/index.js
CHANGED
|
@@ -4922,9 +4922,9 @@ var require_lib = __commonJS({
|
|
|
4922
4922
|
}
|
|
4923
4923
|
});
|
|
4924
4924
|
|
|
4925
|
-
// node_modules/tweetnacl/nacl-fast.js
|
|
4925
|
+
// ../node_modules/tweetnacl/nacl-fast.js
|
|
4926
4926
|
var require_nacl_fast = __commonJS({
|
|
4927
|
-
"node_modules/tweetnacl/nacl-fast.js"(exports, module) {
|
|
4927
|
+
"../node_modules/tweetnacl/nacl-fast.js"(exports, module) {
|
|
4928
4928
|
(function(nacl3) {
|
|
4929
4929
|
"use strict";
|
|
4930
4930
|
var gf = function(init) {
|
|
@@ -7146,9 +7146,9 @@ var require_nacl_fast = __commonJS({
|
|
|
7146
7146
|
}
|
|
7147
7147
|
});
|
|
7148
7148
|
|
|
7149
|
-
// node_modules/tweetnacl-util/nacl-util.js
|
|
7149
|
+
// ../node_modules/tweetnacl-util/nacl-util.js
|
|
7150
7150
|
var require_nacl_util = __commonJS({
|
|
7151
|
-
"node_modules/tweetnacl-util/nacl-util.js"(exports, module) {
|
|
7151
|
+
"../node_modules/tweetnacl-util/nacl-util.js"(exports, module) {
|
|
7152
7152
|
(function(root, f) {
|
|
7153
7153
|
"use strict";
|
|
7154
7154
|
if (typeof module !== "undefined" && module.exports) module.exports = f();
|
|
@@ -7212,9 +7212,9 @@ var require_nacl_util = __commonJS({
|
|
|
7212
7212
|
}
|
|
7213
7213
|
});
|
|
7214
7214
|
|
|
7215
|
-
//
|
|
7215
|
+
// ../shared/crypto/dist/nacl.js
|
|
7216
7216
|
var require_nacl = __commonJS({
|
|
7217
|
-
"
|
|
7217
|
+
"../shared/crypto/dist/nacl.js"(exports) {
|
|
7218
7218
|
"use strict";
|
|
7219
7219
|
var __importDefault = exports && exports.__importDefault || function(mod) {
|
|
7220
7220
|
return mod && mod.__esModule ? mod : { "default": mod };
|
|
@@ -7231,9 +7231,9 @@ var require_nacl = __commonJS({
|
|
|
7231
7231
|
}
|
|
7232
7232
|
});
|
|
7233
7233
|
|
|
7234
|
-
//
|
|
7234
|
+
// ../shared/crypto/dist/keygen.js
|
|
7235
7235
|
var require_keygen = __commonJS({
|
|
7236
|
-
"
|
|
7236
|
+
"../shared/crypto/dist/keygen.js"(exports) {
|
|
7237
7237
|
"use strict";
|
|
7238
7238
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7239
7239
|
exports.generateSigningKeyPair = generateSigningKeyPair2;
|
|
@@ -7272,9 +7272,9 @@ var require_keygen = __commonJS({
|
|
|
7272
7272
|
}
|
|
7273
7273
|
});
|
|
7274
7274
|
|
|
7275
|
-
//
|
|
7275
|
+
// ../shared/crypto/dist/signer.js
|
|
7276
7276
|
var require_signer = __commonJS({
|
|
7277
|
-
"
|
|
7277
|
+
"../shared/crypto/dist/signer.js"(exports) {
|
|
7278
7278
|
"use strict";
|
|
7279
7279
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7280
7280
|
exports.sign = sign;
|
|
@@ -7289,9 +7289,9 @@ var require_signer = __commonJS({
|
|
|
7289
7289
|
}
|
|
7290
7290
|
});
|
|
7291
7291
|
|
|
7292
|
-
//
|
|
7292
|
+
// ../shared/crypto/dist/keyExchange.js
|
|
7293
7293
|
var require_keyExchange = __commonJS({
|
|
7294
|
-
"
|
|
7294
|
+
"../shared/crypto/dist/keyExchange.js"(exports) {
|
|
7295
7295
|
"use strict";
|
|
7296
7296
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7297
7297
|
exports.computeSharedSecret = computeSharedSecret2;
|
|
@@ -7342,9 +7342,9 @@ var require_keyExchange = __commonJS({
|
|
|
7342
7342
|
}
|
|
7343
7343
|
});
|
|
7344
7344
|
|
|
7345
|
-
//
|
|
7345
|
+
// ../shared/crypto/dist/cipher.js
|
|
7346
7346
|
var require_cipher = __commonJS({
|
|
7347
|
-
"
|
|
7347
|
+
"../shared/crypto/dist/cipher.js"(exports) {
|
|
7348
7348
|
"use strict";
|
|
7349
7349
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7350
7350
|
exports.generateNonce = generateNonce;
|
|
@@ -7368,9 +7368,9 @@ var require_cipher = __commonJS({
|
|
|
7368
7368
|
}
|
|
7369
7369
|
});
|
|
7370
7370
|
|
|
7371
|
-
//
|
|
7371
|
+
// ../shared/crypto/dist/message.js
|
|
7372
7372
|
var require_message = __commonJS({
|
|
7373
|
-
"
|
|
7373
|
+
"../shared/crypto/dist/message.js"(exports) {
|
|
7374
7374
|
"use strict";
|
|
7375
7375
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7376
7376
|
exports.createMessage = createMessage;
|
|
@@ -7484,9 +7484,9 @@ var require_message = __commonJS({
|
|
|
7484
7484
|
}
|
|
7485
7485
|
});
|
|
7486
7486
|
|
|
7487
|
-
//
|
|
7487
|
+
// ../shared/crypto/dist/password.js
|
|
7488
7488
|
var require_password = __commonJS({
|
|
7489
|
-
"
|
|
7489
|
+
"../shared/crypto/dist/password.js"(exports) {
|
|
7490
7490
|
"use strict";
|
|
7491
7491
|
var __createBinding = exports && exports.__createBinding || (Object.create ? (function(o, m, k, k2) {
|
|
7492
7492
|
if (k2 === void 0) k2 = k;
|
|
@@ -7564,9 +7564,9 @@ var require_password = __commonJS({
|
|
|
7564
7564
|
}
|
|
7565
7565
|
});
|
|
7566
7566
|
|
|
7567
|
-
//
|
|
7567
|
+
// ../shared/crypto/dist/handshake.js
|
|
7568
7568
|
var require_handshake = __commonJS({
|
|
7569
|
-
"
|
|
7569
|
+
"../shared/crypto/dist/handshake.js"(exports) {
|
|
7570
7570
|
"use strict";
|
|
7571
7571
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7572
7572
|
exports.createHandshakeRequest = createHandshakeRequest2;
|
|
@@ -7675,9 +7675,9 @@ var require_handshake = __commonJS({
|
|
|
7675
7675
|
}
|
|
7676
7676
|
});
|
|
7677
7677
|
|
|
7678
|
-
//
|
|
7679
|
-
var
|
|
7680
|
-
"
|
|
7678
|
+
// ../shared/crypto/dist/index.js
|
|
7679
|
+
var require_dist = __commonJS({
|
|
7680
|
+
"../shared/crypto/dist/index.js"(exports) {
|
|
7681
7681
|
"use strict";
|
|
7682
7682
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7683
7683
|
exports.completeHandshake = exports.createHandshakeResponse = exports.createHandshakeRequest = exports.decryptWithPassword = exports.encryptWithPassword = exports.decryptMessage = exports.encryptMessage = exports.parseMessage = exports.createMessage = exports.generateNonce = exports.decrypt = exports.encrypt = exports.deriveSessionKey = exports.computeSharedSecret = exports.verify = exports.sign = exports.getPublicKeyFingerprint = exports.deriveX25519FromEd25519 = exports.generateKeyExchangeKeyPair = exports.generateSigningKeyPair = exports.encodeBase64 = exports.decodeBase64 = exports.encodeUTF8 = exports.decodeUTF8 = exports.nacl = void 0;
|
|
@@ -7913,9 +7913,13 @@ var PluginStore = class {
|
|
|
7913
7913
|
this.tempNumbers = [];
|
|
7914
7914
|
this.pendingRequests = [];
|
|
7915
7915
|
this.pendingHandshakes = /* @__PURE__ */ new Map();
|
|
7916
|
+
this.offlineMessages = [];
|
|
7916
7917
|
this.dataDir = "";
|
|
7917
7918
|
this.storePath = "";
|
|
7918
7919
|
this.encryptionSalt = crypto3.randomBytes(16);
|
|
7920
|
+
this.saveTimer = null;
|
|
7921
|
+
this.saveDebounceMs = 1e3;
|
|
7922
|
+
this.dirty = false;
|
|
7919
7923
|
}
|
|
7920
7924
|
/**
|
|
7921
7925
|
* Set the data directory for persistence.
|
|
@@ -7934,12 +7938,38 @@ var PluginStore = class {
|
|
|
7934
7938
|
return deriveEncryptionKey(this.agentId || "default-aicq-node", this.encryptionSalt);
|
|
7935
7939
|
}
|
|
7936
7940
|
/**
|
|
7937
|
-
*
|
|
7941
|
+
* Mark the store as dirty and schedule a debounced save.
|
|
7942
|
+
* For operations that need immediate persistence (e.g., new keys), use saveNow().
|
|
7943
|
+
*/
|
|
7944
|
+
markDirty() {
|
|
7945
|
+
this.dirty = true;
|
|
7946
|
+
if (!this.saveTimer) {
|
|
7947
|
+
this.saveTimer = setTimeout(() => {
|
|
7948
|
+
this.saveTimer = null;
|
|
7949
|
+
this.flushSave();
|
|
7950
|
+
}, this.saveDebounceMs);
|
|
7951
|
+
}
|
|
7952
|
+
}
|
|
7953
|
+
/**
|
|
7954
|
+
* Save the current state to disk immediately (atomic write).
|
|
7938
7955
|
*
|
|
7939
|
-
*
|
|
7940
|
-
* derived from the node ID and
|
|
7956
|
+
* Uses write-to-temp + rename for crash safety. Secret keys are encrypted
|
|
7957
|
+
* at rest using AES-256-GCM with a key derived from the node ID and salt.
|
|
7941
7958
|
*/
|
|
7942
|
-
|
|
7959
|
+
saveNow() {
|
|
7960
|
+
if (this.saveTimer) {
|
|
7961
|
+
clearTimeout(this.saveTimer);
|
|
7962
|
+
this.saveTimer = null;
|
|
7963
|
+
}
|
|
7964
|
+
this.flushSave();
|
|
7965
|
+
}
|
|
7966
|
+
/**
|
|
7967
|
+
* Internal save implementation — writes atomically via temp file + rename.
|
|
7968
|
+
*/
|
|
7969
|
+
flushSave() {
|
|
7970
|
+
if (!this.dirty && this.saveTimer === null)
|
|
7971
|
+
return;
|
|
7972
|
+
this.dirty = false;
|
|
7943
7973
|
if (!this.storePath)
|
|
7944
7974
|
return;
|
|
7945
7975
|
const encKey = this.getEncryptionKey();
|
|
@@ -7980,25 +8010,43 @@ var PluginStore = class {
|
|
|
7980
8010
|
requesterId: p.requesterId,
|
|
7981
8011
|
tempNumber: p.tempNumber,
|
|
7982
8012
|
timestamp: p.timestamp.toISOString()
|
|
7983
|
-
}))
|
|
8013
|
+
})),
|
|
8014
|
+
offlineMessages: this.offlineMessages
|
|
7984
8015
|
};
|
|
7985
8016
|
try {
|
|
7986
|
-
|
|
8017
|
+
const tmpPath = this.storePath + ".tmp";
|
|
8018
|
+
const jsonStr = JSON.stringify(serialized, null, 2);
|
|
8019
|
+
fs2.writeFileSync(tmpPath, jsonStr, "utf-8");
|
|
8020
|
+
fs2.renameSync(tmpPath, this.storePath);
|
|
7987
8021
|
} catch (err) {
|
|
7988
8022
|
console.error("[PluginStore] Failed to save state:", err);
|
|
7989
8023
|
}
|
|
7990
8024
|
}
|
|
8025
|
+
/**
|
|
8026
|
+
* @deprecated Use markDirty() or saveNow() for better performance.
|
|
8027
|
+
* Kept for backward compatibility — performs immediate save.
|
|
8028
|
+
*/
|
|
8029
|
+
save() {
|
|
8030
|
+
this.saveNow();
|
|
8031
|
+
}
|
|
7991
8032
|
/**
|
|
7992
8033
|
* Load state from disk, if available.
|
|
7993
8034
|
*
|
|
7994
8035
|
* Decrypts secret keys using AES-256-GCM with the stored salt and
|
|
7995
|
-
* node ID.
|
|
8036
|
+
* node ID. Falls back to plaintext if the data was stored before
|
|
7996
8037
|
* encryption was added (backward compatibility).
|
|
7997
8038
|
*/
|
|
7998
8039
|
load() {
|
|
7999
8040
|
if (!this.storePath || !fs2.existsSync(this.storePath)) {
|
|
8000
8041
|
return false;
|
|
8001
8042
|
}
|
|
8043
|
+
const tmpPath = this.storePath + ".tmp";
|
|
8044
|
+
if (!fs2.existsSync(this.storePath) && fs2.existsSync(tmpPath)) {
|
|
8045
|
+
try {
|
|
8046
|
+
fs2.renameSync(tmpPath, this.storePath);
|
|
8047
|
+
} catch {
|
|
8048
|
+
}
|
|
8049
|
+
}
|
|
8002
8050
|
try {
|
|
8003
8051
|
const raw = fs2.readFileSync(this.storePath, "utf-8");
|
|
8004
8052
|
const data = JSON.parse(raw);
|
|
@@ -8061,18 +8109,101 @@ var PluginStore = class {
|
|
|
8061
8109
|
tempNumber: p.tempNumber,
|
|
8062
8110
|
timestamp: new Date(p.timestamp)
|
|
8063
8111
|
}));
|
|
8112
|
+
this.offlineMessages = (data.offlineMessages || []).map((m) => ({
|
|
8113
|
+
id: m.id,
|
|
8114
|
+
targetId: m.targetId,
|
|
8115
|
+
encryptedData: m.encryptedData,
|
|
8116
|
+
timestamp: m.timestamp,
|
|
8117
|
+
retryCount: m.retryCount || 0,
|
|
8118
|
+
maxRetries: m.maxRetries || 10
|
|
8119
|
+
}));
|
|
8120
|
+
if (fs2.existsSync(tmpPath)) {
|
|
8121
|
+
try {
|
|
8122
|
+
fs2.unlinkSync(tmpPath);
|
|
8123
|
+
} catch {
|
|
8124
|
+
}
|
|
8125
|
+
}
|
|
8064
8126
|
return true;
|
|
8065
8127
|
} catch (err) {
|
|
8066
8128
|
console.error("[PluginStore] Failed to load state:", err);
|
|
8129
|
+
if (fs2.existsSync(tmpPath)) {
|
|
8130
|
+
console.info("[PluginStore] Attempting recovery from temp file...");
|
|
8131
|
+
try {
|
|
8132
|
+
fs2.renameSync(tmpPath, this.storePath);
|
|
8133
|
+
return this.load();
|
|
8134
|
+
} catch {
|
|
8135
|
+
console.error("[PluginStore] Recovery from temp file failed");
|
|
8136
|
+
}
|
|
8137
|
+
}
|
|
8067
8138
|
return false;
|
|
8068
8139
|
}
|
|
8069
8140
|
}
|
|
8141
|
+
// ----------------------------------------------------------------
|
|
8142
|
+
// Offline message queue
|
|
8143
|
+
// ----------------------------------------------------------------
|
|
8144
|
+
/**
|
|
8145
|
+
* Add a message to the offline queue for later delivery.
|
|
8146
|
+
*/
|
|
8147
|
+
enqueueOfflineMessage(targetId, encryptedData) {
|
|
8148
|
+
const msg = {
|
|
8149
|
+
id: v4_default(),
|
|
8150
|
+
targetId,
|
|
8151
|
+
encryptedData,
|
|
8152
|
+
timestamp: Date.now(),
|
|
8153
|
+
retryCount: 0,
|
|
8154
|
+
maxRetries: 10
|
|
8155
|
+
};
|
|
8156
|
+
this.offlineMessages.push(msg);
|
|
8157
|
+
this.markDirty();
|
|
8158
|
+
return msg;
|
|
8159
|
+
}
|
|
8160
|
+
/**
|
|
8161
|
+
* Dequeue the next pending offline message.
|
|
8162
|
+
*/
|
|
8163
|
+
dequeueOfflineMessage() {
|
|
8164
|
+
return this.offlineMessages.shift();
|
|
8165
|
+
}
|
|
8166
|
+
/**
|
|
8167
|
+
* Get the number of pending offline messages.
|
|
8168
|
+
*/
|
|
8169
|
+
getOfflineMessageCount() {
|
|
8170
|
+
return this.offlineMessages.length;
|
|
8171
|
+
}
|
|
8172
|
+
/**
|
|
8173
|
+
* Peek at the offline message queue without removing.
|
|
8174
|
+
*/
|
|
8175
|
+
peekOfflineMessages() {
|
|
8176
|
+
return [...this.offlineMessages];
|
|
8177
|
+
}
|
|
8178
|
+
/**
|
|
8179
|
+
* Clear all pending offline messages.
|
|
8180
|
+
*/
|
|
8181
|
+
clearOfflineMessages() {
|
|
8182
|
+
this.offlineMessages = [];
|
|
8183
|
+
this.markDirty();
|
|
8184
|
+
}
|
|
8185
|
+
/**
|
|
8186
|
+
* Remove expired offline messages (older than 24 hours).
|
|
8187
|
+
*/
|
|
8188
|
+
cleanupExpiredOfflineMessages() {
|
|
8189
|
+
const now = Date.now();
|
|
8190
|
+
const threshold = 24 * 60 * 60 * 1e3;
|
|
8191
|
+
const before = this.offlineMessages.length;
|
|
8192
|
+
this.offlineMessages = this.offlineMessages.filter((m) => now - m.timestamp < threshold);
|
|
8193
|
+
if (this.offlineMessages.length !== before) {
|
|
8194
|
+
this.markDirty();
|
|
8195
|
+
}
|
|
8196
|
+
return before - this.offlineMessages.length;
|
|
8197
|
+
}
|
|
8198
|
+
// ----------------------------------------------------------------
|
|
8199
|
+
// Friend management
|
|
8200
|
+
// ----------------------------------------------------------------
|
|
8070
8201
|
/**
|
|
8071
8202
|
* Add a friend record.
|
|
8072
8203
|
*/
|
|
8073
8204
|
addFriend(friend) {
|
|
8074
8205
|
this.friends.set(friend.id, friend);
|
|
8075
|
-
this.
|
|
8206
|
+
this.markDirty();
|
|
8076
8207
|
}
|
|
8077
8208
|
/**
|
|
8078
8209
|
* Remove a friend by ID and clean up associated session.
|
|
@@ -8081,7 +8212,7 @@ var PluginStore = class {
|
|
|
8081
8212
|
const removed = this.friends.delete(friendId);
|
|
8082
8213
|
if (removed) {
|
|
8083
8214
|
this.sessions.delete(friendId);
|
|
8084
|
-
this.
|
|
8215
|
+
this.markDirty();
|
|
8085
8216
|
}
|
|
8086
8217
|
return removed;
|
|
8087
8218
|
}
|
|
@@ -8097,6 +8228,9 @@ var PluginStore = class {
|
|
|
8097
8228
|
getFriendCount() {
|
|
8098
8229
|
return this.friends.size;
|
|
8099
8230
|
}
|
|
8231
|
+
// ----------------------------------------------------------------
|
|
8232
|
+
// Session management
|
|
8233
|
+
// ----------------------------------------------------------------
|
|
8100
8234
|
/**
|
|
8101
8235
|
* Set a session for a peer.
|
|
8102
8236
|
*/
|
|
@@ -8106,7 +8240,7 @@ var PluginStore = class {
|
|
|
8106
8240
|
if (friend) {
|
|
8107
8241
|
friend.sessionKey = session.sessionKey;
|
|
8108
8242
|
}
|
|
8109
|
-
this.
|
|
8243
|
+
this.saveNow();
|
|
8110
8244
|
}
|
|
8111
8245
|
/**
|
|
8112
8246
|
* Get a session by peer ID.
|
|
@@ -8120,12 +8254,15 @@ var PluginStore = class {
|
|
|
8120
8254
|
removeSession(peerId) {
|
|
8121
8255
|
return this.sessions.delete(peerId);
|
|
8122
8256
|
}
|
|
8257
|
+
// ----------------------------------------------------------------
|
|
8258
|
+
// Temp number management
|
|
8259
|
+
// ----------------------------------------------------------------
|
|
8123
8260
|
/**
|
|
8124
8261
|
* Add a temp number record.
|
|
8125
8262
|
*/
|
|
8126
8263
|
addTempNumber(number, expiresAt) {
|
|
8127
8264
|
this.tempNumbers.push({ number, expiresAt });
|
|
8128
|
-
this.
|
|
8265
|
+
this.markDirty();
|
|
8129
8266
|
}
|
|
8130
8267
|
/**
|
|
8131
8268
|
* Revoke (remove) a temp number.
|
|
@@ -8134,7 +8271,7 @@ var PluginStore = class {
|
|
|
8134
8271
|
const idx = this.tempNumbers.findIndex((t) => t.number === number);
|
|
8135
8272
|
if (idx !== -1) {
|
|
8136
8273
|
this.tempNumbers.splice(idx, 1);
|
|
8137
|
-
this.
|
|
8274
|
+
this.markDirty();
|
|
8138
8275
|
return true;
|
|
8139
8276
|
}
|
|
8140
8277
|
return false;
|
|
@@ -8147,15 +8284,18 @@ var PluginStore = class {
|
|
|
8147
8284
|
const before = this.tempNumbers.length;
|
|
8148
8285
|
this.tempNumbers = this.tempNumbers.filter((t) => t.expiresAt > now);
|
|
8149
8286
|
if (this.tempNumbers.length !== before) {
|
|
8150
|
-
this.
|
|
8287
|
+
this.markDirty();
|
|
8151
8288
|
}
|
|
8152
8289
|
}
|
|
8290
|
+
// ----------------------------------------------------------------
|
|
8291
|
+
// Pending friend requests
|
|
8292
|
+
// ----------------------------------------------------------------
|
|
8153
8293
|
/**
|
|
8154
8294
|
* Add a pending friend request.
|
|
8155
8295
|
*/
|
|
8156
8296
|
addPendingRequest(request) {
|
|
8157
8297
|
this.pendingRequests.push(request);
|
|
8158
|
-
this.
|
|
8298
|
+
this.markDirty();
|
|
8159
8299
|
}
|
|
8160
8300
|
/**
|
|
8161
8301
|
* Remove a pending friend request.
|
|
@@ -8164,11 +8304,14 @@ var PluginStore = class {
|
|
|
8164
8304
|
const idx = this.pendingRequests.findIndex((p) => p.requesterId === requesterId);
|
|
8165
8305
|
if (idx !== -1) {
|
|
8166
8306
|
this.pendingRequests.splice(idx, 1);
|
|
8167
|
-
this.
|
|
8307
|
+
this.markDirty();
|
|
8168
8308
|
return true;
|
|
8169
8309
|
}
|
|
8170
8310
|
return false;
|
|
8171
8311
|
}
|
|
8312
|
+
// ----------------------------------------------------------------
|
|
8313
|
+
// Handshake state
|
|
8314
|
+
// ----------------------------------------------------------------
|
|
8172
8315
|
/**
|
|
8173
8316
|
* Set a pending handshake state.
|
|
8174
8317
|
*/
|
|
@@ -8191,7 +8334,7 @@ var PluginStore = class {
|
|
|
8191
8334
|
|
|
8192
8335
|
// dist/services/identityService.js
|
|
8193
8336
|
var import_qrcode = __toESM(require_lib(), 1);
|
|
8194
|
-
var import_crypto3 = __toESM(
|
|
8337
|
+
var import_crypto3 = __toESM(require_dist(), 1);
|
|
8195
8338
|
import * as crypto4 from "crypto";
|
|
8196
8339
|
var IdentityService = class {
|
|
8197
8340
|
constructor(store, logger) {
|
|
@@ -8365,16 +8508,66 @@ var IdentityService = class {
|
|
|
8365
8508
|
|
|
8366
8509
|
// dist/services/serverClient.js
|
|
8367
8510
|
import WebSocket from "ws";
|
|
8511
|
+
var DEFAULT_CONFIG = {
|
|
8512
|
+
initialReconnectDelay: 1e3,
|
|
8513
|
+
maxReconnectDelay: 6e4,
|
|
8514
|
+
reconnectBackoffFactor: 2,
|
|
8515
|
+
heartbeatIntervalMs: 3e4,
|
|
8516
|
+
requestTimeoutMs: 3e4
|
|
8517
|
+
};
|
|
8368
8518
|
var ServerClient = class {
|
|
8369
|
-
constructor(serverUrl, store, logger) {
|
|
8519
|
+
constructor(serverUrl, store, logger, config2) {
|
|
8370
8520
|
this.ws = null;
|
|
8371
8521
|
this.wsReconnectTimer = null;
|
|
8372
8522
|
this.heartbeatTimer = null;
|
|
8373
8523
|
this.wsConnected = false;
|
|
8524
|
+
this.connectionState = "offline";
|
|
8525
|
+
this.reconnectDelay = DEFAULT_CONFIG.initialReconnectDelay;
|
|
8526
|
+
this.reconnectAttempts = 0;
|
|
8527
|
+
this.stateChangeCallbacks = [];
|
|
8374
8528
|
this.wsHandlers = /* @__PURE__ */ new Map();
|
|
8375
8529
|
this.serverUrl = serverUrl;
|
|
8376
8530
|
this.store = store;
|
|
8377
8531
|
this.logger = logger;
|
|
8532
|
+
this.config = { ...DEFAULT_CONFIG, ...config2 };
|
|
8533
|
+
}
|
|
8534
|
+
// ----------------------------------------------------------------
|
|
8535
|
+
// Connection state management
|
|
8536
|
+
// ----------------------------------------------------------------
|
|
8537
|
+
/**
|
|
8538
|
+
* Get the current connection state.
|
|
8539
|
+
*/
|
|
8540
|
+
getConnectionState() {
|
|
8541
|
+
return this.connectionState;
|
|
8542
|
+
}
|
|
8543
|
+
/**
|
|
8544
|
+
* Register a callback for connection state changes.
|
|
8545
|
+
*/
|
|
8546
|
+
onConnectionStateChange(callback) {
|
|
8547
|
+
this.stateChangeCallbacks.push(callback);
|
|
8548
|
+
}
|
|
8549
|
+
/**
|
|
8550
|
+
* Remove a connection state callback.
|
|
8551
|
+
*/
|
|
8552
|
+
offConnectionStateChange(callback) {
|
|
8553
|
+
this.stateChangeCallbacks = this.stateChangeCallbacks.filter((cb) => cb !== callback);
|
|
8554
|
+
}
|
|
8555
|
+
/**
|
|
8556
|
+
* Update connection state and notify listeners.
|
|
8557
|
+
*/
|
|
8558
|
+
setConnectionState(newState) {
|
|
8559
|
+
const previousState = this.connectionState;
|
|
8560
|
+
if (previousState === newState)
|
|
8561
|
+
return;
|
|
8562
|
+
this.connectionState = newState;
|
|
8563
|
+
this.logger.info(`[Server] Connection state: ${previousState} \u2192 ${newState}`);
|
|
8564
|
+
for (const callback of this.stateChangeCallbacks) {
|
|
8565
|
+
try {
|
|
8566
|
+
callback(newState, previousState);
|
|
8567
|
+
} catch (err) {
|
|
8568
|
+
this.logger.error("[Server] Connection state callback error:", err);
|
|
8569
|
+
}
|
|
8570
|
+
}
|
|
8378
8571
|
}
|
|
8379
8572
|
// ----------------------------------------------------------------
|
|
8380
8573
|
// WebSocket connection
|
|
@@ -8383,15 +8576,42 @@ var ServerClient = class {
|
|
|
8383
8576
|
* Connect to the server via WebSocket and start heartbeat.
|
|
8384
8577
|
*/
|
|
8385
8578
|
connectWebSocket() {
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8579
|
+
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
|
|
8580
|
+
this.logger.debug("[Server] WebSocket already connecting/connected");
|
|
8581
|
+
return;
|
|
8582
|
+
}
|
|
8583
|
+
let wsUrl;
|
|
8584
|
+
try {
|
|
8585
|
+
const baseUrl = this.serverUrl.replace(/^http/, "ws");
|
|
8586
|
+
const url = new URL(baseUrl + "/ws");
|
|
8587
|
+
url.port = "443";
|
|
8588
|
+
url.protocol = "wss:";
|
|
8589
|
+
wsUrl = url.toString();
|
|
8590
|
+
} catch {
|
|
8591
|
+
wsUrl = this.serverUrl.replace(/^https?/, "wss") + "/ws";
|
|
8592
|
+
}
|
|
8391
8593
|
this.logger.info("[Server] Connecting WebSocket to " + wsUrl);
|
|
8392
|
-
this.
|
|
8594
|
+
this.setConnectionState("reconnecting");
|
|
8595
|
+
try {
|
|
8596
|
+
this.ws = new WebSocket(wsUrl);
|
|
8597
|
+
} catch (err) {
|
|
8598
|
+
this.logger.error("[Server] Failed to create WebSocket:", err);
|
|
8599
|
+
this.setConnectionState("offline");
|
|
8600
|
+
this.scheduleReconnect();
|
|
8601
|
+
return;
|
|
8602
|
+
}
|
|
8603
|
+
const connectTimeout = setTimeout(() => {
|
|
8604
|
+
if (this.ws && this.ws.readyState !== WebSocket.OPEN) {
|
|
8605
|
+
this.logger.warn("[Server] WebSocket connection timeout");
|
|
8606
|
+
this.ws.terminate();
|
|
8607
|
+
}
|
|
8608
|
+
}, this.config.requestTimeoutMs);
|
|
8393
8609
|
this.ws.on("open", () => {
|
|
8610
|
+
clearTimeout(connectTimeout);
|
|
8394
8611
|
this.wsConnected = true;
|
|
8612
|
+
this.reconnectDelay = this.config.initialReconnectDelay;
|
|
8613
|
+
this.reconnectAttempts = 0;
|
|
8614
|
+
this.setConnectionState("online");
|
|
8395
8615
|
this.logger.info("[Server] WebSocket connected");
|
|
8396
8616
|
this.wsSend({
|
|
8397
8617
|
type: "online",
|
|
@@ -8409,29 +8629,30 @@ var ServerClient = class {
|
|
|
8409
8629
|
}
|
|
8410
8630
|
});
|
|
8411
8631
|
this.ws.on("close", (code, reason) => {
|
|
8632
|
+
clearTimeout(connectTimeout);
|
|
8412
8633
|
this.wsConnected = false;
|
|
8413
8634
|
this.logger.info("[Server] WebSocket closed:", code, reason.toString());
|
|
8414
8635
|
this.stopHeartbeat();
|
|
8636
|
+
this.setConnectionState("offline");
|
|
8415
8637
|
this.scheduleReconnect();
|
|
8416
8638
|
});
|
|
8417
8639
|
this.ws.on("error", (err) => {
|
|
8640
|
+
clearTimeout(connectTimeout);
|
|
8418
8641
|
this.logger.error("[Server] WebSocket error:", err.message);
|
|
8419
8642
|
});
|
|
8420
8643
|
}
|
|
8421
8644
|
/**
|
|
8422
|
-
* Disconnect the WebSocket.
|
|
8645
|
+
* Disconnect the WebSocket and stop reconnection.
|
|
8423
8646
|
*/
|
|
8424
8647
|
disconnectWebSocket() {
|
|
8425
8648
|
this.stopHeartbeat();
|
|
8426
|
-
|
|
8427
|
-
clearTimeout(this.wsReconnectTimer);
|
|
8428
|
-
this.wsReconnectTimer = null;
|
|
8429
|
-
}
|
|
8649
|
+
this.cancelReconnect();
|
|
8430
8650
|
if (this.ws) {
|
|
8431
8651
|
this.ws.close();
|
|
8432
8652
|
this.ws = null;
|
|
8433
8653
|
}
|
|
8434
8654
|
this.wsConnected = false;
|
|
8655
|
+
this.setConnectionState("offline");
|
|
8435
8656
|
}
|
|
8436
8657
|
/**
|
|
8437
8658
|
* Check if the WebSocket is connected.
|
|
@@ -8449,13 +8670,50 @@ var ServerClient = class {
|
|
|
8449
8670
|
}
|
|
8450
8671
|
/**
|
|
8451
8672
|
* Send a JSON message over the WebSocket.
|
|
8673
|
+
* Returns true if the message was sent, false if offline.
|
|
8452
8674
|
*/
|
|
8453
8675
|
wsSend(data) {
|
|
8454
8676
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
8455
8677
|
this.ws.send(JSON.stringify(data));
|
|
8456
|
-
|
|
8457
|
-
this.logger.warn("[Server] Cannot send \u2014 WebSocket not open");
|
|
8678
|
+
return true;
|
|
8458
8679
|
}
|
|
8680
|
+
this.logger.warn("[Server] Cannot send \u2014 WebSocket not open");
|
|
8681
|
+
return false;
|
|
8682
|
+
}
|
|
8683
|
+
// ----------------------------------------------------------------
|
|
8684
|
+
// Exponential backoff reconnection
|
|
8685
|
+
// ----------------------------------------------------------------
|
|
8686
|
+
/**
|
|
8687
|
+
* Schedule a reconnection attempt with exponential backoff.
|
|
8688
|
+
*
|
|
8689
|
+
* Delay formula: min(initialDelay * backoffFactor^attempts, maxDelay)
|
|
8690
|
+
* With jitter: delay * (0.75 + Math.random() * 0.5) to avoid thundering herd
|
|
8691
|
+
*/
|
|
8692
|
+
scheduleReconnect() {
|
|
8693
|
+
if (this.wsReconnectTimer)
|
|
8694
|
+
return;
|
|
8695
|
+
const jitter = 0.75 + Math.random() * 0.5;
|
|
8696
|
+
const delay = Math.min(this.reconnectDelay * jitter, this.config.maxReconnectDelay);
|
|
8697
|
+
this.reconnectAttempts++;
|
|
8698
|
+
this.logger.info(`[Server] Reconnecting in ${Math.round(delay)}ms (attempt #${this.reconnectAttempts})`);
|
|
8699
|
+
this.setConnectionState("reconnecting");
|
|
8700
|
+
this.wsReconnectTimer = setTimeout(() => {
|
|
8701
|
+
this.wsReconnectTimer = null;
|
|
8702
|
+
this.logger.info("[Server] Attempting WebSocket reconnect...");
|
|
8703
|
+
this.connectWebSocket();
|
|
8704
|
+
}, delay);
|
|
8705
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * this.config.reconnectBackoffFactor, this.config.maxReconnectDelay);
|
|
8706
|
+
}
|
|
8707
|
+
/**
|
|
8708
|
+
* Cancel a pending reconnection.
|
|
8709
|
+
*/
|
|
8710
|
+
cancelReconnect() {
|
|
8711
|
+
if (this.wsReconnectTimer) {
|
|
8712
|
+
clearTimeout(this.wsReconnectTimer);
|
|
8713
|
+
this.wsReconnectTimer = null;
|
|
8714
|
+
}
|
|
8715
|
+
this.reconnectDelay = this.config.initialReconnectDelay;
|
|
8716
|
+
this.reconnectAttempts = 0;
|
|
8459
8717
|
}
|
|
8460
8718
|
// ----------------------------------------------------------------
|
|
8461
8719
|
// REST API methods
|
|
@@ -8570,9 +8828,10 @@ var ServerClient = class {
|
|
|
8570
8828
|
}
|
|
8571
8829
|
/**
|
|
8572
8830
|
* Send a relay message via the server (WebSocket fallback for P2P).
|
|
8831
|
+
* Returns true if sent, false if offline.
|
|
8573
8832
|
*/
|
|
8574
8833
|
sendRelayMessage(targetId, payload) {
|
|
8575
|
-
this.wsSend({
|
|
8834
|
+
return this.wsSend({
|
|
8576
8835
|
type: "relay",
|
|
8577
8836
|
targetId,
|
|
8578
8837
|
payload,
|
|
@@ -8585,11 +8844,15 @@ var ServerClient = class {
|
|
|
8585
8844
|
async fetchPost(path7, body) {
|
|
8586
8845
|
const url = this.serverUrl + path7;
|
|
8587
8846
|
try {
|
|
8847
|
+
const controller = new AbortController();
|
|
8848
|
+
const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
|
|
8588
8849
|
const resp = await fetch(url, {
|
|
8589
8850
|
method: "POST",
|
|
8590
8851
|
headers: { "Content-Type": "application/json" },
|
|
8591
|
-
body: JSON.stringify(body)
|
|
8852
|
+
body: JSON.stringify(body),
|
|
8853
|
+
signal: controller.signal
|
|
8592
8854
|
});
|
|
8855
|
+
clearTimeout(timeout);
|
|
8593
8856
|
if (!resp.ok) {
|
|
8594
8857
|
const text = await resp.text();
|
|
8595
8858
|
this.logger.error(`[Server] API error ${resp.status} on ${path7}: ${text}`);
|
|
@@ -8597,14 +8860,21 @@ var ServerClient = class {
|
|
|
8597
8860
|
}
|
|
8598
8861
|
return await resp.json();
|
|
8599
8862
|
} catch (err) {
|
|
8600
|
-
|
|
8863
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
8864
|
+
this.logger.error(`[Server] API request timeout for ${path7}`);
|
|
8865
|
+
} else {
|
|
8866
|
+
this.logger.error(`[Server] API request failed for ${path7}:`, err);
|
|
8867
|
+
}
|
|
8601
8868
|
return null;
|
|
8602
8869
|
}
|
|
8603
8870
|
}
|
|
8604
8871
|
async fetchGet(path7) {
|
|
8605
8872
|
const url = this.serverUrl + path7;
|
|
8606
8873
|
try {
|
|
8607
|
-
const
|
|
8874
|
+
const controller = new AbortController();
|
|
8875
|
+
const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
|
|
8876
|
+
const resp = await fetch(url, { signal: controller.signal });
|
|
8877
|
+
clearTimeout(timeout);
|
|
8608
8878
|
if (!resp.ok) {
|
|
8609
8879
|
const text = await resp.text();
|
|
8610
8880
|
this.logger.error(`[Server] API error ${resp.status} on ${path7}: ${text}`);
|
|
@@ -8612,18 +8882,26 @@ var ServerClient = class {
|
|
|
8612
8882
|
}
|
|
8613
8883
|
return await resp.json();
|
|
8614
8884
|
} catch (err) {
|
|
8615
|
-
|
|
8885
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
8886
|
+
this.logger.error(`[Server] GET request timeout for ${path7}`);
|
|
8887
|
+
} else {
|
|
8888
|
+
this.logger.error(`[Server] GET request failed for ${path7}:`, err);
|
|
8889
|
+
}
|
|
8616
8890
|
return null;
|
|
8617
8891
|
}
|
|
8618
8892
|
}
|
|
8619
8893
|
async del(path7, body) {
|
|
8620
8894
|
const url = this.serverUrl + path7;
|
|
8621
8895
|
try {
|
|
8896
|
+
const controller = new AbortController();
|
|
8897
|
+
const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
|
|
8622
8898
|
const resp = await fetch(url, {
|
|
8623
8899
|
method: "DELETE",
|
|
8624
8900
|
headers: { "Content-Type": "application/json" },
|
|
8625
|
-
body: body ? JSON.stringify(body) : void 0
|
|
8901
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
8902
|
+
signal: controller.signal
|
|
8626
8903
|
});
|
|
8904
|
+
clearTimeout(timeout);
|
|
8627
8905
|
return resp.ok;
|
|
8628
8906
|
} catch (err) {
|
|
8629
8907
|
this.logger.error(`[Server] DELETE request failed for ${path7}:`, err);
|
|
@@ -8633,11 +8911,15 @@ var ServerClient = class {
|
|
|
8633
8911
|
async post(path7, body) {
|
|
8634
8912
|
const url = this.serverUrl + path7;
|
|
8635
8913
|
try {
|
|
8914
|
+
const controller = new AbortController();
|
|
8915
|
+
const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
|
|
8636
8916
|
const resp = await fetch(url, {
|
|
8637
8917
|
method: "POST",
|
|
8638
8918
|
headers: { "Content-Type": "application/json" },
|
|
8639
|
-
body: JSON.stringify(body)
|
|
8919
|
+
body: JSON.stringify(body),
|
|
8920
|
+
signal: controller.signal
|
|
8640
8921
|
});
|
|
8922
|
+
clearTimeout(timeout);
|
|
8641
8923
|
return resp.ok;
|
|
8642
8924
|
} catch (err) {
|
|
8643
8925
|
this.logger.error(`[Server] API request failed for ${path7}:`, err);
|
|
@@ -8658,9 +8940,10 @@ var ServerClient = class {
|
|
|
8658
8940
|
}
|
|
8659
8941
|
}
|
|
8660
8942
|
startHeartbeat() {
|
|
8943
|
+
this.stopHeartbeat();
|
|
8661
8944
|
this.heartbeatTimer = setInterval(() => {
|
|
8662
8945
|
this.wsSend({ type: "ping", agentId: this.store.agentId, timestamp: Date.now() });
|
|
8663
|
-
},
|
|
8946
|
+
}, this.config.heartbeatIntervalMs);
|
|
8664
8947
|
}
|
|
8665
8948
|
stopHeartbeat() {
|
|
8666
8949
|
if (this.heartbeatTimer) {
|
|
@@ -8668,19 +8951,10 @@ var ServerClient = class {
|
|
|
8668
8951
|
this.heartbeatTimer = null;
|
|
8669
8952
|
}
|
|
8670
8953
|
}
|
|
8671
|
-
scheduleReconnect() {
|
|
8672
|
-
if (this.wsReconnectTimer)
|
|
8673
|
-
return;
|
|
8674
|
-
this.wsReconnectTimer = setTimeout(() => {
|
|
8675
|
-
this.wsReconnectTimer = null;
|
|
8676
|
-
this.logger.info("[Server] Attempting WebSocket reconnect...");
|
|
8677
|
-
this.connectWebSocket();
|
|
8678
|
-
}, 5e3);
|
|
8679
|
-
}
|
|
8680
8954
|
};
|
|
8681
8955
|
|
|
8682
8956
|
// dist/handshake/handshakeManager.js
|
|
8683
|
-
var import_crypto4 = __toESM(
|
|
8957
|
+
var import_crypto4 = __toESM(require_dist(), 1);
|
|
8684
8958
|
import * as crypto5 from "crypto";
|
|
8685
8959
|
var HandshakeManager = class {
|
|
8686
8960
|
constructor(store, serverClient, config2, logger) {
|
|
@@ -9096,7 +9370,7 @@ var P2PConnectionManager = class {
|
|
|
9096
9370
|
};
|
|
9097
9371
|
|
|
9098
9372
|
// dist/fileTransfer/transferManager.js
|
|
9099
|
-
var import_crypto5 = __toESM(
|
|
9373
|
+
var import_crypto5 = __toESM(require_dist(), 1);
|
|
9100
9374
|
import * as fs3 from "fs";
|
|
9101
9375
|
import * as path3 from "path";
|
|
9102
9376
|
var DEFAULT_CHUNK_SIZE = 64 * 1024;
|
|
@@ -9319,7 +9593,7 @@ var FileTransferManager = class {
|
|
|
9319
9593
|
};
|
|
9320
9594
|
|
|
9321
9595
|
// dist/channels/encryptedChat.js
|
|
9322
|
-
var import_crypto6 = __toESM(
|
|
9596
|
+
var import_crypto6 = __toESM(require_dist(), 1);
|
|
9323
9597
|
import * as fs4 from "fs";
|
|
9324
9598
|
import * as path4 from "path";
|
|
9325
9599
|
function safeFilePath(filePath, allowedDir) {
|
|
@@ -9335,12 +9609,18 @@ var EncryptedChatChannel = class {
|
|
|
9335
9609
|
this.api = null;
|
|
9336
9610
|
this.dataDir = "";
|
|
9337
9611
|
this.fileChunkBuffers = /* @__PURE__ */ new Map();
|
|
9612
|
+
this.flushingOffline = false;
|
|
9338
9613
|
this.store = store;
|
|
9339
9614
|
this.handshakeManager = handshakeManager;
|
|
9340
9615
|
this.p2pManager = p2pManager;
|
|
9341
9616
|
this.serverClient = serverClient;
|
|
9342
9617
|
this.logger = logger;
|
|
9343
9618
|
this.dataDir = dataDir;
|
|
9619
|
+
this.serverClient.onConnectionStateChange((newState, _prevState) => {
|
|
9620
|
+
if (newState === "online") {
|
|
9621
|
+
this.onReconnected();
|
|
9622
|
+
}
|
|
9623
|
+
});
|
|
9344
9624
|
}
|
|
9345
9625
|
/**
|
|
9346
9626
|
* Set the OpenClaw API reference (for emitting events).
|
|
@@ -9348,6 +9628,17 @@ var EncryptedChatChannel = class {
|
|
|
9348
9628
|
setAPI(api) {
|
|
9349
9629
|
this.api = api;
|
|
9350
9630
|
}
|
|
9631
|
+
/**
|
|
9632
|
+
* Called when the WebSocket connection is re-established.
|
|
9633
|
+
* Flushes all queued offline messages.
|
|
9634
|
+
*/
|
|
9635
|
+
onReconnected() {
|
|
9636
|
+
const pendingCount = this.store.getOfflineMessageCount();
|
|
9637
|
+
if (pendingCount > 0) {
|
|
9638
|
+
this.logger.info(`[Chat] Back online \u2014 flushing ${pendingCount} queued offline messages`);
|
|
9639
|
+
this.flushOfflineMessages();
|
|
9640
|
+
}
|
|
9641
|
+
}
|
|
9351
9642
|
/**
|
|
9352
9643
|
* Handle an incoming encrypted message from a peer.
|
|
9353
9644
|
*
|
|
@@ -9382,7 +9673,7 @@ var EncryptedChatChannel = class {
|
|
|
9382
9673
|
});
|
|
9383
9674
|
}
|
|
9384
9675
|
friend.lastMessageAt = /* @__PURE__ */ new Date();
|
|
9385
|
-
this.store.
|
|
9676
|
+
this.store.markDirty();
|
|
9386
9677
|
}
|
|
9387
9678
|
/**
|
|
9388
9679
|
* Send an encrypted message to a peer.
|
|
@@ -9390,7 +9681,9 @@ var EncryptedChatChannel = class {
|
|
|
9390
9681
|
* Encrypts with the session key, signs with the Ed25519 identity key,
|
|
9391
9682
|
* and sends via P2P if available, falling back to WebSocket relay.
|
|
9392
9683
|
*
|
|
9393
|
-
*
|
|
9684
|
+
* When offline, the message is queued for later delivery.
|
|
9685
|
+
*
|
|
9686
|
+
* @returns true if the message was sent or queued successfully
|
|
9394
9687
|
*/
|
|
9395
9688
|
send(toId, plaintext) {
|
|
9396
9689
|
const friend = this.store.getFriend(toId);
|
|
@@ -9408,14 +9701,26 @@ var EncryptedChatChannel = class {
|
|
|
9408
9701
|
}
|
|
9409
9702
|
const wireData = (0, import_crypto6.encryptMessage)(plaintext, sessionKey, this.store.identityKeys.secretKey, this.store.identityKeys.publicKey);
|
|
9410
9703
|
const buf = Buffer.from(wireData);
|
|
9411
|
-
|
|
9412
|
-
|
|
9704
|
+
const encodedData = (0, import_crypto6.encodeBase64)(wireData);
|
|
9705
|
+
const isOnline = this.serverClient.isConnected();
|
|
9706
|
+
if (isOnline) {
|
|
9707
|
+
if (this.p2pManager.isConnected(toId) && this.p2pManager.send(toId, buf)) {
|
|
9708
|
+
this.logger.debug("[Chat] Sent message via P2P to " + toId);
|
|
9709
|
+
} else {
|
|
9710
|
+
const sent = this.serverClient.sendRelayMessage(toId, {
|
|
9711
|
+
channel: "encrypted-chat",
|
|
9712
|
+
data: encodedData
|
|
9713
|
+
});
|
|
9714
|
+
if (!sent) {
|
|
9715
|
+
this.logger.warn("[Chat] WebSocket send failed \u2014 queuing message for offline delivery");
|
|
9716
|
+
this.store.enqueueOfflineMessage(toId, encodedData);
|
|
9717
|
+
} else {
|
|
9718
|
+
this.logger.debug("[Chat] Sent message via relay to " + toId);
|
|
9719
|
+
}
|
|
9720
|
+
}
|
|
9413
9721
|
} else {
|
|
9414
|
-
this.
|
|
9415
|
-
|
|
9416
|
-
data: (0, import_crypto6.encodeBase64)(wireData)
|
|
9417
|
-
});
|
|
9418
|
-
this.logger.debug("[Chat] Sent message via relay to " + toId);
|
|
9722
|
+
this.logger.info("[Chat] Offline \u2014 message queued for delivery to " + toId);
|
|
9723
|
+
this.store.enqueueOfflineMessage(toId, encodedData);
|
|
9419
9724
|
}
|
|
9420
9725
|
const session = this.store.getSession(toId);
|
|
9421
9726
|
if (session) {
|
|
@@ -9423,10 +9728,71 @@ var EncryptedChatChannel = class {
|
|
|
9423
9728
|
if (session.messageCount % 100 === 0 || Date.now() - session.createdAt.getTime() > 36e5) {
|
|
9424
9729
|
this.handshakeManager.rotateSessionKey(toId);
|
|
9425
9730
|
}
|
|
9426
|
-
this.store.
|
|
9731
|
+
this.store.markDirty();
|
|
9427
9732
|
}
|
|
9428
9733
|
return true;
|
|
9429
9734
|
}
|
|
9735
|
+
/**
|
|
9736
|
+
* Flush all queued offline messages.
|
|
9737
|
+
* Called automatically when connection is re-established.
|
|
9738
|
+
*/
|
|
9739
|
+
flushOfflineMessages() {
|
|
9740
|
+
if (this.flushingOffline)
|
|
9741
|
+
return;
|
|
9742
|
+
this.flushingOffline = true;
|
|
9743
|
+
const flushNext = () => {
|
|
9744
|
+
const msg = this.store.dequeueOfflineMessage();
|
|
9745
|
+
if (!msg) {
|
|
9746
|
+
this.flushingOffline = false;
|
|
9747
|
+
const remaining = this.store.getOfflineMessageCount();
|
|
9748
|
+
if (remaining === 0) {
|
|
9749
|
+
this.logger.info("[Chat] All offline messages flushed");
|
|
9750
|
+
}
|
|
9751
|
+
return;
|
|
9752
|
+
}
|
|
9753
|
+
if (!this.serverClient.isConnected()) {
|
|
9754
|
+
this.store.offlineMessages.unshift(msg);
|
|
9755
|
+
this.flushingOffline = false;
|
|
9756
|
+
this.logger.warn("[Chat] Connection lost during offline flush");
|
|
9757
|
+
return;
|
|
9758
|
+
}
|
|
9759
|
+
const friend = this.store.getFriend(msg.targetId);
|
|
9760
|
+
let sessionKey = this.handshakeManager.getSessionKey(msg.targetId);
|
|
9761
|
+
if (!sessionKey && friend?.sessionKey) {
|
|
9762
|
+
sessionKey = friend.sessionKey;
|
|
9763
|
+
}
|
|
9764
|
+
if (!sessionKey) {
|
|
9765
|
+
this.logger.warn("[Chat] Skipping offline message to " + msg.targetId + " (no session key)");
|
|
9766
|
+
this.store.markDirty();
|
|
9767
|
+
setImmediate(flushNext);
|
|
9768
|
+
return;
|
|
9769
|
+
}
|
|
9770
|
+
const sent = this.serverClient.sendRelayMessage(msg.targetId, {
|
|
9771
|
+
channel: "encrypted-chat",
|
|
9772
|
+
data: msg.encryptedData
|
|
9773
|
+
});
|
|
9774
|
+
if (sent) {
|
|
9775
|
+
this.logger.debug("[Chat] Flushed offline message to " + msg.targetId);
|
|
9776
|
+
} else {
|
|
9777
|
+
this.logger.warn("[Chat] Failed to flush offline message to " + msg.targetId);
|
|
9778
|
+
msg.retryCount++;
|
|
9779
|
+
if (msg.retryCount < msg.maxRetries) {
|
|
9780
|
+
this.store.offlineMessages.unshift(msg);
|
|
9781
|
+
} else {
|
|
9782
|
+
this.logger.error("[Chat] Dropped offline message to " + msg.targetId + " (max retries exceeded)");
|
|
9783
|
+
}
|
|
9784
|
+
}
|
|
9785
|
+
this.store.markDirty();
|
|
9786
|
+
setImmediate(flushNext);
|
|
9787
|
+
};
|
|
9788
|
+
flushNext();
|
|
9789
|
+
}
|
|
9790
|
+
/**
|
|
9791
|
+
* Get the count of pending offline messages.
|
|
9792
|
+
*/
|
|
9793
|
+
getPendingOfflineCount() {
|
|
9794
|
+
return this.store.getOfflineMessageCount();
|
|
9795
|
+
}
|
|
9430
9796
|
/**
|
|
9431
9797
|
* Handle an incoming file chunk from a peer.
|
|
9432
9798
|
*
|
|
@@ -9511,6 +9877,7 @@ var EncryptedChatChannel = class {
|
|
|
9511
9877
|
*/
|
|
9512
9878
|
cleanup() {
|
|
9513
9879
|
this.fileChunkBuffers.clear();
|
|
9880
|
+
this.flushingOffline = false;
|
|
9514
9881
|
}
|
|
9515
9882
|
};
|
|
9516
9883
|
|
|
@@ -9759,7 +10126,7 @@ var BeforeToolCallHook = class {
|
|
|
9759
10126
|
};
|
|
9760
10127
|
|
|
9761
10128
|
// dist/hooks/messageSending.js
|
|
9762
|
-
var import_crypto7 = __toESM(
|
|
10129
|
+
var import_crypto7 = __toESM(require_dist(), 1);
|
|
9763
10130
|
var MessageSendingHook = class {
|
|
9764
10131
|
constructor(store, handshakeManager, logger) {
|
|
9765
10132
|
this.store = store;
|
|
@@ -10090,6 +10457,26 @@ tbody tr:hover { background: var(--bg3); }
|
|
|
10090
10457
|
.toggle-label input:checked + .toggle-slider { background: var(--accent); }
|
|
10091
10458
|
.toggle-label input:checked + .toggle-slider::after { left: 21px; background: #fff; }
|
|
10092
10459
|
|
|
10460
|
+
/* Offline banner */
|
|
10461
|
+
.offline-banner {
|
|
10462
|
+
background: linear-gradient(90deg, #7f1d1d, #991b1b);
|
|
10463
|
+
color: #fca5a5;
|
|
10464
|
+
padding: 10px 24px;
|
|
10465
|
+
font-size: 13px;
|
|
10466
|
+
display: flex;
|
|
10467
|
+
align-items: center;
|
|
10468
|
+
gap: 10px;
|
|
10469
|
+
animation: fadeIn .2s ease-out;
|
|
10470
|
+
}
|
|
10471
|
+
.offline-banner .offline-icon {
|
|
10472
|
+
font-size: 16px;
|
|
10473
|
+
animation: pulse 2s infinite;
|
|
10474
|
+
}
|
|
10475
|
+
@keyframes pulse {
|
|
10476
|
+
0%, 100% { opacity: 1; }
|
|
10477
|
+
50% { opacity: 0.4; }
|
|
10478
|
+
}
|
|
10479
|
+
|
|
10093
10480
|
/* Responsive */
|
|
10094
10481
|
@media (max-width: 768px) {
|
|
10095
10482
|
.sidebar { position: fixed; left: -260px; z-index: 50; height: 100vh; transition: left var(--transition); }
|
|
@@ -10106,6 +10493,43 @@ const API = '/api';
|
|
|
10106
10493
|
let currentPage = 'dashboard';
|
|
10107
10494
|
let refreshTimer = null;
|
|
10108
10495
|
|
|
10496
|
+
// \u2500\u2500 Offline detection \u2500\u2500
|
|
10497
|
+
let isOffline = false;
|
|
10498
|
+
let offlineBannerEl = null;
|
|
10499
|
+
|
|
10500
|
+
function updateOnlineStatus() {
|
|
10501
|
+
const wasOffline = isOffline;
|
|
10502
|
+
isOffline = !navigator.onLine;
|
|
10503
|
+
if (isOffline && !wasOffline) {
|
|
10504
|
+
showOfflineBanner();
|
|
10505
|
+
} else if (!isOffline && wasOffline) {
|
|
10506
|
+
hideOfflineBanner();
|
|
10507
|
+
// Reload current page on reconnection
|
|
10508
|
+
loadPage(currentPage);
|
|
10509
|
+
}
|
|
10510
|
+
}
|
|
10511
|
+
|
|
10512
|
+
function showOfflineBanner() {
|
|
10513
|
+
if (offlineBannerEl) return;
|
|
10514
|
+
offlineBannerEl = document.createElement('div');
|
|
10515
|
+
offlineBannerEl.className = 'offline-banner';
|
|
10516
|
+
offlineBannerEl.innerHTML = '<span class="offline-icon">\u{1F50C}</span><span>You are offline. Some features may be limited. Data is loaded from local cache.</span>';
|
|
10517
|
+
const mainContent = document.querySelector('.main');
|
|
10518
|
+
if (mainContent) {
|
|
10519
|
+
mainContent.insertBefore(offlineBannerEl, mainContent.firstChild);
|
|
10520
|
+
}
|
|
10521
|
+
}
|
|
10522
|
+
|
|
10523
|
+
function hideOfflineBanner() {
|
|
10524
|
+
if (offlineBannerEl) {
|
|
10525
|
+
offlineBannerEl.remove();
|
|
10526
|
+
offlineBannerEl = null;
|
|
10527
|
+
}
|
|
10528
|
+
}
|
|
10529
|
+
|
|
10530
|
+
window.addEventListener('online', updateOnlineStatus);
|
|
10531
|
+
window.addEventListener('offline', updateOnlineStatus);
|
|
10532
|
+
|
|
10109
10533
|
// \u2500\u2500 jQuery-style helpers \u2500\u2500
|
|
10110
10534
|
const $ = (sel, ctx) => (ctx || document).querySelector(sel);
|
|
10111
10535
|
const $$ = (sel, ctx) => Array.from((ctx || document).querySelectorAll(sel));
|
|
@@ -10192,7 +10616,11 @@ function loadPage(page) {
|
|
|
10192
10616
|
async function loadDashboard() {
|
|
10193
10617
|
const el = $('#dashboard-content');
|
|
10194
10618
|
html(el, '<div class="loading-mask"><div class="spinner"></div>Loading dashboard...</div>');
|
|
10195
|
-
const
|
|
10619
|
+
const results = await Promise.allSettled([api('/status'), api('/friends'), api('/identity'), api('/mgmt-url')]);
|
|
10620
|
+
const status = results[0].status === 'fulfilled' ? results[0].value : { error: results[0].reason?.message || 'Failed' };
|
|
10621
|
+
const friends = results[1].status === 'fulfilled' ? results[1].value : { friends: [], error: true };
|
|
10622
|
+
const identity = results[2].status === 'fulfilled' ? results[2].value : { agentId: '\u2014', publicKeyFingerprint: '\u2014', serverUrl: '\u2014', connected: false };
|
|
10623
|
+
const mgmtUrl = results[3].status === 'fulfilled' ? results[3].value : { mgmtUrl: window.location.origin };
|
|
10196
10624
|
if (status.error) { html(el, '<div class="empty"><div class="icon">\u26A0\uFE0F</div><p>Failed to connect to AICQ plugin</p></div>'); return; }
|
|
10197
10625
|
const connCls = status.connected ? 'dot-ok' : 'dot-err';
|
|
10198
10626
|
const connText = status.connected ? 'Connected' : 'Disconnected';
|
|
@@ -10442,7 +10870,17 @@ let friendsFilter = 'all';
|
|
|
10442
10870
|
async function loadFriends() {
|
|
10443
10871
|
const el = $('#friends-content');
|
|
10444
10872
|
html(el, '<div class="loading-mask"><div class="spinner"></div>Loading friends...</div>');
|
|
10445
|
-
const
|
|
10873
|
+
const results = await Promise.allSettled([api('/friends'), api('/friends/requests'), api('/sessions')]);
|
|
10874
|
+
const friends = results[0].status === 'fulfilled' ? results[0].value : { friends: [] };
|
|
10875
|
+
const requests = results[1].status === 'fulfilled' ? results[1].value : { requests: [] };
|
|
10876
|
+
const sessions = results[2].status === 'fulfilled' ? results[2].value : { sessions: [] };
|
|
10877
|
+
|
|
10878
|
+
// Show offline banner if friends data came from cache
|
|
10879
|
+
if (friends.offline || friends.error) {
|
|
10880
|
+
showOfflineBanner();
|
|
10881
|
+
} else {
|
|
10882
|
+
hideOfflineBanner();
|
|
10883
|
+
}
|
|
10446
10884
|
|
|
10447
10885
|
// Sub-tabs
|
|
10448
10886
|
const friendCount = (friends.friends || []).length;
|
|
@@ -10494,7 +10932,7 @@ function renderFriendsList(friends) {
|
|
|
10494
10932
|
<button class="filter-btn \${friendsFilter==='human'?'active':''}" onclick="friendsFilter='human';filterFriendTable()">Human</button>
|
|
10495
10933
|
</div>
|
|
10496
10934
|
<span style="flex:1"></span>
|
|
10497
|
-
<button class="btn btn-sm btn-primary" onclick="showAddFriendModal()">\u2795 Add Friend</button>
|
|
10935
|
+
<button class="btn btn-sm btn-primary" onclick="showAddFriendModal()" \${isOffline ? 'disabled title="Unavailable while offline"' : ''}>\u2795 Add Friend</button>
|
|
10498
10936
|
<button class="btn btn-sm btn-default" onclick="loadFriends()">\u{1F504}</button>
|
|
10499
10937
|
</div>
|
|
10500
10938
|
<div class="card" style="padding:0;overflow:hidden">
|
|
@@ -11387,6 +11825,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
11387
11825
|
|
|
11388
11826
|
// Auto-refresh status every 30s
|
|
11389
11827
|
refreshTimer = setInterval(() => {
|
|
11828
|
+
updateOnlineStatus();
|
|
11390
11829
|
if (currentPage === 'dashboard') loadDashboard();
|
|
11391
11830
|
// Update status dot
|
|
11392
11831
|
api('/status').then(s => {
|
|
@@ -11395,6 +11834,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
11395
11834
|
if (dot) { dot.className = 'dot ' + (s.connected ? 'dot-ok' : 'dot-err'); }
|
|
11396
11835
|
const txt = $('#header-status');
|
|
11397
11836
|
if (txt) txt.textContent = s.connected ? 'Connected' : 'Disconnected';
|
|
11837
|
+
// Auto-remove offline banner when server reconnects
|
|
11838
|
+
if (s.connected) hideOfflineBanner();
|
|
11398
11839
|
}
|
|
11399
11840
|
});
|
|
11400
11841
|
}, 30000);
|
|
@@ -11964,8 +12405,16 @@ function createManagementHandler(ctx) {
|
|
|
11964
12405
|
});
|
|
11965
12406
|
return json(res, { friends });
|
|
11966
12407
|
} catch (err) {
|
|
11967
|
-
const
|
|
11968
|
-
|
|
12408
|
+
const friends = Array.from(store.friends.values()).map((f) => ({
|
|
12409
|
+
id: f.id,
|
|
12410
|
+
publicKeyFingerprint: f.publicKeyFingerprint || "",
|
|
12411
|
+
permissions: f.permissions || [],
|
|
12412
|
+
addedAt: f.addedAt?.toISOString() || null,
|
|
12413
|
+
lastMessageAt: f.lastMessageAt?.toISOString() || null,
|
|
12414
|
+
friendType: f.friendType || null,
|
|
12415
|
+
aiName: f.aiName || null
|
|
12416
|
+
}));
|
|
12417
|
+
return json(res, { friends, offline: true });
|
|
11969
12418
|
}
|
|
11970
12419
|
}
|
|
11971
12420
|
if (apiPath === "/friends" && method === "POST") {
|
|
@@ -12064,8 +12513,13 @@ function createManagementHandler(ctx) {
|
|
|
12064
12513
|
const data = await resp.json();
|
|
12065
12514
|
return json(res, { requests: data.requests || [] });
|
|
12066
12515
|
} catch (err) {
|
|
12067
|
-
const
|
|
12068
|
-
|
|
12516
|
+
const requests = store.pendingRequests.map((p) => ({
|
|
12517
|
+
id: p.requesterId,
|
|
12518
|
+
fromId: p.requesterId,
|
|
12519
|
+
status: "pending",
|
|
12520
|
+
createdAt: p.timestamp.toISOString()
|
|
12521
|
+
}));
|
|
12522
|
+
return json(res, { requests, offline: true });
|
|
12069
12523
|
}
|
|
12070
12524
|
}
|
|
12071
12525
|
if (apiPath.match(/^\/friends\/requests\/[^/]+\/accept$/) && method === "POST") {
|
|
@@ -12745,6 +13199,9 @@ var plugin = definePluginEntry({
|
|
|
12745
13199
|
} catch (e) {
|
|
12746
13200
|
logger.warn("[Init] WS connect failed: " + (e instanceof Error ? e.message : e));
|
|
12747
13201
|
}
|
|
13202
|
+
serverClient.onConnectionStateChange((newState, prevState) => {
|
|
13203
|
+
logger.info("[Init] Connection state changed: " + prevState + " \u2192 " + newState);
|
|
13204
|
+
});
|
|
12748
13205
|
serverClient.onWsMessage("relay", (data) => {
|
|
12749
13206
|
const msg = data;
|
|
12750
13207
|
if (!msg?.payload)
|
|
@@ -12758,14 +13215,9 @@ var plugin = definePluginEntry({
|
|
|
12758
13215
|
}
|
|
12759
13216
|
});
|
|
12760
13217
|
setInterval(() => {
|
|
12761
|
-
|
|
12762
|
-
|
|
12763
|
-
serverClient.connectWebSocket();
|
|
12764
|
-
} catch (_e) {
|
|
12765
|
-
}
|
|
12766
|
-
}
|
|
13218
|
+
store.cleanupExpiredTempNumbers();
|
|
13219
|
+
store.cleanupExpiredOfflineMessages();
|
|
12767
13220
|
}, 6e4);
|
|
12768
|
-
setInterval(() => store.cleanupExpiredTempNumbers(), 6e4);
|
|
12769
13221
|
api.registerTool({
|
|
12770
13222
|
label: "AICQ Friend Manager",
|
|
12771
13223
|
name: "chat-friend",
|
|
@@ -12910,10 +13362,12 @@ var plugin = definePluginEntry({
|
|
|
12910
13362
|
try {
|
|
12911
13363
|
return {
|
|
12912
13364
|
connected: serverClient.isConnected(),
|
|
13365
|
+
connectionState: serverClient.getConnectionState(),
|
|
12913
13366
|
agentId: aicqAgentId,
|
|
12914
13367
|
fingerprint: identityService.getPublicKeyFingerprint(),
|
|
12915
13368
|
friendCount: store.getFriendCount(),
|
|
12916
13369
|
sessionCount: store.sessions.size,
|
|
13370
|
+
offlineMessageCount: store.getOfflineMessageCount(),
|
|
12917
13371
|
serverUrl
|
|
12918
13372
|
};
|
|
12919
13373
|
} catch (err) {
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "aicq-chat",
|
|
3
3
|
"name": "AICQ Encrypted Chat",
|
|
4
|
-
"version": "1.
|
|
5
|
-
"description": "End-to-end encrypted chat plugin supporting AI-AI, Human-AI communication with P2P messaging
|
|
4
|
+
"version": "1.3.0",
|
|
5
|
+
"description": "End-to-end encrypted chat plugin supporting AI-AI, Human-AI communication with P2P messaging, offline queue, and Noise-XK handshake (Ed25519/X25519/AES-256-GCM)",
|
|
6
6
|
"enabledByDefault": false,
|
|
7
7
|
"channels": ["encrypted-chat"],
|
|
8
8
|
"tools": ["chat-friend", "chat-send", "chat-export-key"],
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aicq-openclaw-plugin",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "AICQ OpenClaw plugin - end-to-end encrypted P2P chat for AI agents",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "AICQ OpenClaw plugin - end-to-end encrypted P2P chat for AI agents with offline queue support",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"files": [
|
|
@@ -30,24 +30,26 @@
|
|
|
30
30
|
"openclaw": ">=2026.4.2"
|
|
31
31
|
},
|
|
32
32
|
"peerDependenciesMeta": {
|
|
33
|
-
"openclaw": {
|
|
33
|
+
"openclaw": {
|
|
34
|
+
"optional": true
|
|
35
|
+
}
|
|
34
36
|
},
|
|
35
37
|
"dependencies": {
|
|
36
38
|
"ws": "^8.16.0"
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|
|
39
41
|
"@aicq/crypto": "file:../shared/crypto",
|
|
40
|
-
"openclaw": "^2026.4.2",
|
|
41
|
-
"esbuild": "^0.25.0",
|
|
42
|
-
"typescript": "^5.3.3",
|
|
43
42
|
"@types/node": "^20.10.0",
|
|
44
|
-
"@types/ws": "^8.5.10",
|
|
45
|
-
"@types/uuid": "^9.0.7",
|
|
46
43
|
"@types/qrcode": "^1.5.5",
|
|
47
|
-
"
|
|
48
|
-
"
|
|
44
|
+
"@types/uuid": "^9.0.7",
|
|
45
|
+
"@types/ws": "^8.5.10",
|
|
46
|
+
"dotenv": "^16.3.1",
|
|
47
|
+
"esbuild": "^0.25.0",
|
|
48
|
+
"openclaw": "^2026.4.2",
|
|
49
49
|
"qrcode": "^1.5.3",
|
|
50
|
-
"
|
|
50
|
+
"ts-node-dev": "^2.0.0",
|
|
51
|
+
"typescript": "^5.3.3",
|
|
52
|
+
"uuid": "^9.0.0"
|
|
51
53
|
},
|
|
52
54
|
"keywords": [
|
|
53
55
|
"openclaw",
|