From b31b520c7c1b9593ed800ec07e3a9d57a4d77b80 Mon Sep 17 00:00:00 2001 From: RC-CHN <67079377+RC-CHN@users.noreply.github.com> Date: Sun, 7 Sep 2025 00:14:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E7=AE=A1=E7=90=86=20?= =?UTF-8?q?T2I=20=E6=A8=A1=E7=89=88=20(#2638)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat:添加t2i模板管理后端api,移除config.py中重复功能 * feat: 添加T2I模板管理功能前端,支持模板的创建、应用和重置 * refactor: 修复错误的保存逻辑,将t2i注册时打印路由信息部分移到基类实现 * remove:移除了路由注册时的打印 * chore: format code * fix: update input variant from solo to outlined for better UI consistency --------- Co-authored-by: Soulter <905617992@qq.com> --- astrbot/core/config/default.py | 7 + .../core/pipeline/result_decorate/stage.py | 6 +- astrbot/core/star/__init__.py | 12 +- astrbot/core/utils/t2i/network_strategy.py | 29 +- astrbot/core/utils/t2i/renderer.py | 10 +- .../t2i/template/astrbot_powershell.html | 184 +++++++++ .../t2i/template/default_template.html.bak | 247 ++++++++++++ astrbot/core/utils/t2i/template_manager.py | 95 +++++ astrbot/dashboard/routes/config.py | 59 +-- astrbot/dashboard/routes/route.py | 21 +- astrbot/dashboard/routes/t2i.py | 232 +++++++++++ astrbot/dashboard/server.py | 6 +- .../components/shared/T2ITemplateEditor.vue | 367 +++++++++++++++--- 13 files changed, 1131 insertions(+), 144 deletions(-) create mode 100644 astrbot/core/utils/t2i/template/astrbot_powershell.html create mode 100644 astrbot/core/utils/t2i/template/default_template.html.bak create mode 100644 astrbot/core/utils/t2i/template_manager.py create mode 100644 astrbot/dashboard/routes/t2i.py diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 7d3d2f7d4..f8e56f48c 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -103,6 +103,7 @@ DEFAULT_CONFIG = { "t2i_strategy": "remote", "t2i_endpoint": "", "t2i_use_file_service": False, + "t2i_active_template": "base", "http_proxy": "", "no_proxy": ["localhost", "127.0.0.1", "::1"], "dashboard": { @@ -2334,6 +2335,12 @@ CONFIG_METADATA_3_SYSTEM = { }, "_special": "t2i_template", }, + "t2i_active_template": { + "description": "当前应用的文转图渲染模板", + "type": "string", + "hint": "此处的值由文转图模板管理页面进行维护。", + "invisible": True, + }, "log_level": { "description": "控制台日志级别", "type": "string", diff --git a/astrbot/core/pipeline/result_decorate/stage.py b/astrbot/core/pipeline/result_decorate/stage.py index f87f7bbc0..4964dd68c 100644 --- a/astrbot/core/pipeline/result_decorate/stage.py +++ b/astrbot/core/pipeline/result_decorate/stage.py @@ -36,6 +36,7 @@ class ResultDecorateStage(Stage): self.t2i_word_threshold = 150 self.t2i_strategy = ctx.astrbot_config["t2i_strategy"] self.t2i_use_network = self.t2i_strategy == "remote" + self.t2i_active_template = ctx.astrbot_config["t2i_active_template"] self.forward_threshold = ctx.astrbot_config["platform_settings"][ "forward_threshold" @@ -247,7 +248,10 @@ class ResultDecorateStage(Stage): render_start = time.time() try: url = await html_renderer.render_t2i( - plain_str, return_url=True, use_network=self.t2i_use_network + plain_str, + return_url=True, + use_network=self.t2i_use_network, + template_name=self.t2i_active_template, ) except BaseException: logger.error("文本转图片失败,使用文本发送。") diff --git a/astrbot/core/star/__init__.py b/astrbot/core/star/__init__.py index fab39294b..70e06d0d5 100644 --- a/astrbot/core/star/__init__.py +++ b/astrbot/core/star/__init__.py @@ -27,14 +27,16 @@ class Star(CommandParserMixin): star_map[cls.__module__].star_cls_type = cls star_map[cls.__module__].module_path = cls.__module__ - @staticmethod - async def text_to_image(text: str, return_url=True) -> str: + async def text_to_image(self, text: str, return_url=True) -> str: """将文本转换为图片""" - return await html_renderer.render_t2i(text, return_url=return_url) + return await html_renderer.render_t2i( + text, + return_url=return_url, + template_name=self.context._config.get("t2i_active_template"), + ) - @staticmethod async def html_render( - tmpl: str, data: dict, return_url=True, options: dict | None = None + self, tmpl: str, data: dict, return_url=True, options: dict | None = None ) -> str: """渲染 HTML""" return await html_renderer.render_custom_template( diff --git a/astrbot/core/utils/t2i/network_strategy.py b/astrbot/core/utils/t2i/network_strategy.py index 2295f051b..c43f9ed2e 100644 --- a/astrbot/core/utils/t2i/network_strategy.py +++ b/astrbot/core/utils/t2i/network_strategy.py @@ -1,6 +1,5 @@ import aiohttp import asyncio -import os import ssl import certifi import logging @@ -8,10 +7,9 @@ import random from . import RenderStrategy from astrbot.core.config import VERSION from astrbot.core.utils.io import download_image_by_url -from astrbot.core.utils.astrbot_path import get_astrbot_data_path +from astrbot.core.utils.t2i.template_manager import TemplateManager ASTRBOT_T2I_DEFAULT_ENDPOINT = "https://t2i.soulter.top/text2img" -CUSTOM_T2I_TEMPLATE_PATH = os.path.join(get_astrbot_data_path(), "t2i_template.html") logger = logging.getLogger("astrbot") @@ -23,26 +21,17 @@ class NetworkRenderStrategy(RenderStrategy): self.BASE_RENDER_URL = ASTRBOT_T2I_DEFAULT_ENDPOINT else: self.BASE_RENDER_URL = self._clean_url(base_url) - self.TEMPLATE_PATH = os.path.join(os.path.dirname(__file__), "template", "base.html") - with open(self.TEMPLATE_PATH, "r", encoding="utf-8") as f: - self.DEFAULT_TEMPLATE = f.read() self.endpoints = [self.BASE_RENDER_URL] + self.template_manager = TemplateManager() async def initialize(self): if self.BASE_RENDER_URL == ASTRBOT_T2I_DEFAULT_ENDPOINT: asyncio.create_task(self.get_official_endpoints()) - async def get_template(self) -> str: - """获取文转图 HTML 模板 - - Returns: - str: 文转图 HTML 模板字符串 - """ - if os.path.exists(CUSTOM_T2I_TEMPLATE_PATH): - with open(CUSTOM_T2I_TEMPLATE_PATH, "r", encoding="utf-8") as f: - return f.read() - return self.DEFAULT_TEMPLATE + async def get_template(self, name: str = "base") -> str: + """通过名称获取文转图 HTML 模板""" + return self.template_manager.get_template(name) async def get_official_endpoints(self): """获取官方的 t2i 端点列表。""" @@ -124,11 +113,15 @@ class NetworkRenderStrategy(RenderStrategy): logger.error(f"All endpoints failed: {last_exception}") raise RuntimeError(f"All endpoints failed: {last_exception}") - async def render(self, text: str, return_url: bool = False) -> str: + async def render( + self, text: str, return_url: bool = False, template_name: str | None = "base" + ) -> str: """ 返回图像的文件路径 """ - tmpl_str = await self.get_template() + if not template_name: + template_name = "base" + tmpl_str = await self.get_template(name=template_name) text = text.replace("`", "\\`") return await self.render_custom_template( tmpl_str, {"text": text, "version": f"v{VERSION}"}, return_url diff --git a/astrbot/core/utils/t2i/renderer.py b/astrbot/core/utils/t2i/renderer.py index a3ceec4ad..122189f93 100644 --- a/astrbot/core/utils/t2i/renderer.py +++ b/astrbot/core/utils/t2i/renderer.py @@ -34,12 +34,18 @@ class HtmlRenderer: ) async def render_t2i( - self, text: str, use_network: bool = True, return_url: bool = False + self, + text: str, + use_network: bool = True, + return_url: bool = False, + template_name: str | None = None, ): """使用默认文转图模板。""" if use_network: try: - return await self.network_strategy.render(text, return_url=return_url) + return await self.network_strategy.render( + text, return_url=return_url, template_name=template_name + ) except BaseException as e: logger.error( f"Failed to render image via AstrBot API: {e}. Falling back to local rendering." diff --git a/astrbot/core/utils/t2i/template/astrbot_powershell.html b/astrbot/core/utils/t2i/template/astrbot_powershell.html new file mode 100644 index 000000000..9ed3e77a5 --- /dev/null +++ b/astrbot/core/utils/t2i/template/astrbot_powershell.html @@ -0,0 +1,184 @@ + + + + + Astrbot PowerShell {{ version }} + + + + + + + + + +
+ > Astrbot PowerShell + {{ version }} +
+ +
+
+
+ + + + + + \ No newline at end of file diff --git a/astrbot/core/utils/t2i/template/default_template.html.bak b/astrbot/core/utils/t2i/template/default_template.html.bak new file mode 100644 index 000000000..257cff3ff --- /dev/null +++ b/astrbot/core/utils/t2i/template/default_template.html.bak @@ -0,0 +1,247 @@ + + + + + + + + + + + + +
+ # AstrBot + {{ version }} +
+
+ + + + + + \ No newline at end of file diff --git a/astrbot/core/utils/t2i/template_manager.py b/astrbot/core/utils/t2i/template_manager.py new file mode 100644 index 000000000..ccc5492fd --- /dev/null +++ b/astrbot/core/utils/t2i/template_manager.py @@ -0,0 +1,95 @@ +# astrbot/core/utils/t2i/template_manager.py + +import os +import shutil +from astrbot.core.utils.astrbot_path import get_astrbot_path + + +class TemplateManager: + """ + 负责管理 t2i HTML 模板的 CRUD 和重置操作。 + """ + + def __init__(self): + # 修正路径拼接,加入缺失的 'astrbot' 目录 + self.template_dir = os.path.join( + get_astrbot_path(), "astrbot", "core", "utils", "t2i", "template" + ) + self.backup_template_path = os.path.join( + self.template_dir, "default_template.html.bak" + ) + # 确保模板目录存在 + os.makedirs(self.template_dir, exist_ok=True) + + # 检查模板目录中是否有 .html 文件 + html_files = [f for f in os.listdir(self.template_dir) if f.endswith(".html")] + if not html_files and os.path.exists(self.backup_template_path): + self.reset_default_template() + + def _get_template_path(self, name: str) -> str: + """获取模板的完整路径,防止路径遍历漏洞。""" + if ".." in name or "/" in name or "\\" in name: + raise ValueError("模板名称包含非法字符。") + return os.path.join(self.template_dir, f"{name}.html") + + def list_templates(self) -> list[dict]: + """列出所有可用的模板。""" + templates = [] + for filename in os.listdir(self.template_dir): + if filename.endswith(".html"): + templates.append( + { + "name": os.path.splitext(filename)[0], + "is_default": filename == "base.html", + } + ) + return templates + + def get_template(self, name: str) -> str: + """获取指定模板的内容。""" + path = self._get_template_path(name) + if not os.path.exists(path): + raise FileNotFoundError("模板不存在。") + with open(path, "r", encoding="utf-8") as f: + return f.read() + + def create_template(self, name: str, content: str): + """创建一个新的模板文件。""" + path = self._get_template_path(name) + if os.path.exists(path): + raise FileExistsError("同名模板已存在。") + with open(path, "w", encoding="utf-8") as f: + f.write(content) + + def update_template(self, name: str, content: str): + """更新一个已存在的模板文件。""" + path = self._get_template_path(name) + if not os.path.exists(path): + raise FileNotFoundError("模板不存在。") + with open(path, "w", encoding="utf-8") as f: + f.write(content) + + def delete_template(self, name: str): + """删除一个模板文件。""" + if name == "base": + raise ValueError("不能删除默认的 base 模板。") + path = self._get_template_path(name) + if not os.path.exists(path): + raise FileNotFoundError("模板不存在。") + os.remove(path) + + def backup_default_template_if_not_exist(self): + """如果备份不存在,则创建默认模板的备份。""" + default_path = os.path.join(self.template_dir, "base.html") + if not os.path.exists(self.backup_template_path) and os.path.exists( + default_path + ): + shutil.copyfile(default_path, self.backup_template_path) + + def reset_default_template(self): + """重置默认模板。""" + if not os.path.exists(self.backup_template_path): + raise FileNotFoundError("默认模板的备份文件不存在,无法重置。") + + default_path = os.path.join(self.template_dir, "base.html") + shutil.copyfile(self.backup_template_path, default_path) diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index 9fbe63ee3..23daeab6b 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -1,6 +1,7 @@ import typing import traceback import os +import copy from .route import Route, Response, RouteContext from astrbot.core.provider.entities import ProviderType from quart import request @@ -16,11 +17,10 @@ from astrbot.core.core_lifecycle import AstrBotCoreLifecycle from astrbot.core.platform.register import platform_registry from astrbot.core.provider.register import provider_registry from astrbot.core.star.star import star_registry -from astrbot.core import logger, html_renderer +from astrbot.core import logger from astrbot.core.provider import Provider from astrbot.core.provider.provider import RerankProvider import asyncio -from astrbot.core.utils.t2i.network_strategy import CUSTOM_T2I_TEMPLATE_PATH def try_cast(value: str, type_: str): @@ -156,6 +156,7 @@ def save_config(post_config: dict, config: AstrBotConfig, is_core: bool = False) raise ValueError(f"验证配置时出现异常: {e}") if errors: raise ValueError(f"格式校验未通过: {errors}") + config.save_config(post_config) @@ -186,56 +187,9 @@ class ConfigRoute(Route): "/config/provider/check_one": ("GET", self.check_one_provider_status), "/config/provider/list": ("GET", self.get_provider_config_list), "/config/provider/model_list": ("GET", self.get_provider_model_list), - "/config/astrbot/t2i-template/get": ("GET", self.get_t2i_template), - "/config/astrbot/t2i-template/save": ("POST", self.post_t2i_template), - "/config/astrbot/t2i-template/delete": ("DELETE", self.delete_t2i_template), } self.register_routes() - async def get_t2i_template(self): - """获取 T2I 模板""" - try: - template = await html_renderer.network_strategy.get_template() - has_custom_template = os.path.exists(CUSTOM_T2I_TEMPLATE_PATH) - return ( - Response() - .ok({"template": template, "has_custom_template": has_custom_template}) - .__dict__ - ) - except Exception as e: - logger.error(traceback.format_exc()) - return Response().error(f"获取模板失败: {str(e)}").__dict__ - - async def post_t2i_template(self): - """保存 T2I 模板""" - try: - post_data = await request.json - if not post_data or "template" not in post_data: - return Response().error("缺少模板内容").__dict__ - - template_content = post_data["template"] - - # 保存自定义模板到文件 - with open(CUSTOM_T2I_TEMPLATE_PATH, "w", encoding="utf-8") as f: - f.write(template_content) - - return Response().ok(message="模板保存成功").__dict__ - except Exception as e: - logger.error(traceback.format_exc()) - return Response().error(f"保存模板失败: {str(e)}").__dict__ - - async def delete_t2i_template(self): - """删除自定义 T2I 模板,恢复默认模板""" - try: - if os.path.exists(CUSTOM_T2I_TEMPLATE_PATH): - os.remove(CUSTOM_T2I_TEMPLATE_PATH) - return Response().ok(message="已恢复默认模板").__dict__ - else: - return Response().ok(message="未找到自定义模板文件").__dict__ - except Exception as e: - logger.error(traceback.format_exc()) - return Response().error(f"删除模板失败: {str(e)}").__dict__ - async def get_abconf_list(self): """获取所有 AstrBot 配置文件的列表""" abconf_list = self.acm.get_conf_list() @@ -766,6 +720,13 @@ class ConfigRoute(Route): if conf_id not in self.acm.confs: raise ValueError(f"配置文件 {conf_id} 不存在") astrbot_config = self.acm.confs[conf_id] + + # 保留服务端的 t2i_active_template 值 + if "t2i_active_template" in astrbot_config: + post_configs["t2i_active_template"] = astrbot_config[ + "t2i_active_template" + ] + save_config(post_configs, astrbot_config, is_core=True) except Exception as e: raise e diff --git a/astrbot/dashboard/routes/route.py b/astrbot/dashboard/routes/route.py index a11fae252..de3ba7c90 100644 --- a/astrbot/dashboard/routes/route.py +++ b/astrbot/dashboard/routes/route.py @@ -1,3 +1,4 @@ +from astrbot.core import logger from astrbot.core.config.astrbot_config import AstrBotConfig from dataclasses import dataclass from quart import Quart @@ -15,8 +16,24 @@ class Route: self.config = context.config def register_routes(self): - for route, (method, func) in self.routes.items(): - self.app.add_url_rule(f"/api{route}", view_func=func, methods=[method]) + def _add_rule(path, method, func): + # 统一添加 /api 前缀 + full_path = f"/api{path}" + self.app.add_url_rule(full_path, view_func=func, methods=[method]) + + # 兼容字典和列表两种格式 + routes_to_register = ( + self.routes.items() if isinstance(self.routes, dict) else self.routes + ) + + for route, definition in routes_to_register: + # 兼容一个路由多个方法 + if isinstance(definition, list): + for method, func in definition: + _add_rule(route, method, func) + else: + method, func = definition + _add_rule(route, method, func) @dataclass diff --git a/astrbot/dashboard/routes/t2i.py b/astrbot/dashboard/routes/t2i.py new file mode 100644 index 000000000..31cdc0bb4 --- /dev/null +++ b/astrbot/dashboard/routes/t2i.py @@ -0,0 +1,232 @@ +# astrbot/dashboard/routes/t2i.py + +from dataclasses import asdict +from quart import jsonify, request + +from astrbot.core import logger +from astrbot.core.core_lifecycle import AstrBotCoreLifecycle +from astrbot.core.utils.t2i.template_manager import TemplateManager +from .route import Response, Route, RouteContext + + +class T2iRoute(Route): + def __init__(self, context: RouteContext, core_lifecycle: AstrBotCoreLifecycle): + super().__init__(context) + self.core_lifecycle = core_lifecycle + self.config = core_lifecycle.astrbot_config + self.manager = TemplateManager() + # 使用列表保证路由注册顺序,避免 / 路由优先匹配 /reset_default + self.routes = [ + ("/t2i/templates", ("GET", self.list_templates)), + ("/t2i/templates/active", ("GET", self.get_active_template)), + ("/t2i/templates/create", ("POST", self.create_template)), + ("/t2i/templates/reset_default", ("POST", self.reset_default_template)), + ("/t2i/templates/set_active", ("POST", self.set_active_template)), + # 动态路由应该在静态路由之后注册 + ( + "/t2i/templates/", + [ + ("GET", self.get_template), + ("PUT", self.update_template), + ("DELETE", self.delete_template), + ], + ), + ] + + # 应用启动时,确保备份存在 + self.manager.backup_default_template_if_not_exist() + + self.register_routes() + + async def list_templates(self): + """获取所有T2I模板列表""" + try: + templates = self.manager.list_templates() + return jsonify(asdict(Response().ok(data=templates))) + except Exception as e: + response = jsonify(asdict(Response().error(str(e)))) + response.status_code = 500 + return response + + async def get_active_template(self): + """获取当前激活的T2I模板""" + try: + active_template = self.config.get("t2i_active_template", "base") + return jsonify( + asdict(Response().ok(data={"active_template": active_template})) + ) + except Exception as e: + logger.error("Error in get_active_template", exc_info=True) + response = jsonify(asdict(Response().error(str(e)))) + response.status_code = 500 + return response + + async def get_template(self, name: str): + """获取指定名称的T2I模板内容""" + try: + content = self.manager.get_template(name) + return jsonify( + asdict(Response().ok(data={"name": name, "content": content})) + ) + except FileNotFoundError: + response = jsonify(asdict(Response().error("Template not found"))) + response.status_code = 404 + return response + except Exception as e: + response = jsonify(asdict(Response().error(str(e)))) + response.status_code = 500 + return response + + async def create_template(self): + """创建一个新的T2I模板""" + try: + data = await request.json + name = data.get("name") + content = data.get("content") + if not name or not content: + response = jsonify( + asdict(Response().error("Name and content are required.")) + ) + response.status_code = 400 + return response + + self.manager.create_template(name, content) + response = jsonify( + asdict( + Response().ok( + data={"name": name}, message="Template created successfully." + ) + ) + ) + response.status_code = 201 + return response + except FileExistsError: + response = jsonify( + asdict(Response().error("Template with this name already exists.")) + ) + response.status_code = 409 + return response + except ValueError as e: + response = jsonify(asdict(Response().error(str(e)))) + response.status_code = 400 + return response + except Exception as e: + response = jsonify(asdict(Response().error(str(e)))) + response.status_code = 500 + return response + + async def update_template(self, name: str): + """更新一个已存在的T2I模板""" + try: + data = await request.json + content = data.get("content") + if content is None: + response = jsonify(asdict(Response().error("Content is required."))) + response.status_code = 400 + return response + + self.manager.update_template(name, content) + return jsonify( + asdict( + Response().ok( + data={"name": name}, message="Template updated successfully." + ) + ) + ) + except FileNotFoundError: + response = jsonify(asdict(Response().error("Template not found."))) + response.status_code = 404 + return response + except ValueError as e: + response = jsonify(asdict(Response().error(str(e)))) + response.status_code = 400 + return response + except Exception as e: + response = jsonify(asdict(Response().error(str(e)))) + response.status_code = 500 + return response + + async def delete_template(self, name: str): + """删除一个T2I模板""" + try: + self.manager.delete_template(name) + return jsonify( + asdict(Response().ok(message="Template deleted successfully.")) + ) + except FileNotFoundError: + response = jsonify(asdict(Response().error("Template not found."))) + response.status_code = 404 + return response + except ValueError as e: + response = jsonify(asdict(Response().error(str(e)))) + response.status_code = 400 + return response + except Exception as e: + response = jsonify(asdict(Response().error(str(e)))) + response.status_code = 500 + return response + + async def set_active_template(self): + """设置当前活动的T2I模板""" + try: + data = await request.json + name = data.get("name") + if not name: + response = jsonify(asdict(Response().error("模板名称(name)不能为空。"))) + response.status_code = 400 + return response + + # 验证模板文件是否存在 + self.manager.get_template(name) + + # 更新配置 + config = self.config + config["t2i_active_template"] = name + config.save_config(config) + + # 热重载以应用更改 + await self.core_lifecycle.reload_pipeline_scheduler("default") + + return jsonify(asdict(Response().ok(message=f"模板 '{name}' 已成功应用。"))) + + except FileNotFoundError: + response = jsonify( + asdict(Response().error(f"模板 '{name}' 不存在,无法应用。")) + ) + response.status_code = 404 + return response + except Exception as e: + logger.error("Error in set_active_template", exc_info=True) + response = jsonify(asdict(Response().error(str(e)))) + response.status_code = 500 + return response + + async def reset_default_template(self): + """重置默认的'base'模板""" + try: + self.manager.reset_default_template() + + # 更新配置,将激活模板也重置为'base' + config = self.config + config["t2i_active_template"] = "base" + config.save_config(config) + + # 热重载以应用更改 + await self.core_lifecycle.reload_pipeline_scheduler("default") + + return jsonify( + asdict( + Response().ok( + message="Default template has been reset and activated." + ) + ) + ) + except FileNotFoundError as e: + response = jsonify(asdict(Response().error(str(e)))) + response.status_code = 404 + return response + except Exception as e: + logger.error("Error in reset_default_template", exc_info=True) + response = jsonify(asdict(Response().error(str(e)))) + response.status_code = 500 + return response diff --git a/astrbot/dashboard/server.py b/astrbot/dashboard/server.py index e22b20524..83f40c8f2 100644 --- a/astrbot/dashboard/server.py +++ b/astrbot/dashboard/server.py @@ -18,6 +18,7 @@ from astrbot.core.utils.io import get_local_ip_addresses from .routes import * from .routes.route import Response, RouteContext from .routes.session_management import SessionManagementRoute +from .routes.t2i import T2iRoute APP: Quart = None @@ -60,9 +61,8 @@ class AstrBotDashboard: self.session_management_route = SessionManagementRoute( self.context, db, core_lifecycle ) - self.persona_route = PersonaRoute( - self.context, db, core_lifecycle - ) + self.persona_route = PersonaRoute(self.context, db, core_lifecycle) + self.t2i_route = T2iRoute(self.context, core_lifecycle) self.app.add_url_rule( "/api/plug/", diff --git a/dashboard/src/components/shared/T2ITemplateEditor.vue b/dashboard/src/components/shared/T2ITemplateEditor.vue index 6e19bbafe..12776f5c3 100644 --- a/dashboard/src/components/shared/T2ITemplateEditor.vue +++ b/dashboard/src/components/shared/T2ITemplateEditor.vue @@ -15,17 +15,59 @@ 自定义文转图 HTML 模板 -
- +
+ + - 恢复默认 - + + - HTML 模板编辑器 + 模板编辑器 - - 保存模板 - +
+ + mdi-plus + 新建 + + + + 重置Base + + + 删除 + + + + 保存 + +
- 保存并应用 + 保存应用当前编辑模板 @@ -121,7 +196,7 @@ 确认重置 - 确定要恢复默认模板吗?这将删除您的自定义模板,此操作无法撤销。 + 确定要将 'base' 模板恢复为默认内容吗?当前编辑器中的任何未保存更改将丢失。此操作无法撤销。 @@ -130,6 +205,37 @@ + + + + + 确认删除 + + 确定要删除模板 '{{ selectedTemplate }}' 吗?此操作无法撤销。 + + + + 取消 + 确认删除 + + + + + + + + 确认操作 + + 确定要保存对 '{{ selectedTemplate }}' 的修改,并将其设为新的活动模板吗? + + + + 取消 + 确认 + + + + @@ -141,18 +247,30 @@ import axios from 'axios' const { t } = useI18n() -// 响应式数据 +// --- 响应式数据 --- const dialog = ref(false) -const resetDialog = ref(false) -const loading = ref(false) +const loading = ref(false) // 用于加载模板列表 const saveLoading = ref(false) const resetLoading = ref(false) const previewLoading = ref(false) +const applyLoading = ref(false) + +// 模板管理 +const templates = ref([]) +const activeTemplate = ref('base') +const selectedTemplate = ref(null) +const editingName = ref('') // 用于新建模式下的名称输入 const templateContent = ref('') -const hasCustomTemplate = ref(false) +const isCreatingNew = ref(false) + +// 对话框状态 +const resetDialog = ref(false) +const deleteDialog = ref(false) +const applyAndCloseDialog = ref(false) + const previewFrame = ref(null) -// 编辑器配置 +// --- 编辑器配置 --- const editorTheme = computed(() => 'vs-light') const editorOptions = { automaticLayout: true, @@ -163,16 +281,13 @@ const editorOptions = { scrollBeyondLastLine: false, } -// 示例数据用于预览 +// --- 预览逻辑 --- const previewData = { text: '这是一个示例文本,用于预览模板效果。\n\n这里可以包含多行文本,支持换行和各种格式。', version: 'v4.0.0' } - -// 生成预览内容 const previewContent = computed(() => { try { - // 简单的模板替换,模拟 Jinja2 渲染 let content = templateContent.value content = content.replace(/\{\{\s*text\s*\|\s*safe\s*\}\}/g, previewData.text) content = content.replace(/\{\{\s*version\s*\}\}/g, previewData.version) @@ -182,58 +297,128 @@ const previewContent = computed(() => { } }) -// 方法 -const loadTemplate = async () => { +// --- API 调用方法 --- + +const loadInitialData = async () => { loading.value = true try { - const response = await axios.get('/api/config/astrbot/t2i-template/get') - if (response.data.status === 'ok') { - templateContent.value = response.data.data.template - hasCustomTemplate.value = response.data.data.has_custom_template + const [listRes, activeRes] = await Promise.all([ + axios.get('/api/t2i/templates'), + axios.get('/api/t2i/templates/active') + ]) + + if (listRes.data.status === 'ok') { + templates.value = listRes.data.data } else { - console.error('加载模板失败:', response.data.message) + console.error('加载模板列表失败:', listRes.data.message) } + + if (activeRes.data.status === 'ok') { + activeTemplate.value = activeRes.data.data.active_template + } else { + console.error('加载活动模板失败:', activeRes.data.message) + } + + // 设置初始选中的模板 + if (templates.value.length > 0) { + selectedTemplate.value = activeTemplate.value + } + } catch (error) { - console.error('加载模板失败:', error) + console.error('加载初始数据失败:', error) } finally { loading.value = false } } +const loadTemplateContent = async (name) => { + if (!name) return + previewLoading.value = true + try { + const response = await axios.get(`/api/t2i/templates/${name}`) + if (response.data.status === 'ok') { + templateContent.value = response.data.data.content + } else { + console.error(`加载模板 '${name}' 失败:`, response.data.message) + } + } catch (error) { + console.error(`加载模板 '${name}' 失败:`, error) + } finally { + previewLoading.value = false + } +} + const saveTemplate = async () => { saveLoading.value = true try { - const response = await axios.post('/api/config/astrbot/t2i-template/save', { - template: templateContent.value - }) - if (response.data.status === 'ok') { - hasCustomTemplate.value = true - closeDialog() + if (isCreatingNew.value) { + // --- 创建新模板 --- + if (!editingName.value) return + const response = await axios.post('/api/t2i/templates/create', { + name: editingName.value, + content: templateContent.value + }) + await loadInitialData() // 重新加载所有数据 + selectedTemplate.value = response.data.data.name + isCreatingNew.value = false } else { - console.error('保存模板失败:', response.data.message) + // --- 更新现有模板 --- + if (!selectedTemplate.value) return + await axios.put(`/api/t2i/templates/${selectedTemplate.value}`, { + content: templateContent.value + }) } } catch (error) { console.error('保存模板失败:', error) + // 可以在此添加错误提示 } finally { saveLoading.value = false } } -const resetToDefault = () => { - resetDialog.value = true +const setActiveTemplate = async (name) => { + applyLoading.value = true + try { + await axios.post('/api/t2i/templates/set_active', { name }) + activeTemplate.value = name + } catch (error) { + console.error(`应用模板 '${name}' 失败:`, error) + } finally { + applyLoading.value = false + } +} + +const confirmDelete = async () => { + if (!selectedTemplate.value || selectedTemplate.value === 'base') return + saveLoading.value = true + try { + const nameToDelete = selectedTemplate.value + await axios.delete(`/api/t2i/templates/${nameToDelete}`) + deleteDialog.value = false + + // 如果删除的是当前活动模板,则将活动模板重置为base + if (activeTemplate.value === nameToDelete) { + await setActiveTemplate('base') + } + await loadInitialData() + selectedTemplate.value = 'base' + } catch (error) { + console.error(`删除模板 '${selectedTemplate.value}' 失败:`, error) + } finally { + saveLoading.value = false + } } const confirmReset = async () => { resetLoading.value = true try { - const response = await axios.delete('/api/config/astrbot/t2i-template/delete') - if (response.data.status === 'ok') { - hasCustomTemplate.value = false - resetDialog.value = false - // 重新加载默认模板 - await loadTemplate() - } else { - console.error('重置模板失败:', response.data.message) + await axios.post('/api/t2i/templates/reset_default') + resetDialog.value = false + if (selectedTemplate.value === 'base') { + await loadTemplateContent('base') + } + if (activeTemplate.value !== 'base') { + await setActiveTemplate('base') } } catch (error) { console.error('重置模板失败:', error) @@ -242,15 +427,58 @@ const confirmReset = async () => { } } +// --- UI 交互方法 --- + +const resetToDefault = () => { + resetDialog.value = true +} + +const newTemplate = () => { + isCreatingNew.value = true + selectedTemplate.value = null + editingName.value = '' + templateContent.value = ` + + + + New Template + + + +
{{ text | safe }}
+ + +` +} + +const promptDelete = () => { + if (selectedTemplate.value && selectedTemplate.value !== 'base') { + deleteDialog.value = true + } +} + +const promptApplyAndClose = () => { + if (!isCreatingNew.value && selectedTemplate.value) { + applyAndCloseDialog.value = true + } +} + +const confirmApplyAndClose = async () => { + if (isCreatingNew.value) return + + await saveTemplate() + await setActiveTemplate(selectedTemplate.value) + applyAndCloseDialog.value = false + closeDialog() +} + const refreshPreview = () => { previewLoading.value = true nextTick(() => { if (previewFrame.value) { previewFrame.value.contentWindow.location.reload() } - setTimeout(() => { - previewLoading.value = false - }, 500) + setTimeout(() => previewLoading.value = false, 500) }) } @@ -258,18 +486,29 @@ const closeDialog = () => { dialog.value = false } +// --- 监听器和生命周期 --- + watch(dialog, (newVal) => { - if (newVal && !templateContent.value) { - loadTemplate() + if (newVal) { + loadInitialData() + } else { + // 关闭时重置状态 + selectedTemplate.value = null + templateContent.value = '' + isCreatingNew.value = false + } +}) + +watch(selectedTemplate, (newName) => { + if (newName) { + isCreatingNew.value = false + loadTemplateContent(newName) } }) defineExpose({ openDialog: () => { dialog.value = true - if (!templateContent.value) { - loadTemplate() - } } })