This commit is contained in:
Wenjie Zhang 2025-12-26 18:47:25 +08:00
commit 8e7cf7c7a5
18 changed files with 247 additions and 141 deletions

View File

@ -1,6 +1,8 @@
# 语析 - 基于大模型的知识库与知识图谱智能体开发平台
# 📚 语析 - 基于大模型的知识库与知识图谱智能体开发平台
<div>
<div align="center">
<a href="https://trendshift.io/repositories/15845" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15845" alt="Yuxi-Know | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[![Stable](https://img.shields.io/badge/stable-v0.3.0-blue.svg)](https://github.com/xerrors/Yuxi-Know/tree/v0.3.0)
[![](https://img.shields.io/badge/Docker-2496ED?style=flat&logo=docker&logoColor=ffffff)](https://github.com/xerrors/Yuxi-Know/blob/main/docker-compose.yml)
@ -9,7 +11,11 @@
[![DeepWiki](https://img.shields.io/badge/DeepWiki-blue.svg)](https://deepwiki.com/xerrors/Yuxi-Know)
[![zread](https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff)](https://zread.ai/xerrors/Yuxi-Know)
[![demo](https://img.shields.io/badge/demo-00A1D6.svg?style=flat&logo=bilibili&logoColor=white)](https://www.bilibili.com/video/BV1DF14BTETq/)
![](https://img.shields.io/github/stars/xerrors/Yuxi-Know)
<br/>
📄 [**文档中心**](https://xerrors.github.io/Yuxi-Know/) |
📽️ [**视频演示**](https://www.bilibili.com/video/BV1DF14BTETq/)
</div>
@ -20,11 +26,6 @@
<img width="1632" height="392" alt="image" src="https://github.com/user-attachments/assets/ec381fde-53dd-4845-a79f-116b823fe989" />
> [!tip]
> 详细文档请查看全新的 📄[**文档中心**](https://xerrors.github.io/Yuxi-Know/)
> 哔哩哔哩观看 📽️[**视频演示**](https://www.bilibili.com/video/BV1DF14BTETq/)
---
**🎉 最新动态**

View File

@ -53,4 +53,8 @@ YUXI_BRAND_FILE_PATH=src/config/static/info.local.yaml
```
**此外**`web/src/stores/theme.js` 中也包含了主题相关的配置(需要修改 `colorPrimary`),可根据需要修改。
**此外**`web/src/stores/theme.js` 中也包含了主题相关的配置(需要修改 `colorPrimary`),可根据需要修改。
## 修改首页
首页提供了一个插槽组件 `web/src/components/ProjectOverview.vue`,可以在该组件中自定义项目介绍,当前为空文件。(借助 AI 编程可以设计出更好看的首页的)

View File

@ -362,38 +362,4 @@ async def get_database_mindmap(db_id: str, current_user: User = Depends(get_admi
raise HTTPException(status_code=500, detail=f"获取思维导图失败: {str(e)}")
@mindmap.post("/database/{db_id}")
async def save_database_mindmap(
db_id: str,
mindmap: dict = Body(..., description="思维导图数据"),
current_user: User = Depends(get_admin_user),
):
"""
保存思维导图到知识库
Args:
db_id: 知识库ID
mindmap: 思维导图数据
Returns:
保存结果
"""
try:
# 检查知识库是否存在
db_info = knowledge_base.get_database_info(db_id)
if not db_info:
raise HTTPException(status_code=404, detail=f"知识库 {db_id} 不存在")
# TODO: 将思维导图保存到知识库元数据中
# 这里需要实现一个方法来更新知识库的元数据
return {
"message": "success",
"db_id": db_id,
}
except HTTPException:
raise
except Exception as e:
logger.error(f"保存思维导图失败: {e}, {traceback.format_exc()}")
raise HTTPException(status_code=500, detail=f"保存思维导图失败: {str(e)}")

View File

@ -13,12 +13,16 @@ class Context(BaseContext):
default_factory=list,
metadata={
"name": "工具",
"options": gen_tool_info(get_tools()), # 这里的选择是所有的工具
"options": lambda: gen_tool_info(get_tools()), # 这里的选择是所有的工具
"description": "工具列表",
},
)
mcps: list[str] = field(
default_factory=list,
metadata={"name": "MCP服务器", "options": list(MCP_SERVERS.keys()), "description": "MCP服务器列表"},
metadata={
"name": "MCP服务器",
"options": lambda: list(MCP_SERVERS.keys()),
"description": "MCP服务器列表",
},
)

View File

@ -6,7 +6,6 @@ import requests
from langchain.tools import tool
from src.agents.common import get_buildin_tools
from src.agents.common.toolkits.mysql import get_mysql_tools
from src.storage.minio import aupload_file_to_minio
from src.utils import logger
@ -52,5 +51,4 @@ def get_tools() -> list[Any]:
"""获取所有可运行的工具(给大模型使用)"""
tools = get_buildin_tools()
tools.append(text_to_img_demo)
tools.extend(get_mysql_tools())
return tools

View File

@ -107,10 +107,14 @@ class BaseContext:
# 提取 Annotated 的元数据
template_metadata = cls._extract_template_metadata(field_type)
options = f.metadata.get("options", [])
if callable(options):
options = options()
configurable_items[f.name] = {
"type": type_name,
"name": f.metadata.get("name", f.name),
"options": f.metadata.get("options", []),
"options": options,
"default": f.default
if f.default is not MISSING
else f.default_factory()

View File

@ -16,15 +16,15 @@ branding:
features:
- label: "GitHub Stars"
value: "2600+"
value: "3000+"
description: "开发者社区的认可与支持"
icon: "stars"
- label: "已解决 Issues"
value: "210+"
value: "250+"
description: "持续改进和问题解决能力"
icon: "issues"
- label: "累计 Commits"
value: "1000+"
value: "1100+"
description: "活跃的开发迭代和功能更新"
icon: "commits"
- label: "开源协议"

View File

@ -22,9 +22,6 @@ except ImportError:
def validate_database_schema(db_path):
return True, []
# TODO:[已完成]为DBManager添加异步支持
# TODO:[已完成]为DBManager添加单例模式
class DBManager(metaclass=SingletonMeta):
"""数据库管理器 - 提供异步数据库连接和会话管理"""

View File

@ -49,18 +49,6 @@ export const mindmapApi = {
*/
getByDatabase: async (dbId) => {
return apiAdminGet(`/api/mindmap/database/${dbId}`)
},
/**
* 保存思维导图到知识库
* @param {string} dbId - 知识库ID
* @param {Object} mindmapData - 思维导图数据
* @returns {Promise} - 保存结果
*/
saveToDatabase: async (dbId, mindmapData) => {
return apiAdminPost(`/api/mindmap/database/${dbId}`, {
mindmap: mindmapData
})
}
}

View File

@ -1043,7 +1043,7 @@ watch(conversations, () => {
.floating-sidebar {
position: absolute !important;
z-index: 100;
z-index: 101;
height: 100%;
left: 0;
top: 0;

View File

@ -221,10 +221,6 @@
<a-button @click="saveConfig" class="save-btn" :class="{'changed': agentStore.hasConfigChanges}">
保存配置
</a-button>
<!-- TODOBUG 目前有 bug 暂时不展示 -->
<!-- <a-button @click="resetConfig" class="reset-btn">
重置
</a-button> -->
</div>
</div>
</div>
@ -424,6 +420,10 @@ const getToolNameById = (toolId) => {
const openToolsModal = async () => {
console.log("availableTools.value", availableTools.value)
try {
//
if (selectedAgentId.value) {
await agentStore.fetchAgentDetail(selectedAgentId.value, true);
}
selectedTools.value = [...(agentConfig.value?.tools || [])];
toolsModalOpen.value = true;
} catch (error) {

View File

@ -39,17 +39,13 @@
:key="index"
class="todo-item"
>
<span class="todo-status" :class="todo.status">
<svg v-if="todo.status === 'completed'" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
</svg>
<svg v-else-if="todo.status === 'in_progress'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="icon spinning">
<circle cx="12" cy="12" r="10" stroke-dasharray="31.416" stroke-dashoffset="31.416" stroke-linecap="round"/>
</svg>
<svg v-else viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="icon">
<circle cx="12" cy="12" r="10"/>
</svg>
</span>
<div class="todo-status">
<CheckCircleOutlined v-if="todo.status === 'completed'" class="icon completed" />
<SyncOutlined v-else-if="todo.status === 'in_progress'" class="icon in-progress" spin />
<ClockCircleOutlined v-else-if="todo.status === 'pending'" class="icon pending" />
<CloseCircleOutlined v-else-if="todo.status === 'cancelled'" class="icon cancelled" />
<QuestionCircleOutlined v-else class="icon unknown" />
</div>
<span class="todo-text">{{ todo.content }}</span>
</div>
</div>
@ -110,6 +106,13 @@
<script setup>
import { computed, ref, watch } from 'vue';
import { Download } from 'lucide-vue-next';
import {
CheckCircleOutlined,
SyncOutlined,
ClockCircleOutlined,
CloseCircleOutlined,
QuestionCircleOutlined
} from '@ant-design/icons-vue'
const props = defineProps({
visible: {
@ -367,59 +370,19 @@ const emitRefresh = () => {
.todo-status {
flex-shrink: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 2px;
border-radius: 50%;
transition: all 0.15s ease;
.icon {
width: 14px;
height: 14px;
transition: all 0.15s ease;
}
.spinning {
animation: spin 1.5s linear infinite;
stroke-dasharray: 31.416;
stroke-dashoffset: 0;
animation: spin 1.5s linear infinite;
}
&.completed {
background: var(--color-success-50);
color: var(--color-success-700);
.icon {
transform: scale(1.1);
}
}
&.in_progress {
background: var(--color-warning-50);
color: var(--color-warning-700);
}
&.pending {
background: var(--gray-100);
color: var(--gray-500);
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
stroke-dashoffset: 0;
}
50% {
stroke-dashoffset: 15.708;
}
100% {
transform: rotate(360deg);
stroke-dashoffset: 0;
font-size: 16px;
&.completed { color: #52c41a; }
&.in-progress { color: #1890ff; }
&.pending { color: #faad14; }
&.cancelled { color: #ff4d4f; }
&.unknown { color: var(--gray-400); }
}
}

View File

@ -0,0 +1,2 @@
<template>
</template>

View File

@ -0,0 +1,118 @@
<template>
<div class="todo-list-result">
<!-- <div class="todo-header">
<h4><UnorderedListOutlined /> 待办事项</h4>
</div> -->
<div class="todo-content">
<div v-for="(item, index) in data" :key="index" class="todo-item">
<div class="todo-status">
<CheckCircleOutlined v-if="item.status === 'completed'" class="icon completed" />
<SyncOutlined v-else-if="item.status === 'in_progress'" class="icon in-progress" spin />
<ClockCircleOutlined v-else-if="item.status === 'pending'" class="icon pending" />
<CloseCircleOutlined v-else-if="item.status === 'cancelled'" class="icon cancelled" />
<QuestionCircleOutlined v-else class="icon unknown" />
</div>
<div class="todo-text" :class="{ completed: item.status === 'completed', cancelled: item.status === 'cancelled' }">
{{ item.content }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import {
UnorderedListOutlined,
CheckCircleOutlined,
SyncOutlined,
ClockCircleOutlined,
CloseCircleOutlined,
QuestionCircleOutlined
} from '@ant-design/icons-vue'
defineProps({
data: {
type: Array,
required: true
}
})
</script>
<style lang="less" scoped>
.todo-list-result {
background: var(--gray-0);
border-radius: 8px;
// border: 1px solid var(--gray-200);
overflow: hidden;
.todo-header {
padding: 12px 16px;
border-bottom: 1px solid var(--gray-100);
background: var(--gray-25);
h4 {
margin: 0;
font-size: 14px;
font-weight: 500;
color: var(--main-color);
display: flex;
align-items: center;
gap: 6px;
}
}
.todo-content {
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
.todo-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 8px 12px;
border-radius: 6px;
background: var(--gray-50);
border: 1px solid var(--gray-100);
transition: background-color 0.2s;
&:hover {
background: var(--gray-100);
}
.todo-status {
padding-top: 2px;
flex-shrink: 0;
.icon {
font-size: 16px;
&.completed { color: #52c41a; }
&.in-progress { color: #1890ff; }
&.pending { color: #faad14; }
&.cancelled { color: #ff4d4f; }
&.unknown { color: var(--gray-400); }
}
}
.todo-text {
font-size: 14px;
line-height: 1.5;
color: var(--gray-800);
word-break: break-word;
&.completed {
text-decoration: line-through;
color: var(--gray-500);
}
&.cancelled {
text-decoration: line-through;
color: var(--gray-400);
}
}
}
}
</style>

View File

@ -19,6 +19,12 @@
ref="graphResultRef"
/>
<!-- 待办事项结果 -->
<TodoListResult
v-else-if="isTodoListResult"
:data="todoListData"
/>
<!-- 计算器结果 -->
<CalculatorResult
v-else-if="isCalculatorResult"
@ -49,6 +55,7 @@ import WebSearchResult from './WebSearchResult.vue'
import KnowledgeBaseResult from './KnowledgeBaseResult.vue'
import KnowledgeGraphResult from './KnowledgeGraphResult.vue'
import CalculatorResult from './CalculatorResult.vue'
import TodoListResult from './TodoListResult.vue'
import { useAgentStore } from '@/stores/agent';
const agentStore = useAgentStore()
@ -81,6 +88,60 @@ const parsedData = computed(() => {
return props.resultContent
})
const todoListData = computed(() => {
if (props.toolName !== 'write_todos') return []
const raw = props.resultContent
// 1. Try from parsedData (JSON object)
const data = parsedData.value
if (data && typeof data === 'object') {
if (Array.isArray(data)) return data
if (data.todos && Array.isArray(data.todos)) return data.todos
}
// 2. Try parsing string if it matches specific pattern
if (typeof raw === 'string') {
let str = raw
if (str.startsWith('Updated todo list to ')) {
str = str.replace('Updated todo list to ', '')
}
// Try regex parsing for Python-like string
const items = []
// Matches {'content': '...', 'status': '...'} with escaped quotes support
// content might contain escaped quotes
const contentRegex = /'content':\s*'((?:[^'\\]|\\.)*)'/
const statusRegex = /'status':\s*'((?:[^'\\]|\\.)*)'/
// Split by "}, {" roughly, or just look for objects
// Since it is a list of dicts, we can match individual dicts
const dictRegex = /\{.*?\}/g
const dictMatches = str.match(dictRegex)
if (dictMatches) {
for (const dictStr of dictMatches) {
const contentMatch = dictStr.match(contentRegex)
const statusMatch = dictStr.match(statusRegex)
if (contentMatch && statusMatch) {
items.push({
content: contentMatch[1].replace(/\\'/g, "'").replace(/\\\\/g, "\\"),
status: statusMatch[1]
})
}
}
}
if (items.length > 0) return items
}
return []
})
const isTodoListResult = computed(() => {
return props.toolName === 'write_todos' && todoListData.value.length > 0
})
//
const isWebSearchResult = computed(() => {
const toolNameLower = props.toolName.toLowerCase()

View File

@ -146,11 +146,11 @@ export const useAgentStore = defineStore('agent', () => {
* 获取单个智能体的详细信息包含配置选项
* @param {string} agentId - 智能体ID
*/
async function fetchAgentDetail(agentId) {
async function fetchAgentDetail(agentId, forceRefresh = false) {
if (!agentId) return
// 如果已经缓存了详细信息,直接返回
if (agentDetails.value[agentId]) {
// 如果已经缓存了详细信息且不强制刷新,直接返回
if (!forceRefresh && agentDetails.value[agentId]) {
return agentDetails.value[agentId]
}

View File

@ -27,17 +27,17 @@ export const useInfoStore = defineStore('info', () => {
// 计算属性 - 功能特性
const features = computed(() => infoConfig.value.features || [{
label: "GitHub Stars",
value: "2600+",
value: "3000+",
description: "开发者社区的认可与支持",
icon: "stars"
}, {
label: "已解决 Issues",
value: "200+",
value: "250+",
description: "持续改进和问题解决能力",
icon: "issues"
}, {
label: "累计 Commits",
value: "1000+",
value: "1100+",
description: "活跃的开发迭代和功能更新",
icon: "commits"
}, {

View File

@ -75,6 +75,8 @@
</div>
</div>
<ProjectOverview />
<footer class="footer">
<div class="footer-content">
<p class="copyright">{{ infoStore.footer?.copyright || '© 2025 All rights reserved' }}</p>
@ -91,6 +93,7 @@ import { useInfoStore } from '@/stores/info'
import { useAgentStore } from '@/stores/agent'
import { useThemeStore } from '@/stores/theme'
import UserInfoComponent from '@/components/UserInfoComponent.vue'
import ProjectOverview from '@/components/ProjectOverview.vue'
import {
BookText,
Bug,
@ -218,7 +221,7 @@ const actionLinks = computed(() => {
<style lang="less" scoped>
.home-container {
height: 100vh;
min-height: 100vh;
display: flex;
flex-direction: column;
color: var(--main-900);
@ -226,7 +229,6 @@ const actionLinks = computed(() => {
position: relative;
overflow-x: hidden;
}
.glass-header {
display: flex;
justify-content: space-between;
@ -266,8 +268,6 @@ const actionLinks = computed(() => {
&:hover {
color: var(--gray-900);
svg {
transform: scale(1.1);
}
@ -511,7 +511,7 @@ const actionLinks = computed(() => {
.section {
width: 100%;
max-width: 1200px;
margin: 0 auto;
margin: 50px auto 0px auto;
padding: 2rem 0;
}