virlen-remote 0.1.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 (99) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +192 -0
  4. package/dist/index.d.ts +44 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +34 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/protocol/answer.d.ts +48 -0
  9. package/dist/protocol/answer.d.ts.map +1 -0
  10. package/dist/protocol/answer.js +65 -0
  11. package/dist/protocol/answer.js.map +1 -0
  12. package/dist/protocol/api.d.ts +576 -0
  13. package/dist/protocol/api.d.ts.map +1 -0
  14. package/dist/protocol/api.js +38 -0
  15. package/dist/protocol/api.js.map +1 -0
  16. package/dist/protocol/endpoint.d.ts +76 -0
  17. package/dist/protocol/endpoint.d.ts.map +1 -0
  18. package/dist/protocol/endpoint.js +307 -0
  19. package/dist/protocol/endpoint.js.map +1 -0
  20. package/dist/protocol/errors.d.ts +32 -0
  21. package/dist/protocol/errors.d.ts.map +1 -0
  22. package/dist/protocol/errors.js +56 -0
  23. package/dist/protocol/errors.js.map +1 -0
  24. package/dist/protocol/frame.d.ts +51 -0
  25. package/dist/protocol/frame.d.ts.map +1 -0
  26. package/dist/protocol/frame.js +154 -0
  27. package/dist/protocol/frame.js.map +1 -0
  28. package/dist/protocol/hello.d.ts +73 -0
  29. package/dist/protocol/hello.d.ts.map +1 -0
  30. package/dist/protocol/hello.js +37 -0
  31. package/dist/protocol/hello.js.map +1 -0
  32. package/dist/protocol/host.d.ts +116 -0
  33. package/dist/protocol/host.d.ts.map +1 -0
  34. package/dist/protocol/host.js +52 -0
  35. package/dist/protocol/host.js.map +1 -0
  36. package/dist/protocol/identity.d.ts +107 -0
  37. package/dist/protocol/identity.d.ts.map +1 -0
  38. package/dist/protocol/identity.js +143 -0
  39. package/dist/protocol/identity.js.map +1 -0
  40. package/dist/protocol/ids.d.ts +2 -0
  41. package/dist/protocol/ids.d.ts.map +1 -0
  42. package/dist/protocol/ids.js +20 -0
  43. package/dist/protocol/ids.js.map +1 -0
  44. package/dist/protocol/pairing.d.ts +43 -0
  45. package/dist/protocol/pairing.d.ts.map +1 -0
  46. package/dist/protocol/pairing.js +78 -0
  47. package/dist/protocol/pairing.js.map +1 -0
  48. package/dist/testing/index.d.ts +6 -0
  49. package/dist/testing/index.d.ts.map +1 -0
  50. package/dist/testing/index.js +5 -0
  51. package/dist/testing/index.js.map +1 -0
  52. package/dist/testing/mock-host.d.ts +66 -0
  53. package/dist/testing/mock-host.d.ts.map +1 -0
  54. package/dist/testing/mock-host.js +573 -0
  55. package/dist/testing/mock-host.js.map +1 -0
  56. package/dist/transport/broadcast.d.ts +26 -0
  57. package/dist/transport/broadcast.d.ts.map +1 -0
  58. package/dist/transport/broadcast.js +71 -0
  59. package/dist/transport/broadcast.js.map +1 -0
  60. package/dist/transport/ice.d.ts +130 -0
  61. package/dist/transport/ice.d.ts.map +1 -0
  62. package/dist/transport/ice.js +338 -0
  63. package/dist/transport/ice.js.map +1 -0
  64. package/dist/transport/memory.d.ts +52 -0
  65. package/dist/transport/memory.d.ts.map +1 -0
  66. package/dist/transport/memory.js +135 -0
  67. package/dist/transport/memory.js.map +1 -0
  68. package/dist/transport/rtc.d.ts +76 -0
  69. package/dist/transport/rtc.d.ts.map +1 -0
  70. package/dist/transport/rtc.js +310 -0
  71. package/dist/transport/rtc.js.map +1 -0
  72. package/dist/transport/signaling.d.ts +101 -0
  73. package/dist/transport/signaling.d.ts.map +1 -0
  74. package/dist/transport/signaling.js +235 -0
  75. package/dist/transport/signaling.js.map +1 -0
  76. package/dist/transport/types.d.ts +41 -0
  77. package/dist/transport/types.d.ts.map +1 -0
  78. package/dist/transport/types.js +11 -0
  79. package/dist/transport/types.js.map +1 -0
  80. package/package.json +69 -0
  81. package/src/index.ts +155 -0
  82. package/src/protocol/answer.ts +89 -0
  83. package/src/protocol/api.ts +517 -0
  84. package/src/protocol/endpoint.ts +405 -0
  85. package/src/protocol/errors.ts +90 -0
  86. package/src/protocol/frame.ts +196 -0
  87. package/src/protocol/hello.ts +112 -0
  88. package/src/protocol/host.ts +150 -0
  89. package/src/protocol/identity.ts +184 -0
  90. package/src/protocol/ids.ts +20 -0
  91. package/src/protocol/pairing.ts +95 -0
  92. package/src/testing/index.ts +5 -0
  93. package/src/testing/mock-host.ts +691 -0
  94. package/src/transport/broadcast.ts +81 -0
  95. package/src/transport/ice.ts +402 -0
  96. package/src/transport/memory.ts +164 -0
  97. package/src/transport/rtc.ts +344 -0
  98. package/src/transport/signaling.ts +317 -0
  99. package/src/transport/types.ts +50 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,40 @@
1
+ # 变更记录
2
+
3
+ 本文件记录对外可见的变更(协议 / 导出面 / 行为)。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
4
+ 版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
5
+
6
+ ## [0.1.0] - 2026-09-30
7
+
8
+ 首次发布。内容为 Virlen「手机控制」M0–M6 期间沉淀的协议栈,两端(virlen-app / virlen-mobile)此前以源码方式共用,
9
+ 本版起改为从 npm 安装。
10
+
11
+ ### Added
12
+
13
+ - **协议层**:帧编解码 / 分片重组(`encodeFrames` / `Reassembler`)、RPC 端点(`Endpoint`:幂等、超时、重放)、
14
+ 错误码(`BridgeError` / `toBridgeError`)、能力协商(`negotiate` / `intersectCapabilities`)、
15
+ 方法表与 DTO(`createCaller` / `createSubscriber` / `registerHostHandlers`、`HostApi` / `MobileApi`)。
16
+ - **设备身份与授权凭证**:`newDeviceKey` / `roomFor` / `issueGrant` / `renewGrant` / `checkGrant` /
17
+ `describeGrantRemaining`(30 天滑动续期 + 90 天硬上限)。
18
+ - **配对载荷**:`buildPairingPayload` / `encodePairingPayload` / `parsePairingPayload` / `roomOfPayload`
19
+ (二维码内容,两端同一份;票据默认 5 分钟有效)。
20
+ - **传输层**:`Transport` 抽象;`RtcTransport`(WebRTC DataChannel,含被顶号 `E_REPLACED` 语义)、
21
+ `MemoryTransport` / `createMemoryPair`(测试)、`BroadcastTransport`(同源跨 tab 联调);
22
+ `SseSignalingClient`(角色化加入、顶号通知、`requireHostOnline`)、
23
+ `fetchRoomStatus` / `fetchHostOnlineMap`(批量在线查询,失败返回空而不抛错)。
24
+ - **流式下行**:`host.event.message.stream` 支持 `mode: 'full' | 'delta'` —— 客户端在 `hello` 里
25
+ 声明 `streamMode: 'delta'` 后只发新增后缀(带 `offset` 供重基准与缺口对齐):
26
+ 一条 n 字回复的下行带宽从 O(n²) 降到 O(n);未声明的客户端继续收整帧。
27
+ - **ICE 配置**:`resolveIceServers` / `fetchIceServers` / `sanitizeIceServers` / `parseIceText` /
28
+ `readCustomIceText` / `writeCustomIceText` —— 客户端**不含任何 TURN 凭证**,
29
+ 默认值由服务端 `GET <信令基址>/ice` 下发,本地缓存 + 多级降级。
30
+ - **测试辅助**(子路径 `virlen-remote/testing`):`createMockHostDataSource()`。
31
+
32
+ ### Notes
33
+
34
+ - **ESM only**,零运行时依赖,Node ≥ 18。
35
+ - 构建产物会补全相对导入的 `.js` 后缀(运行时 ESM 加载器要求),见 `scripts/build.mjs`。
36
+ - 对外类型不用 DOM 类型做契约(ICE 条目为 `IceServerInit`),避免要求消费方开启 DOM lib。
37
+ - 若消费方 tsconfig 关闭了 `strictNullChecks`,判别字段请用 `x.ok === false` 显式比较:
38
+ 真值判断不收敛联合类型。
39
+
40
+ [0.1.0]: https://github.com/WeiChens/virlen-remote/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 WeiChens
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # virlen-remote
2
+
3
+ Virlen「手机控制电脑」功能的**协议栈共享包** —— 电脑端(virlen-app)与手机端(virlen-mobile)
4
+ 共用同一份实现,避免两端各写一份契约然后慢慢漂移。
5
+
6
+ - **零运行时依赖**:WebRTC 用浏览器原生 `RTCPeerConnection`,信令用 `fetch` + `EventSource`,
7
+ Node 侧只用内存传输做测试。
8
+ - **ESM only**(`"type": "module"`):Node ≥ 18,或任意现代打包器(Vite / webpack / esbuild / Rollup)。
9
+ - 自带类型声明(`.d.ts` + `.d.ts.map`,随包附源码便于跳转)。
10
+ - **源码与产物里都没有任何 TURN 凭证** —— ICE 默认值由服务端下发,见下文。
11
+
12
+ ## 安装
13
+
14
+ ```bash
15
+ npm i virlen-remote
16
+ # 或 pnpm add virlen-remote / yarn add virlen-remote
17
+ ```
18
+
19
+ ```ts
20
+ import { Endpoint, RtcTransport } from 'virlen-remote'
21
+ import { createMockHostDataSource } from 'virlen-remote/testing' // 仅测试/联调用
22
+ ```
23
+
24
+ ## 它包含什么
25
+
26
+ | 分层 | 内容 |
27
+ |---|---|
28
+ | `protocol/` | 帧编解码与分片重组、RPC(`Endpoint` / 幂等 / 超时 / 重放)、错误码、能力协商、方法表与 DTO、设备身份与授权凭证、配对载荷、流式下行(整帧 / 增量) |
29
+ | `transport/` | `Transport` 抽象 + 三种实现(`RtcTransport` / `MemoryTransport` / `BroadcastTransport`)、`SseSignalingClient`(信令)、ICE 配置解析 |
30
+ | `testing/`(子路径) | `createMockHostDataSource()`:可直接挂到 `registerHostHandlers` 的 mock 宿主,供两端联调与单测使用 |
31
+
32
+ ## 快速开始
33
+
34
+ ### 电脑端(host,发起 offer)
35
+
36
+ ```ts
37
+ import {
38
+ Endpoint,
39
+ RtcTransport,
40
+ SseSignalingClient,
41
+ registerHostHandlers,
42
+ newDeviceKey,
43
+ roomFor,
44
+ resolveIceServers,
45
+ } from 'virlen-remote'
46
+
47
+ // 设备 key 首次生成后**必须持久化**:它既是「这台电脑是谁」,也是房间号的来源
48
+ const deviceKey = newDeviceKey('host')
49
+ const signalUrl = 'https://your-server/api/rtc/'
50
+ const room = roomFor(deviceKey)
51
+
52
+ // ICE 默认值从信令服务取(服务端配置;本机可自定义覆盖)
53
+ const ice = await resolveIceServers({ baseUrl: signalUrl, storage: localStorage })
54
+
55
+ const signaling = new SseSignalingClient({
56
+ baseUrl: signalUrl,
57
+ room,
58
+ role: 'host',
59
+ deviceKey,
60
+ clientName: '我的电脑',
61
+ })
62
+ const transport = new RtcTransport({ role: 'host', signaling, iceServers: ice.servers })
63
+ const endpoint = new Endpoint({ transport })
64
+
65
+ registerHostHandlers(endpoint, myDataSource) // 实现 HostDataSource 端口即可
66
+ void transport.start()
67
+ ```
68
+
69
+ ### 手机端(guest,应答方)
70
+
71
+ ```ts
72
+ import {
73
+ Endpoint,
74
+ RtcTransport,
75
+ SseSignalingClient,
76
+ createCaller,
77
+ type HostApi,
78
+ } from 'virlen-remote'
79
+
80
+ const signaling = new SseSignalingClient({
81
+ baseUrl: signalUrl,
82
+ room, // 与电脑端同一个房间(由电脑 key 派生)
83
+ role: 'guest',
84
+ deviceKey: mobileKey,
85
+ requireHostOnline: true, // 房间里没有电脑时立刻失败,而不是空等超时
86
+ })
87
+ const transport = new RtcTransport({ role: 'guest', signaling, iceServers: ice.servers })
88
+ await transport.start()
89
+ await transport.whenReady()
90
+
91
+ const caller = createCaller<HostApi>(new Endpoint({ transport }))
92
+ const hello = await caller.call('host.hello', {
93
+ protocolVersion: 1,
94
+ client: { platform: 'ios-web', appVersion: '0.1.0' },
95
+ capabilities: ['session.list', 'session.send'],
96
+ token, // 首次配对是一次性票据;之后是上一轮拿到的授权凭证
97
+ mobileKey,
98
+ mobileName: 'iPhone · 3f9a',
99
+ })
100
+ // 首次配对就在这里拿到长期凭证:
101
+ // hello.grant → { token: 'gt-…', issuedAt, expiresAt }
102
+ ```
103
+
104
+ ### 配对(二维码 / 配对串)
105
+
106
+ ```ts
107
+ import {
108
+ buildPairingPayload,
109
+ encodePairingPayload,
110
+ parsePairingPayload,
111
+ roomOfPayload,
112
+ } from 'virlen-remote'
113
+
114
+ const payload = buildPairingPayload({ host: deviceKey, name: '我的电脑', ticket, signal: signalUrl })
115
+ const qrText = encodePairingPayload(payload) // 画进二维码
116
+ const back = parsePairingPayload(qrText) // 手机端解析(无法识别时返回 null)
117
+ if (back) await connect(roomOfPayload(back), back.host, back.ticket)
118
+ ```
119
+
120
+ ## ICE / TURN 配置
121
+
122
+ **本包与两个客户端里都没有任何 TURN 凭证。** 默认值由**服务端**提供:
123
+
124
+ ```
125
+ GET <信令基址>/ice
126
+ → { "v": 1,
127
+ "iceServers": [ { "urls": "stun:turn.example.com:3478" },
128
+ { "urls": "turn:turn.example.com:3478", "username": "…", "credential": "…" } ],
129
+ "mode": "static" | "rest" | "none",
130
+ "ttlSec": 3600, "expiresAt": 1730000000000 }
131
+ ```
132
+
133
+ 客户端用 `resolveIceServers()` 解析,优先级与降级:
134
+
135
+ ```
136
+ 自定义(用户手填)
137
+ > 服务端下发的本地缓存(默认 6 小时)
138
+ > 服务端现取
139
+ > 过期缓存(信令暂时不可达时,旧的也比没有强)
140
+ > 空(仅本机候选;局域网可用,跨网多半不行)
141
+ ```
142
+
143
+ ```ts
144
+ import { resolveIceServers } from 'virlen-remote'
145
+
146
+ const ice = await resolveIceServers({
147
+ baseUrl: signalUrl,
148
+ customText: localStorage.getItem('virlen.rtc.ice'), // 用户自定义(空 = 用服务端默认)
149
+ storage: localStorage, // 缓存位置
150
+ })
151
+ ice.servers // → 交给 new RTCPeerConnection({ iceServers })
152
+ ice.source // → 'custom' | 'remote' | 'cache' | 'stale-cache' | 'none'
153
+ ice.detail // → 给用户看的中文说明(两端同一套口径)
154
+ ice.warning // → 降级原因(如「信令服务暂不可达,先用上次缓存」)
155
+ ```
156
+
157
+ 要点:
158
+
159
+ - **永不抛错**:任何一步失败都降级到下一优先级,并把来源如实报给上层 ——
160
+ 「拿不到 TURN 配置」不该让用户连不上,只该让跨网连通率变差;
161
+ - 服务端可配**静态口令**,也可用 coturn `use-auth-secret` 下发**临时凭证**
162
+ (HMAC-SHA1 现算,密钥永不下发;被抄走也只有一小时额度);
163
+ - 凭证会随 `iceServers` 一起进内存,因此**不要**把它写进日志或埋点
164
+ (本包提供 `iceCount` / `iceSource` 这类只记元数据的写法)。
165
+
166
+ ## 约定与边界
167
+
168
+ - **不用 DOM 类型做对外契约**:例如 ICE 条目是结构等价的 `IceServerInit`
169
+ 而非 DOM 的 `RTCIceServer`,避免把「需要 DOM lib」扩散到所有消费方;
170
+ - `Transport.onError?()` 是**可选**方法:只有 `RtcTransport` 有致命错误通道(如被顶号 `E_REPLACED`);
171
+ - **布尔判别字段请显式比较**(`x.ok === false` 而不是 `!x.ok`):若消费方 tsconfig 关了
172
+ `strictNullChecks`,真值判断不收敛联合类型;
173
+ - **被顶号不要自动重连**:信令服务是「后来者优先」,自动重连会让两台设备互相顶号;
174
+ - 房间号由电脑设备 key 派生(`roomFor`):**知道 key 就能进房间**,授权关口是凭证校验。
175
+
176
+ ## 维护者:构建与发布
177
+
178
+ ```bash
179
+ pnpm install
180
+ pnpm typecheck && pnpm test # 类型检查 + 用例
181
+ pnpm build # 产出 dist/(ESM + .d.ts)
182
+ npm publish # prepublishOnly 会自动跑上面三步
183
+ ```
184
+
185
+ `pnpm build` 在 `tsc` 之后会把产物里的**相对导入补上 `.js` 后缀**:
186
+ 源码里的免后缀写法对打包器友好,但**运行时 ESM 加载器不认**(`./protocol/frame` 会 404)。
187
+ 本包是被 `import` 直接消费的,所以产物必须是合法 ESM。实现见 `scripts/build.mjs`
188
+ (零新增依赖,逐文件可 diff)。
189
+
190
+ ## License
191
+
192
+ [MIT](./LICENSE)
@@ -0,0 +1,44 @@
1
+ /**
2
+ * virlen-remote —— 手机控制协议栈共享包
3
+ *
4
+ * 电脑端(virlen-app)与手机端(virlen-mobile)共用同一份实现
5
+ * (见 docs/phone-control-bridge.md §1「越往下越同构」、§6「共享包」)。
6
+ *
7
+ * 分层:
8
+ * protocol/ 与传输无关的逻辑(帧 / RPC / 错误 / 能力协商 / 方法表)
9
+ * transport/ 字节通道抽象 + 内存实现(RTC 实现属 M3)
10
+ *
11
+ * **零运行时依赖**:WebRTC 用浏览器原生 `RTCPeerConnection`;Node 侧只用 memory transport 做测试。
12
+ */
13
+ export { PROTOCOL_VERSION, HEADER_SIZE, DEFAULT_MAX_FRAME_PAYLOAD, FrameKind } from './protocol/frame.js';
14
+ export { encodeHeader, decodeHeader, frameBody, encodeFrames, Reassembler } from './protocol/frame.js';
15
+ export type { FrameHeader, DecodedMessage } from './protocol/frame.js';
16
+ export { BridgeError, toBridgeError } from './protocol/errors.js';
17
+ export type { ErrorCode, WireError, BridgeErrorOptions } from './protocol/errors.js';
18
+ export { newRequestId } from './protocol/ids.js';
19
+ export { HOST_KEY_PREFIX, MOBILE_KEY_PREFIX, GRANT_PREFIX, ROOM_PREFIX, GRANT_TTL_MS, GRANT_MAX_LIFETIME_MS, randomKey, newDeviceKey, newGrantToken, isDeviceKey, roomFor, hostKeyFromRoom, issueGrant, isGrantExpired, renewGrant, checkGrant, describeGrantRemaining, } from './protocol/identity.js';
20
+ export type { DeviceKind, GrantRecord, CredentialRejectReason, RoomStatus } from './protocol/identity.js';
21
+ export { PAIRING_PAYLOAD_VERSION, PAIRING_TICKET_TTL_MS, buildPairingPayload, encodePairingPayload, parsePairingPayload, roomOfPayload, } from './protocol/pairing.js';
22
+ export type { PairingPayload } from './protocol/pairing.js';
23
+ export { normalizeChoiceAnswer, answerActionError, CHOICE_JOINER } from './protocol/answer.js';
24
+ export type { ChoiceAnswer } from './protocol/answer.js';
25
+ export { negotiate, intersectCapabilities } from './protocol/hello.js';
26
+ export { Endpoint } from './protocol/endpoint.js';
27
+ export type { CallContext, EventContext, RpcHandler, EventSubscriber, CallOptions, EndpointOptions, } from './protocol/endpoint.js';
28
+ export { createCaller, createSubscriber, registerHandlers } from './protocol/api.js';
29
+ export { DEFAULT_CONTEXT_WINDOW_TOKENS, COMPRESS_MIN_RATIO } from './protocol/api.js';
30
+ export type { ParamsOf, ResultOf, TypedCaller, TypedSubscriber, HandlerImpl, HostApi, MobileApi, HostEvents, MobileEvents, SessionSummaryDTO, MessageDTO, RuntimeDTO, InteractionDTO, InteractionOutcome, StreamMode, ApprovalTier, SendParams, MsgPageParams, MsgPageDTO, AnswerParams, AnswerAction, AnswerResult, AnswerRejectReason, CreateSessionParams, RenameSessionParams, PinSessionParams, DeleteSessionParams, ModelProviderDTO, WorkspaceOptionDTO, ContextInfoDTO, SetModelParams, ContextParams, CompressParams, } from './protocol/api.js';
31
+ export type { HelloParams, HelloResult, ClientInfo, NegotiationInput, Negotiated } from './protocol/hello.js';
32
+ export { registerHostHandlers } from './protocol/host.js';
33
+ export type { HostDataSource, HostEmit, HostRegistration, RegisterHostOptions } from './protocol/host.js';
34
+ export type { Transport, TransportState } from './transport/types.js';
35
+ export { MemoryTransport, createMemoryPair } from './transport/memory.js';
36
+ export type { MemoryTransportMetrics } from './transport/memory.js';
37
+ export { BroadcastTransport, createBroadcastPair } from './transport/broadcast.js';
38
+ export { SseSignalingClient, fetchRoomStatus, fetchHostOnlineMap } from './transport/signaling.js';
39
+ export type { SignalingChannel, SignalingRole, SseSignalingOptions, EventSourceLike, KickedInfo, FetchRoomStatusOptions, } from './transport/signaling.js';
40
+ export { RtcTransport } from './transport/rtc.js';
41
+ export type { RtcTransportOptions } from './transport/rtc.js';
42
+ export { ICE_CONFIG_VERSION, ICE_API_PATH, ICE_CUSTOM_STORAGE_KEY, ICE_REMOTE_STORAGE_KEY, ICE_CACHE_TTL_MS, ICE_FETCH_TIMEOUT_MS, sanitizeIceServers, parseIceText, formatIceServers, describeIceSource, fetchIceServers, resolveIceServers, readCustomIceText, writeCustomIceText, } from './transport/ice.js';
43
+ export type { IceServerInit, IceSource, IceStoragePort, ResolvedIceServers, ParseIceResult, FetchIceOptions, ResolveIceOptions, } from './transport/ice.js';
44
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,34 @@
1
+ /**
2
+ * virlen-remote —— 手机控制协议栈共享包
3
+ *
4
+ * 电脑端(virlen-app)与手机端(virlen-mobile)共用同一份实现
5
+ * (见 docs/phone-control-bridge.md §1「越往下越同构」、§6「共享包」)。
6
+ *
7
+ * 分层:
8
+ * protocol/ 与传输无关的逻辑(帧 / RPC / 错误 / 能力协商 / 方法表)
9
+ * transport/ 字节通道抽象 + 内存实现(RTC 实现属 M3)
10
+ *
11
+ * **零运行时依赖**:WebRTC 用浏览器原生 `RTCPeerConnection`;Node 侧只用 memory transport 做测试。
12
+ */
13
+ // ── protocol ──
14
+ export { PROTOCOL_VERSION, HEADER_SIZE, DEFAULT_MAX_FRAME_PAYLOAD, FrameKind } from './protocol/frame.js';
15
+ export { encodeHeader, decodeHeader, frameBody, encodeFrames, Reassembler } from './protocol/frame.js';
16
+ export { BridgeError, toBridgeError } from './protocol/errors.js';
17
+ export { newRequestId } from './protocol/ids.js';
18
+ // ── 设备身份与授权凭证(M6,§30)──
19
+ export { HOST_KEY_PREFIX, MOBILE_KEY_PREFIX, GRANT_PREFIX, ROOM_PREFIX, GRANT_TTL_MS, GRANT_MAX_LIFETIME_MS, randomKey, newDeviceKey, newGrantToken, isDeviceKey, roomFor, hostKeyFromRoom, issueGrant, isGrantExpired, renewGrant, checkGrant, describeGrantRemaining, } from './protocol/identity.js';
20
+ // ── 配对载荷(二维码内容,两端同一份)──
21
+ export { PAIRING_PAYLOAD_VERSION, PAIRING_TICKET_TTL_MS, buildPairingPayload, encodePairingPayload, parsePairingPayload, roomOfPayload, } from './protocol/pairing.js';
22
+ export { normalizeChoiceAnswer, answerActionError, CHOICE_JOINER } from './protocol/answer.js';
23
+ export { negotiate, intersectCapabilities } from './protocol/hello.js';
24
+ export { Endpoint } from './protocol/endpoint.js';
25
+ export { createCaller, createSubscriber, registerHandlers } from './protocol/api.js';
26
+ export { DEFAULT_CONTEXT_WINDOW_TOKENS, COMPRESS_MIN_RATIO } from './protocol/api.js';
27
+ export { registerHostHandlers } from './protocol/host.js';
28
+ export { MemoryTransport, createMemoryPair } from './transport/memory.js';
29
+ export { BroadcastTransport, createBroadcastPair } from './transport/broadcast.js';
30
+ export { SseSignalingClient, fetchRoomStatus, fetchHostOnlineMap } from './transport/signaling.js';
31
+ export { RtcTransport } from './transport/rtc.js';
32
+ // ── ICE 配置(M7,§31):客户端不再内置任何 TURN 凭证 ──
33
+ export { ICE_CONFIG_VERSION, ICE_API_PATH, ICE_CUSTOM_STORAGE_KEY, ICE_REMOTE_STORAGE_KEY, ICE_CACHE_TTL_MS, ICE_FETCH_TIMEOUT_MS, sanitizeIceServers, parseIceText, formatIceServers, describeIceSource, fetchIceServers, resolveIceServers, readCustomIceText, writeCustomIceText, } from './transport/ice.js';
34
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,iBAAiB;AACjB,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,yBAAyB,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACtG,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,SAAS,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAGnG,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAG9D,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAE7C,yBAAyB;AACzB,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,qBAAqB,EACrB,SAAS,EACT,YAAY,EACZ,aAAa,EACb,WAAW,EACX,OAAO,EACP,eAAe,EACf,UAAU,EACV,cAAc,EACd,UAAU,EACV,UAAU,EACV,sBAAsB,GACvB,MAAM,qBAAqB,CAAA;AAG5B,yBAAyB;AACzB,OAAO,EACL,uBAAuB,EACvB,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,aAAa,GACd,MAAM,oBAAoB,CAAA;AAG3B,OAAO,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAG3F,OAAO,EAAE,SAAS,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AAEnE,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAU9C,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AACjF,OAAO,EAAE,6BAA6B,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAsClF,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAItD,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAEtE,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAC/E,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAS/F,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAG9C,yCAAyC;AACzC,OAAO,EACL,kBAAkB,EAClB,YAAY,EACZ,sBAAsB,EACtB,sBAAsB,EACtB,gBAAgB,EAChB,oBAAoB,EACpB,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,iBAAiB,CAAA"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * 应答载荷的规范化 —— **手机侧与电脑侧共用同一份**(M4)。
3
+ *
4
+ * 为什么必须共用:手机回答「AI 提问」时,最终要交给引擎的是一个 `ToolResult`
5
+ * (`{ content, uiData }`),而电脑侧 UI 早就定下了这个形态(见 `tool-ui.tsx`):
6
+ * `content` = `[selected.join(', '), customReply].filter(Boolean).join(';')`。
7
+ *
8
+ * 如果手机侧、电脑侧桥接层、mock 宿主各写一份「怎么拼」,三份必然漂移
9
+ * —— 而漂移的表现是「AI 收到了内容,但消息渲染不出来(`uiData` 缺失)」这种**看不出错在谁**的故障。
10
+ * 故:**唯一实现放这里**,其余全部调用它。
11
+ *
12
+ * 手机端允许三种输入(宽容解析,避免因 UI 形态变化而连带改协议):
13
+ * - 字符串 → 等价于「自定义回复」;
14
+ * - 字符串数组 → 等价于「选中了这些选项」;
15
+ * - 对象 `{ selected?, customReply? }` → 精确形态(推荐)。
16
+ *
17
+ * 空结果一律返回 `null`(调用方据此回 `invalid-value`)——
18
+ * 给引擎发一条空回执会让 AI 无从判断用户到底选了什么。
19
+ */
20
+ import type { AnswerAction, AnswerRejectReason, InteractionDTO } from './api.js';
21
+ export interface ChoiceAnswer {
22
+ content: string;
23
+ uiData: {
24
+ selected: string[];
25
+ customReply: string;
26
+ };
27
+ }
28
+ /** 分隔符:与电脑侧 UI 的 `t(';')` 一致(中文分号)。 */
29
+ export declare const CHOICE_JOINER = "\uFF1B";
30
+ export declare function normalizeChoiceAnswer(value: unknown): ChoiceAnswer | null;
31
+ /**
32
+ * 应答动作的**合法性校验** —— 真实电脑侧(`interaction-registry`)与 mock 宿主共用同一份。
33
+ *
34
+ * 为什么放在共享包(M4 教训延续,2026-09-27 真机反馈):动作「对某类交互是否合法」是**协议语义**,
35
+ * 不是某一端的实现细节。两端各写一份时 mock 会偏宽松 ——
36
+ * 「手机对 AI 提问点取消 → 真实电脑端回 `unsupported-by-host`」这类缺陷在手机端单测里测不出来,
37
+ * 只能等到真机才暴露。
38
+ *
39
+ * 判据(与桌面 `tool-ui.tsx` 的按钮一一对应):
40
+ * - `choice`(AI 提问):`choose` 确认 / `deny` 取消 / `shelve` 暂存;`allow` 无意义;
41
+ * - `authorization`(授权):`allow` / `deny` / `shelve`;`choose` 无意义;
42
+ * - `authorization + presentation: 'terminal'`(终端内确认):**没有暂存语义**
43
+ * (它是「命令已就绪,等你按回车」,没有可暂存的对象)。
44
+ *
45
+ * @returns 不合法时返回拒绝原因(固定 `unsupported-by-host`);合法时返回 `null`。
46
+ */
47
+ export declare function answerActionError(interaction: Pick<InteractionDTO, 'kind' | 'presentation'>, action: AnswerAction): AnswerRejectReason | null;
48
+ //# sourceMappingURL=answer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"answer.d.ts","sourceRoot":"","sources":["../../src/protocol/answer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,OAAO,CAAA;AAE7E,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAA;CACpD;AAED,uCAAuC;AACvC,eAAO,MAAM,aAAa,WAAM,CAAA;AAEhC,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,GAAG,IAAI,CA6BzE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,cAAc,CAAC,EAC1D,MAAM,EAAE,YAAY,GACnB,kBAAkB,GAAG,IAAI,CAS3B"}
@@ -0,0 +1,65 @@
1
+ /** 分隔符:与电脑侧 UI 的 `t(';')` 一致(中文分号)。 */
2
+ export const CHOICE_JOINER = ';';
3
+ export function normalizeChoiceAnswer(value) {
4
+ let selected = [];
5
+ let customReply = '';
6
+ if (typeof value === 'string') {
7
+ customReply = value.trim();
8
+ }
9
+ else if (Array.isArray(value)) {
10
+ selected = value
11
+ .filter((v) => typeof v === 'string')
12
+ .map((v) => v.trim())
13
+ .filter(Boolean);
14
+ }
15
+ else if (value && typeof value === 'object') {
16
+ const v = value;
17
+ if (Array.isArray(v.selected)) {
18
+ selected = v.selected
19
+ .filter((x) => typeof x === 'string')
20
+ .map((x) => x.trim())
21
+ .filter(Boolean);
22
+ }
23
+ if (typeof v.customReply === 'string')
24
+ customReply = v.customReply.trim();
25
+ }
26
+ else {
27
+ return null;
28
+ }
29
+ if (!selected.length && !customReply)
30
+ return null;
31
+ const parts = [];
32
+ if (selected.length)
33
+ parts.push(selected.join(', '));
34
+ if (customReply)
35
+ parts.push(customReply);
36
+ return { content: parts.join(CHOICE_JOINER), uiData: { selected, customReply } };
37
+ }
38
+ /**
39
+ * 应答动作的**合法性校验** —— 真实电脑侧(`interaction-registry`)与 mock 宿主共用同一份。
40
+ *
41
+ * 为什么放在共享包(M4 教训延续,2026-09-27 真机反馈):动作「对某类交互是否合法」是**协议语义**,
42
+ * 不是某一端的实现细节。两端各写一份时 mock 会偏宽松 ——
43
+ * 「手机对 AI 提问点取消 → 真实电脑端回 `unsupported-by-host`」这类缺陷在手机端单测里测不出来,
44
+ * 只能等到真机才暴露。
45
+ *
46
+ * 判据(与桌面 `tool-ui.tsx` 的按钮一一对应):
47
+ * - `choice`(AI 提问):`choose` 确认 / `deny` 取消 / `shelve` 暂存;`allow` 无意义;
48
+ * - `authorization`(授权):`allow` / `deny` / `shelve`;`choose` 无意义;
49
+ * - `authorization + presentation: 'terminal'`(终端内确认):**没有暂存语义**
50
+ * (它是「命令已就绪,等你按回车」,没有可暂存的对象)。
51
+ *
52
+ * @returns 不合法时返回拒绝原因(固定 `unsupported-by-host`);合法时返回 `null`。
53
+ */
54
+ export function answerActionError(interaction, action) {
55
+ if (interaction.kind === 'choice') {
56
+ return action === 'allow' ? 'unsupported-by-host' : null;
57
+ }
58
+ if (action === 'choose')
59
+ return 'unsupported-by-host';
60
+ if (action === 'shelve' && interaction.presentation === 'terminal') {
61
+ return 'unsupported-by-host';
62
+ }
63
+ return null;
64
+ }
65
+ //# sourceMappingURL=answer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"answer.js","sourceRoot":"","sources":["../../src/protocol/answer.ts"],"names":[],"mappings":"AA0BA,uCAAuC;AACvC,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,CAAA;AAEhC,MAAM,UAAU,qBAAqB,CAAC,KAAc;IAClD,IAAI,QAAQ,GAAa,EAAE,CAAA;IAC3B,IAAI,WAAW,GAAG,EAAE,CAAA;IAEpB,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,WAAW,GAAG,KAAK,CAAC,IAAI,EAAE,CAAA;IAC5B,CAAC;SAAM,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAChC,QAAQ,GAAG,KAAK;aACb,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC;aACjD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;aACpB,MAAM,CAAC,OAAO,CAAC,CAAA;IACpB,CAAC;SAAM,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9C,MAAM,CAAC,GAAG,KAAsD,CAAA;QAChE,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC9B,QAAQ,GAAG,CAAC,CAAC,QAAQ;iBAClB,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC;iBACjD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;iBACpB,MAAM,CAAC,OAAO,CAAC,CAAA;QACpB,CAAC;QACD,IAAI,OAAO,CAAC,CAAC,WAAW,KAAK,QAAQ;YAAE,WAAW,GAAG,CAAC,CAAC,WAAW,CAAC,IAAI,EAAE,CAAA;IAC3E,CAAC;SAAM,CAAC;QACN,OAAO,IAAI,CAAA;IACb,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,MAAM,IAAI,CAAC,WAAW;QAAE,OAAO,IAAI,CAAA;IACjD,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,IAAI,QAAQ,CAAC,MAAM;QAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;IACpD,IAAI,WAAW;QAAE,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;IACxC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,WAAW,EAAE,EAAE,CAAA;AAClF,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,iBAAiB,CAC/B,WAA0D,EAC1D,MAAoB;IAEpB,IAAI,WAAW,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAClC,OAAO,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,IAAI,CAAA;IAC1D,CAAC;IACD,IAAI,MAAM,KAAK,QAAQ;QAAE,OAAO,qBAAqB,CAAA;IACrD,IAAI,MAAM,KAAK,QAAQ,IAAI,WAAW,CAAC,YAAY,KAAK,UAAU,EAAE,CAAC;QACnE,OAAO,qBAAqB,CAAA;IAC9B,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC"}