feat(product-suite): add prompt template settings
This commit is contained in:
+112
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from importlib import resources
|
||||
|
||||
from . import appconfig
|
||||
@@ -11,10 +12,14 @@ TITLE_PROMPT_PATH = appconfig.title_prompt_path()
|
||||
TITLE_TEMPLATES_DIR = appconfig.title_templates_dir()
|
||||
COVER_PROMPTS_DIR = appconfig.cover_prompts_dir()
|
||||
IMAGE_STUDIO_PROMPTS_DIR = appconfig.image_studio_prompts_dir()
|
||||
PRODUCT_SUITE_PROMPT_PATH = appconfig.product_suite_prompt_path()
|
||||
TEMPLATE_EXT = ".txt"
|
||||
INVALID_NAME_CHARS = set('\\/:*?"<>|')
|
||||
DEFAULT_PROMPTS_PACKAGE = "app.default_prompts"
|
||||
DEFAULT_COVER_PROMPTS_PACKAGE = "app.default_prompts.cover"
|
||||
DEFAULT_PRODUCT_SUITE_PROMPTS_PACKAGE = "app.default_prompts.product_suite"
|
||||
DEFAULT_PRODUCT_SUITE_PROMPT_NAME = "base.txt"
|
||||
PRODUCT_SUITE_DEFAULT_ERROR = "内置套图提示词模板无效,请重新安装软件或联系技术支持。"
|
||||
|
||||
|
||||
class PromptError(RuntimeError):
|
||||
@@ -69,6 +74,74 @@ def ensure_default_prompts(
|
||||
save_cover_template(name, text, cover_prompts_dir)
|
||||
|
||||
|
||||
def read_product_suite_prompt(path=PRODUCT_SUITE_PROMPT_PATH) -> str:
|
||||
"""Read a product-suite template without hiding invalid user content."""
|
||||
|
||||
if not os.path.exists(path):
|
||||
return ""
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8") as fh:
|
||||
return fh.read()
|
||||
except (OSError, UnicodeError) as exc:
|
||||
raise PromptError("套图提示词模板读取失败:%s" % exc) from exc
|
||||
|
||||
|
||||
def load_product_suite_prompt(path=PRODUCT_SUITE_PROMPT_PATH) -> str:
|
||||
"""Load and validate the current user product-suite template."""
|
||||
|
||||
text = read_product_suite_prompt(path)
|
||||
if not text.strip():
|
||||
raise PromptError("套图提示词模板不存在或为空")
|
||||
_validate_product_suite_prompt(text, "套图提示词模板无效")
|
||||
return text
|
||||
|
||||
|
||||
def load_default_product_suite_prompt() -> str:
|
||||
"""Load and validate the packaged product-suite template."""
|
||||
|
||||
try:
|
||||
text = (
|
||||
resources.files(DEFAULT_PRODUCT_SUITE_PROMPTS_PACKAGE)
|
||||
.joinpath(DEFAULT_PRODUCT_SUITE_PROMPT_NAME)
|
||||
.read_text(encoding="utf-8")
|
||||
)
|
||||
except (FileNotFoundError, ModuleNotFoundError, OSError, UnicodeError) as exc:
|
||||
raise PromptError(PRODUCT_SUITE_DEFAULT_ERROR) from exc
|
||||
try:
|
||||
_validate_product_suite_prompt(text, "内置套图提示词模板无效")
|
||||
except PromptError as exc:
|
||||
raise PromptError(PRODUCT_SUITE_DEFAULT_ERROR) from exc
|
||||
return text
|
||||
|
||||
|
||||
def save_product_suite_prompt(text, path=PRODUCT_SUITE_PROMPT_PATH) -> None:
|
||||
"""Validate and atomically save the user product-suite template."""
|
||||
|
||||
_validate_product_suite_prompt(text, "套图提示词模板无效")
|
||||
_atomic_write_text(path, str(text))
|
||||
|
||||
|
||||
def ensure_default_product_suite_prompt(path=PRODUCT_SUITE_PROMPT_PATH) -> str:
|
||||
"""Seed a validated packaged template without overwriting user content."""
|
||||
|
||||
if os.path.exists(path):
|
||||
current = read_product_suite_prompt(path)
|
||||
if current.strip():
|
||||
_validate_product_suite_prompt(current, "套图提示词模板无效")
|
||||
return current
|
||||
default_text = load_default_product_suite_prompt()
|
||||
save_product_suite_prompt(default_text, path)
|
||||
return default_text
|
||||
|
||||
|
||||
def restore_default_product_suite_prompt(path=PRODUCT_SUITE_PROMPT_PATH) -> str:
|
||||
"""Validate and atomically restore the packaged product-suite template."""
|
||||
|
||||
default_text = load_default_product_suite_prompt()
|
||||
save_product_suite_prompt(default_text, path)
|
||||
return default_text
|
||||
|
||||
|
||||
def list_templates(directory):
|
||||
"""Return prompt template names sorted by display name."""
|
||||
|
||||
@@ -273,6 +346,45 @@ def _has_non_empty_file(path) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _validate_product_suite_prompt(text, prefix) -> None:
|
||||
from . import product_suite
|
||||
|
||||
try:
|
||||
product_suite.validate_product_suite_prompt(text)
|
||||
except product_suite.ProductSuitePromptError as exc:
|
||||
raise PromptError("%s:%s" % (prefix, exc)) from exc
|
||||
|
||||
|
||||
def _atomic_write_text(path, text) -> None:
|
||||
target = os.path.abspath(path)
|
||||
directory = os.path.dirname(target)
|
||||
if directory:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
temporary_path = ""
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w",
|
||||
encoding="utf-8",
|
||||
dir=directory or None,
|
||||
prefix=".prompt-",
|
||||
suffix=".tmp",
|
||||
delete=False,
|
||||
) as handle:
|
||||
temporary_path = handle.name
|
||||
handle.write(str(text))
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
os.replace(temporary_path, target)
|
||||
except OSError as exc:
|
||||
raise PromptError("套图提示词模板保存失败:%s" % exc) from exc
|
||||
finally:
|
||||
if temporary_path and os.path.exists(temporary_path):
|
||||
try:
|
||||
os.remove(temporary_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _read_default_prompt_text(filename) -> str:
|
||||
try:
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user