collabdocchat 1.2.13 → 2.0.1
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 +219 -218
- package/index.html +2 -0
- package/install-and-start.bat +5 -0
- package/install-and-start.sh +5 -0
- package/package.json +9 -2
- package/scripts/generate-docs.js +448 -0
- package/scripts/pre-publish-check.js +213 -0
- package/scripts/start-app.js +15 -15
- package/server/index.js +38 -6
- package/server/middleware/cache.js +115 -0
- package/server/middleware/errorHandler.js +209 -0
- package/server/models/Document.js +66 -59
- package/server/models/File.js +49 -43
- package/server/models/Group.js +6 -0
- package/server/models/KnowledgeBase.js +254 -0
- package/server/models/Message.js +43 -0
- package/server/models/Task.js +87 -55
- package/server/models/User.js +67 -60
- package/server/models/Workflow.js +249 -0
- package/server/routes/ai.js +327 -0
- package/server/routes/audit.js +245 -210
- package/server/routes/backup.js +108 -0
- package/server/routes/chunked-upload.js +343 -0
- package/server/routes/export.js +440 -0
- package/server/routes/files.js +294 -218
- package/server/routes/groups.js +182 -0
- package/server/routes/knowledge.js +509 -0
- package/server/routes/tasks.js +257 -110
- package/server/routes/workflows.js +380 -0
- package/server/utils/backup.js +439 -0
- package/server/utils/cache.js +223 -0
- package/server/utils/workflow-engine.js +479 -0
- package/server/websocket/enhanced.js +509 -0
- package/server/websocket/index.js +233 -1
- package/src/components/knowledge-modal.js +485 -0
- package/src/components/optimized-poll-detail.js +724 -0
- package/src/main.js +5 -0
- package/src/pages/admin-dashboard.js +2248 -44
- package/src/pages/optimized-backup-view.js +616 -0
- package/src/pages/optimized-knowledge-view.js +803 -0
- package/src/pages/optimized-task-detail.js +843 -0
- package/src/pages/optimized-workflow-view.js +806 -0
- package/src/pages/simplified-workflows.js +651 -0
- package/src/pages/user-dashboard.js +677 -58
- package/src/services/api.js +64 -0
- package/src/services/auth.js +1 -1
- package/src/services/websocket.js +124 -16
- package/src/styles/collaboration-modern.js +708 -0
- package/src/styles/enhancements.css +392 -0
- package/src/styles/main.css +620 -1420
- package/src/styles/responsive.css +1000 -0
- package/src/styles/sidebar-fix.css +60 -0
- package/src/utils/ai-assistant.js +1398 -0
- package/src/utils/chat-enhancements.js +509 -0
- package/src/utils/collaboration-enhancer.js +1151 -0
- package/src/utils/feature-integrator.js +1724 -0
- package/src/utils/onboarding-guide.js +734 -0
- package/src/utils/performance.js +394 -0
- package/src/utils/permission-manager.js +890 -0
- package/src/utils/responsive-handler.js +491 -0
- package/src/utils/theme-manager.js +811 -0
- package/src/utils/ui-enhancements-loader.js +329 -0
- package/USAGE.md +0 -298
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// 分片上传路由
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import multer from 'multer';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { dirname } from 'path';
|
|
8
|
+
import crypto from 'crypto';
|
|
9
|
+
import { authenticate } from '../middleware/auth.js';
|
|
10
|
+
import File from '../models/File.js';
|
|
11
|
+
import Group from '../models/Group.js';
|
|
12
|
+
import { asyncHandler, ValidationError, NotFoundError } from '../middleware/errorHandler.js';
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = dirname(__filename);
|
|
16
|
+
|
|
17
|
+
const router = express.Router();
|
|
18
|
+
|
|
19
|
+
// 临时分片存储目录
|
|
20
|
+
const chunksDir = path.join(__dirname, '../../uploads/chunks');
|
|
21
|
+
const uploadsDir = path.join(__dirname, '../../uploads');
|
|
22
|
+
|
|
23
|
+
// 确保目录存在
|
|
24
|
+
[chunksDir, uploadsDir].forEach(dir => {
|
|
25
|
+
if (!fs.existsSync(dir)) {
|
|
26
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// 上传会话存储(生产环境应使用 Redis)
|
|
31
|
+
const uploadSessions = new Map();
|
|
32
|
+
|
|
33
|
+
// 配置 multer 用于分片上传
|
|
34
|
+
const chunkStorage = multer.diskStorage({
|
|
35
|
+
destination: (req, file, cb) => {
|
|
36
|
+
const sessionId = req.body.sessionId;
|
|
37
|
+
const sessionDir = path.join(chunksDir, sessionId);
|
|
38
|
+
|
|
39
|
+
if (!fs.existsSync(sessionDir)) {
|
|
40
|
+
fs.mkdirSync(sessionDir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
cb(null, sessionDir);
|
|
44
|
+
},
|
|
45
|
+
filename: (req, file, cb) => {
|
|
46
|
+
const chunkIndex = req.body.chunkIndex;
|
|
47
|
+
cb(null, `chunk-${chunkIndex}`);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const chunkUpload = multer({
|
|
52
|
+
storage: chunkStorage,
|
|
53
|
+
limits: {
|
|
54
|
+
fileSize: 5 * 1024 * 1024 // 每个分片最大 5MB
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// 生成唯一会话ID
|
|
59
|
+
const generateSessionId = () => {
|
|
60
|
+
return crypto.randomBytes(16).toString('hex');
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// 初始化分片上传
|
|
64
|
+
router.post('/init', authenticate, asyncHandler(async (req, res) => {
|
|
65
|
+
const { filename, fileSize, totalChunks, groupId, mimetype } = req.body;
|
|
66
|
+
|
|
67
|
+
// 验证参数
|
|
68
|
+
if (!filename || !fileSize || !totalChunks || !groupId) {
|
|
69
|
+
throw new ValidationError('缺少必要参数');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 验证群组
|
|
73
|
+
const group = await Group.findById(groupId);
|
|
74
|
+
if (!group) {
|
|
75
|
+
throw new NotFoundError('群组');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 检查用户是否在群组中
|
|
79
|
+
const isMember = group.members.some(m => m.toString() === req.user.userId);
|
|
80
|
+
if (!isMember) {
|
|
81
|
+
throw new ValidationError('您不在该群组中');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 创建上传会话
|
|
85
|
+
const sessionId = generateSessionId();
|
|
86
|
+
const session = {
|
|
87
|
+
id: sessionId,
|
|
88
|
+
filename,
|
|
89
|
+
fileSize,
|
|
90
|
+
totalChunks,
|
|
91
|
+
uploadedChunks: new Set(),
|
|
92
|
+
groupId,
|
|
93
|
+
userId: req.user.userId,
|
|
94
|
+
mimetype,
|
|
95
|
+
createdAt: Date.now(),
|
|
96
|
+
expiresAt: Date.now() + 24 * 60 * 60 * 1000 // 24小时后过期
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
uploadSessions.set(sessionId, session);
|
|
100
|
+
|
|
101
|
+
res.json({
|
|
102
|
+
success: true,
|
|
103
|
+
data: {
|
|
104
|
+
sessionId,
|
|
105
|
+
chunkSize: 5 * 1024 * 1024, // 5MB
|
|
106
|
+
expiresAt: session.expiresAt
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
// 上传分片
|
|
112
|
+
router.post('/chunk', authenticate, chunkUpload.single('chunk'), asyncHandler(async (req, res) => {
|
|
113
|
+
const { sessionId, chunkIndex, chunkHash } = req.body;
|
|
114
|
+
|
|
115
|
+
if (!sessionId || chunkIndex === undefined) {
|
|
116
|
+
throw new ValidationError('缺少必要参数');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 获取上传会话
|
|
120
|
+
const session = uploadSessions.get(sessionId);
|
|
121
|
+
if (!session) {
|
|
122
|
+
throw new NotFoundError('上传会话');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 验证用户
|
|
126
|
+
if (session.userId !== req.user.userId) {
|
|
127
|
+
throw new ValidationError('无权访问此上传会话');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 检查会话是否过期
|
|
131
|
+
if (Date.now() > session.expiresAt) {
|
|
132
|
+
uploadSessions.delete(sessionId);
|
|
133
|
+
throw new ValidationError('上传会话已过期');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 验证分片哈希(可选,用于完整性检查)
|
|
137
|
+
if (chunkHash && req.file) {
|
|
138
|
+
const fileBuffer = fs.readFileSync(req.file.path);
|
|
139
|
+
const hash = crypto.createHash('md5').update(fileBuffer).digest('hex');
|
|
140
|
+
|
|
141
|
+
if (hash !== chunkHash) {
|
|
142
|
+
fs.unlinkSync(req.file.path);
|
|
143
|
+
throw new ValidationError('分片数据损坏');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 记录已上传的分片
|
|
148
|
+
session.uploadedChunks.add(parseInt(chunkIndex));
|
|
149
|
+
|
|
150
|
+
// 计算上传进度
|
|
151
|
+
const progress = (session.uploadedChunks.size / session.totalChunks) * 100;
|
|
152
|
+
|
|
153
|
+
res.json({
|
|
154
|
+
success: true,
|
|
155
|
+
data: {
|
|
156
|
+
chunkIndex: parseInt(chunkIndex),
|
|
157
|
+
uploaded: true,
|
|
158
|
+
progress: Math.round(progress),
|
|
159
|
+
uploadedChunks: session.uploadedChunks.size,
|
|
160
|
+
totalChunks: session.totalChunks
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}));
|
|
164
|
+
|
|
165
|
+
// 完成上传(合并分片)
|
|
166
|
+
router.post('/complete', authenticate, asyncHandler(async (req, res) => {
|
|
167
|
+
const { sessionId, description } = req.body;
|
|
168
|
+
|
|
169
|
+
if (!sessionId) {
|
|
170
|
+
throw new ValidationError('缺少会话ID');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 获取上传会话
|
|
174
|
+
const session = uploadSessions.get(sessionId);
|
|
175
|
+
if (!session) {
|
|
176
|
+
throw new NotFoundError('上传会话');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 验证用户
|
|
180
|
+
if (session.userId !== req.user.userId) {
|
|
181
|
+
throw new ValidationError('无权访问此上传会话');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 检查是否所有分片都已上传
|
|
185
|
+
if (session.uploadedChunks.size !== session.totalChunks) {
|
|
186
|
+
throw new ValidationError(`还有 ${session.totalChunks - session.uploadedChunks.size} 个分片未上传`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 合并分片
|
|
190
|
+
const sessionDir = path.join(chunksDir, sessionId);
|
|
191
|
+
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
|
192
|
+
const ext = path.extname(session.filename);
|
|
193
|
+
const finalFilename = uniqueSuffix + ext;
|
|
194
|
+
const finalPath = path.join(uploadsDir, finalFilename);
|
|
195
|
+
|
|
196
|
+
const writeStream = fs.createWriteStream(finalPath);
|
|
197
|
+
|
|
198
|
+
// 按顺序合并分片
|
|
199
|
+
for (let i = 0; i < session.totalChunks; i++) {
|
|
200
|
+
const chunkPath = path.join(sessionDir, `chunk-${i}`);
|
|
201
|
+
|
|
202
|
+
if (!fs.existsSync(chunkPath)) {
|
|
203
|
+
throw new ValidationError(`分片 ${i} 不存在`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const chunkBuffer = fs.readFileSync(chunkPath);
|
|
207
|
+
writeStream.write(chunkBuffer);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
writeStream.end();
|
|
211
|
+
|
|
212
|
+
// 等待写入完成
|
|
213
|
+
await new Promise((resolve, reject) => {
|
|
214
|
+
writeStream.on('finish', resolve);
|
|
215
|
+
writeStream.on('error', reject);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// 验证文件大小
|
|
219
|
+
const stats = fs.statSync(finalPath);
|
|
220
|
+
if (stats.size !== session.fileSize) {
|
|
221
|
+
fs.unlinkSync(finalPath);
|
|
222
|
+
throw new ValidationError('文件大小不匹配,上传失败');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 创建文件记录
|
|
226
|
+
const file = new File({
|
|
227
|
+
filename: finalFilename,
|
|
228
|
+
originalName: session.filename,
|
|
229
|
+
path: finalPath,
|
|
230
|
+
mimetype: session.mimetype,
|
|
231
|
+
size: stats.size,
|
|
232
|
+
group: session.groupId,
|
|
233
|
+
uploader: session.userId,
|
|
234
|
+
description: description || ''
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await file.save();
|
|
238
|
+
|
|
239
|
+
// 更新群组的文件列表
|
|
240
|
+
await Group.findByIdAndUpdate(session.groupId, {
|
|
241
|
+
$push: { files: file._id }
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// 清理临时文件和会话
|
|
245
|
+
try {
|
|
246
|
+
fs.rmSync(sessionDir, { recursive: true, force: true });
|
|
247
|
+
} catch (err) {
|
|
248
|
+
console.error('清理临时文件失败:', err);
|
|
249
|
+
}
|
|
250
|
+
uploadSessions.delete(sessionId);
|
|
251
|
+
|
|
252
|
+
res.json({
|
|
253
|
+
success: true,
|
|
254
|
+
message: '文件上传成功',
|
|
255
|
+
data: { file }
|
|
256
|
+
});
|
|
257
|
+
}));
|
|
258
|
+
|
|
259
|
+
// 取消上传
|
|
260
|
+
router.delete('/cancel/:sessionId', authenticate, asyncHandler(async (req, res) => {
|
|
261
|
+
const { sessionId } = req.params;
|
|
262
|
+
|
|
263
|
+
const session = uploadSessions.get(sessionId);
|
|
264
|
+
if (!session) {
|
|
265
|
+
throw new NotFoundError('上传会话');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 验证用户
|
|
269
|
+
if (session.userId !== req.user.userId) {
|
|
270
|
+
throw new ValidationError('无权访问此上传会话');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 清理临时文件
|
|
274
|
+
const sessionDir = path.join(chunksDir, sessionId);
|
|
275
|
+
try {
|
|
276
|
+
if (fs.existsSync(sessionDir)) {
|
|
277
|
+
fs.rmSync(sessionDir, { recursive: true, force: true });
|
|
278
|
+
}
|
|
279
|
+
} catch (err) {
|
|
280
|
+
console.error('清理临时文件失败:', err);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 删除会话
|
|
284
|
+
uploadSessions.delete(sessionId);
|
|
285
|
+
|
|
286
|
+
res.json({
|
|
287
|
+
success: true,
|
|
288
|
+
message: '上传已取消'
|
|
289
|
+
});
|
|
290
|
+
}));
|
|
291
|
+
|
|
292
|
+
// 获取上传进度
|
|
293
|
+
router.get('/progress/:sessionId', authenticate, asyncHandler(async (req, res) => {
|
|
294
|
+
const { sessionId } = req.params;
|
|
295
|
+
|
|
296
|
+
const session = uploadSessions.get(sessionId);
|
|
297
|
+
if (!session) {
|
|
298
|
+
throw new NotFoundError('上传会话');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 验证用户
|
|
302
|
+
if (session.userId !== req.user.userId) {
|
|
303
|
+
throw new ValidationError('无权访问此上传会话');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const progress = (session.uploadedChunks.size / session.totalChunks) * 100;
|
|
307
|
+
|
|
308
|
+
res.json({
|
|
309
|
+
success: true,
|
|
310
|
+
data: {
|
|
311
|
+
sessionId,
|
|
312
|
+
progress: Math.round(progress),
|
|
313
|
+
uploadedChunks: session.uploadedChunks.size,
|
|
314
|
+
totalChunks: session.totalChunks,
|
|
315
|
+
filename: session.filename,
|
|
316
|
+
fileSize: session.fileSize
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}));
|
|
320
|
+
|
|
321
|
+
// 定期清理过期会话(每小时执行一次)
|
|
322
|
+
setInterval(() => {
|
|
323
|
+
const now = Date.now();
|
|
324
|
+
for (const [sessionId, session] of uploadSessions.entries()) {
|
|
325
|
+
if (now > session.expiresAt) {
|
|
326
|
+
// 清理临时文件
|
|
327
|
+
const sessionDir = path.join(chunksDir, sessionId);
|
|
328
|
+
try {
|
|
329
|
+
if (fs.existsSync(sessionDir)) {
|
|
330
|
+
fs.rmSync(sessionDir, { recursive: true, force: true });
|
|
331
|
+
}
|
|
332
|
+
} catch (err) {
|
|
333
|
+
console.error('清理过期会话失败:', err);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
uploadSessions.delete(sessionId);
|
|
337
|
+
console.log(`清理过期上传会话: ${sessionId}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}, 60 * 60 * 1000); // 每小时
|
|
341
|
+
|
|
342
|
+
export default router;
|
|
343
|
+
|