feat: Step 5 引擎接入 + H1-H3/S3 修复 + 审计 D1/D2/D5 修复
引擎接入(dispatcher/spawner/ticker → handler 统一路由): - dispatcher: guardrail/on_checks_passed/on_complete → handler 查询 - spawner: _build_prompt/_build_api_section → handler.build_prompt - ticker: 虚拟项目扫描/assignee/claimed/review/幻觉门控 → handler 判断 Handler 缺陷修复: - H1: _mark_task_status 加 3 次重试(防 DB 锁) - H2: review @mention 加 comment_type='review' - H3: review 非 approved 保持 review 状态(不标 working) - S3: 通知链接改 Gitea(PR/Issue/Commit) 审计修复: - D1: pre_spawn 返回值未检查 → 加 if not 抛 RuntimeError - D2: PromptContext 缺 from_agent/mail_type → 从 must_haves 解析 - D5: _check_reply 查错表 → 恢复查 tasks 表找 in_reply_to 旧方法保留未删(deprecated),确认稳定后再清理。
This commit is contained in:
@@ -127,32 +127,35 @@ class BaseTaskHandler:
|
||||
task_id, e)
|
||||
|
||||
def _mark_task_status(self, db_path: Path, task_id: str, status: str) -> None:
|
||||
"""更新任务状态 + 写审计事件。
|
||||
从 dispatcher._mark_task_status 迁移。"""
|
||||
try:
|
||||
conn = get_connection(db_path)
|
||||
"""更新任务状态 + 写审计事件(带 3 次重试,防 SQLite DB 锁)。"""
|
||||
for attempt in range(3):
|
||||
try:
|
||||
conn.execute("BEGIN IMMEDIATE")
|
||||
old_row = conn.execute(
|
||||
"SELECT status FROM tasks WHERE id=?", (task_id,)
|
||||
).fetchone()
|
||||
old_status = old_row["status"] if old_row else "unknown"
|
||||
conn.execute(
|
||||
"UPDATE tasks SET status=?, updated_at=datetime('now') WHERE id=?",
|
||||
(status, task_id),
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO events (task_id, agent, event_type, payload) "
|
||||
"VALUES (?, 'handler', 'status_change', ?)",
|
||||
(task_id,
|
||||
f'{{"from": "{old_status}", "to": "{status}", '
|
||||
f'"source": "{self.task_type}_handler"}}'),
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
logger.error("Task %s: mark status error: %s", task_id, e)
|
||||
conn = get_connection(db_path)
|
||||
try:
|
||||
conn.execute("BEGIN IMMEDIATE")
|
||||
old_row = conn.execute(
|
||||
"SELECT status FROM tasks WHERE id=?", (task_id,)
|
||||
).fetchone()
|
||||
old_status = old_row["status"] if old_row else "unknown"
|
||||
conn.execute(
|
||||
"UPDATE tasks SET status=?, updated_at=datetime('now') WHERE id=?",
|
||||
(status, task_id),
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO events (task_id, agent, event_type, payload) "
|
||||
"VALUES (?, 'handler', 'status_change', ?)",
|
||||
(task_id,
|
||||
f'{{"from": "{old_status}", "to": "{status}", '
|
||||
f'"source": "{self.task_type}_handler"}}'),
|
||||
)
|
||||
conn.commit()
|
||||
return
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
logger.warning("Handler: mark %s → %s attempt %d failed: %s",
|
||||
task_id, status, attempt + 1, e)
|
||||
logger.error("Handler: mark %s → %s all 3 attempts failed", task_id, status)
|
||||
|
||||
def _auto_mark_working(self, task_id: str, db_path: Path) -> bool:
|
||||
"""pending → working(mail/toolchain 通用)。"""
|
||||
|
||||
+73
-103
@@ -22,6 +22,7 @@ from src.blackboard.models import Task
|
||||
from src.blackboard.db import get_connection
|
||||
from src.daemon.spawner import AgentBusyError
|
||||
from src.daemon.router import AgentRouter
|
||||
from src.daemon.task_type_registry import TaskTypeRegistry
|
||||
|
||||
logger = logging.getLogger("moziplus-v2.dispatcher")
|
||||
|
||||
@@ -123,10 +124,11 @@ class Dispatcher:
|
||||
"status": "dispatched"|"skipped"|"error"|"blocked", "reason": str}
|
||||
"""
|
||||
# 安全红线检查(调度前拦截)
|
||||
# Mail 是 Agent 间通信,不做 guardrail 检查
|
||||
is_mail = project_config.get(
|
||||
"project_id") == "_mail" if project_config else False
|
||||
if self.guardrails and not is_mail:
|
||||
# handler 项目(_mail/_toolchain)不做 guardrail 检查
|
||||
handler = TaskTypeRegistry.get_by_project(
|
||||
project_config.get("project_id", "") if project_config else "")
|
||||
is_handler_task = handler is not None
|
||||
if self.guardrails and not is_handler_task:
|
||||
violations = self.guardrails.check_task(task)
|
||||
critical = [
|
||||
v for v in violations if v.action in (
|
||||
@@ -190,27 +192,26 @@ class Dispatcher:
|
||||
}
|
||||
|
||||
try:
|
||||
# [v2.7.1] Mail: 标 working 移到 spawn_full_agent 内部(check 通过后、subprocess 前)
|
||||
is_mail = project_config.get(
|
||||
"project_id") == "_mail" if project_config else False
|
||||
if is_mail:
|
||||
db_path = Path(
|
||||
project_config["db_path"]) if project_config and "db_path" in project_config else None
|
||||
# [Step 5] Handler: pre_spawn + on_checks_passed 统一
|
||||
project_id = project_config.get("project_id", "") if project_config else ""
|
||||
handler = TaskTypeRegistry.get_by_project(project_id)
|
||||
db_path = Path(
|
||||
project_config["db_path"]) if project_config and "db_path" in project_config else None
|
||||
|
||||
# on_checks_passed: 所有检查通过后才标 working,检查失败不标
|
||||
# on_checks_passed: handler 项目在 check 通过后调用 handler.pre_spawn
|
||||
on_checks_passed = None
|
||||
_mail_marked_working = False
|
||||
if is_mail and db_path:
|
||||
handler_marked_working = False
|
||||
if handler and db_path:
|
||||
_task_id = task.id
|
||||
_mail_db = db_path
|
||||
_disp = self
|
||||
_handler_db = db_path
|
||||
_handler = handler
|
||||
|
||||
def _mail_on_checks_passed():
|
||||
nonlocal _mail_marked_working
|
||||
if not _disp._mail_auto_working(_task_id, _mail_db):
|
||||
raise RuntimeError("mail_auto_working_failed")
|
||||
_mail_marked_working = True
|
||||
on_checks_passed = _mail_on_checks_passed
|
||||
def _handler_on_checks_passed():
|
||||
nonlocal handler_marked_working
|
||||
if not _handler.pre_spawn(_task_id, _handler_db):
|
||||
raise RuntimeError("handler_pre_spawn_failed")
|
||||
handler_marked_working = True
|
||||
on_checks_passed = _handler_on_checks_passed
|
||||
|
||||
# 构建 spawn message
|
||||
message = self._build_spawn_message(task, agent_id, project_config,
|
||||
@@ -218,94 +219,46 @@ class Dispatcher:
|
||||
"mode", ""),
|
||||
spawn_type=action_type or "executor")
|
||||
|
||||
# v2.7.2: on_complete 只含业务逻辑,不含 counter.release
|
||||
# counter.release 由 spawn_full_agent 内部的 wrapped_on_complete 保证
|
||||
# [Step 5] Handler: on_complete 统一走 handler.post_complete
|
||||
# 保留旧路径作为 fallback(无 handler 的项目)
|
||||
on_complete = None
|
||||
if is_mail:
|
||||
if handler:
|
||||
_task_id = task.id
|
||||
_mail_db = db_path
|
||||
_must_haves = task.must_haves or ""
|
||||
_dispatcher = self
|
||||
_handler_db = db_path
|
||||
_handler = handler
|
||||
|
||||
def _mail_on_complete(aid, outcome):
|
||||
# 幻觉门控:检查是否有回复,自动标 done/failed
|
||||
def _handler_on_complete(aid, outcome):
|
||||
try:
|
||||
_dispatcher._mail_auto_complete(
|
||||
_task_id, aid, _mail_db, _must_haves, outcome=outcome)
|
||||
_handler.post_complete(
|
||||
_task_id, aid, outcome, _handler_db)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Mail %s: on_complete error: %s", _task_id, e)
|
||||
on_complete = _mail_on_complete
|
||||
"Handler %s: on_complete error: %s", _task_id, e)
|
||||
on_complete = _handler_on_complete
|
||||
else:
|
||||
# #02: Task 路径也加 on_complete(幻觉门控)
|
||||
# 旧路径:无 handler 的项目(_general 等)
|
||||
_task_id = task.id
|
||||
_task_db = Path(
|
||||
project_config["db_path"]) if project_config and "db_path" in project_config else None
|
||||
_task_db = db_path
|
||||
_dispatcher = self
|
||||
_is_review = action_type == "review"
|
||||
|
||||
# #07.2: executor/review 统一 crash 回退
|
||||
ROLLBACK_CURRENT_AGENT_OUTCOMES = frozenset({
|
||||
"crashed", "compact_failed", "process_crash",
|
||||
"session_stuck", "compact_hanging",
|
||||
})
|
||||
|
||||
def _task_on_complete(aid, outcome):
|
||||
def _legacy_on_complete(aid, outcome):
|
||||
try:
|
||||
# #07.2: 统一 crash 回退——executor 和 review 都回退 current_agent
|
||||
if outcome in ROLLBACK_CURRENT_AGENT_OUTCOMES and _task_db:
|
||||
_dispatcher._rollback_current_agent(
|
||||
_task_db, _task_id, aid)
|
||||
|
||||
if _is_review:
|
||||
if _task_db and outcome in (
|
||||
"completed", "session_revived"):
|
||||
# #09: 读 verdict 决定后续动作
|
||||
conn = get_connection(_task_db)
|
||||
try:
|
||||
review = conn.execute(
|
||||
"SELECT verdict FROM reviews WHERE task_id=? ORDER BY created_at DESC LIMIT 1",
|
||||
(_task_id,)
|
||||
).fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if review and review["verdict"] == "approved":
|
||||
_dispatcher._mark_task_status(
|
||||
_task_db, _task_id, "done")
|
||||
logger.info(
|
||||
"Task %s: review approved, marking done", _task_id)
|
||||
else:
|
||||
# 非 approved → @mention 被审
|
||||
# agent(assignee,非 current_agent)
|
||||
verdict_str = review["verdict"] if review else "未知"
|
||||
conn2 = get_connection(_task_db)
|
||||
try:
|
||||
task_row = conn2.execute(
|
||||
"SELECT assignee FROM tasks WHERE id=?", (_task_id,)).fetchone()
|
||||
finally:
|
||||
conn2.close()
|
||||
|
||||
if task_row and task_row["assignee"]:
|
||||
from src.blackboard.blackboard import Blackboard
|
||||
bb = Blackboard(_task_db)
|
||||
bb.add_comment(_task_id, "daemon",
|
||||
f"@{task_row['assignee']} 审查结论: {verdict_str},请查看详情并决定接受或反驳",
|
||||
comment_type="review")
|
||||
logger.info("Task %s: review verdict=%s, notified assignee=%s",
|
||||
_task_id, verdict_str, task_row["assignee"] if task_row else "?")
|
||||
# 不标 done,保持 review 状态
|
||||
else:
|
||||
logger.warning(
|
||||
"Task %s: review agent %s (%s), NOT marking done", _task_id, aid, outcome)
|
||||
else:
|
||||
# executor: 三信号验证 → 标 review
|
||||
if not _is_review:
|
||||
_dispatcher._task_auto_complete(
|
||||
_task_id, _task_db)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Task %s: on_complete error: %s", _task_id, e)
|
||||
on_complete = _task_on_complete
|
||||
"Legacy %s: on_complete error: %s", _task_id, e)
|
||||
on_complete = _legacy_on_complete
|
||||
|
||||
session_id = await self.spawner.spawn_full_agent(
|
||||
agent_id=agent_id,
|
||||
@@ -354,8 +307,26 @@ class Dispatcher:
|
||||
}
|
||||
except Exception as e:
|
||||
# on_checks_passed 已执行但 subprocess 失败 → 回退 working → pending
|
||||
if _mail_marked_working:
|
||||
self._mail_revert_to_pending(task.id, db_path)
|
||||
if handler_marked_working and handler and db_path:
|
||||
# handler 项目:回退到 pending
|
||||
try:
|
||||
conn = get_connection(db_path)
|
||||
try:
|
||||
conn.execute("BEGIN IMMEDIATE")
|
||||
row = conn.execute(
|
||||
"SELECT status FROM tasks WHERE id=?", (task.id,)).fetchone()
|
||||
if row and row["status"] == "working":
|
||||
conn.execute(
|
||||
"UPDATE tasks SET status='pending', updated_at=datetime('now') WHERE id=?",
|
||||
(task.id,))
|
||||
conn.commit()
|
||||
logger.info(
|
||||
"Task %s: reverted working → pending (spawn failed)", task.id)
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception as revert_err:
|
||||
logger.error(
|
||||
"Task %s: failed to revert to pending: %s", task.id, revert_err)
|
||||
self._record_routing(
|
||||
task, decision, "error", str(e), _routing_db)
|
||||
return {
|
||||
@@ -580,17 +551,18 @@ class Dispatcher:
|
||||
try:
|
||||
# NOTE: _legacy_dispatch 仅在 router=None 时触发,当前配置不会进入。
|
||||
# Mail 永远走 dispatch() 主路径(on_checks_passed 方案),不走此路径。
|
||||
# 如果未来 legacy 路径被启用,需同步 on_checks_passed 逻辑。
|
||||
is_mail_legacy = project_config.get(
|
||||
"project_id") == "_mail" if project_config else False
|
||||
if is_mail_legacy:
|
||||
# [Step 5] handler 统一:用注册表查 handler
|
||||
project_id_legacy = project_config.get("project_id", "") if project_config else ""
|
||||
handler_legacy = TaskTypeRegistry.get_by_project(project_id_legacy)
|
||||
if handler_legacy:
|
||||
db_path_legacy = Path(
|
||||
project_config["db_path"]) if project_config and "db_path" in project_config else None
|
||||
if not db_path_legacy or not self._mail_auto_working(
|
||||
task.id, db_path_legacy):
|
||||
if db_path_legacy:
|
||||
handler_legacy.pre_spawn(task.id, db_path_legacy)
|
||||
else:
|
||||
return {"level": level.value, "agent_id": agent_id,
|
||||
"session_id": None, "status": "error",
|
||||
"reason": "mail_auto_working_failed"}
|
||||
"reason": "no db_path for handler"}
|
||||
|
||||
if hasattr(self.spawner,
|
||||
'build_spawn_message') and project_config:
|
||||
@@ -612,20 +584,18 @@ class Dispatcher:
|
||||
|
||||
# v2.7.2: on_complete 只含业务逻辑
|
||||
on_complete_legacy = None
|
||||
if is_mail_legacy:
|
||||
if handler_legacy:
|
||||
_t_id = task.id
|
||||
_m_db = db_path_legacy
|
||||
_m_mh = task.must_haves or ""
|
||||
_disp = self
|
||||
_h_db = db_path_legacy
|
||||
_h = handler_legacy
|
||||
|
||||
def _mail_oc_legacy(aid, outcome):
|
||||
def _handler_oc_legacy(aid, outcome):
|
||||
try:
|
||||
_disp._mail_auto_complete(
|
||||
_t_id, aid, _m_db, _m_mh, outcome=outcome)
|
||||
_h.post_complete(_t_id, aid, outcome, _h_db)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Mail %s: legacy on_complete error: %s", _t_id, e)
|
||||
on_complete_legacy = _mail_oc_legacy
|
||||
"Handler %s: legacy on_complete error: %s", _t_id, e)
|
||||
on_complete_legacy = _handler_oc_legacy
|
||||
|
||||
session_id = await self.spawner.spawn_full_agent(
|
||||
agent_id=agent_id, message=message,
|
||||
|
||||
@@ -93,23 +93,26 @@ class MailHandler(BaseTaskHandler):
|
||||
return "request"
|
||||
|
||||
def _check_reply(self, task_id: str, db_path: Path) -> bool:
|
||||
"""检查是否已回复(从 dispatcher._mail_check_reply 迁移)"""
|
||||
"""检查是否已回复(查 tasks 表找 in_reply_to 回复邮件)
|
||||
|
||||
从 dispatcher._mail_check_reply 迁移。
|
||||
Mail 回复机制:创建新 task,must_haves JSON 中包含 in_reply_to = original_task_id。
|
||||
不能查 comments 表——回复邮件是独立的 task,不是 comment。
|
||||
"""
|
||||
try:
|
||||
conn = get_connection(db_path)
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT COUNT(*) as cnt FROM comments "
|
||||
"WHERE task_id=? AND author != 'daemon' "
|
||||
"AND comment_type != 'system'",
|
||||
(task_id,)
|
||||
"SELECT id FROM tasks WHERE id != ? AND must_haves LIKE ? LIMIT 1",
|
||||
(task_id, f'%{task_id}%'),
|
||||
).fetchone()
|
||||
count = row["cnt"] if row else 0
|
||||
return count > 0
|
||||
return row is not None
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
logger.error("Mail %s: check reply error: %s", task_id, e)
|
||||
return False
|
||||
# 查询失败时保守处理:假设有回复(避免误标 failed)
|
||||
return True
|
||||
|
||||
def check_completion(self, task_id: str, db_path: Path) -> bool:
|
||||
"""ticker 级别的完成检查:检查是否已回复"""
|
||||
|
||||
+32
-6
@@ -278,10 +278,30 @@ class AgentSpawner:
|
||||
task_id, title, description, must_haves,
|
||||
project_id, agent_id)
|
||||
|
||||
# mail 任务用精简模板
|
||||
if project_id == "_mail":
|
||||
return self._build_mail_prompt(
|
||||
task_id, title, description, must_haves, agent_id)
|
||||
# handler 路径:Task/Mail/Toolchain 用各自的 PromptSection 构建
|
||||
from src.daemon.task_type_registry import TaskTypeRegistry
|
||||
handler = TaskTypeRegistry.get_by_project(project_id)
|
||||
if handler:
|
||||
from src.daemon.prompt_composer import PromptContext
|
||||
# 从 must_haves 解析 mail 元数据(from / performative)
|
||||
from_agent = ""
|
||||
mail_type = ""
|
||||
try:
|
||||
meta = json.loads(must_haves) if must_haves else {}
|
||||
from_agent = meta.get("from", "")
|
||||
mail_type = meta.get("performative", meta.get("type", ""))
|
||||
except Exception:
|
||||
pass
|
||||
ctx = PromptContext(
|
||||
task_id=task_id, title=title, description=description or "",
|
||||
must_haves=must_haves or "", project_id=project_id,
|
||||
agent_id=agent_id, role=spawn_type,
|
||||
spawn_type=spawn_type,
|
||||
from_agent=from_agent, mail_type=mail_type,
|
||||
)
|
||||
return handler.build_prompt(ctx)
|
||||
|
||||
# 旧路径保留:_general 等非 handler 项目
|
||||
|
||||
# 走 BootstrapBuilder 新路径
|
||||
if self.bootstrap_builder and task is not None:
|
||||
@@ -321,8 +341,14 @@ class AgentSpawner:
|
||||
def _build_api_section(self, project_id: str, task_id: str,
|
||||
agent_id: str) -> str:
|
||||
"""构建 API 回写操作指令(BootstrapBuilder 模式下补充)"""
|
||||
# mail 任务直接 done,不走 review
|
||||
success_status = '"done"' if project_id == "_mail" else '"review"'
|
||||
# handler 项目(_mail/_toolchain)的 success_status 由 PromptSection 处理
|
||||
# 这里只处理无 handler 的项目(normal task)
|
||||
from src.daemon.task_type_registry import TaskTypeRegistry
|
||||
handler = TaskTypeRegistry.get_by_project(project_id)
|
||||
if handler:
|
||||
success_status = '"done"' if handler.target_success_status == "done" else '"review"'
|
||||
else:
|
||||
success_status = '"review"'
|
||||
return f"""## 操作指令
|
||||
|
||||
### 状态回写
|
||||
|
||||
@@ -185,6 +185,51 @@ class TaskHandler(BaseTaskHandler):
|
||||
|
||||
# === 子类实现 ===
|
||||
|
||||
def post_complete(self, task_id: str, agent_id: str,
|
||||
outcome: str, db_path: Path) -> None:
|
||||
"""Task on_complete:区分 executor 和 review。
|
||||
|
||||
executor: 基类统一流程(crash → verify → mark review)
|
||||
review: handle_review_complete(读 verdict → done/keep review)
|
||||
"""
|
||||
# crash 处理(所有类型共用)
|
||||
if outcome in self.CRASH_OUTCOMES:
|
||||
self._rollback_current_agent(db_path, task_id, agent_id)
|
||||
return
|
||||
|
||||
# 检查当前任务状态:如果是 review 状态 → review 完成流程
|
||||
try:
|
||||
conn = get_connection(db_path)
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT status FROM tasks WHERE id=?", (task_id,)
|
||||
).fetchone()
|
||||
task_status = row["status"] if row else "unknown"
|
||||
finally:
|
||||
conn.close()
|
||||
except Exception:
|
||||
task_status = "unknown"
|
||||
|
||||
if task_status == "review":
|
||||
# review 完成流程:只处理正常 outcome
|
||||
if outcome in ("completed", "session_revived"):
|
||||
self.handle_review_complete(task_id, db_path)
|
||||
else:
|
||||
logger.warning(
|
||||
"Task %s: review agent %s abnormal outcome=%s, keeping review",
|
||||
task_id, agent_id, outcome)
|
||||
else:
|
||||
# executor 完成流程:基类统一 verify → mark
|
||||
result = self.verify_completion(task_id, db_path)
|
||||
if result.passed:
|
||||
self._mark_task_status(db_path, task_id, self.target_success_status())
|
||||
logger.info("Task %s: verify passed (%s), marked %s",
|
||||
task_id, result.reason, self.target_success_status())
|
||||
else:
|
||||
logger.info(
|
||||
"Task %s: verify not passed (%s), leaving working",
|
||||
task_id, result.reason)
|
||||
|
||||
def target_success_status(self) -> str:
|
||||
"""task 类型验证通过后进 review。"""
|
||||
return "review"
|
||||
@@ -309,19 +354,18 @@ class TaskHandler(BaseTaskHandler):
|
||||
task_id, reviewer)
|
||||
else:
|
||||
# 非 approved:通过 blackboard comment @mention assignee
|
||||
# 保持 review 状态,让 assignee 自行决定下一步
|
||||
conn.execute(
|
||||
"INSERT INTO comments (task_id, author, content) "
|
||||
"VALUES (?, 'system', ?)",
|
||||
"INSERT INTO comments (task_id, author, content, comment_type) "
|
||||
"VALUES (?, 'system', ?, 'review')",
|
||||
(task_id,
|
||||
f"@{assignee} review 未通过 (verdict={verdict}, "
|
||||
f"reviewer={reviewer}): {review_comment}")
|
||||
)
|
||||
conn.commit()
|
||||
# 回到 working 让 assignee 重新处理
|
||||
self._mark_task_status(db_path, task_id, "working")
|
||||
logger.info(
|
||||
"Task %s: review not approved (%s by %s), "
|
||||
"@mentioned assignee %s, back to working",
|
||||
"@mentioned assignee %s, keeping review status",
|
||||
task_id, verdict, reviewer, assignee
|
||||
)
|
||||
finally:
|
||||
|
||||
+36
-25
@@ -215,18 +215,21 @@ class Ticker:
|
||||
logger.exception("Tick %d _general error", tick_num)
|
||||
results["projects"]["_general"] = {"error": str(e)}
|
||||
|
||||
# 虚拟项目 _mail:飞鸽传书
|
||||
mail_db = Path(self.registry.root) / "_mail" / "blackboard.db"
|
||||
if mail_db.exists() and "_mail" not in active_projects:
|
||||
try:
|
||||
pr = await self._tick_project("_mail", {
|
||||
"id": "_mail", "name": "飞鸽传书",
|
||||
"status": "active", "source": "virtual",
|
||||
})
|
||||
results["projects"]["_mail"] = pr
|
||||
except Exception as e:
|
||||
logger.exception("Tick %d _mail error", tick_num)
|
||||
results["projects"]["_mail"] = {"error": str(e)}
|
||||
# 虚拟项目:从注册表自动发现 + _general 硬编码
|
||||
from src.daemon.task_type_registry import TaskTypeRegistry
|
||||
for vp in TaskTypeRegistry.virtual_projects():
|
||||
vp_db = Path(self.registry.root) / vp / "blackboard.db"
|
||||
if vp_db.exists() and vp not in active_projects:
|
||||
try:
|
||||
vp_name = {"_mail": "飞鸽传书", "_toolchain": "工具链事件"}.get(vp, vp)
|
||||
pr = await self._tick_project(vp, {
|
||||
"id": vp, "name": vp_name,
|
||||
"status": "active", "source": "virtual",
|
||||
})
|
||||
results["projects"][vp] = pr
|
||||
except Exception as e:
|
||||
logger.exception("Tick %d %s error", tick_num, vp)
|
||||
results["projects"][vp] = {"error": str(e)}
|
||||
|
||||
logger.debug(
|
||||
"Tick %d complete: %d projects",
|
||||
@@ -948,9 +951,11 @@ Parent Task ID: {parent_task.id}
|
||||
|
||||
now = datetime.utcnow().isoformat()
|
||||
# 重置到 pending 时清空 assignee(避免残留导致重复路由到同一 Agent)
|
||||
# 但 Mail 的 assignee 是收件人,永不清空
|
||||
# handler 虚拟项目(_mail 等)的 assignee 是收件人,永不清空
|
||||
if new_status == "pending":
|
||||
if self._current_project_id == "_mail":
|
||||
from src.daemon.task_type_registry import TaskTypeRegistry
|
||||
handler = TaskTypeRegistry.get_by_project(self._current_project_id)
|
||||
if handler:
|
||||
conn.execute(
|
||||
"UPDATE tasks SET status=?, updated_at=? WHERE id=?",
|
||||
(new_status, now, task_id),
|
||||
@@ -1025,15 +1030,17 @@ Parent Task ID: {parent_task.id}
|
||||
"full", "escalate"):
|
||||
conn = get_connection(db_path)
|
||||
try:
|
||||
# [v2.7.1] Mail 已在 dispatcher 中标 working,跳过 claimed
|
||||
if project_id == "_mail":
|
||||
# [Step 5] handler 项目已在 dispatcher 中标 working,跳过 claimed
|
||||
from src.daemon.task_type_registry import TaskTypeRegistry
|
||||
handler = TaskTypeRegistry.get_by_project(project_id)
|
||||
if handler:
|
||||
conn.execute(
|
||||
"UPDATE tasks SET current_agent=? WHERE id=?",
|
||||
(result["agent_id"], task.id),
|
||||
)
|
||||
conn.commit()
|
||||
dispatched.append(task.id)
|
||||
logger.info("Dispatched %s to %s (session=%s, mail auto-working)",
|
||||
logger.info("Dispatched %s to %s (session=%s, handler auto-working)",
|
||||
task.id, result["agent_id"],
|
||||
result.get("session_id"))
|
||||
else:
|
||||
@@ -1300,8 +1307,10 @@ Parent Task ID: {parent_task.id}
|
||||
async def _dispatch_reviews(self, db_path: Path,
|
||||
project_id: str) -> List[str]:
|
||||
"""扫描 review 状态任务,检查是否有产出,调度审查 Agent"""
|
||||
# mail 任务不走 review 流程,直接跳过
|
||||
if project_id == "_mail":
|
||||
# handler 项目(_mail/_toolchain)不走 review 流程
|
||||
from src.daemon.task_type_registry import TaskTypeRegistry
|
||||
handler = TaskTypeRegistry.get_by_project(project_id)
|
||||
if handler:
|
||||
return []
|
||||
|
||||
queries = Queries(db_path)
|
||||
@@ -1470,10 +1479,10 @@ Parent Task ID: {parent_task.id}
|
||||
|
||||
elapsed = (now - start_time).total_seconds() / 60.0
|
||||
if elapsed > timeout_minutes:
|
||||
# [v2.7.1] Mail 幻觉门控兜底:有回复 + working → done
|
||||
if self._current_project_id == "_mail":
|
||||
has_reply = self._mail_check_reply(task.id, db_path)
|
||||
if has_reply:
|
||||
# [Step 5] handler 幻觉门控兜底:check_completion 通过 + working → done
|
||||
from src.daemon.task_type_registry import TaskTypeRegistry
|
||||
handler = TaskTypeRegistry.get_by_project(self._current_project_id)
|
||||
if handler and handler.check_completion(task.id, db_path):
|
||||
conn = get_connection(db_path)
|
||||
try:
|
||||
ok = self._transition_status(
|
||||
@@ -1621,8 +1630,10 @@ Parent Task ID: {parent_task.id}
|
||||
project_dirs[project_id] = self.registry.root / \
|
||||
project_id / "blackboard.db"
|
||||
|
||||
# 虚拟项目
|
||||
for virtual_id in ("_general", "_mail"):
|
||||
# 虚拟项目:_general + 注册表自动发现
|
||||
from src.daemon.task_type_registry import TaskTypeRegistry
|
||||
virtual_ids = ["_general"] + TaskTypeRegistry.virtual_projects()
|
||||
for virtual_id in virtual_ids:
|
||||
virtual_db = Path(self.registry.root) / \
|
||||
virtual_id / "blackboard.db"
|
||||
if virtual_db.exists() and virtual_id not in project_dirs:
|
||||
|
||||
@@ -199,6 +199,23 @@ class ToolchainHandler(BaseTaskHandler):
|
||||
event_type, event_data,
|
||||
)
|
||||
|
||||
def _build_gitea_links(self, event_type: str, event_data: dict) -> str:
|
||||
"""根据事件类型构建 Gitea 链接。"""
|
||||
links = []
|
||||
repo = event_data.get("repo", "")
|
||||
base_url = "http://192.168.2.154:3000"
|
||||
|
||||
if "pr_number" in event_data:
|
||||
links.append(f"PR: {base_url}/{repo}/pulls/{event_data['pr_number']}")
|
||||
if "issue_number" in event_data:
|
||||
links.append(f"Issue: {base_url}/{repo}/issues/{event_data['issue_number']}")
|
||||
if "commit" in event_data:
|
||||
links.append(f"Commit: {base_url}/{repo}/commit/{event_data['commit']}")
|
||||
if "branch" in event_data and "commit" not in event_data:
|
||||
links.append(f"分支: {event_data['branch']}")
|
||||
|
||||
return "\n".join(links) if links else "(无法提取链接,请检查黑板任务详情)"
|
||||
|
||||
def _notify_via_mail_api(
|
||||
self,
|
||||
task_id: str,
|
||||
@@ -225,6 +242,9 @@ class ToolchainHandler(BaseTaskHandler):
|
||||
f" - {k}: {v}" for k, v in event_data.items()
|
||||
)
|
||||
|
||||
# 构建 Gitea 链接
|
||||
gitea_links = self._build_gitea_links(event_type, event_data)
|
||||
|
||||
title = f"[toolchain-handler] 工具链事件处理失败: {task_id}"
|
||||
text = (
|
||||
f"任务 {task_id} 验证失败\n\n"
|
||||
@@ -232,7 +252,7 @@ class ToolchainHandler(BaseTaskHandler):
|
||||
f"事件详情:\n{event_details or ' (无)'}\n\n"
|
||||
f"失败原因: {reason}\n"
|
||||
f"证据: {evidence}\n\n"
|
||||
f"黑板任务: http://localhost:8083/ → 项目 _toolchain → 任务 {task_id}\n\n"
|
||||
f"{gitea_links}\n\n"
|
||||
f"行动指引: {action_hint}"
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user