EchoMate is a TypeScript Discord AI group chat bot built with discord.js v14 and an OpenAI-compatible chat API. It is designed as a local, maintainable MVP with slash commands, optional mention replies, short-term channel memory, rate limiting, structured logs, prompt guard rules, and health diagnostics.
- Slash commands:
/chat,/reset,/stats,/usage,/persona,/lang,/ping,/models,/debug - Optional
@EchoMatemention trigger - OpenAI-compatible API support via
AI_BASE_URL - Provider strategy support for MiMo Web Search and plain OpenAI-compatible APIs
- Optional app-side Tavily web search with prompt injection
- Streaming AI replies with throttled Discord message edits
- Per-user and per-channel rate limits
- Daily mention limit
- Channel/thread/DM-scoped context with TTL cleanup
- Token estimation with
gpt-tokenizer - Prompt Guard rules loaded from
config/prompt-guard-rules.json - Owner-only
/debugdiagnostics - Optional local
/livez,/readyz, and/healthzendpoints - Optional SQLite-backed conversation, rate limit, and usage persistence
- ESLint, Prettier, Vitest, Husky, lint-staged
- Node.js 20+
- npm
- A Discord application and bot token
- An OpenAI-compatible API key and model
- Network access to Discord. In restricted networks, enable TUN mode or configure a working proxy.
Install dependencies:
npm installCreate .env from the example:
copy .env.example .envFill in the required values:
DISCORD_TOKEN=your-discord-bot-token
DISCORD_CLIENT_ID=your-application-id
DISCORD_GUILD_ID=your-test-server-id
BOT_OWNER_ID=your-discord-user-id
AI_BASE_URL=https://your-openai-compatible-api/v1
AI_API_KEY=your-api-key
AI_MODEL=your-model-name
AI_PROVIDER=autoFor example:
AI_BASE_URL=https://api.xiaomimimo.com/v1
AI_MODEL=mimo-v2.5-proIf a real API key was ever committed or shared, rotate it in the provider console.
EchoMate can keep standard OpenAI-compatible requests for most providers, and enable MiMo's server-managed Web Search when the configured base URL is MiMo.
AI_PROVIDER=auto
AI_WEB_SEARCH_ENABLED=true
AI_WEB_SEARCH_MODE=auto
AI_WEB_SEARCH_MAX_KEYWORD=5
AI_WEB_SEARCH_LIMIT=3
AI_THINKING_TYPE=disabled
AI_SHOW_SEARCH_ANNOTATIONS=false
AI_NOTIFY_SEARCH_DOWNGRADE=trueProvider options:
auto: detect MiMo fromAI_BASE_URL, otherwise use standard OpenAI-compatible behavior.mimo: force MiMo provider extensions.openai-compatible: send standard Chat Completions parameters only.standard: disable all provider extensions for compatibility troubleshooting.
MiMo Web Search notes:
- Enable the Web Search Plugin in the MiMo Console before using it.
- Plugin enable/disable changes can take a few minutes to take effect.
- Search can add latency and provider-side cost.
- Search failures are downgraded once to a no-search answer so Discord users still get a response.
- Raw search results and annotations are not saved in conversation context.
If you do not want to pay for MiMo's native Web Search plugin, EchoMate can call Tavily itself and inject compact search snippets into the current prompt. This mode is mutually exclusive with MiMo native search.
AI_WEB_SEARCH_ENABLED=false
SEARCH_ENABLED=true
SEARCH_PROVIDER=tavily
SEARCH_API_KEY=your-tavily-key
SEARCH_RESULT_LIMIT=2
SEARCH_CACHE_TTL_MS=300000
SEARCH_RATE_LIMIT_MAX=10
SEARCH_RATE_LIMIT_WINDOW_MS=60000
SEARCH_DAILY_LIMIT=100
SEARCH_DAILY_WARNING_RATIO=0.8
SEARCH_LLM_INTENT_ENABLED=false
SEARCH_SHOW_SKIP_REASON=false
SEARCH_PROGRESS_NOTICE=trueApp-side search notes:
- MiMo native search wins when both
AI_WEB_SEARCH_ENABLED=trueandSEARCH_ENABLED=true. - Search results are temporary context for one reply only; they are not saved to channel memory.
- Cache hits do not consume per-user search limits or the global daily budget.
- Tavily calls can add latency and consume provider quota;
/debugshows usage diagnostics. SEARCH_LLM_INTENT_ENABLED=trueenables a low-token yes/no classifier for ambiguous prompts.
EchoMate defaults to in-memory storage for local development. For long-running Docker or VPS deployments, enable SQLite:
STORAGE_DRIVER=sqlite
SQLITE_DB_PATH=data/echomate.sqlite
USAGE_RETENTION_DAYS=90
SQLITE_MAX_DB_SIZE_MB=512
HEALTH_CHECK_PORT=3000
CHANNEL_ALLOWLIST=
CHANNEL_BLOCKLIST=
MESSAGE_REFERENCE_ENABLED=true
FEEDBACK_REACTIONS_ENABLED=falseSQLite mode persists conversation context, rate-limit windows, and usage summaries across restarts. Usage records do not store user prompts or AI response text. Current in-memory conversations are not migrated when switching to SQLite.
Conversation memory is scoped as follows:
- Discord threads and forum posts:
thread:{threadId} - Guild text channels:
channel:{channelId} - DMs:
dm:{userId}
Back up SQLite while the bot is stopped, or copy the database from data/. A simple daily backup policy is to keep timestamped copies for 7 days.
Copy-Item data/echomate.sqlite "data/backup/echomate-$(Get-Date -Format yyyyMMdd).sqlite"CHANNEL_ALLOWLIST and CHANNEL_BLOCKLIST accept comma-separated Discord channel or thread IDs. The blocklist wins over the allowlist.
When MESSAGE_REFERENCE_ENABLED=true, mentioning the bot while replying to a previous bot message includes that referenced bot message as context for the follow-up. FEEDBACK_REACTIONS_ENABLED=true makes successful bot replies add thumbs-up/down reactions for lightweight feedback; the bot needs permission to add reactions.
When HEALTH_CHECK_PORT is enabled:
/livezreturns process liveness./readyzchecks Discord readiness, storage, memory, and DB size status./metricsexposes a Prometheus-style text snapshot for request, error, token, and latency counters.
In the Discord Developer Portal:
- Create an application.
- Add a bot and copy its token.
- Copy the Application ID into
DISCORD_CLIENT_ID. - Enable Message Content Intent only if you want
@mentionreplies. - Invite the bot with these scopes:
botapplications.commands
- Grant minimal permissions:
Send MessagesUse Slash Commands- Optional:
Read Message History
- Leave Interactions Endpoint URL empty for this Gateway-based bot.
Users can tune replies without changing the global system prompt:
/persona style:technical
/lang language:简体中文
Preferences are stored per Discord user. In SQLite mode they persist across restarts; in memory mode they reset when the bot restarts.
Register slash commands to the configured test guild:
npm run registerIf Discord is unreachable from your network, use a proxy or TUN mode. With PowerShell:
$env:HTTPS_PROXY="http://127.0.0.1:7890"
npm run registernpm run devWith a proxy:
$env:HTTPS_PROXY="http://127.0.0.1:7890"
npm run devWhen startup is healthy, logs should include:
discord shard ready
discord bot is ready
runtime AI configuration
Check whether the bot token, application ID, guild ID, and AI settings are wired correctly:
npm run doctorExpected important line:
Client ID matches token application: true
npm run dev # Start the bot in watch mode
npm run register # Register Discord slash commands
npm run doctor # Diagnose Discord token/client configuration
npm run lint # Run ESLint
npm run typecheck # Run TypeScript checks
npm test # Run Vitest
npm run test:coverage # Run Vitest with coverage thresholds
npm run build # Compile TypeScript
npm run docker:build # Build the production Docker imagesrc/
├── application/ # Chat use case orchestration
├── commands/ # Slash command definitions and handlers
├── config/ # Env schema, model defaults, DI container
├── events/ # Discord event handlers
├── health/ # Optional local health server
├── services/
│ ├── ai/ # AI service and Prompt Guard
│ ├── context/ # ContextManager and tokenizer
│ ├── discord/ # Streaming reply handler
│ └── rateLimit/ # User/channel/mention limiters
├── types/
└── utils/
.envis ignored by git. Do not commit real Discord or AI keys.DISCORD_GUILD_IDis recommended during development because guild commands update quickly.- Global command registration is supported when
DISCORD_GUILD_IDis empty, but Discord propagation can be slow. - MiMo Web Search is supported through the provider strategy layer. Other providers keep standard OpenAI-compatible behavior unless an adapter is added.