nodebb-plugin-integration 0.4.2 → 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/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 +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/templates/admin/plugins/integration.tpl +0 -130
package/src/ai/indexer.js
CHANGED
|
@@ -1,383 +1,196 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
function createId(prefix, id) {
|
|
39
|
-
|
|
40
|
-
/*
|
|
41
|
-
* Qdrant supports UUID or integer.
|
|
42
|
-
* Create deterministic UUID from source id.
|
|
43
|
-
*/
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return crypto
|
|
47
|
-
|
|
48
|
-
.createHash("sha256")
|
|
49
|
-
|
|
50
|
-
.update(
|
|
51
|
-
|
|
52
|
-
`${prefix}:${id}`
|
|
53
|
-
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
.digest("hex")
|
|
57
|
-
|
|
58
|
-
.substring(0, 32);
|
|
59
|
-
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
|
|
5
|
+
const Config = require("../config");
|
|
6
|
+
const Posts = require("../nodebb/posts");
|
|
7
|
+
const Topics = require("../nodebb/topics");
|
|
8
|
+
const Embeddings = require("./embeddings");
|
|
9
|
+
const Qdrant = require("./qdrant");
|
|
10
|
+
|
|
11
|
+
function createId(pid) {
|
|
12
|
+
const hash = crypto
|
|
13
|
+
.createHash("sha256")
|
|
14
|
+
.update(`nodebb-post:${pid}`)
|
|
15
|
+
.digest();
|
|
16
|
+
|
|
17
|
+
hash[6] = (hash[6] & 0x0f) | 0x50;
|
|
18
|
+
hash[8] = (hash[8] & 0x3f) | 0x80;
|
|
19
|
+
|
|
20
|
+
const hex = hash.toString("hex");
|
|
21
|
+
|
|
22
|
+
return [
|
|
23
|
+
hex.slice(0, 8),
|
|
24
|
+
hex.slice(8, 12),
|
|
25
|
+
hex.slice(12, 16),
|
|
26
|
+
hex.slice(16, 20),
|
|
27
|
+
hex.slice(20, 32)
|
|
28
|
+
].join("-");
|
|
60
29
|
}
|
|
61
30
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
async function indexDocument(
|
|
75
|
-
|
|
76
|
-
type,
|
|
77
|
-
|
|
78
|
-
id,
|
|
79
|
-
|
|
80
|
-
text,
|
|
81
|
-
|
|
82
|
-
payload = {}
|
|
83
|
-
|
|
84
|
-
) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if(!text || !text.trim()) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return false;
|
|
93
|
-
|
|
94
|
-
|
|
31
|
+
async function isAiEnabled() {
|
|
32
|
+
const settings = await Config.get();
|
|
33
|
+
return settings.aiProvider === "openai";
|
|
95
34
|
}
|
|
96
35
|
|
|
36
|
+
async function removePost(pid) {
|
|
37
|
+
if (!(await isAiEnabled())) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
97
40
|
|
|
41
|
+
pid = Number(pid);
|
|
98
42
|
|
|
43
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
99
46
|
|
|
47
|
+
await Qdrant.deletePoints([createId(pid)]);
|
|
100
48
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const vector =
|
|
104
|
-
|
|
105
|
-
await Embeddings.create(
|
|
106
|
-
|
|
107
|
-
text
|
|
108
|
-
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if(!vector) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
console.log(
|
|
121
|
-
|
|
122
|
-
"[Indexer] embedding unavailable"
|
|
123
|
-
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return false;
|
|
128
|
-
|
|
129
|
-
|
|
49
|
+
return true;
|
|
130
50
|
}
|
|
131
51
|
|
|
52
|
+
async function indexPost(postOrPid) {
|
|
53
|
+
if (!(await isAiEnabled())) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
132
56
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
await Qdrant.ensureCollection();
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const pointId =
|
|
148
|
-
|
|
149
|
-
createId(
|
|
150
|
-
|
|
151
|
-
type,
|
|
152
|
-
|
|
153
|
-
id
|
|
154
|
-
|
|
57
|
+
const pid = Number(
|
|
58
|
+
typeof postOrPid === "object"
|
|
59
|
+
? postOrPid?.pid
|
|
60
|
+
: postOrPid
|
|
155
61
|
);
|
|
156
62
|
|
|
63
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) {
|
|
64
|
+
throw new TypeError("Invalid post ID");
|
|
65
|
+
}
|
|
157
66
|
|
|
67
|
+
const meta = await Posts.getPostMeta(pid);
|
|
158
68
|
|
|
69
|
+
if (!meta) {
|
|
70
|
+
await removePost(pid);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
159
73
|
|
|
74
|
+
const settings = await Config.get();
|
|
75
|
+
const cid = Number(meta.cid);
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
meta.deleted === true ||
|
|
79
|
+
Number(meta.deleted) === 1 ||
|
|
80
|
+
!Number.isSafeInteger(cid) ||
|
|
81
|
+
cid <= 0 ||
|
|
82
|
+
(
|
|
83
|
+
Array.isArray(settings.excludedCategories) &&
|
|
84
|
+
settings.excludedCategories.includes(cid)
|
|
85
|
+
)
|
|
86
|
+
) {
|
|
87
|
+
await removePost(pid);
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
160
90
|
|
|
91
|
+
const tid = Number(meta.tid);
|
|
92
|
+
const uid = Number(meta.uid);
|
|
161
93
|
|
|
94
|
+
if (
|
|
95
|
+
!Number.isSafeInteger(tid) || tid <= 0 ||
|
|
96
|
+
!Number.isSafeInteger(uid) || uid <= 0
|
|
97
|
+
) {
|
|
98
|
+
throw new Error("Invalid post metadata for post " + pid);
|
|
99
|
+
}
|
|
162
100
|
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
await Qdrant.upsert(
|
|
166
|
-
|
|
167
|
-
pointId,
|
|
168
|
-
|
|
169
|
-
vector,
|
|
170
|
-
|
|
171
|
-
{
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
type,
|
|
175
|
-
|
|
101
|
+
const topic = await Topics.getTopic(tid);
|
|
176
102
|
|
|
177
|
-
|
|
103
|
+
if (!topic || topic.deleted === true || Number(topic.deleted) === 1) {
|
|
104
|
+
await removePost(pid);
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
178
107
|
|
|
108
|
+
const content = await Posts.getPostContent(pid);
|
|
109
|
+
const post = { ...meta, ...content };
|
|
110
|
+
const text = Embeddings.preparePost(post);
|
|
179
111
|
|
|
180
|
-
|
|
112
|
+
if (!text) {
|
|
113
|
+
await removePost(pid);
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
181
116
|
|
|
117
|
+
const vector = await Embeddings.create(text);
|
|
182
118
|
|
|
183
|
-
|
|
119
|
+
if (
|
|
120
|
+
!Array.isArray(vector) ||
|
|
121
|
+
vector.length !== settings.embeddingSize ||
|
|
122
|
+
!vector.every(Number.isFinite)
|
|
123
|
+
) {
|
|
124
|
+
throw new Error("Invalid embedding returned for post " + pid);
|
|
125
|
+
}
|
|
184
126
|
|
|
127
|
+
await Qdrant.ensureCollection();
|
|
185
128
|
|
|
129
|
+
await Qdrant.upsert([
|
|
130
|
+
{
|
|
131
|
+
id: createId(pid),
|
|
132
|
+
vector,
|
|
133
|
+
payload: {
|
|
134
|
+
type: "post",
|
|
135
|
+
pid,
|
|
136
|
+
tid,
|
|
137
|
+
uid,
|
|
138
|
+
cid
|
|
139
|
+
}
|
|
186
140
|
}
|
|
141
|
+
]);
|
|
187
142
|
|
|
188
|
-
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if(result) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
console.log(
|
|
203
|
-
|
|
204
|
-
`[Indexer] indexed ${type}:${id}`
|
|
205
|
-
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return result;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
/*
|
|
237
|
-
|
|
238
|
-
Index post
|
|
239
|
-
*/
|
|
240
|
-
|
|
241
|
-
async function indexPost(post) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if(!post) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
return false;
|
|
250
|
-
|
|
251
|
-
|
|
143
|
+
return true;
|
|
252
144
|
}
|
|
253
145
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
return indexDocument(
|
|
261
|
-
|
|
262
|
-
"post",
|
|
263
|
-
|
|
264
|
-
post.pid,
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
post.content || post.text || "",
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
{
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
pid:
|
|
274
|
-
post.pid,
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
uid:
|
|
278
|
-
post.uid,
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
tid:
|
|
282
|
-
post.tid
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
146
|
+
async function indexPosts(pids = []) {
|
|
147
|
+
if (!Array.isArray(pids)) {
|
|
148
|
+
throw new TypeError("Post IDs must be an array");
|
|
286
149
|
}
|
|
287
150
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
/*
|
|
306
|
-
|
|
307
|
-
Index topic
|
|
308
|
-
*/
|
|
309
|
-
|
|
310
|
-
async function indexTopic(topic) {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
if(!topic) {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
return false;
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
return indexDocument(
|
|
330
|
-
|
|
331
|
-
"topic",
|
|
332
|
-
|
|
333
|
-
topic.tid,
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
topic.title || "",
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
{
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
tid:
|
|
343
|
-
topic.tid,
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
uid:
|
|
347
|
-
topic.uid
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
151
|
+
if (!(await isAiEnabled())) {
|
|
152
|
+
return pids.map(postId => ({
|
|
153
|
+
pid: Number(postId),
|
|
154
|
+
indexed: false
|
|
155
|
+
}));
|
|
351
156
|
}
|
|
352
157
|
|
|
158
|
+
const unique = [
|
|
159
|
+
...new Set(
|
|
160
|
+
pids
|
|
161
|
+
.map(Number)
|
|
162
|
+
.filter(pid => Number.isSafeInteger(pid) && pid > 0)
|
|
163
|
+
)
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
const results = [];
|
|
167
|
+
|
|
168
|
+
for (const pid of unique) {
|
|
169
|
+
try {
|
|
170
|
+
results.push({
|
|
171
|
+
pid,
|
|
172
|
+
indexed: await indexPost(pid)
|
|
173
|
+
});
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.error(
|
|
176
|
+
`[Integration] indexing post ${pid} failed:`,
|
|
177
|
+
error.message
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
results.push({
|
|
181
|
+
pid,
|
|
182
|
+
indexed: false,
|
|
183
|
+
error: error.message
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
353
187
|
|
|
354
|
-
|
|
355
|
-
|
|
188
|
+
return results;
|
|
356
189
|
}
|
|
357
190
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
191
|
module.exports = {
|
|
371
|
-
|
|
372
|
-
indexPost,
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
indexDocument
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
};
|
|
192
|
+
createId,
|
|
193
|
+
indexPost,
|
|
194
|
+
indexPosts,
|
|
195
|
+
removePost
|
|
196
|
+
};
|