Skip to content

Commit 47eeff3

Browse files
lekoOwOSXP-Simon
andauthored
fix(template-utils): str.format -> string.Template (#149)
* feat: upgrade prompt templates from str.format to string.Template for improved compatibility * fix(get_astrbot_data_path): fallback 到 get_astrbot_data_path 方法 * feat: upgrade prompt templates from str.format to string.Template for improved compatibility * fix: strict mode * fix: 补齐文件名格式的自动迁移 * fix(ChatQualityAnalyzer): 删除无效的 prompt 提示语句 * ruff --------- Co-authored-by: SXP-Simon <sxp20061207@163.com>
1 parent 5e2da8c commit 47eeff3

9 files changed

Lines changed: 232 additions & 37 deletions

File tree

_conf_schema.json

Lines changed: 12 additions & 12 deletions
Large diffs are not rendered by default.

main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,12 @@ async def _run_initialization(self, source: str):
220220

221221
logger.info(f"正在执行插件初始化 (来源: {source})...")
222222

223+
# 0. 自动升级旧版 prompt 模板(str.format -> string.Template)并回写配置
224+
try:
225+
self.config_manager.upgrade_prompt_templates()
226+
except Exception as e:
227+
logger.warning(f"自动升级 prompt 模板失败: {e}")
228+
223229
# 1. 尝试发现 bot 实例
224230
await self.bot_manager.initialize_from_config()
225231

src/infrastructure/analysis/analyzers/chat_quality_analyzer.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from ....domain.models.data_models import QualityDimension, QualityReview, TokenUsage
99
from ....utils.logger import logger
10+
from ...utils.template_utils import render_template
1011
from ..utils import InfoUtils
1112
from ..utils.json_utils import extract_quality_with_regex, parse_json_object_response
1213
from ..utils.llm_utils import (
@@ -85,8 +86,10 @@ def build_prompt(self, data: list[dict]) -> str:
8586

8687
prompt_template = self.config_manager.get_quality_analysis_prompt()
8788

88-
if not prompt_template:
89-
prompt_template = """你是一个毒舌且幽默的群聊质量分析师。
89+
if prompt_template:
90+
return render_template(prompt_template, messages_text=messages_text)
91+
92+
prompt_template = """你是一个毒舌且幽默的群聊质量分析师。
9093
请分析以下群聊记录,输出一份"聊天质量锐评"。
9194
9295
## 任务目标:
@@ -120,10 +123,10 @@ def build_prompt(self, data: list[dict]) -> str:
120123
```
121124
122125
群聊记录:
123-
{messages_text}
126+
${messages_text}
124127
"""
125128

126-
return prompt_template.format(messages_text=messages_text)
129+
return render_template(prompt_template, messages_text=messages_text)
127130

128131
def extract_with_regex(self, result_text: str, max_count: int) -> list[dict]:
129132
"""
@@ -291,10 +294,10 @@ async def summarize_batch_reviews(
291294
)
292295
reviews_text += f"\n批次 {i + 1} [{title}]:\n- 维度表现: {dims}\n- 核心摘要: {summary}\n"
293296

294-
prompt_template = self.config_manager.get_quality_summary_prompt()
295-
296-
if not prompt_template:
297-
prompt_template = """你是一个毒舌且幽默的群聊质量分析师。
297+
# 获取配置中的汇总提示词模板,如果没有则使用默认模板
298+
prompt_template = (
299+
self.config_manager.get_quality_summary_prompt()
300+
or """你是一个毒舌且幽默的群聊质量分析师。
298301
你现在有一份今天全天分散时间段的多个“增量批次点评笔记”。
299302
你的任务是将这些分散的笔记汇总成一份最终的“全天聊天质量终极锐评”。
300303
@@ -327,7 +330,8 @@ async def summarize_batch_reviews(
327330
}}
328331
```
329332
"""
330-
prompt = prompt_template.format(reviews_text=reviews_text)
333+
)
334+
prompt = render_template(prompt_template, reviews_text=reviews_text)
331335

332336
# 调用 LLM 进行汇总
333337
system_prompt = await self._build_system_prompt(umo)

src/infrastructure/analysis/analyzers/golden_quote_analyzer.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from ....domain.models.data_models import GoldenQuote, TokenUsage
99
from ....utils.logger import logger
10+
from ...utils.template_utils import render_template
1011
from ..utils import InfoUtils
1112
from ..utils.json_utils import extract_golden_quotes_with_regex
1213
from ..utils.response_validation import validate_golden_quote_items
@@ -64,15 +65,14 @@ def build_prompt(self, data: list[dict]) -> str:
6465
prompt_template = self.config_manager.get_golden_quote_analysis_prompt()
6566

6667
if prompt_template:
67-
# 使用配置中的 prompt 并替换变量
6868
try:
69-
prompt = prompt_template.format(
70-
max_golden_quotes=max_golden_quotes, messages_text=messages_text
69+
prompt = render_template(
70+
prompt_template,
71+
max_golden_quotes=max_golden_quotes,
72+
messages_text=messages_text,
7173
)
7274
logger.info("使用配置中的金句分析提示词")
7375
return prompt
74-
except KeyError as e:
75-
logger.warning(f"金句分析提示词变量格式错误: {e}")
7676
except Exception as e:
7777
logger.warning(f"应用金句分析提示词失败: {e}")
7878

src/infrastructure/analysis/analyzers/topic_analyzer.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from ....domain.models.data_models import SummaryTopic, TokenUsage
1010
from ....utils.logger import logger
11+
from ...utils.template_utils import render_template
1112
from ..utils import InfoUtils
1213
from ..utils.json_utils import extract_topics_with_regex
1314
from ..utils.response_validation import validate_topic_items
@@ -162,15 +163,14 @@ def build_prompt(self, data: list[dict]) -> str:
162163
prompt_template = self.config_manager.get_topic_analysis_prompt()
163164

164165
if prompt_template:
165-
# 使用配置中的 prompt 并替换变量
166166
try:
167-
prompt = prompt_template.format(
168-
max_topics=max_topics, messages_text=messages_text
167+
prompt = render_template(
168+
prompt_template,
169+
max_topics=max_topics,
170+
messages_text=messages_text,
169171
)
170172
logger.info("使用配置中的话题分析提示词")
171173
return prompt
172-
except KeyError as e:
173-
logger.warning(f"话题分析提示词变量格式错误: {e}")
174174
except Exception as e:
175175
logger.warning(f"应用话题分析提示词失败: {e}")
176176

src/infrastructure/analysis/analyzers/user_title_analyzer.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from ....domain.models.data_models import TokenUsage, UserTitle
77
from ....utils.logger import logger
8+
from ...utils.template_utils import render_template
89
from ..utils.json_utils import extract_user_titles_with_regex
910
from ..utils.response_validation import validate_user_title_items
1011
from ..utils.structured_output_schema import JSONObject, build_user_titles_schema
@@ -65,13 +66,10 @@ def build_prompt(self, data: dict) -> str:
6566
prompt_template = self.config_manager.get_user_title_analysis_prompt()
6667

6768
if prompt_template:
68-
# 使用配置中的 prompt 并替换变量
6969
try:
70-
prompt = prompt_template.format(users_text=users_text)
70+
prompt = render_template(prompt_template, users_text=users_text)
7171
logger.info("使用配置中的用户称号分析提示词")
7272
return prompt
73-
except KeyError as e:
74-
logger.warning(f"用户称号分析提示词变量格式错误: {e}")
7573
except Exception as e:
7674
logger.warning(f"应用用户称号分析提示词失败: {e}")
7775

src/infrastructure/config/config_manager.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from astrbot.api.star import StarTools
1010

1111
from ...utils.logger import logger
12+
from ..utils.template_utils import upgrade_str_format_template
1213

1314

1415
class ConfigManager:
@@ -332,6 +333,80 @@ def set_quality_analysis_prompt(self, prompt: str):
332333
prompts["quality_analysis_prompts"]["quality_v2_prompt"] = prompt
333334
self.config.save_config()
334335

336+
def _upgrade_config_item(self, group: str, key: str, setter_func):
337+
"""升级指定配置项的值(从 str.format -> string.Template),并回写。"""
338+
# 如果是 prompts,则先取 prompts 分组,再取子分组 (group)
339+
if group in (
340+
"quality_analysis_prompts",
341+
"topic_analysis_prompts",
342+
"user_title_analysis_prompts",
343+
"golden_quote_analysis_prompts",
344+
):
345+
target_group = self._get_group("prompts").get(group, {})
346+
else:
347+
target_group = self._get_group(group)
348+
349+
val = target_group.get(key, "")
350+
if not val or not isinstance(val, str):
351+
return False
352+
353+
upgraded_val, upgraded = upgrade_str_format_template(val)
354+
if upgraded and upgraded_val != val:
355+
setter_func(upgraded_val)
356+
logger.info(
357+
f"配置项 {group}.{key} 发现旧版语法并已自动升级为 string.Template 格式。"
358+
)
359+
return True
360+
return False
361+
362+
def upgrade_prompt_templates(self):
363+
"""启动时调用,扫描并升级所有可配置的模板(含 prompt 和文件名)。"""
364+
modified = False
365+
# 1. 提示词模板升级
366+
modified |= self._upgrade_config_item(
367+
"quality_analysis_prompts",
368+
"quality_v2_prompt",
369+
self.set_quality_analysis_prompt,
370+
)
371+
modified |= self._upgrade_config_item(
372+
"quality_analysis_prompts",
373+
"quality_summary_prompt",
374+
self.set_quality_summary_prompt,
375+
)
376+
modified |= self._upgrade_config_item(
377+
"topic_analysis_prompts",
378+
"topic_prompt",
379+
self.set_topic_analysis_prompt,
380+
)
381+
modified |= self._upgrade_config_item(
382+
"user_title_analysis_prompts",
383+
"user_title_prompt",
384+
self.set_user_title_analysis_prompt,
385+
)
386+
modified |= self._upgrade_config_item(
387+
"golden_quote_analysis_prompts",
388+
"golden_quote_v2_prompt",
389+
self.set_golden_quote_analysis_prompt,
390+
)
391+
392+
# 2. 文件名格式升级
393+
modified |= self._upgrade_config_item(
394+
"pdf",
395+
"pdf_filename_format",
396+
self.set_pdf_filename_format,
397+
)
398+
modified |= self._upgrade_config_item(
399+
"html",
400+
"html_filename_format",
401+
self.set_html_filename_format,
402+
)
403+
404+
if modified:
405+
logger.info(
406+
"已完成所有配置模板从 str.format 到 string.Template 的安全迁移。(已自动回写配置)"
407+
)
408+
return modified
409+
335410
def get_quality_summary_prompt(self, style: str = "quality_summary_prompt") -> str:
336411
"""获取聊天质量汇总分析提示词模板"""
337412
prompts_config = self._get_group("prompts").get("quality_analysis_prompts", {})
@@ -542,6 +617,11 @@ def set_pdf_filename_format(self, format_str: str):
542617
self._ensure_group("pdf")["pdf_filename_format"] = format_str
543618
self.config.save_config()
544619

620+
def set_html_filename_format(self, format_str: str):
621+
"""设置HTML文件名格式"""
622+
self._ensure_group("html")["html_filename_format"] = format_str
623+
self.config.save_config()
624+
545625
def get_report_template(self) -> str:
546626
"""获取报告模板名称"""
547627
return self._get_group("basic").get("report_template", "scrapbook")

src/infrastructure/reporting/generators.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
from ...domain.repositories.report_repository import IReportGenerator
2323
from ...utils.logger import logger
24+
from ..utils.template_utils import render_template
2425
from ..visualization.activity_charts import ActivityVisualizer
2526
from .templates import HTMLTemplates
2627

@@ -94,9 +95,9 @@ def _build_safe_report_path(
9495
}
9596

9697
try:
97-
formatted = filename_format.format(**safe_context)
98+
formatted = render_template(filename_format, strict=True, **safe_context)
9899
except Exception as e:
99-
raise ValueError(f"文件名格式化失败: {e}") from e
100+
raise ValueError(f"文件名模板渲染失败: {e}") from e
100101

101102
if os.path.isabs(formatted):
102103
raise ValueError("文件名格式不得为绝对路径")
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""安全模板渲染工具(String Template 兼容)"""
2+
3+
import re
4+
from string import Template
5+
6+
from ...utils.logger import logger
7+
8+
# 统一默认 placeholder
9+
PLACEHOLDERS = {
10+
# 分析类核心变量
11+
"messages_text": "${messages_text}",
12+
"reviews_text": "${reviews_text}",
13+
"max_topics": "${max_topics}",
14+
"users_text": "${users_text}",
15+
"max_golden_quotes": "${max_golden_quotes}",
16+
# 文件名渲染类变量
17+
"group_id": "${group_id}",
18+
"date": "${date}",
19+
"ulid": "${ulid}",
20+
}
21+
22+
23+
def is_str_format_template(template: str) -> bool:
24+
"""判断模板是否为 str.format 风格。
25+
26+
只认为满足:
27+
1) 不包含 String Template `${var}` 或 `$var`
28+
2) 包含 str.format `{var}`(非 `{{...}}`)
29+
"""
30+
if not template:
31+
return False
32+
33+
# 1. 預先建立排除模式 (匹配 ${var} 或 $var)
34+
# 使用 set 去重並組合
35+
dollar_patterns = [re.escape(v) for v in PLACEHOLDERS.values()] + [
36+
rf"\${re.escape(k)}" for k in PLACEHOLDERS.keys()
37+
]
38+
exclude_regex = "|".join(dollar_patterns)
39+
40+
# 如果包含任何 $ 相關的佔位符,則不視為 str.format 模板
41+
if re.search(exclude_regex, template):
42+
return False
43+
44+
# 2. 檢查是否包含標準的 {key},確保匹配單個花括號包裹的 Key
45+
for key in PLACEHOLDERS.keys():
46+
# (?<!\{) 前面不能有 {
47+
# \{{key}\} 匹配 {key}
48+
# (?!\}) 後面不能有 }
49+
# (?<!\$) 前面不能有 $
50+
pattern = rf"(?<![\{{\$])\{{{key}\}}(?!\}})"
51+
if re.search(pattern, template):
52+
return True
53+
return False
54+
55+
56+
def upgrade_str_format_template(template: str) -> tuple[str, bool]:
57+
"""如果模板是 str.format 风格,则自动升级为 string.Template。
58+
59+
返回 (升级后的模板, 是否升级)
60+
"""
61+
if template is None:
62+
return "", False
63+
64+
if not is_str_format_template(template):
65+
return template, False
66+
67+
# 先转义原文中的 $,避免被 Template 误解释为占位符
68+
safe_template = template.replace("$", "$$")
69+
70+
# 将 {var} 转为 ${var}
71+
safe_template = re.sub(
72+
r"(?<![\{\$])\{([_a-zA-Z][_a-zA-Z0-9]*)\}(?!\})",
73+
lambda m: f"${{{m.group(1)}}}",
74+
safe_template,
75+
)
76+
77+
# 将双括号回退为单括号(str.format 里表示字面量大括号)
78+
safe_template = safe_template.replace("{{", "{").replace("}}", "}")
79+
80+
return safe_template, True
81+
82+
83+
def render_template(template: str, strict: bool = False, **kwargs) -> str:
84+
"""渲染模板(String Template)。
85+
86+
Args:
87+
template: 模板字符串
88+
strict: 是否使用严格模式(变量缺失则抛出异常)
89+
**kwargs: 渲染变量
90+
91+
由于插件启动时已完成 str.format 兼容升级,运行时直接按 string.Template 渲染。
92+
"""
93+
if template is None:
94+
return ""
95+
96+
try:
97+
t = Template(template)
98+
return t.substitute(**kwargs) if strict else t.safe_substitute(**kwargs)
99+
except Exception as e:
100+
if strict:
101+
raise
102+
logger.warning(
103+
f"[template_utils] 模板渲染失败,返回原始文本,错误: {e}",
104+
exc_info=True,
105+
)
106+
return template

0 commit comments

Comments
 (0)