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