diff --git a/_conf_schema.json b/_conf_schema.json index 4b06b59..0a47a11 100644 --- a/_conf_schema.json +++ b/_conf_schema.json @@ -356,8 +356,8 @@ "pdf_filename_format": { "type": "string", "description": "PDF文件名格式", - "default": "群聊分析报告_{group_id}_{date}_{ulid}.pdf", - "hint": "PDF文件名格式,支持变量:{group_id}(群号)、{date}(日期)、{ulid}(时间排序ID),支持子目录/层级。" + "default": "群聊分析报告_${group_id}_${date}_${ulid}.pdf", + "hint": "PDF文件名格式,支持变量:${group_id}(群号)、${date}(日期)、${ulid}(时间排序ID),支持子目录/层级。" } } }, @@ -381,8 +381,8 @@ "html_filename_format": { "type": "string", "description": "HTML文件名格式", - "default": "群聊分析报告_{group_id}_{date}_{ulid}.html", - "hint": "HTML文件名格式,支持变量:{group_id}(群号)、{date}(日期)、{ulid}(时间排序ID),支持子目录/层级。" + "default": "群聊分析报告_${group_id}_${date}_${ulid}.html", + "hint": "HTML文件名格式,支持变量:${group_id}(群号)、${date}(日期)、${ulid}(时间排序ID),支持子目录/层级。" } } }, @@ -431,42 +431,42 @@ "topic_analysis_prompts": { "description": "话题分析提示词模板", "type": "object", - "hint": "话题分析提示词模板,可自定义修改,保留变量 {max_topics} 和 {messages_text} 写法不要更改,需要写 JSON 块请使用双花括号 {{ ... }} 兼容当前编辑存在的问题。非常推荐根据实际群聊情况进行优化,保留一定提示样本示例供模型参考", + "hint": "话题分析提示词模板,可自定义修改,保留变量 ${max_topics} 和 ${messages_text} 写法不要更改,需要写 JSON 块请使用双花括号 {{ ... }} 兼容当前编辑存在的问题。非常推荐根据实际群聊情况进行优化,保留一定提示样本示例供模型参考", "items": { "topic_prompt": { "description": "默认话题分析提示词", "type": "text", "editor_mode": true, "editor_language": "markdown", - "default": "你是一个帮我进行群聊信息总结的助手,生成总结内容时,你需要严格遵守下面的几个准则:\n\n请分析接下来提供的群聊记录,提取出最多 **{max_topics}** 个主要话题。根据实际聊天内容提取所有最有意义的话题。\n\n## 对于每个话题,请提供:\n\n1. **话题名称**(突出主题内容,尽量简明扼要,控制在 10 字以内)\n2. **主要参与者的用户ID**(最多 5 人,按参与度排序)\n3. **话题详细描述**(包含关键信息和结论)\n\n## 注意事项:\n\n- 对于比较有价值的点,稍微用一两句话详细讲讲,让读者能了解讨论的深度\n- 对于其中的部分信息,你需要特意提到主题施加的主体是谁,即明确指出\"谁做了什么\"\n- **用户引用**:在话题详情描述中,如果提到了具体用户,请使用 `[用户ID]` 的格式来指代(例如 `[123456]`)。不要只写昵称。我们会自动渲染头像。\n- 对于每一条总结,尽量讲清楚前因后果,不要只列出结论\n- 如果某个话题有明确的结论或共识,请在描述中体现\n- 忽略无意义的闲聊、灌水、单纯的表情回复等\n- 优先选择讨论深度较深、参与人数较多的话题\n- 如果消息太少或没有明确话题,可以返回空数组 []\n\n群聊记录格式: [HH:MM] [用户ID]: 消息内容\n\n群聊记录:\n{messages_text}\n\n---\n\n## 重要:必须返回标准 JSON 格式\n\n严格遵守以下规则:\n\n1. 只使用英文双引号 `\"` ,不要使用中文引号 `“` `”`\n2. 字符串内容中的引号必须转义为 `\\\"`\n3. 多个对象之间用逗号分隔\n4. 数组元素之间用逗号分隔\n5. 不要在 JSON 外添加任何文字说明\n6. 描述内容避免使用特殊符号,用普通文字表达\n\n### 返回格式示例:\n\n```json\n[\n {{\n \"topic\": \"话题名称\",\n \"contributors\": [\"123456789\", \"987654321\"],\n \"detail\": \"话题的详细描述,包含讨论内容、关键信息和结论。注意:在描述中提及用户时,使用 [用户ID] 格式,例如 [123456789]。\"\n }},\n {{\n \"topic\": \"另一个话题\",\n \"contributors\": [\"111222333\", \"444555666\"],\n \"detail\": \"另一个话题的详细描述...\"\n }}\n]\n```\n\n**注意**:返回的内容必须是纯 JSON,不要包含 markdown 代码块标记或其他格式。" + "default": "你是一个帮我进行群聊信息总结的助手,生成总结内容时,你需要严格遵守下面的几个准则:\n\n请分析接下来提供的群聊记录,提取出最多 **${max_topics}** 个主要话题。根据实际聊天内容提取所有最有意义的话题。\n\n## 对于每个话题,请提供:\n\n1. **话题名称**(突出主题内容,尽量简明扼要,控制在 10 字以内)\n2. **主要参与者的用户ID**(最多 5 人,按参与度排序)\n3. **话题详细描述**(包含关键信息和结论)\n\n## 注意事项:\n\n- 对于比较有价值的点,稍微用一两句话详细讲讲,让读者能了解讨论的深度\n- 对于其中的部分信息,你需要特意提到主题施加的主体是谁,即明确指出\"谁做了什么\"\n- **用户引用**:在话题详情描述中,如果提到了具体用户,请使用 `[用户ID]` 的格式来指代(例如 `[123456]`)。不要只写昵称。我们会自动渲染头像。\n- 对于每一条总结,尽量讲清楚前因后果,不要只列出结论\n- 如果某个话题有明确的结论或共识,请在描述中体现\n- 忽略无意义的闲聊、灌水、单纯的表情回复等\n- 优先选择讨论深度较深、参与人数较多的话题\n- 如果消息太少或没有明确话题,可以返回空数组 []\n\n群聊记录格式: [HH:MM] [用户ID]: 消息内容\n\n群聊记录:\n${messages_text}\n\n---\n\n## 重要:必须返回标准 JSON 格式\n\n严格遵守以下规则:\n\n1. 只使用英文双引号 `\"` ,不要使用中文引号 `“` `”`\n2. 字符串内容中的引号必须转义为 `\\\"`\n3. 多个对象之间用逗号分隔\n4. 数组元素之间用逗号分隔\n5. 不要在 JSON 外添加任何文字说明\n6. 描述内容避免使用特殊符号,用普通文字表达\n\n### 返回格式示例:\n\n```json\n[\n {{\n \"topic\": \"话题名称\",\n \"contributors\": [\"123456789\", \"987654321\"],\n \"detail\": \"话题的详细描述,包含讨论内容、关键信息和结论。注意:在描述中提及用户时,使用 [用户ID] 格式,例如 [123456789]。\"\n }},\n {{\n \"topic\": \"另一个话题\",\n \"contributors\": [\"111222333\", \"444555666\"],\n \"detail\": \"另一个话题的详细描述...\"\n }}\n]\n```\n\n**注意**:返回的内容必须是纯 JSON,不要包含 markdown 代码块标记或其他格式。" } } }, "user_title_analysis_prompts": { "description": "用户称号分析提示词模板", "type": "object", - "hint": "用户称号分析提示词模板,可自定义修改,保留变量 {users_text} 写法不要更改,需要写 JSON 块请使用双花括号 {{ ... }} 兼容当前编辑存在的问题。非常推荐根据实际群聊情况进行优化,保留一定提示样本示例供模型参考", + "hint": "用户称号分析提示词模板,可自定义修改,保留变量 ${users_text} 写法不要更改,需要写 JSON 块请使用双花括号 {{ ... }} 兼容当前编辑存在的问题。非常推荐根据实际群聊情况进行优化,保留一定提示样本示例供模型参考", "items": { "user_title_prompt": { "description": "默认用户称号分析提示词", "type": "text", "editor_mode": true, "editor_language": "markdown", - "default": "请为以下群友分配合适的称号和 MBTI 类型。\n\n## 规则:\n\n- 每个人只能有一个称号\n- 每个称号只能给一个人\n\n## 可选称号:\n\n- **龙王**: 发言频繁但内容轻松的人\n- **技术专家**: 经常讨论技术话题的人\n- **夜猫子**: 经常在深夜发言的人\n- **表情包军火库**: 经常发表情的人\n- **沉默终结者**: 经常开启话题的人\n- **评论家**: 平均发言长度很长的人\n- **阳角**: 在群里很有影响力的人\n- **互动达人**: 经常回复别人的人\n- *...(你可以自行进行拓展添加)*\n\n## 用户数据:\n\n{users_text}\n\n---\n\n### 返回格式示例:\n\n```json\n[\n {{\n \"name\": \"用户名\",\n \"user_id\": \"123456789\",\n \"title\": \"称号\",\n \"mbti\": \"MBTI类型\",\n \"reason\": \"获得此称号的原因\"\n }}\n]\n```\n\n**注意**:请以纯 JSON 格式返回,不要包含 markdown 代码块标记。" + "default": "请为以下群友分配合适的称号和 MBTI 类型。\n\n## 规则:\n\n- 每个人只能有一个称号\n- 每个称号只能给一个人\n\n## 可选称号:\n\n- **龙王**: 发言频繁但内容轻松的人\n- **技术专家**: 经常讨论技术话题的人\n- **夜猫子**: 经常在深夜发言的人\n- **表情包军火库**: 经常发表情的人\n- **沉默终结者**: 经常开启话题的人\n- **评论家**: 平均发言长度很长的人\n- **阳角**: 在群里很有影响力的人\n- **互动达人**: 经常回复别人的人\n- *...(你可以自行进行拓展添加)*\n\n## 用户数据:\n\n${users_text}\n\n---\n\n### 返回格式示例:\n\n```json\n[\n {{\n \"name\": \"用户名\",\n \"user_id\": \"123456789\",\n \"title\": \"称号\",\n \"mbti\": \"MBTI类型\",\n \"reason\": \"获得此称号的原因\"\n }}\n]\n```\n\n**注意**:请以纯 JSON 格式返回,不要包含 markdown 代码块标记。" } } }, "golden_quote_analysis_prompts": { "description": "金句分析提示词模板", "type": "object", - "hint": "金句分析提示词模板,可自定义修改,保留变量 {max_golden_quotes} 和 {messages_text} 写法不要更改,需要写 JSON 块请使用双花括号 {{ ... }} 兼容当前编辑存在的问题。非常推荐根据实际群聊情况进行优化,保留一定提示样本示例供模型参考", + "hint": "金句分析提示词模板,可自定义修改,保留变量 ${max_golden_quotes} 和 ${messages_text} 写法不要更改,需要写 JSON 块请使用双花括号 {{ ... }} 兼容当前编辑存在的问题。非常推荐根据实际群聊情况进行优化,保留一定提示样本示例供模型参考", "items": { "golden_quote_v2_prompt": { "description": "默认金句分析提示词", "type": "text", "editor_mode": true, "editor_language": "markdown", - "default": "请从以下群聊记录中挑选出 **{max_golden_quotes}** 句最具冲击力、最令人惊叹的「金句」。\n\n## 金句标准:\n\n- **核心标准**:**逆天的神人发言**,即具备颠覆常识的脑洞、逻辑跳脱的表达或强烈反差感的原创内容\n- **典型特征**:包含某些争议话题元素、夸张类比、反常规结论、一本正经的「胡说八道」或突破语境的清奇思路,并且具备一定的冲击力,让人印象深刻\n\n## 对于每个金句,请提供:\n\n1. **原文内容**(完整保留发言细节)\n2. **发言人用户ID**(必须严格使用消息记录中提供的 [用户ID])\n3. **选择理由**(具体说明其「逆天」之处,如逻辑颠覆点/脑洞角度/反差感/争议话题元素)\n\n## 严格约束:\n\n- 优先筛选 **逆天指数最高** 的内容:\n - 发情、性压抑话题 > 争议话题 > 元素级 > 颠覆认知级 > 逻辑跳脱级 > 趣味调侃级\n - 剔除单纯玩梗或网络热词堆砌的普通发言\n- **用户引用**:在选择理由(reason)中,如果提到了具体用户,请使用 `[用户ID]` 的格式来指代(例如 `[123456]`)。不要只写昵称。我们会自动渲染头像。\n- **身份对齐**:返回的 `sender` 字段必须是 `[用户ID]` 格式(例如 `[123456]`)。我们会根据 ID 自动还原昵称和头像。\n\n## 群聊记录格式: [HH:MM] [用户ID]: 消息内容\n\n## 群聊记录:\n\n{messages_text}\n\n---\n\n### 返回格式示例:\n\n```json\n[\n {{\n \"content\": \"金句原文\",\n \"sender\": \"[123456789]\",\n \"reason\": \"这句话太逆天了,尤其是对 [987654321] 的逻辑降维打击。\"\n }}\n]\n```\n\n**注意**:返回的内容必须是纯 JSON,不要包含 markdown 代码块标记或其他格式。" + "default": "请从以下群聊记录中挑选出 **${max_golden_quotes}** 句最具冲击力、最令人惊叹的「金句」。\n\n## 金句标准:\n\n- **核心标准**:**逆天的神人发言**,即具备颠覆常识的脑洞、逻辑跳脱的表达或强烈反差感的原创内容\n- **典型特征**:包含某些争议话题元素、夸张类比、反常规结论、一本正经的「胡说八道」或突破语境的清奇思路,并且具备一定的冲击力,让人印象深刻\n\n## 对于每个金句,请提供:\n\n1. **原文内容**(完整保留发言细节)\n2. **发言人用户ID**(必须严格使用消息记录中提供的 [用户ID])\n3. **选择理由**(具体说明其「逆天」之处,如逻辑颠覆点/脑洞角度/反差感/争议话题元素)\n\n## 严格约束:\n\n- 优先筛选 **逆天指数最高** 的内容:\n - 发情、性压抑话题 > 争议话题 > 元素级 > 颠覆认知级 > 逻辑跳脱级 > 趣味调侃级\n - 剔除单纯玩梗或网络热词堆砌的普通发言\n- **用户引用**:在选择理由(reason)中,如果提到了具体用户,请使用 `[用户ID]` 的格式来指代(例如 `[123456]`)。不要只写昵称。我们会自动渲染头像。\n- **身份对齐**:返回的 `sender` 字段必须是 `[用户ID]` 格式(例如 `[123456]`)。我们会根据 ID 自动还原昵称和头像。\n\n## 群聊记录格式: [HH:MM] [用户ID]: 消息内容\n\n## 群聊记录:\n\n${messages_text}\n\n---\n\n### 返回格式示例:\n\n```json\n[\n {{\n \"content\": \"金句原文\",\n \"sender\": \"[123456789]\",\n \"reason\": \"这句话太逆天了,尤其是对 [987654321] 的逻辑降维打击。\"\n }}\n]\n```\n\n**注意**:返回的内容必须是纯 JSON,不要包含 markdown 代码块标记或其他格式。" } } }, @@ -480,14 +480,14 @@ "type": "text", "editor_mode": true, "editor_language": "markdown", - "default": "你是一个毒舌且幽默的群聊质量分析师。\n请分析以下群聊记录,输出一份\"聊天质量锐评\"。\n\n## 任务目标:\n1. **维度划分**:将聊天内容划分为 3-6 个【高层级、抽象、泛化】的维度(例如:就业焦虑、生涯规划、技术方案研究、情感树洞、无意义水群等)。\n2. **严禁在维度名称(name)中出现任何具体的群聊人物名、项目名、具体的报错内容或细碎的事件点。标题必须保持高度抽象且字数简练(2-6个字)。**\n3. 为每个维度计算一个大致的百分比占位(总和小于等于 100%)。\n4. **点评内容**:为每个维度写一句犀利、幽默、毒舌或温情的点评。具体的吐槽内容、具体的细节事件描述请放在这里。\n5. **全群表现**:给出一句总结性的评价,作为总结标题对应的“金句”。\n6. **主题设定**:设定一个本次报告的主题标题和副标题。\n\n## 点评风格指南:\n- 语言要接地气,多用互联网黑话。吐槽要精准,避重就轻。\n- **只有维度名称(name)需要抽象,点评(comment)和总结(summary)可以非常具体和生动。**\n\n## 返回格式要求:\n必须以纯 JSON 格式返回,不得包含任何 Markdown 格式。\n\n```json\n{{\n \"title\": \"今日群聊主题\",\n \"subtitle\": \"副标题\",\n \"dimensions\": [\n {{\n \"name\": \"抽象维度名\",\n \"percentage\": 比例,\n \"comment\": \"维度的毒舌点评\"\n }}\n ],\n \"summary\": \"一句总结性的金句\"\n}}\n```\n\n群聊记录:\n{messages_text}" + "default": "你是一个毒舌且幽默的群聊质量分析师。\n请分析以下群聊记录,输出一份\"聊天质量锐评\"。\n\n## 任务目标:\n1. **维度划分**:将聊天内容划分为 3-6 个【高层级、抽象、泛化】的维度(例如:就业焦虑、生涯规划、技术方案研究、情感树洞、无意义水群等)。\n2. **严禁在维度名称(name)中出现任何具体的群聊人物名、项目名、具体的报错内容或细碎的事件点。标题必须保持高度抽象且字数简练(2-6个字)。**\n3. 为每个维度计算一个大致的百分比占位(总和小于等于 100%)。\n4. **点评内容**:为每个维度写一句犀利、幽默、毒舌或温情的点评。具体的吐槽内容、具体的细节事件描述请放在这里。\n5. **全群表现**:给出一句总结性的评价,作为总结标题对应的“金句”。\n6. **主题设定**:设定一个本次报告的主题标题和副标题。\n\n## 点评风格指南:\n- 语言要接地气,多用互联网黑话。吐槽要精准,避重就轻。\n- **只有维度名称(name)需要抽象,点评(comment)和总结(summary)可以非常具体和生动。**\n\n## 返回格式要求:\n必须以纯 JSON 格式返回,不得包含任何 Markdown 格式。\n\n```json\n{{\n \"title\": \"今日群聊主题\",\n \"subtitle\": \"副标题\",\n \"dimensions\": [\n {{\n \"name\": \"抽象维度名\",\n \"percentage\": 比例,\n \"comment\": \"维度的毒舌点评\"\n }}\n ],\n \"summary\": \"一句总结性的金句\"\n}}\n```\n\n群聊记录:\n${messages_text}" }, "quality_summary_prompt": { "description": "增量汇总聊天质量分析提示词", "type": "text", "editor_mode": true, "editor_language": "markdown", - "default": "你是一个毒舌且幽默的群聊质量分析师。\n你现在有一份今天全天分散时间段的多个“增量批次点评笔记”。\n你的任务是将这些分散的笔记汇总成一份最终的“全天聊天质量终极锐评”。\n\n## 任务目标:\n1. **全局抽象维度**:根据各批次的维度表现,平衡权重,提取出 3-6 个覆盖全天的【核心、上层抽象】课题维度(如:职场/行业风向、技术架构演进、社畜心理博弈等)。\n2. **严禁在维度名称(name)中出现具体的批次细节。标题必须代表全天的某种趋势。**\n3. **百分比融合**:根据全天笔记的频率和强度,给出一个代表全天整体分布的比例(总和不超过100%)。\n4. **终极点评**:为每个汇总维度写出一句升华后的全天总结性点评。可以融合具体批次中的有趣槽点。\n5. **终极总结**:拟定全天的大型主题标题、副标题,并给出一句霸气的全天表现总结。\n\n## 风格要求:\n- 只有维度名称(name)需要高度概括抽象。\n- 点评(comment)和总结(summary)请尽量生动、毒舌、具体,要把一整天的梗串联起来。\n\n## 返回格式要求:\n必须以纯 JSON 格式返回,不得包含任何 Markdown 格式。\n\n```json\n{{\n \"title\": \"今日群聊主题\",\n \"subtitle\": \"副标题\",\n \"dimensions\": [\n {{\n \"name\": \"抽象大类标题\",\n \"percentage\": 比例,\n \"comment\": \"维度的全天锐评\"\n }}\n ],\n \"summary\": \"全天总结金句\"\n}}\n```\n\n增量汇总数据:\n{reviews_text}" + "default": "你是一个毒舌且幽默的群聊质量分析师。\n你现在有一份今天全天分散时间段的多个“增量批次点评笔记”。\n你的任务是将这些分散的笔记汇总成一份最终的“全天聊天质量终极锐评”。\n\n## 任务目标:\n1. **全局抽象维度**:根据各批次的维度表现,平衡权重,提取出 3-6 个覆盖全天的【核心、上层抽象】课题维度(如:职场/行业风向、技术架构演进、社畜心理博弈等)。\n2. **严禁在维度名称(name)中出现具体的批次细节。标题必须代表全天的某种趋势。**\n3. **百分比融合**:根据全天笔记的频率和强度,给出一个代表全天整体分布的比例(总和不超过100%)。\n4. **终极点评**:为每个汇总维度写出一句升华后的全天总结性点评。可以融合具体批次中的有趣槽点。\n5. **终极总结**:拟定全天的大型主题标题、副标题,并给出一句霸气的全天表现总结。\n\n## 风格要求:\n- 只有维度名称(name)需要高度概括抽象。\n- 点评(comment)和总结(summary)请尽量生动、毒舌、具体,要把一整天的梗串联起来。\n\n## 返回格式要求:\n必须以纯 JSON 格式返回,不得包含任何 Markdown 格式。\n\n```json\n{{\n \"title\": \"今日群聊主题\",\n \"subtitle\": \"副标题\",\n \"dimensions\": [\n {{\n \"name\": \"抽象大类标题\",\n \"percentage\": 比例,\n \"comment\": \"维度的全天锐评\"\n }}\n ],\n \"summary\": \"全天总结金句\"\n}}\n```\n\n增量汇总数据:\n${reviews_text}" } } } diff --git a/main.py b/main.py index 7f8f55e..d6ad796 100644 --- a/main.py +++ b/main.py @@ -220,6 +220,12 @@ class GroupDailyAnalysis(Star): logger.info(f"正在执行插件初始化 (来源: {source})...") + # 0. 自动升级旧版 prompt 模板(str.format -> string.Template)并回写配置 + try: + self.config_manager.upgrade_prompt_templates() + except Exception as e: + logger.warning(f"自动升级 prompt 模板失败: {e}") + # 1. 尝试发现 bot 实例 await self.bot_manager.initialize_from_config() diff --git a/src/infrastructure/analysis/analyzers/chat_quality_analyzer.py b/src/infrastructure/analysis/analyzers/chat_quality_analyzer.py index be788dc..7c45ab7 100644 --- a/src/infrastructure/analysis/analyzers/chat_quality_analyzer.py +++ b/src/infrastructure/analysis/analyzers/chat_quality_analyzer.py @@ -7,6 +7,7 @@ from datetime import datetime from ....domain.models.data_models import QualityDimension, QualityReview, TokenUsage from ....utils.logger import logger +from ...utils.template_utils import render_template from ..utils import InfoUtils from ..utils.json_utils import extract_quality_with_regex, parse_json_object_response from ..utils.llm_utils import ( @@ -85,8 +86,10 @@ class ChatQualityAnalyzer(BaseAnalyzer[QualityReview]): prompt_template = self.config_manager.get_quality_analysis_prompt() - if not prompt_template: - prompt_template = """你是一个毒舌且幽默的群聊质量分析师。 + if prompt_template: + return render_template(prompt_template, messages_text=messages_text) + + prompt_template = """你是一个毒舌且幽默的群聊质量分析师。 请分析以下群聊记录,输出一份"聊天质量锐评"。 ## 任务目标: @@ -120,10 +123,10 @@ class ChatQualityAnalyzer(BaseAnalyzer[QualityReview]): ``` 群聊记录: -{messages_text} +${messages_text} """ - return prompt_template.format(messages_text=messages_text) + return render_template(prompt_template, messages_text=messages_text) def extract_with_regex(self, result_text: str, max_count: int) -> list[dict]: """ @@ -291,10 +294,10 @@ class ChatQualityAnalyzer(BaseAnalyzer[QualityReview]): ) reviews_text += f"\n批次 {i + 1} [{title}]:\n- 维度表现: {dims}\n- 核心摘要: {summary}\n" - prompt_template = self.config_manager.get_quality_summary_prompt() - - if not prompt_template: - prompt_template = """你是一个毒舌且幽默的群聊质量分析师。 + # 获取配置中的汇总提示词模板,如果没有则使用默认模板 + prompt_template = ( + self.config_manager.get_quality_summary_prompt() + or """你是一个毒舌且幽默的群聊质量分析师。 你现在有一份今天全天分散时间段的多个“增量批次点评笔记”。 你的任务是将这些分散的笔记汇总成一份最终的“全天聊天质量终极锐评”。 @@ -327,7 +330,8 @@ class ChatQualityAnalyzer(BaseAnalyzer[QualityReview]): }} ``` """ - prompt = prompt_template.format(reviews_text=reviews_text) + ) + prompt = render_template(prompt_template, reviews_text=reviews_text) # 调用 LLM 进行汇总 system_prompt = await self._build_system_prompt(umo) diff --git a/src/infrastructure/analysis/analyzers/golden_quote_analyzer.py b/src/infrastructure/analysis/analyzers/golden_quote_analyzer.py index 236c19d..7f3115a 100644 --- a/src/infrastructure/analysis/analyzers/golden_quote_analyzer.py +++ b/src/infrastructure/analysis/analyzers/golden_quote_analyzer.py @@ -7,6 +7,7 @@ from datetime import datetime from ....domain.models.data_models import GoldenQuote, TokenUsage from ....utils.logger import logger +from ...utils.template_utils import render_template from ..utils import InfoUtils from ..utils.json_utils import extract_golden_quotes_with_regex from ..utils.response_validation import validate_golden_quote_items @@ -64,15 +65,14 @@ class GoldenQuoteAnalyzer(BaseAnalyzer[GoldenQuote]): prompt_template = self.config_manager.get_golden_quote_analysis_prompt() if prompt_template: - # 使用配置中的 prompt 并替换变量 try: - prompt = prompt_template.format( - max_golden_quotes=max_golden_quotes, messages_text=messages_text + prompt = render_template( + prompt_template, + max_golden_quotes=max_golden_quotes, + messages_text=messages_text, ) logger.info("使用配置中的金句分析提示词") return prompt - except KeyError as e: - logger.warning(f"金句分析提示词变量格式错误: {e}") except Exception as e: logger.warning(f"应用金句分析提示词失败: {e}") diff --git a/src/infrastructure/analysis/analyzers/topic_analyzer.py b/src/infrastructure/analysis/analyzers/topic_analyzer.py index 8e272b0..b55aef8 100644 --- a/src/infrastructure/analysis/analyzers/topic_analyzer.py +++ b/src/infrastructure/analysis/analyzers/topic_analyzer.py @@ -8,6 +8,7 @@ from datetime import datetime from ....domain.models.data_models import SummaryTopic, TokenUsage from ....utils.logger import logger +from ...utils.template_utils import render_template from ..utils import InfoUtils from ..utils.json_utils import extract_topics_with_regex from ..utils.response_validation import validate_topic_items @@ -162,15 +163,14 @@ class TopicAnalyzer(BaseAnalyzer[SummaryTopic]): prompt_template = self.config_manager.get_topic_analysis_prompt() if prompt_template: - # 使用配置中的 prompt 并替换变量 try: - prompt = prompt_template.format( - max_topics=max_topics, messages_text=messages_text + prompt = render_template( + prompt_template, + max_topics=max_topics, + messages_text=messages_text, ) logger.info("使用配置中的话题分析提示词") return prompt - except KeyError as e: - logger.warning(f"话题分析提示词变量格式错误: {e}") except Exception as e: logger.warning(f"应用话题分析提示词失败: {e}") diff --git a/src/infrastructure/analysis/analyzers/user_title_analyzer.py b/src/infrastructure/analysis/analyzers/user_title_analyzer.py index 7c2b84d..4cbed7e 100644 --- a/src/infrastructure/analysis/analyzers/user_title_analyzer.py +++ b/src/infrastructure/analysis/analyzers/user_title_analyzer.py @@ -5,6 +5,7 @@ from ....domain.models.data_models import TokenUsage, UserTitle from ....utils.logger import logger +from ...utils.template_utils import render_template from ..utils.json_utils import extract_user_titles_with_regex from ..utils.response_validation import validate_user_title_items from ..utils.structured_output_schema import JSONObject, build_user_titles_schema @@ -65,13 +66,10 @@ class UserTitleAnalyzer(BaseAnalyzer[UserTitle]): prompt_template = self.config_manager.get_user_title_analysis_prompt() if prompt_template: - # 使用配置中的 prompt 并替换变量 try: - prompt = prompt_template.format(users_text=users_text) + prompt = render_template(prompt_template, users_text=users_text) logger.info("使用配置中的用户称号分析提示词") return prompt - except KeyError as e: - logger.warning(f"用户称号分析提示词变量格式错误: {e}") except Exception as e: logger.warning(f"应用用户称号分析提示词失败: {e}") diff --git a/src/infrastructure/config/config_manager.py b/src/infrastructure/config/config_manager.py index 8009b6e..28a8caa 100644 --- a/src/infrastructure/config/config_manager.py +++ b/src/infrastructure/config/config_manager.py @@ -9,6 +9,7 @@ from astrbot.api import AstrBotConfig from astrbot.api.star import StarTools from ...utils.logger import logger +from ..utils.template_utils import upgrade_str_format_template class ConfigManager: @@ -332,6 +333,80 @@ class ConfigManager: prompts["quality_analysis_prompts"]["quality_v2_prompt"] = prompt self.config.save_config() + def _upgrade_config_item(self, group: str, key: str, setter_func): + """升级指定配置项的值(从 str.format -> string.Template),并回写。""" + # 如果是 prompts,则先取 prompts 分组,再取子分组 (group) + if group in ( + "quality_analysis_prompts", + "topic_analysis_prompts", + "user_title_analysis_prompts", + "golden_quote_analysis_prompts", + ): + target_group = self._get_group("prompts").get(group, {}) + else: + target_group = self._get_group(group) + + val = target_group.get(key, "") + if not val or not isinstance(val, str): + return False + + upgraded_val, upgraded = upgrade_str_format_template(val) + if upgraded and upgraded_val != val: + setter_func(upgraded_val) + logger.info( + f"配置项 {group}.{key} 发现旧版语法并已自动升级为 string.Template 格式。" + ) + return True + return False + + def upgrade_prompt_templates(self): + """启动时调用,扫描并升级所有可配置的模板(含 prompt 和文件名)。""" + modified = False + # 1. 提示词模板升级 + modified |= self._upgrade_config_item( + "quality_analysis_prompts", + "quality_v2_prompt", + self.set_quality_analysis_prompt, + ) + modified |= self._upgrade_config_item( + "quality_analysis_prompts", + "quality_summary_prompt", + self.set_quality_summary_prompt, + ) + modified |= self._upgrade_config_item( + "topic_analysis_prompts", + "topic_prompt", + self.set_topic_analysis_prompt, + ) + modified |= self._upgrade_config_item( + "user_title_analysis_prompts", + "user_title_prompt", + self.set_user_title_analysis_prompt, + ) + modified |= self._upgrade_config_item( + "golden_quote_analysis_prompts", + "golden_quote_v2_prompt", + self.set_golden_quote_analysis_prompt, + ) + + # 2. 文件名格式升级 + modified |= self._upgrade_config_item( + "pdf", + "pdf_filename_format", + self.set_pdf_filename_format, + ) + modified |= self._upgrade_config_item( + "html", + "html_filename_format", + self.set_html_filename_format, + ) + + if modified: + logger.info( + "已完成所有配置模板从 str.format 到 string.Template 的安全迁移。(已自动回写配置)" + ) + return modified + def get_quality_summary_prompt(self, style: str = "quality_summary_prompt") -> str: """获取聊天质量汇总分析提示词模板""" prompts_config = self._get_group("prompts").get("quality_analysis_prompts", {}) @@ -542,6 +617,11 @@ class ConfigManager: self._ensure_group("pdf")["pdf_filename_format"] = format_str self.config.save_config() + def set_html_filename_format(self, format_str: str): + """设置HTML文件名格式""" + self._ensure_group("html")["html_filename_format"] = format_str + self.config.save_config() + def get_report_template(self) -> str: """获取报告模板名称""" return self._get_group("basic").get("report_template", "scrapbook") diff --git a/src/infrastructure/reporting/generators.py b/src/infrastructure/reporting/generators.py index 61a416f..a87be1f 100644 --- a/src/infrastructure/reporting/generators.py +++ b/src/infrastructure/reporting/generators.py @@ -21,6 +21,7 @@ from markupsafe import Markup from ...domain.repositories.report_repository import IReportGenerator from ...utils.logger import logger +from ..utils.template_utils import render_template from ..visualization.activity_charts import ActivityVisualizer from .templates import HTMLTemplates @@ -94,9 +95,9 @@ class ReportGenerator(IReportGenerator): } try: - formatted = filename_format.format(**safe_context) + formatted = render_template(filename_format, strict=True, **safe_context) except Exception as e: - raise ValueError(f"文件名格式化失败: {e}") from e + raise ValueError(f"文件名模板渲染失败: {e}") from e if os.path.isabs(formatted): raise ValueError("文件名格式不得为绝对路径") diff --git a/src/infrastructure/utils/template_utils.py b/src/infrastructure/utils/template_utils.py new file mode 100644 index 0000000..6147e57 --- /dev/null +++ b/src/infrastructure/utils/template_utils.py @@ -0,0 +1,106 @@ +"""安全模板渲染工具(String Template 兼容)""" + +import re +from string import Template + +from ...utils.logger import logger + +# 统一默认 placeholder +PLACEHOLDERS = { + # 分析类核心变量 + "messages_text": "${messages_text}", + "reviews_text": "${reviews_text}", + "max_topics": "${max_topics}", + "users_text": "${users_text}", + "max_golden_quotes": "${max_golden_quotes}", + # 文件名渲染类变量 + "group_id": "${group_id}", + "date": "${date}", + "ulid": "${ulid}", +} + + +def is_str_format_template(template: str) -> bool: + """判断模板是否为 str.format 风格。 + + 只认为满足: + 1) 不包含 String Template `${var}` 或 `$var` + 2) 包含 str.format `{var}`(非 `{{...}}`) + """ + if not template: + return False + + # 1. 預先建立排除模式 (匹配 ${var} 或 $var) + # 使用 set 去重並組合 + dollar_patterns = [re.escape(v) for v in PLACEHOLDERS.values()] + [ + rf"\${re.escape(k)}" for k in PLACEHOLDERS.keys() + ] + exclude_regex = "|".join(dollar_patterns) + + # 如果包含任何 $ 相關的佔位符,則不視為 str.format 模板 + if re.search(exclude_regex, template): + return False + + # 2. 檢查是否包含標準的 {key},確保匹配單個花括號包裹的 Key + for key in PLACEHOLDERS.keys(): + # (? tuple[str, bool]: + """如果模板是 str.format 风格,则自动升级为 string.Template。 + + 返回 (升级后的模板, 是否升级) + """ + if template is None: + return "", False + + if not is_str_format_template(template): + return template, False + + # 先转义原文中的 $,避免被 Template 误解释为占位符 + safe_template = template.replace("$", "$$") + + # 将 {var} 转为 ${var} + safe_template = re.sub( + r"(? str: + """渲染模板(String Template)。 + + Args: + template: 模板字符串 + strict: 是否使用严格模式(变量缺失则抛出异常) + **kwargs: 渲染变量 + + 由于插件启动时已完成 str.format 兼容升级,运行时直接按 string.Template 渲染。 + """ + if template is None: + return "" + + try: + t = Template(template) + return t.substitute(**kwargs) if strict else t.safe_substitute(**kwargs) + except Exception as e: + if strict: + raise + logger.warning( + f"[template_utils] 模板渲染失败,返回原始文本,错误: {e}", + exc_info=True, + ) + return template