mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
6acdd191c7
commit bcdb026ae43e4f95d0b2c4f9bd440a2df9d6b514 Author: Ran <16112591+chen-ran@users.noreply.github.com> Date: Thu Feb 12 17:10:32 2026 +0800 chore: update DEVELOPMENT.md commit30281742efMerge:ca5c6a15b05f13Author: BBQ <bbq@BBQdeMacBook-Air.local> Date: Thu Feb 12 15:49:17 2026 +0800 merge(github/main): integrate fx dependency injection framework Merge upstream fx refactor and adapt all services to use go.uber.org/fx for dependency injection. Resolve conflicts in main.go, server.go, and service constructors while preserving our domain model changes. - Fix telegram adapter panic on shutdown (double close channel) - Fix feishu adapter processing messages after stop - Increase directory lookup timeout from 2s to 5s commitca5c6a1866Author: BBQ <bbq@BBQdeMacBook-Air.local> Date: Thu Feb 12 15:33:09 2026 +0800 refactor(core): restructure conversation, channel and message domains - Rename chat module to conversation with flow-based architecture - Move channelidentities into channel/identities subpackage - Add channel/route for routing logic - Add message service with event hub - Add MCP providers: container, directory, schedule - Refactor Feishu/Telegram adapters with directory and stream support - Add platform management page and channel badges in web UI - Update database schema for conversations, messages and channel routes - Add @memoh/shared package for cross-package type definitions commit75e2ef0467Merge:d99ba3801cb6c8Author: BBQ <bbq@BBQdeMacBook-Air.local> Date: Thu Feb 12 14:45:49 2026 +0800 merge(github): merge github/main, resolve index.ts URL conflict Keep our defensive absolute-URL check in createAuthFetcher. commitd99ba38b7dMerge:860e20f35ce7d1Author: BBQ <bbq@BBQdeMacBook-Air.local> Date: Thu Feb 12 05:20:18 2026 +0800 merge(github): merge github/main, keep our code and docs/spec commit860e20fe70Author: BBQ <bbq@BBQdeMacBook-Air.local> Date: Wed Feb 11 22:13:27 2026 +0800 docs(docs): add concepts and style guides for VitePress site - Add concepts: identity-and-binding, index (en/zh) - Add style: terminology (en/zh) - Update index and zh/index - Update .vitepress/config.ts commita75fdb8040Author: BBQ <bbq@BBQdeMacBook-Air.local> Date: Wed Feb 11 17:37:16 2026 +0800 refactor(mcp): standardize unified tool gateway on go-sdk Split business executors from federation sources and migrate unified tool/federation transports to the official go-sdk for stricter MCP compliance and safer session lifecycle handling. Add targeted regression tests for accept compatibility, initialization retries, pending cleanup, and include updated swagger artifacts. commit02b33c8e85Author: BBQ <bbq@BBQdeMacBook-Air.local> Date: Wed Feb 11 15:42:21 2026 +0800 refactor(core): finalize user-centric identity and policy cleanup Unify auth and chat identity semantics around user_id, enforce personal-bot owner-only authorization, and remove legacy compatibility branches in integration tests. commit06e8619a37Author: BBQ <bbq@BBQdeMacBook-Air.local> Date: Wed Feb 11 14:47:03 2026 +0800 refactor(core): migrate channel identity and binding across app Align channel identity and bind flow across backend and app-facing layers, including generated swagger artifacts and package lock updates while excluding docs content changes.
337 lines
13 KiB
SQL
337 lines
13 KiB
SQL
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_role') THEN
|
|
CREATE TYPE user_role AS ENUM ('member', 'admin');
|
|
END IF;
|
|
END
|
|
$$;
|
|
|
|
-- users: Memoh user principal
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
username TEXT,
|
|
email TEXT,
|
|
password_hash TEXT,
|
|
role user_role NOT NULL DEFAULT 'member',
|
|
display_name TEXT,
|
|
avatar_url TEXT,
|
|
data_root TEXT,
|
|
last_login_at TIMESTAMPTZ,
|
|
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 'auto',
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CONSTRAINT users_email_unique UNIQUE (email),
|
|
CONSTRAINT users_username_unique UNIQUE (username)
|
|
);
|
|
|
|
-- channel_identities: unified inbound identity subject
|
|
CREATE TABLE IF NOT EXISTS channel_identities (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
|
channel_type TEXT NOT NULL,
|
|
channel_subject_id TEXT NOT NULL,
|
|
display_name TEXT,
|
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CONSTRAINT channel_identities_channel_type_subject_unique UNIQUE (channel_type, channel_subject_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_channel_identities_user_id ON channel_identities(user_id);
|
|
|
|
-- user_channel_bindings: outbound delivery config
|
|
CREATE TABLE IF NOT EXISTS user_channel_bindings (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
channel_type TEXT NOT NULL,
|
|
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CONSTRAINT user_channel_bindings_unique UNIQUE (user_id, channel_type)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_user_channel_bindings_user_id ON user_channel_bindings(user_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS llm_providers (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name TEXT NOT NULL,
|
|
client_type TEXT NOT NULL,
|
|
base_url TEXT NOT NULL,
|
|
api_key TEXT NOT NULL,
|
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CONSTRAINT llm_providers_name_unique UNIQUE (name),
|
|
CONSTRAINT llm_providers_client_type_check CHECK (client_type IN ('openai', 'openai-compat', 'anthropic', 'google', 'ollama'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS models (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
model_id TEXT NOT NULL,
|
|
name TEXT,
|
|
llm_provider_id UUID NOT NULL REFERENCES llm_providers(id) ON DELETE CASCADE,
|
|
dimensions INTEGER,
|
|
is_multimodal BOOLEAN NOT NULL DEFAULT false,
|
|
type TEXT NOT NULL DEFAULT 'chat',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CONSTRAINT models_model_id_unique UNIQUE (model_id),
|
|
CONSTRAINT models_type_check CHECK (type IN ('chat', 'embedding')),
|
|
CONSTRAINT models_dimensions_check CHECK (type != 'embedding' OR dimensions IS NOT NULL)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS model_variants (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
model_uuid UUID NOT NULL REFERENCES models(id) ON DELETE CASCADE,
|
|
variant_id TEXT NOT NULL,
|
|
weight INTEGER NOT NULL,
|
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_model_variants_model_uuid ON model_variants(model_uuid);
|
|
CREATE INDEX IF NOT EXISTS idx_model_variants_variant_id ON model_variants(variant_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS bots (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
owner_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
type TEXT NOT NULL,
|
|
display_name TEXT,
|
|
avatar_url TEXT,
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
status TEXT NOT NULL DEFAULT 'ready',
|
|
max_context_load_time INTEGER NOT NULL DEFAULT 1440,
|
|
language TEXT NOT NULL DEFAULT 'auto',
|
|
allow_guest BOOLEAN NOT NULL DEFAULT false,
|
|
chat_model_id UUID REFERENCES models(id) ON DELETE SET NULL,
|
|
memory_model_id UUID REFERENCES models(id) ON DELETE SET NULL,
|
|
embedding_model_id UUID REFERENCES models(id) ON DELETE SET NULL,
|
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CONSTRAINT bots_type_check CHECK (type IN ('personal', 'public')),
|
|
CONSTRAINT bots_status_check CHECK (status IN ('creating', 'ready', 'deleting'))
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_bots_owner_user_id ON bots(owner_user_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS bot_members (
|
|
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
role TEXT NOT NULL DEFAULT 'member',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CONSTRAINT bot_members_role_check CHECK (role IN ('owner', 'admin', 'member')),
|
|
CONSTRAINT bot_members_unique UNIQUE (bot_id, user_id)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_bot_members_user_id ON bot_members(user_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS mcp_connections (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
type TEXT NOT NULL,
|
|
config JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CONSTRAINT mcp_connections_type_check CHECK (type IN ('stdio', 'http', 'sse')),
|
|
CONSTRAINT mcp_connections_unique UNIQUE (bot_id, name)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_mcp_connections_bot_id ON mcp_connections(bot_id);
|
|
|
|
-- Bot history is bot-scoped (one history container per bot).
|
|
|
|
CREATE TABLE IF NOT EXISTS bot_channel_configs (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
|
|
channel_type TEXT NOT NULL,
|
|
credentials JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
external_identity TEXT,
|
|
self_identity JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
routing JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
capabilities JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
verified_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CONSTRAINT bot_channel_status_check CHECK (status IN ('pending', 'verified', 'disabled')),
|
|
CONSTRAINT bot_channel_unique UNIQUE (bot_id, channel_type)
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_bot_channel_external_identity
|
|
ON bot_channel_configs(channel_type, external_identity);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_bot_channel_bot_id ON bot_channel_configs(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);
|
|
|
|
-- channel_identity_bind_codes: one-time codes for channel identity->user linking
|
|
CREATE TABLE IF NOT EXISTS channel_identity_bind_codes (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
token TEXT NOT NULL,
|
|
issued_by_user_id UUID NOT NULL REFERENCES users(id),
|
|
channel_type TEXT,
|
|
expires_at TIMESTAMPTZ,
|
|
used_at TIMESTAMPTZ,
|
|
used_by_channel_identity_id UUID REFERENCES channel_identities(id),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
CONSTRAINT channel_identity_bind_codes_token_unique UNIQUE (token)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_channel_identity_bind_codes_channel_type ON channel_identity_bind_codes(channel_type);
|
|
|
|
-- bot_channel_routes: route mapping for inbound channel threads to bot history.
|
|
CREATE TABLE IF NOT EXISTS bot_channel_routes (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
|
|
channel_type TEXT NOT NULL,
|
|
channel_config_id UUID REFERENCES bot_channel_configs(id) ON DELETE SET NULL,
|
|
external_conversation_id TEXT NOT NULL,
|
|
external_thread_id TEXT,
|
|
default_reply_target TEXT,
|
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_bot_channel_routes_unique
|
|
ON bot_channel_routes (bot_id, channel_type, external_conversation_id, COALESCE(external_thread_id, ''));
|
|
CREATE INDEX IF NOT EXISTS idx_bot_channel_routes_bot ON bot_channel_routes(bot_id);
|
|
|
|
-- bot_history_messages: unified message history under bot scope.
|
|
CREATE TABLE IF NOT EXISTS bot_history_messages (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
|
|
route_id UUID REFERENCES bot_channel_routes(id) ON DELETE SET NULL,
|
|
sender_channel_identity_id UUID REFERENCES channel_identities(id),
|
|
sender_account_user_id UUID REFERENCES users(id),
|
|
channel_type TEXT,
|
|
source_message_id TEXT,
|
|
source_reply_to_message_id TEXT,
|
|
role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system', 'tool')),
|
|
content JSONB NOT NULL,
|
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_bot_history_messages_bot_created ON bot_history_messages(bot_id, created_at);
|
|
CREATE INDEX IF NOT EXISTS idx_bot_history_messages_route ON bot_history_messages(route_id);
|
|
CREATE INDEX IF NOT EXISTS idx_bot_history_messages_source_lookup
|
|
ON bot_history_messages(channel_type, source_message_id);
|
|
CREATE INDEX IF NOT EXISTS idx_bot_history_messages_reply_lookup
|
|
ON bot_history_messages(channel_type, source_reply_to_message_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS containers (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
|
|
container_id TEXT NOT NULL,
|
|
container_name TEXT NOT NULL,
|
|
image TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'created',
|
|
namespace TEXT NOT NULL DEFAULT 'default',
|
|
auto_start BOOLEAN NOT NULL DEFAULT true,
|
|
host_path TEXT,
|
|
container_path TEXT NOT NULL DEFAULT '/data',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
last_started_at TIMESTAMPTZ,
|
|
last_stopped_at TIMESTAMPTZ,
|
|
CONSTRAINT containers_container_id_unique UNIQUE (container_id),
|
|
CONSTRAINT containers_container_name_unique UNIQUE (container_name)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_containers_bot_id ON containers(bot_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS snapshots (
|
|
id TEXT PRIMARY KEY,
|
|
container_id TEXT NOT NULL REFERENCES containers(container_id) ON DELETE CASCADE,
|
|
parent_snapshot_id TEXT REFERENCES snapshots(id) ON DELETE SET NULL,
|
|
snapshotter TEXT NOT NULL,
|
|
digest TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_snapshots_container_id ON snapshots(container_id);
|
|
CREATE INDEX IF NOT EXISTS idx_snapshots_parent_id ON snapshots(parent_snapshot_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS container_versions (
|
|
id TEXT PRIMARY KEY,
|
|
container_id TEXT NOT NULL REFERENCES containers(container_id) ON DELETE CASCADE,
|
|
snapshot_id TEXT NOT NULL REFERENCES snapshots(id) ON DELETE RESTRICT,
|
|
version INTEGER NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
UNIQUE (container_id, version)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_container_versions_container_id ON container_versions(container_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS lifecycle_events (
|
|
id TEXT PRIMARY KEY,
|
|
container_id TEXT NOT NULL REFERENCES containers(container_id) ON DELETE CASCADE,
|
|
event_type TEXT NOT NULL,
|
|
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_lifecycle_events_container_id ON lifecycle_events(container_id);
|
|
CREATE INDEX IF NOT EXISTS idx_lifecycle_events_event_type ON lifecycle_events(event_type);
|
|
|
|
CREATE TABLE IF NOT EXISTS schedule (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name TEXT NOT NULL,
|
|
description TEXT NOT NULL,
|
|
pattern TEXT NOT NULL,
|
|
max_calls INTEGER,
|
|
current_calls INTEGER NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
enabled BOOLEAN NOT NULL DEFAULT true,
|
|
command TEXT NOT NULL,
|
|
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_schedule_bot_id ON schedule(bot_id);
|
|
CREATE INDEX IF NOT EXISTS idx_schedule_enabled ON schedule(enabled);
|
|
|
|
CREATE TABLE IF NOT EXISTS subagents (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
name TEXT NOT NULL,
|
|
description TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
deleted BOOLEAN NOT NULL DEFAULT false,
|
|
deleted_at TIMESTAMPTZ,
|
|
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
|
|
messages JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
skills JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
CONSTRAINT subagents_name_unique UNIQUE (bot_id, name)
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_subagents_bot_id ON subagents(bot_id);
|
|
CREATE INDEX IF NOT EXISTS idx_subagents_deleted ON subagents(deleted);
|
|
|