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.
Files changed (228) hide show
  1. package/.next/BUILD_ID +1 -0
  2. package/.next/app-path-routes-manifest.json +19 -0
  3. package/.next/build-manifest.json +20 -0
  4. package/.next/dev/build-manifest.json +18 -0
  5. package/.next/dev/cache/webpack/client-development/0.pack.gz +0 -0
  6. package/.next/dev/cache/webpack/client-development/index.pack.gz +0 -0
  7. package/.next/dev/cache/webpack/server-development/0.pack.gz +0 -0
  8. package/.next/dev/cache/webpack/server-development/index.pack.gz +0 -0
  9. package/.next/dev/react-loadable-manifest.json +1 -0
  10. package/.next/dev/server/app/api/config/route_client-reference-manifest.js +1 -0
  11. package/.next/dev/server/app/api/documents/route_client-reference-manifest.js +1 -0
  12. package/.next/dev/server/app/api/init/route_client-reference-manifest.js +1 -0
  13. package/.next/dev/server/app/api/peers/route_client-reference-manifest.js +1 -0
  14. package/.next/dev/server/app/api/transfer/download/route_client-reference-manifest.js +1 -0
  15. package/.next/dev/server/app/api/transfer/prepare/route_client-reference-manifest.js +1 -0
  16. package/.next/dev/server/app/api/transfer/shared/download/route_client-reference-manifest.js +1 -0
  17. package/.next/dev/server/app/api/transfer/shared/route_client-reference-manifest.js +1 -0
  18. package/.next/dev/server/app/api/transfer/tasks/route_client-reference-manifest.js +1 -0
  19. package/.next/dev/server/app/page_client-reference-manifest.js +1 -0
  20. package/.next/dev/server/app-paths-manifest.json +3 -0
  21. package/.next/dev/server/middleware-build-manifest.js +18 -0
  22. package/.next/dev/server/middleware-react-loadable-manifest.js +1 -0
  23. package/.next/dev/server/next-font-manifest.js +1 -0
  24. package/.next/dev/server/next-font-manifest.json +1 -0
  25. package/.next/dev/server/pages-manifest.json +1 -0
  26. package/.next/dev/server/server-reference-manifest.js +1 -0
  27. package/.next/dev/server/server-reference-manifest.json +5 -0
  28. 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
  29. package/.next/dev/server/webpack-runtime.js +209 -0
  30. package/.next/dev/static/development/_buildManifest.js +1 -0
  31. package/.next/dev/static/development/_ssgManifest.js +1 -0
  32. package/.next/dev/types/app/layout.ts +87 -0
  33. package/.next/dev/types/app/page.ts +87 -0
  34. package/.next/dev/types/package.json +1 -0
  35. package/.next/diagnostics/build-diagnostics.json +6 -0
  36. package/.next/diagnostics/framework.json +1 -0
  37. package/.next/export-marker.json +6 -0
  38. package/.next/images-manifest.json +68 -0
  39. package/.next/next-minimal-server.js.nft.json +1 -0
  40. package/.next/next-server.js.nft.json +1 -0
  41. package/.next/package.json +1 -0
  42. package/.next/prerender-manifest.json +114 -0
  43. package/.next/react-loadable-manifest.json +1 -0
  44. package/.next/required-server-files.js +337 -0
  45. package/.next/required-server-files.json +337 -0
  46. package/.next/routes-manifest.json +147 -0
  47. package/.next/server/app/_global-error/page.js +32 -0
  48. package/.next/server/app/_global-error/page.js.nft.json +1 -0
  49. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -0
  50. package/.next/server/app/_global-error.html +1 -0
  51. package/.next/server/app/_global-error.meta +16 -0
  52. package/.next/server/app/_global-error.rsc +14 -0
  53. package/.next/server/app/_global-error.segments/_full.segment.rsc +14 -0
  54. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +5 -0
  55. package/.next/server/app/_global-error.segments/_global-error.segment.rsc +5 -0
  56. package/.next/server/app/_global-error.segments/_head.segment.rsc +5 -0
  57. package/.next/server/app/_global-error.segments/_index.segment.rsc +5 -0
  58. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -0
  59. package/.next/server/app/_not-found/page.js +7 -0
  60. package/.next/server/app/_not-found/page.js.nft.json +1 -0
  61. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
  62. package/.next/server/app/_not-found.html +6 -0
  63. package/.next/server/app/_not-found.meta +16 -0
  64. package/.next/server/app/_not-found.rsc +18 -0
  65. package/.next/server/app/_not-found.segments/_full.segment.rsc +18 -0
  66. package/.next/server/app/_not-found.segments/_head.segment.rsc +6 -0
  67. package/.next/server/app/_not-found.segments/_index.segment.rsc +5 -0
  68. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +5 -0
  69. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +5 -0
  70. package/.next/server/app/_not-found.segments/_tree.segment.rsc +4 -0
  71. package/.next/server/app/api/board/sync/route.js +1 -0
  72. package/.next/server/app/api/board/sync/route.js.nft.json +1 -0
  73. package/.next/server/app/api/board/sync/route_client-reference-manifest.js +1 -0
  74. package/.next/server/app/api/config/route.js +1 -0
  75. package/.next/server/app/api/config/route.js.nft.json +1 -0
  76. package/.next/server/app/api/config/route_client-reference-manifest.js +1 -0
  77. package/.next/server/app/api/documents/route.js +1 -0
  78. package/.next/server/app/api/documents/route.js.nft.json +1 -0
  79. package/.next/server/app/api/documents/route_client-reference-manifest.js +1 -0
  80. package/.next/server/app/api/documents/sync/route.js +1 -0
  81. package/.next/server/app/api/documents/sync/route.js.nft.json +1 -0
  82. package/.next/server/app/api/documents/sync/route_client-reference-manifest.js +1 -0
  83. package/.next/server/app/api/init/route.js +1 -0
  84. package/.next/server/app/api/init/route.js.nft.json +1 -0
  85. package/.next/server/app/api/init/route_client-reference-manifest.js +1 -0
  86. package/.next/server/app/api/peers/route.js +1 -0
  87. package/.next/server/app/api/peers/route.js.nft.json +1 -0
  88. package/.next/server/app/api/peers/route_client-reference-manifest.js +1 -0
  89. package/.next/server/app/api/transfer/download/route.js +1 -0
  90. package/.next/server/app/api/transfer/download/route.js.nft.json +1 -0
  91. package/.next/server/app/api/transfer/download/route_client-reference-manifest.js +1 -0
  92. package/.next/server/app/api/transfer/prepare/route.js +1 -0
  93. package/.next/server/app/api/transfer/prepare/route.js.nft.json +1 -0
  94. package/.next/server/app/api/transfer/prepare/route_client-reference-manifest.js +1 -0
  95. package/.next/server/app/api/transfer/request/route.js +1 -0
  96. package/.next/server/app/api/transfer/request/route.js.nft.json +1 -0
  97. package/.next/server/app/api/transfer/request/route_client-reference-manifest.js +1 -0
  98. package/.next/server/app/api/transfer/shared/download/route.js +1 -0
  99. package/.next/server/app/api/transfer/shared/download/route.js.nft.json +1 -0
  100. package/.next/server/app/api/transfer/shared/download/route_client-reference-manifest.js +1 -0
  101. package/.next/server/app/api/transfer/shared/route.js +1 -0
  102. package/.next/server/app/api/transfer/shared/route.js.nft.json +1 -0
  103. package/.next/server/app/api/transfer/shared/route_client-reference-manifest.js +1 -0
  104. package/.next/server/app/api/transfer/start/route.js +1 -0
  105. package/.next/server/app/api/transfer/start/route.js.nft.json +1 -0
  106. package/.next/server/app/api/transfer/start/route_client-reference-manifest.js +1 -0
  107. package/.next/server/app/api/transfer/tasks/route.js +1 -0
  108. package/.next/server/app/api/transfer/tasks/route.js.nft.json +1 -0
  109. package/.next/server/app/api/transfer/tasks/route_client-reference-manifest.js +1 -0
  110. package/.next/server/app/favicon.ico/route.js +1 -0
  111. package/.next/server/app/favicon.ico/route.js.nft.json +1 -0
  112. package/.next/server/app/favicon.ico.body +0 -0
  113. package/.next/server/app/favicon.ico.meta +1 -0
  114. package/.next/server/app/index.html +6 -0
  115. package/.next/server/app/index.meta +14 -0
  116. package/.next/server/app/index.rsc +21 -0
  117. package/.next/server/app/index.segments/__PAGE__.segment.rsc +10 -0
  118. package/.next/server/app/index.segments/_full.segment.rsc +21 -0
  119. package/.next/server/app/index.segments/_head.segment.rsc +6 -0
  120. package/.next/server/app/index.segments/_index.segment.rsc +5 -0
  121. package/.next/server/app/index.segments/_tree.segment.rsc +5 -0
  122. package/.next/server/app/page.js +115 -0
  123. package/.next/server/app/page.js.nft.json +1 -0
  124. package/.next/server/app/page_client-reference-manifest.js +1 -0
  125. package/.next/server/app-paths-manifest.json +19 -0
  126. package/.next/server/chunks/31.js +2 -0
  127. package/.next/server/chunks/4.js +1 -0
  128. package/.next/server/chunks/404.js +1 -0
  129. package/.next/server/chunks/516.js +1 -0
  130. package/.next/server/chunks/718.js +45 -0
  131. package/.next/server/chunks/887.js +1 -0
  132. package/.next/server/chunks/891.js +18 -0
  133. package/.next/server/functions-config-manifest.json +4 -0
  134. package/.next/server/interception-route-rewrite-manifest.js +1 -0
  135. package/.next/server/middleware-build-manifest.js +1 -0
  136. package/.next/server/middleware-manifest.json +6 -0
  137. package/.next/server/middleware-react-loadable-manifest.js +1 -0
  138. package/.next/server/next-font-manifest.js +1 -0
  139. package/.next/server/next-font-manifest.json +1 -0
  140. package/.next/server/pages/404.html +6 -0
  141. package/.next/server/pages/500.html +1 -0
  142. package/.next/server/pages-manifest.json +4 -0
  143. package/.next/server/prefetch-hints.json +1 -0
  144. package/.next/server/server-reference-manifest.js +1 -0
  145. package/.next/server/server-reference-manifest.json +1 -0
  146. package/.next/server/webpack-runtime.js +1 -0
  147. package/.next/static/JAumJsHdasa7bQ7jxx37q/_buildManifest.js +1 -0
  148. package/.next/static/JAumJsHdasa7bQ7jxx37q/_ssgManifest.js +1 -0
  149. package/.next/static/chunks/148-2d58b90f6dc8cfaf.js +32 -0
  150. package/.next/static/chunks/5d8f0495-d87c92750ebe0885.js +82 -0
  151. package/.next/static/chunks/649-20578e0ca00dbde1.js +14 -0
  152. package/.next/static/chunks/991cd08a-0938e33045166413.js +1 -0
  153. package/.next/static/chunks/app/_global-error/page-a02f063e5aaa162f.js +1 -0
  154. package/.next/static/chunks/app/_not-found/page-5696c6e9b39a4885.js +1 -0
  155. package/.next/static/chunks/app/api/board/sync/route-a02f063e5aaa162f.js +1 -0
  156. package/.next/static/chunks/app/api/config/route-a02f063e5aaa162f.js +1 -0
  157. package/.next/static/chunks/app/api/documents/route-a02f063e5aaa162f.js +1 -0
  158. package/.next/static/chunks/app/api/documents/sync/route-a02f063e5aaa162f.js +1 -0
  159. package/.next/static/chunks/app/api/init/route-a02f063e5aaa162f.js +1 -0
  160. package/.next/static/chunks/app/api/peers/route-a02f063e5aaa162f.js +1 -0
  161. package/.next/static/chunks/app/api/transfer/download/route-a02f063e5aaa162f.js +1 -0
  162. package/.next/static/chunks/app/api/transfer/prepare/route-a02f063e5aaa162f.js +1 -0
  163. package/.next/static/chunks/app/api/transfer/request/route-a02f063e5aaa162f.js +1 -0
  164. package/.next/static/chunks/app/api/transfer/shared/download/route-a02f063e5aaa162f.js +1 -0
  165. package/.next/static/chunks/app/api/transfer/shared/route-a02f063e5aaa162f.js +1 -0
  166. package/.next/static/chunks/app/api/transfer/start/route-a02f063e5aaa162f.js +1 -0
  167. package/.next/static/chunks/app/api/transfer/tasks/route-a02f063e5aaa162f.js +1 -0
  168. package/.next/static/chunks/app/layout-f1c1109b37ee9a67.js +1 -0
  169. package/.next/static/chunks/app/page-d667dde9ae730a9e.js +15 -0
  170. package/.next/static/chunks/be838f7e-d7eb8d1a464523ea.js +1 -0
  171. package/.next/static/chunks/framework-1af2d653ea416252.js +1 -0
  172. package/.next/static/chunks/main-app-2475c374c5ac40b5.js +1 -0
  173. package/.next/static/chunks/main-c04fb4d60a5182d4.js +5 -0
  174. package/.next/static/chunks/next/dist/client/components/builtin/app-error-a02f063e5aaa162f.js +1 -0
  175. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-a02f063e5aaa162f.js +1 -0
  176. package/.next/static/chunks/next/dist/client/components/builtin/global-error-9771ee9f95e5e628.js +1 -0
  177. package/.next/static/chunks/next/dist/client/components/builtin/not-found-a02f063e5aaa162f.js +1 -0
  178. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-a02f063e5aaa162f.js +1 -0
  179. package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  180. package/.next/static/chunks/webpack-1bf2fae924e39757.js +1 -0
  181. package/.next/static/css/30baeec9f6889470.css +1 -0
  182. package/.next/static/css/30f58f83a0192172.css +1 -0
  183. package/.next/static/media/0f1bdadaf30e2d5f-s.woff2 +0 -0
  184. package/.next/static/media/22a5144ee8d83bca-s.p.woff2 +0 -0
  185. package/.next/static/media/2c34d62a75506231-s.woff2 +0 -0
  186. package/.next/static/media/601f5c280d60caca-s.woff2 +0 -0
  187. package/.next/static/media/9766a7e9e2e0ad5a-s.woff2 +0 -0
  188. package/.next/static/media/a115172161b307bb-s.woff2 +0 -0
  189. package/.next/static/media/aa016aab0e6d1295-s.woff2 +0 -0
  190. package/.next/static/media/b66cf8e69499582a-s.woff2 +0 -0
  191. package/.next/static/media/d100b2a099e34044-s.woff2 +0 -0
  192. package/.next/static/media/f5271587012faf78-s.p.woff2 +0 -0
  193. package/.next/static/media/f639721981034f88-s.woff2 +0 -0
  194. package/.next/trace +3 -0
  195. package/.next/trace-build +1 -0
  196. package/.next/types/app/api/board/sync/route.ts +351 -0
  197. package/.next/types/app/api/config/route.ts +351 -0
  198. package/.next/types/app/api/documents/route.ts +351 -0
  199. package/.next/types/app/api/documents/sync/route.ts +351 -0
  200. package/.next/types/app/api/init/route.ts +351 -0
  201. package/.next/types/app/api/peers/route.ts +351 -0
  202. package/.next/types/app/api/transfer/download/route.ts +351 -0
  203. package/.next/types/app/api/transfer/prepare/route.ts +351 -0
  204. package/.next/types/app/api/transfer/request/route.ts +351 -0
  205. package/.next/types/app/api/transfer/shared/download/route.ts +351 -0
  206. package/.next/types/app/api/transfer/shared/route.ts +351 -0
  207. package/.next/types/app/api/transfer/start/route.ts +351 -0
  208. package/.next/types/app/api/transfer/tasks/route.ts +351 -0
  209. package/.next/types/app/layout.ts +87 -0
  210. package/.next/types/app/page.ts +87 -0
  211. package/.next/types/cache-life.d.ts +145 -0
  212. package/.next/types/package.json +1 -0
  213. package/.next/types/routes.d.ts +85 -0
  214. package/.next/types/validator.ts +187 -0
  215. package/README.md +85 -0
  216. package/bin/cli.js +51 -0
  217. package/package.json +52 -0
  218. package/public/file.svg +1 -0
  219. package/public/globe.svg +1 -0
  220. package/public/next.svg +1 -0
  221. package/public/vercel.svg +1 -0
  222. package/public/window.svg +1 -0
  223. package/src/services/configService.ts +144 -0
  224. package/src/services/documentService.ts +210 -0
  225. package/src/services/fileService.ts +595 -0
  226. package/src/services/mdnsService.ts +284 -0
  227. package/src/services/socketService.ts +214 -0
  228. 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
+ }