feat: 添加 CLAUDE.md 文件以提供项目开发指导

- 新增 CLAUDE.md 文件,包含项目概述、开发环境管理、项目架构、开发规范、特色功能模块及调试和故障排除指南。
- 详细描述了使用 Docker Compose 管理开发环境的步骤和常用命令。
- 介绍了项目的技术栈、核心目录结构及主要服务,确保开发者能够快速上手。
This commit is contained in:
Wenjie Zhang 2025-11-12 11:00:39 +08:00
parent 0e92aaa99a
commit 67ddbe6f40
20 changed files with 315 additions and 108 deletions

158
CLAUDE.md
View File

@ -1 +1,159 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概述
Yuxi-Know 是一个基于大模型的智能知识库与知识图谱智能体开发平台,融合了 RAG 技术与知识图谱技术,基于 LangGraph v1 + Vue.js + FastAPI + LightRAG 架构构建。项目完全通过 Docker Compose 进行管理,支持热重载开发。
## 开发环境管理
### 核心原则
- 所有开发调试都应在运行的 Docker 容器环境中进行
- 使用 `docker compose up -d` 启动完整开发环境
- api-dev 和 web-dev 服务均配置热重载,本地修改代码后无需重启容器
- 先检查 `docker ps` 确认服务状态,使用 `docker logs api-dev --tail 100` 查看日志
### 常用命令
#### 项目启动和停止
```bash
# 启动所有服务
make start
# 或
docker compose up -d
# 停止所有服务
make stop
# 或
docker compose down
# 查看日志
make logs
# 或
docker logs --tail=50 api-dev
```
#### 后端开发(在 Docker 容器内)
```bash
# 代码检查和格式化
make lint # 检查代码规范
make format # 格式化代码
make format_diff # 查看格式化差异
# 运行测试
make router-tests # 运行 API 路由测试
# 直接在容器内执行命令
docker compose exec api uv run python your_script.py
```
#### 前端开发
```bash
# 在 web 目录下执行
pnpm run dev # 开发模式(已通过 Docker 配置)
pnpm run build # 构建生产版本
pnpm run lint # ESLint 检查
pnpm run format # Prettier 格式化
```
#### 文档开发
```bash
# 在 docs 目录下执行
pnpm run docs:dev # 开发文档
pnpm run docs:build # 构建文档
```
## 项目架构
### 技术栈
- **后端**: FastAPI + Python 3.11+,使用 uv 管理依赖
- **前端**: Vue 3.5 + Vite 7 + Ant Design Vue + Pinia
- **AI框架**: LangChain v1 + LangGraph v1 + LightRAG
- **数据库**: Neo4j (图数据库) + Milvus (向量数据库) + MinIO (对象存储)
- **容器化**: Docker Compose 多服务编排
### 核心目录结构
```
├── server/ # FastAPI 后端服务
├── web/ # Vue.js 前端应用
├── src/ # 核心业务逻辑代码
├── docs/ # 文档中心 (VitePress)
├── test/ # 测试代码
├── docker/ # Docker 配置文件
├── scripts/ # 脚本工具
├── saves/ # 数据保存目录
├── models/ # 模型文件目录
└── docker-compose.yml
```
### 主要服务
- `api-dev`: FastAPI 后端服务 (端口 5050)
- `web-dev`: Vue.js 前端服务 (端口 5173)
- `graph`: Neo4j 图数据库 (端口 7474, 7687)
- `milvus`: Milvus 向量数据库 (端口 19530)
- `minio`: MinIO 对象存储 (端口 9000, 9001)
- `mineru`: MinerU 文档解析服务 (可选)
- `paddlex`: PaddleX OCR 服务 (可选)
## 开发规范
### 前端开发规范
- **API 接口**: 所有 API 接口定义在 `web/src/apis` 下,继承自 `apiGet/apiPost/apiRequest`
- **图标**: 从 `@ant-design/icons-vue``lucide-vue-next` 选取
- **样式**: 使用 Less优先采用 `web/src/assets/css/base.css` 中的颜色
- **UI风格**: 简洁一致,避免悬停位移、过度阴影和渐变色
- **组件库**: 基于 Ant Design Vue保持组件一致性
### 后端开发规范
- **包管理**: 使用 uv 管理依赖,调试时使用 `uv run`
- **代码规范**: 符合 Pythonic 风格,支持 Python 3.12+ 语法
- **代码质量**: 使用 `make lint` 检查,`make format` 格式化
- **API设计**: 遵循 RESTful 规范,使用 Pydantic 进行数据验证
### 通用开发规范
- **文档更新**: 代码更新后同步更新 `docs/latest` 中的相关文档
- **测试**: 测试脚本放在 `test/` 目录,从 Docker 容器中运行
- **环境变量**: 配置通过 `.env` 文件管理
- **数据安全**: 敏感数据不提交到版本控制
## 特色功能模块
### 智能体系统
- 基于 LangGraph v1 的智能体框架
- 支持多代理协作和工具调用
- 提供完整的智能体开发套件
### 知识管理
- 多模态文档解析 (PDF、Word、图片等)
- 知识图谱自动构建和可视化
- 向量化存储和检索
### 数据处理
- MinerU 文档解析集成
- PaddleX OCR 文字识别
- 支持多种模型提供商 (OpenAI、DeepSeek、阿里云等)
## 调试和故障排除
### 常见问题排查
1. **服务启动失败**: 检查端口占用和 Docker 服务状态
2. **API 连接问题**: 确认 `VITE_API_URL` 环境变量配置
3. **模型加载问题**: 检查 `models/` 目录和权限设置
4. **数据库连接**: 确认 Neo4j 和 Milvus 服务健康状态
### 日志查看
```bash
# 查看各服务日志
docker logs api-dev --tail 100
docker logs web-dev --tail 50
docker logs graph --tail 50
docker logs milvus --tail 50
```
### 性能监控
- 使用健康检查端点 `/api/system/health`
- 监控 GPU 使用情况 (如果使用 GPU 服务)
- 检查内存和磁盘使用情况
You MUST read `./AGENTS.md`

View File

@ -180,31 +180,41 @@ def _serialize_attachment(record: dict) -> dict:
}
async def save_partial_message(conv_mgr, thread_id, full_msg, error_type="interrupted"):
async def save_partial_message(conv_mgr, thread_id, full_msg=None, error_message=None, error_type="interrupted"):
"""
保存部分生成的AI消息到数据库
当streaming被中断时用于保存已生成的部分内容
"""
if not full_msg:
return None
统一保存AI消息到数据库的函数
Args:
conv_mgr: 对话管理器
thread_id: 线程ID
full_msg: 完整的AI消息对象可选
error_message: 纯错误消息文本当full_msg为空时使用
error_type: 错误类型标识
"""
try:
msg_dict = full_msg.model_dump() if hasattr(full_msg, "model_dump") else {}
content = full_msg.content if hasattr(full_msg, "content") else str(full_msg)
if full_msg:
# 保存部分生成的AI消息
msg_dict = full_msg.model_dump() if hasattr(full_msg, "model_dump") else {}
content = full_msg.content if hasattr(full_msg, "content") else str(full_msg)
extra_metadata = msg_dict | {"error_type": error_type}
else:
# 保存纯错误消息
content = error_message or f"发生错误: {error_type}"
extra_metadata = {"error_type": error_type, "is_error": True}
saved_msg = conv_mgr.add_message_by_thread_id(
thread_id=thread_id,
role="assistant",
content=content,
message_type="text",
extra_metadata=msg_dict | {"error_type": error_type},
extra_metadata=extra_metadata,
)
logger.info(f"Saved partial message due to {error_type}: {saved_msg.id}")
logger.info(f"Saved message due to {error_type}: {saved_msg.id}")
return saved_msg
except Exception as e:
logger.error(f"Error saving partial message: {e}")
logger.error(f"Error saving message: {e}")
logger.error(traceback.format_exc())
return None
@ -367,7 +377,7 @@ async def get_agent(current_user: User = Depends(get_required_user)):
"examples": agent_info.get("examples", []),
"configurable_items": agent_info.get("configurable_items", []),
"has_checkpointer": agent_info.get("has_checkpointer", False),
"capabilities": agent_info.get("capabilities", []) # 智能体能力列表
"capabilities": agent_info.get("capabilities", []), # 智能体能力列表
}
for agent_info in agents_info
]
@ -513,16 +523,19 @@ async def chat_agent(
# 客户端主动中断连接,检查中断并保存已生成的部分内容
logger.warning(f"Client disconnected, cancelling stream: {e}")
# 如果有手动维护的 full_msg直接保存到数据库
if full_msg:
# 创建新的 db session因为原 session 可能已关闭
new_db = db_manager.get_session()
try:
new_conv_manager = ConversationManager(new_db)
await save_partial_message(new_conv_manager, thread_id, full_msg, "interrupted")
finally:
new_db.close()
# 保存中断消息到数据库
new_db = db_manager.get_session()
try:
new_conv_manager = ConversationManager(new_db)
await save_partial_message(
new_conv_manager,
thread_id,
full_msg=full_msg,
error_message="对话已中断" if not full_msg else None,
error_type="interrupted",
)
finally:
new_db.close()
# 通知前端中断(可能发送不到,但用于一致性)
yield make_chunk(status="interrupted", message="对话已中断", meta=meta)
@ -530,16 +543,19 @@ async def chat_agent(
except Exception as e:
logger.error(f"Error streaming messages: {e}, {traceback.format_exc()}")
# 如果有手动维护的 full_msg直接保存到数据库
if full_msg:
# 创建新的 db session因为原 session 可能已关闭
new_db = db_manager.get_session()
try:
new_conv_manager = ConversationManager(new_db)
await save_partial_message(new_conv_manager, thread_id, full_msg, "unexpected_error")
finally:
new_db.close()
# 保存错误消息到数据库
new_db = db_manager.get_session()
try:
new_conv_manager = ConversationManager(new_db)
await save_partial_message(
new_conv_manager,
thread_id,
full_msg=full_msg,
error_message=f"Error streaming messages: {e}" if not full_msg else None,
error_type="unexpected_error",
)
finally:
new_db.close()
yield make_chunk(message=f"Error streaming messages: {e}", status="error")
@ -644,28 +660,61 @@ async def resume_agent_chat(
resume_command, context=context, config={"configurable": input_context}, stream_mode="messages"
)
async for msg, metadata in stream_source:
# 确保msg有正确的ID结构
msg_dict = msg.model_dump()
if "id" not in msg_dict:
msg_dict["id"] = str(uuid.uuid4())
try:
async for msg, metadata in stream_source:
# 确保msg有正确的ID结构
msg_dict = msg.model_dump()
if "id" not in msg_dict:
msg_dict["id"] = str(uuid.uuid4())
yield make_resume_chunk(
content=getattr(msg, "content", ""), msg=msg_dict, metadata=metadata, status="loading"
yield make_resume_chunk(
content=getattr(msg, "content", ""), msg=msg_dict, metadata=metadata, status="loading"
)
meta["time_cost"] = asyncio.get_event_loop().time() - start_time
yield make_resume_chunk(status="finished", meta=meta)
# 保存消息到数据库
langgraph_config = {"configurable": input_context}
conv_manager = ConversationManager(db)
await save_messages_from_langgraph_state(
agent_instance=agent,
thread_id=thread_id,
conv_mgr=conv_manager,
config_dict=langgraph_config,
)
meta["time_cost"] = asyncio.get_event_loop().time() - start_time
yield make_resume_chunk(status="finished", meta=meta)
except (asyncio.CancelledError, ConnectionError) as e:
# 客户端主动中断连接
logger.warning(f"Client disconnected during resume: {e}")
# 保存消息到数据库
langgraph_config = {"configurable": input_context}
conv_manager = ConversationManager(db)
await save_messages_from_langgraph_state(
agent_instance=agent,
thread_id=thread_id,
conv_mgr=conv_manager,
config_dict=langgraph_config,
)
# 保存中断消息到数据库
new_db = db_manager.get_session()
try:
new_conv_manager = ConversationManager(new_db)
await save_partial_message(
new_conv_manager, thread_id, error_message="对话恢复已中断", error_type="resume_interrupted"
)
finally:
new_db.close()
yield make_resume_chunk(status="interrupted", message="对话恢复已中断", meta=meta)
except Exception as e:
# 处理其他异常
logger.error(f"Error during resume: {e}, {traceback.format_exc()}")
# 保存错误消息到数据库
new_db = db_manager.get_session()
try:
new_conv_manager = ConversationManager(new_db)
await save_partial_message(
new_conv_manager, thread_id, error_message=f"Error during resume: {e}", error_type="resume_error"
)
finally:
new_db.close()
yield make_resume_chunk(message=f"Error during resume: {e}", status="error")
return StreamingResponse(stream_resume(), media_type="application/json")

View File

@ -247,12 +247,14 @@ async def add_documents(
error_type = "timeout" if isinstance(doc_error, TimeoutError) else "processing_error"
error_msg = "处理超时" if isinstance(doc_error, TimeoutError) else "处理失败"
processed_items.append({
"item": item,
"status": "failed",
"error": f"{error_msg}: {str(doc_error)}",
"error_type": error_type
})
processed_items.append(
{
"item": item,
"status": "failed",
"error": f"{error_msg}: {str(doc_error)}",
"error_type": error_type,
}
)
except asyncio.CancelledError:
await context.set_progress(100.0, "任务已取消")
@ -262,13 +264,15 @@ async def add_documents(
logger.exception(f"Task processing failed: {task_error}")
await context.set_progress(100.0, f"任务处理失败: {str(task_error)}")
# 将所有未处理的文档标记为失败
for item in items[len(processed_items):]:
processed_items.append({
"item": item,
"status": "failed",
"error": f"任务失败: {str(task_error)}",
"error_type": "task_failed"
})
for item in items[len(processed_items) :]:
processed_items.append(
{
"item": item,
"status": "failed",
"error": f"任务失败: {str(task_error)}",
"error_type": "task_failed",
}
)
raise
item_type = "URL" if content_type == "url" else "文件"
@ -600,8 +604,7 @@ async def get_knowledge_base_query_params(db_id: str, current_user: User = Depen
"type": "select",
"default": reranker_config.get("model", ""),
"options": [
{"label": info.name, "value": model_id}
for model_id, info in config.reranker_names.items()
{"label": info.name, "value": model_id} for model_id, info in config.reranker_names.items()
],
"description": "覆盖默认配置,选择用于本次查询的重排序模型",
}
@ -675,8 +678,7 @@ async def get_knowledge_base_query_params(db_id: str, current_user: User = Depen
"type": "select",
"default": reranker_config.get("model", ""),
"options": [
{"label": info.name, "value": model_id}
for model_id, info in config.reranker_names.items()
{"label": info.name, "value": model_id} for model_id, info in config.reranker_names.items()
],
"description": "覆盖默认配置,选择用于本次查询的重排序模型",
}

View File

@ -51,8 +51,6 @@ async def update_config_batch(items: dict = Body(...), current_user: User = Depe
return config.dump_config()
@system.get("/logs")
def get_system_logs(current_user: User = Depends(get_admin_user)):
"""获取系统日志"""

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import importlib.util
import os
import tomllib as tomli
from abc import abstractmethod
from pathlib import Path
@ -9,8 +10,6 @@ from langgraph.checkpoint.memory import InMemorySaver
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver, aiosqlite
from langgraph.graph.state import CompiledStateGraph
import tomllib as tomli
from src import config as sys_config
from src.agents.common.context import BaseContext
from src.utils import logger
@ -163,7 +162,6 @@ class BaseAgent:
"""获取异步存储实例"""
return AsyncSqliteSaver(await self.get_async_conn())
def load_metadata(self) -> dict:
"""Load metadata from metadata.toml file in the agent's source directory."""
if self._metadata_cache is not None:

View File

@ -1,6 +1,6 @@
from .attachment_middleware import inject_attachment_context
from .context_middlewares import context_aware_prompt, context_based_model
from .dynamic_tool_middleware import DynamicToolMiddleware
from .attachment_middleware import inject_attachment_context
__all__ = [
"DynamicToolMiddleware",

View File

@ -40,8 +40,7 @@ def _build_attachment_prompt(attachments: Sequence[dict]) -> str | None:
return None
instructions = (
"以下为用户提供的附件内容,请综合这些文件与用户的新问题进行回答。"
"如附件与问题无关,可忽略附件内容:\n\n"
"以下为用户提供的附件内容,请综合这些文件与用户的新问题进行回答。如附件与问题无关,可忽略附件内容:\n\n"
)
return instructions + "\n\n".join(chunks)

View File

@ -14,9 +14,7 @@ def context_aware_prompt(request: ModelRequest) -> str:
@wrap_model_call
async def context_based_model(
request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]
) -> ModelResponse:
async def context_based_model(request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]) -> ModelResponse:
"""从 runtime context 动态选择模型"""
model_spec = request.runtime.context.model
model = load_chat_model(model_spec)

View File

@ -11,6 +11,7 @@ calc_agent = create_agent(
system_prompt="你可以使用计算器工具,处理各种数学计算任务。",
)
@tool(name_or_callable="calc_agent_tool", description="进行计算任务,输入是数学表达式或描述,输出计算结果。")
async def calc_agent_tool(description: str) -> str:
"""CalcAgent 工具 - 使用子智能体 CalcAgent 进行计算任务"""

View File

@ -88,7 +88,7 @@ def mysql_list_tables() -> str:
all_table_names = "\n".join(table_names)
result = f"数据库中的表:\n{all_table_names}"
if db_note := conn_manager.config.get('description'):
if db_note := conn_manager.config.get("description"):
result = f"数据库说明: {db_note}\n\n" + result
logger.info(f"Retrieved {len(table_names)} tables from database")
return result

View File

@ -11,10 +11,10 @@ from pydantic import BaseModel, Field
from src import config, graph_base, knowledge_base
from src.utils import logger
search = TavilySearch(max_results=10)
search.metadata = {"name": "Tavily 网页搜索"}
@tool(name_or_callable="计算器", description="可以对给定的2个数字选择进行 add, subtract, multiply, divide 运算")
def calculator(a: float, b: float, operation: str) -> float:
try:
@ -38,7 +38,7 @@ def calculator(a: float, b: float, operation: str) -> float:
@tool(name_or_callable="人工审批工具(Debug)", description="请求人工审批工具,用于在执行重要操作前获得人类确认。")
def get_approved_user_goal(
operation_description: str,
)->dict:
) -> dict:
"""
请求人工审批在执行重要操作前获得人类确认
@ -78,6 +78,7 @@ KG_QUERY_DESCRIPTION = """
关键词query使用可能帮助回答这个问题的关键词进行查询不要直接使用用户的原始输入去查询
"""
@tool(name_or_callable="查询知识图谱", description=KG_QUERY_DESCRIPTION)
def query_knowledge_graph(query: Annotated[str, "The keyword to query knowledge graph."]) -> Any:
"""使用这个工具可以查询知识图谱中包含的三元组信息。关键词query使用可能帮助回答这个问题的关键词进行查询不要直接使用用户的原始输入去查询。"""

View File

@ -4,7 +4,6 @@ from dataclasses import field
from src.agents.common.context import BaseContext
DEEP_PROMPT = """你是一个能够处理复杂、多步骤任务的深度分析代理。
你拥有以下资源

View File

@ -1,17 +1,20 @@
"""Deep Agent - 基于create_deep_agent的深度分析智能体"""
import os
from typing import Literal
from deepagents import create_deep_agent
from tavily import TavilyClient
from src.agents.common import BaseAgent, load_chat_model
from src.agents.common.tools import search
from src.agents.common.middlewares import (
context_aware_prompt,
context_based_model,
inject_attachment_context,
)
from src.agents.common.tools import search
from .context import DeepContext
from tavily import TavilyClient
# 最佳实践是初始化客户端一次并复用它。
tavily_client = TavilyClient(api_key=os.environ["TAVILY_API_KEY"])
@ -169,8 +172,8 @@ research_instructions = """你是一位专家级研究员。你的工作是进
使用此工具对给定的查询进行网络搜索你可以指定结果数量主题以及是否包含原始内容
"""
class DeepAgent(BaseAgent):
class DeepAgent(BaseAgent):
name = "深度分析智能体"
description = "具备规划、深度分析和子智能体协作能力的智能体,可以处理复杂的多步骤任务"
context_schema = DeepContext

View File

@ -1,4 +1,3 @@
from langchain.agents import create_agent
from src import config

View File

@ -323,9 +323,7 @@ class ChromaKB(KnowledgeBase):
try:
rerank_start = time.time()
documents_text = [chunk["content"] for chunk in retrieved_chunks]
rerank_scores = await reranker.acompute_score(
[query_text, documents_text], normalize=True
)
rerank_scores = await reranker.acompute_score([query_text, documents_text], normalize=True)
for chunk, rerank_score in zip(retrieved_chunks, rerank_scores):
chunk["rerank_score"] = float(rerank_score)

View File

@ -369,9 +369,7 @@ class MilvusKB(KnowledgeBase):
try:
rerank_start = time.time()
documents_text = [chunk["content"] for chunk in retrieved_chunks]
rerank_scores = await reranker.acompute_score(
[query_text, documents_text], normalize=True
)
rerank_scores = await reranker.acompute_score([query_text, documents_text], normalize=True)
for chunk, rerank_score in zip(retrieved_chunks, rerank_scores):
chunk["rerank_score"] = float(rerank_score)

View File

@ -85,9 +85,7 @@ async def test_admin_can_create_vector_db_with_reranker(test_client, admin_heade
assert reranker_config.get("enabled") is True
assert reranker_config.get("model") == "siliconflow/BAAI/bge-reranker-v2-m3"
params_response = await test_client.get(
f"/api/knowledge/databases/{db_id}/query-params", headers=admin_headers
)
params_response = await test_client.get(f"/api/knowledge/databases/{db_id}/query-params", headers=admin_headers)
assert params_response.status_code == 200, params_response.text
params_payload = params_response.json()

View File

@ -13,9 +13,7 @@ async def test_reranker_list_requires_admin(test_client, standard_user):
public_response = await test_client.get("/api/settings/rerankers")
assert public_response.status_code == 401
forbidden_response = await test_client.get(
"/api/settings/rerankers", headers=standard_user["headers"]
)
forbidden_response = await test_client.get("/api/settings/rerankers", headers=standard_user["headers"])
assert forbidden_response.status_code == 403

View File

@ -436,6 +436,7 @@ const cleanupThreadState = (threadId) => {
// ==================== STREAM HANDLING LOGIC ====================
const resetOnGoingConv = (threadId = null, preserveMessages = false) => {
console.log('🔄 [RESET] Resetting on going conversation:', threadId, preserveMessages);
if (threadId) {
// 线
const threadState = getThreadState(threadId);
@ -449,8 +450,8 @@ const resetOnGoingConv = (threadId = null, preserveMessages = false) => {
//
setTimeout(() => {
if (threadState.onGoingConv) {
threadState.onGoingConv = createOnGoingConvState();
}
threadState.onGoingConv = createOnGoingConvState();
}
}, 100);
} else {
threadState.onGoingConv = createOnGoingConvState();
@ -469,8 +470,8 @@ const resetOnGoingConv = (threadId = null, preserveMessages = false) => {
if (preserveMessages) {
setTimeout(() => {
if (threadState.onGoingConv) {
threadState.onGoingConv = createOnGoingConvState();
}
threadState.onGoingConv = createOnGoingConvState();
}
}, 100);
} else {
threadState.onGoingConv = createOnGoingConvState();
@ -547,8 +548,8 @@ const _processStreamChunk = (chunk, threadId) => {
}
fetchThreadMessages({ agentId: currentAgentId.value, threadId: threadId })
.finally(() => {
resetOnGoingConv(threadId, true);
});
resetOnGoingConv(threadId, true);
});
return true;
}
@ -646,11 +647,17 @@ const updateThread = async (threadId, title) => {
};
// 线
const fetchThreadMessages = async ({ agentId, threadId }) => {
const fetchThreadMessages = async ({ agentId, threadId, delay = 0 }) => {
if (!threadId || !agentId) return;
//
if (delay > 0) {
await new Promise(resolve => setTimeout(resolve, delay));
}
try {
const response = await agentApi.getAgentHistory(agentId, threadId);
console.log('🔄 [FETCH] Thread messages:', response);
threadMessages.value[threadId] = response.history || [];
} catch (error) {
handleChatError(error, 'load');
@ -963,7 +970,7 @@ const handleSendOrStop = async () => {
//
try {
await fetchThreadMessages({ agentId: currentAgentId.value, threadId: threadId });
await fetchThreadMessages({ agentId: currentAgentId.value, threadId: threadId, delay: 100 });
message.info('已中断对话生成');
} catch (error) {
console.error('刷新消息历史失败:', error);

View File

@ -118,8 +118,11 @@
<div v-if="providerConfig.allModels.length === 0" class="modal-no-models">
<a-alert v-if="!modelStatus[providerConfig.provider]" type="warning" message="请在 .env 中配置对应的 APIKEY并重新启动服务" />
<div v-else>
<a-alert type="warning" message="该提供商暂未适配获取模型列表的方法,如需添加模型,请编辑 src/config/static/models.py 。" />
<img src="@/assets/pics/guides/how-to-add-models.png" alt="添加模型指引" style="width: 100%; height: 100%; margin-top: 16px;">
<a-alert type="warning" message="该提供商暂未适配获取模型列表的方法,如需添加模型,请编辑 src/config/static/models.py 。">
<template #description>
<a href="https://xerrors.github.io/Yuxi-Know/latest/intro/model-config.html" target="_blank">模型配置</a>
</template>
</a-alert>
</div>
</div>
</div>