nodebb-plugin-integration 0.3.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/LICENSE +22 -0
- package/README.md +320 -0
- package/README.ru.md +347 -0
- package/library.js +171 -0
- package/package.json +53 -0
- package/plugin.json +23 -0
- package/src/ai/embeddings.js +254 -0
- package/src/ai/indexer.js +383 -0
- package/src/ai/openai.js +331 -0
- package/src/ai/qdrant.js +564 -0
- package/src/ai/search.js +283 -0
- package/src/config/index.js +322 -0
- package/src/events/index.js +324 -0
- package/src/mcp/server.js +232 -0
- package/src/mcp/tools.js +654 -0
- package/src/nodebb/index.js +306 -0
- package/src/nodebb/meta.js +273 -0
- package/src/nodebb/posts.js +348 -0
- package/src/nodebb/topics.js +356 -0
- package/src/nodebb/users.js +346 -0
- package/src/routes/api.js +240 -0
- package/src/routes/index.js +110 -0
package/library.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* NodeBB Integration Layer
|
|
5
|
+
*
|
|
6
|
+
* Main plugin entry point
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
require("dotenv").config();
|
|
10
|
+
|
|
11
|
+
const Config = require("./src/config");
|
|
12
|
+
const Routes = require("./src/routes");
|
|
13
|
+
const Events = require("./src/events");
|
|
14
|
+
const Indexer = require("./src/ai/indexer");
|
|
15
|
+
const MCP = require("./src/mcp/server");
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
const Plugin = {};
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* NodeBB hook:
|
|
23
|
+
*
|
|
24
|
+
* static:app.load
|
|
25
|
+
*/
|
|
26
|
+
Plugin.init = async function ({ router } = {}) {
|
|
27
|
+
|
|
28
|
+
console.log(
|
|
29
|
+
"[NodeBB Integration] initializing"
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if (router) {
|
|
34
|
+
|
|
35
|
+
Routes.init(
|
|
36
|
+
router
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
} else {
|
|
40
|
+
|
|
41
|
+
console.log(
|
|
42
|
+
"[NodeBB Integration] router unavailable (standalone mode)"
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
Events &&
|
|
50
|
+
typeof Events.init === "function"
|
|
51
|
+
) {
|
|
52
|
+
|
|
53
|
+
await Events.init();
|
|
54
|
+
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
if (
|
|
59
|
+
Config?.mcp?.enabled &&
|
|
60
|
+
process.env.MCP_AUTOSTART === "true" &&
|
|
61
|
+
MCP &&
|
|
62
|
+
typeof MCP.start === "function"
|
|
63
|
+
) {
|
|
64
|
+
|
|
65
|
+
await MCP.start();
|
|
66
|
+
|
|
67
|
+
console.log(
|
|
68
|
+
"[NodeBB Integration] MCP started"
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
console.log(
|
|
75
|
+
"[NodeBB Integration] ready"
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* NodeBB hook:
|
|
83
|
+
*
|
|
84
|
+
* action:post.save
|
|
85
|
+
*/
|
|
86
|
+
Plugin.onPostSave = async function (post) {
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
|
|
90
|
+
if (
|
|
91
|
+
Events &&
|
|
92
|
+
typeof Events.postSave === "function"
|
|
93
|
+
) {
|
|
94
|
+
|
|
95
|
+
await Events.postSave(
|
|
96
|
+
post
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
Indexer &&
|
|
104
|
+
typeof Indexer.indexPost === "function"
|
|
105
|
+
) {
|
|
106
|
+
|
|
107
|
+
await Indexer.indexPost(
|
|
108
|
+
post
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
} catch (error) {
|
|
115
|
+
|
|
116
|
+
console.error(
|
|
117
|
+
"[Plugin] post save error",
|
|
118
|
+
error.message
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* NodeBB hook:
|
|
128
|
+
*
|
|
129
|
+
* action:topic.save
|
|
130
|
+
*/
|
|
131
|
+
Plugin.onTopicSave = async function (topic) {
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
|
|
135
|
+
if (
|
|
136
|
+
Events &&
|
|
137
|
+
typeof Events.topicCreate === "function"
|
|
138
|
+
) {
|
|
139
|
+
|
|
140
|
+
await Events.topicCreate(
|
|
141
|
+
topic
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
Indexer &&
|
|
149
|
+
typeof Indexer.indexTopic === "function"
|
|
150
|
+
) {
|
|
151
|
+
|
|
152
|
+
await Indexer.indexTopic(
|
|
153
|
+
topic
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
} catch (error) {
|
|
160
|
+
|
|
161
|
+
console.error(
|
|
162
|
+
"[Plugin] topic save error",
|
|
163
|
+
error.message
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
module.exports = Plugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nodebb-plugin-integration",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "AI integration layer for NodeBB forums with MCP, semantic search, RAG and Qdrant knowledge pipeline",
|
|
5
|
+
"main": "library.js",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/Sergey-ek/nodebb-plugin-integration.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"nodebb",
|
|
13
|
+
"nodebb-plugin",
|
|
14
|
+
"ai",
|
|
15
|
+
"mcp",
|
|
16
|
+
"model-context-protocol",
|
|
17
|
+
"rag",
|
|
18
|
+
"qdrant",
|
|
19
|
+
"embeddings",
|
|
20
|
+
"semantic-search",
|
|
21
|
+
"knowledge-base"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"library.js",
|
|
28
|
+
"plugin.json",
|
|
29
|
+
"src",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "cross-env NODE_ENV=test mocha test/*.test.js",
|
|
35
|
+
"check": "node --check library.js && node --check src/routes/index.js && node --check src/routes/api.js && node --check src/mcp/server.js && node --check src/mcp/tools.js",
|
|
36
|
+
"diagnose": "node -e \"console.log(require('./src/config').info())\"",
|
|
37
|
+
"mcp": "node src/mcp/server.js"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
41
|
+
"@qdrant/js-client-rest": "^1.12.0",
|
|
42
|
+
"dotenv": "^16.4.7",
|
|
43
|
+
"node-fetch": "^3.3.2",
|
|
44
|
+
"openai": "^6.45.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"cross-env": "^10.0.0",
|
|
48
|
+
"mocha": "^11.7.6"
|
|
49
|
+
},
|
|
50
|
+
"nbbpm": {
|
|
51
|
+
"compatibility": "^4.4.1"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/plugin.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "nodebb-plugin-integration",
|
|
3
|
+
"name": "NodeBB Integration Layer",
|
|
4
|
+
"description": "AI integration layer for NodeBB with MCP, semantic search, RAG and Qdrant knowledge pipeline",
|
|
5
|
+
"library": "./library.js",
|
|
6
|
+
"staticDirs": {
|
|
7
|
+
"public": "public"
|
|
8
|
+
},
|
|
9
|
+
"hooks": [
|
|
10
|
+
{
|
|
11
|
+
"hook": "static:app.load",
|
|
12
|
+
"method": "init"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"hook": "action:post.save",
|
|
16
|
+
"method": "onPostSave"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"hook": "action:topic.save",
|
|
20
|
+
"method": "onTopicSave"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* NodeBB Integration Layer
|
|
6
|
+
*
|
|
7
|
+
* Embeddings Service
|
|
8
|
+
*
|
|
9
|
+
* Unified interface for:
|
|
10
|
+
*
|
|
11
|
+
* - OpenAI embeddings
|
|
12
|
+
* - Fake test embeddings
|
|
13
|
+
*
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
const Config =
|
|
19
|
+
require("../config");
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
const OpenAI =
|
|
23
|
+
require("./openai");
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
/*
|
|
32
|
+
* Embedding size
|
|
33
|
+
*
|
|
34
|
+
* text-embedding-3-small = 1536
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
const SIZE =
|
|
39
|
+
|
|
40
|
+
Number(
|
|
41
|
+
|
|
42
|
+
process.env.EMBEDDING_SIZE ||
|
|
43
|
+
|
|
44
|
+
1536
|
|
45
|
+
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
/*
|
|
57
|
+
* Generate fake vector
|
|
58
|
+
*
|
|
59
|
+
* Used for:
|
|
60
|
+
*
|
|
61
|
+
* - tests
|
|
62
|
+
* - development
|
|
63
|
+
*
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
function fakeEmbedding() {
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
const vector = [];
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
for(let i = 0; i < SIZE; i++) {
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
vector.push(
|
|
78
|
+
|
|
79
|
+
Math.random()
|
|
80
|
+
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
return vector;
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
/*
|
|
102
|
+
* Select provider
|
|
103
|
+
*/
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
function provider() {
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
|
|
112
|
+
process.env.AI_PROVIDER ||
|
|
113
|
+
|
|
114
|
+
"fake"
|
|
115
|
+
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
/*
|
|
130
|
+
* Create embedding
|
|
131
|
+
*/
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
async function create(text) {
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
if(!text || !text.trim()) {
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
return null;
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
/*
|
|
154
|
+
* OpenAI mode
|
|
155
|
+
*/
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if(
|
|
159
|
+
|
|
160
|
+
provider() === "openai"
|
|
161
|
+
|
|
162
|
+
) {
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
const vector =
|
|
167
|
+
|
|
168
|
+
await OpenAI.embeddings(
|
|
169
|
+
|
|
170
|
+
text
|
|
171
|
+
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
return vector;
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
/*
|
|
191
|
+
* Development/Test mode
|
|
192
|
+
*/
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
return fakeEmbedding();
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
/*
|
|
210
|
+
* Service information
|
|
211
|
+
*/
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
function info() {
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
provider:
|
|
221
|
+
provider(),
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
size:
|
|
225
|
+
SIZE
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
module.exports = {
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
create,
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
info,
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
SIZE
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
};
|