From d2d5ef1c5c50040280cfeca5a90092379a2b882f Mon Sep 17 00:00:00 2001 From: Soulter <37870767+Soulter@users.noreply.github.com> Date: Sun, 31 Aug 2025 12:11:55 +0900 Subject: [PATCH] feat: add custom T2I template editor (#2581) --- astrbot/core/config/default.py | 11 +- astrbot/core/utils/t2i/network_strategy.py | 23 +- astrbot/dashboard/routes/config.py | 50 ++- .../src/components/shared/AstrBotConfigV4.vue | 4 + .../components/shared/T2ITemplateEditor.vue | 307 ++++++++++++++++++ 5 files changed, 387 insertions(+), 8 deletions(-) create mode 100644 dashboard/src/components/shared/T2ITemplateEditor.vue diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index cfcb74362..4e432002a 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -2261,13 +2261,22 @@ CONFIG_METADATA_3_SYSTEM = { "options": ["remote", "local"], }, "t2i_endpoint": { - "description": "文本转图像服务接口", + "description": "文本转图像服务 API 地址", "type": "string", "hint": "为空时使用 AstrBot API 服务", "condition": { "t2i_strategy": "remote", }, }, + "t2i_template": { + "description": "文本转图像自定义模版", + "type": "bool", + "hint": "启用后可自定义 HTML 模板用于文转图渲染。", + "condition": { + "t2i_strategy": "remote", + }, + "_special": "t2i_template" + }, "log_level": { "description": "控制台日志级别", "type": "string", diff --git a/astrbot/core/utils/t2i/network_strategy.py b/astrbot/core/utils/t2i/network_strategy.py index 963c2f5dd..2295f051b 100644 --- a/astrbot/core/utils/t2i/network_strategy.py +++ b/astrbot/core/utils/t2i/network_strategy.py @@ -8,8 +8,10 @@ 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 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") @@ -21,7 +23,9 @@ 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") + 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] @@ -29,6 +33,17 @@ class NetworkRenderStrategy(RenderStrategy): 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_official_endpoints(self): """获取官方的 t2i 端点列表。""" try: @@ -113,11 +128,7 @@ class NetworkRenderStrategy(RenderStrategy): """ 返回图像的文件路径 """ - with open( - os.path.join(self.TEMPLATE_PATH, "base.html"), "r", encoding="utf-8" - ) as f: - tmpl_str = f.read() - assert tmpl_str + tmpl_str = await self.get_template() text = text.replace("`", "\\`") return await self.render_custom_template( tmpl_str, {"text": text, "version": f"v{VERSION}"}, return_url diff --git a/astrbot/dashboard/routes/config.py b/astrbot/dashboard/routes/config.py index 120497189..8cb548c62 100644 --- a/astrbot/dashboard/routes/config.py +++ b/astrbot/dashboard/routes/config.py @@ -16,9 +16,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 +from astrbot.core import logger, html_renderer from astrbot.core.provider import Provider import asyncio +from astrbot.core.utils.t2i.network_strategy import CUSTOM_T2I_TEMPLATE_PATH def try_cast(value: str, type_: str): @@ -184,9 +185,56 @@ 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() diff --git a/dashboard/src/components/shared/AstrBotConfigV4.vue b/dashboard/src/components/shared/AstrBotConfigV4.vue index b5f418b5c..228df2f0b 100644 --- a/dashboard/src/components/shared/AstrBotConfigV4.vue +++ b/dashboard/src/components/shared/AstrBotConfigV4.vue @@ -6,6 +6,7 @@ import ProviderSelector from './ProviderSelector.vue' import PersonaSelector from './PersonaSelector.vue' import KnowledgeBaseSelector from './KnowledgeBaseSelector.vue' import PluginSetSelector from './PluginSetSelector.vue' +import T2ITemplateEditor from './T2ITemplateEditor.vue' import { useI18n } from '@/i18n/composables' @@ -246,6 +247,9 @@ function hasVisibleItemsAfter(items, currentIndex) { v-model="createSelectorModel(itemKey).value" /> +
+ +
diff --git a/dashboard/src/components/shared/T2ITemplateEditor.vue b/dashboard/src/components/shared/T2ITemplateEditor.vue new file mode 100644 index 000000000..6e19bbafe --- /dev/null +++ b/dashboard/src/components/shared/T2ITemplateEditor.vue @@ -0,0 +1,307 @@ +