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/src/ai/indexer.js CHANGED
@@ -1,383 +1,196 @@
1
1
  "use strict";
2
2
 
3
- /*
4
-
5
- NodeBB Integration Layer
6
-
7
-
8
- Knowledge Indexer
9
-
10
-
11
-
12
- */
13
-
14
-
15
-
16
-
17
- const Embeddings =
18
- require("./embeddings");
19
-
20
- const Qdrant =
21
- require("./qdrant");
22
-
23
- const crypto =
24
- require("crypto");
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 result =
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
- sourceId: id,
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
- text,
112
+ if (!text) {
113
+ await removePost(pid);
114
+ return false;
115
+ }
181
116
 
117
+ const vector = await Embeddings.create(text);
182
118
 
183
- ...payload
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
- indexTopic,
376
-
377
-
378
- indexDocument
379
-
380
-
381
-
382
-
383
- };
192
+ createId,
193
+ indexPost,
194
+ indexPosts,
195
+ removePost
196
+ };