From 3920aa4fe17dc9008eedd0190ca28b123b0981c7 Mon Sep 17 00:00:00 2001 From: cfdaily Date: Fri, 29 May 2026 08:32:34 +0800 Subject: [PATCH] auto-sync: 2026-05-29 08:32:34 --- src/daemon/ticker.py | 126 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/src/daemon/ticker.py b/src/daemon/ticker.py index 91900a6..68819fa 100644 --- a/src/daemon/ticker.py +++ b/src/daemon/ticker.py @@ -535,6 +535,132 @@ Project ID: {project_id} logger.exception("Failed to spawn pangtong review for %s", parent_task.id) return False + # ------------------------------------------------------------------ + # @mention 通知处理 (v2.9 #01) + # ------------------------------------------------------------------ + + MENTION_MAX_RETRIES = 5 + + async def _process_mentions(self, db_path: Path, + project_id: str) -> List[str]: + """扫描 pending mentions → spawn 被 @ 的 Agent + + 流程(§3.4): + 1. 扫描 mention_queue 中 pending 且 retry_count < 5 的记录 + 2. 按 mentioned_agent 分组,同一 agent 多条 mention 合并为一次 spawn + 3. 尝试 spawn,成功 → notified,失败 → retry_count++ + """ + if not self.spawner: + return [] + + bb = Blackboard(db_path) + mentions = bb.get_pending_mentions(max_retries=self.MENTION_MAX_RETRIES) + if not mentions: + return [] + + # 按 mentioned_agent 分组 + agent_mentions: Dict[str, List[Dict]] = {} + for m in mentions: + aid = m["mentioned_agent"] + agent_mentions.setdefault(aid, []).append(m) + + processed: List[str] = [] + + for agent_id, items in agent_mentions.items(): + try: + # 构建 mention 摘要 + mention_lines = [] + task_ids = set() + for item in items: + mention_lines.append( + f"- [{item.get('comment_author', '?')}] {item.get('comment_body', '')[:200]}" + ) + task_ids.add(item["task_id"]) + + # 取第一个 task 作为 spawn 上下文 + tid = items[0]["task_id"] + task = bb.get_task(tid) + if not task: + continue + + # 构建 mention prompt + prompt = self._build_mention_prompt( + agent_id, task, mention_lines, project_id) + + # spawn + result = await self.spawner.spawn_full_agent( + agent_id=agent_id, + message=prompt, + new_session=True, + task_id=tid, + use_main_session=False, + ) + + if result is not None: + # 成功 → 标记所有该 agent 的 mentions 为 notified + for item in items: + bb.mark_mention_notified(item["id"]) + processed.append(agent_id) + logger.info("Mention spawn success: %s (%d mentions)", agent_id, len(items)) + else: + # spawn 失败 → 递增 retry_count + for item in items: + bb.mark_mention_retry(item["id"]) + logger.warning("Mention spawn failed: %s, retrying next tick", agent_id) + + except Exception as e: + logger.exception("Mention processing error for agent %s", agent_id) + for item in items: + try: + if item.get("retry_count", 0) >= self.MENTION_MAX_RETRIES - 1: + bb.mark_mention_failed(item["id"]) + else: + bb.mark_mention_retry(item["id"]) + except Exception: + pass + + return processed + + def _build_mention_prompt(self, agent_id: str, task: Any, + mention_lines: List[str], + project_id: str) -> str: + """构建 @mention 通知 prompt""" + mentions_text = "\n".join(mention_lines[:10]) + api_host = getattr(self.spawner, 'api_host', '127.0.0.1') if self.spawner else '127.0.0.1' + api_port = getattr(self.spawner, 'api_port', 8083) if self.spawner else 8083 + + return f"""你在黑板上被 @ 了,请查看并回应。 + +## 相关讨论 +{mentions_text} + +## 任务上下文 +- 项目: {project_id} +- 任务: {task.title} +- 描述: {task.description or '无'} + +## 操作 +1. 先读黑板了解上下文: +```bash +curl http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task.id}?expand=all +``` + +2. 在黑板上回应(写 comment): +```bash +curl -X POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task.id}/comments \ + -H 'Content-Type: application/json' \ + -d '{{"author": "{agent_id}", "body": "你的回应"}}' +``` + +3. 如果讨论收敛到可执行的任务,可以创建 sub task +4. 完成后标记 done: +```bash +curl -X POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task.id}/status \ + -H 'Content-Type: application/json' \ + -d '{{"status": "done", "agent": "{agent_id}"}}' +``` +""" + def _advance_dependencies(self, db_path: Path) -> List[str]: """检查 blocked 任务,若所有依赖已完成则推进为 pending