Browse Source

启动问题修复

master
MSI-7950X\刘泽明 6 hours ago
parent
commit
139e7c390a
  1. 275
      backend/database.py
  2. 2
      backend/dependencies.py
  3. 2
      backend/main.py
  4. 154
      backend/models/__init__.py
  5. 18
      backend/modules/audit/router.py
  6. 2
      backend/modules/memory/manager.py
  7. 1
      frontend/src/components/layout/AdminLayout.vue
  8. 12
      frontend/src/router/index.ts
  9. 4
      frontend/src/views/dashboard/Dashboard.vue
  10. 10
      frontend/src/views/flow/FlowEditor.vue
  11. 110
      frontend/src/views/flow/node-configs/LlmConfig.vue
  12. 12
      frontend/src/views/flow/node-configs/TemplateTransformConfig.vue
  13. 858
      frontend/src/views/model/ModelProviderManager.vue

275
backend/database.py

@ -37,14 +37,287 @@ async def init_db():
async def _run_migrations():
"""执行数据库增量迁移,在已有表上安全添加新字段。"""
"""执行数据库增量迁移:创建缺失的表并安全添加新字段。"""
async with async_engine.begin() as conn:
# --- 创建可能缺失的新表 ---
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS agent_configs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(200) NOT NULL,
description TEXT,
system_prompt TEXT,
model VARCHAR(100) DEFAULT '',
model_instance_id UUID,
embedding_model_id UUID,
temperature FLOAT DEFAULT 0.7,
tools JSONB DEFAULT '[]',
status VARCHAR(20) DEFAULT 'active',
creator_id UUID REFERENCES users(id),
created_at TIMESTAMP DEFAULT now(),
updated_at TIMESTAMP DEFAULT now()
)
"""))
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS model_providers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
provider_type VARCHAR(50) NOT NULL,
base_url VARCHAR(500),
api_key VARCHAR(500),
extra_config JSONB DEFAULT '{}',
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT now()
)
"""))
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS model_instances (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
provider_id UUID NOT NULL REFERENCES model_providers(id) ON DELETE CASCADE,
model_name VARCHAR(200) NOT NULL,
model_type VARCHAR(20) NOT NULL,
display_name VARCHAR(200),
capabilities JSONB DEFAULT '{}',
default_params JSONB DEFAULT '{}',
is_default BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT now()
)
"""))
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS custom_tools (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
schema_json JSONB NOT NULL DEFAULT '{}',
endpoint_url VARCHAR(500) DEFAULT '',
method VARCHAR(10) DEFAULT 'GET',
path VARCHAR(500) DEFAULT '',
headers_json JSONB DEFAULT '{}',
auth_type VARCHAR(20) DEFAULT 'none',
auth_config JSONB DEFAULT '{}',
is_active BOOLEAN DEFAULT TRUE,
created_by UUID REFERENCES users(id),
created_at TIMESTAMP DEFAULT now(),
updated_at TIMESTAMP DEFAULT now()
)
"""))
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS mcp_services (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
endpoint_url VARCHAR(500) NOT NULL,
status VARCHAR(20) DEFAULT 'active',
config JSONB DEFAULT '{}',
last_heartbeat TIMESTAMP,
created_at TIMESTAMP DEFAULT now(),
updated_at TIMESTAMP DEFAULT now()
)
"""))
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
action VARCHAR(50) NOT NULL,
resource_type VARCHAR(50) NOT NULL,
resource_id VARCHAR(100),
detail JSONB DEFAULT '{}',
ip_address VARCHAR(50),
user_agent VARCHAR(500),
created_at TIMESTAMP DEFAULT now()
)
"""))
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS notification_templates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) UNIQUE NOT NULL,
channel VARCHAR(20) NOT NULL,
event_type VARCHAR(50) NOT NULL,
subject_template VARCHAR(500),
body_template TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT now(),
updated_at TIMESTAMP DEFAULT now()
)
"""))
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS system_metrics (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
metric_name VARCHAR(50) NOT NULL,
metric_value FLOAT,
tags JSONB DEFAULT '{}',
recorded_at TIMESTAMP DEFAULT now()
)
"""))
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS flow_executions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
flow_id UUID NOT NULL REFERENCES flow_definitions(id),
session_id VARCHAR(100),
status VARCHAR(20) DEFAULT 'running',
input_data JSONB DEFAULT '{}',
output_data JSONB DEFAULT '{}',
error_message TEXT,
started_by UUID REFERENCES users(id),
started_at TIMESTAMP DEFAULT now(),
finished_at TIMESTAMP,
duration_ms INTEGER
)
"""))
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS memory_messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
flow_execution_id UUID NOT NULL REFERENCES flow_executions(id) ON DELETE CASCADE,
node_id VARCHAR(100) NOT NULL,
role VARCHAR(20) NOT NULL,
content TEXT NOT NULL,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT now()
)
"""))
# --- 为已有表添加缺失列 ---
await conn.execute(text(
"ALTER TABLE flow_definitions ADD COLUMN IF NOT EXISTS published_version_id UUID REFERENCES flow_versions(id)"
))
await conn.execute(text(
"ALTER TABLE flow_definitions ADD COLUMN IF NOT EXISTS draft_definition_json JSONB"
))
await conn.execute(text(
"ALTER TABLE flow_definitions ADD COLUMN IF NOT EXISTS flow_mode VARCHAR(20) DEFAULT 'chatflow'"
))
await conn.execute(text(
"ALTER TABLE flow_definitions ADD COLUMN IF NOT EXISTS published_to_wecom BOOLEAN DEFAULT FALSE"
))
await conn.execute(text(
"ALTER TABLE flow_definitions ADD COLUMN IF NOT EXISTS published_to_web BOOLEAN DEFAULT FALSE"
))
await conn.execute(text(
"ALTER TABLE agent_configs ADD COLUMN IF NOT EXISTS model_instance_id UUID"
))
await conn.execute(text(
"ALTER TABLE agent_configs ADD COLUMN IF NOT EXISTS embedding_model_id UUID"
))
# mcp_services 表可能存在旧版本,补齐所有缺失列
await conn.execute(text(
"ALTER TABLE mcp_services ADD COLUMN IF NOT EXISTS name VARCHAR(100) NOT NULL DEFAULT ''"
))
await conn.execute(text(
"ALTER TABLE mcp_services ADD COLUMN IF NOT EXISTS endpoint_url VARCHAR(500) NOT NULL DEFAULT ''"
))
await conn.execute(text(
"ALTER TABLE mcp_services ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'active'"
))
await conn.execute(text(
"ALTER TABLE mcp_services ADD COLUMN IF NOT EXISTS config JSONB DEFAULT '{}'"
))
await conn.execute(text(
"ALTER TABLE mcp_services ADD COLUMN IF NOT EXISTS last_heartbeat TIMESTAMP"
))
await conn.execute(text(
"ALTER TABLE mcp_services ADD COLUMN IF NOT EXISTS created_at TIMESTAMP DEFAULT now()"
))
await conn.execute(text(
"ALTER TABLE mcp_services ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT now()"
))
# --- 其他新表的列级迁移(防止旧版本表缺少字段) ---
# model_providers
for col_sql in [
"name VARCHAR(100) NOT NULL DEFAULT ''",
"provider_type VARCHAR(50) NOT NULL DEFAULT 'openai_compatible'",
"base_url VARCHAR(500) DEFAULT ''",
"api_key VARCHAR(500) DEFAULT ''",
"extra_config JSONB DEFAULT '{}'",
"is_active BOOLEAN DEFAULT TRUE",
"created_at TIMESTAMP DEFAULT now()",
]:
await conn.execute(text(f"ALTER TABLE model_providers ADD COLUMN IF NOT EXISTS {col_sql}"))
# model_instances
for col_sql in [
"id UUID PRIMARY KEY DEFAULT gen_random_uuid()",
"provider_id UUID NOT NULL REFERENCES model_providers(id) ON DELETE CASCADE",
"model_name VARCHAR(200) NOT NULL DEFAULT ''",
"model_type VARCHAR(20) NOT NULL DEFAULT 'llm'",
"display_name VARCHAR(200) DEFAULT ''",
"capabilities JSONB DEFAULT '{}'",
"default_params JSONB DEFAULT '{}'",
"is_default BOOLEAN DEFAULT FALSE",
"is_active BOOLEAN DEFAULT TRUE",
"created_at TIMESTAMP DEFAULT now()",
]:
await conn.execute(text(f"ALTER TABLE model_instances ADD COLUMN IF NOT EXISTS {col_sql}"))
# custom_tools
for col_sql in [
"name VARCHAR(100) UNIQUE NOT NULL DEFAULT ''",
"description TEXT",
"schema_json JSONB NOT NULL DEFAULT '{}'",
"endpoint_url VARCHAR(500) DEFAULT ''",
"method VARCHAR(10) DEFAULT 'GET'",
"path VARCHAR(500) DEFAULT ''",
"headers_json JSONB DEFAULT '{}'",
"auth_type VARCHAR(20) DEFAULT 'none'",
"auth_config JSONB DEFAULT '{}'",
"is_active BOOLEAN DEFAULT TRUE",
"created_by UUID REFERENCES users(id)",
"created_at TIMESTAMP DEFAULT now()",
"updated_at TIMESTAMP DEFAULT now()",
]:
await conn.execute(text(f"ALTER TABLE custom_tools ADD COLUMN IF NOT EXISTS {col_sql}"))
# audit_logs
for col_sql in [
"user_id UUID REFERENCES users(id)",
"action VARCHAR(50) NOT NULL DEFAULT ''",
"resource_type VARCHAR(50) NOT NULL DEFAULT ''",
"resource_id VARCHAR(100)",
"detail JSONB DEFAULT '{}'",
"ip_address VARCHAR(50)",
"user_agent VARCHAR(500)",
"created_at TIMESTAMP DEFAULT now()",
]:
await conn.execute(text(f"ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS {col_sql}"))
# notification_templates
for col_sql in [
"name VARCHAR(100) UNIQUE NOT NULL DEFAULT ''",
"channel VARCHAR(20) NOT NULL DEFAULT ''",
"event_type VARCHAR(50) NOT NULL DEFAULT ''",
"subject_template VARCHAR(500)",
"body_template TEXT",
"is_active BOOLEAN DEFAULT TRUE",
"created_at TIMESTAMP DEFAULT now()",
"updated_at TIMESTAMP DEFAULT now()",
]:
await conn.execute(text(f"ALTER TABLE notification_templates ADD COLUMN IF NOT EXISTS {col_sql}"))
# system_metrics
for col_sql in [
"metric_name VARCHAR(50) NOT NULL DEFAULT ''",
"metric_value FLOAT",
"tags JSONB DEFAULT '{}'",
"recorded_at TIMESTAMP DEFAULT now()",
]:
await conn.execute(text(f"ALTER TABLE system_metrics ADD COLUMN IF NOT EXISTS {col_sql}"))
# flow_executions
for col_sql in [
"flow_id UUID NOT NULL REFERENCES flow_definitions(id)",
"session_id VARCHAR(100)",
"status VARCHAR(20) DEFAULT 'running'",
"input_data JSONB DEFAULT '{}'",
"output_data JSONB DEFAULT '{}'",
"error_message TEXT",
"started_by UUID REFERENCES users(id)",
"started_at TIMESTAMP DEFAULT now()",
"finished_at TIMESTAMP",
"duration_ms INTEGER",
]:
await conn.execute(text(f"ALTER TABLE flow_executions ADD COLUMN IF NOT EXISTS {col_sql}"))
# memory_messages
for col_sql in [
"flow_execution_id UUID NOT NULL REFERENCES flow_executions(id) ON DELETE CASCADE",
"node_id VARCHAR(100) NOT NULL DEFAULT ''",
"role VARCHAR(20) NOT NULL DEFAULT 'user'",
"content TEXT NOT NULL DEFAULT ''",
"metadata JSONB DEFAULT '{}'",
"created_at TIMESTAMP DEFAULT now()",
]:
await conn.execute(text(f"ALTER TABLE memory_messages ADD COLUMN IF NOT EXISTS {col_sql}"))
async def get_db():

2
backend/dependencies.py

@ -25,7 +25,7 @@ async def get_current_user(
if credentials:
try:
payload = jwt.decode(
credentials=credentials.credentials,
credentials.credentials,
settings.JWT_SECRET,
algorithms=[settings.JWT_ALGORITHM],
)

2
backend/main.py

@ -5,7 +5,6 @@ from modules.auth.router import router as auth_router
from modules.org.router import router as org_router
from modules.rbac.router import router as rbac_router
from modules.wecom.router import router as wecom_router
from modules.agent_manager.router import router as agent_manager_router
from modules.task.router import router as task_router
from modules.monitor.router import router as monitor_router
from modules.mcp_registry.router import router as mcp_router
@ -61,7 +60,6 @@ app.include_router(auth_router) # 认证模块
app.include_router(org_router) # 组织架构模块
app.include_router(rbac_router) # 权限管理模块
app.include_router(wecom_router) # 企业微信模块
app.include_router(agent_manager_router) # 智能体管理模块
app.include_router(task_router) # 任务管理模块
app.include_router(monitor_router) # 监控模块
app.include_router(mcp_router) # MCP 服务注册模块

154
backend/models/__init__.py

@ -211,3 +211,157 @@ class FlowTemplate(Base):
created_by = Column(UUID(as_uuid=True), ForeignKey("users.id")) # 创建者用户 ID
created_at = Column(DateTime, default=datetime.utcnow) # 记录创建时间
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # 记录更新时间
class AgentConfig(Base):
"""智能体配置表 (agent_configs),存储 AI 智能体的配置参数。"""
__tablename__ = "agent_configs"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) # 配置唯一标识 UUID
name = Column(String(200), nullable=False) # 智能体名称
description = Column(Text) # 智能体描述
system_prompt = Column(Text) # 系统提示词
model = Column(String(100), default="") # 默认模型名称
model_instance_id = Column(UUID(as_uuid=True), nullable=True) # 关联的模型实例 ID
embedding_model_id = Column(UUID(as_uuid=True), nullable=True) # 关联的嵌入模型 ID
temperature = Column(Float, default=0.7) # 温度参数
tools = Column(JSON, default=list) # 可用工具列表
status = Column(String(20), default="active") # 状态:active/inactive
creator_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) # 创建者用户 ID
created_at = Column(DateTime, default=datetime.utcnow) # 记录创建时间
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # 记录更新时间
class ModelProvider(Base):
"""模型供应商表 (model_providers),存储 LLM/Embedding/Rerank 模型供应商的接入配置。"""
__tablename__ = "model_providers"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) # 供应商唯一标识 UUID
name = Column(String(100), nullable=False) # 供应商名称(如 OpenAI、智谱AI)
provider_type = Column(String(50), nullable=False) # 供应商类型:openai_compatible/openai/zhipu/ollama/deepseek
base_url = Column(String(500)) # API 基础地址
api_key = Column(String(500)) # API 密钥(加密存储)
extra_config = Column(JSON, default=dict) # 额外配置(如 region、project 等)
is_active = Column(Boolean, default=True) # 是否启用
created_at = Column(DateTime, default=datetime.utcnow) # 记录创建时间
class ModelInstance(Base):
"""模型实例表 (model_instances),存储供应商下的具体模型实例。支持 LLM/Embedding/Rerank 三种类型。"""
__tablename__ = "model_instances"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) # 实例唯一标识 UUID
provider_id = Column(UUID(as_uuid=True), ForeignKey("model_providers.id", ondelete="CASCADE"), nullable=False) # 所属供应商 ID
model_name = Column(String(200), nullable=False) # 模型标识名(如 gpt-4o、text-embedding-3-small、bge-reranker-v2-m3)
model_type = Column(String(20), nullable=False) # 模型类型:llm/embedding/rerank
display_name = Column(String(200)) # 显示名称(如 GPT-4o、Embedding-3-Small、BGE Reranker)
capabilities = Column(JSON, default=dict) # 能力配置(LLM: vision/function_calling; Embedding/Rerank: dimension/max_tokens)
default_params = Column(JSON, default=dict) # 默认参数(如 temperature、max_tokens、dimension)
is_default = Column(Boolean, default=False) # 是否为该类型下的默认模型
is_active = Column(Boolean, default=True) # 是否启用
created_at = Column(DateTime, default=datetime.utcnow) # 记录创建时间
class CustomTool(Base):
"""自定义工具表 (custom_tools),存储用户自定义的 HTTP 工具定义。"""
__tablename__ = "custom_tools"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) # 工具唯一标识 UUID
name = Column(String(100), unique=True, nullable=False) # 工具名称(唯一)
description = Column(Text) # 工具描述
schema_json = Column(JSON, nullable=False, default=dict) # 参数 Schema 定义 JSON
endpoint_url = Column(String(500), default="") # API 端点 URL
method = Column(String(10), default="GET") # HTTP 方法
path = Column(String(500), default="") # API 路径
headers_json = Column(JSON, default=dict) # 自定义请求头
auth_type = Column(String(20), default="none") # 认证类型:none/api_key/bearer
auth_config = Column(JSON, default=dict) # 认证配置信息
is_active = Column(Boolean, default=True) # 是否启用(软删除标记)
created_by = Column(UUID(as_uuid=True), ForeignKey("users.id")) # 创建者用户 ID
created_at = Column(DateTime, default=datetime.utcnow) # 记录创建时间
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # 记录更新时间
class MCPService(Base):
"""MCP 服务注册表 (mcp_services),存储 Model Context Protocol 服务的注册信息。"""
__tablename__ = "mcp_services"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) # 服务唯一标识 UUID
name = Column(String(100), nullable=False) # 服务名称
endpoint_url = Column(String(500), nullable=False) # MCP 服务端点 URL
status = Column(String(20), default="active") # 服务状态:active/inactive/error
config = Column(JSON, default=dict) # 服务配置信息
last_heartbeat = Column(DateTime) # 最后心跳时间
created_at = Column(DateTime, default=datetime.utcnow) # 记录创建时间
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # 记录更新时间
class AuditLog(Base):
"""审计日志表 (audit_logs),记录系统关键操作的审计追踪日志。"""
__tablename__ = "audit_logs"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) # 日志唯一标识 UUID
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True) # 操作用户 ID
action = Column(String(50), nullable=False) # 操作类型(create/update/delete/login/export)
resource_type = Column(String(50), nullable=False) # 资源类型(user/agent/flow/document/task)
resource_id = Column(String(100)) # 资源 ID
detail = Column(JSON, default=dict) # 操作详情 JSON
ip_address = Column(String(50)) # 操作来源 IP
user_agent = Column(String(500)) # 客户端 User-Agent
created_at = Column(DateTime, default=datetime.utcnow) # 日志记录时间
class NotificationTemplate(Base):
"""通知模板表 (notification_templates),存储企业微信等渠道的消息通知模板。"""
__tablename__ = "notification_templates"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) # 模板唯一标识 UUID
name = Column(String(100), unique=True, nullable=False) # 模板名称(唯一)
channel = Column(String(20), nullable=False) # 通知渠道:wecom/email/webhook
event_type = Column(String(50), nullable=False) # 触发事件类型
subject_template = Column(String(500)) # 消息标题模板
body_template = Column(Text) # 消息正文模板
is_active = Column(Boolean, default=True) # 是否启用
created_at = Column(DateTime, default=datetime.utcnow) # 记录创建时间
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # 记录更新时间
class SystemMetric(Base):
"""系统指标表 (system_metrics),存储系统运行时性能指标数据。"""
__tablename__ = "system_metrics"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) # 指标唯一标识 UUID
metric_name = Column(String(50), nullable=False) # 指标名称(cpu/memory/request_latency/active_users)
metric_value = Column(Float) # 指标数值
tags = Column(JSON, default=dict) # 标签维度(如 instance、region)
recorded_at = Column(DateTime, default=datetime.utcnow, index=True) # 记录时间(索引)
class FlowExecution(Base):
"""流程执行记录表 (flow_executions),存储工作流每次执行的详细记录。"""
__tablename__ = "flow_executions"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) # 执行记录唯一标识 UUID
flow_id = Column(UUID(as_uuid=True), ForeignKey("flow_definitions.id"), nullable=False) # 关联的流程定义 ID
session_id = Column(String(100)) # 关联的会话 ID
status = Column(String(20), default="running") # 执行状态:running/completed/failed/cancelled
input_data = Column(JSON, default=dict) # 输入数据
output_data = Column(JSON, default=dict) # 输出数据
error_message = Column(Text) # 错误信息(失败时记录)
started_by = Column(UUID(as_uuid=True), ForeignKey("users.id")) # 触发执行的用户 ID
started_at = Column(DateTime, default=datetime.utcnow) # 开始时间
finished_at = Column(DateTime) # 结束时间
duration_ms = Column(Integer) # 执行耗时(毫秒)
class MemoryMessage(Base):
"""记忆消息表 (memory_messages),存储流程引擎中节点的中间记忆数据。"""
__tablename__ = "memory_messages"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) # 消息唯一标识 UUID
flow_execution_id = Column(UUID(as_uuid=True), ForeignKey("flow_executions.id", ondelete="CASCADE"), nullable=False) # 关联的流程执行 ID
node_id = Column(String(100), nullable=False) # 节点标识
role = Column(String(20), nullable=False) # 消息角色:user/assistant/system/tool
content = Column(Text, nullable=False) # 消息内容
metadata_ = Column("metadata", JSON, default=dict) # 元数据
created_at = Column(DateTime, default=datetime.utcnow) # 记录创建时间

18
backend/modules/audit/router.py

@ -26,7 +26,7 @@ async def list_logs(
page_size: int = Query(20, ge=1, le=100),
action: str | None = Query(None),
resource: str | None = Query(None),
operator_id: uuid.UUID | None = Query(None),
user_id: uuid.UUID | None = Query(None),
date_from: datetime | None = Query(None),
date_to: datetime | None = Query(None),
db: AsyncSession = Depends(get_db),
@ -39,7 +39,7 @@ async def list_logs(
page_size: 每页数量最大 100
action: 可选的操作类型筛选条件
resource: 可选的资源类型筛选条件
operator_id: 可选的操作人 ID 筛选条件
user_id: 可选的操作人 ID 筛选条件
date_from: 可选的起始时间筛选条件
date_to: 可选的结束时间筛选条件
db: 异步数据库会话
@ -51,9 +51,9 @@ async def list_logs(
if action:
conditions.append(AuditLog.action == action)
if resource:
conditions.append(AuditLog.resource == resource)
if operator_id:
conditions.append(AuditLog.operator_id == operator_id)
conditions.append(AuditLog.resource_type == resource)
if user_id:
conditions.append(AuditLog.user_id == user_id)
if date_from:
conditions.append(AuditLog.created_at >= date_from)
if date_to:
@ -135,8 +135,8 @@ async def audit_stats(request: Request, db: AsyncSession = Depends(get_db)):
# 最常见的资源类型 TOP 10
top_resources = await db.execute(
select(AuditLog.resource, func.count(AuditLog.id))
.group_by(AuditLog.resource)
select(AuditLog.resource_type, func.count(AuditLog.id))
.group_by(AuditLog.resource_type)
.order_by(func.count(AuditLog.id).desc())
.limit(10)
)
@ -191,9 +191,9 @@ async def export_logs(
writer.writerow([
str(log.id),
log.created_at.isoformat() if log.created_at else "",
str(log.operator_id) if log.operator_id else "",
str(log.user_id) if log.user_id else "",
log.action,
log.resource or "",
log.resource_type or "",
log.resource_id or "",
str(log.detail)[:500] if log.detail else "",
log.ip_address or "",

2
backend/modules/memory/manager.py

@ -658,7 +658,7 @@ class MemoryManager:
latest_scene = atoms_result.fetchone()
if latest_scene:
from datetime import timezone, timedelta
ago = datetime.now(timezone.utc) - latest_scene[0].replace(tzinfo=timezone=timezone.utc)
ago = datetime.now(timezone.utc) - latest_scene[0].replace(tzinfo=timezone.utc)
if ago < timedelta(hours=12):
return

1
frontend/src/components/layout/AdminLayout.vue

@ -54,7 +54,6 @@
</template>
<el-menu-item index="/admin/model/providers">模型供应商管理</el-menu-item>
<el-menu-item index="/admin/rag/knowledge">知识库管理</el-menu-item>
<el-menu-item index="/admin/agent/list">智能体管理</el-menu-item>
<el-menu-item index="/admin/wecom/config">企微机器人配置</el-menu-item>
</el-sub-menu>

12
frontend/src/router/index.ts

@ -173,18 +173,6 @@ const router = createRouter({
component: () => import('@/views/model/ModelProviderManager.vue'),
meta: { title: '模型供应商管理', perms: ['admin:access'] },
},
{
path: 'agent/list',
name: 'AdminAgentList',
component: () => import('@/views/agent/AgentList.vue'),
meta: { title: '智能体管理', perms: ['admin:access'] },
},
{
path: 'agent/chat/:type',
name: 'AdminAgentChat',
component: () => import('@/views/agent/AgentChat.vue'),
meta: { title: '智能体对话', perms: ['admin:access'] },
},
{
path: 'monitor/employees',
name: 'AdminMonitorEmployees',

4
frontend/src/views/dashboard/Dashboard.vue

@ -33,8 +33,8 @@
<el-card>
<template #header>快捷入口</template>
<div class="shortcuts">
<el-button type="primary" plain style="width: 100%; margin-bottom: 8px" @click="$router.push('/admin/agent/list')">
智能体对话
<el-button type="primary" plain style="width: 100%; margin-bottom: 8px" @click="$router.push('/admin/model/providers')">
模型管理
</el-button>
<el-button type="success" plain style="width: 100%; margin-bottom: 8px" @click="$router.push('/admin/task/create')" v-if="userStore.hasPermission('task:create')">
创建任务

10
frontend/src/views/flow/FlowEditor.vue

@ -83,7 +83,7 @@
v-if="selectedNode?.data?.type"
:is="getConfigComponent(selectedNode.data.type)"
v-model="selectedNodeData.config"
:agent-list="agentList"
:model-list="modelList"
:mcp-servers="mcpServers"
@change="onConfigChange"
/>
@ -163,7 +163,7 @@ const loadingVersions = ref(false)
const selectedNodeId = ref('')
const selectedNodeData = ref<any>({})
const mcpServers = ref<any[]>([])
const agentList = ref<any[]>([])
const modelList = ref<any[]>([])
const nodes = ref<any[]>([])
const edges = ref<any[]>([])
@ -378,8 +378,8 @@ async function loadMcpServers() {
try { const { mcpApi } = await import('@/api'); const res: any = await mcpApi.getServers(); mcpServers.value = Array.isArray(res) ? res : (res?.data || []) } catch {}
}
async function loadAgents() {
try { const { agentApi } = await import('@/api'); const res: any = await agentApi.getList(); agentList.value = Array.isArray(res?.data) ? res.data : (Array.isArray(res) ? res : []) } catch {}
async function loadModels() {
try { const { modelProviderApi } = await import('@/api'); const res: any = await modelProviderApi.getAllModels(); modelList.value = Array.isArray(res?.data) ? res.data : (Array.isArray(res) ? res : []) } catch {}
}
async function saveFlow() {
@ -445,7 +445,7 @@ function formatTime(ts: string) {
onMounted(async () => {
try {
if (isEdit.value) { await loadFlow() }
await Promise.allSettled([loadMcpServers(), loadAgents()])
await Promise.allSettled([loadMcpServers(), loadModels()])
startAutoSave()
} catch (e: any) {
console.error('FlowEditor init error:', e)

110
frontend/src/views/flow/node-configs/LlmConfig.vue

@ -1,43 +1,60 @@
<template>
<div class="node-config">
<el-divider content-position="left">Agent配置</el-divider>
<el-form-item label="选择Agent">
<el-select :model-value="modelValue.agent_id" @change="onAgentSelect" placeholder="手动配置或选择已有Agent" clearable>
<el-option v-for="a in agentList" :key="a.id" :label="a.name" :value="a.id" />
<el-divider content-position="left">模型配置</el-divider>
<el-form-item label="选择模型">
<el-select :model-value="modelValue.model || ''" @change="onModelSelect" placeholder="从模型管理中选择或手动输入" filterable clearable allow-create>
<template #header>
<div style="padding: 4px 0; color: #909399; font-size: 12px;">
仅显示已启用的 LLM 模型来自模型管理
</div>
</template>
<el-option-group label="大语言模型 (LLM)">
<el-option
v-for="m in llmModels"
:key="m.id"
:label="`${m.display_name || m.model_name} (${getProviderName(m.provider_id)})`"
:value="m.model_name"
>
<span style="display: flex; justify-content: space-between; align-items: center;">
<span>{{ m.display_name || m.model_name }}</span>
<el-tag size="small" type="info" style="margin-left: 8px;">{{ getProviderName(m.provider_id) }}</el-tag>
</span>
</el-option>
</el-option-group>
</el-select>
</el-form-item>
<el-divider content-position="left">模型配置</el-divider>
<!-- 选中模型后显示其默认参数提示 -->
<div v-if="selectedModelInfo" style="margin-bottom: 12px; padding: 8px 12px; background: #f0f9ff; border-radius: 6px; font-size: 12px; color: #606266;">
<span v-if="(selectedModelInfo.default_params || {}).temperature !== undefined">
Temperature={{ selectedModelInfo.default_params.temperature }}
</span>
<span v-if="(selectedModelInfo.default_params || {}).max_tokens" style="margin-left: 8px;">
MaxTokens={{ selectedModelInfo.default_params.max_tokens }}
</span>
<el-tag v-if="(selectedModelInfo.capabilities || {}).vision" size="small" type="primary" effect="plain" style="margin-left: 8px;">Vision</el-tag>
<el-tag v-if="(selectedModelInfo.capabilities || {}).function_calling" size="small" type="primary" effect="plain" style="margin-left: 4px;">FC</el-tag>
</div>
<el-form-item label="系统提示词">
<el-input :model-value="modelValue.system_prompt" type="textarea" :rows="4" @input="(e: any) => update('system_prompt', e)" placeholder="输入系统提示词,定义AI角色和行为" />
</el-form-item>
<el-form-item label="模型">
<el-select :model-value="modelValue.model || 'gpt-4o-mini'" @change="update('model', $event)">
<el-option label="GPT-4o-mini" value="gpt-4o-mini" />
<el-option label="GPT-4o" value="gpt-4o" />
<el-option label="GPT-3.5-turbo" value="gpt-3.5-turbo" />
<el-option label="DeepSeek-V3" value="deepseek-chat" />
<el-option label="DeepSeek-R1" value="deepseek-reasoner" />
</el-select>
</el-form-item>
<el-form-item label="温度">
<el-slider :model-value="modelValue.temperature ?? 0.7" :min="0" :max="2" :step="0.1" @change="update('temperature', $event)" />
</el-form-item>
<el-form-item label="最大Token数">
<el-input-number :model-value="modelValue.max_tokens ?? 2000" :min="1" :max="4000" :step="100" @change="update('max_tokens', $event)" />
<el-input-number :model-value="modelValue.max_tokens ?? 2000" :min="1" :max="200000" :step="256" @change="update('max_tokens', $event)" />
</el-form-item>
<el-divider content-position="left">记忆配置</el-divider>
<el-form-item label="上下文长度">
<el-input-number :model-value="modelValue.context_length ?? 5" :min="1" :max="20" :step="1" @change="update('context_length', $event)" />
</el-form-item>
<el-form-item label="记忆模式">
<el-select :model-value="modelValue.memory_mode || 'short_term'" @change="update('memory_mode', $event)">
<el-option label="无记忆" value="none" />
@ -45,13 +62,13 @@
<el-option label="长期记忆" value="long_term" />
</el-select>
</el-form-item>
<el-divider content-position="left">高级选项</el-divider>
<el-form-item label="流式输出">
<el-switch :model-value="modelValue.stream ?? true" @change="update('stream', $event)" />
</el-form-item>
<el-form-item label="函数调用">
<el-switch :model-value="modelValue.tool_call ?? false" @change="update('tool_call', $event)" />
</el-form-item>
@ -59,9 +76,11 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
modelValue: any
agentList: any[]
modelList: any[]
}>()
const emit = defineEmits(['change', 'update:modelValue'])
@ -71,17 +90,36 @@ function update(key: string, val: any) {
emit('update:modelValue', { ...props.modelValue, [key]: val })
}
function onAgentSelect(val: string) {
// LLM
const llmModels = computed(() => {
return (props.modelList || []).filter((m: any) => m.model_type === 'llm' && m.is_active !== false)
})
//
const selectedModelInfo = computed(() => {
const modelName = props.modelValue?.model
if (!modelName) return null
return llmModels.value.find((m: any) => m.model_name === modelName)
})
function getProviderName(providerId: string): string {
// providerId UUID
// ID
return providerId ? String(providerId).slice(0, 8) + '...' : '-'
}
function onModelSelect(val: string) {
update('model', val)
//
if (!val) return
const agent = props.agentList.find(a => a.id === val)
if (agent) {
const model = llmModels.value.find((m: any) => m.model_name === val)
if (model) {
const params = model.default_params || {}
const updated = { ...props.modelValue }
updated.system_prompt = agent.system_prompt || updated.system_prompt
updated.model = agent.model || updated.model
updated.temperature = agent.temperature ?? updated.temperature
updated.max_tokens = agent.max_tokens || updated.max_tokens
if (params.temperature !== undefined && !updated.temperature) updated.temperature = params.temperature
if (params.max_tokens && !updated.max_tokens) updated.max_tokens = params.max_tokens
if ((model.capabilities || {}).function_calling && updated.tool_call === false) updated.tool_call = true
emit('update:modelValue', updated)
}
emit('change')
}
</script>

12
frontend/src/views/flow/node-configs/TemplateTransformConfig.vue

@ -30,13 +30,13 @@
<div class="example-box">
<p class="example-title">常用模板</p>
<div class="example-item" @click="setTemplate('请帮我处理:{{input}}')">
<div class="example-item" @click="setTemplate(templateExamples.simple)">
<code>请帮我处理{"{{input}}"}</code>
</div>
<div class="example-item" @click="setTemplate('根据{{rag_node.output}},回答:{{input}}')">
<div class="example-item" @click="setTemplate(templateExamples.rag)">
<code>根据{"{{rag_node.output}}"}回答{"{{input}}"}</code>
</div>
<div class="example-item" @click="setTemplate('{\"query\": \"{{input}}\", \"context\": \"{{trigger.data}}\"}')">
<div class="example-item" @click="setTemplate(templateExamples.json)">
<code>{"{"}"query": "{"{{input}}"}", "context": "{"{{trigger.data}}"}"{"}"}</code>
</div>
</div>
@ -50,6 +50,12 @@ const props = defineProps<{
const emit = defineEmits(['change', 'update:modelValue'])
const templateExamples = {
simple: '请帮我处理:{{input}}',
rag: '根据{{rag_node.output}},回答:{{input}}',
json: '{"query": "{{input}}", "context": "{{trigger.data}}"}',
}
function update(key: string, val: any) {
emit('change')
emit('update:modelValue', { ...props.modelValue, [key]: val })

858
frontend/src/views/model/ModelProviderManager.vue

@ -1,139 +1,339 @@
<template>
<div class="model-provider-manager">
<h2 style="margin-bottom: 16px;">模型供应商管理</h2>
<h2 style="margin-bottom: 16px;">模型管理</h2>
<div style="margin-bottom: 16px; display: flex; gap: 12px;">
<el-button type="primary" @click="openProviderDialog()">添加供应商</el-button>
<!-- 操作栏 -->
<div style="margin-bottom: 16px; display: flex; gap: 12px; align-items: center;">
<el-button type="primary" @click="openAddModelDialog()">
<el-icon><Plus /></el-icon>
</el-button>
</div>
<div v-if="!selectedProvider">
<el-table :data="providers" v-loading="loading" style="width: 100%">
<el-table-column prop="name" label="供应商名称" width="180" />
<el-table-column prop="provider_type" label="类型" width="120">
<template #default="{ row }">
<el-tag size="small">{{ row.provider_type }}</el-tag>
<!-- 按模型类型 Tab 切换 -->
<el-tabs v-model="activeType" @tab-change="loadModels">
<el-tab-pane name="all">
<template #label>
<span>全部模型 <el-badge :value="allModels.length" :max="99" v-if="allModels.length > 0" /></span>
</template>
</el-tab-pane>
<el-tab-pane name="llm">
<template #label>
<span>🤖 LLM 大语言模型 <el-badge :value="models.filter(m => m.model_type === 'llm').length" :max="99" v-if="models.filter(m => m.model_type === 'llm').length > 0" /></span>
</template>
</el-tab-pane>
<el-tab-pane name="embedding">
<template #label>
<span>📐 Embedding 向量嵌入 <el-badge :value="models.filter(m => m.model_type === 'embedding').length" :max="99" v-if="models.filter(m => m.model_type === 'embedding').length > 0" /></span>
</template>
</el-tab-pane>
<el-tab-pane name="rerank">
<template #label>
<span>🔄 Rerank 重排序 <el-badge :value="models.filter(m => m.model_type === 'rerank').length" :max="99" v-if="models.filter(m => m.model_type === 'rerank').length > 0" /></span>
</template>
</el-tab-pane>
</el-tabs>
<!-- 模型列表 -->
<el-table :data="displayModels" v-loading="loading" style="width: 100%">
<el-table-column label="模型信息" min-width="280">
<template #default="{ row }">
<div style="display: flex; align-items: center; gap: 10px;">
<el-tag :type="modelTagType(row.model_type)" size="small">{{ modelTypeName(row.model_type) }}</el-tag>
<div>
<div style="font-weight: 500;">{{ row.display_name || row.model_name }}</div>
<code style="font-size: 11px; color: #909399; background: #f5f7fa; padding: 1px 6px; border-radius: 3px;">{{ row.model_name }}</code>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="所属供应商" width="160">
<template #default="{ row }">
<span>{{ getProviderName(row.provider_id) }}</span>
</template>
</el-table-column>
<el-table-column label="配置参数" min-width="260">
<template #default="{ row }">
<div v-if="row.model_type === 'llm'" style="display: flex; gap: 6px; flex-wrap: wrap;">
<el-tag v-if="(row.default_params || {}).temperature !== undefined && (row.default_params || {}).temperature !== ''" size="small" effect="plain">T={{ (row.default_params || {}).temperature }}</el-tag>
<el-tag v-if="(row.default_params || {}).max_tokens" size="small" effect="plain">MaxTok={{ (row.default_params || {}).max_tokens }}</el-tag>
<el-tag v-if="(row.capabilities || {}).vision" size="small" type="primary" effect="plain">Vision</el-tag>
<el-tag v-if="(row.capabilities || {}).function_calling" size="small" type="primary" effect="plain">FC</el-tag>
<el-tag v-if="row.is_default" size="small" type="success" effect="plain">默认</el-tag>
</div>
<div v-else-if="row.model_type === 'embedding'" style="display: flex; gap: 6px; flex-wrap: wrap;">
<el-tag v-if="(row.default_params || {}).dimension" size="small" effect="plain">Dim={{ (row.default_params || {}).dimension }}</el-tag>
<el-tag v-if="(row.default_params || {}).max_tokens" size="small" effect="plain">Chunk={{ (row.default_params || {}).max_tokens }}</el-tag>
<el-tag v-if="row.is_default" size="small" type="success" effect="plain">默认</el-tag>
</div>
<div v-else-if="row.model_type === 'rerank'" style="display: flex; gap: 6px; flex-wrap: wrap;">
<el-tag v-if="(row.default_params || {}).dimension" size="small" effect="plain">Dim={{ (row.default_params || {}).dimension }}</el-tag>
<el-tag v-if="(row.default_params || {}).max_tokens" size="small" effect="plain">MaxTok={{ (row.default_params || {}).max_tokens }}</el-tag>
<el-tag v-if="row.is_default" size="small" type="success" effect="plain">默认</el-tag>
</div>
<span v-else style="color: #c0c4cc; font-size: 12px;">-</span>
</template>
</el-table-column>
<el-table-column prop="is_active" label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.is_active ? 'success' : 'danger'" size="small">{{ row.is_active ? '启用' : '禁用' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button size="small" @click="openEditModelDialog(row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteModel(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- ========== 添加/编辑模型弹窗Dify 风格先选类型再填配置========== -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="680px" :close-on-click-modal="false">
<!-- 步骤 1选择模型类型 -->
<div v-if="step === 1" style="padding: 20px 0;">
<h4 style="margin-bottom: 20px;">请选择要添加的模型类型</h4>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px;">
<div
v-for="t in modelTypes"
:key="t.value"
class="type-card"
:class="{ selected: form.model_type === t.value }"
@click="selectModelType(t.value)"
>
<div class="type-icon">{{ t.icon }}</div>
<div class="type-name">{{ t.name }}</div>
<div class="type-desc">{{ t.desc }}</div>
</div>
</div>
</div>
<!-- 步骤 2填写模型配置根据类型动态切换表单 -->
<div v-if="step === 2">
<!-- 返回类型选择 -->
<el-button text type="primary" @click="step = 1" style="margin-bottom: 16px;">
返回重新选择类型
</el-button>
<el-form :model="form" label-width="130px">
<!-- 公共字段 -->
<el-form-item label="所属供应商" required>
<el-select v-model="form.provider_id" placeholder="请选择供应商" style="width: 100%">
<el-option v-for="p in providers" :key="p.id" :label="p.name" :value="p.id" />
</el-select>
</el-form-item>
<el-form-item label="模型标识名" required>
<el-input v-model="form.model_name" :placeholder="currentTypePlaceholder.name">
<template #append>
<el-tooltip content="与 API 调用时使用的模型名称一致,如 gpt-4o、text-embedding-3-small">
<el-icon><InfoFilled /></el-icon>
</el-tooltip>
</template>
</el-input>
</el-form-item>
<el-form-item label="显示名称">
<el-input v-model="form.display_name" :placeholder="currentTypePlaceholder.displayName" />
</el-form-item>
<el-divider />
<!-- ====== LLM 专用配置 ====== -->
<template v-if="form.model_type === 'llm'">
<el-divider content-position="left">
<el-icon><Cpu /></el-icon> LLM
</el-divider>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="Temperature">
<el-input-number v-model="llmForm.temperature" :min="0" :max="2" :step="0.1" :precision="2" controls-position="right" style="width: 100%" />
<div style="font-size: 11px; color: #909399; margin-top: 2px;">控制随机性值越高输出越多样</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Max Tokens">
<el-input-number v-model="llmForm.max_tokens" :min="1" :max="200000" :step="256" controls-position="right" style="width: 100%" />
<div style="font-size: 11px; color: #909399; margin-top: 2px;">最大输出 Token </div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="Top P">
<el-input-number v-model="llmForm.top_p" :min="0" :max="1" :step="0.05" :precision="2" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Frequency Penalty">
<el-input-number v-model="llmForm.frequency_penalty" :min="-2" :max="2" :step="0.1" :precision="2" controls-position="right" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="能力支持">
<div style="padding: 12px; background: #f0f9ff; border-radius: 8px;">
<el-checkbox v-model="llmForm.vision">👁 支持视觉理解 (Vision / 多模态)</el-checkbox>
<el-checkbox v-model="llmForm.function_calling"> 支持函数调用 (Function Calling / Tool Use)</el-checkbox>
<el-checkbox v-model="llmForm.streaming">🔄 支持流式输出 (Streaming)</el-checkbox>
</div>
</el-form-item>
<el-form-item label="高级参数 (JSON)">
<el-input v-model="llmForm.extraParamsJson" type="textarea" :rows="3" placeholder='{"stop": ["\n\n"], "presence_penalty": 0}' />
</el-form-item>
</template>
</el-table-column>
<el-table-column prop="base_url" label="API 端点" show-overflow-tooltip />
<el-table-column prop="is_active" label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.is_active ? 'success' : 'info'" size="small">
{{ row.is_active ? '启用' : '禁用' }}
</el-tag>
<!-- ====== Embedding 专用配置 ====== -->
<template v-if="form.model_type === 'embedding'">
<el-divider content-position="left">
<el-icon><Grid /></el-icon> Embedding
</el-divider>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="向量维度" required>
<el-select v-model="embForm.dimension" style="width: 100%" filterable allow-create>
<el-option label="1536 (OpenAI Ada/Text-Embedding)" value="1536" />
<el-option label="768 (OpenAI Small)" value="768" />
<el-option label="1024 (BGE Large)" value="1024" />
<el-option label="384 (BGE Small)" value="384" />
<el-option label="2048 (Cohere / 其他)" value="2048" />
<el-option label="4096 (自定义大维度)" value="4096" />
</el-select>
<div style="font-size: 11px; color: #909399; margin-top: 2px;">输出向量的维度大小</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="最大分块 Token 数">
<el-input-number v-model="embForm.max_chunk_tokens" :min="128" :max="8192" :step="64" controls-position="right" style="width: 100%" />
<div style="font-size: 11px; color: #909399; margin-top: 2px;">单次文本分块的最大 Token </div>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="高级参数 (JSON)">
<el-input v-model="embForm.extraParamsJson" type="textarea" :rows="3" placeholder='{"batch_size": 32, "normalize": true}' />
</el-form-item>
</template>
</el-table-column>
<el-table-column label="操作" width="280">
<template #default="{ row }">
<el-button size="small" @click="selectProvider(row)">管理模型</el-button>
<el-button size="small" @click="openProviderDialog(row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteProvider(row.id)">删除</el-button>
<!-- ====== Rerank 专用配置 ====== -->
<template v-if="form.model_type === 'rerank'">
<el-divider content-position="left">
<el-icon><Sort /></el-icon> Rerank
</el-divider>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="向量维度" required>
<el-select v-model="rerankForm.dimension" style="width: 100%" filterable allow-create>
<el-option label="1024 (BGE Reranker V2 M3)" value="1024" />
<el-option label="768 (BGE Reranker Base)" value="768" />
<el-option label="384 (BGE Reranker Small)" value="384" />
<el-option label="256 (MiniLM Reranker)" value="256" />
<el-option label="4096 (Cohere Rerank)" value="4096" />
</el-select>
<div style="font-size: 11px; color: #909399; margin-top: 2px;">查询向量与文档向量的维度</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="最大 Token 数">
<el-input-number v-model="rerankForm.max_tokens" :min="512" :max="8192" :step="256" controls-position="right" style="width: 100%" />
<div style="font-size: 11px; color: #909399; margin-top: 2px;">输入文本的最大长度</div>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="评分方式">
<el-radio-group v-model="rerankForm.score_method">
<el-radio value="cosine">余弦相似度 (Cosine Similarity)</el-radio>
<el-radio value="dotproduct">点积 (Dot Product)</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="高级参数 (JSON)">
<el-input v-model="rerankForm.extraParamsJson" type="textarea" :rows="3" placeholder='{"return_documents": true, "top_k": 5}' />
</el-form-item>
</template>
</el-table-column>
</el-table>
</div>
<div v-else>
<div style="margin-bottom: 12px; display: flex; gap: 12px; align-items: center;">
<el-button @click="selectedProvider = null"> 返回供应商列表</el-button>
<span style="font-size: 16px; font-weight: bold;">{{ selectedProvider.name }} - 模型管理</span>
<el-button type="primary" size="small" @click="openModelDialog()">添加模型</el-button>
<!-- 公共底部字段 -->
<el-divider />
<el-form-item label="设为默认模型">
<el-switch v-model="form.is_default" />
<span style="margin-left: 8px; color: #999; font-size: 12px;">设为 {{ currentTypeName }} 类型的默认调用模型</span>
</el-form-item>
<el-form-item label="启用状态">
<el-switch v-model="form.is_active" active-text="启用" inactive-text="禁用" />
</el-form-item>
</el-form>
</div>
<el-tabs v-model="modelTab">
<el-tab-pane label="LLM" name="llm" />
<el-tab-pane label="Embedding" name="embedding" />
<el-tab-pane label="Rerank" name="rerank" />
</el-tabs>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button v-if="step === 1" type="primary" @click="goToStep2" :disabled="!form.model_type">下一步</el-button>
<el-button v-else type="primary" @click="saveModel" :loading="saving">{{ editingId ? '保存修改' : '添加模型' }}</el-button>
</template>
</el-dialog>
<el-table :data="filteredModels" v-loading="modelLoading" style="width: 100%">
<el-table-column prop="model_name" label="模型名" width="200" />
<el-table-column prop="display_name" label="显示名称" width="200" />
<el-table-column prop="model_type" label="类型" width="100">
<!-- ========== 供应商管理抽屉次要功能========== -->
<el-drawer v-model="providerDrawerVisible" title="供应商管理" direction="rtl" size="420px">
<template #header>
<div style="display: flex; justify-content: space-between; align-items: center;">
<span>供应商管理</span>
<el-button type="primary" size="small" @click="openProviderDialog()">添加供应商</el-button>
</div>
</template>
<el-table :data="providers" v-loading="providerLoading" size="small">
<el-table-column prop="name" label="名称" />
<el-table-column prop="provider_type" label="类型" width="120">
<template #default="{ row }">
<el-tag size="small">{{ row.model_type }}</el-tag>
<el-tag :type="providerTagType(row.provider_type)" size="small">{{ providerTagName(row.provider_type) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="is_default" label="默认" width="80">
<el-table-column prop="base_url" label="API 端点" show-overflow-tooltip />
<el-table-column prop="is_active" label="状态" width="70">
<template #default="{ row }">
<el-tag :type="row.is_default ? 'success' : 'info'" size="small">
{{ row.is_default ? '是' : '否' }}
</el-tag>
<el-tag :type="row.is_active ? 'success' : 'info'" size="small">{{ row.is_active ? '启' : '停' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button size="small" @click="openModelDialog(row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteModel(row.id)">删除</el-button>
<el-button size="small" link @click="openProviderDialog(row)">编辑</el-button>
<el-button size="small" link type="danger" @click="deleteProvider(row.id)"></el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-drawer>
<el-dialog v-model="providerDialogVisible" :title="editingProviderId ? '编辑供应商' : '添加供应商'" width="550px">
<el-form :model="providerForm" label-width="100px">
<el-form-item label="供应商名称" required>
<el-input v-model="providerForm.name" placeholder="如 OpenAI / 智谱AI / 本地Ollama" />
<!-- 供应商表单弹窗 -->
<el-dialog v-model="providerDialogVisible" :title="editingProviderId ? '编辑供应商' : '添加供应商'" width="520px">
<el-form :model="providerForm" label-width="90px">
<el-form-item label="名称" required>
<el-input v-model="providerForm.name" placeholder="如 OpenAI、智谱AI、Ollama 本地" />
</el-form-item>
<el-form-item label="类型" required>
<el-form-item label="接入类型" required>
<el-select v-model="providerForm.provider_type" style="width: 100%">
<el-option label="OpenAI兼容" value="openai_compatible" />
<el-option label="OpenAI" value="openai" />
<el-option label="智谱AI" value="zhipu" />
<el-option label="Ollama" value="ollama" />
<el-option label="OpenAI 兼容接口" value="openai_compatible" />
<el-option label="OpenAI 官方" value="openai" />
<el-option label="智谱 AI (Zhipu)" value="zhipu" />
<el-option label="Ollama 本地部署" value="ollama" />
<el-option label="DeepSeek" value="deepseek" />
</el-select>
</el-form-item>
<el-form-item label="API 端点">
<el-input v-model="providerForm.base_url" placeholder="如 https://api.openai.com/v1" />
<el-input v-model="providerForm.base_url" placeholder="https://api.openai.com/v1" />
</el-form-item>
<el-form-item label="API Key">
<el-input v-model="providerForm.api_key" type="password" placeholder="密钥(加密存储)" show-password />
<el-input v-model="providerForm.api_key" type="password" show-password placeholder="密钥(加密存储)" />
</el-form-item>
<el-form-item label="启用">
<el-switch v-model="providerForm.is_active" />
<el-switch v-model="providerForm.is_active" active-text="启用" inactive-text="禁用" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="providerDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveProvider" :loading="saving">保存</el-button>
</template>
</el-dialog>
<el-dialog v-model="modelDialogVisible" :title="editingModelId ? '编辑模型' : '添加模型'" width="550px">
<el-form :model="modelForm" label-width="100px">
<el-form-item label="模型类型" required>
<el-select v-model="modelForm.model_type" style="width: 100%" :disabled="!!editingModelId">
<el-option label="LLM(大语言模型)" value="llm" />
<el-option label="Embedding(嵌入模型)" value="embedding" />
<el-option label="Rerank(重排序模型)" value="rerank" />
</el-select>
</el-form-item>
<el-form-item label="模型名" required>
<el-input v-model="modelForm.model_name" placeholder="如 gpt-4o / text-embedding-3-small" />
</el-form-item>
<el-form-item label="显示名称">
<el-input v-model="modelForm.display_name" placeholder="如 GPT-4o" />
</el-form-item>
<el-form-item label="设为默认">
<el-switch v-model="modelForm.is_default" />
<span style="margin-left: 8px; color: #999; font-size: 12px;">该类型下的默认模型</span>
</el-form-item>
<el-form-item label="能力配置" v-if="modelForm.model_type === 'llm'">
<div style="width: 100%;">
<el-checkbox v-model="llmVision" style="margin-right: 16px;">支持 Vision</el-checkbox>
<el-checkbox v-model="llmFunctionCalling">支持 Function Calling</el-checkbox>
</div>
</el-form-item>
<el-form-item label="默认参数">
<el-input v-model="defaultParamsJson" type="textarea" :rows="3" placeholder='{"temperature": 0.7, "max_tokens": 4096}' />
</el-form-item>
<el-form-item label="启用">
<el-switch v-model="modelForm.is_active" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="modelDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveModel" :loading="modelSaving">保存</el-button>
<el-button type="primary" @click="saveProvider" :loading="providerSaving">保存</el-button>
</template>
</el-dialog>
</div>
@ -142,14 +342,70 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, InfoFilled, Cpu, Grid, Sort } from '@element-plus/icons-vue'
import api from '@/api'
const providers = ref<any[]>([])
// ==================== ====================
const loading = ref(false)
const saving = ref(false)
const dialogVisible = ref(false)
const step = ref(1) // 1=, 2=
const editingId = ref('')
const activeType = ref('all')
const providers = ref<any[]>([])
const models = ref<any[]>([])
const providerLoading = ref(false)
const providerSaving = ref(false)
const providerDrawerVisible = ref(false)
const providerDialogVisible = ref(false)
const editingProviderId = ref('')
// ==================== ====================
const modelTypes = [
{ value: 'llm', icon: '🤖', name: 'LLM 大语言模型', desc: '对话、生成、推理、代码编写等通用任务' },
{ value: 'embedding', icon: '📐', name: 'Embedding 向量嵌入', desc: '将文本转换为向量,用于语义搜索和RAG检索' },
{ value: 'rerank', icon: '🔄', name: 'Rerank 重排序', desc: '对搜索结果进行相关性重排序,提升精度' },
]
// ==================== ====================
const form = reactive({
model_type: '',
provider_id: '',
model_name: '',
display_name: '',
is_default: false,
is_active: true,
})
// LLM
const llmForm = reactive({
temperature: 0.7,
max_tokens: 4096,
top_p: 1,
frequency_penalty: 0,
vision: false,
function_calling: false,
streaming: true,
extraParamsJson: '',
})
// Embedding
const embForm = reactive({
dimension: '1536',
max_chunk_tokens: 8192,
extraParamsJson: '',
})
// Rerank
const rerankForm = reactive({
dimension: '1024',
max_tokens: 512,
score_method: 'cosine',
extraParamsJson: '',
})
//
const providerForm = reactive({
name: '',
provider_type: 'openai_compatible',
@ -158,44 +414,231 @@ const providerForm = reactive({
is_active: true,
})
const selectedProvider = ref<any>(null)
const models = ref<any[]>([])
const modelLoading = ref(false)
const modelSaving = ref(false)
const modelDialogVisible = ref(false)
const editingModelId = ref('')
const modelTab = ref('llm')
const llmVision = ref(false)
const llmFunctionCalling = ref(false)
const defaultParamsJson = ref('')
const modelForm = reactive({
model_name: '',
model_type: 'llm',
display_name: '',
is_default: false,
is_active: true,
// ==================== ====================
const allModels = computed(() => models.value)
const displayModels = computed(() => {
if (activeType.value === 'all') return models.value
return models.value.filter((m: any) => m.model_type === activeType.value)
})
const filteredModels = computed(() => models.value.filter((m: any) => m.model_type === modelTab.value))
const dialogTitle = computed(() => {
if (editingId.value) return `编辑模型 - ${currentTypeName.value}`
return `添加${currentTypeName.value}模型`
})
const currentTypeName = computed(() => {
const t = modelTypes.find(t => t.value === form.model_type)
return t ? t.name.replace(/^(LLM|Embedding|Rerank)\s*/, '') : ''
})
const currentTypePlaceholder = computed(() => {
const map: Record<string, any> = {
llm: { name: '如 gpt-4o、claude-3-opus、deepseek-chat、qwen-max', displayName: '如 GPT-4o Turbo' },
embedding: { name: '如 text-embedding-3-small、bge-large-zh-v1.5', displayName: '如 Text-Embedding-3-Small' },
rerank: { name: '如 bge-reranker-v2-m3、cohere-rerank-3-multilingual', displayName: '如 BGE Reranker V2 M3' },
}
return map[form.model_type] || { name: '', displayName: '' }
})
// ==================== ====================
function modelTypeName(type: string): string {
const map: Record<string, string> = { llm: 'LLM', embedding: 'Embedding', rerank: 'Rerank' }
return map[type] || type
}
function modelTagType(type: string): string {
const map: Record<string, string> = { llm: '', embedding: 'success', rerank: 'warning' }
return map[type] || ''
}
function providerTagName(type: string): string {
const map: Record<string, string> = { openai_compatible: 'OpenAI 兼容', openai: 'OpenAI', zhipu: '智谱 AI', ollama: 'Ollama', deepseek: 'DeepSeek' }
return map[type] || type
}
function providerTagType(type: string): string {
const map: Record<string, string> = { openai_compatible: '', openai: '', zhipu: 'success', ollama: 'warning', deepseek: 'danger' }
return map[type] || ''
}
function getProviderName(providerId: string): string {
const p = providers.value.find((x: any) => x.id === providerId)
return p ? p.name : '-'
}
function buildPayloadFromForm() {
let capabilities: any = {}
let defaultParams: any = {}
if (form.model_type === 'llm') {
capabilities = {}
if (llmForm.vision) capabilities.vision = true
if (llmForm.function_calling) capabilities.function_calling = true
if (llmForm.streaming) capabilities.streaming = true
defaultParams = {
temperature: llmForm.temperature,
max_tokens: llmForm.max_tokens,
top_p: llmForm.top_p,
frequency_penalty: llmForm.frequency_penalty,
}
try { Object.assign(defaultParams, JSON.parse(llmForm.extraParamsJson || '{}')) } catch { /* ignore */ }
} else if (form.model_type === 'embedding') {
defaultParams = {
dimension: embForm.dimension,
max_tokens: embForm.max_chunk_tokens,
}
try { Object.assign(defaultParams, JSON.parse(embForm.extraParamsJson || '{}')) } catch { /* ignore */ }
} else if (form.model_type === 'rerank') {
defaultParams = {
dimension: rerankForm.dimension,
max_tokens: rerankForm.max_tokens,
score_method: rerankForm.score_method,
}
try { Object.assign(defaultParams, JSON.parse(rerankForm.extraParamsJson || '{}')) } catch { /* ignore */ }
}
return {
...form,
capabilities,
default_params: defaultParams,
}
}
function populateFormFromData(data: any) {
form.model_type = data.model_type
form.provider_id = data.provider_id
form.model_name = data.model_name
form.display_name = data.display_name || ''
form.is_default = !!data.is_default
form.is_active = !!data.is_active
const caps = data.capabilities || {}
const params = data.default_params || {}
if (data.model_type === 'llm') {
llmForm.temperature = params.temperature ?? 0.7
llmForm.max_tokens = params.max_tokens ?? 4096
llmForm.top_p = params.top_p ?? 1
llmForm.frequency_penalty = params.frequency_penalty ?? 0
llmForm.vision = !!caps.vision
llmForm.function_calling = !!caps.function_calling
llmForm.streaming = !!caps.streaming
//
const extra: Record<string, any> = { ...params }
delete extra.temperature; delete extra.max_tokens; delete extra.top_p
delete extra.frequency_penalty
llmForm.extraParamsJson = Object.keys(extra).length > 0 ? JSON.stringify(extra, null, 2) : ''
} else if (data.model_type === 'embedding') {
embForm.dimension = String(params.dimension ?? '1536')
embForm.max_chunk_tokens = params.max_tokens ?? 8192
const extra: Record<string, any> = { ...params }
delete extra.dimension; delete extra.max_tokens
embForm.extraParamsJson = Object.keys(extra).length > 0 ? JSON.stringify(extra, null, 2) : ''
} else if (data.model_type === 'rerank') {
rerankForm.dimension = String(params.dimension ?? '1024')
rerankForm.max_tokens = params.max_tokens ?? 512
rerankForm.score_method = params.score_method ?? 'cosine'
const extra: Record<string, any> = { ...params }
delete extra.dimension; delete extra.max_tokens; delete extra.score_method
rerankForm.extraParamsJson = Object.keys(extra).length > 0 ? JSON.stringify(extra, null, 2) : ''
}
}
function resetForm() {
form.model_type = ''
form.provider_id = ''
form.model_name = ''
form.display_name = ''
form.is_default = false
form.is_active = true
step.value = 1
llmForm.temperature = 0.7
llmForm.max_tokens = 4096
llmForm.top_p = 1
llmForm.frequency_penalty = 0
llmForm.vision = false
llmForm.function_calling = false
llmForm.streaming = true
llmForm.extraParamsJson = ''
embForm.dimension = '1536'
embForm.max_chunk_tokens = 8192
embForm.extraParamsJson = ''
rerankForm.dimension = '1024'
rerankForm.max_tokens = 512
rerankForm.score_method = 'cosine'
rerankForm.extraParamsJson = ''
}
// ==================== ====================
function openAddModelDialog() {
resetForm()
editingId.value = ''
dialogVisible.value = true
step.value = 1
}
function selectModelType(type: string) {
form.model_type = type
}
function goToStep2() {
if (!form.model_type) return
if (!form.provider_id && providers.value.length > 0) {
form.provider_id = providers.value[0].id
}
step.value = 2
}
function openEditModelDialog(row: any) {
resetForm()
editingId.value = row.id
populateFormFromData(row)
dialogVisible.value = true
step.value = 2
}
async function saveModel() {
if (!form.provider_id) {
ElMessage.warning('请选择所属供应商')
return
}
if (!form.model_name.trim()) {
ElMessage.warning('请输入模型标识名')
return
}
saving.value = true
try {
const payload = buildPayloadFromForm()
if (editingId.value) {
await api.put(`/model-providers/${payload.provider_id}/models/${editingId.value}`, payload)
ElMessage.success('模型已更新')
} else {
await api.post(`/model-providers/${payload.provider_id}/models`, payload)
ElMessage.success('模型已添加')
}
dialogVisible.value = false
await loadModels()
} catch { /* interceptor handles error */ } finally {
saving.value = false
}
}
function resetProviderForm() {
providerForm.name = ''
providerForm.provider_type = 'openai_compatible'
providerForm.base_url = ''
providerForm.api_key = ''
providerForm.is_active = true
async function deleteModel(row: any) {
try {
await ElMessageBox.confirm(`确定删除模型「${row.display_name || row.model_name}」?`, '确认删除', { type: 'warning' })
await api.delete(`/model-providers/${row.provider_id}/models/${row.id}`)
ElMessage.success('已删除')
await loadModels()
} catch { /* cancelled */ }
}
function resetModelForm() {
modelForm.model_name = ''
modelForm.model_type = 'llm'
modelForm.display_name = ''
modelForm.is_default = false
modelForm.is_active = true
llmVision.value = false
llmFunctionCalling.value = false
defaultParamsJson.value = ''
// ==================== ====================
function openProviderDrawer() {
loadProviders()
providerDrawerVisible.value = true
}
function openProviderDialog(row?: any) {
@ -203,28 +646,22 @@ function openProviderDialog(row?: any) {
editingProviderId.value = row.id
providerForm.name = row.name
providerForm.provider_type = row.provider_type
providerForm.base_url = row.base_url
providerForm.base_url = row.base_url || ''
providerForm.api_key = row.api_key || ''
providerForm.is_active = row.is_active
} else {
editingProviderId.value = ''
resetProviderForm()
providerForm.name = ''
providerForm.provider_type = 'openai_compatible'
providerForm.base_url = ''
providerForm.api_key = ''
providerForm.is_active = true
}
providerDialogVisible.value = true
}
async function loadProviders() {
loading.value = true
try {
const res: any = await api.get('/model-providers')
providers.value = res?.data || res || []
} finally {
loading.value = false
}
}
async function saveProvider() {
saving.value = true
providerSaving.value = true
try {
const data = { ...providerForm }
if (editingProviderId.value) {
@ -236,103 +673,80 @@ async function saveProvider() {
}
providerDialogVisible.value = false
await loadProviders()
} catch {
// interceptor handles error
} finally {
saving.value = false
} catch { /* interceptor handles error */ } finally {
providerSaving.value = false
}
}
async function deleteProvider(id: string) {
try {
await ElMessageBox.confirm('删除供应商将同时删除其下所有模型配置,确定继续?', '确认删除', { type: 'warning' })
await ElMessageBox.confirm('删除供应商会同时删除其下所有模型,确定继续?', '警告', { type: 'warning' })
await api.delete(`/model-providers/${id}`)
ElMessage.success('已删除')
if (selectedProvider.value?.id === id) selectedProvider.value = null
await loadProviders()
await loadModels()
} catch { /* cancelled */ }
}
async function selectProvider(provider: any) {
selectedProvider.value = provider
await loadModels(provider.id)
}
async function loadModels(providerId: string) {
modelLoading.value = true
// ==================== ====================
async function loadProviders() {
providerLoading.value = true
try {
const res: any = await api.get(`/model-providers/${providerId}/models`)
models.value = res?.data || res || []
const res: any = await api.get('/model-providers')
providers.value = res?.data || res || []
} finally {
modelLoading.value = false
}
}
function openModelDialog(row?: any) {
if (row) {
editingModelId.value = row.id
modelForm.model_name = row.model_name
modelForm.model_type = row.model_type
modelForm.display_name = row.display_name || ''
modelForm.is_default = row.is_default
modelForm.is_active = row.is_active
const caps = row.capabilities || {}
llmVision.value = !!caps.vision
llmFunctionCalling.value = !!caps.function_calling
defaultParamsJson.value = row.default_params ? JSON.stringify(row.default_params, null, 2) : ''
} else {
editingModelId.value = ''
resetModelForm()
providerLoading.value = false
}
modelDialogVisible.value = true
}
async function saveModel() {
if (!selectedProvider.value) return
modelSaving.value = true
async function loadModels() {
loading.value = true
try {
let defaultParams = {}
try { defaultParams = JSON.parse(defaultParamsJson.value || '{}') } catch { /* keep empty */ }
const capabilities: any = {}
if (modelForm.model_type === 'llm') {
capabilities.vision = llmVision.value
capabilities.function_calling = llmFunctionCalling.value
}
const data = {
...modelForm,
capabilities,
default_params: defaultParams,
}
if (editingModelId.value) {
await api.put(`/model-providers/${selectedProvider.value.id}/models/${editingModelId.value}`, data)
ElMessage.success('模型已更新')
} else {
await api.post(`/model-providers/${selectedProvider.value.id}/models`, data)
ElMessage.success('模型已添加')
}
modelDialogVisible.value = false
await loadModels(selectedProvider.value.id)
} catch {
// interceptor handles error
const res: any = await api.get('/model-providers/models/all')
models.value = res?.data || res || []
} finally {
modelSaving.value = false
loading.value = false
}
}
async function deleteModel(id: string) {
if (!selectedProvider.value) return
try {
await ElMessageBox.confirm('确定删除此模型配置?', '确认删除', { type: 'warning' })
await api.delete(`/model-providers/${selectedProvider.value.id}/models/${id}`)
ElMessage.success('已删除')
await loadModels(selectedProvider.value.id)
} catch { /* cancelled */ }
}
onMounted(() => {
loadProviders()
loadModels()
})
</script>
</script>
<style scoped>
.type-card {
border: 2px solid #e4e7ed;
border-radius: 12px;
padding: 24px 16px;
text-align: center;
cursor: pointer;
transition: all 0.25s ease;
}
.type-card:hover {
border-color: #409eff;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
transform: translateY(-2px);
}
.type-card.selected {
border-color: #409eff;
background: linear-gradient(135deg, #ecf5ff 0%, #f0f9ff 100%);
box-shadow: 0 4px 16px rgba(64, 158, 255, 0.2);
}
.type-icon {
font-size: 36px;
margin-bottom: 8px;
}
.type-name {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
}
.type-desc {
font-size: 12px;
color: #909399;
line-height: 1.5;
}
</style>

Loading…
Cancel
Save