nodebb-plugin-integration 0.4.1 → 0.4.4
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/README.md +199 -320
- package/README.ru.md +199 -347
- package/languages/en-GB/integration.json +15 -0
- package/languages/ru/integration.json +15 -0
- package/library.js +93 -115
- package/package.json +60 -53
- package/plugin.json +50 -33
- package/public/js/admin.js +58 -0
- package/public/templates/admin/plugins/integration.tpl +213 -0
- package/src/ai/embeddings.js +46 -253
- package/src/ai/indexer.js +156 -343
- package/src/ai/openai.js +74 -330
- package/src/ai/qdrant.js +114 -517
- package/src/ai/search.js +138 -251
- package/src/config/index.js +176 -133
- package/src/config/validation.js +250 -0
- package/src/events/index.js +174 -324
- package/src/mcp/manager.js +396 -0
- package/src/mcp/server.js +338 -172
- package/src/mcp/tools.js +158 -617
- package/src/nodebb/categories.js +39 -0
- package/src/nodebb/index.js +72 -305
- package/src/nodebb/meta.js +24 -273
- package/src/nodebb/posts.js +93 -348
- package/src/nodebb/privileges.js +68 -0
- package/src/nodebb/topics.js +91 -355
- package/src/nodebb/users.js +62 -345
- package/src/routes/api.js +255 -97
- package/src/routes/index.js +64 -49
package/src/config/index.js
CHANGED
|
@@ -1,133 +1,176 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
const Validation = require("./validation");
|
|
5
|
+
|
|
6
|
+
const DEFAULTS = Object.freeze({
|
|
7
|
+
aiProvider: "openai",
|
|
8
|
+
openaiApiKey: "",
|
|
9
|
+
openaiModel: "text-embedding-3-small",
|
|
10
|
+
embeddingSize: 1536,
|
|
11
|
+
qdrantUrl: "http://qdrant:6333",
|
|
12
|
+
collection: "nodebb_knowledge",
|
|
13
|
+
excludedCategories: [],
|
|
14
|
+
|
|
15
|
+
mcpEnabled: false,
|
|
16
|
+
mcpHost: "127.0.0.1",
|
|
17
|
+
mcpPort: 3100,
|
|
18
|
+
mcpNodebbUrl: "http://127.0.0.1:4567",
|
|
19
|
+
mcpBearerToken: "",
|
|
20
|
+
mcpServiceUid: 0,
|
|
21
|
+
|
|
22
|
+
internalApiToken: ""
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const SETTINGS_KEY = "nodebb-plugin-integration";
|
|
26
|
+
|
|
27
|
+
let nativeMeta = null;
|
|
28
|
+
|
|
29
|
+
function getMeta() {
|
|
30
|
+
if (nativeMeta) {
|
|
31
|
+
return nativeMeta;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
if (typeof nodebb !== "undefined" && nodebb &&
|
|
36
|
+
typeof nodebb.require === "function") {
|
|
37
|
+
nativeMeta = nodebb.require("./src/meta");
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
throw new Error(`Cannot load NodeBB meta module: ${error.message}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!nativeMeta || !nativeMeta.settings) {
|
|
44
|
+
throw new Error("NodeBB settings API is unavailable");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return nativeMeta;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function get() {
|
|
51
|
+
const meta = getMeta();
|
|
52
|
+
const saved = await meta.settings.get(SETTINGS_KEY);
|
|
53
|
+
const settings = Validation.normalize(saved || {}, DEFAULTS);
|
|
54
|
+
|
|
55
|
+
if (!settings.internalApiToken) {
|
|
56
|
+
settings.internalApiToken = crypto.randomBytes(48).toString("hex");
|
|
57
|
+
await meta.settings.set(SETTINGS_KEY, settings);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return settings;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function set(values = {}) {
|
|
64
|
+
if (!values || typeof values !== "object" || Array.isArray(values)) {
|
|
65
|
+
throw new TypeError("Settings must be an object");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const current = await get();
|
|
69
|
+
const next = { ...current };
|
|
70
|
+
|
|
71
|
+
const allowed = [
|
|
72
|
+
"aiProvider",
|
|
73
|
+
"openaiApiKey",
|
|
74
|
+
"openaiModel",
|
|
75
|
+
"embeddingSize",
|
|
76
|
+
"qdrantUrl",
|
|
77
|
+
"collection",
|
|
78
|
+
"excludedCategories",
|
|
79
|
+
"mcpEnabled",
|
|
80
|
+
"mcpHost",
|
|
81
|
+
"mcpPort",
|
|
82
|
+
"mcpNodebbUrl",
|
|
83
|
+
"mcpBearerToken",
|
|
84
|
+
"mcpServiceUid"
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
for (const key of allowed) {
|
|
88
|
+
if (Object.prototype.hasOwnProperty.call(values, key)) {
|
|
89
|
+
next[key] = values[key];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const key of ["openaiApiKey", "mcpBearerToken"]) {
|
|
94
|
+
if (
|
|
95
|
+
Object.prototype.hasOwnProperty.call(values, key) &&
|
|
96
|
+
(typeof values[key] !== "string" || !values[key].trim())
|
|
97
|
+
) {
|
|
98
|
+
next[key] = current[key];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (Object.prototype.hasOwnProperty.call(values, "aiProvider")) {
|
|
103
|
+
next.aiProvider = Validation.validateAiProvider(values.aiProvider);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (Object.prototype.hasOwnProperty.call(values, "qdrantUrl")) {
|
|
107
|
+
next.qdrantUrl = Validation.validateQdrantUrl(values.qdrantUrl);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (Object.prototype.hasOwnProperty.call(values, "mcpHost")) {
|
|
111
|
+
next.mcpHost = Validation.validateMcpHost(values.mcpHost);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (Object.prototype.hasOwnProperty.call(values, "mcpPort")) {
|
|
115
|
+
next.mcpPort = Validation.validatePort(values.mcpPort);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (Object.prototype.hasOwnProperty.call(values, "mcpNodebbUrl")) {
|
|
119
|
+
next.mcpNodebbUrl = Validation.validateNodebbUrl(values.mcpNodebbUrl);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (Object.prototype.hasOwnProperty.call(values, "mcpServiceUid")) {
|
|
123
|
+
next.mcpServiceUid = Validation.validateServiceUid(values.mcpServiceUid);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (Object.prototype.hasOwnProperty.call(values, "mcpBearerToken")) {
|
|
127
|
+
next.mcpBearerToken = Validation.validateBearerToken(next.mcpBearerToken);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const normalized = Validation.normalize(next, DEFAULTS);
|
|
131
|
+
|
|
132
|
+
if (normalized.mcpEnabled) {
|
|
133
|
+
if (!normalized.mcpBearerToken) {
|
|
134
|
+
throw new Error("Set MCP Bearer Token before enabling MCP");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!normalized.mcpServiceUid) {
|
|
138
|
+
throw new Error("Set MCP service UID before enabling MCP");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
await getMeta().settings.set(SETTINGS_KEY, normalized);
|
|
143
|
+
|
|
144
|
+
return normalized;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function info() {
|
|
148
|
+
const settings = await get();
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
aiProvider: settings.aiProvider,
|
|
152
|
+
aiProviderOpenai: settings.aiProvider === "openai",
|
|
153
|
+
aiProviderFake: settings.aiProvider === "fake",
|
|
154
|
+
openaiConfigured: Boolean(settings.openaiApiKey),
|
|
155
|
+
openaiModel: settings.openaiModel,
|
|
156
|
+
embeddingSize: settings.embeddingSize,
|
|
157
|
+
qdrantUrl: settings.qdrantUrl,
|
|
158
|
+
collection: settings.collection,
|
|
159
|
+
excludedCategories: settings.excludedCategories,
|
|
160
|
+
|
|
161
|
+
mcpEnabled: settings.mcpEnabled,
|
|
162
|
+
mcpHost: settings.mcpHost,
|
|
163
|
+
mcpPort: settings.mcpPort,
|
|
164
|
+
mcpNodebbUrl: settings.mcpNodebbUrl,
|
|
165
|
+
mcpBearerTokenConfigured: Boolean(settings.mcpBearerToken),
|
|
166
|
+
mcpServiceUid: settings.mcpServiceUid,
|
|
167
|
+
internalApiConfigured: Boolean(settings.internalApiToken)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = {
|
|
172
|
+
DEFAULTS,
|
|
173
|
+
get,
|
|
174
|
+
set,
|
|
175
|
+
info
|
|
176
|
+
};
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const net = require("net");
|
|
4
|
+
|
|
5
|
+
function normalizeBoolean(value) {
|
|
6
|
+
return value === true ||
|
|
7
|
+
value === "true" ||
|
|
8
|
+
value === "on" ||
|
|
9
|
+
value === 1;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function normalizeCategories(value) {
|
|
13
|
+
if (!Array.isArray(value)) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return [...new Set(
|
|
18
|
+
value
|
|
19
|
+
.map(Number)
|
|
20
|
+
.filter(item => Number.isSafeInteger(item) && item > 0)
|
|
21
|
+
)];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function validateHttpUrl(value, label) {
|
|
25
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
26
|
+
throw new Error(`${label} is required`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let parsed;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
parsed = new URL(value.trim());
|
|
33
|
+
} catch {
|
|
34
|
+
throw new Error(`Invalid ${label}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
!["http:", "https:"].includes(parsed.protocol) ||
|
|
39
|
+
!parsed.hostname ||
|
|
40
|
+
parsed.username ||
|
|
41
|
+
parsed.password ||
|
|
42
|
+
parsed.search ||
|
|
43
|
+
parsed.hash
|
|
44
|
+
) {
|
|
45
|
+
throw new Error(`${label} must be a valid HTTP or HTTPS URL`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function validateQdrantUrl(value) {
|
|
52
|
+
return validateHttpUrl(value, "Qdrant URL");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function validateNodebbUrl(value) {
|
|
56
|
+
return validateHttpUrl(value, "NodeBB API URL");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function validateAiProvider(value) {
|
|
60
|
+
if (value !== "openai" && value !== "fake") {
|
|
61
|
+
throw new Error("AI provider must be openai or fake");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function validateMcpHost(value) {
|
|
68
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
69
|
+
throw new Error("MCP bind address is required");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const host = value.trim();
|
|
73
|
+
|
|
74
|
+
if (host.length > 253 || /[\s/\\?#@]/.test(host)) {
|
|
75
|
+
throw new Error("Invalid MCP bind address");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (net.isIP(host)) {
|
|
79
|
+
return host;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const labels = host.split(".");
|
|
83
|
+
|
|
84
|
+
const validHostname = labels.every((label) => {
|
|
85
|
+
if (
|
|
86
|
+
label.length < 1 ||
|
|
87
|
+
label.length > 63 ||
|
|
88
|
+
!/[a-zA-Z0-9]/.test(label[0]) ||
|
|
89
|
+
!/[a-zA-Z0-9]/.test(label[label.length - 1])
|
|
90
|
+
) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return [...label].every((char) => (
|
|
95
|
+
(char >= "a" && char <= "z") ||
|
|
96
|
+
(char >= "A" && char <= "Z") ||
|
|
97
|
+
(char >= "0" && char <= "9") ||
|
|
98
|
+
char === "-"
|
|
99
|
+
));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!validHostname) {
|
|
103
|
+
throw new Error("Invalid MCP bind address");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return host;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function validatePort(value) {
|
|
110
|
+
const port = Number(value);
|
|
111
|
+
|
|
112
|
+
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) {
|
|
113
|
+
throw new Error("MCP port must be between 1 and 65535");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return port;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function validateServiceUid(value) {
|
|
120
|
+
const uid = Number(value);
|
|
121
|
+
|
|
122
|
+
if (!Number.isSafeInteger(uid) || uid < 1) {
|
|
123
|
+
throw new Error("MCP service UID must be a positive NodeBB user ID");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return uid;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function validateBearerToken(value) {
|
|
130
|
+
if (typeof value !== "string" || /[\r\n]/.test(value)) {
|
|
131
|
+
throw new Error("Invalid MCP Bearer Token");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const token = value.trim();
|
|
135
|
+
|
|
136
|
+
if (/\s/.test(token)) {
|
|
137
|
+
throw new Error("MCP Bearer Token must not contain whitespace");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (token && token.length < 32) {
|
|
141
|
+
throw new Error("MCP Bearer Token must contain at least 32 characters");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (token.length > 512) {
|
|
145
|
+
throw new Error("Invalid MCP Bearer Token");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return token;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function normalize(values = {}, defaults) {
|
|
152
|
+
const result = {
|
|
153
|
+
...defaults,
|
|
154
|
+
...values
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
result.aiProvider = validateAiProvider(result.aiProvider);
|
|
159
|
+
} catch {
|
|
160
|
+
result.aiProvider = defaults.aiProvider;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
result.openaiApiKey =
|
|
164
|
+
typeof result.openaiApiKey === "string"
|
|
165
|
+
? result.openaiApiKey.trim()
|
|
166
|
+
: "";
|
|
167
|
+
|
|
168
|
+
result.openaiModel =
|
|
169
|
+
typeof result.openaiModel === "string" &&
|
|
170
|
+
result.openaiModel.trim()
|
|
171
|
+
? result.openaiModel.trim()
|
|
172
|
+
: defaults.openaiModel;
|
|
173
|
+
|
|
174
|
+
result.embeddingSize = Number(result.embeddingSize);
|
|
175
|
+
|
|
176
|
+
if (
|
|
177
|
+
!Number.isSafeInteger(result.embeddingSize) ||
|
|
178
|
+
result.embeddingSize < 1
|
|
179
|
+
) {
|
|
180
|
+
result.embeddingSize = defaults.embeddingSize;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
result.qdrantUrl = validateQdrantUrl(result.qdrantUrl);
|
|
185
|
+
} catch {
|
|
186
|
+
result.qdrantUrl = defaults.qdrantUrl;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
result.collection =
|
|
190
|
+
typeof result.collection === "string" &&
|
|
191
|
+
/^[a-zA-Z0-9_-]+$/.test(result.collection.trim())
|
|
192
|
+
? result.collection.trim()
|
|
193
|
+
: defaults.collection;
|
|
194
|
+
|
|
195
|
+
result.excludedCategories =
|
|
196
|
+
normalizeCategories(result.excludedCategories);
|
|
197
|
+
|
|
198
|
+
result.mcpEnabled = normalizeBoolean(result.mcpEnabled);
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
result.mcpHost = validateMcpHost(result.mcpHost);
|
|
202
|
+
} catch {
|
|
203
|
+
result.mcpHost = defaults.mcpHost;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
result.mcpPort = validatePort(result.mcpPort);
|
|
208
|
+
} catch {
|
|
209
|
+
result.mcpPort = defaults.mcpPort;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
result.mcpNodebbUrl = validateNodebbUrl(result.mcpNodebbUrl);
|
|
214
|
+
} catch {
|
|
215
|
+
result.mcpNodebbUrl = defaults.mcpNodebbUrl;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
result.mcpServiceUid = validateServiceUid(result.mcpServiceUid);
|
|
220
|
+
} catch {
|
|
221
|
+
result.mcpServiceUid = 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
result.mcpBearerToken = validateBearerToken(result.mcpBearerToken);
|
|
226
|
+
} catch {
|
|
227
|
+
result.mcpBearerToken = "";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
result.internalApiToken =
|
|
231
|
+
typeof result.internalApiToken === "string"
|
|
232
|
+
? result.internalApiToken
|
|
233
|
+
: "";
|
|
234
|
+
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = {
|
|
239
|
+
normalizeBoolean,
|
|
240
|
+
normalizeCategories,
|
|
241
|
+
validateHttpUrl,
|
|
242
|
+
validateQdrantUrl,
|
|
243
|
+
validateNodebbUrl,
|
|
244
|
+
validateAiProvider,
|
|
245
|
+
validateMcpHost,
|
|
246
|
+
validatePort,
|
|
247
|
+
validateServiceUid,
|
|
248
|
+
validateBearerToken,
|
|
249
|
+
normalize
|
|
250
|
+
};
|