@24klynx/channels 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,145 @@
1
+ import { Message } from "@lynx/core";
2
+
3
+ //#region src/types.d.ts
4
+ /** What a channel can do. */
5
+ interface ChannelCapabilities {
6
+ /** Text messages. */
7
+ text: boolean;
8
+ /** Images / files. */
9
+ media: boolean;
10
+ /** Interactive cards / buttons. */
11
+ interactive: boolean;
12
+ /** Voice messages. */
13
+ voice: boolean;
14
+ /** Supports message reactions (πŸ‘, πŸ‘Ž, etc.). */
15
+ reactions: boolean;
16
+ /** Supports message editing after sending. */
17
+ edit: boolean;
18
+ /** Supports message recall / delete. */
19
+ recall: boolean;
20
+ }
21
+ /**
22
+ * A single messaging channel adapter.
23
+ *
24
+ * Structural typing β€” any object with these methods is a valid adapter.
25
+ * No `implements` keyword needed.
26
+ */
27
+ interface ChannelAdapter {
28
+ /** Unique channel id, e.g. "feishu" or "qq". */
29
+ readonly channelId: string;
30
+ /** Static capability declaration. */
31
+ readonly capabilities: ChannelCapabilities;
32
+ /**
33
+ * Initialise the adapter (open connections, verify credentials, etc.).
34
+ * Called once at startup.
35
+ */
36
+ init(): Promise<void>;
37
+ /**
38
+ * Start listening for incoming messages.
39
+ *
40
+ * The callback fires for each incoming message. Return a cleanup
41
+ * function that stops listening.
42
+ */
43
+ onMessage(handler: (message: Message) => void): () => void;
44
+ /**
45
+ * Send a message through this channel.
46
+ *
47
+ * Returns the platform‑assigned message id.
48
+ */
49
+ sendMessage(message: Message): Promise<string>;
50
+ /**
51
+ * Edit a previously sent message.
52
+ * Throws {@link ChannelError} if the channel doesn't support editing.
53
+ */
54
+ editMessage?(platformMessageId: string, newContent: string): Promise<void>;
55
+ /**
56
+ * Clean up resources (close connections, abort pending requests).
57
+ */
58
+ destroy(): Promise<void>;
59
+ }
60
+ /** Connection state for a live channel. */
61
+ type ChannelState = {
62
+ status: "disconnected";
63
+ } | {
64
+ status: "connecting";
65
+ } | {
66
+ status: "connected";
67
+ connectedAt: number;
68
+ } | {
69
+ status: "error";
70
+ message: string;
71
+ retryable: boolean;
72
+ };
73
+ //#endregion
74
+ //#region src/contracts.d.ts
75
+ /**
76
+ * Verify that a channel adapter fulfills its capability contract.
77
+ *
78
+ * Throws {@link ChannelError} if the adapter claims a capability
79
+ * but doesn't implement the corresponding method.
80
+ */
81
+ declare function validateCapabilities(adapter: ChannelAdapter): void;
82
+ /**
83
+ * Return a human‑readable summary of what a channel supports.
84
+ */
85
+ declare function describeCapabilities(adapter: ChannelAdapter): string;
86
+ //#endregion
87
+ //#region src/live.d.ts
88
+ interface LiveChannel {
89
+ /** The current connection state. */
90
+ readonly state: ChannelState;
91
+ /** Transition to a new state. */
92
+ transition(next: ChannelState): void;
93
+ /** Register a listener for state changes. */
94
+ onChange(handler: (prev: ChannelState, next: ChannelState) => void): () => void;
95
+ }
96
+ /**
97
+ * Create a live channel state machine starting from "disconnected".
98
+ */
99
+ declare function createLiveChannel(): LiveChannel;
100
+ //#endregion
101
+ //#region src/channels/feishu.d.ts
102
+ interface FeishuConfig {
103
+ /** ι£žδΉ¦εΊ”η”¨ App ID. */
104
+ appId: string;
105
+ /** ι£žδΉ¦εΊ”η”¨ App Secret. */
106
+ appSecret: string;
107
+ /** Webhook verification token (for HTTP mode, not used in WS mode). */
108
+ verificationToken?: string;
109
+ /** API base URL (defaults to https://open.feishu.cn). */
110
+ baseUrl?: string;
111
+ }
112
+ /**
113
+ * Create a 飞书 channel adapter.
114
+ *
115
+ * Sending uses the REST API. Receiving uses a WebSocket long‑connection
116
+ * established during init(). Incoming messages are parsed and dispatched
117
+ * to registered onMessage handlers.
118
+ */
119
+ declare function createFeishuAdapter(config: FeishuConfig): ChannelAdapter;
120
+ //#endregion
121
+ //#region src/registry.d.ts
122
+ /** Lightweight registry for channel adapters. */
123
+ interface ChannelRegistry {
124
+ /** Register a channel adapter. Replaces any existing adapter with the same ID. */
125
+ register(adapter: ChannelAdapter): void;
126
+ /** Remove a channel by ID. Calls adapter.destroy() first. */
127
+ unregister(channelId: string): Promise<void>;
128
+ /** Get an adapter by ID. */
129
+ get(channelId: string): ChannelAdapter | undefined;
130
+ /** Initialize all registered adapters. */
131
+ initAll(): Promise<void>;
132
+ /** Destroy all registered adapters. */
133
+ destroyAll(): Promise<void>;
134
+ /** Set the handler for all incoming messages from any channel. */
135
+ onMessage(handler: (msg: Message) => void): void;
136
+ /** List IDs of all registered adapters. */
137
+ list(): string[];
138
+ }
139
+ /**
140
+ * Create a channel registry for managing multiple channel adapters.
141
+ */
142
+ declare function createChannelRegistry(): ChannelRegistry;
143
+ //#endregion
144
+ export { type ChannelAdapter, type ChannelCapabilities, type ChannelRegistry, type ChannelState, type FeishuConfig, type LiveChannel, createChannelRegistry, createFeishuAdapter, createLiveChannel, describeCapabilities, validateCapabilities };
145
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/contracts.ts","../src/live.ts","../src/channels/feishu.ts","../src/registry.ts"],"mappings":";;;;UAaiB,mBAAA;EAQf;EANA,IAAA;EAUA;EARA,KAAA;EAUM;EARN,WAAA;EAmBe;EAjBf,KAAA;;EAEA,SAAA;EA0BQ;EAxBR,IAAA;EAuCqB;EArCrB,MAAA;AAAA;;;;;;;UAWe,cAAA;EAWP;EAAA,SATC,SAAA;EAiBoB;EAAA,SAdpB,YAAA,EAAc,mBAAA;EAcb;;;;EARV,IAAA,IAAQ,OAAA;EAqBR;;;;;;EAbA,SAAA,CAAU,OAAA,GAAU,OAAA,EAAS,OAAA;EAkBX;AAMpB;;;;EAjBE,WAAA,CAAY,OAAA,EAAS,OAAA,GAAU,OAAA;EAmB3B;;;;EAbJ,WAAA,EAAa,iBAAA,UAA2B,UAAA,WAAqB,OAAA;EAevB;;AAAS;EAV/C,OAAA,IAAW,OAAA;AAAA;;KAMD,YAAA;EACN,MAAA;AAAA;EACA,MAAA;AAAA;EACA,MAAA;EAAqB,WAAA;AAAA;EACrB,MAAA;EAAiB,OAAA;EAAiB,SAAA;AAAA;;;;;;;;;iBClExB,oBAAA,CAAqB,OAAuB,EAAd,cAAc;ADQpD;AAWR;;AAXQ,iBCMQ,oBAAA,CAAqB,OAAuB,EAAd,cAAc;;;UCnB3C,WAAA;EFSf;EAAA,SEPS,KAAA,EAAO,YAAA;EFWhB;EETA,UAAA,CAAW,IAAA,EAAM,YAAA;EFSX;EEPN,QAAA,CAAS,OAAA,GAAU,IAAA,EAAM,YAAA,EAAc,IAAA,EAAM,YAAA;AAAA;;;;iBAQ/B,iBAAA,IAAqB,WAAW;;;UCP/B,YAAA;EHiBA;EGff,KAAA;;EAEA,SAAA;EHwBQ;EGtBR,iBAAA;EHqCqB;EGnCrB,OAAA;AAAA;;;;;;;;iBAuEc,mBAAA,CAAoB,MAAA,EAAQ,YAAA,GAAe,cAAc;;;;UCxFxD,eAAA;EJWf;EITA,QAAA,CAAS,OAAA,EAAS,cAAA;EJalB;EIVA,UAAA,CAAW,SAAA,WAAoB,OAAA;EJUzB;EIPN,GAAA,CAAI,SAAA,WAAoB,cAAA;EJkBK;EIf7B,OAAA,IAAW,OAAA;EJoBY;EIjBvB,UAAA,IAAc,OAAA;EJ+Be;EI5B7B,SAAA,CAAU,OAAA,GAAU,GAAA,EAAK,OAAA;EJmCM;EIhC/B,IAAA;AAAA;;;;iBAMc,qBAAA,IAAyB,eAAe"}
package/dist/index.mjs ADDED
@@ -0,0 +1,395 @@
1
+ import { ChannelError, asMessageId } from "@lynx/core";
2
+ //#region src/contracts.ts
3
+ /**
4
+ * Channel capability validation.
5
+ *
6
+ * Before using a channel adapter, the runtime verifies that the adapter
7
+ * actually provides the methods it claims to have in its capability
8
+ * declaration.
9
+ */
10
+ /**
11
+ * Verify that a channel adapter fulfills its capability contract.
12
+ *
13
+ * Throws {@link ChannelError} if the adapter claims a capability
14
+ * but doesn't implement the corresponding method.
15
+ */
16
+ function validateCapabilities(adapter) {
17
+ if (adapter.capabilities.edit && typeof adapter.editMessage !== "function") throw new ChannelError(`Channel "${adapter.channelId}" claims edit capability but does not implement editMessage()`, {
18
+ category: "channel",
19
+ userVisible: false
20
+ });
21
+ }
22
+ /**
23
+ * Return a human‑readable summary of what a channel supports.
24
+ */
25
+ function describeCapabilities(adapter) {
26
+ const cap = adapter.capabilities;
27
+ const flags = [];
28
+ if (cap.text) flags.push("text");
29
+ if (cap.media) flags.push("media");
30
+ if (cap.interactive) flags.push("interactive");
31
+ if (cap.voice) flags.push("voice");
32
+ if (cap.reactions) flags.push("reactions");
33
+ if (cap.edit) flags.push("edit");
34
+ if (cap.recall) flags.push("recall");
35
+ return `${adapter.channelId}: ${flags.join(", ") || "no capabilities"}`;
36
+ }
37
+ //#endregion
38
+ //#region src/live.ts
39
+ /**
40
+ * Create a live channel state machine starting from "disconnected".
41
+ */
42
+ function createLiveChannel() {
43
+ let current = { status: "disconnected" };
44
+ const listeners = /* @__PURE__ */ new Set();
45
+ return {
46
+ get state() {
47
+ return current;
48
+ },
49
+ transition(next) {
50
+ const prev = current;
51
+ current = next;
52
+ for (const listener of listeners) try {
53
+ listener(prev, next);
54
+ } catch {}
55
+ },
56
+ onChange(handler) {
57
+ listeners.add(handler);
58
+ return () => listeners.delete(handler);
59
+ }
60
+ };
61
+ }
62
+ //#endregion
63
+ //#region src/channels/feishu.ts
64
+ /**
65
+ * 飞书 (Feishu/Lark) channel adapter.
66
+ *
67
+ * Implements the {@link ChannelAdapter} interface for the 飞书
68
+ * messaging platform. Uses the 飞书 Open API for sending and
69
+ * WebSocket long‑connection for receiving events.
70
+ *
71
+ * Receiving flow:
72
+ * 1. init() β†’ get tenant_access_token
73
+ * 2. POST /open-apis/ws/v1/connection β†’ get WebSocket endpoint
74
+ * 3. Connect WebSocket β†’ receive events
75
+ * 4. Parse im.message.receive_v1 β†’ convert to Lynx Message
76
+ * 5. Dispatch to registered onMessage handlers
77
+ */
78
+ const DEFAULT_BASE_URL = "https://open.feishu.cn";
79
+ /** Reconnect delay range in ms. */
80
+ const RECONNECT_MIN_MS = 1e3;
81
+ const RECONNECT_MAX_MS = 3e4;
82
+ /** WebSocket ping interval in ms. */
83
+ const PING_INTERVAL_MS = 3e4;
84
+ const FEISHU_CAPABILITIES = {
85
+ text: true,
86
+ media: true,
87
+ interactive: true,
88
+ voice: false,
89
+ reactions: false,
90
+ edit: true,
91
+ recall: true
92
+ };
93
+ /**
94
+ * Create a 飞书 channel adapter.
95
+ *
96
+ * Sending uses the REST API. Receiving uses a WebSocket long‑connection
97
+ * established during init(). Incoming messages are parsed and dispatched
98
+ * to registered onMessage handlers.
99
+ */
100
+ function createFeishuAdapter(config) {
101
+ const baseUrl = config.baseUrl ?? DEFAULT_BASE_URL;
102
+ let accessToken;
103
+ const messageHandlers = /* @__PURE__ */ new Set();
104
+ let ws = null;
105
+ let reconnectTimer = null;
106
+ let pingTimer = null;
107
+ let reconnectDelay = RECONNECT_MIN_MS;
108
+ let destroyed = false;
109
+ /** Establish the WebSocket connection and start receiving events. */
110
+ async function connectWs() {
111
+ if (destroyed || !accessToken) return;
112
+ try {
113
+ const connResp = await fetch(`${baseUrl}/open-apis/ws/v1/connection`, {
114
+ method: "POST",
115
+ headers: {
116
+ Authorization: `Bearer ${accessToken}`,
117
+ "Content-Type": "application/json"
118
+ }
119
+ });
120
+ if (!connResp.ok) throw new Error(`θŽ·ε– WebSocket η«―η‚Ήε€±θ΄₯:HTTP ${connResp.status}`);
121
+ const connData = await connResp.json();
122
+ if (connData.code !== 0 || !connData.data?.endpoint) throw new Error(`θŽ·ε– WebSocket η«―η‚Ήε€±θ΄₯:${connData.msg ?? "ζ— η«―η‚Ή"}`);
123
+ ws = new WebSocket(connData.data.endpoint);
124
+ ws.addEventListener("open", () => {
125
+ reconnectDelay = RECONNECT_MIN_MS;
126
+ startPing();
127
+ });
128
+ ws.addEventListener("message", (event) => {
129
+ try {
130
+ const raw = typeof event.data === "string" ? event.data : "";
131
+ if (!raw) return;
132
+ handleWsMessage(JSON.parse(raw));
133
+ } catch {}
134
+ });
135
+ ws.addEventListener("close", () => {
136
+ stopPing();
137
+ ws = null;
138
+ if (!destroyed) scheduleReconnect();
139
+ });
140
+ ws.addEventListener("error", () => {});
141
+ } catch {
142
+ if (!destroyed) scheduleReconnect();
143
+ }
144
+ }
145
+ /** Process an incoming WebSocket message from Feishu. */
146
+ function handleWsMessage(msg) {
147
+ if (msg.type === "url_verification" && msg.challenge) {
148
+ ws?.send(JSON.stringify({
149
+ type: "url_verification",
150
+ challenge: msg.challenge
151
+ }));
152
+ return;
153
+ }
154
+ if (msg.type === "event_callback" && msg.data) {
155
+ if (msg.data.header?.event_type === "im.message.receive_v1") {
156
+ const lynxMsg = convertToLynxMessage(msg.data.event);
157
+ if (lynxMsg) dispatchToHandlers(lynxMsg);
158
+ }
159
+ }
160
+ }
161
+ /** Dispatch a message to all registered handlers, catching errors per handler. */
162
+ function dispatchToHandlers(msg) {
163
+ for (const handler of messageHandlers) try {
164
+ handler(msg);
165
+ } catch {}
166
+ }
167
+ /** Convert a Feishu message event to a Lynx Message. */
168
+ function convertToLynxMessage(event) {
169
+ const msg = event.message;
170
+ if (!msg) return null;
171
+ let text = "";
172
+ if (msg.message_type === "text") try {
173
+ text = JSON.parse(msg.content).text ?? "";
174
+ } catch {
175
+ text = msg.content;
176
+ }
177
+ if (!text) return null;
178
+ return {
179
+ id: asMessageId(msg.message_id),
180
+ role: "user",
181
+ content: [{
182
+ type: "text",
183
+ text
184
+ }],
185
+ timestamp: Number(msg.create_time) * 1e3,
186
+ turnIndex: 0
187
+ };
188
+ }
189
+ /** Schedule a reconnection attempt with exponential backoff. */
190
+ function scheduleReconnect() {
191
+ if (destroyed || reconnectTimer) return;
192
+ reconnectTimer = setTimeout(() => {
193
+ reconnectTimer = null;
194
+ connectWs();
195
+ }, reconnectDelay);
196
+ reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
197
+ }
198
+ /** Start the WebSocket ping keepalive. */
199
+ function startPing() {
200
+ stopPing();
201
+ pingTimer = setInterval(() => {
202
+ if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: "ping" }));
203
+ }, PING_INTERVAL_MS);
204
+ }
205
+ /** Stop the WebSocket ping keepalive. */
206
+ function stopPing() {
207
+ if (pingTimer) {
208
+ clearInterval(pingTimer);
209
+ pingTimer = null;
210
+ }
211
+ }
212
+ /** Tear down the WebSocket connection and all timers. */
213
+ function disconnectWs() {
214
+ if (reconnectTimer) {
215
+ clearTimeout(reconnectTimer);
216
+ reconnectTimer = null;
217
+ }
218
+ stopPing();
219
+ if (ws) {
220
+ ws.close(1e3, "adapter destroyed");
221
+ ws = null;
222
+ }
223
+ }
224
+ return {
225
+ channelId: "feishu",
226
+ capabilities: FEISHU_CAPABILITIES,
227
+ async init() {
228
+ if (!config.appId || !config.appSecret) throw new ChannelError("飞书 adapter requires appId and appSecret", {
229
+ category: "channel",
230
+ recoverable: false,
231
+ userVisible: true
232
+ });
233
+ try {
234
+ const response = await fetch(`${baseUrl}/open-apis/auth/v3/tenant_access_token/internal`, {
235
+ method: "POST",
236
+ headers: { "Content-Type": "application/json" },
237
+ body: JSON.stringify({
238
+ app_id: config.appId,
239
+ app_secret: config.appSecret
240
+ })
241
+ });
242
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
243
+ const data = await response.json();
244
+ if (!data.tenant_access_token) throw new Error("响应中无 tenant_access_token");
245
+ accessToken = data.tenant_access_token;
246
+ } catch (err) {
247
+ throw new ChannelError(`飞书 authentication failed: ${String(err)}`, {
248
+ category: "channel",
249
+ recoverable: true,
250
+ retryable: true
251
+ });
252
+ }
253
+ await connectWs();
254
+ },
255
+ onMessage(handler) {
256
+ messageHandlers.add(handler);
257
+ return () => messageHandlers.delete(handler);
258
+ },
259
+ async sendMessage(message) {
260
+ if (!accessToken) throw new ChannelError("ι£žδΉ¦ι€‚ι…ε™¨ζœͺεˆε§‹εŒ– β€” θ―·ε…ˆθ°ƒη”¨ init()", {
261
+ category: "channel",
262
+ recoverable: true,
263
+ retryable: true
264
+ });
265
+ const text = extractTextContent(message);
266
+ try {
267
+ const response = await fetch(`${baseUrl}/open-apis/im/v1/messages?receive_id_type=open_id`, {
268
+ method: "POST",
269
+ headers: {
270
+ "Content-Type": "application/json",
271
+ Authorization: `Bearer ${accessToken}`
272
+ },
273
+ body: JSON.stringify({
274
+ receive_id: message.id,
275
+ msg_type: "text",
276
+ content: JSON.stringify({ text })
277
+ })
278
+ });
279
+ if (!response.ok) throw new ChannelError(`飞书 send failed: HTTP ${response.status}`, {
280
+ category: "channel",
281
+ recoverable: true,
282
+ retryable: true
283
+ });
284
+ return (await response.json()).data?.message_id ?? "unknown";
285
+ } catch (err) {
286
+ throw new ChannelError(`飞书 sendMessage error: ${String(err)}`, {
287
+ category: "channel",
288
+ recoverable: true,
289
+ retryable: true
290
+ });
291
+ }
292
+ },
293
+ /**
294
+ * Edit a previously sent message via the 飞书 Open API.
295
+ *
296
+ * Uses PUT /open-apis/im/v1/messages/:message_id to update the
297
+ * text content of an existing message.
298
+ */
299
+ async editMessage(platformMessageId, newContent) {
300
+ if (!accessToken) throw new ChannelError("ι£žδΉ¦ι€‚ι…ε™¨ζœͺεˆε§‹εŒ– β€” θ―·ε…ˆθ°ƒη”¨ init()", {
301
+ category: "channel",
302
+ recoverable: true,
303
+ retryable: true
304
+ });
305
+ try {
306
+ const response = await fetch(`${baseUrl}/open-apis/im/v1/messages/${platformMessageId}`, {
307
+ method: "PUT",
308
+ headers: {
309
+ "Content-Type": "application/json",
310
+ Authorization: `Bearer ${accessToken}`
311
+ },
312
+ body: JSON.stringify({ content: JSON.stringify({ text: newContent }) })
313
+ });
314
+ if (!response.ok) throw new ChannelError(`飞书 editMessage failed: HTTP ${response.status}`, {
315
+ category: "channel",
316
+ recoverable: true,
317
+ retryable: true
318
+ });
319
+ } catch (err) {
320
+ if (err instanceof ChannelError) throw err;
321
+ throw new ChannelError(`飞书 editMessage error: ${String(err)}`, {
322
+ category: "channel",
323
+ recoverable: true,
324
+ retryable: true
325
+ });
326
+ }
327
+ },
328
+ async destroy() {
329
+ destroyed = true;
330
+ disconnectWs();
331
+ messageHandlers.clear();
332
+ accessToken = void 0;
333
+ }
334
+ };
335
+ }
336
+ function extractTextContent(message) {
337
+ for (const block of message.content) if (block.type === "text") return block.text;
338
+ return "";
339
+ }
340
+ //#endregion
341
+ //#region src/registry.ts
342
+ /**
343
+ * Create a channel registry for managing multiple channel adapters.
344
+ */
345
+ function createChannelRegistry() {
346
+ const adapters = /* @__PURE__ */ new Map();
347
+ const cleanups = /* @__PURE__ */ new Map();
348
+ let messageHandler = null;
349
+ return {
350
+ register(adapter) {
351
+ const prevCleanup = cleanups.get(adapter.channelId);
352
+ if (prevCleanup) prevCleanup();
353
+ adapters.set(adapter.channelId, adapter);
354
+ const unsub = adapter.onMessage((msg) => {
355
+ if (messageHandler) try {
356
+ messageHandler(msg);
357
+ } catch {}
358
+ });
359
+ cleanups.set(adapter.channelId, unsub);
360
+ },
361
+ async unregister(channelId) {
362
+ const cleanup = cleanups.get(channelId);
363
+ if (cleanup) cleanup();
364
+ cleanups.delete(channelId);
365
+ const adapter = adapters.get(channelId);
366
+ adapters.delete(channelId);
367
+ if (adapter) try {
368
+ await adapter.destroy();
369
+ } catch {}
370
+ },
371
+ get(channelId) {
372
+ return adapters.get(channelId);
373
+ },
374
+ async initAll() {
375
+ const results = await Promise.allSettled([...adapters.values()].map((a) => a.init()));
376
+ for (const result of results) if (result.status === "rejected") process.stderr.write(`[lynx] Channel init failed: ${String(result.reason)}\n`);
377
+ },
378
+ async destroyAll() {
379
+ for (const cleanup of cleanups.values()) cleanup();
380
+ cleanups.clear();
381
+ await Promise.allSettled([...adapters.values()].map((a) => a.destroy()));
382
+ adapters.clear();
383
+ },
384
+ onMessage(handler) {
385
+ messageHandler = handler;
386
+ },
387
+ list() {
388
+ return [...adapters.keys()];
389
+ }
390
+ };
391
+ }
392
+ //#endregion
393
+ export { createChannelRegistry, createFeishuAdapter, createLiveChannel, describeCapabilities, validateCapabilities };
394
+
395
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +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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"}
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@24klynx/channels",
3
+ "version": "0.1.0",
4
+ "description": "Message channel adapters β€” QQ, Feishu, WeChat",
5
+ "type": "module",
6
+ "main": "./dist/index.mjs",
7
+ "types": "./dist/index.d.mts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.mjs",
11
+ "types": "./dist/index.d.mts"
12
+ }
13
+ },
14
+ "dependencies": {
15
+ "@24klynx/core": "0.1.0"
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "scripts": {
24
+ "build": "tsdown --config-loader tsx",
25
+ "test": "vitest run --passWithNoTests",
26
+ "typecheck": "tsgo --noEmit"
27
+ }
28
+ }