mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 13:38:43 +00:00
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>
This commit is contained in:
+12
-12
@@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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("文件名格式不得为绝对路径")
|
||||
|
||||
@@ -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():
|
||||
# (?<!\{) 前面不能有 {
|
||||
# \{{key}\} 匹配 {key}
|
||||
# (?!\}) 後面不能有 }
|
||||
# (?<!\$) 前面不能有 $
|
||||
pattern = rf"(?<![\{{\$])\{{{key}\}}(?!\}})"
|
||||
if re.search(pattern, template):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def upgrade_str_format_template(template: str) -> 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"(?<![\{\$])\{([_a-zA-Z][_a-zA-Z0-9]*)\}(?!\})",
|
||||
lambda m: f"${{{m.group(1)}}}",
|
||||
safe_template,
|
||||
)
|
||||
|
||||
# 将双括号回退为单括号(str.format 里表示字面量大括号)
|
||||
safe_template = safe_template.replace("{{", "{").replace("}}", "}")
|
||||
|
||||
return safe_template, True
|
||||
|
||||
|
||||
def render_template(template: str, strict: bool = False, **kwargs) -> 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
|
||||
Reference in New Issue
Block a user