feat: 完成T-302p提示词管理
新增app/prompts.py,支持标题提示词读写、封面模板列表/读取/保存/重命名/删除,以及旧标题、新标题、商品id、店铺变量替换。
Tab②接入标题提示词启动回显和保存,封面模板下拉、新建、保存、另存为、重命名、删除、插入{新标题}与变量预览;本任务仍不调用AI、不写SQLite。
新增tests/test_prompts.py并扩展GUI测试覆盖提示词文件管理和预览;同步任务看板、API、routes、current-state和progress,下一个任务更新为T-303。
This commit is contained in:
+133
@@ -0,0 +1,133 @@
|
||||
"""Prompt file management and variable rendering."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
|
||||
TITLE_PROMPT_PATH = "title_prompt.txt"
|
||||
COVER_PROMPTS_DIR = os.path.join("prompts", "cover")
|
||||
TEMPLATE_EXT = ".txt"
|
||||
INVALID_NAME_CHARS = set('\\/:*?"<>|')
|
||||
|
||||
|
||||
class PromptError(RuntimeError):
|
||||
"""Raised when prompt files or template names are invalid."""
|
||||
|
||||
|
||||
def load_title_prompt(path=TITLE_PROMPT_PATH) -> str:
|
||||
"""Load the title prompt text. Missing file means an empty prompt."""
|
||||
|
||||
if not os.path.exists(path):
|
||||
return ""
|
||||
with open(path, "r", encoding="utf-8") as fh:
|
||||
return fh.read()
|
||||
|
||||
|
||||
def save_title_prompt(text, path=TITLE_PROMPT_PATH) -> None:
|
||||
"""Save the title prompt text as UTF-8."""
|
||||
|
||||
directory = os.path.dirname(os.path.abspath(path))
|
||||
if directory:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
fh.write(str(text or ""))
|
||||
|
||||
|
||||
def list_cover_templates(directory=COVER_PROMPTS_DIR):
|
||||
"""Return cover template names sorted by display name."""
|
||||
|
||||
if not os.path.isdir(directory):
|
||||
return []
|
||||
names = []
|
||||
for filename in os.listdir(directory):
|
||||
if filename.lower().endswith(TEMPLATE_EXT):
|
||||
names.append(filename[: -len(TEMPLATE_EXT)])
|
||||
return sorted(names, key=str.casefold)
|
||||
|
||||
|
||||
def load_cover_template(name, directory=COVER_PROMPTS_DIR) -> str:
|
||||
"""Load one cover prompt template."""
|
||||
|
||||
path = _template_path(name, directory)
|
||||
if not os.path.exists(path):
|
||||
raise PromptError(f"封面提示词模板不存在: {_normalize_name(name)}")
|
||||
with open(path, "r", encoding="utf-8") as fh:
|
||||
return fh.read()
|
||||
|
||||
|
||||
def save_cover_template(name, text, directory=COVER_PROMPTS_DIR) -> None:
|
||||
"""Save one cover prompt template as UTF-8."""
|
||||
|
||||
path = _template_path(name, directory)
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
fh.write(str(text or ""))
|
||||
|
||||
|
||||
def rename_cover_template(old, new, directory=COVER_PROMPTS_DIR) -> None:
|
||||
"""Rename a cover prompt template with duplicate-name protection."""
|
||||
|
||||
old_path = _template_path(old, directory)
|
||||
new_path = _template_path(new, directory)
|
||||
if not os.path.exists(old_path):
|
||||
raise PromptError(f"封面提示词模板不存在: {_normalize_name(old)}")
|
||||
if os.path.exists(new_path):
|
||||
raise PromptError(f"封面提示词模板已存在: {_normalize_name(new)}")
|
||||
os.makedirs(os.path.dirname(new_path), exist_ok=True)
|
||||
os.replace(old_path, new_path)
|
||||
|
||||
|
||||
def delete_cover_template(name, directory=COVER_PROMPTS_DIR) -> None:
|
||||
"""Delete one cover prompt template."""
|
||||
|
||||
path = _template_path(name, directory)
|
||||
if not os.path.exists(path):
|
||||
raise PromptError(f"封面提示词模板不存在: {_normalize_name(name)}")
|
||||
os.remove(path)
|
||||
|
||||
|
||||
def render_prompt(template_text, task) -> str:
|
||||
"""Render known task variables in a prompt template."""
|
||||
|
||||
values = {
|
||||
"旧标题": _task_value(task, "old_title", "旧标题"),
|
||||
"新标题": _task_value(task, "new_title", "新标题"),
|
||||
"商品id": _task_value(task, "item_id", "商品id", "商品ID"),
|
||||
"店铺": _task_value(task, "shop", "account_name", "店铺", "alias"),
|
||||
}
|
||||
rendered = str(template_text or "")
|
||||
for name, value in values.items():
|
||||
rendered = rendered.replace("{" + name + "}", value)
|
||||
return rendered
|
||||
|
||||
|
||||
def _task_value(task, *names) -> str:
|
||||
for name in names:
|
||||
value = None
|
||||
if isinstance(task, dict):
|
||||
value = task.get(name)
|
||||
else:
|
||||
value = getattr(task, name, None)
|
||||
if value is not None:
|
||||
return str(value)
|
||||
return ""
|
||||
|
||||
|
||||
def _template_path(name, directory) -> str:
|
||||
normalized = _normalize_name(name)
|
||||
return os.path.abspath(os.path.join(directory, normalized + TEMPLATE_EXT))
|
||||
|
||||
|
||||
def _normalize_name(name) -> str:
|
||||
value = str(name or "").strip()
|
||||
if value.lower().endswith(TEMPLATE_EXT):
|
||||
value = value[: -len(TEMPLATE_EXT)]
|
||||
value = value.strip()
|
||||
if not value:
|
||||
raise PromptError("封面提示词模板名不能为空")
|
||||
if value in {".", ".."} or any(char in INVALID_NAME_CHARS for char in value):
|
||||
raise PromptError(f"封面提示词模板名非法: {value}")
|
||||
if os.path.basename(value) != value:
|
||||
raise PromptError(f"封面提示词模板名非法: {value}")
|
||||
return value
|
||||
Reference in New Issue
Block a user