aicq-openclaw-plugin 1.3.0 → 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.
Files changed (2) hide show
  1. package/dist/index.js +1994 -369
  2. package/package.json +1 -1
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
  });
@@ -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();
@@ -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),
@@ -7916,7 +7916,7 @@ var PluginStore = class {
7916
7916
  this.offlineMessages = [];
7917
7917
  this.dataDir = "";
7918
7918
  this.storePath = "";
7919
- this.encryptionSalt = crypto3.randomBytes(16);
7919
+ this.encryptionSalt = crypto4.randomBytes(16);
7920
7920
  this.saveTimer = null;
7921
7921
  this.saveDebounceMs = 1e3;
7922
7922
  this.dirty = false;
@@ -8335,7 +8335,7 @@ var PluginStore = class {
8335
8335
  // dist/services/identityService.js
8336
8336
  var import_qrcode = __toESM(require_lib(), 1);
8337
8337
  var import_crypto3 = __toESM(require_dist(), 1);
8338
- import * as crypto4 from "crypto";
8338
+ import * as crypto5 from "crypto";
8339
8339
  var IdentityService = class {
8340
8340
  constructor(store, logger) {
8341
8341
  this.exportTimers = /* @__PURE__ */ new Map();
@@ -8390,7 +8390,7 @@ var IdentityService = class {
8390
8390
  * @returns QR code data URL (string starting with "data:image/png;base64,")
8391
8391
  */
8392
8392
  async exportPrivateKeyQR(password) {
8393
- const exportToken = crypto4.randomBytes(32).toString("hex");
8393
+ const exportToken = crypto5.randomBytes(32).toString("hex");
8394
8394
  const exportPayload = {
8395
8395
  a: this.store.agentId,
8396
8396
  pk: (0, import_crypto3.encodeBase64)(this.store.identityKeys.publicKey),
@@ -8513,10 +8513,13 @@ var DEFAULT_CONFIG = {
8513
8513
  maxReconnectDelay: 6e4,
8514
8514
  reconnectBackoffFactor: 2,
8515
8515
  heartbeatIntervalMs: 3e4,
8516
- requestTimeoutMs: 3e4
8516
+ requestTimeoutMs: 3e4,
8517
+ initialRetryWindowMs: 6e4,
8518
+ hourlyCheckIntervalMs: 36e5
8517
8519
  };
8518
8520
  var ServerClient = class {
8519
8521
  constructor(serverUrl, store, logger, config2) {
8522
+ this.authToken = "";
8520
8523
  this.ws = null;
8521
8524
  this.wsReconnectTimer = null;
8522
8525
  this.heartbeatTimer = null;
@@ -8524,6 +8527,9 @@ var ServerClient = class {
8524
8527
  this.connectionState = "offline";
8525
8528
  this.reconnectDelay = DEFAULT_CONFIG.initialReconnectDelay;
8526
8529
  this.reconnectAttempts = 0;
8530
+ this.connectStartTimestamp = 0;
8531
+ this.hourlyCheckMode = false;
8532
+ this.hourlyCheckTimer = null;
8527
8533
  this.stateChangeCallbacks = [];
8528
8534
  this.wsHandlers = /* @__PURE__ */ new Map();
8529
8535
  this.serverUrl = serverUrl;
@@ -8531,6 +8537,37 @@ var ServerClient = class {
8531
8537
  this.logger = logger;
8532
8538
  this.config = { ...DEFAULT_CONFIG, ...config2 };
8533
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
+ }
8534
8571
  // ----------------------------------------------------------------
8535
8572
  // Connection state management
8536
8573
  // ----------------------------------------------------------------
@@ -8574,12 +8611,17 @@ var ServerClient = class {
8574
8611
  // ----------------------------------------------------------------
8575
8612
  /**
8576
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.
8577
8616
  */
8578
8617
  connectWebSocket() {
8579
8618
  if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
8580
8619
  this.logger.debug("[Server] WebSocket already connecting/connected");
8581
8620
  return;
8582
8621
  }
8622
+ if (this.connectStartTimestamp === 0 && !this.hourlyCheckMode) {
8623
+ this.connectStartTimestamp = Date.now();
8624
+ }
8583
8625
  let wsUrl;
8584
8626
  try {
8585
8627
  const baseUrl = this.serverUrl.replace(/^http/, "ws");
@@ -8611,12 +8653,16 @@ var ServerClient = class {
8611
8653
  this.wsConnected = true;
8612
8654
  this.reconnectDelay = this.config.initialReconnectDelay;
8613
8655
  this.reconnectAttempts = 0;
8656
+ this.connectStartTimestamp = 0;
8657
+ this.hourlyCheckMode = false;
8658
+ this.cancelHourlyCheck();
8614
8659
  this.setConnectionState("online");
8615
8660
  this.logger.info("[Server] WebSocket connected");
8616
8661
  this.wsSend({
8617
8662
  type: "online",
8618
8663
  nodeId: this.store.agentId,
8619
- 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 } : {}
8620
8666
  });
8621
8667
  this.startHeartbeat();
8622
8668
  });
@@ -8642,7 +8688,7 @@ var ServerClient = class {
8642
8688
  });
8643
8689
  }
8644
8690
  /**
8645
- * Disconnect the WebSocket and stop reconnection.
8691
+ * Disconnect the WebSocket and stop all reconnection attempts.
8646
8692
  */
8647
8693
  disconnectWebSocket() {
8648
8694
  this.stopHeartbeat();
@@ -8652,6 +8698,7 @@ var ServerClient = class {
8652
8698
  this.ws = null;
8653
8699
  }
8654
8700
  this.wsConnected = false;
8701
+ this.connectStartTimestamp = 0;
8655
8702
  this.setConnectionState("offline");
8656
8703
  }
8657
8704
  /**
@@ -8681,15 +8728,30 @@ var ServerClient = class {
8681
8728
  return false;
8682
8729
  }
8683
8730
  // ----------------------------------------------------------------
8684
- // Exponential backoff reconnection
8731
+ // Reconnection: try for 1 min, then hourly
8685
8732
  // ----------------------------------------------------------------
8686
8733
  /**
8687
- * Schedule a reconnection attempt with exponential backoff.
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.
8688
8739
  *
8689
- * Delay formula: min(initialDelay * backoffFactor^attempts, maxDelay)
8690
- * With jitter: delay * (0.75 + Math.random() * 0.5) to avoid thundering herd
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.
8691
8744
  */
8692
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
+ }
8693
8755
  if (this.wsReconnectTimer)
8694
8756
  return;
8695
8757
  const jitter = 0.75 + Math.random() * 0.5;
@@ -8704,6 +8766,36 @@ var ServerClient = class {
8704
8766
  }, delay);
8705
8767
  this.reconnectDelay = Math.min(this.reconnectDelay * this.config.reconnectBackoffFactor, this.config.maxReconnectDelay);
8706
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
+ }
8707
8799
  /**
8708
8800
  * Cancel a pending reconnection.
8709
8801
  */
@@ -8712,20 +8804,27 @@ var ServerClient = class {
8712
8804
  clearTimeout(this.wsReconnectTimer);
8713
8805
  this.wsReconnectTimer = null;
8714
8806
  }
8807
+ this.cancelHourlyCheck();
8715
8808
  this.reconnectDelay = this.config.initialReconnectDelay;
8716
8809
  this.reconnectAttempts = 0;
8810
+ this.connectStartTimestamp = 0;
8717
8811
  }
8718
8812
  // ----------------------------------------------------------------
8719
8813
  // REST API methods
8720
8814
  // ----------------------------------------------------------------
8721
8815
  /**
8722
8816
  * Register this node on the server.
8817
+ * Captures JWT token from response if returned.
8723
8818
  */
8724
8819
  async registerNode(agentId, publicKey) {
8725
- return this.post("/api/v1/node/register", {
8820
+ const res = await this.fetchPost("/api/v1/node/register", {
8726
8821
  id: agentId,
8727
8822
  publicKey: Buffer.from(publicKey).toString("base64")
8728
8823
  });
8824
+ if (res?.token) {
8825
+ this.setAuthToken(res.token);
8826
+ }
8827
+ return res?.ok ?? false;
8729
8828
  }
8730
8829
  /**
8731
8830
  * Request a temporary 6-digit number for friend discovery.
@@ -8848,12 +8947,15 @@ var ServerClient = class {
8848
8947
  const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
8849
8948
  const resp = await fetch(url, {
8850
8949
  method: "POST",
8851
- headers: { "Content-Type": "application/json" },
8950
+ headers: this.authHeaders(),
8852
8951
  body: JSON.stringify(body),
8853
8952
  signal: controller.signal
8854
8953
  });
8855
8954
  clearTimeout(timeout);
8856
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
+ }
8857
8959
  const text = await resp.text();
8858
8960
  this.logger.error(`[Server] API error ${resp.status} on ${path7}: ${text}`);
8859
8961
  return null;
@@ -8873,7 +8975,10 @@ var ServerClient = class {
8873
8975
  try {
8874
8976
  const controller = new AbortController();
8875
8977
  const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
8876
- const resp = await fetch(url, { signal: controller.signal });
8978
+ const resp = await fetch(url, {
8979
+ signal: controller.signal,
8980
+ headers: this.authToken ? { Authorization: "Bearer " + this.authToken } : {}
8981
+ });
8877
8982
  clearTimeout(timeout);
8878
8983
  if (!resp.ok) {
8879
8984
  const text = await resp.text();
@@ -8897,7 +9002,7 @@ var ServerClient = class {
8897
9002
  const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
8898
9003
  const resp = await fetch(url, {
8899
9004
  method: "DELETE",
8900
- headers: { "Content-Type": "application/json" },
9005
+ headers: this.authHeaders(),
8901
9006
  body: body ? JSON.stringify(body) : void 0,
8902
9007
  signal: controller.signal
8903
9008
  });
@@ -8915,7 +9020,7 @@ var ServerClient = class {
8915
9020
  const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
8916
9021
  const resp = await fetch(url, {
8917
9022
  method: "POST",
8918
- headers: { "Content-Type": "application/json" },
9023
+ headers: this.authHeaders(),
8919
9024
  body: JSON.stringify(body),
8920
9025
  signal: controller.signal
8921
9026
  });
@@ -8955,7 +9060,7 @@ var ServerClient = class {
8955
9060
 
8956
9061
  // dist/handshake/handshakeManager.js
8957
9062
  var import_crypto4 = __toESM(require_dist(), 1);
8958
- import * as crypto5 from "crypto";
9063
+ import * as crypto6 from "crypto";
8959
9064
  var HandshakeManager = class {
8960
9065
  constructor(store, serverClient, config2, logger) {
8961
9066
  this.pendingInitiates = /* @__PURE__ */ new Map();
@@ -9173,7 +9278,7 @@ var HandshakeManager = class {
9173
9278
  combined.set(ee, 0);
9174
9279
  combined.set(se, 32);
9175
9280
  combined.set(es, 64);
9176
- const nonce = crypto5.randomBytes(16).toString("hex");
9281
+ const nonce = crypto6.randomBytes(16).toString("hex");
9177
9282
  const newSessionKey = (0, import_crypto4.deriveSessionKey)(combined, "aicq-session-rotate-" + nonce);
9178
9283
  const updatedSession = {
9179
9284
  peerId,
@@ -10187,17 +10292,17 @@ var MessageSendingHook = class {
10187
10292
  var CSS = `
10188
10293
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
10189
10294
  :root {
10190
- --bg: #0f1117; --bg2: #1a1d27; --bg3: #242836; --bg4: #2e3347;
10191
- --bg5: #353a50; --text: #e4e6ef; --text2: #9499b3; --text3: #5c6080;
10192
- --accent: #6366f1; --accent2: #818cf8; --accent-bg: rgba(99,102,241,.12);
10193
- --ok: #34d399; --ok-bg: rgba(52,211,153,.12); --warn: #fbbf24; --warn-bg: rgba(251,191,36,.12);
10194
- --danger: #ef4444; --danger-bg: rgba(239,68,68,.12); --info: #60a5fa; --info-bg: rgba(96,165,250,.12);
10195
- --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);
10196
10301
  --sidebar-w: 240px; --header-h: 56px;
10197
10302
  --transition: .2s cubic-bezier(.4,0,.2,1);
10198
10303
  }
10199
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; }
10200
- a { color: var(--info); text-decoration: none; }
10305
+ a { color: var(--accent); text-decoration: none; }
10201
10306
  ::-webkit-scrollbar { width: 6px; height: 6px; }
10202
10307
  ::-webkit-scrollbar-track { background: transparent; }
10203
10308
  ::-webkit-scrollbar-thumb { background: var(--bg4); border-radius: 3px; }
@@ -10209,7 +10314,7 @@ a { color: var(--info); text-decoration: none; }
10209
10314
  /* Sidebar */
10210
10315
  .sidebar {
10211
10316
  width: var(--sidebar-w); min-width: var(--sidebar-w); height: 100vh;
10212
- background: var(--bg2); border-right: 1px solid var(--border);
10317
+ background: #ffffff; border-right: 1px solid var(--border);
10213
10318
  display: flex; flex-direction: column; transition: width var(--transition), min-width var(--transition);
10214
10319
  z-index: 20; overflow: hidden;
10215
10320
  }
@@ -10224,7 +10329,7 @@ a { color: var(--info); text-decoration: none; }
10224
10329
  border-bottom: 1px solid var(--border); min-height: var(--header-h);
10225
10330
  }
10226
10331
  .sidebar-logo {
10227
- 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);
10228
10333
  display: grid; place-items: center; font-size: 13px; font-weight: 800; color: #fff; flex-shrink: 0;
10229
10334
  }
10230
10335
  .sidebar-header-text h1 { font-size: 14px; font-weight: 700; line-height: 1.2; }
@@ -10263,7 +10368,7 @@ a { color: var(--info); text-decoration: none; }
10263
10368
  .main-header {
10264
10369
  height: var(--header-h); min-height: var(--header-h);
10265
10370
  display: flex; align-items: center; gap: 16px; padding: 0 24px;
10266
- background: var(--bg2); border-bottom: 1px solid var(--border);
10371
+ background: #ffffff; border-bottom: 1px solid var(--border);
10267
10372
  }
10268
10373
  .toggle-btn {
10269
10374
  width: 32px; height: 32px; border-radius: 6px; background: var(--bg3);
@@ -10295,12 +10400,12 @@ a { color: var(--info); text-decoration: none; }
10295
10400
  .btn-default:hover:not(:disabled) { background: var(--bg4); }
10296
10401
  .btn-primary { background: var(--accent); color: #fff; }
10297
10402
  .btn-primary:hover:not(:disabled) { background: var(--accent2); }
10298
- .btn-danger { background: var(--danger-bg); color: #fca5a5; border: 1px solid rgba(239,68,68,.2); }
10299
- .btn-danger:hover:not(:disabled) { background: rgba(239,68,68,.2); }
10300
- .btn-ok { background: var(--ok-bg); color: #6ee7b7; border: 1px solid rgba(52,211,153,.2); }
10301
- .btn-ok:hover:not(:disabled) { background: rgba(52,211,153,.2); }
10302
- .btn-warn { background: var(--warn-bg); color: #fde68a; border: 1px solid rgba(251,191,36,.2); }
10303
- .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); }
10304
10409
  .btn-ghost { background: transparent; color: var(--text2); }
10305
10410
  .btn-ghost:hover:not(:disabled) { background: var(--bg3); color: var(--text); }
10306
10411
  .btn-sm { padding: 4px 10px; font-size: 12px; }
@@ -10373,17 +10478,18 @@ tbody tr:hover { background: var(--bg3); }
10373
10478
  .provider-card .prov-desc { font-size: 12px; color: var(--text3); margin-bottom: 10px; }
10374
10479
  .provider-card .prov-model { font-size: 11px; color: var(--text2); background: var(--bg3); padding: 3px 8px; border-radius: 4px; display: inline-block; }
10375
10480
  .provider-card .prov-actions { margin-top: 12px; display: flex; gap: 6px; }
10481
+ .provider-card.custom-provider { border-color: var(--accent); border-style: dashed; }
10376
10482
 
10377
10483
  /* Modal */
10378
10484
  .modal-overlay {
10379
- 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;
10380
10486
  align-items: center; justify-content: center; z-index: 100;
10381
10487
  animation: fadeIn .15s ease-out;
10382
10488
  }
10383
10489
  .modal-overlay.hidden { display: none; }
10384
10490
  .modal {
10385
10491
  background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius-lg);
10386
- 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);
10387
10493
  max-height: 85vh; overflow-y: auto; animation: modalIn .2s ease-out;
10388
10494
  }
10389
10495
  @keyframes modalIn { from { transform: scale(.95); opacity: 0; } to { transform: scale(1); opacity: 1; } }
@@ -10419,15 +10525,15 @@ tbody tr:hover { background: var(--bg3); }
10419
10525
  /* Toast */
10420
10526
  .toast-container { position: fixed; bottom: 20px; right: 20px; z-index: 200; display: flex; flex-direction: column; gap: 8px; }
10421
10527
  .toast {
10422
- padding: 12px 20px; border-radius: var(--radius); color: #fff; font-size: 13px;
10423
- 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;
10424
10530
  max-width: 400px;
10425
10531
  }
10426
10532
  .toast.hidden { display: none; }
10427
- .toast-ok { background: #065f46; border: 1px solid var(--ok); }
10428
- .toast-err { background: #7f1d1d; border: 1px solid var(--danger); }
10429
- .toast-info { background: #1e3a5f; border: 1px solid var(--info); }
10430
- .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; }
10431
10537
  @keyframes slideIn { from { transform: translateX(20px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
10432
10538
 
10433
10539
  /* Actions cell */
@@ -10459,8 +10565,9 @@ tbody tr:hover { background: var(--bg3); }
10459
10565
 
10460
10566
  /* Offline banner */
10461
10567
  .offline-banner {
10462
- background: linear-gradient(90deg, #7f1d1d, #991b1b);
10463
- color: #fca5a5;
10568
+ background: #fef2f2;
10569
+ color: #991b1b;
10570
+ border-bottom: 1px solid #fecaca;
10464
10571
  padding: 10px 24px;
10465
10572
  font-size: 13px;
10466
10573
  display: flex;
@@ -10488,6 +10595,380 @@ tbody tr:hover { background: var(--bg3); }
10488
10595
  }
10489
10596
  `;
10490
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
+
10491
10972
  // \u2500\u2500 Globals \u2500\u2500
10492
10973
  const API = '/api';
10493
10974
  let currentPage = 'dashboard';
@@ -10513,7 +10994,7 @@ function showOfflineBanner() {
10513
10994
  if (offlineBannerEl) return;
10514
10995
  offlineBannerEl = document.createElement('div');
10515
10996
  offlineBannerEl.className = 'offline-banner';
10516
- offlineBannerEl.innerHTML = '<span class="offline-icon">\u{1F50C}</span><span>You are offline. Some features may be limited. Data is loaded from local cache.</span>';
10997
+ offlineBannerEl.innerHTML = '<span class="offline-icon">\u{1F50C}</span><span>' + t('offline_msg') + '</span>';
10517
10998
  const mainContent = document.querySelector('.main');
10518
10999
  if (mainContent) {
10519
11000
  mainContent.insertBefore(offlineBannerEl, mainContent.firstChild);
@@ -10571,13 +11052,13 @@ function escHtml(s) { if (s == null) return ''; const d = document.createElement
10571
11052
  function timeAgo(iso) {
10572
11053
  if (!iso) return '\u2014';
10573
11054
  const diff = Date.now() - new Date(iso).getTime();
10574
- if (diff < 0) return 'just now';
11055
+ if (diff < 0) return t('just_now');
10575
11056
  const m = Math.floor(diff / 60000), h = Math.floor(m / 60), d = Math.floor(h / 24);
10576
- 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');
10577
11058
  return new Date(iso).toLocaleDateString();
10578
11059
  }
10579
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); }
10580
- 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')); }
10581
11062
 
10582
11063
  // \u2500\u2500 Modal \u2500\u2500
10583
11064
  function showModal(id) { show(id); }
@@ -10607,6 +11088,7 @@ function loadPage(page) {
10607
11088
  case 'friends': loadFriends(); break;
10608
11089
  case 'models': loadModels(); break;
10609
11090
  case 'settings': loadSettings(); break;
11091
+ case 'openclaw': loadOpenClawConfig(); break;
10610
11092
  }
10611
11093
  }
10612
11094
 
@@ -10615,15 +11097,15 @@ function loadPage(page) {
10615
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
10616
11098
  async function loadDashboard() {
10617
11099
  const el = $('#dashboard-content');
10618
- 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>');
10619
11101
  const results = await Promise.allSettled([api('/status'), api('/friends'), api('/identity'), api('/mgmt-url')]);
10620
11102
  const status = results[0].status === 'fulfilled' ? results[0].value : { error: results[0].reason?.message || 'Failed' };
10621
11103
  const friends = results[1].status === 'fulfilled' ? results[1].value : { friends: [], error: true };
10622
11104
  const identity = results[2].status === 'fulfilled' ? results[2].value : { agentId: '\u2014', publicKeyFingerprint: '\u2014', serverUrl: '\u2014', connected: false };
10623
11105
  const mgmtUrl = results[3].status === 'fulfilled' ? results[3].value : { mgmtUrl: window.location.origin };
10624
- if (status.error) { html(el, '<div class="empty"><div class="icon">\u26A0\uFE0F</div><p>Failed to connect to AICQ plugin</p></div>'); return; }
11106
+ if (status.error) { html(el, '<div class="empty"><div class="icon">\u26A0\uFE0F</div><p>' + t('failed_connect') + '</p></div>'); return; }
10625
11107
  const connCls = status.connected ? 'dot-ok' : 'dot-err';
10626
- const connText = status.connected ? 'Connected' : 'Disconnected';
11108
+ const connText = status.connected ? t('connected') : t('disconnected');
10627
11109
  const friendList = friends.friends || [];
10628
11110
  const aiFriends = friendList.filter(f => f.friendType === 'ai').length;
10629
11111
  const humanFriends = friendList.filter(f => f.friendType !== 'ai').length;
@@ -10633,7 +11115,7 @@ async function loadDashboard() {
10633
11115
  <div class="stats-grid">
10634
11116
  <div class="stat-card">
10635
11117
  <div class="stat-icon" style="background:var(--accent-bg)">\u{1F4E1}</div>
10636
- <div class="stat-label">Server Status</div>
11118
+ <div class="stat-label">\${t('server_status')}</div>
10637
11119
  <div class="stat-value" style="font-size:16px;display:flex;align-items:center;gap:8px">
10638
11120
  <span class="dot \${connCls}"></span> \${connText}
10639
11121
  </div>
@@ -10641,42 +11123,42 @@ async function loadDashboard() {
10641
11123
  </div>
10642
11124
  <div class="stat-card">
10643
11125
  <div class="stat-icon" style="background:var(--ok-bg)">\u{1F465}</div>
10644
- <div class="stat-label">Total Friends</div>
11126
+ <div class="stat-label">\${t('total_friends')}</div>
10645
11127
  <div class="stat-value">\${friendList.length}</div>
10646
11128
  <div class="stat-sub">\${aiFriends} AI \xB7 \${humanFriends} Human</div>
10647
11129
  </div>
10648
11130
  <div class="stat-card">
10649
11131
  <div class="stat-icon" style="background:var(--info-bg)">\u{1F517}</div>
10650
- <div class="stat-label">Active Sessions</div>
11132
+ <div class="stat-label">\${t('active_sessions')}</div>
10651
11133
  <div class="stat-value">\${status.sessionCount || 0}</div>
10652
- <div class="stat-sub">Encrypted sessions</div>
11134
+ <div class="stat-sub">\${t('encrypted_sessions')}</div>
10653
11135
  </div>
10654
11136
  <div class="stat-card">
10655
11137
  <div class="stat-icon" style="background:var(--warn-bg)">\u{1F511}</div>
10656
- <div class="stat-label">Agent ID</div>
11138
+ <div class="stat-label">\${t('agent_id')}</div>
10657
11139
  <div class="stat-value mono" style="font-size:13px">\${escHtml(status.agentId)}</div>
10658
- <div class="stat-sub">Fingerprint: \${escHtml(status.fingerprint)}</div>
11140
+ <div class="stat-sub">\${t('fingerprint')}: \${escHtml(status.fingerprint)}</div>
10659
11141
  </div>
10660
11142
  </div>
10661
11143
  <div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
10662
11144
  <div class="card">
10663
- <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>
10664
11146
  \${renderMiniFriendList(friendList.slice(0, 5))}
10665
11147
  </div>
10666
11148
  <div class="card">
10667
- <div class="card-header"><div class="card-title">\u{1F916} Identity Info</div></div>
10668
- <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>
10669
- <div class="detail-row"><div class="detail-key">Fingerprint</div><div class="detail-val mono">\${escHtml(identity.publicKeyFingerprint)}</div></div>
10670
- <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>
10671
- <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>
10672
- <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>
10673
11155
  </div>
10674
11156
  </div>
10675
11157
  <div class="card" style="margin-top:0">
10676
- <div class="card-header"><div class="card-title">\u{1F5A5}\uFE0F Management UI Access</div></div>
10677
- <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>
10678
- <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>
10679
- <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>
10680
11162
  </div>
10681
11163
  \\\`);
10682
11164
 
@@ -10686,7 +11168,7 @@ async function loadDashboard() {
10686
11168
  }
10687
11169
 
10688
11170
  function renderMiniFriendList(friends) {
10689
- 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>';
10690
11172
  let html = '';
10691
11173
  friends.forEach(f => {
10692
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>';
@@ -10699,7 +11181,7 @@ function renderMiniFriendList(friends) {
10699
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
10700
11182
  async function loadAgents() {
10701
11183
  const el = $('#agents-content');
10702
- 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>');
10703
11185
  const data = await api('/agents');
10704
11186
  if (data.error) { html(el, '<div class="empty"><div class="icon">\u26A0\uFE0F</div><p>' + escHtml(data.error) + '</p></div>'); return; }
10705
11187
 
@@ -10709,13 +11191,16 @@ async function loadAgents() {
10709
11191
 
10710
11192
  let rows = '';
10711
11193
  agents.forEach((a, i) => {
10712
- const modelBadge = a.model ? '<span class="badge badge-accent">' + escHtml(a.model) + '</span>' : '<span class="badge badge-ghost">default</span>';
10713
- const providerBadge = a.provider ? '<span class="tag">' + escHtml(a.provider) + '</span>' : '';
10714
- 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>' : '';
10715
11200
 
10716
11201
  rows += \\\`<tr>
10717
- <td>\${statusBadge}</td>
10718
- <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>
10719
11204
  <td>\${modelBadge}</td>
10720
11205
  <td>\${providerBadge}</td>
10721
11206
  <td>\${escHtml(a.systemPrompt ? a.systemPrompt.substring(0, 60) + '...' : '\u2014')}</td>
@@ -10731,23 +11216,23 @@ async function loadAgents() {
10731
11216
 
10732
11217
  if (!agents.length) {
10733
11218
  html(el, \\\`
10734
- <p class="section-desc">Reads agent configurations from <strong>\${escHtml(configSource)}</strong>. Configure your agents in the config file.</p>
10735
- <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>
10736
11221
  \\\`);
10737
11222
  return;
10738
11223
  }
10739
11224
 
10740
11225
  html(el, \\\`
10741
11226
  <div class="toolbar">
10742
- <div class="search-box"><input type="text" placeholder="Search agents..." id="agent-search" oninput="filterAgentTable()"></div>
10743
- <button class="btn btn-sm btn-primary" onclick="showAddAgentModal()">\u2795 Add Agent</button>
10744
- <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>
10745
11230
  </div>
10746
- <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>
10747
11232
  <div class="card" style="padding:0;overflow:hidden">
10748
11233
  <div style="overflow-x:auto">
10749
11234
  <table>
10750
- <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>
10751
11236
  <tbody id="agent-table-body">\${rows}</tbody>
10752
11237
  </table>
10753
11238
  </div>
@@ -10755,6 +11240,17 @@ async function loadAgents() {
10755
11240
  \\\`);
10756
11241
  }
10757
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
+
10758
11254
  function filterAgentTable() {
10759
11255
  const q = ($('#agent-search')?.value || '').toLowerCase();
10760
11256
  $$('#agent-table-body tr').forEach(tr => {
@@ -10773,23 +11269,34 @@ function viewAgent(index) {
10773
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>';
10774
11270
  }
10775
11271
  }
10776
- html('#view-agent-body', details || '<div class="empty"><p>No data</p></div>');
10777
- $('#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');
10778
11274
  showModal('modal-view-agent');
10779
11275
  }
10780
11276
 
10781
11277
  async function deleteAgent(index) {
10782
- if (!confirm('Are you sure you want to delete this agent?')) return;
10783
- const r = await api('/agents/' + index, { method: 'DELETE' });
10784
- if (r.success) { toast('Agent deleted', 'ok'); loadAgents(); }
10785
- 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'); }
10786
11291
  }
10787
11292
 
10788
11293
  let _editAgentIndex = null;
11294
+ let _editAgentIsProviderModel = false;
10789
11295
 
10790
11296
  function showAddAgentModal() {
10791
11297
  _editAgentIndex = null;
10792
- $('#agent-form-title').textContent = '\u2795 Add New Agent';
11298
+ _editAgentIsProviderModel = false;
11299
+ $('#agent-form-title').textContent = t('add_new_agent');
10793
11300
  $('#agent-form-name').value = '';
10794
11301
  $('#agent-form-id').value = '';
10795
11302
  $('#agent-form-model').value = '';
@@ -10809,9 +11316,10 @@ function showEditAgentModal(index) {
10809
11316
  const a = agents[index];
10810
11317
  if (!a) return;
10811
11318
  _editAgentIndex = index;
10812
- $('#agent-form-title').textContent = '\u270F\uFE0F Edit Agent';
11319
+ _editAgentIsProviderModel = a._source === 'provider-model';
11320
+ $('#agent-form-title').textContent = t('edit_agent');
10813
11321
  $('#agent-form-name').value = a.name || '';
10814
- $('#agent-form-id').value = a.id || '';
11322
+ $('#agent-form-id').value = a._source === 'provider-model' ? (a._configPath || '') : (a.id || '');
10815
11323
  $('#agent-form-model').value = a.model || '';
10816
11324
  $('#agent-form-provider').value = a.provider || '';
10817
11325
  $('#agent-form-prompt').value = a.systemPrompt || '';
@@ -10820,6 +11328,13 @@ function showEditAgentModal(index) {
10820
11328
  $('#agent-form-max-tokens').value = a.maxTokens ?? 4096;
10821
11329
  $('#agent-form-top-p').value = a.topP ?? 1;
10822
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
+ }
10823
11338
  showModal('modal-add-agent');
10824
11339
  }
10825
11340
 
@@ -10842,23 +11357,29 @@ async function saveAgent() {
10842
11357
  tools: toolsRaw ? toolsRaw.split(',').map(t => t.trim()).filter(Boolean) : [],
10843
11358
  };
10844
11359
 
10845
- if (!agent.name) { toast('Agent name is required', 'warn'); return; }
11360
+ if (!agent.name) { toast(t('agent_name_required'), 'warn'); return; }
10846
11361
 
10847
11362
  let r;
10848
11363
  if (_editAgentIndex !== null) {
10849
- // Edit existing
10850
- 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 }) });
10851
11372
  } else {
10852
11373
  // Add new
10853
11374
  r = await api('/agents', { method: 'POST', body: JSON.stringify({ agent }) });
10854
11375
  }
10855
11376
 
10856
11377
  if (r.success) {
10857
- toast(_editAgentIndex !== null ? 'Agent updated' : 'Agent added', 'ok');
11378
+ toast(_editAgentIndex !== null ? t('agent_updated') : t('agent_added'), 'ok');
10858
11379
  hideModal('modal-add-agent');
10859
11380
  loadAgents();
10860
11381
  } else {
10861
- toast(r.message || r.error || 'Failed', 'err');
11382
+ toast(r.message || r.error || t('failed'), 'err');
10862
11383
  }
10863
11384
  }
10864
11385
 
@@ -10869,7 +11390,7 @@ let friendsFilter = 'all';
10869
11390
 
10870
11391
  async function loadFriends() {
10871
11392
  const el = $('#friends-content');
10872
- 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>');
10873
11394
  const results = await Promise.allSettled([api('/friends'), api('/friends/requests'), api('/sessions')]);
10874
11395
  const friends = results[0].status === 'fulfilled' ? results[0].value : { friends: [] };
10875
11396
  const requests = results[1].status === 'fulfilled' ? results[1].value : { requests: [] };
@@ -10888,9 +11409,9 @@ async function loadFriends() {
10888
11409
  const sessCount = (sessions.sessions || []).length;
10889
11410
 
10890
11411
  html('#friends-tabs', \\\`
10891
- <button class="filter-btn \${friendsSubTab==='friends'?'active':''}" onclick="friendsSubTab='friends';loadFriends()">\u{1F465} Friends (<span id="fc">\${friendCount}</span>)</button>
10892
- <button class="filter-btn \${friendsSubTab==='requests'?'active':''}" onclick="friendsSubTab='requests';loadFriends()">\u{1F4E8} Requests (<span id="rc">\${reqCount}</span>)</button>
10893
- <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>
10894
11415
  \\\`);
10895
11416
 
10896
11417
  window._friendsData = friends;
@@ -10925,24 +11446,24 @@ function renderFriendsList(friends) {
10925
11446
 
10926
11447
  html(el, \\\`
10927
11448
  <div class="toolbar">
10928
- <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>
10929
11450
  <div class="filter-group">
10930
- <button class="filter-btn \${friendsFilter==='all'?'active':''}" onclick="friendsFilter='all';filterFriendTable()">All</button>
10931
- <button class="filter-btn \${friendsFilter==='ai'?'active':''}" onclick="friendsFilter='ai';filterFriendTable()">AI</button>
10932
- <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>
10933
11454
  </div>
10934
11455
  <span style="flex:1"></span>
10935
- <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>
10936
11457
  <button class="btn btn-sm btn-default" onclick="loadFriends()">\u{1F504}</button>
10937
11458
  </div>
10938
11459
  <div class="card" style="padding:0;overflow:hidden">
10939
11460
  <div style="overflow-x:auto;max-height:calc(100vh - 280px);overflow-y:auto">
10940
11461
  <table>
10941
- <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>
10942
11463
  <tbody id="friend-table-body">\${rows}</tbody>
10943
11464
  </table>
10944
11465
  </div>
10945
- \${!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>' : ''}
10946
11467
  </div>
10947
11468
  \\\`);
10948
11469
  }
@@ -10967,18 +11488,18 @@ function renderRequestsList(requests) {
10967
11488
  <td><span class="badge badge-\${stCls}">\${escHtml(r.status)}</span></td>
10968
11489
  <td>\${timeAgo(r.createdAt)}</td>
10969
11490
  <td>
10970
- \${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'}
10971
11492
  </td>
10972
11493
  </tr>\\\`;
10973
11494
  });
10974
11495
  html(el, \\\`
10975
- <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>
10976
11497
  <div class="card" style="padding:0;overflow:hidden">
10977
11498
  <div style="overflow-x:auto"><table>
10978
- <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>
10979
11500
  <tbody>\${rows}</tbody>
10980
11501
  </table></div>
10981
- \${!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>' : ''}
10982
11503
  </div>
10983
11504
  \\\`);
10984
11505
  }
@@ -10990,17 +11511,17 @@ function renderSessionsList(sessions) {
10990
11511
  rows += \\\`<tr>
10991
11512
  <td class="mono" style="font-size:12px;cursor:pointer" onclick="copyText('\${escHtml(s.peerId)}')">\${escHtml(s.peerId)} \u{1F4CB}</td>
10992
11513
  <td>\${timeAgo(s.createdAt)}</td>
10993
- <td><span class="badge badge-info">\${s.messageCount} messages</span></td>
11514
+ <td><span class="badge badge-info">\${s.messageCount} \${t('messages')}</span></td>
10994
11515
  </tr>\\\`;
10995
11516
  });
10996
11517
  html(el, \\\`
10997
- <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>
10998
11519
  <div class="card" style="padding:0;overflow:hidden">
10999
11520
  <div style="overflow-x:auto"><table>
11000
- <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>
11001
11522
  <tbody>\${rows}</tbody>
11002
11523
  </table></div>
11003
- \${!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>' : ''}
11004
11525
  </div>
11005
11526
  \\\`);
11006
11527
  }
@@ -11008,18 +11529,18 @@ function renderSessionsList(sessions) {
11008
11529
  function showAddFriendModal() { $('#add-friend-target').value = ''; showModal('modal-add-friend'); setTimeout(() => $('#add-friend-target')?.focus(), 100); }
11009
11530
  async function addFriend() {
11010
11531
  const target = $('#add-friend-target').value.trim();
11011
- if (!target) { toast('Enter a temp number or node ID', 'warn'); return; }
11532
+ if (!target) { toast(t('enter_temp_or_id'), 'warn'); return; }
11012
11533
  hideModal('modal-add-friend');
11013
- toast('Sending friend request...', 'info');
11534
+ toast(t('sending_request'), 'info');
11014
11535
  const r = await api('/friends', { method: 'POST', body: JSON.stringify({ target }) });
11015
- if (r.success) { toast(r.message || 'Friend request sent!', 'ok'); loadFriends(); }
11016
- 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'); }
11017
11538
  }
11018
11539
  async function removeFriend(id) {
11019
- if (!confirm('Remove friend ' + id + '?')) return;
11540
+ if (!confirm(t('remove_friend_confirm') + id + '?')) return;
11020
11541
  const r = await api('/friends/' + encodeURIComponent(id), { method: 'DELETE' });
11021
- if (r.success) { toast('Friend removed', 'ok'); loadFriends(); }
11022
- 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'); }
11023
11544
  }
11024
11545
 
11025
11546
  let _editFriendId = null;
@@ -11034,16 +11555,16 @@ async function saveFriendPerms() {
11034
11555
  if ($('#perm-chat').checked) perms.push('chat');
11035
11556
  if ($('#perm-exec').checked) perms.push('exec');
11036
11557
  const r = await api('/friends/' + encodeURIComponent(_editFriendId) + '/permissions', { method: 'PUT', body: JSON.stringify({ permissions: perms }) });
11037
- if (r.success) { toast('Permissions updated', 'ok'); hideModal('modal-permissions'); loadFriends(); }
11038
- 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'); }
11039
11560
  }
11040
11561
  async function acceptFriendReq(id) {
11041
11562
  const r = await api('/friends/requests/' + encodeURIComponent(id) + '/accept', { method: 'POST', body: JSON.stringify({ permissions: ['chat'] }) });
11042
- 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'); }
11043
11564
  }
11044
11565
  async function rejectFriendReq(id) {
11045
11566
  const r = await api('/friends/requests/' + encodeURIComponent(id) + '/reject', { method: 'POST', body: JSON.stringify({}) });
11046
- 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'); }
11047
11568
  }
11048
11569
 
11049
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
@@ -11053,36 +11574,81 @@ let _modelProviders = null;
11053
11574
 
11054
11575
  async function loadModels() {
11055
11576
  const el = $('#models-content');
11056
- 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>');
11057
11578
  const data = await api('/models');
11058
11579
  if (data.error) { html(el, '<div class="empty"><div class="icon">\u26A0\uFE0F</div><p>' + escHtml(data.error) + '</p></div>'); return; }
11059
11580
  _modelProviders = data;
11060
11581
  renderModels(data);
11061
11582
  }
11062
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
+
11063
11595
  function renderModels(data) {
11064
11596
  const el = $('#models-content');
11065
11597
  const providers = data.providers || [];
11066
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
+ }
11067
11614
 
11068
11615
  let cards = '';
11069
11616
  providers.forEach(p => {
11070
- 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);
11071
11618
  const statusBadge = p.configured
11072
- ? '<span class="badge badge-ok">\u25CF Configured</span>'
11073
- : '<span class="badge badge-ghost">Not set</span>';
11074
- 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
+ }
11075
11637
 
11076
11638
  cards += \\\`
11077
- <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) + "')"}">
11078
11640
  <div class="prov-head">
11079
- <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>
11080
11642
  \${statusBadge}
11081
11643
  </div>
11082
- <div class="prov-desc">\${escHtml(p.description)}</div>
11083
- \${currentModel}
11644
+ <div class="prov-desc">\${escHtml(p.description || '')}</div>
11645
+ \${modelInfo}
11084
11646
  <div class="prov-actions">
11085
- <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
+ }
11086
11652
  </div>
11087
11653
  </div>\\\`;
11088
11654
  });
@@ -11091,14 +11657,16 @@ function renderModels(data) {
11091
11657
  if (data.currentModels && data.currentModels.length) {
11092
11658
  let rows = '';
11093
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>' : '';
11094
11661
  rows += \\\`<tr>
11095
11662
  <td style="font-weight:500">\${escHtml(m.provider)}</td>
11096
- <td class="mono">\${escHtml(m.modelId)}</td>
11097
- <td><span class="badge badge-ok">\u25CF Key set</span></td>
11098
- <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>
11099
11667
  <td>
11100
11668
  <div class="actions-cell">
11101
- <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>
11102
11670
  <button class="btn btn-sm btn-danger" onclick="deleteModelProvider('\${escHtml(m.providerId)}')" title="Delete">\u{1F5D1}\uFE0F</button>
11103
11671
  </div>
11104
11672
  </td>
@@ -11106,61 +11674,174 @@ function renderModels(data) {
11106
11674
  });
11107
11675
  activeModelsSection = \\\`
11108
11676
  <div class="card" style="margin-top:20px">
11109
- <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>
11110
11678
  <div style="overflow-x:auto"><table>
11111
- <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>
11112
11680
  <tbody>\${rows}</tbody>
11113
11681
  </table></div>
11114
11682
  </div>\\\`;
11115
11683
  }
11116
11684
 
11117
11685
  html(el, \\\`
11686
+ \${defaultBanner}
11118
11687
  <div class="stats-grid" style="margin-bottom:24px">
11119
11688
  <div class="stat-card">
11120
11689
  <div class="stat-icon" style="background:var(--accent-bg)">\u{1F9E0}</div>
11121
- <div class="stat-label">Configured</div>
11690
+ <div class="stat-label">\${t('configured')}</div>
11122
11691
  <div class="stat-value">\${configured} / \${providers.length}</div>
11123
- <div class="stat-sub">Providers with API keys</div>
11692
+ <div class="stat-sub">\${t('providers_with_keys')}</div>
11124
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>
11125
11701
  </div>
11126
- <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>
11127
11702
  <div class="provider-grid">\${cards}</div>
11128
11703
  \${activeModelsSection}
11129
11704
  \\\`);
11130
11705
  }
11131
11706
 
11132
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
+
11133
11791
  function showModelConfigModal(id) {
11134
11792
  const p = (_modelProviders?.providers || []).find(x => x.id === id);
11135
- if (!p) { toast('Provider not found', 'err'); return; }
11793
+ if (!p) { toast(t('provider_not_found'), 'err'); return; }
11136
11794
  _editProviderId = id;
11137
11795
  $('#model-name').textContent = p.name;
11138
- $('#model-icon').textContent = p.id === 'openai' ? '\u{1F7E2}' : p.id === 'anthropic' ? '\u{1F7E0}' : '\u{1F7E2}';
11796
+ $('#model-icon').textContent = getProviderIcon(p.id);
11139
11797
  $('#model-api-key').value = '';
11140
- $('#model-api-key').placeholder = p.apiKeyHint || 'Enter API key';
11798
+ $('#model-api-key').placeholder = p.apiKeyHint || t('enter_api_key');
11141
11799
  $('#model-model-id').value = p.modelId || '';
11142
- $('#model-model-id').placeholder = p.modelHint || 'Model ID';
11800
+ $('#model-model-id').placeholder = p.modelHint || t('enter_model_id');
11143
11801
  $('#model-base-url').value = p.baseUrl || '';
11144
- $('#model-base-url').placeholder = p.baseUrlHint || 'Default URL';
11145
- $('#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
+ }
11146
11827
  showModal('modal-model-config');
11147
11828
  }
11148
11829
  async function saveModelConfig() {
11149
11830
  const apiKey = $('#model-api-key').value.trim();
11150
11831
  const modelId = $('#model-model-id').value.trim();
11151
11832
  const baseUrl = $('#model-base-url').value.trim();
11152
- 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; }
11153
11834
  hideModal('modal-model-config');
11154
- toast('Saving configuration...', 'info');
11835
+ toast(t('saving_config'), 'info');
11155
11836
  const r = await api('/models/' + encodeURIComponent(_editProviderId), { method: 'PUT', body: JSON.stringify({ apiKey, modelId, baseUrl }) });
11156
- if (r.success) { toast(r.message || 'Configuration saved!', 'ok'); loadModels(); }
11157
- 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'); }
11158
11839
  }
11159
11840
  async function deleteModelProvider(providerId) {
11160
- 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;
11161
11842
  const r = await api('/models/' + encodeURIComponent(providerId), { method: 'DELETE' });
11162
- if (r.success) { toast('Provider configuration deleted', 'ok'); loadModels(); }
11163
- 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'); }
11164
11845
  }
11165
11846
 
11166
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
@@ -11191,7 +11872,7 @@ function formatUptime(seconds) {
11191
11872
 
11192
11873
  async function loadSettings() {
11193
11874
  const el = $('#settings-content');
11194
- 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>');
11195
11876
 
11196
11877
  const settings = await api('/settings');
11197
11878
  if (settings.error) {
@@ -11203,11 +11884,11 @@ async function loadSettings() {
11203
11884
 
11204
11885
  // Render settings tabs nav
11205
11886
  html('#settings-tabs', \\\`
11206
- <button class="filter-btn \${_settingsTab==='connection'?'active':''}" onclick="_settingsTab='connection';renderSettingsTab()">\u{1F50C} Connection</button>
11207
- <button class="filter-btn \${_settingsTab==='friends'?'active':''}" onclick="_settingsTab='friends';renderSettingsTab()">\u{1F465} Friends</button>
11208
- <button class="filter-btn \${_settingsTab==='security'?'active':''}" onclick="_settingsTab='security';renderSettingsTab()">\u{1F512} Security</button>
11209
- <button class="filter-btn \${_settingsTab==='advanced'?'active':''}" onclick="_settingsTab='advanced';renderSettingsTab()">\u2699\uFE0F Advanced</button>
11210
- <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>
11211
11892
  \\\`);
11212
11893
 
11213
11894
  renderSettingsTab();
@@ -11216,11 +11897,11 @@ async function loadSettings() {
11216
11897
  function renderSettingsTab() {
11217
11898
  // Update tab buttons
11218
11899
  html('#settings-tabs', \\\`
11219
- <button class="filter-btn \${_settingsTab==='connection'?'active':''}" onclick="_settingsTab='connection';renderSettingsTab()">\u{1F50C} Connection</button>
11220
- <button class="filter-btn \${_settingsTab==='friends'?'active':''}" onclick="_settingsTab='friends';renderSettingsTab()">\u{1F465} Friends</button>
11221
- <button class="filter-btn \${_settingsTab==='security'?'active':''}" onclick="_settingsTab='security';renderSettingsTab()">\u{1F512} Security</button>
11222
- <button class="filter-btn \${_settingsTab==='advanced'?'active':''}" onclick="_settingsTab='advanced';renderSettingsTab()">\u2699\uFE0F Advanced</button>
11223
- <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>
11224
11905
  \\\`);
11225
11906
 
11226
11907
  switch (_settingsTab) {
@@ -11233,7 +11914,7 @@ function renderSettingsTab() {
11233
11914
  }
11234
11915
 
11235
11916
  function sectionSaveBtn(section, id) {
11236
- 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>
11237
11918
  <span id="status-\${id}" style="font-size:12px;color:var(--text3);margin-left:8px"></span>\\\`;
11238
11919
  }
11239
11920
 
@@ -11243,51 +11924,51 @@ function renderSettingsConnection() {
11243
11924
  const el = $('#settings-content');
11244
11925
 
11245
11926
  html(el, \\\`
11246
- <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>
11247
11928
 
11248
11929
  <div class="card">
11249
11930
  <div class="card-header">
11250
- <div class="card-title">\u{1F310} Server Connection</div>
11251
- <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>
11252
11933
  </div>
11253
11934
  <div class="form-group">
11254
- <label>Server URL</label>
11935
+ <label>\${t('server_url_label')}</label>
11255
11936
  <div style="display:flex;gap:8px;align-items:start">
11256
11937
  <div style="flex:1">
11257
11938
  <div class="input-prefix">
11258
11939
  <span class="prefix">\u{1F310}</span>
11259
11940
  <input type="url" id="set-server-url" value="\${escHtml(s.serverUrl || '')}" placeholder="https://aicq.online">
11260
11941
  </div>
11261
- <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>
11262
11943
  </div>
11263
- <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>
11264
11945
  </div>
11265
11946
  <div id="conn-test-result" style="margin-top:8px"></div>
11266
11947
  </div>
11267
11948
 
11268
11949
  <div class="form-row">
11269
11950
  <div class="form-group">
11270
- <label>Connection Timeout (seconds)</label>
11951
+ <label>\${t('conn_timeout')}</label>
11271
11952
  <input type="number" id="set-connection-timeout" value="\${s.connectionTimeout || 30}" min="5" max="120" placeholder="30">
11272
- <div class="hint">HTTP request timeout (5\u2013120s). Default: 30s.</div>
11953
+ <div class="hint">\${t('conn_timeout_hint')}</div>
11273
11954
  </div>
11274
11955
  <div class="form-group">
11275
- <label>WS Auto-Reconnect</label>
11956
+ <label>\${t('ws_auto_reconnect')}</label>
11276
11957
  <div style="display:flex;align-items:center;gap:10px;margin-top:6px">
11277
11958
  <label class="toggle-label">
11278
11959
  <input type="checkbox" id="set-ws-auto-reconnect" \${s.wsAutoReconnect ? 'checked' : ''}>
11279
11960
  <span class="toggle-slider"></span>
11280
- <span>Auto-reconnect when disconnected</span>
11961
+ <span>\${t('auto_reconnect_label')}</span>
11281
11962
  </label>
11282
11963
  </div>
11283
- <div class="hint">Automatically reconnect WebSocket on disconnection.</div>
11964
+ <div class="hint">\${t('auto_reconnect_hint')}</div>
11284
11965
  </div>
11285
11966
  </div>
11286
11967
 
11287
11968
  <div class="form-group">
11288
- <label>WS Reconnect Interval (seconds)</label>
11969
+ <label>\${t('ws_reconnect_interval')}</label>
11289
11970
  <input type="number" id="set-ws-reconnect-interval" value="\${s.wsReconnectInterval || 60}" min="5" max="600" placeholder="60">
11290
- <div class="hint">Interval between reconnection attempts (5\u2013600s). Default: 60s.</div>
11971
+ <div class="hint">\${t('ws_reconnect_hint')}</div>
11291
11972
  </div>
11292
11973
 
11293
11974
  <div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
@@ -11296,11 +11977,11 @@ function renderSettingsConnection() {
11296
11977
  </div>
11297
11978
 
11298
11979
  <div class="card">
11299
- <div class="card-header"><div class="card-title">\u{1F4C1} Config File</div></div>
11300
- <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>
11301
- <div class="detail-row"><div class="detail-key">Plugin Version</div><div class="detail-val">1.1.1</div></div>
11302
- <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>
11303
- <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>
11304
11985
  </div>
11305
11986
  \\\`);
11306
11987
  }
@@ -11310,37 +11991,37 @@ async function testConnection() {
11310
11991
  const resultEl = $('#conn-test-result');
11311
11992
  const url = $('#set-server-url')?.value?.trim() || _settingsData.serverUrl;
11312
11993
 
11313
- if (!url) { toast('Enter a server URL first', 'warn'); return; }
11994
+ if (!url) { toast(t('enter_server_url'), 'warn'); return; }
11314
11995
 
11315
- if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner" style="width:14px;height:14px;border-width:2px;margin:0"></span> Testing...'; }
11316
- 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>');
11317
11998
 
11318
11999
  const r = await api('/settings/test-connection', {
11319
12000
  method: 'POST',
11320
12001
  body: JSON.stringify({ serverUrl: url, timeout: 10000 }),
11321
12002
  });
11322
12003
 
11323
- if (btn) { btn.disabled = false; btn.innerHTML = '\u{1F50D} Test'; }
12004
+ if (btn) { btn.disabled = false; btn.innerHTML = '\u{1F50D} ' + t('test'); }
11324
12005
 
11325
12006
  if (r.success) {
11326
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>';
11327
12008
  if (resultEl) html(resultEl, \\\`
11328
12009
  <div style="display:flex;align-items:center;gap:10px;font-size:12px;color:var(--ok)">
11329
- <span class="dot dot-ok"></span> Connected successfully \${latencyBadge}
12010
+ <span class="dot dot-ok"></span> \${t('conn_ok')} \${latencyBadge}
11330
12011
  \${r.serverInfo?.version ? '<span class="tag">v' + escHtml(r.serverInfo.version) + '</span>' : ''}
11331
12012
  </div>
11332
12013
  \\\`);
11333
- toast('Connection OK! Latency: ' + r.latency + 'ms', 'ok');
12014
+ toast(t('conn_ok_latency') + r.latency + 'ms', 'ok');
11334
12015
  } else {
11335
12016
  const cls = r.status === 'timeout' ? 'warn' : 'danger';
11336
12017
  const icon = r.status === 'timeout' ? '\u23F1\uFE0F' : '\u274C';
11337
12018
  if (resultEl) html(resultEl, \\\`
11338
12019
  <div style="font-size:12px;color:var(--\${cls});display:flex;align-items:center;gap:8px">
11339
- \${icon} \${escHtml(r.message || 'Connection failed')}
12020
+ \${icon} \${escHtml(r.message || t('conn_failed'))}
11340
12021
  <span class="badge badge-ghost">\${r.latency}ms</span>
11341
12022
  </div>
11342
12023
  \\\`);
11343
- toast(r.message || 'Connection failed', 'err');
12024
+ toast(r.message || t('conn_failed'), 'err');
11344
12025
  }
11345
12026
  }
11346
12027
 
@@ -11350,58 +12031,58 @@ function renderSettingsFriends() {
11350
12031
  const el = $('#settings-content');
11351
12032
 
11352
12033
  html(el, \\\`
11353
- <p class="section-desc">Configure friend management, permissions, and temporary number settings.</p>
12034
+ <p class="section-desc">\${t('friends_tab_desc')}</p>
11354
12035
 
11355
12036
  <div class="stats-grid" style="margin-bottom:20px">
11356
12037
  <div class="stat-card">
11357
12038
  <div class="stat-icon" style="background:var(--ok-bg)">\u{1F465}</div>
11358
- <div class="stat-label">Friends</div>
12039
+ <div class="stat-label">\${t('friends')}</div>
11359
12040
  <div class="stat-value">\${s.friendCount || 0}</div>
11360
- <div class="stat-sub">of \${s.maxFriends || 200} max</div>
12041
+ <div class="stat-sub">\${t('of_max')}\${s.maxFriends || 200}</div>
11361
12042
  </div>
11362
12043
  <div class="stat-card">
11363
12044
  <div class="stat-icon" style="background:var(--info-bg)">\u{1F517}</div>
11364
- <div class="stat-label">Sessions</div>
12045
+ <div class="stat-label">\${t('sessions')}</div>
11365
12046
  <div class="stat-value">\${s.sessionCount || 0}</div>
11366
- <div class="stat-sub">Encrypted sessions</div>
12047
+ <div class="stat-sub">\${t('encrypted_sessions')}</div>
11367
12048
  </div>
11368
12049
  </div>
11369
12050
 
11370
12051
  <div class="card">
11371
- <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>
11372
12053
  <div class="form-row">
11373
12054
  <div class="form-group">
11374
- <label>Max Friends</label>
12055
+ <label>\${t('max_friends')}</label>
11375
12056
  <input type="number" id="set-max-friends" value="\${s.maxFriends || 200}" min="1" max="10000" placeholder="200">
11376
- <div class="hint">Maximum number of encrypted friend connections (1\u201310,000).</div>
12057
+ <div class="hint">\${t('max_friends')} (1\u201310,000)</div>
11377
12058
  </div>
11378
12059
  <div class="form-group">
11379
- <label>Auto-Accept Friends</label>
12060
+ <label>\${t('auto_accept')}</label>
11380
12061
  <div style="display:flex;align-items:center;gap:10px;margin-top:6px">
11381
12062
  <label class="toggle-label">
11382
12063
  <input type="checkbox" id="set-auto-accept" \${s.autoAcceptFriends ? 'checked' : ''}>
11383
12064
  <span class="toggle-slider"></span>
11384
- <span>Automatically accept requests</span>
12065
+ <span>\${t('auto_accept_label')}</span>
11385
12066
  </label>
11386
12067
  </div>
11387
- <div class="hint">When enabled, incoming friend requests are accepted without review.</div>
12068
+ <div class="hint">\${t('auto_accept_hint')}</div>
11388
12069
  </div>
11389
12070
  </div>
11390
12071
  <div class="form-group">
11391
- <label>Default Permissions for New Friends</label>
12072
+ <label>\${t('default_perms')}</label>
11392
12073
  <div style="display:flex;gap:16px;margin-top:6px;flex-wrap:wrap">
11393
12074
  <label class="toggle-label">
11394
12075
  <input type="checkbox" id="set-perm-chat" \${(s.defaultPermissions || []).includes('chat') ? 'checked' : ''}>
11395
12076
  <span class="toggle-slider"></span>
11396
- <span>\u{1F4AC} Chat</span>
12077
+ <span>\${t('chat_perm')}</span>
11397
12078
  </label>
11398
12079
  <label class="toggle-label">
11399
12080
  <input type="checkbox" id="set-perm-exec" \${(s.defaultPermissions || []).includes('exec') ? 'checked' : ''}>
11400
12081
  <span class="toggle-slider"></span>
11401
- <span>\u{1F527} Exec</span>
12082
+ <span>\${t('exec_perm')}</span>
11402
12083
  </label>
11403
12084
  </div>
11404
- <div class="hint">Default permissions applied when auto-accepting new friend requests.</div>
12085
+ <div class="hint">\${t('default_perms_hint')}</div>
11405
12086
  </div>
11406
12087
  <div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
11407
12088
  \${sectionSaveBtn('friends', 'friends')}
@@ -11409,11 +12090,11 @@ function renderSettingsFriends() {
11409
12090
  </div>
11410
12091
 
11411
12092
  <div class="card">
11412
- <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>
11413
12094
  <div class="form-group">
11414
- <label>Temp Number Expiry (seconds)</label>
12095
+ <label>\${t('temp_expiry')}</label>
11415
12096
  <input type="number" id="set-temp-expiry" value="\${s.tempNumberExpiry || 300}" min="60" max="3600" placeholder="300">
11416
- <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>
11417
12098
  </div>
11418
12099
  <div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
11419
12100
  \${sectionSaveBtn('temp', 'temp')}
@@ -11428,36 +12109,36 @@ function renderSettingsSecurity() {
11428
12109
  const el = $('#settings-content');
11429
12110
 
11430
12111
  html(el, \\\`
11431
- <p class="section-desc">Configure encryption, P2P, and identity security settings.</p>
12112
+ <p class="section-desc">\${t('sec_desc')}</p>
11432
12113
 
11433
12114
  <div class="card">
11434
- <div class="card-header"><div class="card-title">\u{1F916} Agent Identity</div></div>
11435
- <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>
11436
- <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>
11437
12118
  <div style="padding-top:12px;display:flex;gap:8px">
11438
- <button class="btn btn-danger btn-sm" onclick="showResetIdentityModal()">\u{1F5D1}\uFE0F Reset Identity</button>
11439
- <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>
11440
12121
  </div>
11441
12122
  </div>
11442
12123
 
11443
12124
  <div class="card">
11444
- <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>
11445
12126
  <div class="form-row">
11446
12127
  <div class="form-group">
11447
- <label>Enable P2P Connections</label>
12128
+ <label>\${t('enable_p2p')}</label>
11448
12129
  <div style="display:flex;align-items:center;gap:10px;margin-top:6px">
11449
12130
  <label class="toggle-label">
11450
12131
  <input type="checkbox" id="set-enable-p2p" \${s.enableP2P ? 'checked' : ''}>
11451
12132
  <span class="toggle-slider"></span>
11452
- <span>Allow direct P2P messaging</span>
12133
+ <span>\${t('allow_p2p')}</span>
11453
12134
  </label>
11454
12135
  </div>
11455
- <div class="hint">Enable peer-to-peer encrypted connections when both parties are online.</div>
12136
+ <div class="hint">\${t('enable_p2p_hint')}</div>
11456
12137
  </div>
11457
12138
  <div class="form-group">
11458
- <label>Handshake Timeout (seconds)</label>
12139
+ <label>\${t('hs_timeout')}</label>
11459
12140
  <input type="number" id="set-handshake-timeout" value="\${s.handshakeTimeout || 60}" min="10" max="300" placeholder="60">
11460
- <div class="hint">Noise-XK handshake timeout (10\u2013300s). Default: 60s.</div>
12141
+ <div class="hint">\${t('hs_timeout_hint')}</div>
11461
12142
  </div>
11462
12143
  </div>
11463
12144
  <div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
@@ -11473,24 +12154,24 @@ function renderSettingsAdvanced() {
11473
12154
  const el = $('#settings-content');
11474
12155
 
11475
12156
  html(el, \\\`
11476
- <p class="section-desc">Advanced settings for file transfer, logging, and configuration management.</p>
12157
+ <p class="section-desc">\${t('adv_desc')}</p>
11477
12158
 
11478
12159
  <div class="card">
11479
- <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>
11480
12161
  <div class="form-row">
11481
12162
  <div class="form-group">
11482
- <label>Enable File Transfer</label>
12163
+ <label>\${t('enable_ft')}</label>
11483
12164
  <div style="display:flex;align-items:center;gap:10px;margin-top:6px">
11484
12165
  <label class="toggle-label">
11485
12166
  <input type="checkbox" id="set-enable-ft" \${s.enableFileTransfer ? 'checked' : ''}>
11486
12167
  <span class="toggle-slider"></span>
11487
- <span>Allow file transfers</span>
12168
+ <span>\${t('allow_ft')}</span>
11488
12169
  </label>
11489
12170
  </div>
11490
- <div class="hint">Enable encrypted file transfer between friends.</div>
12171
+ <div class="hint">\${t('enable_ft_hint')}</div>
11491
12172
  </div>
11492
12173
  <div class="form-group">
11493
- <label>Max File Size</label>
12174
+ <label>\${t('max_file_size')}</label>
11494
12175
  <select id="set-max-file-size">
11495
12176
  <option value="10485760" \${s.maxFileSize <= 10485760 ? 'selected' : ''}>10 MB</option>
11496
12177
  <option value="52428800" \${s.maxFileSize > 10485760 && s.maxFileSize <= 52428800 ? 'selected' : ''}>50 MB</option>
@@ -11498,7 +12179,7 @@ function renderSettingsAdvanced() {
11498
12179
  <option value="524288000" \${s.maxFileSize > 104857600 && s.maxFileSize <= 524288000 ? 'selected' : ''}>500 MB</option>
11499
12180
  <option value="1073741824" \${s.maxFileSize > 524288000 ? 'selected' : ''}>1 GB</option>
11500
12181
  </select>
11501
- <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>
11502
12183
  </div>
11503
12184
  </div>
11504
12185
  <div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
@@ -11507,17 +12188,17 @@ function renderSettingsAdvanced() {
11507
12188
  </div>
11508
12189
 
11509
12190
  <div class="card">
11510
- <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>
11511
12192
  <div class="form-group">
11512
- <label>Log Level</label>
12193
+ <label>\${t('log_level')}</label>
11513
12194
  <select id="set-log-level" style="max-width:300px">
11514
- <option value="debug" \${s.logLevel === 'debug' ? 'selected' : ''}>\u{1F41B} Debug \u2014 Verbose output for troubleshooting</option>
11515
- <option value="info" \${s.logLevel === 'info' ? 'selected' : ''}>\u2139\uFE0F Info \u2014 General information (default)</option>
11516
- <option value="warn" \${s.logLevel === 'warn' ? 'selected' : ''}>\u26A0\uFE0F Warn \u2014 Warnings and important events</option>
11517
- <option value="error" \${s.logLevel === 'error' ? 'selected' : ''}>\u274C Error \u2014 Errors only</option>
11518
- <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>
11519
12200
  </select>
11520
- <div class="hint">Controls the verbosity of plugin log output.</div>
12201
+ <div class="hint">\${t('log_level_hint')}</div>
11521
12202
  </div>
11522
12203
  <div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
11523
12204
  \${sectionSaveBtn('logging', 'log')}
@@ -11525,12 +12206,12 @@ function renderSettingsAdvanced() {
11525
12206
  </div>
11526
12207
 
11527
12208
  <div class="card">
11528
- <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>
11529
12210
  <div style="display:flex;gap:10px;flex-wrap:wrap">
11530
- <button class="btn btn-default btn-sm" onclick="exportSettings()">\u{1F4E5} Export Settings</button>
11531
- <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>
11532
12213
  </div>
11533
- <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>
11534
12215
  </div>
11535
12216
  \\\`);
11536
12217
  }
@@ -11539,7 +12220,7 @@ function renderSettingsAdvanced() {
11539
12220
  async function saveSettingsSection(section, id) {
11540
12221
  const btn = $('#btn-save-' + id);
11541
12222
  const statusEl = $('#status-' + id);
11542
- if (btn) { btn.disabled = true; btn.textContent = 'Saving...'; }
12223
+ if (btn) { btn.disabled = true; btn.textContent = t('saving'); }
11543
12224
  if (statusEl) { statusEl.textContent = ''; statusEl.style.color = 'var(--text3)'; }
11544
12225
 
11545
12226
  let data = {};
@@ -11587,17 +12268,17 @@ async function saveSettingsSection(section, id) {
11587
12268
  body: JSON.stringify({ section, data }),
11588
12269
  });
11589
12270
 
11590
- if (btn) { btn.disabled = false; btn.textContent = '\u{1F4BE} Save'; }
12271
+ if (btn) { btn.disabled = false; btn.textContent = t('save'); }
11591
12272
 
11592
12273
  if (r.success) {
11593
- toast('Settings saved: ' + section, 'ok');
11594
- 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)'; }
11595
12276
  // Refresh settings data
11596
12277
  const fresh = await api('/settings');
11597
12278
  if (fresh && !fresh.error) { _settingsData = fresh; }
11598
12279
  } else {
11599
- toast(r.message || r.error || 'Save failed', 'err');
11600
- 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)'; }
11601
12282
  }
11602
12283
  }
11603
12284
 
@@ -11615,15 +12296,15 @@ async function saveSettings() {
11615
12296
  const r = await api('/settings', { method: 'PUT', body: JSON.stringify(allData) });
11616
12297
  _settingsSaving = false;
11617
12298
 
11618
- if (r.success) { toast('All settings saved!', 'ok'); setTimeout(() => loadSettings(), 800); }
11619
- 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'); }
11620
12301
  }
11621
12302
 
11622
12303
  // \u2500\u2500 Reset Identity \u2500\u2500
11623
12304
  function showResetIdentityModal() {
11624
12305
  $('#reset-confirm-input').value = '';
11625
12306
  $('#reset-confirm-btn').disabled = true;
11626
- $('#reset-confirm-btn').textContent = '\u{1F5D1}\uFE0F Delete Everything';
12307
+ $('#reset-confirm-btn').textContent = t('delete_everything');
11627
12308
  showModal('modal-reset-identity');
11628
12309
  setTimeout(() => $('#reset-confirm-input')?.focus(), 100);
11629
12310
  }
@@ -11631,27 +12312,27 @@ function showResetIdentityModal() {
11631
12312
  function checkResetConfirm() {
11632
12313
  const v = $('#reset-confirm-input')?.value?.trim();
11633
12314
  const btn = $('#reset-confirm-btn');
11634
- 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'); }
11635
12316
  }
11636
12317
 
11637
12318
  async function executeResetIdentity() {
11638
12319
  const btn = $('#reset-confirm-btn');
11639
- if (btn) { btn.disabled = true; btn.textContent = 'Resetting...'; }
12320
+ if (btn) { btn.disabled = true; btn.textContent = t('resetting'); }
11640
12321
 
11641
12322
  const r = await api('/settings/reset-identity', {
11642
12323
  method: 'POST',
11643
12324
  body: JSON.stringify({ confirm: true }),
11644
12325
  });
11645
12326
 
11646
- if (btn) { btn.disabled = false; btn.textContent = '\u{1F5D1}\uFE0F Delete Everything'; }
12327
+ if (btn) { btn.disabled = false; btn.textContent = t('delete_everything'); }
11647
12328
 
11648
12329
  if (r.success) {
11649
- toast('Identity reset successfully. Please restart the plugin.', 'ok');
12330
+ toast(t('reset_success'), 'ok');
11650
12331
  hideModal('modal-reset-identity');
11651
12332
  // Reload settings to reflect cleared state
11652
12333
  setTimeout(() => loadSettings(), 1000);
11653
12334
  } else {
11654
- toast(r.message || r.error || 'Reset failed', 'err');
12335
+ toast(r.message || r.error || t('reset_failed'), 'err');
11655
12336
  }
11656
12337
  }
11657
12338
 
@@ -11668,7 +12349,7 @@ async function exportSettings() {
11668
12349
  a.download = 'aicq-settings-' + new Date().toISOString().slice(0, 10) + '.json';
11669
12350
  a.click();
11670
12351
  URL.revokeObjectURL(url);
11671
- toast('Settings exported successfully', 'ok');
12352
+ toast(t('exported_success'), 'ok');
11672
12353
  }
11673
12354
 
11674
12355
  function showImportSettingsModal() {
@@ -11679,27 +12360,27 @@ function showImportSettingsModal() {
11679
12360
 
11680
12361
  async function executeImportSettings() {
11681
12362
  const raw = $('#import-json-input')?.value?.trim();
11682
- if (!raw) { toast('Paste JSON settings first', 'warn'); return; }
12363
+ if (!raw) { toast(t('paste_json'), 'warn'); return; }
11683
12364
 
11684
12365
  let settings;
11685
- 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; }
11686
12367
 
11687
12368
  const btn = $('#import-confirm-btn');
11688
- if (btn) { btn.disabled = true; btn.textContent = 'Importing...'; }
12369
+ if (btn) { btn.disabled = true; btn.textContent = t('importing'); }
11689
12370
 
11690
12371
  const r = await api('/settings/import', {
11691
12372
  method: 'POST',
11692
12373
  body: JSON.stringify({ settings, merge: true }),
11693
12374
  });
11694
12375
 
11695
- if (btn) { btn.disabled = false; btn.textContent = '\u{1F4E4} Import'; }
12376
+ if (btn) { btn.disabled = false; btn.textContent = t('import_settings'); }
11696
12377
 
11697
12378
  if (r.success) {
11698
- toast('Settings imported successfully!', 'ok');
12379
+ toast(t('imported_success'), 'ok');
11699
12380
  hideModal('modal-import-settings');
11700
12381
  setTimeout(() => loadSettings(), 800);
11701
12382
  } else {
11702
- toast(r.message || r.error || 'Import failed', 'err');
12383
+ toast(r.message || r.error || t('import_failed'), 'err');
11703
12384
  }
11704
12385
  }
11705
12386
 
@@ -11710,7 +12391,7 @@ let _jsonEditorConfigFile = '';
11710
12391
 
11711
12392
  async function renderSettingsJsonEditor() {
11712
12393
  const el = $('#settings-content');
11713
- 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>');
11714
12395
 
11715
12396
  const queryParams = _jsonEditorConfigFile ? '?file=' + encodeURIComponent(_jsonEditorConfigFile) : '';
11716
12397
  const data = await api('/config-file/raw' + queryParams);
@@ -11742,7 +12423,7 @@ async function renderSettingsJsonEditor() {
11742
12423
 
11743
12424
  <div class="card">
11744
12425
  <div class="card-header">
11745
- <div class="card-title">\u{1F4DD} Config JSON Editor</div>
12426
+ <div class="card-title">\${t('json_editor')}</div>
11746
12427
  <div style="display:flex;gap:8px;align-items:center">
11747
12428
  <span class="mono" style="font-size:11px;color:var(--text3)">\${escHtml(data.filePath)}</span>
11748
12429
  <button class="btn btn-sm btn-default" onclick="renderSettingsJsonEditor()">\u{1F504} Reload</button>
@@ -11750,19 +12431,19 @@ async function renderSettingsJsonEditor() {
11750
12431
  </div>
11751
12432
  \${fileSelectorHtml}
11752
12433
  <div class="form-group">
11753
- <label>Raw JSON Configuration</label>
12434
+ <label>\${t('raw_json')}</label>
11754
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>
11755
- <div class="hint">Directly edit the configuration JSON. Use the Format button to prettify.</div>
12436
+ <div class="hint">\${t('raw_json_hint')}</div>
11756
12437
  </div>
11757
12438
  <div id="json-editor-status" style="margin-bottom:12px;font-size:12px"></div>
11758
12439
  <div class="form-actions" style="justify-content:space-between">
11759
12440
  <div style="display:flex;gap:8px">
11760
- <button class="btn btn-sm btn-default" onclick="formatJsonEditor()">\u{1F4D0} Format</button>
11761
- <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>
11762
12443
  </div>
11763
12444
  <div style="display:flex;gap:8px">
11764
- <button class="btn btn-sm btn-default" onclick="renderSettingsJsonEditor()">\u21A9\uFE0F Revert</button>
11765
- <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>
11766
12447
  </div>
11767
12448
  </div>
11768
12449
  </div>
@@ -11775,10 +12456,10 @@ function formatJsonEditor() {
11775
12456
  try {
11776
12457
  const obj = JSON.parse(ta.value);
11777
12458
  ta.value = JSON.stringify(obj, null, 2);
11778
- toast('JSON formatted', 'ok');
11779
- $('#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>';
11780
12461
  } catch (e) {
11781
- toast('Invalid JSON: ' + e.message, 'err');
12462
+ toast(t('invalid_json') + e.message, 'err');
11782
12463
  $('#json-editor-status').innerHTML = '<span style="color:var(--danger)">\u2717 ' + escHtml(e.message) + '</span>';
11783
12464
  }
11784
12465
  }
@@ -11787,29 +12468,439 @@ async function saveJsonConfig() {
11787
12468
  const btn = $('#btn-save-json');
11788
12469
  const statusEl = $('#json-editor-status');
11789
12470
  const raw = $('#json-editor')?.value;
11790
- if (!raw) { toast('No content to save', 'warn'); return; }
12471
+ if (!raw) { toast(t('no_content'), 'warn'); return; }
11791
12472
 
11792
12473
  // Validate first
11793
12474
  try { JSON.parse(raw); } catch (e) {
11794
- toast('Invalid JSON: ' + e.message, 'err');
12475
+ toast(t('invalid_json') + e.message, 'err');
11795
12476
  if (statusEl) statusEl.innerHTML = '<span style="color:var(--danger)">\u2717 ' + escHtml(e.message) + '</span>';
11796
12477
  return;
11797
12478
  }
11798
12479
 
11799
- if (btn) { btn.disabled = true; btn.textContent = 'Saving...'; }
11800
- 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>';
11801
12482
 
11802
12483
  const queryParams = _jsonEditorConfigFile ? '?file=' + encodeURIComponent(_jsonEditorConfigFile) : '';
11803
12484
  const r = await api('/config-file/raw' + queryParams, { method: 'PUT', body: JSON.stringify({ content: raw }) });
11804
12485
 
11805
- if (btn) { btn.disabled = false; btn.textContent = '\u{1F4BE} Save Config'; }
12486
+ if (btn) { btn.disabled = false; btn.textContent = t('save_config'); }
11806
12487
 
11807
12488
  if (r.success) {
11808
- toast('Config saved successfully!', 'ok');
12489
+ toast(t('config_saved'), 'ok');
11809
12490
  if (statusEl) statusEl.innerHTML = '<span style="color:var(--ok)">\u2713 Saved at ' + new Date().toLocaleTimeString() + '</span>';
11810
12491
  } else {
11811
- toast(r.message || 'Save failed', 'err');
11812
- 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');
11813
12904
  }
11814
12905
  }
11815
12906
 
@@ -11833,7 +12924,7 @@ document.addEventListener('DOMContentLoaded', () => {
11833
12924
  const dot = $('#header-dot');
11834
12925
  if (dot) { dot.className = 'dot ' + (s.connected ? 'dot-ok' : 'dot-err'); }
11835
12926
  const txt = $('#header-status');
11836
- if (txt) txt.textContent = s.connected ? 'Connected' : 'Disconnected';
12927
+ if (txt) txt.textContent = s.connected ? t('connected') : t('disconnected');
11837
12928
  // Auto-remove offline banner when server reconnects
11838
12929
  if (s.connected) hideOfflineBanner();
11839
12930
  }
@@ -11872,6 +12963,10 @@ var HTML = `<!DOCTYPE html>
11872
12963
  <div class="nav-group-title">System</div>
11873
12964
  <div class="nav-item" data-page="settings"><span class="nav-icon">\u2699\uFE0F</span><span class="nav-label">Settings</span></div>
11874
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>
11875
12970
  </nav>
11876
12971
  <div class="sidebar-footer" onclick="toggleSidebar()">
11877
12972
  <span>\u25C0</span><span class="sidebar-footer-text">Collapse sidebar</span>
@@ -11914,6 +13009,9 @@ var HTML = `<!DOCTYPE html>
11914
13009
  <div id="settings-content"></div>
11915
13010
  </div>
11916
13011
 
13012
+ <!-- OpenClaw Config -->
13013
+ <div class="page" id="page-openclaw"><div id="openclaw-content"></div></div>
13014
+
11917
13015
  </div>
11918
13016
  </main>
11919
13017
  </div>
@@ -11972,6 +13070,7 @@ var HTML = `<!DOCTYPE html>
11972
13070
  <div class="input-prefix"><span class="prefix">\u{1F310}</span><input id="model-base-url" type="text" placeholder="https://..."></div>
11973
13071
  <div class="hint">Custom endpoint URL. Only needed for proxies or self-hosted models.</div>
11974
13072
  </div>
13073
+ <div id="model-multi-list" style="display:none;margin-top:12px"></div>
11975
13074
  <div class="form-actions">
11976
13075
  <button class="btn btn-default" onclick="hideModal('modal-model-config')">Cancel</button>
11977
13076
  <button class="btn btn-primary" onclick="saveModelConfig()">\u{1F4BE} Save Configuration</button>
@@ -11979,6 +13078,39 @@ var HTML = `<!DOCTYPE html>
11979
13078
  </div>
11980
13079
  </div>
11981
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
+
11982
13114
  <!-- Modal: View Agent -->
11983
13115
  <div class="modal-overlay hidden" id="modal-view-agent" onclick="if(event.target===this)hideModal('modal-view-agent')">
11984
13116
  <div class="modal">
@@ -11994,7 +13126,7 @@ var HTML = `<!DOCTYPE html>
11994
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>
11995
13127
  <div style="margin-bottom:16px">
11996
13128
  <div class="card" style="border-color:var(--danger);background:var(--danger-bg)">
11997
- <p style="font-size:13px;color:#fca5a5;line-height:1.6">
13129
+ <p style="font-size:13px;color:#991b1b;line-height:1.6">
11998
13130
  <strong>\u26A0\uFE0F WARNING: This is a destructive operation!</strong><br><br>
11999
13131
  This will permanently delete:<br>
12000
13132
  \u2022 Your Ed25519 key pair and agent ID<br>
@@ -12121,29 +13253,59 @@ var MODEL_PROVIDERS = [
12121
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" },
12122
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" },
12123
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" },
12124
- { 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" }
12125
13267
  ];
12126
13268
  function findConfigPath() {
12127
- const openclawPaths = [
12128
- 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 = [
12129
13289
  path5.join(os.homedir(), ".config", "openclaw", "openclaw.json"),
12130
13290
  path5.join(os.homedir(), ".openclaw", "openclaw.json"),
12131
13291
  path5.join(os.homedir(), "openclaw.json")
12132
13292
  ];
12133
- for (const p of openclawPaths) {
13293
+ for (const p of openclawHomePaths) {
12134
13294
  try {
12135
13295
  if (fs5.existsSync(p))
12136
13296
  return p;
12137
13297
  } catch {
12138
13298
  }
12139
13299
  }
12140
- const stableclawPaths = [
12141
- path5.join(process.cwd(), "stableclaw.json"),
13300
+ const fromCwdStable = searchUpward("stableclaw.json");
13301
+ if (fromCwdStable)
13302
+ return fromCwdStable;
13303
+ const stableclawHomePaths = [
12142
13304
  path5.join(os.homedir(), ".config", "stableclaw", "stableclaw.json"),
12143
13305
  path5.join(os.homedir(), ".stableclaw", "stableclaw.json"),
12144
13306
  path5.join(os.homedir(), "stableclaw.json")
12145
13307
  ];
12146
- for (const p of stableclawPaths) {
13308
+ for (const p of stableclawHomePaths) {
12147
13309
  try {
12148
13310
  if (fs5.existsSync(p))
12149
13311
  return p;
@@ -12177,9 +13339,30 @@ function writeConfig(config2) {
12177
13339
  }
12178
13340
  function extractAgentsFromConfig(config2) {
12179
13341
  const agents = [];
12180
- const agentsVal = config2.agents;
12181
- if (Array.isArray(agentsVal)) {
12182
- 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) {
12183
13366
  if (typeof a === "object" && a !== null) {
12184
13367
  agents.push(a);
12185
13368
  }
@@ -12187,7 +13370,56 @@ function extractAgentsFromConfig(config2) {
12187
13370
  }
12188
13371
  const agentVal = config2.agent;
12189
13372
  if (typeof agentVal === "object" && agentVal !== null && !Array.isArray(agentVal)) {
12190
- 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
+ }
12191
13423
  }
12192
13424
  if (agents.length === 0) {
12193
13425
  for (const [key, val] of Object.entries(config2)) {
@@ -12208,34 +13440,174 @@ function getModelProviders(config2) {
12208
13440
  if (model?.providers)
12209
13441
  providersSection = model.providers;
12210
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
+ }
12211
13493
  const providers = MODEL_PROVIDERS.map((p) => {
12212
13494
  const pc = providersSection?.[p.configKey] ?? config2[p.configKey];
12213
- const apiKey = pc?.apiKey || "";
12214
- 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
+ }
12215
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
+ }
12216
13525
  return {
12217
13526
  ...p,
12218
13527
  configured: Boolean(apiKey || p.id === "ollama" && baseUrl),
12219
13528
  apiKey: apiKey ? apiKey.substring(0, 6) + "\u2022\u2022\u2022\u2022\u2022\u2022" + apiKey.slice(-4) : "",
12220
13529
  apiKeyHasValue: Boolean(apiKey),
12221
13530
  modelId,
12222
- baseUrl
13531
+ baseUrl,
13532
+ models: modelsList,
13533
+ modelCount: modelsList.length,
13534
+ isCustom: false
12223
13535
  };
12224
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
+ }
12225
13572
  const currentModels = [];
12226
- for (const p of MODEL_PROVIDERS) {
12227
- const pc = providersSection?.[p.configKey] ?? config2[p.configKey];
12228
- 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 {
12229
13599
  currentModels.push({
12230
- provider: p.name,
12231
- providerId: p.id,
12232
- modelId: pc.model || pc.defaultModel || p.modelHint,
13600
+ provider: pName,
13601
+ providerId: pId,
13602
+ modelId: p.modelId || pModelHint,
12233
13603
  hasApiKey: true,
12234
- baseUrl: pc.baseUrl || pc.baseURL || p.baseUrlHint
13604
+ baseUrl: pBaseUrl,
13605
+ isDefault: false,
13606
+ isCustom
12235
13607
  });
12236
13608
  }
12237
13609
  }
12238
- return { providers, currentModels };
13610
+ return { providers, currentModels, defaultModel, customProviders: customProvidersSection || [] };
12239
13611
  }
12240
13612
  function parseApiPath(reqUrl) {
12241
13613
  if (!reqUrl)
@@ -12354,14 +13726,33 @@ function createManagementHandler(ctx) {
12354
13726
  });
12355
13727
  }
12356
13728
  if (apiPath.startsWith("/agents/") && method === "DELETE") {
12357
- const idxStr = decodeURIComponent(apiPath.slice("/agents/".length));
12358
- const idx = parseInt(idxStr, 10);
12359
- if (isNaN(idx) || idx < 0)
12360
- return json(res, { success: false, message: "Invalid agent index" }, 400);
13729
+ const identifier = decodeURIComponent(apiPath.slice("/agents/".length));
12361
13730
  const result = readConfig();
12362
13731
  if (!result)
12363
13732
  return json(res, { success: false, message: "No config file found" }, 400);
12364
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);
12365
13756
  let agentsArr;
12366
13757
  if (Array.isArray(config2.agents)) {
12367
13758
  agentsArr = config2.agents;
@@ -12387,14 +13778,17 @@ function createManagementHandler(ctx) {
12387
13778
  }
12388
13779
  if (apiPath === "/friends" && method === "GET") {
12389
13780
  try {
12390
- 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
+ });
12391
13784
  if (!resp.ok)
12392
13785
  return json(res, { error: "Server error: " + await resp.text() }, 502);
12393
13786
  const data = await resp.json();
12394
13787
  const friends = (data.friends || []).map((f) => {
12395
- const local = store.getFriend(f.nodeId);
13788
+ const friendId = f.id || f.nodeId;
13789
+ const local = store.getFriend(friendId);
12396
13790
  return {
12397
- id: f.nodeId,
13791
+ id: friendId,
12398
13792
  publicKeyFingerprint: f.publicKeyFingerprint || local?.publicKeyFingerprint || "",
12399
13793
  permissions: f.permissions || local?.permissions || [],
12400
13794
  addedAt: f.addedAt || local?.addedAt?.toISOString() || null,
@@ -12426,7 +13820,9 @@ function createManagementHandler(ctx) {
12426
13820
  let friendId = target;
12427
13821
  if (isTempNumber) {
12428
13822
  try {
12429
- 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
+ });
12430
13826
  if (!resolveResp.ok)
12431
13827
  return json(res, { success: false, message: "Temp number not found or expired" });
12432
13828
  const resolveData = await resolveResp.json();
@@ -12439,7 +13835,7 @@ function createManagementHandler(ctx) {
12439
13835
  try {
12440
13836
  const hsResp = await fetch(serverUrl + "/api/v1/handshake/initiate", {
12441
13837
  method: "POST",
12442
- headers: { "Content-Type": "application/json" },
13838
+ headers: serverClient.authHeaders(),
12443
13839
  body: JSON.stringify({ requesterId: aicqAgentId, targetTempNumber: target })
12444
13840
  });
12445
13841
  if (!hsResp.ok)
@@ -12462,7 +13858,7 @@ function createManagementHandler(ctx) {
12462
13858
  try {
12463
13859
  const rmResp = await fetch(serverUrl + "/api/v1/friends/" + friendId, {
12464
13860
  method: "DELETE",
12465
- headers: { "Content-Type": "application/json" },
13861
+ headers: serverClient.authHeaders(),
12466
13862
  body: JSON.stringify({ nodeId: aicqAgentId })
12467
13863
  });
12468
13864
  if (!rmResp.ok)
@@ -12488,7 +13884,7 @@ function createManagementHandler(ctx) {
12488
13884
  try {
12489
13885
  const resp = await fetch(serverUrl + "/api/v1/friends/" + friendId + "/permissions", {
12490
13886
  method: "PUT",
12491
- headers: { "Content-Type": "application/json" },
13887
+ headers: serverClient.authHeaders(),
12492
13888
  body: JSON.stringify({ nodeId: aicqAgentId, permissions })
12493
13889
  });
12494
13890
  if (!resp.ok)
@@ -12507,7 +13903,9 @@ function createManagementHandler(ctx) {
12507
13903
  }
12508
13904
  if (apiPath === "/friends/requests" && method === "GET") {
12509
13905
  try {
12510
- 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
+ });
12511
13909
  if (!resp.ok)
12512
13910
  return json(res, { error: "Server error: " + await resp.text() }, 502);
12513
13911
  const data = await resp.json();
@@ -12530,8 +13928,8 @@ function createManagementHandler(ctx) {
12530
13928
  try {
12531
13929
  const resp = await fetch(serverUrl + "/api/v1/friends/requests/" + requestId + "/accept", {
12532
13930
  method: "POST",
12533
- headers: { "Content-Type": "application/json" },
12534
- body: JSON.stringify({ permissions: body.permissions || ["chat"] })
13931
+ headers: serverClient.authHeaders(),
13932
+ body: JSON.stringify({ accountId: aicqAgentId, permissions: body.permissions || ["chat"] })
12535
13933
  });
12536
13934
  if (!resp.ok)
12537
13935
  return json(res, { success: false, message: "Failed: " + await resp.text() });
@@ -12549,8 +13947,8 @@ function createManagementHandler(ctx) {
12549
13947
  try {
12550
13948
  const resp = await fetch(serverUrl + "/api/v1/friends/requests/" + requestId + "/reject", {
12551
13949
  method: "POST",
12552
- headers: { "Content-Type": "application/json" },
12553
- body: JSON.stringify({})
13950
+ headers: serverClient.authHeaders(),
13951
+ body: JSON.stringify({ accountId: aicqAgentId })
12554
13952
  });
12555
13953
  if (!resp.ok)
12556
13954
  return json(res, { success: false, message: "Failed: " + await resp.text() });
@@ -12572,9 +13970,37 @@ function createManagementHandler(ctx) {
12572
13970
  if (apiPath === "/models" && method === "GET") {
12573
13971
  const result = readConfig();
12574
13972
  if (!result)
12575
- 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" });
12576
13974
  return json(res, getModelProviders(result.config));
12577
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
+ }
12578
14004
  if (apiPath.match(/^\/models\/[^/]+$/) && method === "PUT") {
12579
14005
  const providerId = decodeURIComponent(apiPath.slice("/models/".length));
12580
14006
  const body = await readBody(req);
@@ -12608,6 +14034,140 @@ function createManagementHandler(ctx) {
12608
14034
  logger.info("[API] Model config saved for provider: " + providerId);
12609
14035
  return json(res, { success: true, message: "Model configuration saved for " + provider.name });
12610
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
+ }
12611
14171
  if (apiPath === "/settings" && method === "GET") {
12612
14172
  const result = readConfig();
12613
14173
  const aicqSection = result?.config?.aicq ?? {};
@@ -12959,10 +14519,7 @@ function createManagementHandler(ctx) {
12959
14519
  return json(res, { success: true, message: "Agent added", index: config2.agents.length - 1 });
12960
14520
  }
12961
14521
  if (apiPath.startsWith("/agents/") && method === "PUT") {
12962
- const idxStr = decodeURIComponent(apiPath.slice("/agents/".length));
12963
- const idx = parseInt(idxStr, 10);
12964
- if (isNaN(idx) || idx < 0)
12965
- return json(res, { success: false, message: "Invalid agent index" }, 400);
14522
+ const identifier = decodeURIComponent(apiPath.slice("/agents/".length));
12966
14523
  const body = await readBody(req);
12967
14524
  const updates = body.agent;
12968
14525
  if (!updates || typeof updates !== "object") {
@@ -12972,11 +14529,42 @@ function createManagementHandler(ctx) {
12972
14529
  if (!result)
12973
14530
  return json(res, { success: false, message: "No config file found" }, 400);
12974
14531
  const config2 = result.config;
12975
- 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);
12976
14564
  if (!Array.isArray(config2.agents)) {
12977
14565
  return json(res, { success: false, message: "No agents array in config" }, 400);
12978
14566
  }
12979
- agentsArr = config2.agents;
14567
+ const agentsArr = config2.agents;
12980
14568
  if (idx >= agentsArr.length) {
12981
14569
  return json(res, { success: false, message: "Agent index out of range" }, 400);
12982
14570
  }
@@ -13235,9 +14823,13 @@ var plugin = definePluginEntry({
13235
14823
  try {
13236
14824
  switch (action) {
13237
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;
13238
14830
  const resp = await fetch(serverUrl + "/api/v1/temp-number/request", {
13239
14831
  method: "POST",
13240
- headers: { "Content-Type": "application/json" },
14832
+ headers: authHeaders,
13241
14833
  body: JSON.stringify({ nodeId: aicqAgentId })
13242
14834
  });
13243
14835
  if (!resp.ok)
@@ -13246,7 +14838,11 @@ var plugin = definePluginEntry({
13246
14838
  return { success: true, tempNumber: data.number, message: "Temp number: " + data.number };
13247
14839
  }
13248
14840
  case "list": {
13249
- 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 });
13250
14846
  if (!resp.ok)
13251
14847
  return { error: "Server error: " + await resp.text() };
13252
14848
  const data = await resp.json();
@@ -13259,15 +14855,23 @@ var plugin = definePluginEntry({
13259
14855
  const isTempNumber = /^\d{6}$/.test(target);
13260
14856
  let friendId = target;
13261
14857
  if (isTempNumber) {
13262
- 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 });
13263
14863
  if (!resolveResp.ok)
13264
14864
  return { error: "Temp number not found or expired" };
13265
14865
  const resolveData = await resolveResp.json();
13266
14866
  friendId = resolveData.nodeId;
13267
14867
  }
14868
+ const hsAuthHeaders = { "Content-Type": "application/json" };
14869
+ const hsToken = serverClient.getAuthToken();
14870
+ if (hsToken)
14871
+ hsAuthHeaders["Authorization"] = "Bearer " + hsToken;
13268
14872
  const hsResp = await fetch(serverUrl + "/api/v1/handshake/initiate", {
13269
14873
  method: "POST",
13270
- headers: { "Content-Type": "application/json" },
14874
+ headers: hsAuthHeaders,
13271
14875
  body: JSON.stringify({ requesterId: aicqAgentId, targetTempNumber: target })
13272
14876
  });
13273
14877
  if (!hsResp.ok)
@@ -13279,9 +14883,13 @@ var plugin = definePluginEntry({
13279
14883
  const target = params?.target;
13280
14884
  if (!target)
13281
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;
13282
14890
  const rmResp = await fetch(serverUrl + "/api/v1/friends/" + target, {
13283
14891
  method: "DELETE",
13284
- headers: { "Content-Type": "application/json" },
14892
+ headers: rmAuthHeaders,
13285
14893
  body: JSON.stringify({ nodeId: aicqAgentId })
13286
14894
  });
13287
14895
  if (!rmResp.ok)
@@ -13289,9 +14897,16 @@ var plugin = definePluginEntry({
13289
14897
  return { success: true, message: "Friend " + target + " removed" };
13290
14898
  }
13291
14899
  case "revoke-temp-number": {
13292
- 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, {
13293
14908
  method: "DELETE",
13294
- headers: { "Content-Type": "application/json" },
14909
+ headers: revokeAuthHeaders,
13295
14910
  body: JSON.stringify({ nodeId: aicqAgentId })
13296
14911
  });
13297
14912
  if (!resp.ok)
@@ -13377,21 +14992,25 @@ var plugin = definePluginEntry({
13377
14992
  });
13378
14993
  api.registerGatewayMethod("aicq.friends.list", async (params) => {
13379
14994
  try {
13380
- 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
+ });
13381
14998
  if (!resp.ok)
13382
14999
  return { error: "Server error: " + await resp.text() };
13383
15000
  const data = await resp.json();
13384
15001
  const friends = data.friends || [];
13385
15002
  const enriched = friends.map((f) => {
13386
- const localFriend = store.getFriend(f.nodeId);
15003
+ const friendId = f.id || f.nodeId;
15004
+ const localFriend = store.getFriend(friendId);
13387
15005
  return {
13388
- id: f.nodeId,
15006
+ id: friendId,
13389
15007
  publicKeyFingerprint: f.publicKeyFingerprint || (localFriend?.publicKeyFingerprint || ""),
13390
15008
  permissions: f.permissions || localFriend?.permissions || [],
13391
15009
  addedAt: f.addedAt || localFriend?.addedAt?.toISOString() || null,
13392
15010
  lastMessageAt: f.lastMessageAt || localFriend?.lastMessageAt?.toISOString() || null,
13393
15011
  friendType: f.friendType || localFriend?.friendType || null,
13394
- aiName: f.aiName || localFriend?.aiName || null
15012
+ aiName: f.aiName || localFriend?.aiName || null,
15013
+ nodeId: f.nodeId || null
13395
15014
  };
13396
15015
  });
13397
15016
  return { friends: enriched };
@@ -13409,7 +15028,9 @@ var plugin = definePluginEntry({
13409
15028
  const isTempNumber = /^\d{6}$/.test(target);
13410
15029
  let friendId = target;
13411
15030
  if (isTempNumber) {
13412
- 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
+ });
13413
15034
  if (!resolveResp.ok)
13414
15035
  return { success: false, message: "Temp number not found or expired" };
13415
15036
  const resolveData = await resolveResp.json();
@@ -13417,7 +15038,7 @@ var plugin = definePluginEntry({
13417
15038
  }
13418
15039
  const hsResp = await fetch(serverUrl + "/api/v1/handshake/initiate", {
13419
15040
  method: "POST",
13420
- headers: { "Content-Type": "application/json" },
15041
+ headers: serverClient.authHeaders(),
13421
15042
  body: JSON.stringify({ requesterId: aicqAgentId, targetTempNumber: target })
13422
15043
  });
13423
15044
  if (!hsResp.ok)
@@ -13443,7 +15064,7 @@ var plugin = definePluginEntry({
13443
15064
  return { success: false, message: "Missing friendId parameter" };
13444
15065
  const rmResp = await fetch(serverUrl + "/api/v1/friends/" + friendId, {
13445
15066
  method: "DELETE",
13446
- headers: { "Content-Type": "application/json" },
15067
+ headers: serverClient.authHeaders(),
13447
15068
  body: JSON.stringify({ nodeId: aicqAgentId })
13448
15069
  });
13449
15070
  if (!rmResp.ok)
@@ -13462,7 +15083,9 @@ var plugin = definePluginEntry({
13462
15083
  const friendId = p.friendId;
13463
15084
  if (!friendId)
13464
15085
  return { error: "Missing friendId parameter" };
13465
- 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
+ });
13466
15089
  if (!resp.ok)
13467
15090
  return { error: "Server error: " + await resp.text() };
13468
15091
  const data = await resp.json();
@@ -13484,7 +15107,7 @@ var plugin = definePluginEntry({
13484
15107
  }
13485
15108
  const resp = await fetch(serverUrl + "/api/v1/friends/" + friendId + "/permissions", {
13486
15109
  method: "PUT",
13487
- headers: { "Content-Type": "application/json" },
15110
+ headers: serverClient.authHeaders(),
13488
15111
  body: JSON.stringify({ nodeId: aicqAgentId, permissions })
13489
15112
  });
13490
15113
  if (!resp.ok)
@@ -13503,7 +15126,9 @@ var plugin = definePluginEntry({
13503
15126
  });
13504
15127
  api.registerGatewayMethod("aicq.friends.requests", async (params) => {
13505
15128
  try {
13506
- 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
+ });
13507
15132
  if (!resp.ok)
13508
15133
  return { error: "Server error: " + await resp.text() };
13509
15134
  const data = await resp.json();
@@ -13525,7 +15150,7 @@ var plugin = definePluginEntry({
13525
15150
  }
13526
15151
  const resp = await fetch(serverUrl + "/api/v1/friends/requests/" + requestId + "/accept", {
13527
15152
  method: "POST",
13528
- headers: { "Content-Type": "application/json" },
15153
+ headers: serverClient.authHeaders(),
13529
15154
  body: JSON.stringify(body)
13530
15155
  });
13531
15156
  if (!resp.ok)
@@ -13545,7 +15170,7 @@ var plugin = definePluginEntry({
13545
15170
  return { success: false, message: "Missing requestId parameter" };
13546
15171
  const resp = await fetch(serverUrl + "/api/v1/friends/requests/" + requestId + "/reject", {
13547
15172
  method: "POST",
13548
- headers: { "Content-Type": "application/json" },
15173
+ headers: serverClient.authHeaders(),
13549
15174
  body: JSON.stringify({})
13550
15175
  });
13551
15176
  if (!resp.ok)