Files
qihuaiflow/backend/python/app/pipeline.py
T
workbuddy 405e836ee2 fix: restore backend/python/app/pipeline.py (lost in mass-deletion 5d7f32d)
Single genuinely-missing module after the deletion. Rest of the 44 'missing'
paths were false positives (app/ prefix artifacts in the deletion commit).
Recovery now matches the 1346fbb baseline used by the restored admin
modules and models.py.
2026-07-11 16:32:45 +08:00

348 lines
13 KiB
Python

"""
Pipeline 消息处理管道 - 参照AstrBot Stage链式设计,同步版兼容FastAPI
消息处理阶段可插拔,链式调用
"""
from abc import ABC, abstractmethod
from typing import Any
import logging
import time
logger = logging.getLogger(__name__)
class StageResult:
"""阶段处理结果"""
def __init__(self, success: bool = True, data: dict = None, error: str = None, stop: bool = False):
self.success = success
self.data = data or {}
self.error = error
self.stop = stop # 是否停止后续阶段
class PipelineStage(ABC):
"""管道阶段基类"""
def __init__(self, name: str):
self.name = name
@abstractmethod
def process(self, context: dict) -> StageResult:
"""处理消息上下文,返回StageResult"""
...
# === 具体阶段实现 ===
class WakingCheckStage(PipelineStage):
"""唤醒检查:检查是否需要AI响应"""
def __init__(self):
super().__init__("waking_check")
def process(self, context: dict) -> StageResult:
message = context.get("message", "")
platform = context.get("platform", "")
config = context.get("channel_config", {})
bot_prefix = config.get("bot_prefix", "@bot")
is_private = context.get("is_private", True)
is_at_bot = bot_prefix.lower() in message.lower() if bot_prefix else False
if is_private or is_at_bot or message.strip().startswith("/"):
return StageResult(success=True, data={"should_respond": True})
# 群聊需要@或唤醒词
if not is_private and not is_at_bot:
return StageResult(success=True, data={"should_respond": False}, stop=True)
return StageResult(success=True, data={"should_respond": True})
class RateLimitCheckStage(PipelineStage):
"""速率限制检查"""
def __init__(self, max_per_minute: int = 30):
super().__init__("rate_limit_check")
self.max_per_minute = max_per_minute
self._counter = {}
def process(self, context: dict) -> StageResult:
user_id = context.get("sender_id", "unknown")
now = time.time()
key = f"{user_id}:{int(now // 60)}"
count = self._counter.get(key, 0)
if count >= self.max_per_minute:
return StageResult(success=False, error="请求过于频繁,请稍后再试", stop=True)
self._counter[key] = count + 1
# 清理旧计数器
keys_to_del = [k for k in self._counter if int(k.split(":")[-1]) < int(now // 60) - 1]
for k in keys_to_del:
del self._counter[k]
return StageResult(success=True)
class ContentSafetyCheckStage(PipelineStage):
"""内容安全检查"""
def __init__(self):
super().__init__("content_safety_check")
self._blocked_words = [] # 可从配置加载
def process(self, context: dict) -> StageResult:
message = context.get("message", "")
for word in self._blocked_words:
if word in message:
return StageResult(success=False, error="消息包含违规内容", stop=True)
return StageResult(success=True)
class UnsupportedStage(PipelineStage):
"""业务逻辑处理阶段基类 - 子类实现具体处理"""
def __init__(self):
super().__init__("process")
def process(self, context: dict) -> StageResult:
# 此阶段由channels.py中的实际AI处理逻辑填充
return StageResult(success=True, data={"processed": True})
class ResponseBuildStage(PipelineStage):
"""响应构建阶段"""
def __init__(self):
super().__init__("respond")
def process(self, context: dict) -> StageResult:
ai_reply = context.get("ai_reply", "")
if not ai_reply:
return StageResult(success=True, data={"reply": ""})
return StageResult(success=True, data={"reply": ai_reply})
# === Pipeline 管道调度器 ===
class PipelineScheduler:
"""管道调度器:按顺序执行各个Stage"""
def __init__(self, stages: list[PipelineStage] = None):
self.stages = stages if stages is not None else self._default_stages()
@staticmethod
def _default_stages() -> list[PipelineStage]:
return [
WakingCheckStage(),
RateLimitCheckStage(max_per_minute=30),
ContentSafetyCheckStage(),
UnsupportedStage(),
ResponseBuildStage(),
]
def add_stage(self, stage: PipelineStage, position: int = None):
"""插入新阶段"""
if position is not None:
self.stages.insert(position, stage)
else:
self.stages.append(stage)
def remove_stage(self, stage_name: str):
"""移除阶段"""
self.stages = [s for s in self.stages if s.name != stage_name]
def execute(self, context: dict) -> dict:
"""执行完整的管道处理"""
context["_pipeline_log"] = []
for stage in self.stages:
try:
result = stage.process(context)
context["_pipeline_log"].append({
"stage": stage.name,
"success": result.success,
"error": result.error,
"stop": result.stop,
})
if not result.success and result.stop:
return {
"success": False,
"error": result.error,
"stage": stage.name,
"pipeline_log": context["_pipeline_log"],
}
if result.stop and result.success:
# 正常停止(如waking_check判断不需要响应)
return {
"success": True,
"data": result.data,
"stage": stage.name,
"pipeline_log": context["_pipeline_log"],
}
context.update(result.data)
except Exception as e:
logger.error(f"[Pipeline] Stage {stage.name} 异常: {e}")
return {
"success": False,
"error": f"管道处理异常({stage.name}): {str(e)}",
"pipeline_log": context["_pipeline_log"],
}
return {
"success": True,
"data": {"reply": context.get("reply", context.get("ai_reply", ""))},
"pipeline_log": context["_pipeline_log"],
}
# ============================================================
# 单元测试
# ============================================================
if __name__ == "__main__":
print("=" * 60)
print("Pipeline 管道模块 单元测试")
print("=" * 60)
# 测试1: Pipeline 创建和执行
print("\n[测试1] Pipeline 创建和执行")
pipeline = PipelineScheduler()
context = {
"message": "你好,帮我查询天气",
"platform": "feishu",
"sender_id": "user_001",
"is_private": True,
"channel_config": {"bot_prefix": "@bot"},
"ai_reply": "你好!今天天气晴好,气温22-30℃。",
}
result = pipeline.execute(context)
assert result["success"] is True, f"Pipeline执行应该成功: {result}"
assert "pipeline_log" in result, "应该有pipeline_log"
# 验证所有5个阶段都执行了
stage_names = [log["stage"] for log in result["pipeline_log"]]
assert len(stage_names) == 5, f"应该执行5个阶段,实际执行了{len(stage_names)}个"
assert stage_names == ["waking_check", "rate_limit_check", "content_safety_check", "process", "respond"], \
f"阶段顺序不对: {stage_names}"
assert result["data"]["reply"] == "你好!今天天气晴好,气温22-30℃。"
print(f" PASS: 所有5个阶段按顺序执行,回复={result['data']['reply'][:30]}...")
# 测试2: 唤醒检查阶段 - 私聊
print("\n[测试2] 唤醒检查阶段 - 私聊消息")
waking = WakingCheckStage()
ctx = {
"message": "你好",
"platform": "telegram",
"is_private": True,
"channel_config": {"bot_prefix": "@bot"},
}
result = waking.process(ctx)
assert result.success is True, "私聊应该唤醒"
assert result.data["should_respond"] is True, "私聊应该响应"
print(f" PASS: 私聊消息正确唤醒, should_respond={result.data['should_respond']}")
# 测试3: 唤醒检查阶段 - 群聊 @bot
print("\n[测试3] 唤醒检查阶段 - 群聊@bot")
ctx = {
"message": "@bot 帮我查一下数据",
"platform": "wecom",
"is_private": False,
"channel_config": {"bot_prefix": "@bot"},
}
result = waking.process(ctx)
assert result.success is True, "群聊@bot应该唤醒"
assert result.data["should_respond"] is True, "群聊@bot应该响应"
print(f" PASS: 群聊@bot正确唤醒, should_respond={result.data['should_respond']}")
# 测试4: 唤醒检查阶段 - 群聊未@bot (应停止)
print("\n[测试4] 唤醒检查阶段 - 群聊未@bot(应停止管道)")
ctx = {
"message": "大家今天天气不错",
"platform": "wecom",
"is_private": False,
"channel_config": {"bot_prefix": "@bot"},
}
result = waking.process(ctx)
assert result.success is True, "处理应该成功"
assert result.data["should_respond"] is False, "群聊未@bot不应该响应"
assert result.stop is True, "应该停止管道"
print(f" PASS: 群聊未@bot正确停止管道, should_respond={result.data['should_respond']}, stop={result.stop}")
# 测试5: 速率限制
print("\n[测试5] 速率限制检查")
rate_limiter = RateLimitCheckStage(max_per_minute=3)
# 前3次应该成功
for i in range(3):
ctx = {"sender_id": "user_test"}
result = rate_limiter.process(ctx)
assert result.success is True, f"第{i+1}次请求应该通过速率限制"
print(" PASS: 前3次请求通过速率限制")
# 第4次应该失败
ctx = {"sender_id": "user_test"}
result = rate_limiter.process(ctx)
assert result.success is False, "第4次请求应该被限流"
assert "频繁" in result.error, f"错误信息应包含'频繁': {result.error}"
assert result.stop is True, "限流应该停止管道"
print(f" PASS: 第4次请求被限流, error={result.error}")
# 测试6: 完整管道链 - 群聊未唤醒场景
print("\n[测试6] 完整管道链 - 群聊未唤醒(应被waking_check停止)")
pipeline = PipelineScheduler()
context = {
"message": "今天天气真好啊",
"platform": "wecom",
"sender_id": "user_002",
"is_private": False,
"channel_config": {"bot_prefix": "@bot"},
}
result = pipeline.execute(context)
assert result["success"] is True, "管道应该正常完成"
assert result["stage"] == "waking_check", f"应在waking_check阶段停止,实际: {result['stage']}"
assert len(result["pipeline_log"]) == 1, f"应该只执行了1个阶段,实际: {len(result['pipeline_log'])}"
print(f" PASS: 群聊未唤醒场景正确在waking_check阶段停止")
# 测试7: 内容安全检查 - 无违规词
print("\n[测试7] 内容安全检查 - 无违规词")
safety = ContentSafetyCheckStage()
ctx = {"message": "正常的聊天内容"}
result = safety.process(ctx)
assert result.success is True, "正常内容应该通过检查"
print(f" PASS: 正常内容通过安全检查")
# 测试8: 异常处理 - Stage抛出异常
print("\n[测试8] Pipeline异常处理")
class BrokenStage(PipelineStage):
def __init__(self):
super().__init__("broken")
def process(self, context):
raise RuntimeError("模拟阶段异常")
pipeline2 = PipelineScheduler(stages=[BrokenStage()])
result = pipeline2.execute({"message": "test"})
assert result["success"] is False, "异常应该导致管道失败"
assert "broken" in result["error"], f"错误信息应包含阶段名: {result['error']}"
print(f" PASS: 阶段异常被正确捕获, error={result['error']}")
# 测试9: Pipeline 添加和移除阶段
print("\n[测试9] Pipeline 阶段增删")
pipeline3 = PipelineScheduler(stages=[])
assert len(pipeline3.stages) == 0, "初始应无阶段"
test_stage = WakingCheckStage()
pipeline3.add_stage(test_stage)
assert len(pipeline3.stages) == 1, "添加后应有1个阶段"
pipeline3.add_stage(RateLimitCheckStage(), position=0)
assert pipeline3.stages[0].name == "rate_limit_check", "插入到位置0应为rate_limit_check"
pipeline3.remove_stage("waking_check")
assert len(pipeline3.stages) == 1, "移除后应有1个阶段"
assert pipeline3.stages[0].name == "rate_limit_check", "剩余应为rate_limit_check"
print(" PASS: 阶段增删操作正确")
print("\n" + "=" * 60)
print("所有测试通过!")
print("=" * 60)