From 8f4e99625ac9a21c3becf65a418c3aba7588a243 Mon Sep 17 00:00:00 2001 From: xunxi Date: Sat, 17 Jan 2026 18:38:27 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E5=BC=95=E5=85=A5=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/core/config/default.py | 1 + astrbot/dashboard/routes/config.py | 323 ++++++++++++++++- .../src/components/shared/AstrBotConfig.vue | 24 +- .../components/shared/ConfigItemRenderer.vue | 19 + .../src/components/shared/FileConfigItem.vue | 336 ++++++++++++++++++ .../i18n/locales/en-US/features/config.json | 14 +- .../i18n/locales/zh-CN/features/config.json | 15 +- dashboard/src/views/ExtensionPage.vue | 1 + dashboard/tsconfig.base.json | 15 + dashboard/tsconfig.dom.json | 7 + dashboard/tsconfig.json | 5 +- dashboard/tsconfig.vite-config.json | 2 +- 12 files changed, 755 insertions(+), 7 deletions(-) create mode 100644 dashboard/src/components/shared/FileConfigItem.vue create mode 100644 dashboard/tsconfig.base.json create mode 100644 dashboard/tsconfig.dom.json diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 510b162a7..730f1e249 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -3217,6 +3217,7 @@ class ChatProviderTemplate(TypedDict): "string": "", "text": "", "list": [], + "file": [], "object": {}, "template_list": [], } diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index bd2f9a264..b842cb662 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -21,7 +21,12 @@ from astrbot.core.provider import Provider from astrbot.core.provider.register import provider_registry from astrbot.core.star.star import star_registry +from astrbot.core.utils.astrbot_path import ( + get_astrbot_plugin_data_path, + get_astrbot_temp_path, +) from astrbot.core.utils.llm_metadata import LLM_METADATAS +from astrbot.core.utils.io import remove_dir from astrbot.core.utils.webhook_utils import ensure_platform_webhook_config from .route import Response, Route, RouteContext @@ -106,6 +111,16 @@ def validate(data: dict, metadata: dict = schema, path=""): _validate_template_list(value, meta, f"{path}{key}", errors, validate) continue + if meta["type"] == "file": + if not _expect_type(value, list, f"{path}{key}", errors, "list"): + continue + for idx, item in enumerate(value): + if not isinstance(item, str): + errors.append( + f"Invalid type {path}{key}[{idx}]: expected string, got {type(item).__name__}", + ) + continue + if meta["type"] == "list" and not isinstance(value, list): errors.append( f"错误的类型 {path}{key}: 期望是 list, 得到了 {type(value).__name__}", @@ -218,6 +233,8 @@ def __init__( "/config/default": ("GET", self.get_default_config), "/config/astrbot/update": ("POST", self.post_astrbot_configs), "/config/plugin/update": ("POST", self.post_plugin_configs), + "/config/plugin/file/upload": ("POST", self.upload_plugin_file), + "/config/plugin/file/delete": ("POST", self.delete_plugin_file), "/config/platform/new": ("POST", self.post_new_platform), "/config/platform/update": ("POST", self.post_update_platform), "/config/platform/delete": ("POST", self.post_delete_platform), @@ -876,6 +893,303 @@ async def post_plugin_configs(self): except Exception as e: return Response().error(str(e)).__dict__ + + def _get_plugin_metadata_by_name(self, plugin_name: str): + for plugin_md in star_registry: + if plugin_md.name == plugin_name: + return plugin_md + return None + + @staticmethod + def _get_schema_item(schema: dict | None, key_path: str) -> dict | None: + if not isinstance(schema, dict) or not key_path: + return None + if key_path in schema: + return schema.get(key_path) + + current = schema + parts = key_path.split(".") + for idx, part in enumerate(parts): + if part not in current: + return None + meta = current.get(part) + if idx == len(parts) - 1: + return meta + if not isinstance(meta, dict) or meta.get("type") != "object": + return None + current = meta.get("items", {}) + return None + + @staticmethod + def _sanitize_filename(name: str) -> str: + cleaned = os.path.basename(name).strip() + if not cleaned or cleaned in {".", ".."}: + return "" + for sep in (os.sep, os.altsep): + if sep: + cleaned = cleaned.replace(sep, "_") + return cleaned + + @staticmethod + def _sanitize_path_segment(segment: str) -> str: + cleaned = [] + for ch in segment: + if ("a" <= ch <= "z") or ("A" <= ch <= "Z") or ch.isdigit() or ch in { + "-", + "_", + }: + cleaned.append(ch) + else: + cleaned.append("_") + result = "".join(cleaned).strip("_") + return result or "_" + + @classmethod + def _config_key_to_folder(cls, key_path: str) -> str: + parts = [cls._sanitize_path_segment(p) for p in key_path.split(".") if p] + return "/".join(parts) if parts else "_" + + @staticmethod + def _normalize_rel_path(rel_path: str | None) -> str | None: + if not isinstance(rel_path, str): + return None + rel = rel_path.replace("\\", "/").lstrip("/") + if not rel: + return None + parts = [p for p in rel.split("/") if p] + if any(part in {".", ".."} for part in parts): + return None + if rel.startswith("../") or "/../" in rel: + return None + return "/".join(parts) + + @staticmethod + def _get_value_by_path(data: dict, key_path: str): + if key_path in data: + return data.get(key_path) + current = data + for part in key_path.split("."): + if not isinstance(current, dict) or part not in current: + return None + current = current.get(part) + return current + + @staticmethod + def _set_value_by_path(data: dict, key_path: str, value) -> None: + if key_path in data: + data[key_path] = value + return + current = data + parts = key_path.split(".") + for part in parts[:-1]: + if part not in current or not isinstance(current[part], dict): + current[part] = {} + current = current[part] + current[parts[-1]] = value + + @classmethod + def _collect_file_keys(cls, schema: dict, prefix: str = "") -> list[str]: + keys = [] + for key, meta in schema.items(): + if not isinstance(meta, dict): + continue + meta_type = meta.get("type") + if meta_type == "file": + keys.append(f"{prefix}{key}" if prefix else key) + elif meta_type == "object": + child_prefix = f"{prefix}{key}." if prefix else f"{key}." + keys.extend(cls._collect_file_keys(meta.get("items", {}), child_prefix)) + return keys + + def _normalize_file_list(self, value, key_path: str) -> tuple[list[str], bool]: + if value is None: + return [], False + if not isinstance(value, list): + raise ValueError(f"Invalid file list for {key_path}") + folder = self._config_key_to_folder(key_path) + expected_prefix = f"files/{folder}/" + results = [] + changed = False + for item in value: + if not isinstance(item, str): + raise ValueError(f"Invalid file entry for {key_path}") + rel = self._normalize_rel_path(item) + if not rel or not rel.startswith("files/"): + raise ValueError(f"Invalid file path: {item}") + if rel.startswith(expected_prefix): + results.append(rel) + continue + if rel.count("/") == 1: + filename = rel.split("/", 1)[1] + if not filename: + raise ValueError(f"Invalid file path: {item}") + results.append(f"{expected_prefix}{filename}") + changed = True + continue + raise ValueError(f"Invalid file path: {item}") + return results, changed + + def _apply_plugin_file_ops(self, plugin_name: str, md, post_configs: dict) -> None: + schema = getattr(md.config, "schema", None) if md and md.config else None + if not isinstance(schema, dict): + return + + file_keys = self._collect_file_keys(schema) + if not file_keys: + return + + old_config = dict(md.config) + new_file_set = set() + old_file_set = set() + + for key_path in file_keys: + new_list, new_changed = self._normalize_file_list( + self._get_value_by_path(post_configs, key_path), + key_path, + ) + if new_changed: + self._set_value_by_path(post_configs, key_path, new_list) + old_list, _ = self._normalize_file_list( + self._get_value_by_path(old_config, key_path), + key_path, + ) + new_file_set.update(new_list) + old_file_set.update(old_list) + + plugin_data_dir = os.path.abspath( + os.path.join(get_astrbot_plugin_data_path(), plugin_name), + ) + staging_root = os.path.abspath( + os.path.join(get_astrbot_temp_path(), "plugin_file_uploads", plugin_name), + ) + + for rel_path in sorted(new_file_set): + final_path = os.path.abspath(os.path.join(plugin_data_dir, rel_path)) + if not final_path.startswith(plugin_data_dir + os.sep): + raise ValueError(f"Invalid file path: {rel_path}") + staged_path = os.path.abspath(os.path.join(staging_root, rel_path)) + if not staged_path.startswith(staging_root + os.sep): + raise ValueError(f"Invalid staged path: {rel_path}") + if os.path.exists(staged_path): + os.makedirs(os.path.dirname(final_path), exist_ok=True) + os.replace(staged_path, final_path) + continue + legacy_path = os.path.join( + plugin_data_dir, + "files", + os.path.basename(rel_path), + ) + if os.path.isfile(legacy_path): + os.makedirs(os.path.dirname(final_path), exist_ok=True) + os.replace(legacy_path, final_path) + continue + if not os.path.exists(final_path): + raise ValueError(f"Missing staged file: {rel_path}") + + retained = new_file_set + for rel_path in sorted(old_file_set - retained): + final_path = os.path.abspath(os.path.join(plugin_data_dir, rel_path)) + if not final_path.startswith(plugin_data_dir + os.sep): + continue + if os.path.isfile(final_path): + os.remove(final_path) + continue + legacy_path = os.path.join( + plugin_data_dir, + "files", + os.path.basename(rel_path), + ) + if os.path.isfile(legacy_path): + os.remove(legacy_path) + + if os.path.isdir(staging_root): + remove_dir(staging_root) + + async def upload_plugin_file(self): + plugin_name = request.args.get("plugin_name") + key_path = request.args.get("key") + if not plugin_name or not key_path: + return Response().error("Missing plugin_name or key parameter").__dict__ + + md = self._get_plugin_metadata_by_name(plugin_name) + if not md or not md.config: + return Response().error( + f"Plugin {plugin_name} not found or has no config", + ).__dict__ + + meta = self._get_schema_item(md.config.schema, key_path) + if not meta or meta.get("type") != "file": + return Response().error("Config item not found or not file type").__dict__ + + file_types = meta.get("file_types") + allowed_exts = [] + if isinstance(file_types, list): + allowed_exts = [str(ext).lstrip(".").lower() for ext in file_types if str(ext).strip()] + + files = await request.files + if not files: + return Response().error("No files uploaded").__dict__ + + staging_root = os.path.join( + get_astrbot_temp_path(), + "plugin_file_uploads", + plugin_name, + ) + os.makedirs(staging_root, exist_ok=True) + + uploaded = [] + folder = self._config_key_to_folder(key_path) + errors = [] + for file in files.values(): + filename = self._sanitize_filename(file.filename or "") + if not filename: + errors.append("Invalid filename") + continue + + ext = os.path.splitext(filename)[1].lstrip(".").lower() + if allowed_exts and ext not in allowed_exts: + errors.append(f"Unsupported file type: {filename}") + continue + + rel_path = f"files/{folder}/{filename}" + save_path = os.path.join(staging_root, rel_path) + os.makedirs(os.path.dirname(save_path), exist_ok=True) + await file.save(save_path) + uploaded.append(rel_path) + + if not uploaded: + return Response().error( + "Upload failed: " + ", ".join(errors) if errors else "Upload failed", + ).__dict__ + + return Response().ok({"uploaded": uploaded, "errors": errors}).__dict__ + + async def delete_plugin_file(self): + plugin_name = request.args.get("plugin_name") + if not plugin_name: + return Response().error("Missing plugin_name parameter").__dict__ + + data = await request.get_json() + rel_path = data.get("path") if isinstance(data, dict) else None + rel_path = self._normalize_rel_path(rel_path) + if not rel_path or not rel_path.startswith("files/"): + return Response().error("Invalid path parameter").__dict__ + + md = self._get_plugin_metadata_by_name(plugin_name) + if not md: + return Response().error(f"Plugin {plugin_name} not found").__dict__ + + staging_root = os.path.abspath( + os.path.join(get_astrbot_temp_path(), "plugin_file_uploads", plugin_name), + ) + staged_path = os.path.abspath( + os.path.normpath(os.path.join(staging_root, rel_path)), + ) + if staged_path.startswith(staging_root + os.sep) and os.path.isfile(staged_path): + os.remove(staged_path) + + return Response().ok(None, "Deletion staged").__dict__ + async def post_new_platform(self): new_platform_config = await request.json @@ -1132,6 +1446,13 @@ async def _save_plugin_configs(self, post_configs: dict, plugin_name: str): raise ValueError(f"插件 {plugin_name} 没有注册配置") try: - save_config(post_configs, md.config) + errors, post_configs = validate_config( + post_configs, getattr(md.config, "schema", {}), is_core=False + ) + if errors: + raise ValueError(f"格式校验未通过: {errors}") + + self._apply_plugin_file_ops(plugin_name, md, post_configs) + md.config.save_config(post_configs) except Exception as e: raise e diff --git a/dashboard/src/components/shared/AstrBotConfig.vue b/dashboard/src/components/shared/AstrBotConfig.vue index 1590f384c..22dc03807 100644 --- a/dashboard/src/components/shared/AstrBotConfig.vue +++ b/dashboard/src/components/shared/AstrBotConfig.vue @@ -20,6 +20,14 @@ const props = defineProps({ type: String, required: true }, + pluginName: { + type: String, + default: '' + }, + pathPrefix: { + type: String, + default: '' + }, isEditing: { type: Boolean, default: false @@ -103,6 +111,10 @@ function shouldShowItem(itemMeta, itemKey) { return true } +function getItemPath(key) { + return props.pathPrefix ? `${props.pathPrefix}.${key}` : key +} + function hasVisibleItemsAfter(items, currentIndex) { const itemEntries = Object.entries(items) @@ -150,7 +162,13 @@ function hasVisibleItemsAfter(items, currentIndex) {
- +
@@ -205,6 +223,8 @@ function hasVisibleItemsAfter(items, currentIndex) { diff --git a/dashboard/src/components/shared/ConfigItemRenderer.vue b/dashboard/src/components/shared/ConfigItemRenderer.vue index 23b8fe0bc..5f2341ee7 100644 --- a/dashboard/src/components/shared/ConfigItemRenderer.vue +++ b/dashboard/src/components/shared/ConfigItemRenderer.vue @@ -178,6 +178,16 @@ hide-details > + + import { VueMonacoEditor } from '@guolao/vue-monaco-editor' import ListConfigItem from './ListConfigItem.vue' +import FileConfigItem from './FileConfigItem.vue' import ObjectEditor from './ObjectEditor.vue' import ProviderSelector from './ProviderSelector.vue' import PersonaSelector from './PersonaSelector.vue' @@ -225,6 +236,14 @@ const props = defineProps({ type: Object, default: null }, + pluginName: { + type: String, + default: '' + }, + configKey: { + type: String, + default: '' + }, loading: { type: Boolean, default: false diff --git a/dashboard/src/components/shared/FileConfigItem.vue b/dashboard/src/components/shared/FileConfigItem.vue new file mode 100644 index 000000000..2e0d7adcf --- /dev/null +++ b/dashboard/src/components/shared/FileConfigItem.vue @@ -0,0 +1,336 @@ + + + + + diff --git a/dashboard/src/i18n/locales/en-US/features/config.json b/dashboard/src/i18n/locales/en-US/features/config.json index c510b6eaa..b8ded720f 100644 --- a/dashboard/src/i18n/locales/en-US/features/config.json +++ b/dashboard/src/i18n/locales/en-US/features/config.json @@ -89,6 +89,18 @@ }, "codeEditor": { "title": "Edit Configuration File" + }, + "fileUpload": { + "button": "Manage Files", + "dialogTitle": "Uploaded Files", + "dropzone": "Upload new file", + "allowedTypes": "Allowed types: {types}", + "empty": "No files uploaded", + "uploadSuccess": "Uploaded {count} files", + "uploadFailed": "Upload failed", + "deleteSuccess": "Deleted file", + "deleteFailed": "Delete failed", + "fileCount": "Files: {count}", + "done": "Done" } } - diff --git a/dashboard/src/i18n/locales/zh-CN/features/config.json b/dashboard/src/i18n/locales/zh-CN/features/config.json index 0be423ef1..2334f1b46 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/config.json +++ b/dashboard/src/i18n/locales/zh-CN/features/config.json @@ -89,5 +89,18 @@ }, "codeEditor": { "title": "编辑配置文件" + }, + "fileUpload": { + "button": "Manage Files", + "dialogTitle": "Uploaded Files", + "dropzone": "上传新文件", + "allowedTypes": "Allowed types: {types}", + "empty": "No files uploaded", + "uploadSuccess": "Uploaded {count} files", + "uploadFailed": "Upload failed", + "deleteSuccess": "Deleted file", + "deleteFailed": "Delete failed", + "fileCount": "Files: {count}", + "done": "完成" } -} \ No newline at end of file +} diff --git a/dashboard/src/views/ExtensionPage.vue b/dashboard/src/views/ExtensionPage.vue index c84862f2d..3ec6fca37 100644 --- a/dashboard/src/views/ExtensionPage.vue +++ b/dashboard/src/views/ExtensionPage.vue @@ -2138,6 +2138,7 @@ watch(isListView, (newVal) => { :metadata="extension_config.metadata" :iterable="extension_config.config" :metadataKey="curr_namespace" + :pluginName="curr_namespace" />

{{ tm("dialogs.config.noConfig") }}

diff --git a/dashboard/tsconfig.base.json b/dashboard/tsconfig.base.json new file mode 100644 index 000000000..f91fb491c --- /dev/null +++ b/dashboard/tsconfig.base.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "jsx": "preserve", + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "useDefineForClassFields": true + } +} diff --git a/dashboard/tsconfig.dom.json b/dashboard/tsconfig.dom.json new file mode 100644 index 000000000..42d8d6d2f --- /dev/null +++ b/dashboard/tsconfig.dom.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "types": ["vite/client"] + } +} diff --git a/dashboard/tsconfig.json b/dashboard/tsconfig.json index 7820a40b1..c3cc8ff90 100644 --- a/dashboard/tsconfig.json +++ b/dashboard/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@vue/tsconfig/tsconfig.dom.json", + "extends": "./tsconfig.dom.json", "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "src/types/.d.ts"], "compilerOptions": { "ignoreDeprecations": "5.0", @@ -7,7 +7,8 @@ "paths": { "@/*": ["./src/*"] }, - "allowJs": true + "allowJs": true, + "noEmit": true }, "references": [ diff --git a/dashboard/tsconfig.vite-config.json b/dashboard/tsconfig.vite-config.json index a3d4b2151..d325881b0 100644 --- a/dashboard/tsconfig.vite-config.json +++ b/dashboard/tsconfig.vite-config.json @@ -1,5 +1,5 @@ { - "extends": "@vue/tsconfig/tsconfig.json", + "extends": "./tsconfig.base.json", "include": ["vite.config.*"], "compilerOptions": { "composite": true, From 9ef4e63c1149b49a41561658f47285347a6ce10d Mon Sep 17 00:00:00 2001 From: xunxi Date: Sat, 17 Jan 2026 18:44:18 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E6=94=AF=E6=8C=81i18n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../i18n/locales/zh-CN/features/config.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/dashboard/src/i18n/locales/zh-CN/features/config.json b/dashboard/src/i18n/locales/zh-CN/features/config.json index 2334f1b46..cd8cc9823 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/config.json +++ b/dashboard/src/i18n/locales/zh-CN/features/config.json @@ -91,16 +91,16 @@ "title": "编辑配置文件" }, "fileUpload": { - "button": "Manage Files", - "dialogTitle": "Uploaded Files", + "button": "管理文件", + "dialogTitle": "已上传文件", "dropzone": "上传新文件", - "allowedTypes": "Allowed types: {types}", - "empty": "No files uploaded", - "uploadSuccess": "Uploaded {count} files", - "uploadFailed": "Upload failed", - "deleteSuccess": "Deleted file", - "deleteFailed": "Delete failed", - "fileCount": "Files: {count}", + "allowedTypes": "允许类型:{types}", + "empty": "暂无已上传文件", + "uploadSuccess": "已上传 {count} 个文件", + "uploadFailed": "上传失败", + "deleteSuccess": "已删除文件", + "deleteFailed": "删除失败", + "fileCount": "文件:{count}", "done": "完成" } } From dabbe701e08abf08b92ecf444ce250eadfb167bf Mon Sep 17 00:00:00 2001 From: xunxi Date: Sat, 17 Jan 2026 19:16:56 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=A4=A7=E5=B0=8F=E9=99=90=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/dashboard/routes/config.py | 11 +++++++++++ .../src/components/shared/FileConfigItem.vue | 17 ++++++++++++++++- .../src/i18n/locales/en-US/features/config.json | 1 + .../src/i18n/locales/zh-CN/features/config.json | 1 + 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index b842cb662..bb1200520 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -31,6 +31,8 @@ from .route import Response, Route, RouteContext +MAX_FILE_BYTES = 500 * 1024 * 1024 + def try_cast(value: Any, type_: str): if type_ == "int": @@ -1146,6 +1148,11 @@ async def upload_plugin_file(self): errors.append("Invalid filename") continue + file_size = getattr(file, "content_length", None) + if isinstance(file_size, int) and file_size > MAX_FILE_BYTES: + errors.append(f"File too large: {filename}") + continue + ext = os.path.splitext(filename)[1].lstrip(".").lower() if allowed_exts and ext not in allowed_exts: errors.append(f"Unsupported file type: {filename}") @@ -1155,6 +1162,10 @@ async def upload_plugin_file(self): save_path = os.path.join(staging_root, rel_path) os.makedirs(os.path.dirname(save_path), exist_ok=True) await file.save(save_path) + if os.path.isfile(save_path) and os.path.getsize(save_path) > MAX_FILE_BYTES: + os.remove(save_path) + errors.append(f"File too large: {filename}") + continue uploaded.append(rel_path) if not uploaded: diff --git a/dashboard/src/components/shared/FileConfigItem.vue b/dashboard/src/components/shared/FileConfigItem.vue index 2e0d7adcf..c1adfa886 100644 --- a/dashboard/src/components/shared/FileConfigItem.vue +++ b/dashboard/src/components/shared/FileConfigItem.vue @@ -107,6 +107,8 @@ const dialog = ref(false) const isDragging = ref(false) const fileInput = ref(null) const uploading = ref(false) +const MAX_FILE_BYTES = 500 * 1024 * 1024 +const MAX_FILE_MB = 500 const fileList = computed({ get: () => (Array.isArray(props.modelValue) ? props.modelValue : []), @@ -165,10 +167,23 @@ const uploadFiles = async (files) => { return } + const oversized = files.filter((file) => file.size > MAX_FILE_BYTES) + if (oversized.length > 0) { + oversized.forEach((file) => { + toast.warning( + tm('fileUpload.fileTooLarge', { name: file.name, max: MAX_FILE_MB }) + ) + }) + } + const validFiles = files.filter((file) => file.size <= MAX_FILE_BYTES) + if (validFiles.length === 0) { + return + } + uploading.value = true try { const formData = new FormData() - files.forEach((file, index) => { + validFiles.forEach((file, index) => { formData.append(`file${index}`, file) }) diff --git a/dashboard/src/i18n/locales/en-US/features/config.json b/dashboard/src/i18n/locales/en-US/features/config.json index b8ded720f..dd3913530 100644 --- a/dashboard/src/i18n/locales/en-US/features/config.json +++ b/dashboard/src/i18n/locales/en-US/features/config.json @@ -98,6 +98,7 @@ "empty": "No files uploaded", "uploadSuccess": "Uploaded {count} files", "uploadFailed": "Upload failed", + "fileTooLarge": "File too large (max {max} MB): {name}", "deleteSuccess": "Deleted file", "deleteFailed": "Delete failed", "fileCount": "Files: {count}", diff --git a/dashboard/src/i18n/locales/zh-CN/features/config.json b/dashboard/src/i18n/locales/zh-CN/features/config.json index cd8cc9823..3e2c8ffff 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/config.json +++ b/dashboard/src/i18n/locales/zh-CN/features/config.json @@ -98,6 +98,7 @@ "empty": "暂无已上传文件", "uploadSuccess": "已上传 {count} 个文件", "uploadFailed": "上传失败", + "fileTooLarge": "文件过大(上限 {max} MB):{name}", "deleteSuccess": "已删除文件", "deleteFailed": "删除失败", "fileCount": "文件:{count}", From ec74fc23110b3925bb1db40fdb448b65f7226e25 Mon Sep 17 00:00:00 2001 From: xunxi Date: Mon, 19 Jan 2026 19:11:12 +0800 Subject: [PATCH 4/6] =?UTF-8?q?=E5=88=A0=E9=99=A4ide=E7=94=9F=E6=88=90?= =?UTF-8?q?=E7=9A=84=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dashboard/tsconfig.base.json | 15 --------------- dashboard/tsconfig.dom.json | 7 ------- dashboard/tsconfig.json | 7 +++---- dashboard/tsconfig.vite-config.json | 4 ++-- 4 files changed, 5 insertions(+), 28 deletions(-) delete mode 100644 dashboard/tsconfig.base.json delete mode 100644 dashboard/tsconfig.dom.json diff --git a/dashboard/tsconfig.base.json b/dashboard/tsconfig.base.json deleted file mode 100644 index f91fb491c..000000000 --- a/dashboard/tsconfig.base.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "Bundler", - "strict": true, - "jsx": "preserve", - "resolveJsonModule": true, - "isolatedModules": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "useDefineForClassFields": true - } -} diff --git a/dashboard/tsconfig.dom.json b/dashboard/tsconfig.dom.json deleted file mode 100644 index 42d8d6d2f..000000000 --- a/dashboard/tsconfig.dom.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "compilerOptions": { - "lib": ["DOM", "DOM.Iterable", "ESNext"], - "types": ["vite/client"] - } -} diff --git a/dashboard/tsconfig.json b/dashboard/tsconfig.json index c3cc8ff90..e83bb639c 100644 --- a/dashboard/tsconfig.json +++ b/dashboard/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "./tsconfig.dom.json", + "extends": "@vue/tsconfig/tsconfig.dom.json", "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "src/types/.d.ts"], "compilerOptions": { "ignoreDeprecations": "5.0", @@ -7,8 +7,7 @@ "paths": { "@/*": ["./src/*"] }, - "allowJs": true, - "noEmit": true + "allowJs": true }, "references": [ @@ -16,4 +15,4 @@ "path": "./tsconfig.vite-config.json" } ] -} +} \ No newline at end of file diff --git a/dashboard/tsconfig.vite-config.json b/dashboard/tsconfig.vite-config.json index d325881b0..170b576bf 100644 --- a/dashboard/tsconfig.vite-config.json +++ b/dashboard/tsconfig.vite-config.json @@ -1,9 +1,9 @@ { - "extends": "./tsconfig.base.json", + "extends": "@vue/tsconfig/tsconfig.json", "include": ["vite.config.*"], "compilerOptions": { "composite": true, "allowJs": true, "types": ["node"] } -} +} \ No newline at end of file From e34e72abd70bf45de0e05f028e7661b2c617f9de Mon Sep 17 00:00:00 2001 From: xunxi Date: Mon, 19 Jan 2026 19:25:18 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=E5=88=A0=E9=99=A4ide=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E7=94=9F=E6=88=90=E7=9A=84=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dashboard/tsconfig.json | 2 +- dashboard/tsconfig.vite-config.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dashboard/tsconfig.json b/dashboard/tsconfig.json index e83bb639c..7820a40b1 100644 --- a/dashboard/tsconfig.json +++ b/dashboard/tsconfig.json @@ -15,4 +15,4 @@ "path": "./tsconfig.vite-config.json" } ] -} \ No newline at end of file +} diff --git a/dashboard/tsconfig.vite-config.json b/dashboard/tsconfig.vite-config.json index 170b576bf..a3d4b2151 100644 --- a/dashboard/tsconfig.vite-config.json +++ b/dashboard/tsconfig.vite-config.json @@ -6,4 +6,4 @@ "allowJs": true, "types": ["node"] } -} \ No newline at end of file +} From f8cdfeca76a86086b411e1bf438a510fbca6a389 Mon Sep 17 00:00:00 2001 From: xunxi Date: Wed, 21 Jan 2026 20:54:16 +0800 Subject: [PATCH 6/6] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dapi=E6=9C=AA=E8=B0=83?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- astrbot/dashboard/routes/config.py | 1 + .../src/components/shared/FileConfigItem.vue | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index bb1200520..54d44b3a4 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -1176,6 +1176,7 @@ async def upload_plugin_file(self): return Response().ok({"uploaded": uploaded, "errors": errors}).__dict__ async def delete_plugin_file(self): + """Delete a staged upload under temp; final deletion happens on config save.""" plugin_name = request.args.get("plugin_name") if not plugin_name: return Response().error("Missing plugin_name parameter").__dict__ diff --git a/dashboard/src/components/shared/FileConfigItem.vue b/dashboard/src/components/shared/FileConfigItem.vue index c1adfa886..f6bc41806 100644 --- a/dashboard/src/components/shared/FileConfigItem.vue +++ b/dashboard/src/components/shared/FileConfigItem.vue @@ -226,6 +226,21 @@ const uploadFiles = async (files) => { const deleteFile = (filePath) => { fileList.value = fileList.value.filter((item) => item !== filePath) + + if (props.pluginName) { + axios + .post( + `/api/config/plugin/file/delete?plugin_name=${encodeURIComponent( + props.pluginName + )}`, + { path: filePath } + ) + .catch((error) => { + console.warn('Staged file delete failed:', error) + toast.warning(tm('fileUpload.deleteFailed')) + }) + } + toast.success(tm('fileUpload.deleteSuccess')) }