collabdocchat 1.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.
@@ -0,0 +1,228 @@
1
+ import jwt from 'jsonwebtoken';
2
+ import * as Y from 'yjs';
3
+ import Group from '../models/Group.js';
4
+ import Message from '../models/Message.js';
5
+
6
+ const clients = new Map();
7
+ const documents = new Map();
8
+
9
+ export const setupWebSocket = (wss) => {
10
+ wss.on('connection', (ws, req) => {
11
+ console.log('🔌 新的 WebSocket 连接');
12
+
13
+ ws.on('message', async (message) => {
14
+ try {
15
+ const data = JSON.parse(message);
16
+
17
+ switch (data.type) {
18
+ case 'auth':
19
+ handleAuth(ws, data);
20
+ break;
21
+ case 'join_group':
22
+ await handleJoinGroup(ws, data);
23
+ break;
24
+ case 'chat_message':
25
+ await handleChatMessage(ws, data, wss);
26
+ break;
27
+ case 'document_sync':
28
+ handleDocumentSync(ws, data, wss);
29
+ break;
30
+ case 'call_response':
31
+ handleCallResponse(ws, data, wss);
32
+ break;
33
+ case 'typing':
34
+ handleTyping(ws, data, wss);
35
+ break;
36
+ default:
37
+ console.log('未知消息类型:', data.type);
38
+ }
39
+ } catch (error) {
40
+ console.error('WebSocket 消息处理错误:', error);
41
+ ws.send(JSON.stringify({ type: 'error', message: error.message }));
42
+ }
43
+ });
44
+
45
+ ws.on('close', () => {
46
+ handleDisconnect(ws, wss);
47
+ });
48
+ });
49
+ };
50
+
51
+ const handleAuth = (ws, data) => {
52
+ try {
53
+ const decoded = jwt.verify(data.token, process.env.JWT_SECRET);
54
+ ws.userId = decoded.userId;
55
+ ws.role = decoded.role;
56
+ clients.set(ws.userId, ws);
57
+
58
+ ws.send(JSON.stringify({
59
+ type: 'auth_success',
60
+ userId: ws.userId,
61
+ role: ws.role
62
+ }));
63
+ } catch (error) {
64
+ ws.send(JSON.stringify({ type: 'auth_error', message: '认证失败' }));
65
+ ws.close();
66
+ }
67
+ };
68
+
69
+ const handleJoinGroup = async (ws, data) => {
70
+ if (!ws.userId) {
71
+ ws.send(JSON.stringify({ type: 'error', message: '未认证,无法加入群组' }));
72
+ return;
73
+ }
74
+
75
+ const group = await Group.findById(data.groupId).select('members mutedAll mutedUsers');
76
+ if (!group) {
77
+ ws.send(JSON.stringify({ type: 'error', message: '群组不存在' }));
78
+ return;
79
+ }
80
+
81
+ const isMember = group.members.some(m => m.toString() === ws.userId);
82
+ if (!isMember) {
83
+ ws.send(JSON.stringify({ type: 'error', message: '你不在该群组中,请先加入群组' }));
84
+ return;
85
+ }
86
+
87
+ ws.groupId = data.groupId;
88
+ ws.send(JSON.stringify({
89
+ type: 'joined_group',
90
+ groupId: data.groupId
91
+ }));
92
+
93
+ ws.send(JSON.stringify({
94
+ type: 'mute_state',
95
+ groupId: data.groupId,
96
+ mutedAll: Boolean(group.mutedAll),
97
+ mutedUsers: (group.mutedUsers || []).map(u => u.toString())
98
+ }));
99
+ };
100
+
101
+ const handleChatMessage = async (ws, data, wss) => {
102
+ if (!ws.userId) {
103
+ ws.send(JSON.stringify({ type: 'error', message: '未认证,无法发送消息' }));
104
+ return;
105
+ }
106
+
107
+ if (!ws.groupId || ws.groupId !== data.groupId) {
108
+ ws.send(JSON.stringify({ type: 'chat_blocked', groupId: data.groupId, reason: 'not_in_group', message: '请先加入该群组再聊天' }));
109
+ return;
110
+ }
111
+
112
+ const group = await Group.findById(data.groupId).select('members admin mutedAll mutedUsers');
113
+ if (!group) {
114
+ ws.send(JSON.stringify({ type: 'error', message: '群组不存在' }));
115
+ return;
116
+ }
117
+
118
+ const isMember = group.members.some(m => m.toString() === ws.userId);
119
+ if (!isMember) {
120
+ ws.send(JSON.stringify({ type: 'chat_blocked', groupId: data.groupId, reason: 'not_in_group', message: '你不在该群组中,无法发言' }));
121
+ return;
122
+ }
123
+
124
+ const isAdmin = group.admin.toString() === ws.userId || ws.role === 'admin';
125
+ const isMutedUser = (group.mutedUsers || []).some(m => m.toString() === ws.userId);
126
+
127
+ if (!isAdmin && group.mutedAll) {
128
+ ws.send(JSON.stringify({ type: 'chat_blocked', groupId: data.groupId, reason: 'muted_all', message: '当前群组已开启全体禁言' }));
129
+ return;
130
+ }
131
+
132
+ if (!isAdmin && isMutedUser) {
133
+ ws.send(JSON.stringify({ type: 'chat_blocked', groupId: data.groupId, reason: 'muted_user', message: '你已被管理员禁言' }));
134
+ return;
135
+ }
136
+
137
+ // 保存消息到数据库
138
+ try {
139
+ const newMessage = new Message({
140
+ group: data.groupId,
141
+ sender: ws.userId,
142
+ username: data.username,
143
+ content: data.content,
144
+ timestamp: new Date()
145
+ });
146
+ await newMessage.save();
147
+ } catch (err) {
148
+ console.error('保存消息失败:', err);
149
+ }
150
+
151
+ const message = {
152
+ type: 'chat_message',
153
+ groupId: data.groupId,
154
+ userId: ws.userId,
155
+ username: data.username,
156
+ content: data.content,
157
+ timestamp: new Date().toISOString()
158
+ };
159
+
160
+ broadcastToGroup(wss, data.groupId, message);
161
+ };
162
+
163
+ const handleDocumentSync = (ws, data, wss) => {
164
+ const { documentId, content, cursorPosition } = data;
165
+
166
+ if (!documents.has(documentId)) {
167
+ documents.set(documentId, new Y.Doc());
168
+ }
169
+
170
+ const message = {
171
+ type: 'document_update',
172
+ documentId,
173
+ content,
174
+ userId: ws.userId,
175
+ cursorPosition,
176
+ timestamp: new Date().toISOString()
177
+ };
178
+
179
+ broadcastToGroup(wss, ws.groupId, message, ws);
180
+ };
181
+
182
+ const handleCallResponse = (ws, data, wss) => {
183
+ const message = {
184
+ type: 'call_response',
185
+ groupId: data.groupId,
186
+ userId: ws.userId,
187
+ username: data.username,
188
+ responded: true,
189
+ timestamp: new Date().toISOString()
190
+ };
191
+
192
+ broadcastToGroup(wss, data.groupId, message);
193
+ };
194
+
195
+ const handleTyping = (ws, data, wss) => {
196
+ const message = {
197
+ type: 'typing',
198
+ documentId: data.documentId,
199
+ userId: ws.userId,
200
+ username: data.username,
201
+ isTyping: data.isTyping
202
+ };
203
+
204
+ broadcastToGroup(wss, ws.groupId, message, ws);
205
+ };
206
+
207
+ const handleDisconnect = (ws, wss) => {
208
+ if (ws.userId) {
209
+ clients.delete(ws.userId);
210
+
211
+ if (ws.groupId) {
212
+ broadcastToGroup(wss, ws.groupId, {
213
+ type: 'user_offline',
214
+ userId: ws.userId
215
+ });
216
+ }
217
+ }
218
+ console.log('🔌 WebSocket 连接关闭');
219
+ };
220
+
221
+ const broadcastToGroup = (wss, groupId, message, excludeWs = null) => {
222
+ wss.clients.forEach(client => {
223
+ if (client.groupId === groupId && client !== excludeWs && client.readyState === 1) {
224
+ client.send(JSON.stringify(message));
225
+ }
226
+ });
227
+ };
228
+
package/src/main.js ADDED
@@ -0,0 +1,53 @@
1
+ import './styles/main.css';
2
+ import { AuthService } from './services/auth.js';
3
+ import { WebSocketService } from './services/websocket.js';
4
+ import { renderLoginPage } from './pages/login.js';
5
+ import { renderAdminDashboard } from './pages/admin-dashboard.js';
6
+ import { renderUserDashboard } from './pages/user-dashboard.js';
7
+
8
+ class App {
9
+ constructor() {
10
+ this.authService = new AuthService();
11
+ this.wsService = new WebSocketService();
12
+ this.currentUser = null;
13
+ this.init();
14
+ }
15
+
16
+ async init() {
17
+ const token = localStorage.getItem('token');
18
+
19
+ if (token) {
20
+ try {
21
+ this.currentUser = await this.authService.getCurrentUser();
22
+ this.wsService.connect(token);
23
+ this.renderDashboard();
24
+ } catch (error) {
25
+ console.error('认证失败:', error);
26
+ this.renderLogin();
27
+ }
28
+ } else {
29
+ this.renderLogin();
30
+ }
31
+ }
32
+
33
+ renderLogin() {
34
+ renderLoginPage(async (user, token) => {
35
+ this.currentUser = user;
36
+ localStorage.setItem('token', token);
37
+ this.wsService.connect(token);
38
+ this.renderDashboard();
39
+ });
40
+ }
41
+
42
+ renderDashboard() {
43
+ if (this.currentUser.role === 'admin') {
44
+ renderAdminDashboard(this.currentUser, this.wsService);
45
+ } else {
46
+ renderUserDashboard(this.currentUser, this.wsService);
47
+ }
48
+ }
49
+ }
50
+
51
+ new App();
52
+
53
+