Compare commits

...

5 Commits

10 changed files with 220 additions and 35 deletions
+5 -2
View File
@@ -88,8 +88,11 @@ class LocalPythonTool(FunctionTool):
self, context: ContextWrapper[AstrAgentContext], code: str, silent: bool = False
) -> ToolExecResult:
if context.context.event.role != "admin":
return "error: Permission denied. Local Python execution is only allowed for admin users. Tell user to set admins in AstrBot WebUI."
return (
"error: Permission denied. Local Python execution is only allowed for admin users. "
"Tell user to set admins in AstrBot WebUI by adding their user ID to the admins list if they need this feature."
f"User's ID is: {context.context.event.get_sender_id()}. User's ID can be found by using /sid command."
)
sb = get_local_booter()
try:
result = await sb.python.exec(code, silent=silent)
+5 -1
View File
@@ -47,7 +47,11 @@ class ExecuteShellTool(FunctionTool):
env: dict = {},
) -> ToolExecResult:
if context.context.event.role != "admin":
return "error: Permission denied. Shell execution is only allowed for admin users. Tell user to Set admins in AstrBot WebUI."
return (
"error: Permission denied. Local shell execution is only allowed for admin users. "
"Tell user to set admins in AstrBot WebUI by adding their user ID to the admins list if they need this feature."
f"User's ID is: {context.context.event.get_sender_id()}. User's ID can be found by using /sid command."
)
if self.is_local:
sb = get_local_booter()
+12
View File
@@ -1029,6 +1029,18 @@ CONFIG_METADATA_2 = {
"proxy": "",
"custom_headers": {},
},
"NVIDIA": {
"id": "nvidia",
"provider": "nvidia",
"type": "openai_chat_completion",
"provider_type": "chat_completion",
"enable": True,
"key": [],
"api_base": "https://integrate.api.nvidia.com/v1",
"timeout": 120,
"proxy": "",
"custom_headers": {},
},
"Azure OpenAI": {
"id": "azure_openai",
"provider": "azure",
@@ -14,8 +14,13 @@
</div>
<!-- 选择对话框 -->
<v-dialog v-model="dialog" max-width="1000px" min-width="800px">
<v-card class="selector-dialog-card">
<v-dialog
v-model="dialog"
:max-width="$vuetify.display.smAndDown ? undefined : '1000px'"
:min-width="$vuetify.display.smAndDown ? undefined : '800px'"
scrollable
>
<v-card class="selector-dialog-card" :class="{ 'selector-dialog-card-mobile': $vuetify.display.smAndDown }">
<v-card-title class="dialog-title d-flex align-center py-4 px-5">
<v-icon class="mr-3" color="primary">mdi-account-circle</v-icon>
<span>{{ labels.dialogTitle || '选择项目' }}</span>
@@ -23,7 +28,7 @@
<v-divider />
<v-card-text class="pa-0" style="height: 600px; max-height: 80vh; overflow: hidden;">
<v-card-text class="selector-dialog-content pa-0">
<div class="selector-layout">
<!-- 左侧文件夹树 -->
<div class="folder-sidebar">
@@ -146,7 +151,7 @@
</div>
</v-card-text>
<v-card-actions class="pa-4">
<v-card-actions class="selector-dialog-actions pa-4">
<v-btn v-if="showCreateButton" variant="text" color="primary" prepend-icon="mdi-plus"
@click="$emit('create')">
{{ labels.createButton || '新建' }}
@@ -406,6 +411,12 @@ export default defineComponent({
overflow: hidden;
}
.selector-dialog-content {
height: 600px;
max-height: 80vh;
overflow: hidden;
}
.dialog-title {
font-size: 1.25rem;
font-weight: 500;
@@ -518,21 +529,44 @@ export default defineComponent({
}
@media (max-width: 600px) {
.selector-dialog-card-mobile {
border-radius: 0;
}
.selector-dialog-content {
height: calc(100vh - 132px);
max-height: none;
}
.dialog-title {
font-size: 1.05rem;
padding: 12px 16px !important;
}
.selector-dialog-actions {
padding: 12px 16px !important;
gap: 8px;
}
.selector-dialog-actions .v-btn {
min-width: 0;
}
.selector-layout {
flex-direction: column;
height: auto;
max-height: 500px;
height: 100%;
max-height: none;
}
.folder-sidebar {
width: 100%;
border-right: none;
border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
max-height: 150px;
max-height: 35vh;
}
.items-list {
max-height: 300px;
max-height: none;
}
}
</style>
@@ -4,7 +4,7 @@
<div class="d-flex align-center ga-2">
<h3 class="mb-0">{{ tm('providerSources.title') }}</h3>
</div>
<v-menu>
<StyledMenu>
<template #activator="{ props }">
<v-btn
v-bind="props"
@@ -17,19 +17,61 @@
{{ tm('providerSources.add') }}
</v-btn>
</template>
<v-list density="compact">
<v-list-item
v-for="sourceType in availableSourceTypes"
:key="sourceType.value"
@click="emitAddSource(sourceType.value)"
>
<v-list-item-title>{{ sourceType.label }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-list-item
v-for="sourceType in availableSourceTypes"
:key="sourceType.value"
class="styled-menu-item"
@click="emitAddSource(sourceType.value)"
>
<template #prepend>
<v-avatar size="18" rounded="0" class="me-2">
<v-img v-if="sourceType.icon" :src="sourceType.icon" alt="provider icon" cover></v-img>
<v-icon v-else size="16">mdi-shape-outline</v-icon>
</v-avatar>
</template>
<v-list-item-title>{{ sourceType.label }}</v-list-item-title>
</v-list-item>
</StyledMenu>
</div>
<div v-if="displayedProviderSources.length > 0">
<div v-if="isMobile && displayedProviderSources.length > 0" class="px-4 pb-3">
<div class="d-flex align-center ga-2">
<v-select
:model-value="selectedId"
:items="mobileSourceItems"
item-title="label"
item-value="value"
:label="tm('providerSources.selectCreated')"
variant="solo-filled"
density="comfortable"
flat
hide-details
class="mobile-source-select"
@update:model-value="onMobileSourceChange"
>
<template #item="{ props: itemProps, item }">
<v-list-item v-bind="itemProps">
<template #prepend>
<v-avatar size="18" rounded="0" class="me-2">
<v-img v-if="item.raw.icon" :src="item.raw.icon" alt="provider icon" cover></v-img>
<v-icon v-else size="16">mdi-shape-outline</v-icon>
</v-avatar>
</template>
</v-list-item>
</template>
</v-select>
<v-btn
v-if="selectedProviderSource"
icon="mdi-delete"
variant="text"
size="small"
color="error"
@click.stop="emitDeleteSource(selectedProviderSource)"
></v-btn>
</div>
</div>
<div v-else-if="displayedProviderSources.length > 0">
<v-list class="provider-source-list" nav density="compact" lines="two">
<v-list-item
v-for="source in displayedProviderSources"
@@ -46,7 +88,7 @@
<v-icon v-else size="32">mdi-creation</v-icon>
</v-avatar>
</template>
<v-list-item-title class="font-weight-bold">{{ getSourceDisplayName(source) }}</v-list-item-title>
<v-list-item-title class="font-weight-bold mb-1" style="font-family: Arial, Helvetica, sans-serif; font-size: 16px;">{{ getSourceDisplayName(source) }}</v-list-item-title>
<v-list-item-subtitle class="text-truncate">{{ source.api_base || 'N/A' }}</v-list-item-subtitle>
<template #append>
<div class="d-flex align-center ga-1">
@@ -72,6 +114,8 @@
<script setup>
import { computed } from 'vue'
import { useDisplay } from 'vuetify'
import StyledMenu from '@/components/shared/StyledMenu.vue'
const props = defineProps({
displayedProviderSources: {
@@ -106,13 +150,30 @@ const emit = defineEmits([
'delete-provider-source'
])
const { smAndDown } = useDisplay()
const selectedId = computed(() => props.selectedProviderSource?.id || null)
const isMobile = computed(() => smAndDown.value)
const mobileSourceItems = computed(() =>
(props.displayedProviderSources || []).map((source) => ({
value: source.id,
label: props.getSourceDisplayName(source),
icon: props.resolveSourceIcon(source),
source
}))
)
const isActive = (source) => {
if (source.isPlaceholder) return false
return selectedId.value !== null && selectedId.value === source.id
}
const onMobileSourceChange = (sourceId) => {
const matched = mobileSourceItems.value.find((item) => item.value === sourceId)
if (matched?.source) {
emitSelectSource(matched.source)
}
}
const emitAddSource = (type) => emit('add-provider-source', type)
const emitSelectSource = (source) => emit('select-provider-source', source)
const emitDeleteSource = (source) => emit('delete-provider-source', source)
@@ -1,11 +1,15 @@
<template>
<v-dialog v-model="showDialog" max-width="500px">
<v-card>
<v-card-title class="text-h2">
<v-dialog
v-model="showDialog"
:max-width="$vuetify.display.smAndDown ? undefined : '760px'"
scrollable
>
<v-card class="persona-form-card" :class="{ 'persona-form-card-mobile': $vuetify.display.smAndDown }">
<v-card-title class="persona-form-title text-h2">
{{ editingPersona ? tm('dialog.edit.title') : tm('dialog.create.title') }}
</v-card-title>
<v-card-text>
<v-card-text class="persona-form-content">
<!-- 创建位置提示 -->
<v-alert
v-if="!editingPersona"
@@ -51,7 +55,7 @@
</v-radio>
</v-radio-group>
<div v-if="toolSelectValue === '1'" class="mt-3 ml-8">
<div v-if="toolSelectValue === '1'" class="mt-3 selected-config-area">
<!-- 工具搜索 -->
<v-text-field v-model="toolSearch" :label="tm('form.searchTools')"
@@ -178,7 +182,7 @@
<v-radio :label="tm('form.skillsSelectSpecific')" value="1"></v-radio>
</v-radio-group>
<div v-if="skillSelectValue === '1'" class="mt-3 ml-8">
<div v-if="skillSelectValue === '1'" class="mt-3 selected-config-area">
<v-text-field v-model="skillSearch" :label="tm('form.searchSkills')"
prepend-inner-icon="mdi-magnify" variant="outlined" density="compact"
hide-details clearable class="mb-3" />
@@ -288,7 +292,7 @@
</v-form>
</v-card-text>
<v-card-actions>
<v-card-actions class="persona-form-actions">
<v-btn v-if="editingPersona" color="error" variant="text" @click="deletePersona">
{{ tm('buttons.delete') }}
</v-btn>
@@ -799,6 +803,32 @@ export default {
</script>
<style scoped>
.persona-form-card {
border-radius: 12px;
overflow: hidden;
}
.persona-form-content {
max-height: min(78vh, 760px);
overflow-y: auto;
}
.persona-form-title {
line-height: 1.3;
}
.persona-form-actions {
position: sticky;
bottom: 0;
z-index: 2;
background: rgb(var(--v-theme-surface));
border-top: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
}
.selected-config-area {
margin-left: 32px;
}
.tools-selection {
max-height: 300px;
overflow-y: auto;
@@ -812,4 +842,38 @@ export default {
.v-virtual-scroll {
padding-bottom: 16px;
}
@media (max-width: 600px) {
.persona-form-card-mobile {
border-radius: 0;
}
.persona-form-content {
max-height: calc(100vh - 128px);
padding: 16px !important;
}
.persona-form-title {
font-size: 1.15rem !important;
padding: 12px 16px !important;
}
.selected-config-area {
margin-left: 0;
}
.tools-selection,
.skills-selection {
max-height: 38vh;
}
.persona-form-actions {
padding: 12px 16px !important;
gap: 8px;
}
.persona-form-actions .v-btn {
min-width: 0;
}
}
</style>
@@ -81,10 +81,14 @@ export function useProviderSources(options: UseProviderSourcesOptions) {
return []
}
const types: Array<{ value: string; label: string }> = []
const types: Array<{ value: string; label: string; icon: string }> = []
for (const [templateName, template] of Object.entries(providerTemplates.value)) {
if (template.provider_type === selectedProviderType.value) {
types.push({ value: templateName, label: templateName })
types.push({
value: templateName,
label: templateName,
icon: getProviderIcon(template.provider)
})
}
}
@@ -94,6 +94,7 @@
"add": "Add",
"empty": "No provider sources",
"selectHint": "Please select a provider source",
"selectCreated": "Select created provider source",
"save": "Save Configuration",
"saveAndFetchModels": "Save and Fetch Models",
"fetchModels": "Fetch Model List",
@@ -146,4 +147,4 @@
"modelId": "Model ID"
}
}
}
}
@@ -95,6 +95,7 @@
"add": "新增",
"empty": "暂无提供商源",
"selectHint": "请选择一个提供商源",
"selectCreated": "选择已创建的提供商源",
"save": "保存配置",
"saveAndFetchModels": "保存并获取模型",
"fetchModels": "获取模型列表",
@@ -147,4 +148,4 @@
"modelId": "模型 ID"
}
}
}
}
+1
View File
@@ -18,6 +18,7 @@ export function getProviderIcon(type) {
'deepseek': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/deepseek.svg',
'modelscope': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/modelscope.svg',
'zhipu': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/zhipu.svg',
'nvidia': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/nvidia-color.svg',
'siliconflow': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/siliconcloud.svg',
'moonshot': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/kimi.svg',
'ppio': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ppio.svg',