collabdocchat 1.2.13 → 2.0.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.
Files changed (62) hide show
  1. package/README.md +219 -218
  2. package/index.html +2 -0
  3. package/install-and-start.bat +5 -0
  4. package/install-and-start.sh +5 -0
  5. package/package.json +9 -2
  6. package/scripts/pre-publish-check.js +213 -0
  7. package/scripts/start-app.js +15 -15
  8. package/server/index.js +38 -6
  9. package/server/middleware/cache.js +115 -0
  10. package/server/middleware/errorHandler.js +209 -0
  11. package/server/models/Document.js +66 -59
  12. package/server/models/File.js +49 -43
  13. package/server/models/Group.js +6 -0
  14. package/server/models/KnowledgeBase.js +254 -0
  15. package/server/models/Message.js +43 -0
  16. package/server/models/Task.js +87 -55
  17. package/server/models/User.js +67 -60
  18. package/server/models/Workflow.js +249 -0
  19. package/server/routes/ai.js +327 -0
  20. package/server/routes/audit.js +245 -210
  21. package/server/routes/backup.js +108 -0
  22. package/server/routes/chunked-upload.js +343 -0
  23. package/server/routes/export.js +440 -0
  24. package/server/routes/files.js +294 -218
  25. package/server/routes/groups.js +182 -0
  26. package/server/routes/knowledge.js +509 -0
  27. package/server/routes/tasks.js +257 -110
  28. package/server/routes/workflows.js +380 -0
  29. package/server/utils/backup.js +439 -0
  30. package/server/utils/cache.js +223 -0
  31. package/server/utils/workflow-engine.js +479 -0
  32. package/server/websocket/enhanced.js +509 -0
  33. package/server/websocket/index.js +233 -1
  34. package/src/components/knowledge-modal.js +485 -0
  35. package/src/components/optimized-poll-detail.js +724 -0
  36. package/src/main.js +5 -0
  37. package/src/pages/admin-dashboard.js +2248 -44
  38. package/src/pages/optimized-backup-view.js +616 -0
  39. package/src/pages/optimized-knowledge-view.js +803 -0
  40. package/src/pages/optimized-task-detail.js +843 -0
  41. package/src/pages/optimized-workflow-view.js +806 -0
  42. package/src/pages/simplified-workflows.js +651 -0
  43. package/src/pages/user-dashboard.js +677 -58
  44. package/src/services/api.js +65 -1
  45. package/src/services/auth.js +1 -1
  46. package/src/services/websocket.js +124 -16
  47. package/src/styles/collaboration-modern.js +708 -0
  48. package/src/styles/enhancements.css +392 -0
  49. package/src/styles/main.css +620 -1420
  50. package/src/styles/responsive.css +1000 -0
  51. package/src/styles/sidebar-fix.css +60 -0
  52. package/src/utils/ai-assistant.js +1398 -0
  53. package/src/utils/chat-enhancements.js +509 -0
  54. package/src/utils/collaboration-enhancer.js +1151 -0
  55. package/src/utils/feature-integrator.js +1724 -0
  56. package/src/utils/onboarding-guide.js +734 -0
  57. package/src/utils/performance.js +394 -0
  58. package/src/utils/permission-manager.js +890 -0
  59. package/src/utils/responsive-handler.js +491 -0
  60. package/src/utils/theme-manager.js +811 -0
  61. package/src/utils/ui-enhancements-loader.js +329 -0
  62. package/USAGE.md +0 -298
@@ -0,0 +1,440 @@
1
+ import express from 'express';
2
+ import archiver from 'archiver';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { dirname } from 'path';
7
+ import Message from '../models/Message.js';
8
+ import Document from '../models/Document.js';
9
+ import Task from '../models/Task.js';
10
+ import File from '../models/File.js';
11
+ import Group from '../models/Group.js';
12
+ import User from '../models/User.js';
13
+ import KnowledgeBase from '../models/KnowledgeBase.js';
14
+ import { authenticate, isAdmin } from '../middleware/auth.js';
15
+ import { asyncHandler, ValidationError, NotFoundError, AuthorizationError } from '../middleware/errorHandler.js';
16
+
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = dirname(__filename);
19
+
20
+ const router = express.Router();
21
+
22
+ // 确保导出目录存在
23
+ const exportsDir = path.join(__dirname, '../../exports');
24
+ if (!fs.existsSync(exportsDir)) {
25
+ fs.mkdirSync(exportsDir, { recursive: true });
26
+ }
27
+
28
+ // 导出群组聊天记录(JSON格式)
29
+ router.get('/group/:groupId/messages/json', authenticate, asyncHandler(async (req, res) => {
30
+ const { startDate, endDate } = req.query;
31
+
32
+ // 验证群组
33
+ const group = await Group.findById(req.params.groupId);
34
+ if (!group) {
35
+ throw new NotFoundError('群组');
36
+ }
37
+
38
+ // 检查权限
39
+ const isMember = group.members.some(m => m.toString() === req.user.userId);
40
+ if (!isMember) {
41
+ throw new AuthorizationError('您不在该群组中');
42
+ }
43
+
44
+ // 构建查询
45
+ const query = { group: req.params.groupId };
46
+ if (startDate || endDate) {
47
+ query.timestamp = {};
48
+ if (startDate) query.timestamp.$gte = new Date(startDate);
49
+ if (endDate) query.timestamp.$lte = new Date(endDate);
50
+ }
51
+
52
+ const messages = await Message.find(query)
53
+ .populate('sender', 'username email')
54
+ .sort({ timestamp: 1 });
55
+
56
+ // 设置响应头
57
+ const filename = `messages_${group.name}_${Date.now()}.json`;
58
+ res.setHeader('Content-Type', 'application/json');
59
+ res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(filename)}"`);
60
+
61
+ res.json({
62
+ group: {
63
+ id: group._id,
64
+ name: group.name,
65
+ description: group.description
66
+ },
67
+ exportDate: new Date().toISOString(),
68
+ messageCount: messages.length,
69
+ messages: messages.map(msg => ({
70
+ id: msg._id,
71
+ sender: msg.sender ? {
72
+ id: msg.sender._id,
73
+ username: msg.sender.username,
74
+ email: msg.sender.email
75
+ } : null,
76
+ content: msg.content,
77
+ timestamp: msg.timestamp,
78
+ isRecalled: msg.isRecalled,
79
+ mentions: msg.mentions,
80
+ messageType: msg.messageType
81
+ }))
82
+ });
83
+ }));
84
+
85
+ // 导出群组聊天记录(TXT格式)
86
+ router.get('/group/:groupId/messages/txt', authenticate, asyncHandler(async (req, res) => {
87
+ const { startDate, endDate } = req.query;
88
+
89
+ // 验证群组
90
+ const group = await Group.findById(req.params.groupId);
91
+ if (!group) {
92
+ throw new NotFoundError('群组');
93
+ }
94
+
95
+ // 检查权限
96
+ const isMember = group.members.some(m => m.toString() === req.user.userId);
97
+ if (!isMember) {
98
+ throw new AuthorizationError('您不在该群组中');
99
+ }
100
+
101
+ // 构建查询
102
+ const query = { group: req.params.groupId };
103
+ if (startDate || endDate) {
104
+ query.timestamp = {};
105
+ if (startDate) query.timestamp.$gte = new Date(startDate);
106
+ if (endDate) query.timestamp.$lte = new Date(endDate);
107
+ }
108
+
109
+ const messages = await Message.find(query)
110
+ .populate('sender', 'username')
111
+ .sort({ timestamp: 1 });
112
+
113
+ // 生成文本内容
114
+ let content = `群组聊天记录导出\n`;
115
+ content += `群组名称: ${group.name}\n`;
116
+ content += `导出时间: ${new Date().toLocaleString('zh-CN')}\n`;
117
+ content += `消息数量: ${messages.length}\n`;
118
+ content += `\n${'='.repeat(80)}\n\n`;
119
+
120
+ messages.forEach(msg => {
121
+ const time = new Date(msg.timestamp).toLocaleString('zh-CN');
122
+ const sender = msg.sender ? msg.sender.username : '未知用户';
123
+ content += `[${time}] ${sender}:\n${msg.content}\n\n`;
124
+ });
125
+
126
+ // 设置响应头
127
+ const filename = `messages_${group.name}_${Date.now()}.txt`;
128
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
129
+ res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(filename)}"`);
130
+
131
+ res.send(content);
132
+ }));
133
+
134
+ // 导出群组文档列表
135
+ router.get('/group/:groupId/documents', authenticate, asyncHandler(async (req, res) => {
136
+ // 验证群组
137
+ const group = await Group.findById(req.params.groupId);
138
+ if (!group) {
139
+ throw new NotFoundError('群组');
140
+ }
141
+
142
+ // 检查权限
143
+ const isMember = group.members.some(m => m.toString() === req.user.userId);
144
+ if (!isMember) {
145
+ throw new AuthorizationError('您不在该群组中');
146
+ }
147
+
148
+ const documents = await Document.find({ group: req.params.groupId })
149
+ .populate('creator', 'username email')
150
+ .sort({ createdAt: -1 });
151
+
152
+ const filename = `documents_${group.name}_${Date.now()}.json`;
153
+ res.setHeader('Content-Type', 'application/json');
154
+ res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(filename)}"`);
155
+
156
+ res.json({
157
+ group: {
158
+ id: group._id,
159
+ name: group.name
160
+ },
161
+ exportDate: new Date().toISOString(),
162
+ documentCount: documents.length,
163
+ documents: documents.map(doc => ({
164
+ id: doc._id,
165
+ title: doc.title,
166
+ content: doc.content,
167
+ creator: doc.creator ? {
168
+ id: doc.creator._id,
169
+ username: doc.creator.username,
170
+ email: doc.creator.email
171
+ } : null,
172
+ createdAt: doc.createdAt,
173
+ updatedAt: doc.updatedAt
174
+ }))
175
+ });
176
+ }));
177
+
178
+ // 导出群组任务列表
179
+ router.get('/group/:groupId/tasks', authenticate, asyncHandler(async (req, res) => {
180
+ // 验证群组
181
+ const group = await Group.findById(req.params.groupId);
182
+ if (!group) {
183
+ throw new NotFoundError('群组');
184
+ }
185
+
186
+ // 检查权限
187
+ const isMember = group.members.some(m => m.toString() === req.user.userId);
188
+ if (!isMember) {
189
+ throw new AuthorizationError('您不在该群组中');
190
+ }
191
+
192
+ const tasks = await Task.find({ group: req.params.groupId })
193
+ .populate('creator', 'username email')
194
+ .populate('assignee', 'username email')
195
+ .sort({ createdAt: -1 });
196
+
197
+ const filename = `tasks_${group.name}_${Date.now()}.json`;
198
+ res.setHeader('Content-Type', 'application/json');
199
+ res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(filename)}"`);
200
+
201
+ res.json({
202
+ group: {
203
+ id: group._id,
204
+ name: group.name
205
+ },
206
+ exportDate: new Date().toISOString(),
207
+ taskCount: tasks.length,
208
+ tasks: tasks.map(task => ({
209
+ id: task._id,
210
+ title: task.title,
211
+ description: task.description,
212
+ status: task.status,
213
+ priority: task.priority,
214
+ creator: task.creator ? {
215
+ id: task.creator._id,
216
+ username: task.creator.username,
217
+ email: task.creator.email
218
+ } : null,
219
+ assignee: task.assignee ? {
220
+ id: task.assignee._id,
221
+ username: task.assignee.username,
222
+ email: task.assignee.email
223
+ } : null,
224
+ dueDate: task.dueDate,
225
+ createdAt: task.createdAt,
226
+ updatedAt: task.updatedAt
227
+ }))
228
+ });
229
+ }));
230
+
231
+ // 导出知识库文档
232
+ router.get('/group/:groupId/knowledge', authenticate, asyncHandler(async (req, res) => {
233
+ // 验证群组
234
+ const group = await Group.findById(req.params.groupId);
235
+ if (!group) {
236
+ throw new NotFoundError('群组');
237
+ }
238
+
239
+ // 检查权限
240
+ const isMember = group.members.some(m => m.toString() === req.user.userId);
241
+ if (!isMember) {
242
+ throw new AuthorizationError('您不在该群组中');
243
+ }
244
+
245
+ const knowledgeList = await KnowledgeBase.find({
246
+ group: req.params.groupId,
247
+ status: 'published'
248
+ })
249
+ .populate('author', 'username email')
250
+ .sort({ createdAt: -1 });
251
+
252
+ const filename = `knowledge_${group.name}_${Date.now()}.json`;
253
+ res.setHeader('Content-Type', 'application/json');
254
+ res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(filename)}"`);
255
+
256
+ res.json({
257
+ group: {
258
+ id: group._id,
259
+ name: group.name
260
+ },
261
+ exportDate: new Date().toISOString(),
262
+ knowledgeCount: knowledgeList.length,
263
+ knowledge: knowledgeList.map(kb => ({
264
+ id: kb._id,
265
+ title: kb.title,
266
+ content: kb.content,
267
+ category: kb.category,
268
+ tags: kb.tags,
269
+ author: kb.author ? {
270
+ id: kb.author._id,
271
+ username: kb.author.username,
272
+ email: kb.author.email
273
+ } : null,
274
+ views: kb.views,
275
+ likesCount: kb.likes.length,
276
+ createdAt: kb.createdAt,
277
+ updatedAt: kb.updatedAt
278
+ }))
279
+ });
280
+ }));
281
+
282
+ // 导出群组完整数据(ZIP压缩包)
283
+ router.get('/group/:groupId/full', authenticate, asyncHandler(async (req, res) => {
284
+ // 验证群组
285
+ const group = await Group.findById(req.params.groupId)
286
+ .populate('admin', 'username email')
287
+ .populate('members', 'username email');
288
+
289
+ if (!group) {
290
+ throw new NotFoundError('群组');
291
+ }
292
+
293
+ // 检查权限(只有管理员可以导出完整数据)
294
+ const isGroupAdmin = group.admin._id.toString() === req.user.userId;
295
+ if (!isGroupAdmin && req.user.role !== 'admin') {
296
+ throw new AuthorizationError('只有管理员可以导出完整数据');
297
+ }
298
+
299
+ // 获取所有数据
300
+ const [messages, documents, tasks, files, knowledgeList] = await Promise.all([
301
+ Message.find({ group: req.params.groupId }).populate('sender', 'username email'),
302
+ Document.find({ group: req.params.groupId }).populate('creator', 'username email'),
303
+ Task.find({ group: req.params.groupId }).populate('creator assignee', 'username email'),
304
+ File.find({ group: req.params.groupId }).populate('uploader', 'username email'),
305
+ KnowledgeBase.find({ group: req.params.groupId }).populate('author', 'username email')
306
+ ]);
307
+
308
+ // 创建ZIP文件
309
+ const filename = `group_${group.name}_${Date.now()}.zip`;
310
+ const zipPath = path.join(exportsDir, filename);
311
+
312
+ res.setHeader('Content-Type', 'application/zip');
313
+ res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(filename)}"`);
314
+
315
+ const archive = archiver('zip', {
316
+ zlib: { level: 9 }
317
+ });
318
+
319
+ archive.on('error', (err) => {
320
+ throw err;
321
+ });
322
+
323
+ archive.pipe(res);
324
+
325
+ // 添加群组信息
326
+ archive.append(JSON.stringify({
327
+ id: group._id,
328
+ name: group.name,
329
+ description: group.description,
330
+ admin: group.admin,
331
+ members: group.members,
332
+ createdAt: group.createdAt
333
+ }, null, 2), { name: 'group_info.json' });
334
+
335
+ // 添加消息
336
+ archive.append(JSON.stringify(messages, null, 2), { name: 'messages.json' });
337
+
338
+ // 添加文档
339
+ archive.append(JSON.stringify(documents, null, 2), { name: 'documents.json' });
340
+
341
+ // 添加任务
342
+ archive.append(JSON.stringify(tasks, null, 2), { name: 'tasks.json' });
343
+
344
+ // 添加文件列表
345
+ archive.append(JSON.stringify(files.map(f => ({
346
+ id: f._id,
347
+ filename: f.filename,
348
+ originalName: f.originalName,
349
+ size: f.size,
350
+ mimetype: f.mimetype,
351
+ uploader: f.uploader,
352
+ createdAt: f.createdAt
353
+ })), null, 2), { name: 'files.json' });
354
+
355
+ // 添加知识库
356
+ archive.append(JSON.stringify(knowledgeList, null, 2), { name: 'knowledge.json' });
357
+
358
+ // 添加README
359
+ const readme = `# ${group.name} 数据导出
360
+
361
+ 导出时间: ${new Date().toLocaleString('zh-CN')}
362
+ 导出人: ${req.user.username}
363
+
364
+ ## 包含内容
365
+
366
+ - group_info.json: 群组基本信息
367
+ - messages.json: 聊天记录 (${messages.length} 条)
368
+ - documents.json: 文档列表 (${documents.length} 个)
369
+ - tasks.json: 任务列表 (${tasks.length} 个)
370
+ - files.json: 文件列表 (${files.length} 个)
371
+ - knowledge.json: 知识库 (${knowledgeList.length} 个)
372
+
373
+ ## 注意事项
374
+
375
+ 1. 此导出不包含实际的上传文件,仅包含文件元数据
376
+ 2. 所有数据均为JSON格式,可使用文本编辑器或JSON查看器打开
377
+ 3. 请妥善保管导出数据,避免泄露敏感信息
378
+ `;
379
+
380
+ archive.append(readme, { name: 'README.md' });
381
+
382
+ await archive.finalize();
383
+ }));
384
+
385
+ // 导出用户个人数据
386
+ router.get('/user/my-data', authenticate, asyncHandler(async (req, res) => {
387
+ const user = await User.findById(req.user.userId);
388
+ if (!user) {
389
+ throw new NotFoundError('用户');
390
+ }
391
+
392
+ // 获取用户相关数据
393
+ const [myMessages, myDocuments, myTasks, myFiles, myKnowledge] = await Promise.all([
394
+ Message.find({ sender: req.user.userId }).populate('group', 'name'),
395
+ Document.find({ creator: req.user.userId }).populate('group', 'name'),
396
+ Task.find({ $or: [{ creator: req.user.userId }, { assignee: req.user.userId }] }).populate('group', 'name'),
397
+ File.find({ uploader: req.user.userId }).populate('group', 'name'),
398
+ KnowledgeBase.find({ author: req.user.userId }).populate('group', 'name')
399
+ ]);
400
+
401
+ const filename = `my_data_${Date.now()}.json`;
402
+ res.setHeader('Content-Type', 'application/json');
403
+ res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(filename)}"`);
404
+
405
+ res.json({
406
+ user: {
407
+ id: user._id,
408
+ username: user.username,
409
+ email: user.email,
410
+ role: user.role,
411
+ createdAt: user.createdAt
412
+ },
413
+ exportDate: new Date().toISOString(),
414
+ statistics: {
415
+ messageCount: myMessages.length,
416
+ documentCount: myDocuments.length,
417
+ taskCount: myTasks.length,
418
+ fileCount: myFiles.length,
419
+ knowledgeCount: myKnowledge.length
420
+ },
421
+ data: {
422
+ messages: myMessages,
423
+ documents: myDocuments,
424
+ tasks: myTasks,
425
+ files: myFiles.map(f => ({
426
+ id: f._id,
427
+ filename: f.filename,
428
+ originalName: f.originalName,
429
+ size: f.size,
430
+ mimetype: f.mimetype,
431
+ group: f.group,
432
+ createdAt: f.createdAt
433
+ })),
434
+ knowledge: myKnowledge
435
+ }
436
+ });
437
+ }));
438
+
439
+ export default router;
440
+