Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b0e24ec49 | |||
| 92d71fffe9 | |||
| 80c22f4f72 | |||
| 6e22d266dd | |||
| 4c285fb521 | |||
| 51c3521aaa | |||
| 32112a3326 |
@@ -1 +1 @@
|
||||
__version__ = "4.17.3"
|
||||
__version__ = "4.17.4"
|
||||
|
||||
@@ -90,7 +90,7 @@ class LocalPythonTool(FunctionTool):
|
||||
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 by adding their user ID to the admins list if they need this feature."
|
||||
"Tell user to set admins in `AstrBot WebUI -> Config -> General Config` 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()
|
||||
|
||||
@@ -49,7 +49,7 @@ class ExecuteShellTool(FunctionTool):
|
||||
if context.context.event.role != "admin":
|
||||
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."
|
||||
"Tell user to set admins in `AstrBot WebUI -> Config -> General Config` 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."
|
||||
)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import Any, TypedDict
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.17.3"
|
||||
VERSION = "4.17.4"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
|
||||
WEBHOOK_SUPPORTED_PLATFORMS = [
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
## What's Changed
|
||||
|
||||
### 新增
|
||||
- 新增 NVIDIA Provider 模板,便于快速接入 NVIDIA 模型服务 ([#5157](https://github.com/AstrBotDevs/AstrBot/issues/5157))。
|
||||
- 支持在 WebUI 搜索配置
|
||||
|
||||
### 修复
|
||||
- 修复 CronJob 页面操作列按钮重叠问题,提升任务管理可用性 ([#5163](https://github.com/AstrBotDevs/AstrBot/issues/5163))。
|
||||
|
||||
### 优化
|
||||
- 优化 Python / Shell 本地执行工具的权限拒绝提示信息引导,提升排障可读性。
|
||||
- Provider 来源面板样式升级,新增菜单交互并完善移动端适配。
|
||||
- PersonaForm 组件增强响应式布局与样式细节,改进不同屏幕下的编辑体验 ([#5162](https://github.com/AstrBotDevs/AstrBot/issues/5162))。
|
||||
- 配置页面新增未保存变更提示,减少误操作导致的配置丢失。
|
||||
- 配置相关组件新增搜索能力并同步更新界面交互,提升配置项定位效率 ([#5168](https://github.com/AstrBotDevs/AstrBot/issues/5168))。
|
||||
|
||||
## What's Changed (EN)
|
||||
|
||||
### New Features
|
||||
- Added an NVIDIA provider template for faster integration with NVIDIA model services ([#5157](https://github.com/AstrBotDevs/AstrBot/issues/5157)).
|
||||
- Added an announcement section to the Welcome page, with localized announcement title support.
|
||||
- Added an FAQ link to the vertical sidebar and updated navigation for localization.
|
||||
|
||||
### Fixes
|
||||
- Fixed overlapping action buttons in the CronJob page action column to improve task management usability ([#5163](https://github.com/AstrBotDevs/AstrBot/issues/5163)).
|
||||
- Improved permission-denied messages for local execution in Python and shell tools for better troubleshooting clarity.
|
||||
|
||||
### Improvements
|
||||
- Enhanced the provider sources panel with a refined menu style and better mobile support.
|
||||
- Improved PersonaForm with responsive layout and styling updates for better editing experience across screen sizes ([#5162](https://github.com/AstrBotDevs/AstrBot/issues/5162)).
|
||||
- Added an unsaved-changes notice on the configuration page to reduce accidental config loss.
|
||||
- Added search functionality to configuration components and updated related UI interactions for faster settings discovery ([#5168](https://github.com/AstrBotDevs/AstrBot/issues/5168)).
|
||||
@@ -2,18 +2,22 @@
|
||||
<div :class="$vuetify.display.mobile ? '' : 'd-flex'">
|
||||
<v-tabs v-model="tab" :direction="$vuetify.display.mobile ? 'horizontal' : 'vertical'"
|
||||
:align-tabs="$vuetify.display.mobile ? 'left' : 'start'" color="deep-purple-accent-4" class="config-tabs">
|
||||
<v-tab v-for="(val, key, index) in metadata" :key="index" :value="index"
|
||||
<v-tab v-for="section in visibleSections" :key="section.key" :value="section.key"
|
||||
style="font-weight: 1000; font-size: 15px">
|
||||
{{ tm(metadata[key]['name']) }}
|
||||
{{ tm(section.value['name']) }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<v-tabs-window v-model="tab" class="config-tabs-window" :style="readonly ? 'pointer-events: none; opacity: 0.6;' : ''">
|
||||
<v-tabs-window-item v-for="(val, key, index) in metadata" v-show="index == tab" :key="index">
|
||||
<v-tabs-window-item v-for="section in visibleSections" :key="section.key" :value="section.key">
|
||||
<v-container fluid>
|
||||
<div v-for="(val2, key2, index2) in metadata[key]['metadata']" :key="key2">
|
||||
<div v-for="(val2, key2, index2) in section.value['metadata']" :key="key2">
|
||||
<!-- Support both traditional and JSON selector metadata -->
|
||||
<AstrBotConfigV4 :metadata="{ [key2]: metadata[key]['metadata'][key2] }" :iterable="config_data"
|
||||
:metadataKey="key2">
|
||||
<AstrBotConfigV4
|
||||
:metadata="{ [key2]: section.value['metadata'][key2] }"
|
||||
:iterable="config_data"
|
||||
:metadataKey="key2"
|
||||
:search-keyword="searchKeyword"
|
||||
>
|
||||
</AstrBotConfigV4>
|
||||
</div>
|
||||
</v-container>
|
||||
@@ -31,6 +35,11 @@
|
||||
|
||||
</v-tabs-window>
|
||||
</div>
|
||||
<v-container v-if="visibleSections.length === 0" fluid class="px-0">
|
||||
<v-alert type="info" variant="tonal">
|
||||
{{ tm('search.noResult') }}
|
||||
</v-alert>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -56,6 +65,10 @@ export default {
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
searchKeyword: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
@@ -76,11 +89,63 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tab: 0, // 用于切换配置标签页
|
||||
tab: null, // 当前激活的配置标签页 key
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
normalizedSearchKeyword() {
|
||||
return String(this.searchKeyword || '').trim().toLowerCase();
|
||||
},
|
||||
visibleSections() {
|
||||
if (!this.metadata || typeof this.metadata !== 'object') {
|
||||
return [];
|
||||
}
|
||||
const allSections = Object.entries(this.metadata).map(([key, value]) => ({ key, value }));
|
||||
if (!this.normalizedSearchKeyword) {
|
||||
return allSections;
|
||||
}
|
||||
return allSections.filter((section) => this.sectionHasSearchMatch(section.value));
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visibleSections(newSections) {
|
||||
const sectionKeys = newSections.map((section) => section.key);
|
||||
if (!sectionKeys.includes(this.tab)) {
|
||||
this.tab = sectionKeys[0] ?? null;
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const sectionKeys = this.visibleSections.map((section) => section.key);
|
||||
this.tab = sectionKeys[0] ?? null;
|
||||
},
|
||||
methods: {
|
||||
// 如果需要添加其他方法,可以在这里添加
|
||||
sectionHasSearchMatch(section) {
|
||||
const keyword = this.normalizedSearchKeyword;
|
||||
if (!keyword) {
|
||||
return true;
|
||||
}
|
||||
const sectionMetadata = section?.metadata || {};
|
||||
return Object.values(sectionMetadata).some((metaItem) => this.metaObjectHasSearchMatch(metaItem, keyword));
|
||||
},
|
||||
metaObjectHasSearchMatch(metaObject, keyword) {
|
||||
if (!metaObject || typeof metaObject !== 'object') {
|
||||
return false;
|
||||
}
|
||||
const target = [
|
||||
this.tm(metaObject.description || ''),
|
||||
this.tm(metaObject.hint || ''),
|
||||
...Object.entries(metaObject.items || {}).flatMap(([itemKey, itemMeta]) => ([
|
||||
itemKey,
|
||||
this.tm(itemMeta?.description || ''),
|
||||
this.tm(itemMeta?.hint || '')
|
||||
]))
|
||||
]
|
||||
.join(' ')
|
||||
.toLowerCase();
|
||||
|
||||
return target.includes(keyword);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -112,4 +177,4 @@ export default {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -14,13 +14,8 @@
|
||||
</div>
|
||||
|
||||
<!-- 选择对话框 -->
|
||||
<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-dialog v-model="dialog" max-width="1000px" min-width="800px">
|
||||
<v-card class="selector-dialog-card">
|
||||
<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>
|
||||
@@ -28,7 +23,7 @@
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-text class="selector-dialog-content pa-0">
|
||||
<v-card-text class="pa-0" style="height: 600px; max-height: 80vh; overflow: hidden;">
|
||||
<div class="selector-layout">
|
||||
<!-- 左侧文件夹树 -->
|
||||
<div class="folder-sidebar">
|
||||
@@ -151,7 +146,7 @@
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="selector-dialog-actions pa-4">
|
||||
<v-card-actions class="pa-4">
|
||||
<v-btn v-if="showCreateButton" variant="text" color="primary" prepend-icon="mdi-plus"
|
||||
@click="$emit('create')">
|
||||
{{ labels.createButton || '新建' }}
|
||||
@@ -411,12 +406,6 @@ export default defineComponent({
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.selector-dialog-content {
|
||||
height: 600px;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
@@ -529,44 +518,21 @@ 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: 100%;
|
||||
max-height: none;
|
||||
height: auto;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.folder-sidebar {
|
||||
width: 100%;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
max-height: 35vh;
|
||||
max-height: 150px;
|
||||
}
|
||||
|
||||
.items-list {
|
||||
max-height: none;
|
||||
max-height: 300px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,6 +19,10 @@ const props = defineProps({
|
||||
metadataKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
searchKeyword: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
@@ -124,16 +128,27 @@ function saveEditedContent() {
|
||||
}
|
||||
|
||||
function shouldShowItem(itemMeta, itemKey) {
|
||||
if (!itemMeta?.condition) {
|
||||
return true
|
||||
}
|
||||
for (const [conditionKey, expectedValue] of Object.entries(itemMeta.condition)) {
|
||||
const actualValue = getValueBySelector(props.iterable, conditionKey)
|
||||
if (actualValue !== expectedValue) {
|
||||
return false
|
||||
if (itemMeta?.condition) {
|
||||
for (const [conditionKey, expectedValue] of Object.entries(itemMeta.condition)) {
|
||||
const actualValue = getValueBySelector(props.iterable, conditionKey)
|
||||
if (actualValue !== expectedValue) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
const keyword = String(props.searchKeyword || '').trim().toLowerCase()
|
||||
if (!keyword) {
|
||||
return true
|
||||
}
|
||||
|
||||
const searchableText = [
|
||||
itemKey,
|
||||
translateIfKey(itemMeta?.description || ''),
|
||||
translateIfKey(itemMeta?.hint || '')
|
||||
].join(' ').toLowerCase()
|
||||
|
||||
return searchableText.includes(keyword)
|
||||
}
|
||||
|
||||
// 检查最外层的 object 是否应该显示
|
||||
@@ -148,7 +163,10 @@ function shouldShowSection() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
const sectionItems = props.metadata?.[props.metadataKey]?.items || {}
|
||||
const hasVisibleItems = Object.entries(sectionItems).some(([itemKey, itemMeta]) => shouldShowItem(itemMeta, itemKey))
|
||||
return hasVisibleItems
|
||||
}
|
||||
|
||||
function hasVisibleItemsAfter(items, currentIndex) {
|
||||
@@ -436,9 +454,13 @@ function getSpecialSubtype(value) {
|
||||
}
|
||||
|
||||
.property-info,
|
||||
.type-indicator,
|
||||
.type-indicator {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.config-input {
|
||||
padding: 4px;
|
||||
padding-left: 24px;
|
||||
padding-right: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"settings": "Settings",
|
||||
"changelog": "Changelog",
|
||||
"documentation": "Documentation",
|
||||
"faq": "FAQ",
|
||||
"github": "GitHub",
|
||||
"drag": "Drag",
|
||||
"groups": {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"messages": {
|
||||
"configApplied": "Configuration successfully applied. To save, you need to click the save button in the bottom right corner.",
|
||||
"configApplyError": "Configuration not applied, JSON format error.",
|
||||
"unsavedChangesNotice": "You have unsaved configuration changes. Click the save button in the bottom-right corner to apply them.",
|
||||
"saveSuccess": "Configuration saved successfully",
|
||||
"saveError": "Failed to save configuration",
|
||||
"loadError": "Failed to load configuration",
|
||||
@@ -68,6 +69,10 @@
|
||||
"normalConfig": "Basic",
|
||||
"systemConfig": "System"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search config items (key/description/hint)",
|
||||
"noResult": "No matching config items found"
|
||||
},
|
||||
"configManagement": {
|
||||
"title": "Configuration Management",
|
||||
"description": "AstrBot supports separate configuration files for different bots. The `default` configuration is used by default.",
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
"newYear": "Happy New Year!"
|
||||
},
|
||||
"subtitle": "You can complete the basic onboarding first. Platform and chat provider setup can both be skipped.",
|
||||
"announcement": {
|
||||
"title": "Announcement"
|
||||
},
|
||||
"onboard": {
|
||||
"title": "Quick Onboarding",
|
||||
"subtitle": "Complete initialization directly on the welcome page.",
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"settings": "设置",
|
||||
"changelog": "更新日志",
|
||||
"documentation": "官方文档",
|
||||
"faq": "FAQ",
|
||||
"github": "GitHub",
|
||||
"drag": "拖拽",
|
||||
"groups": {
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"messages": {
|
||||
"configApplied": "配置成功应用。如要保存,需再点击右下角保存按钮。",
|
||||
"configApplyError": "配置未应用,Json 格式错误。",
|
||||
"unsavedChangesNotice": "当前配置有未保存修改。请点击右下角保存按钮以生效。",
|
||||
"saveSuccess": "配置保存成功",
|
||||
"saveError": "配置保存失败",
|
||||
"loadError": "配置加载失败",
|
||||
@@ -68,6 +69,10 @@
|
||||
"normalConfig": "普通",
|
||||
"systemConfig": "系统"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜索配置项(字段名/描述/提示)",
|
||||
"noResult": "未找到匹配的配置项"
|
||||
},
|
||||
"configManagement": {
|
||||
"title": "配置文件管理",
|
||||
"description": "AstrBot 支持针对不同机器人分别设置配置文件。默认会使用 `default` 配置。",
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
"newYear": "新年快乐!"
|
||||
},
|
||||
"subtitle": "可以先完成基础引导,平台和对话提供商都支持稍后再配置。",
|
||||
"announcement": {
|
||||
"title": "公告"
|
||||
},
|
||||
"onboard": {
|
||||
"title": "快速引导",
|
||||
"subtitle": "欢迎页可直接完成初始化。",
|
||||
|
||||
@@ -7,7 +7,7 @@ import NavItem from './NavItem.vue';
|
||||
import { applySidebarCustomization } from '@/utils/sidebarCustomization';
|
||||
import ChangelogDialog from '@/components/shared/ChangelogDialog.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
const customizer = useCustomizerStore();
|
||||
const sidebarMenu = shallowRef(sidebarItems);
|
||||
@@ -109,6 +109,13 @@ function openIframeLink(url) {
|
||||
}
|
||||
}
|
||||
|
||||
function openFaqLink() {
|
||||
const faqUrl = locale.value === 'en-US'
|
||||
? 'https://docs.astrbot.app/en/faq.html'
|
||||
: 'https://docs.astrbot.app/faq.html';
|
||||
openIframeLink(faqUrl);
|
||||
}
|
||||
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
let isDragging = false;
|
||||
@@ -264,6 +271,10 @@ function openChangelogDialog() {
|
||||
@click="toggleIframe">
|
||||
{{ t('core.navigation.documentation') }}
|
||||
</v-btn>
|
||||
<v-btn class="sidebar-footer-btn" size="small" variant="text" prepend-icon="mdi-frequently-asked-questions"
|
||||
@click="openFaqLink">
|
||||
{{ t('core.navigation.faq') }}
|
||||
</v-btn>
|
||||
<v-btn class="sidebar-footer-btn" size="small" variant="text" prepend-icon="mdi-github"
|
||||
@click="openIframeLink('https://github.com/AstrBotDevs/AstrBot')">
|
||||
{{ t('core.navigation.github') }}
|
||||
|
||||
@@ -4,17 +4,39 @@
|
||||
<div v-if="selectedConfigID || isSystemConfig" class="mt-4 config-panel"
|
||||
style="display: flex; flex-direction: column; align-items: start;">
|
||||
|
||||
<div class="d-flex flex-row pr-4"
|
||||
<div class="config-toolbar d-flex flex-row pr-4"
|
||||
style="margin-bottom: 16px; align-items: center; gap: 12px; width: 100%; justify-content: space-between;">
|
||||
<div class="d-flex flex-row align-center" style="gap: 12px;">
|
||||
<v-select style="min-width: 130px;" v-model="selectedConfigID" :items="configSelectItems" item-title="name" :disabled="initialConfigId !== null"
|
||||
<div class="config-toolbar-controls d-flex flex-row align-center" style="gap: 12px;">
|
||||
<v-select class="config-select" style="min-width: 130px;" v-model="selectedConfigID" :items="configSelectItems" item-title="name" :disabled="initialConfigId !== null"
|
||||
v-if="!isSystemConfig" item-value="id" :label="tm('configSelection.selectConfig')" hide-details density="compact" rounded="md"
|
||||
variant="outlined" @update:model-value="onConfigSelect">
|
||||
</v-select>
|
||||
<v-text-field
|
||||
class="config-search-input"
|
||||
v-model="configSearchKeyword"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
:label="tm('search.placeholder')"
|
||||
hide-details
|
||||
density="compact"
|
||||
rounded="md"
|
||||
variant="outlined"
|
||||
style="min-width: 280px;"
|
||||
/>
|
||||
<!-- <a style="color: inherit;" href="https://blog.astrbot.app/posts/what-is-changed-in-4.0.0/#%E5%A4%9A%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6" target="_blank"><v-btn icon="mdi-help-circle" size="small" variant="plain"></v-btn></a> -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<v-slide-y-transition>
|
||||
<div v-if="fetched && hasUnsavedChanges" class="unsaved-changes-banner-wrap">
|
||||
<v-banner
|
||||
icon="$warning"
|
||||
lines="one"
|
||||
class="unsaved-changes-banner my-4"
|
||||
>
|
||||
{{ tm('messages.unsavedChangesNotice') }}
|
||||
</v-banner>
|
||||
</div>
|
||||
</v-slide-y-transition>
|
||||
<!-- <v-progress-linear v-if="!fetched" indeterminate color="primary"></v-progress-linear> -->
|
||||
|
||||
<v-slide-y-transition mode="out-in">
|
||||
@@ -23,6 +45,7 @@
|
||||
<AstrBotCoreConfigWrapper
|
||||
:metadata="metadata"
|
||||
:config_data="config_data"
|
||||
:search-keyword="configSearchKeyword"
|
||||
/>
|
||||
|
||||
<v-tooltip :text="tm('actions.save')" location="left">
|
||||
@@ -235,6 +258,12 @@ export default {
|
||||
});
|
||||
return items;
|
||||
},
|
||||
hasUnsavedChanges() {
|
||||
if (!this.fetched) {
|
||||
return false;
|
||||
}
|
||||
return this.getConfigSnapshot(this.config_data) !== this.lastSavedConfigSnapshot;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
config_data_str(val) {
|
||||
@@ -269,9 +298,11 @@ export default {
|
||||
save_message: "",
|
||||
save_message_success: "",
|
||||
configContentKey: 0,
|
||||
lastSavedConfigSnapshot: '',
|
||||
|
||||
// 配置类型切换
|
||||
configType: 'normal', // 'normal' 或 'system'
|
||||
configSearchKeyword: '',
|
||||
|
||||
// 系统配置开关
|
||||
isSystemConfig: false,
|
||||
@@ -383,6 +414,7 @@ export default {
|
||||
params: params
|
||||
}).then((res) => {
|
||||
this.config_data = res.data.data.config;
|
||||
this.lastSavedConfigSnapshot = this.getConfigSnapshot(this.config_data);
|
||||
this.fetched = true
|
||||
this.metadata = res.data.data.metadata;
|
||||
this.configContentKey += 1;
|
||||
@@ -407,6 +439,7 @@ export default {
|
||||
|
||||
axios.post('/api/config/astrbot/update', postData).then((res) => {
|
||||
if (res.data.status === "ok") {
|
||||
this.lastSavedConfigSnapshot = this.getConfigSnapshot(this.config_data);
|
||||
this.save_message = res.data.message || this.messages.saveSuccess;
|
||||
this.save_message_snack = true;
|
||||
this.save_message_success = "success";
|
||||
@@ -601,6 +634,9 @@ export default {
|
||||
closeTestChat() {
|
||||
this.testChatDrawer = false;
|
||||
this.testConfigId = null;
|
||||
},
|
||||
getConfigSnapshot(config) {
|
||||
return JSON.stringify(config ?? {});
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -612,6 +648,26 @@ export default {
|
||||
text-transform: none !important;
|
||||
}
|
||||
|
||||
.unsaved-changes-banner {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.v-theme--light .unsaved-changes-banner {
|
||||
background-color: #f1f4f9 !important;
|
||||
}
|
||||
|
||||
.v-theme--dark .unsaved-changes-banner {
|
||||
background-color: #2d2d2d !important;
|
||||
}
|
||||
|
||||
.unsaved-changes-banner-wrap {
|
||||
position: sticky;
|
||||
top: calc(var(--v-layout-top, 64px));
|
||||
z-index: 20;
|
||||
width: 100%;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
/* 按钮切换样式优化 */
|
||||
.v-btn-toggle .v-btn {
|
||||
transition: all 0.3s ease !important;
|
||||
@@ -659,6 +715,21 @@ export default {
|
||||
.config-panel {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.config-toolbar {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.config-toolbar-controls {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.config-select,
|
||||
.config-search-input {
|
||||
width: 100%;
|
||||
min-width: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 测试聊天抽屉样式 */
|
||||
|
||||
@@ -55,11 +55,12 @@
|
||||
<template #item.last_run_at="{ item }">{{ formatTime(item.last_run_at) }}</template>
|
||||
<template #item.note="{ item }">{{ item.note || tm('table.notAvailable') }}</template>
|
||||
<template #item.actions="{ item }">
|
||||
<div class="d-flex" style="gap: 8px;">
|
||||
<div class="d-flex align-center flex-nowrap" style="gap: 12px; min-width: 140px;">
|
||||
<v-switch v-model="item.enabled" inset density="compact" hide-details color="primary"
|
||||
@change="toggleJob(item)" />
|
||||
<v-btn size="small" variant="text" color="primary" @click="deleteJob(item)">{{ tm('actions.delete')
|
||||
}}</v-btn>
|
||||
class="mt-0" @change="toggleJob(item)" />
|
||||
<v-btn size="small" variant="text" color="error" @click="deleteJob(item)">
|
||||
{{ tm('actions.delete') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
|
||||
@@ -116,6 +116,21 @@
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="showAnnouncement" class="px-4 mb-4">
|
||||
<v-col cols="12">
|
||||
<v-card class="welcome-card pa-6" elevation="0" border>
|
||||
<div class="mb-4 text-h3 font-weight-bold">
|
||||
{{ tm('announcement.title') }}
|
||||
</div>
|
||||
<MarkdownRender
|
||||
:content="welcomeAnnouncement"
|
||||
:typewriter="false"
|
||||
class="welcome-announcement-markdown markdown-content"
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<AddNewPlatform v-model:show="showAddPlatformDialog" :metadata="platformMetadata" :config_data="platformConfigData"
|
||||
@@ -129,12 +144,16 @@ import { computed, ref, watch, onMounted } from 'vue';
|
||||
import axios from 'axios';
|
||||
import AddNewPlatform from '@/components/platform/AddNewPlatform.vue';
|
||||
import ProviderConfigDialog from '@/components/chat/ProviderConfigDialog.vue';
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import { useI18n, useModuleI18n } from '@/i18n/composables';
|
||||
import { useToast } from '@/utils/toast';
|
||||
import { MarkdownRender } from 'markstream-vue';
|
||||
import 'markstream-vue/index.css';
|
||||
import 'highlight.js/styles/github.css';
|
||||
|
||||
type StepState = 'pending' | 'completed' | 'skipped';
|
||||
|
||||
const { tm } = useModuleI18n('features/welcome');
|
||||
const { locale } = useI18n();
|
||||
const { success: showSuccess, error: showError } = useToast();
|
||||
|
||||
const showAddPlatformDialog = ref(false);
|
||||
@@ -148,6 +167,38 @@ const providerCountBeforeOpen = ref(0);
|
||||
|
||||
const platformStepState = ref<StepState>('pending');
|
||||
const providerStepState = ref<StepState>('pending');
|
||||
const welcomeAnnouncementRaw = ref<unknown>(null);
|
||||
|
||||
function resolveWelcomeAnnouncement(raw: unknown, currentLocale: string) {
|
||||
if (typeof raw === 'string') {
|
||||
return raw.trim();
|
||||
}
|
||||
|
||||
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const localeMap = raw as Record<string, unknown>;
|
||||
const normalized = currentLocale.replace('-', '_');
|
||||
const preferredKeys =
|
||||
normalized.startsWith('zh')
|
||||
? [normalized, 'zh_CN', 'zh-CN', 'zh', 'en_US', 'en-US', 'en']
|
||||
: [normalized, 'en_US', 'en-US', 'en', 'zh_CN', 'zh-CN', 'zh'];
|
||||
|
||||
for (const key of preferredKeys) {
|
||||
const value = localeMap[key];
|
||||
if (typeof value === 'string' && value.trim().length > 0) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
const welcomeAnnouncement = computed(() =>
|
||||
resolveWelcomeAnnouncement(welcomeAnnouncementRaw.value, locale.value)
|
||||
);
|
||||
const showAnnouncement = computed(() => welcomeAnnouncement.value.length > 0);
|
||||
|
||||
const springFestivalDates: Record<number, string> = {
|
||||
2025: '01-29',
|
||||
@@ -285,7 +336,19 @@ async function syncDefaultConfigProviderIfNeeded() {
|
||||
showSuccess(tm('onboard.providerDefaultUpdated', { id: targetProviderId }));
|
||||
}
|
||||
|
||||
async function loadWelcomeAnnouncement() {
|
||||
try {
|
||||
const res = await axios.get('https://cloud.astrbot.app/api/v1/announcement');
|
||||
welcomeAnnouncementRaw.value = res?.data?.data?.notice?.welcome_page ?? null;
|
||||
} catch (e) {
|
||||
welcomeAnnouncementRaw.value = null;
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadWelcomeAnnouncement();
|
||||
|
||||
try {
|
||||
await loadPlatformConfigBase();
|
||||
if ((platformConfigData.value.platform || []).length > 0) {
|
||||
@@ -363,4 +426,8 @@ watch(showProviderDialog, async (visible, wasVisible) => {
|
||||
.welcome-card {
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.welcome-announcement-markdown {
|
||||
line-height: 1.7;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "astrbot-desktop",
|
||||
"version": "4.17.3",
|
||||
"version": "4.17.4",
|
||||
"description": "AstrBot desktop wrapper",
|
||||
"private": true,
|
||||
"main": "main.js",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.17.3"
|
||||
version = "4.17.4"
|
||||
description = "Easy-to-use multi-platform LLM chatbot and development framework"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
||||
Reference in New Issue
Block a user