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.
- package/CHANGELOG.md +40 -0
- package/LICENSE +21 -0
- package/README.md +192 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol/answer.d.ts +48 -0
- package/dist/protocol/answer.d.ts.map +1 -0
- package/dist/protocol/answer.js +65 -0
- package/dist/protocol/answer.js.map +1 -0
- package/dist/protocol/api.d.ts +576 -0
- package/dist/protocol/api.d.ts.map +1 -0
- package/dist/protocol/api.js +38 -0
- package/dist/protocol/api.js.map +1 -0
- package/dist/protocol/endpoint.d.ts +76 -0
- package/dist/protocol/endpoint.d.ts.map +1 -0
- package/dist/protocol/endpoint.js +307 -0
- package/dist/protocol/endpoint.js.map +1 -0
- package/dist/protocol/errors.d.ts +32 -0
- package/dist/protocol/errors.d.ts.map +1 -0
- package/dist/protocol/errors.js +56 -0
- package/dist/protocol/errors.js.map +1 -0
- package/dist/protocol/frame.d.ts +51 -0
- package/dist/protocol/frame.d.ts.map +1 -0
- package/dist/protocol/frame.js +154 -0
- package/dist/protocol/frame.js.map +1 -0
- package/dist/protocol/hello.d.ts +73 -0
- package/dist/protocol/hello.d.ts.map +1 -0
- package/dist/protocol/hello.js +37 -0
- package/dist/protocol/hello.js.map +1 -0
- package/dist/protocol/host.d.ts +116 -0
- package/dist/protocol/host.d.ts.map +1 -0
- package/dist/protocol/host.js +52 -0
- package/dist/protocol/host.js.map +1 -0
- package/dist/protocol/identity.d.ts +107 -0
- package/dist/protocol/identity.d.ts.map +1 -0
- package/dist/protocol/identity.js +143 -0
- package/dist/protocol/identity.js.map +1 -0
- package/dist/protocol/ids.d.ts +2 -0
- package/dist/protocol/ids.d.ts.map +1 -0
- package/dist/protocol/ids.js +20 -0
- package/dist/protocol/ids.js.map +1 -0
- package/dist/protocol/pairing.d.ts +43 -0
- package/dist/protocol/pairing.d.ts.map +1 -0
- package/dist/protocol/pairing.js +78 -0
- package/dist/protocol/pairing.js.map +1 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/mock-host.d.ts +66 -0
- package/dist/testing/mock-host.d.ts.map +1 -0
- package/dist/testing/mock-host.js +573 -0
- package/dist/testing/mock-host.js.map +1 -0
- package/dist/transport/broadcast.d.ts +26 -0
- package/dist/transport/broadcast.d.ts.map +1 -0
- package/dist/transport/broadcast.js +71 -0
- package/dist/transport/broadcast.js.map +1 -0
- package/dist/transport/ice.d.ts +130 -0
- package/dist/transport/ice.d.ts.map +1 -0
- package/dist/transport/ice.js +338 -0
- package/dist/transport/ice.js.map +1 -0
- package/dist/transport/memory.d.ts +52 -0
- package/dist/transport/memory.d.ts.map +1 -0
- package/dist/transport/memory.js +135 -0
- package/dist/transport/memory.js.map +1 -0
- package/dist/transport/rtc.d.ts +76 -0
- package/dist/transport/rtc.d.ts.map +1 -0
- package/dist/transport/rtc.js +310 -0
- package/dist/transport/rtc.js.map +1 -0
- package/dist/transport/signaling.d.ts +101 -0
- package/dist/transport/signaling.d.ts.map +1 -0
- package/dist/transport/signaling.js +235 -0
- package/dist/transport/signaling.js.map +1 -0
- package/dist/transport/types.d.ts +41 -0
- package/dist/transport/types.d.ts.map +1 -0
- package/dist/transport/types.js +11 -0
- package/dist/transport/types.js.map +1 -0
- package/package.json +69 -0
- package/src/index.ts +155 -0
- package/src/protocol/answer.ts +89 -0
- package/src/protocol/api.ts +517 -0
- package/src/protocol/endpoint.ts +405 -0
- package/src/protocol/errors.ts +90 -0
- package/src/protocol/frame.ts +196 -0
- package/src/protocol/hello.ts +112 -0
- package/src/protocol/host.ts +150 -0
- package/src/protocol/identity.ts +184 -0
- package/src/protocol/ids.ts +20 -0
- package/src/protocol/pairing.ts +95 -0
- package/src/testing/index.ts +5 -0
- package/src/testing/mock-host.ts +691 -0
- package/src/transport/broadcast.ts +81 -0
- package/src/transport/ice.ts +402 -0
- package/src/transport/memory.ts +164 -0
- package/src/transport/rtc.ts +344 -0
- package/src/transport/signaling.ts +317 -0
- package/src/transport/types.ts +50 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 设备身份与授权凭证(M6,见 docs/phone-control-bridge.md §30)。
|
|
3
|
+
*
|
|
4
|
+
* 一句话:**设备 key 是「谁」,授权凭证是「凭什么是它」。**
|
|
5
|
+
*
|
|
6
|
+
* - **设备 key**:首次运行生成一次,之后**持久化不变**(手机写 localStorage,电脑写
|
|
7
|
+
* `<data_dir>/phone-device.json`)。它不是硬件指纹 —— 见 §30.1 的取舍说明:
|
|
8
|
+
* PWA 拿不到可靠的硬件指纹(浏览器升级 / 隐私模式 / 清数据即变,同型号还会撞),
|
|
9
|
+
* 所以唯一能同时满足「不重复」与「重新获取还是同一个」的做法就是随机生成 + 落盘。
|
|
10
|
+
* - **房间号**由电脑设备 key 派生(`roomFor`):于是「第二次连接」只需要拿电脑 key 去问
|
|
11
|
+
* 信令服务「这台电脑在不在线」,不必再扫码。
|
|
12
|
+
* - **授权凭证**(grant):由**电脑端**签发,绑定手机设备 key,默认 30 天有效、
|
|
13
|
+
* 每次成功连接**滑动续期**,但单次签发最长 `GRANT_MAX_LIFETIME_MS`(90 天)——
|
|
14
|
+
* 到顶后必须重新扫码授权(用户 2026-09-27 拍板)。
|
|
15
|
+
*
|
|
16
|
+
* 本模块**零依赖、两端同一份**:电脑端与手机端都必须用这里的常量与判定函数,
|
|
17
|
+
* 否则必然出现「一端认为有效、另一端认为过期」的分叉(§18.5 的教训)。
|
|
18
|
+
*/
|
|
19
|
+
/** 电脑设备 key 前缀(desktop key)。 */
|
|
20
|
+
export const HOST_KEY_PREFIX = 'dk-';
|
|
21
|
+
/** 手机设备 key 前缀(mobile key)。 */
|
|
22
|
+
export const MOBILE_KEY_PREFIX = 'mk-';
|
|
23
|
+
/** 授权凭证前缀(grant)。 */
|
|
24
|
+
export const GRANT_PREFIX = 'gt-';
|
|
25
|
+
/** 授权凭证的单次续期时长(30 天)。 */
|
|
26
|
+
export const GRANT_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
27
|
+
/**
|
|
28
|
+
* 单次签发的**最长寿命**(90 天,从 `issuedAt` 起算)。
|
|
29
|
+
*
|
|
30
|
+
* 为什么要有硬上限:滑动续期若没有上限,一个凭证就等于永久授权 —— 手机丢失/被借用时
|
|
31
|
+
* 电脑端的「移除」是唯一刹车,而用户未必想得起来去看那张列表。
|
|
32
|
+
*/
|
|
33
|
+
export const GRANT_MAX_LIFETIME_MS = 90 * 24 * 60 * 60 * 1000;
|
|
34
|
+
/** 房间名前缀(信令层)。与 M3 起的旧房间名同构:`virlen:<电脑标识>`。 */
|
|
35
|
+
export const ROOM_PREFIX = 'virlen:';
|
|
36
|
+
/** 生成 `prefix + 十六进制随机串`(默认 16 字节 = 32 hex)。 */
|
|
37
|
+
export function randomKey(prefix, bytes = 16) {
|
|
38
|
+
const buf = new Uint8Array(bytes);
|
|
39
|
+
const c = globalThis.crypto;
|
|
40
|
+
if (c && typeof c.getRandomValues === 'function') {
|
|
41
|
+
c.getRandomValues(buf);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
// 无 WebCrypto 的环境(老 Node / 极端降级):Math.random 兜底。
|
|
45
|
+
// ⚠️ 只用于「本地身份标识」,不是加密码 —— 真正的安全边界是授权凭证与电脑端确认弹窗。
|
|
46
|
+
for (let i = 0; i < buf.length; i += 1)
|
|
47
|
+
buf[i] = Math.floor(Math.random() * 256);
|
|
48
|
+
}
|
|
49
|
+
let out = prefix;
|
|
50
|
+
for (const b of buf)
|
|
51
|
+
out += b.toString(16).padStart(2, '0');
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
/** 生成一个设备 key(`host` → `dk-…`,`mobile` → `mk-…`)。 */
|
|
55
|
+
export function newDeviceKey(kind) {
|
|
56
|
+
return randomKey(kind === 'host' ? HOST_KEY_PREFIX : MOBILE_KEY_PREFIX);
|
|
57
|
+
}
|
|
58
|
+
/** 生成一个授权凭证串。 */
|
|
59
|
+
export function newGrantToken() {
|
|
60
|
+
return randomKey(GRANT_PREFIX, 24);
|
|
61
|
+
}
|
|
62
|
+
const KEY_RE = /^(dk|mk)-[0-9a-f]{8,64}$/;
|
|
63
|
+
/**
|
|
64
|
+
* 是否为合法设备 key。`kind` 不传则不校验前缀归属。
|
|
65
|
+
*
|
|
66
|
+
* 只用于**早期拒掉明显是垃圾的输入**(日志可读、列表可渲染),不是安全校验 ——
|
|
67
|
+
* 真正的授权判定在电脑端的凭证表。
|
|
68
|
+
*/
|
|
69
|
+
export function isDeviceKey(value, kind) {
|
|
70
|
+
if (typeof value !== 'string' || !KEY_RE.test(value))
|
|
71
|
+
return false;
|
|
72
|
+
if (!kind)
|
|
73
|
+
return true;
|
|
74
|
+
return value.startsWith(kind === 'host' ? HOST_KEY_PREFIX : MOBILE_KEY_PREFIX);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 电脑设备 key → 信令房间号。
|
|
78
|
+
*
|
|
79
|
+
* ⚠️ 兼容 M3 的旧房间名:旧 `hostId`(如 `host-ab12cd34`,无前缀)传进来同样得到
|
|
80
|
+
* `virlen:host-ab12cd34` —— 与旧实现逐字一致,于是**旧二维码仍然可用**(§30.6)。
|
|
81
|
+
*/
|
|
82
|
+
export function roomFor(hostKey) {
|
|
83
|
+
return ROOM_PREFIX + hostKey;
|
|
84
|
+
}
|
|
85
|
+
/** 房间号 → 电脑设备 key(非本前缀派生时返回 `null`)。 */
|
|
86
|
+
export function hostKeyFromRoom(room) {
|
|
87
|
+
if (typeof room !== 'string' || !room.startsWith(ROOM_PREFIX))
|
|
88
|
+
return null;
|
|
89
|
+
const key = room.slice(ROOM_PREFIX.length);
|
|
90
|
+
return key ? key : null;
|
|
91
|
+
}
|
|
92
|
+
/** 签发一条新凭证(`now` 可注入,便于测试)。 */
|
|
93
|
+
export function issueGrant(now = Date.now()) {
|
|
94
|
+
return {
|
|
95
|
+
token: newGrantToken(),
|
|
96
|
+
issuedAt: now,
|
|
97
|
+
expiresAt: now + GRANT_TTL_MS,
|
|
98
|
+
lastSeenAt: now,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** 凭证是否已过期。 */
|
|
102
|
+
export function isGrantExpired(grant, now = Date.now()) {
|
|
103
|
+
return now >= grant.expiresAt;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 滑动续期:把到期时间推后到 `now + 30 天`,但**不超过** `issuedAt + 90 天`。
|
|
107
|
+
*
|
|
108
|
+
* 返回新对象(不原地改),调用方负责持久化 —— 「算」与「存」分开,便于测试与审计。
|
|
109
|
+
*/
|
|
110
|
+
export function renewGrant(grant, now = Date.now()) {
|
|
111
|
+
const cap = grant.issuedAt + GRANT_MAX_LIFETIME_MS;
|
|
112
|
+
return {
|
|
113
|
+
...grant,
|
|
114
|
+
expiresAt: Math.min(now + GRANT_TTL_MS, cap),
|
|
115
|
+
lastSeenAt: now,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 凭证可用性判定(**两端共用**)。
|
|
120
|
+
* 返回 `null` = 可用;否则是拒绝原因。
|
|
121
|
+
*/
|
|
122
|
+
export function checkGrant(grant, now = Date.now()) {
|
|
123
|
+
if (!grant || typeof grant.token !== 'string' || !grant.token)
|
|
124
|
+
return 'invalid';
|
|
125
|
+
if (isGrantExpired(grant, now))
|
|
126
|
+
return 'expired';
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
/** 剩余有效期的自然语言(两端 UI 共用同一套口径,避免「还剩 3 天」与「2.9 天」打架)。 */
|
|
130
|
+
export function describeGrantRemaining(grant, now = Date.now()) {
|
|
131
|
+
const left = grant.expiresAt - now;
|
|
132
|
+
if (left <= 0)
|
|
133
|
+
return '已过期';
|
|
134
|
+
const days = Math.floor(left / (24 * 60 * 60 * 1000));
|
|
135
|
+
if (days >= 1)
|
|
136
|
+
return `剩余 ${days} 天`;
|
|
137
|
+
const hours = Math.floor(left / (60 * 60 * 1000));
|
|
138
|
+
if (hours >= 1)
|
|
139
|
+
return `剩余 ${hours} 小时`;
|
|
140
|
+
const minutes = Math.max(1, Math.floor(left / 60000));
|
|
141
|
+
return `剩余 ${minutes} 分钟`;
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=identity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/protocol/identity.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ids.d.ts","sourceRoot":"","sources":["../../src/protocol/ids.ts"],"names":[],"mappings":"AAWA,wBAAgB,YAAY,IAAI,MAAM,CAQrC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* requestId 生成 —— 零依赖。
|
|
3
|
+
*
|
|
4
|
+
* 用于 RPC 幂等(见 docs/phone-control-bridge.md §3.2):手机网络会闪断,重连后必然重发,
|
|
5
|
+
* 电脑侧靠 requestId 去重、**返回上次结果而非重放**。
|
|
6
|
+
*
|
|
7
|
+
* 优先用 `crypto.randomUUID()`(浏览器安全上下文 / Node ≥ 19 均可用);
|
|
8
|
+
* 退化为「时间戳 + 单调计数 + 随机」——**不用于安全用途**,仅需高概率唯一。
|
|
9
|
+
*/
|
|
10
|
+
let fallbackCounter = 0;
|
|
11
|
+
export function newRequestId() {
|
|
12
|
+
const c = globalThis.crypto;
|
|
13
|
+
if (c && typeof c.randomUUID === 'function') {
|
|
14
|
+
return c.randomUUID();
|
|
15
|
+
}
|
|
16
|
+
fallbackCounter = (fallbackCounter + 1) >>> 0;
|
|
17
|
+
const rand = Math.random().toString(36).slice(2, 10);
|
|
18
|
+
return `${Date.now().toString(36)}-${fallbackCounter.toString(36)}-${rand}`;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=ids.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ids.js","sourceRoot":"","sources":["../../src/protocol/ids.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,IAAI,eAAe,GAAG,CAAC,CAAA;AAEvB,MAAM,UAAU,YAAY;IAC1B,MAAM,CAAC,GAAI,UAAkC,CAAC,MAAM,CAAA;IACpD,IAAI,CAAC,IAAI,OAAO,CAAC,CAAC,UAAU,KAAK,UAAU,EAAE,CAAC;QAC5C,OAAO,CAAC,CAAC,UAAU,EAAE,CAAA;IACvB,CAAC;IACD,eAAe,GAAG,CAAC,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,CAAA;IAC7C,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IACpD,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,eAAe,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,IAAI,EAAE,CAAA;AAC7E,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** 当前载荷版本。v1 与 v2 形状相同(差异在语义:v2 起 `host` 是设备 key、`ticket` 是一次性票据)。 */
|
|
2
|
+
export declare const PAIRING_PAYLOAD_VERSION = 2;
|
|
3
|
+
/**
|
|
4
|
+
* 配对票据有效期(默认 5 分钟)。
|
|
5
|
+
*
|
|
6
|
+
* 用户反馈「扫码后提示二维码失效」时的值曾是 2 分钟;现在 **5 分钟 + 面板打开即重生成 +
|
|
7
|
+
* 到期自动重生成**(见 `phoneControlStore`)三管齐下,真机上基本不可能再扫到过期码。
|
|
8
|
+
*/
|
|
9
|
+
export declare const PAIRING_TICKET_TTL_MS: number;
|
|
10
|
+
export interface PairingPayload {
|
|
11
|
+
v: number;
|
|
12
|
+
/** 电脑设备 key(也是房间号的来源)。 */
|
|
13
|
+
host: string;
|
|
14
|
+
/** 电脑显示名(手机列表里显示)。 */
|
|
15
|
+
name: string;
|
|
16
|
+
/** 一次性配对票据。 */
|
|
17
|
+
ticket: string;
|
|
18
|
+
/** 信令基址(如 `https://virlen.cn/api/rtc/`)。 */
|
|
19
|
+
signal?: string;
|
|
20
|
+
/** 信令房间号(缺省 `roomFor(host)`;旧码里带着它)。 */
|
|
21
|
+
room?: string;
|
|
22
|
+
}
|
|
23
|
+
/** 构造载荷(只填必要字段;`room` 交给两端各自派生,避免又多一处可漂移的冗余)。 */
|
|
24
|
+
export declare function buildPairingPayload(input: {
|
|
25
|
+
host: string;
|
|
26
|
+
name: string;
|
|
27
|
+
ticket: string;
|
|
28
|
+
signal?: string;
|
|
29
|
+
}): PairingPayload;
|
|
30
|
+
/** 序列化为二维码内容。 */
|
|
31
|
+
export declare function encodePairingPayload(payload: PairingPayload): string;
|
|
32
|
+
/**
|
|
33
|
+
* 解析二维码 / 配对串。无法识别返回 `null`(调用方给「不是 Virlen 配对码」的文案)。
|
|
34
|
+
*
|
|
35
|
+
* 宽容点(都是真机上会遇到的东西):
|
|
36
|
+
* - `v` 缺省按 1 处理(旧码);
|
|
37
|
+
* - `name` 缺省回退 `host`;
|
|
38
|
+
* - `ticket` 允许是任意非空串(前缀只作可读性,不作校验 —— 电脑端的判定才是权威)。
|
|
39
|
+
*/
|
|
40
|
+
export declare function parsePairingPayload(text: string): PairingPayload | null;
|
|
41
|
+
/** 载荷 → 房间号(显式 `room` 优先,否则由电脑 key 派生)。 */
|
|
42
|
+
export declare function roomOfPayload(payload: PairingPayload): string;
|
|
43
|
+
//# sourceMappingURL=pairing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../../src/protocol/pairing.ts"],"names":[],"mappings":"AAcA,sEAAsE;AACtE,eAAO,MAAM,uBAAuB,IAAI,CAAA;AAExC;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,QAAgB,CAAA;AAElD,MAAM,WAAW,cAAc;IAC7B,CAAC,EAAE,MAAM,CAAA;IACT,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,sBAAsB;IACtB,IAAI,EAAE,MAAM,CAAA;IACZ,eAAe;IACf,MAAM,EAAE,MAAM,CAAA;IACd,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,wCAAwC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,iDAAiD;AACjD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE;IACzC,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,GAAG,cAAc,CAQjB;AAED,iBAAiB;AACjB,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAEpE;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAqBvE;AAED,2CAA2C;AAC3C,wBAAgB,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,MAAM,CAE7D"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配对载荷(二维码 / 配对串的内容)—— **两端同一份**(M6,见 docs/phone-control-bridge.md §30.2)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么搬到共享包:此前电脑端 `pairingPayload()` 与手机端 `parsePayload()` 各写一份 interface,
|
|
5
|
+
* 字段含义靠注释对齐 —— 这类「契约写两份」的分叉只会在真机上暴露(§26/§29 都是这个形状的缺陷)。
|
|
6
|
+
*
|
|
7
|
+
* 字段:
|
|
8
|
+
* - `host`:**电脑设备 key**(`dk-…`;M3 的旧码是 `host-…`,同样能解析)。
|
|
9
|
+
* - `ticket`:**一次性**配对票据(不是长期凭证!兑换后电脑端签发 `grant` 回传,见 §30.3)。
|
|
10
|
+
* - `signal`:信令基址;缺省 = 同源 Broadcast 联调(生产必有)。
|
|
11
|
+
* - `room`:信令房间号;**缺省由 `roomFor(host)` 派生**。保留字段只为兼容旧码。
|
|
12
|
+
*/
|
|
13
|
+
import { roomFor } from './identity.js';
|
|
14
|
+
/** 当前载荷版本。v1 与 v2 形状相同(差异在语义:v2 起 `host` 是设备 key、`ticket` 是一次性票据)。 */
|
|
15
|
+
export const PAIRING_PAYLOAD_VERSION = 2;
|
|
16
|
+
/**
|
|
17
|
+
* 配对票据有效期(默认 5 分钟)。
|
|
18
|
+
*
|
|
19
|
+
* 用户反馈「扫码后提示二维码失效」时的值曾是 2 分钟;现在 **5 分钟 + 面板打开即重生成 +
|
|
20
|
+
* 到期自动重生成**(见 `phoneControlStore`)三管齐下,真机上基本不可能再扫到过期码。
|
|
21
|
+
*/
|
|
22
|
+
export const PAIRING_TICKET_TTL_MS = 5 * 60 * 1000;
|
|
23
|
+
/** 构造载荷(只填必要字段;`room` 交给两端各自派生,避免又多一处可漂移的冗余)。 */
|
|
24
|
+
export function buildPairingPayload(input) {
|
|
25
|
+
return {
|
|
26
|
+
v: PAIRING_PAYLOAD_VERSION,
|
|
27
|
+
host: input.host,
|
|
28
|
+
name: input.name,
|
|
29
|
+
ticket: input.ticket,
|
|
30
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** 序列化为二维码内容。 */
|
|
34
|
+
export function encodePairingPayload(payload) {
|
|
35
|
+
return JSON.stringify(payload);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 解析二维码 / 配对串。无法识别返回 `null`(调用方给「不是 Virlen 配对码」的文案)。
|
|
39
|
+
*
|
|
40
|
+
* 宽容点(都是真机上会遇到的东西):
|
|
41
|
+
* - `v` 缺省按 1 处理(旧码);
|
|
42
|
+
* - `name` 缺省回退 `host`;
|
|
43
|
+
* - `ticket` 允许是任意非空串(前缀只作可读性,不作校验 —— 电脑端的判定才是权威)。
|
|
44
|
+
*/
|
|
45
|
+
export function parsePairingPayload(text) {
|
|
46
|
+
if (typeof text !== 'string' || !text.trim())
|
|
47
|
+
return null;
|
|
48
|
+
let raw;
|
|
49
|
+
try {
|
|
50
|
+
raw = JSON.parse(text);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
if (!raw || typeof raw !== 'object')
|
|
56
|
+
return null;
|
|
57
|
+
const obj = raw;
|
|
58
|
+
if (typeof obj.host !== 'string' || !obj.host.trim())
|
|
59
|
+
return null;
|
|
60
|
+
if (typeof obj.ticket !== 'string' || !obj.ticket.trim())
|
|
61
|
+
return null;
|
|
62
|
+
const payload = {
|
|
63
|
+
v: typeof obj.v === 'number' ? obj.v : 1,
|
|
64
|
+
host: obj.host.trim(),
|
|
65
|
+
name: typeof obj.name === 'string' && obj.name.trim() ? obj.name.trim() : obj.host.trim(),
|
|
66
|
+
ticket: obj.ticket.trim(),
|
|
67
|
+
};
|
|
68
|
+
if (typeof obj.signal === 'string' && obj.signal.trim())
|
|
69
|
+
payload.signal = obj.signal.trim();
|
|
70
|
+
if (typeof obj.room === 'string' && obj.room.trim())
|
|
71
|
+
payload.room = obj.room.trim();
|
|
72
|
+
return payload;
|
|
73
|
+
}
|
|
74
|
+
/** 载荷 → 房间号(显式 `room` 优先,否则由电脑 key 派生)。 */
|
|
75
|
+
export function roomOfPayload(payload) {
|
|
76
|
+
return payload.room ?? roomFor(payload.host);
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=pairing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairing.js","sourceRoot":"","sources":["../../src/protocol/pairing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAEpC,sEAAsE;AACtE,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAA;AAExC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAA;AAgBlD,iDAAiD;AACjD,MAAM,UAAU,mBAAmB,CAAC,KAKnC;IACC,OAAO;QACL,CAAC,EAAE,uBAAuB;QAC1B,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAClD,CAAA;AACH,CAAC;AAED,iBAAiB;AACjB,MAAM,UAAU,oBAAoB,CAAC,OAAuB;IAC1D,OAAO,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAA;AAChC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;QAAE,OAAO,IAAI,CAAA;IACzD,IAAI,GAAY,CAAA;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IACxB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;IACD,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAA;IAChD,MAAM,GAAG,GAAG,GAA8B,CAAA;IAC1C,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE;QAAE,OAAO,IAAI,CAAA;IACjE,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE;QAAE,OAAO,IAAI,CAAA;IACrE,MAAM,OAAO,GAAmB;QAC9B,CAAC,EAAE,OAAO,GAAG,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACxC,IAAI,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE;QACrB,IAAI,EAAE,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE;QACzF,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE;KAC1B,CAAA;IACD,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE;QAAE,OAAO,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAA;IAC3F,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE;QAAE,OAAO,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAA;IACnF,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,2CAA2C;AAC3C,MAAM,UAAU,aAAa,CAAC,OAAuB;IACnD,OAAO,OAAO,CAAC,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;AAC9C,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAA;AACtD,YAAY,EAAE,kBAAkB,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAA"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { HostDataSource, HostEmit } from '../protocol/host.js';
|
|
2
|
+
import type { ApprovalTier } from '../protocol/api.js';
|
|
3
|
+
export interface MockHostOptions {
|
|
4
|
+
/** 流式段数。 */
|
|
5
|
+
streamSteps?: number;
|
|
6
|
+
/** 每段间隔(ms)。 */
|
|
7
|
+
streamDelayMs?: number;
|
|
8
|
+
/**
|
|
9
|
+
* `demo-1` 的预置消息条数(默认 2)。
|
|
10
|
+
*
|
|
11
|
+
* 供分页验证 / harness 压测用:默认值下只有 2 条,永远触发不了「还有更早的消息」。
|
|
12
|
+
*/
|
|
13
|
+
demoMessageCount?: number;
|
|
14
|
+
/**
|
|
15
|
+
* 给 `demo-1` 追加一条带工具名的工具消息(默认关:会改变默认消息条数,影响分页用例)。
|
|
16
|
+
*
|
|
17
|
+
* 打开后手机端能看到「工具 · list_files」气泡(harness 人工验证用)。
|
|
18
|
+
*/
|
|
19
|
+
demoToolMessage?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** 构造演示用交互(供 harness / 单测触发手机端卡片)。 */
|
|
22
|
+
export interface MockInteractionSpec {
|
|
23
|
+
kind?: 'choice' | 'authorization';
|
|
24
|
+
tier?: ApprovalTier;
|
|
25
|
+
sessionId?: string;
|
|
26
|
+
question?: string;
|
|
27
|
+
options?: string[];
|
|
28
|
+
multi?: boolean;
|
|
29
|
+
permName?: string;
|
|
30
|
+
title?: string;
|
|
31
|
+
subTitle?: string;
|
|
32
|
+
desc?: string;
|
|
33
|
+
hint?: string;
|
|
34
|
+
risk?: string;
|
|
35
|
+
sandboxBypass?: boolean;
|
|
36
|
+
presentation?: 'modal' | 'terminal';
|
|
37
|
+
}
|
|
38
|
+
export interface MockHostDataSource extends HostDataSource {
|
|
39
|
+
/** 由 harness / 测试在注册后回填(把 `HostEvents` 推给对端)。 */
|
|
40
|
+
bind(emit: HostEmit): void;
|
|
41
|
+
/** 推一个待应答交互给手机(返回其 `interactionId`)。 */
|
|
42
|
+
triggerInteraction(spec?: MockInteractionSpec): string;
|
|
43
|
+
/** 当前仍挂起的交互 id。 */
|
|
44
|
+
pendingInteractions(): string[];
|
|
45
|
+
/**
|
|
46
|
+
* 已收到的 RPC 方法名流水(M5)。
|
|
47
|
+
*
|
|
48
|
+
* 供单测断言「请求确实到达了电脑侧」—— 否则只能靠副作用反推,
|
|
49
|
+
* 而像 `cancel` 这种「无事发生」的方法就没有可观察的副作用。
|
|
50
|
+
*/
|
|
51
|
+
readonly calls: string[];
|
|
52
|
+
/** 手动推一次上下文占用变化(模拟电脑侧使用量增长 / 压缩),供手机端 UI 联调。 */
|
|
53
|
+
bumpContext(sessionId: string, tokens: number): void;
|
|
54
|
+
/**
|
|
55
|
+
* 手动推一次「工具参数生成进度」(§27),供手机端 UI 联调 / 单测。
|
|
56
|
+
*
|
|
57
|
+
* 真实链路里由引擎的 `AgentEvent.tool_progress` 驱动 —— 那是 provider 在**累积 tool 参数**
|
|
58
|
+
* 期间唯一的事件(其余时间静默)。`progress = null` 表示清空(工具开始执行 / 本轮结束)。
|
|
59
|
+
*/
|
|
60
|
+
setToolProgress(sessionId: string, progress: {
|
|
61
|
+
name: string;
|
|
62
|
+
chars: number;
|
|
63
|
+
} | null): void;
|
|
64
|
+
}
|
|
65
|
+
export declare function createMockHostDataSource(options?: MockHostOptions): MockHostDataSource;
|
|
66
|
+
//# sourceMappingURL=mock-host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-host.d.ts","sourceRoot":"","sources":["../../src/testing/mock-host.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAEhE,OAAO,KAAK,EAGV,YAAY,EAiBb,MAAM,iBAAiB,CAAA;AAGxB,MAAM,WAAW,eAAe;IAC9B,YAAY;IACZ,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,gBAAgB;IAChB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;CAC1B;AAED,sCAAsC;AACtC,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,EAAE,QAAQ,GAAG,eAAe,CAAA;IACjC,IAAI,CAAC,EAAE,YAAY,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,YAAY,CAAC,EAAE,OAAO,GAAG,UAAU,CAAA;CACpC;AAED,MAAM,WAAW,kBAAmB,SAAQ,cAAc;IACxD,iDAAiD;IACjD,IAAI,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAAA;IAC1B,wCAAwC;IACxC,kBAAkB,CAAC,IAAI,CAAC,EAAE,mBAAmB,GAAG,MAAM,CAAA;IACtD,mBAAmB;IACnB,mBAAmB,IAAI,MAAM,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAA;IACxB,gDAAgD;IAChD,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACpD;;;;;OAKG;IACH,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,GAAG,IAAI,CAAA;CAC3F;AAWD,wBAAgB,wBAAwB,CAAC,OAAO,GAAE,eAAoB,GAAG,kBAAkB,CAqiB1F"}
|