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