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
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
<div class="acp-page-container">
|
|
2
|
+
|
|
3
|
+
<h1>
|
|
4
|
+
<i class="fa fa-robot"></i>
|
|
5
|
+
{{title}}
|
|
6
|
+
</h1>
|
|
7
|
+
|
|
8
|
+
<hr />
|
|
9
|
+
|
|
10
|
+
<form role="form">
|
|
11
|
+
|
|
12
|
+
<h3>AI Provider</h3>
|
|
13
|
+
|
|
14
|
+
<div class="mb-3">
|
|
15
|
+
<label class="form-label">Provider</label>
|
|
16
|
+
<select class="form-select" name="aiProvider">
|
|
17
|
+
<option value="openai" <!-- IF settings.aiProviderOpenai -->selected<!-- ENDIF settings.aiProviderOpenai -->>
|
|
18
|
+
OpenAI
|
|
19
|
+
</option>
|
|
20
|
+
<option value="fake" <!-- IF settings.aiProviderFake -->selected<!-- ENDIF settings.aiProviderFake -->>
|
|
21
|
+
Fake (AI disabled)
|
|
22
|
+
</option>
|
|
23
|
+
</select>
|
|
24
|
+
<div class="form-text">
|
|
25
|
+
Fake disables AI embeddings, indexing, semantic search and Qdrant access.
|
|
26
|
+
NodeBB and MCP tools remain available.
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<h3>OpenAI</h3>
|
|
31
|
+
|
|
32
|
+
<div class="mb-3">
|
|
33
|
+
<label class="form-label">OpenAI API Key</label>
|
|
34
|
+
<input
|
|
35
|
+
type="password"
|
|
36
|
+
class="form-control"
|
|
37
|
+
name="openaiApiKey"
|
|
38
|
+
autocomplete="new-password"
|
|
39
|
+
placeholder="Leave empty to keep the current key"
|
|
40
|
+
/>
|
|
41
|
+
<!-- IF settings.openaiConfigured -->
|
|
42
|
+
<div class="form-text text-success">API key is configured</div>
|
|
43
|
+
<!-- ELSE -->
|
|
44
|
+
<div class="form-text">API key is not configured</div>
|
|
45
|
+
<!-- ENDIF settings.openaiConfigured -->
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="mb-3">
|
|
49
|
+
<label class="form-label">Embedding Model</label>
|
|
50
|
+
<input
|
|
51
|
+
class="form-control"
|
|
52
|
+
name="openaiModel"
|
|
53
|
+
value="{settings.openaiModel}"
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="mb-3">
|
|
58
|
+
<label class="form-label">Embedding Dimensions</label>
|
|
59
|
+
<input
|
|
60
|
+
class="form-control"
|
|
61
|
+
type="number"
|
|
62
|
+
name="embeddingSize"
|
|
63
|
+
value="{settings.embeddingSize}"
|
|
64
|
+
min="1"
|
|
65
|
+
/>
|
|
66
|
+
<div class="form-text">
|
|
67
|
+
The default dimension for text-embedding-3-small is 1536.
|
|
68
|
+
Changing this value requires a matching Qdrant collection.
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<hr />
|
|
73
|
+
|
|
74
|
+
<h3>Qdrant</h3>
|
|
75
|
+
|
|
76
|
+
<div class="mb-3">
|
|
77
|
+
<label class="form-label">Qdrant URL</label>
|
|
78
|
+
<input
|
|
79
|
+
class="form-control"
|
|
80
|
+
name="qdrantUrl"
|
|
81
|
+
value="{settings.qdrantUrl}"
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div class="mb-3">
|
|
86
|
+
<label class="form-label">Qdrant Collection</label>
|
|
87
|
+
<input
|
|
88
|
+
class="form-control"
|
|
89
|
+
name="collection"
|
|
90
|
+
value="{settings.collection}"
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<hr />
|
|
95
|
+
|
|
96
|
+
<h3>Indexing</h3>
|
|
97
|
+
|
|
98
|
+
<div class="mb-3">
|
|
99
|
+
<label class="form-label">Excluded Categories</label>
|
|
100
|
+
<select
|
|
101
|
+
class="form-select"
|
|
102
|
+
name="excludedCategories"
|
|
103
|
+
multiple
|
|
104
|
+
size="10"
|
|
105
|
+
>
|
|
106
|
+
<!-- BEGIN categories -->
|
|
107
|
+
<option value="{categories.cid}" <!-- IF categories.selected -->selected<!-- ENDIF categories.selected -->>
|
|
108
|
+
{categories.name}
|
|
109
|
+
</option>
|
|
110
|
+
<!-- END categories -->
|
|
111
|
+
</select>
|
|
112
|
+
<div class="form-text">
|
|
113
|
+
Posts in selected categories are excluded from indexing and search.
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<hr />
|
|
118
|
+
|
|
119
|
+
<h3>MCP Server</h3>
|
|
120
|
+
|
|
121
|
+
<div class="form-check mb-3">
|
|
122
|
+
<input
|
|
123
|
+
class="form-check-input"
|
|
124
|
+
type="checkbox"
|
|
125
|
+
name="mcpEnabled"
|
|
126
|
+
id="mcpEnabled"
|
|
127
|
+
<!-- IF settings.mcpEnabled -->checked<!-- ENDIF settings.mcpEnabled -->
|
|
128
|
+
/>
|
|
129
|
+
<label class="form-check-label" for="mcpEnabled">
|
|
130
|
+
Enable MCP Server
|
|
131
|
+
</label>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div class="mb-3">
|
|
135
|
+
<label class="form-label">Bind Address</label>
|
|
136
|
+
<input
|
|
137
|
+
class="form-control"
|
|
138
|
+
name="mcpHost"
|
|
139
|
+
value="{settings.mcpHost}"
|
|
140
|
+
placeholder="127.0.0.1"
|
|
141
|
+
/>
|
|
142
|
+
<div class="form-text">
|
|
143
|
+
127.0.0.1 allows local connections only.
|
|
144
|
+
0.0.0.0 listens on all network interfaces.
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div class="mb-3">
|
|
149
|
+
<label class="form-label">HTTP Port</label>
|
|
150
|
+
<input
|
|
151
|
+
class="form-control"
|
|
152
|
+
type="number"
|
|
153
|
+
name="mcpPort"
|
|
154
|
+
value="{settings.mcpPort}"
|
|
155
|
+
min="1"
|
|
156
|
+
max="65535"
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div class="mb-3">
|
|
161
|
+
<label class="form-label">NodeBB API URL</label>
|
|
162
|
+
<input
|
|
163
|
+
class="form-control"
|
|
164
|
+
name="mcpNodebbUrl"
|
|
165
|
+
value="{settings.mcpNodebbUrl}"
|
|
166
|
+
placeholder="http://127.0.0.1:4567"
|
|
167
|
+
/>
|
|
168
|
+
<div class="form-text">
|
|
169
|
+
Base URL used by the MCP process to access the internal NodeBB API.
|
|
170
|
+
Use an address reachable from the MCP process environment.
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<div class="mb-3">
|
|
175
|
+
<label class="form-label">MCP Bearer Token</label>
|
|
176
|
+
<input
|
|
177
|
+
class="form-control"
|
|
178
|
+
type="password"
|
|
179
|
+
name="mcpBearerToken"
|
|
180
|
+
autocomplete="new-password"
|
|
181
|
+
placeholder="Leave empty to keep the current token"
|
|
182
|
+
/>
|
|
183
|
+
<!-- IF settings.mcpBearerTokenConfigured -->
|
|
184
|
+
<div class="form-text text-success">Bearer token is configured</div>
|
|
185
|
+
<!-- ELSE -->
|
|
186
|
+
<div class="form-text">Bearer token is not configured</div>
|
|
187
|
+
<!-- ENDIF settings.mcpBearerTokenConfigured -->
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div class="mb-3">
|
|
191
|
+
<label class="form-label">NodeBB Service UID</label>
|
|
192
|
+
<input
|
|
193
|
+
class="form-control"
|
|
194
|
+
type="number"
|
|
195
|
+
name="mcpServiceUid"
|
|
196
|
+
value="{settings.mcpServiceUid}"
|
|
197
|
+
min="1"
|
|
198
|
+
/>
|
|
199
|
+
<div class="form-text">
|
|
200
|
+
NodeBB user ID used by MCP for search and access-control checks.
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<hr />
|
|
205
|
+
|
|
206
|
+
<button id="save" class="btn btn-primary" type="button">
|
|
207
|
+
<i class="fa fa-save"></i>
|
|
208
|
+
Save Settings
|
|
209
|
+
</button>
|
|
210
|
+
|
|
211
|
+
</form>
|
|
212
|
+
|
|
213
|
+
</div>
|
package/src/ai/embeddings.js
CHANGED
|
@@ -1,254 +1,47 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const OpenAI = require("./openai");
|
|
4
|
+
|
|
5
|
+
const MAX_TEXT_LENGTH = 30000;
|
|
6
|
+
|
|
7
|
+
function normalizeText(value) {
|
|
8
|
+
if (typeof value !== "string") {
|
|
9
|
+
return "";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return value
|
|
13
|
+
.replace(/<[^>]*>/g, " ")
|
|
14
|
+
.replace(/ /gi, " ")
|
|
15
|
+
.replace(/&/gi, "&")
|
|
16
|
+
.replace(/</gi, "<")
|
|
17
|
+
.replace(/>/gi, ">")
|
|
18
|
+
.replace(/"/gi, '"')
|
|
19
|
+
.replace(/'/gi, "'")
|
|
20
|
+
.replace(/\s+/g, " ")
|
|
21
|
+
.trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function preparePost(post) {
|
|
25
|
+
if (!post || typeof post !== "object") {
|
|
26
|
+
throw new TypeError("Post is required");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const content = normalizeText(post.content);
|
|
30
|
+
|
|
31
|
+
if (!content) {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return content.slice(0, MAX_TEXT_LENGTH);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function create(text) {
|
|
39
|
+
return OpenAI.createEmbedding(text);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = {
|
|
43
|
+
normalizeText,
|
|
44
|
+
preparePost,
|
|
45
|
+
create,
|
|
46
|
+
MAX_TEXT_LENGTH
|
|
254
47
|
};
|