mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbece6af7f | ||
|
|
b1e68182bd | ||
|
|
45a64bea78 | ||
|
|
aec8735388 | ||
|
|
1d91faaa49 | ||
|
|
e1e21c0063 |
@@ -32,7 +32,7 @@
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 💬 大模型对话、Agent:支持多种大模型,适配群聊和私聊;具有多轮对话、工具调用、多模态能力,并深度适配 [Dify](https://dify.ai)。目前支持 QQ、QQ频道、企业微信、飞书,后续还将支持微信、WhatsApp、Discord等平台。
|
||||
- 💬 大模型对话、Agent:支持多种大模型,适配群聊和私聊;具有多轮对话、工具调用、多模态能力,并深度适配 [Dify](https://dify.ai)。目前支持 QQ、QQ频道、企业微信、飞书、Discord,后续还将支持个人微信、WhatsApp、Telegram 等平台。
|
||||
- 🛠️ 高稳定性、功能完备:原生支持访问控制、限速、敏感词过滤等机制;配置简单,支持多种部署方式。
|
||||
- 🧩 插件扩展、活跃社区:支持事件驱动、组件扩展等插件机制;丰富生态,目前已有数十个[插件](https://docs.langbot.app/plugin/plugin-intro.html)
|
||||
- 😻 [New] Web 管理面板:支持通过浏览器管理 LangBot 实例,具体支持功能,查看[文档](https://docs.langbot.app/webui/intro.html)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
semantic_version = "v3.4.5"
|
||||
semantic_version = "v3.4.5.2"
|
||||
|
||||
debug_mode = False
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
},
|
||||
{
|
||||
"adapter": "discord",
|
||||
"enable": true,
|
||||
"enable": false,
|
||||
"client_id": "1234567890",
|
||||
"token": "XXXXXXXXXX"
|
||||
}
|
||||
|
||||
181
web/src/components/MarketPluginCard.vue
Normal file
181
web/src/components/MarketPluginCard.vue
Normal file
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<div class="plugin-card">
|
||||
<div class="plugin-card-header">
|
||||
<div class="plugin-id">
|
||||
<div class="plugin-card-author">{{ plugin.author }} /</div>
|
||||
<div class="plugin-card-title">{{ plugin.name }}</div>
|
||||
</div>
|
||||
<div class="plugin-card-badges">
|
||||
<v-icon class="plugin-github-source" icon="mdi-github" v-if="plugin.repository != ''"
|
||||
@click="openGithubSource"></v-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="plugin-card-description" >{{ plugin.description }}</div>
|
||||
|
||||
<div class="plugin-card-brief-info">
|
||||
<div class="plugin-card-brief-info-item">
|
||||
<v-icon id="plugin-stars-icon" icon="mdi-star" />
|
||||
<div id="plugin-stars-count">{{ plugin.stars }}</div>
|
||||
</div>
|
||||
<v-btn color="primary" @click="installPlugin" density="compact">安装</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
plugin: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['install']);
|
||||
|
||||
const openGithubSource = () => {
|
||||
window.open("https://"+props.plugin.repository, '_blank');
|
||||
}
|
||||
|
||||
const installPlugin = () => {
|
||||
emit('install', props.plugin);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.plugin-card {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 0.8rem;
|
||||
padding-left: 1rem;
|
||||
margin: 1rem 0;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
height: 10rem;
|
||||
}
|
||||
|
||||
.plugin-card-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.plugin-card-author {
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.plugin-card-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.plugin-card-description {
|
||||
font-size: 0.7rem;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
margin-top: -1rem;
|
||||
height: 2rem;
|
||||
/* 超出部分自动换行,最多两行 */
|
||||
text-overflow: ellipsis;
|
||||
overflow-y: hidden;
|
||||
white-space: wrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.plugin-card-badges {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.plugin-github-source {
|
||||
cursor: pointer;
|
||||
color: #222;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.plugin-disabled {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
height: 1.3rem;
|
||||
padding-inline: 0.4rem;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
||||
.plugin-card-brief-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
/* background-color: #f0f0f0; */
|
||||
gap: 0.8rem;
|
||||
margin-left: -0.2rem;
|
||||
margin-bottom: 0rem;
|
||||
}
|
||||
|
||||
.plugin-card-events {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.plugin-card-events-icon {
|
||||
font-size: 1.8rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.plugin-card-events-count {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.plugin-card-functions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.plugin-card-functions-icon {
|
||||
font-size: 1.6rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.plugin-card-functions-count {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.plugin-card-brief-info-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
#plugin-stars-icon {
|
||||
color: #0073ff;
|
||||
}
|
||||
|
||||
#plugin-stars-count {
|
||||
margin-top: 0.1rem;
|
||||
font-weight: 700;
|
||||
color: #0073ff;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.plugin-card-brief-info-item:hover .plugin-card-brief-info-item-icon {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
228
web/src/components/Marketplace.vue
Normal file
228
web/src/components/Marketplace.vue
Normal file
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<div id="marketplace-container">
|
||||
<div id="marketplace-search-bar">
|
||||
|
||||
<span style="width: 14rem;">
|
||||
<v-text-field id="marketplace-search-bar-search-input" variant="solo" v-model="proxy.$store.state.marketplaceParams.query" label="搜索"
|
||||
density="compact" @update:model-value="updateSearch" />
|
||||
</span>
|
||||
<!--下拉选择排序-->
|
||||
<span style="width: 10rem;">
|
||||
<v-select id="marketplace-search-bar-sort-select" v-model="sort" :items="sortItems" variant="solo"
|
||||
label="排序" density="compact" @update:model-value="updateSort" />
|
||||
</span>
|
||||
<span style="margin-left: 1rem;">
|
||||
<div id="marketplace-search-bar-total-plugins-count">
|
||||
共 {{ proxy.$store.state.marketplaceTotalPluginsCount }} 个插件
|
||||
</div>
|
||||
</span>
|
||||
<span style="margin-left: 1rem;">
|
||||
<!-- 分页 -->
|
||||
<v-pagination style="width: 14rem;" v-model="proxy.$store.state.marketplaceParams.page"
|
||||
:length="proxy.$store.state.marketplaceTotalPages" variant="solo" density="compact"
|
||||
total-visible="4" @update:model-value="updatePage" />
|
||||
</span>
|
||||
</div>
|
||||
<div id="marketplace-plugins-container" ref="pluginsContainer">
|
||||
<div id="marketplace-plugins-container-inner">
|
||||
<MarketPluginCard v-for="plugin in proxy.$store.state.marketplacePlugins" :key="plugin.id" :plugin="plugin" @install="installPlugin" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MarketPluginCard from './MarketPluginCard.vue'
|
||||
|
||||
import { ref, getCurrentInstance, onMounted } from 'vue'
|
||||
|
||||
import { inject } from "vue";
|
||||
|
||||
const snackbar = inject('snackbar');
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
|
||||
const pluginsContainer = ref(null)
|
||||
|
||||
const sortItems = ref([
|
||||
'最近新增',
|
||||
'最多星标',
|
||||
'最近更新',
|
||||
])
|
||||
|
||||
const sortParams = ref({
|
||||
'最近新增': {
|
||||
sort_by: 'created_at',
|
||||
sort_order: 'DESC',
|
||||
},
|
||||
'最多星标': {
|
||||
sort_by: 'stars',
|
||||
sort_order: 'DESC',
|
||||
},
|
||||
'最近更新': {
|
||||
sort_by: 'pushed_at',
|
||||
sort_order: 'DESC',
|
||||
}
|
||||
})
|
||||
|
||||
const sort = ref(sortItems.value[0])
|
||||
|
||||
proxy.$store.state.marketplaceParams.sort_by = sortParams.value[sort.value].sort_by
|
||||
proxy.$store.state.marketplaceParams.sort_order = sortParams.value[sort.value].sort_order
|
||||
|
||||
const updateSort = (value) => {
|
||||
console.log(value)
|
||||
proxy.$store.state.marketplaceParams.sort_by = sortParams.value[value].sort_by
|
||||
proxy.$store.state.marketplaceParams.sort_order = sortParams.value[value].sort_order
|
||||
proxy.$store.state.marketplaceParams.page = 1
|
||||
|
||||
console.log(proxy.$store.state.marketplaceParams)
|
||||
fetchMarketplacePlugins()
|
||||
}
|
||||
|
||||
const updatePage = (value) => {
|
||||
proxy.$store.state.marketplaceParams.page = value
|
||||
fetchMarketplacePlugins()
|
||||
}
|
||||
|
||||
const updateSearch = (value) => {
|
||||
console.log(value)
|
||||
proxy.$store.state.marketplaceParams.query = value
|
||||
proxy.$store.state.marketplaceParams.page = 1
|
||||
fetchMarketplacePlugins()
|
||||
}
|
||||
|
||||
const calculatePluginsPerPage = () => {
|
||||
if (!pluginsContainer.value) return 10
|
||||
|
||||
const containerWidth = pluginsContainer.value.clientWidth
|
||||
const containerHeight = pluginsContainer.value.clientHeight
|
||||
|
||||
console.log(containerWidth, containerHeight)
|
||||
|
||||
// 每个卡片宽度18rem + gap 16px
|
||||
const cardWidth = 18 * 16 + 16 // rem转px
|
||||
// 每个卡片高度9rem + gap 16px
|
||||
const cardHeight = 9 * 16 + 16
|
||||
|
||||
// 计算每行可以放几个卡片
|
||||
const cardsPerRow = Math.floor(containerWidth / cardWidth)
|
||||
// 计算每行可以放几行
|
||||
const rows = Math.floor(containerHeight / cardHeight)
|
||||
|
||||
// 计算每页总数
|
||||
const perPage = cardsPerRow * rows
|
||||
|
||||
proxy.$store.state.marketplaceParams.per_page = perPage > 0 ? perPage : 10
|
||||
}
|
||||
|
||||
const fetchMarketplacePlugins = async () => {
|
||||
calculatePluginsPerPage()
|
||||
proxy.$axios.post('https://space.langbot.app/api/v1/market/plugins', {
|
||||
query: proxy.$store.state.marketplaceParams.query,
|
||||
sort_by: proxy.$store.state.marketplaceParams.sort_by,
|
||||
sort_order: proxy.$store.state.marketplaceParams.sort_order,
|
||||
page: proxy.$store.state.marketplaceParams.page,
|
||||
page_size: proxy.$store.state.marketplaceParams.per_page,
|
||||
}).then(response => {
|
||||
console.log(response.data)
|
||||
if (response.data.code != 0) {
|
||||
snackbar.error(response.data.msg)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析出 name 和 author
|
||||
response.data.data.plugins.forEach(plugin => {
|
||||
plugin.name = plugin.repository.split('/')[2]
|
||||
plugin.author = plugin.repository.split('/')[1]
|
||||
})
|
||||
|
||||
proxy.$store.state.marketplacePlugins = response.data.data.plugins
|
||||
proxy.$store.state.marketplaceTotalPluginsCount = response.data.data.total
|
||||
|
||||
let totalPages = Math.floor(response.data.data.total / proxy.$store.state.marketplaceParams.per_page)
|
||||
if (response.data.data.total % proxy.$store.state.marketplaceParams.per_page != 0) {
|
||||
totalPages += 1
|
||||
}
|
||||
proxy.$store.state.marketplaceTotalPages = totalPages
|
||||
}).catch(error => {
|
||||
snackbar.error(error)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
calculatePluginsPerPage()
|
||||
fetchMarketplacePlugins()
|
||||
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', () => {
|
||||
calculatePluginsPerPage()
|
||||
fetchMarketplacePlugins()
|
||||
})
|
||||
})
|
||||
|
||||
const emit = defineEmits(['installPlugin'])
|
||||
|
||||
const installPlugin = (plugin) => {
|
||||
emit('installPlugin', plugin.repository)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#marketplace-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#marketplace-search-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 1rem;
|
||||
padding-right: 1rem;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#marketplace-search-bar-search-input {
|
||||
position: relative;
|
||||
left: 1rem;
|
||||
width: 10rem;
|
||||
}
|
||||
|
||||
#marketplace-search-bar-total-plugins-count {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
margin-top: 0.5rem;
|
||||
color: #666;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.plugin-card {
|
||||
width: 18rem;
|
||||
height: 9rem;
|
||||
}
|
||||
|
||||
#marketplace-plugins-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin-inline: 0rem;
|
||||
width: 100%;
|
||||
height: calc(100vh - 16rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#marketplace-plugins-container-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -2,6 +2,10 @@
|
||||
<PageTitle title="插件" @refresh="refresh" />
|
||||
<v-card id="plugins-toolbar">
|
||||
<div id="view-btns">
|
||||
<v-btn-toggle id="plugins-view-toggle" color="primary" v-model="proxy.$store.state.pluginsView" mandatory density="compact">
|
||||
<v-btn class="plugins-view-toggle-btn" value="installed" density="compact">已安装</v-btn>
|
||||
<v-btn class="plugins-view-toggle-btn" value="market" density="compact">插件市场</v-btn>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
<div id="operation-btns">
|
||||
<v-tooltip text="设置插件优先级" location="top">
|
||||
@@ -78,17 +82,21 @@
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card>
|
||||
<div class="plugins-container">
|
||||
<div class="plugins-container" v-if="proxy.$store.state.pluginsView == 'installed'">
|
||||
<v-alert id="no-plugins-alert" v-if="plugins.length == 0" color="warning" icon="$warning" title="暂无插件" text="暂无已安装的插件,请安装插件" density="compact" style="margin-inline: 1rem;"></v-alert>
|
||||
<PluginCard class="plugin-card" v-if="plugins.length > 0" v-for="plugin in plugins" :key="plugin.name" :plugin="plugin"
|
||||
@toggle="togglePlugin" @update="updatePlugin" @remove="removePlugin" />
|
||||
</div>
|
||||
<div class="plugins-container" v-if="proxy.$store.state.pluginsView == 'market'">
|
||||
<Marketplace @installPlugin="installMarketplacePlugin" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import PageTitle from '@/components/PageTitle.vue'
|
||||
import PluginCard from '@/components/PluginCard.vue'
|
||||
import Marketplace from '@/components/Marketplace.vue'
|
||||
|
||||
import draggable from 'vuedraggable'
|
||||
|
||||
@@ -154,6 +162,12 @@ const removePlugin = (plugin) => {
|
||||
})
|
||||
}
|
||||
|
||||
const installMarketplacePlugin = (repository) => {
|
||||
|
||||
installDialogSource.value = 'https://'+repository
|
||||
isInstallDialogActive.value = true
|
||||
}
|
||||
|
||||
const installPlugin = () => {
|
||||
|
||||
if (installDialogSource.value == '' || installDialogSource.value.trim() == '') {
|
||||
@@ -224,6 +238,11 @@ const installDialogSource = ref('')
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
#plugins-view-toggle {
|
||||
margin: 0.5rem;
|
||||
box-shadow: 0 0 0 2px #dddddd;
|
||||
}
|
||||
|
||||
#operation-btns {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -18,7 +18,18 @@ export default createStore({
|
||||
tokenValid: false,
|
||||
systemInitialized: true,
|
||||
jwtToken: '',
|
||||
}
|
||||
},
|
||||
pluginsView: 'installed',
|
||||
marketplaceParams: {
|
||||
query: '',
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
sort_by: 'pushed_at',
|
||||
sort_order: 'DESC',
|
||||
},
|
||||
marketplacePlugins: [],
|
||||
marketplaceTotalPages: 0,
|
||||
marketplaceTotalPluginsCount: 0,
|
||||
},
|
||||
mutations: {
|
||||
initializeFetch() {
|
||||
|
||||
Reference in New Issue
Block a user