@tiledesk/tiledesk-server 2.18.1 → 2.18.3
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/CHANGELOG.md +5 -1
- package/app.js +2 -0
- package/docs/routes-answered.md +153 -0
- package/migrations/1757601159298-project_user_role_type.js +92 -33
- package/models/kb_setting.js +73 -7
- package/package.json +1 -1
- package/routes/answered.js +227 -0
- package/routes/unanswered.js +31 -18
package/CHANGELOG.md
CHANGED
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
🚀 IN PRODUCTION 🚀
|
|
6
6
|
(https://www.npmjs.com/package/@tiledesk/tiledesk-server/v/2.3.77)
|
|
7
7
|
|
|
8
|
-
# 2.18.
|
|
8
|
+
# 2.18.3
|
|
9
9
|
- Added permissions logic
|
|
10
10
|
- Added custom roles support
|
|
11
|
+
- Add answered questions functionality
|
|
12
|
+
- Added AnsweredQuestion schema with TTL index for automatic deletion.
|
|
13
|
+
- Updated UnansweredQuestion schema to include additional fields and improved query handling for searching and sorting.
|
|
14
|
+
- Enhanced the deletion process for unanswered questions.
|
|
11
15
|
|
|
12
16
|
# 2.17.4
|
|
13
17
|
- Refactor error handling and code structure in webhook.js
|
package/app.js
CHANGED
|
@@ -130,6 +130,7 @@ var integration = require('./routes/integration')
|
|
|
130
130
|
var kbsettings = require('./routes/kbsettings');
|
|
131
131
|
var kb = require('./routes/kb');
|
|
132
132
|
var unanswered = require('./routes/unanswered');
|
|
133
|
+
var answered = require('./routes/answered');
|
|
133
134
|
|
|
134
135
|
// var admin = require('./routes/admin');
|
|
135
136
|
var faqpub = require('./routes/faqpub');
|
|
@@ -643,6 +644,7 @@ app.use('/:projectid/mcp', [passport.authenticate(['basic', 'jwt'], { session: f
|
|
|
643
644
|
|
|
644
645
|
app.use('/:projectid/kbsettings', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('agent', ['bot','subscription'])], kbsettings);
|
|
645
646
|
app.use('/:projectid/kb/unanswered', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])], unanswered);
|
|
647
|
+
app.use('/:projectid/kb/answered', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])], answered);
|
|
646
648
|
app.use('/:projectid/kb', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])], kb);
|
|
647
649
|
|
|
648
650
|
app.use('/:projectid/logs', [passport.authenticate(['basic', 'jwt'], { session: false }), validtoken, roleChecker.hasRole('agent')], logs);
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Route `kb/answered` — domande con risposta (Knowledge Base)
|
|
2
|
+
|
|
3
|
+
Documentazione per [`routes/answered.js`](../routes/answered.js).
|
|
4
|
+
|
|
5
|
+
## Scopo
|
|
6
|
+
|
|
7
|
+
API REST per gestire le **domande già risposte** associate a un **namespace** della Knowledge Base del progetto. I documenti sono persistiti nel modello Mongoose `AnsweredQuestion` (vedi [`models/kb_setting.js`](../models/kb_setting.js)).
|
|
8
|
+
|
|
9
|
+
Ogni operazione è vincolata al **progetto corrente** (`req.projectid`, derivato dal segmento `:projectid` nell’URL) e alla validità del **namespace** (deve esistere un `Namespace` con `id` uguale al `namespace` richiesto e `id_project` uguale al progetto).
|
|
10
|
+
|
|
11
|
+
## Montaggio e autenticazione
|
|
12
|
+
|
|
13
|
+
In [`app.js`](../app.js) il router è montato così:
|
|
14
|
+
|
|
15
|
+
- **Path base:** `/:projectid/kb/answered`
|
|
16
|
+
- **Middleware:** `passport.authenticate(['basic', 'jwt'])`, `validtoken`, `roleChecker.hasRoleOrTypes('admin', ['bot','subscription'])`
|
|
17
|
+
|
|
18
|
+
Sono quindi richiesti autenticazione (Basic o JWT), token valido e ruolo **admin** oppure tipi **bot** / **subscription**.
|
|
19
|
+
|
|
20
|
+
Esempio di URL completo (sviluppo locale):
|
|
21
|
+
|
|
22
|
+
```http
|
|
23
|
+
GET http://localhost:3000/{projectId}/kb/answered/{namespace}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Sostituire `{projectId}` con l’ID MongoDB del progetto.
|
|
27
|
+
|
|
28
|
+
## Modello dati (`AnsweredQuestion`)
|
|
29
|
+
|
|
30
|
+
| Campo | Tipo | Note |
|
|
31
|
+
|---------------|----------|-------------------------------------------|
|
|
32
|
+
| `id_project` | string | Impostato dal server dal contesto progetto |
|
|
33
|
+
| `namespace` | string | ID del namespace (validato contro `Namespace`) |
|
|
34
|
+
| `question` | string | Obbligatorio |
|
|
35
|
+
| `answer` | string | Obbligatorio |
|
|
36
|
+
| `tokens` | number | Opzionale |
|
|
37
|
+
| `request_id` | string | Opzionale |
|
|
38
|
+
| `created_at` / `updated_at` | date | Da `timestamps: true` |
|
|
39
|
+
|
|
40
|
+
Indici rilevanti: TTL su `created_at`, indice composto `(id_project, namespace, created_at)`, indice **testo** su `question` e `answer` (pesi question 5, answer 1) per la ricerca full-text.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Endpoint
|
|
45
|
+
|
|
46
|
+
Tutti i path qui sotto sono relativi a `/:projectid/kb/answered`.
|
|
47
|
+
|
|
48
|
+
### `POST /`
|
|
49
|
+
|
|
50
|
+
Aggiunge una nuova domanda risposta.
|
|
51
|
+
|
|
52
|
+
**Body JSON**
|
|
53
|
+
|
|
54
|
+
| Campo | Obbligatorio | Descrizione |
|
|
55
|
+
|-------------|--------------|--------------------|
|
|
56
|
+
| `namespace` | sì | ID namespace |
|
|
57
|
+
| `question` | sì | Testo domanda |
|
|
58
|
+
| `answer` | sì | Testo risposta |
|
|
59
|
+
| `tokens` | no | Numero opzionale |
|
|
60
|
+
| `request_id`| no | Riferimento richiesta |
|
|
61
|
+
|
|
62
|
+
**Risposte**
|
|
63
|
+
|
|
64
|
+
| HTTP | Significato |
|
|
65
|
+
|------|-------------|
|
|
66
|
+
| 200 | Documento salvato (corpo = documento MongoDB creato) |
|
|
67
|
+
| 400 | Parametri mancanti (`namespace`, `question`, `answer`) |
|
|
68
|
+
| 403 | Namespace non appartenente al progetto |
|
|
69
|
+
| 500 | Errore server |
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
### `GET /:namespace`
|
|
74
|
+
|
|
75
|
+
Elenco paginato delle domande risposte per il namespace.
|
|
76
|
+
|
|
77
|
+
**Query**
|
|
78
|
+
|
|
79
|
+
| Parametro | Default | Descrizione |
|
|
80
|
+
|--------------|---------|-------------|
|
|
81
|
+
| `page` | `0` | Pagina (0-based) |
|
|
82
|
+
| `limit` | `20` | Elementi per pagina |
|
|
83
|
+
| `sortField` | `created_at` | Campo ordinamento |
|
|
84
|
+
| `direction` | `-1` | `1` crescente, `-1` decrescente |
|
|
85
|
+
| `search` | — | Se presente, attiva ricerca **full-text** (`$text`); l’ordinamento usa lo score di testo |
|
|
86
|
+
|
|
87
|
+
**Risposta 200**
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"count": 0,
|
|
92
|
+
"questions": [],
|
|
93
|
+
"query": {
|
|
94
|
+
"page": 0,
|
|
95
|
+
"limit": 20,
|
|
96
|
+
"sortField": "created_at",
|
|
97
|
+
"direction": -1,
|
|
98
|
+
"search": "..."
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Altri codici:** `400` (namespace mancante), `403` (namespace non valido per il progetto), `500`.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### `DELETE /:id`
|
|
108
|
+
|
|
109
|
+
Elimina una singola domanda per `_id` MongoDB.
|
|
110
|
+
|
|
111
|
+
**Risposte:** `200` con `{ success, message }`, `404` se non trovata (o non del progetto), `500`.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
### `DELETE /namespace/:namespace`
|
|
116
|
+
|
|
117
|
+
Elimina **tutte** le domande risposte per `namespace` nel progetto corrente.
|
|
118
|
+
|
|
119
|
+
**Risposta 200:** `{ success, count: <deletedCount>, message }`.
|
|
120
|
+
|
|
121
|
+
**403** se il namespace non appartiene al progetto. **500** in errore.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
### `GET /count/:namespace`
|
|
126
|
+
|
|
127
|
+
Restituisce il conteggio documenti per `id_project` + `namespace`.
|
|
128
|
+
|
|
129
|
+
**Risposta 200:** `{ count: <number> }`.
|
|
130
|
+
|
|
131
|
+
**400** / **403** / **500** come negli altri endpoint con validazione namespace.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Funzione interna `validateNamespace(id_project, namespace_id)`
|
|
136
|
+
|
|
137
|
+
Verifica che esista un documento `Namespace` con `id_project` e `id: namespace_id`. Usata in creazione, lettura elenco, cancellazione bulk e conteggio.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Nota sull’ordine delle route Express
|
|
142
|
+
|
|
143
|
+
Nel file è definito `GET /:namespace` **prima** di `GET /count/:namespace`. In Express la prima route che corrisponde vince: una richiesta del tipo `GET .../kb/answered/count/<namespace>` può essere interpretata come `GET /:namespace` con `namespace` uguale alla stringa letterale `count`, invece che come endpoint di conteggio.
|
|
144
|
+
|
|
145
|
+
Per far funzionare l’URL `GET /count/:namespace` come previsto dal codice, in genere occorre registrare **`GET /count/:namespace` prima di `GET /:namespace`**, oppure usare un prefisso path diverso per uno dei due. Verificare il comportamento in ambiente reale o adeguare l’ordine delle route se il conteggio non risponde come atteso.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Riferimenti
|
|
150
|
+
|
|
151
|
+
- Router: [`routes/answered.js`](../routes/answered.js)
|
|
152
|
+
- Modello e indici: [`models/kb_setting.js`](../models/kb_setting.js) (`AnsweredQuestionSchema`)
|
|
153
|
+
- Montaggio app: [`app.js`](../app.js) (`/:projectid/kb/answered`)
|
|
@@ -1,45 +1,104 @@
|
|
|
1
1
|
var Project_user = require("../models/project_user");
|
|
2
2
|
var winston = require('../config/winston');
|
|
3
3
|
|
|
4
|
+
const BATCH_SIZE = 100;
|
|
5
|
+
/** Log a progress line every this many bulkWrite rounds (plus always after the 1st). */
|
|
6
|
+
const PROGRESS_LOG_EVERY_BATCHES = 10;
|
|
7
|
+
|
|
8
|
+
/** Only documents that still need roleType (idempotent re-runs). */
|
|
9
|
+
const WITHOUT_ROLE_TYPE = {
|
|
10
|
+
$or: [{ roleType: { $exists: false } }, { roleType: null }]
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const AGENT_ROLES_FILTER = {
|
|
14
|
+
$and: [
|
|
15
|
+
{
|
|
16
|
+
$or: [
|
|
17
|
+
{ role: 'agent' },
|
|
18
|
+
{ role: 'supervisor' },
|
|
19
|
+
{ role: 'admin' },
|
|
20
|
+
{ role: 'owner' }
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
WITHOUT_ROLE_TYPE
|
|
24
|
+
]
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const USER_ROLES_FILTER = {
|
|
28
|
+
$and: [{ $or: [{ role: 'user' }, { role: 'guest' }] }, WITHOUT_ROLE_TYPE]
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
async function batchSetRoleType(filter, roleType, phaseLabel) {
|
|
32
|
+
const totalMatching = await Project_user.countDocuments(filter);
|
|
33
|
+
const started = Date.now();
|
|
34
|
+
winston.info(
|
|
35
|
+
`[project_user_role_type] ${phaseLabel}: ${totalMatching} documents match filter; streaming _id + bulkWrite in chunks of ${BATCH_SIZE}`
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
let modified = 0;
|
|
39
|
+
let scanned = 0;
|
|
40
|
+
let bulkRounds = 0;
|
|
41
|
+
const cursor = Project_user.find(filter).select('_id').lean().cursor();
|
|
42
|
+
let batch = [];
|
|
43
|
+
|
|
44
|
+
function logProgress(reason) {
|
|
45
|
+
const elapsedSec = ((Date.now() - started) / 1000).toFixed(1);
|
|
46
|
+
winston.info(
|
|
47
|
+
`[project_user_role_type] ${phaseLabel} ${reason}: scanned=${scanned}/${totalMatching}, modifiedThisPhase=${modified}, ${elapsedSec}s elapsed`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for await (const doc of cursor) {
|
|
52
|
+
scanned++;
|
|
53
|
+
batch.push({
|
|
54
|
+
updateOne: {
|
|
55
|
+
filter: { _id: doc._id, ...WITHOUT_ROLE_TYPE },
|
|
56
|
+
update: { $set: { roleType } }
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
if (batch.length >= BATCH_SIZE) {
|
|
60
|
+
const result = await Project_user.bulkWrite(batch);
|
|
61
|
+
modified += result.modifiedCount;
|
|
62
|
+
batch = [];
|
|
63
|
+
bulkRounds++;
|
|
64
|
+
if (bulkRounds === 1 || bulkRounds % PROGRESS_LOG_EVERY_BATCHES === 0) {
|
|
65
|
+
logProgress(`progress (bulk round ${bulkRounds})`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (batch.length > 0) {
|
|
70
|
+
const result = await Project_user.bulkWrite(batch);
|
|
71
|
+
modified += result.modifiedCount;
|
|
72
|
+
bulkRounds++;
|
|
73
|
+
logProgress(`final partial batch (bulk round ${bulkRounds})`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
logProgress('phase complete');
|
|
77
|
+
return modified;
|
|
78
|
+
}
|
|
79
|
+
|
|
4
80
|
/**
|
|
5
81
|
* Make any changes you need to make to the database here
|
|
6
82
|
*/
|
|
7
|
-
async function up
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (err) {
|
|
22
|
-
winston.error("Error appling the migration script", err);
|
|
23
|
-
}
|
|
24
|
-
winston.info("Schema updated for " + updates.nModified + " project_user of type user"),
|
|
25
|
-
winston.debug("updates",updates)
|
|
26
|
-
return resolve('ok');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
83
|
+
async function up() {
|
|
84
|
+
try {
|
|
85
|
+
const agentsModified = await batchSetRoleType(AGENT_ROLES_FILTER, 1, 'agents roleType=1');
|
|
86
|
+
winston.info(`[project_user_role_type] Agents phase done: ${agentsModified} documents modified`);
|
|
87
|
+
|
|
88
|
+
const usersModified = await batchSetRoleType(USER_ROLES_FILTER, 2, 'users/guest roleType=2');
|
|
89
|
+
winston.info(`[project_user_role_type] Users/guest phase done: ${usersModified} documents modified`);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
winston.error('[project_user_role_type] Error applying migration:', err);
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
36
97
|
* Make any changes that UNDO the up function side effects here (if possible)
|
|
37
98
|
*/
|
|
38
|
-
async function down
|
|
39
|
-
|
|
99
|
+
async function down() {
|
|
40
100
|
// Write migration here
|
|
41
101
|
// console.log("down*********");
|
|
42
102
|
}
|
|
43
103
|
|
|
44
|
-
module.exports = { up, down };
|
|
45
|
-
|
|
104
|
+
module.exports = { up, down };
|
package/models/kb_setting.js
CHANGED
|
@@ -2,10 +2,24 @@ let mongoose = require('mongoose');
|
|
|
2
2
|
let Schema = mongoose.Schema;
|
|
3
3
|
let winston = require('../config/winston');
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
const DEFAULT_UNANSWERED_TTL_SEC = 7 * 24 * 60 * 60; // 7 days
|
|
6
|
+
const DEFAULT_ANSWERED_TTL_SEC = 7 * 24 * 60 * 60; // 7 days
|
|
7
|
+
|
|
8
|
+
function ttlSecondsFromEnv(raw, fallbackSec) {
|
|
9
|
+
if (raw == null || String(raw).trim() === '') return fallbackSec;
|
|
10
|
+
const n = Number(raw);
|
|
11
|
+
return Number.isFinite(n) && n >= 0 ? n : fallbackSec;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let expireAfterSeconds = ttlSecondsFromEnv(
|
|
15
|
+
process.env.UNANSWERED_QUESTION_EXPIRATION_TIME,
|
|
16
|
+
DEFAULT_UNANSWERED_TTL_SEC
|
|
17
|
+
);
|
|
18
|
+
let expireAnsweredAfterSeconds = ttlSecondsFromEnv(
|
|
19
|
+
process.env.ANSWERED_QUESTION_EXPIRATION_TIME,
|
|
20
|
+
DEFAULT_ANSWERED_TTL_SEC
|
|
21
|
+
);
|
|
22
|
+
|
|
9
23
|
|
|
10
24
|
const EngineSchema = new Schema({
|
|
11
25
|
name: {
|
|
@@ -203,13 +217,63 @@ const UnansweredQuestionSchema = new Schema({
|
|
|
203
217
|
question: {
|
|
204
218
|
type: String,
|
|
205
219
|
required: true
|
|
220
|
+
},
|
|
221
|
+
request_id: {
|
|
222
|
+
type: String,
|
|
223
|
+
required: false,
|
|
224
|
+
},
|
|
225
|
+
sender: {
|
|
226
|
+
type: String,
|
|
227
|
+
required: false,
|
|
206
228
|
}
|
|
207
229
|
},{
|
|
208
230
|
timestamps: true
|
|
209
231
|
});
|
|
210
232
|
|
|
211
|
-
|
|
212
|
-
|
|
233
|
+
const AnsweredQuestionSchema = new Schema({
|
|
234
|
+
id_project: {
|
|
235
|
+
type: String,
|
|
236
|
+
required: true,
|
|
237
|
+
index: true
|
|
238
|
+
},
|
|
239
|
+
namespace: {
|
|
240
|
+
type: String,
|
|
241
|
+
required: true,
|
|
242
|
+
index: true
|
|
243
|
+
},
|
|
244
|
+
question: {
|
|
245
|
+
type: String,
|
|
246
|
+
required: true
|
|
247
|
+
},
|
|
248
|
+
answer: {
|
|
249
|
+
type: String,
|
|
250
|
+
required: true
|
|
251
|
+
},
|
|
252
|
+
tokens: {
|
|
253
|
+
type: Number,
|
|
254
|
+
required: false
|
|
255
|
+
},
|
|
256
|
+
request_id: {
|
|
257
|
+
type: String,
|
|
258
|
+
required: false
|
|
259
|
+
}
|
|
260
|
+
}, {
|
|
261
|
+
timestamps: true
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Add TTL index to automatically delete documents
|
|
265
|
+
UnansweredQuestionSchema.index({ createdAt: 1 }, { expireAfterSeconds: expireAfterSeconds });
|
|
266
|
+
UnansweredQuestionSchema.index({ id_project: 1, namespace: 1, createdAt: -1 });
|
|
267
|
+
UnansweredQuestionSchema.index({ question: "text" });
|
|
268
|
+
|
|
269
|
+
AnsweredQuestionSchema.index({ createdAt: 1 }, { expireAfterSeconds: expireAnsweredAfterSeconds });
|
|
270
|
+
AnsweredQuestionSchema.index({ id_project: 1, namespace: 1, createdAt: -1 });
|
|
271
|
+
AnsweredQuestionSchema.index(
|
|
272
|
+
{ question: "text", answer: "text" },
|
|
273
|
+
{ weights: { question: 3, answer: 1 } }
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
|
|
213
277
|
|
|
214
278
|
// DEPRECATED !! - Start
|
|
215
279
|
const KBSettingSchema = new Schema({
|
|
@@ -246,6 +310,7 @@ const Engine = mongoose.model('Engine', EngineSchema)
|
|
|
246
310
|
const Namespace = mongoose.model('Namespace', NamespaceSchema)
|
|
247
311
|
const KB = mongoose.model('KB', KBSchema)
|
|
248
312
|
const UnansweredQuestion = mongoose.model('UnansweredQuestion', UnansweredQuestionSchema)
|
|
313
|
+
const AnsweredQuestion = mongoose.model('AnsweredQuestion', AnsweredQuestionSchema)
|
|
249
314
|
|
|
250
315
|
|
|
251
316
|
module.exports = {
|
|
@@ -253,5 +318,6 @@ module.exports = {
|
|
|
253
318
|
Namespace: Namespace,
|
|
254
319
|
Engine: Engine,
|
|
255
320
|
KB: KB,
|
|
256
|
-
UnansweredQuestion: UnansweredQuestion
|
|
321
|
+
UnansweredQuestion: UnansweredQuestion,
|
|
322
|
+
AnsweredQuestion: AnsweredQuestion
|
|
257
323
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const router = express.Router();
|
|
3
|
+
const { Namespace, AnsweredQuestion } = require('../models/kb_setting');
|
|
4
|
+
const winston = require('../config/winston');
|
|
5
|
+
|
|
6
|
+
// Add a new unanswerd question
|
|
7
|
+
router.post('/', async (req, res) => {
|
|
8
|
+
try {
|
|
9
|
+
const { namespace, question, answer, tokens, request_id } = req.body;
|
|
10
|
+
const id_project = req.projectid;
|
|
11
|
+
|
|
12
|
+
if (!namespace || !question || !answer) {
|
|
13
|
+
return res.status(400).json({
|
|
14
|
+
success: false,
|
|
15
|
+
error: "Missing required parameters: namespace, question and answer"
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Check if namespae belongs to project
|
|
20
|
+
const isValidNamespace = await validateNamespace(id_project, namespace);
|
|
21
|
+
if (!isValidNamespace) {
|
|
22
|
+
return res.status(403).json({
|
|
23
|
+
success: false,
|
|
24
|
+
error: "Not allowed. The namespace does not belong to the current project."
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const answeredQuestion = new AnsweredQuestion({
|
|
29
|
+
id_project,
|
|
30
|
+
namespace,
|
|
31
|
+
question,
|
|
32
|
+
answer,
|
|
33
|
+
tokens,
|
|
34
|
+
request_id,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const savedQuestion = await answeredQuestion.save();
|
|
38
|
+
res.status(200).json(savedQuestion);
|
|
39
|
+
|
|
40
|
+
} catch (error) {
|
|
41
|
+
winston.error('Error adding answered question:', error);
|
|
42
|
+
res.status(500).json({
|
|
43
|
+
success: false,
|
|
44
|
+
error: "Error adding answered question"
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Get all answered questions for a namespace
|
|
50
|
+
router.get('/:namespace', async (req, res) => {
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const { namespace } = req.params;
|
|
54
|
+
const id_project = req.projectid;
|
|
55
|
+
|
|
56
|
+
if (!namespace) {
|
|
57
|
+
return res.status(400).json({
|
|
58
|
+
success: false,
|
|
59
|
+
error: "Missing required parameter: namespace"
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check if namespace belongs to project
|
|
64
|
+
const isValidNamespace = await validateNamespace(id_project, namespace);
|
|
65
|
+
if (!isValidNamespace) {
|
|
66
|
+
return res.status(403).json({
|
|
67
|
+
success: false,
|
|
68
|
+
error: "Not allowed. The namespace does not belong to the current project."
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const page = parseInt(req.query.page) || 0;
|
|
73
|
+
const limit = parseInt(req.query.limit) || 20;
|
|
74
|
+
const sortField = req.query.sortField || 'created_at';
|
|
75
|
+
const direction = parseInt(req.query.direction) || -1;
|
|
76
|
+
|
|
77
|
+
const filter = { id_project, namespace };
|
|
78
|
+
|
|
79
|
+
let projection = undefined;
|
|
80
|
+
|
|
81
|
+
if (req.query.search) {
|
|
82
|
+
filter.$text = { $search: req.query.search };
|
|
83
|
+
// Add score to projection if it's a text search
|
|
84
|
+
projection = { score: { $meta: "textScore" } };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let sortObj;
|
|
88
|
+
if (projection && projection.score) {
|
|
89
|
+
sortObj = { score: { $meta: "textScore" } };
|
|
90
|
+
} else {
|
|
91
|
+
sortObj = { [sortField]: direction };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const questions = await AnsweredQuestion.find(filter, projection)
|
|
95
|
+
.sort(sortObj)
|
|
96
|
+
.skip(page * limit)
|
|
97
|
+
.limit(limit);
|
|
98
|
+
|
|
99
|
+
const count = await AnsweredQuestion.countDocuments(filter);
|
|
100
|
+
|
|
101
|
+
res.status(200).json({
|
|
102
|
+
count,
|
|
103
|
+
questions,
|
|
104
|
+
query: {
|
|
105
|
+
page,
|
|
106
|
+
limit,
|
|
107
|
+
sortField,
|
|
108
|
+
direction,
|
|
109
|
+
search: req.query.search || undefined
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
} catch (error) {
|
|
114
|
+
winston.error('Error getting answered questions:', error);
|
|
115
|
+
res.status(500).json({
|
|
116
|
+
success: false,
|
|
117
|
+
error: "Error getting answered questions"
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
router.delete('/:id', async (req, res) => {
|
|
124
|
+
try {
|
|
125
|
+
const { id } = req.params;
|
|
126
|
+
const id_project = req.projectid;
|
|
127
|
+
|
|
128
|
+
const deleted = await AnsweredQuestion.findOneAndDelete({ _id: id, id_project });
|
|
129
|
+
if (!deleted) {
|
|
130
|
+
return res.status(404).json({
|
|
131
|
+
success: false,
|
|
132
|
+
error: "Question not found"
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
res.status(200).json({
|
|
137
|
+
success: true,
|
|
138
|
+
message: "Question deleted successfully"
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
} catch (error) {
|
|
142
|
+
winston.error('Error deleting answered question:', error);
|
|
143
|
+
res.status(500).json({
|
|
144
|
+
success: false,
|
|
145
|
+
error: "Error deleting answered question"
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
router.delete('/namespace/:namespace', async (req, res) => {
|
|
151
|
+
try {
|
|
152
|
+
const { namespace } = req.params;
|
|
153
|
+
const id_project = req.projectid;
|
|
154
|
+
|
|
155
|
+
// Check if namespace belongs to project
|
|
156
|
+
const isValidNamespace = await validateNamespace(id_project, namespace);
|
|
157
|
+
if (!isValidNamespace) {
|
|
158
|
+
return res.status(403).json({
|
|
159
|
+
success: false,
|
|
160
|
+
error: "Not allowed. The namespace does not belong to the current project."
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const result = await AnsweredQuestion.deleteMany({ id_project, namespace });
|
|
165
|
+
res.status(200).json({
|
|
166
|
+
success: true,
|
|
167
|
+
count: result.deletedCount,
|
|
168
|
+
message: "All questions deleted successfully"
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
} catch (error) {
|
|
172
|
+
winston.error('Error deleting answered questions:', error);
|
|
173
|
+
res.status(500).json({
|
|
174
|
+
success: false,
|
|
175
|
+
error: "Error deleting answered questions"
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
router.get('/count/:namespace', async (req, res) => {
|
|
181
|
+
try {
|
|
182
|
+
const { namespace } = req.params;
|
|
183
|
+
const id_project = req.projectid;
|
|
184
|
+
|
|
185
|
+
if (!namespace) {
|
|
186
|
+
return res.status(400).json({
|
|
187
|
+
success: false,
|
|
188
|
+
error: "Missing required parameter: namespace"
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Check if namespace belongs to project
|
|
193
|
+
const isValidNamespace = await validateNamespace(id_project, namespace);
|
|
194
|
+
if (!isValidNamespace) {
|
|
195
|
+
return res.status(403).json({
|
|
196
|
+
success: false,
|
|
197
|
+
error: "Not allowed. The namespace does not belong to the current project."
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const count = await AnsweredQuestion.countDocuments({ id_project, namespace });
|
|
202
|
+
res.status(200).json({ count });
|
|
203
|
+
|
|
204
|
+
} catch (error) {
|
|
205
|
+
winston.error('Error counting answered questions:', error);
|
|
206
|
+
res.status(500).json({
|
|
207
|
+
success: false,
|
|
208
|
+
error: "Error counting answered questions"
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// Helper function to validate namespace
|
|
214
|
+
async function validateNamespace(id_project, namespace_id) {
|
|
215
|
+
try {
|
|
216
|
+
const namespace = await Namespace.findOne({
|
|
217
|
+
id_project: id_project,
|
|
218
|
+
id: namespace_id
|
|
219
|
+
});
|
|
220
|
+
return !!namespace;
|
|
221
|
+
} catch (err) {
|
|
222
|
+
winston.error('validate namespace error: ', err);
|
|
223
|
+
throw err;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = router;
|
package/routes/unanswered.js
CHANGED
|
@@ -6,7 +6,7 @@ var winston = require('../config/winston');
|
|
|
6
6
|
// Add a new unanswered question
|
|
7
7
|
router.post('/', async (req, res) => {
|
|
8
8
|
try {
|
|
9
|
-
const { namespace, question } = req.body;
|
|
9
|
+
const { namespace, question, request_id, sender } = req.body;
|
|
10
10
|
const id_project = req.projectid;
|
|
11
11
|
|
|
12
12
|
if (!namespace || !question) {
|
|
@@ -28,7 +28,9 @@ router.post('/', async (req, res) => {
|
|
|
28
28
|
const unansweredQuestion = new UnansweredQuestion({
|
|
29
29
|
id_project,
|
|
30
30
|
namespace,
|
|
31
|
-
question
|
|
31
|
+
question,
|
|
32
|
+
request_id,
|
|
33
|
+
sender
|
|
32
34
|
});
|
|
33
35
|
|
|
34
36
|
const savedQuestion = await unansweredQuestion.save();
|
|
@@ -70,18 +72,29 @@ router.get('/:namespace', async (req, res) => {
|
|
|
70
72
|
const sortField = req.query.sortField || 'createdAt';
|
|
71
73
|
const direction = parseInt(req.query.direction) || -1;
|
|
72
74
|
|
|
73
|
-
const
|
|
74
|
-
id_project,
|
|
75
|
-
namespace
|
|
76
|
-
})
|
|
77
|
-
.sort({ [sortField]: direction })
|
|
78
|
-
.skip(page * limit)
|
|
79
|
-
.limit(limit);
|
|
75
|
+
const filter = { id_project, namespace };
|
|
80
76
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
77
|
+
let projection = undefined;
|
|
78
|
+
|
|
79
|
+
if (req.query.search) {
|
|
80
|
+
filter.$text = { $search: req.query.search };
|
|
81
|
+
// Add score to projection if it's a text search
|
|
82
|
+
projection = { score: { $meta: "textScore" } };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let sortObj;
|
|
86
|
+
if (projection && projection.score) {
|
|
87
|
+
sortObj = { score: { $meta: "textScore" } };
|
|
88
|
+
} else {
|
|
89
|
+
sortObj = { [sortField]: direction };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const questions = await UnansweredQuestion.find(filter, projection)
|
|
93
|
+
.sort(sortObj)
|
|
94
|
+
.skip(page * limit)
|
|
95
|
+
.limit(limit);
|
|
96
|
+
|
|
97
|
+
const count = await UnansweredQuestion.countDocuments(filter);
|
|
85
98
|
|
|
86
99
|
res.status(200).json({
|
|
87
100
|
count,
|
|
@@ -90,7 +103,8 @@ router.get('/:namespace', async (req, res) => {
|
|
|
90
103
|
page,
|
|
91
104
|
limit,
|
|
92
105
|
sortField,
|
|
93
|
-
direction
|
|
106
|
+
direction,
|
|
107
|
+
search: req.query.search || undefined
|
|
94
108
|
}
|
|
95
109
|
});
|
|
96
110
|
|
|
@@ -109,20 +123,19 @@ router.delete('/:id', async (req, res) => {
|
|
|
109
123
|
const { id } = req.params;
|
|
110
124
|
const id_project = req.projectid;
|
|
111
125
|
|
|
112
|
-
const
|
|
113
|
-
if (!
|
|
126
|
+
const deleted = await UnansweredQuestion.findOneAndDelete({ _id: id, id_project });
|
|
127
|
+
if (!deleted) {
|
|
114
128
|
return res.status(404).json({
|
|
115
129
|
success: false,
|
|
116
130
|
error: "Question not found"
|
|
117
131
|
});
|
|
118
132
|
}
|
|
119
133
|
|
|
120
|
-
await UnansweredQuestion.deleteOne({ _id: id });
|
|
121
134
|
res.status(200).json({
|
|
122
135
|
success: true,
|
|
123
136
|
message: "Question deleted successfully"
|
|
124
137
|
});
|
|
125
|
-
|
|
138
|
+
|
|
126
139
|
} catch (error) {
|
|
127
140
|
winston.error('Error deleting unanswered question:', error);
|
|
128
141
|
res.status(500).json({
|