chattransfer 0.0.0-stage → 2.0.10

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 (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.ar.md +208 -0
  3. package/README.md +207 -2
  4. package/README.zh-CN.md +208 -0
  5. package/bin/chattransfer.js +38 -0
  6. package/frontend/README.md +5 -0
  7. package/frontend/dist/assets/browser-BqOeEQBb.js +8 -0
  8. package/frontend/dist/assets/index-DTsuxum5.js +976 -0
  9. package/frontend/dist/assets/index-Dw5X-Gok.css +1 -0
  10. package/frontend/dist/assets/logo-DQUxWwh1.png +0 -0
  11. package/frontend/dist/index.html +14 -0
  12. package/frontend/dist/pin.svg +1 -0
  13. package/frontend/dist/vite.svg +1 -0
  14. package/package.json +62 -4
  15. package/server/cli-options.js +69 -0
  16. package/server/config/loader.js +36 -0
  17. package/server/config.js +63 -0
  18. package/server/index.js +132 -0
  19. package/server/room-manager.js +75 -0
  20. package/server/routes/checkLogin.js +24 -0
  21. package/server/routes/files.js +49 -0
  22. package/server/routes/geneBUserQR.js +37 -0
  23. package/server/routes/geneQR.js +39 -0
  24. package/server/routes/getHomeDir.js +27 -0
  25. package/server/routes/getIndexInfo.js +42 -0
  26. package/server/routes/getIndexInfo2.js +42 -0
  27. package/server/routes/index.js +36 -0
  28. package/server/routes/openCachePath.js +63 -0
  29. package/server/routes/qr.js +63 -0
  30. package/server/routes/scanLogin.js +33 -0
  31. package/server/routes/session.js +15 -0
  32. package/server/routes/setCachePath.js +94 -0
  33. package/server/routes/system.js +107 -0
  34. package/server/services/message-history-service.js +476 -0
  35. package/server/services/message-service.js +31 -0
  36. package/server/socket/index.js +232 -0
  37. package/server/socket/session-manager.js +3 -0
  38. package/server/telemetry/amplitude-provider.js +17 -0
  39. package/server/telemetry/compat.js +17 -0
  40. package/server/telemetry/index.js +26 -0
  41. package/server/telemetry/noop-provider.js +7 -0
  42. package/server/tool.js +71 -0
  43. package/server/utils/app-root.js +11 -0
  44. package/server/utils/network.js +21 -0
  45. package/server/utils/request-url.js +15 -0
  46. package/server/utils/runtime-options.js +88 -0
  47. package/server/utils/runtime-paths.js +87 -0
@@ -0,0 +1,232 @@
1
+ const socketIo = require('socket.io');
2
+ const roomManager = require('./session-manager');
3
+ const { trackEvent } = require('../telemetry/compat');
4
+ const {
5
+ decorateGroupMessage,
6
+ decoratePrivateMessage
7
+ } = require('../services/message-service');
8
+
9
+ module.exports = function registerSocket(server, historyService) {
10
+ if (!historyService) {
11
+ throw new Error('historyService is required');
12
+ }
13
+
14
+ const io = socketIo(server, {
15
+ cors: {
16
+ origin: true,
17
+ methods: ['GET', 'POST']
18
+ },
19
+ pingTimeout: 30000,
20
+ pingInterval: 25000
21
+ });
22
+ const chatRoom = 'group-chat';
23
+
24
+ const getClientIp = (socket) => {
25
+ const address = socket.handshake.address || socket.request.connection.remoteAddress;
26
+
27
+ return address ? address.replace(/^::ffff:/, '') : '';
28
+ };
29
+
30
+ const broadcastUserList = () => {
31
+ io.to(chatRoom).emit('user_list_updated', {
32
+ users: roomManager.listUsers()
33
+ });
34
+ };
35
+
36
+ const trackMessage = (type, direction, params, socket) => {
37
+ trackEvent('message_action', {
38
+ chat_type: type,
39
+ msg_type: params.msgType || 'TEXT',
40
+ direction,
41
+ sender: params.sender,
42
+ visitor_id: params.visitorId || 'unknown-visitor',
43
+ user_agent: socket.handshake.headers['user-agent']
44
+ });
45
+ };
46
+
47
+ const persistMessage = async (message) => {
48
+ try {
49
+ return { message: await historyService.saveMessage(message), error: null };
50
+ } catch (error) {
51
+ console.error('[History] save message failed:', error.message);
52
+ return { message, error };
53
+ }
54
+ };
55
+
56
+ const broadcastHistoryCleared = ({ chatType, targetUser, sessionId }, sourceSocket) => {
57
+ const payload = { chatType, targetUser, sessionId };
58
+
59
+ if (chatType === 'group') {
60
+ io.to(chatRoom).emit('history_cleared', payload);
61
+ return;
62
+ }
63
+
64
+ sourceSocket.emit('history_cleared', payload);
65
+ const receiver = roomManager.getUser(targetUser);
66
+ if (receiver && receiver.status === roomManager.getOnline()) {
67
+ io.to(receiver.socketId).emit('history_cleared', payload);
68
+ }
69
+ };
70
+
71
+ io.on('connection', (socket) => {
72
+ console.log(`[Socket] connected: ${socket.id}`);
73
+
74
+ socket.on('login', ({ uid } = {}) => {
75
+ console.log(`[Socket] login: ${uid}`);
76
+ });
77
+
78
+ socket.on('online', (username) => {
79
+ console.log(`[Socket] online: ${username} ${socket.id}`);
80
+ socket.username = username;
81
+ socket.join(chatRoom);
82
+ roomManager.setOnline({
83
+ username,
84
+ socketId: socket.id,
85
+ ip: getClientIp(socket)
86
+ });
87
+ broadcastUserList();
88
+ });
89
+
90
+ socket.on('private_chat', async (params, ack) => {
91
+ const decorated = decoratePrivateMessage(params, roomManager.getUser(params.sender));
92
+ const { message: savedMessage, error } = await persistMessage(decorated);
93
+
94
+ if (typeof ack === 'function') {
95
+ ack(error ? { ...decorated, status: 'failed', error: 'Message persistence failed' } : savedMessage);
96
+ }
97
+
98
+ if (error) return;
99
+
100
+ trackMessage('private', 'send', decorated, socket);
101
+
102
+ const receiver = roomManager.getUser(savedMessage.receiver);
103
+
104
+ if (receiver && receiver.status === roomManager.getOnline()) {
105
+ socket.to(receiver.socketId).emit('reply_private_chat', savedMessage);
106
+
107
+ trackEvent('message_action', {
108
+ chat_type: 'private',
109
+ msg_type: decorated.msgType || 'TEXT',
110
+ direction: 'receive',
111
+ receiver: decorated.receiver,
112
+ user_agent: socket.handshake.headers['user-agent']
113
+ });
114
+ } else {
115
+ console.log(`${decorated.receiver} is offline`);
116
+ }
117
+ });
118
+
119
+ socket.on('group_online', (username, nickname) => {
120
+ console.log(`[Socket] group_online: ${username} ${socket.id} nickname=${nickname}`);
121
+ socket.join(chatRoom);
122
+ socket.username = username;
123
+ roomManager.setOnline({
124
+ username,
125
+ socketId: socket.id,
126
+ nickname,
127
+ ip: getClientIp(socket)
128
+ });
129
+ socket.emit('group_name_updated', {
130
+ groupName: roomManager.getGroupName(),
131
+ updatedBy: ''
132
+ });
133
+ broadcastUserList();
134
+ });
135
+
136
+ socket.on('update_nickname', ({ sender, nickname }) => {
137
+ roomManager.setNickname(sender, nickname);
138
+ broadcastUserList();
139
+ });
140
+
141
+ socket.on('update_group_name', ({ groupName, updatedBy }) => {
142
+ roomManager.setGroupName(groupName);
143
+ io.to(chatRoom).emit('group_name_updated', {
144
+ groupName: roomManager.getGroupName(),
145
+ updatedBy: updatedBy || ''
146
+ });
147
+ });
148
+
149
+ socket.on('group_chat', async (params, ack) => {
150
+ const decorated = decorateGroupMessage(params, roomManager.getUser(params.sender));
151
+ const { message: savedMessage, error } = await persistMessage(decorated);
152
+
153
+ if (typeof ack === 'function') {
154
+ ack(error ? { ...decorated, status: 'failed', error: 'Message persistence failed' } : savedMessage);
155
+ }
156
+
157
+ if (error) return;
158
+
159
+ trackMessage('group', 'send', decorated, socket);
160
+ socket.broadcast.to(chatRoom).emit('reply_group_chat', savedMessage);
161
+
162
+ trackEvent('message_action', {
163
+ chat_type: 'group',
164
+ msg_type: decorated.msgType || 'TEXT',
165
+ direction: 'broadcast',
166
+ user_agent: socket.handshake.headers['user-agent']
167
+ });
168
+ });
169
+
170
+ socket.on('history_query', async (params = {}, ack) => {
171
+ if (typeof ack !== 'function') return;
172
+
173
+ try {
174
+ const requester = socket.username || params.requester;
175
+ const chatType = params.chatType || (
176
+ params.targetUser === 'group_chat' ? 'group' : 'private'
177
+ );
178
+ const data = await historyService.queryHistory({
179
+ requester,
180
+ chatType,
181
+ targetUser: params.targetUser,
182
+ cursor: params.cursor,
183
+ limit: params.limit
184
+ });
185
+
186
+ ack({ code: 0, data });
187
+ } catch (error) {
188
+ console.error('[History] query failed:', error.message);
189
+ ack({ code: -1, message: 'Failed to load history' });
190
+ }
191
+ });
192
+
193
+ socket.on('history_clear', async (params = {}, ack) => {
194
+ if (typeof ack !== 'function') return;
195
+
196
+ try {
197
+ const requester = socket.username || params.requester;
198
+ const chatType = params.chatType || (
199
+ params.targetUser === 'group_chat' ? 'group' : 'private'
200
+ );
201
+ const conversation = historyService.getConversation({
202
+ requester,
203
+ chatType,
204
+ targetUser: params.targetUser
205
+ });
206
+ const data = await historyService.clearHistory({
207
+ requester,
208
+ chatType,
209
+ targetUser: params.targetUser
210
+ });
211
+
212
+ broadcastHistoryCleared({
213
+ chatType,
214
+ targetUser: params.targetUser,
215
+ sessionId: conversation.sessionId
216
+ }, socket);
217
+ ack({ code: 0, data });
218
+ } catch (error) {
219
+ console.error('[History] clear failed:', error.message);
220
+ ack({ code: -1, message: 'Failed to clear history' });
221
+ }
222
+ });
223
+
224
+ socket.on('disconnect', () => {
225
+ console.log(`[Socket] disconnect: ${socket.username || 'unknown'} ${socket.id}`);
226
+ roomManager.setOffline(socket.username);
227
+ broadcastUserList();
228
+ });
229
+ });
230
+
231
+ return io;
232
+ };
@@ -0,0 +1,3 @@
1
+ const { RoomManager } = require('../room-manager');
2
+
3
+ module.exports = new RoomManager();
@@ -0,0 +1,17 @@
1
+ const axios = require('axios');
2
+
3
+ class AmplitudeProvider {
4
+ constructor({ apiKey, endpoint }) {
5
+ this.apiKey = apiKey;
6
+ this.endpoint = endpoint || 'https://api2.amplitude.com/2/httpapi';
7
+ }
8
+
9
+ async track(event) {
10
+ await axios.post(this.endpoint, {
11
+ api_key: this.apiKey,
12
+ events: [event]
13
+ });
14
+ }
15
+ }
16
+
17
+ module.exports = AmplitudeProvider;
@@ -0,0 +1,17 @@
1
+ const { loadConfig } = require('../config/loader');
2
+ const { createTelemetryProvider } = require('./index');
3
+
4
+ const provider = createTelemetryProvider(loadConfig());
5
+
6
+ async function trackEvent(eventType, eventProperties = {}, customUserId = null) {
7
+ return provider.track({
8
+ event_type: eventType,
9
+ event_properties: eventProperties,
10
+ user_id: customUserId || undefined,
11
+ time: Date.now()
12
+ });
13
+ }
14
+
15
+ module.exports = {
16
+ trackEvent
17
+ };
@@ -0,0 +1,26 @@
1
+ const { loadConfig } = require('../config/loader');
2
+ const AmplitudeProvider = require('./amplitude-provider');
3
+ const NoopProvider = require('./noop-provider');
4
+
5
+ const providers = {
6
+ amplitude: AmplitudeProvider
7
+ };
8
+
9
+ function createTelemetryProvider(config = loadConfig()) {
10
+ const analytics = config.analytics || {};
11
+ const enabled = analytics.enabled && Boolean(analytics.apiKey);
12
+ const Provider = providers[analytics.provider || 'amplitude'];
13
+
14
+ if (!enabled || !Provider) {
15
+ return new NoopProvider();
16
+ }
17
+
18
+ return new Provider({
19
+ apiKey: analytics.apiKey,
20
+ endpoint: analytics.endpoint
21
+ });
22
+ }
23
+
24
+ module.exports = {
25
+ createTelemetryProvider
26
+ };
@@ -0,0 +1,7 @@
1
+ class NoopProvider {
2
+ async track() {
3
+ return Promise.resolve();
4
+ }
5
+ }
6
+
7
+ module.exports = NoopProvider;
package/server/tool.js ADDED
@@ -0,0 +1,71 @@
1
+ const os = require('os');
2
+ const path = require('path');
3
+ const { execFile } = require('child_process');
4
+ var qr = require('qr-image');
5
+ module.exports.getIPAddress = function () {
6
+ var interfaces = os.networkInterfaces();
7
+ for (var devName in interfaces) {
8
+ var iface = interfaces[devName];
9
+ for (var i = 0; i < iface.length; i++) {
10
+ var alias = iface[i];
11
+ if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
12
+ return alias.address;
13
+ }
14
+ }
15
+ }
16
+ }
17
+
18
+ //打开默认浏览器
19
+ module.exports.openDefaultBrowser = function (url, callback = () => {}) {
20
+ console.log("使用浏览器打开首页");
21
+ switch (process.platform) {
22
+ case "darwin":
23
+ execFile('open', [url], callback);
24
+ break;
25
+ case "win32":
26
+ execFile('cmd.exe', ['/c', 'start', '', url], callback);
27
+ break;
28
+ default:
29
+ execFile('xdg-open', [url], callback);
30
+ }
31
+ }
32
+
33
+ module.exports.getAppWritableRoot = function () {
34
+ if (process.pkg) {
35
+ // Pkg环境:使用可执行文件所在目录(可写)
36
+ console.log('Running in pkg environment');
37
+ return path.dirname(process.execPath);
38
+ } else {
39
+ // 开发环境:使用项目根目录(server目录的上级)
40
+ console.log('Running in development environment');
41
+ return path.join(__dirname, '..');
42
+ }
43
+ }
44
+ module.exports.initCachePath = function (cachePath,callback) {
45
+ console.log('开始初始化缓存目录');
46
+
47
+ // 如果不存在则创建,都则不创建
48
+ let fs = require('fs');
49
+ if(fs.existsSync(cachePath)){
50
+ console.log('缓存目录已存在,无需创建');
51
+ callback();
52
+ return;
53
+ };
54
+ fs.mkdir(cachePath, { recursive: true }, (err) => {
55
+ if (err) {
56
+ console.log('出错:',err)
57
+ } else {
58
+ console.log("初始化缓存目录成功")
59
+ }
60
+ callback(err);
61
+ })
62
+
63
+ }
64
+
65
+ module.exports.homeDir = os.userInfo().homedir;
66
+ module.exports.geneQR = function(url){
67
+ var img_string = qr.imageSync(url, { type: 'png' ,size:10});
68
+ const base64Str = 'data:image/png;base64,' + Buffer.from(img_string, 'utf8').toString('base64')
69
+ return base64Str;
70
+ }
71
+
@@ -0,0 +1,11 @@
1
+ const path = require('path');
2
+
3
+ function getAppWritableRoot() {
4
+ return process.pkg
5
+ ? path.dirname(process.execPath)
6
+ : path.join(__dirname, '..', '..');
7
+ }
8
+
9
+ module.exports = {
10
+ getAppWritableRoot
11
+ };
@@ -0,0 +1,21 @@
1
+ const os = require('os');
2
+
3
+ function getIPAddress() {
4
+ const interfaces = os.networkInterfaces();
5
+
6
+ for (const interfaceName of Object.keys(interfaces)) {
7
+ const addresses = interfaces[interfaceName] || [];
8
+
9
+ for (const address of addresses) {
10
+ if (address.family === 'IPv4' && address.address !== '127.0.0.1' && !address.internal) {
11
+ return address.address;
12
+ }
13
+ }
14
+ }
15
+
16
+ return undefined;
17
+ }
18
+
19
+ module.exports = {
20
+ getIPAddress
21
+ };
@@ -0,0 +1,15 @@
1
+ function getRequestBaseUrl(req) {
2
+ const publicUrl = process.env.CHATTRANSFER_PUBLIC_URL;
3
+
4
+ if (publicUrl) {
5
+ return publicUrl.replace(/\/$/, '');
6
+ }
7
+
8
+ const forwardedProtocol = req.headers['x-forwarded-proto'];
9
+ const protocol = forwardedProtocol || req.protocol;
10
+ return `${protocol}://${req.headers.host}`;
11
+ }
12
+
13
+ module.exports = {
14
+ getRequestBaseUrl
15
+ };
@@ -0,0 +1,88 @@
1
+ const path = require('path');
2
+
3
+ const DEFAULT_HOST = '0.0.0.0';
4
+ const DEFAULT_PORT = 55555;
5
+
6
+ function isTruthy(value) {
7
+ return ['1', 'true', 'yes', 'on'].includes(String(value).toLowerCase());
8
+ }
9
+
10
+ function parsePort(value, source) {
11
+ const port = Number(value);
12
+
13
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
14
+ throw new Error(`Invalid ${source}: ${value}`);
15
+ }
16
+
17
+ return port;
18
+ }
19
+
20
+ function normalizePublicUrl(value) {
21
+ if (!value) return null;
22
+
23
+ try {
24
+ const url = new URL(value);
25
+
26
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
27
+ throw new Error('Invalid public URL protocol');
28
+ }
29
+
30
+ const basePath = url.pathname === '/' ? '' : url.pathname.replace(/\/$/, '');
31
+ return `${url.protocol}//${url.host}${basePath}`;
32
+ } catch (error) {
33
+ throw new Error(`Invalid public URL: ${value}`);
34
+ }
35
+ }
36
+
37
+ function shouldOpenBrowser({ env = process.env, explicitValue, platform = process.platform } = {}) {
38
+ if (typeof explicitValue === 'boolean') {
39
+ return explicitValue;
40
+ }
41
+
42
+ if (env.CHATTRANSFER_NO_BROWSER !== undefined) {
43
+ return !isTruthy(env.CHATTRANSFER_NO_BROWSER);
44
+ }
45
+
46
+ if (platform === 'linux' && !env.DISPLAY && !env.WAYLAND_DISPLAY) {
47
+ return false;
48
+ }
49
+
50
+ return true;
51
+ }
52
+
53
+ function resolveStartOptions(overrides = {}, env = process.env) {
54
+ const portValue = overrides.port ?? env.CHATTRANSFER_PORT;
55
+ const port = portValue === undefined || portValue === null || portValue === ''
56
+ ? DEFAULT_PORT
57
+ : parsePort(portValue, 'port');
58
+ const host = overrides.host ?? env.CHATTRANSFER_HOST ?? DEFAULT_HOST;
59
+ const dataDir = overrides.dataDir ?? env.CHATTRANSFER_DATA_DIR;
60
+ const publicUrl = normalizePublicUrl(overrides.publicUrl ?? env.CHATTRANSFER_PUBLIC_URL);
61
+ const openBrowser = shouldOpenBrowser({
62
+ env,
63
+ explicitValue: overrides.openBrowser
64
+ });
65
+
66
+ if (typeof host !== 'string' || !host.trim()) {
67
+ throw new Error('Invalid host');
68
+ }
69
+
70
+ return {
71
+ host: host.trim(),
72
+ port,
73
+ explicitPort: overrides.port !== undefined || env.CHATTRANSFER_PORT !== undefined,
74
+ dataDir: dataDir ? path.resolve(String(dataDir)) : null,
75
+ publicUrl,
76
+ openBrowser
77
+ };
78
+ }
79
+
80
+ module.exports = {
81
+ DEFAULT_HOST,
82
+ DEFAULT_PORT,
83
+ isTruthy,
84
+ normalizePublicUrl,
85
+ parsePort,
86
+ resolveStartOptions,
87
+ shouldOpenBrowser
88
+ };
@@ -0,0 +1,87 @@
1
+ const os = require('os');
2
+ const path = require('path');
3
+
4
+ const RUNTIMES = new Set(['development', 'npm', 'pkg', 'docker']);
5
+
6
+ function getRuntimeName() {
7
+ const runtime = process.env.CHATTRANSFER_RUNTIME;
8
+
9
+ if (runtime && RUNTIMES.has(runtime)) {
10
+ return runtime;
11
+ }
12
+
13
+ return process.pkg ? 'pkg' : 'development';
14
+ }
15
+
16
+ function getUserDataDir() {
17
+ if (process.platform === 'darwin') {
18
+ return path.join(os.homedir(), 'Library', 'Application Support', 'ChatTransfer');
19
+ }
20
+
21
+ if (process.platform === 'win32') {
22
+ const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
23
+ return path.join(appData, 'ChatTransfer');
24
+ }
25
+
26
+ const xdgDataHome = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
27
+ return path.join(xdgDataHome, 'ChatTransfer');
28
+ }
29
+
30
+ function getDefaultDataDir() {
31
+ const explicitDataDir = process.env.CHATTRANSFER_DATA_DIR;
32
+
33
+ if (explicitDataDir) {
34
+ return path.resolve(explicitDataDir);
35
+ }
36
+
37
+ if (process.env.CHATTRANSFER_CONFIG_FILE) {
38
+ return path.dirname(path.resolve(process.env.CHATTRANSFER_CONFIG_FILE));
39
+ }
40
+
41
+ const runtime = getRuntimeName();
42
+
43
+ if (runtime === 'pkg') {
44
+ return path.dirname(process.execPath);
45
+ }
46
+
47
+ if (runtime === 'npm' || runtime === 'docker') {
48
+ return getUserDataDir();
49
+ }
50
+
51
+ return path.join(__dirname, '..', '..');
52
+ }
53
+
54
+ function getConfigFile() {
55
+ return process.env.CHATTRANSFER_CONFIG_FILE
56
+ ? path.resolve(process.env.CHATTRANSFER_CONFIG_FILE)
57
+ : path.join(getDefaultDataDir(), 'config.json');
58
+ }
59
+
60
+ function getTelemetryConfigFile() {
61
+ return path.join(getDefaultDataDir(), 'config', 'local.json');
62
+ }
63
+
64
+ function getDatabaseFile() {
65
+ const runtime = getRuntimeName();
66
+
67
+ if (runtime === 'npm' || runtime === 'docker') {
68
+ return path.join(getDefaultDataDir(), 'chattransfer.db');
69
+ }
70
+
71
+ return path.join(getDefaultDataDir(), 'uploads', 'history', 'chattransfer.db');
72
+ }
73
+
74
+ function usesIndependentDatabaseFile() {
75
+ const runtime = getRuntimeName();
76
+ return runtime === 'npm' || runtime === 'docker';
77
+ }
78
+
79
+ module.exports = {
80
+ getConfigFile,
81
+ getDatabaseFile,
82
+ getDefaultDataDir,
83
+ getRuntimeName,
84
+ getTelemetryConfigFile,
85
+ getUserDataDir,
86
+ usesIndependentDatabaseFile
87
+ };