@lingyao037/openclaw-lingyao-cli 1.3.2 → 1.3.4
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/cli.mjs +4 -5
- package/dist/index.d.ts +7 -1
- package/dist/index.js +81 -98
- package/dist/index.js.map +1 -1
- package/dist/setup-entry.js +8 -4
- package/dist/setup-entry.js.map +1 -1
- package/package.json +1 -1
package/dist/setup-entry.js
CHANGED
|
@@ -101,7 +101,10 @@ function createGatewayAdapter(getOrchestrator2) {
|
|
|
101
101
|
throw new Error("Orchestrator not initialized. Ensure setRuntime was called.");
|
|
102
102
|
}
|
|
103
103
|
ctx.log?.info(`Starting account "${ctx.accountId}"`);
|
|
104
|
-
await orchestrator2.start(ctx.account
|
|
104
|
+
await orchestrator2.start(ctx.account, {
|
|
105
|
+
channelRuntime: ctx.channelRuntime,
|
|
106
|
+
cfg: ctx.cfg
|
|
107
|
+
});
|
|
105
108
|
ctx.log?.info(`Account "${ctx.accountId}" started successfully`);
|
|
106
109
|
},
|
|
107
110
|
async stopAccount(ctx) {
|
|
@@ -356,6 +359,7 @@ function createSetupAdapter() {
|
|
|
356
359
|
// src/orchestrator.ts
|
|
357
360
|
import { hostname, networkInterfaces } from "os";
|
|
358
361
|
import { createHash } from "crypto";
|
|
362
|
+
import { dispatchInboundDirectDmWithRuntime } from "openclaw/plugin-sdk/channel-inbound";
|
|
359
363
|
|
|
360
364
|
// src/server-client.ts
|
|
361
365
|
import axios from "axios";
|
|
@@ -367,16 +371,16 @@ import WebSocket from "ws";
|
|
|
367
371
|
function getMachineId() {
|
|
368
372
|
try {
|
|
369
373
|
const interfaces = networkInterfaces();
|
|
370
|
-
const
|
|
374
|
+
const macSet = /* @__PURE__ */ new Set();
|
|
371
375
|
for (const iface of Object.values(interfaces)) {
|
|
372
376
|
if (!iface) continue;
|
|
373
377
|
for (const alias of iface) {
|
|
374
378
|
if (!alias.internal && alias.mac && alias.mac !== "00:00:00:00:00:00") {
|
|
375
|
-
|
|
379
|
+
macSet.add(alias.mac);
|
|
376
380
|
}
|
|
377
381
|
}
|
|
378
382
|
}
|
|
379
|
-
macs.sort();
|
|
383
|
+
const macs = [...macSet].sort();
|
|
380
384
|
if (macs.length > 0) {
|
|
381
385
|
return createHash("md5").update(macs.join("")).digest("hex").substring(0, 8);
|
|
382
386
|
}
|
package/dist/setup-entry.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/setup-entry.ts","../src/api.ts","../src/runtime.ts","../src/adapters/config.ts","../src/adapters/gateway.ts","../src/adapters/status.ts","../src/adapters/directory.ts","../src/adapters/messaging.ts","../src/adapters/outbound.ts","../src/adapters/setup.ts","../src/orchestrator.ts","../src/server-client.ts","../src/websocket-client.ts","../src/config-schema.ts"],"sourcesContent":["import { defineSetupPluginEntry } from 'openclaw/plugin-sdk/channel-core';\n\nimport { lingyaoPlugin } from './api.js';\n\nexport default defineSetupPluginEntry(lingyaoPlugin);\n","import type { PluginRuntime, ChannelPlugin } from 'openclaw/plugin-sdk';\nimport { createChatChannelPlugin } from 'openclaw/plugin-sdk/channel-core';\n\nimport { setRuntime, adaptPluginRuntime } from './runtime.js';\nimport { createConfigAdapter, type ResolvedAccount } from './adapters/config.js';\nimport { createGatewayAdapter } from './adapters/gateway.js';\nimport { createStatusAdapter } from './adapters/status.js';\nimport type { LingyaoProbeResult } from './adapters/status.js';\nimport { createDirectoryAdapter } from './adapters/directory.js';\nimport { createMessagingAdapter } from './adapters/messaging.js';\nimport { createOutboundAdapter } from './adapters/outbound.js';\nimport { createSetupAdapter } from './adapters/setup.js';\nimport { MultiAccountOrchestrator } from './orchestrator.js';\nimport { lingyaoChannelConfigSchema } from './config-schema.js';\n\nexport * from './types.js';\nexport type { AgentMessage } from './bot.js';\nexport { validateConfig, getDefaultConfig } from './config-schema.js';\n\nlet orchestrator: MultiAccountOrchestrator | null = null;\n\nfunction getOrchestrator(): MultiAccountOrchestrator | null {\n return orchestrator;\n}\n\nconst configAdapter = createConfigAdapter();\nconst setupAdapter = createSetupAdapter();\nconst messagingAdapter = createMessagingAdapter();\nconst gatewayAdapter = createGatewayAdapter(getOrchestrator);\nconst directoryAdapter = createDirectoryAdapter(getOrchestrator);\nconst outboundAdapter = createOutboundAdapter(getOrchestrator);\n\nconst securityOptions = {\n dm: {\n channelKey: 'lingyao',\n resolvePolicy: (account: ResolvedAccount) => account.dmPolicy,\n resolveAllowFrom: (account: ResolvedAccount) => account.allowFrom,\n defaultPolicy: 'pairing' as const,\n },\n};\n\nconst pairingOptions = {\n text: {\n idLabel: '设备 ID',\n message: '设备已批准配对',\n notify: async (params: { cfg: unknown; id: string }): Promise<void> => {\n const orc = getOrchestrator();\n if (!orc) return;\n\n const config = params.cfg as Record<string, unknown>;\n const channels = config.channels as Record<string, unknown> | undefined;\n const lingyao = channels?.lingyao as { accounts?: Record<string, unknown> } | undefined;\n const accountIds = lingyao?.accounts ? Object.keys(lingyao.accounts) : ['default'];\n\n for (const accountId of accountIds) {\n const sent = orc.sendNotification(accountId, params.id, {\n type: 'pairing_confirmed',\n message: pairingOptions.text.message,\n });\n if (sent) break;\n }\n },\n },\n};\n\nconst capabilities = {\n chatTypes: ['direct'] as ('direct' | 'group' | 'channel' | 'thread')[],\n media: false,\n reactions: false,\n threads: false,\n polls: false,\n edit: false,\n unsend: false,\n reply: false,\n effects: false,\n groupManagement: false,\n nativeCommands: false,\n blockStreaming: true,\n};\n\nconst meta = {\n id: 'lingyao',\n label: '灵爻',\n selectionLabel: '灵爻 (HarmonyOS)',\n docsPath: '/channels/lingyao',\n docsLabel: '灵爻文档',\n blurb: '通过 lingyao.live 服务器中转与鸿蒙灵爻 App 双向同步日记和记忆',\n order: 50,\n aliases: ['lingyao', '灵爻'],\n};\n\nexport const lingyaoPlugin: ChannelPlugin<ResolvedAccount, LingyaoProbeResult> = {\n ...createChatChannelPlugin<ResolvedAccount, LingyaoProbeResult>({\n base: {\n id: 'lingyao',\n meta,\n capabilities,\n configSchema: lingyaoChannelConfigSchema,\n config: configAdapter,\n setup: setupAdapter,\n status: createStatusAdapter(getOrchestrator),\n },\n security: securityOptions,\n pairing: pairingOptions,\n outbound: outboundAdapter,\n }),\n gateway: gatewayAdapter,\n directory: directoryAdapter,\n messaging: messagingAdapter,\n};\n\nexport function initializeLingyaoRuntime(runtime: PluginRuntime): void {\n const adapted = adaptPluginRuntime(runtime as Parameters<typeof adaptPluginRuntime>[0]);\n setRuntime(adapted);\n orchestrator = new MultiAccountOrchestrator(adapted);\n}\n","import type { LingyaoRuntime } from \"./types.js\";\nimport { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/**\n * Global runtime instance storage\n */\nlet globalRuntime: LingyaoRuntime | null = null;\n\n/**\n * Set the global runtime instance\n */\nexport function setRuntime(runtime: LingyaoRuntime): void {\n globalRuntime = runtime;\n}\n\n/**\n * Get the global runtime instance\n */\nexport function getRuntime(): LingyaoRuntime {\n if (!globalRuntime) {\n throw new Error(\"Runtime not initialized. Call setRuntime() first.\");\n }\n return globalRuntime;\n}\n\n/**\n * Check if runtime is initialized\n */\nexport function hasRuntime(): boolean {\n return globalRuntime !== null;\n}\n\n/**\n * Clear the global runtime instance\n */\nexport function clearRuntime(): void {\n globalRuntime = null;\n}\n\n/**\n * Adapt OpenClaw PluginRuntime to LingyaoRuntime.\n *\n * Bridges the SDK's PluginRuntime (logger, state dir) to the\n * internal LingyaoRuntime interface used by orchestrator/bot/ws.\n */\nexport function adaptPluginRuntime(pr: {\n logging?: { getChildLogger?: () => { info: (msg: string, ...args: unknown[]) => void; warn: (msg: string, ...args: unknown[]) => void; error: (msg: string, ...args: unknown[]) => void; debug?: (msg: string, ...args: unknown[]) => void } };\n state?: { resolveStateDir?: () => string };\n}): LingyaoRuntime {\n const noop = (..._args: unknown[]) => {};\n const rawLogger = pr.logging?.getChildLogger?.() ?? {\n info: console.info.bind(console),\n warn: console.warn.bind(console),\n error: console.error.bind(console),\n };\n const childLogger = {\n info: rawLogger.info,\n warn: rawLogger.warn,\n error: rawLogger.error,\n debug: rawLogger.debug ?? noop,\n };\n\n const stateDir = pr.state?.resolveStateDir?.() ?? join(process.cwd(), '.lingyao-data');\n const storeDir = join(stateDir, 'lingyao');\n\n return {\n config: { enabled: true },\n logger: childLogger,\n storage: {\n async get(key: string): Promise<unknown | null> {\n try {\n const filePath = join(storeDir, `${key}.json`);\n if (!existsSync(filePath)) return null;\n const data = readFileSync(filePath, 'utf-8');\n return JSON.parse(data);\n } catch {\n return null;\n }\n },\n async set(key: string, value: unknown): Promise<void> {\n if (!existsSync(storeDir)) {\n mkdirSync(storeDir, { recursive: true });\n }\n const filePath = join(storeDir, `${key}.json`);\n writeFileSync(filePath, JSON.stringify(value, null, 2), 'utf-8');\n },\n async delete(key: string): Promise<void> {\n const filePath = join(storeDir, `${key}.json`);\n if (existsSync(filePath)) {\n unlinkSync(filePath);\n }\n },\n },\n tools: {\n async call(): Promise<unknown> {\n throw new Error('Tool calls not available in SDK mode');\n },\n },\n };\n}\n","/**\n * Config Adapter - Account resolution, config validation\n *\n * serverUrl is NOT exposed to users — hardcoded as LINGYAO_SERVER_URL.\n */\n\nimport type { OpenClawConfig } from 'openclaw/plugin-sdk';\nimport type { LingyaoAccountConfig } from '../types.js';\n\ntype LingyaoDmPolicy = 'pairing' | 'allowlist' | 'open';\n\n/**\n * Resolved account after config resolution.\n */\nexport interface ResolvedAccount {\n readonly id: string;\n readonly accountId?: string | null;\n readonly enabled: boolean;\n readonly dmPolicy: LingyaoDmPolicy;\n readonly allowFrom: string[];\n readonly gatewayId?: string;\n readonly rawConfig: LingyaoAccountConfig;\n}\n\n/**\n * Normalize legacy Lingyao DM policy values to current OpenClaw semantics.\n *\n * `paired` was the old plugin-local name for the standard `pairing` mode.\n * `deny` had no SDK equivalent; map it to an empty `allowlist` so behavior\n * stays strict instead of silently opening access.\n */\nfunction normalizeDmPolicy(raw: unknown): LingyaoDmPolicy {\n switch (raw) {\n case 'pairing':\n case 'allowlist':\n case 'open':\n return raw;\n case 'paired':\n return 'pairing';\n case 'deny':\n return 'allowlist';\n default:\n return 'pairing';\n }\n}\n\nfunction extractChannelConfig(cfg: OpenClawConfig): Record<string, unknown> {\n const channels = (cfg as Record<string, unknown>)?.channels as Record<string, unknown> | undefined;\n return (channels?.lingyao as Record<string, unknown> | undefined) ?? {};\n}\n\n/**\n * Extract the Lingyao accounts from OpenClaw config.\n *\n * Lingyao supports top-level single-account fields and nested multi-account\n * fields. Nested accounts inherit unspecified values from the top-level\n * section so the UI and runtime agree on the effective account config.\n */\nfunction extractAccounts(cfg: OpenClawConfig): Record<string, LingyaoAccountConfig> {\n const lingyao = extractChannelConfig(cfg);\n const accounts = lingyao?.accounts as Record<string, LingyaoAccountConfig> | undefined;\n const baseConfig: LingyaoAccountConfig = {\n enabled: lingyao.enabled as boolean | undefined,\n dmPolicy: normalizeDmPolicy(lingyao.dmPolicy),\n allowFrom: Array.isArray(lingyao.allowFrom) ? (lingyao.allowFrom as string[]) : [],\n gatewayId: lingyao.gatewayId as string | undefined,\n websocketHeartbeatIntervalMs: lingyao.websocketHeartbeatIntervalMs as number | undefined,\n };\n\n if (!accounts || Object.keys(accounts).length === 0) {\n return { default: baseConfig };\n }\n\n return Object.fromEntries(\n Object.entries(accounts).map(([accountId, accountConfig]) => {\n const mergedAllowFrom =\n Array.isArray(accountConfig?.allowFrom) && accountConfig.allowFrom.length > 0\n ? accountConfig.allowFrom\n : baseConfig.allowFrom;\n\n const normalized: LingyaoAccountConfig = {\n ...baseConfig,\n ...accountConfig,\n dmPolicy: normalizeDmPolicy(accountConfig?.dmPolicy ?? baseConfig.dmPolicy),\n allowFrom: mergedAllowFrom,\n };\n\n return [accountId, normalized];\n })\n );\n}\n\n/**\n * Create the config adapter.\n */\nexport function createConfigAdapter() {\n return {\n listAccountIds(cfg: OpenClawConfig): string[] {\n const accounts = extractAccounts(cfg);\n const ids = Object.keys(accounts);\n return ids.length > 0 ? ids : ['default'];\n },\n\n resolveAccount(cfg: OpenClawConfig, accountId?: string | null): ResolvedAccount {\n const accounts = extractAccounts(cfg);\n const ids = Object.keys(accounts);\n const channelConfig = extractChannelConfig(cfg);\n const configuredDefaultAccount = channelConfig.defaultAccount as string | undefined;\n const resolvedId =\n accountId ?? configuredDefaultAccount ?? (ids.includes('default') ? 'default' : ids[0]);\n\n if (!resolvedId) {\n throw new Error('No lingyao accounts configured');\n }\n\n const accountConfig = accounts[resolvedId];\n if (!accountConfig) {\n throw new Error(`Account \"${resolvedId}\" not found`);\n }\n\n return {\n id: resolvedId,\n accountId: resolvedId,\n enabled: (accountConfig as Record<string, unknown>)?.enabled !== false,\n dmPolicy: normalizeDmPolicy((accountConfig as Record<string, unknown>)?.dmPolicy),\n allowFrom: ((accountConfig as Record<string, unknown>)?.allowFrom as string[]) ?? [],\n gatewayId: (accountConfig as Record<string, unknown>)?.gatewayId as string | undefined,\n rawConfig: accountConfig as LingyaoAccountConfig,\n };\n },\n\n isConfigured(_account: ResolvedAccount, _cfg: OpenClawConfig): boolean {\n return true;\n },\n\n isEnabled(account: ResolvedAccount, _cfg: OpenClawConfig): boolean {\n return account.enabled;\n },\n };\n}\n","/**\n * Gateway Adapter - Account lifecycle management\n *\n * Implements ChannelGatewayAdapter:\n * - startAccount: create WS/HTTP connections for an account\n * - stopAccount: tear down connections for an account\n *\n * Delegates to MultiAccountOrchestrator for all operations.\n */\n\nimport type { ChannelGatewayContext } from 'openclaw/plugin-sdk';\nimport type { MultiAccountOrchestrator } from '../orchestrator.js';\nimport type { ResolvedAccount } from './config.js';\n\n/**\n * Create the gateway adapter.\n */\nexport function createGatewayAdapter(\n getOrchestrator: () => MultiAccountOrchestrator | null\n) {\n return {\n async startAccount(ctx: ChannelGatewayContext<ResolvedAccount>): Promise<void> {\n const orchestrator = getOrchestrator();\n if (!orchestrator) {\n throw new Error('Orchestrator not initialized. Ensure setRuntime was called.');\n }\n\n ctx.log?.info(`Starting account \"${ctx.accountId}\"`);\n\n await orchestrator.start(ctx.account);\n\n ctx.log?.info(`Account \"${ctx.accountId}\" started successfully`);\n },\n\n async stopAccount(ctx: ChannelGatewayContext<ResolvedAccount>): Promise<void> {\n const orchestrator = getOrchestrator();\n if (!orchestrator) {\n throw new Error('Orchestrator not initialized');\n }\n\n ctx.log?.info(`Stopping account \"${ctx.accountId}\"`);\n\n await orchestrator.stop(ctx.accountId);\n },\n };\n}\n","import {\n buildBaseChannelStatusSummary,\n createDefaultChannelRuntimeState,\n} from 'openclaw/plugin-sdk/channel-status';\nimport { createComputedAccountStatusAdapter } from 'openclaw/plugin-sdk/status-helpers';\nimport type { MultiAccountOrchestrator } from '../orchestrator.js';\nimport type { ResolvedAccount } from './config.js';\n\n/**\n * Probe result structure.\n */\nexport interface LingyaoProbeResult {\n ok: boolean;\n status: 'healthy' | 'degraded' | 'unhealthy';\n wsConnected: boolean;\n uptime?: number;\n checks?: Record<string, { passed: boolean; message: string; duration?: number }>;\n error?: string;\n}\n\n/**\n * Create the status adapter.\n */\nexport function createStatusAdapter(\n getOrchestrator: () => MultiAccountOrchestrator | null,\n _runtime?: unknown\n): ReturnType<typeof createComputedAccountStatusAdapter<ResolvedAccount, LingyaoProbeResult>> {\n return createComputedAccountStatusAdapter<ResolvedAccount, LingyaoProbeResult>({\n defaultRuntime: createDefaultChannelRuntimeState('default'),\n async probeAccount(params: {\n account: ResolvedAccount;\n timeoutMs: number;\n cfg: import('openclaw/plugin-sdk').OpenClawConfig;\n }): Promise<LingyaoProbeResult> {\n const orchestrator = getOrchestrator();\n if (!orchestrator) {\n return {\n ok: false,\n status: 'unhealthy',\n wsConnected: false,\n error: 'Orchestrator not initialized',\n };\n }\n\n const state = orchestrator.getAccountState(params.account.id);\n if (!state) {\n return {\n ok: false,\n status: 'unhealthy',\n wsConnected: false,\n error: 'Account not started',\n };\n }\n\n try {\n const healthResult = await state.probe.runHealthChecks();\n const wsConnected = state.wsClient?.isConnected() ?? false;\n\n let status: 'healthy' | 'degraded' | 'unhealthy' = 'healthy';\n switch (healthResult.status) {\n case 'healthy':\n status = wsConnected ? 'healthy' : 'degraded';\n break;\n case 'degraded':\n status = 'degraded';\n break;\n case 'unhealthy':\n status = 'unhealthy';\n break;\n }\n\n const checks: Record<string, { passed: boolean; message: string; duration?: number }> = {};\n for (const [name, result] of healthResult.checks) {\n checks[name] = {\n passed: result.passed,\n message: result.message ?? '',\n duration: result.duration,\n };\n }\n\n return {\n ok: status !== 'unhealthy',\n status,\n wsConnected,\n uptime: Date.now() - state.startTime,\n checks,\n };\n } catch (error) {\n return {\n ok: false,\n status: 'unhealthy',\n wsConnected: false,\n error: (error as Error).message,\n };\n }\n },\n\n buildChannelSummary({ snapshot }: { snapshot: any }): Record<string, unknown> {\n return buildBaseChannelStatusSummary(snapshot, {\n connected: snapshot.connected ?? false,\n healthState: snapshot.healthState ?? 'unhealthy',\n dmPolicy: snapshot.dmPolicy ?? 'pairing',\n });\n },\n\n resolveAccountSnapshot({ account, probe }: { account: ResolvedAccount; probe?: LingyaoProbeResult }) {\n const orchestrator = getOrchestrator();\n const state = orchestrator?.getAccountState(account.id);\n const connected = state?.wsClient?.isConnected() ?? false;\n const normalizedProbe = probe ?? {\n ok: false,\n status: 'unhealthy' as const,\n wsConnected: false,\n error: 'Probe not available',\n };\n\n return {\n accountId: account.id,\n enabled: account.enabled,\n configured: true,\n extra: {\n connected,\n healthState: normalizedProbe.status,\n dmPolicy: account.dmPolicy,\n allowFrom: account.allowFrom,\n gatewayId: account.gatewayId ?? state?.gatewayId ?? null,\n },\n };\n },\n });\n}\n","/**\n * Directory Adapter - List paired devices (peers)\n *\n * Implements ChannelDirectoryAdapter:\n * - listPeers: return all active paired devices for an account\n */\n\nimport type { OpenClawConfig } from 'openclaw/plugin-sdk';\nimport type { MultiAccountOrchestrator } from '../orchestrator.js';\n\n/**\n * Directory types (not exported by SDK, defined locally to match contract).\n */\nexport interface ChannelDirectoryEntry {\n kind: 'user' | 'group' | 'channel';\n id: string;\n name?: string;\n handle?: string;\n avatarUrl?: string;\n rank?: number;\n raw?: unknown;\n}\n\nexport interface ChannelDirectoryListParams {\n cfg: OpenClawConfig;\n accountId?: string | null;\n query?: string | null;\n limit?: number | null;\n runtime: unknown;\n}\n\n/**\n * Create the directory adapter.\n */\nexport function createDirectoryAdapter(\n getOrchestrator: () => MultiAccountOrchestrator | null\n) {\n return {\n async listPeers(params: ChannelDirectoryListParams): Promise<ChannelDirectoryEntry[]> {\n const orchestrator = getOrchestrator();\n if (!orchestrator) {\n return [];\n }\n\n const accountId = params.accountId ?? 'default';\n const accountManager = orchestrator.getAccountManager(accountId);\n if (!accountManager) {\n return [];\n }\n\n const activeAccounts = accountManager.getActiveAccounts();\n\n let entries: ChannelDirectoryEntry[] = activeAccounts.map(account => ({\n kind: 'user' as const,\n id: account.deviceId,\n name: account.deviceInfo.name || account.deviceId,\n handle: account.deviceId,\n raw: account,\n }));\n\n if (params.query) {\n const q = params.query.toLowerCase();\n entries = entries.filter(\n e => e.id.toLowerCase().includes(q) || (e.name?.toLowerCase().includes(q) ?? false)\n );\n }\n\n if (params.limit != null && params.limit > 0) {\n entries = entries.slice(0, params.limit);\n }\n\n return entries;\n },\n };\n}\n","/**\n * Messaging Adapter - Target normalization and session resolution\n *\n * Implements ChannelMessagingAdapter:\n * - normalizeTarget: strip \"lingyao:\" prefix, return pure deviceId\n * - resolveSessionTarget: return \"lingyao:{id}\" format\n * - resolveSessionConversation: map rawId to conversation structure\n * - inferTargetChatType: always \"direct\" (Lingyao has no groups)\n */\n\nconst PREFIX = 'lingyao:';\n\nexport function createMessagingAdapter() {\n return {\n normalizeTarget(raw: string): string | undefined {\n if (!raw || typeof raw !== 'string') {\n return undefined;\n }\n\n const target = raw.startsWith(PREFIX)\n ? raw.slice(PREFIX.length)\n : raw;\n\n if (!target) {\n return undefined;\n }\n\n return target;\n },\n\n resolveSessionTarget(params: {\n kind: 'direct' | 'group' | 'channel';\n id: string;\n threadId?: string | null;\n }): string | undefined {\n return `${PREFIX}${params.id}`;\n },\n\n resolveSessionConversation(params: {\n kind: 'group' | 'channel';\n rawId: string;\n }): {\n id: string;\n threadId?: string | null;\n baseConversationId?: string | null;\n parentConversationCandidates?: string[];\n } | null {\n // Lingyao has no group/channel encoding in rawId — treat as passthrough\n return {\n id: params.rawId,\n baseConversationId: params.rawId,\n };\n },\n\n inferTargetChatType(_params: {\n to: string;\n }): 'direct' | undefined {\n return 'direct';\n },\n };\n}\n","/**\n * Outbound Adapter - Send messages from Agent to App devices\n *\n * Implements ChannelOutboundAdapter:\n * - deliveryMode: \"direct\" (synchronous via WS)\n * - sendText: send plain text notification\n * - sendPayload: send structured payload notification\n * - resolveTarget: validate and resolve target deviceId\n */\n\nimport type { OpenClawConfig } from 'openclaw/plugin-sdk';\nimport type { MultiAccountOrchestrator } from '../orchestrator.js';\n\n/**\n * Outbound types (not exported by SDK, defined locally to match contract).\n */\nexport interface ChannelOutboundContext {\n cfg: OpenClawConfig;\n to: string;\n text: string;\n mediaUrl?: string;\n accountId?: string | null;\n silent?: boolean;\n}\n\nexport interface ChannelOutboundPayloadContext extends ChannelOutboundContext {\n payload: unknown;\n}\n\nexport interface OutboundDeliveryResult {\n channel: string;\n messageId: string;\n chatId?: string;\n timestamp?: number;\n meta?: Record<string, unknown>;\n}\n\n/**\n * Create the outbound adapter.\n */\nexport function createOutboundAdapter(\n getOrchestrator: () => MultiAccountOrchestrator | null\n) {\n return {\n deliveryMode: 'direct' as const,\n\n async sendText(ctx: ChannelOutboundContext): Promise<OutboundDeliveryResult> {\n const orchestrator = getOrchestrator();\n if (!orchestrator) {\n throw new Error('Orchestrator not initialized');\n }\n\n const accountId = ctx.accountId ?? 'default';\n const sent = orchestrator.sendNotification(\n accountId,\n ctx.to,\n { title: 'OpenClaw', body: ctx.text }\n );\n\n if (!sent) {\n throw new Error(\n `Failed to send text to device \"${ctx.to}\" on account \"${accountId}\": not connected`\n );\n }\n\n return {\n channel: 'lingyao',\n messageId: `out_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,\n chatId: ctx.to,\n timestamp: Date.now(),\n };\n },\n\n async sendPayload(ctx: ChannelOutboundPayloadContext): Promise<OutboundDeliveryResult> {\n const orchestrator = getOrchestrator();\n if (!orchestrator) {\n throw new Error('Orchestrator not initialized');\n }\n\n const accountId = ctx.accountId ?? 'default';\n const sent = orchestrator.sendNotification(\n accountId,\n ctx.to,\n ctx.payload as Record<string, unknown>\n );\n\n if (!sent) {\n throw new Error(\n `Failed to send payload to device \"${ctx.to}\" on account \"${accountId}\": not connected`\n );\n }\n\n return {\n channel: 'lingyao',\n messageId: `out_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,\n chatId: ctx.to,\n timestamp: Date.now(),\n };\n },\n\n resolveTarget(params: {\n cfg?: OpenClawConfig;\n to?: string;\n accountId?: string | null;\n }): { ok: true; to: string } | { ok: false; error: Error } {\n const raw = params.to;\n if (!raw || typeof raw !== 'string' || raw.trim().length === 0) {\n return { ok: false, error: new Error('Target deviceId is empty or missing') };\n }\n return { ok: true, to: raw.trim() };\n },\n };\n}\n","/**\n * Setup adapter for Lingyao channel plugin.\n *\n * Lingyao has no tokens or bot credentials — setup simply enables\n * the channel and the default account. DM policy defaults to \"pairing\".\n */\n\nimport { createPatchedAccountSetupAdapter } from 'openclaw/plugin-sdk/setup-runtime';\nimport type { ChannelSetupAdapter } from 'openclaw/plugin-sdk';\n\nexport function createSetupAdapter(): ChannelSetupAdapter {\n return createPatchedAccountSetupAdapter({\n channelKey: 'lingyao',\n alwaysUseAccounts: true,\n ensureChannelEnabled: true,\n ensureAccountEnabled: true,\n buildPatch() {\n // Lingyao uses device pairing via relay server, no credentials needed.\n // The default dmPolicy (\"pairing\") and empty allowFrom are sufficient.\n return {};\n },\n });\n}\n","/**\n * Multi-Account Orchestrator\n *\n * Manages per-account WS/HTTP instances for multi-account support.\n * Each account gets independent LingyaoWSClient, ServerHttpClient,\n * AccountManager, MessageProcessor, Probe, and Monitor instances.\n *\n * Data flow:\n * Gateway Adapter → orchestrator.start(account) → WS connect\n * Inbound (App) → WS message → orchestrator → MessageProcessor → Agent\n * Outbound (Agent)→ orchestrator.sendNotification() → WS → App\n */\n\nimport { hostname, networkInterfaces } from 'node:os';\nimport { createHash } from 'node:crypto';\nimport { LINGYAO_SERVER_URL, getLingyaoGatewayWsUrl } from './types.js';\nimport { resolveLingyaoWsHeartbeatIntervalMs } from './ws-heartbeat-interval.js';\nimport type { LingyaoRuntime } from './types.js';\nimport type { ResolvedAccount } from './adapters/config.js';\nimport { ServerHttpClient } from './server-client.js';\nimport { LingyaoWSClient } from './websocket-client.js';\nimport { AccountManager } from './accounts.js';\nimport { MessageProcessor, type AgentMessage } from './bot.js';\nimport { Probe } from './probe.js';\nimport { Monitor, MonitoringEvent } from './metrics.js';\nimport { ErrorHandler } from './errors.js';\n\n/**\n * Per-account runtime state\n */\ninterface AccountState {\n accountId: string;\n wsClient: LingyaoWSClient | null;\n httpClient: ServerHttpClient | null;\n accountManager: AccountManager;\n messageProcessor: MessageProcessor | null;\n probe: Probe;\n monitor: Monitor;\n errorHandler: ErrorHandler;\n status: 'stopped' | 'starting' | 'running' | 'stopping' | 'error';\n startTime: number;\n gatewayId: string;\n /** One automatic clear+re-register after WS HTTP 404 (avoid infinite loops). */\n wsHandshake404RecoveryAttempted?: boolean;\n}\n\n/**\n * 获取机器的稳定标识符 (基于 MAC 地址)\n */\nfunction getMachineId(): string {\n try {\n const interfaces = networkInterfaces();\n const macs: string[] = [];\n\n // 收集所有非内部回环的 MAC 地址,排序保证跨重启稳定\n for (const iface of Object.values(interfaces)) {\n if (!iface) continue;\n for (const alias of iface) {\n if (!alias.internal && alias.mac && alias.mac !== '00:00:00:00:00:00') {\n macs.push(alias.mac);\n }\n }\n }\n macs.sort();\n \n if (macs.length > 0) {\n // 排序后的 MAC 地址拼接,取 MD5 前 8 位作为机器稳定标识\n return createHash('md5').update(macs.join('')).digest('hex').substring(0, 8);\n }\n } catch (e) {\n // 忽略获取网卡失败的错误\n }\n \n // 回退:使用随机后缀(由于在内存中缓存,单次运行期间稳定)\n return Math.random().toString(36).substring(2, 10);\n}\n\n// 缓存 machineId,避免频繁计算\nconst MACHINE_ID = getMachineId();\n\n/**\n * Generate a gateway ID for an account\n * 使用主机名 + 机器稳定标识(MAC哈希) + 账户ID,确保跨重启稳定\n */\nfunction generateGatewayId(accountId: string): string {\n const host = hostname().split('.')[0].replace(/[^a-z0-9]/gi, '').toLowerCase();\n return `gw_openclaw_${host}_${MACHINE_ID}_${accountId}`;\n}\n\nexport class MultiAccountOrchestrator {\n private runtime: LingyaoRuntime;\n private accounts: Map<string, AccountState> = new Map();\n private messageHandler: ((message: AgentMessage) => void | Promise<void>) | null = null;\n\n constructor(runtime: LingyaoRuntime) {\n this.runtime = runtime;\n }\n\n /**\n * Set the message handler for delivering messages to the Agent.\n * Propagates to all running accounts.\n */\n setMessageHandler(handler: (message: AgentMessage) => void | Promise<void>): void {\n this.messageHandler = handler;\n for (const state of this.accounts.values()) {\n if (state.messageProcessor) {\n state.messageProcessor.setMessageHandler(handler);\n }\n }\n }\n\n /**\n * Get the current message handler (for gateway adapter injection).\n */\n getMessageHandler(): ((message: AgentMessage) => void | Promise<void>) | null {\n return this.messageHandler;\n }\n\n /**\n * Start an account: create components, register to server, connect WS.\n */\n async start(account: ResolvedAccount): Promise<void> {\n const { id: accountId } = account;\n\n const existing = this.accounts.get(accountId);\n if (existing?.status === 'running') {\n this.runtime.logger.warn(`Account \"${accountId}\" is already running`);\n return;\n }\n\n this.runtime.logger.info(`Starting account \"${accountId}\"`);\n\n const gatewayId = account.gatewayId ?? generateGatewayId(accountId);\n const storagePrefix = `lingyao:${accountId}`;\n\n const accountManager = new AccountManager(this.runtime);\n const messageProcessor = new MessageProcessor(this.runtime);\n const probe = new Probe(this.runtime);\n const monitor = new Monitor(this.runtime);\n const errorHandler = new ErrorHandler(this.runtime);\n const httpClient = new ServerHttpClient(\n this.runtime,\n gatewayId,\n { baseURL: LINGYAO_SERVER_URL },\n storagePrefix\n );\n\n const state: AccountState = {\n accountId,\n wsClient: null,\n httpClient,\n accountManager,\n messageProcessor,\n probe,\n monitor,\n errorHandler,\n status: 'starting',\n startTime: Date.now(),\n gatewayId,\n };\n\n this.accounts.set(accountId, state);\n\n try {\n await accountManager.initialize();\n await messageProcessor.initialize();\n\n if (this.messageHandler) {\n messageProcessor.setMessageHandler(this.messageHandler);\n }\n\n // Register to server (restore from storage or fresh register)\n await this.registerToServer(state);\n\n const wsHeartbeatMs = resolveLingyaoWsHeartbeatIntervalMs(account, httpClient);\n this.runtime.logger.info(`Lingyao WebSocket heartbeat interval: ${wsHeartbeatMs}ms (relay serverConfig / config)`);\n\n // Create and connect WebSocket client\n const wsClient = new LingyaoWSClient(this.runtime, {\n url: getLingyaoGatewayWsUrl(),\n gatewayId,\n token: httpClient.getGatewayToken() ?? undefined,\n reconnectInterval: 5000,\n heartbeatInterval: wsHeartbeatMs,\n messageHandler: this.createMessageHandler(state),\n eventHandler: this.createEventHandler(state),\n });\n\n await wsClient.connect();\n state.wsClient = wsClient;\n state.status = 'running';\n\n this.runtime.logger.info(`Account \"${accountId}\" started successfully`);\n } catch (error) {\n state.status = 'error';\n this.runtime.logger.error(`Failed to start account \"${accountId}\"`, error);\n throw error;\n }\n }\n\n /**\n * Stop an account: disconnect WS, stop heartbeat.\n */\n async stop(accountId: string): Promise<void> {\n const state = this.accounts.get(accountId);\n if (!state || state.status === 'stopped') {\n return;\n }\n\n this.runtime.logger.info(`Stopping account \"${accountId}\"`);\n state.status = 'stopping';\n\n if (state.wsClient) {\n state.wsClient.disconnect();\n state.wsClient = null;\n }\n\n if (state.httpClient) {\n state.httpClient.stopHeartbeat();\n }\n\n state.status = 'stopped';\n this.runtime.logger.info(`Account \"${accountId}\" stopped`);\n }\n\n /**\n * Stop all running accounts.\n */\n async stopAll(): Promise<void> {\n const stops = Array.from(this.accounts.keys()).map(id => this.stop(id));\n await Promise.all(stops);\n }\n\n /**\n * Get account state by ID.\n */\n getAccountState(accountId: string): AccountState | undefined {\n return this.accounts.get(accountId);\n }\n\n /**\n * Get account's WS client.\n */\n getWSClient(accountId: string): LingyaoWSClient | null {\n return this.accounts.get(accountId)?.wsClient ?? null;\n }\n\n /**\n * Get account's HTTP client.\n */\n getHttpClient(accountId: string): ServerHttpClient | null {\n return this.accounts.get(accountId)?.httpClient ?? null;\n }\n\n /**\n * Get account's AccountManager.\n */\n getAccountManager(accountId: string): AccountManager | null {\n return this.accounts.get(accountId)?.accountManager ?? null;\n }\n\n /**\n * Get account's Probe.\n */\n getProbe(accountId: string): Probe | null {\n return this.accounts.get(accountId)?.probe ?? null;\n }\n\n /**\n * Get account's Monitor.\n */\n getMonitor(accountId: string): Monitor | null {\n return this.accounts.get(accountId)?.monitor ?? null;\n }\n\n /**\n * Send notification to a device on a specific account.\n * Returns true if sent, false if WS not connected.\n */\n sendNotification(\n accountId: string,\n deviceId: string,\n notification: Record<string, unknown>\n ): boolean {\n const wsClient = this.getWSClient(accountId);\n if (!wsClient || !wsClient.isConnected()) {\n this.runtime.logger.warn(`[${accountId}] Cannot send notification: WS not connected`, { deviceId });\n return false;\n }\n\n this.runtime.logger.info(`[${accountId}] Sending notification to device`, {\n deviceId,\n notificationType: notification.type,\n });\n\n wsClient.sendNotification(deviceId, notification);\n return true;\n }\n\n /**\n * List all running account IDs.\n */\n getRunningAccountIds(): string[] {\n return Array.from(this.accounts.entries())\n .filter(([, state]) => state.status === 'running')\n .map(([id]) => id);\n }\n\n /**\n * Register to lingyao server for a specific account.\n */\n private async registerToServer(state: AccountState): Promise<void> {\n if (!state.httpClient) {\n throw new Error('HTTP client not available');\n }\n\n try {\n const restored = await state.httpClient.restoreFromStorage();\n if (restored) {\n this.runtime.logger.info(`Account \"${state.accountId}\": session restored from storage`);\n return;\n }\n\n this.runtime.logger.info(`Account \"${state.accountId}\": registering to server...`, {\n gatewayId: state.gatewayId,\n });\n\n const response = await state.httpClient.register({\n websocket: true,\n compression: false,\n maxMessageSize: 1048576,\n });\n\n this.runtime.logger.info(`Account \"${state.accountId}\": registered successfully`, {\n expiresAt: new Date(response.expiresAt).toISOString(),\n });\n } catch (error) {\n this.runtime.logger.error(`Account \"${state.accountId}\": registration failed`, error);\n // Don't throw - WS client will attempt connection anyway and auto-reconnect\n }\n }\n\n /**\n * Create message handler for inbound App messages on a specific account.\n */\n private createMessageHandler(state: AccountState) {\n return async (message: any): Promise<void> => {\n try {\n const appMessage = message.payload;\n const deviceId = appMessage.deviceId;\n const msg = appMessage.message;\n\n this.runtime.logger.info(`[${state.accountId}] Received message from App`, {\n deviceId,\n messageType: msg.type,\n messageId: msg.id,\n });\n\n state.monitor.recordEvent(MonitoringEvent.MESSAGE_RECEIVED, {\n deviceId,\n messageType: msg.type,\n });\n\n switch (msg.type) {\n case 'sync_diary':\n case 'sync_memory':\n await this.handleSyncMessage(state, deviceId, msg);\n break;\n case 'heartbeat':\n await state.accountManager.updateLastSeen(deviceId);\n state.monitor.recordEvent(MonitoringEvent.HEARTBEAT_RECEIVED, { deviceId });\n break;\n default:\n this.runtime.logger.warn(`[${state.accountId}] Unknown message type`, { type: msg.type });\n }\n } catch (error) {\n this.runtime.logger.error(`[${state.accountId}] Error handling App message`, error);\n state.monitor.recordEvent(MonitoringEvent.ERROR_OCCURRED, {\n errorType: 'message_handling',\n });\n }\n };\n }\n\n /**\n * Handle sync message (diary or memory).\n */\n private async handleSyncMessage(\n state: AccountState,\n deviceId: string,\n message: {\n id: string;\n type: 'sync_diary' | 'sync_memory';\n timestamp: number;\n content: string;\n metadata?: Record<string, unknown>;\n }\n ): Promise<void> {\n if (!state.messageProcessor) {\n this.runtime.logger.warn(`[${state.accountId}] Message processor not initialized, dropping message`, {\n messageId: message.id,\n messageType: message.type,\n deviceId,\n });\n return;\n }\n\n this.runtime.logger.info(`[${state.accountId}] Routing sync message to Agent`, {\n messageId: message.id,\n messageType: message.type,\n deviceId,\n contentLength: message.content?.length ?? 0,\n });\n\n const agentMessage: AgentMessage = {\n id: message.id,\n type: message.type === 'sync_diary' ? 'diary' : 'memory',\n from: deviceId,\n deviceId,\n content: message.content,\n metadata: message.metadata || {},\n timestamp: message.timestamp,\n };\n\n await state.messageProcessor.deliverToAgent(agentMessage);\n }\n\n /**\n * Create event handler for WS connection events on a specific account.\n */\n private createEventHandler(state: AccountState) {\n return (event: any): void => {\n switch (event.type) {\n case 'connected':\n this.runtime.logger.info(`[${state.accountId}] WS connected`, {\n connectionId: event.connectionId,\n });\n state.monitor.recordEvent(MonitoringEvent.CONNECTION_OPEN, {\n connectionId: event.connectionId,\n });\n break;\n case 'disconnected':\n this.runtime.logger.warn(`[${state.accountId}] WS disconnected`, {\n code: event.code,\n reason: event.reason,\n });\n state.monitor.recordEvent(MonitoringEvent.CONNECTION_CLOSE, {\n code: event.code,\n reason: event.reason,\n });\n\n // 如果是因为 Token 无效导致的断开,触发重新注册和重连\n if (event.code === 1008) {\n this.runtime.logger.warn(`[${state.accountId}] Token invalid (1008). Forcing re-registration...`);\n this.handleInvalidToken(state).catch(err => {\n this.runtime.logger.error(`[${state.accountId}] Failed to re-register after 1008`, err);\n });\n }\n break;\n case 'error':\n this.runtime.logger.error(`[${state.accountId}] WS error`, event.error);\n state.probe.recordError(event.error.message, 'websocket');\n state.monitor.recordEvent(MonitoringEvent.CONNECTION_ERROR, {\n error: event.error,\n });\n state.errorHandler.handleError(event.error);\n break;\n case 'fatal_handshake':\n if (event.reason === 'http_404') {\n void this.handleWsHandshake404(state);\n }\n break;\n case 'pairing_completed':\n this.handlePairingCompleted(state, event);\n break;\n }\n };\n }\n\n /**\n * Handle pairing completed event from WS — auto-bind device.\n */\n private async handlePairingCompleted(state: AccountState, event: any): Promise<void> {\n const { deviceId, deviceInfo } = event;\n this.runtime.logger.info(`[${state.accountId}] Pairing completed, auto-binding device`, { deviceId, deviceInfo });\n\n try {\n await state.accountManager.addDevice(deviceId, {\n name: deviceInfo?.name ?? deviceId,\n platform: deviceInfo?.platform ?? 'harmonyos',\n version: deviceInfo?.version ?? '',\n });\n this.runtime.logger.info(`[${state.accountId}] Device auto-bound: ${deviceId}`);\n } catch (error) {\n this.runtime.logger.error(`[${state.accountId}] Failed to auto-bind device: ${deviceId}`, error);\n }\n }\n\n /**\n * After HTTP 404 on WebSocket upgrade: clear local tokens, re-register once, reconnect.\n * Does not fix wrong server URL or permanently deleted gateway rows (user must fix config).\n */\n private async handleWsHandshake404(state: AccountState): Promise<void> {\n if (state.wsHandshake404RecoveryAttempted) {\n this.runtime.logger.error(\n `[${state.accountId}] Lingyao WebSocket still failing after one recovery attempt. ` +\n `Check that wss://…/lyoc/gateway/ws is deployed; if the gateway was removed, delete ` +\n `\\`channels.lingyao.accounts.${state.accountId}.gatewayId\\` or use a new account id, then restart.`\n );\n return;\n }\n state.wsHandshake404RecoveryAttempted = true;\n\n const { httpClient, wsClient, accountId } = state;\n if (!httpClient || !wsClient) {\n return;\n }\n\n this.runtime.logger.info(`[${accountId}] WS HTTP 404 — clearing local session and re-registering...`);\n\n try {\n await httpClient.clearLocalSession();\n const response = await httpClient.register({\n websocket: true,\n compression: false,\n maxMessageSize: 1048576,\n });\n wsClient.updateToken(response.gatewayToken);\n await wsClient.connect();\n this.runtime.logger.info(`[${accountId}] Re-register OK; WebSocket reconnect issued.`);\n } catch (error) {\n const msg = error instanceof Error ? error.message : String(error);\n if (msg.includes('already registered')) {\n this.runtime.logger.error(\n `[${accountId}] Re-register failed: gateway still registered on server but local session was cleared. ` +\n `Remove or change \\`gatewayId\\` under this account, or reset the gateway on lingyao.live, then restart OpenClaw.`,\n error\n );\n } else {\n this.runtime.logger.error(`[${accountId}] WS 404 recovery (clear + register) failed`, error);\n }\n }\n }\n\n /**\n * Handle invalid token by re-registering the gateway and reconnecting WS\n */\n private async handleInvalidToken(state: AccountState): Promise<void> {\n if (!state.httpClient || !state.wsClient) {\n return;\n }\n\n try {\n this.runtime.logger.info(`[${state.accountId}] Requesting new gateway token...`);\n // 强制重新注册以获取新 token\n const response = await state.httpClient.register({\n websocket: true,\n compression: false,\n maxMessageSize: 1048576,\n });\n\n this.runtime.logger.info(`[${state.accountId}] Obtained new token. Reconnecting WS...`);\n // 更新 WS 客户端的 token 并重新连接\n state.wsClient.updateToken(response.gatewayToken);\n await state.wsClient.connect();\n } catch (error) {\n this.runtime.logger.error(`[${state.accountId}] Failed to handle invalid token`, error);\n // 可以在此处接入 errorHandler 以实现更高级的退避重试\n state.errorHandler.handleError(error as Error);\n }\n }\n}\n","/**\n * 灵爻服务器 HTTP 客户端\n *\n * 主动连接到灵爻服务器,实现 Gateway 注册、心跳、消息推送\n * 符合 openapi.yaml 规范\n */\n\nimport axios from 'axios';\nimport type { LingyaoRuntime } from './types.js';\n\n// 类型定义 - 避免 axios 类型导出在 DTS 构建中的问题\ntype AxiosInstance = ReturnType<typeof axios.create>;\n\ninterface AxiosErrorLike extends Error {\n response?: {\n status?: number;\n data?: any;\n };\n config?: any;\n code?: string;\n isAxiosError?: boolean;\n}\n\n/**\n * axios 错误类型守卫\n */\nfunction isAxiosError(error: unknown): error is AxiosErrorLike {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'isAxiosError' in error &&\n (error as AxiosErrorLike).isAxiosError === true\n );\n}\n\n/**\n * 服务器 API 配置\n */\nexport interface ServerConfig {\n baseURL: string;\n apiBase: string;\n timeout: number;\n connectionTimeout: number;\n}\n\n/**\n * Gateway 注册响应\n */\nexport interface GatewayRegisterResponse {\n gatewayToken: string;\n expiresAt: number;\n webhookSecret: string;\n serverConfig: {\n heartbeatInterval: number;\n maxOfflineMessages: number;\n supportedMessageTypes: string[];\n };\n}\n\n/**\n * 心跳响应\n */\nexport interface HeartbeatResponse {\n serverTime: number;\n pendingMessages: number;\n}\n\n/**\n * 发送消息请求\n */\nexport interface SendMessageRequest {\n deviceId: string;\n message: {\n id: string;\n type: 'notify_text' | 'notify_action';\n timestamp: number;\n payload: {\n title?: string;\n body?: string;\n action?: {\n type: 'open_memory' | 'view_diary' | 'custom';\n params?: Record<string, unknown>;\n };\n };\n };\n options?: {\n priority?: 'normal' | 'low' | 'high';\n ttl?: number;\n };\n}\n\n/**\n * 发送消息响应\n */\nexport interface SendMessageResponse {\n messageId: string;\n status: 'queued' | 'delivered' | 'failed';\n deliveredAt: number | null;\n queued: boolean;\n}\n\n/**\n * Gateway 状态\n */\nexport type GatewayStatus = 'online' | 'offline' | 'maintenance';\n\n/**\n * HTTP 客户端实现\n */\nexport class ServerHttpClient {\n private runtime: LingyaoRuntime;\n private config: ServerConfig;\n private axiosInstance: AxiosInstance;\n private gatewayToken: string | null = null;\n private webhookSecret: string | null = null;\n private tokenExpiresAt: number = 0;\n private heartbeatInterval: number = 30000;\n private heartbeatTimer: NodeJS.Timeout | null = null;\n private gatewayId: string;\n private isRegistered: boolean = false;\n private isConnecting: boolean = false;\n\n private storagePrefix: string;\n\n constructor(\n runtime: LingyaoRuntime,\n gatewayId: string,\n serverConfig: Partial<ServerConfig> = {},\n storagePrefix: string = 'lingyao'\n ) {\n this.runtime = runtime;\n this.gatewayId = gatewayId;\n this.storagePrefix = storagePrefix;\n this.config = {\n baseURL: serverConfig.baseURL || 'https://api.lingyao.live',\n // Public API (api.lingyao.live) serves gateway HTTP under /lyoc; local relay also accepts /lyoc (see server getApiPathSuffix).\n apiBase: serverConfig.apiBase || '/lyoc',\n timeout: serverConfig.timeout || 30000,\n connectionTimeout: serverConfig.connectionTimeout || 5000,\n };\n\n // 创建 axios 实例\n this.axiosInstance = axios.create({\n baseURL: this.config.baseURL + this.config.apiBase,\n timeout: this.config.timeout,\n headers: {\n 'Content-Type': 'application/json',\n },\n });\n\n // 请求拦截器 - 添加 Token\n this.axiosInstance.interceptors.request.use(\n (config) => {\n if (this.gatewayToken && config.headers) {\n config.headers.Authorization = `Bearer ${this.gatewayToken}`;\n }\n return config;\n },\n (error) => Promise.reject(error)\n );\n\n // 响应拦截器 - 处理错误\n this.axiosInstance.interceptors.response.use(\n (response) => response,\n async (error: unknown) => {\n if (isAxiosError(error)) {\n if (error.response?.status === 401) {\n // Token 过期,尝试刷新\n this.runtime.logger.warn('Token expired, attempting to re-register...');\n await this.register();\n }\n }\n return Promise.reject(error);\n }\n );\n }\n\n /**\n * 注册 Gateway 到服务器\n */\n async register(\n capabilities: {\n websocket?: boolean;\n compression?: boolean;\n maxMessageSize?: number;\n } = {}\n ): Promise<GatewayRegisterResponse> {\n if (this.isConnecting) {\n throw new Error('Registration already in progress');\n }\n\n this.isConnecting = true;\n\n try {\n this.runtime.logger.info(`Registering gateway ${this.gatewayId} to server...`);\n\n const response = await this.axiosInstance.post<GatewayRegisterResponse>(\n '/gateway/register',\n {\n gatewayId: this.gatewayId,\n version: '0.1.0',\n capabilities: {\n websocket: false,\n compression: false,\n ...capabilities,\n },\n }\n );\n\n const data = response.data;\n\n this.gatewayToken = data.gatewayToken;\n this.webhookSecret = data.webhookSecret;\n this.tokenExpiresAt = data.expiresAt;\n this.heartbeatInterval = data.serverConfig.heartbeatInterval;\n this.isRegistered = true;\n\n // 保存到存储\n await this.runtime.storage.set(this.storageKey('gatewayToken'), this.gatewayToken);\n await this.runtime.storage.set(this.storageKey('webhookSecret'), this.webhookSecret);\n await this.runtime.storage.set(this.storageKey('tokenExpiresAt'), this.tokenExpiresAt);\n await this.runtime.storage.set(this.storageKey('serverConfig'), data.serverConfig);\n\n this.runtime.logger.info('Gateway registered successfully', {\n expiresAt: new Date(this.tokenExpiresAt).toISOString(),\n heartbeatInterval: this.heartbeatInterval,\n });\n\n // 启动心跳\n this.startHeartbeat();\n\n return data;\n } catch (error: unknown) {\n if (isAxiosError(error)) {\n const axiosError = error as AxiosErrorLike;\n const status = axiosError.response?.status;\n const data = axiosError.response?.data as { code?: string; details?: string };\n\n if (status === 409) {\n throw new Error('Gateway already registered');\n } else if (status === 400) {\n throw new Error(`Invalid request: ${data?.details || 'Unknown error'}`);\n } else if (status === 404) {\n throw new Error(\n 'Lingyao gateway register returned 404. Check server URL and /lyoc/gateway/register; ' +\n 'the gatewayId may be invalid or the API may not be deployed on this host.'\n );\n }\n\n throw new Error(`Registration failed: ${axiosError.message}`);\n }\n throw error;\n } finally {\n this.isConnecting = false;\n }\n }\n\n /**\n * 发送心跳\n */\n async heartbeat(\n status: GatewayStatus = 'online',\n activeConnections: number = 0\n ): Promise<HeartbeatResponse> {\n if (!this.isRegistered || !this.gatewayToken) {\n throw new Error('Gateway not registered');\n }\n\n try {\n const response = await this.axiosInstance.post<HeartbeatResponse>(\n '/gateway/heartbeat',\n {\n timestamp: Date.now(),\n status,\n activeConnections,\n }\n );\n\n return response.data;\n } catch (error: unknown) {\n if (isAxiosError(error)) {\n const axiosError = error as AxiosErrorLike;\n this.runtime.logger.error('Heartbeat failed', {\n status: axiosError.response?.status,\n data: axiosError.response?.data,\n });\n }\n throw error;\n }\n }\n\n /**\n * 发送消息到灵爻 App\n */\n async sendMessage(\n deviceId: string,\n messageType: 'notify_text' | 'notify_action',\n payload: SendMessageRequest['message']['payload'],\n options?: SendMessageRequest['options']\n ): Promise<SendMessageResponse> {\n if (!this.isRegistered || !this.gatewayToken) {\n throw new Error('Gateway not registered');\n }\n\n const messageId = this.generateMessageId();\n\n try {\n const response = await this.axiosInstance.post<SendMessageResponse>(\n '/gateway/messages',\n {\n deviceId,\n message: {\n id: messageId,\n type: messageType,\n timestamp: Date.now(),\n payload,\n },\n options: options || {},\n }\n );\n\n this.runtime.logger.debug('Message sent', {\n messageId,\n deviceId,\n status: response.data.status,\n });\n\n return response.data;\n } catch (error: unknown) {\n if (isAxiosError(error)) {\n const axiosError = error as AxiosErrorLike;\n const status = axiosError.response?.status;\n const data = axiosError.response?.data as { error?: string };\n\n if (status === 404) {\n throw new Error(`Device not found: ${deviceId}`);\n } else if (status === 429) {\n throw new Error('Message queue full, please retry later');\n }\n\n throw new Error(`Send message failed: ${data?.error || axiosError.message}`);\n }\n throw error;\n }\n }\n\n /**\n * 启动心跳循环\n */\n private startHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n }\n\n this.heartbeatTimer = setInterval(\n async () => {\n try {\n await this.heartbeat();\n } catch (error) {\n this.runtime.logger.error('Heartbeat error', error);\n }\n },\n this.heartbeatInterval\n );\n\n this.runtime.logger.info('Heartbeat started', {\n interval: this.heartbeatInterval,\n });\n }\n\n /**\n * 停止心跳循环\n */\n stopHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n this.runtime.logger.info('Heartbeat stopped');\n }\n }\n\n /**\n * 获取 Webhook Secret\n */\n getWebhookSecret(): string | null {\n return this.webhookSecret;\n }\n\n /**\n * 获取 Gateway Token\n */\n getGatewayToken(): string | null {\n return this.gatewayToken;\n }\n\n /**\n * HTTP 注册/恢复后由 `serverConfig.heartbeatInterval` 写入(毫秒),用于与 WebSocket `gateway_heartbeat` 对齐。\n */\n getHeartbeatIntervalMs(): number {\n return this.heartbeatInterval > 0 ? this.heartbeatInterval : 30000;\n }\n\n /**\n * 检查 Token 是否即将过期\n */\n isTokenExpiringSoon(thresholdMs: number = 7 * 24 * 60 * 60 * 1000): boolean {\n return this.tokenExpiresAt - Date.now() < thresholdMs;\n }\n\n /**\n * 检查是否已注册\n */\n isReady(): boolean {\n return this.isRegistered && !!this.gatewayToken;\n }\n\n /**\n * Clear local gateway token/session (storage + in-memory). Used when WS handshake\n * fails with 404 or when forcing re-registration with the same gatewayId.\n */\n async clearLocalSession(): Promise<void> {\n this.stopHeartbeat();\n this.gatewayToken = null;\n this.webhookSecret = null;\n this.tokenExpiresAt = 0;\n this.isRegistered = false;\n\n const keys = ['gatewayToken', 'webhookSecret', 'tokenExpiresAt', 'serverConfig'] as const;\n for (const k of keys) {\n try {\n await this.runtime.storage.delete(this.storageKey(k));\n } catch (e) {\n this.runtime.logger.warn(`Failed to delete storage key ${k}`, e);\n }\n }\n\n this.runtime.logger.info('Cleared local Lingyao gateway session');\n }\n\n /**\n * 从存储恢复会话\n */\n async restoreFromStorage(): Promise<boolean> {\n try {\n const token = await this.runtime.storage.get(this.storageKey('gatewayToken'));\n const secret = await this.runtime.storage.get(this.storageKey('webhookSecret'));\n const expiresAt = await this.runtime.storage.get(this.storageKey('tokenExpiresAt')) as number | undefined;\n const serverConfig = await this.runtime.storage.get(this.storageKey('serverConfig')) as GatewayRegisterResponse['serverConfig'] | undefined;\n\n if (token && secret && expiresAt && serverConfig) {\n // 检查是否过期\n if (expiresAt > Date.now()) {\n this.gatewayToken = token as string;\n this.webhookSecret = secret as string;\n this.tokenExpiresAt = expiresAt;\n this.heartbeatInterval = serverConfig.heartbeatInterval;\n this.isRegistered = true;\n\n // 启动心跳\n this.startHeartbeat();\n\n this.runtime.logger.info('Session restored from storage', {\n expiresAt: new Date(this.tokenExpiresAt).toISOString(),\n });\n\n return true;\n } else {\n this.runtime.logger.warn('Stored token expired, need to re-register');\n }\n }\n } catch (error) {\n this.runtime.logger.error('Failed to restore session from storage', error);\n }\n\n return false;\n }\n\n /**\n * Build a namespaced storage key for multi-account support\n */\n private storageKey(name: string): string {\n return `${this.storagePrefix}:${name}`;\n }\n\n /**\n * 生成消息 ID\n */\n private generateMessageId(): string {\n return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;\n }\n\n}\n","/**\n * Lingyao WebSocket Client\n *\n * 主动连接到 lingyao.live 服务器的 WebSocket 客户端\n * 实现:\n * - 自动连接和重连\n * - 心跳机制\n * - 消息发送和接收\n * - 在线状态管理\n */\n\nimport WebSocket from \"ws\";\nimport type { LingyaoRuntime } from \"./types.js\";\n\n/**\n * WebSocket 连接状态\n */\nexport type ConnectionState = \"connecting\" | \"connected\" | \"disconnected\" | \"error\";\n\n/**\n * WebSocket 消息类型(与 `lingyao/server/src/server.ts` 中 Gateway 协议一致)\n */\nexport enum WSMessageType {\n // Gateway → 服务器\n GATEWAY_REGISTER = \"gateway_register\",\n GATEWAY_HEARTBEAT = \"gateway_heartbeat\",\n GATEWAY_SEND_MESSAGE = \"gateway_send_message\",\n\n // 服务器 → Gateway\n GATEWAY_REGISTERED = \"gateway_registered\",\n GATEWAY_HEARTBEAT_ACK = \"gateway_heartbeat_ack\",\n MESSAGE_DELIVERED = \"message_delivered\",\n MESSAGE_FAILED = \"message_failed\",\n APP_MESSAGE = \"app_message\",\n DEVICE_ONLINE = \"device_online\",\n PAIRING_COMPLETED = \"pairing_completed\",\n ERROR = \"error\",\n}\n\n/**\n * WebSocket 消息基础格式\n */\nexport interface WSMessage {\n type: WSMessageType | string;\n id: string;\n timestamp: number;\n payload?: any;\n}\n\n/**\n * 注册消息\n */\nexport interface RegisterMessage extends WSMessage {\n type: WSMessageType.GATEWAY_REGISTER;\n payload: {\n gatewayId: string;\n version: string;\n capabilities: {\n websocket: boolean;\n compression: boolean;\n maxMessageSize: number;\n };\n };\n}\n\n/**\n * 心跳消息\n */\nexport interface HeartbeatMessage extends WSMessage {\n type: WSMessageType.GATEWAY_HEARTBEAT;\n payload: {\n timestamp: number;\n status: \"online\";\n };\n}\n\n/**\n * 发送消息\n */\nexport interface SendMessage extends WSMessage {\n type: WSMessageType.GATEWAY_SEND_MESSAGE;\n payload: {\n deviceId: string;\n message: {\n id: string;\n type: \"notify_text\" | \"notify_action\";\n timestamp: number;\n payload: any;\n };\n };\n}\n\n/**\n * 接收到的 App 消息\n */\nexport interface AppMessage extends WSMessage {\n type: WSMessageType.APP_MESSAGE;\n payload: {\n deviceId: string;\n message: {\n id: string;\n type: \"sync_diary\" | \"sync_memory\" | \"heartbeat\";\n timestamp: number;\n content: string;\n metadata?: Record<string, unknown>;\n };\n };\n}\n\n/**\n * WebSocket 客户端事件\n */\nexport type WSClientEvent =\n | { type: \"connected\"; connectionId: string }\n | { type: \"disconnected\"; code: number; reason: string }\n | { type: \"error\"; error: Error }\n | { type: \"fatal_handshake\"; reason: \"http_404\" }\n | { type: \"message\"; message: WSMessage }\n | { type: \"appMessage\"; deviceId: string; message: AppMessage[\"payload\"][\"message\"] }\n | { type: \"pairing_completed\"; deviceId: string; deviceInfo: { name: string; platform: string; version: string }; sessionId: string };\n\n/** True when the `ws` library reports HTTP 404 on the WebSocket upgrade (invalid path or gateway rejected at edge). */\nexport function isWebsocketUpgradeNotFoundError(message: string): boolean {\n return /Unexpected server response:\\s*404/i.test(message) || /\\b404\\b/.test(message);\n}\n\n/**\n * 将服务端类型映射到本客户端 handler 使用的 key(与 {@link WSMessageType} 一致)。\n * 仍接受旧版短名 `registered` / `heartbeat_ack`,便于与历史部署混连。\n */\nexport function normalizeIncomingGatewayMessageType(type: string): string {\n switch (type) {\n case \"registered\":\n return WSMessageType.GATEWAY_REGISTERED;\n case \"heartbeat_ack\":\n return WSMessageType.GATEWAY_HEARTBEAT_ACK;\n case \"gateway_registered\":\n return WSMessageType.GATEWAY_REGISTERED;\n case \"gateway_heartbeat_ack\":\n return WSMessageType.GATEWAY_HEARTBEAT_ACK;\n default:\n return type;\n }\n}\n\n/**\n * WebSocket 客户端配置\n */\nexport interface WSClientConfig {\n url: string;\n gatewayId: string;\n token?: string;\n reconnectInterval: number;\n heartbeatInterval: number;\n messageHandler?: (message: AppMessage) => void | Promise<void>;\n eventHandler?: (event: WSClientEvent) => void;\n}\n\n/**\n * Lingyao WebSocket Client\n *\n * 主动连接到 lingyao.live 服务器的 WebSocket 客户端\n */\nexport class LingyaoWSClient {\n private config: WSClientConfig;\n private ws: WebSocket | null = null;\n private state: ConnectionState = \"disconnected\";\n private connectionId: string | null = null;\n private heartbeatTimer: NodeJS.Timeout | null = null;\n private reconnectTimer: NodeJS.Timeout | null = null;\n /** When set, close handler will not schedule reconnect (e.g. HTTP 404 on upgrade). */\n private suppressReconnect = false;\n private messageHandlers: Map<string, (msg: WSMessage) => void> = new Map();\n private logger: LingyaoRuntime[\"logger\"];\n\n constructor(runtime: LingyaoRuntime, config: WSClientConfig) {\n this.logger = runtime.logger;\n this.config = { ...config };\n\n this.registerMessageHandler(WSMessageType.GATEWAY_REGISTERED, this.handleRegistered.bind(this));\n this.registerMessageHandler(WSMessageType.GATEWAY_HEARTBEAT_ACK, this.handleHeartbeatAck.bind(this));\n this.registerMessageHandler(WSMessageType.MESSAGE_DELIVERED, this.handleMessageDelivered.bind(this));\n this.registerMessageHandler(WSMessageType.MESSAGE_FAILED, this.handleMessageFailed.bind(this));\n this.registerMessageHandler(WSMessageType.APP_MESSAGE, this.handleAppMessage.bind(this));\n this.registerMessageHandler(WSMessageType.DEVICE_ONLINE, this.handleDeviceOnline.bind(this));\n this.registerMessageHandler(WSMessageType.PAIRING_COMPLETED, this.handlePairingCompleted.bind(this));\n this.registerMessageHandler(WSMessageType.ERROR, this.handleError.bind(this));\n }\n\n /**\n * 连接到服务器\n */\n async connect(): Promise<void> {\n if (this.state === \"connecting\" || this.state === \"connected\") {\n this.logger.warn(\"WebSocket already connecting or connected\");\n return;\n }\n\n this.state = \"connecting\";\n this.suppressReconnect = false;\n this.emitEvent({ type: \"disconnected\", code: 0, reason: \"Reconnecting\" });\n\n try {\n this.logger.info(`Connecting to Lingyao server: ${this.config.url}`);\n\n const wsUrl = this.config.token\n ? `${this.config.url}?token=${encodeURIComponent(this.config.token)}`\n : this.config.url;\n\n this.ws = new WebSocket(wsUrl, {\n headers: {\n \"X-Gateway-ID\": this.config.gatewayId,\n },\n });\n\n this.setupWebSocketHandlers();\n } catch (error) {\n this.state = \"error\";\n this.emitEvent({ type: \"error\", error: error as Error });\n this.scheduleReconnect();\n }\n }\n\n /**\n * 设置 WebSocket 事件处理器\n */\n private setupWebSocketHandlers(): void {\n if (!this.ws) return;\n\n this.ws.on(\"open\", () => {\n this.handleOpen();\n });\n\n this.ws.on(\"message\", async (data: Buffer) => {\n await this.handleMessage(data);\n });\n\n this.ws.on(\"error\", (error) => {\n this.handleErrorEvent(error);\n });\n\n this.ws.on(\"close\", (code: number, reason: Buffer) => {\n this.handleClose(code, reason.toString());\n });\n }\n\n /**\n * 处理连接打开\n */\n private handleOpen(): void {\n this.state = \"connected\";\n this.connectionId = this.generateConnectionId();\n\n this.logger.info(\"WebSocket connected to Lingyao server\", {\n connectionId: this.connectionId,\n });\n\n this.emitEvent({\n type: \"connected\",\n connectionId: this.connectionId!,\n });\n\n // 发送注册消息\n this.sendRegister();\n\n // 启动心跳\n this.startHeartbeat();\n\n // 清除重连定时器\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n }\n\n /**\n * 处理接收消息\n */\n private async handleMessage(data: Buffer): Promise<void> {\n try {\n const message: WSMessage = JSON.parse(data.toString());\n const rawType = String(message.type);\n const handlerKey = normalizeIncomingGatewayMessageType(rawType);\n this.logger.debug(\"Received message from server\", { type: rawType, handlerKey });\n\n const handler = this.messageHandlers.get(handlerKey);\n if (handler) {\n handler(message);\n } else {\n this.logger.warn(\"No handler for message type\", { type: rawType });\n }\n\n this.emitEvent({ type: \"message\", message });\n } catch (error) {\n this.logger.error(\"Error handling message\", error);\n }\n }\n\n /**\n * 处理连接错误\n */\n private handleErrorEvent(error: Error): void {\n const msg = error?.message ?? String(error);\n if (isWebsocketUpgradeNotFoundError(msg)) {\n this.suppressReconnect = true;\n this.logger.error(\n \"WebSocket handshake failed with HTTP 404 — stopping reconnect loop. \" +\n \"If the gateway was removed server-side, remove `gatewayId` from channels.lingyao.accounts.* \" +\n \"or use a new account id; if the path is wrong, verify wss://…/lyoc/gateway/ws is deployed on api.lingyao.live.\",\n { gatewayId: this.config.gatewayId, message: msg }\n );\n } else {\n this.logger.error(\"WebSocket error\", error);\n }\n this.state = \"error\";\n this.emitEvent({ type: \"error\", error });\n }\n\n /**\n * 处理连接关闭\n */\n private handleClose(code: number, reason: string): void {\n this.logger.warn(\"WebSocket connection closed\", { code, reason });\n this.state = \"disconnected\";\n this.connectionId = null;\n\n this.stopHeartbeat();\n this.emitEvent({ type: \"disconnected\", code, reason });\n\n // 1008 表示 Token 无效,不应继续使用原 Token 重连,交给外部重新注册\n if (code === 1008) {\n this.logger.error(\"WebSocket closed with 1008 (Invalid Token). Stopping reconnect loop.\");\n return;\n }\n\n if (this.suppressReconnect) {\n this.suppressReconnect = false;\n this.emitEvent({ type: \"fatal_handshake\", reason: \"http_404\" });\n return;\n }\n\n // 如果不是正常关闭,尝试重连\n if (code !== 1000) {\n this.scheduleReconnect();\n }\n }\n\n /**\n * 发送注册消息\n */\n private sendRegister(): void {\n const message: RegisterMessage = {\n type: WSMessageType.GATEWAY_REGISTER,\n id: this.generateMessageId(),\n timestamp: Date.now(),\n payload: {\n gatewayId: this.config.gatewayId,\n version: \"0.2.0\",\n capabilities: {\n websocket: true,\n compression: false,\n maxMessageSize: 1048576, // 1MB\n },\n },\n };\n\n this.send(message);\n }\n\n /**\n * 处理注册响应\n */\n private handleRegistered(message: WSMessage): void {\n this.logger.info(\"Gateway registered to Lingyao server\", {\n messageId: message.id,\n });\n }\n\n /**\n * 发送心跳\n */\n private sendHeartbeat(): void {\n const message: HeartbeatMessage = {\n type: WSMessageType.GATEWAY_HEARTBEAT,\n id: this.generateMessageId(),\n timestamp: Date.now(),\n payload: {\n timestamp: Date.now(),\n status: \"online\",\n },\n };\n\n this.send(message);\n }\n\n /**\n * 处理心跳确认\n */\n private handleHeartbeatAck(_message: WSMessage): void {\n this.logger.debug(\"Heartbeat acknowledged\");\n }\n\n /**\n * 启动心跳\n */\n private startHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n }\n\n this.heartbeatTimer = setInterval(() => {\n if (this.state === \"connected\") {\n this.sendHeartbeat();\n }\n }, this.config.heartbeatInterval);\n }\n\n /**\n * 停止心跳\n */\n private stopHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n }\n\n /**\n * 安排重连\n */\n private scheduleReconnect(): void {\n if (this.reconnectTimer) {\n return; // 已经安排了重连\n }\n\n this.logger.info(`Scheduling reconnect in ${this.config.reconnectInterval}ms`);\n\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = null;\n this.connect();\n }, this.config.reconnectInterval);\n }\n\n /**\n * 发送消息到服务器\n */\n send(message: WSMessage): void {\n if (!this.ws || this.state !== \"connected\") {\n throw new Error(\"WebSocket not connected\");\n }\n\n try {\n this.ws.send(JSON.stringify(message));\n this.logger.debug(\"Sent message to server\", { type: message.type });\n } catch (error) {\n this.logger.error(\"Failed to send message\", error);\n throw error;\n }\n }\n\n /**\n * 发送通知到鸿蒙 App\n */\n sendNotification(deviceId: string, notification: any): void {\n const message: SendMessage = {\n type: WSMessageType.GATEWAY_SEND_MESSAGE,\n id: this.generateMessageId(),\n timestamp: Date.now(),\n payload: {\n deviceId,\n message: {\n id: this.generateMessageId(),\n type: \"notify_action\",\n timestamp: Date.now(),\n payload: notification,\n },\n },\n };\n\n this.send(message);\n }\n\n /**\n * 处理 App 消息\n */\n private handleAppMessage(message: WSMessage): void {\n if (message.type !== WSMessageType.APP_MESSAGE) return;\n\n const appMessage = message as AppMessage;\n this.logger.info(\"Received message from App\", {\n deviceId: appMessage.payload.deviceId,\n messageType: appMessage.payload.message.type,\n });\n\n if (this.config.messageHandler) {\n // 异步处理,不阻塞 WebSocket\n Promise.resolve(this.config.messageHandler(appMessage)).catch((error: unknown) => {\n this.logger.error(\"Error handling App message\", error);\n });\n }\n }\n\n /**\n * 处理消息发送成功\n */\n private handleMessageDelivered(message: WSMessage): void {\n this.logger.debug(\"Message delivered successfully\", {\n messageId: message.id,\n });\n }\n\n /**\n * 设备上线(服务器可选推送)\n */\n private handleDeviceOnline(message: WSMessage): void {\n this.logger.debug(\"Device online (server push)\", {\n payload: message.payload,\n });\n }\n\n /**\n * 处理配对完成通知(来自 lingyao.live 服务器)\n */\n private handlePairingCompleted(message: WSMessage): void {\n const payload = message.payload;\n this.logger.info(\"Pairing completed\", {\n deviceId: payload?.deviceId,\n sessionId: payload?.sessionId,\n });\n\n if (this.config.eventHandler) {\n this.config.eventHandler({\n type: \"pairing_completed\",\n deviceId: payload?.deviceId,\n deviceInfo: payload?.deviceInfo,\n sessionId: payload?.sessionId,\n });\n }\n }\n\n /**\n * 处理消息发送失败\n */\n private handleMessageFailed(message: WSMessage): void {\n this.logger.warn(\"Message delivery failed\", {\n messageId: message.id,\n });\n }\n\n /**\n * 处理服务器错误\n */\n private handleError(message: WSMessage): void {\n this.logger.error(\"Server error\", message);\n }\n\n /**\n * 注册消息处理器\n */\n registerMessageHandler(\n type: string,\n handler: (message: WSMessage) => void\n ): void {\n this.messageHandlers.set(type, handler);\n }\n\n /**\n * 发送事件\n */\n private emitEvent(event: WSClientEvent): void {\n if (this.config.eventHandler) {\n this.config.eventHandler(event);\n }\n }\n\n /**\n * 更新 WebSocket 连接使用的 token\n */\n updateToken(token: string): void {\n this.config.token = token;\n }\n\n /**\n * 断开连接\n */\n disconnect(): void {\n this.logger.info(\"Disconnecting WebSocket from Lingyao server\");\n\n this.stopHeartbeat();\n\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n\n if (this.ws) {\n this.ws.close(1000, \"Client disconnect\");\n this.ws = null;\n }\n\n this.state = \"disconnected\";\n this.connectionId = null;\n }\n\n /**\n * 获取连接状态\n */\n getState(): ConnectionState {\n return this.state;\n }\n\n /**\n * 获取连接 ID\n */\n getConnectionId(): string | null {\n return this.connectionId;\n }\n\n /**\n * 是否已连接\n */\n isConnected(): boolean {\n return this.state === \"connected\";\n }\n\n /**\n * 生成消息 ID\n */\n private generateMessageId(): string {\n return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;\n }\n\n /**\n * 生成连接 ID\n */\n private generateConnectionId(): string {\n return `conn_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;\n }\n}\n","import { z } from \"zod\";\nimport type { LingyaoConfig } from \"./types.js\";\n\n/**\n * OpenClaw-standard DM policy values.\n */\nexport const lingyaoDmPolicySchema = z.enum([\"pairing\", \"allowlist\", \"open\"]);\n\nconst allowFromSchema = z.array(z.string()).optional();\n\nexport const lingyaoAccountConfigSchema = z.object({\n enabled: z.boolean().optional(),\n dmPolicy: lingyaoDmPolicySchema.optional(),\n allowFrom: allowFromSchema,\n websocketHeartbeatIntervalMs: z.number().int().min(5000).max(120000).optional(),\n});\n\n/**\n * Zod schema for Lingyao channel configuration.\n *\n * Lingyao supports both top-level single-account config and\n * `channels.lingyao.accounts.<id>` multi-account config. Account entries inherit\n * unspecified values from the top-level section.\n */\nexport const lingyaoConfigSchema = z.object({\n enabled: z.boolean().default(true),\n dmPolicy: lingyaoDmPolicySchema.optional().default(\"pairing\"),\n allowFrom: allowFromSchema.default([]),\n websocketHeartbeatIntervalMs: z.number().int().min(5000).max(120000).optional(),\n defaultAccount: z.string().min(1).optional(),\n accounts: z.record(lingyaoAccountConfigSchema).optional(),\n});\n\nexport type LingyaoConfigSchema = z.infer<typeof lingyaoConfigSchema>;\nexport const lingyaoChannelConfigSchema = {\n schema: {\n type: \"object\",\n additionalProperties: false,\n properties: {\n enabled: {\n type: \"boolean\",\n default: true,\n },\n dmPolicy: {\n type: \"string\",\n enum: [\"pairing\", \"allowlist\", \"open\"],\n default: \"pairing\",\n },\n allowFrom: {\n type: \"array\",\n items: { type: \"string\" },\n default: [],\n },\n defaultAccount: {\n type: \"string\",\n },\n websocketHeartbeatIntervalMs: {\n type: \"integer\",\n minimum: 5000,\n maximum: 120000,\n description:\n \"WebSocket gateway_heartbeat interval in ms (default: server register response). Use 5000–55000 for typical relay timeout.\",\n },\n accounts: {\n type: \"object\",\n additionalProperties: false,\n properties: {\n enabled: {\n type: \"boolean\",\n default: true,\n },\n dmPolicy: {\n type: \"string\",\n enum: [\"pairing\", \"allowlist\", \"open\"],\n },\n allowFrom: {\n type: \"array\",\n items: { type: \"string\" },\n },\n websocketHeartbeatIntervalMs: {\n type: \"integer\",\n minimum: 5000,\n maximum: 120000,\n },\n },\n default: {},\n },\n },\n },\n} as const;\n\n/**\n * Validate configuration object\n */\nexport function validateConfig(config: unknown): LingyaoConfig {\n return lingyaoConfigSchema.parse(config);\n}\n\n/**\n * Safely parse configuration, returning null if invalid\n */\nexport function safeParseConfig(config: unknown): LingyaoConfig | null {\n const result = lingyaoConfigSchema.safeParse(config);\n return result.success ? result.data : null;\n}\n\n/**\n * Get default configuration\n */\nexport function getDefaultConfig(): LingyaoConfig {\n return {\n enabled: true,\n dmPolicy: \"pairing\",\n allowFrom: [],\n };\n}\n"],"mappings":"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","names":["getOrchestrator","orchestrator","getOrchestrator","orchestrator","getOrchestrator","orchestrator","getOrchestrator","orchestrator"]}
|
|
1
|
+
{"version":3,"sources":["../src/setup-entry.ts","../src/api.ts","../src/runtime.ts","../src/adapters/config.ts","../src/adapters/gateway.ts","../src/adapters/status.ts","../src/adapters/directory.ts","../src/adapters/messaging.ts","../src/adapters/outbound.ts","../src/adapters/setup.ts","../src/orchestrator.ts","../src/server-client.ts","../src/websocket-client.ts","../src/config-schema.ts"],"sourcesContent":["import { defineSetupPluginEntry } from 'openclaw/plugin-sdk/channel-core';\n\nimport { lingyaoPlugin } from './api.js';\n\nexport default defineSetupPluginEntry(lingyaoPlugin);\n","import type { PluginRuntime, ChannelPlugin } from 'openclaw/plugin-sdk';\nimport { createChatChannelPlugin } from 'openclaw/plugin-sdk/channel-core';\n\nimport { setRuntime, adaptPluginRuntime } from './runtime.js';\nimport { createConfigAdapter, type ResolvedAccount } from './adapters/config.js';\nimport { createGatewayAdapter } from './adapters/gateway.js';\nimport { createStatusAdapter } from './adapters/status.js';\nimport type { LingyaoProbeResult } from './adapters/status.js';\nimport { createDirectoryAdapter } from './adapters/directory.js';\nimport { createMessagingAdapter } from './adapters/messaging.js';\nimport { createOutboundAdapter } from './adapters/outbound.js';\nimport { createSetupAdapter } from './adapters/setup.js';\nimport { MultiAccountOrchestrator } from './orchestrator.js';\nimport { lingyaoChannelConfigSchema } from './config-schema.js';\n\nexport * from './types.js';\nexport type { AgentMessage } from './bot.js';\nexport { validateConfig, getDefaultConfig } from './config-schema.js';\n\nlet orchestrator: MultiAccountOrchestrator | null = null;\n\nfunction getOrchestrator(): MultiAccountOrchestrator | null {\n return orchestrator;\n}\n\nconst configAdapter = createConfigAdapter();\nconst setupAdapter = createSetupAdapter();\nconst messagingAdapter = createMessagingAdapter();\nconst gatewayAdapter = createGatewayAdapter(getOrchestrator);\nconst directoryAdapter = createDirectoryAdapter(getOrchestrator);\nconst outboundAdapter = createOutboundAdapter(getOrchestrator);\n\nconst securityOptions = {\n dm: {\n channelKey: 'lingyao',\n resolvePolicy: (account: ResolvedAccount) => account.dmPolicy,\n resolveAllowFrom: (account: ResolvedAccount) => account.allowFrom,\n defaultPolicy: 'pairing' as const,\n },\n};\n\nconst pairingOptions = {\n text: {\n idLabel: '设备 ID',\n message: '设备已批准配对',\n notify: async (params: { cfg: unknown; id: string }): Promise<void> => {\n const orc = getOrchestrator();\n if (!orc) return;\n\n const config = params.cfg as Record<string, unknown>;\n const channels = config.channels as Record<string, unknown> | undefined;\n const lingyao = channels?.lingyao as { accounts?: Record<string, unknown> } | undefined;\n const accountIds = lingyao?.accounts ? Object.keys(lingyao.accounts) : ['default'];\n\n for (const accountId of accountIds) {\n const sent = orc.sendNotification(accountId, params.id, {\n type: 'pairing_confirmed',\n message: pairingOptions.text.message,\n });\n if (sent) break;\n }\n },\n },\n};\n\nconst capabilities = {\n chatTypes: ['direct'] as ('direct' | 'group' | 'channel' | 'thread')[],\n media: false,\n reactions: false,\n threads: false,\n polls: false,\n edit: false,\n unsend: false,\n reply: false,\n effects: false,\n groupManagement: false,\n nativeCommands: false,\n blockStreaming: true,\n};\n\nconst meta = {\n id: 'lingyao',\n label: '灵爻',\n selectionLabel: '灵爻 (HarmonyOS)',\n docsPath: '/channels/lingyao',\n docsLabel: '灵爻文档',\n blurb: '通过 lingyao.live 服务器中转与鸿蒙灵爻 App 双向同步日记和记忆',\n order: 50,\n aliases: ['lingyao', '灵爻'],\n};\n\nexport const lingyaoPlugin: ChannelPlugin<ResolvedAccount, LingyaoProbeResult> = {\n ...createChatChannelPlugin<ResolvedAccount, LingyaoProbeResult>({\n base: {\n id: 'lingyao',\n meta,\n capabilities,\n configSchema: lingyaoChannelConfigSchema,\n config: configAdapter,\n setup: setupAdapter,\n status: createStatusAdapter(getOrchestrator),\n },\n security: securityOptions,\n pairing: pairingOptions,\n outbound: outboundAdapter,\n }),\n gateway: gatewayAdapter,\n directory: directoryAdapter,\n messaging: messagingAdapter,\n};\n\nexport function initializeLingyaoRuntime(runtime: PluginRuntime): void {\n const adapted = adaptPluginRuntime(runtime as Parameters<typeof adaptPluginRuntime>[0]);\n setRuntime(adapted);\n orchestrator = new MultiAccountOrchestrator(adapted);\n}\n","import type { LingyaoRuntime } from \"./types.js\";\nimport { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from \"node:fs\";\nimport { join } from \"node:path\";\n\n/**\n * Global runtime instance storage\n */\nlet globalRuntime: LingyaoRuntime | null = null;\n\n/**\n * Set the global runtime instance\n */\nexport function setRuntime(runtime: LingyaoRuntime): void {\n globalRuntime = runtime;\n}\n\n/**\n * Get the global runtime instance\n */\nexport function getRuntime(): LingyaoRuntime {\n if (!globalRuntime) {\n throw new Error(\"Runtime not initialized. Call setRuntime() first.\");\n }\n return globalRuntime;\n}\n\n/**\n * Check if runtime is initialized\n */\nexport function hasRuntime(): boolean {\n return globalRuntime !== null;\n}\n\n/**\n * Clear the global runtime instance\n */\nexport function clearRuntime(): void {\n globalRuntime = null;\n}\n\n/**\n * Adapt OpenClaw PluginRuntime to LingyaoRuntime.\n *\n * Bridges the SDK's PluginRuntime (logger, state dir) to the\n * internal LingyaoRuntime interface used by orchestrator/bot/ws.\n */\nexport function adaptPluginRuntime(pr: {\n logging?: { getChildLogger?: () => { info: (msg: string, ...args: unknown[]) => void; warn: (msg: string, ...args: unknown[]) => void; error: (msg: string, ...args: unknown[]) => void; debug?: (msg: string, ...args: unknown[]) => void } };\n state?: { resolveStateDir?: () => string };\n}): LingyaoRuntime {\n const noop = (..._args: unknown[]) => {};\n const rawLogger = pr.logging?.getChildLogger?.() ?? {\n info: console.info.bind(console),\n warn: console.warn.bind(console),\n error: console.error.bind(console),\n };\n const childLogger = {\n info: rawLogger.info,\n warn: rawLogger.warn,\n error: rawLogger.error,\n debug: rawLogger.debug ?? noop,\n };\n\n const stateDir = pr.state?.resolveStateDir?.() ?? join(process.cwd(), '.lingyao-data');\n const storeDir = join(stateDir, 'lingyao');\n\n return {\n config: { enabled: true },\n logger: childLogger,\n storage: {\n async get(key: string): Promise<unknown | null> {\n try {\n const filePath = join(storeDir, `${key}.json`);\n if (!existsSync(filePath)) return null;\n const data = readFileSync(filePath, 'utf-8');\n return JSON.parse(data);\n } catch {\n return null;\n }\n },\n async set(key: string, value: unknown): Promise<void> {\n if (!existsSync(storeDir)) {\n mkdirSync(storeDir, { recursive: true });\n }\n const filePath = join(storeDir, `${key}.json`);\n writeFileSync(filePath, JSON.stringify(value, null, 2), 'utf-8');\n },\n async delete(key: string): Promise<void> {\n const filePath = join(storeDir, `${key}.json`);\n if (existsSync(filePath)) {\n unlinkSync(filePath);\n }\n },\n },\n tools: {\n async call(): Promise<unknown> {\n throw new Error('Tool calls not available in SDK mode');\n },\n },\n };\n}\n","/**\n * Config Adapter - Account resolution, config validation\n *\n * serverUrl is NOT exposed to users — hardcoded as LINGYAO_SERVER_URL.\n */\n\nimport type { OpenClawConfig } from 'openclaw/plugin-sdk';\nimport type { LingyaoAccountConfig } from '../types.js';\n\ntype LingyaoDmPolicy = 'pairing' | 'allowlist' | 'open';\n\n/**\n * Resolved account after config resolution.\n */\nexport interface ResolvedAccount {\n readonly id: string;\n readonly accountId?: string | null;\n readonly enabled: boolean;\n readonly dmPolicy: LingyaoDmPolicy;\n readonly allowFrom: string[];\n readonly gatewayId?: string;\n readonly rawConfig: LingyaoAccountConfig;\n}\n\n/**\n * Normalize legacy Lingyao DM policy values to current OpenClaw semantics.\n *\n * `paired` was the old plugin-local name for the standard `pairing` mode.\n * `deny` had no SDK equivalent; map it to an empty `allowlist` so behavior\n * stays strict instead of silently opening access.\n */\nfunction normalizeDmPolicy(raw: unknown): LingyaoDmPolicy {\n switch (raw) {\n case 'pairing':\n case 'allowlist':\n case 'open':\n return raw;\n case 'paired':\n return 'pairing';\n case 'deny':\n return 'allowlist';\n default:\n return 'pairing';\n }\n}\n\nfunction extractChannelConfig(cfg: OpenClawConfig): Record<string, unknown> {\n const channels = (cfg as Record<string, unknown>)?.channels as Record<string, unknown> | undefined;\n return (channels?.lingyao as Record<string, unknown> | undefined) ?? {};\n}\n\n/**\n * Extract the Lingyao accounts from OpenClaw config.\n *\n * Lingyao supports top-level single-account fields and nested multi-account\n * fields. Nested accounts inherit unspecified values from the top-level\n * section so the UI and runtime agree on the effective account config.\n */\nfunction extractAccounts(cfg: OpenClawConfig): Record<string, LingyaoAccountConfig> {\n const lingyao = extractChannelConfig(cfg);\n const accounts = lingyao?.accounts as Record<string, LingyaoAccountConfig> | undefined;\n const baseConfig: LingyaoAccountConfig = {\n enabled: lingyao.enabled as boolean | undefined,\n dmPolicy: normalizeDmPolicy(lingyao.dmPolicy),\n allowFrom: Array.isArray(lingyao.allowFrom) ? (lingyao.allowFrom as string[]) : [],\n gatewayId: lingyao.gatewayId as string | undefined,\n websocketHeartbeatIntervalMs: lingyao.websocketHeartbeatIntervalMs as number | undefined,\n };\n\n if (!accounts || Object.keys(accounts).length === 0) {\n return { default: baseConfig };\n }\n\n return Object.fromEntries(\n Object.entries(accounts).map(([accountId, accountConfig]) => {\n const mergedAllowFrom =\n Array.isArray(accountConfig?.allowFrom) && accountConfig.allowFrom.length > 0\n ? accountConfig.allowFrom\n : baseConfig.allowFrom;\n\n const normalized: LingyaoAccountConfig = {\n ...baseConfig,\n ...accountConfig,\n dmPolicy: normalizeDmPolicy(accountConfig?.dmPolicy ?? baseConfig.dmPolicy),\n allowFrom: mergedAllowFrom,\n };\n\n return [accountId, normalized];\n })\n );\n}\n\n/**\n * Create the config adapter.\n */\nexport function createConfigAdapter() {\n return {\n listAccountIds(cfg: OpenClawConfig): string[] {\n const accounts = extractAccounts(cfg);\n const ids = Object.keys(accounts);\n return ids.length > 0 ? ids : ['default'];\n },\n\n resolveAccount(cfg: OpenClawConfig, accountId?: string | null): ResolvedAccount {\n const accounts = extractAccounts(cfg);\n const ids = Object.keys(accounts);\n const channelConfig = extractChannelConfig(cfg);\n const configuredDefaultAccount = channelConfig.defaultAccount as string | undefined;\n const resolvedId =\n accountId ?? configuredDefaultAccount ?? (ids.includes('default') ? 'default' : ids[0]);\n\n if (!resolvedId) {\n throw new Error('No lingyao accounts configured');\n }\n\n const accountConfig = accounts[resolvedId];\n if (!accountConfig) {\n throw new Error(`Account \"${resolvedId}\" not found`);\n }\n\n return {\n id: resolvedId,\n accountId: resolvedId,\n enabled: (accountConfig as Record<string, unknown>)?.enabled !== false,\n dmPolicy: normalizeDmPolicy((accountConfig as Record<string, unknown>)?.dmPolicy),\n allowFrom: ((accountConfig as Record<string, unknown>)?.allowFrom as string[]) ?? [],\n gatewayId: (accountConfig as Record<string, unknown>)?.gatewayId as string | undefined,\n rawConfig: accountConfig as LingyaoAccountConfig,\n };\n },\n\n isConfigured(_account: ResolvedAccount, _cfg: OpenClawConfig): boolean {\n return true;\n },\n\n isEnabled(account: ResolvedAccount, _cfg: OpenClawConfig): boolean {\n return account.enabled;\n },\n };\n}\n","/**\n * Gateway Adapter - Account lifecycle management\n *\n * Implements ChannelGatewayAdapter:\n * - startAccount: create WS/HTTP connections for an account\n * - stopAccount: tear down connections for an account\n *\n * Delegates to MultiAccountOrchestrator for all operations.\n */\n\nimport type { ChannelGatewayContext, PluginRuntime, OpenClawConfig } from 'openclaw/plugin-sdk';\nimport type { MultiAccountOrchestrator } from '../orchestrator.js';\nimport type { ResolvedAccount } from './config.js';\n\n/**\n * Create the gateway adapter.\n */\nexport function createGatewayAdapter(\n getOrchestrator: () => MultiAccountOrchestrator | null\n) {\n return {\n async startAccount(ctx: ChannelGatewayContext<ResolvedAccount>): Promise<void> {\n const orchestrator = getOrchestrator();\n if (!orchestrator) {\n throw new Error('Orchestrator not initialized. Ensure setRuntime was called.');\n }\n\n ctx.log?.info(`Starting account \"${ctx.accountId}\"`);\n\n await orchestrator.start(ctx.account, {\n channelRuntime: ctx.channelRuntime as PluginRuntime['channel'] | undefined,\n cfg: ctx.cfg as OpenClawConfig,\n });\n\n ctx.log?.info(`Account \"${ctx.accountId}\" started successfully`);\n },\n\n async stopAccount(ctx: ChannelGatewayContext<ResolvedAccount>): Promise<void> {\n const orchestrator = getOrchestrator();\n if (!orchestrator) {\n throw new Error('Orchestrator not initialized');\n }\n\n ctx.log?.info(`Stopping account \"${ctx.accountId}\"`);\n\n await orchestrator.stop(ctx.accountId);\n },\n };\n}\n","import {\n buildBaseChannelStatusSummary,\n createDefaultChannelRuntimeState,\n} from 'openclaw/plugin-sdk/channel-status';\nimport { createComputedAccountStatusAdapter } from 'openclaw/plugin-sdk/status-helpers';\nimport type { MultiAccountOrchestrator } from '../orchestrator.js';\nimport type { ResolvedAccount } from './config.js';\n\n/**\n * Probe result structure.\n */\nexport interface LingyaoProbeResult {\n ok: boolean;\n status: 'healthy' | 'degraded' | 'unhealthy';\n wsConnected: boolean;\n uptime?: number;\n checks?: Record<string, { passed: boolean; message: string; duration?: number }>;\n error?: string;\n}\n\n/**\n * Create the status adapter.\n */\nexport function createStatusAdapter(\n getOrchestrator: () => MultiAccountOrchestrator | null,\n _runtime?: unknown\n): ReturnType<typeof createComputedAccountStatusAdapter<ResolvedAccount, LingyaoProbeResult>> {\n return createComputedAccountStatusAdapter<ResolvedAccount, LingyaoProbeResult>({\n defaultRuntime: createDefaultChannelRuntimeState('default'),\n async probeAccount(params: {\n account: ResolvedAccount;\n timeoutMs: number;\n cfg: import('openclaw/plugin-sdk').OpenClawConfig;\n }): Promise<LingyaoProbeResult> {\n const orchestrator = getOrchestrator();\n if (!orchestrator) {\n return {\n ok: false,\n status: 'unhealthy',\n wsConnected: false,\n error: 'Orchestrator not initialized',\n };\n }\n\n const state = orchestrator.getAccountState(params.account.id);\n if (!state) {\n return {\n ok: false,\n status: 'unhealthy',\n wsConnected: false,\n error: 'Account not started',\n };\n }\n\n try {\n const healthResult = await state.probe.runHealthChecks();\n const wsConnected = state.wsClient?.isConnected() ?? false;\n\n let status: 'healthy' | 'degraded' | 'unhealthy' = 'healthy';\n switch (healthResult.status) {\n case 'healthy':\n status = wsConnected ? 'healthy' : 'degraded';\n break;\n case 'degraded':\n status = 'degraded';\n break;\n case 'unhealthy':\n status = 'unhealthy';\n break;\n }\n\n const checks: Record<string, { passed: boolean; message: string; duration?: number }> = {};\n for (const [name, result] of healthResult.checks) {\n checks[name] = {\n passed: result.passed,\n message: result.message ?? '',\n duration: result.duration,\n };\n }\n\n return {\n ok: status !== 'unhealthy',\n status,\n wsConnected,\n uptime: Date.now() - state.startTime,\n checks,\n };\n } catch (error) {\n return {\n ok: false,\n status: 'unhealthy',\n wsConnected: false,\n error: (error as Error).message,\n };\n }\n },\n\n buildChannelSummary({ snapshot }: { snapshot: any }): Record<string, unknown> {\n return buildBaseChannelStatusSummary(snapshot, {\n connected: snapshot.connected ?? false,\n healthState: snapshot.healthState ?? 'unhealthy',\n dmPolicy: snapshot.dmPolicy ?? 'pairing',\n });\n },\n\n resolveAccountSnapshot({ account, probe }: { account: ResolvedAccount; probe?: LingyaoProbeResult }) {\n const orchestrator = getOrchestrator();\n const state = orchestrator?.getAccountState(account.id);\n const connected = state?.wsClient?.isConnected() ?? false;\n const normalizedProbe = probe ?? {\n ok: false,\n status: 'unhealthy' as const,\n wsConnected: false,\n error: 'Probe not available',\n };\n\n return {\n accountId: account.id,\n enabled: account.enabled,\n configured: true,\n extra: {\n connected,\n healthState: normalizedProbe.status,\n dmPolicy: account.dmPolicy,\n allowFrom: account.allowFrom,\n gatewayId: account.gatewayId ?? state?.gatewayId ?? null,\n },\n };\n },\n });\n}\n","/**\n * Directory Adapter - List paired devices (peers)\n *\n * Implements ChannelDirectoryAdapter:\n * - listPeers: return all active paired devices for an account\n */\n\nimport type { OpenClawConfig } from 'openclaw/plugin-sdk';\nimport type { MultiAccountOrchestrator } from '../orchestrator.js';\n\n/**\n * Directory types (not exported by SDK, defined locally to match contract).\n */\nexport interface ChannelDirectoryEntry {\n kind: 'user' | 'group' | 'channel';\n id: string;\n name?: string;\n handle?: string;\n avatarUrl?: string;\n rank?: number;\n raw?: unknown;\n}\n\nexport interface ChannelDirectoryListParams {\n cfg: OpenClawConfig;\n accountId?: string | null;\n query?: string | null;\n limit?: number | null;\n runtime: unknown;\n}\n\n/**\n * Create the directory adapter.\n */\nexport function createDirectoryAdapter(\n getOrchestrator: () => MultiAccountOrchestrator | null\n) {\n return {\n async listPeers(params: ChannelDirectoryListParams): Promise<ChannelDirectoryEntry[]> {\n const orchestrator = getOrchestrator();\n if (!orchestrator) {\n return [];\n }\n\n const accountId = params.accountId ?? 'default';\n const accountManager = orchestrator.getAccountManager(accountId);\n if (!accountManager) {\n return [];\n }\n\n const activeAccounts = accountManager.getActiveAccounts();\n\n let entries: ChannelDirectoryEntry[] = activeAccounts.map(account => ({\n kind: 'user' as const,\n id: account.deviceId,\n name: account.deviceInfo.name || account.deviceId,\n handle: account.deviceId,\n raw: account,\n }));\n\n if (params.query) {\n const q = params.query.toLowerCase();\n entries = entries.filter(\n e => e.id.toLowerCase().includes(q) || (e.name?.toLowerCase().includes(q) ?? false)\n );\n }\n\n if (params.limit != null && params.limit > 0) {\n entries = entries.slice(0, params.limit);\n }\n\n return entries;\n },\n };\n}\n","/**\n * Messaging Adapter - Target normalization and session resolution\n *\n * Implements ChannelMessagingAdapter:\n * - normalizeTarget: strip \"lingyao:\" prefix, return pure deviceId\n * - resolveSessionTarget: return \"lingyao:{id}\" format\n * - resolveSessionConversation: map rawId to conversation structure\n * - inferTargetChatType: always \"direct\" (Lingyao has no groups)\n */\n\nconst PREFIX = 'lingyao:';\n\nexport function createMessagingAdapter() {\n return {\n normalizeTarget(raw: string): string | undefined {\n if (!raw || typeof raw !== 'string') {\n return undefined;\n }\n\n const target = raw.startsWith(PREFIX)\n ? raw.slice(PREFIX.length)\n : raw;\n\n if (!target) {\n return undefined;\n }\n\n return target;\n },\n\n resolveSessionTarget(params: {\n kind: 'direct' | 'group' | 'channel';\n id: string;\n threadId?: string | null;\n }): string | undefined {\n return `${PREFIX}${params.id}`;\n },\n\n resolveSessionConversation(params: {\n kind: 'group' | 'channel';\n rawId: string;\n }): {\n id: string;\n threadId?: string | null;\n baseConversationId?: string | null;\n parentConversationCandidates?: string[];\n } | null {\n // Lingyao has no group/channel encoding in rawId — treat as passthrough\n return {\n id: params.rawId,\n baseConversationId: params.rawId,\n };\n },\n\n inferTargetChatType(_params: {\n to: string;\n }): 'direct' | undefined {\n return 'direct';\n },\n };\n}\n","/**\n * Outbound Adapter - Send messages from Agent to App devices\n *\n * Implements ChannelOutboundAdapter:\n * - deliveryMode: \"direct\" (synchronous via WS)\n * - sendText: send plain text notification\n * - sendPayload: send structured payload notification\n * - resolveTarget: validate and resolve target deviceId\n */\n\nimport type { OpenClawConfig } from 'openclaw/plugin-sdk';\nimport type { MultiAccountOrchestrator } from '../orchestrator.js';\n\n/**\n * Outbound types (not exported by SDK, defined locally to match contract).\n */\nexport interface ChannelOutboundContext {\n cfg: OpenClawConfig;\n to: string;\n text: string;\n mediaUrl?: string;\n accountId?: string | null;\n silent?: boolean;\n}\n\nexport interface ChannelOutboundPayloadContext extends ChannelOutboundContext {\n payload: unknown;\n}\n\nexport interface OutboundDeliveryResult {\n channel: string;\n messageId: string;\n chatId?: string;\n timestamp?: number;\n meta?: Record<string, unknown>;\n}\n\n/**\n * Create the outbound adapter.\n */\nexport function createOutboundAdapter(\n getOrchestrator: () => MultiAccountOrchestrator | null\n) {\n return {\n deliveryMode: 'direct' as const,\n\n async sendText(ctx: ChannelOutboundContext): Promise<OutboundDeliveryResult> {\n const orchestrator = getOrchestrator();\n if (!orchestrator) {\n throw new Error('Orchestrator not initialized');\n }\n\n const accountId = ctx.accountId ?? 'default';\n const sent = orchestrator.sendNotification(\n accountId,\n ctx.to,\n { title: 'OpenClaw', body: ctx.text }\n );\n\n if (!sent) {\n throw new Error(\n `Failed to send text to device \"${ctx.to}\" on account \"${accountId}\": not connected`\n );\n }\n\n return {\n channel: 'lingyao',\n messageId: `out_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,\n chatId: ctx.to,\n timestamp: Date.now(),\n };\n },\n\n async sendPayload(ctx: ChannelOutboundPayloadContext): Promise<OutboundDeliveryResult> {\n const orchestrator = getOrchestrator();\n if (!orchestrator) {\n throw new Error('Orchestrator not initialized');\n }\n\n const accountId = ctx.accountId ?? 'default';\n const sent = orchestrator.sendNotification(\n accountId,\n ctx.to,\n ctx.payload as Record<string, unknown>\n );\n\n if (!sent) {\n throw new Error(\n `Failed to send payload to device \"${ctx.to}\" on account \"${accountId}\": not connected`\n );\n }\n\n return {\n channel: 'lingyao',\n messageId: `out_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,\n chatId: ctx.to,\n timestamp: Date.now(),\n };\n },\n\n resolveTarget(params: {\n cfg?: OpenClawConfig;\n to?: string;\n accountId?: string | null;\n }): { ok: true; to: string } | { ok: false; error: Error } {\n const raw = params.to;\n if (!raw || typeof raw !== 'string' || raw.trim().length === 0) {\n return { ok: false, error: new Error('Target deviceId is empty or missing') };\n }\n return { ok: true, to: raw.trim() };\n },\n };\n}\n","/**\n * Setup adapter for Lingyao channel plugin.\n *\n * Lingyao has no tokens or bot credentials — setup simply enables\n * the channel and the default account. DM policy defaults to \"pairing\".\n */\n\nimport { createPatchedAccountSetupAdapter } from 'openclaw/plugin-sdk/setup-runtime';\nimport type { ChannelSetupAdapter } from 'openclaw/plugin-sdk';\n\nexport function createSetupAdapter(): ChannelSetupAdapter {\n return createPatchedAccountSetupAdapter({\n channelKey: 'lingyao',\n alwaysUseAccounts: true,\n ensureChannelEnabled: true,\n ensureAccountEnabled: true,\n buildPatch() {\n // Lingyao uses device pairing via relay server, no credentials needed.\n // The default dmPolicy (\"pairing\") and empty allowFrom are sufficient.\n return {};\n },\n });\n}\n","/**\n * Multi-Account Orchestrator\n *\n * Manages per-account WS/HTTP instances for multi-account support.\n * Each account gets independent LingyaoWSClient, ServerHttpClient,\n * AccountManager, Probe, and Monitor instances.\n *\n * Data flow:\n * Gateway Adapter → orchestrator.start(account, { channelRuntime, cfg }) → WS connect\n * Inbound (App) → WS message → orchestrator → dispatchInboundDirectDmWithRuntime → Agent\n * Outbound (Agent)→ deliver callback → orchestrator.sendNotification() → WS → App\n */\n\nimport { hostname, networkInterfaces } from 'node:os';\nimport { createHash } from 'node:crypto';\nimport { LINGYAO_SERVER_URL, getLingyaoGatewayWsUrl } from './types.js';\nimport { resolveLingyaoWsHeartbeatIntervalMs } from './ws-heartbeat-interval.js';\nimport type { LingyaoRuntime } from './types.js';\nimport type { ResolvedAccount } from './adapters/config.js';\nimport type { PluginRuntime, OpenClawConfig } from 'openclaw/plugin-sdk';\nimport { dispatchInboundDirectDmWithRuntime } from 'openclaw/plugin-sdk/channel-inbound';\nimport { ServerHttpClient } from './server-client.js';\nimport { LingyaoWSClient } from './websocket-client.js';\nimport { AccountManager } from './accounts.js';\nimport { Probe } from './probe.js';\nimport { Monitor, MonitoringEvent } from './metrics.js';\nimport { ErrorHandler } from './errors.js';\n\n/**\n * Per-account runtime state\n */\ninterface AccountState {\n accountId: string;\n wsClient: LingyaoWSClient | null;\n httpClient: ServerHttpClient | null;\n accountManager: AccountManager;\n probe: Probe;\n monitor: Monitor;\n errorHandler: ErrorHandler;\n status: 'stopped' | 'starting' | 'running' | 'stopping' | 'error';\n startTime: number;\n gatewayId: string;\n channelRuntime?: PluginRuntime['channel'];\n cfg?: OpenClawConfig;\n /** One automatic clear+re-register after WS HTTP 404 (avoid infinite loops). */\n wsHandshake404RecoveryAttempted?: boolean;\n}\n\n/**\n * 获取机器的稳定标识符 (基于 MAC 地址)\n */\nfunction getMachineId(): string {\n try {\n const interfaces = networkInterfaces();\n const macSet = new Set<string>();\n\n // 收集所有非内部回环的 MAC 地址,去重后排序保证跨重启稳定\n for (const iface of Object.values(interfaces)) {\n if (!iface) continue;\n for (const alias of iface) {\n if (!alias.internal && alias.mac && alias.mac !== '00:00:00:00:00:00') {\n macSet.add(alias.mac);\n }\n }\n }\n const macs = [...macSet].sort();\n\n if (macs.length > 0) {\n // 排序后的 MAC 地址拼接,取 MD5 前 8 位作为机器稳定标识\n return createHash('md5').update(macs.join('')).digest('hex').substring(0, 8);\n }\n } catch (e) {\n // 忽略获取网卡失败的错误\n }\n\n // 回退:使用随机后缀(由于在内存中缓存,单次运行期间稳定)\n return Math.random().toString(36).substring(2, 10);\n}\n\n// 缓存 machineId,避免频繁计算\nconst MACHINE_ID = getMachineId();\n\n/**\n * Generate a gateway ID for an account\n * 使用主机名 + 机器稳定标识(MAC哈希) + 账户ID,确保跨重启稳定\n */\nfunction generateGatewayId(accountId: string): string {\n const host = hostname().split('.')[0].replace(/[^a-z0-9]/gi, '').toLowerCase();\n return `gw_openclaw_${host}_${MACHINE_ID}_${accountId}`;\n}\n\nexport class MultiAccountOrchestrator {\n private runtime: LingyaoRuntime;\n private accounts: Map<string, AccountState> = new Map();\n\n constructor(runtime: LingyaoRuntime) {\n this.runtime = runtime;\n }\n\n /**\n * Start an account: create components, register to server, connect WS.\n */\n async start(\n account: ResolvedAccount,\n context?: {\n channelRuntime?: PluginRuntime['channel'];\n cfg?: OpenClawConfig;\n }\n ): Promise<void> {\n const { id: accountId } = account;\n\n const existing = this.accounts.get(accountId);\n if (existing?.status === 'running') {\n this.runtime.logger.warn(`Account \"${accountId}\" is already running`);\n return;\n }\n\n this.runtime.logger.info(`Starting account \"${accountId}\"`);\n\n const gatewayId = account.gatewayId ?? generateGatewayId(accountId);\n const storagePrefix = `lingyao:${accountId}`;\n\n const accountManager = new AccountManager(this.runtime);\n const probe = new Probe(this.runtime);\n const monitor = new Monitor(this.runtime);\n const errorHandler = new ErrorHandler(this.runtime);\n const httpClient = new ServerHttpClient(\n this.runtime,\n gatewayId,\n { baseURL: LINGYAO_SERVER_URL },\n storagePrefix\n );\n\n const state: AccountState = {\n accountId,\n wsClient: null,\n httpClient,\n accountManager,\n probe,\n monitor,\n errorHandler,\n status: 'starting',\n startTime: Date.now(),\n gatewayId,\n channelRuntime: context?.channelRuntime,\n cfg: context?.cfg,\n };\n\n this.accounts.set(accountId, state);\n\n try {\n await accountManager.initialize();\n\n // Register to server (restore from storage or fresh register)\n await this.registerToServer(state);\n\n const wsHeartbeatMs = resolveLingyaoWsHeartbeatIntervalMs(account, httpClient);\n this.runtime.logger.info(`Lingyao WebSocket heartbeat interval: ${wsHeartbeatMs}ms (relay serverConfig / config)`);\n\n // Create and connect WebSocket client\n const wsClient = new LingyaoWSClient(this.runtime, {\n url: getLingyaoGatewayWsUrl(),\n gatewayId,\n token: httpClient.getGatewayToken() ?? undefined,\n reconnectInterval: 5000,\n heartbeatInterval: wsHeartbeatMs,\n messageHandler: this.createMessageHandler(state),\n eventHandler: this.createEventHandler(state),\n });\n\n await wsClient.connect();\n state.wsClient = wsClient;\n state.status = 'running';\n\n this.runtime.logger.info(`Account \"${accountId}\" started successfully`);\n } catch (error) {\n state.status = 'error';\n this.runtime.logger.error(`Failed to start account \"${accountId}\"`, error);\n throw error;\n }\n }\n\n /**\n * Stop an account: disconnect WS, stop heartbeat.\n */\n async stop(accountId: string): Promise<void> {\n const state = this.accounts.get(accountId);\n if (!state || state.status === 'stopped') {\n return;\n }\n\n this.runtime.logger.info(`Stopping account \"${accountId}\"`);\n state.status = 'stopping';\n\n if (state.wsClient) {\n state.wsClient.disconnect();\n state.wsClient = null;\n }\n\n if (state.httpClient) {\n state.httpClient.stopHeartbeat();\n }\n\n state.status = 'stopped';\n this.runtime.logger.info(`Account \"${accountId}\" stopped`);\n }\n\n /**\n * Stop all running accounts.\n */\n async stopAll(): Promise<void> {\n const stops = Array.from(this.accounts.keys()).map(id => this.stop(id));\n await Promise.all(stops);\n }\n\n /**\n * Get account state by ID.\n */\n getAccountState(accountId: string): AccountState | undefined {\n return this.accounts.get(accountId);\n }\n\n /**\n * Get account's WS client.\n */\n getWSClient(accountId: string): LingyaoWSClient | null {\n return this.accounts.get(accountId)?.wsClient ?? null;\n }\n\n /**\n * Get account's HTTP client.\n */\n getHttpClient(accountId: string): ServerHttpClient | null {\n return this.accounts.get(accountId)?.httpClient ?? null;\n }\n\n /**\n * Get account's AccountManager.\n */\n getAccountManager(accountId: string): AccountManager | null {\n return this.accounts.get(accountId)?.accountManager ?? null;\n }\n\n /**\n * Get account's Probe.\n */\n getProbe(accountId: string): Probe | null {\n return this.accounts.get(accountId)?.probe ?? null;\n }\n\n /**\n * Get account's Monitor.\n */\n getMonitor(accountId: string): Monitor | null {\n return this.accounts.get(accountId)?.monitor ?? null;\n }\n\n /**\n * Send notification to a device on a specific account.\n * Returns true if sent, false if WS not connected.\n */\n sendNotification(\n accountId: string,\n deviceId: string,\n notification: Record<string, unknown>\n ): boolean {\n const wsClient = this.getWSClient(accountId);\n if (!wsClient || !wsClient.isConnected()) {\n this.runtime.logger.warn(`[${accountId}] Cannot send notification: WS not connected`, { deviceId });\n return false;\n }\n\n this.runtime.logger.info(`[${accountId}] Sending notification to device`, {\n deviceId,\n notificationType: notification.type,\n });\n\n wsClient.sendNotification(deviceId, notification);\n return true;\n }\n\n /**\n * List all running account IDs.\n */\n getRunningAccountIds(): string[] {\n return Array.from(this.accounts.entries())\n .filter(([, state]) => state.status === 'running')\n .map(([id]) => id);\n }\n\n /**\n * Register to lingyao server for a specific account.\n */\n private async registerToServer(state: AccountState): Promise<void> {\n if (!state.httpClient) {\n throw new Error('HTTP client not available');\n }\n\n try {\n const restored = await state.httpClient.restoreFromStorage();\n if (restored) {\n this.runtime.logger.info(`Account \"${state.accountId}\": session restored from storage`);\n return;\n }\n\n this.runtime.logger.info(`Account \"${state.accountId}\": registering to server...`, {\n gatewayId: state.gatewayId,\n });\n\n const response = await state.httpClient.register({\n websocket: true,\n compression: false,\n maxMessageSize: 1048576,\n });\n\n this.runtime.logger.info(`Account \"${state.accountId}\": registered successfully`, {\n expiresAt: new Date(response.expiresAt).toISOString(),\n });\n } catch (error) {\n this.runtime.logger.error(`Account \"${state.accountId}\": registration failed`, error);\n // Don't throw - WS client will attempt connection anyway and auto-reconnect\n }\n }\n\n /**\n * Create message handler for inbound App messages on a specific account.\n */\n private createMessageHandler(state: AccountState) {\n return async (message: any): Promise<void> => {\n try {\n const appMessage = message.payload;\n const deviceId = appMessage.deviceId;\n const msg = appMessage.message;\n\n this.runtime.logger.info(`[${state.accountId}] Received message from App`, {\n deviceId,\n messageType: msg.type,\n messageId: msg.id,\n });\n\n state.monitor.recordEvent(MonitoringEvent.MESSAGE_RECEIVED, {\n deviceId,\n messageType: msg.type,\n });\n\n switch (msg.type) {\n case 'sync_diary':\n case 'sync_memory':\n await this.handleSyncMessage(state, deviceId, msg);\n break;\n case 'heartbeat':\n await state.accountManager.updateLastSeen(deviceId);\n state.monitor.recordEvent(MonitoringEvent.HEARTBEAT_RECEIVED, { deviceId });\n break;\n default:\n this.runtime.logger.warn(`[${state.accountId}] Unknown message type`, { type: msg.type });\n }\n } catch (error) {\n this.runtime.logger.error(`[${state.accountId}] Error handling App message`, error);\n state.monitor.recordEvent(MonitoringEvent.ERROR_OCCURRED, {\n errorType: 'message_handling',\n });\n }\n };\n }\n\n /**\n * Handle sync message (diary or memory) via SDK dispatch pipeline.\n */\n private async handleSyncMessage(\n state: AccountState,\n deviceId: string,\n message: {\n id: string;\n type: 'sync_diary' | 'sync_memory';\n timestamp: number;\n content: string;\n metadata?: Record<string, unknown>;\n }\n ): Promise<void> {\n if (!state.channelRuntime || !state.cfg) {\n this.runtime.logger.warn(`[${state.accountId}] channelRuntime/cfg not available, cannot dispatch to Agent`, {\n messageId: message.id,\n messageType: message.type,\n deviceId,\n });\n return;\n }\n\n this.runtime.logger.info(`[${state.accountId}] Dispatching sync message to Agent via SDK pipeline`, {\n messageId: message.id,\n messageType: message.type,\n deviceId,\n contentLength: message.content?.length ?? 0,\n });\n\n const dmRuntime = {\n channel: {\n routing: {\n resolveAgentRoute: state.channelRuntime.routing.resolveAgentRoute,\n },\n session: {\n resolveStorePath: state.channelRuntime.session.resolveStorePath,\n readSessionUpdatedAt: state.channelRuntime.session.readSessionUpdatedAt,\n recordInboundSession: state.channelRuntime.session.recordInboundSession,\n },\n reply: {\n resolveEnvelopeFormatOptions: state.channelRuntime.reply.resolveEnvelopeFormatOptions,\n formatAgentEnvelope: state.channelRuntime.reply.formatAgentEnvelope,\n finalizeInboundContext: state.channelRuntime.reply.finalizeInboundContext,\n dispatchReplyWithBufferedBlockDispatcher:\n state.channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher,\n },\n },\n };\n\n try {\n await dispatchInboundDirectDmWithRuntime({\n cfg: state.cfg,\n runtime: dmRuntime,\n channel: 'lingyao',\n channelLabel: '灵爻',\n accountId: state.accountId,\n peer: { kind: 'direct', id: deviceId },\n senderId: deviceId,\n senderAddress: deviceId,\n recipientAddress: state.gatewayId,\n conversationLabel: `灵爻 DM with ${deviceId}`,\n rawBody: message.content,\n messageId: message.id,\n timestamp: message.timestamp,\n bodyForAgent: message.content,\n provider: 'lingyao',\n surface: 'lingyao',\n extraContext: {\n messageType: message.type === 'sync_diary' ? 'diary' : 'memory',\n metadata: message.metadata || {},\n },\n deliver: async (payload) => {\n const sent = this.sendNotification(state.accountId, deviceId, {\n type: 'agent_reply',\n text: payload.text,\n mediaUrl: payload.mediaUrl,\n mediaUrls: payload.mediaUrls,\n });\n if (!sent) {\n this.runtime.logger.warn(\n `[${state.accountId}] Failed to deliver Agent reply to ${deviceId}: WS not connected`\n );\n }\n },\n onRecordError: (err: unknown) => {\n this.runtime.logger.error(`[${state.accountId}] Failed to record inbound session`, err);\n },\n onDispatchError: (err: unknown, info: { kind: string }) => {\n this.runtime.logger.error(`[${state.accountId}] Dispatch error`, { err, kind: info.kind });\n },\n });\n } catch (error) {\n this.runtime.logger.error(`[${state.accountId}] dispatchInboundDirectDmWithRuntime failed`, error);\n throw error;\n }\n }\n\n /**\n * Create event handler for WS connection events on a specific account.\n */\n private createEventHandler(state: AccountState) {\n return (event: any): void => {\n switch (event.type) {\n case 'connected':\n this.runtime.logger.info(`[${state.accountId}] WS connected`, {\n connectionId: event.connectionId,\n });\n state.monitor.recordEvent(MonitoringEvent.CONNECTION_OPEN, {\n connectionId: event.connectionId,\n });\n break;\n case 'disconnected':\n this.runtime.logger.warn(`[${state.accountId}] WS disconnected`, {\n code: event.code,\n reason: event.reason,\n });\n state.monitor.recordEvent(MonitoringEvent.CONNECTION_CLOSE, {\n code: event.code,\n reason: event.reason,\n });\n\n // 如果是因为 Token 无效导致的断开,触发重新注册和重连\n if (event.code === 1008) {\n this.runtime.logger.warn(`[${state.accountId}] Token invalid (1008). Forcing re-registration...`);\n this.handleInvalidToken(state).catch(err => {\n this.runtime.logger.error(`[${state.accountId}] Failed to re-register after 1008`, err);\n });\n }\n break;\n case 'error':\n this.runtime.logger.error(`[${state.accountId}] WS error`, event.error);\n state.probe.recordError(event.error.message, 'websocket');\n state.monitor.recordEvent(MonitoringEvent.CONNECTION_ERROR, {\n error: event.error,\n });\n state.errorHandler.handleError(event.error);\n break;\n case 'fatal_handshake':\n if (event.reason === 'http_404') {\n void this.handleWsHandshake404(state);\n }\n break;\n case 'pairing_completed':\n this.handlePairingCompleted(state, event);\n break;\n }\n };\n }\n\n /**\n * Handle pairing completed event from WS — auto-bind device.\n */\n private async handlePairingCompleted(state: AccountState, event: any): Promise<void> {\n const { deviceId, deviceInfo } = event;\n this.runtime.logger.info(`[${state.accountId}] Pairing completed, auto-binding device`, { deviceId, deviceInfo });\n\n try {\n await state.accountManager.addDevice(deviceId, {\n name: deviceInfo?.name ?? deviceId,\n platform: deviceInfo?.platform ?? 'harmonyos',\n version: deviceInfo?.version ?? '',\n });\n this.runtime.logger.info(`[${state.accountId}] Device auto-bound: ${deviceId}`);\n } catch (error) {\n this.runtime.logger.error(`[${state.accountId}] Failed to auto-bind device: ${deviceId}`, error);\n }\n }\n\n /**\n * After HTTP 404 on WebSocket upgrade: clear local tokens, re-register once, reconnect.\n * Does not fix wrong server URL or permanently deleted gateway rows (user must fix config).\n */\n private async handleWsHandshake404(state: AccountState): Promise<void> {\n if (state.wsHandshake404RecoveryAttempted) {\n this.runtime.logger.error(\n `[${state.accountId}] Lingyao WebSocket still failing after one recovery attempt. ` +\n `Check that wss://…/lyoc/gateway/ws is deployed; if the gateway was removed, delete ` +\n `\\`channels.lingyao.accounts.${state.accountId}.gatewayId\\` or use a new account id, then restart.`\n );\n return;\n }\n state.wsHandshake404RecoveryAttempted = true;\n\n const { httpClient, wsClient, accountId } = state;\n if (!httpClient || !wsClient) {\n return;\n }\n\n this.runtime.logger.info(`[${accountId}] WS HTTP 404 — clearing local session and re-registering...`);\n\n try {\n await httpClient.clearLocalSession();\n const response = await httpClient.register({\n websocket: true,\n compression: false,\n maxMessageSize: 1048576,\n });\n wsClient.updateToken(response.gatewayToken);\n await wsClient.connect();\n this.runtime.logger.info(`[${accountId}] Re-register OK; WebSocket reconnect issued.`);\n } catch (error) {\n const msg = error instanceof Error ? error.message : String(error);\n if (msg.includes('already registered')) {\n this.runtime.logger.error(\n `[${accountId}] Re-register failed: gateway still registered on server but local session was cleared. ` +\n `Remove or change \\`gatewayId\\` under this account, or reset the gateway on lingyao.live, then restart OpenClaw.`,\n error\n );\n } else {\n this.runtime.logger.error(`[${accountId}] WS 404 recovery (clear + register) failed`, error);\n }\n }\n }\n\n /**\n * Handle invalid token by re-registering the gateway and reconnecting WS\n */\n private async handleInvalidToken(state: AccountState): Promise<void> {\n if (!state.httpClient || !state.wsClient) {\n return;\n }\n\n try {\n this.runtime.logger.info(`[${state.accountId}] Requesting new gateway token...`);\n // 强制重新注册以获取新 token\n const response = await state.httpClient.register({\n websocket: true,\n compression: false,\n maxMessageSize: 1048576,\n });\n\n this.runtime.logger.info(`[${state.accountId}] Obtained new token. Reconnecting WS...`);\n // 更新 WS 客户端的 token 并重新连接\n state.wsClient.updateToken(response.gatewayToken);\n await state.wsClient.connect();\n } catch (error) {\n this.runtime.logger.error(`[${state.accountId}] Failed to handle invalid token`, error);\n // 可以在此处接入 errorHandler 以实现更高级的退避重试\n state.errorHandler.handleError(error as Error);\n }\n }\n}\n","/**\n * 灵爻服务器 HTTP 客户端\n *\n * 主动连接到灵爻服务器,实现 Gateway 注册、心跳、消息推送\n * 符合 openapi.yaml 规范\n */\n\nimport axios from 'axios';\nimport type { LingyaoRuntime } from './types.js';\n\n// 类型定义 - 避免 axios 类型导出在 DTS 构建中的问题\ntype AxiosInstance = ReturnType<typeof axios.create>;\n\ninterface AxiosErrorLike extends Error {\n response?: {\n status?: number;\n data?: any;\n };\n config?: any;\n code?: string;\n isAxiosError?: boolean;\n}\n\n/**\n * axios 错误类型守卫\n */\nfunction isAxiosError(error: unknown): error is AxiosErrorLike {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'isAxiosError' in error &&\n (error as AxiosErrorLike).isAxiosError === true\n );\n}\n\n/**\n * 服务器 API 配置\n */\nexport interface ServerConfig {\n baseURL: string;\n apiBase: string;\n timeout: number;\n connectionTimeout: number;\n}\n\n/**\n * Gateway 注册响应\n */\nexport interface GatewayRegisterResponse {\n gatewayToken: string;\n expiresAt: number;\n webhookSecret: string;\n serverConfig: {\n heartbeatInterval: number;\n maxOfflineMessages: number;\n supportedMessageTypes: string[];\n };\n}\n\n/**\n * 心跳响应\n */\nexport interface HeartbeatResponse {\n serverTime: number;\n pendingMessages: number;\n}\n\n/**\n * 发送消息请求\n */\nexport interface SendMessageRequest {\n deviceId: string;\n message: {\n id: string;\n type: 'notify_text' | 'notify_action';\n timestamp: number;\n payload: {\n title?: string;\n body?: string;\n action?: {\n type: 'open_memory' | 'view_diary' | 'custom';\n params?: Record<string, unknown>;\n };\n };\n };\n options?: {\n priority?: 'normal' | 'low' | 'high';\n ttl?: number;\n };\n}\n\n/**\n * 发送消息响应\n */\nexport interface SendMessageResponse {\n messageId: string;\n status: 'queued' | 'delivered' | 'failed';\n deliveredAt: number | null;\n queued: boolean;\n}\n\n/**\n * Gateway 状态\n */\nexport type GatewayStatus = 'online' | 'offline' | 'maintenance';\n\n/**\n * HTTP 客户端实现\n */\nexport class ServerHttpClient {\n private runtime: LingyaoRuntime;\n private config: ServerConfig;\n private axiosInstance: AxiosInstance;\n private gatewayToken: string | null = null;\n private webhookSecret: string | null = null;\n private tokenExpiresAt: number = 0;\n private heartbeatInterval: number = 30000;\n private heartbeatTimer: NodeJS.Timeout | null = null;\n private gatewayId: string;\n private isRegistered: boolean = false;\n private isConnecting: boolean = false;\n\n private storagePrefix: string;\n\n constructor(\n runtime: LingyaoRuntime,\n gatewayId: string,\n serverConfig: Partial<ServerConfig> = {},\n storagePrefix: string = 'lingyao'\n ) {\n this.runtime = runtime;\n this.gatewayId = gatewayId;\n this.storagePrefix = storagePrefix;\n this.config = {\n baseURL: serverConfig.baseURL || 'https://api.lingyao.live',\n // Public API (api.lingyao.live) serves gateway HTTP under /lyoc; local relay also accepts /lyoc (see server getApiPathSuffix).\n apiBase: serverConfig.apiBase || '/lyoc',\n timeout: serverConfig.timeout || 30000,\n connectionTimeout: serverConfig.connectionTimeout || 5000,\n };\n\n // 创建 axios 实例\n this.axiosInstance = axios.create({\n baseURL: this.config.baseURL + this.config.apiBase,\n timeout: this.config.timeout,\n headers: {\n 'Content-Type': 'application/json',\n },\n });\n\n // 请求拦截器 - 添加 Token\n this.axiosInstance.interceptors.request.use(\n (config) => {\n if (this.gatewayToken && config.headers) {\n config.headers.Authorization = `Bearer ${this.gatewayToken}`;\n }\n return config;\n },\n (error) => Promise.reject(error)\n );\n\n // 响应拦截器 - 处理错误\n this.axiosInstance.interceptors.response.use(\n (response) => response,\n async (error: unknown) => {\n if (isAxiosError(error)) {\n if (error.response?.status === 401) {\n // Token 过期,尝试刷新\n this.runtime.logger.warn('Token expired, attempting to re-register...');\n await this.register();\n }\n }\n return Promise.reject(error);\n }\n );\n }\n\n /**\n * 注册 Gateway 到服务器\n */\n async register(\n capabilities: {\n websocket?: boolean;\n compression?: boolean;\n maxMessageSize?: number;\n } = {}\n ): Promise<GatewayRegisterResponse> {\n if (this.isConnecting) {\n throw new Error('Registration already in progress');\n }\n\n this.isConnecting = true;\n\n try {\n this.runtime.logger.info(`Registering gateway ${this.gatewayId} to server...`);\n\n const response = await this.axiosInstance.post<GatewayRegisterResponse>(\n '/gateway/register',\n {\n gatewayId: this.gatewayId,\n version: '0.1.0',\n capabilities: {\n websocket: false,\n compression: false,\n ...capabilities,\n },\n }\n );\n\n const data = response.data;\n\n this.gatewayToken = data.gatewayToken;\n this.webhookSecret = data.webhookSecret;\n this.tokenExpiresAt = data.expiresAt;\n this.heartbeatInterval = data.serverConfig.heartbeatInterval;\n this.isRegistered = true;\n\n // 保存到存储\n await this.runtime.storage.set(this.storageKey('gatewayToken'), this.gatewayToken);\n await this.runtime.storage.set(this.storageKey('webhookSecret'), this.webhookSecret);\n await this.runtime.storage.set(this.storageKey('tokenExpiresAt'), this.tokenExpiresAt);\n await this.runtime.storage.set(this.storageKey('serverConfig'), data.serverConfig);\n\n this.runtime.logger.info('Gateway registered successfully', {\n expiresAt: new Date(this.tokenExpiresAt).toISOString(),\n heartbeatInterval: this.heartbeatInterval,\n });\n\n // 启动心跳\n this.startHeartbeat();\n\n return data;\n } catch (error: unknown) {\n if (isAxiosError(error)) {\n const axiosError = error as AxiosErrorLike;\n const status = axiosError.response?.status;\n const data = axiosError.response?.data as { code?: string; details?: string };\n\n if (status === 409) {\n throw new Error('Gateway already registered');\n } else if (status === 400) {\n throw new Error(`Invalid request: ${data?.details || 'Unknown error'}`);\n } else if (status === 404) {\n throw new Error(\n 'Lingyao gateway register returned 404. Check server URL and /lyoc/gateway/register; ' +\n 'the gatewayId may be invalid or the API may not be deployed on this host.'\n );\n }\n\n throw new Error(`Registration failed: ${axiosError.message}`);\n }\n throw error;\n } finally {\n this.isConnecting = false;\n }\n }\n\n /**\n * 发送心跳\n */\n async heartbeat(\n status: GatewayStatus = 'online',\n activeConnections: number = 0\n ): Promise<HeartbeatResponse> {\n if (!this.isRegistered || !this.gatewayToken) {\n throw new Error('Gateway not registered');\n }\n\n try {\n const response = await this.axiosInstance.post<HeartbeatResponse>(\n '/gateway/heartbeat',\n {\n timestamp: Date.now(),\n status,\n activeConnections,\n }\n );\n\n return response.data;\n } catch (error: unknown) {\n if (isAxiosError(error)) {\n const axiosError = error as AxiosErrorLike;\n this.runtime.logger.error('Heartbeat failed', {\n status: axiosError.response?.status,\n data: axiosError.response?.data,\n });\n }\n throw error;\n }\n }\n\n /**\n * 发送消息到灵爻 App\n */\n async sendMessage(\n deviceId: string,\n messageType: 'notify_text' | 'notify_action',\n payload: SendMessageRequest['message']['payload'],\n options?: SendMessageRequest['options']\n ): Promise<SendMessageResponse> {\n if (!this.isRegistered || !this.gatewayToken) {\n throw new Error('Gateway not registered');\n }\n\n const messageId = this.generateMessageId();\n\n try {\n const response = await this.axiosInstance.post<SendMessageResponse>(\n '/gateway/messages',\n {\n deviceId,\n message: {\n id: messageId,\n type: messageType,\n timestamp: Date.now(),\n payload,\n },\n options: options || {},\n }\n );\n\n this.runtime.logger.debug('Message sent', {\n messageId,\n deviceId,\n status: response.data.status,\n });\n\n return response.data;\n } catch (error: unknown) {\n if (isAxiosError(error)) {\n const axiosError = error as AxiosErrorLike;\n const status = axiosError.response?.status;\n const data = axiosError.response?.data as { error?: string };\n\n if (status === 404) {\n throw new Error(`Device not found: ${deviceId}`);\n } else if (status === 429) {\n throw new Error('Message queue full, please retry later');\n }\n\n throw new Error(`Send message failed: ${data?.error || axiosError.message}`);\n }\n throw error;\n }\n }\n\n /**\n * 启动心跳循环\n */\n private startHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n }\n\n this.heartbeatTimer = setInterval(\n async () => {\n try {\n await this.heartbeat();\n } catch (error) {\n this.runtime.logger.error('Heartbeat error', error);\n }\n },\n this.heartbeatInterval\n );\n\n this.runtime.logger.info('Heartbeat started', {\n interval: this.heartbeatInterval,\n });\n }\n\n /**\n * 停止心跳循环\n */\n stopHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n this.runtime.logger.info('Heartbeat stopped');\n }\n }\n\n /**\n * 获取 Webhook Secret\n */\n getWebhookSecret(): string | null {\n return this.webhookSecret;\n }\n\n /**\n * 获取 Gateway Token\n */\n getGatewayToken(): string | null {\n return this.gatewayToken;\n }\n\n /**\n * HTTP 注册/恢复后由 `serverConfig.heartbeatInterval` 写入(毫秒),用于与 WebSocket `gateway_heartbeat` 对齐。\n */\n getHeartbeatIntervalMs(): number {\n return this.heartbeatInterval > 0 ? this.heartbeatInterval : 30000;\n }\n\n /**\n * 检查 Token 是否即将过期\n */\n isTokenExpiringSoon(thresholdMs: number = 7 * 24 * 60 * 60 * 1000): boolean {\n return this.tokenExpiresAt - Date.now() < thresholdMs;\n }\n\n /**\n * 检查是否已注册\n */\n isReady(): boolean {\n return this.isRegistered && !!this.gatewayToken;\n }\n\n /**\n * Clear local gateway token/session (storage + in-memory). Used when WS handshake\n * fails with 404 or when forcing re-registration with the same gatewayId.\n */\n async clearLocalSession(): Promise<void> {\n this.stopHeartbeat();\n this.gatewayToken = null;\n this.webhookSecret = null;\n this.tokenExpiresAt = 0;\n this.isRegistered = false;\n\n const keys = ['gatewayToken', 'webhookSecret', 'tokenExpiresAt', 'serverConfig'] as const;\n for (const k of keys) {\n try {\n await this.runtime.storage.delete(this.storageKey(k));\n } catch (e) {\n this.runtime.logger.warn(`Failed to delete storage key ${k}`, e);\n }\n }\n\n this.runtime.logger.info('Cleared local Lingyao gateway session');\n }\n\n /**\n * 从存储恢复会话\n */\n async restoreFromStorage(): Promise<boolean> {\n try {\n const token = await this.runtime.storage.get(this.storageKey('gatewayToken'));\n const secret = await this.runtime.storage.get(this.storageKey('webhookSecret'));\n const expiresAt = await this.runtime.storage.get(this.storageKey('tokenExpiresAt')) as number | undefined;\n const serverConfig = await this.runtime.storage.get(this.storageKey('serverConfig')) as GatewayRegisterResponse['serverConfig'] | undefined;\n\n if (token && secret && expiresAt && serverConfig) {\n // 检查是否过期\n if (expiresAt > Date.now()) {\n this.gatewayToken = token as string;\n this.webhookSecret = secret as string;\n this.tokenExpiresAt = expiresAt;\n this.heartbeatInterval = serverConfig.heartbeatInterval;\n this.isRegistered = true;\n\n // 启动心跳\n this.startHeartbeat();\n\n this.runtime.logger.info('Session restored from storage', {\n expiresAt: new Date(this.tokenExpiresAt).toISOString(),\n });\n\n return true;\n } else {\n this.runtime.logger.warn('Stored token expired, need to re-register');\n }\n }\n } catch (error) {\n this.runtime.logger.error('Failed to restore session from storage', error);\n }\n\n return false;\n }\n\n /**\n * Build a namespaced storage key for multi-account support\n */\n private storageKey(name: string): string {\n return `${this.storagePrefix}:${name}`;\n }\n\n /**\n * 生成消息 ID\n */\n private generateMessageId(): string {\n return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;\n }\n\n}\n","/**\n * Lingyao WebSocket Client\n *\n * 主动连接到 lingyao.live 服务器的 WebSocket 客户端\n * 实现:\n * - 自动连接和重连\n * - 心跳机制\n * - 消息发送和接收\n * - 在线状态管理\n */\n\nimport WebSocket from \"ws\";\nimport type { LingyaoRuntime } from \"./types.js\";\n\n/**\n * WebSocket 连接状态\n */\nexport type ConnectionState = \"connecting\" | \"connected\" | \"disconnected\" | \"error\";\n\n/**\n * WebSocket 消息类型(与 `lingyao/server/src/server.ts` 中 Gateway 协议一致)\n */\nexport enum WSMessageType {\n // Gateway → 服务器\n GATEWAY_REGISTER = \"gateway_register\",\n GATEWAY_HEARTBEAT = \"gateway_heartbeat\",\n GATEWAY_SEND_MESSAGE = \"gateway_send_message\",\n\n // 服务器 → Gateway\n GATEWAY_REGISTERED = \"gateway_registered\",\n GATEWAY_HEARTBEAT_ACK = \"gateway_heartbeat_ack\",\n MESSAGE_DELIVERED = \"message_delivered\",\n MESSAGE_FAILED = \"message_failed\",\n APP_MESSAGE = \"app_message\",\n DEVICE_ONLINE = \"device_online\",\n PAIRING_COMPLETED = \"pairing_completed\",\n ERROR = \"error\",\n}\n\n/**\n * WebSocket 消息基础格式\n */\nexport interface WSMessage {\n type: WSMessageType | string;\n id: string;\n timestamp: number;\n payload?: any;\n}\n\n/**\n * 注册消息\n */\nexport interface RegisterMessage extends WSMessage {\n type: WSMessageType.GATEWAY_REGISTER;\n payload: {\n gatewayId: string;\n version: string;\n capabilities: {\n websocket: boolean;\n compression: boolean;\n maxMessageSize: number;\n };\n };\n}\n\n/**\n * 心跳消息\n */\nexport interface HeartbeatMessage extends WSMessage {\n type: WSMessageType.GATEWAY_HEARTBEAT;\n payload: {\n timestamp: number;\n status: \"online\";\n };\n}\n\n/**\n * 发送消息\n */\nexport interface SendMessage extends WSMessage {\n type: WSMessageType.GATEWAY_SEND_MESSAGE;\n payload: {\n deviceId: string;\n message: {\n id: string;\n type: \"notify_text\" | \"notify_action\";\n timestamp: number;\n payload: any;\n };\n };\n}\n\n/**\n * 接收到的 App 消息\n */\nexport interface AppMessage extends WSMessage {\n type: WSMessageType.APP_MESSAGE;\n payload: {\n deviceId: string;\n message: {\n id: string;\n type: \"sync_diary\" | \"sync_memory\" | \"heartbeat\";\n timestamp: number;\n content: string;\n metadata?: Record<string, unknown>;\n };\n };\n}\n\n/**\n * WebSocket 客户端事件\n */\nexport type WSClientEvent =\n | { type: \"connected\"; connectionId: string }\n | { type: \"disconnected\"; code: number; reason: string }\n | { type: \"error\"; error: Error }\n | { type: \"fatal_handshake\"; reason: \"http_404\" }\n | { type: \"message\"; message: WSMessage }\n | { type: \"appMessage\"; deviceId: string; message: AppMessage[\"payload\"][\"message\"] }\n | { type: \"pairing_completed\"; deviceId: string; deviceInfo: { name: string; platform: string; version: string }; sessionId: string };\n\n/** True when the `ws` library reports HTTP 404 on the WebSocket upgrade (invalid path or gateway rejected at edge). */\nexport function isWebsocketUpgradeNotFoundError(message: string): boolean {\n return /Unexpected server response:\\s*404/i.test(message) || /\\b404\\b/.test(message);\n}\n\n/**\n * 将服务端类型映射到本客户端 handler 使用的 key(与 {@link WSMessageType} 一致)。\n * 仍接受旧版短名 `registered` / `heartbeat_ack`,便于与历史部署混连。\n */\nexport function normalizeIncomingGatewayMessageType(type: string): string {\n switch (type) {\n case \"registered\":\n return WSMessageType.GATEWAY_REGISTERED;\n case \"heartbeat_ack\":\n return WSMessageType.GATEWAY_HEARTBEAT_ACK;\n case \"gateway_registered\":\n return WSMessageType.GATEWAY_REGISTERED;\n case \"gateway_heartbeat_ack\":\n return WSMessageType.GATEWAY_HEARTBEAT_ACK;\n default:\n return type;\n }\n}\n\n/**\n * WebSocket 客户端配置\n */\nexport interface WSClientConfig {\n url: string;\n gatewayId: string;\n token?: string;\n reconnectInterval: number;\n heartbeatInterval: number;\n messageHandler?: (message: AppMessage) => void | Promise<void>;\n eventHandler?: (event: WSClientEvent) => void;\n}\n\n/**\n * Lingyao WebSocket Client\n *\n * 主动连接到 lingyao.live 服务器的 WebSocket 客户端\n */\nexport class LingyaoWSClient {\n private config: WSClientConfig;\n private ws: WebSocket | null = null;\n private state: ConnectionState = \"disconnected\";\n private connectionId: string | null = null;\n private heartbeatTimer: NodeJS.Timeout | null = null;\n private reconnectTimer: NodeJS.Timeout | null = null;\n /** When set, close handler will not schedule reconnect (e.g. HTTP 404 on upgrade). */\n private suppressReconnect = false;\n private messageHandlers: Map<string, (msg: WSMessage) => void> = new Map();\n private logger: LingyaoRuntime[\"logger\"];\n\n constructor(runtime: LingyaoRuntime, config: WSClientConfig) {\n this.logger = runtime.logger;\n this.config = { ...config };\n\n this.registerMessageHandler(WSMessageType.GATEWAY_REGISTERED, this.handleRegistered.bind(this));\n this.registerMessageHandler(WSMessageType.GATEWAY_HEARTBEAT_ACK, this.handleHeartbeatAck.bind(this));\n this.registerMessageHandler(WSMessageType.MESSAGE_DELIVERED, this.handleMessageDelivered.bind(this));\n this.registerMessageHandler(WSMessageType.MESSAGE_FAILED, this.handleMessageFailed.bind(this));\n this.registerMessageHandler(WSMessageType.APP_MESSAGE, this.handleAppMessage.bind(this));\n this.registerMessageHandler(WSMessageType.DEVICE_ONLINE, this.handleDeviceOnline.bind(this));\n this.registerMessageHandler(WSMessageType.PAIRING_COMPLETED, this.handlePairingCompleted.bind(this));\n this.registerMessageHandler(WSMessageType.ERROR, this.handleError.bind(this));\n }\n\n /**\n * 连接到服务器\n */\n async connect(): Promise<void> {\n if (this.state === \"connecting\" || this.state === \"connected\") {\n this.logger.warn(\"WebSocket already connecting or connected\");\n return;\n }\n\n this.state = \"connecting\";\n this.suppressReconnect = false;\n this.emitEvent({ type: \"disconnected\", code: 0, reason: \"Reconnecting\" });\n\n try {\n this.logger.info(`Connecting to Lingyao server: ${this.config.url}`);\n\n const wsUrl = this.config.token\n ? `${this.config.url}?token=${encodeURIComponent(this.config.token)}`\n : this.config.url;\n\n this.ws = new WebSocket(wsUrl, {\n headers: {\n \"X-Gateway-ID\": this.config.gatewayId,\n },\n });\n\n this.setupWebSocketHandlers();\n } catch (error) {\n this.state = \"error\";\n this.emitEvent({ type: \"error\", error: error as Error });\n this.scheduleReconnect();\n }\n }\n\n /**\n * 设置 WebSocket 事件处理器\n */\n private setupWebSocketHandlers(): void {\n if (!this.ws) return;\n\n this.ws.on(\"open\", () => {\n this.handleOpen();\n });\n\n this.ws.on(\"message\", async (data: Buffer) => {\n await this.handleMessage(data);\n });\n\n this.ws.on(\"error\", (error) => {\n this.handleErrorEvent(error);\n });\n\n this.ws.on(\"close\", (code: number, reason: Buffer) => {\n this.handleClose(code, reason.toString());\n });\n }\n\n /**\n * 处理连接打开\n */\n private handleOpen(): void {\n this.state = \"connected\";\n this.connectionId = this.generateConnectionId();\n\n this.logger.info(\"WebSocket connected to Lingyao server\", {\n connectionId: this.connectionId,\n });\n\n this.emitEvent({\n type: \"connected\",\n connectionId: this.connectionId!,\n });\n\n // 发送注册消息\n this.sendRegister();\n\n // 启动心跳\n this.startHeartbeat();\n\n // 清除重连定时器\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n }\n\n /**\n * 处理接收消息\n */\n private async handleMessage(data: Buffer): Promise<void> {\n try {\n const message: WSMessage = JSON.parse(data.toString());\n const rawType = String(message.type);\n const handlerKey = normalizeIncomingGatewayMessageType(rawType);\n this.logger.debug(\"Received message from server\", { type: rawType, handlerKey });\n\n const handler = this.messageHandlers.get(handlerKey);\n if (handler) {\n handler(message);\n } else {\n this.logger.warn(\"No handler for message type\", { type: rawType });\n }\n\n this.emitEvent({ type: \"message\", message });\n } catch (error) {\n this.logger.error(\"Error handling message\", error);\n }\n }\n\n /**\n * 处理连接错误\n */\n private handleErrorEvent(error: Error): void {\n const msg = error?.message ?? String(error);\n if (isWebsocketUpgradeNotFoundError(msg)) {\n this.suppressReconnect = true;\n this.logger.error(\n \"WebSocket handshake failed with HTTP 404 — stopping reconnect loop. \" +\n \"If the gateway was removed server-side, remove `gatewayId` from channels.lingyao.accounts.* \" +\n \"or use a new account id; if the path is wrong, verify wss://…/lyoc/gateway/ws is deployed on api.lingyao.live.\",\n { gatewayId: this.config.gatewayId, message: msg }\n );\n } else {\n this.logger.error(\"WebSocket error\", error);\n }\n this.state = \"error\";\n this.emitEvent({ type: \"error\", error });\n }\n\n /**\n * 处理连接关闭\n */\n private handleClose(code: number, reason: string): void {\n this.logger.warn(\"WebSocket connection closed\", { code, reason });\n this.state = \"disconnected\";\n this.connectionId = null;\n\n this.stopHeartbeat();\n this.emitEvent({ type: \"disconnected\", code, reason });\n\n // 1008 表示 Token 无效,不应继续使用原 Token 重连,交给外部重新注册\n if (code === 1008) {\n this.logger.error(\"WebSocket closed with 1008 (Invalid Token). Stopping reconnect loop.\");\n return;\n }\n\n if (this.suppressReconnect) {\n this.suppressReconnect = false;\n this.emitEvent({ type: \"fatal_handshake\", reason: \"http_404\" });\n return;\n }\n\n // 如果不是正常关闭,尝试重连\n if (code !== 1000) {\n this.scheduleReconnect();\n }\n }\n\n /**\n * 发送注册消息\n */\n private sendRegister(): void {\n const message: RegisterMessage = {\n type: WSMessageType.GATEWAY_REGISTER,\n id: this.generateMessageId(),\n timestamp: Date.now(),\n payload: {\n gatewayId: this.config.gatewayId,\n version: \"0.2.0\",\n capabilities: {\n websocket: true,\n compression: false,\n maxMessageSize: 1048576, // 1MB\n },\n },\n };\n\n this.send(message);\n }\n\n /**\n * 处理注册响应\n */\n private handleRegistered(message: WSMessage): void {\n this.logger.info(\"Gateway registered to Lingyao server\", {\n messageId: message.id,\n });\n }\n\n /**\n * 发送心跳\n */\n private sendHeartbeat(): void {\n const message: HeartbeatMessage = {\n type: WSMessageType.GATEWAY_HEARTBEAT,\n id: this.generateMessageId(),\n timestamp: Date.now(),\n payload: {\n timestamp: Date.now(),\n status: \"online\",\n },\n };\n\n this.send(message);\n }\n\n /**\n * 处理心跳确认\n */\n private handleHeartbeatAck(_message: WSMessage): void {\n this.logger.debug(\"Heartbeat acknowledged\");\n }\n\n /**\n * 启动心跳\n */\n private startHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n }\n\n this.heartbeatTimer = setInterval(() => {\n if (this.state === \"connected\") {\n this.sendHeartbeat();\n }\n }, this.config.heartbeatInterval);\n }\n\n /**\n * 停止心跳\n */\n private stopHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n }\n\n /**\n * 安排重连\n */\n private scheduleReconnect(): void {\n if (this.reconnectTimer) {\n return; // 已经安排了重连\n }\n\n this.logger.info(`Scheduling reconnect in ${this.config.reconnectInterval}ms`);\n\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = null;\n this.connect();\n }, this.config.reconnectInterval);\n }\n\n /**\n * 发送消息到服务器\n */\n send(message: WSMessage): void {\n if (!this.ws || this.state !== \"connected\") {\n throw new Error(\"WebSocket not connected\");\n }\n\n try {\n this.ws.send(JSON.stringify(message));\n this.logger.debug(\"Sent message to server\", { type: message.type });\n } catch (error) {\n this.logger.error(\"Failed to send message\", error);\n throw error;\n }\n }\n\n /**\n * 发送通知到鸿蒙 App\n */\n sendNotification(deviceId: string, notification: any): void {\n const message: SendMessage = {\n type: WSMessageType.GATEWAY_SEND_MESSAGE,\n id: this.generateMessageId(),\n timestamp: Date.now(),\n payload: {\n deviceId,\n message: {\n id: this.generateMessageId(),\n type: \"notify_action\",\n timestamp: Date.now(),\n payload: notification,\n },\n },\n };\n\n this.send(message);\n }\n\n /**\n * 处理 App 消息\n */\n private handleAppMessage(message: WSMessage): void {\n if (message.type !== WSMessageType.APP_MESSAGE) return;\n\n const appMessage = message as AppMessage;\n this.logger.info(\"Received message from App\", {\n deviceId: appMessage.payload.deviceId,\n messageType: appMessage.payload.message.type,\n });\n\n if (this.config.messageHandler) {\n // 异步处理,不阻塞 WebSocket\n Promise.resolve(this.config.messageHandler(appMessage)).catch((error: unknown) => {\n this.logger.error(\"Error handling App message\", error);\n });\n }\n }\n\n /**\n * 处理消息发送成功\n */\n private handleMessageDelivered(message: WSMessage): void {\n this.logger.debug(\"Message delivered successfully\", {\n messageId: message.id,\n });\n }\n\n /**\n * 设备上线(服务器可选推送)\n */\n private handleDeviceOnline(message: WSMessage): void {\n this.logger.debug(\"Device online (server push)\", {\n payload: message.payload,\n });\n }\n\n /**\n * 处理配对完成通知(来自 lingyao.live 服务器)\n */\n private handlePairingCompleted(message: WSMessage): void {\n const payload = message.payload;\n this.logger.info(\"Pairing completed\", {\n deviceId: payload?.deviceId,\n sessionId: payload?.sessionId,\n });\n\n if (this.config.eventHandler) {\n this.config.eventHandler({\n type: \"pairing_completed\",\n deviceId: payload?.deviceId,\n deviceInfo: payload?.deviceInfo,\n sessionId: payload?.sessionId,\n });\n }\n }\n\n /**\n * 处理消息发送失败\n */\n private handleMessageFailed(message: WSMessage): void {\n this.logger.warn(\"Message delivery failed\", {\n messageId: message.id,\n });\n }\n\n /**\n * 处理服务器错误\n */\n private handleError(message: WSMessage): void {\n this.logger.error(\"Server error\", message);\n }\n\n /**\n * 注册消息处理器\n */\n registerMessageHandler(\n type: string,\n handler: (message: WSMessage) => void\n ): void {\n this.messageHandlers.set(type, handler);\n }\n\n /**\n * 发送事件\n */\n private emitEvent(event: WSClientEvent): void {\n if (this.config.eventHandler) {\n this.config.eventHandler(event);\n }\n }\n\n /**\n * 更新 WebSocket 连接使用的 token\n */\n updateToken(token: string): void {\n this.config.token = token;\n }\n\n /**\n * 断开连接\n */\n disconnect(): void {\n this.logger.info(\"Disconnecting WebSocket from Lingyao server\");\n\n this.stopHeartbeat();\n\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n\n if (this.ws) {\n this.ws.close(1000, \"Client disconnect\");\n this.ws = null;\n }\n\n this.state = \"disconnected\";\n this.connectionId = null;\n }\n\n /**\n * 获取连接状态\n */\n getState(): ConnectionState {\n return this.state;\n }\n\n /**\n * 获取连接 ID\n */\n getConnectionId(): string | null {\n return this.connectionId;\n }\n\n /**\n * 是否已连接\n */\n isConnected(): boolean {\n return this.state === \"connected\";\n }\n\n /**\n * 生成消息 ID\n */\n private generateMessageId(): string {\n return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;\n }\n\n /**\n * 生成连接 ID\n */\n private generateConnectionId(): string {\n return `conn_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;\n }\n}\n","import { z } from \"zod\";\nimport type { LingyaoConfig } from \"./types.js\";\n\n/**\n * OpenClaw-standard DM policy values.\n */\nexport const lingyaoDmPolicySchema = z.enum([\"pairing\", \"allowlist\", \"open\"]);\n\nconst allowFromSchema = z.array(z.string()).optional();\n\nexport const lingyaoAccountConfigSchema = z.object({\n enabled: z.boolean().optional(),\n dmPolicy: lingyaoDmPolicySchema.optional(),\n allowFrom: allowFromSchema,\n websocketHeartbeatIntervalMs: z.number().int().min(5000).max(120000).optional(),\n});\n\n/**\n * Zod schema for Lingyao channel configuration.\n *\n * Lingyao supports both top-level single-account config and\n * `channels.lingyao.accounts.<id>` multi-account config. Account entries inherit\n * unspecified values from the top-level section.\n */\nexport const lingyaoConfigSchema = z.object({\n enabled: z.boolean().default(true),\n dmPolicy: lingyaoDmPolicySchema.optional().default(\"pairing\"),\n allowFrom: allowFromSchema.default([]),\n websocketHeartbeatIntervalMs: z.number().int().min(5000).max(120000).optional(),\n defaultAccount: z.string().min(1).optional(),\n accounts: z.record(lingyaoAccountConfigSchema).optional(),\n});\n\nexport type LingyaoConfigSchema = z.infer<typeof lingyaoConfigSchema>;\nexport const lingyaoChannelConfigSchema = {\n schema: {\n type: \"object\",\n additionalProperties: false,\n properties: {\n enabled: {\n type: \"boolean\",\n default: true,\n },\n dmPolicy: {\n type: \"string\",\n enum: [\"pairing\", \"allowlist\", \"open\"],\n default: \"pairing\",\n },\n allowFrom: {\n type: \"array\",\n items: { type: \"string\" },\n default: [],\n },\n defaultAccount: {\n type: \"string\",\n },\n websocketHeartbeatIntervalMs: {\n type: \"integer\",\n minimum: 5000,\n maximum: 120000,\n description:\n \"WebSocket gateway_heartbeat interval in ms (default: server register response). Use 5000–55000 for typical relay timeout.\",\n },\n accounts: {\n type: \"object\",\n additionalProperties: false,\n properties: {\n enabled: {\n type: \"boolean\",\n default: true,\n },\n dmPolicy: {\n type: \"string\",\n enum: [\"pairing\", \"allowlist\", \"open\"],\n },\n allowFrom: {\n type: \"array\",\n items: { type: \"string\" },\n },\n websocketHeartbeatIntervalMs: {\n type: \"integer\",\n minimum: 5000,\n maximum: 120000,\n },\n },\n default: {},\n },\n },\n },\n} as const;\n\n/**\n * Validate configuration object\n */\nexport function validateConfig(config: unknown): LingyaoConfig {\n return lingyaoConfigSchema.parse(config);\n}\n\n/**\n * Safely parse configuration, returning null if invalid\n */\nexport function safeParseConfig(config: unknown): LingyaoConfig | null {\n const result = lingyaoConfigSchema.safeParse(config);\n return result.success ? result.data : null;\n}\n\n/**\n * Get default configuration\n */\nexport function getDefaultConfig(): LingyaoConfig {\n return {\n enabled: true,\n dmPolicy: \"pairing\",\n allowFrom: [],\n };\n}\n"],"mappings":"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","names":["getOrchestrator","orchestrator","getOrchestrator","orchestrator","getOrchestrator","orchestrator","getOrchestrator","orchestrator"]}
|
package/package.json
CHANGED