@lingyao037/openclaw-lingyao-cli 0.9.0 → 0.9.2
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/dist/{accounts-BNuShH7y.d.ts → accounts-B9ijYIIu.d.ts} +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/index.d.ts +37 -14
- package/dist/index.js +38 -35
- package/dist/index.js.map +1 -1
- package/dist/setup-entry.d.ts +3 -0
- package/dist/setup-entry.js +3039 -0
- package/dist/setup-entry.js.map +1 -0
- package/openclaw.plugin.json +3 -0
- package/package.json +8 -2
|
@@ -0,0 +1,3039 @@
|
|
|
1
|
+
// src/api.ts
|
|
2
|
+
import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
|
|
3
|
+
|
|
4
|
+
// src/runtime.ts
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
var globalRuntime = null;
|
|
8
|
+
function setRuntime(runtime) {
|
|
9
|
+
globalRuntime = runtime;
|
|
10
|
+
}
|
|
11
|
+
function adaptPluginRuntime(pr) {
|
|
12
|
+
const noop = (..._args) => {
|
|
13
|
+
};
|
|
14
|
+
const rawLogger = pr.logging?.getChildLogger?.() ?? {
|
|
15
|
+
info: console.info.bind(console),
|
|
16
|
+
warn: console.warn.bind(console),
|
|
17
|
+
error: console.error.bind(console)
|
|
18
|
+
};
|
|
19
|
+
const childLogger = {
|
|
20
|
+
info: rawLogger.info,
|
|
21
|
+
warn: rawLogger.warn,
|
|
22
|
+
error: rawLogger.error,
|
|
23
|
+
debug: rawLogger.debug ?? noop
|
|
24
|
+
};
|
|
25
|
+
const stateDir = pr.state?.resolveStateDir?.() ?? join(process.cwd(), ".lingyao-data");
|
|
26
|
+
const storeDir = join(stateDir, "lingyao");
|
|
27
|
+
return {
|
|
28
|
+
config: { enabled: true },
|
|
29
|
+
logger: childLogger,
|
|
30
|
+
storage: {
|
|
31
|
+
async get(key) {
|
|
32
|
+
try {
|
|
33
|
+
const filePath = join(storeDir, `${key}.json`);
|
|
34
|
+
if (!existsSync(filePath)) return null;
|
|
35
|
+
const data = readFileSync(filePath, "utf-8");
|
|
36
|
+
return JSON.parse(data);
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
async set(key, value) {
|
|
42
|
+
if (!existsSync(storeDir)) {
|
|
43
|
+
mkdirSync(storeDir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
const filePath = join(storeDir, `${key}.json`);
|
|
46
|
+
writeFileSync(filePath, JSON.stringify(value, null, 2), "utf-8");
|
|
47
|
+
},
|
|
48
|
+
async delete(key) {
|
|
49
|
+
const filePath = join(storeDir, `${key}.json`);
|
|
50
|
+
if (existsSync(filePath)) {
|
|
51
|
+
unlinkSync(filePath);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
tools: {
|
|
56
|
+
async call() {
|
|
57
|
+
throw new Error("Tool calls not available in SDK mode");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/adapters/config.ts
|
|
64
|
+
function extractAccounts(cfg) {
|
|
65
|
+
const channels = cfg?.channels;
|
|
66
|
+
const lingyao = channels?.lingyao;
|
|
67
|
+
const accounts = lingyao?.accounts;
|
|
68
|
+
if (!accounts) return {};
|
|
69
|
+
return accounts;
|
|
70
|
+
}
|
|
71
|
+
function createConfigAdapter() {
|
|
72
|
+
return {
|
|
73
|
+
listAccountIds(cfg) {
|
|
74
|
+
const accounts = extractAccounts(cfg);
|
|
75
|
+
return Object.keys(accounts);
|
|
76
|
+
},
|
|
77
|
+
resolveAccount(cfg, accountId) {
|
|
78
|
+
const accounts = extractAccounts(cfg);
|
|
79
|
+
const ids = Object.keys(accounts);
|
|
80
|
+
if (ids.length === 0) {
|
|
81
|
+
accounts["default"] = {};
|
|
82
|
+
}
|
|
83
|
+
const resolvedId = accountId ?? (ids.includes("default") ? "default" : ids[0]);
|
|
84
|
+
if (!resolvedId) {
|
|
85
|
+
throw new Error("No lingyao accounts configured");
|
|
86
|
+
}
|
|
87
|
+
const accountConfig = accounts[resolvedId];
|
|
88
|
+
if (!accountConfig) {
|
|
89
|
+
throw new Error(`Account "${resolvedId}" not found`);
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
id: resolvedId,
|
|
93
|
+
accountId: resolvedId,
|
|
94
|
+
enabled: accountConfig?.enabled !== false,
|
|
95
|
+
dmPolicy: accountConfig?.dmPolicy ?? "paired",
|
|
96
|
+
allowFrom: accountConfig?.allowFrom ?? [],
|
|
97
|
+
gatewayId: accountConfig?.gatewayId,
|
|
98
|
+
rawConfig: accountConfig
|
|
99
|
+
};
|
|
100
|
+
},
|
|
101
|
+
isConfigured(_account, _cfg) {
|
|
102
|
+
return true;
|
|
103
|
+
},
|
|
104
|
+
isEnabled(account, _cfg) {
|
|
105
|
+
return account.enabled;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/adapters/gateway.ts
|
|
111
|
+
function createGatewayAdapter(getOrchestrator2) {
|
|
112
|
+
return {
|
|
113
|
+
async startAccount(ctx) {
|
|
114
|
+
const orchestrator2 = getOrchestrator2();
|
|
115
|
+
if (!orchestrator2) {
|
|
116
|
+
throw new Error("Orchestrator not initialized. Ensure setRuntime was called.");
|
|
117
|
+
}
|
|
118
|
+
ctx.log?.info(`Starting account "${ctx.accountId}"`);
|
|
119
|
+
await orchestrator2.start(ctx.account);
|
|
120
|
+
ctx.log?.info(`Account "${ctx.accountId}" started successfully`);
|
|
121
|
+
},
|
|
122
|
+
async stopAccount(ctx) {
|
|
123
|
+
const orchestrator2 = getOrchestrator2();
|
|
124
|
+
if (!orchestrator2) {
|
|
125
|
+
throw new Error("Orchestrator not initialized");
|
|
126
|
+
}
|
|
127
|
+
ctx.log?.info(`Stopping account "${ctx.accountId}"`);
|
|
128
|
+
await orchestrator2.stop(ctx.accountId);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/adapters/status.ts
|
|
134
|
+
function createStatusAdapter(getOrchestrator2, _runtime) {
|
|
135
|
+
return {
|
|
136
|
+
async probeAccount(params) {
|
|
137
|
+
const orchestrator2 = getOrchestrator2();
|
|
138
|
+
if (!orchestrator2) {
|
|
139
|
+
return {
|
|
140
|
+
ok: false,
|
|
141
|
+
status: "unhealthy",
|
|
142
|
+
wsConnected: false,
|
|
143
|
+
error: "Orchestrator not initialized"
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const state = orchestrator2.getAccountState(params.account.id);
|
|
147
|
+
if (!state) {
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
status: "unhealthy",
|
|
151
|
+
wsConnected: false,
|
|
152
|
+
error: "Account not started"
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const healthResult = await state.probe.runHealthChecks();
|
|
157
|
+
const wsConnected = state.wsClient?.isConnected() ?? false;
|
|
158
|
+
let status = "healthy";
|
|
159
|
+
switch (healthResult.status) {
|
|
160
|
+
case "healthy":
|
|
161
|
+
status = wsConnected ? "healthy" : "degraded";
|
|
162
|
+
break;
|
|
163
|
+
case "degraded":
|
|
164
|
+
status = "degraded";
|
|
165
|
+
break;
|
|
166
|
+
case "unhealthy":
|
|
167
|
+
status = "unhealthy";
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
const checks = {};
|
|
171
|
+
for (const [name, result] of healthResult.checks) {
|
|
172
|
+
checks[name] = {
|
|
173
|
+
passed: result.passed,
|
|
174
|
+
message: result.message ?? "",
|
|
175
|
+
duration: result.duration
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
ok: status !== "unhealthy",
|
|
180
|
+
status,
|
|
181
|
+
wsConnected,
|
|
182
|
+
uptime: Date.now() - state.startTime,
|
|
183
|
+
checks
|
|
184
|
+
};
|
|
185
|
+
} catch (error) {
|
|
186
|
+
return {
|
|
187
|
+
ok: false,
|
|
188
|
+
status: "unhealthy",
|
|
189
|
+
wsConnected: false,
|
|
190
|
+
error: error.message
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
async buildChannelSummary(params) {
|
|
195
|
+
const orchestrator2 = getOrchestrator2();
|
|
196
|
+
const accountId = params.account.id;
|
|
197
|
+
const state = orchestrator2?.getAccountState(accountId);
|
|
198
|
+
const runningIds = orchestrator2?.getRunningAccountIds() ?? [];
|
|
199
|
+
return {
|
|
200
|
+
accountId,
|
|
201
|
+
status: state?.status ?? "stopped",
|
|
202
|
+
wsConnected: state?.wsClient?.isConnected() ?? false,
|
|
203
|
+
isDefault: accountId === params.defaultAccountId,
|
|
204
|
+
running: runningIds.includes(accountId),
|
|
205
|
+
uptime: state ? Date.now() - state.startTime : 0
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/adapters/directory.ts
|
|
212
|
+
function createDirectoryAdapter(getOrchestrator2) {
|
|
213
|
+
return {
|
|
214
|
+
async listPeers(params) {
|
|
215
|
+
const orchestrator2 = getOrchestrator2();
|
|
216
|
+
if (!orchestrator2) {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
const accountId = params.accountId ?? "default";
|
|
220
|
+
const accountManager = orchestrator2.getAccountManager(accountId);
|
|
221
|
+
if (!accountManager) {
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
const activeAccounts = accountManager.getActiveAccounts();
|
|
225
|
+
let entries = activeAccounts.map((account) => ({
|
|
226
|
+
kind: "user",
|
|
227
|
+
id: account.deviceId,
|
|
228
|
+
name: account.deviceInfo.name || account.deviceId,
|
|
229
|
+
handle: account.deviceId,
|
|
230
|
+
raw: account
|
|
231
|
+
}));
|
|
232
|
+
if (params.query) {
|
|
233
|
+
const q = params.query.toLowerCase();
|
|
234
|
+
entries = entries.filter(
|
|
235
|
+
(e) => e.id.toLowerCase().includes(q) || (e.name?.toLowerCase().includes(q) ?? false)
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
if (params.limit != null && params.limit > 0) {
|
|
239
|
+
entries = entries.slice(0, params.limit);
|
|
240
|
+
}
|
|
241
|
+
return entries;
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/adapters/messaging.ts
|
|
247
|
+
var PREFIX = "lingyao:";
|
|
248
|
+
function createMessagingAdapter() {
|
|
249
|
+
return {
|
|
250
|
+
normalizeTarget(raw) {
|
|
251
|
+
if (!raw || typeof raw !== "string") {
|
|
252
|
+
return void 0;
|
|
253
|
+
}
|
|
254
|
+
const target = raw.startsWith(PREFIX) ? raw.slice(PREFIX.length) : raw;
|
|
255
|
+
if (!target) {
|
|
256
|
+
return void 0;
|
|
257
|
+
}
|
|
258
|
+
return target;
|
|
259
|
+
},
|
|
260
|
+
resolveSessionTarget(params) {
|
|
261
|
+
return `${PREFIX}${params.id}`;
|
|
262
|
+
},
|
|
263
|
+
inferTargetChatType(_params) {
|
|
264
|
+
return "direct";
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/adapters/outbound.ts
|
|
270
|
+
function createOutboundAdapter(getOrchestrator2) {
|
|
271
|
+
return {
|
|
272
|
+
deliveryMode: "direct",
|
|
273
|
+
async sendText(ctx) {
|
|
274
|
+
const orchestrator2 = getOrchestrator2();
|
|
275
|
+
if (!orchestrator2) {
|
|
276
|
+
throw new Error("Orchestrator not initialized");
|
|
277
|
+
}
|
|
278
|
+
const accountId = ctx.accountId ?? "default";
|
|
279
|
+
const sent = orchestrator2.sendNotification(
|
|
280
|
+
accountId,
|
|
281
|
+
ctx.to,
|
|
282
|
+
{ title: "OpenClaw", body: ctx.text }
|
|
283
|
+
);
|
|
284
|
+
if (!sent) {
|
|
285
|
+
throw new Error(
|
|
286
|
+
`Failed to send text to device "${ctx.to}" on account "${accountId}": not connected`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
channel: "lingyao",
|
|
291
|
+
messageId: `out_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
292
|
+
chatId: ctx.to,
|
|
293
|
+
timestamp: Date.now()
|
|
294
|
+
};
|
|
295
|
+
},
|
|
296
|
+
async sendPayload(ctx) {
|
|
297
|
+
const orchestrator2 = getOrchestrator2();
|
|
298
|
+
if (!orchestrator2) {
|
|
299
|
+
throw new Error("Orchestrator not initialized");
|
|
300
|
+
}
|
|
301
|
+
const accountId = ctx.accountId ?? "default";
|
|
302
|
+
const sent = orchestrator2.sendNotification(
|
|
303
|
+
accountId,
|
|
304
|
+
ctx.to,
|
|
305
|
+
ctx.payload
|
|
306
|
+
);
|
|
307
|
+
if (!sent) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
`Failed to send payload to device "${ctx.to}" on account "${accountId}": not connected`
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
channel: "lingyao",
|
|
314
|
+
messageId: `out_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
315
|
+
chatId: ctx.to,
|
|
316
|
+
timestamp: Date.now()
|
|
317
|
+
};
|
|
318
|
+
},
|
|
319
|
+
resolveTarget(params) {
|
|
320
|
+
const raw = params.to;
|
|
321
|
+
if (!raw || typeof raw !== "string" || raw.trim().length === 0) {
|
|
322
|
+
return { ok: false, error: new Error("Target deviceId is empty or missing") };
|
|
323
|
+
}
|
|
324
|
+
return { ok: true, to: raw.trim() };
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/adapters/setup.ts
|
|
330
|
+
import { createPatchedAccountSetupAdapter } from "openclaw/plugin-sdk/setup";
|
|
331
|
+
function createSetupAdapter() {
|
|
332
|
+
return createPatchedAccountSetupAdapter({
|
|
333
|
+
channelKey: "lingyao",
|
|
334
|
+
alwaysUseAccounts: true,
|
|
335
|
+
ensureChannelEnabled: true,
|
|
336
|
+
ensureAccountEnabled: true,
|
|
337
|
+
buildPatch() {
|
|
338
|
+
return {};
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/orchestrator.ts
|
|
344
|
+
import { hostname, networkInterfaces } from "os";
|
|
345
|
+
import { createHash as createHash2 } from "crypto";
|
|
346
|
+
|
|
347
|
+
// src/types.ts
|
|
348
|
+
var LINGYAO_SERVER_URL = "https://api.lingyao.live";
|
|
349
|
+
|
|
350
|
+
// src/server-client.ts
|
|
351
|
+
import axios from "axios";
|
|
352
|
+
function isAxiosError(error) {
|
|
353
|
+
return typeof error === "object" && error !== null && "isAxiosError" in error && error.isAxiosError === true;
|
|
354
|
+
}
|
|
355
|
+
var ServerHttpClient = class {
|
|
356
|
+
runtime;
|
|
357
|
+
config;
|
|
358
|
+
axiosInstance;
|
|
359
|
+
gatewayToken = null;
|
|
360
|
+
webhookSecret = null;
|
|
361
|
+
tokenExpiresAt = 0;
|
|
362
|
+
heartbeatInterval = 3e4;
|
|
363
|
+
heartbeatTimer = null;
|
|
364
|
+
gatewayId;
|
|
365
|
+
isRegistered = false;
|
|
366
|
+
isConnecting = false;
|
|
367
|
+
storagePrefix;
|
|
368
|
+
constructor(runtime, gatewayId, serverConfig = {}, storagePrefix = "lingyao") {
|
|
369
|
+
this.runtime = runtime;
|
|
370
|
+
this.gatewayId = gatewayId;
|
|
371
|
+
this.storagePrefix = storagePrefix;
|
|
372
|
+
this.config = {
|
|
373
|
+
baseURL: serverConfig.baseURL || "https://api.lingyao.live",
|
|
374
|
+
apiBase: serverConfig.apiBase || "/v1",
|
|
375
|
+
timeout: serverConfig.timeout || 3e4,
|
|
376
|
+
connectionTimeout: serverConfig.connectionTimeout || 5e3
|
|
377
|
+
};
|
|
378
|
+
this.axiosInstance = axios.create({
|
|
379
|
+
baseURL: this.config.baseURL + this.config.apiBase,
|
|
380
|
+
timeout: this.config.timeout,
|
|
381
|
+
headers: {
|
|
382
|
+
"Content-Type": "application/json"
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
this.axiosInstance.interceptors.request.use(
|
|
386
|
+
(config) => {
|
|
387
|
+
if (this.gatewayToken && config.headers) {
|
|
388
|
+
config.headers.Authorization = `Bearer ${this.gatewayToken}`;
|
|
389
|
+
}
|
|
390
|
+
return config;
|
|
391
|
+
},
|
|
392
|
+
(error) => Promise.reject(error)
|
|
393
|
+
);
|
|
394
|
+
this.axiosInstance.interceptors.response.use(
|
|
395
|
+
(response) => response,
|
|
396
|
+
async (error) => {
|
|
397
|
+
if (isAxiosError(error)) {
|
|
398
|
+
if (error.response?.status === 401) {
|
|
399
|
+
this.runtime.logger.warn("Token expired, attempting to re-register...");
|
|
400
|
+
await this.register();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return Promise.reject(error);
|
|
404
|
+
}
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* 注册 Gateway 到服务器
|
|
409
|
+
*/
|
|
410
|
+
async register(capabilities2 = {}) {
|
|
411
|
+
if (this.isConnecting) {
|
|
412
|
+
throw new Error("Registration already in progress");
|
|
413
|
+
}
|
|
414
|
+
this.isConnecting = true;
|
|
415
|
+
try {
|
|
416
|
+
this.runtime.logger.info(`Registering gateway ${this.gatewayId} to server...`);
|
|
417
|
+
const response = await this.axiosInstance.post(
|
|
418
|
+
"/gateway/register",
|
|
419
|
+
{
|
|
420
|
+
gatewayId: this.gatewayId,
|
|
421
|
+
version: "0.1.0",
|
|
422
|
+
capabilities: {
|
|
423
|
+
websocket: false,
|
|
424
|
+
compression: false,
|
|
425
|
+
...capabilities2
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
);
|
|
429
|
+
const data = response.data;
|
|
430
|
+
this.gatewayToken = data.gatewayToken;
|
|
431
|
+
this.webhookSecret = data.webhookSecret;
|
|
432
|
+
this.tokenExpiresAt = data.expiresAt;
|
|
433
|
+
this.heartbeatInterval = data.serverConfig.heartbeatInterval;
|
|
434
|
+
this.isRegistered = true;
|
|
435
|
+
await this.runtime.storage.set(this.storageKey("gatewayToken"), this.gatewayToken);
|
|
436
|
+
await this.runtime.storage.set(this.storageKey("webhookSecret"), this.webhookSecret);
|
|
437
|
+
await this.runtime.storage.set(this.storageKey("tokenExpiresAt"), this.tokenExpiresAt);
|
|
438
|
+
await this.runtime.storage.set(this.storageKey("serverConfig"), data.serverConfig);
|
|
439
|
+
this.runtime.logger.info("Gateway registered successfully", {
|
|
440
|
+
expiresAt: new Date(this.tokenExpiresAt).toISOString(),
|
|
441
|
+
heartbeatInterval: this.heartbeatInterval
|
|
442
|
+
});
|
|
443
|
+
this.startHeartbeat();
|
|
444
|
+
return data;
|
|
445
|
+
} catch (error) {
|
|
446
|
+
if (isAxiosError(error)) {
|
|
447
|
+
const axiosError = error;
|
|
448
|
+
const status = axiosError.response?.status;
|
|
449
|
+
const data = axiosError.response?.data;
|
|
450
|
+
if (status === 409) {
|
|
451
|
+
throw new Error("Gateway already registered");
|
|
452
|
+
} else if (status === 400) {
|
|
453
|
+
throw new Error(`Invalid request: ${data?.details || "Unknown error"}`);
|
|
454
|
+
}
|
|
455
|
+
throw new Error(`Registration failed: ${axiosError.message}`);
|
|
456
|
+
}
|
|
457
|
+
throw error;
|
|
458
|
+
} finally {
|
|
459
|
+
this.isConnecting = false;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* 发送心跳
|
|
464
|
+
*/
|
|
465
|
+
async heartbeat(status = "online", activeConnections = 0) {
|
|
466
|
+
if (!this.isRegistered || !this.gatewayToken) {
|
|
467
|
+
throw new Error("Gateway not registered");
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
const response = await this.axiosInstance.post(
|
|
471
|
+
"/gateway/heartbeat",
|
|
472
|
+
{
|
|
473
|
+
timestamp: Date.now(),
|
|
474
|
+
status,
|
|
475
|
+
activeConnections
|
|
476
|
+
}
|
|
477
|
+
);
|
|
478
|
+
return response.data;
|
|
479
|
+
} catch (error) {
|
|
480
|
+
if (isAxiosError(error)) {
|
|
481
|
+
const axiosError = error;
|
|
482
|
+
this.runtime.logger.error("Heartbeat failed", {
|
|
483
|
+
status: axiosError.response?.status,
|
|
484
|
+
data: axiosError.response?.data
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
throw error;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* 发送消息到灵爻 App
|
|
492
|
+
*/
|
|
493
|
+
async sendMessage(deviceId, messageType, payload, options) {
|
|
494
|
+
if (!this.isRegistered || !this.gatewayToken) {
|
|
495
|
+
throw new Error("Gateway not registered");
|
|
496
|
+
}
|
|
497
|
+
const messageId = this.generateMessageId();
|
|
498
|
+
try {
|
|
499
|
+
const response = await this.axiosInstance.post(
|
|
500
|
+
"/gateway/messages",
|
|
501
|
+
{
|
|
502
|
+
deviceId,
|
|
503
|
+
message: {
|
|
504
|
+
id: messageId,
|
|
505
|
+
type: messageType,
|
|
506
|
+
timestamp: Date.now(),
|
|
507
|
+
payload
|
|
508
|
+
},
|
|
509
|
+
options: options || {}
|
|
510
|
+
}
|
|
511
|
+
);
|
|
512
|
+
this.runtime.logger.debug("Message sent", {
|
|
513
|
+
messageId,
|
|
514
|
+
deviceId,
|
|
515
|
+
status: response.data.status
|
|
516
|
+
});
|
|
517
|
+
return response.data;
|
|
518
|
+
} catch (error) {
|
|
519
|
+
if (isAxiosError(error)) {
|
|
520
|
+
const axiosError = error;
|
|
521
|
+
const status = axiosError.response?.status;
|
|
522
|
+
const data = axiosError.response?.data;
|
|
523
|
+
if (status === 404) {
|
|
524
|
+
throw new Error(`Device not found: ${deviceId}`);
|
|
525
|
+
} else if (status === 429) {
|
|
526
|
+
throw new Error("Message queue full, please retry later");
|
|
527
|
+
}
|
|
528
|
+
throw new Error(`Send message failed: ${data?.error || axiosError.message}`);
|
|
529
|
+
}
|
|
530
|
+
throw error;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* 启动心跳循环
|
|
535
|
+
*/
|
|
536
|
+
startHeartbeat() {
|
|
537
|
+
if (this.heartbeatTimer) {
|
|
538
|
+
clearInterval(this.heartbeatTimer);
|
|
539
|
+
}
|
|
540
|
+
this.heartbeatTimer = setInterval(
|
|
541
|
+
async () => {
|
|
542
|
+
try {
|
|
543
|
+
await this.heartbeat();
|
|
544
|
+
} catch (error) {
|
|
545
|
+
this.runtime.logger.error("Heartbeat error", error);
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
this.heartbeatInterval
|
|
549
|
+
);
|
|
550
|
+
this.runtime.logger.info("Heartbeat started", {
|
|
551
|
+
interval: this.heartbeatInterval
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* 停止心跳循环
|
|
556
|
+
*/
|
|
557
|
+
stopHeartbeat() {
|
|
558
|
+
if (this.heartbeatTimer) {
|
|
559
|
+
clearInterval(this.heartbeatTimer);
|
|
560
|
+
this.heartbeatTimer = null;
|
|
561
|
+
this.runtime.logger.info("Heartbeat stopped");
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* 获取 Webhook Secret
|
|
566
|
+
*/
|
|
567
|
+
getWebhookSecret() {
|
|
568
|
+
return this.webhookSecret;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* 获取 Gateway Token
|
|
572
|
+
*/
|
|
573
|
+
getGatewayToken() {
|
|
574
|
+
return this.gatewayToken;
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* 检查 Token 是否即将过期
|
|
578
|
+
*/
|
|
579
|
+
isTokenExpiringSoon(thresholdMs = 7 * 24 * 60 * 60 * 1e3) {
|
|
580
|
+
return this.tokenExpiresAt - Date.now() < thresholdMs;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* 检查是否已注册
|
|
584
|
+
*/
|
|
585
|
+
isReady() {
|
|
586
|
+
return this.isRegistered && !!this.gatewayToken;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* 从存储恢复会话
|
|
590
|
+
*/
|
|
591
|
+
async restoreFromStorage() {
|
|
592
|
+
try {
|
|
593
|
+
const token = await this.runtime.storage.get(this.storageKey("gatewayToken"));
|
|
594
|
+
const secret = await this.runtime.storage.get(this.storageKey("webhookSecret"));
|
|
595
|
+
const expiresAt = await this.runtime.storage.get(this.storageKey("tokenExpiresAt"));
|
|
596
|
+
const serverConfig = await this.runtime.storage.get(this.storageKey("serverConfig"));
|
|
597
|
+
if (token && secret && expiresAt && serverConfig) {
|
|
598
|
+
if (expiresAt > Date.now()) {
|
|
599
|
+
this.gatewayToken = token;
|
|
600
|
+
this.webhookSecret = secret;
|
|
601
|
+
this.tokenExpiresAt = expiresAt;
|
|
602
|
+
this.heartbeatInterval = serverConfig.heartbeatInterval;
|
|
603
|
+
this.isRegistered = true;
|
|
604
|
+
this.startHeartbeat();
|
|
605
|
+
this.runtime.logger.info("Session restored from storage", {
|
|
606
|
+
expiresAt: new Date(this.tokenExpiresAt).toISOString()
|
|
607
|
+
});
|
|
608
|
+
return true;
|
|
609
|
+
} else {
|
|
610
|
+
this.runtime.logger.warn("Stored token expired, need to re-register");
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
} catch (error) {
|
|
614
|
+
this.runtime.logger.error("Failed to restore session from storage", error);
|
|
615
|
+
}
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Build a namespaced storage key for multi-account support
|
|
620
|
+
*/
|
|
621
|
+
storageKey(name) {
|
|
622
|
+
return `${this.storagePrefix}:${name}`;
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* 生成消息 ID
|
|
626
|
+
*/
|
|
627
|
+
generateMessageId() {
|
|
628
|
+
return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
// src/websocket-client.ts
|
|
633
|
+
import WebSocket from "ws";
|
|
634
|
+
var LingyaoWSClient = class {
|
|
635
|
+
config;
|
|
636
|
+
ws = null;
|
|
637
|
+
state = "disconnected";
|
|
638
|
+
connectionId = null;
|
|
639
|
+
heartbeatTimer = null;
|
|
640
|
+
reconnectTimer = null;
|
|
641
|
+
messageHandlers = /* @__PURE__ */ new Map();
|
|
642
|
+
logger;
|
|
643
|
+
constructor(runtime, config) {
|
|
644
|
+
this.logger = runtime.logger;
|
|
645
|
+
this.config = { ...config };
|
|
646
|
+
this.registerMessageHandler("registered" /* REGISTERED */, this.handleRegistered.bind(this));
|
|
647
|
+
this.registerMessageHandler("heartbeat_ack" /* HEARTBEAT_ACK */, this.handleHeartbeatAck.bind(this));
|
|
648
|
+
this.registerMessageHandler("message_delivered" /* MESSAGE_DELIVERED */, this.handleMessageDelivered.bind(this));
|
|
649
|
+
this.registerMessageHandler("message_failed" /* MESSAGE_FAILED */, this.handleMessageFailed.bind(this));
|
|
650
|
+
this.registerMessageHandler("app_message" /* APP_MESSAGE */, this.handleAppMessage.bind(this));
|
|
651
|
+
this.registerMessageHandler("pairing_completed" /* PAIRING_COMPLETED */, this.handlePairingCompleted.bind(this));
|
|
652
|
+
this.registerMessageHandler("error" /* ERROR */, this.handleError.bind(this));
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* 连接到服务器
|
|
656
|
+
*/
|
|
657
|
+
async connect() {
|
|
658
|
+
if (this.state === "connecting" || this.state === "connected") {
|
|
659
|
+
this.logger.warn("WebSocket already connecting or connected");
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
this.state = "connecting";
|
|
663
|
+
this.emitEvent({ type: "disconnected", code: 0, reason: "Reconnecting" });
|
|
664
|
+
try {
|
|
665
|
+
this.logger.info(`Connecting to Lingyao server: ${this.config.url}`);
|
|
666
|
+
const wsUrl = this.config.token ? `${this.config.url}?token=${encodeURIComponent(this.config.token)}` : this.config.url;
|
|
667
|
+
this.ws = new WebSocket(wsUrl, {
|
|
668
|
+
headers: {
|
|
669
|
+
"X-Gateway-ID": this.config.gatewayId
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
this.setupWebSocketHandlers();
|
|
673
|
+
} catch (error) {
|
|
674
|
+
this.state = "error";
|
|
675
|
+
this.emitEvent({ type: "error", error });
|
|
676
|
+
this.scheduleReconnect();
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* 设置 WebSocket 事件处理器
|
|
681
|
+
*/
|
|
682
|
+
setupWebSocketHandlers() {
|
|
683
|
+
if (!this.ws) return;
|
|
684
|
+
this.ws.on("open", () => {
|
|
685
|
+
this.handleOpen();
|
|
686
|
+
});
|
|
687
|
+
this.ws.on("message", async (data) => {
|
|
688
|
+
await this.handleMessage(data);
|
|
689
|
+
});
|
|
690
|
+
this.ws.on("error", (error) => {
|
|
691
|
+
this.handleErrorEvent(error);
|
|
692
|
+
});
|
|
693
|
+
this.ws.on("close", (code, reason) => {
|
|
694
|
+
this.handleClose(code, reason.toString());
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* 处理连接打开
|
|
699
|
+
*/
|
|
700
|
+
handleOpen() {
|
|
701
|
+
this.state = "connected";
|
|
702
|
+
this.connectionId = this.generateConnectionId();
|
|
703
|
+
this.logger.info("WebSocket connected to Lingyao server", {
|
|
704
|
+
connectionId: this.connectionId
|
|
705
|
+
});
|
|
706
|
+
this.emitEvent({
|
|
707
|
+
type: "connected",
|
|
708
|
+
connectionId: this.connectionId
|
|
709
|
+
});
|
|
710
|
+
this.sendRegister();
|
|
711
|
+
this.startHeartbeat();
|
|
712
|
+
if (this.reconnectTimer) {
|
|
713
|
+
clearTimeout(this.reconnectTimer);
|
|
714
|
+
this.reconnectTimer = null;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* 处理接收消息
|
|
719
|
+
*/
|
|
720
|
+
async handleMessage(data) {
|
|
721
|
+
try {
|
|
722
|
+
const message = JSON.parse(data.toString());
|
|
723
|
+
this.logger.debug("Received message from server", { type: message.type });
|
|
724
|
+
const handler = this.messageHandlers.get(message.type);
|
|
725
|
+
if (handler) {
|
|
726
|
+
handler(message);
|
|
727
|
+
} else {
|
|
728
|
+
this.logger.warn("No handler for message type", { type: message.type });
|
|
729
|
+
}
|
|
730
|
+
this.emitEvent({ type: "message", message });
|
|
731
|
+
} catch (error) {
|
|
732
|
+
this.logger.error("Error handling message", error);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* 处理连接错误
|
|
737
|
+
*/
|
|
738
|
+
handleErrorEvent(error) {
|
|
739
|
+
this.logger.error("WebSocket error", error);
|
|
740
|
+
this.state = "error";
|
|
741
|
+
this.emitEvent({ type: "error", error });
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* 处理连接关闭
|
|
745
|
+
*/
|
|
746
|
+
handleClose(code, reason) {
|
|
747
|
+
this.logger.warn("WebSocket connection closed", { code, reason });
|
|
748
|
+
this.state = "disconnected";
|
|
749
|
+
this.connectionId = null;
|
|
750
|
+
this.stopHeartbeat();
|
|
751
|
+
this.emitEvent({ type: "disconnected", code, reason });
|
|
752
|
+
if (code === 1008) {
|
|
753
|
+
this.logger.error("WebSocket closed with 1008 (Invalid Token). Stopping reconnect loop.");
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
if (code !== 1e3) {
|
|
757
|
+
this.scheduleReconnect();
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* 发送注册消息
|
|
762
|
+
*/
|
|
763
|
+
sendRegister() {
|
|
764
|
+
const message = {
|
|
765
|
+
type: "register" /* REGISTER */,
|
|
766
|
+
id: this.generateMessageId(),
|
|
767
|
+
timestamp: Date.now(),
|
|
768
|
+
payload: {
|
|
769
|
+
gatewayId: this.config.gatewayId,
|
|
770
|
+
version: "0.2.0",
|
|
771
|
+
capabilities: {
|
|
772
|
+
websocket: true,
|
|
773
|
+
compression: false,
|
|
774
|
+
maxMessageSize: 1048576
|
|
775
|
+
// 1MB
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
this.send(message);
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* 处理注册响应
|
|
783
|
+
*/
|
|
784
|
+
handleRegistered(message) {
|
|
785
|
+
this.logger.info("Gateway registered to Lingyao server", {
|
|
786
|
+
messageId: message.id
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* 发送心跳
|
|
791
|
+
*/
|
|
792
|
+
sendHeartbeat() {
|
|
793
|
+
const message = {
|
|
794
|
+
type: "heartbeat" /* HEARTBEAT */,
|
|
795
|
+
id: this.generateMessageId(),
|
|
796
|
+
timestamp: Date.now(),
|
|
797
|
+
payload: {
|
|
798
|
+
timestamp: Date.now(),
|
|
799
|
+
status: "online"
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
this.send(message);
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* 处理心跳确认
|
|
806
|
+
*/
|
|
807
|
+
handleHeartbeatAck(_message) {
|
|
808
|
+
this.logger.debug("Heartbeat acknowledged");
|
|
809
|
+
}
|
|
810
|
+
/**
|
|
811
|
+
* 启动心跳
|
|
812
|
+
*/
|
|
813
|
+
startHeartbeat() {
|
|
814
|
+
if (this.heartbeatTimer) {
|
|
815
|
+
clearInterval(this.heartbeatTimer);
|
|
816
|
+
}
|
|
817
|
+
this.heartbeatTimer = setInterval(() => {
|
|
818
|
+
if (this.state === "connected") {
|
|
819
|
+
this.sendHeartbeat();
|
|
820
|
+
}
|
|
821
|
+
}, this.config.heartbeatInterval);
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* 停止心跳
|
|
825
|
+
*/
|
|
826
|
+
stopHeartbeat() {
|
|
827
|
+
if (this.heartbeatTimer) {
|
|
828
|
+
clearInterval(this.heartbeatTimer);
|
|
829
|
+
this.heartbeatTimer = null;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* 安排重连
|
|
834
|
+
*/
|
|
835
|
+
scheduleReconnect() {
|
|
836
|
+
if (this.reconnectTimer) {
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
this.logger.info(`Scheduling reconnect in ${this.config.reconnectInterval}ms`);
|
|
840
|
+
this.reconnectTimer = setTimeout(() => {
|
|
841
|
+
this.reconnectTimer = null;
|
|
842
|
+
this.connect();
|
|
843
|
+
}, this.config.reconnectInterval);
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* 发送消息到服务器
|
|
847
|
+
*/
|
|
848
|
+
send(message) {
|
|
849
|
+
if (!this.ws || this.state !== "connected") {
|
|
850
|
+
throw new Error("WebSocket not connected");
|
|
851
|
+
}
|
|
852
|
+
try {
|
|
853
|
+
this.ws.send(JSON.stringify(message));
|
|
854
|
+
this.logger.debug("Sent message to server", { type: message.type });
|
|
855
|
+
} catch (error) {
|
|
856
|
+
this.logger.error("Failed to send message", error);
|
|
857
|
+
throw error;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* 发送通知到鸿蒙 App
|
|
862
|
+
*/
|
|
863
|
+
sendNotification(deviceId, notification) {
|
|
864
|
+
const message = {
|
|
865
|
+
type: "send_message" /* SEND_MESSAGE */,
|
|
866
|
+
id: this.generateMessageId(),
|
|
867
|
+
timestamp: Date.now(),
|
|
868
|
+
payload: {
|
|
869
|
+
deviceId,
|
|
870
|
+
message: {
|
|
871
|
+
id: this.generateMessageId(),
|
|
872
|
+
type: "notify_action",
|
|
873
|
+
timestamp: Date.now(),
|
|
874
|
+
payload: notification
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
this.send(message);
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* 处理 App 消息
|
|
882
|
+
*/
|
|
883
|
+
handleAppMessage(message) {
|
|
884
|
+
if (message.type !== "app_message" /* APP_MESSAGE */) return;
|
|
885
|
+
const appMessage = message;
|
|
886
|
+
this.logger.info("Received message from App", {
|
|
887
|
+
deviceId: appMessage.payload.deviceId,
|
|
888
|
+
messageType: appMessage.payload.message.type
|
|
889
|
+
});
|
|
890
|
+
if (this.config.messageHandler) {
|
|
891
|
+
Promise.resolve(this.config.messageHandler(appMessage)).catch((error) => {
|
|
892
|
+
this.logger.error("Error handling App message", error);
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* 处理消息发送成功
|
|
898
|
+
*/
|
|
899
|
+
handleMessageDelivered(message) {
|
|
900
|
+
this.logger.debug("Message delivered successfully", {
|
|
901
|
+
messageId: message.id
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* 处理配对完成通知(来自 lingyao.live 服务器)
|
|
906
|
+
*/
|
|
907
|
+
handlePairingCompleted(message) {
|
|
908
|
+
const payload = message.payload;
|
|
909
|
+
this.logger.info("Pairing completed", {
|
|
910
|
+
deviceId: payload?.deviceId,
|
|
911
|
+
sessionId: payload?.sessionId
|
|
912
|
+
});
|
|
913
|
+
if (this.config.eventHandler) {
|
|
914
|
+
this.config.eventHandler({
|
|
915
|
+
type: "pairing_completed",
|
|
916
|
+
deviceId: payload?.deviceId,
|
|
917
|
+
deviceInfo: payload?.deviceInfo,
|
|
918
|
+
sessionId: payload?.sessionId
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* 处理消息发送失败
|
|
924
|
+
*/
|
|
925
|
+
handleMessageFailed(message) {
|
|
926
|
+
this.logger.warn("Message delivery failed", {
|
|
927
|
+
messageId: message.id
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* 处理服务器错误
|
|
932
|
+
*/
|
|
933
|
+
handleError(message) {
|
|
934
|
+
this.logger.error("Server error", message);
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* 注册消息处理器
|
|
938
|
+
*/
|
|
939
|
+
registerMessageHandler(type, handler) {
|
|
940
|
+
this.messageHandlers.set(type, handler);
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* 发送事件
|
|
944
|
+
*/
|
|
945
|
+
emitEvent(event) {
|
|
946
|
+
if (this.config.eventHandler) {
|
|
947
|
+
this.config.eventHandler(event);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* 更新 WebSocket 连接使用的 token
|
|
952
|
+
*/
|
|
953
|
+
updateToken(token) {
|
|
954
|
+
this.config.token = token;
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* 断开连接
|
|
958
|
+
*/
|
|
959
|
+
disconnect() {
|
|
960
|
+
this.logger.info("Disconnecting WebSocket from Lingyao server");
|
|
961
|
+
this.stopHeartbeat();
|
|
962
|
+
if (this.reconnectTimer) {
|
|
963
|
+
clearTimeout(this.reconnectTimer);
|
|
964
|
+
this.reconnectTimer = null;
|
|
965
|
+
}
|
|
966
|
+
if (this.ws) {
|
|
967
|
+
this.ws.close(1e3, "Client disconnect");
|
|
968
|
+
this.ws = null;
|
|
969
|
+
}
|
|
970
|
+
this.state = "disconnected";
|
|
971
|
+
this.connectionId = null;
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* 获取连接状态
|
|
975
|
+
*/
|
|
976
|
+
getState() {
|
|
977
|
+
return this.state;
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* 获取连接 ID
|
|
981
|
+
*/
|
|
982
|
+
getConnectionId() {
|
|
983
|
+
return this.connectionId;
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* 是否已连接
|
|
987
|
+
*/
|
|
988
|
+
isConnected() {
|
|
989
|
+
return this.state === "connected";
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* 生成消息 ID
|
|
993
|
+
*/
|
|
994
|
+
generateMessageId() {
|
|
995
|
+
return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* 生成连接 ID
|
|
999
|
+
*/
|
|
1000
|
+
generateConnectionId() {
|
|
1001
|
+
return `conn_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
// src/accounts.ts
|
|
1006
|
+
var STORAGE_KEY_ACCOUNTS = "lingyao:accounts";
|
|
1007
|
+
var STORAGE_KEY_PENDING_PAIRINGS = "lingyao:pending_pairings";
|
|
1008
|
+
var AccountManager = class {
|
|
1009
|
+
runtime;
|
|
1010
|
+
accounts = /* @__PURE__ */ new Map();
|
|
1011
|
+
pendingPairings = /* @__PURE__ */ new Map();
|
|
1012
|
+
constructor(runtime) {
|
|
1013
|
+
this.runtime = runtime;
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Initialize account manager from storage
|
|
1017
|
+
*/
|
|
1018
|
+
async initialize() {
|
|
1019
|
+
try {
|
|
1020
|
+
const stored = await this.runtime.storage.get(STORAGE_KEY_ACCOUNTS);
|
|
1021
|
+
if (stored && Array.isArray(stored)) {
|
|
1022
|
+
for (const account of stored) {
|
|
1023
|
+
this.accounts.set(account.deviceId, account);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const pending = await this.runtime.storage.get(STORAGE_KEY_PENDING_PAIRINGS);
|
|
1027
|
+
if (pending && Array.isArray(pending)) {
|
|
1028
|
+
const now = Date.now();
|
|
1029
|
+
for (const session of pending) {
|
|
1030
|
+
if (session.expiresAt > now) {
|
|
1031
|
+
this.pendingPairings.set(session.code, session);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
this.runtime.logger.info(
|
|
1036
|
+
`AccountManager initialized: ${this.accounts.size} accounts, ${this.pendingPairings.size} pending pairings`
|
|
1037
|
+
);
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
this.runtime.logger.error("Failed to initialize AccountManager", error);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Get account by device ID
|
|
1044
|
+
*/
|
|
1045
|
+
getAccount(deviceId) {
|
|
1046
|
+
return this.accounts.get(deviceId);
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Get account by device token
|
|
1050
|
+
*/
|
|
1051
|
+
getAccountByToken(token) {
|
|
1052
|
+
for (const account of this.accounts.values()) {
|
|
1053
|
+
if (account.deviceToken.token === token) {
|
|
1054
|
+
return account;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
return void 0;
|
|
1058
|
+
}
|
|
1059
|
+
/**
|
|
1060
|
+
* Get all active accounts
|
|
1061
|
+
*/
|
|
1062
|
+
getActiveAccounts() {
|
|
1063
|
+
return Array.from(this.accounts.values()).filter(
|
|
1064
|
+
(acc) => acc.status === "active"
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Create a new pairing session
|
|
1069
|
+
*/
|
|
1070
|
+
async createPairingSession(code, expiresAt) {
|
|
1071
|
+
const session = {
|
|
1072
|
+
code,
|
|
1073
|
+
createdAt: Date.now(),
|
|
1074
|
+
expiresAt
|
|
1075
|
+
};
|
|
1076
|
+
this.pendingPairings.set(code, session);
|
|
1077
|
+
await this.savePendingPairings();
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Get pairing session by code
|
|
1081
|
+
*/
|
|
1082
|
+
getPairingSession(code) {
|
|
1083
|
+
return this.pendingPairings.get(code);
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Remove a pending pairing (e.g. expired or cancelled)
|
|
1087
|
+
*/
|
|
1088
|
+
async deletePendingPairing(code) {
|
|
1089
|
+
if (!this.pendingPairings.delete(code)) {
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
await this.savePendingPairings();
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Confirm pairing and create account
|
|
1096
|
+
*/
|
|
1097
|
+
async confirmPairing(pairingCode, deviceToken, deviceInfo) {
|
|
1098
|
+
const session = this.pendingPairings.get(pairingCode);
|
|
1099
|
+
if (!session) {
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
const now = Date.now();
|
|
1103
|
+
if (session.expiresAt < now) {
|
|
1104
|
+
this.pendingPairings.delete(pairingCode);
|
|
1105
|
+
await this.savePendingPairings();
|
|
1106
|
+
return null;
|
|
1107
|
+
}
|
|
1108
|
+
const account = {
|
|
1109
|
+
deviceId: deviceToken.deviceId,
|
|
1110
|
+
deviceInfo,
|
|
1111
|
+
deviceToken,
|
|
1112
|
+
pairedAt: now,
|
|
1113
|
+
lastSeenAt: now,
|
|
1114
|
+
status: "active"
|
|
1115
|
+
};
|
|
1116
|
+
this.accounts.set(deviceToken.deviceId, account);
|
|
1117
|
+
this.pendingPairings.delete(pairingCode);
|
|
1118
|
+
await Promise.all([
|
|
1119
|
+
this.saveAccounts(),
|
|
1120
|
+
this.savePendingPairings()
|
|
1121
|
+
]);
|
|
1122
|
+
this.runtime.logger.info(
|
|
1123
|
+
`Account paired: ${deviceToken.deviceId} (${deviceInfo.name})`
|
|
1124
|
+
);
|
|
1125
|
+
return account;
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Update account's last seen timestamp
|
|
1129
|
+
*/
|
|
1130
|
+
async updateLastSeen(deviceId) {
|
|
1131
|
+
const account = this.accounts.get(deviceId);
|
|
1132
|
+
if (account) {
|
|
1133
|
+
account.lastSeenAt = Date.now();
|
|
1134
|
+
await this.saveAccounts();
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Revoke an account
|
|
1139
|
+
*/
|
|
1140
|
+
async revokeAccount(deviceId) {
|
|
1141
|
+
const account = this.accounts.get(deviceId);
|
|
1142
|
+
if (!account) {
|
|
1143
|
+
return false;
|
|
1144
|
+
}
|
|
1145
|
+
account.status = "revoked";
|
|
1146
|
+
await this.saveAccounts();
|
|
1147
|
+
this.runtime.logger.info(`Account revoked: ${deviceId}`);
|
|
1148
|
+
return true;
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Manually add a device by deviceId (user-initiated pairing).
|
|
1152
|
+
* No pairing code or deviceToken required — the user explicitly
|
|
1153
|
+
* trusts this device from the OpenClaw CLI.
|
|
1154
|
+
*/
|
|
1155
|
+
async addDevice(deviceId, deviceInfo) {
|
|
1156
|
+
const existing = this.accounts.get(deviceId);
|
|
1157
|
+
if (existing) {
|
|
1158
|
+
existing.status = "active";
|
|
1159
|
+
existing.deviceInfo = deviceInfo;
|
|
1160
|
+
existing.lastSeenAt = Date.now();
|
|
1161
|
+
await this.saveAccounts();
|
|
1162
|
+
this.runtime.logger.info(`Device re-activated: ${deviceId} (${deviceInfo.name})`);
|
|
1163
|
+
return existing;
|
|
1164
|
+
}
|
|
1165
|
+
const now = Date.now();
|
|
1166
|
+
const account = {
|
|
1167
|
+
deviceId,
|
|
1168
|
+
deviceInfo,
|
|
1169
|
+
deviceToken: {
|
|
1170
|
+
deviceId,
|
|
1171
|
+
pairingId: `manual_${now}`,
|
|
1172
|
+
token: "",
|
|
1173
|
+
secret: "",
|
|
1174
|
+
expiresAt: 0,
|
|
1175
|
+
deviceInfo
|
|
1176
|
+
},
|
|
1177
|
+
pairedAt: now,
|
|
1178
|
+
lastSeenAt: now,
|
|
1179
|
+
status: "active"
|
|
1180
|
+
};
|
|
1181
|
+
this.accounts.set(deviceId, account);
|
|
1182
|
+
await this.saveAccounts();
|
|
1183
|
+
this.runtime.logger.info(`Device added: ${deviceId} (${deviceInfo.name})`);
|
|
1184
|
+
return account;
|
|
1185
|
+
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Refresh device token
|
|
1188
|
+
*/
|
|
1189
|
+
async refreshDeviceToken(deviceId, newToken) {
|
|
1190
|
+
const account = this.accounts.get(deviceId);
|
|
1191
|
+
if (!account) {
|
|
1192
|
+
return false;
|
|
1193
|
+
}
|
|
1194
|
+
account.deviceToken = newToken;
|
|
1195
|
+
await this.saveAccounts();
|
|
1196
|
+
this.runtime.logger.info(`Token refreshed for: ${deviceId}`);
|
|
1197
|
+
return true;
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Clean up expired accounts
|
|
1201
|
+
*/
|
|
1202
|
+
async cleanupExpired() {
|
|
1203
|
+
const now = Date.now();
|
|
1204
|
+
const expired = [];
|
|
1205
|
+
for (const [deviceId, account] of this.accounts.entries()) {
|
|
1206
|
+
if (account.deviceToken.expiresAt < now) {
|
|
1207
|
+
expired.push(deviceId);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
for (const deviceId of expired) {
|
|
1211
|
+
this.accounts.delete(deviceId);
|
|
1212
|
+
}
|
|
1213
|
+
if (expired.length > 0) {
|
|
1214
|
+
await this.saveAccounts();
|
|
1215
|
+
this.runtime.logger.info(`Cleaned up ${expired.length} expired accounts`);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* Save accounts to storage
|
|
1220
|
+
*/
|
|
1221
|
+
async saveAccounts() {
|
|
1222
|
+
const accounts = Array.from(this.accounts.values());
|
|
1223
|
+
await this.runtime.storage.set(STORAGE_KEY_ACCOUNTS, accounts);
|
|
1224
|
+
}
|
|
1225
|
+
/**
|
|
1226
|
+
* Save pending pairings to storage
|
|
1227
|
+
*/
|
|
1228
|
+
async savePendingPairings() {
|
|
1229
|
+
const sessions = Array.from(this.pendingPairings.values());
|
|
1230
|
+
await this.runtime.storage.set(STORAGE_KEY_PENDING_PAIRINGS, sessions);
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
|
|
1234
|
+
// src/crypto.ts
|
|
1235
|
+
import { createHmac, createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
1236
|
+
var CryptoManager = class {
|
|
1237
|
+
/**
|
|
1238
|
+
* Sign a message with HMAC-SHA256
|
|
1239
|
+
*/
|
|
1240
|
+
static signMessage(message, secret) {
|
|
1241
|
+
const messageString = this.prepareMessageForSigning(message);
|
|
1242
|
+
return createHmac("sha256", secret).update(messageString).digest("base64");
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Verify a message signature
|
|
1246
|
+
*/
|
|
1247
|
+
static verifyMessage(message, signature, secret) {
|
|
1248
|
+
try {
|
|
1249
|
+
const expectedSignature = this.signMessage(message, secret);
|
|
1250
|
+
const expectedBuffer = Buffer.from(expectedSignature);
|
|
1251
|
+
const providedBuffer = Buffer.from(signature);
|
|
1252
|
+
if (expectedBuffer.length !== providedBuffer.length) {
|
|
1253
|
+
return false;
|
|
1254
|
+
}
|
|
1255
|
+
return timingSafeEqual(expectedBuffer, providedBuffer);
|
|
1256
|
+
} catch {
|
|
1257
|
+
return false;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Sign any data string
|
|
1262
|
+
*/
|
|
1263
|
+
static sign(data, secret) {
|
|
1264
|
+
return createHmac("sha256", secret).update(data).digest("base64");
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Verify data signature
|
|
1268
|
+
*/
|
|
1269
|
+
static verify(data, signature, secret) {
|
|
1270
|
+
try {
|
|
1271
|
+
const expectedSignature = this.sign(data, secret);
|
|
1272
|
+
const expectedBuffer = Buffer.from(expectedSignature);
|
|
1273
|
+
const providedBuffer = Buffer.from(signature);
|
|
1274
|
+
if (expectedBuffer.length !== providedBuffer.length) {
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
return timingSafeEqual(expectedBuffer, providedBuffer);
|
|
1278
|
+
} catch {
|
|
1279
|
+
return false;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Generate a random nonce
|
|
1284
|
+
*/
|
|
1285
|
+
static generateNonce() {
|
|
1286
|
+
return randomBytes(16).toString("hex");
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Hash data using SHA-256
|
|
1290
|
+
*/
|
|
1291
|
+
static hash(data) {
|
|
1292
|
+
return createHash("sha256").update(data).digest("hex");
|
|
1293
|
+
}
|
|
1294
|
+
/**
|
|
1295
|
+
* Generate a cryptographically random ID
|
|
1296
|
+
*/
|
|
1297
|
+
static generateId() {
|
|
1298
|
+
return randomBytes(16).toString("hex");
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Prepare message for signing by serializing in a deterministic way
|
|
1302
|
+
*/
|
|
1303
|
+
static prepareMessageForSigning(message) {
|
|
1304
|
+
const parts = [
|
|
1305
|
+
message.id,
|
|
1306
|
+
message.type,
|
|
1307
|
+
message.timestamp.toString(),
|
|
1308
|
+
message.from,
|
|
1309
|
+
message.to,
|
|
1310
|
+
JSON.stringify(message.payload)
|
|
1311
|
+
];
|
|
1312
|
+
return parts.join("|");
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Encrypt data (for future use)
|
|
1316
|
+
*/
|
|
1317
|
+
static encrypt(data, _key) {
|
|
1318
|
+
const iv = randomBytes(16);
|
|
1319
|
+
return {
|
|
1320
|
+
encrypted: data,
|
|
1321
|
+
// Placeholder
|
|
1322
|
+
iv: iv.toString("hex")
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Decrypt data (for future use)
|
|
1327
|
+
*/
|
|
1328
|
+
static decrypt(encrypted, _key, _iv) {
|
|
1329
|
+
return encrypted;
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Derive a key from a password and salt
|
|
1333
|
+
*/
|
|
1334
|
+
static deriveKey(password, salt) {
|
|
1335
|
+
return createHmac("sha256", salt).update(password).digest("hex");
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Generate a fingerprint for a device
|
|
1339
|
+
*/
|
|
1340
|
+
static generateDeviceFingerprint(deviceInfo) {
|
|
1341
|
+
const data = JSON.stringify(deviceInfo, Object.keys(deviceInfo).sort());
|
|
1342
|
+
return this.hash(data).substring(0, 16);
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Verify message timestamp is within acceptable range
|
|
1346
|
+
*/
|
|
1347
|
+
static verifyTimestamp(timestamp, maxDriftSeconds = 300) {
|
|
1348
|
+
const now = Date.now();
|
|
1349
|
+
const drift = Math.abs(now - timestamp);
|
|
1350
|
+
return drift <= maxDriftSeconds * 1e3;
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Check if a message is a replay attack
|
|
1354
|
+
*/
|
|
1355
|
+
static checkReplay(messageId, seenMessages, _maxAge = 36e5) {
|
|
1356
|
+
return seenMessages.has(messageId);
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Mark a message as seen to prevent replay
|
|
1360
|
+
*/
|
|
1361
|
+
static markMessageSeen(messageId, seenMessages, _maxAge = 36e5) {
|
|
1362
|
+
seenMessages.add(messageId);
|
|
1363
|
+
if (seenMessages.size > 1e4) {
|
|
1364
|
+
const entries = Array.from(seenMessages);
|
|
1365
|
+
for (let i = 0; i < entries.length / 2; i++) {
|
|
1366
|
+
seenMessages.delete(entries[i]);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
|
|
1372
|
+
// src/bot.ts
|
|
1373
|
+
var MessageProcessor = class {
|
|
1374
|
+
runtime;
|
|
1375
|
+
accountManager;
|
|
1376
|
+
messageQueue = /* @__PURE__ */ new Map();
|
|
1377
|
+
seenMessages = /* @__PURE__ */ new Set();
|
|
1378
|
+
messageHandler = null;
|
|
1379
|
+
maxQueueSize = 100;
|
|
1380
|
+
constructor(runtime, accountManager) {
|
|
1381
|
+
this.runtime = runtime;
|
|
1382
|
+
this.accountManager = accountManager;
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Initialize the message processor
|
|
1386
|
+
*/
|
|
1387
|
+
async initialize() {
|
|
1388
|
+
this.runtime.logger.info("Message Processor initialized");
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Set the message handler for delivering messages to Agent
|
|
1392
|
+
*/
|
|
1393
|
+
setMessageHandler(handler) {
|
|
1394
|
+
this.messageHandler = handler;
|
|
1395
|
+
}
|
|
1396
|
+
/**
|
|
1397
|
+
* Get the currently registered Agent message handler.
|
|
1398
|
+
*/
|
|
1399
|
+
getMessageHandler() {
|
|
1400
|
+
return this.messageHandler;
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Deliver a normalized message directly to the registered Agent handler.
|
|
1404
|
+
*/
|
|
1405
|
+
async deliverToAgent(message) {
|
|
1406
|
+
if (!this.messageHandler) {
|
|
1407
|
+
this.runtime.logger.warn("No message handler set, message not delivered to Agent");
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
await this.messageHandler(message);
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Process sync request from app
|
|
1414
|
+
*/
|
|
1415
|
+
async processSync(deviceId, request) {
|
|
1416
|
+
const account = this.accountManager.getAccount(deviceId);
|
|
1417
|
+
if (!account) {
|
|
1418
|
+
return {
|
|
1419
|
+
processed: [],
|
|
1420
|
+
failed: request.messages.map((m) => ({
|
|
1421
|
+
id: m.id,
|
|
1422
|
+
error: "Device not found"
|
|
1423
|
+
}))
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
const processed = [];
|
|
1427
|
+
const failed = [];
|
|
1428
|
+
for (const message of request.messages) {
|
|
1429
|
+
try {
|
|
1430
|
+
if (message.signature) {
|
|
1431
|
+
const isValid = CryptoManager.verifyMessage(
|
|
1432
|
+
message,
|
|
1433
|
+
message.signature,
|
|
1434
|
+
account.deviceToken.secret
|
|
1435
|
+
);
|
|
1436
|
+
if (!isValid) {
|
|
1437
|
+
failed.push({ id: message.id, error: "Invalid signature" });
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
if (this.isMessageSeen(message.id)) {
|
|
1442
|
+
this.runtime.logger.debug(`Duplicate message ignored: ${message.id}`);
|
|
1443
|
+
processed.push(message.id);
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1446
|
+
if (!CryptoManager.verifyTimestamp(message.timestamp)) {
|
|
1447
|
+
failed.push({ id: message.id, error: "Invalid timestamp" });
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
const result = await this.processMessage(deviceId, message);
|
|
1451
|
+
if (result.success) {
|
|
1452
|
+
processed.push(message.id);
|
|
1453
|
+
this.markMessageSeen(message.id);
|
|
1454
|
+
} else {
|
|
1455
|
+
failed.push({ id: message.id, error: result.error ?? "Processing failed" });
|
|
1456
|
+
}
|
|
1457
|
+
} catch (error) {
|
|
1458
|
+
this.runtime.logger.error(`Error processing message ${message.id}`, error);
|
|
1459
|
+
failed.push({ id: message.id, error: "Internal error" });
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
return { processed, failed };
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Process a single message
|
|
1466
|
+
*/
|
|
1467
|
+
async processMessage(deviceId, message) {
|
|
1468
|
+
this.runtime.logger.debug(
|
|
1469
|
+
`Processing message ${message.type} from ${deviceId}`
|
|
1470
|
+
);
|
|
1471
|
+
switch (message.type) {
|
|
1472
|
+
case "sync_diary" /* SYNC_DIARY */:
|
|
1473
|
+
return await this.processDiarySync(deviceId, message);
|
|
1474
|
+
case "sync_memory" /* SYNC_MEMORY */:
|
|
1475
|
+
return await this.processMemorySync(deviceId, message);
|
|
1476
|
+
case "sync_ack" /* SYNC_ACK */:
|
|
1477
|
+
return await this.processSyncAck(deviceId, message);
|
|
1478
|
+
case "heartbeat" /* HEARTBEAT */:
|
|
1479
|
+
return await this.processHeartbeat(deviceId, message);
|
|
1480
|
+
default:
|
|
1481
|
+
return { success: false, error: "Unknown message type" };
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Process diary sync - Pass directly to Agent
|
|
1486
|
+
*/
|
|
1487
|
+
async processDiarySync(deviceId, message) {
|
|
1488
|
+
try {
|
|
1489
|
+
const payload = message.payload;
|
|
1490
|
+
const agentMessage = {
|
|
1491
|
+
id: message.id,
|
|
1492
|
+
type: "diary",
|
|
1493
|
+
from: `lingyao:${deviceId}`,
|
|
1494
|
+
deviceId,
|
|
1495
|
+
content: payload.content,
|
|
1496
|
+
metadata: {
|
|
1497
|
+
diaryId: payload.diaryId,
|
|
1498
|
+
title: payload.title,
|
|
1499
|
+
emotion: payload.emotion,
|
|
1500
|
+
tags: payload.tags,
|
|
1501
|
+
mediaUrls: payload.mediaUrls,
|
|
1502
|
+
createdAt: payload.createdAt,
|
|
1503
|
+
updatedAt: payload.updatedAt
|
|
1504
|
+
},
|
|
1505
|
+
timestamp: message.timestamp
|
|
1506
|
+
};
|
|
1507
|
+
await this.deliverToAgent(agentMessage);
|
|
1508
|
+
this.runtime.logger.info(
|
|
1509
|
+
`Diary message passed to Agent: ${payload.diaryId} from ${deviceId}`
|
|
1510
|
+
);
|
|
1511
|
+
return { success: true };
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
this.runtime.logger.error("Diary sync error", error);
|
|
1514
|
+
return { success: false, error: "Failed to sync diary" };
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* Process memory sync - Pass directly to Agent
|
|
1519
|
+
*/
|
|
1520
|
+
async processMemorySync(deviceId, message) {
|
|
1521
|
+
try {
|
|
1522
|
+
const payload = message.payload;
|
|
1523
|
+
const agentMessage = {
|
|
1524
|
+
id: message.id,
|
|
1525
|
+
type: "memory",
|
|
1526
|
+
from: `lingyao:${deviceId}`,
|
|
1527
|
+
deviceId,
|
|
1528
|
+
content: payload.content,
|
|
1529
|
+
metadata: {
|
|
1530
|
+
memoryId: payload.memoryId,
|
|
1531
|
+
memoryType: payload.type,
|
|
1532
|
+
importance: payload.importance,
|
|
1533
|
+
...payload.metadata,
|
|
1534
|
+
timestamp: payload.timestamp
|
|
1535
|
+
},
|
|
1536
|
+
timestamp: message.timestamp
|
|
1537
|
+
};
|
|
1538
|
+
await this.deliverToAgent(agentMessage);
|
|
1539
|
+
this.runtime.logger.info(
|
|
1540
|
+
`Memory message passed to Agent: ${payload.memoryId} from ${deviceId}`
|
|
1541
|
+
);
|
|
1542
|
+
return { success: true };
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
this.runtime.logger.error("Memory sync error", error);
|
|
1545
|
+
return { success: false, error: "Failed to sync memory" };
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
/**
|
|
1549
|
+
* Process sync acknowledgment
|
|
1550
|
+
*/
|
|
1551
|
+
async processSyncAck(_deviceId, message) {
|
|
1552
|
+
try {
|
|
1553
|
+
this.runtime.logger.debug(`Sync ack received: ${message.id}`);
|
|
1554
|
+
return { success: true };
|
|
1555
|
+
} catch (error) {
|
|
1556
|
+
this.runtime.logger.error("Sync ack error", error);
|
|
1557
|
+
return { success: false, error: "Failed to process ack" };
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Process heartbeat
|
|
1562
|
+
*/
|
|
1563
|
+
async processHeartbeat(deviceId, _message) {
|
|
1564
|
+
try {
|
|
1565
|
+
await this.accountManager.updateLastSeen(deviceId);
|
|
1566
|
+
return { success: true };
|
|
1567
|
+
} catch (error) {
|
|
1568
|
+
this.runtime.logger.error("Heartbeat error", error);
|
|
1569
|
+
return { success: false, error: "Failed to process heartbeat" };
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* Poll for new messages (long-polling)
|
|
1574
|
+
*/
|
|
1575
|
+
async pollMessages(deviceId, timeout = 3e4) {
|
|
1576
|
+
return new Promise((resolve) => {
|
|
1577
|
+
const startTime = Date.now();
|
|
1578
|
+
const checkInterval = 100;
|
|
1579
|
+
const checkForMessages = () => {
|
|
1580
|
+
const queue = this.messageQueue.get(deviceId);
|
|
1581
|
+
const messages = queue?.filter((m) => !m.delivered) ?? [];
|
|
1582
|
+
if (messages.length > 0) {
|
|
1583
|
+
for (const msg of messages) {
|
|
1584
|
+
msg.delivered = true;
|
|
1585
|
+
}
|
|
1586
|
+
resolve(messages.map((m) => m.message));
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
const elapsed = Date.now() - startTime;
|
|
1590
|
+
if (elapsed >= timeout) {
|
|
1591
|
+
resolve([]);
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
setTimeout(checkForMessages, checkInterval);
|
|
1595
|
+
};
|
|
1596
|
+
checkForMessages();
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Queue a message for a device
|
|
1601
|
+
*/
|
|
1602
|
+
queueMessage(deviceId, message) {
|
|
1603
|
+
let queue = this.messageQueue.get(deviceId);
|
|
1604
|
+
if (!queue) {
|
|
1605
|
+
queue = [];
|
|
1606
|
+
this.messageQueue.set(deviceId, queue);
|
|
1607
|
+
}
|
|
1608
|
+
if (queue.length >= this.maxQueueSize) {
|
|
1609
|
+
const oldestIndex = queue.findIndex((m) => !m.delivered);
|
|
1610
|
+
if (oldestIndex >= 0) {
|
|
1611
|
+
queue.splice(oldestIndex, 1);
|
|
1612
|
+
} else {
|
|
1613
|
+
this.runtime.logger.warn(
|
|
1614
|
+
`Message queue full for ${deviceId}, dropping message`
|
|
1615
|
+
);
|
|
1616
|
+
return false;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
queue.push({
|
|
1620
|
+
message,
|
|
1621
|
+
deviceId,
|
|
1622
|
+
createdAt: Date.now(),
|
|
1623
|
+
delivered: false
|
|
1624
|
+
});
|
|
1625
|
+
this.runtime.logger.debug(
|
|
1626
|
+
`Message queued for ${deviceId}: ${message.id}`
|
|
1627
|
+
);
|
|
1628
|
+
return true;
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Acknowledge messages
|
|
1632
|
+
*/
|
|
1633
|
+
async acknowledgeMessages(messageIds) {
|
|
1634
|
+
for (const deviceId of this.messageQueue.keys()) {
|
|
1635
|
+
const queue = this.messageQueue.get(deviceId);
|
|
1636
|
+
if (!queue) continue;
|
|
1637
|
+
for (const msgId of messageIds) {
|
|
1638
|
+
const index = queue.findIndex((m) => m.message.id === msgId);
|
|
1639
|
+
if (index >= 0) {
|
|
1640
|
+
queue.splice(index, 1);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
if (queue.length === 0) {
|
|
1644
|
+
this.messageQueue.delete(deviceId);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
this.runtime.logger.debug(`Acknowledged ${messageIds.length} messages`);
|
|
1648
|
+
}
|
|
1649
|
+
/**
|
|
1650
|
+
* Check if message has been seen (deduplication)
|
|
1651
|
+
*/
|
|
1652
|
+
isMessageSeen(messageId) {
|
|
1653
|
+
return this.seenMessages.has(messageId);
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* Mark message as seen
|
|
1657
|
+
*/
|
|
1658
|
+
markMessageSeen(messageId) {
|
|
1659
|
+
this.seenMessages.add(messageId);
|
|
1660
|
+
if (this.seenMessages.size > 1e4) {
|
|
1661
|
+
let count = 0;
|
|
1662
|
+
const targetDeletes = Math.floor(this.seenMessages.size / 2);
|
|
1663
|
+
for (const id of this.seenMessages) {
|
|
1664
|
+
this.seenMessages.delete(id);
|
|
1665
|
+
count++;
|
|
1666
|
+
if (count >= targetDeletes) break;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Get queue size for a device
|
|
1672
|
+
*/
|
|
1673
|
+
getQueueSize(deviceId) {
|
|
1674
|
+
const queue = this.messageQueue.get(deviceId);
|
|
1675
|
+
return queue?.filter((m) => !m.delivered).length ?? 0;
|
|
1676
|
+
}
|
|
1677
|
+
/**
|
|
1678
|
+
* Get total queue size
|
|
1679
|
+
*/
|
|
1680
|
+
getTotalQueueSize() {
|
|
1681
|
+
let total = 0;
|
|
1682
|
+
for (const queue of this.messageQueue.values()) {
|
|
1683
|
+
total += queue.filter((m) => !m.delivered).length;
|
|
1684
|
+
}
|
|
1685
|
+
return total;
|
|
1686
|
+
}
|
|
1687
|
+
};
|
|
1688
|
+
|
|
1689
|
+
// src/probe.ts
|
|
1690
|
+
var Probe = class {
|
|
1691
|
+
runtime;
|
|
1692
|
+
startTime = Date.now();
|
|
1693
|
+
lastError = null;
|
|
1694
|
+
lastErrorTime = null;
|
|
1695
|
+
errorCounts = /* @__PURE__ */ new Map();
|
|
1696
|
+
healthChecks = /* @__PURE__ */ new Map();
|
|
1697
|
+
constructor(runtime) {
|
|
1698
|
+
this.runtime = runtime;
|
|
1699
|
+
this.registerHealthCheck("uptime", this.checkUptime);
|
|
1700
|
+
this.registerHealthCheck("errors", this.checkErrors);
|
|
1701
|
+
}
|
|
1702
|
+
/**
|
|
1703
|
+
* Register a custom health check
|
|
1704
|
+
*/
|
|
1705
|
+
registerHealthCheck(name, check) {
|
|
1706
|
+
this.healthChecks.set(name, check);
|
|
1707
|
+
}
|
|
1708
|
+
/**
|
|
1709
|
+
* Run all health checks
|
|
1710
|
+
*/
|
|
1711
|
+
async runHealthChecks() {
|
|
1712
|
+
const checks = /* @__PURE__ */ new Map();
|
|
1713
|
+
let overallStatus = "healthy" /* HEALTHY */;
|
|
1714
|
+
for (const [name, checkFn] of this.healthChecks.entries()) {
|
|
1715
|
+
const start = Date.now();
|
|
1716
|
+
try {
|
|
1717
|
+
const result = await checkFn();
|
|
1718
|
+
const duration = Date.now() - start;
|
|
1719
|
+
checks.set(name, {
|
|
1720
|
+
passed: result.passed,
|
|
1721
|
+
message: result.message,
|
|
1722
|
+
duration
|
|
1723
|
+
});
|
|
1724
|
+
if (!result.passed && overallStatus === "healthy" /* HEALTHY */) {
|
|
1725
|
+
overallStatus = "degraded" /* DEGRADED */;
|
|
1726
|
+
}
|
|
1727
|
+
} catch (error) {
|
|
1728
|
+
const duration = Date.now() - start;
|
|
1729
|
+
checks.set(name, {
|
|
1730
|
+
passed: false,
|
|
1731
|
+
message: "Health check failed with exception",
|
|
1732
|
+
duration,
|
|
1733
|
+
error
|
|
1734
|
+
});
|
|
1735
|
+
overallStatus = "unhealthy" /* UNHEALTHY */;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
return {
|
|
1739
|
+
status: overallStatus,
|
|
1740
|
+
checks,
|
|
1741
|
+
timestamp: Date.now()
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Get channel status for reporting
|
|
1746
|
+
*/
|
|
1747
|
+
getChannelStatus(configured, running, activeAccounts = 0) {
|
|
1748
|
+
return {
|
|
1749
|
+
configured,
|
|
1750
|
+
running,
|
|
1751
|
+
lastError: this.lastError ?? void 0,
|
|
1752
|
+
activeAccounts,
|
|
1753
|
+
uptime: Date.now() - this.startTime,
|
|
1754
|
+
status: running ? "healthy" /* HEALTHY */ : "unhealthy" /* UNHEALTHY */
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Get health status for API endpoint
|
|
1759
|
+
*/
|
|
1760
|
+
async getHealthStatus(activeConnections = 0, queuedMessages = 0) {
|
|
1761
|
+
const healthResult = await this.runHealthChecks();
|
|
1762
|
+
let status;
|
|
1763
|
+
switch (healthResult.status) {
|
|
1764
|
+
case "healthy" /* HEALTHY */:
|
|
1765
|
+
status = "healthy";
|
|
1766
|
+
break;
|
|
1767
|
+
case "degraded" /* DEGRADED */:
|
|
1768
|
+
status = "degraded";
|
|
1769
|
+
break;
|
|
1770
|
+
case "unhealthy" /* UNHEALTHY */:
|
|
1771
|
+
status = "unhealthy";
|
|
1772
|
+
break;
|
|
1773
|
+
}
|
|
1774
|
+
return {
|
|
1775
|
+
status,
|
|
1776
|
+
uptime: Date.now() - this.startTime,
|
|
1777
|
+
activeConnections,
|
|
1778
|
+
queuedMessages,
|
|
1779
|
+
lastError: this.lastError ?? void 0
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Record an error
|
|
1784
|
+
*/
|
|
1785
|
+
recordError(error, category = "general") {
|
|
1786
|
+
this.lastError = error;
|
|
1787
|
+
this.lastErrorTime = Date.now();
|
|
1788
|
+
const count = this.errorCounts.get(category) ?? 0;
|
|
1789
|
+
this.errorCounts.set(category, count + 1);
|
|
1790
|
+
this.runtime.logger.error(`[${category}] ${error}`);
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Get last error
|
|
1794
|
+
*/
|
|
1795
|
+
getLastError() {
|
|
1796
|
+
if (this.lastErrorTime && Date.now() - this.lastErrorTime < 3e5) {
|
|
1797
|
+
return this.lastError;
|
|
1798
|
+
}
|
|
1799
|
+
return null;
|
|
1800
|
+
}
|
|
1801
|
+
/**
|
|
1802
|
+
* Clear last error
|
|
1803
|
+
*/
|
|
1804
|
+
clearLastError() {
|
|
1805
|
+
this.lastError = null;
|
|
1806
|
+
this.lastErrorTime = null;
|
|
1807
|
+
}
|
|
1808
|
+
/**
|
|
1809
|
+
* Get error counts by category
|
|
1810
|
+
*/
|
|
1811
|
+
getErrorCounts() {
|
|
1812
|
+
return Object.fromEntries(this.errorCounts);
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Reset error counts
|
|
1816
|
+
*/
|
|
1817
|
+
resetErrorCounts() {
|
|
1818
|
+
this.errorCounts.clear();
|
|
1819
|
+
}
|
|
1820
|
+
/**
|
|
1821
|
+
* Get uptime in milliseconds
|
|
1822
|
+
*/
|
|
1823
|
+
getUptime() {
|
|
1824
|
+
return Date.now() - this.startTime;
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Get uptime formatted as human-readable string
|
|
1828
|
+
*/
|
|
1829
|
+
getUptimeString() {
|
|
1830
|
+
const uptime = this.getUptime();
|
|
1831
|
+
const seconds = Math.floor(uptime / 1e3);
|
|
1832
|
+
const minutes = Math.floor(seconds / 60);
|
|
1833
|
+
const hours = Math.floor(minutes / 60);
|
|
1834
|
+
const days = Math.floor(hours / 24);
|
|
1835
|
+
if (days > 0) {
|
|
1836
|
+
return `${days}d ${hours % 24}h ${minutes % 60}m`;
|
|
1837
|
+
} else if (hours > 0) {
|
|
1838
|
+
return `${hours}h ${minutes % 60}m`;
|
|
1839
|
+
} else if (minutes > 0) {
|
|
1840
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
1841
|
+
} else {
|
|
1842
|
+
return `${seconds}s`;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
/**
|
|
1846
|
+
* Health check: uptime
|
|
1847
|
+
*/
|
|
1848
|
+
checkUptime = async () => {
|
|
1849
|
+
const uptime = this.getUptime();
|
|
1850
|
+
return {
|
|
1851
|
+
passed: uptime > 0,
|
|
1852
|
+
message: `Uptime: ${this.getUptimeString()}`,
|
|
1853
|
+
duration: 0
|
|
1854
|
+
};
|
|
1855
|
+
};
|
|
1856
|
+
/**
|
|
1857
|
+
* Health check: errors
|
|
1858
|
+
*/
|
|
1859
|
+
checkErrors = async () => {
|
|
1860
|
+
const recentErrors = this.lastErrorTime && Date.now() - this.lastErrorTime < 6e4;
|
|
1861
|
+
return {
|
|
1862
|
+
passed: !recentErrors,
|
|
1863
|
+
message: recentErrors ? `Recent error: ${this.lastError}` : "No recent errors",
|
|
1864
|
+
duration: 0
|
|
1865
|
+
};
|
|
1866
|
+
};
|
|
1867
|
+
/**
|
|
1868
|
+
* Create status adapter for OpenClaw integration
|
|
1869
|
+
*/
|
|
1870
|
+
createStatusAdapter(configured) {
|
|
1871
|
+
return {
|
|
1872
|
+
getStatus: async (running, activeAccounts = 0, activeConnections = 0, queuedMessages = 0) => ({
|
|
1873
|
+
...this.getChannelStatus(configured, running, activeAccounts),
|
|
1874
|
+
activeConnections,
|
|
1875
|
+
queuedMessages
|
|
1876
|
+
})
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
};
|
|
1880
|
+
|
|
1881
|
+
// src/metrics.ts
|
|
1882
|
+
var MetricsManager = class {
|
|
1883
|
+
metrics = /* @__PURE__ */ new Map();
|
|
1884
|
+
counters = /* @__PURE__ */ new Map();
|
|
1885
|
+
gauges = /* @__PURE__ */ new Map();
|
|
1886
|
+
histograms = /* @__PURE__ */ new Map();
|
|
1887
|
+
histogramBuckets = /* @__PURE__ */ new Map();
|
|
1888
|
+
constructor(_runtime) {
|
|
1889
|
+
this.initializeDefaultMetrics();
|
|
1890
|
+
}
|
|
1891
|
+
/**
|
|
1892
|
+
* 初始化默认指标
|
|
1893
|
+
*/
|
|
1894
|
+
initializeDefaultMetrics() {
|
|
1895
|
+
this.declareMetric({
|
|
1896
|
+
name: "lingyao_connection_count",
|
|
1897
|
+
type: "gauge" /* GAUGE */,
|
|
1898
|
+
description: "\u5F53\u524D\u6D3B\u8DC3\u8FDE\u63A5\u6570"
|
|
1899
|
+
});
|
|
1900
|
+
this.declareMetric({
|
|
1901
|
+
name: "lingyao_connection_total",
|
|
1902
|
+
type: "counter" /* COUNTER */,
|
|
1903
|
+
description: "\u603B\u8FDE\u63A5\u6B21\u6570"
|
|
1904
|
+
});
|
|
1905
|
+
this.declareMetric({
|
|
1906
|
+
name: "lingyao_disconnection_total",
|
|
1907
|
+
type: "counter" /* COUNTER */,
|
|
1908
|
+
description: "\u603B\u65AD\u5F00\u8FDE\u63A5\u6B21\u6570"
|
|
1909
|
+
});
|
|
1910
|
+
this.declareMetric({
|
|
1911
|
+
name: "lingyao_message_total",
|
|
1912
|
+
type: "counter" /* COUNTER */,
|
|
1913
|
+
description: "\u603B\u6D88\u606F\u6570",
|
|
1914
|
+
labels: ["direction", "type"]
|
|
1915
|
+
});
|
|
1916
|
+
this.declareMetric({
|
|
1917
|
+
name: "lingyao_message_failed_total",
|
|
1918
|
+
type: "counter" /* COUNTER */,
|
|
1919
|
+
description: "\u5931\u8D25\u6D88\u606F\u603B\u6570",
|
|
1920
|
+
labels: ["direction", "reason"]
|
|
1921
|
+
});
|
|
1922
|
+
this.declareMetric({
|
|
1923
|
+
name: "lingyao_message_latency",
|
|
1924
|
+
type: "histogram" /* HISTOGRAM */,
|
|
1925
|
+
description: "\u6D88\u606F\u5904\u7406\u5EF6\u8FDF",
|
|
1926
|
+
buckets: [1, 5, 10, 25, 50, 100, 250, 500, 1e3, 2500, 5e3, 1e4]
|
|
1927
|
+
});
|
|
1928
|
+
this.declareMetric({
|
|
1929
|
+
name: "lingyao_error_total",
|
|
1930
|
+
type: "counter" /* COUNTER */,
|
|
1931
|
+
description: "\u9519\u8BEF\u603B\u6570",
|
|
1932
|
+
labels: ["type", "severity"]
|
|
1933
|
+
});
|
|
1934
|
+
this.declareMetric({
|
|
1935
|
+
name: "lingyao_heartbeat_total",
|
|
1936
|
+
type: "counter" /* COUNTER */,
|
|
1937
|
+
description: "\u5FC3\u8DF3\u603B\u6570"
|
|
1938
|
+
});
|
|
1939
|
+
this.declareMetric({
|
|
1940
|
+
name: "lingyao_heartbeat_missed_total",
|
|
1941
|
+
type: "counter" /* COUNTER */,
|
|
1942
|
+
description: "\u5FC3\u8DF3\u4E22\u5931\u603B\u6570"
|
|
1943
|
+
});
|
|
1944
|
+
this.declareMetric({
|
|
1945
|
+
name: "lingyao_reconnect_total",
|
|
1946
|
+
type: "counter" /* COUNTER */,
|
|
1947
|
+
description: "\u91CD\u8FDE\u6B21\u6570"
|
|
1948
|
+
});
|
|
1949
|
+
this.declareMetric({
|
|
1950
|
+
name: "lingyao_queue_size",
|
|
1951
|
+
type: "gauge" /* GAUGE */,
|
|
1952
|
+
description: "\u5F53\u524D\u961F\u5217\u5927\u5C0F"
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
/**
|
|
1956
|
+
* 声明指标
|
|
1957
|
+
*/
|
|
1958
|
+
declareMetric(config) {
|
|
1959
|
+
if (config.type === "histogram" /* HISTOGRAM */ && config.buckets) {
|
|
1960
|
+
this.histogramBuckets.set(config.name, config.buckets);
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
/**
|
|
1964
|
+
* 计数器增加
|
|
1965
|
+
*/
|
|
1966
|
+
incrementCounter(name, value = 1, labels) {
|
|
1967
|
+
const current = this.counters.get(name) || 0;
|
|
1968
|
+
this.counters.set(name, current + value);
|
|
1969
|
+
this.recordMetric({
|
|
1970
|
+
name,
|
|
1971
|
+
type: "counter" /* COUNTER */,
|
|
1972
|
+
value: current + value,
|
|
1973
|
+
timestamp: Date.now(),
|
|
1974
|
+
labels
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
/**
|
|
1978
|
+
* 设置仪表盘值
|
|
1979
|
+
*/
|
|
1980
|
+
setGauge(name, value, labels) {
|
|
1981
|
+
this.gauges.set(name, value);
|
|
1982
|
+
this.recordMetric({
|
|
1983
|
+
name,
|
|
1984
|
+
type: "gauge" /* GAUGE */,
|
|
1985
|
+
value,
|
|
1986
|
+
timestamp: Date.now(),
|
|
1987
|
+
labels
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
/**
|
|
1991
|
+
* 记录直方图值
|
|
1992
|
+
*/
|
|
1993
|
+
recordHistogram(name, value, labels) {
|
|
1994
|
+
let values = this.histograms.get(name);
|
|
1995
|
+
if (!values) {
|
|
1996
|
+
values = [];
|
|
1997
|
+
this.histograms.set(name, values);
|
|
1998
|
+
}
|
|
1999
|
+
values.push(value);
|
|
2000
|
+
this.recordMetric({
|
|
2001
|
+
name,
|
|
2002
|
+
type: "histogram" /* HISTOGRAM */,
|
|
2003
|
+
value,
|
|
2004
|
+
timestamp: Date.now(),
|
|
2005
|
+
labels
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
/**
|
|
2009
|
+
* 记录指标
|
|
2010
|
+
*/
|
|
2011
|
+
recordMetric(data) {
|
|
2012
|
+
const key = `${data.name}_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
2013
|
+
this.metrics.set(key, data);
|
|
2014
|
+
if (this.metrics.size > 1e4) {
|
|
2015
|
+
let count = 0;
|
|
2016
|
+
const deleteCount = Math.floor(this.metrics.size * 0.1);
|
|
2017
|
+
for (const k of this.metrics.keys()) {
|
|
2018
|
+
this.metrics.delete(k);
|
|
2019
|
+
if (++count >= deleteCount) break;
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
/**
|
|
2024
|
+
* 获取所有指标
|
|
2025
|
+
*/
|
|
2026
|
+
getMetrics() {
|
|
2027
|
+
return Array.from(this.metrics.values());
|
|
2028
|
+
}
|
|
2029
|
+
/**
|
|
2030
|
+
* 获取特定指标的当前值
|
|
2031
|
+
*/
|
|
2032
|
+
getMetricValue(name) {
|
|
2033
|
+
if (this.counters.has(name)) {
|
|
2034
|
+
return this.counters.get(name);
|
|
2035
|
+
}
|
|
2036
|
+
if (this.gauges.has(name)) {
|
|
2037
|
+
return this.gauges.get(name);
|
|
2038
|
+
}
|
|
2039
|
+
return void 0;
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* 获取直方图统计数据
|
|
2043
|
+
*/
|
|
2044
|
+
getHistogramStats(name) {
|
|
2045
|
+
const values = this.histograms.get(name);
|
|
2046
|
+
if (!values || values.length === 0) {
|
|
2047
|
+
return null;
|
|
2048
|
+
}
|
|
2049
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
2050
|
+
const count = sorted.length;
|
|
2051
|
+
const sum = sorted.reduce((a, b) => a + b, 0);
|
|
2052
|
+
return {
|
|
2053
|
+
count,
|
|
2054
|
+
min: sorted[0],
|
|
2055
|
+
max: sorted[count - 1],
|
|
2056
|
+
avg: sum / count,
|
|
2057
|
+
p50: sorted[Math.floor(count * 0.5)],
|
|
2058
|
+
p95: sorted[Math.floor(count * 0.95)],
|
|
2059
|
+
p99: sorted[Math.floor(count * 0.99)]
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
/**
|
|
2063
|
+
* 重置所有指标
|
|
2064
|
+
*/
|
|
2065
|
+
reset() {
|
|
2066
|
+
this.metrics.clear();
|
|
2067
|
+
this.counters.clear();
|
|
2068
|
+
this.gauges.clear();
|
|
2069
|
+
this.histograms.clear();
|
|
2070
|
+
}
|
|
2071
|
+
/**
|
|
2072
|
+
* 生成 Prometheus 格式的指标导出
|
|
2073
|
+
*/
|
|
2074
|
+
exportPrometheus() {
|
|
2075
|
+
const lines = [];
|
|
2076
|
+
for (const [name, value] of [...this.counters, ...this.gauges]) {
|
|
2077
|
+
lines.push(`${name} ${value}`);
|
|
2078
|
+
}
|
|
2079
|
+
for (const [name] of this.histograms) {
|
|
2080
|
+
const stats = this.getHistogramStats(name);
|
|
2081
|
+
if (stats) {
|
|
2082
|
+
lines.push(`${name}_count ${stats.count}`);
|
|
2083
|
+
lines.push(`${name}_sum ${stats.avg * stats.count}`);
|
|
2084
|
+
lines.push(`${name}_bucket{le="+Inf"} ${stats.count}`);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
return lines.join("\n");
|
|
2088
|
+
}
|
|
2089
|
+
};
|
|
2090
|
+
var Monitor = class {
|
|
2091
|
+
runtime;
|
|
2092
|
+
metrics;
|
|
2093
|
+
eventHandlers = /* @__PURE__ */ new Map();
|
|
2094
|
+
constructor(runtime) {
|
|
2095
|
+
this.runtime = runtime;
|
|
2096
|
+
this.metrics = new MetricsManager(runtime);
|
|
2097
|
+
}
|
|
2098
|
+
/**
|
|
2099
|
+
* 记录事件
|
|
2100
|
+
*/
|
|
2101
|
+
recordEvent(event, data = {}) {
|
|
2102
|
+
const eventData = {
|
|
2103
|
+
event,
|
|
2104
|
+
timestamp: Date.now(),
|
|
2105
|
+
data
|
|
2106
|
+
};
|
|
2107
|
+
this.updateMetricsForEvent(event, data);
|
|
2108
|
+
const handlers = this.eventHandlers.get(event) || [];
|
|
2109
|
+
handlers.forEach((handler) => {
|
|
2110
|
+
try {
|
|
2111
|
+
handler(eventData);
|
|
2112
|
+
} catch (error) {
|
|
2113
|
+
this.runtime.logger.error("Error in monitoring event handler", error);
|
|
2114
|
+
}
|
|
2115
|
+
});
|
|
2116
|
+
this.runtime.logger.debug(`Monitoring event: ${event}`, data);
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* 为事件更新指标
|
|
2120
|
+
*/
|
|
2121
|
+
updateMetricsForEvent(event, data) {
|
|
2122
|
+
switch (event) {
|
|
2123
|
+
case "connection_open" /* CONNECTION_OPEN */:
|
|
2124
|
+
this.metrics.incrementCounter("lingyao_connection_total");
|
|
2125
|
+
this.metrics.setGauge("lingyao_connection_count", data.activeConnections || 1);
|
|
2126
|
+
break;
|
|
2127
|
+
case "connection_close" /* CONNECTION_CLOSE */:
|
|
2128
|
+
this.metrics.incrementCounter("lingyao_disconnection_total");
|
|
2129
|
+
this.metrics.setGauge("lingyao_connection_count", data.activeConnections || 0);
|
|
2130
|
+
break;
|
|
2131
|
+
case "reconnect" /* RECONNECT */:
|
|
2132
|
+
this.metrics.incrementCounter("lingyao_reconnect_total");
|
|
2133
|
+
break;
|
|
2134
|
+
case "message_received" /* MESSAGE_RECEIVED */:
|
|
2135
|
+
this.metrics.incrementCounter("lingyao_message_total", 1, {
|
|
2136
|
+
direction: "in",
|
|
2137
|
+
type: data.messageType || "unknown"
|
|
2138
|
+
});
|
|
2139
|
+
break;
|
|
2140
|
+
case "message_sent" /* MESSAGE_SENT */:
|
|
2141
|
+
this.metrics.incrementCounter("lingyao_message_total", 1, {
|
|
2142
|
+
direction: "out",
|
|
2143
|
+
type: data.messageType || "unknown"
|
|
2144
|
+
});
|
|
2145
|
+
break;
|
|
2146
|
+
case "message_delivered" /* MESSAGE_DELIVERED */:
|
|
2147
|
+
if (data.latency) {
|
|
2148
|
+
this.metrics.recordHistogram("lingyao_message_latency", data.latency);
|
|
2149
|
+
}
|
|
2150
|
+
break;
|
|
2151
|
+
case "message_failed" /* MESSAGE_FAILED */:
|
|
2152
|
+
this.metrics.incrementCounter("lingyao_message_failed_total", 1, {
|
|
2153
|
+
direction: data.direction || "unknown",
|
|
2154
|
+
reason: data.reason || "unknown"
|
|
2155
|
+
});
|
|
2156
|
+
break;
|
|
2157
|
+
case "heartbeat_sent" /* HEARTBEAT_SENT */:
|
|
2158
|
+
this.metrics.incrementCounter("lingyao_heartbeat_total");
|
|
2159
|
+
break;
|
|
2160
|
+
case "heartbeat_missed" /* HEARTBEAT_MISSED */:
|
|
2161
|
+
this.metrics.incrementCounter("lingyao_heartbeat_missed_total");
|
|
2162
|
+
break;
|
|
2163
|
+
case "error_occurred" /* ERROR_OCCURRED */:
|
|
2164
|
+
this.metrics.incrementCounter("lingyao_error_total", 1, {
|
|
2165
|
+
type: data.errorType || "unknown",
|
|
2166
|
+
severity: data.severity || "medium"
|
|
2167
|
+
});
|
|
2168
|
+
break;
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* 注册事件处理器
|
|
2173
|
+
*/
|
|
2174
|
+
on(event, handler) {
|
|
2175
|
+
if (!this.eventHandlers.has(event)) {
|
|
2176
|
+
this.eventHandlers.set(event, []);
|
|
2177
|
+
}
|
|
2178
|
+
this.eventHandlers.get(event).push(handler);
|
|
2179
|
+
}
|
|
2180
|
+
/**
|
|
2181
|
+
* 移除事件处理器
|
|
2182
|
+
*/
|
|
2183
|
+
off(event, handler) {
|
|
2184
|
+
const handlers = this.eventHandlers.get(event);
|
|
2185
|
+
if (handlers) {
|
|
2186
|
+
const index = handlers.indexOf(handler);
|
|
2187
|
+
if (index > -1) {
|
|
2188
|
+
handlers.splice(index, 1);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
/**
|
|
2193
|
+
* 获取指标管理器
|
|
2194
|
+
*/
|
|
2195
|
+
getMetrics() {
|
|
2196
|
+
return this.metrics;
|
|
2197
|
+
}
|
|
2198
|
+
/**
|
|
2199
|
+
* 获取摘要报告
|
|
2200
|
+
*/
|
|
2201
|
+
getSummary() {
|
|
2202
|
+
return {
|
|
2203
|
+
uptime: Date.now() - this.runtime.startTime,
|
|
2204
|
+
connections: this.metrics.getMetricValue("lingyao_connection_total") || 0,
|
|
2205
|
+
messages: {
|
|
2206
|
+
sent: this.metrics.getMetricValue("lingyao_message_total") || 0,
|
|
2207
|
+
received: this.metrics.getMetricValue("lingyao_message_total") || 0,
|
|
2208
|
+
failed: this.metrics.getMetricValue("lingyao_message_failed_total") || 0
|
|
2209
|
+
},
|
|
2210
|
+
errors: this.metrics.getMetricValue("lingyao_error_total") || 0,
|
|
2211
|
+
heartbeats: {
|
|
2212
|
+
sent: this.metrics.getMetricValue("lingyao_heartbeat_total") || 0,
|
|
2213
|
+
missed: this.metrics.getMetricValue("lingyao_heartbeat_missed_total") || 0
|
|
2214
|
+
}
|
|
2215
|
+
};
|
|
2216
|
+
}
|
|
2217
|
+
};
|
|
2218
|
+
|
|
2219
|
+
// src/errors.ts
|
|
2220
|
+
var LingyaoError = class extends Error {
|
|
2221
|
+
code;
|
|
2222
|
+
severity;
|
|
2223
|
+
timestamp;
|
|
2224
|
+
context;
|
|
2225
|
+
cause;
|
|
2226
|
+
constructor(message, code, severity = "medium" /* MEDIUM */, context, cause) {
|
|
2227
|
+
super(message);
|
|
2228
|
+
this.name = this.constructor.name;
|
|
2229
|
+
this.code = code;
|
|
2230
|
+
this.severity = severity;
|
|
2231
|
+
this.timestamp = Date.now();
|
|
2232
|
+
this.context = context;
|
|
2233
|
+
this.cause = cause;
|
|
2234
|
+
Error.captureStackTrace(this, this.constructor);
|
|
2235
|
+
}
|
|
2236
|
+
/**
|
|
2237
|
+
* 转换为 JSON 可序列化格式
|
|
2238
|
+
*/
|
|
2239
|
+
toJSON() {
|
|
2240
|
+
return {
|
|
2241
|
+
name: this.name,
|
|
2242
|
+
message: this.message,
|
|
2243
|
+
code: this.code,
|
|
2244
|
+
severity: this.severity,
|
|
2245
|
+
timestamp: this.timestamp,
|
|
2246
|
+
context: this.context,
|
|
2247
|
+
cause: this.cause?.message,
|
|
2248
|
+
stack: this.stack
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
};
|
|
2252
|
+
var RetryableError = class extends LingyaoError {
|
|
2253
|
+
retryable;
|
|
2254
|
+
retryAfter;
|
|
2255
|
+
maxRetries;
|
|
2256
|
+
constructor(message, context, cause) {
|
|
2257
|
+
super(message, "RETRYABLE_ERROR", "low" /* LOW */, context, cause);
|
|
2258
|
+
this.retryable = true;
|
|
2259
|
+
this.retryAfter = context?.retryAfter;
|
|
2260
|
+
this.maxRetries = context?.maxRetries;
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
2263
|
+
var ErrorHandler = class {
|
|
2264
|
+
runtime;
|
|
2265
|
+
errorCounts = /* @__PURE__ */ new Map();
|
|
2266
|
+
lastErrors = /* @__PURE__ */ new Map();
|
|
2267
|
+
errorThresholds = /* @__PURE__ */ new Map();
|
|
2268
|
+
cleanupTimer = null;
|
|
2269
|
+
errorTimestamps = /* @__PURE__ */ new Map();
|
|
2270
|
+
constructor(runtime) {
|
|
2271
|
+
this.runtime = runtime;
|
|
2272
|
+
this.cleanupTimer = setInterval(() => this.cleanupOldErrors(), 5 * 60 * 1e3);
|
|
2273
|
+
if (this.cleanupTimer.unref) {
|
|
2274
|
+
this.cleanupTimer.unref();
|
|
2275
|
+
}
|
|
2276
|
+
this.setErrorThreshold("CONNECTION_ERROR", { count: 5, window: 6e4 });
|
|
2277
|
+
this.setErrorThreshold("AUTHENTICATION_ERROR", { count: 3, window: 6e4 });
|
|
2278
|
+
this.setErrorThreshold("NETWORK_ERROR", { count: 10, window: 6e4 });
|
|
2279
|
+
}
|
|
2280
|
+
destroy() {
|
|
2281
|
+
if (this.cleanupTimer) {
|
|
2282
|
+
clearInterval(this.cleanupTimer);
|
|
2283
|
+
this.cleanupTimer = null;
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
cleanupOldErrors() {
|
|
2287
|
+
const now = Date.now();
|
|
2288
|
+
for (const [code, timestamps] of this.errorTimestamps.entries()) {
|
|
2289
|
+
const thresholdConfig = this.errorThresholds.get(code);
|
|
2290
|
+
const windowMs = thresholdConfig?.window || 36e5;
|
|
2291
|
+
const validTimestamps = timestamps.filter((ts) => now - ts <= windowMs);
|
|
2292
|
+
if (validTimestamps.length === 0) {
|
|
2293
|
+
this.errorTimestamps.delete(code);
|
|
2294
|
+
this.errorCounts.delete(code);
|
|
2295
|
+
} else {
|
|
2296
|
+
this.errorTimestamps.set(code, validTimestamps);
|
|
2297
|
+
this.errorCounts.set(code, validTimestamps.length);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
/**
|
|
2302
|
+
* 处理错误
|
|
2303
|
+
*/
|
|
2304
|
+
handleError(error) {
|
|
2305
|
+
const lingyaoError = error instanceof LingyaoError ? error : new LingyaoError(error.message, "UNKNOWN_ERROR", "medium" /* MEDIUM */, {}, error);
|
|
2306
|
+
this.recordError(lingyaoError);
|
|
2307
|
+
switch (lingyaoError.severity) {
|
|
2308
|
+
case "low" /* LOW */:
|
|
2309
|
+
this.runtime.logger.debug(lingyaoError.message, lingyaoError.toJSON());
|
|
2310
|
+
break;
|
|
2311
|
+
case "medium" /* MEDIUM */:
|
|
2312
|
+
this.runtime.logger.warn(lingyaoError.message, lingyaoError.toJSON());
|
|
2313
|
+
break;
|
|
2314
|
+
case "high" /* HIGH */:
|
|
2315
|
+
case "critical" /* CRITICAL */:
|
|
2316
|
+
this.runtime.logger.error(lingyaoError.message, lingyaoError.toJSON());
|
|
2317
|
+
break;
|
|
2318
|
+
}
|
|
2319
|
+
this.checkThresholds(lingyaoError);
|
|
2320
|
+
}
|
|
2321
|
+
/**
|
|
2322
|
+
* 记录错误
|
|
2323
|
+
*/
|
|
2324
|
+
recordError(error) {
|
|
2325
|
+
const key = error.code;
|
|
2326
|
+
const timestamp = Date.now();
|
|
2327
|
+
const thresholdConfig = this.errorThresholds.get(key);
|
|
2328
|
+
const windowMs = thresholdConfig?.window || 36e5;
|
|
2329
|
+
let timestamps = this.errorTimestamps.get(key) || [];
|
|
2330
|
+
timestamps = timestamps.filter((ts) => timestamp - ts <= windowMs);
|
|
2331
|
+
timestamps.push(timestamp);
|
|
2332
|
+
this.errorTimestamps.set(key, timestamps);
|
|
2333
|
+
this.errorCounts.set(key, timestamps.length);
|
|
2334
|
+
this.lastErrors.set(key, {
|
|
2335
|
+
error,
|
|
2336
|
+
timestamp
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* 检查错误阈值
|
|
2341
|
+
*/
|
|
2342
|
+
checkThresholds(error) {
|
|
2343
|
+
const threshold = this.errorThresholds.get(error.code);
|
|
2344
|
+
if (!threshold) {
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
const count = this.errorCounts.get(error.code) || 0;
|
|
2348
|
+
if (count >= threshold.count) {
|
|
2349
|
+
this.runtime.logger.error(
|
|
2350
|
+
`Error threshold exceeded for ${error.code}`,
|
|
2351
|
+
{
|
|
2352
|
+
code: error.code,
|
|
2353
|
+
count,
|
|
2354
|
+
threshold: threshold.count,
|
|
2355
|
+
window: threshold.window
|
|
2356
|
+
}
|
|
2357
|
+
);
|
|
2358
|
+
this.triggerAlert(error, count);
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
/**
|
|
2362
|
+
* 触发警报
|
|
2363
|
+
*/
|
|
2364
|
+
triggerAlert(error, count) {
|
|
2365
|
+
this.runtime.logger.error("ALERT", {
|
|
2366
|
+
type: "error_threshold_exceeded",
|
|
2367
|
+
error: error.toJSON(),
|
|
2368
|
+
count
|
|
2369
|
+
});
|
|
2370
|
+
}
|
|
2371
|
+
/**
|
|
2372
|
+
* 设置错误阈值
|
|
2373
|
+
*/
|
|
2374
|
+
setErrorThreshold(code, threshold) {
|
|
2375
|
+
this.errorThresholds.set(code, threshold);
|
|
2376
|
+
}
|
|
2377
|
+
/**
|
|
2378
|
+
* 获取错误统计
|
|
2379
|
+
*/
|
|
2380
|
+
getErrorStats() {
|
|
2381
|
+
const byCode = {};
|
|
2382
|
+
let total = 0;
|
|
2383
|
+
for (const [code, count] of this.errorCounts) {
|
|
2384
|
+
byCode[code] = count;
|
|
2385
|
+
total += count;
|
|
2386
|
+
}
|
|
2387
|
+
const recent = Array.from(this.lastErrors.entries()).map(([code, { error, timestamp }]) => ({ code, error, timestamp })).sort((a, b) => b.timestamp - a.timestamp).slice(0, 10);
|
|
2388
|
+
return { byCode, total, recent };
|
|
2389
|
+
}
|
|
2390
|
+
/**
|
|
2391
|
+
* 重置错误计数
|
|
2392
|
+
*/
|
|
2393
|
+
resetErrorCounts(code) {
|
|
2394
|
+
if (code) {
|
|
2395
|
+
this.errorCounts.delete(code);
|
|
2396
|
+
this.lastErrors.delete(code);
|
|
2397
|
+
} else {
|
|
2398
|
+
this.errorCounts.clear();
|
|
2399
|
+
this.lastErrors.clear();
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
/**
|
|
2403
|
+
* 包装异步函数以自动处理错误
|
|
2404
|
+
*/
|
|
2405
|
+
async wrapAsync(fn, context) {
|
|
2406
|
+
try {
|
|
2407
|
+
return await fn();
|
|
2408
|
+
} catch (error) {
|
|
2409
|
+
const operation = context?.operation || "unknown";
|
|
2410
|
+
if (error instanceof LingyaoError) {
|
|
2411
|
+
this.handleError(error);
|
|
2412
|
+
} else if (context?.retryable) {
|
|
2413
|
+
const retryableError = new RetryableError(
|
|
2414
|
+
`Operation "${operation}" failed`,
|
|
2415
|
+
{ operation },
|
|
2416
|
+
error
|
|
2417
|
+
);
|
|
2418
|
+
this.handleError(retryableError);
|
|
2419
|
+
} else {
|
|
2420
|
+
this.handleError(new LingyaoError(
|
|
2421
|
+
`Operation "${operation}" failed: ${error.message}`,
|
|
2422
|
+
"OPERATION_FAILED",
|
|
2423
|
+
"medium" /* MEDIUM */,
|
|
2424
|
+
{ operation },
|
|
2425
|
+
error
|
|
2426
|
+
));
|
|
2427
|
+
}
|
|
2428
|
+
if (context?.fallback !== void 0) {
|
|
2429
|
+
return context.fallback;
|
|
2430
|
+
}
|
|
2431
|
+
throw error;
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
/**
|
|
2435
|
+
* 创建带有错误处理的重试逻辑
|
|
2436
|
+
*/
|
|
2437
|
+
async retry(fn, options = {}) {
|
|
2438
|
+
const {
|
|
2439
|
+
maxRetries = 3,
|
|
2440
|
+
retryDelay = 1e3,
|
|
2441
|
+
backoff = true,
|
|
2442
|
+
onRetry
|
|
2443
|
+
} = options;
|
|
2444
|
+
let lastError = null;
|
|
2445
|
+
let delay = retryDelay;
|
|
2446
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
2447
|
+
try {
|
|
2448
|
+
return await fn();
|
|
2449
|
+
} catch (error) {
|
|
2450
|
+
lastError = error;
|
|
2451
|
+
if (attempt === maxRetries) {
|
|
2452
|
+
break;
|
|
2453
|
+
}
|
|
2454
|
+
if (onRetry) {
|
|
2455
|
+
onRetry(lastError, attempt + 1);
|
|
2456
|
+
}
|
|
2457
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2458
|
+
if (backoff) {
|
|
2459
|
+
delay *= 2;
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
throw new RetryableError(
|
|
2464
|
+
`Operation failed after ${maxRetries + 1} attempts`,
|
|
2465
|
+
{ maxRetries },
|
|
2466
|
+
lastError ?? new Error("Unknown retry failure")
|
|
2467
|
+
);
|
|
2468
|
+
}
|
|
2469
|
+
/**
|
|
2470
|
+
* 判断错误是否可重试
|
|
2471
|
+
*/
|
|
2472
|
+
isRetryable(error) {
|
|
2473
|
+
if (error instanceof RetryableError) {
|
|
2474
|
+
return error.retryable;
|
|
2475
|
+
}
|
|
2476
|
+
if (error instanceof LingyaoError) {
|
|
2477
|
+
switch (error.code) {
|
|
2478
|
+
case "NETWORK_ERROR":
|
|
2479
|
+
case "TIMEOUT_ERROR":
|
|
2480
|
+
case "CONNECTION_ERROR":
|
|
2481
|
+
return true;
|
|
2482
|
+
default:
|
|
2483
|
+
return false;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
return false;
|
|
2487
|
+
}
|
|
2488
|
+
/**
|
|
2489
|
+
* 判断错误是否需要降级处理
|
|
2490
|
+
*/
|
|
2491
|
+
shouldDegrade(error) {
|
|
2492
|
+
if (error instanceof LingyaoError) {
|
|
2493
|
+
return error.severity === "high" /* HIGH */ || error.severity === "critical" /* CRITICAL */;
|
|
2494
|
+
}
|
|
2495
|
+
return false;
|
|
2496
|
+
}
|
|
2497
|
+
/**
|
|
2498
|
+
* 创建降级响应
|
|
2499
|
+
*/
|
|
2500
|
+
createDegradedResponse(fallback, error) {
|
|
2501
|
+
return {
|
|
2502
|
+
success: false,
|
|
2503
|
+
degraded: true,
|
|
2504
|
+
fallback,
|
|
2505
|
+
error
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2508
|
+
};
|
|
2509
|
+
|
|
2510
|
+
// src/orchestrator.ts
|
|
2511
|
+
function getMachineId() {
|
|
2512
|
+
try {
|
|
2513
|
+
const interfaces = networkInterfaces();
|
|
2514
|
+
let macStr = "";
|
|
2515
|
+
for (const name of Object.keys(interfaces)) {
|
|
2516
|
+
const iface = interfaces[name];
|
|
2517
|
+
if (!iface) continue;
|
|
2518
|
+
for (const alias of iface) {
|
|
2519
|
+
if (!alias.internal && alias.mac && alias.mac !== "00:00:00:00:00:00") {
|
|
2520
|
+
macStr += alias.mac;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
if (macStr) {
|
|
2525
|
+
return createHash2("md5").update(macStr).digest("hex").substring(0, 8);
|
|
2526
|
+
}
|
|
2527
|
+
} catch (e) {
|
|
2528
|
+
}
|
|
2529
|
+
return Math.random().toString(36).substring(2, 10);
|
|
2530
|
+
}
|
|
2531
|
+
var MACHINE_ID = getMachineId();
|
|
2532
|
+
function generateGatewayId(accountId) {
|
|
2533
|
+
const host = hostname().split(".")[0].replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
2534
|
+
return `gw_openclaw_${host}_${MACHINE_ID}_${accountId}`;
|
|
2535
|
+
}
|
|
2536
|
+
var MultiAccountOrchestrator = class {
|
|
2537
|
+
runtime;
|
|
2538
|
+
accounts = /* @__PURE__ */ new Map();
|
|
2539
|
+
messageHandler = null;
|
|
2540
|
+
constructor(runtime) {
|
|
2541
|
+
this.runtime = runtime;
|
|
2542
|
+
}
|
|
2543
|
+
/**
|
|
2544
|
+
* Set the message handler for delivering messages to the Agent.
|
|
2545
|
+
* Propagates to all running accounts.
|
|
2546
|
+
*/
|
|
2547
|
+
setMessageHandler(handler) {
|
|
2548
|
+
this.messageHandler = handler;
|
|
2549
|
+
for (const state of this.accounts.values()) {
|
|
2550
|
+
if (state.messageProcessor) {
|
|
2551
|
+
state.messageProcessor.setMessageHandler(handler);
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
/**
|
|
2556
|
+
* Get the current message handler (for gateway adapter injection).
|
|
2557
|
+
*/
|
|
2558
|
+
getMessageHandler() {
|
|
2559
|
+
return this.messageHandler;
|
|
2560
|
+
}
|
|
2561
|
+
/**
|
|
2562
|
+
* Start an account: create components, register to server, connect WS.
|
|
2563
|
+
*/
|
|
2564
|
+
async start(account) {
|
|
2565
|
+
const { id: accountId } = account;
|
|
2566
|
+
const existing = this.accounts.get(accountId);
|
|
2567
|
+
if (existing?.status === "running") {
|
|
2568
|
+
this.runtime.logger.warn(`Account "${accountId}" is already running`);
|
|
2569
|
+
return;
|
|
2570
|
+
}
|
|
2571
|
+
this.runtime.logger.info(`Starting account "${accountId}"`);
|
|
2572
|
+
const gatewayId = account.gatewayId ?? generateGatewayId(accountId);
|
|
2573
|
+
const storagePrefix = `lingyao:${accountId}`;
|
|
2574
|
+
const accountManager = new AccountManager(this.runtime);
|
|
2575
|
+
const messageProcessor = new MessageProcessor(this.runtime, accountManager);
|
|
2576
|
+
const probe = new Probe(this.runtime);
|
|
2577
|
+
const monitor = new Monitor(this.runtime);
|
|
2578
|
+
const errorHandler = new ErrorHandler(this.runtime);
|
|
2579
|
+
const httpClient = new ServerHttpClient(
|
|
2580
|
+
this.runtime,
|
|
2581
|
+
gatewayId,
|
|
2582
|
+
{ baseURL: LINGYAO_SERVER_URL },
|
|
2583
|
+
storagePrefix
|
|
2584
|
+
);
|
|
2585
|
+
const state = {
|
|
2586
|
+
accountId,
|
|
2587
|
+
wsClient: null,
|
|
2588
|
+
httpClient,
|
|
2589
|
+
accountManager,
|
|
2590
|
+
messageProcessor,
|
|
2591
|
+
probe,
|
|
2592
|
+
monitor,
|
|
2593
|
+
errorHandler,
|
|
2594
|
+
status: "starting",
|
|
2595
|
+
startTime: Date.now(),
|
|
2596
|
+
gatewayId
|
|
2597
|
+
};
|
|
2598
|
+
this.accounts.set(accountId, state);
|
|
2599
|
+
try {
|
|
2600
|
+
await accountManager.initialize();
|
|
2601
|
+
await messageProcessor.initialize();
|
|
2602
|
+
if (this.messageHandler) {
|
|
2603
|
+
messageProcessor.setMessageHandler(this.messageHandler);
|
|
2604
|
+
}
|
|
2605
|
+
await this.registerToServer(state);
|
|
2606
|
+
const wsClient = new LingyaoWSClient(this.runtime, {
|
|
2607
|
+
url: `${LINGYAO_SERVER_URL}/v1/gateway/ws`,
|
|
2608
|
+
gatewayId,
|
|
2609
|
+
token: httpClient.getGatewayToken() ?? void 0,
|
|
2610
|
+
reconnectInterval: 5e3,
|
|
2611
|
+
heartbeatInterval: 3e4,
|
|
2612
|
+
messageHandler: this.createMessageHandler(state),
|
|
2613
|
+
eventHandler: this.createEventHandler(state)
|
|
2614
|
+
});
|
|
2615
|
+
await wsClient.connect();
|
|
2616
|
+
state.wsClient = wsClient;
|
|
2617
|
+
state.status = "running";
|
|
2618
|
+
this.runtime.logger.info(`Account "${accountId}" started successfully`);
|
|
2619
|
+
} catch (error) {
|
|
2620
|
+
state.status = "error";
|
|
2621
|
+
this.runtime.logger.error(`Failed to start account "${accountId}"`, error);
|
|
2622
|
+
throw error;
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
/**
|
|
2626
|
+
* Stop an account: disconnect WS, stop heartbeat.
|
|
2627
|
+
*/
|
|
2628
|
+
async stop(accountId) {
|
|
2629
|
+
const state = this.accounts.get(accountId);
|
|
2630
|
+
if (!state || state.status === "stopped") {
|
|
2631
|
+
return;
|
|
2632
|
+
}
|
|
2633
|
+
this.runtime.logger.info(`Stopping account "${accountId}"`);
|
|
2634
|
+
state.status = "stopping";
|
|
2635
|
+
if (state.wsClient) {
|
|
2636
|
+
state.wsClient.disconnect();
|
|
2637
|
+
state.wsClient = null;
|
|
2638
|
+
}
|
|
2639
|
+
if (state.httpClient) {
|
|
2640
|
+
state.httpClient.stopHeartbeat();
|
|
2641
|
+
}
|
|
2642
|
+
state.status = "stopped";
|
|
2643
|
+
this.runtime.logger.info(`Account "${accountId}" stopped`);
|
|
2644
|
+
}
|
|
2645
|
+
/**
|
|
2646
|
+
* Stop all running accounts.
|
|
2647
|
+
*/
|
|
2648
|
+
async stopAll() {
|
|
2649
|
+
const stops = Array.from(this.accounts.keys()).map((id) => this.stop(id));
|
|
2650
|
+
await Promise.all(stops);
|
|
2651
|
+
}
|
|
2652
|
+
/**
|
|
2653
|
+
* Get account state by ID.
|
|
2654
|
+
*/
|
|
2655
|
+
getAccountState(accountId) {
|
|
2656
|
+
return this.accounts.get(accountId);
|
|
2657
|
+
}
|
|
2658
|
+
/**
|
|
2659
|
+
* Get account's WS client.
|
|
2660
|
+
*/
|
|
2661
|
+
getWSClient(accountId) {
|
|
2662
|
+
return this.accounts.get(accountId)?.wsClient ?? null;
|
|
2663
|
+
}
|
|
2664
|
+
/**
|
|
2665
|
+
* Get account's HTTP client.
|
|
2666
|
+
*/
|
|
2667
|
+
getHttpClient(accountId) {
|
|
2668
|
+
return this.accounts.get(accountId)?.httpClient ?? null;
|
|
2669
|
+
}
|
|
2670
|
+
/**
|
|
2671
|
+
* Get account's AccountManager.
|
|
2672
|
+
*/
|
|
2673
|
+
getAccountManager(accountId) {
|
|
2674
|
+
return this.accounts.get(accountId)?.accountManager ?? null;
|
|
2675
|
+
}
|
|
2676
|
+
/**
|
|
2677
|
+
* Get account's Probe.
|
|
2678
|
+
*/
|
|
2679
|
+
getProbe(accountId) {
|
|
2680
|
+
return this.accounts.get(accountId)?.probe ?? null;
|
|
2681
|
+
}
|
|
2682
|
+
/**
|
|
2683
|
+
* Get account's Monitor.
|
|
2684
|
+
*/
|
|
2685
|
+
getMonitor(accountId) {
|
|
2686
|
+
return this.accounts.get(accountId)?.monitor ?? null;
|
|
2687
|
+
}
|
|
2688
|
+
/**
|
|
2689
|
+
* Send notification to a device on a specific account.
|
|
2690
|
+
* Returns true if sent, false if WS not connected.
|
|
2691
|
+
*/
|
|
2692
|
+
sendNotification(accountId, deviceId, notification) {
|
|
2693
|
+
const wsClient = this.getWSClient(accountId);
|
|
2694
|
+
if (!wsClient || !wsClient.isConnected()) {
|
|
2695
|
+
return false;
|
|
2696
|
+
}
|
|
2697
|
+
wsClient.sendNotification(deviceId, notification);
|
|
2698
|
+
return true;
|
|
2699
|
+
}
|
|
2700
|
+
/**
|
|
2701
|
+
* List all running account IDs.
|
|
2702
|
+
*/
|
|
2703
|
+
getRunningAccountIds() {
|
|
2704
|
+
return Array.from(this.accounts.entries()).filter(([, state]) => state.status === "running").map(([id]) => id);
|
|
2705
|
+
}
|
|
2706
|
+
/**
|
|
2707
|
+
* Register to lingyao server for a specific account.
|
|
2708
|
+
*/
|
|
2709
|
+
async registerToServer(state) {
|
|
2710
|
+
if (!state.httpClient) {
|
|
2711
|
+
throw new Error("HTTP client not available");
|
|
2712
|
+
}
|
|
2713
|
+
try {
|
|
2714
|
+
const restored = await state.httpClient.restoreFromStorage();
|
|
2715
|
+
if (restored) {
|
|
2716
|
+
this.runtime.logger.info(`Account "${state.accountId}": session restored from storage`);
|
|
2717
|
+
return;
|
|
2718
|
+
}
|
|
2719
|
+
this.runtime.logger.info(`Account "${state.accountId}": registering to server...`, {
|
|
2720
|
+
gatewayId: state.gatewayId
|
|
2721
|
+
});
|
|
2722
|
+
const response = await state.httpClient.register({
|
|
2723
|
+
websocket: true,
|
|
2724
|
+
compression: false,
|
|
2725
|
+
maxMessageSize: 1048576
|
|
2726
|
+
});
|
|
2727
|
+
this.runtime.logger.info(`Account "${state.accountId}": registered successfully`, {
|
|
2728
|
+
expiresAt: new Date(response.expiresAt).toISOString()
|
|
2729
|
+
});
|
|
2730
|
+
} catch (error) {
|
|
2731
|
+
this.runtime.logger.error(`Account "${state.accountId}": registration failed`, error);
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
/**
|
|
2735
|
+
* Create message handler for inbound App messages on a specific account.
|
|
2736
|
+
*/
|
|
2737
|
+
createMessageHandler(state) {
|
|
2738
|
+
return async (message) => {
|
|
2739
|
+
try {
|
|
2740
|
+
const appMessage = message.payload;
|
|
2741
|
+
const deviceId = appMessage.deviceId;
|
|
2742
|
+
const msg = appMessage.message;
|
|
2743
|
+
this.runtime.logger.info(`[${state.accountId}] Received message from App`, {
|
|
2744
|
+
deviceId,
|
|
2745
|
+
messageType: msg.type,
|
|
2746
|
+
messageId: msg.id
|
|
2747
|
+
});
|
|
2748
|
+
state.monitor.recordEvent("message_received" /* MESSAGE_RECEIVED */, {
|
|
2749
|
+
deviceId,
|
|
2750
|
+
messageType: msg.type
|
|
2751
|
+
});
|
|
2752
|
+
switch (msg.type) {
|
|
2753
|
+
case "sync_diary":
|
|
2754
|
+
case "sync_memory":
|
|
2755
|
+
await this.handleSyncMessage(state, deviceId, msg);
|
|
2756
|
+
break;
|
|
2757
|
+
case "heartbeat":
|
|
2758
|
+
await state.accountManager.updateLastSeen(deviceId);
|
|
2759
|
+
state.monitor.recordEvent("heartbeat_received" /* HEARTBEAT_RECEIVED */, { deviceId });
|
|
2760
|
+
break;
|
|
2761
|
+
default:
|
|
2762
|
+
this.runtime.logger.warn(`[${state.accountId}] Unknown message type`, { type: msg.type });
|
|
2763
|
+
}
|
|
2764
|
+
} catch (error) {
|
|
2765
|
+
this.runtime.logger.error(`[${state.accountId}] Error handling App message`, error);
|
|
2766
|
+
state.monitor.recordEvent("error_occurred" /* ERROR_OCCURRED */, {
|
|
2767
|
+
errorType: "message_handling"
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
};
|
|
2771
|
+
}
|
|
2772
|
+
/**
|
|
2773
|
+
* Handle sync message (diary or memory).
|
|
2774
|
+
*/
|
|
2775
|
+
async handleSyncMessage(state, deviceId, message) {
|
|
2776
|
+
if (!state.messageProcessor) {
|
|
2777
|
+
this.runtime.logger.warn(`[${state.accountId}] Message processor not initialized`);
|
|
2778
|
+
return;
|
|
2779
|
+
}
|
|
2780
|
+
const agentMessage = {
|
|
2781
|
+
id: message.id,
|
|
2782
|
+
type: message.type === "sync_diary" ? "diary" : "memory",
|
|
2783
|
+
from: deviceId,
|
|
2784
|
+
deviceId,
|
|
2785
|
+
content: message.content,
|
|
2786
|
+
metadata: message.metadata || {},
|
|
2787
|
+
timestamp: message.timestamp
|
|
2788
|
+
};
|
|
2789
|
+
await state.messageProcessor.deliverToAgent(agentMessage);
|
|
2790
|
+
}
|
|
2791
|
+
/**
|
|
2792
|
+
* Create event handler for WS connection events on a specific account.
|
|
2793
|
+
*/
|
|
2794
|
+
createEventHandler(state) {
|
|
2795
|
+
return (event) => {
|
|
2796
|
+
switch (event.type) {
|
|
2797
|
+
case "connected":
|
|
2798
|
+
this.runtime.logger.info(`[${state.accountId}] WS connected`, {
|
|
2799
|
+
connectionId: event.connectionId
|
|
2800
|
+
});
|
|
2801
|
+
state.monitor.recordEvent("connection_open" /* CONNECTION_OPEN */, {
|
|
2802
|
+
connectionId: event.connectionId
|
|
2803
|
+
});
|
|
2804
|
+
break;
|
|
2805
|
+
case "disconnected":
|
|
2806
|
+
this.runtime.logger.warn(`[${state.accountId}] WS disconnected`, {
|
|
2807
|
+
code: event.code,
|
|
2808
|
+
reason: event.reason
|
|
2809
|
+
});
|
|
2810
|
+
state.monitor.recordEvent("connection_close" /* CONNECTION_CLOSE */, {
|
|
2811
|
+
code: event.code,
|
|
2812
|
+
reason: event.reason
|
|
2813
|
+
});
|
|
2814
|
+
if (event.code === 1008) {
|
|
2815
|
+
this.runtime.logger.warn(`[${state.accountId}] Token invalid (1008). Forcing re-registration...`);
|
|
2816
|
+
this.handleInvalidToken(state).catch((err) => {
|
|
2817
|
+
this.runtime.logger.error(`[${state.accountId}] Failed to re-register after 1008`, err);
|
|
2818
|
+
});
|
|
2819
|
+
}
|
|
2820
|
+
break;
|
|
2821
|
+
case "error":
|
|
2822
|
+
this.runtime.logger.error(`[${state.accountId}] WS error`, event.error);
|
|
2823
|
+
state.probe.recordError(event.error.message, "websocket");
|
|
2824
|
+
state.monitor.recordEvent("connection_error" /* CONNECTION_ERROR */, {
|
|
2825
|
+
error: event.error
|
|
2826
|
+
});
|
|
2827
|
+
state.errorHandler.handleError(event.error);
|
|
2828
|
+
break;
|
|
2829
|
+
case "pairing_completed":
|
|
2830
|
+
this.handlePairingCompleted(state, event);
|
|
2831
|
+
break;
|
|
2832
|
+
}
|
|
2833
|
+
};
|
|
2834
|
+
}
|
|
2835
|
+
/**
|
|
2836
|
+
* Handle pairing completed event from WS — auto-bind device.
|
|
2837
|
+
*/
|
|
2838
|
+
async handlePairingCompleted(state, event) {
|
|
2839
|
+
const { deviceId, deviceInfo } = event;
|
|
2840
|
+
this.runtime.logger.info(`[${state.accountId}] Pairing completed, auto-binding device`, { deviceId, deviceInfo });
|
|
2841
|
+
try {
|
|
2842
|
+
await state.accountManager.addDevice(deviceId, {
|
|
2843
|
+
name: deviceInfo?.name ?? deviceId,
|
|
2844
|
+
platform: deviceInfo?.platform ?? "harmonyos",
|
|
2845
|
+
version: deviceInfo?.version ?? ""
|
|
2846
|
+
});
|
|
2847
|
+
this.runtime.logger.info(`[${state.accountId}] Device auto-bound: ${deviceId}`);
|
|
2848
|
+
} catch (error) {
|
|
2849
|
+
this.runtime.logger.error(`[${state.accountId}] Failed to auto-bind device: ${deviceId}`, error);
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
/**
|
|
2853
|
+
* Handle invalid token by re-registering the gateway and reconnecting WS
|
|
2854
|
+
*/
|
|
2855
|
+
async handleInvalidToken(state) {
|
|
2856
|
+
if (!state.httpClient || !state.wsClient) {
|
|
2857
|
+
return;
|
|
2858
|
+
}
|
|
2859
|
+
try {
|
|
2860
|
+
this.runtime.logger.info(`[${state.accountId}] Requesting new gateway token...`);
|
|
2861
|
+
const response = await state.httpClient.register({
|
|
2862
|
+
websocket: true,
|
|
2863
|
+
compression: false,
|
|
2864
|
+
maxMessageSize: 1048576
|
|
2865
|
+
});
|
|
2866
|
+
this.runtime.logger.info(`[${state.accountId}] Obtained new token. Reconnecting WS...`);
|
|
2867
|
+
state.wsClient.updateToken(response.gatewayToken);
|
|
2868
|
+
await state.wsClient.connect();
|
|
2869
|
+
} catch (error) {
|
|
2870
|
+
this.runtime.logger.error(`[${state.accountId}] Failed to handle invalid token`, error);
|
|
2871
|
+
state.errorHandler.handleError(error);
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
};
|
|
2875
|
+
|
|
2876
|
+
// src/channel.ts
|
|
2877
|
+
import { createHash as createHash3 } from "crypto";
|
|
2878
|
+
import { hostname as hostname2, networkInterfaces as networkInterfaces2 } from "os";
|
|
2879
|
+
function getMachineId2() {
|
|
2880
|
+
try {
|
|
2881
|
+
const interfaces = networkInterfaces2();
|
|
2882
|
+
let macStr = "";
|
|
2883
|
+
for (const name of Object.keys(interfaces)) {
|
|
2884
|
+
const iface = interfaces[name];
|
|
2885
|
+
if (!iface) continue;
|
|
2886
|
+
for (const alias of iface) {
|
|
2887
|
+
if (!alias.internal && alias.mac && alias.mac !== "00:00:00:00:00:00") {
|
|
2888
|
+
macStr += alias.mac;
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
if (macStr) {
|
|
2893
|
+
return createHash3("md5").update(macStr).digest("hex").substring(0, 8);
|
|
2894
|
+
}
|
|
2895
|
+
} catch (e) {
|
|
2896
|
+
}
|
|
2897
|
+
return Math.random().toString(36).substring(2, 10);
|
|
2898
|
+
}
|
|
2899
|
+
var MACHINE_ID2 = getMachineId2();
|
|
2900
|
+
|
|
2901
|
+
// src/config-schema.ts
|
|
2902
|
+
import { z } from "zod";
|
|
2903
|
+
var lingyaoConfigSchema = z.object({
|
|
2904
|
+
enabled: z.boolean().default(true),
|
|
2905
|
+
maxOfflineMessages: z.number().int().min(1).max(1e3).optional().default(100),
|
|
2906
|
+
tokenExpiryDays: z.number().int().min(1).max(365).optional().default(30)
|
|
2907
|
+
});
|
|
2908
|
+
function getDefaultConfig() {
|
|
2909
|
+
return {
|
|
2910
|
+
enabled: true,
|
|
2911
|
+
maxOfflineMessages: 100,
|
|
2912
|
+
tokenExpiryDays: 30
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
// src/api.ts
|
|
2917
|
+
var orchestrator = null;
|
|
2918
|
+
function getOrchestrator() {
|
|
2919
|
+
return orchestrator;
|
|
2920
|
+
}
|
|
2921
|
+
var configAdapter = createConfigAdapter();
|
|
2922
|
+
var setupAdapter = createSetupAdapter();
|
|
2923
|
+
var messagingAdapter = createMessagingAdapter();
|
|
2924
|
+
var gatewayAdapter = createGatewayAdapter(getOrchestrator);
|
|
2925
|
+
var directoryAdapter = createDirectoryAdapter(getOrchestrator);
|
|
2926
|
+
var outboundAdapter = createOutboundAdapter(getOrchestrator);
|
|
2927
|
+
var securityOptions = {
|
|
2928
|
+
dm: {
|
|
2929
|
+
channelKey: "lingyao",
|
|
2930
|
+
resolvePolicy: (account) => account.dmPolicy,
|
|
2931
|
+
resolveAllowFrom: (account) => account.allowFrom,
|
|
2932
|
+
defaultPolicy: "paired"
|
|
2933
|
+
}
|
|
2934
|
+
};
|
|
2935
|
+
var pairingOptions = {
|
|
2936
|
+
text: {
|
|
2937
|
+
idLabel: "\u8BBE\u5907 ID",
|
|
2938
|
+
message: "\u8BBE\u5907\u5DF2\u6279\u51C6\u914D\u5BF9",
|
|
2939
|
+
notify: async (params) => {
|
|
2940
|
+
const orc = getOrchestrator();
|
|
2941
|
+
if (!orc) return;
|
|
2942
|
+
const config = params.cfg;
|
|
2943
|
+
const channels = config.channels;
|
|
2944
|
+
const lingyao = channels?.lingyao;
|
|
2945
|
+
const accountIds = lingyao?.accounts ? Object.keys(lingyao.accounts) : ["default"];
|
|
2946
|
+
for (const accountId of accountIds) {
|
|
2947
|
+
const sent = orc.sendNotification(accountId, params.id, {
|
|
2948
|
+
type: "pairing_confirmed",
|
|
2949
|
+
message: pairingOptions.text.message
|
|
2950
|
+
});
|
|
2951
|
+
if (sent) break;
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
};
|
|
2956
|
+
var capabilities = {
|
|
2957
|
+
chatTypes: ["direct"],
|
|
2958
|
+
media: false,
|
|
2959
|
+
reactions: false,
|
|
2960
|
+
threads: false,
|
|
2961
|
+
polls: false,
|
|
2962
|
+
edit: false,
|
|
2963
|
+
unsend: false,
|
|
2964
|
+
reply: false,
|
|
2965
|
+
effects: false,
|
|
2966
|
+
groupManagement: false,
|
|
2967
|
+
nativeCommands: false,
|
|
2968
|
+
blockStreaming: true
|
|
2969
|
+
};
|
|
2970
|
+
var meta = {
|
|
2971
|
+
id: "lingyao",
|
|
2972
|
+
label: "\u7075\u723B",
|
|
2973
|
+
selectionLabel: "\u7075\u723B (HarmonyOS)",
|
|
2974
|
+
docsPath: "/channels/lingyao",
|
|
2975
|
+
docsLabel: "\u7075\u723B\u6587\u6863",
|
|
2976
|
+
blurb: "\u901A\u8FC7 lingyao.live \u670D\u52A1\u5668\u4E2D\u8F6C\u4E0E\u9E3F\u8499\u7075\u723B App \u53CC\u5411\u540C\u6B65\u65E5\u8BB0\u548C\u8BB0\u5FC6",
|
|
2977
|
+
order: 50,
|
|
2978
|
+
aliases: ["lingyao", "\u7075\u723B"]
|
|
2979
|
+
};
|
|
2980
|
+
var lingyaoPlugin = {
|
|
2981
|
+
...createChatChannelPlugin({
|
|
2982
|
+
base: {
|
|
2983
|
+
id: "lingyao",
|
|
2984
|
+
meta,
|
|
2985
|
+
capabilities,
|
|
2986
|
+
config: configAdapter,
|
|
2987
|
+
setup: setupAdapter
|
|
2988
|
+
},
|
|
2989
|
+
security: securityOptions,
|
|
2990
|
+
pairing: pairingOptions,
|
|
2991
|
+
outbound: outboundAdapter
|
|
2992
|
+
}),
|
|
2993
|
+
gateway: gatewayAdapter,
|
|
2994
|
+
status: void 0,
|
|
2995
|
+
directory: directoryAdapter,
|
|
2996
|
+
messaging: messagingAdapter
|
|
2997
|
+
};
|
|
2998
|
+
function initializeLingyaoRuntime(runtime) {
|
|
2999
|
+
const adapted = adaptPluginRuntime(runtime);
|
|
3000
|
+
setRuntime(adapted);
|
|
3001
|
+
orchestrator = new MultiAccountOrchestrator(adapted);
|
|
3002
|
+
lingyaoPlugin.status = createStatusAdapter(getOrchestrator, adapted);
|
|
3003
|
+
}
|
|
3004
|
+
var pluginMetadata = {
|
|
3005
|
+
name: "lingyao",
|
|
3006
|
+
version: "0.9.1",
|
|
3007
|
+
description: "Lingyao Channel Plugin - bidirectional sync via lingyao.live server relay",
|
|
3008
|
+
type: "channel",
|
|
3009
|
+
capabilities: {
|
|
3010
|
+
chatTypes: ["direct"],
|
|
3011
|
+
media: false,
|
|
3012
|
+
reactions: false,
|
|
3013
|
+
threads: false
|
|
3014
|
+
},
|
|
3015
|
+
defaultConfig: getDefaultConfig()
|
|
3016
|
+
};
|
|
3017
|
+
|
|
3018
|
+
// src/runtime-api.ts
|
|
3019
|
+
function setLingyaoRuntime(runtime) {
|
|
3020
|
+
initializeLingyaoRuntime(runtime);
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
// src/index.ts
|
|
3024
|
+
var plugin = {
|
|
3025
|
+
id: "lingyao",
|
|
3026
|
+
name: "Lingyao",
|
|
3027
|
+
description: "Lingyao Channel Plugin - bidirectional sync via lingyao.live server relay",
|
|
3028
|
+
register(api) {
|
|
3029
|
+
if (api.runtime) {
|
|
3030
|
+
setLingyaoRuntime(api.runtime);
|
|
3031
|
+
}
|
|
3032
|
+
api.registerChannel({ plugin: lingyaoPlugin });
|
|
3033
|
+
}
|
|
3034
|
+
};
|
|
3035
|
+
var index_default = plugin;
|
|
3036
|
+
export {
|
|
3037
|
+
index_default as default
|
|
3038
|
+
};
|
|
3039
|
+
//# sourceMappingURL=setup-entry.js.map
|