-
{{ extension.author }} /
-
-
- {{ extension.name }}
-
-
-
+
+
+
+
+
+
+
+
+
+
+
- {{ tm("card.status.hasUpdate") }}: {{ extension.online_version }}
-
-
-
-
-
- {{ tm("card.status.disabled") }}
-
-
-
-
-
- {{ extension.version }}
-
-
-
- {{ extension.online_version }}
-
-
-
- {{ extension.handlers?.length }}{{ tm("card.status.handlersCount") }}
-
-
- {{ tag === 'danger' ? tm('tags.danger') : tag }}
-
+
+
+ 📄 {{ tm('buttons.viewDocs') }}
+
+
+
+
+ {{ tm('buttons.install') }}
+
+
+
+ {{ tm('status.installed') }}
+
+
+
+
+
+
+
+
+ {{ tm('card.actions.pluginConfig') }}
+
+
+
+ {{ tm('card.actions.uninstallPlugin') }}
+
+
+
+ {{ tm('card.actions.reloadPlugin') }}
+
+
+
+
+ {{ extension.activated ? tm('buttons.disable') : tm('buttons.enable') }}{{
+ tm('card.actions.togglePlugin') }}
+
+
+
+
+ {{ tm('card.actions.viewHandlers') }} ({{ extension.handlers.length
+ }})
+
+
+
+
+ {{ tm('card.actions.updateTo') }} {{ extension.online_version || extension.version }}
+
+
+
+
+
-
- {{ extension.desc }}
+
+
+
+ {{ extension.author }} / {{ extension.name }}
+
+
+ {{ extension.display_name?.length ? extension.display_name : extension.name }}
+
+
+
+
+ {{ tm("card.status.hasUpdate") }}: {{ extension.online_version }}
+
+
+
+
+
+ {{ tm("card.status.disabled") }}
+
+
+
+
+
+
+ {{ extension.version }}
+
+
+
+ {{ extension.online_version }}
+
+
+
+ {{ extension.handlers?.length }}{{ tm("card.status.handlersCount") }}
+
+
+ {{ tag === 'danger' ? tm('tags.danger') : tag }}
+
+
+
+
+ {{ extension.desc }}
+
-
-
-
-
-
-
-
-
+
+
+ {{ tm('buttons.viewDocs') }}
+
+
+ {{ tm('card.actions.pluginConfig') }}
+
-
-
-
-
-
-
-
{{ extension.name }}
-
-
-
-
- {{ tm("card.actions.pluginConfig") }}
-
-
-
- {{ tm("card.actions.uninstallPlugin") }}
-
-
-
- {{ tm("card.actions.reloadPlugin") }}
-
-
-
- {{ extension.activated ? tm('buttons.disable') : tm('buttons.enable') }}{{ tm("card.actions.togglePlugin") }}
-
-
-
- {{ tm("card.actions.viewHandlers") }} ({{ extension.handlers.length }})
-
-
-
- {{ tm("card.actions.updateTo") }} {{ extension.online_version || extension.version }}
-
-
-
-
-
-
-
-
-
@@ -219,9 +229,27 @@ const viewReadme = () => {
margin-left: 12px;
}
+.extension-title {
+ display: flex;
+ align-items: center;
+}
+
+.extension-title__text {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ padding-top: 6px
+}
+
@media (max-width: 600px) {
.extension-image-container {
margin-left: 8px;
}
}
+
+.extension-actions {
+ margin-top: auto;
+ gap: 8px;
+}
diff --git a/dashboard/src/components/shared/ItemCard.vue b/dashboard/src/components/shared/ItemCard.vue
index 14453ae7a..7c3013239 100644
--- a/dashboard/src/components/shared/ItemCard.vue
+++ b/dashboard/src/components/shared/ItemCard.vue
@@ -27,7 +27,9 @@
{{ t('core.common.itemCard.delete') }}
@@ -35,7 +37,9 @@
{{ t('core.common.itemCard.edit') }}
@@ -44,11 +48,14 @@
v-if="showCopyButton"
variant="tonal"
color="secondary"
+ size="small"
rounded="xl"
+ :disabled="loading"
@click="$emit('copy', item)"
>
{{ t('core.common.itemCard.copy') }}
+
@@ -120,7 +127,6 @@ export default {
transition: all 0.3s ease;
overflow: hidden;
min-height: 220px;
- margin-bottom: 16px;
display: flex;
flex-direction: column;
justify-content: space-between;
diff --git a/dashboard/src/components/shared/KnowledgeBaseSelector.vue b/dashboard/src/components/shared/KnowledgeBaseSelector.vue
index 3e0a8be15..a90e6f5e2 100644
--- a/dashboard/src/components/shared/KnowledgeBaseSelector.vue
+++ b/dashboard/src/components/shared/KnowledgeBaseSelector.vue
@@ -1,11 +1,21 @@
-
+
未选择
-
- {{ modelValue }}
-
+
+
+ {{ name }}
+
+
{{ buttonText }}
@@ -21,86 +31,57 @@
-
-
-
mdi-puzzle-outline
-
知识库插件未安装
-
- 前往知识库页面
-
-
-
-
-
-
-
- mdi-close-circle-outline
-
- 不使用
- 不使用任何知识库
-
-
- mdi-check-circle
-
-
-
-
-
+
- {{ kb.emoji || '🙂' }}
+ {{ kb.emoji || '📚' }}
- {{ kb.collection_name }}
+ {{ kb.kb_name }}
{{ kb.description || '无描述' }}
- - {{ kb.count }} 项知识
+ - {{ kb.doc_count }} 个文档
+ - {{ kb.chunk_count }} 个块
- mdi-check-circle
+
+ mdi-checkbox-marked
+
+
+ mdi-checkbox-blank-outline
+
-
-
暂无知识库
-
+
+
mdi-database-off
+
暂无知识库
+
创建知识库
-
-
-
-
mdi-database-off
-
暂无知识库
-
- 创建知识库
-
-
+
+ 已选择 {{ selectedKnowledgeBases.length }} 个知识库
+
取消
+ @click="confirmSelection">
确认选择
@@ -115,8 +96,8 @@ import { useRouter } from 'vue-router'
const props = defineProps({
modelValue: {
- type: String,
- default: ''
+ type: Array,
+ default: () => []
},
buttonText: {
type: String,
@@ -130,74 +111,87 @@ const router = useRouter()
const dialog = ref(false)
const knowledgeBaseList = ref([])
const loading = ref(false)
-const selectedKnowledgeBase = ref('')
-const pluginInstalled = ref(false)
+const selectedKnowledgeBases = ref([])
-// 监听 modelValue 变化,同步到 selectedKnowledgeBase
+// 监听 modelValue 变化,同步到 selectedKnowledgeBases
watch(() => props.modelValue, (newValue) => {
- selectedKnowledgeBase.value = newValue || ''
+ selectedKnowledgeBases.value = Array.isArray(newValue) ? [...newValue] : []
}, { immediate: true })
async function openDialog() {
- selectedKnowledgeBase.value = props.modelValue || ''
+ // 初始化选中状态
+ selectedKnowledgeBases.value = Array.isArray(props.modelValue)
+ ? [...props.modelValue]
+ : []
+
dialog.value = true
- await checkPluginAndLoadKnowledgeBases()
+ await loadKnowledgeBases()
}
-async function checkPluginAndLoadKnowledgeBases() {
+async function loadKnowledgeBases() {
loading.value = true
try {
- // 首先检查插件是否安装
- const pluginResponse = await axios.get('/api/plugin/get?name=astrbot_plugin_knowledge_base')
+ const response = await axios.get('/api/kb/list', {
+ params: {
+ page: 1,
+ page_size: 100
+ }
+ })
- if (pluginResponse.data.status === 'ok' && pluginResponse.data.data.length > 0) {
- pluginInstalled.value = true
- // 插件已安装,获取知识库列表
- await loadKnowledgeBases()
+ if (response.data.status === 'ok') {
+ knowledgeBaseList.value = response.data.data.items || []
} else {
- pluginInstalled.value = false
+ console.error('加载知识库列表失败:', response.data.message)
knowledgeBaseList.value = []
}
} catch (error) {
- console.error('检查知识库插件失败:', error)
- pluginInstalled.value = false
+ console.error('加载知识库列表失败:', error)
knowledgeBaseList.value = []
} finally {
loading.value = false
}
}
-async function loadKnowledgeBases() {
- try {
- const response = await axios.get('/api/plug/alkaid/kb/collections')
- if (response.data.status === 'ok') {
- knowledgeBaseList.value = response.data.data || []
- } else {
- knowledgeBaseList.value = []
- }
- } catch (error) {
- console.error('加载知识库列表失败:', error)
- knowledgeBaseList.value = []
+function isSelected(kbName) {
+ return selectedKnowledgeBases.value.includes(kbName)
+}
+
+function selectKnowledgeBase(kbName) {
+ // 多选模式:切换选中状态
+ const index = selectedKnowledgeBases.value.indexOf(kbName)
+ if (index > -1) {
+ selectedKnowledgeBases.value.splice(index, 1)
+ } else {
+ selectedKnowledgeBases.value.push(kbName)
}
}
-function selectKnowledgeBase(kb) {
- selectedKnowledgeBase.value = kb.collection_name
+function removeKnowledgeBase(kbName) {
+ const index = selectedKnowledgeBases.value.indexOf(kbName)
+ if (index > -1) {
+ selectedKnowledgeBases.value.splice(index, 1)
+ }
+
+ // 立即更新父组件
+ emit('update:modelValue', [...selectedKnowledgeBases.value])
}
function confirmSelection() {
- emit('update:modelValue', selectedKnowledgeBase.value)
+ emit('update:modelValue', [...selectedKnowledgeBases.value])
dialog.value = false
}
function cancelSelection() {
- selectedKnowledgeBase.value = props.modelValue || ''
+ // 恢复到原始值
+ selectedKnowledgeBases.value = Array.isArray(props.modelValue)
+ ? [...props.modelValue]
+ : []
dialog.value = false
}
function goToKnowledgeBasePage() {
dialog.value = false
- router.push('/alkaid/knowledge-base')
+ router.push('/knowledge-base')
}
@@ -222,4 +216,8 @@ function goToKnowledgeBasePage() {
align-items: center;
justify-content: center;
}
+
+.gap-1 {
+ gap: 4px;
+}
diff --git a/dashboard/src/components/shared/PersonaForm.vue b/dashboard/src/components/shared/PersonaForm.vue
new file mode 100644
index 000000000..48f1a0d0e
--- /dev/null
+++ b/dashboard/src/components/shared/PersonaForm.vue
@@ -0,0 +1,536 @@
+
+
+
+
+ {{ editingPersona ? tm('dialog.edit.title') : tm('dialog.create.title') }}
+
+
+
+
+
+
+
+
+
+
+
+
+ mdi-tools
+ {{ tm('form.tools') }}
+
+ {{ personaForm.tools.length }}
+
+
+
+
+
+
+ {{ tm('form.toolsHelp') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ tm('form.mcpServersQuickSelect') }}
+
+
+ mdi-server
+ {{ server.name }}
+
+ ({{ server.tools.length }})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.name }}
+
+ {{ item.mcp_server_name }}
+
+
+
+
+ {{ truncateText(item.description, 100) }}
+
+
+
+
+
+
+
+
mdi-tools
+
{{ tm('form.noToolsAvailable')
+ }}
+
+
+
+
+
mdi-magnify
+
{{ tm('form.noToolsFound') }}
+
+
+
+
+
+
+
{{ tm('form.loadingTools')
+ }}
+
+
+
+
+
+
+ {{ tm('form.selectedTools') }}
+
+ ({{ tm('form.allSelected') }})
+
+
+ ({{ personaForm.tools.length }})
+
+
+
+
+ {{ toolName }}
+
+
+
+ {{ tm('form.noToolsSelected') }}
+
+
+
+
+
+
+
+
+
+
+ mdi-chat
+ {{ tm('form.presetDialogs') }}
+
+ {{ personaForm.begin_dialogs.length / 2 }}
+
+
+
+
+
+
+ {{ tm('form.presetDialogsHelp') }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ tm('buttons.addDialogPair') }}
+
+
+
+
+
+
+
+
+
+
+ {{ tm('buttons.cancel') }}
+
+
+ {{ tm('buttons.save') }}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dashboard/src/components/shared/PersonaSelector.vue b/dashboard/src/components/shared/PersonaSelector.vue
index c4fbfa10d..c90ee2df2 100644
--- a/dashboard/src/components/shared/PersonaSelector.vue
+++ b/dashboard/src/components/shared/PersonaSelector.vue
@@ -18,7 +18,7 @@
选择人格
-
+
@@ -48,6 +48,9 @@
+
+ 创建新人格
+
取消
+
+
+
\ No newline at end of file
diff --git a/dashboard/src/layouts/full/vertical-sidebar/VerticalSidebar.vue b/dashboard/src/layouts/full/vertical-sidebar/VerticalSidebar.vue
index e1c9f62d8..d21321fc3 100644
--- a/dashboard/src/layouts/full/vertical-sidebar/VerticalSidebar.vue
+++ b/dashboard/src/layouts/full/vertical-sidebar/VerticalSidebar.vue
@@ -11,8 +11,13 @@ const customizer = useCustomizerStore();
const sidebarMenu = shallowRef(sidebarItems);
const showIframe = ref(false);
+const starCount = ref(null);
+
+const sidebarWidth = ref(235);
+const minSidebarWidth = 200;
+const maxSidebarWidth = 300;
+const isResizing = ref(false);
-// 默认桌面端 iframe 样式
const iframeStyle = ref({
position: 'fixed',
bottom: '16px',
@@ -29,14 +34,13 @@ const iframeStyle = ref({
boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)',
});
-// 如果为移动端,则采用百分比尺寸,并设置初始位置
if (window.innerWidth < 768) {
iframeStyle.value = {
position: 'fixed',
top: '10%',
left: '0%',
width: '100%',
- height: '50%',
+ height: '80%',
minWidth: '300px',
minHeight: '200px',
background: 'white',
@@ -46,7 +50,6 @@ if (window.innerWidth < 768) {
borderRadius: '12px',
boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)',
};
- // 移动端默认关闭侧边栏
customizer.Sidebar_drawer = false;
}
@@ -74,12 +77,10 @@ function openIframeLink(url) {
}
}
-// 拖拽相关变量与函数
let offsetX = 0;
let offsetY = 0;
let isDragging = false;
-// 辅助函数:限制数值在一定范围内
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
@@ -91,7 +92,6 @@ function startDrag(clientX, clientY) {
offsetX = clientX - rect.left;
offsetY = clientY - rect.top;
document.body.style.userSelect = 'none';
- // 绑定全局鼠标和触摸事件
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
document.addEventListener('touchmove', onTouchMove, { passive: false });
@@ -149,6 +149,53 @@ function endDrag() {
document.removeEventListener('touchend', onTouchEnd);
}
+function startSidebarResize(event) {
+ isResizing.value = true;
+ document.body.style.userSelect = 'none';
+ document.body.style.cursor = 'ew-resize';
+
+ const startX = event.clientX;
+ const startWidth = sidebarWidth.value;
+
+ function onMouseMoveResize(event) {
+ if (!isResizing.value) return;
+
+ const deltaX = event.clientX - startX;
+ const newWidth = Math.max(minSidebarWidth, Math.min(maxSidebarWidth, startWidth + deltaX));
+ sidebarWidth.value = newWidth;
+ }
+
+ function onMouseUpResize() {
+ isResizing.value = false;
+ document.body.style.userSelect = '';
+ document.body.style.cursor = '';
+ document.removeEventListener('mousemove', onMouseMoveResize);
+ document.removeEventListener('mouseup', onMouseUpResize);
+ }
+
+ document.addEventListener('mousemove', onMouseMoveResize);
+ document.addEventListener('mouseup', onMouseUpResize);
+}
+
+function formatNumber(num) {
+ return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
+}
+
+async function fetchStarCount() {
+ try {
+ const response = await fetch('https://cloud.astrbot.app/api/v1/github/repo-info');
+ const data = await response.json();
+ if (data.data && data.data.stargazers_count) {
+ starCount.value = data.data.stargazers_count;
+ console.debug('Fetched star count:', starCount.value);
+ }
+ } catch (error) {
+ console.debug('Failed to fetch star count:', error);
+ }
+}
+
+fetchStarCount();
+
@@ -159,7 +206,7 @@ function endDrag() {
rail-width="80"
app
class="leftSidebar"
- width="220"
+ :width="sidebarWidth"
:rail="customizer.mini_sidebar"
>
+
+
+
-
+
{{ t('core.navigation.drag') }}
-
-
-
-
\ No newline at end of file
+
+
+
\ No newline at end of file
diff --git a/dashboard/src/layouts/full/vertical-sidebar/sidebarItem.ts b/dashboard/src/layouts/full/vertical-sidebar/sidebarItem.ts
index 861a51e47..3203985cd 100644
--- a/dashboard/src/layouts/full/vertical-sidebar/sidebarItem.ts
+++ b/dashboard/src/layouts/full/vertical-sidebar/sidebarItem.ts
@@ -18,31 +18,26 @@ export interface menu {
// 在组件中使用时需要通过t()函数进行翻译
// 所有键名都使用 core.navigation.* 格式
const sidebarItem: menu[] = [
- {
- title: 'core.navigation.dashboard',
- icon: 'mdi-view-dashboard',
- to: '/dashboard/default'
- },
{
title: 'core.navigation.platforms',
- icon: 'mdi-message-processing',
- to: '/platforms',
+ icon: 'mdi-robot',
+ to: '/',
},
{
title: 'core.navigation.providers',
icon: 'mdi-creation',
to: '/providers',
},
+ {
+ title: 'core.navigation.config',
+ icon: 'mdi-cog',
+ to: '/config',
+ },
{
title: 'core.navigation.toolUse',
icon: 'mdi-function-variant',
to: '/tool-use'
},
- {
- title: 'core.navigation.persona',
- icon: 'mdi-heart',
- to: '/persona'
- },
{
title: 'core.navigation.extension',
icon: 'mdi-puzzle',
@@ -50,13 +45,8 @@ const sidebarItem: menu[] = [
},
{
title: 'core.navigation.knowledgeBase',
- icon: 'mdi-text-box-search',
- to: '/alkaid/knowledge-base',
- },
- {
- title: 'core.navigation.config',
- icon: 'mdi-cog',
- to: '/config',
+ icon: 'mdi-book-open-variant',
+ to: '/knowledge-base',
},
{
title: 'core.navigation.chat',
@@ -64,20 +54,36 @@ const sidebarItem: menu[] = [
to: '/chat'
},
{
- title: 'core.navigation.conversation',
- icon: 'mdi-database',
- to: '/conversation'
- },
- {
- title: 'core.navigation.sessionManagement',
- icon: 'mdi-account-group',
- to: '/session-management'
- },
- {
- title: 'core.navigation.console',
- icon: 'mdi-console',
- to: '/console'
- },
+ title: 'core.navigation.groups.more',
+ icon: 'mdi-dots-horizontal',
+ children: [
+ {
+ title: 'core.navigation.persona',
+ icon: 'mdi-heart',
+ to: '/persona'
+ },
+ {
+ title: 'core.navigation.conversation',
+ icon: 'mdi-database',
+ to: '/conversation'
+ },
+ {
+ title: 'core.navigation.sessionManagement',
+ icon: 'mdi-account-group',
+ to: '/session-management'
+ },
+ {
+ title: 'core.navigation.dashboard',
+ icon: 'mdi-view-dashboard',
+ to: '/dashboard/default'
+ },
+ {
+ title: 'core.navigation.console',
+ icon: 'mdi-console',
+ to: '/console'
+ },
+ ]
+ }
// {
// title: 'Project ATRI',
// icon: 'mdi-grain',
diff --git a/dashboard/src/main.ts b/dashboard/src/main.ts
index 7d522fe6d..5c7b8c096 100644
--- a/dashboard/src/main.ts
+++ b/dashboard/src/main.ts
@@ -49,6 +49,6 @@ axios.interceptors.request.use((config) => {
loader.config({
paths: {
- vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.43.0/min/vs',
+ vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.54.0/min/vs',
},
})
\ No newline at end of file
diff --git a/dashboard/src/router/MainRoutes.ts b/dashboard/src/router/MainRoutes.ts
index 29b3bf5e7..276d37444 100644
--- a/dashboard/src/router/MainRoutes.ts
+++ b/dashboard/src/router/MainRoutes.ts
@@ -3,13 +3,13 @@ const MainRoutes = {
meta: {
requiresAuth: true
},
- redirect: '/main/dashboard/default',
+ redirect: '/main/platforms',
component: () => import('@/layouts/full/FullLayout.vue'),
children: [
{
- name: 'Dashboard',
+ name: 'MainPage',
path: '/',
- component: () => import('@/views/dashboards/default/DefaultDashboard.vue')
+ component: () => import('@/views/PlatformPage.vue')
},
{
name: 'Extensions',
@@ -66,6 +66,37 @@ const MainRoutes = {
path: '/console',
component: () => import('@/views/ConsolePage.vue')
},
+ {
+ name: 'NativeKnowledgeBase',
+ path: '/knowledge-base',
+ component: () => import('@/views/knowledge-base/index.vue'),
+ children: [
+ {
+ path: '',
+ name: 'NativeKBList',
+ component: () => import('@/views/knowledge-base/KBList.vue')
+ },
+ {
+ path: ':kbId',
+ name: 'NativeKBDetail',
+ component: () => import('@/views/knowledge-base/KBDetail.vue'),
+ props: true
+ },
+ {
+ path: ':kbId/document/:docId',
+ name: 'NativeDocumentDetail',
+ component: () => import('@/views/knowledge-base/DocumentDetail.vue'),
+ props: true
+ }
+ ]
+ },
+
+ // 旧版本的知识库路由
+ {
+ name: 'KnowledgeBase',
+ path: '/alkaid/knowledge-base',
+ component: () => import('@/views/alkaid/KnowledgeBase.vue'),
+ },
// {
// name: 'Alkaid',
// path: '/alkaid',
@@ -88,11 +119,6 @@ const MainRoutes = {
// }
// ]
// },
- {
- name: 'KnowledgeBase',
- path: '/alkaid/knowledge-base',
- component: () => import('@/views/alkaid/KnowledgeBase.vue')
- },
{
name: 'Chat',
path: '/chat',
diff --git a/dashboard/src/stores/common.js b/dashboard/src/stores/common.js
index fb951fc0a..17d760bcc 100644
--- a/dashboard/src/stores/common.js
+++ b/dashboard/src/stores/common.js
@@ -159,10 +159,10 @@ export const useCommonStore = defineStore({
if (!force && this.pluginMarketData.length > 0) {
return Promise.resolve(this.pluginMarketData);
}
-
+
// 如果是强制刷新,添加 force_refresh 参数
const url = force ? '/api/plugin/market_list?force_refresh=true' : '/api/plugin/market_list';
-
+
return axios.get(url)
.then((res) => {
let data = []
@@ -180,6 +180,7 @@ export const useCommonStore = defineStore({
"pinned": res.data.data[key]?.pinned ? res.data.data[key].pinned : false,
"stars": res.data.data[key]?.stars ? res.data.data[key].stars : 0,
"updated_at": res.data.data[key]?.updated_at ? res.data.data[key].updated_at : "",
+ "display_name": res.data.data[key]?.display_name ? res.data.data[key].display_name : "",
})
}
this.pluginMarketData = data;
diff --git a/dashboard/src/theme/LightTheme.ts b/dashboard/src/theme/LightTheme.ts
index b8fdec259..84240cb13 100644
--- a/dashboard/src/theme/LightTheme.ts
+++ b/dashboard/src/theme/LightTheme.ts
@@ -42,7 +42,7 @@ const PurpleTheme: ThemeTypes = {
preBg: 'rgb(249, 249, 249)',
code: 'rgb(13, 13, 13)',
chatMessageBubble: '#e7ebf4',
- mcpCardBg: '#f7f2f9',
+ mcpCardBg: '#ecf2faff',
}
};
diff --git a/dashboard/src/utils/platformUtils.js b/dashboard/src/utils/platformUtils.js
index 2656c56c7..7ddbad7ae 100644
--- a/dashboard/src/utils/platformUtils.js
+++ b/dashboard/src/utils/platformUtils.js
@@ -10,7 +10,7 @@
export function getPlatformIcon(name) {
if (name === 'aiocqhttp' || name === 'qq_official' || name === 'qq_official_webhook') {
return new URL('@/assets/images/platform_logos/qq.png', import.meta.url).href
- } else if (name === 'wecom') {
+ } else if (name === 'wecom' || name === 'wecom_ai_bot') {
return new URL('@/assets/images/platform_logos/wecom.png', import.meta.url).href
} else if (name === 'wechatpadpro' || name === 'weixin_official_account' || name === 'wechat') {
return new URL('@/assets/images/platform_logos/wechat.png', import.meta.url).href
@@ -46,6 +46,7 @@ export function getTutorialLink(platformType) {
"qq_official": "https://docs.astrbot.app/deploy/platform/qqofficial/websockets.html",
"aiocqhttp": "https://docs.astrbot.app/deploy/platform/aiocqhttp/napcat.html",
"wecom": "https://docs.astrbot.app/deploy/platform/wecom.html",
+ "wecom_ai_bot": "https://docs.astrbot.app/deploy/platform/wecom_ai_bot.html",
"lark": "https://docs.astrbot.app/deploy/platform/lark.html",
"telegram": "https://docs.astrbot.app/deploy/platform/telegram.html",
"dingtalk": "https://docs.astrbot.app/deploy/platform/dingtalk.html",
diff --git a/dashboard/src/views/AboutPage.vue b/dashboard/src/views/AboutPage.vue
index 4c9a9d777..03ce75f70 100644
--- a/dashboard/src/views/AboutPage.vue
+++ b/dashboard/src/views/AboutPage.vue
@@ -5,11 +5,11 @@
{{ tm('hero.title') }}
{{ tm('hero.subtitle') }}
-
{{ tm('hero.starButton') }}
-
{{ tm('hero.issueButton') }}
diff --git a/dashboard/src/views/AlkaidPage.vue b/dashboard/src/views/AlkaidPage.vue
index 73233fa59..c23cf1f37 100644
--- a/dashboard/src/views/AlkaidPage.vue
+++ b/dashboard/src/views/AlkaidPage.vue
@@ -7,9 +7,9 @@
{{ tm('page.subtitle') }}
-
+
mdi-text-box-search
{{ tm('page.navigation.knowledgeBase') }}
@@ -21,7 +21,7 @@
{{ tm('page.navigation.longTermMemory') }}
mdi-tools
{{ tm('page.navigation.other') }}
diff --git a/dashboard/src/views/ConfigPage.vue b/dashboard/src/views/ConfigPage.vue
index ef022d9d7..133c5eebf 100644
--- a/dashboard/src/views/ConfigPage.vue
+++ b/dashboard/src/views/ConfigPage.vue
@@ -8,15 +8,9 @@
-
-
-
-
-
@@ -33,52 +27,26 @@
-
+
-
-
-
-
-
- {{ metadata[key]['name'] }}
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
+
@@ -118,7 +86,7 @@
- AstrBot 支持针对不同消息平台实例分别设置配置文件。默认会使用 `default` 配置。
+ AstrBot 支持针对不同机器人分别设置配置文件。默认会使用 `default` 配置。
新建配置文件
@@ -128,8 +96,6 @@
- 当前应用于: {{ formatUmop(config.umop) }}
-
{{ isEditingConfig ? '编辑配置文件' : '新建配置文件' }}
-
-
名称
- 应用于
-
-
-
-
- 指定消息平台...
-
-
-
-
-
-
-
-
-
-
-
-
-
UMO 格式: [platform_id]:[message_type]:[session_id]。通配符 * 或留空表示全部。使用 /sid 查看某个聊天的 UMO。
-
-
-
-
- 可视化
-
-
- 手动编辑
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 添加规则
-
-
-
-
-
-
-
-
-
-
-
-
- 预览:
- 未配置任何规则
-
-
- {{ rule }}
-
-
- 这些规则对应的会话将使用此配置文件。
-
-
-
-
-
-
-
取消
+ :disabled="!configFormData.name">
{{ isEditingConfig ? '更新' : '创建' }}
@@ -308,7 +140,7 @@
diff --git a/dashboard/src/views/ToolUsePage.vue b/dashboard/src/views/ToolUsePage.vue
index 4cb2183da..db8fee905 100644
--- a/dashboard/src/views/ToolUsePage.vue
+++ b/dashboard/src/views/ToolUsePage.vue
@@ -141,6 +141,8 @@
+ *{{ tm('dialogs.addServer.tips.timeoutConfig') }}
+
+
+
+ 建议您更换使用新版知识库功能。
+
+
@@ -105,9 +110,9 @@
{{ tm('createDialog.cancel')
- }}
+ }}
{{ tm('createDialog.create')
- }}
+ }}
@@ -132,7 +137,7 @@
{{ tm('emojiPicker.close')
- }}
+ }}
@@ -159,8 +164,8 @@
mdi-sort-variant
- 重排序模型: {{ rerankProviderConfigs.
- find(provider => provider.id === currentKB.rerank_provider_id)?.rerank_model || '未设置' }}
+ 重排序模型: {{rerankProviderConfigs.
+ find(provider => provider.id === currentKB.rerank_provider_id)?.rerank_model || '未设置'}}
💡 使用方式: 在聊天页中输入 "/kb use {{ currentKB.collection_name }}"
@@ -411,7 +416,7 @@
{{
tm('deleteDialog.cancel')
- }}
+ }}
{{
tm('deleteDialog.delete') }}
@@ -603,6 +608,7 @@ export default {
.then(response => {
if (response.data.status !== 'ok' || response.data.data.length === 0) {
this.showSnackbar(this.tm('messages.pluginNotAvailable'), 'error');
+ this.installed = false;
return
}
if (!response.data.data[0].activated) {
diff --git a/dashboard/src/views/knowledge-base/DocumentDetail.vue b/dashboard/src/views/knowledge-base/DocumentDetail.vue
new file mode 100644
index 000000000..fb64e628c
--- /dev/null
+++ b/dashboard/src/views/knowledge-base/DocumentDetail.vue
@@ -0,0 +1,512 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('info.title') }}
+
+
+
+
+
+
mdi-label
+
+
{{ t('info.name') }}
+
{{ document.doc_name }}
+
+
+
+
+
+
+ {{ getFileIcon(document.file_type) }}
+
+
+
{{ t('info.type') }}
+
{{ document.file_type || '-' }}
+
+
+
+
+
+
mdi-file-chart
+
+
{{ t('info.size') }}
+
{{ formatFileSize(document.file_size) }}
+
+
+
+
+
+
mdi-text-box
+
+
{{ t('info.chunkCount') }}
+
{{ document.chunk_count || 0 }}
+
+
+
+
+
+
mdi-calendar
+
+
{{ t('info.createdAt') }}
+
{{ formatDate(document.created_at) }}
+
+
+
+
+
+
+
+
+
+
+ {{ t('chunks.title') }}
+
+ {{ totalChunks }} {{ t('chunks.title') }}
+
+
+
+
+
+
+
+
+
+
+
+ #{{ item.chunk_index + 1 }}
+
+
+
+
+
+ {{ item.content }}
+
+
+
+
+
+ {{ item.char_count }} 字符
+
+
+
+
+
+
+
+
+
+
+
+
mdi-text-box-outline
+
{{ t('chunks.empty') }}
+
+
+
+
+
+
+
+
+ {{ t('chunks.showing') }} {{ (page - 1) * pageSize + 1 }} - {{ Math.min(page * pageSize, totalChunks) }} / {{ totalChunks }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('view.title') }}
+
+
+
+
+
+
+
+
+ mdi-pound
+
+ {{ t('view.index') }}
+ #{{ (selectedChunk?.chunk_index || 0) + 1 }}
+
+
+
+
+ mdi-text
+
+ {{ t('view.charCount') }}
+ {{ selectedChunk?.char_count || 0 }} 字符
+
+
+
+
+ mdi-key
+
+ {{ t('view.vecDocId') }}
+ {{ selectedChunk?.chunk_id || '-' }}
+
+
+
+
+
+ {{ t('view.content') }}
+
+ {{ selectedChunk?.content }}
+
+
+
+
+
+
+ {{ t('view.close') }}
+
+
+
+
+
+
+
+ {{ snackbar.text }}
+
+
+
+
+
+
+
diff --git a/dashboard/src/views/knowledge-base/KBDetail.vue b/dashboard/src/views/knowledge-base/KBDetail.vue
new file mode 100644
index 000000000..793ad16b6
--- /dev/null
+++ b/dashboard/src/views/knowledge-base/KBDetail.vue
@@ -0,0 +1,351 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mdi-information-outline
+ {{ t('tabs.overview') }}
+
+
+ mdi-file-document-multiple
+ {{ t('tabs.documents') }}
+ {{ kb.doc_count || 0 }}
+
+
+ mdi-magnify
+ {{ t('tabs.retrieval') }}
+
+
+ mdi-cog
+ {{ t('tabs.settings') }}
+
+
+
+
+
+
+
+
+
+
+ {{ t('overview.title') }}
+
+
+
+
+
+ mdi-label
+
+ {{ t('overview.name') }}
+ {{ kb.kb_name }}
+
+
+
+
+ mdi-text
+
+ {{ t('overview.description') }}
+ {{ kb.description }}
+
+
+
+
+ mdi-emoticon
+
+ {{ t('overview.emoji') }}
+ {{ kb.emoji || '📚' }}
+
+
+
+
+ mdi-calendar-plus
+
+ {{ t('overview.createdAt') }}
+ {{ formatDate(kb.created_at) }}
+
+
+
+
+ mdi-calendar-edit
+
+ {{ t('overview.updatedAt') }}
+ {{ formatDate(kb.updated_at) }}
+
+
+
+
+
+
+
+
+ {{ t('overview.stats') }}
+
+
+
+
+
+
mdi-file-document
+
{{ kb.doc_count || 0 }}
+
{{ t('overview.docCount') }}
+
+
+
+
+
mdi-text-box
+
{{ kb.chunk_count || 0 }}
+
{{ t('overview.chunkCount') }}
+
+
+
+
+
+
+
+ {{ t('overview.embeddingModel') }}
+
+
+
+
+
+ mdi-vector-point
+
+ {{ t('overview.embeddingModel') }}
+ {{ kb.embedding_provider_id || t('overview.notSet') }}
+
+
+
+
+ mdi-sort-ascending
+
+ {{ t('overview.rerankModel') }}
+ {{ kb.rerank_provider_id || t('overview.notSet') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ snackbar.text }}
+
+
+
+
+
+
+
diff --git a/dashboard/src/views/knowledge-base/KBList.vue b/dashboard/src/views/knowledge-base/KBList.vue
new file mode 100644
index 000000000..509aaa557
--- /dev/null
+++ b/dashboard/src/views/knowledge-base/KBList.vue
@@ -0,0 +1,615 @@
+
+
+
+
+
+
+
+
+ {{ t('list.create') }}
+
+
+ {{ t('list.refresh') }}
+
+
+
+
+
+
+
{{ t('list.loading') }}
+
+
+
+
+
+
{{ kb.emoji || '📚' }}
+
{{ kb.kb_name }}
+
{{ kb.description || '暂无描述' }}
+
+
+
+ mdi-file-document
+ {{ kb.doc_count || 0 }} {{ t('list.documents') }}
+
+
+ mdi-text-box
+ {{ kb.chunk_count || 0 }} {{ t('list.chunks') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ mdi-book-open-variant
+
{{ t('list.empty') }}
+
+ {{ t('list.create') }}
+
+
+
+
+
+
+
+ {{ editingKB ? t('edit.title') : t('create.title') }}
+
+
+
+
+
+
+
+
+
+
+ {{ formData.emoji }}
+
+
{{ t('create.emojiLabel') }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('create.providerInfo', {
+ id: item.raw.id,
+ dimensions: item.raw.embedding_dimensions || 'N/A'
+ }) }}
+
+
+
+
+
+
+
+
+
+ {{ t('create.rerankProviderInfo', { id: item.raw.id }) }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('create.cancel') }}
+
+
+ {{ editingKB ? t('edit.submit') : t('create.submit') }}
+
+
+
+
+
+
+
+
+ {{ t('emoji.title') }}
+
+
+
+
{{ t(`emoji.categories.${category.key}`) }}
+
+
+
+
+
+
+
+ {{ t('emoji.close') }}
+
+
+
+
+
+
+
+
+ {{ t('delete.title') }}
+
+
+ {{ t('delete.confirmText', { name: deleteTarget?.kb_name || '' }) }}
+
+ {{ t('delete.warning') }}
+
+
+
+
+
+
+ {{ t('delete.cancel') }}
+
+
+ {{ t('delete.confirm') }}
+
+
+
+
+
+
+
+ {{ snackbar.text }}
+
+
+
+
+
+
+
+
+
+
diff --git a/dashboard/src/views/knowledge-base/components/DocumentsTab.vue b/dashboard/src/views/knowledge-base/components/DocumentsTab.vue
new file mode 100644
index 000000000..9e146e86a
--- /dev/null
+++ b/dashboard/src/views/knowledge-base/components/DocumentsTab.vue
@@ -0,0 +1,660 @@
+
+
+
+
+
+ {{ t('documents.upload') }}
+
+
+
+
+
+
+
+
+
+
+ {{ getFileIcon(item.file_type) }}
+
+
+
{{ item.doc_name }}
+
+
+
+ {{ getStageText(item.uploadProgress?.stage || 'waiting') }}
+
+ ({{ item.uploadProgress.current }} / {{ item.uploadProgress.total }})
+
+
+
+
+
+
+
+
+
+ {{ formatFileSize(item.file_size) }}
+
+
+
+ {{ formatDate(item.created_at) }}
+
+
+
+
+
+
+
+
+
+
mdi-file-document-outline
+
{{ t('documents.empty') }}
+
+
+
+
+
+
+
+
+
+ {{ t('upload.title') }}
+
+
+
+
+
+
+
+
+
+
mdi-cloud-upload
+
{{ t('upload.dropzone') }}
+
{{ t('upload.supportedFormats') }}.txt, .md, .pdf, .docx,
+ .xls, .xlsx
+
{{ t('upload.maxSize') }}
+
最多可上传 10 个文件
+
+
+
+
+
+ 已选择 {{ selectedFiles.length }} 个文件
+ 清空
+
+
+
+
+
+
{{ getFileIcon(file.name) }}
+
+
{{ file.name }}
+
{{ formatFileSize(file.size) }}
+
+
+
+
+
+
+
+
+
+
+
+
{{ t('upload.chunkSettings') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ t('upload.batchSettings') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('upload.cancel') }}
+
+
+ {{ t('upload.submit') }}
+
+
+
+
+
+
+
+
+ {{ t('documents.delete') }}
+
+
+ {{ t('documents.deleteConfirm', { name: deleteTarget?.doc_name || '' }) }}
+
+ {{ t('documents.deleteWarning') }}
+
+
+
+
+
+ 取消
+
+ 删除
+
+
+
+
+
+
+
+ {{ snackbar.text }}
+
+
+
+
+
+
+
diff --git a/dashboard/src/views/knowledge-base/components/RetrievalTab.vue b/dashboard/src/views/knowledge-base/components/RetrievalTab.vue
new file mode 100644
index 000000000..b1ed3ca1e
--- /dev/null
+++ b/dashboard/src/views/knowledge-base/components/RetrievalTab.vue
@@ -0,0 +1,248 @@
+
+
+
+ {{ t('retrieval.title') }}
+
+ {{ t('retrieval.subtitle') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('retrieval.settings') }}
+
+
+
+
+
+
+ mdi-bug
+ Debug (t-SNE)
+
+
+
+
+
+
+
+
+
+ {{ loading ? t('retrieval.searching') : t('retrieval.search') }}
+
+
+
+
+
+
+
+
+
{{ t('retrieval.results') }}
+
+ {{ results.length }} {{ t('retrieval.results') }}
+
+
+
+
+
+
+
+
+ #{{ index + 1 }}
+
+
+ {{ t('retrieval.chunk', { index: result.chunk_index }) }}
+
+
+
+ mdi-file-document
+ {{ result.doc_name }}
+
+
+ mdi-text
+ {{ t('retrieval.charCount', { count: result.char_count }) }}
+
+
+
+
+ {{ t('retrieval.score') }}: {{ result.score.toFixed(4) }}
+
+
+
+
+
+
+
+ {{ result.content }}
+
+
+
+
+
+
+
+
mdi-text-box-search-outline
+
{{ t('retrieval.noResults') }}
+
{{ t('retrieval.tryDifferentQuery') }}
+
+
+
+
+
+
+
+ {{ snackbar.text }}
+
+
+
+
+
+
+
diff --git a/dashboard/src/views/knowledge-base/components/SettingsTab.vue b/dashboard/src/views/knowledge-base/components/SettingsTab.vue
new file mode 100644
index 000000000..fb3911aaa
--- /dev/null
+++ b/dashboard/src/views/knowledge-base/components/SettingsTab.vue
@@ -0,0 +1,316 @@
+
+
+
+ {{ t('settings.title') }}
+
+
+
+
+
+ {{ t('settings.basic') }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('settings.retrieval') }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('settings.embeddingProvider') }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('settings.tips') }}
+
+
+
+ 注意: 修改嵌入模型会导致现有的向量数据失效,建议重新上传文档。不同的嵌入模型生成的向量不兼容,可能导致检索结果不准确。
+
+
+
+
+
+
+
+
+
+ {{ t('settings.save') }}
+
+
+
+
+
+
+ {{ snackbar.text }}
+
+
+
+
+
+
+ mdi-alert
+ 确认修改嵌入模型
+
+
+
+ 警告: 修改嵌入模型将导致以下影响:
+
+
+ 现有的向量数据将失效
+ 检索功能可能无法正常工作
+ 建议删除现有文档后重新上传
+ 不同嵌入模型生成的向量不兼容
+
+
+ 您确定要将嵌入模型从 {{ originalEmbeddingProvider }} 修改为 {{ pendingEmbeddingProvider }} 吗?
+
+
+
+
+
+ 取消
+
+
+ 确认修改
+
+
+
+
+
+
+
+
+
+
diff --git a/dashboard/src/views/knowledge-base/index.vue b/dashboard/src/views/knowledge-base/index.vue
new file mode 100644
index 000000000..121f3a3b8
--- /dev/null
+++ b/dashboard/src/views/knowledge-base/index.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dashboard/vite.config.ts b/dashboard/vite.config.ts
index 673187ded..b52ad65b6 100644
--- a/dashboard/vite.config.ts
+++ b/dashboard/vite.config.ts
@@ -39,7 +39,7 @@ export default defineConfig({
port: 3000,
proxy: {
'/api': {
- target: 'http://localhost:6185/',
+ target: 'http://127.0.0.1:6185/',
changeOrigin: true,
}
}
diff --git a/main.py b/main.py
index b3c8e5893..be0d4f307 100644
--- a/main.py
+++ b/main.py
@@ -63,7 +63,7 @@ async def check_dashboard_files(webui_dir: str | None = None):
return data_dist_path
logger.info(
- "开始下载管理面板文件...高峰期(晚上)可能导致较慢的速度。如多次下载失败,请前往 https://github.com/Soulter/AstrBot/releases/latest 下载 dist.zip,并将其中的 dist 文件夹解压至 data 目录下。"
+ "开始下载管理面板文件...高峰期(晚上)可能导致较慢的速度。如多次下载失败,请前往 https://github.com/AstrBotDevs/AstrBot/releases/latest 下载 dist.zip,并将其中的 dist 文件夹解压至 data 目录下。"
)
try:
diff --git a/packages/astrbot/commands/alter_cmd.py b/packages/astrbot/commands/alter_cmd.py
index bad5072cf..18d6c1305 100644
--- a/packages/astrbot/commands/alter_cmd.py
+++ b/packages/astrbot/commands/alter_cmd.py
@@ -6,26 +6,7 @@ from astrbot.core.star.star import star_map
from astrbot.core.star.filter.command import CommandFilter
from astrbot.core.star.filter.command_group import CommandGroupFilter
from astrbot.core.star.filter.permission import PermissionTypeFilter
-from enum import Enum
-
-
-class RstScene(Enum):
- GROUP_UNIQUE_ON = ("group_unique_on", "群聊+会话隔离开启")
- GROUP_UNIQUE_OFF = ("group_unique_off", "群聊+会话隔离关闭")
- PRIVATE = ("private", "私聊")
-
- @property
- def key(self) -> str:
- return self.value[0]
-
- @property
- def name(self) -> str:
- return self.value[1]
-
- @classmethod
- def from_index(cls, index: int) -> "RstScene":
- mapping = {1: cls.GROUP_UNIQUE_ON, 2: cls.GROUP_UNIQUE_OFF, 3: cls.PRIVATE}
- return mapping[index]
+from .utils.rst_scene import RstScene
class AlterCmdCommands(CommandParserMixin):
@@ -58,8 +39,9 @@ class AlterCmdCommands(CommandParserMixin):
)
return
- cmd_name = " ".join(token.tokens[1:-1])
- cmd_type = token.get(-1)
+ # 兼容 reset scene 的专门配置
+ cmd_name = token.get(1)
+ cmd_type = token.get(2)
if cmd_name == "reset" and cmd_type == "config":
from astrbot.api import sp
@@ -123,6 +105,8 @@ class AlterCmdCommands(CommandParserMixin):
return
# 查找指令
+ cmd_name = " ".join(token.tokens[1:-1])
+ cmd_type = token.get(-1)
found_command = None
cmd_group = False
for handler in star_handlers_registry:
diff --git a/packages/astrbot/commands/conversation.py b/packages/astrbot/commands/conversation.py
index 2d5317644..1a8ce746b 100644
--- a/packages/astrbot/commands/conversation.py
+++ b/packages/astrbot/commands/conversation.py
@@ -6,40 +6,27 @@ from astrbot.core.platform.message_type import MessageType
from astrbot.core.provider.sources.dify_source import ProviderDify
from astrbot.core.provider.sources.coze_source import ProviderCoze
from astrbot.api import sp, logger
+from ..long_term_memory import LongTermMemory
+from .utils.rst_scene import RstScene
from typing import Union
-from enum import Enum
-
-
-class RstScene(Enum):
- GROUP_UNIQUE_ON = ("group_unique_on", "群聊+会话隔离开启")
- GROUP_UNIQUE_OFF = ("group_unique_off", "群聊+会话隔离关闭")
- PRIVATE = ("private", "私聊")
-
- @property
- def key(self) -> str:
- return self.value[0]
-
- @property
- def name(self) -> str:
- return self.value[1]
-
- @classmethod
- def from_index(cls, index: int) -> "RstScene":
- mapping = {1: cls.GROUP_UNIQUE_ON, 2: cls.GROUP_UNIQUE_OFF, 3: cls.PRIVATE}
- return mapping[index]
-
- @classmethod
- def get_scene(cls, is_group: bool, is_unique_session: bool) -> "RstScene":
- if is_group:
- return cls.GROUP_UNIQUE_ON if is_unique_session else cls.GROUP_UNIQUE_OFF
- return cls.PRIVATE
class ConversationCommands:
- def __init__(self, context: star.Context, ltm=None):
+ def __init__(self, context: star.Context, ltm: LongTermMemory | None = None):
self.context = context
self.ltm = ltm
+ async def _get_current_persona_id(self, session_id):
+ curr = await self.context.conversation_manager.get_curr_conversation_id(
+ session_id
+ )
+ if not curr:
+ return None
+ conv = await self.context.conversation_manager.get_conversation(
+ session_id, curr
+ )
+ return conv.persona_id
+
def ltm_enabled(self, event: AstrMessageEvent):
if not self.ltm:
return False
@@ -254,8 +241,9 @@ class ConversationCommands:
)
return
+ cpersona = await self._get_current_persona_id(message.unified_msg_origin)
cid = await self.context.conversation_manager.new_conversation(
- message.unified_msg_origin, message.get_platform_id()
+ message.unified_msg_origin, message.get_platform_id(), persona_id=cpersona
)
# 长期记忆
@@ -289,8 +277,10 @@ class ConversationCommands:
session_id=sid,
)
)
+
+ cpersona = await self._get_current_persona_id(session)
cid = await self.context.conversation_manager.new_conversation(
- session, message.get_platform_id()
+ session, message.get_platform_id(), persona_id=cpersona
)
message.set_result(
MessageEventResult().message(
@@ -433,8 +423,9 @@ class ConversationCommands:
await self.context.conversation_manager.delete_conversation(
message.unified_msg_origin, session_curr_cid
)
- message.set_result(
- MessageEventResult().message(
- "删除当前对话成功。不再处于对话状态,使用 /switch 序号 切换到其他对话或 /new 创建。"
- )
- )
+
+ ret = "删除当前对话成功。不再处于对话状态,使用 /switch 序号 切换到其他对话或 /new 创建。"
+ if self.ltm and self.ltm_enabled(message):
+ cnt = await self.ltm.remove_session(event=message)
+ ret += f"\n聊天增强: 已清除 {cnt} 条聊天记录。"
+ message.set_result(MessageEventResult().message(ret))
diff --git a/packages/astrbot/commands/provider.py b/packages/astrbot/commands/provider.py
index 3a184475b..85754d0b3 100644
--- a/packages/astrbot/commands/provider.py
+++ b/packages/astrbot/commands/provider.py
@@ -129,7 +129,7 @@ class ProviderCommands:
)
return
i = 1
- ret = "下面列出了此服务提供商可用模型:"
+ ret = "下面列出了此模型提供商可用模型:"
for model in models:
ret += f"\n{i}. {model}"
i += 1
@@ -159,7 +159,11 @@ class ProviderCommands:
message.set_result(
MessageEventResult().message("切换模型未知错误: " + str(e))
)
- message.set_result(MessageEventResult().message("切换模型成功。"))
+ message.set_result(
+ MessageEventResult().message(
+ f"切换模型成功。当前提供商: [{prov.meta().id}] 当前模型: [{prov.get_model()}]"
+ )
+ )
else:
prov.set_model(idx_or_name)
message.set_result(
diff --git a/packages/astrbot/commands/sid.py b/packages/astrbot/commands/sid.py
index 165683e43..101b22134 100644
--- a/packages/astrbot/commands/sid.py
+++ b/packages/astrbot/commands/sid.py
@@ -11,19 +11,26 @@ class SIDCommand:
self.context = context
async def sid(self, event: AstrMessageEvent):
- """获取会话 ID 和 管理员 ID"""
+ """获取消息来源信息"""
sid = event.unified_msg_origin
user_id = str(event.get_sender_id())
- ret = f"""SID: {sid} 此 ID 可用于设置会话白名单。
-/wl
添加白名单, /dwl 删除白名单。
-
-UID: {user_id} 此 ID 可用于设置管理员。
-/op 授权管理员, /deop 取消管理员。"""
+ umo_platform = event.session.platform_id
+ umo_msg_type = event.session.message_type.value
+ umo_session_id = event.session.session_id
+ ret = (
+ f"UMO: 「{sid}」 此值可用于设置白名单。\n"
+ f"UID: 「{user_id}」 此值可用于设置管理员。\n"
+ f"消息会话来源信息:\n"
+ f" 机器人 ID: 「{umo_platform}」\n"
+ f" 消息类型: 「{umo_msg_type}」\n"
+ f" 会话 ID: 「{umo_session_id}」\n"
+ f"消息来源可用于配置机器人的配置文件路由。"
+ )
if (
self.context.get_config()["platform_settings"]["unique_session"]
and event.get_group_id()
):
- ret += f"\n\n当前处于独立会话模式, 此群 ID: {event.get_group_id()}, 也可将此 ID 加入白名单来放行整个群聊。"
+ ret += f"\n\n当前处于独立会话模式, 此群 ID: 「{event.get_group_id()}」, 也可将此 ID 加入白名单来放行整个群聊。"
event.set_result(MessageEventResult().message(ret).use_t2i(False))
diff --git a/packages/astrbot/commands/utils/rst_scene.py b/packages/astrbot/commands/utils/rst_scene.py
new file mode 100644
index 000000000..d93007404
--- /dev/null
+++ b/packages/astrbot/commands/utils/rst_scene.py
@@ -0,0 +1,26 @@
+from enum import Enum
+
+
+class RstScene(Enum):
+ GROUP_UNIQUE_ON = ("group_unique_on", "群聊+会话隔离开启")
+ GROUP_UNIQUE_OFF = ("group_unique_off", "群聊+会话隔离关闭")
+ PRIVATE = ("private", "私聊")
+
+ @property
+ def key(self) -> str:
+ return self.value[0]
+
+ @property
+ def name(self) -> str:
+ return self.value[1]
+
+ @classmethod
+ def from_index(cls, index: int) -> "RstScene":
+ mapping = {1: cls.GROUP_UNIQUE_ON, 2: cls.GROUP_UNIQUE_OFF, 3: cls.PRIVATE}
+ return mapping[index]
+
+ @classmethod
+ def get_scene(cls, is_group: bool, is_unique_session: bool) -> "RstScene":
+ if is_group:
+ return cls.GROUP_UNIQUE_ON if is_unique_session else cls.GROUP_UNIQUE_OFF
+ return cls.PRIVATE
diff --git a/packages/astrbot/long_term_memory.py b/packages/astrbot/long_term_memory.py
index f1d13fa69..dc2484860 100644
--- a/packages/astrbot/long_term_memory.py
+++ b/packages/astrbot/long_term_memory.py
@@ -4,7 +4,7 @@ import random
import astrbot.api.star as star
from astrbot.api.event import AstrMessageEvent
from astrbot.api.platform import MessageType
-from astrbot.api.provider import ProviderRequest
+from astrbot.api.provider import ProviderRequest, Provider
from astrbot.api.message_components import Plain, Image
from astrbot import logger
from collections import defaultdict
@@ -32,6 +32,7 @@ class LongTermMemory:
image_caption = (
True
if cfg["provider_settings"]["default_image_caption_provider_id"]
+ and cfg["provider_ltm_settings"]["image_caption"]
else False
)
image_caption_prompt = cfg["provider_settings"]["image_caption_prompt"]
@@ -73,6 +74,8 @@ class LongTermMemory:
provider = self.context.get_provider_by_id(image_caption_provider_id)
if not provider:
raise Exception(f"没有找到 ID 为 {image_caption_provider_id} 的提供商")
+ if not isinstance(provider, Provider):
+ raise Exception(f"提供商类型错误({type(provider)}),无法获取图片描述")
response = await provider.text_chat(
prompt=image_caption_prompt,
session_id=uuid.uuid4().hex,
@@ -122,8 +125,11 @@ class LongTermMemory:
elif isinstance(comp, Image):
if cfg["image_caption"]:
try:
+ url = comp.url if comp.url else comp.file
+ if not url:
+ raise Exception("图片 URL 为空")
caption = await self.get_image_caption(
- comp.url if comp.url else comp.file,
+ url,
cfg["image_caption_provider_id"],
cfg["image_caption_prompt"],
)
diff --git a/packages/astrbot/main.py b/packages/astrbot/main.py
index 272864633..6fd0b0e5a 100644
--- a/packages/astrbot/main.py
+++ b/packages/astrbot/main.py
@@ -41,7 +41,7 @@ class Main(star.Star):
self.tool_c = ToolCommands(self.context)
self.plugin_c = PluginCommands(self.context)
self.admin_c = AdminCommands(self.context)
- self.conversation_c = ConversationCommands(self.context)
+ self.conversation_c = ConversationCommands(self.context, self.ltm)
self.provider_c = ProviderCommands(self.context)
self.persona_c = PersonaCommands(self.context)
self.alter_cmd_c = AlterCmdCommands(self.context)
diff --git a/packages/astrbot/process_llm_request.py b/packages/astrbot/process_llm_request.py
index 2b785fd4d..8f17dd0dc 100644
--- a/packages/astrbot/process_llm_request.py
+++ b/packages/astrbot/process_llm_request.py
@@ -1,3 +1,4 @@
+import copy
import astrbot.api.star as star
import builtins
import datetime
@@ -41,7 +42,7 @@ class ProcessLLMRequest:
if persona:
if prompt := persona["prompt"]:
req.system_prompt += prompt
- if begin_dialogs := persona["_begin_dialogs_processed"]:
+ if begin_dialogs := copy.deepcopy(persona["_begin_dialogs_processed"]):
req.contexts[:0] = begin_dialogs
# tools select
diff --git a/packages/thinking_filter/main.py b/packages/thinking_filter/main.py
index a10138134..3d2729669 100644
--- a/packages/thinking_filter/main.py
+++ b/packages/thinking_filter/main.py
@@ -1,9 +1,19 @@
import re
+import json
+import logging
+from typing import Any, Tuple
+
from astrbot.api.event import filter, AstrMessageEvent
from astrbot.api.star import Context, Star
from astrbot.api.provider import LLMResponse
from openai.types.chat.chat_completion import ChatCompletion
+try:
+ # 谨慎引入,避免在未安装 google-genai 的环境下报错
+ from google.genai.types import GenerateContentResponse
+except Exception: # pragma: no cover - 兼容无此依赖的运行环境
+ GenerateContentResponse = None # type: ignore
+
class R1Filter(Star):
def __init__(self, context: Context):
@@ -14,7 +24,36 @@ class R1Filter(Star):
cfg = self.context.get_config(umo=event.unified_msg_origin).get(
"provider_settings", {}
)
- if cfg.get("display_reasoning_text", False):
+ show_reasoning = cfg.get("display_reasoning_text", False)
+
+ # --- Gemini: 过滤/展示 thought:true 片段 ---
+ # Gemini 可能在 parts 中注入 {"thought": true, "text": "..."}
+ # 官方 SDK 默认不会返回此字段。
+ if GenerateContentResponse is not None and isinstance(
+ response.raw_completion, GenerateContentResponse
+ ):
+ thought_text, answer_text = self._extract_gemini_texts(
+ response.raw_completion
+ )
+
+ if thought_text or answer_text:
+ # 有明确的思考/正文分离信号,则按配置处理
+ if show_reasoning:
+ merged = (
+ (f"🤔思考:{thought_text}\n\n" if thought_text else "")
+ + (answer_text or "")
+ ).strip()
+ if merged:
+ response.completion_text = merged
+ return
+ else:
+ # 默认隐藏思考内容,仅保留正文
+ if answer_text:
+ response.completion_text = answer_text
+ return
+
+ # --- 非 Gemini 或无明确 thought:true 情况 ---
+ if show_reasoning:
# 显示推理内容的处理逻辑
if (
response
@@ -60,3 +99,105 @@ class R1Filter(Star):
)
response.completion_text = completion_text
+
+ # ------------------------
+ # helpers
+ # ------------------------
+ def _get_part_dict(self, p: Any) -> dict:
+ """优先使用 SDK 标准序列化方法获取字典,失败则逐级回退。
+
+ 顺序: model_dump → model_dump_json → json → to_dict → dict → __dict__。
+ """
+ for getter in ("model_dump", "model_dump_json", "json", "to_dict", "dict"):
+ fn = getattr(p, getter, None)
+ if callable(fn):
+ try:
+ result = fn()
+ if isinstance(result, (str, bytes)):
+ try:
+ if isinstance(result, bytes):
+ result = result.decode("utf-8", "ignore")
+ return json.loads(result) or {}
+ except json.JSONDecodeError:
+ continue
+ if isinstance(result, dict):
+ return result
+ except (AttributeError, TypeError):
+ continue
+ except Exception as e:
+ logging.exception(
+ f"Unexpected error when calling {getter} on {type(p).__name__}: {e}"
+ )
+ continue
+ try:
+ d = getattr(p, "__dict__", None)
+ if isinstance(d, dict):
+ return d
+ except (AttributeError, TypeError):
+ pass
+ except Exception as e:
+ logging.exception(
+ f"Unexpected error when accessing __dict__ on {type(p).__name__}: {e}"
+ )
+ return {}
+
+ def _is_thought_part(self, p: Any) -> bool:
+ """判断是否为思考片段。
+
+ 规则:
+ 1) 直接 thought 属性
+ 2) 字典字段 thought 或 metadata.thought
+ 3) data/raw/extra/_raw 中嵌入的 JSON 串包含 thought: true
+ """
+ try:
+ if getattr(p, "thought", False):
+ return True
+ except Exception:
+ # best-effort
+ pass
+
+ d = self._get_part_dict(p)
+ if d.get("thought") is True:
+ return True
+ meta = d.get("metadata")
+ if isinstance(meta, dict) and meta.get("thought") is True:
+ return True
+ for k in ("data", "raw", "extra", "_raw"):
+ v = d.get(k)
+ if isinstance(v, (str, bytes)):
+ try:
+ if isinstance(v, bytes):
+ v = v.decode("utf-8", "ignore")
+ parsed = json.loads(v)
+ if isinstance(parsed, dict) and parsed.get("thought") is True:
+ return True
+ except json.JSONDecodeError:
+ continue
+ return False
+
+ def _extract_gemini_texts(self, resp: Any) -> Tuple[str, str]:
+ """从 GenerateContentResponse 中提取 (思考文本, 正文文本)。"""
+ try:
+ cand0 = next(iter(getattr(resp, "candidates", []) or []), None)
+ if not cand0:
+ return "", ""
+ content = getattr(cand0, "content", None)
+ parts = getattr(content, "parts", None) or []
+ except (AttributeError, TypeError, ValueError):
+ return "", ""
+
+ thought_buf: list[str] = []
+ answer_buf: list[str] = []
+ for p in parts:
+ txt = getattr(p, "text", None)
+ if txt is None:
+ continue
+ txt_str = str(txt).strip()
+ if not txt_str:
+ continue
+ if self._is_thought_part(p):
+ thought_buf.append(txt_str)
+ else:
+ answer_buf.append(txt_str)
+
+ return "\n".join(thought_buf).strip(), "\n".join(answer_buf).strip()
diff --git a/packages/web_searcher/engines/google.py b/packages/web_searcher/engines/google.py
deleted file mode 100644
index ac66f7d72..000000000
--- a/packages/web_searcher/engines/google.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import os
-from googlesearch import search
-
-from . import SearchEngine, SearchResult
-
-from typing import List
-
-
-class Google(SearchEngine):
- def __init__(self) -> None:
- super().__init__()
- self.proxy = os.environ.get("https_proxy")
-
- async def search(self, query: str, num_results: int) -> List[SearchResult]:
- results = []
- try:
- ls = search(
- query,
- advanced=True,
- num_results=num_results,
- timeout=3,
- proxy=self.proxy,
- )
- for i in ls:
- results.append(
- SearchResult(title=i.title, url=i.url, snippet=i.description)
- )
- except Exception as e:
- raise e
- return results
diff --git a/packages/web_searcher/main.py b/packages/web_searcher/main.py
index b799e5a17..635f3ebb7 100644
--- a/packages/web_searcher/main.py
+++ b/packages/web_searcher/main.py
@@ -10,7 +10,6 @@ from astrbot.core.provider.func_tool_manager import FunctionToolManager
from .engines import SearchResult
from .engines.bing import Bing
from .engines.sogo import Sogo
-from .engines.google import Google
from readability import Document
from bs4 import BeautifulSoup
from .engines import HEADERS, USER_AGENTS
@@ -46,7 +45,7 @@ class Main(star.Star):
self.bing_search = Bing()
self.sogo_search = Sogo()
- self.google = Google()
+ self.baidu_initialized = False
async def _tidy_text(self, text: str) -> str:
"""清理文本,去除空格、换行符等"""
@@ -90,15 +89,9 @@ class Main(star.Star):
) -> list[SearchResult]:
results = []
try:
- results = await self.google.search(query, num_results)
+ results = await self.bing_search.search(query, num_results)
except Exception as e:
- logger.error(f"google search error: {e}, try the next one...")
- if len(results) == 0:
- logger.debug("search google failed")
- try:
- results = await self.bing_search.search(query, num_results)
- except Exception as e:
- logger.error(f"bing search error: {e}, try the next one...")
+ logger.error(f"bing search error: {e}, try the next one...")
if len(results) == 0:
logger.debug("search bing failed")
try:
@@ -220,6 +213,30 @@ class Main(star.Star):
return ret
+ async def ensure_baidu_ai_search_mcp(self, umo: str | None = None):
+ if self.baidu_initialized:
+ return
+ cfg = self.context.get_config(umo=umo)
+ key = cfg.get("provider_settings", {}).get(
+ "websearch_baidu_app_builder_key", ""
+ )
+ if not key:
+ raise ValueError(
+ "Error: Baidu AI Search API key is not configured in AstrBot."
+ )
+ func_tool_mgr = self.context.get_llm_tool_manager()
+ await func_tool_mgr.enable_mcp_server(
+ "baidu_ai_search",
+ config={
+ "transport": "sse",
+ "url": f"http://appbuilder.baidu.com/v2/ai_search/mcp/sse?api_key={key}",
+ "headers": {},
+ "timeout": 30,
+ },
+ )
+ self.baidu_initialized = True
+ logger.info("Successfully initialized Baidu AI Search MCP server.")
+
@llm_tool(name="fetch_url")
async def fetch_website_content(self, event: AstrMessageEvent, url: str) -> str:
"""fetch the content of a website with the given web url
@@ -366,6 +383,7 @@ class Main(star.Star):
tool_set.add_tool(fetch_url_t)
tool_set.remove_tool("web_search_tavily")
tool_set.remove_tool("tavily_extract_web_page")
+ tool_set.remove_tool("AIsearch")
elif provider == "tavily":
web_search_tavily = func_tool_mgr.get_func("web_search_tavily")
tavily_extract_web_page = func_tool_mgr.get_func("tavily_extract_web_page")
@@ -375,3 +393,17 @@ class Main(star.Star):
tool_set.add_tool(tavily_extract_web_page)
tool_set.remove_tool("web_search")
tool_set.remove_tool("fetch_url")
+ tool_set.remove_tool("AIsearch")
+ elif provider == "baidu_ai_search":
+ try:
+ await self.ensure_baidu_ai_search_mcp(event.unified_msg_origin)
+ aisearch_tool = func_tool_mgr.get_func("AIsearch")
+ if not aisearch_tool:
+ raise ValueError("Cannot get Baidu AI Search MCP tool.")
+ tool_set.add_tool(aisearch_tool)
+ tool_set.remove_tool("web_search")
+ tool_set.remove_tool("fetch_url")
+ tool_set.remove_tool("web_search_tavily")
+ tool_set.remove_tool("tavily_extract_web_page")
+ except Exception as e:
+ logger.error(f"Cannot Initialize Baidu AI Search MCP Server: {e}")
diff --git a/pyproject.toml b/pyproject.toml
index e7a2e49a0..ef491eeb0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "AstrBot"
-version = "4.3.1"
+version = "4.5.0"
description = "易上手的多平台 LLM 聊天机器人及开发框架"
readme = "README.md"
requires-python = ">=3.10"
@@ -24,7 +24,6 @@ dependencies = [
"faiss-cpu==1.10.0",
"filelock>=3.18.0",
"google-genai>=1.14.0",
- "mi-googlesearch-python==1.3.0.post1",
"lark-oapi>=1.4.15",
"lxml-html-clean>=0.4.2",
"mcp>=1.8.0",
@@ -52,6 +51,12 @@ dependencies = [
"audioop-lts ; python_full_version >= '3.13'",
"click>=8.2.1",
"shipyard-python-sdk>=0.2.3",
+ "pypdf>=6.1.1",
+ "aiofiles>=25.1.0",
+ "rank-bm25>=0.2.2",
+ "jieba>=0.42.1",
+ "markitdown-no-magika[docx,xls,xlsx]>=0.1.2",
+ "xinference-client",
]
[project.scripts]
@@ -85,6 +90,7 @@ target-version = "py310"
[dependency-groups]
dev = [
+ "commitizen>=4.9.1",
"pytest>=8.4.1",
"pytest-asyncio>=1.1.0",
"pytest-cov>=6.2.1",
diff --git a/requirements.txt b/requirements.txt
index 5af016b28..308c27e51 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -44,3 +44,9 @@ sqlmodel
deprecated
sqlalchemy[asyncio]
audioop-lts; python_version>='3.13'
+pypdf
+aiofiles
+rank-bm25
+jieba
+markitdown-no-magika[docx,xls,xlsx]
+xinference-client
\ No newline at end of file