@zhihand/mcp 0.29.0 → 0.32.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/bin/zhihand +448 -212
- package/dist/core/command.d.ts +5 -5
- package/dist/core/command.js +6 -8
- package/dist/core/config.d.ts +48 -21
- package/dist/core/config.js +178 -42
- package/dist/core/device.d.ts +28 -19
- package/dist/core/device.js +168 -145
- package/dist/core/logger.d.ts +17 -0
- package/dist/core/logger.js +32 -0
- package/dist/core/pair.d.ts +39 -31
- package/dist/core/pair.js +205 -77
- package/dist/core/registry.d.ts +60 -0
- package/dist/core/registry.js +415 -0
- package/dist/core/screenshot.d.ts +3 -3
- package/dist/core/screenshot.js +3 -2
- package/dist/core/sse.d.ts +40 -18
- package/dist/core/sse.js +122 -62
- package/dist/core/ws.d.ts +92 -0
- package/dist/core/ws.js +327 -0
- package/dist/daemon/dispatcher.d.ts +3 -1
- package/dist/daemon/dispatcher.js +4 -3
- package/dist/daemon/heartbeat.d.ts +4 -4
- package/dist/daemon/heartbeat.js +1 -1
- package/dist/daemon/index.js +10 -8
- package/dist/daemon/prompt-listener.d.ts +8 -7
- package/dist/daemon/prompt-listener.js +59 -99
- package/dist/index.d.ts +3 -3
- package/dist/index.js +104 -40
- package/dist/openclaw.adapter.js +10 -2
- package/dist/tools/control.d.ts +10 -3
- package/dist/tools/control.js +18 -24
- package/dist/tools/pair.d.ts +1 -1
- package/dist/tools/pair.js +22 -28
- package/dist/tools/resolve.d.ts +7 -0
- package/dist/tools/resolve.js +22 -0
- package/dist/tools/schemas.d.ts +9 -1
- package/dist/tools/schemas.js +10 -8
- package/dist/tools/screenshot.d.ts +3 -2
- package/dist/tools/screenshot.js +2 -2
- package/dist/tools/system.d.ts +3 -5
- package/dist/tools/system.js +19 -6
- package/package.json +3 -1
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device registry — the single source of truth for all paired devices,
|
|
3
|
+
* their live state, and multi-user WebSocket streams.
|
|
4
|
+
*
|
|
5
|
+
* Groups devices under users. Each user has one UserEventWebSocket.
|
|
6
|
+
* Online detection is server-authoritative (no local heartbeat polling).
|
|
7
|
+
* Config hot-reload via fs.watchFile.
|
|
8
|
+
*/
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import { loadConfig, getConfigPath, resolveDefaultEndpoint, addDeviceToUser as configAddDeviceToUser, updateDeviceLastSeen as configUpdateDeviceLastSeen, } from "./config.js";
|
|
11
|
+
import { extractStatic, computeCapabilities, normalizeProfilePayload, } from "./device.js";
|
|
12
|
+
import { UserEventWebSocket, fetchUserCredentials, } from "./ws.js";
|
|
13
|
+
import { log } from "./logger.js";
|
|
14
|
+
const LIST_CHANGED_DEBOUNCE_MS = 2500;
|
|
15
|
+
const CONFIG_WATCH_INTERVAL_MS = 1000;
|
|
16
|
+
const CONFIG_RECONCILE_DEBOUNCE_MS = 300;
|
|
17
|
+
class Registry {
|
|
18
|
+
userStates = new Map();
|
|
19
|
+
listChangedSubs = new Set();
|
|
20
|
+
debounceTimer = null;
|
|
21
|
+
lastOnlineSet = new Set();
|
|
22
|
+
initialized = false;
|
|
23
|
+
configWatchActive = false;
|
|
24
|
+
reconcileTimer = null;
|
|
25
|
+
// ── Public API ──────────────────────────────────────────
|
|
26
|
+
get(credentialId) {
|
|
27
|
+
for (const us of this.userStates.values()) {
|
|
28
|
+
const d = us.devices.get(credentialId);
|
|
29
|
+
if (d)
|
|
30
|
+
return d;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
list() {
|
|
35
|
+
const all = [];
|
|
36
|
+
for (const us of this.userStates.values()) {
|
|
37
|
+
for (const d of us.devices.values()) {
|
|
38
|
+
all.push(d);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return all;
|
|
42
|
+
}
|
|
43
|
+
listOnline() {
|
|
44
|
+
return this.list()
|
|
45
|
+
.filter((d) => d.online)
|
|
46
|
+
.sort((a, b) => b.lastSeenAtMs - a.lastSeenAtMs);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Most-recently-active online device across all users.
|
|
50
|
+
*/
|
|
51
|
+
resolveDefault() {
|
|
52
|
+
const online = this.listOnline();
|
|
53
|
+
return online.length > 0 ? online[0] : null;
|
|
54
|
+
}
|
|
55
|
+
isMultiUser() {
|
|
56
|
+
return this.userStates.size > 1;
|
|
57
|
+
}
|
|
58
|
+
toRuntimeConfig(state) {
|
|
59
|
+
const us = this.userStates.get(state.userId);
|
|
60
|
+
return {
|
|
61
|
+
controlPlaneEndpoint: us?.endpoint ?? resolveDefaultEndpoint(),
|
|
62
|
+
credentialId: state.credentialId,
|
|
63
|
+
controllerToken: us?.controllerToken ?? "",
|
|
64
|
+
timeoutMs: 10_000,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
subscribe(cb) {
|
|
68
|
+
this.listChangedSubs.add(cb);
|
|
69
|
+
return () => this.listChangedSubs.delete(cb);
|
|
70
|
+
}
|
|
71
|
+
// ── Init / Shutdown ────────────────────────────────────
|
|
72
|
+
async init() {
|
|
73
|
+
if (this.initialized)
|
|
74
|
+
return;
|
|
75
|
+
this.initialized = true;
|
|
76
|
+
const cfg = loadConfig();
|
|
77
|
+
const endpoint = resolveDefaultEndpoint();
|
|
78
|
+
const users = Object.values(cfg.users);
|
|
79
|
+
// Create user states and start WS streams
|
|
80
|
+
const fetchPromises = [];
|
|
81
|
+
for (const userRec of users) {
|
|
82
|
+
const us = this.createUserState(userRec, endpoint);
|
|
83
|
+
this.userStates.set(userRec.user_id, us);
|
|
84
|
+
this.startUserStream(us);
|
|
85
|
+
// Fetch initial credentials with online status
|
|
86
|
+
fetchPromises.push(this.fetchAndPopulateDevices(us, userRec).catch(() => {
|
|
87
|
+
// Non-fatal: populate from config alone
|
|
88
|
+
this.populateDevicesFromConfig(us, userRec);
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
// Overall 5s cap on initial fetches
|
|
92
|
+
await Promise.race([
|
|
93
|
+
Promise.all(fetchPromises),
|
|
94
|
+
new Promise((r) => setTimeout(r, 5000)),
|
|
95
|
+
]);
|
|
96
|
+
// Start config hot-reload watcher
|
|
97
|
+
this.startConfigWatch();
|
|
98
|
+
}
|
|
99
|
+
shutdown() {
|
|
100
|
+
for (const us of this.userStates.values()) {
|
|
101
|
+
us.stream?.stop();
|
|
102
|
+
us.stream = null;
|
|
103
|
+
}
|
|
104
|
+
this.userStates.clear();
|
|
105
|
+
if (this.debounceTimer) {
|
|
106
|
+
clearTimeout(this.debounceTimer);
|
|
107
|
+
this.debounceTimer = null;
|
|
108
|
+
}
|
|
109
|
+
if (this.reconcileTimer) {
|
|
110
|
+
clearTimeout(this.reconcileTimer);
|
|
111
|
+
this.reconcileTimer = null;
|
|
112
|
+
}
|
|
113
|
+
this.stopConfigWatch();
|
|
114
|
+
this.listChangedSubs.clear();
|
|
115
|
+
this.initialized = false;
|
|
116
|
+
}
|
|
117
|
+
// ── Online set change detection ─────────────────────────
|
|
118
|
+
computeOnlineSet() {
|
|
119
|
+
const out = new Set();
|
|
120
|
+
for (const us of this.userStates.values()) {
|
|
121
|
+
for (const d of us.devices.values()) {
|
|
122
|
+
if (d.online)
|
|
123
|
+
out.add(d.credentialId);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
setsEqual(a, b) {
|
|
129
|
+
if (a.size !== b.size)
|
|
130
|
+
return false;
|
|
131
|
+
for (const x of a)
|
|
132
|
+
if (!b.has(x))
|
|
133
|
+
return false;
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
scheduleListChanged() {
|
|
137
|
+
const now = this.computeOnlineSet();
|
|
138
|
+
if (this.setsEqual(now, this.lastOnlineSet))
|
|
139
|
+
return;
|
|
140
|
+
if (this.debounceTimer)
|
|
141
|
+
return;
|
|
142
|
+
this.debounceTimer = setTimeout(() => {
|
|
143
|
+
this.debounceTimer = null;
|
|
144
|
+
const current = this.computeOnlineSet();
|
|
145
|
+
if (this.setsEqual(current, this.lastOnlineSet))
|
|
146
|
+
return;
|
|
147
|
+
this.lastOnlineSet = current;
|
|
148
|
+
for (const cb of this.listChangedSubs) {
|
|
149
|
+
try {
|
|
150
|
+
cb();
|
|
151
|
+
}
|
|
152
|
+
catch { /* swallow */ }
|
|
153
|
+
}
|
|
154
|
+
}, LIST_CHANGED_DEBOUNCE_MS);
|
|
155
|
+
}
|
|
156
|
+
// ── User/Device state management ───────────────────────
|
|
157
|
+
createUserState(userRec, endpoint) {
|
|
158
|
+
return {
|
|
159
|
+
userId: userRec.user_id,
|
|
160
|
+
controllerToken: userRec.controller_token,
|
|
161
|
+
label: userRec.label,
|
|
162
|
+
endpoint,
|
|
163
|
+
stream: null,
|
|
164
|
+
devices: new Map(),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
makeDeviceState(record, userId, userLabel, online = false) {
|
|
168
|
+
return {
|
|
169
|
+
credentialId: record.credential_id,
|
|
170
|
+
userId,
|
|
171
|
+
userLabel,
|
|
172
|
+
label: record.label,
|
|
173
|
+
platform: record.platform,
|
|
174
|
+
online,
|
|
175
|
+
lastSeenAtMs: 0,
|
|
176
|
+
profile: null,
|
|
177
|
+
capabilities: null,
|
|
178
|
+
profileReceivedAtMs: 0,
|
|
179
|
+
rawAttributes: {},
|
|
180
|
+
record,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
populateDevicesFromConfig(us, userRec) {
|
|
184
|
+
for (const dev of userRec.devices) {
|
|
185
|
+
if (!us.devices.has(dev.credential_id)) {
|
|
186
|
+
us.devices.set(dev.credential_id, this.makeDeviceState(dev, us.userId, us.label));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async fetchAndPopulateDevices(us, userRec) {
|
|
191
|
+
const creds = await fetchUserCredentials(us.endpoint, us.userId, us.controllerToken);
|
|
192
|
+
// Populate from server response (has online status)
|
|
193
|
+
for (const cred of creds) {
|
|
194
|
+
const existing = us.devices.get(cred.credential_id);
|
|
195
|
+
if (existing) {
|
|
196
|
+
existing.online = cred.online ?? false;
|
|
197
|
+
if (cred.online) {
|
|
198
|
+
existing.lastSeenAtMs = Date.now();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
const record = {
|
|
203
|
+
credential_id: cred.credential_id,
|
|
204
|
+
label: cred.label ?? cred.credential_id,
|
|
205
|
+
platform: cred.platform ?? "unknown",
|
|
206
|
+
paired_at: cred.paired_at ?? new Date().toISOString(),
|
|
207
|
+
last_seen_at: cred.last_seen_at ?? new Date().toISOString(),
|
|
208
|
+
};
|
|
209
|
+
const state = this.makeDeviceState(record, us.userId, us.label, cred.online ?? false);
|
|
210
|
+
if (cred.online)
|
|
211
|
+
state.lastSeenAtMs = Date.now();
|
|
212
|
+
us.devices.set(cred.credential_id, state);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Also ensure all config devices are present
|
|
216
|
+
this.populateDevicesFromConfig(us, userRec);
|
|
217
|
+
}
|
|
218
|
+
// ── WS stream management ──────────────────────────────
|
|
219
|
+
startUserStream(us) {
|
|
220
|
+
if (us.stream)
|
|
221
|
+
return;
|
|
222
|
+
us.stream = new UserEventWebSocket(us.userId, us.controllerToken, us.endpoint, {
|
|
223
|
+
onDeviceOnline: (credentialId) => {
|
|
224
|
+
const d = us.devices.get(credentialId);
|
|
225
|
+
if (d) {
|
|
226
|
+
d.online = true;
|
|
227
|
+
d.lastSeenAtMs = Date.now();
|
|
228
|
+
this.touchLastSeen(d);
|
|
229
|
+
log.debug(`[registry] ${credentialId} online`);
|
|
230
|
+
this.scheduleListChanged();
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
onDeviceOffline: (credentialId) => {
|
|
234
|
+
const d = us.devices.get(credentialId);
|
|
235
|
+
if (d) {
|
|
236
|
+
d.online = false;
|
|
237
|
+
log.debug(`[registry] ${credentialId} offline`);
|
|
238
|
+
this.scheduleListChanged();
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
onDeviceProfileUpdated: (credentialId, profile) => {
|
|
242
|
+
const d = us.devices.get(credentialId);
|
|
243
|
+
if (d) {
|
|
244
|
+
const attrs = normalizeProfilePayload(profile);
|
|
245
|
+
d.rawAttributes = attrs;
|
|
246
|
+
d.profileReceivedAtMs = Date.now();
|
|
247
|
+
d.profile = extractStatic(attrs);
|
|
248
|
+
d.capabilities = computeCapabilities(attrs, d.profileReceivedAtMs);
|
|
249
|
+
const plat = d.profile.platform;
|
|
250
|
+
if (plat === "ios" || plat === "android") {
|
|
251
|
+
d.platform = plat;
|
|
252
|
+
d.record.platform = plat;
|
|
253
|
+
}
|
|
254
|
+
this.touchLastSeen(d);
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
onCommandAcked: (event) => {
|
|
258
|
+
// Already handled by handleWSEvent in the stream dispatch
|
|
259
|
+
},
|
|
260
|
+
onCredentialAdded: (credential) => {
|
|
261
|
+
const credId = credential.credential_id;
|
|
262
|
+
if (credId && !us.devices.has(credId)) {
|
|
263
|
+
const record = {
|
|
264
|
+
credential_id: credId,
|
|
265
|
+
label: credential.label ?? credId,
|
|
266
|
+
platform: (credential.platform ?? "unknown"),
|
|
267
|
+
paired_at: credential.paired_at ?? new Date().toISOString(),
|
|
268
|
+
last_seen_at: new Date().toISOString(),
|
|
269
|
+
};
|
|
270
|
+
const state = this.makeDeviceState(record, us.userId, us.label, true);
|
|
271
|
+
state.lastSeenAtMs = Date.now();
|
|
272
|
+
us.devices.set(credId, state);
|
|
273
|
+
// Persist to config immediately so fs.watchFile reconciliation
|
|
274
|
+
// doesn't wipe this device before the pairing CLI writes it.
|
|
275
|
+
try {
|
|
276
|
+
configAddDeviceToUser(us.userId, record);
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// non-fatal — pairing CLI will persist shortly
|
|
280
|
+
}
|
|
281
|
+
this.scheduleListChanged();
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
onCredentialRemoved: (credentialId) => {
|
|
285
|
+
us.devices.delete(credentialId);
|
|
286
|
+
this.scheduleListChanged();
|
|
287
|
+
},
|
|
288
|
+
onConnected: () => {
|
|
289
|
+
log.debug(`[registry] WS connected for user ${us.userId}`);
|
|
290
|
+
// Re-fetch credentials to reconcile missed events
|
|
291
|
+
fetchUserCredentials(us.endpoint, us.userId, us.controllerToken)
|
|
292
|
+
.then((creds) => {
|
|
293
|
+
const onlineIds = new Set(creds.filter((c) => c.online).map((c) => c.credential_id));
|
|
294
|
+
for (const d of us.devices.values()) {
|
|
295
|
+
const wasOnline = d.online;
|
|
296
|
+
d.online = onlineIds.has(d.credentialId);
|
|
297
|
+
if (d.online)
|
|
298
|
+
d.lastSeenAtMs = Date.now();
|
|
299
|
+
if (wasOnline !== d.online) {
|
|
300
|
+
this.scheduleListChanged();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
.catch(() => { });
|
|
305
|
+
},
|
|
306
|
+
onDisconnected: () => {
|
|
307
|
+
log.debug(`[registry] WS disconnected for user ${us.userId}`);
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
us.stream.start();
|
|
311
|
+
}
|
|
312
|
+
touchLastSeen(state) {
|
|
313
|
+
state.lastSeenAtMs = Date.now();
|
|
314
|
+
const iso = new Date(state.lastSeenAtMs).toISOString();
|
|
315
|
+
try {
|
|
316
|
+
configUpdateDeviceLastSeen(state.userId, state.credentialId, iso);
|
|
317
|
+
state.record.last_seen_at = iso;
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
// non-fatal
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// ── Config hot-reload ─────────────────────────────────
|
|
324
|
+
startConfigWatch() {
|
|
325
|
+
if (this.configWatchActive)
|
|
326
|
+
return;
|
|
327
|
+
this.configWatchActive = true;
|
|
328
|
+
const configPath = getConfigPath();
|
|
329
|
+
try {
|
|
330
|
+
fs.watchFile(configPath, { interval: CONFIG_WATCH_INTERVAL_MS }, () => {
|
|
331
|
+
// Debounce reconciliation
|
|
332
|
+
if (this.reconcileTimer)
|
|
333
|
+
clearTimeout(this.reconcileTimer);
|
|
334
|
+
this.reconcileTimer = setTimeout(() => {
|
|
335
|
+
this.reconcileTimer = null;
|
|
336
|
+
this.reconcileConfig();
|
|
337
|
+
}, CONFIG_RECONCILE_DEBOUNCE_MS);
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
// watchFile not available — skip hot-reload
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
stopConfigWatch() {
|
|
345
|
+
if (!this.configWatchActive)
|
|
346
|
+
return;
|
|
347
|
+
this.configWatchActive = false;
|
|
348
|
+
try {
|
|
349
|
+
fs.unwatchFile(getConfigPath());
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
// ignore
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
reconcileConfig() {
|
|
356
|
+
const cfg = loadConfig();
|
|
357
|
+
const endpoint = resolveDefaultEndpoint();
|
|
358
|
+
const configUserIds = new Set(Object.keys(cfg.users));
|
|
359
|
+
// Remove users no longer in config
|
|
360
|
+
for (const [userId, us] of this.userStates) {
|
|
361
|
+
if (!configUserIds.has(userId)) {
|
|
362
|
+
us.stream?.stop();
|
|
363
|
+
us.stream = null;
|
|
364
|
+
this.userStates.delete(userId);
|
|
365
|
+
log.debug(`[registry] Removed user ${userId} (config hot-reload)`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// Add new users / reconcile devices
|
|
369
|
+
for (const userRec of Object.values(cfg.users)) {
|
|
370
|
+
let us = this.userStates.get(userRec.user_id);
|
|
371
|
+
if (!us) {
|
|
372
|
+
// New user
|
|
373
|
+
us = this.createUserState(userRec, endpoint);
|
|
374
|
+
this.userStates.set(userRec.user_id, us);
|
|
375
|
+
this.populateDevicesFromConfig(us, userRec);
|
|
376
|
+
this.startUserStream(us);
|
|
377
|
+
log.debug(`[registry] Added user ${userRec.user_id} (config hot-reload)`);
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
// Update token/label if changed
|
|
381
|
+
us.controllerToken = userRec.controller_token;
|
|
382
|
+
us.label = userRec.label;
|
|
383
|
+
// Reconcile devices
|
|
384
|
+
const configDevIds = new Set(userRec.devices.map((d) => d.credential_id));
|
|
385
|
+
// Remove devices no longer in config — but keep online devices
|
|
386
|
+
// that were added via WS credential.added but not yet persisted
|
|
387
|
+
// by the pairing CLI (race window).
|
|
388
|
+
for (const credId of us.devices.keys()) {
|
|
389
|
+
if (!configDevIds.has(credId)) {
|
|
390
|
+
const d = us.devices.get(credId);
|
|
391
|
+
if (d && d.online) {
|
|
392
|
+
log.debug(`[registry] Keeping online device ${credId} despite missing from config`);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
us.devices.delete(credId);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
// Add new devices
|
|
399
|
+
for (const dev of userRec.devices) {
|
|
400
|
+
if (!us.devices.has(dev.credential_id)) {
|
|
401
|
+
us.devices.set(dev.credential_id, this.makeDeviceState(dev, us.userId, us.label));
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
// Update label/platform from config
|
|
405
|
+
const d = us.devices.get(dev.credential_id);
|
|
406
|
+
d.label = dev.label;
|
|
407
|
+
d.userLabel = us.label;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
this.scheduleListChanged();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
export const registry = new Registry();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ZhiHandRuntimeConfig } from "./config.ts";
|
|
2
2
|
export declare function getSnapshotStaleThresholdMs(): number;
|
|
3
3
|
export interface ScreenshotResult {
|
|
4
4
|
buffer: Buffer;
|
|
@@ -9,5 +9,5 @@ export interface ScreenshotResult {
|
|
|
9
9
|
sequence: number;
|
|
10
10
|
stale: boolean;
|
|
11
11
|
}
|
|
12
|
-
export declare function fetchScreenshot(config:
|
|
13
|
-
export declare function fetchScreenshotBinary(config:
|
|
12
|
+
export declare function fetchScreenshot(config: ZhiHandRuntimeConfig): Promise<ScreenshotResult>;
|
|
13
|
+
export declare function fetchScreenshotBinary(config: ZhiHandRuntimeConfig): Promise<Buffer>;
|
package/dist/core/screenshot.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { log } from "./logger.js";
|
|
2
|
+
const dbg = (msg) => log.debug(msg);
|
|
2
3
|
// Snapshot is considered stale if the server-reported age exceeds this
|
|
3
4
|
// threshold. Configurable via env ZHIHAND_SNAPSHOT_MAX_AGE_MS.
|
|
4
5
|
// Default 5s: typical HID command + capture + upload is well under 2s;
|
|
@@ -29,7 +30,7 @@ export async function fetchScreenshot(config) {
|
|
|
29
30
|
const response = await fetch(url, {
|
|
30
31
|
method: "GET",
|
|
31
32
|
headers: {
|
|
32
|
-
"
|
|
33
|
+
"Authorization": `Bearer ${config.controllerToken}`,
|
|
33
34
|
"Accept": "image/jpeg",
|
|
34
35
|
},
|
|
35
36
|
signal: controller.signal,
|
package/dist/core/sse.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ZhiHandRuntimeConfig } from "./config.ts";
|
|
2
2
|
import type { QueuedCommandRecord, WaitForCommandAckResult } from "./command.ts";
|
|
3
3
|
export interface SSEEvent {
|
|
4
4
|
id: string;
|
|
@@ -6,29 +6,51 @@ export interface SSEEvent {
|
|
|
6
6
|
kind: string;
|
|
7
7
|
credential_id: string;
|
|
8
8
|
command?: QueuedCommandRecord;
|
|
9
|
+
device_profile?: Record<string, unknown>;
|
|
10
|
+
credential?: Record<string, unknown>;
|
|
9
11
|
sequence: number;
|
|
10
12
|
}
|
|
11
13
|
export declare function handleSSEEvent(event: SSEEvent): void;
|
|
12
14
|
export declare function subscribeToCommandAck(commandId: string, callback: (cmd: QueuedCommandRecord) => void): () => void;
|
|
15
|
+
export interface UserEventStreamHandlers {
|
|
16
|
+
onDeviceOnline: (credentialId: string) => void;
|
|
17
|
+
onDeviceOffline: (credentialId: string) => void;
|
|
18
|
+
onDeviceProfileUpdated: (credentialId: string, profile: Record<string, unknown>) => void;
|
|
19
|
+
onCommandAcked: (event: SSEEvent) => void;
|
|
20
|
+
onCredentialAdded: (credential: Record<string, unknown>) => void;
|
|
21
|
+
onCredentialRemoved: (credentialId: string) => void;
|
|
22
|
+
onConnected: () => void;
|
|
23
|
+
onDisconnected: () => void;
|
|
24
|
+
}
|
|
25
|
+
export declare class UserEventStream {
|
|
26
|
+
private userId;
|
|
27
|
+
private controllerToken;
|
|
28
|
+
private endpoint;
|
|
29
|
+
private handlers;
|
|
30
|
+
private abortController;
|
|
31
|
+
private _connected;
|
|
32
|
+
constructor(userId: string, controllerToken: string, endpoint: string, handlers: UserEventStreamHandlers);
|
|
33
|
+
get connected(): boolean;
|
|
34
|
+
start(): void;
|
|
35
|
+
stop(): void;
|
|
36
|
+
private runLoop;
|
|
37
|
+
private dispatchEvent;
|
|
38
|
+
}
|
|
39
|
+
export interface CredentialResponse {
|
|
40
|
+
credential_id: string;
|
|
41
|
+
label?: string;
|
|
42
|
+
platform?: string;
|
|
43
|
+
online?: boolean;
|
|
44
|
+
paired_at?: string;
|
|
45
|
+
last_seen_at?: string;
|
|
46
|
+
device_profile?: Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
export declare function fetchUserCredentials(endpoint: string, userId: string, controllerToken: string, onlineFilter?: boolean): Promise<CredentialResponse[]>;
|
|
13
49
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* Reconnects automatically on connection loss.
|
|
17
|
-
*/
|
|
18
|
-
export declare function connectSSE(config: ZhiHandConfig): void;
|
|
19
|
-
/**
|
|
20
|
-
* Disconnect the SSE event stream.
|
|
21
|
-
*/
|
|
22
|
-
export declare function disconnectSSE(): void;
|
|
23
|
-
/**
|
|
24
|
-
* Whether the SSE stream is currently connected.
|
|
25
|
-
*/
|
|
26
|
-
export declare function isSSEConnected(): boolean;
|
|
27
|
-
/**
|
|
28
|
-
* Wait for command ACK via SSE push.
|
|
29
|
-
* Falls back to polling if SSE is not active.
|
|
50
|
+
* Wait for command ACK via SSE push (which should already be connected by the
|
|
51
|
+
* registry). Falls back to polling.
|
|
30
52
|
*/
|
|
31
|
-
export declare function waitForCommandAck(config:
|
|
53
|
+
export declare function waitForCommandAck(config: ZhiHandRuntimeConfig, options: {
|
|
32
54
|
commandId: string;
|
|
33
55
|
timeoutMs?: number;
|
|
34
56
|
signal?: AbortSignal;
|