feat: Step 2-4 Task/Mail/Toolchain handlers + PromptSections + BaseTaskHandler
- base_task_handler.py: 基类统一4步流程(crash→verify→mark→notify) - task_handler.py: 5 PromptSections + 三信号验证 + review流程 - mail_handler.py: 3 PromptSections + inform/request区分 + 基类统一流程 - toolchain_handler.py: 3 PromptSections + 模板引擎渲染 + Mail API通知 - 背靠背设计-编码一致性检查通过(4严重已修/6轻微保留)
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
"""toolchain_handler.py — 工具链事件 handler。
|
||||
|
||||
处理 Gitea Webhook 事件(CI 失败、Review 请求、Issue 指派等)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
from src.daemon.base_task_handler import BaseTaskHandler, VerifyResult
|
||||
from src.daemon.prompt_composer import PromptComposer, PromptContext
|
||||
from src.daemon.toolchain_templates import render_template, _TEMPLATE_MAP
|
||||
from src.blackboard.db import get_connection
|
||||
|
||||
logger = logging.getLogger("moziplus-v2.handler.toolchain")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Toolchain PromptSections
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class ToolchainContextSection:
|
||||
"""事件类型 + 事件详情(priority=10)"""
|
||||
|
||||
name: str = "toolchain_context"
|
||||
priority: int = 10
|
||||
|
||||
def render(self, context: PromptContext) -> str:
|
||||
event_type = context.event_type
|
||||
event_data: Dict = context.event_data or {}
|
||||
|
||||
if event_type in _TEMPLATE_MAP:
|
||||
# 使用模板引擎渲染已知事件
|
||||
variables = {k: str(v) for k, v in event_data.items()}
|
||||
return render_template(event_type, variables)
|
||||
|
||||
# fallback:通用事件描述
|
||||
lines = [f"## 工具链事件", f""]
|
||||
lines.append(f"- **事件类型**: {event_type or '未知'}")
|
||||
if event_data:
|
||||
lines.append(f"- **事件详情**:")
|
||||
for key, value in event_data.items():
|
||||
lines.append(f" - {key}: {value}")
|
||||
lines.append(f"")
|
||||
return "\n".join(lines)
|
||||
|
||||
def should_include(self, context: PromptContext) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class ToolchainApiSection:
|
||||
"""API 操作指令(priority=40),success_status=done"""
|
||||
|
||||
name: str = "toolchain_api"
|
||||
priority: int = 40
|
||||
|
||||
API_HOST = "localhost:8083"
|
||||
|
||||
def render(self, context: PromptContext) -> str:
|
||||
lines = [
|
||||
"## API 操作指令",
|
||||
"",
|
||||
f"项目 ID: `{context.project_id}`",
|
||||
f"任务 ID: `{context.task_id}`",
|
||||
"",
|
||||
"### 完成后必须更新任务状态",
|
||||
"完成后务必通过以下命令将任务标记为 **done**:",
|
||||
"```bash",
|
||||
f'curl -s -X POST "http://{self.API_HOST}/api/projects/{context.project_id}/tasks/{context.task_id}/status" \\',
|
||||
' -H "Content-Type: application/json" \\',
|
||||
' -d \'{"status": "done"}\'',
|
||||
"```",
|
||||
"",
|
||||
"### 提交产出",
|
||||
"如有产出(如 review 结果、修复方案),提交到任务 outputs:",
|
||||
"```bash",
|
||||
f'curl -s -X POST "http://{self.API_HOST}/api/projects/{context.project_id}/tasks/{context.task_id}/outputs" \\',
|
||||
' -H "Content-Type: application/json" \\',
|
||||
' -d \'{"content": "<你的产出内容>", "type": "text"}\'',
|
||||
"```",
|
||||
"",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
def should_include(self, context: PromptContext) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class ToolchainConstraintsSection:
|
||||
"""硬约束(priority=50)"""
|
||||
|
||||
name: str = "toolchain_constraints"
|
||||
priority: int = 50
|
||||
|
||||
def render(self, context: PromptContext) -> str:
|
||||
lines = [
|
||||
"## 硬约束",
|
||||
"",
|
||||
"1. **必须标 done**:处理完成后必须通过 API 将任务状态更新为 `done`,否则视为未完成",
|
||||
"2. **产出不能为空**:必须提交有意义的产出(output 或 comment),不能只改状态",
|
||||
"3. **单一职责**:只处理本次事件相关的操作,不要越界执行无关任务",
|
||||
"4. **出错即报告**:如果无法处理(如权限不足、资源不存在),在 comment 中说明原因并标 done",
|
||||
"5. **不要创建新任务**:工具链事件只处理当前事件,不衍生新任务",
|
||||
"",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
def should_include(self, context: PromptContext) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ToolchainHandler
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class ToolchainHandler(BaseTaskHandler):
|
||||
"""工具链事件 handler。"""
|
||||
|
||||
task_type = "toolchain"
|
||||
virtual_project = "_toolchain"
|
||||
|
||||
def target_success_status(self) -> str:
|
||||
return "done"
|
||||
|
||||
def pre_spawn(self, task_id: str, db_path: Path) -> bool:
|
||||
"""auto_working:pending → working"""
|
||||
return self._auto_mark_working(task_id, db_path)
|
||||
|
||||
def get_sections(self) -> List:
|
||||
"""返回 3 个 Toolchain PromptSection 实例"""
|
||||
return [
|
||||
ToolchainContextSection(),
|
||||
ToolchainApiSection(),
|
||||
ToolchainConstraintsSection(),
|
||||
]
|
||||
|
||||
def build_prompt(self, context: PromptContext) -> str:
|
||||
"""通过 PromptComposer 拼装 sections 为最终 prompt"""
|
||||
composer = PromptComposer()
|
||||
composer.add_many(self.get_sections())
|
||||
return composer.compose(context)
|
||||
|
||||
def verify_completion(self, task_id: str, db_path: Path) -> VerifyResult:
|
||||
"""检查行动输出(output 或 comment 有实质内容)"""
|
||||
try:
|
||||
conn = get_connection(db_path)
|
||||
try:
|
||||
# 检查 output
|
||||
output_count = conn.execute(
|
||||
"SELECT COUNT(*) FROM outputs WHERE task_id=?", (task_id,)
|
||||
).fetchone()[0]
|
||||
if output_count > 0:
|
||||
return VerifyResult(True, "has_output", f"output_count={output_count}")
|
||||
|
||||
# 检查 comment(非系统、有实质内容)
|
||||
comment_count = conn.execute(
|
||||
"SELECT COUNT(*) FROM comments WHERE task_id=? "
|
||||
"AND author != 'system' AND LENGTH(content) >= 20",
|
||||
(task_id,)
|
||||
).fetchone()[0]
|
||||
if comment_count > 0:
|
||||
return VerifyResult(True, "has_comment", f"comment_count={comment_count}")
|
||||
|
||||
return VerifyResult(False, "no_action", "output=0, comment=0")
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
logger.error("Toolchain %s: verify error: %s", task_id, e)
|
||||
return VerifyResult(False, "verify_error", str(e))
|
||||
|
||||
def on_failure(self, task_id: str, agent_id: str,
|
||||
db_path: Path, verify: VerifyResult) -> None:
|
||||
"""验证失败 → 标 failed + Mail API 通知主公"""
|
||||
self._mark_task_status(db_path, task_id, "failed")
|
||||
logger.info("Toolchain %s: verify failed (%s), marked failed", task_id, verify.reason)
|
||||
self._notify_via_mail_api(task_id, verify.reason, verify.evidence)
|
||||
|
||||
def _notify_via_mail_api(self, task_id: str, reason: str, evidence: str) -> None:
|
||||
"""通过 Mail API 发通知给主公"""
|
||||
payload = json.dumps({
|
||||
"from": "daemon",
|
||||
"to": "pangtong-fujunshi",
|
||||
"title": f"工具链事件处理失败: {task_id}",
|
||||
"text": (
|
||||
f"任务 {task_id} 验证失败: {reason}\n"
|
||||
f"证据: {evidence}\n\n请人工检查。"
|
||||
),
|
||||
"type": "inform",
|
||||
}, ensure_ascii=False)
|
||||
try:
|
||||
subprocess.run(
|
||||
[
|
||||
"curl", "-s", "-X", "POST",
|
||||
"http://localhost:8083/api/mail",
|
||||
"-H", "Content-Type: application/json",
|
||||
"-d", payload,
|
||||
],
|
||||
timeout=5,
|
||||
capture_output=True,
|
||||
)
|
||||
logger.info("Toolchain %s: sent failure notification to pangtong-fujunshi via Mail API", task_id)
|
||||
except Exception as e:
|
||||
logger.warning("Toolchain %s: failed to notify via Mail API: %s", task_id, e)
|
||||
Reference in New Issue
Block a user