@ondevplann/remotetype 0.1.2 → 0.1.3

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/ws-client.js CHANGED
@@ -1,217 +1,217 @@
1
- 'use strict';
2
-
3
- /**
4
- * WebSocket 客户端:连接公网 Serve,断线自动重连(指数退避 1s→2s→4s…最大 10s)。
5
- * 对应需求文档 4.2.2 第 1 条。
6
- *
7
- * 连接后立即发送 register 帧声明身份与 clientId,
8
- * 服务端据此建立 clientId -> socket 的路由表。
9
- */
10
-
11
- const EventEmitter = require('events');
12
- const WebSocket = require('ws');
13
-
14
- class ServeClient extends EventEmitter {
15
- /**
16
- * @param {object} opts
17
- * @param {string} opts.url ws:// 或 wss:// 地址
18
- * @param {string} opts.clientId 本机唯一标识(用于服务端路由)
19
- * @param {string} [opts.token] 注册门禁 token(服务器设 RT_TOKEN 后必须携带)
20
- * @param {object} opts.logger
21
- * @param {number} [opts.maxBackoffMs] 退避上限,默认 10000
22
- * @param {number} [opts.baseBackoffMs] 退避基数,默认 1000
23
- * @param {number} [opts.pingIntervalMs]应用层心跳间隔,默认 25000
24
- */
25
- constructor(opts) {
26
- super();
27
- this.url = opts.url;
28
- this.clientId = opts.clientId;
29
- this.token = opts.token ?? null;
30
- this.logger = opts.logger;
31
- this.baseBackoffMs = opts.baseBackoffMs ?? 1000;
32
- this.maxBackoffMs = opts.maxBackoffMs ?? 10000;
33
- this.pingIntervalMs = opts.pingIntervalMs ?? 25000;
34
- // 允许注入自定义 WebSocket 实现(便于测试)
35
- this.WebSocketImpl = opts.WebSocketImpl ?? WebSocket;
36
-
37
- this.ws = null;
38
- this.closedByUser = false;
39
- this.backoffMs = this.baseBackoffMs;
40
- this.reconnectTimer = null;
41
- this.pingTimer = null;
42
- this.attempts = 0;
43
-
44
- this.stats = { connects: 0, reconnects: 0, messages: 0, errors: 0 };
45
- }
46
-
47
- get connected() {
48
- return Boolean(this.ws && this.ws.readyState === this.WebSocketImpl.OPEN);
49
- }
50
-
51
- /** 建立连接(幂等:已连接或有重连计划时直接返回)。 */
52
- connect() {
53
- if (this.ws && (this.ws.readyState === this.WebSocketImpl.OPEN || this.ws.readyState === this.WebSocketImpl.CONNECTING)) {
54
- return;
55
- }
56
- this.closedByUser = false;
57
- this.logger?.info('connecting to serve', { url: this.url, clientId: this.clientId });
58
-
59
- let ws;
60
- try {
61
- ws = new this.WebSocketImpl(this.url, {
62
- handshakeTimeout: 10000,
63
- // 应用层自己做心跳;ws 内置 ping 仍保留用于链路探测
64
- perMessageDeflate: false,
65
- });
66
- } catch (e) {
67
- this.logger?.error('failed to create websocket', { error: e.message });
68
- this._scheduleReconnect();
69
- return;
70
- }
71
-
72
- this.ws = ws;
73
-
74
- ws.on('open', () => {
75
- this.attempts += 1;
76
- this.stats.connects += 1;
77
- this.backoffMs = this.baseBackoffMs; // 成功后重置退避
78
- this.logger?.info('connected to serve', { url: this.url });
79
- // 声明身份,服务端据此建立路由;token 仅在配置时携带
80
- this._send({ type: 'register', role: 'pc', clientId: this.clientId, token: this.token || undefined });
81
- this._startPing();
82
- this.emit('connected');
83
- });
84
-
85
- ws.on('message', (raw) => {
86
- let msg;
87
- try {
88
- msg = JSON.parse(raw.toString());
89
- } catch {
90
- this.logger?.warn('received invalid json from serve');
91
- return;
92
- }
93
-
94
- // 服务端非文本帧:welcome / registered / ack / error / pong
95
- if (msg.type === 'error') {
96
- this.stats.errors += 1;
97
- this.logger?.error('serve reported error', { error: msg.error });
98
- this.emit('serve-error', msg);
99
- return;
100
- }
101
- if (msg.type === 'welcome' || msg.type === 'registered' || msg.type === 'pong') {
102
- this.logger?.debug(`serve frame: ${msg.type}`, msg);
103
- if (msg.type === 'registered') this.emit('registered', msg);
104
- return;
105
- }
106
- if (msg.type === 'ack') {
107
- this.logger?.debug('serve ack', msg);
108
- return;
109
- }
110
-
111
- // 转发下来的文本
112
- if (typeof msg.text === 'string') {
113
- this.stats.messages += 1;
114
- this.emit('text', { text: msg.text, clientId: msg.clientId, ts: msg.ts });
115
- return;
116
- }
117
-
118
- this.logger?.debug('unhandled message from serve', { keys: Object.keys(msg) });
119
- });
120
-
121
- ws.on('close', (code, reason) => {
122
- this._stopPing();
123
- const why = reason ? reason.toString() : '';
124
- if (this.closedByUser) {
125
- this.logger?.info('websocket closed by user', { code, reason: why });
126
- return;
127
- }
128
- // 4403 = 注册门禁拒绝(token 缺失/错误):同样的 token 重连必然同样被拒,
129
- // 无限重试只会刷屏,这里停止重连并上抛,交给上层提示用户检查配置。
130
- if (code === 4403) {
131
- this.logger?.error('注册被服务端拒绝(token 无效或缺失),已停止重连', { reason: why });
132
- this.authRejected = true;
133
- this.emit('auth-rejected', { reason: why });
134
- return;
135
- }
136
- this.logger?.warn('websocket closed, will reconnect', { code, reason: why });
137
- this.emit('disconnected', { code, reason: why });
138
- this._scheduleReconnect();
139
- });
140
-
141
- ws.on('error', (e) => {
142
- this.stats.errors += 1;
143
- // ECONNREFUSED 在服务端未启动时很常见,降为 warn 避免刷屏
144
- const level = e.code === 'ECONNREFUSED' ? 'warn' : 'error';
145
- this.logger?.[level]('websocket error', { error: e.message, code: e.code });
146
- // error 之后 close 必然触发,重连交给 close 处理
147
- });
148
- }
149
-
150
- /** 指数退避:1s, 2s, 4s, 8s, 10s, 10s... */
151
- _scheduleReconnect() {
152
- if (this.closedByUser || this.reconnectTimer) return;
153
- const delay = Math.min(this.backoffMs, this.maxBackoffMs);
154
- this.stats.reconnects += 1;
155
- this.logger?.info(`reconnecting in ${delay}ms`, { attempt: this.stats.reconnects });
156
- this.emit('reconnecting', { delayMs: delay, attempt: this.stats.reconnects });
157
-
158
- this.reconnectTimer = setTimeout(() => {
159
- this.reconnectTimer = null;
160
- this.connect();
161
- }, delay);
162
- this.reconnectTimer.unref?.();
163
-
164
- // 指数增长
165
- this.backoffMs = Math.min(this.backoffMs * 2, this.maxBackoffMs);
166
- }
167
-
168
- _startPing() {
169
- this._stopPing();
170
- if (this.pingIntervalMs <= 0) return;
171
- this.pingTimer = setInterval(() => {
172
- if (!this.connected) return;
173
- this._send({ type: 'ping', ts: Date.now() });
174
- // ws 内置 ping,帮助中间代理保持连接
175
- try { this.ws.ping(); } catch { /* ignore */ }
176
- }, this.pingIntervalMs);
177
- this.pingTimer.unref?.();
178
- }
179
-
180
- _stopPing() {
181
- if (this.pingTimer) {
182
- clearInterval(this.pingTimer);
183
- this.pingTimer = null;
184
- }
185
- }
186
-
187
- _send(obj) {
188
- if (!this.connected) return false;
189
- try {
190
- this.ws.send(JSON.stringify(obj));
191
- return true;
192
- } catch (e) {
193
- this.logger?.warn('failed to send to serve', { error: e.message });
194
- return false;
195
- }
196
- }
197
-
198
- /** 发送一条 JSON 消息到服务端(公共接口,如 PC→手机 的 ACK 路由帧)。 */
199
- send(obj) {
200
- return this._send(obj);
201
- }
202
-
203
- /** 主动关闭,不再重连。用于优雅退出。 */
204
- close() {
205
- this.closedByUser = true;
206
- this._stopPing();
207
- if (this.reconnectTimer) {
208
- clearTimeout(this.reconnectTimer);
209
- this.reconnectTimer = null;
210
- }
211
- if (this.ws) {
212
- try { this.ws.close(1000, 'client shutdown'); } catch { /* ignore */ }
213
- }
214
- }
215
- }
216
-
217
- module.exports = { ServeClient };
1
+ 'use strict';
2
+
3
+ /**
4
+ * WebSocket 客户端:连接公网 Serve,断线自动重连(指数退避 1s→2s→4s…最大 10s)。
5
+ * 对应需求文档 4.2.2 第 1 条。
6
+ *
7
+ * 连接后立即发送 register 帧声明身份与 clientId,
8
+ * 服务端据此建立 clientId -> socket 的路由表。
9
+ */
10
+
11
+ const EventEmitter = require('events');
12
+ const WebSocket = require('ws');
13
+
14
+ class ServeClient extends EventEmitter {
15
+ /**
16
+ * @param {object} opts
17
+ * @param {string} opts.url ws:// 或 wss:// 地址
18
+ * @param {string} opts.clientId 本机唯一标识(用于服务端路由)
19
+ * @param {string} [opts.token] 注册门禁 token(服务器设 RT_TOKEN 后必须携带)
20
+ * @param {object} opts.logger
21
+ * @param {number} [opts.maxBackoffMs] 退避上限,默认 10000
22
+ * @param {number} [opts.baseBackoffMs] 退避基数,默认 1000
23
+ * @param {number} [opts.pingIntervalMs]应用层心跳间隔,默认 25000
24
+ */
25
+ constructor(opts) {
26
+ super();
27
+ this.url = opts.url;
28
+ this.clientId = opts.clientId;
29
+ this.token = opts.token ?? null;
30
+ this.logger = opts.logger;
31
+ this.baseBackoffMs = opts.baseBackoffMs ?? 1000;
32
+ this.maxBackoffMs = opts.maxBackoffMs ?? 10000;
33
+ this.pingIntervalMs = opts.pingIntervalMs ?? 25000;
34
+ // 允许注入自定义 WebSocket 实现(便于测试)
35
+ this.WebSocketImpl = opts.WebSocketImpl ?? WebSocket;
36
+
37
+ this.ws = null;
38
+ this.closedByUser = false;
39
+ this.backoffMs = this.baseBackoffMs;
40
+ this.reconnectTimer = null;
41
+ this.pingTimer = null;
42
+ this.attempts = 0;
43
+
44
+ this.stats = { connects: 0, reconnects: 0, messages: 0, errors: 0 };
45
+ }
46
+
47
+ get connected() {
48
+ return Boolean(this.ws && this.ws.readyState === this.WebSocketImpl.OPEN);
49
+ }
50
+
51
+ /** 建立连接(幂等:已连接或有重连计划时直接返回)。 */
52
+ connect() {
53
+ if (this.ws && (this.ws.readyState === this.WebSocketImpl.OPEN || this.ws.readyState === this.WebSocketImpl.CONNECTING)) {
54
+ return;
55
+ }
56
+ this.closedByUser = false;
57
+ this.logger?.info('connecting to serve', { url: this.url, clientId: this.clientId });
58
+
59
+ let ws;
60
+ try {
61
+ ws = new this.WebSocketImpl(this.url, {
62
+ handshakeTimeout: 10000,
63
+ // 应用层自己做心跳;ws 内置 ping 仍保留用于链路探测
64
+ perMessageDeflate: false,
65
+ });
66
+ } catch (e) {
67
+ this.logger?.error('failed to create websocket', { error: e.message });
68
+ this._scheduleReconnect();
69
+ return;
70
+ }
71
+
72
+ this.ws = ws;
73
+
74
+ ws.on('open', () => {
75
+ this.attempts += 1;
76
+ this.stats.connects += 1;
77
+ this.backoffMs = this.baseBackoffMs; // 成功后重置退避
78
+ this.logger?.info('connected to serve', { url: this.url });
79
+ // 声明身份,服务端据此建立路由;token 仅在配置时携带
80
+ this._send({ type: 'register', role: 'pc', clientId: this.clientId, token: this.token || undefined });
81
+ this._startPing();
82
+ this.emit('connected');
83
+ });
84
+
85
+ ws.on('message', (raw) => {
86
+ let msg;
87
+ try {
88
+ msg = JSON.parse(raw.toString());
89
+ } catch {
90
+ this.logger?.warn('received invalid json from serve');
91
+ return;
92
+ }
93
+
94
+ // 服务端非文本帧:welcome / registered / ack / error / pong
95
+ if (msg.type === 'error') {
96
+ this.stats.errors += 1;
97
+ this.logger?.error('serve reported error', { error: msg.error });
98
+ this.emit('serve-error', msg);
99
+ return;
100
+ }
101
+ if (msg.type === 'welcome' || msg.type === 'registered' || msg.type === 'pong') {
102
+ this.logger?.debug(`serve frame: ${msg.type}`, msg);
103
+ if (msg.type === 'registered') this.emit('registered', msg);
104
+ return;
105
+ }
106
+ if (msg.type === 'ack') {
107
+ this.logger?.debug('serve ack', msg);
108
+ return;
109
+ }
110
+
111
+ // 转发下来的文本
112
+ if (typeof msg.text === 'string') {
113
+ this.stats.messages += 1;
114
+ this.emit('text', { text: msg.text, clientId: msg.clientId, ts: msg.ts });
115
+ return;
116
+ }
117
+
118
+ this.logger?.debug('unhandled message from serve', { keys: Object.keys(msg) });
119
+ });
120
+
121
+ ws.on('close', (code, reason) => {
122
+ this._stopPing();
123
+ const why = reason ? reason.toString() : '';
124
+ if (this.closedByUser) {
125
+ this.logger?.info('websocket closed by user', { code, reason: why });
126
+ return;
127
+ }
128
+ // 4403 = 注册门禁拒绝(token 缺失/错误):同样的 token 重连必然同样被拒,
129
+ // 无限重试只会刷屏,这里停止重连并上抛,交给上层提示用户检查配置。
130
+ if (code === 4403) {
131
+ this.logger?.error('注册被服务端拒绝(token 无效或缺失),已停止重连', { reason: why });
132
+ this.authRejected = true;
133
+ this.emit('auth-rejected', { reason: why });
134
+ return;
135
+ }
136
+ this.logger?.warn('websocket closed, will reconnect', { code, reason: why });
137
+ this.emit('disconnected', { code, reason: why });
138
+ this._scheduleReconnect();
139
+ });
140
+
141
+ ws.on('error', (e) => {
142
+ this.stats.errors += 1;
143
+ // ECONNREFUSED 在服务端未启动时很常见,降为 warn 避免刷屏
144
+ const level = e.code === 'ECONNREFUSED' ? 'warn' : 'error';
145
+ this.logger?.[level]('websocket error', { error: e.message, code: e.code });
146
+ // error 之后 close 必然触发,重连交给 close 处理
147
+ });
148
+ }
149
+
150
+ /** 指数退避:1s, 2s, 4s, 8s, 10s, 10s... */
151
+ _scheduleReconnect() {
152
+ if (this.closedByUser || this.reconnectTimer) return;
153
+ const delay = Math.min(this.backoffMs, this.maxBackoffMs);
154
+ this.stats.reconnects += 1;
155
+ this.logger?.info(`reconnecting in ${delay}ms`, { attempt: this.stats.reconnects });
156
+ this.emit('reconnecting', { delayMs: delay, attempt: this.stats.reconnects });
157
+
158
+ this.reconnectTimer = setTimeout(() => {
159
+ this.reconnectTimer = null;
160
+ this.connect();
161
+ }, delay);
162
+ this.reconnectTimer.unref?.();
163
+
164
+ // 指数增长
165
+ this.backoffMs = Math.min(this.backoffMs * 2, this.maxBackoffMs);
166
+ }
167
+
168
+ _startPing() {
169
+ this._stopPing();
170
+ if (this.pingIntervalMs <= 0) return;
171
+ this.pingTimer = setInterval(() => {
172
+ if (!this.connected) return;
173
+ this._send({ type: 'ping', ts: Date.now() });
174
+ // ws 内置 ping,帮助中间代理保持连接
175
+ try { this.ws.ping(); } catch { /* ignore */ }
176
+ }, this.pingIntervalMs);
177
+ this.pingTimer.unref?.();
178
+ }
179
+
180
+ _stopPing() {
181
+ if (this.pingTimer) {
182
+ clearInterval(this.pingTimer);
183
+ this.pingTimer = null;
184
+ }
185
+ }
186
+
187
+ _send(obj) {
188
+ if (!this.connected) return false;
189
+ try {
190
+ this.ws.send(JSON.stringify(obj));
191
+ return true;
192
+ } catch (e) {
193
+ this.logger?.warn('failed to send to serve', { error: e.message });
194
+ return false;
195
+ }
196
+ }
197
+
198
+ /** 发送一条 JSON 消息到服务端(公共接口,如 PC→手机 的 ACK 路由帧)。 */
199
+ send(obj) {
200
+ return this._send(obj);
201
+ }
202
+
203
+ /** 主动关闭,不再重连。用于优雅退出。 */
204
+ close() {
205
+ this.closedByUser = true;
206
+ this._stopPing();
207
+ if (this.reconnectTimer) {
208
+ clearTimeout(this.reconnectTimer);
209
+ this.reconnectTimer = null;
210
+ }
211
+ if (this.ws) {
212
+ try { this.ws.close(1000, 'client shutdown'); } catch { /* ignore */ }
213
+ }
214
+ }
215
+ }
216
+
217
+ module.exports = { ServeClient };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ondevplann/remotetype",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "RemoteType PC 客户端:npx 一键启动,接收手机端语音识别文本并注入到当前焦点输入框",
5
5
  "bin": {
6
6
  "remotetype": "./bin/index.js"