@peng272/dsh-wechat-ilink 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/cli.js +13 -10
- package/lib/share.js +38 -4
- package/package.json +1 -1
package/lib/cli.js
CHANGED
|
@@ -236,22 +236,25 @@ async function cmdShare(args) {
|
|
|
236
236
|
const shareDir = resolveShareDir()
|
|
237
237
|
const port = Number(args[1]) || DEFAULT_PORT
|
|
238
238
|
const published = publishFile(shareDir, abs)
|
|
239
|
-
const
|
|
239
|
+
const urlLan = shareUrl(shareDir, published.storedName, port)
|
|
240
|
+
const urlV6 = shareUrlV6(shareDir, published.storedName, port)
|
|
240
241
|
|
|
241
242
|
out(`已发布 ${published.originalName} (${published.bytes} 字节)`)
|
|
242
243
|
out(`存放于 ${shareDir}`)
|
|
243
|
-
out(
|
|
244
|
-
|
|
244
|
+
out(`局域网 ${urlLan}`)
|
|
245
|
+
out(`公网v6 ${urlV6 ?? '(本机没有公网 IPv6)'}`)
|
|
246
|
+
if (!lanAddress()) out('⚠️ 本机没有局域网 IPv4 地址。')
|
|
245
247
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
)
|
|
248
|
+
// Two links on purpose: which one works depends on where the phone is, and the
|
|
249
|
+
// phone is the only place that can decide. Same Wi-Fi -> the LAN link; mobile
|
|
250
|
+
// data -> the IPv6 link (if the carrier routes inbound).
|
|
251
|
+
const lines = [`📎 ${published.originalName}(${published.bytes} 字节)`, '', '手机和电脑在同一个 Wi-Fi 时,点这个:', urlLan]
|
|
252
|
+
if (urlV6) lines.push('', '手机用流量(不同网络)时,点这个:', urlV6)
|
|
253
|
+
else lines.push('', '(本机没有公网 IPv6,离开局域网需要别的通道)')
|
|
254
|
+
|
|
255
|
+
const res = await client.sendText(account.token, inbound.userId, lines.join('\n'), inbound.contextToken)
|
|
252
256
|
if (res?.message_id) {
|
|
253
257
|
out(`✅ 链接消息已发送 message_id=${res.message_id}`)
|
|
254
|
-
out(' (请确认手机收到;若打不开,通常是手机不在同一 Wi-Fi)')
|
|
255
258
|
return 0
|
|
256
259
|
}
|
|
257
260
|
out(`⚠️ 发送返回: ${JSON.stringify(res)}`)
|
package/lib/share.js
CHANGED
|
@@ -33,6 +33,28 @@ export function lanAddress() {
|
|
|
33
33
|
return null
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Global (public) IPv6 address, or null.
|
|
38
|
+
*
|
|
39
|
+
* Prefers a stable interface identifier (`SuffixOrigin: link`, i.e. EUI-64 style)
|
|
40
|
+
* over a privacy-extension one, because the prefix may rotate while the suffix
|
|
41
|
+
* does not. Falls back to any global unicast address.
|
|
42
|
+
*/
|
|
43
|
+
export function publicV6Address() {
|
|
44
|
+
const ifaces = os.networkInterfaces()
|
|
45
|
+
const globals = []
|
|
46
|
+
for (const list of Object.values(ifaces)) {
|
|
47
|
+
for (const entry of list ?? []) {
|
|
48
|
+
if (entry.family !== 'IPv6' || entry.internal) continue
|
|
49
|
+
if (!/^(2|3)/.test(entry.address)) continue // global unicast only (2000::/3)
|
|
50
|
+
globals.push(entry)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (globals.length === 0) return null
|
|
54
|
+
const stable = globals.find((e) => e.scopeid === 0 && /^(240e|2400|2401|2409|2408)/.test(e.address))
|
|
55
|
+
return (stable ?? globals[0]).address
|
|
56
|
+
}
|
|
57
|
+
|
|
36
58
|
/** Read the share token, creating it on first use. */
|
|
37
59
|
export function ensureToken(shareDir) {
|
|
38
60
|
fs.mkdirSync(shareDir, { recursive: true })
|
|
@@ -65,13 +87,21 @@ export function publishFile(shareDir, sourcePath) {
|
|
|
65
87
|
return { storedName, bytes: fs.statSync(sourcePath).size, originalName }
|
|
66
88
|
}
|
|
67
89
|
|
|
68
|
-
/** Build the URL a phone can open. */
|
|
90
|
+
/** Build the URL a phone can open over the local network. */
|
|
69
91
|
export function shareUrl(shareDir, storedName, port = DEFAULT_PORT) {
|
|
70
92
|
const token = ensureToken(shareDir)
|
|
71
93
|
const host = lanAddress() ?? '127.0.0.1'
|
|
72
94
|
return `http://${host}:${port}/f/${token}/${encodeURIComponent(storedName)}`
|
|
73
95
|
}
|
|
74
96
|
|
|
97
|
+
/** Same file over the public IPv6 address (reachable when the phone is NOT on the LAN). */
|
|
98
|
+
export function shareUrlV6(shareDir, storedName, port = DEFAULT_PORT) {
|
|
99
|
+
const host = publicV6Address()
|
|
100
|
+
if (!host) return null
|
|
101
|
+
const token = ensureToken(shareDir)
|
|
102
|
+
return `http://[${host}]:${port}/f/${token}/${encodeURIComponent(storedName)}`
|
|
103
|
+
}
|
|
104
|
+
|
|
75
105
|
function escapeHtml(text) {
|
|
76
106
|
return String(text).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c])
|
|
77
107
|
}
|
|
@@ -164,9 +194,13 @@ export function startShareServer({ shareDir, port = DEFAULT_PORT, log = () => {}
|
|
|
164
194
|
fs.createReadStream(resolved).pipe(res)
|
|
165
195
|
})
|
|
166
196
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
197
|
+
// Dual-stack: bind the IPv6 wildcard, which also accepts IPv4 on Windows, so
|
|
198
|
+
// the same port serves the LAN URL and the public IPv6 URL.
|
|
199
|
+
server.listen(port, '::', () => {
|
|
200
|
+
log(`share server listening on [::]:${port} dir=${shareDir}`)
|
|
201
|
+
log(`LAN URL base : http://${lanAddress() ?? '<no-lan-ip>'}:${port}/f/${token}/`)
|
|
202
|
+
const v6 = publicV6Address()
|
|
203
|
+
log(`public V6 base: ${v6 ? `http://[${v6}]:${port}/f/${token}/` : '(no global IPv6)'}`)
|
|
170
204
|
})
|
|
171
205
|
|
|
172
206
|
return server
|
package/package.json
CHANGED