Refactor backend to support interface-definition driven model resources
- Introduce a new model structure for managing interface definitions and model resources, enhancing the backend's capability to handle various service integrations. - Update the Makefile to reflect changes in database seeding and resource management commands. - Remove the deprecated credentials management routes and replace them with a unified model registry API. - Modify existing routes and schemas to align with the new model structure, ensuring seamless integration with the frontend. - Enhance database seeding scripts to populate new model resources and their configurations. - Update README documentation to reflect the new architecture and usage instructions for model resources and interface definitions.
This commit is contained in:
@@ -1,15 +1,16 @@
|
||||
"""数据表定义(SQLAlchemy 2.0)。
|
||||
|
||||
两张表,职责分离(见设计):
|
||||
- ProviderCredential:模型凭证(key 明文存,同 dograh,靠 DB 访问控制兜底;读时打码)
|
||||
- Assistant:助手配置,**只存模型/音色的"选项名",不嵌 key**
|
||||
模型注册表由接口定义驱动:
|
||||
- InterfaceDefinition:具体接入协议及其动态表单字段
|
||||
- ModelResource:模型配置与鉴权值
|
||||
- AssistantModelBinding:助手按能力选择模型资源
|
||||
|
||||
助手运行时再用 kind 去 ProviderCredential 取真 key(services/config_resolver.py)。
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import JSON, Boolean, DateTime, Float, ForeignKey, String, func
|
||||
from sqlalchemy import JSON, Boolean, DateTime, ForeignKey, String, func
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
|
||||
|
||||
@@ -17,22 +18,39 @@ class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class ProviderCredential(Base):
|
||||
"""模型资源凭证。字段对齐前端 ComponentsModelsPage 的 ModelResource。"""
|
||||
class InterfaceDefinition(Base):
|
||||
"""具体接入协议,例如 xfyun-tts 与 xfyun-super-tts。"""
|
||||
|
||||
__tablename__ = "provider_credentials"
|
||||
__tablename__ = "interface_definitions"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(40), primary_key=True) # model_xxx
|
||||
name: Mapped[str] = mapped_column(String(128), default="") # 资源名称,如 "DeepSeek-V3"
|
||||
model_id: Mapped[str] = mapped_column(String(128), default="") # 模型ID,如 "deepseek-chat"
|
||||
type: Mapped[str] = mapped_column(String(16), index=True) # LLM|ASR|TTS|Realtime|Embedding
|
||||
interface_type: Mapped[str] = mapped_column(String(32), default="openai") # openai|xfyun|dashscope|gemini
|
||||
api_url: Mapped[str] = mapped_column(String(512), default="")
|
||||
api_key: Mapped[str] = mapped_column(String(512), default="") # 明文
|
||||
voice: Mapped[str] = mapped_column(String(128), default="") # TTS 音色
|
||||
speed: Mapped[float] = mapped_column(Float, default=1.0) # TTS 语速
|
||||
language: Mapped[str] = mapped_column(String(32), default="") # ASR 语言
|
||||
# 同一 type 下的默认凭证(后端解析用;前端 ModelResource 无此字段,留作可选)
|
||||
interface_type: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(128))
|
||||
capability: Mapped[str] = mapped_column(String(16), index=True)
|
||||
field_schema: Mapped[dict] = mapped_column(JSONB, default=dict)
|
||||
enabled: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
version: Mapped[int] = mapped_column(default=1)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
|
||||
class ModelResource(Base):
|
||||
"""统一模型资源:接口类型决定能力、鉴权字段和调用参数。"""
|
||||
|
||||
__tablename__ = "model_resources"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(40), primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(128), default="")
|
||||
capability: Mapped[str] = mapped_column(String(16), index=True)
|
||||
interface_type: Mapped[str] = mapped_column(
|
||||
String(64),
|
||||
ForeignKey("interface_definitions.interface_type", ondelete="RESTRICT"),
|
||||
index=True,
|
||||
)
|
||||
values: Mapped[dict] = mapped_column(JSONB, default=dict)
|
||||
secrets: Mapped[dict] = mapped_column(JSONB, default=dict)
|
||||
enabled: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
is_default: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
@@ -41,7 +59,7 @@ class ProviderCredential(Base):
|
||||
|
||||
|
||||
class KnowledgeBase(Base):
|
||||
"""知识库注册表。本身引用一个 Embedding 凭证(用哪个向量模型)。
|
||||
"""知识库注册表。本身引用一个 Embedding 模型资源。
|
||||
|
||||
文档/分块(pgvector)是 KB 内部实现,这里先不展开;助手侧只认 knowledge_base_id。
|
||||
"""
|
||||
@@ -51,10 +69,9 @@ class KnowledgeBase(Base):
|
||||
id: Mapped[str] = mapped_column(String(40), primary_key=True) # kb_xxx
|
||||
name: Mapped[str] = mapped_column(String(128))
|
||||
description: Mapped[str] = mapped_column(String(2048), default="")
|
||||
# 该 KB 用哪个向量模型;凭证被删则置空
|
||||
embedding_credential_id: Mapped[str | None] = mapped_column(
|
||||
embedding_model_resource_id: Mapped[str | None] = mapped_column(
|
||||
String(40),
|
||||
ForeignKey("provider_credentials.id", ondelete="SET NULL"),
|
||||
ForeignKey("model_resources.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
)
|
||||
status: Mapped[str] = mapped_column(String(16), default="active") # active|archived
|
||||
@@ -80,19 +97,6 @@ class Assistant(Base):
|
||||
greeting: Mapped[str] = mapped_column(String(2048), default="")
|
||||
enable_interrupt: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
|
||||
# ---- 引用"注册好的资源":凭证被删 → SET NULL(resolver 有默认/.env 兜底) ----
|
||||
llm_credential_id: Mapped[str | None] = mapped_column(
|
||||
String(40), ForeignKey("provider_credentials.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
asr_credential_id: Mapped[str | None] = mapped_column(
|
||||
String(40), ForeignKey("provider_credentials.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
tts_credential_id: Mapped[str | None] = mapped_column(
|
||||
String(40), ForeignKey("provider_credentials.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
realtime_credential_id: Mapped[str | None] = mapped_column(
|
||||
String(40), ForeignKey("provider_credentials.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
# KB 引用:被引用时禁止删 KB(RESTRICT),无默认兜底
|
||||
knowledge_base_id: Mapped[str | None] = mapped_column(
|
||||
String(40), ForeignKey("knowledge_bases.id", ondelete="RESTRICT"), nullable=True
|
||||
@@ -101,7 +105,7 @@ class Assistant(Base):
|
||||
# ---- 瘦类型专属字段(真列,稀疏:按 type 用其中几列) ----
|
||||
prompt: Mapped[str] = mapped_column(String(8192), default="") # prompt / opencode
|
||||
api_url: Mapped[str] = mapped_column(String(512), default="") # dify / fastgpt / opencode
|
||||
api_key: Mapped[str] = mapped_column(String(512), default="") # dify / fastgpt / opencode(打码/哨兵,同凭证)
|
||||
api_key: Mapped[str] = mapped_column(String(512), default="") # dify / fastgpt / opencode(打码/哨兵)
|
||||
app_id: Mapped[str] = mapped_column(String(128), default="") # fastgpt
|
||||
# workflow 专属:图(nodes/edges)。要版本化时再迁出到 assistant_workflow 表
|
||||
graph: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||
@@ -110,3 +114,26 @@ class Assistant(Base):
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
|
||||
class AssistantModelBinding(Base):
|
||||
"""助手按能力绑定统一模型资源;config 可覆盖资源默认 options。"""
|
||||
|
||||
__tablename__ = "assistant_model_bindings"
|
||||
|
||||
assistant_id: Mapped[str] = mapped_column(
|
||||
String(40),
|
||||
ForeignKey("assistants.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
)
|
||||
capability: Mapped[str] = mapped_column(String(16), primary_key=True)
|
||||
model_resource_id: Mapped[str] = mapped_column(
|
||||
String(40),
|
||||
ForeignKey("model_resources.id", ondelete="RESTRICT"),
|
||||
index=True,
|
||||
)
|
||||
config: Mapped[dict] = mapped_column(JSONB, default=dict)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
@@ -1,51 +1,42 @@
|
||||
-- 知识库 + 助手种子数据(依赖 seed_credentials.sql 先灌入 model_xxx 凭证)。
|
||||
--
|
||||
-- 用法(从仓库根目录):
|
||||
-- docker compose exec -T postgres psql -U postgres -d postgres < backend/db/seed_assistants.sql
|
||||
-- 或:make db-seed-assistants(凭证已就绪时);make db-seed 会按顺序全灌。
|
||||
--
|
||||
-- 说明:
|
||||
-- * id 固定(kb_001 / asst_001..005)+ ON CONFLICT 幂等,可重复执行。
|
||||
-- * 引用 seed_credentials 的 model_001(LLM)/003(ASR)/005(TTS)/010(Embedding)。
|
||||
-- * 宽表 STI:瘦类型用真列(prompt/api_url/api_key/app_id),workflow 用 graph 列。
|
||||
-- * api_key 在库里明文(读取走 API 才打码),这里填示例占位。
|
||||
-- 知识库 + 助手种子数据。依赖 seed_model_resources.sql。
|
||||
|
||||
-- 知识库(引用 Embedding 凭证 model_010)
|
||||
INSERT INTO knowledge_bases (id, name, description, embedding_credential_id, status)
|
||||
INSERT INTO knowledge_bases
|
||||
(id, name, description, embedding_model_resource_id, status)
|
||||
VALUES
|
||||
('kb_001', '政务政策知识库', '政策解读 / 办事指南示例库', 'model_010', 'active')
|
||||
('kb_001', '政务政策知识库', '政策解读 / 办事指南示例库', 'model_003', 'active')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 助手(一种类型一条)
|
||||
INSERT INTO assistants (
|
||||
id, name, type, runtime_mode, greeting, enable_interrupt,
|
||||
llm_credential_id, asr_credential_id, tts_credential_id,
|
||||
realtime_credential_id, knowledge_base_id,
|
||||
prompt, api_url, api_key, app_id, graph
|
||||
knowledge_base_id, prompt, api_url, api_key, app_id, graph
|
||||
) VALUES
|
||||
-- 提示词:llm/asr/tts + 知识库,prompt 真列
|
||||
('asst_001', '政务咨询助手', 'prompt', 'pipeline', '您好,我是政务助手,请问有什么可以帮您?', TRUE,
|
||||
'model_001', 'model_003', 'model_005', NULL, 'kb_001',
|
||||
'你是一名专业的政务咨询助手,回答准确、简洁,不编造政策内容。', '', '', '', '{}'),
|
||||
|
||||
-- 工作流:asr/tts + graph 列(最小图)
|
||||
'kb_001', '你是一名专业的政务咨询助手,回答准确、简洁,不编造政策内容。', '', '', '', '{}'),
|
||||
('asst_002', '热线工单助手', 'workflow', 'pipeline', '', TRUE,
|
||||
NULL, 'model_003', 'model_005', NULL, NULL,
|
||||
'', '', '', '',
|
||||
NULL, '', '', '', '',
|
||||
'{"nodes":[{"id":"1","type":"startCall","position":{"x":0,"y":0},"data":{"name":"开场","prompt":"你好,请问需要办理什么业务?"}}],"edges":[]}'),
|
||||
|
||||
-- Dify:asr/tts + api_url/api_key
|
||||
('asst_003', 'Dify 客服助手', 'dify', 'pipeline', '', TRUE,
|
||||
NULL, 'model_003', 'model_005', NULL, NULL,
|
||||
'', 'https://api.dify.ai/v1', 'app-dify-demo-key', '', '{}'),
|
||||
|
||||
-- FastGPT:asr/tts + app_id/api_url/api_key
|
||||
NULL, '', 'https://api.dify.ai/v1', 'app-dify-demo-key', '', '{}'),
|
||||
('asst_004', 'FastGPT 售后助手', 'fastgpt', 'pipeline', '', TRUE,
|
||||
NULL, 'model_003', 'model_005', NULL, NULL,
|
||||
'', 'https://api.fastgpt.in/api/v1/chat/completions', 'fastgpt-demo-key', 'app-fastgpt-001', '{}'),
|
||||
|
||||
-- OpenCode:asr/tts + prompt/api_url/api_key
|
||||
NULL, '', 'https://api.fastgpt.in/api/v1/chat/completions', 'fastgpt-demo-key', 'app-fastgpt-001', '{}'),
|
||||
('asst_005', 'OpenCode 代码助手', 'opencode', 'pipeline', '', TRUE,
|
||||
NULL, 'model_003', 'model_005', NULL, NULL,
|
||||
'你是一个代码助手的语音界面,用简洁口语回答工程问题。', 'http://localhost:4096', 'opencode-demo-key', '', '{}')
|
||||
NULL, '你是一个代码助手的语音界面,用简洁口语回答工程问题。', 'http://localhost:4096', 'opencode-demo-key', '', '{}')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO assistant_model_bindings
|
||||
(assistant_id, capability, model_resource_id, config)
|
||||
VALUES
|
||||
('asst_001', 'LLM', 'model_001', '{}'),
|
||||
('asst_001', 'ASR', 'model_002', '{}'),
|
||||
('asst_001', 'TTS', 'model_004', '{}'),
|
||||
('asst_002', 'ASR', 'model_002', '{}'),
|
||||
('asst_002', 'TTS', 'model_004', '{}'),
|
||||
('asst_003', 'ASR', 'model_002', '{}'),
|
||||
('asst_003', 'TTS', 'model_004', '{}'),
|
||||
('asst_004', 'ASR', 'model_002', '{}'),
|
||||
('asst_004', 'TTS', 'model_004', '{}'),
|
||||
('asst_005', 'ASR', 'model_002', '{}'),
|
||||
('asst_005', 'TTS', 'model_004', '{}')
|
||||
ON CONFLICT (assistant_id, capability) DO UPDATE SET
|
||||
model_resource_id = EXCLUDED.model_resource_id,
|
||||
updated_at = now();
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
-- 模型凭证种子数据(对应前端原 mockModels 的 12 条)。
|
||||
--
|
||||
-- 用法(从仓库根目录):
|
||||
-- docker compose exec -T postgres psql -U postgres -d postgres < backend/db/seed_credentials.sql
|
||||
--
|
||||
-- 说明:
|
||||
-- * id 固定为 model_001..012,配合 ON CONFLICT 做幂等更新。
|
||||
-- * api_key 在库里是明文(读取走 API 时才打码),这里填的是占位示例 key。
|
||||
-- * 每种 type 选第一条置为默认(is_default),供后端 config_resolver 解析使用。
|
||||
-- * TTS 使用 voice/speed;ASR 使用 language;其他类型保持空值/default。
|
||||
|
||||
INSERT INTO provider_credentials
|
||||
(id, name, model_id, type, interface_type, api_url, api_key, voice, speed, language, is_default)
|
||||
VALUES
|
||||
('model_001', 'DeepSeek-Chat', 'deepseek-chat', 'LLM', 'openai', 'https://api.deepseek.com/v1', 'sk-230701ff1b6143ecbf322b3170606016', '', 1.0, '', TRUE),
|
||||
('model_002', 'SiliconFlow-TeleSpeechASR', 'TeleAI/TeleSpeechASR', 'ASR', 'openai', 'https://api.siliconflow.cn/v1', 'sk-uudpgflahqqjbofhgcbwjjefgwhvwwmxgeyehcueqlemwavq', '', 1.0, 'zh', FALSE),
|
||||
('model_003', 'SiliconFlow-Qwen3-Embedding-4B', 'Qwen/Qwen3-Embedding-4B', 'Embedding', 'openai', 'https://api.siliconflow.cn/v1', 'sk-uudpgflahqqjbofhgcbwjjefgwhvwwmxgeyehcueqlemwavq', '', 1.0, '', TRUE),
|
||||
('model_004', 'SiliconFlow-CosyVoice2-0.5B', 'FunAudioLLM/CosyVoice2-0.5B', 'TTS', 'openai', 'https://api.siliconflow.cn/v1', 'sk-uudpgflahqqjbofhgcbwjjefgwhvwwmxgeyehcueqlemwavq', 'FunAudioLLM/CosyVoice2-0.5B:anna', 1.0, '', FALSE),
|
||||
('model_005', 'Qwen-Max', 'qwen-max', 'LLM', 'openai', 'https://dashscope.aliyuncs.com/compatible-mode/v1', 'sk-qwen-4d8e2a6f0c', '', 1.0, '', FALSE),
|
||||
('model_006', '讯飞语音识别', 'iat', 'ASR', 'xfyun', 'https://iat-api.xfyun.cn/v2/iat', '{"appId":"replace-me","apiKey":"replace-me","apiSecret":"replace-me"}', '', 1.0, 'zh', TRUE),
|
||||
('model_007', 'Paraformer 识别', 'paraformer-realtime-v2', 'ASR', 'dashscope', 'https://dashscope.aliyuncs.com/api/v1/services/audio/asr', 'sk-paraformer-2e4f6a', '', 1.0, 'zh', FALSE),
|
||||
('model_008', '讯飞语音合成', 'tts', 'TTS', 'xfyun', 'https://tts-api.xfyun.cn/v2/tts', '{"appId":"replace-me","apiKey":"replace-me","apiSecret":"replace-me"}', 'xiaoyan', 1.0, '', TRUE),
|
||||
('model_009', 'CosyVoice 合成', 'cosyvoice-v1', 'TTS', 'dashscope', 'https://dashscope.aliyuncs.com/api/v1/services/audio/tts', 'sk-cosyvoice-1a3c5e', 'longxiaochun', 1.0, '', FALSE),
|
||||
('model_010', 'GPT Realtime', 'gpt-4o-realtime-preview', 'Realtime', 'openai', 'https://api.openai.com/v1/realtime', 'sk-realtime-3b5d7f9a1c', '', 1.0, '', TRUE),
|
||||
('model_011', 'Gemini Live', 'gemini-2.0-flash-live', 'Realtime', 'gemini', 'https://generativelanguage.googleapis.com/v1beta', 'gm-live-5e7a9c1b3d', '', 1.0, '', FALSE),
|
||||
('model_012', 'text-embedding-3', 'text-embedding-3-small', 'Embedding', 'openai', 'https://api.openai.com/v1/embeddings', 'sk-embed-0c2e4a6f8b', '', 1.0, '', FALSE)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
model_id = EXCLUDED.model_id,
|
||||
type = EXCLUDED.type,
|
||||
interface_type = EXCLUDED.interface_type,
|
||||
api_url = EXCLUDED.api_url,
|
||||
api_key = EXCLUDED.api_key,
|
||||
voice = EXCLUDED.voice,
|
||||
speed = EXCLUDED.speed,
|
||||
language = EXCLUDED.language,
|
||||
is_default = EXCLUDED.is_default,
|
||||
updated_at = now();
|
||||
48
backend/db/seed_model_resources.sql
Normal file
48
backend/db/seed_model_resources.sql
Normal file
@@ -0,0 +1,48 @@
|
||||
-- 模型资源种子数据。依赖应用启动时写入 interface_definitions。
|
||||
|
||||
INSERT INTO model_resources
|
||||
(id, name, capability, interface_type, values, secrets, enabled, is_default)
|
||||
VALUES
|
||||
('model_001', 'DeepSeek-Chat', 'LLM', 'openai-llm',
|
||||
'{"modelId":"deepseek-chat","apiUrl":"https://api.deepseek.com/v1","temperature":0.7}',
|
||||
'{"apiKey":"replace-me"}', TRUE, TRUE),
|
||||
('model_002', 'SiliconFlow-TeleSpeechASR', 'ASR', 'openai-asr',
|
||||
'{"modelId":"TeleAI/TeleSpeechASR","apiUrl":"https://api.siliconflow.cn/v1","language":"zh"}',
|
||||
'{"apiKey":"replace-me"}', TRUE, FALSE),
|
||||
('model_003', 'SiliconFlow-Qwen3-Embedding-4B', 'Embedding', 'openai-embedding',
|
||||
'{"modelId":"Qwen/Qwen3-Embedding-4B","apiUrl":"https://api.siliconflow.cn/v1"}',
|
||||
'{"apiKey":"replace-me"}', TRUE, TRUE),
|
||||
('model_004', 'SiliconFlow-CosyVoice2-0.5B', 'TTS', 'openai-tts',
|
||||
'{"modelId":"FunAudioLLM/CosyVoice2-0.5B","apiUrl":"https://api.siliconflow.cn/v1","voice":"FunAudioLLM/CosyVoice2-0.5B:anna","speed":1.0,"sourceSampleRate":24000}',
|
||||
'{"apiKey":"replace-me"}', TRUE, FALSE),
|
||||
'{"apiKey":"replace-me"}', TRUE, FALSE),
|
||||
('model_005', '讯飞语音识别', 'ASR', 'xfyun-asr',
|
||||
'{"apiUrl":"https://iat-api.xfyun.cn/v2/iat","language":"zh_cn","domain":"iat","accent":"mandarin","dynamicCorrection":false,"frameSize":1280}',
|
||||
'{"appId":"replace-me","apiKey":"replace-me","apiSecret":"replace-me"}', TRUE, TRUE),
|
||||
('model_006', 'Paraformer 识别', 'ASR', 'dashscope-asr',
|
||||
'{"modelId":"paraformer-realtime-v2","apiUrl":"https://dashscope.aliyuncs.com/api/v1/services/audio/asr","language":"zh"}',
|
||||
'{"apiKey":"replace-me"}', TRUE, FALSE),
|
||||
('model_007', '讯飞语音合成', 'TTS', 'xfyun-tts',
|
||||
'{"apiUrl":"https://tts-api.xfyun.cn/v2/tts","voice":"xiaoyan","speed":50,"volume":50,"pitch":50,"sourceSampleRate":16000}',
|
||||
'{"appId":"replace-me","apiKey":"replace-me","apiSecret":"replace-me"}', TRUE, TRUE),
|
||||
('model_008', 'CosyVoice 合成', 'TTS', 'dashscope-tts',
|
||||
'{"modelId":"cosyvoice-v1","apiUrl":"https://dashscope.aliyuncs.com/api/v1/services/audio/tts","voice":"longxiaochun"}',
|
||||
'{"apiKey":"replace-me"}', TRUE, FALSE),
|
||||
('model_009', 'GPT Realtime', 'Realtime', 'openai-realtime',
|
||||
'{"modelId":"gpt-4o-realtime-preview","apiUrl":"https://api.openai.com/v1/realtime"}',
|
||||
'{"apiKey":"replace-me"}', TRUE, TRUE),
|
||||
('model_010', 'Gemini Live', 'Realtime', 'gemini-realtime',
|
||||
'{"modelId":"gemini-2.0-flash-live","apiUrl":"https://generativelanguage.googleapis.com/v1beta"}',
|
||||
'{"apiKey":"replace-me"}', TRUE, FALSE),
|
||||
('model_011', 'text-embedding-3', 'Embedding', 'openai-embedding',
|
||||
'{"modelId":"text-embedding-3-small","apiUrl":"https://api.openai.com/v1/embeddings"}',
|
||||
'{"apiKey":"replace-me"}', TRUE, FALSE)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
capability = EXCLUDED.capability,
|
||||
interface_type = EXCLUDED.interface_type,
|
||||
values = EXCLUDED.values,
|
||||
secrets = EXCLUDED.secrets,
|
||||
enabled = EXCLUDED.enabled,
|
||||
is_default = EXCLUDED.is_default,
|
||||
updated_at = now();
|
||||
@@ -6,9 +6,11 @@
|
||||
"""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
import json
|
||||
|
||||
import config
|
||||
from db.models import Base
|
||||
from services.interface_catalog import INTERFACE_DEFINITIONS
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import (
|
||||
AsyncSession,
|
||||
@@ -28,22 +30,26 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
||||
async def init_db() -> None:
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
# MVP 兼容迁移:create_all 不会给已存在的表补列。
|
||||
await conn.execute(
|
||||
text(
|
||||
"ALTER TABLE provider_credentials "
|
||||
"ADD COLUMN IF NOT EXISTS voice VARCHAR(128) NOT NULL DEFAULT ''"
|
||||
"ALTER TABLE interface_definitions "
|
||||
"ALTER COLUMN field_schema TYPE JSONB USING field_schema::jsonb"
|
||||
)
|
||||
)
|
||||
await conn.execute(
|
||||
text(
|
||||
"ALTER TABLE provider_credentials "
|
||||
"ADD COLUMN IF NOT EXISTS speed DOUBLE PRECISION NOT NULL DEFAULT 1.0"
|
||||
for definition in INTERFACE_DEFINITIONS:
|
||||
await conn.execute(
|
||||
text(
|
||||
"INSERT INTO interface_definitions "
|
||||
"(interface_type, name, capability, field_schema, enabled, version) "
|
||||
"VALUES (:interface_type, :name, :capability, CAST(:field_schema AS jsonb), TRUE, 1) "
|
||||
"ON CONFLICT (interface_type) DO UPDATE SET "
|
||||
"name = EXCLUDED.name, capability = EXCLUDED.capability, "
|
||||
"field_schema = EXCLUDED.field_schema, enabled = TRUE, updated_at = now()"
|
||||
),
|
||||
{
|
||||
"interface_type": definition["interface_type"],
|
||||
"name": definition["name"],
|
||||
"capability": definition["capability"],
|
||||
"field_schema": json.dumps({"fields": definition["fields"]}),
|
||||
},
|
||||
)
|
||||
)
|
||||
await conn.execute(
|
||||
text(
|
||||
"ALTER TABLE provider_credentials "
|
||||
"ADD COLUMN IF NOT EXISTS language VARCHAR(32) NOT NULL DEFAULT ''"
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user