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/search.js
CHANGED
|
@@ -1,283 +1,170 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
require("./embeddings");
|
|
19
|
-
|
|
20
|
-
const Qdrant =
|
|
21
|
-
require("./qdrant");
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
async function search(
|
|
36
|
-
|
|
37
|
-
query,
|
|
38
|
-
|
|
39
|
-
limit = 5
|
|
40
|
-
|
|
41
|
-
) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if(!query || !query.trim()) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return [];
|
|
50
|
-
|
|
51
|
-
|
|
3
|
+
const Config = require("../config");
|
|
4
|
+
const Embeddings = require("./embeddings");
|
|
5
|
+
const Qdrant = require("./qdrant");
|
|
6
|
+
const Posts = require("../nodebb/posts");
|
|
7
|
+
const Privileges = require("../nodebb/privileges");
|
|
8
|
+
|
|
9
|
+
const DEFAULT_LIMIT = 5;
|
|
10
|
+
const MAX_LIMIT = 20;
|
|
11
|
+
const OVERFETCH_FACTOR = 5;
|
|
12
|
+
|
|
13
|
+
function createAiDisabledError() {
|
|
14
|
+
const error = new Error("AI provider is disabled. Select OpenAI to enable semantic search.");
|
|
15
|
+
error.code = "AI_DISABLED";
|
|
16
|
+
error.statusCode = 503;
|
|
17
|
+
return error;
|
|
52
18
|
}
|
|
53
19
|
|
|
20
|
+
function validateUid(uid) {
|
|
21
|
+
uid = Number(uid);
|
|
54
22
|
|
|
23
|
+
if (!Number.isSafeInteger(uid) || uid < 0) {
|
|
24
|
+
throw new TypeError("Invalid user ID");
|
|
25
|
+
}
|
|
55
26
|
|
|
27
|
+
return uid;
|
|
28
|
+
}
|
|
56
29
|
|
|
30
|
+
function normalizeLimit(limit) {
|
|
31
|
+
limit = Number(limit);
|
|
57
32
|
|
|
33
|
+
if (!Number.isSafeInteger(limit)) {
|
|
34
|
+
return DEFAULT_LIMIT;
|
|
35
|
+
}
|
|
58
36
|
|
|
37
|
+
return Math.max(1, Math.min(limit, MAX_LIMIT));
|
|
38
|
+
}
|
|
59
39
|
|
|
40
|
+
async function search(query, uid, limit = DEFAULT_LIMIT) {
|
|
41
|
+
const settings = await Config.get();
|
|
60
42
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const vector =
|
|
66
|
-
|
|
67
|
-
await Embeddings.create(
|
|
68
|
-
|
|
69
|
-
query
|
|
70
|
-
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if(!vector) {
|
|
81
|
-
|
|
82
|
-
|
|
43
|
+
if (settings.aiProvider !== "openai") {
|
|
44
|
+
throw createAiDisabledError();
|
|
45
|
+
}
|
|
83
46
|
|
|
47
|
+
if (typeof query !== "string" || !query.trim()) {
|
|
84
48
|
return [];
|
|
85
|
-
|
|
86
|
-
|
|
87
49
|
}
|
|
88
50
|
|
|
51
|
+
uid = validateUid(uid);
|
|
52
|
+
limit = normalizeLimit(limit);
|
|
89
53
|
|
|
54
|
+
const excludedCategories = Array.isArray(settings.excludedCategories)
|
|
55
|
+
? settings.excludedCategories
|
|
56
|
+
: [];
|
|
90
57
|
|
|
58
|
+
const vector = await Embeddings.create(query.trim());
|
|
91
59
|
|
|
60
|
+
if (
|
|
61
|
+
!Array.isArray(vector) ||
|
|
62
|
+
vector.length !== settings.embeddingSize ||
|
|
63
|
+
!vector.every(Number.isFinite)
|
|
64
|
+
) {
|
|
65
|
+
throw new Error("Invalid query embedding");
|
|
66
|
+
}
|
|
92
67
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const results =
|
|
97
|
-
|
|
98
|
-
await Qdrant.search(
|
|
99
|
-
|
|
100
|
-
vector,
|
|
101
|
-
|
|
102
|
-
limit
|
|
103
|
-
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return results.map(
|
|
114
|
-
|
|
115
|
-
item =>
|
|
116
|
-
|
|
117
|
-
({
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
id:
|
|
121
|
-
|
|
122
|
-
item.id,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
score:
|
|
127
|
-
|
|
128
|
-
item.score,
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
payload:
|
|
133
|
-
|
|
134
|
-
item.payload || {}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
})
|
|
139
|
-
|
|
68
|
+
const candidates = await Qdrant.search(
|
|
69
|
+
vector,
|
|
70
|
+
Math.min(limit * OVERFETCH_FACTOR, 100)
|
|
140
71
|
);
|
|
141
72
|
|
|
73
|
+
if (!Array.isArray(candidates) || !candidates.length) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
142
76
|
|
|
77
|
+
const allowed = [];
|
|
78
|
+
|
|
79
|
+
for (const candidate of candidates) {
|
|
80
|
+
const payload = candidate.payload || {};
|
|
81
|
+
const pid = Number(payload.pid);
|
|
82
|
+
const tid = Number(payload.tid);
|
|
83
|
+
const cid = Number(payload.cid);
|
|
84
|
+
|
|
85
|
+
if (
|
|
86
|
+
payload.type !== "post" ||
|
|
87
|
+
!Number.isSafeInteger(pid) || pid <= 0 ||
|
|
88
|
+
!Number.isSafeInteger(tid) || tid <= 0 ||
|
|
89
|
+
!Number.isSafeInteger(cid) || cid <= 0
|
|
90
|
+
) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (excludedCategories.includes(cid)) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const [postAllowed, topicAllowed] = await Promise.all([
|
|
99
|
+
Privileges.canReadPost(pid, uid),
|
|
100
|
+
Privileges.canReadTopic(tid, uid)
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
if (postAllowed && topicAllowed) {
|
|
104
|
+
allowed.push({
|
|
105
|
+
pid,
|
|
106
|
+
tid,
|
|
107
|
+
cid,
|
|
108
|
+
score: Number.isFinite(Number(candidate.score))
|
|
109
|
+
? Number(candidate.score)
|
|
110
|
+
: 0
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (allowed.length >= limit) {
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
143
118
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
catch(error) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
console.error(
|
|
152
|
-
|
|
153
|
-
"[Search] error",
|
|
154
|
-
|
|
155
|
-
error.message
|
|
119
|
+
if (!allowed.length) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
156
122
|
|
|
123
|
+
const posts = await Posts.getPosts(
|
|
124
|
+
allowed.map(item => item.pid),
|
|
125
|
+
uid
|
|
157
126
|
);
|
|
158
127
|
|
|
128
|
+
const postsById = new Map(
|
|
129
|
+
posts.map(post => [Number(post.pid), post])
|
|
130
|
+
);
|
|
159
131
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
132
|
+
return allowed
|
|
133
|
+
.map(item => {
|
|
134
|
+
const post = postsById.get(item.pid);
|
|
135
|
+
|
|
136
|
+
if (
|
|
137
|
+
!post ||
|
|
138
|
+
post.deleted === true ||
|
|
139
|
+
Number(post.deleted) === 1 ||
|
|
140
|
+
Number(post.pid) !== item.pid ||
|
|
141
|
+
Number(post.tid) !== item.tid ||
|
|
142
|
+
Number(post.cid) !== item.cid ||
|
|
143
|
+
!Number.isSafeInteger(Number(post.uid)) ||
|
|
144
|
+
Number(post.uid) <= 0 ||
|
|
145
|
+
excludedCategories.includes(Number(post.cid))
|
|
146
|
+
) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
pid: item.pid,
|
|
152
|
+
tid: item.tid,
|
|
153
|
+
uid: Number(post.uid),
|
|
154
|
+
cid: item.cid,
|
|
155
|
+
score: item.score,
|
|
156
|
+
content: post.content
|
|
157
|
+
};
|
|
158
|
+
})
|
|
159
|
+
.filter(Boolean)
|
|
160
|
+
.slice(0, limit);
|
|
169
161
|
}
|
|
170
162
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
async function searchText(
|
|
184
|
-
|
|
185
|
-
text,
|
|
186
|
-
|
|
187
|
-
limit = 5
|
|
188
|
-
|
|
189
|
-
) {
|
|
190
|
-
|
|
191
|
-
return search(
|
|
192
|
-
|
|
193
|
-
text,
|
|
194
|
-
|
|
195
|
-
limit
|
|
196
|
-
|
|
197
|
-
);
|
|
198
|
-
|
|
163
|
+
async function searchText(query, uid, limit = DEFAULT_LIMIT) {
|
|
164
|
+
return search(query, uid, limit);
|
|
199
165
|
}
|
|
200
166
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
function formatResults(results) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return results.map(
|
|
219
|
-
|
|
220
|
-
result =>
|
|
221
|
-
|
|
222
|
-
({
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
score:
|
|
226
|
-
|
|
227
|
-
result.score,
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
text:
|
|
232
|
-
|
|
233
|
-
result.payload.text || "",
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
type:
|
|
238
|
-
|
|
239
|
-
result.payload.type || null,
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
sourceId:
|
|
244
|
-
|
|
245
|
-
result.payload.sourceId || null
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
167
|
module.exports = {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
searchText,
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
formatResults
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
};
|
|
168
|
+
search,
|
|
169
|
+
searchText
|
|
170
|
+
};
|