aicq-openclaw-plugin 1.2.1 → 1.4.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 CHANGED
@@ -105,7 +105,7 @@ var require_main = __commonJS({
105
105
  var fs6 = __require("fs");
106
106
  var path7 = __require("path");
107
107
  var os2 = __require("os");
108
- var crypto6 = __require("crypto");
108
+ var crypto7 = __require("crypto");
109
109
  var packageJson = require_package();
110
110
  var version = packageJson.version;
111
111
  var LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg;
@@ -324,7 +324,7 @@ var require_main = __commonJS({
324
324
  const authTag = ciphertext.subarray(-16);
325
325
  ciphertext = ciphertext.subarray(12, -16);
326
326
  try {
327
- const aesgcm = crypto6.createDecipheriv("aes-256-gcm", key, nonce);
327
+ const aesgcm = crypto7.createDecipheriv("aes-256-gcm", key, nonce);
328
328
  aesgcm.setAuthTag(authTag);
329
329
  return `${aesgcm.update(ciphertext)}${aesgcm.final()}`;
330
330
  } catch (error) {
@@ -7120,22 +7120,22 @@ var require_nacl_fast = __commonJS({
7120
7120
  randombytes = fn;
7121
7121
  };
7122
7122
  (function() {
7123
- var crypto6 = typeof self !== "undefined" ? self.crypto || self.msCrypto : null;
7124
- if (crypto6 && crypto6.getRandomValues) {
7123
+ var crypto7 = typeof self !== "undefined" ? self.crypto || self.msCrypto : null;
7124
+ if (crypto7 && crypto7.getRandomValues) {
7125
7125
  var QUOTA = 65536;
7126
7126
  nacl3.setPRNG(function(x, n) {
7127
7127
  var i, v = new Uint8Array(n);
7128
7128
  for (i = 0; i < n; i += QUOTA) {
7129
- crypto6.getRandomValues(v.subarray(i, i + Math.min(n - i, QUOTA)));
7129
+ crypto7.getRandomValues(v.subarray(i, i + Math.min(n - i, QUOTA)));
7130
7130
  }
7131
7131
  for (i = 0; i < n; i++) x[i] = v[i];
7132
7132
  cleanup(v);
7133
7133
  });
7134
7134
  } else if (typeof __require !== "undefined") {
7135
- crypto6 = __require("crypto");
7136
- if (crypto6 && crypto6.randomBytes) {
7135
+ crypto7 = __require("crypto");
7136
+ if (crypto7 && crypto7.randomBytes) {
7137
7137
  nacl3.setPRNG(function(x, n) {
7138
- var i, v = crypto6.randomBytes(n);
7138
+ var i, v = crypto7.randomBytes(n);
7139
7139
  for (i = 0; i < n; i++) x[i] = v[i];
7140
7140
  cleanup(v);
7141
7141
  });
@@ -7212,9 +7212,9 @@ var require_nacl_util = __commonJS({
7212
7212
  }
7213
7213
  });
7214
7214
 
7215
- // node_modules/@aicq/crypto/dist/nacl.js
7215
+ // ../shared/crypto/dist/nacl.js
7216
7216
  var require_nacl = __commonJS({
7217
- "node_modules/@aicq/crypto/dist/nacl.js"(exports) {
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
- // node_modules/@aicq/crypto/dist/keygen.js
7234
+ // ../shared/crypto/dist/keygen.js
7235
7235
  var require_keygen = __commonJS({
7236
- "node_modules/@aicq/crypto/dist/keygen.js"(exports) {
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
- // node_modules/@aicq/crypto/dist/signer.js
7275
+ // ../shared/crypto/dist/signer.js
7276
7276
  var require_signer = __commonJS({
7277
- "node_modules/@aicq/crypto/dist/signer.js"(exports) {
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
- // node_modules/@aicq/crypto/dist/keyExchange.js
7292
+ // ../shared/crypto/dist/keyExchange.js
7293
7293
  var require_keyExchange = __commonJS({
7294
- "node_modules/@aicq/crypto/dist/keyExchange.js"(exports) {
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
- // node_modules/@aicq/crypto/dist/cipher.js
7345
+ // ../shared/crypto/dist/cipher.js
7346
7346
  var require_cipher = __commonJS({
7347
- "node_modules/@aicq/crypto/dist/cipher.js"(exports) {
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
- // node_modules/@aicq/crypto/dist/message.js
7371
+ // ../shared/crypto/dist/message.js
7372
7372
  var require_message = __commonJS({
7373
- "node_modules/@aicq/crypto/dist/message.js"(exports) {
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
- // node_modules/@aicq/crypto/dist/password.js
7487
+ // ../shared/crypto/dist/password.js
7488
7488
  var require_password = __commonJS({
7489
- "node_modules/@aicq/crypto/dist/password.js"(exports) {
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;
@@ -7529,11 +7529,11 @@ var require_password = __commonJS({
7529
7529
  exports.deriveKeyFromPassword = deriveKeyFromPassword;
7530
7530
  exports.encryptWithPassword = encryptWithPassword2;
7531
7531
  exports.decryptWithPassword = decryptWithPassword2;
7532
- var crypto6 = __importStar(__require("crypto"));
7532
+ var crypto7 = __importStar(__require("crypto"));
7533
7533
  var nacl_js_1 = require_nacl();
7534
7534
  var cipher_js_1 = require_cipher();
7535
7535
  function deriveKeyFromPassword(password, salt, iterations = 1e5, keyLength = 32) {
7536
- return Uint8Array.from(crypto6.pbkdf2Sync(password, salt, iterations, keyLength, "sha512"));
7536
+ return Uint8Array.from(crypto7.pbkdf2Sync(password, salt, iterations, keyLength, "sha512"));
7537
7537
  }
7538
7538
  function deriveKeyLegacy(password, salt, iterations = 1e5) {
7539
7539
  const encoder = new TextEncoder();
@@ -7564,9 +7564,9 @@ var require_password = __commonJS({
7564
7564
  }
7565
7565
  });
7566
7566
 
7567
- // node_modules/@aicq/crypto/dist/handshake.js
7567
+ // ../shared/crypto/dist/handshake.js
7568
7568
  var require_handshake = __commonJS({
7569
- "node_modules/@aicq/crypto/dist/handshake.js"(exports) {
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
- // node_modules/@aicq/crypto/dist/index.js
7678
+ // ../shared/crypto/dist/index.js
7679
7679
  var require_dist = __commonJS({
7680
- "node_modules/@aicq/crypto/dist/index.js"(exports) {
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;
@@ -7779,12 +7779,12 @@ import * as fs from "fs";
7779
7779
  import * as path from "path";
7780
7780
 
7781
7781
  // node_modules/uuid/dist/esm-node/rng.js
7782
- import crypto from "crypto";
7782
+ import crypto2 from "crypto";
7783
7783
  var rnds8Pool = new Uint8Array(256);
7784
7784
  var poolPtr = rnds8Pool.length;
7785
7785
  function rng() {
7786
7786
  if (poolPtr > rnds8Pool.length - 16) {
7787
- crypto.randomFillSync(rnds8Pool);
7787
+ crypto2.randomFillSync(rnds8Pool);
7788
7788
  poolPtr = 0;
7789
7789
  }
7790
7790
  return rnds8Pool.slice(poolPtr, poolPtr += 16);
@@ -7800,9 +7800,9 @@ function unsafeStringify(arr, offset = 0) {
7800
7800
  }
7801
7801
 
7802
7802
  // node_modules/uuid/dist/esm-node/native.js
7803
- import crypto2 from "crypto";
7803
+ import crypto3 from "crypto";
7804
7804
  var native_default = {
7805
- randomUUID: crypto2.randomUUID
7805
+ randomUUID: crypto3.randomUUID
7806
7806
  };
7807
7807
 
7808
7808
  // node_modules/uuid/dist/esm-node/v4.js
@@ -7863,7 +7863,7 @@ function generateAgentId() {
7863
7863
  // dist/store.js
7864
7864
  import * as fs2 from "fs";
7865
7865
  import * as path2 from "path";
7866
- import * as crypto3 from "crypto";
7866
+ import * as crypto4 from "crypto";
7867
7867
  function encodeBuffer(buf) {
7868
7868
  return Buffer.from(buf).toString("base64");
7869
7869
  }
@@ -7871,11 +7871,11 @@ function decodeBuffer(b64) {
7871
7871
  return new Uint8Array(Buffer.from(b64, "base64"));
7872
7872
  }
7873
7873
  function deriveEncryptionKey(nodeId, salt) {
7874
- return Uint8Array.from(crypto3.pbkdf2Sync(nodeId, salt, 1e5, 32, "sha512"));
7874
+ return Uint8Array.from(crypto4.pbkdf2Sync(nodeId, salt, 1e5, 32, "sha512"));
7875
7875
  }
7876
7876
  function aes256GcmEncrypt(plaintext, key) {
7877
- const iv = crypto3.randomBytes(12);
7878
- const cipher = crypto3.createCipheriv("aes-256-gcm", key, iv);
7877
+ const iv = crypto4.randomBytes(12);
7878
+ const cipher = crypto4.createCipheriv("aes-256-gcm", key, iv);
7879
7879
  const encrypted = Buffer.concat([
7880
7880
  cipher.update(plaintext),
7881
7881
  cipher.final()
@@ -7892,7 +7892,7 @@ function aes256GcmDecrypt(packedBase64, key) {
7892
7892
  const iv = packed.subarray(0, 12);
7893
7893
  const authTag = packed.subarray(packed.length - 16);
7894
7894
  const ciphertext = packed.subarray(12, packed.length - 16);
7895
- const decipher = crypto3.createDecipheriv("aes-256-gcm", key, iv);
7895
+ const decipher = crypto4.createDecipheriv("aes-256-gcm", key, iv);
7896
7896
  decipher.setAuthTag(authTag);
7897
7897
  const decrypted = Buffer.concat([
7898
7898
  decipher.update(ciphertext),
@@ -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
- this.encryptionSalt = crypto3.randomBytes(16);
7919
+ this.encryptionSalt = crypto4.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
- * Save the current state to disk.
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
- * Secret keys are encrypted at rest using AES-256-GCM with a key
7940
- * derived from the node ID and a stored random salt.
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
- save() {
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
- fs2.writeFileSync(this.storePath, JSON.stringify(serialized, null, 2), "utf-8");
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. Falls back to plaintext if the data was stored before
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.save();
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.save();
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.save();
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.save();
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.save();
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.save();
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.save();
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.save();
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
  */
@@ -8192,7 +8335,7 @@ var PluginStore = class {
8192
8335
  // dist/services/identityService.js
8193
8336
  var import_qrcode = __toESM(require_lib(), 1);
8194
8337
  var import_crypto3 = __toESM(require_dist(), 1);
8195
- import * as crypto4 from "crypto";
8338
+ import * as crypto5 from "crypto";
8196
8339
  var IdentityService = class {
8197
8340
  constructor(store, logger) {
8198
8341
  this.exportTimers = /* @__PURE__ */ new Map();
@@ -8247,7 +8390,7 @@ var IdentityService = class {
8247
8390
  * @returns QR code data URL (string starting with "data:image/png;base64,")
8248
8391
  */
8249
8392
  async exportPrivateKeyQR(password) {
8250
- const exportToken = crypto4.randomBytes(32).toString("hex");
8393
+ const exportToken = crypto5.randomBytes(32).toString("hex");
8251
8394
  const exportPayload = {
8252
8395
  a: this.store.agentId,
8253
8396
  pk: (0, import_crypto3.encodeBase64)(this.store.identityKeys.publicKey),
@@ -8365,38 +8508,161 @@ 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
+ initialRetryWindowMs: 6e4,
8518
+ hourlyCheckIntervalMs: 36e5
8519
+ };
8368
8520
  var ServerClient = class {
8369
- constructor(serverUrl, store, logger) {
8521
+ constructor(serverUrl, store, logger, config2) {
8522
+ this.authToken = "";
8370
8523
  this.ws = null;
8371
8524
  this.wsReconnectTimer = null;
8372
8525
  this.heartbeatTimer = null;
8373
8526
  this.wsConnected = false;
8527
+ this.connectionState = "offline";
8528
+ this.reconnectDelay = DEFAULT_CONFIG.initialReconnectDelay;
8529
+ this.reconnectAttempts = 0;
8530
+ this.connectStartTimestamp = 0;
8531
+ this.hourlyCheckMode = false;
8532
+ this.hourlyCheckTimer = null;
8533
+ this.stateChangeCallbacks = [];
8374
8534
  this.wsHandlers = /* @__PURE__ */ new Map();
8375
8535
  this.serverUrl = serverUrl;
8376
8536
  this.store = store;
8377
8537
  this.logger = logger;
8538
+ this.config = { ...DEFAULT_CONFIG, ...config2 };
8539
+ }
8540
+ /**
8541
+ * Check whether the initial retry window has elapsed.
8542
+ */
8543
+ isInitialRetryWindowExpired() {
8544
+ if (this.connectStartTimestamp === 0)
8545
+ return false;
8546
+ return Date.now() - this.connectStartTimestamp >= this.config.initialRetryWindowMs;
8547
+ }
8548
+ /**
8549
+ * Set the JWT auth token for all subsequent requests.
8550
+ */
8551
+ setAuthToken(token) {
8552
+ this.authToken = token;
8553
+ this.logger.info("[Server] Auth token set (" + token.substring(0, 12) + "...)");
8554
+ }
8555
+ /**
8556
+ * Get the current auth token.
8557
+ */
8558
+ getAuthToken() {
8559
+ return this.authToken;
8560
+ }
8561
+ /**
8562
+ * Build common headers including Authorization when token is available.
8563
+ */
8564
+ authHeaders() {
8565
+ const headers = { "Content-Type": "application/json" };
8566
+ if (this.authToken) {
8567
+ headers["Authorization"] = "Bearer " + this.authToken;
8568
+ }
8569
+ return headers;
8570
+ }
8571
+ // ----------------------------------------------------------------
8572
+ // Connection state management
8573
+ // ----------------------------------------------------------------
8574
+ /**
8575
+ * Get the current connection state.
8576
+ */
8577
+ getConnectionState() {
8578
+ return this.connectionState;
8579
+ }
8580
+ /**
8581
+ * Register a callback for connection state changes.
8582
+ */
8583
+ onConnectionStateChange(callback) {
8584
+ this.stateChangeCallbacks.push(callback);
8585
+ }
8586
+ /**
8587
+ * Remove a connection state callback.
8588
+ */
8589
+ offConnectionStateChange(callback) {
8590
+ this.stateChangeCallbacks = this.stateChangeCallbacks.filter((cb) => cb !== callback);
8591
+ }
8592
+ /**
8593
+ * Update connection state and notify listeners.
8594
+ */
8595
+ setConnectionState(newState) {
8596
+ const previousState = this.connectionState;
8597
+ if (previousState === newState)
8598
+ return;
8599
+ this.connectionState = newState;
8600
+ this.logger.info(`[Server] Connection state: ${previousState} \u2192 ${newState}`);
8601
+ for (const callback of this.stateChangeCallbacks) {
8602
+ try {
8603
+ callback(newState, previousState);
8604
+ } catch (err) {
8605
+ this.logger.error("[Server] Connection state callback error:", err);
8606
+ }
8607
+ }
8378
8608
  }
8379
8609
  // ----------------------------------------------------------------
8380
8610
  // WebSocket connection
8381
8611
  // ----------------------------------------------------------------
8382
8612
  /**
8383
8613
  * Connect to the server via WebSocket and start heartbeat.
8614
+ * Strategy: retry aggressively for `initialRetryWindowMs` (default 1 minute),
8615
+ * then stop and switch to hourly checks.
8384
8616
  */
8385
8617
  connectWebSocket() {
8386
- const baseUrl = this.serverUrl.replace(/^http/, "ws");
8387
- const url = new URL(baseUrl + "/ws");
8388
- url.port = "443";
8389
- url.protocol = "wss:";
8390
- const wsUrl = url.toString();
8618
+ if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
8619
+ this.logger.debug("[Server] WebSocket already connecting/connected");
8620
+ return;
8621
+ }
8622
+ if (this.connectStartTimestamp === 0 && !this.hourlyCheckMode) {
8623
+ this.connectStartTimestamp = Date.now();
8624
+ }
8625
+ let wsUrl;
8626
+ try {
8627
+ const baseUrl = this.serverUrl.replace(/^http/, "ws");
8628
+ const url = new URL(baseUrl + "/ws");
8629
+ url.port = "443";
8630
+ url.protocol = "wss:";
8631
+ wsUrl = url.toString();
8632
+ } catch {
8633
+ wsUrl = this.serverUrl.replace(/^https?/, "wss") + "/ws";
8634
+ }
8391
8635
  this.logger.info("[Server] Connecting WebSocket to " + wsUrl);
8392
- this.ws = new WebSocket(wsUrl);
8636
+ this.setConnectionState("reconnecting");
8637
+ try {
8638
+ this.ws = new WebSocket(wsUrl);
8639
+ } catch (err) {
8640
+ this.logger.error("[Server] Failed to create WebSocket:", err);
8641
+ this.setConnectionState("offline");
8642
+ this.scheduleReconnect();
8643
+ return;
8644
+ }
8645
+ const connectTimeout = setTimeout(() => {
8646
+ if (this.ws && this.ws.readyState !== WebSocket.OPEN) {
8647
+ this.logger.warn("[Server] WebSocket connection timeout");
8648
+ this.ws.terminate();
8649
+ }
8650
+ }, this.config.requestTimeoutMs);
8393
8651
  this.ws.on("open", () => {
8652
+ clearTimeout(connectTimeout);
8394
8653
  this.wsConnected = true;
8654
+ this.reconnectDelay = this.config.initialReconnectDelay;
8655
+ this.reconnectAttempts = 0;
8656
+ this.connectStartTimestamp = 0;
8657
+ this.hourlyCheckMode = false;
8658
+ this.cancelHourlyCheck();
8659
+ this.setConnectionState("online");
8395
8660
  this.logger.info("[Server] WebSocket connected");
8396
8661
  this.wsSend({
8397
8662
  type: "online",
8398
8663
  nodeId: this.store.agentId,
8399
- publicKey: Buffer.from(this.store.identityKeys.publicKey).toString("base64")
8664
+ publicKey: Buffer.from(this.store.identityKeys.publicKey).toString("base64"),
8665
+ ...this.authToken ? { token: this.authToken } : {}
8400
8666
  });
8401
8667
  this.startHeartbeat();
8402
8668
  });
@@ -8409,29 +8675,31 @@ var ServerClient = class {
8409
8675
  }
8410
8676
  });
8411
8677
  this.ws.on("close", (code, reason) => {
8678
+ clearTimeout(connectTimeout);
8412
8679
  this.wsConnected = false;
8413
8680
  this.logger.info("[Server] WebSocket closed:", code, reason.toString());
8414
8681
  this.stopHeartbeat();
8682
+ this.setConnectionState("offline");
8415
8683
  this.scheduleReconnect();
8416
8684
  });
8417
8685
  this.ws.on("error", (err) => {
8686
+ clearTimeout(connectTimeout);
8418
8687
  this.logger.error("[Server] WebSocket error:", err.message);
8419
8688
  });
8420
8689
  }
8421
8690
  /**
8422
- * Disconnect the WebSocket.
8691
+ * Disconnect the WebSocket and stop all reconnection attempts.
8423
8692
  */
8424
8693
  disconnectWebSocket() {
8425
8694
  this.stopHeartbeat();
8426
- if (this.wsReconnectTimer) {
8427
- clearTimeout(this.wsReconnectTimer);
8428
- this.wsReconnectTimer = null;
8429
- }
8695
+ this.cancelReconnect();
8430
8696
  if (this.ws) {
8431
8697
  this.ws.close();
8432
8698
  this.ws = null;
8433
8699
  }
8434
8700
  this.wsConnected = false;
8701
+ this.connectStartTimestamp = 0;
8702
+ this.setConnectionState("offline");
8435
8703
  }
8436
8704
  /**
8437
8705
  * Check if the WebSocket is connected.
@@ -8449,25 +8717,114 @@ var ServerClient = class {
8449
8717
  }
8450
8718
  /**
8451
8719
  * Send a JSON message over the WebSocket.
8720
+ * Returns true if the message was sent, false if offline.
8452
8721
  */
8453
8722
  wsSend(data) {
8454
8723
  if (this.ws && this.ws.readyState === WebSocket.OPEN) {
8455
8724
  this.ws.send(JSON.stringify(data));
8456
- } else {
8457
- this.logger.warn("[Server] Cannot send \u2014 WebSocket not open");
8725
+ return true;
8458
8726
  }
8727
+ this.logger.warn("[Server] Cannot send \u2014 WebSocket not open");
8728
+ return false;
8729
+ }
8730
+ // ----------------------------------------------------------------
8731
+ // Reconnection: try for 1 min, then hourly
8732
+ // ----------------------------------------------------------------
8733
+ /**
8734
+ * Schedule a reconnection attempt.
8735
+ *
8736
+ * Phase 1 (initial retry window, default 1 minute):
8737
+ * Retries with exponential backoff (1s → 2s → 4s → ... → 60s max).
8738
+ * Once the window expires, stops retrying and switches to hourly checks.
8739
+ *
8740
+ * Phase 2 (hourly check mode):
8741
+ * After the initial burst fails, sets up a timer that retries once
8742
+ * every hour. If a retry succeeds, hourly mode is cancelled and
8743
+ * normal operation resumes.
8744
+ */
8745
+ scheduleReconnect() {
8746
+ if (this.hourlyCheckMode) {
8747
+ this.logger.info("[Server] Hourly check mode active \u2014 next attempt scheduled automatically");
8748
+ return;
8749
+ }
8750
+ if (this.isInitialRetryWindowExpired()) {
8751
+ this.logger.warn(`[Server] Initial retry window (${this.config.initialRetryWindowMs / 1e3}s) expired after ${this.reconnectAttempts} attempts. Switching to hourly check mode.`);
8752
+ this.enterHourlyCheckMode();
8753
+ return;
8754
+ }
8755
+ if (this.wsReconnectTimer)
8756
+ return;
8757
+ const jitter = 0.75 + Math.random() * 0.5;
8758
+ const delay = Math.min(this.reconnectDelay * jitter, this.config.maxReconnectDelay);
8759
+ this.reconnectAttempts++;
8760
+ this.logger.info(`[Server] Reconnecting in ${Math.round(delay)}ms (attempt #${this.reconnectAttempts})`);
8761
+ this.setConnectionState("reconnecting");
8762
+ this.wsReconnectTimer = setTimeout(() => {
8763
+ this.wsReconnectTimer = null;
8764
+ this.logger.info("[Server] Attempting WebSocket reconnect...");
8765
+ this.connectWebSocket();
8766
+ }, delay);
8767
+ this.reconnectDelay = Math.min(this.reconnectDelay * this.config.reconnectBackoffFactor, this.config.maxReconnectDelay);
8768
+ }
8769
+ /**
8770
+ * Enter hourly check mode: stop aggressive retries,
8771
+ * set up a single timer that fires once per hour.
8772
+ */
8773
+ enterHourlyCheckMode() {
8774
+ this.hourlyCheckMode = true;
8775
+ this.reconnectDelay = this.config.initialReconnectDelay;
8776
+ this.reconnectAttempts = 0;
8777
+ if (this.wsReconnectTimer) {
8778
+ clearTimeout(this.wsReconnectTimer);
8779
+ this.wsReconnectTimer = null;
8780
+ }
8781
+ this.setConnectionState("offline");
8782
+ this.logger.info(`[Server] Entering hourly check mode \u2014 will retry every ${this.config.hourlyCheckIntervalMs / 6e4} minutes`);
8783
+ this.hourlyCheckTimer = setTimeout(() => {
8784
+ this.hourlyCheckTimer = null;
8785
+ this.logger.info("[Server] Hourly check: attempting to reconnect...");
8786
+ this.connectWebSocket();
8787
+ }, this.config.hourlyCheckIntervalMs);
8788
+ }
8789
+ /**
8790
+ * Cancel the hourly check timer.
8791
+ */
8792
+ cancelHourlyCheck() {
8793
+ if (this.hourlyCheckTimer) {
8794
+ clearTimeout(this.hourlyCheckTimer);
8795
+ this.hourlyCheckTimer = null;
8796
+ }
8797
+ this.hourlyCheckMode = false;
8798
+ }
8799
+ /**
8800
+ * Cancel a pending reconnection.
8801
+ */
8802
+ cancelReconnect() {
8803
+ if (this.wsReconnectTimer) {
8804
+ clearTimeout(this.wsReconnectTimer);
8805
+ this.wsReconnectTimer = null;
8806
+ }
8807
+ this.cancelHourlyCheck();
8808
+ this.reconnectDelay = this.config.initialReconnectDelay;
8809
+ this.reconnectAttempts = 0;
8810
+ this.connectStartTimestamp = 0;
8459
8811
  }
8460
8812
  // ----------------------------------------------------------------
8461
8813
  // REST API methods
8462
8814
  // ----------------------------------------------------------------
8463
8815
  /**
8464
8816
  * Register this node on the server.
8817
+ * Captures JWT token from response if returned.
8465
8818
  */
8466
8819
  async registerNode(agentId, publicKey) {
8467
- return this.post("/api/v1/node/register", {
8820
+ const res = await this.fetchPost("/api/v1/node/register", {
8468
8821
  id: agentId,
8469
8822
  publicKey: Buffer.from(publicKey).toString("base64")
8470
8823
  });
8824
+ if (res?.token) {
8825
+ this.setAuthToken(res.token);
8826
+ }
8827
+ return res?.ok ?? false;
8471
8828
  }
8472
8829
  /**
8473
8830
  * Request a temporary 6-digit number for friend discovery.
@@ -8570,9 +8927,10 @@ var ServerClient = class {
8570
8927
  }
8571
8928
  /**
8572
8929
  * Send a relay message via the server (WebSocket fallback for P2P).
8930
+ * Returns true if sent, false if offline.
8573
8931
  */
8574
8932
  sendRelayMessage(targetId, payload) {
8575
- this.wsSend({
8933
+ return this.wsSend({
8576
8934
  type: "relay",
8577
8935
  targetId,
8578
8936
  payload,
@@ -8585,26 +8943,43 @@ var ServerClient = class {
8585
8943
  async fetchPost(path7, body) {
8586
8944
  const url = this.serverUrl + path7;
8587
8945
  try {
8946
+ const controller = new AbortController();
8947
+ const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
8588
8948
  const resp = await fetch(url, {
8589
8949
  method: "POST",
8590
- headers: { "Content-Type": "application/json" },
8591
- body: JSON.stringify(body)
8950
+ headers: this.authHeaders(),
8951
+ body: JSON.stringify(body),
8952
+ signal: controller.signal
8592
8953
  });
8954
+ clearTimeout(timeout);
8593
8955
  if (!resp.ok) {
8956
+ if (resp.status === 401 && this.authToken) {
8957
+ this.logger.warn(`[Server] 401 Unauthorized on ${path7} \u2014 token may be expired`);
8958
+ }
8594
8959
  const text = await resp.text();
8595
8960
  this.logger.error(`[Server] API error ${resp.status} on ${path7}: ${text}`);
8596
8961
  return null;
8597
8962
  }
8598
8963
  return await resp.json();
8599
8964
  } catch (err) {
8600
- this.logger.error(`[Server] API request failed for ${path7}:`, err);
8965
+ if (err instanceof DOMException && err.name === "AbortError") {
8966
+ this.logger.error(`[Server] API request timeout for ${path7}`);
8967
+ } else {
8968
+ this.logger.error(`[Server] API request failed for ${path7}:`, err);
8969
+ }
8601
8970
  return null;
8602
8971
  }
8603
8972
  }
8604
8973
  async fetchGet(path7) {
8605
8974
  const url = this.serverUrl + path7;
8606
8975
  try {
8607
- const resp = await fetch(url);
8976
+ const controller = new AbortController();
8977
+ const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
8978
+ const resp = await fetch(url, {
8979
+ signal: controller.signal,
8980
+ headers: this.authToken ? { Authorization: "Bearer " + this.authToken } : {}
8981
+ });
8982
+ clearTimeout(timeout);
8608
8983
  if (!resp.ok) {
8609
8984
  const text = await resp.text();
8610
8985
  this.logger.error(`[Server] API error ${resp.status} on ${path7}: ${text}`);
@@ -8612,18 +8987,26 @@ var ServerClient = class {
8612
8987
  }
8613
8988
  return await resp.json();
8614
8989
  } catch (err) {
8615
- this.logger.error(`[Server] GET request failed for ${path7}:`, err);
8990
+ if (err instanceof DOMException && err.name === "AbortError") {
8991
+ this.logger.error(`[Server] GET request timeout for ${path7}`);
8992
+ } else {
8993
+ this.logger.error(`[Server] GET request failed for ${path7}:`, err);
8994
+ }
8616
8995
  return null;
8617
8996
  }
8618
8997
  }
8619
8998
  async del(path7, body) {
8620
8999
  const url = this.serverUrl + path7;
8621
9000
  try {
9001
+ const controller = new AbortController();
9002
+ const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
8622
9003
  const resp = await fetch(url, {
8623
9004
  method: "DELETE",
8624
- headers: { "Content-Type": "application/json" },
8625
- body: body ? JSON.stringify(body) : void 0
9005
+ headers: this.authHeaders(),
9006
+ body: body ? JSON.stringify(body) : void 0,
9007
+ signal: controller.signal
8626
9008
  });
9009
+ clearTimeout(timeout);
8627
9010
  return resp.ok;
8628
9011
  } catch (err) {
8629
9012
  this.logger.error(`[Server] DELETE request failed for ${path7}:`, err);
@@ -8633,11 +9016,15 @@ var ServerClient = class {
8633
9016
  async post(path7, body) {
8634
9017
  const url = this.serverUrl + path7;
8635
9018
  try {
9019
+ const controller = new AbortController();
9020
+ const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
8636
9021
  const resp = await fetch(url, {
8637
9022
  method: "POST",
8638
- headers: { "Content-Type": "application/json" },
8639
- body: JSON.stringify(body)
9023
+ headers: this.authHeaders(),
9024
+ body: JSON.stringify(body),
9025
+ signal: controller.signal
8640
9026
  });
9027
+ clearTimeout(timeout);
8641
9028
  return resp.ok;
8642
9029
  } catch (err) {
8643
9030
  this.logger.error(`[Server] API request failed for ${path7}:`, err);
@@ -8658,9 +9045,10 @@ var ServerClient = class {
8658
9045
  }
8659
9046
  }
8660
9047
  startHeartbeat() {
9048
+ this.stopHeartbeat();
8661
9049
  this.heartbeatTimer = setInterval(() => {
8662
9050
  this.wsSend({ type: "ping", agentId: this.store.agentId, timestamp: Date.now() });
8663
- }, 3e4);
9051
+ }, this.config.heartbeatIntervalMs);
8664
9052
  }
8665
9053
  stopHeartbeat() {
8666
9054
  if (this.heartbeatTimer) {
@@ -8668,20 +9056,11 @@ var ServerClient = class {
8668
9056
  this.heartbeatTimer = null;
8669
9057
  }
8670
9058
  }
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
9059
  };
8681
9060
 
8682
9061
  // dist/handshake/handshakeManager.js
8683
9062
  var import_crypto4 = __toESM(require_dist(), 1);
8684
- import * as crypto5 from "crypto";
9063
+ import * as crypto6 from "crypto";
8685
9064
  var HandshakeManager = class {
8686
9065
  constructor(store, serverClient, config2, logger) {
8687
9066
  this.pendingInitiates = /* @__PURE__ */ new Map();
@@ -8899,7 +9278,7 @@ var HandshakeManager = class {
8899
9278
  combined.set(ee, 0);
8900
9279
  combined.set(se, 32);
8901
9280
  combined.set(es, 64);
8902
- const nonce = crypto5.randomBytes(16).toString("hex");
9281
+ const nonce = crypto6.randomBytes(16).toString("hex");
8903
9282
  const newSessionKey = (0, import_crypto4.deriveSessionKey)(combined, "aicq-session-rotate-" + nonce);
8904
9283
  const updatedSession = {
8905
9284
  peerId,
@@ -9335,12 +9714,18 @@ var EncryptedChatChannel = class {
9335
9714
  this.api = null;
9336
9715
  this.dataDir = "";
9337
9716
  this.fileChunkBuffers = /* @__PURE__ */ new Map();
9717
+ this.flushingOffline = false;
9338
9718
  this.store = store;
9339
9719
  this.handshakeManager = handshakeManager;
9340
9720
  this.p2pManager = p2pManager;
9341
9721
  this.serverClient = serverClient;
9342
9722
  this.logger = logger;
9343
9723
  this.dataDir = dataDir;
9724
+ this.serverClient.onConnectionStateChange((newState, _prevState) => {
9725
+ if (newState === "online") {
9726
+ this.onReconnected();
9727
+ }
9728
+ });
9344
9729
  }
9345
9730
  /**
9346
9731
  * Set the OpenClaw API reference (for emitting events).
@@ -9348,6 +9733,17 @@ var EncryptedChatChannel = class {
9348
9733
  setAPI(api) {
9349
9734
  this.api = api;
9350
9735
  }
9736
+ /**
9737
+ * Called when the WebSocket connection is re-established.
9738
+ * Flushes all queued offline messages.
9739
+ */
9740
+ onReconnected() {
9741
+ const pendingCount = this.store.getOfflineMessageCount();
9742
+ if (pendingCount > 0) {
9743
+ this.logger.info(`[Chat] Back online \u2014 flushing ${pendingCount} queued offline messages`);
9744
+ this.flushOfflineMessages();
9745
+ }
9746
+ }
9351
9747
  /**
9352
9748
  * Handle an incoming encrypted message from a peer.
9353
9749
  *
@@ -9382,7 +9778,7 @@ var EncryptedChatChannel = class {
9382
9778
  });
9383
9779
  }
9384
9780
  friend.lastMessageAt = /* @__PURE__ */ new Date();
9385
- this.store.save();
9781
+ this.store.markDirty();
9386
9782
  }
9387
9783
  /**
9388
9784
  * Send an encrypted message to a peer.
@@ -9390,7 +9786,9 @@ var EncryptedChatChannel = class {
9390
9786
  * Encrypts with the session key, signs with the Ed25519 identity key,
9391
9787
  * and sends via P2P if available, falling back to WebSocket relay.
9392
9788
  *
9393
- * @returns true if the message was sent successfully
9789
+ * When offline, the message is queued for later delivery.
9790
+ *
9791
+ * @returns true if the message was sent or queued successfully
9394
9792
  */
9395
9793
  send(toId, plaintext) {
9396
9794
  const friend = this.store.getFriend(toId);
@@ -9408,14 +9806,26 @@ var EncryptedChatChannel = class {
9408
9806
  }
9409
9807
  const wireData = (0, import_crypto6.encryptMessage)(plaintext, sessionKey, this.store.identityKeys.secretKey, this.store.identityKeys.publicKey);
9410
9808
  const buf = Buffer.from(wireData);
9411
- if (this.p2pManager.isConnected(toId) && this.p2pManager.send(toId, buf)) {
9412
- this.logger.debug("[Chat] Sent message via P2P to " + toId);
9809
+ const encodedData = (0, import_crypto6.encodeBase64)(wireData);
9810
+ const isOnline = this.serverClient.isConnected();
9811
+ if (isOnline) {
9812
+ if (this.p2pManager.isConnected(toId) && this.p2pManager.send(toId, buf)) {
9813
+ this.logger.debug("[Chat] Sent message via P2P to " + toId);
9814
+ } else {
9815
+ const sent = this.serverClient.sendRelayMessage(toId, {
9816
+ channel: "encrypted-chat",
9817
+ data: encodedData
9818
+ });
9819
+ if (!sent) {
9820
+ this.logger.warn("[Chat] WebSocket send failed \u2014 queuing message for offline delivery");
9821
+ this.store.enqueueOfflineMessage(toId, encodedData);
9822
+ } else {
9823
+ this.logger.debug("[Chat] Sent message via relay to " + toId);
9824
+ }
9825
+ }
9413
9826
  } else {
9414
- this.serverClient.sendRelayMessage(toId, {
9415
- channel: "encrypted-chat",
9416
- data: (0, import_crypto6.encodeBase64)(wireData)
9417
- });
9418
- this.logger.debug("[Chat] Sent message via relay to " + toId);
9827
+ this.logger.info("[Chat] Offline \u2014 message queued for delivery to " + toId);
9828
+ this.store.enqueueOfflineMessage(toId, encodedData);
9419
9829
  }
9420
9830
  const session = this.store.getSession(toId);
9421
9831
  if (session) {
@@ -9423,10 +9833,71 @@ var EncryptedChatChannel = class {
9423
9833
  if (session.messageCount % 100 === 0 || Date.now() - session.createdAt.getTime() > 36e5) {
9424
9834
  this.handshakeManager.rotateSessionKey(toId);
9425
9835
  }
9426
- this.store.save();
9836
+ this.store.markDirty();
9427
9837
  }
9428
9838
  return true;
9429
9839
  }
9840
+ /**
9841
+ * Flush all queued offline messages.
9842
+ * Called automatically when connection is re-established.
9843
+ */
9844
+ flushOfflineMessages() {
9845
+ if (this.flushingOffline)
9846
+ return;
9847
+ this.flushingOffline = true;
9848
+ const flushNext = () => {
9849
+ const msg = this.store.dequeueOfflineMessage();
9850
+ if (!msg) {
9851
+ this.flushingOffline = false;
9852
+ const remaining = this.store.getOfflineMessageCount();
9853
+ if (remaining === 0) {
9854
+ this.logger.info("[Chat] All offline messages flushed");
9855
+ }
9856
+ return;
9857
+ }
9858
+ if (!this.serverClient.isConnected()) {
9859
+ this.store.offlineMessages.unshift(msg);
9860
+ this.flushingOffline = false;
9861
+ this.logger.warn("[Chat] Connection lost during offline flush");
9862
+ return;
9863
+ }
9864
+ const friend = this.store.getFriend(msg.targetId);
9865
+ let sessionKey = this.handshakeManager.getSessionKey(msg.targetId);
9866
+ if (!sessionKey && friend?.sessionKey) {
9867
+ sessionKey = friend.sessionKey;
9868
+ }
9869
+ if (!sessionKey) {
9870
+ this.logger.warn("[Chat] Skipping offline message to " + msg.targetId + " (no session key)");
9871
+ this.store.markDirty();
9872
+ setImmediate(flushNext);
9873
+ return;
9874
+ }
9875
+ const sent = this.serverClient.sendRelayMessage(msg.targetId, {
9876
+ channel: "encrypted-chat",
9877
+ data: msg.encryptedData
9878
+ });
9879
+ if (sent) {
9880
+ this.logger.debug("[Chat] Flushed offline message to " + msg.targetId);
9881
+ } else {
9882
+ this.logger.warn("[Chat] Failed to flush offline message to " + msg.targetId);
9883
+ msg.retryCount++;
9884
+ if (msg.retryCount < msg.maxRetries) {
9885
+ this.store.offlineMessages.unshift(msg);
9886
+ } else {
9887
+ this.logger.error("[Chat] Dropped offline message to " + msg.targetId + " (max retries exceeded)");
9888
+ }
9889
+ }
9890
+ this.store.markDirty();
9891
+ setImmediate(flushNext);
9892
+ };
9893
+ flushNext();
9894
+ }
9895
+ /**
9896
+ * Get the count of pending offline messages.
9897
+ */
9898
+ getPendingOfflineCount() {
9899
+ return this.store.getOfflineMessageCount();
9900
+ }
9430
9901
  /**
9431
9902
  * Handle an incoming file chunk from a peer.
9432
9903
  *
@@ -9511,6 +9982,7 @@ var EncryptedChatChannel = class {
9511
9982
  */
9512
9983
  cleanup() {
9513
9984
  this.fileChunkBuffers.clear();
9985
+ this.flushingOffline = false;
9514
9986
  }
9515
9987
  };
9516
9988
 
@@ -9820,17 +10292,17 @@ var MessageSendingHook = class {
9820
10292
  var CSS = `
9821
10293
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9822
10294
  :root {
9823
- --bg: #0f1117; --bg2: #1a1d27; --bg3: #242836; --bg4: #2e3347;
9824
- --bg5: #353a50; --text: #e4e6ef; --text2: #9499b3; --text3: #5c6080;
9825
- --accent: #6366f1; --accent2: #818cf8; --accent-bg: rgba(99,102,241,.12);
9826
- --ok: #34d399; --ok-bg: rgba(52,211,153,.12); --warn: #fbbf24; --warn-bg: rgba(251,191,36,.12);
9827
- --danger: #ef4444; --danger-bg: rgba(239,68,68,.12); --info: #60a5fa; --info-bg: rgba(96,165,250,.12);
9828
- --border: #2e3347; --radius: 8px; --radius-lg: 12px; --shadow: 0 2px 12px rgba(0,0,0,.3);
10295
+ --bg: #f5f7fa; --bg2: #ffffff; --bg3: #f0f2f5; --bg4: #e4e7ec;
10296
+ --bg5: #d1d5db; --text: #1f2937; --text2: #6b7280; --text3: #9ca3af;
10297
+ --accent: #4f46e5; --accent2: #6366f1; --accent-bg: rgba(79,70,229,.08);
10298
+ --ok: #10b981; --ok-bg: rgba(16,185,129,.08); --warn: #f59e0b; --warn-bg: rgba(245,158,11,.08);
10299
+ --danger: #ef4444; --danger-bg: rgba(239,68,68,.08); --info: #3b82f6; --info-bg: rgba(59,130,246,.08);
10300
+ --border: #e5e7eb; --radius: 8px; --radius-lg: 12px; --shadow: 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04);
9829
10301
  --sidebar-w: 240px; --header-h: 56px;
9830
10302
  --transition: .2s cubic-bezier(.4,0,.2,1);
9831
10303
  }
9832
10304
  html, body { height: 100%; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: var(--text); font-size: 14px; line-height: 1.6; overflow: hidden; }
9833
- a { color: var(--info); text-decoration: none; }
10305
+ a { color: var(--accent); text-decoration: none; }
9834
10306
  ::-webkit-scrollbar { width: 6px; height: 6px; }
9835
10307
  ::-webkit-scrollbar-track { background: transparent; }
9836
10308
  ::-webkit-scrollbar-thumb { background: var(--bg4); border-radius: 3px; }
@@ -9842,7 +10314,7 @@ a { color: var(--info); text-decoration: none; }
9842
10314
  /* Sidebar */
9843
10315
  .sidebar {
9844
10316
  width: var(--sidebar-w); min-width: var(--sidebar-w); height: 100vh;
9845
- background: var(--bg2); border-right: 1px solid var(--border);
10317
+ background: #ffffff; border-right: 1px solid var(--border);
9846
10318
  display: flex; flex-direction: column; transition: width var(--transition), min-width var(--transition);
9847
10319
  z-index: 20; overflow: hidden;
9848
10320
  }
@@ -9857,7 +10329,7 @@ a { color: var(--info); text-decoration: none; }
9857
10329
  border-bottom: 1px solid var(--border); min-height: var(--header-h);
9858
10330
  }
9859
10331
  .sidebar-logo {
9860
- width: 32px; height: 32px; border-radius: 8px; background: linear-gradient(135deg, var(--accent), #a855f7);
10332
+ width: 32px; height: 32px; border-radius: 8px; background: linear-gradient(135deg, var(--accent), #7c3aed);
9861
10333
  display: grid; place-items: center; font-size: 13px; font-weight: 800; color: #fff; flex-shrink: 0;
9862
10334
  }
9863
10335
  .sidebar-header-text h1 { font-size: 14px; font-weight: 700; line-height: 1.2; }
@@ -9896,7 +10368,7 @@ a { color: var(--info); text-decoration: none; }
9896
10368
  .main-header {
9897
10369
  height: var(--header-h); min-height: var(--header-h);
9898
10370
  display: flex; align-items: center; gap: 16px; padding: 0 24px;
9899
- background: var(--bg2); border-bottom: 1px solid var(--border);
10371
+ background: #ffffff; border-bottom: 1px solid var(--border);
9900
10372
  }
9901
10373
  .toggle-btn {
9902
10374
  width: 32px; height: 32px; border-radius: 6px; background: var(--bg3);
@@ -9928,12 +10400,12 @@ a { color: var(--info); text-decoration: none; }
9928
10400
  .btn-default:hover:not(:disabled) { background: var(--bg4); }
9929
10401
  .btn-primary { background: var(--accent); color: #fff; }
9930
10402
  .btn-primary:hover:not(:disabled) { background: var(--accent2); }
9931
- .btn-danger { background: var(--danger-bg); color: #fca5a5; border: 1px solid rgba(239,68,68,.2); }
9932
- .btn-danger:hover:not(:disabled) { background: rgba(239,68,68,.2); }
9933
- .btn-ok { background: var(--ok-bg); color: #6ee7b7; border: 1px solid rgba(52,211,153,.2); }
9934
- .btn-ok:hover:not(:disabled) { background: rgba(52,211,153,.2); }
9935
- .btn-warn { background: var(--warn-bg); color: #fde68a; border: 1px solid rgba(251,191,36,.2); }
9936
- .btn-warn:hover:not(:disabled) { background: rgba(251,191,36,.2); }
10403
+ .btn-danger { background: var(--danger-bg); color: #dc2626; border: 1px solid rgba(239,68,68,.15); }
10404
+ .btn-danger:hover:not(:disabled) { background: rgba(239,68,68,.15); }
10405
+ .btn-ok { background: var(--ok-bg); color: #059669; border: 1px solid rgba(16,185,129,.15); }
10406
+ .btn-ok:hover:not(:disabled) { background: rgba(16,185,129,.15); }
10407
+ .btn-warn { background: var(--warn-bg); color: #d97706; border: 1px solid rgba(245,158,11,.15); }
10408
+ .btn-warn:hover:not(:disabled) { background: rgba(245,158,11,.15); }
9937
10409
  .btn-ghost { background: transparent; color: var(--text2); }
9938
10410
  .btn-ghost:hover:not(:disabled) { background: var(--bg3); color: var(--text); }
9939
10411
  .btn-sm { padding: 4px 10px; font-size: 12px; }
@@ -10006,17 +10478,18 @@ tbody tr:hover { background: var(--bg3); }
10006
10478
  .provider-card .prov-desc { font-size: 12px; color: var(--text3); margin-bottom: 10px; }
10007
10479
  .provider-card .prov-model { font-size: 11px; color: var(--text2); background: var(--bg3); padding: 3px 8px; border-radius: 4px; display: inline-block; }
10008
10480
  .provider-card .prov-actions { margin-top: 12px; display: flex; gap: 6px; }
10481
+ .provider-card.custom-provider { border-color: var(--accent); border-style: dashed; }
10009
10482
 
10010
10483
  /* Modal */
10011
10484
  .modal-overlay {
10012
- position: fixed; inset: 0; background: rgba(0,0,0,.65); display: flex;
10485
+ position: fixed; inset: 0; background: rgba(0,0,0,.3); display: flex;
10013
10486
  align-items: center; justify-content: center; z-index: 100;
10014
10487
  animation: fadeIn .15s ease-out;
10015
10488
  }
10016
10489
  .modal-overlay.hidden { display: none; }
10017
10490
  .modal {
10018
10491
  background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius-lg);
10019
- padding: 28px; width: 90%; max-width: 520px; box-shadow: 0 8px 32px rgba(0,0,0,.5);
10492
+ padding: 28px; width: 90%; max-width: 520px; box-shadow: 0 10px 25px rgba(0,0,0,.1), 0 6px 10px rgba(0,0,0,.06);
10020
10493
  max-height: 85vh; overflow-y: auto; animation: modalIn .2s ease-out;
10021
10494
  }
10022
10495
  @keyframes modalIn { from { transform: scale(.95); opacity: 0; } to { transform: scale(1); opacity: 1; } }
@@ -10052,15 +10525,15 @@ tbody tr:hover { background: var(--bg3); }
10052
10525
  /* Toast */
10053
10526
  .toast-container { position: fixed; bottom: 20px; right: 20px; z-index: 200; display: flex; flex-direction: column; gap: 8px; }
10054
10527
  .toast {
10055
- padding: 12px 20px; border-radius: var(--radius); color: #fff; font-size: 13px;
10056
- animation: slideIn .2s ease-out; box-shadow: var(--shadow); display: flex; align-items: center; gap: 8px;
10528
+ padding: 12px 20px; border-radius: var(--radius); color: var(--text); font-size: 13px;
10529
+ animation: slideIn .2s ease-out; box-shadow: 0 4px 12px rgba(0,0,0,.08); display: flex; align-items: center; gap: 8px;
10057
10530
  max-width: 400px;
10058
10531
  }
10059
10532
  .toast.hidden { display: none; }
10060
- .toast-ok { background: #065f46; border: 1px solid var(--ok); }
10061
- .toast-err { background: #7f1d1d; border: 1px solid var(--danger); }
10062
- .toast-info { background: #1e3a5f; border: 1px solid var(--info); }
10063
- .toast-warn { background: #78350f; border: 1px solid var(--warn); }
10533
+ .toast-ok { background: #ecfdf5; border: 1px solid #a7f3d0; color: #065f46; }
10534
+ .toast-err { background: #fef2f2; border: 1px solid #fecaca; color: #991b1b; }
10535
+ .toast-info { background: #eff6ff; border: 1px solid #bfdbfe; color: #1e3a5f; }
10536
+ .toast-warn { background: #fffbeb; border: 1px solid #fde68a; color: #92400e; }
10064
10537
  @keyframes slideIn { from { transform: translateX(20px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
10065
10538
 
10066
10539
  /* Actions cell */
@@ -10092,8 +10565,9 @@ tbody tr:hover { background: var(--bg3); }
10092
10565
 
10093
10566
  /* Offline banner */
10094
10567
  .offline-banner {
10095
- background: linear-gradient(90deg, #7f1d1d, #991b1b);
10096
- color: #fca5a5;
10568
+ background: #fef2f2;
10569
+ color: #991b1b;
10570
+ border-bottom: 1px solid #fecaca;
10097
10571
  padding: 10px 24px;
10098
10572
  font-size: 13px;
10099
10573
  display: flex;
@@ -10121,6 +10595,380 @@ tbody tr:hover { background: var(--bg3); }
10121
10595
  }
10122
10596
  `;
10123
10597
  var JS = `
10598
+ // \u2500\u2500 i18n \u2500\u2500
10599
+ const _lang = 'zh'; // Default Chinese; set to 'en' to switch to English
10600
+ const _T = {
10601
+ // Sidebar
10602
+ nav_overview: { zh: '\u6982\u89C8', en: 'Overview' },
10603
+ nav_management: { zh: '\u7BA1\u7406', en: 'Management' },
10604
+ nav_system: { zh: '\u7CFB\u7EDF', en: 'System' },
10605
+ nav_dashboard: { zh: '\u4EEA\u8868\u76D8', en: 'Dashboard' },
10606
+ nav_agents: { zh: '\u667A\u80FD\u4F53', en: 'Agents' },
10607
+ nav_friends: { zh: '\u597D\u53CB', en: 'Friends' },
10608
+ nav_models: { zh: '\u6A21\u578B', en: 'Models' },
10609
+ nav_settings: { zh: '\u8BBE\u7F6E', en: 'Settings' },
10610
+ collapse_sidebar: { zh: '\u6536\u8D77\u4FA7\u680F', en: 'Collapse sidebar' },
10611
+ management_console: { zh: '\u7BA1\u7406\u63A7\u5236\u53F0', en: 'Management Console' },
10612
+ // Header
10613
+ connecting: { zh: '\u8FDE\u63A5\u4E2D...', en: 'Connecting...' },
10614
+ connected: { zh: '\u5DF2\u8FDE\u63A5', en: 'Connected' },
10615
+ disconnected: { zh: '\u5DF2\u65AD\u5F00', en: 'Disconnected' },
10616
+ refresh: { zh: '\u5237\u65B0', en: 'Refresh' },
10617
+ // Dashboard
10618
+ loading_dashboard: { zh: '\u6B63\u5728\u52A0\u8F7D\u4EEA\u8868\u76D8...', en: 'Loading dashboard...' },
10619
+ failed_connect: { zh: '\u65E0\u6CD5\u8FDE\u63A5\u5230 AICQ \u63D2\u4EF6', en: 'Failed to connect to AICQ plugin' },
10620
+ server_status: { zh: '\u670D\u52A1\u5668\u72B6\u6001', en: 'Server Status' },
10621
+ total_friends: { zh: '\u597D\u53CB\u603B\u6570', en: 'Total Friends' },
10622
+ active_sessions: { zh: '\u6D3B\u8DC3\u4F1A\u8BDD', en: 'Active Sessions' },
10623
+ encrypted_sessions: { zh: '\u52A0\u5BC6\u4F1A\u8BDD', en: 'Encrypted sessions' },
10624
+ agent_id: { zh: '\u667A\u80FD\u4F53 ID', en: 'Agent ID' },
10625
+ fingerprint: { zh: '\u6307\u7EB9', en: 'Fingerprint' },
10626
+ recent_friends: { zh: '\u6700\u8FD1\u597D\u53CB', en: 'Recent Friends' },
10627
+ view_all: { zh: '\u67E5\u770B\u5168\u90E8 \u2192', en: 'View All \u2192' },
10628
+ identity_info: { zh: '\u8EAB\u4EFD\u4FE1\u606F', en: 'Identity Info' },
10629
+ server_url: { zh: '\u670D\u52A1\u5668\u5730\u5740', en: 'Server URL' },
10630
+ connection: { zh: '\u8FDE\u63A5', en: 'Connection' },
10631
+ online: { zh: '\u5728\u7EBF', en: 'Online' },
10632
+ offline: { zh: '\u79BB\u7EBF', en: 'Offline' },
10633
+ plugin_version: { zh: '\u63D2\u4EF6\u7248\u672C', en: 'Plugin Version' },
10634
+ mgmt_ui_access: { zh: '\u7BA1\u7406\u754C\u9762\u8BBF\u95EE', en: 'Management UI Access' },
10635
+ current_url: { zh: '\u5F53\u524D\u5730\u5740', en: 'Current URL' },
10636
+ local_access: { zh: '\u672C\u5730\u8BBF\u95EE', en: 'Local Access' },
10637
+ open: { zh: '\u6253\u5F00', en: 'Open' },
10638
+ gateway_path: { zh: '\u7F51\u5173\u8DEF\u5F84', en: 'Gateway Path' },
10639
+ no_friends_yet: { zh: '\u6682\u65E0\u597D\u53CB', en: 'No friends yet' },
10640
+ // Agents
10641
+ loading_agents: { zh: '\u6B63\u5728\u52A0\u8F7D\u667A\u80FD\u4F53...', en: 'Loading agents...' },
10642
+ active: { zh: '\u542F\u7528', en: 'active' },
10643
+ disabled: { zh: '\u7981\u7528', en: 'disabled' },
10644
+ default_model: { zh: '\u9ED8\u8BA4', en: 'default' },
10645
+ no_agents_configured: { zh: '\u672A\u914D\u7F6E\u667A\u80FD\u4F53', en: 'No agents configured' },
10646
+ add_agents_hint: { zh: '\u8BF7\u5728 openclaw.json \u6216 stableclaw.json \u914D\u7F6E\u6587\u4EF6\u4E2D\u6DFB\u52A0\u667A\u80FD\u4F53', en: 'Add agents to your openclaw.json or stableclaw.json config file' },
10647
+ search_agents: { zh: '\u641C\u7D22\u667A\u80FD\u4F53...', en: 'Search agents...' },
10648
+ add_agent: { zh: '\u6DFB\u52A0\u667A\u80FD\u4F53', en: 'Add Agent' },
10649
+ agent_list_from: { zh: '\u667A\u80FD\u4F53\u5217\u8868\u6765\u81EA', en: 'Agent list from' },
10650
+ total_label: { zh: '\u603B\u8BA1', en: 'Total' },
10651
+ agents_configured: { zh: '\u4E2A\u667A\u80FD\u4F53\u5DF2\u914D\u7F6E', en: 'agents configured' },
10652
+ status: { zh: '\u72B6\u6001', en: 'Status' },
10653
+ agent: { zh: '\u667A\u80FD\u4F53', en: 'Agent' },
10654
+ model: { zh: '\u6A21\u578B', en: 'Model' },
10655
+ provider: { zh: '\u63D0\u4F9B\u5546', en: 'Provider' },
10656
+ system_prompt: { zh: '\u7CFB\u7EDF\u63D0\u793A\u8BCD', en: 'System Prompt' },
10657
+ actions: { zh: '\u64CD\u4F5C', en: 'Actions' },
10658
+ confirm_delete_agent: { zh: '\u786E\u5B9A\u8981\u5220\u9664\u8FD9\u4E2A\u667A\u80FD\u4F53\u5417\uFF1F', en: 'Are you sure you want to delete this agent?' },
10659
+ agent_deleted: { zh: '\u667A\u80FD\u4F53\u5DF2\u5220\u9664', en: 'Agent deleted' },
10660
+ delete_failed: { zh: '\u5220\u9664\u5931\u8D25', en: 'Delete failed' },
10661
+ default_model_badge: { zh: '\u9ED8\u8BA4', en: 'Default' },
10662
+ provider_model: { zh: '\u6765\u81EA\u6A21\u578B\u63D0\u4F9B\u5546', en: 'From model provider' },
10663
+ default_model_label: { zh: '\u9ED8\u8BA4\u6A21\u578B', en: 'Default Model' },
10664
+ set_as_default: { zh: '\u8BBE\u4E3A\u9ED8\u8BA4', en: 'Set as default' },
10665
+ models_under_provider: { zh: '\u4E2A\u6A21\u578B', en: 'models' },
10666
+ model_id_label: { zh: '\u6A21\u578BID', en: 'Model ID' },
10667
+ model_name_label: { zh: '\u6A21\u578B\u540D\u79F0', en: 'Model Name' },
10668
+ add_new_agent: { zh: '\u2795 \u6DFB\u52A0\u65B0\u667A\u80FD\u4F53', en: '\u2795 Add New Agent' },
10669
+ edit_agent: { zh: '\u270F\uFE0F \u7F16\u8F91\u667A\u80FD\u4F53', en: '\u270F\uFE0F Edit Agent' },
10670
+ agent_name_required: { zh: '\u8BF7\u8F93\u5165\u667A\u80FD\u4F53\u540D\u79F0', en: 'Agent name is required' },
10671
+ agent_updated: { zh: '\u667A\u80FD\u4F53\u5DF2\u66F4\u65B0', en: 'Agent updated' },
10672
+ agent_added: { zh: '\u667A\u80FD\u4F53\u5DF2\u6DFB\u52A0', en: 'Agent added' },
10673
+ no_data: { zh: '\u6682\u65E0\u6570\u636E', en: 'No data' },
10674
+ // Friends
10675
+ loading_friends: { zh: '\u6B63\u5728\u52A0\u8F7D\u597D\u53CB...', en: 'Loading friends...' },
10676
+ friends: { zh: '\u597D\u53CB', en: 'Friends' },
10677
+ requests: { zh: '\u8BF7\u6C42', en: 'Requests' },
10678
+ sessions: { zh: '\u4F1A\u8BDD', en: 'Sessions' },
10679
+ search_friends: { zh: '\u641C\u7D22\u597D\u53CB...', en: 'Search friends...' },
10680
+ all: { zh: '\u5168\u90E8', en: 'All' },
10681
+ ai: { zh: 'AI', en: 'AI' },
10682
+ human: { zh: '\u4EBA\u7C7B', en: 'Human' },
10683
+ add_friend: { zh: '\u6DFB\u52A0\u597D\u53CB', en: 'Add Friend' },
10684
+ type: { zh: '\u7C7B\u578B', en: 'Type' },
10685
+ friend_label: { zh: '\u597D\u53CB', en: 'Friend' },
10686
+ permissions: { zh: '\u6743\u9650', en: 'Permissions' },
10687
+ last_message: { zh: '\u6700\u540E\u6D88\u606F', en: 'Last Message' },
10688
+ add_friend_hint: { zh: '\u4F7F\u75286\u4F4D\u4E34\u65F6\u53F7\u7801\u6216\u8282\u70B9ID\u6DFB\u52A0\u597D\u53CB', en: 'Add a friend using their 6-digit temp number or node ID' },
10689
+ unavailable_offline: { zh: '\u79BB\u7EBF\u65F6\u4E0D\u53EF\u7528', en: 'Unavailable while offline' },
10690
+ request_id: { zh: '\u8BF7\u6C42 ID', en: 'Request ID' },
10691
+ from: { zh: '\u6765\u81EA', en: 'From' },
10692
+ time: { zh: '\u65F6\u95F4', en: 'Time' },
10693
+ no_pending_requests: { zh: '\u6682\u65E0\u5F85\u5904\u7406\u8BF7\u6C42', en: 'No pending requests' },
10694
+ accept: { zh: '\u63A5\u53D7', en: 'Accept' },
10695
+ reject: { zh: '\u62D2\u7EDD', en: 'Reject' },
10696
+ peer_id: { zh: '\u5BF9\u7AEF ID', en: 'Peer ID' },
10697
+ established: { zh: '\u5EFA\u7ACB\u65F6\u95F4', en: 'Established' },
10698
+ messages: { zh: '\u6761\u6D88\u606F', en: 'messages' },
10699
+ no_active_sessions: { zh: '\u6682\u65E0\u6D3B\u8DC3\u4F1A\u8BDD', en: 'No active sessions' },
10700
+ enter_temp_or_id: { zh: '\u8BF7\u8F93\u5165\u4E34\u65F6\u53F7\u7801\u6216\u8282\u70B9ID', en: 'Enter a temp number or node ID' },
10701
+ sending_request: { zh: '\u6B63\u5728\u53D1\u9001\u597D\u53CB\u8BF7\u6C42...', en: 'Sending friend request...' },
10702
+ friend_request_sent: { zh: '\u597D\u53CB\u8BF7\u6C42\u5DF2\u53D1\u9001\uFF01', en: 'Friend request sent!' },
10703
+ failed_add_friend: { zh: '\u6DFB\u52A0\u597D\u53CB\u5931\u8D25', en: 'Failed to add friend' },
10704
+ remove_friend_confirm: { zh: '\u786E\u5B9A\u79FB\u9664\u597D\u53CB ', en: 'Remove friend ' },
10705
+ friend_removed: { zh: '\u597D\u53CB\u5DF2\u79FB\u9664', en: 'Friend removed' },
10706
+ edit_permissions: { zh: '\u7F16\u8F91\u6743\u9650', en: 'Edit Permissions' },
10707
+ friend_permissions: { zh: '\u597D\u53CB\u6743\u9650', en: 'Friend Permissions' },
10708
+ chat_perm: { zh: '\u{1F4AC} \u804A\u5929', en: '\u{1F4AC} Chat' },
10709
+ exec_perm: { zh: '\u{1F527} \u6267\u884C', en: '\u{1F527} Exec' },
10710
+ chat_perm_hint: { zh: '\uFF08\u53D1\u9001/\u63A5\u6536\u6D88\u606F\uFF09', en: '(send/receive messages)' },
10711
+ exec_perm_hint: { zh: '\uFF08\u6267\u884C\u5DE5\u5177/\u547D\u4EE4\uFF09', en: '(execute tools/commands)' },
10712
+ save_permissions: { zh: '\u4FDD\u5B58\u6743\u9650', en: 'Save Permissions' },
10713
+ permissions_updated: { zh: '\u6743\u9650\u5DF2\u66F4\u65B0', en: 'Permissions updated' },
10714
+ request_accepted: { zh: '\u8BF7\u6C42\u5DF2\u63A5\u53D7', en: 'Request accepted' },
10715
+ request_rejected: { zh: '\u8BF7\u6C42\u5DF2\u62D2\u7EDD', en: 'Request rejected' },
10716
+ // Models
10717
+ loading_models: { zh: '\u6B63\u5728\u52A0\u8F7D\u6A21\u578B\u914D\u7F6E...', en: 'Loading model configuration...' },
10718
+ configured: { zh: '\u5DF2\u914D\u7F6E', en: 'Configured' },
10719
+ providers_with_keys: { zh: '\u5DF2\u914D\u7F6EAPI\u5BC6\u94A5\u7684\u63D0\u4F9B\u5546', en: 'Providers with API keys' },
10720
+ configure: { zh: '\u914D\u7F6E', en: 'Configure' },
10721
+ not_set: { zh: '\u672A\u8BBE\u7F6E', en: 'Not set' },
10722
+ key_set: { zh: '\u25CF \u5DF2\u8BBE\u7F6E\u5BC6\u94A5', en: '\u25CF Key set' },
10723
+ active_model_configs: { zh: '\u5F53\u524D\u6A21\u578B\u914D\u7F6E', en: 'Active Model Configurations' },
10724
+ default_model_global: { zh: '\u5168\u5C40\u9ED8\u8BA4\u6A21\u578B', en: 'Global Default Model' },
10725
+ model_count: { zh: '\u6A21\u578B\u6570\u91CF', en: 'Model Count' },
10726
+ multi_model: { zh: '\u591A\u6A21\u578B', en: 'Multi-model' },
10727
+ base_url: { zh: '\u57FA\u7840\u5730\u5740', en: 'Base URL' },
10728
+ configure_providers_desc: { zh: '\u4E3A\u667A\u80FD\u4F53\u914D\u7F6ELLM\u63D0\u4F9B\u5546\u3002\u70B9\u51FB\u63D0\u4F9B\u5546\u5361\u7247\u8BBE\u7F6E\u6216\u66F4\u65B0API\u5BC6\u94A5\u3001\u6A21\u578B\u548C\u57FA\u7840\u5730\u5740\u3002\u66F4\u6539\u5C06\u76F4\u63A5\u4FDD\u5B58\u5230\u914D\u7F6E\u6587\u4EF6\u3002', en: 'Configure LLM providers for your agents. Click a provider card to set or update the API key, model, and base URL. Changes are saved directly to your config file.' },
10729
+ current_key: { zh: '\u5F53\u524D\uFF1A', en: 'Current: ' },
10730
+ no_api_key: { zh: '\u672A\u914D\u7F6EAPI\u5BC6\u94A5', en: 'No API key configured' },
10731
+ provider_not_found: { zh: '\u672A\u627E\u5230\u63D0\u4F9B\u5546', en: 'Provider not found' },
10732
+ enter_api_key: { zh: '\u8F93\u5165API\u5BC6\u94A5', en: 'Enter API key' },
10733
+ enter_model_id: { zh: '\u8F93\u5165\u6A21\u578BID', en: 'Model ID' },
10734
+ default_url: { zh: '\u9ED8\u8BA4\u5730\u5740', en: 'Default URL' },
10735
+ enter_api_key_or_model: { zh: '\u8BF7\u81F3\u5C11\u8F93\u5165API\u5BC6\u94A5\u6216\u6A21\u578BID', en: 'Enter at least an API key or model ID' },
10736
+ saving_config: { zh: '\u6B63\u5728\u4FDD\u5B58\u914D\u7F6E...', en: 'Saving configuration...' },
10737
+ config_saved: { zh: '\u914D\u7F6E\u5DF2\u4FDD\u5B58\uFF01', en: 'Configuration saved!' },
10738
+ failed_save: { zh: '\u4FDD\u5B58\u5931\u8D25', en: 'Failed to save' },
10739
+ confirm_delete_provider: { zh: '\u786E\u5B9A\u5220\u9664\u63D0\u4F9B\u5546 "', en: 'Delete configuration for provider "' },
10740
+ provider_deleted: { zh: '\u63D0\u4F9B\u5546\u914D\u7F6E\u5DF2\u5220\u9664', en: 'Provider configuration deleted' },
10741
+ // Settings
10742
+ loading_settings: { zh: '\u6B63\u5728\u52A0\u8F7D\u8BBE\u7F6E...', en: 'Loading settings...' },
10743
+ tab_connection: { zh: '\u{1F50C} \u8FDE\u63A5', en: '\u{1F50C} Connection' },
10744
+ tab_friends: { zh: '\u{1F465} \u597D\u53CB', en: '\u{1F465} Friends' },
10745
+ tab_security: { zh: '\u{1F512} \u5B89\u5168', en: '\u{1F512} Security' },
10746
+ tab_advanced: { zh: '\u2699\uFE0F \u9AD8\u7EA7', en: '\u2699\uFE0F Advanced' },
10747
+ tab_json: { zh: '\u{1F4DD} JSON\u7F16\u8F91', en: '\u{1F4DD} JSON Editor' },
10748
+ conn_desc: { zh: '\u914D\u7F6E\u670D\u52A1\u5668\u8FDE\u63A5\u548CWebSocket\u8BBE\u7F6E\u3002\u66F4\u6539\u9700\u8981\u91CD\u542F\u63D2\u4EF6\u624D\u80FD\u5B8C\u5168\u751F\u6548\u3002', en: 'Configure server connection and WebSocket settings. Changes require a plugin restart to take full effect.' },
10749
+ server_connection: { zh: '\u{1F310} \u670D\u52A1\u5668\u8FDE\u63A5', en: '\u{1F310} Server Connection' },
10750
+ server_url_label: { zh: '\u670D\u52A1\u5668\u5730\u5740', en: 'Server URL' },
10751
+ server_url_hint: { zh: 'AICQ\u4E2D\u7EE7\u670D\u52A1\u5668\u7684HTTPS\u5730\u5740\u3002WebSocket\u8DEF\u5F84 /ws \u4F1A\u81EA\u52A8\u8FFD\u52A0\u3002', en: 'The HTTPS URL of the AICQ relay server. WebSocket path /ws is auto-appended.' },
10752
+ conn_timeout: { zh: '\u8FDE\u63A5\u8D85\u65F6\uFF08\u79D2\uFF09', en: 'Connection Timeout (seconds)' },
10753
+ conn_timeout_hint: { zh: 'HTTP\u8BF7\u6C42\u8D85\u65F6\u65F6\u95F4\uFF085-120\u79D2\uFF09\u3002\u9ED8\u8BA4\uFF1A30\u79D2\u3002', en: 'HTTP request timeout (5\u2013120s). Default: 30s.' },
10754
+ ws_auto_reconnect: { zh: 'WebSocket\u81EA\u52A8\u91CD\u8FDE', en: 'WS Auto-Reconnect' },
10755
+ auto_reconnect_label: { zh: '\u65AD\u5F00\u65F6\u81EA\u52A8\u91CD\u8FDE', en: 'Auto-reconnect when disconnected' },
10756
+ auto_reconnect_hint: { zh: '\u65AD\u5F00\u8FDE\u63A5\u540E\u81EA\u52A8\u91CD\u65B0\u8FDE\u63A5WebSocket\u3002', en: 'Automatically reconnect WebSocket on disconnection.' },
10757
+ ws_reconnect_interval: { zh: '\u91CD\u8FDE\u95F4\u9694\uFF08\u79D2\uFF09', en: 'WS Reconnect Interval (seconds)' },
10758
+ ws_reconnect_hint: { zh: '\u91CD\u8FDE\u5C1D\u8BD5\u4E4B\u95F4\u7684\u95F4\u9694\uFF085-600\u79D2\uFF09\u3002\u9ED8\u8BA4\uFF1A60\u79D2\u3002', en: 'Interval between reconnection attempts (5\u2013600s). Default: 60s.' },
10759
+ test: { zh: '\u6D4B\u8BD5', en: 'Test' },
10760
+ testing: { zh: '\u6D4B\u8BD5\u4E2D...', en: 'Testing...' },
10761
+ enter_server_url: { zh: '\u8BF7\u5148\u8F93\u5165\u670D\u52A1\u5668\u5730\u5740', en: 'Enter a server URL first' },
10762
+ conn_ok: { zh: '\u8FDE\u63A5\u6210\u529F', en: 'Connected successfully' },
10763
+ conn_ok_latency: { zh: '\u8FDE\u63A5\u6210\u529F\uFF01\u5EF6\u8FDF\uFF1A', en: 'Connection OK! Latency: ' },
10764
+ conn_failed: { zh: '\u8FDE\u63A5\u5931\u8D25', en: 'Connection failed' },
10765
+ config_file: { zh: '\u{1F4C1} \u914D\u7F6E\u6587\u4EF6', en: '\u{1F4C1} Config File' },
10766
+ source: { zh: '\u6765\u6E90', en: 'Source' },
10767
+ mgmt_ui: { zh: '\u7BA1\u7406\u754C\u9762', en: 'Management UI' },
10768
+ uptime: { zh: '\u8FD0\u884C\u65F6\u95F4', en: 'Uptime' },
10769
+ not_found: { zh: '\u672A\u627E\u5230', en: 'Not found' },
10770
+ friends_tab_desc: { zh: '\u914D\u7F6E\u597D\u53CB\u7BA1\u7406\u3001\u6743\u9650\u548C\u4E34\u65F6\u53F7\u7801\u8BBE\u7F6E\u3002', en: 'Configure friend management, permissions, and temporary number settings.' },
10771
+ of_max: { zh: ' / \u6700\u5927 ', en: ' of ' },
10772
+ max_friends: { zh: '\u6700\u5927\u597D\u53CB\u6570', en: 'Max Friends' },
10773
+ auto_accept: { zh: '\u81EA\u52A8\u63A5\u53D7\u597D\u53CB', en: 'Auto-Accept Friends' },
10774
+ auto_accept_label: { zh: '\u81EA\u52A8\u63A5\u53D7\u8BF7\u6C42', en: 'Automatically accept requests' },
10775
+ auto_accept_hint: { zh: '\u542F\u7528\u540E\uFF0C\u4F20\u5165\u7684\u597D\u53CB\u8BF7\u6C42\u5C06\u81EA\u52A8\u63A5\u53D7\u3002', en: 'When enabled, incoming friend requests are accepted without review.' },
10776
+ default_perms: { zh: '\u65B0\u597D\u53CB\u9ED8\u8BA4\u6743\u9650', en: 'Default Permissions for New Friends' },
10777
+ default_perms_hint: { zh: '\u81EA\u52A8\u63A5\u53D7\u65B0\u597D\u53CB\u8BF7\u6C42\u65F6\u5E94\u7528\u7684\u9ED8\u8BA4\u6743\u9650\u3002', en: 'Default permissions applied when auto-accepting new friend requests.' },
10778
+ temp_numbers: { zh: '\u{1F522} \u4E34\u65F6\u53F7\u7801', en: '\u{1F522} Temporary Numbers' },
10779
+ temp_expiry: { zh: '\u4E34\u65F6\u53F7\u7801\u6709\u6548\u671F\uFF08\u79D2\uFF09', en: 'Temp Number Expiry (seconds)' },
10780
+ temp_expiry_hint: { zh: '\u4E34\u65F6\u597D\u53CB\u53F7\u7801\u7684\u6709\u6548\u65F6\u95F4\uFF0860-3600\u79D2\uFF09\u3002\u9ED8\u8BA4\uFF1A5\u5206\u949F\u3002', en: 'How long a temporary friend number remains valid (60\u20133600s). Default: 5 minutes.' },
10781
+ sec_desc: { zh: '\u914D\u7F6E\u52A0\u5BC6\u3001P2P\u548C\u8EAB\u4EFD\u5B89\u5168\u8BBE\u7F6E\u3002', en: 'Configure encryption, P2P, and identity security settings.' },
10782
+ agent_identity: { zh: '\u{1F916} \u667A\u80FD\u4F53\u8EAB\u4EFD', en: '\u{1F916} Agent Identity' },
10783
+ public_key_fp: { zh: '\u516C\u94A5\u6307\u7EB9', en: 'Public Key Fingerprint' },
10784
+ reset_identity: { zh: '\u{1F5D1}\uFE0F \u91CD\u7F6E\u8EAB\u4EFD', en: '\u{1F5D1}\uFE0F Reset Identity' },
10785
+ reset_identity_warn: { zh: '\u26A0\uFE0F \u8FD9\u5C06\u6C38\u4E45\u5220\u9664\u6240\u6709\u597D\u53CB\u3001\u4F1A\u8BDD\u548C\u5BC6\u94A5', en: '\u26A0\uFE0F This deletes all friends, sessions, and keys permanently' },
10786
+ p2p_encryption: { zh: '\u{1F512} P2P\u4E0E\u52A0\u5BC6', en: '\u{1F512} P2P & Encryption' },
10787
+ enable_p2p: { zh: '\u542F\u7528P2P\u8FDE\u63A5', en: 'Enable P2P Connections' },
10788
+ allow_p2p: { zh: '\u5141\u8BB8\u76F4\u63A5P2P\u6D88\u606F', en: 'Allow direct P2P messaging' },
10789
+ enable_p2p_hint: { zh: '\u53CC\u65B9\u90FD\u5728\u7EBF\u65F6\u542F\u7528\u70B9\u5BF9\u70B9\u52A0\u5BC6\u8FDE\u63A5\u3002', en: 'Enable peer-to-peer encrypted connections when both parties are online.' },
10790
+ hs_timeout: { zh: '\u63E1\u624B\u8D85\u65F6\uFF08\u79D2\uFF09', en: 'Handshake Timeout (seconds)' },
10791
+ hs_timeout_hint: { zh: 'Noise-XK\u63E1\u624B\u8D85\u65F6\u65F6\u95F4\uFF0810-300\u79D2\uFF09\u3002\u9ED8\u8BA4\uFF1A60\u79D2\u3002', en: 'Noise-XK handshake timeout (10\u2013300s). Default: 60s.' },
10792
+ adv_desc: { zh: '\u6587\u4EF6\u4F20\u8F93\u3001\u65E5\u5FD7\u548C\u914D\u7F6E\u7BA1\u7406\u7684\u9AD8\u7EA7\u8BBE\u7F6E\u3002', en: 'Advanced settings for file transfer, logging, and configuration management.' },
10793
+ file_transfer: { zh: '\u{1F4CE} \u6587\u4EF6\u4F20\u8F93', en: '\u{1F4CE} File Transfer' },
10794
+ enable_ft: { zh: '\u542F\u7528\u6587\u4EF6\u4F20\u8F93', en: 'Enable File Transfer' },
10795
+ allow_ft: { zh: '\u5141\u8BB8\u6587\u4EF6\u4F20\u8F93', en: 'Allow file transfers' },
10796
+ enable_ft_hint: { zh: '\u542F\u7528\u597D\u53CB\u95F4\u7684\u52A0\u5BC6\u6587\u4EF6\u4F20\u8F93\u3002', en: 'Enable encrypted file transfer between friends.' },
10797
+ max_file_size: { zh: '\u6700\u5927\u6587\u4EF6\u5927\u5C0F', en: 'Max File Size' },
10798
+ max_file_size_hint: { zh: '\u52A0\u5BC6\u4F20\u8F93\u7684\u6700\u5927\u6587\u4EF6\u5927\u5C0F\u3002\u5F53\u524D\uFF1A', en: 'Maximum file size for encrypted transfers. Current: ' },
10799
+ logging: { zh: '\u{1F4CB} \u65E5\u5FD7', en: '\u{1F4CB} Logging' },
10800
+ log_level: { zh: '\u65E5\u5FD7\u7EA7\u522B', en: 'Log Level' },
10801
+ log_debug: { zh: '\u{1F41B} \u8C03\u8BD5 \u2014 \u8BE6\u7EC6\u8F93\u51FA', en: '\u{1F41B} Debug \u2014 Verbose output for troubleshooting' },
10802
+ log_info: { zh: '\u2139\uFE0F \u4FE1\u606F \u2014 \u4E00\u822C\u4FE1\u606F\uFF08\u9ED8\u8BA4\uFF09', en: '\u2139\uFE0F Info \u2014 General information (default)' },
10803
+ log_warn: { zh: '\u26A0\uFE0F \u8B66\u544A \u2014 \u8B66\u544A\u548C\u91CD\u8981\u4E8B\u4EF6', en: '\u26A0\uFE0F Warn \u2014 Warnings and important events' },
10804
+ log_error: { zh: '\u274C \u9519\u8BEF \u2014 \u4EC5\u9519\u8BEF', en: '\u274C Error \u2014 Errors only' },
10805
+ log_none: { zh: '\u{1F507} \u65E0 \u2014 \u7981\u7528\u6240\u6709\u65E5\u5FD7', en: '\u{1F507} None \u2014 Disable all logging' },
10806
+ log_level_hint: { zh: '\u63A7\u5236\u63D2\u4EF6\u65E5\u5FD7\u8F93\u51FA\u7684\u8BE6\u7EC6\u7A0B\u5EA6\u3002', en: 'Controls the verbosity of plugin log output.' },
10807
+ import_export: { zh: '\u{1F4E6} \u5BFC\u5165/\u5BFC\u51FA\u8BBE\u7F6E', en: '\u{1F4E6} Import / Export Settings' },
10808
+ export_settings: { zh: '\u{1F4E5} \u5BFC\u51FA\u8BBE\u7F6E', en: '\u{1F4E5} Export Settings' },
10809
+ import_settings: { zh: '\u{1F4E4} \u5BFC\u5165\u8BBE\u7F6E', en: '\u{1F4E4} Import Settings' },
10810
+ import_export_hint: { zh: '\u5C06\u5F53\u524DAICQ\u63D2\u4EF6\u8BBE\u7F6E\u5BFC\u51FA\u4E3AJSON\u3002\u5BFC\u5165\u53EF\u4ECE\u5907\u4EFD\u6062\u590D\u8BBE\u7F6E\u3002', en: 'Export current AICQ plugin settings as JSON. Import to restore settings from a backup.' },
10811
+ save: { zh: '\u{1F4BE} \u4FDD\u5B58', en: '\u{1F4BE} Save' },
10812
+ saving: { zh: '\u4FDD\u5B58\u4E2D...', en: 'Saving...' },
10813
+ saved: { zh: '\u2713 \u5DF2\u4FDD\u5B58', en: '\u2713 Saved' },
10814
+ settings_saved: { zh: '\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\uFF1A', en: 'Settings saved: ' },
10815
+ all_saved: { zh: '\u6240\u6709\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\uFF01', en: 'All settings saved!' },
10816
+ delete_everything: { zh: '\u{1F5D1}\uFE0F \u5220\u9664\u6240\u6709\u6570\u636E', en: '\u{1F5D1}\uFE0F Delete Everything' },
10817
+ confirm_delete: { zh: '\u{1F5D1}\uFE0F \u786E\u8BA4\u5220\u9664', en: '\u{1F5D1}\uFE0F Confirm Delete' },
10818
+ resetting: { zh: '\u91CD\u7F6E\u4E2D...', en: 'Resetting...' },
10819
+ reset_success: { zh: '\u8EAB\u4EFD\u91CD\u7F6E\u6210\u529F\uFF0C\u8BF7\u91CD\u542F\u63D2\u4EF6\u3002', en: 'Identity reset successfully. Please restart the plugin.' },
10820
+ reset_failed: { zh: '\u91CD\u7F6E\u5931\u8D25', en: 'Reset failed' },
10821
+ exported_success: { zh: '\u8BBE\u7F6E\u5BFC\u51FA\u6210\u529F', en: 'Settings exported successfully' },
10822
+ paste_json: { zh: '\u8BF7\u5148\u7C98\u8D34JSON\u8BBE\u7F6E', en: 'Paste JSON settings first' },
10823
+ importing: { zh: '\u5BFC\u5165\u4E2D...', en: 'Importing...' },
10824
+ imported_success: { zh: '\u8BBE\u7F6E\u5BFC\u5165\u6210\u529F\uFF01', en: 'Settings imported successfully!' },
10825
+ import_failed: { zh: '\u5BFC\u5165\u5931\u8D25', en: 'Import failed' },
10826
+ loading_config: { zh: '\u6B63\u5728\u52A0\u8F7D\u914D\u7F6E...', en: 'Loading config...' },
10827
+ json_editor_desc: { zh: '\u76F4\u63A5\u7F16\u8F91\u539F\u59CBJSON\u914D\u7F6E\u3002\u6CE8\u610F\u8BED\u6CD5\u2014\u2014\u65E0\u6548\u7684JSON\u5C06\u88AB\u62D2\u7EDD\u3002', en: 'Edit the raw JSON configuration directly. Be careful with syntax \u2014 invalid JSON will be rejected.' },
10828
+ json_editor: { zh: '\u{1F4DD} JSON\u914D\u7F6E\u7F16\u8F91\u5668', en: '\u{1F4DD} Config JSON Editor' },
10829
+ raw_json: { zh: '\u539F\u59CBJSON\u914D\u7F6E', en: 'Raw JSON Configuration' },
10830
+ raw_json_hint: { zh: '\u76F4\u63A5\u7F16\u8F91\u914D\u7F6EJSON\u3002\u4F7F\u7528\u683C\u5F0F\u5316\u6309\u94AE\u7F8E\u5316\u3002', en: 'Directly edit the configuration JSON. Use the Format button to prettify.' },
10831
+ format: { zh: '\u{1F4D0} \u683C\u5F0F\u5316', en: '\u{1F4D0} Format' },
10832
+ copy: { zh: '\u{1F4CB} \u590D\u5236', en: '\u{1F4CB} Copy' },
10833
+ revert: { zh: '\u21A9\uFE0F \u8FD8\u539F', en: '\u21A9\uFE0F Revert' },
10834
+ save_config: { zh: '\u{1F4BE} \u4FDD\u5B58\u914D\u7F6E', en: '\u{1F4BE} Save Config' },
10835
+ json_formatted: { zh: 'JSON\u5DF2\u683C\u5F0F\u5316', en: 'JSON formatted' },
10836
+ valid_json: { zh: '\u2713 \u6709\u6548JSON', en: '\u2713 Valid JSON' },
10837
+ invalid_json: { zh: '\u2717 \u65E0\u6548JSON\uFF1A', en: '\u2717 Invalid JSON: ' },
10838
+ no_content: { zh: '\u6CA1\u6709\u53EF\u4FDD\u5B58\u7684\u5185\u5BB9', en: 'No content to save' },
10839
+ config_saved: { zh: '\u914D\u7F6E\u5DF2\u4FDD\u5B58\uFF01', en: 'Config saved successfully!' },
10840
+ testing_conn_to: { zh: '\u6B63\u5728\u6D4B\u8BD5\u5230 ', en: 'Testing connection to ' },
10841
+ config_file_label: { zh: '\u{1F4C4} \u914D\u7F6E\u6587\u4EF6', en: '\u{1F4C4} Config File' },
10842
+ // Modals
10843
+ add_friend_title: { zh: '\u2795 \u6DFB\u52A0\u597D\u53CB', en: '\u2795 Add Friend' },
10844
+ temp_or_node: { zh: '\u4E34\u65F6\u53F7\u7801\u6216\u8282\u70B9ID', en: 'Temp Number or Node ID' },
10845
+ temp_or_node_ph: { zh: '6\u4F4D\u53F7\u7801\u6216\u8282\u70B9ID', en: '6-digit number or node ID' },
10846
+ temp_or_node_hint: { zh: '\u8F93\u5165\u597D\u53CB\u76846\u4F4D\u4E34\u65F6\u53F7\u7801\u6216\u5B8C\u6574\u8282\u70B9ID\u3002', en: 'Enter the 6-digit temporary number or the full node ID of your friend.' },
10847
+ cancel: { zh: '\u53D6\u6D88', en: 'Cancel' },
10848
+ send_request: { zh: '\u53D1\u9001\u8BF7\u6C42', en: 'Send Request' },
10849
+ close: { zh: '\u5173\u95ED', en: 'Close' },
10850
+ save_agent: { zh: '\u{1F4BE} \u4FDD\u5B58\u667A\u80FD\u4F53', en: '\u{1F4BE} Save Agent' },
10851
+ save_configuration: { zh: '\u{1F4BE} \u4FDD\u5B58\u914D\u7F6E', en: '\u{1F4BE} Save Configuration' },
10852
+ reset_identity_title: { zh: '\u{1F5D1}\uFE0F \u91CD\u7F6E\u667A\u80FD\u4F53\u8EAB\u4EFD', en: '\u{1F5D1}\uFE0F Reset Agent Identity' },
10853
+ reset_warning_title: { zh: '\u26A0\uFE0F \u8B66\u544A\uFF1A\u8FD9\u662F\u4E00\u4E2A\u7834\u574F\u6027\u64CD\u4F5C\uFF01', en: '\u26A0\uFE0F WARNING: This is a destructive operation!' },
10854
+ reset_warning_desc: { zh: '\u8FD9\u5C06\u6C38\u4E45\u5220\u9664\uFF1A', en: 'This will permanently delete:' },
10855
+ reset_keypair: { zh: '\u2022 \u4F60\u7684Ed25519\u5BC6\u94A5\u5BF9\u548C\u667A\u80FD\u4F53ID', en: '\u2022 Your Ed25519 key pair and agent ID' },
10856
+ reset_friends: { zh: '\u2022 \u6240\u6709\u597D\u53CB\u8FDE\u63A5\u548C\u4F1A\u8BDD', en: '\u2022 All friend connections and sessions' },
10857
+ reset_requests: { zh: '\u2022 \u6240\u6709\u5F85\u5904\u7406\u7684\u597D\u53CB\u8BF7\u6C42', en: '\u2022 All pending friend requests' },
10858
+ reset_temp: { zh: '\u2022 \u6240\u6709\u4E34\u65F6\u53F7\u7801', en: '\u2022 All temporary numbers' },
10859
+ reset_restart_hint: { zh: '\u91CD\u7F6E\u540E\uFF0C\u5FC5\u987B\u91CD\u542F\u63D2\u4EF6\u4EE5\u751F\u6210\u65B0\u8EAB\u4EFD\u3002', en: 'After reset, you must restart the plugin to generate a new identity.' },
10860
+ type_reset: { zh: '\u8F93\u5165 RESET \u4EE5\u786E\u8BA4', en: 'Type RESET to confirm' },
10861
+ import_title: { zh: '\u{1F4E4} \u5BFC\u5165\u8BBE\u7F6E', en: '\u{1F4E4} Import Settings' },
10862
+ paste_json_label: { zh: '\u7C98\u8D34JSON\u8BBE\u7F6E', en: 'Paste JSON Settings' },
10863
+ paste_json_ph: { zh: '{"serverUrl": "https://...", ...}', en: '{"serverUrl": "https://...", ...}' },
10864
+ paste_json_hint: { zh: '\u7C98\u8D34\u4ECE\u53E6\u4E00\u4E2AAICQ\u5B9E\u4F8B\u5BFC\u51FA\u7684JSON\u8BBE\u7F6E\u3002\u8BBE\u7F6E\u5C06\u4E0E\u73B0\u6709\u503C\u5408\u5E76\u3002', en: 'Paste the JSON settings exported from another AICQ instance. Settings will be merged with existing values.' },
10865
+ agent_name: { zh: '\u667A\u80FD\u4F53\u540D\u79F0 *', en: 'Agent Name *' },
10866
+ agent_id_label: { zh: '\u667A\u80FD\u4F53ID', en: 'Agent ID' },
10867
+ agent_id_hint: { zh: '\u552F\u4E00\u6807\u8BC6\u7B26\u3002\u7559\u7A7A\u81EA\u52A8\u751F\u6210\u3002', en: 'Unique identifier. Leave empty for auto-generation.' },
10868
+ model_label: { zh: '\u6A21\u578B', en: 'Model' },
10869
+ provider_label: { zh: '\u63D0\u4F9B\u5546', en: 'Provider' },
10870
+ system_prompt_label: { zh: '\u7CFB\u7EDF\u63D0\u793A\u8BCD', en: 'System Prompt' },
10871
+ temperature: { zh: '\u6E29\u5EA6', en: 'Temperature' },
10872
+ max_tokens: { zh: '\u6700\u5927Token\u6570', en: 'Max Tokens' },
10873
+ top_p: { zh: 'Top P', en: 'Top P' },
10874
+ tools: { zh: '\u5DE5\u5177', en: 'Tools' },
10875
+ enabled: { zh: '\u542F\u7528', en: 'Enabled' },
10876
+ // Utilities
10877
+ copied_clipboard: { zh: '\u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F', en: 'Copied to clipboard' },
10878
+ copy_failed: { zh: '\u590D\u5236\u5931\u8D25', en: 'Copy failed' },
10879
+ just_now: { zh: '\u521A\u521A', en: 'just now' },
10880
+ min_ago: { zh: '\u5206\u949F\u524D', en: ' min ago' },
10881
+ h_ago: { zh: '\u5C0F\u65F6\u524D', en: 'h ago' },
10882
+ d_ago: { zh: '\u5929\u524D', en: 'd ago' },
10883
+ none: { zh: '\u65E0', en: 'none' },
10884
+ failed: { zh: '\u5931\u8D25', en: 'Failed' },
10885
+ // Offline
10886
+ offline_msg: { zh: '\u60A8\u5F53\u524D\u5904\u4E8E\u79BB\u7EBF\u72B6\u6001\u3002\u90E8\u5206\u529F\u80FD\u53EF\u80FD\u53D7\u9650\u3002\u6570\u636E\u4ECE\u672C\u5730\u7F13\u5B58\u52A0\u8F7D\u3002', en: 'You are offline. Some features may be limited. Data is loaded from local cache.' },
10887
+ // Custom Providers
10888
+ add_custom_provider: { zh: '\u2795 \u6DFB\u52A0\u81EA\u5B9A\u4E49\u63D0\u4F9B\u5546', en: '\u2795 Add Custom Provider' },
10889
+ custom_provider: { zh: '\u{1F9E9} \u81EA\u5B9A\u4E49\u63D0\u4F9B\u5546', en: '\u{1F9E9} Custom Provider' },
10890
+ custom_provider_desc: { zh: '\u624B\u52A8\u6DFB\u52A0\u517C\u5BB9 OpenAI API \u683C\u5F0F\u7684\u81EA\u5B9A\u4E49\u6A21\u578B\u63D0\u4F9B\u5546', en: 'Manually add custom model providers compatible with OpenAI API format' },
10891
+ provider_name: { zh: '\u63D0\u4F9B\u5546\u540D\u79F0', en: 'Provider Name' },
10892
+ provider_name_placeholder: { zh: '\u4F8B\u5982\uFF1AMy Custom API', en: 'e.g., My Custom API' },
10893
+ provider_name_required: { zh: '\u8BF7\u8F93\u5165\u63D0\u4F9B\u5546\u540D\u79F0', en: 'Provider name is required' },
10894
+ custom_provider_added: { zh: '\u81EA\u5B9A\u4E49\u63D0\u4F9B\u5546\u5DF2\u6DFB\u52A0', en: 'Custom provider added' },
10895
+ custom_provider_updated: { zh: '\u81EA\u5B9A\u4E49\u63D0\u4F9B\u5546\u5DF2\u66F4\u65B0', en: 'Custom provider updated' },
10896
+ custom_provider_deleted: { zh: '\u81EA\u5B9A\u4E49\u63D0\u4F9B\u5546\u5DF2\u5220\u9664', en: 'Custom provider deleted' },
10897
+ confirm_delete_custom: { zh: '\u786E\u5B9A\u8981\u5220\u9664\u81EA\u5B9A\u4E49\u63D0\u4F9B\u5546 "{name}" \u5417\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002', en: 'Are you sure you want to delete custom provider "{name}"? This cannot be undone.' },
10898
+ edit_custom_provider: { zh: '\u7F16\u8F91\u81EA\u5B9A\u4E49\u63D0\u4F9B\u5546', en: 'Edit Custom Provider' },
10899
+ save_custom: { zh: '\u{1F4BE} \u4FDD\u5B58', en: '\u{1F4BE} Save' },
10900
+ description_label: { zh: '\u63CF\u8FF0', en: 'Description' },
10901
+ description_placeholder: { zh: '\u53EF\u9009\uFF1A\u63CF\u8FF0\u6B64\u63D0\u4F9B\u5546\u7684\u7528\u9014', en: 'Optional: describe what this provider is for' },
10902
+ api_key_label: { zh: 'API \u5BC6\u94A5', en: 'API Key' },
10903
+ model_id_label2: { zh: '\u6A21\u578B ID', en: 'Model ID' },
10904
+ base_url_label: { zh: '\u57FA\u7840\u5730\u5740', en: 'Base URL' },
10905
+ // OpenClaw Config
10906
+ nav_openclaw: { zh: 'OpenClaw', en: 'OpenClaw' },
10907
+ openclaw_config: { zh: 'OpenClaw \u914D\u7F6E', en: 'OpenClaw Configuration' },
10908
+ loading_openclaw: { zh: '\u6B63\u5728\u52A0\u8F7D\u914D\u7F6E...', en: 'Loading configuration...' },
10909
+ openclaw_desc: { zh: '\u7BA1\u7406 OpenClaw \u7684\u667A\u80FD\u4F53\u914D\u7F6E\u3001\u8DEF\u7531\u7ED1\u5B9A\u548C\u9891\u9053\u8BBE\u7F6E\u3002\u66F4\u6539\u5C06\u76F4\u63A5\u4FDD\u5B58\u5230 openclaw.json \u914D\u7F6E\u6587\u4EF6\u3002', en: 'Manage OpenClaw agent configuration, routing bindings, and channel settings. Changes are saved directly to the openclaw.json config file.' },
10910
+ // Agent Defaults
10911
+ agent_defaults: { zh: '\u{1F916} \u667A\u80FD\u4F53\u9ED8\u8BA4\u8BBE\u7F6E', en: '\u{1F916} Agent Defaults' },
10912
+ agent_defaults_desc: { zh: '\u914D\u7F6E\u6240\u6709\u667A\u80FD\u4F53\u7684\u9ED8\u8BA4\u53C2\u6570\uFF0C\u5305\u62EC\u6A21\u578B\u9009\u62E9\u3001\u5E76\u53D1\u6570\u3001\u538B\u7F29\u6A21\u5F0F\u7B49', en: 'Configure default parameters for all agents, including model selection, concurrency, compaction mode, etc.' },
10913
+ compaction_mode: { zh: '\u538B\u7F29\u6A21\u5F0F', en: 'Compaction Mode' },
10914
+ max_concurrent: { zh: '\u6700\u5927\u5E76\u53D1\u6570', en: 'Max Concurrent' },
10915
+ primary_model: { zh: '\u4E3B\u6A21\u578B', en: 'Primary Model' },
10916
+ fallback_models: { zh: '\u5907\u7528\u6A21\u578B', en: 'Fallback Models' },
10917
+ image_model: { zh: '\u56FE\u50CF\u6A21\u578B', en: 'Image Model' },
10918
+ subagent_max_concurrent: { zh: '\u5B50\u667A\u80FD\u4F53\u6700\u5927\u5E76\u53D1', en: 'Subagent Max Concurrent' },
10919
+ thinking_default: { zh: '\u9ED8\u8BA4\u601D\u8003\u6A21\u5F0F', en: 'Thinking Default' },
10920
+ workspace: { zh: '\u5DE5\u4F5C\u7A7A\u95F4', en: 'Workspace' },
10921
+ models_registry: { zh: '\u6A21\u578B\u6CE8\u518C\u8868', en: 'Models Registry' },
10922
+ // Agent List
10923
+ oc_agent_list: { zh: '\u{1F4CB} \u667A\u80FD\u4F53\u5217\u8868', en: '\u{1F4CB} Agent List' },
10924
+ oc_agent_list_desc: { zh: "\u914D\u7F6E\u5404\u4E2A\u667A\u80FD\u4F53\u7684\u8EAB\u4EFD\u3001\u6A21\u578B\u548C\u5DE5\u5177\u6743\u9650\u3002\u6BCF\u4E2A\u667A\u80FD\u4F53\u53EF\u4EE5\u5206\u914D\u4E0D\u540C\u7684\u6A21\u578B\u548C\u5DE5\u5177\u914D\u7F6E\u3002", en: "Configure each agent's identity, model, and tool permissions. Each agent can be assigned different models and tool configurations." },
10925
+ add_agent_btn: { zh: '\u2795 \u6DFB\u52A0\u667A\u80FD\u4F53', en: '\u2795 Add Agent' },
10926
+ agent_identity_name: { zh: '\u8EAB\u4EFD\u540D\u79F0', en: 'Identity Name' },
10927
+ agent_model_primary: { zh: '\u4E3B\u6A21\u578B', en: 'Primary Model' },
10928
+ agent_tools_profile: { zh: '\u5DE5\u5177\u914D\u7F6E', en: 'Tools Profile' },
10929
+ agent_workspace: { zh: '\u5DE5\u4F5C\u7A7A\u95F4', en: 'Workspace' },
10930
+ no_agents_in_list: { zh: '\u6682\u65E0\u667A\u80FD\u4F53\uFF0C\u70B9\u51FB\u4E0A\u65B9\u6309\u94AE\u6DFB\u52A0', en: 'No agents configured. Click the button above to add one.' },
10931
+ // Bindings
10932
+ bindings_title: { zh: '\u{1F517} \u8DEF\u7531\u7ED1\u5B9A', en: '\u{1F517} Routing Bindings' },
10933
+ bindings_desc: { zh: '\u914D\u7F6E\u6D88\u606F\u8DEF\u7531\u89C4\u5219\uFF0C\u5C06\u9891\u9053\u6216\u8D26\u53F7\u7ED1\u5B9A\u5230\u6307\u5B9A\u7684\u667A\u80FD\u4F53\u3002\u652F\u6301\u6309\u9891\u9053\u7C7B\u578B\u6216\u8D26\u53F7ID\u5339\u914D\u3002', en: 'Configure message routing rules to bind channels or accounts to specific agents. Supports matching by channel type or account ID.' },
10934
+ add_binding: { zh: '\u2795 \u6DFB\u52A0\u7ED1\u5B9A', en: '\u2795 Add Binding' },
10935
+ binding_agent_id: { zh: '\u667A\u80FD\u4F53ID', en: 'Agent ID' },
10936
+ binding_channel: { zh: '\u9891\u9053', en: 'Channel' },
10937
+ binding_account_id: { zh: '\u8D26\u53F7ID', en: 'Account ID' },
10938
+ binding_type: { zh: '\u7C7B\u578B', en: 'Type' },
10939
+ no_bindings: { zh: '\u6682\u65E0\u8DEF\u7531\u7ED1\u5B9A', en: 'No routing bindings configured' },
10940
+ // Channels
10941
+ channels_title: { zh: '\u{1F4E1} \u9891\u9053\u914D\u7F6E', en: '\u{1F4E1} Channel Configuration' },
10942
+ channels_desc: { zh: '\u914D\u7F6E\u5916\u90E8\u901A\u4FE1\u9891\u9053\uFF08\u5982 Telegram\uFF09\u3002\u6BCF\u4E2A\u9891\u9053\u53EF\u4EE5\u6709\u591A\u4E2A\u8D26\u53F7\uFF0C\u652F\u6301\u4E0D\u540C\u7684\u6D88\u606F\u7B56\u7565\u548C\u6743\u9650\u3002', en: 'Configure external communication channels (e.g., Telegram). Each channel can have multiple accounts with different messaging policies and permissions.' },
10943
+ channel_enabled: { zh: '\u5DF2\u542F\u7528', en: 'Enabled' },
10944
+ channel_disabled: { zh: '\u5DF2\u7981\u7528', en: 'Disabled' },
10945
+ group_policy: { zh: '\u7FA4\u7EC4\u7B56\u7565', en: 'Group Policy' },
10946
+ accounts: { zh: '\u8D26\u53F7', en: 'Accounts' },
10947
+ add_account: { zh: '\u2795 \u6DFB\u52A0\u8D26\u53F7', en: '\u2795 Add Account' },
10948
+ bot_token: { zh: 'Bot Token', en: 'Bot Token' },
10949
+ dm_policy: { zh: '\u79C1\u4FE1\u7B56\u7565', en: 'DM Policy' },
10950
+ allow_from: { zh: '\u5141\u8BB8\u6765\u6E90', en: 'Allow From' },
10951
+ add_allow_from: { zh: '\u6DFB\u52A0\u5141\u8BB8\u6765\u6E90', en: 'Add Allow From' },
10952
+ no_channels_configured: { zh: '\u6682\u65E0\u9891\u9053\u914D\u7F6E', en: 'No channels configured' },
10953
+ // OpenClaw Actions
10954
+ save_openclaw_config: { zh: '\u{1F4BE} \u4FDD\u5B58 OpenClaw \u914D\u7F6E', en: '\u{1F4BE} Save OpenClaw Config' },
10955
+ openclaw_config_saved: { zh: 'OpenClaw \u914D\u7F6E\u5DF2\u4FDD\u5B58\uFF01', en: 'OpenClaw configuration saved!' },
10956
+ confirm_delete_binding: { zh: '\u786E\u5B9A\u5220\u9664\u6B64\u8DEF\u7531\u7ED1\u5B9A\uFF1F', en: 'Delete this routing binding?' },
10957
+ binding_deleted: { zh: '\u8DEF\u7531\u7ED1\u5B9A\u5DF2\u5220\u9664', en: 'Routing binding deleted' },
10958
+ confirm_delete_oc_agent: { zh: '\u786E\u5B9A\u5220\u9664\u667A\u80FD\u4F53 "{name}" \u5417\uFF1F', en: 'Are you sure you want to delete agent "{name}"?' },
10959
+ confirm_delete_account: { zh: '\u786E\u5B9A\u5220\u9664\u6B64\u8D26\u53F7\uFF1F', en: 'Are you sure you want to delete this account?' },
10960
+ // Misc
10961
+ add_item: { zh: '\u2795 \u6DFB\u52A0', en: '\u2795 Add' },
10962
+ remove_item: { zh: '\u{1F5D1}\uFE0F \u5220\u9664', en: '\u{1F5D1}\uFE0F Delete' },
10963
+ edit_item: { zh: '\u270F\uFE0F \u7F16\u8F91', en: '\u270F\uFE0F Edit' },
10964
+ tab_defaults: { zh: '\u{1F916} \u9ED8\u8BA4\u8BBE\u7F6E', en: '\u{1F916} Defaults' },
10965
+ tab_agents: { zh: '\u{1F4CB} \u667A\u80FD\u4F53', en: '\u{1F4CB} Agents' },
10966
+ tab_bindings: { zh: '\u{1F517} \u7ED1\u5B9A', en: '\u{1F517} Bindings' },
10967
+ tab_channels: { zh: '\u{1F4E1} \u9891\u9053', en: '\u{1F4E1} Channels' },
10968
+ };
10969
+ function t(key) { return (_T[key] && _T[key][_lang]) || key; }
10970
+ function translateStatic() { document.querySelectorAll('[data-i18n]').forEach(el => { const k = el.getAttribute('data-i18n'); if (k && _T[k]) { el.textContent = _T[k][_lang] || el.textContent; } }); document.querySelectorAll('[data-i18n-ph]').forEach(el => { const k = el.getAttribute('data-i18n-ph'); if (k && _T[k]) { el.placeholder = _T[k][_lang] || el.placeholder; } }); }
10971
+
10124
10972
  // \u2500\u2500 Globals \u2500\u2500
10125
10973
  const API = '/api';
10126
10974
  let currentPage = 'dashboard';
@@ -10146,7 +10994,7 @@ function showOfflineBanner() {
10146
10994
  if (offlineBannerEl) return;
10147
10995
  offlineBannerEl = document.createElement('div');
10148
10996
  offlineBannerEl.className = 'offline-banner';
10149
- 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>';
10997
+ offlineBannerEl.innerHTML = '<span class="offline-icon">\u{1F50C}</span><span>' + t('offline_msg') + '</span>';
10150
10998
  const mainContent = document.querySelector('.main');
10151
10999
  if (mainContent) {
10152
11000
  mainContent.insertBefore(offlineBannerEl, mainContent.firstChild);
@@ -10204,13 +11052,13 @@ function escHtml(s) { if (s == null) return ''; const d = document.createElement
10204
11052
  function timeAgo(iso) {
10205
11053
  if (!iso) return '\u2014';
10206
11054
  const diff = Date.now() - new Date(iso).getTime();
10207
- if (diff < 0) return 'just now';
11055
+ if (diff < 0) return t('just_now');
10208
11056
  const m = Math.floor(diff / 60000), h = Math.floor(m / 60), d = Math.floor(h / 24);
10209
- if (m < 1) return 'just now'; if (m < 60) return m + ' min ago'; if (h < 24) return h + 'h ago'; if (d < 30) return d + 'd ago';
11057
+ if (m < 1) return t('just_now'); if (m < 60) return m + t('min_ago'); if (h < 24) return h + t('h_ago'); if (d < 30) return d + t('d_ago');
10210
11058
  return new Date(iso).toLocaleDateString();
10211
11059
  }
10212
11060
  function maskKey(s) { if (!s || s.length < 12) return s || ''; return s.substring(0, 6) + '\u2022\u2022\u2022\u2022\u2022\u2022' + s.slice(-4); }
10213
- function copyText(text) { navigator.clipboard.writeText(text).then(() => toast('Copied to clipboard', 'ok')).catch(() => toast('Copy failed', 'err')); }
11061
+ function copyText(text) { navigator.clipboard.writeText(text).then(() => toast(t('copied_clipboard'), 'ok')).catch(() => toast(t('copy_failed'), 'err')); }
10214
11062
 
10215
11063
  // \u2500\u2500 Modal \u2500\u2500
10216
11064
  function showModal(id) { show(id); }
@@ -10240,6 +11088,7 @@ function loadPage(page) {
10240
11088
  case 'friends': loadFriends(); break;
10241
11089
  case 'models': loadModels(); break;
10242
11090
  case 'settings': loadSettings(); break;
11091
+ case 'openclaw': loadOpenClawConfig(); break;
10243
11092
  }
10244
11093
  }
10245
11094
 
@@ -10248,15 +11097,15 @@ function loadPage(page) {
10248
11097
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
10249
11098
  async function loadDashboard() {
10250
11099
  const el = $('#dashboard-content');
10251
- html(el, '<div class="loading-mask"><div class="spinner"></div>Loading dashboard...</div>');
11100
+ html(el, '<div class="loading-mask"><div class="spinner"></div>' + t('loading_dashboard') + '</div>');
10252
11101
  const results = await Promise.allSettled([api('/status'), api('/friends'), api('/identity'), api('/mgmt-url')]);
10253
11102
  const status = results[0].status === 'fulfilled' ? results[0].value : { error: results[0].reason?.message || 'Failed' };
10254
11103
  const friends = results[1].status === 'fulfilled' ? results[1].value : { friends: [], error: true };
10255
11104
  const identity = results[2].status === 'fulfilled' ? results[2].value : { agentId: '\u2014', publicKeyFingerprint: '\u2014', serverUrl: '\u2014', connected: false };
10256
11105
  const mgmtUrl = results[3].status === 'fulfilled' ? results[3].value : { mgmtUrl: window.location.origin };
10257
- if (status.error) { html(el, '<div class="empty"><div class="icon">\u26A0\uFE0F</div><p>Failed to connect to AICQ plugin</p></div>'); return; }
11106
+ if (status.error) { html(el, '<div class="empty"><div class="icon">\u26A0\uFE0F</div><p>' + t('failed_connect') + '</p></div>'); return; }
10258
11107
  const connCls = status.connected ? 'dot-ok' : 'dot-err';
10259
- const connText = status.connected ? 'Connected' : 'Disconnected';
11108
+ const connText = status.connected ? t('connected') : t('disconnected');
10260
11109
  const friendList = friends.friends || [];
10261
11110
  const aiFriends = friendList.filter(f => f.friendType === 'ai').length;
10262
11111
  const humanFriends = friendList.filter(f => f.friendType !== 'ai').length;
@@ -10266,7 +11115,7 @@ async function loadDashboard() {
10266
11115
  <div class="stats-grid">
10267
11116
  <div class="stat-card">
10268
11117
  <div class="stat-icon" style="background:var(--accent-bg)">\u{1F4E1}</div>
10269
- <div class="stat-label">Server Status</div>
11118
+ <div class="stat-label">\${t('server_status')}</div>
10270
11119
  <div class="stat-value" style="font-size:16px;display:flex;align-items:center;gap:8px">
10271
11120
  <span class="dot \${connCls}"></span> \${connText}
10272
11121
  </div>
@@ -10274,42 +11123,42 @@ async function loadDashboard() {
10274
11123
  </div>
10275
11124
  <div class="stat-card">
10276
11125
  <div class="stat-icon" style="background:var(--ok-bg)">\u{1F465}</div>
10277
- <div class="stat-label">Total Friends</div>
11126
+ <div class="stat-label">\${t('total_friends')}</div>
10278
11127
  <div class="stat-value">\${friendList.length}</div>
10279
11128
  <div class="stat-sub">\${aiFriends} AI \xB7 \${humanFriends} Human</div>
10280
11129
  </div>
10281
11130
  <div class="stat-card">
10282
11131
  <div class="stat-icon" style="background:var(--info-bg)">\u{1F517}</div>
10283
- <div class="stat-label">Active Sessions</div>
11132
+ <div class="stat-label">\${t('active_sessions')}</div>
10284
11133
  <div class="stat-value">\${status.sessionCount || 0}</div>
10285
- <div class="stat-sub">Encrypted sessions</div>
11134
+ <div class="stat-sub">\${t('encrypted_sessions')}</div>
10286
11135
  </div>
10287
11136
  <div class="stat-card">
10288
11137
  <div class="stat-icon" style="background:var(--warn-bg)">\u{1F511}</div>
10289
- <div class="stat-label">Agent ID</div>
11138
+ <div class="stat-label">\${t('agent_id')}</div>
10290
11139
  <div class="stat-value mono" style="font-size:13px">\${escHtml(status.agentId)}</div>
10291
- <div class="stat-sub">Fingerprint: \${escHtml(status.fingerprint)}</div>
11140
+ <div class="stat-sub">\${t('fingerprint')}: \${escHtml(status.fingerprint)}</div>
10292
11141
  </div>
10293
11142
  </div>
10294
11143
  <div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
10295
11144
  <div class="card">
10296
- <div class="card-header"><div class="card-title">\u{1F4CB} Recent Friends</div><button class="btn btn-sm btn-ghost" onclick="navigate('friends')">View All \u2192</button></div>
11145
+ <div class="card-header"><div class="card-title">\u{1F4CB} \${t('recent_friends')}</div><button class="btn btn-sm btn-ghost" onclick="navigate('friends')">\${t('view_all')}</button></div>
10297
11146
  \${renderMiniFriendList(friendList.slice(0, 5))}
10298
11147
  </div>
10299
11148
  <div class="card">
10300
- <div class="card-header"><div class="card-title">\u{1F916} Identity Info</div></div>
10301
- <div class="detail-row"><div class="detail-key">Agent ID</div><div class="detail-val mono" style="cursor:pointer" onclick="copyText('\${identity.agentId}')">\${escHtml(identity.agentId)} \u{1F4CB}</div></div>
10302
- <div class="detail-row"><div class="detail-key">Fingerprint</div><div class="detail-val mono">\${escHtml(identity.publicKeyFingerprint)}</div></div>
10303
- <div class="detail-row"><div class="detail-key">Server URL</div><div class="detail-val mono" style="cursor:pointer" onclick="copyText('\${identity.serverUrl}')">\${escHtml(identity.serverUrl)} \u{1F4CB}</div></div>
10304
- <div class="detail-row"><div class="detail-key">Connection</div><div class="detail-val"><span class="badge badge-\${identity.connected ? 'ok' : 'danger'}">\${identity.connected ? 'Online' : 'Offline'}</span></div></div>
10305
- <div class="detail-row"><div class="detail-key">Plugin Version</div><div class="detail-val"><span class="badge badge-accent">v1.2.0</span></div></div>
11149
+ <div class="card-header"><div class="card-title">\u{1F916} \${t('identity_info')}</div></div>
11150
+ <div class="detail-row"><div class="detail-key">\${t('agent_id')}</div><div class="detail-val mono" style="cursor:pointer" onclick="copyText('\${identity.agentId}')">\${escHtml(identity.agentId)} \u{1F4CB}</div></div>
11151
+ <div class="detail-row"><div class="detail-key">\${t('fingerprint')}</div><div class="detail-val mono">\${escHtml(identity.publicKeyFingerprint)}</div></div>
11152
+ <div class="detail-row"><div class="detail-key">\${t('server_url')}</div><div class="detail-val mono" style="cursor:pointer" onclick="copyText('\${identity.serverUrl}')">\${escHtml(identity.serverUrl)} \u{1F4CB}</div></div>
11153
+ <div class="detail-row"><div class="detail-key">\${t('connection')}</div><div class="detail-val"><span class="badge badge-\${identity.connected ? 'ok' : 'danger'}">\${identity.connected ? t('online') : t('offline')}</span></div></div>
11154
+ <div class="detail-row"><div class="detail-key">\${t('plugin_version')}</div><div class="detail-val"><span class="badge badge-accent">v1.2.0</span></div></div>
10306
11155
  </div>
10307
11156
  </div>
10308
11157
  <div class="card" style="margin-top:0">
10309
- <div class="card-header"><div class="card-title">\u{1F5A5}\uFE0F Management UI Access</div></div>
10310
- <div class="detail-row"><div class="detail-key">Current URL</div><div class="detail-val"><a href="\${escHtml(mgmtLink)}" target="_blank" style="color:var(--info);text-decoration:underline">\${escHtml(mgmtLink)}</a></div></div>
10311
- <div class="detail-row"><div class="detail-key">Local Access</div><div class="detail-val"><a href="http://127.0.0.1:6109" target="_blank" style="color:var(--info);text-decoration:underline">http://127.0.0.1:6109</a> <button class="btn btn-sm btn-primary" onclick="window.open('http://127.0.0.1:6109','_blank')" style="margin-left:8px">\u{1F517} Open</button></div></div>
10312
- <div class="detail-row"><div class="detail-key">Gateway Path</div><div class="detail-val mono">/plugins/aicq-chat/</div></div>
11158
+ <div class="card-header"><div class="card-title">\u{1F5A5}\uFE0F \${t('mgmt_ui_access')}</div></div>
11159
+ <div class="detail-row"><div class="detail-key">\${t('current_url')}</div><div class="detail-val"><a href="\${escHtml(mgmtLink)}" target="_blank" style="color:var(--info);text-decoration:underline">\${escHtml(mgmtLink)}</a></div></div>
11160
+ <div class="detail-row"><div class="detail-key">\${t('local_access')}</div><div class="detail-val"><a href="http://127.0.0.1:6109" target="_blank" style="color:var(--info);text-decoration:underline">http://127.0.0.1:6109</a> <button class="btn btn-sm btn-primary" onclick="window.open('http://127.0.0.1:6109','_blank')" style="margin-left:8px">\u{1F517} \${t('open')}</button></div></div>
11161
+ <div class="detail-row"><div class="detail-key">\${t('gateway_path')}</div><div class="detail-val mono">/plugins/aicq-chat/</div></div>
10313
11162
  </div>
10314
11163
  \\\`);
10315
11164
 
@@ -10319,7 +11168,7 @@ async function loadDashboard() {
10319
11168
  }
10320
11169
 
10321
11170
  function renderMiniFriendList(friends) {
10322
- if (!friends.length) return '<div class="empty"><p>No friends yet</p></div>';
11171
+ if (!friends.length) return '<div class="empty"><p>' + t('no_friends_yet') + '</p></div>';
10323
11172
  let html = '';
10324
11173
  friends.forEach(f => {
10325
11174
  html += '<div class="detail-row"><div class="detail-key"><span class="badge badge-' + (f.friendType === 'ai' ? 'info' : 'ghost') + '">' + escHtml(f.friendType || '?') + '</span></div><div class="detail-val mono truncate" style="font-size:12px">' + escHtml(f.id) + '</div></div>';
@@ -10332,7 +11181,7 @@ function renderMiniFriendList(friends) {
10332
11181
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
10333
11182
  async function loadAgents() {
10334
11183
  const el = $('#agents-content');
10335
- html(el, '<div class="loading-mask"><div class="spinner"></div>Loading agents...</div>');
11184
+ html(el, '<div class="loading-mask"><div class="spinner"></div>' + t('loading_agents') + '</div>');
10336
11185
  const data = await api('/agents');
10337
11186
  if (data.error) { html(el, '<div class="empty"><div class="icon">\u26A0\uFE0F</div><p>' + escHtml(data.error) + '</p></div>'); return; }
10338
11187
 
@@ -10342,13 +11191,16 @@ async function loadAgents() {
10342
11191
 
10343
11192
  let rows = '';
10344
11193
  agents.forEach((a, i) => {
10345
- const modelBadge = a.model ? '<span class="badge badge-accent">' + escHtml(a.model) + '</span>' : '<span class="badge badge-ghost">default</span>';
10346
- const providerBadge = a.provider ? '<span class="tag">' + escHtml(a.provider) + '</span>' : '';
10347
- const statusBadge = a.enabled !== false ? '<span class="badge badge-ok">active</span>' : '<span class="badge badge-warn">disabled</span>';
11194
+ const isProviderModel = a._source === 'provider-model';
11195
+ const modelBadge = a.model ? '<span class="badge badge-accent">' + escHtml(a.model) + '</span>' : '<span class="badge badge-ghost">' + t('default_model') + '</span>';
11196
+ const providerName = isProviderModel ? escHtml(capitalizeProvider(a.provider || '')) : escHtml(a.provider || '');
11197
+ const providerBadge = providerName ? '<span class="tag">' + providerName + (a.isDefault ? ' \u2B50' : '') + '</span>' : '';
11198
+ const statusBadge = a.enabled !== false ? '<span class="badge badge-ok">' + t('active') + '</span>' : '<span class="badge badge-warn">' + t('disabled') + '</span>';
11199
+ const defaultBadge = a.isDefault ? ' <span class="badge badge-warn" style="font-size:10px">' + t('default_model_badge') + '</span>' : '';
10348
11200
 
10349
11201
  rows += \\\`<tr>
10350
- <td>\${statusBadge}</td>
10351
- <td><div style="font-weight:600">\${escHtml(a.name || a.id || 'Agent ' + (i + 1))}</div><div class="mono" style="font-size:11px;color:var(--text3)">\${escHtml(a.id || '\u2014')}</div></td>
11202
+ <td>\${statusBadge}\${defaultBadge}</td>
11203
+ <td><div style="font-weight:600">\${escHtml(a.name || 'Agent ' + (i + 1))}</div><div class="mono" style="font-size:11px;color:var(--text3)">\${isProviderModel ? escHtml(a._configPath || '') : escHtml(a.id || '\u2014')}</div></td>
10352
11204
  <td>\${modelBadge}</td>
10353
11205
  <td>\${providerBadge}</td>
10354
11206
  <td>\${escHtml(a.systemPrompt ? a.systemPrompt.substring(0, 60) + '...' : '\u2014')}</td>
@@ -10364,23 +11216,23 @@ async function loadAgents() {
10364
11216
 
10365
11217
  if (!agents.length) {
10366
11218
  html(el, \\\`
10367
- <p class="section-desc">Reads agent configurations from <strong>\${escHtml(configSource)}</strong>. Configure your agents in the config file.</p>
10368
- <div class="empty"><div class="icon">\u{1F916}</div><p>No agents configured</p><p class="sub">Add agents to your openclaw.json or stableclaw.json config file</p></div>
11219
+ <p class="section-desc">\${t('agent_list_from')} <strong>\${escHtml(configSource)}</strong></p>
11220
+ <div class="empty"><div class="icon">\u{1F916}</div><p>\${t('no_agents_configured')}</p><p class="sub">\${t('add_agents_hint')}</p></div>
10369
11221
  \\\`);
10370
11222
  return;
10371
11223
  }
10372
11224
 
10373
11225
  html(el, \\\`
10374
11226
  <div class="toolbar">
10375
- <div class="search-box"><input type="text" placeholder="Search agents..." id="agent-search" oninput="filterAgentTable()"></div>
10376
- <button class="btn btn-sm btn-primary" onclick="showAddAgentModal()">\u2795 Add Agent</button>
10377
- <button class="btn btn-sm btn-default" onclick="loadAgents()">\u{1F504} Refresh</button>
11227
+ <div class="search-box"><input type="text" placeholder="\${t('search_agents')}" id="agent-search" oninput="filterAgentTable()"></div>
11228
+ <button class="btn btn-sm btn-primary" onclick="showAddAgentModal()">\${t('add_agent')}</button>
11229
+ <button class="btn btn-sm btn-default" onclick="loadAgents()">\u{1F504} \${t('refresh')}</button>
10378
11230
  </div>
10379
- <p class="section-desc">Agent list from <strong style="color:var(--accent2)">\${escHtml(configSource)}</strong>. Total: <strong>\${agents.length}</strong> agents configured.</p>
11231
+ <p class="section-desc">\${t('agent_list_from')} <strong style="color:var(--accent2)">\${escHtml(configSource)}</strong>. \${t('total_label')}: <strong>\${agents.length}</strong> \${t('agents_configured')}.</p>
10380
11232
  <div class="card" style="padding:0;overflow:hidden">
10381
11233
  <div style="overflow-x:auto">
10382
11234
  <table>
10383
- <thead><tr><th style="width:60px">Status</th><th>Agent</th><th>Model</th><th>Provider</th><th>System Prompt</th><th style="width:90px">Actions</th></tr></thead>
11235
+ <thead><tr><th style="width:80px">\${t('status')}</th><th>\${t('agent')}</th><th>\${t('model')}</th><th>\${t('provider')}</th><th>\${t('system_prompt')}</th><th style="width:90px">\${t('actions')}</th></tr></thead>
10384
11236
  <tbody id="agent-table-body">\${rows}</tbody>
10385
11237
  </table>
10386
11238
  </div>
@@ -10388,6 +11240,17 @@ async function loadAgents() {
10388
11240
  \\\`);
10389
11241
  }
10390
11242
 
11243
+ function capitalizeProvider(id) {
11244
+ const names = {
11245
+ modelscope: 'ModelScope', zhipu: 'Zhipu AI', qwen: 'Qwen', doubao: 'Doubao',
11246
+ moonshot: 'Moonshot', minimax: 'MiniMax', stepfun: 'StepFun', baidu: 'Baidu',
11247
+ spark: 'Spark', deepseek: 'DeepSeek', openai: 'OpenAI', anthropic: 'Anthropic',
11248
+ google: 'Google AI', groq: 'Groq', ollama: 'Ollama', openrouter: 'OpenRouter',
11249
+ mistral: 'Mistral AI', together: 'Together AI', fireworks: 'Fireworks AI',
11250
+ };
11251
+ return names[id] || (id ? id.charAt(0).toUpperCase() + id.slice(1) : '\u2014');
11252
+ }
11253
+
10391
11254
  function filterAgentTable() {
10392
11255
  const q = ($('#agent-search')?.value || '').toLowerCase();
10393
11256
  $$('#agent-table-body tr').forEach(tr => {
@@ -10406,23 +11269,34 @@ function viewAgent(index) {
10406
11269
  details += '<div class="detail-row"><div class="detail-key">' + escHtml(k) + '</div><div class="detail-val mono" style="font-size:12px;cursor:pointer" onclick="copyText(decodeURIComponent(\\'' + encodeURIComponent(String(v)) + '\\'))">' + display + ' \u{1F4CB}</div></div>';
10407
11270
  }
10408
11271
  }
10409
- html('#view-agent-body', details || '<div class="empty"><p>No data</p></div>');
10410
- $('#view-agent-title').textContent = a.name || a.id || 'Agent';
11272
+ html('#view-agent-body', details || '<div class="empty"><p>' + t('no_data') + '</p></div>');
11273
+ $('#view-agent-title').textContent = a.name || a.id || t('agent');
10411
11274
  showModal('modal-view-agent');
10412
11275
  }
10413
11276
 
10414
11277
  async function deleteAgent(index) {
10415
- if (!confirm('Are you sure you want to delete this agent?')) return;
10416
- const r = await api('/agents/' + index, { method: 'DELETE' });
10417
- if (r.success) { toast('Agent deleted', 'ok'); loadAgents(); }
10418
- else { toast(r.message || r.error || 'Delete failed', 'err'); }
11278
+ const agents = window._lastAgentsData?.agents || [];
11279
+ const a = agents[index];
11280
+ if (!a) return;
11281
+ if (!confirm(t('confirm_delete_agent'))) return;
11282
+ let identifier;
11283
+ if (a._source === 'provider-model') {
11284
+ identifier = 'provider:' + (a._providerId || '') + ':' + (a._modelIndex || 0);
11285
+ } else {
11286
+ identifier = index;
11287
+ }
11288
+ const r = await api('/agents/' + encodeURIComponent(identifier), { method: 'DELETE' });
11289
+ if (r.success) { toast(t('agent_deleted'), 'ok'); loadAgents(); }
11290
+ else { toast(r.message || r.error || t('delete_failed'), 'err'); }
10419
11291
  }
10420
11292
 
10421
11293
  let _editAgentIndex = null;
11294
+ let _editAgentIsProviderModel = false;
10422
11295
 
10423
11296
  function showAddAgentModal() {
10424
11297
  _editAgentIndex = null;
10425
- $('#agent-form-title').textContent = '\u2795 Add New Agent';
11298
+ _editAgentIsProviderModel = false;
11299
+ $('#agent-form-title').textContent = t('add_new_agent');
10426
11300
  $('#agent-form-name').value = '';
10427
11301
  $('#agent-form-id').value = '';
10428
11302
  $('#agent-form-model').value = '';
@@ -10442,9 +11316,10 @@ function showEditAgentModal(index) {
10442
11316
  const a = agents[index];
10443
11317
  if (!a) return;
10444
11318
  _editAgentIndex = index;
10445
- $('#agent-form-title').textContent = '\u270F\uFE0F Edit Agent';
11319
+ _editAgentIsProviderModel = a._source === 'provider-model';
11320
+ $('#agent-form-title').textContent = t('edit_agent');
10446
11321
  $('#agent-form-name').value = a.name || '';
10447
- $('#agent-form-id').value = a.id || '';
11322
+ $('#agent-form-id').value = a._source === 'provider-model' ? (a._configPath || '') : (a.id || '');
10448
11323
  $('#agent-form-model').value = a.model || '';
10449
11324
  $('#agent-form-provider').value = a.provider || '';
10450
11325
  $('#agent-form-prompt').value = a.systemPrompt || '';
@@ -10453,6 +11328,13 @@ function showEditAgentModal(index) {
10453
11328
  $('#agent-form-max-tokens').value = a.maxTokens ?? 4096;
10454
11329
  $('#agent-form-top-p').value = a.topP ?? 1;
10455
11330
  $('#agent-form-tools').value = Array.isArray(a.tools) ? a.tools.join(', ') : (a.tools || '');
11331
+ if (a._source === 'provider-model') {
11332
+ $('#agent-form-id').readOnly = true;
11333
+ $('#agent-form-id').title = a._configPath || '';
11334
+ } else {
11335
+ $('#agent-form-id').readOnly = false;
11336
+ $('#agent-form-id').title = '';
11337
+ }
10456
11338
  showModal('modal-add-agent');
10457
11339
  }
10458
11340
 
@@ -10475,23 +11357,29 @@ async function saveAgent() {
10475
11357
  tools: toolsRaw ? toolsRaw.split(',').map(t => t.trim()).filter(Boolean) : [],
10476
11358
  };
10477
11359
 
10478
- if (!agent.name) { toast('Agent name is required', 'warn'); return; }
11360
+ if (!agent.name) { toast(t('agent_name_required'), 'warn'); return; }
10479
11361
 
10480
11362
  let r;
10481
11363
  if (_editAgentIndex !== null) {
10482
- // Edit existing
10483
- r = await api('/agents/' + _editAgentIndex, { method: 'PUT', body: JSON.stringify({ agent }) });
11364
+ let identifier;
11365
+ if (_editAgentIsProviderModel) {
11366
+ const a = (window._lastAgentsData?.agents || [])[_editAgentIndex];
11367
+ identifier = 'provider:' + (a?._providerId || '') + ':' + (a?._modelIndex || 0);
11368
+ } else {
11369
+ identifier = _editAgentIndex;
11370
+ }
11371
+ r = await api('/agents/' + encodeURIComponent(identifier), { method: 'PUT', body: JSON.stringify({ agent }) });
10484
11372
  } else {
10485
11373
  // Add new
10486
11374
  r = await api('/agents', { method: 'POST', body: JSON.stringify({ agent }) });
10487
11375
  }
10488
11376
 
10489
11377
  if (r.success) {
10490
- toast(_editAgentIndex !== null ? 'Agent updated' : 'Agent added', 'ok');
11378
+ toast(_editAgentIndex !== null ? t('agent_updated') : t('agent_added'), 'ok');
10491
11379
  hideModal('modal-add-agent');
10492
11380
  loadAgents();
10493
11381
  } else {
10494
- toast(r.message || r.error || 'Failed', 'err');
11382
+ toast(r.message || r.error || t('failed'), 'err');
10495
11383
  }
10496
11384
  }
10497
11385
 
@@ -10502,7 +11390,7 @@ let friendsFilter = 'all';
10502
11390
 
10503
11391
  async function loadFriends() {
10504
11392
  const el = $('#friends-content');
10505
- html(el, '<div class="loading-mask"><div class="spinner"></div>Loading friends...</div>');
11393
+ html(el, '<div class="loading-mask"><div class="spinner"></div>' + t('loading_friends') + '</div>');
10506
11394
  const results = await Promise.allSettled([api('/friends'), api('/friends/requests'), api('/sessions')]);
10507
11395
  const friends = results[0].status === 'fulfilled' ? results[0].value : { friends: [] };
10508
11396
  const requests = results[1].status === 'fulfilled' ? results[1].value : { requests: [] };
@@ -10521,9 +11409,9 @@ async function loadFriends() {
10521
11409
  const sessCount = (sessions.sessions || []).length;
10522
11410
 
10523
11411
  html('#friends-tabs', \\\`
10524
- <button class="filter-btn \${friendsSubTab==='friends'?'active':''}" onclick="friendsSubTab='friends';loadFriends()">\u{1F465} Friends (<span id="fc">\${friendCount}</span>)</button>
10525
- <button class="filter-btn \${friendsSubTab==='requests'?'active':''}" onclick="friendsSubTab='requests';loadFriends()">\u{1F4E8} Requests (<span id="rc">\${reqCount}</span>)</button>
10526
- <button class="filter-btn \${friendsSubTab==='sessions'?'active':''}" onclick="friendsSubTab='sessions';loadFriends()">\u{1F517} Sessions (<span id="sc">\${sessCount}</span>)</button>
11412
+ <button class="filter-btn \${friendsSubTab==='friends'?'active':''}" onclick="friendsSubTab='friends';loadFriends()">\u{1F465} \${t('friends')} (<span id="fc">\${friendCount}</span>)</button>
11413
+ <button class="filter-btn \${friendsSubTab==='requests'?'active':''}" onclick="friendsSubTab='requests';loadFriends()">\u{1F4E8} \${t('requests')} (<span id="rc">\${reqCount}</span>)</button>
11414
+ <button class="filter-btn \${friendsSubTab==='sessions'?'active':''}" onclick="friendsSubTab='sessions';loadFriends()">\u{1F517} \${t('sessions')} (<span id="sc">\${sessCount}</span>)</button>
10527
11415
  \\\`);
10528
11416
 
10529
11417
  window._friendsData = friends;
@@ -10558,24 +11446,24 @@ function renderFriendsList(friends) {
10558
11446
 
10559
11447
  html(el, \\\`
10560
11448
  <div class="toolbar">
10561
- <div class="search-box"><input type="text" placeholder="Search friends..." id="friend-search" oninput="filterFriendTable()"></div>
11449
+ <div class="search-box"><input type="text" placeholder="\${t('search_friends')}" id="friend-search" oninput="filterFriendTable()"></div>
10562
11450
  <div class="filter-group">
10563
- <button class="filter-btn \${friendsFilter==='all'?'active':''}" onclick="friendsFilter='all';filterFriendTable()">All</button>
10564
- <button class="filter-btn \${friendsFilter==='ai'?'active':''}" onclick="friendsFilter='ai';filterFriendTable()">AI</button>
10565
- <button class="filter-btn \${friendsFilter==='human'?'active':''}" onclick="friendsFilter='human';filterFriendTable()">Human</button>
11451
+ <button class="filter-btn \${friendsFilter==='all'?'active':''}" onclick="friendsFilter='all';filterFriendTable()">\${t('all')}</button>
11452
+ <button class="filter-btn \${friendsFilter==='ai'?'active':''}" onclick="friendsFilter='ai';filterFriendTable()">\${t('ai')}</button>
11453
+ <button class="filter-btn \${friendsFilter==='human'?'active':''}" onclick="friendsFilter='human';filterFriendTable()">\${t('human')}</button>
10566
11454
  </div>
10567
11455
  <span style="flex:1"></span>
10568
- <button class="btn btn-sm btn-primary" onclick="showAddFriendModal()" \${isOffline ? 'disabled title="Unavailable while offline"' : ''}>\u2795 Add Friend</button>
11456
+ <button class="btn btn-sm btn-primary" onclick="showAddFriendModal()" \${isOffline ? 'disabled title="' + t('unavailable_offline') + '"' : ''}>\${t('add_friend')}</button>
10569
11457
  <button class="btn btn-sm btn-default" onclick="loadFriends()">\u{1F504}</button>
10570
11458
  </div>
10571
11459
  <div class="card" style="padding:0;overflow:hidden">
10572
11460
  <div style="overflow-x:auto;max-height:calc(100vh - 280px);overflow-y:auto">
10573
11461
  <table>
10574
- <thead><tr><th style="width:60px">Type</th><th>Friend</th><th>Permissions</th><th>Fingerprint</th><th>Last Message</th><th style="width:80px">Actions</th></tr></thead>
11462
+ <thead><tr><th style="width:60px">\${t('type')}</th><th>\${t('friend_label')}</th><th>\${t('permissions')}</th><th>\${t('fingerprint')}</th><th>\${t('last_message')}</th><th style="width:80px">\${t('actions')}</th></tr></thead>
10575
11463
  <tbody id="friend-table-body">\${rows}</tbody>
10576
11464
  </table>
10577
11465
  </div>
10578
- \${!friends.length ? '<div class="empty"><div class="icon">\u{1F465}</div><p>No friends yet</p><p class="sub">Add a friend using their 6-digit temp number or node ID</p></div>' : ''}
11466
+ \${!friends.length ? '<div class="empty"><div class="icon">\u{1F465}</div><p>' + t('no_friends_yet') + '</p><p class="sub">' + t('add_friend_hint') + '</p></div>' : ''}
10579
11467
  </div>
10580
11468
  \\\`);
10581
11469
  }
@@ -10600,18 +11488,18 @@ function renderRequestsList(requests) {
10600
11488
  <td><span class="badge badge-\${stCls}">\${escHtml(r.status)}</span></td>
10601
11489
  <td>\${timeAgo(r.createdAt)}</td>
10602
11490
  <td>
10603
- \${r.status === 'pending' ? '<div class="actions-cell"><button class="btn btn-sm btn-ok" onclick="acceptFriendReq(\\'' + escHtml(r.id) + '\\')">\u2713 Accept</button><button class="btn btn-sm btn-danger" onclick="rejectFriendReq(\\'' + escHtml(r.id) + '\\')">\u2717 Reject</button></div>' : '\u2014'}
11491
+ \${r.status === 'pending' ? '<div class="actions-cell"><button class="btn btn-sm btn-ok" onclick="acceptFriendReq(\\'' + escHtml(r.id) + '\\')">\u2713 \${t('accept')}</button><button class="btn btn-sm btn-danger" onclick="rejectFriendReq(\\'' + escHtml(r.id) + '\\')">\u2717 \${t('reject')}</button></div>' : '\u2014'}
10604
11492
  </td>
10605
11493
  </tr>\\\`;
10606
11494
  });
10607
11495
  html(el, \\\`
10608
- <div class="toolbar"><button class="btn btn-sm btn-default" onclick="loadFriends()">\u{1F504} Refresh</button></div>
11496
+ <div class="toolbar"><button class="btn btn-sm btn-default" onclick="loadFriends()">\u{1F504} \${t('refresh')}</button></div>
10609
11497
  <div class="card" style="padding:0;overflow:hidden">
10610
11498
  <div style="overflow-x:auto"><table>
10611
- <thead><tr><th>Request ID</th><th>From</th><th>Status</th><th>Time</th><th style="width:160px">Actions</th></tr></thead>
11499
+ <thead><tr><th>\${t('request_id')}</th><th>\${t('from')}</th><th>\${t('status')}</th><th>\${t('time')}</th><th style="width:160px">\${t('actions')}</th></tr></thead>
10612
11500
  <tbody>\${rows}</tbody>
10613
11501
  </table></div>
10614
- \${!requests.length ? '<div class="empty"><div class="icon">\u{1F4E8}</div><p>No pending requests</p></div>' : ''}
11502
+ \${!requests.length ? '<div class="empty"><div class="icon">\u{1F4E8}</div><p>' + t('no_pending_requests') + '</p></div>' : ''}
10615
11503
  </div>
10616
11504
  \\\`);
10617
11505
  }
@@ -10623,17 +11511,17 @@ function renderSessionsList(sessions) {
10623
11511
  rows += \\\`<tr>
10624
11512
  <td class="mono" style="font-size:12px;cursor:pointer" onclick="copyText('\${escHtml(s.peerId)}')">\${escHtml(s.peerId)} \u{1F4CB}</td>
10625
11513
  <td>\${timeAgo(s.createdAt)}</td>
10626
- <td><span class="badge badge-info">\${s.messageCount} messages</span></td>
11514
+ <td><span class="badge badge-info">\${s.messageCount} \${t('messages')}</span></td>
10627
11515
  </tr>\\\`;
10628
11516
  });
10629
11517
  html(el, \\\`
10630
- <div class="toolbar"><button class="btn btn-sm btn-default" onclick="loadFriends()">\u{1F504} Refresh</button></div>
11518
+ <div class="toolbar"><button class="btn btn-sm btn-default" onclick="loadFriends()">\u{1F504} \${t('refresh')}</button></div>
10631
11519
  <div class="card" style="padding:0;overflow:hidden">
10632
11520
  <div style="overflow-x:auto"><table>
10633
- <thead><tr><th>Peer ID</th><th>Established</th><th>Messages</th></tr></thead>
11521
+ <thead><tr><th>\${t('peer_id')}</th><th>\${t('established')}</th><th>\${t('messages')}</th></tr></thead>
10634
11522
  <tbody>\${rows}</tbody>
10635
11523
  </table></div>
10636
- \${!sessions.length ? '<div class="empty"><div class="icon">\u{1F517}</div><p>No active sessions</p></div>' : ''}
11524
+ \${!sessions.length ? '<div class="empty"><div class="icon">\u{1F517}</div><p>' + t('no_active_sessions') + '</p></div>' : ''}
10637
11525
  </div>
10638
11526
  \\\`);
10639
11527
  }
@@ -10641,18 +11529,18 @@ function renderSessionsList(sessions) {
10641
11529
  function showAddFriendModal() { $('#add-friend-target').value = ''; showModal('modal-add-friend'); setTimeout(() => $('#add-friend-target')?.focus(), 100); }
10642
11530
  async function addFriend() {
10643
11531
  const target = $('#add-friend-target').value.trim();
10644
- if (!target) { toast('Enter a temp number or node ID', 'warn'); return; }
11532
+ if (!target) { toast(t('enter_temp_or_id'), 'warn'); return; }
10645
11533
  hideModal('modal-add-friend');
10646
- toast('Sending friend request...', 'info');
11534
+ toast(t('sending_request'), 'info');
10647
11535
  const r = await api('/friends', { method: 'POST', body: JSON.stringify({ target }) });
10648
- if (r.success) { toast(r.message || 'Friend request sent!', 'ok'); loadFriends(); }
10649
- else { toast(r.message || r.error || 'Failed to add friend', 'err'); }
11536
+ if (r.success) { toast(r.message || t('friend_request_sent'), 'ok'); loadFriends(); }
11537
+ else { toast(r.message || r.error || t('failed_add_friend'), 'err'); }
10650
11538
  }
10651
11539
  async function removeFriend(id) {
10652
- if (!confirm('Remove friend ' + id + '?')) return;
11540
+ if (!confirm(t('remove_friend_confirm') + id + '?')) return;
10653
11541
  const r = await api('/friends/' + encodeURIComponent(id), { method: 'DELETE' });
10654
- if (r.success) { toast('Friend removed', 'ok'); loadFriends(); }
10655
- else { toast(r.message || r.error || 'Failed', 'err'); }
11542
+ if (r.success) { toast(t('friend_removed'), 'ok'); loadFriends(); }
11543
+ else { toast(r.message || r.error || t('failed'), 'err'); }
10656
11544
  }
10657
11545
 
10658
11546
  let _editFriendId = null;
@@ -10667,16 +11555,16 @@ async function saveFriendPerms() {
10667
11555
  if ($('#perm-chat').checked) perms.push('chat');
10668
11556
  if ($('#perm-exec').checked) perms.push('exec');
10669
11557
  const r = await api('/friends/' + encodeURIComponent(_editFriendId) + '/permissions', { method: 'PUT', body: JSON.stringify({ permissions: perms }) });
10670
- if (r.success) { toast('Permissions updated', 'ok'); hideModal('modal-permissions'); loadFriends(); }
10671
- else { toast(r.message || r.error || 'Failed', 'err'); }
11558
+ if (r.success) { toast(t('permissions_updated'), 'ok'); hideModal('modal-permissions'); loadFriends(); }
11559
+ else { toast(r.message || r.error || t('failed'), 'err'); }
10672
11560
  }
10673
11561
  async function acceptFriendReq(id) {
10674
11562
  const r = await api('/friends/requests/' + encodeURIComponent(id) + '/accept', { method: 'POST', body: JSON.stringify({ permissions: ['chat'] }) });
10675
- if (r.success) { toast('Request accepted', 'ok'); loadFriends(); } else { toast(r.message || r.error || 'Failed', 'err'); }
11563
+ if (r.success) { toast(t('request_accepted'), 'ok'); loadFriends(); } else { toast(r.message || r.error || t('failed'), 'err'); }
10676
11564
  }
10677
11565
  async function rejectFriendReq(id) {
10678
11566
  const r = await api('/friends/requests/' + encodeURIComponent(id) + '/reject', { method: 'POST', body: JSON.stringify({}) });
10679
- if (r.success) { toast('Request rejected', 'ok'); loadFriends(); } else { toast(r.message || r.error || 'Failed', 'err'); }
11567
+ if (r.success) { toast(t('request_rejected'), 'ok'); loadFriends(); } else { toast(r.message || r.error || t('failed'), 'err'); }
10680
11568
  }
10681
11569
 
10682
11570
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
@@ -10686,36 +11574,81 @@ let _modelProviders = null;
10686
11574
 
10687
11575
  async function loadModels() {
10688
11576
  const el = $('#models-content');
10689
- html(el, '<div class="loading-mask"><div class="spinner"></div>Loading model configuration...</div>');
11577
+ html(el, '<div class="loading-mask"><div class="spinner"></div>' + t('loading_models') + '</div>');
10690
11578
  const data = await api('/models');
10691
11579
  if (data.error) { html(el, '<div class="empty"><div class="icon">\u26A0\uFE0F</div><p>' + escHtml(data.error) + '</p></div>'); return; }
10692
11580
  _modelProviders = data;
10693
11581
  renderModels(data);
10694
11582
  }
10695
11583
 
11584
+ function getProviderIcon(id) {
11585
+ const icons = {
11586
+ openai: '\u{1F7E2}', anthropic: '\u{1F7E0}', google: '\u{1F535}', ollama: '\u{1F7E3}', deepseek: '\u{1F537}',
11587
+ groq: '\u26A1', openrouter: '\u{1F310}', mistral: '\u{1F300}', together: '\u{1F52E}', fireworks: '\u{1F386}',
11588
+ modelscope: '\u{1F3D7}\uFE0F', zhipu: '\u{1F9E0}', qwen: '\u2601\uFE0F', doubao: '\u{1FAD8}', moonshot: '\u{1F319}',
11589
+ minimax: '\u{1F537}', stepfun: '\u{1F4C8}', baidu: '\u{1F50D}', spark: '\u2728',
11590
+ };
11591
+ if (id && id.startsWith('custom-')) return '\u{1F9E9}';
11592
+ return icons[id] || '\u26AA';
11593
+ }
11594
+
10696
11595
  function renderModels(data) {
10697
11596
  const el = $('#models-content');
10698
11597
  const providers = data.providers || [];
10699
11598
  const configured = providers.filter(p => p.configured).length;
11599
+ const defaultModel = data.defaultModel || '';
11600
+
11601
+ // Default model banner
11602
+ let defaultBanner = '';
11603
+ if (defaultModel) {
11604
+ defaultBanner = \\\`
11605
+ <div class="card" style="border-color:var(--warn);background:var(--warn-bg)">
11606
+ <div class="card-header">
11607
+ <div class="card-title" style="color:#d97706">\u2B50 \${t('default_model_global')}</div>
11608
+ </div>
11609
+ <div style="display:flex;align-items:center;gap:12px">
11610
+ <span class="badge badge-warn" style="font-size:13px;padding:4px 14px">\${escHtml(defaultModel)}</span>
11611
+ </div>
11612
+ </div>\\\`;
11613
+ }
10700
11614
 
10701
11615
  let cards = '';
10702
11616
  providers.forEach(p => {
10703
- const icon = p.id === 'openai' ? '\u{1F7E2}' : p.id === 'anthropic' ? '\u{1F7E0}' : p.id === 'google' ? '\u{1F535}' : p.id === 'ollama' ? '\u{1F7E3}' : p.id === 'deepseek' ? '\u{1F537}' : p.id === 'groq' ? '\u26A1' : p.id === 'openrouter' ? '\u{1F310}' : '\u26AA';
11617
+ const icon = getProviderIcon(p.id);
10704
11618
  const statusBadge = p.configured
10705
- ? '<span class="badge badge-ok">\u25CF Configured</span>'
10706
- : '<span class="badge badge-ghost">Not set</span>';
10707
- const currentModel = p.modelId ? '<span class="prov-model">' + escHtml(p.modelId) + '</span>' : '';
11619
+ ? '<span class="badge badge-ok">' + t('key_set') + '</span>'
11620
+ : '<span class="badge badge-ghost">' + t('not_set') + '</span>';
11621
+
11622
+ // Show multi-model info
11623
+ let modelInfo = '';
11624
+ if (p.configured && p.modelCount > 0) {
11625
+ modelInfo = '<span class="prov-model">' + t('multi_model') + ': ' + p.modelCount + ' ' + t('models_under_provider') + '</span>';
11626
+ const shownModels = (p.models || []).slice(0, 3);
11627
+ shownModels.forEach(m => {
11628
+ const isDef = defaultModel === (m.id || '');
11629
+ modelInfo += '<span class="prov-model" style="margin-left:4px">' + escHtml(m.name || m.id || '') + (isDef ? ' \u2B50' : '') + '</span>';
11630
+ });
11631
+ if (p.modelCount > 3) {
11632
+ modelInfo += '<span class="prov-model" style="color:var(--text3);margin-left:4px">+' + (p.modelCount - 3) + ' more</span>';
11633
+ }
11634
+ } else if (p.modelId) {
11635
+ modelInfo = '<span class="prov-model">' + escHtml(p.modelId) + '</span>';
11636
+ }
10708
11637
 
10709
11638
  cards += \\\`
10710
- <div class="provider-card" onclick="showModelConfigModal('\${escHtml(p.id)}')">
11639
+ <div class="provider-card \${p.isCustom ? 'custom-provider' : ''}" onclick="\${p.isCustom ? "showEditCustomProviderModal('" + escHtml(p.id) + "')" : "showModelConfigModal('" + escHtml(p.id) + "')"}">
10711
11640
  <div class="prov-head">
10712
- <div class="prov-name">\${icon} \${escHtml(p.name)}</div>
11641
+ <div class="prov-name">\${icon} \${escHtml(p.name)}\${p.isCustom ? ' <span class="tag" style="background:var(--accent-bg);color:var(--accent2)">\u{1F9E9} Custom</span>' : ''}</div>
10713
11642
  \${statusBadge}
10714
11643
  </div>
10715
- <div class="prov-desc">\${escHtml(p.description)}</div>
10716
- \${currentModel}
11644
+ <div class="prov-desc">\${escHtml(p.description || '')}</div>
11645
+ \${modelInfo}
10717
11646
  <div class="prov-actions">
10718
- <button class="btn btn-sm btn-primary" onclick="event.stopPropagation();showModelConfigModal('\${escHtml(p.id)}')">Configure</button>
11647
+ \${p.isCustom
11648
+ ? '<button class="btn btn-sm btn-ok" onclick="event.stopPropagation();showEditCustomProviderModal(\\'' + escHtml(p.id) + '\\')">\u270F\uFE0F ' + t('configure') + '</button>' +
11649
+ '<button class="btn btn-sm btn-danger" onclick="event.stopPropagation();deleteCustomProvider(\\'' + escHtml(p.id) + '\\',\\'' + escHtml(p.name) + '\\')">\u{1F5D1}\uFE0F</button>'
11650
+ : '<button class="btn btn-sm btn-primary" onclick="event.stopPropagation();showModelConfigModal(\\'' + escHtml(p.id) + '\\')">' + t('configure') + '</button>'
11651
+ }
10719
11652
  </div>
10720
11653
  </div>\\\`;
10721
11654
  });
@@ -10724,14 +11657,16 @@ function renderModels(data) {
10724
11657
  if (data.currentModels && data.currentModels.length) {
10725
11658
  let rows = '';
10726
11659
  data.currentModels.forEach(m => {
11660
+ const defaultTag = m.isDefault ? ' <span class="badge badge-warn" style="font-size:10px">\u2B50 ' + t('default_model_badge') + '</span>' : '';
10727
11661
  rows += \\\`<tr>
10728
11662
  <td style="font-weight:500">\${escHtml(m.provider)}</td>
10729
- <td class="mono">\${escHtml(m.modelId)}</td>
10730
- <td><span class="badge badge-ok">\u25CF Key set</span></td>
10731
- <td class="mono" style="font-size:11px">\${escHtml(m.baseUrl || 'default')}</td>
11663
+ <td class="mono">\${escHtml(m.modelId)}\${defaultTag}</td>
11664
+ <td>\${escHtml(m.modelName || '')}</td>
11665
+ <td><span class="badge badge-ok">' + t('key_set') + '</span></td>
11666
+ <td class="mono" style="font-size:11px">\${escHtml(m.baseUrl || t('default_model'))}</td>
10732
11667
  <td>
10733
11668
  <div class="actions-cell">
10734
- <button class="btn btn-sm btn-ghost" onclick="showModelConfigModal('\${escHtml(m.providerId)}')">Edit</button>
11669
+ <button class="btn btn-sm btn-ghost" onclick="showModelConfigModal('\${escHtml(m.providerId)}')">\${t('configure')}</button>
10735
11670
  <button class="btn btn-sm btn-danger" onclick="deleteModelProvider('\${escHtml(m.providerId)}')" title="Delete">\u{1F5D1}\uFE0F</button>
10736
11671
  </div>
10737
11672
  </td>
@@ -10739,61 +11674,174 @@ function renderModels(data) {
10739
11674
  });
10740
11675
  activeModelsSection = \\\`
10741
11676
  <div class="card" style="margin-top:20px">
10742
- <div class="card-header"><div class="card-title">\u{1F4CA} Active Model Configurations</div></div>
11677
+ <div class="card-header"><div class="card-title">\u{1F4CA} \${t('active_model_configs')}</div></div>
10743
11678
  <div style="overflow-x:auto"><table>
10744
- <thead><tr><th>Provider</th><th>Model</th><th>API Key</th><th>Base URL</th><th>Actions</th></tr></thead>
11679
+ <thead><tr><th>\${t('provider')}</th><th>\${t('model')}</th><th>\${t('model_name_label')}</th><th>API Key</th><th>\${t('base_url')}</th><th>\${t('actions')}</th></tr></thead>
10745
11680
  <tbody>\${rows}</tbody>
10746
11681
  </table></div>
10747
11682
  </div>\\\`;
10748
11683
  }
10749
11684
 
10750
11685
  html(el, \\\`
11686
+ \${defaultBanner}
10751
11687
  <div class="stats-grid" style="margin-bottom:24px">
10752
11688
  <div class="stat-card">
10753
11689
  <div class="stat-icon" style="background:var(--accent-bg)">\u{1F9E0}</div>
10754
- <div class="stat-label">Configured</div>
11690
+ <div class="stat-label">\${t('configured')}</div>
10755
11691
  <div class="stat-value">\${configured} / \${providers.length}</div>
10756
- <div class="stat-sub">Providers with API keys</div>
11692
+ <div class="stat-sub">\${t('providers_with_keys')}</div>
10757
11693
  </div>
11694
+ \${defaultModel ? '<div class="stat-card"><div class="stat-icon" style="background:var(--warn-bg)">\u2B50</div><div class="stat-label">' + t('default_model_label') + '</div><div class="stat-value mono" style="font-size:13px">' + escHtml(defaultModel) + '</div></div>' : ''}
11695
+ </div>
11696
+ <p class="section-desc">\${t('configure_providers_desc')}</p>
11697
+ <div style="margin-bottom:16px;text-align:right">
11698
+ <button class="btn btn-primary" onclick="showAddCustomProviderModal()" style="font-size:13px">
11699
+ \${t('add_custom_provider')}
11700
+ </button>
10758
11701
  </div>
10759
- <p class="section-desc">Configure LLM providers for your agents. Click a provider card to set or update the API key, model, and base URL. Changes are saved directly to your config file.</p>
10760
11702
  <div class="provider-grid">\${cards}</div>
10761
11703
  \${activeModelsSection}
10762
11704
  \\\`);
10763
11705
  }
10764
11706
 
10765
11707
  let _editProviderId = null;
11708
+
11709
+ // --- Custom Provider Functions ---
11710
+ var _editCustomId = null;
11711
+
11712
+ function showAddCustomProviderModal() {
11713
+ _editCustomId = null;
11714
+ document.getElementById('custom-provider-title').textContent = t('custom_provider');
11715
+ document.getElementById('custom-name').value = '';
11716
+ document.getElementById('custom-api-key').value = '';
11717
+ document.getElementById('custom-model-id').value = '';
11718
+ document.getElementById('custom-base-url').value = '';
11719
+ document.getElementById('custom-description').value = '';
11720
+ showModal('modal-custom-provider');
11721
+ }
11722
+
11723
+ function showEditCustomProviderModal(id) {
11724
+ if (!_modelProviders) return;
11725
+ const p = _modelProviders.providers.find(pr => pr.id === id);
11726
+ if (!p || !p.isCustom) return;
11727
+ _editCustomId = id;
11728
+ document.getElementById('custom-provider-title').textContent = t('edit_custom_provider');
11729
+ document.getElementById('custom-name').value = p.name || '';
11730
+ document.getElementById('custom-api-key').value = '';
11731
+ document.getElementById('custom-model-id').value = p.modelId || p.modelHint || '';
11732
+ document.getElementById('custom-base-url').value = p.baseUrl || '';
11733
+ document.getElementById('custom-description').value = p.description || '';
11734
+ showModal('modal-custom-provider');
11735
+ }
11736
+
11737
+ async function saveCustomProvider() {
11738
+ var name = document.getElementById('custom-name').value.trim();
11739
+ var apiKey = document.getElementById('custom-api-key').value.trim();
11740
+ var modelId = document.getElementById('custom-model-id').value.trim();
11741
+ var baseUrl = document.getElementById('custom-base-url').value.trim();
11742
+ var description = document.getElementById('custom-description').value.trim();
11743
+
11744
+ if (!name) { toast(t('provider_name_required'), 'err'); return; }
11745
+
11746
+ var url, method;
11747
+ if (_editCustomId) {
11748
+ url = '/api/models/custom/' + encodeURIComponent(_editCustomId);
11749
+ method = 'PUT';
11750
+ } else {
11751
+ url = '/api/models/custom';
11752
+ method = 'POST';
11753
+ }
11754
+
11755
+ var body = { name: name };
11756
+ if (apiKey) body.apiKey = apiKey;
11757
+ if (modelId) body.modelId = modelId;
11758
+ if (baseUrl) body.baseUrl = baseUrl;
11759
+ if (description) body.description = description;
11760
+
11761
+ try {
11762
+ var resp = await fetch(url, { method: method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
11763
+ var data = await resp.json();
11764
+ if (data.success) {
11765
+ hideModal('modal-custom-provider');
11766
+ toast(_editCustomId ? t('custom_provider_updated') : t('custom_provider_added'), 'ok');
11767
+ loadModels();
11768
+ } else {
11769
+ toast(data.message || 'Failed', 'err');
11770
+ }
11771
+ } catch (e) {
11772
+ toast('Request failed: ' + e.message, 'err');
11773
+ }
11774
+ }
11775
+
11776
+ function deleteCustomProvider(id, name) {
11777
+ if (!confirm(t('confirm_delete_custom').replace('{name}', name || id))) return;
11778
+ fetch('/api/models/custom/' + encodeURIComponent(id), { method: 'DELETE' })
11779
+ .then(function(r) { return r.json(); })
11780
+ .then(function(data) {
11781
+ if (data.success) {
11782
+ toast(t('custom_provider_deleted'), 'ok');
11783
+ loadModels();
11784
+ } else {
11785
+ toast(data.message || 'Failed to delete', 'err');
11786
+ }
11787
+ })
11788
+ .catch(function(e) { toast('Delete failed: ' + e.message, 'err'); });
11789
+ }
11790
+
10766
11791
  function showModelConfigModal(id) {
10767
11792
  const p = (_modelProviders?.providers || []).find(x => x.id === id);
10768
- if (!p) { toast('Provider not found', 'err'); return; }
11793
+ if (!p) { toast(t('provider_not_found'), 'err'); return; }
10769
11794
  _editProviderId = id;
10770
11795
  $('#model-name').textContent = p.name;
10771
- $('#model-icon').textContent = p.id === 'openai' ? '\u{1F7E2}' : p.id === 'anthropic' ? '\u{1F7E0}' : '\u{1F7E2}';
11796
+ $('#model-icon').textContent = getProviderIcon(p.id);
10772
11797
  $('#model-api-key').value = '';
10773
- $('#model-api-key').placeholder = p.apiKeyHint || 'Enter API key';
11798
+ $('#model-api-key').placeholder = p.apiKeyHint || t('enter_api_key');
10774
11799
  $('#model-model-id').value = p.modelId || '';
10775
- $('#model-model-id').placeholder = p.modelHint || 'Model ID';
11800
+ $('#model-model-id').placeholder = p.modelHint || t('enter_model_id');
10776
11801
  $('#model-base-url').value = p.baseUrl || '';
10777
- $('#model-base-url').placeholder = p.baseUrlHint || 'Default URL';
10778
- $('#model-current-key').textContent = p.apiKeyHasValue ? 'Current: ' + p.apiKey : 'No API key configured';
11802
+ $('#model-base-url').placeholder = p.baseUrlHint || t('default_url');
11803
+ $('#model-current-key').textContent = p.apiKeyHasValue ? t('current_key') + p.apiKey : t('no_api_key');
11804
+ // Show multi-model list if available
11805
+ const modelsListEl = document.getElementById('model-multi-list');
11806
+ if (modelsListEl) {
11807
+ if (p.models && p.models.length > 0) {
11808
+ const defaultModel = _modelProviders?.defaultModel || '';
11809
+ let html = '<div style="font-size:12px;color:var(--text2);margin-bottom:8px;font-weight:600">' + t('model_count') + ': ' + p.models.length + '</div>';
11810
+ html += '<div style="max-height:160px;overflow-y:auto">';
11811
+ p.models.forEach(m => {
11812
+ const isDef = defaultModel === (m.id || '');
11813
+ html += '<div style="display:flex;align-items:center;gap:8px;padding:4px 0;font-size:12px;border-bottom:1px solid var(--border)">' +
11814
+ '<span class="mono" style="flex:1">' + escHtml(m.name || m.id || '') + '</span>' +
11815
+ '<span class="mono" style="color:var(--text3);font-size:11px">' + escHtml(m.id || '') + '</span>' +
11816
+ (isDef ? '<span class="badge badge-warn" style="font-size:10px">\u2B50</span>' : '') +
11817
+ '</div>';
11818
+ });
11819
+ html += '</div>';
11820
+ modelsListEl.innerHTML = html;
11821
+ modelsListEl.style.display = '';
11822
+ } else {
11823
+ modelsListEl.innerHTML = '';
11824
+ modelsListEl.style.display = 'none';
11825
+ }
11826
+ }
10779
11827
  showModal('modal-model-config');
10780
11828
  }
10781
11829
  async function saveModelConfig() {
10782
11830
  const apiKey = $('#model-api-key').value.trim();
10783
11831
  const modelId = $('#model-model-id').value.trim();
10784
11832
  const baseUrl = $('#model-base-url').value.trim();
10785
- if (!apiKey && !modelId) { toast('Enter at least an API key or model ID', 'warn'); return; }
11833
+ if (!apiKey && !modelId) { toast(t('enter_api_key_or_model'), 'warn'); return; }
10786
11834
  hideModal('modal-model-config');
10787
- toast('Saving configuration...', 'info');
11835
+ toast(t('saving_config'), 'info');
10788
11836
  const r = await api('/models/' + encodeURIComponent(_editProviderId), { method: 'PUT', body: JSON.stringify({ apiKey, modelId, baseUrl }) });
10789
- if (r.success) { toast(r.message || 'Configuration saved!', 'ok'); loadModels(); }
10790
- else { toast(r.message || r.error || 'Failed to save', 'err'); }
11837
+ if (r.success) { toast(r.message || t('config_saved'), 'ok'); loadModels(); }
11838
+ else { toast(r.message || r.error || t('failed_save'), 'err'); }
10791
11839
  }
10792
11840
  async function deleteModelProvider(providerId) {
10793
- if (!confirm('Delete configuration for provider "' + providerId + '"? This will remove its API key and model settings.')) return;
11841
+ if (!confirm(t('confirm_delete_provider') + providerId + '"?')) return;
10794
11842
  const r = await api('/models/' + encodeURIComponent(providerId), { method: 'DELETE' });
10795
- if (r.success) { toast('Provider configuration deleted', 'ok'); loadModels(); }
10796
- else { toast(r.message || r.error || 'Delete failed', 'err'); }
11843
+ if (r.success) { toast(t('provider_deleted'), 'ok'); loadModels(); }
11844
+ else { toast(r.message || r.error || t('delete_failed'), 'err'); }
10797
11845
  }
10798
11846
 
10799
11847
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
@@ -10824,7 +11872,7 @@ function formatUptime(seconds) {
10824
11872
 
10825
11873
  async function loadSettings() {
10826
11874
  const el = $('#settings-content');
10827
- html(el, '<div class="loading-mask"><div class="spinner"></div>Loading settings...</div>');
11875
+ html(el, '<div class="loading-mask"><div class="spinner"></div>' + t('loading_settings') + '</div>');
10828
11876
 
10829
11877
  const settings = await api('/settings');
10830
11878
  if (settings.error) {
@@ -10836,11 +11884,11 @@ async function loadSettings() {
10836
11884
 
10837
11885
  // Render settings tabs nav
10838
11886
  html('#settings-tabs', \\\`
10839
- <button class="filter-btn \${_settingsTab==='connection'?'active':''}" onclick="_settingsTab='connection';renderSettingsTab()">\u{1F50C} Connection</button>
10840
- <button class="filter-btn \${_settingsTab==='friends'?'active':''}" onclick="_settingsTab='friends';renderSettingsTab()">\u{1F465} Friends</button>
10841
- <button class="filter-btn \${_settingsTab==='security'?'active':''}" onclick="_settingsTab='security';renderSettingsTab()">\u{1F512} Security</button>
10842
- <button class="filter-btn \${_settingsTab==='advanced'?'active':''}" onclick="_settingsTab='advanced';renderSettingsTab()">\u2699\uFE0F Advanced</button>
10843
- <button class="filter-btn \${_settingsTab==='json'?'active':''}" onclick="_settingsTab='json';renderSettingsTab()">\u{1F4DD} JSON Editor</button>
11887
+ <button class="filter-btn \${_settingsTab==='connection'?'active':''}" onclick="_settingsTab='connection';renderSettingsTab()">\${t('tab_connection')}</button>
11888
+ <button class="filter-btn \${_settingsTab==='friends'?'active':''}" onclick="_settingsTab='friends';renderSettingsTab()">\${t('tab_friends')}</button>
11889
+ <button class="filter-btn \${_settingsTab==='security'?'active':''}" onclick="_settingsTab='security';renderSettingsTab()">\${t('tab_security')}</button>
11890
+ <button class="filter-btn \${_settingsTab==='advanced'?'active':''}" onclick="_settingsTab='advanced';renderSettingsTab()">\${t('tab_advanced')}</button>
11891
+ <button class="filter-btn \${_settingsTab==='json'?'active':''}" onclick="_settingsTab='json';renderSettingsTab()">\${t('tab_json')}</button>
10844
11892
  \\\`);
10845
11893
 
10846
11894
  renderSettingsTab();
@@ -10849,11 +11897,11 @@ async function loadSettings() {
10849
11897
  function renderSettingsTab() {
10850
11898
  // Update tab buttons
10851
11899
  html('#settings-tabs', \\\`
10852
- <button class="filter-btn \${_settingsTab==='connection'?'active':''}" onclick="_settingsTab='connection';renderSettingsTab()">\u{1F50C} Connection</button>
10853
- <button class="filter-btn \${_settingsTab==='friends'?'active':''}" onclick="_settingsTab='friends';renderSettingsTab()">\u{1F465} Friends</button>
10854
- <button class="filter-btn \${_settingsTab==='security'?'active':''}" onclick="_settingsTab='security';renderSettingsTab()">\u{1F512} Security</button>
10855
- <button class="filter-btn \${_settingsTab==='advanced'?'active':''}" onclick="_settingsTab='advanced';renderSettingsTab()">\u2699\uFE0F Advanced</button>
10856
- <button class="filter-btn \${_settingsTab==='json'?'active':''}" onclick="_settingsTab='json';renderSettingsTab()">\u{1F4DD} JSON Editor</button>
11900
+ <button class="filter-btn \${_settingsTab==='connection'?'active':''}" onclick="_settingsTab='connection';renderSettingsTab()">\${t('tab_connection')}</button>
11901
+ <button class="filter-btn \${_settingsTab==='friends'?'active':''}" onclick="_settingsTab='friends';renderSettingsTab()">\${t('tab_friends')}</button>
11902
+ <button class="filter-btn \${_settingsTab==='security'?'active':''}" onclick="_settingsTab='security';renderSettingsTab()">\${t('tab_security')}</button>
11903
+ <button class="filter-btn \${_settingsTab==='advanced'?'active':''}" onclick="_settingsTab='advanced';renderSettingsTab()">\${t('tab_advanced')}</button>
11904
+ <button class="filter-btn \${_settingsTab==='json'?'active':''}" onclick="_settingsTab='json';renderSettingsTab()">\${t('tab_json')}</button>
10857
11905
  \\\`);
10858
11906
 
10859
11907
  switch (_settingsTab) {
@@ -10866,7 +11914,7 @@ function renderSettingsTab() {
10866
11914
  }
10867
11915
 
10868
11916
  function sectionSaveBtn(section, id) {
10869
- return \\\`<button class="btn btn-primary btn-sm" id="btn-save-\${id}" onclick="saveSettingsSection('\${section}', '\${id}')">\u{1F4BE} Save</button>
11917
+ return \\\`<button class="btn btn-primary btn-sm" id="btn-save-\${id}" onclick="saveSettingsSection('\${section}', '\${id}')">\${t('save')}</button>
10870
11918
  <span id="status-\${id}" style="font-size:12px;color:var(--text3);margin-left:8px"></span>\\\`;
10871
11919
  }
10872
11920
 
@@ -10876,51 +11924,51 @@ function renderSettingsConnection() {
10876
11924
  const el = $('#settings-content');
10877
11925
 
10878
11926
  html(el, \\\`
10879
- <p class="section-desc">Configure server connection and WebSocket settings. Changes require a plugin restart to take full effect.</p>
11927
+ <p class="section-desc">\${t('conn_desc')}</p>
10880
11928
 
10881
11929
  <div class="card">
10882
11930
  <div class="card-header">
10883
- <div class="card-title">\u{1F310} Server Connection</div>
10884
- <span class="badge badge-\${s.connected ? 'ok' : 'danger'}">\${s.connected ? '\u25CF Connected' : '\u25CB Disconnected'}</span>
11931
+ <div class="card-title">\${t('server_connection')}</div>
11932
+ <span class="badge badge-\${s.connected ? 'ok' : 'danger'}">\${s.connected ? '\u25CF ' + t('connected') : '\u25CB ' + t('disconnected')}</span>
10885
11933
  </div>
10886
11934
  <div class="form-group">
10887
- <label>Server URL</label>
11935
+ <label>\${t('server_url_label')}</label>
10888
11936
  <div style="display:flex;gap:8px;align-items:start">
10889
11937
  <div style="flex:1">
10890
11938
  <div class="input-prefix">
10891
11939
  <span class="prefix">\u{1F310}</span>
10892
11940
  <input type="url" id="set-server-url" value="\${escHtml(s.serverUrl || '')}" placeholder="https://aicq.online">
10893
11941
  </div>
10894
- <div class="hint">The HTTPS URL of the AICQ relay server. WebSocket path /ws is auto-appended.</div>
11942
+ <div class="hint">\${t('server_url_hint')}</div>
10895
11943
  </div>
10896
- <button class="btn btn-ok btn-sm" id="btn-test-conn" onclick="testConnection()" style="white-space:nowrap;margin-top:1px">\u{1F50D} Test</button>
11944
+ <button class="btn btn-ok btn-sm" id="btn-test-conn" onclick="testConnection()" style="white-space:nowrap;margin-top:1px">\u{1F50D} \${t('test')}</button>
10897
11945
  </div>
10898
11946
  <div id="conn-test-result" style="margin-top:8px"></div>
10899
11947
  </div>
10900
11948
 
10901
11949
  <div class="form-row">
10902
11950
  <div class="form-group">
10903
- <label>Connection Timeout (seconds)</label>
11951
+ <label>\${t('conn_timeout')}</label>
10904
11952
  <input type="number" id="set-connection-timeout" value="\${s.connectionTimeout || 30}" min="5" max="120" placeholder="30">
10905
- <div class="hint">HTTP request timeout (5\u2013120s). Default: 30s.</div>
11953
+ <div class="hint">\${t('conn_timeout_hint')}</div>
10906
11954
  </div>
10907
11955
  <div class="form-group">
10908
- <label>WS Auto-Reconnect</label>
11956
+ <label>\${t('ws_auto_reconnect')}</label>
10909
11957
  <div style="display:flex;align-items:center;gap:10px;margin-top:6px">
10910
11958
  <label class="toggle-label">
10911
11959
  <input type="checkbox" id="set-ws-auto-reconnect" \${s.wsAutoReconnect ? 'checked' : ''}>
10912
11960
  <span class="toggle-slider"></span>
10913
- <span>Auto-reconnect when disconnected</span>
11961
+ <span>\${t('auto_reconnect_label')}</span>
10914
11962
  </label>
10915
11963
  </div>
10916
- <div class="hint">Automatically reconnect WebSocket on disconnection.</div>
11964
+ <div class="hint">\${t('auto_reconnect_hint')}</div>
10917
11965
  </div>
10918
11966
  </div>
10919
11967
 
10920
11968
  <div class="form-group">
10921
- <label>WS Reconnect Interval (seconds)</label>
11969
+ <label>\${t('ws_reconnect_interval')}</label>
10922
11970
  <input type="number" id="set-ws-reconnect-interval" value="\${s.wsReconnectInterval || 60}" min="5" max="600" placeholder="60">
10923
- <div class="hint">Interval between reconnection attempts (5\u2013600s). Default: 60s.</div>
11971
+ <div class="hint">\${t('ws_reconnect_hint')}</div>
10924
11972
  </div>
10925
11973
 
10926
11974
  <div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
@@ -10929,11 +11977,11 @@ function renderSettingsConnection() {
10929
11977
  </div>
10930
11978
 
10931
11979
  <div class="card">
10932
- <div class="card-header"><div class="card-title">\u{1F4C1} Config File</div></div>
10933
- <div class="detail-row"><div class="detail-key">Source</div><div class="detail-val mono" style="cursor:pointer" onclick="copyText('\${escHtml(s.configPath || '')}')">\${escHtml(s.configPath || 'Not found')} \u{1F4CB}</div></div>
10934
- <div class="detail-row"><div class="detail-key">Plugin Version</div><div class="detail-val">1.1.1</div></div>
10935
- <div class="detail-row"><div class="detail-key">Management UI</div><div class="detail-val" id="mgmt-url-display" style="cursor:pointer" onclick="copyText(document.getElementById('mgmt-url-display')?.textContent || '')"></div></div>
10936
- <div class="detail-row"><div class="detail-key">Uptime</div><div class="detail-val">\${formatUptime(s.uptimeSeconds)}</div></div>
11980
+ <div class="card-header"><div class="card-title">\${t('config_file')}</div></div>
11981
+ <div class="detail-row"><div class="detail-key">\${t('source')}</div><div class="detail-val mono" style="cursor:pointer" onclick="copyText('\${escHtml(s.configPath || '')}')">\${escHtml(s.configPath || t('not_found'))} \u{1F4CB}</div></div>
11982
+ <div class="detail-row"><div class="detail-key">\${t('plugin_version')}</div><div class="detail-val">1.1.1</div></div>
11983
+ <div class="detail-row"><div class="detail-key">\${t('mgmt_ui')}</div><div class="detail-val" id="mgmt-url-display" style="cursor:pointer" onclick="copyText(document.getElementById('mgmt-url-display')?.textContent || '')"></div></div>
11984
+ <div class="detail-row"><div class="detail-key">\${t('uptime')}</div><div class="detail-val">\${formatUptime(s.uptimeSeconds)}</div></div>
10937
11985
  </div>
10938
11986
  \\\`);
10939
11987
  }
@@ -10943,37 +11991,37 @@ async function testConnection() {
10943
11991
  const resultEl = $('#conn-test-result');
10944
11992
  const url = $('#set-server-url')?.value?.trim() || _settingsData.serverUrl;
10945
11993
 
10946
- if (!url) { toast('Enter a server URL first', 'warn'); return; }
11994
+ if (!url) { toast(t('enter_server_url'), 'warn'); return; }
10947
11995
 
10948
- if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner" style="width:14px;height:14px;border-width:2px;margin:0"></span> Testing...'; }
10949
- if (resultEl) html(resultEl, '<div style="font-size:12px;color:var(--text3);display:flex;align-items:center;gap:6px"><span class="spinner" style="width:12px;height:12px;border-width:2px"></span> Testing connection to ' + escHtml(url) + '...</div>');
11996
+ if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner" style="width:14px;height:14px;border-width:2px;margin:0"></span> ' + t('testing'); }
11997
+ if (resultEl) html(resultEl, '<div style="font-size:12px;color:var(--text3);display:flex;align-items:center;gap:6px"><span class="spinner" style="width:12px;height:12px;border-width:2px"></span> ' + t('testing_conn_to') + escHtml(url) + '...</div>');
10950
11998
 
10951
11999
  const r = await api('/settings/test-connection', {
10952
12000
  method: 'POST',
10953
12001
  body: JSON.stringify({ serverUrl: url, timeout: 10000 }),
10954
12002
  });
10955
12003
 
10956
- if (btn) { btn.disabled = false; btn.innerHTML = '\u{1F50D} Test'; }
12004
+ if (btn) { btn.disabled = false; btn.innerHTML = '\u{1F50D} ' + t('test'); }
10957
12005
 
10958
12006
  if (r.success) {
10959
12007
  const latencyBadge = r.latency < 200 ? '<span class="badge badge-ok">' + r.latency + 'ms</span>' : r.latency < 1000 ? '<span class="badge badge-warn">' + r.latency + 'ms</span>' : '<span class="badge badge-danger">' + r.latency + 'ms</span>';
10960
12008
  if (resultEl) html(resultEl, \\\`
10961
12009
  <div style="display:flex;align-items:center;gap:10px;font-size:12px;color:var(--ok)">
10962
- <span class="dot dot-ok"></span> Connected successfully \${latencyBadge}
12010
+ <span class="dot dot-ok"></span> \${t('conn_ok')} \${latencyBadge}
10963
12011
  \${r.serverInfo?.version ? '<span class="tag">v' + escHtml(r.serverInfo.version) + '</span>' : ''}
10964
12012
  </div>
10965
12013
  \\\`);
10966
- toast('Connection OK! Latency: ' + r.latency + 'ms', 'ok');
12014
+ toast(t('conn_ok_latency') + r.latency + 'ms', 'ok');
10967
12015
  } else {
10968
12016
  const cls = r.status === 'timeout' ? 'warn' : 'danger';
10969
12017
  const icon = r.status === 'timeout' ? '\u23F1\uFE0F' : '\u274C';
10970
12018
  if (resultEl) html(resultEl, \\\`
10971
12019
  <div style="font-size:12px;color:var(--\${cls});display:flex;align-items:center;gap:8px">
10972
- \${icon} \${escHtml(r.message || 'Connection failed')}
12020
+ \${icon} \${escHtml(r.message || t('conn_failed'))}
10973
12021
  <span class="badge badge-ghost">\${r.latency}ms</span>
10974
12022
  </div>
10975
12023
  \\\`);
10976
- toast(r.message || 'Connection failed', 'err');
12024
+ toast(r.message || t('conn_failed'), 'err');
10977
12025
  }
10978
12026
  }
10979
12027
 
@@ -10983,58 +12031,58 @@ function renderSettingsFriends() {
10983
12031
  const el = $('#settings-content');
10984
12032
 
10985
12033
  html(el, \\\`
10986
- <p class="section-desc">Configure friend management, permissions, and temporary number settings.</p>
12034
+ <p class="section-desc">\${t('friends_tab_desc')}</p>
10987
12035
 
10988
12036
  <div class="stats-grid" style="margin-bottom:20px">
10989
12037
  <div class="stat-card">
10990
12038
  <div class="stat-icon" style="background:var(--ok-bg)">\u{1F465}</div>
10991
- <div class="stat-label">Friends</div>
12039
+ <div class="stat-label">\${t('friends')}</div>
10992
12040
  <div class="stat-value">\${s.friendCount || 0}</div>
10993
- <div class="stat-sub">of \${s.maxFriends || 200} max</div>
12041
+ <div class="stat-sub">\${t('of_max')}\${s.maxFriends || 200}</div>
10994
12042
  </div>
10995
12043
  <div class="stat-card">
10996
12044
  <div class="stat-icon" style="background:var(--info-bg)">\u{1F517}</div>
10997
- <div class="stat-label">Sessions</div>
12045
+ <div class="stat-label">\${t('sessions')}</div>
10998
12046
  <div class="stat-value">\${s.sessionCount || 0}</div>
10999
- <div class="stat-sub">Encrypted sessions</div>
12047
+ <div class="stat-sub">\${t('encrypted_sessions')}</div>
11000
12048
  </div>
11001
12049
  </div>
11002
12050
 
11003
12051
  <div class="card">
11004
- <div class="card-header"><div class="card-title">\u{1F465} Friend Limits & Permissions</div></div>
12052
+ <div class="card-header"><div class="card-title">\u{1F465} \${t('max_friends')} & \${t('permissions')}</div></div>
11005
12053
  <div class="form-row">
11006
12054
  <div class="form-group">
11007
- <label>Max Friends</label>
12055
+ <label>\${t('max_friends')}</label>
11008
12056
  <input type="number" id="set-max-friends" value="\${s.maxFriends || 200}" min="1" max="10000" placeholder="200">
11009
- <div class="hint">Maximum number of encrypted friend connections (1\u201310,000).</div>
12057
+ <div class="hint">\${t('max_friends')} (1\u201310,000)</div>
11010
12058
  </div>
11011
12059
  <div class="form-group">
11012
- <label>Auto-Accept Friends</label>
12060
+ <label>\${t('auto_accept')}</label>
11013
12061
  <div style="display:flex;align-items:center;gap:10px;margin-top:6px">
11014
12062
  <label class="toggle-label">
11015
12063
  <input type="checkbox" id="set-auto-accept" \${s.autoAcceptFriends ? 'checked' : ''}>
11016
12064
  <span class="toggle-slider"></span>
11017
- <span>Automatically accept requests</span>
12065
+ <span>\${t('auto_accept_label')}</span>
11018
12066
  </label>
11019
12067
  </div>
11020
- <div class="hint">When enabled, incoming friend requests are accepted without review.</div>
12068
+ <div class="hint">\${t('auto_accept_hint')}</div>
11021
12069
  </div>
11022
12070
  </div>
11023
12071
  <div class="form-group">
11024
- <label>Default Permissions for New Friends</label>
12072
+ <label>\${t('default_perms')}</label>
11025
12073
  <div style="display:flex;gap:16px;margin-top:6px;flex-wrap:wrap">
11026
12074
  <label class="toggle-label">
11027
12075
  <input type="checkbox" id="set-perm-chat" \${(s.defaultPermissions || []).includes('chat') ? 'checked' : ''}>
11028
12076
  <span class="toggle-slider"></span>
11029
- <span>\u{1F4AC} Chat</span>
12077
+ <span>\${t('chat_perm')}</span>
11030
12078
  </label>
11031
12079
  <label class="toggle-label">
11032
12080
  <input type="checkbox" id="set-perm-exec" \${(s.defaultPermissions || []).includes('exec') ? 'checked' : ''}>
11033
12081
  <span class="toggle-slider"></span>
11034
- <span>\u{1F527} Exec</span>
12082
+ <span>\${t('exec_perm')}</span>
11035
12083
  </label>
11036
12084
  </div>
11037
- <div class="hint">Default permissions applied when auto-accepting new friend requests.</div>
12085
+ <div class="hint">\${t('default_perms_hint')}</div>
11038
12086
  </div>
11039
12087
  <div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
11040
12088
  \${sectionSaveBtn('friends', 'friends')}
@@ -11042,11 +12090,11 @@ function renderSettingsFriends() {
11042
12090
  </div>
11043
12091
 
11044
12092
  <div class="card">
11045
- <div class="card-header"><div class="card-title">\u{1F522} Temporary Numbers</div></div>
12093
+ <div class="card-header"><div class="card-title">\${t('temp_numbers')}</div></div>
11046
12094
  <div class="form-group">
11047
- <label>Temp Number Expiry (seconds)</label>
12095
+ <label>\${t('temp_expiry')}</label>
11048
12096
  <input type="number" id="set-temp-expiry" value="\${s.tempNumberExpiry || 300}" min="60" max="3600" placeholder="300">
11049
- <div class="hint">How long a temporary friend number remains valid (60\u20133600s). Default: 5 minutes.</div>
12097
+ <div class="hint">\${t('temp_expiry_hint')}</div>
11050
12098
  </div>
11051
12099
  <div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
11052
12100
  \${sectionSaveBtn('temp', 'temp')}
@@ -11061,36 +12109,36 @@ function renderSettingsSecurity() {
11061
12109
  const el = $('#settings-content');
11062
12110
 
11063
12111
  html(el, \\\`
11064
- <p class="section-desc">Configure encryption, P2P, and identity security settings.</p>
12112
+ <p class="section-desc">\${t('sec_desc')}</p>
11065
12113
 
11066
12114
  <div class="card">
11067
- <div class="card-header"><div class="card-title">\u{1F916} Agent Identity</div></div>
11068
- <div class="detail-row"><div class="detail-key">Agent ID</div><div class="detail-val mono" style="cursor:pointer" onclick="copyText('\${escHtml(s.agentId)}')">\${escHtml(s.agentId)} \u{1F4CB}</div></div>
11069
- <div class="detail-row"><div class="detail-key">Public Key Fingerprint</div><div class="detail-val mono">\${escHtml(s.publicKeyFingerprint || '\u2014')}</div></div>
12115
+ <div class="card-header"><div class="card-title">\${t('agent_identity')}</div></div>
12116
+ <div class="detail-row"><div class="detail-key">\${t('agent_id')}</div><div class="detail-val mono" style="cursor:pointer" onclick="copyText('\${escHtml(s.agentId)}')">\${escHtml(s.agentId)} \u{1F4CB}</div></div>
12117
+ <div class="detail-row"><div class="detail-key">\${t('public_key_fp')}</div><div class="detail-val mono">\${escHtml(s.publicKeyFingerprint || '\u2014')}</div></div>
11070
12118
  <div style="padding-top:12px;display:flex;gap:8px">
11071
- <button class="btn btn-danger btn-sm" onclick="showResetIdentityModal()">\u{1F5D1}\uFE0F Reset Identity</button>
11072
- <span style="font-size:12px;color:var(--text3);display:flex;align-items:center">\u26A0\uFE0F This deletes all friends, sessions, and keys permanently</span>
12119
+ <button class="btn btn-danger btn-sm" onclick="showResetIdentityModal()">\${t('reset_identity')}</button>
12120
+ <span style="font-size:12px;color:var(--text3);display:flex;align-items:center">\${t('reset_identity_warn')}</span>
11073
12121
  </div>
11074
12122
  </div>
11075
12123
 
11076
12124
  <div class="card">
11077
- <div class="card-header"><div class="card-title">\u{1F512} P2P & Encryption</div></div>
12125
+ <div class="card-header"><div class="card-title">\${t('p2p_encryption')}</div></div>
11078
12126
  <div class="form-row">
11079
12127
  <div class="form-group">
11080
- <label>Enable P2P Connections</label>
12128
+ <label>\${t('enable_p2p')}</label>
11081
12129
  <div style="display:flex;align-items:center;gap:10px;margin-top:6px">
11082
12130
  <label class="toggle-label">
11083
12131
  <input type="checkbox" id="set-enable-p2p" \${s.enableP2P ? 'checked' : ''}>
11084
12132
  <span class="toggle-slider"></span>
11085
- <span>Allow direct P2P messaging</span>
12133
+ <span>\${t('allow_p2p')}</span>
11086
12134
  </label>
11087
12135
  </div>
11088
- <div class="hint">Enable peer-to-peer encrypted connections when both parties are online.</div>
12136
+ <div class="hint">\${t('enable_p2p_hint')}</div>
11089
12137
  </div>
11090
12138
  <div class="form-group">
11091
- <label>Handshake Timeout (seconds)</label>
12139
+ <label>\${t('hs_timeout')}</label>
11092
12140
  <input type="number" id="set-handshake-timeout" value="\${s.handshakeTimeout || 60}" min="10" max="300" placeholder="60">
11093
- <div class="hint">Noise-XK handshake timeout (10\u2013300s). Default: 60s.</div>
12141
+ <div class="hint">\${t('hs_timeout_hint')}</div>
11094
12142
  </div>
11095
12143
  </div>
11096
12144
  <div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
@@ -11106,24 +12154,24 @@ function renderSettingsAdvanced() {
11106
12154
  const el = $('#settings-content');
11107
12155
 
11108
12156
  html(el, \\\`
11109
- <p class="section-desc">Advanced settings for file transfer, logging, and configuration management.</p>
12157
+ <p class="section-desc">\${t('adv_desc')}</p>
11110
12158
 
11111
12159
  <div class="card">
11112
- <div class="card-header"><div class="card-title">\u{1F4CE} File Transfer</div></div>
12160
+ <div class="card-header"><div class="card-title">\${t('file_transfer')}</div></div>
11113
12161
  <div class="form-row">
11114
12162
  <div class="form-group">
11115
- <label>Enable File Transfer</label>
12163
+ <label>\${t('enable_ft')}</label>
11116
12164
  <div style="display:flex;align-items:center;gap:10px;margin-top:6px">
11117
12165
  <label class="toggle-label">
11118
12166
  <input type="checkbox" id="set-enable-ft" \${s.enableFileTransfer ? 'checked' : ''}>
11119
12167
  <span class="toggle-slider"></span>
11120
- <span>Allow file transfers</span>
12168
+ <span>\${t('allow_ft')}</span>
11121
12169
  </label>
11122
12170
  </div>
11123
- <div class="hint">Enable encrypted file transfer between friends.</div>
12171
+ <div class="hint">\${t('enable_ft_hint')}</div>
11124
12172
  </div>
11125
12173
  <div class="form-group">
11126
- <label>Max File Size</label>
12174
+ <label>\${t('max_file_size')}</label>
11127
12175
  <select id="set-max-file-size">
11128
12176
  <option value="10485760" \${s.maxFileSize <= 10485760 ? 'selected' : ''}>10 MB</option>
11129
12177
  <option value="52428800" \${s.maxFileSize > 10485760 && s.maxFileSize <= 52428800 ? 'selected' : ''}>50 MB</option>
@@ -11131,7 +12179,7 @@ function renderSettingsAdvanced() {
11131
12179
  <option value="524288000" \${s.maxFileSize > 104857600 && s.maxFileSize <= 524288000 ? 'selected' : ''}>500 MB</option>
11132
12180
  <option value="1073741824" \${s.maxFileSize > 524288000 ? 'selected' : ''}>1 GB</option>
11133
12181
  </select>
11134
- <div class="hint">Maximum file size for encrypted transfers. Current: \${formatBytes(s.maxFileSize)}.</div>
12182
+ <div class="hint">\${t('max_file_size_hint')}\${formatBytes(s.maxFileSize)}.</div>
11135
12183
  </div>
11136
12184
  </div>
11137
12185
  <div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
@@ -11140,17 +12188,17 @@ function renderSettingsAdvanced() {
11140
12188
  </div>
11141
12189
 
11142
12190
  <div class="card">
11143
- <div class="card-header"><div class="card-title">\u{1F4CB} Logging</div></div>
12191
+ <div class="card-header"><div class="card-title">\${t('logging')}</div></div>
11144
12192
  <div class="form-group">
11145
- <label>Log Level</label>
12193
+ <label>\${t('log_level')}</label>
11146
12194
  <select id="set-log-level" style="max-width:300px">
11147
- <option value="debug" \${s.logLevel === 'debug' ? 'selected' : ''}>\u{1F41B} Debug \u2014 Verbose output for troubleshooting</option>
11148
- <option value="info" \${s.logLevel === 'info' ? 'selected' : ''}>\u2139\uFE0F Info \u2014 General information (default)</option>
11149
- <option value="warn" \${s.logLevel === 'warn' ? 'selected' : ''}>\u26A0\uFE0F Warn \u2014 Warnings and important events</option>
11150
- <option value="error" \${s.logLevel === 'error' ? 'selected' : ''}>\u274C Error \u2014 Errors only</option>
11151
- <option value="none" \${s.logLevel === 'none' ? 'selected' : ''}>\u{1F507} None \u2014 Disable all logging</option>
12195
+ <option value="debug" \${s.logLevel === 'debug' ? 'selected' : ''}>\${t('log_debug')}</option>
12196
+ <option value="info" \${s.logLevel === 'info' ? 'selected' : ''}>\${t('log_info')}</option>
12197
+ <option value="warn" \${s.logLevel === 'warn' ? 'selected' : ''}>\${t('log_warn')}</option>
12198
+ <option value="error" \${s.logLevel === 'error' ? 'selected' : ''}>\${t('log_error')}</option>
12199
+ <option value="none" \${s.logLevel === 'none' ? 'selected' : ''}>\${t('log_none')}</option>
11152
12200
  </select>
11153
- <div class="hint">Controls the verbosity of plugin log output.</div>
12201
+ <div class="hint">\${t('log_level_hint')}</div>
11154
12202
  </div>
11155
12203
  <div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
11156
12204
  \${sectionSaveBtn('logging', 'log')}
@@ -11158,12 +12206,12 @@ function renderSettingsAdvanced() {
11158
12206
  </div>
11159
12207
 
11160
12208
  <div class="card">
11161
- <div class="card-header"><div class="card-title">\u{1F4E6} Import / Export Settings</div></div>
12209
+ <div class="card-header"><div class="card-title">\${t('import_export')}</div></div>
11162
12210
  <div style="display:flex;gap:10px;flex-wrap:wrap">
11163
- <button class="btn btn-default btn-sm" onclick="exportSettings()">\u{1F4E5} Export Settings</button>
11164
- <button class="btn btn-ok btn-sm" onclick="showImportSettingsModal()">\u{1F4E4} Import Settings</button>
12211
+ <button class="btn btn-default btn-sm" onclick="exportSettings()">\${t('export_settings')}</button>
12212
+ <button class="btn btn-ok btn-sm" onclick="showImportSettingsModal()">\${t('import_settings')}</button>
11165
12213
  </div>
11166
- <div class="hint" style="margin-top:10px">Export current AICQ plugin settings as JSON. Import to restore settings from a backup.</div>
12214
+ <div class="hint" style="margin-top:10px">\${t('import_export_hint')}</div>
11167
12215
  </div>
11168
12216
  \\\`);
11169
12217
  }
@@ -11172,7 +12220,7 @@ function renderSettingsAdvanced() {
11172
12220
  async function saveSettingsSection(section, id) {
11173
12221
  const btn = $('#btn-save-' + id);
11174
12222
  const statusEl = $('#status-' + id);
11175
- if (btn) { btn.disabled = true; btn.textContent = 'Saving...'; }
12223
+ if (btn) { btn.disabled = true; btn.textContent = t('saving'); }
11176
12224
  if (statusEl) { statusEl.textContent = ''; statusEl.style.color = 'var(--text3)'; }
11177
12225
 
11178
12226
  let data = {};
@@ -11220,17 +12268,17 @@ async function saveSettingsSection(section, id) {
11220
12268
  body: JSON.stringify({ section, data }),
11221
12269
  });
11222
12270
 
11223
- if (btn) { btn.disabled = false; btn.textContent = '\u{1F4BE} Save'; }
12271
+ if (btn) { btn.disabled = false; btn.textContent = t('save'); }
11224
12272
 
11225
12273
  if (r.success) {
11226
- toast('Settings saved: ' + section, 'ok');
11227
- if (statusEl) { statusEl.textContent = '\u2713 Saved'; statusEl.style.color = 'var(--ok)'; }
12274
+ toast(t('settings_saved') + section, 'ok');
12275
+ if (statusEl) { statusEl.textContent = t('saved'); statusEl.style.color = 'var(--ok)'; }
11228
12276
  // Refresh settings data
11229
12277
  const fresh = await api('/settings');
11230
12278
  if (fresh && !fresh.error) { _settingsData = fresh; }
11231
12279
  } else {
11232
- toast(r.message || r.error || 'Save failed', 'err');
11233
- if (statusEl) { statusEl.textContent = '\u2717 ' + (r.message || 'Failed'); statusEl.style.color = 'var(--danger)'; }
12280
+ toast(r.message || r.error || t('failed_save'), 'err');
12281
+ if (statusEl) { statusEl.textContent = '\u2717 ' + (r.message || t('failed')); statusEl.style.color = 'var(--danger)'; }
11234
12282
  }
11235
12283
  }
11236
12284
 
@@ -11248,15 +12296,15 @@ async function saveSettings() {
11248
12296
  const r = await api('/settings', { method: 'PUT', body: JSON.stringify(allData) });
11249
12297
  _settingsSaving = false;
11250
12298
 
11251
- if (r.success) { toast('All settings saved!', 'ok'); setTimeout(() => loadSettings(), 800); }
11252
- else { toast(r.message || r.error || 'Save failed', 'err'); }
12299
+ if (r.success) { toast(t('all_saved'), 'ok'); setTimeout(() => loadSettings(), 800); }
12300
+ else { toast(r.message || r.error || t('failed_save'), 'err'); }
11253
12301
  }
11254
12302
 
11255
12303
  // \u2500\u2500 Reset Identity \u2500\u2500
11256
12304
  function showResetIdentityModal() {
11257
12305
  $('#reset-confirm-input').value = '';
11258
12306
  $('#reset-confirm-btn').disabled = true;
11259
- $('#reset-confirm-btn').textContent = '\u{1F5D1}\uFE0F Delete Everything';
12307
+ $('#reset-confirm-btn').textContent = t('delete_everything');
11260
12308
  showModal('modal-reset-identity');
11261
12309
  setTimeout(() => $('#reset-confirm-input')?.focus(), 100);
11262
12310
  }
@@ -11264,27 +12312,27 @@ function showResetIdentityModal() {
11264
12312
  function checkResetConfirm() {
11265
12313
  const v = $('#reset-confirm-input')?.value?.trim();
11266
12314
  const btn = $('#reset-confirm-btn');
11267
- if (btn) { btn.disabled = (v !== 'RESET'); btn.textContent = v === 'RESET' ? '\u{1F5D1}\uFE0F Confirm Delete' : '\u{1F5D1}\uFE0F Delete Everything'; }
12315
+ if (btn) { btn.disabled = (v !== 'RESET'); btn.textContent = v === 'RESET' ? t('confirm_delete') : t('delete_everything'); }
11268
12316
  }
11269
12317
 
11270
12318
  async function executeResetIdentity() {
11271
12319
  const btn = $('#reset-confirm-btn');
11272
- if (btn) { btn.disabled = true; btn.textContent = 'Resetting...'; }
12320
+ if (btn) { btn.disabled = true; btn.textContent = t('resetting'); }
11273
12321
 
11274
12322
  const r = await api('/settings/reset-identity', {
11275
12323
  method: 'POST',
11276
12324
  body: JSON.stringify({ confirm: true }),
11277
12325
  });
11278
12326
 
11279
- if (btn) { btn.disabled = false; btn.textContent = '\u{1F5D1}\uFE0F Delete Everything'; }
12327
+ if (btn) { btn.disabled = false; btn.textContent = t('delete_everything'); }
11280
12328
 
11281
12329
  if (r.success) {
11282
- toast('Identity reset successfully. Please restart the plugin.', 'ok');
12330
+ toast(t('reset_success'), 'ok');
11283
12331
  hideModal('modal-reset-identity');
11284
12332
  // Reload settings to reflect cleared state
11285
12333
  setTimeout(() => loadSettings(), 1000);
11286
12334
  } else {
11287
- toast(r.message || r.error || 'Reset failed', 'err');
12335
+ toast(r.message || r.error || t('reset_failed'), 'err');
11288
12336
  }
11289
12337
  }
11290
12338
 
@@ -11301,7 +12349,7 @@ async function exportSettings() {
11301
12349
  a.download = 'aicq-settings-' + new Date().toISOString().slice(0, 10) + '.json';
11302
12350
  a.click();
11303
12351
  URL.revokeObjectURL(url);
11304
- toast('Settings exported successfully', 'ok');
12352
+ toast(t('exported_success'), 'ok');
11305
12353
  }
11306
12354
 
11307
12355
  function showImportSettingsModal() {
@@ -11312,27 +12360,27 @@ function showImportSettingsModal() {
11312
12360
 
11313
12361
  async function executeImportSettings() {
11314
12362
  const raw = $('#import-json-input')?.value?.trim();
11315
- if (!raw) { toast('Paste JSON settings first', 'warn'); return; }
12363
+ if (!raw) { toast(t('paste_json'), 'warn'); return; }
11316
12364
 
11317
12365
  let settings;
11318
- try { settings = JSON.parse(raw); } catch (e) { toast('Invalid JSON: ' + e.message, 'err'); return; }
12366
+ try { settings = JSON.parse(raw); } catch (e) { toast(t('invalid_json') + e.message, 'err'); return; }
11319
12367
 
11320
12368
  const btn = $('#import-confirm-btn');
11321
- if (btn) { btn.disabled = true; btn.textContent = 'Importing...'; }
12369
+ if (btn) { btn.disabled = true; btn.textContent = t('importing'); }
11322
12370
 
11323
12371
  const r = await api('/settings/import', {
11324
12372
  method: 'POST',
11325
12373
  body: JSON.stringify({ settings, merge: true }),
11326
12374
  });
11327
12375
 
11328
- if (btn) { btn.disabled = false; btn.textContent = '\u{1F4E4} Import'; }
12376
+ if (btn) { btn.disabled = false; btn.textContent = t('import_settings'); }
11329
12377
 
11330
12378
  if (r.success) {
11331
- toast('Settings imported successfully!', 'ok');
12379
+ toast(t('imported_success'), 'ok');
11332
12380
  hideModal('modal-import-settings');
11333
12381
  setTimeout(() => loadSettings(), 800);
11334
12382
  } else {
11335
- toast(r.message || r.error || 'Import failed', 'err');
12383
+ toast(r.message || r.error || t('import_failed'), 'err');
11336
12384
  }
11337
12385
  }
11338
12386
 
@@ -11343,7 +12391,7 @@ let _jsonEditorConfigFile = '';
11343
12391
 
11344
12392
  async function renderSettingsJsonEditor() {
11345
12393
  const el = $('#settings-content');
11346
- html(el, '<div class="loading-mask"><div class="spinner"></div>Loading config...</div>');
12394
+ html(el, '<div class="loading-mask"><div class="spinner"></div>' + t('loading_config') + '</div>');
11347
12395
 
11348
12396
  const queryParams = _jsonEditorConfigFile ? '?file=' + encodeURIComponent(_jsonEditorConfigFile) : '';
11349
12397
  const data = await api('/config-file/raw' + queryParams);
@@ -11375,7 +12423,7 @@ async function renderSettingsJsonEditor() {
11375
12423
 
11376
12424
  <div class="card">
11377
12425
  <div class="card-header">
11378
- <div class="card-title">\u{1F4DD} Config JSON Editor</div>
12426
+ <div class="card-title">\${t('json_editor')}</div>
11379
12427
  <div style="display:flex;gap:8px;align-items:center">
11380
12428
  <span class="mono" style="font-size:11px;color:var(--text3)">\${escHtml(data.filePath)}</span>
11381
12429
  <button class="btn btn-sm btn-default" onclick="renderSettingsJsonEditor()">\u{1F504} Reload</button>
@@ -11383,19 +12431,19 @@ async function renderSettingsJsonEditor() {
11383
12431
  </div>
11384
12432
  \${fileSelectorHtml}
11385
12433
  <div class="form-group">
11386
- <label>Raw JSON Configuration</label>
12434
+ <label>\${t('raw_json')}</label>
11387
12435
  <textarea id="json-editor" style="min-height:400px;font-family:'SF Mono','Fira Code','Cascadia Code',monospace;font-size:12px;line-height:1.5;tab-size:2;background:var(--bg)" spellcheck="false">\${escHtml(data.content)}</textarea>
11388
- <div class="hint">Directly edit the configuration JSON. Use the Format button to prettify.</div>
12436
+ <div class="hint">\${t('raw_json_hint')}</div>
11389
12437
  </div>
11390
12438
  <div id="json-editor-status" style="margin-bottom:12px;font-size:12px"></div>
11391
12439
  <div class="form-actions" style="justify-content:space-between">
11392
12440
  <div style="display:flex;gap:8px">
11393
- <button class="btn btn-sm btn-default" onclick="formatJsonEditor()">\u{1F4D0} Format</button>
11394
- <button class="btn btn-sm btn-default" onclick="copyText($('#json-editor')?.value || '')">\u{1F4CB} Copy</button>
12441
+ <button class="btn btn-sm btn-default" onclick="formatJsonEditor()">\${t('format')}</button>
12442
+ <button class="btn btn-sm btn-default" onclick="copyText($('#json-editor')?.value || '')">\${t('copy')}</button>
11395
12443
  </div>
11396
12444
  <div style="display:flex;gap:8px">
11397
- <button class="btn btn-sm btn-default" onclick="renderSettingsJsonEditor()">\u21A9\uFE0F Revert</button>
11398
- <button class="btn btn-sm btn-primary" id="btn-save-json" onclick="saveJsonConfig()">\u{1F4BE} Save Config</button>
12445
+ <button class="btn btn-sm btn-default" onclick="renderSettingsJsonEditor()">\${t('revert')}</button>
12446
+ <button class="btn btn-sm btn-primary" id="btn-save-json" onclick="saveJsonConfig()">\${t('save_config')}</button>
11399
12447
  </div>
11400
12448
  </div>
11401
12449
  </div>
@@ -11408,10 +12456,10 @@ function formatJsonEditor() {
11408
12456
  try {
11409
12457
  const obj = JSON.parse(ta.value);
11410
12458
  ta.value = JSON.stringify(obj, null, 2);
11411
- toast('JSON formatted', 'ok');
11412
- $('#json-editor-status').innerHTML = '<span style="color:var(--ok)">\u2713 Valid JSON</span>';
12459
+ toast(t('json_formatted'), 'ok');
12460
+ $('#json-editor-status').innerHTML = '<span style="color:var(--ok)">' + t('valid_json') + '</span>';
11413
12461
  } catch (e) {
11414
- toast('Invalid JSON: ' + e.message, 'err');
12462
+ toast(t('invalid_json') + e.message, 'err');
11415
12463
  $('#json-editor-status').innerHTML = '<span style="color:var(--danger)">\u2717 ' + escHtml(e.message) + '</span>';
11416
12464
  }
11417
12465
  }
@@ -11420,29 +12468,439 @@ async function saveJsonConfig() {
11420
12468
  const btn = $('#btn-save-json');
11421
12469
  const statusEl = $('#json-editor-status');
11422
12470
  const raw = $('#json-editor')?.value;
11423
- if (!raw) { toast('No content to save', 'warn'); return; }
12471
+ if (!raw) { toast(t('no_content'), 'warn'); return; }
11424
12472
 
11425
12473
  // Validate first
11426
12474
  try { JSON.parse(raw); } catch (e) {
11427
- toast('Invalid JSON: ' + e.message, 'err');
12475
+ toast(t('invalid_json') + e.message, 'err');
11428
12476
  if (statusEl) statusEl.innerHTML = '<span style="color:var(--danger)">\u2717 ' + escHtml(e.message) + '</span>';
11429
12477
  return;
11430
12478
  }
11431
12479
 
11432
- if (btn) { btn.disabled = true; btn.textContent = 'Saving...'; }
11433
- if (statusEl) statusEl.innerHTML = '<span style="color:var(--text3)"><span class="spinner" style="width:12px;height:12px;border-width:2px;display:inline-block;vertical-align:middle;margin-right:6px"></span> Saving...</span>';
12480
+ if (btn) { btn.disabled = true; btn.textContent = t('saving'); }
12481
+ if (statusEl) statusEl.innerHTML = '<span style="color:var(--text3)"><span class="spinner" style="width:12px;height:12px;border-width:2px;display:inline-block;vertical-align:middle;margin-right:6px"></span> ' + t('saving') + '</span>';
11434
12482
 
11435
12483
  const queryParams = _jsonEditorConfigFile ? '?file=' + encodeURIComponent(_jsonEditorConfigFile) : '';
11436
12484
  const r = await api('/config-file/raw' + queryParams, { method: 'PUT', body: JSON.stringify({ content: raw }) });
11437
12485
 
11438
- if (btn) { btn.disabled = false; btn.textContent = '\u{1F4BE} Save Config'; }
12486
+ if (btn) { btn.disabled = false; btn.textContent = t('save_config'); }
11439
12487
 
11440
12488
  if (r.success) {
11441
- toast('Config saved successfully!', 'ok');
12489
+ toast(t('config_saved'), 'ok');
11442
12490
  if (statusEl) statusEl.innerHTML = '<span style="color:var(--ok)">\u2713 Saved at ' + new Date().toLocaleTimeString() + '</span>';
11443
12491
  } else {
11444
- toast(r.message || 'Save failed', 'err');
11445
- if (statusEl) statusEl.innerHTML = '<span style="color:var(--danger)">\u2717 ' + escHtml(r.message || 'Failed') + '</span>';
12492
+ toast(r.message || t('failed_save'), 'err');
12493
+ if (statusEl) statusEl.innerHTML = '<span style="color:var(--danger)">\u2717 ' + escHtml(r.message || t('failed')) + '</span>';
12494
+ }
12495
+ }
12496
+
12497
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
12498
+ // PAGE: OpenClaw Config
12499
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
12500
+ let _ocConfig = null;
12501
+ let _ocTab = 'defaults';
12502
+
12503
+ async function loadOpenClawConfig() {
12504
+ const el = $('#openclaw-content');
12505
+ html(el, '<div class="loading-mask"><div class="spinner"></div>' + t('loading_openclaw') + '</div>');
12506
+ try {
12507
+ const data = await api('/openclaw-config');
12508
+ if (data.error) { html(el, '<div class="card"><p style="color:var(--danger)">' + escHtml(data.error) + '</p></div>'); return; }
12509
+ _ocConfig = data;
12510
+ _ocTab = 'defaults';
12511
+ renderOpenClawConfig(data);
12512
+ } catch (e) {
12513
+ html(el, '<div class="card"><p style="color:var(--danger)">' + escHtml(e.message || 'Failed to load') + '</p></div>');
12514
+ }
12515
+ }
12516
+
12517
+ function switchOpenClawTab(tab) {
12518
+ _ocTab = tab;
12519
+ $$('.oc-tab').forEach(b => b.classList.toggle('active', b.dataset.ocTab === tab));
12520
+ $$('.oc-panel').forEach(p => { p.style.display = p.id === 'oc-panel-' + tab ? '' : 'none'; });
12521
+ }
12522
+
12523
+ function renderOpenClawConfig(data) {
12524
+ const el = $('#openclaw-content');
12525
+ const tabBtns = [
12526
+ { id: 'defaults', label: t('tab_defaults') },
12527
+ { id: 'agents', label: t('tab_agents') },
12528
+ { id: 'bindings', label: t('tab_bindings') },
12529
+ { id: 'channels', label: t('tab_channels') },
12530
+ ];
12531
+ let tabsHtml = '<div style="display:flex;gap:6px;margin-bottom:20px;flex-wrap:wrap">';
12532
+ for (const tb of tabBtns) {
12533
+ tabsHtml += '<button class="btn btn-sm filter-btn oc-tab' + (tb.id === _ocTab ? ' active' : '') + '" data-oc-tab="' + tb.id + '" onclick="switchOpenClawTab(\\'' + tb.id + '\\')">' + tb.label + '</button>';
12534
+ }
12535
+ tabsHtml += '</div>';
12536
+
12537
+ let panelsHtml = '<div id="oc-panel-defaults" class="oc-panel"' + (_ocTab !== 'defaults' ? ' style="display:none"' : '') + '>' + renderOcDefaults(data.agents) + '</div>';
12538
+ panelsHtml += '<div id="oc-panel-agents" class="oc-panel"' + (_ocTab !== 'agents' ? ' style="display:none"' : '') + '>' + renderOcAgentList(data.agents) + '</div>';
12539
+ panelsHtml += '<div id="oc-panel-bindings" class="oc-panel"' + (_ocTab !== 'bindings' ? ' style="display:none"' : '') + '>' + renderOcBindings(data.bindings) + '</div>';
12540
+ panelsHtml += '<div id="oc-panel-channels" class="oc-panel"' + (_ocTab !== 'channels' ? ' style="display:none"' : '') + '>' + renderOcChannels(data.channels) + '</div>';
12541
+
12542
+ html(el, '<div class="section-desc">' + t('openclaw_desc') + '</div>' +
12543
+ '<div class="card"><div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;flex-wrap:wrap;gap:8px"><div class="card-title">' + t('openclaw_config') + '</div>' +
12544
+ '<button class="btn btn-primary btn-sm" onclick="saveOpenClawConfig()">' + t('save_openclaw_config') + '</button></div>' +
12545
+ tabsHtml + panelsHtml + '</div>');
12546
+ }
12547
+
12548
+ function renderOcDefaults(agents) {
12549
+ const d = agents?.defaults || {};
12550
+ const model = d.model || {};
12551
+ const imgModel = d.imageModel || {};
12552
+ const subagents = d.subagents || {};
12553
+ const modelsRegistry = d.models || {};
12554
+
12555
+ let fallbacksHtml = '';
12556
+ const fallbacks = model.fallbacks || [];
12557
+ for (let i = 0; i < fallbacks.length; i++) {
12558
+ fallbacksHtml += '<div style="display:flex;gap:6px;align-items:center;margin-bottom:6px"><input type="text" class="oc-fallback-item" value="' + escHtml(fallbacks[i]) + '" style="flex:1" placeholder="model-id"><button class="btn btn-sm btn-danger" onclick="this.parentElement.remove()">\u2715</button></div>';
12559
+ }
12560
+ fallbacksHtml += '<button class="btn btn-sm btn-default" onclick="addOcListItem(this,\\'oc-fallback-item\\')">\u2795 ' + t('add_item') + '</button>';
12561
+
12562
+ let imgFallbacksHtml = '';
12563
+ const imgFallbacks = imgModel.fallbacks || [];
12564
+ for (let i = 0; i < imgFallbacks.length; i++) {
12565
+ imgFallbacksHtml += '<div style="display:flex;gap:6px;align-items:center;margin-bottom:6px"><input type="text" class="oc-img-fallback-item" value="' + escHtml(imgFallbacks[i]) + '" style="flex:1" placeholder="model-id"><button class="btn btn-sm btn-danger" onclick="this.parentElement.remove()">\u2715</button></div>';
12566
+ }
12567
+ imgFallbacksHtml += '<button class="btn btn-sm btn-default" onclick="addOcListItem(this,\\'oc-img-fallback-item\\')">\u2795 ' + t('add_item') + '</button>';
12568
+
12569
+ let modelsRegHtml = '';
12570
+ const modelKeys = Object.keys(modelsRegistry);
12571
+ for (let i = 0; i < modelKeys.length; i++) {
12572
+ const k = modelKeys[i];
12573
+ modelsRegHtml += '<div style="display:flex;gap:6px;align-items:center;margin-bottom:6px"><input type="text" class="oc-model-reg-key" value="' + escHtml(k) + '" style="width:45%" placeholder="model-id"><span style="color:var(--text3)">\u2192</span><input type="text" class="oc-model-reg-val" value="" style="flex:1" placeholder="{}" disabled><button class="btn btn-sm btn-danger" onclick="this.parentElement.remove()">\u2715</button></div>';
12574
+ }
12575
+ modelsRegHtml += '<button class="btn btn-sm btn-default" onclick="addOcModelRegItem(this)">\u2795 ' + t('add_item') + '</button>';
12576
+
12577
+ return '<div class="card" style="margin-bottom:16px"><div class="card-title">' + t('agent_defaults') + '</div><div class="card-desc">' + t('agent_defaults_desc') + '</div>' +
12578
+ '<div class="form-group"><label>' + t('compaction_mode') + '</label><input type="text" id="oc-compaction-mode" value="' + escHtml((d.compaction?.mode) || '') + '" placeholder="safeguard"></div>' +
12579
+ '<div class="form-row">' +
12580
+ '<div class="form-group"><label>' + t('primary_model') + '</label><input type="text" id="oc-primary-model" value="' + escHtml(model.primary || '') + '" placeholder="zerotoken/glm-4-flash"></div>' +
12581
+ '<div class="form-group"><label>' + t('max_concurrent') + '</label><input type="number" id="oc-max-concurrent" value="' + (d.maxConcurrent || '') + '" min="1" max="32"></div>' +
12582
+ '</div>' +
12583
+ '<div class="form-group"><label>' + t('fallback_models') + '</label><div id="oc-fallbacks-list">' + fallbacksHtml + '</div></div>' +
12584
+ '<div class="form-row">' +
12585
+ '<div class="form-group"><label>' + t('image_model') + ' (Primary)</label><input type="text" id="oc-img-model-primary" value="' + escHtml(imgModel.primary || '') + '" placeholder="modelscope/ZhipuAI/GLM-5"></div>' +
12586
+ '<div class="form-group"><label>' + t('subagent_max_concurrent') + '</label><input type="number" id="oc-subagent-max" value="' + (subagents.maxConcurrent || '') + '" min="1" max="32"></div>' +
12587
+ '</div>' +
12588
+ '<div class="form-group"><label>' + t('image_model') + ' (Fallbacks)</label><div id="oc-img-fallbacks-list">' + imgFallbacksHtml + '</div></div>' +
12589
+ '<div class="form-row">' +
12590
+ '<div class="form-group"><label>' + t('thinking_default') + '</label><select id="oc-thinking-default"><option value="off"' + (d.thinkingDefault === 'off' || !d.thinkingDefault ? ' selected' : '') + '>off</option><option value="low"' + (d.thinkingDefault === 'low' ? ' selected' : '') + '>low</option><option value="medium"' + (d.thinkingDefault === 'medium' ? ' selected' : '') + '>medium</option><option value="high"' + (d.thinkingDefault === 'high' ? ' selected' : '') + '>high</option></select></div>' +
12591
+ '<div class="form-group"><label>' + t('workspace') + '</label><input type="text" id="oc-workspace" value="' + escHtml(d.workspace || '') + '" placeholder="~/.openclaw/workspace"></div>' +
12592
+ '</div>' +
12593
+ '<div class="form-group"><label>' + t('models_registry') + '</label><div id="oc-models-reg-list">' + modelsRegHtml + '</div></div>' +
12594
+ '</div>';
12595
+ }
12596
+
12597
+ function renderOcAgentList(agents) {
12598
+ const list = agents?.list || [];
12599
+ let tableHtml = '';
12600
+ if (list.length === 0) {
12601
+ tableHtml = '<div class="empty"><div class="icon">\u{1F916}</div><p>' + t('no_agents_in_list') + '</p></div>';
12602
+ } else {
12603
+ tableHtml = '<table><thead><tr><th>ID</th><th>' + t('agent_identity_name') + '</th><th>' + t('agent_model_primary') + '</th><th>' + t('agent_tools_profile') + '</th><th>' + t('actions') + '</th></tr></thead><tbody>';
12604
+ for (let i = 0; i < list.length; i++) {
12605
+ const a = list[i];
12606
+ tableHtml += '<tr><td class="mono">' + escHtml(a.id || '') + '</td><td>' + escHtml(a.identity?.name || '') + '</td><td class="mono" style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml(a.model?.primary || '') + '</td><td><span class="badge badge-ghost">' + escHtml(a.tools?.profile || '') + '</span></td>' +
12607
+ '<td class="actions-cell"><button class="btn btn-sm btn-default" onclick="editOcAgent(' + i + ')">\u270F\uFE0F</button><button class="btn btn-sm btn-danger" onclick="deleteOcAgent(' + i + ')">\u{1F5D1}\uFE0F</button></td></tr>';
12608
+ }
12609
+ tableHtml += '</tbody></table>';
12610
+ }
12611
+ return '<div class="card"><div class="card-header"><div><div class="card-title">' + t('oc_agent_list') + '</div><div class="card-desc">' + t('oc_agent_list_desc') + '</div></div>' +
12612
+ '<button class="btn btn-sm btn-primary" onclick="addOcAgent()">' + t('add_agent_btn') + '</button></div>' +
12613
+ '<div id="oc-agents-table">' + tableHtml + '</div></div>';
12614
+ }
12615
+
12616
+ function renderOcBindings(bindings) {
12617
+ bindings = bindings || [];
12618
+ let tableHtml = '';
12619
+ if (bindings.length === 0) {
12620
+ tableHtml = '<div class="empty"><div class="icon">\u{1F517}</div><p>' + t('no_bindings') + '</p></div>';
12621
+ } else {
12622
+ tableHtml = '<table><thead><tr><th>' + t('binding_agent_id') + '</th><th>' + t('binding_channel') + '</th><th>' + t('binding_account_id') + '</th><th>' + t('binding_type') + '</th><th>' + t('actions') + '</th></tr></thead><tbody>';
12623
+ for (let i = 0; i < bindings.length; i++) {
12624
+ const b = bindings[i];
12625
+ tableHtml += '<tr><td class="mono">' + escHtml(b.agentId || '') + '</td><td class="mono">' + escHtml(b.match?.channel || '') + '</td><td class="mono">' + escHtml(b.match?.accountId || '-') + '</td><td><span class="badge badge-ghost">' + escHtml(b.type || 'route') + '</span></td>' +
12626
+ '<td class="actions-cell"><button class="btn btn-sm btn-danger" onclick="deleteOcBinding(' + i + ')">\u{1F5D1}\uFE0F</button></td></tr>';
12627
+ }
12628
+ tableHtml += '</tbody></table>';
12629
+ }
12630
+ return '<div class="card"><div class="card-header"><div><div class="card-title">' + t('bindings_title') + '</div><div class="card-desc">' + t('bindings_desc') + '</div></div>' +
12631
+ '<button class="btn btn-sm btn-primary" onclick="addOcBinding()">' + t('add_binding') + '</button></div>' +
12632
+ '<div id="oc-bindings-table">' + tableHtml + '</div></div>';
12633
+ }
12634
+
12635
+ function renderOcChannels(channels) {
12636
+ channels = channels || {};
12637
+ const channelKeys = Object.keys(channels);
12638
+ let html2 = '';
12639
+ if (channelKeys.length === 0) {
12640
+ html2 = '<div class="empty"><div class="icon">\u{1F4E1}</div><p>' + t('no_channels_configured') + '</p></div>';
12641
+ } else {
12642
+ for (const ck of channelKeys) {
12643
+ const ch = channels[ck];
12644
+ const accs = ch.accounts || {};
12645
+ const accKeys = Object.keys(accs);
12646
+ const isEnabled = ch.enabled !== false;
12647
+ html2 += '<div class="card" style="margin-bottom:16px">';
12648
+ html2 += '<div class="card-header"><div class="card-title">\u{1F4E1} ' + escHtml(ck) + '</div>' +
12649
+ '<div style="display:flex;gap:8px;align-items:center"><span class="badge ' + (isEnabled ? 'badge-ok' : 'badge-danger') + '">' + (isEnabled ? t('channel_enabled') : t('channel_disabled')) + '</span>' +
12650
+ '<label class="toggle-label"><input type="checkbox" class="oc-ch-enabled" data-channel="' + escHtml(ck) + '"' + (isEnabled ? ' checked' : '') + '><span class="toggle-slider"></span></label></div></div>';
12651
+
12652
+ html2 += '<div class="form-row">';
12653
+ html2 += '<div class="form-group"><label>' + t('group_policy') + '</label><input type="text" class="oc-ch-policy" data-channel="' + escHtml(ck) + '" value="' + escHtml(ch.groupPolicy || '') + '" placeholder="open"></div>';
12654
+ html2 += '</div>';
12655
+
12656
+ html2 += '<div class="form-group"><label>' + t('accounts') + ' (' + accKeys.length + ')</label>';
12657
+ for (const ak of accKeys) {
12658
+ const acc = accs[ak];
12659
+ const allowFrom = acc.allowFrom || [];
12660
+ html2 += '<div class="card" style="margin-bottom:10px;padding:14px;background:var(--bg3)">';
12661
+ html2 += '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px"><strong class="mono">' + escHtml(ak) + '</strong><div class="actions-cell"><button class="btn btn-sm btn-default" onclick="editOcAccount(\\'' + escHtml(ck) + '\\',\\'' + escHtml(ak) + '\\')">\u270F\uFE0F</button><button class="btn btn-sm btn-danger" onclick="deleteOcAccount(\\'' + escHtml(ck) + '\\',\\'' + escHtml(ak) + '\\')">\u{1F5D1}\uFE0F</button></div></div>';
12662
+ html2 += '<div class="form-group"><label>Bot Token</label><input type="password" class="oc-acc-token" data-channel="' + escHtml(ck) + '" data-account="' + escHtml(ak) + '" value="' + escHtml(acc.botToken || '') + '" placeholder="****"></div>';
12663
+ html2 += '<div class="form-group"><label>' + t('dm_policy') + '</label><select class="oc-acc-dm-policy" data-channel="' + escHtml(ck) + '" data-account="' + escHtml(ak) + '"><option value="allowlist"' + (acc.dmPolicy === 'allowlist' ? ' selected' : '') + '>allowlist</option><option value="open"' + (acc.dmPolicy === 'open' ? ' selected' : '') + '>open</option><option value="block"' + (acc.dmPolicy === 'block' ? ' selected' : '') + '>block</option></select></div>';
12664
+ html2 += '<div class="form-group"><label>' + t('allow_from') + '</label>';
12665
+ for (const af of allowFrom) {
12666
+ html2 += '<div style="display:flex;gap:6px;align-items:center;margin-bottom:4px"><input type="text" class="oc-allow-from" data-channel="' + escHtml(ck) + '" data-account="' + escHtml(ak) + '" value="' + escHtml(af) + '" style="flex:1"><button class="btn btn-sm btn-danger" onclick="this.parentElement.remove()">\u2715</button></div>';
12667
+ }
12668
+ html2 += '<button class="btn btn-sm btn-default" data-channel="' + escHtml(ck) + '" data-account="' + escHtml(ak) + '" onclick="addOcAllowFrom(this)">\u2795 ' + t('add_allow_from') + '</button>';
12669
+ html2 += '</div></div>';
12670
+ }
12671
+ html2 += '<button class="btn btn-sm btn-default" onclick="addOcAccount(\\'' + escHtml(ck) + '\\')">\u2795 ' + t('add_account') + '</button>';
12672
+ html2 += '</div></div>';
12673
+ }
12674
+ }
12675
+ return '<div class="card"><div class="card-header"><div><div class="card-title">' + t('channels_title') + '</div><div class="card-desc">' + t('channels_desc') + '</div></div></div>' +
12676
+ '<div id="oc-channels-content">' + html2 + '</div></div>';
12677
+ }
12678
+
12679
+ // \u2500\u2500 OpenClaw dynamic list helpers \u2500\u2500
12680
+ function addOcListItem(btn, inputClass) {
12681
+ const container = btn.parentElement;
12682
+ const div = document.createElement('div');
12683
+ div.style.cssText = 'display:flex;gap:6px;align-items:center;margin-bottom:6px';
12684
+ div.innerHTML = '<input type="text" class="' + inputClass + '" value="" style="flex:1" placeholder="model-id"><button class="btn btn-sm btn-danger" onclick="this.parentElement.remove()">\u2715</button>';
12685
+ container.insertBefore(div, btn);
12686
+ div.querySelector('input').focus();
12687
+ }
12688
+
12689
+ function addOcModelRegItem(btn) {
12690
+ const container = btn.parentElement;
12691
+ const div = document.createElement('div');
12692
+ div.style.cssText = 'display:flex;gap:6px;align-items:center;margin-bottom:6px';
12693
+ div.innerHTML = '<input type="text" class="oc-model-reg-key" value="" style="width:45%" placeholder="model-id"><span style="color:var(--text3)">\u2192</span><input type="text" class="oc-model-reg-val" value="" style="flex:1" placeholder="{}" disabled><button class="btn btn-sm btn-danger" onclick="this.parentElement.remove()">\u2715</button>';
12694
+ container.insertBefore(div, btn);
12695
+ div.querySelector('.oc-model-reg-key').focus();
12696
+ }
12697
+
12698
+ function addOcAllowFrom(btn) {
12699
+ const container = btn.parentElement;
12700
+ const div = document.createElement('div');
12701
+ div.style.cssText = 'display:flex;gap:6px;align-items:center;margin-bottom:4px';
12702
+ div.innerHTML = '<input type="text" class="oc-allow-from" data-channel="' + (btn.dataset.channel || '') + '" data-account="' + (btn.dataset.account || '') + '" value="" style="flex:1"><button class="btn btn-sm btn-danger" onclick="this.parentElement.remove()">\u2715</button>';
12703
+ container.insertBefore(div, btn);
12704
+ div.querySelector('input').focus();
12705
+ }
12706
+
12707
+ // \u2500\u2500 OpenClaw CRUD operations \u2500\u2500
12708
+ function addOcAgent() {
12709
+ if (!_ocConfig) return;
12710
+ if (!_ocConfig.agents) _ocConfig.agents = { defaults: _ocConfig.agents?.defaults || {} };
12711
+ if (!Array.isArray(_ocConfig.agents.list)) _ocConfig.agents.list = [];
12712
+ _ocConfig.agents.list.push({ id: 'new-agent-' + Date.now(), identity: { name: '' }, model: { primary: '' }, tools: { profile: 'full' }, workspace: '' });
12713
+ renderOpenClawConfig(_ocConfig);
12714
+ switchOpenClawTab('agents');
12715
+ toast(t('agent_added'), 'ok');
12716
+ }
12717
+
12718
+ function editOcAgent(idx) {
12719
+ if (!_ocConfig?.agents?.list?.[idx]) return;
12720
+ const a = _ocConfig.agents.list[idx];
12721
+ const newId = prompt('Agent ID:', a.id);
12722
+ if (newId === null) return;
12723
+ const newName = prompt('Identity Name:', a.identity?.name || '');
12724
+ if (newName === null) return;
12725
+ const newModel = prompt('Primary Model:', a.model?.primary || '');
12726
+ if (newModel === null) return;
12727
+ const newTools = prompt('Tools Profile:', a.tools?.profile || '');
12728
+ if (newTools === null) return;
12729
+ const newWorkspace = prompt('Workspace:', a.workspace || '');
12730
+ if (newWorkspace === null) return;
12731
+ a.id = newId;
12732
+ a.identity = { name: newName };
12733
+ a.model = { primary: newModel };
12734
+ a.tools = { profile: newTools };
12735
+ a.workspace = newWorkspace;
12736
+ renderOpenClawConfig(_ocConfig);
12737
+ switchOpenClawTab('agents');
12738
+ }
12739
+
12740
+ function deleteOcAgent(idx) {
12741
+ if (!_ocConfig?.agents?.list) return;
12742
+ const a = _ocConfig.agents.list[idx];
12743
+ const name = a?.id || a?.identity?.name || '';
12744
+ if (!confirm(t('confirm_delete_oc_agent').replace('{name}', name))) return;
12745
+ _ocConfig.agents.list.splice(idx, 1);
12746
+ renderOpenClawConfig(_ocConfig);
12747
+ switchOpenClawTab('agents');
12748
+ toast(t('agent_deleted'), 'ok');
12749
+ }
12750
+
12751
+ function addOcBinding() {
12752
+ if (!_ocConfig) return;
12753
+ if (!Array.isArray(_ocConfig.bindings)) _ocConfig.bindings = [];
12754
+ const agentId = prompt('Agent ID:');
12755
+ if (!agentId) return;
12756
+ const channel = prompt('Channel:', '');
12757
+ const accountId = prompt('Account ID (optional):', '');
12758
+ const type = prompt('Type (route/direct):', 'route');
12759
+ const binding = { agentId };
12760
+ if (channel) binding.match = { channel };
12761
+ if (accountId) { if (!binding.match) binding.match = {}; binding.match.accountId = accountId; }
12762
+ if (type) binding.type = type;
12763
+ _ocConfig.bindings.push(binding);
12764
+ renderOpenClawConfig(_ocConfig);
12765
+ switchOpenClawTab('bindings');
12766
+ toast(t('openclaw_config_saved'), 'ok');
12767
+ }
12768
+
12769
+ function deleteOcBinding(idx) {
12770
+ if (!_ocConfig?.bindings) return;
12771
+ if (!confirm(t('confirm_delete_binding'))) return;
12772
+ _ocConfig.bindings.splice(idx, 1);
12773
+ renderOpenClawConfig(_ocConfig);
12774
+ switchOpenClawTab('bindings');
12775
+ toast(t('binding_deleted'), 'ok');
12776
+ }
12777
+
12778
+ function addOcAccount(channelKey) {
12779
+ if (!_ocConfig?.channels) return;
12780
+ const ch = _ocConfig.channels[channelKey];
12781
+ if (!ch) return;
12782
+ if (!ch.accounts) ch.accounts = {};
12783
+ const accId = prompt('Account ID:');
12784
+ if (!accId) return;
12785
+ ch.accounts[accId] = { botToken: '', dmPolicy: 'allowlist', allowFrom: [] };
12786
+ renderOpenClawConfig(_ocConfig);
12787
+ switchOpenClawTab('channels');
12788
+ toast(t('openclaw_config_saved'), 'ok');
12789
+ }
12790
+
12791
+ function editOcAccount(channelKey, accountId) {
12792
+ if (!_ocConfig?.channels?.[channelKey]?.accounts?.[accountId]) return;
12793
+ const acc = _ocConfig.channels[channelKey].accounts[accountId];
12794
+ const token = prompt('Bot Token:', acc.botToken || '');
12795
+ if (token === null) return;
12796
+ acc.botToken = token;
12797
+ renderOpenClawConfig(_ocConfig);
12798
+ switchOpenClawTab('channels');
12799
+ }
12800
+
12801
+ function deleteOcAccount(channelKey, accountId) {
12802
+ if (!_ocConfig?.channels?.[channelKey]?.accounts) return;
12803
+ if (!confirm(t('confirm_delete_account'))) return;
12804
+ delete _ocConfig.channels[channelKey].accounts[accountId];
12805
+ renderOpenClawConfig(_ocConfig);
12806
+ switchOpenClawTab('channels');
12807
+ toast(t('openclaw_config_saved'), 'ok');
12808
+ }
12809
+
12810
+ async function saveOpenClawConfig() {
12811
+ if (!_ocConfig) return;
12812
+ // Collect defaults
12813
+ const defaults = _ocConfig.agents?.defaults || {};
12814
+ const model = defaults.model || {};
12815
+ const imgModel = defaults.imageModel || {};
12816
+ const subagents = defaults.subagents || {};
12817
+ const modelsRegistry = defaults.models || {};
12818
+
12819
+ // Collect fallbacks
12820
+ const fallbackItems = $$('.oc-fallback-item');
12821
+ const fallbacks = [];
12822
+ for (const el of fallbackItems) { if (el.value.trim()) fallbacks.push(el.value.trim()); }
12823
+
12824
+ const imgFallbackItems = $$('.oc-img-fallback-item');
12825
+ const imgFallbacks = [];
12826
+ for (const el of imgFallbackItems) { if (el.value.trim()) imgFallbacks.push(el.value.trim()); }
12827
+
12828
+ const modelRegKeys = $$('.oc-model-reg-key');
12829
+ const newModelsReg = {};
12830
+ for (const el of modelRegKeys) { if (el.value.trim()) newModelsReg[el.value.trim()] = {}; }
12831
+
12832
+ const newDefaults = {
12833
+ compaction: { mode: ($('#oc-compaction-mode')?.value || defaults.compaction?.mode || 'safeguard') },
12834
+ model: { primary: $('#oc-primary-model')?.value || model.primary || '', fallbacks },
12835
+ imageModel: { primary: $('#oc-img-model-primary')?.value || imgModel.primary || '', fallbacks: imgFallbacks },
12836
+ maxConcurrent: parseInt($('#oc-max-concurrent')?.value) || defaults.maxConcurrent || 4,
12837
+ subagents: { maxConcurrent: parseInt($('#oc-subagent-max')?.value) || subagents.maxConcurrent || 8 },
12838
+ thinkingDefault: $('#oc-thinking-default')?.value || defaults.thinkingDefault || 'off',
12839
+ workspace: $('#oc-workspace')?.value || defaults.workspace || '',
12840
+ models: newModelsReg,
12841
+ };
12842
+
12843
+ // Collect channels state
12844
+ const channels = _ocConfig.channels || {};
12845
+ const chEnabledEls = $$('.oc-ch-enabled');
12846
+ for (const el of chEnabledEls) {
12847
+ const ck = el.dataset.channel;
12848
+ if (channels[ck]) channels[ck].enabled = el.checked;
12849
+ }
12850
+ const chPolicyEls = $$('.oc-ch-policy');
12851
+ for (const el of chPolicyEls) {
12852
+ const ck = el.dataset.channel;
12853
+ if (channels[ck]) channels[ck].groupPolicy = el.value;
12854
+ }
12855
+ // Collect account data
12856
+ const tokenEls = $$('.oc-acc-token');
12857
+ for (const el of tokenEls) {
12858
+ const ck = el.dataset.channel;
12859
+ const ak = el.dataset.account;
12860
+ if (channels[ck]?.accounts?.[ak]) channels[ck].accounts[ak].botToken = el.value;
12861
+ }
12862
+ const dmPolicyEls = $$('.oc-acc-dm-policy');
12863
+ for (const el of dmPolicyEls) {
12864
+ const ck = el.dataset.channel;
12865
+ const ak = el.dataset.account;
12866
+ if (channels[ck]?.accounts?.[ak]) channels[ck].accounts[ak].dmPolicy = el.value;
12867
+ }
12868
+ const allowFromEls = $$('.oc-allow-from');
12869
+ for (const el of allowFromEls) {
12870
+ const ck = el.dataset.channel;
12871
+ const ak = el.dataset.account;
12872
+ if (channels[ck]?.accounts?.[ak]) {
12873
+ if (!channels[ck].accounts[ak].allowFrom) channels[ck].accounts[ak].allowFrom = [];
12874
+ channels[ck].accounts[ak].allowFrom.push(el.value.trim());
12875
+ }
12876
+ }
12877
+ // Deduplicate allowFrom per account
12878
+ for (const ck of Object.keys(channels)) {
12879
+ const ch = channels[ck];
12880
+ if (!ch.accounts) continue;
12881
+ for (const ak of Object.keys(ch.accounts)) {
12882
+ const af = ch.accounts[ak].allowFrom;
12883
+ if (Array.isArray(af)) ch.accounts[ak].allowFrom = [...new Set(af)].filter(x => x);
12884
+ }
12885
+ }
12886
+
12887
+ const payload = {
12888
+ agents: { defaults: newDefaults, list: _ocConfig.agents?.list || [] },
12889
+ bindings: _ocConfig.bindings || [],
12890
+ channels,
12891
+ };
12892
+
12893
+ try {
12894
+ const r = await api('/openclaw-config', { method: 'PUT', body: JSON.stringify(payload) });
12895
+ if (r.success) {
12896
+ toast(t('openclaw_config_saved'), 'ok');
12897
+ _ocConfig = await api('/openclaw-config');
12898
+ renderOpenClawConfig(_ocConfig);
12899
+ } else {
12900
+ toast(r.message || t('failed_save'), 'err');
12901
+ }
12902
+ } catch (e) {
12903
+ toast(e.message || t('failed_save'), 'err');
11446
12904
  }
11447
12905
  }
11448
12906
 
@@ -11466,7 +12924,7 @@ document.addEventListener('DOMContentLoaded', () => {
11466
12924
  const dot = $('#header-dot');
11467
12925
  if (dot) { dot.className = 'dot ' + (s.connected ? 'dot-ok' : 'dot-err'); }
11468
12926
  const txt = $('#header-status');
11469
- if (txt) txt.textContent = s.connected ? 'Connected' : 'Disconnected';
12927
+ if (txt) txt.textContent = s.connected ? t('connected') : t('disconnected');
11470
12928
  // Auto-remove offline banner when server reconnects
11471
12929
  if (s.connected) hideOfflineBanner();
11472
12930
  }
@@ -11505,6 +12963,10 @@ var HTML = `<!DOCTYPE html>
11505
12963
  <div class="nav-group-title">System</div>
11506
12964
  <div class="nav-item" data-page="settings"><span class="nav-icon">\u2699\uFE0F</span><span class="nav-label">Settings</span></div>
11507
12965
  </div>
12966
+ <div class="nav-group">
12967
+ <div class="nav-group-title">OpenClaw</div>
12968
+ <div class="nav-item" data-page="openclaw"><span class="nav-icon">\u2699\uFE0F</span><span class="nav-label">OpenClaw</span></div>
12969
+ </div>
11508
12970
  </nav>
11509
12971
  <div class="sidebar-footer" onclick="toggleSidebar()">
11510
12972
  <span>\u25C0</span><span class="sidebar-footer-text">Collapse sidebar</span>
@@ -11547,6 +13009,9 @@ var HTML = `<!DOCTYPE html>
11547
13009
  <div id="settings-content"></div>
11548
13010
  </div>
11549
13011
 
13012
+ <!-- OpenClaw Config -->
13013
+ <div class="page" id="page-openclaw"><div id="openclaw-content"></div></div>
13014
+
11550
13015
  </div>
11551
13016
  </main>
11552
13017
  </div>
@@ -11605,6 +13070,7 @@ var HTML = `<!DOCTYPE html>
11605
13070
  <div class="input-prefix"><span class="prefix">\u{1F310}</span><input id="model-base-url" type="text" placeholder="https://..."></div>
11606
13071
  <div class="hint">Custom endpoint URL. Only needed for proxies or self-hosted models.</div>
11607
13072
  </div>
13073
+ <div id="model-multi-list" style="display:none;margin-top:12px"></div>
11608
13074
  <div class="form-actions">
11609
13075
  <button class="btn btn-default" onclick="hideModal('modal-model-config')">Cancel</button>
11610
13076
  <button class="btn btn-primary" onclick="saveModelConfig()">\u{1F4BE} Save Configuration</button>
@@ -11612,6 +13078,39 @@ var HTML = `<!DOCTYPE html>
11612
13078
  </div>
11613
13079
  </div>
11614
13080
 
13081
+ <!-- Modal: Custom Provider -->
13082
+ <div class="modal-overlay hidden" id="modal-custom-provider" onclick="if(event.target===this)hideModal('modal-custom-provider')">
13083
+ <div class="modal" style="max-width:520px">
13084
+ <div class="modal-header"><h3>\u{1F9E9} <span id="custom-provider-title">Custom Provider / \u81EA\u5B9A\u4E49\u63D0\u4F9B\u5546</span></h3><button class="modal-close" onclick="hideModal('modal-custom-provider')">\u2715</button></div>
13085
+ <p style="font-size:12px;color:var(--text3);margin-bottom:16px">Manually add custom model providers compatible with OpenAI API format. / \u624B\u52A8\u6DFB\u52A0\u517C\u5BB9 OpenAI API \u683C\u5F0F\u7684\u81EA\u5B9A\u4E49\u6A21\u578B\u63D0\u4F9B\u5546</p>
13086
+ <div class="form-group">
13087
+ <label>\u{1F4E6} Provider Name / \u63D0\u4F9B\u5546\u540D\u79F0 *</label>
13088
+ <input id="custom-name" type="text" placeholder="e.g., My Custom API / \u4F8B\u5982\uFF1AMy Custom API">
13089
+ </div>
13090
+ <div class="form-group">
13091
+ <label>\u{1F511} API Key</label>
13092
+ <input id="custom-api-key" type="password" placeholder="sk-...">
13093
+ <div class="hint">Leave blank to keep the existing key when editing.</div>
13094
+ </div>
13095
+ <div class="form-group">
13096
+ <label>\u{1F916} Model ID</label>
13097
+ <input id="custom-model-id" type="text" placeholder="gpt-4o">
13098
+ </div>
13099
+ <div class="form-group">
13100
+ <label>\u{1F310} Base URL</label>
13101
+ <input id="custom-base-url" type="text" placeholder="https://api.example.com/v1">
13102
+ </div>
13103
+ <div class="form-group">
13104
+ <label>\u{1F4DD} Description / \u63CF\u8FF0</label>
13105
+ <textarea id="custom-description" rows="2" placeholder="Optional: describe what this provider is for / \u53EF\u9009\uFF1A\u63CF\u8FF0\u6B64\u63D0\u4F9B\u5546\u7684\u7528\u9014"></textarea>
13106
+ </div>
13107
+ <div class="form-actions">
13108
+ <button class="btn btn-default" onclick="hideModal('modal-custom-provider')">Cancel / \u53D6\u6D88</button>
13109
+ <button class="btn btn-primary" onclick="saveCustomProvider()">\u{1F4BE} Save / \u4FDD\u5B58</button>
13110
+ </div>
13111
+ </div>
13112
+ </div>
13113
+
11615
13114
  <!-- Modal: View Agent -->
11616
13115
  <div class="modal-overlay hidden" id="modal-view-agent" onclick="if(event.target===this)hideModal('modal-view-agent')">
11617
13116
  <div class="modal">
@@ -11627,7 +13126,7 @@ var HTML = `<!DOCTYPE html>
11627
13126
  <div class="modal-header"><h3>\u{1F5D1}\uFE0F Reset Agent Identity</h3><button class="modal-close" onclick="hideModal('modal-reset-identity')">\u2715</button></div>
11628
13127
  <div style="margin-bottom:16px">
11629
13128
  <div class="card" style="border-color:var(--danger);background:var(--danger-bg)">
11630
- <p style="font-size:13px;color:#fca5a5;line-height:1.6">
13129
+ <p style="font-size:13px;color:#991b1b;line-height:1.6">
11631
13130
  <strong>\u26A0\uFE0F WARNING: This is a destructive operation!</strong><br><br>
11632
13131
  This will permanently delete:<br>
11633
13132
  \u2022 Your Ed25519 key pair and agent ID<br>
@@ -11754,29 +13253,59 @@ var MODEL_PROVIDERS = [
11754
13253
  { id: "openrouter", name: "OpenRouter", description: "Unified API for 200+ open-source and commercial models", apiKeyHint: "sk-or-...", modelHint: "openai/gpt-4o", baseUrlHint: "https://openrouter.ai/api/v1", configKey: "openrouter" },
11755
13254
  { id: "mistral", name: "Mistral AI", description: "Mistral Large, Medium, Small, Codestral", apiKeyHint: "(your key)", modelHint: "mistral-large-latest", baseUrlHint: "https://api.mistral.ai/v1", configKey: "mistral" },
11756
13255
  { id: "together", name: "Together AI", description: "Open-source models with fast inference", apiKeyHint: "...", modelHint: "meta-llama/Llama-3-70b-chat-hf", baseUrlHint: "https://api.together.xyz/v1", configKey: "together" },
11757
- { id: "fireworks", name: "Fireworks AI", description: "Fast open-source model serving", apiKeyHint: "...", modelHint: "accounts/fireworks/models/llama-v3-70b-instruct", baseUrlHint: "https://api.fireworks.ai/inference/v1", configKey: "fireworks" }
13256
+ { id: "fireworks", name: "Fireworks AI", description: "Fast open-source model serving", apiKeyHint: "...", modelHint: "accounts/fireworks/models/llama-v3-70b-instruct", baseUrlHint: "https://api.fireworks.ai/inference/v1", configKey: "fireworks" },
13257
+ // Chinese / local providers
13258
+ { id: "modelscope", name: "ModelScope (\u9B54\u642D)", description: "ZhipuAI GLM-5, Kimi-K2.5, MiniMax, Step models", apiKeyHint: "ms-...", modelHint: "Qwen/Qwen3-235B-A22B", baseUrlHint: "https://api-inference.modelscope.cn/v1", configKey: "modelscope" },
13259
+ { id: "zhipu", name: "Zhipu AI (\u667A\u8C31)", description: "GLM-5, GLM-4, CogView, CogVideoX", apiKeyHint: "...", modelHint: "glm-5", baseUrlHint: "https://open.bigmodel.cn/api/paas/v4", configKey: "zhipu" },
13260
+ { id: "qwen", name: "Alibaba Qwen (\u901A\u4E49\u5343\u95EE)", description: "Qwen3-235B, Qwen3-32B, Qwen-VL", apiKeyHint: "sk-...", modelHint: "qwen3-235b-a22b", baseUrlHint: "https://dashscope.aliyuncs.com/compatible-mode/v1", configKey: "qwen" },
13261
+ { id: "doubao", name: "ByteDance Doubao (\u8C46\u5305)", description: "Doubao Pro, Doubao Lite \u2014 ByteDance LLMs", apiKeyHint: "...", modelHint: "doubao-pro-32k", baseUrlHint: "https://ark.cn-beijing.volces.com/api/v3", configKey: "doubao" },
13262
+ { id: "moonshot", name: "Moonshot/Kimi (\u6708\u4E4B\u6697\u9762)", description: "Kimi-K2.5, Moonshot-v1 \u2014 strong Chinese reasoning", apiKeyHint: "sk-...", modelHint: "moonshot-v1-128k", baseUrlHint: "https://api.moonshot.cn/v1", configKey: "moonshot" },
13263
+ { id: "minimax", name: "MiniMax", description: "MiniMax-M2.5, abab \u2014 multimodal AI", apiKeyHint: "...", modelHint: "MiniMax-M2.5", baseUrlHint: "https://api.minimax.chat/v1", configKey: "minimax" },
13264
+ { id: "stepfun", name: "StepFun (\u9636\u8DC3\u661F\u8FB0)", description: "Step-3.5, Step-2 \u2014 reasoning models", apiKeyHint: "...", modelHint: "step-3.5-flash", baseUrlHint: "https://api.stepfun.com/v1", configKey: "stepfun" },
13265
+ { id: "baidu", name: "Baidu Wenxin (\u767E\u5EA6\u6587\u5FC3)", description: "ERNIE 4.0, ERNIE 3.5 \u2014 Baidu LLMs", apiKeyHint: "...", modelHint: "ernie-4.0-8k", baseUrlHint: "https://aip.baidubce.com/rpc/2.0/ai_custom", configKey: "baidu" },
13266
+ { id: "spark", name: "iFlytek Spark (\u8BAF\u98DE\u661F\u706B)", description: "Spark 4.0 Ultra, Spark 3.5 \u2014 iFlytek LLMs", apiKeyHint: "...", modelHint: "spark-4.0-ultra", baseUrlHint: "https://spark-api-open.xf-yun.com/v1", configKey: "spark" }
11758
13267
  ];
11759
13268
  function findConfigPath() {
11760
- const openclawPaths = [
11761
- path5.join(process.cwd(), "openclaw.json"),
13269
+ function searchUpward(fileName) {
13270
+ let dir = process.cwd();
13271
+ for (let i = 0; i < 10; i++) {
13272
+ const candidate = path5.join(dir, fileName);
13273
+ try {
13274
+ if (fs5.existsSync(candidate))
13275
+ return candidate;
13276
+ } catch {
13277
+ }
13278
+ const parent = path5.dirname(dir);
13279
+ if (parent === dir)
13280
+ break;
13281
+ dir = parent;
13282
+ }
13283
+ return null;
13284
+ }
13285
+ const fromCwd = searchUpward("openclaw.json");
13286
+ if (fromCwd)
13287
+ return fromCwd;
13288
+ const openclawHomePaths = [
11762
13289
  path5.join(os.homedir(), ".config", "openclaw", "openclaw.json"),
11763
13290
  path5.join(os.homedir(), ".openclaw", "openclaw.json"),
11764
13291
  path5.join(os.homedir(), "openclaw.json")
11765
13292
  ];
11766
- for (const p of openclawPaths) {
13293
+ for (const p of openclawHomePaths) {
11767
13294
  try {
11768
13295
  if (fs5.existsSync(p))
11769
13296
  return p;
11770
13297
  } catch {
11771
13298
  }
11772
13299
  }
11773
- const stableclawPaths = [
11774
- path5.join(process.cwd(), "stableclaw.json"),
13300
+ const fromCwdStable = searchUpward("stableclaw.json");
13301
+ if (fromCwdStable)
13302
+ return fromCwdStable;
13303
+ const stableclawHomePaths = [
11775
13304
  path5.join(os.homedir(), ".config", "stableclaw", "stableclaw.json"),
11776
13305
  path5.join(os.homedir(), ".stableclaw", "stableclaw.json"),
11777
13306
  path5.join(os.homedir(), "stableclaw.json")
11778
13307
  ];
11779
- for (const p of stableclawPaths) {
13308
+ for (const p of stableclawHomePaths) {
11780
13309
  try {
11781
13310
  if (fs5.existsSync(p))
11782
13311
  return p;
@@ -11810,9 +13339,30 @@ function writeConfig(config2) {
11810
13339
  }
11811
13340
  function extractAgentsFromConfig(config2) {
11812
13341
  const agents = [];
11813
- const agentsVal = config2.agents;
11814
- if (Array.isArray(agentsVal)) {
11815
- for (const a of agentsVal) {
13342
+ const agentsObj = config2.agents;
13343
+ if (typeof agentsObj === "object" && agentsObj !== null && !Array.isArray(agentsObj)) {
13344
+ const agentsRecord = agentsObj;
13345
+ if (typeof agentsRecord.defaults === "object" && agentsRecord.defaults !== null) {
13346
+ agents.push({
13347
+ _source: "agents-defaults",
13348
+ name: "(Defaults)",
13349
+ ...agentsRecord.defaults
13350
+ });
13351
+ }
13352
+ const listArr = agentsRecord.list;
13353
+ if (Array.isArray(listArr)) {
13354
+ for (const a of listArr) {
13355
+ if (typeof a === "object" && a !== null) {
13356
+ agents.push({
13357
+ _source: "agents-list",
13358
+ ...a
13359
+ });
13360
+ }
13361
+ }
13362
+ }
13363
+ }
13364
+ if (agents.length === 0 && Array.isArray(config2.agents)) {
13365
+ for (const a of config2.agents) {
11816
13366
  if (typeof a === "object" && a !== null) {
11817
13367
  agents.push(a);
11818
13368
  }
@@ -11820,7 +13370,56 @@ function extractAgentsFromConfig(config2) {
11820
13370
  }
11821
13371
  const agentVal = config2.agent;
11822
13372
  if (typeof agentVal === "object" && agentVal !== null && !Array.isArray(agentVal)) {
11823
- agents.unshift(agentVal);
13373
+ const agentRecord = agentVal;
13374
+ if (agentRecord.model) {
13375
+ const existingPrimary = agents.find((a) => a.default === true || a.isDefault === true);
13376
+ if (!existingPrimary) {
13377
+ agents.unshift({
13378
+ _source: "agent-primary",
13379
+ name: "Primary Agent",
13380
+ default: true,
13381
+ ...agentRecord
13382
+ });
13383
+ }
13384
+ } else if (agents.length === 0) {
13385
+ agents.unshift({
13386
+ _source: "agent-single",
13387
+ ...agentRecord
13388
+ });
13389
+ }
13390
+ }
13391
+ const modelsSection = config2.models;
13392
+ const defaultModel = modelsSection?.defaultModel || "";
13393
+ if (modelsSection && typeof modelsSection === "object") {
13394
+ const providersSection = modelsSection.providers;
13395
+ if (providersSection && typeof providersSection === "object") {
13396
+ for (const [providerId, providerConfig] of Object.entries(providersSection)) {
13397
+ if (typeof providerConfig !== "object" || providerConfig === null)
13398
+ continue;
13399
+ const pc = providerConfig;
13400
+ const modelsArr = pc.models;
13401
+ if (Array.isArray(modelsArr)) {
13402
+ for (let mi = 0; mi < modelsArr.length; mi++) {
13403
+ const m = modelsArr[mi];
13404
+ if (typeof m === "object" && m !== null) {
13405
+ const mObj = m;
13406
+ const agentEntry = {
13407
+ _source: "provider-model",
13408
+ _providerId: providerId,
13409
+ _modelIndex: mi,
13410
+ _configPath: "models.providers." + providerId + ".models." + mi,
13411
+ name: mObj.name || mObj.id || providerId + "/" + mi,
13412
+ model: mObj.id || "",
13413
+ provider: providerId,
13414
+ enabled: true,
13415
+ isDefault: defaultModel === mObj.id
13416
+ };
13417
+ agents.push(agentEntry);
13418
+ }
13419
+ }
13420
+ }
13421
+ }
13422
+ }
11824
13423
  }
11825
13424
  if (agents.length === 0) {
11826
13425
  for (const [key, val] of Object.entries(config2)) {
@@ -11841,34 +13440,174 @@ function getModelProviders(config2) {
11841
13440
  if (model?.providers)
11842
13441
  providersSection = model.providers;
11843
13442
  }
13443
+ if (!providersSection || typeof providersSection !== "object") {
13444
+ const models = config2.models;
13445
+ if (models?.providers)
13446
+ providersSection = models.providers;
13447
+ }
13448
+ const modelsSection = config2.models;
13449
+ let defaultModel = modelsSection?.defaultModel || "";
13450
+ const agentObj = config2.agent;
13451
+ const agentModel = agentObj?.model;
13452
+ if (typeof agentModel === "string" && !defaultModel) {
13453
+ defaultModel = agentModel;
13454
+ } else if (typeof agentModel === "object" && agentModel) {
13455
+ if (!defaultModel)
13456
+ defaultModel = agentModel.primary || "";
13457
+ }
13458
+ const authProfiles = {};
13459
+ const authObj = config2.auth;
13460
+ if (authObj && typeof authObj === "object") {
13461
+ const profiles = authObj.profiles;
13462
+ if (profiles && typeof profiles === "object") {
13463
+ for (const [profileKey, profileVal] of Object.entries(profiles)) {
13464
+ if (typeof profileVal !== "object" || profileVal === null)
13465
+ continue;
13466
+ const pv = profileVal;
13467
+ authProfiles[profileKey] = {
13468
+ provider: pv.provider || "",
13469
+ mode: pv.mode || ""
13470
+ };
13471
+ }
13472
+ }
13473
+ }
13474
+ const envVars = {};
13475
+ const envObj = config2.env;
13476
+ if (envObj && typeof envObj === "object") {
13477
+ const vars = envObj.vars;
13478
+ if (vars && typeof vars === "object") {
13479
+ for (const [key, val] of Object.entries(vars)) {
13480
+ if (typeof val === "string") {
13481
+ envVars[key] = val;
13482
+ }
13483
+ }
13484
+ }
13485
+ for (const [key, val] of Object.entries(envObj)) {
13486
+ if (key === "vars" || key === "shellEnv")
13487
+ continue;
13488
+ if (typeof val === "string" && (key.includes("API_KEY") || key.includes("_KEY") || key.toLowerCase().includes("apikey"))) {
13489
+ envVars[key] = val;
13490
+ }
13491
+ }
13492
+ }
11844
13493
  const providers = MODEL_PROVIDERS.map((p) => {
11845
13494
  const pc = providersSection?.[p.configKey] ?? config2[p.configKey];
11846
- const apiKey = pc?.apiKey || "";
11847
- const modelId = pc?.model || pc?.defaultModel || "";
13495
+ let apiKey = pc?.apiKey || "";
13496
+ if (!apiKey) {
13497
+ for (const [profileKey, profile] of Object.entries(authProfiles)) {
13498
+ if (profile.provider === p.configKey && profile.mode === "api_key") {
13499
+ apiKey = "__auth_profile__";
13500
+ }
13501
+ }
13502
+ }
13503
+ if (apiKey === "" || apiKey === "__auth_profile__") {
13504
+ for (const [envKey, envVal] of Object.entries(envVars)) {
13505
+ if (envKey.toUpperCase().includes(p.configKey.toUpperCase()) || p.configKey === "openai" && envKey.toUpperCase().includes("OPENAI") || p.configKey === "anthropic" && envKey.toUpperCase().includes("ANTHROPIC") || p.configKey === "google" && envKey.toUpperCase().includes("GOOGLE") || p.configKey === "groq" && envKey.toUpperCase().includes("GROQ") || p.configKey === "deepseek" && envKey.toUpperCase().includes("DEEPSEEK") || p.configKey === "openrouter" && envKey.toUpperCase().includes("OPENROUTER") || p.configKey === "zhipu" && envKey.toUpperCase().includes("ZHIPU") || p.configKey === "qwen" && envKey.toUpperCase().includes("QWEN") || p.configKey === "moonshot" && envKey.toUpperCase().includes("MOONSHOT") || p.configKey === "doubao" && envKey.toUpperCase().includes("DOUBAO") || p.configKey === "stepfun" && envKey.toUpperCase().includes("STEPFUN")) {
13506
+ apiKey = envVal;
13507
+ break;
13508
+ }
13509
+ }
13510
+ }
13511
+ let singleModelId = pc?.model || pc?.defaultModel || "";
13512
+ if (!singleModelId && defaultModel && defaultModel.startsWith(p.configKey + "/")) {
13513
+ singleModelId = defaultModel.split("/").slice(1).join("/");
13514
+ }
11848
13515
  const baseUrl = pc?.baseUrl || pc?.baseURL || "";
13516
+ let modelsList = [];
13517
+ const modelsArr = pc?.models;
13518
+ if (Array.isArray(modelsArr)) {
13519
+ modelsList = modelsArr.filter((m) => typeof m === "object" && m !== null).map((m) => m);
13520
+ }
13521
+ let modelId = singleModelId;
13522
+ if (modelsList.length > 0 && !modelId) {
13523
+ modelId = defaultModel || modelsList[0]?.id || "";
13524
+ }
11849
13525
  return {
11850
13526
  ...p,
11851
13527
  configured: Boolean(apiKey || p.id === "ollama" && baseUrl),
11852
13528
  apiKey: apiKey ? apiKey.substring(0, 6) + "\u2022\u2022\u2022\u2022\u2022\u2022" + apiKey.slice(-4) : "",
11853
13529
  apiKeyHasValue: Boolean(apiKey),
11854
13530
  modelId,
11855
- baseUrl
13531
+ baseUrl,
13532
+ models: modelsList,
13533
+ modelCount: modelsList.length,
13534
+ isCustom: false
11856
13535
  };
11857
13536
  });
13537
+ const customProvidersSection = providersSection?.custom;
13538
+ const customProviders = [];
13539
+ if (Array.isArray(customProvidersSection)) {
13540
+ for (let i = 0; i < customProvidersSection.length; i++) {
13541
+ const cp = customProvidersSection[i];
13542
+ if (typeof cp !== "object" || cp === null)
13543
+ continue;
13544
+ const cpObj = cp;
13545
+ const cApiKey = cpObj.apiKey || "";
13546
+ const cModelId = cpObj.modelId || cpObj.model || "";
13547
+ const cBaseUrl = cpObj.baseUrl || cpObj.baseURL || "";
13548
+ const cId = cpObj.id || "custom-" + i;
13549
+ let cModelsList = [];
13550
+ const cModelsArr = cpObj.models;
13551
+ if (Array.isArray(cModelsArr)) {
13552
+ cModelsList = cModelsArr.filter((m) => typeof m === "object" && m !== null).map((m) => m);
13553
+ }
13554
+ providers.push({
13555
+ id: cId,
13556
+ name: cpObj.name || cId,
13557
+ description: cpObj.description || "Custom provider",
13558
+ configured: Boolean(cApiKey || cBaseUrl),
13559
+ apiKey: cApiKey ? cApiKey.substring(0, 6) + "\u2022\u2022\u2022\u2022\u2022\u2022" + cApiKey.slice(-4) : "",
13560
+ apiKeyHasValue: Boolean(cApiKey),
13561
+ apiKeyHint: "sk-...",
13562
+ modelId: cModelsList.length > 0 ? defaultModel || cModelsList[0]?.id || "" : cModelId,
13563
+ modelHint: cModelId || "model-name",
13564
+ baseUrl: cBaseUrl,
13565
+ baseUrlHint: cBaseUrl || "https://...",
13566
+ models: cModelsList,
13567
+ modelCount: cModelsList.length,
13568
+ isCustom: true
13569
+ });
13570
+ }
13571
+ }
11858
13572
  const currentModels = [];
11859
- for (const p of MODEL_PROVIDERS) {
11860
- const pc = providersSection?.[p.configKey] ?? config2[p.configKey];
11861
- if (pc?.apiKey) {
13573
+ for (const p of providers) {
13574
+ if (!p.apiKeyHasValue)
13575
+ continue;
13576
+ const pId = p.id;
13577
+ const pName = p.name;
13578
+ const isCustom = p.isCustom;
13579
+ const pModels = p.models;
13580
+ const pBaseUrl = p.baseUrl;
13581
+ const pModelHint = p.modelHint || "";
13582
+ if (pModels && pModels.length > 0) {
13583
+ for (const m of pModels) {
13584
+ if (typeof m === "object" && m !== null) {
13585
+ const mObj = m;
13586
+ currentModels.push({
13587
+ provider: pName,
13588
+ providerId: pId,
13589
+ modelId: mObj.id || "",
13590
+ modelName: mObj.name || "",
13591
+ hasApiKey: true,
13592
+ baseUrl: pBaseUrl,
13593
+ isDefault: defaultModel === mObj.id,
13594
+ isCustom
13595
+ });
13596
+ }
13597
+ }
13598
+ } else {
11862
13599
  currentModels.push({
11863
- provider: p.name,
11864
- providerId: p.id,
11865
- modelId: pc.model || pc.defaultModel || p.modelHint,
13600
+ provider: pName,
13601
+ providerId: pId,
13602
+ modelId: p.modelId || pModelHint,
11866
13603
  hasApiKey: true,
11867
- baseUrl: pc.baseUrl || pc.baseURL || p.baseUrlHint
13604
+ baseUrl: pBaseUrl,
13605
+ isDefault: false,
13606
+ isCustom
11868
13607
  });
11869
13608
  }
11870
13609
  }
11871
- return { providers, currentModels };
13610
+ return { providers, currentModels, defaultModel, customProviders: customProvidersSection || [] };
11872
13611
  }
11873
13612
  function parseApiPath(reqUrl) {
11874
13613
  if (!reqUrl)
@@ -11987,14 +13726,33 @@ function createManagementHandler(ctx) {
11987
13726
  });
11988
13727
  }
11989
13728
  if (apiPath.startsWith("/agents/") && method === "DELETE") {
11990
- const idxStr = decodeURIComponent(apiPath.slice("/agents/".length));
11991
- const idx = parseInt(idxStr, 10);
11992
- if (isNaN(idx) || idx < 0)
11993
- return json(res, { success: false, message: "Invalid agent index" }, 400);
13729
+ const identifier = decodeURIComponent(apiPath.slice("/agents/".length));
11994
13730
  const result = readConfig();
11995
13731
  if (!result)
11996
13732
  return json(res, { success: false, message: "No config file found" }, 400);
11997
13733
  const config2 = result.config;
13734
+ const providerModelMatch = identifier.match(/^provider:([^:]+):(\d+)$/);
13735
+ if (providerModelMatch) {
13736
+ const providerId = providerModelMatch[1];
13737
+ const modelIdx = parseInt(providerModelMatch[2], 10);
13738
+ const modelsSection = config2.models;
13739
+ const providersObj = modelsSection?.providers;
13740
+ const provConfig = providersObj?.[providerId];
13741
+ const modelsArr = provConfig?.models;
13742
+ if (!modelsArr || modelIdx >= modelsArr.length) {
13743
+ return json(res, { success: false, message: "Provider model index out of range" }, 400);
13744
+ }
13745
+ modelsArr.splice(modelIdx, 1);
13746
+ const written2 = writeConfig(config2);
13747
+ if (written2) {
13748
+ logger.info("[API] Provider model deleted: " + providerId + ".models[" + modelIdx + "]");
13749
+ return json(res, { success: true, message: "Model removed from " + providerId });
13750
+ }
13751
+ return json(res, { success: false, message: "Failed to write config" }, 500);
13752
+ }
13753
+ const idx = parseInt(identifier, 10);
13754
+ if (isNaN(idx) || idx < 0)
13755
+ return json(res, { success: false, message: "Invalid agent identifier" }, 400);
11998
13756
  let agentsArr;
11999
13757
  if (Array.isArray(config2.agents)) {
12000
13758
  agentsArr = config2.agents;
@@ -12020,14 +13778,17 @@ function createManagementHandler(ctx) {
12020
13778
  }
12021
13779
  if (apiPath === "/friends" && method === "GET") {
12022
13780
  try {
12023
- const resp = await fetch(serverUrl + "/api/v1/friends?nodeId=" + aicqAgentId);
13781
+ const resp = await fetch(serverUrl + "/api/v1/friends?nodeId=" + aicqAgentId, {
13782
+ headers: serverClient.authHeaders()
13783
+ });
12024
13784
  if (!resp.ok)
12025
13785
  return json(res, { error: "Server error: " + await resp.text() }, 502);
12026
13786
  const data = await resp.json();
12027
13787
  const friends = (data.friends || []).map((f) => {
12028
- const local = store.getFriend(f.nodeId);
13788
+ const friendId = f.id || f.nodeId;
13789
+ const local = store.getFriend(friendId);
12029
13790
  return {
12030
- id: f.nodeId,
13791
+ id: friendId,
12031
13792
  publicKeyFingerprint: f.publicKeyFingerprint || local?.publicKeyFingerprint || "",
12032
13793
  permissions: f.permissions || local?.permissions || [],
12033
13794
  addedAt: f.addedAt || local?.addedAt?.toISOString() || null,
@@ -12059,7 +13820,9 @@ function createManagementHandler(ctx) {
12059
13820
  let friendId = target;
12060
13821
  if (isTempNumber) {
12061
13822
  try {
12062
- const resolveResp = await fetch(serverUrl + "/api/v1/temp-number/" + target);
13823
+ const resolveResp = await fetch(serverUrl + "/api/v1/temp-number/" + target, {
13824
+ headers: serverClient.authHeaders()
13825
+ });
12063
13826
  if (!resolveResp.ok)
12064
13827
  return json(res, { success: false, message: "Temp number not found or expired" });
12065
13828
  const resolveData = await resolveResp.json();
@@ -12072,7 +13835,7 @@ function createManagementHandler(ctx) {
12072
13835
  try {
12073
13836
  const hsResp = await fetch(serverUrl + "/api/v1/handshake/initiate", {
12074
13837
  method: "POST",
12075
- headers: { "Content-Type": "application/json" },
13838
+ headers: serverClient.authHeaders(),
12076
13839
  body: JSON.stringify({ requesterId: aicqAgentId, targetTempNumber: target })
12077
13840
  });
12078
13841
  if (!hsResp.ok)
@@ -12095,7 +13858,7 @@ function createManagementHandler(ctx) {
12095
13858
  try {
12096
13859
  const rmResp = await fetch(serverUrl + "/api/v1/friends/" + friendId, {
12097
13860
  method: "DELETE",
12098
- headers: { "Content-Type": "application/json" },
13861
+ headers: serverClient.authHeaders(),
12099
13862
  body: JSON.stringify({ nodeId: aicqAgentId })
12100
13863
  });
12101
13864
  if (!rmResp.ok)
@@ -12121,7 +13884,7 @@ function createManagementHandler(ctx) {
12121
13884
  try {
12122
13885
  const resp = await fetch(serverUrl + "/api/v1/friends/" + friendId + "/permissions", {
12123
13886
  method: "PUT",
12124
- headers: { "Content-Type": "application/json" },
13887
+ headers: serverClient.authHeaders(),
12125
13888
  body: JSON.stringify({ nodeId: aicqAgentId, permissions })
12126
13889
  });
12127
13890
  if (!resp.ok)
@@ -12140,7 +13903,9 @@ function createManagementHandler(ctx) {
12140
13903
  }
12141
13904
  if (apiPath === "/friends/requests" && method === "GET") {
12142
13905
  try {
12143
- const resp = await fetch(serverUrl + "/api/v1/friends/requests?nodeId=" + aicqAgentId);
13906
+ const resp = await fetch(serverUrl + "/api/v1/friends/requests?accountId=" + aicqAgentId, {
13907
+ headers: serverClient.authHeaders()
13908
+ });
12144
13909
  if (!resp.ok)
12145
13910
  return json(res, { error: "Server error: " + await resp.text() }, 502);
12146
13911
  const data = await resp.json();
@@ -12163,8 +13928,8 @@ function createManagementHandler(ctx) {
12163
13928
  try {
12164
13929
  const resp = await fetch(serverUrl + "/api/v1/friends/requests/" + requestId + "/accept", {
12165
13930
  method: "POST",
12166
- headers: { "Content-Type": "application/json" },
12167
- body: JSON.stringify({ permissions: body.permissions || ["chat"] })
13931
+ headers: serverClient.authHeaders(),
13932
+ body: JSON.stringify({ accountId: aicqAgentId, permissions: body.permissions || ["chat"] })
12168
13933
  });
12169
13934
  if (!resp.ok)
12170
13935
  return json(res, { success: false, message: "Failed: " + await resp.text() });
@@ -12182,8 +13947,8 @@ function createManagementHandler(ctx) {
12182
13947
  try {
12183
13948
  const resp = await fetch(serverUrl + "/api/v1/friends/requests/" + requestId + "/reject", {
12184
13949
  method: "POST",
12185
- headers: { "Content-Type": "application/json" },
12186
- body: JSON.stringify({})
13950
+ headers: serverClient.authHeaders(),
13951
+ body: JSON.stringify({ accountId: aicqAgentId })
12187
13952
  });
12188
13953
  if (!resp.ok)
12189
13954
  return json(res, { success: false, message: "Failed: " + await resp.text() });
@@ -12205,9 +13970,37 @@ function createManagementHandler(ctx) {
12205
13970
  if (apiPath === "/models" && method === "GET") {
12206
13971
  const result = readConfig();
12207
13972
  if (!result)
12208
- return json(res, { providers: MODEL_PROVIDERS, currentModels: [], error: "No config file found" });
13973
+ return json(res, { providers: MODEL_PROVIDERS, currentModels: [], defaultModel: "", error: "No config file found" });
12209
13974
  return json(res, getModelProviders(result.config));
12210
13975
  }
13976
+ if (apiPath === "/models/default" && method === "GET") {
13977
+ const result = readConfig();
13978
+ if (!result)
13979
+ return json(res, { defaultModel: "", error: "No config file found" });
13980
+ const modelsSection = result.config.models;
13981
+ const defaultModel = modelsSection?.defaultModel || "";
13982
+ let defaultProvider = "";
13983
+ let defaultModelName = "";
13984
+ if (defaultModel && modelsSection?.providers) {
13985
+ const providersObj = modelsSection.providers;
13986
+ for (const [provId, provConfig] of Object.entries(providersObj)) {
13987
+ const pc = provConfig;
13988
+ if (Array.isArray(pc.models)) {
13989
+ const found = pc.models.find((m) => m.id === defaultModel);
13990
+ if (found) {
13991
+ defaultProvider = provId;
13992
+ defaultModelName = found.name || found.id || "";
13993
+ break;
13994
+ }
13995
+ }
13996
+ }
13997
+ }
13998
+ return json(res, {
13999
+ defaultModel,
14000
+ defaultProvider,
14001
+ defaultModelName
14002
+ });
14003
+ }
12211
14004
  if (apiPath.match(/^\/models\/[^/]+$/) && method === "PUT") {
12212
14005
  const providerId = decodeURIComponent(apiPath.slice("/models/".length));
12213
14006
  const body = await readBody(req);
@@ -12241,6 +14034,140 @@ function createManagementHandler(ctx) {
12241
14034
  logger.info("[API] Model config saved for provider: " + providerId);
12242
14035
  return json(res, { success: true, message: "Model configuration saved for " + provider.name });
12243
14036
  }
14037
+ if (apiPath === "/models/custom" && method === "POST") {
14038
+ const body = await readBody(req);
14039
+ const name = body.name?.trim();
14040
+ const apiKey = body.apiKey;
14041
+ const modelId = body.modelId;
14042
+ const baseUrl = body.baseUrl;
14043
+ const description = body.description;
14044
+ if (!name)
14045
+ return json(res, { success: false, message: "Provider name is required" }, 400);
14046
+ if (!apiKey && !modelId && !baseUrl) {
14047
+ return json(res, { success: false, message: "At least one of API Key, Model ID, or Base URL is required" }, 400);
14048
+ }
14049
+ const result = readConfig();
14050
+ if (!result)
14051
+ return json(res, { success: false, message: "No config file found" }, 400);
14052
+ const config2 = result.config;
14053
+ if (!config2.providers || typeof config2.providers !== "object") {
14054
+ config2.providers = {};
14055
+ }
14056
+ const providers = config2.providers;
14057
+ if (!Array.isArray(providers.custom)) {
14058
+ providers.custom = [];
14059
+ }
14060
+ const customArr = providers.custom;
14061
+ const newId = "custom-" + crypto.randomUUID().replace(/-/g, "").substring(0, 8);
14062
+ const duplicate = customArr.find((c) => c.name?.toLowerCase() === name.toLowerCase());
14063
+ if (duplicate) {
14064
+ return json(res, { success: false, message: "A provider with this name already exists" }, 409);
14065
+ }
14066
+ const newProvider = {
14067
+ id: newId,
14068
+ name,
14069
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
14070
+ };
14071
+ if (apiKey)
14072
+ newProvider.apiKey = apiKey;
14073
+ if (modelId)
14074
+ newProvider.modelId = modelId;
14075
+ if (baseUrl)
14076
+ newProvider.baseUrl = baseUrl;
14077
+ if (description)
14078
+ newProvider.description = description;
14079
+ customArr.push(newProvider);
14080
+ const written = writeConfig(config2);
14081
+ if (!written)
14082
+ return json(res, { success: false, message: "Failed to write config file" }, 500);
14083
+ logger.info("[API] Custom provider added: " + name + " (" + newId + ")");
14084
+ return json(res, { success: true, message: "Custom provider '" + name + "' added", providerId: newId });
14085
+ }
14086
+ if (apiPath.match(/^\/models\/custom\/[^/]+$/) && method === "PUT") {
14087
+ const customId = decodeURIComponent(apiPath.slice("/models/custom/".length));
14088
+ const body = await readBody(req);
14089
+ const result = readConfig();
14090
+ if (!result)
14091
+ return json(res, { success: false, message: "No config file found" }, 400);
14092
+ const config2 = result.config;
14093
+ const providers = config2.providers;
14094
+ const customArr = providers?.custom;
14095
+ if (!Array.isArray(customArr)) {
14096
+ return json(res, { success: false, message: "No custom providers found" }, 404);
14097
+ }
14098
+ const idx = customArr.findIndex((c) => c.id === customId);
14099
+ if (idx === -1) {
14100
+ return json(res, { success: false, message: "Custom provider not found" }, 404);
14101
+ }
14102
+ const cp = customArr[idx];
14103
+ if (body.name)
14104
+ cp.name = body.name.trim();
14105
+ if (body.apiKey !== void 0)
14106
+ cp.apiKey = body.apiKey;
14107
+ if (body.modelId !== void 0)
14108
+ cp.modelId = body.modelId;
14109
+ if (body.baseUrl !== void 0)
14110
+ cp.baseUrl = body.baseUrl;
14111
+ if (body.description !== void 0)
14112
+ cp.description = body.description;
14113
+ const written = writeConfig(config2);
14114
+ if (!written)
14115
+ return json(res, { success: false, message: "Failed to write config file" }, 500);
14116
+ logger.info("[API] Custom provider updated: " + customId);
14117
+ return json(res, { success: true, message: "Custom provider updated" });
14118
+ }
14119
+ if (apiPath.match(/^\/models\/custom\/[^/]+$/) && method === "DELETE") {
14120
+ const customId = decodeURIComponent(apiPath.slice("/models/custom/".length));
14121
+ const result = readConfig();
14122
+ if (!result)
14123
+ return json(res, { success: false, message: "No config file found" }, 400);
14124
+ const config2 = result.config;
14125
+ const providers = config2.providers;
14126
+ const customArr = providers?.custom;
14127
+ if (!Array.isArray(customArr)) {
14128
+ return json(res, { success: false, message: "No custom providers found" }, 404);
14129
+ }
14130
+ const idx = customArr.findIndex((c) => c.id === customId);
14131
+ if (idx === -1) {
14132
+ return json(res, { success: false, message: "Custom provider not found" }, 404);
14133
+ }
14134
+ const removed = customArr.splice(idx, 1)[0];
14135
+ const written = writeConfig(config2);
14136
+ if (!written)
14137
+ return json(res, { success: false, message: "Failed to write config file" }, 500);
14138
+ logger.info("[API] Custom provider deleted: " + (removed.name || customId));
14139
+ return json(res, { success: true, message: "Custom provider '" + (removed.name || customId) + "' deleted" });
14140
+ }
14141
+ if (apiPath === "/openclaw-config" && method === "GET") {
14142
+ const result = readConfig();
14143
+ if (!result)
14144
+ return json(res, { error: "No config file found" }, 400);
14145
+ const config2 = result.config;
14146
+ return json(res, {
14147
+ agents: config2.agents || null,
14148
+ bindings: config2.bindings || [],
14149
+ channels: config2.channels || null,
14150
+ configPath: result.configPath
14151
+ });
14152
+ }
14153
+ if (apiPath === "/openclaw-config" && method === "PUT") {
14154
+ const body = await readBody(req);
14155
+ const result = readConfig();
14156
+ if (!result)
14157
+ return json(res, { success: false, message: "No config file found" }, 400);
14158
+ const config2 = result.config;
14159
+ if (body.agents !== void 0)
14160
+ config2.agents = body.agents;
14161
+ if (body.bindings !== void 0)
14162
+ config2.bindings = body.bindings;
14163
+ if (body.channels !== void 0)
14164
+ config2.channels = body.channels;
14165
+ const written = writeConfig(config2);
14166
+ if (!written)
14167
+ return json(res, { success: false, message: "Failed to write config" }, 500);
14168
+ logger.info("[API] OpenClaw config updated");
14169
+ return json(res, { success: true, message: "Configuration saved" });
14170
+ }
12244
14171
  if (apiPath === "/settings" && method === "GET") {
12245
14172
  const result = readConfig();
12246
14173
  const aicqSection = result?.config?.aicq ?? {};
@@ -12592,10 +14519,7 @@ function createManagementHandler(ctx) {
12592
14519
  return json(res, { success: true, message: "Agent added", index: config2.agents.length - 1 });
12593
14520
  }
12594
14521
  if (apiPath.startsWith("/agents/") && method === "PUT") {
12595
- const idxStr = decodeURIComponent(apiPath.slice("/agents/".length));
12596
- const idx = parseInt(idxStr, 10);
12597
- if (isNaN(idx) || idx < 0)
12598
- return json(res, { success: false, message: "Invalid agent index" }, 400);
14522
+ const identifier = decodeURIComponent(apiPath.slice("/agents/".length));
12599
14523
  const body = await readBody(req);
12600
14524
  const updates = body.agent;
12601
14525
  if (!updates || typeof updates !== "object") {
@@ -12605,11 +14529,42 @@ function createManagementHandler(ctx) {
12605
14529
  if (!result)
12606
14530
  return json(res, { success: false, message: "No config file found" }, 400);
12607
14531
  const config2 = result.config;
12608
- let agentsArr;
14532
+ const providerModelMatch = identifier.match(/^provider:([^:]+):(\d+)$/);
14533
+ if (providerModelMatch) {
14534
+ const providerId = providerModelMatch[1];
14535
+ const modelIdx = parseInt(providerModelMatch[2], 10);
14536
+ const modelsSection = config2.models;
14537
+ const providersObj = modelsSection?.providers;
14538
+ const provConfig = providersObj?.[providerId];
14539
+ const modelsArr = provConfig?.models;
14540
+ if (!modelsArr || modelIdx >= modelsArr.length) {
14541
+ return json(res, { success: false, message: "Provider model index out of range" }, 400);
14542
+ }
14543
+ const modelEntry = modelsArr[modelIdx];
14544
+ if (updates.name)
14545
+ modelEntry.name = updates.name;
14546
+ if (updates.id)
14547
+ modelEntry.id = updates.id;
14548
+ if (updates.provider)
14549
+ modelEntry.provider = updates.provider;
14550
+ for (const [k, v] of Object.entries(updates)) {
14551
+ if (!["name", "id", "provider", "_source", "_providerId", "_modelIndex", "_configPath", "isDefault"].includes(k)) {
14552
+ modelEntry[k] = v;
14553
+ }
14554
+ }
14555
+ const written2 = writeConfig(config2);
14556
+ if (!written2)
14557
+ return json(res, { success: false, message: "Failed to write config" }, 500);
14558
+ logger.info("[API] Provider model updated: " + providerId + ".models[" + modelIdx + "]");
14559
+ return json(res, { success: true, message: "Model updated in " + providerId });
14560
+ }
14561
+ const idx = parseInt(identifier, 10);
14562
+ if (isNaN(idx) || idx < 0)
14563
+ return json(res, { success: false, message: "Invalid agent identifier" }, 400);
12609
14564
  if (!Array.isArray(config2.agents)) {
12610
14565
  return json(res, { success: false, message: "No agents array in config" }, 400);
12611
14566
  }
12612
- agentsArr = config2.agents;
14567
+ const agentsArr = config2.agents;
12613
14568
  if (idx >= agentsArr.length) {
12614
14569
  return json(res, { success: false, message: "Agent index out of range" }, 400);
12615
14570
  }
@@ -12832,6 +14787,9 @@ var plugin = definePluginEntry({
12832
14787
  } catch (e) {
12833
14788
  logger.warn("[Init] WS connect failed: " + (e instanceof Error ? e.message : e));
12834
14789
  }
14790
+ serverClient.onConnectionStateChange((newState, prevState) => {
14791
+ logger.info("[Init] Connection state changed: " + prevState + " \u2192 " + newState);
14792
+ });
12835
14793
  serverClient.onWsMessage("relay", (data) => {
12836
14794
  const msg = data;
12837
14795
  if (!msg?.payload)
@@ -12845,14 +14803,9 @@ var plugin = definePluginEntry({
12845
14803
  }
12846
14804
  });
12847
14805
  setInterval(() => {
12848
- if (!serverClient.isConnected()) {
12849
- try {
12850
- serverClient.connectWebSocket();
12851
- } catch (_e) {
12852
- }
12853
- }
14806
+ store.cleanupExpiredTempNumbers();
14807
+ store.cleanupExpiredOfflineMessages();
12854
14808
  }, 6e4);
12855
- setInterval(() => store.cleanupExpiredTempNumbers(), 6e4);
12856
14809
  api.registerTool({
12857
14810
  label: "AICQ Friend Manager",
12858
14811
  name: "chat-friend",
@@ -12870,9 +14823,13 @@ var plugin = definePluginEntry({
12870
14823
  try {
12871
14824
  switch (action) {
12872
14825
  case "request-temp-number": {
14826
+ const authHeaders = { "Content-Type": "application/json" };
14827
+ const token = serverClient.getAuthToken();
14828
+ if (token)
14829
+ authHeaders["Authorization"] = "Bearer " + token;
12873
14830
  const resp = await fetch(serverUrl + "/api/v1/temp-number/request", {
12874
14831
  method: "POST",
12875
- headers: { "Content-Type": "application/json" },
14832
+ headers: authHeaders,
12876
14833
  body: JSON.stringify({ nodeId: aicqAgentId })
12877
14834
  });
12878
14835
  if (!resp.ok)
@@ -12881,7 +14838,11 @@ var plugin = definePluginEntry({
12881
14838
  return { success: true, tempNumber: data.number, message: "Temp number: " + data.number };
12882
14839
  }
12883
14840
  case "list": {
12884
- const resp = await fetch(serverUrl + "/api/v1/friends?nodeId=" + aicqAgentId);
14841
+ const listAuthHeaders = {};
14842
+ const listToken = serverClient.getAuthToken();
14843
+ if (listToken)
14844
+ listAuthHeaders["Authorization"] = "Bearer " + listToken;
14845
+ const resp = await fetch(serverUrl + "/api/v1/friends?nodeId=" + aicqAgentId, { headers: listAuthHeaders });
12885
14846
  if (!resp.ok)
12886
14847
  return { error: "Server error: " + await resp.text() };
12887
14848
  const data = await resp.json();
@@ -12894,15 +14855,23 @@ var plugin = definePluginEntry({
12894
14855
  const isTempNumber = /^\d{6}$/.test(target);
12895
14856
  let friendId = target;
12896
14857
  if (isTempNumber) {
12897
- const resolveResp = await fetch(serverUrl + "/api/v1/temp-number/" + target);
14858
+ const resolveAuthHeaders = {};
14859
+ const resolveToken = serverClient.getAuthToken();
14860
+ if (resolveToken)
14861
+ resolveAuthHeaders["Authorization"] = "Bearer " + resolveToken;
14862
+ const resolveResp = await fetch(serverUrl + "/api/v1/temp-number/" + target, { headers: resolveAuthHeaders });
12898
14863
  if (!resolveResp.ok)
12899
14864
  return { error: "Temp number not found or expired" };
12900
14865
  const resolveData = await resolveResp.json();
12901
14866
  friendId = resolveData.nodeId;
12902
14867
  }
14868
+ const hsAuthHeaders = { "Content-Type": "application/json" };
14869
+ const hsToken = serverClient.getAuthToken();
14870
+ if (hsToken)
14871
+ hsAuthHeaders["Authorization"] = "Bearer " + hsToken;
12903
14872
  const hsResp = await fetch(serverUrl + "/api/v1/handshake/initiate", {
12904
14873
  method: "POST",
12905
- headers: { "Content-Type": "application/json" },
14874
+ headers: hsAuthHeaders,
12906
14875
  body: JSON.stringify({ requesterId: aicqAgentId, targetTempNumber: target })
12907
14876
  });
12908
14877
  if (!hsResp.ok)
@@ -12914,9 +14883,13 @@ var plugin = definePluginEntry({
12914
14883
  const target = params?.target;
12915
14884
  if (!target)
12916
14885
  return { error: "Missing target (friend ID to remove)" };
14886
+ const rmAuthHeaders = { "Content-Type": "application/json" };
14887
+ const rmToken = serverClient.getAuthToken();
14888
+ if (rmToken)
14889
+ rmAuthHeaders["Authorization"] = "Bearer " + rmToken;
12917
14890
  const rmResp = await fetch(serverUrl + "/api/v1/friends/" + target, {
12918
14891
  method: "DELETE",
12919
- headers: { "Content-Type": "application/json" },
14892
+ headers: rmAuthHeaders,
12920
14893
  body: JSON.stringify({ nodeId: aicqAgentId })
12921
14894
  });
12922
14895
  if (!rmResp.ok)
@@ -12924,9 +14897,16 @@ var plugin = definePluginEntry({
12924
14897
  return { success: true, message: "Friend " + target + " removed" };
12925
14898
  }
12926
14899
  case "revoke-temp-number": {
12927
- const resp = await fetch(serverUrl + "/api/v1/temp-number/" + aicqAgentId, {
14900
+ const revokeTarget = params?.target;
14901
+ if (!revokeTarget)
14902
+ return { error: "Missing target (temp number to revoke)" };
14903
+ const revokeAuthHeaders = { "Content-Type": "application/json" };
14904
+ const revokeToken = serverClient.getAuthToken();
14905
+ if (revokeToken)
14906
+ revokeAuthHeaders["Authorization"] = "Bearer " + revokeToken;
14907
+ const resp = await fetch(serverUrl + "/api/v1/temp-number/" + revokeTarget + "?nodeId=" + aicqAgentId, {
12928
14908
  method: "DELETE",
12929
- headers: { "Content-Type": "application/json" },
14909
+ headers: revokeAuthHeaders,
12930
14910
  body: JSON.stringify({ nodeId: aicqAgentId })
12931
14911
  });
12932
14912
  if (!resp.ok)
@@ -12997,10 +14977,12 @@ var plugin = definePluginEntry({
12997
14977
  try {
12998
14978
  return {
12999
14979
  connected: serverClient.isConnected(),
14980
+ connectionState: serverClient.getConnectionState(),
13000
14981
  agentId: aicqAgentId,
13001
14982
  fingerprint: identityService.getPublicKeyFingerprint(),
13002
14983
  friendCount: store.getFriendCount(),
13003
14984
  sessionCount: store.sessions.size,
14985
+ offlineMessageCount: store.getOfflineMessageCount(),
13004
14986
  serverUrl
13005
14987
  };
13006
14988
  } catch (err) {
@@ -13010,21 +14992,25 @@ var plugin = definePluginEntry({
13010
14992
  });
13011
14993
  api.registerGatewayMethod("aicq.friends.list", async (params) => {
13012
14994
  try {
13013
- const resp = await fetch(serverUrl + "/api/v1/friends?nodeId=" + aicqAgentId);
14995
+ const resp = await fetch(serverUrl + "/api/v1/friends?nodeId=" + aicqAgentId, {
14996
+ headers: serverClient.authHeaders()
14997
+ });
13014
14998
  if (!resp.ok)
13015
14999
  return { error: "Server error: " + await resp.text() };
13016
15000
  const data = await resp.json();
13017
15001
  const friends = data.friends || [];
13018
15002
  const enriched = friends.map((f) => {
13019
- const localFriend = store.getFriend(f.nodeId);
15003
+ const friendId = f.id || f.nodeId;
15004
+ const localFriend = store.getFriend(friendId);
13020
15005
  return {
13021
- id: f.nodeId,
15006
+ id: friendId,
13022
15007
  publicKeyFingerprint: f.publicKeyFingerprint || (localFriend?.publicKeyFingerprint || ""),
13023
15008
  permissions: f.permissions || localFriend?.permissions || [],
13024
15009
  addedAt: f.addedAt || localFriend?.addedAt?.toISOString() || null,
13025
15010
  lastMessageAt: f.lastMessageAt || localFriend?.lastMessageAt?.toISOString() || null,
13026
15011
  friendType: f.friendType || localFriend?.friendType || null,
13027
- aiName: f.aiName || localFriend?.aiName || null
15012
+ aiName: f.aiName || localFriend?.aiName || null,
15013
+ nodeId: f.nodeId || null
13028
15014
  };
13029
15015
  });
13030
15016
  return { friends: enriched };
@@ -13042,7 +15028,9 @@ var plugin = definePluginEntry({
13042
15028
  const isTempNumber = /^\d{6}$/.test(target);
13043
15029
  let friendId = target;
13044
15030
  if (isTempNumber) {
13045
- const resolveResp = await fetch(serverUrl + "/api/v1/temp-number/" + target);
15031
+ const resolveResp = await fetch(serverUrl + "/api/v1/temp-number/" + target, {
15032
+ headers: serverClient.authHeaders()
15033
+ });
13046
15034
  if (!resolveResp.ok)
13047
15035
  return { success: false, message: "Temp number not found or expired" };
13048
15036
  const resolveData = await resolveResp.json();
@@ -13050,7 +15038,7 @@ var plugin = definePluginEntry({
13050
15038
  }
13051
15039
  const hsResp = await fetch(serverUrl + "/api/v1/handshake/initiate", {
13052
15040
  method: "POST",
13053
- headers: { "Content-Type": "application/json" },
15041
+ headers: serverClient.authHeaders(),
13054
15042
  body: JSON.stringify({ requesterId: aicqAgentId, targetTempNumber: target })
13055
15043
  });
13056
15044
  if (!hsResp.ok)
@@ -13076,7 +15064,7 @@ var plugin = definePluginEntry({
13076
15064
  return { success: false, message: "Missing friendId parameter" };
13077
15065
  const rmResp = await fetch(serverUrl + "/api/v1/friends/" + friendId, {
13078
15066
  method: "DELETE",
13079
- headers: { "Content-Type": "application/json" },
15067
+ headers: serverClient.authHeaders(),
13080
15068
  body: JSON.stringify({ nodeId: aicqAgentId })
13081
15069
  });
13082
15070
  if (!rmResp.ok)
@@ -13095,7 +15083,9 @@ var plugin = definePluginEntry({
13095
15083
  const friendId = p.friendId;
13096
15084
  if (!friendId)
13097
15085
  return { error: "Missing friendId parameter" };
13098
- const resp = await fetch(serverUrl + "/api/v1/friends/" + friendId + "/permissions?nodeId=" + aicqAgentId);
15086
+ const resp = await fetch(serverUrl + "/api/v1/friends/" + friendId + "/permissions?nodeId=" + aicqAgentId, {
15087
+ headers: serverClient.authHeaders()
15088
+ });
13099
15089
  if (!resp.ok)
13100
15090
  return { error: "Server error: " + await resp.text() };
13101
15091
  const data = await resp.json();
@@ -13117,7 +15107,7 @@ var plugin = definePluginEntry({
13117
15107
  }
13118
15108
  const resp = await fetch(serverUrl + "/api/v1/friends/" + friendId + "/permissions", {
13119
15109
  method: "PUT",
13120
- headers: { "Content-Type": "application/json" },
15110
+ headers: serverClient.authHeaders(),
13121
15111
  body: JSON.stringify({ nodeId: aicqAgentId, permissions })
13122
15112
  });
13123
15113
  if (!resp.ok)
@@ -13136,7 +15126,9 @@ var plugin = definePluginEntry({
13136
15126
  });
13137
15127
  api.registerGatewayMethod("aicq.friends.requests", async (params) => {
13138
15128
  try {
13139
- const resp = await fetch(serverUrl + "/api/v1/friends/requests?nodeId=" + aicqAgentId);
15129
+ const resp = await fetch(serverUrl + "/api/v1/friends/requests?accountId=" + aicqAgentId, {
15130
+ headers: serverClient.authHeaders()
15131
+ });
13140
15132
  if (!resp.ok)
13141
15133
  return { error: "Server error: " + await resp.text() };
13142
15134
  const data = await resp.json();
@@ -13158,7 +15150,7 @@ var plugin = definePluginEntry({
13158
15150
  }
13159
15151
  const resp = await fetch(serverUrl + "/api/v1/friends/requests/" + requestId + "/accept", {
13160
15152
  method: "POST",
13161
- headers: { "Content-Type": "application/json" },
15153
+ headers: serverClient.authHeaders(),
13162
15154
  body: JSON.stringify(body)
13163
15155
  });
13164
15156
  if (!resp.ok)
@@ -13178,7 +15170,7 @@ var plugin = definePluginEntry({
13178
15170
  return { success: false, message: "Missing requestId parameter" };
13179
15171
  const resp = await fetch(serverUrl + "/api/v1/friends/requests/" + requestId + "/reject", {
13180
15172
  method: "POST",
13181
- headers: { "Content-Type": "application/json" },
15173
+ headers: serverClient.authHeaders(),
13182
15174
  body: JSON.stringify({})
13183
15175
  });
13184
15176
  if (!resp.ok)