share-home 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.
- package/.next/BUILD_ID +1 -0
- package/.next/app-path-routes-manifest.json +19 -0
- package/.next/build-manifest.json +20 -0
- package/.next/dev/build-manifest.json +18 -0
- package/.next/dev/cache/webpack/client-development/0.pack.gz +0 -0
- package/.next/dev/cache/webpack/client-development/index.pack.gz +0 -0
- package/.next/dev/cache/webpack/server-development/0.pack.gz +0 -0
- package/.next/dev/cache/webpack/server-development/index.pack.gz +0 -0
- package/.next/dev/react-loadable-manifest.json +1 -0
- package/.next/dev/server/app/api/config/route_client-reference-manifest.js +1 -0
- package/.next/dev/server/app/api/documents/route_client-reference-manifest.js +1 -0
- package/.next/dev/server/app/api/init/route_client-reference-manifest.js +1 -0
- package/.next/dev/server/app/api/peers/route_client-reference-manifest.js +1 -0
- package/.next/dev/server/app/api/transfer/download/route_client-reference-manifest.js +1 -0
- package/.next/dev/server/app/api/transfer/prepare/route_client-reference-manifest.js +1 -0
- package/.next/dev/server/app/api/transfer/shared/download/route_client-reference-manifest.js +1 -0
- package/.next/dev/server/app/api/transfer/shared/route_client-reference-manifest.js +1 -0
- package/.next/dev/server/app/api/transfer/tasks/route_client-reference-manifest.js +1 -0
- package/.next/dev/server/app/page_client-reference-manifest.js +1 -0
- package/.next/dev/server/app-paths-manifest.json +3 -0
- package/.next/dev/server/middleware-build-manifest.js +18 -0
- package/.next/dev/server/middleware-react-loadable-manifest.js +1 -0
- package/.next/dev/server/next-font-manifest.js +1 -0
- package/.next/dev/server/next-font-manifest.json +1 -0
- package/.next/dev/server/pages-manifest.json +1 -0
- package/.next/dev/server/server-reference-manifest.js +1 -0
- package/.next/dev/server/server-reference-manifest.json +5 -0
- package/.next/dev/server/vendor-chunks/next@16.2.6_@babel+core@7.29.0_react-dom@19.2.4_react@19.2.4__react@19.2.4.js +3998 -0
- package/.next/dev/server/webpack-runtime.js +209 -0
- package/.next/dev/static/development/_buildManifest.js +1 -0
- package/.next/dev/static/development/_ssgManifest.js +1 -0
- package/.next/dev/types/app/layout.ts +87 -0
- package/.next/dev/types/app/page.ts +87 -0
- package/.next/dev/types/package.json +1 -0
- package/.next/diagnostics/build-diagnostics.json +6 -0
- package/.next/diagnostics/framework.json +1 -0
- package/.next/export-marker.json +6 -0
- package/.next/images-manifest.json +68 -0
- package/.next/next-minimal-server.js.nft.json +1 -0
- package/.next/next-server.js.nft.json +1 -0
- package/.next/package.json +1 -0
- package/.next/prerender-manifest.json +114 -0
- package/.next/react-loadable-manifest.json +1 -0
- package/.next/required-server-files.js +337 -0
- package/.next/required-server-files.json +337 -0
- package/.next/routes-manifest.json +147 -0
- package/.next/server/app/_global-error/page.js +32 -0
- package/.next/server/app/_global-error/page.js.nft.json +1 -0
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -0
- package/.next/server/app/_global-error.html +1 -0
- package/.next/server/app/_global-error.meta +16 -0
- package/.next/server/app/_global-error.rsc +14 -0
- package/.next/server/app/_global-error.segments/_full.segment.rsc +14 -0
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +5 -0
- package/.next/server/app/_global-error.segments/_global-error.segment.rsc +5 -0
- package/.next/server/app/_global-error.segments/_head.segment.rsc +5 -0
- package/.next/server/app/_global-error.segments/_index.segment.rsc +5 -0
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -0
- package/.next/server/app/_not-found/page.js +7 -0
- package/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/.next/server/app/_not-found.html +6 -0
- package/.next/server/app/_not-found.meta +16 -0
- package/.next/server/app/_not-found.rsc +18 -0
- package/.next/server/app/_not-found.segments/_full.segment.rsc +18 -0
- package/.next/server/app/_not-found.segments/_head.segment.rsc +6 -0
- package/.next/server/app/_not-found.segments/_index.segment.rsc +5 -0
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +5 -0
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +5 -0
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +4 -0
- package/.next/server/app/api/board/sync/route.js +1 -0
- package/.next/server/app/api/board/sync/route.js.nft.json +1 -0
- package/.next/server/app/api/board/sync/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/config/route.js +1 -0
- package/.next/server/app/api/config/route.js.nft.json +1 -0
- package/.next/server/app/api/config/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/documents/route.js +1 -0
- package/.next/server/app/api/documents/route.js.nft.json +1 -0
- package/.next/server/app/api/documents/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/documents/sync/route.js +1 -0
- package/.next/server/app/api/documents/sync/route.js.nft.json +1 -0
- package/.next/server/app/api/documents/sync/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/init/route.js +1 -0
- package/.next/server/app/api/init/route.js.nft.json +1 -0
- package/.next/server/app/api/init/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/peers/route.js +1 -0
- package/.next/server/app/api/peers/route.js.nft.json +1 -0
- package/.next/server/app/api/peers/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/transfer/download/route.js +1 -0
- package/.next/server/app/api/transfer/download/route.js.nft.json +1 -0
- package/.next/server/app/api/transfer/download/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/transfer/prepare/route.js +1 -0
- package/.next/server/app/api/transfer/prepare/route.js.nft.json +1 -0
- package/.next/server/app/api/transfer/prepare/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/transfer/request/route.js +1 -0
- package/.next/server/app/api/transfer/request/route.js.nft.json +1 -0
- package/.next/server/app/api/transfer/request/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/transfer/shared/download/route.js +1 -0
- package/.next/server/app/api/transfer/shared/download/route.js.nft.json +1 -0
- package/.next/server/app/api/transfer/shared/download/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/transfer/shared/route.js +1 -0
- package/.next/server/app/api/transfer/shared/route.js.nft.json +1 -0
- package/.next/server/app/api/transfer/shared/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/transfer/start/route.js +1 -0
- package/.next/server/app/api/transfer/start/route.js.nft.json +1 -0
- package/.next/server/app/api/transfer/start/route_client-reference-manifest.js +1 -0
- package/.next/server/app/api/transfer/tasks/route.js +1 -0
- package/.next/server/app/api/transfer/tasks/route.js.nft.json +1 -0
- package/.next/server/app/api/transfer/tasks/route_client-reference-manifest.js +1 -0
- package/.next/server/app/favicon.ico/route.js +1 -0
- package/.next/server/app/favicon.ico/route.js.nft.json +1 -0
- package/.next/server/app/favicon.ico.body +0 -0
- package/.next/server/app/favicon.ico.meta +1 -0
- package/.next/server/app/index.html +6 -0
- package/.next/server/app/index.meta +14 -0
- package/.next/server/app/index.rsc +21 -0
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +10 -0
- package/.next/server/app/index.segments/_full.segment.rsc +21 -0
- package/.next/server/app/index.segments/_head.segment.rsc +6 -0
- package/.next/server/app/index.segments/_index.segment.rsc +5 -0
- package/.next/server/app/index.segments/_tree.segment.rsc +5 -0
- package/.next/server/app/page.js +115 -0
- package/.next/server/app/page.js.nft.json +1 -0
- package/.next/server/app/page_client-reference-manifest.js +1 -0
- package/.next/server/app-paths-manifest.json +19 -0
- package/.next/server/chunks/31.js +2 -0
- package/.next/server/chunks/4.js +1 -0
- package/.next/server/chunks/404.js +1 -0
- package/.next/server/chunks/516.js +1 -0
- package/.next/server/chunks/718.js +45 -0
- package/.next/server/chunks/887.js +1 -0
- package/.next/server/chunks/891.js +18 -0
- package/.next/server/functions-config-manifest.json +4 -0
- package/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/.next/server/middleware-build-manifest.js +1 -0
- package/.next/server/middleware-manifest.json +6 -0
- package/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/.next/server/next-font-manifest.js +1 -0
- package/.next/server/next-font-manifest.json +1 -0
- package/.next/server/pages/404.html +6 -0
- package/.next/server/pages/500.html +1 -0
- package/.next/server/pages-manifest.json +4 -0
- package/.next/server/prefetch-hints.json +1 -0
- package/.next/server/server-reference-manifest.js +1 -0
- package/.next/server/server-reference-manifest.json +1 -0
- package/.next/server/webpack-runtime.js +1 -0
- package/.next/static/JAumJsHdasa7bQ7jxx37q/_buildManifest.js +1 -0
- package/.next/static/JAumJsHdasa7bQ7jxx37q/_ssgManifest.js +1 -0
- package/.next/static/chunks/148-2d58b90f6dc8cfaf.js +32 -0
- package/.next/static/chunks/5d8f0495-d87c92750ebe0885.js +82 -0
- package/.next/static/chunks/649-20578e0ca00dbde1.js +14 -0
- package/.next/static/chunks/991cd08a-0938e33045166413.js +1 -0
- package/.next/static/chunks/app/_global-error/page-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/_not-found/page-5696c6e9b39a4885.js +1 -0
- package/.next/static/chunks/app/api/board/sync/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/api/config/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/api/documents/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/api/documents/sync/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/api/init/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/api/peers/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/api/transfer/download/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/api/transfer/prepare/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/api/transfer/request/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/api/transfer/shared/download/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/api/transfer/shared/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/api/transfer/start/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/api/transfer/tasks/route-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/app/layout-f1c1109b37ee9a67.js +1 -0
- package/.next/static/chunks/app/page-d667dde9ae730a9e.js +15 -0
- package/.next/static/chunks/be838f7e-d7eb8d1a464523ea.js +1 -0
- package/.next/static/chunks/framework-1af2d653ea416252.js +1 -0
- package/.next/static/chunks/main-app-2475c374c5ac40b5.js +1 -0
- package/.next/static/chunks/main-c04fb4d60a5182d4.js +5 -0
- package/.next/static/chunks/next/dist/client/components/builtin/app-error-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/forbidden-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/global-error-9771ee9f95e5e628.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/not-found-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-a02f063e5aaa162f.js +1 -0
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/.next/static/chunks/webpack-1bf2fae924e39757.js +1 -0
- package/.next/static/css/30baeec9f6889470.css +1 -0
- package/.next/static/css/30f58f83a0192172.css +1 -0
- package/.next/static/media/0f1bdadaf30e2d5f-s.woff2 +0 -0
- package/.next/static/media/22a5144ee8d83bca-s.p.woff2 +0 -0
- package/.next/static/media/2c34d62a75506231-s.woff2 +0 -0
- package/.next/static/media/601f5c280d60caca-s.woff2 +0 -0
- package/.next/static/media/9766a7e9e2e0ad5a-s.woff2 +0 -0
- package/.next/static/media/a115172161b307bb-s.woff2 +0 -0
- package/.next/static/media/aa016aab0e6d1295-s.woff2 +0 -0
- package/.next/static/media/b66cf8e69499582a-s.woff2 +0 -0
- package/.next/static/media/d100b2a099e34044-s.woff2 +0 -0
- package/.next/static/media/f5271587012faf78-s.p.woff2 +0 -0
- package/.next/static/media/f639721981034f88-s.woff2 +0 -0
- package/.next/trace +3 -0
- package/.next/trace-build +1 -0
- package/.next/types/app/api/board/sync/route.ts +351 -0
- package/.next/types/app/api/config/route.ts +351 -0
- package/.next/types/app/api/documents/route.ts +351 -0
- package/.next/types/app/api/documents/sync/route.ts +351 -0
- package/.next/types/app/api/init/route.ts +351 -0
- package/.next/types/app/api/peers/route.ts +351 -0
- package/.next/types/app/api/transfer/download/route.ts +351 -0
- package/.next/types/app/api/transfer/prepare/route.ts +351 -0
- package/.next/types/app/api/transfer/request/route.ts +351 -0
- package/.next/types/app/api/transfer/shared/download/route.ts +351 -0
- package/.next/types/app/api/transfer/shared/route.ts +351 -0
- package/.next/types/app/api/transfer/start/route.ts +351 -0
- package/.next/types/app/api/transfer/tasks/route.ts +351 -0
- package/.next/types/app/layout.ts +87 -0
- package/.next/types/app/page.ts +87 -0
- package/.next/types/cache-life.d.ts +145 -0
- package/.next/types/package.json +1 -0
- package/.next/types/routes.d.ts +85 -0
- package/.next/types/validator.ts +187 -0
- package/README.md +85 -0
- package/bin/cli.js +51 -0
- package/package.json +52 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/services/configService.ts +144 -0
- package/src/services/documentService.ts +210 -0
- package/src/services/fileService.ts +595 -0
- package/src/services/mdnsService.ts +284 -0
- package/src/services/socketService.ts +214 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { Bonjour, Service } from 'bonjour-service';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import { Peer } from '../types/peer';
|
|
4
|
+
|
|
5
|
+
export class MdnsService {
|
|
6
|
+
private bonjour: InstanceType<typeof Bonjour> | null = null;
|
|
7
|
+
private publishedService: InstanceType<typeof Service> | null = null;
|
|
8
|
+
private browser: any = null;
|
|
9
|
+
|
|
10
|
+
// 维护局域网在线节点,Key 为 Peer ID
|
|
11
|
+
private peers: Map<string, Peer> = new Map();
|
|
12
|
+
private onPeersChangeCallbacks: Array<(peers: Peer[]) => void> = [];
|
|
13
|
+
|
|
14
|
+
private selfId: string = '';
|
|
15
|
+
private localIp: string = '';
|
|
16
|
+
|
|
17
|
+
private constructor() {
|
|
18
|
+
this.localIp = this.detectLocalIp();
|
|
19
|
+
this.selfId = `peer_${this.localIp.replace(/\./g, '_')}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public static getInstance(): MdnsService {
|
|
23
|
+
const globalSymbols = global as any;
|
|
24
|
+
if (!globalSymbols.__mdns_service_instance__) {
|
|
25
|
+
globalSymbols.__mdns_service_instance__ = new MdnsService();
|
|
26
|
+
}
|
|
27
|
+
return globalSymbols.__mdns_service_instance__;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 启动本机的 mDNS 服务广播与局域网节点发现
|
|
32
|
+
*/
|
|
33
|
+
public start(nickname: string, avatar: string, webPort: number): void {
|
|
34
|
+
if (this.bonjour) {
|
|
35
|
+
// 已经启动,则更新广播元数据
|
|
36
|
+
this.updateBroadcast(nickname, avatar);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.bonjour = new Bonjour();
|
|
41
|
+
this.localIp = this.detectLocalIp();
|
|
42
|
+
|
|
43
|
+
console.log(`[mDNS] 正在启动自发现服务. 本机IP: ${this.localIp}, 端口: ${webPort}`);
|
|
44
|
+
|
|
45
|
+
// 1. 广播本机服务
|
|
46
|
+
this.publishedService = this.bonjour.publish({
|
|
47
|
+
name: `ShareHome-${this.selfId}`,
|
|
48
|
+
type: 'sharehome',
|
|
49
|
+
port: webPort,
|
|
50
|
+
txt: {
|
|
51
|
+
id: this.selfId,
|
|
52
|
+
nickname: nickname,
|
|
53
|
+
avatar: avatar,
|
|
54
|
+
ip: this.localIp,
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
this.publishedService.on('error', (err) => {
|
|
59
|
+
console.error('[mDNS] 广播服务发生异常:', err);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// 2. 扫描局域网内的其他 ShareHome 服务
|
|
63
|
+
this.browser = this.bonjour.find({ type: 'sharehome' });
|
|
64
|
+
|
|
65
|
+
this.browser.on('up', (service: InstanceType<typeof Service>) => {
|
|
66
|
+
const txt = service.txt || {};
|
|
67
|
+
const peerId = txt.id;
|
|
68
|
+
const peerIp = txt.ip || service.referer?.address;
|
|
69
|
+
|
|
70
|
+
if (!peerId || !peerIp || peerId === this.selfId) {
|
|
71
|
+
return; // 忽略本机以及无效广播
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const newPeer: Peer = {
|
|
75
|
+
id: peerId,
|
|
76
|
+
nickname: txt.nickname || '局域网伙伴',
|
|
77
|
+
avatar: txt.avatar || 'avatar-1',
|
|
78
|
+
ip: peerIp,
|
|
79
|
+
port: service.port,
|
|
80
|
+
lastSeen: Date.now(),
|
|
81
|
+
isSelf: false,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
this.peers.set(peerId, newPeer);
|
|
85
|
+
console.log(`[mDNS] 发现新设备上线: ${newPeer.nickname} (${newPeer.ip})`);
|
|
86
|
+
this.notifyListeners();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
this.browser.on('down', (service: InstanceType<typeof Service>) => {
|
|
90
|
+
// 寻找对应的节点移出
|
|
91
|
+
const txt = service.txt || {};
|
|
92
|
+
const peerId = txt.id;
|
|
93
|
+
if (peerId && this.peers.has(peerId)) {
|
|
94
|
+
const removedPeer = this.peers.get(peerId);
|
|
95
|
+
this.peers.delete(peerId);
|
|
96
|
+
console.log(`[mDNS] 设备下线: ${removedPeer?.nickname}`);
|
|
97
|
+
this.notifyListeners();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 更新广播的昵称和头像元数据
|
|
104
|
+
*/
|
|
105
|
+
public updateBroadcast(nickname: string, avatar: string): void {
|
|
106
|
+
if (this.publishedService) {
|
|
107
|
+
console.log(`[mDNS] 正在更新本机广播元数据: ${nickname}`);
|
|
108
|
+
// bonjour-service 允许动态更新 TXT 记录
|
|
109
|
+
try {
|
|
110
|
+
// @ts-ignore
|
|
111
|
+
this.publishedService.updateTxt({
|
|
112
|
+
id: this.selfId,
|
|
113
|
+
nickname: nickname,
|
|
114
|
+
avatar: avatar,
|
|
115
|
+
ip: this.localIp,
|
|
116
|
+
});
|
|
117
|
+
} catch (err) {
|
|
118
|
+
console.error('[mDNS] 更新 TXT 记录失败,执行重启广播。');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 关闭 mDNS 服务
|
|
125
|
+
*/
|
|
126
|
+
public stop(): void {
|
|
127
|
+
if (this.browser) {
|
|
128
|
+
this.browser.stop();
|
|
129
|
+
}
|
|
130
|
+
if (this.publishedService) {
|
|
131
|
+
this.publishedService.stop(() => {
|
|
132
|
+
console.log('[mDNS] 停止本机广播服务。');
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (this.bonjour) {
|
|
136
|
+
this.bonjour.destroy();
|
|
137
|
+
this.bonjour = null;
|
|
138
|
+
}
|
|
139
|
+
this.peers.clear();
|
|
140
|
+
this.notifyListeners();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* 注册由 Web 浏览器访问建立的虚拟 Peer
|
|
145
|
+
*/
|
|
146
|
+
public registerWebPeer(id: string, ip: string, nickname: string, avatar: string, port = 3000, os = 'Windows'): void {
|
|
147
|
+
if (id === this.selfId) return;
|
|
148
|
+
|
|
149
|
+
// 智能修正:若 Web 客户端通过本地回环地址访问,将其 IP 修正为本机的物理局域网 IP
|
|
150
|
+
let clientIp = ip;
|
|
151
|
+
if (clientIp === '127.0.0.1' || clientIp === '::1' || clientIp === 'localhost') {
|
|
152
|
+
clientIp = this.localIp;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const newPeer: Peer = {
|
|
156
|
+
id,
|
|
157
|
+
nickname,
|
|
158
|
+
avatar,
|
|
159
|
+
ip: clientIp,
|
|
160
|
+
port,
|
|
161
|
+
lastSeen: Date.now(),
|
|
162
|
+
isSelf: false,
|
|
163
|
+
os
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
this.peers.set(id, newPeer);
|
|
167
|
+
console.log(`[mDNS] 收到 Web 浏览器虚拟终端注册: ${nickname} (${clientIp}), OS: ${os}`);
|
|
168
|
+
this.notifyListeners();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 注销指定 Web 虚拟 Peer
|
|
173
|
+
*/
|
|
174
|
+
public unregisterWebPeer(id: string): void {
|
|
175
|
+
if (this.peers.has(id)) {
|
|
176
|
+
const removedPeer = this.peers.get(id);
|
|
177
|
+
this.peers.delete(id);
|
|
178
|
+
console.log(`[mDNS] Web 浏览器虚拟终端下线: ${removedPeer?.nickname}`);
|
|
179
|
+
this.notifyListeners();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 获取所有在线设备列表
|
|
185
|
+
*/
|
|
186
|
+
public getPeers(): Peer[] {
|
|
187
|
+
return Array.from(this.peers.values());
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
public getSelfId(): string {
|
|
191
|
+
return this.selfId;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
public getLocalIp(): string {
|
|
195
|
+
return this.localIp;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 监听列表变化
|
|
200
|
+
*/
|
|
201
|
+
public onPeersChange(callback: (peers: Peer[]) => void): void {
|
|
202
|
+
this.onPeersChangeCallbacks.push(callback);
|
|
203
|
+
// 立即触发一次初始数据
|
|
204
|
+
callback(this.getPeers());
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private notifyListeners(): void {
|
|
208
|
+
const peersList = this.getPeers();
|
|
209
|
+
for (const callback of this.onPeersChangeCallbacks) {
|
|
210
|
+
callback(peersList);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 获取本机的局域网 IPv4 地址 (智能过滤虚拟网卡,优先获取物理 WLAN/以太网网口 IP)
|
|
216
|
+
*/
|
|
217
|
+
private detectLocalIp(): string {
|
|
218
|
+
const interfaces = os.networkInterfaces();
|
|
219
|
+
const candidates: Array<{ name: string; address: string; isVirtual: boolean; isPreferred: boolean }> = [];
|
|
220
|
+
|
|
221
|
+
// 虚拟/代理/虚拟网卡等常见关键字过滤
|
|
222
|
+
const virtualKeywords = [
|
|
223
|
+
'vmware', 'virtualbox', 'vbox', 'wsl', 'vethernet',
|
|
224
|
+
'meta', 'clash', 'zerotier', 'tailscale', 'tun',
|
|
225
|
+
'tap', 'loopback', 'vpn', 'host-only', 'sandbox'
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
// 物理网卡优先关键字
|
|
229
|
+
const physicalKeywords = [
|
|
230
|
+
'wlan', 'wifi', 'wireless', '无线', 'ethernet',
|
|
231
|
+
'以太', '乙太', '本地连接', '区域连接'
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
for (const devName in interfaces) {
|
|
235
|
+
const iface = interfaces[devName];
|
|
236
|
+
if (!iface) continue;
|
|
237
|
+
|
|
238
|
+
const nameLower = devName.toLowerCase();
|
|
239
|
+
const isVirtual = virtualKeywords.some(keyword => nameLower.includes(keyword));
|
|
240
|
+
const isPreferred = physicalKeywords.some(keyword => nameLower.includes(keyword));
|
|
241
|
+
|
|
242
|
+
for (let i = 0; i < iface.length; i++) {
|
|
243
|
+
const alias = iface[i];
|
|
244
|
+
if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
|
|
245
|
+
candidates.push({
|
|
246
|
+
name: devName,
|
|
247
|
+
address: alias.address,
|
|
248
|
+
isVirtual,
|
|
249
|
+
isPreferred
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 1. 优先筛选出非虚拟且被选为 Preferred 的物理网卡
|
|
256
|
+
const preferredPhysical = candidates.filter(c => !c.isVirtual && c.isPreferred);
|
|
257
|
+
if (preferredPhysical.length > 0) {
|
|
258
|
+
console.log(`[mDNS] 精准选择物理网卡: ${preferredPhysical[0].name} (${preferredPhysical[0].address})`);
|
|
259
|
+
return preferredPhysical[0].address;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 2. 其次筛选出非虚拟的其他网卡
|
|
263
|
+
const realCandidates = candidates.filter(c => !c.isVirtual);
|
|
264
|
+
if (realCandidates.length > 0) {
|
|
265
|
+
console.log(`[mDNS] 选择非虚拟网卡: ${realCandidates[0].name} (${realCandidates[0].address})`);
|
|
266
|
+
return realCandidates[0].address;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 3. 如果全部是虚拟网卡,看是否有 Preferred 的虚拟网卡
|
|
270
|
+
const preferredVirtual = candidates.filter(c => c.isPreferred);
|
|
271
|
+
if (preferredVirtual.length > 0) {
|
|
272
|
+
console.log(`[mDNS] 未检测到物理网卡,选择首选虚拟网卡: ${preferredVirtual[0].name} (${preferredVirtual[0].address})`);
|
|
273
|
+
return preferredVirtual[0].address;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 4. 兜底返回第一个非本地回环地址
|
|
277
|
+
if (candidates.length > 0) {
|
|
278
|
+
console.log(`[mDNS] 未检测到匹配网卡,使用首个 IPv4 网卡: ${candidates[0].name} (${candidates[0].address})`);
|
|
279
|
+
return candidates[0].address;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return '127.0.0.1';
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
2
|
+
import { Peer } from '../types/peer';
|
|
3
|
+
import { MdnsService } from './mdnsService';
|
|
4
|
+
import { FileService } from './fileService';
|
|
5
|
+
|
|
6
|
+
export class SocketService {
|
|
7
|
+
private wss: WebSocketServer | null = null;
|
|
8
|
+
private clients: Set<WebSocket> = new Set();
|
|
9
|
+
private isStarted = false;
|
|
10
|
+
|
|
11
|
+
private constructor() {}
|
|
12
|
+
|
|
13
|
+
public static getInstance(): SocketService {
|
|
14
|
+
const globalSymbols = global as any;
|
|
15
|
+
if (!globalSymbols.__socket_service_instance__) {
|
|
16
|
+
globalSymbols.__socket_service_instance__ = new SocketService();
|
|
17
|
+
}
|
|
18
|
+
return globalSymbols.__socket_service_instance__;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 在指定端口启动 WebSocket 服务器,服务于本机浏览器前端 (绑定 0.0.0.0 以支持局域网外端穿透)
|
|
23
|
+
*/
|
|
24
|
+
public start(port: number): void {
|
|
25
|
+
if (this.isStarted) {
|
|
26
|
+
console.log(`[WebSocket] 通信服务已在运行中,无需重复启动。`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log(`[WebSocket] 正在启动通信服务, 监听端口: ${port}, 绑定 Host: 0.0.0.0`);
|
|
31
|
+
try {
|
|
32
|
+
this.wss = new WebSocketServer({ port, host: '0.0.0.0' });
|
|
33
|
+
|
|
34
|
+
this.wss.on('connection', (ws: WebSocket, req: any) => {
|
|
35
|
+
this.clients.add(ws);
|
|
36
|
+
|
|
37
|
+
let clientId: string | null = null;
|
|
38
|
+
let nickname = '局域网伙伴';
|
|
39
|
+
let avatar = 'avatar-1';
|
|
40
|
+
let os = 'Windows';
|
|
41
|
+
try {
|
|
42
|
+
const urlObj = new URL(req.url || '', 'http://localhost');
|
|
43
|
+
clientId = urlObj.searchParams.get('clientId');
|
|
44
|
+
nickname = urlObj.searchParams.get('nickname') || '局域网伙伴';
|
|
45
|
+
avatar = urlObj.searchParams.get('avatar') || 'avatar-1';
|
|
46
|
+
os = urlObj.searchParams.get('os') || 'Windows';
|
|
47
|
+
} catch (e) {}
|
|
48
|
+
|
|
49
|
+
console.log(`[WebSocket] 客户端已建立连接. ClientId: ${clientId || 'unknown'}. 当前连接数: ${this.clients.size}`);
|
|
50
|
+
|
|
51
|
+
// 精准获取局域网内客户端物理 IPv4 地址 (处理本地 IPv6 回环前缀及反向代理转发字段)
|
|
52
|
+
let clientIp = req.socket.remoteAddress || '127.0.0.1';
|
|
53
|
+
if (clientIp.startsWith('::ffff:')) {
|
|
54
|
+
clientIp = clientIp.substring(7);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const forwardedFor = req.headers['x-forwarded-for'];
|
|
58
|
+
if (forwardedFor) {
|
|
59
|
+
const ip = typeof forwardedFor === 'string' ? forwardedFor.split(',')[0].trim() : forwardedFor[0].trim();
|
|
60
|
+
if (ip) {
|
|
61
|
+
clientIp = ip;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const mdns = MdnsService.getInstance();
|
|
66
|
+
|
|
67
|
+
// 智能修正:若 WebSocket 客户端来自本机本地回环,将其映射为局域网物理 IP 以支持外部网络通信
|
|
68
|
+
if (clientIp === '::1' || clientIp === '127.0.0.1' || clientIp === 'localhost') {
|
|
69
|
+
clientIp = mdns.getLocalIp();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (clientId) {
|
|
73
|
+
(ws as any).clientId = clientId;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (clientId && clientId !== mdns.getSelfId()) {
|
|
77
|
+
mdns.registerWebPeer(clientId, clientIp, nickname, avatar, 3000, os);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 监听客户端发来的测试或控制指令
|
|
81
|
+
ws.on('message', (message: string) => {
|
|
82
|
+
try {
|
|
83
|
+
const parsed = JSON.parse(message);
|
|
84
|
+
console.log('[WebSocket] 收到前端控制消息:', parsed);
|
|
85
|
+
|
|
86
|
+
// 收到前端中转互传握手请求:'transfer:request'
|
|
87
|
+
if (parsed.event === 'transfer:request') {
|
|
88
|
+
const { targetClientId, ...metadata } = parsed.data;
|
|
89
|
+
console.log(`[WebSocket] 转发文件传输申请,目标客户端: ${targetClientId}`);
|
|
90
|
+
this.sendToClient(targetClientId, 'transfer:request', metadata);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 收到接收端点击“拒绝文件接收”:'transfer:reject'
|
|
94
|
+
if (parsed.event === 'transfer:reject') {
|
|
95
|
+
const { taskId } = parsed.data;
|
|
96
|
+
console.log(`[WebSocket] 收到互传任务拒绝信令: ${taskId}`);
|
|
97
|
+
// 广播给所有人,同步前端任务列表状态
|
|
98
|
+
this.broadcast('transfer:reject', { taskId });
|
|
99
|
+
}
|
|
100
|
+
} catch (e) {
|
|
101
|
+
// 忽略非 JSON 数据
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
ws.on('close', () => {
|
|
106
|
+
this.clients.delete(ws);
|
|
107
|
+
console.log(`[WebSocket] 客户端已断开连接. ClientId: ${clientId || 'unknown'}. 当前连接数: ${this.clients.size}`);
|
|
108
|
+
|
|
109
|
+
if (clientId) {
|
|
110
|
+
const mdns = MdnsService.getInstance();
|
|
111
|
+
if (clientId !== mdns.getSelfId()) {
|
|
112
|
+
mdns.unregisterWebPeer(clientId);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
ws.on('error', (err) => {
|
|
118
|
+
console.error('[WebSocket] 连接出现异常:', err);
|
|
119
|
+
this.clients.delete(ws);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// 建立连接后,给前端发送一个欢迎及就绪包
|
|
123
|
+
this.sendTo(ws, 'system:ready', { timestamp: Date.now() });
|
|
124
|
+
|
|
125
|
+
// 智能自愈补发:当客户端重新连入或刷新页面时,若有针对它的 pending(待接收)传输任务,补发 WS 握手提示
|
|
126
|
+
if (clientId) {
|
|
127
|
+
const pendingTasks = FileService.getInstance().getPendingTasksForClient(clientId);
|
|
128
|
+
for (const task of pendingTasks) {
|
|
129
|
+
console.log(`[WebSocket] [自愈补发] 客户端 ${clientId} 上线,重新推送待接收任务: ${task.fileName}`);
|
|
130
|
+
this.sendTo(ws, 'transfer:request', {
|
|
131
|
+
taskId: task.id,
|
|
132
|
+
senderId: task.senderId,
|
|
133
|
+
senderName: task.senderName,
|
|
134
|
+
fileName: task.fileName,
|
|
135
|
+
fileSize: task.fileSize,
|
|
136
|
+
downloadUrl: task.downloadUrl
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
this.wss.on('error', (err: any) => {
|
|
143
|
+
if (err.code === 'EADDRINUSE') {
|
|
144
|
+
console.warn(`[WebSocket] 端口 ${port} 已被占用,可能之前的热更新进程仍在活动中。将跳过重新绑定。`);
|
|
145
|
+
this.isStarted = true;
|
|
146
|
+
} else {
|
|
147
|
+
console.error('[WebSocket] 服务端捕获到未知异常:', err);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
this.isStarted = true;
|
|
152
|
+
} catch (err: any) {
|
|
153
|
+
console.error('[WebSocket] 服务启动发生异常:', err);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 关闭 WebSocket 服务
|
|
159
|
+
*/
|
|
160
|
+
public stop(): void {
|
|
161
|
+
if (this.wss) {
|
|
162
|
+
this.wss.close(() => {
|
|
163
|
+
console.log('[WebSocket] 通信服务已关闭。');
|
|
164
|
+
});
|
|
165
|
+
this.wss = null;
|
|
166
|
+
}
|
|
167
|
+
this.clients.clear();
|
|
168
|
+
this.isStarted = false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 向所有连接的前端页面广播事件
|
|
173
|
+
* @param event 事件名
|
|
174
|
+
* @param data 附带的数据体
|
|
175
|
+
*/
|
|
176
|
+
public broadcast(event: string, data: any): void {
|
|
177
|
+
const payload = JSON.stringify({ event, data });
|
|
178
|
+
for (const client of this.clients) {
|
|
179
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
180
|
+
client.send(payload);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 智能自愈:当传输任务完成 (transfer:complete),自动物理清理发送端暂存大文件
|
|
185
|
+
if (event === 'transfer:complete' && data && data.taskId) {
|
|
186
|
+
console.log(`[WebSocket] 检测到互传完成广播,自动启动发送端物理暂存文件自愈清理. TaskId: ${data.taskId}`);
|
|
187
|
+
FileService.getInstance().cleanupTransferFile(data.taskId);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 向单个连接发送消息
|
|
193
|
+
*/
|
|
194
|
+
private sendTo(ws: WebSocket, event: string, data: any): void {
|
|
195
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
196
|
+
ws.send(JSON.stringify({ event, data }));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 向指定 clientId 定向发送消息
|
|
202
|
+
*/
|
|
203
|
+
public sendToClient(targetClientId: string, event: string, data: any): boolean {
|
|
204
|
+
const payload = JSON.stringify({ event, data });
|
|
205
|
+
let sent = false;
|
|
206
|
+
for (const client of this.clients) {
|
|
207
|
+
if ((client as any).clientId === targetClientId && client.readyState === WebSocket.OPEN) {
|
|
208
|
+
client.send(payload);
|
|
209
|
+
sent = true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return sent;
|
|
213
|
+
}
|
|
214
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "react-jsx",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [
|
|
17
|
+
{
|
|
18
|
+
"name": "next"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"paths": {
|
|
22
|
+
"@/*": ["./src/*"]
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"include": [
|
|
26
|
+
"next-env.d.ts",
|
|
27
|
+
"**/*.ts",
|
|
28
|
+
"**/*.tsx",
|
|
29
|
+
".next/types/**/*.ts",
|
|
30
|
+
".next/dev/types/**/*.ts",
|
|
31
|
+
"**/*.mts"
|
|
32
|
+
],
|
|
33
|
+
"exclude": ["node_modules"]
|
|
34
|
+
}
|