feat: channel gateway implementation and multi-bot refactor

- Refactor channel manager with support for Sender/Receiver interfaces and hot-swappable adapters.
- Implement identity routing and pre-authentication logic for inbound messages.
- Update database schema to support bot pre-auth keys and extended channel session metadata.
- Add Telegram and Feishu channel configuration and adapter enhancements.
- Update Swagger documentation and internal handlers for channel management.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
BBQ
2026-02-06 14:41:54 +08:00
parent 0bba6d2913
commit 5a35ef34ac
106 changed files with 7910 additions and 3044 deletions
+1 -1
View File
@@ -6,8 +6,8 @@ DROP TABLE IF EXISTS container_versions;
DROP TABLE IF EXISTS snapshots;
DROP TABLE IF EXISTS containers;
DROP TABLE IF EXISTS channel_sessions;
DROP TABLE IF EXISTS contact_bind_tokens;
DROP TABLE IF EXISTS contact_channels;
DROP TABLE IF EXISTS bot_preauth_keys;
DROP TABLE IF EXISTS contacts;
DROP TABLE IF EXISTS bot_channel_configs;
DROP TABLE IF EXISTS user_channel_bindings;
+23 -19
View File
@@ -95,7 +95,7 @@ CREATE INDEX IF NOT EXISTS idx_bot_members_user_id ON bot_members(user_id);
CREATE TABLE IF NOT EXISTS bot_settings (
bot_id UUID PRIMARY KEY REFERENCES bots(id) ON DELETE CASCADE,
max_context_load_time INTEGER NOT NULL DEFAULT 1440,
language TEXT NOT NULL DEFAULT 'Same as user input',
language TEXT NOT NULL DEFAULT 'auto',
allow_guest BOOLEAN NOT NULL DEFAULT false
);
@@ -125,6 +125,7 @@ CREATE TABLE IF NOT EXISTS history (
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
session_id TEXT NOT NULL,
messages JSONB NOT NULL,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
skills TEXT[] NOT NULL DEFAULT '{}'::text[],
timestamp TIMESTAMPTZ NOT NULL
);
@@ -187,6 +188,20 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_contacts_bot_user_unique
CREATE INDEX IF NOT EXISTS idx_contacts_bot_id ON contacts(bot_id);
CREATE TABLE IF NOT EXISTS bot_preauth_keys (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
token TEXT NOT NULL,
issued_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
expires_at TIMESTAMPTZ,
used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT bot_preauth_keys_unique UNIQUE (token)
);
CREATE INDEX IF NOT EXISTS idx_bot_preauth_keys_bot_id ON bot_preauth_keys(bot_id);
CREATE INDEX IF NOT EXISTS idx_bot_preauth_keys_expires ON bot_preauth_keys(expires_at);
CREATE TABLE IF NOT EXISTS contact_channels (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
@@ -202,23 +217,6 @@ CREATE TABLE IF NOT EXISTS contact_channels (
CREATE INDEX IF NOT EXISTS idx_contact_channels_contact_id ON contact_channels(contact_id);
CREATE INDEX IF NOT EXISTS idx_contact_channels_platform_external ON contact_channels(platform, external_id);
CREATE TABLE IF NOT EXISTS contact_bind_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
contact_id UUID NOT NULL REFERENCES contacts(id) ON DELETE CASCADE,
token TEXT NOT NULL,
target_platform TEXT,
target_external_id TEXT,
issued_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
expires_at TIMESTAMPTZ NOT NULL,
used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT contact_bind_tokens_unique UNIQUE (token)
);
CREATE INDEX IF NOT EXISTS idx_contact_bind_tokens_contact_id ON contact_bind_tokens(contact_id);
CREATE INDEX IF NOT EXISTS idx_contact_bind_tokens_expires ON contact_bind_tokens(expires_at);
CREATE TABLE IF NOT EXISTS channel_sessions (
session_id TEXT PRIMARY KEY,
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
@@ -226,6 +224,9 @@ CREATE TABLE IF NOT EXISTS channel_sessions (
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
contact_id UUID REFERENCES contacts(id) ON DELETE SET NULL,
platform TEXT NOT NULL,
reply_target TEXT,
thread_id TEXT,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
@@ -325,6 +326,9 @@ CREATE INDEX IF NOT EXISTS idx_subagents_deleted ON subagents(deleted);
CREATE TABLE IF NOT EXISTS user_settings (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
chat_model_id TEXT,
memory_model_id TEXT,
embedding_model_id TEXT,
max_context_load_time INTEGER NOT NULL DEFAULT 1440,
language TEXT NOT NULL DEFAULT 'Same as user input'
language TEXT NOT NULL DEFAULT 'auto'
);
+87
View File
@@ -0,0 +1,87 @@
-- name: GetBotChannelConfig :one
SELECT id, bot_id, channel_type, credentials, external_identity, self_identity, routing, capabilities, status, verified_at, created_at, updated_at
FROM bot_channel_configs
WHERE bot_id = $1 AND channel_type = $2
LIMIT 1;
-- name: GetBotChannelConfigByExternalIdentity :one
SELECT id, bot_id, channel_type, credentials, external_identity, self_identity, routing, capabilities, status, verified_at, created_at, updated_at
FROM bot_channel_configs
WHERE channel_type = $1 AND external_identity = $2
LIMIT 1;
-- name: UpsertBotChannelConfig :one
INSERT INTO bot_channel_configs (
bot_id, channel_type, credentials, external_identity, self_identity, routing, capabilities, status, verified_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT (bot_id, channel_type)
DO UPDATE SET
credentials = EXCLUDED.credentials,
external_identity = EXCLUDED.external_identity,
self_identity = EXCLUDED.self_identity,
routing = EXCLUDED.routing,
capabilities = EXCLUDED.capabilities,
status = EXCLUDED.status,
verified_at = EXCLUDED.verified_at,
updated_at = now()
RETURNING id, bot_id, channel_type, credentials, external_identity, self_identity, routing, capabilities, status, verified_at, created_at, updated_at;
-- name: ListBotChannelConfigsByType :many
SELECT id, bot_id, channel_type, credentials, external_identity, self_identity, routing, capabilities, status, verified_at, created_at, updated_at
FROM bot_channel_configs
WHERE channel_type = $1
ORDER BY created_at DESC;
-- name: GetUserChannelBinding :one
SELECT id, user_id, channel_type, config, created_at, updated_at
FROM user_channel_bindings
WHERE user_id = $1 AND channel_type = $2
LIMIT 1;
-- name: UpsertUserChannelBinding :one
INSERT INTO user_channel_bindings (user_id, channel_type, config)
VALUES ($1, $2, $3)
ON CONFLICT (user_id, channel_type)
DO UPDATE SET
config = EXCLUDED.config,
updated_at = now()
RETURNING id, user_id, channel_type, config, created_at, updated_at;
-- name: ListUserChannelBindingsByType :many
SELECT id, user_id, channel_type, config, created_at, updated_at
FROM user_channel_bindings
WHERE channel_type = $1
ORDER BY created_at DESC;
-- name: GetChannelSessionByID :one
SELECT session_id, bot_id, channel_config_id, user_id, contact_id, platform, reply_target, thread_id, metadata, created_at, updated_at
FROM channel_sessions
WHERE session_id = $1
LIMIT 1;
-- name: ListChannelSessionsByBotPlatform :many
SELECT session_id, bot_id, channel_config_id, user_id, contact_id, platform, reply_target, thread_id, metadata, created_at, updated_at
FROM channel_sessions
WHERE bot_id = $1 AND platform = $2
ORDER BY updated_at DESC;
-- name: UpsertChannelSession :one
INSERT INTO channel_sessions (session_id, bot_id, channel_config_id, user_id, contact_id, platform, reply_target, thread_id, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT (session_id)
DO UPDATE SET
bot_id = EXCLUDED.bot_id,
channel_config_id = EXCLUDED.channel_config_id,
user_id = EXCLUDED.user_id,
contact_id = EXCLUDED.contact_id,
platform = EXCLUDED.platform,
reply_target = EXCLUDED.reply_target,
thread_id = EXCLUDED.thread_id,
metadata = EXCLUDED.metadata,
updated_at = now()
RETURNING session_id, bot_id, channel_config_id, user_id, contact_id, platform, reply_target, thread_id, metadata, created_at, updated_at;
-- name: DeleteChannelSession :exec
DELETE FROM channel_sessions
WHERE session_id = $1;
+76
View File
@@ -0,0 +1,76 @@
-- name: CreateContact :one
INSERT INTO contacts (bot_id, user_id, display_name, alias, tags, status, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, bot_id, user_id, display_name, alias, tags, status, metadata, created_at, updated_at;
-- name: GetContactByID :one
SELECT id, bot_id, user_id, display_name, alias, tags, status, metadata, created_at, updated_at
FROM contacts
WHERE id = $1
LIMIT 1;
-- name: GetContactByUserID :one
SELECT id, bot_id, user_id, display_name, alias, tags, status, metadata, created_at, updated_at
FROM contacts
WHERE bot_id = $1 AND user_id = $2
LIMIT 1;
-- name: ListContactsByBot :many
SELECT id, bot_id, user_id, display_name, alias, tags, status, metadata, created_at, updated_at
FROM contacts
WHERE bot_id = $1
ORDER BY created_at DESC;
-- name: SearchContacts :many
SELECT id, bot_id, user_id, display_name, alias, tags, status, metadata, created_at, updated_at
FROM contacts
WHERE bot_id = $1
AND (
display_name ILIKE sqlc.arg(query)
OR alias ILIKE sqlc.arg(query)
OR EXISTS (
SELECT 1 FROM unnest(tags) AS tag WHERE tag ILIKE sqlc.arg(query)
)
)
ORDER BY created_at DESC;
-- name: UpdateContact :one
UPDATE contacts
SET display_name = COALESCE(sqlc.narg(display_name), display_name),
alias = COALESCE(sqlc.narg(alias), alias),
tags = COALESCE(sqlc.narg(tags), tags),
status = COALESCE(NULLIF(sqlc.arg(status)::text, ''), status),
metadata = COALESCE(sqlc.narg(metadata), metadata),
updated_at = now()
WHERE id = sqlc.arg(id)
RETURNING id, bot_id, user_id, display_name, alias, tags, status, metadata, created_at, updated_at;
-- name: UpdateContactUser :one
UPDATE contacts
SET user_id = $2,
updated_at = now()
WHERE id = $1
RETURNING id, bot_id, user_id, display_name, alias, tags, status, metadata, created_at, updated_at;
-- name: UpsertContactChannel :one
INSERT INTO contact_channels (bot_id, contact_id, platform, external_id, metadata)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (bot_id, platform, external_id)
DO UPDATE SET
contact_id = EXCLUDED.contact_id,
metadata = EXCLUDED.metadata,
updated_at = now()
RETURNING id, bot_id, contact_id, platform, external_id, metadata, created_at, updated_at;
-- name: GetContactChannelByIdentity :one
SELECT id, bot_id, contact_id, platform, external_id, metadata, created_at, updated_at
FROM contact_channels
WHERE bot_id = $1 AND platform = $2 AND external_id = $3
LIMIT 1;
-- name: ListContactChannelsByContact :many
SELECT id, bot_id, contact_id, platform, external_id, metadata, created_at, updated_at
FROM contact_channels
WHERE contact_id = $1
ORDER BY created_at DESC;
+3
View File
@@ -31,3 +31,6 @@ ON CONFLICT (container_id) DO UPDATE SET
-- name: GetContainerByContainerID :one
SELECT * FROM containers WHERE container_id = sqlc.arg(container_id);
-- name: GetContainerByBotID :one
SELECT * FROM containers WHERE bot_id = sqlc.arg(bot_id) ORDER BY updated_at DESC LIMIT 1;
+6 -6
View File
@@ -1,21 +1,21 @@
-- name: CreateHistory :one
INSERT INTO history (bot_id, session_id, messages, skills, timestamp)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, bot_id, session_id, messages, skills, timestamp;
INSERT INTO history (bot_id, session_id, messages, metadata, skills, timestamp)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, bot_id, session_id, messages, metadata, skills, timestamp;
-- name: ListHistoryByBotSessionSince :many
SELECT id, bot_id, session_id, messages, skills, timestamp
SELECT id, bot_id, session_id, messages, metadata, skills, timestamp
FROM history
WHERE bot_id = $1 AND session_id = $2 AND timestamp >= $3
ORDER BY timestamp ASC;
-- name: GetHistoryByID :one
SELECT id, bot_id, session_id, messages, skills, timestamp
SELECT id, bot_id, session_id, messages, metadata, skills, timestamp
FROM history
WHERE id = $1;
-- name: ListHistoryByBotSession :many
SELECT id, bot_id, session_id, messages, skills, timestamp
SELECT id, bot_id, session_id, messages, metadata, skills, timestamp
FROM history
WHERE bot_id = $1 AND session_id = $2
ORDER BY timestamp DESC
+16
View File
@@ -0,0 +1,16 @@
-- name: CreateBotPreauthKey :one
INSERT INTO bot_preauth_keys (bot_id, token, issued_by_user_id, expires_at)
VALUES ($1, $2, $3, $4)
RETURNING id, bot_id, token, issued_by_user_id, expires_at, used_at, created_at;
-- name: GetBotPreauthKey :one
SELECT id, bot_id, token, issued_by_user_id, expires_at, used_at, created_at
FROM bot_preauth_keys
WHERE token = $1
LIMIT 1;
-- name: MarkBotPreauthKeyUsed :one
UPDATE bot_preauth_keys
SET used_at = now()
WHERE id = $1
RETURNING id, bot_id, token, issued_by_user_id, expires_at, used_at, created_at;