@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.
Files changed (3) hide show
  1. package/lib/cli.js +13 -10
  2. package/lib/share.js +38 -4
  3. 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 url = shareUrl(shareDir, published.storedName, port)
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(`链接 ${url}`)
244
- if (!lanAddress()) out('⚠️ 本机没有局域网 IPv4 地址,手机可能访问不到这个链接。')
244
+ out(`局域网 ${urlLan}`)
245
+ out(`公网v6 ${urlV6 ?? '(本机没有公网 IPv6)'}`)
246
+ if (!lanAddress()) out('⚠️ 本机没有局域网 IPv4 地址。')
245
247
 
246
- const res = await client.sendText(
247
- account.token,
248
- inbound.userId,
249
- `📎 ${published.originalName}(${published.bytes} 字节)\n在手机上点开下载:\n${url}`,
250
- inbound.contextToken,
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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[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
- server.listen(port, '0.0.0.0', () => {
168
- log(`share server listening on 0.0.0.0:${port} dir=${shareDir}`)
169
- log(`phone URL base: http://${lanAddress() ?? '<no-lan-ip>'}:${port}/f/${token}/`)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peng272/dsh-wechat-ilink",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "WeChat ClawBot (Tencent iLink) channel for DeepSeek Harness 0.2.0-rc.2 — talk to DSH from WeChat.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",