diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c48f0fe --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,7 @@ +# AGENTS.md + +## 编码要求 +1. 每次编码需要书写详细的中文注释 +2. 每次写完了之后需要执行commit +3. 每次提交commit需要用中文写清楚这次提交的变更项 + diff --git a/config.yaml b/config.yaml index de1dfe4..cf4ac8d 100644 --- a/config.yaml +++ b/config.yaml @@ -66,6 +66,18 @@ llm: workflow_output_key: "text" response_mode: "streaming" request_timeout: 180 + dify_workflow_douyu_daily_report: + provider: "dify" + mode: "workflow" + # 斗鱼日报专用工作流:请替换为你在 Dify 上创建的“斗鱼日报”应用 Key。 + api_key: "" + api_base_url: "http://192.168.2.240/v1" + endpoint: "workflows/run" + # 工作流最终输出字段建议固定为 text,便于统一客户端直接读取结果文本。 + workflow_output_key: "text" + response_mode: "blocking" + # 斗鱼日报 payload 较大,适当提高超时时间,避免高峰时段超时回退。 + request_timeout: 240 dify_chat_global_news: provider: "dify" mode: "chat" diff --git a/docs/dify_douyu_daily_report_workflow.md b/docs/dify_douyu_daily_report_workflow.md new file mode 100644 index 0000000..90e1856 --- /dev/null +++ b/docs/dify_douyu_daily_report_workflow.md @@ -0,0 +1,109 @@ +# Dify 工作流设计:斗鱼日报迁移 + +## 1. 目标 +- 让 `plugins/douyu` 的日报生成从 OpenAI-compatible 切到 Dify Workflow。 +- 同一个工作流同时处理两类任务:`daily_report`(整段日报)和 `danmu_summary`(图片上半部分弹幕总结)。 +- 输出字段统一为 `text`,与项目内 `workflow_output_key` 对齐。 + +## 2. 项目侧输入约定 +斗鱼插件会向 Dify Workflow 发送以下输入(`inputs`): +- `task_type`: 任务类型,值为 `daily_report` 或 `danmu_summary` +- `query`: 用户提示词(等价于 `user_prompt`) +- `system_prompt`: 系统提示词 +- `user_prompt`: 用户提示词 +- `report_payload`: 结构化 payload(对象) +- `report_payload_json`: payload 的 JSON 字符串 +- `room_id`: 房间号 +- `anchor_day`: 报告日期,例如 `2026-04-20` +- `nickname`: 主播昵称 +- `max_length`: 最大输出长度(默认 1800) + +## 3. Dify 工作流节点编排 +按以下顺序创建节点。 + +1. `Start` +- 输入变量按“项目侧输入约定”逐个创建。 + +2. `Code` 节点(命名建议:`build_prompt`) +- 作用:根据 `task_type` 统一拼装最终 prompt,避免在 LLM 节点里写复杂模板。 +- 输入:`task_type`、`system_prompt`、`user_prompt`、`report_payload_json`、`max_length` +- 输出: + - `final_prompt` + - `safe_max_length` +- Python 示例: + +```python +import json + +def main(task_type: str, system_prompt: str, user_prompt: str, report_payload_json: str, max_length: int = 1800): + task = (task_type or "").strip().lower() + length = int(max_length or 1800) + payload_text = report_payload_json or "{}" + + # 统一模板:保留项目侧 prompt,同时附加长度约束。 + if task == "danmu_summary": + suffix = "\\n\\n请严格输出弹幕总结,不要输出运营策略,不要使用代码块。" + else: + suffix = "\\n\\n请严格输出完整日报正文,不要输出代码块。" + + final_prompt = ( + f"{system_prompt or ''}\\n\\n" + f"{user_prompt or ''}\\n\\n" + f"【输出长度约束】最多 {length} 字。\\n" + f"【结构化材料】{payload_text}" + f"{suffix}" + ).strip() + + return { + "final_prompt": final_prompt, + "safe_max_length": length, + } +``` + +3. `LLM` 节点(命名建议:`report_llm`) +- 模型建议:你当前 Dify 可用的大模型里,优先选长上下文、中文稳定的模型。 +- 输入:`build_prompt.final_prompt` +- 参数建议: + - Temperature:`0.2 - 0.4` + - Max tokens:`1200 - 2000`(按模型上限调整) + +4. `Code` 节点(命名建议:`normalize_output`) +- 作用:做统一清洗与截断,确保最终给项目的是纯文本。 +- 输入:`report_llm.text`、`build_prompt.safe_max_length` +- 输出:`text` +- Python 示例: + +```python +def main(text: str, safe_max_length: int = 1800): + value = (text or "").strip() + max_len = int(safe_max_length or 1800) + + # 清理连续空行,避免图片模板排版过疏。 + while "\\n\\n\\n" in value: + value = value.replace("\\n\\n\\n", "\\n\\n") + + if len(value) > max_len: + value = value[: max_len - 20].rstrip() + "\\n...(已截断)" + + return {"text": value} +``` + +5. `End` +- 输出变量固定为 `text`。 + +## 4. 关键配置要求 +- Dify 应用类型选择 `Workflow`。 +- API 路径使用 `/v1/workflows/run`。 +- 项目侧 `workflow_output_key` 必须为 `text`。 +- 响应模式建议 `blocking`,避免日报场景流式拼接不完整。 + +## 5. 仓库内已完成的切换项 +- `plugins/douyu/main.py`:新增 Dify 专用输入封装与 provider 分流调用。 +- `plugins/douyu/config.toml`:`report_api.backend` 已切换为 `dify_workflow_douyu_daily_report`。 +- `config.yaml`:新增 `llm.backends.dify_workflow_douyu_daily_report` 模板配置。 + +## 6. 上线前检查 +1. 在 `config.yaml` 填入 Dify 工作流真实 `api_key`。 +2. 执行群命令:`#强制斗鱼弹幕日报 2026-04-19`。 +3. 观察日志是否出现 `斗鱼每日报告 LLM 生成失败`。 +4. 若失败,先在 Dify 控制台手动 Run,确认 `End.text` 有值。 diff --git a/plugins/douyu/config.toml b/plugins/douyu/config.toml index 059d3de..3a31dd4 100644 --- a/plugins/douyu/config.toml +++ b/plugins/douyu/config.toml @@ -27,6 +27,5 @@ daily_report_send_image = true audience_stats_sample_interval_seconds = 0 [Douyu.report_api] -backend = "openai_compatible_ai_auto_response" -temperature = 0.3 -max_tokens = 900 +# 切换到 Dify 斗鱼日报专用工作流;对应配置位于根目录 config.yaml 的 llm.backends。 +backend = "dify_workflow_douyu_daily_report" diff --git a/plugins/douyu/main.py b/plugins/douyu/main.py index 9dbef04..e847ab8 100644 --- a/plugins/douyu/main.py +++ b/plugins/douyu/main.py @@ -1828,14 +1828,94 @@ class DouyuPlugin(MessagePluginInterface): def _build_operator_summary_lines(self, payload: Dict[str, Any]) -> List[str]: return [line.strip()[2:].strip() for line in self._build_operator_summary_text(payload).splitlines() if line.strip().startswith("- ")] + def _build_dify_daily_report_inputs( + self, + *, + task_type: str, + system_prompt: str, + user_prompt: str, + payload: Dict[str, Any], + ) -> Dict[str, Any]: + """ + 组装斗鱼日报在 Dify Workflow 下的输入参数。 + 设计目标: + 1. 让工作流既能拿到“最终自然语言提示词”,也能拿到“结构化原始载荷”; + 2. 让一个工作流通过 task_type 同时处理「日报正文」和「弹幕摘要」两类任务; + 3. 保留关键元信息,便于在工作流内做分支、日志与降级兜底。 + """ + meta = payload.get("report_meta", {}) or {} + room_id = str(meta.get("room_id") or "").strip() + anchor_day = str(meta.get("anchor_day") or "").strip() + nickname = str(meta.get("nickname") or meta.get("room_name") or "").strip() + payload_json = json.dumps(payload, ensure_ascii=False) + return { + # 任务路由字段:在 Dify 条件分支里用于区分日报正文/弹幕摘要。 + "task_type": task_type, + # 兼容 Workflow 中直接读取 query 的场景。 + "query": user_prompt, + # 保留原有两段提示词,便于工作流内部二次拼装或调试。 + "system_prompt": system_prompt, + "user_prompt": user_prompt, + # 结构化报告载荷:既提供对象,也提供 JSON 文本,适配不同节点处理能力。 + "report_payload": payload, + "report_payload_json": payload_json, + # 关键元信息:用于日志、标题拼接、数据看板或异常追踪。 + "room_id": room_id, + "anchor_day": anchor_day, + "nickname": nickname, + # 控制输出长度:避免 Dify 侧生成超长内容后再被本地硬截断。 + "max_length": int(self._daily_report_max_length or 1800), + } + + def _call_daily_report_llm( + self, + *, + task_type: str, + system_prompt: str, + user_prompt: str, + payload: Dict[str, Any], + tag: str, + ) -> str: + """ + 统一封装斗鱼日报 LLM 调用。 + - Dify provider:走 run(inputs) 进入 Workflow,确保输入结构稳定可编排; + - 其他 provider:保持原 chat(system,user) 行为,兼容现有 OpenAI-compatible 配置。 + """ + if not self._daily_report_llm_client: + return "" + meta = payload.get("report_meta", {}) or {} + room_id = str(meta.get("room_id") or "").strip() + user_id = f"douyu_daily_report_{room_id or 'unknown'}" + if self._daily_report_llm_client.provider == "dify": + inputs = self._build_dify_daily_report_inputs( + task_type=task_type, + system_prompt=system_prompt, + user_prompt=user_prompt, + payload=payload, + ) + result = self._daily_report_llm_client.run( + prompt=user_prompt, + user=user_id, + inputs=inputs, + tag=tag, + ) + return str((result or {}).get("text", "") or "").strip() + return self._daily_report_llm_client.chat( + system_prompt, + user_prompt, + user_id=user_id, + ).strip() + async def _generate_danmu_summary_text(self, payload: Dict[str, Any]) -> str: if self._daily_report_use_llm and self._daily_report_llm_client: system_prompt, user_prompt = self._build_danmu_summary_prompt(payload) result = await asyncio.to_thread( - self._daily_report_llm_client.chat, - system_prompt, - user_prompt, - f"douyu_danmu_summary_{(payload.get('report_meta', {}) or {}).get('room_id', '')}", + self._call_daily_report_llm, + task_type="danmu_summary", + system_prompt=system_prompt, + user_prompt=user_prompt, + payload=payload, + tag=f"douyu_danmu_summary_{(payload.get('report_meta', {}) or {}).get('room_id', '')}", ) if result: return result.strip() @@ -1919,10 +1999,12 @@ class DouyuPlugin(MessagePluginInterface): if self._daily_report_use_llm and self._daily_report_llm_client: system_prompt, user_prompt = self._build_daily_report_prompt(payload) result = await asyncio.to_thread( - self._daily_report_llm_client.chat, - system_prompt, - user_prompt, - f"douyu_daily_report_{(payload.get('report_meta', {}) or {}).get('room_id', '')}", + self._call_daily_report_llm, + task_type="daily_report", + system_prompt=system_prompt, + user_prompt=user_prompt, + payload=payload, + tag=f"douyu_daily_report_{(payload.get('report_meta', {}) or {}).get('room_id', '')}", ) if result: text = result.strip()