From 965fc929e1185f86cac7e349176a2c8ab6ab5af9 Mon Sep 17 00:00:00 2001 From: Rdmclin2 Date: Tue, 31 Mar 2026 00:26:32 +0800 Subject: [PATCH] feat: add unified messaging tool for cross-platform communication (#13296) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ feat: add cross-platform message tool for AI bot channel operations Implement a unified message tool (`lobe-message`) that provides AI with messaging capabilities across Discord, Telegram, Slack, Google Chat, and IRC through a single interface with platform-specific extensions. Core APIs: sendMessage, readMessages, editMessage, deleteMessage, searchMessages, reactToMessage, getReactions, pin/unpin management, channel/member info, thread operations, and polls. Architecture follows the established builtin-tool pattern: - Package: @lobechat/builtin-tool-message (manifest, types, executor, ExecutionRuntime, client components) - Registry: registered in builtin-tools (renders, inspectors, interventions, streamings) - Server runtime: stub service ready for platform adapter integration https://claude.ai/code/session_011sHc6R7V4cSYKere9RY1QM * feat: implement platform specific message service * chore: add wechat platform * chore: update wechat api service * chore: update protocol implementation * chore: optimize platform api test * fix: lark domain error * feat: support bot message cli * chore: refactor adapter to service * chore: optimize bot status fetch * fix: bot status * fix: channel nav ignore * feat: message tool support bot manage * feat: add lobe-message runtime * feat: support direct message * feat: add history limit * chore: update const limit * feat: optimize server id message history limit * chore: optimize system role & inject platform environment info * chore: update readMessages vibe * fix: form body width 50% * chore: optimize tool prompt * chore: update i18n files * chore: optimize read message system role and update bot message lh * updage readMessage api rate limit * chore: comatible for readMessages * fix: feishu readMessage implementation error * fix: test case * chore: update i18n files * fix: lint error * chore: add timeout for conversaction case * fix: message test case * fix: vite gzip error --------- Co-authored-by: Claude --- apps/cli/README.md | 11 + apps/cli/src/commands/bot.ts | 341 ++-- apps/cli/src/commands/botMessage.ts | 564 +++++++ e2e/src/steps/agent/conversation.steps.ts | 2 +- locales/ar/agent.json | 6 + locales/ar/chat.json | 4 + locales/ar/models.json | 25 +- locales/ar/onboarding.json | 35 + locales/ar/plugin.json | 16 + locales/ar/providers.json | 1 - locales/ar/ui.json | 3 + locales/bg-BG/agent.json | 6 + locales/bg-BG/chat.json | 4 + locales/bg-BG/models.json | 29 +- locales/bg-BG/onboarding.json | 35 + locales/bg-BG/plugin.json | 16 + locales/bg-BG/providers.json | 1 - locales/bg-BG/ui.json | 3 + locales/de-DE/agent.json | 6 + locales/de-DE/chat.json | 4 + locales/de-DE/models.json | 25 +- locales/de-DE/onboarding.json | 35 + locales/de-DE/plugin.json | 16 + locales/de-DE/providers.json | 1 - locales/de-DE/ui.json | 3 + locales/en-US/agent.json | 6 + locales/en-US/models.json | 33 +- locales/en-US/onboarding.json | 30 + locales/en-US/plugin.json | 16 + locales/en-US/providers.json | 1 - locales/es-ES/agent.json | 6 + locales/es-ES/chat.json | 4 + locales/es-ES/models.json | 23 +- locales/es-ES/onboarding.json | 35 + locales/es-ES/plugin.json | 16 + locales/es-ES/providers.json | 1 - locales/es-ES/ui.json | 3 + locales/fa-IR/agent.json | 6 + locales/fa-IR/chat.json | 4 + locales/fa-IR/models.json | 33 +- locales/fa-IR/onboarding.json | 35 + locales/fa-IR/plugin.json | 16 + locales/fa-IR/providers.json | 1 - locales/fa-IR/ui.json | 3 + locales/fr-FR/agent.json | 6 + locales/fr-FR/chat.json | 4 + locales/fr-FR/models.json | 25 +- locales/fr-FR/onboarding.json | 35 + locales/fr-FR/plugin.json | 16 + locales/fr-FR/providers.json | 1 - locales/fr-FR/ui.json | 3 + locales/it-IT/agent.json | 6 + locales/it-IT/chat.json | 4 + locales/it-IT/models.json | 27 +- locales/it-IT/onboarding.json | 35 + locales/it-IT/plugin.json | 16 + locales/it-IT/providers.json | 1 - locales/it-IT/ui.json | 3 + locales/ja-JP/agent.json | 6 + locales/ja-JP/chat.json | 4 + locales/ja-JP/models.json | 33 +- locales/ja-JP/onboarding.json | 35 + locales/ja-JP/plugin.json | 16 + locales/ja-JP/providers.json | 1 - locales/ja-JP/ui.json | 3 + locales/ko-KR/agent.json | 6 + locales/ko-KR/chat.json | 4 + locales/ko-KR/models.json | 33 +- locales/ko-KR/onboarding.json | 35 + locales/ko-KR/plugin.json | 16 + locales/ko-KR/providers.json | 1 - locales/ko-KR/ui.json | 3 + locales/nl-NL/agent.json | 6 + locales/nl-NL/chat.json | 4 + locales/nl-NL/models.json | 33 +- locales/nl-NL/onboarding.json | 35 + locales/nl-NL/plugin.json | 16 + locales/nl-NL/providers.json | 1 - locales/nl-NL/ui.json | 3 + locales/pl-PL/agent.json | 6 + locales/pl-PL/chat.json | 4 + locales/pl-PL/models.json | 33 +- locales/pl-PL/onboarding.json | 35 + locales/pl-PL/plugin.json | 16 + locales/pl-PL/providers.json | 1 - locales/pl-PL/ui.json | 3 + locales/pt-BR/agent.json | 6 + locales/pt-BR/chat.json | 4 + locales/pt-BR/models.json | 25 +- locales/pt-BR/onboarding.json | 35 + locales/pt-BR/plugin.json | 16 + locales/pt-BR/providers.json | 1 - locales/pt-BR/ui.json | 3 + locales/ru-RU/agent.json | 6 + locales/ru-RU/chat.json | 4 + locales/ru-RU/models.json | 33 +- locales/ru-RU/onboarding.json | 35 + locales/ru-RU/plugin.json | 16 + locales/ru-RU/providers.json | 1 - locales/ru-RU/ui.json | 3 + locales/tr-TR/agent.json | 6 + locales/tr-TR/chat.json | 4 + locales/tr-TR/models.json | 31 +- locales/tr-TR/onboarding.json | 35 + locales/tr-TR/plugin.json | 16 + locales/tr-TR/providers.json | 1 - locales/tr-TR/ui.json | 3 + locales/vi-VN/agent.json | 6 + locales/vi-VN/chat.json | 4 + locales/vi-VN/models.json | 29 +- locales/vi-VN/onboarding.json | 35 + locales/vi-VN/plugin.json | 16 + locales/vi-VN/providers.json | 1 - locales/vi-VN/ui.json | 3 + locales/zh-CN/agent.json | 6 + locales/zh-CN/models.json | 33 +- locales/zh-CN/onboarding.json | 39 +- locales/zh-CN/plugin.json | 23 +- locales/zh-CN/providers.json | 1 - locales/zh-TW/agent.json | 6 + locales/zh-TW/chat.json | 4 + locales/zh-TW/models.json | 29 +- locales/zh-TW/onboarding.json | 35 + locales/zh-TW/plugin.json | 16 + locales/zh-TW/providers.json | 1 - locales/zh-TW/ui.json | 3 + package.json | 1 + .../src/lobehub/references/bot.ts | 21 + packages/builtin-tool-message/package.json | 23 + .../src/ExecutionRuntime/index.ts | 690 +++++++++ .../src/client/Inspector/index.ts | 7 + .../src/client/Intervention/index.ts | 2 + .../src/client/Render/index.ts | 7 + .../src/client/Streaming/index.ts | 2 + .../builtin-tool-message/src/client/index.ts | 15 + .../src/executor/index.ts | 247 +++ packages/builtin-tool-message/src/index.ts | 9 + packages/builtin-tool-message/src/manifest.ts | 685 +++++++++ .../builtin-tool-message/src/systemRole.ts | 87 ++ packages/builtin-tool-message/src/types.ts | 486 ++++++ packages/builtin-tools/package.json | 1 + packages/builtin-tools/src/index.ts | 6 + packages/builtin-tools/src/inspectors.ts | 2 + packages/builtin-tools/src/interventions.ts | 2 + packages/builtin-tools/src/renders.ts | 2 + packages/builtin-tools/src/streamings.ts | 2 + packages/const/src/bot.ts | 18 + packages/const/src/index.ts | 1 + packages/const/src/recommendedSkill.ts | 1 + .../src/prompts/botPlatformContext/index.ts | 41 +- src/locales/default/agent.ts | 8 + .../agent/_layout/Sidebar/Header/Nav.tsx | 6 +- .../(main)/agent/channel/detail/Body.tsx | 5 +- src/routes/(main)/agent/channel/index.tsx | 17 +- src/server/routers/lambda/agentBotProvider.ts | 28 +- src/server/routers/lambda/botMessage.ts | 475 ++++++ src/server/routers/lambda/index.ts | 2 + src/server/services/bot/AgentBridgeService.ts | 8 +- src/server/services/bot/BotMessageRouter.ts | 4 +- src/server/services/bot/platforms/const.ts | 18 +- .../services/bot/platforms/discord/api.ts | 176 +++ .../services/bot/platforms/discord/const.ts | 2 + .../bot/platforms/discord/protocol-spec.md | 1354 ++++++++++++++++ .../services/bot/platforms/discord/schema.ts | 25 +- .../services/bot/platforms/discord/service.ts | 276 ++++ .../services/bot/platforms/feishu/client.ts | 14 +- .../services/bot/platforms/feishu/const.ts | 2 + .../platforms/feishu/definitions/schema.ts | 24 +- .../bot/platforms/feishu/protocol-spec.md | 1290 ++++++++++++++++ .../services/bot/platforms/feishu/service.ts | 198 +++ .../bot/platforms/qq/protocol-spec.md | 1274 +++++++++++++++ .../services/bot/platforms/qq/schema.ts | 7 +- .../services/bot/platforms/qq/service.ts | 163 ++ .../services/bot/platforms/registry.test.ts | 2 +- src/server/services/bot/platforms/registry.ts | 7 +- .../services/bot/platforms/slack/api.ts | 120 +- .../services/bot/platforms/slack/const.ts | 2 + .../bot/platforms/slack/protocol-spec.md | 1365 +++++++++++++++++ .../services/bot/platforms/slack/schema.ts | 25 +- .../services/bot/platforms/slack/service.ts | 221 +++ .../services/bot/platforms/telegram/api.ts | 89 ++ .../bot/platforms/telegram/protocol-spec.md | 1230 +++++++++++++++ .../services/bot/platforms/telegram/schema.ts | 9 +- .../bot/platforms/telegram/service.ts | 180 +++ src/server/services/bot/platforms/types.ts | 1 + .../services/bot/platforms/wechat/schema.ts | 5 +- .../services/bot/platforms/wechat/service.ts | 169 ++ .../serverRuntimes/__tests__/message.test.ts | 356 +++++ .../toolExecution/serverRuntimes/index.ts | 2 + .../message/MessageDispatcherService.ts | 154 ++ .../message/PlatformUnsupportedError.ts | 9 + .../serverRuntimes/message/adapters/types.ts | 6 + .../serverRuntimes/message/index.ts | 172 +++ src/services/agentBotProvider.ts | 4 - src/store/agent/slices/bot/action.ts | 37 +- .../tool/slices/builtin/executors/index.ts | 2 + .../slices/builtin/executors/lobe-message.ts | 328 ++++ vite.config.ts | 1 + 198 files changed, 14293 insertions(+), 549 deletions(-) create mode 100644 apps/cli/src/commands/botMessage.ts create mode 100644 packages/builtin-tool-message/package.json create mode 100644 packages/builtin-tool-message/src/ExecutionRuntime/index.ts create mode 100644 packages/builtin-tool-message/src/client/Inspector/index.ts create mode 100644 packages/builtin-tool-message/src/client/Intervention/index.ts create mode 100644 packages/builtin-tool-message/src/client/Render/index.ts create mode 100644 packages/builtin-tool-message/src/client/Streaming/index.ts create mode 100644 packages/builtin-tool-message/src/client/index.ts create mode 100644 packages/builtin-tool-message/src/executor/index.ts create mode 100644 packages/builtin-tool-message/src/index.ts create mode 100644 packages/builtin-tool-message/src/manifest.ts create mode 100644 packages/builtin-tool-message/src/systemRole.ts create mode 100644 packages/builtin-tool-message/src/types.ts create mode 100644 packages/const/src/bot.ts create mode 100644 src/server/routers/lambda/botMessage.ts create mode 100644 src/server/services/bot/platforms/discord/const.ts create mode 100644 src/server/services/bot/platforms/discord/protocol-spec.md create mode 100644 src/server/services/bot/platforms/discord/service.ts create mode 100644 src/server/services/bot/platforms/feishu/const.ts create mode 100644 src/server/services/bot/platforms/feishu/protocol-spec.md create mode 100644 src/server/services/bot/platforms/feishu/service.ts create mode 100644 src/server/services/bot/platforms/qq/protocol-spec.md create mode 100644 src/server/services/bot/platforms/qq/service.ts create mode 100644 src/server/services/bot/platforms/slack/const.ts create mode 100644 src/server/services/bot/platforms/slack/protocol-spec.md create mode 100644 src/server/services/bot/platforms/slack/service.ts create mode 100644 src/server/services/bot/platforms/telegram/protocol-spec.md create mode 100644 src/server/services/bot/platforms/telegram/service.ts create mode 100644 src/server/services/bot/platforms/wechat/service.ts create mode 100644 src/server/services/toolExecution/serverRuntimes/__tests__/message.test.ts create mode 100644 src/server/services/toolExecution/serverRuntimes/message/MessageDispatcherService.ts create mode 100644 src/server/services/toolExecution/serverRuntimes/message/PlatformUnsupportedError.ts create mode 100644 src/server/services/toolExecution/serverRuntimes/message/adapters/types.ts create mode 100644 src/server/services/toolExecution/serverRuntimes/message/index.ts create mode 100644 src/store/tool/slices/builtin/executors/lobe-message.ts diff --git a/apps/cli/README.md b/apps/cli/README.md index 2243c402af..eb6cc0fdb8 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -15,6 +15,17 @@ LobeHub command-line interface. - To make `lh` available in your shell, run `bun run cli:link`. - After linking, if your shell still cannot find `lh`, run `rehash` in `zsh`. +## Custom Server URL + +By default the CLI connects to `https://app.lobehub.com`. To point it at a different server (e.g. a local instance): + +| Method | Command | Persistence | +| -------------------- | --------------------------------------------------------------- | ----------------------------------- | +| Environment variable | `LOBEHUB_SERVER=http://localhost:4000 bun run dev -- ` | Current command only | +| Login flag | `lh login --server http://localhost:4000` | Saved to `~/.lobehub/settings.json` | + +Priority: `LOBEHUB_SERVER` env var > `settings.json` > default official URL. + ## Shell Completion ### Install completion for a linked CLI diff --git a/apps/cli/src/commands/bot.ts b/apps/cli/src/commands/bot.ts index d0ada779f8..802ccd2fe3 100644 --- a/apps/cli/src/commands/bot.ts +++ b/apps/cli/src/commands/bot.ts @@ -1,39 +1,130 @@ import type { Command } from 'commander'; import pc from 'picocolors'; +import type { TrpcClient } from '../api/client'; import { getTrpcClient } from '../api/client'; -import { confirm, outputJson, printTable } from '../utils/format'; +import { confirm, outputJson, printBoxTable, printTable, timeAgo } from '../utils/format'; import { log } from '../utils/logger'; +import { registerBotMessageCommands } from './botMessage'; -const SUPPORTED_PLATFORMS = ['discord', 'slack', 'telegram', 'lark', 'feishu', 'wechat']; +// ── Helpers ────────────────────────────────────────────── -const PLATFORM_CREDENTIAL_FIELDS: Record = { - discord: ['botToken', 'publicKey'], - feishu: ['appSecret'], - lark: ['appSecret'], - slack: ['botToken', 'signingSecret'], - telegram: ['botToken'], - wechat: ['botToken', 'botId'], +function maskValue(val: string): string { + if (val.length > 8) return val.slice(0, 4) + '****' + val.slice(-4); + return '****'; +} + +function camelToFlag(name: string): string { + return '--' + name.replaceAll(/([A-Z])/g, '-$1').toLowerCase(); +} + +/** Extract credential field definitions from a platform schema. */ +function getCredentialFields(platformDef: any): any[] { + const credSchema = (platformDef.schema ?? []).find( + (f: any) => f.key === 'credentials' && f.properties, + ); + return credSchema?.properties ?? []; +} + +/** Extract credential values from CLI options based on platform schema. */ +function extractCredentials( + platformDef: any, + options: Record, +): { credentials: Record; missing: any[] } { + const fields = getCredentialFields(platformDef); + const credentials: Record = {}; + + for (const field of fields) { + const value = options[field.key]; + if (typeof value === 'string') { + credentials[field.key] = value; + } + } + + const missing = fields.filter((f: any) => f.required && !credentials[f.key]); + return { credentials, missing }; +} + +/** Find a bot by ID from the user's bot list. */ +async function findBot(client: TrpcClient, botId: string) { + const bots = await client.agentBotProvider.list.query(); + const bot = (bots as any[]).find((b: any) => b.id === botId); + if (!bot) { + log.error(`Bot integration not found: ${botId}`); + process.exit(1); + } + return bot; +} + +const STATUS_COLORS: Record string> = { + connected: pc.green, + disconnected: pc.dim, + failed: pc.red, + queued: pc.yellow, + starting: pc.yellow, + unknown: pc.dim, }; -function parseCredentials( - platform: string, - options: Record, -): Record { - const creds: Record = {}; - - if (options.botToken) creds.botToken = options.botToken; - if (options.botId) creds.botId = options.botId; - if (options.publicKey) creds.publicKey = options.publicKey; - if (options.signingSecret) creds.signingSecret = options.signingSecret; - if (options.appSecret) creds.appSecret = options.appSecret; - - return creds; +/** Validate a platform ID and return its definition. */ +async function resolvePlatform(client: TrpcClient, platformId: string) { + const platforms = await client.agentBotProvider.listPlatforms.query(); + const def = (platforms as any[]).find((p: any) => p.id === platformId); + if (!def) { + const ids = (platforms as any[]).map((p: any) => p.id).join(', '); + log.error(`Invalid platform "${platformId}". Must be one of: ${ids}`); + log.info('Run `lh bot platforms` to see required credentials for each platform.'); + process.exit(1); + } + return def; } +// ── Command Registration ───────────────────────────────── + export function registerBotCommand(program: Command) { const bot = program.command('bot').description('Manage bot integrations'); + // Register message subcommand group + registerBotMessageCommands(bot); + + // ── platforms ─────────────────────────────────────────── + + bot + .command('platforms') + .description('List supported platforms and their required credentials') + .option('--json', 'Output JSON') + .action(async (options: { json?: boolean }) => { + const client = await getTrpcClient(); + const platforms = await client.agentBotProvider.listPlatforms.query(); + + if (options.json) { + outputJson(platforms); + return; + } + + console.log(pc.bold('Supported platforms:\n')); + + for (const p of platforms as any[]) { + console.log(` ${pc.bold(pc.cyan(p.id))}`); + if (p.name) console.log(` Name: ${p.name}`); + + const fields = getCredentialFields(p); + const required = fields.filter((f: any) => f.required); + const optional = fields.filter((f: any) => !f.required); + + if (required.length > 0) { + console.log( + ` Required: ${required.map((f: any) => pc.yellow(camelToFlag(f.key))).join(', ')}`, + ); + } + if (optional.length > 0) { + console.log( + ` Optional: ${optional.map((f: any) => pc.dim(camelToFlag(f.key))).join(', ')}`, + ); + } + console.log(); + } + }); + // ── list ────────────────────────────────────────────── bot @@ -63,15 +154,20 @@ export function registerBotCommand(program: Command) { return; } - const rows = items.map((b: any) => [ - b.id || '', - b.platform || '', - b.applicationId || '', - b.agentId || '', - b.enabled ? pc.green('enabled') : pc.dim('disabled'), - ]); + const rows = items.map((b: any) => { + const status = b.enabled ? (b.runtimeStatus ?? 'disconnected') : 'disabled'; + const colorFn = STATUS_COLORS[status] ?? pc.dim; + return [ + b.id || '', + b.platform || '', + b.applicationId || '', + b.agentId || '', + colorFn(status), + b.updatedAt ? timeAgo(b.updatedAt) : pc.dim('-'), + ]; + }); - printTable(rows, ['ID', 'PLATFORM', 'APP ID', 'AGENT', 'STATUS']); + printTable(rows, ['ID', 'PLATFORM', 'APP ID', 'AGENT', 'STATUS', 'UPDATED']); }); // ── view ────────────────────────────────────────────── @@ -79,44 +175,62 @@ export function registerBotCommand(program: Command) { bot .command('view ') .description('View bot integration details') - .requiredOption('-a, --agent ', 'Agent ID') .option('--json [fields]', 'Output JSON, optionally specify fields (comma-separated)') - .action(async (botId: string, options: { agent: string; json?: string | boolean }) => { - const client = await getTrpcClient(); - const result = await client.agentBotProvider.getByAgentId.query({ - agentId: options.agent, - }); - const items = Array.isArray(result) ? result : []; - const item = items.find((b: any) => b.id === botId); + .option('--show-credentials', 'Show full credential values (unmasked)') + .action( + async (botId: string, options: { json?: string | boolean; showCredentials?: boolean }) => { + const client = await getTrpcClient(); + const b = await findBot(client, botId); - if (!item) { - log.error(`Bot integration not found: ${botId}`); - process.exit(1); - return; - } - - if (options.json !== undefined) { - const fields = typeof options.json === 'string' ? options.json : undefined; - outputJson(item, fields); - return; - } - - const b = item as any; - console.log(pc.bold(`${b.platform} bot`)); - console.log(pc.dim(`ID: ${b.id}`)); - console.log(`Application ID: ${b.applicationId}`); - console.log(`Status: ${b.enabled ? pc.green('enabled') : pc.dim('disabled')}`); - - if (b.credentials && typeof b.credentials === 'object') { - console.log(); - console.log(pc.bold('Credentials:')); - for (const [key, value] of Object.entries(b.credentials)) { - const val = String(value); - const masked = val.length > 8 ? val.slice(0, 4) + '****' + val.slice(-4) : '****'; - console.log(` ${key}: ${masked}`); + if (options.json !== undefined) { + const fields = typeof options.json === 'string' ? options.json : undefined; + outputJson(b, fields); + return; } - } - }); + + const status = b.enabled ? (b.runtimeStatus ?? 'disconnected') : 'disabled'; + const statusColorFn = STATUS_COLORS[status] ?? pc.dim; + + const credentialLines: string[] = []; + if (b.credentials && typeof b.credentials === 'object') { + for (const [key, value] of Object.entries(b.credentials)) { + const val = String(value); + const display = options.showCredentials ? val : maskValue(val); + credentialLines.push(`${pc.dim(key)}: ${display}`); + } + } + + const settingsLines: string[] = []; + if (b.settings && typeof b.settings === 'object') { + for (const [key, value] of Object.entries(b.settings)) { + settingsLines.push(`${pc.dim(key)}: ${JSON.stringify(value)}`); + } + } + + printBoxTable( + [ + { header: 'Field', key: 'field' }, + { header: 'Value', key: 'value' }, + ], + [ + { field: 'ID', value: b.id || '' }, + { field: 'Platform', value: pc.cyan(b.platform || '') }, + { field: 'Application ID', value: b.applicationId || '' }, + { field: 'Agent ID', value: b.agentId || '' }, + { field: 'Status', value: statusColorFn(status) }, + ...(credentialLines.length > 0 + ? [{ field: 'Credentials', value: credentialLines }] + : []), + ...(settingsLines.length > 0 ? [{ field: 'Settings', value: settingsLines }] : []), + ...(b.createdAt + ? [{ field: 'Created', value: new Date(b.createdAt).toLocaleString() }] + : []), + ...(b.updatedAt ? [{ field: 'Updated', value: timeAgo(b.updatedAt) }] : []), + ], + `${b.platform} bot`, + ); + }, + ); // ── add ─────────────────────────────────────────────── @@ -124,13 +238,18 @@ export function registerBotCommand(program: Command) { .command('add') .description('Add a bot integration to an agent') .requiredOption('-a, --agent ', 'Agent ID') - .requiredOption('--platform ', `Platform: ${SUPPORTED_PLATFORMS.join(', ')}`) + .requiredOption('--platform ', 'Platform (run `lh bot platforms` to see options)') .requiredOption('--app-id ', 'Application ID for webhook routing') - .option('--bot-token ', 'Bot token') + .option('--bot-token ', 'Bot token (Discord, Slack, Telegram)') .option('--bot-id ', 'Bot ID (WeChat)') .option('--public-key ', 'Public key (Discord)') .option('--signing-secret ', 'Signing secret (Slack)') - .option('--app-secret ', 'App secret (Lark/Feishu)') + .option('--app-secret ', 'App secret (Lark, Feishu, QQ)') + .option('--secret-token ', 'Secret token (Telegram)') + .option('--webhook-proxy-url ', 'Webhook proxy URL (Telegram)') + .option('--encrypt-key ', 'Encrypt key (Feishu)') + .option('--verification-token ', 'Verification token (Feishu)') + .option('--json', 'Output created bot as JSON') .action( async (options: { agent: string; @@ -138,34 +257,39 @@ export function registerBotCommand(program: Command) { appSecret?: string; botId?: string; botToken?: string; + encryptKey?: string; + json?: boolean; platform: string; publicKey?: string; + secretToken?: string; signingSecret?: string; + verificationToken?: string; + webhookProxyUrl?: string; }) => { - if (!SUPPORTED_PLATFORMS.includes(options.platform)) { - log.error(`Invalid platform. Must be one of: ${SUPPORTED_PLATFORMS.join(', ')}`); - process.exit(1); - return; - } + const client = await getTrpcClient(); + const platformDef = await resolvePlatform(client, options.platform); - const credentials = parseCredentials(options.platform, options); - const requiredFields = PLATFORM_CREDENTIAL_FIELDS[options.platform] || []; - const missing = requiredFields.filter((f) => !credentials[f]); + const { credentials, missing } = extractCredentials(platformDef, options); if (missing.length > 0) { log.error( - `Missing required credentials for ${options.platform}: ${missing.map((f) => '--' + f.replaceAll(/([A-Z])/g, '-$1').toLowerCase()).join(', ')}`, + `Missing required credentials for ${options.platform}: ${missing.map((f: any) => camelToFlag(f.key)).join(', ')}`, ); process.exit(1); return; } - const client = await getTrpcClient(); const result = await client.agentBotProvider.create.mutate({ agentId: options.agent, applicationId: options.appId, credentials, platform: options.platform, }); + + if (options.json) { + outputJson(result); + return; + } + const r = result as any; console.log( `${pc.green('✓')} Added ${pc.bold(options.platform)} bot ${pc.bold(r.id || '')}`, @@ -183,6 +307,10 @@ export function registerBotCommand(program: Command) { .option('--public-key ', 'New public key') .option('--signing-secret ', 'New signing secret') .option('--app-secret ', 'New app secret') + .option('--secret-token ', 'New secret token') + .option('--webhook-proxy-url ', 'New webhook proxy URL') + .option('--encrypt-key ', 'New encrypt key') + .option('--verification-token ', 'New verification token') .option('--app-id ', 'New application ID') .option('--platform ', 'New platform') .action( @@ -193,20 +321,23 @@ export function registerBotCommand(program: Command) { appSecret?: string; botId?: string; botToken?: string; + encryptKey?: string; platform?: string; publicKey?: string; + secretToken?: string; signingSecret?: string; + verificationToken?: string; + webhookProxyUrl?: string; }, ) => { + const client = await getTrpcClient(); const input: Record = { id: botId }; - const credentials: Record = {}; - if (options.botToken) credentials.botToken = options.botToken; - if (options.botId) credentials.botId = options.botId; - if (options.publicKey) credentials.publicKey = options.publicKey; - if (options.signingSecret) credentials.signingSecret = options.signingSecret; - if (options.appSecret) credentials.appSecret = options.appSecret; + const existing = await findBot(client, botId); + const platform = options.platform ?? existing.platform; + const platformDef = await resolvePlatform(client, platform); + const { credentials } = extractCredentials(platformDef, options); if (Object.keys(credentials).length > 0) input.credentials = credentials; if (options.appId) input.applicationId = options.appId; if (options.platform) input.platform = options.platform; @@ -217,7 +348,6 @@ export function registerBotCommand(program: Command) { return; } - const client = await getTrpcClient(); await client.agentBotProvider.update.mutate(input as any); console.log(`${pc.green('✓')} Updated bot ${pc.bold(botId)}`); }, @@ -263,28 +393,41 @@ export function registerBotCommand(program: Command) { console.log(`${pc.green('✓')} Disabled bot ${pc.bold(botId)}`); }); + // ── test ─────────────────────────────────────────────── + + bot + .command('test ') + .description('Test bot credentials against the platform API') + .action(async (botId: string) => { + const client = await getTrpcClient(); + const b = await findBot(client, botId); + + log.status(`Testing ${b.platform} credentials for ${b.applicationId}...`); + + try { + await client.agentBotProvider.testConnection.mutate({ + applicationId: b.applicationId, + platform: b.platform, + }); + console.log(`${pc.green('✓')} Credentials are valid for ${pc.bold(b.platform)} bot`); + } catch (err: any) { + const message = err?.message || 'Connection test failed'; + log.error(`Credential test failed: ${message}`); + process.exit(1); + } + }); + // ── connect ─────────────────────────────────────────── bot .command('connect ') .description('Connect and start a bot') - .requiredOption('-a, --agent ', 'Agent ID') - .action(async (botId: string, options: { agent: string }) => { - // First fetch the bot to get platform and applicationId + .action(async (botId: string) => { const client = await getTrpcClient(); - const result = await client.agentBotProvider.getByAgentId.query({ - agentId: options.agent, - }); - const items = Array.isArray(result) ? result : []; - const item = items.find((b: any) => b.id === botId); + const b = await findBot(client, botId); - if (!item) { - log.error(`Bot integration not found: ${botId}`); - process.exit(1); - return; - } + log.status(`Connecting ${b.platform} bot ${b.applicationId}...`); - const b = item as any; const connectResult = await client.agentBotProvider.connectBot.mutate({ applicationId: b.applicationId, platform: b.platform, diff --git a/apps/cli/src/commands/botMessage.ts b/apps/cli/src/commands/botMessage.ts new file mode 100644 index 0000000000..d4285803ff --- /dev/null +++ b/apps/cli/src/commands/botMessage.ts @@ -0,0 +1,564 @@ +import { DEFAULT_BOT_HISTORY_LIMIT } from '@lobechat/const'; +import type { Command } from 'commander'; +import pc from 'picocolors'; + +import { getTrpcClient } from '../api/client'; +import { confirm, outputJson, printTable, truncate } from '../utils/format'; +import { log } from '../utils/logger'; + +export function registerBotMessageCommands(bot: Command) { + const message = bot + .command('message') + .description('Send and manage messages on connected platforms'); + + // ── send ──────────────────────────────────────────────── + + message + .command('send ') + .description('Send a message to a channel') + .requiredOption('--target ', 'Target channel / conversation ID') + .requiredOption('--message ', 'Message content') + .option('--reply-to ', 'Reply to a specific message') + .option('--json', 'Output JSON') + .action( + async ( + botId: string, + options: { json?: boolean; message: string; replyTo?: string; target: string }, + ) => { + const client = await getTrpcClient(); + const result = await client.botMessage.sendMessage.mutate({ + botId, + channelId: options.target, + content: options.message, + replyTo: options.replyTo, + }); + + if (options.json) { + outputJson(result); + return; + } + + const r = result as any; + console.log( + `${pc.green('✓')} Message sent${r.messageId ? ` (${pc.dim(r.messageId)})` : ''}`, + ); + }, + ); + + // ── read ──────────────────────────────────────────────── + + message + .command('read ') + .description('Read messages from a channel') + .requiredOption('--target ', 'Target channel / conversation ID') + .option('--limit ', 'Max messages to fetch', String(DEFAULT_BOT_HISTORY_LIMIT)) + .option('--before ', 'Read messages before this ID') + .option('--after ', 'Read messages after this ID') + .option('--start-time ', 'Start time as Unix seconds (Feishu/Lark)') + .option('--end-time ', 'End time as Unix seconds (Feishu/Lark)') + .option('--cursor ', 'Pagination cursor from a previous response (Feishu/Lark)') + .option('--json', 'Output JSON') + .action( + async ( + botId: string, + options: { + after?: string; + before?: string; + cursor?: string; + endTime?: string; + json?: boolean; + limit?: string; + startTime?: string; + target: string; + }, + ) => { + const client = await getTrpcClient(); + const result = await client.botMessage.readMessages.query({ + after: options.after, + before: options.before, + botId, + channelId: options.target, + cursor: options.cursor, + endTime: options.endTime, + limit: options.limit ? Number.parseInt(options.limit, 10) : undefined, + startTime: options.startTime, + }); + + if (options.json) { + outputJson(result); + return; + } + + const messages = (result as any).messages ?? []; + if (messages.length === 0) { + console.log('No messages found.'); + return; + } + + const rows = messages.map((m: any) => [ + m.id || '', + m.author?.name || '', + truncate(m.content || '', 60), + m.timestamp || '', + ]); + + printTable(rows, ['ID', 'AUTHOR', 'CONTENT', 'TIME']); + + const r = result as any; + if (r.hasMore && r.nextCursor) { + console.log( + `\nMore messages available. Use ${pc.dim(`--cursor ${r.nextCursor}`)} to fetch next page.`, + ); + } + }, + ); + + // ── edit ──────────────────────────────────────────────── + + message + .command('edit ') + .description('Edit a message') + .requiredOption('--target ', 'Channel ID') + .requiredOption('--message-id ', 'Message ID to edit') + .requiredOption('--message ', 'New message content') + .action( + async (botId: string, options: { message: string; messageId: string; target: string }) => { + const client = await getTrpcClient(); + await client.botMessage.editMessage.mutate({ + botId, + channelId: options.target, + content: options.message, + messageId: options.messageId, + }); + + console.log(`${pc.green('✓')} Message ${pc.bold(options.messageId)} edited`); + }, + ); + + // ── delete ────────────────────────────────────────────── + + message + .command('delete ') + .description('Delete a message') + .requiredOption('--target ', 'Channel ID') + .requiredOption('--message-id ', 'Message ID to delete') + .option('--yes', 'Skip confirmation prompt') + .action( + async (botId: string, options: { messageId: string; target: string; yes?: boolean }) => { + if (!options.yes) { + const confirmed = await confirm('Are you sure you want to delete this message?'); + if (!confirmed) { + console.log('Cancelled.'); + return; + } + } + + const client = await getTrpcClient(); + await client.botMessage.deleteMessage.mutate({ + botId, + channelId: options.target, + messageId: options.messageId, + }); + + console.log(`${pc.green('✓')} Message ${pc.bold(options.messageId)} deleted`); + }, + ); + + // ── search ────────────────────────────────────────────── + + message + .command('search ') + .description('Search messages in a channel') + .requiredOption('--target ', 'Channel ID to search in') + .requiredOption('--query ', 'Search query') + .option('--author-id ', 'Filter by author ID') + .option('--limit ', 'Max results') + .option('--json', 'Output JSON') + .action( + async ( + botId: string, + options: { + authorId?: string; + json?: boolean; + limit?: string; + query: string; + target: string; + }, + ) => { + const client = await getTrpcClient(); + const result = await client.botMessage.searchMessages.query({ + authorId: options.authorId, + botId, + channelId: options.target, + limit: options.limit ? Number.parseInt(options.limit, 10) : undefined, + query: options.query, + }); + + if (options.json) { + outputJson(result); + return; + } + + const messages = (result as any).messages ?? []; + if (messages.length === 0) { + console.log('No messages found.'); + return; + } + + const rows = messages.map((m: any) => [ + m.id || '', + m.author?.name || '', + truncate(m.content || '', 60), + ]); + + printTable(rows, ['ID', 'AUTHOR', 'CONTENT']); + }, + ); + + // ── react ─────────────────────────────────────────────── + + message + .command('react ') + .description('Add an emoji reaction to a message') + .requiredOption('--target ', 'Channel ID') + .requiredOption('--message-id ', 'Message ID to react to') + .requiredOption('--emoji ', 'Emoji to react with') + .action( + async (botId: string, options: { emoji: string; messageId: string; target: string }) => { + const client = await getTrpcClient(); + await client.botMessage.reactToMessage.mutate({ + botId, + channelId: options.target, + emoji: options.emoji, + messageId: options.messageId, + }); + + console.log( + `${pc.green('✓')} Reacted with ${options.emoji} to message ${pc.bold(options.messageId)}`, + ); + }, + ); + + // ── reactions ─────────────────────────────────────────── + + message + .command('reactions ') + .description('List reactions on a message') + .requiredOption('--target ', 'Channel ID') + .requiredOption('--message-id ', 'Message ID') + .option('--json', 'Output JSON') + .action( + async (botId: string, options: { json?: boolean; messageId: string; target: string }) => { + const client = await getTrpcClient(); + const result = await client.botMessage.getReactions.query({ + botId, + channelId: options.target, + messageId: options.messageId, + }); + + if (options.json) { + outputJson(result); + return; + } + + const reactions = (result as any).reactions ?? []; + if (reactions.length === 0) { + console.log('No reactions found.'); + return; + } + + const rows = reactions.map((r: any) => [r.emoji || '', String(r.count || 0)]); + printTable(rows, ['EMOJI', 'COUNT']); + }, + ); + + // ── pin ───────────────────────────────────────────────── + + message + .command('pin ') + .description('Pin a message') + .requiredOption('--target ', 'Channel ID') + .requiredOption('--message-id ', 'Message ID to pin') + .action(async (botId: string, options: { messageId: string; target: string }) => { + const client = await getTrpcClient(); + await client.botMessage.pinMessage.mutate({ + botId, + channelId: options.target, + messageId: options.messageId, + }); + + console.log(`${pc.green('✓')} Pinned message ${pc.bold(options.messageId)}`); + }); + + // ── unpin ─────────────────────────────────────────────── + + message + .command('unpin ') + .description('Unpin a message') + .requiredOption('--target ', 'Channel ID') + .requiredOption('--message-id ', 'Message ID to unpin') + .action(async (botId: string, options: { messageId: string; target: string }) => { + const client = await getTrpcClient(); + await client.botMessage.unpinMessage.mutate({ + botId, + channelId: options.target, + messageId: options.messageId, + }); + + console.log(`${pc.green('✓')} Unpinned message ${pc.bold(options.messageId)}`); + }); + + // ── pins ──────────────────────────────────────────────── + + message + .command('pins ') + .description('List pinned messages') + .requiredOption('--target ', 'Channel ID') + .option('--json', 'Output JSON') + .action(async (botId: string, options: { json?: boolean; target: string }) => { + const client = await getTrpcClient(); + const result = await client.botMessage.listPins.query({ + botId, + channelId: options.target, + }); + + if (options.json) { + outputJson(result); + return; + } + + const messages = (result as any).messages ?? []; + if (messages.length === 0) { + console.log('No pinned messages.'); + return; + } + + const rows = messages.map((m: any) => [ + m.id || '', + m.author?.name || '', + truncate(m.content || '', 60), + ]); + + printTable(rows, ['ID', 'AUTHOR', 'CONTENT']); + }); + + // ── poll ──────────────────────────────────────────────── + + message + .command('poll ') + .description('Create a poll') + .requiredOption('--target ', 'Channel ID') + .requiredOption('--poll-question ', 'Poll question') + .requiredOption('--poll-option