aicq-openclaw-plugin 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2435 -443
- package/openclaw.plugin.json +2 -2
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -105,7 +105,7 @@ var require_main = __commonJS({
|
|
|
105
105
|
var fs6 = __require("fs");
|
|
106
106
|
var path7 = __require("path");
|
|
107
107
|
var os2 = __require("os");
|
|
108
|
-
var
|
|
108
|
+
var crypto7 = __require("crypto");
|
|
109
109
|
var packageJson = require_package();
|
|
110
110
|
var version = packageJson.version;
|
|
111
111
|
var LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg;
|
|
@@ -324,7 +324,7 @@ var require_main = __commonJS({
|
|
|
324
324
|
const authTag = ciphertext.subarray(-16);
|
|
325
325
|
ciphertext = ciphertext.subarray(12, -16);
|
|
326
326
|
try {
|
|
327
|
-
const aesgcm =
|
|
327
|
+
const aesgcm = crypto7.createDecipheriv("aes-256-gcm", key, nonce);
|
|
328
328
|
aesgcm.setAuthTag(authTag);
|
|
329
329
|
return `${aesgcm.update(ciphertext)}${aesgcm.final()}`;
|
|
330
330
|
} catch (error) {
|
|
@@ -7120,22 +7120,22 @@ var require_nacl_fast = __commonJS({
|
|
|
7120
7120
|
randombytes = fn;
|
|
7121
7121
|
};
|
|
7122
7122
|
(function() {
|
|
7123
|
-
var
|
|
7124
|
-
if (
|
|
7123
|
+
var crypto7 = typeof self !== "undefined" ? self.crypto || self.msCrypto : null;
|
|
7124
|
+
if (crypto7 && crypto7.getRandomValues) {
|
|
7125
7125
|
var QUOTA = 65536;
|
|
7126
7126
|
nacl3.setPRNG(function(x, n) {
|
|
7127
7127
|
var i, v = new Uint8Array(n);
|
|
7128
7128
|
for (i = 0; i < n; i += QUOTA) {
|
|
7129
|
-
|
|
7129
|
+
crypto7.getRandomValues(v.subarray(i, i + Math.min(n - i, QUOTA)));
|
|
7130
7130
|
}
|
|
7131
7131
|
for (i = 0; i < n; i++) x[i] = v[i];
|
|
7132
7132
|
cleanup(v);
|
|
7133
7133
|
});
|
|
7134
7134
|
} else if (typeof __require !== "undefined") {
|
|
7135
|
-
|
|
7136
|
-
if (
|
|
7135
|
+
crypto7 = __require("crypto");
|
|
7136
|
+
if (crypto7 && crypto7.randomBytes) {
|
|
7137
7137
|
nacl3.setPRNG(function(x, n) {
|
|
7138
|
-
var i, v =
|
|
7138
|
+
var i, v = crypto7.randomBytes(n);
|
|
7139
7139
|
for (i = 0; i < n; i++) x[i] = v[i];
|
|
7140
7140
|
cleanup(v);
|
|
7141
7141
|
});
|
|
@@ -7212,9 +7212,9 @@ var require_nacl_util = __commonJS({
|
|
|
7212
7212
|
}
|
|
7213
7213
|
});
|
|
7214
7214
|
|
|
7215
|
-
//
|
|
7215
|
+
// ../shared/crypto/dist/nacl.js
|
|
7216
7216
|
var require_nacl = __commonJS({
|
|
7217
|
-
"
|
|
7217
|
+
"../shared/crypto/dist/nacl.js"(exports) {
|
|
7218
7218
|
"use strict";
|
|
7219
7219
|
var __importDefault = exports && exports.__importDefault || function(mod) {
|
|
7220
7220
|
return mod && mod.__esModule ? mod : { "default": mod };
|
|
@@ -7231,9 +7231,9 @@ var require_nacl = __commonJS({
|
|
|
7231
7231
|
}
|
|
7232
7232
|
});
|
|
7233
7233
|
|
|
7234
|
-
//
|
|
7234
|
+
// ../shared/crypto/dist/keygen.js
|
|
7235
7235
|
var require_keygen = __commonJS({
|
|
7236
|
-
"
|
|
7236
|
+
"../shared/crypto/dist/keygen.js"(exports) {
|
|
7237
7237
|
"use strict";
|
|
7238
7238
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7239
7239
|
exports.generateSigningKeyPair = generateSigningKeyPair2;
|
|
@@ -7272,9 +7272,9 @@ var require_keygen = __commonJS({
|
|
|
7272
7272
|
}
|
|
7273
7273
|
});
|
|
7274
7274
|
|
|
7275
|
-
//
|
|
7275
|
+
// ../shared/crypto/dist/signer.js
|
|
7276
7276
|
var require_signer = __commonJS({
|
|
7277
|
-
"
|
|
7277
|
+
"../shared/crypto/dist/signer.js"(exports) {
|
|
7278
7278
|
"use strict";
|
|
7279
7279
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7280
7280
|
exports.sign = sign;
|
|
@@ -7289,9 +7289,9 @@ var require_signer = __commonJS({
|
|
|
7289
7289
|
}
|
|
7290
7290
|
});
|
|
7291
7291
|
|
|
7292
|
-
//
|
|
7292
|
+
// ../shared/crypto/dist/keyExchange.js
|
|
7293
7293
|
var require_keyExchange = __commonJS({
|
|
7294
|
-
"
|
|
7294
|
+
"../shared/crypto/dist/keyExchange.js"(exports) {
|
|
7295
7295
|
"use strict";
|
|
7296
7296
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7297
7297
|
exports.computeSharedSecret = computeSharedSecret2;
|
|
@@ -7342,9 +7342,9 @@ var require_keyExchange = __commonJS({
|
|
|
7342
7342
|
}
|
|
7343
7343
|
});
|
|
7344
7344
|
|
|
7345
|
-
//
|
|
7345
|
+
// ../shared/crypto/dist/cipher.js
|
|
7346
7346
|
var require_cipher = __commonJS({
|
|
7347
|
-
"
|
|
7347
|
+
"../shared/crypto/dist/cipher.js"(exports) {
|
|
7348
7348
|
"use strict";
|
|
7349
7349
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7350
7350
|
exports.generateNonce = generateNonce;
|
|
@@ -7368,9 +7368,9 @@ var require_cipher = __commonJS({
|
|
|
7368
7368
|
}
|
|
7369
7369
|
});
|
|
7370
7370
|
|
|
7371
|
-
//
|
|
7371
|
+
// ../shared/crypto/dist/message.js
|
|
7372
7372
|
var require_message = __commonJS({
|
|
7373
|
-
"
|
|
7373
|
+
"../shared/crypto/dist/message.js"(exports) {
|
|
7374
7374
|
"use strict";
|
|
7375
7375
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7376
7376
|
exports.createMessage = createMessage;
|
|
@@ -7484,9 +7484,9 @@ var require_message = __commonJS({
|
|
|
7484
7484
|
}
|
|
7485
7485
|
});
|
|
7486
7486
|
|
|
7487
|
-
//
|
|
7487
|
+
// ../shared/crypto/dist/password.js
|
|
7488
7488
|
var require_password = __commonJS({
|
|
7489
|
-
"
|
|
7489
|
+
"../shared/crypto/dist/password.js"(exports) {
|
|
7490
7490
|
"use strict";
|
|
7491
7491
|
var __createBinding = exports && exports.__createBinding || (Object.create ? (function(o, m, k, k2) {
|
|
7492
7492
|
if (k2 === void 0) k2 = k;
|
|
@@ -7529,11 +7529,11 @@ var require_password = __commonJS({
|
|
|
7529
7529
|
exports.deriveKeyFromPassword = deriveKeyFromPassword;
|
|
7530
7530
|
exports.encryptWithPassword = encryptWithPassword2;
|
|
7531
7531
|
exports.decryptWithPassword = decryptWithPassword2;
|
|
7532
|
-
var
|
|
7532
|
+
var crypto7 = __importStar(__require("crypto"));
|
|
7533
7533
|
var nacl_js_1 = require_nacl();
|
|
7534
7534
|
var cipher_js_1 = require_cipher();
|
|
7535
7535
|
function deriveKeyFromPassword(password, salt, iterations = 1e5, keyLength = 32) {
|
|
7536
|
-
return Uint8Array.from(
|
|
7536
|
+
return Uint8Array.from(crypto7.pbkdf2Sync(password, salt, iterations, keyLength, "sha512"));
|
|
7537
7537
|
}
|
|
7538
7538
|
function deriveKeyLegacy(password, salt, iterations = 1e5) {
|
|
7539
7539
|
const encoder = new TextEncoder();
|
|
@@ -7564,9 +7564,9 @@ var require_password = __commonJS({
|
|
|
7564
7564
|
}
|
|
7565
7565
|
});
|
|
7566
7566
|
|
|
7567
|
-
//
|
|
7567
|
+
// ../shared/crypto/dist/handshake.js
|
|
7568
7568
|
var require_handshake = __commonJS({
|
|
7569
|
-
"
|
|
7569
|
+
"../shared/crypto/dist/handshake.js"(exports) {
|
|
7570
7570
|
"use strict";
|
|
7571
7571
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7572
7572
|
exports.createHandshakeRequest = createHandshakeRequest2;
|
|
@@ -7675,9 +7675,9 @@ var require_handshake = __commonJS({
|
|
|
7675
7675
|
}
|
|
7676
7676
|
});
|
|
7677
7677
|
|
|
7678
|
-
//
|
|
7678
|
+
// ../shared/crypto/dist/index.js
|
|
7679
7679
|
var require_dist = __commonJS({
|
|
7680
|
-
"
|
|
7680
|
+
"../shared/crypto/dist/index.js"(exports) {
|
|
7681
7681
|
"use strict";
|
|
7682
7682
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7683
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;
|
|
@@ -7779,12 +7779,12 @@ import * as fs from "fs";
|
|
|
7779
7779
|
import * as path from "path";
|
|
7780
7780
|
|
|
7781
7781
|
// node_modules/uuid/dist/esm-node/rng.js
|
|
7782
|
-
import
|
|
7782
|
+
import crypto2 from "crypto";
|
|
7783
7783
|
var rnds8Pool = new Uint8Array(256);
|
|
7784
7784
|
var poolPtr = rnds8Pool.length;
|
|
7785
7785
|
function rng() {
|
|
7786
7786
|
if (poolPtr > rnds8Pool.length - 16) {
|
|
7787
|
-
|
|
7787
|
+
crypto2.randomFillSync(rnds8Pool);
|
|
7788
7788
|
poolPtr = 0;
|
|
7789
7789
|
}
|
|
7790
7790
|
return rnds8Pool.slice(poolPtr, poolPtr += 16);
|
|
@@ -7800,9 +7800,9 @@ function unsafeStringify(arr, offset = 0) {
|
|
|
7800
7800
|
}
|
|
7801
7801
|
|
|
7802
7802
|
// node_modules/uuid/dist/esm-node/native.js
|
|
7803
|
-
import
|
|
7803
|
+
import crypto3 from "crypto";
|
|
7804
7804
|
var native_default = {
|
|
7805
|
-
randomUUID:
|
|
7805
|
+
randomUUID: crypto3.randomUUID
|
|
7806
7806
|
};
|
|
7807
7807
|
|
|
7808
7808
|
// node_modules/uuid/dist/esm-node/v4.js
|
|
@@ -7863,7 +7863,7 @@ function generateAgentId() {
|
|
|
7863
7863
|
// dist/store.js
|
|
7864
7864
|
import * as fs2 from "fs";
|
|
7865
7865
|
import * as path2 from "path";
|
|
7866
|
-
import * as
|
|
7866
|
+
import * as crypto4 from "crypto";
|
|
7867
7867
|
function encodeBuffer(buf) {
|
|
7868
7868
|
return Buffer.from(buf).toString("base64");
|
|
7869
7869
|
}
|
|
@@ -7871,11 +7871,11 @@ function decodeBuffer(b64) {
|
|
|
7871
7871
|
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
7872
7872
|
}
|
|
7873
7873
|
function deriveEncryptionKey(nodeId, salt) {
|
|
7874
|
-
return Uint8Array.from(
|
|
7874
|
+
return Uint8Array.from(crypto4.pbkdf2Sync(nodeId, salt, 1e5, 32, "sha512"));
|
|
7875
7875
|
}
|
|
7876
7876
|
function aes256GcmEncrypt(plaintext, key) {
|
|
7877
|
-
const iv =
|
|
7878
|
-
const cipher =
|
|
7877
|
+
const iv = crypto4.randomBytes(12);
|
|
7878
|
+
const cipher = crypto4.createCipheriv("aes-256-gcm", key, iv);
|
|
7879
7879
|
const encrypted = Buffer.concat([
|
|
7880
7880
|
cipher.update(plaintext),
|
|
7881
7881
|
cipher.final()
|
|
@@ -7892,7 +7892,7 @@ function aes256GcmDecrypt(packedBase64, key) {
|
|
|
7892
7892
|
const iv = packed.subarray(0, 12);
|
|
7893
7893
|
const authTag = packed.subarray(packed.length - 16);
|
|
7894
7894
|
const ciphertext = packed.subarray(12, packed.length - 16);
|
|
7895
|
-
const decipher =
|
|
7895
|
+
const decipher = crypto4.createDecipheriv("aes-256-gcm", key, iv);
|
|
7896
7896
|
decipher.setAuthTag(authTag);
|
|
7897
7897
|
const decrypted = Buffer.concat([
|
|
7898
7898
|
decipher.update(ciphertext),
|
|
@@ -7913,9 +7913,13 @@ var PluginStore = class {
|
|
|
7913
7913
|
this.tempNumbers = [];
|
|
7914
7914
|
this.pendingRequests = [];
|
|
7915
7915
|
this.pendingHandshakes = /* @__PURE__ */ new Map();
|
|
7916
|
+
this.offlineMessages = [];
|
|
7916
7917
|
this.dataDir = "";
|
|
7917
7918
|
this.storePath = "";
|
|
7918
|
-
this.encryptionSalt =
|
|
7919
|
+
this.encryptionSalt = crypto4.randomBytes(16);
|
|
7920
|
+
this.saveTimer = null;
|
|
7921
|
+
this.saveDebounceMs = 1e3;
|
|
7922
|
+
this.dirty = false;
|
|
7919
7923
|
}
|
|
7920
7924
|
/**
|
|
7921
7925
|
* Set the data directory for persistence.
|
|
@@ -7934,12 +7938,38 @@ var PluginStore = class {
|
|
|
7934
7938
|
return deriveEncryptionKey(this.agentId || "default-aicq-node", this.encryptionSalt);
|
|
7935
7939
|
}
|
|
7936
7940
|
/**
|
|
7937
|
-
*
|
|
7941
|
+
* Mark the store as dirty and schedule a debounced save.
|
|
7942
|
+
* For operations that need immediate persistence (e.g., new keys), use saveNow().
|
|
7943
|
+
*/
|
|
7944
|
+
markDirty() {
|
|
7945
|
+
this.dirty = true;
|
|
7946
|
+
if (!this.saveTimer) {
|
|
7947
|
+
this.saveTimer = setTimeout(() => {
|
|
7948
|
+
this.saveTimer = null;
|
|
7949
|
+
this.flushSave();
|
|
7950
|
+
}, this.saveDebounceMs);
|
|
7951
|
+
}
|
|
7952
|
+
}
|
|
7953
|
+
/**
|
|
7954
|
+
* Save the current state to disk immediately (atomic write).
|
|
7938
7955
|
*
|
|
7939
|
-
*
|
|
7940
|
-
* derived from the node ID and
|
|
7956
|
+
* Uses write-to-temp + rename for crash safety. Secret keys are encrypted
|
|
7957
|
+
* at rest using AES-256-GCM with a key derived from the node ID and salt.
|
|
7941
7958
|
*/
|
|
7942
|
-
|
|
7959
|
+
saveNow() {
|
|
7960
|
+
if (this.saveTimer) {
|
|
7961
|
+
clearTimeout(this.saveTimer);
|
|
7962
|
+
this.saveTimer = null;
|
|
7963
|
+
}
|
|
7964
|
+
this.flushSave();
|
|
7965
|
+
}
|
|
7966
|
+
/**
|
|
7967
|
+
* Internal save implementation — writes atomically via temp file + rename.
|
|
7968
|
+
*/
|
|
7969
|
+
flushSave() {
|
|
7970
|
+
if (!this.dirty && this.saveTimer === null)
|
|
7971
|
+
return;
|
|
7972
|
+
this.dirty = false;
|
|
7943
7973
|
if (!this.storePath)
|
|
7944
7974
|
return;
|
|
7945
7975
|
const encKey = this.getEncryptionKey();
|
|
@@ -7980,25 +8010,43 @@ var PluginStore = class {
|
|
|
7980
8010
|
requesterId: p.requesterId,
|
|
7981
8011
|
tempNumber: p.tempNumber,
|
|
7982
8012
|
timestamp: p.timestamp.toISOString()
|
|
7983
|
-
}))
|
|
8013
|
+
})),
|
|
8014
|
+
offlineMessages: this.offlineMessages
|
|
7984
8015
|
};
|
|
7985
8016
|
try {
|
|
7986
|
-
|
|
8017
|
+
const tmpPath = this.storePath + ".tmp";
|
|
8018
|
+
const jsonStr = JSON.stringify(serialized, null, 2);
|
|
8019
|
+
fs2.writeFileSync(tmpPath, jsonStr, "utf-8");
|
|
8020
|
+
fs2.renameSync(tmpPath, this.storePath);
|
|
7987
8021
|
} catch (err) {
|
|
7988
8022
|
console.error("[PluginStore] Failed to save state:", err);
|
|
7989
8023
|
}
|
|
7990
8024
|
}
|
|
8025
|
+
/**
|
|
8026
|
+
* @deprecated Use markDirty() or saveNow() for better performance.
|
|
8027
|
+
* Kept for backward compatibility — performs immediate save.
|
|
8028
|
+
*/
|
|
8029
|
+
save() {
|
|
8030
|
+
this.saveNow();
|
|
8031
|
+
}
|
|
7991
8032
|
/**
|
|
7992
8033
|
* Load state from disk, if available.
|
|
7993
8034
|
*
|
|
7994
8035
|
* Decrypts secret keys using AES-256-GCM with the stored salt and
|
|
7995
|
-
* node ID.
|
|
8036
|
+
* node ID. Falls back to plaintext if the data was stored before
|
|
7996
8037
|
* encryption was added (backward compatibility).
|
|
7997
8038
|
*/
|
|
7998
8039
|
load() {
|
|
7999
8040
|
if (!this.storePath || !fs2.existsSync(this.storePath)) {
|
|
8000
8041
|
return false;
|
|
8001
8042
|
}
|
|
8043
|
+
const tmpPath = this.storePath + ".tmp";
|
|
8044
|
+
if (!fs2.existsSync(this.storePath) && fs2.existsSync(tmpPath)) {
|
|
8045
|
+
try {
|
|
8046
|
+
fs2.renameSync(tmpPath, this.storePath);
|
|
8047
|
+
} catch {
|
|
8048
|
+
}
|
|
8049
|
+
}
|
|
8002
8050
|
try {
|
|
8003
8051
|
const raw = fs2.readFileSync(this.storePath, "utf-8");
|
|
8004
8052
|
const data = JSON.parse(raw);
|
|
@@ -8061,18 +8109,101 @@ var PluginStore = class {
|
|
|
8061
8109
|
tempNumber: p.tempNumber,
|
|
8062
8110
|
timestamp: new Date(p.timestamp)
|
|
8063
8111
|
}));
|
|
8112
|
+
this.offlineMessages = (data.offlineMessages || []).map((m) => ({
|
|
8113
|
+
id: m.id,
|
|
8114
|
+
targetId: m.targetId,
|
|
8115
|
+
encryptedData: m.encryptedData,
|
|
8116
|
+
timestamp: m.timestamp,
|
|
8117
|
+
retryCount: m.retryCount || 0,
|
|
8118
|
+
maxRetries: m.maxRetries || 10
|
|
8119
|
+
}));
|
|
8120
|
+
if (fs2.existsSync(tmpPath)) {
|
|
8121
|
+
try {
|
|
8122
|
+
fs2.unlinkSync(tmpPath);
|
|
8123
|
+
} catch {
|
|
8124
|
+
}
|
|
8125
|
+
}
|
|
8064
8126
|
return true;
|
|
8065
8127
|
} catch (err) {
|
|
8066
8128
|
console.error("[PluginStore] Failed to load state:", err);
|
|
8129
|
+
if (fs2.existsSync(tmpPath)) {
|
|
8130
|
+
console.info("[PluginStore] Attempting recovery from temp file...");
|
|
8131
|
+
try {
|
|
8132
|
+
fs2.renameSync(tmpPath, this.storePath);
|
|
8133
|
+
return this.load();
|
|
8134
|
+
} catch {
|
|
8135
|
+
console.error("[PluginStore] Recovery from temp file failed");
|
|
8136
|
+
}
|
|
8137
|
+
}
|
|
8067
8138
|
return false;
|
|
8068
8139
|
}
|
|
8069
8140
|
}
|
|
8141
|
+
// ----------------------------------------------------------------
|
|
8142
|
+
// Offline message queue
|
|
8143
|
+
// ----------------------------------------------------------------
|
|
8144
|
+
/**
|
|
8145
|
+
* Add a message to the offline queue for later delivery.
|
|
8146
|
+
*/
|
|
8147
|
+
enqueueOfflineMessage(targetId, encryptedData) {
|
|
8148
|
+
const msg = {
|
|
8149
|
+
id: v4_default(),
|
|
8150
|
+
targetId,
|
|
8151
|
+
encryptedData,
|
|
8152
|
+
timestamp: Date.now(),
|
|
8153
|
+
retryCount: 0,
|
|
8154
|
+
maxRetries: 10
|
|
8155
|
+
};
|
|
8156
|
+
this.offlineMessages.push(msg);
|
|
8157
|
+
this.markDirty();
|
|
8158
|
+
return msg;
|
|
8159
|
+
}
|
|
8160
|
+
/**
|
|
8161
|
+
* Dequeue the next pending offline message.
|
|
8162
|
+
*/
|
|
8163
|
+
dequeueOfflineMessage() {
|
|
8164
|
+
return this.offlineMessages.shift();
|
|
8165
|
+
}
|
|
8166
|
+
/**
|
|
8167
|
+
* Get the number of pending offline messages.
|
|
8168
|
+
*/
|
|
8169
|
+
getOfflineMessageCount() {
|
|
8170
|
+
return this.offlineMessages.length;
|
|
8171
|
+
}
|
|
8172
|
+
/**
|
|
8173
|
+
* Peek at the offline message queue without removing.
|
|
8174
|
+
*/
|
|
8175
|
+
peekOfflineMessages() {
|
|
8176
|
+
return [...this.offlineMessages];
|
|
8177
|
+
}
|
|
8178
|
+
/**
|
|
8179
|
+
* Clear all pending offline messages.
|
|
8180
|
+
*/
|
|
8181
|
+
clearOfflineMessages() {
|
|
8182
|
+
this.offlineMessages = [];
|
|
8183
|
+
this.markDirty();
|
|
8184
|
+
}
|
|
8185
|
+
/**
|
|
8186
|
+
* Remove expired offline messages (older than 24 hours).
|
|
8187
|
+
*/
|
|
8188
|
+
cleanupExpiredOfflineMessages() {
|
|
8189
|
+
const now = Date.now();
|
|
8190
|
+
const threshold = 24 * 60 * 60 * 1e3;
|
|
8191
|
+
const before = this.offlineMessages.length;
|
|
8192
|
+
this.offlineMessages = this.offlineMessages.filter((m) => now - m.timestamp < threshold);
|
|
8193
|
+
if (this.offlineMessages.length !== before) {
|
|
8194
|
+
this.markDirty();
|
|
8195
|
+
}
|
|
8196
|
+
return before - this.offlineMessages.length;
|
|
8197
|
+
}
|
|
8198
|
+
// ----------------------------------------------------------------
|
|
8199
|
+
// Friend management
|
|
8200
|
+
// ----------------------------------------------------------------
|
|
8070
8201
|
/**
|
|
8071
8202
|
* Add a friend record.
|
|
8072
8203
|
*/
|
|
8073
8204
|
addFriend(friend) {
|
|
8074
8205
|
this.friends.set(friend.id, friend);
|
|
8075
|
-
this.
|
|
8206
|
+
this.markDirty();
|
|
8076
8207
|
}
|
|
8077
8208
|
/**
|
|
8078
8209
|
* Remove a friend by ID and clean up associated session.
|
|
@@ -8081,7 +8212,7 @@ var PluginStore = class {
|
|
|
8081
8212
|
const removed = this.friends.delete(friendId);
|
|
8082
8213
|
if (removed) {
|
|
8083
8214
|
this.sessions.delete(friendId);
|
|
8084
|
-
this.
|
|
8215
|
+
this.markDirty();
|
|
8085
8216
|
}
|
|
8086
8217
|
return removed;
|
|
8087
8218
|
}
|
|
@@ -8097,6 +8228,9 @@ var PluginStore = class {
|
|
|
8097
8228
|
getFriendCount() {
|
|
8098
8229
|
return this.friends.size;
|
|
8099
8230
|
}
|
|
8231
|
+
// ----------------------------------------------------------------
|
|
8232
|
+
// Session management
|
|
8233
|
+
// ----------------------------------------------------------------
|
|
8100
8234
|
/**
|
|
8101
8235
|
* Set a session for a peer.
|
|
8102
8236
|
*/
|
|
@@ -8106,7 +8240,7 @@ var PluginStore = class {
|
|
|
8106
8240
|
if (friend) {
|
|
8107
8241
|
friend.sessionKey = session.sessionKey;
|
|
8108
8242
|
}
|
|
8109
|
-
this.
|
|
8243
|
+
this.saveNow();
|
|
8110
8244
|
}
|
|
8111
8245
|
/**
|
|
8112
8246
|
* Get a session by peer ID.
|
|
@@ -8120,12 +8254,15 @@ var PluginStore = class {
|
|
|
8120
8254
|
removeSession(peerId) {
|
|
8121
8255
|
return this.sessions.delete(peerId);
|
|
8122
8256
|
}
|
|
8257
|
+
// ----------------------------------------------------------------
|
|
8258
|
+
// Temp number management
|
|
8259
|
+
// ----------------------------------------------------------------
|
|
8123
8260
|
/**
|
|
8124
8261
|
* Add a temp number record.
|
|
8125
8262
|
*/
|
|
8126
8263
|
addTempNumber(number, expiresAt) {
|
|
8127
8264
|
this.tempNumbers.push({ number, expiresAt });
|
|
8128
|
-
this.
|
|
8265
|
+
this.markDirty();
|
|
8129
8266
|
}
|
|
8130
8267
|
/**
|
|
8131
8268
|
* Revoke (remove) a temp number.
|
|
@@ -8134,7 +8271,7 @@ var PluginStore = class {
|
|
|
8134
8271
|
const idx = this.tempNumbers.findIndex((t) => t.number === number);
|
|
8135
8272
|
if (idx !== -1) {
|
|
8136
8273
|
this.tempNumbers.splice(idx, 1);
|
|
8137
|
-
this.
|
|
8274
|
+
this.markDirty();
|
|
8138
8275
|
return true;
|
|
8139
8276
|
}
|
|
8140
8277
|
return false;
|
|
@@ -8147,15 +8284,18 @@ var PluginStore = class {
|
|
|
8147
8284
|
const before = this.tempNumbers.length;
|
|
8148
8285
|
this.tempNumbers = this.tempNumbers.filter((t) => t.expiresAt > now);
|
|
8149
8286
|
if (this.tempNumbers.length !== before) {
|
|
8150
|
-
this.
|
|
8287
|
+
this.markDirty();
|
|
8151
8288
|
}
|
|
8152
8289
|
}
|
|
8290
|
+
// ----------------------------------------------------------------
|
|
8291
|
+
// Pending friend requests
|
|
8292
|
+
// ----------------------------------------------------------------
|
|
8153
8293
|
/**
|
|
8154
8294
|
* Add a pending friend request.
|
|
8155
8295
|
*/
|
|
8156
8296
|
addPendingRequest(request) {
|
|
8157
8297
|
this.pendingRequests.push(request);
|
|
8158
|
-
this.
|
|
8298
|
+
this.markDirty();
|
|
8159
8299
|
}
|
|
8160
8300
|
/**
|
|
8161
8301
|
* Remove a pending friend request.
|
|
@@ -8164,11 +8304,14 @@ var PluginStore = class {
|
|
|
8164
8304
|
const idx = this.pendingRequests.findIndex((p) => p.requesterId === requesterId);
|
|
8165
8305
|
if (idx !== -1) {
|
|
8166
8306
|
this.pendingRequests.splice(idx, 1);
|
|
8167
|
-
this.
|
|
8307
|
+
this.markDirty();
|
|
8168
8308
|
return true;
|
|
8169
8309
|
}
|
|
8170
8310
|
return false;
|
|
8171
8311
|
}
|
|
8312
|
+
// ----------------------------------------------------------------
|
|
8313
|
+
// Handshake state
|
|
8314
|
+
// ----------------------------------------------------------------
|
|
8172
8315
|
/**
|
|
8173
8316
|
* Set a pending handshake state.
|
|
8174
8317
|
*/
|
|
@@ -8192,7 +8335,7 @@ var PluginStore = class {
|
|
|
8192
8335
|
// dist/services/identityService.js
|
|
8193
8336
|
var import_qrcode = __toESM(require_lib(), 1);
|
|
8194
8337
|
var import_crypto3 = __toESM(require_dist(), 1);
|
|
8195
|
-
import * as
|
|
8338
|
+
import * as crypto5 from "crypto";
|
|
8196
8339
|
var IdentityService = class {
|
|
8197
8340
|
constructor(store, logger) {
|
|
8198
8341
|
this.exportTimers = /* @__PURE__ */ new Map();
|
|
@@ -8247,7 +8390,7 @@ var IdentityService = class {
|
|
|
8247
8390
|
* @returns QR code data URL (string starting with "data:image/png;base64,")
|
|
8248
8391
|
*/
|
|
8249
8392
|
async exportPrivateKeyQR(password) {
|
|
8250
|
-
const exportToken =
|
|
8393
|
+
const exportToken = crypto5.randomBytes(32).toString("hex");
|
|
8251
8394
|
const exportPayload = {
|
|
8252
8395
|
a: this.store.agentId,
|
|
8253
8396
|
pk: (0, import_crypto3.encodeBase64)(this.store.identityKeys.publicKey),
|
|
@@ -8365,38 +8508,161 @@ var IdentityService = class {
|
|
|
8365
8508
|
|
|
8366
8509
|
// dist/services/serverClient.js
|
|
8367
8510
|
import WebSocket from "ws";
|
|
8511
|
+
var DEFAULT_CONFIG = {
|
|
8512
|
+
initialReconnectDelay: 1e3,
|
|
8513
|
+
maxReconnectDelay: 6e4,
|
|
8514
|
+
reconnectBackoffFactor: 2,
|
|
8515
|
+
heartbeatIntervalMs: 3e4,
|
|
8516
|
+
requestTimeoutMs: 3e4,
|
|
8517
|
+
initialRetryWindowMs: 6e4,
|
|
8518
|
+
hourlyCheckIntervalMs: 36e5
|
|
8519
|
+
};
|
|
8368
8520
|
var ServerClient = class {
|
|
8369
|
-
constructor(serverUrl, store, logger) {
|
|
8521
|
+
constructor(serverUrl, store, logger, config2) {
|
|
8522
|
+
this.authToken = "";
|
|
8370
8523
|
this.ws = null;
|
|
8371
8524
|
this.wsReconnectTimer = null;
|
|
8372
8525
|
this.heartbeatTimer = null;
|
|
8373
8526
|
this.wsConnected = false;
|
|
8527
|
+
this.connectionState = "offline";
|
|
8528
|
+
this.reconnectDelay = DEFAULT_CONFIG.initialReconnectDelay;
|
|
8529
|
+
this.reconnectAttempts = 0;
|
|
8530
|
+
this.connectStartTimestamp = 0;
|
|
8531
|
+
this.hourlyCheckMode = false;
|
|
8532
|
+
this.hourlyCheckTimer = null;
|
|
8533
|
+
this.stateChangeCallbacks = [];
|
|
8374
8534
|
this.wsHandlers = /* @__PURE__ */ new Map();
|
|
8375
8535
|
this.serverUrl = serverUrl;
|
|
8376
8536
|
this.store = store;
|
|
8377
8537
|
this.logger = logger;
|
|
8538
|
+
this.config = { ...DEFAULT_CONFIG, ...config2 };
|
|
8539
|
+
}
|
|
8540
|
+
/**
|
|
8541
|
+
* Check whether the initial retry window has elapsed.
|
|
8542
|
+
*/
|
|
8543
|
+
isInitialRetryWindowExpired() {
|
|
8544
|
+
if (this.connectStartTimestamp === 0)
|
|
8545
|
+
return false;
|
|
8546
|
+
return Date.now() - this.connectStartTimestamp >= this.config.initialRetryWindowMs;
|
|
8547
|
+
}
|
|
8548
|
+
/**
|
|
8549
|
+
* Set the JWT auth token for all subsequent requests.
|
|
8550
|
+
*/
|
|
8551
|
+
setAuthToken(token) {
|
|
8552
|
+
this.authToken = token;
|
|
8553
|
+
this.logger.info("[Server] Auth token set (" + token.substring(0, 12) + "...)");
|
|
8554
|
+
}
|
|
8555
|
+
/**
|
|
8556
|
+
* Get the current auth token.
|
|
8557
|
+
*/
|
|
8558
|
+
getAuthToken() {
|
|
8559
|
+
return this.authToken;
|
|
8560
|
+
}
|
|
8561
|
+
/**
|
|
8562
|
+
* Build common headers including Authorization when token is available.
|
|
8563
|
+
*/
|
|
8564
|
+
authHeaders() {
|
|
8565
|
+
const headers = { "Content-Type": "application/json" };
|
|
8566
|
+
if (this.authToken) {
|
|
8567
|
+
headers["Authorization"] = "Bearer " + this.authToken;
|
|
8568
|
+
}
|
|
8569
|
+
return headers;
|
|
8570
|
+
}
|
|
8571
|
+
// ----------------------------------------------------------------
|
|
8572
|
+
// Connection state management
|
|
8573
|
+
// ----------------------------------------------------------------
|
|
8574
|
+
/**
|
|
8575
|
+
* Get the current connection state.
|
|
8576
|
+
*/
|
|
8577
|
+
getConnectionState() {
|
|
8578
|
+
return this.connectionState;
|
|
8579
|
+
}
|
|
8580
|
+
/**
|
|
8581
|
+
* Register a callback for connection state changes.
|
|
8582
|
+
*/
|
|
8583
|
+
onConnectionStateChange(callback) {
|
|
8584
|
+
this.stateChangeCallbacks.push(callback);
|
|
8585
|
+
}
|
|
8586
|
+
/**
|
|
8587
|
+
* Remove a connection state callback.
|
|
8588
|
+
*/
|
|
8589
|
+
offConnectionStateChange(callback) {
|
|
8590
|
+
this.stateChangeCallbacks = this.stateChangeCallbacks.filter((cb) => cb !== callback);
|
|
8591
|
+
}
|
|
8592
|
+
/**
|
|
8593
|
+
* Update connection state and notify listeners.
|
|
8594
|
+
*/
|
|
8595
|
+
setConnectionState(newState) {
|
|
8596
|
+
const previousState = this.connectionState;
|
|
8597
|
+
if (previousState === newState)
|
|
8598
|
+
return;
|
|
8599
|
+
this.connectionState = newState;
|
|
8600
|
+
this.logger.info(`[Server] Connection state: ${previousState} \u2192 ${newState}`);
|
|
8601
|
+
for (const callback of this.stateChangeCallbacks) {
|
|
8602
|
+
try {
|
|
8603
|
+
callback(newState, previousState);
|
|
8604
|
+
} catch (err) {
|
|
8605
|
+
this.logger.error("[Server] Connection state callback error:", err);
|
|
8606
|
+
}
|
|
8607
|
+
}
|
|
8378
8608
|
}
|
|
8379
8609
|
// ----------------------------------------------------------------
|
|
8380
8610
|
// WebSocket connection
|
|
8381
8611
|
// ----------------------------------------------------------------
|
|
8382
8612
|
/**
|
|
8383
8613
|
* Connect to the server via WebSocket and start heartbeat.
|
|
8614
|
+
* Strategy: retry aggressively for `initialRetryWindowMs` (default 1 minute),
|
|
8615
|
+
* then stop and switch to hourly checks.
|
|
8384
8616
|
*/
|
|
8385
8617
|
connectWebSocket() {
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
|
|
8390
|
-
|
|
8618
|
+
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
|
|
8619
|
+
this.logger.debug("[Server] WebSocket already connecting/connected");
|
|
8620
|
+
return;
|
|
8621
|
+
}
|
|
8622
|
+
if (this.connectStartTimestamp === 0 && !this.hourlyCheckMode) {
|
|
8623
|
+
this.connectStartTimestamp = Date.now();
|
|
8624
|
+
}
|
|
8625
|
+
let wsUrl;
|
|
8626
|
+
try {
|
|
8627
|
+
const baseUrl = this.serverUrl.replace(/^http/, "ws");
|
|
8628
|
+
const url = new URL(baseUrl + "/ws");
|
|
8629
|
+
url.port = "443";
|
|
8630
|
+
url.protocol = "wss:";
|
|
8631
|
+
wsUrl = url.toString();
|
|
8632
|
+
} catch {
|
|
8633
|
+
wsUrl = this.serverUrl.replace(/^https?/, "wss") + "/ws";
|
|
8634
|
+
}
|
|
8391
8635
|
this.logger.info("[Server] Connecting WebSocket to " + wsUrl);
|
|
8392
|
-
this.
|
|
8636
|
+
this.setConnectionState("reconnecting");
|
|
8637
|
+
try {
|
|
8638
|
+
this.ws = new WebSocket(wsUrl);
|
|
8639
|
+
} catch (err) {
|
|
8640
|
+
this.logger.error("[Server] Failed to create WebSocket:", err);
|
|
8641
|
+
this.setConnectionState("offline");
|
|
8642
|
+
this.scheduleReconnect();
|
|
8643
|
+
return;
|
|
8644
|
+
}
|
|
8645
|
+
const connectTimeout = setTimeout(() => {
|
|
8646
|
+
if (this.ws && this.ws.readyState !== WebSocket.OPEN) {
|
|
8647
|
+
this.logger.warn("[Server] WebSocket connection timeout");
|
|
8648
|
+
this.ws.terminate();
|
|
8649
|
+
}
|
|
8650
|
+
}, this.config.requestTimeoutMs);
|
|
8393
8651
|
this.ws.on("open", () => {
|
|
8652
|
+
clearTimeout(connectTimeout);
|
|
8394
8653
|
this.wsConnected = true;
|
|
8654
|
+
this.reconnectDelay = this.config.initialReconnectDelay;
|
|
8655
|
+
this.reconnectAttempts = 0;
|
|
8656
|
+
this.connectStartTimestamp = 0;
|
|
8657
|
+
this.hourlyCheckMode = false;
|
|
8658
|
+
this.cancelHourlyCheck();
|
|
8659
|
+
this.setConnectionState("online");
|
|
8395
8660
|
this.logger.info("[Server] WebSocket connected");
|
|
8396
8661
|
this.wsSend({
|
|
8397
8662
|
type: "online",
|
|
8398
8663
|
nodeId: this.store.agentId,
|
|
8399
|
-
publicKey: Buffer.from(this.store.identityKeys.publicKey).toString("base64")
|
|
8664
|
+
publicKey: Buffer.from(this.store.identityKeys.publicKey).toString("base64"),
|
|
8665
|
+
...this.authToken ? { token: this.authToken } : {}
|
|
8400
8666
|
});
|
|
8401
8667
|
this.startHeartbeat();
|
|
8402
8668
|
});
|
|
@@ -8409,29 +8675,31 @@ var ServerClient = class {
|
|
|
8409
8675
|
}
|
|
8410
8676
|
});
|
|
8411
8677
|
this.ws.on("close", (code, reason) => {
|
|
8678
|
+
clearTimeout(connectTimeout);
|
|
8412
8679
|
this.wsConnected = false;
|
|
8413
8680
|
this.logger.info("[Server] WebSocket closed:", code, reason.toString());
|
|
8414
8681
|
this.stopHeartbeat();
|
|
8682
|
+
this.setConnectionState("offline");
|
|
8415
8683
|
this.scheduleReconnect();
|
|
8416
8684
|
});
|
|
8417
8685
|
this.ws.on("error", (err) => {
|
|
8686
|
+
clearTimeout(connectTimeout);
|
|
8418
8687
|
this.logger.error("[Server] WebSocket error:", err.message);
|
|
8419
8688
|
});
|
|
8420
8689
|
}
|
|
8421
8690
|
/**
|
|
8422
|
-
* Disconnect the WebSocket.
|
|
8691
|
+
* Disconnect the WebSocket and stop all reconnection attempts.
|
|
8423
8692
|
*/
|
|
8424
8693
|
disconnectWebSocket() {
|
|
8425
8694
|
this.stopHeartbeat();
|
|
8426
|
-
|
|
8427
|
-
clearTimeout(this.wsReconnectTimer);
|
|
8428
|
-
this.wsReconnectTimer = null;
|
|
8429
|
-
}
|
|
8695
|
+
this.cancelReconnect();
|
|
8430
8696
|
if (this.ws) {
|
|
8431
8697
|
this.ws.close();
|
|
8432
8698
|
this.ws = null;
|
|
8433
8699
|
}
|
|
8434
8700
|
this.wsConnected = false;
|
|
8701
|
+
this.connectStartTimestamp = 0;
|
|
8702
|
+
this.setConnectionState("offline");
|
|
8435
8703
|
}
|
|
8436
8704
|
/**
|
|
8437
8705
|
* Check if the WebSocket is connected.
|
|
@@ -8449,25 +8717,114 @@ var ServerClient = class {
|
|
|
8449
8717
|
}
|
|
8450
8718
|
/**
|
|
8451
8719
|
* Send a JSON message over the WebSocket.
|
|
8720
|
+
* Returns true if the message was sent, false if offline.
|
|
8452
8721
|
*/
|
|
8453
8722
|
wsSend(data) {
|
|
8454
8723
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
8455
8724
|
this.ws.send(JSON.stringify(data));
|
|
8456
|
-
|
|
8457
|
-
this.logger.warn("[Server] Cannot send \u2014 WebSocket not open");
|
|
8725
|
+
return true;
|
|
8458
8726
|
}
|
|
8727
|
+
this.logger.warn("[Server] Cannot send \u2014 WebSocket not open");
|
|
8728
|
+
return false;
|
|
8729
|
+
}
|
|
8730
|
+
// ----------------------------------------------------------------
|
|
8731
|
+
// Reconnection: try for 1 min, then hourly
|
|
8732
|
+
// ----------------------------------------------------------------
|
|
8733
|
+
/**
|
|
8734
|
+
* Schedule a reconnection attempt.
|
|
8735
|
+
*
|
|
8736
|
+
* Phase 1 (initial retry window, default 1 minute):
|
|
8737
|
+
* Retries with exponential backoff (1s → 2s → 4s → ... → 60s max).
|
|
8738
|
+
* Once the window expires, stops retrying and switches to hourly checks.
|
|
8739
|
+
*
|
|
8740
|
+
* Phase 2 (hourly check mode):
|
|
8741
|
+
* After the initial burst fails, sets up a timer that retries once
|
|
8742
|
+
* every hour. If a retry succeeds, hourly mode is cancelled and
|
|
8743
|
+
* normal operation resumes.
|
|
8744
|
+
*/
|
|
8745
|
+
scheduleReconnect() {
|
|
8746
|
+
if (this.hourlyCheckMode) {
|
|
8747
|
+
this.logger.info("[Server] Hourly check mode active \u2014 next attempt scheduled automatically");
|
|
8748
|
+
return;
|
|
8749
|
+
}
|
|
8750
|
+
if (this.isInitialRetryWindowExpired()) {
|
|
8751
|
+
this.logger.warn(`[Server] Initial retry window (${this.config.initialRetryWindowMs / 1e3}s) expired after ${this.reconnectAttempts} attempts. Switching to hourly check mode.`);
|
|
8752
|
+
this.enterHourlyCheckMode();
|
|
8753
|
+
return;
|
|
8754
|
+
}
|
|
8755
|
+
if (this.wsReconnectTimer)
|
|
8756
|
+
return;
|
|
8757
|
+
const jitter = 0.75 + Math.random() * 0.5;
|
|
8758
|
+
const delay = Math.min(this.reconnectDelay * jitter, this.config.maxReconnectDelay);
|
|
8759
|
+
this.reconnectAttempts++;
|
|
8760
|
+
this.logger.info(`[Server] Reconnecting in ${Math.round(delay)}ms (attempt #${this.reconnectAttempts})`);
|
|
8761
|
+
this.setConnectionState("reconnecting");
|
|
8762
|
+
this.wsReconnectTimer = setTimeout(() => {
|
|
8763
|
+
this.wsReconnectTimer = null;
|
|
8764
|
+
this.logger.info("[Server] Attempting WebSocket reconnect...");
|
|
8765
|
+
this.connectWebSocket();
|
|
8766
|
+
}, delay);
|
|
8767
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * this.config.reconnectBackoffFactor, this.config.maxReconnectDelay);
|
|
8768
|
+
}
|
|
8769
|
+
/**
|
|
8770
|
+
* Enter hourly check mode: stop aggressive retries,
|
|
8771
|
+
* set up a single timer that fires once per hour.
|
|
8772
|
+
*/
|
|
8773
|
+
enterHourlyCheckMode() {
|
|
8774
|
+
this.hourlyCheckMode = true;
|
|
8775
|
+
this.reconnectDelay = this.config.initialReconnectDelay;
|
|
8776
|
+
this.reconnectAttempts = 0;
|
|
8777
|
+
if (this.wsReconnectTimer) {
|
|
8778
|
+
clearTimeout(this.wsReconnectTimer);
|
|
8779
|
+
this.wsReconnectTimer = null;
|
|
8780
|
+
}
|
|
8781
|
+
this.setConnectionState("offline");
|
|
8782
|
+
this.logger.info(`[Server] Entering hourly check mode \u2014 will retry every ${this.config.hourlyCheckIntervalMs / 6e4} minutes`);
|
|
8783
|
+
this.hourlyCheckTimer = setTimeout(() => {
|
|
8784
|
+
this.hourlyCheckTimer = null;
|
|
8785
|
+
this.logger.info("[Server] Hourly check: attempting to reconnect...");
|
|
8786
|
+
this.connectWebSocket();
|
|
8787
|
+
}, this.config.hourlyCheckIntervalMs);
|
|
8788
|
+
}
|
|
8789
|
+
/**
|
|
8790
|
+
* Cancel the hourly check timer.
|
|
8791
|
+
*/
|
|
8792
|
+
cancelHourlyCheck() {
|
|
8793
|
+
if (this.hourlyCheckTimer) {
|
|
8794
|
+
clearTimeout(this.hourlyCheckTimer);
|
|
8795
|
+
this.hourlyCheckTimer = null;
|
|
8796
|
+
}
|
|
8797
|
+
this.hourlyCheckMode = false;
|
|
8798
|
+
}
|
|
8799
|
+
/**
|
|
8800
|
+
* Cancel a pending reconnection.
|
|
8801
|
+
*/
|
|
8802
|
+
cancelReconnect() {
|
|
8803
|
+
if (this.wsReconnectTimer) {
|
|
8804
|
+
clearTimeout(this.wsReconnectTimer);
|
|
8805
|
+
this.wsReconnectTimer = null;
|
|
8806
|
+
}
|
|
8807
|
+
this.cancelHourlyCheck();
|
|
8808
|
+
this.reconnectDelay = this.config.initialReconnectDelay;
|
|
8809
|
+
this.reconnectAttempts = 0;
|
|
8810
|
+
this.connectStartTimestamp = 0;
|
|
8459
8811
|
}
|
|
8460
8812
|
// ----------------------------------------------------------------
|
|
8461
8813
|
// REST API methods
|
|
8462
8814
|
// ----------------------------------------------------------------
|
|
8463
8815
|
/**
|
|
8464
8816
|
* Register this node on the server.
|
|
8817
|
+
* Captures JWT token from response if returned.
|
|
8465
8818
|
*/
|
|
8466
8819
|
async registerNode(agentId, publicKey) {
|
|
8467
|
-
|
|
8820
|
+
const res = await this.fetchPost("/api/v1/node/register", {
|
|
8468
8821
|
id: agentId,
|
|
8469
8822
|
publicKey: Buffer.from(publicKey).toString("base64")
|
|
8470
8823
|
});
|
|
8824
|
+
if (res?.token) {
|
|
8825
|
+
this.setAuthToken(res.token);
|
|
8826
|
+
}
|
|
8827
|
+
return res?.ok ?? false;
|
|
8471
8828
|
}
|
|
8472
8829
|
/**
|
|
8473
8830
|
* Request a temporary 6-digit number for friend discovery.
|
|
@@ -8570,9 +8927,10 @@ var ServerClient = class {
|
|
|
8570
8927
|
}
|
|
8571
8928
|
/**
|
|
8572
8929
|
* Send a relay message via the server (WebSocket fallback for P2P).
|
|
8930
|
+
* Returns true if sent, false if offline.
|
|
8573
8931
|
*/
|
|
8574
8932
|
sendRelayMessage(targetId, payload) {
|
|
8575
|
-
this.wsSend({
|
|
8933
|
+
return this.wsSend({
|
|
8576
8934
|
type: "relay",
|
|
8577
8935
|
targetId,
|
|
8578
8936
|
payload,
|
|
@@ -8585,26 +8943,43 @@ var ServerClient = class {
|
|
|
8585
8943
|
async fetchPost(path7, body) {
|
|
8586
8944
|
const url = this.serverUrl + path7;
|
|
8587
8945
|
try {
|
|
8946
|
+
const controller = new AbortController();
|
|
8947
|
+
const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
|
|
8588
8948
|
const resp = await fetch(url, {
|
|
8589
8949
|
method: "POST",
|
|
8590
|
-
headers:
|
|
8591
|
-
body: JSON.stringify(body)
|
|
8950
|
+
headers: this.authHeaders(),
|
|
8951
|
+
body: JSON.stringify(body),
|
|
8952
|
+
signal: controller.signal
|
|
8592
8953
|
});
|
|
8954
|
+
clearTimeout(timeout);
|
|
8593
8955
|
if (!resp.ok) {
|
|
8956
|
+
if (resp.status === 401 && this.authToken) {
|
|
8957
|
+
this.logger.warn(`[Server] 401 Unauthorized on ${path7} \u2014 token may be expired`);
|
|
8958
|
+
}
|
|
8594
8959
|
const text = await resp.text();
|
|
8595
8960
|
this.logger.error(`[Server] API error ${resp.status} on ${path7}: ${text}`);
|
|
8596
8961
|
return null;
|
|
8597
8962
|
}
|
|
8598
8963
|
return await resp.json();
|
|
8599
8964
|
} catch (err) {
|
|
8600
|
-
|
|
8965
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
8966
|
+
this.logger.error(`[Server] API request timeout for ${path7}`);
|
|
8967
|
+
} else {
|
|
8968
|
+
this.logger.error(`[Server] API request failed for ${path7}:`, err);
|
|
8969
|
+
}
|
|
8601
8970
|
return null;
|
|
8602
8971
|
}
|
|
8603
8972
|
}
|
|
8604
8973
|
async fetchGet(path7) {
|
|
8605
8974
|
const url = this.serverUrl + path7;
|
|
8606
8975
|
try {
|
|
8607
|
-
const
|
|
8976
|
+
const controller = new AbortController();
|
|
8977
|
+
const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
|
|
8978
|
+
const resp = await fetch(url, {
|
|
8979
|
+
signal: controller.signal,
|
|
8980
|
+
headers: this.authToken ? { Authorization: "Bearer " + this.authToken } : {}
|
|
8981
|
+
});
|
|
8982
|
+
clearTimeout(timeout);
|
|
8608
8983
|
if (!resp.ok) {
|
|
8609
8984
|
const text = await resp.text();
|
|
8610
8985
|
this.logger.error(`[Server] API error ${resp.status} on ${path7}: ${text}`);
|
|
@@ -8612,18 +8987,26 @@ var ServerClient = class {
|
|
|
8612
8987
|
}
|
|
8613
8988
|
return await resp.json();
|
|
8614
8989
|
} catch (err) {
|
|
8615
|
-
|
|
8990
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
8991
|
+
this.logger.error(`[Server] GET request timeout for ${path7}`);
|
|
8992
|
+
} else {
|
|
8993
|
+
this.logger.error(`[Server] GET request failed for ${path7}:`, err);
|
|
8994
|
+
}
|
|
8616
8995
|
return null;
|
|
8617
8996
|
}
|
|
8618
8997
|
}
|
|
8619
8998
|
async del(path7, body) {
|
|
8620
8999
|
const url = this.serverUrl + path7;
|
|
8621
9000
|
try {
|
|
9001
|
+
const controller = new AbortController();
|
|
9002
|
+
const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
|
|
8622
9003
|
const resp = await fetch(url, {
|
|
8623
9004
|
method: "DELETE",
|
|
8624
|
-
headers:
|
|
8625
|
-
body: body ? JSON.stringify(body) : void 0
|
|
9005
|
+
headers: this.authHeaders(),
|
|
9006
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
9007
|
+
signal: controller.signal
|
|
8626
9008
|
});
|
|
9009
|
+
clearTimeout(timeout);
|
|
8627
9010
|
return resp.ok;
|
|
8628
9011
|
} catch (err) {
|
|
8629
9012
|
this.logger.error(`[Server] DELETE request failed for ${path7}:`, err);
|
|
@@ -8633,11 +9016,15 @@ var ServerClient = class {
|
|
|
8633
9016
|
async post(path7, body) {
|
|
8634
9017
|
const url = this.serverUrl + path7;
|
|
8635
9018
|
try {
|
|
9019
|
+
const controller = new AbortController();
|
|
9020
|
+
const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
|
|
8636
9021
|
const resp = await fetch(url, {
|
|
8637
9022
|
method: "POST",
|
|
8638
|
-
headers:
|
|
8639
|
-
body: JSON.stringify(body)
|
|
9023
|
+
headers: this.authHeaders(),
|
|
9024
|
+
body: JSON.stringify(body),
|
|
9025
|
+
signal: controller.signal
|
|
8640
9026
|
});
|
|
9027
|
+
clearTimeout(timeout);
|
|
8641
9028
|
return resp.ok;
|
|
8642
9029
|
} catch (err) {
|
|
8643
9030
|
this.logger.error(`[Server] API request failed for ${path7}:`, err);
|
|
@@ -8658,9 +9045,10 @@ var ServerClient = class {
|
|
|
8658
9045
|
}
|
|
8659
9046
|
}
|
|
8660
9047
|
startHeartbeat() {
|
|
9048
|
+
this.stopHeartbeat();
|
|
8661
9049
|
this.heartbeatTimer = setInterval(() => {
|
|
8662
9050
|
this.wsSend({ type: "ping", agentId: this.store.agentId, timestamp: Date.now() });
|
|
8663
|
-
},
|
|
9051
|
+
}, this.config.heartbeatIntervalMs);
|
|
8664
9052
|
}
|
|
8665
9053
|
stopHeartbeat() {
|
|
8666
9054
|
if (this.heartbeatTimer) {
|
|
@@ -8668,20 +9056,11 @@ var ServerClient = class {
|
|
|
8668
9056
|
this.heartbeatTimer = null;
|
|
8669
9057
|
}
|
|
8670
9058
|
}
|
|
8671
|
-
scheduleReconnect() {
|
|
8672
|
-
if (this.wsReconnectTimer)
|
|
8673
|
-
return;
|
|
8674
|
-
this.wsReconnectTimer = setTimeout(() => {
|
|
8675
|
-
this.wsReconnectTimer = null;
|
|
8676
|
-
this.logger.info("[Server] Attempting WebSocket reconnect...");
|
|
8677
|
-
this.connectWebSocket();
|
|
8678
|
-
}, 5e3);
|
|
8679
|
-
}
|
|
8680
9059
|
};
|
|
8681
9060
|
|
|
8682
9061
|
// dist/handshake/handshakeManager.js
|
|
8683
9062
|
var import_crypto4 = __toESM(require_dist(), 1);
|
|
8684
|
-
import * as
|
|
9063
|
+
import * as crypto6 from "crypto";
|
|
8685
9064
|
var HandshakeManager = class {
|
|
8686
9065
|
constructor(store, serverClient, config2, logger) {
|
|
8687
9066
|
this.pendingInitiates = /* @__PURE__ */ new Map();
|
|
@@ -8899,7 +9278,7 @@ var HandshakeManager = class {
|
|
|
8899
9278
|
combined.set(ee, 0);
|
|
8900
9279
|
combined.set(se, 32);
|
|
8901
9280
|
combined.set(es, 64);
|
|
8902
|
-
const nonce =
|
|
9281
|
+
const nonce = crypto6.randomBytes(16).toString("hex");
|
|
8903
9282
|
const newSessionKey = (0, import_crypto4.deriveSessionKey)(combined, "aicq-session-rotate-" + nonce);
|
|
8904
9283
|
const updatedSession = {
|
|
8905
9284
|
peerId,
|
|
@@ -9335,12 +9714,18 @@ var EncryptedChatChannel = class {
|
|
|
9335
9714
|
this.api = null;
|
|
9336
9715
|
this.dataDir = "";
|
|
9337
9716
|
this.fileChunkBuffers = /* @__PURE__ */ new Map();
|
|
9717
|
+
this.flushingOffline = false;
|
|
9338
9718
|
this.store = store;
|
|
9339
9719
|
this.handshakeManager = handshakeManager;
|
|
9340
9720
|
this.p2pManager = p2pManager;
|
|
9341
9721
|
this.serverClient = serverClient;
|
|
9342
9722
|
this.logger = logger;
|
|
9343
9723
|
this.dataDir = dataDir;
|
|
9724
|
+
this.serverClient.onConnectionStateChange((newState, _prevState) => {
|
|
9725
|
+
if (newState === "online") {
|
|
9726
|
+
this.onReconnected();
|
|
9727
|
+
}
|
|
9728
|
+
});
|
|
9344
9729
|
}
|
|
9345
9730
|
/**
|
|
9346
9731
|
* Set the OpenClaw API reference (for emitting events).
|
|
@@ -9348,6 +9733,17 @@ var EncryptedChatChannel = class {
|
|
|
9348
9733
|
setAPI(api) {
|
|
9349
9734
|
this.api = api;
|
|
9350
9735
|
}
|
|
9736
|
+
/**
|
|
9737
|
+
* Called when the WebSocket connection is re-established.
|
|
9738
|
+
* Flushes all queued offline messages.
|
|
9739
|
+
*/
|
|
9740
|
+
onReconnected() {
|
|
9741
|
+
const pendingCount = this.store.getOfflineMessageCount();
|
|
9742
|
+
if (pendingCount > 0) {
|
|
9743
|
+
this.logger.info(`[Chat] Back online \u2014 flushing ${pendingCount} queued offline messages`);
|
|
9744
|
+
this.flushOfflineMessages();
|
|
9745
|
+
}
|
|
9746
|
+
}
|
|
9351
9747
|
/**
|
|
9352
9748
|
* Handle an incoming encrypted message from a peer.
|
|
9353
9749
|
*
|
|
@@ -9382,7 +9778,7 @@ var EncryptedChatChannel = class {
|
|
|
9382
9778
|
});
|
|
9383
9779
|
}
|
|
9384
9780
|
friend.lastMessageAt = /* @__PURE__ */ new Date();
|
|
9385
|
-
this.store.
|
|
9781
|
+
this.store.markDirty();
|
|
9386
9782
|
}
|
|
9387
9783
|
/**
|
|
9388
9784
|
* Send an encrypted message to a peer.
|
|
@@ -9390,7 +9786,9 @@ var EncryptedChatChannel = class {
|
|
|
9390
9786
|
* Encrypts with the session key, signs with the Ed25519 identity key,
|
|
9391
9787
|
* and sends via P2P if available, falling back to WebSocket relay.
|
|
9392
9788
|
*
|
|
9393
|
-
*
|
|
9789
|
+
* When offline, the message is queued for later delivery.
|
|
9790
|
+
*
|
|
9791
|
+
* @returns true if the message was sent or queued successfully
|
|
9394
9792
|
*/
|
|
9395
9793
|
send(toId, plaintext) {
|
|
9396
9794
|
const friend = this.store.getFriend(toId);
|
|
@@ -9408,14 +9806,26 @@ var EncryptedChatChannel = class {
|
|
|
9408
9806
|
}
|
|
9409
9807
|
const wireData = (0, import_crypto6.encryptMessage)(plaintext, sessionKey, this.store.identityKeys.secretKey, this.store.identityKeys.publicKey);
|
|
9410
9808
|
const buf = Buffer.from(wireData);
|
|
9411
|
-
|
|
9412
|
-
|
|
9809
|
+
const encodedData = (0, import_crypto6.encodeBase64)(wireData);
|
|
9810
|
+
const isOnline = this.serverClient.isConnected();
|
|
9811
|
+
if (isOnline) {
|
|
9812
|
+
if (this.p2pManager.isConnected(toId) && this.p2pManager.send(toId, buf)) {
|
|
9813
|
+
this.logger.debug("[Chat] Sent message via P2P to " + toId);
|
|
9814
|
+
} else {
|
|
9815
|
+
const sent = this.serverClient.sendRelayMessage(toId, {
|
|
9816
|
+
channel: "encrypted-chat",
|
|
9817
|
+
data: encodedData
|
|
9818
|
+
});
|
|
9819
|
+
if (!sent) {
|
|
9820
|
+
this.logger.warn("[Chat] WebSocket send failed \u2014 queuing message for offline delivery");
|
|
9821
|
+
this.store.enqueueOfflineMessage(toId, encodedData);
|
|
9822
|
+
} else {
|
|
9823
|
+
this.logger.debug("[Chat] Sent message via relay to " + toId);
|
|
9824
|
+
}
|
|
9825
|
+
}
|
|
9413
9826
|
} else {
|
|
9414
|
-
this.
|
|
9415
|
-
|
|
9416
|
-
data: (0, import_crypto6.encodeBase64)(wireData)
|
|
9417
|
-
});
|
|
9418
|
-
this.logger.debug("[Chat] Sent message via relay to " + toId);
|
|
9827
|
+
this.logger.info("[Chat] Offline \u2014 message queued for delivery to " + toId);
|
|
9828
|
+
this.store.enqueueOfflineMessage(toId, encodedData);
|
|
9419
9829
|
}
|
|
9420
9830
|
const session = this.store.getSession(toId);
|
|
9421
9831
|
if (session) {
|
|
@@ -9423,10 +9833,71 @@ var EncryptedChatChannel = class {
|
|
|
9423
9833
|
if (session.messageCount % 100 === 0 || Date.now() - session.createdAt.getTime() > 36e5) {
|
|
9424
9834
|
this.handshakeManager.rotateSessionKey(toId);
|
|
9425
9835
|
}
|
|
9426
|
-
this.store.
|
|
9836
|
+
this.store.markDirty();
|
|
9427
9837
|
}
|
|
9428
9838
|
return true;
|
|
9429
9839
|
}
|
|
9840
|
+
/**
|
|
9841
|
+
* Flush all queued offline messages.
|
|
9842
|
+
* Called automatically when connection is re-established.
|
|
9843
|
+
*/
|
|
9844
|
+
flushOfflineMessages() {
|
|
9845
|
+
if (this.flushingOffline)
|
|
9846
|
+
return;
|
|
9847
|
+
this.flushingOffline = true;
|
|
9848
|
+
const flushNext = () => {
|
|
9849
|
+
const msg = this.store.dequeueOfflineMessage();
|
|
9850
|
+
if (!msg) {
|
|
9851
|
+
this.flushingOffline = false;
|
|
9852
|
+
const remaining = this.store.getOfflineMessageCount();
|
|
9853
|
+
if (remaining === 0) {
|
|
9854
|
+
this.logger.info("[Chat] All offline messages flushed");
|
|
9855
|
+
}
|
|
9856
|
+
return;
|
|
9857
|
+
}
|
|
9858
|
+
if (!this.serverClient.isConnected()) {
|
|
9859
|
+
this.store.offlineMessages.unshift(msg);
|
|
9860
|
+
this.flushingOffline = false;
|
|
9861
|
+
this.logger.warn("[Chat] Connection lost during offline flush");
|
|
9862
|
+
return;
|
|
9863
|
+
}
|
|
9864
|
+
const friend = this.store.getFriend(msg.targetId);
|
|
9865
|
+
let sessionKey = this.handshakeManager.getSessionKey(msg.targetId);
|
|
9866
|
+
if (!sessionKey && friend?.sessionKey) {
|
|
9867
|
+
sessionKey = friend.sessionKey;
|
|
9868
|
+
}
|
|
9869
|
+
if (!sessionKey) {
|
|
9870
|
+
this.logger.warn("[Chat] Skipping offline message to " + msg.targetId + " (no session key)");
|
|
9871
|
+
this.store.markDirty();
|
|
9872
|
+
setImmediate(flushNext);
|
|
9873
|
+
return;
|
|
9874
|
+
}
|
|
9875
|
+
const sent = this.serverClient.sendRelayMessage(msg.targetId, {
|
|
9876
|
+
channel: "encrypted-chat",
|
|
9877
|
+
data: msg.encryptedData
|
|
9878
|
+
});
|
|
9879
|
+
if (sent) {
|
|
9880
|
+
this.logger.debug("[Chat] Flushed offline message to " + msg.targetId);
|
|
9881
|
+
} else {
|
|
9882
|
+
this.logger.warn("[Chat] Failed to flush offline message to " + msg.targetId);
|
|
9883
|
+
msg.retryCount++;
|
|
9884
|
+
if (msg.retryCount < msg.maxRetries) {
|
|
9885
|
+
this.store.offlineMessages.unshift(msg);
|
|
9886
|
+
} else {
|
|
9887
|
+
this.logger.error("[Chat] Dropped offline message to " + msg.targetId + " (max retries exceeded)");
|
|
9888
|
+
}
|
|
9889
|
+
}
|
|
9890
|
+
this.store.markDirty();
|
|
9891
|
+
setImmediate(flushNext);
|
|
9892
|
+
};
|
|
9893
|
+
flushNext();
|
|
9894
|
+
}
|
|
9895
|
+
/**
|
|
9896
|
+
* Get the count of pending offline messages.
|
|
9897
|
+
*/
|
|
9898
|
+
getPendingOfflineCount() {
|
|
9899
|
+
return this.store.getOfflineMessageCount();
|
|
9900
|
+
}
|
|
9430
9901
|
/**
|
|
9431
9902
|
* Handle an incoming file chunk from a peer.
|
|
9432
9903
|
*
|
|
@@ -9511,6 +9982,7 @@ var EncryptedChatChannel = class {
|
|
|
9511
9982
|
*/
|
|
9512
9983
|
cleanup() {
|
|
9513
9984
|
this.fileChunkBuffers.clear();
|
|
9985
|
+
this.flushingOffline = false;
|
|
9514
9986
|
}
|
|
9515
9987
|
};
|
|
9516
9988
|
|
|
@@ -9820,17 +10292,17 @@ var MessageSendingHook = class {
|
|
|
9820
10292
|
var CSS = `
|
|
9821
10293
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9822
10294
|
:root {
|
|
9823
|
-
--bg: #
|
|
9824
|
-
--bg5: #
|
|
9825
|
-
--accent: #
|
|
9826
|
-
--ok: #
|
|
9827
|
-
--danger: #ef4444; --danger-bg: rgba(239,68,68,.
|
|
9828
|
-
--border: #
|
|
10295
|
+
--bg: #f5f7fa; --bg2: #ffffff; --bg3: #f0f2f5; --bg4: #e4e7ec;
|
|
10296
|
+
--bg5: #d1d5db; --text: #1f2937; --text2: #6b7280; --text3: #9ca3af;
|
|
10297
|
+
--accent: #4f46e5; --accent2: #6366f1; --accent-bg: rgba(79,70,229,.08);
|
|
10298
|
+
--ok: #10b981; --ok-bg: rgba(16,185,129,.08); --warn: #f59e0b; --warn-bg: rgba(245,158,11,.08);
|
|
10299
|
+
--danger: #ef4444; --danger-bg: rgba(239,68,68,.08); --info: #3b82f6; --info-bg: rgba(59,130,246,.08);
|
|
10300
|
+
--border: #e5e7eb; --radius: 8px; --radius-lg: 12px; --shadow: 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04);
|
|
9829
10301
|
--sidebar-w: 240px; --header-h: 56px;
|
|
9830
10302
|
--transition: .2s cubic-bezier(.4,0,.2,1);
|
|
9831
10303
|
}
|
|
9832
10304
|
html, body { height: 100%; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: var(--text); font-size: 14px; line-height: 1.6; overflow: hidden; }
|
|
9833
|
-
a { color: var(--
|
|
10305
|
+
a { color: var(--accent); text-decoration: none; }
|
|
9834
10306
|
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
9835
10307
|
::-webkit-scrollbar-track { background: transparent; }
|
|
9836
10308
|
::-webkit-scrollbar-thumb { background: var(--bg4); border-radius: 3px; }
|
|
@@ -9842,7 +10314,7 @@ a { color: var(--info); text-decoration: none; }
|
|
|
9842
10314
|
/* Sidebar */
|
|
9843
10315
|
.sidebar {
|
|
9844
10316
|
width: var(--sidebar-w); min-width: var(--sidebar-w); height: 100vh;
|
|
9845
|
-
background:
|
|
10317
|
+
background: #ffffff; border-right: 1px solid var(--border);
|
|
9846
10318
|
display: flex; flex-direction: column; transition: width var(--transition), min-width var(--transition);
|
|
9847
10319
|
z-index: 20; overflow: hidden;
|
|
9848
10320
|
}
|
|
@@ -9857,7 +10329,7 @@ a { color: var(--info); text-decoration: none; }
|
|
|
9857
10329
|
border-bottom: 1px solid var(--border); min-height: var(--header-h);
|
|
9858
10330
|
}
|
|
9859
10331
|
.sidebar-logo {
|
|
9860
|
-
width: 32px; height: 32px; border-radius: 8px; background: linear-gradient(135deg, var(--accent), #
|
|
10332
|
+
width: 32px; height: 32px; border-radius: 8px; background: linear-gradient(135deg, var(--accent), #7c3aed);
|
|
9861
10333
|
display: grid; place-items: center; font-size: 13px; font-weight: 800; color: #fff; flex-shrink: 0;
|
|
9862
10334
|
}
|
|
9863
10335
|
.sidebar-header-text h1 { font-size: 14px; font-weight: 700; line-height: 1.2; }
|
|
@@ -9896,7 +10368,7 @@ a { color: var(--info); text-decoration: none; }
|
|
|
9896
10368
|
.main-header {
|
|
9897
10369
|
height: var(--header-h); min-height: var(--header-h);
|
|
9898
10370
|
display: flex; align-items: center; gap: 16px; padding: 0 24px;
|
|
9899
|
-
background:
|
|
10371
|
+
background: #ffffff; border-bottom: 1px solid var(--border);
|
|
9900
10372
|
}
|
|
9901
10373
|
.toggle-btn {
|
|
9902
10374
|
width: 32px; height: 32px; border-radius: 6px; background: var(--bg3);
|
|
@@ -9928,12 +10400,12 @@ a { color: var(--info); text-decoration: none; }
|
|
|
9928
10400
|
.btn-default:hover:not(:disabled) { background: var(--bg4); }
|
|
9929
10401
|
.btn-primary { background: var(--accent); color: #fff; }
|
|
9930
10402
|
.btn-primary:hover:not(:disabled) { background: var(--accent2); }
|
|
9931
|
-
.btn-danger { background: var(--danger-bg); color: #
|
|
9932
|
-
.btn-danger:hover:not(:disabled) { background: rgba(239,68,68,.
|
|
9933
|
-
.btn-ok { background: var(--ok-bg); color: #
|
|
9934
|
-
.btn-ok:hover:not(:disabled) { background: rgba(
|
|
9935
|
-
.btn-warn { background: var(--warn-bg); color: #
|
|
9936
|
-
.btn-warn:hover:not(:disabled) { background: rgba(
|
|
10403
|
+
.btn-danger { background: var(--danger-bg); color: #dc2626; border: 1px solid rgba(239,68,68,.15); }
|
|
10404
|
+
.btn-danger:hover:not(:disabled) { background: rgba(239,68,68,.15); }
|
|
10405
|
+
.btn-ok { background: var(--ok-bg); color: #059669; border: 1px solid rgba(16,185,129,.15); }
|
|
10406
|
+
.btn-ok:hover:not(:disabled) { background: rgba(16,185,129,.15); }
|
|
10407
|
+
.btn-warn { background: var(--warn-bg); color: #d97706; border: 1px solid rgba(245,158,11,.15); }
|
|
10408
|
+
.btn-warn:hover:not(:disabled) { background: rgba(245,158,11,.15); }
|
|
9937
10409
|
.btn-ghost { background: transparent; color: var(--text2); }
|
|
9938
10410
|
.btn-ghost:hover:not(:disabled) { background: var(--bg3); color: var(--text); }
|
|
9939
10411
|
.btn-sm { padding: 4px 10px; font-size: 12px; }
|
|
@@ -10006,17 +10478,18 @@ tbody tr:hover { background: var(--bg3); }
|
|
|
10006
10478
|
.provider-card .prov-desc { font-size: 12px; color: var(--text3); margin-bottom: 10px; }
|
|
10007
10479
|
.provider-card .prov-model { font-size: 11px; color: var(--text2); background: var(--bg3); padding: 3px 8px; border-radius: 4px; display: inline-block; }
|
|
10008
10480
|
.provider-card .prov-actions { margin-top: 12px; display: flex; gap: 6px; }
|
|
10481
|
+
.provider-card.custom-provider { border-color: var(--accent); border-style: dashed; }
|
|
10009
10482
|
|
|
10010
10483
|
/* Modal */
|
|
10011
10484
|
.modal-overlay {
|
|
10012
|
-
position: fixed; inset: 0; background: rgba(0,0,0,.
|
|
10485
|
+
position: fixed; inset: 0; background: rgba(0,0,0,.3); display: flex;
|
|
10013
10486
|
align-items: center; justify-content: center; z-index: 100;
|
|
10014
10487
|
animation: fadeIn .15s ease-out;
|
|
10015
10488
|
}
|
|
10016
10489
|
.modal-overlay.hidden { display: none; }
|
|
10017
10490
|
.modal {
|
|
10018
10491
|
background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius-lg);
|
|
10019
|
-
padding: 28px; width: 90%; max-width: 520px; box-shadow: 0
|
|
10492
|
+
padding: 28px; width: 90%; max-width: 520px; box-shadow: 0 10px 25px rgba(0,0,0,.1), 0 6px 10px rgba(0,0,0,.06);
|
|
10020
10493
|
max-height: 85vh; overflow-y: auto; animation: modalIn .2s ease-out;
|
|
10021
10494
|
}
|
|
10022
10495
|
@keyframes modalIn { from { transform: scale(.95); opacity: 0; } to { transform: scale(1); opacity: 1; } }
|
|
@@ -10052,15 +10525,15 @@ tbody tr:hover { background: var(--bg3); }
|
|
|
10052
10525
|
/* Toast */
|
|
10053
10526
|
.toast-container { position: fixed; bottom: 20px; right: 20px; z-index: 200; display: flex; flex-direction: column; gap: 8px; }
|
|
10054
10527
|
.toast {
|
|
10055
|
-
padding: 12px 20px; border-radius: var(--radius); color:
|
|
10056
|
-
animation: slideIn .2s ease-out; box-shadow:
|
|
10528
|
+
padding: 12px 20px; border-radius: var(--radius); color: var(--text); font-size: 13px;
|
|
10529
|
+
animation: slideIn .2s ease-out; box-shadow: 0 4px 12px rgba(0,0,0,.08); display: flex; align-items: center; gap: 8px;
|
|
10057
10530
|
max-width: 400px;
|
|
10058
10531
|
}
|
|
10059
10532
|
.toast.hidden { display: none; }
|
|
10060
|
-
.toast-ok { background: #
|
|
10061
|
-
.toast-err { background: #
|
|
10062
|
-
.toast-info { background: #
|
|
10063
|
-
.toast-warn { background: #
|
|
10533
|
+
.toast-ok { background: #ecfdf5; border: 1px solid #a7f3d0; color: #065f46; }
|
|
10534
|
+
.toast-err { background: #fef2f2; border: 1px solid #fecaca; color: #991b1b; }
|
|
10535
|
+
.toast-info { background: #eff6ff; border: 1px solid #bfdbfe; color: #1e3a5f; }
|
|
10536
|
+
.toast-warn { background: #fffbeb; border: 1px solid #fde68a; color: #92400e; }
|
|
10064
10537
|
@keyframes slideIn { from { transform: translateX(20px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
|
10065
10538
|
|
|
10066
10539
|
/* Actions cell */
|
|
@@ -10092,8 +10565,9 @@ tbody tr:hover { background: var(--bg3); }
|
|
|
10092
10565
|
|
|
10093
10566
|
/* Offline banner */
|
|
10094
10567
|
.offline-banner {
|
|
10095
|
-
background:
|
|
10096
|
-
color: #
|
|
10568
|
+
background: #fef2f2;
|
|
10569
|
+
color: #991b1b;
|
|
10570
|
+
border-bottom: 1px solid #fecaca;
|
|
10097
10571
|
padding: 10px 24px;
|
|
10098
10572
|
font-size: 13px;
|
|
10099
10573
|
display: flex;
|
|
@@ -10121,6 +10595,380 @@ tbody tr:hover { background: var(--bg3); }
|
|
|
10121
10595
|
}
|
|
10122
10596
|
`;
|
|
10123
10597
|
var JS = `
|
|
10598
|
+
// \u2500\u2500 i18n \u2500\u2500
|
|
10599
|
+
const _lang = 'zh'; // Default Chinese; set to 'en' to switch to English
|
|
10600
|
+
const _T = {
|
|
10601
|
+
// Sidebar
|
|
10602
|
+
nav_overview: { zh: '\u6982\u89C8', en: 'Overview' },
|
|
10603
|
+
nav_management: { zh: '\u7BA1\u7406', en: 'Management' },
|
|
10604
|
+
nav_system: { zh: '\u7CFB\u7EDF', en: 'System' },
|
|
10605
|
+
nav_dashboard: { zh: '\u4EEA\u8868\u76D8', en: 'Dashboard' },
|
|
10606
|
+
nav_agents: { zh: '\u667A\u80FD\u4F53', en: 'Agents' },
|
|
10607
|
+
nav_friends: { zh: '\u597D\u53CB', en: 'Friends' },
|
|
10608
|
+
nav_models: { zh: '\u6A21\u578B', en: 'Models' },
|
|
10609
|
+
nav_settings: { zh: '\u8BBE\u7F6E', en: 'Settings' },
|
|
10610
|
+
collapse_sidebar: { zh: '\u6536\u8D77\u4FA7\u680F', en: 'Collapse sidebar' },
|
|
10611
|
+
management_console: { zh: '\u7BA1\u7406\u63A7\u5236\u53F0', en: 'Management Console' },
|
|
10612
|
+
// Header
|
|
10613
|
+
connecting: { zh: '\u8FDE\u63A5\u4E2D...', en: 'Connecting...' },
|
|
10614
|
+
connected: { zh: '\u5DF2\u8FDE\u63A5', en: 'Connected' },
|
|
10615
|
+
disconnected: { zh: '\u5DF2\u65AD\u5F00', en: 'Disconnected' },
|
|
10616
|
+
refresh: { zh: '\u5237\u65B0', en: 'Refresh' },
|
|
10617
|
+
// Dashboard
|
|
10618
|
+
loading_dashboard: { zh: '\u6B63\u5728\u52A0\u8F7D\u4EEA\u8868\u76D8...', en: 'Loading dashboard...' },
|
|
10619
|
+
failed_connect: { zh: '\u65E0\u6CD5\u8FDE\u63A5\u5230 AICQ \u63D2\u4EF6', en: 'Failed to connect to AICQ plugin' },
|
|
10620
|
+
server_status: { zh: '\u670D\u52A1\u5668\u72B6\u6001', en: 'Server Status' },
|
|
10621
|
+
total_friends: { zh: '\u597D\u53CB\u603B\u6570', en: 'Total Friends' },
|
|
10622
|
+
active_sessions: { zh: '\u6D3B\u8DC3\u4F1A\u8BDD', en: 'Active Sessions' },
|
|
10623
|
+
encrypted_sessions: { zh: '\u52A0\u5BC6\u4F1A\u8BDD', en: 'Encrypted sessions' },
|
|
10624
|
+
agent_id: { zh: '\u667A\u80FD\u4F53 ID', en: 'Agent ID' },
|
|
10625
|
+
fingerprint: { zh: '\u6307\u7EB9', en: 'Fingerprint' },
|
|
10626
|
+
recent_friends: { zh: '\u6700\u8FD1\u597D\u53CB', en: 'Recent Friends' },
|
|
10627
|
+
view_all: { zh: '\u67E5\u770B\u5168\u90E8 \u2192', en: 'View All \u2192' },
|
|
10628
|
+
identity_info: { zh: '\u8EAB\u4EFD\u4FE1\u606F', en: 'Identity Info' },
|
|
10629
|
+
server_url: { zh: '\u670D\u52A1\u5668\u5730\u5740', en: 'Server URL' },
|
|
10630
|
+
connection: { zh: '\u8FDE\u63A5', en: 'Connection' },
|
|
10631
|
+
online: { zh: '\u5728\u7EBF', en: 'Online' },
|
|
10632
|
+
offline: { zh: '\u79BB\u7EBF', en: 'Offline' },
|
|
10633
|
+
plugin_version: { zh: '\u63D2\u4EF6\u7248\u672C', en: 'Plugin Version' },
|
|
10634
|
+
mgmt_ui_access: { zh: '\u7BA1\u7406\u754C\u9762\u8BBF\u95EE', en: 'Management UI Access' },
|
|
10635
|
+
current_url: { zh: '\u5F53\u524D\u5730\u5740', en: 'Current URL' },
|
|
10636
|
+
local_access: { zh: '\u672C\u5730\u8BBF\u95EE', en: 'Local Access' },
|
|
10637
|
+
open: { zh: '\u6253\u5F00', en: 'Open' },
|
|
10638
|
+
gateway_path: { zh: '\u7F51\u5173\u8DEF\u5F84', en: 'Gateway Path' },
|
|
10639
|
+
no_friends_yet: { zh: '\u6682\u65E0\u597D\u53CB', en: 'No friends yet' },
|
|
10640
|
+
// Agents
|
|
10641
|
+
loading_agents: { zh: '\u6B63\u5728\u52A0\u8F7D\u667A\u80FD\u4F53...', en: 'Loading agents...' },
|
|
10642
|
+
active: { zh: '\u542F\u7528', en: 'active' },
|
|
10643
|
+
disabled: { zh: '\u7981\u7528', en: 'disabled' },
|
|
10644
|
+
default_model: { zh: '\u9ED8\u8BA4', en: 'default' },
|
|
10645
|
+
no_agents_configured: { zh: '\u672A\u914D\u7F6E\u667A\u80FD\u4F53', en: 'No agents configured' },
|
|
10646
|
+
add_agents_hint: { zh: '\u8BF7\u5728 openclaw.json \u6216 stableclaw.json \u914D\u7F6E\u6587\u4EF6\u4E2D\u6DFB\u52A0\u667A\u80FD\u4F53', en: 'Add agents to your openclaw.json or stableclaw.json config file' },
|
|
10647
|
+
search_agents: { zh: '\u641C\u7D22\u667A\u80FD\u4F53...', en: 'Search agents...' },
|
|
10648
|
+
add_agent: { zh: '\u6DFB\u52A0\u667A\u80FD\u4F53', en: 'Add Agent' },
|
|
10649
|
+
agent_list_from: { zh: '\u667A\u80FD\u4F53\u5217\u8868\u6765\u81EA', en: 'Agent list from' },
|
|
10650
|
+
total_label: { zh: '\u603B\u8BA1', en: 'Total' },
|
|
10651
|
+
agents_configured: { zh: '\u4E2A\u667A\u80FD\u4F53\u5DF2\u914D\u7F6E', en: 'agents configured' },
|
|
10652
|
+
status: { zh: '\u72B6\u6001', en: 'Status' },
|
|
10653
|
+
agent: { zh: '\u667A\u80FD\u4F53', en: 'Agent' },
|
|
10654
|
+
model: { zh: '\u6A21\u578B', en: 'Model' },
|
|
10655
|
+
provider: { zh: '\u63D0\u4F9B\u5546', en: 'Provider' },
|
|
10656
|
+
system_prompt: { zh: '\u7CFB\u7EDF\u63D0\u793A\u8BCD', en: 'System Prompt' },
|
|
10657
|
+
actions: { zh: '\u64CD\u4F5C', en: 'Actions' },
|
|
10658
|
+
confirm_delete_agent: { zh: '\u786E\u5B9A\u8981\u5220\u9664\u8FD9\u4E2A\u667A\u80FD\u4F53\u5417\uFF1F', en: 'Are you sure you want to delete this agent?' },
|
|
10659
|
+
agent_deleted: { zh: '\u667A\u80FD\u4F53\u5DF2\u5220\u9664', en: 'Agent deleted' },
|
|
10660
|
+
delete_failed: { zh: '\u5220\u9664\u5931\u8D25', en: 'Delete failed' },
|
|
10661
|
+
default_model_badge: { zh: '\u9ED8\u8BA4', en: 'Default' },
|
|
10662
|
+
provider_model: { zh: '\u6765\u81EA\u6A21\u578B\u63D0\u4F9B\u5546', en: 'From model provider' },
|
|
10663
|
+
default_model_label: { zh: '\u9ED8\u8BA4\u6A21\u578B', en: 'Default Model' },
|
|
10664
|
+
set_as_default: { zh: '\u8BBE\u4E3A\u9ED8\u8BA4', en: 'Set as default' },
|
|
10665
|
+
models_under_provider: { zh: '\u4E2A\u6A21\u578B', en: 'models' },
|
|
10666
|
+
model_id_label: { zh: '\u6A21\u578BID', en: 'Model ID' },
|
|
10667
|
+
model_name_label: { zh: '\u6A21\u578B\u540D\u79F0', en: 'Model Name' },
|
|
10668
|
+
add_new_agent: { zh: '\u2795 \u6DFB\u52A0\u65B0\u667A\u80FD\u4F53', en: '\u2795 Add New Agent' },
|
|
10669
|
+
edit_agent: { zh: '\u270F\uFE0F \u7F16\u8F91\u667A\u80FD\u4F53', en: '\u270F\uFE0F Edit Agent' },
|
|
10670
|
+
agent_name_required: { zh: '\u8BF7\u8F93\u5165\u667A\u80FD\u4F53\u540D\u79F0', en: 'Agent name is required' },
|
|
10671
|
+
agent_updated: { zh: '\u667A\u80FD\u4F53\u5DF2\u66F4\u65B0', en: 'Agent updated' },
|
|
10672
|
+
agent_added: { zh: '\u667A\u80FD\u4F53\u5DF2\u6DFB\u52A0', en: 'Agent added' },
|
|
10673
|
+
no_data: { zh: '\u6682\u65E0\u6570\u636E', en: 'No data' },
|
|
10674
|
+
// Friends
|
|
10675
|
+
loading_friends: { zh: '\u6B63\u5728\u52A0\u8F7D\u597D\u53CB...', en: 'Loading friends...' },
|
|
10676
|
+
friends: { zh: '\u597D\u53CB', en: 'Friends' },
|
|
10677
|
+
requests: { zh: '\u8BF7\u6C42', en: 'Requests' },
|
|
10678
|
+
sessions: { zh: '\u4F1A\u8BDD', en: 'Sessions' },
|
|
10679
|
+
search_friends: { zh: '\u641C\u7D22\u597D\u53CB...', en: 'Search friends...' },
|
|
10680
|
+
all: { zh: '\u5168\u90E8', en: 'All' },
|
|
10681
|
+
ai: { zh: 'AI', en: 'AI' },
|
|
10682
|
+
human: { zh: '\u4EBA\u7C7B', en: 'Human' },
|
|
10683
|
+
add_friend: { zh: '\u6DFB\u52A0\u597D\u53CB', en: 'Add Friend' },
|
|
10684
|
+
type: { zh: '\u7C7B\u578B', en: 'Type' },
|
|
10685
|
+
friend_label: { zh: '\u597D\u53CB', en: 'Friend' },
|
|
10686
|
+
permissions: { zh: '\u6743\u9650', en: 'Permissions' },
|
|
10687
|
+
last_message: { zh: '\u6700\u540E\u6D88\u606F', en: 'Last Message' },
|
|
10688
|
+
add_friend_hint: { zh: '\u4F7F\u75286\u4F4D\u4E34\u65F6\u53F7\u7801\u6216\u8282\u70B9ID\u6DFB\u52A0\u597D\u53CB', en: 'Add a friend using their 6-digit temp number or node ID' },
|
|
10689
|
+
unavailable_offline: { zh: '\u79BB\u7EBF\u65F6\u4E0D\u53EF\u7528', en: 'Unavailable while offline' },
|
|
10690
|
+
request_id: { zh: '\u8BF7\u6C42 ID', en: 'Request ID' },
|
|
10691
|
+
from: { zh: '\u6765\u81EA', en: 'From' },
|
|
10692
|
+
time: { zh: '\u65F6\u95F4', en: 'Time' },
|
|
10693
|
+
no_pending_requests: { zh: '\u6682\u65E0\u5F85\u5904\u7406\u8BF7\u6C42', en: 'No pending requests' },
|
|
10694
|
+
accept: { zh: '\u63A5\u53D7', en: 'Accept' },
|
|
10695
|
+
reject: { zh: '\u62D2\u7EDD', en: 'Reject' },
|
|
10696
|
+
peer_id: { zh: '\u5BF9\u7AEF ID', en: 'Peer ID' },
|
|
10697
|
+
established: { zh: '\u5EFA\u7ACB\u65F6\u95F4', en: 'Established' },
|
|
10698
|
+
messages: { zh: '\u6761\u6D88\u606F', en: 'messages' },
|
|
10699
|
+
no_active_sessions: { zh: '\u6682\u65E0\u6D3B\u8DC3\u4F1A\u8BDD', en: 'No active sessions' },
|
|
10700
|
+
enter_temp_or_id: { zh: '\u8BF7\u8F93\u5165\u4E34\u65F6\u53F7\u7801\u6216\u8282\u70B9ID', en: 'Enter a temp number or node ID' },
|
|
10701
|
+
sending_request: { zh: '\u6B63\u5728\u53D1\u9001\u597D\u53CB\u8BF7\u6C42...', en: 'Sending friend request...' },
|
|
10702
|
+
friend_request_sent: { zh: '\u597D\u53CB\u8BF7\u6C42\u5DF2\u53D1\u9001\uFF01', en: 'Friend request sent!' },
|
|
10703
|
+
failed_add_friend: { zh: '\u6DFB\u52A0\u597D\u53CB\u5931\u8D25', en: 'Failed to add friend' },
|
|
10704
|
+
remove_friend_confirm: { zh: '\u786E\u5B9A\u79FB\u9664\u597D\u53CB ', en: 'Remove friend ' },
|
|
10705
|
+
friend_removed: { zh: '\u597D\u53CB\u5DF2\u79FB\u9664', en: 'Friend removed' },
|
|
10706
|
+
edit_permissions: { zh: '\u7F16\u8F91\u6743\u9650', en: 'Edit Permissions' },
|
|
10707
|
+
friend_permissions: { zh: '\u597D\u53CB\u6743\u9650', en: 'Friend Permissions' },
|
|
10708
|
+
chat_perm: { zh: '\u{1F4AC} \u804A\u5929', en: '\u{1F4AC} Chat' },
|
|
10709
|
+
exec_perm: { zh: '\u{1F527} \u6267\u884C', en: '\u{1F527} Exec' },
|
|
10710
|
+
chat_perm_hint: { zh: '\uFF08\u53D1\u9001/\u63A5\u6536\u6D88\u606F\uFF09', en: '(send/receive messages)' },
|
|
10711
|
+
exec_perm_hint: { zh: '\uFF08\u6267\u884C\u5DE5\u5177/\u547D\u4EE4\uFF09', en: '(execute tools/commands)' },
|
|
10712
|
+
save_permissions: { zh: '\u4FDD\u5B58\u6743\u9650', en: 'Save Permissions' },
|
|
10713
|
+
permissions_updated: { zh: '\u6743\u9650\u5DF2\u66F4\u65B0', en: 'Permissions updated' },
|
|
10714
|
+
request_accepted: { zh: '\u8BF7\u6C42\u5DF2\u63A5\u53D7', en: 'Request accepted' },
|
|
10715
|
+
request_rejected: { zh: '\u8BF7\u6C42\u5DF2\u62D2\u7EDD', en: 'Request rejected' },
|
|
10716
|
+
// Models
|
|
10717
|
+
loading_models: { zh: '\u6B63\u5728\u52A0\u8F7D\u6A21\u578B\u914D\u7F6E...', en: 'Loading model configuration...' },
|
|
10718
|
+
configured: { zh: '\u5DF2\u914D\u7F6E', en: 'Configured' },
|
|
10719
|
+
providers_with_keys: { zh: '\u5DF2\u914D\u7F6EAPI\u5BC6\u94A5\u7684\u63D0\u4F9B\u5546', en: 'Providers with API keys' },
|
|
10720
|
+
configure: { zh: '\u914D\u7F6E', en: 'Configure' },
|
|
10721
|
+
not_set: { zh: '\u672A\u8BBE\u7F6E', en: 'Not set' },
|
|
10722
|
+
key_set: { zh: '\u25CF \u5DF2\u8BBE\u7F6E\u5BC6\u94A5', en: '\u25CF Key set' },
|
|
10723
|
+
active_model_configs: { zh: '\u5F53\u524D\u6A21\u578B\u914D\u7F6E', en: 'Active Model Configurations' },
|
|
10724
|
+
default_model_global: { zh: '\u5168\u5C40\u9ED8\u8BA4\u6A21\u578B', en: 'Global Default Model' },
|
|
10725
|
+
model_count: { zh: '\u6A21\u578B\u6570\u91CF', en: 'Model Count' },
|
|
10726
|
+
multi_model: { zh: '\u591A\u6A21\u578B', en: 'Multi-model' },
|
|
10727
|
+
base_url: { zh: '\u57FA\u7840\u5730\u5740', en: 'Base URL' },
|
|
10728
|
+
configure_providers_desc: { zh: '\u4E3A\u667A\u80FD\u4F53\u914D\u7F6ELLM\u63D0\u4F9B\u5546\u3002\u70B9\u51FB\u63D0\u4F9B\u5546\u5361\u7247\u8BBE\u7F6E\u6216\u66F4\u65B0API\u5BC6\u94A5\u3001\u6A21\u578B\u548C\u57FA\u7840\u5730\u5740\u3002\u66F4\u6539\u5C06\u76F4\u63A5\u4FDD\u5B58\u5230\u914D\u7F6E\u6587\u4EF6\u3002', en: 'Configure LLM providers for your agents. Click a provider card to set or update the API key, model, and base URL. Changes are saved directly to your config file.' },
|
|
10729
|
+
current_key: { zh: '\u5F53\u524D\uFF1A', en: 'Current: ' },
|
|
10730
|
+
no_api_key: { zh: '\u672A\u914D\u7F6EAPI\u5BC6\u94A5', en: 'No API key configured' },
|
|
10731
|
+
provider_not_found: { zh: '\u672A\u627E\u5230\u63D0\u4F9B\u5546', en: 'Provider not found' },
|
|
10732
|
+
enter_api_key: { zh: '\u8F93\u5165API\u5BC6\u94A5', en: 'Enter API key' },
|
|
10733
|
+
enter_model_id: { zh: '\u8F93\u5165\u6A21\u578BID', en: 'Model ID' },
|
|
10734
|
+
default_url: { zh: '\u9ED8\u8BA4\u5730\u5740', en: 'Default URL' },
|
|
10735
|
+
enter_api_key_or_model: { zh: '\u8BF7\u81F3\u5C11\u8F93\u5165API\u5BC6\u94A5\u6216\u6A21\u578BID', en: 'Enter at least an API key or model ID' },
|
|
10736
|
+
saving_config: { zh: '\u6B63\u5728\u4FDD\u5B58\u914D\u7F6E...', en: 'Saving configuration...' },
|
|
10737
|
+
config_saved: { zh: '\u914D\u7F6E\u5DF2\u4FDD\u5B58\uFF01', en: 'Configuration saved!' },
|
|
10738
|
+
failed_save: { zh: '\u4FDD\u5B58\u5931\u8D25', en: 'Failed to save' },
|
|
10739
|
+
confirm_delete_provider: { zh: '\u786E\u5B9A\u5220\u9664\u63D0\u4F9B\u5546 "', en: 'Delete configuration for provider "' },
|
|
10740
|
+
provider_deleted: { zh: '\u63D0\u4F9B\u5546\u914D\u7F6E\u5DF2\u5220\u9664', en: 'Provider configuration deleted' },
|
|
10741
|
+
// Settings
|
|
10742
|
+
loading_settings: { zh: '\u6B63\u5728\u52A0\u8F7D\u8BBE\u7F6E...', en: 'Loading settings...' },
|
|
10743
|
+
tab_connection: { zh: '\u{1F50C} \u8FDE\u63A5', en: '\u{1F50C} Connection' },
|
|
10744
|
+
tab_friends: { zh: '\u{1F465} \u597D\u53CB', en: '\u{1F465} Friends' },
|
|
10745
|
+
tab_security: { zh: '\u{1F512} \u5B89\u5168', en: '\u{1F512} Security' },
|
|
10746
|
+
tab_advanced: { zh: '\u2699\uFE0F \u9AD8\u7EA7', en: '\u2699\uFE0F Advanced' },
|
|
10747
|
+
tab_json: { zh: '\u{1F4DD} JSON\u7F16\u8F91', en: '\u{1F4DD} JSON Editor' },
|
|
10748
|
+
conn_desc: { zh: '\u914D\u7F6E\u670D\u52A1\u5668\u8FDE\u63A5\u548CWebSocket\u8BBE\u7F6E\u3002\u66F4\u6539\u9700\u8981\u91CD\u542F\u63D2\u4EF6\u624D\u80FD\u5B8C\u5168\u751F\u6548\u3002', en: 'Configure server connection and WebSocket settings. Changes require a plugin restart to take full effect.' },
|
|
10749
|
+
server_connection: { zh: '\u{1F310} \u670D\u52A1\u5668\u8FDE\u63A5', en: '\u{1F310} Server Connection' },
|
|
10750
|
+
server_url_label: { zh: '\u670D\u52A1\u5668\u5730\u5740', en: 'Server URL' },
|
|
10751
|
+
server_url_hint: { zh: 'AICQ\u4E2D\u7EE7\u670D\u52A1\u5668\u7684HTTPS\u5730\u5740\u3002WebSocket\u8DEF\u5F84 /ws \u4F1A\u81EA\u52A8\u8FFD\u52A0\u3002', en: 'The HTTPS URL of the AICQ relay server. WebSocket path /ws is auto-appended.' },
|
|
10752
|
+
conn_timeout: { zh: '\u8FDE\u63A5\u8D85\u65F6\uFF08\u79D2\uFF09', en: 'Connection Timeout (seconds)' },
|
|
10753
|
+
conn_timeout_hint: { zh: 'HTTP\u8BF7\u6C42\u8D85\u65F6\u65F6\u95F4\uFF085-120\u79D2\uFF09\u3002\u9ED8\u8BA4\uFF1A30\u79D2\u3002', en: 'HTTP request timeout (5\u2013120s). Default: 30s.' },
|
|
10754
|
+
ws_auto_reconnect: { zh: 'WebSocket\u81EA\u52A8\u91CD\u8FDE', en: 'WS Auto-Reconnect' },
|
|
10755
|
+
auto_reconnect_label: { zh: '\u65AD\u5F00\u65F6\u81EA\u52A8\u91CD\u8FDE', en: 'Auto-reconnect when disconnected' },
|
|
10756
|
+
auto_reconnect_hint: { zh: '\u65AD\u5F00\u8FDE\u63A5\u540E\u81EA\u52A8\u91CD\u65B0\u8FDE\u63A5WebSocket\u3002', en: 'Automatically reconnect WebSocket on disconnection.' },
|
|
10757
|
+
ws_reconnect_interval: { zh: '\u91CD\u8FDE\u95F4\u9694\uFF08\u79D2\uFF09', en: 'WS Reconnect Interval (seconds)' },
|
|
10758
|
+
ws_reconnect_hint: { zh: '\u91CD\u8FDE\u5C1D\u8BD5\u4E4B\u95F4\u7684\u95F4\u9694\uFF085-600\u79D2\uFF09\u3002\u9ED8\u8BA4\uFF1A60\u79D2\u3002', en: 'Interval between reconnection attempts (5\u2013600s). Default: 60s.' },
|
|
10759
|
+
test: { zh: '\u6D4B\u8BD5', en: 'Test' },
|
|
10760
|
+
testing: { zh: '\u6D4B\u8BD5\u4E2D...', en: 'Testing...' },
|
|
10761
|
+
enter_server_url: { zh: '\u8BF7\u5148\u8F93\u5165\u670D\u52A1\u5668\u5730\u5740', en: 'Enter a server URL first' },
|
|
10762
|
+
conn_ok: { zh: '\u8FDE\u63A5\u6210\u529F', en: 'Connected successfully' },
|
|
10763
|
+
conn_ok_latency: { zh: '\u8FDE\u63A5\u6210\u529F\uFF01\u5EF6\u8FDF\uFF1A', en: 'Connection OK! Latency: ' },
|
|
10764
|
+
conn_failed: { zh: '\u8FDE\u63A5\u5931\u8D25', en: 'Connection failed' },
|
|
10765
|
+
config_file: { zh: '\u{1F4C1} \u914D\u7F6E\u6587\u4EF6', en: '\u{1F4C1} Config File' },
|
|
10766
|
+
source: { zh: '\u6765\u6E90', en: 'Source' },
|
|
10767
|
+
mgmt_ui: { zh: '\u7BA1\u7406\u754C\u9762', en: 'Management UI' },
|
|
10768
|
+
uptime: { zh: '\u8FD0\u884C\u65F6\u95F4', en: 'Uptime' },
|
|
10769
|
+
not_found: { zh: '\u672A\u627E\u5230', en: 'Not found' },
|
|
10770
|
+
friends_tab_desc: { zh: '\u914D\u7F6E\u597D\u53CB\u7BA1\u7406\u3001\u6743\u9650\u548C\u4E34\u65F6\u53F7\u7801\u8BBE\u7F6E\u3002', en: 'Configure friend management, permissions, and temporary number settings.' },
|
|
10771
|
+
of_max: { zh: ' / \u6700\u5927 ', en: ' of ' },
|
|
10772
|
+
max_friends: { zh: '\u6700\u5927\u597D\u53CB\u6570', en: 'Max Friends' },
|
|
10773
|
+
auto_accept: { zh: '\u81EA\u52A8\u63A5\u53D7\u597D\u53CB', en: 'Auto-Accept Friends' },
|
|
10774
|
+
auto_accept_label: { zh: '\u81EA\u52A8\u63A5\u53D7\u8BF7\u6C42', en: 'Automatically accept requests' },
|
|
10775
|
+
auto_accept_hint: { zh: '\u542F\u7528\u540E\uFF0C\u4F20\u5165\u7684\u597D\u53CB\u8BF7\u6C42\u5C06\u81EA\u52A8\u63A5\u53D7\u3002', en: 'When enabled, incoming friend requests are accepted without review.' },
|
|
10776
|
+
default_perms: { zh: '\u65B0\u597D\u53CB\u9ED8\u8BA4\u6743\u9650', en: 'Default Permissions for New Friends' },
|
|
10777
|
+
default_perms_hint: { zh: '\u81EA\u52A8\u63A5\u53D7\u65B0\u597D\u53CB\u8BF7\u6C42\u65F6\u5E94\u7528\u7684\u9ED8\u8BA4\u6743\u9650\u3002', en: 'Default permissions applied when auto-accepting new friend requests.' },
|
|
10778
|
+
temp_numbers: { zh: '\u{1F522} \u4E34\u65F6\u53F7\u7801', en: '\u{1F522} Temporary Numbers' },
|
|
10779
|
+
temp_expiry: { zh: '\u4E34\u65F6\u53F7\u7801\u6709\u6548\u671F\uFF08\u79D2\uFF09', en: 'Temp Number Expiry (seconds)' },
|
|
10780
|
+
temp_expiry_hint: { zh: '\u4E34\u65F6\u597D\u53CB\u53F7\u7801\u7684\u6709\u6548\u65F6\u95F4\uFF0860-3600\u79D2\uFF09\u3002\u9ED8\u8BA4\uFF1A5\u5206\u949F\u3002', en: 'How long a temporary friend number remains valid (60\u20133600s). Default: 5 minutes.' },
|
|
10781
|
+
sec_desc: { zh: '\u914D\u7F6E\u52A0\u5BC6\u3001P2P\u548C\u8EAB\u4EFD\u5B89\u5168\u8BBE\u7F6E\u3002', en: 'Configure encryption, P2P, and identity security settings.' },
|
|
10782
|
+
agent_identity: { zh: '\u{1F916} \u667A\u80FD\u4F53\u8EAB\u4EFD', en: '\u{1F916} Agent Identity' },
|
|
10783
|
+
public_key_fp: { zh: '\u516C\u94A5\u6307\u7EB9', en: 'Public Key Fingerprint' },
|
|
10784
|
+
reset_identity: { zh: '\u{1F5D1}\uFE0F \u91CD\u7F6E\u8EAB\u4EFD', en: '\u{1F5D1}\uFE0F Reset Identity' },
|
|
10785
|
+
reset_identity_warn: { zh: '\u26A0\uFE0F \u8FD9\u5C06\u6C38\u4E45\u5220\u9664\u6240\u6709\u597D\u53CB\u3001\u4F1A\u8BDD\u548C\u5BC6\u94A5', en: '\u26A0\uFE0F This deletes all friends, sessions, and keys permanently' },
|
|
10786
|
+
p2p_encryption: { zh: '\u{1F512} P2P\u4E0E\u52A0\u5BC6', en: '\u{1F512} P2P & Encryption' },
|
|
10787
|
+
enable_p2p: { zh: '\u542F\u7528P2P\u8FDE\u63A5', en: 'Enable P2P Connections' },
|
|
10788
|
+
allow_p2p: { zh: '\u5141\u8BB8\u76F4\u63A5P2P\u6D88\u606F', en: 'Allow direct P2P messaging' },
|
|
10789
|
+
enable_p2p_hint: { zh: '\u53CC\u65B9\u90FD\u5728\u7EBF\u65F6\u542F\u7528\u70B9\u5BF9\u70B9\u52A0\u5BC6\u8FDE\u63A5\u3002', en: 'Enable peer-to-peer encrypted connections when both parties are online.' },
|
|
10790
|
+
hs_timeout: { zh: '\u63E1\u624B\u8D85\u65F6\uFF08\u79D2\uFF09', en: 'Handshake Timeout (seconds)' },
|
|
10791
|
+
hs_timeout_hint: { zh: 'Noise-XK\u63E1\u624B\u8D85\u65F6\u65F6\u95F4\uFF0810-300\u79D2\uFF09\u3002\u9ED8\u8BA4\uFF1A60\u79D2\u3002', en: 'Noise-XK handshake timeout (10\u2013300s). Default: 60s.' },
|
|
10792
|
+
adv_desc: { zh: '\u6587\u4EF6\u4F20\u8F93\u3001\u65E5\u5FD7\u548C\u914D\u7F6E\u7BA1\u7406\u7684\u9AD8\u7EA7\u8BBE\u7F6E\u3002', en: 'Advanced settings for file transfer, logging, and configuration management.' },
|
|
10793
|
+
file_transfer: { zh: '\u{1F4CE} \u6587\u4EF6\u4F20\u8F93', en: '\u{1F4CE} File Transfer' },
|
|
10794
|
+
enable_ft: { zh: '\u542F\u7528\u6587\u4EF6\u4F20\u8F93', en: 'Enable File Transfer' },
|
|
10795
|
+
allow_ft: { zh: '\u5141\u8BB8\u6587\u4EF6\u4F20\u8F93', en: 'Allow file transfers' },
|
|
10796
|
+
enable_ft_hint: { zh: '\u542F\u7528\u597D\u53CB\u95F4\u7684\u52A0\u5BC6\u6587\u4EF6\u4F20\u8F93\u3002', en: 'Enable encrypted file transfer between friends.' },
|
|
10797
|
+
max_file_size: { zh: '\u6700\u5927\u6587\u4EF6\u5927\u5C0F', en: 'Max File Size' },
|
|
10798
|
+
max_file_size_hint: { zh: '\u52A0\u5BC6\u4F20\u8F93\u7684\u6700\u5927\u6587\u4EF6\u5927\u5C0F\u3002\u5F53\u524D\uFF1A', en: 'Maximum file size for encrypted transfers. Current: ' },
|
|
10799
|
+
logging: { zh: '\u{1F4CB} \u65E5\u5FD7', en: '\u{1F4CB} Logging' },
|
|
10800
|
+
log_level: { zh: '\u65E5\u5FD7\u7EA7\u522B', en: 'Log Level' },
|
|
10801
|
+
log_debug: { zh: '\u{1F41B} \u8C03\u8BD5 \u2014 \u8BE6\u7EC6\u8F93\u51FA', en: '\u{1F41B} Debug \u2014 Verbose output for troubleshooting' },
|
|
10802
|
+
log_info: { zh: '\u2139\uFE0F \u4FE1\u606F \u2014 \u4E00\u822C\u4FE1\u606F\uFF08\u9ED8\u8BA4\uFF09', en: '\u2139\uFE0F Info \u2014 General information (default)' },
|
|
10803
|
+
log_warn: { zh: '\u26A0\uFE0F \u8B66\u544A \u2014 \u8B66\u544A\u548C\u91CD\u8981\u4E8B\u4EF6', en: '\u26A0\uFE0F Warn \u2014 Warnings and important events' },
|
|
10804
|
+
log_error: { zh: '\u274C \u9519\u8BEF \u2014 \u4EC5\u9519\u8BEF', en: '\u274C Error \u2014 Errors only' },
|
|
10805
|
+
log_none: { zh: '\u{1F507} \u65E0 \u2014 \u7981\u7528\u6240\u6709\u65E5\u5FD7', en: '\u{1F507} None \u2014 Disable all logging' },
|
|
10806
|
+
log_level_hint: { zh: '\u63A7\u5236\u63D2\u4EF6\u65E5\u5FD7\u8F93\u51FA\u7684\u8BE6\u7EC6\u7A0B\u5EA6\u3002', en: 'Controls the verbosity of plugin log output.' },
|
|
10807
|
+
import_export: { zh: '\u{1F4E6} \u5BFC\u5165/\u5BFC\u51FA\u8BBE\u7F6E', en: '\u{1F4E6} Import / Export Settings' },
|
|
10808
|
+
export_settings: { zh: '\u{1F4E5} \u5BFC\u51FA\u8BBE\u7F6E', en: '\u{1F4E5} Export Settings' },
|
|
10809
|
+
import_settings: { zh: '\u{1F4E4} \u5BFC\u5165\u8BBE\u7F6E', en: '\u{1F4E4} Import Settings' },
|
|
10810
|
+
import_export_hint: { zh: '\u5C06\u5F53\u524DAICQ\u63D2\u4EF6\u8BBE\u7F6E\u5BFC\u51FA\u4E3AJSON\u3002\u5BFC\u5165\u53EF\u4ECE\u5907\u4EFD\u6062\u590D\u8BBE\u7F6E\u3002', en: 'Export current AICQ plugin settings as JSON. Import to restore settings from a backup.' },
|
|
10811
|
+
save: { zh: '\u{1F4BE} \u4FDD\u5B58', en: '\u{1F4BE} Save' },
|
|
10812
|
+
saving: { zh: '\u4FDD\u5B58\u4E2D...', en: 'Saving...' },
|
|
10813
|
+
saved: { zh: '\u2713 \u5DF2\u4FDD\u5B58', en: '\u2713 Saved' },
|
|
10814
|
+
settings_saved: { zh: '\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\uFF1A', en: 'Settings saved: ' },
|
|
10815
|
+
all_saved: { zh: '\u6240\u6709\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\uFF01', en: 'All settings saved!' },
|
|
10816
|
+
delete_everything: { zh: '\u{1F5D1}\uFE0F \u5220\u9664\u6240\u6709\u6570\u636E', en: '\u{1F5D1}\uFE0F Delete Everything' },
|
|
10817
|
+
confirm_delete: { zh: '\u{1F5D1}\uFE0F \u786E\u8BA4\u5220\u9664', en: '\u{1F5D1}\uFE0F Confirm Delete' },
|
|
10818
|
+
resetting: { zh: '\u91CD\u7F6E\u4E2D...', en: 'Resetting...' },
|
|
10819
|
+
reset_success: { zh: '\u8EAB\u4EFD\u91CD\u7F6E\u6210\u529F\uFF0C\u8BF7\u91CD\u542F\u63D2\u4EF6\u3002', en: 'Identity reset successfully. Please restart the plugin.' },
|
|
10820
|
+
reset_failed: { zh: '\u91CD\u7F6E\u5931\u8D25', en: 'Reset failed' },
|
|
10821
|
+
exported_success: { zh: '\u8BBE\u7F6E\u5BFC\u51FA\u6210\u529F', en: 'Settings exported successfully' },
|
|
10822
|
+
paste_json: { zh: '\u8BF7\u5148\u7C98\u8D34JSON\u8BBE\u7F6E', en: 'Paste JSON settings first' },
|
|
10823
|
+
importing: { zh: '\u5BFC\u5165\u4E2D...', en: 'Importing...' },
|
|
10824
|
+
imported_success: { zh: '\u8BBE\u7F6E\u5BFC\u5165\u6210\u529F\uFF01', en: 'Settings imported successfully!' },
|
|
10825
|
+
import_failed: { zh: '\u5BFC\u5165\u5931\u8D25', en: 'Import failed' },
|
|
10826
|
+
loading_config: { zh: '\u6B63\u5728\u52A0\u8F7D\u914D\u7F6E...', en: 'Loading config...' },
|
|
10827
|
+
json_editor_desc: { zh: '\u76F4\u63A5\u7F16\u8F91\u539F\u59CBJSON\u914D\u7F6E\u3002\u6CE8\u610F\u8BED\u6CD5\u2014\u2014\u65E0\u6548\u7684JSON\u5C06\u88AB\u62D2\u7EDD\u3002', en: 'Edit the raw JSON configuration directly. Be careful with syntax \u2014 invalid JSON will be rejected.' },
|
|
10828
|
+
json_editor: { zh: '\u{1F4DD} JSON\u914D\u7F6E\u7F16\u8F91\u5668', en: '\u{1F4DD} Config JSON Editor' },
|
|
10829
|
+
raw_json: { zh: '\u539F\u59CBJSON\u914D\u7F6E', en: 'Raw JSON Configuration' },
|
|
10830
|
+
raw_json_hint: { zh: '\u76F4\u63A5\u7F16\u8F91\u914D\u7F6EJSON\u3002\u4F7F\u7528\u683C\u5F0F\u5316\u6309\u94AE\u7F8E\u5316\u3002', en: 'Directly edit the configuration JSON. Use the Format button to prettify.' },
|
|
10831
|
+
format: { zh: '\u{1F4D0} \u683C\u5F0F\u5316', en: '\u{1F4D0} Format' },
|
|
10832
|
+
copy: { zh: '\u{1F4CB} \u590D\u5236', en: '\u{1F4CB} Copy' },
|
|
10833
|
+
revert: { zh: '\u21A9\uFE0F \u8FD8\u539F', en: '\u21A9\uFE0F Revert' },
|
|
10834
|
+
save_config: { zh: '\u{1F4BE} \u4FDD\u5B58\u914D\u7F6E', en: '\u{1F4BE} Save Config' },
|
|
10835
|
+
json_formatted: { zh: 'JSON\u5DF2\u683C\u5F0F\u5316', en: 'JSON formatted' },
|
|
10836
|
+
valid_json: { zh: '\u2713 \u6709\u6548JSON', en: '\u2713 Valid JSON' },
|
|
10837
|
+
invalid_json: { zh: '\u2717 \u65E0\u6548JSON\uFF1A', en: '\u2717 Invalid JSON: ' },
|
|
10838
|
+
no_content: { zh: '\u6CA1\u6709\u53EF\u4FDD\u5B58\u7684\u5185\u5BB9', en: 'No content to save' },
|
|
10839
|
+
config_saved: { zh: '\u914D\u7F6E\u5DF2\u4FDD\u5B58\uFF01', en: 'Config saved successfully!' },
|
|
10840
|
+
testing_conn_to: { zh: '\u6B63\u5728\u6D4B\u8BD5\u5230 ', en: 'Testing connection to ' },
|
|
10841
|
+
config_file_label: { zh: '\u{1F4C4} \u914D\u7F6E\u6587\u4EF6', en: '\u{1F4C4} Config File' },
|
|
10842
|
+
// Modals
|
|
10843
|
+
add_friend_title: { zh: '\u2795 \u6DFB\u52A0\u597D\u53CB', en: '\u2795 Add Friend' },
|
|
10844
|
+
temp_or_node: { zh: '\u4E34\u65F6\u53F7\u7801\u6216\u8282\u70B9ID', en: 'Temp Number or Node ID' },
|
|
10845
|
+
temp_or_node_ph: { zh: '6\u4F4D\u53F7\u7801\u6216\u8282\u70B9ID', en: '6-digit number or node ID' },
|
|
10846
|
+
temp_or_node_hint: { zh: '\u8F93\u5165\u597D\u53CB\u76846\u4F4D\u4E34\u65F6\u53F7\u7801\u6216\u5B8C\u6574\u8282\u70B9ID\u3002', en: 'Enter the 6-digit temporary number or the full node ID of your friend.' },
|
|
10847
|
+
cancel: { zh: '\u53D6\u6D88', en: 'Cancel' },
|
|
10848
|
+
send_request: { zh: '\u53D1\u9001\u8BF7\u6C42', en: 'Send Request' },
|
|
10849
|
+
close: { zh: '\u5173\u95ED', en: 'Close' },
|
|
10850
|
+
save_agent: { zh: '\u{1F4BE} \u4FDD\u5B58\u667A\u80FD\u4F53', en: '\u{1F4BE} Save Agent' },
|
|
10851
|
+
save_configuration: { zh: '\u{1F4BE} \u4FDD\u5B58\u914D\u7F6E', en: '\u{1F4BE} Save Configuration' },
|
|
10852
|
+
reset_identity_title: { zh: '\u{1F5D1}\uFE0F \u91CD\u7F6E\u667A\u80FD\u4F53\u8EAB\u4EFD', en: '\u{1F5D1}\uFE0F Reset Agent Identity' },
|
|
10853
|
+
reset_warning_title: { zh: '\u26A0\uFE0F \u8B66\u544A\uFF1A\u8FD9\u662F\u4E00\u4E2A\u7834\u574F\u6027\u64CD\u4F5C\uFF01', en: '\u26A0\uFE0F WARNING: This is a destructive operation!' },
|
|
10854
|
+
reset_warning_desc: { zh: '\u8FD9\u5C06\u6C38\u4E45\u5220\u9664\uFF1A', en: 'This will permanently delete:' },
|
|
10855
|
+
reset_keypair: { zh: '\u2022 \u4F60\u7684Ed25519\u5BC6\u94A5\u5BF9\u548C\u667A\u80FD\u4F53ID', en: '\u2022 Your Ed25519 key pair and agent ID' },
|
|
10856
|
+
reset_friends: { zh: '\u2022 \u6240\u6709\u597D\u53CB\u8FDE\u63A5\u548C\u4F1A\u8BDD', en: '\u2022 All friend connections and sessions' },
|
|
10857
|
+
reset_requests: { zh: '\u2022 \u6240\u6709\u5F85\u5904\u7406\u7684\u597D\u53CB\u8BF7\u6C42', en: '\u2022 All pending friend requests' },
|
|
10858
|
+
reset_temp: { zh: '\u2022 \u6240\u6709\u4E34\u65F6\u53F7\u7801', en: '\u2022 All temporary numbers' },
|
|
10859
|
+
reset_restart_hint: { zh: '\u91CD\u7F6E\u540E\uFF0C\u5FC5\u987B\u91CD\u542F\u63D2\u4EF6\u4EE5\u751F\u6210\u65B0\u8EAB\u4EFD\u3002', en: 'After reset, you must restart the plugin to generate a new identity.' },
|
|
10860
|
+
type_reset: { zh: '\u8F93\u5165 RESET \u4EE5\u786E\u8BA4', en: 'Type RESET to confirm' },
|
|
10861
|
+
import_title: { zh: '\u{1F4E4} \u5BFC\u5165\u8BBE\u7F6E', en: '\u{1F4E4} Import Settings' },
|
|
10862
|
+
paste_json_label: { zh: '\u7C98\u8D34JSON\u8BBE\u7F6E', en: 'Paste JSON Settings' },
|
|
10863
|
+
paste_json_ph: { zh: '{"serverUrl": "https://...", ...}', en: '{"serverUrl": "https://...", ...}' },
|
|
10864
|
+
paste_json_hint: { zh: '\u7C98\u8D34\u4ECE\u53E6\u4E00\u4E2AAICQ\u5B9E\u4F8B\u5BFC\u51FA\u7684JSON\u8BBE\u7F6E\u3002\u8BBE\u7F6E\u5C06\u4E0E\u73B0\u6709\u503C\u5408\u5E76\u3002', en: 'Paste the JSON settings exported from another AICQ instance. Settings will be merged with existing values.' },
|
|
10865
|
+
agent_name: { zh: '\u667A\u80FD\u4F53\u540D\u79F0 *', en: 'Agent Name *' },
|
|
10866
|
+
agent_id_label: { zh: '\u667A\u80FD\u4F53ID', en: 'Agent ID' },
|
|
10867
|
+
agent_id_hint: { zh: '\u552F\u4E00\u6807\u8BC6\u7B26\u3002\u7559\u7A7A\u81EA\u52A8\u751F\u6210\u3002', en: 'Unique identifier. Leave empty for auto-generation.' },
|
|
10868
|
+
model_label: { zh: '\u6A21\u578B', en: 'Model' },
|
|
10869
|
+
provider_label: { zh: '\u63D0\u4F9B\u5546', en: 'Provider' },
|
|
10870
|
+
system_prompt_label: { zh: '\u7CFB\u7EDF\u63D0\u793A\u8BCD', en: 'System Prompt' },
|
|
10871
|
+
temperature: { zh: '\u6E29\u5EA6', en: 'Temperature' },
|
|
10872
|
+
max_tokens: { zh: '\u6700\u5927Token\u6570', en: 'Max Tokens' },
|
|
10873
|
+
top_p: { zh: 'Top P', en: 'Top P' },
|
|
10874
|
+
tools: { zh: '\u5DE5\u5177', en: 'Tools' },
|
|
10875
|
+
enabled: { zh: '\u542F\u7528', en: 'Enabled' },
|
|
10876
|
+
// Utilities
|
|
10877
|
+
copied_clipboard: { zh: '\u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F', en: 'Copied to clipboard' },
|
|
10878
|
+
copy_failed: { zh: '\u590D\u5236\u5931\u8D25', en: 'Copy failed' },
|
|
10879
|
+
just_now: { zh: '\u521A\u521A', en: 'just now' },
|
|
10880
|
+
min_ago: { zh: '\u5206\u949F\u524D', en: ' min ago' },
|
|
10881
|
+
h_ago: { zh: '\u5C0F\u65F6\u524D', en: 'h ago' },
|
|
10882
|
+
d_ago: { zh: '\u5929\u524D', en: 'd ago' },
|
|
10883
|
+
none: { zh: '\u65E0', en: 'none' },
|
|
10884
|
+
failed: { zh: '\u5931\u8D25', en: 'Failed' },
|
|
10885
|
+
// Offline
|
|
10886
|
+
offline_msg: { zh: '\u60A8\u5F53\u524D\u5904\u4E8E\u79BB\u7EBF\u72B6\u6001\u3002\u90E8\u5206\u529F\u80FD\u53EF\u80FD\u53D7\u9650\u3002\u6570\u636E\u4ECE\u672C\u5730\u7F13\u5B58\u52A0\u8F7D\u3002', en: 'You are offline. Some features may be limited. Data is loaded from local cache.' },
|
|
10887
|
+
// Custom Providers
|
|
10888
|
+
add_custom_provider: { zh: '\u2795 \u6DFB\u52A0\u81EA\u5B9A\u4E49\u63D0\u4F9B\u5546', en: '\u2795 Add Custom Provider' },
|
|
10889
|
+
custom_provider: { zh: '\u{1F9E9} \u81EA\u5B9A\u4E49\u63D0\u4F9B\u5546', en: '\u{1F9E9} Custom Provider' },
|
|
10890
|
+
custom_provider_desc: { zh: '\u624B\u52A8\u6DFB\u52A0\u517C\u5BB9 OpenAI API \u683C\u5F0F\u7684\u81EA\u5B9A\u4E49\u6A21\u578B\u63D0\u4F9B\u5546', en: 'Manually add custom model providers compatible with OpenAI API format' },
|
|
10891
|
+
provider_name: { zh: '\u63D0\u4F9B\u5546\u540D\u79F0', en: 'Provider Name' },
|
|
10892
|
+
provider_name_placeholder: { zh: '\u4F8B\u5982\uFF1AMy Custom API', en: 'e.g., My Custom API' },
|
|
10893
|
+
provider_name_required: { zh: '\u8BF7\u8F93\u5165\u63D0\u4F9B\u5546\u540D\u79F0', en: 'Provider name is required' },
|
|
10894
|
+
custom_provider_added: { zh: '\u81EA\u5B9A\u4E49\u63D0\u4F9B\u5546\u5DF2\u6DFB\u52A0', en: 'Custom provider added' },
|
|
10895
|
+
custom_provider_updated: { zh: '\u81EA\u5B9A\u4E49\u63D0\u4F9B\u5546\u5DF2\u66F4\u65B0', en: 'Custom provider updated' },
|
|
10896
|
+
custom_provider_deleted: { zh: '\u81EA\u5B9A\u4E49\u63D0\u4F9B\u5546\u5DF2\u5220\u9664', en: 'Custom provider deleted' },
|
|
10897
|
+
confirm_delete_custom: { zh: '\u786E\u5B9A\u8981\u5220\u9664\u81EA\u5B9A\u4E49\u63D0\u4F9B\u5546 "{name}" \u5417\uFF1F\u6B64\u64CD\u4F5C\u4E0D\u53EF\u6062\u590D\u3002', en: 'Are you sure you want to delete custom provider "{name}"? This cannot be undone.' },
|
|
10898
|
+
edit_custom_provider: { zh: '\u7F16\u8F91\u81EA\u5B9A\u4E49\u63D0\u4F9B\u5546', en: 'Edit Custom Provider' },
|
|
10899
|
+
save_custom: { zh: '\u{1F4BE} \u4FDD\u5B58', en: '\u{1F4BE} Save' },
|
|
10900
|
+
description_label: { zh: '\u63CF\u8FF0', en: 'Description' },
|
|
10901
|
+
description_placeholder: { zh: '\u53EF\u9009\uFF1A\u63CF\u8FF0\u6B64\u63D0\u4F9B\u5546\u7684\u7528\u9014', en: 'Optional: describe what this provider is for' },
|
|
10902
|
+
api_key_label: { zh: 'API \u5BC6\u94A5', en: 'API Key' },
|
|
10903
|
+
model_id_label2: { zh: '\u6A21\u578B ID', en: 'Model ID' },
|
|
10904
|
+
base_url_label: { zh: '\u57FA\u7840\u5730\u5740', en: 'Base URL' },
|
|
10905
|
+
// OpenClaw Config
|
|
10906
|
+
nav_openclaw: { zh: 'OpenClaw', en: 'OpenClaw' },
|
|
10907
|
+
openclaw_config: { zh: 'OpenClaw \u914D\u7F6E', en: 'OpenClaw Configuration' },
|
|
10908
|
+
loading_openclaw: { zh: '\u6B63\u5728\u52A0\u8F7D\u914D\u7F6E...', en: 'Loading configuration...' },
|
|
10909
|
+
openclaw_desc: { zh: '\u7BA1\u7406 OpenClaw \u7684\u667A\u80FD\u4F53\u914D\u7F6E\u3001\u8DEF\u7531\u7ED1\u5B9A\u548C\u9891\u9053\u8BBE\u7F6E\u3002\u66F4\u6539\u5C06\u76F4\u63A5\u4FDD\u5B58\u5230 openclaw.json \u914D\u7F6E\u6587\u4EF6\u3002', en: 'Manage OpenClaw agent configuration, routing bindings, and channel settings. Changes are saved directly to the openclaw.json config file.' },
|
|
10910
|
+
// Agent Defaults
|
|
10911
|
+
agent_defaults: { zh: '\u{1F916} \u667A\u80FD\u4F53\u9ED8\u8BA4\u8BBE\u7F6E', en: '\u{1F916} Agent Defaults' },
|
|
10912
|
+
agent_defaults_desc: { zh: '\u914D\u7F6E\u6240\u6709\u667A\u80FD\u4F53\u7684\u9ED8\u8BA4\u53C2\u6570\uFF0C\u5305\u62EC\u6A21\u578B\u9009\u62E9\u3001\u5E76\u53D1\u6570\u3001\u538B\u7F29\u6A21\u5F0F\u7B49', en: 'Configure default parameters for all agents, including model selection, concurrency, compaction mode, etc.' },
|
|
10913
|
+
compaction_mode: { zh: '\u538B\u7F29\u6A21\u5F0F', en: 'Compaction Mode' },
|
|
10914
|
+
max_concurrent: { zh: '\u6700\u5927\u5E76\u53D1\u6570', en: 'Max Concurrent' },
|
|
10915
|
+
primary_model: { zh: '\u4E3B\u6A21\u578B', en: 'Primary Model' },
|
|
10916
|
+
fallback_models: { zh: '\u5907\u7528\u6A21\u578B', en: 'Fallback Models' },
|
|
10917
|
+
image_model: { zh: '\u56FE\u50CF\u6A21\u578B', en: 'Image Model' },
|
|
10918
|
+
subagent_max_concurrent: { zh: '\u5B50\u667A\u80FD\u4F53\u6700\u5927\u5E76\u53D1', en: 'Subagent Max Concurrent' },
|
|
10919
|
+
thinking_default: { zh: '\u9ED8\u8BA4\u601D\u8003\u6A21\u5F0F', en: 'Thinking Default' },
|
|
10920
|
+
workspace: { zh: '\u5DE5\u4F5C\u7A7A\u95F4', en: 'Workspace' },
|
|
10921
|
+
models_registry: { zh: '\u6A21\u578B\u6CE8\u518C\u8868', en: 'Models Registry' },
|
|
10922
|
+
// Agent List
|
|
10923
|
+
oc_agent_list: { zh: '\u{1F4CB} \u667A\u80FD\u4F53\u5217\u8868', en: '\u{1F4CB} Agent List' },
|
|
10924
|
+
oc_agent_list_desc: { zh: "\u914D\u7F6E\u5404\u4E2A\u667A\u80FD\u4F53\u7684\u8EAB\u4EFD\u3001\u6A21\u578B\u548C\u5DE5\u5177\u6743\u9650\u3002\u6BCF\u4E2A\u667A\u80FD\u4F53\u53EF\u4EE5\u5206\u914D\u4E0D\u540C\u7684\u6A21\u578B\u548C\u5DE5\u5177\u914D\u7F6E\u3002", en: "Configure each agent's identity, model, and tool permissions. Each agent can be assigned different models and tool configurations." },
|
|
10925
|
+
add_agent_btn: { zh: '\u2795 \u6DFB\u52A0\u667A\u80FD\u4F53', en: '\u2795 Add Agent' },
|
|
10926
|
+
agent_identity_name: { zh: '\u8EAB\u4EFD\u540D\u79F0', en: 'Identity Name' },
|
|
10927
|
+
agent_model_primary: { zh: '\u4E3B\u6A21\u578B', en: 'Primary Model' },
|
|
10928
|
+
agent_tools_profile: { zh: '\u5DE5\u5177\u914D\u7F6E', en: 'Tools Profile' },
|
|
10929
|
+
agent_workspace: { zh: '\u5DE5\u4F5C\u7A7A\u95F4', en: 'Workspace' },
|
|
10930
|
+
no_agents_in_list: { zh: '\u6682\u65E0\u667A\u80FD\u4F53\uFF0C\u70B9\u51FB\u4E0A\u65B9\u6309\u94AE\u6DFB\u52A0', en: 'No agents configured. Click the button above to add one.' },
|
|
10931
|
+
// Bindings
|
|
10932
|
+
bindings_title: { zh: '\u{1F517} \u8DEF\u7531\u7ED1\u5B9A', en: '\u{1F517} Routing Bindings' },
|
|
10933
|
+
bindings_desc: { zh: '\u914D\u7F6E\u6D88\u606F\u8DEF\u7531\u89C4\u5219\uFF0C\u5C06\u9891\u9053\u6216\u8D26\u53F7\u7ED1\u5B9A\u5230\u6307\u5B9A\u7684\u667A\u80FD\u4F53\u3002\u652F\u6301\u6309\u9891\u9053\u7C7B\u578B\u6216\u8D26\u53F7ID\u5339\u914D\u3002', en: 'Configure message routing rules to bind channels or accounts to specific agents. Supports matching by channel type or account ID.' },
|
|
10934
|
+
add_binding: { zh: '\u2795 \u6DFB\u52A0\u7ED1\u5B9A', en: '\u2795 Add Binding' },
|
|
10935
|
+
binding_agent_id: { zh: '\u667A\u80FD\u4F53ID', en: 'Agent ID' },
|
|
10936
|
+
binding_channel: { zh: '\u9891\u9053', en: 'Channel' },
|
|
10937
|
+
binding_account_id: { zh: '\u8D26\u53F7ID', en: 'Account ID' },
|
|
10938
|
+
binding_type: { zh: '\u7C7B\u578B', en: 'Type' },
|
|
10939
|
+
no_bindings: { zh: '\u6682\u65E0\u8DEF\u7531\u7ED1\u5B9A', en: 'No routing bindings configured' },
|
|
10940
|
+
// Channels
|
|
10941
|
+
channels_title: { zh: '\u{1F4E1} \u9891\u9053\u914D\u7F6E', en: '\u{1F4E1} Channel Configuration' },
|
|
10942
|
+
channels_desc: { zh: '\u914D\u7F6E\u5916\u90E8\u901A\u4FE1\u9891\u9053\uFF08\u5982 Telegram\uFF09\u3002\u6BCF\u4E2A\u9891\u9053\u53EF\u4EE5\u6709\u591A\u4E2A\u8D26\u53F7\uFF0C\u652F\u6301\u4E0D\u540C\u7684\u6D88\u606F\u7B56\u7565\u548C\u6743\u9650\u3002', en: 'Configure external communication channels (e.g., Telegram). Each channel can have multiple accounts with different messaging policies and permissions.' },
|
|
10943
|
+
channel_enabled: { zh: '\u5DF2\u542F\u7528', en: 'Enabled' },
|
|
10944
|
+
channel_disabled: { zh: '\u5DF2\u7981\u7528', en: 'Disabled' },
|
|
10945
|
+
group_policy: { zh: '\u7FA4\u7EC4\u7B56\u7565', en: 'Group Policy' },
|
|
10946
|
+
accounts: { zh: '\u8D26\u53F7', en: 'Accounts' },
|
|
10947
|
+
add_account: { zh: '\u2795 \u6DFB\u52A0\u8D26\u53F7', en: '\u2795 Add Account' },
|
|
10948
|
+
bot_token: { zh: 'Bot Token', en: 'Bot Token' },
|
|
10949
|
+
dm_policy: { zh: '\u79C1\u4FE1\u7B56\u7565', en: 'DM Policy' },
|
|
10950
|
+
allow_from: { zh: '\u5141\u8BB8\u6765\u6E90', en: 'Allow From' },
|
|
10951
|
+
add_allow_from: { zh: '\u6DFB\u52A0\u5141\u8BB8\u6765\u6E90', en: 'Add Allow From' },
|
|
10952
|
+
no_channels_configured: { zh: '\u6682\u65E0\u9891\u9053\u914D\u7F6E', en: 'No channels configured' },
|
|
10953
|
+
// OpenClaw Actions
|
|
10954
|
+
save_openclaw_config: { zh: '\u{1F4BE} \u4FDD\u5B58 OpenClaw \u914D\u7F6E', en: '\u{1F4BE} Save OpenClaw Config' },
|
|
10955
|
+
openclaw_config_saved: { zh: 'OpenClaw \u914D\u7F6E\u5DF2\u4FDD\u5B58\uFF01', en: 'OpenClaw configuration saved!' },
|
|
10956
|
+
confirm_delete_binding: { zh: '\u786E\u5B9A\u5220\u9664\u6B64\u8DEF\u7531\u7ED1\u5B9A\uFF1F', en: 'Delete this routing binding?' },
|
|
10957
|
+
binding_deleted: { zh: '\u8DEF\u7531\u7ED1\u5B9A\u5DF2\u5220\u9664', en: 'Routing binding deleted' },
|
|
10958
|
+
confirm_delete_oc_agent: { zh: '\u786E\u5B9A\u5220\u9664\u667A\u80FD\u4F53 "{name}" \u5417\uFF1F', en: 'Are you sure you want to delete agent "{name}"?' },
|
|
10959
|
+
confirm_delete_account: { zh: '\u786E\u5B9A\u5220\u9664\u6B64\u8D26\u53F7\uFF1F', en: 'Are you sure you want to delete this account?' },
|
|
10960
|
+
// Misc
|
|
10961
|
+
add_item: { zh: '\u2795 \u6DFB\u52A0', en: '\u2795 Add' },
|
|
10962
|
+
remove_item: { zh: '\u{1F5D1}\uFE0F \u5220\u9664', en: '\u{1F5D1}\uFE0F Delete' },
|
|
10963
|
+
edit_item: { zh: '\u270F\uFE0F \u7F16\u8F91', en: '\u270F\uFE0F Edit' },
|
|
10964
|
+
tab_defaults: { zh: '\u{1F916} \u9ED8\u8BA4\u8BBE\u7F6E', en: '\u{1F916} Defaults' },
|
|
10965
|
+
tab_agents: { zh: '\u{1F4CB} \u667A\u80FD\u4F53', en: '\u{1F4CB} Agents' },
|
|
10966
|
+
tab_bindings: { zh: '\u{1F517} \u7ED1\u5B9A', en: '\u{1F517} Bindings' },
|
|
10967
|
+
tab_channels: { zh: '\u{1F4E1} \u9891\u9053', en: '\u{1F4E1} Channels' },
|
|
10968
|
+
};
|
|
10969
|
+
function t(key) { return (_T[key] && _T[key][_lang]) || key; }
|
|
10970
|
+
function translateStatic() { document.querySelectorAll('[data-i18n]').forEach(el => { const k = el.getAttribute('data-i18n'); if (k && _T[k]) { el.textContent = _T[k][_lang] || el.textContent; } }); document.querySelectorAll('[data-i18n-ph]').forEach(el => { const k = el.getAttribute('data-i18n-ph'); if (k && _T[k]) { el.placeholder = _T[k][_lang] || el.placeholder; } }); }
|
|
10971
|
+
|
|
10124
10972
|
// \u2500\u2500 Globals \u2500\u2500
|
|
10125
10973
|
const API = '/api';
|
|
10126
10974
|
let currentPage = 'dashboard';
|
|
@@ -10146,7 +10994,7 @@ function showOfflineBanner() {
|
|
|
10146
10994
|
if (offlineBannerEl) return;
|
|
10147
10995
|
offlineBannerEl = document.createElement('div');
|
|
10148
10996
|
offlineBannerEl.className = 'offline-banner';
|
|
10149
|
-
offlineBannerEl.innerHTML = '<span class="offline-icon">\u{1F50C}</span><span>
|
|
10997
|
+
offlineBannerEl.innerHTML = '<span class="offline-icon">\u{1F50C}</span><span>' + t('offline_msg') + '</span>';
|
|
10150
10998
|
const mainContent = document.querySelector('.main');
|
|
10151
10999
|
if (mainContent) {
|
|
10152
11000
|
mainContent.insertBefore(offlineBannerEl, mainContent.firstChild);
|
|
@@ -10204,13 +11052,13 @@ function escHtml(s) { if (s == null) return ''; const d = document.createElement
|
|
|
10204
11052
|
function timeAgo(iso) {
|
|
10205
11053
|
if (!iso) return '\u2014';
|
|
10206
11054
|
const diff = Date.now() - new Date(iso).getTime();
|
|
10207
|
-
if (diff < 0) return '
|
|
11055
|
+
if (diff < 0) return t('just_now');
|
|
10208
11056
|
const m = Math.floor(diff / 60000), h = Math.floor(m / 60), d = Math.floor(h / 24);
|
|
10209
|
-
if (m < 1) return '
|
|
11057
|
+
if (m < 1) return t('just_now'); if (m < 60) return m + t('min_ago'); if (h < 24) return h + t('h_ago'); if (d < 30) return d + t('d_ago');
|
|
10210
11058
|
return new Date(iso).toLocaleDateString();
|
|
10211
11059
|
}
|
|
10212
11060
|
function maskKey(s) { if (!s || s.length < 12) return s || ''; return s.substring(0, 6) + '\u2022\u2022\u2022\u2022\u2022\u2022' + s.slice(-4); }
|
|
10213
|
-
function copyText(text) { navigator.clipboard.writeText(text).then(() => toast('
|
|
11061
|
+
function copyText(text) { navigator.clipboard.writeText(text).then(() => toast(t('copied_clipboard'), 'ok')).catch(() => toast(t('copy_failed'), 'err')); }
|
|
10214
11062
|
|
|
10215
11063
|
// \u2500\u2500 Modal \u2500\u2500
|
|
10216
11064
|
function showModal(id) { show(id); }
|
|
@@ -10240,6 +11088,7 @@ function loadPage(page) {
|
|
|
10240
11088
|
case 'friends': loadFriends(); break;
|
|
10241
11089
|
case 'models': loadModels(); break;
|
|
10242
11090
|
case 'settings': loadSettings(); break;
|
|
11091
|
+
case 'openclaw': loadOpenClawConfig(); break;
|
|
10243
11092
|
}
|
|
10244
11093
|
}
|
|
10245
11094
|
|
|
@@ -10248,15 +11097,15 @@ function loadPage(page) {
|
|
|
10248
11097
|
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
10249
11098
|
async function loadDashboard() {
|
|
10250
11099
|
const el = $('#dashboard-content');
|
|
10251
|
-
html(el, '<div class="loading-mask"><div class="spinner"></div>
|
|
11100
|
+
html(el, '<div class="loading-mask"><div class="spinner"></div>' + t('loading_dashboard') + '</div>');
|
|
10252
11101
|
const results = await Promise.allSettled([api('/status'), api('/friends'), api('/identity'), api('/mgmt-url')]);
|
|
10253
11102
|
const status = results[0].status === 'fulfilled' ? results[0].value : { error: results[0].reason?.message || 'Failed' };
|
|
10254
11103
|
const friends = results[1].status === 'fulfilled' ? results[1].value : { friends: [], error: true };
|
|
10255
11104
|
const identity = results[2].status === 'fulfilled' ? results[2].value : { agentId: '\u2014', publicKeyFingerprint: '\u2014', serverUrl: '\u2014', connected: false };
|
|
10256
11105
|
const mgmtUrl = results[3].status === 'fulfilled' ? results[3].value : { mgmtUrl: window.location.origin };
|
|
10257
|
-
if (status.error) { html(el, '<div class="empty"><div class="icon">\u26A0\uFE0F</div><p>
|
|
11106
|
+
if (status.error) { html(el, '<div class="empty"><div class="icon">\u26A0\uFE0F</div><p>' + t('failed_connect') + '</p></div>'); return; }
|
|
10258
11107
|
const connCls = status.connected ? 'dot-ok' : 'dot-err';
|
|
10259
|
-
const connText = status.connected ? '
|
|
11108
|
+
const connText = status.connected ? t('connected') : t('disconnected');
|
|
10260
11109
|
const friendList = friends.friends || [];
|
|
10261
11110
|
const aiFriends = friendList.filter(f => f.friendType === 'ai').length;
|
|
10262
11111
|
const humanFriends = friendList.filter(f => f.friendType !== 'ai').length;
|
|
@@ -10266,7 +11115,7 @@ async function loadDashboard() {
|
|
|
10266
11115
|
<div class="stats-grid">
|
|
10267
11116
|
<div class="stat-card">
|
|
10268
11117
|
<div class="stat-icon" style="background:var(--accent-bg)">\u{1F4E1}</div>
|
|
10269
|
-
<div class="stat-label"
|
|
11118
|
+
<div class="stat-label">\${t('server_status')}</div>
|
|
10270
11119
|
<div class="stat-value" style="font-size:16px;display:flex;align-items:center;gap:8px">
|
|
10271
11120
|
<span class="dot \${connCls}"></span> \${connText}
|
|
10272
11121
|
</div>
|
|
@@ -10274,42 +11123,42 @@ async function loadDashboard() {
|
|
|
10274
11123
|
</div>
|
|
10275
11124
|
<div class="stat-card">
|
|
10276
11125
|
<div class="stat-icon" style="background:var(--ok-bg)">\u{1F465}</div>
|
|
10277
|
-
<div class="stat-label"
|
|
11126
|
+
<div class="stat-label">\${t('total_friends')}</div>
|
|
10278
11127
|
<div class="stat-value">\${friendList.length}</div>
|
|
10279
11128
|
<div class="stat-sub">\${aiFriends} AI \xB7 \${humanFriends} Human</div>
|
|
10280
11129
|
</div>
|
|
10281
11130
|
<div class="stat-card">
|
|
10282
11131
|
<div class="stat-icon" style="background:var(--info-bg)">\u{1F517}</div>
|
|
10283
|
-
<div class="stat-label"
|
|
11132
|
+
<div class="stat-label">\${t('active_sessions')}</div>
|
|
10284
11133
|
<div class="stat-value">\${status.sessionCount || 0}</div>
|
|
10285
|
-
<div class="stat-sub"
|
|
11134
|
+
<div class="stat-sub">\${t('encrypted_sessions')}</div>
|
|
10286
11135
|
</div>
|
|
10287
11136
|
<div class="stat-card">
|
|
10288
11137
|
<div class="stat-icon" style="background:var(--warn-bg)">\u{1F511}</div>
|
|
10289
|
-
<div class="stat-label"
|
|
11138
|
+
<div class="stat-label">\${t('agent_id')}</div>
|
|
10290
11139
|
<div class="stat-value mono" style="font-size:13px">\${escHtml(status.agentId)}</div>
|
|
10291
|
-
<div class="stat-sub"
|
|
11140
|
+
<div class="stat-sub">\${t('fingerprint')}: \${escHtml(status.fingerprint)}</div>
|
|
10292
11141
|
</div>
|
|
10293
11142
|
</div>
|
|
10294
11143
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
|
|
10295
11144
|
<div class="card">
|
|
10296
|
-
<div class="card-header"><div class="card-title">\u{1F4CB}
|
|
11145
|
+
<div class="card-header"><div class="card-title">\u{1F4CB} \${t('recent_friends')}</div><button class="btn btn-sm btn-ghost" onclick="navigate('friends')">\${t('view_all')}</button></div>
|
|
10297
11146
|
\${renderMiniFriendList(friendList.slice(0, 5))}
|
|
10298
11147
|
</div>
|
|
10299
11148
|
<div class="card">
|
|
10300
|
-
<div class="card-header"><div class="card-title">\u{1F916}
|
|
10301
|
-
<div class="detail-row"><div class="detail-key"
|
|
10302
|
-
<div class="detail-row"><div class="detail-key"
|
|
10303
|
-
<div class="detail-row"><div class="detail-key"
|
|
10304
|
-
<div class="detail-row"><div class="detail-key"
|
|
10305
|
-
<div class="detail-row"><div class="detail-key"
|
|
11149
|
+
<div class="card-header"><div class="card-title">\u{1F916} \${t('identity_info')}</div></div>
|
|
11150
|
+
<div class="detail-row"><div class="detail-key">\${t('agent_id')}</div><div class="detail-val mono" style="cursor:pointer" onclick="copyText('\${identity.agentId}')">\${escHtml(identity.agentId)} \u{1F4CB}</div></div>
|
|
11151
|
+
<div class="detail-row"><div class="detail-key">\${t('fingerprint')}</div><div class="detail-val mono">\${escHtml(identity.publicKeyFingerprint)}</div></div>
|
|
11152
|
+
<div class="detail-row"><div class="detail-key">\${t('server_url')}</div><div class="detail-val mono" style="cursor:pointer" onclick="copyText('\${identity.serverUrl}')">\${escHtml(identity.serverUrl)} \u{1F4CB}</div></div>
|
|
11153
|
+
<div class="detail-row"><div class="detail-key">\${t('connection')}</div><div class="detail-val"><span class="badge badge-\${identity.connected ? 'ok' : 'danger'}">\${identity.connected ? t('online') : t('offline')}</span></div></div>
|
|
11154
|
+
<div class="detail-row"><div class="detail-key">\${t('plugin_version')}</div><div class="detail-val"><span class="badge badge-accent">v1.2.0</span></div></div>
|
|
10306
11155
|
</div>
|
|
10307
11156
|
</div>
|
|
10308
11157
|
<div class="card" style="margin-top:0">
|
|
10309
|
-
<div class="card-header"><div class="card-title">\u{1F5A5}\uFE0F
|
|
10310
|
-
<div class="detail-row"><div class="detail-key"
|
|
10311
|
-
<div class="detail-row"><div class="detail-key"
|
|
10312
|
-
<div class="detail-row"><div class="detail-key"
|
|
11158
|
+
<div class="card-header"><div class="card-title">\u{1F5A5}\uFE0F \${t('mgmt_ui_access')}</div></div>
|
|
11159
|
+
<div class="detail-row"><div class="detail-key">\${t('current_url')}</div><div class="detail-val"><a href="\${escHtml(mgmtLink)}" target="_blank" style="color:var(--info);text-decoration:underline">\${escHtml(mgmtLink)}</a></div></div>
|
|
11160
|
+
<div class="detail-row"><div class="detail-key">\${t('local_access')}</div><div class="detail-val"><a href="http://127.0.0.1:6109" target="_blank" style="color:var(--info);text-decoration:underline">http://127.0.0.1:6109</a> <button class="btn btn-sm btn-primary" onclick="window.open('http://127.0.0.1:6109','_blank')" style="margin-left:8px">\u{1F517} \${t('open')}</button></div></div>
|
|
11161
|
+
<div class="detail-row"><div class="detail-key">\${t('gateway_path')}</div><div class="detail-val mono">/plugins/aicq-chat/</div></div>
|
|
10313
11162
|
</div>
|
|
10314
11163
|
\\\`);
|
|
10315
11164
|
|
|
@@ -10319,7 +11168,7 @@ async function loadDashboard() {
|
|
|
10319
11168
|
}
|
|
10320
11169
|
|
|
10321
11170
|
function renderMiniFriendList(friends) {
|
|
10322
|
-
if (!friends.length) return '<div class="empty"><p>
|
|
11171
|
+
if (!friends.length) return '<div class="empty"><p>' + t('no_friends_yet') + '</p></div>';
|
|
10323
11172
|
let html = '';
|
|
10324
11173
|
friends.forEach(f => {
|
|
10325
11174
|
html += '<div class="detail-row"><div class="detail-key"><span class="badge badge-' + (f.friendType === 'ai' ? 'info' : 'ghost') + '">' + escHtml(f.friendType || '?') + '</span></div><div class="detail-val mono truncate" style="font-size:12px">' + escHtml(f.id) + '</div></div>';
|
|
@@ -10332,7 +11181,7 @@ function renderMiniFriendList(friends) {
|
|
|
10332
11181
|
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
10333
11182
|
async function loadAgents() {
|
|
10334
11183
|
const el = $('#agents-content');
|
|
10335
|
-
html(el, '<div class="loading-mask"><div class="spinner"></div>
|
|
11184
|
+
html(el, '<div class="loading-mask"><div class="spinner"></div>' + t('loading_agents') + '</div>');
|
|
10336
11185
|
const data = await api('/agents');
|
|
10337
11186
|
if (data.error) { html(el, '<div class="empty"><div class="icon">\u26A0\uFE0F</div><p>' + escHtml(data.error) + '</p></div>'); return; }
|
|
10338
11187
|
|
|
@@ -10342,13 +11191,16 @@ async function loadAgents() {
|
|
|
10342
11191
|
|
|
10343
11192
|
let rows = '';
|
|
10344
11193
|
agents.forEach((a, i) => {
|
|
10345
|
-
const
|
|
10346
|
-
const
|
|
10347
|
-
const
|
|
11194
|
+
const isProviderModel = a._source === 'provider-model';
|
|
11195
|
+
const modelBadge = a.model ? '<span class="badge badge-accent">' + escHtml(a.model) + '</span>' : '<span class="badge badge-ghost">' + t('default_model') + '</span>';
|
|
11196
|
+
const providerName = isProviderModel ? escHtml(capitalizeProvider(a.provider || '')) : escHtml(a.provider || '');
|
|
11197
|
+
const providerBadge = providerName ? '<span class="tag">' + providerName + (a.isDefault ? ' \u2B50' : '') + '</span>' : '';
|
|
11198
|
+
const statusBadge = a.enabled !== false ? '<span class="badge badge-ok">' + t('active') + '</span>' : '<span class="badge badge-warn">' + t('disabled') + '</span>';
|
|
11199
|
+
const defaultBadge = a.isDefault ? ' <span class="badge badge-warn" style="font-size:10px">' + t('default_model_badge') + '</span>' : '';
|
|
10348
11200
|
|
|
10349
11201
|
rows += \\\`<tr>
|
|
10350
|
-
<td>\${statusBadge}</td>
|
|
10351
|
-
<td><div style="font-weight:600">\${escHtml(a.name ||
|
|
11202
|
+
<td>\${statusBadge}\${defaultBadge}</td>
|
|
11203
|
+
<td><div style="font-weight:600">\${escHtml(a.name || 'Agent ' + (i + 1))}</div><div class="mono" style="font-size:11px;color:var(--text3)">\${isProviderModel ? escHtml(a._configPath || '') : escHtml(a.id || '\u2014')}</div></td>
|
|
10352
11204
|
<td>\${modelBadge}</td>
|
|
10353
11205
|
<td>\${providerBadge}</td>
|
|
10354
11206
|
<td>\${escHtml(a.systemPrompt ? a.systemPrompt.substring(0, 60) + '...' : '\u2014')}</td>
|
|
@@ -10364,23 +11216,23 @@ async function loadAgents() {
|
|
|
10364
11216
|
|
|
10365
11217
|
if (!agents.length) {
|
|
10366
11218
|
html(el, \\\`
|
|
10367
|
-
<p class="section-desc"
|
|
10368
|
-
<div class="empty"><div class="icon">\u{1F916}</div><p
|
|
11219
|
+
<p class="section-desc">\${t('agent_list_from')} <strong>\${escHtml(configSource)}</strong></p>
|
|
11220
|
+
<div class="empty"><div class="icon">\u{1F916}</div><p>\${t('no_agents_configured')}</p><p class="sub">\${t('add_agents_hint')}</p></div>
|
|
10369
11221
|
\\\`);
|
|
10370
11222
|
return;
|
|
10371
11223
|
}
|
|
10372
11224
|
|
|
10373
11225
|
html(el, \\\`
|
|
10374
11226
|
<div class="toolbar">
|
|
10375
|
-
<div class="search-box"><input type="text" placeholder="
|
|
10376
|
-
<button class="btn btn-sm btn-primary" onclick="showAddAgentModal()"
|
|
10377
|
-
<button class="btn btn-sm btn-default" onclick="loadAgents()">\u{1F504}
|
|
11227
|
+
<div class="search-box"><input type="text" placeholder="\${t('search_agents')}" id="agent-search" oninput="filterAgentTable()"></div>
|
|
11228
|
+
<button class="btn btn-sm btn-primary" onclick="showAddAgentModal()">\${t('add_agent')}</button>
|
|
11229
|
+
<button class="btn btn-sm btn-default" onclick="loadAgents()">\u{1F504} \${t('refresh')}</button>
|
|
10378
11230
|
</div>
|
|
10379
|
-
<p class="section-desc"
|
|
11231
|
+
<p class="section-desc">\${t('agent_list_from')} <strong style="color:var(--accent2)">\${escHtml(configSource)}</strong>. \${t('total_label')}: <strong>\${agents.length}</strong> \${t('agents_configured')}.</p>
|
|
10380
11232
|
<div class="card" style="padding:0;overflow:hidden">
|
|
10381
11233
|
<div style="overflow-x:auto">
|
|
10382
11234
|
<table>
|
|
10383
|
-
<thead><tr><th style="width:
|
|
11235
|
+
<thead><tr><th style="width:80px">\${t('status')}</th><th>\${t('agent')}</th><th>\${t('model')}</th><th>\${t('provider')}</th><th>\${t('system_prompt')}</th><th style="width:90px">\${t('actions')}</th></tr></thead>
|
|
10384
11236
|
<tbody id="agent-table-body">\${rows}</tbody>
|
|
10385
11237
|
</table>
|
|
10386
11238
|
</div>
|
|
@@ -10388,6 +11240,17 @@ async function loadAgents() {
|
|
|
10388
11240
|
\\\`);
|
|
10389
11241
|
}
|
|
10390
11242
|
|
|
11243
|
+
function capitalizeProvider(id) {
|
|
11244
|
+
const names = {
|
|
11245
|
+
modelscope: 'ModelScope', zhipu: 'Zhipu AI', qwen: 'Qwen', doubao: 'Doubao',
|
|
11246
|
+
moonshot: 'Moonshot', minimax: 'MiniMax', stepfun: 'StepFun', baidu: 'Baidu',
|
|
11247
|
+
spark: 'Spark', deepseek: 'DeepSeek', openai: 'OpenAI', anthropic: 'Anthropic',
|
|
11248
|
+
google: 'Google AI', groq: 'Groq', ollama: 'Ollama', openrouter: 'OpenRouter',
|
|
11249
|
+
mistral: 'Mistral AI', together: 'Together AI', fireworks: 'Fireworks AI',
|
|
11250
|
+
};
|
|
11251
|
+
return names[id] || (id ? id.charAt(0).toUpperCase() + id.slice(1) : '\u2014');
|
|
11252
|
+
}
|
|
11253
|
+
|
|
10391
11254
|
function filterAgentTable() {
|
|
10392
11255
|
const q = ($('#agent-search')?.value || '').toLowerCase();
|
|
10393
11256
|
$$('#agent-table-body tr').forEach(tr => {
|
|
@@ -10406,23 +11269,34 @@ function viewAgent(index) {
|
|
|
10406
11269
|
details += '<div class="detail-row"><div class="detail-key">' + escHtml(k) + '</div><div class="detail-val mono" style="font-size:12px;cursor:pointer" onclick="copyText(decodeURIComponent(\\'' + encodeURIComponent(String(v)) + '\\'))">' + display + ' \u{1F4CB}</div></div>';
|
|
10407
11270
|
}
|
|
10408
11271
|
}
|
|
10409
|
-
html('#view-agent-body', details || '<div class="empty"><p>
|
|
10410
|
-
$('#view-agent-title').textContent = a.name || a.id || '
|
|
11272
|
+
html('#view-agent-body', details || '<div class="empty"><p>' + t('no_data') + '</p></div>');
|
|
11273
|
+
$('#view-agent-title').textContent = a.name || a.id || t('agent');
|
|
10411
11274
|
showModal('modal-view-agent');
|
|
10412
11275
|
}
|
|
10413
11276
|
|
|
10414
11277
|
async function deleteAgent(index) {
|
|
10415
|
-
|
|
10416
|
-
const
|
|
10417
|
-
if (
|
|
10418
|
-
|
|
11278
|
+
const agents = window._lastAgentsData?.agents || [];
|
|
11279
|
+
const a = agents[index];
|
|
11280
|
+
if (!a) return;
|
|
11281
|
+
if (!confirm(t('confirm_delete_agent'))) return;
|
|
11282
|
+
let identifier;
|
|
11283
|
+
if (a._source === 'provider-model') {
|
|
11284
|
+
identifier = 'provider:' + (a._providerId || '') + ':' + (a._modelIndex || 0);
|
|
11285
|
+
} else {
|
|
11286
|
+
identifier = index;
|
|
11287
|
+
}
|
|
11288
|
+
const r = await api('/agents/' + encodeURIComponent(identifier), { method: 'DELETE' });
|
|
11289
|
+
if (r.success) { toast(t('agent_deleted'), 'ok'); loadAgents(); }
|
|
11290
|
+
else { toast(r.message || r.error || t('delete_failed'), 'err'); }
|
|
10419
11291
|
}
|
|
10420
11292
|
|
|
10421
11293
|
let _editAgentIndex = null;
|
|
11294
|
+
let _editAgentIsProviderModel = false;
|
|
10422
11295
|
|
|
10423
11296
|
function showAddAgentModal() {
|
|
10424
11297
|
_editAgentIndex = null;
|
|
10425
|
-
|
|
11298
|
+
_editAgentIsProviderModel = false;
|
|
11299
|
+
$('#agent-form-title').textContent = t('add_new_agent');
|
|
10426
11300
|
$('#agent-form-name').value = '';
|
|
10427
11301
|
$('#agent-form-id').value = '';
|
|
10428
11302
|
$('#agent-form-model').value = '';
|
|
@@ -10442,9 +11316,10 @@ function showEditAgentModal(index) {
|
|
|
10442
11316
|
const a = agents[index];
|
|
10443
11317
|
if (!a) return;
|
|
10444
11318
|
_editAgentIndex = index;
|
|
10445
|
-
|
|
11319
|
+
_editAgentIsProviderModel = a._source === 'provider-model';
|
|
11320
|
+
$('#agent-form-title').textContent = t('edit_agent');
|
|
10446
11321
|
$('#agent-form-name').value = a.name || '';
|
|
10447
|
-
$('#agent-form-id').value = a.id || '';
|
|
11322
|
+
$('#agent-form-id').value = a._source === 'provider-model' ? (a._configPath || '') : (a.id || '');
|
|
10448
11323
|
$('#agent-form-model').value = a.model || '';
|
|
10449
11324
|
$('#agent-form-provider').value = a.provider || '';
|
|
10450
11325
|
$('#agent-form-prompt').value = a.systemPrompt || '';
|
|
@@ -10453,6 +11328,13 @@ function showEditAgentModal(index) {
|
|
|
10453
11328
|
$('#agent-form-max-tokens').value = a.maxTokens ?? 4096;
|
|
10454
11329
|
$('#agent-form-top-p').value = a.topP ?? 1;
|
|
10455
11330
|
$('#agent-form-tools').value = Array.isArray(a.tools) ? a.tools.join(', ') : (a.tools || '');
|
|
11331
|
+
if (a._source === 'provider-model') {
|
|
11332
|
+
$('#agent-form-id').readOnly = true;
|
|
11333
|
+
$('#agent-form-id').title = a._configPath || '';
|
|
11334
|
+
} else {
|
|
11335
|
+
$('#agent-form-id').readOnly = false;
|
|
11336
|
+
$('#agent-form-id').title = '';
|
|
11337
|
+
}
|
|
10456
11338
|
showModal('modal-add-agent');
|
|
10457
11339
|
}
|
|
10458
11340
|
|
|
@@ -10475,23 +11357,29 @@ async function saveAgent() {
|
|
|
10475
11357
|
tools: toolsRaw ? toolsRaw.split(',').map(t => t.trim()).filter(Boolean) : [],
|
|
10476
11358
|
};
|
|
10477
11359
|
|
|
10478
|
-
if (!agent.name) { toast('
|
|
11360
|
+
if (!agent.name) { toast(t('agent_name_required'), 'warn'); return; }
|
|
10479
11361
|
|
|
10480
11362
|
let r;
|
|
10481
11363
|
if (_editAgentIndex !== null) {
|
|
10482
|
-
|
|
10483
|
-
|
|
11364
|
+
let identifier;
|
|
11365
|
+
if (_editAgentIsProviderModel) {
|
|
11366
|
+
const a = (window._lastAgentsData?.agents || [])[_editAgentIndex];
|
|
11367
|
+
identifier = 'provider:' + (a?._providerId || '') + ':' + (a?._modelIndex || 0);
|
|
11368
|
+
} else {
|
|
11369
|
+
identifier = _editAgentIndex;
|
|
11370
|
+
}
|
|
11371
|
+
r = await api('/agents/' + encodeURIComponent(identifier), { method: 'PUT', body: JSON.stringify({ agent }) });
|
|
10484
11372
|
} else {
|
|
10485
11373
|
// Add new
|
|
10486
11374
|
r = await api('/agents', { method: 'POST', body: JSON.stringify({ agent }) });
|
|
10487
11375
|
}
|
|
10488
11376
|
|
|
10489
11377
|
if (r.success) {
|
|
10490
|
-
toast(_editAgentIndex !== null ? '
|
|
11378
|
+
toast(_editAgentIndex !== null ? t('agent_updated') : t('agent_added'), 'ok');
|
|
10491
11379
|
hideModal('modal-add-agent');
|
|
10492
11380
|
loadAgents();
|
|
10493
11381
|
} else {
|
|
10494
|
-
toast(r.message || r.error || '
|
|
11382
|
+
toast(r.message || r.error || t('failed'), 'err');
|
|
10495
11383
|
}
|
|
10496
11384
|
}
|
|
10497
11385
|
|
|
@@ -10502,7 +11390,7 @@ let friendsFilter = 'all';
|
|
|
10502
11390
|
|
|
10503
11391
|
async function loadFriends() {
|
|
10504
11392
|
const el = $('#friends-content');
|
|
10505
|
-
html(el, '<div class="loading-mask"><div class="spinner"></div>
|
|
11393
|
+
html(el, '<div class="loading-mask"><div class="spinner"></div>' + t('loading_friends') + '</div>');
|
|
10506
11394
|
const results = await Promise.allSettled([api('/friends'), api('/friends/requests'), api('/sessions')]);
|
|
10507
11395
|
const friends = results[0].status === 'fulfilled' ? results[0].value : { friends: [] };
|
|
10508
11396
|
const requests = results[1].status === 'fulfilled' ? results[1].value : { requests: [] };
|
|
@@ -10521,9 +11409,9 @@ async function loadFriends() {
|
|
|
10521
11409
|
const sessCount = (sessions.sessions || []).length;
|
|
10522
11410
|
|
|
10523
11411
|
html('#friends-tabs', \\\`
|
|
10524
|
-
<button class="filter-btn \${friendsSubTab==='friends'?'active':''}" onclick="friendsSubTab='friends';loadFriends()">\u{1F465}
|
|
10525
|
-
<button class="filter-btn \${friendsSubTab==='requests'?'active':''}" onclick="friendsSubTab='requests';loadFriends()">\u{1F4E8}
|
|
10526
|
-
<button class="filter-btn \${friendsSubTab==='sessions'?'active':''}" onclick="friendsSubTab='sessions';loadFriends()">\u{1F517}
|
|
11412
|
+
<button class="filter-btn \${friendsSubTab==='friends'?'active':''}" onclick="friendsSubTab='friends';loadFriends()">\u{1F465} \${t('friends')} (<span id="fc">\${friendCount}</span>)</button>
|
|
11413
|
+
<button class="filter-btn \${friendsSubTab==='requests'?'active':''}" onclick="friendsSubTab='requests';loadFriends()">\u{1F4E8} \${t('requests')} (<span id="rc">\${reqCount}</span>)</button>
|
|
11414
|
+
<button class="filter-btn \${friendsSubTab==='sessions'?'active':''}" onclick="friendsSubTab='sessions';loadFriends()">\u{1F517} \${t('sessions')} (<span id="sc">\${sessCount}</span>)</button>
|
|
10527
11415
|
\\\`);
|
|
10528
11416
|
|
|
10529
11417
|
window._friendsData = friends;
|
|
@@ -10558,24 +11446,24 @@ function renderFriendsList(friends) {
|
|
|
10558
11446
|
|
|
10559
11447
|
html(el, \\\`
|
|
10560
11448
|
<div class="toolbar">
|
|
10561
|
-
<div class="search-box"><input type="text" placeholder="
|
|
11449
|
+
<div class="search-box"><input type="text" placeholder="\${t('search_friends')}" id="friend-search" oninput="filterFriendTable()"></div>
|
|
10562
11450
|
<div class="filter-group">
|
|
10563
|
-
<button class="filter-btn \${friendsFilter==='all'?'active':''}" onclick="friendsFilter='all';filterFriendTable()"
|
|
10564
|
-
<button class="filter-btn \${friendsFilter==='ai'?'active':''}" onclick="friendsFilter='ai';filterFriendTable()"
|
|
10565
|
-
<button class="filter-btn \${friendsFilter==='human'?'active':''}" onclick="friendsFilter='human';filterFriendTable()"
|
|
11451
|
+
<button class="filter-btn \${friendsFilter==='all'?'active':''}" onclick="friendsFilter='all';filterFriendTable()">\${t('all')}</button>
|
|
11452
|
+
<button class="filter-btn \${friendsFilter==='ai'?'active':''}" onclick="friendsFilter='ai';filterFriendTable()">\${t('ai')}</button>
|
|
11453
|
+
<button class="filter-btn \${friendsFilter==='human'?'active':''}" onclick="friendsFilter='human';filterFriendTable()">\${t('human')}</button>
|
|
10566
11454
|
</div>
|
|
10567
11455
|
<span style="flex:1"></span>
|
|
10568
|
-
<button class="btn btn-sm btn-primary" onclick="showAddFriendModal()" \${isOffline ? 'disabled title="
|
|
11456
|
+
<button class="btn btn-sm btn-primary" onclick="showAddFriendModal()" \${isOffline ? 'disabled title="' + t('unavailable_offline') + '"' : ''}>\${t('add_friend')}</button>
|
|
10569
11457
|
<button class="btn btn-sm btn-default" onclick="loadFriends()">\u{1F504}</button>
|
|
10570
11458
|
</div>
|
|
10571
11459
|
<div class="card" style="padding:0;overflow:hidden">
|
|
10572
11460
|
<div style="overflow-x:auto;max-height:calc(100vh - 280px);overflow-y:auto">
|
|
10573
11461
|
<table>
|
|
10574
|
-
<thead><tr><th style="width:60px"
|
|
11462
|
+
<thead><tr><th style="width:60px">\${t('type')}</th><th>\${t('friend_label')}</th><th>\${t('permissions')}</th><th>\${t('fingerprint')}</th><th>\${t('last_message')}</th><th style="width:80px">\${t('actions')}</th></tr></thead>
|
|
10575
11463
|
<tbody id="friend-table-body">\${rows}</tbody>
|
|
10576
11464
|
</table>
|
|
10577
11465
|
</div>
|
|
10578
|
-
\${!friends.length ? '<div class="empty"><div class="icon">\u{1F465}</div><p>
|
|
11466
|
+
\${!friends.length ? '<div class="empty"><div class="icon">\u{1F465}</div><p>' + t('no_friends_yet') + '</p><p class="sub">' + t('add_friend_hint') + '</p></div>' : ''}
|
|
10579
11467
|
</div>
|
|
10580
11468
|
\\\`);
|
|
10581
11469
|
}
|
|
@@ -10600,18 +11488,18 @@ function renderRequestsList(requests) {
|
|
|
10600
11488
|
<td><span class="badge badge-\${stCls}">\${escHtml(r.status)}</span></td>
|
|
10601
11489
|
<td>\${timeAgo(r.createdAt)}</td>
|
|
10602
11490
|
<td>
|
|
10603
|
-
\${r.status === 'pending' ? '<div class="actions-cell"><button class="btn btn-sm btn-ok" onclick="acceptFriendReq(\\'' + escHtml(r.id) + '\\')">\u2713
|
|
11491
|
+
\${r.status === 'pending' ? '<div class="actions-cell"><button class="btn btn-sm btn-ok" onclick="acceptFriendReq(\\'' + escHtml(r.id) + '\\')">\u2713 \${t('accept')}</button><button class="btn btn-sm btn-danger" onclick="rejectFriendReq(\\'' + escHtml(r.id) + '\\')">\u2717 \${t('reject')}</button></div>' : '\u2014'}
|
|
10604
11492
|
</td>
|
|
10605
11493
|
</tr>\\\`;
|
|
10606
11494
|
});
|
|
10607
11495
|
html(el, \\\`
|
|
10608
|
-
<div class="toolbar"><button class="btn btn-sm btn-default" onclick="loadFriends()">\u{1F504}
|
|
11496
|
+
<div class="toolbar"><button class="btn btn-sm btn-default" onclick="loadFriends()">\u{1F504} \${t('refresh')}</button></div>
|
|
10609
11497
|
<div class="card" style="padding:0;overflow:hidden">
|
|
10610
11498
|
<div style="overflow-x:auto"><table>
|
|
10611
|
-
<thead><tr><th
|
|
11499
|
+
<thead><tr><th>\${t('request_id')}</th><th>\${t('from')}</th><th>\${t('status')}</th><th>\${t('time')}</th><th style="width:160px">\${t('actions')}</th></tr></thead>
|
|
10612
11500
|
<tbody>\${rows}</tbody>
|
|
10613
11501
|
</table></div>
|
|
10614
|
-
\${!requests.length ? '<div class="empty"><div class="icon">\u{1F4E8}</div><p>
|
|
11502
|
+
\${!requests.length ? '<div class="empty"><div class="icon">\u{1F4E8}</div><p>' + t('no_pending_requests') + '</p></div>' : ''}
|
|
10615
11503
|
</div>
|
|
10616
11504
|
\\\`);
|
|
10617
11505
|
}
|
|
@@ -10623,17 +11511,17 @@ function renderSessionsList(sessions) {
|
|
|
10623
11511
|
rows += \\\`<tr>
|
|
10624
11512
|
<td class="mono" style="font-size:12px;cursor:pointer" onclick="copyText('\${escHtml(s.peerId)}')">\${escHtml(s.peerId)} \u{1F4CB}</td>
|
|
10625
11513
|
<td>\${timeAgo(s.createdAt)}</td>
|
|
10626
|
-
<td><span class="badge badge-info">\${s.messageCount} messages</span></td>
|
|
11514
|
+
<td><span class="badge badge-info">\${s.messageCount} \${t('messages')}</span></td>
|
|
10627
11515
|
</tr>\\\`;
|
|
10628
11516
|
});
|
|
10629
11517
|
html(el, \\\`
|
|
10630
|
-
<div class="toolbar"><button class="btn btn-sm btn-default" onclick="loadFriends()">\u{1F504}
|
|
11518
|
+
<div class="toolbar"><button class="btn btn-sm btn-default" onclick="loadFriends()">\u{1F504} \${t('refresh')}</button></div>
|
|
10631
11519
|
<div class="card" style="padding:0;overflow:hidden">
|
|
10632
11520
|
<div style="overflow-x:auto"><table>
|
|
10633
|
-
<thead><tr><th
|
|
11521
|
+
<thead><tr><th>\${t('peer_id')}</th><th>\${t('established')}</th><th>\${t('messages')}</th></tr></thead>
|
|
10634
11522
|
<tbody>\${rows}</tbody>
|
|
10635
11523
|
</table></div>
|
|
10636
|
-
\${!sessions.length ? '<div class="empty"><div class="icon">\u{1F517}</div><p>
|
|
11524
|
+
\${!sessions.length ? '<div class="empty"><div class="icon">\u{1F517}</div><p>' + t('no_active_sessions') + '</p></div>' : ''}
|
|
10637
11525
|
</div>
|
|
10638
11526
|
\\\`);
|
|
10639
11527
|
}
|
|
@@ -10641,18 +11529,18 @@ function renderSessionsList(sessions) {
|
|
|
10641
11529
|
function showAddFriendModal() { $('#add-friend-target').value = ''; showModal('modal-add-friend'); setTimeout(() => $('#add-friend-target')?.focus(), 100); }
|
|
10642
11530
|
async function addFriend() {
|
|
10643
11531
|
const target = $('#add-friend-target').value.trim();
|
|
10644
|
-
if (!target) { toast('
|
|
11532
|
+
if (!target) { toast(t('enter_temp_or_id'), 'warn'); return; }
|
|
10645
11533
|
hideModal('modal-add-friend');
|
|
10646
|
-
toast('
|
|
11534
|
+
toast(t('sending_request'), 'info');
|
|
10647
11535
|
const r = await api('/friends', { method: 'POST', body: JSON.stringify({ target }) });
|
|
10648
|
-
if (r.success) { toast(r.message || '
|
|
10649
|
-
else { toast(r.message || r.error || '
|
|
11536
|
+
if (r.success) { toast(r.message || t('friend_request_sent'), 'ok'); loadFriends(); }
|
|
11537
|
+
else { toast(r.message || r.error || t('failed_add_friend'), 'err'); }
|
|
10650
11538
|
}
|
|
10651
11539
|
async function removeFriend(id) {
|
|
10652
|
-
if (!confirm('
|
|
11540
|
+
if (!confirm(t('remove_friend_confirm') + id + '?')) return;
|
|
10653
11541
|
const r = await api('/friends/' + encodeURIComponent(id), { method: 'DELETE' });
|
|
10654
|
-
if (r.success) { toast('
|
|
10655
|
-
else { toast(r.message || r.error || '
|
|
11542
|
+
if (r.success) { toast(t('friend_removed'), 'ok'); loadFriends(); }
|
|
11543
|
+
else { toast(r.message || r.error || t('failed'), 'err'); }
|
|
10656
11544
|
}
|
|
10657
11545
|
|
|
10658
11546
|
let _editFriendId = null;
|
|
@@ -10667,16 +11555,16 @@ async function saveFriendPerms() {
|
|
|
10667
11555
|
if ($('#perm-chat').checked) perms.push('chat');
|
|
10668
11556
|
if ($('#perm-exec').checked) perms.push('exec');
|
|
10669
11557
|
const r = await api('/friends/' + encodeURIComponent(_editFriendId) + '/permissions', { method: 'PUT', body: JSON.stringify({ permissions: perms }) });
|
|
10670
|
-
if (r.success) { toast('
|
|
10671
|
-
else { toast(r.message || r.error || '
|
|
11558
|
+
if (r.success) { toast(t('permissions_updated'), 'ok'); hideModal('modal-permissions'); loadFriends(); }
|
|
11559
|
+
else { toast(r.message || r.error || t('failed'), 'err'); }
|
|
10672
11560
|
}
|
|
10673
11561
|
async function acceptFriendReq(id) {
|
|
10674
11562
|
const r = await api('/friends/requests/' + encodeURIComponent(id) + '/accept', { method: 'POST', body: JSON.stringify({ permissions: ['chat'] }) });
|
|
10675
|
-
if (r.success) { toast('
|
|
11563
|
+
if (r.success) { toast(t('request_accepted'), 'ok'); loadFriends(); } else { toast(r.message || r.error || t('failed'), 'err'); }
|
|
10676
11564
|
}
|
|
10677
11565
|
async function rejectFriendReq(id) {
|
|
10678
11566
|
const r = await api('/friends/requests/' + encodeURIComponent(id) + '/reject', { method: 'POST', body: JSON.stringify({}) });
|
|
10679
|
-
if (r.success) { toast('
|
|
11567
|
+
if (r.success) { toast(t('request_rejected'), 'ok'); loadFriends(); } else { toast(r.message || r.error || t('failed'), 'err'); }
|
|
10680
11568
|
}
|
|
10681
11569
|
|
|
10682
11570
|
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
@@ -10686,36 +11574,81 @@ let _modelProviders = null;
|
|
|
10686
11574
|
|
|
10687
11575
|
async function loadModels() {
|
|
10688
11576
|
const el = $('#models-content');
|
|
10689
|
-
html(el, '<div class="loading-mask"><div class="spinner"></div>
|
|
11577
|
+
html(el, '<div class="loading-mask"><div class="spinner"></div>' + t('loading_models') + '</div>');
|
|
10690
11578
|
const data = await api('/models');
|
|
10691
11579
|
if (data.error) { html(el, '<div class="empty"><div class="icon">\u26A0\uFE0F</div><p>' + escHtml(data.error) + '</p></div>'); return; }
|
|
10692
11580
|
_modelProviders = data;
|
|
10693
11581
|
renderModels(data);
|
|
10694
11582
|
}
|
|
10695
11583
|
|
|
11584
|
+
function getProviderIcon(id) {
|
|
11585
|
+
const icons = {
|
|
11586
|
+
openai: '\u{1F7E2}', anthropic: '\u{1F7E0}', google: '\u{1F535}', ollama: '\u{1F7E3}', deepseek: '\u{1F537}',
|
|
11587
|
+
groq: '\u26A1', openrouter: '\u{1F310}', mistral: '\u{1F300}', together: '\u{1F52E}', fireworks: '\u{1F386}',
|
|
11588
|
+
modelscope: '\u{1F3D7}\uFE0F', zhipu: '\u{1F9E0}', qwen: '\u2601\uFE0F', doubao: '\u{1FAD8}', moonshot: '\u{1F319}',
|
|
11589
|
+
minimax: '\u{1F537}', stepfun: '\u{1F4C8}', baidu: '\u{1F50D}', spark: '\u2728',
|
|
11590
|
+
};
|
|
11591
|
+
if (id && id.startsWith('custom-')) return '\u{1F9E9}';
|
|
11592
|
+
return icons[id] || '\u26AA';
|
|
11593
|
+
}
|
|
11594
|
+
|
|
10696
11595
|
function renderModels(data) {
|
|
10697
11596
|
const el = $('#models-content');
|
|
10698
11597
|
const providers = data.providers || [];
|
|
10699
11598
|
const configured = providers.filter(p => p.configured).length;
|
|
11599
|
+
const defaultModel = data.defaultModel || '';
|
|
11600
|
+
|
|
11601
|
+
// Default model banner
|
|
11602
|
+
let defaultBanner = '';
|
|
11603
|
+
if (defaultModel) {
|
|
11604
|
+
defaultBanner = \\\`
|
|
11605
|
+
<div class="card" style="border-color:var(--warn);background:var(--warn-bg)">
|
|
11606
|
+
<div class="card-header">
|
|
11607
|
+
<div class="card-title" style="color:#d97706">\u2B50 \${t('default_model_global')}</div>
|
|
11608
|
+
</div>
|
|
11609
|
+
<div style="display:flex;align-items:center;gap:12px">
|
|
11610
|
+
<span class="badge badge-warn" style="font-size:13px;padding:4px 14px">\${escHtml(defaultModel)}</span>
|
|
11611
|
+
</div>
|
|
11612
|
+
</div>\\\`;
|
|
11613
|
+
}
|
|
10700
11614
|
|
|
10701
11615
|
let cards = '';
|
|
10702
11616
|
providers.forEach(p => {
|
|
10703
|
-
const icon = p.id
|
|
11617
|
+
const icon = getProviderIcon(p.id);
|
|
10704
11618
|
const statusBadge = p.configured
|
|
10705
|
-
? '<span class="badge badge-ok"
|
|
10706
|
-
: '<span class="badge badge-ghost">
|
|
10707
|
-
|
|
11619
|
+
? '<span class="badge badge-ok">' + t('key_set') + '</span>'
|
|
11620
|
+
: '<span class="badge badge-ghost">' + t('not_set') + '</span>';
|
|
11621
|
+
|
|
11622
|
+
// Show multi-model info
|
|
11623
|
+
let modelInfo = '';
|
|
11624
|
+
if (p.configured && p.modelCount > 0) {
|
|
11625
|
+
modelInfo = '<span class="prov-model">' + t('multi_model') + ': ' + p.modelCount + ' ' + t('models_under_provider') + '</span>';
|
|
11626
|
+
const shownModels = (p.models || []).slice(0, 3);
|
|
11627
|
+
shownModels.forEach(m => {
|
|
11628
|
+
const isDef = defaultModel === (m.id || '');
|
|
11629
|
+
modelInfo += '<span class="prov-model" style="margin-left:4px">' + escHtml(m.name || m.id || '') + (isDef ? ' \u2B50' : '') + '</span>';
|
|
11630
|
+
});
|
|
11631
|
+
if (p.modelCount > 3) {
|
|
11632
|
+
modelInfo += '<span class="prov-model" style="color:var(--text3);margin-left:4px">+' + (p.modelCount - 3) + ' more</span>';
|
|
11633
|
+
}
|
|
11634
|
+
} else if (p.modelId) {
|
|
11635
|
+
modelInfo = '<span class="prov-model">' + escHtml(p.modelId) + '</span>';
|
|
11636
|
+
}
|
|
10708
11637
|
|
|
10709
11638
|
cards += \\\`
|
|
10710
|
-
<div class="provider-card" onclick="showModelConfigModal('
|
|
11639
|
+
<div class="provider-card \${p.isCustom ? 'custom-provider' : ''}" onclick="\${p.isCustom ? "showEditCustomProviderModal('" + escHtml(p.id) + "')" : "showModelConfigModal('" + escHtml(p.id) + "')"}">
|
|
10711
11640
|
<div class="prov-head">
|
|
10712
|
-
<div class="prov-name">\${icon} \${escHtml(p.name)}</div>
|
|
11641
|
+
<div class="prov-name">\${icon} \${escHtml(p.name)}\${p.isCustom ? ' <span class="tag" style="background:var(--accent-bg);color:var(--accent2)">\u{1F9E9} Custom</span>' : ''}</div>
|
|
10713
11642
|
\${statusBadge}
|
|
10714
11643
|
</div>
|
|
10715
|
-
<div class="prov-desc">\${escHtml(p.description)}</div>
|
|
10716
|
-
\${
|
|
11644
|
+
<div class="prov-desc">\${escHtml(p.description || '')}</div>
|
|
11645
|
+
\${modelInfo}
|
|
10717
11646
|
<div class="prov-actions">
|
|
10718
|
-
|
|
11647
|
+
\${p.isCustom
|
|
11648
|
+
? '<button class="btn btn-sm btn-ok" onclick="event.stopPropagation();showEditCustomProviderModal(\\'' + escHtml(p.id) + '\\')">\u270F\uFE0F ' + t('configure') + '</button>' +
|
|
11649
|
+
'<button class="btn btn-sm btn-danger" onclick="event.stopPropagation();deleteCustomProvider(\\'' + escHtml(p.id) + '\\',\\'' + escHtml(p.name) + '\\')">\u{1F5D1}\uFE0F</button>'
|
|
11650
|
+
: '<button class="btn btn-sm btn-primary" onclick="event.stopPropagation();showModelConfigModal(\\'' + escHtml(p.id) + '\\')">' + t('configure') + '</button>'
|
|
11651
|
+
}
|
|
10719
11652
|
</div>
|
|
10720
11653
|
</div>\\\`;
|
|
10721
11654
|
});
|
|
@@ -10724,14 +11657,16 @@ function renderModels(data) {
|
|
|
10724
11657
|
if (data.currentModels && data.currentModels.length) {
|
|
10725
11658
|
let rows = '';
|
|
10726
11659
|
data.currentModels.forEach(m => {
|
|
11660
|
+
const defaultTag = m.isDefault ? ' <span class="badge badge-warn" style="font-size:10px">\u2B50 ' + t('default_model_badge') + '</span>' : '';
|
|
10727
11661
|
rows += \\\`<tr>
|
|
10728
11662
|
<td style="font-weight:500">\${escHtml(m.provider)}</td>
|
|
10729
|
-
<td class="mono">\${escHtml(m.modelId)}</td>
|
|
10730
|
-
<td
|
|
10731
|
-
<td class="
|
|
11663
|
+
<td class="mono">\${escHtml(m.modelId)}\${defaultTag}</td>
|
|
11664
|
+
<td>\${escHtml(m.modelName || '')}</td>
|
|
11665
|
+
<td><span class="badge badge-ok">' + t('key_set') + '</span></td>
|
|
11666
|
+
<td class="mono" style="font-size:11px">\${escHtml(m.baseUrl || t('default_model'))}</td>
|
|
10732
11667
|
<td>
|
|
10733
11668
|
<div class="actions-cell">
|
|
10734
|
-
<button class="btn btn-sm btn-ghost" onclick="showModelConfigModal('\${escHtml(m.providerId)}')"
|
|
11669
|
+
<button class="btn btn-sm btn-ghost" onclick="showModelConfigModal('\${escHtml(m.providerId)}')">\${t('configure')}</button>
|
|
10735
11670
|
<button class="btn btn-sm btn-danger" onclick="deleteModelProvider('\${escHtml(m.providerId)}')" title="Delete">\u{1F5D1}\uFE0F</button>
|
|
10736
11671
|
</div>
|
|
10737
11672
|
</td>
|
|
@@ -10739,61 +11674,174 @@ function renderModels(data) {
|
|
|
10739
11674
|
});
|
|
10740
11675
|
activeModelsSection = \\\`
|
|
10741
11676
|
<div class="card" style="margin-top:20px">
|
|
10742
|
-
<div class="card-header"><div class="card-title">\u{1F4CA}
|
|
11677
|
+
<div class="card-header"><div class="card-title">\u{1F4CA} \${t('active_model_configs')}</div></div>
|
|
10743
11678
|
<div style="overflow-x:auto"><table>
|
|
10744
|
-
<thead><tr><th
|
|
11679
|
+
<thead><tr><th>\${t('provider')}</th><th>\${t('model')}</th><th>\${t('model_name_label')}</th><th>API Key</th><th>\${t('base_url')}</th><th>\${t('actions')}</th></tr></thead>
|
|
10745
11680
|
<tbody>\${rows}</tbody>
|
|
10746
11681
|
</table></div>
|
|
10747
11682
|
</div>\\\`;
|
|
10748
11683
|
}
|
|
10749
11684
|
|
|
10750
11685
|
html(el, \\\`
|
|
11686
|
+
\${defaultBanner}
|
|
10751
11687
|
<div class="stats-grid" style="margin-bottom:24px">
|
|
10752
11688
|
<div class="stat-card">
|
|
10753
11689
|
<div class="stat-icon" style="background:var(--accent-bg)">\u{1F9E0}</div>
|
|
10754
|
-
<div class="stat-label"
|
|
11690
|
+
<div class="stat-label">\${t('configured')}</div>
|
|
10755
11691
|
<div class="stat-value">\${configured} / \${providers.length}</div>
|
|
10756
|
-
<div class="stat-sub"
|
|
11692
|
+
<div class="stat-sub">\${t('providers_with_keys')}</div>
|
|
10757
11693
|
</div>
|
|
11694
|
+
\${defaultModel ? '<div class="stat-card"><div class="stat-icon" style="background:var(--warn-bg)">\u2B50</div><div class="stat-label">' + t('default_model_label') + '</div><div class="stat-value mono" style="font-size:13px">' + escHtml(defaultModel) + '</div></div>' : ''}
|
|
11695
|
+
</div>
|
|
11696
|
+
<p class="section-desc">\${t('configure_providers_desc')}</p>
|
|
11697
|
+
<div style="margin-bottom:16px;text-align:right">
|
|
11698
|
+
<button class="btn btn-primary" onclick="showAddCustomProviderModal()" style="font-size:13px">
|
|
11699
|
+
\${t('add_custom_provider')}
|
|
11700
|
+
</button>
|
|
10758
11701
|
</div>
|
|
10759
|
-
<p class="section-desc">Configure LLM providers for your agents. Click a provider card to set or update the API key, model, and base URL. Changes are saved directly to your config file.</p>
|
|
10760
11702
|
<div class="provider-grid">\${cards}</div>
|
|
10761
11703
|
\${activeModelsSection}
|
|
10762
11704
|
\\\`);
|
|
10763
11705
|
}
|
|
10764
11706
|
|
|
10765
11707
|
let _editProviderId = null;
|
|
11708
|
+
|
|
11709
|
+
// --- Custom Provider Functions ---
|
|
11710
|
+
var _editCustomId = null;
|
|
11711
|
+
|
|
11712
|
+
function showAddCustomProviderModal() {
|
|
11713
|
+
_editCustomId = null;
|
|
11714
|
+
document.getElementById('custom-provider-title').textContent = t('custom_provider');
|
|
11715
|
+
document.getElementById('custom-name').value = '';
|
|
11716
|
+
document.getElementById('custom-api-key').value = '';
|
|
11717
|
+
document.getElementById('custom-model-id').value = '';
|
|
11718
|
+
document.getElementById('custom-base-url').value = '';
|
|
11719
|
+
document.getElementById('custom-description').value = '';
|
|
11720
|
+
showModal('modal-custom-provider');
|
|
11721
|
+
}
|
|
11722
|
+
|
|
11723
|
+
function showEditCustomProviderModal(id) {
|
|
11724
|
+
if (!_modelProviders) return;
|
|
11725
|
+
const p = _modelProviders.providers.find(pr => pr.id === id);
|
|
11726
|
+
if (!p || !p.isCustom) return;
|
|
11727
|
+
_editCustomId = id;
|
|
11728
|
+
document.getElementById('custom-provider-title').textContent = t('edit_custom_provider');
|
|
11729
|
+
document.getElementById('custom-name').value = p.name || '';
|
|
11730
|
+
document.getElementById('custom-api-key').value = '';
|
|
11731
|
+
document.getElementById('custom-model-id').value = p.modelId || p.modelHint || '';
|
|
11732
|
+
document.getElementById('custom-base-url').value = p.baseUrl || '';
|
|
11733
|
+
document.getElementById('custom-description').value = p.description || '';
|
|
11734
|
+
showModal('modal-custom-provider');
|
|
11735
|
+
}
|
|
11736
|
+
|
|
11737
|
+
async function saveCustomProvider() {
|
|
11738
|
+
var name = document.getElementById('custom-name').value.trim();
|
|
11739
|
+
var apiKey = document.getElementById('custom-api-key').value.trim();
|
|
11740
|
+
var modelId = document.getElementById('custom-model-id').value.trim();
|
|
11741
|
+
var baseUrl = document.getElementById('custom-base-url').value.trim();
|
|
11742
|
+
var description = document.getElementById('custom-description').value.trim();
|
|
11743
|
+
|
|
11744
|
+
if (!name) { toast(t('provider_name_required'), 'err'); return; }
|
|
11745
|
+
|
|
11746
|
+
var url, method;
|
|
11747
|
+
if (_editCustomId) {
|
|
11748
|
+
url = '/api/models/custom/' + encodeURIComponent(_editCustomId);
|
|
11749
|
+
method = 'PUT';
|
|
11750
|
+
} else {
|
|
11751
|
+
url = '/api/models/custom';
|
|
11752
|
+
method = 'POST';
|
|
11753
|
+
}
|
|
11754
|
+
|
|
11755
|
+
var body = { name: name };
|
|
11756
|
+
if (apiKey) body.apiKey = apiKey;
|
|
11757
|
+
if (modelId) body.modelId = modelId;
|
|
11758
|
+
if (baseUrl) body.baseUrl = baseUrl;
|
|
11759
|
+
if (description) body.description = description;
|
|
11760
|
+
|
|
11761
|
+
try {
|
|
11762
|
+
var resp = await fetch(url, { method: method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
|
11763
|
+
var data = await resp.json();
|
|
11764
|
+
if (data.success) {
|
|
11765
|
+
hideModal('modal-custom-provider');
|
|
11766
|
+
toast(_editCustomId ? t('custom_provider_updated') : t('custom_provider_added'), 'ok');
|
|
11767
|
+
loadModels();
|
|
11768
|
+
} else {
|
|
11769
|
+
toast(data.message || 'Failed', 'err');
|
|
11770
|
+
}
|
|
11771
|
+
} catch (e) {
|
|
11772
|
+
toast('Request failed: ' + e.message, 'err');
|
|
11773
|
+
}
|
|
11774
|
+
}
|
|
11775
|
+
|
|
11776
|
+
function deleteCustomProvider(id, name) {
|
|
11777
|
+
if (!confirm(t('confirm_delete_custom').replace('{name}', name || id))) return;
|
|
11778
|
+
fetch('/api/models/custom/' + encodeURIComponent(id), { method: 'DELETE' })
|
|
11779
|
+
.then(function(r) { return r.json(); })
|
|
11780
|
+
.then(function(data) {
|
|
11781
|
+
if (data.success) {
|
|
11782
|
+
toast(t('custom_provider_deleted'), 'ok');
|
|
11783
|
+
loadModels();
|
|
11784
|
+
} else {
|
|
11785
|
+
toast(data.message || 'Failed to delete', 'err');
|
|
11786
|
+
}
|
|
11787
|
+
})
|
|
11788
|
+
.catch(function(e) { toast('Delete failed: ' + e.message, 'err'); });
|
|
11789
|
+
}
|
|
11790
|
+
|
|
10766
11791
|
function showModelConfigModal(id) {
|
|
10767
11792
|
const p = (_modelProviders?.providers || []).find(x => x.id === id);
|
|
10768
|
-
if (!p) { toast('
|
|
11793
|
+
if (!p) { toast(t('provider_not_found'), 'err'); return; }
|
|
10769
11794
|
_editProviderId = id;
|
|
10770
11795
|
$('#model-name').textContent = p.name;
|
|
10771
|
-
$('#model-icon').textContent = p.id
|
|
11796
|
+
$('#model-icon').textContent = getProviderIcon(p.id);
|
|
10772
11797
|
$('#model-api-key').value = '';
|
|
10773
|
-
$('#model-api-key').placeholder = p.apiKeyHint || '
|
|
11798
|
+
$('#model-api-key').placeholder = p.apiKeyHint || t('enter_api_key');
|
|
10774
11799
|
$('#model-model-id').value = p.modelId || '';
|
|
10775
|
-
$('#model-model-id').placeholder = p.modelHint || '
|
|
11800
|
+
$('#model-model-id').placeholder = p.modelHint || t('enter_model_id');
|
|
10776
11801
|
$('#model-base-url').value = p.baseUrl || '';
|
|
10777
|
-
$('#model-base-url').placeholder = p.baseUrlHint || '
|
|
10778
|
-
$('#model-current-key').textContent = p.apiKeyHasValue ? '
|
|
11802
|
+
$('#model-base-url').placeholder = p.baseUrlHint || t('default_url');
|
|
11803
|
+
$('#model-current-key').textContent = p.apiKeyHasValue ? t('current_key') + p.apiKey : t('no_api_key');
|
|
11804
|
+
// Show multi-model list if available
|
|
11805
|
+
const modelsListEl = document.getElementById('model-multi-list');
|
|
11806
|
+
if (modelsListEl) {
|
|
11807
|
+
if (p.models && p.models.length > 0) {
|
|
11808
|
+
const defaultModel = _modelProviders?.defaultModel || '';
|
|
11809
|
+
let html = '<div style="font-size:12px;color:var(--text2);margin-bottom:8px;font-weight:600">' + t('model_count') + ': ' + p.models.length + '</div>';
|
|
11810
|
+
html += '<div style="max-height:160px;overflow-y:auto">';
|
|
11811
|
+
p.models.forEach(m => {
|
|
11812
|
+
const isDef = defaultModel === (m.id || '');
|
|
11813
|
+
html += '<div style="display:flex;align-items:center;gap:8px;padding:4px 0;font-size:12px;border-bottom:1px solid var(--border)">' +
|
|
11814
|
+
'<span class="mono" style="flex:1">' + escHtml(m.name || m.id || '') + '</span>' +
|
|
11815
|
+
'<span class="mono" style="color:var(--text3);font-size:11px">' + escHtml(m.id || '') + '</span>' +
|
|
11816
|
+
(isDef ? '<span class="badge badge-warn" style="font-size:10px">\u2B50</span>' : '') +
|
|
11817
|
+
'</div>';
|
|
11818
|
+
});
|
|
11819
|
+
html += '</div>';
|
|
11820
|
+
modelsListEl.innerHTML = html;
|
|
11821
|
+
modelsListEl.style.display = '';
|
|
11822
|
+
} else {
|
|
11823
|
+
modelsListEl.innerHTML = '';
|
|
11824
|
+
modelsListEl.style.display = 'none';
|
|
11825
|
+
}
|
|
11826
|
+
}
|
|
10779
11827
|
showModal('modal-model-config');
|
|
10780
11828
|
}
|
|
10781
11829
|
async function saveModelConfig() {
|
|
10782
11830
|
const apiKey = $('#model-api-key').value.trim();
|
|
10783
11831
|
const modelId = $('#model-model-id').value.trim();
|
|
10784
11832
|
const baseUrl = $('#model-base-url').value.trim();
|
|
10785
|
-
if (!apiKey && !modelId) { toast('
|
|
11833
|
+
if (!apiKey && !modelId) { toast(t('enter_api_key_or_model'), 'warn'); return; }
|
|
10786
11834
|
hideModal('modal-model-config');
|
|
10787
|
-
toast('
|
|
11835
|
+
toast(t('saving_config'), 'info');
|
|
10788
11836
|
const r = await api('/models/' + encodeURIComponent(_editProviderId), { method: 'PUT', body: JSON.stringify({ apiKey, modelId, baseUrl }) });
|
|
10789
|
-
if (r.success) { toast(r.message || '
|
|
10790
|
-
else { toast(r.message || r.error || '
|
|
11837
|
+
if (r.success) { toast(r.message || t('config_saved'), 'ok'); loadModels(); }
|
|
11838
|
+
else { toast(r.message || r.error || t('failed_save'), 'err'); }
|
|
10791
11839
|
}
|
|
10792
11840
|
async function deleteModelProvider(providerId) {
|
|
10793
|
-
if (!confirm('
|
|
11841
|
+
if (!confirm(t('confirm_delete_provider') + providerId + '"?')) return;
|
|
10794
11842
|
const r = await api('/models/' + encodeURIComponent(providerId), { method: 'DELETE' });
|
|
10795
|
-
if (r.success) { toast('
|
|
10796
|
-
else { toast(r.message || r.error || '
|
|
11843
|
+
if (r.success) { toast(t('provider_deleted'), 'ok'); loadModels(); }
|
|
11844
|
+
else { toast(r.message || r.error || t('delete_failed'), 'err'); }
|
|
10797
11845
|
}
|
|
10798
11846
|
|
|
10799
11847
|
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
@@ -10824,7 +11872,7 @@ function formatUptime(seconds) {
|
|
|
10824
11872
|
|
|
10825
11873
|
async function loadSettings() {
|
|
10826
11874
|
const el = $('#settings-content');
|
|
10827
|
-
html(el, '<div class="loading-mask"><div class="spinner"></div>
|
|
11875
|
+
html(el, '<div class="loading-mask"><div class="spinner"></div>' + t('loading_settings') + '</div>');
|
|
10828
11876
|
|
|
10829
11877
|
const settings = await api('/settings');
|
|
10830
11878
|
if (settings.error) {
|
|
@@ -10836,11 +11884,11 @@ async function loadSettings() {
|
|
|
10836
11884
|
|
|
10837
11885
|
// Render settings tabs nav
|
|
10838
11886
|
html('#settings-tabs', \\\`
|
|
10839
|
-
<button class="filter-btn \${_settingsTab==='connection'?'active':''}" onclick="_settingsTab='connection';renderSettingsTab()"
|
|
10840
|
-
<button class="filter-btn \${_settingsTab==='friends'?'active':''}" onclick="_settingsTab='friends';renderSettingsTab()"
|
|
10841
|
-
<button class="filter-btn \${_settingsTab==='security'?'active':''}" onclick="_settingsTab='security';renderSettingsTab()"
|
|
10842
|
-
<button class="filter-btn \${_settingsTab==='advanced'?'active':''}" onclick="_settingsTab='advanced';renderSettingsTab()"
|
|
10843
|
-
<button class="filter-btn \${_settingsTab==='json'?'active':''}" onclick="_settingsTab='json';renderSettingsTab()"
|
|
11887
|
+
<button class="filter-btn \${_settingsTab==='connection'?'active':''}" onclick="_settingsTab='connection';renderSettingsTab()">\${t('tab_connection')}</button>
|
|
11888
|
+
<button class="filter-btn \${_settingsTab==='friends'?'active':''}" onclick="_settingsTab='friends';renderSettingsTab()">\${t('tab_friends')}</button>
|
|
11889
|
+
<button class="filter-btn \${_settingsTab==='security'?'active':''}" onclick="_settingsTab='security';renderSettingsTab()">\${t('tab_security')}</button>
|
|
11890
|
+
<button class="filter-btn \${_settingsTab==='advanced'?'active':''}" onclick="_settingsTab='advanced';renderSettingsTab()">\${t('tab_advanced')}</button>
|
|
11891
|
+
<button class="filter-btn \${_settingsTab==='json'?'active':''}" onclick="_settingsTab='json';renderSettingsTab()">\${t('tab_json')}</button>
|
|
10844
11892
|
\\\`);
|
|
10845
11893
|
|
|
10846
11894
|
renderSettingsTab();
|
|
@@ -10849,11 +11897,11 @@ async function loadSettings() {
|
|
|
10849
11897
|
function renderSettingsTab() {
|
|
10850
11898
|
// Update tab buttons
|
|
10851
11899
|
html('#settings-tabs', \\\`
|
|
10852
|
-
<button class="filter-btn \${_settingsTab==='connection'?'active':''}" onclick="_settingsTab='connection';renderSettingsTab()"
|
|
10853
|
-
<button class="filter-btn \${_settingsTab==='friends'?'active':''}" onclick="_settingsTab='friends';renderSettingsTab()"
|
|
10854
|
-
<button class="filter-btn \${_settingsTab==='security'?'active':''}" onclick="_settingsTab='security';renderSettingsTab()"
|
|
10855
|
-
<button class="filter-btn \${_settingsTab==='advanced'?'active':''}" onclick="_settingsTab='advanced';renderSettingsTab()"
|
|
10856
|
-
<button class="filter-btn \${_settingsTab==='json'?'active':''}" onclick="_settingsTab='json';renderSettingsTab()"
|
|
11900
|
+
<button class="filter-btn \${_settingsTab==='connection'?'active':''}" onclick="_settingsTab='connection';renderSettingsTab()">\${t('tab_connection')}</button>
|
|
11901
|
+
<button class="filter-btn \${_settingsTab==='friends'?'active':''}" onclick="_settingsTab='friends';renderSettingsTab()">\${t('tab_friends')}</button>
|
|
11902
|
+
<button class="filter-btn \${_settingsTab==='security'?'active':''}" onclick="_settingsTab='security';renderSettingsTab()">\${t('tab_security')}</button>
|
|
11903
|
+
<button class="filter-btn \${_settingsTab==='advanced'?'active':''}" onclick="_settingsTab='advanced';renderSettingsTab()">\${t('tab_advanced')}</button>
|
|
11904
|
+
<button class="filter-btn \${_settingsTab==='json'?'active':''}" onclick="_settingsTab='json';renderSettingsTab()">\${t('tab_json')}</button>
|
|
10857
11905
|
\\\`);
|
|
10858
11906
|
|
|
10859
11907
|
switch (_settingsTab) {
|
|
@@ -10866,7 +11914,7 @@ function renderSettingsTab() {
|
|
|
10866
11914
|
}
|
|
10867
11915
|
|
|
10868
11916
|
function sectionSaveBtn(section, id) {
|
|
10869
|
-
return \\\`<button class="btn btn-primary btn-sm" id="btn-save-\${id}" onclick="saveSettingsSection('\${section}', '\${id}')"
|
|
11917
|
+
return \\\`<button class="btn btn-primary btn-sm" id="btn-save-\${id}" onclick="saveSettingsSection('\${section}', '\${id}')">\${t('save')}</button>
|
|
10870
11918
|
<span id="status-\${id}" style="font-size:12px;color:var(--text3);margin-left:8px"></span>\\\`;
|
|
10871
11919
|
}
|
|
10872
11920
|
|
|
@@ -10876,51 +11924,51 @@ function renderSettingsConnection() {
|
|
|
10876
11924
|
const el = $('#settings-content');
|
|
10877
11925
|
|
|
10878
11926
|
html(el, \\\`
|
|
10879
|
-
<p class="section-desc"
|
|
11927
|
+
<p class="section-desc">\${t('conn_desc')}</p>
|
|
10880
11928
|
|
|
10881
11929
|
<div class="card">
|
|
10882
11930
|
<div class="card-header">
|
|
10883
|
-
<div class="card-title"
|
|
10884
|
-
<span class="badge badge-\${s.connected ? 'ok' : 'danger'}">\${s.connected ? '\u25CF
|
|
11931
|
+
<div class="card-title">\${t('server_connection')}</div>
|
|
11932
|
+
<span class="badge badge-\${s.connected ? 'ok' : 'danger'}">\${s.connected ? '\u25CF ' + t('connected') : '\u25CB ' + t('disconnected')}</span>
|
|
10885
11933
|
</div>
|
|
10886
11934
|
<div class="form-group">
|
|
10887
|
-
<label
|
|
11935
|
+
<label>\${t('server_url_label')}</label>
|
|
10888
11936
|
<div style="display:flex;gap:8px;align-items:start">
|
|
10889
11937
|
<div style="flex:1">
|
|
10890
11938
|
<div class="input-prefix">
|
|
10891
11939
|
<span class="prefix">\u{1F310}</span>
|
|
10892
11940
|
<input type="url" id="set-server-url" value="\${escHtml(s.serverUrl || '')}" placeholder="https://aicq.online">
|
|
10893
11941
|
</div>
|
|
10894
|
-
<div class="hint"
|
|
11942
|
+
<div class="hint">\${t('server_url_hint')}</div>
|
|
10895
11943
|
</div>
|
|
10896
|
-
<button class="btn btn-ok btn-sm" id="btn-test-conn" onclick="testConnection()" style="white-space:nowrap;margin-top:1px">\u{1F50D}
|
|
11944
|
+
<button class="btn btn-ok btn-sm" id="btn-test-conn" onclick="testConnection()" style="white-space:nowrap;margin-top:1px">\u{1F50D} \${t('test')}</button>
|
|
10897
11945
|
</div>
|
|
10898
11946
|
<div id="conn-test-result" style="margin-top:8px"></div>
|
|
10899
11947
|
</div>
|
|
10900
11948
|
|
|
10901
11949
|
<div class="form-row">
|
|
10902
11950
|
<div class="form-group">
|
|
10903
|
-
<label
|
|
11951
|
+
<label>\${t('conn_timeout')}</label>
|
|
10904
11952
|
<input type="number" id="set-connection-timeout" value="\${s.connectionTimeout || 30}" min="5" max="120" placeholder="30">
|
|
10905
|
-
<div class="hint"
|
|
11953
|
+
<div class="hint">\${t('conn_timeout_hint')}</div>
|
|
10906
11954
|
</div>
|
|
10907
11955
|
<div class="form-group">
|
|
10908
|
-
<label
|
|
11956
|
+
<label>\${t('ws_auto_reconnect')}</label>
|
|
10909
11957
|
<div style="display:flex;align-items:center;gap:10px;margin-top:6px">
|
|
10910
11958
|
<label class="toggle-label">
|
|
10911
11959
|
<input type="checkbox" id="set-ws-auto-reconnect" \${s.wsAutoReconnect ? 'checked' : ''}>
|
|
10912
11960
|
<span class="toggle-slider"></span>
|
|
10913
|
-
<span
|
|
11961
|
+
<span>\${t('auto_reconnect_label')}</span>
|
|
10914
11962
|
</label>
|
|
10915
11963
|
</div>
|
|
10916
|
-
<div class="hint"
|
|
11964
|
+
<div class="hint">\${t('auto_reconnect_hint')}</div>
|
|
10917
11965
|
</div>
|
|
10918
11966
|
</div>
|
|
10919
11967
|
|
|
10920
11968
|
<div class="form-group">
|
|
10921
|
-
<label
|
|
11969
|
+
<label>\${t('ws_reconnect_interval')}</label>
|
|
10922
11970
|
<input type="number" id="set-ws-reconnect-interval" value="\${s.wsReconnectInterval || 60}" min="5" max="600" placeholder="60">
|
|
10923
|
-
<div class="hint"
|
|
11971
|
+
<div class="hint">\${t('ws_reconnect_hint')}</div>
|
|
10924
11972
|
</div>
|
|
10925
11973
|
|
|
10926
11974
|
<div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
|
|
@@ -10929,11 +11977,11 @@ function renderSettingsConnection() {
|
|
|
10929
11977
|
</div>
|
|
10930
11978
|
|
|
10931
11979
|
<div class="card">
|
|
10932
|
-
<div class="card-header"><div class="card-title"
|
|
10933
|
-
<div class="detail-row"><div class="detail-key"
|
|
10934
|
-
<div class="detail-row"><div class="detail-key"
|
|
10935
|
-
<div class="detail-row"><div class="detail-key"
|
|
10936
|
-
<div class="detail-row"><div class="detail-key"
|
|
11980
|
+
<div class="card-header"><div class="card-title">\${t('config_file')}</div></div>
|
|
11981
|
+
<div class="detail-row"><div class="detail-key">\${t('source')}</div><div class="detail-val mono" style="cursor:pointer" onclick="copyText('\${escHtml(s.configPath || '')}')">\${escHtml(s.configPath || t('not_found'))} \u{1F4CB}</div></div>
|
|
11982
|
+
<div class="detail-row"><div class="detail-key">\${t('plugin_version')}</div><div class="detail-val">1.1.1</div></div>
|
|
11983
|
+
<div class="detail-row"><div class="detail-key">\${t('mgmt_ui')}</div><div class="detail-val" id="mgmt-url-display" style="cursor:pointer" onclick="copyText(document.getElementById('mgmt-url-display')?.textContent || '')"></div></div>
|
|
11984
|
+
<div class="detail-row"><div class="detail-key">\${t('uptime')}</div><div class="detail-val">\${formatUptime(s.uptimeSeconds)}</div></div>
|
|
10937
11985
|
</div>
|
|
10938
11986
|
\\\`);
|
|
10939
11987
|
}
|
|
@@ -10943,37 +11991,37 @@ async function testConnection() {
|
|
|
10943
11991
|
const resultEl = $('#conn-test-result');
|
|
10944
11992
|
const url = $('#set-server-url')?.value?.trim() || _settingsData.serverUrl;
|
|
10945
11993
|
|
|
10946
|
-
if (!url) { toast('
|
|
11994
|
+
if (!url) { toast(t('enter_server_url'), 'warn'); return; }
|
|
10947
11995
|
|
|
10948
|
-
if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner" style="width:14px;height:14px;border-width:2px;margin:0"></span>
|
|
10949
|
-
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>
|
|
11996
|
+
if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner" style="width:14px;height:14px;border-width:2px;margin:0"></span> ' + t('testing'); }
|
|
11997
|
+
if (resultEl) html(resultEl, '<div style="font-size:12px;color:var(--text3);display:flex;align-items:center;gap:6px"><span class="spinner" style="width:12px;height:12px;border-width:2px"></span> ' + t('testing_conn_to') + escHtml(url) + '...</div>');
|
|
10950
11998
|
|
|
10951
11999
|
const r = await api('/settings/test-connection', {
|
|
10952
12000
|
method: 'POST',
|
|
10953
12001
|
body: JSON.stringify({ serverUrl: url, timeout: 10000 }),
|
|
10954
12002
|
});
|
|
10955
12003
|
|
|
10956
|
-
if (btn) { btn.disabled = false; btn.innerHTML = '\u{1F50D}
|
|
12004
|
+
if (btn) { btn.disabled = false; btn.innerHTML = '\u{1F50D} ' + t('test'); }
|
|
10957
12005
|
|
|
10958
12006
|
if (r.success) {
|
|
10959
12007
|
const latencyBadge = r.latency < 200 ? '<span class="badge badge-ok">' + r.latency + 'ms</span>' : r.latency < 1000 ? '<span class="badge badge-warn">' + r.latency + 'ms</span>' : '<span class="badge badge-danger">' + r.latency + 'ms</span>';
|
|
10960
12008
|
if (resultEl) html(resultEl, \\\`
|
|
10961
12009
|
<div style="display:flex;align-items:center;gap:10px;font-size:12px;color:var(--ok)">
|
|
10962
|
-
<span class="dot dot-ok"></span>
|
|
12010
|
+
<span class="dot dot-ok"></span> \${t('conn_ok')} \${latencyBadge}
|
|
10963
12011
|
\${r.serverInfo?.version ? '<span class="tag">v' + escHtml(r.serverInfo.version) + '</span>' : ''}
|
|
10964
12012
|
</div>
|
|
10965
12013
|
\\\`);
|
|
10966
|
-
toast('
|
|
12014
|
+
toast(t('conn_ok_latency') + r.latency + 'ms', 'ok');
|
|
10967
12015
|
} else {
|
|
10968
12016
|
const cls = r.status === 'timeout' ? 'warn' : 'danger';
|
|
10969
12017
|
const icon = r.status === 'timeout' ? '\u23F1\uFE0F' : '\u274C';
|
|
10970
12018
|
if (resultEl) html(resultEl, \\\`
|
|
10971
12019
|
<div style="font-size:12px;color:var(--\${cls});display:flex;align-items:center;gap:8px">
|
|
10972
|
-
\${icon} \${escHtml(r.message || '
|
|
12020
|
+
\${icon} \${escHtml(r.message || t('conn_failed'))}
|
|
10973
12021
|
<span class="badge badge-ghost">\${r.latency}ms</span>
|
|
10974
12022
|
</div>
|
|
10975
12023
|
\\\`);
|
|
10976
|
-
toast(r.message || '
|
|
12024
|
+
toast(r.message || t('conn_failed'), 'err');
|
|
10977
12025
|
}
|
|
10978
12026
|
}
|
|
10979
12027
|
|
|
@@ -10983,58 +12031,58 @@ function renderSettingsFriends() {
|
|
|
10983
12031
|
const el = $('#settings-content');
|
|
10984
12032
|
|
|
10985
12033
|
html(el, \\\`
|
|
10986
|
-
<p class="section-desc"
|
|
12034
|
+
<p class="section-desc">\${t('friends_tab_desc')}</p>
|
|
10987
12035
|
|
|
10988
12036
|
<div class="stats-grid" style="margin-bottom:20px">
|
|
10989
12037
|
<div class="stat-card">
|
|
10990
12038
|
<div class="stat-icon" style="background:var(--ok-bg)">\u{1F465}</div>
|
|
10991
|
-
<div class="stat-label"
|
|
12039
|
+
<div class="stat-label">\${t('friends')}</div>
|
|
10992
12040
|
<div class="stat-value">\${s.friendCount || 0}</div>
|
|
10993
|
-
<div class="stat-sub"
|
|
12041
|
+
<div class="stat-sub">\${t('of_max')}\${s.maxFriends || 200}</div>
|
|
10994
12042
|
</div>
|
|
10995
12043
|
<div class="stat-card">
|
|
10996
12044
|
<div class="stat-icon" style="background:var(--info-bg)">\u{1F517}</div>
|
|
10997
|
-
<div class="stat-label"
|
|
12045
|
+
<div class="stat-label">\${t('sessions')}</div>
|
|
10998
12046
|
<div class="stat-value">\${s.sessionCount || 0}</div>
|
|
10999
|
-
<div class="stat-sub"
|
|
12047
|
+
<div class="stat-sub">\${t('encrypted_sessions')}</div>
|
|
11000
12048
|
</div>
|
|
11001
12049
|
</div>
|
|
11002
12050
|
|
|
11003
12051
|
<div class="card">
|
|
11004
|
-
<div class="card-header"><div class="card-title">\u{1F465}
|
|
12052
|
+
<div class="card-header"><div class="card-title">\u{1F465} \${t('max_friends')} & \${t('permissions')}</div></div>
|
|
11005
12053
|
<div class="form-row">
|
|
11006
12054
|
<div class="form-group">
|
|
11007
|
-
<label
|
|
12055
|
+
<label>\${t('max_friends')}</label>
|
|
11008
12056
|
<input type="number" id="set-max-friends" value="\${s.maxFriends || 200}" min="1" max="10000" placeholder="200">
|
|
11009
|
-
<div class="hint"
|
|
12057
|
+
<div class="hint">\${t('max_friends')} (1\u201310,000)</div>
|
|
11010
12058
|
</div>
|
|
11011
12059
|
<div class="form-group">
|
|
11012
|
-
<label
|
|
12060
|
+
<label>\${t('auto_accept')}</label>
|
|
11013
12061
|
<div style="display:flex;align-items:center;gap:10px;margin-top:6px">
|
|
11014
12062
|
<label class="toggle-label">
|
|
11015
12063
|
<input type="checkbox" id="set-auto-accept" \${s.autoAcceptFriends ? 'checked' : ''}>
|
|
11016
12064
|
<span class="toggle-slider"></span>
|
|
11017
|
-
<span
|
|
12065
|
+
<span>\${t('auto_accept_label')}</span>
|
|
11018
12066
|
</label>
|
|
11019
12067
|
</div>
|
|
11020
|
-
<div class="hint"
|
|
12068
|
+
<div class="hint">\${t('auto_accept_hint')}</div>
|
|
11021
12069
|
</div>
|
|
11022
12070
|
</div>
|
|
11023
12071
|
<div class="form-group">
|
|
11024
|
-
<label
|
|
12072
|
+
<label>\${t('default_perms')}</label>
|
|
11025
12073
|
<div style="display:flex;gap:16px;margin-top:6px;flex-wrap:wrap">
|
|
11026
12074
|
<label class="toggle-label">
|
|
11027
12075
|
<input type="checkbox" id="set-perm-chat" \${(s.defaultPermissions || []).includes('chat') ? 'checked' : ''}>
|
|
11028
12076
|
<span class="toggle-slider"></span>
|
|
11029
|
-
<span
|
|
12077
|
+
<span>\${t('chat_perm')}</span>
|
|
11030
12078
|
</label>
|
|
11031
12079
|
<label class="toggle-label">
|
|
11032
12080
|
<input type="checkbox" id="set-perm-exec" \${(s.defaultPermissions || []).includes('exec') ? 'checked' : ''}>
|
|
11033
12081
|
<span class="toggle-slider"></span>
|
|
11034
|
-
<span
|
|
12082
|
+
<span>\${t('exec_perm')}</span>
|
|
11035
12083
|
</label>
|
|
11036
12084
|
</div>
|
|
11037
|
-
<div class="hint"
|
|
12085
|
+
<div class="hint">\${t('default_perms_hint')}</div>
|
|
11038
12086
|
</div>
|
|
11039
12087
|
<div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
|
|
11040
12088
|
\${sectionSaveBtn('friends', 'friends')}
|
|
@@ -11042,11 +12090,11 @@ function renderSettingsFriends() {
|
|
|
11042
12090
|
</div>
|
|
11043
12091
|
|
|
11044
12092
|
<div class="card">
|
|
11045
|
-
<div class="card-header"><div class="card-title"
|
|
12093
|
+
<div class="card-header"><div class="card-title">\${t('temp_numbers')}</div></div>
|
|
11046
12094
|
<div class="form-group">
|
|
11047
|
-
<label
|
|
12095
|
+
<label>\${t('temp_expiry')}</label>
|
|
11048
12096
|
<input type="number" id="set-temp-expiry" value="\${s.tempNumberExpiry || 300}" min="60" max="3600" placeholder="300">
|
|
11049
|
-
<div class="hint"
|
|
12097
|
+
<div class="hint">\${t('temp_expiry_hint')}</div>
|
|
11050
12098
|
</div>
|
|
11051
12099
|
<div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
|
|
11052
12100
|
\${sectionSaveBtn('temp', 'temp')}
|
|
@@ -11061,36 +12109,36 @@ function renderSettingsSecurity() {
|
|
|
11061
12109
|
const el = $('#settings-content');
|
|
11062
12110
|
|
|
11063
12111
|
html(el, \\\`
|
|
11064
|
-
<p class="section-desc"
|
|
12112
|
+
<p class="section-desc">\${t('sec_desc')}</p>
|
|
11065
12113
|
|
|
11066
12114
|
<div class="card">
|
|
11067
|
-
<div class="card-header"><div class="card-title"
|
|
11068
|
-
<div class="detail-row"><div class="detail-key"
|
|
11069
|
-
<div class="detail-row"><div class="detail-key"
|
|
12115
|
+
<div class="card-header"><div class="card-title">\${t('agent_identity')}</div></div>
|
|
12116
|
+
<div class="detail-row"><div class="detail-key">\${t('agent_id')}</div><div class="detail-val mono" style="cursor:pointer" onclick="copyText('\${escHtml(s.agentId)}')">\${escHtml(s.agentId)} \u{1F4CB}</div></div>
|
|
12117
|
+
<div class="detail-row"><div class="detail-key">\${t('public_key_fp')}</div><div class="detail-val mono">\${escHtml(s.publicKeyFingerprint || '\u2014')}</div></div>
|
|
11070
12118
|
<div style="padding-top:12px;display:flex;gap:8px">
|
|
11071
|
-
<button class="btn btn-danger btn-sm" onclick="showResetIdentityModal()"
|
|
11072
|
-
<span style="font-size:12px;color:var(--text3);display:flex;align-items:center"
|
|
12119
|
+
<button class="btn btn-danger btn-sm" onclick="showResetIdentityModal()">\${t('reset_identity')}</button>
|
|
12120
|
+
<span style="font-size:12px;color:var(--text3);display:flex;align-items:center">\${t('reset_identity_warn')}</span>
|
|
11073
12121
|
</div>
|
|
11074
12122
|
</div>
|
|
11075
12123
|
|
|
11076
12124
|
<div class="card">
|
|
11077
|
-
<div class="card-header"><div class="card-title"
|
|
12125
|
+
<div class="card-header"><div class="card-title">\${t('p2p_encryption')}</div></div>
|
|
11078
12126
|
<div class="form-row">
|
|
11079
12127
|
<div class="form-group">
|
|
11080
|
-
<label
|
|
12128
|
+
<label>\${t('enable_p2p')}</label>
|
|
11081
12129
|
<div style="display:flex;align-items:center;gap:10px;margin-top:6px">
|
|
11082
12130
|
<label class="toggle-label">
|
|
11083
12131
|
<input type="checkbox" id="set-enable-p2p" \${s.enableP2P ? 'checked' : ''}>
|
|
11084
12132
|
<span class="toggle-slider"></span>
|
|
11085
|
-
<span
|
|
12133
|
+
<span>\${t('allow_p2p')}</span>
|
|
11086
12134
|
</label>
|
|
11087
12135
|
</div>
|
|
11088
|
-
<div class="hint"
|
|
12136
|
+
<div class="hint">\${t('enable_p2p_hint')}</div>
|
|
11089
12137
|
</div>
|
|
11090
12138
|
<div class="form-group">
|
|
11091
|
-
<label
|
|
12139
|
+
<label>\${t('hs_timeout')}</label>
|
|
11092
12140
|
<input type="number" id="set-handshake-timeout" value="\${s.handshakeTimeout || 60}" min="10" max="300" placeholder="60">
|
|
11093
|
-
<div class="hint"
|
|
12141
|
+
<div class="hint">\${t('hs_timeout_hint')}</div>
|
|
11094
12142
|
</div>
|
|
11095
12143
|
</div>
|
|
11096
12144
|
<div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
|
|
@@ -11106,24 +12154,24 @@ function renderSettingsAdvanced() {
|
|
|
11106
12154
|
const el = $('#settings-content');
|
|
11107
12155
|
|
|
11108
12156
|
html(el, \\\`
|
|
11109
|
-
<p class="section-desc"
|
|
12157
|
+
<p class="section-desc">\${t('adv_desc')}</p>
|
|
11110
12158
|
|
|
11111
12159
|
<div class="card">
|
|
11112
|
-
<div class="card-header"><div class="card-title"
|
|
12160
|
+
<div class="card-header"><div class="card-title">\${t('file_transfer')}</div></div>
|
|
11113
12161
|
<div class="form-row">
|
|
11114
12162
|
<div class="form-group">
|
|
11115
|
-
<label
|
|
12163
|
+
<label>\${t('enable_ft')}</label>
|
|
11116
12164
|
<div style="display:flex;align-items:center;gap:10px;margin-top:6px">
|
|
11117
12165
|
<label class="toggle-label">
|
|
11118
12166
|
<input type="checkbox" id="set-enable-ft" \${s.enableFileTransfer ? 'checked' : ''}>
|
|
11119
12167
|
<span class="toggle-slider"></span>
|
|
11120
|
-
<span
|
|
12168
|
+
<span>\${t('allow_ft')}</span>
|
|
11121
12169
|
</label>
|
|
11122
12170
|
</div>
|
|
11123
|
-
<div class="hint"
|
|
12171
|
+
<div class="hint">\${t('enable_ft_hint')}</div>
|
|
11124
12172
|
</div>
|
|
11125
12173
|
<div class="form-group">
|
|
11126
|
-
<label
|
|
12174
|
+
<label>\${t('max_file_size')}</label>
|
|
11127
12175
|
<select id="set-max-file-size">
|
|
11128
12176
|
<option value="10485760" \${s.maxFileSize <= 10485760 ? 'selected' : ''}>10 MB</option>
|
|
11129
12177
|
<option value="52428800" \${s.maxFileSize > 10485760 && s.maxFileSize <= 52428800 ? 'selected' : ''}>50 MB</option>
|
|
@@ -11131,7 +12179,7 @@ function renderSettingsAdvanced() {
|
|
|
11131
12179
|
<option value="524288000" \${s.maxFileSize > 104857600 && s.maxFileSize <= 524288000 ? 'selected' : ''}>500 MB</option>
|
|
11132
12180
|
<option value="1073741824" \${s.maxFileSize > 524288000 ? 'selected' : ''}>1 GB</option>
|
|
11133
12181
|
</select>
|
|
11134
|
-
<div class="hint"
|
|
12182
|
+
<div class="hint">\${t('max_file_size_hint')}\${formatBytes(s.maxFileSize)}.</div>
|
|
11135
12183
|
</div>
|
|
11136
12184
|
</div>
|
|
11137
12185
|
<div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
|
|
@@ -11140,17 +12188,17 @@ function renderSettingsAdvanced() {
|
|
|
11140
12188
|
</div>
|
|
11141
12189
|
|
|
11142
12190
|
<div class="card">
|
|
11143
|
-
<div class="card-header"><div class="card-title"
|
|
12191
|
+
<div class="card-header"><div class="card-title">\${t('logging')}</div></div>
|
|
11144
12192
|
<div class="form-group">
|
|
11145
|
-
<label
|
|
12193
|
+
<label>\${t('log_level')}</label>
|
|
11146
12194
|
<select id="set-log-level" style="max-width:300px">
|
|
11147
|
-
<option value="debug" \${s.logLevel === 'debug' ? 'selected' : ''}
|
|
11148
|
-
<option value="info" \${s.logLevel === 'info' ? 'selected' : ''}
|
|
11149
|
-
<option value="warn" \${s.logLevel === 'warn' ? 'selected' : ''}
|
|
11150
|
-
<option value="error" \${s.logLevel === 'error' ? 'selected' : ''}
|
|
11151
|
-
<option value="none" \${s.logLevel === 'none' ? 'selected' : ''}
|
|
12195
|
+
<option value="debug" \${s.logLevel === 'debug' ? 'selected' : ''}>\${t('log_debug')}</option>
|
|
12196
|
+
<option value="info" \${s.logLevel === 'info' ? 'selected' : ''}>\${t('log_info')}</option>
|
|
12197
|
+
<option value="warn" \${s.logLevel === 'warn' ? 'selected' : ''}>\${t('log_warn')}</option>
|
|
12198
|
+
<option value="error" \${s.logLevel === 'error' ? 'selected' : ''}>\${t('log_error')}</option>
|
|
12199
|
+
<option value="none" \${s.logLevel === 'none' ? 'selected' : ''}>\${t('log_none')}</option>
|
|
11152
12200
|
</select>
|
|
11153
|
-
<div class="hint"
|
|
12201
|
+
<div class="hint">\${t('log_level_hint')}</div>
|
|
11154
12202
|
</div>
|
|
11155
12203
|
<div style="display:flex;justify-content:flex-end;padding-top:8px;border-top:1px solid var(--border);margin-top:8px">
|
|
11156
12204
|
\${sectionSaveBtn('logging', 'log')}
|
|
@@ -11158,12 +12206,12 @@ function renderSettingsAdvanced() {
|
|
|
11158
12206
|
</div>
|
|
11159
12207
|
|
|
11160
12208
|
<div class="card">
|
|
11161
|
-
<div class="card-header"><div class="card-title"
|
|
12209
|
+
<div class="card-header"><div class="card-title">\${t('import_export')}</div></div>
|
|
11162
12210
|
<div style="display:flex;gap:10px;flex-wrap:wrap">
|
|
11163
|
-
<button class="btn btn-default btn-sm" onclick="exportSettings()"
|
|
11164
|
-
<button class="btn btn-ok btn-sm" onclick="showImportSettingsModal()"
|
|
12211
|
+
<button class="btn btn-default btn-sm" onclick="exportSettings()">\${t('export_settings')}</button>
|
|
12212
|
+
<button class="btn btn-ok btn-sm" onclick="showImportSettingsModal()">\${t('import_settings')}</button>
|
|
11165
12213
|
</div>
|
|
11166
|
-
<div class="hint" style="margin-top:10px"
|
|
12214
|
+
<div class="hint" style="margin-top:10px">\${t('import_export_hint')}</div>
|
|
11167
12215
|
</div>
|
|
11168
12216
|
\\\`);
|
|
11169
12217
|
}
|
|
@@ -11172,7 +12220,7 @@ function renderSettingsAdvanced() {
|
|
|
11172
12220
|
async function saveSettingsSection(section, id) {
|
|
11173
12221
|
const btn = $('#btn-save-' + id);
|
|
11174
12222
|
const statusEl = $('#status-' + id);
|
|
11175
|
-
if (btn) { btn.disabled = true; btn.textContent = '
|
|
12223
|
+
if (btn) { btn.disabled = true; btn.textContent = t('saving'); }
|
|
11176
12224
|
if (statusEl) { statusEl.textContent = ''; statusEl.style.color = 'var(--text3)'; }
|
|
11177
12225
|
|
|
11178
12226
|
let data = {};
|
|
@@ -11220,17 +12268,17 @@ async function saveSettingsSection(section, id) {
|
|
|
11220
12268
|
body: JSON.stringify({ section, data }),
|
|
11221
12269
|
});
|
|
11222
12270
|
|
|
11223
|
-
if (btn) { btn.disabled = false; btn.textContent = '
|
|
12271
|
+
if (btn) { btn.disabled = false; btn.textContent = t('save'); }
|
|
11224
12272
|
|
|
11225
12273
|
if (r.success) {
|
|
11226
|
-
toast('
|
|
11227
|
-
if (statusEl) { statusEl.textContent = '
|
|
12274
|
+
toast(t('settings_saved') + section, 'ok');
|
|
12275
|
+
if (statusEl) { statusEl.textContent = t('saved'); statusEl.style.color = 'var(--ok)'; }
|
|
11228
12276
|
// Refresh settings data
|
|
11229
12277
|
const fresh = await api('/settings');
|
|
11230
12278
|
if (fresh && !fresh.error) { _settingsData = fresh; }
|
|
11231
12279
|
} else {
|
|
11232
|
-
toast(r.message || r.error || '
|
|
11233
|
-
if (statusEl) { statusEl.textContent = '\u2717 ' + (r.message || '
|
|
12280
|
+
toast(r.message || r.error || t('failed_save'), 'err');
|
|
12281
|
+
if (statusEl) { statusEl.textContent = '\u2717 ' + (r.message || t('failed')); statusEl.style.color = 'var(--danger)'; }
|
|
11234
12282
|
}
|
|
11235
12283
|
}
|
|
11236
12284
|
|
|
@@ -11248,15 +12296,15 @@ async function saveSettings() {
|
|
|
11248
12296
|
const r = await api('/settings', { method: 'PUT', body: JSON.stringify(allData) });
|
|
11249
12297
|
_settingsSaving = false;
|
|
11250
12298
|
|
|
11251
|
-
if (r.success) { toast('
|
|
11252
|
-
else { toast(r.message || r.error || '
|
|
12299
|
+
if (r.success) { toast(t('all_saved'), 'ok'); setTimeout(() => loadSettings(), 800); }
|
|
12300
|
+
else { toast(r.message || r.error || t('failed_save'), 'err'); }
|
|
11253
12301
|
}
|
|
11254
12302
|
|
|
11255
12303
|
// \u2500\u2500 Reset Identity \u2500\u2500
|
|
11256
12304
|
function showResetIdentityModal() {
|
|
11257
12305
|
$('#reset-confirm-input').value = '';
|
|
11258
12306
|
$('#reset-confirm-btn').disabled = true;
|
|
11259
|
-
$('#reset-confirm-btn').textContent = '
|
|
12307
|
+
$('#reset-confirm-btn').textContent = t('delete_everything');
|
|
11260
12308
|
showModal('modal-reset-identity');
|
|
11261
12309
|
setTimeout(() => $('#reset-confirm-input')?.focus(), 100);
|
|
11262
12310
|
}
|
|
@@ -11264,27 +12312,27 @@ function showResetIdentityModal() {
|
|
|
11264
12312
|
function checkResetConfirm() {
|
|
11265
12313
|
const v = $('#reset-confirm-input')?.value?.trim();
|
|
11266
12314
|
const btn = $('#reset-confirm-btn');
|
|
11267
|
-
if (btn) { btn.disabled = (v !== 'RESET'); btn.textContent = v === 'RESET' ? '
|
|
12315
|
+
if (btn) { btn.disabled = (v !== 'RESET'); btn.textContent = v === 'RESET' ? t('confirm_delete') : t('delete_everything'); }
|
|
11268
12316
|
}
|
|
11269
12317
|
|
|
11270
12318
|
async function executeResetIdentity() {
|
|
11271
12319
|
const btn = $('#reset-confirm-btn');
|
|
11272
|
-
if (btn) { btn.disabled = true; btn.textContent = '
|
|
12320
|
+
if (btn) { btn.disabled = true; btn.textContent = t('resetting'); }
|
|
11273
12321
|
|
|
11274
12322
|
const r = await api('/settings/reset-identity', {
|
|
11275
12323
|
method: 'POST',
|
|
11276
12324
|
body: JSON.stringify({ confirm: true }),
|
|
11277
12325
|
});
|
|
11278
12326
|
|
|
11279
|
-
if (btn) { btn.disabled = false; btn.textContent = '
|
|
12327
|
+
if (btn) { btn.disabled = false; btn.textContent = t('delete_everything'); }
|
|
11280
12328
|
|
|
11281
12329
|
if (r.success) {
|
|
11282
|
-
toast('
|
|
12330
|
+
toast(t('reset_success'), 'ok');
|
|
11283
12331
|
hideModal('modal-reset-identity');
|
|
11284
12332
|
// Reload settings to reflect cleared state
|
|
11285
12333
|
setTimeout(() => loadSettings(), 1000);
|
|
11286
12334
|
} else {
|
|
11287
|
-
toast(r.message || r.error || '
|
|
12335
|
+
toast(r.message || r.error || t('reset_failed'), 'err');
|
|
11288
12336
|
}
|
|
11289
12337
|
}
|
|
11290
12338
|
|
|
@@ -11301,7 +12349,7 @@ async function exportSettings() {
|
|
|
11301
12349
|
a.download = 'aicq-settings-' + new Date().toISOString().slice(0, 10) + '.json';
|
|
11302
12350
|
a.click();
|
|
11303
12351
|
URL.revokeObjectURL(url);
|
|
11304
|
-
toast('
|
|
12352
|
+
toast(t('exported_success'), 'ok');
|
|
11305
12353
|
}
|
|
11306
12354
|
|
|
11307
12355
|
function showImportSettingsModal() {
|
|
@@ -11312,27 +12360,27 @@ function showImportSettingsModal() {
|
|
|
11312
12360
|
|
|
11313
12361
|
async function executeImportSettings() {
|
|
11314
12362
|
const raw = $('#import-json-input')?.value?.trim();
|
|
11315
|
-
if (!raw) { toast('
|
|
12363
|
+
if (!raw) { toast(t('paste_json'), 'warn'); return; }
|
|
11316
12364
|
|
|
11317
12365
|
let settings;
|
|
11318
|
-
try { settings = JSON.parse(raw); } catch (e) { toast('
|
|
12366
|
+
try { settings = JSON.parse(raw); } catch (e) { toast(t('invalid_json') + e.message, 'err'); return; }
|
|
11319
12367
|
|
|
11320
12368
|
const btn = $('#import-confirm-btn');
|
|
11321
|
-
if (btn) { btn.disabled = true; btn.textContent = '
|
|
12369
|
+
if (btn) { btn.disabled = true; btn.textContent = t('importing'); }
|
|
11322
12370
|
|
|
11323
12371
|
const r = await api('/settings/import', {
|
|
11324
12372
|
method: 'POST',
|
|
11325
12373
|
body: JSON.stringify({ settings, merge: true }),
|
|
11326
12374
|
});
|
|
11327
12375
|
|
|
11328
|
-
if (btn) { btn.disabled = false; btn.textContent = '
|
|
12376
|
+
if (btn) { btn.disabled = false; btn.textContent = t('import_settings'); }
|
|
11329
12377
|
|
|
11330
12378
|
if (r.success) {
|
|
11331
|
-
toast('
|
|
12379
|
+
toast(t('imported_success'), 'ok');
|
|
11332
12380
|
hideModal('modal-import-settings');
|
|
11333
12381
|
setTimeout(() => loadSettings(), 800);
|
|
11334
12382
|
} else {
|
|
11335
|
-
toast(r.message || r.error || '
|
|
12383
|
+
toast(r.message || r.error || t('import_failed'), 'err');
|
|
11336
12384
|
}
|
|
11337
12385
|
}
|
|
11338
12386
|
|
|
@@ -11343,7 +12391,7 @@ let _jsonEditorConfigFile = '';
|
|
|
11343
12391
|
|
|
11344
12392
|
async function renderSettingsJsonEditor() {
|
|
11345
12393
|
const el = $('#settings-content');
|
|
11346
|
-
html(el, '<div class="loading-mask"><div class="spinner"></div>
|
|
12394
|
+
html(el, '<div class="loading-mask"><div class="spinner"></div>' + t('loading_config') + '</div>');
|
|
11347
12395
|
|
|
11348
12396
|
const queryParams = _jsonEditorConfigFile ? '?file=' + encodeURIComponent(_jsonEditorConfigFile) : '';
|
|
11349
12397
|
const data = await api('/config-file/raw' + queryParams);
|
|
@@ -11375,7 +12423,7 @@ async function renderSettingsJsonEditor() {
|
|
|
11375
12423
|
|
|
11376
12424
|
<div class="card">
|
|
11377
12425
|
<div class="card-header">
|
|
11378
|
-
<div class="card-title"
|
|
12426
|
+
<div class="card-title">\${t('json_editor')}</div>
|
|
11379
12427
|
<div style="display:flex;gap:8px;align-items:center">
|
|
11380
12428
|
<span class="mono" style="font-size:11px;color:var(--text3)">\${escHtml(data.filePath)}</span>
|
|
11381
12429
|
<button class="btn btn-sm btn-default" onclick="renderSettingsJsonEditor()">\u{1F504} Reload</button>
|
|
@@ -11383,19 +12431,19 @@ async function renderSettingsJsonEditor() {
|
|
|
11383
12431
|
</div>
|
|
11384
12432
|
\${fileSelectorHtml}
|
|
11385
12433
|
<div class="form-group">
|
|
11386
|
-
<label
|
|
12434
|
+
<label>\${t('raw_json')}</label>
|
|
11387
12435
|
<textarea id="json-editor" style="min-height:400px;font-family:'SF Mono','Fira Code','Cascadia Code',monospace;font-size:12px;line-height:1.5;tab-size:2;background:var(--bg)" spellcheck="false">\${escHtml(data.content)}</textarea>
|
|
11388
|
-
<div class="hint"
|
|
12436
|
+
<div class="hint">\${t('raw_json_hint')}</div>
|
|
11389
12437
|
</div>
|
|
11390
12438
|
<div id="json-editor-status" style="margin-bottom:12px;font-size:12px"></div>
|
|
11391
12439
|
<div class="form-actions" style="justify-content:space-between">
|
|
11392
12440
|
<div style="display:flex;gap:8px">
|
|
11393
|
-
<button class="btn btn-sm btn-default" onclick="formatJsonEditor()"
|
|
11394
|
-
<button class="btn btn-sm btn-default" onclick="copyText($('#json-editor')?.value || '')"
|
|
12441
|
+
<button class="btn btn-sm btn-default" onclick="formatJsonEditor()">\${t('format')}</button>
|
|
12442
|
+
<button class="btn btn-sm btn-default" onclick="copyText($('#json-editor')?.value || '')">\${t('copy')}</button>
|
|
11395
12443
|
</div>
|
|
11396
12444
|
<div style="display:flex;gap:8px">
|
|
11397
|
-
<button class="btn btn-sm btn-default" onclick="renderSettingsJsonEditor()"
|
|
11398
|
-
<button class="btn btn-sm btn-primary" id="btn-save-json" onclick="saveJsonConfig()"
|
|
12445
|
+
<button class="btn btn-sm btn-default" onclick="renderSettingsJsonEditor()">\${t('revert')}</button>
|
|
12446
|
+
<button class="btn btn-sm btn-primary" id="btn-save-json" onclick="saveJsonConfig()">\${t('save_config')}</button>
|
|
11399
12447
|
</div>
|
|
11400
12448
|
</div>
|
|
11401
12449
|
</div>
|
|
@@ -11408,10 +12456,10 @@ function formatJsonEditor() {
|
|
|
11408
12456
|
try {
|
|
11409
12457
|
const obj = JSON.parse(ta.value);
|
|
11410
12458
|
ta.value = JSON.stringify(obj, null, 2);
|
|
11411
|
-
toast('
|
|
11412
|
-
$('#json-editor-status').innerHTML = '<span style="color:var(--ok)"
|
|
12459
|
+
toast(t('json_formatted'), 'ok');
|
|
12460
|
+
$('#json-editor-status').innerHTML = '<span style="color:var(--ok)">' + t('valid_json') + '</span>';
|
|
11413
12461
|
} catch (e) {
|
|
11414
|
-
toast('
|
|
12462
|
+
toast(t('invalid_json') + e.message, 'err');
|
|
11415
12463
|
$('#json-editor-status').innerHTML = '<span style="color:var(--danger)">\u2717 ' + escHtml(e.message) + '</span>';
|
|
11416
12464
|
}
|
|
11417
12465
|
}
|
|
@@ -11420,29 +12468,439 @@ async function saveJsonConfig() {
|
|
|
11420
12468
|
const btn = $('#btn-save-json');
|
|
11421
12469
|
const statusEl = $('#json-editor-status');
|
|
11422
12470
|
const raw = $('#json-editor')?.value;
|
|
11423
|
-
if (!raw) { toast('
|
|
12471
|
+
if (!raw) { toast(t('no_content'), 'warn'); return; }
|
|
11424
12472
|
|
|
11425
12473
|
// Validate first
|
|
11426
12474
|
try { JSON.parse(raw); } catch (e) {
|
|
11427
|
-
toast('
|
|
12475
|
+
toast(t('invalid_json') + e.message, 'err');
|
|
11428
12476
|
if (statusEl) statusEl.innerHTML = '<span style="color:var(--danger)">\u2717 ' + escHtml(e.message) + '</span>';
|
|
11429
12477
|
return;
|
|
11430
12478
|
}
|
|
11431
12479
|
|
|
11432
|
-
if (btn) { btn.disabled = true; btn.textContent = '
|
|
11433
|
-
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>
|
|
12480
|
+
if (btn) { btn.disabled = true; btn.textContent = t('saving'); }
|
|
12481
|
+
if (statusEl) statusEl.innerHTML = '<span style="color:var(--text3)"><span class="spinner" style="width:12px;height:12px;border-width:2px;display:inline-block;vertical-align:middle;margin-right:6px"></span> ' + t('saving') + '</span>';
|
|
11434
12482
|
|
|
11435
12483
|
const queryParams = _jsonEditorConfigFile ? '?file=' + encodeURIComponent(_jsonEditorConfigFile) : '';
|
|
11436
12484
|
const r = await api('/config-file/raw' + queryParams, { method: 'PUT', body: JSON.stringify({ content: raw }) });
|
|
11437
12485
|
|
|
11438
|
-
if (btn) { btn.disabled = false; btn.textContent = '
|
|
12486
|
+
if (btn) { btn.disabled = false; btn.textContent = t('save_config'); }
|
|
11439
12487
|
|
|
11440
12488
|
if (r.success) {
|
|
11441
|
-
toast('
|
|
12489
|
+
toast(t('config_saved'), 'ok');
|
|
11442
12490
|
if (statusEl) statusEl.innerHTML = '<span style="color:var(--ok)">\u2713 Saved at ' + new Date().toLocaleTimeString() + '</span>';
|
|
11443
12491
|
} else {
|
|
11444
|
-
toast(r.message || '
|
|
11445
|
-
if (statusEl) statusEl.innerHTML = '<span style="color:var(--danger)">\u2717 ' + escHtml(r.message || '
|
|
12492
|
+
toast(r.message || t('failed_save'), 'err');
|
|
12493
|
+
if (statusEl) statusEl.innerHTML = '<span style="color:var(--danger)">\u2717 ' + escHtml(r.message || t('failed')) + '</span>';
|
|
12494
|
+
}
|
|
12495
|
+
}
|
|
12496
|
+
|
|
12497
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
12498
|
+
// PAGE: OpenClaw Config
|
|
12499
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
12500
|
+
let _ocConfig = null;
|
|
12501
|
+
let _ocTab = 'defaults';
|
|
12502
|
+
|
|
12503
|
+
async function loadOpenClawConfig() {
|
|
12504
|
+
const el = $('#openclaw-content');
|
|
12505
|
+
html(el, '<div class="loading-mask"><div class="spinner"></div>' + t('loading_openclaw') + '</div>');
|
|
12506
|
+
try {
|
|
12507
|
+
const data = await api('/openclaw-config');
|
|
12508
|
+
if (data.error) { html(el, '<div class="card"><p style="color:var(--danger)">' + escHtml(data.error) + '</p></div>'); return; }
|
|
12509
|
+
_ocConfig = data;
|
|
12510
|
+
_ocTab = 'defaults';
|
|
12511
|
+
renderOpenClawConfig(data);
|
|
12512
|
+
} catch (e) {
|
|
12513
|
+
html(el, '<div class="card"><p style="color:var(--danger)">' + escHtml(e.message || 'Failed to load') + '</p></div>');
|
|
12514
|
+
}
|
|
12515
|
+
}
|
|
12516
|
+
|
|
12517
|
+
function switchOpenClawTab(tab) {
|
|
12518
|
+
_ocTab = tab;
|
|
12519
|
+
$$('.oc-tab').forEach(b => b.classList.toggle('active', b.dataset.ocTab === tab));
|
|
12520
|
+
$$('.oc-panel').forEach(p => { p.style.display = p.id === 'oc-panel-' + tab ? '' : 'none'; });
|
|
12521
|
+
}
|
|
12522
|
+
|
|
12523
|
+
function renderOpenClawConfig(data) {
|
|
12524
|
+
const el = $('#openclaw-content');
|
|
12525
|
+
const tabBtns = [
|
|
12526
|
+
{ id: 'defaults', label: t('tab_defaults') },
|
|
12527
|
+
{ id: 'agents', label: t('tab_agents') },
|
|
12528
|
+
{ id: 'bindings', label: t('tab_bindings') },
|
|
12529
|
+
{ id: 'channels', label: t('tab_channels') },
|
|
12530
|
+
];
|
|
12531
|
+
let tabsHtml = '<div style="display:flex;gap:6px;margin-bottom:20px;flex-wrap:wrap">';
|
|
12532
|
+
for (const tb of tabBtns) {
|
|
12533
|
+
tabsHtml += '<button class="btn btn-sm filter-btn oc-tab' + (tb.id === _ocTab ? ' active' : '') + '" data-oc-tab="' + tb.id + '" onclick="switchOpenClawTab(\\'' + tb.id + '\\')">' + tb.label + '</button>';
|
|
12534
|
+
}
|
|
12535
|
+
tabsHtml += '</div>';
|
|
12536
|
+
|
|
12537
|
+
let panelsHtml = '<div id="oc-panel-defaults" class="oc-panel"' + (_ocTab !== 'defaults' ? ' style="display:none"' : '') + '>' + renderOcDefaults(data.agents) + '</div>';
|
|
12538
|
+
panelsHtml += '<div id="oc-panel-agents" class="oc-panel"' + (_ocTab !== 'agents' ? ' style="display:none"' : '') + '>' + renderOcAgentList(data.agents) + '</div>';
|
|
12539
|
+
panelsHtml += '<div id="oc-panel-bindings" class="oc-panel"' + (_ocTab !== 'bindings' ? ' style="display:none"' : '') + '>' + renderOcBindings(data.bindings) + '</div>';
|
|
12540
|
+
panelsHtml += '<div id="oc-panel-channels" class="oc-panel"' + (_ocTab !== 'channels' ? ' style="display:none"' : '') + '>' + renderOcChannels(data.channels) + '</div>';
|
|
12541
|
+
|
|
12542
|
+
html(el, '<div class="section-desc">' + t('openclaw_desc') + '</div>' +
|
|
12543
|
+
'<div class="card"><div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;flex-wrap:wrap;gap:8px"><div class="card-title">' + t('openclaw_config') + '</div>' +
|
|
12544
|
+
'<button class="btn btn-primary btn-sm" onclick="saveOpenClawConfig()">' + t('save_openclaw_config') + '</button></div>' +
|
|
12545
|
+
tabsHtml + panelsHtml + '</div>');
|
|
12546
|
+
}
|
|
12547
|
+
|
|
12548
|
+
function renderOcDefaults(agents) {
|
|
12549
|
+
const d = agents?.defaults || {};
|
|
12550
|
+
const model = d.model || {};
|
|
12551
|
+
const imgModel = d.imageModel || {};
|
|
12552
|
+
const subagents = d.subagents || {};
|
|
12553
|
+
const modelsRegistry = d.models || {};
|
|
12554
|
+
|
|
12555
|
+
let fallbacksHtml = '';
|
|
12556
|
+
const fallbacks = model.fallbacks || [];
|
|
12557
|
+
for (let i = 0; i < fallbacks.length; i++) {
|
|
12558
|
+
fallbacksHtml += '<div style="display:flex;gap:6px;align-items:center;margin-bottom:6px"><input type="text" class="oc-fallback-item" value="' + escHtml(fallbacks[i]) + '" style="flex:1" placeholder="model-id"><button class="btn btn-sm btn-danger" onclick="this.parentElement.remove()">\u2715</button></div>';
|
|
12559
|
+
}
|
|
12560
|
+
fallbacksHtml += '<button class="btn btn-sm btn-default" onclick="addOcListItem(this,\\'oc-fallback-item\\')">\u2795 ' + t('add_item') + '</button>';
|
|
12561
|
+
|
|
12562
|
+
let imgFallbacksHtml = '';
|
|
12563
|
+
const imgFallbacks = imgModel.fallbacks || [];
|
|
12564
|
+
for (let i = 0; i < imgFallbacks.length; i++) {
|
|
12565
|
+
imgFallbacksHtml += '<div style="display:flex;gap:6px;align-items:center;margin-bottom:6px"><input type="text" class="oc-img-fallback-item" value="' + escHtml(imgFallbacks[i]) + '" style="flex:1" placeholder="model-id"><button class="btn btn-sm btn-danger" onclick="this.parentElement.remove()">\u2715</button></div>';
|
|
12566
|
+
}
|
|
12567
|
+
imgFallbacksHtml += '<button class="btn btn-sm btn-default" onclick="addOcListItem(this,\\'oc-img-fallback-item\\')">\u2795 ' + t('add_item') + '</button>';
|
|
12568
|
+
|
|
12569
|
+
let modelsRegHtml = '';
|
|
12570
|
+
const modelKeys = Object.keys(modelsRegistry);
|
|
12571
|
+
for (let i = 0; i < modelKeys.length; i++) {
|
|
12572
|
+
const k = modelKeys[i];
|
|
12573
|
+
modelsRegHtml += '<div style="display:flex;gap:6px;align-items:center;margin-bottom:6px"><input type="text" class="oc-model-reg-key" value="' + escHtml(k) + '" style="width:45%" placeholder="model-id"><span style="color:var(--text3)">\u2192</span><input type="text" class="oc-model-reg-val" value="" style="flex:1" placeholder="{}" disabled><button class="btn btn-sm btn-danger" onclick="this.parentElement.remove()">\u2715</button></div>';
|
|
12574
|
+
}
|
|
12575
|
+
modelsRegHtml += '<button class="btn btn-sm btn-default" onclick="addOcModelRegItem(this)">\u2795 ' + t('add_item') + '</button>';
|
|
12576
|
+
|
|
12577
|
+
return '<div class="card" style="margin-bottom:16px"><div class="card-title">' + t('agent_defaults') + '</div><div class="card-desc">' + t('agent_defaults_desc') + '</div>' +
|
|
12578
|
+
'<div class="form-group"><label>' + t('compaction_mode') + '</label><input type="text" id="oc-compaction-mode" value="' + escHtml((d.compaction?.mode) || '') + '" placeholder="safeguard"></div>' +
|
|
12579
|
+
'<div class="form-row">' +
|
|
12580
|
+
'<div class="form-group"><label>' + t('primary_model') + '</label><input type="text" id="oc-primary-model" value="' + escHtml(model.primary || '') + '" placeholder="zerotoken/glm-4-flash"></div>' +
|
|
12581
|
+
'<div class="form-group"><label>' + t('max_concurrent') + '</label><input type="number" id="oc-max-concurrent" value="' + (d.maxConcurrent || '') + '" min="1" max="32"></div>' +
|
|
12582
|
+
'</div>' +
|
|
12583
|
+
'<div class="form-group"><label>' + t('fallback_models') + '</label><div id="oc-fallbacks-list">' + fallbacksHtml + '</div></div>' +
|
|
12584
|
+
'<div class="form-row">' +
|
|
12585
|
+
'<div class="form-group"><label>' + t('image_model') + ' (Primary)</label><input type="text" id="oc-img-model-primary" value="' + escHtml(imgModel.primary || '') + '" placeholder="modelscope/ZhipuAI/GLM-5"></div>' +
|
|
12586
|
+
'<div class="form-group"><label>' + t('subagent_max_concurrent') + '</label><input type="number" id="oc-subagent-max" value="' + (subagents.maxConcurrent || '') + '" min="1" max="32"></div>' +
|
|
12587
|
+
'</div>' +
|
|
12588
|
+
'<div class="form-group"><label>' + t('image_model') + ' (Fallbacks)</label><div id="oc-img-fallbacks-list">' + imgFallbacksHtml + '</div></div>' +
|
|
12589
|
+
'<div class="form-row">' +
|
|
12590
|
+
'<div class="form-group"><label>' + t('thinking_default') + '</label><select id="oc-thinking-default"><option value="off"' + (d.thinkingDefault === 'off' || !d.thinkingDefault ? ' selected' : '') + '>off</option><option value="low"' + (d.thinkingDefault === 'low' ? ' selected' : '') + '>low</option><option value="medium"' + (d.thinkingDefault === 'medium' ? ' selected' : '') + '>medium</option><option value="high"' + (d.thinkingDefault === 'high' ? ' selected' : '') + '>high</option></select></div>' +
|
|
12591
|
+
'<div class="form-group"><label>' + t('workspace') + '</label><input type="text" id="oc-workspace" value="' + escHtml(d.workspace || '') + '" placeholder="~/.openclaw/workspace"></div>' +
|
|
12592
|
+
'</div>' +
|
|
12593
|
+
'<div class="form-group"><label>' + t('models_registry') + '</label><div id="oc-models-reg-list">' + modelsRegHtml + '</div></div>' +
|
|
12594
|
+
'</div>';
|
|
12595
|
+
}
|
|
12596
|
+
|
|
12597
|
+
function renderOcAgentList(agents) {
|
|
12598
|
+
const list = agents?.list || [];
|
|
12599
|
+
let tableHtml = '';
|
|
12600
|
+
if (list.length === 0) {
|
|
12601
|
+
tableHtml = '<div class="empty"><div class="icon">\u{1F916}</div><p>' + t('no_agents_in_list') + '</p></div>';
|
|
12602
|
+
} else {
|
|
12603
|
+
tableHtml = '<table><thead><tr><th>ID</th><th>' + t('agent_identity_name') + '</th><th>' + t('agent_model_primary') + '</th><th>' + t('agent_tools_profile') + '</th><th>' + t('actions') + '</th></tr></thead><tbody>';
|
|
12604
|
+
for (let i = 0; i < list.length; i++) {
|
|
12605
|
+
const a = list[i];
|
|
12606
|
+
tableHtml += '<tr><td class="mono">' + escHtml(a.id || '') + '</td><td>' + escHtml(a.identity?.name || '') + '</td><td class="mono" style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml(a.model?.primary || '') + '</td><td><span class="badge badge-ghost">' + escHtml(a.tools?.profile || '') + '</span></td>' +
|
|
12607
|
+
'<td class="actions-cell"><button class="btn btn-sm btn-default" onclick="editOcAgent(' + i + ')">\u270F\uFE0F</button><button class="btn btn-sm btn-danger" onclick="deleteOcAgent(' + i + ')">\u{1F5D1}\uFE0F</button></td></tr>';
|
|
12608
|
+
}
|
|
12609
|
+
tableHtml += '</tbody></table>';
|
|
12610
|
+
}
|
|
12611
|
+
return '<div class="card"><div class="card-header"><div><div class="card-title">' + t('oc_agent_list') + '</div><div class="card-desc">' + t('oc_agent_list_desc') + '</div></div>' +
|
|
12612
|
+
'<button class="btn btn-sm btn-primary" onclick="addOcAgent()">' + t('add_agent_btn') + '</button></div>' +
|
|
12613
|
+
'<div id="oc-agents-table">' + tableHtml + '</div></div>';
|
|
12614
|
+
}
|
|
12615
|
+
|
|
12616
|
+
function renderOcBindings(bindings) {
|
|
12617
|
+
bindings = bindings || [];
|
|
12618
|
+
let tableHtml = '';
|
|
12619
|
+
if (bindings.length === 0) {
|
|
12620
|
+
tableHtml = '<div class="empty"><div class="icon">\u{1F517}</div><p>' + t('no_bindings') + '</p></div>';
|
|
12621
|
+
} else {
|
|
12622
|
+
tableHtml = '<table><thead><tr><th>' + t('binding_agent_id') + '</th><th>' + t('binding_channel') + '</th><th>' + t('binding_account_id') + '</th><th>' + t('binding_type') + '</th><th>' + t('actions') + '</th></tr></thead><tbody>';
|
|
12623
|
+
for (let i = 0; i < bindings.length; i++) {
|
|
12624
|
+
const b = bindings[i];
|
|
12625
|
+
tableHtml += '<tr><td class="mono">' + escHtml(b.agentId || '') + '</td><td class="mono">' + escHtml(b.match?.channel || '') + '</td><td class="mono">' + escHtml(b.match?.accountId || '-') + '</td><td><span class="badge badge-ghost">' + escHtml(b.type || 'route') + '</span></td>' +
|
|
12626
|
+
'<td class="actions-cell"><button class="btn btn-sm btn-danger" onclick="deleteOcBinding(' + i + ')">\u{1F5D1}\uFE0F</button></td></tr>';
|
|
12627
|
+
}
|
|
12628
|
+
tableHtml += '</tbody></table>';
|
|
12629
|
+
}
|
|
12630
|
+
return '<div class="card"><div class="card-header"><div><div class="card-title">' + t('bindings_title') + '</div><div class="card-desc">' + t('bindings_desc') + '</div></div>' +
|
|
12631
|
+
'<button class="btn btn-sm btn-primary" onclick="addOcBinding()">' + t('add_binding') + '</button></div>' +
|
|
12632
|
+
'<div id="oc-bindings-table">' + tableHtml + '</div></div>';
|
|
12633
|
+
}
|
|
12634
|
+
|
|
12635
|
+
function renderOcChannels(channels) {
|
|
12636
|
+
channels = channels || {};
|
|
12637
|
+
const channelKeys = Object.keys(channels);
|
|
12638
|
+
let html2 = '';
|
|
12639
|
+
if (channelKeys.length === 0) {
|
|
12640
|
+
html2 = '<div class="empty"><div class="icon">\u{1F4E1}</div><p>' + t('no_channels_configured') + '</p></div>';
|
|
12641
|
+
} else {
|
|
12642
|
+
for (const ck of channelKeys) {
|
|
12643
|
+
const ch = channels[ck];
|
|
12644
|
+
const accs = ch.accounts || {};
|
|
12645
|
+
const accKeys = Object.keys(accs);
|
|
12646
|
+
const isEnabled = ch.enabled !== false;
|
|
12647
|
+
html2 += '<div class="card" style="margin-bottom:16px">';
|
|
12648
|
+
html2 += '<div class="card-header"><div class="card-title">\u{1F4E1} ' + escHtml(ck) + '</div>' +
|
|
12649
|
+
'<div style="display:flex;gap:8px;align-items:center"><span class="badge ' + (isEnabled ? 'badge-ok' : 'badge-danger') + '">' + (isEnabled ? t('channel_enabled') : t('channel_disabled')) + '</span>' +
|
|
12650
|
+
'<label class="toggle-label"><input type="checkbox" class="oc-ch-enabled" data-channel="' + escHtml(ck) + '"' + (isEnabled ? ' checked' : '') + '><span class="toggle-slider"></span></label></div></div>';
|
|
12651
|
+
|
|
12652
|
+
html2 += '<div class="form-row">';
|
|
12653
|
+
html2 += '<div class="form-group"><label>' + t('group_policy') + '</label><input type="text" class="oc-ch-policy" data-channel="' + escHtml(ck) + '" value="' + escHtml(ch.groupPolicy || '') + '" placeholder="open"></div>';
|
|
12654
|
+
html2 += '</div>';
|
|
12655
|
+
|
|
12656
|
+
html2 += '<div class="form-group"><label>' + t('accounts') + ' (' + accKeys.length + ')</label>';
|
|
12657
|
+
for (const ak of accKeys) {
|
|
12658
|
+
const acc = accs[ak];
|
|
12659
|
+
const allowFrom = acc.allowFrom || [];
|
|
12660
|
+
html2 += '<div class="card" style="margin-bottom:10px;padding:14px;background:var(--bg3)">';
|
|
12661
|
+
html2 += '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px"><strong class="mono">' + escHtml(ak) + '</strong><div class="actions-cell"><button class="btn btn-sm btn-default" onclick="editOcAccount(\\'' + escHtml(ck) + '\\',\\'' + escHtml(ak) + '\\')">\u270F\uFE0F</button><button class="btn btn-sm btn-danger" onclick="deleteOcAccount(\\'' + escHtml(ck) + '\\',\\'' + escHtml(ak) + '\\')">\u{1F5D1}\uFE0F</button></div></div>';
|
|
12662
|
+
html2 += '<div class="form-group"><label>Bot Token</label><input type="password" class="oc-acc-token" data-channel="' + escHtml(ck) + '" data-account="' + escHtml(ak) + '" value="' + escHtml(acc.botToken || '') + '" placeholder="****"></div>';
|
|
12663
|
+
html2 += '<div class="form-group"><label>' + t('dm_policy') + '</label><select class="oc-acc-dm-policy" data-channel="' + escHtml(ck) + '" data-account="' + escHtml(ak) + '"><option value="allowlist"' + (acc.dmPolicy === 'allowlist' ? ' selected' : '') + '>allowlist</option><option value="open"' + (acc.dmPolicy === 'open' ? ' selected' : '') + '>open</option><option value="block"' + (acc.dmPolicy === 'block' ? ' selected' : '') + '>block</option></select></div>';
|
|
12664
|
+
html2 += '<div class="form-group"><label>' + t('allow_from') + '</label>';
|
|
12665
|
+
for (const af of allowFrom) {
|
|
12666
|
+
html2 += '<div style="display:flex;gap:6px;align-items:center;margin-bottom:4px"><input type="text" class="oc-allow-from" data-channel="' + escHtml(ck) + '" data-account="' + escHtml(ak) + '" value="' + escHtml(af) + '" style="flex:1"><button class="btn btn-sm btn-danger" onclick="this.parentElement.remove()">\u2715</button></div>';
|
|
12667
|
+
}
|
|
12668
|
+
html2 += '<button class="btn btn-sm btn-default" data-channel="' + escHtml(ck) + '" data-account="' + escHtml(ak) + '" onclick="addOcAllowFrom(this)">\u2795 ' + t('add_allow_from') + '</button>';
|
|
12669
|
+
html2 += '</div></div>';
|
|
12670
|
+
}
|
|
12671
|
+
html2 += '<button class="btn btn-sm btn-default" onclick="addOcAccount(\\'' + escHtml(ck) + '\\')">\u2795 ' + t('add_account') + '</button>';
|
|
12672
|
+
html2 += '</div></div>';
|
|
12673
|
+
}
|
|
12674
|
+
}
|
|
12675
|
+
return '<div class="card"><div class="card-header"><div><div class="card-title">' + t('channels_title') + '</div><div class="card-desc">' + t('channels_desc') + '</div></div></div>' +
|
|
12676
|
+
'<div id="oc-channels-content">' + html2 + '</div></div>';
|
|
12677
|
+
}
|
|
12678
|
+
|
|
12679
|
+
// \u2500\u2500 OpenClaw dynamic list helpers \u2500\u2500
|
|
12680
|
+
function addOcListItem(btn, inputClass) {
|
|
12681
|
+
const container = btn.parentElement;
|
|
12682
|
+
const div = document.createElement('div');
|
|
12683
|
+
div.style.cssText = 'display:flex;gap:6px;align-items:center;margin-bottom:6px';
|
|
12684
|
+
div.innerHTML = '<input type="text" class="' + inputClass + '" value="" style="flex:1" placeholder="model-id"><button class="btn btn-sm btn-danger" onclick="this.parentElement.remove()">\u2715</button>';
|
|
12685
|
+
container.insertBefore(div, btn);
|
|
12686
|
+
div.querySelector('input').focus();
|
|
12687
|
+
}
|
|
12688
|
+
|
|
12689
|
+
function addOcModelRegItem(btn) {
|
|
12690
|
+
const container = btn.parentElement;
|
|
12691
|
+
const div = document.createElement('div');
|
|
12692
|
+
div.style.cssText = 'display:flex;gap:6px;align-items:center;margin-bottom:6px';
|
|
12693
|
+
div.innerHTML = '<input type="text" class="oc-model-reg-key" value="" style="width:45%" placeholder="model-id"><span style="color:var(--text3)">\u2192</span><input type="text" class="oc-model-reg-val" value="" style="flex:1" placeholder="{}" disabled><button class="btn btn-sm btn-danger" onclick="this.parentElement.remove()">\u2715</button>';
|
|
12694
|
+
container.insertBefore(div, btn);
|
|
12695
|
+
div.querySelector('.oc-model-reg-key').focus();
|
|
12696
|
+
}
|
|
12697
|
+
|
|
12698
|
+
function addOcAllowFrom(btn) {
|
|
12699
|
+
const container = btn.parentElement;
|
|
12700
|
+
const div = document.createElement('div');
|
|
12701
|
+
div.style.cssText = 'display:flex;gap:6px;align-items:center;margin-bottom:4px';
|
|
12702
|
+
div.innerHTML = '<input type="text" class="oc-allow-from" data-channel="' + (btn.dataset.channel || '') + '" data-account="' + (btn.dataset.account || '') + '" value="" style="flex:1"><button class="btn btn-sm btn-danger" onclick="this.parentElement.remove()">\u2715</button>';
|
|
12703
|
+
container.insertBefore(div, btn);
|
|
12704
|
+
div.querySelector('input').focus();
|
|
12705
|
+
}
|
|
12706
|
+
|
|
12707
|
+
// \u2500\u2500 OpenClaw CRUD operations \u2500\u2500
|
|
12708
|
+
function addOcAgent() {
|
|
12709
|
+
if (!_ocConfig) return;
|
|
12710
|
+
if (!_ocConfig.agents) _ocConfig.agents = { defaults: _ocConfig.agents?.defaults || {} };
|
|
12711
|
+
if (!Array.isArray(_ocConfig.agents.list)) _ocConfig.agents.list = [];
|
|
12712
|
+
_ocConfig.agents.list.push({ id: 'new-agent-' + Date.now(), identity: { name: '' }, model: { primary: '' }, tools: { profile: 'full' }, workspace: '' });
|
|
12713
|
+
renderOpenClawConfig(_ocConfig);
|
|
12714
|
+
switchOpenClawTab('agents');
|
|
12715
|
+
toast(t('agent_added'), 'ok');
|
|
12716
|
+
}
|
|
12717
|
+
|
|
12718
|
+
function editOcAgent(idx) {
|
|
12719
|
+
if (!_ocConfig?.agents?.list?.[idx]) return;
|
|
12720
|
+
const a = _ocConfig.agents.list[idx];
|
|
12721
|
+
const newId = prompt('Agent ID:', a.id);
|
|
12722
|
+
if (newId === null) return;
|
|
12723
|
+
const newName = prompt('Identity Name:', a.identity?.name || '');
|
|
12724
|
+
if (newName === null) return;
|
|
12725
|
+
const newModel = prompt('Primary Model:', a.model?.primary || '');
|
|
12726
|
+
if (newModel === null) return;
|
|
12727
|
+
const newTools = prompt('Tools Profile:', a.tools?.profile || '');
|
|
12728
|
+
if (newTools === null) return;
|
|
12729
|
+
const newWorkspace = prompt('Workspace:', a.workspace || '');
|
|
12730
|
+
if (newWorkspace === null) return;
|
|
12731
|
+
a.id = newId;
|
|
12732
|
+
a.identity = { name: newName };
|
|
12733
|
+
a.model = { primary: newModel };
|
|
12734
|
+
a.tools = { profile: newTools };
|
|
12735
|
+
a.workspace = newWorkspace;
|
|
12736
|
+
renderOpenClawConfig(_ocConfig);
|
|
12737
|
+
switchOpenClawTab('agents');
|
|
12738
|
+
}
|
|
12739
|
+
|
|
12740
|
+
function deleteOcAgent(idx) {
|
|
12741
|
+
if (!_ocConfig?.agents?.list) return;
|
|
12742
|
+
const a = _ocConfig.agents.list[idx];
|
|
12743
|
+
const name = a?.id || a?.identity?.name || '';
|
|
12744
|
+
if (!confirm(t('confirm_delete_oc_agent').replace('{name}', name))) return;
|
|
12745
|
+
_ocConfig.agents.list.splice(idx, 1);
|
|
12746
|
+
renderOpenClawConfig(_ocConfig);
|
|
12747
|
+
switchOpenClawTab('agents');
|
|
12748
|
+
toast(t('agent_deleted'), 'ok');
|
|
12749
|
+
}
|
|
12750
|
+
|
|
12751
|
+
function addOcBinding() {
|
|
12752
|
+
if (!_ocConfig) return;
|
|
12753
|
+
if (!Array.isArray(_ocConfig.bindings)) _ocConfig.bindings = [];
|
|
12754
|
+
const agentId = prompt('Agent ID:');
|
|
12755
|
+
if (!agentId) return;
|
|
12756
|
+
const channel = prompt('Channel:', '');
|
|
12757
|
+
const accountId = prompt('Account ID (optional):', '');
|
|
12758
|
+
const type = prompt('Type (route/direct):', 'route');
|
|
12759
|
+
const binding = { agentId };
|
|
12760
|
+
if (channel) binding.match = { channel };
|
|
12761
|
+
if (accountId) { if (!binding.match) binding.match = {}; binding.match.accountId = accountId; }
|
|
12762
|
+
if (type) binding.type = type;
|
|
12763
|
+
_ocConfig.bindings.push(binding);
|
|
12764
|
+
renderOpenClawConfig(_ocConfig);
|
|
12765
|
+
switchOpenClawTab('bindings');
|
|
12766
|
+
toast(t('openclaw_config_saved'), 'ok');
|
|
12767
|
+
}
|
|
12768
|
+
|
|
12769
|
+
function deleteOcBinding(idx) {
|
|
12770
|
+
if (!_ocConfig?.bindings) return;
|
|
12771
|
+
if (!confirm(t('confirm_delete_binding'))) return;
|
|
12772
|
+
_ocConfig.bindings.splice(idx, 1);
|
|
12773
|
+
renderOpenClawConfig(_ocConfig);
|
|
12774
|
+
switchOpenClawTab('bindings');
|
|
12775
|
+
toast(t('binding_deleted'), 'ok');
|
|
12776
|
+
}
|
|
12777
|
+
|
|
12778
|
+
function addOcAccount(channelKey) {
|
|
12779
|
+
if (!_ocConfig?.channels) return;
|
|
12780
|
+
const ch = _ocConfig.channels[channelKey];
|
|
12781
|
+
if (!ch) return;
|
|
12782
|
+
if (!ch.accounts) ch.accounts = {};
|
|
12783
|
+
const accId = prompt('Account ID:');
|
|
12784
|
+
if (!accId) return;
|
|
12785
|
+
ch.accounts[accId] = { botToken: '', dmPolicy: 'allowlist', allowFrom: [] };
|
|
12786
|
+
renderOpenClawConfig(_ocConfig);
|
|
12787
|
+
switchOpenClawTab('channels');
|
|
12788
|
+
toast(t('openclaw_config_saved'), 'ok');
|
|
12789
|
+
}
|
|
12790
|
+
|
|
12791
|
+
function editOcAccount(channelKey, accountId) {
|
|
12792
|
+
if (!_ocConfig?.channels?.[channelKey]?.accounts?.[accountId]) return;
|
|
12793
|
+
const acc = _ocConfig.channels[channelKey].accounts[accountId];
|
|
12794
|
+
const token = prompt('Bot Token:', acc.botToken || '');
|
|
12795
|
+
if (token === null) return;
|
|
12796
|
+
acc.botToken = token;
|
|
12797
|
+
renderOpenClawConfig(_ocConfig);
|
|
12798
|
+
switchOpenClawTab('channels');
|
|
12799
|
+
}
|
|
12800
|
+
|
|
12801
|
+
function deleteOcAccount(channelKey, accountId) {
|
|
12802
|
+
if (!_ocConfig?.channels?.[channelKey]?.accounts) return;
|
|
12803
|
+
if (!confirm(t('confirm_delete_account'))) return;
|
|
12804
|
+
delete _ocConfig.channels[channelKey].accounts[accountId];
|
|
12805
|
+
renderOpenClawConfig(_ocConfig);
|
|
12806
|
+
switchOpenClawTab('channels');
|
|
12807
|
+
toast(t('openclaw_config_saved'), 'ok');
|
|
12808
|
+
}
|
|
12809
|
+
|
|
12810
|
+
async function saveOpenClawConfig() {
|
|
12811
|
+
if (!_ocConfig) return;
|
|
12812
|
+
// Collect defaults
|
|
12813
|
+
const defaults = _ocConfig.agents?.defaults || {};
|
|
12814
|
+
const model = defaults.model || {};
|
|
12815
|
+
const imgModel = defaults.imageModel || {};
|
|
12816
|
+
const subagents = defaults.subagents || {};
|
|
12817
|
+
const modelsRegistry = defaults.models || {};
|
|
12818
|
+
|
|
12819
|
+
// Collect fallbacks
|
|
12820
|
+
const fallbackItems = $$('.oc-fallback-item');
|
|
12821
|
+
const fallbacks = [];
|
|
12822
|
+
for (const el of fallbackItems) { if (el.value.trim()) fallbacks.push(el.value.trim()); }
|
|
12823
|
+
|
|
12824
|
+
const imgFallbackItems = $$('.oc-img-fallback-item');
|
|
12825
|
+
const imgFallbacks = [];
|
|
12826
|
+
for (const el of imgFallbackItems) { if (el.value.trim()) imgFallbacks.push(el.value.trim()); }
|
|
12827
|
+
|
|
12828
|
+
const modelRegKeys = $$('.oc-model-reg-key');
|
|
12829
|
+
const newModelsReg = {};
|
|
12830
|
+
for (const el of modelRegKeys) { if (el.value.trim()) newModelsReg[el.value.trim()] = {}; }
|
|
12831
|
+
|
|
12832
|
+
const newDefaults = {
|
|
12833
|
+
compaction: { mode: ($('#oc-compaction-mode')?.value || defaults.compaction?.mode || 'safeguard') },
|
|
12834
|
+
model: { primary: $('#oc-primary-model')?.value || model.primary || '', fallbacks },
|
|
12835
|
+
imageModel: { primary: $('#oc-img-model-primary')?.value || imgModel.primary || '', fallbacks: imgFallbacks },
|
|
12836
|
+
maxConcurrent: parseInt($('#oc-max-concurrent')?.value) || defaults.maxConcurrent || 4,
|
|
12837
|
+
subagents: { maxConcurrent: parseInt($('#oc-subagent-max')?.value) || subagents.maxConcurrent || 8 },
|
|
12838
|
+
thinkingDefault: $('#oc-thinking-default')?.value || defaults.thinkingDefault || 'off',
|
|
12839
|
+
workspace: $('#oc-workspace')?.value || defaults.workspace || '',
|
|
12840
|
+
models: newModelsReg,
|
|
12841
|
+
};
|
|
12842
|
+
|
|
12843
|
+
// Collect channels state
|
|
12844
|
+
const channels = _ocConfig.channels || {};
|
|
12845
|
+
const chEnabledEls = $$('.oc-ch-enabled');
|
|
12846
|
+
for (const el of chEnabledEls) {
|
|
12847
|
+
const ck = el.dataset.channel;
|
|
12848
|
+
if (channels[ck]) channels[ck].enabled = el.checked;
|
|
12849
|
+
}
|
|
12850
|
+
const chPolicyEls = $$('.oc-ch-policy');
|
|
12851
|
+
for (const el of chPolicyEls) {
|
|
12852
|
+
const ck = el.dataset.channel;
|
|
12853
|
+
if (channels[ck]) channels[ck].groupPolicy = el.value;
|
|
12854
|
+
}
|
|
12855
|
+
// Collect account data
|
|
12856
|
+
const tokenEls = $$('.oc-acc-token');
|
|
12857
|
+
for (const el of tokenEls) {
|
|
12858
|
+
const ck = el.dataset.channel;
|
|
12859
|
+
const ak = el.dataset.account;
|
|
12860
|
+
if (channels[ck]?.accounts?.[ak]) channels[ck].accounts[ak].botToken = el.value;
|
|
12861
|
+
}
|
|
12862
|
+
const dmPolicyEls = $$('.oc-acc-dm-policy');
|
|
12863
|
+
for (const el of dmPolicyEls) {
|
|
12864
|
+
const ck = el.dataset.channel;
|
|
12865
|
+
const ak = el.dataset.account;
|
|
12866
|
+
if (channels[ck]?.accounts?.[ak]) channels[ck].accounts[ak].dmPolicy = el.value;
|
|
12867
|
+
}
|
|
12868
|
+
const allowFromEls = $$('.oc-allow-from');
|
|
12869
|
+
for (const el of allowFromEls) {
|
|
12870
|
+
const ck = el.dataset.channel;
|
|
12871
|
+
const ak = el.dataset.account;
|
|
12872
|
+
if (channels[ck]?.accounts?.[ak]) {
|
|
12873
|
+
if (!channels[ck].accounts[ak].allowFrom) channels[ck].accounts[ak].allowFrom = [];
|
|
12874
|
+
channels[ck].accounts[ak].allowFrom.push(el.value.trim());
|
|
12875
|
+
}
|
|
12876
|
+
}
|
|
12877
|
+
// Deduplicate allowFrom per account
|
|
12878
|
+
for (const ck of Object.keys(channels)) {
|
|
12879
|
+
const ch = channels[ck];
|
|
12880
|
+
if (!ch.accounts) continue;
|
|
12881
|
+
for (const ak of Object.keys(ch.accounts)) {
|
|
12882
|
+
const af = ch.accounts[ak].allowFrom;
|
|
12883
|
+
if (Array.isArray(af)) ch.accounts[ak].allowFrom = [...new Set(af)].filter(x => x);
|
|
12884
|
+
}
|
|
12885
|
+
}
|
|
12886
|
+
|
|
12887
|
+
const payload = {
|
|
12888
|
+
agents: { defaults: newDefaults, list: _ocConfig.agents?.list || [] },
|
|
12889
|
+
bindings: _ocConfig.bindings || [],
|
|
12890
|
+
channels,
|
|
12891
|
+
};
|
|
12892
|
+
|
|
12893
|
+
try {
|
|
12894
|
+
const r = await api('/openclaw-config', { method: 'PUT', body: JSON.stringify(payload) });
|
|
12895
|
+
if (r.success) {
|
|
12896
|
+
toast(t('openclaw_config_saved'), 'ok');
|
|
12897
|
+
_ocConfig = await api('/openclaw-config');
|
|
12898
|
+
renderOpenClawConfig(_ocConfig);
|
|
12899
|
+
} else {
|
|
12900
|
+
toast(r.message || t('failed_save'), 'err');
|
|
12901
|
+
}
|
|
12902
|
+
} catch (e) {
|
|
12903
|
+
toast(e.message || t('failed_save'), 'err');
|
|
11446
12904
|
}
|
|
11447
12905
|
}
|
|
11448
12906
|
|
|
@@ -11466,7 +12924,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
11466
12924
|
const dot = $('#header-dot');
|
|
11467
12925
|
if (dot) { dot.className = 'dot ' + (s.connected ? 'dot-ok' : 'dot-err'); }
|
|
11468
12926
|
const txt = $('#header-status');
|
|
11469
|
-
if (txt) txt.textContent = s.connected ? '
|
|
12927
|
+
if (txt) txt.textContent = s.connected ? t('connected') : t('disconnected');
|
|
11470
12928
|
// Auto-remove offline banner when server reconnects
|
|
11471
12929
|
if (s.connected) hideOfflineBanner();
|
|
11472
12930
|
}
|
|
@@ -11505,6 +12963,10 @@ var HTML = `<!DOCTYPE html>
|
|
|
11505
12963
|
<div class="nav-group-title">System</div>
|
|
11506
12964
|
<div class="nav-item" data-page="settings"><span class="nav-icon">\u2699\uFE0F</span><span class="nav-label">Settings</span></div>
|
|
11507
12965
|
</div>
|
|
12966
|
+
<div class="nav-group">
|
|
12967
|
+
<div class="nav-group-title">OpenClaw</div>
|
|
12968
|
+
<div class="nav-item" data-page="openclaw"><span class="nav-icon">\u2699\uFE0F</span><span class="nav-label">OpenClaw</span></div>
|
|
12969
|
+
</div>
|
|
11508
12970
|
</nav>
|
|
11509
12971
|
<div class="sidebar-footer" onclick="toggleSidebar()">
|
|
11510
12972
|
<span>\u25C0</span><span class="sidebar-footer-text">Collapse sidebar</span>
|
|
@@ -11547,6 +13009,9 @@ var HTML = `<!DOCTYPE html>
|
|
|
11547
13009
|
<div id="settings-content"></div>
|
|
11548
13010
|
</div>
|
|
11549
13011
|
|
|
13012
|
+
<!-- OpenClaw Config -->
|
|
13013
|
+
<div class="page" id="page-openclaw"><div id="openclaw-content"></div></div>
|
|
13014
|
+
|
|
11550
13015
|
</div>
|
|
11551
13016
|
</main>
|
|
11552
13017
|
</div>
|
|
@@ -11605,6 +13070,7 @@ var HTML = `<!DOCTYPE html>
|
|
|
11605
13070
|
<div class="input-prefix"><span class="prefix">\u{1F310}</span><input id="model-base-url" type="text" placeholder="https://..."></div>
|
|
11606
13071
|
<div class="hint">Custom endpoint URL. Only needed for proxies or self-hosted models.</div>
|
|
11607
13072
|
</div>
|
|
13073
|
+
<div id="model-multi-list" style="display:none;margin-top:12px"></div>
|
|
11608
13074
|
<div class="form-actions">
|
|
11609
13075
|
<button class="btn btn-default" onclick="hideModal('modal-model-config')">Cancel</button>
|
|
11610
13076
|
<button class="btn btn-primary" onclick="saveModelConfig()">\u{1F4BE} Save Configuration</button>
|
|
@@ -11612,6 +13078,39 @@ var HTML = `<!DOCTYPE html>
|
|
|
11612
13078
|
</div>
|
|
11613
13079
|
</div>
|
|
11614
13080
|
|
|
13081
|
+
<!-- Modal: Custom Provider -->
|
|
13082
|
+
<div class="modal-overlay hidden" id="modal-custom-provider" onclick="if(event.target===this)hideModal('modal-custom-provider')">
|
|
13083
|
+
<div class="modal" style="max-width:520px">
|
|
13084
|
+
<div class="modal-header"><h3>\u{1F9E9} <span id="custom-provider-title">Custom Provider / \u81EA\u5B9A\u4E49\u63D0\u4F9B\u5546</span></h3><button class="modal-close" onclick="hideModal('modal-custom-provider')">\u2715</button></div>
|
|
13085
|
+
<p style="font-size:12px;color:var(--text3);margin-bottom:16px">Manually add custom model providers compatible with OpenAI API format. / \u624B\u52A8\u6DFB\u52A0\u517C\u5BB9 OpenAI API \u683C\u5F0F\u7684\u81EA\u5B9A\u4E49\u6A21\u578B\u63D0\u4F9B\u5546</p>
|
|
13086
|
+
<div class="form-group">
|
|
13087
|
+
<label>\u{1F4E6} Provider Name / \u63D0\u4F9B\u5546\u540D\u79F0 *</label>
|
|
13088
|
+
<input id="custom-name" type="text" placeholder="e.g., My Custom API / \u4F8B\u5982\uFF1AMy Custom API">
|
|
13089
|
+
</div>
|
|
13090
|
+
<div class="form-group">
|
|
13091
|
+
<label>\u{1F511} API Key</label>
|
|
13092
|
+
<input id="custom-api-key" type="password" placeholder="sk-...">
|
|
13093
|
+
<div class="hint">Leave blank to keep the existing key when editing.</div>
|
|
13094
|
+
</div>
|
|
13095
|
+
<div class="form-group">
|
|
13096
|
+
<label>\u{1F916} Model ID</label>
|
|
13097
|
+
<input id="custom-model-id" type="text" placeholder="gpt-4o">
|
|
13098
|
+
</div>
|
|
13099
|
+
<div class="form-group">
|
|
13100
|
+
<label>\u{1F310} Base URL</label>
|
|
13101
|
+
<input id="custom-base-url" type="text" placeholder="https://api.example.com/v1">
|
|
13102
|
+
</div>
|
|
13103
|
+
<div class="form-group">
|
|
13104
|
+
<label>\u{1F4DD} Description / \u63CF\u8FF0</label>
|
|
13105
|
+
<textarea id="custom-description" rows="2" placeholder="Optional: describe what this provider is for / \u53EF\u9009\uFF1A\u63CF\u8FF0\u6B64\u63D0\u4F9B\u5546\u7684\u7528\u9014"></textarea>
|
|
13106
|
+
</div>
|
|
13107
|
+
<div class="form-actions">
|
|
13108
|
+
<button class="btn btn-default" onclick="hideModal('modal-custom-provider')">Cancel / \u53D6\u6D88</button>
|
|
13109
|
+
<button class="btn btn-primary" onclick="saveCustomProvider()">\u{1F4BE} Save / \u4FDD\u5B58</button>
|
|
13110
|
+
</div>
|
|
13111
|
+
</div>
|
|
13112
|
+
</div>
|
|
13113
|
+
|
|
11615
13114
|
<!-- Modal: View Agent -->
|
|
11616
13115
|
<div class="modal-overlay hidden" id="modal-view-agent" onclick="if(event.target===this)hideModal('modal-view-agent')">
|
|
11617
13116
|
<div class="modal">
|
|
@@ -11627,7 +13126,7 @@ var HTML = `<!DOCTYPE html>
|
|
|
11627
13126
|
<div class="modal-header"><h3>\u{1F5D1}\uFE0F Reset Agent Identity</h3><button class="modal-close" onclick="hideModal('modal-reset-identity')">\u2715</button></div>
|
|
11628
13127
|
<div style="margin-bottom:16px">
|
|
11629
13128
|
<div class="card" style="border-color:var(--danger);background:var(--danger-bg)">
|
|
11630
|
-
<p style="font-size:13px;color:#
|
|
13129
|
+
<p style="font-size:13px;color:#991b1b;line-height:1.6">
|
|
11631
13130
|
<strong>\u26A0\uFE0F WARNING: This is a destructive operation!</strong><br><br>
|
|
11632
13131
|
This will permanently delete:<br>
|
|
11633
13132
|
\u2022 Your Ed25519 key pair and agent ID<br>
|
|
@@ -11754,29 +13253,59 @@ var MODEL_PROVIDERS = [
|
|
|
11754
13253
|
{ id: "openrouter", name: "OpenRouter", description: "Unified API for 200+ open-source and commercial models", apiKeyHint: "sk-or-...", modelHint: "openai/gpt-4o", baseUrlHint: "https://openrouter.ai/api/v1", configKey: "openrouter" },
|
|
11755
13254
|
{ id: "mistral", name: "Mistral AI", description: "Mistral Large, Medium, Small, Codestral", apiKeyHint: "(your key)", modelHint: "mistral-large-latest", baseUrlHint: "https://api.mistral.ai/v1", configKey: "mistral" },
|
|
11756
13255
|
{ id: "together", name: "Together AI", description: "Open-source models with fast inference", apiKeyHint: "...", modelHint: "meta-llama/Llama-3-70b-chat-hf", baseUrlHint: "https://api.together.xyz/v1", configKey: "together" },
|
|
11757
|
-
{ id: "fireworks", name: "Fireworks AI", description: "Fast open-source model serving", apiKeyHint: "...", modelHint: "accounts/fireworks/models/llama-v3-70b-instruct", baseUrlHint: "https://api.fireworks.ai/inference/v1", configKey: "fireworks" }
|
|
13256
|
+
{ id: "fireworks", name: "Fireworks AI", description: "Fast open-source model serving", apiKeyHint: "...", modelHint: "accounts/fireworks/models/llama-v3-70b-instruct", baseUrlHint: "https://api.fireworks.ai/inference/v1", configKey: "fireworks" },
|
|
13257
|
+
// Chinese / local providers
|
|
13258
|
+
{ id: "modelscope", name: "ModelScope (\u9B54\u642D)", description: "ZhipuAI GLM-5, Kimi-K2.5, MiniMax, Step models", apiKeyHint: "ms-...", modelHint: "Qwen/Qwen3-235B-A22B", baseUrlHint: "https://api-inference.modelscope.cn/v1", configKey: "modelscope" },
|
|
13259
|
+
{ id: "zhipu", name: "Zhipu AI (\u667A\u8C31)", description: "GLM-5, GLM-4, CogView, CogVideoX", apiKeyHint: "...", modelHint: "glm-5", baseUrlHint: "https://open.bigmodel.cn/api/paas/v4", configKey: "zhipu" },
|
|
13260
|
+
{ id: "qwen", name: "Alibaba Qwen (\u901A\u4E49\u5343\u95EE)", description: "Qwen3-235B, Qwen3-32B, Qwen-VL", apiKeyHint: "sk-...", modelHint: "qwen3-235b-a22b", baseUrlHint: "https://dashscope.aliyuncs.com/compatible-mode/v1", configKey: "qwen" },
|
|
13261
|
+
{ id: "doubao", name: "ByteDance Doubao (\u8C46\u5305)", description: "Doubao Pro, Doubao Lite \u2014 ByteDance LLMs", apiKeyHint: "...", modelHint: "doubao-pro-32k", baseUrlHint: "https://ark.cn-beijing.volces.com/api/v3", configKey: "doubao" },
|
|
13262
|
+
{ id: "moonshot", name: "Moonshot/Kimi (\u6708\u4E4B\u6697\u9762)", description: "Kimi-K2.5, Moonshot-v1 \u2014 strong Chinese reasoning", apiKeyHint: "sk-...", modelHint: "moonshot-v1-128k", baseUrlHint: "https://api.moonshot.cn/v1", configKey: "moonshot" },
|
|
13263
|
+
{ id: "minimax", name: "MiniMax", description: "MiniMax-M2.5, abab \u2014 multimodal AI", apiKeyHint: "...", modelHint: "MiniMax-M2.5", baseUrlHint: "https://api.minimax.chat/v1", configKey: "minimax" },
|
|
13264
|
+
{ id: "stepfun", name: "StepFun (\u9636\u8DC3\u661F\u8FB0)", description: "Step-3.5, Step-2 \u2014 reasoning models", apiKeyHint: "...", modelHint: "step-3.5-flash", baseUrlHint: "https://api.stepfun.com/v1", configKey: "stepfun" },
|
|
13265
|
+
{ id: "baidu", name: "Baidu Wenxin (\u767E\u5EA6\u6587\u5FC3)", description: "ERNIE 4.0, ERNIE 3.5 \u2014 Baidu LLMs", apiKeyHint: "...", modelHint: "ernie-4.0-8k", baseUrlHint: "https://aip.baidubce.com/rpc/2.0/ai_custom", configKey: "baidu" },
|
|
13266
|
+
{ id: "spark", name: "iFlytek Spark (\u8BAF\u98DE\u661F\u706B)", description: "Spark 4.0 Ultra, Spark 3.5 \u2014 iFlytek LLMs", apiKeyHint: "...", modelHint: "spark-4.0-ultra", baseUrlHint: "https://spark-api-open.xf-yun.com/v1", configKey: "spark" }
|
|
11758
13267
|
];
|
|
11759
13268
|
function findConfigPath() {
|
|
11760
|
-
|
|
11761
|
-
|
|
13269
|
+
function searchUpward(fileName) {
|
|
13270
|
+
let dir = process.cwd();
|
|
13271
|
+
for (let i = 0; i < 10; i++) {
|
|
13272
|
+
const candidate = path5.join(dir, fileName);
|
|
13273
|
+
try {
|
|
13274
|
+
if (fs5.existsSync(candidate))
|
|
13275
|
+
return candidate;
|
|
13276
|
+
} catch {
|
|
13277
|
+
}
|
|
13278
|
+
const parent = path5.dirname(dir);
|
|
13279
|
+
if (parent === dir)
|
|
13280
|
+
break;
|
|
13281
|
+
dir = parent;
|
|
13282
|
+
}
|
|
13283
|
+
return null;
|
|
13284
|
+
}
|
|
13285
|
+
const fromCwd = searchUpward("openclaw.json");
|
|
13286
|
+
if (fromCwd)
|
|
13287
|
+
return fromCwd;
|
|
13288
|
+
const openclawHomePaths = [
|
|
11762
13289
|
path5.join(os.homedir(), ".config", "openclaw", "openclaw.json"),
|
|
11763
13290
|
path5.join(os.homedir(), ".openclaw", "openclaw.json"),
|
|
11764
13291
|
path5.join(os.homedir(), "openclaw.json")
|
|
11765
13292
|
];
|
|
11766
|
-
for (const p of
|
|
13293
|
+
for (const p of openclawHomePaths) {
|
|
11767
13294
|
try {
|
|
11768
13295
|
if (fs5.existsSync(p))
|
|
11769
13296
|
return p;
|
|
11770
13297
|
} catch {
|
|
11771
13298
|
}
|
|
11772
13299
|
}
|
|
11773
|
-
const
|
|
11774
|
-
|
|
13300
|
+
const fromCwdStable = searchUpward("stableclaw.json");
|
|
13301
|
+
if (fromCwdStable)
|
|
13302
|
+
return fromCwdStable;
|
|
13303
|
+
const stableclawHomePaths = [
|
|
11775
13304
|
path5.join(os.homedir(), ".config", "stableclaw", "stableclaw.json"),
|
|
11776
13305
|
path5.join(os.homedir(), ".stableclaw", "stableclaw.json"),
|
|
11777
13306
|
path5.join(os.homedir(), "stableclaw.json")
|
|
11778
13307
|
];
|
|
11779
|
-
for (const p of
|
|
13308
|
+
for (const p of stableclawHomePaths) {
|
|
11780
13309
|
try {
|
|
11781
13310
|
if (fs5.existsSync(p))
|
|
11782
13311
|
return p;
|
|
@@ -11810,9 +13339,30 @@ function writeConfig(config2) {
|
|
|
11810
13339
|
}
|
|
11811
13340
|
function extractAgentsFromConfig(config2) {
|
|
11812
13341
|
const agents = [];
|
|
11813
|
-
const
|
|
11814
|
-
if (Array.isArray(
|
|
11815
|
-
|
|
13342
|
+
const agentsObj = config2.agents;
|
|
13343
|
+
if (typeof agentsObj === "object" && agentsObj !== null && !Array.isArray(agentsObj)) {
|
|
13344
|
+
const agentsRecord = agentsObj;
|
|
13345
|
+
if (typeof agentsRecord.defaults === "object" && agentsRecord.defaults !== null) {
|
|
13346
|
+
agents.push({
|
|
13347
|
+
_source: "agents-defaults",
|
|
13348
|
+
name: "(Defaults)",
|
|
13349
|
+
...agentsRecord.defaults
|
|
13350
|
+
});
|
|
13351
|
+
}
|
|
13352
|
+
const listArr = agentsRecord.list;
|
|
13353
|
+
if (Array.isArray(listArr)) {
|
|
13354
|
+
for (const a of listArr) {
|
|
13355
|
+
if (typeof a === "object" && a !== null) {
|
|
13356
|
+
agents.push({
|
|
13357
|
+
_source: "agents-list",
|
|
13358
|
+
...a
|
|
13359
|
+
});
|
|
13360
|
+
}
|
|
13361
|
+
}
|
|
13362
|
+
}
|
|
13363
|
+
}
|
|
13364
|
+
if (agents.length === 0 && Array.isArray(config2.agents)) {
|
|
13365
|
+
for (const a of config2.agents) {
|
|
11816
13366
|
if (typeof a === "object" && a !== null) {
|
|
11817
13367
|
agents.push(a);
|
|
11818
13368
|
}
|
|
@@ -11820,7 +13370,56 @@ function extractAgentsFromConfig(config2) {
|
|
|
11820
13370
|
}
|
|
11821
13371
|
const agentVal = config2.agent;
|
|
11822
13372
|
if (typeof agentVal === "object" && agentVal !== null && !Array.isArray(agentVal)) {
|
|
11823
|
-
|
|
13373
|
+
const agentRecord = agentVal;
|
|
13374
|
+
if (agentRecord.model) {
|
|
13375
|
+
const existingPrimary = agents.find((a) => a.default === true || a.isDefault === true);
|
|
13376
|
+
if (!existingPrimary) {
|
|
13377
|
+
agents.unshift({
|
|
13378
|
+
_source: "agent-primary",
|
|
13379
|
+
name: "Primary Agent",
|
|
13380
|
+
default: true,
|
|
13381
|
+
...agentRecord
|
|
13382
|
+
});
|
|
13383
|
+
}
|
|
13384
|
+
} else if (agents.length === 0) {
|
|
13385
|
+
agents.unshift({
|
|
13386
|
+
_source: "agent-single",
|
|
13387
|
+
...agentRecord
|
|
13388
|
+
});
|
|
13389
|
+
}
|
|
13390
|
+
}
|
|
13391
|
+
const modelsSection = config2.models;
|
|
13392
|
+
const defaultModel = modelsSection?.defaultModel || "";
|
|
13393
|
+
if (modelsSection && typeof modelsSection === "object") {
|
|
13394
|
+
const providersSection = modelsSection.providers;
|
|
13395
|
+
if (providersSection && typeof providersSection === "object") {
|
|
13396
|
+
for (const [providerId, providerConfig] of Object.entries(providersSection)) {
|
|
13397
|
+
if (typeof providerConfig !== "object" || providerConfig === null)
|
|
13398
|
+
continue;
|
|
13399
|
+
const pc = providerConfig;
|
|
13400
|
+
const modelsArr = pc.models;
|
|
13401
|
+
if (Array.isArray(modelsArr)) {
|
|
13402
|
+
for (let mi = 0; mi < modelsArr.length; mi++) {
|
|
13403
|
+
const m = modelsArr[mi];
|
|
13404
|
+
if (typeof m === "object" && m !== null) {
|
|
13405
|
+
const mObj = m;
|
|
13406
|
+
const agentEntry = {
|
|
13407
|
+
_source: "provider-model",
|
|
13408
|
+
_providerId: providerId,
|
|
13409
|
+
_modelIndex: mi,
|
|
13410
|
+
_configPath: "models.providers." + providerId + ".models." + mi,
|
|
13411
|
+
name: mObj.name || mObj.id || providerId + "/" + mi,
|
|
13412
|
+
model: mObj.id || "",
|
|
13413
|
+
provider: providerId,
|
|
13414
|
+
enabled: true,
|
|
13415
|
+
isDefault: defaultModel === mObj.id
|
|
13416
|
+
};
|
|
13417
|
+
agents.push(agentEntry);
|
|
13418
|
+
}
|
|
13419
|
+
}
|
|
13420
|
+
}
|
|
13421
|
+
}
|
|
13422
|
+
}
|
|
11824
13423
|
}
|
|
11825
13424
|
if (agents.length === 0) {
|
|
11826
13425
|
for (const [key, val] of Object.entries(config2)) {
|
|
@@ -11841,34 +13440,174 @@ function getModelProviders(config2) {
|
|
|
11841
13440
|
if (model?.providers)
|
|
11842
13441
|
providersSection = model.providers;
|
|
11843
13442
|
}
|
|
13443
|
+
if (!providersSection || typeof providersSection !== "object") {
|
|
13444
|
+
const models = config2.models;
|
|
13445
|
+
if (models?.providers)
|
|
13446
|
+
providersSection = models.providers;
|
|
13447
|
+
}
|
|
13448
|
+
const modelsSection = config2.models;
|
|
13449
|
+
let defaultModel = modelsSection?.defaultModel || "";
|
|
13450
|
+
const agentObj = config2.agent;
|
|
13451
|
+
const agentModel = agentObj?.model;
|
|
13452
|
+
if (typeof agentModel === "string" && !defaultModel) {
|
|
13453
|
+
defaultModel = agentModel;
|
|
13454
|
+
} else if (typeof agentModel === "object" && agentModel) {
|
|
13455
|
+
if (!defaultModel)
|
|
13456
|
+
defaultModel = agentModel.primary || "";
|
|
13457
|
+
}
|
|
13458
|
+
const authProfiles = {};
|
|
13459
|
+
const authObj = config2.auth;
|
|
13460
|
+
if (authObj && typeof authObj === "object") {
|
|
13461
|
+
const profiles = authObj.profiles;
|
|
13462
|
+
if (profiles && typeof profiles === "object") {
|
|
13463
|
+
for (const [profileKey, profileVal] of Object.entries(profiles)) {
|
|
13464
|
+
if (typeof profileVal !== "object" || profileVal === null)
|
|
13465
|
+
continue;
|
|
13466
|
+
const pv = profileVal;
|
|
13467
|
+
authProfiles[profileKey] = {
|
|
13468
|
+
provider: pv.provider || "",
|
|
13469
|
+
mode: pv.mode || ""
|
|
13470
|
+
};
|
|
13471
|
+
}
|
|
13472
|
+
}
|
|
13473
|
+
}
|
|
13474
|
+
const envVars = {};
|
|
13475
|
+
const envObj = config2.env;
|
|
13476
|
+
if (envObj && typeof envObj === "object") {
|
|
13477
|
+
const vars = envObj.vars;
|
|
13478
|
+
if (vars && typeof vars === "object") {
|
|
13479
|
+
for (const [key, val] of Object.entries(vars)) {
|
|
13480
|
+
if (typeof val === "string") {
|
|
13481
|
+
envVars[key] = val;
|
|
13482
|
+
}
|
|
13483
|
+
}
|
|
13484
|
+
}
|
|
13485
|
+
for (const [key, val] of Object.entries(envObj)) {
|
|
13486
|
+
if (key === "vars" || key === "shellEnv")
|
|
13487
|
+
continue;
|
|
13488
|
+
if (typeof val === "string" && (key.includes("API_KEY") || key.includes("_KEY") || key.toLowerCase().includes("apikey"))) {
|
|
13489
|
+
envVars[key] = val;
|
|
13490
|
+
}
|
|
13491
|
+
}
|
|
13492
|
+
}
|
|
11844
13493
|
const providers = MODEL_PROVIDERS.map((p) => {
|
|
11845
13494
|
const pc = providersSection?.[p.configKey] ?? config2[p.configKey];
|
|
11846
|
-
|
|
11847
|
-
|
|
13495
|
+
let apiKey = pc?.apiKey || "";
|
|
13496
|
+
if (!apiKey) {
|
|
13497
|
+
for (const [profileKey, profile] of Object.entries(authProfiles)) {
|
|
13498
|
+
if (profile.provider === p.configKey && profile.mode === "api_key") {
|
|
13499
|
+
apiKey = "__auth_profile__";
|
|
13500
|
+
}
|
|
13501
|
+
}
|
|
13502
|
+
}
|
|
13503
|
+
if (apiKey === "" || apiKey === "__auth_profile__") {
|
|
13504
|
+
for (const [envKey, envVal] of Object.entries(envVars)) {
|
|
13505
|
+
if (envKey.toUpperCase().includes(p.configKey.toUpperCase()) || p.configKey === "openai" && envKey.toUpperCase().includes("OPENAI") || p.configKey === "anthropic" && envKey.toUpperCase().includes("ANTHROPIC") || p.configKey === "google" && envKey.toUpperCase().includes("GOOGLE") || p.configKey === "groq" && envKey.toUpperCase().includes("GROQ") || p.configKey === "deepseek" && envKey.toUpperCase().includes("DEEPSEEK") || p.configKey === "openrouter" && envKey.toUpperCase().includes("OPENROUTER") || p.configKey === "zhipu" && envKey.toUpperCase().includes("ZHIPU") || p.configKey === "qwen" && envKey.toUpperCase().includes("QWEN") || p.configKey === "moonshot" && envKey.toUpperCase().includes("MOONSHOT") || p.configKey === "doubao" && envKey.toUpperCase().includes("DOUBAO") || p.configKey === "stepfun" && envKey.toUpperCase().includes("STEPFUN")) {
|
|
13506
|
+
apiKey = envVal;
|
|
13507
|
+
break;
|
|
13508
|
+
}
|
|
13509
|
+
}
|
|
13510
|
+
}
|
|
13511
|
+
let singleModelId = pc?.model || pc?.defaultModel || "";
|
|
13512
|
+
if (!singleModelId && defaultModel && defaultModel.startsWith(p.configKey + "/")) {
|
|
13513
|
+
singleModelId = defaultModel.split("/").slice(1).join("/");
|
|
13514
|
+
}
|
|
11848
13515
|
const baseUrl = pc?.baseUrl || pc?.baseURL || "";
|
|
13516
|
+
let modelsList = [];
|
|
13517
|
+
const modelsArr = pc?.models;
|
|
13518
|
+
if (Array.isArray(modelsArr)) {
|
|
13519
|
+
modelsList = modelsArr.filter((m) => typeof m === "object" && m !== null).map((m) => m);
|
|
13520
|
+
}
|
|
13521
|
+
let modelId = singleModelId;
|
|
13522
|
+
if (modelsList.length > 0 && !modelId) {
|
|
13523
|
+
modelId = defaultModel || modelsList[0]?.id || "";
|
|
13524
|
+
}
|
|
11849
13525
|
return {
|
|
11850
13526
|
...p,
|
|
11851
13527
|
configured: Boolean(apiKey || p.id === "ollama" && baseUrl),
|
|
11852
13528
|
apiKey: apiKey ? apiKey.substring(0, 6) + "\u2022\u2022\u2022\u2022\u2022\u2022" + apiKey.slice(-4) : "",
|
|
11853
13529
|
apiKeyHasValue: Boolean(apiKey),
|
|
11854
13530
|
modelId,
|
|
11855
|
-
baseUrl
|
|
13531
|
+
baseUrl,
|
|
13532
|
+
models: modelsList,
|
|
13533
|
+
modelCount: modelsList.length,
|
|
13534
|
+
isCustom: false
|
|
11856
13535
|
};
|
|
11857
13536
|
});
|
|
13537
|
+
const customProvidersSection = providersSection?.custom;
|
|
13538
|
+
const customProviders = [];
|
|
13539
|
+
if (Array.isArray(customProvidersSection)) {
|
|
13540
|
+
for (let i = 0; i < customProvidersSection.length; i++) {
|
|
13541
|
+
const cp = customProvidersSection[i];
|
|
13542
|
+
if (typeof cp !== "object" || cp === null)
|
|
13543
|
+
continue;
|
|
13544
|
+
const cpObj = cp;
|
|
13545
|
+
const cApiKey = cpObj.apiKey || "";
|
|
13546
|
+
const cModelId = cpObj.modelId || cpObj.model || "";
|
|
13547
|
+
const cBaseUrl = cpObj.baseUrl || cpObj.baseURL || "";
|
|
13548
|
+
const cId = cpObj.id || "custom-" + i;
|
|
13549
|
+
let cModelsList = [];
|
|
13550
|
+
const cModelsArr = cpObj.models;
|
|
13551
|
+
if (Array.isArray(cModelsArr)) {
|
|
13552
|
+
cModelsList = cModelsArr.filter((m) => typeof m === "object" && m !== null).map((m) => m);
|
|
13553
|
+
}
|
|
13554
|
+
providers.push({
|
|
13555
|
+
id: cId,
|
|
13556
|
+
name: cpObj.name || cId,
|
|
13557
|
+
description: cpObj.description || "Custom provider",
|
|
13558
|
+
configured: Boolean(cApiKey || cBaseUrl),
|
|
13559
|
+
apiKey: cApiKey ? cApiKey.substring(0, 6) + "\u2022\u2022\u2022\u2022\u2022\u2022" + cApiKey.slice(-4) : "",
|
|
13560
|
+
apiKeyHasValue: Boolean(cApiKey),
|
|
13561
|
+
apiKeyHint: "sk-...",
|
|
13562
|
+
modelId: cModelsList.length > 0 ? defaultModel || cModelsList[0]?.id || "" : cModelId,
|
|
13563
|
+
modelHint: cModelId || "model-name",
|
|
13564
|
+
baseUrl: cBaseUrl,
|
|
13565
|
+
baseUrlHint: cBaseUrl || "https://...",
|
|
13566
|
+
models: cModelsList,
|
|
13567
|
+
modelCount: cModelsList.length,
|
|
13568
|
+
isCustom: true
|
|
13569
|
+
});
|
|
13570
|
+
}
|
|
13571
|
+
}
|
|
11858
13572
|
const currentModels = [];
|
|
11859
|
-
for (const p of
|
|
11860
|
-
|
|
11861
|
-
|
|
13573
|
+
for (const p of providers) {
|
|
13574
|
+
if (!p.apiKeyHasValue)
|
|
13575
|
+
continue;
|
|
13576
|
+
const pId = p.id;
|
|
13577
|
+
const pName = p.name;
|
|
13578
|
+
const isCustom = p.isCustom;
|
|
13579
|
+
const pModels = p.models;
|
|
13580
|
+
const pBaseUrl = p.baseUrl;
|
|
13581
|
+
const pModelHint = p.modelHint || "";
|
|
13582
|
+
if (pModels && pModels.length > 0) {
|
|
13583
|
+
for (const m of pModels) {
|
|
13584
|
+
if (typeof m === "object" && m !== null) {
|
|
13585
|
+
const mObj = m;
|
|
13586
|
+
currentModels.push({
|
|
13587
|
+
provider: pName,
|
|
13588
|
+
providerId: pId,
|
|
13589
|
+
modelId: mObj.id || "",
|
|
13590
|
+
modelName: mObj.name || "",
|
|
13591
|
+
hasApiKey: true,
|
|
13592
|
+
baseUrl: pBaseUrl,
|
|
13593
|
+
isDefault: defaultModel === mObj.id,
|
|
13594
|
+
isCustom
|
|
13595
|
+
});
|
|
13596
|
+
}
|
|
13597
|
+
}
|
|
13598
|
+
} else {
|
|
11862
13599
|
currentModels.push({
|
|
11863
|
-
provider:
|
|
11864
|
-
providerId:
|
|
11865
|
-
modelId:
|
|
13600
|
+
provider: pName,
|
|
13601
|
+
providerId: pId,
|
|
13602
|
+
modelId: p.modelId || pModelHint,
|
|
11866
13603
|
hasApiKey: true,
|
|
11867
|
-
baseUrl:
|
|
13604
|
+
baseUrl: pBaseUrl,
|
|
13605
|
+
isDefault: false,
|
|
13606
|
+
isCustom
|
|
11868
13607
|
});
|
|
11869
13608
|
}
|
|
11870
13609
|
}
|
|
11871
|
-
return { providers, currentModels };
|
|
13610
|
+
return { providers, currentModels, defaultModel, customProviders: customProvidersSection || [] };
|
|
11872
13611
|
}
|
|
11873
13612
|
function parseApiPath(reqUrl) {
|
|
11874
13613
|
if (!reqUrl)
|
|
@@ -11987,14 +13726,33 @@ function createManagementHandler(ctx) {
|
|
|
11987
13726
|
});
|
|
11988
13727
|
}
|
|
11989
13728
|
if (apiPath.startsWith("/agents/") && method === "DELETE") {
|
|
11990
|
-
const
|
|
11991
|
-
const idx = parseInt(idxStr, 10);
|
|
11992
|
-
if (isNaN(idx) || idx < 0)
|
|
11993
|
-
return json(res, { success: false, message: "Invalid agent index" }, 400);
|
|
13729
|
+
const identifier = decodeURIComponent(apiPath.slice("/agents/".length));
|
|
11994
13730
|
const result = readConfig();
|
|
11995
13731
|
if (!result)
|
|
11996
13732
|
return json(res, { success: false, message: "No config file found" }, 400);
|
|
11997
13733
|
const config2 = result.config;
|
|
13734
|
+
const providerModelMatch = identifier.match(/^provider:([^:]+):(\d+)$/);
|
|
13735
|
+
if (providerModelMatch) {
|
|
13736
|
+
const providerId = providerModelMatch[1];
|
|
13737
|
+
const modelIdx = parseInt(providerModelMatch[2], 10);
|
|
13738
|
+
const modelsSection = config2.models;
|
|
13739
|
+
const providersObj = modelsSection?.providers;
|
|
13740
|
+
const provConfig = providersObj?.[providerId];
|
|
13741
|
+
const modelsArr = provConfig?.models;
|
|
13742
|
+
if (!modelsArr || modelIdx >= modelsArr.length) {
|
|
13743
|
+
return json(res, { success: false, message: "Provider model index out of range" }, 400);
|
|
13744
|
+
}
|
|
13745
|
+
modelsArr.splice(modelIdx, 1);
|
|
13746
|
+
const written2 = writeConfig(config2);
|
|
13747
|
+
if (written2) {
|
|
13748
|
+
logger.info("[API] Provider model deleted: " + providerId + ".models[" + modelIdx + "]");
|
|
13749
|
+
return json(res, { success: true, message: "Model removed from " + providerId });
|
|
13750
|
+
}
|
|
13751
|
+
return json(res, { success: false, message: "Failed to write config" }, 500);
|
|
13752
|
+
}
|
|
13753
|
+
const idx = parseInt(identifier, 10);
|
|
13754
|
+
if (isNaN(idx) || idx < 0)
|
|
13755
|
+
return json(res, { success: false, message: "Invalid agent identifier" }, 400);
|
|
11998
13756
|
let agentsArr;
|
|
11999
13757
|
if (Array.isArray(config2.agents)) {
|
|
12000
13758
|
agentsArr = config2.agents;
|
|
@@ -12020,14 +13778,17 @@ function createManagementHandler(ctx) {
|
|
|
12020
13778
|
}
|
|
12021
13779
|
if (apiPath === "/friends" && method === "GET") {
|
|
12022
13780
|
try {
|
|
12023
|
-
const resp = await fetch(serverUrl + "/api/v1/friends?nodeId=" + aicqAgentId
|
|
13781
|
+
const resp = await fetch(serverUrl + "/api/v1/friends?nodeId=" + aicqAgentId, {
|
|
13782
|
+
headers: serverClient.authHeaders()
|
|
13783
|
+
});
|
|
12024
13784
|
if (!resp.ok)
|
|
12025
13785
|
return json(res, { error: "Server error: " + await resp.text() }, 502);
|
|
12026
13786
|
const data = await resp.json();
|
|
12027
13787
|
const friends = (data.friends || []).map((f) => {
|
|
12028
|
-
const
|
|
13788
|
+
const friendId = f.id || f.nodeId;
|
|
13789
|
+
const local = store.getFriend(friendId);
|
|
12029
13790
|
return {
|
|
12030
|
-
id:
|
|
13791
|
+
id: friendId,
|
|
12031
13792
|
publicKeyFingerprint: f.publicKeyFingerprint || local?.publicKeyFingerprint || "",
|
|
12032
13793
|
permissions: f.permissions || local?.permissions || [],
|
|
12033
13794
|
addedAt: f.addedAt || local?.addedAt?.toISOString() || null,
|
|
@@ -12059,7 +13820,9 @@ function createManagementHandler(ctx) {
|
|
|
12059
13820
|
let friendId = target;
|
|
12060
13821
|
if (isTempNumber) {
|
|
12061
13822
|
try {
|
|
12062
|
-
const resolveResp = await fetch(serverUrl + "/api/v1/temp-number/" + target
|
|
13823
|
+
const resolveResp = await fetch(serverUrl + "/api/v1/temp-number/" + target, {
|
|
13824
|
+
headers: serverClient.authHeaders()
|
|
13825
|
+
});
|
|
12063
13826
|
if (!resolveResp.ok)
|
|
12064
13827
|
return json(res, { success: false, message: "Temp number not found or expired" });
|
|
12065
13828
|
const resolveData = await resolveResp.json();
|
|
@@ -12072,7 +13835,7 @@ function createManagementHandler(ctx) {
|
|
|
12072
13835
|
try {
|
|
12073
13836
|
const hsResp = await fetch(serverUrl + "/api/v1/handshake/initiate", {
|
|
12074
13837
|
method: "POST",
|
|
12075
|
-
headers:
|
|
13838
|
+
headers: serverClient.authHeaders(),
|
|
12076
13839
|
body: JSON.stringify({ requesterId: aicqAgentId, targetTempNumber: target })
|
|
12077
13840
|
});
|
|
12078
13841
|
if (!hsResp.ok)
|
|
@@ -12095,7 +13858,7 @@ function createManagementHandler(ctx) {
|
|
|
12095
13858
|
try {
|
|
12096
13859
|
const rmResp = await fetch(serverUrl + "/api/v1/friends/" + friendId, {
|
|
12097
13860
|
method: "DELETE",
|
|
12098
|
-
headers:
|
|
13861
|
+
headers: serverClient.authHeaders(),
|
|
12099
13862
|
body: JSON.stringify({ nodeId: aicqAgentId })
|
|
12100
13863
|
});
|
|
12101
13864
|
if (!rmResp.ok)
|
|
@@ -12121,7 +13884,7 @@ function createManagementHandler(ctx) {
|
|
|
12121
13884
|
try {
|
|
12122
13885
|
const resp = await fetch(serverUrl + "/api/v1/friends/" + friendId + "/permissions", {
|
|
12123
13886
|
method: "PUT",
|
|
12124
|
-
headers:
|
|
13887
|
+
headers: serverClient.authHeaders(),
|
|
12125
13888
|
body: JSON.stringify({ nodeId: aicqAgentId, permissions })
|
|
12126
13889
|
});
|
|
12127
13890
|
if (!resp.ok)
|
|
@@ -12140,7 +13903,9 @@ function createManagementHandler(ctx) {
|
|
|
12140
13903
|
}
|
|
12141
13904
|
if (apiPath === "/friends/requests" && method === "GET") {
|
|
12142
13905
|
try {
|
|
12143
|
-
const resp = await fetch(serverUrl + "/api/v1/friends/requests?
|
|
13906
|
+
const resp = await fetch(serverUrl + "/api/v1/friends/requests?accountId=" + aicqAgentId, {
|
|
13907
|
+
headers: serverClient.authHeaders()
|
|
13908
|
+
});
|
|
12144
13909
|
if (!resp.ok)
|
|
12145
13910
|
return json(res, { error: "Server error: " + await resp.text() }, 502);
|
|
12146
13911
|
const data = await resp.json();
|
|
@@ -12163,8 +13928,8 @@ function createManagementHandler(ctx) {
|
|
|
12163
13928
|
try {
|
|
12164
13929
|
const resp = await fetch(serverUrl + "/api/v1/friends/requests/" + requestId + "/accept", {
|
|
12165
13930
|
method: "POST",
|
|
12166
|
-
headers:
|
|
12167
|
-
body: JSON.stringify({ permissions: body.permissions || ["chat"] })
|
|
13931
|
+
headers: serverClient.authHeaders(),
|
|
13932
|
+
body: JSON.stringify({ accountId: aicqAgentId, permissions: body.permissions || ["chat"] })
|
|
12168
13933
|
});
|
|
12169
13934
|
if (!resp.ok)
|
|
12170
13935
|
return json(res, { success: false, message: "Failed: " + await resp.text() });
|
|
@@ -12182,8 +13947,8 @@ function createManagementHandler(ctx) {
|
|
|
12182
13947
|
try {
|
|
12183
13948
|
const resp = await fetch(serverUrl + "/api/v1/friends/requests/" + requestId + "/reject", {
|
|
12184
13949
|
method: "POST",
|
|
12185
|
-
headers:
|
|
12186
|
-
body: JSON.stringify({})
|
|
13950
|
+
headers: serverClient.authHeaders(),
|
|
13951
|
+
body: JSON.stringify({ accountId: aicqAgentId })
|
|
12187
13952
|
});
|
|
12188
13953
|
if (!resp.ok)
|
|
12189
13954
|
return json(res, { success: false, message: "Failed: " + await resp.text() });
|
|
@@ -12205,9 +13970,37 @@ function createManagementHandler(ctx) {
|
|
|
12205
13970
|
if (apiPath === "/models" && method === "GET") {
|
|
12206
13971
|
const result = readConfig();
|
|
12207
13972
|
if (!result)
|
|
12208
|
-
return json(res, { providers: MODEL_PROVIDERS, currentModels: [], error: "No config file found" });
|
|
13973
|
+
return json(res, { providers: MODEL_PROVIDERS, currentModels: [], defaultModel: "", error: "No config file found" });
|
|
12209
13974
|
return json(res, getModelProviders(result.config));
|
|
12210
13975
|
}
|
|
13976
|
+
if (apiPath === "/models/default" && method === "GET") {
|
|
13977
|
+
const result = readConfig();
|
|
13978
|
+
if (!result)
|
|
13979
|
+
return json(res, { defaultModel: "", error: "No config file found" });
|
|
13980
|
+
const modelsSection = result.config.models;
|
|
13981
|
+
const defaultModel = modelsSection?.defaultModel || "";
|
|
13982
|
+
let defaultProvider = "";
|
|
13983
|
+
let defaultModelName = "";
|
|
13984
|
+
if (defaultModel && modelsSection?.providers) {
|
|
13985
|
+
const providersObj = modelsSection.providers;
|
|
13986
|
+
for (const [provId, provConfig] of Object.entries(providersObj)) {
|
|
13987
|
+
const pc = provConfig;
|
|
13988
|
+
if (Array.isArray(pc.models)) {
|
|
13989
|
+
const found = pc.models.find((m) => m.id === defaultModel);
|
|
13990
|
+
if (found) {
|
|
13991
|
+
defaultProvider = provId;
|
|
13992
|
+
defaultModelName = found.name || found.id || "";
|
|
13993
|
+
break;
|
|
13994
|
+
}
|
|
13995
|
+
}
|
|
13996
|
+
}
|
|
13997
|
+
}
|
|
13998
|
+
return json(res, {
|
|
13999
|
+
defaultModel,
|
|
14000
|
+
defaultProvider,
|
|
14001
|
+
defaultModelName
|
|
14002
|
+
});
|
|
14003
|
+
}
|
|
12211
14004
|
if (apiPath.match(/^\/models\/[^/]+$/) && method === "PUT") {
|
|
12212
14005
|
const providerId = decodeURIComponent(apiPath.slice("/models/".length));
|
|
12213
14006
|
const body = await readBody(req);
|
|
@@ -12241,6 +14034,140 @@ function createManagementHandler(ctx) {
|
|
|
12241
14034
|
logger.info("[API] Model config saved for provider: " + providerId);
|
|
12242
14035
|
return json(res, { success: true, message: "Model configuration saved for " + provider.name });
|
|
12243
14036
|
}
|
|
14037
|
+
if (apiPath === "/models/custom" && method === "POST") {
|
|
14038
|
+
const body = await readBody(req);
|
|
14039
|
+
const name = body.name?.trim();
|
|
14040
|
+
const apiKey = body.apiKey;
|
|
14041
|
+
const modelId = body.modelId;
|
|
14042
|
+
const baseUrl = body.baseUrl;
|
|
14043
|
+
const description = body.description;
|
|
14044
|
+
if (!name)
|
|
14045
|
+
return json(res, { success: false, message: "Provider name is required" }, 400);
|
|
14046
|
+
if (!apiKey && !modelId && !baseUrl) {
|
|
14047
|
+
return json(res, { success: false, message: "At least one of API Key, Model ID, or Base URL is required" }, 400);
|
|
14048
|
+
}
|
|
14049
|
+
const result = readConfig();
|
|
14050
|
+
if (!result)
|
|
14051
|
+
return json(res, { success: false, message: "No config file found" }, 400);
|
|
14052
|
+
const config2 = result.config;
|
|
14053
|
+
if (!config2.providers || typeof config2.providers !== "object") {
|
|
14054
|
+
config2.providers = {};
|
|
14055
|
+
}
|
|
14056
|
+
const providers = config2.providers;
|
|
14057
|
+
if (!Array.isArray(providers.custom)) {
|
|
14058
|
+
providers.custom = [];
|
|
14059
|
+
}
|
|
14060
|
+
const customArr = providers.custom;
|
|
14061
|
+
const newId = "custom-" + crypto.randomUUID().replace(/-/g, "").substring(0, 8);
|
|
14062
|
+
const duplicate = customArr.find((c) => c.name?.toLowerCase() === name.toLowerCase());
|
|
14063
|
+
if (duplicate) {
|
|
14064
|
+
return json(res, { success: false, message: "A provider with this name already exists" }, 409);
|
|
14065
|
+
}
|
|
14066
|
+
const newProvider = {
|
|
14067
|
+
id: newId,
|
|
14068
|
+
name,
|
|
14069
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
14070
|
+
};
|
|
14071
|
+
if (apiKey)
|
|
14072
|
+
newProvider.apiKey = apiKey;
|
|
14073
|
+
if (modelId)
|
|
14074
|
+
newProvider.modelId = modelId;
|
|
14075
|
+
if (baseUrl)
|
|
14076
|
+
newProvider.baseUrl = baseUrl;
|
|
14077
|
+
if (description)
|
|
14078
|
+
newProvider.description = description;
|
|
14079
|
+
customArr.push(newProvider);
|
|
14080
|
+
const written = writeConfig(config2);
|
|
14081
|
+
if (!written)
|
|
14082
|
+
return json(res, { success: false, message: "Failed to write config file" }, 500);
|
|
14083
|
+
logger.info("[API] Custom provider added: " + name + " (" + newId + ")");
|
|
14084
|
+
return json(res, { success: true, message: "Custom provider '" + name + "' added", providerId: newId });
|
|
14085
|
+
}
|
|
14086
|
+
if (apiPath.match(/^\/models\/custom\/[^/]+$/) && method === "PUT") {
|
|
14087
|
+
const customId = decodeURIComponent(apiPath.slice("/models/custom/".length));
|
|
14088
|
+
const body = await readBody(req);
|
|
14089
|
+
const result = readConfig();
|
|
14090
|
+
if (!result)
|
|
14091
|
+
return json(res, { success: false, message: "No config file found" }, 400);
|
|
14092
|
+
const config2 = result.config;
|
|
14093
|
+
const providers = config2.providers;
|
|
14094
|
+
const customArr = providers?.custom;
|
|
14095
|
+
if (!Array.isArray(customArr)) {
|
|
14096
|
+
return json(res, { success: false, message: "No custom providers found" }, 404);
|
|
14097
|
+
}
|
|
14098
|
+
const idx = customArr.findIndex((c) => c.id === customId);
|
|
14099
|
+
if (idx === -1) {
|
|
14100
|
+
return json(res, { success: false, message: "Custom provider not found" }, 404);
|
|
14101
|
+
}
|
|
14102
|
+
const cp = customArr[idx];
|
|
14103
|
+
if (body.name)
|
|
14104
|
+
cp.name = body.name.trim();
|
|
14105
|
+
if (body.apiKey !== void 0)
|
|
14106
|
+
cp.apiKey = body.apiKey;
|
|
14107
|
+
if (body.modelId !== void 0)
|
|
14108
|
+
cp.modelId = body.modelId;
|
|
14109
|
+
if (body.baseUrl !== void 0)
|
|
14110
|
+
cp.baseUrl = body.baseUrl;
|
|
14111
|
+
if (body.description !== void 0)
|
|
14112
|
+
cp.description = body.description;
|
|
14113
|
+
const written = writeConfig(config2);
|
|
14114
|
+
if (!written)
|
|
14115
|
+
return json(res, { success: false, message: "Failed to write config file" }, 500);
|
|
14116
|
+
logger.info("[API] Custom provider updated: " + customId);
|
|
14117
|
+
return json(res, { success: true, message: "Custom provider updated" });
|
|
14118
|
+
}
|
|
14119
|
+
if (apiPath.match(/^\/models\/custom\/[^/]+$/) && method === "DELETE") {
|
|
14120
|
+
const customId = decodeURIComponent(apiPath.slice("/models/custom/".length));
|
|
14121
|
+
const result = readConfig();
|
|
14122
|
+
if (!result)
|
|
14123
|
+
return json(res, { success: false, message: "No config file found" }, 400);
|
|
14124
|
+
const config2 = result.config;
|
|
14125
|
+
const providers = config2.providers;
|
|
14126
|
+
const customArr = providers?.custom;
|
|
14127
|
+
if (!Array.isArray(customArr)) {
|
|
14128
|
+
return json(res, { success: false, message: "No custom providers found" }, 404);
|
|
14129
|
+
}
|
|
14130
|
+
const idx = customArr.findIndex((c) => c.id === customId);
|
|
14131
|
+
if (idx === -1) {
|
|
14132
|
+
return json(res, { success: false, message: "Custom provider not found" }, 404);
|
|
14133
|
+
}
|
|
14134
|
+
const removed = customArr.splice(idx, 1)[0];
|
|
14135
|
+
const written = writeConfig(config2);
|
|
14136
|
+
if (!written)
|
|
14137
|
+
return json(res, { success: false, message: "Failed to write config file" }, 500);
|
|
14138
|
+
logger.info("[API] Custom provider deleted: " + (removed.name || customId));
|
|
14139
|
+
return json(res, { success: true, message: "Custom provider '" + (removed.name || customId) + "' deleted" });
|
|
14140
|
+
}
|
|
14141
|
+
if (apiPath === "/openclaw-config" && method === "GET") {
|
|
14142
|
+
const result = readConfig();
|
|
14143
|
+
if (!result)
|
|
14144
|
+
return json(res, { error: "No config file found" }, 400);
|
|
14145
|
+
const config2 = result.config;
|
|
14146
|
+
return json(res, {
|
|
14147
|
+
agents: config2.agents || null,
|
|
14148
|
+
bindings: config2.bindings || [],
|
|
14149
|
+
channels: config2.channels || null,
|
|
14150
|
+
configPath: result.configPath
|
|
14151
|
+
});
|
|
14152
|
+
}
|
|
14153
|
+
if (apiPath === "/openclaw-config" && method === "PUT") {
|
|
14154
|
+
const body = await readBody(req);
|
|
14155
|
+
const result = readConfig();
|
|
14156
|
+
if (!result)
|
|
14157
|
+
return json(res, { success: false, message: "No config file found" }, 400);
|
|
14158
|
+
const config2 = result.config;
|
|
14159
|
+
if (body.agents !== void 0)
|
|
14160
|
+
config2.agents = body.agents;
|
|
14161
|
+
if (body.bindings !== void 0)
|
|
14162
|
+
config2.bindings = body.bindings;
|
|
14163
|
+
if (body.channels !== void 0)
|
|
14164
|
+
config2.channels = body.channels;
|
|
14165
|
+
const written = writeConfig(config2);
|
|
14166
|
+
if (!written)
|
|
14167
|
+
return json(res, { success: false, message: "Failed to write config" }, 500);
|
|
14168
|
+
logger.info("[API] OpenClaw config updated");
|
|
14169
|
+
return json(res, { success: true, message: "Configuration saved" });
|
|
14170
|
+
}
|
|
12244
14171
|
if (apiPath === "/settings" && method === "GET") {
|
|
12245
14172
|
const result = readConfig();
|
|
12246
14173
|
const aicqSection = result?.config?.aicq ?? {};
|
|
@@ -12592,10 +14519,7 @@ function createManagementHandler(ctx) {
|
|
|
12592
14519
|
return json(res, { success: true, message: "Agent added", index: config2.agents.length - 1 });
|
|
12593
14520
|
}
|
|
12594
14521
|
if (apiPath.startsWith("/agents/") && method === "PUT") {
|
|
12595
|
-
const
|
|
12596
|
-
const idx = parseInt(idxStr, 10);
|
|
12597
|
-
if (isNaN(idx) || idx < 0)
|
|
12598
|
-
return json(res, { success: false, message: "Invalid agent index" }, 400);
|
|
14522
|
+
const identifier = decodeURIComponent(apiPath.slice("/agents/".length));
|
|
12599
14523
|
const body = await readBody(req);
|
|
12600
14524
|
const updates = body.agent;
|
|
12601
14525
|
if (!updates || typeof updates !== "object") {
|
|
@@ -12605,11 +14529,42 @@ function createManagementHandler(ctx) {
|
|
|
12605
14529
|
if (!result)
|
|
12606
14530
|
return json(res, { success: false, message: "No config file found" }, 400);
|
|
12607
14531
|
const config2 = result.config;
|
|
12608
|
-
|
|
14532
|
+
const providerModelMatch = identifier.match(/^provider:([^:]+):(\d+)$/);
|
|
14533
|
+
if (providerModelMatch) {
|
|
14534
|
+
const providerId = providerModelMatch[1];
|
|
14535
|
+
const modelIdx = parseInt(providerModelMatch[2], 10);
|
|
14536
|
+
const modelsSection = config2.models;
|
|
14537
|
+
const providersObj = modelsSection?.providers;
|
|
14538
|
+
const provConfig = providersObj?.[providerId];
|
|
14539
|
+
const modelsArr = provConfig?.models;
|
|
14540
|
+
if (!modelsArr || modelIdx >= modelsArr.length) {
|
|
14541
|
+
return json(res, { success: false, message: "Provider model index out of range" }, 400);
|
|
14542
|
+
}
|
|
14543
|
+
const modelEntry = modelsArr[modelIdx];
|
|
14544
|
+
if (updates.name)
|
|
14545
|
+
modelEntry.name = updates.name;
|
|
14546
|
+
if (updates.id)
|
|
14547
|
+
modelEntry.id = updates.id;
|
|
14548
|
+
if (updates.provider)
|
|
14549
|
+
modelEntry.provider = updates.provider;
|
|
14550
|
+
for (const [k, v] of Object.entries(updates)) {
|
|
14551
|
+
if (!["name", "id", "provider", "_source", "_providerId", "_modelIndex", "_configPath", "isDefault"].includes(k)) {
|
|
14552
|
+
modelEntry[k] = v;
|
|
14553
|
+
}
|
|
14554
|
+
}
|
|
14555
|
+
const written2 = writeConfig(config2);
|
|
14556
|
+
if (!written2)
|
|
14557
|
+
return json(res, { success: false, message: "Failed to write config" }, 500);
|
|
14558
|
+
logger.info("[API] Provider model updated: " + providerId + ".models[" + modelIdx + "]");
|
|
14559
|
+
return json(res, { success: true, message: "Model updated in " + providerId });
|
|
14560
|
+
}
|
|
14561
|
+
const idx = parseInt(identifier, 10);
|
|
14562
|
+
if (isNaN(idx) || idx < 0)
|
|
14563
|
+
return json(res, { success: false, message: "Invalid agent identifier" }, 400);
|
|
12609
14564
|
if (!Array.isArray(config2.agents)) {
|
|
12610
14565
|
return json(res, { success: false, message: "No agents array in config" }, 400);
|
|
12611
14566
|
}
|
|
12612
|
-
agentsArr = config2.agents;
|
|
14567
|
+
const agentsArr = config2.agents;
|
|
12613
14568
|
if (idx >= agentsArr.length) {
|
|
12614
14569
|
return json(res, { success: false, message: "Agent index out of range" }, 400);
|
|
12615
14570
|
}
|
|
@@ -12832,6 +14787,9 @@ var plugin = definePluginEntry({
|
|
|
12832
14787
|
} catch (e) {
|
|
12833
14788
|
logger.warn("[Init] WS connect failed: " + (e instanceof Error ? e.message : e));
|
|
12834
14789
|
}
|
|
14790
|
+
serverClient.onConnectionStateChange((newState, prevState) => {
|
|
14791
|
+
logger.info("[Init] Connection state changed: " + prevState + " \u2192 " + newState);
|
|
14792
|
+
});
|
|
12835
14793
|
serverClient.onWsMessage("relay", (data) => {
|
|
12836
14794
|
const msg = data;
|
|
12837
14795
|
if (!msg?.payload)
|
|
@@ -12845,14 +14803,9 @@ var plugin = definePluginEntry({
|
|
|
12845
14803
|
}
|
|
12846
14804
|
});
|
|
12847
14805
|
setInterval(() => {
|
|
12848
|
-
|
|
12849
|
-
|
|
12850
|
-
serverClient.connectWebSocket();
|
|
12851
|
-
} catch (_e) {
|
|
12852
|
-
}
|
|
12853
|
-
}
|
|
14806
|
+
store.cleanupExpiredTempNumbers();
|
|
14807
|
+
store.cleanupExpiredOfflineMessages();
|
|
12854
14808
|
}, 6e4);
|
|
12855
|
-
setInterval(() => store.cleanupExpiredTempNumbers(), 6e4);
|
|
12856
14809
|
api.registerTool({
|
|
12857
14810
|
label: "AICQ Friend Manager",
|
|
12858
14811
|
name: "chat-friend",
|
|
@@ -12870,9 +14823,13 @@ var plugin = definePluginEntry({
|
|
|
12870
14823
|
try {
|
|
12871
14824
|
switch (action) {
|
|
12872
14825
|
case "request-temp-number": {
|
|
14826
|
+
const authHeaders = { "Content-Type": "application/json" };
|
|
14827
|
+
const token = serverClient.getAuthToken();
|
|
14828
|
+
if (token)
|
|
14829
|
+
authHeaders["Authorization"] = "Bearer " + token;
|
|
12873
14830
|
const resp = await fetch(serverUrl + "/api/v1/temp-number/request", {
|
|
12874
14831
|
method: "POST",
|
|
12875
|
-
headers:
|
|
14832
|
+
headers: authHeaders,
|
|
12876
14833
|
body: JSON.stringify({ nodeId: aicqAgentId })
|
|
12877
14834
|
});
|
|
12878
14835
|
if (!resp.ok)
|
|
@@ -12881,7 +14838,11 @@ var plugin = definePluginEntry({
|
|
|
12881
14838
|
return { success: true, tempNumber: data.number, message: "Temp number: " + data.number };
|
|
12882
14839
|
}
|
|
12883
14840
|
case "list": {
|
|
12884
|
-
const
|
|
14841
|
+
const listAuthHeaders = {};
|
|
14842
|
+
const listToken = serverClient.getAuthToken();
|
|
14843
|
+
if (listToken)
|
|
14844
|
+
listAuthHeaders["Authorization"] = "Bearer " + listToken;
|
|
14845
|
+
const resp = await fetch(serverUrl + "/api/v1/friends?nodeId=" + aicqAgentId, { headers: listAuthHeaders });
|
|
12885
14846
|
if (!resp.ok)
|
|
12886
14847
|
return { error: "Server error: " + await resp.text() };
|
|
12887
14848
|
const data = await resp.json();
|
|
@@ -12894,15 +14855,23 @@ var plugin = definePluginEntry({
|
|
|
12894
14855
|
const isTempNumber = /^\d{6}$/.test(target);
|
|
12895
14856
|
let friendId = target;
|
|
12896
14857
|
if (isTempNumber) {
|
|
12897
|
-
const
|
|
14858
|
+
const resolveAuthHeaders = {};
|
|
14859
|
+
const resolveToken = serverClient.getAuthToken();
|
|
14860
|
+
if (resolveToken)
|
|
14861
|
+
resolveAuthHeaders["Authorization"] = "Bearer " + resolveToken;
|
|
14862
|
+
const resolveResp = await fetch(serverUrl + "/api/v1/temp-number/" + target, { headers: resolveAuthHeaders });
|
|
12898
14863
|
if (!resolveResp.ok)
|
|
12899
14864
|
return { error: "Temp number not found or expired" };
|
|
12900
14865
|
const resolveData = await resolveResp.json();
|
|
12901
14866
|
friendId = resolveData.nodeId;
|
|
12902
14867
|
}
|
|
14868
|
+
const hsAuthHeaders = { "Content-Type": "application/json" };
|
|
14869
|
+
const hsToken = serverClient.getAuthToken();
|
|
14870
|
+
if (hsToken)
|
|
14871
|
+
hsAuthHeaders["Authorization"] = "Bearer " + hsToken;
|
|
12903
14872
|
const hsResp = await fetch(serverUrl + "/api/v1/handshake/initiate", {
|
|
12904
14873
|
method: "POST",
|
|
12905
|
-
headers:
|
|
14874
|
+
headers: hsAuthHeaders,
|
|
12906
14875
|
body: JSON.stringify({ requesterId: aicqAgentId, targetTempNumber: target })
|
|
12907
14876
|
});
|
|
12908
14877
|
if (!hsResp.ok)
|
|
@@ -12914,9 +14883,13 @@ var plugin = definePluginEntry({
|
|
|
12914
14883
|
const target = params?.target;
|
|
12915
14884
|
if (!target)
|
|
12916
14885
|
return { error: "Missing target (friend ID to remove)" };
|
|
14886
|
+
const rmAuthHeaders = { "Content-Type": "application/json" };
|
|
14887
|
+
const rmToken = serverClient.getAuthToken();
|
|
14888
|
+
if (rmToken)
|
|
14889
|
+
rmAuthHeaders["Authorization"] = "Bearer " + rmToken;
|
|
12917
14890
|
const rmResp = await fetch(serverUrl + "/api/v1/friends/" + target, {
|
|
12918
14891
|
method: "DELETE",
|
|
12919
|
-
headers:
|
|
14892
|
+
headers: rmAuthHeaders,
|
|
12920
14893
|
body: JSON.stringify({ nodeId: aicqAgentId })
|
|
12921
14894
|
});
|
|
12922
14895
|
if (!rmResp.ok)
|
|
@@ -12924,9 +14897,16 @@ var plugin = definePluginEntry({
|
|
|
12924
14897
|
return { success: true, message: "Friend " + target + " removed" };
|
|
12925
14898
|
}
|
|
12926
14899
|
case "revoke-temp-number": {
|
|
12927
|
-
const
|
|
14900
|
+
const revokeTarget = params?.target;
|
|
14901
|
+
if (!revokeTarget)
|
|
14902
|
+
return { error: "Missing target (temp number to revoke)" };
|
|
14903
|
+
const revokeAuthHeaders = { "Content-Type": "application/json" };
|
|
14904
|
+
const revokeToken = serverClient.getAuthToken();
|
|
14905
|
+
if (revokeToken)
|
|
14906
|
+
revokeAuthHeaders["Authorization"] = "Bearer " + revokeToken;
|
|
14907
|
+
const resp = await fetch(serverUrl + "/api/v1/temp-number/" + revokeTarget + "?nodeId=" + aicqAgentId, {
|
|
12928
14908
|
method: "DELETE",
|
|
12929
|
-
headers:
|
|
14909
|
+
headers: revokeAuthHeaders,
|
|
12930
14910
|
body: JSON.stringify({ nodeId: aicqAgentId })
|
|
12931
14911
|
});
|
|
12932
14912
|
if (!resp.ok)
|
|
@@ -12997,10 +14977,12 @@ var plugin = definePluginEntry({
|
|
|
12997
14977
|
try {
|
|
12998
14978
|
return {
|
|
12999
14979
|
connected: serverClient.isConnected(),
|
|
14980
|
+
connectionState: serverClient.getConnectionState(),
|
|
13000
14981
|
agentId: aicqAgentId,
|
|
13001
14982
|
fingerprint: identityService.getPublicKeyFingerprint(),
|
|
13002
14983
|
friendCount: store.getFriendCount(),
|
|
13003
14984
|
sessionCount: store.sessions.size,
|
|
14985
|
+
offlineMessageCount: store.getOfflineMessageCount(),
|
|
13004
14986
|
serverUrl
|
|
13005
14987
|
};
|
|
13006
14988
|
} catch (err) {
|
|
@@ -13010,21 +14992,25 @@ var plugin = definePluginEntry({
|
|
|
13010
14992
|
});
|
|
13011
14993
|
api.registerGatewayMethod("aicq.friends.list", async (params) => {
|
|
13012
14994
|
try {
|
|
13013
|
-
const resp = await fetch(serverUrl + "/api/v1/friends?nodeId=" + aicqAgentId
|
|
14995
|
+
const resp = await fetch(serverUrl + "/api/v1/friends?nodeId=" + aicqAgentId, {
|
|
14996
|
+
headers: serverClient.authHeaders()
|
|
14997
|
+
});
|
|
13014
14998
|
if (!resp.ok)
|
|
13015
14999
|
return { error: "Server error: " + await resp.text() };
|
|
13016
15000
|
const data = await resp.json();
|
|
13017
15001
|
const friends = data.friends || [];
|
|
13018
15002
|
const enriched = friends.map((f) => {
|
|
13019
|
-
const
|
|
15003
|
+
const friendId = f.id || f.nodeId;
|
|
15004
|
+
const localFriend = store.getFriend(friendId);
|
|
13020
15005
|
return {
|
|
13021
|
-
id:
|
|
15006
|
+
id: friendId,
|
|
13022
15007
|
publicKeyFingerprint: f.publicKeyFingerprint || (localFriend?.publicKeyFingerprint || ""),
|
|
13023
15008
|
permissions: f.permissions || localFriend?.permissions || [],
|
|
13024
15009
|
addedAt: f.addedAt || localFriend?.addedAt?.toISOString() || null,
|
|
13025
15010
|
lastMessageAt: f.lastMessageAt || localFriend?.lastMessageAt?.toISOString() || null,
|
|
13026
15011
|
friendType: f.friendType || localFriend?.friendType || null,
|
|
13027
|
-
aiName: f.aiName || localFriend?.aiName || null
|
|
15012
|
+
aiName: f.aiName || localFriend?.aiName || null,
|
|
15013
|
+
nodeId: f.nodeId || null
|
|
13028
15014
|
};
|
|
13029
15015
|
});
|
|
13030
15016
|
return { friends: enriched };
|
|
@@ -13042,7 +15028,9 @@ var plugin = definePluginEntry({
|
|
|
13042
15028
|
const isTempNumber = /^\d{6}$/.test(target);
|
|
13043
15029
|
let friendId = target;
|
|
13044
15030
|
if (isTempNumber) {
|
|
13045
|
-
const resolveResp = await fetch(serverUrl + "/api/v1/temp-number/" + target
|
|
15031
|
+
const resolveResp = await fetch(serverUrl + "/api/v1/temp-number/" + target, {
|
|
15032
|
+
headers: serverClient.authHeaders()
|
|
15033
|
+
});
|
|
13046
15034
|
if (!resolveResp.ok)
|
|
13047
15035
|
return { success: false, message: "Temp number not found or expired" };
|
|
13048
15036
|
const resolveData = await resolveResp.json();
|
|
@@ -13050,7 +15038,7 @@ var plugin = definePluginEntry({
|
|
|
13050
15038
|
}
|
|
13051
15039
|
const hsResp = await fetch(serverUrl + "/api/v1/handshake/initiate", {
|
|
13052
15040
|
method: "POST",
|
|
13053
|
-
headers:
|
|
15041
|
+
headers: serverClient.authHeaders(),
|
|
13054
15042
|
body: JSON.stringify({ requesterId: aicqAgentId, targetTempNumber: target })
|
|
13055
15043
|
});
|
|
13056
15044
|
if (!hsResp.ok)
|
|
@@ -13076,7 +15064,7 @@ var plugin = definePluginEntry({
|
|
|
13076
15064
|
return { success: false, message: "Missing friendId parameter" };
|
|
13077
15065
|
const rmResp = await fetch(serverUrl + "/api/v1/friends/" + friendId, {
|
|
13078
15066
|
method: "DELETE",
|
|
13079
|
-
headers:
|
|
15067
|
+
headers: serverClient.authHeaders(),
|
|
13080
15068
|
body: JSON.stringify({ nodeId: aicqAgentId })
|
|
13081
15069
|
});
|
|
13082
15070
|
if (!rmResp.ok)
|
|
@@ -13095,7 +15083,9 @@ var plugin = definePluginEntry({
|
|
|
13095
15083
|
const friendId = p.friendId;
|
|
13096
15084
|
if (!friendId)
|
|
13097
15085
|
return { error: "Missing friendId parameter" };
|
|
13098
|
-
const resp = await fetch(serverUrl + "/api/v1/friends/" + friendId + "/permissions?nodeId=" + aicqAgentId
|
|
15086
|
+
const resp = await fetch(serverUrl + "/api/v1/friends/" + friendId + "/permissions?nodeId=" + aicqAgentId, {
|
|
15087
|
+
headers: serverClient.authHeaders()
|
|
15088
|
+
});
|
|
13099
15089
|
if (!resp.ok)
|
|
13100
15090
|
return { error: "Server error: " + await resp.text() };
|
|
13101
15091
|
const data = await resp.json();
|
|
@@ -13117,7 +15107,7 @@ var plugin = definePluginEntry({
|
|
|
13117
15107
|
}
|
|
13118
15108
|
const resp = await fetch(serverUrl + "/api/v1/friends/" + friendId + "/permissions", {
|
|
13119
15109
|
method: "PUT",
|
|
13120
|
-
headers:
|
|
15110
|
+
headers: serverClient.authHeaders(),
|
|
13121
15111
|
body: JSON.stringify({ nodeId: aicqAgentId, permissions })
|
|
13122
15112
|
});
|
|
13123
15113
|
if (!resp.ok)
|
|
@@ -13136,7 +15126,9 @@ var plugin = definePluginEntry({
|
|
|
13136
15126
|
});
|
|
13137
15127
|
api.registerGatewayMethod("aicq.friends.requests", async (params) => {
|
|
13138
15128
|
try {
|
|
13139
|
-
const resp = await fetch(serverUrl + "/api/v1/friends/requests?
|
|
15129
|
+
const resp = await fetch(serverUrl + "/api/v1/friends/requests?accountId=" + aicqAgentId, {
|
|
15130
|
+
headers: serverClient.authHeaders()
|
|
15131
|
+
});
|
|
13140
15132
|
if (!resp.ok)
|
|
13141
15133
|
return { error: "Server error: " + await resp.text() };
|
|
13142
15134
|
const data = await resp.json();
|
|
@@ -13158,7 +15150,7 @@ var plugin = definePluginEntry({
|
|
|
13158
15150
|
}
|
|
13159
15151
|
const resp = await fetch(serverUrl + "/api/v1/friends/requests/" + requestId + "/accept", {
|
|
13160
15152
|
method: "POST",
|
|
13161
|
-
headers:
|
|
15153
|
+
headers: serverClient.authHeaders(),
|
|
13162
15154
|
body: JSON.stringify(body)
|
|
13163
15155
|
});
|
|
13164
15156
|
if (!resp.ok)
|
|
@@ -13178,7 +15170,7 @@ var plugin = definePluginEntry({
|
|
|
13178
15170
|
return { success: false, message: "Missing requestId parameter" };
|
|
13179
15171
|
const resp = await fetch(serverUrl + "/api/v1/friends/requests/" + requestId + "/reject", {
|
|
13180
15172
|
method: "POST",
|
|
13181
|
-
headers:
|
|
15173
|
+
headers: serverClient.authHeaders(),
|
|
13182
15174
|
body: JSON.stringify({})
|
|
13183
15175
|
});
|
|
13184
15176
|
if (!resp.ok)
|