diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 4b98af7..466569d 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -5,11 +5,13 @@ # # 注意:只保留 pull_request 触发,避免 push + pull_request 双倍触发 # -# Gitea v1.23.4 限制注意: -# - 不支持 failure() 表达式,用 always() + shell 条件判断替代 -# - 不支持 concurrency / continue-on-error / timeout-minutes / permissions -# - 无内置 GITEA_TOKEN,需手动配置 PAT 为 secret -# - runs-on 只支持单个 label +# Gitea v1.26.2 已支持: +# - concurrency groups(#32751) +# - 可配置 GITEA_TOKEN 权限(#36173) +# +# 仍不支持: +# - failure() 表达式,用 always() + shell 条件判断替代 +# - continue-on-error / timeout-minutes / permissions name: CI @@ -17,6 +19,10 @@ on: pull_request: types: [opened, synchronize] +concurrency: + group: ci-${{ gitea.ref }} + cancel-in-progress: true + jobs: # ── Job 1: Lint ────────────────────────────────────── lint: diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index af91aab..d6a6a42 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -3,10 +3,15 @@ # 触发条件: # - push 到 main 分支 # -# Gitea v1.23.4 限制注意: -# - 不支持 failure() 表达式 -# - 不支持 concurrency / permissions -# - 部署脚本 scripts/deploy.sh,支持 --version/--rollback/--health-check +# Gitea v1.26.2 已支持: +# - concurrency groups(#32751) +# - 可配置 GITEA_TOKEN 权限(#36173) +# +# 仍不支持: +# - failure() 表达式,用 always() + shell 条件判断替代 +# - permissions +# +# 部署脚本 scripts/deploy.sh,支持 --version/--rollback/--health-check name: Deploy @@ -14,6 +19,10 @@ on: push: branches: [main] +concurrency: + group: deploy-${{ gitea.ref }} + cancel-in-progress: false + jobs: # ── Job 1: CI(main 分支跑完整测试)───────────────── ci: diff --git a/.gitea/workflows/e2e.yml b/.gitea/workflows/e2e.yml index 2751ec9..80d931d 100644 --- a/.gitea/workflows/e2e.yml +++ b/.gitea/workflows/e2e.yml @@ -9,8 +9,12 @@ # Agent spawn 走生产 openclaw(全局单例,无法隔离), # 测试 case 用 UUID 前缀标识。 # -# Gitea v1.23.4 限制注意: -# - 不支持 workflow_run 触发器(无法直接 needs 另一个 workflow 的 job) +# Gitea v1.26.2 已支持: +# - concurrency groups +# - workflow_dispatch 触发器(已支持) +# +# 仍不支持: +# - workflow_run 触发器(无法直接 needs 另一个 workflow 的 job) # - 此 workflow 需手动触发或在 deploy.yml 中以 needs 方式调用 # - 实际使用时可能需要合并到 deploy.yml 作为同一个 workflow 的 job # - 或依赖 daemon Webhook 监听 deploy 完成事件后通过 API 触发 @@ -19,13 +23,16 @@ name: E2E Tests on: workflow_dispatch: - # 手动触发,可选参数 inputs: test_filter: description: 'Test filter (e.g. tests/e2e/test_api.py)' required: false default: 'tests/e2e/' +concurrency: + group: e2e-${{ gitea.ref }} + cancel-in-progress: true + jobs: e2e: runs-on: ubuntu-latest diff --git a/src/daemon/base_task_handler.py b/src/daemon/base_task_handler.py index f373540..112f556 100644 --- a/src/daemon/base_task_handler.py +++ b/src/daemon/base_task_handler.py @@ -10,7 +10,7 @@ from dataclasses import dataclass from pathlib import Path from typing import Optional -from src.daemon.prompt_composer import PromptContext, PromptComposer, PromptSection +from src.daemon.prompt_composer import PromptContext, PromptSection from src.blackboard.db import get_connection logger = logging.getLogger("moziplus-v2.handler") @@ -28,46 +28,46 @@ class VerifyResult: class BaseTaskHandler: """所有 task type handler 的基类。 - + 职责:L2 引擎注入层的业务逻辑——prompt 构建、完成验证、状态标记。 不管:进程生命周期、exit 分类、重试决策(这些归 spawner)。 """ - + # crash 类 outcome(进程级异常,需要 rollback) CRASH_OUTCOMES = frozenset({ "crashed", "compact_failed", "process_crash", "session_stuck", "compact_hanging", }) - + task_type: str = "" virtual_project: Optional[str] = None display_name: str = "" # 中文展示名(ticker 扫描日志用) - + # === 子类必须实现 === - + def build_prompt(self, context: PromptContext) -> str: """构建 L2 prompt(通过 PromptComposer 拼 section)。子类实现。""" raise NotImplementedError - + def verify_completion(self, task_id: str, db_path: Path) -> VerifyResult: """验证任务完成质量。每个 handler 自己的验证逻辑。子类实现。""" raise NotImplementedError - + def target_success_status(self) -> str: """验证通过后的目标状态。task='review', mail/toolchain='done'""" return "review" - + def get_sections(self) -> list[PromptSection]: """返回此 handler 的 prompt section 列表。子类实现。""" return [] - + # === 基类提供统一流程 === - + def pre_spawn(self, task_id: str, db_path: Path) -> bool: """spawn 前业务准备。默认 True。 mail/toolchain override 为 auto_working。""" return True - + def post_complete(self, task_id: str, agent_id: str, outcome: str, db_path: Path) -> None: """spawn 完成后的业务处理。统一 4 步流程: @@ -80,10 +80,10 @@ class BaseTaskHandler: if outcome in self.CRASH_OUTCOMES: self._rollback_current_agent(db_path, task_id, agent_id) return - + # 2. verify result = self.verify_completion(task_id, db_path) - + # 3. mark if result.passed: self._mark_task_status(db_path, task_id, self.target_success_status()) @@ -92,20 +92,20 @@ class BaseTaskHandler: else: # 4. notify self.on_failure(task_id, agent_id, db_path, result) - + def on_failure(self, task_id: str, agent_id: str, db_path: Path, verify: VerifyResult) -> None: """验证失败处理。默认:标 failed。子类可 override。""" self._mark_task_status(db_path, task_id, "failed") logger.info("Task %s: verify failed (%s), marked failed", - task_id, verify.reason) - + task_id, verify.reason) + def check_completion(self, task_id: str, db_path: Path) -> bool: """ticker 级别的完成检查。默认:False。""" return False - + # === 内部工具方法 === - + def _rollback_current_agent(self, db_path: Path, task_id: str, agent_id: str) -> None: """crash 后回退 current_agent → assignee,避免 exclude_current 卡死。 从 dispatcher._rollback_current_agent 迁移。""" @@ -126,7 +126,7 @@ class BaseTaskHandler: except Exception as e: logger.warning("Task %s: failed to rollback current_agent: %s", task_id, e) - + def _mark_task_status(self, db_path: Path, task_id: str, status: str) -> None: """更新任务状态 + 写审计事件(带 3 次重试,防 SQLite DB 锁)。""" for attempt in range(3): @@ -157,7 +157,7 @@ class BaseTaskHandler: 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 通用)。""" try: diff --git a/src/daemon/mail_handler.py b/src/daemon/mail_handler.py index 4ba3cab..2b19287 100644 --- a/src/daemon/mail_handler.py +++ b/src/daemon/mail_handler.py @@ -7,7 +7,6 @@ from __future__ import annotations import json import logging from pathlib import Path -from typing import Dict, Optional from src.daemon.base_task_handler import BaseTaskHandler, VerifyResult from src.daemon.prompt_composer import PromptComposer, PromptContext @@ -15,6 +14,7 @@ from src.blackboard.db import get_connection logger = logging.getLogger("moziplus-v2.handler.mail") + class MailHandler(BaseTaskHandler): """Mail 任务 handler。""" @@ -65,7 +65,7 @@ class MailHandler(BaseTaskHandler): """request 验证失败 → 标 failed + 通知发件人""" self._mark_task_status(db_path, task_id, "failed") logger.info("Mail %s: request verify failed (%s), marked failed", - task_id, verify.reason) + task_id, verify.reason) # 通知发件人 try: @@ -95,7 +95,7 @@ class MailHandler(BaseTaskHandler): def _check_reply(self, task_id: str, db_path: Path) -> bool: """检查是否已回复(查 tasks 表找 in_reply_to 回复邮件) - + 从 dispatcher._mail_check_reply 迁移。 Mail 回复机制:创建新 task,must_haves JSON 中包含 in_reply_to = original_task_id。 不能查 comments 表——回复邮件是独立的 task,不是 comment。 diff --git a/src/daemon/prompt_composer.py b/src/daemon/prompt_composer.py index e3694d7..2eb6fa4 100644 --- a/src/daemon/prompt_composer.py +++ b/src/daemon/prompt_composer.py @@ -6,7 +6,7 @@ prompt_composer.py — PromptSection Protocol + PromptContext + PromptComposer import logging from dataclasses import dataclass, field -from typing import Any, Dict, List, Optional, Protocol, runtime_checkable +from typing import Dict, List, Optional, Protocol, runtime_checkable logger = logging.getLogger("moziplus-v2.prompt_composer") diff --git a/src/daemon/spawner.py b/src/daemon/spawner.py index c0cc1db..fb0c315 100644 --- a/src/daemon/spawner.py +++ b/src/daemon/spawner.py @@ -16,11 +16,10 @@ from pathlib import Path from typing import Any, Dict, List, Optional from src.blackboard.db import get_connection +from src.daemon.task_type_registry import TaskTypeRegistry logger = logging.getLogger("moziplus-v2.spawner") -from src.daemon.task_type_registry import TaskTypeRegistry - # ── Prompt 模板 ── diff --git a/src/daemon/task_handler.py b/src/daemon/task_handler.py index 0b447e2..33dd82a 100644 --- a/src/daemon/task_handler.py +++ b/src/daemon/task_handler.py @@ -7,7 +7,7 @@ from __future__ import annotations import logging import os from pathlib import Path -from typing import Dict, List, Optional +from typing import Dict, Optional from src.daemon.base_task_handler import BaseTaskHandler, VerifyResult from src.daemon.prompt_composer import PromptComposer, PromptContext @@ -182,7 +182,7 @@ class TaskConstraintsSection: class TaskHandler(BaseTaskHandler): """黑板标准任务 handler。 - + - verify: 三信号检查(output / comment / terminal status) - 成功 → review - 失败 → 保持 working,让 ticker 重试 @@ -198,7 +198,7 @@ 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) """ diff --git a/src/daemon/task_type_registry.py b/src/daemon/task_type_registry.py index 061fcd0..0c0504f 100644 --- a/src/daemon/task_type_registry.py +++ b/src/daemon/task_type_registry.py @@ -9,7 +9,7 @@ from __future__ import annotations import logging from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Protocol, runtime_checkable +from typing import TYPE_CHECKING, Dict, Optional, Protocol, runtime_checkable if TYPE_CHECKING: from src.daemon.prompt_composer import PromptContext diff --git a/src/daemon/toolchain_handler.py b/src/daemon/toolchain_handler.py index 2612693..3ee37ce 100644 --- a/src/daemon/toolchain_handler.py +++ b/src/daemon/toolchain_handler.py @@ -38,13 +38,13 @@ class ToolchainContextSection: return render_template(event_type, variables) # fallback:通用事件描述 - lines = [f"## 工具链事件", f""] + lines = ["## 工具链事件", ""] lines.append(f"- **事件类型**: {event_type or '未知'}") if event_data: - lines.append(f"- **事件详情**:") + lines.append("- **事件详情**:") for key, value in event_data.items(): lines.append(f" - {key}: {value}") - lines.append(f"") + lines.append("") return "\n".join(lines) def should_include(self, context: PromptContext) -> bool: