nodebb-plugin-integration 0.4.2 → 0.5.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/README.md +199 -320
- package/README.ru.md +199 -347
- package/library.js +93 -115
- package/package.json +60 -57
- package/plugin.json +50 -33
- package/public/js/admin.js +58 -45
- 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 +347 -177
- 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/templates/admin/plugins/integration.tpl +0 -130
package/src/ai/qdrant.js
CHANGED
|
@@ -1,564 +1,161 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
NodeBB Integration Layer
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Qdrant Vector Database Adapter
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const {
|
|
18
|
-
|
|
19
|
-
QdrantClient
|
|
20
|
-
|
|
21
|
-
} = require("@qdrant/js-client-rest");
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const Config =
|
|
27
|
-
require("../config");
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
3
|
+
const { QdrantClient } = require("@qdrant/js-client-rest");
|
|
4
|
+
const Config = require("../config");
|
|
40
5
|
|
|
41
6
|
let client = null;
|
|
7
|
+
let clientUrl = null;
|
|
42
8
|
|
|
9
|
+
async function getClient() {
|
|
10
|
+
const settings = await Config.get();
|
|
43
11
|
|
|
12
|
+
if (settings.aiProvider !== "openai") {
|
|
13
|
+
const error = new Error(
|
|
14
|
+
"Qdrant is disabled because AI provider is set to fake"
|
|
15
|
+
);
|
|
16
|
+
error.code = "AI_DISABLED";
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
44
19
|
|
|
20
|
+
if (!settings.qdrantUrl) {
|
|
21
|
+
throw new Error("Qdrant URL is not configured");
|
|
22
|
+
}
|
|
45
23
|
|
|
24
|
+
if (client && clientUrl === settings.qdrantUrl) {
|
|
25
|
+
return client;
|
|
26
|
+
}
|
|
46
27
|
|
|
28
|
+
client = new QdrantClient({
|
|
29
|
+
url: settings.qdrantUrl,
|
|
30
|
+
timeout: 30000
|
|
31
|
+
});
|
|
47
32
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
function getClient() {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if(client) {
|
|
61
|
-
|
|
33
|
+
clientUrl = settings.qdrantUrl;
|
|
62
34
|
|
|
63
35
|
return client;
|
|
64
|
-
|
|
65
|
-
|
|
66
36
|
}
|
|
67
37
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
client = new QdrantClient({
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
url:
|
|
79
|
-
|
|
80
|
-
Config.qdrant.url
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return client;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
function isTestMode() {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return (
|
|
117
|
-
|
|
118
|
-
process.env.NODE_ENV === "test"
|
|
119
|
-
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
38
|
+
async function collectionExists(qdrant, collection) {
|
|
39
|
+
const result = await qdrant.collectionExists(collection);
|
|
40
|
+
return result.exists === true;
|
|
125
41
|
}
|
|
126
42
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
43
|
async function ensureCollection() {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
console.log(
|
|
149
|
-
|
|
150
|
-
"[Qdrant] test mode skip"
|
|
151
|
-
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return true;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const qdrant =
|
|
168
|
-
|
|
169
|
-
getClient();
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
try {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const collections =
|
|
183
|
-
|
|
184
|
-
await qdrant.getCollections();
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const exists =
|
|
194
|
-
|
|
195
|
-
collections.collections.some(
|
|
196
|
-
|
|
197
|
-
c =>
|
|
198
|
-
|
|
199
|
-
c.name === Config.qdrant.collection
|
|
200
|
-
|
|
201
|
-
);
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if(!exists) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
await qdrant.createCollection(
|
|
215
|
-
|
|
216
|
-
Config.qdrant.collection,
|
|
217
|
-
|
|
218
|
-
{
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
vectors:
|
|
222
|
-
|
|
223
|
-
{
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
size:
|
|
227
|
-
|
|
228
|
-
Config.embeddings.size,
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
distance:
|
|
232
|
-
|
|
233
|
-
"Cosine"
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
|
|
44
|
+
const settings = await Config.get();
|
|
45
|
+
const qdrant = await getClient();
|
|
46
|
+
|
|
47
|
+
if (!(await collectionExists(qdrant, settings.collection))) {
|
|
48
|
+
await qdrant.createCollection(settings.collection, {
|
|
49
|
+
vectors: {
|
|
50
|
+
size: settings.embeddingSize,
|
|
51
|
+
distance: "Cosine"
|
|
239
52
|
}
|
|
53
|
+
});
|
|
240
54
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
console.log(
|
|
251
|
-
|
|
252
|
-
"[Qdrant] collection created"
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
253
57
|
|
|
58
|
+
const info = await qdrant.getCollection(settings.collection);
|
|
59
|
+
const vectors = info.config?.params?.vectors;
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
!vectors ||
|
|
63
|
+
typeof vectors.size !== "number" ||
|
|
64
|
+
vectors.size !== settings.embeddingSize ||
|
|
65
|
+
vectors.distance !== "Cosine"
|
|
66
|
+
) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
"Qdrant collection has an incompatible vector configuration"
|
|
254
69
|
);
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
70
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return true;
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
71
|
}
|
|
272
72
|
|
|
73
|
+
async function upsert(points) {
|
|
74
|
+
if (!Array.isArray(points) || !points.length) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
273
77
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
console.error(
|
|
279
|
-
|
|
280
|
-
"[Qdrant] collection error",
|
|
281
|
-
|
|
282
|
-
error.message
|
|
283
|
-
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
return false;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
async function upsert(
|
|
312
|
-
|
|
313
|
-
id,
|
|
314
|
-
|
|
315
|
-
vector,
|
|
316
|
-
|
|
317
|
-
payload = {}
|
|
318
|
-
|
|
319
|
-
) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if(isTestMode()) {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
return true;
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const qdrant =
|
|
340
|
-
|
|
341
|
-
getClient();
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
try {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
await qdrant.upsert(
|
|
355
|
-
|
|
356
|
-
Config.qdrant.collection,
|
|
357
|
-
|
|
358
|
-
{
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
wait: true,
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
points:
|
|
365
|
-
|
|
366
|
-
[
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
{
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
id,
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
vector,
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
payload
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
]
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
);
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
return true;
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
catch(error) {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
console.error(
|
|
412
|
-
|
|
413
|
-
"[Qdrant] upsert error",
|
|
414
|
-
|
|
415
|
-
error.message
|
|
416
|
-
|
|
417
|
-
);
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
return false;
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
async function search(
|
|
445
|
-
|
|
446
|
-
vector,
|
|
447
|
-
|
|
448
|
-
limit = 5
|
|
449
|
-
|
|
450
|
-
) {
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
if(isTestMode()) {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
return [];
|
|
460
|
-
|
|
78
|
+
const settings = await Config.get();
|
|
79
|
+
const qdrant = await getClient();
|
|
461
80
|
|
|
81
|
+
await qdrant.upsert(settings.collection, {
|
|
82
|
+
wait: true,
|
|
83
|
+
points
|
|
84
|
+
});
|
|
462
85
|
}
|
|
463
86
|
|
|
87
|
+
async function search(vector, limit = 20) {
|
|
88
|
+
if (!Array.isArray(vector)) {
|
|
89
|
+
throw new TypeError("Search vector must be an array");
|
|
90
|
+
}
|
|
464
91
|
|
|
92
|
+
const settings = await Config.get();
|
|
465
93
|
|
|
94
|
+
if (vector.length !== settings.embeddingSize) {
|
|
95
|
+
throw new Error("Search vector dimension mismatch");
|
|
96
|
+
}
|
|
466
97
|
|
|
98
|
+
const qdrant = await getClient();
|
|
467
99
|
|
|
100
|
+
if (!(await collectionExists(qdrant, settings.collection))) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
468
103
|
|
|
104
|
+
const result = await qdrant.query(settings.collection, {
|
|
105
|
+
query: vector,
|
|
106
|
+
limit,
|
|
107
|
+
with_payload: true,
|
|
108
|
+
with_vector: false
|
|
109
|
+
});
|
|
469
110
|
|
|
470
|
-
|
|
471
|
-
const qdrant =
|
|
472
|
-
|
|
473
|
-
getClient();
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
try {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
return await qdrant.search(
|
|
487
|
-
|
|
488
|
-
Config.qdrant.collection,
|
|
489
|
-
|
|
490
|
-
{
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
vector,
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
limit,
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
with_payload: true
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
);
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
111
|
+
return Array.isArray(result.points) ? result.points : [];
|
|
509
112
|
}
|
|
510
113
|
|
|
114
|
+
async function deletePoints(ids) {
|
|
115
|
+
if (!Array.isArray(ids) || !ids.length) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
511
118
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
console.error(
|
|
517
|
-
|
|
518
|
-
"[Qdrant] search error",
|
|
519
|
-
|
|
520
|
-
error.message
|
|
521
|
-
|
|
522
|
-
);
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
return [];
|
|
119
|
+
const settings = await Config.get();
|
|
120
|
+
const qdrant = await getClient();
|
|
527
121
|
|
|
122
|
+
if (!(await collectionExists(qdrant, settings.collection))) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
528
125
|
|
|
126
|
+
await qdrant.delete(settings.collection, {
|
|
127
|
+
wait: true,
|
|
128
|
+
points: ids
|
|
129
|
+
});
|
|
529
130
|
}
|
|
530
131
|
|
|
132
|
+
async function deleteByFilter(filter) {
|
|
133
|
+
if (
|
|
134
|
+
!filter ||
|
|
135
|
+
typeof filter !== "object" ||
|
|
136
|
+
Array.isArray(filter)
|
|
137
|
+
) {
|
|
138
|
+
throw new TypeError("Qdrant filter is required");
|
|
139
|
+
}
|
|
531
140
|
|
|
141
|
+
const settings = await Config.get();
|
|
142
|
+
const qdrant = await getClient();
|
|
532
143
|
|
|
144
|
+
if (!(await collectionExists(qdrant, settings.collection))) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
533
147
|
|
|
148
|
+
await qdrant.delete(settings.collection, {
|
|
149
|
+
wait: true,
|
|
150
|
+
filter
|
|
151
|
+
});
|
|
534
152
|
}
|
|
535
153
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
154
|
module.exports = {
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
upsert,
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
search
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
};
|
|
155
|
+
getClient,
|
|
156
|
+
ensureCollection,
|
|
157
|
+
upsert,
|
|
158
|
+
search,
|
|
159
|
+
deletePoints,
|
|
160
|
+
deleteByFilter
|
|
161
|
+
};
|