aicq-openclaw-plugin 1.0.6 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { createRequire } from "module"; const require = createRequire(import.meta.url);
1
2
  var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -4921,9 +4922,9 @@ var require_lib = __commonJS({
4921
4922
  }
4922
4923
  });
4923
4924
 
4924
- // ../node_modules/tweetnacl/nacl-fast.js
4925
+ // node_modules/tweetnacl/nacl-fast.js
4925
4926
  var require_nacl_fast = __commonJS({
4926
- "../node_modules/tweetnacl/nacl-fast.js"(exports, module) {
4927
+ "node_modules/tweetnacl/nacl-fast.js"(exports, module) {
4927
4928
  (function(nacl3) {
4928
4929
  "use strict";
4929
4930
  var gf = function(init) {
@@ -7145,9 +7146,9 @@ var require_nacl_fast = __commonJS({
7145
7146
  }
7146
7147
  });
7147
7148
 
7148
- // ../node_modules/tweetnacl-util/nacl-util.js
7149
+ // node_modules/tweetnacl-util/nacl-util.js
7149
7150
  var require_nacl_util = __commonJS({
7150
- "../node_modules/tweetnacl-util/nacl-util.js"(exports, module) {
7151
+ "node_modules/tweetnacl-util/nacl-util.js"(exports, module) {
7151
7152
  (function(root, f) {
7152
7153
  "use strict";
7153
7154
  if (typeof module !== "undefined" && module.exports) module.exports = f();
@@ -7211,9 +7212,9 @@ var require_nacl_util = __commonJS({
7211
7212
  }
7212
7213
  });
7213
7214
 
7214
- // ../shared/crypto/dist/nacl.js
7215
+ // node_modules/@aicq/crypto/nacl.js
7215
7216
  var require_nacl = __commonJS({
7216
- "../shared/crypto/dist/nacl.js"(exports) {
7217
+ "node_modules/@aicq/crypto/nacl.js"(exports) {
7217
7218
  "use strict";
7218
7219
  var __importDefault = exports && exports.__importDefault || function(mod) {
7219
7220
  return mod && mod.__esModule ? mod : { "default": mod };
@@ -7230,9 +7231,9 @@ var require_nacl = __commonJS({
7230
7231
  }
7231
7232
  });
7232
7233
 
7233
- // ../shared/crypto/dist/keygen.js
7234
+ // node_modules/@aicq/crypto/keygen.js
7234
7235
  var require_keygen = __commonJS({
7235
- "../shared/crypto/dist/keygen.js"(exports) {
7236
+ "node_modules/@aicq/crypto/keygen.js"(exports) {
7236
7237
  "use strict";
7237
7238
  Object.defineProperty(exports, "__esModule", { value: true });
7238
7239
  exports.generateSigningKeyPair = generateSigningKeyPair2;
@@ -7271,9 +7272,9 @@ var require_keygen = __commonJS({
7271
7272
  }
7272
7273
  });
7273
7274
 
7274
- // ../shared/crypto/dist/signer.js
7275
+ // node_modules/@aicq/crypto/signer.js
7275
7276
  var require_signer = __commonJS({
7276
- "../shared/crypto/dist/signer.js"(exports) {
7277
+ "node_modules/@aicq/crypto/signer.js"(exports) {
7277
7278
  "use strict";
7278
7279
  Object.defineProperty(exports, "__esModule", { value: true });
7279
7280
  exports.sign = sign;
@@ -7288,9 +7289,9 @@ var require_signer = __commonJS({
7288
7289
  }
7289
7290
  });
7290
7291
 
7291
- // ../shared/crypto/dist/keyExchange.js
7292
+ // node_modules/@aicq/crypto/keyExchange.js
7292
7293
  var require_keyExchange = __commonJS({
7293
- "../shared/crypto/dist/keyExchange.js"(exports) {
7294
+ "node_modules/@aicq/crypto/keyExchange.js"(exports) {
7294
7295
  "use strict";
7295
7296
  Object.defineProperty(exports, "__esModule", { value: true });
7296
7297
  exports.computeSharedSecret = computeSharedSecret2;
@@ -7341,9 +7342,9 @@ var require_keyExchange = __commonJS({
7341
7342
  }
7342
7343
  });
7343
7344
 
7344
- // ../shared/crypto/dist/cipher.js
7345
+ // node_modules/@aicq/crypto/cipher.js
7345
7346
  var require_cipher = __commonJS({
7346
- "../shared/crypto/dist/cipher.js"(exports) {
7347
+ "node_modules/@aicq/crypto/cipher.js"(exports) {
7347
7348
  "use strict";
7348
7349
  Object.defineProperty(exports, "__esModule", { value: true });
7349
7350
  exports.generateNonce = generateNonce;
@@ -7367,9 +7368,9 @@ var require_cipher = __commonJS({
7367
7368
  }
7368
7369
  });
7369
7370
 
7370
- // ../shared/crypto/dist/message.js
7371
+ // node_modules/@aicq/crypto/message.js
7371
7372
  var require_message = __commonJS({
7372
- "../shared/crypto/dist/message.js"(exports) {
7373
+ "node_modules/@aicq/crypto/message.js"(exports) {
7373
7374
  "use strict";
7374
7375
  Object.defineProperty(exports, "__esModule", { value: true });
7375
7376
  exports.createMessage = createMessage;
@@ -7483,9 +7484,9 @@ var require_message = __commonJS({
7483
7484
  }
7484
7485
  });
7485
7486
 
7486
- // ../shared/crypto/dist/password.js
7487
+ // node_modules/@aicq/crypto/password.js
7487
7488
  var require_password = __commonJS({
7488
- "../shared/crypto/dist/password.js"(exports) {
7489
+ "node_modules/@aicq/crypto/password.js"(exports) {
7489
7490
  "use strict";
7490
7491
  var __createBinding = exports && exports.__createBinding || (Object.create ? (function(o, m, k, k2) {
7491
7492
  if (k2 === void 0) k2 = k;
@@ -7563,9 +7564,9 @@ var require_password = __commonJS({
7563
7564
  }
7564
7565
  });
7565
7566
 
7566
- // ../shared/crypto/dist/handshake.js
7567
+ // node_modules/@aicq/crypto/handshake.js
7567
7568
  var require_handshake = __commonJS({
7568
- "../shared/crypto/dist/handshake.js"(exports) {
7569
+ "node_modules/@aicq/crypto/handshake.js"(exports) {
7569
7570
  "use strict";
7570
7571
  Object.defineProperty(exports, "__esModule", { value: true });
7571
7572
  exports.createHandshakeRequest = createHandshakeRequest2;
@@ -7674,9 +7675,9 @@ var require_handshake = __commonJS({
7674
7675
  }
7675
7676
  });
7676
7677
 
7677
- // ../shared/crypto/dist/index.js
7678
- var require_dist = __commonJS({
7679
- "../shared/crypto/dist/index.js"(exports) {
7678
+ // node_modules/@aicq/crypto/index.js
7679
+ var require_crypto = __commonJS({
7680
+ "node_modules/@aicq/crypto/index.js"(exports) {
7680
7681
  "use strict";
7681
7682
  Object.defineProperty(exports, "__esModule", { value: true });
7682
7683
  exports.completeHandshake = exports.createHandshakeResponse = exports.createHandshakeRequest = exports.decryptWithPassword = exports.encryptWithPassword = exports.decryptMessage = exports.encryptMessage = exports.parseMessage = exports.createMessage = exports.generateNonce = exports.decrypt = exports.encrypt = exports.deriveSessionKey = exports.computeSharedSecret = exports.verify = exports.sign = exports.getPublicKeyFingerprint = exports.deriveX25519FromEd25519 = exports.generateKeyExchangeKeyPair = exports.generateSigningKeyPair = exports.encodeBase64 = exports.decodeBase64 = exports.encodeUTF8 = exports.decodeUTF8 = exports.nacl = void 0;
@@ -7775,7 +7776,6 @@ import { definePluginEntry } from "openclaw/plugin-sdk/core";
7775
7776
  // dist/config.js
7776
7777
  import * as fs from "fs";
7777
7778
  import * as path from "path";
7778
- import { fileURLToPath } from "url";
7779
7779
 
7780
7780
  // node_modules/uuid/dist/esm-node/rng.js
7781
7781
  import crypto from "crypto";
@@ -7825,13 +7825,12 @@ function v4(options, buf, offset) {
7825
7825
  var v4_default = v4;
7826
7826
 
7827
7827
  // dist/config.js
7828
- var __filename = fileURLToPath(import.meta.url);
7829
- var __dirname = path.dirname(__filename);
7828
+ var _dirname = typeof __dirname !== "undefined" ? __dirname : process.cwd();
7830
7829
  var SERVER_URL = process.env.AICQ_SERVER_URL || "https://aicq.online:61018";
7831
7830
  function loadConfig(overrides) {
7832
7831
  let schemaDefaults = {};
7833
7832
  try {
7834
- const manifestPath = path.resolve(__dirname, "..", "openclaw.plugin.json");
7833
+ const manifestPath = path.resolve(_dirname, "..", "openclaw.plugin.json");
7835
7834
  const manifestRaw = fs.readFileSync(manifestPath, "utf-8");
7836
7835
  const manifest = JSON.parse(manifestRaw);
7837
7836
  const schema = manifest.configSchema;
@@ -8191,7 +8190,7 @@ var PluginStore = class {
8191
8190
 
8192
8191
  // dist/services/identityService.js
8193
8192
  var import_qrcode = __toESM(require_lib(), 1);
8194
- var import_crypto3 = __toESM(require_dist(), 1);
8193
+ var import_crypto3 = __toESM(require_crypto(), 1);
8195
8194
  import * as crypto4 from "crypto";
8196
8195
  var IdentityService = class {
8197
8196
  constructor(store, logger) {
@@ -8676,7 +8675,7 @@ var ServerClient = class {
8676
8675
  };
8677
8676
 
8678
8677
  // dist/handshake/handshakeManager.js
8679
- var import_crypto4 = __toESM(require_dist(), 1);
8678
+ var import_crypto4 = __toESM(require_crypto(), 1);
8680
8679
  import * as crypto5 from "crypto";
8681
8680
  var HandshakeManager = class {
8682
8681
  constructor(store, serverClient, config2, logger) {
@@ -9092,7 +9091,7 @@ var P2PConnectionManager = class {
9092
9091
  };
9093
9092
 
9094
9093
  // dist/fileTransfer/transferManager.js
9095
- var import_crypto5 = __toESM(require_dist(), 1);
9094
+ var import_crypto5 = __toESM(require_crypto(), 1);
9096
9095
  import * as fs3 from "fs";
9097
9096
  import * as path3 from "path";
9098
9097
  var DEFAULT_CHUNK_SIZE = 64 * 1024;
@@ -9315,7 +9314,7 @@ var FileTransferManager = class {
9315
9314
  };
9316
9315
 
9317
9316
  // dist/channels/encryptedChat.js
9318
- var import_crypto6 = __toESM(require_dist(), 1);
9317
+ var import_crypto6 = __toESM(require_crypto(), 1);
9319
9318
  import * as fs4 from "fs";
9320
9319
  import * as path4 from "path";
9321
9320
  function safeFilePath(filePath, allowedDir) {
@@ -9755,7 +9754,7 @@ var BeforeToolCallHook = class {
9755
9754
  };
9756
9755
 
9757
9756
  // dist/hooks/messageSending.js
9758
- var import_crypto7 = __toESM(require_dist(), 1);
9757
+ var import_crypto7 = __toESM(require_crypto(), 1);
9759
9758
  var MessageSendingHook = class {
9760
9759
  constructor(store, handshakeManager, logger) {
9761
9760
  this.store = store;
@@ -10072,6 +10071,20 @@ tbody tr:hover { background: var(--bg3); }
10072
10071
  /* Section desc */
10073
10072
  .section-desc { font-size: 13px; color: var(--text2); margin-bottom: 20px; line-height: 1.6; }
10074
10073
 
10074
+ /* Toggle switch */
10075
+ .toggle-label { display: flex; align-items: center; gap: 10px; cursor: pointer; font-size: 13px; color: var(--text); user-select: none; text-transform: none !important; letter-spacing: normal !important; font-weight: 400 !important; }
10076
+ .toggle-label input[type=checkbox] { display: none; }
10077
+ .toggle-slider {
10078
+ width: 40px; height: 22px; background: var(--bg4); border-radius: 11px;
10079
+ position: relative; transition: background var(--transition); flex-shrink: 0;
10080
+ }
10081
+ .toggle-slider::after {
10082
+ content: ''; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px;
10083
+ background: var(--text3); border-radius: 50%; transition: all var(--transition);
10084
+ }
10085
+ .toggle-label input:checked + .toggle-slider { background: var(--accent); }
10086
+ .toggle-label input:checked + .toggle-slider::after { left: 21px; background: #fff; }
10087
+
10075
10088
  /* Responsive */
10076
10089
  @media (max-width: 768px) {
10077
10090
  .sidebar { position: fixed; left: -260px; z-index: 50; height: 100vh; transition: left var(--transition); }
@@ -10114,37 +10127,14 @@ function createToastContainer() {
10114
10127
  return c;
10115
10128
  }
10116
10129
 
10117
- // \u2500\u2500 API with timeout \u2500\u2500
10130
+ // \u2500\u2500 API \u2500\u2500
10118
10131
  async function api(path, opts = {}) {
10119
- const timeout = (opts._timeout || 8000);
10120
- const controller = new AbortController();
10121
- const timer = setTimeout(() => controller.abort(), timeout);
10122
10132
  try {
10123
- const res = await fetch(API + path, { headers: { 'Content-Type': 'application/json', ...opts.headers }, ...opts, signal: controller.signal });
10124
- clearTimeout(timer);
10133
+ const res = await fetch(API + path, { headers: { 'Content-Type': 'application/json', ...opts.headers }, ...opts });
10125
10134
  const data = await res.json();
10126
10135
  if (!res.ok && !data.error) data.error = 'HTTP ' + res.status;
10127
10136
  return data;
10128
- } catch (e) {
10129
- clearTimeout(timer);
10130
- const msg = e.name === 'AbortError' ? 'Request timed out (' + (timeout/1000) + 's)' : e.message;
10131
- return { error: msg };
10132
- }
10133
- }
10134
-
10135
- // Safe helper: run promise, never throw, return { data, error }
10136
- async function safeApi(path, opts) {
10137
- try {
10138
- const data = await api(path, opts);
10139
- return { data, error: data.error || null };
10140
- } catch (e) {
10141
- return { data: null, error: e.message || 'Unknown error' };
10142
- }
10143
- }
10144
-
10145
- // Render an inline error block
10146
- function errorBlock(title, msg) {
10147
- return '<div class="card" style="border-color:var(--danger)"><div style="display:flex;align-items:center;gap:10px"><span style="font-size:20px">\u26A0\uFE0F</span><div><div style="font-weight:600;color:var(--danger)">' + escHtml(title) + '</div><div style="font-size:12px;color:var(--text3);margin-top:2px">' + escHtml(msg) + '</div></div></div></div>';
10137
+ } catch (e) { return { error: e.message }; }
10148
10138
  }
10149
10139
 
10150
10140
  // \u2500\u2500 Utilities \u2500\u2500
@@ -10197,49 +10187,14 @@ function loadPage(page) {
10197
10187
  async function loadDashboard() {
10198
10188
  const el = $('#dashboard-content');
10199
10189
  html(el, '<div class="loading-mask"><div class="spinner"></div>Loading dashboard...</div>');
10200
-
10201
- // Load local data first (status + identity), then remote (friends) \u2014 each independent
10202
- const [statusRes, identityRes] = await Promise.all([safeApi('/status'), safeApi('/identity')]);
10203
- const status = statusRes.data || {};
10204
- const identity = identityRes.data || {};
10205
-
10206
- // Friends is remote \u2014 load independently, don't block
10207
- const friendsRes = await safeApi('/friends', { _timeout: 10000 });
10208
- const friendList = (friendsRes.data?.friends) || [];
10209
- const friendsError = friendsRes.error;
10210
-
10190
+ const [status, friends, identity] = await Promise.all([api('/status'), api('/friends'), api('/identity')]);
10191
+ if (status.error) { html(el, '<div class="empty"><div class="icon">\u26A0\uFE0F</div><p>Failed to connect to AICQ plugin</p></div>'); return; }
10211
10192
  const connCls = status.connected ? 'dot-ok' : 'dot-err';
10212
10193
  const connText = status.connected ? 'Connected' : 'Disconnected';
10194
+ const friendList = friends.friends || [];
10213
10195
  const aiFriends = friendList.filter(f => f.friendType === 'ai').length;
10214
10196
  const humanFriends = friendList.filter(f => f.friendType !== 'ai').length;
10215
10197
 
10216
- // Update header status dot
10217
- const hDot = $('#header-dot');
10218
- if (hDot) hDot.className = 'dot ' + (status.connected ? 'dot-ok' : 'dot-err');
10219
- const hTxt = $('#header-status');
10220
- if (hTxt) hTxt.textContent = status.connected ? 'Connected' : 'Disconnected';
10221
-
10222
- // Build friends section
10223
- let friendsSection = '';
10224
- if (friendsError) {
10225
- friendsSection = '<div class="card"><div class="card-header"><div class="card-title">\u{1F4CB} Friends</div><button class="btn btn-sm btn-ghost" onclick="navigate('friends')">View All \u2192</button></div>' + errorBlock('Friend List Unavailable', friendsError + ' \u2014 Remote AICQ server may be unreachable. <button class="btn btn-sm btn-default" style="margin-top:8px" onclick="loadDashboard()">\u{1F504} Retry</button>') + '</div>';
10226
- } else {
10227
- friendsSection = '<div class="card"><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>' + renderMiniFriendList(friendList.slice(0, 5)) + '</div>';
10228
- }
10229
-
10230
- // Build identity section
10231
- let identitySection = '';
10232
- if (identityRes.error) {
10233
- identitySection = errorBlock('Identity Unavailable', identityRes.error);
10234
- } else {
10235
- identitySection = \\\`
10236
- <div class="detail-row"><div class="detail-key">Agent ID</div><div class="detail-val mono" style="cursor:pointer" onclick="copyText('\${escHtml(identity.agentId || '')}')">\${escHtml(identity.agentId || '\u2014')} \u{1F4CB}</div></div>
10237
- <div class="detail-row"><div class="detail-key">Fingerprint</div><div class="detail-val mono">\${escHtml(identity.publicKeyFingerprint || '\u2014')}</div></div>
10238
- <div class="detail-row"><div class="detail-key">Server URL</div><div class="detail-val mono" style="cursor:pointer" onclick="copyText('\${escHtml(identity.serverUrl || '')}')">\${escHtml(identity.serverUrl || '\u2014')} \u{1F4CB}</div></div>
10239
- <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>
10240
- \\\`;
10241
- }
10242
-
10243
10198
  html(el, \\\`
10244
10199
  <div class="stats-grid">
10245
10200
  <div class="stat-card">
@@ -10248,13 +10203,13 @@ async function loadDashboard() {
10248
10203
  <div class="stat-value" style="font-size:16px;display:flex;align-items:center;gap:8px">
10249
10204
  <span class="dot \${connCls}"></span> \${connText}
10250
10205
  </div>
10251
- <div class="stat-sub">\${escHtml(status.serverUrl || '\u2014')}</div>
10206
+ <div class="stat-sub">\${escHtml(status.serverUrl)}</div>
10252
10207
  </div>
10253
10208
  <div class="stat-card">
10254
10209
  <div class="stat-icon" style="background:var(--ok-bg)">\u{1F465}</div>
10255
10210
  <div class="stat-label">Total Friends</div>
10256
- <div class="stat-value">\${friendsError ? '\u2014' : friendList.length}</div>
10257
- <div class="stat-sub">\${friendsError ? 'unavailable' : aiFriends + ' AI \xB7 ' + humanFriends + ' Human'}</div>
10211
+ <div class="stat-value">\${friendList.length}</div>
10212
+ <div class="stat-sub">\${aiFriends} AI \xB7 \${humanFriends} Human</div>
10258
10213
  </div>
10259
10214
  <div class="stat-card">
10260
10215
  <div class="stat-icon" style="background:var(--info-bg)">\u{1F517}</div>
@@ -10265,15 +10220,21 @@ async function loadDashboard() {
10265
10220
  <div class="stat-card">
10266
10221
  <div class="stat-icon" style="background:var(--warn-bg)">\u{1F511}</div>
10267
10222
  <div class="stat-label">Agent ID</div>
10268
- <div class="stat-value mono" style="font-size:13px">\${escHtml(status.agentId || '\u2014')}</div>
10269
- <div class="stat-sub">Fingerprint: \${escHtml(status.fingerprint || '\u2014')}</div>
10223
+ <div class="stat-value mono" style="font-size:13px">\${escHtml(status.agentId)}</div>
10224
+ <div class="stat-sub">Fingerprint: \${escHtml(status.fingerprint)}</div>
10270
10225
  </div>
10271
10226
  </div>
10272
10227
  <div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
10273
- \${friendsSection}
10228
+ <div class="card">
10229
+ <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>
10230
+ \${renderMiniFriendList(friendList.slice(0, 5))}
10231
+ </div>
10274
10232
  <div class="card">
10275
10233
  <div class="card-header"><div class="card-title">\u{1F916} Identity Info</div></div>
10276
- \${identitySection}
10234
+ <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>
10235
+ <div class="detail-row"><div class="detail-key">Fingerprint</div><div class="detail-val mono">\${escHtml(identity.publicKeyFingerprint)}</div></div>
10236
+ <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>
10237
+ <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>
10277
10238
  </div>
10278
10239
  </div>
10279
10240
  \\\`);
@@ -10297,6 +10258,7 @@ async function loadAgents() {
10297
10258
  const data = await api('/agents');
10298
10259
  if (data.error) { html(el, '<div class="empty"><div class="icon">\u26A0\uFE0F</div><p>' + escHtml(data.error) + '</p></div>'); return; }
10299
10260
 
10261
+ window._lastAgentsData = data;
10300
10262
  const agents = data.agents || [];
10301
10263
  const configSource = data.configSource || 'unknown';
10302
10264
 
@@ -10315,6 +10277,7 @@ async function loadAgents() {
10315
10277
  <td>
10316
10278
  <div class="actions-cell">
10317
10279
  <button class="btn btn-sm btn-ghost" onclick="viewAgent(\${i})" title="View">\u{1F441}\uFE0F</button>
10280
+ <button class="btn btn-sm btn-ok" onclick="showEditAgentModal(\${i})" title="Edit">\u270F\uFE0F</button>
10318
10281
  <button class="btn btn-sm btn-danger" onclick="deleteAgent(\${i})" title="Delete">\u{1F5D1}\uFE0F</button>
10319
10282
  </div>
10320
10283
  </td>
@@ -10332,6 +10295,7 @@ async function loadAgents() {
10332
10295
  html(el, \\\`
10333
10296
  <div class="toolbar">
10334
10297
  <div class="search-box"><input type="text" placeholder="Search agents..." id="agent-search" oninput="filterAgentTable()"></div>
10298
+ <button class="btn btn-sm btn-primary" onclick="showAddAgentModal()">\u2795 Add Agent</button>
10335
10299
  <button class="btn btn-sm btn-default" onclick="loadAgents()">\u{1F504} Refresh</button>
10336
10300
  </div>
10337
10301
  <p class="section-desc">Agent list from <strong style="color:var(--accent2)">\${escHtml(configSource)}</strong>. Total: <strong>\${agents.length}</strong> agents configured.</p>
@@ -10376,6 +10340,83 @@ async function deleteAgent(index) {
10376
10340
  else { toast(r.message || r.error || 'Delete failed', 'err'); }
10377
10341
  }
10378
10342
 
10343
+ let _editAgentIndex = null;
10344
+
10345
+ function showAddAgentModal() {
10346
+ _editAgentIndex = null;
10347
+ $('#agent-form-title').textContent = '\u2795 Add New Agent';
10348
+ $('#agent-form-name').value = '';
10349
+ $('#agent-form-id').value = '';
10350
+ $('#agent-form-model').value = '';
10351
+ $('#agent-form-provider').value = '';
10352
+ $('#agent-form-prompt').value = '';
10353
+ $('#agent-form-enabled').checked = true;
10354
+ $('#agent-form-temperature').value = '0.7';
10355
+ $('#agent-form-max-tokens').value = '4096';
10356
+ $('#agent-form-top-p').value = '1';
10357
+ $('#agent-form-tools').value = '';
10358
+ showModal('modal-add-agent');
10359
+ setTimeout(() => $('#agent-form-name')?.focus(), 100);
10360
+ }
10361
+
10362
+ function showEditAgentModal(index) {
10363
+ const agents = window._lastAgentsData?.agents || [];
10364
+ const a = agents[index];
10365
+ if (!a) return;
10366
+ _editAgentIndex = index;
10367
+ $('#agent-form-title').textContent = '\u270F\uFE0F Edit Agent';
10368
+ $('#agent-form-name').value = a.name || '';
10369
+ $('#agent-form-id').value = a.id || '';
10370
+ $('#agent-form-model').value = a.model || '';
10371
+ $('#agent-form-provider').value = a.provider || '';
10372
+ $('#agent-form-prompt').value = a.systemPrompt || '';
10373
+ $('#agent-form-enabled').checked = a.enabled !== false;
10374
+ $('#agent-form-temperature').value = a.temperature ?? 0.7;
10375
+ $('#agent-form-max-tokens').value = a.maxTokens ?? 4096;
10376
+ $('#agent-form-top-p').value = a.topP ?? 1;
10377
+ $('#agent-form-tools').value = Array.isArray(a.tools) ? a.tools.join(', ') : (a.tools || '');
10378
+ showModal('modal-add-agent');
10379
+ }
10380
+
10381
+ async function saveAgent() {
10382
+ const tempVal = parseFloat($('#agent-form-temperature')?.value);
10383
+ const maxTokensVal = parseInt($('#agent-form-max-tokens')?.value, 10);
10384
+ const topPVal = parseFloat($('#agent-form-top-p')?.value);
10385
+ const toolsRaw = $('#agent-form-tools')?.value?.trim() || '';
10386
+
10387
+ const agent = {
10388
+ name: $('#agent-form-name')?.value?.trim() || '',
10389
+ id: $('#agent-form-id')?.value?.trim() || '',
10390
+ model: $('#agent-form-model')?.value?.trim() || '',
10391
+ provider: $('#agent-form-provider')?.value?.trim() || '',
10392
+ systemPrompt: $('#agent-form-prompt')?.value?.trim() || '',
10393
+ enabled: $('#agent-form-enabled')?.checked ?? true,
10394
+ temperature: isNaN(tempVal) ? 0.7 : Math.min(2, Math.max(0, tempVal)),
10395
+ maxTokens: isNaN(maxTokensVal) ? 4096 : maxTokensVal,
10396
+ topP: isNaN(topPVal) ? 1 : Math.min(1, Math.max(0, topPVal)),
10397
+ tools: toolsRaw ? toolsRaw.split(',').map(t => t.trim()).filter(Boolean) : [],
10398
+ };
10399
+
10400
+ if (!agent.name) { toast('Agent name is required', 'warn'); return; }
10401
+
10402
+ let r;
10403
+ if (_editAgentIndex !== null) {
10404
+ // Edit existing
10405
+ r = await api('/agents/' + _editAgentIndex, { method: 'PUT', body: JSON.stringify({ agent }) });
10406
+ } else {
10407
+ // Add new
10408
+ r = await api('/agents', { method: 'POST', body: JSON.stringify({ agent }) });
10409
+ }
10410
+
10411
+ if (r.success) {
10412
+ toast(_editAgentIndex !== null ? 'Agent updated' : 'Agent added', 'ok');
10413
+ hideModal('modal-add-agent');
10414
+ loadAgents();
10415
+ } else {
10416
+ toast(r.message || r.error || 'Failed', 'err');
10417
+ }
10418
+ }
10419
+
10379
10420
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
10380
10421
  // PAGE: Friends Management
10381
10422
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
@@ -10384,17 +10425,12 @@ let friendsFilter = 'all';
10384
10425
  async function loadFriends() {
10385
10426
  const el = $('#friends-content');
10386
10427
  html(el, '<div class="loading-mask"><div class="spinner"></div>Loading friends...</div>');
10428
+ const [friends, requests, sessions] = await Promise.all([api('/friends'), api('/friends/requests'), api('/sessions')]);
10387
10429
 
10388
- // Load all three independently \u2014 any can fail without blocking others
10389
- const [friendsRes, requestsRes, sessionsRes] = await Promise.all([
10390
- safeApi('/friends', { _timeout: 10000 }),
10391
- safeApi('/friends/requests', { _timeout: 10000 }),
10392
- safeApi('/sessions', { _timeout: 5000 }),
10393
- ]);
10394
-
10395
- const friendCount = (friendsRes.data?.friends || []).length;
10396
- const reqCount = (requestsRes.data?.requests || []).length;
10397
- const sessCount = (sessionsRes.data?.sessions || []).length;
10430
+ // Sub-tabs
10431
+ const friendCount = (friends.friends || []).length;
10432
+ const reqCount = (requests.requests || []).length;
10433
+ const sessCount = (sessions.sessions || []).length;
10398
10434
 
10399
10435
  html('#friends-tabs', \\\`
10400
10436
  <button class="filter-btn \${friendsSubTab==='friends'?'active':''}" onclick="friendsSubTab='friends';loadFriends()">\u{1F465} Friends (<span id="fc">\${friendCount}</span>)</button>
@@ -10402,29 +10438,13 @@ async function loadFriends() {
10402
10438
  <button class="filter-btn \${friendsSubTab==='sessions'?'active':''}" onclick="friendsSubTab='sessions';loadFriends()">\u{1F517} Sessions (<span id="sc">\${sessCount}</span>)</button>
10403
10439
  \\\`);
10404
10440
 
10405
- window._friendsData = friendsRes.data || {};
10406
- window._requestsData = requestsRes.data || {};
10407
- window._sessionsData = sessionsRes.data || {};
10441
+ window._friendsData = friends;
10442
+ window._requestsData = requests;
10443
+ window._sessionsData = sessions;
10408
10444
 
10409
- if (friendsSubTab === 'friends') {
10410
- if (friendsRes.error) {
10411
- html(el, '<div class="toolbar"><button class="btn btn-sm btn-default" onclick="loadFriends()">\u{1F504} Retry</button></div>' + errorBlock('Cannot Load Friends', friendsRes.error + ' \u2014 AICQ remote server is unreachable. Check your network or server status.'));
10412
- } else {
10413
- renderFriendsList(friendsRes.data.friends || []);
10414
- }
10415
- } else if (friendsSubTab === 'requests') {
10416
- if (requestsRes.error) {
10417
- html(el, '<div class="toolbar"><button class="btn btn-sm btn-default" onclick="loadFriends()">\u{1F504} Retry</button></div>' + errorBlock('Cannot Load Requests', requestsRes.error));
10418
- } else {
10419
- renderRequestsList(requestsRes.data.requests || []);
10420
- }
10421
- } else {
10422
- if (sessionsRes.error) {
10423
- html(el, '<div class="toolbar"><button class="btn btn-sm btn-default" onclick="loadFriends()">\u{1F504} Retry</button></div>' + errorBlock('Cannot Load Sessions', sessionsRes.error));
10424
- } else {
10425
- renderSessionsList(sessionsRes.data.sessions || []);
10426
- }
10427
- }
10445
+ if (friendsSubTab === 'friends') renderFriendsList(friends.friends || []);
10446
+ else if (friendsSubTab === 'requests') renderRequestsList(requests.requests || []);
10447
+ else renderSessionsList(sessions.sessions || []);
10428
10448
  }
10429
10449
  window.friendsSubTab = 'friends';
10430
10450
 
@@ -10621,7 +10641,12 @@ function renderModels(data) {
10621
10641
  <td class="mono">\${escHtml(m.modelId)}</td>
10622
10642
  <td><span class="badge badge-ok">\u25CF Key set</span></td>
10623
10643
  <td class="mono" style="font-size:11px">\${escHtml(m.baseUrl || 'default')}</td>
10624
- <td><button class="btn btn-sm btn-ghost" onclick="showModelConfigModal('\${escHtml(m.providerId)}')">Edit</button></td>
10644
+ <td>
10645
+ <div class="actions-cell">
10646
+ <button class="btn btn-sm btn-ghost" onclick="showModelConfigModal('\${escHtml(m.providerId)}')">Edit</button>
10647
+ <button class="btn btn-sm btn-danger" onclick="deleteModelProvider('\${escHtml(m.providerId)}')" title="Delete">\u{1F5D1}\uFE0F</button>
10648
+ </div>
10649
+ </td>
10625
10650
  </tr>\\\`;
10626
10651
  });
10627
10652
  activeModelsSection = \\\`
@@ -10676,73 +10701,660 @@ async function saveModelConfig() {
10676
10701
  if (r.success) { toast(r.message || 'Configuration saved!', 'ok'); loadModels(); }
10677
10702
  else { toast(r.message || r.error || 'Failed to save', 'err'); }
10678
10703
  }
10704
+ async function deleteModelProvider(providerId) {
10705
+ if (!confirm('Delete configuration for provider "' + providerId + '"? This will remove its API key and model settings.')) return;
10706
+ const r = await api('/models/' + encodeURIComponent(providerId), { method: 'DELETE' });
10707
+ if (r.success) { toast('Provider configuration deleted', 'ok'); loadModels(); }
10708
+ else { toast(r.message || r.error || 'Delete failed', 'err'); }
10709
+ }
10679
10710
 
10680
10711
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
10681
- // PAGE: Settings
10712
+ // PAGE: Settings (comprehensive with AJAX, tabs, live test)
10682
10713
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
10714
+ let _settingsSaving = false;
10715
+ let _settingsData = null;
10716
+ let _settingsTab = 'connection';
10717
+
10718
+ function formatBytes(bytes) {
10719
+ if (!bytes || bytes === 0) return '0 B';
10720
+ const k = 1024, sizes = ['B', 'KB', 'MB', 'GB'];
10721
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
10722
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
10723
+ }
10724
+
10725
+ function formatUptime(seconds) {
10726
+ if (!seconds) return '\u2014';
10727
+ const d = Math.floor(seconds / 86400), h = Math.floor((seconds % 86400) / 3600);
10728
+ const m = Math.floor((seconds % 3600) / 60), s = seconds % 60;
10729
+ let parts = [];
10730
+ if (d > 0) parts.push(d + 'd');
10731
+ if (h > 0) parts.push(h + 'h');
10732
+ if (m > 0) parts.push(m + 'm');
10733
+ parts.push(s + 's');
10734
+ return parts.join(' ');
10735
+ }
10736
+
10683
10737
  async function loadSettings() {
10684
10738
  const el = $('#settings-content');
10685
10739
  html(el, '<div class="loading-mask"><div class="spinner"></div>Loading settings...</div>');
10686
- const [statusRes, identityRes, configRes] = await Promise.all([safeApi('/status'), safeApi('/identity'), safeApi('/config')]);
10687
10740
 
10688
- let out = '<p class="section-desc">AICQ plugin runtime configuration and system information.</p>';
10741
+ const settings = await api('/settings');
10742
+ if (settings.error) {
10743
+ html(el, '<div class="empty"><div class="icon">\u26A0\uFE0F</div><p>' + escHtml(settings.error) + '</p></div>');
10744
+ return;
10745
+ }
10746
+
10747
+ _settingsData = settings;
10748
+
10749
+ // Render settings tabs nav
10750
+ html('#settings-tabs', \\\`
10751
+ <button class="filter-btn \${_settingsTab==='connection'?'active':''}" onclick="_settingsTab='connection';renderSettingsTab()">\u{1F50C} Connection</button>
10752
+ <button class="filter-btn \${_settingsTab==='friends'?'active':''}" onclick="_settingsTab='friends';renderSettingsTab()">\u{1F465} Friends</button>
10753
+ <button class="filter-btn \${_settingsTab==='security'?'active':''}" onclick="_settingsTab='security';renderSettingsTab()">\u{1F512} Security</button>
10754
+ <button class="filter-btn \${_settingsTab==='advanced'?'active':''}" onclick="_settingsTab='advanced';renderSettingsTab()">\u2699\uFE0F Advanced</button>
10755
+ <button class="filter-btn \${_settingsTab==='json'?'active':''}" onclick="_settingsTab='json';renderSettingsTab()">\u{1F4DD} JSON Editor</button>
10756
+ \\\`);
10757
+
10758
+ renderSettingsTab();
10759
+ }
10760
+
10761
+ function renderSettingsTab() {
10762
+ // Update tab buttons
10763
+ html('#settings-tabs', \\\`
10764
+ <button class="filter-btn \${_settingsTab==='connection'?'active':''}" onclick="_settingsTab='connection';renderSettingsTab()">\u{1F50C} Connection</button>
10765
+ <button class="filter-btn \${_settingsTab==='friends'?'active':''}" onclick="_settingsTab='friends';renderSettingsTab()">\u{1F465} Friends</button>
10766
+ <button class="filter-btn \${_settingsTab==='security'?'active':''}" onclick="_settingsTab='security';renderSettingsTab()">\u{1F512} Security</button>
10767
+ <button class="filter-btn \${_settingsTab==='advanced'?'active':''}" onclick="_settingsTab='advanced';renderSettingsTab()">\u2699\uFE0F Advanced</button>
10768
+ <button class="filter-btn \${_settingsTab==='json'?'active':''}" onclick="_settingsTab='json';renderSettingsTab()">\u{1F4DD} JSON Editor</button>
10769
+ \\\`);
10770
+
10771
+ switch (_settingsTab) {
10772
+ case 'connection': renderSettingsConnection(); break;
10773
+ case 'friends': renderSettingsFriends(); break;
10774
+ case 'security': renderSettingsSecurity(); break;
10775
+ case 'advanced': renderSettingsAdvanced(); break;
10776
+ case 'json': renderSettingsJsonEditor(); break;
10777
+ }
10778
+ }
10779
+
10780
+ function sectionSaveBtn(section, id) {
10781
+ return \\\`<button class="btn btn-primary btn-sm" id="btn-save-\${id}" onclick="saveSettingsSection('\${section}', '\${id}')">\u{1F4BE} Save</button>
10782
+ <span id="status-\${id}" style="font-size:12px;color:var(--text3);margin-left:8px"></span>\\\`;
10783
+ }
10784
+
10785
+ // \u2500\u2500 CONNECTION TAB \u2500\u2500
10786
+ function renderSettingsConnection() {
10787
+ const s = _settingsData;
10788
+ const el = $('#settings-content');
10789
+
10790
+ html(el, \\\`
10791
+ <p class="section-desc">Configure server connection and WebSocket settings. Changes require a plugin restart to take full effect.</p>
10792
+
10793
+ <div class="card">
10794
+ <div class="card-header">
10795
+ <div class="card-title">\u{1F310} Server Connection</div>
10796
+ <span class="badge badge-\${s.connected ? 'ok' : 'danger'}">\${s.connected ? '\u25CF Connected' : '\u25CB Disconnected'}</span>
10797
+ </div>
10798
+ <div class="form-group">
10799
+ <label>Server URL</label>
10800
+ <div style="display:flex;gap:8px;align-items:start">
10801
+ <div style="flex:1">
10802
+ <div class="input-prefix">
10803
+ <span class="prefix">\u{1F310}</span>
10804
+ <input type="url" id="set-server-url" value="\${escHtml(s.serverUrl || '')}" placeholder="https://aicq.online:61018">
10805
+ </div>
10806
+ <div class="hint">The HTTPS URL of the AICQ relay server. WebSocket path /ws is auto-appended.</div>
10807
+ </div>
10808
+ <button class="btn btn-ok btn-sm" id="btn-test-conn" onclick="testConnection()" style="white-space:nowrap;margin-top:1px">\u{1F50D} Test</button>
10809
+ </div>
10810
+ <div id="conn-test-result" style="margin-top:8px"></div>
10811
+ </div>
10812
+
10813
+ <div class="form-row">
10814
+ <div class="form-group">
10815
+ <label>Connection Timeout (seconds)</label>
10816
+ <input type="number" id="set-connection-timeout" value="\${s.connectionTimeout || 30}" min="5" max="120" placeholder="30">
10817
+ <div class="hint">HTTP request timeout (5\u2013120s). Default: 30s.</div>
10818
+ </div>
10819
+ <div class="form-group">
10820
+ <label>WS Auto-Reconnect</label>
10821
+ <div style="display:flex;align-items:center;gap:10px;margin-top:6px">
10822
+ <label class="toggle-label">
10823
+ <input type="checkbox" id="set-ws-auto-reconnect" \${s.wsAutoReconnect ? 'checked' : ''}>
10824
+ <span class="toggle-slider"></span>
10825
+ <span>Auto-reconnect when disconnected</span>
10826
+ </label>
10827
+ </div>
10828
+ <div class="hint">Automatically reconnect WebSocket on disconnection.</div>
10829
+ </div>
10830
+ </div>
10831
+
10832
+ <div class="form-group">
10833
+ <label>WS Reconnect Interval (seconds)</label>
10834
+ <input type="number" id="set-ws-reconnect-interval" value="\${s.wsReconnectInterval || 60}" min="5" max="600" placeholder="60">
10835
+ <div class="hint">Interval between reconnection attempts (5\u2013600s). Default: 60s.</div>
10836
+ </div>
10837
+
10838
+ <div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
10839
+ \${sectionSaveBtn('connection', 'conn')}
10840
+ </div>
10841
+ </div>
10842
+
10843
+ <div class="card">
10844
+ <div class="card-header"><div class="card-title">\u{1F4C1} Config File</div></div>
10845
+ <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>
10846
+ <div class="detail-row"><div class="detail-key">Plugin Version</div><div class="detail-val">1.0.4</div></div>
10847
+ <div class="detail-row"><div class="detail-key">Uptime</div><div class="detail-val">\${formatUptime(s.uptimeSeconds)}</div></div>
10848
+ </div>
10849
+ \\\`);
10850
+ }
10851
+
10852
+ async function testConnection() {
10853
+ const btn = $('#btn-test-conn');
10854
+ const resultEl = $('#conn-test-result');
10855
+ const url = $('#set-server-url')?.value?.trim() || _settingsData.serverUrl;
10856
+
10857
+ if (!url) { toast('Enter a server URL first', 'warn'); return; }
10858
+
10859
+ if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner" style="width:14px;height:14px;border-width:2px;margin:0"></span> Testing...'; }
10860
+ 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>');
10689
10861
 
10690
- // Connection section
10691
- if (statusRes.error) {
10692
- out += errorBlock('Connection Status Unavailable', statusRes.error);
10862
+ const r = await api('/settings/test-connection', {
10863
+ method: 'POST',
10864
+ body: JSON.stringify({ serverUrl: url, timeout: 10000 }),
10865
+ });
10866
+
10867
+ if (btn) { btn.disabled = false; btn.innerHTML = '\u{1F50D} Test'; }
10868
+
10869
+ if (r.success) {
10870
+ 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>';
10871
+ if (resultEl) html(resultEl, \\\`
10872
+ <div style="display:flex;align-items:center;gap:10px;font-size:12px;color:var(--ok)">
10873
+ <span class="dot dot-ok"></span> Connected successfully \${latencyBadge}
10874
+ \${r.serverInfo?.version ? '<span class="tag">v' + escHtml(r.serverInfo.version) + '</span>' : ''}
10875
+ </div>
10876
+ \\\`);
10877
+ toast('Connection OK! Latency: ' + r.latency + 'ms', 'ok');
10693
10878
  } else {
10694
- const s = statusRes.data;
10695
- out += \\\`
10696
- <div class="card">
10697
- <div class="card-header"><div class="card-title">\u{1F50C} Connection Settings</div></div>
10698
- <div class="detail-row"><div class="detail-key">Server URL</div><div class="detail-val mono" style="cursor:pointer" onclick="copyText('\${escHtml(s.serverUrl)}')">\${escHtml(s.serverUrl)} \u{1F4CB}</div></div>
10699
- <div class="detail-row"><div class="detail-key">WebSocket Status</div><div class="detail-val"><span class="badge badge-\${s.connected ? 'ok' : 'danger'}">\${s.connected ? 'Connected' : 'Disconnected'}</span></div></div>
10879
+ const cls = r.status === 'timeout' ? 'warn' : 'danger';
10880
+ const icon = r.status === 'timeout' ? '\u23F1\uFE0F' : '\u274C';
10881
+ if (resultEl) html(resultEl, \\\`
10882
+ <div style="font-size:12px;color:var(--\${cls});display:flex;align-items:center;gap:8px">
10883
+ \${icon} \${escHtml(r.message || 'Connection failed')}
10884
+ <span class="badge badge-ghost">\${r.latency}ms</span>
10700
10885
  </div>
10701
- \\\`;
10886
+ \\\`);
10887
+ toast(r.message || 'Connection failed', 'err');
10702
10888
  }
10889
+ }
10703
10890
 
10704
- // Identity section
10705
- if (identityRes.error) {
10706
- out += errorBlock('Agent Identity Unavailable', identityRes.error);
10707
- } else {
10708
- const i = identityRes.data;
10709
- out += \\\`
10710
- <div class="card">
10711
- <div class="card-header"><div class="card-title">\u{1F916} Agent Identity</div></div>
10712
- <div class="detail-row"><div class="detail-key">Agent ID</div><div class="detail-val mono" style="cursor:pointer" onclick="copyText('\${escHtml(i.agentId)}')">\${escHtml(i.agentId)} \u{1F4CB}</div></div>
10713
- <div class="detail-row"><div class="detail-key">Public Key Fingerprint</div><div class="detail-val mono">\${escHtml(i.publicKeyFingerprint)}</div></div>
10891
+ // \u2500\u2500 FRIENDS TAB \u2500\u2500
10892
+ function renderSettingsFriends() {
10893
+ const s = _settingsData;
10894
+ const el = $('#settings-content');
10895
+
10896
+ html(el, \\\`
10897
+ <p class="section-desc">Configure friend management, permissions, and temporary number settings.</p>
10898
+
10899
+ <div class="stats-grid" style="margin-bottom:20px">
10900
+ <div class="stat-card">
10901
+ <div class="stat-icon" style="background:var(--ok-bg)">\u{1F465}</div>
10902
+ <div class="stat-label">Friends</div>
10903
+ <div class="stat-value">\${s.friendCount || 0}</div>
10904
+ <div class="stat-sub">of \${s.maxFriends || 200} max</div>
10714
10905
  </div>
10715
- \\\`;
10906
+ <div class="stat-card">
10907
+ <div class="stat-icon" style="background:var(--info-bg)">\u{1F517}</div>
10908
+ <div class="stat-label">Sessions</div>
10909
+ <div class="stat-value">\${s.sessionCount || 0}</div>
10910
+ <div class="stat-sub">Encrypted sessions</div>
10911
+ </div>
10912
+ </div>
10913
+
10914
+ <div class="card">
10915
+ <div class="card-header"><div class="card-title">\u{1F465} Friend Limits & Permissions</div></div>
10916
+ <div class="form-row">
10917
+ <div class="form-group">
10918
+ <label>Max Friends</label>
10919
+ <input type="number" id="set-max-friends" value="\${s.maxFriends || 200}" min="1" max="10000" placeholder="200">
10920
+ <div class="hint">Maximum number of encrypted friend connections (1\u201310,000).</div>
10921
+ </div>
10922
+ <div class="form-group">
10923
+ <label>Auto-Accept Friends</label>
10924
+ <div style="display:flex;align-items:center;gap:10px;margin-top:6px">
10925
+ <label class="toggle-label">
10926
+ <input type="checkbox" id="set-auto-accept" \${s.autoAcceptFriends ? 'checked' : ''}>
10927
+ <span class="toggle-slider"></span>
10928
+ <span>Automatically accept requests</span>
10929
+ </label>
10930
+ </div>
10931
+ <div class="hint">When enabled, incoming friend requests are accepted without review.</div>
10932
+ </div>
10933
+ </div>
10934
+ <div class="form-group">
10935
+ <label>Default Permissions for New Friends</label>
10936
+ <div style="display:flex;gap:16px;margin-top:6px;flex-wrap:wrap">
10937
+ <label class="toggle-label">
10938
+ <input type="checkbox" id="set-perm-chat" \${(s.defaultPermissions || []).includes('chat') ? 'checked' : ''}>
10939
+ <span class="toggle-slider"></span>
10940
+ <span>\u{1F4AC} Chat</span>
10941
+ </label>
10942
+ <label class="toggle-label">
10943
+ <input type="checkbox" id="set-perm-exec" \${(s.defaultPermissions || []).includes('exec') ? 'checked' : ''}>
10944
+ <span class="toggle-slider"></span>
10945
+ <span>\u{1F527} Exec</span>
10946
+ </label>
10947
+ </div>
10948
+ <div class="hint">Default permissions applied when auto-accepting new friend requests.</div>
10949
+ </div>
10950
+ <div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
10951
+ \${sectionSaveBtn('friends', 'friends')}
10952
+ </div>
10953
+ </div>
10954
+
10955
+ <div class="card">
10956
+ <div class="card-header"><div class="card-title">\u{1F522} Temporary Numbers</div></div>
10957
+ <div class="form-group">
10958
+ <label>Temp Number Expiry (seconds)</label>
10959
+ <input type="number" id="set-temp-expiry" value="\${s.tempNumberExpiry || 300}" min="60" max="3600" placeholder="300">
10960
+ <div class="hint">How long a temporary friend number remains valid (60\u20133600s). Default: 5 minutes.</div>
10961
+ </div>
10962
+ <div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
10963
+ \${sectionSaveBtn('temp', 'temp')}
10964
+ </div>
10965
+ </div>
10966
+ \\\`);
10967
+ }
10968
+
10969
+ // \u2500\u2500 SECURITY TAB \u2500\u2500
10970
+ function renderSettingsSecurity() {
10971
+ const s = _settingsData;
10972
+ const el = $('#settings-content');
10973
+
10974
+ html(el, \\\`
10975
+ <p class="section-desc">Configure encryption, P2P, and identity security settings.</p>
10976
+
10977
+ <div class="card">
10978
+ <div class="card-header"><div class="card-title">\u{1F916} Agent Identity</div></div>
10979
+ <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>
10980
+ <div class="detail-row"><div class="detail-key">Public Key Fingerprint</div><div class="detail-val mono">\${escHtml(s.publicKeyFingerprint || '\u2014')}</div></div>
10981
+ <div style="padding-top:12px;display:flex;gap:8px">
10982
+ <button class="btn btn-danger btn-sm" onclick="showResetIdentityModal()">\u{1F5D1}\uFE0F Reset Identity</button>
10983
+ <span style="font-size:12px;color:var(--text3);display:flex;align-items:center">\u26A0\uFE0F This deletes all friends, sessions, and keys permanently</span>
10984
+ </div>
10985
+ </div>
10986
+
10987
+ <div class="card">
10988
+ <div class="card-header"><div class="card-title">\u{1F512} P2P & Encryption</div></div>
10989
+ <div class="form-row">
10990
+ <div class="form-group">
10991
+ <label>Enable P2P Connections</label>
10992
+ <div style="display:flex;align-items:center;gap:10px;margin-top:6px">
10993
+ <label class="toggle-label">
10994
+ <input type="checkbox" id="set-enable-p2p" \${s.enableP2P ? 'checked' : ''}>
10995
+ <span class="toggle-slider"></span>
10996
+ <span>Allow direct P2P messaging</span>
10997
+ </label>
10998
+ </div>
10999
+ <div class="hint">Enable peer-to-peer encrypted connections when both parties are online.</div>
11000
+ </div>
11001
+ <div class="form-group">
11002
+ <label>Handshake Timeout (seconds)</label>
11003
+ <input type="number" id="set-handshake-timeout" value="\${s.handshakeTimeout || 60}" min="10" max="300" placeholder="60">
11004
+ <div class="hint">Noise-XK handshake timeout (10\u2013300s). Default: 60s.</div>
11005
+ </div>
11006
+ </div>
11007
+ <div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
11008
+ \${sectionSaveBtn('security', 'sec')}
11009
+ </div>
11010
+ </div>
11011
+ \\\`);
11012
+ }
11013
+
11014
+ // \u2500\u2500 ADVANCED TAB \u2500\u2500
11015
+ function renderSettingsAdvanced() {
11016
+ const s = _settingsData;
11017
+ const el = $('#settings-content');
11018
+
11019
+ html(el, \\\`
11020
+ <p class="section-desc">Advanced settings for file transfer, logging, and configuration management.</p>
11021
+
11022
+ <div class="card">
11023
+ <div class="card-header"><div class="card-title">\u{1F4CE} File Transfer</div></div>
11024
+ <div class="form-row">
11025
+ <div class="form-group">
11026
+ <label>Enable File Transfer</label>
11027
+ <div style="display:flex;align-items:center;gap:10px;margin-top:6px">
11028
+ <label class="toggle-label">
11029
+ <input type="checkbox" id="set-enable-ft" \${s.enableFileTransfer ? 'checked' : ''}>
11030
+ <span class="toggle-slider"></span>
11031
+ <span>Allow file transfers</span>
11032
+ </label>
11033
+ </div>
11034
+ <div class="hint">Enable encrypted file transfer between friends.</div>
11035
+ </div>
11036
+ <div class="form-group">
11037
+ <label>Max File Size</label>
11038
+ <select id="set-max-file-size">
11039
+ <option value="10485760" \${s.maxFileSize <= 10485760 ? 'selected' : ''}>10 MB</option>
11040
+ <option value="52428800" \${s.maxFileSize > 10485760 && s.maxFileSize <= 52428800 ? 'selected' : ''}>50 MB</option>
11041
+ <option value="104857600" \${s.maxFileSize > 52428800 && s.maxFileSize <= 104857600 ? 'selected' : ''}>100 MB</option>
11042
+ <option value="524288000" \${s.maxFileSize > 104857600 && s.maxFileSize <= 524288000 ? 'selected' : ''}>500 MB</option>
11043
+ <option value="1073741824" \${s.maxFileSize > 524288000 ? 'selected' : ''}>1 GB</option>
11044
+ </select>
11045
+ <div class="hint">Maximum file size for encrypted transfers. Current: \${formatBytes(s.maxFileSize)}.</div>
11046
+ </div>
11047
+ </div>
11048
+ <div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
11049
+ \${sectionSaveBtn('filetransfer', 'ft')}
11050
+ </div>
11051
+ </div>
11052
+
11053
+ <div class="card">
11054
+ <div class="card-header"><div class="card-title">\u{1F4CB} Logging</div></div>
11055
+ <div class="form-group">
11056
+ <label>Log Level</label>
11057
+ <select id="set-log-level" style="max-width:300px">
11058
+ <option value="debug" \${s.logLevel === 'debug' ? 'selected' : ''}>\u{1F41B} Debug \u2014 Verbose output for troubleshooting</option>
11059
+ <option value="info" \${s.logLevel === 'info' ? 'selected' : ''}>\u2139\uFE0F Info \u2014 General information (default)</option>
11060
+ <option value="warn" \${s.logLevel === 'warn' ? 'selected' : ''}>\u26A0\uFE0F Warn \u2014 Warnings and important events</option>
11061
+ <option value="error" \${s.logLevel === 'error' ? 'selected' : ''}>\u274C Error \u2014 Errors only</option>
11062
+ <option value="none" \${s.logLevel === 'none' ? 'selected' : ''}>\u{1F507} None \u2014 Disable all logging</option>
11063
+ </select>
11064
+ <div class="hint">Controls the verbosity of plugin log output.</div>
11065
+ </div>
11066
+ <div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
11067
+ \${sectionSaveBtn('logging', 'log')}
11068
+ </div>
11069
+ </div>
11070
+
11071
+ <div class="card">
11072
+ <div class="card-header"><div class="card-title">\u{1F4E6} Import / Export Settings</div></div>
11073
+ <div style="display:flex;gap:10px;flex-wrap:wrap">
11074
+ <button class="btn btn-default btn-sm" onclick="exportSettings()">\u{1F4E5} Export Settings</button>
11075
+ <button class="btn btn-ok btn-sm" onclick="showImportSettingsModal()">\u{1F4E4} Import Settings</button>
11076
+ </div>
11077
+ <div class="hint" style="margin-top:10px">Export current AICQ plugin settings as JSON. Import to restore settings from a backup.</div>
11078
+ </div>
11079
+ \\\`);
11080
+ }
11081
+
11082
+ // \u2500\u2500 Section Save (AJAX) \u2500\u2500
11083
+ async function saveSettingsSection(section, id) {
11084
+ const btn = $('#btn-save-' + id);
11085
+ const statusEl = $('#status-' + id);
11086
+ if (btn) { btn.disabled = true; btn.textContent = 'Saving...'; }
11087
+ if (statusEl) { statusEl.textContent = ''; statusEl.style.color = 'var(--text3)'; }
11088
+
11089
+ let data = {};
11090
+ switch (section) {
11091
+ case 'connection':
11092
+ data = {
11093
+ serverUrl: $('#set-server-url')?.value?.trim(),
11094
+ connectionTimeout: parseInt($('#set-connection-timeout')?.value, 10),
11095
+ wsAutoReconnect: $('#set-ws-auto-reconnect')?.checked ?? true,
11096
+ wsReconnectInterval: parseInt($('#set-ws-reconnect-interval')?.value, 10),
11097
+ };
11098
+ break;
11099
+ case 'friends':
11100
+ data = {
11101
+ maxFriends: parseInt($('#set-max-friends')?.value, 10),
11102
+ autoAcceptFriends: $('#set-auto-accept')?.checked ?? false,
11103
+ defaultPermissions: [
11104
+ ...(($('#set-perm-chat')?.checked) ? ['chat'] : []),
11105
+ ...(($('#set-perm-exec')?.checked) ? ['exec'] : []),
11106
+ ],
11107
+ };
11108
+ break;
11109
+ case 'temp':
11110
+ data = { tempNumberExpiry: parseInt($('#set-temp-expiry')?.value, 10) };
11111
+ break;
11112
+ case 'security':
11113
+ data = {
11114
+ enableP2P: $('#set-enable-p2p')?.checked ?? true,
11115
+ handshakeTimeout: parseInt($('#set-handshake-timeout')?.value, 10),
11116
+ };
11117
+ break;
11118
+ case 'filetransfer':
11119
+ data = {
11120
+ enableFileTransfer: $('#set-enable-ft')?.checked ?? true,
11121
+ maxFileSize: parseInt($('#set-max-file-size')?.value, 10),
11122
+ };
11123
+ break;
11124
+ case 'logging':
11125
+ data = { logLevel: $('#set-log-level')?.value || 'info' };
11126
+ break;
10716
11127
  }
10717
11128
 
10718
- // Config file section
10719
- if (configRes.error) {
10720
- out += errorBlock('Config File Not Found', configRes.error);
11129
+ const r = await api('/settings/section', {
11130
+ method: 'POST',
11131
+ body: JSON.stringify({ section, data }),
11132
+ });
11133
+
11134
+ if (btn) { btn.disabled = false; btn.textContent = '\u{1F4BE} Save'; }
11135
+
11136
+ if (r.success) {
11137
+ toast('Settings saved: ' + section, 'ok');
11138
+ if (statusEl) { statusEl.textContent = '\u2713 Saved'; statusEl.style.color = 'var(--ok)'; }
11139
+ // Refresh settings data
11140
+ const fresh = await api('/settings');
11141
+ if (fresh && !fresh.error) { _settingsData = fresh; }
10721
11142
  } else {
10722
- const c = configRes.data;
10723
- out += \\\`
10724
- <div class="card">
10725
- <div class="card-header"><div class="card-title">\u{1F4C1} Config File</div></div>
10726
- <div class="detail-row"><div class="detail-key">Source</div><div class="detail-val" style="cursor:pointer" onclick="copyText('\${escHtml(c.configPath || '')}')">\${escHtml(c.configPath || 'Not found')} \u{1F4CB}</div></div>
10727
- <div class="detail-row"><div class="detail-key">Config Size</div><div class="detail-val">\${c.configSize || 0} bytes</div></div>
10728
- </div>
10729
- \\\`;
11143
+ toast(r.message || r.error || 'Save failed', 'err');
11144
+ if (statusEl) { statusEl.textContent = '\u2717 ' + (r.message || 'Failed'); statusEl.style.color = 'var(--danger)'; }
10730
11145
  }
11146
+ }
10731
11147
 
10732
- // Statistics section
10733
- if (!statusRes.error) {
10734
- const s = statusRes.data;
10735
- out += \\\`
10736
- <div class="card">
10737
- <div class="card-header"><div class="card-title">\u{1F4CA} Statistics</div></div>
10738
- <div class="detail-row"><div class="detail-key">Friends Count</div><div class="detail-val">\${s.friendCount || 0}</div></div>
10739
- <div class="detail-row"><div class="detail-key">Active Sessions</div><div class="detail-val">\${s.sessionCount || 0}</div></div>
10740
- <div class="detail-row"><div class="detail-key">Plugin Version</div><div class="detail-val">1.0.5</div></div>
11148
+ // \u2500\u2500 Full Save All (legacy support) \u2500\u2500
11149
+ async function saveSettings() {
11150
+ if (_settingsSaving) return;
11151
+ _settingsSaving = true;
11152
+
11153
+ const allData = {
11154
+ serverUrl: $('#set-server-url')?.value?.trim(),
11155
+ maxFriends: parseInt($('#set-max-friends')?.value, 10),
11156
+ autoAcceptFriends: $('#set-auto-accept')?.checked ?? false,
11157
+ };
11158
+
11159
+ const r = await api('/settings', { method: 'PUT', body: JSON.stringify(allData) });
11160
+ _settingsSaving = false;
11161
+
11162
+ if (r.success) { toast('All settings saved!', 'ok'); setTimeout(() => loadSettings(), 800); }
11163
+ else { toast(r.message || r.error || 'Save failed', 'err'); }
11164
+ }
11165
+
11166
+ // \u2500\u2500 Reset Identity \u2500\u2500
11167
+ function showResetIdentityModal() {
11168
+ $('#reset-confirm-input').value = '';
11169
+ $('#reset-confirm-btn').disabled = true;
11170
+ $('#reset-confirm-btn').textContent = '\u{1F5D1}\uFE0F Delete Everything';
11171
+ showModal('modal-reset-identity');
11172
+ setTimeout(() => $('#reset-confirm-input')?.focus(), 100);
11173
+ }
11174
+
11175
+ function checkResetConfirm() {
11176
+ const v = $('#reset-confirm-input')?.value?.trim();
11177
+ const btn = $('#reset-confirm-btn');
11178
+ if (btn) { btn.disabled = (v !== 'RESET'); btn.textContent = v === 'RESET' ? '\u{1F5D1}\uFE0F Confirm Delete' : '\u{1F5D1}\uFE0F Delete Everything'; }
11179
+ }
11180
+
11181
+ async function executeResetIdentity() {
11182
+ const btn = $('#reset-confirm-btn');
11183
+ if (btn) { btn.disabled = true; btn.textContent = 'Resetting...'; }
11184
+
11185
+ const r = await api('/settings/reset-identity', {
11186
+ method: 'POST',
11187
+ body: JSON.stringify({ confirm: true }),
11188
+ });
11189
+
11190
+ if (btn) { btn.disabled = false; btn.textContent = '\u{1F5D1}\uFE0F Delete Everything'; }
11191
+
11192
+ if (r.success) {
11193
+ toast('Identity reset successfully. Please restart the plugin.', 'ok');
11194
+ hideModal('modal-reset-identity');
11195
+ // Reload settings to reflect cleared state
11196
+ setTimeout(() => loadSettings(), 1000);
11197
+ } else {
11198
+ toast(r.message || r.error || 'Reset failed', 'err');
11199
+ }
11200
+ }
11201
+
11202
+ // \u2500\u2500 Export / Import \u2500\u2500
11203
+ async function exportSettings() {
11204
+ const r = await api('/settings/export');
11205
+ if (r.error) { toast(r.error, 'err'); return; }
11206
+
11207
+ const json = JSON.stringify(r.settings || r, null, 2);
11208
+ const blob = new Blob([json], { type: 'application/json' });
11209
+ const url = URL.createObjectURL(blob);
11210
+ const a = document.createElement('a');
11211
+ a.href = url;
11212
+ a.download = 'aicq-settings-' + new Date().toISOString().slice(0, 10) + '.json';
11213
+ a.click();
11214
+ URL.revokeObjectURL(url);
11215
+ toast('Settings exported successfully', 'ok');
11216
+ }
11217
+
11218
+ function showImportSettingsModal() {
11219
+ $('#import-json-input').value = '';
11220
+ showModal('modal-import-settings');
11221
+ setTimeout(() => $('#import-json-input')?.focus(), 100);
11222
+ }
11223
+
11224
+ async function executeImportSettings() {
11225
+ const raw = $('#import-json-input')?.value?.trim();
11226
+ if (!raw) { toast('Paste JSON settings first', 'warn'); return; }
11227
+
11228
+ let settings;
11229
+ try { settings = JSON.parse(raw); } catch (e) { toast('Invalid JSON: ' + e.message, 'err'); return; }
11230
+
11231
+ const btn = $('#import-confirm-btn');
11232
+ if (btn) { btn.disabled = true; btn.textContent = 'Importing...'; }
11233
+
11234
+ const r = await api('/settings/import', {
11235
+ method: 'POST',
11236
+ body: JSON.stringify({ settings, merge: true }),
11237
+ });
11238
+
11239
+ if (btn) { btn.disabled = false; btn.textContent = '\u{1F4E4} Import'; }
11240
+
11241
+ if (r.success) {
11242
+ toast('Settings imported successfully!', 'ok');
11243
+ hideModal('modal-import-settings');
11244
+ setTimeout(() => loadSettings(), 800);
11245
+ } else {
11246
+ toast(r.message || r.error || 'Import failed', 'err');
11247
+ }
11248
+ }
11249
+
11250
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
11251
+ // JSON Config Editor
11252
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
11253
+ let _jsonEditorConfigFile = '';
11254
+
11255
+ async function renderSettingsJsonEditor() {
11256
+ const el = $('#settings-content');
11257
+ html(el, '<div class="loading-mask"><div class="spinner"></div>Loading config...</div>');
11258
+
11259
+ const queryParams = _jsonEditorConfigFile ? '?file=' + encodeURIComponent(_jsonEditorConfigFile) : '';
11260
+ const data = await api('/config-file/raw' + queryParams);
11261
+ if (data.error) {
11262
+ html(el, '<div class="empty"><div class="icon">\u26A0\uFE0F</div><p>' + escHtml(data.error) + '</p></div>');
11263
+ return;
11264
+ }
11265
+
11266
+ _jsonEditorConfigFile = data.fileName || '';
11267
+ const hasMultipleFiles = data.availableFiles && data.availableFiles.length > 1;
11268
+ let fileSelectorHtml = '';
11269
+ if (hasMultipleFiles) {
11270
+ const options = data.availableFiles.map(f =>
11271
+ '<option value="' + escHtml(f) + '"' + (f === data.fileName ? ' selected' : '') + '>' + escHtml(f) + '</option>'
11272
+ ).join('');
11273
+ fileSelectorHtml = \\\`
11274
+ <div class="form-group" style="margin-bottom:16px">
11275
+ <label>\u{1F4C4} Config File</label>
11276
+ <select id="json-editor-file-select" onchange="_jsonEditorConfigFile=this.value;renderSettingsJsonEditor()" style="max-width:300px">
11277
+ \${options}
11278
+ </select>
11279
+ </div>\\\`;
11280
+ }
11281
+
11282
+ html(el, \\\` <p class="section-desc">
11283
+ Edit the raw JSON configuration directly. Be careful with syntax \u2014 invalid JSON will be rejected.
11284
+ <span class="badge badge-accent" style="margin-left:8px">\u{1F4C4} \${escHtml(data.fileName)}</span>
11285
+ </p>
11286
+
11287
+ <div class="card">
11288
+ <div class="card-header">
11289
+ <div class="card-title">\u{1F4DD} Config JSON Editor</div>
11290
+ <div style="display:flex;gap:8px;align-items:center">
11291
+ <span class="mono" style="font-size:11px;color:var(--text3)">\${escHtml(data.filePath)}</span>
11292
+ <button class="btn btn-sm btn-default" onclick="renderSettingsJsonEditor()">\u{1F504} Reload</button>
11293
+ </div>
10741
11294
  </div>
10742
- \\\`;
11295
+ \${fileSelectorHtml}
11296
+ <div class="form-group">
11297
+ <label>Raw JSON Configuration</label>
11298
+ <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>
11299
+ <div class="hint">Directly edit the configuration JSON. Use the Format button to prettify.</div>
11300
+ </div>
11301
+ <div id="json-editor-status" style="margin-bottom:12px;font-size:12px"></div>
11302
+ <div class="form-actions" style="justify-content:space-between">
11303
+ <div style="display:flex;gap:8px">
11304
+ <button class="btn btn-sm btn-default" onclick="formatJsonEditor()">\u{1F4D0} Format</button>
11305
+ <button class="btn btn-sm btn-default" onclick="copyText($('#json-editor')?.value || '')">\u{1F4CB} Copy</button>
11306
+ </div>
11307
+ <div style="display:flex;gap:8px">
11308
+ <button class="btn btn-sm btn-default" onclick="renderSettingsJsonEditor()">\u21A9\uFE0F Revert</button>
11309
+ <button class="btn btn-sm btn-primary" id="btn-save-json" onclick="saveJsonConfig()">\u{1F4BE} Save Config</button>
11310
+ </div>
11311
+ </div>
11312
+ </div>
11313
+ \`);
11314
+ }
11315
+
11316
+ function formatJsonEditor() {
11317
+ const ta = $('#json-editor');
11318
+ if (!ta) return;
11319
+ try {
11320
+ const obj = JSON.parse(ta.value);
11321
+ ta.value = JSON.stringify(obj, null, 2);
11322
+ toast('JSON formatted', 'ok');
11323
+ $('#json-editor-status').innerHTML = '<span style="color:var(--ok)">\u2713 Valid JSON</span>';
11324
+ } catch (e) {
11325
+ toast('Invalid JSON: ' + e.message, 'err');
11326
+ $('#json-editor-status').innerHTML = '<span style="color:var(--danger)">\u2717 ' + escHtml(e.message) + '</span>';
10743
11327
  }
11328
+ }
11329
+
11330
+ async function saveJsonConfig() {
11331
+ const btn = $('#btn-save-json');
11332
+ const statusEl = $('#json-editor-status');
11333
+ const raw = $('#json-editor')?.value;
11334
+ if (!raw) { toast('No content to save', 'warn'); return; }
10744
11335
 
10745
- html(el, out);
11336
+ // Validate first
11337
+ try { JSON.parse(raw); } catch (e) {
11338
+ toast('Invalid JSON: ' + e.message, 'err');
11339
+ if (statusEl) statusEl.innerHTML = '<span style="color:var(--danger)">\u2717 ' + escHtml(e.message) + '</span>';
11340
+ return;
11341
+ }
11342
+
11343
+ if (btn) { btn.disabled = true; btn.textContent = 'Saving...'; }
11344
+ 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>';
11345
+
11346
+ const queryParams = _jsonEditorConfigFile ? '?file=' + encodeURIComponent(_jsonEditorConfigFile) : '';
11347
+ const r = await api('/config-file/raw' + queryParams, { method: 'PUT', body: JSON.stringify({ content: raw }) });
11348
+
11349
+ if (btn) { btn.disabled = false; btn.textContent = '\u{1F4BE} Save Config'; }
11350
+
11351
+ if (r.success) {
11352
+ toast('Config saved successfully!', 'ok');
11353
+ if (statusEl) statusEl.innerHTML = '<span style="color:var(--ok)">\u2713 Saved at ' + new Date().toLocaleTimeString() + '</span>';
11354
+ } else {
11355
+ toast(r.message || 'Save failed', 'err');
11356
+ if (statusEl) statusEl.innerHTML = '<span style="color:var(--danger)">\u2717 ' + escHtml(r.message || 'Failed') + '</span>';
11357
+ }
10746
11358
  }
10747
11359
 
10748
11360
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
@@ -10838,7 +11450,10 @@ var HTML = `<!DOCTYPE html>
10838
11450
  <div class="page" id="page-models"><div id="models-content"></div></div>
10839
11451
 
10840
11452
  <!-- Settings -->
10841
- <div class="page" id="page-settings"><div id="settings-content"></div></div>
11453
+ <div class="page" id="page-settings">
11454
+ <div id="settings-tabs" style="display:flex;gap:6px;margin-bottom:16px;flex-wrap:wrap"></div>
11455
+ <div id="settings-content"></div>
11456
+ </div>
10842
11457
 
10843
11458
  </div>
10844
11459
  </main>
@@ -10914,9 +11529,118 @@ var HTML = `<!DOCTYPE html>
10914
11529
  </div>
10915
11530
  </div>
10916
11531
 
11532
+ <!-- Modal: Reset Identity -->
11533
+ <div class="modal-overlay hidden" id="modal-reset-identity" onclick="if(event.target===this)hideModal('modal-reset-identity')">
11534
+ <div class="modal">
11535
+ <div class="modal-header"><h3>\u{1F5D1}\uFE0F Reset Agent Identity</h3><button class="modal-close" onclick="hideModal('modal-reset-identity')">\u2715</button></div>
11536
+ <div style="margin-bottom:16px">
11537
+ <div class="card" style="border-color:var(--danger);background:var(--danger-bg)">
11538
+ <p style="font-size:13px;color:#fca5a5;line-height:1.6">
11539
+ <strong>\u26A0\uFE0F WARNING: This is a destructive operation!</strong><br><br>
11540
+ This will permanently delete:<br>
11541
+ \u2022 Your Ed25519 key pair and agent ID<br>
11542
+ \u2022 All friend connections and sessions<br>
11543
+ \u2022 All pending friend requests<br>
11544
+ \u2022 All temporary numbers<br><br>
11545
+ After reset, you must restart the plugin to generate a new identity.
11546
+ </p>
11547
+ </div>
11548
+ </div>
11549
+ <div class="form-group">
11550
+ <label>Type RESET to confirm</label>
11551
+ <input id="reset-confirm-input" type="text" placeholder="RESET" oninput="checkResetConfirm()" autocomplete="off" style="border-color:var(--danger)">
11552
+ </div>
11553
+ <div class="form-actions">
11554
+ <button class="btn btn-default" onclick="hideModal('modal-reset-identity')">Cancel</button>
11555
+ <button class="btn btn-danger" id="reset-confirm-btn" onclick="executeResetIdentity()" disabled>\u{1F5D1}\uFE0F Delete Everything</button>
11556
+ </div>
11557
+ </div>
11558
+ </div>
11559
+
11560
+ <!-- Modal: Import Settings -->
11561
+ <div class="modal-overlay hidden" id="modal-import-settings" onclick="if(event.target===this)hideModal('modal-import-settings')">
11562
+ <div class="modal" style="max-width:580px">
11563
+ <div class="modal-header"><h3>\u{1F4E4} Import Settings</h3><button class="modal-close" onclick="hideModal('modal-import-settings')">\u2715</button></div>
11564
+ <div class="form-group">
11565
+ <label>Paste JSON Settings</label>
11566
+ <textarea id="import-json-input" rows="10" placeholder='{"serverUrl": "https://...", "maxFriends": 200, ...}' style="font-family:'SF Mono','Fira Code','Cascadia Code',monospace;font-size:12px;line-height:1.5"></textarea>
11567
+ <div class="hint">Paste the JSON settings exported from another AICQ instance. Settings will be merged with existing values.</div>
11568
+ </div>
11569
+ <div class="form-actions">
11570
+ <button class="btn btn-default" onclick="hideModal('modal-import-settings')">Cancel</button>
11571
+ <button class="btn btn-primary" id="import-confirm-btn" onclick="executeImportSettings()">\u{1F4E4} Import</button>
11572
+ </div>
11573
+ </div>
11574
+ </div>
11575
+
10917
11576
  <!-- Toast Container -->
10918
11577
  <div id="toast-container" class="toast-container"></div>
10919
11578
 
11579
+ <!-- Modal: Add/Edit Agent -->
11580
+ <div class="modal-overlay hidden" id="modal-add-agent" onclick="if(event.target===this)hideModal('modal-add-agent')">
11581
+ <div class="modal">
11582
+ <div class="modal-header"><h3 id="agent-form-title">\u2795 Add Agent</h3><button class="modal-close" onclick="hideModal('modal-add-agent')">\u2715</button></div>
11583
+ <div class="form-group">
11584
+ <label>Agent Name *</label>
11585
+ <input type="text" id="agent-form-name" placeholder="e.g. My Assistant">
11586
+ </div>
11587
+ <div class="form-group">
11588
+ <label>Agent ID</label>
11589
+ <input type="text" id="agent-form-id" placeholder="auto-generated if empty">
11590
+ <div class="hint">Unique identifier. Leave empty for auto-generation.</div>
11591
+ </div>
11592
+ <div class="form-row">
11593
+ <div class="form-group">
11594
+ <label>Model</label>
11595
+ <input type="text" id="agent-form-model" placeholder="gpt-4o">
11596
+ </div>
11597
+ <div class="form-group">
11598
+ <label>Provider</label>
11599
+ <input type="text" id="agent-form-provider" placeholder="openai">
11600
+ </div>
11601
+ </div>
11602
+ <div class="form-group">
11603
+ <label>System Prompt</label>
11604
+ <textarea id="agent-form-prompt" rows="4" placeholder="You are a helpful assistant..."></textarea>
11605
+ </div>
11606
+ <div class="form-row">
11607
+ <div class="form-group">
11608
+ <label>Temperature</label>
11609
+ <input type="number" id="agent-form-temperature" min="0" max="2" step="0.1" value="0.7">
11610
+ <div class="hint">0 = deterministic, 2 = creative. Default: 0.7</div>
11611
+ </div>
11612
+ <div class="form-group">
11613
+ <label>Max Tokens</label>
11614
+ <input type="number" id="agent-form-max-tokens" min="1" step="1" value="4096">
11615
+ <div class="hint">Maximum response length. Default: 4096</div>
11616
+ </div>
11617
+ </div>
11618
+ <div class="form-row">
11619
+ <div class="form-group">
11620
+ <label>Top P</label>
11621
+ <input type="number" id="agent-form-top-p" min="0" max="1" step="0.05" value="1">
11622
+ <div class="hint">Nucleus sampling. Default: 1</div>
11623
+ </div>
11624
+ <div class="form-group">
11625
+ <label>Tools</label>
11626
+ <input type="text" id="agent-form-tools" placeholder="web_search, code_exec, ...">
11627
+ <div class="hint">Comma-separated list of tool names</div>
11628
+ </div>
11629
+ </div>
11630
+ <div class="form-group">
11631
+ <label class="toggle-label">
11632
+ <input type="checkbox" id="agent-form-enabled" checked>
11633
+ <span class="toggle-slider"></span>
11634
+ <span>Enabled</span>
11635
+ </label>
11636
+ </div>
11637
+ <div class="form-actions">
11638
+ <button class="btn btn-default" onclick="hideModal('modal-add-agent')">Cancel</button>
11639
+ <button class="btn btn-primary" onclick="saveAgent()">\u{1F4BE} Save Agent</button>
11640
+ </div>
11641
+ </div>
11642
+ </div>
11643
+
10920
11644
  <script>${JS}</script>
10921
11645
  </body>
10922
11646
  </html>`;
@@ -11096,7 +11820,7 @@ async function readBody(req) {
11096
11820
  });
11097
11821
  }
11098
11822
  function createManagementHandler(ctx) {
11099
- const { store, identityService, serverClient, serverUrl, aicqAgentId, logger, html } = ctx;
11823
+ const { store, identityService, serverClient, serverUrl, aicqAgentId, logger, html, chatChannel } = ctx;
11100
11824
  return async (req, res) => {
11101
11825
  const urlPath = parseApiPath(req.url || "/");
11102
11826
  const method = (req.method || "GET").toUpperCase();
@@ -11412,6 +12136,462 @@ function createManagementHandler(ctx) {
11412
12136
  logger.info("[API] Model config saved for provider: " + providerId);
11413
12137
  return json(res, { success: true, message: "Model configuration saved for " + provider.name });
11414
12138
  }
12139
+ if (apiPath === "/settings" && method === "GET") {
12140
+ const result = readConfig();
12141
+ const aicqSection = result?.config?.aicq ?? {};
12142
+ const pluginsSection = result?.config?.plugins;
12143
+ const pluginSection = pluginsSection?.["aicq-chat"];
12144
+ const merged = { ...aicqSection, ...pluginSection };
12145
+ return json(res, {
12146
+ // Connection settings
12147
+ serverUrl: merged.serverUrl || serverUrl,
12148
+ wsReconnectInterval: merged.wsReconnectInterval || 60,
12149
+ wsAutoReconnect: merged.wsAutoReconnect !== false,
12150
+ connectionTimeout: merged.connectionTimeout || 30,
12151
+ // Friend settings
12152
+ maxFriends: merged.maxFriends || 200,
12153
+ autoAcceptFriends: Boolean(merged.autoAcceptFriends),
12154
+ defaultPermissions: merged.defaultPermissions || ["chat"],
12155
+ // Temp number settings
12156
+ tempNumberExpiry: merged.tempNumberExpiry || 300,
12157
+ // File transfer settings
12158
+ maxFileSize: merged.maxFileSize || 104857600,
12159
+ enableFileTransfer: merged.enableFileTransfer !== false,
12160
+ allowedFileTypes: merged.allowedFileTypes || null,
12161
+ // Logging
12162
+ logLevel: merged.logLevel || "info",
12163
+ // Security / encryption
12164
+ enableP2P: merged.enableP2P !== false,
12165
+ handshakeTimeout: merged.handshakeTimeout || 60,
12166
+ // Identity (read-only)
12167
+ agentId: aicqAgentId,
12168
+ publicKeyFingerprint: identityService.getPublicKeyFingerprint(),
12169
+ connected: serverClient.isConnected(),
12170
+ // Config file info
12171
+ configSource: result ? path5.basename(result.configPath) : "none",
12172
+ configPath: result?.configPath || null,
12173
+ // Runtime info
12174
+ friendCount: store.getFriendCount(),
12175
+ sessionCount: store.sessions.size,
12176
+ uptimeSeconds: Math.floor(process.uptime())
12177
+ });
12178
+ }
12179
+ if (apiPath === "/settings" && method === "PUT") {
12180
+ const body = await readBody(req);
12181
+ const newServerUrl = body.serverUrl;
12182
+ const newMaxFriends = body.maxFriends;
12183
+ const newAutoAccept = body.autoAcceptFriends;
12184
+ const newWsReconnectInterval = body.wsReconnectInterval;
12185
+ const newWsAutoReconnect = body.wsAutoReconnect;
12186
+ const newConnectionTimeout = body.connectionTimeout;
12187
+ const newTempNumberExpiry = body.tempNumberExpiry;
12188
+ const newMaxFileSize = body.maxFileSize;
12189
+ const newEnableFileTransfer = body.enableFileTransfer;
12190
+ const newAllowedFileTypes = body.allowedFileTypes;
12191
+ const newLogLevel = body.logLevel;
12192
+ const newEnableP2P = body.enableP2P;
12193
+ const newHandshakeTimeout = body.handshakeTimeout;
12194
+ const newDefaultPermissions = body.defaultPermissions;
12195
+ if (newServerUrl !== void 0 && typeof newServerUrl !== "string") {
12196
+ return json(res, { success: false, message: "serverUrl must be a string" }, 400);
12197
+ }
12198
+ if (newMaxFriends !== void 0 && (typeof newMaxFriends !== "number" || newMaxFriends < 1 || newMaxFriends > 1e4)) {
12199
+ return json(res, { success: false, message: "maxFriends must be a number between 1 and 10000" }, 400);
12200
+ }
12201
+ if (newAutoAccept !== void 0 && typeof newAutoAccept !== "boolean") {
12202
+ return json(res, { success: false, message: "autoAcceptFriends must be a boolean" }, 400);
12203
+ }
12204
+ if (newWsReconnectInterval !== void 0 && (typeof newWsReconnectInterval !== "number" || newWsReconnectInterval < 5 || newWsReconnectInterval > 600)) {
12205
+ return json(res, { success: false, message: "wsReconnectInterval must be between 5 and 600 seconds" }, 400);
12206
+ }
12207
+ if (newConnectionTimeout !== void 0 && (typeof newConnectionTimeout !== "number" || newConnectionTimeout < 5 || newConnectionTimeout > 120)) {
12208
+ return json(res, { success: false, message: "connectionTimeout must be between 5 and 120 seconds" }, 400);
12209
+ }
12210
+ if (newTempNumberExpiry !== void 0 && (typeof newTempNumberExpiry !== "number" || newTempNumberExpiry < 60 || newTempNumberExpiry > 3600)) {
12211
+ return json(res, { success: false, message: "tempNumberExpiry must be between 60 and 3600 seconds" }, 400);
12212
+ }
12213
+ if (newMaxFileSize !== void 0 && (typeof newMaxFileSize !== "number" || newMaxFileSize < 1024 || newMaxFileSize > 1073741824)) {
12214
+ return json(res, { success: false, message: "maxFileSize must be between 1KB and 1GB" }, 400);
12215
+ }
12216
+ if (newLogLevel !== void 0 && !["debug", "info", "warn", "error", "none"].includes(newLogLevel)) {
12217
+ return json(res, { success: false, message: "logLevel must be one of: debug, info, warn, error, none" }, 400);
12218
+ }
12219
+ if (newHandshakeTimeout !== void 0 && (typeof newHandshakeTimeout !== "number" || newHandshakeTimeout < 10 || newHandshakeTimeout > 300)) {
12220
+ return json(res, { success: false, message: "handshakeTimeout must be between 10 and 300 seconds" }, 400);
12221
+ }
12222
+ const result = readConfig();
12223
+ if (!result) {
12224
+ return json(res, { success: false, message: "No config file found. Create openclaw.json first." }, 400);
12225
+ }
12226
+ const config2 = result.config;
12227
+ if (!config2.plugins || typeof config2.plugins !== "object") {
12228
+ config2.plugins = {};
12229
+ }
12230
+ const plugins = config2.plugins;
12231
+ if (!plugins["aicq-chat"] || typeof plugins["aicq-chat"] !== "object") {
12232
+ plugins["aicq-chat"] = {};
12233
+ }
12234
+ const aicqConfig = plugins["aicq-chat"];
12235
+ if (newServerUrl !== void 0)
12236
+ aicqConfig.serverUrl = newServerUrl;
12237
+ if (newMaxFriends !== void 0)
12238
+ aicqConfig.maxFriends = newMaxFriends;
12239
+ if (newAutoAccept !== void 0)
12240
+ aicqConfig.autoAcceptFriends = newAutoAccept;
12241
+ if (newWsReconnectInterval !== void 0)
12242
+ aicqConfig.wsReconnectInterval = newWsReconnectInterval;
12243
+ if (newWsAutoReconnect !== void 0)
12244
+ aicqConfig.wsAutoReconnect = newWsAutoReconnect;
12245
+ if (newConnectionTimeout !== void 0)
12246
+ aicqConfig.connectionTimeout = newConnectionTimeout;
12247
+ if (newTempNumberExpiry !== void 0)
12248
+ aicqConfig.tempNumberExpiry = newTempNumberExpiry;
12249
+ if (newMaxFileSize !== void 0)
12250
+ aicqConfig.maxFileSize = newMaxFileSize;
12251
+ if (newEnableFileTransfer !== void 0)
12252
+ aicqConfig.enableFileTransfer = newEnableFileTransfer;
12253
+ if (newAllowedFileTypes !== void 0)
12254
+ aicqConfig.allowedFileTypes = newAllowedFileTypes;
12255
+ if (newLogLevel !== void 0)
12256
+ aicqConfig.logLevel = newLogLevel;
12257
+ if (newEnableP2P !== void 0)
12258
+ aicqConfig.enableP2P = newEnableP2P;
12259
+ if (newHandshakeTimeout !== void 0)
12260
+ aicqConfig.handshakeTimeout = newHandshakeTimeout;
12261
+ if (newDefaultPermissions !== void 0)
12262
+ aicqConfig.defaultPermissions = newDefaultPermissions;
12263
+ const written = writeConfig(config2);
12264
+ if (!written) {
12265
+ return json(res, { success: false, message: "Failed to write config file" }, 500);
12266
+ }
12267
+ logger.info("[API] Settings saved: " + JSON.stringify(body));
12268
+ return json(res, { success: true, message: "Settings saved successfully" });
12269
+ }
12270
+ if (apiPath === "/settings/test-connection" && method === "POST") {
12271
+ const body = await readBody(req);
12272
+ const testUrl = body.serverUrl || serverUrl;
12273
+ const startTime = Date.now();
12274
+ try {
12275
+ const controller = new AbortController();
12276
+ const timeout = setTimeout(() => controller.abort(), body.timeout || 1e4);
12277
+ const resp = await fetch(testUrl + "/api/v1/health", {
12278
+ method: "GET",
12279
+ signal: controller.signal,
12280
+ headers: { "Content-Type": "application/json" }
12281
+ });
12282
+ clearTimeout(timeout);
12283
+ const latency = Date.now() - startTime;
12284
+ let serverInfo = {};
12285
+ try {
12286
+ serverInfo = await resp.json();
12287
+ } catch {
12288
+ }
12289
+ if (resp.ok) {
12290
+ return json(res, {
12291
+ success: true,
12292
+ status: "ok",
12293
+ statusCode: resp.status,
12294
+ latency,
12295
+ serverUrl: testUrl,
12296
+ serverInfo
12297
+ });
12298
+ } else {
12299
+ return json(res, {
12300
+ success: false,
12301
+ status: "error",
12302
+ statusCode: resp.status,
12303
+ latency,
12304
+ serverUrl: testUrl,
12305
+ message: "Server returned HTTP " + resp.status
12306
+ });
12307
+ }
12308
+ } catch (err) {
12309
+ const latency = Date.now() - startTime;
12310
+ const msg = err instanceof Error ? err.message : String(err);
12311
+ const isTimeout = msg.includes("abort") || msg.includes("timeout");
12312
+ return json(res, {
12313
+ success: false,
12314
+ status: isTimeout ? "timeout" : "unreachable",
12315
+ latency,
12316
+ serverUrl: testUrl,
12317
+ message: isTimeout ? "Connection timed out after " + latency + "ms" : "Cannot reach server: " + msg
12318
+ });
12319
+ }
12320
+ }
12321
+ if (apiPath === "/settings/reset-identity" && method === "POST") {
12322
+ const body = await readBody(req);
12323
+ const confirmReset = body.confirm;
12324
+ if (!confirmReset) {
12325
+ return json(res, { success: false, message: "Confirmation required. Set { confirm: true } to proceed." }, 400);
12326
+ }
12327
+ try {
12328
+ identityService.cleanup();
12329
+ chatChannel?.cleanup?.();
12330
+ serverClient.disconnectWebSocket();
12331
+ store.friends.clear();
12332
+ store.sessions.clear();
12333
+ store.pendingHandshakes.clear();
12334
+ store.pendingRequests = [];
12335
+ store.tempNumbers = [];
12336
+ store.save();
12337
+ logger.warn("[API] Agent identity reset by user via settings UI");
12338
+ return json(res, {
12339
+ success: true,
12340
+ message: "Identity reset successfully. All friends, sessions, and keys have been deleted. Restart the plugin to generate a new identity."
12341
+ });
12342
+ } catch (err) {
12343
+ const msg = err instanceof Error ? err.message : String(err);
12344
+ logger.error("[API] Identity reset failed: " + msg);
12345
+ return json(res, { success: false, message: "Failed to reset identity: " + msg }, 500);
12346
+ }
12347
+ }
12348
+ if (apiPath === "/settings/export" && method === "GET") {
12349
+ const result = readConfig();
12350
+ if (!result)
12351
+ return json(res, { error: "No config file found" }, 400);
12352
+ const pluginsSection = result.config.plugins;
12353
+ const pluginSection = pluginsSection?.["aicq-chat"];
12354
+ return json(res, {
12355
+ exportDate: (/* @__PURE__ */ new Date()).toISOString(),
12356
+ pluginVersion: "1.0.4",
12357
+ settings: pluginSection || {},
12358
+ fullConfig: result.config
12359
+ });
12360
+ }
12361
+ if (apiPath === "/settings/import" && method === "POST") {
12362
+ const body = await readBody(req);
12363
+ const settings = body.settings;
12364
+ const merge = body.merge;
12365
+ if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
12366
+ return json(res, { success: false, message: "Invalid settings object. Provide { settings: {...} }" }, 400);
12367
+ }
12368
+ const result = readConfig();
12369
+ if (!result) {
12370
+ return json(res, { success: false, message: "No config file found" }, 400);
12371
+ }
12372
+ const config2 = result.config;
12373
+ if (!config2.plugins || typeof config2.plugins !== "object") {
12374
+ config2.plugins = {};
12375
+ }
12376
+ const plugins = config2.plugins;
12377
+ if (!plugins["aicq-chat"] || typeof plugins["aicq-chat"] !== "object" || !merge) {
12378
+ plugins["aicq-chat"] = {};
12379
+ }
12380
+ const aicqConfig = plugins["aicq-chat"];
12381
+ Object.assign(aicqConfig, settings);
12382
+ const written = writeConfig(config2);
12383
+ if (!written) {
12384
+ return json(res, { success: false, message: "Failed to write config" }, 500);
12385
+ }
12386
+ logger.info("[API] Settings imported: " + Object.keys(settings).join(", "));
12387
+ return json(res, { success: true, message: "Settings imported successfully" });
12388
+ }
12389
+ if (apiPath === "/settings/section" && method === "POST") {
12390
+ const body = await readBody(req);
12391
+ const section = body.section;
12392
+ const data = body.data;
12393
+ if (!section || !data) {
12394
+ return json(res, { success: false, message: "Missing section or data" }, 400);
12395
+ }
12396
+ const result = readConfig();
12397
+ if (!result) {
12398
+ return json(res, { success: false, message: "No config file found" }, 400);
12399
+ }
12400
+ const config2 = result.config;
12401
+ if (!config2.plugins || typeof config2.plugins !== "object") {
12402
+ config2.plugins = {};
12403
+ }
12404
+ const plugins = config2.plugins;
12405
+ if (!plugins["aicq-chat"] || typeof plugins["aicq-chat"] !== "object") {
12406
+ plugins["aicq-chat"] = {};
12407
+ }
12408
+ const aicqConfig = plugins["aicq-chat"];
12409
+ for (const [key, value] of Object.entries(data)) {
12410
+ aicqConfig[key] = value;
12411
+ }
12412
+ const written = writeConfig(config2);
12413
+ if (!written) {
12414
+ return json(res, { success: false, message: "Failed to write config" }, 500);
12415
+ }
12416
+ logger.info("[API] Settings section saved: " + section);
12417
+ return json(res, { success: true, message: 'Section "' + section + '" saved' });
12418
+ }
12419
+ if (apiPath === "/config/raw" && method === "GET") {
12420
+ const result = readConfig();
12421
+ if (!result)
12422
+ return json(res, { error: "No config file found" }, 404);
12423
+ const raw = fs5.readFileSync(result.configPath, "utf-8");
12424
+ return json(res, {
12425
+ configPath: result.configPath,
12426
+ configSource: path5.basename(result.configPath),
12427
+ rawJson: raw,
12428
+ config: result.config
12429
+ });
12430
+ }
12431
+ if (apiPath === "/config/raw" && method === "PUT") {
12432
+ const body = await readBody(req);
12433
+ const rawJson = body.rawJson;
12434
+ if (!rawJson)
12435
+ return json(res, { success: false, message: "Missing rawJson" }, 400);
12436
+ let parsed;
12437
+ try {
12438
+ parsed = JSON.parse(rawJson);
12439
+ } catch (e) {
12440
+ return json(res, { success: false, message: "Invalid JSON: " + (e instanceof Error ? e.message : String(e)) }, 400);
12441
+ }
12442
+ const configPath = findConfigPath();
12443
+ if (!configPath)
12444
+ return json(res, { success: false, message: "No config file found" }, 400);
12445
+ try {
12446
+ fs5.writeFileSync(configPath, JSON.stringify(parsed, null, 2), "utf-8");
12447
+ logger.info("[API] Config file updated via raw JSON editor");
12448
+ return json(res, { success: true, message: "Config file saved", configPath });
12449
+ } catch (e) {
12450
+ const msg = e instanceof Error ? e.message : String(e);
12451
+ return json(res, { success: false, message: "Write failed: " + msg }, 500);
12452
+ }
12453
+ }
12454
+ if (apiPath === "/agents" && method === "POST") {
12455
+ const body = await readBody(req);
12456
+ const agent = body.agent;
12457
+ if (!agent || typeof agent !== "object") {
12458
+ return json(res, { success: false, message: "Missing agent object" }, 400);
12459
+ }
12460
+ const result = readConfig();
12461
+ if (!result)
12462
+ return json(res, { success: false, message: "No config file found" }, 400);
12463
+ const config2 = result.config;
12464
+ if (!Array.isArray(config2.agents)) {
12465
+ config2.agents = [];
12466
+ const singleAgent = config2.agent;
12467
+ if (typeof singleAgent === "object" && singleAgent !== null && !Array.isArray(singleAgent)) {
12468
+ config2.agents.push(singleAgent);
12469
+ delete config2.agent;
12470
+ }
12471
+ }
12472
+ config2.agents.push(agent);
12473
+ const written = writeConfig(config2);
12474
+ if (!written)
12475
+ return json(res, { success: false, message: "Failed to write config" }, 500);
12476
+ logger.info("[API] Agent added via UI");
12477
+ return json(res, { success: true, message: "Agent added", index: config2.agents.length - 1 });
12478
+ }
12479
+ if (apiPath.startsWith("/agents/") && method === "PUT") {
12480
+ const idxStr = decodeURIComponent(apiPath.slice("/agents/".length));
12481
+ const idx = parseInt(idxStr, 10);
12482
+ if (isNaN(idx) || idx < 0)
12483
+ return json(res, { success: false, message: "Invalid agent index" }, 400);
12484
+ const body = await readBody(req);
12485
+ const updates = body.agent;
12486
+ if (!updates || typeof updates !== "object") {
12487
+ return json(res, { success: false, message: "Missing agent object" }, 400);
12488
+ }
12489
+ const result = readConfig();
12490
+ if (!result)
12491
+ return json(res, { success: false, message: "No config file found" }, 400);
12492
+ const config2 = result.config;
12493
+ let agentsArr;
12494
+ if (!Array.isArray(config2.agents)) {
12495
+ return json(res, { success: false, message: "No agents array in config" }, 400);
12496
+ }
12497
+ agentsArr = config2.agents;
12498
+ if (idx >= agentsArr.length) {
12499
+ return json(res, { success: false, message: "Agent index out of range" }, 400);
12500
+ }
12501
+ Object.assign(agentsArr[idx], updates);
12502
+ const written = writeConfig(config2);
12503
+ if (!written)
12504
+ return json(res, { success: false, message: "Failed to write config" }, 500);
12505
+ logger.info("[API] Agent updated at index " + idx);
12506
+ return json(res, { success: true, message: "Agent updated" });
12507
+ }
12508
+ if (apiPath === "/config/switch" && method === "POST") {
12509
+ const body = await readBody(req);
12510
+ const target = body.target;
12511
+ if (target !== "openclaw" && target !== "stableclaw") {
12512
+ return json(res, { success: false, message: "target must be 'openclaw' or 'stableclaw'" }, 400);
12513
+ }
12514
+ const targetFile = target + ".json";
12515
+ const currentResult = readConfig();
12516
+ if (!currentResult)
12517
+ return json(res, { success: false, message: "No current config file found" }, 400);
12518
+ const currentBasename = path5.basename(currentResult.configPath);
12519
+ if (currentBasename === targetFile) {
12520
+ return json(res, { success: false, message: "Already using " + targetFile }, 400);
12521
+ }
12522
+ const targetPath = path5.join(path5.dirname(currentResult.configPath), targetFile);
12523
+ try {
12524
+ const raw = fs5.readFileSync(currentResult.configPath, "utf-8");
12525
+ fs5.writeFileSync(targetPath, raw, "utf-8");
12526
+ logger.info("[API] Config copied to " + targetFile);
12527
+ return json(res, { success: true, message: "Config copied to " + targetFile, newPath: targetPath });
12528
+ } catch (e) {
12529
+ const msg = e instanceof Error ? e.message : String(e);
12530
+ return json(res, { success: false, message: "Failed: " + msg }, 500);
12531
+ }
12532
+ }
12533
+ if (apiPath === "/config-file/raw" && method === "GET") {
12534
+ const configPath = findConfigPath();
12535
+ if (!configPath)
12536
+ return json(res, { error: "No config file found" }, 404);
12537
+ const configName = path5.basename(configPath);
12538
+ const raw = fs5.readFileSync(configPath, "utf-8");
12539
+ const config2 = JSON.parse(raw);
12540
+ const stats = fs5.statSync(configPath);
12541
+ return json(res, {
12542
+ configPath,
12543
+ configName,
12544
+ raw,
12545
+ config: config2,
12546
+ size: stats.size,
12547
+ modified: stats.mtime.toISOString()
12548
+ });
12549
+ }
12550
+ if (apiPath === "/config-file/raw" && method === "PUT") {
12551
+ const body = await readBody(req);
12552
+ const content = body.content;
12553
+ if (!content)
12554
+ return json(res, { success: false, message: "Missing content field" }, 400);
12555
+ let parsed;
12556
+ try {
12557
+ parsed = JSON.parse(content);
12558
+ } catch (e) {
12559
+ return json(res, { success: false, message: "Invalid JSON: " + (e instanceof Error ? e.message : String(e)) }, 400);
12560
+ }
12561
+ const configPath = findConfigPath();
12562
+ if (!configPath)
12563
+ return json(res, { success: false, message: "No config file found" }, 400);
12564
+ try {
12565
+ fs5.writeFileSync(configPath, JSON.stringify(parsed, null, 2), "utf-8");
12566
+ logger.info("[API] Config file written via /config-file/raw");
12567
+ return json(res, { success: true, message: "Config file saved", configPath });
12568
+ } catch (e) {
12569
+ const msg = e instanceof Error ? e.message : String(e);
12570
+ return json(res, { success: false, message: "Write failed: " + msg }, 500);
12571
+ }
12572
+ }
12573
+ if (apiPath.match(/^\/models\/[^/]+$/) && method === "DELETE") {
12574
+ const providerId = decodeURIComponent(apiPath.slice("/models/".length));
12575
+ const provider = MODEL_PROVIDERS.find((p) => p.id === providerId);
12576
+ if (!provider)
12577
+ return json(res, { success: false, message: "Unknown provider: " + providerId }, 400);
12578
+ const result = readConfig();
12579
+ if (!result)
12580
+ return json(res, { success: false, message: "No config file found" }, 400);
12581
+ const config2 = result.config;
12582
+ const providersSection = config2.providers;
12583
+ if (providersSection && typeof providersSection === "object" && providersSection[provider.configKey]) {
12584
+ providersSection[provider.configKey] = {};
12585
+ }
12586
+ if (config2[provider.configKey]) {
12587
+ config2[provider.configKey] = {};
12588
+ }
12589
+ const written = writeConfig(config2);
12590
+ if (!written)
12591
+ return json(res, { success: false, message: "Failed to write config file" }, 500);
12592
+ logger.info("[API] Model config cleared for provider: " + providerId);
12593
+ return json(res, { success: true, message: "Model configuration cleared for " + provider.name });
12594
+ }
11415
12595
  res.writeHead(404, { "Content-Type": "application/json" });
11416
12596
  res.end(JSON.stringify({ error: "Not found: " + apiPath }));
11417
12597
  } catch (err) {
@@ -11972,44 +13152,65 @@ var plugin = definePluginEntry({
11972
13152
  serverUrl,
11973
13153
  aicqAgentId,
11974
13154
  logger,
11975
- html: managementHtml
11976
- });
11977
- const mgmtPort = parseInt(process.env.AICQ_MGMT_PORT || "16888", 10);
11978
- const mgmtServer = http.createServer((req, res) => {
11979
- managementHandler(req, res).catch((err) => {
11980
- logger.error("[HTTP] Management server error: " + (err instanceof Error ? err.message : err));
11981
- if (!res.headersSent) {
11982
- res.writeHead(500, { "Content-Type": "text/plain" });
11983
- }
11984
- res.end("Internal Server Error");
11985
- });
11986
- });
11987
- mgmtServer.listen(mgmtPort, "127.0.0.1", () => {
11988
- logger.info("[Init] Management UI HTTP server running at http://127.0.0.1:" + mgmtPort + "/");
13155
+ html: managementHtml,
13156
+ chatChannel
11989
13157
  });
11990
- mgmtServer.on("error", (err) => {
11991
- if (err.code === "EADDRINUSE") {
11992
- logger.warn("[Init] Management UI port " + mgmtPort + " already in use, trying " + (mgmtPort + 1));
11993
- mgmtServer.close();
11994
- mgmtServer.listen(mgmtPort + 1, "127.0.0.1", () => {
11995
- logger.info("[Init] Management UI HTTP server running at http://127.0.0.1:" + (mgmtPort + 1) + "/");
13158
+ const apiKeys = Object.keys(api).filter((k) => typeof api[k] === "function");
13159
+ logger.info("[Init] Available API methods: " + apiKeys.join(", "));
13160
+ let mgmtPort = 6109;
13161
+ let mgmtUiRegistered = false;
13162
+ if (api.registerHttpRoute) {
13163
+ try {
13164
+ api.registerHttpRoute({
13165
+ path: "/plugins/aicq-chat",
13166
+ auth: "plugin",
13167
+ match: "prefix",
13168
+ handler: managementHandler
11996
13169
  });
11997
- } else {
11998
- logger.error("[Init] Management UI HTTP server error: " + err.message);
13170
+ logger.info("[Init] Management UI registered via gateway at /plugins/aicq-chat/");
13171
+ mgmtUiRegistered = true;
13172
+ } catch (routeErr) {
13173
+ logger.warn("[Init] Gateway route registration failed: " + (routeErr instanceof Error ? routeErr.message : String(routeErr)));
13174
+ }
13175
+ }
13176
+ if (!mgmtUiRegistered) {
13177
+ try {
13178
+ mgmtPort = parseInt(process.env.AICQ_MGMT_PORT || "6109", 10);
13179
+ const mgmtServer = http.createServer((req, res) => {
13180
+ managementHandler(req, res).catch((err) => {
13181
+ logger.error("[HTTP] Management server error: " + (err instanceof Error ? err.message : err));
13182
+ if (!res.headersSent) {
13183
+ res.writeHead(500, { "Content-Type": "text/plain" });
13184
+ }
13185
+ res.end("Internal Server Error");
13186
+ });
13187
+ });
13188
+ mgmtServer.listen(mgmtPort, "127.0.0.1", () => {
13189
+ logger.info("[Init] Management UI HTTP server running at http://127.0.0.1:" + mgmtPort + "/");
13190
+ });
13191
+ mgmtServer.on("error", (err) => {
13192
+ if (err.code === "EADDRINUSE") {
13193
+ logger.warn("[Init] Management UI port " + mgmtPort + " already in use, trying " + (mgmtPort + 1));
13194
+ mgmtServer.close();
13195
+ mgmtServer.listen(mgmtPort + 1, "127.0.0.1", () => {
13196
+ logger.info("[Init] Management UI HTTP server running at http://127.0.0.1:" + (mgmtPort + 1) + "/");
13197
+ });
13198
+ } else {
13199
+ logger.error("[Init] Management UI HTTP server error: " + err.message);
13200
+ }
13201
+ });
13202
+ logger.info("[Init] Standalone management UI server starting on port " + mgmtPort);
13203
+ } catch (httpErr) {
13204
+ logger.error("[Init] Failed to start management UI server: " + (httpErr instanceof Error ? httpErr.message : String(httpErr)));
11999
13205
  }
12000
- });
12001
- if (api.registerHttpRoute) {
12002
- api.registerHttpRoute({
12003
- path: "/aicq-chat",
12004
- auth: "gateway",
12005
- match: "prefix",
12006
- handler: managementHandler
12007
- });
12008
- logger.info("[Init] Management UI also registered via gateway at /plugins/aicq-chat/");
12009
13206
  }
12010
13207
  logger.info("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
12011
13208
  logger.info(" AICQ Plugin activated successfully!");
12012
- logger.info(" Management UI: http://127.0.0.1:" + mgmtPort + "/");
13209
+ if (mgmtUiRegistered) {
13210
+ logger.info(" Management UI: via gateway /plugins/aicq-chat/");
13211
+ } else {
13212
+ logger.info(" Management UI: http://127.0.0.1:" + mgmtPort + "/");
13213
+ }
12013
13214
  logger.info("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
12014
13215
  }
12015
13216
  });