@lingyao037/openclaw-lingyao-cli 0.9.7 → 0.9.8

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.
@@ -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/channel.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 type { LingyaoRuntime, LingyaoConfig } from './types.js';\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 { LingyaoChannel } from './channel.js';\nimport {\n validateConfig,\n getDefaultConfig,\n lingyaoChannelConfigSchema,\n} from './config-schema.js';\n\nexport * from './types.js';\nexport type { AgentMessage } from './bot.js';\nexport { LingyaoChannel } from './channel.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\n/** @deprecated Use the default export (OpenClaw SDK entry) instead. */\nexport async function createPlugin(\n runtime: LingyaoRuntime,\n config: Partial<LingyaoConfig> = {}\n): Promise<LingyaoChannel> {\n const fullConfig = { ...getDefaultConfig(), ...config };\n const validatedConfig = validateConfig(fullConfig);\n const channel = new LingyaoChannel(runtime, validatedConfig);\n await channel.initialize();\n return channel;\n}\n\n/** @deprecated Use openclaw.plugin.json for plugin metadata. */\nexport const pluginMetadata = {\n name: 'lingyao',\n version: '0.9.7',\n description: 'Lingyao Channel Plugin - bidirectional sync via lingyao.live server relay',\n type: 'channel',\n capabilities: {\n chatTypes: ['direct'],\n media: false,\n reactions: false,\n threads: false,\n },\n defaultConfig: getDefaultConfig(),\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 maxOfflineMessages: lingyao.maxOfflineMessages as number | undefined,\n tokenExpiryDays: lingyao.tokenExpiryDays as number | undefined,\n gatewayId: lingyao.gatewayId as string | 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 * - 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: 'group' | 'channel';\n id: string;\n threadId?: string | null;\n }): string | undefined {\n return `${PREFIX}${params.id}`;\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 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 let macStr = '';\n \n // 遍历所有网络接口,收集非内部回环的 MAC 地址\n for (const name of Object.keys(interfaces)) {\n const iface = interfaces[name];\n if (!iface) continue;\n \n for (const alias of iface) {\n if (!alias.internal && alias.mac && alias.mac !== '00:00:00:00:00:00') {\n macStr += alias.mac;\n }\n }\n }\n \n if (macStr) {\n // 取 MAC 地址的 MD5 前 8 位作为机器稳定标识\n return createHash('md5').update(macStr).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, accountManager);\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 // 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: 30000,\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 return false;\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`);\n return;\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 * 检查 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","/**\n * 灵爻频道插件 - 服务器中转模式\n *\n * 架构:\n * 1. OpenClaw 作为 WebSocket 客户端,主动连接到 lingyao.live 服务器\n * 2. 鸿蒙 App 也作为 WebSocket 客户端,主动连接到 lingyao.live 服务器\n * 3. lingyao.live 服务器作为消息中转站\n * 4. 双方都不需要公网 IP,也不需要开放端口\n */\n\nimport { createHash } from 'node:crypto';\nimport { hostname, networkInterfaces } from 'node:os';\n\n/**\n * 获取机器的稳定标识符 (基于 MAC 地址)\n */\nfunction getMachineId(): string {\n try {\n const interfaces = networkInterfaces();\n let macStr = '';\n \n // 遍历所有网络接口,收集非内部回环的 MAC 地址\n for (const name of Object.keys(interfaces)) {\n const iface = interfaces[name];\n if (!iface) continue;\n \n for (const alias of iface) {\n if (!alias.internal && alias.mac && alias.mac !== '00:00:00:00:00:00') {\n macStr += alias.mac;\n }\n }\n }\n \n if (macStr) {\n // 取 MAC 地址的 MD5 前 8 位作为机器稳定标识\n return createHash('md5').update(macStr).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();\nimport {\n LINGYAO_SERVER_URL,\n getLingyaoGatewayWsUrl,\n type LingyaoRuntime,\n LingyaoConfig,\n HealthStatus,\n NotifyPayload,\n} from \"./types.js\";\nimport { AccountManager } from \"./accounts.js\";\nimport { TokenManager } from \"./token.js\";\nimport { MessageProcessor, AgentMessage } from \"./bot.js\";\nimport { Probe } from \"./probe.js\";\nimport { setRuntime } from \"./runtime.js\";\nimport { ServerHttpClient } from \"./server-client.js\";\nimport { LingyaoWSClient } from \"./websocket-client.js\";\nimport { Monitor, MonitoringEvent } from \"./metrics.js\";\nimport { ErrorHandler, ConnectionError } from \"./errors.js\";\n\n/**\n * 灵爻频道插件 - 服务器中转模式\n */\nexport class LingyaoChannel {\n private runtime: LingyaoRuntime;\n private config: LingyaoConfig;\n private accountManager: AccountManager;\n private tokenManager: TokenManager;\n private serverClient: ServerHttpClient | null = null;\n private wsClient: LingyaoWSClient | null = null;\n private processor: MessageProcessor | null = null;\n private probe: Probe;\n private monitor: Monitor;\n private errorHandler: ErrorHandler;\n private startTime: number = 0;\n private gatewayId: string;\n private isRunning: boolean = false;\n private messageHandler: ((message: AgentMessage) => void | Promise<void>) | null = null;\n\n constructor(runtime: LingyaoRuntime, config: LingyaoConfig) {\n this.runtime = runtime;\n this.config = config;\n this.startTime = Date.now();\n\n // 生成 Gateway ID\n this.gatewayId = this.generateGatewayId();\n\n // Set global runtime\n setRuntime(runtime);\n\n // Initialize components\n this.accountManager = new AccountManager(runtime);\n this.tokenManager = new TokenManager(runtime);\n this.probe = new Probe(runtime);\n this.monitor = new Monitor(runtime);\n this.errorHandler = new ErrorHandler(runtime);\n\n // Initialize server client\n this.serverClient = new ServerHttpClient(runtime, this.gatewayId, {\n baseURL: LINGYAO_SERVER_URL,\n apiBase: '/v1',\n });\n }\n\n /**\n * 初始化频道\n */\n async initialize(): Promise<void> {\n this.runtime.logger.info(\"Initializing Lingyao Channel (Server Relay Mode)...\");\n\n // Initialize account manager\n await this.accountManager.initialize();\n\n // Initialize message processor\n this.processor = new MessageProcessor(\n this.runtime,\n this.accountManager\n );\n await this.processor.initialize();\n\n if (this.messageHandler) {\n this.processor.setMessageHandler(this.messageHandler);\n }\n\n this.runtime.logger.info(\"Lingyao Channel initialized successfully\");\n }\n\n /**\n * 启动频道\n */\n async start(): Promise<void> {\n if (this.isRunning) {\n this.runtime.logger.warn(\"Lingyao Channel is already running\");\n return;\n }\n\n this.runtime.logger.info(\"Starting Lingyao Channel...\");\n\n try {\n // 1. 向服务器注册\n await this.registerToServer();\n\n // 2. 创建 WebSocket 客户端\n this.wsClient = new LingyaoWSClient(this.runtime, {\n url: getLingyaoGatewayWsUrl(),\n gatewayId: this.gatewayId,\n token: this.serverClient?.getGatewayToken() ?? undefined,\n reconnectInterval: 5000,\n heartbeatInterval: 30000,\n messageHandler: this.handleAppMessage.bind(this),\n eventHandler: this.handleClientEvent.bind(this),\n });\n\n // 3. 连接到服务器\n await this.wsClient.connect();\n\n this.isRunning = true;\n this.runtime.logger.info(\"Lingyao Channel started successfully\");\n } catch (error) {\n this.runtime.logger.error(\"Failed to start Lingyao Channel\", error);\n throw error;\n }\n }\n\n /**\n * 向 lingyao 服务器注册 Gateway\n */\n private async registerToServer(): Promise<void> {\n if (!this.serverClient) {\n throw new Error('Server client not available');\n }\n\n try {\n // 尝试从存储恢复会话\n const restored = await this.serverClient.restoreFromStorage();\n\n if (restored) {\n this.runtime.logger.info('Previous server registration restored');\n return;\n }\n\n this.runtime.logger.info('Registering to lingyao server...', {\n gatewayId: this.gatewayId,\n });\n\n // 向服务器注册\n const registerResponse = await this.serverClient.register({\n websocket: true,\n compression: false,\n maxMessageSize: 1048576,\n });\n\n this.runtime.logger.info('Registered to lingyao server successfully', {\n expiresAt: new Date(registerResponse.expiresAt).toISOString(),\n heartbeatInterval: registerResponse.serverConfig.heartbeatInterval,\n });\n } catch (error) {\n const err = error as Error;\n this.runtime.logger.error('Failed to register to server', err);\n\n // 注册失败不影响启动,WebSocket 客户端会自动重连\n this.runtime.logger.info('Will attempt WebSocket connection anyway');\n }\n }\n\n /**\n * 处理来自鸿蒙 App 的消息\n */\n private async handleAppMessage(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('Received message from App', {\n deviceId,\n messageType: msg.type,\n messageId: msg.id,\n });\n\n // 记录接收消息事件\n this.monitor.recordEvent(MonitoringEvent.MESSAGE_RECEIVED, {\n deviceId,\n messageType: msg.type,\n messageId: msg.id,\n });\n\n // 处理不同类型的消息\n switch (msg.type) {\n case 'sync_diary':\n case 'sync_memory':\n await this.handleSyncMessage(deviceId, msg);\n break;\n case 'heartbeat':\n // 心跳消息,不需要特殊处理\n this.monitor.recordEvent(MonitoringEvent.HEARTBEAT_RECEIVED, {\n deviceId,\n });\n break;\n default:\n this.runtime.logger.warn('Unknown message type', { type: msg.type });\n }\n } catch (error) {\n this.runtime.logger.error('Error handling App message', error);\n this.monitor.recordEvent(MonitoringEvent.ERROR_OCCURRED, {\n errorType: 'message_handling',\n severity: 'medium',\n });\n }\n }\n\n /**\n * 处理同步消息(日记、记忆等)\n */\n private async handleSyncMessage(\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 (!this.processor) {\n this.runtime.logger.warn('Message processor not initialized');\n return;\n }\n\n // 转换为 Agent 消息格式\n const agentMessage: AgentMessage = {\n id: message.id,\n type: this.mapMessageType(message.type),\n from: deviceId,\n deviceId,\n content: message.content,\n metadata: message.metadata || {},\n timestamp: message.timestamp,\n };\n\n await this.processor.deliverToAgent(agentMessage);\n }\n\n /**\n * 映射消息类型\n */\n private mapMessageType(appType: \"sync_diary\" | \"sync_memory\"): AgentMessage[\"type\"] {\n return appType === \"sync_diary\" ? \"diary\" : \"memory\";\n }\n\n /**\n * 处理 WebSocket 客户端事件\n */\n private handleClientEvent(event: any): void {\n this.runtime.logger.debug('WebSocket client event', { type: event.type });\n\n switch (event.type) {\n case 'connected':\n this.runtime.logger.info('Connected to Lingyao server', {\n connectionId: event.connectionId,\n });\n this.monitor.recordEvent(MonitoringEvent.CONNECTION_OPEN, {\n connectionId: event.connectionId,\n });\n break;\n case 'disconnected':\n this.runtime.logger.warn('Disconnected from Lingyao server', {\n code: event.code,\n reason: event.reason,\n });\n this.monitor.recordEvent(MonitoringEvent.CONNECTION_CLOSE, {\n code: event.code,\n reason: event.reason,\n });\n break;\n case 'error':\n this.runtime.logger.error('WebSocket client error', event.error);\n this.probe.recordError(event.error.message, \"websocket\");\n this.monitor.recordEvent(MonitoringEvent.CONNECTION_ERROR, {\n error: event.error,\n });\n this.errorHandler.handleError(\n new ConnectionError(\n `WebSocket error: ${event.error.message}`,\n { event: event.type },\n event.error\n )\n );\n break;\n case 'fatal_handshake':\n this.runtime.logger.error(\n 'Lingyao WebSocket upgrade failed (HTTP 404). Use the OpenClaw plugin path for automatic recovery, or clear gateway session / gatewayId in config.'\n );\n break;\n }\n }\n\n /**\n * 停止频道\n */\n async stop(): Promise<void> {\n if (!this.isRunning) {\n return;\n }\n\n this.runtime.logger.info(\"Stopping Lingyao Channel...\");\n\n this.isRunning = false;\n\n // 断开 WebSocket 连接\n if (this.wsClient) {\n this.wsClient.disconnect();\n this.wsClient = null;\n }\n\n // 停止服务器心跳\n if (this.serverClient) {\n this.serverClient.stopHeartbeat();\n }\n\n this.runtime.logger.info(\"Lingyao Channel stopped\");\n }\n\n /**\n * 发送通知到设备\n */\n async sendNotification(\n deviceId: string,\n notification: NotifyPayload\n ): Promise<{ success: boolean; error?: string; queued?: boolean }> {\n if (!this.wsClient || !this.wsClient.isConnected()) {\n this.monitor.recordEvent(MonitoringEvent.MESSAGE_FAILED, {\n deviceId,\n reason: 'not_connected',\n direction: 'out',\n });\n return {\n success: false,\n error: \"Not connected to Lingyao server\",\n };\n }\n\n try {\n this.wsClient.sendNotification(deviceId, notification);\n this.monitor.recordEvent(MonitoringEvent.MESSAGE_SENT, {\n deviceId,\n messageType: 'notification',\n });\n return { success: true };\n } catch (error) {\n const err = error as Error;\n this.monitor.recordEvent(MonitoringEvent.MESSAGE_FAILED, {\n deviceId,\n reason: err.message,\n direction: 'out',\n });\n return {\n success: false,\n error: err.message,\n };\n }\n }\n\n /**\n * 发送文本到设备\n */\n async sendText(\n deviceId: string,\n text: string\n ): Promise<{ success: boolean; error?: string; queued?: boolean }> {\n return await this.sendNotification(deviceId, {\n title: 'OpenClaw',\n body: text,\n });\n }\n\n /**\n * 获取健康状态\n */\n async getHealthStatus(): Promise<HealthStatus> {\n const activeConnections = this.wsClient?.isConnected() ? 1 : 0;\n const queuedMessages = this.processor?.getTotalQueueSize() ?? 0;\n const lastError = this.probe.getLastError() ?? undefined;\n\n let status: \"healthy\" | \"degraded\" | \"unhealthy\" = \"healthy\";\n\n if (lastError) {\n status = \"degraded\";\n }\n\n if (!this.config.enabled) {\n status = \"unhealthy\";\n }\n\n if (!this.wsClient?.isConnected()) {\n status = \"degraded\";\n }\n\n const uptime = Date.now() - this.startTime;\n\n return {\n status,\n uptime,\n activeConnections,\n queuedMessages,\n lastError,\n };\n }\n\n /**\n * 获取配置\n */\n getConfig(): LingyaoConfig {\n return { ...this.config };\n }\n\n /**\n * 更新配置\n */\n async updateConfig(updates: Partial<LingyaoConfig>): Promise<void> {\n this.config = { ...this.config, ...updates };\n this.runtime.logger.info(\"Configuration updated\", updates);\n }\n\n /**\n * 获取账号管理器\n */\n getAccountManager(): AccountManager {\n return this.accountManager;\n }\n\n /**\n * 获取 Token 管理器\n */\n getTokenManager(): TokenManager {\n return this.tokenManager;\n }\n\n /**\n * 获取服务器客户端\n */\n getServerClient(): ServerHttpClient | null {\n return this.serverClient;\n }\n\n /**\n /**\n * 获取 WebSocket 客户端\n */\n getWSClient(): LingyaoWSClient | null {\n return this.wsClient;\n }\n\n /**\n * 获取消息处理器\n */\n getMessageProcessor(): MessageProcessor | null {\n return this.processor;\n }\n\n /**\n * 设置消息处理器\n */\n setMessageHandler(handler: (message: AgentMessage) => void | Promise<void>): void {\n this.messageHandler = handler;\n if (this.processor) {\n this.processor.setMessageHandler(handler);\n }\n this.runtime.logger.info(\"Message handler registered for Agent integration\");\n }\n\n /**\n * 获取监控器\n */\n getMonitor(): Monitor {\n return this.monitor;\n }\n\n /**\n * 获取错误处理器\n */\n getErrorHandler(): ErrorHandler {\n return this.errorHandler;\n }\n\n /**\n * 获取监控摘要\n */\n getMonitoringSummary() {\n return this.monitor.getSummary();\n }\n\n /**\n * 获取错误统计\n */\n getErrorStats() {\n return this.errorHandler.getErrorStats();\n }\n\n /**\n * 生成 Gateway ID\n */\n private generateGatewayId(): string {\n const hostname = this.getRuntimeHostname();\n return `gw_openclaw_${hostname}_${MACHINE_ID}`;\n }\n\n /**\n * 获取运行时主机名\n */\n private getRuntimeHostname(): string {\n try {\n return hostname().split('.')[0].replace(/[^a-z0-9]/gi, '').toLowerCase();\n } catch {\n return 'unknown';\n }\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 maxOfflineMessages: z.number().int().min(1).max(1000).optional(),\n tokenExpiryDays: z.number().int().min(1).max(365).optional(),\n gatewayId: z.string().min(1).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 maxOfflineMessages: z.number().int().min(1).max(1000).optional().default(100),\n tokenExpiryDays: z.number().int().min(1).max(365).optional().default(30),\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: true,\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 maxOfflineMessages: {\n type: \"integer\",\n minimum: 1,\n maximum: 1000,\n default: 100,\n },\n tokenExpiryDays: {\n type: \"integer\",\n minimum: 1,\n maximum: 365,\n default: 30,\n },\n defaultAccount: {\n type: \"string\",\n },\n accounts: {\n type: \"object\",\n additionalProperties: {\n type: \"object\",\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 maxOfflineMessages: {\n type: \"integer\",\n minimum: 1,\n maximum: 1000,\n },\n tokenExpiryDays: {\n type: \"integer\",\n minimum: 1,\n maximum: 365,\n },\n gatewayId: {\n type: \"string\",\n },\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 maxOfflineMessages: 100,\n tokenExpiryDays: 30,\n };\n}\n"],"mappings":"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","names":["getOrchestrator","orchestrator","getOrchestrator","orchestrator","getOrchestrator","orchestrator","getOrchestrator","orchestrator","createHash","hostname","networkInterfaces","getMachineId","networkInterfaces","createHash","MACHINE_ID"]}
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/channel.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 type { LingyaoRuntime, LingyaoConfig } from './types.js';\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 { LingyaoChannel } from './channel.js';\nimport {\n validateConfig,\n getDefaultConfig,\n lingyaoChannelConfigSchema,\n} from './config-schema.js';\n\nexport * from './types.js';\nexport type { AgentMessage } from './bot.js';\nexport { LingyaoChannel } from './channel.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\n/** @deprecated Use the default export (OpenClaw SDK entry) instead. */\nexport async function createPlugin(\n runtime: LingyaoRuntime,\n config: Partial<LingyaoConfig> = {}\n): Promise<LingyaoChannel> {\n const fullConfig = { ...getDefaultConfig(), ...config };\n const validatedConfig = validateConfig(fullConfig);\n const channel = new LingyaoChannel(runtime, validatedConfig);\n await channel.initialize();\n return channel;\n}\n\n/** @deprecated Use openclaw.plugin.json for plugin metadata. */\nexport const pluginMetadata = {\n name: 'lingyao',\n version: '0.9.8',\n description: 'Lingyao Channel Plugin - bidirectional sync via lingyao.live server relay',\n type: 'channel',\n capabilities: {\n chatTypes: ['direct'],\n media: false,\n reactions: false,\n threads: false,\n },\n defaultConfig: getDefaultConfig(),\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 maxOfflineMessages: lingyao.maxOfflineMessages as number | undefined,\n tokenExpiryDays: lingyao.tokenExpiryDays as number | undefined,\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 * - 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: 'group' | 'channel';\n id: string;\n threadId?: string | null;\n }): string | undefined {\n return `${PREFIX}${params.id}`;\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 let macStr = '';\n \n // 遍历所有网络接口,收集非内部回环的 MAC 地址\n for (const name of Object.keys(interfaces)) {\n const iface = interfaces[name];\n if (!iface) continue;\n \n for (const alias of iface) {\n if (!alias.internal && alias.mac && alias.mac !== '00:00:00:00:00:00') {\n macStr += alias.mac;\n }\n }\n }\n \n if (macStr) {\n // 取 MAC 地址的 MD5 前 8 位作为机器稳定标识\n return createHash('md5').update(macStr).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, accountManager);\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 return false;\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`);\n return;\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","/**\n * 灵爻频道插件 - 服务器中转模式\n *\n * 架构:\n * 1. OpenClaw 作为 WebSocket 客户端,主动连接到 lingyao.live 服务器\n * 2. 鸿蒙 App 也作为 WebSocket 客户端,主动连接到 lingyao.live 服务器\n * 3. lingyao.live 服务器作为消息中转站\n * 4. 双方都不需要公网 IP,也不需要开放端口\n */\n\nimport { createHash } from 'node:crypto';\nimport { hostname, networkInterfaces } from 'node:os';\n\n/**\n * 获取机器的稳定标识符 (基于 MAC 地址)\n */\nfunction getMachineId(): string {\n try {\n const interfaces = networkInterfaces();\n let macStr = '';\n \n // 遍历所有网络接口,收集非内部回环的 MAC 地址\n for (const name of Object.keys(interfaces)) {\n const iface = interfaces[name];\n if (!iface) continue;\n \n for (const alias of iface) {\n if (!alias.internal && alias.mac && alias.mac !== '00:00:00:00:00:00') {\n macStr += alias.mac;\n }\n }\n }\n \n if (macStr) {\n // 取 MAC 地址的 MD5 前 8 位作为机器稳定标识\n return createHash('md5').update(macStr).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();\nimport {\n LINGYAO_SERVER_URL,\n getLingyaoGatewayWsUrl,\n type LingyaoRuntime,\n LingyaoConfig,\n HealthStatus,\n NotifyPayload,\n} from \"./types.js\";\nimport { resolveLingyaoWsHeartbeatIntervalMsFromHttp } from \"./ws-heartbeat-interval.js\";\nimport { AccountManager } from \"./accounts.js\";\nimport { TokenManager } from \"./token.js\";\nimport { MessageProcessor, AgentMessage } from \"./bot.js\";\nimport { Probe } from \"./probe.js\";\nimport { setRuntime } from \"./runtime.js\";\nimport { ServerHttpClient } from \"./server-client.js\";\nimport { LingyaoWSClient } from \"./websocket-client.js\";\nimport { Monitor, MonitoringEvent } from \"./metrics.js\";\nimport { ErrorHandler, ConnectionError } from \"./errors.js\";\n\n/**\n * 灵爻频道插件 - 服务器中转模式\n */\nexport class LingyaoChannel {\n private runtime: LingyaoRuntime;\n private config: LingyaoConfig;\n private accountManager: AccountManager;\n private tokenManager: TokenManager;\n private serverClient: ServerHttpClient | null = null;\n private wsClient: LingyaoWSClient | null = null;\n private processor: MessageProcessor | null = null;\n private probe: Probe;\n private monitor: Monitor;\n private errorHandler: ErrorHandler;\n private startTime: number = 0;\n private gatewayId: string;\n private isRunning: boolean = false;\n private messageHandler: ((message: AgentMessage) => void | Promise<void>) | null = null;\n\n constructor(runtime: LingyaoRuntime, config: LingyaoConfig) {\n this.runtime = runtime;\n this.config = config;\n this.startTime = Date.now();\n\n // 生成 Gateway ID\n this.gatewayId = this.generateGatewayId();\n\n // Set global runtime\n setRuntime(runtime);\n\n // Initialize components\n this.accountManager = new AccountManager(runtime);\n this.tokenManager = new TokenManager(runtime);\n this.probe = new Probe(runtime);\n this.monitor = new Monitor(runtime);\n this.errorHandler = new ErrorHandler(runtime);\n\n // Initialize server client\n this.serverClient = new ServerHttpClient(runtime, this.gatewayId, {\n baseURL: LINGYAO_SERVER_URL,\n });\n }\n\n /**\n * 初始化频道\n */\n async initialize(): Promise<void> {\n this.runtime.logger.info(\"Initializing Lingyao Channel (Server Relay Mode)...\");\n\n // Initialize account manager\n await this.accountManager.initialize();\n\n // Initialize message processor\n this.processor = new MessageProcessor(\n this.runtime,\n this.accountManager\n );\n await this.processor.initialize();\n\n if (this.messageHandler) {\n this.processor.setMessageHandler(this.messageHandler);\n }\n\n this.runtime.logger.info(\"Lingyao Channel initialized successfully\");\n }\n\n /**\n * 启动频道\n */\n async start(): Promise<void> {\n if (this.isRunning) {\n this.runtime.logger.warn(\"Lingyao Channel is already running\");\n return;\n }\n\n this.runtime.logger.info(\"Starting Lingyao Channel...\");\n\n try {\n // 1. 向服务器注册\n await this.registerToServer();\n\n const wsHeartbeatMs = this.serverClient\n ? resolveLingyaoWsHeartbeatIntervalMsFromHttp(this.serverClient)\n : 30_000;\n\n // 2. 创建 WebSocket 客户端\n this.wsClient = new LingyaoWSClient(this.runtime, {\n url: getLingyaoGatewayWsUrl(),\n gatewayId: this.gatewayId,\n token: this.serverClient?.getGatewayToken() ?? undefined,\n reconnectInterval: 5000,\n heartbeatInterval: wsHeartbeatMs,\n messageHandler: this.handleAppMessage.bind(this),\n eventHandler: this.handleClientEvent.bind(this),\n });\n\n // 3. 连接到服务器\n await this.wsClient.connect();\n\n this.isRunning = true;\n this.runtime.logger.info(\"Lingyao Channel started successfully\");\n } catch (error) {\n this.runtime.logger.error(\"Failed to start Lingyao Channel\", error);\n throw error;\n }\n }\n\n /**\n * 向 lingyao 服务器注册 Gateway\n */\n private async registerToServer(): Promise<void> {\n if (!this.serverClient) {\n throw new Error('Server client not available');\n }\n\n try {\n // 尝试从存储恢复会话\n const restored = await this.serverClient.restoreFromStorage();\n\n if (restored) {\n this.runtime.logger.info('Previous server registration restored');\n return;\n }\n\n this.runtime.logger.info('Registering to lingyao server...', {\n gatewayId: this.gatewayId,\n });\n\n // 向服务器注册\n const registerResponse = await this.serverClient.register({\n websocket: true,\n compression: false,\n maxMessageSize: 1048576,\n });\n\n this.runtime.logger.info('Registered to lingyao server successfully', {\n expiresAt: new Date(registerResponse.expiresAt).toISOString(),\n heartbeatInterval: registerResponse.serverConfig.heartbeatInterval,\n });\n } catch (error) {\n const err = error as Error;\n this.runtime.logger.error('Failed to register to server', err);\n\n // 注册失败不影响启动,WebSocket 客户端会自动重连\n this.runtime.logger.info('Will attempt WebSocket connection anyway');\n }\n }\n\n /**\n * 处理来自鸿蒙 App 的消息\n */\n private async handleAppMessage(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('Received message from App', {\n deviceId,\n messageType: msg.type,\n messageId: msg.id,\n });\n\n // 记录接收消息事件\n this.monitor.recordEvent(MonitoringEvent.MESSAGE_RECEIVED, {\n deviceId,\n messageType: msg.type,\n messageId: msg.id,\n });\n\n // 处理不同类型的消息\n switch (msg.type) {\n case 'sync_diary':\n case 'sync_memory':\n await this.handleSyncMessage(deviceId, msg);\n break;\n case 'heartbeat':\n // 心跳消息,不需要特殊处理\n this.monitor.recordEvent(MonitoringEvent.HEARTBEAT_RECEIVED, {\n deviceId,\n });\n break;\n default:\n this.runtime.logger.warn('Unknown message type', { type: msg.type });\n }\n } catch (error) {\n this.runtime.logger.error('Error handling App message', error);\n this.monitor.recordEvent(MonitoringEvent.ERROR_OCCURRED, {\n errorType: 'message_handling',\n severity: 'medium',\n });\n }\n }\n\n /**\n * 处理同步消息(日记、记忆等)\n */\n private async handleSyncMessage(\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 (!this.processor) {\n this.runtime.logger.warn('Message processor not initialized');\n return;\n }\n\n // 转换为 Agent 消息格式\n const agentMessage: AgentMessage = {\n id: message.id,\n type: this.mapMessageType(message.type),\n from: deviceId,\n deviceId,\n content: message.content,\n metadata: message.metadata || {},\n timestamp: message.timestamp,\n };\n\n await this.processor.deliverToAgent(agentMessage);\n }\n\n /**\n * 映射消息类型\n */\n private mapMessageType(appType: \"sync_diary\" | \"sync_memory\"): AgentMessage[\"type\"] {\n return appType === \"sync_diary\" ? \"diary\" : \"memory\";\n }\n\n /**\n * 处理 WebSocket 客户端事件\n */\n private handleClientEvent(event: any): void {\n this.runtime.logger.debug('WebSocket client event', { type: event.type });\n\n switch (event.type) {\n case 'connected':\n this.runtime.logger.info('Connected to Lingyao server', {\n connectionId: event.connectionId,\n });\n this.monitor.recordEvent(MonitoringEvent.CONNECTION_OPEN, {\n connectionId: event.connectionId,\n });\n break;\n case 'disconnected':\n this.runtime.logger.warn('Disconnected from Lingyao server', {\n code: event.code,\n reason: event.reason,\n });\n this.monitor.recordEvent(MonitoringEvent.CONNECTION_CLOSE, {\n code: event.code,\n reason: event.reason,\n });\n break;\n case 'error':\n this.runtime.logger.error('WebSocket client error', event.error);\n this.probe.recordError(event.error.message, \"websocket\");\n this.monitor.recordEvent(MonitoringEvent.CONNECTION_ERROR, {\n error: event.error,\n });\n this.errorHandler.handleError(\n new ConnectionError(\n `WebSocket error: ${event.error.message}`,\n { event: event.type },\n event.error\n )\n );\n break;\n case 'fatal_handshake':\n this.runtime.logger.error(\n 'Lingyao WebSocket upgrade failed (HTTP 404). Use the OpenClaw plugin path for automatic recovery, or clear gateway session / gatewayId in config.'\n );\n break;\n }\n }\n\n /**\n * 停止频道\n */\n async stop(): Promise<void> {\n if (!this.isRunning) {\n return;\n }\n\n this.runtime.logger.info(\"Stopping Lingyao Channel...\");\n\n this.isRunning = false;\n\n // 断开 WebSocket 连接\n if (this.wsClient) {\n this.wsClient.disconnect();\n this.wsClient = null;\n }\n\n // 停止服务器心跳\n if (this.serverClient) {\n this.serverClient.stopHeartbeat();\n }\n\n this.runtime.logger.info(\"Lingyao Channel stopped\");\n }\n\n /**\n * 发送通知到设备\n */\n async sendNotification(\n deviceId: string,\n notification: NotifyPayload\n ): Promise<{ success: boolean; error?: string; queued?: boolean }> {\n if (!this.wsClient || !this.wsClient.isConnected()) {\n this.monitor.recordEvent(MonitoringEvent.MESSAGE_FAILED, {\n deviceId,\n reason: 'not_connected',\n direction: 'out',\n });\n return {\n success: false,\n error: \"Not connected to Lingyao server\",\n };\n }\n\n try {\n this.wsClient.sendNotification(deviceId, notification);\n this.monitor.recordEvent(MonitoringEvent.MESSAGE_SENT, {\n deviceId,\n messageType: 'notification',\n });\n return { success: true };\n } catch (error) {\n const err = error as Error;\n this.monitor.recordEvent(MonitoringEvent.MESSAGE_FAILED, {\n deviceId,\n reason: err.message,\n direction: 'out',\n });\n return {\n success: false,\n error: err.message,\n };\n }\n }\n\n /**\n * 发送文本到设备\n */\n async sendText(\n deviceId: string,\n text: string\n ): Promise<{ success: boolean; error?: string; queued?: boolean }> {\n return await this.sendNotification(deviceId, {\n title: 'OpenClaw',\n body: text,\n });\n }\n\n /**\n * 获取健康状态\n */\n async getHealthStatus(): Promise<HealthStatus> {\n const activeConnections = this.wsClient?.isConnected() ? 1 : 0;\n const queuedMessages = this.processor?.getTotalQueueSize() ?? 0;\n const lastError = this.probe.getLastError() ?? undefined;\n\n let status: \"healthy\" | \"degraded\" | \"unhealthy\" = \"healthy\";\n\n if (lastError) {\n status = \"degraded\";\n }\n\n if (!this.config.enabled) {\n status = \"unhealthy\";\n }\n\n if (!this.wsClient?.isConnected()) {\n status = \"degraded\";\n }\n\n const uptime = Date.now() - this.startTime;\n\n return {\n status,\n uptime,\n activeConnections,\n queuedMessages,\n lastError,\n };\n }\n\n /**\n * 获取配置\n */\n getConfig(): LingyaoConfig {\n return { ...this.config };\n }\n\n /**\n * 更新配置\n */\n async updateConfig(updates: Partial<LingyaoConfig>): Promise<void> {\n this.config = { ...this.config, ...updates };\n this.runtime.logger.info(\"Configuration updated\", updates);\n }\n\n /**\n * 获取账号管理器\n */\n getAccountManager(): AccountManager {\n return this.accountManager;\n }\n\n /**\n * 获取 Token 管理器\n */\n getTokenManager(): TokenManager {\n return this.tokenManager;\n }\n\n /**\n * 获取服务器客户端\n */\n getServerClient(): ServerHttpClient | null {\n return this.serverClient;\n }\n\n /**\n /**\n * 获取 WebSocket 客户端\n */\n getWSClient(): LingyaoWSClient | null {\n return this.wsClient;\n }\n\n /**\n * 获取消息处理器\n */\n getMessageProcessor(): MessageProcessor | null {\n return this.processor;\n }\n\n /**\n * 设置消息处理器\n */\n setMessageHandler(handler: (message: AgentMessage) => void | Promise<void>): void {\n this.messageHandler = handler;\n if (this.processor) {\n this.processor.setMessageHandler(handler);\n }\n this.runtime.logger.info(\"Message handler registered for Agent integration\");\n }\n\n /**\n * 获取监控器\n */\n getMonitor(): Monitor {\n return this.monitor;\n }\n\n /**\n * 获取错误处理器\n */\n getErrorHandler(): ErrorHandler {\n return this.errorHandler;\n }\n\n /**\n * 获取监控摘要\n */\n getMonitoringSummary() {\n return this.monitor.getSummary();\n }\n\n /**\n * 获取错误统计\n */\n getErrorStats() {\n return this.errorHandler.getErrorStats();\n }\n\n /**\n * 生成 Gateway ID\n */\n private generateGatewayId(): string {\n const hostname = this.getRuntimeHostname();\n return `gw_openclaw_${hostname}_${MACHINE_ID}`;\n }\n\n /**\n * 获取运行时主机名\n */\n private getRuntimeHostname(): string {\n try {\n return hostname().split('.')[0].replace(/[^a-z0-9]/gi, '').toLowerCase();\n } catch {\n return 'unknown';\n }\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 maxOfflineMessages: z.number().int().min(1).max(1000).optional(),\n tokenExpiryDays: z.number().int().min(1).max(365).optional(),\n gatewayId: z.string().min(1).optional(),\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 maxOfflineMessages: z.number().int().min(1).max(1000).optional().default(100),\n tokenExpiryDays: z.number().int().min(1).max(365).optional().default(30),\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: true,\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 maxOfflineMessages: {\n type: \"integer\",\n minimum: 1,\n maximum: 1000,\n default: 100,\n },\n tokenExpiryDays: {\n type: \"integer\",\n minimum: 1,\n maximum: 365,\n default: 30,\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: {\n type: \"object\",\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 maxOfflineMessages: {\n type: \"integer\",\n minimum: 1,\n maximum: 1000,\n },\n tokenExpiryDays: {\n type: \"integer\",\n minimum: 1,\n maximum: 365,\n },\n gatewayId: {\n type: \"string\",\n },\n websocketHeartbeatIntervalMs: {\n type: \"integer\",\n minimum: 5000,\n maximum: 120000,\n },\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 maxOfflineMessages: 100,\n tokenExpiryDays: 30,\n };\n}\n"],"mappings":"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","names":["getOrchestrator","orchestrator","getOrchestrator","orchestrator","getOrchestrator","orchestrator","getOrchestrator","orchestrator","createHash","hostname","networkInterfaces","getMachineId","networkInterfaces","createHash","MACHINE_ID"]}
@@ -1,4 +1,4 @@
1
- import { h as LingyaoAccountConfig } from './types-LFC6Wpqo.js';
1
+ import { h as LingyaoAccountConfig } from './types-D3SmE-b0.js';
2
2
 
3
3
  /**
4
4
  * Config Adapter - Account resolution, config validation
@@ -186,6 +186,11 @@ interface LingyaoAccountConfig {
186
186
  maxOfflineMessages?: number;
187
187
  tokenExpiryDays?: number;
188
188
  gatewayId?: string;
189
+ /**
190
+ * WebSocket `gateway_heartbeat` 间隔(毫秒)。默认使用注册接口返回的 `heartbeatInterval`。
191
+ * 建议 5000–55000,须小于中继 `heartbeatTimeout`(常见 60s)。
192
+ */
193
+ websocketHeartbeatIntervalMs?: number;
189
194
  }
190
195
  /**
191
196
  * Channel configuration
@@ -196,6 +201,8 @@ interface LingyaoConfig {
196
201
  tokenExpiryDays?: number;
197
202
  dmPolicy?: "pairing" | "allowlist" | "open";
198
203
  allowFrom?: string[];
204
+ /** 顶层默认,可被 `accounts.<id>` 覆盖 */
205
+ websocketHeartbeatIntervalMs?: number;
199
206
  accounts?: Record<string, LingyaoAccountConfig>;
200
207
  }
201
208
  /**
@@ -26,6 +26,12 @@
26
26
  "defaultAccount": {
27
27
  "type": "string"
28
28
  },
29
+ "websocketHeartbeatIntervalMs": {
30
+ "type": "integer",
31
+ "minimum": 5000,
32
+ "maximum": 120000,
33
+ "description": "WebSocket gateway_heartbeat interval (ms); default follows server register. Typical 30000."
34
+ },
29
35
  "accounts": {
30
36
  "type": "object",
31
37
  "additionalProperties": {
@@ -52,6 +58,11 @@
52
58
  },
53
59
  "gatewayId": {
54
60
  "type": "string"
61
+ },
62
+ "websocketHeartbeatIntervalMs": {
63
+ "type": "integer",
64
+ "minimum": 5000,
65
+ "maximum": 120000
55
66
  }
56
67
  }
57
68
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lingyao037/openclaw-lingyao-cli",
3
- "version": "0.9.7",
3
+ "version": "0.9.8",
4
4
  "description": "Lingyao Channel Plugin for OpenClaw - bidirectional sync via lingyao.live server relay",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",