feat: 新增凭证管理与渠道状态展示功能
1. 新增渠道凭证相关API与前端展示逻辑 2. 实现凭证状态自动拉取与手动刷新功能 3. 优化聊天查询支持内部用户ID参数 4. 修复部分代码格式与异常处理逻辑 5. 新增项目代码维基文档
This commit is contained in:
parent
f419f57f63
commit
277bf20153
892
CODE_WIKI.md
Normal file
892
CODE_WIKI.md
Normal file
@ -0,0 +1,892 @@
|
||||
# Yuxi 项目 Code Wiki
|
||||
|
||||
> 本文档是 Yuxi(语析)项目的结构化代码百科,涵盖项目整体架构、主要模块职责、关键类与函数说明、依赖关系以及项目运行方式等关键信息。
|
||||
>
|
||||
> 项目版本:v0.6.2 | 最后更新:2026-05-14
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [项目概述](#1-项目概述)
|
||||
2. [整体架构](#2-整体架构)
|
||||
3. [后端核心模块](#3-后端核心模块)
|
||||
4. [前端核心模块](#4-前端核心模块)
|
||||
5. [关键类与函数详解](#5-关键类与函数详解)
|
||||
6. [数据流与运行链路](#6-数据流与运行链路)
|
||||
7. [依赖关系](#7-依赖关系)
|
||||
8. [项目运行方式](#8-项目运行方式)
|
||||
9. [测试体系](#9-测试体系)
|
||||
10. [附录:目录结构总览](#10-附录目录结构总览)
|
||||
|
||||
---
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
**Yuxi(语析)** 是一个基于大模型的智能知识库与知识图谱智能体开发平台,融合了 RAG 技术与知识图谱技术,基于 **LangGraph v1 + Vue.js + FastAPI + LightRAG** 架构构建。
|
||||
|
||||
### 核心特性
|
||||
|
||||
- **智能体开发**:基于 LangGraph,支持子智能体、Skills、MCPs、Tools 与中间件机制
|
||||
- **知识库(RAG)**:多格式文档解析,支持 Embedding / Rerank 配置及知识库评估
|
||||
- **知识图谱**:基于 LightRAG 的图谱构建与可视化,支持属性图谱并参与智能体推理
|
||||
- **平台与工程化**:Vue + FastAPI 架构,支持暗黑模式、Docker 与生产级部署
|
||||
- **多渠道网关**:支持 Slack、Discord、Telegram、飞书、微信、QQ 等 20+ 渠道接入
|
||||
|
||||
### 技术栈
|
||||
|
||||
| 层级 | 技术 |
|
||||
|------|------|
|
||||
| 前端 | Vue 3 + Vite + Pinia + Ant Design Vue + Sigma.js/G6 |
|
||||
| 后端 API | FastAPI + Uvicorn |
|
||||
| 智能体框架 | LangGraph + LangChain |
|
||||
| 任务队列 | ARQ (Redis) |
|
||||
| 数据库 | PostgreSQL (业务数据) + Neo4j (图谱) + Milvus (向量) |
|
||||
| 对象存储 | MinIO |
|
||||
| 缓存/消息 | Redis |
|
||||
| 文档解析 | MinerU + PaddleX + RapidOCR + DeepSeek OCR |
|
||||
| 沙盒执行 | Docker Sandbox |
|
||||
|
||||
---
|
||||
|
||||
## 2. 整体架构
|
||||
|
||||
### 2.1 架构分层
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 前端层 (Frontend) │
|
||||
│ Vue 3 + Vite + Pinia + Ant Design Vue + Sigma.js/G6 │
|
||||
│ web/src/ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ API 网关层 (Gateway) │
|
||||
│ FastAPI + 路由注册 + 认证中间件 + CORS + 限流 │
|
||||
│ backend/server/ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 业务服务层 (Services) │
|
||||
│ 聊天服务、运行队列、知识库、Skills、MCP、SubAgents │
|
||||
│ backend/package/yuxi/services/ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 智能体层 (Agents) │
|
||||
│ BaseAgent + LangGraph + 中间件 + 工具集 + 沙盒后端 │
|
||||
│ backend/package/yuxi/agents/ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 数据访问层 (Repositories) │
|
||||
│ SQLAlchemy + 异步会话 + 业务模型 │
|
||||
│ backend/package/yuxi/repositories/ │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 基础设施层 (Infrastructure) │
|
||||
│ PostgreSQL + Neo4j + Milvus + Redis + MinIO │
|
||||
│ backend/package/yuxi/storage/ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 Docker Compose 服务拓扑
|
||||
|
||||
| 服务 | 容器名 | 端口 | 职责 |
|
||||
|------|--------|------|------|
|
||||
| api-dev | api-dev | 5050 | FastAPI 主服务(热重载) |
|
||||
| worker-dev | worker-dev | - | ARQ 异步任务 Worker |
|
||||
| web-dev | web-dev | 5173 | Vue 前端(热重载) |
|
||||
| sandbox-provisioner | sandbox-provisioner | 8002 | 沙盒环境供应器 |
|
||||
| postgres | postgres | 5432 | 业务与知识库元数据 |
|
||||
| redis | redis | 6379 | 运行事件、队列状态、缓存 |
|
||||
| minio | minio | 9000/9001 | 对象存储 |
|
||||
| milvus | milvus | 19530 | 向量检索 |
|
||||
| graph | graph | 7474/7687 | Neo4j 知识图谱 |
|
||||
| mineru-vllm-server | mineru-vllm-server | 30000 | 文档解析 VLLM 服务(profile: all) |
|
||||
| mineru-api | mineru-api | 30001 | 文档解析 API(profile: all) |
|
||||
| paddlex | paddlex-ocr | 8080 | PaddleX OCR(profile: all) |
|
||||
|
||||
---
|
||||
|
||||
## 3. 后端核心模块
|
||||
|
||||
后端分为两个顶层边界:`backend/server` 是 Web 应用入口与 HTTP 适配层,`backend/package/yuxi` 是可复用业务包。
|
||||
|
||||
### 3.1 入口与路由层 (`backend/server/`)
|
||||
|
||||
#### 3.1.1 主入口 (`server/main.py`)
|
||||
|
||||
- 创建 FastAPI 应用,注册 lifespan 生命周期
|
||||
- 挂载所有业务路由到 `/api` 前缀
|
||||
- 注册中间件:CORS、访问日志、登录限流、认证
|
||||
- 集成 WebSocket 聊天端点和 Slack Webhook
|
||||
|
||||
#### 3.1.2 路由注册 (`server/routers/__init__.py`)
|
||||
|
||||
| 路由模块 | API 前缀 | 职责 | LITE 模式 |
|
||||
|----------|----------|------|-----------|
|
||||
| `system_router.py` | `/api/system/*` | 健康检查、全局配置 | ✓ |
|
||||
| `auth_router.py` | `/api/auth/*` | 登录、用户信息、OIDC | ✓ |
|
||||
| `chat_router.py` | `/api/chat/*` | 对话、消息流、运行态 | ✓ |
|
||||
| `dashboard_router.py` | `/api/dashboard/*` | 仪表盘聚合数据 | ✓ |
|
||||
| `auth_dept_router.py` | `/api/departments/*` | 部门与权限 | ✓ |
|
||||
| `system_task_router.py` | `/api/tasks/*` | 后台任务管理 | ✓ |
|
||||
| `mcp_router.py` | `/api/system/mcp-servers/*` | MCP 服务管理 | ✓ |
|
||||
| `model_provider_router.py` | `/api/system/model-providers/*` | 模型供应商配置 | ✓ |
|
||||
| `skill_router.py` | `/api/system/skills/*` | Skills 管理 | ✓ |
|
||||
| `subagent_router.py` | `/api/system/subagents/*` | 子智能体管理 | ✓ |
|
||||
| `tool_router.py` | `/api/system/tools/*` | 工具列表与配置 | ✓ |
|
||||
| `auth_apikey_router.py` | `/api/apikey/*` | API Key 管理 | ✓ |
|
||||
| `filesystem_router.py` | `/api/viewer/filesystem/*` | 工作台文件系统视图 | ✓ |
|
||||
| `workspace_router.py` | `/api/workspace/*` | 用户个人工作区 | ✓ |
|
||||
| `channels_router.py` | `/api/channels/*` | 多渠道网关管理 | ✓ |
|
||||
| `webhook_router.py` | `/api/webhook/*` | 统一 Webhook 分发 | ✓ |
|
||||
| `knowledge_router.py` | `/api/knowledge/*` | 知识库管理与检索 | ✗ |
|
||||
| `knowledge_eval_router.py` | `/api/evaluation/*` | 知识库评估 | ✗ |
|
||||
| `knowledge_mindmap_router.py` | `/api/mindmap/*` | 思维导图生成与查询 | ✗ |
|
||||
| `graph_router.py` | `/api/graph/*` | 图谱查询与管理 | ✗ |
|
||||
|
||||
#### 3.1.3 Worker 入口 (`server/worker_main.py`)
|
||||
|
||||
- ARQ Worker 入口,导入 `WorkerSettings`
|
||||
- 处理智能体运行等异步任务
|
||||
|
||||
### 3.2 智能体体系 (`backend/package/yuxi/agents/`)
|
||||
|
||||
#### 3.2.1 核心基类
|
||||
|
||||
| 文件 | 类/函数 | 职责 |
|
||||
|------|---------|------|
|
||||
| `base.py` | `BaseAgent` | 智能体基类,定义 graph 编译、消息流式处理、历史记录、checkpointer 管理 |
|
||||
| `context.py` | `BaseContext` | 智能体运行配置上下文,包含模型、工具、知识库、MCP、Skills、子智能体等配置 |
|
||||
| `state.py` | `BaseState` | LangGraph 状态定义,包含 messages 和 artifacts |
|
||||
| `state.py` | `AgentStatePayload` | 前端可消费的序列化状态结构 |
|
||||
| `models.py` | `load_chat_model()` | 根据 v1/v2 spec 加载聊天模型,支持多供应商 |
|
||||
|
||||
#### 3.2.2 内置智能体 (`agents/buildin/`)
|
||||
|
||||
| 智能体 | 文件 | 职责 |
|
||||
|--------|------|------|
|
||||
| ChatbotAgent | `chatbot/graph.py` | 基础对话机器人,支持文件上传、知识库、MCP、Skills、子智能体 |
|
||||
| DeepAgent | `deep_agent/graph.py` | 深度研究智能体,用于复杂任务分解与执行 |
|
||||
|
||||
#### 3.2.3 中间件 (`agents/middlewares/`)
|
||||
|
||||
中间件负责把各种能力挂载到智能体运行时:
|
||||
|
||||
| 中间件 | 职责 |
|
||||
|--------|------|
|
||||
| `KnowledgeBaseMiddleware` | 知识库检索工具注入 |
|
||||
| `SkillsMiddleware` | Skills 提示词注入、依赖展开、动态激活 |
|
||||
| `RuntimeConfigMiddleware` | 运行时配置应用(模型/工具/MCP/提示词) |
|
||||
| `SummaryOffloadMiddleware` | 上下文摘要优化(token 阈值触发) |
|
||||
| `SubAgentMiddleware` | 子智能体调度 |
|
||||
| `FilesystemMiddleware` | 沙盒文件系统后端 |
|
||||
| `TodoListMiddleware` | 待办事项管理 |
|
||||
| `PatchToolCallsMiddleware` | 工具调用补丁 |
|
||||
| `ModelRetryMiddleware` | 模型重试机制 |
|
||||
|
||||
#### 3.2.4 工具集 (`agents/toolkits/`)
|
||||
|
||||
| 模块 | 职责 |
|
||||
|------|------|
|
||||
| `buildin/tools.py` | 内置工具(如 tavily_search、ask_user_question) |
|
||||
| `kbs/tools.py` | 知识库相关工具 |
|
||||
| `mysql/tools.py` | MySQL 数据库工具 |
|
||||
| `registry.py` | 工具注册中心 |
|
||||
|
||||
#### 3.2.5 后端执行 (`agents/backends/`)
|
||||
|
||||
| 模块 | 职责 |
|
||||
|------|------|
|
||||
| `sandbox/backend.py` | 沙盒执行后端 |
|
||||
| `sandbox/paths.py` | 沙盒路径管理 |
|
||||
| `composite.py` | 复合后端(知识库 + Skills + 沙盒) |
|
||||
| `skills_backend.py` | Skills 执行后端 |
|
||||
|
||||
### 3.3 服务层 (`backend/package/yuxi/services/`)
|
||||
|
||||
服务层是用例层,负责串联 repositories、agents、knowledge、storage 和外部系统。
|
||||
|
||||
| 服务文件 | 职责 | 关键函数 |
|
||||
|----------|------|----------|
|
||||
| `chat_service.py` | 聊天核心服务 | `agent_chat()`, `stream_agent_chat()`, `stream_agent_resume()`, `get_agent_state_view()` |
|
||||
| `agent_run_service.py` | Agent 运行管理(创建/轮询/取消) | `create_agent_run_view()`, `stream_agent_run_events()`, `cancel_agent_run_view()` |
|
||||
| `run_worker.py` | ARQ Worker 任务处理 | `process_agent_run()`, `RunContext`, `ChunkedEventWriter` |
|
||||
| `run_queue_service.py` | 运行队列与事件流 | `get_arq_pool()`, `append_run_stream_event()`, `publish_cancel_signal()` |
|
||||
| `skill_service.py` | Skills 业务逻辑 | `import_skill_zip()`, `list_skills()`, `install_builtin_skill()`, `update_builtin_skill()` |
|
||||
| `mcp_service.py` | MCP 服务管理 | `get_mcp_tools()`, `get_enabled_mcp_server_config()`, `ensure_builtin_mcp_servers_in_db()` |
|
||||
| `subagent_service.py` | 子智能体管理 | `get_subagents_from_names()`, `init_builtin_subagents()` |
|
||||
| `langfuse_service.py` | 可观测性追踪 | `build_run_context()`, `get_trace_info()`, `flush_langfuse()` |
|
||||
| `model_provider_service.py` | 模型供应商配置 | `ensure_builtin_model_providers_in_db()`, `get_all_model_providers()` |
|
||||
| `model_cache.py` | 模型信息缓存 | `model_cache.rebuild()`, `is_v2_spec_format()` |
|
||||
| `conversation_service.py` | 对话管理 | - |
|
||||
| `filesystem_service.py` | 文件系统服务 | - |
|
||||
| `knowledge_fs_service.py` | 知识库文件服务 | - |
|
||||
| `task_service.py` | 后台任务调度 | `tasker.start()`, `tasker.shutdown()` |
|
||||
| `upload_utils.py` | 文件上传工具 | - |
|
||||
| `workspace_service.py` | 工作区服务 | - |
|
||||
| `evaluation_service.py` | 知识库评估 | - |
|
||||
| `feedback_service.py` | 反馈服务 | - |
|
||||
| `oidc_service.py` | OIDC 认证 | - |
|
||||
| `tool_service.py` | 工具元数据 | `get_tool_metadata()` |
|
||||
|
||||
### 3.4 知识库领域 (`backend/package/yuxi/knowledge/`)
|
||||
|
||||
| 文件 | 类/函数 | 职责 |
|
||||
|------|---------|------|
|
||||
| `base.py` | `KnowledgeBase` (ABC) | 知识库抽象基类,定义统一接口(创建/删除/查询/文件管理) |
|
||||
| `manager.py` | `KnowledgeBaseManager` | 知识库管理器,统一管理多种类型知识库实例 |
|
||||
| `factory.py` | `KnowledgeBaseFactory` | 知识库工厂,根据类型创建实例 |
|
||||
| `implementations/dify.py` | `DifyKnowledgeBase` | Dify 知识库实现 |
|
||||
| `implementations/milvus.py` | `MilvusKnowledgeBase` | Milvus 向量知识库实现 |
|
||||
| `chunking/` | - | 文档分块策略(RAGflow-like 语义分块) |
|
||||
| `graphs/adapters/` | - | 图谱适配与上传服务 |
|
||||
| `utils/kb_utils.py` | - | 知识库通用工具 |
|
||||
|
||||
### 3.5 数据访问层 (`backend/package/yuxi/repositories/`)
|
||||
|
||||
| 仓库文件 | 职责 |
|
||||
|----------|------|
|
||||
| `skill_repository.py` | Skills CRUD |
|
||||
| `task_repository.py` | 后台任务 CRUD |
|
||||
| `user_repository.py` | 用户 CRUD |
|
||||
| `agent_config_repository.py` | 智能体配置 CRUD |
|
||||
| `agent_run_repository.py` | 智能体运行记录 CRUD |
|
||||
| `conversation_repository.py` | 对话与消息 CRUD |
|
||||
| `knowledge_base_repository.py` | 知识库元数据 CRUD |
|
||||
| `knowledge_file_repository.py` | 知识库文件 CRUD |
|
||||
| `evaluation_repository.py` | 评估基准与结果 CRUD |
|
||||
|
||||
### 3.6 存储层 (`backend/package/yuxi/storage/`)
|
||||
|
||||
| 模块 | 职责 |
|
||||
|------|------|
|
||||
| `postgres/manager.py` | `PostgresManager` - 数据库连接池、会话管理、schema 迁移 |
|
||||
| `postgres/models_business.py` | 业务数据模型(User、Conversation、AgentRun 等) |
|
||||
| `postgres/models_knowledge.py` | 知识库数据模型(KnowledgeBase、KnowledgeFile 等) |
|
||||
| `postgres/models_channels.py` | 渠道网关数据模型 |
|
||||
| `minio/client.py` | MinIO 客户端封装 |
|
||||
|
||||
### 3.7 多渠道网关 (`backend/package/yuxi/channels/`)
|
||||
|
||||
支持 20+ 即时通讯渠道接入:
|
||||
|
||||
| 渠道 | 适配器位置 |
|
||||
|------|-----------|
|
||||
| Slack | `channels/adapters/slack/` |
|
||||
| Discord | `channels/adapters/discord/` |
|
||||
| Telegram | `channels/adapters/telegram/` |
|
||||
| 飞书 (Feishu) | `channels/adapters/feishu/` |
|
||||
| 微信 (WeChat) | `channels/adapters/wechat/` |
|
||||
| QQ 机器人 | `channels/adapters/qqbot/` |
|
||||
| 钉钉 | `channels/adapters/dingding/` |
|
||||
| Microsoft Teams | `channels/adapters/msteams/` |
|
||||
| Matrix | `channels/adapters/matrix/` |
|
||||
| IRC | `channels/adapters/irc/` |
|
||||
| Line | `channels/adapters/line/` |
|
||||
| Twitch | `channels/adapters/twitch/` |
|
||||
| Nostr | `channels/adapters/nostr/` |
|
||||
| Signal | `channels/adapters/signal/` |
|
||||
| WhatsApp | `channels/adapters/whatsapp/` |
|
||||
| Urbit | `channels/adapters/urbit/` |
|
||||
| 元宝 (Yuanbao) | `channels/adapters/yuanbao/` |
|
||||
| Zalo | `channels/adapters/zalo_oa/`, `zalo_user/` |
|
||||
|
||||
核心文件:
|
||||
- `channels/manager.py` - 渠道管理器
|
||||
- `channels/base.py` - 渠道基类
|
||||
- `channels/router.py` - 消息路由
|
||||
- `channels/message_actions.py` - 消息动作处理
|
||||
|
||||
### 3.8 配置与模型 (`backend/package/yuxi/config/`, `models/`)
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `config/app.py` | 应用配置管理 |
|
||||
| `config/builtin_providers.py` | 内置模型供应商配置 |
|
||||
| `config/static/models.py` | 静态模型信息 |
|
||||
| `models/chat.py` | 聊天模型适配 |
|
||||
| `models/embed.py` | Embedding 模型适配 |
|
||||
| `models/rerank.py` | Rerank 模型适配 |
|
||||
|
||||
### 3.9 文档解析 (`backend/package/yuxi/plugins/parser/`)
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `unified.py` | `Parser` 统一入口,封装所有解析实现 |
|
||||
| `factory.py` | 解析器工厂 |
|
||||
| `base.py` | 解析器抽象基类 |
|
||||
| `mineru.py` | MinerU 解析实现 |
|
||||
| `mineru_official.py` | MinerU 官方 API 实现 |
|
||||
| `pp_structure_v3.py` | PaddleX 结构解析 |
|
||||
| `rapid_ocr.py` | RapidOCR 实现 |
|
||||
| `deepseek_ocr.py` | DeepSeek OCR 实现 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 前端核心模块
|
||||
|
||||
前端是 Vue 3 + Vite 应用,业务入口集中在 `web/src/`。
|
||||
|
||||
### 4.1 入口与路由 (`web/src/`)
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `main.js` | 应用挂载入口 |
|
||||
| `App.vue` | 根组件 |
|
||||
| `router/index.js` | 路由配置,含权限守卫(requiresAuth/requiresAdmin/requiresSuperAdmin) |
|
||||
|
||||
**路由表:**
|
||||
|
||||
| 路径 | 页面 | 权限 |
|
||||
|------|------|------|
|
||||
| `/` | HomeView | 公开 |
|
||||
| `/login` | LoginView | 公开 |
|
||||
| `/agent` | AgentView | 登录用户 |
|
||||
| `/agent/:thread_id` | AgentView(带线程ID) | 登录用户 |
|
||||
| `/workspace` | WorkspaceView | 登录用户 |
|
||||
| `/graph` | GraphView | 管理员 |
|
||||
| `/database` | DataBaseView | 管理员 |
|
||||
| `/database/:database_id` | DataBaseInfoView | 管理员 |
|
||||
| `/dashboard` | DashboardView | 管理员 |
|
||||
| `/model-config` | ModelConfigView | 管理员 |
|
||||
| `/channels` | ChannelManageView | 管理员 |
|
||||
| `/extensions` | ExtensionsView | 超级管理员 |
|
||||
|
||||
### 4.2 API 封装 (`web/src/apis/`)
|
||||
|
||||
所有后端接口统一封装,复用 `base.js` 的请求、鉴权和错误处理。
|
||||
|
||||
| API 文件 | 职责 |
|
||||
|----------|------|
|
||||
| `base.js` | HTTP 客户端、请求/响应拦截、权限头处理 |
|
||||
| `agent_api.js` | 智能体管理、聊天接口 |
|
||||
| `knowledge_api.js` | 知识库管理、文档管理、查询 |
|
||||
| `graph_api.js` | 知识图谱操作 |
|
||||
| `mcp_api.js` | MCP 服务器管理 |
|
||||
| `skill_api.js` | Skills 管理 |
|
||||
| `subagent_api.js` | 子智能体管理 |
|
||||
| `system_api.js` | 系统状态、配置 |
|
||||
| `auth_api.js` | 认证、用户信息 |
|
||||
| `dashboard_api.js` | 仪表盘数据 |
|
||||
| `department_api.js` | 部门管理 |
|
||||
| `channel_api.js` | 渠道管理 |
|
||||
| `tool_api.js` | 工具信息 |
|
||||
| `tasker.js` | 任务管理 |
|
||||
| `mindmap_api.js` | 思维导图 |
|
||||
| `workspace_api.js` | 工作区 |
|
||||
| `viewer_filesystem.js` | 文件系统视图 |
|
||||
| `apikey_api.js` | API Key 管理 |
|
||||
|
||||
### 4.3 状态管理 (`web/src/stores/`)
|
||||
|
||||
使用 Pinia + `pinia-plugin-persistedstate` 持久化。
|
||||
|
||||
| Store 文件 | 职责 |
|
||||
|-----------|------|
|
||||
| `user.js` | 用户信息、登录状态、权限 |
|
||||
| `agent.js` | 智能体配置、初始化状态 |
|
||||
| `chatThreads.js` | 聊天线程列表、当前线程 |
|
||||
| `database.js` | 知识库列表、当前知识库 |
|
||||
| `graphStore.js` | 图谱数据、可视化状态 |
|
||||
| `theme.js` | 主题(暗黑/亮色) |
|
||||
| `config.js` | 系统配置 |
|
||||
| `chatUI.js` | 聊天界面状态 |
|
||||
| `tasker.js` | 任务状态 |
|
||||
| `channel.js` | 渠道配置 |
|
||||
| `info.js` | 系统信息 |
|
||||
|
||||
### 4.4 可组合逻辑 (`web/src/composables/`)
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `useAgentStreamHandler.js` | Agent 流式响应处理 |
|
||||
| `useAgentRunStream.js` | Agent runs SSE 流处理 |
|
||||
| `useApproval.js` | 审批逻辑处理 |
|
||||
| `useMention.js` | @提及功能 |
|
||||
| `useStreamSmoother.js` | 流式消息平滑调度 |
|
||||
|
||||
### 4.5 视图与组件 (`web/src/views/`, `components/`)
|
||||
|
||||
| 视图 | 职责 |
|
||||
|------|------|
|
||||
| `AgentView.vue` | 智能体对话主页面 |
|
||||
| `WorkspaceView.vue` | 个人工作区(文件管理) |
|
||||
| `DataBaseView.vue` | 知识库列表与管理 |
|
||||
| `DataBaseInfoView.vue` | 知识库详情与文件管理 |
|
||||
| `GraphView.vue` | 知识图谱可视化 |
|
||||
| `DashboardView.vue` | 数据统计仪表盘 |
|
||||
| `ModelConfigView.vue` | 模型供应商配置 |
|
||||
| `ChannelManageView.vue` | 渠道网关管理 |
|
||||
| `ExtensionsView.vue` | 扩展管理(Skills/MCP/SubAgents) |
|
||||
| `LoginView.vue` | 登录页面 |
|
||||
| `HomeView.vue` | 首页 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键类与函数详解
|
||||
|
||||
### 5.1 智能体基类
|
||||
|
||||
#### `BaseAgent` (`agents/base.py`)
|
||||
|
||||
```python
|
||||
class BaseAgent:
|
||||
name = "base_agent"
|
||||
description = "base_agent"
|
||||
capabilities: list[str] = []
|
||||
context_schema: type[BaseContext] = BaseContext
|
||||
|
||||
# 核心方法
|
||||
async def get_graph(self, **kwargs) -> CompiledStateGraph # 子类必须实现
|
||||
async def stream_messages(self, messages, input_context=None, **kwargs) # 流式消息
|
||||
async def stream_messages_with_state(self, messages, input_context=None, **kwargs) # 流式消息+状态
|
||||
async def invoke_messages(self, messages, input_context=None, **kwargs) # 同步调用
|
||||
async def get_history(self, user_id, thread_id) -> list[dict] # 获取历史
|
||||
async def get_info(self, include_configurable_items=True) # 获取元数据
|
||||
def reload_graph(self) # 重置 graph 缓存
|
||||
```
|
||||
|
||||
#### `BaseContext` (`agents/context.py`)
|
||||
|
||||
```python
|
||||
@dataclass(kw_only=True)
|
||||
class BaseContext:
|
||||
thread_id: str # 对话线程ID
|
||||
user_id: str # 用户ID
|
||||
system_prompt: str # 系统提示词
|
||||
model: str # 主模型(v2 spec: provider_id:model_id)
|
||||
tools: list[str] # 启用工具列表
|
||||
knowledges: list[str] # 启用知识库列表
|
||||
mcps: list[str] # 启用 MCP 服务器列表
|
||||
skills: list[str] # 启用 Skills 列表
|
||||
subagents_model: str # 子智能体默认模型
|
||||
subagents: list[str] # 启用子智能体列表
|
||||
summary_threshold: int # 上下文摘要触发阈值(KB)
|
||||
```
|
||||
|
||||
### 5.2 聊天服务
|
||||
|
||||
#### `chat_service.py` 核心函数
|
||||
|
||||
| 函数 | 职责 |
|
||||
|------|------|
|
||||
| `agent_chat()` | 非流式对话,返回完整响应 |
|
||||
| `stream_agent_chat()` | 流式对话,yield 事件块 |
|
||||
| `stream_agent_resume()` | 恢复中断的对话(处理 human-in-the-loop) |
|
||||
| `get_agent_state_view()` | 获取 Agent 当前状态 |
|
||||
| `save_messages_from_langgraph_state()` | 从 LangGraph state 持久化消息 |
|
||||
| `extract_agent_state()` | 从 state 提取 todos/files/artifacts |
|
||||
|
||||
### 5.3 Agent Run 服务
|
||||
|
||||
#### `agent_run_service.py` 核心函数
|
||||
|
||||
| 函数 | 职责 |
|
||||
|------|------|
|
||||
| `create_agent_run_view()` | 创建后台运行任务,入队 ARQ |
|
||||
| `stream_agent_run_events()` | SSE 流式推送运行事件 |
|
||||
| `cancel_agent_run_view()` | 取消运行任务 |
|
||||
| `get_active_run_by_thread()` | 获取线程的活跃运行 |
|
||||
|
||||
### 5.4 Worker 任务处理
|
||||
|
||||
#### `run_worker.py` 核心类/函数
|
||||
|
||||
| 类/函数 | 职责 |
|
||||
|---------|------|
|
||||
| `process_agent_run()` | ARQ 任务主函数,消费流并写入 Redis |
|
||||
| `RunContext` | 运行上下文,管理取消信号监听 |
|
||||
| `ChunkedEventWriter` | 事件块缓冲写入器 |
|
||||
| `WorkerSettings` | ARQ Worker 配置(max_tries=2, job_timeout=900s) |
|
||||
|
||||
### 5.5 知识库管理
|
||||
|
||||
#### `KnowledgeBaseManager` (`knowledge/manager.py`)
|
||||
|
||||
```python
|
||||
class KnowledgeBaseManager:
|
||||
async def create_database(name, description, kb_type="lightrag", ...) # 创建知识库
|
||||
async def delete_database(db_id) # 删除知识库
|
||||
async def add_file_record(db_id, item, params) # 添加文件记录
|
||||
async def parse_file(db_id, file_id) # 解析文件为 Markdown
|
||||
async def index_file(db_id, file_id) # 索引文件到向量库
|
||||
async def aquery(query_text, db_id, **kwargs) # 异步查询
|
||||
def get_retrievers() -> dict[str, dict] # 获取所有检索器
|
||||
```
|
||||
|
||||
### 5.6 MCP 服务
|
||||
|
||||
#### `mcp_service.py` 核心函数
|
||||
|
||||
| 函数 | 职责 |
|
||||
|------|------|
|
||||
| `get_mcp_tools()` | 获取指定服务器的工具(带缓存) |
|
||||
| `get_enabled_mcp_tools()` | Agent 统一入口(自动过滤 disabled_tools) |
|
||||
| `get_tools_from_all_servers()` | 获取所有启用服务器的工具 |
|
||||
| `ensure_builtin_mcp_servers_in_db()` | 同步内置 MCP 配置到数据库 |
|
||||
| `create_mcp_server()` / `update_mcp_server()` / `delete_mcp_server()` | CRUD |
|
||||
|
||||
### 5.7 Skills 服务
|
||||
|
||||
#### `skill_service.py` 核心函数
|
||||
|
||||
| 函数 | 职责 |
|
||||
|------|------|
|
||||
| `import_skill_zip()` | 从 ZIP 导入 Skill |
|
||||
| `list_skills()` | 列出所有 Skills |
|
||||
| `install_builtin_skill()` | 安装内置 Skill |
|
||||
| `update_builtin_skill()` | 更新内置 Skill(带覆盖确认) |
|
||||
| `sync_thread_visible_skills()` | 同步线程可见 Skills 到沙盒 |
|
||||
|
||||
### 5.8 数据库管理
|
||||
|
||||
#### `PostgresManager` (`storage/postgres/manager.py`)
|
||||
|
||||
```python
|
||||
class PostgresManager(metaclass=SingletonMeta):
|
||||
def initialize() # 初始化连接池
|
||||
async def create_business_tables() # 创建业务表
|
||||
async def ensure_business_schema() # 确保业务 schema(增量迁移)
|
||||
async def ensure_knowledge_schema() # 确保知识库 schema
|
||||
async def get_async_session_context() # 异步会话上下文管理器
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 数据流与运行链路
|
||||
|
||||
### 6.1 典型智能体对话流程
|
||||
|
||||
```
|
||||
1. AgentView 收集输入、附件、配置
|
||||
↓
|
||||
2. web/src/apis/agent_api.js 调用 /api/chat/*
|
||||
↓
|
||||
3. server/routers/chat_router.py → chat_service / agent_run_service
|
||||
↓
|
||||
4. 服务层读取 conversation、agent_config、tools、skills、knowledge
|
||||
↓
|
||||
5a. 同步模式: 直接调用 stream_agent_chat() → 返回 SSE 流
|
||||
5b. 异步模式: create_agent_run_view() → ARQ 入队 → worker 执行
|
||||
↓
|
||||
6. worker 执行 LangGraph 智能体
|
||||
- 中间件挂载知识库、工具、Skills、MCP、附件、沙盒
|
||||
- 运行事件写入 Redis
|
||||
↓
|
||||
7. 最终状态和业务记录写入 Postgres
|
||||
文件和产物落到 saves/、MinIO 或沙盒映射目录
|
||||
↓
|
||||
8. 前端通过 SSE/轮询消费运行事件
|
||||
渲染消息、工具调用、引用来源、产物卡片、文件预览
|
||||
```
|
||||
|
||||
### 6.2 Agent Run 异步流程
|
||||
|
||||
```
|
||||
用户请求
|
||||
↓
|
||||
create_agent_run_view()
|
||||
- 创建 AgentRun 记录(status=pending)
|
||||
- ARQ enqueue_job("process_agent_run", run.id)
|
||||
↓
|
||||
worker process_agent_run()
|
||||
- 加载用户和配置
|
||||
- mark_run_running()
|
||||
- stream_agent_chat() 消费流
|
||||
- ChunkedEventWriter 缓冲写入 Redis
|
||||
- 处理取消信号
|
||||
- mark_run_terminal(status=completed/failed/cancelled)
|
||||
↓
|
||||
前端 SSE 连接 /api/chat/runs/{run_id}/events
|
||||
- 轮询 Redis 事件
|
||||
- 心跳保活
|
||||
- 终端状态返回 close 事件
|
||||
```
|
||||
|
||||
### 6.3 知识库文档处理流程
|
||||
|
||||
```
|
||||
上传文件
|
||||
↓
|
||||
add_file_record() → status=UPLOADED
|
||||
↓
|
||||
parse_file() → 调用 Parser.aparse()
|
||||
- status=PARSING
|
||||
- 解析为 Markdown
|
||||
- 保存到 MinIO
|
||||
- status=PARSED / ERROR_PARSING
|
||||
↓
|
||||
index_file() → 向量化和图谱构建
|
||||
- status=INDEXING
|
||||
- 分块 → Embedding → 向量库
|
||||
- status=INDEXED / ERROR_INDEXING
|
||||
↓
|
||||
aquery() → 检索 → Rerank → 返回结果
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 依赖关系
|
||||
|
||||
### 7.1 后端核心依赖
|
||||
|
||||
```
|
||||
fastapi>=0.121 # Web 框架
|
||||
uvicorn[standard]>=0.34 # ASGI 服务器
|
||||
arq>=0.26.3 # 异步任务队列
|
||||
langgraph>=1.0.1 # 智能体编排
|
||||
langchain>=1.2.0 # LLM 框架
|
||||
langchain-openai>=1.0 # OpenAI 兼容层
|
||||
langchain-mcp-adapters # MCP 适配
|
||||
lightrag-hku>=1.4.6 # 知识图谱
|
||||
neo4j>=5.28 # 图数据库
|
||||
pymilvus>=2.5 # 向量数据库
|
||||
asyncpg>=0.30 # PostgreSQL 异步驱动
|
||||
psycopg[binary,pool] # PostgreSQL 连接池
|
||||
redis>=5.2 # 缓存/消息
|
||||
minio>=7.2 # 对象存储
|
||||
sqlalchemy[asyncio]>=2 # ORM
|
||||
langfuse>=4.0 # 可观测性
|
||||
pydantic # 数据验证
|
||||
pyjwt>=2.8 # JWT 认证
|
||||
```
|
||||
|
||||
### 7.2 前端核心依赖
|
||||
|
||||
```
|
||||
vue@^3.5 # 框架
|
||||
vue-router@^4.6 # 路由
|
||||
pinia@^3.0 # 状态管理
|
||||
pinia-plugin-persistedstate # 状态持久化
|
||||
ant-design-vue@^4.2 # UI 组件库
|
||||
lucide-vue-next # 图标库
|
||||
@antv/g6@^5.0 # 图谱可视化
|
||||
sigma@^3.0 # 图谱渲染
|
||||
@vueuse/core # 组合式工具
|
||||
vite@^7.3 # 构建工具
|
||||
less@^4.5 # CSS 预处理器
|
||||
marked@^16 # Markdown 渲染
|
||||
highlight.js # 代码高亮
|
||||
```
|
||||
|
||||
### 7.3 模块依赖图
|
||||
|
||||
```
|
||||
server/main.py
|
||||
├── server/routers/*
|
||||
│ └── yuxi.services.*
|
||||
│ ├── yuxi.agents.* (BaseAgent, agent_manager)
|
||||
│ ├── yuxi.knowledge.* (KnowledgeBaseManager)
|
||||
│ ├── yuxi.repositories.* (SQLAlchemy CRUD)
|
||||
│ ├── yuxi.storage.* (Postgres, MinIO)
|
||||
│ └── yuxi.channels.* (多渠道网关)
|
||||
└── server/utils/* (lifespan, auth, middleware)
|
||||
|
||||
yuxi.agents.base.BaseAgent
|
||||
├── yuxi.agents.context.BaseContext
|
||||
├── yuxi.agents.models.load_chat_model
|
||||
└── yuxi.storage.postgres.manager.pg_manager
|
||||
|
||||
yuxi.services.chat_service
|
||||
├── yuxi.agents.buildin.agent_manager
|
||||
├── yuxi.repositories.conversation_repository
|
||||
├── yuxi.services.langfuse_service
|
||||
└── yuxi.plugins.guard.content_guard
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 项目运行方式
|
||||
|
||||
### 8.1 环境要求
|
||||
|
||||
- Docker & Docker Compose
|
||||
- Python 3.12+(后端开发)
|
||||
- Node.js 20+ + pnpm(前端开发)
|
||||
- Git
|
||||
|
||||
### 8.2 快速启动
|
||||
|
||||
```bash
|
||||
# 1. 克隆项目
|
||||
git clone --branch v0.6.2 --depth 1 https://github.com/xerrors/Yuxi.git
|
||||
cd Yuxi
|
||||
|
||||
# 2. 初始化(创建 .env 等)
|
||||
# Linux/macOS
|
||||
./scripts/init.sh
|
||||
# Windows PowerShell
|
||||
.\scripts\init.ps1
|
||||
|
||||
# 3. 启动全部服务
|
||||
docker compose up -d
|
||||
|
||||
# 或使用 Makefile
|
||||
make up
|
||||
|
||||
# 4. 访问前端
|
||||
open http://localhost:5173
|
||||
```
|
||||
|
||||
### 8.3 LITE 模式(轻量启动)
|
||||
|
||||
跳过知识库、图谱、评估等重依赖:
|
||||
|
||||
```bash
|
||||
make up-lite
|
||||
# 等效于:
|
||||
# LITE_MODE=true VITE_USE_RUNS_API=false docker compose up -d postgres redis minio api web
|
||||
```
|
||||
|
||||
### 8.4 开发常用命令
|
||||
|
||||
```bash
|
||||
# 查看日志
|
||||
make logs
|
||||
docker logs api-dev --tail 100
|
||||
docker logs worker-dev --tail 100
|
||||
|
||||
# 代码格式化
|
||||
make format
|
||||
# 等效于:
|
||||
cd backend && uv run ruff format package
|
||||
cd backend && uv run ruff check package --fix
|
||||
cd web && pnpm run format
|
||||
cd web && pnpm run lint
|
||||
|
||||
# 运行测试
|
||||
cd backend && uv run --group test pytest
|
||||
|
||||
# 停止服务
|
||||
make down
|
||||
```
|
||||
|
||||
### 8.5 开发环境特性
|
||||
|
||||
- **热重载**:`api-dev` 和 `web-dev` 服务均配置热重载,本地修改代码后无需重启容器
|
||||
- **代码挂载**:`backend/server`、`backend/package`、`web/src` 等目录挂载到容器
|
||||
- **调试**:使用 `docker logs` 查看实时日志
|
||||
|
||||
---
|
||||
|
||||
## 9. 测试体系
|
||||
|
||||
测试代码放在 `backend/test/`,按三层组织:
|
||||
|
||||
| 层级 | 目录 | 职责 |
|
||||
|------|------|------|
|
||||
| 单元测试 | `test/unit/` | 不依赖外部服务的纯逻辑测试 |
|
||||
| 集成测试 | `test/integration/` | API 路由测试(需运行中的服务) |
|
||||
| 端到端测试 | `test/e2e/` | 完整业务流程测试 |
|
||||
|
||||
### 9.1 运行测试
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
uv run --group test pytest
|
||||
|
||||
# 带覆盖率
|
||||
uv run --group test pytest --cov=yuxi --cov-report=html
|
||||
|
||||
# 仅运行单元测试
|
||||
uv run --group test pytest -m unit
|
||||
|
||||
# 仅运行集成测试
|
||||
uv run --group test pytest -m integration
|
||||
|
||||
# 仅运行 e2e 测试
|
||||
uv run --group test pytest -m e2e
|
||||
```
|
||||
|
||||
### 9.2 测试配置
|
||||
|
||||
- `pytest.ini` 配置在 `backend/pyproject.toml` 中
|
||||
- 全局 fixtures 在 `test/conftest.py`
|
||||
- 各层独立 `conftest.py` 管理层级 fixtures
|
||||
|
||||
---
|
||||
|
||||
## 10. 附录:目录结构总览
|
||||
|
||||
```
|
||||
ForcePilot/
|
||||
├── .github/ # GitHub 配置(Issue 模板、工作流)
|
||||
│ ├── ISSUE_TEMPLATE/
|
||||
│ └── workflows/
|
||||
├── backend/ # 后端代码
|
||||
│ ├── package/ # 可复用业务包
|
||||
│ │ └── yuxi/ # 核心包
|
||||
│ │ ├── agents/ # 智能体体系
|
||||
│ │ │ ├── backends/ # 执行后端(沙盒、Skills)
|
||||
│ │ │ ├── buildin/ # 内置智能体
|
||||
│ │ │ ├── middlewares/# 中间件
|
||||
│ │ │ ├── toolkits/ # 工具注册与实现
|
||||
│ │ │ ├── base.py # BaseAgent
|
||||
│ │ │ ├── context.py # BaseContext
|
||||
│ │ │ ├── models.py # 模型加载
|
||||
│ │ │ └── state.py # 状态定义
|
||||
│ │ ├── channels/ # 多渠道网关
|
||||
│ │ │ └── adapters/ # 各渠道适配器
|
||||
│ │ ├── config/ # 应用配置
|
||||
│ │ ├── gateway/ # 网关协议
|
||||
│ │ ├── knowledge/ # 知识库领域
|
||||
│ │ │ ├── chunking/ # 分块策略
|
||||
│ │ │ ├── graphs/ # 图谱适配
|
||||
│ │ │ └── implementations/ # 具体实现
|
||||
│ │ ├── models/ # 模型适配(chat/embed/rerank)
|
||||
│ │ ├── plugins/ # 插件
|
||||
│ │ │ └── parser/ # 文档解析
|
||||
│ │ ├── repositories/ # 数据访问层
|
||||
│ │ ├── services/ # 业务服务层
|
||||
│ │ ├── storage/ # 存储基础设施
|
||||
│ │ │ ├── minio/ # 对象存储
|
||||
│ │ │ └── postgres/ # PostgreSQL
|
||||
│ │ └── utils/ # 通用工具
|
||||
│ ├── server/ # Web 应用入口
|
||||
│ │ ├── routers/ # HTTP 路由
|
||||
│ │ ├── utils/ # Web 层工具
|
||||
│ │ ├── main.py # FastAPI 入口
|
||||
│ │ └── worker_main.py # Worker 入口
|
||||
│ ├── test/ # 测试代码
|
||||
│ │ ├── unit/ # 单元测试
|
||||
│ │ ├── integration/ # 集成测试
|
||||
│ │ └── e2e/ # 端到端测试
|
||||
│ └── pyproject.toml # 后端项目配置
|
||||
├── web/ # 前端代码
|
||||
│ ├── src/
|
||||
│ │ ├── apis/ # API 封装
|
||||
│ │ ├── components/ # 可复用组件
|
||||
│ │ ├── composables/ # 可组合逻辑
|
||||
│ │ ├── layouts/ # 布局组件
|
||||
│ │ ├── router/ # 路由配置
|
||||
│ │ ├── stores/ # Pinia 状态
|
||||
│ │ ├── utils/ # 前端工具
|
||||
│ │ ├── views/ # 页面级视图
|
||||
│ │ ├── App.vue # 根组件
|
||||
│ │ └── main.js # 入口
|
||||
│ ├── public/
|
||||
│ ├── index.html
|
||||
│ ├── package.json
|
||||
│ └── vite.config.js
|
||||
├── docker/ # Docker 配置
|
||||
│ ├── api.Dockerfile
|
||||
│ ├── web.Dockerfile
|
||||
│ ├── sandbox_provisioner/ # 沙盒供应器
|
||||
│ └── volumes/ # 数据卷挂载点
|
||||
├── docs/ # 项目文档
|
||||
│ ├── .vitepress/ # VitePress 配置
|
||||
│ ├── agents/ # Agent 开发文档
|
||||
│ ├── develop-guides/ # 开发指南
|
||||
│ └── vibe/ # 开发者笔记
|
||||
├── docker-compose.yml # Docker Compose 配置
|
||||
├── Makefile # 快捷命令
|
||||
├── README.md # 项目说明
|
||||
├── ARCHITECTURE.md # 架构文档
|
||||
├── AGENTS.md # 开发准则
|
||||
└── .env.template # 环境变量模板
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 参考文档
|
||||
|
||||
- [ARCHITECTURE.md](ARCHITECTURE.md) - 架构代码地图
|
||||
- [AGENTS.md](AGENTS.md) - 开发准则与行为规范
|
||||
- [README.md](README.md) - 项目快速开始
|
||||
- [docker-compose.yml](docker-compose.yml) - 服务拓扑与配置
|
||||
- [backend/package/pyproject.toml](backend/package/pyproject.toml) - Python 依赖
|
||||
- [web/package.json](web/package.json) - Node.js 依赖
|
||||
@ -95,9 +95,7 @@ class ChannelMessageRecordRepository:
|
||||
cutoff = utc_now_naive() - timedelta(seconds=timeout_seconds)
|
||||
return [r for r in records if r.created_at and r.created_at < cutoff]
|
||||
|
||||
async def get_recent_records(
|
||||
self, channel_id: str, chat_id: str, limit: int = 10
|
||||
) -> list[ChannelMsgRecord]:
|
||||
async def get_recent_records(self, channel_id: str, chat_id: str, limit: int = 10) -> list[ChannelMsgRecord]:
|
||||
result = await self.db.execute(
|
||||
select(ChannelMsgRecord)
|
||||
.where(
|
||||
@ -109,12 +107,12 @@ class ChannelMessageRecordRepository:
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_chat_message_count(
|
||||
self, channel_id: str, chat_id: str, since_hours: int = 24
|
||||
) -> int:
|
||||
async def get_chat_message_count(self, channel_id: str, chat_id: str, since_hours: int = 24) -> int:
|
||||
cutoff = utc_now_naive() - timedelta(hours=since_hours)
|
||||
result = await self.db.execute(
|
||||
select(func.count()).select_from(ChannelMsgRecord).where(
|
||||
select(func.count())
|
||||
.select_from(ChannelMsgRecord)
|
||||
.where(
|
||||
ChannelMsgRecord.channel_id == channel_id,
|
||||
ChannelMsgRecord.chat_id == chat_id,
|
||||
ChannelMsgRecord.created_at >= cutoff,
|
||||
|
||||
@ -4,6 +4,7 @@ from typing import Any
|
||||
|
||||
from sqlalchemy import (
|
||||
BigInteger,
|
||||
Boolean,
|
||||
Column,
|
||||
DateTime,
|
||||
Index,
|
||||
@ -17,6 +18,31 @@ from yuxi.storage.postgres.models_business import Base
|
||||
from yuxi.utils.datetime_utils import format_utc_datetime, utc_now_naive
|
||||
|
||||
|
||||
class ChannelConfig(Base):
|
||||
__tablename__ = "channels_configs"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
channel_id = Column(String(32), unique=True, nullable=False, index=True)
|
||||
config_json = Column(JSONB, nullable=False, default=dict)
|
||||
enabled = Column(Boolean, nullable=False, default=True)
|
||||
registered_by = Column(String(64), nullable=True)
|
||||
created_at = Column(DateTime, nullable=False, default=utc_now_naive)
|
||||
updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive)
|
||||
|
||||
__table_args__ = (Index("idx_channels_configs_channel_id", "channel_id"),)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"channel_id": self.channel_id,
|
||||
"config_json": self.config_json or {},
|
||||
"enabled": self.enabled,
|
||||
"registered_by": self.registered_by,
|
||||
"created_at": format_utc_datetime(self.created_at),
|
||||
"updated_at": format_utc_datetime(self.updated_at),
|
||||
}
|
||||
|
||||
|
||||
class ChannelUserMapping(Base):
|
||||
"""渠道用户映射表 - 渠道用户与内部用户的绑定关系"""
|
||||
|
||||
@ -64,6 +90,18 @@ class ChannelThreadMapping(Base):
|
||||
"internal_user_id",
|
||||
name="uq_channel_thread",
|
||||
),
|
||||
Index(
|
||||
"idx_thread_mapping_user_recent",
|
||||
"channel_id",
|
||||
"internal_user_id",
|
||||
text("last_active_at DESC"),
|
||||
),
|
||||
Index(
|
||||
"idx_thread_mapping_chat_lookup",
|
||||
"channel_id",
|
||||
"channel_chat_id",
|
||||
text("last_active_at DESC"),
|
||||
),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
@ -115,6 +153,12 @@ class ChannelMsgRecord(Base):
|
||||
postgresql_where=(status == "processing"),
|
||||
),
|
||||
Index("idx_msg_records_chat", "channel_id", "chat_id"),
|
||||
Index(
|
||||
"idx_msg_records_channel_rt",
|
||||
"channel_id",
|
||||
"response_time_ms",
|
||||
postgresql_where=(response_time_ms.isnot(None)),
|
||||
),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
@ -148,7 +192,7 @@ class ChannelPolicyConfig(Base):
|
||||
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
channel_id = Column(String(32), nullable=False, unique=True)
|
||||
group_chat_mode = Column(String(16), nullable=False, default="all")
|
||||
group_chat_mode = Column(String(16), nullable=False, default="mention_only")
|
||||
whitelist_ids = Column(JSONB, nullable=False, default=list)
|
||||
welcome_message = Column(String(500), nullable=True)
|
||||
work_hours_start = Column(String(5), nullable=True, default="09:00")
|
||||
@ -183,7 +227,6 @@ class ChannelRoutingRule(Base):
|
||||
channel_id = Column(String(32), nullable=False)
|
||||
command = Column(String(64), nullable=False)
|
||||
agent_config_id = Column(String(64), nullable=False)
|
||||
priority = Column(Integer, nullable=False, default=0)
|
||||
created_at = Column(DateTime, nullable=False, default=utc_now_naive)
|
||||
updated_at = Column(DateTime, nullable=False, default=utc_now_naive, onupdate=utc_now_naive)
|
||||
|
||||
@ -198,7 +241,6 @@ class ChannelRoutingRule(Base):
|
||||
"channel_id": self.channel_id,
|
||||
"command": self.command,
|
||||
"agent_config_id": self.agent_config_id,
|
||||
"priority": self.priority,
|
||||
"created_at": format_utc_datetime(self.created_at),
|
||||
"updated_at": format_utc_datetime(self.updated_at),
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@ from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from server.routers import router
|
||||
from server.routers.ws_chat_router import ws_chat
|
||||
from yuxi.channels.adapters.slack.http_handler import slack_webhook
|
||||
from yuxi.channels.adapters.nostr.profile_api import profile_router
|
||||
from yuxi.channels.message_actions import ChannelNotFoundError
|
||||
from server.utils.lifespan import lifespan
|
||||
from server.utils.auth_middleware import is_public_path
|
||||
from server.utils.common_utils import setup_logging
|
||||
@ -47,8 +47,15 @@ app.include_router(router, prefix="/api")
|
||||
app.include_router(ws_chat)
|
||||
# Slack HTTP Webhook (单独注册以处理 URL verification challenge)
|
||||
app.include_router(slack_webhook)
|
||||
# Nostr Profile API
|
||||
app.include_router(profile_router)
|
||||
|
||||
|
||||
@app.exception_handler(ChannelNotFoundError)
|
||||
async def channel_not_found_handler(request: Request, exc: ChannelNotFoundError):
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
content={"code": -1, "data": None, "message": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
# CORS 设置
|
||||
app.add_middleware(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -89,11 +89,13 @@ async def websocket_chat(ws: WebSocket, client_id: str, token: str = Query(None)
|
||||
return
|
||||
|
||||
await ws.accept()
|
||||
await ws.send_json({
|
||||
"type": "connected",
|
||||
"payload": {"client_id": client_id, "user_id": user_id},
|
||||
"timestamp": _now_iso(),
|
||||
})
|
||||
await ws.send_json(
|
||||
{
|
||||
"type": "connected",
|
||||
"payload": {"client_id": client_id, "user_id": user_id},
|
||||
"timestamp": _now_iso(),
|
||||
}
|
||||
)
|
||||
|
||||
last_msg_time = time.monotonic()
|
||||
rate_bucket: list[float] = []
|
||||
@ -139,11 +141,13 @@ async def websocket_chat(ws: WebSocket, client_id: str, token: str = Query(None)
|
||||
now = time.monotonic()
|
||||
rate_bucket = [t for t in rate_bucket if now - t < 1.0]
|
||||
if len(rate_bucket) >= MAX_MSG_PER_SEC:
|
||||
await ws.send_json({
|
||||
"type": "error",
|
||||
"payload": {"code": 429, "message": "Rate limit exceeded"},
|
||||
"timestamp": _now_iso(),
|
||||
})
|
||||
await ws.send_json(
|
||||
{
|
||||
"type": "error",
|
||||
"payload": {"code": 429, "message": "Rate limit exceeded"},
|
||||
"timestamp": _now_iso(),
|
||||
}
|
||||
)
|
||||
continue
|
||||
rate_bucket.append(now)
|
||||
|
||||
@ -173,31 +177,37 @@ async def websocket_chat(ws: WebSocket, client_id: str, token: str = Query(None)
|
||||
)
|
||||
|
||||
request_id = identity.channel_message_id
|
||||
await ws.send_json({
|
||||
"type": "typing",
|
||||
"payload": {"status": "agent", "request_id": request_id},
|
||||
"timestamp": _now_iso(),
|
||||
})
|
||||
await ws.send_json(
|
||||
{
|
||||
"type": "typing",
|
||||
"payload": {"status": "agent", "request_id": request_id},
|
||||
"timestamp": _now_iso(),
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
await get_channel_manager().dispatch_inbound(message)
|
||||
except Exception as e:
|
||||
logger.error(f"WS message processing error: {e}")
|
||||
await ws.send_json({
|
||||
"type": "error",
|
||||
"payload": {
|
||||
"request_id": request_id,
|
||||
"code": 500,
|
||||
"message": f"处理出错: {str(e)}",
|
||||
},
|
||||
"timestamp": _now_iso(),
|
||||
})
|
||||
await ws.send_json(
|
||||
{
|
||||
"type": "error",
|
||||
"payload": {
|
||||
"request_id": request_id,
|
||||
"code": 500,
|
||||
"message": f"处理出错: {str(e)}",
|
||||
},
|
||||
"timestamp": _now_iso(),
|
||||
}
|
||||
)
|
||||
|
||||
await ws.send_json({
|
||||
"type": "done",
|
||||
"payload": {"request_id": request_id},
|
||||
"timestamp": _now_iso(),
|
||||
})
|
||||
await ws.send_json(
|
||||
{
|
||||
"type": "done",
|
||||
"payload": {"request_id": request_id},
|
||||
"timestamp": _now_iso(),
|
||||
}
|
||||
)
|
||||
|
||||
except WebSocketDisconnect:
|
||||
logger.info(f"WS client {client_id} disconnected")
|
||||
|
||||
@ -185,7 +185,14 @@ export const channelApi = {
|
||||
|
||||
getUserMapping: (channelId, userId) => apiGet(`/api/channels/${channelId}/users/${userId}`),
|
||||
|
||||
getChatMapping: (channelId, chatId) => apiGet(`/api/channels/${channelId}/chats/${chatId}`),
|
||||
getChatMapping: (channelId, chatId, internalUserId) => {
|
||||
const params = internalUserId ? { internal_user_id: internalUserId } : {}
|
||||
return apiGet(`/api/channels/${channelId}/chats/${chatId}`, { params })
|
||||
},
|
||||
|
||||
getCredentialStatus: (channelId) => apiGet(`/api/channels/${channelId}/credential-status`),
|
||||
|
||||
refreshCredential: (channelId) => apiAdminPost(`/api/channels/${channelId}/refresh-credential`),
|
||||
|
||||
getDashboardChannelStats: () => apiGet('/api/channels/status')
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ const userResult = ref(null)
|
||||
const userLoading = ref(false)
|
||||
|
||||
const chatSearchId = ref('')
|
||||
const chatInternalUserId = ref('')
|
||||
const chatResult = ref(null)
|
||||
const chatLoading = ref(false)
|
||||
|
||||
@ -50,7 +51,7 @@ async function searchChat() {
|
||||
chatLoading.value = true
|
||||
chatResult.value = null
|
||||
try {
|
||||
const res = await channelApi.getChatMapping(props.channelId, cid)
|
||||
const res = await channelApi.getChatMapping(props.channelId, cid, chatInternalUserId.value || undefined)
|
||||
if (seq !== searchChatSeq) return
|
||||
chatResult.value = res.data
|
||||
} catch (e) {
|
||||
@ -137,6 +138,15 @@ async function searchChat() {
|
||||
搜索
|
||||
</button>
|
||||
</div>
|
||||
<div class="search-bar">
|
||||
<input
|
||||
v-model="chatInternalUserId"
|
||||
type="text"
|
||||
class="search-input"
|
||||
placeholder="内部用户 ID(可选,用于精确匹配)..."
|
||||
@keydown.enter="searchChat"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="chatLoading" class="search-loading">搜索中...</div>
|
||||
<div v-else-if="chatResult" class="result-card">
|
||||
<div class="result-row">
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { RotateCcw, Plug, MessageCircle } from 'lucide-vue-next'
|
||||
import { RotateCcw, Plug, MessageCircle, Shield, RefreshCw } from 'lucide-vue-next'
|
||||
import ChannelStatusBadge from './ChannelStatusBadge.vue'
|
||||
import { channelIcons } from '@/constants/channelIcons'
|
||||
|
||||
@ -8,10 +8,12 @@ const props = defineProps({
|
||||
channel: { type: Object, required: true },
|
||||
toggling: { type: Boolean, default: false },
|
||||
batchMode: { type: Boolean, default: false },
|
||||
selected: { type: Boolean, default: false }
|
||||
selected: { type: Boolean, default: false },
|
||||
credentialStatus: { type: Object, default: null },
|
||||
credentialLoading: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['manage', 'toggle', 'restart', 'test', 'select'])
|
||||
const emit = defineEmits(['manage', 'toggle', 'restart', 'test', 'select', 'refreshCredential'])
|
||||
|
||||
const imgError = ref(false)
|
||||
|
||||
@ -41,6 +43,47 @@ const healthLabel = computed(() => {
|
||||
const map = { healthy: '正常', degraded: '降级', unhealthy: '异常' }
|
||||
return map[healthClass.value]
|
||||
})
|
||||
|
||||
const credentialClass = computed(() => {
|
||||
const cs = props.credentialStatus
|
||||
if (!cs || !cs.has_credential) return 'cred-none'
|
||||
if (cs.is_expired) return 'cred-expired'
|
||||
if (cs.expires_at && cs.source === 'db') {
|
||||
const remaining = new Date(cs.expires_at).getTime() - Date.now()
|
||||
if (remaining <= 3600000) return 'cred-soon'
|
||||
return 'cred-ok'
|
||||
}
|
||||
if (cs.source === 'memory') return 'cred-memory'
|
||||
if (cs.source === 'config' || cs.source === 'env') return 'cred-static'
|
||||
if (cs.source === 'file') return 'cred-file'
|
||||
return 'cred-ok'
|
||||
})
|
||||
|
||||
const credentialLabel = computed(() => {
|
||||
const map = {
|
||||
'cred-ok': '凭证正常',
|
||||
'cred-soon': '即将过期',
|
||||
'cred-expired': '已过期',
|
||||
'cred-none': '无凭证',
|
||||
'cred-memory': '内存存储',
|
||||
'cred-static': '静态凭证',
|
||||
'cred-file': '文件存储'
|
||||
}
|
||||
return map[credentialClass.value] || '凭证正常'
|
||||
})
|
||||
|
||||
const credentialSourceLabel = computed(() => {
|
||||
const map = { db: '数据库', config: '配置文件', env: '环境变量', file: '文件', memory: '内存', none: '无' }
|
||||
const cs = props.credentialStatus
|
||||
return cs ? (map[cs.source] || cs.source) : ''
|
||||
})
|
||||
|
||||
const showRefreshBtn = computed(() => {
|
||||
const cs = props.credentialStatus
|
||||
if (!cs) return false
|
||||
return cs.source === 'db' || cs.source === 'memory'
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -81,6 +124,31 @@ const healthLabel = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="credentialStatus" class="card-credential">
|
||||
<div class="credential-header">
|
||||
<div class="credential-status-badge" :class="credentialClass">
|
||||
<Shield :size="12" />
|
||||
<span>{{ credentialLabel }}</span>
|
||||
</div>
|
||||
<span class="credential-source">来源: {{ credentialSourceLabel }}</span>
|
||||
<button
|
||||
v-if="showRefreshBtn"
|
||||
class="btn-refresh-cred"
|
||||
:disabled="credentialLoading"
|
||||
title="刷新凭证"
|
||||
@click="emit('refreshCredential', channel.channel_id)"
|
||||
>
|
||||
<RefreshCw :size="12" :class="{ spinning: credentialLoading }" />
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="credentialStatus.last_updated" class="credential-detail">
|
||||
<span>更新于 {{ credentialStatus.last_updated }}</span>
|
||||
</div>
|
||||
<div v-if="credentialStatus.expires_at" class="credential-detail">
|
||||
<span>过期时间 {{ credentialStatus.expires_at }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="channel.health && channel.enabled" class="card-health">
|
||||
<span class="health-item">
|
||||
<span class="health-dot" :class="healthClass"></span>
|
||||
@ -305,6 +373,70 @@ const healthLabel = computed(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.card-credential {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
background: var(--gray-10);
|
||||
border: 1px solid var(--gray-100);
|
||||
|
||||
.credential-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.credential-status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
&.cred-ok { background: var(--color-success-100); color: var(--color-success-700); }
|
||||
&.cred-soon { background: var(--color-warning-100); color: var(--color-warning-700); }
|
||||
&.cred-expired { background: var(--color-error-100); color: var(--color-error-700); }
|
||||
&.cred-none { background: var(--gray-100); color: var(--gray-500); }
|
||||
&.cred-memory { background: var(--color-warning-100); color: var(--color-warning-700); }
|
||||
&.cred-static { background: var(--gray-100); color: var(--gray-600); }
|
||||
&.cred-file { background: var(--gray-100); color: var(--gray-600); }
|
||||
}
|
||||
|
||||
.credential-source {
|
||||
font-size: 12px;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
.btn-refresh-cred {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 1px solid var(--gray-200);
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--gray-500);
|
||||
cursor: pointer;
|
||||
margin-left: auto;
|
||||
transition: border-color 0.2s, color 0.2s;
|
||||
|
||||
&:hover { border-color: var(--main-200); color: var(--main-color); }
|
||||
&:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
.spinning { animation: spin 1s linear infinite; }
|
||||
}
|
||||
|
||||
.credential-detail {
|
||||
font-size: 11px;
|
||||
color: var(--gray-500);
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -360,4 +492,9 @@ const healthLabel = computed(() => {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -31,6 +31,23 @@ export const useChannelStore = defineStore('channel', () => {
|
||||
try {
|
||||
const res = await channelApi.getStatusList()
|
||||
channels.value = res.data?.channels || {}
|
||||
|
||||
const channelIds = Object.keys(channels.value)
|
||||
if (channelIds.length > 0) {
|
||||
const credResults = await Promise.allSettled(
|
||||
channelIds.map((id) =>
|
||||
channelApi.getCredentialStatus(id).then((r) => ({ id, data: r.data }))
|
||||
)
|
||||
)
|
||||
for (const result of credResults) {
|
||||
if (result.status === 'fulfilled' && result.value?.data) {
|
||||
channels.value[result.value.id] = {
|
||||
...channels.value[result.value.id],
|
||||
credential_status: result.value.data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
loadError.value = e.message
|
||||
} finally {
|
||||
@ -166,6 +183,35 @@ export const useChannelStore = defineStore('channel', () => {
|
||||
return res
|
||||
}
|
||||
|
||||
async function fetchCredentialStatus(channelId) {
|
||||
try {
|
||||
const res = await channelApi.getCredentialStatus(channelId)
|
||||
if (res.data) {
|
||||
channels.value[channelId] = {
|
||||
...channels.value[channelId],
|
||||
credential_status: res.data
|
||||
}
|
||||
}
|
||||
return res.data
|
||||
} catch (e) {
|
||||
console.warn('获取凭证状态失败:', e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshCredential(channelId) {
|
||||
try {
|
||||
const res = await channelApi.refreshCredential(channelId)
|
||||
if (res.data) {
|
||||
await fetchCredentialStatus(channelId)
|
||||
}
|
||||
return res
|
||||
} catch (e) {
|
||||
console.warn('刷新凭证失败:', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
function sendMessage(content, opts) {
|
||||
return wsClient.value?.send(content, opts)
|
||||
}
|
||||
@ -255,6 +301,8 @@ export const useChannelStore = defineStore('channel', () => {
|
||||
updatePolicy,
|
||||
registerChannel,
|
||||
unregisterChannel,
|
||||
fetchCredentialStatus,
|
||||
refreshCredential,
|
||||
sendMessage,
|
||||
initWS,
|
||||
disconnectWS,
|
||||
|
||||
@ -4,7 +4,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import {
|
||||
ArrowLeft, RefreshCw, Play, Square, RotateCcw, Plug, CheckSquare, X,
|
||||
Plus, MessageCircle, Settings
|
||||
Plus, MessageCircle, Settings, Shield
|
||||
} from 'lucide-vue-next'
|
||||
import { useChannelStore } from '@/stores/channel'
|
||||
import { useAgentStore } from '@/stores/agent'
|
||||
@ -41,6 +41,7 @@ const showTestDialog = ref(false)
|
||||
const editableConfig = ref({})
|
||||
const editablePolicy = ref({})
|
||||
const actionLoading = reactive({})
|
||||
const credRefreshLoading = ref(false)
|
||||
const policySaving = ref(false)
|
||||
|
||||
const refreshing = ref(false)
|
||||
@ -268,7 +269,8 @@ async function loadChannelDetail(channelId) {
|
||||
await Promise.all([
|
||||
channelStore.fetchChannelDetail(channelId),
|
||||
channelStore.fetchChannelPolicy(channelId),
|
||||
channelStore.fetchRouting(channelId)
|
||||
channelStore.fetchRouting(channelId),
|
||||
channelStore.fetchCredentialStatus(channelId)
|
||||
])
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
@ -466,6 +468,48 @@ function handleCardTest(channelId) {
|
||||
router.push({ name: 'ChannelDetailComp', params: { channelId }, query: { tab: 'test' } })
|
||||
}
|
||||
|
||||
function credentialSourceLabel(source) {
|
||||
const map = { db: '数据库', config: '配置文件', env: '环境变量', file: '文件', memory: '内存', none: '无' }
|
||||
return map[source] || source || '-'
|
||||
}
|
||||
|
||||
function credentialStatusClass(cs) {
|
||||
if (!cs || !cs.has_credential) return 'cred-none'
|
||||
if (cs.is_expired) return 'cred-expired'
|
||||
if (cs.expires_at && cs.source === 'db') {
|
||||
const remaining = new Date(cs.expires_at).getTime() - Date.now()
|
||||
if (remaining <= 3600000) return 'cred-soon'
|
||||
return 'cred-ok'
|
||||
}
|
||||
if (cs.source === 'memory') return 'cred-memory'
|
||||
return 'cred-ok'
|
||||
}
|
||||
|
||||
function credentialStatusLabel(cs) {
|
||||
const map = {
|
||||
'cred-ok': '凭证正常',
|
||||
'cred-soon': '即将过期',
|
||||
'cred-expired': '已过期',
|
||||
'cred-none': '无凭证',
|
||||
'cred-memory': '内存存储'
|
||||
}
|
||||
return map[credentialStatusClass(cs)] || '凭证正常'
|
||||
}
|
||||
|
||||
async function handleRefreshCredential(channelId) {
|
||||
if (credRefreshLoading.value) return
|
||||
credRefreshLoading.value = true
|
||||
try {
|
||||
await channelStore.refreshCredential(channelId)
|
||||
message.success('凭证刷新成功')
|
||||
await loadChannelDetail(channelId)
|
||||
} catch (e) {
|
||||
message.error(e.message || '刷新凭证失败')
|
||||
} finally {
|
||||
credRefreshLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!props.channelId) {
|
||||
await channelStore.fetchAllStatus()
|
||||
@ -700,6 +744,54 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="channelDetail.credential_status" class="credential-info-section">
|
||||
<h4 class="subsection-title">
|
||||
<Shield :size="14" />
|
||||
凭证状态
|
||||
<a-button
|
||||
size="small"
|
||||
type="link"
|
||||
:loading="credRefreshLoading"
|
||||
@click="handleRefreshCredential(detailChannelId)"
|
||||
>
|
||||
<RefreshCw :size="12" />
|
||||
刷新凭证
|
||||
</a-button>
|
||||
</h4>
|
||||
<div class="credential-info-grid">
|
||||
<div class="info-item">
|
||||
<span class="info-label">凭证来源</span>
|
||||
<span class="info-value">
|
||||
<span class="cred-source-badge" :class="`cred-source-${channelDetail.credential_status.source}`">
|
||||
{{ credentialSourceLabel(channelDetail.credential_status.source) }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">凭证类型</span>
|
||||
<span class="info-value">{{ channelDetail.credential_status.credential_type || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">状态</span>
|
||||
<span class="info-value">
|
||||
<span class="cred-status-badge" :class="credentialStatusClass(channelDetail.credential_status)">
|
||||
{{ credentialStatusLabel(channelDetail.credential_status) }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">最后更新</span>
|
||||
<span class="info-value">{{ channelDetail.credential_status.last_updated || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">过期时间</span>
|
||||
<span class="info-value">
|
||||
{{ channelDetail.credential_status.expires_at || '无过期时间' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="channel-actions">
|
||||
<a-button
|
||||
type="primary"
|
||||
@ -1471,4 +1563,59 @@ onUnmounted(() => {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.credential-info-section {
|
||||
margin-top: 16px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--gray-150);
|
||||
|
||||
.subsection-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.credential-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.cred-source-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
&.cred-source-db { background: var(--color-success-50); color: var(--color-success-700); }
|
||||
&.cred-source-config { background: var(--gray-100); color: var(--gray-600); }
|
||||
&.cred-source-env { background: var(--gray-100); color: var(--gray-600); }
|
||||
&.cred-source-file { background: var(--gray-100); color: var(--gray-600); }
|
||||
&.cred-source-memory { background: var(--color-warning-50); color: var(--color-warning-700); }
|
||||
&.cred-source-none { background: var(--gray-100); color: var(--gray-500); }
|
||||
}
|
||||
|
||||
.cred-status-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
&.cred-ok { background: var(--color-success-50); color: var(--color-success-700); }
|
||||
&.cred-soon { background: var(--color-warning-50); color: var(--color-warning-700); }
|
||||
&.cred-expired { background: var(--color-error-50); color: var(--color-error-700); }
|
||||
&.cred-none { background: var(--gray-100); color: var(--gray-500); }
|
||||
&.cred-memory { background: var(--color-warning-50); color: var(--color-warning-700); }
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue
Block a user