fix(dashboard): complete i18n support for shared components (#4327)

* fix(dashboard): complete i18n support for shared components

- Replace hardcoded Chinese strings with i18n translations in:
  - PluginSetSelector.vue
  - ProviderSelector.vue
  - PersonaSelector.vue
  - KnowledgeBaseSelector.vue
  - T2ITemplateEditor.vue
  - AstrBotConfigV4.vue
  - ConfigItemRenderer.vue
  - ProxySelector.vue
  - ListConfigItem.vue

- Add missing translations to locale files:
  - core/shared.json: personaSelector, t2iTemplateEditor
  - core/common.json: autoDetect
  - features/settings.json: network.proxySelector

- Change prop defaults from hardcoded Chinese to empty strings,
  allowing components to use i18n fallback translations

* fix(i18n): 修正插件选择器标签的翻译格式,添加冒号

* fix(deployment): 添加持久化 machine-id PVC 和初始化容器,优化资源限制
This commit is contained in:
RC-CHN
2026-01-05 09:45:28 +08:00
committed by GitHub
parent 7f5cc7cf1a
commit 9bcf9bf2a0
17 changed files with 265 additions and 80 deletions
@@ -233,12 +233,12 @@ function getSpecialSubtype(value) {
<div v-if="createSelectorModel(itemKey).value && createSelectorModel(itemKey).value.length > 0"
class="selected-plugins-full-width">
<div class="plugins-header">
<small class="text-grey">已选择的插件</small>
<small class="text-grey">{{ t('core.shared.pluginSetSelector.selectedPluginsLabel') }}</small>
</div>
<div class="d-flex flex-wrap ga-2 mt-2">
<v-chip v-for="plugin in (createSelectorModel(itemKey).value || [])" :key="plugin" size="small" label
color="primary" variant="outlined">
{{ plugin === '*' ? '所有插件' : plugin }}
{{ plugin === '*' ? t('core.shared.pluginSetSelector.allPluginsLabel') : plugin }}
</v-chip>
</div>
</div>
@@ -20,13 +20,13 @@
</template>
<template v-else-if="itemMeta?._special === 'provider_pool'">
<ProviderSelector :model-value="modelValue" @update:model-value="emitUpdate" :provider-type="'chat_completion'"
button-text="选择提供商池..." />
:button-text="t('core.shared.providerSelector.selectProviderPool')" />
</template>
<template v-else-if="itemMeta?._special === 'select_persona'">
<PersonaSelector :model-value="modelValue" @update:model-value="emitUpdate" />
</template>
<template v-else-if="itemMeta?._special === 'persona_pool'">
<PersonaSelector :model-value="modelValue" @update:model-value="emitUpdate" button-text="选择人格池..." />
<PersonaSelector :model-value="modelValue" @update:model-value="emitUpdate" :button-text="t('core.shared.personaSelector.selectPersonaPool')" />
</template>
<template v-else-if="itemMeta?._special === 'select_knowledgebase'">
<KnowledgeBaseSelector :model-value="modelValue" @update:model-value="emitUpdate" />
@@ -56,7 +56,7 @@
:loading="loading"
class="ml-2"
>
自动检测
{{ t('core.common.autoDetect') }}
</v-btn>
</div>
</template>
@@ -20,7 +20,7 @@
</div>
</div>
<v-btn size="small" color="primary" variant="tonal" @click="openDialog" style="flex-shrink: 0;">
{{ buttonText }}
{{ buttonText || tm('knowledgeBaseSelector.buttonText') }}
</v-btn>
</div>
@@ -105,7 +105,7 @@ const props = defineProps({
},
buttonText: {
type: String,
default: '选择知识库...'
default: ''
}
})
@@ -175,11 +175,11 @@ const props = defineProps({
},
buttonText: {
type: String,
default: '修改'
default: ''
},
dialogTitle: {
type: String,
default: '修改列表项'
default: ''
},
maxDisplayItems: {
type: Number,
@@ -1,13 +1,13 @@
<template>
<div class="d-flex align-center justify-space-between">
<span v-if="!modelValue" style="color: rgb(var(--v-theme-primaryText));">
未选择
{{ tm('personaSelector.notSelected') }}
</span>
<span v-else>
{{ modelValue === 'default' ? '默认人格' : modelValue }}
{{ modelValue === 'default' ? tm('personaSelector.defaultPersona') : modelValue }}
</span>
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
{{ buttonText }}
{{ buttonText || tm('personaSelector.buttonText') }}
</v-btn>
</div>
@@ -15,7 +15,7 @@
<v-dialog v-model="dialog" max-width="600px">
<v-card>
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
选择人格
{{ tm('personaSelector.dialogTitle') }}
</v-card-title>
<v-card-text class="pa-2" style="max-height: 400px; overflow-y: auto;">
@@ -30,9 +30,9 @@
:active="selectedPersona === persona.persona_id"
rounded="md"
class="ma-1">
<v-list-item-title>{{ persona.persona_id === 'default' ? '默认人格' : persona.persona_id }}</v-list-item-title>
<v-list-item-title>{{ persona.persona_id === 'default' ? tm('personaSelector.defaultPersona') : persona.persona_id }}</v-list-item-title>
<v-list-item-subtitle>
{{ persona.system_prompt ? persona.system_prompt.substring(0, 50) + '...' : '无描述' }}
{{ persona.system_prompt ? persona.system_prompt.substring(0, 50) + '...' : tm('personaSelector.noDescription') }}
</v-list-item-subtitle>
<template v-slot:append>
@@ -43,21 +43,21 @@
<div v-else-if="!loading && personaList.length === 0" class="text-center py-8">
<v-icon size="64" color="grey-lighten-1">mdi-account-off</v-icon>
<p class="text-grey mt-4">暂无可用的人格</p>
<p class="text-grey mt-4">{{ tm('personaSelector.noPersonas') }}</p>
</div>
</v-card-text>
<v-card-actions class="pa-4">
<v-btn variant="text" color="primary" prepend-icon="mdi-plus" @click="openCreatePersona">
创建新人格
{{ tm('personaSelector.createPersona') }}
</v-btn>
<v-spacer></v-spacer>
<v-btn variant="text" @click="cancelSelection">取消</v-btn>
<v-btn
color="primary"
<v-btn variant="text" @click="cancelSelection">{{ t('core.common.cancel') }}</v-btn>
<v-btn
color="primary"
@click="confirmSelection"
:disabled="!selectedPersona">
确认选择
{{ t('core.common.confirm') }}
</v-btn>
</v-card-actions>
</v-card>
@@ -78,6 +78,7 @@
import { ref, watch } from 'vue'
import axios from 'axios'
import PersonaForm from './PersonaForm.vue'
import { useI18n, useModuleI18n } from '@/i18n/composables'
const props = defineProps({
modelValue: {
@@ -86,11 +87,13 @@ const props = defineProps({
},
buttonText: {
type: String,
default: '选择人格...'
default: ''
}
})
const emit = defineEmits(['update:modelValue'])
const { t } = useI18n()
const { tm } = useModuleI18n('core.shared')
const dialog = ref(false)
const personaList = ref([])
@@ -14,7 +14,7 @@
</span>
</div>
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
{{ buttonText }}
{{ buttonText || tm('pluginSetSelector.buttonText') }}
</v-btn>
</div>
</div>
@@ -113,7 +113,7 @@ const props = defineProps({
},
buttonText: {
type: String,
default: '选择插件集合...'
default: ''
},
maxDisplayItems: {
type: Number,
@@ -7,7 +7,7 @@
{{ modelValue }}
</span>
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
{{ buttonText }}
{{ buttonText || tm('providerSelector.buttonText') }}
</v-btn>
</div>
@@ -134,7 +134,7 @@ const props = defineProps({
},
buttonText: {
type: String,
default: '选择提供商...'
default: ''
}
})
@@ -1,13 +1,13 @@
<template>
<h5>GitHub 加速</h5>
<h5>{{ tm('network.proxySelector.title') }}</h5>
<v-radio-group class="mt-2" v-model="radioValue" hide-details="true">
<v-radio label="不使用 GitHub 加速" value="0"></v-radio>
<v-radio :label="tm('network.proxySelector.noProxy')" value="0"></v-radio>
<v-radio value="1">
<template v-slot:label>
<span>使用 GitHub 加速</span>
<span>{{ tm('network.proxySelector.useProxy') }}</span>
<v-btn v-if="radioValue === '1'" class="ml-2" @click="testAllProxies" size="x-small"
variant="tonal" :loading="loadingTestingConnection">
测试代理连通性
{{ tm('network.proxySelector.testConnection') }}
</v-btn>
</template>
</v-radio>
@@ -20,15 +20,15 @@
<div class="d-flex align-center">
<span class="mr-2">{{ proxy }}</span>
<div v-if="proxyStatus[idx]">
<v-chip
:color="proxyStatus[idx].available ? 'success' : 'error'"
size="x-small"
<v-chip
:color="proxyStatus[idx].available ? 'success' : 'error'"
size="x-small"
class="mr-1">
{{ proxyStatus[idx].available ? '可用' : '不可用' }}
{{ proxyStatus[idx].available ? tm('network.proxySelector.available') : tm('network.proxySelector.unavailable') }}
</v-chip>
<v-chip
v-if="proxyStatus[idx].available"
color="info"
<v-chip
v-if="proxyStatus[idx].available"
color="info"
size="x-small">
{{ proxyStatus[idx].latency }}ms
</v-chip>
@@ -36,10 +36,10 @@
</div>
</template>
</v-radio>
<v-radio color="primary" value="-1" label="自定义">
<v-radio color="primary" value="-1" :label="tm('network.proxySelector.custom')">
<template v-slot:label v-if="githubProxyRadioControl === '-1'">
<v-text-field density="compact" v-model="selectedGitHubProxy" variant="outlined"
style="width: 100vw;" placeholder="自定义" hide-details="true">
style="width: 100vw;" :placeholder="tm('network.proxySelector.custom')" hide-details="true">
</v-text-field>
</template>
</v-radio>
@@ -1,32 +1,32 @@
<template>
<v-dialog v-model="dialog" max-width="1400px" persistent scrollable>
<template v-slot:activator="{ props }">
<v-btn
<v-btn
v-bind="props"
variant="outlined"
color="primary"
variant="outlined"
color="primary"
size="small"
:loading="loading"
>
自定义 T2I 模板
{{ tm('t2iTemplateEditor.buttonText') }}
</v-btn>
</template>
<v-card>
<v-card-title class="d-flex align-center justify-space-between">
<span>自定义文转图 HTML 模板</span>
<span>{{ tm('t2iTemplateEditor.dialogTitle') }}</span>
<v-spacer></v-spacer>
<div class="d-flex align-center gap-2" style="width: 60%">
<v-text-field
v-if="isCreatingNew"
v-model="editingName"
label="输入新模板名称"
:label="tm('t2iTemplateEditor.newTemplateNameLabel')"
density="compact"
hide-details
variant="outlined"
class="flex-grow-1"
autofocus
:rules="[v => !!v || '名称不能为空']"
:rules="[v => !!v || tm('t2iTemplateEditor.nameRequired')]"
></v-text-field>
<v-select
v-else
@@ -34,7 +34,7 @@
:items="templates"
item-title="name"
item-value="name"
label="选择模板"
:label="tm('t2iTemplateEditor.selectTemplateLabel')"
density="compact"
hide-details
variant="outlined"
@@ -51,7 +51,7 @@
size="small"
class="ml-2"
>
已应用
{{ tm('t2iTemplateEditor.applied') }}
</v-chip>
<v-btn
v-else
@@ -62,7 +62,7 @@
@click.stop="setActiveTemplate(item.raw.name)"
:loading="applyLoading"
>
应用
{{ tm('t2iTemplateEditor.apply') }}
</v-btn>
</template>
</v-list-item>
@@ -83,7 +83,7 @@
<!-- 左侧编辑器 -->
<v-col cols="6" class="d-flex flex-column">
<v-toolbar density="compact" color="surface-variant">
<v-toolbar-title class="text-subtitle-2">模板编辑器</v-toolbar-title>
<v-toolbar-title class="text-subtitle-2">{{ tm('t2iTemplateEditor.templateEditor') }}</v-toolbar-title>
<v-spacer></v-spacer>
<div class="d-flex align-center pa-1" style="border: 1px solid rgba(0,0,0,0.1); border-radius: 8px;">
<v-btn
@@ -93,7 +93,7 @@
color="success"
>
<v-icon left>mdi-plus</v-icon>
新建
{{ tm('t2iTemplateEditor.new') }}
</v-btn>
<v-divider vertical class="mx-1"></v-divider>
<v-btn
@@ -103,7 +103,7 @@
:loading="resetLoading"
color="warning"
>
重置Base
{{ tm('t2iTemplateEditor.resetBase') }}
</v-btn>
<v-btn
variant="text"
@@ -112,7 +112,7 @@
color="error"
:disabled="isCreatingNew || selectedTemplate === 'base' || !selectedTemplate"
>
删除
{{ tm('t2iTemplateEditor.delete') }}
</v-btn>
<v-divider vertical class="mx-1"></v-divider>
<v-btn
@@ -123,7 +123,7 @@
color="primary"
:disabled="(isCreatingNew && !editingName) || (!isCreatingNew && !selectedTemplate)"
>
保存
{{ tm('t2iTemplateEditor.save') }}
</v-btn>
</div>
</v-toolbar>
@@ -141,15 +141,15 @@
<!-- 右侧预览 -->
<v-col cols="6" class="d-flex flex-column">
<v-toolbar density="compact" color="surface-variant">
<v-toolbar-title class="text-subtitle-2">实时预览(可能有差异)</v-toolbar-title>
<v-toolbar-title class="text-subtitle-2">{{ tm('t2iTemplateEditor.livePreview') }}</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn
variant="text"
size="small"
<v-btn
variant="text"
size="small"
@click="refreshPreview"
:loading="previewLoading"
>
刷新预览
{{ tm('t2iTemplateEditor.refreshPreview') }}
</v-btn>
</v-toolbar>
<div class="flex-grow-1 preview-container">
@@ -168,7 +168,7 @@
<v-col>
<div class="text-caption text-grey">
<v-icon size="16" class="mr-1">mdi-information</v-icon>
支持 jinja2 语法可用变量<code> text | safe </code>要渲染的文本, <code> version </code>AstrBot 版本
{{ tm('t2iTemplateEditor.syntaxHint') }}
</div>
</v-col>
<v-col cols="auto">
@@ -176,7 +176,7 @@
variant="text"
@click="closeDialog"
>
取消
{{ t('core.common.cancel') }}
</v-btn>
<v-btn
color="primary"
@@ -184,7 +184,7 @@
:loading="saveLoading"
:disabled="isCreatingNew || !selectedTemplate"
>
保存应用当前编辑模板
{{ tm('t2iTemplateEditor.saveAndApply') }}
</v-btn>
</v-col>
</v-row>
@@ -194,14 +194,14 @@
<!-- 确认重置对话框 -->
<v-dialog v-model="resetDialog" max-width="400px">
<v-card>
<v-card-title>确认重置</v-card-title>
<v-card-title>{{ tm('t2iTemplateEditor.confirmReset') }}</v-card-title>
<v-card-text>
确定要将 'base' 模板恢复为默认内容吗当前编辑器中的任何未保存更改将丢失此操作无法撤销
{{ tm('t2iTemplateEditor.confirmResetMessage') }}
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="resetDialog = false">取消</v-btn>
<v-btn color="warning" @click="confirmReset" :loading="resetLoading">确认重置</v-btn>
<v-btn text @click="resetDialog = false">{{ t('core.common.cancel') }}</v-btn>
<v-btn color="warning" @click="confirmReset" :loading="resetLoading">{{ tm('t2iTemplateEditor.confirmResetButton') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@@ -209,14 +209,14 @@
<!-- 删除确认对话框 -->
<v-dialog v-model="deleteDialog" max-width="400px">
<v-card>
<v-card-title>确认删除</v-card-title>
<v-card-title>{{ tm('t2iTemplateEditor.confirmDelete') }}</v-card-title>
<v-card-text>
确定要删除模板 '{{ selectedTemplate }}' 此操作无法撤销
{{ tm('t2iTemplateEditor.confirmDeleteMessage', { name: selectedTemplate }) }}
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="deleteDialog = false">取消</v-btn>
<v-btn color="error" @click="confirmDelete" :loading="saveLoading">确认删除</v-btn>
<v-btn text @click="deleteDialog = false">{{ t('core.common.cancel') }}</v-btn>
<v-btn color="error" @click="confirmDelete" :loading="saveLoading">{{ tm('t2iTemplateEditor.confirmDeleteButton') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@@ -224,14 +224,14 @@
<!-- 保存并应用确认对话框 -->
<v-dialog v-model="applyAndCloseDialog" max-width="500px">
<v-card>
<v-card-title>确认操作</v-card-title>
<v-card-title>{{ tm('t2iTemplateEditor.confirmAction') }}</v-card-title>
<v-card-text>
确定要保存对 '{{ selectedTemplate }}' 的修改并将其设为新的活动模板吗
{{ tm('t2iTemplateEditor.confirmApplyMessage', { name: selectedTemplate }) }}
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="applyAndCloseDialog = false">取消</v-btn>
<v-btn color="primary" @click="confirmApplyAndClose" :loading="saveLoading">确认</v-btn>
<v-btn text @click="applyAndCloseDialog = false">{{ t('core.common.cancel') }}</v-btn>
<v-btn color="primary" @click="confirmApplyAndClose" :loading="saveLoading">{{ t('core.common.confirm') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@@ -242,10 +242,11 @@
<script setup>
import { ref, computed, nextTick, watch } from 'vue'
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
import { useI18n } from '@/i18n/composables'
import { useI18n, useModuleI18n } from '@/i18n/composables'
import axios from 'axios'
const { t } = useI18n()
const { tm } = useModuleI18n('core.shared')
// --- 响应式数据 ---
const dialog = ref(false)
@@ -35,6 +35,7 @@
"yes": "Yes",
"no": "No",
"imagePreview": "Image Preview",
"autoDetect": "Auto Detect",
"dialog": {
"confirmTitle": "Confirm Action",
"confirmMessage": "Are you sure you want to perform this action?",
@@ -28,7 +28,9 @@
"cancelSelection": "Cancel",
"noDescription": "No description",
"notActivated": "Not activated",
"note": "*System plugins and disabled plugins are not shown."
"note": "*System plugins and disabled plugins are not shown.",
"selectedPluginsLabel": "Selected Plugins:",
"allPluginsLabel": "All Plugins"
},
"providerSelector": {
"notSelected": "Not selected",
@@ -42,6 +44,45 @@
"clearSelectionSubtitle": "Clear current selection",
"unknownType": "Unknown type",
"createProvider": "Create Provider",
"manageProviders": "Provider Management"
"manageProviders": "Provider Management",
"selectProviderPool": "Select Provider Pool..."
},
"personaSelector": {
"notSelected": "Not selected",
"defaultPersona": "Default Persona",
"buttonText": "Select Persona...",
"dialogTitle": "Select Persona",
"noDescription": "No description",
"noPersonas": "No personas available",
"createPersona": "Create New Persona",
"cancelSelection": "Cancel",
"confirmSelection": "Confirm Selection",
"selectPersonaPool": "Select Persona Pool..."
},
"t2iTemplateEditor": {
"buttonText": "Customize T2I Template",
"dialogTitle": "Customize Text-to-Image HTML Template",
"newTemplateNameLabel": "Enter new template name",
"nameRequired": "Name is required",
"selectTemplateLabel": "Select Template",
"applied": "Applied",
"apply": "Apply",
"templateEditor": "Template Editor",
"new": "New",
"resetBase": "Reset Base",
"delete": "Delete",
"save": "Save",
"livePreview": "Live Preview (may differ)",
"refreshPreview": "Refresh Preview",
"syntaxHint": "Supports jinja2 syntax. Available variables: text | safe (text to render), version (AstrBot version)",
"saveAndApply": "Save and Apply Current Template",
"confirmReset": "Confirm Reset",
"confirmResetMessage": "Are you sure you want to reset the 'base' template to default content? Any unsaved changes in the editor will be lost. This action cannot be undone.",
"confirmResetButton": "Confirm Reset",
"confirmDelete": "Confirm Delete",
"confirmDeleteMessage": "Are you sure you want to delete template '{name}'? This action cannot be undone.",
"confirmDeleteButton": "Confirm Delete",
"confirmAction": "Confirm Action",
"confirmApplyMessage": "Are you sure you want to save changes to '{name}' and set it as the active template?"
}
}
@@ -5,6 +5,15 @@
"title": "GitHub Proxy Address",
"subtitle": "Set the GitHub proxy address used when downloading plugins or updating AstrBot. This is effective in mainland China's network environment. Can be customized, input takes effect in real time. All addresses do not guarantee stability. If errors occur when updating plugins/projects, please first check if the proxy address is working properly.",
"label": "Select GitHub Proxy Address"
},
"proxySelector": {
"title": "GitHub Proxy",
"noProxy": "Don't use GitHub Proxy",
"useProxy": "Use GitHub Proxy",
"testConnection": "Test Connection",
"available": "Available",
"unavailable": "Unavailable",
"custom": "Custom"
}
},
"system": {
@@ -35,6 +35,7 @@
"yes": "是",
"no": "否",
"imagePreview": "图片预览",
"autoDetect": "自动检测",
"dialog": {
"confirmTitle": "确认操作",
"confirmMessage": "你确定要执行此操作吗?",
@@ -28,7 +28,9 @@
"cancelSelection": "取消",
"noDescription": "无描述",
"notActivated": "未激活",
"note": "*不显示系统插件和已经在插件页禁用的插件。"
"note": "*不显示系统插件和已经在插件页禁用的插件。",
"selectedPluginsLabel": "已选择的插件:",
"allPluginsLabel": "所有插件"
},
"providerSelector": {
"notSelected": "未选择",
@@ -42,6 +44,45 @@
"clearSelectionSubtitle": "清除当前选择",
"unknownType": "未知类型",
"createProvider": "创建提供商",
"manageProviders": "提供商管理"
"manageProviders": "提供商管理",
"selectProviderPool": "选择提供商池..."
},
"personaSelector": {
"notSelected": "未选择",
"defaultPersona": "默认人格",
"buttonText": "选择人格...",
"dialogTitle": "选择人格",
"noDescription": "无描述",
"noPersonas": "暂无可用的人格",
"createPersona": "创建新人格",
"cancelSelection": "取消",
"confirmSelection": "确认选择",
"selectPersonaPool": "选择人格池..."
},
"t2iTemplateEditor": {
"buttonText": "自定义 T2I 模板",
"dialogTitle": "自定义文转图 HTML 模板",
"newTemplateNameLabel": "输入新模板名称",
"nameRequired": "名称不能为空",
"selectTemplateLabel": "选择模板",
"applied": "已应用",
"apply": "应用",
"templateEditor": "模板编辑器",
"new": "新建",
"resetBase": "重置Base",
"delete": "删除",
"save": "保存",
"livePreview": "实时预览(可能有差异)",
"refreshPreview": "刷新预览",
"syntaxHint": "支持 jinja2 语法。可用变量:text | safe(要渲染的文本), versionAstrBot 版本)",
"saveAndApply": "保存应用当前编辑模板",
"confirmReset": "确认重置",
"confirmResetMessage": "确定要将 'base' 模板恢复为默认内容吗?当前编辑器中的任何未保存更改将丢失。此操作无法撤销。",
"confirmResetButton": "确认重置",
"confirmDelete": "确认删除",
"confirmDeleteMessage": "确定要删除模板 '{name}' 吗?此操作无法撤销。",
"confirmDeleteButton": "确认删除",
"confirmAction": "确认操作",
"confirmApplyMessage": "确定要保存对 '{name}' 的修改,并将其设为新的活动模板吗?"
}
}
@@ -5,6 +5,15 @@
"title": "GitHub 加速地址",
"subtitle": "设置下载插件或者更新 AstrBot 时所用的 GitHub 加速地址。这在中国大陆的网络环境有效。可以自定义,输入结果实时生效。所有地址均不保证稳定性,如果在更新插件/项目时出现报错,请首先检查加速地址是否能正常使用。",
"label": "选择 GitHub 加速地址"
},
"proxySelector": {
"title": "GitHub 加速",
"noProxy": "不使用 GitHub 加速",
"useProxy": "使用 GitHub 加速",
"testConnection": "测试代理连通性",
"available": "可用",
"unavailable": "不可用",
"custom": "自定义"
}
},
"system": {
+17
View File
@@ -43,4 +43,21 @@ spec:
resources:
requests:
storage: 5Gi
# storageClassName: standard
---
# 持久化 machine-id,保持设备标识不变
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: napcat-machine-id-pvc
namespace: astrbot-ns
labels:
app: astrbot-stack
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Mi # 只需存储一个 32 字节的文件
# storageClassName: standard
+63 -1
View File
@@ -17,6 +17,32 @@ spec:
labels:
app: astrbot-stack
spec:
# 设置固定主机名,避免 Pod 重启后主机名变化触发风控
hostname: napcat-host
subdomain: astrbot-stack
# 优雅关闭时间,给 NapCat 足够时间保存状态
terminationGracePeriodSeconds: 60
# 初始化容器:首次生成随机 machine-id,后续复用
initContainers:
- name: init-machine-id
image: busybox:latest
command:
- /bin/sh
- -c
- |
# 仅在 machine-id 不存在时随机生成一个
if [ ! -f /machine-id-data/machine-id ]; then
# 使用 /dev/urandom 生成随机 UUID (32位十六进制)
cat /proc/sys/kernel/random/uuid | tr -d '-' > /machine-id-data/machine-id
echo "Machine ID generated: $(cat /machine-id-data/machine-id)"
else
echo "Machine ID exists: $(cat /machine-id-data/machine-id)"
fi
volumeMounts:
- name: machine-id-data
mountPath: /machine-id-data
containers:
- name: napcat
image: mlikiowa/napcat-docker:latest
@@ -28,9 +54,19 @@ spec:
value: "1000"
- name: MODE
value: "astrbot"
- name: TZ
value: "Asia/Shanghai"
ports:
- containerPort: 6099
name: napcat-web
# 资源限制:确保 Guaranteed QoS,减少被驱逐的可能
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "1000m"
volumeMounts:
- name: shared-data
mountPath: /AstrBot/data
@@ -38,6 +74,14 @@ spec:
mountPath: /app/napcat/config
- name: napcat-qq
mountPath: /app/.config/QQ
# 挂载持久化的 machine-id
- name: machine-id-data
mountPath: /etc/machine-id
subPath: machine-id
readOnly: true
- name: localtime
mountPath: /etc/localtime
readOnly: true
- name: astrbot
image: soulter/astrbot:latest
@@ -48,9 +92,19 @@ spec:
ports:
- containerPort: 6185
name: astrbot-web
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
volumeMounts:
- name: shared-data
mountPath: /AstrBot/data
- name: localtime
mountPath: /etc/localtime
readOnly: true
volumes:
- name: shared-data
@@ -61,4 +115,12 @@ spec:
claimName: napcat-config-pvc
- name: napcat-qq
persistentVolumeClaim:
claimName: napcat-qq-pvc
claimName: napcat-qq-pvc
# 持久化 machine-id(首次随机生成,后续复用)
- name: machine-id-data
persistentVolumeClaim:
claimName: napcat-machine-id-pvc
- name: localtime
hostPath:
path: /etc/localtime
type: File