@ebowwa/channel-types 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.
- package/dist/config.d.ts +157 -0
- package/dist/config.js +257 -0
- package/dist/example.d.ts +30 -0
- package/dist/example.js +221 -0
- package/dist/index.d.ts +212 -0
- package/dist/index.js +60 -0
- package/package.json +24 -0
- package/src/config.ts +429 -0
- package/src/example.ts +287 -0
- package/src/index.ts +367 -0
- package/tsconfig.json +20 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ebowwa/channel-types/config
|
|
3
|
+
*
|
|
4
|
+
* Composable channel configuration loading.
|
|
5
|
+
* Supports Doppler (env vars), JSON files, and programmatic config.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const config = loadChannelConfig(); // From env
|
|
9
|
+
* const config = parseChannelConfig(jsonString); // From JSON
|
|
10
|
+
* const config = createChannelConfig({ ... }); // Programmatic
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ChannelPlatform, ChannelId } from "./index.js";
|
|
14
|
+
import { createChannelId } from "./index.js";
|
|
15
|
+
|
|
16
|
+
// ============================================================
|
|
17
|
+
// CHANNEL CONFIG SCHEMA
|
|
18
|
+
// ============================================================
|
|
19
|
+
|
|
20
|
+
/** Base configuration for all channels */
|
|
21
|
+
export interface BaseChannelConfig {
|
|
22
|
+
/** Channel platform */
|
|
23
|
+
platform: ChannelPlatform;
|
|
24
|
+
|
|
25
|
+
/** Unique account/bot identifier */
|
|
26
|
+
accountId: string;
|
|
27
|
+
|
|
28
|
+
/** Enable/disable channel */
|
|
29
|
+
enabled: boolean;
|
|
30
|
+
|
|
31
|
+
/** Human-readable label */
|
|
32
|
+
label?: string;
|
|
33
|
+
|
|
34
|
+
/** Optional instance ID for multiple instances */
|
|
35
|
+
instanceId?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Telegram channel configuration */
|
|
39
|
+
export interface TelegramChannelConfig extends BaseChannelConfig {
|
|
40
|
+
platform: "telegram";
|
|
41
|
+
/** Bot token from @BotFather */
|
|
42
|
+
botToken: string;
|
|
43
|
+
/** Polling interval in ms (default: 1000) */
|
|
44
|
+
pollingInterval?: number;
|
|
45
|
+
/** Allowed user IDs (empty = all) */
|
|
46
|
+
allowedUsers?: number[];
|
|
47
|
+
/** Allowed chat IDs (empty = all) */
|
|
48
|
+
allowedChats?: number[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Discord channel configuration */
|
|
52
|
+
export interface DiscordChannelConfig extends BaseChannelConfig {
|
|
53
|
+
platform: "discord";
|
|
54
|
+
/** Bot token */
|
|
55
|
+
botToken: string;
|
|
56
|
+
/** Application ID */
|
|
57
|
+
applicationId?: string;
|
|
58
|
+
/** Guild ID to restrict to */
|
|
59
|
+
guildId?: string;
|
|
60
|
+
/** Channel IDs to listen to */
|
|
61
|
+
channelIds?: string[];
|
|
62
|
+
/** Enable slash commands */
|
|
63
|
+
enableSlashCommands?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** WhatsApp channel configuration */
|
|
67
|
+
export interface WhatsAppChannelConfig extends BaseChannelConfig {
|
|
68
|
+
platform: "whatsapp";
|
|
69
|
+
/** Webhook URL for business API */
|
|
70
|
+
webhookUrl?: string;
|
|
71
|
+
/** Phone number ID */
|
|
72
|
+
phoneNumberId?: string;
|
|
73
|
+
/** Access token */
|
|
74
|
+
accessToken?: string;
|
|
75
|
+
/** Or use QR code pairing (non-business) */
|
|
76
|
+
sessionPath?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** iMessage channel configuration */
|
|
80
|
+
export interface IMessageChannelConfig extends BaseChannelConfig {
|
|
81
|
+
platform: "imessage";
|
|
82
|
+
/** BlueBubbles server URL */
|
|
83
|
+
serverUrl?: string;
|
|
84
|
+
/** BlueBubbles password */
|
|
85
|
+
password?: string;
|
|
86
|
+
/** Or use local iMessage */
|
|
87
|
+
local?: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Slack channel configuration */
|
|
91
|
+
export interface SlackChannelConfig extends BaseChannelConfig {
|
|
92
|
+
platform: "slack";
|
|
93
|
+
/** Bot token (xoxb-...) */
|
|
94
|
+
botToken: string;
|
|
95
|
+
/** App token (xapp-...) for Socket Mode */
|
|
96
|
+
appToken?: string;
|
|
97
|
+
/** Signing secret */
|
|
98
|
+
signingSecret?: string;
|
|
99
|
+
/** Channel IDs to listen to */
|
|
100
|
+
channelIds?: string[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Signal channel configuration */
|
|
104
|
+
export interface SignalChannelConfig extends BaseChannelConfig {
|
|
105
|
+
platform: "signal";
|
|
106
|
+
/** Phone number */
|
|
107
|
+
phoneNumber: string;
|
|
108
|
+
/** signal-cli path */
|
|
109
|
+
cliPath?: string;
|
|
110
|
+
/** Signal REST API URL */
|
|
111
|
+
apiUrl?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** CLI channel configuration (terminal-based) */
|
|
115
|
+
export interface CLIChannelConfig extends BaseChannelConfig {
|
|
116
|
+
platform: "cli";
|
|
117
|
+
/** Enable interactive mode */
|
|
118
|
+
interactive?: boolean;
|
|
119
|
+
/** Prompt string */
|
|
120
|
+
prompt?: string;
|
|
121
|
+
/** History file path */
|
|
122
|
+
historyPath?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Web channel configuration */
|
|
126
|
+
export interface WebChannelConfig extends BaseChannelConfig {
|
|
127
|
+
platform: "web";
|
|
128
|
+
/** Port to listen on */
|
|
129
|
+
port?: number;
|
|
130
|
+
/** Host to bind to */
|
|
131
|
+
host?: string;
|
|
132
|
+
/** API key for authentication */
|
|
133
|
+
apiKey?: string;
|
|
134
|
+
/** Enable CORS */
|
|
135
|
+
cors?: boolean;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Union of all channel configs */
|
|
139
|
+
export type ChannelConfig =
|
|
140
|
+
| TelegramChannelConfig
|
|
141
|
+
| DiscordChannelConfig
|
|
142
|
+
| WhatsAppChannelConfig
|
|
143
|
+
| IMessageChannelConfig
|
|
144
|
+
| SlackChannelConfig
|
|
145
|
+
| SignalChannelConfig
|
|
146
|
+
| CLIChannelConfig
|
|
147
|
+
| WebChannelConfig;
|
|
148
|
+
|
|
149
|
+
/** Map of channel name -> config */
|
|
150
|
+
export type ChannelConfigMap = Record<string, ChannelConfig>;
|
|
151
|
+
|
|
152
|
+
// ============================================================
|
|
153
|
+
// CONFIG LOADING FROM DOPPLER (ENV VARS)
|
|
154
|
+
// ============================================================
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Environment variable naming convention:
|
|
158
|
+
*
|
|
159
|
+
* {PLATFORM}_ENABLED - Enable channel (true/false)
|
|
160
|
+
* {PLATFORM}_BOT_TOKEN - Bot token
|
|
161
|
+
* {PLATFORM}_ACCOUNT_ID - Account identifier
|
|
162
|
+
* {PLATFORM}_{OPTION} - Platform-specific options
|
|
163
|
+
*
|
|
164
|
+
* Examples:
|
|
165
|
+
* TELEGRAM_ENABLED=true
|
|
166
|
+
* TELEGRAM_BOT_TOKEN=123456:ABC
|
|
167
|
+
* DISCORD_ENABLED=true
|
|
168
|
+
* DISCORD_BOT_TOKEN=xyz.abc
|
|
169
|
+
* DISCORD_GUILD_ID=123456789
|
|
170
|
+
*/
|
|
171
|
+
|
|
172
|
+
/** Load all channel configs from environment variables (Doppler) */
|
|
173
|
+
export function loadChannelConfigsFromEnv(): ChannelConfigMap {
|
|
174
|
+
const configs: ChannelConfigMap = {};
|
|
175
|
+
|
|
176
|
+
// Telegram
|
|
177
|
+
if (isChannelEnabled("TELEGRAM")) {
|
|
178
|
+
configs.telegram = loadTelegramConfig();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Discord
|
|
182
|
+
if (isChannelEnabled("DISCORD")) {
|
|
183
|
+
configs.discord = loadDiscordConfig();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// WhatsApp
|
|
187
|
+
if (isChannelEnabled("WHATSAPP")) {
|
|
188
|
+
configs.whatsapp = loadWhatsAppConfig();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// iMessage
|
|
192
|
+
if (isChannelEnabled("IMESSAGE")) {
|
|
193
|
+
configs.imessage = loadIMessageConfig();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Slack
|
|
197
|
+
if (isChannelEnabled("SLACK")) {
|
|
198
|
+
configs.slack = loadSlackConfig();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Signal
|
|
202
|
+
if (isChannelEnabled("SIGNAL")) {
|
|
203
|
+
configs.signal = loadSignalConfig();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// CLI (enabled by default)
|
|
207
|
+
if (isChannelEnabled("CLI", true)) {
|
|
208
|
+
configs.cli = loadCLIConfig();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Web
|
|
212
|
+
if (isChannelEnabled("WEB")) {
|
|
213
|
+
configs.web = loadWebConfig();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return configs;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Check if a channel is enabled via env var */
|
|
220
|
+
function isChannelEnabled(prefix: string, defaultEnabled = false): boolean {
|
|
221
|
+
const value = process.env[`${prefix}_ENABLED`];
|
|
222
|
+
if (value === undefined) return defaultEnabled;
|
|
223
|
+
return value === "true" || value === "1";
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Get env var with fallback */
|
|
227
|
+
function getEnv(key: string, fallback?: string): string | undefined {
|
|
228
|
+
return process.env[key] ?? fallback;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Get required env var (throws if missing) */
|
|
232
|
+
function requireEnv(key: string): string {
|
|
233
|
+
const value = process.env[key];
|
|
234
|
+
if (!value) {
|
|
235
|
+
throw new Error(`Required environment variable ${key} is not set`);
|
|
236
|
+
}
|
|
237
|
+
return value;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Parse comma-separated list from env */
|
|
241
|
+
function parseList(value?: string): string[] | undefined {
|
|
242
|
+
if (!value) return undefined;
|
|
243
|
+
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Parse number list from env */
|
|
247
|
+
function parseNumberList(value?: string): number[] | undefined {
|
|
248
|
+
const list = parseList(value);
|
|
249
|
+
return list?.map((s) => parseInt(s, 10)).filter((n) => !isNaN(n));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Individual channel loaders
|
|
253
|
+
|
|
254
|
+
function loadTelegramConfig(): TelegramChannelConfig {
|
|
255
|
+
return {
|
|
256
|
+
platform: "telegram",
|
|
257
|
+
accountId: getEnv("TELEGRAM_ACCOUNT_ID", "default")!,
|
|
258
|
+
enabled: true,
|
|
259
|
+
botToken: requireEnv("TELEGRAM_BOT_TOKEN"),
|
|
260
|
+
pollingInterval: parseInt(getEnv("TELEGRAM_POLLING_INTERVAL", "1000")!, 10),
|
|
261
|
+
allowedUsers: parseNumberList(getEnv("TELEGRAM_ALLOWED_USERS")),
|
|
262
|
+
allowedChats: parseNumberList(getEnv("TELEGRAM_ALLOWED_CHATS")),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function loadDiscordConfig(): DiscordChannelConfig {
|
|
267
|
+
return {
|
|
268
|
+
platform: "discord",
|
|
269
|
+
accountId: getEnv("DISCORD_ACCOUNT_ID", "default")!,
|
|
270
|
+
enabled: true,
|
|
271
|
+
botToken: requireEnv("DISCORD_BOT_TOKEN"),
|
|
272
|
+
applicationId: getEnv("DISCORD_APPLICATION_ID"),
|
|
273
|
+
guildId: getEnv("DISCORD_GUILD_ID"),
|
|
274
|
+
channelIds: parseList(getEnv("DISCORD_CHANNEL_IDS")),
|
|
275
|
+
enableSlashCommands: getEnv("DISCORD_ENABLE_SLASH_COMMANDS", "true") === "true",
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function loadWhatsAppConfig(): WhatsAppChannelConfig {
|
|
280
|
+
return {
|
|
281
|
+
platform: "whatsapp",
|
|
282
|
+
accountId: getEnv("WHATSAPP_ACCOUNT_ID", "default")!,
|
|
283
|
+
enabled: true,
|
|
284
|
+
webhookUrl: getEnv("WHATSAPP_WEBHOOK_URL"),
|
|
285
|
+
phoneNumberId: getEnv("WHATSAPP_PHONE_NUMBER_ID"),
|
|
286
|
+
accessToken: getEnv("WHATSAPP_ACCESS_TOKEN"),
|
|
287
|
+
sessionPath: getEnv("WHATSAPP_SESSION_PATH"),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function loadIMessageConfig(): IMessageChannelConfig {
|
|
292
|
+
return {
|
|
293
|
+
platform: "imessage",
|
|
294
|
+
accountId: getEnv("IMESSAGE_ACCOUNT_ID", "default")!,
|
|
295
|
+
enabled: true,
|
|
296
|
+
serverUrl: getEnv("IMESSAGE_SERVER_URL"),
|
|
297
|
+
password: getEnv("IMESSAGE_PASSWORD"),
|
|
298
|
+
local: getEnv("IMESSAGE_LOCAL", "false") === "true",
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function loadSlackConfig(): SlackChannelConfig {
|
|
303
|
+
return {
|
|
304
|
+
platform: "slack",
|
|
305
|
+
accountId: getEnv("SLACK_ACCOUNT_ID", "default")!,
|
|
306
|
+
enabled: true,
|
|
307
|
+
botToken: requireEnv("SLACK_BOT_TOKEN"),
|
|
308
|
+
appToken: getEnv("SLACK_APP_TOKEN"),
|
|
309
|
+
signingSecret: getEnv("SLACK_SIGNING_SECRET"),
|
|
310
|
+
channelIds: parseList(getEnv("SLACK_CHANNEL_IDS")),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function loadSignalConfig(): SignalChannelConfig {
|
|
315
|
+
return {
|
|
316
|
+
platform: "signal",
|
|
317
|
+
accountId: getEnv("SIGNAL_ACCOUNT_ID", "default")!,
|
|
318
|
+
enabled: true,
|
|
319
|
+
phoneNumber: requireEnv("SIGNAL_PHONE_NUMBER"),
|
|
320
|
+
cliPath: getEnv("SIGNAL_CLI_PATH"),
|
|
321
|
+
apiUrl: getEnv("SIGNAL_API_URL"),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function loadCLIConfig(): CLIChannelConfig {
|
|
326
|
+
return {
|
|
327
|
+
platform: "cli",
|
|
328
|
+
accountId: getEnv("CLI_ACCOUNT_ID", "default")!,
|
|
329
|
+
enabled: true,
|
|
330
|
+
interactive: getEnv("CLI_INTERACTIVE", "true") === "true",
|
|
331
|
+
prompt: getEnv("CLI_PROMPT", "> "),
|
|
332
|
+
historyPath: getEnv("CLI_HISTORY_PATH"),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function loadWebConfig(): WebChannelConfig {
|
|
337
|
+
return {
|
|
338
|
+
platform: "web",
|
|
339
|
+
accountId: getEnv("WEB_ACCOUNT_ID", "default")!,
|
|
340
|
+
enabled: true,
|
|
341
|
+
port: parseInt(getEnv("WEB_PORT", "3000")!, 10),
|
|
342
|
+
host: getEnv("WEB_HOST", "0.0.0.0"),
|
|
343
|
+
apiKey: getEnv("WEB_API_KEY"),
|
|
344
|
+
cors: getEnv("WEB_CORS", "true") === "true",
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ============================================================
|
|
349
|
+
// CONFIG UTILITIES
|
|
350
|
+
// ============================================================
|
|
351
|
+
|
|
352
|
+
/** Get ChannelId from config */
|
|
353
|
+
export function getChannelId(config: ChannelConfig): ChannelId {
|
|
354
|
+
return createChannelId(config.platform, config.accountId, config.instanceId);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Validate channel config */
|
|
358
|
+
export function validateChannelConfig(config: ChannelConfig): string[] {
|
|
359
|
+
const errors: string[] = [];
|
|
360
|
+
|
|
361
|
+
switch (config.platform) {
|
|
362
|
+
case "telegram":
|
|
363
|
+
if (!config.botToken) errors.push("Telegram: botToken is required");
|
|
364
|
+
break;
|
|
365
|
+
case "discord":
|
|
366
|
+
if (!config.botToken) errors.push("Discord: botToken is required");
|
|
367
|
+
break;
|
|
368
|
+
case "whatsapp":
|
|
369
|
+
if (!config.webhookUrl && !config.sessionPath) {
|
|
370
|
+
errors.push("WhatsApp: webhookUrl or sessionPath is required");
|
|
371
|
+
}
|
|
372
|
+
break;
|
|
373
|
+
case "signal":
|
|
374
|
+
if (!config.phoneNumber) errors.push("Signal: phoneNumber is required");
|
|
375
|
+
break;
|
|
376
|
+
case "slack":
|
|
377
|
+
if (!config.botToken) errors.push("Slack: botToken is required");
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return errors;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Filter configs by enabled status */
|
|
385
|
+
export function getEnabledChannels(configs: ChannelConfigMap): ChannelConfig[] {
|
|
386
|
+
return Object.values(configs).filter((c) => c.enabled);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Filter configs by platform */
|
|
390
|
+
export function getChannelsByPlatform(
|
|
391
|
+
configs: ChannelConfigMap,
|
|
392
|
+
platform: ChannelPlatform
|
|
393
|
+
): ChannelConfig[] {
|
|
394
|
+
return Object.values(configs).filter((c) => c.platform === platform);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** Create config programmatically */
|
|
398
|
+
export function createChannelConfig<T extends ChannelConfig>(config: T): T {
|
|
399
|
+
const errors = validateChannelConfig(config);
|
|
400
|
+
if (errors.length > 0) {
|
|
401
|
+
throw new Error(`Invalid channel config: ${errors.join(", ")}`);
|
|
402
|
+
}
|
|
403
|
+
return config;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** Parse config from JSON string */
|
|
407
|
+
export function parseChannelConfig(json: string): ChannelConfig {
|
|
408
|
+
const config = JSON.parse(json) as ChannelConfig;
|
|
409
|
+
const errors = validateChannelConfig(config);
|
|
410
|
+
if (errors.length > 0) {
|
|
411
|
+
throw new Error(`Invalid channel config: ${errors.join(", ")}`);
|
|
412
|
+
}
|
|
413
|
+
return config;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Parse multiple configs from JSON string */
|
|
417
|
+
export function parseChannelConfigs(json: string): ChannelConfigMap {
|
|
418
|
+
return JSON.parse(json) as ChannelConfigMap;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Serialize config to JSON */
|
|
422
|
+
export function serializeChannelConfig(config: ChannelConfig): string {
|
|
423
|
+
return JSON.stringify(config, null, 2);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Serialize multiple configs to JSON */
|
|
427
|
+
export function serializeChannelConfigs(configs: ChannelConfigMap): string {
|
|
428
|
+
return JSON.stringify(configs, null, 2);
|
|
429
|
+
}
|
package/src/example.ts
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: Composable Channel-LLM Architecture
|
|
3
|
+
*
|
|
4
|
+
* This demonstrates how to:
|
|
5
|
+
* 1. Create a channel connector (Discord)
|
|
6
|
+
* 2. Create an LLM handler (GLM)
|
|
7
|
+
* 3. Connect them via a bridge
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
ChannelBridge,
|
|
12
|
+
ChannelCapabilities,
|
|
13
|
+
ChannelConnector,
|
|
14
|
+
ChannelId,
|
|
15
|
+
ChannelMessage,
|
|
16
|
+
ChannelResponse,
|
|
17
|
+
LLMHandler,
|
|
18
|
+
MessageHandler,
|
|
19
|
+
StreamChunk,
|
|
20
|
+
} from "./index.js";
|
|
21
|
+
import {
|
|
22
|
+
createChannelId,
|
|
23
|
+
createMessageRef,
|
|
24
|
+
DEFAULT_CAPABILITIES,
|
|
25
|
+
RICH_CAPABILITIES,
|
|
26
|
+
} from "./index.js";
|
|
27
|
+
|
|
28
|
+
// ============================================================
|
|
29
|
+
// EXAMPLE: Discord Channel Connector
|
|
30
|
+
// ============================================================
|
|
31
|
+
|
|
32
|
+
class DiscordChannel implements ChannelConnector {
|
|
33
|
+
readonly id: ChannelId;
|
|
34
|
+
readonly label = "Discord";
|
|
35
|
+
readonly capabilities: ChannelCapabilities = RICH_CAPABILITIES;
|
|
36
|
+
|
|
37
|
+
private messageHandler?: MessageHandler;
|
|
38
|
+
private connected = false;
|
|
39
|
+
|
|
40
|
+
constructor(accountId: string) {
|
|
41
|
+
this.id = createChannelId("discord", accountId);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async start(): Promise<void> {
|
|
45
|
+
// In real impl: connect to Discord gateway
|
|
46
|
+
this.connected = true;
|
|
47
|
+
console.log(`[${this.label}] Started`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async stop(): Promise<void> {
|
|
51
|
+
this.connected = false;
|
|
52
|
+
console.log(`[${this.label}] Stopped`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
onMessage(handler: MessageHandler): void {
|
|
56
|
+
this.messageHandler = handler;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async send(response: ChannelResponse): Promise<void> {
|
|
60
|
+
// In real impl: send to Discord API
|
|
61
|
+
console.log(`[${this.label}] Sending: ${response.content.text}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async stream(
|
|
65
|
+
response: ChannelResponse,
|
|
66
|
+
chunks: AsyncIterable<StreamChunk>
|
|
67
|
+
): Promise<void> {
|
|
68
|
+
// In real impl: edit message with streaming content
|
|
69
|
+
let fullText = "";
|
|
70
|
+
for await (const chunk of chunks) {
|
|
71
|
+
fullText += chunk.text;
|
|
72
|
+
console.log(`[${this.label}] Stream: ${fullText}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
isConnected(): boolean {
|
|
77
|
+
return this.connected;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Simulate receiving a message (for testing)
|
|
81
|
+
async simulateMessage(text: string, senderId: string): Promise<void> {
|
|
82
|
+
if (!this.messageHandler) return;
|
|
83
|
+
|
|
84
|
+
const message: ChannelMessage = {
|
|
85
|
+
messageId: `msg-${Date.now()}`,
|
|
86
|
+
channelId: this.id,
|
|
87
|
+
timestamp: new Date(),
|
|
88
|
+
sender: { id: senderId, username: `user_${senderId}` },
|
|
89
|
+
text,
|
|
90
|
+
context: { isDM: true },
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const response = await this.messageHandler(message);
|
|
94
|
+
if (response) {
|
|
95
|
+
await this.send(response);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ============================================================
|
|
101
|
+
// EXAMPLE: GLM LLM Handler
|
|
102
|
+
// ============================================================
|
|
103
|
+
|
|
104
|
+
class GLMHandler implements LLMHandler {
|
|
105
|
+
readonly id = "glm-4.7";
|
|
106
|
+
readonly model = "glm-4-flash";
|
|
107
|
+
|
|
108
|
+
private ready = true;
|
|
109
|
+
|
|
110
|
+
async process(message: ChannelMessage): Promise<ChannelResponse> {
|
|
111
|
+
// In real impl: call GLM API
|
|
112
|
+
const startTime = Date.now();
|
|
113
|
+
|
|
114
|
+
// Simulate LLM response
|
|
115
|
+
const responseText = this.generateResponse(message.text);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
content: {
|
|
119
|
+
text: responseText,
|
|
120
|
+
replyToOriginal: true,
|
|
121
|
+
},
|
|
122
|
+
replyTo: createMessageRef(message.messageId, message.channelId),
|
|
123
|
+
isComplete: true,
|
|
124
|
+
metadata: {
|
|
125
|
+
model: this.model,
|
|
126
|
+
latency: Date.now() - startTime,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async* stream(message: ChannelMessage): AsyncIterable<StreamChunk> {
|
|
132
|
+
// In real impl: stream from GLM API
|
|
133
|
+
const response = await this.process(message);
|
|
134
|
+
const words = response.content.text.split(" ");
|
|
135
|
+
|
|
136
|
+
for (let i = 0; i < words.length; i++) {
|
|
137
|
+
yield {
|
|
138
|
+
text: words[i] + " ",
|
|
139
|
+
done: i === words.length - 1,
|
|
140
|
+
seq: i,
|
|
141
|
+
};
|
|
142
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
isReady(): boolean {
|
|
147
|
+
return this.ready;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private generateResponse(input: string): string {
|
|
151
|
+
// Simple echo + prefix for demo
|
|
152
|
+
if (input.toLowerCase().includes("hello")) {
|
|
153
|
+
return "Hello! How can I help you today?";
|
|
154
|
+
}
|
|
155
|
+
if (input.toLowerCase().includes("status")) {
|
|
156
|
+
return "All systems operational. GLM 4.7 is ready to assist.";
|
|
157
|
+
}
|
|
158
|
+
return `I received your message: "${input}"`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ============================================================
|
|
163
|
+
// EXAMPLE: Simple Bridge Implementation
|
|
164
|
+
// ============================================================
|
|
165
|
+
|
|
166
|
+
class SimpleBridge implements ChannelBridge {
|
|
167
|
+
private channels: Map<string, ChannelConnector> = new Map();
|
|
168
|
+
private handlers: Map<string, LLMHandler> = new Map();
|
|
169
|
+
private defaultHandlers: Map<string, string> = new Map();
|
|
170
|
+
|
|
171
|
+
registerChannel(channel: ChannelConnector): void {
|
|
172
|
+
const key = this.channelKey(channel.id);
|
|
173
|
+
this.channels.set(key, channel);
|
|
174
|
+
|
|
175
|
+
// Set up message routing
|
|
176
|
+
channel.onMessage(async (message) => {
|
|
177
|
+
const handlerId = this.defaultHandlers.get(key);
|
|
178
|
+
if (!handlerId) {
|
|
179
|
+
console.warn(`No handler for channel ${key}`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const handler = this.handlers.get(handlerId);
|
|
184
|
+
if (!handler) {
|
|
185
|
+
console.warn(`Handler ${handlerId} not found`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return handler.process(message);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
console.log(`[Bridge] Registered channel: ${channel.label}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
registerHandler(handler: LLMHandler): void {
|
|
196
|
+
this.handlers.set(handler.id, handler);
|
|
197
|
+
console.log(`[Bridge] Registered handler: ${handler.id}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
setDefaultHandler(channelId: ChannelId, handlerId: string): void {
|
|
201
|
+
this.defaultHandlers.set(this.channelKey(channelId), handlerId);
|
|
202
|
+
console.log(`[Bridge] Set default handler: ${this.channelKey(channelId)} → ${handlerId}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async start(): Promise<void> {
|
|
206
|
+
for (const channel of this.channels.values()) {
|
|
207
|
+
await channel.start();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async stop(): Promise<void> {
|
|
212
|
+
for (const channel of this.channels.values()) {
|
|
213
|
+
await channel.stop();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
getChannels(): ChannelConnector[] {
|
|
218
|
+
return Array.from(this.channels.values());
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
getHandlers(): LLMHandler[] {
|
|
222
|
+
return Array.from(this.handlers.values());
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private channelKey(id: ChannelId): string {
|
|
226
|
+
return `${id.platform}:${id.accountId}`;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ============================================================
|
|
231
|
+
// USAGE EXAMPLE
|
|
232
|
+
// ============================================================
|
|
233
|
+
|
|
234
|
+
async function main() {
|
|
235
|
+
console.log("=== Composable Channel-LLM Example ===\n");
|
|
236
|
+
|
|
237
|
+
// 1. Create bridge
|
|
238
|
+
const bridge = new SimpleBridge();
|
|
239
|
+
|
|
240
|
+
// 2. Create and register LLM handler
|
|
241
|
+
const glmHandler = new GLMHandler();
|
|
242
|
+
bridge.registerHandler(glmHandler);
|
|
243
|
+
|
|
244
|
+
// 3. Create and register channel
|
|
245
|
+
const discordChannel = new DiscordChannel("my-bot-123");
|
|
246
|
+
bridge.registerChannel(discordChannel);
|
|
247
|
+
|
|
248
|
+
// 4. Connect channel to handler
|
|
249
|
+
bridge.setDefaultHandler(discordChannel.id, glmHandler.id);
|
|
250
|
+
|
|
251
|
+
// 5. Start the bridge
|
|
252
|
+
await bridge.start();
|
|
253
|
+
|
|
254
|
+
// 6. Simulate messages
|
|
255
|
+
console.log("\n--- Simulating messages ---\n");
|
|
256
|
+
await discordChannel.simulateMessage("Hello!", "user-1");
|
|
257
|
+
await discordChannel.simulateMessage("What's the status?", "user-2");
|
|
258
|
+
await discordChannel.simulateMessage("Can you help me with a task?", "user-3");
|
|
259
|
+
|
|
260
|
+
// 7. Stop
|
|
261
|
+
console.log("\n--- Stopping ---\n");
|
|
262
|
+
await bridge.stop();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Run example
|
|
266
|
+
main().catch(console.error);
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Output:
|
|
270
|
+
*
|
|
271
|
+
* === Composable Channel-LLM Example ===
|
|
272
|
+
*
|
|
273
|
+
* [Bridge] Registered handler: glm-4.7
|
|
274
|
+
* [Bridge] Registered channel: Discord
|
|
275
|
+
* [Bridge] Set default handler: discord:my-bot-123 → glm-4.7
|
|
276
|
+
* [Discord] Started
|
|
277
|
+
*
|
|
278
|
+
* --- Simulating messages ---
|
|
279
|
+
*
|
|
280
|
+
* [Discord] Sending: Hello! How can I help you today?
|
|
281
|
+
* [Discord] Sending: All systems operational. GLM 4.7 is ready to assist.
|
|
282
|
+
* [Discord] Sending: I received your message: "Can you help me with a task?"
|
|
283
|
+
*
|
|
284
|
+
* --- Stopping ---
|
|
285
|
+
*
|
|
286
|
+
* [Discord] Stopped
|
|
287
|
+
*/
|