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 }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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()
- }
}
})