@tiledesk/tiledesk-server 2.19.4 → 2.19.6
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 +7 -0
- package/models/kb_setting.js +2 -99
- package/package.json +1 -1
- package/routes/answered.js +243 -264
- package/routes/kb.js +162 -126
- package/routes/unanswered.js +277 -296
- package/routes/webhook.js +1 -2
- package/services/aiManager.js +41 -23
- package/services/kbQuestionService.js +98 -0
- package/test/kbRoute.js +148 -7
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
🚀 IN PRODUCTION 🚀
|
|
6
6
|
(https://www.npmjs.com/package/@tiledesk/tiledesk-server/v/2.3.77)
|
|
7
7
|
|
|
8
|
+
# 2.19.6
|
|
9
|
+
- Improved management of answered and unanswered questions, duration and token consumption
|
|
10
|
+
- Improved namespace embedding management
|
|
11
|
+
|
|
12
|
+
# 2.19.5 (aborted)
|
|
13
|
+
- Improved management of answered and unanswered questions, duration and token consumption
|
|
14
|
+
|
|
8
15
|
# 2.19.4
|
|
9
16
|
- Added missing id_project in data sent for qa and single scrape
|
|
10
17
|
|
package/models/kb_setting.js
CHANGED
|
@@ -20,67 +20,6 @@ let expireAnsweredAfterSeconds = ttlSecondsFromEnv(
|
|
|
20
20
|
DEFAULT_ANSWERED_TTL_SEC
|
|
21
21
|
);
|
|
22
22
|
|
|
23
|
-
const EMPTY_EMBEDDING_API_KEY = '';
|
|
24
|
-
|
|
25
|
-
function logEmbeddingApiKeyWriteAttempt(source, attemptedValue, meta = {}) {
|
|
26
|
-
if (attemptedValue === EMPTY_EMBEDDING_API_KEY || attemptedValue == null) {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
winston.warn('[Namespace.embedding.api_key] non-empty value blocked on ' + source, {
|
|
30
|
-
...meta,
|
|
31
|
-
attemptedValue,
|
|
32
|
-
stack: new Error().stack
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function sanitizeEmbeddingApiKeyObject(embedding, source, meta = {}) {
|
|
37
|
-
if (!embedding || typeof embedding !== 'object') {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
if (!Object.prototype.hasOwnProperty.call(embedding, 'api_key')) {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
if (embedding.api_key === EMPTY_EMBEDDING_API_KEY || embedding.api_key == null) {
|
|
44
|
-
embedding.api_key = EMPTY_EMBEDDING_API_KEY;
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
logEmbeddingApiKeyWriteAttempt(source, embedding.api_key, meta);
|
|
48
|
-
embedding.api_key = EMPTY_EMBEDDING_API_KEY;
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function sanitizeEmbeddingApiKeyUpdate(update, source, meta = {}) {
|
|
53
|
-
if (!update || typeof update !== 'object') {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
let changed = false;
|
|
58
|
-
|
|
59
|
-
const sanitizeContainer = (container) => {
|
|
60
|
-
if (!container || typeof container !== 'object') {
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
if (sanitizeEmbeddingApiKeyObject(container.embedding, source, meta)) {
|
|
64
|
-
changed = true;
|
|
65
|
-
}
|
|
66
|
-
if (Object.prototype.hasOwnProperty.call(container, 'embedding.api_key')
|
|
67
|
-
&& container['embedding.api_key'] !== EMPTY_EMBEDDING_API_KEY) {
|
|
68
|
-
logEmbeddingApiKeyWriteAttempt(source, container['embedding.api_key'], meta);
|
|
69
|
-
container['embedding.api_key'] = EMPTY_EMBEDDING_API_KEY;
|
|
70
|
-
changed = true;
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
sanitizeContainer(update);
|
|
75
|
-
['$set', '$setOnInsert'].forEach((key) => {
|
|
76
|
-
if (update[key]) {
|
|
77
|
-
sanitizeContainer(update[key]);
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
return changed;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
23
|
|
|
85
24
|
const EngineSchema = new Schema({
|
|
86
25
|
name: {
|
|
@@ -139,11 +78,8 @@ const EmbeddingSchema = new Schema({
|
|
|
139
78
|
api_key: {
|
|
140
79
|
type: String,
|
|
141
80
|
required: false,
|
|
142
|
-
default:
|
|
143
|
-
set:
|
|
144
|
-
logEmbeddingApiKeyWriteAttempt('embedding.api_key setter', value);
|
|
145
|
-
return EMPTY_EMBEDDING_API_KEY;
|
|
146
|
-
}
|
|
81
|
+
default: '',
|
|
82
|
+
set: () => ''
|
|
147
83
|
}
|
|
148
84
|
}, {
|
|
149
85
|
_id: false // This is schema is always used as an embedded object inside NamespaceSchema
|
|
@@ -186,39 +122,6 @@ const NamespaceSchema = new Schema({
|
|
|
186
122
|
timestamps: true
|
|
187
123
|
})
|
|
188
124
|
|
|
189
|
-
NamespaceSchema.pre('save', function (next) {
|
|
190
|
-
if (this.isModified('embedding.api_key')) {
|
|
191
|
-
const attemptedValue = this.get('embedding.api_key');
|
|
192
|
-
logEmbeddingApiKeyWriteAttempt('save', attemptedValue, {
|
|
193
|
-
namespaceId: this.id,
|
|
194
|
-
id_project: this.id_project
|
|
195
|
-
});
|
|
196
|
-
this.set('embedding.api_key', EMPTY_EMBEDDING_API_KEY);
|
|
197
|
-
}
|
|
198
|
-
next();
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
['updateOne', 'updateMany', 'findOneAndUpdate'].forEach((hook) => {
|
|
202
|
-
NamespaceSchema.pre(hook, function (next) {
|
|
203
|
-
const update = this.getUpdate();
|
|
204
|
-
if (sanitizeEmbeddingApiKeyUpdate(update, hook, { query: this.getQuery() })) {
|
|
205
|
-
this.setUpdate(update);
|
|
206
|
-
}
|
|
207
|
-
next();
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
NamespaceSchema.pre('insertMany', function (next, docs) {
|
|
212
|
-
if (Array.isArray(docs)) {
|
|
213
|
-
docs.forEach((doc) => {
|
|
214
|
-
sanitizeEmbeddingApiKeyObject(doc.embedding, 'insertMany', {
|
|
215
|
-
namespaceId: doc.id,
|
|
216
|
-
id_project: doc.id_project
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
next();
|
|
221
|
-
});
|
|
222
125
|
|
|
223
126
|
var KBSchema = new Schema({
|
|
224
127
|
id_project: {
|
package/package.json
CHANGED
package/routes/answered.js
CHANGED
|
@@ -6,295 +6,274 @@ var fastCsv = require('fast-csv');
|
|
|
6
6
|
|
|
7
7
|
// Add a new unanswerd question
|
|
8
8
|
router.post('/', async (req, res) => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const answeredQuestion = new AnsweredQuestion({
|
|
30
|
-
id_project,
|
|
31
|
-
namespace,
|
|
32
|
-
question,
|
|
33
|
-
answer,
|
|
34
|
-
tokens,
|
|
35
|
-
request_id,
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const savedQuestion = await answeredQuestion.save();
|
|
39
|
-
res.status(200).json(savedQuestion);
|
|
40
|
-
|
|
41
|
-
} catch (error) {
|
|
42
|
-
winston.error('Error adding answered question:', error);
|
|
43
|
-
res.status(500).json({
|
|
44
|
-
success: false,
|
|
45
|
-
error: "Error adding answered question"
|
|
46
|
-
});
|
|
47
|
-
}
|
|
9
|
+
try {
|
|
10
|
+
|
|
11
|
+
const data = req.body;
|
|
12
|
+
const id_project = req.projectid;
|
|
13
|
+
|
|
14
|
+
const savedQuestion = await KbQuestionService.createAnsweredQuestion(
|
|
15
|
+
id_project,
|
|
16
|
+
data
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
res.status(200).json(savedQuestion);
|
|
20
|
+
|
|
21
|
+
} catch (error) {
|
|
22
|
+
winston.error('Error adding answered question:', error);
|
|
23
|
+
res.status(error.status || 500).json({
|
|
24
|
+
success: false,
|
|
25
|
+
error: error.error || "Error adding answered question"
|
|
26
|
+
});
|
|
27
|
+
}
|
|
48
28
|
})
|
|
49
29
|
|
|
50
30
|
// Get all answered questions for a namespace
|
|
51
31
|
router.get('/:namespace', async (req, res) => {
|
|
52
32
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const page = parseInt(req.query.page) || 0;
|
|
74
|
-
const limit = parseInt(req.query.limit) || 20;
|
|
75
|
-
const sortField = req.query.sortField || 'created_at';
|
|
76
|
-
const direction = parseInt(req.query.direction) || -1;
|
|
77
|
-
|
|
78
|
-
const filter = { id_project, namespace };
|
|
79
|
-
|
|
80
|
-
let projection = undefined;
|
|
81
|
-
|
|
82
|
-
if (req.query.search) {
|
|
83
|
-
filter.$text = { $search: req.query.search };
|
|
84
|
-
// Add score to projection if it's a text search
|
|
85
|
-
projection = { score: { $meta: "textScore" } };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
let sortObj;
|
|
89
|
-
if (projection && projection.score) {
|
|
90
|
-
sortObj = { score: { $meta: "textScore" } };
|
|
91
|
-
} else {
|
|
92
|
-
sortObj = { [sortField]: direction };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const questions = await AnsweredQuestion.find(filter, projection)
|
|
96
|
-
.sort(sortObj)
|
|
97
|
-
.skip(page * limit)
|
|
98
|
-
.limit(limit);
|
|
99
|
-
|
|
100
|
-
const count = await AnsweredQuestion.countDocuments(filter);
|
|
101
|
-
|
|
102
|
-
res.status(200).json({
|
|
103
|
-
count,
|
|
104
|
-
questions,
|
|
105
|
-
query: {
|
|
106
|
-
page,
|
|
107
|
-
limit,
|
|
108
|
-
sortField,
|
|
109
|
-
direction,
|
|
110
|
-
search: req.query.search || undefined
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
} catch (error) {
|
|
115
|
-
winston.error('Error getting answered questions:', error);
|
|
116
|
-
res.status(500).json({
|
|
117
|
-
success: false,
|
|
118
|
-
error: "Error getting answered questions"
|
|
119
|
-
});
|
|
33
|
+
try {
|
|
34
|
+
const { namespace } = req.params;
|
|
35
|
+
const id_project = req.projectid;
|
|
36
|
+
|
|
37
|
+
if (!namespace) {
|
|
38
|
+
return res.status(400).json({
|
|
39
|
+
success: false,
|
|
40
|
+
error: "Missing required parameter: namespace"
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check if namespace belongs to project
|
|
45
|
+
const isValidNamespace = await validateNamespace(id_project, namespace);
|
|
46
|
+
if (!isValidNamespace) {
|
|
47
|
+
return res.status(403).json({
|
|
48
|
+
success: false,
|
|
49
|
+
error: "Not allowed. The namespace does not belong to the current project."
|
|
50
|
+
})
|
|
120
51
|
}
|
|
121
52
|
|
|
53
|
+
const page = parseInt(req.query.page) || 0;
|
|
54
|
+
const limit = parseInt(req.query.limit) || 20;
|
|
55
|
+
const sortField = req.query.sortField || 'createdAt';
|
|
56
|
+
const direction = parseInt(req.query.direction) || -1;
|
|
57
|
+
|
|
58
|
+
const filter = { id_project, namespace };
|
|
59
|
+
|
|
60
|
+
let projection = undefined;
|
|
61
|
+
|
|
62
|
+
if (req.query.search) {
|
|
63
|
+
filter.$text = { $search: req.query.search };
|
|
64
|
+
// Add score to projection if it's a text search
|
|
65
|
+
projection = { score: { $meta: "textScore" } };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let sortObj;
|
|
69
|
+
if (projection && projection.score) {
|
|
70
|
+
sortObj = { score: { $meta: "textScore" } };
|
|
71
|
+
} else {
|
|
72
|
+
sortObj = { [sortField]: direction };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const questions = await AnsweredQuestion.find(filter, projection)
|
|
76
|
+
.sort(sortObj)
|
|
77
|
+
.skip(page * limit)
|
|
78
|
+
.limit(limit);
|
|
79
|
+
|
|
80
|
+
const count = await AnsweredQuestion.countDocuments(filter);
|
|
81
|
+
|
|
82
|
+
res.status(200).json({
|
|
83
|
+
count,
|
|
84
|
+
questions,
|
|
85
|
+
query: {
|
|
86
|
+
page,
|
|
87
|
+
limit,
|
|
88
|
+
sortField,
|
|
89
|
+
direction,
|
|
90
|
+
search: req.query.search || undefined
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
} catch (error) {
|
|
95
|
+
winston.error('Error getting answered questions:', error);
|
|
96
|
+
res.status(500).json({
|
|
97
|
+
success: false,
|
|
98
|
+
error: "Error getting answered questions"
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
122
102
|
})
|
|
123
103
|
|
|
124
104
|
router.delete('/:id', async (req, res) => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
res.status(200).json({
|
|
138
|
-
success: true,
|
|
139
|
-
message: "Question deleted successfully"
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
} catch (error) {
|
|
143
|
-
winston.error('Error deleting answered question:', error);
|
|
144
|
-
res.status(500).json({
|
|
145
|
-
success: false,
|
|
146
|
-
error: "Error deleting answered question"
|
|
147
|
-
});
|
|
105
|
+
try {
|
|
106
|
+
const { id } = req.params;
|
|
107
|
+
const id_project = req.projectid;
|
|
108
|
+
|
|
109
|
+
const deleted = await AnsweredQuestion.findOneAndDelete({ _id: id, id_project });
|
|
110
|
+
if (!deleted) {
|
|
111
|
+
return res.status(404).json({
|
|
112
|
+
success: false,
|
|
113
|
+
error: "Question not found"
|
|
114
|
+
});
|
|
148
115
|
}
|
|
116
|
+
|
|
117
|
+
res.status(200).json({
|
|
118
|
+
success: true,
|
|
119
|
+
message: "Question deleted successfully"
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
} catch (error) {
|
|
123
|
+
winston.error('Error deleting answered question:', error);
|
|
124
|
+
res.status(500).json({
|
|
125
|
+
success: false,
|
|
126
|
+
error: "Error deleting answered question"
|
|
127
|
+
});
|
|
128
|
+
}
|
|
149
129
|
})
|
|
150
130
|
|
|
151
131
|
router.delete('/namespace/:namespace', async (req, res) => {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const result = await AnsweredQuestion.deleteMany({ id_project, namespace });
|
|
166
|
-
res.status(200).json({
|
|
167
|
-
success: true,
|
|
168
|
-
count: result.deletedCount,
|
|
169
|
-
message: "All questions deleted successfully"
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
} catch (error) {
|
|
173
|
-
winston.error('Error deleting answered questions:', error);
|
|
174
|
-
res.status(500).json({
|
|
175
|
-
success: false,
|
|
176
|
-
error: "Error deleting answered questions"
|
|
177
|
-
});
|
|
132
|
+
try {
|
|
133
|
+
const { namespace } = req.params;
|
|
134
|
+
const id_project = req.projectid;
|
|
135
|
+
|
|
136
|
+
// Check if namespace belongs to project
|
|
137
|
+
const isValidNamespace = await validateNamespace(id_project, namespace);
|
|
138
|
+
if (!isValidNamespace) {
|
|
139
|
+
return res.status(403).json({
|
|
140
|
+
success: false,
|
|
141
|
+
error: "Not allowed. The namespace does not belong to the current project."
|
|
142
|
+
});
|
|
178
143
|
}
|
|
144
|
+
|
|
145
|
+
const result = await AnsweredQuestion.deleteMany({ id_project, namespace });
|
|
146
|
+
res.status(200).json({
|
|
147
|
+
success: true,
|
|
148
|
+
count: result.deletedCount,
|
|
149
|
+
message: "All questions deleted successfully"
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
} catch (error) {
|
|
153
|
+
winston.error('Error deleting answered questions:', error);
|
|
154
|
+
res.status(500).json({
|
|
155
|
+
success: false,
|
|
156
|
+
error: "Error deleting answered questions"
|
|
157
|
+
});
|
|
158
|
+
}
|
|
179
159
|
})
|
|
180
160
|
|
|
181
161
|
router.get('/count/:namespace', async (req, res) => {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const count = await AnsweredQuestion.countDocuments({ id_project, namespace });
|
|
203
|
-
res.status(200).json({ count });
|
|
204
|
-
|
|
205
|
-
} catch (error) {
|
|
206
|
-
winston.error('Error counting answered questions:', error);
|
|
207
|
-
res.status(500).json({
|
|
208
|
-
success: false,
|
|
209
|
-
error: "Error counting answered questions"
|
|
210
|
-
});
|
|
162
|
+
try {
|
|
163
|
+
const { namespace } = req.params;
|
|
164
|
+
const id_project = req.projectid;
|
|
165
|
+
|
|
166
|
+
if (!namespace) {
|
|
167
|
+
return res.status(400).json({
|
|
168
|
+
success: false,
|
|
169
|
+
error: "Missing required parameter: namespace"
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Check if namespace belongs to project
|
|
174
|
+
const isValidNamespace = await validateNamespace(id_project, namespace);
|
|
175
|
+
if (!isValidNamespace) {
|
|
176
|
+
return res.status(403).json({
|
|
177
|
+
success: false,
|
|
178
|
+
error: "Not allowed. The namespace does not belong to the current project."
|
|
179
|
+
});
|
|
211
180
|
}
|
|
181
|
+
|
|
182
|
+
const count = await AnsweredQuestion.countDocuments({ id_project, namespace });
|
|
183
|
+
res.status(200).json({ count });
|
|
184
|
+
|
|
185
|
+
} catch (error) {
|
|
186
|
+
winston.error('Error counting answered questions:', error);
|
|
187
|
+
res.status(500).json({
|
|
188
|
+
success: false,
|
|
189
|
+
error: "Error counting answered questions"
|
|
190
|
+
});
|
|
191
|
+
}
|
|
212
192
|
})
|
|
213
193
|
|
|
214
194
|
router.get('/:namespace/export', async (req, res) => {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const questions = await AnsweredQuestion.find({ id_project, namespace })
|
|
236
|
-
.sort({ createdAt: -1 })
|
|
237
|
-
.lean();
|
|
238
|
-
|
|
239
|
-
const safeFilename = String(namespace).replace(/[^\w.-]+/g, '_') || 'export';
|
|
240
|
-
|
|
241
|
-
if (mode === 'json') {
|
|
242
|
-
const questionsJson = questions.map((q) => {
|
|
243
|
-
const { __v, updatedAt, ...doc } = q;
|
|
244
|
-
return doc;
|
|
245
|
-
});
|
|
246
|
-
const payload = {
|
|
247
|
-
namespace,
|
|
248
|
-
exportedAt: new Date().toISOString(),
|
|
249
|
-
count: questionsJson.length,
|
|
250
|
-
questions: questionsJson
|
|
251
|
-
};
|
|
252
|
-
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
253
|
-
res.setHeader('Content-Disposition', `attachment; filename="answered-${safeFilename}.json"`);
|
|
254
|
-
return res.send(JSON.stringify(payload, null, 2));
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
|
258
|
-
res.setHeader('Content-Disposition', `attachment; filename="answered-${safeFilename}.csv"`);
|
|
259
|
-
|
|
260
|
-
const csvStream = fastCsv.format({ headers: true });
|
|
261
|
-
csvStream.pipe(res);
|
|
262
|
-
|
|
263
|
-
for (const q of questions) {
|
|
264
|
-
csvStream.write({
|
|
265
|
-
id: String(q._id),
|
|
266
|
-
namespace: q.namespace,
|
|
267
|
-
question: q.question,
|
|
268
|
-
answer: q.answer,
|
|
269
|
-
tokens: q.tokens != null ? q.tokens : '',
|
|
270
|
-
request_id: q.request_id || '',
|
|
271
|
-
createdAt: q.createdAt ? new Date(q.createdAt).toISOString() : ''
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
csvStream.end();
|
|
275
|
-
} catch (error) {
|
|
276
|
-
winston.error('Error exporting answered questions:', error);
|
|
277
|
-
if (!res.headersSent) {
|
|
278
|
-
res.status(500).json({
|
|
279
|
-
success: false,
|
|
280
|
-
error: "Error exporting answered questions"
|
|
281
|
-
});
|
|
282
|
-
}
|
|
195
|
+
try {
|
|
196
|
+
const { namespace } = req.params;
|
|
197
|
+
const id_project = req.projectid;
|
|
198
|
+
const mode = String(req.query.mode || 'csv').toLowerCase();
|
|
199
|
+
|
|
200
|
+
if (mode !== 'csv' && mode !== 'json') {
|
|
201
|
+
return res.status(400).json({
|
|
202
|
+
success: false,
|
|
203
|
+
error: 'Invalid format. Use mode=json or mode=csv'
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const isValidNamespace = await validateNamespace(id_project, namespace);
|
|
208
|
+
if (!isValidNamespace) {
|
|
209
|
+
return res.status(403).json({
|
|
210
|
+
success: false,
|
|
211
|
+
error: "Not allowed. The namespace does not belong to the current project."
|
|
212
|
+
});
|
|
283
213
|
}
|
|
214
|
+
|
|
215
|
+
const questions = await AnsweredQuestion.find({ id_project, namespace })
|
|
216
|
+
.sort({ createdAt: -1 })
|
|
217
|
+
.lean();
|
|
218
|
+
|
|
219
|
+
const safeFilename = String(namespace).replace(/[^\w.-]+/g, '_') || 'export';
|
|
220
|
+
|
|
221
|
+
if (mode === 'json') {
|
|
222
|
+
const questionsJson = questions.map((q) => {
|
|
223
|
+
const { __v, updatedAt, ...doc } = q;
|
|
224
|
+
return doc;
|
|
225
|
+
});
|
|
226
|
+
const payload = {
|
|
227
|
+
namespace,
|
|
228
|
+
exportedAt: new Date().toISOString(),
|
|
229
|
+
count: questionsJson.length,
|
|
230
|
+
questions: questionsJson
|
|
231
|
+
};
|
|
232
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
233
|
+
res.setHeader('Content-Disposition', `attachment; filename="answered-${safeFilename}.json"`);
|
|
234
|
+
return res.send(JSON.stringify(payload, null, 2));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
res.setHeader('Content-Type', 'text/csv; charset=utf-8');
|
|
238
|
+
res.setHeader('Content-Disposition', `attachment; filename="answered-${safeFilename}.csv"`);
|
|
239
|
+
|
|
240
|
+
const csvStream = fastCsv.format({ headers: true });
|
|
241
|
+
csvStream.pipe(res);
|
|
242
|
+
|
|
243
|
+
for (const q of questions) {
|
|
244
|
+
csvStream.write({
|
|
245
|
+
id: String(q._id),
|
|
246
|
+
namespace: q.namespace,
|
|
247
|
+
question: q.question,
|
|
248
|
+
answer: q.answer,
|
|
249
|
+
tokens: q.tokens != null ? q.tokens : '',
|
|
250
|
+
request_id: q.request_id || '',
|
|
251
|
+
createdAt: q.createdAt ? new Date(q.createdAt).toISOString() : ''
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
csvStream.end();
|
|
255
|
+
} catch (error) {
|
|
256
|
+
winston.error('Error exporting answered questions:', error);
|
|
257
|
+
if (!res.headersSent) {
|
|
258
|
+
res.status(500).json({
|
|
259
|
+
success: false,
|
|
260
|
+
error: "Error exporting answered questions"
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
284
264
|
});
|
|
285
265
|
|
|
286
266
|
// Helper function to validate namespace
|
|
287
267
|
async function validateNamespace(id_project, namespace_id) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
268
|
+
try {
|
|
269
|
+
const namespace = await Namespace.findOne({
|
|
270
|
+
id_project: id_project,
|
|
271
|
+
id: namespace_id
|
|
272
|
+
});
|
|
273
|
+
return !!namespace;
|
|
274
|
+
} catch (err) {
|
|
275
|
+
winston.error('validate namespace error: ', err);
|
|
276
|
+
throw err;
|
|
277
|
+
}
|
|
298
278
|
}
|
|
299
|
-
|
|
300
|
-
module.exports = router;
|
|
279
|
+
module.exports = router;
|