dsh-plugin-mobile-link 0.6.6 → 0.8.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/lib/unicode.js ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * UTF-16 安全边界:落单代理项修复与按码元安全截断。
3
+ *
4
+ * 为什么需要(2026-10-17 事故的同一个根):
5
+ *
6
+ * JS 字符串按 UTF-16 码元索引。凡是 `text.slice(0, n)` 这类截断,n 落在代理对
7
+ * 中间时就会留下**落单的高代理项**(`\ud83d`),序列化成 JSON 后是 `"\ud83d…"`。
8
+ * 这种帧不是合法 Unicode:中转要按 UTF-8 计费,编码直接抛错(Python 版当年就是
9
+ * 这么把连接打死的);iOS 的 `JSONDecoder` 会整条拒绝(`Missing low code point
10
+ * in surrogate pair`)。中转现在会用 U+FFFD 修好再转发,但**生产端不该发出这种
11
+ * 帧**——它是本连接器的出口,帧有效性归这里。
12
+ *
13
+ * 两件事分开:
14
+ * 1. `repairLoneSurrogates`:出口兜底。帧里已经有落单代理项时替换成 U+FFFD,
15
+ * 与中转的修复口径一致;
16
+ * 2. `truncateSafe` / `sliceRangeSafe`:自己截断时就不制造落单代理项(预览、
17
+ * 搜索片段、diff 正文)。兜底只防别人的数据,自己造出来的必须从源头修。
18
+ */
19
+
20
+ /** JSON 转义里的四位十六进制(大小写都认)。 */
21
+ const HEX_UNIT = /^[0-9a-fA-F]{4}$/
22
+
23
+ function hexUnitAt(text, at) {
24
+ if (at + 4 > text.length) return -1
25
+ const raw = text.slice(at, at + 4)
26
+ return HEX_UNIT.test(raw) ? Number.parseInt(raw, 16) : -1
27
+ }
28
+
29
+ /** 这个反斜杠本身是不是被转义了(前面有奇数个连续反斜杠)。 */
30
+ function isEscapedBackslash(text, index) {
31
+ let count = 0
32
+ for (let i = index - 1; i >= 0 && text[i] === '\\'; i -= 1) count += 1
33
+ return count % 2 === 1
34
+ }
35
+
36
+ function isHighUnit(unit) {
37
+ return unit >= 0xd800 && unit <= 0xdbff
38
+ }
39
+
40
+ function isLowUnit(unit) {
41
+ return unit >= 0xdc00 && unit <= 0xdfff
42
+ }
43
+
44
+ /**
45
+ * 扫描一段 JSON 文本,把**落单**的 `\uD800`–`\uDFFF` 转义换成 `\ufffd`。
46
+ *
47
+ * 只认真正的转义(`\\ud83d` 是字面量);合法配对原样保留。替换串与转义同为
48
+ * 6 个字符,帧长度不变。返回值带修复条数,调用方据此决定要不要记一条告警。
49
+ *
50
+ * @param {string} text JSON 文本
51
+ * @returns {{ text: string, repaired: number }}
52
+ */
53
+ export function repairLoneSurrogatesWithCount(text) {
54
+ if (typeof text !== 'string') return { text, repaired: 0 }
55
+ // 廉价前置判断:正常帧连 `\ud`/`\uD` 都没有,不做完整扫描。
56
+ if (text.indexOf('\\ud') < 0 && text.indexOf('\\uD') < 0) return { text, repaired: 0 }
57
+
58
+ let out = ''
59
+ let last = 0
60
+ let repaired = 0
61
+ for (let i = 0; i + 5 < text.length; i += 1) {
62
+ if (text[i] !== '\\' || text[i + 1] !== 'u') continue
63
+ if (isEscapedBackslash(text, i)) continue
64
+ const unit = hexUnitAt(text, i + 2)
65
+ if (unit < 0) continue
66
+ if (isHighUnit(unit)) {
67
+ let paired = false
68
+ if (text[i + 6] === '\\' && text[i + 7] === 'u') {
69
+ const next = hexUnitAt(text, i + 8)
70
+ paired = isLowUnit(next)
71
+ }
72
+ if (paired) {
73
+ i += 11 // 整个 `\uXXXX\uXXXX` 是合法配对:跳过 12 个字符
74
+ continue
75
+ }
76
+ } else if (!isLowUnit(unit)) {
77
+ continue
78
+ }
79
+ out += `${text.slice(last, i)}\\ufffd`
80
+ last = i + 6
81
+ repaired += 1
82
+ }
83
+ if (repaired === 0) return { text, repaired: 0 }
84
+ return { text: out + text.slice(last), repaired }
85
+ }
86
+
87
+ /** `repairLoneSurrogatesWithCount` 只取文本的那一半。 */
88
+ export function repairLoneSurrogates(text) {
89
+ return repairLoneSurrogatesWithCount(text).text
90
+ }
91
+
92
+ /**
93
+ * 截到 `end` 个**码元**为止,且不把代理对切开。
94
+ *
95
+ * 与 `text.slice(0, end)` 的差别只有一种:第 `end` 个码元是高代理项时把它让给
96
+ * 后半截(结果最多少 1 个码元),从而不会留下落单的高代理项。
97
+ */
98
+ export function truncateSafe(text, end) {
99
+ if (typeof text !== 'string' || !(end > 0)) return ''
100
+ if (text.length <= end) return text
101
+ const unit = text.charCodeAt(end - 1)
102
+ return text.slice(0, isHighUnit(unit) ? end - 1 : end)
103
+ }
104
+
105
+ /**
106
+ * `text.slice(start, end)` 的安全版本:起点若落在低代理项上就后移一位,终点若
107
+ * 落在高代理项上就前移一位;两端都不会制造落单代理项。
108
+ */
109
+ export function sliceRangeSafe(text, start, end) {
110
+ if (typeof text !== 'string') return ''
111
+ let from = Math.max(0, Math.min(Number(start) || 0, text.length))
112
+ let to = Math.max(from, Math.min(Number(end) || 0, text.length))
113
+ if (isLowUnit(text.charCodeAt(from))) from += 1
114
+ if (isHighUnit(text.charCodeAt(to - 1))) to -= 1
115
+ return text.slice(from, Math.max(from, to))
116
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-mobile-link",
3
- "version": "0.6.6",
3
+ "version": "0.8.0",
4
4
  "description": "Put your PC's DeepSeek Harness in your pocket: dials a relay over WSS so the native iOS client reaches this machine even without a public IP.",
5
5
  "keywords": [
6
6
  "dsh",