diff --git a/astrbot/core/astr_agent_tool_exec.py b/astrbot/core/astr_agent_tool_exec.py index 5d40f48fa..060453161 100644 --- a/astrbot/core/astr_agent_tool_exec.py +++ b/astrbot/core/astr_agent_tool_exec.py @@ -256,7 +256,7 @@ async def call_local_llm_tool( # 这里逐步执行异步生成器, 对于每个 yield 返回的 ret, 执行下面的代码 # 返回值只能是 MessageEventResult 或者 None(无返回值) _has_yielded = True - if isinstance(ret, (MessageEventResult, CommandResult)): + if isinstance(ret, MessageEventResult | CommandResult): # 如果返回值是 MessageEventResult, 设置结果并继续 event.set_result(ret) yield @@ -273,7 +273,7 @@ async def call_local_llm_tool( elif inspect.iscoroutine(ready_to_call): # 如果只是一个协程, 直接执行 ret = await ready_to_call - if isinstance(ret, (MessageEventResult, CommandResult)): + if isinstance(ret, MessageEventResult | CommandResult): event.set_result(ret) yield else: 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/core/message/components.py b/astrbot/core/message/components.py index 050e36521..280276089 100644 --- a/astrbot/core/message/components.py +++ b/astrbot/core/message/components.py @@ -567,7 +567,7 @@ def __init__(self, content: list[BaseMessageComponent], **_): async def to_dict(self): data_content = [] for comp in self.content: - if isinstance(comp, (Image, Record)): + if isinstance(comp, Image | Record): # For Image and Record segments, we convert them to base64 bs64 = await comp.convert_to_base64() data_content.append( @@ -584,7 +584,7 @@ async def to_dict(self): # For File segments, we need to handle the file differently d = await comp.to_dict() data_content.append(d) - elif isinstance(comp, (Node, Nodes)): + elif isinstance(comp, Node | Nodes): # For Node segments, we recursively convert them to dict d = await comp.to_dict() data_content.append(d) diff --git a/astrbot/core/pipeline/context_utils.py b/astrbot/core/pipeline/context_utils.py index 1f5ba43a0..9402ce3e6 100644 --- a/astrbot/core/pipeline/context_utils.py +++ b/astrbot/core/pipeline/context_utils.py @@ -48,7 +48,7 @@ async def call_handler( # 这里逐步执行异步生成器, 对于每个 yield 返回的 ret, 执行下面的代码 # 返回值只能是 MessageEventResult 或者 None(无返回值) _has_yielded = True - if isinstance(ret, (MessageEventResult, CommandResult)): + if isinstance(ret, MessageEventResult | CommandResult): # 如果返回值是 MessageEventResult, 设置结果并继续 event.set_result(ret) yield @@ -65,7 +65,7 @@ async def call_handler( elif inspect.iscoroutine(ready_to_call): # 如果只是一个协程, 直接执行 ret = await ready_to_call - if isinstance(ret, (MessageEventResult, CommandResult)): + if isinstance(ret, MessageEventResult | CommandResult): event.set_result(ret) yield else: diff --git a/astrbot/core/pipeline/preprocess_stage/stage.py b/astrbot/core/pipeline/preprocess_stage/stage.py index a69d07ffb..6544f85c1 100644 --- a/astrbot/core/pipeline/preprocess_stage/stage.py +++ b/astrbot/core/pipeline/preprocess_stage/stage.py @@ -52,7 +52,7 @@ async def process( message_chain = event.get_messages() for idx, component in enumerate(message_chain): - if isinstance(component, (Record, Image)) and component.url: + if isinstance(component, Record | Image) and component.url: for mapping in mappings: from_, to_ = mapping.split(":") from_ = from_.removesuffix("/") diff --git a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py index 43d88c5ad..a424b1731 100644 --- a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +++ b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py @@ -510,7 +510,7 @@ async def process( has_valid_message = bool(event.message_str and event.message_str.strip()) # 检查是否有图片或其他媒体内容 has_media_content = any( - isinstance(comp, (Image, File)) for comp in event.message_obj.message + isinstance(comp, Image | File) for comp in event.message_obj.message ) if ( diff --git a/astrbot/core/pipeline/scheduler.py b/astrbot/core/pipeline/scheduler.py index 5fb3034f5..8569f945a 100644 --- a/astrbot/core/pipeline/scheduler.py +++ b/astrbot/core/pipeline/scheduler.py @@ -82,7 +82,7 @@ async def execute(self, event: AstrMessageEvent): await self._process_stages(event) # 如果没有发送操作, 则发送一个空消息, 以便于后续的处理 - if isinstance(event, (WebChatMessageEvent, WecomAIBotMessageEvent)): + if isinstance(event, WebChatMessageEvent | WecomAIBotMessageEvent): await event.send(None) logger.debug("pipeline 执行完毕。") diff --git a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py index 293b462d3..3d84cbd44 100644 --- a/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py +++ b/astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py @@ -33,7 +33,7 @@ def __init__( @staticmethod async def _from_segment_to_dict(segment: BaseMessageComponent) -> dict: """修复部分字段""" - if isinstance(segment, (Image, Record)): + if isinstance(segment, Image | Record): # For Image and Record segments, we convert them to base64 bs64 = await segment.convert_to_base64() return { @@ -110,7 +110,7 @@ async def send_message( """ # 转发消息、文件消息不能和普通消息混在一起发送 send_one_by_one = any( - isinstance(seg, (Node, Nodes, File)) for seg in message_chain.chain + isinstance(seg, Node | Nodes | File) for seg in message_chain.chain ) if not send_one_by_one: ret = await cls._parse_onebot_json(message_chain) @@ -119,7 +119,7 @@ async def send_message( await cls._dispatch_send(bot, event, is_group, session_id, ret) return for seg in message_chain.chain: - if isinstance(seg, (Node, Nodes)): + if isinstance(seg, Node | Nodes): # 合并转发消息 if isinstance(seg, Node): nodes = Nodes([seg]) diff --git a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py index d693c4206..6076bfc1b 100644 --- a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +++ b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py @@ -90,12 +90,10 @@ async def _post_send(self, stream: dict | None = None): if not isinstance( source, - ( - botpy.message.Message, - botpy.message.GroupMessage, - botpy.message.DirectMessage, - botpy.message.C2CMessage, - ), + botpy.message.Message + | botpy.message.GroupMessage + | botpy.message.DirectMessage + | botpy.message.C2CMessage, ): logger.warning(f"[QQOfficial] 不支持的消息源类型: {type(source)}") return None @@ -120,7 +118,7 @@ async def _post_send(self, stream: dict | None = None): "msg_id": self.message_obj.message_id, } - if not isinstance(source, (botpy.message.Message, botpy.message.DirectMessage)): + if not isinstance(source, botpy.message.Message | botpy.message.DirectMessage): payload["msg_seq"] = random.randint(1, 10000) ret = None diff --git a/astrbot/core/star/command_management.py b/astrbot/core/star/command_management.py index 3801932b0..ba3e39017 100644 --- a/astrbot/core/star/command_management.py +++ b/astrbot/core/star/command_management.py @@ -303,7 +303,7 @@ def _locate_primary_filter( handler: StarHandlerMetadata, ) -> CommandFilter | CommandGroupFilter | None: for filter_ref in handler.event_filters: - if isinstance(filter_ref, (CommandFilter, CommandGroupFilter)): + if isinstance(filter_ref, CommandFilter | CommandGroupFilter): return filter_ref return None diff --git a/astrbot/core/star/config.py b/astrbot/core/star/config.py index a9af974c5..2b590921d 100644 --- a/astrbot/core/star/config.py +++ b/astrbot/core/star/config.py @@ -38,7 +38,7 @@ def put_config(namespace: str, name: str, key: str, value, description: str): raise ValueError("namespace 不能以 internal_ 开头。") if not isinstance(key, str): raise ValueError("key 只支持 str 类型。") - if not isinstance(value, (str, int, float, bool, list)): + if not isinstance(value, str | int | float | bool | list): raise ValueError("value 只支持 str, int, float, bool, list 类型。") config_dir = os.path.join(get_astrbot_data_path(), "config") diff --git a/astrbot/core/star/filter/command.py b/astrbot/core/star/filter/command.py index 51ad5f089..e86ee85af 100755 --- a/astrbot/core/star/filter/command.py +++ b/astrbot/core/star/filter/command.py @@ -115,7 +115,7 @@ def validate_and_convert_params( # 没有 GreedyStr 的情况 if i >= len(params): if ( - isinstance(param_type_or_default_val, (type, types.UnionType)) + isinstance(param_type_or_default_val, type | types.UnionType) or typing.get_origin(param_type_or_default_val) is typing.Union or param_type_or_default_val is inspect.Parameter.empty ): diff --git a/astrbot/core/star/filter/custom_filter.py b/astrbot/core/star/filter/custom_filter.py index d57b5cac0..ff2df9bcd 100644 --- a/astrbot/core/star/filter/custom_filter.py +++ b/astrbot/core/star/filter/custom_filter.py @@ -37,7 +37,7 @@ def __and__(self, other): class CustomFilterOr(CustomFilter): def __init__(self, filter1: CustomFilter, filter2: CustomFilter): super().__init__() - if not isinstance(filter1, (CustomFilter, CustomFilterAnd, CustomFilterOr)): + if not isinstance(filter1, CustomFilter | CustomFilterAnd | CustomFilterOr): raise ValueError( "CustomFilter lass can only operate with other CustomFilter.", ) @@ -51,7 +51,7 @@ def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool: class CustomFilterAnd(CustomFilter): def __init__(self, filter1: CustomFilter, filter2: CustomFilter): super().__init__() - if not isinstance(filter1, (CustomFilter, CustomFilterAnd, CustomFilterOr)): + if not isinstance(filter1, CustomFilter | CustomFilterAnd | CustomFilterOr): raise ValueError( "CustomFilter lass can only operate with other CustomFilter.", ) diff --git a/astrbot/core/star/register/star_handler.py b/astrbot/core/star/register/star_handler.py index a2644feef..024aaf2d0 100644 --- a/astrbot/core/star/register/star_handler.py +++ b/astrbot/core/star/register/star_handler.py @@ -150,7 +150,7 @@ def register_custom_filter(custom_type_filter, *args, **kwargs): if args: raise_error = args[0] - if not isinstance(custom_filter, (CustomFilterAnd, CustomFilterOr)): + if not isinstance(custom_filter, CustomFilterAnd | CustomFilterOr): custom_filter = custom_filter(raise_error) def decorator(awaitable): diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index bd2f9a264..c5998682c 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -2,6 +2,7 @@ import inspect import os import traceback +from pathlib import Path from typing import Any from quart import request @@ -20,11 +21,22 @@ from astrbot.core.platform.register import platform_cls_map, platform_registry 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.star.star import StarMetadata, star_registry +from astrbot.core.utils.astrbot_path import ( + get_astrbot_plugin_data_path, +) from astrbot.core.utils.llm_metadata import LLM_METADATAS from astrbot.core.utils.webhook_utils import ensure_platform_webhook_config from .route import Response, Route, RouteContext +from .util import ( + config_key_to_folder, + get_schema_item, + normalize_rel_path, + sanitize_filename, +) + +MAX_FILE_BYTES = 500 * 1024 * 1024 def try_cast(value: Any, type_: str): @@ -106,6 +118,32 @@ 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 + normalized = normalize_rel_path(item) + if not normalized or not normalized.startswith("files/"): + errors.append( + f"Invalid file path {path}{key}[{idx}]: {item}", + ) + continue + key_path = f"{path}{key}" + expected_folder = config_key_to_folder(key_path) + expected_prefix = f"files/{expected_folder}/" + if not normalized.startswith(expected_prefix): + errors.append( + f"Invalid file path {path}{key}[{idx}]: {item}", + ) + continue + value[idx] = normalized + continue + if meta["type"] == "list" and not isinstance(value, list): errors.append( f"错误的类型 {path}{key}: 期望是 list, 得到了 {type(value).__name__}", @@ -218,6 +256,9 @@ 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/file/upload": ("POST", self.upload_config_file), + "/config/file/delete": ("POST", self.delete_config_file), + "/config/file/get": ("GET", self.get_config_file_list), "/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 +917,193 @@ 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) -> StarMetadata | None: + for plugin_md in star_registry: + if plugin_md.name == plugin_name: + return plugin_md + return None + + def _resolve_config_file_scope( + self, + ) -> tuple[str, str, str, StarMetadata, AstrBotConfig]: + """将请求参数解析为一个明确的配置作用域。 + + 当前支持的 scope: + - scope=plugin:name=,key= + """ + + scope = request.args.get("scope") or "plugin" + name = request.args.get("name") + key_path = request.args.get("key") + + if scope != "plugin": + raise ValueError(f"Unsupported scope: {scope}") + if not name or not key_path: + raise ValueError("Missing name or key parameter") + + md = self._get_plugin_metadata_by_name(name) + if not md or not md.config: + raise ValueError(f"Plugin {name} not found or has no config") + + return scope, name, key_path, md, md.config + + async def upload_config_file(self): + """上传文件到插件数据目录(用于某个 file 类型配置项)。""" + + try: + scope, name, key_path, md, config = self._resolve_config_file_scope() + except ValueError as e: + return Response().error(str(e)).__dict__ + + meta = get_schema_item(getattr(config, "schema", None), 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: list[str] = [] + 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__ + + storage_root_path = Path(get_astrbot_plugin_data_path()).resolve(strict=False) + plugin_root_path = (storage_root_path / name).resolve(strict=False) + try: + plugin_root_path.relative_to(storage_root_path) + except ValueError: + return Response().error("Invalid name parameter").__dict__ + plugin_root_path.mkdir(parents=True, exist_ok=True) + + uploaded: list[str] = [] + folder = config_key_to_folder(key_path) + errors: list[str] = [] + for file in files.values(): + filename = sanitize_filename(file.filename or "") + if not filename: + 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}") + continue + + rel_path = f"files/{folder}/{filename}" + save_path = (plugin_root_path / rel_path).resolve(strict=False) + try: + save_path.relative_to(plugin_root_path) + except ValueError: + errors.append(f"Invalid path: {filename}") + continue + + save_path.parent.mkdir(parents=True, exist_ok=True) + await file.save(str(save_path)) + if save_path.is_file() and save_path.stat().st_size > MAX_FILE_BYTES: + save_path.unlink() + errors.append(f"File too large: {filename}") + continue + 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_config_file(self): + """删除插件数据目录中的文件。""" + + scope = request.args.get("scope") or "plugin" + name = request.args.get("name") + if not name: + return Response().error("Missing name parameter").__dict__ + if scope != "plugin": + return Response().error(f"Unsupported scope: {scope}").__dict__ + + data = await request.get_json() + rel_path = data.get("path") if isinstance(data, dict) else None + rel_path = 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(name) + if not md: + return Response().error(f"Plugin {name} not found").__dict__ + + storage_root_path = Path(get_astrbot_plugin_data_path()).resolve(strict=False) + plugin_root_path = (storage_root_path / name).resolve(strict=False) + try: + plugin_root_path.relative_to(storage_root_path) + except ValueError: + return Response().error("Invalid name parameter").__dict__ + target_path = (plugin_root_path / rel_path).resolve(strict=False) + try: + target_path.relative_to(plugin_root_path) + except ValueError: + return Response().error("Invalid path parameter").__dict__ + if target_path.is_file(): + target_path.unlink() + + return Response().ok(None, "Deleted").__dict__ + + async def get_config_file_list(self): + """获取配置项对应目录下的文件列表。""" + + try: + _, name, key_path, _, config = self._resolve_config_file_scope() + except ValueError as e: + return Response().error(str(e)).__dict__ + + meta = get_schema_item(getattr(config, "schema", None), key_path) + if not meta or meta.get("type") != "file": + return Response().error("Config item not found or not file type").__dict__ + + storage_root_path = Path(get_astrbot_plugin_data_path()).resolve(strict=False) + plugin_root_path = (storage_root_path / name).resolve(strict=False) + try: + plugin_root_path.relative_to(storage_root_path) + except ValueError: + return Response().error("Invalid name parameter").__dict__ + + folder = config_key_to_folder(key_path) + target_dir = (plugin_root_path / "files" / folder).resolve(strict=False) + try: + target_dir.relative_to(plugin_root_path) + except ValueError: + return Response().error("Invalid path parameter").__dict__ + + if not target_dir.exists() or not target_dir.is_dir(): + return Response().ok({"files": []}).__dict__ + + files: list[str] = [] + for path in target_dir.rglob("*"): + if not path.is_file(): + continue + try: + rel_path = path.relative_to(plugin_root_path).as_posix() + except ValueError: + continue + if rel_path.startswith("files/"): + files.append(rel_path) + + return Response().ok({"files": files}).__dict__ + async def post_new_platform(self): new_platform_config = await request.json @@ -1130,8 +1358,14 @@ async def _save_plugin_configs(self, post_configs: dict, plugin_name: str): raise ValueError(f"插件 {plugin_name} 不存在") if not md.config: raise ValueError(f"插件 {plugin_name} 没有注册配置") + assert md.config is not None 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}") + md.config.save_config(post_configs) except Exception as e: raise e diff --git a/astrbot/dashboard/routes/util.py b/astrbot/dashboard/routes/util.py new file mode 100644 index 000000000..105619815 --- /dev/null +++ b/astrbot/dashboard/routes/util.py @@ -0,0 +1,102 @@ +"""Dashboard 路由工具集。 + +这里放一些 dashboard routes 可复用的小工具函数。 + +目前主要用于「配置文件上传(file 类型配置项)」功能: +- 清洗/规范化用户可控的文件名与相对路径 +- 将配置 key 映射到配置项独立子目录 +""" + +import os + + +def get_schema_item(schema: dict | None, key_path: str) -> dict | None: + """按 dot-path 获取 schema 的节点。 + + 同时支持: + - 扁平 schema(直接 key 命中) + - 嵌套 object schema({type: "object", items: {...}}) + """ + + 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 + + +def sanitize_filename(name: str) -> str: + """清洗上传文件名,避免路径穿越与非法名称。 + + - 丢弃目录部分,仅保留 basename + - 将路径分隔符替换为下划线 + - 拒绝空字符串 / "." / ".." + """ + + 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 + + +def sanitize_path_segment(segment: str) -> str: + """清洗目录片段(URL/path 安全,避免穿越)。 + + 仅保留 [A-Za-z0-9_-],其余替换为 "_" + """ + + 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 "_" + + +def config_key_to_folder(key_path: str) -> str: + """将 dot-path 的配置 key 转成稳定的文件夹路径。""" + + parts = [sanitize_path_segment(p) for p in key_path.split(".") if p] + return "/".join(parts) if parts else "_" + + +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) 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..71e232696 --- /dev/null +++ b/dashboard/src/components/shared/FileConfigItem.vue @@ -0,0 +1,407 @@ + + + + + diff --git a/dashboard/src/i18n/locales/en-US/features/config.json b/dashboard/src/i18n/locales/en-US/features/config.json index c510b6eaa..ef595bd71 100644 --- a/dashboard/src/i18n/locales/en-US/features/config.json +++ b/dashboard/src/i18n/locales/en-US/features/config.json @@ -89,6 +89,23 @@ }, "codeEditor": { "title": "Edit Configuration File" + }, + "fileUpload": { + "button": "Manage Files", + "dialogTitle": "Uploaded Files", + "dropzone": "Upload new file", + "allowedTypes": "Allowed types: {types}", + "empty": "No files uploaded", + "statusMissing": "Missing file", + "statusUnconfigured": "Not in config", + "uploadSuccess": "Uploaded {count} files", + "uploadFailed": "Upload failed", + "loadFailed": "Failed to load file list", + "fileTooLarge": "File too large (max {max} MB): {name}", + "deleteSuccess": "Deleted file", + "deleteFailed": "Delete failed", + "addToConfig": "Added to config", + "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..bf0c709b0 100644 --- a/dashboard/src/i18n/locales/zh-CN/features/config.json +++ b/dashboard/src/i18n/locales/zh-CN/features/config.json @@ -89,5 +89,23 @@ }, "codeEditor": { "title": "编辑配置文件" + }, + "fileUpload": { + "button": "管理文件", + "dialogTitle": "已上传文件", + "dropzone": "上传新文件", + "allowedTypes": "允许类型:{types}", + "empty": "暂无已上传文件", + "statusMissing": "文件缺失", + "statusUnconfigured": "未加入配置", + "uploadSuccess": "已上传 {count} 个文件", + "uploadFailed": "上传失败", + "loadFailed": "获取文件列表失败", + "fileTooLarge": "文件过大(上限 {max} MB):{name}", + "deleteSuccess": "已删除文件", + "deleteFailed": "删除失败", + "addToConfig": "已加入配置", + "fileCount": "文件:{count}", + "done": "完成" } -} \ No newline at end of file +} diff --git a/dashboard/src/views/ExtensionPage.vue b/dashboard/src/views/ExtensionPage.vue index c84862f2d..a1bb81a69 100644 --- a/dashboard/src/views/ExtensionPage.vue +++ b/dashboard/src/views/ExtensionPage.vue @@ -2127,19 +2127,22 @@ watch(isListView, (newVal) => { - + - {{ + {{ tm("dialogs.config.title") }} - -

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

+
+ +

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

+