Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 53 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
[![GitHub release](https://img.shields.io/github/v/release/Doctor-wu/agent-im-relay)](https://github.com/Doctor-wu/agent-im-relay/releases)
![Node >=20](https://img.shields.io/badge/node-%3E%3D20-339933)
![TypeScript](https://img.shields.io/badge/language-TypeScript-3178C6)
[![Discord](https://img.shields.io/badge/platform-Discord-5865F2)](https://discord.com)
[![Feishu](https://img.shields.io/badge/platform-Feishu-00B96B)](https://open.feishu.cn)
![Discord](https://img.shields.io/badge/platform-Discord-5865F2)
![Feishu long connection](https://img.shields.io/badge/platform-Feishu-long_connection-00B96B)
![Telegram](https://img.shields.io/badge/platform-Telegram-26A5E4)

---

Expand Down Expand Up @@ -165,14 +166,59 @@ apps/
agent-inbox/ @doctorwu/agent-inbox - End-user CLI entrypoint and interactive setup wizard

packages/
core/ @agent-im-relay/core - Shared runtime, session management, and backend abstractions
discord/ @agent-im-relay/discord - Discord adapter
feishu/ @agent-im-relay/feishu - Feishu adapter
core/ @agent-im-relay/core — Shared runtime, state, orchestration
discord/ @agent-im-relay/discord — Discord adapter runtime
feishu/ @agent-im-relay/feishu — Feishu adapter runtime
telegram/ @agent-im-relay/telegram — Telegram adapter runtime
```

Architecture design document: [docs/agent-inbox-architecture.md](docs/agent-inbox-architecture.md)

---
The Feishu adapter now stays inside `@agent-im-relay/feishu` and uses the official persistent connection flow directly:

- Long-connection ingress through Feishu's event dispatcher and WebSocket client
- Private-chat launchers that create dedicated session chats and return native shared-chat receipts
- Session-group reference messages plus mirrored original prompts for readable context
- One-shot interrupt cards for each user message inside a session chat
- Sticky per-conversation session continuity until explicit teardown
- Inbound file download and outbound artifact upload support
- Optional event verification and decryption via `FEISHU_VERIFICATION_TOKEN` and `FEISHU_ENCRYPT_KEY`

Typical startup flow:

1. Enable persistent connection mode in the Feishu developer console.
2. Configure `FEISHU_APP_ID` and `FEISHU_APP_SECRET`, plus `FEISHU_ENCRYPT_KEY` / `FEISHU_VERIFICATION_TOKEN` if your app uses them.
3. Start `pnpm dev:feishu` on the machine that has the local agent CLI tools and workspace.
4. Send the bot a private message to create a `Session · {promptPreview}` chat, then continue inside that session chat with the per-message interrupt card.

## Telegram Runtime

The Telegram adapter uses [grammY](https://grammy.dev/) and supports three conversation modes:

- Private chat with the bot directly
- Group chat via `@mention` — bot replies in a reply thread per conversation
- Channel + Discussion Group — each channel post automatically triggers an agent session in its comment thread; replies in the thread continue the same session

Typical startup flow:

1. Create a bot via [@BotFather](https://t.me/BotFather), copy the token.
2. Disable Privacy Mode: `/mybots` → Bot Settings → Group Privacy → Turn off.
3. For channel mode: link a Discussion Group to your channel (channel Edit → Discussion), then add the bot as admin to both the channel and the discussion group.
4. Configure `TELEGRAM_BOT_TOKEN` and optionally `TELEGRAM_ALLOWED_USER_IDS`.
5. Start `pnpm dev:telegram`.
Comment on lines +194 to +208
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add Telegram to the earlier platform overview too.

This section documents Telegram as a first-class adapter, but the earlier “Multiple IM platforms” copy and the “Supported IM Platforms” table still only list Discord and Feishu. Right now the README is internally inconsistent, and Telegram looks unsupported unless readers scroll this far.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 141 - 155, Update the README's earlier "Multiple IM
platforms" overview and the "Supported IM Platforms" table to include Telegram
as a first-class adapter: add Telegram (using grammY) to the list alongside
Discord and Feishu, mention its three conversation modes (private, group
`@mention`, channel+discussion group) and note the
TELEGRAM_BOT_TOKEN/TELEGRAM_ALLOWED_USER_IDS config and startup command (pnpm
dev:telegram) so the intro and the table are consistent with the existing
"Telegram Runtime" section.


## Install

The primary distribution path is npm:

```bash
npm install -g @doctorwu/agent-inbox

# Or run without a global install
npx @doctorwu/agent-inbox
```

On first run, `agent-inbox` creates `~/.agent-inbox/` as needed and enters the interactive setup flow automatically when no IM is configured yet. Users do not need to create `config.jsonl` by hand before the first `npx` run.

## Development

Expand All @@ -193,6 +239,7 @@ pnpm start
pnpm dev:discord
pnpm dev:feishu
pnpm dev:slack
pnpm dev:telegram
```

All adapter development commands load configuration from `~/.agent-inbox/config.jsonl`.
Expand Down
5 changes: 4 additions & 1 deletion apps/agent-inbox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
"access": "public"
},
"scripts": {
"build": "pnpm --filter @agent-im-relay/core build && pnpm --filter @agent-im-relay/discord build && pnpm --filter @agent-im-relay/feishu build && pnpm --filter @agent-im-relay/slack build && vp pack",
"build": "pnpm --filter @agent-im-relay/core build && pnpm --filter @agent-im-relay/discord build && pnpm --filter @agent-im-relay/feishu build && pnpm --filter @agent-im-relay/slack build && pnpm --filter @agent-im-relay/telegram build && vp pack",
"build:sea": "node ./scripts/build-executable.mjs",
"build:all": "pnpm run build && pnpm run build:sea",
"prepack": "pnpm run build",
"start": "node dist/index.mjs",
"test": "vitest run",
Expand All @@ -29,6 +31,7 @@
"@agent-im-relay/discord": "workspace:*",
"@agent-im-relay/feishu": "workspace:*",
"@agent-im-relay/slack": "workspace:*",
"@agent-im-relay/telegram": "workspace:*",
"@types/node": "^22.10.0",
"vitest": "^3.2.4"
},
Expand Down
25 changes: 24 additions & 1 deletion apps/agent-inbox/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export {
} from '@agent-im-relay/core';

export type {
AvailableIm,
DiscordImConfig,
DiscordImRecord,
FeishuImConfig,
Expand All @@ -26,3 +25,27 @@ export type {
SlackImConfig,
SlackImRecord,
} from '@agent-im-relay/core';

import type { AvailableIm as CoreAvailableIm } from '@agent-im-relay/core';

export type TelegramImConfig = {
botToken?: string;
allowedUserIds?: number[];
};

export type TelegramImRecord = {
type: 'im';
id: 'telegram';
enabled: boolean;
note?: string;
config: TelegramImConfig;
};

export type TelegramAvailableIm = {
id: 'telegram';
note?: string;
config: Required<Pick<TelegramImConfig, 'botToken'>> & Pick<TelegramImConfig, 'allowedUserIds'>;
};

export type AvailableIm = CoreAvailableIm | TelegramAvailableIm;

32 changes: 25 additions & 7 deletions apps/agent-inbox/src/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ type RuntimeLoaders = {
discord?: () => Promise<{ startDiscordRuntime: () => Promise<unknown> }>;
feishu?: () => Promise<{ startFeishuRuntime: () => Promise<unknown> }>;
slack?: () => Promise<{ startSlackRuntime: () => Promise<unknown> }>;
telegram?: () => Promise<{ startTelegramRuntime: () => Promise<unknown> }>;
};

function importRuntimeModule<T>(specifier: string): Promise<T> {
Expand Down Expand Up @@ -52,6 +53,8 @@ export function applyRuntimeEnvironment(
delete process.env['SLACK_APP_TOKEN'];
delete process.env['SLACK_SIGNING_SECRET'];
delete process.env['SLACK_SOCKET_MODE'];
delete process.env['TELEGRAM_BOT_TOKEN'];
delete process.env['TELEGRAM_ALLOWED_USER_IDS'];

if (selectedIm.id === 'discord') {
process.env['DISCORD_TOKEN'] = selectedIm.config.token;
Expand All @@ -72,10 +75,18 @@ export function applyRuntimeEnvironment(
return;
}

process.env['SLACK_BOT_TOKEN'] = selectedIm.config.botToken;
process.env['SLACK_APP_TOKEN'] = selectedIm.config.appToken;
process.env['SLACK_SIGNING_SECRET'] = selectedIm.config.signingSecret;
setOptionalEnv('SLACK_SOCKET_MODE', String(selectedIm.config.socketMode ?? true));
if (selectedIm.id === 'slack') {
process.env['SLACK_BOT_TOKEN'] = selectedIm.config.botToken;
process.env['SLACK_APP_TOKEN'] = selectedIm.config.appToken;
process.env['SLACK_SIGNING_SECRET'] = selectedIm.config.signingSecret;
setOptionalEnv('SLACK_SOCKET_MODE', String(selectedIm.config.socketMode ?? true));
return;
}

if (selectedIm.id === 'telegram') {
process.env['TELEGRAM_BOT_TOKEN'] = selectedIm.config.botToken;
setOptionalEnv('TELEGRAM_ALLOWED_USER_IDS', selectedIm.config.allowedUserIds?.join(','));
}
}

export async function startSelectedIm(
Expand All @@ -100,7 +111,14 @@ export async function startSelectedIm(
return;
}

const loadSlack = loaders.slack ?? (() => importRuntimeModule<{ startSlackRuntime: () => Promise<unknown> }>('@agent-im-relay/slack'));
const slack = await loadSlack();
await slack.startSlackRuntime();
if (selectedIm.id === 'slack') {
const loadSlack = loaders.slack ?? (() => importRuntimeModule<{ startSlackRuntime: () => Promise<unknown> }>('@agent-im-relay/slack'));
const slack = await loadSlack();
await slack.startSlackRuntime();
return;
}

const loadTelegram = loaders.telegram ?? (() => importRuntimeModule<{ startTelegramRuntime: () => Promise<unknown> }>('@agent-im-relay/telegram'));
const telegram = await loadTelegram();
await telegram.startTelegramRuntime();
}
49 changes: 46 additions & 3 deletions apps/agent-inbox/src/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ import type {
SlackImRecord,
LoadedAppConfig,
} from './config';
import type { TelegramImRecord } from './config';
import { loadAppConfig, saveAppConfig, upsertRecord } from './config';

const ALL_PLATFORM_IDS = ['discord', 'feishu', 'slack'] as const;
const ALL_PLATFORM_IDS = ['discord', 'feishu', 'slack', 'telegram'] as const;
type PlatformId = (typeof ALL_PLATFORM_IDS)[number];

const PLATFORM_LABELS: Record<PlatformId, string> = {
discord: 'Discord (Recommended)',
feishu: 'Feishu (飞书)',
slack: 'Slack',
telegram: 'Telegram',
};

const PLATFORM_HINTS: Partial<Record<PlatformId, string>> = {
Expand Down Expand Up @@ -185,6 +187,45 @@ async function buildSlackRecord(): Promise<SlackImRecord> {
};
}

async function buildTelegramRecord(): Promise<TelegramImRecord> {
const result = await p.group(
{
botToken: () =>
p.password({
message: 'Telegram bot token (from @BotFather)',
validate: v => (!v || v.length === 0 ? 'Required' : undefined),
}),
allowedUserIds: () =>
p.text({
message: 'Allowed user IDs (comma-separated, optional)',
placeholder: 'Leave empty to allow all users',
}),
},
{
onCancel: () => {
p.cancel('Setup cancelled.');
process.exit(0);
},
},
);

const rawIds = result.allowedUserIds?.trim();
const allowedUserIds = rawIds
? rawIds.split(',').map(s => Number.parseInt(s.trim(), 10)).filter(n => Number.isFinite(n) && n > 0)
: undefined;

return {
type: 'im',
id: 'telegram',
enabled: true,
note: 'Telegram bot',
config: {
botToken: result.botToken,
allowedUserIds: allowedUserIds?.length ? allowedUserIds : undefined,
},
};
}

export async function runSetup(
paths: RelayPaths,
unconfiguredPlatforms: PlatformId[],
Expand Down Expand Up @@ -218,11 +259,13 @@ export async function runSetup(
? await buildDiscordRecord()
: platformId === 'feishu'
? await buildFeishuRecord()
: await buildSlackRecord();
: platformId === 'slack'
? await buildSlackRecord()
: await buildTelegramRecord();

const nextRecords = upsertRecord(
current.records as AppConfigRecord[],
nextRecord,
nextRecord as AppConfigRecord,
);
await saveAppConfig(paths, nextRecords);

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"start": "pnpm --filter ./apps/agent-inbox start",
"dev:discord": "pnpm --filter @agent-im-relay/discord dev",
"dev:feishu": "pnpm --filter @agent-im-relay/feishu dev",
"dev:slack": "pnpm --filter @agent-im-relay/slack dev"
"dev:slack": "pnpm --filter @agent-im-relay/slack dev",
"dev:telegram": "pnpm --filter @agent-im-relay/telegram dev"
},
"devDependencies": {
"@types/node": "^22.10.0",
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/relay-platform.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const relayPlatforms = ['discord', 'feishu', 'slack'] as const;
export const relayPlatforms = ['discord', 'feishu', 'slack', 'telegram'] as const;

export type RelayPlatform = (typeof relayPlatforms)[number];

Expand All @@ -7,6 +7,8 @@ export function isRelayPlatform(value: unknown): value is RelayPlatform {
}

export function inferRelayPlatformFromConversationId(conversationId: string): RelayPlatform {
if (conversationId.startsWith('tg-')) return 'telegram';

if (/^\d+$/.test(conversationId)) {
return 'discord';
}
Expand Down
31 changes: 31 additions & 0 deletions packages/telegram/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "@agent-im-relay/telegram",
"version": "1.0.0",
"type": "module",
"main": "dist/index.mjs",
"types": "dist/index.d.mts",
"exports": {
".": {
"import": "./dist/index.mjs",
"types": "./dist/index.d.mts"
}
},
"scripts": {
"build": "tsdown",
"dev": "tsx watch src/index.ts",
"start": "node dist/index.mjs",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@agent-im-relay/core": "workspace:*",
"dotenv": "^16.4.7",
"grammy": "^1"
},
"devDependencies": {
"@types/node": "^22.10.0",
"tsdown": "^0.21.0",
"tsx": "^4.19.2",
"vitest": "^3.2.4"
}
}
Loading