feat: add media asset system, channel lifecycle refactor, and chat attachments (#54)

This commit is contained in:
BBQ
2026-02-17 19:06:46 +08:00
committed by GitHub
parent 0bdc31311c
commit df7876a30c
106 changed files with 7942 additions and 1274 deletions
+4
View File
@@ -1,3 +1,7 @@
DROP TABLE IF EXISTS bot_history_message_assets;
DROP TABLE IF EXISTS media_assets;
DROP TABLE IF EXISTS bot_storage_bindings;
DROP TABLE IF EXISTS storage_providers;
DROP TABLE IF EXISTS subagents;
DROP TABLE IF EXISTS schedule;
DROP TABLE IF EXISTS lifecycle_events;
+63 -3
View File
@@ -86,7 +86,7 @@ CREATE TABLE IF NOT EXISTS models (
name TEXT,
llm_provider_id UUID NOT NULL REFERENCES llm_providers(id) ON DELETE CASCADE,
dimensions INTEGER,
is_multimodal BOOLEAN NOT NULL DEFAULT false,
input_modalities TEXT[] NOT NULL DEFAULT ARRAY['text']::TEXT[],
type TEXT NOT NULL DEFAULT 'chat',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
@@ -169,11 +169,10 @@ CREATE TABLE IF NOT EXISTS bot_channel_configs (
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',
disabled BOOLEAN NOT NULL DEFAULT false,
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)
);
@@ -343,3 +342,64 @@ CREATE TABLE IF NOT EXISTS subagents (
CREATE INDEX IF NOT EXISTS idx_subagents_bot_id ON subagents(bot_id);
CREATE INDEX IF NOT EXISTS idx_subagents_deleted ON subagents(deleted);
-- storage_providers: pluggable object storage backends
CREATE TABLE IF NOT EXISTS storage_providers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
provider TEXT NOT NULL,
config JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT storage_providers_name_unique UNIQUE (name),
CONSTRAINT storage_providers_provider_check CHECK (provider IN ('localfs', 's3', 'gcs'))
);
-- bot_storage_bindings: per-bot storage backend selection
CREATE TABLE IF NOT EXISTS bot_storage_bindings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
storage_provider_id UUID NOT NULL REFERENCES storage_providers(id) ON DELETE CASCADE,
base_path TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT bot_storage_bindings_unique UNIQUE (bot_id)
);
CREATE INDEX IF NOT EXISTS idx_bot_storage_bindings_bot_id ON bot_storage_bindings(bot_id);
-- media_assets: immutable media objects with dedup by content hash
CREATE TABLE IF NOT EXISTS media_assets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
storage_provider_id UUID REFERENCES storage_providers(id) ON DELETE SET NULL,
content_hash TEXT NOT NULL,
media_type TEXT NOT NULL,
mime TEXT NOT NULL DEFAULT 'application/octet-stream',
size_bytes BIGINT NOT NULL DEFAULT 0,
storage_key TEXT NOT NULL,
original_name TEXT,
width INTEGER,
height INTEGER,
duration_ms BIGINT,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT media_assets_bot_hash_unique UNIQUE (bot_id, content_hash)
);
CREATE INDEX IF NOT EXISTS idx_media_assets_bot_id ON media_assets(bot_id);
CREATE INDEX IF NOT EXISTS idx_media_assets_content_hash ON media_assets(content_hash);
-- bot_history_message_assets: join table linking messages to media assets
CREATE TABLE IF NOT EXISTS bot_history_message_assets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
message_id UUID NOT NULL REFERENCES bot_history_messages(id) ON DELETE CASCADE,
asset_id UUID NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT 'attachment',
ordinal INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT message_asset_unique UNIQUE (message_id, asset_id)
);
CREATE INDEX IF NOT EXISTS idx_message_assets_message_id ON bot_history_message_assets(message_id);
CREATE INDEX IF NOT EXISTS idx_message_assets_asset_id ON bot_history_message_assets(asset_id);
@@ -0,0 +1,13 @@
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'bot_channel_configs' AND column_name = 'disabled'
) THEN
ALTER TABLE bot_channel_configs ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'verified';
UPDATE bot_channel_configs SET status = CASE WHEN disabled THEN 'disabled' ELSE 'verified' END;
ALTER TABLE bot_channel_configs DROP COLUMN disabled;
ALTER TABLE bot_channel_configs DROP CONSTRAINT IF EXISTS bot_channel_status_check;
ALTER TABLE bot_channel_configs ADD CONSTRAINT bot_channel_status_check CHECK (status IN ('pending', 'verified', 'disabled'));
END IF;
END $$;
@@ -0,0 +1,13 @@
-- Replace status (TEXT) with disabled (BOOLEAN). Idempotent: no-op when already migrated.
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'bot_channel_configs' AND column_name = 'status'
) THEN
ALTER TABLE bot_channel_configs ADD COLUMN IF NOT EXISTS disabled BOOLEAN NOT NULL DEFAULT false;
UPDATE bot_channel_configs SET disabled = (status = 'disabled');
ALTER TABLE bot_channel_configs DROP CONSTRAINT IF EXISTS bot_channel_status_check;
ALTER TABLE bot_channel_configs DROP COLUMN status;
END IF;
END $$;
@@ -0,0 +1,7 @@
ALTER TABLE models ADD COLUMN IF NOT EXISTS is_multimodal BOOLEAN NOT NULL DEFAULT false;
UPDATE models SET is_multimodal = true WHERE 'image' = ANY(input_modalities);
UPDATE models SET is_multimodal = false WHERE NOT ('image' = ANY(input_modalities));
ALTER TABLE models DROP COLUMN IF EXISTS input_modalities;
ALTER TABLE models DROP COLUMN IF EXISTS output_modalities;
@@ -0,0 +1,8 @@
-- Replace is_multimodal boolean with input modality array.
ALTER TABLE models ADD COLUMN IF NOT EXISTS input_modalities TEXT[] NOT NULL DEFAULT ARRAY['text']::TEXT[];
-- Migrate existing data: true -> ['text','image'], false -> ['text']
UPDATE models SET input_modalities = ARRAY['text','image']::TEXT[] WHERE is_multimodal = true;
UPDATE models SET input_modalities = ARRAY['text']::TEXT[] WHERE is_multimodal = false;
ALTER TABLE models DROP COLUMN IF EXISTS is_multimodal;
+4
View File
@@ -0,0 +1,4 @@
DROP TABLE IF EXISTS bot_history_message_assets;
DROP TABLE IF EXISTS media_assets;
DROP TABLE IF EXISTS bot_storage_bindings;
DROP TABLE IF EXISTS storage_providers;
+60
View File
@@ -0,0 +1,60 @@
-- storage_providers: pluggable object storage backends
CREATE TABLE IF NOT EXISTS storage_providers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
provider TEXT NOT NULL,
config JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT storage_providers_name_unique UNIQUE (name),
CONSTRAINT storage_providers_provider_check CHECK (provider IN ('localfs', 's3', 'gcs'))
);
-- bot_storage_bindings: per-bot storage backend selection
CREATE TABLE IF NOT EXISTS bot_storage_bindings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
storage_provider_id UUID NOT NULL REFERENCES storage_providers(id) ON DELETE CASCADE,
base_path TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT bot_storage_bindings_unique UNIQUE (bot_id)
);
CREATE INDEX IF NOT EXISTS idx_bot_storage_bindings_bot_id ON bot_storage_bindings(bot_id);
-- media_assets: immutable media objects with dedup by content hash
CREATE TABLE IF NOT EXISTS media_assets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
bot_id UUID NOT NULL REFERENCES bots(id) ON DELETE CASCADE,
storage_provider_id UUID REFERENCES storage_providers(id) ON DELETE SET NULL,
content_hash TEXT NOT NULL,
media_type TEXT NOT NULL,
mime TEXT NOT NULL DEFAULT 'application/octet-stream',
size_bytes BIGINT NOT NULL DEFAULT 0,
storage_key TEXT NOT NULL,
original_name TEXT,
width INTEGER,
height INTEGER,
duration_ms BIGINT,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT media_assets_bot_hash_unique UNIQUE (bot_id, content_hash)
);
CREATE INDEX IF NOT EXISTS idx_media_assets_bot_id ON media_assets(bot_id);
CREATE INDEX IF NOT EXISTS idx_media_assets_content_hash ON media_assets(content_hash);
-- bot_history_message_assets: join table linking messages to media assets
CREATE TABLE IF NOT EXISTS bot_history_message_assets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
message_id UUID NOT NULL REFERENCES bot_history_messages(id) ON DELETE CASCADE,
asset_id UUID NOT NULL REFERENCES media_assets(id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT 'attachment',
ordinal INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT message_asset_unique UNIQUE (message_id, asset_id)
);
CREATE INDEX IF NOT EXISTS idx_message_assets_message_id ON bot_history_message_assets(message_id);
CREATE INDEX IF NOT EXISTS idx_message_assets_asset_id ON bot_history_message_assets(asset_id);