From 10270b559558f47a6084ae9a45022ad447c5d386 Mon Sep 17 00:00:00 2001
From: Soulter <905617992@qq.com>
Date: Sat, 17 May 2025 15:38:51 +0800
Subject: [PATCH 01/18] feat: alkaid framework and supports to customize webapi
endpoint
---
astrbot/core/star/context.py | 5 +
astrbot/dashboard/server.py | 10 +
dashboard/package.json | 3 +
.../full/vertical-sidebar/VerticalSidebar.vue | 4 +
.../full/vertical-sidebar/sidebarItem.ts | 6 +-
dashboard/src/router/MainRoutes.ts | 6 +-
dashboard/src/views/ATRIProject.vue | 87 ------
dashboard/src/views/AlkaidPage.vue | 249 ++++++++++++++++++
dashboard/src/views/ChatPage.vue | 28 +-
9 files changed, 292 insertions(+), 106 deletions(-)
delete mode 100644 dashboard/src/views/ATRIProject.vue
create mode 100644 dashboard/src/views/AlkaidPage.vue
diff --git a/astrbot/core/star/context.py b/astrbot/core/star/context.py
index d2df31ec4..b1aed8ed9 100644
--- a/astrbot/core/star/context.py
+++ b/astrbot/core/star/context.py
@@ -42,6 +42,8 @@ class Context:
platform_manager: PlatformManager = None
+ registered_web_apis: list = []
+
# back compatibility
_register_tasks: List[Awaitable] = []
_star_manager = None
@@ -301,3 +303,6 @@ class Context:
注册一个异步任务。
"""
self._register_tasks.append(task)
+
+ def register_web_api(self, route: str, view_handler: Awaitable, methods: list, desc: str):
+ self.registered_web_apis.append((route, view_handler, methods, desc))
diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py
index 124291718..a34606d31 100644
--- a/astrbot/dashboard/server.py
+++ b/astrbot/dashboard/server.py
@@ -15,6 +15,8 @@ from astrbot.core.db import BaseDatabase
from astrbot.core.utils.io import get_local_ip_addresses
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
+APP: Quart = None
+
class AstrBotDashboard:
def __init__(
@@ -27,6 +29,7 @@ class AstrBotDashboard:
self.config = core_lifecycle.astrbot_config
self.data_path = os.path.abspath(os.path.join(get_astrbot_data_path(), "dist"))
self.app = Quart("dashboard", static_folder=self.data_path, static_url_path="/")
+ APP = self.app # noqa
self.app.config["MAX_CONTENT_LENGTH"] = (
128 * 1024 * 1024
) # 将 Flask 允许的最大上传文件体大小设置为 128 MB
@@ -51,6 +54,13 @@ class AstrBotDashboard:
self.conversation_route = ConversationRoute(self.context, db, core_lifecycle)
self.file_route = FileRoute(self.context)
+ if self.core_lifecycle.star_context.registered_web_apis:
+ for api in self.core_lifecycle.star_context.registered_web_apis:
+ route, view_handler, methods, _ = api
+ self.app.add_url_rule(
+ f"/api/plug{route}", view_func=view_handler, methods=methods
+ )
+
self.shutdown_event = shutdown_event
async def auth_middleware(self):
diff --git a/dashboard/package.json b/dashboard/package.json
index a7edd4935..59d3f3042 100644
--- a/dashboard/package.json
+++ b/dashboard/package.json
@@ -21,12 +21,15 @@
"axios-mock-adapter": "^1.22.0",
"chance": "1.1.11",
"date-fns": "2.30.0",
+ "graphology": "^0.26.0",
+ "graphology-layout-force": "^0.2.4",
"highlight.js": "^11.11.1",
"js-md5": "^0.8.3",
"lodash": "4.17.21",
"marked": "^15.0.7",
"pinia": "2.1.6",
"remixicon": "3.5.0",
+ "sigma": "^3.0.1",
"vee-validate": "4.11.3",
"vite-plugin-vuetify": "1.0.2",
"vue": "3.3.4",
diff --git a/dashboard/src/layouts/full/vertical-sidebar/VerticalSidebar.vue b/dashboard/src/layouts/full/vertical-sidebar/VerticalSidebar.vue
index d505e0422..82f160746 100644
--- a/dashboard/src/layouts/full/vertical-sidebar/VerticalSidebar.vue
+++ b/dashboard/src/layouts/full/vertical-sidebar/VerticalSidebar.vue
@@ -166,6 +166,10 @@ function endDrag() {
+
+ 🔧 设置
+
+
官方文档
diff --git a/dashboard/src/layouts/full/vertical-sidebar/sidebarItem.ts b/dashboard/src/layouts/full/vertical-sidebar/sidebarItem.ts
index 803642e95..e8f49c741 100644
--- a/dashboard/src/layouts/full/vertical-sidebar/sidebarItem.ts
+++ b/dashboard/src/layouts/full/vertical-sidebar/sidebarItem.ts
@@ -66,9 +66,9 @@ const sidebarItem: menu[] = [
to: '/console'
},
{
- title: '设置',
- icon: 'mdi-wrench',
- to: '/settings'
+ title: 'Alkaid',
+ icon: 'mdi-test-tube',
+ to: '/alkaid'
},
{
title: '关于',
diff --git a/dashboard/src/router/MainRoutes.ts b/dashboard/src/router/MainRoutes.ts
index f1ac3002d..2d3defd54 100644
--- a/dashboard/src/router/MainRoutes.ts
+++ b/dashboard/src/router/MainRoutes.ts
@@ -57,9 +57,9 @@ const MainRoutes = {
component: () => import('@/views/ConsolePage.vue')
},
{
- name: 'Project ATRI',
- path: '/project-atri',
- component: () => import('@/views/ATRIProject.vue')
+ name: 'Alkaid',
+ path: '/alkaid',
+ component: () => import('@/views/AlkaidPage.vue')
},
{
name: 'Chat',
diff --git a/dashboard/src/views/ATRIProject.vue b/dashboard/src/views/ATRIProject.vue
deleted file mode 100644
index 4c9a771d4..000000000
--- a/dashboard/src/views/ATRIProject.vue
+++ /dev/null
@@ -1,87 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ save_message }}
-
-
-
-
-
\ No newline at end of file
diff --git a/dashboard/src/views/AlkaidPage.vue b/dashboard/src/views/AlkaidPage.vue
new file mode 100644
index 000000000..3690bedf5
--- /dev/null
+++ b/dashboard/src/views/AlkaidPage.vue
@@ -0,0 +1,249 @@
+
+
+
+
+
+
+
+
+
The Alkaid Project.
+ AstrBot 实验性项目
+
+
+
+
+ mdi-dots-hexagon
+ 长期记忆层
+
+
+ mdi-dots-horizontal
+ 其他
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dashboard/src/views/ChatPage.vue b/dashboard/src/views/ChatPage.vue
index 07eb41c22..e686e693c 100644
--- a/dashboard/src/views/ChatPage.vue
+++ b/dashboard/src/views/ChatPage.vue
@@ -12,19 +12,22 @@ marked.setOptions({
-
@@ -96,13 +113,13 @@
mdi-close
-
+
上传文件
搜索内容
-
+
@@ -111,21 +128,14 @@
上传文件到知识库
支持 txt、pdf、word、excel 等多种格式
-
-
-
+
+
-
+
@@ -136,71 +146,53 @@
mdi-close
-
+
-
+
上传到知识库
-
+
-
+
-
+
-
-
-
+
+
+
-
+
-
+
搜索结果
- mdi-file-document-outline
- {{ result.metadata.source }}
+ mdi-file-document-outline
+ {{
+ result.metadata.source }}
-
+
相关度: {{ Math.round(result.score * 100) }}%
@@ -208,7 +200,7 @@
-
+
没有找到匹配的内容
@@ -222,6 +214,22 @@
+
+
+
+ 确认删除
+
+ 您确定要删除知识库 {{ deleteTarget.collection_name }} 吗?
+ 此操作不可逆,所有知识库内容将被永久删除。
+
+
+
+ 取消
+ 删除
+
+
+
+
{{ snackbar.text }}
@@ -236,6 +244,8 @@ export default {
name: 'KnowledgeBase',
data() {
return {
+ installed: true,
+ installing: false,
kbCollections: [],
showCreateDialog: false,
showEmojiPicker: false,
@@ -287,13 +297,58 @@ export default {
searchResults: [],
searching: false,
searchPerformed: false,
- topK: 5
+ topK: 5,
+ showDeleteDialog: false,
+ deleteTarget: {
+ collection_name: ''
+ },
+ deleting: false
}
},
mounted() {
- this.getKBCollections();
+ this.checkPlugin();
},
methods: {
+ checkPlugin() {
+ axios.get('/api/plugin/get?name=astrbot_plugin_knowledge_base')
+ .then(response => {
+ if (response.data.status !== 'ok') {
+ this.showSnackbar('插件未安装或不可用', 'error');
+ }
+ if (response.data.data.length > 0) {
+ this.installed = true;
+ this.getKBCollections();
+ } else {
+ this.installed = false;
+ }
+ })
+ .catch(error => {
+ console.error('Error checking plugin:', error);
+ this.showSnackbar('检查插件失败', 'error');
+ })
+ },
+
+ installPlugin() {
+ this.installing = true;
+ axios.post('/api/plugin/install', {
+ url: "https://github.com/soulter/astrbot_plugin_knowledge_base",
+ proxy: localStorage.getItem('selectedGitHubProxy') || ""
+ })
+ .then(response => {
+ if (response.data.status === 'ok') {
+ this.checkPlugin();
+ } else {
+ this.showSnackbar(response.data.message || '安装失败', 'error');
+ }
+ })
+ .catch(error => {
+ console.error('Error installing plugin:', error);
+ this.showSnackbar('安装插件失败', 'error');
+ }).finally(() => {
+ this.installing = false;
+ });
+ },
+
getKBCollections() {
axios.get('/api/plug/alkaid/kb/collections')
.then(response => {
@@ -353,7 +408,7 @@ export default {
this.showContentDialog = true;
this.resetContentDialog();
},
-
+
resetContentDialog() {
this.activeTab = 'upload';
this.selectedFile = null;
@@ -361,28 +416,28 @@ export default {
this.searchResults = [];
this.searchPerformed = false;
},
-
+
triggerFileInput() {
this.$refs.fileInput.click();
},
-
+
onFileSelected(event) {
const files = event.target.files;
if (files.length > 0) {
this.selectedFile = files[0];
}
},
-
+
onFileDrop(event) {
const files = event.dataTransfer.files;
if (files.length > 0) {
this.selectedFile = files[0];
}
},
-
+
getFileIcon(filename) {
const extension = filename.split('.').pop().toLowerCase();
-
+
switch (extension) {
case 'pdf':
return 'mdi-file-pdf-box';
@@ -401,53 +456,53 @@ export default {
return 'mdi-file-outline';
}
},
-
+
uploadFile() {
if (!this.selectedFile) {
this.showSnackbar('请先选择文件', 'warning');
return;
}
-
+
this.uploading = true;
-
+
const formData = new FormData();
formData.append('file', this.selectedFile);
formData.append('collection_name', this.currentKB.collection_name);
-
+
axios.post('/api/plug/alkaid/kb/collection/add_file', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
- .then(response => {
- if (response.data.status === 'ok') {
- this.showSnackbar('文件上传成功');
- this.selectedFile = null;
-
- // 刷新知识库列表,获取更新的数量
- this.getKBCollections();
- } else {
- this.showSnackbar(response.data.message || '上传失败', 'error');
- }
- })
- .catch(error => {
- console.error('Error uploading file:', error);
- this.showSnackbar('文件上传失败', 'error');
- })
- .finally(() => {
- this.uploading = false;
- });
+ .then(response => {
+ if (response.data.status === 'ok') {
+ this.showSnackbar('文件上传成功');
+ this.selectedFile = null;
+
+ // 刷新知识库列表,获取更新的数量
+ this.getKBCollections();
+ } else {
+ this.showSnackbar(response.data.message || '上传失败', 'error');
+ }
+ })
+ .catch(error => {
+ console.error('Error uploading file:', error);
+ this.showSnackbar('文件上传失败', 'error');
+ })
+ .finally(() => {
+ this.uploading = false;
+ });
},
-
+
searchKnowledgeBase() {
if (!this.searchQuery.trim()) {
this.showSnackbar('请输入搜索内容', 'warning');
return;
}
-
+
this.searching = true;
this.searchPerformed = true;
-
+
axios.get(`/api/plug/alkaid/kb/collection/search`, {
params: {
collection_name: this.currentKB.collection_name,
@@ -455,26 +510,26 @@ export default {
top_k: this.topK
}
})
- .then(response => {
- if (response.data.status === 'ok') {
- this.searchResults = response.data.data || [];
-
- if (this.searchResults.length === 0) {
- this.showSnackbar('没有找到匹配的内容', 'info');
+ .then(response => {
+ if (response.data.status === 'ok') {
+ this.searchResults = response.data.data || [];
+
+ if (this.searchResults.length === 0) {
+ this.showSnackbar('没有找到匹配的内容', 'info');
+ }
+ } else {
+ this.showSnackbar(response.data.message || '搜索失败', 'error');
+ this.searchResults = [];
}
- } else {
- this.showSnackbar(response.data.message || '搜索失败', 'error');
+ })
+ .catch(error => {
+ console.error('Error searching knowledge base:', error);
+ this.showSnackbar('搜索知识库失败', 'error');
this.searchResults = [];
- }
- })
- .catch(error => {
- console.error('Error searching knowledge base:', error);
- this.showSnackbar('搜索知识库失败', 'error');
- this.searchResults = [];
- })
- .finally(() => {
- this.searching = false;
- });
+ })
+ .finally(() => {
+ this.searching = false;
+ });
},
showSnackbar(text, color = 'success') {
@@ -486,7 +541,43 @@ export default {
selectEmoji(emoji) {
this.newKB.emoji = emoji;
this.showEmojiPicker = false;
- }
+ },
+
+ confirmDelete(kb) {
+ this.deleteTarget = kb;
+ this.showDeleteDialog = true;
+ },
+
+ deleteKnowledgeBase() {
+ if (!this.deleteTarget.collection_name) {
+ this.showSnackbar('删除目标不存在', 'error');
+ return;
+ }
+
+ this.deleting = true;
+
+ axios.get('/api/plug/alkaid/kb/collection/delete', {
+ params: {
+ collection_name: this.deleteTarget.collection_name
+ }
+ })
+ .then(response => {
+ if (response.data.status === 'ok') {
+ this.showSnackbar('知识库删除成功');
+ this.getKBCollections(); // 刷新列表
+ this.showDeleteDialog = false;
+ } else {
+ this.showSnackbar(response.data.message || '删除失败', 'error');
+ }
+ })
+ .catch(error => {
+ console.error('Error deleting knowledge base:', error);
+ this.showSnackbar('删除知识库失败', 'error');
+ })
+ .finally(() => {
+ this.deleting = false;
+ });
+ },
}
}
@@ -502,7 +593,7 @@ export default {
.kb-card {
height: 280px;
border-radius: 8px;
- overflow: hidden;
+ overflow: hidden;
position: relative;
cursor: pointer;
display: flex;
@@ -638,4 +729,22 @@ export default {
background-color: rgba(0, 0, 0, 0.02);
border-radius: 4px;
}
+
+.kb-actions {
+ position: absolute;
+ bottom: 10px;
+ right: 10px;
+ display: flex;
+ gap: 8px;
+ opacity: 0;
+ transition: opacity 0.2s ease;
+}
+
+.kb-card {
+ position: relative;
+}
+
+.kb-card:hover .kb-actions {
+ opacity: 1;
+}
From cf814e81ee1b7542ca0dbb49865330a457010f59 Mon Sep 17 00:00:00 2001
From: Soulter <905617992@qq.com>
Date: Fri, 23 May 2025 16:41:33 +0800
Subject: [PATCH 15/18] chore: delete alkaid route
---
.../src/layouts/full/vertical-sidebar/sidebarItem.ts | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/dashboard/src/layouts/full/vertical-sidebar/sidebarItem.ts b/dashboard/src/layouts/full/vertical-sidebar/sidebarItem.ts
index e8f49c741..f541d1d91 100644
--- a/dashboard/src/layouts/full/vertical-sidebar/sidebarItem.ts
+++ b/dashboard/src/layouts/full/vertical-sidebar/sidebarItem.ts
@@ -65,11 +65,11 @@ const sidebarItem: menu[] = [
icon: 'mdi-console',
to: '/console'
},
- {
- title: 'Alkaid',
- icon: 'mdi-test-tube',
- to: '/alkaid'
- },
+ // {
+ // title: 'Alkaid',
+ // icon: 'mdi-test-tube',
+ // to: '/alkaid'
+ // },
{
title: '关于',
icon: 'mdi-information',
From 0fa164e50dae3b7ca71a497d6cca50650d2985ee Mon Sep 17 00:00:00 2001
From: Soulter <37870767+Soulter@users.noreply.github.com>
Date: Fri, 23 May 2025 16:48:29 +0800
Subject: [PATCH 16/18] =?UTF-8?q?perf:=20=E4=BD=BF=E7=94=A8=20HTML=20autoc?=
=?UTF-8?q?omplete=20=E5=B1=9E=E6=80=A7=E7=A6=81=E7=94=A8=E6=B5=8F?=
=?UTF-8?q?=E8=A7=88=E5=99=A8=E8=87=AA=E5=8A=A8=E5=A1=AB=E5=85=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
---
dashboard/src/views/ChatPage.vue | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dashboard/src/views/ChatPage.vue b/dashboard/src/views/ChatPage.vue
index e686e693c..ad149bcd0 100644
--- a/dashboard/src/views/ChatPage.vue
+++ b/dashboard/src/views/ChatPage.vue
@@ -154,7 +154,7 @@ marked.setOptions({
-
From 5125568ea29a45f4e148d1bb39a96269aa223aba Mon Sep 17 00:00:00 2001
From: Soulter <37870767+Soulter@users.noreply.github.com>
Date: Fri, 23 May 2025 16:49:08 +0800
Subject: [PATCH 17/18] =?UTF-8?q?perf:=20=E4=BA=A4=E6=8D=A2=20if/else=20?=
=?UTF-8?q?=E8=A1=A8=E8=BE=BE=E5=BC=8F=E7=9A=84=E5=88=86=E6=94=AF=E4=BB=A5?=
=?UTF-8?q?=E5=88=A0=E9=99=A4=E5=90=A6=E5=AE=9A?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
---
astrbot/core/db/vec_db/faiss_impl/vec_db.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/astrbot/core/db/vec_db/faiss_impl/vec_db.py b/astrbot/core/db/vec_db/faiss_impl/vec_db.py
index a393df46a..87edad7e7 100644
--- a/astrbot/core/db/vec_db/faiss_impl/vec_db.py
+++ b/astrbot/core/db/vec_db/faiss_impl/vec_db.py
@@ -76,7 +76,7 @@ class FaissVecDB(BaseVecDB):
embedding = await self.embedding_provider.get_embedding(query)
scores, indices = await self.embedding_storage.search(
vector=np.array([embedding]).astype("float32"),
- k=k if not metadata_filters else fetch_k,
+ k=fetch_k if metadata_filters else k,
)
# TODO: rerank
if len(indices[0]) == 0 or indices[0][0] == -1:
From a36e11973db133052f905bb5b6e2eda3f683a827 Mon Sep 17 00:00:00 2001
From: Soulter <37870767+Soulter@users.noreply.github.com>
Date: Fri, 23 May 2025 16:56:09 +0800
Subject: [PATCH 18/18] perf: code quality
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
---
astrbot/core/db/vec_db/faiss_impl/embedding_storage.py | 6 +++---
astrbot/core/db/vec_db/faiss_impl/vec_db.py | 4 +---
2 files changed, 4 insertions(+), 6 deletions(-)
diff --git a/astrbot/core/db/vec_db/faiss_impl/embedding_storage.py b/astrbot/core/db/vec_db/faiss_impl/embedding_storage.py
index 985f00cc9..262a459e3 100644
--- a/astrbot/core/db/vec_db/faiss_impl/embedding_storage.py
+++ b/astrbot/core/db/vec_db/faiss_impl/embedding_storage.py
@@ -9,14 +9,14 @@ import numpy as np
class EmbeddingStorage:
- def __init__(self, dimention: int, path: str = None):
- self.dimention = dimention
+ def __init__(self, dimension: int, path: str = None):
+ self.dimension = dimension
self.path = path
self.index = None
if path and os.path.exists(path):
self.index = faiss.read_index(path)
else:
- base_index = faiss.IndexFlatL2(dimention)
+ base_index = faiss.IndexFlatL2(dimension)
self.index = faiss.IndexIDMap(base_index)
self.storage = {}
diff --git a/astrbot/core/db/vec_db/faiss_impl/vec_db.py b/astrbot/core/db/vec_db/faiss_impl/vec_db.py
index 87edad7e7..e4122e547 100644
--- a/astrbot/core/db/vec_db/faiss_impl/vec_db.py
+++ b/astrbot/core/db/vec_db/faiss_impl/vec_db.py
@@ -91,9 +91,7 @@ class FaissVecDB(BaseVecDB):
return []
result_docs = []
- idx_pos = {}
- for idx, fetch_doc in enumerate(fetched_docs):
- idx_pos[fetch_doc["id"]] = idx
+ idx_pos = {fetch_doc["id"]: idx for idx, fetch_doc in enumerate(fetched_docs)}
for i, indice_idx in enumerate(indices[0]):
pos = idx_pos.get(indice_idx)
if pos is None: