diff --git a/Makefile b/Makefile index 308654d..88c94e6 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ PSQL = docker compose exec -T postgres psql -U postgres -d postgres .DEFAULT_GOAL := help -.PHONY: help up down restart logs api-logs db db-list db-seed db-seed-model-resources db-seed-assistants db-clear db-reset db-drop-schema db-reset-hard +.PHONY: help up down restart logs api-logs db db-list db-up db-schema db-seed-interface-definitions db-init db-seed db-seed-model-resources db-seed-assistants db-clear db-reset db-drop-schema db-reset-hard help: ## 列出所有可用目标 @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \ @@ -37,22 +37,31 @@ db-list: ## 列出模型资源与助手 @$(PSQL) -c "SELECT id, name, capability, interface_type, is_default FROM model_resources ORDER BY id;" @$(PSQL) -c "SELECT id, name, type FROM assistants ORDER BY id;" +db-up: ## 仅启动 postgres + docker compose up -d postgres + +db-schema: ## 用纯 SQL 建表/补齐开发库表结构 + $(PSQL) < backend/db/schema.sql + +db-seed-interface-definitions: ## 灌入接口定义种子(幂等) + $(PSQL) < backend/db/seed_interface_definitions.sql + +db-init: db-schema db-seed-interface-definitions ## 用纯 SQL 建表并同步接口定义 + db-seed-model-resources: ## 灌入 12 条模型资源种子(幂等) $(PSQL) < backend/db/seed_model_resources.sql db-seed-assistants: ## 灌入 知识库 + 助手 种子(幂等;依赖模型资源已就绪) $(PSQL) < backend/db/seed_assistants.sql -db-seed: db-seed-model-resources db-seed-assistants ## 全量灌种子(模型资源→知识库→助手,幂等,可重复执行) +db-seed: db-init db-seed-model-resources db-seed-assistants ## 全量灌种子(接口定义→模型资源→知识库→助手,幂等,可重复执行) db-clear: ## 清空 助手/知识库/模型资源(按依赖顺序;表不存在时跳过) @$(PSQL) -c "DO \$$\$$ BEGIN IF EXISTS (SELECT FROM pg_tables WHERE schemaname = 'public' AND tablename = 'model_resources') THEN TRUNCATE assistant_model_bindings, assistants, knowledge_bases, model_resources CASCADE; END IF; END \$$\$$;" -db-reset: db-clear db-seed ## 清空后重新灌全部种子 +db-reset: db-up db-clear db-seed ## 启动库、建表/清空后重新灌全部种子 db-drop-schema: ## 删除并重建 public schema(开发用:表结构变更后会清空全部数据) @$(PSQL) -c "DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public;" -db-reset-hard: db-drop-schema restart ## 重建 schema,重启 api 让 init_db 建表,再灌种子 - sleep 3 - $(MAKE) db-seed +db-reset-hard: db-up db-drop-schema db-seed ## 启动库、重建 schema,建表,再灌种子 diff --git a/backend/db/schema.sql b/backend/db/schema.sql new file mode 100644 index 0000000..221a423 --- /dev/null +++ b/backend/db/schema.sql @@ -0,0 +1,91 @@ +-- Development schema bootstrap. Keep this in sync with backend/db/models.py. + +CREATE TABLE IF NOT EXISTS interface_definitions ( + interface_type VARCHAR(64) PRIMARY KEY, + name VARCHAR(128) NOT NULL, + capability VARCHAR(16) NOT NULL, + field_schema JSONB NOT NULL DEFAULT '{}'::jsonb, + enabled BOOLEAN NOT NULL DEFAULT TRUE, + version INTEGER NOT NULL DEFAULT 1, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE IF NOT EXISTS model_resources ( + id VARCHAR(40) PRIMARY KEY, + name VARCHAR(128) NOT NULL DEFAULT '', + capability VARCHAR(16) NOT NULL, + interface_type VARCHAR(64) NOT NULL REFERENCES interface_definitions(interface_type) ON DELETE RESTRICT, + values JSONB NOT NULL DEFAULT '{}'::jsonb, + secrets JSONB NOT NULL DEFAULT '{}'::jsonb, + support_image_input BOOLEAN NOT NULL DEFAULT FALSE, + enabled BOOLEAN NOT NULL DEFAULT TRUE, + is_default BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE IF NOT EXISTS knowledge_bases ( + id VARCHAR(40) PRIMARY KEY, + name VARCHAR(128) NOT NULL, + description VARCHAR(2048) NOT NULL DEFAULT '', + embedding_model_resource_id VARCHAR(40) REFERENCES model_resources(id) ON DELETE SET NULL, + status VARCHAR(16) NOT NULL DEFAULT 'active', + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE IF NOT EXISTS assistants ( + id VARCHAR(40) PRIMARY KEY, + name VARCHAR(128) NOT NULL, + type VARCHAR(16) NOT NULL DEFAULT 'prompt', + runtime_mode VARCHAR(16) NOT NULL DEFAULT 'pipeline', + greeting VARCHAR(2048) NOT NULL DEFAULT '', + enable_interrupt BOOLEAN NOT NULL DEFAULT TRUE, + vision_enabled BOOLEAN NOT NULL DEFAULT FALSE, + vision_model_resource_id VARCHAR(40) REFERENCES model_resources(id) ON DELETE SET NULL, + knowledge_base_id VARCHAR(40) REFERENCES knowledge_bases(id) ON DELETE RESTRICT, + prompt VARCHAR(8192) NOT NULL DEFAULT '', + api_url VARCHAR(512) NOT NULL DEFAULT '', + api_key VARCHAR(512) NOT NULL DEFAULT '', + app_id VARCHAR(128) NOT NULL DEFAULT '', + graph JSON NOT NULL DEFAULT '{}'::json, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE IF NOT EXISTS assistant_model_bindings ( + assistant_id VARCHAR(40) NOT NULL REFERENCES assistants(id) ON DELETE CASCADE, + capability VARCHAR(16) NOT NULL, + model_resource_id VARCHAR(40) NOT NULL REFERENCES model_resources(id) ON DELETE RESTRICT, + config JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (assistant_id, capability) +); + +-- Lightweight forward-compatible patches for existing dev databases. +ALTER TABLE interface_definitions + ALTER COLUMN field_schema TYPE JSONB USING field_schema::jsonb; + +ALTER TABLE assistants + ADD COLUMN IF NOT EXISTS vision_enabled BOOLEAN NOT NULL DEFAULT FALSE, + ADD COLUMN IF NOT EXISTS vision_model_resource_id VARCHAR(40); + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'assistants_vision_model_resource_id_fkey' + ) THEN + ALTER TABLE assistants + ADD CONSTRAINT assistants_vision_model_resource_id_fkey + FOREIGN KEY (vision_model_resource_id) REFERENCES model_resources(id) ON DELETE SET NULL; + END IF; +END $$; + +CREATE INDEX IF NOT EXISTS ix_interface_definitions_capability ON interface_definitions(capability); +CREATE INDEX IF NOT EXISTS ix_model_resources_capability ON model_resources(capability); +CREATE INDEX IF NOT EXISTS ix_model_resources_interface_type ON model_resources(interface_type); +CREATE INDEX IF NOT EXISTS ix_knowledge_bases_embedding_model_resource_id ON knowledge_bases(embedding_model_resource_id); +CREATE INDEX IF NOT EXISTS ix_assistants_type ON assistants(type); +CREATE INDEX IF NOT EXISTS ix_assistant_model_bindings_model_resource_id ON assistant_model_bindings(model_resource_id); diff --git a/backend/db/seed_interface_definitions.sql b/backend/db/seed_interface_definitions.sql new file mode 100644 index 0000000..edd0af3 --- /dev/null +++ b/backend/db/seed_interface_definitions.sql @@ -0,0 +1,119 @@ +-- Built-in interface definitions. Mirrors backend/services/interface_catalog.py. + +INSERT INTO interface_definitions + (interface_type, name, capability, field_schema, enabled, version) +VALUES + ('openai-llm', 'OpenAI Compatible LLM', 'LLM', + $$ {"fields":[ + {"key":"modelId","label":"Model ID","group":"values","type":"text","required":true}, + {"key":"apiUrl","label":"API URL","group":"values","type":"url","required":true}, + {"key":"apiKey","label":"API Key","group":"secrets","type":"password","required":true}, + {"key":"temperature","label":"Temperature","group":"values","type":"number","required":false,"default":0.7} + ]} $$::jsonb, TRUE, 1), + ('openai-asr', 'OpenAI Compatible ASR', 'ASR', + $$ {"fields":[ + {"key":"modelId","label":"Model ID","group":"values","type":"text","required":true}, + {"key":"apiUrl","label":"API URL","group":"values","type":"url","required":true}, + {"key":"apiKey","label":"API Key","group":"secrets","type":"password","required":true}, + {"key":"language","label":"Language","group":"values","type":"text","required":false,"default":"zh"} + ]} $$::jsonb, TRUE, 1), + ('openai-tts', 'OpenAI Compatible TTS', 'TTS', + $$ {"fields":[ + {"key":"modelId","label":"Model ID","group":"values","type":"text","required":true}, + {"key":"apiUrl","label":"API URL","group":"values","type":"url","required":true}, + {"key":"apiKey","label":"API Key","group":"secrets","type":"password","required":true}, + {"key":"voice","label":"Voice","group":"values","type":"text","required":false}, + {"key":"speed","label":"Speed","group":"values","type":"number","required":false,"default":1.0}, + {"key":"sourceSampleRate","label":"Source Sample Rate","group":"values","type":"number","required":false,"default":24000} + ]} $$::jsonb, TRUE, 1), + ('openai-embedding', 'OpenAI Compatible Embedding', 'Embedding', + $$ {"fields":[ + {"key":"modelId","label":"Model ID","group":"values","type":"text","required":true}, + {"key":"apiUrl","label":"API URL","group":"values","type":"url","required":true}, + {"key":"apiKey","label":"API Key","group":"secrets","type":"password","required":true}, + {"key":"dimensions","label":"Dimensions","group":"values","type":"number","required":false} + ]} $$::jsonb, TRUE, 1), + ('openai-realtime', 'OpenAI Realtime', 'Realtime', + $$ {"fields":[ + {"key":"modelId","label":"Model ID","group":"values","type":"text","required":true}, + {"key":"apiUrl","label":"API URL","group":"values","type":"url","required":true}, + {"key":"apiKey","label":"API Key","group":"secrets","type":"password","required":true}, + {"key":"voice","label":"Voice","group":"values","type":"text","required":false} + ]} $$::jsonb, TRUE, 1), + ('stepfun-realtime', 'StepFun StepAudio Realtime', 'Realtime', + $$ {"fields":[ + {"key":"modelId","label":"Model ID","group":"values","type":"text","required":true}, + {"key":"apiUrl","label":"API URL","group":"values","type":"url","required":true}, + {"key":"apiKey","label":"API Key","group":"secrets","type":"password","required":true}, + {"key":"voice","label":"Voice","group":"values","type":"text","required":true,"default":"linjiajiejie"}, + {"key":"inputSampleRate","label":"Input Sample Rate","group":"values","type":"number","required":false,"default":24000}, + {"key":"outputSampleRate","label":"Output Sample Rate","group":"values","type":"number","required":false,"default":24000}, + {"key":"prefixPaddingMs","label":"VAD Prefix Padding (ms)","group":"values","type":"number","required":false,"default":500}, + {"key":"silenceDurationMs","label":"VAD Silence Duration (ms)","group":"values","type":"number","required":false,"default":300}, + {"key":"energyAwakenessThreshold","label":"VAD Energy Threshold","group":"values","type":"number","required":false,"default":2500} + ]} $$::jsonb, TRUE, 1), + ('xfyun-asr', 'Xfyun Streaming ASR', 'ASR', + $$ {"fields":[ + {"key":"apiUrl","label":"WebSocket URL","group":"values","type":"url","required":true}, + {"key":"appId","label":"App ID","group":"secrets","type":"password","required":true}, + {"key":"apiKey","label":"API Key","group":"secrets","type":"password","required":true}, + {"key":"apiSecret","label":"API Secret","group":"secrets","type":"password","required":true}, + {"key":"language","label":"Language","group":"values","type":"text","required":false,"default":"zh_cn"}, + {"key":"domain","label":"Domain","group":"values","type":"text","required":false,"default":"iat"}, + {"key":"accent","label":"Accent","group":"values","type":"text","required":false,"default":"mandarin"}, + {"key":"dynamicCorrection","label":"Dynamic Correction","group":"values","type":"boolean","required":false,"default":false}, + {"key":"frameSize","label":"Frame Size","group":"values","type":"number","required":false,"default":1280} + ]} $$::jsonb, TRUE, 1), + ('xfyun-tts', 'Xfyun TTS', 'TTS', + $$ {"fields":[ + {"key":"apiUrl","label":"WebSocket URL","group":"values","type":"url","required":true}, + {"key":"appId","label":"App ID","group":"secrets","type":"password","required":true}, + {"key":"apiKey","label":"API Key","group":"secrets","type":"password","required":true}, + {"key":"apiSecret","label":"API Secret","group":"secrets","type":"password","required":true}, + {"key":"voice","label":"Voice","group":"values","type":"text","required":false}, + {"key":"speed","label":"Speed","group":"values","type":"number","required":false,"default":50}, + {"key":"volume","label":"Volume","group":"values","type":"number","required":false,"default":50}, + {"key":"pitch","label":"Pitch","group":"values","type":"number","required":false,"default":50}, + {"key":"sourceSampleRate","label":"Source Sample Rate","group":"values","type":"number","required":false,"default":16000} + ]} $$::jsonb, TRUE, 1), + ('xfyun-super-tts', 'Xfyun Super TTS', 'TTS', + $$ {"fields":[ + {"key":"apiUrl","label":"WebSocket URL","group":"values","type":"url","required":true}, + {"key":"appId","label":"App ID","group":"secrets","type":"password","required":true}, + {"key":"apiKey","label":"API Key","group":"secrets","type":"password","required":true}, + {"key":"apiSecret","label":"API Secret","group":"secrets","type":"password","required":true}, + {"key":"voice","label":"Voice","group":"values","type":"text","required":false}, + {"key":"speed","label":"Speed","group":"values","type":"number","required":false,"default":50}, + {"key":"volume","label":"Volume","group":"values","type":"number","required":false,"default":50}, + {"key":"pitch","label":"Pitch","group":"values","type":"number","required":false,"default":50}, + {"key":"oralLevel","label":"Oral Level","group":"values","type":"text","required":false,"default":"mid"}, + {"key":"sourceSampleRate","label":"Source Sample Rate","group":"values","type":"number","required":false,"default":24000}, + {"key":"textAggregationMode","label":"Text Aggregation Mode","group":"values","type":"text","required":false,"default":"token"} + ]} $$::jsonb, TRUE, 1), + ('dashscope-asr', 'DashScope ASR', 'ASR', + $$ {"fields":[ + {"key":"modelId","label":"Model ID","group":"values","type":"text","required":true}, + {"key":"apiUrl","label":"API URL","group":"values","type":"url","required":true}, + {"key":"apiKey","label":"API Key","group":"secrets","type":"password","required":true}, + {"key":"language","label":"Language","group":"values","type":"text","required":false,"default":"zh"} + ]} $$::jsonb, TRUE, 1), + ('dashscope-tts', 'DashScope TTS', 'TTS', + $$ {"fields":[ + {"key":"modelId","label":"Model ID","group":"values","type":"text","required":true}, + {"key":"apiUrl","label":"API URL","group":"values","type":"url","required":true}, + {"key":"apiKey","label":"API Key","group":"secrets","type":"password","required":true}, + {"key":"voice","label":"Voice","group":"values","type":"text","required":false} + ]} $$::jsonb, TRUE, 1), + ('gemini-realtime', 'Gemini Realtime', 'Realtime', + $$ {"fields":[ + {"key":"modelId","label":"Model ID","group":"values","type":"text","required":true}, + {"key":"apiUrl","label":"API URL","group":"values","type":"url","required":true}, + {"key":"apiKey","label":"API Key","group":"secrets","type":"password","required":true} + ]} $$::jsonb, TRUE, 1) +ON CONFLICT (interface_type) DO UPDATE SET + name = EXCLUDED.name, + capability = EXCLUDED.capability, + field_schema = EXCLUDED.field_schema, + enabled = TRUE, + version = EXCLUDED.version, + updated_at = now(); diff --git a/backend/db/seed_model_resources.sql b/backend/db/seed_model_resources.sql index e5c45ec..09dae28 100644 --- a/backend/db/seed_model_resources.sql +++ b/backend/db/seed_model_resources.sql @@ -1,4 +1,4 @@ --- 模型资源种子数据。依赖应用启动时写入 interface_definitions。 +-- 模型资源种子数据。依赖 seed_interface_definitions.sql。 INSERT INTO model_resources (id, name, capability, interface_type, values, secrets, support_image_input, enabled, is_default)