merge: reconcile main with origin/main

This commit is contained in:
肖泽涛 2026-02-25 16:54:03 +08:00
commit ffb32ed242
20 changed files with 491 additions and 80 deletions

View File

@ -17,6 +17,7 @@
<img src="https://trendshift.io/api/badge/repositories/15845" alt="Yuxi-Know | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/>
</a>
[**文档中心**](https://xerrors.github.io/Yuxi-Know/) |
[**视频演示**](https://www.bilibili.com/video/BV1DF14BTETq/)
@ -107,9 +108,7 @@
- 更多智能体开发套件 中间件、子智能体,更简洁,更易上手。
</details>
<img width="1846" height="434" alt="image" src="https://github.com/user-attachments/assets/ccbd2cbf-574a-4cd3-baac-167b3c619b6e" />
<img width="4224" height="1006" alt="image" src="https://github.com/user-attachments/assets/66a85b70-5a40-4c5e-aeaa-18b3c85aa76f" />
## 快速开始

View File

@ -741,6 +741,11 @@ async def update_thread(
)
# ================================
# > === 附件管理分组 ===
# ================================
@chat.post("/thread/{thread_id}/attachments", response_model=AttachmentResponse)
async def upload_thread_attachment(
thread_id: str,

View File

@ -52,9 +52,9 @@ class TableListModel(BaseModel):
pass
@tool(name_or_callable="查询表名及说明", args_schema=TableListModel)
@tool(name_or_callable="mysql_list_tables", args_schema=TableListModel)
def mysql_list_tables() -> str:
"""获取数据库中的所有表名
"""【查询表名及说明】获取数据库中的所有表名
这个工具用来列出当前数据库中所有的表名帮助你了解数据库的结构
"""
@ -107,9 +107,9 @@ class TableDescribeModel(BaseModel):
table_name: str = Field(description="要查询的表名", example="users")
@tool(name_or_callable="描述表", args_schema=TableDescribeModel)
@tool(name_or_callable="mysql_describe_table", args_schema=TableDescribeModel)
def mysql_describe_table(table_name: Annotated[str, "要查询结构的表名"]) -> str:
"""获取指定表的详细结构信息
"""【描述表】获取指定表的详细结构信息
这个工具用来查看表的字段信息数据类型是否允许NULL默认值键类型等
帮助你了解表的结构以便编写正确的SQL查询
@ -203,12 +203,12 @@ class QueryModel(BaseModel):
timeout: int | None = Field(default=60, description="查询超时时间默认60秒最大600秒", ge=1, le=600)
@tool(name_or_callable="执行 SQL 查询", args_schema=QueryModel)
@tool(name_or_callable="mysql_query", args_schema=QueryModel)
def mysql_query(
sql: Annotated[str, "要执行的SQL查询语句只能是SELECT语句"],
timeout: Annotated[int | None, "查询超时时间默认60秒最大600秒"] = 60,
) -> str:
"""执行只读的SQL查询语句
"""【执行 SQL 查询】执行只读的SQL查询语句
这个工具用来执行SQL查询并返回结果支持复杂的SELECT查询包括JOINGROUP BY等
注意只能执行查询操作不能修改数据

View File

@ -1,16 +1,25 @@
from dataclasses import dataclass, field
from typing import Annotated
from deepagents.backends import StateBackend
from deepagents.middleware.filesystem import FilesystemMiddleware
from langchain.agents import create_agent
from src.agents.common import BaseAgent, BaseContext, load_chat_model
from src.agents.common.middlewares import (
RuntimeConfigMiddleware,
save_attachments_to_fs,
)
from src.agents.common.toolkits.mysql import get_mysql_tools
from src.services.mcp_service import get_mcp_server_names, get_tools_from_all_servers
from src.utils import logger
def _create_fs_backend(rt):
"""创建文件存储后端"""
return StateBackend(rt)
PROMPT = """你的任务是根据用户的指令,使用数据库工具和图表绘制工具,构建 SQL 查询报告。
你需要根据用户的指令生成相应的 SQL 查询并将查询结果以报表的形式返回给用户
在生成报表时你可以调用工具生成图表以更直观地展示数据
@ -52,6 +61,10 @@ class SqlReporterAgent(BaseAgent):
"MySQL 工具默认启用无法选择mcp 默认启用 Charts MCPs。"
)
context_schema = ReporterContext
capabilities = [
"file_upload",
"files",
]
def __init__(self, **kwargs):
super().__init__(**kwargs)
@ -66,7 +79,9 @@ class SqlReporterAgent(BaseAgent):
system_prompt=context.system_prompt,
tools=get_mysql_tools(), # MySQL 工具默认启用,这里添加的 tools不会在工具选择框中出现
middleware=[
FilesystemMiddleware(backend=_create_fs_backend), # 文件系统后端
RuntimeConfigMiddleware(extra_tools=all_mcp_tools),
save_attachments_to_fs, # 附件保存到文件系统
],
checkpointer=await self._get_checkpointer(),
)

View File

@ -49,9 +49,9 @@ DEFAULT_CHAT_MODEL_PROVIDERS: dict[str, ChatModelProvider] = {
name="OpenAI",
url="https://platform.openai.com/docs/models",
base_url="https://api.openai.com/v1",
default="gpt-4o-mini",
default="gpt-5-mini",
env="OPENAI_API_KEY",
models=["gpt-4", "gpt-4o", "gpt-4o-mini"],
models=["gpt-5.2", "gpt-5-mini", "gpt-5.2-pro"],
),
"deepseek": ChatModelProvider(
name="DeepSeek",
@ -65,22 +65,21 @@ DEFAULT_CHAT_MODEL_PROVIDERS: dict[str, ChatModelProvider] = {
name="智谱AI (Zhipu)",
url="https://open.bigmodel.cn/dev/api",
base_url="https://open.bigmodel.cn/api/paas/v4/",
default="glm-4.5-flash",
default="glm-4.7-flash",
env="ZHIPUAI_API_KEY",
models=["glm-4.6", "glm-4.5-air", "glm-4.5-flash"],
models=["glm-5", "glm-4.5-air", "glm-4.7-flash"],
),
"siliconflow": ChatModelProvider(
name="SiliconFlow",
url="https://cloud.siliconflow.cn/models",
base_url="https://api.siliconflow.cn/v1",
default="deepseek-ai/DeepSeek-V3.2",
default="Pro/deepseek-ai/DeepSeek-V3.2",
env="SILICONFLOW_API_KEY",
models=[
"deepseek-ai/DeepSeek-V3.2",
"Qwen/Qwen3-235B-A22B-Thinking-2507",
"Qwen/Qwen3-235B-A22B-Instruct-2507",
"moonshotai/Kimi-K2-Instruct-0905",
"zai-org/GLM-4.6",
"Pro/deepseek-ai/DeepSeek-V3.2",
"Pro/MiniMaxAI/MiniMax-M2.5",
"Pro/zai-org/GLM-5",
"Pro/moonshotai/Kimi-K2.5",
],
),
# "together": ChatModelProvider(
@ -101,33 +100,31 @@ DEFAULT_CHAT_MODEL_PROVIDERS: dict[str, ChatModelProvider] = {
"qwen-max-latest",
"qwen-plus-latest",
"qwen-turbo-latest",
"qwen3-235b-a22b-thinking-2507",
"qwen3-235b-a22b-instruct-2507",
],
),
"ark": ChatModelProvider(
name="豆包Ark",
url="https://console.volcengine.com/ark/region:ark+cn-beijing/model",
base_url="https://ark.cn-beijing.volces.com/api/v3",
default="doubao-seed-1-6-250615",
default="doubao-seed-2-0-lite-260215",
env="ARK_API_KEY",
models=[
"doubao-seed-1-6-250615",
"doubao-seed-1-6-thinking-250715",
"doubao-seed-1-6-flash-250715",
"doubao-seed-2-0-pro-260215",
"doubao-seed-2-0-lite-260215",
"doubao-seed-2-0-mini-260215",
],
),
"openrouter": ChatModelProvider(
name="OpenRouter",
url="https://openrouter.ai/models",
base_url="https://openrouter.ai/api/v1",
default="openai/gpt-4o",
default="x-ai/grok-4.1-fast",
env="OPENROUTER_API_KEY",
models=[
"openai/gpt-4o",
"anthropic/claude-opus-4.6",
"anthropic/claude-sonnet-4.5",
"x-ai/grok-4.1-fast",
"x-ai/grok-4",
"google/gemini-2.5-pro",
"anthropic/claude-sonnet-4",
],
),
# "moonshot": ChatModelProvider(
@ -148,7 +145,7 @@ DEFAULT_CHAT_MODEL_PROVIDERS: dict[str, ChatModelProvider] = {
base_url="https://api-inference.modelscope.cn/v1/",
default="deepseek-ai/DeepSeek-V3.2",
env="MODELSCOPE_ACCESS_TOKEN",
models=["Qwen/Qwen3-32B", "deepseek-ai/DeepSeek-V3.2"],
models=["ZhipuAI/GLM-5", "ZhipuAI/GLM-4.7-Flash", "MiniMax/MiniMax-M2.5", "moonshotai/Kimi-K2.5", ""],
),
}

View File

@ -225,7 +225,7 @@ class KnowledgeBase(ABC):
# Save to metadata
self.files_meta[file_id] = metadata
await self._save_metadata()
await self._persist_file(file_id)
return metadata
@ -273,7 +273,7 @@ class KnowledgeBase(ABC):
self.files_meta[file_id]["updated_at"] = utc_isoformat()
if operator_id:
self.files_meta[file_id]["updated_by"] = operator_id
await self._save_metadata()
await self._persist_file(file_id)
# Add to processing queue
self._add_to_processing_queue(file_id)
@ -310,7 +310,7 @@ class KnowledgeBase(ABC):
self.files_meta[file_id]["updated_at"] = utc_isoformat()
if operator_id:
self.files_meta[file_id]["updated_by"] = operator_id
await self._save_metadata()
await self._persist_file(file_id)
return self.files_meta[file_id]
@ -323,7 +323,7 @@ class KnowledgeBase(ABC):
self.files_meta[file_id]["updated_at"] = utc_isoformat()
if operator_id:
self.files_meta[file_id]["updated_by"] = operator_id
await self._save_metadata()
await self._persist_file(file_id)
raise
@ -358,7 +358,7 @@ class KnowledgeBase(ABC):
logger.debug(f"[update_file_params] file_id={file_id}, updated_params={current_params}")
await self._save_metadata()
await self._persist_file(file_id)
async def _save_markdown_to_minio(self, db_id: str, file_id: str, content: str) -> str:
"""Save markdown content to MinIO and return HTTP URL"""
@ -453,7 +453,7 @@ class KnowledgeBase(ABC):
"metadata": kwargs,
"created_at": utc_isoformat(),
}
await self._save_metadata()
await self._persist_kb(db_id)
# 创建工作目录
working_dir = os.path.join(self.work_dir, db_id)
@ -518,7 +518,7 @@ class KnowledgeBase(ABC):
"path": folder_name,
"file_type": "folder",
}
await self._save_metadata()
await self._persist_file(folder_id)
return self.files_meta[folder_id]
@abstractmethod
@ -860,7 +860,7 @@ class KnowledgeBase(ABC):
current = parent_meta.get("parent_id")
meta["parent_id"] = new_parent_id
await self._save_metadata()
await self._persist_file(file_id)
return meta
@abstractmethod
@ -1146,3 +1146,78 @@ class KnowledgeBase(ABC):
}
if existing is None:
await eval_repo.create_benchmark(payload)
async def _persist_file(self, file_id: str) -> None:
"""只保存单个文件到数据库,避免全量遍历"""
from src.repositories.knowledge_file_repository import KnowledgeFileRepository
file_repo = KnowledgeFileRepository()
if file_id not in self.files_meta:
return
meta = self.files_meta[file_id]
db_id = meta.get("database_id")
if not db_id:
return
await file_repo.upsert(
file_id=file_id,
data={
"db_id": db_id,
"parent_id": meta.get("parent_id"),
"filename": meta.get("filename") or "",
"original_filename": meta.get("original_filename"),
"file_type": meta.get("file_type"),
"path": meta.get("path"),
"minio_url": meta.get("minio_url"),
"markdown_file": meta.get("markdown_file"),
"status": meta.get("status"),
"content_hash": meta.get("content_hash"),
"file_size": meta.get("size"),
"content_type": meta.get("content_type"),
"processing_params": meta.get("processing_params"),
"is_folder": meta.get("is_folder", False),
"error_message": meta.get("error"),
"created_by": str(meta.get("created_by")) if meta.get("created_by") else None,
"updated_by": str(meta.get("updated_by")) if meta.get("updated_by") else None,
},
)
async def _persist_kb(self, db_id: str) -> None:
"""只保存单个知识库到数据库,避免全量遍历"""
from src.repositories.knowledge_base_repository import KnowledgeBaseRepository
kb_repo = KnowledgeBaseRepository()
if db_id not in self.databases_meta:
return
meta = self.databases_meta[db_id]
existing = await kb_repo.get_by_id(db_id)
payload = {
"db_id": db_id,
"name": meta.get("name") or db_id,
"description": meta.get("description"),
"kb_type": meta.get("kb_type") or self.kb_type,
"embed_info": meta.get("embed_info"),
"llm_info": meta.get("llm_info"),
"query_params": meta.get("query_params"),
"additional_params": meta.get("metadata") or {},
}
if existing is None:
await kb_repo.create(payload)
else:
await kb_repo.update(
db_id,
{
"name": payload["name"],
"description": payload["description"],
"kb_type": payload["kb_type"],
"embed_info": payload["embed_info"],
"llm_info": payload["llm_info"],
"query_params": payload["query_params"],
"additional_params": payload["additional_params"],
},
)

View File

@ -313,7 +313,7 @@ class LightRagKB(KnowledgeBase):
self.files_meta[file_id]["updated_at"] = utc_isoformat()
if operator_id:
self.files_meta[file_id]["updated_by"] = operator_id
await self._save_metadata()
await self._persist_file(file_id)
# Add to processing queue
self._add_to_processing_queue(file_id)
@ -358,7 +358,7 @@ class LightRagKB(KnowledgeBase):
self.files_meta[file_id]["updated_at"] = utc_isoformat()
if operator_id:
self.files_meta[file_id]["updated_by"] = operator_id
await self._save_metadata()
await self._persist_file(file_id)
return self.files_meta[file_id]
@ -369,7 +369,7 @@ class LightRagKB(KnowledgeBase):
self.files_meta[file_id]["updated_at"] = utc_isoformat()
if operator_id:
self.files_meta[file_id]["updated_by"] = operator_id
await self._save_metadata()
await self._persist_file(file_id)
raise
finally:
@ -416,7 +416,7 @@ class LightRagKB(KnowledgeBase):
)
self.files_meta[file_id]["processing_params"] = resolved_params
self.files_meta[file_id]["status"] = "processing"
await self._save_metadata()
await self._persist_file(file_id)
# 重新解析文件为 markdown
if content_type != "file":
@ -447,7 +447,7 @@ class LightRagKB(KnowledgeBase):
# 更新元数据状态
self.files_meta[file_id]["status"] = "done"
await self._save_metadata()
await self._persist_file(file_id)
# 从处理队列中移除
self._remove_from_processing_queue(file_id)
@ -463,7 +463,7 @@ class LightRagKB(KnowledgeBase):
logger.error(f"更新{content_type} {file_path} 失败: {error_msg}, {traceback.format_exc()}")
self.files_meta[file_id]["status"] = "failed"
self.files_meta[file_id]["error"] = error_msg
await self._save_metadata()
await self._persist_file(file_id)
# 从处理队列中移除
self._remove_from_processing_queue(file_id)
@ -576,7 +576,6 @@ class LightRagKB(KnowledgeBase):
from src.repositories.knowledge_file_repository import KnowledgeFileRepository
await KnowledgeFileRepository().delete(file_id)
await self._save_metadata()
async def get_file_basic_info(self, db_id: str, file_id: str) -> dict:
"""获取文件基本信息(仅元数据)"""

View File

@ -338,7 +338,7 @@ class MilvusKB(KnowledgeBase):
self.files_meta[file_id]["updated_at"] = utc_isoformat()
if operator_id:
self.files_meta[file_id]["updated_by"] = operator_id
await self._save_metadata()
await self._persist_file(file_id)
return self.files_meta[file_id]
except Exception as e:
@ -349,7 +349,7 @@ class MilvusKB(KnowledgeBase):
self.files_meta[file_id]["updated_at"] = utc_isoformat()
if operator_id:
self.files_meta[file_id]["updated_by"] = operator_id
await self._save_metadata()
await self._persist_file(file_id)
raise
finally:
@ -402,7 +402,7 @@ class MilvusKB(KnowledgeBase):
)
self.files_meta[file_id]["processing_params"] = resolved_params
self.files_meta[file_id]["status"] = "processing"
await self._save_metadata()
await self._persist_file(file_id)
# 重新解析文件为 markdown
if content_type != "file":
@ -440,7 +440,7 @@ class MilvusKB(KnowledgeBase):
# 更新元数据状态
async with self._metadata_lock:
self.files_meta[file_id]["status"] = "done"
await self._save_metadata()
await self._persist_file(file_id)
# 从处理队列中移除
self._remove_from_processing_queue(file_id)
@ -455,7 +455,7 @@ class MilvusKB(KnowledgeBase):
logger.error(f"更新{content_type} {file_path} 失败: {e}, {traceback.format_exc()}")
async with self._metadata_lock:
self.files_meta[file_id]["status"] = "failed"
await self._save_metadata()
await self._persist_file(file_id)
# 从处理队列中移除
self._remove_from_processing_queue(file_id)
@ -712,7 +712,6 @@ class MilvusKB(KnowledgeBase):
from src.repositories.knowledge_file_repository import KnowledgeFileRepository
await KnowledgeFileRepository().delete(file_id)
await self._save_metadata()
async def get_file_basic_info(self, db_id: str, file_id: str) -> dict:
"""获取文件基本信息(仅元数据)"""

View File

@ -452,8 +452,7 @@ async def stream_agent_chat(
if agent_state:
yield make_chunk(status="agent_state", agent_state=agent_state, meta=meta)
yield make_chunk(status="finished", meta=meta)
# 先存储数据库,再返回 finished避免前端查询时数据未落库
await save_messages_from_langgraph_state(
agent_instance=agent,
thread_id=thread_id,
@ -461,6 +460,8 @@ async def stream_agent_chat(
config_dict=langgraph_config,
)
yield make_chunk(status="finished", meta=meta)
except (asyncio.CancelledError, ConnectionError) as e:
logger.warning(f"Client disconnected, cancelling stream: {e}")
@ -593,8 +594,8 @@ async def stream_agent_resume(
yield chunk
meta["time_cost"] = asyncio.get_event_loop().time() - start_time
yield make_resume_chunk(status="finished", meta=meta)
# 先存储数据库,再返回 finished避免前端查询时数据未落库
conv_repo = ConversationRepository(db)
await save_messages_from_langgraph_state(
agent_instance=agent,
@ -603,6 +604,8 @@ async def stream_agent_resume(
config_dict=langgraph_config,
)
yield make_resume_chunk(status="finished", meta=meta)
except (asyncio.CancelledError, ConnectionError) as e:
logger.warning(f"Client disconnected during resume: {e}")

View File

@ -2,6 +2,7 @@ import uuid
from datetime import UTC, datetime
from fastapi import HTTPException, UploadFile
from langgraph.types import Command
from sqlalchemy.ext.asyncio import AsyncSession
from src.agents import agent_manager
@ -78,6 +79,8 @@ async def _sync_thread_attachment_state(
graph = await agent.get_graph()
config = {"configurable": {"thread_id": thread_id, "user_id": str(user_id)}}
# 先获取现有 state保留非附件文件
state = await graph.aget_state(config)
state_values = getattr(state, "values", {}) if state else {}
existing_files = state_values.get("files", {}) if isinstance(state_values, dict) else {}
@ -97,6 +100,7 @@ async def _sync_thread_attachment_state(
for removed_path in prev_attachment_paths - next_attachment_paths:
file_updates[removed_path] = None
# 使用 Command 确保 reducer 被正确应用
await graph.aupdate_state(
config=config,
values={
@ -265,7 +269,7 @@ async def upload_thread_attachment_view(
"uploaded_at": utc_isoformat(),
"truncated": conversion.truncated,
"file_path": file_path, # 用于 StateBackend前端不返回此字段
"minio_url": minio_url,
"minio_url": minio_url, # 暂未使用
}
await conv_repo.add_attachment(conversation.id, attachment_record)
all_attachments = await conv_repo.get_attachments(conversation.id)

View File

@ -1431,7 +1431,7 @@ const handleSendMessage = async ({ image } = {}) => {
} finally {
threadState.streamAbortController = null
//
fetchThreadMessages({ agentId: currentAgentId.value, threadId: threadId, delay: 500 }).finally(
fetchThreadMessages({ agentId: currentAgentId.value, threadId: threadId }).finally(
() => {
//
resetOnGoingConv(threadId)
@ -1515,7 +1515,7 @@ const handleApprovalWithStream = async (approved) => {
}
//
fetchThreadMessages({ agentId: currentAgentId.value, threadId: threadId, delay: 500 }).finally(
fetchThreadMessages({ agentId: currentAgentId.value, threadId: threadId }).finally(
() => {
resetOnGoingConv(threadId)
scrollController.scrollToBottom()
@ -1815,7 +1815,7 @@ watch(
background: var(--gray-0);
border-radius: 12px;
box-shadow: 0 4px 20px var(--shadow-1);
border: 1px solid var(--gray-200);
border: 1px solid var(--gray-150);
min-width: 0;
will-change: flex-basis;
}

View File

@ -1,6 +1,7 @@
<template>
<MessageInputComponent
ref="inputRef"
:key="inputKey"
:model-value="modelValue"
@update:modelValue="updateValue"
:is-loading="isLoading"
@ -48,7 +49,7 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, watch } from 'vue'
import { message } from 'ant-design-vue'
import MessageInputComponent from '@/components/MessageInputComponent.vue'
import ImagePreviewComponent from '@/components/ImagePreviewComponent.vue'
@ -84,6 +85,20 @@ const emit = defineEmits([
const inputRef = ref(null)
const currentImage = ref(null)
// key
const inputKey = ref(0)
// hasStateContent state state
watch(
() => props.hasStateContent,
(newVal, oldVal) => {
// hasStateContent true false
if (oldVal === true && newVal === false) {
inputKey.value++
}
}
)
const updateValue = (val) => {
emit('update:modelValue', val)
}
@ -110,12 +125,14 @@ const handleAttachmentUpload = async (files) => {
}
try {
const hide = message.loading({ content: '正在上传附件...', key: 'upload-attachment', duration: 0 })
for (const file of files) {
await threadApi.uploadThreadAttachment(threadId, file)
message.success(`${file.name} 上传成功`)
}
message.success({ content: '附件上传成功', key: 'upload-attachment', duration: 2 })
emit('attachment-changed', threadId)
} catch (error) {
message.destroy('upload-attachment')
handleChatError(error, 'upload')
}
}

View File

@ -565,7 +565,7 @@ const stopResize = () => {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
padding: 4px 16px;
height: 40px;
background: var(--gray-25);
flex-shrink: 0;
@ -623,7 +623,7 @@ const stopResize = () => {
background: var(--gray-25);
position: relative;
align-items: center;
padding: 8px 6px;
padding: 8px 10px;
padding-top: 0px;
gap: 4px;
flex-shrink: 0;
@ -655,7 +655,7 @@ const stopResize = () => {
.tab-content {
flex: 1;
overflow-y: auto;
padding: 16px;
padding: 8px;
min-height: 0; /* Important for flex child scroll */
/* 自定义滚动条 */
@ -817,10 +817,8 @@ const stopResize = () => {
min-height: 300px;
max-height: 60vh;
overflow-y: auto;
background: var(--main-5);
border-radius: 6px;
padding: 16px;
border: 1px solid var(--gray-200);
&::-webkit-scrollbar {
width: 8px;
@ -1020,7 +1018,6 @@ const stopResize = () => {
/* Specific Ant Design Tree Overrides */
.file-tree-container :deep(.ant-tree) {
background: var(--gray-25);
font-family: inherit;
font-size: 14px;
overflow: hidden;

View File

@ -25,7 +25,7 @@
</span>
<!-- 视图模式切换 -->
<div class="view-controls" v-if="file && hasContent">
<div class="view-controls" v-if="file && hasChunks">
<a-segmented v-model:value="viewMode" :options="viewModeOptions" />
</div>
@ -141,21 +141,22 @@ const hasIndexed = computed(() => ['done', 'indexed'].includes(file.value?.statu
const hasContent = computed(
() => (file.value?.lines && file.value?.lines.length > 0) || file.value?.content
)
//
const hasChunks = computed(() => mappedChunks.value && mappedChunks.value.length > 0)
const viewModeOptions = computed(() => {
const options = [{ label: 'Markdown', value: 'markdown' }]
if (hasIndexed.value) {
// Chunks
if (hasChunks.value) {
options.push({ label: 'Chunks', value: 'chunks' })
}
return options
})
//
// chunks markdown
watch(file, (newFile) => {
if (newFile) {
if (!hasIndexed.value) {
viewMode.value = 'markdown'
}
if (newFile && !hasChunks.value) {
viewMode.value = 'markdown'
}
})

View File

@ -38,6 +38,15 @@
<!-- 编辑文件 -->
<EditFileTool v-else-if="isEditFileResult" :tool-call="toolCall" />
<!-- MySQL 查询 -->
<MysqlQueryTool v-else-if="isMysqlQueryResult" :tool-call="toolCall" />
<!-- MySQL 描述表 -->
<MysqlDescribeTableTool v-else-if="isMysqlDescribeTableResult" :tool-call="toolCall" />
<!-- MySQL 列出表 -->
<MysqlListTablesTool v-else-if="isMysqlListTablesResult" :tool-call="toolCall" />
<!-- 默认展示 -->
<BaseToolCall v-else :tool-call="toolCall" />
</template>
@ -61,6 +70,9 @@ import ListDirectoryTool from './tools/ListDirectoryTool.vue'
import SearchFileContentTool from './tools/SearchFileContentTool.vue'
import GlobTool from './tools/GlobTool.vue'
import EditFileTool from './tools/EditFileTool.vue'
import MysqlQueryTool from './tools/MysqlQueryTool.vue'
import MysqlDescribeTableTool from './tools/MysqlDescribeTableTool.vue'
import MysqlListTablesTool from './tools/MysqlListTablesTool.vue'
const props = defineProps({
toolCall: {
@ -170,6 +182,18 @@ const isEditFileResult = computed(() => {
return toolName.value === 'edit_file' || toolName.value === 'replace'
})
const isMysqlQueryResult = computed(() => {
return toolName.value === 'mysql_query'
})
const isMysqlDescribeTableResult = computed(() => {
return toolName.value === 'mysql_describe_table'
})
const isMysqlListTablesResult = computed(() => {
return toolName.value === 'mysql_list_tables'
})
//
const graphToolCallRef = ref(null)
const refreshGraph = () => {

View File

@ -15,3 +15,6 @@ export { default as ListDirectoryTool } from './tools/ListDirectoryTool.vue'
export { default as SearchFileContentTool } from './tools/SearchFileContentTool.vue'
export { default as GlobTool } from './tools/GlobTool.vue'
export { default as EditFileTool } from './tools/EditFileTool.vue'
export { default as MysqlQueryTool } from './tools/MysqlQueryTool.vue'
export { default as MysqlDescribeTableTool } from './tools/MysqlDescribeTableTool.vue'
export { default as MysqlListTablesTool } from './tools/MysqlListTablesTool.vue'

View File

@ -0,0 +1,85 @@
<template>
<BaseToolCall :tool-call="toolCall" :default-expanded="true" :hide-params="true">
<template #header-success>
<span class="sep-header">
<span class="keywords">描述表结构</span>
<span class="description code">{{
extractTableName(toolCall.args || toolCall.function?.arguments)
}}</span>
</span>
</template>
<template #result="{ resultContent }">
<div class="mysql-result">
<pre class="result-text">{{ formatResult(resultContent) }}</pre>
</div>
</template>
</BaseToolCall>
</template>
<script setup>
import BaseToolCall from '../BaseToolCall.vue'
const props = defineProps({
toolCall: {
type: Object,
required: true
}
})
const formatResult = (content) => {
if (!content) return ''
if (typeof content === 'string') {
try {
const parsed = JSON.parse(content)
return JSON.stringify(parsed, null, 2)
} catch {
return content
}
}
if (typeof content === 'object') {
return JSON.stringify(content, null, 2)
}
return String(content)
}
const extractTableName = (args) => {
if (!args) return ''
let parsedArgs = args
if (typeof args === 'string') {
try {
parsedArgs = JSON.parse(args)
} catch {
return args
}
}
return parsedArgs?.table_name || ''
}
</script>
<style lang="less" scoped>
.mysql-result {
border-radius: 8px;
padding: 12px;
.result-text {
margin: 0;
font-size: 12px;
line-height: 1.4;
color: var(--gray-700);
white-space: pre-wrap;
word-break: break-word;
max-height: 400px;
overflow-y: auto;
background: var(--gray-50);
padding: 10px;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
}
}
</style>

View File

@ -0,0 +1,67 @@
<template>
<BaseToolCall :tool-call="toolCall" :default-expanded="true" :hide-params="true">
<template #header-success>
<span class="sep-header">
<span class="keywords">列出数据库表</span>
</span>
</template>
<template #result="{ resultContent }">
<div class="mysql-result">
<pre class="result-text">{{ formatResult(resultContent) }}</pre>
</div>
</template>
</BaseToolCall>
</template>
<script setup>
import BaseToolCall from '../BaseToolCall.vue'
const props = defineProps({
toolCall: {
type: Object,
required: true
}
})
const formatResult = (content) => {
if (!content) return ''
if (typeof content === 'string') {
try {
const parsed = JSON.parse(content)
return JSON.stringify(parsed, null, 2)
} catch {
return content
}
}
if (typeof content === 'object') {
return JSON.stringify(content, null, 2)
}
return String(content)
}
</script>
<style lang="less" scoped>
.mysql-result {
border-radius: 8px;
padding: 12px;
.result-text {
margin: 0;
font-size: 12px;
line-height: 1.4;
color: var(--gray-700);
white-space: pre-wrap;
word-break: break-word;
max-height: 400px;
overflow-y: auto;
background: var(--gray-50);
padding: 10px;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
}
}
</style>

View File

@ -0,0 +1,121 @@
<template>
<BaseToolCall :tool-call="toolCall" :default-expanded="true">
<template #header-success>
<span class="sep-header">
<span class="keywords">执行SQL查询</span>
<span class="description">{{
truncateSql(extractSql(toolCall.args || toolCall.function?.arguments))
}}</span>
</span>
</template>
<template #params="{ args }">
<div class="mysql-params">
<pre class="sql-text">{{ extractSql(args) }}</pre>
</div>
</template>
<template #result="{ resultContent }">
<div class="mysql-result">
<pre class="result-text">{{ formatResult(resultContent) }}</pre>
</div>
</template>
</BaseToolCall>
</template>
<script setup>
import BaseToolCall from '../BaseToolCall.vue'
const props = defineProps({
toolCall: {
type: Object,
required: true
}
})
const formatResult = (content) => {
if (!content) return ''
// JSON
if (typeof content === 'string') {
try {
const parsed = JSON.parse(content)
return JSON.stringify(parsed, null, 2)
} catch {
return content
}
}
//
if (typeof content === 'object') {
return JSON.stringify(content, null, 2)
}
return String(content)
}
const extractSql = (args) => {
if (!args) return ''
// args
let parsedArgs = args
if (typeof args === 'string') {
try {
parsedArgs = JSON.parse(args)
} catch {
return args
}
}
// sql
const sql = parsedArgs?.sql || parsedArgs?.query
return sql || JSON.stringify(parsedArgs, null, 2)
}
const truncateSql = (sql, maxLength = 50) => {
if (!sql) return ''
//
const singleLine = sql.replace(/\s+/g, ' ').trim()
if (singleLine.length <= maxLength) return singleLine
return singleLine.slice(0, maxLength) + '...'
}
</script>
<style lang="less" scoped>
.mysql-params {
.sql-text {
margin: 0;
font-size: 11px;
line-height: 1.4;
color: var(--gray-800);
white-space: pre-wrap;
word-break: break-word;
padding: 6px;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
max-height: 200px;
overflow-y: auto;
}
}
.mysql-result {
// background: var(--gray-0);
border-radius: 8px;
padding: 12px;
.result-text {
margin: 0;
font-size: 12px;
line-height: 1.4;
color: var(--gray-700);
white-space: pre-wrap;
word-break: break-word;
max-height: 400px;
overflow-y: auto;
background: var(--gray-50);
padding: 10px;
border-radius: 4px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
}
}
</style>

View File

@ -596,20 +596,21 @@ const handleMouseUp = () => {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
background: var(--color-warning-50);
border-radius: 4px;
padding: 6px 12px;
background: var(--color-info-50);
border-left: 3px solid var(--color-info-500);
border-radius: 2px;
font-size: 13px;
color: var(--color-warning-700);
color: var(--color-info-700);
cursor: pointer;
transition: all 0.2s;
&:hover {
background: var(--color-warning-100);
background: var(--color-info-100);
}
svg {
color: var(--color-warning-700);
color: var(--color-info-500);
}
}
}
@ -660,7 +661,6 @@ const handleMouseUp = () => {
:deep(.ant-tabs-nav) {
margin-bottom: 0;
// background-color: var(--gray-0);
border-bottom: 1px solid var(--gray-200);
}
:deep(.ant-tabs-extra-content) {