@24klynx/channels 0.1.0 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { Message } from "@lynx/core";
1
+ import { Message } from "@24klynx/core";
2
2
 
3
3
  //#region src/types.d.ts
4
4
  /** What a channel can do. */
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { ChannelError, asMessageId } from "@lynx/core";
1
+ import { ChannelError, asMessageId } from "@24klynx/core";
2
2
  //#region src/contracts.ts
3
3
  /**
4
4
  * Channel capability validation.
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/contracts.ts","../src/live.ts","../src/channels/feishu.ts","../src/registry.ts"],"sourcesContent":["/**\n * Channel capability validation.\n *\n * Before using a channel adapter, the runtime verifies that the adapter\n * actually provides the methods it claims to have in its capability\n * declaration.\n */\n\nimport { ChannelError } from \"@lynx/core\";\nimport type { ChannelAdapter } from \"./types.js\";\n\n// ── Public API ─────────────────────────────────────\n\n/**\n * Verify that a channel adapter fulfills its capability contract.\n *\n * Throws {@link ChannelError} if the adapter claims a capability\n * but doesn't implement the corresponding method.\n */\nexport function validateCapabilities(adapter: ChannelAdapter): void {\n const cap = adapter.capabilities;\n\n if (cap.edit && typeof adapter.editMessage !== \"function\") {\n throw new ChannelError(\n `Channel \"${adapter.channelId}\" claims edit capability but does not implement editMessage()`,\n { category: \"channel\", userVisible: false },\n );\n }\n}\n\n/**\n * Return a human‑readable summary of what a channel supports.\n */\nexport function describeCapabilities(adapter: ChannelAdapter): string {\n const cap = adapter.capabilities;\n const flags: string[] = [];\n\n if (cap.text) flags.push(\"text\");\n if (cap.media) flags.push(\"media\");\n if (cap.interactive) flags.push(\"interactive\");\n if (cap.voice) flags.push(\"voice\");\n if (cap.reactions) flags.push(\"reactions\");\n if (cap.edit) flags.push(\"edit\");\n if (cap.recall) flags.push(\"recall\");\n\n return `${adapter.channelId}: ${flags.join(\", \") || \"no capabilities\"}`;\n}\n","/**\n * Live message state machine.\n *\n * Each channel adapter runs through connection states:\n * disconnected → connecting → connected (or error)\n *\n * The state machine tracks transitions and emits state change events\n * so the TUI can show connection status badges.\n */\n\nimport type { ChannelState } from \"./types.js\";\n\n// ── Types ──────────────────────────────────────────\n\nexport interface LiveChannel {\n /** The current connection state. */\n readonly state: ChannelState;\n /** Transition to a new state. */\n transition(next: ChannelState): void;\n /** Register a listener for state changes. */\n onChange(handler: (prev: ChannelState, next: ChannelState) => void): () => void;\n}\n\n// ── Public API ─────────────────────────────────────\n\n/**\n * Create a live channel state machine starting from \"disconnected\".\n */\nexport function createLiveChannel(): LiveChannel {\n let current: ChannelState = { status: \"disconnected\" };\n const listeners = new Set<(prev: ChannelState, next: ChannelState) => void>();\n\n const live: LiveChannel = {\n get state(): ChannelState {\n return current;\n },\n\n transition(next: ChannelState): void {\n const prev = current;\n current = next;\n for (const listener of listeners) {\n try {\n listener(prev, next);\n } catch {\n // Listener errors must not break the state machine\n }\n }\n },\n\n onChange(handler: (prev: ChannelState, next: ChannelState) => void): () => void {\n listeners.add(handler);\n return () => listeners.delete(handler);\n },\n };\n\n return live;\n}\n","/**\n * 飞书 (Feishu/Lark) channel adapter.\n *\n * Implements the {@link ChannelAdapter} interface for the 飞书\n * messaging platform. Uses the 飞书 Open API for sending and\n * WebSocket long‑connection for receiving events.\n *\n * Receiving flow:\n * 1. init() → get tenant_access_token\n * 2. POST /open-apis/ws/v1/connection → get WebSocket endpoint\n * 3. Connect WebSocket → receive events\n * 4. Parse im.message.receive_v1 → convert to Lynx Message\n * 5. Dispatch to registered onMessage handlers\n */\n\nimport { ChannelError, asMessageId } from \"@lynx/core\";\nimport type { Message } from \"@lynx/core\";\nimport type { ChannelAdapter, ChannelCapabilities } from \"../types.js\";\n\n// ── Configuration ──────────────────────────────────\n\nexport interface FeishuConfig {\n /** 飞书应用 App ID. */\n appId: string;\n /** 飞书应用 App Secret. */\n appSecret: string;\n /** Webhook verification token (for HTTP mode, not used in WS mode). */\n verificationToken?: string;\n /** API base URL (defaults to https://open.feishu.cn). */\n baseUrl?: string;\n}\n\nconst DEFAULT_BASE_URL = \"https://open.feishu.cn\";\n\n/** Reconnect delay range in ms. */\nconst RECONNECT_MIN_MS = 1_000;\nconst RECONNECT_MAX_MS = 30_000;\n\n/** WebSocket ping interval in ms. */\nconst PING_INTERVAL_MS = 30_000;\n\n// ── Capabilities ───────────────────────────────────\n\nconst FEISHU_CAPABILITIES: ChannelCapabilities = {\n text: true,\n media: true,\n interactive: true,\n voice: false,\n reactions: false,\n edit: true,\n recall: true,\n};\n\n// ── Feishu event types (subset) ────────────────────\n\ninterface FeishuHeader {\n event_id: string;\n event_type: string;\n tenant_key: string;\n app_id: string;\n}\n\ninterface FeishuEventEnvelope {\n schema: string;\n header: FeishuHeader;\n event: Record<string, unknown>;\n}\n\ninterface FeishuWsMessage {\n type: string;\n challenge?: string;\n data?: FeishuEventEnvelope;\n}\n\ninterface FeishuMessageEvent {\n sender?: {\n sender_id?: {\n open_id?: string;\n user_id?: string;\n };\n };\n message?: {\n message_id: string;\n chat_id: string;\n chat_type: string;\n create_time: string;\n message_type: string;\n content: string; // JSON-encoded string\n };\n}\n\n// ── Adapter ────────────────────────────────────────\n\n/**\n * Create a 飞书 channel adapter.\n *\n * Sending uses the REST API. Receiving uses a WebSocket long‑connection\n * established during init(). Incoming messages are parsed and dispatched\n * to registered onMessage handlers.\n */\nexport function createFeishuAdapter(config: FeishuConfig): ChannelAdapter {\n const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;\n let accessToken: string | undefined;\n const messageHandlers = new Set<(msg: Message) => void>();\n\n // WebSocket state\n let ws: WebSocket | null = null;\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n let pingTimer: ReturnType<typeof setInterval> | null = null;\n let reconnectDelay = RECONNECT_MIN_MS;\n let destroyed = false;\n\n // ── WebSocket connection ─────────────────────────\n\n /** Establish the WebSocket connection and start receiving events. */\n async function connectWs(): Promise<void> {\n if (destroyed || !accessToken) return;\n\n try {\n // 1. Get WebSocket endpoint from Feishu\n const connResp = await fetch(`${baseUrl}/open-apis/ws/v1/connection`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${accessToken}`,\n \"Content-Type\": \"application/json\",\n },\n });\n\n if (!connResp.ok) {\n throw new Error(`获取 WebSocket 端点失败:HTTP ${connResp.status}`);\n }\n\n const connData = (await connResp.json()) as {\n code: number;\n msg?: string;\n data?: { endpoint?: string; tcp_conn_id?: string };\n };\n\n if (connData.code !== 0 || !connData.data?.endpoint) {\n throw new Error(`获取 WebSocket 端点失败:${connData.msg ?? \"无端点\"}`);\n }\n\n // 2. Connect WebSocket\n ws = new WebSocket(connData.data.endpoint);\n\n ws.addEventListener(\"open\", () => {\n reconnectDelay = RECONNECT_MIN_MS; // reset backoff on successful connect\n startPing();\n });\n\n ws.addEventListener(\"message\", (event) => {\n try {\n const raw = typeof event.data === \"string\" ? event.data : \"\";\n if (!raw) return;\n const msg = JSON.parse(raw) as FeishuWsMessage;\n handleWsMessage(msg);\n } catch {\n // Malformed messages are logged and dropped\n }\n });\n\n ws.addEventListener(\"close\", () => {\n stopPing();\n ws = null;\n if (!destroyed) {\n scheduleReconnect();\n }\n });\n\n ws.addEventListener(\"error\", () => {\n // The \"close\" event will fire after \"error\", so reconnect is handled there\n });\n } catch {\n if (!destroyed) {\n scheduleReconnect();\n }\n }\n }\n\n /** Process an incoming WebSocket message from Feishu. */\n function handleWsMessage(msg: FeishuWsMessage): void {\n // URL verification challenge — respond inline\n if (msg.type === \"url_verification\" && msg.challenge) {\n ws?.send(JSON.stringify({ type: \"url_verification\", challenge: msg.challenge }));\n return;\n }\n\n // Event callback — parse and dispatch\n if (msg.type === \"event_callback\" && msg.data) {\n const eventType = msg.data.header?.event_type;\n if (eventType === \"im.message.receive_v1\") {\n const lynxMsg = convertToLynxMessage(msg.data.event as FeishuMessageEvent);\n if (lynxMsg) dispatchToHandlers(lynxMsg);\n }\n }\n }\n\n /** Dispatch a message to all registered handlers, catching errors per handler. */\n function dispatchToHandlers(msg: Message): void {\n for (const handler of messageHandlers) {\n try {\n handler(msg);\n } catch {\n // Handler errors must not break the dispatch loop\n }\n }\n }\n\n /** Convert a Feishu message event to a Lynx Message. */\n function convertToLynxMessage(event: FeishuMessageEvent): Message | null {\n const msg = event.message;\n if (!msg) return null;\n\n let text = \"\";\n if (msg.message_type === \"text\") {\n try {\n const content = JSON.parse(msg.content) as { text?: string };\n text = content.text ?? \"\";\n } catch {\n text = msg.content;\n }\n }\n\n if (!text) return null;\n\n return {\n id: asMessageId(msg.message_id),\n role: \"user\",\n content: [{ type: \"text\", text }],\n timestamp: Number(msg.create_time) * 1000,\n turnIndex: 0, // Caller is responsible for assigning turn index\n };\n }\n\n /** Schedule a reconnection attempt with exponential backoff. */\n function scheduleReconnect(): void {\n if (destroyed || reconnectTimer) return;\n\n reconnectTimer = setTimeout(() => {\n reconnectTimer = null;\n connectWs();\n }, reconnectDelay);\n\n // Exponential backoff with jitter\n reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);\n }\n\n /** Start the WebSocket ping keepalive. */\n function startPing(): void {\n stopPing();\n pingTimer = setInterval(() => {\n if (ws?.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({ type: \"ping\" }));\n }\n }, PING_INTERVAL_MS);\n }\n\n /** Stop the WebSocket ping keepalive. */\n function stopPing(): void {\n if (pingTimer) {\n clearInterval(pingTimer);\n pingTimer = null;\n }\n }\n\n /** Tear down the WebSocket connection and all timers. */\n function disconnectWs(): void {\n if (reconnectTimer) {\n clearTimeout(reconnectTimer);\n reconnectTimer = null;\n }\n stopPing();\n if (ws) {\n ws.close(1000, \"adapter destroyed\");\n ws = null;\n }\n }\n\n // ── Adapter object ───────────────────────────────\n\n const adapter: ChannelAdapter = {\n channelId: \"feishu\",\n capabilities: FEISHU_CAPABILITIES,\n\n async init(): Promise<void> {\n if (!config.appId || !config.appSecret) {\n throw new ChannelError(\"飞书 adapter requires appId and appSecret\", {\n category: \"channel\",\n recoverable: false,\n userVisible: true,\n });\n }\n\n // Fetch tenant access token\n try {\n const response = await fetch(`${baseUrl}/open-apis/auth/v3/tenant_access_token/internal`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n app_id: config.appId,\n app_secret: config.appSecret,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}`);\n }\n\n const data = (await response.json()) as { tenant_access_token?: string };\n if (!data.tenant_access_token) {\n throw new Error(\"响应中无 tenant_access_token\");\n }\n\n accessToken = data.tenant_access_token;\n } catch (err) {\n throw new ChannelError(`飞书 authentication failed: ${String(err)}`, {\n category: \"channel\",\n recoverable: true,\n retryable: true,\n });\n }\n\n // Start WebSocket receiving\n await connectWs();\n },\n\n onMessage(handler: (msg: Message) => void): () => void {\n messageHandlers.add(handler);\n return () => messageHandlers.delete(handler);\n },\n\n async sendMessage(message: Message): Promise<string> {\n if (!accessToken) {\n throw new ChannelError(\"飞书适配器未初始化 — 请先调用 init()\", {\n category: \"channel\",\n recoverable: true,\n retryable: true,\n });\n }\n\n const text = extractTextContent(message);\n\n try {\n const response = await fetch(\n `${baseUrl}/open-apis/im/v1/messages?receive_id_type=open_id`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n },\n body: JSON.stringify({\n receive_id: message.id, // In production, resolve real open_id\n msg_type: \"text\",\n content: JSON.stringify({ text }),\n }),\n },\n );\n\n if (!response.ok) {\n throw new ChannelError(`飞书 send failed: HTTP ${response.status}`, {\n category: \"channel\",\n recoverable: true,\n retryable: true,\n });\n }\n\n const data = (await response.json()) as { data?: { message_id?: string } };\n return data.data?.message_id ?? \"unknown\";\n } catch (err) {\n throw new ChannelError(`飞书 sendMessage error: ${String(err)}`, {\n category: \"channel\",\n recoverable: true,\n retryable: true,\n });\n }\n },\n\n /**\n * Edit a previously sent message via the 飞书 Open API.\n *\n * Uses PUT /open-apis/im/v1/messages/:message_id to update the\n * text content of an existing message.\n */\n async editMessage(platformMessageId: string, newContent: string): Promise<void> {\n if (!accessToken) {\n throw new ChannelError(\"飞书适配器未初始化 — 请先调用 init()\", {\n category: \"channel\",\n recoverable: true,\n retryable: true,\n });\n }\n\n try {\n const response = await fetch(`${baseUrl}/open-apis/im/v1/messages/${platformMessageId}`, {\n method: \"PUT\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n },\n body: JSON.stringify({\n content: JSON.stringify({ text: newContent }),\n }),\n });\n\n if (!response.ok) {\n throw new ChannelError(`飞书 editMessage failed: HTTP ${response.status}`, {\n category: \"channel\",\n recoverable: true,\n retryable: true,\n });\n }\n } catch (err) {\n if (err instanceof ChannelError) throw err;\n throw new ChannelError(`飞书 editMessage error: ${String(err)}`, {\n category: \"channel\",\n recoverable: true,\n retryable: true,\n });\n }\n },\n\n async destroy(): Promise<void> {\n destroyed = true;\n disconnectWs();\n messageHandlers.clear();\n accessToken = undefined;\n },\n };\n\n return adapter;\n}\n\n// ── Helpers ────────────────────────────────────────\n\nfunction extractTextContent(message: Message): string {\n for (const block of message.content) {\n if (block.type === \"text\") {\n return block.text;\n }\n }\n return \"\";\n}\n","/**\n * ChannelRegistry — manages multiple {@link ChannelAdapter} instances.\n *\n * Each adapter is registered with a unique channel ID. The registry\n * handles init/destroy lifecycle and routes incoming messages to\n * a single onMessage handler (wired to the agent engine).\n */\n\nimport type { ChannelAdapter } from \"./types.js\";\nimport type { Message } from \"@lynx/core\";\n\n/** Lightweight registry for channel adapters. */\nexport interface ChannelRegistry {\n /** Register a channel adapter. Replaces any existing adapter with the same ID. */\n register(adapter: ChannelAdapter): void;\n\n /** Remove a channel by ID. Calls adapter.destroy() first. */\n unregister(channelId: string): Promise<void>;\n\n /** Get an adapter by ID. */\n get(channelId: string): ChannelAdapter | undefined;\n\n /** Initialize all registered adapters. */\n initAll(): Promise<void>;\n\n /** Destroy all registered adapters. */\n destroyAll(): Promise<void>;\n\n /** Set the handler for all incoming messages from any channel. */\n onMessage(handler: (msg: Message) => void): void;\n\n /** List IDs of all registered adapters. */\n list(): string[];\n}\n\n/**\n * Create a channel registry for managing multiple channel adapters.\n */\nexport function createChannelRegistry(): ChannelRegistry {\n const adapters = new Map<string, ChannelAdapter>();\n const cleanups = new Map<string, () => void>();\n let messageHandler: ((msg: Message) => void) | null = null;\n\n return {\n register(adapter: ChannelAdapter): void {\n // Clean up previous adapter if replacing\n const prevCleanup = cleanups.get(adapter.channelId);\n if (prevCleanup) prevCleanup();\n\n adapters.set(adapter.channelId, adapter);\n\n // Wire incoming messages\n const unsub = adapter.onMessage((msg) => {\n if (messageHandler) {\n try {\n messageHandler(msg);\n } catch {\n /* don't break dispatch */\n }\n }\n });\n cleanups.set(adapter.channelId, unsub);\n },\n\n async unregister(channelId: string): Promise<void> {\n const cleanup = cleanups.get(channelId);\n if (cleanup) cleanup();\n cleanups.delete(channelId);\n\n const adapter = adapters.get(channelId);\n adapters.delete(channelId);\n if (adapter) {\n try {\n await adapter.destroy();\n } catch {\n /* best effort */\n }\n }\n },\n\n get(channelId: string): ChannelAdapter | undefined {\n return adapters.get(channelId);\n },\n\n async initAll(): Promise<void> {\n const results = await Promise.allSettled([...adapters.values()].map((a) => a.init()));\n for (const result of results) {\n if (result.status === \"rejected\") {\n // Log but don't block — one channel failure shouldn't crash all\n process.stderr.write(`[lynx] Channel init failed: ${String(result.reason)}\\n`);\n }\n }\n },\n\n async destroyAll(): Promise<void> {\n for (const cleanup of cleanups.values()) cleanup();\n cleanups.clear();\n await Promise.allSettled([...adapters.values()].map((a) => a.destroy()));\n adapters.clear();\n },\n\n onMessage(handler: (msg: Message) => void): void {\n messageHandler = handler;\n },\n\n list(): string[] {\n return [...adapters.keys()];\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/contracts.ts","../src/live.ts","../src/channels/feishu.ts","../src/registry.ts"],"sourcesContent":["/**\n * Channel capability validation.\n *\n * Before using a channel adapter, the runtime verifies that the adapter\n * actually provides the methods it claims to have in its capability\n * declaration.\n */\n\nimport { ChannelError } from \"@24klynx/core\";\nimport type { ChannelAdapter } from \"./types.js\";\n\n// ── Public API ─────────────────────────────────────\n\n/**\n * Verify that a channel adapter fulfills its capability contract.\n *\n * Throws {@link ChannelError} if the adapter claims a capability\n * but doesn't implement the corresponding method.\n */\nexport function validateCapabilities(adapter: ChannelAdapter): void {\n const cap = adapter.capabilities;\n\n if (cap.edit && typeof adapter.editMessage !== \"function\") {\n throw new ChannelError(\n `Channel \"${adapter.channelId}\" claims edit capability but does not implement editMessage()`,\n { category: \"channel\", userVisible: false },\n );\n }\n}\n\n/**\n * Return a human‑readable summary of what a channel supports.\n */\nexport function describeCapabilities(adapter: ChannelAdapter): string {\n const cap = adapter.capabilities;\n const flags: string[] = [];\n\n if (cap.text) flags.push(\"text\");\n if (cap.media) flags.push(\"media\");\n if (cap.interactive) flags.push(\"interactive\");\n if (cap.voice) flags.push(\"voice\");\n if (cap.reactions) flags.push(\"reactions\");\n if (cap.edit) flags.push(\"edit\");\n if (cap.recall) flags.push(\"recall\");\n\n return `${adapter.channelId}: ${flags.join(\", \") || \"no capabilities\"}`;\n}\n","/**\n * Live message state machine.\n *\n * Each channel adapter runs through connection states:\n * disconnected → connecting → connected (or error)\n *\n * The state machine tracks transitions and emits state change events\n * so the TUI can show connection status badges.\n */\n\nimport type { ChannelState } from \"./types.js\";\n\n// ── Types ──────────────────────────────────────────\n\nexport interface LiveChannel {\n /** The current connection state. */\n readonly state: ChannelState;\n /** Transition to a new state. */\n transition(next: ChannelState): void;\n /** Register a listener for state changes. */\n onChange(handler: (prev: ChannelState, next: ChannelState) => void): () => void;\n}\n\n// ── Public API ─────────────────────────────────────\n\n/**\n * Create a live channel state machine starting from \"disconnected\".\n */\nexport function createLiveChannel(): LiveChannel {\n let current: ChannelState = { status: \"disconnected\" };\n const listeners = new Set<(prev: ChannelState, next: ChannelState) => void>();\n\n const live: LiveChannel = {\n get state(): ChannelState {\n return current;\n },\n\n transition(next: ChannelState): void {\n const prev = current;\n current = next;\n for (const listener of listeners) {\n try {\n listener(prev, next);\n } catch {\n // Listener errors must not break the state machine\n }\n }\n },\n\n onChange(handler: (prev: ChannelState, next: ChannelState) => void): () => void {\n listeners.add(handler);\n return () => listeners.delete(handler);\n },\n };\n\n return live;\n}\n","/**\n * 飞书 (Feishu/Lark) channel adapter.\n *\n * Implements the {@link ChannelAdapter} interface for the 飞书\n * messaging platform. Uses the 飞书 Open API for sending and\n * WebSocket long‑connection for receiving events.\n *\n * Receiving flow:\n * 1. init() → get tenant_access_token\n * 2. POST /open-apis/ws/v1/connection → get WebSocket endpoint\n * 3. Connect WebSocket → receive events\n * 4. Parse im.message.receive_v1 → convert to Lynx Message\n * 5. Dispatch to registered onMessage handlers\n */\n\nimport { ChannelError, asMessageId } from \"@24klynx/core\";\nimport type { Message } from \"@24klynx/core\";\nimport type { ChannelAdapter, ChannelCapabilities } from \"../types.js\";\n\n// ── Configuration ──────────────────────────────────\n\nexport interface FeishuConfig {\n /** 飞书应用 App ID. */\n appId: string;\n /** 飞书应用 App Secret. */\n appSecret: string;\n /** Webhook verification token (for HTTP mode, not used in WS mode). */\n verificationToken?: string;\n /** API base URL (defaults to https://open.feishu.cn). */\n baseUrl?: string;\n}\n\nconst DEFAULT_BASE_URL = \"https://open.feishu.cn\";\n\n/** Reconnect delay range in ms. */\nconst RECONNECT_MIN_MS = 1_000;\nconst RECONNECT_MAX_MS = 30_000;\n\n/** WebSocket ping interval in ms. */\nconst PING_INTERVAL_MS = 30_000;\n\n// ── Capabilities ───────────────────────────────────\n\nconst FEISHU_CAPABILITIES: ChannelCapabilities = {\n text: true,\n media: true,\n interactive: true,\n voice: false,\n reactions: false,\n edit: true,\n recall: true,\n};\n\n// ── Feishu event types (subset) ────────────────────\n\ninterface FeishuHeader {\n event_id: string;\n event_type: string;\n tenant_key: string;\n app_id: string;\n}\n\ninterface FeishuEventEnvelope {\n schema: string;\n header: FeishuHeader;\n event: Record<string, unknown>;\n}\n\ninterface FeishuWsMessage {\n type: string;\n challenge?: string;\n data?: FeishuEventEnvelope;\n}\n\ninterface FeishuMessageEvent {\n sender?: {\n sender_id?: {\n open_id?: string;\n user_id?: string;\n };\n };\n message?: {\n message_id: string;\n chat_id: string;\n chat_type: string;\n create_time: string;\n message_type: string;\n content: string; // JSON-encoded string\n };\n}\n\n// ── Adapter ────────────────────────────────────────\n\n/**\n * Create a 飞书 channel adapter.\n *\n * Sending uses the REST API. Receiving uses a WebSocket long‑connection\n * established during init(). Incoming messages are parsed and dispatched\n * to registered onMessage handlers.\n */\nexport function createFeishuAdapter(config: FeishuConfig): ChannelAdapter {\n const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;\n let accessToken: string | undefined;\n const messageHandlers = new Set<(msg: Message) => void>();\n\n // WebSocket state\n let ws: WebSocket | null = null;\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n let pingTimer: ReturnType<typeof setInterval> | null = null;\n let reconnectDelay = RECONNECT_MIN_MS;\n let destroyed = false;\n\n // ── WebSocket connection ─────────────────────────\n\n /** Establish the WebSocket connection and start receiving events. */\n async function connectWs(): Promise<void> {\n if (destroyed || !accessToken) return;\n\n try {\n // 1. Get WebSocket endpoint from Feishu\n const connResp = await fetch(`${baseUrl}/open-apis/ws/v1/connection`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${accessToken}`,\n \"Content-Type\": \"application/json\",\n },\n });\n\n if (!connResp.ok) {\n throw new Error(`获取 WebSocket 端点失败:HTTP ${connResp.status}`);\n }\n\n const connData = (await connResp.json()) as {\n code: number;\n msg?: string;\n data?: { endpoint?: string; tcp_conn_id?: string };\n };\n\n if (connData.code !== 0 || !connData.data?.endpoint) {\n throw new Error(`获取 WebSocket 端点失败:${connData.msg ?? \"无端点\"}`);\n }\n\n // 2. Connect WebSocket\n ws = new WebSocket(connData.data.endpoint);\n\n ws.addEventListener(\"open\", () => {\n reconnectDelay = RECONNECT_MIN_MS; // reset backoff on successful connect\n startPing();\n });\n\n ws.addEventListener(\"message\", (event) => {\n try {\n const raw = typeof event.data === \"string\" ? event.data : \"\";\n if (!raw) return;\n const msg = JSON.parse(raw) as FeishuWsMessage;\n handleWsMessage(msg);\n } catch {\n // Malformed messages are logged and dropped\n }\n });\n\n ws.addEventListener(\"close\", () => {\n stopPing();\n ws = null;\n if (!destroyed) {\n scheduleReconnect();\n }\n });\n\n ws.addEventListener(\"error\", () => {\n // The \"close\" event will fire after \"error\", so reconnect is handled there\n });\n } catch {\n if (!destroyed) {\n scheduleReconnect();\n }\n }\n }\n\n /** Process an incoming WebSocket message from Feishu. */\n function handleWsMessage(msg: FeishuWsMessage): void {\n // URL verification challenge — respond inline\n if (msg.type === \"url_verification\" && msg.challenge) {\n ws?.send(JSON.stringify({ type: \"url_verification\", challenge: msg.challenge }));\n return;\n }\n\n // Event callback — parse and dispatch\n if (msg.type === \"event_callback\" && msg.data) {\n const eventType = msg.data.header?.event_type;\n if (eventType === \"im.message.receive_v1\") {\n const lynxMsg = convertToLynxMessage(msg.data.event as FeishuMessageEvent);\n if (lynxMsg) dispatchToHandlers(lynxMsg);\n }\n }\n }\n\n /** Dispatch a message to all registered handlers, catching errors per handler. */\n function dispatchToHandlers(msg: Message): void {\n for (const handler of messageHandlers) {\n try {\n handler(msg);\n } catch {\n // Handler errors must not break the dispatch loop\n }\n }\n }\n\n /** Convert a Feishu message event to a Lynx Message. */\n function convertToLynxMessage(event: FeishuMessageEvent): Message | null {\n const msg = event.message;\n if (!msg) return null;\n\n let text = \"\";\n if (msg.message_type === \"text\") {\n try {\n const content = JSON.parse(msg.content) as { text?: string };\n text = content.text ?? \"\";\n } catch {\n text = msg.content;\n }\n }\n\n if (!text) return null;\n\n return {\n id: asMessageId(msg.message_id),\n role: \"user\",\n content: [{ type: \"text\", text }],\n timestamp: Number(msg.create_time) * 1000,\n turnIndex: 0, // Caller is responsible for assigning turn index\n };\n }\n\n /** Schedule a reconnection attempt with exponential backoff. */\n function scheduleReconnect(): void {\n if (destroyed || reconnectTimer) return;\n\n reconnectTimer = setTimeout(() => {\n reconnectTimer = null;\n connectWs();\n }, reconnectDelay);\n\n // Exponential backoff with jitter\n reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);\n }\n\n /** Start the WebSocket ping keepalive. */\n function startPing(): void {\n stopPing();\n pingTimer = setInterval(() => {\n if (ws?.readyState === WebSocket.OPEN) {\n ws.send(JSON.stringify({ type: \"ping\" }));\n }\n }, PING_INTERVAL_MS);\n }\n\n /** Stop the WebSocket ping keepalive. */\n function stopPing(): void {\n if (pingTimer) {\n clearInterval(pingTimer);\n pingTimer = null;\n }\n }\n\n /** Tear down the WebSocket connection and all timers. */\n function disconnectWs(): void {\n if (reconnectTimer) {\n clearTimeout(reconnectTimer);\n reconnectTimer = null;\n }\n stopPing();\n if (ws) {\n ws.close(1000, \"adapter destroyed\");\n ws = null;\n }\n }\n\n // ── Adapter object ───────────────────────────────\n\n const adapter: ChannelAdapter = {\n channelId: \"feishu\",\n capabilities: FEISHU_CAPABILITIES,\n\n async init(): Promise<void> {\n if (!config.appId || !config.appSecret) {\n throw new ChannelError(\"飞书 adapter requires appId and appSecret\", {\n category: \"channel\",\n recoverable: false,\n userVisible: true,\n });\n }\n\n // Fetch tenant access token\n try {\n const response = await fetch(`${baseUrl}/open-apis/auth/v3/tenant_access_token/internal`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n app_id: config.appId,\n app_secret: config.appSecret,\n }),\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}`);\n }\n\n const data = (await response.json()) as { tenant_access_token?: string };\n if (!data.tenant_access_token) {\n throw new Error(\"响应中无 tenant_access_token\");\n }\n\n accessToken = data.tenant_access_token;\n } catch (err) {\n throw new ChannelError(`飞书 authentication failed: ${String(err)}`, {\n category: \"channel\",\n recoverable: true,\n retryable: true,\n });\n }\n\n // Start WebSocket receiving\n await connectWs();\n },\n\n onMessage(handler: (msg: Message) => void): () => void {\n messageHandlers.add(handler);\n return () => messageHandlers.delete(handler);\n },\n\n async sendMessage(message: Message): Promise<string> {\n if (!accessToken) {\n throw new ChannelError(\"飞书适配器未初始化 — 请先调用 init()\", {\n category: \"channel\",\n recoverable: true,\n retryable: true,\n });\n }\n\n const text = extractTextContent(message);\n\n try {\n const response = await fetch(\n `${baseUrl}/open-apis/im/v1/messages?receive_id_type=open_id`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n },\n body: JSON.stringify({\n receive_id: message.id, // In production, resolve real open_id\n msg_type: \"text\",\n content: JSON.stringify({ text }),\n }),\n },\n );\n\n if (!response.ok) {\n throw new ChannelError(`飞书 send failed: HTTP ${response.status}`, {\n category: \"channel\",\n recoverable: true,\n retryable: true,\n });\n }\n\n const data = (await response.json()) as { data?: { message_id?: string } };\n return data.data?.message_id ?? \"unknown\";\n } catch (err) {\n throw new ChannelError(`飞书 sendMessage error: ${String(err)}`, {\n category: \"channel\",\n recoverable: true,\n retryable: true,\n });\n }\n },\n\n /**\n * Edit a previously sent message via the 飞书 Open API.\n *\n * Uses PUT /open-apis/im/v1/messages/:message_id to update the\n * text content of an existing message.\n */\n async editMessage(platformMessageId: string, newContent: string): Promise<void> {\n if (!accessToken) {\n throw new ChannelError(\"飞书适配器未初始化 — 请先调用 init()\", {\n category: \"channel\",\n recoverable: true,\n retryable: true,\n });\n }\n\n try {\n const response = await fetch(`${baseUrl}/open-apis/im/v1/messages/${platformMessageId}`, {\n method: \"PUT\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${accessToken}`,\n },\n body: JSON.stringify({\n content: JSON.stringify({ text: newContent }),\n }),\n });\n\n if (!response.ok) {\n throw new ChannelError(`飞书 editMessage failed: HTTP ${response.status}`, {\n category: \"channel\",\n recoverable: true,\n retryable: true,\n });\n }\n } catch (err) {\n if (err instanceof ChannelError) throw err;\n throw new ChannelError(`飞书 editMessage error: ${String(err)}`, {\n category: \"channel\",\n recoverable: true,\n retryable: true,\n });\n }\n },\n\n async destroy(): Promise<void> {\n destroyed = true;\n disconnectWs();\n messageHandlers.clear();\n accessToken = undefined;\n },\n };\n\n return adapter;\n}\n\n// ── Helpers ────────────────────────────────────────\n\nfunction extractTextContent(message: Message): string {\n for (const block of message.content) {\n if (block.type === \"text\") {\n return block.text;\n }\n }\n return \"\";\n}\n","/**\n * ChannelRegistry — manages multiple {@link ChannelAdapter} instances.\n *\n * Each adapter is registered with a unique channel ID. The registry\n * handles init/destroy lifecycle and routes incoming messages to\n * a single onMessage handler (wired to the agent engine).\n */\n\nimport type { ChannelAdapter } from \"./types.js\";\nimport type { Message } from \"@24klynx/core\";\n\n/** Lightweight registry for channel adapters. */\nexport interface ChannelRegistry {\n /** Register a channel adapter. Replaces any existing adapter with the same ID. */\n register(adapter: ChannelAdapter): void;\n\n /** Remove a channel by ID. Calls adapter.destroy() first. */\n unregister(channelId: string): Promise<void>;\n\n /** Get an adapter by ID. */\n get(channelId: string): ChannelAdapter | undefined;\n\n /** Initialize all registered adapters. */\n initAll(): Promise<void>;\n\n /** Destroy all registered adapters. */\n destroyAll(): Promise<void>;\n\n /** Set the handler for all incoming messages from any channel. */\n onMessage(handler: (msg: Message) => void): void;\n\n /** List IDs of all registered adapters. */\n list(): string[];\n}\n\n/**\n * Create a channel registry for managing multiple channel adapters.\n */\nexport function createChannelRegistry(): ChannelRegistry {\n const adapters = new Map<string, ChannelAdapter>();\n const cleanups = new Map<string, () => void>();\n let messageHandler: ((msg: Message) => void) | null = null;\n\n return {\n register(adapter: ChannelAdapter): void {\n // Clean up previous adapter if replacing\n const prevCleanup = cleanups.get(adapter.channelId);\n if (prevCleanup) prevCleanup();\n\n adapters.set(adapter.channelId, adapter);\n\n // Wire incoming messages\n const unsub = adapter.onMessage((msg) => {\n if (messageHandler) {\n try {\n messageHandler(msg);\n } catch {\n /* don't break dispatch */\n }\n }\n });\n cleanups.set(adapter.channelId, unsub);\n },\n\n async unregister(channelId: string): Promise<void> {\n const cleanup = cleanups.get(channelId);\n if (cleanup) cleanup();\n cleanups.delete(channelId);\n\n const adapter = adapters.get(channelId);\n adapters.delete(channelId);\n if (adapter) {\n try {\n await adapter.destroy();\n } catch {\n /* best effort */\n }\n }\n },\n\n get(channelId: string): ChannelAdapter | undefined {\n return adapters.get(channelId);\n },\n\n async initAll(): Promise<void> {\n const results = await Promise.allSettled([...adapters.values()].map((a) => a.init()));\n for (const result of results) {\n if (result.status === \"rejected\") {\n // Log but don't block — one channel failure shouldn't crash all\n process.stderr.write(`[lynx] Channel init failed: ${String(result.reason)}\\n`);\n }\n }\n },\n\n async destroyAll(): Promise<void> {\n for (const cleanup of cleanups.values()) cleanup();\n cleanups.clear();\n await Promise.allSettled([...adapters.values()].map((a) => a.destroy()));\n adapters.clear();\n },\n\n onMessage(handler: (msg: Message) => void): void {\n messageHandler = handler;\n },\n\n list(): string[] {\n return [...adapters.keys()];\n },\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "@24klynx/channels",
3
- "version": "0.1.0",
3
+ "version": "0.1.4",
4
4
  "description": "Message channel adapters — QQ, Feishu, WeChat",
5
+ "files": [
6
+ "dist"
7
+ ],
5
8
  "type": "module",
6
9
  "main": "./dist/index.mjs",
7
10
  "types": "./dist/index.d.mts",
@@ -11,15 +14,12 @@
11
14
  "types": "./dist/index.d.mts"
12
15
  }
13
16
  },
14
- "dependencies": {
15
- "@24klynx/core": "0.1.0"
16
- },
17
- "files": [
18
- "dist"
19
- ],
20
17
  "publishConfig": {
21
18
  "access": "public"
22
19
  },
20
+ "dependencies": {
21
+ "@24klynx/core": "0.1.4"
22
+ },
23
23
  "scripts": {
24
24
  "build": "tsdown --config-loader tsx",
25
25
  "test": "vitest run --passWithNoTests",