斗鱼日报切换Dify工作流并补齐迁移文档
1. douyu插件新增Dify专用日报调用封装,按provider分流,Dify走run(inputs)并传task_type与结构化payload。\n2. 修正斗鱼日报与弹幕总结生成路径,统一改为新封装,保证Dify workflow可接收完整提示词与元信息。\n3. 新增llm后端dify_workflow_douyu_daily_report模板配置,约定workflow_output_key=text与更高超时。\n4. 斗鱼插件report_api后端切换为dify_workflow_douyu_daily_report。\n5. 新增Dify工作流设计文档,包含输入字段、节点编排、代码节点示例与上线检查步骤。
This commit is contained in:
7
AGENTS.md
Normal file
7
AGENTS.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## 编码要求
|
||||||
|
1. 每次编码需要书写详细的中文注释
|
||||||
|
2. 每次写完了之后需要执行commit
|
||||||
|
3. 每次提交commit需要用中文写清楚这次提交的变更项
|
||||||
|
|
||||||
12
config.yaml
12
config.yaml
@@ -66,6 +66,18 @@ llm:
|
|||||||
workflow_output_key: "text"
|
workflow_output_key: "text"
|
||||||
response_mode: "streaming"
|
response_mode: "streaming"
|
||||||
request_timeout: 180
|
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:
|
dify_chat_global_news:
|
||||||
provider: "dify"
|
provider: "dify"
|
||||||
mode: "chat"
|
mode: "chat"
|
||||||
|
|||||||
109
docs/dify_douyu_daily_report_workflow.md
Normal file
109
docs/dify_douyu_daily_report_workflow.md
Normal file
@@ -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` 有值。
|
||||||
@@ -27,6 +27,5 @@ daily_report_send_image = true
|
|||||||
audience_stats_sample_interval_seconds = 0
|
audience_stats_sample_interval_seconds = 0
|
||||||
|
|
||||||
[Douyu.report_api]
|
[Douyu.report_api]
|
||||||
backend = "openai_compatible_ai_auto_response"
|
# 切换到 Dify 斗鱼日报专用工作流;对应配置位于根目录 config.yaml 的 llm.backends。
|
||||||
temperature = 0.3
|
backend = "dify_workflow_douyu_daily_report"
|
||||||
max_tokens = 900
|
|
||||||
|
|||||||
@@ -1828,14 +1828,94 @@ class DouyuPlugin(MessagePluginInterface):
|
|||||||
def _build_operator_summary_lines(self, payload: Dict[str, Any]) -> List[str]:
|
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("- ")]
|
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:
|
async def _generate_danmu_summary_text(self, payload: Dict[str, Any]) -> str:
|
||||||
if self._daily_report_use_llm and self._daily_report_llm_client:
|
if self._daily_report_use_llm and self._daily_report_llm_client:
|
||||||
system_prompt, user_prompt = self._build_danmu_summary_prompt(payload)
|
system_prompt, user_prompt = self._build_danmu_summary_prompt(payload)
|
||||||
result = await asyncio.to_thread(
|
result = await asyncio.to_thread(
|
||||||
self._daily_report_llm_client.chat,
|
self._call_daily_report_llm,
|
||||||
system_prompt,
|
task_type="danmu_summary",
|
||||||
user_prompt,
|
system_prompt=system_prompt,
|
||||||
f"douyu_danmu_summary_{(payload.get('report_meta', {}) or {}).get('room_id', '')}",
|
user_prompt=user_prompt,
|
||||||
|
payload=payload,
|
||||||
|
tag=f"douyu_danmu_summary_{(payload.get('report_meta', {}) or {}).get('room_id', '')}",
|
||||||
)
|
)
|
||||||
if result:
|
if result:
|
||||||
return result.strip()
|
return result.strip()
|
||||||
@@ -1919,10 +1999,12 @@ class DouyuPlugin(MessagePluginInterface):
|
|||||||
if self._daily_report_use_llm and self._daily_report_llm_client:
|
if self._daily_report_use_llm and self._daily_report_llm_client:
|
||||||
system_prompt, user_prompt = self._build_daily_report_prompt(payload)
|
system_prompt, user_prompt = self._build_daily_report_prompt(payload)
|
||||||
result = await asyncio.to_thread(
|
result = await asyncio.to_thread(
|
||||||
self._daily_report_llm_client.chat,
|
self._call_daily_report_llm,
|
||||||
system_prompt,
|
task_type="daily_report",
|
||||||
user_prompt,
|
system_prompt=system_prompt,
|
||||||
f"douyu_daily_report_{(payload.get('report_meta', {}) or {}).get('room_id', '')}",
|
user_prompt=user_prompt,
|
||||||
|
payload=payload,
|
||||||
|
tag=f"douyu_daily_report_{(payload.get('report_meta', {}) or {}).get('room_id', '')}",
|
||||||
)
|
)
|
||||||
if result:
|
if result:
|
||||||
text = result.strip()
|
text = result.strip()
|
||||||
|
|||||||
Reference in New Issue
Block a user