aicq-openclaw-plugin 1.0.7 → 1.1.1

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;
@@ -7770,12 +7771,12 @@ var require_dist = __commonJS({
7770
7771
  var dotenv = __toESM(require_main(), 1);
7771
7772
  import * as path6 from "path";
7772
7773
  import * as http from "http";
7774
+ import { exec } from "child_process";
7773
7775
  import { definePluginEntry } from "openclaw/plugin-sdk/core";
7774
7776
 
7775
7777
  // dist/config.js
7776
7778
  import * as fs from "fs";
7777
7779
  import * as path from "path";
7778
- import { fileURLToPath } from "url";
7779
7780
 
7780
7781
  // node_modules/uuid/dist/esm-node/rng.js
7781
7782
  import crypto from "crypto";
@@ -7825,13 +7826,12 @@ function v4(options, buf, offset) {
7825
7826
  var v4_default = v4;
7826
7827
 
7827
7828
  // dist/config.js
7828
- var __filename = fileURLToPath(import.meta.url);
7829
- var __dirname2 = path.dirname(__filename);
7830
- var SERVER_URL = process.env.AICQ_SERVER_URL || "https://aicq.online:61018";
7829
+ var _dirname = typeof __dirname !== "undefined" ? __dirname : process.cwd();
7830
+ var SERVER_URL = process.env.AICQ_SERVER_URL || "https://aicq.online";
7831
7831
  function loadConfig(overrides) {
7832
7832
  let schemaDefaults = {};
7833
7833
  try {
7834
- const manifestPath = path.resolve(__dirname2, "..", "openclaw.plugin.json");
7834
+ const manifestPath = path.resolve(_dirname, "..", "openclaw.plugin.json");
7835
7835
  const manifestRaw = fs.readFileSync(manifestPath, "utf-8");
7836
7836
  const manifest = JSON.parse(manifestRaw);
7837
7837
  const schema = manifest.configSchema;
@@ -8191,7 +8191,7 @@ var PluginStore = class {
8191
8191
 
8192
8192
  // dist/services/identityService.js
8193
8193
  var import_qrcode = __toESM(require_lib(), 1);
8194
- var import_crypto3 = __toESM(require_dist(), 1);
8194
+ var import_crypto3 = __toESM(require_crypto(), 1);
8195
8195
  import * as crypto4 from "crypto";
8196
8196
  var IdentityService = class {
8197
8197
  constructor(store, logger) {
@@ -8676,7 +8676,7 @@ var ServerClient = class {
8676
8676
  };
8677
8677
 
8678
8678
  // dist/handshake/handshakeManager.js
8679
- var import_crypto4 = __toESM(require_dist(), 1);
8679
+ var import_crypto4 = __toESM(require_crypto(), 1);
8680
8680
  import * as crypto5 from "crypto";
8681
8681
  var HandshakeManager = class {
8682
8682
  constructor(store, serverClient, config2, logger) {
@@ -9092,7 +9092,7 @@ var P2PConnectionManager = class {
9092
9092
  };
9093
9093
 
9094
9094
  // dist/fileTransfer/transferManager.js
9095
- var import_crypto5 = __toESM(require_dist(), 1);
9095
+ var import_crypto5 = __toESM(require_crypto(), 1);
9096
9096
  import * as fs3 from "fs";
9097
9097
  import * as path3 from "path";
9098
9098
  var DEFAULT_CHUNK_SIZE = 64 * 1024;
@@ -9315,7 +9315,7 @@ var FileTransferManager = class {
9315
9315
  };
9316
9316
 
9317
9317
  // dist/channels/encryptedChat.js
9318
- var import_crypto6 = __toESM(require_dist(), 1);
9318
+ var import_crypto6 = __toESM(require_crypto(), 1);
9319
9319
  import * as fs4 from "fs";
9320
9320
  import * as path4 from "path";
9321
9321
  function safeFilePath(filePath, allowedDir) {
@@ -9755,7 +9755,7 @@ var BeforeToolCallHook = class {
9755
9755
  };
9756
9756
 
9757
9757
  // dist/hooks/messageSending.js
9758
- var import_crypto7 = __toESM(require_dist(), 1);
9758
+ var import_crypto7 = __toESM(require_crypto(), 1);
9759
9759
  var MessageSendingHook = class {
9760
9760
  constructor(store, handshakeManager, logger) {
9761
9761
  this.store = store;
@@ -10072,6 +10072,20 @@ tbody tr:hover { background: var(--bg3); }
10072
10072
  /* Section desc */
10073
10073
  .section-desc { font-size: 13px; color: var(--text2); margin-bottom: 20px; line-height: 1.6; }
10074
10074
 
10075
+ /* Toggle switch */
10076
+ .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; }
10077
+ .toggle-label input[type=checkbox] { display: none; }
10078
+ .toggle-slider {
10079
+ width: 40px; height: 22px; background: var(--bg4); border-radius: 11px;
10080
+ position: relative; transition: background var(--transition); flex-shrink: 0;
10081
+ }
10082
+ .toggle-slider::after {
10083
+ content: ''; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px;
10084
+ background: var(--text3); border-radius: 50%; transition: all var(--transition);
10085
+ }
10086
+ .toggle-label input:checked + .toggle-slider { background: var(--accent); }
10087
+ .toggle-label input:checked + .toggle-slider::after { left: 21px; background: #fff; }
10088
+
10075
10089
  /* Responsive */
10076
10090
  @media (max-width: 768px) {
10077
10091
  .sidebar { position: fixed; left: -260px; z-index: 50; height: 100vh; transition: left var(--transition); }
@@ -10114,37 +10128,14 @@ function createToastContainer() {
10114
10128
  return c;
10115
10129
  }
10116
10130
 
10117
- // \u2500\u2500 API with timeout \u2500\u2500
10131
+ // \u2500\u2500 API \u2500\u2500
10118
10132
  async function api(path, opts = {}) {
10119
- const timeout = (opts._timeout || 8000);
10120
- const controller = new AbortController();
10121
- const timer = setTimeout(() => controller.abort(), timeout);
10122
10133
  try {
10123
- const res = await fetch(API + path, { headers: { 'Content-Type': 'application/json', ...opts.headers }, ...opts, signal: controller.signal });
10124
- clearTimeout(timer);
10134
+ const res = await fetch(API + path, { headers: { 'Content-Type': 'application/json', ...opts.headers }, ...opts });
10125
10135
  const data = await res.json();
10126
10136
  if (!res.ok && !data.error) data.error = 'HTTP ' + res.status;
10127
10137
  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>';
10138
+ } catch (e) { return { error: e.message }; }
10148
10139
  }
10149
10140
 
10150
10141
  // \u2500\u2500 Utilities \u2500\u2500
@@ -10192,86 +10183,74 @@ function loadPage(page) {
10192
10183
  }
10193
10184
 
10194
10185
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
10195
- // PAGE: Dashboard \u2014 render skeleton immediately, load data async
10186
+ // PAGE: Dashboard
10196
10187
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
10197
10188
  async function loadDashboard() {
10198
10189
  const el = $('#dashboard-content');
10199
- // Render skeleton immediately \u2014 never show spinner that blocks UI
10190
+ html(el, '<div class="loading-mask"><div class="spinner"></div>Loading dashboard...</div>');
10191
+ const [status, friends, identity, mgmtUrl] = await Promise.all([api('/status'), api('/friends'), api('/identity'), api('/mgmt-url')]);
10192
+ if (status.error) { html(el, '<div class="empty"><div class="icon">\u26A0\uFE0F</div><p>Failed to connect to AICQ plugin</p></div>'); return; }
10193
+ const connCls = status.connected ? 'dot-ok' : 'dot-err';
10194
+ const connText = status.connected ? 'Connected' : 'Disconnected';
10195
+ const friendList = friends.friends || [];
10196
+ const aiFriends = friendList.filter(f => f.friendType === 'ai').length;
10197
+ const humanFriends = friendList.filter(f => f.friendType !== 'ai').length;
10198
+ const mgmtLink = mgmtUrl?.mgmtUrl || window.location.origin;
10199
+
10200
10200
  html(el, \\\`
10201
10201
  <div class="stats-grid">
10202
- <div class="stat-card"><div class="stat-icon" style="background:var(--accent-bg)">\u{1F4E1}</div><div class="stat-label">Server Status</div><div class="stat-value" style="font-size:16px"><div class="spinner" style="width:16px;height:16px;border-width:2px;margin:0 auto"></div></div><div class="stat-sub" id="dash-server-url">Loading...</div></div>
10203
- <div class="stat-card"><div class="stat-icon" style="background:var(--ok-bg)">\u{1F465}</div><div class="stat-label">Total Friends</div><div class="stat-value" id="dash-friend-count"><div class="spinner" style="width:16px;height:16px;border-width:2px;margin:0 auto"></div></div><div class="stat-sub" id="dash-friend-sub">loading...</div></div>
10204
- <div class="stat-card"><div class="stat-icon" style="background:var(--info-bg)">\u{1F517}</div><div class="stat-label">Active Sessions</div><div class="stat-value" id="dash-sessions"><div class="spinner" style="width:16px;height:16px;border-width:2px;margin:0 auto"></div></div><div class="stat-sub">Encrypted sessions</div></div>
10205
- <div class="stat-card"><div class="stat-icon" style="background:var(--warn-bg)">\u{1F511}</div><div class="stat-label">Agent ID</div><div class="stat-value mono" style="font-size:13px" id="dash-agent-id">\u2014</div><div class="stat-sub" id="dash-fingerprint">\u2014</div></div>
10202
+ <div class="stat-card">
10203
+ <div class="stat-icon" style="background:var(--accent-bg)">\u{1F4E1}</div>
10204
+ <div class="stat-label">Server Status</div>
10205
+ <div class="stat-value" style="font-size:16px;display:flex;align-items:center;gap:8px">
10206
+ <span class="dot \${connCls}"></span> \${connText}
10207
+ </div>
10208
+ <div class="stat-sub">\${escHtml(status.serverUrl)}</div>
10209
+ </div>
10210
+ <div class="stat-card">
10211
+ <div class="stat-icon" style="background:var(--ok-bg)">\u{1F465}</div>
10212
+ <div class="stat-label">Total Friends</div>
10213
+ <div class="stat-value">\${friendList.length}</div>
10214
+ <div class="stat-sub">\${aiFriends} AI \xB7 \${humanFriends} Human</div>
10215
+ </div>
10216
+ <div class="stat-card">
10217
+ <div class="stat-icon" style="background:var(--info-bg)">\u{1F517}</div>
10218
+ <div class="stat-label">Active Sessions</div>
10219
+ <div class="stat-value">\${status.sessionCount || 0}</div>
10220
+ <div class="stat-sub">Encrypted sessions</div>
10221
+ </div>
10222
+ <div class="stat-card">
10223
+ <div class="stat-icon" style="background:var(--warn-bg)">\u{1F511}</div>
10224
+ <div class="stat-label">Agent ID</div>
10225
+ <div class="stat-value mono" style="font-size:13px">\${escHtml(status.agentId)}</div>
10226
+ <div class="stat-sub">Fingerprint: \${escHtml(status.fingerprint)}</div>
10227
+ </div>
10206
10228
  </div>
10207
10229
  <div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
10208
- <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><div id="dash-friends-list"><div class="loading-mask" style="padding:20px"><div class="spinner"></div>Loading friends...</div></div></div>
10209
- <div class="card"><div class="card-header"><div class="card-title">\u{1F916} Identity Info</div></div><div id="dash-identity"><div class="loading-mask" style="padding:20px"><div class="spinner"></div>Loading...</div></div></div>
10230
+ <div class="card">
10231
+ <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>
10232
+ \${renderMiniFriendList(friendList.slice(0, 5))}
10233
+ </div>
10234
+ <div class="card">
10235
+ <div class="card-header"><div class="card-title">\u{1F916} Identity Info</div></div>
10236
+ <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>
10237
+ <div class="detail-row"><div class="detail-key">Fingerprint</div><div class="detail-val mono">\${escHtml(identity.publicKeyFingerprint)}</div></div>
10238
+ <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>
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
+ <div class="detail-row"><div class="detail-key">Plugin Version</div><div class="detail-val"><span class="badge badge-accent">v1.1.1</span></div></div>
10241
+ </div>
10242
+ </div>
10243
+ <div class="card" style="margin-top:0">
10244
+ <div class="card-header"><div class="card-title">\u{1F5A5}\uFE0F Management UI Access</div></div>
10245
+ <div class="detail-row"><div class="detail-key">Current URL</div><div class="detail-val"><a href="\${escHtml(mgmtLink)}" target="_blank" style="color:var(--info);text-decoration:underline">\${escHtml(mgmtLink)}</a></div></div>
10246
+ <div class="detail-row"><div class="detail-key">Local Access</div><div class="detail-val"><a href="http://127.0.0.1:6109" target="_blank" style="color:var(--info);text-decoration:underline">http://127.0.0.1:6109</a> <button class="btn btn-sm btn-primary" onclick="window.open('http://127.0.0.1:6109','_blank')" style="margin-left:8px">\u{1F517} Open</button></div></div>
10247
+ <div class="detail-row"><div class="detail-key">Gateway Path</div><div class="detail-val mono">/plugins/aicq-chat/</div></div>
10210
10248
  </div>
10211
10249
  \\\`);
10212
10250
 
10213
- // Load each data source independently \u2014 no await blocking between them
10214
- const statusRes = await safeApi('/status', { _timeout: 5000 });
10215
- const status = statusRes.data || {};
10216
-
10217
- // Update status card immediately
10218
- const connCls = status.connected ? 'dot-ok' : 'dot-err';
10219
- const connText = status.connected ? 'Connected' : 'Disconnected';
10220
- const connEl = $('#dash-server-url');
10221
- if (connEl) connEl.textContent = status.serverUrl || '\u2014';
10222
- const statusValEl = el.querySelector('.stat-value');
10223
- if (statusValEl) statusValEl.innerHTML = '<span class="dot ' + connCls + '" style="display:inline-block;width:8px;height:8px;vertical-align:middle"></span> <span style="font-size:16px">' + connText + '</span>';
10224
-
10225
- // Update header dot
10226
- const hDot = $('#header-dot');
10227
- if (hDot) hDot.className = 'dot ' + connCls;
10228
- const hTxt = $('#header-status');
10229
- if (hTxt) hTxt.textContent = connText;
10230
-
10231
- // Agent ID card
10232
- const agentEl = $('#dash-agent-id');
10233
- if (agentEl) agentEl.textContent = status.agentId || '\u2014';
10234
- const fpEl = $('#dash-fingerprint');
10235
- if (fpEl) fpEl.textContent = 'Fingerprint: ' + (status.fingerprint || '\u2014');
10236
- const sessEl = $('#dash-sessions');
10237
- if (sessEl) sessEl.textContent = status.sessionCount || 0;
10238
-
10239
- // Identity \u2014 load independently
10240
- safeApi('/identity', { _timeout: 5000 }).then(identityRes => {
10241
- const identityEl = $('#dash-identity');
10242
- if (!identityEl) return;
10243
- if (identityRes.error) {
10244
- identityEl.innerHTML = errorBlock('Identity Unavailable', identityRes.error + ' <button class="btn btn-sm btn-default" style="margin-top:8px" onclick="loadDashboard()">\u{1F504} Retry</button>');
10245
- return;
10246
- }
10247
- const identity = identityRes.data || {};
10248
- identityEl.innerHTML = \\\`
10249
- <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>
10250
- <div class="detail-row"><div class="detail-key">Fingerprint</div><div class="detail-val mono">\${escHtml(identity.publicKeyFingerprint || '\u2014')}</div></div>
10251
- <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>
10252
- <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>
10253
- \\\`;
10254
- });
10255
-
10256
- // Friends \u2014 load independently from remote server
10257
- safeApi('/friends', { _timeout: 10000 }).then(friendsRes => {
10258
- const fcEl = $('#dash-friend-count');
10259
- const fsEl = $('#dash-friend-sub');
10260
- const flEl = $('#dash-friends-list');
10261
- if (friendsRes.error) {
10262
- if (fcEl) fcEl.textContent = '\u2014';
10263
- if (fsEl) fsEl.textContent = 'unavailable';
10264
- if (flEl) flEl.innerHTML = errorBlock('Friend List Unavailable', friendsRes.error + ' \u2014 Remote server unreachable. <button class="btn btn-sm btn-default" style="margin-top:8px" onclick="loadDashboard()">\u{1F504} Retry</button>');
10265
- toast('Cannot load friends: ' + friendsRes.error, 'warn');
10266
- return;
10267
- }
10268
- const friendList = friendsRes.data?.friends || [];
10269
- const aiFriends = friendList.filter(f => f.friendType === 'ai').length;
10270
- const humanFriends = friendList.filter(f => f.friendType !== 'ai').length;
10271
- if (fcEl) fcEl.textContent = friendList.length;
10272
- if (fsEl) fsEl.textContent = aiFriends + ' AI \xB7 ' + humanFriends + ' Human';
10273
- if (flEl) flEl.innerHTML = renderMiniFriendList(friendList.slice(0, 5));
10274
- });
10251
+ // Also set the mgmt-url-display in settings
10252
+ const mgmtUrlEl = document.getElementById('mgmt-url-display');
10253
+ if (mgmtUrlEl) mgmtUrlEl.textContent = window.location.href;
10275
10254
  }
10276
10255
 
10277
10256
  function renderMiniFriendList(friends) {
@@ -10292,6 +10271,7 @@ async function loadAgents() {
10292
10271
  const data = await api('/agents');
10293
10272
  if (data.error) { html(el, '<div class="empty"><div class="icon">\u26A0\uFE0F</div><p>' + escHtml(data.error) + '</p></div>'); return; }
10294
10273
 
10274
+ window._lastAgentsData = data;
10295
10275
  const agents = data.agents || [];
10296
10276
  const configSource = data.configSource || 'unknown';
10297
10277
 
@@ -10310,6 +10290,7 @@ async function loadAgents() {
10310
10290
  <td>
10311
10291
  <div class="actions-cell">
10312
10292
  <button class="btn btn-sm btn-ghost" onclick="viewAgent(\${i})" title="View">\u{1F441}\uFE0F</button>
10293
+ <button class="btn btn-sm btn-ok" onclick="showEditAgentModal(\${i})" title="Edit">\u270F\uFE0F</button>
10313
10294
  <button class="btn btn-sm btn-danger" onclick="deleteAgent(\${i})" title="Delete">\u{1F5D1}\uFE0F</button>
10314
10295
  </div>
10315
10296
  </td>
@@ -10327,6 +10308,7 @@ async function loadAgents() {
10327
10308
  html(el, \\\`
10328
10309
  <div class="toolbar">
10329
10310
  <div class="search-box"><input type="text" placeholder="Search agents..." id="agent-search" oninput="filterAgentTable()"></div>
10311
+ <button class="btn btn-sm btn-primary" onclick="showAddAgentModal()">\u2795 Add Agent</button>
10330
10312
  <button class="btn btn-sm btn-default" onclick="loadAgents()">\u{1F504} Refresh</button>
10331
10313
  </div>
10332
10314
  <p class="section-desc">Agent list from <strong style="color:var(--accent2)">\${escHtml(configSource)}</strong>. Total: <strong>\${agents.length}</strong> agents configured.</p>
@@ -10371,6 +10353,83 @@ async function deleteAgent(index) {
10371
10353
  else { toast(r.message || r.error || 'Delete failed', 'err'); }
10372
10354
  }
10373
10355
 
10356
+ let _editAgentIndex = null;
10357
+
10358
+ function showAddAgentModal() {
10359
+ _editAgentIndex = null;
10360
+ $('#agent-form-title').textContent = '\u2795 Add New Agent';
10361
+ $('#agent-form-name').value = '';
10362
+ $('#agent-form-id').value = '';
10363
+ $('#agent-form-model').value = '';
10364
+ $('#agent-form-provider').value = '';
10365
+ $('#agent-form-prompt').value = '';
10366
+ $('#agent-form-enabled').checked = true;
10367
+ $('#agent-form-temperature').value = '0.7';
10368
+ $('#agent-form-max-tokens').value = '4096';
10369
+ $('#agent-form-top-p').value = '1';
10370
+ $('#agent-form-tools').value = '';
10371
+ showModal('modal-add-agent');
10372
+ setTimeout(() => $('#agent-form-name')?.focus(), 100);
10373
+ }
10374
+
10375
+ function showEditAgentModal(index) {
10376
+ const agents = window._lastAgentsData?.agents || [];
10377
+ const a = agents[index];
10378
+ if (!a) return;
10379
+ _editAgentIndex = index;
10380
+ $('#agent-form-title').textContent = '\u270F\uFE0F Edit Agent';
10381
+ $('#agent-form-name').value = a.name || '';
10382
+ $('#agent-form-id').value = a.id || '';
10383
+ $('#agent-form-model').value = a.model || '';
10384
+ $('#agent-form-provider').value = a.provider || '';
10385
+ $('#agent-form-prompt').value = a.systemPrompt || '';
10386
+ $('#agent-form-enabled').checked = a.enabled !== false;
10387
+ $('#agent-form-temperature').value = a.temperature ?? 0.7;
10388
+ $('#agent-form-max-tokens').value = a.maxTokens ?? 4096;
10389
+ $('#agent-form-top-p').value = a.topP ?? 1;
10390
+ $('#agent-form-tools').value = Array.isArray(a.tools) ? a.tools.join(', ') : (a.tools || '');
10391
+ showModal('modal-add-agent');
10392
+ }
10393
+
10394
+ async function saveAgent() {
10395
+ const tempVal = parseFloat($('#agent-form-temperature')?.value);
10396
+ const maxTokensVal = parseInt($('#agent-form-max-tokens')?.value, 10);
10397
+ const topPVal = parseFloat($('#agent-form-top-p')?.value);
10398
+ const toolsRaw = $('#agent-form-tools')?.value?.trim() || '';
10399
+
10400
+ const agent = {
10401
+ name: $('#agent-form-name')?.value?.trim() || '',
10402
+ id: $('#agent-form-id')?.value?.trim() || '',
10403
+ model: $('#agent-form-model')?.value?.trim() || '',
10404
+ provider: $('#agent-form-provider')?.value?.trim() || '',
10405
+ systemPrompt: $('#agent-form-prompt')?.value?.trim() || '',
10406
+ enabled: $('#agent-form-enabled')?.checked ?? true,
10407
+ temperature: isNaN(tempVal) ? 0.7 : Math.min(2, Math.max(0, tempVal)),
10408
+ maxTokens: isNaN(maxTokensVal) ? 4096 : maxTokensVal,
10409
+ topP: isNaN(topPVal) ? 1 : Math.min(1, Math.max(0, topPVal)),
10410
+ tools: toolsRaw ? toolsRaw.split(',').map(t => t.trim()).filter(Boolean) : [],
10411
+ };
10412
+
10413
+ if (!agent.name) { toast('Agent name is required', 'warn'); return; }
10414
+
10415
+ let r;
10416
+ if (_editAgentIndex !== null) {
10417
+ // Edit existing
10418
+ r = await api('/agents/' + _editAgentIndex, { method: 'PUT', body: JSON.stringify({ agent }) });
10419
+ } else {
10420
+ // Add new
10421
+ r = await api('/agents', { method: 'POST', body: JSON.stringify({ agent }) });
10422
+ }
10423
+
10424
+ if (r.success) {
10425
+ toast(_editAgentIndex !== null ? 'Agent updated' : 'Agent added', 'ok');
10426
+ hideModal('modal-add-agent');
10427
+ loadAgents();
10428
+ } else {
10429
+ toast(r.message || r.error || 'Failed', 'err');
10430
+ }
10431
+ }
10432
+
10374
10433
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
10375
10434
  // PAGE: Friends Management
10376
10435
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
@@ -10379,17 +10438,12 @@ let friendsFilter = 'all';
10379
10438
  async function loadFriends() {
10380
10439
  const el = $('#friends-content');
10381
10440
  html(el, '<div class="loading-mask"><div class="spinner"></div>Loading friends...</div>');
10441
+ const [friends, requests, sessions] = await Promise.all([api('/friends'), api('/friends/requests'), api('/sessions')]);
10382
10442
 
10383
- // Load all three independently \u2014 any can fail without blocking others
10384
- const [friendsRes, requestsRes, sessionsRes] = await Promise.all([
10385
- safeApi('/friends', { _timeout: 10000 }),
10386
- safeApi('/friends/requests', { _timeout: 10000 }),
10387
- safeApi('/sessions', { _timeout: 5000 }),
10388
- ]);
10389
-
10390
- const friendCount = (friendsRes.data?.friends || []).length;
10391
- const reqCount = (requestsRes.data?.requests || []).length;
10392
- const sessCount = (sessionsRes.data?.sessions || []).length;
10443
+ // Sub-tabs
10444
+ const friendCount = (friends.friends || []).length;
10445
+ const reqCount = (requests.requests || []).length;
10446
+ const sessCount = (sessions.sessions || []).length;
10393
10447
 
10394
10448
  html('#friends-tabs', \\\`
10395
10449
  <button class="filter-btn \${friendsSubTab==='friends'?'active':''}" onclick="friendsSubTab='friends';loadFriends()">\u{1F465} Friends (<span id="fc">\${friendCount}</span>)</button>
@@ -10397,29 +10451,13 @@ async function loadFriends() {
10397
10451
  <button class="filter-btn \${friendsSubTab==='sessions'?'active':''}" onclick="friendsSubTab='sessions';loadFriends()">\u{1F517} Sessions (<span id="sc">\${sessCount}</span>)</button>
10398
10452
  \\\`);
10399
10453
 
10400
- window._friendsData = friendsRes.data || {};
10401
- window._requestsData = requestsRes.data || {};
10402
- window._sessionsData = sessionsRes.data || {};
10454
+ window._friendsData = friends;
10455
+ window._requestsData = requests;
10456
+ window._sessionsData = sessions;
10403
10457
 
10404
- if (friendsSubTab === 'friends') {
10405
- if (friendsRes.error) {
10406
- 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.'));
10407
- } else {
10408
- renderFriendsList(friendsRes.data.friends || []);
10409
- }
10410
- } else if (friendsSubTab === 'requests') {
10411
- if (requestsRes.error) {
10412
- 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));
10413
- } else {
10414
- renderRequestsList(requestsRes.data.requests || []);
10415
- }
10416
- } else {
10417
- if (sessionsRes.error) {
10418
- 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));
10419
- } else {
10420
- renderSessionsList(sessionsRes.data.sessions || []);
10421
- }
10422
- }
10458
+ if (friendsSubTab === 'friends') renderFriendsList(friends.friends || []);
10459
+ else if (friendsSubTab === 'requests') renderRequestsList(requests.requests || []);
10460
+ else renderSessionsList(sessions.sessions || []);
10423
10461
  }
10424
10462
  window.friendsSubTab = 'friends';
10425
10463
 
@@ -10616,7 +10654,12 @@ function renderModels(data) {
10616
10654
  <td class="mono">\${escHtml(m.modelId)}</td>
10617
10655
  <td><span class="badge badge-ok">\u25CF Key set</span></td>
10618
10656
  <td class="mono" style="font-size:11px">\${escHtml(m.baseUrl || 'default')}</td>
10619
- <td><button class="btn btn-sm btn-ghost" onclick="showModelConfigModal('\${escHtml(m.providerId)}')">Edit</button></td>
10657
+ <td>
10658
+ <div class="actions-cell">
10659
+ <button class="btn btn-sm btn-ghost" onclick="showModelConfigModal('\${escHtml(m.providerId)}')">Edit</button>
10660
+ <button class="btn btn-sm btn-danger" onclick="deleteModelProvider('\${escHtml(m.providerId)}')" title="Delete">\u{1F5D1}\uFE0F</button>
10661
+ </div>
10662
+ </td>
10620
10663
  </tr>\\\`;
10621
10664
  });
10622
10665
  activeModelsSection = \\\`
@@ -10671,180 +10714,661 @@ async function saveModelConfig() {
10671
10714
  if (r.success) { toast(r.message || 'Configuration saved!', 'ok'); loadModels(); }
10672
10715
  else { toast(r.message || r.error || 'Failed to save', 'err'); }
10673
10716
  }
10717
+ async function deleteModelProvider(providerId) {
10718
+ if (!confirm('Delete configuration for provider "' + providerId + '"? This will remove its API key and model settings.')) return;
10719
+ const r = await api('/models/' + encodeURIComponent(providerId), { method: 'DELETE' });
10720
+ if (r.success) { toast('Provider configuration deleted', 'ok'); loadModels(); }
10721
+ else { toast(r.message || r.error || 'Delete failed', 'err'); }
10722
+ }
10674
10723
 
10675
10724
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
10676
- // PAGE: Settings \u2014 with Update button
10725
+ // PAGE: Settings (comprehensive with AJAX, tabs, live test)
10677
10726
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
10678
- let _updating = false;
10727
+ let _settingsSaving = false;
10728
+ let _settingsData = null;
10729
+ let _settingsTab = 'connection';
10730
+
10731
+ function formatBytes(bytes) {
10732
+ if (!bytes || bytes === 0) return '0 B';
10733
+ const k = 1024, sizes = ['B', 'KB', 'MB', 'GB'];
10734
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
10735
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
10736
+ }
10737
+
10738
+ function formatUptime(seconds) {
10739
+ if (!seconds) return '\u2014';
10740
+ const d = Math.floor(seconds / 86400), h = Math.floor((seconds % 86400) / 3600);
10741
+ const m = Math.floor((seconds % 3600) / 60), s = seconds % 60;
10742
+ let parts = [];
10743
+ if (d > 0) parts.push(d + 'd');
10744
+ if (h > 0) parts.push(h + 'h');
10745
+ if (m > 0) parts.push(m + 'm');
10746
+ parts.push(s + 's');
10747
+ return parts.join(' ');
10748
+ }
10679
10749
 
10680
10750
  async function loadSettings() {
10681
10751
  const el = $('#settings-content');
10682
- // Render skeleton immediately
10683
- html(el, \\\`
10684
- <p class="section-desc">AICQ plugin runtime configuration and system information.</p>
10685
- <div class="card" id="settings-update-card"><div class="loading-mask" style="padding:20px"><div class="spinner"></div>Loading...</div></div>
10686
- <div id="settings-rest"></div>
10752
+ html(el, '<div class="loading-mask"><div class="spinner"></div>Loading settings...</div>');
10753
+
10754
+ const settings = await api('/settings');
10755
+ if (settings.error) {
10756
+ html(el, '<div class="empty"><div class="icon">\u26A0\uFE0F</div><p>' + escHtml(settings.error) + '</p></div>');
10757
+ return;
10758
+ }
10759
+
10760
+ _settingsData = settings;
10761
+
10762
+ // Render settings tabs nav
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>
10687
10769
  \\\`);
10688
10770
 
10689
- // Load update check, status, identity, config \u2014 all independently
10690
- const [updateRes, statusRes, identityRes, configRes, pluginRes] = await Promise.all([
10691
- safeApi('/update/check', { _timeout: 15000 }),
10692
- safeApi('/status', { _timeout: 5000 }),
10693
- safeApi('/identity', { _timeout: 5000 }),
10694
- safeApi('/config', { _timeout: 5000 }),
10695
- safeApi('/plugin-info', { _timeout: 5000 }),
10696
- ]);
10771
+ renderSettingsTab();
10772
+ }
10773
+
10774
+ function renderSettingsTab() {
10775
+ // Update tab buttons
10776
+ html('#settings-tabs', \\\`
10777
+ <button class="filter-btn \${_settingsTab==='connection'?'active':''}" onclick="_settingsTab='connection';renderSettingsTab()">\u{1F50C} Connection</button>
10778
+ <button class="filter-btn \${_settingsTab==='friends'?'active':''}" onclick="_settingsTab='friends';renderSettingsTab()">\u{1F465} Friends</button>
10779
+ <button class="filter-btn \${_settingsTab==='security'?'active':''}" onclick="_settingsTab='security';renderSettingsTab()">\u{1F512} Security</button>
10780
+ <button class="filter-btn \${_settingsTab==='advanced'?'active':''}" onclick="_settingsTab='advanced';renderSettingsTab()">\u2699\uFE0F Advanced</button>
10781
+ <button class="filter-btn \${_settingsTab==='json'?'active':''}" onclick="_settingsTab='json';renderSettingsTab()">\u{1F4DD} JSON Editor</button>
10782
+ \\\`);
10697
10783
 
10698
- // \u2500\u2500 Update Card \u2500\u2500
10699
- const updateCard = $('#settings-update-card');
10700
- if (updateCard) {
10701
- const upd = updateRes.data || {};
10702
- const pInfo = pluginRes.data || {};
10703
- const currentVer = pInfo.version || upd.currentVersion || 'unknown';
10704
- const latestVer = upd.latestVersion || currentVer;
10705
- const hasUpdate = upd.updateAvailable;
10784
+ switch (_settingsTab) {
10785
+ case 'connection': renderSettingsConnection(); break;
10786
+ case 'friends': renderSettingsFriends(); break;
10787
+ case 'security': renderSettingsSecurity(); break;
10788
+ case 'advanced': renderSettingsAdvanced(); break;
10789
+ case 'json': renderSettingsJsonEditor(); break;
10790
+ }
10791
+ }
10792
+
10793
+ function sectionSaveBtn(section, id) {
10794
+ return \\\`<button class="btn btn-primary btn-sm" id="btn-save-\${id}" onclick="saveSettingsSection('\${section}', '\${id}')">\u{1F4BE} Save</button>
10795
+ <span id="status-\${id}" style="font-size:12px;color:var(--text3);margin-left:8px"></span>\\\`;
10796
+ }
10797
+
10798
+ // \u2500\u2500 CONNECTION TAB \u2500\u2500
10799
+ function renderSettingsConnection() {
10800
+ const s = _settingsData;
10801
+ const el = $('#settings-content');
10802
+
10803
+ html(el, \\\`
10804
+ <p class="section-desc">Configure server connection and WebSocket settings. Changes require a plugin restart to take full effect.</p>
10706
10805
 
10707
- updateCard.innerHTML = \\\`
10806
+ <div class="card">
10708
10807
  <div class="card-header">
10709
- <div class="card-title">\u{1F504} Plugin Update</div>
10710
- <div style="display:flex;gap:6px;align-items:center">
10711
- <span class="badge badge-\${hasUpdate ? 'warn' : 'ok'}">\${hasUpdate ? 'Update Available!' : 'Up to date'}</span>
10808
+ <div class="card-title">\u{1F310} Server Connection</div>
10809
+ <span class="badge badge-\${s.connected ? 'ok' : 'danger'}">\${s.connected ? '\u25CF Connected' : '\u25CB Disconnected'}</span>
10810
+ </div>
10811
+ <div class="form-group">
10812
+ <label>Server URL</label>
10813
+ <div style="display:flex;gap:8px;align-items:start">
10814
+ <div style="flex:1">
10815
+ <div class="input-prefix">
10816
+ <span class="prefix">\u{1F310}</span>
10817
+ <input type="url" id="set-server-url" value="\${escHtml(s.serverUrl || '')}" placeholder="https://aicq.online">
10818
+ </div>
10819
+ <div class="hint">The HTTPS URL of the AICQ relay server. WebSocket path /ws is auto-appended.</div>
10820
+ </div>
10821
+ <button class="btn btn-ok btn-sm" id="btn-test-conn" onclick="testConnection()" style="white-space:nowrap;margin-top:1px">\u{1F50D} Test</button>
10822
+ </div>
10823
+ <div id="conn-test-result" style="margin-top:8px"></div>
10824
+ </div>
10825
+
10826
+ <div class="form-row">
10827
+ <div class="form-group">
10828
+ <label>Connection Timeout (seconds)</label>
10829
+ <input type="number" id="set-connection-timeout" value="\${s.connectionTimeout || 30}" min="5" max="120" placeholder="30">
10830
+ <div class="hint">HTTP request timeout (5\u2013120s). Default: 30s.</div>
10831
+ </div>
10832
+ <div class="form-group">
10833
+ <label>WS Auto-Reconnect</label>
10834
+ <div style="display:flex;align-items:center;gap:10px;margin-top:6px">
10835
+ <label class="toggle-label">
10836
+ <input type="checkbox" id="set-ws-auto-reconnect" \${s.wsAutoReconnect ? 'checked' : ''}>
10837
+ <span class="toggle-slider"></span>
10838
+ <span>Auto-reconnect when disconnected</span>
10839
+ </label>
10840
+ </div>
10841
+ <div class="hint">Automatically reconnect WebSocket on disconnection.</div>
10712
10842
  </div>
10713
10843
  </div>
10714
- <div class="detail-row"><div class="detail-key">Current Version</div><div class="detail-val mono">\${escHtml(currentVer)}</div></div>
10715
- <div class="detail-row"><div class="detail-key">Latest Version</div><div class="detail-val mono">\${escHtml(latestVer)}</div></div>
10716
- <div class="detail-row"><div class="detail-key">Package</div><div class="detail-val mono">aicq-openclaw-plugin</div></div>
10717
- \${pluginRes.error ? '' : \\\`
10718
- <div class="detail-row"><div class="detail-key">Node.js</div><div class="detail-val mono">\${escHtml(pInfo.nodeVersion || '\u2014')}</div></div>
10719
- <div class="detail-row"><div class="detail-key">Uptime</div><div class="detail-val">\${pInfo.uptime ? Math.floor(pInfo.uptime / 3600) + 'h ' + Math.floor((pInfo.uptime % 3600) / 60) + 'm' : '\u2014'}</div></div>
10720
- \\\`}
10721
- <div style="margin-top:16px;display:flex;gap:8px;flex-wrap:wrap">
10722
- <button class="btn btn-primary" id="btn-check-update" onclick="checkForUpdates()">\u{1F50D} Check for Updates</button>
10723
- <button class="btn btn-ok \${hasUpdate ? '' : 'btn-ghost'}" id="btn-do-update" onclick="doUpdate(false)">\u{1F4E5} \${hasUpdate ? 'Update to v' + escHtml(latestVer) : 'Incremental Update'}</button>
10724
- <button class="btn btn-warn" id="btn-force-update" onclick="doUpdate(true)">\u{1F503} Force Reinstall</button>
10844
+
10845
+ <div class="form-group">
10846
+ <label>WS Reconnect Interval (seconds)</label>
10847
+ <input type="number" id="set-ws-reconnect-interval" value="\${s.wsReconnectInterval || 60}" min="5" max="600" placeholder="60">
10848
+ <div class="hint">Interval between reconnection attempts (5\u2013600s). Default: 60s.</div>
10725
10849
  </div>
10726
- <div id="update-output" style="display:none;margin-top:12px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);padding:12px;max-height:300px;overflow-y:auto">
10727
- <pre style="font-size:12px;color:var(--text2);white-space:pre-wrap;margin:0" id="update-log"></pre>
10850
+
10851
+ <div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
10852
+ \${sectionSaveBtn('connection', 'conn')}
10728
10853
  </div>
10729
- \\\`;
10730
- }
10854
+ </div>
10855
+
10856
+ <div class="card">
10857
+ <div class="card-header"><div class="card-title">\u{1F4C1} Config File</div></div>
10858
+ <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>
10859
+ <div class="detail-row"><div class="detail-key">Plugin Version</div><div class="detail-val">1.1.1</div></div>
10860
+ <div class="detail-row"><div class="detail-key">Management UI</div><div class="detail-val" id="mgmt-url-display" style="cursor:pointer" onclick="copyText(document.getElementById('mgmt-url-display')?.textContent || '')"></div></div>
10861
+ <div class="detail-row"><div class="detail-key">Uptime</div><div class="detail-val">\${formatUptime(s.uptimeSeconds)}</div></div>
10862
+ </div>
10863
+ \\\`);
10864
+ }
10865
+
10866
+ async function testConnection() {
10867
+ const btn = $('#btn-test-conn');
10868
+ const resultEl = $('#conn-test-result');
10869
+ const url = $('#set-server-url')?.value?.trim() || _settingsData.serverUrl;
10870
+
10871
+ if (!url) { toast('Enter a server URL first', 'warn'); return; }
10731
10872
 
10732
- // \u2500\u2500 Rest of settings \u2500\u2500
10733
- const restEl = $('#settings-rest');
10734
- if (!restEl) return;
10735
- let out = '';
10873
+ if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner" style="width:14px;height:14px;border-width:2px;margin:0"></span> Testing...'; }
10874
+ 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>');
10736
10875
 
10737
- // Connection section
10738
- if (statusRes.error) {
10739
- out += errorBlock('Connection Status Unavailable', statusRes.error);
10876
+ const r = await api('/settings/test-connection', {
10877
+ method: 'POST',
10878
+ body: JSON.stringify({ serverUrl: url, timeout: 10000 }),
10879
+ });
10880
+
10881
+ if (btn) { btn.disabled = false; btn.innerHTML = '\u{1F50D} Test'; }
10882
+
10883
+ if (r.success) {
10884
+ 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>';
10885
+ if (resultEl) html(resultEl, \\\`
10886
+ <div style="display:flex;align-items:center;gap:10px;font-size:12px;color:var(--ok)">
10887
+ <span class="dot dot-ok"></span> Connected successfully \${latencyBadge}
10888
+ \${r.serverInfo?.version ? '<span class="tag">v' + escHtml(r.serverInfo.version) + '</span>' : ''}
10889
+ </div>
10890
+ \\\`);
10891
+ toast('Connection OK! Latency: ' + r.latency + 'ms', 'ok');
10740
10892
  } else {
10741
- const s = statusRes.data;
10742
- out += \\\`
10743
- <div class="card">
10744
- <div class="card-header"><div class="card-title">\u{1F50C} Connection Settings</div></div>
10745
- <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>
10746
- <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>
10893
+ const cls = r.status === 'timeout' ? 'warn' : 'danger';
10894
+ const icon = r.status === 'timeout' ? '\u23F1\uFE0F' : '\u274C';
10895
+ if (resultEl) html(resultEl, \\\`
10896
+ <div style="font-size:12px;color:var(--\${cls});display:flex;align-items:center;gap:8px">
10897
+ \${icon} \${escHtml(r.message || 'Connection failed')}
10898
+ <span class="badge badge-ghost">\${r.latency}ms</span>
10747
10899
  </div>
10748
- \\\`;
10900
+ \\\`);
10901
+ toast(r.message || 'Connection failed', 'err');
10749
10902
  }
10903
+ }
10750
10904
 
10751
- // Identity section
10752
- if (identityRes.error) {
10753
- out += errorBlock('Agent Identity Unavailable', identityRes.error);
10754
- } else {
10755
- const i = identityRes.data;
10756
- out += \\\`
10757
- <div class="card">
10758
- <div class="card-header"><div class="card-title">\u{1F916} Agent Identity</div></div>
10759
- <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>
10760
- <div class="detail-row"><div class="detail-key">Public Key Fingerprint</div><div class="detail-val mono">\${escHtml(i.publicKeyFingerprint)}</div></div>
10905
+ // \u2500\u2500 FRIENDS TAB \u2500\u2500
10906
+ function renderSettingsFriends() {
10907
+ const s = _settingsData;
10908
+ const el = $('#settings-content');
10909
+
10910
+ html(el, \\\`
10911
+ <p class="section-desc">Configure friend management, permissions, and temporary number settings.</p>
10912
+
10913
+ <div class="stats-grid" style="margin-bottom:20px">
10914
+ <div class="stat-card">
10915
+ <div class="stat-icon" style="background:var(--ok-bg)">\u{1F465}</div>
10916
+ <div class="stat-label">Friends</div>
10917
+ <div class="stat-value">\${s.friendCount || 0}</div>
10918
+ <div class="stat-sub">of \${s.maxFriends || 200} max</div>
10761
10919
  </div>
10762
- \\\`;
10763
- }
10920
+ <div class="stat-card">
10921
+ <div class="stat-icon" style="background:var(--info-bg)">\u{1F517}</div>
10922
+ <div class="stat-label">Sessions</div>
10923
+ <div class="stat-value">\${s.sessionCount || 0}</div>
10924
+ <div class="stat-sub">Encrypted sessions</div>
10925
+ </div>
10926
+ </div>
10764
10927
 
10765
- // Config file section
10766
- if (configRes.error) {
10767
- out += errorBlock('Config File Not Found', configRes.error);
10768
- } else {
10769
- const c = configRes.data;
10770
- out += \\\`
10771
- <div class="card">
10772
- <div class="card-header"><div class="card-title">\u{1F4C1} Config File</div></div>
10773
- <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>
10774
- <div class="detail-row"><div class="detail-key">Config Size</div><div class="detail-val">\${c.configSize || 0} bytes</div></div>
10928
+ <div class="card">
10929
+ <div class="card-header"><div class="card-title">\u{1F465} Friend Limits & Permissions</div></div>
10930
+ <div class="form-row">
10931
+ <div class="form-group">
10932
+ <label>Max Friends</label>
10933
+ <input type="number" id="set-max-friends" value="\${s.maxFriends || 200}" min="1" max="10000" placeholder="200">
10934
+ <div class="hint">Maximum number of encrypted friend connections (1\u201310,000).</div>
10935
+ </div>
10936
+ <div class="form-group">
10937
+ <label>Auto-Accept Friends</label>
10938
+ <div style="display:flex;align-items:center;gap:10px;margin-top:6px">
10939
+ <label class="toggle-label">
10940
+ <input type="checkbox" id="set-auto-accept" \${s.autoAcceptFriends ? 'checked' : ''}>
10941
+ <span class="toggle-slider"></span>
10942
+ <span>Automatically accept requests</span>
10943
+ </label>
10944
+ </div>
10945
+ <div class="hint">When enabled, incoming friend requests are accepted without review.</div>
10946
+ </div>
10775
10947
  </div>
10776
- \\\`;
10777
- }
10948
+ <div class="form-group">
10949
+ <label>Default Permissions for New Friends</label>
10950
+ <div style="display:flex;gap:16px;margin-top:6px;flex-wrap:wrap">
10951
+ <label class="toggle-label">
10952
+ <input type="checkbox" id="set-perm-chat" \${(s.defaultPermissions || []).includes('chat') ? 'checked' : ''}>
10953
+ <span class="toggle-slider"></span>
10954
+ <span>\u{1F4AC} Chat</span>
10955
+ </label>
10956
+ <label class="toggle-label">
10957
+ <input type="checkbox" id="set-perm-exec" \${(s.defaultPermissions || []).includes('exec') ? 'checked' : ''}>
10958
+ <span class="toggle-slider"></span>
10959
+ <span>\u{1F527} Exec</span>
10960
+ </label>
10961
+ </div>
10962
+ <div class="hint">Default permissions applied when auto-accepting new friend requests.</div>
10963
+ </div>
10964
+ <div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
10965
+ \${sectionSaveBtn('friends', 'friends')}
10966
+ </div>
10967
+ </div>
10778
10968
 
10779
- // Statistics section
10780
- if (!statusRes.error) {
10781
- const s = statusRes.data;
10782
- out += \\\`
10783
- <div class="card">
10784
- <div class="card-header"><div class="card-title">\u{1F4CA} Statistics</div></div>
10785
- <div class="detail-row"><div class="detail-key">Friends Count</div><div class="detail-val">\${s.friendCount || 0}</div></div>
10786
- <div class="detail-row"><div class="detail-key">Active Sessions</div><div class="detail-val">\${s.sessionCount || 0}</div></div>
10787
- <div class="detail-row"><div class="detail-key">Plugin Version</div><div class="detail-val">\${escHtml(pluginRes.data?.version || 'unknown')}</div></div>
10969
+ <div class="card">
10970
+ <div class="card-header"><div class="card-title">\u{1F522} Temporary Numbers</div></div>
10971
+ <div class="form-group">
10972
+ <label>Temp Number Expiry (seconds)</label>
10973
+ <input type="number" id="set-temp-expiry" value="\${s.tempNumberExpiry || 300}" min="60" max="3600" placeholder="300">
10974
+ <div class="hint">How long a temporary friend number remains valid (60\u20133600s). Default: 5 minutes.</div>
10975
+ </div>
10976
+ <div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
10977
+ \${sectionSaveBtn('temp', 'temp')}
10978
+ </div>
10979
+ </div>
10980
+ \\\`);
10981
+ }
10982
+
10983
+ // \u2500\u2500 SECURITY TAB \u2500\u2500
10984
+ function renderSettingsSecurity() {
10985
+ const s = _settingsData;
10986
+ const el = $('#settings-content');
10987
+
10988
+ html(el, \\\`
10989
+ <p class="section-desc">Configure encryption, P2P, and identity security settings.</p>
10990
+
10991
+ <div class="card">
10992
+ <div class="card-header"><div class="card-title">\u{1F916} Agent Identity</div></div>
10993
+ <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>
10994
+ <div class="detail-row"><div class="detail-key">Public Key Fingerprint</div><div class="detail-val mono">\${escHtml(s.publicKeyFingerprint || '\u2014')}</div></div>
10995
+ <div style="padding-top:12px;display:flex;gap:8px">
10996
+ <button class="btn btn-danger btn-sm" onclick="showResetIdentityModal()">\u{1F5D1}\uFE0F Reset Identity</button>
10997
+ <span style="font-size:12px;color:var(--text3);display:flex;align-items:center">\u26A0\uFE0F This deletes all friends, sessions, and keys permanently</span>
10998
+ </div>
10999
+ </div>
11000
+
11001
+ <div class="card">
11002
+ <div class="card-header"><div class="card-title">\u{1F512} P2P & Encryption</div></div>
11003
+ <div class="form-row">
11004
+ <div class="form-group">
11005
+ <label>Enable P2P Connections</label>
11006
+ <div style="display:flex;align-items:center;gap:10px;margin-top:6px">
11007
+ <label class="toggle-label">
11008
+ <input type="checkbox" id="set-enable-p2p" \${s.enableP2P ? 'checked' : ''}>
11009
+ <span class="toggle-slider"></span>
11010
+ <span>Allow direct P2P messaging</span>
11011
+ </label>
11012
+ </div>
11013
+ <div class="hint">Enable peer-to-peer encrypted connections when both parties are online.</div>
11014
+ </div>
11015
+ <div class="form-group">
11016
+ <label>Handshake Timeout (seconds)</label>
11017
+ <input type="number" id="set-handshake-timeout" value="\${s.handshakeTimeout || 60}" min="10" max="300" placeholder="60">
11018
+ <div class="hint">Noise-XK handshake timeout (10\u2013300s). Default: 60s.</div>
11019
+ </div>
11020
+ </div>
11021
+ <div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
11022
+ \${sectionSaveBtn('security', 'sec')}
11023
+ </div>
11024
+ </div>
11025
+ \\\`);
11026
+ }
11027
+
11028
+ // \u2500\u2500 ADVANCED TAB \u2500\u2500
11029
+ function renderSettingsAdvanced() {
11030
+ const s = _settingsData;
11031
+ const el = $('#settings-content');
11032
+
11033
+ html(el, \\\`
11034
+ <p class="section-desc">Advanced settings for file transfer, logging, and configuration management.</p>
11035
+
11036
+ <div class="card">
11037
+ <div class="card-header"><div class="card-title">\u{1F4CE} File Transfer</div></div>
11038
+ <div class="form-row">
11039
+ <div class="form-group">
11040
+ <label>Enable File Transfer</label>
11041
+ <div style="display:flex;align-items:center;gap:10px;margin-top:6px">
11042
+ <label class="toggle-label">
11043
+ <input type="checkbox" id="set-enable-ft" \${s.enableFileTransfer ? 'checked' : ''}>
11044
+ <span class="toggle-slider"></span>
11045
+ <span>Allow file transfers</span>
11046
+ </label>
11047
+ </div>
11048
+ <div class="hint">Enable encrypted file transfer between friends.</div>
11049
+ </div>
11050
+ <div class="form-group">
11051
+ <label>Max File Size</label>
11052
+ <select id="set-max-file-size">
11053
+ <option value="10485760" \${s.maxFileSize <= 10485760 ? 'selected' : ''}>10 MB</option>
11054
+ <option value="52428800" \${s.maxFileSize > 10485760 && s.maxFileSize <= 52428800 ? 'selected' : ''}>50 MB</option>
11055
+ <option value="104857600" \${s.maxFileSize > 52428800 && s.maxFileSize <= 104857600 ? 'selected' : ''}>100 MB</option>
11056
+ <option value="524288000" \${s.maxFileSize > 104857600 && s.maxFileSize <= 524288000 ? 'selected' : ''}>500 MB</option>
11057
+ <option value="1073741824" \${s.maxFileSize > 524288000 ? 'selected' : ''}>1 GB</option>
11058
+ </select>
11059
+ <div class="hint">Maximum file size for encrypted transfers. Current: \${formatBytes(s.maxFileSize)}.</div>
11060
+ </div>
11061
+ </div>
11062
+ <div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
11063
+ \${sectionSaveBtn('filetransfer', 'ft')}
11064
+ </div>
11065
+ </div>
11066
+
11067
+ <div class="card">
11068
+ <div class="card-header"><div class="card-title">\u{1F4CB} Logging</div></div>
11069
+ <div class="form-group">
11070
+ <label>Log Level</label>
11071
+ <select id="set-log-level" style="max-width:300px">
11072
+ <option value="debug" \${s.logLevel === 'debug' ? 'selected' : ''}>\u{1F41B} Debug \u2014 Verbose output for troubleshooting</option>
11073
+ <option value="info" \${s.logLevel === 'info' ? 'selected' : ''}>\u2139\uFE0F Info \u2014 General information (default)</option>
11074
+ <option value="warn" \${s.logLevel === 'warn' ? 'selected' : ''}>\u26A0\uFE0F Warn \u2014 Warnings and important events</option>
11075
+ <option value="error" \${s.logLevel === 'error' ? 'selected' : ''}>\u274C Error \u2014 Errors only</option>
11076
+ <option value="none" \${s.logLevel === 'none' ? 'selected' : ''}>\u{1F507} None \u2014 Disable all logging</option>
11077
+ </select>
11078
+ <div class="hint">Controls the verbosity of plugin log output.</div>
11079
+ </div>
11080
+ <div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
11081
+ \${sectionSaveBtn('logging', 'log')}
11082
+ </div>
11083
+ </div>
11084
+
11085
+ <div class="card">
11086
+ <div class="card-header"><div class="card-title">\u{1F4E6} Import / Export Settings</div></div>
11087
+ <div style="display:flex;gap:10px;flex-wrap:wrap">
11088
+ <button class="btn btn-default btn-sm" onclick="exportSettings()">\u{1F4E5} Export Settings</button>
11089
+ <button class="btn btn-ok btn-sm" onclick="showImportSettingsModal()">\u{1F4E4} Import Settings</button>
10788
11090
  </div>
10789
- \\\`;
11091
+ <div class="hint" style="margin-top:10px">Export current AICQ plugin settings as JSON. Import to restore settings from a backup.</div>
11092
+ </div>
11093
+ \\\`);
11094
+ }
11095
+
11096
+ // \u2500\u2500 Section Save (AJAX) \u2500\u2500
11097
+ async function saveSettingsSection(section, id) {
11098
+ const btn = $('#btn-save-' + id);
11099
+ const statusEl = $('#status-' + id);
11100
+ if (btn) { btn.disabled = true; btn.textContent = 'Saving...'; }
11101
+ if (statusEl) { statusEl.textContent = ''; statusEl.style.color = 'var(--text3)'; }
11102
+
11103
+ let data = {};
11104
+ switch (section) {
11105
+ case 'connection':
11106
+ data = {
11107
+ serverUrl: $('#set-server-url')?.value?.trim(),
11108
+ connectionTimeout: parseInt($('#set-connection-timeout')?.value, 10),
11109
+ wsAutoReconnect: $('#set-ws-auto-reconnect')?.checked ?? true,
11110
+ wsReconnectInterval: parseInt($('#set-ws-reconnect-interval')?.value, 10),
11111
+ };
11112
+ break;
11113
+ case 'friends':
11114
+ data = {
11115
+ maxFriends: parseInt($('#set-max-friends')?.value, 10),
11116
+ autoAcceptFriends: $('#set-auto-accept')?.checked ?? false,
11117
+ defaultPermissions: [
11118
+ ...(($('#set-perm-chat')?.checked) ? ['chat'] : []),
11119
+ ...(($('#set-perm-exec')?.checked) ? ['exec'] : []),
11120
+ ],
11121
+ };
11122
+ break;
11123
+ case 'temp':
11124
+ data = { tempNumberExpiry: parseInt($('#set-temp-expiry')?.value, 10) };
11125
+ break;
11126
+ case 'security':
11127
+ data = {
11128
+ enableP2P: $('#set-enable-p2p')?.checked ?? true,
11129
+ handshakeTimeout: parseInt($('#set-handshake-timeout')?.value, 10),
11130
+ };
11131
+ break;
11132
+ case 'filetransfer':
11133
+ data = {
11134
+ enableFileTransfer: $('#set-enable-ft')?.checked ?? true,
11135
+ maxFileSize: parseInt($('#set-max-file-size')?.value, 10),
11136
+ };
11137
+ break;
11138
+ case 'logging':
11139
+ data = { logLevel: $('#set-log-level')?.value || 'info' };
11140
+ break;
10790
11141
  }
10791
11142
 
10792
- restEl.innerHTML = out;
11143
+ const r = await api('/settings/section', {
11144
+ method: 'POST',
11145
+ body: JSON.stringify({ section, data }),
11146
+ });
11147
+
11148
+ if (btn) { btn.disabled = false; btn.textContent = '\u{1F4BE} Save'; }
11149
+
11150
+ if (r.success) {
11151
+ toast('Settings saved: ' + section, 'ok');
11152
+ if (statusEl) { statusEl.textContent = '\u2713 Saved'; statusEl.style.color = 'var(--ok)'; }
11153
+ // Refresh settings data
11154
+ const fresh = await api('/settings');
11155
+ if (fresh && !fresh.error) { _settingsData = fresh; }
11156
+ } else {
11157
+ toast(r.message || r.error || 'Save failed', 'err');
11158
+ if (statusEl) { statusEl.textContent = '\u2717 ' + (r.message || 'Failed'); statusEl.style.color = 'var(--danger)'; }
11159
+ }
10793
11160
  }
10794
11161
 
10795
- async function checkForUpdates() {
10796
- const btn = $('#btn-check-update');
10797
- if (btn) { btn.disabled = true; btn.textContent = '\u{1F50D} Checking...'; }
10798
- toast('Checking for updates...', 'info');
10799
- const data = await api('/update/check', { _timeout: 15000 });
10800
- if (data.error) { toast('Check failed: ' + data.error, 'err'); }
10801
- else if (data.updateAvailable) { toast('New version available: v' + data.latestVersion, 'ok'); }
10802
- else { toast('Plugin is up to date (v' + data.currentVersion + ')', 'ok'); }
10803
- if (btn) { btn.disabled = false; btn.textContent = '\u{1F50D} Check for Updates'; }
10804
- loadSettings();
11162
+ // \u2500\u2500 Full Save All (legacy support) \u2500\u2500
11163
+ async function saveSettings() {
11164
+ if (_settingsSaving) return;
11165
+ _settingsSaving = true;
11166
+
11167
+ const allData = {
11168
+ serverUrl: $('#set-server-url')?.value?.trim(),
11169
+ maxFriends: parseInt($('#set-max-friends')?.value, 10),
11170
+ autoAcceptFriends: $('#set-auto-accept')?.checked ?? false,
11171
+ };
11172
+
11173
+ const r = await api('/settings', { method: 'PUT', body: JSON.stringify(allData) });
11174
+ _settingsSaving = false;
11175
+
11176
+ if (r.success) { toast('All settings saved!', 'ok'); setTimeout(() => loadSettings(), 800); }
11177
+ else { toast(r.message || r.error || 'Save failed', 'err'); }
11178
+ }
11179
+
11180
+ // \u2500\u2500 Reset Identity \u2500\u2500
11181
+ function showResetIdentityModal() {
11182
+ $('#reset-confirm-input').value = '';
11183
+ $('#reset-confirm-btn').disabled = true;
11184
+ $('#reset-confirm-btn').textContent = '\u{1F5D1}\uFE0F Delete Everything';
11185
+ showModal('modal-reset-identity');
11186
+ setTimeout(() => $('#reset-confirm-input')?.focus(), 100);
10805
11187
  }
10806
11188
 
10807
- async function doUpdate(force) {
10808
- if (_updating) { toast('Update already in progress...', 'warn'); return; }
10809
- const label = force ? 'Force reinstall' : 'Incremental update';
10810
- if (!confirm(label + ' aicq-openclaw-plugin? This will download and install the latest version from npm.')) return;
10811
-
10812
- _updating = true;
10813
- const btnDo = $('#btn-do-update');
10814
- const btnForce = $('#btn-force-update');
10815
- if (btnDo) btnDo.disabled = true;
10816
- if (btnForce) btnForce.disabled = true;
10817
-
10818
- const outputEl = $('#update-output');
10819
- const logEl = $('#update-log');
10820
- if (outputEl) outputEl.style.display = 'block';
10821
- if (logEl) logEl.textContent = 'Starting ' + label.toLowerCase() + '...
10822
- ';
10823
- toast(label + ' starting...', 'info');
10824
-
10825
- const r = await api('/update', {
11189
+ function checkResetConfirm() {
11190
+ const v = $('#reset-confirm-input')?.value?.trim();
11191
+ const btn = $('#reset-confirm-btn');
11192
+ if (btn) { btn.disabled = (v !== 'RESET'); btn.textContent = v === 'RESET' ? '\u{1F5D1}\uFE0F Confirm Delete' : '\u{1F5D1}\uFE0F Delete Everything'; }
11193
+ }
11194
+
11195
+ async function executeResetIdentity() {
11196
+ const btn = $('#reset-confirm-btn');
11197
+ if (btn) { btn.disabled = true; btn.textContent = 'Resetting...'; }
11198
+
11199
+ const r = await api('/settings/reset-identity', {
10826
11200
  method: 'POST',
10827
- body: JSON.stringify({ force }),
10828
- _timeout: 120000,
11201
+ body: JSON.stringify({ confirm: true }),
10829
11202
  });
10830
11203
 
10831
- if (logEl) logEl.textContent = r.output || r.message || (r.success ? 'Done!' : 'Failed.');
11204
+ if (btn) { btn.disabled = false; btn.textContent = '\u{1F5D1}\uFE0F Delete Everything'; }
11205
+
10832
11206
  if (r.success) {
10833
- toast('Update successful! Restart the service to apply.', 'ok');
10834
- if (logEl) logEl.textContent += '
11207
+ toast('Identity reset successfully. Please restart the plugin.', 'ok');
11208
+ hideModal('modal-reset-identity');
11209
+ // Reload settings to reflect cleared state
11210
+ setTimeout(() => loadSettings(), 1000);
11211
+ } else {
11212
+ toast(r.message || r.error || 'Reset failed', 'err');
11213
+ }
11214
+ }
11215
+
11216
+ // \u2500\u2500 Export / Import \u2500\u2500
11217
+ async function exportSettings() {
11218
+ const r = await api('/settings/export');
11219
+ if (r.error) { toast(r.error, 'err'); return; }
11220
+
11221
+ const json = JSON.stringify(r.settings || r, null, 2);
11222
+ const blob = new Blob([json], { type: 'application/json' });
11223
+ const url = URL.createObjectURL(blob);
11224
+ const a = document.createElement('a');
11225
+ a.href = url;
11226
+ a.download = 'aicq-settings-' + new Date().toISOString().slice(0, 10) + '.json';
11227
+ a.click();
11228
+ URL.revokeObjectURL(url);
11229
+ toast('Settings exported successfully', 'ok');
11230
+ }
11231
+
11232
+ function showImportSettingsModal() {
11233
+ $('#import-json-input').value = '';
11234
+ showModal('modal-import-settings');
11235
+ setTimeout(() => $('#import-json-input')?.focus(), 100);
11236
+ }
11237
+
11238
+ async function executeImportSettings() {
11239
+ const raw = $('#import-json-input')?.value?.trim();
11240
+ if (!raw) { toast('Paste JSON settings first', 'warn'); return; }
10835
11241
 
10836
- \u2705 ' + (r.message || 'Done! Please restart the service.');
11242
+ let settings;
11243
+ try { settings = JSON.parse(raw); } catch (e) { toast('Invalid JSON: ' + e.message, 'err'); return; }
11244
+
11245
+ const btn = $('#import-confirm-btn');
11246
+ if (btn) { btn.disabled = true; btn.textContent = 'Importing...'; }
11247
+
11248
+ const r = await api('/settings/import', {
11249
+ method: 'POST',
11250
+ body: JSON.stringify({ settings, merge: true }),
11251
+ });
11252
+
11253
+ if (btn) { btn.disabled = false; btn.textContent = '\u{1F4E4} Import'; }
11254
+
11255
+ if (r.success) {
11256
+ toast('Settings imported successfully!', 'ok');
11257
+ hideModal('modal-import-settings');
11258
+ setTimeout(() => loadSettings(), 800);
10837
11259
  } else {
10838
- toast('Update failed: ' + (r.message || 'Unknown error'), 'err');
10839
- if (logEl) logEl.textContent += '
11260
+ toast(r.message || r.error || 'Import failed', 'err');
11261
+ }
11262
+ }
11263
+
11264
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
11265
+ // JSON Config Editor
11266
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
11267
+ let _jsonEditorConfigFile = '';
11268
+
11269
+ async function renderSettingsJsonEditor() {
11270
+ const el = $('#settings-content');
11271
+ html(el, '<div class="loading-mask"><div class="spinner"></div>Loading config...</div>');
11272
+
11273
+ const queryParams = _jsonEditorConfigFile ? '?file=' + encodeURIComponent(_jsonEditorConfigFile) : '';
11274
+ const data = await api('/config-file/raw' + queryParams);
11275
+ if (data.error) {
11276
+ html(el, '<div class="empty"><div class="icon">\u26A0\uFE0F</div><p>' + escHtml(data.error) + '</p></div>');
11277
+ return;
11278
+ }
11279
+
11280
+ _jsonEditorConfigFile = data.fileName || '';
11281
+ const hasMultipleFiles = data.availableFiles && data.availableFiles.length > 1;
11282
+ let fileSelectorHtml = '';
11283
+ if (hasMultipleFiles) {
11284
+ const options = data.availableFiles.map(f =>
11285
+ '<option value="' + escHtml(f) + '"' + (f === data.fileName ? ' selected' : '') + '>' + escHtml(f) + '</option>'
11286
+ ).join('');
11287
+ fileSelectorHtml = \\\`
11288
+ <div class="form-group" style="margin-bottom:16px">
11289
+ <label>\u{1F4C4} Config File</label>
11290
+ <select id="json-editor-file-select" onchange="_jsonEditorConfigFile=this.value;renderSettingsJsonEditor()" style="max-width:300px">
11291
+ \${options}
11292
+ </select>
11293
+ </div>\\\`;
11294
+ }
11295
+
11296
+ html(el, \\\` <p class="section-desc">
11297
+ Edit the raw JSON configuration directly. Be careful with syntax \u2014 invalid JSON will be rejected.
11298
+ <span class="badge badge-accent" style="margin-left:8px">\u{1F4C4} \${escHtml(data.fileName)}</span>
11299
+ </p>
11300
+
11301
+ <div class="card">
11302
+ <div class="card-header">
11303
+ <div class="card-title">\u{1F4DD} Config JSON Editor</div>
11304
+ <div style="display:flex;gap:8px;align-items:center">
11305
+ <span class="mono" style="font-size:11px;color:var(--text3)">\${escHtml(data.filePath)}</span>
11306
+ <button class="btn btn-sm btn-default" onclick="renderSettingsJsonEditor()">\u{1F504} Reload</button>
11307
+ </div>
11308
+ </div>
11309
+ \${fileSelectorHtml}
11310
+ <div class="form-group">
11311
+ <label>Raw JSON Configuration</label>
11312
+ <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>
11313
+ <div class="hint">Directly edit the configuration JSON. Use the Format button to prettify.</div>
11314
+ </div>
11315
+ <div id="json-editor-status" style="margin-bottom:12px;font-size:12px"></div>
11316
+ <div class="form-actions" style="justify-content:space-between">
11317
+ <div style="display:flex;gap:8px">
11318
+ <button class="btn btn-sm btn-default" onclick="formatJsonEditor()">\u{1F4D0} Format</button>
11319
+ <button class="btn btn-sm btn-default" onclick="copyText($('#json-editor')?.value || '')">\u{1F4CB} Copy</button>
11320
+ </div>
11321
+ <div style="display:flex;gap:8px">
11322
+ <button class="btn btn-sm btn-default" onclick="renderSettingsJsonEditor()">\u21A9\uFE0F Revert</button>
11323
+ <button class="btn btn-sm btn-primary" id="btn-save-json" onclick="saveJsonConfig()">\u{1F4BE} Save Config</button>
11324
+ </div>
11325
+ </div>
11326
+ </div>
11327
+ \`);
11328
+ }
11329
+
11330
+ function formatJsonEditor() {
11331
+ const ta = $('#json-editor');
11332
+ if (!ta) return;
11333
+ try {
11334
+ const obj = JSON.parse(ta.value);
11335
+ ta.value = JSON.stringify(obj, null, 2);
11336
+ toast('JSON formatted', 'ok');
11337
+ $('#json-editor-status').innerHTML = '<span style="color:var(--ok)">\u2713 Valid JSON</span>';
11338
+ } catch (e) {
11339
+ toast('Invalid JSON: ' + e.message, 'err');
11340
+ $('#json-editor-status').innerHTML = '<span style="color:var(--danger)">\u2717 ' + escHtml(e.message) + '</span>';
11341
+ }
11342
+ }
10840
11343
 
10841
- \u274C ' + (r.message || 'Failed.');
11344
+ async function saveJsonConfig() {
11345
+ const btn = $('#btn-save-json');
11346
+ const statusEl = $('#json-editor-status');
11347
+ const raw = $('#json-editor')?.value;
11348
+ if (!raw) { toast('No content to save', 'warn'); return; }
11349
+
11350
+ // Validate first
11351
+ try { JSON.parse(raw); } catch (e) {
11352
+ toast('Invalid JSON: ' + e.message, 'err');
11353
+ if (statusEl) statusEl.innerHTML = '<span style="color:var(--danger)">\u2717 ' + escHtml(e.message) + '</span>';
11354
+ return;
10842
11355
  }
10843
11356
 
10844
- _updating = false;
10845
- if (btnDo) btnDo.disabled = false;
10846
- if (btnForce) btnForce.disabled = false;
10847
- loadSettings();
11357
+ if (btn) { btn.disabled = true; btn.textContent = 'Saving...'; }
11358
+ 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>';
11359
+
11360
+ const queryParams = _jsonEditorConfigFile ? '?file=' + encodeURIComponent(_jsonEditorConfigFile) : '';
11361
+ const r = await api('/config-file/raw' + queryParams, { method: 'PUT', body: JSON.stringify({ content: raw }) });
11362
+
11363
+ if (btn) { btn.disabled = false; btn.textContent = '\u{1F4BE} Save Config'; }
11364
+
11365
+ if (r.success) {
11366
+ toast('Config saved successfully!', 'ok');
11367
+ if (statusEl) statusEl.innerHTML = '<span style="color:var(--ok)">\u2713 Saved at ' + new Date().toLocaleTimeString() + '</span>';
11368
+ } else {
11369
+ toast(r.message || 'Save failed', 'err');
11370
+ if (statusEl) statusEl.innerHTML = '<span style="color:var(--danger)">\u2717 ' + escHtml(r.message || 'Failed') + '</span>';
11371
+ }
10848
11372
  }
10849
11373
 
10850
11374
  // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
@@ -10925,7 +11449,7 @@ var HTML = `<!DOCTYPE html>
10925
11449
  <div class="main-content">
10926
11450
 
10927
11451
  <!-- Dashboard -->
10928
- <div class="page active" id="page-dashboard"><div id="dashboard-content"></div></div>
11452
+ <div class="page active" id="page-dashboard"><div id="dashboard-content"><div class="loading-mask"><div class="spinner"></div>Loading...</div></div></div>
10929
11453
 
10930
11454
  <!-- Agents -->
10931
11455
  <div class="page" id="page-agents"><div id="agents-content"></div></div>
@@ -10940,7 +11464,10 @@ var HTML = `<!DOCTYPE html>
10940
11464
  <div class="page" id="page-models"><div id="models-content"></div></div>
10941
11465
 
10942
11466
  <!-- Settings -->
10943
- <div class="page" id="page-settings"><div id="settings-content"></div></div>
11467
+ <div class="page" id="page-settings">
11468
+ <div id="settings-tabs" style="display:flex;gap:6px;margin-bottom:16px;flex-wrap:wrap"></div>
11469
+ <div id="settings-content"></div>
11470
+ </div>
10944
11471
 
10945
11472
  </div>
10946
11473
  </main>
@@ -11016,9 +11543,118 @@ var HTML = `<!DOCTYPE html>
11016
11543
  </div>
11017
11544
  </div>
11018
11545
 
11546
+ <!-- Modal: Reset Identity -->
11547
+ <div class="modal-overlay hidden" id="modal-reset-identity" onclick="if(event.target===this)hideModal('modal-reset-identity')">
11548
+ <div class="modal">
11549
+ <div class="modal-header"><h3>\u{1F5D1}\uFE0F Reset Agent Identity</h3><button class="modal-close" onclick="hideModal('modal-reset-identity')">\u2715</button></div>
11550
+ <div style="margin-bottom:16px">
11551
+ <div class="card" style="border-color:var(--danger);background:var(--danger-bg)">
11552
+ <p style="font-size:13px;color:#fca5a5;line-height:1.6">
11553
+ <strong>\u26A0\uFE0F WARNING: This is a destructive operation!</strong><br><br>
11554
+ This will permanently delete:<br>
11555
+ \u2022 Your Ed25519 key pair and agent ID<br>
11556
+ \u2022 All friend connections and sessions<br>
11557
+ \u2022 All pending friend requests<br>
11558
+ \u2022 All temporary numbers<br><br>
11559
+ After reset, you must restart the plugin to generate a new identity.
11560
+ </p>
11561
+ </div>
11562
+ </div>
11563
+ <div class="form-group">
11564
+ <label>Type RESET to confirm</label>
11565
+ <input id="reset-confirm-input" type="text" placeholder="RESET" oninput="checkResetConfirm()" autocomplete="off" style="border-color:var(--danger)">
11566
+ </div>
11567
+ <div class="form-actions">
11568
+ <button class="btn btn-default" onclick="hideModal('modal-reset-identity')">Cancel</button>
11569
+ <button class="btn btn-danger" id="reset-confirm-btn" onclick="executeResetIdentity()" disabled>\u{1F5D1}\uFE0F Delete Everything</button>
11570
+ </div>
11571
+ </div>
11572
+ </div>
11573
+
11574
+ <!-- Modal: Import Settings -->
11575
+ <div class="modal-overlay hidden" id="modal-import-settings" onclick="if(event.target===this)hideModal('modal-import-settings')">
11576
+ <div class="modal" style="max-width:580px">
11577
+ <div class="modal-header"><h3>\u{1F4E4} Import Settings</h3><button class="modal-close" onclick="hideModal('modal-import-settings')">\u2715</button></div>
11578
+ <div class="form-group">
11579
+ <label>Paste JSON Settings</label>
11580
+ <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>
11581
+ <div class="hint">Paste the JSON settings exported from another AICQ instance. Settings will be merged with existing values.</div>
11582
+ </div>
11583
+ <div class="form-actions">
11584
+ <button class="btn btn-default" onclick="hideModal('modal-import-settings')">Cancel</button>
11585
+ <button class="btn btn-primary" id="import-confirm-btn" onclick="executeImportSettings()">\u{1F4E4} Import</button>
11586
+ </div>
11587
+ </div>
11588
+ </div>
11589
+
11019
11590
  <!-- Toast Container -->
11020
11591
  <div id="toast-container" class="toast-container"></div>
11021
11592
 
11593
+ <!-- Modal: Add/Edit Agent -->
11594
+ <div class="modal-overlay hidden" id="modal-add-agent" onclick="if(event.target===this)hideModal('modal-add-agent')">
11595
+ <div class="modal">
11596
+ <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>
11597
+ <div class="form-group">
11598
+ <label>Agent Name *</label>
11599
+ <input type="text" id="agent-form-name" placeholder="e.g. My Assistant">
11600
+ </div>
11601
+ <div class="form-group">
11602
+ <label>Agent ID</label>
11603
+ <input type="text" id="agent-form-id" placeholder="auto-generated if empty">
11604
+ <div class="hint">Unique identifier. Leave empty for auto-generation.</div>
11605
+ </div>
11606
+ <div class="form-row">
11607
+ <div class="form-group">
11608
+ <label>Model</label>
11609
+ <input type="text" id="agent-form-model" placeholder="gpt-4o">
11610
+ </div>
11611
+ <div class="form-group">
11612
+ <label>Provider</label>
11613
+ <input type="text" id="agent-form-provider" placeholder="openai">
11614
+ </div>
11615
+ </div>
11616
+ <div class="form-group">
11617
+ <label>System Prompt</label>
11618
+ <textarea id="agent-form-prompt" rows="4" placeholder="You are a helpful assistant..."></textarea>
11619
+ </div>
11620
+ <div class="form-row">
11621
+ <div class="form-group">
11622
+ <label>Temperature</label>
11623
+ <input type="number" id="agent-form-temperature" min="0" max="2" step="0.1" value="0.7">
11624
+ <div class="hint">0 = deterministic, 2 = creative. Default: 0.7</div>
11625
+ </div>
11626
+ <div class="form-group">
11627
+ <label>Max Tokens</label>
11628
+ <input type="number" id="agent-form-max-tokens" min="1" step="1" value="4096">
11629
+ <div class="hint">Maximum response length. Default: 4096</div>
11630
+ </div>
11631
+ </div>
11632
+ <div class="form-row">
11633
+ <div class="form-group">
11634
+ <label>Top P</label>
11635
+ <input type="number" id="agent-form-top-p" min="0" max="1" step="0.05" value="1">
11636
+ <div class="hint">Nucleus sampling. Default: 1</div>
11637
+ </div>
11638
+ <div class="form-group">
11639
+ <label>Tools</label>
11640
+ <input type="text" id="agent-form-tools" placeholder="web_search, code_exec, ...">
11641
+ <div class="hint">Comma-separated list of tool names</div>
11642
+ </div>
11643
+ </div>
11644
+ <div class="form-group">
11645
+ <label class="toggle-label">
11646
+ <input type="checkbox" id="agent-form-enabled" checked>
11647
+ <span class="toggle-slider"></span>
11648
+ <span>Enabled</span>
11649
+ </label>
11650
+ </div>
11651
+ <div class="form-actions">
11652
+ <button class="btn btn-default" onclick="hideModal('modal-add-agent')">Cancel</button>
11653
+ <button class="btn btn-primary" onclick="saveAgent()">\u{1F4BE} Save Agent</button>
11654
+ </div>
11655
+ </div>
11656
+ </div>
11657
+
11022
11658
  <script>${JS}</script>
11023
11659
  </body>
11024
11660
  </html>`;
@@ -11030,7 +11666,6 @@ function getManagementHTML() {
11030
11666
  import * as fs5 from "fs";
11031
11667
  import * as path5 from "path";
11032
11668
  import * as os from "os";
11033
- import { execSync, exec } from "child_process";
11034
11669
  var MODEL_PROVIDERS = [
11035
11670
  { id: "openai", name: "OpenAI", description: "GPT-4o, GPT-4, GPT-3.5, o1, o3", apiKeyHint: "sk-...", modelHint: "gpt-4o", baseUrlHint: "https://api.openai.com/v1", configKey: "openai" },
11036
11671
  { id: "anthropic", name: "Anthropic", description: "Claude 4, Claude 3.5 Sonnet, Haiku, Opus", apiKeyHint: "sk-ant-...", modelHint: "claude-sonnet-4-20250514", baseUrlHint: "https://api.anthropic.com", configKey: "anthropic" },
@@ -11199,7 +11834,7 @@ async function readBody(req) {
11199
11834
  });
11200
11835
  }
11201
11836
  function createManagementHandler(ctx) {
11202
- const { store, identityService, serverClient, serverUrl, aicqAgentId, logger, html } = ctx;
11837
+ const { store, identityService, serverClient, serverUrl, aicqAgentId, logger, html, chatChannel } = ctx;
11203
11838
  return async (req, res) => {
11204
11839
  const urlPath = parseApiPath(req.url || "/");
11205
11840
  const method = (req.method || "GET").toUpperCase();
@@ -11515,146 +12150,472 @@ function createManagementHandler(ctx) {
11515
12150
  logger.info("[API] Model config saved for provider: " + providerId);
11516
12151
  return json(res, { success: true, message: "Model configuration saved for " + provider.name });
11517
12152
  }
11518
- if (apiPath === "/update/check" && method === "GET") {
12153
+ if (apiPath === "/settings" && method === "GET") {
12154
+ const result = readConfig();
12155
+ const aicqSection = result?.config?.aicq ?? {};
12156
+ const pluginsSection = result?.config?.plugins;
12157
+ const pluginSection = pluginsSection?.["aicq-chat"];
12158
+ const merged = { ...aicqSection, ...pluginSection };
12159
+ return json(res, {
12160
+ // Connection settings
12161
+ serverUrl: merged.serverUrl || serverUrl,
12162
+ wsReconnectInterval: merged.wsReconnectInterval || 60,
12163
+ wsAutoReconnect: merged.wsAutoReconnect !== false,
12164
+ connectionTimeout: merged.connectionTimeout || 30,
12165
+ // Friend settings
12166
+ maxFriends: merged.maxFriends || 200,
12167
+ autoAcceptFriends: Boolean(merged.autoAcceptFriends),
12168
+ defaultPermissions: merged.defaultPermissions || ["chat"],
12169
+ // Temp number settings
12170
+ tempNumberExpiry: merged.tempNumberExpiry || 300,
12171
+ // File transfer settings
12172
+ maxFileSize: merged.maxFileSize || 104857600,
12173
+ enableFileTransfer: merged.enableFileTransfer !== false,
12174
+ allowedFileTypes: merged.allowedFileTypes || null,
12175
+ // Logging
12176
+ logLevel: merged.logLevel || "info",
12177
+ // Security / encryption
12178
+ enableP2P: merged.enableP2P !== false,
12179
+ handshakeTimeout: merged.handshakeTimeout || 60,
12180
+ // Identity (read-only)
12181
+ agentId: aicqAgentId,
12182
+ publicKeyFingerprint: identityService.getPublicKeyFingerprint(),
12183
+ connected: serverClient.isConnected(),
12184
+ // Config file info
12185
+ configSource: result ? path5.basename(result.configPath) : "none",
12186
+ configPath: result?.configPath || null,
12187
+ // Runtime info
12188
+ friendCount: store.getFriendCount(),
12189
+ sessionCount: store.sessions.size,
12190
+ uptimeSeconds: Math.floor(process.uptime())
12191
+ });
12192
+ }
12193
+ if (apiPath === "/settings" && method === "PUT") {
12194
+ const body = await readBody(req);
12195
+ const newServerUrl = body.serverUrl;
12196
+ const newMaxFriends = body.maxFriends;
12197
+ const newAutoAccept = body.autoAcceptFriends;
12198
+ const newWsReconnectInterval = body.wsReconnectInterval;
12199
+ const newWsAutoReconnect = body.wsAutoReconnect;
12200
+ const newConnectionTimeout = body.connectionTimeout;
12201
+ const newTempNumberExpiry = body.tempNumberExpiry;
12202
+ const newMaxFileSize = body.maxFileSize;
12203
+ const newEnableFileTransfer = body.enableFileTransfer;
12204
+ const newAllowedFileTypes = body.allowedFileTypes;
12205
+ const newLogLevel = body.logLevel;
12206
+ const newEnableP2P = body.enableP2P;
12207
+ const newHandshakeTimeout = body.handshakeTimeout;
12208
+ const newDefaultPermissions = body.defaultPermissions;
12209
+ if (newServerUrl !== void 0 && typeof newServerUrl !== "string") {
12210
+ return json(res, { success: false, message: "serverUrl must be a string" }, 400);
12211
+ }
12212
+ if (newMaxFriends !== void 0 && (typeof newMaxFriends !== "number" || newMaxFriends < 1 || newMaxFriends > 1e4)) {
12213
+ return json(res, { success: false, message: "maxFriends must be a number between 1 and 10000" }, 400);
12214
+ }
12215
+ if (newAutoAccept !== void 0 && typeof newAutoAccept !== "boolean") {
12216
+ return json(res, { success: false, message: "autoAcceptFriends must be a boolean" }, 400);
12217
+ }
12218
+ if (newWsReconnectInterval !== void 0 && (typeof newWsReconnectInterval !== "number" || newWsReconnectInterval < 5 || newWsReconnectInterval > 600)) {
12219
+ return json(res, { success: false, message: "wsReconnectInterval must be between 5 and 600 seconds" }, 400);
12220
+ }
12221
+ if (newConnectionTimeout !== void 0 && (typeof newConnectionTimeout !== "number" || newConnectionTimeout < 5 || newConnectionTimeout > 120)) {
12222
+ return json(res, { success: false, message: "connectionTimeout must be between 5 and 120 seconds" }, 400);
12223
+ }
12224
+ if (newTempNumberExpiry !== void 0 && (typeof newTempNumberExpiry !== "number" || newTempNumberExpiry < 60 || newTempNumberExpiry > 3600)) {
12225
+ return json(res, { success: false, message: "tempNumberExpiry must be between 60 and 3600 seconds" }, 400);
12226
+ }
12227
+ if (newMaxFileSize !== void 0 && (typeof newMaxFileSize !== "number" || newMaxFileSize < 1024 || newMaxFileSize > 1073741824)) {
12228
+ return json(res, { success: false, message: "maxFileSize must be between 1KB and 1GB" }, 400);
12229
+ }
12230
+ if (newLogLevel !== void 0 && !["debug", "info", "warn", "error", "none"].includes(newLogLevel)) {
12231
+ return json(res, { success: false, message: "logLevel must be one of: debug, info, warn, error, none" }, 400);
12232
+ }
12233
+ if (newHandshakeTimeout !== void 0 && (typeof newHandshakeTimeout !== "number" || newHandshakeTimeout < 10 || newHandshakeTimeout > 300)) {
12234
+ return json(res, { success: false, message: "handshakeTimeout must be between 10 and 300 seconds" }, 400);
12235
+ }
12236
+ const result = readConfig();
12237
+ if (!result) {
12238
+ return json(res, { success: false, message: "No config file found. Create openclaw.json first." }, 400);
12239
+ }
12240
+ const config2 = result.config;
12241
+ if (!config2.plugins || typeof config2.plugins !== "object") {
12242
+ config2.plugins = {};
12243
+ }
12244
+ const plugins = config2.plugins;
12245
+ if (!plugins["aicq-chat"] || typeof plugins["aicq-chat"] !== "object") {
12246
+ plugins["aicq-chat"] = {};
12247
+ }
12248
+ const aicqConfig = plugins["aicq-chat"];
12249
+ if (newServerUrl !== void 0)
12250
+ aicqConfig.serverUrl = newServerUrl;
12251
+ if (newMaxFriends !== void 0)
12252
+ aicqConfig.maxFriends = newMaxFriends;
12253
+ if (newAutoAccept !== void 0)
12254
+ aicqConfig.autoAcceptFriends = newAutoAccept;
12255
+ if (newWsReconnectInterval !== void 0)
12256
+ aicqConfig.wsReconnectInterval = newWsReconnectInterval;
12257
+ if (newWsAutoReconnect !== void 0)
12258
+ aicqConfig.wsAutoReconnect = newWsAutoReconnect;
12259
+ if (newConnectionTimeout !== void 0)
12260
+ aicqConfig.connectionTimeout = newConnectionTimeout;
12261
+ if (newTempNumberExpiry !== void 0)
12262
+ aicqConfig.tempNumberExpiry = newTempNumberExpiry;
12263
+ if (newMaxFileSize !== void 0)
12264
+ aicqConfig.maxFileSize = newMaxFileSize;
12265
+ if (newEnableFileTransfer !== void 0)
12266
+ aicqConfig.enableFileTransfer = newEnableFileTransfer;
12267
+ if (newAllowedFileTypes !== void 0)
12268
+ aicqConfig.allowedFileTypes = newAllowedFileTypes;
12269
+ if (newLogLevel !== void 0)
12270
+ aicqConfig.logLevel = newLogLevel;
12271
+ if (newEnableP2P !== void 0)
12272
+ aicqConfig.enableP2P = newEnableP2P;
12273
+ if (newHandshakeTimeout !== void 0)
12274
+ aicqConfig.handshakeTimeout = newHandshakeTimeout;
12275
+ if (newDefaultPermissions !== void 0)
12276
+ aicqConfig.defaultPermissions = newDefaultPermissions;
12277
+ const written = writeConfig(config2);
12278
+ if (!written) {
12279
+ return json(res, { success: false, message: "Failed to write config file" }, 500);
12280
+ }
12281
+ logger.info("[API] Settings saved: " + JSON.stringify(body));
12282
+ return json(res, { success: true, message: "Settings saved successfully" });
12283
+ }
12284
+ if (apiPath === "/settings/test-connection" && method === "POST") {
12285
+ const body = await readBody(req);
12286
+ const testUrl = body.serverUrl || serverUrl;
12287
+ const startTime = Date.now();
11519
12288
  try {
11520
- const pkgPath = path5.join(__dirname, "..", "package.json");
11521
- let currentVersion = "unknown";
12289
+ const controller = new AbortController();
12290
+ const timeout = setTimeout(() => controller.abort(), body.timeout || 1e4);
12291
+ const resp = await fetch(testUrl + "/api/v1/health", {
12292
+ method: "GET",
12293
+ signal: controller.signal,
12294
+ headers: { "Content-Type": "application/json" }
12295
+ });
12296
+ clearTimeout(timeout);
12297
+ const latency = Date.now() - startTime;
12298
+ let serverInfo = {};
11522
12299
  try {
11523
- const pkgRaw = fs5.readFileSync(pkgPath, "utf-8");
11524
- const pkg = JSON.parse(pkgRaw);
11525
- currentVersion = pkg.version || "unknown";
12300
+ serverInfo = await resp.json();
11526
12301
  } catch {
11527
12302
  }
11528
- if (currentVersion === "unknown") {
11529
- const altPkgPath = path5.join(process.cwd(), "plugin", "package.json");
11530
- try {
11531
- const pkg = JSON.parse(fs5.readFileSync(altPkgPath, "utf-8"));
11532
- currentVersion = pkg.version || "unknown";
11533
- } catch {
11534
- }
11535
- }
11536
- let latestVersion = currentVersion;
11537
- let updateAvailable = false;
11538
- try {
11539
- const npmView = execSync("npm view aicq-openclaw-plugin version --registry https://registry.npmjs.org 2>/dev/null", {
11540
- timeout: 15e3,
11541
- encoding: "utf-8"
11542
- }).trim();
11543
- if (npmView) {
11544
- latestVersion = npmView.replace(/^\^|~|\s+.*$/g, "").trim();
11545
- updateAvailable = latestVersion !== currentVersion;
11546
- }
11547
- } catch (npmErr) {
11548
- logger.warn("[API] npm view failed: " + (npmErr instanceof Error ? npmErr.message : String(npmErr)));
12303
+ if (resp.ok) {
12304
+ return json(res, {
12305
+ success: true,
12306
+ status: "ok",
12307
+ statusCode: resp.status,
12308
+ latency,
12309
+ serverUrl: testUrl,
12310
+ serverInfo
12311
+ });
12312
+ } else {
12313
+ return json(res, {
12314
+ success: false,
12315
+ status: "error",
12316
+ statusCode: resp.status,
12317
+ latency,
12318
+ serverUrl: testUrl,
12319
+ message: "Server returned HTTP " + resp.status
12320
+ });
11549
12321
  }
11550
- return json(res, {
11551
- currentVersion,
11552
- latestVersion,
11553
- updateAvailable,
11554
- packageName: "aicq-openclaw-plugin"
11555
- });
11556
12322
  } catch (err) {
12323
+ const latency = Date.now() - startTime;
11557
12324
  const msg = err instanceof Error ? err.message : String(err);
11558
- return json(res, { error: "Failed to check for updates: " + msg }, 500);
12325
+ const isTimeout = msg.includes("abort") || msg.includes("timeout");
12326
+ return json(res, {
12327
+ success: false,
12328
+ status: isTimeout ? "timeout" : "unreachable",
12329
+ latency,
12330
+ serverUrl: testUrl,
12331
+ message: isTimeout ? "Connection timed out after " + latency + "ms" : "Cannot reach server: " + msg
12332
+ });
11559
12333
  }
11560
12334
  }
11561
- if (apiPath === "/update" && method === "POST") {
12335
+ if (apiPath === "/settings/reset-identity" && method === "POST") {
11562
12336
  const body = await readBody(req);
11563
- const force = body.force;
12337
+ const confirmReset = body.confirm;
12338
+ if (!confirmReset) {
12339
+ return json(res, { success: false, message: "Confirmation required. Set { confirm: true } to proceed." }, 400);
12340
+ }
11564
12341
  try {
11565
- logger.info("[API] Starting npm update for aicq-openclaw-plugin...");
11566
- const pluginDir = __dirname;
11567
- let npmRoot = pluginDir;
11568
- for (let i = 0; i < 5; i++) {
11569
- if (npmRoot.endsWith("node_modules"))
11570
- break;
11571
- npmRoot = path5.dirname(npmRoot);
11572
- }
11573
- const parentDir = path5.dirname(npmRoot);
11574
- let installDir = parentDir;
11575
- const candidates = [
11576
- parentDir,
11577
- path5.join(os.homedir(), ".openclaw", "plugins"),
11578
- path5.join(os.homedir(), ".config", "openclaw", "plugins")
11579
- ];
11580
- let updateCmd;
11581
- if (force) {
11582
- updateCmd = `npm install aicq-openclaw-plugin@latest --dangerously-force-unsafe-install --registry https://registry.npmjs.org`;
11583
- } else {
11584
- updateCmd = `npm update aicq-openclaw-plugin --dangerously-force-unsafe-install --registry https://registry.npmjs.org`;
11585
- }
11586
- let installPath = parentDir;
11587
- try {
11588
- const pkgJsonPath = path5.join(pluginDir, "package.json");
11589
- const realPkgDir = fs5.realpathSync(path5.dirname(pkgJsonPath));
11590
- if (realPkgDir.includes("node_modules")) {
11591
- const nmIdx = realPkgDir.lastIndexOf("node_modules");
11592
- installPath = realPkgDir.substring(0, nmIdx + 13);
11593
- }
11594
- } catch {
11595
- }
11596
- logger.info("[API] Running: " + updateCmd + " in " + installPath);
11597
- const result = await new Promise((resolve3) => {
11598
- const child = exec(updateCmd, {
11599
- cwd: installPath,
11600
- timeout: 12e4,
11601
- encoding: "utf-8",
11602
- env: { ...process.env, NODE_NO_WARNINGS: "1" }
11603
- }, (error, stdout, stderr) => {
11604
- if (error) {
11605
- resolve3("ERROR: " + (error.message || String(error)) + "\n\n" + (stderr || ""));
11606
- } else {
11607
- resolve3(stdout || stderr || "Update completed (no output)");
11608
- }
11609
- });
11610
- child.stdout?.on("data", (d) => {
11611
- });
11612
- child.stderr?.on("data", (d) => {
11613
- });
11614
- });
11615
- let newVersion = "unknown";
11616
- try {
11617
- const pkgRaw = fs5.readFileSync(path5.join(__dirname, "..", "package.json"), "utf-8");
11618
- newVersion = JSON.parse(pkgRaw).version || "unknown";
11619
- } catch {
11620
- }
11621
- const success = !result.startsWith("ERROR");
11622
- logger.info("[API] Update " + (success ? "succeeded" : "failed") + ": " + result.substring(0, 200));
12342
+ identityService.cleanup();
12343
+ chatChannel?.cleanup?.();
12344
+ serverClient.disconnectWebSocket();
12345
+ store.friends.clear();
12346
+ store.sessions.clear();
12347
+ store.pendingHandshakes.clear();
12348
+ store.pendingRequests = [];
12349
+ store.tempNumbers = [];
12350
+ store.save();
12351
+ logger.warn("[API] Agent identity reset by user via settings UI");
11623
12352
  return json(res, {
11624
- success,
11625
- message: success ? "Plugin updated successfully! Restart the service to apply changes." : "Update failed.",
11626
- output: result.substring(0, 2e3),
11627
- newVersion,
11628
- needsRestart: success
12353
+ success: true,
12354
+ message: "Identity reset successfully. All friends, sessions, and keys have been deleted. Restart the plugin to generate a new identity."
11629
12355
  });
11630
12356
  } catch (err) {
11631
12357
  const msg = err instanceof Error ? err.message : String(err);
11632
- logger.error("[API] Update error: " + msg);
11633
- return json(res, { success: false, message: "Update failed: " + msg }, 500);
12358
+ logger.error("[API] Identity reset failed: " + msg);
12359
+ return json(res, { success: false, message: "Failed to reset identity: " + msg }, 500);
12360
+ }
12361
+ }
12362
+ if (apiPath === "/settings/export" && method === "GET") {
12363
+ const result = readConfig();
12364
+ if (!result)
12365
+ return json(res, { error: "No config file found" }, 400);
12366
+ const pluginsSection = result.config.plugins;
12367
+ const pluginSection = pluginsSection?.["aicq-chat"];
12368
+ return json(res, {
12369
+ exportDate: (/* @__PURE__ */ new Date()).toISOString(),
12370
+ pluginVersion: "1.1.1",
12371
+ settings: pluginSection || {},
12372
+ fullConfig: result.config
12373
+ });
12374
+ }
12375
+ if (apiPath === "/settings/import" && method === "POST") {
12376
+ const body = await readBody(req);
12377
+ const settings = body.settings;
12378
+ const merge = body.merge;
12379
+ if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
12380
+ return json(res, { success: false, message: "Invalid settings object. Provide { settings: {...} }" }, 400);
12381
+ }
12382
+ const result = readConfig();
12383
+ if (!result) {
12384
+ return json(res, { success: false, message: "No config file found" }, 400);
12385
+ }
12386
+ const config2 = result.config;
12387
+ if (!config2.plugins || typeof config2.plugins !== "object") {
12388
+ config2.plugins = {};
12389
+ }
12390
+ const plugins = config2.plugins;
12391
+ if (!plugins["aicq-chat"] || typeof plugins["aicq-chat"] !== "object" || !merge) {
12392
+ plugins["aicq-chat"] = {};
12393
+ }
12394
+ const aicqConfig = plugins["aicq-chat"];
12395
+ Object.assign(aicqConfig, settings);
12396
+ const written = writeConfig(config2);
12397
+ if (!written) {
12398
+ return json(res, { success: false, message: "Failed to write config" }, 500);
12399
+ }
12400
+ logger.info("[API] Settings imported: " + Object.keys(settings).join(", "));
12401
+ return json(res, { success: true, message: "Settings imported successfully" });
12402
+ }
12403
+ if (apiPath === "/settings/section" && method === "POST") {
12404
+ const body = await readBody(req);
12405
+ const section = body.section;
12406
+ const data = body.data;
12407
+ if (!section || !data) {
12408
+ return json(res, { success: false, message: "Missing section or data" }, 400);
12409
+ }
12410
+ const result = readConfig();
12411
+ if (!result) {
12412
+ return json(res, { success: false, message: "No config file found" }, 400);
12413
+ }
12414
+ const config2 = result.config;
12415
+ if (!config2.plugins || typeof config2.plugins !== "object") {
12416
+ config2.plugins = {};
12417
+ }
12418
+ const plugins = config2.plugins;
12419
+ if (!plugins["aicq-chat"] || typeof plugins["aicq-chat"] !== "object") {
12420
+ plugins["aicq-chat"] = {};
12421
+ }
12422
+ const aicqConfig = plugins["aicq-chat"];
12423
+ for (const [key, value] of Object.entries(data)) {
12424
+ aicqConfig[key] = value;
12425
+ }
12426
+ const written = writeConfig(config2);
12427
+ if (!written) {
12428
+ return json(res, { success: false, message: "Failed to write config" }, 500);
11634
12429
  }
12430
+ logger.info("[API] Settings section saved: " + section);
12431
+ return json(res, { success: true, message: 'Section "' + section + '" saved' });
11635
12432
  }
11636
- if (apiPath === "/plugin-info" && method === "GET") {
11637
- let pkgVersion = "unknown";
12433
+ if (apiPath === "/config/raw" && method === "GET") {
12434
+ const result = readConfig();
12435
+ if (!result)
12436
+ return json(res, { error: "No config file found" }, 404);
12437
+ const raw = fs5.readFileSync(result.configPath, "utf-8");
12438
+ return json(res, {
12439
+ configPath: result.configPath,
12440
+ configSource: path5.basename(result.configPath),
12441
+ rawJson: raw,
12442
+ config: result.config
12443
+ });
12444
+ }
12445
+ if (apiPath === "/config/raw" && method === "PUT") {
12446
+ const body = await readBody(req);
12447
+ const rawJson = body.rawJson;
12448
+ if (!rawJson)
12449
+ return json(res, { success: false, message: "Missing rawJson" }, 400);
12450
+ let parsed;
11638
12451
  try {
11639
- const pkgPath = path5.join(__dirname, "..", "package.json");
11640
- const pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
11641
- pkgVersion = pkg.version || "unknown";
11642
- } catch {
11643
- const altPkgPath = path5.join(process.cwd(), "plugin", "package.json");
11644
- try {
11645
- pkgVersion = JSON.parse(fs5.readFileSync(altPkgPath, "utf-8")).version || "unknown";
11646
- } catch {
12452
+ parsed = JSON.parse(rawJson);
12453
+ } catch (e) {
12454
+ return json(res, { success: false, message: "Invalid JSON: " + (e instanceof Error ? e.message : String(e)) }, 400);
12455
+ }
12456
+ const configPath = findConfigPath();
12457
+ if (!configPath)
12458
+ return json(res, { success: false, message: "No config file found" }, 400);
12459
+ try {
12460
+ fs5.writeFileSync(configPath, JSON.stringify(parsed, null, 2), "utf-8");
12461
+ logger.info("[API] Config file updated via raw JSON editor");
12462
+ return json(res, { success: true, message: "Config file saved", configPath });
12463
+ } catch (e) {
12464
+ const msg = e instanceof Error ? e.message : String(e);
12465
+ return json(res, { success: false, message: "Write failed: " + msg }, 500);
12466
+ }
12467
+ }
12468
+ if (apiPath === "/mgmt-url" && method === "GET") {
12469
+ const host = req.headers?.host || "127.0.0.1:6109";
12470
+ const protocol = req.headers?.["x-forwarded-proto"] || "http";
12471
+ const mgmtUrl = protocol + "://" + host;
12472
+ return json(res, {
12473
+ mgmtUrl,
12474
+ standaloneUrl: "http://127.0.0.1:6109",
12475
+ gatewayPath: "/plugins/aicq-chat/"
12476
+ });
12477
+ }
12478
+ if (apiPath === "/agents" && method === "POST") {
12479
+ const body = await readBody(req);
12480
+ const agent = body.agent;
12481
+ if (!agent || typeof agent !== "object") {
12482
+ return json(res, { success: false, message: "Missing agent object" }, 400);
12483
+ }
12484
+ const result = readConfig();
12485
+ if (!result)
12486
+ return json(res, { success: false, message: "No config file found" }, 400);
12487
+ const config2 = result.config;
12488
+ if (!Array.isArray(config2.agents)) {
12489
+ config2.agents = [];
12490
+ const singleAgent = config2.agent;
12491
+ if (typeof singleAgent === "object" && singleAgent !== null && !Array.isArray(singleAgent)) {
12492
+ config2.agents.push(singleAgent);
12493
+ delete config2.agent;
11647
12494
  }
11648
12495
  }
12496
+ config2.agents.push(agent);
12497
+ const written = writeConfig(config2);
12498
+ if (!written)
12499
+ return json(res, { success: false, message: "Failed to write config" }, 500);
12500
+ logger.info("[API] Agent added via UI");
12501
+ return json(res, { success: true, message: "Agent added", index: config2.agents.length - 1 });
12502
+ }
12503
+ if (apiPath.startsWith("/agents/") && method === "PUT") {
12504
+ const idxStr = decodeURIComponent(apiPath.slice("/agents/".length));
12505
+ const idx = parseInt(idxStr, 10);
12506
+ if (isNaN(idx) || idx < 0)
12507
+ return json(res, { success: false, message: "Invalid agent index" }, 400);
12508
+ const body = await readBody(req);
12509
+ const updates = body.agent;
12510
+ if (!updates || typeof updates !== "object") {
12511
+ return json(res, { success: false, message: "Missing agent object" }, 400);
12512
+ }
12513
+ const result = readConfig();
12514
+ if (!result)
12515
+ return json(res, { success: false, message: "No config file found" }, 400);
12516
+ const config2 = result.config;
12517
+ let agentsArr;
12518
+ if (!Array.isArray(config2.agents)) {
12519
+ return json(res, { success: false, message: "No agents array in config" }, 400);
12520
+ }
12521
+ agentsArr = config2.agents;
12522
+ if (idx >= agentsArr.length) {
12523
+ return json(res, { success: false, message: "Agent index out of range" }, 400);
12524
+ }
12525
+ Object.assign(agentsArr[idx], updates);
12526
+ const written = writeConfig(config2);
12527
+ if (!written)
12528
+ return json(res, { success: false, message: "Failed to write config" }, 500);
12529
+ logger.info("[API] Agent updated at index " + idx);
12530
+ return json(res, { success: true, message: "Agent updated" });
12531
+ }
12532
+ if (apiPath === "/config/switch" && method === "POST") {
12533
+ const body = await readBody(req);
12534
+ const target = body.target;
12535
+ if (target !== "openclaw" && target !== "stableclaw") {
12536
+ return json(res, { success: false, message: "target must be 'openclaw' or 'stableclaw'" }, 400);
12537
+ }
12538
+ const targetFile = target + ".json";
12539
+ const currentResult = readConfig();
12540
+ if (!currentResult)
12541
+ return json(res, { success: false, message: "No current config file found" }, 400);
12542
+ const currentBasename = path5.basename(currentResult.configPath);
12543
+ if (currentBasename === targetFile) {
12544
+ return json(res, { success: false, message: "Already using " + targetFile }, 400);
12545
+ }
12546
+ const targetPath = path5.join(path5.dirname(currentResult.configPath), targetFile);
12547
+ try {
12548
+ const raw = fs5.readFileSync(currentResult.configPath, "utf-8");
12549
+ fs5.writeFileSync(targetPath, raw, "utf-8");
12550
+ logger.info("[API] Config copied to " + targetFile);
12551
+ return json(res, { success: true, message: "Config copied to " + targetFile, newPath: targetPath });
12552
+ } catch (e) {
12553
+ const msg = e instanceof Error ? e.message : String(e);
12554
+ return json(res, { success: false, message: "Failed: " + msg }, 500);
12555
+ }
12556
+ }
12557
+ if (apiPath === "/config-file/raw" && method === "GET") {
12558
+ const configPath = findConfigPath();
12559
+ if (!configPath)
12560
+ return json(res, { error: "No config file found" }, 404);
12561
+ const configName = path5.basename(configPath);
12562
+ const raw = fs5.readFileSync(configPath, "utf-8");
12563
+ const config2 = JSON.parse(raw);
12564
+ const stats = fs5.statSync(configPath);
11649
12565
  return json(res, {
11650
- version: pkgVersion,
11651
- name: "aicq-openclaw-plugin",
11652
- uptime: process.uptime(),
11653
- nodeVersion: process.version,
11654
- platform: process.platform,
11655
- pid: process.pid
12566
+ configPath,
12567
+ configName,
12568
+ raw,
12569
+ config: config2,
12570
+ size: stats.size,
12571
+ modified: stats.mtime.toISOString()
11656
12572
  });
11657
12573
  }
12574
+ if (apiPath === "/config-file/raw" && method === "PUT") {
12575
+ const body = await readBody(req);
12576
+ const content = body.content;
12577
+ if (!content)
12578
+ return json(res, { success: false, message: "Missing content field" }, 400);
12579
+ let parsed;
12580
+ try {
12581
+ parsed = JSON.parse(content);
12582
+ } catch (e) {
12583
+ return json(res, { success: false, message: "Invalid JSON: " + (e instanceof Error ? e.message : String(e)) }, 400);
12584
+ }
12585
+ const configPath = findConfigPath();
12586
+ if (!configPath)
12587
+ return json(res, { success: false, message: "No config file found" }, 400);
12588
+ try {
12589
+ fs5.writeFileSync(configPath, JSON.stringify(parsed, null, 2), "utf-8");
12590
+ logger.info("[API] Config file written via /config-file/raw");
12591
+ return json(res, { success: true, message: "Config file saved", configPath });
12592
+ } catch (e) {
12593
+ const msg = e instanceof Error ? e.message : String(e);
12594
+ return json(res, { success: false, message: "Write failed: " + msg }, 500);
12595
+ }
12596
+ }
12597
+ if (apiPath.match(/^\/models\/[^/]+$/) && method === "DELETE") {
12598
+ const providerId = decodeURIComponent(apiPath.slice("/models/".length));
12599
+ const provider = MODEL_PROVIDERS.find((p) => p.id === providerId);
12600
+ if (!provider)
12601
+ return json(res, { success: false, message: "Unknown provider: " + providerId }, 400);
12602
+ const result = readConfig();
12603
+ if (!result)
12604
+ return json(res, { success: false, message: "No config file found" }, 400);
12605
+ const config2 = result.config;
12606
+ const providersSection = config2.providers;
12607
+ if (providersSection && typeof providersSection === "object" && providersSection[provider.configKey]) {
12608
+ providersSection[provider.configKey] = {};
12609
+ }
12610
+ if (config2[provider.configKey]) {
12611
+ config2[provider.configKey] = {};
12612
+ }
12613
+ const written = writeConfig(config2);
12614
+ if (!written)
12615
+ return json(res, { success: false, message: "Failed to write config file" }, 500);
12616
+ logger.info("[API] Model config cleared for provider: " + providerId);
12617
+ return json(res, { success: true, message: "Model configuration cleared for " + provider.name });
12618
+ }
11658
12619
  res.writeHead(404, { "Content-Type": "application/json" });
11659
12620
  res.end(JSON.stringify({ error: "Not found: " + apiPath }));
11660
12621
  } catch (err) {
@@ -11712,7 +12673,7 @@ var plugin = definePluginEntry({
11712
12673
  debug: (msg, ...args) => ocLog.debug?.(msg, ...args) ?? console.log("[aicq-chat DEBUG]", msg, ...args)
11713
12674
  };
11714
12675
  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");
11715
- logger.info(" AICQ Encrypted Chat Plugin v1.0.0");
12676
+ logger.info(" AICQ Encrypted Chat Plugin v1.1.1");
11716
12677
  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");
11717
12678
  const pluginCfg = api.pluginConfig ?? {};
11718
12679
  const config2 = loadConfig({
@@ -12215,44 +13176,84 @@ var plugin = definePluginEntry({
12215
13176
  serverUrl,
12216
13177
  aicqAgentId,
12217
13178
  logger,
12218
- html: managementHtml
12219
- });
12220
- const mgmtPort = parseInt(process.env.AICQ_MGMT_PORT || "16888", 10);
12221
- const mgmtServer = http.createServer((req, res) => {
12222
- managementHandler(req, res).catch((err) => {
12223
- logger.error("[HTTP] Management server error: " + (err instanceof Error ? err.message : err));
12224
- if (!res.headersSent) {
12225
- res.writeHead(500, { "Content-Type": "text/plain" });
12226
- }
12227
- res.end("Internal Server Error");
12228
- });
13179
+ html: managementHtml,
13180
+ chatChannel
12229
13181
  });
12230
- mgmtServer.listen(mgmtPort, "127.0.0.1", () => {
12231
- logger.info("[Init] Management UI HTTP server running at http://127.0.0.1:" + mgmtPort + "/");
12232
- });
12233
- mgmtServer.on("error", (err) => {
12234
- if (err.code === "EADDRINUSE") {
12235
- logger.warn("[Init] Management UI port " + mgmtPort + " already in use, trying " + (mgmtPort + 1));
12236
- mgmtServer.close();
12237
- mgmtServer.listen(mgmtPort + 1, "127.0.0.1", () => {
12238
- logger.info("[Init] Management UI HTTP server running at http://127.0.0.1:" + (mgmtPort + 1) + "/");
13182
+ const apiKeys = Object.keys(api).filter((k) => typeof api[k] === "function");
13183
+ logger.info("[Init] Available API methods: " + apiKeys.join(", "));
13184
+ let mgmtPort = 6109;
13185
+ let actualMgmtPort = mgmtPort;
13186
+ let mgmtUiRegistered = false;
13187
+ const autoOpenDelay = setTimeout(() => {
13188
+ try {
13189
+ const url = mgmtUiRegistered ? "http://127.0.0.1:" + actualMgmtPort + "/plugins/aicq-chat/" : "http://127.0.0.1:" + actualMgmtPort + "/";
13190
+ const cmd = process.platform === "win32" ? "start" : process.platform === "darwin" ? "open" : "xdg-open";
13191
+ exec(cmd + ' "' + url + '"', (err) => {
13192
+ if (err)
13193
+ logger.debug("[Init] Auto-open browser skipped: " + (err.message || err));
13194
+ else
13195
+ logger.info("[Init] Management UI opened in browser: " + url);
12239
13196
  });
12240
- } else {
12241
- logger.error("[Init] Management UI HTTP server error: " + err.message);
13197
+ } catch (_e) {
13198
+ logger.debug("[Init] Auto-open browser not available");
12242
13199
  }
12243
- });
13200
+ }, 3e3);
12244
13201
  if (api.registerHttpRoute) {
12245
- api.registerHttpRoute({
12246
- path: "/aicq-chat",
12247
- auth: "gateway",
12248
- match: "prefix",
12249
- handler: managementHandler
12250
- });
12251
- logger.info("[Init] Management UI also registered via gateway at /plugins/aicq-chat/");
13202
+ try {
13203
+ api.registerHttpRoute({
13204
+ path: "/plugins/aicq-chat",
13205
+ auth: "plugin",
13206
+ match: "prefix",
13207
+ handler: managementHandler
13208
+ });
13209
+ logger.info("[Init] Management UI registered via gateway at /plugins/aicq-chat/");
13210
+ mgmtUiRegistered = true;
13211
+ } catch (routeErr) {
13212
+ logger.warn("[Init] Gateway route registration failed: " + (routeErr instanceof Error ? routeErr.message : String(routeErr)));
13213
+ }
13214
+ }
13215
+ if (!mgmtUiRegistered) {
13216
+ try {
13217
+ mgmtPort = parseInt(process.env.AICQ_MGMT_PORT || "6109", 10);
13218
+ actualMgmtPort = mgmtPort;
13219
+ const mgmtServer = http.createServer((req, res) => {
13220
+ managementHandler(req, res).catch((err) => {
13221
+ logger.error("[HTTP] Management server error: " + (err instanceof Error ? err.message : err));
13222
+ if (!res.headersSent) {
13223
+ res.writeHead(500, { "Content-Type": "text/plain" });
13224
+ }
13225
+ res.end("Internal Server Error");
13226
+ });
13227
+ });
13228
+ mgmtServer.listen(mgmtPort, "127.0.0.1", () => {
13229
+ actualMgmtPort = mgmtPort;
13230
+ logger.info("[Init] Management UI HTTP server running at http://127.0.0.1:" + mgmtPort + "/");
13231
+ });
13232
+ mgmtServer.on("error", (err) => {
13233
+ if (err.code === "EADDRINUSE") {
13234
+ actualMgmtPort = mgmtPort + 1;
13235
+ logger.warn("[Init] Management UI port " + mgmtPort + " already in use, trying " + actualMgmtPort);
13236
+ mgmtServer.close();
13237
+ mgmtServer.listen(actualMgmtPort, "127.0.0.1", () => {
13238
+ logger.info("[Init] Management UI HTTP server running at http://127.0.0.1:" + actualMgmtPort + "/");
13239
+ });
13240
+ } else {
13241
+ logger.error("[Init] Management UI HTTP server error: " + err.message);
13242
+ }
13243
+ });
13244
+ logger.info("[Init] Standalone management UI server starting on port " + mgmtPort);
13245
+ } catch (httpErr) {
13246
+ logger.error("[Init] Failed to start management UI server: " + (httpErr instanceof Error ? httpErr.message : String(httpErr)));
13247
+ }
12252
13248
  }
12253
13249
  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");
12254
13250
  logger.info(" AICQ Plugin activated successfully!");
12255
- logger.info(" Management UI: http://127.0.0.1:" + mgmtPort + "/");
13251
+ if (mgmtUiRegistered) {
13252
+ logger.info(" Management UI: via gateway /plugins/aicq-chat/");
13253
+ logger.info(" Management UI (local): http://127.0.0.1:" + actualMgmtPort + "/plugins/aicq-chat/");
13254
+ } else {
13255
+ logger.info(" Management UI: http://127.0.0.1:" + actualMgmtPort + "/");
13256
+ }
12256
13257
  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");
12257
13258
  }
12258
13259
  });