chore: simayi-approved changes - lint fixes, toolchain improvements, healthz
All changes reviewed and APPROVED in PR #12 (Review ID: 40): - toolchain_routes: webhook repo/org format compat, content dedup (sha256), closed issue filter - dispatcher: inform mail crash 误标 done 修复 - ticker: cleanup and improvements - healthz endpoint - conftest: integration/e2e deselect markers - docs: design docs, test-guide updates - various lint/whitespace fixes across 30 files
This commit is contained in:
@@ -5,17 +5,17 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Query
|
from fastapi import APIRouter, HTTPException, Query
|
||||||
|
|
||||||
from src.blackboard.operations import Blackboard
|
from src.blackboard.operations import Blackboard
|
||||||
from src.blackboard.models import Task, Review
|
from src.blackboard.models import Task, Review
|
||||||
from src.blackboard.queries import Queries
|
from src.blackboard.queries import Queries
|
||||||
from src.blackboard.db import VALID_STATUSES, OUTPUT_TYPES
|
from src.blackboard.db import VALID_STATUSES, VALID_TRANSITIONS, COMMENT_TYPES, OUTPUT_TYPES
|
||||||
from src.blackboard.registry import ProjectRegistry
|
from src.blackboard.registry import ProjectRegistry
|
||||||
|
|
||||||
import src.utils as _utils
|
from src.utils import get_data_root
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/projects/{project_id}", tags=["blackboard"])
|
router = APIRouter(prefix="/api/projects/{project_id}", tags=["blackboard"])
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ def _validate_project(project_id: str) -> str:
|
|||||||
"""校验 project_id,已知项目/虚拟项目放行,未知项目返回 400"""
|
"""校验 project_id,已知项目/虚拟项目放行,未知项目返回 400"""
|
||||||
if project_id in _VIRTUAL_PROJECTS:
|
if project_id in _VIRTUAL_PROJECTS:
|
||||||
return project_id
|
return project_id
|
||||||
reg = ProjectRegistry(_utils.get_data_root())
|
reg = ProjectRegistry(get_data_root())
|
||||||
if reg.get_project(project_id):
|
if reg.get_project(project_id):
|
||||||
return project_id
|
return project_id
|
||||||
raise HTTPException(400, {
|
raise HTTPException(400, {
|
||||||
@@ -43,12 +43,12 @@ def _validate_project(project_id: str) -> str:
|
|||||||
|
|
||||||
def _bb(project_id: str) -> Blackboard:
|
def _bb(project_id: str) -> Blackboard:
|
||||||
_validate_project(project_id)
|
_validate_project(project_id)
|
||||||
return Blackboard(_utils.get_data_root() / project_id / "blackboard.db")
|
return Blackboard(get_data_root() / project_id / "blackboard.db")
|
||||||
|
|
||||||
|
|
||||||
def _q(project_id: str) -> Queries:
|
def _q(project_id: str) -> Queries:
|
||||||
_validate_project(project_id)
|
_validate_project(project_id)
|
||||||
return Queries(_utils.get_data_root() / project_id / "blackboard.db")
|
return Queries(get_data_root() / project_id / "blackboard.db")
|
||||||
|
|
||||||
|
|
||||||
# --- Tasks ---
|
# --- Tasks ---
|
||||||
@@ -100,7 +100,7 @@ async def create_task(project_id: str, body: Dict[str, Any]):
|
|||||||
date_str = datetime.now().strftime('%Y%m%d')
|
date_str = datetime.now().strftime('%Y%m%d')
|
||||||
# seq: 查当前项目最大 seq
|
# seq: 查当前项目最大 seq
|
||||||
import sqlite3
|
import sqlite3
|
||||||
db_path = _utils.get_data_root() / project_id / "blackboard.db"
|
db_path = get_data_root() / project_id / "blackboard.db"
|
||||||
try:
|
try:
|
||||||
conn = sqlite3.connect(str(db_path), timeout=5)
|
conn = sqlite3.connect(str(db_path), timeout=5)
|
||||||
max_id_row = conn.execute(
|
max_id_row = conn.execute(
|
||||||
@@ -240,7 +240,7 @@ async def update_status(project_id: str, task_id: str, body: Dict[str, Any]):
|
|||||||
})
|
})
|
||||||
|
|
||||||
if not bb.update_task_status(task_id, new_status,
|
if not bb.update_task_status(task_id, new_status,
|
||||||
agent=body.get("agent")):
|
agent=body.get("agent")):
|
||||||
raise HTTPException(409, {
|
raise HTTPException(409, {
|
||||||
"error": "transition_failed",
|
"error": "transition_failed",
|
||||||
"detail": f"Status update failed for {task_id}",
|
"detail": f"Status update failed for {task_id}",
|
||||||
@@ -265,7 +265,6 @@ async def update_status(project_id: str, task_id: str, body: Dict[str, Any]):
|
|||||||
# --- @mention 自动提取(#04) ---
|
# --- @mention 自动提取(#04) ---
|
||||||
_KNOWN_AGENT_IDS: list = []
|
_KNOWN_AGENT_IDS: list = []
|
||||||
|
|
||||||
|
|
||||||
def _init_agent_ids():
|
def _init_agent_ids():
|
||||||
"""从配置文件加载 Agent ID 列表"""
|
"""从配置文件加载 Agent ID 列表"""
|
||||||
global _KNOWN_AGENT_IDS
|
global _KNOWN_AGENT_IDS
|
||||||
@@ -280,7 +279,6 @@ def _init_agent_ids():
|
|||||||
except Exception:
|
except Exception:
|
||||||
_KNOWN_AGENT_IDS = []
|
_KNOWN_AGENT_IDS = []
|
||||||
|
|
||||||
|
|
||||||
def _extract_mentions(text: str) -> list:
|
def _extract_mentions(text: str) -> list:
|
||||||
"""从文本中自动提取 @agent-id 格式的 mention"""
|
"""从文本中自动提取 @agent-id 格式的 mention"""
|
||||||
import re
|
import re
|
||||||
@@ -319,8 +317,8 @@ async def add_comment(project_id: str, task_id: str, body: Dict[str, Any]):
|
|||||||
merged_mentions = list(set(explicit_mentions + auto_mentions))
|
merged_mentions = list(set(explicit_mentions + auto_mentions))
|
||||||
|
|
||||||
cid = bb.add_comment(task_id, body["author"], comment_body,
|
cid = bb.add_comment(task_id, body["author"], comment_body,
|
||||||
comment_type=body.get("comment_type", "general"),
|
comment_type=body.get("comment_type", "general"),
|
||||||
mentions=merged_mentions)
|
mentions=merged_mentions)
|
||||||
if merged_mentions:
|
if merged_mentions:
|
||||||
bb.record_mentions(cid, task_id, merged_mentions)
|
bb.record_mentions(cid, task_id, merged_mentions)
|
||||||
# #10: SSE 通知前端黑板有新 comment
|
# #10: SSE 通知前端黑板有新 comment
|
||||||
@@ -426,8 +424,8 @@ async def get_decisions(project_id: str, task_id: str):
|
|||||||
async def add_decision(project_id: str, task_id: str, body: Dict[str, Any]):
|
async def add_decision(project_id: str, task_id: str, body: Dict[str, Any]):
|
||||||
bb = _bb(project_id)
|
bb = _bb(project_id)
|
||||||
did = bb.add_decision(task_id, body["decider"], body["decision"],
|
did = bb.add_decision(task_id, body["decider"], body["decision"],
|
||||||
body["rationale"],
|
body["rationale"],
|
||||||
alternatives=body.get("alternatives"))
|
alternatives=body.get("alternatives"))
|
||||||
return {"ok": True, "decision_id": did}
|
return {"ok": True, "decision_id": did}
|
||||||
|
|
||||||
|
|
||||||
@@ -437,7 +435,7 @@ async def add_decision(project_id: str, task_id: str, body: Dict[str, Any]):
|
|||||||
async def add_observation(project_id: str, task_id: str, body: Dict[str, Any]):
|
async def add_observation(project_id: str, task_id: str, body: Dict[str, Any]):
|
||||||
bb = _bb(project_id)
|
bb = _bb(project_id)
|
||||||
oid = bb.add_observation(task_id, body["observer"], body["body"],
|
oid = bb.add_observation(task_id, body["observer"], body["body"],
|
||||||
severity=body.get("severity", "info"))
|
severity=body.get("severity", "info"))
|
||||||
return {"ok": True, "observation_id": oid}
|
return {"ok": True, "observation_id": oid}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from pydantic import BaseModel
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from src.blackboard.operations import Blackboard
|
from src.blackboard.operations import Blackboard
|
||||||
import src.utils as _utils
|
from src.utils import get_data_root
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/projects/{project_id}/tasks/{task_id}/checkpoints", tags=["checkpoints"])
|
router = APIRouter(prefix="/api/projects/{project_id}/tasks/{task_id}/checkpoints", tags=["checkpoints"])
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ class ResolveCheckpointRequest(BaseModel):
|
|||||||
# ── 工具 ──
|
# ── 工具 ──
|
||||||
|
|
||||||
def _bb(project_id: str) -> Blackboard:
|
def _bb(project_id: str) -> Blackboard:
|
||||||
db_path = _utils.get_data_root() / project_id / "blackboard.db"
|
db_path = get_data_root() / project_id / "blackboard.db"
|
||||||
if not db_path.exists():
|
if not db_path.exists():
|
||||||
raise HTTPException(status_code=404, detail="Project not found")
|
raise HTTPException(status_code=404, detail="Project not found")
|
||||||
return Blackboard(db_path)
|
return Blackboard(db_path)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Query
|
from fastapi import APIRouter, HTTPException, Query
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ from src.blackboard.db import init_db
|
|||||||
from src.blackboard.models import Task
|
from src.blackboard.models import Task
|
||||||
from src.blackboard.operations import Blackboard
|
from src.blackboard.operations import Blackboard
|
||||||
from src.blackboard.queries import Queries
|
from src.blackboard.queries import Queries
|
||||||
import src.utils as _utils
|
from src.utils import get_data_root
|
||||||
|
|
||||||
|
|
||||||
def _get_valid_agents() -> set:
|
def _get_valid_agents() -> set:
|
||||||
@@ -36,14 +36,13 @@ def _get_valid_agents() -> set:
|
|||||||
# fallback:硬编码
|
# fallback:硬编码
|
||||||
return {"zhangfei-dev", "guanyu-dev", "zhaoyun-data", "jiangwei-infra", "pangtong-fujunshi", "simayi-challenger"}
|
return {"zhangfei-dev", "guanyu-dev", "zhaoyun-data", "jiangwei-infra", "pangtong-fujunshi", "simayi-challenger"}
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/mail", tags=["mail"])
|
router = APIRouter(prefix="/api/mail", tags=["mail"])
|
||||||
|
|
||||||
MAIL_PROJECT_ID = "_mail"
|
MAIL_PROJECT_ID = "_mail"
|
||||||
|
|
||||||
|
|
||||||
def _db_path() -> Path:
|
def _db_path() -> Path:
|
||||||
root = _utils.get_data_root()
|
root = get_data_root()
|
||||||
db = root / MAIL_PROJECT_ID / "blackboard.db"
|
db = root / MAIL_PROJECT_ID / "blackboard.db"
|
||||||
db.parent.mkdir(parents=True, exist_ok=True)
|
db.parent.mkdir(parents=True, exist_ok=True)
|
||||||
init_db(db)
|
init_db(db)
|
||||||
@@ -223,7 +222,7 @@ async def send_mail(body: Dict[str, Any]):
|
|||||||
|
|
||||||
# A8: 只有原邮件的双方能回复(严格 1 对 1)
|
# A8: 只有原邮件的双方能回复(严格 1 对 1)
|
||||||
if from_agent not in (orig_from, orig_to):
|
if from_agent not in (orig_from, orig_to):
|
||||||
raise HTTPException(400, "只有邮件的发送者或接收者可以回复")
|
raise HTTPException(400, f"只有邮件的发送者或接收者可以回复")
|
||||||
|
|
||||||
# A6/A7: 自动纠正 to → 原邮件发件者
|
# A6/A7: 自动纠正 to → 原邮件发件者
|
||||||
to_agent = body.get("to", "").strip()
|
to_agent = body.get("to", "").strip()
|
||||||
|
|||||||
@@ -3,18 +3,18 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Query
|
from fastapi import APIRouter, HTTPException, Query
|
||||||
|
|
||||||
from src.blackboard.registry import ProjectRegistry
|
from src.blackboard.registry import ProjectRegistry
|
||||||
import src.utils as _utils
|
from src.utils import get_data_root
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/projects", tags=["projects"])
|
router = APIRouter(prefix="/api/projects", tags=["projects"])
|
||||||
|
|
||||||
|
|
||||||
def _registry() -> ProjectRegistry:
|
def _registry() -> ProjectRegistry:
|
||||||
return ProjectRegistry(_utils.get_data_root())
|
return ProjectRegistry(get_data_root())
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
@@ -76,7 +76,7 @@ async def list_projects():
|
|||||||
async def create_project(body: Dict[str, Any]):
|
async def create_project(body: Dict[str, Any]):
|
||||||
reg = _registry()
|
reg = _registry()
|
||||||
try:
|
try:
|
||||||
reg.create_project(
|
info = reg.create_project(
|
||||||
body["id"], body["name"],
|
body["id"], body["name"],
|
||||||
agents=body.get("agents", []),
|
agents=body.get("agents", []),
|
||||||
description=body.get("description", ""),
|
description=body.get("description", ""),
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from src.blackboard.models import Task
|
|||||||
from src.blackboard.operations import Blackboard
|
from src.blackboard.operations import Blackboard
|
||||||
from src.config.agents import AGENT_IDS
|
from src.config.agents import AGENT_IDS
|
||||||
from src.daemon.toolchain_templates import render_template
|
from src.daemon.toolchain_templates import render_template
|
||||||
import src.utils as _utils
|
from src.utils import get_data_root
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -166,12 +166,13 @@ def _calc_risk_level(changed_files: List[str]) -> str:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
MAIL_PROJECT_ID = "_mail"
|
MAIL_PROJECT_ID = "_mail"
|
||||||
|
|
||||||
|
|
||||||
def _mail_db_path() -> Path:
|
def _mail_db_path() -> Path:
|
||||||
"""获取 Mail 数据库路径,确保目录存在。"""
|
"""获取 Mail 数据库路径,确保目录存在。"""
|
||||||
root = _utils.get_data_root()
|
root = get_data_root()
|
||||||
db = root / MAIL_PROJECT_ID / "blackboard.db"
|
db = root / MAIL_PROJECT_ID / "blackboard.db"
|
||||||
db.parent.mkdir(parents=True, exist_ok=True)
|
db.parent.mkdir(parents=True, exist_ok=True)
|
||||||
init_db(db)
|
init_db(db)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
def init_db(db_path: Path) -> None:
|
def init_db(db_path: Path) -> None:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from typing import Any, Dict, List, Optional
|
|||||||
|
|
||||||
from .db import (
|
from .db import (
|
||||||
VALID_TRANSITIONS,
|
VALID_TRANSITIONS,
|
||||||
|
VALID_STATUSES,
|
||||||
COMMENT_TYPES,
|
COMMENT_TYPES,
|
||||||
EVENT_TYPES,
|
EVENT_TYPES,
|
||||||
OUTPUT_TYPES,
|
OUTPUT_TYPES,
|
||||||
@@ -692,6 +693,7 @@ class Blackboard:
|
|||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
# ── Checkpoint CRUD(M3) ──
|
# ── Checkpoint CRUD(M3) ──
|
||||||
|
|
||||||
def create_checkpoint(
|
def create_checkpoint(
|
||||||
|
|||||||
@@ -355,3 +355,4 @@ class ProjectRegistry:
|
|||||||
|
|
||||||
def reload(self) -> None:
|
def reload(self) -> None:
|
||||||
"""兼容旧接口(SQLite 不需要 reload cache)"""
|
"""兼容旧接口(SQLite 不需要 reload cache)"""
|
||||||
|
pass
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ from pathlib import Path
|
|||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from src.blackboard.operations import Blackboard
|
from src.blackboard.operations import Blackboard
|
||||||
import src.utils as _utils
|
from src.utils import get_data_root
|
||||||
from src.blackboard.models import Task, Review
|
from src.blackboard.models import Task, Comment, Output, Decision, Observation, Review, Experience
|
||||||
from src.blackboard.queries import Queries
|
from src.blackboard.queries import Queries
|
||||||
from src.blackboard.registry import ProjectRegistry
|
from src.blackboard.registry import ProjectRegistry
|
||||||
|
|
||||||
|
|
||||||
def _find_project_root() -> Path:
|
def _find_project_root() -> Path:
|
||||||
return _utils.get_data_root()
|
return get_data_root()
|
||||||
|
|
||||||
|
|
||||||
def _get_bb(project_id: str) -> Blackboard:
|
def _get_bb(project_id: str) -> Blackboard:
|
||||||
@@ -262,7 +262,7 @@ def build_admin_parser() -> argparse.ArgumentParser:
|
|||||||
p_pc.add_argument("--description", default="")
|
p_pc.add_argument("--description", default="")
|
||||||
|
|
||||||
# project list
|
# project list
|
||||||
sub.add_parser("project-list", help="List projects")
|
p_pl = sub.add_parser("project-list", help="List projects")
|
||||||
|
|
||||||
# project archive
|
# project archive
|
||||||
p_pa = sub.add_parser("project-archive", help="Archive project")
|
p_pa = sub.add_parser("project-archive", help="Archive project")
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ A 类 Skill 由引擎确定性注入全文,不靠 Description 触发。
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import Any, List
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
logger = logging.getLogger("moziplus-v2.bootstrap")
|
logger = logging.getLogger("moziplus-v2.bootstrap")
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class ActiveAgentCounter:
|
|||||||
cd = seconds if seconds is not None else self._default_cooldown_seconds
|
cd = seconds if seconds is not None else self._default_cooldown_seconds
|
||||||
self._cooldown_until[agent_id] = time.time() + cd
|
self._cooldown_until[agent_id] = time.time() + cd
|
||||||
logger.info("Cooldown set for %s: %.0fs (until %.0f)",
|
logger.info("Cooldown set for %s: %.0fs (until %.0f)",
|
||||||
agent_id, cd, self._cooldown_until[agent_id])
|
agent_id, cd, self._cooldown_until[agent_id])
|
||||||
|
|
||||||
async def can_acquire(self, agent_id: str, session_id: str = "main") -> bool:
|
async def can_acquire(self, agent_id: str, session_id: str = "main") -> bool:
|
||||||
"""三层检查:cooldown → global → per agent → per session key"""
|
"""三层检查:cooldown → global → per agent → per session key"""
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
@@ -21,7 +22,7 @@ from typing import Any, Dict, List, Optional
|
|||||||
from src.blackboard.models import Task
|
from src.blackboard.models import Task
|
||||||
from src.blackboard.db import get_connection
|
from src.blackboard.db import get_connection
|
||||||
from src.daemon.spawner import AgentBusyError
|
from src.daemon.spawner import AgentBusyError
|
||||||
from src.daemon.router import AgentRouter
|
from src.daemon.router import AgentRouter, RouteDecision
|
||||||
|
|
||||||
logger = logging.getLogger("moziplus-v2.dispatcher")
|
logger = logging.getLogger("moziplus-v2.dispatcher")
|
||||||
|
|
||||||
@@ -193,7 +194,6 @@ class Dispatcher:
|
|||||||
_task_id = task.id
|
_task_id = task.id
|
||||||
_mail_db = db_path
|
_mail_db = db_path
|
||||||
_disp = self
|
_disp = self
|
||||||
|
|
||||||
def _mail_on_checks_passed():
|
def _mail_on_checks_passed():
|
||||||
nonlocal _mail_marked_working
|
nonlocal _mail_marked_working
|
||||||
if not _disp._mail_auto_working(_task_id, _mail_db):
|
if not _disp._mail_auto_working(_task_id, _mail_db):
|
||||||
@@ -203,8 +203,8 @@ class Dispatcher:
|
|||||||
|
|
||||||
# 构建 spawn message
|
# 构建 spawn message
|
||||||
message = self._build_spawn_message(task, agent_id, project_config,
|
message = self._build_spawn_message(task, agent_id, project_config,
|
||||||
mode=decision.get("mode", ""),
|
mode=decision.get("mode", ""),
|
||||||
spawn_type=action_type or "executor")
|
spawn_type=action_type or "executor")
|
||||||
|
|
||||||
# v2.7.2: on_complete 只含业务逻辑,不含 counter.release
|
# v2.7.2: on_complete 只含业务逻辑,不含 counter.release
|
||||||
# counter.release 由 spawn_full_agent 内部的 wrapped_on_complete 保证
|
# counter.release 由 spawn_full_agent 内部的 wrapped_on_complete 保证
|
||||||
@@ -269,8 +269,8 @@ class Dispatcher:
|
|||||||
from src.blackboard.blackboard import Blackboard
|
from src.blackboard.blackboard import Blackboard
|
||||||
bb = Blackboard(_task_db)
|
bb = Blackboard(_task_db)
|
||||||
bb.add_comment(_task_id, "daemon",
|
bb.add_comment(_task_id, "daemon",
|
||||||
f"@{task_row['assignee']} 审查结论: {verdict_str},请查看详情并决定接受或反驳",
|
f"@{task_row['assignee']} 审查结论: {verdict_str},请查看详情并决定接受或反驳",
|
||||||
comment_type="review")
|
comment_type="review")
|
||||||
logger.info("Task %s: review verdict=%s, notified assignee=%s",
|
logger.info("Task %s: review verdict=%s, notified assignee=%s",
|
||||||
_task_id, verdict_str, task_row["assignee"] if task_row else "?")
|
_task_id, verdict_str, task_row["assignee"] if task_row else "?")
|
||||||
# 不标 done,保持 review 状态
|
# 不标 done,保持 review 状态
|
||||||
@@ -661,7 +661,7 @@ class Dispatcher:
|
|||||||
logger.error("Mail %s: failed to revert to pending: %s", task_id, e)
|
logger.error("Mail %s: failed to revert to pending: %s", task_id, e)
|
||||||
|
|
||||||
def _mail_auto_complete(self, task_id: str, agent_id: str,
|
def _mail_auto_complete(self, task_id: str, agent_id: str,
|
||||||
db_path: Path, must_haves: str) -> None:
|
db_path: Path, must_haves: str, outcome=None) -> None:
|
||||||
"""Mail 任务:on_complete 后自动标 done/failed(含幻觉门控)"""
|
"""Mail 任务:on_complete 后自动标 done/failed(含幻觉门控)"""
|
||||||
try:
|
try:
|
||||||
# 解析 performative
|
# 解析 performative
|
||||||
@@ -866,7 +866,7 @@ class Dispatcher:
|
|||||||
logger.error("Task %s: mark status error: %s", task_id, e)
|
logger.error("Task %s: mark status error: %s", task_id, e)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _check_crash_limit(task_id: str, db_path: Path, limit: int = 3,
|
def _check_crash_limit(task_id: str, db_path: pathlib.Path, limit: int = 3,
|
||||||
window_minutes: int = 30) -> bool:
|
window_minutes: int = 30) -> bool:
|
||||||
"""v2.8.1 Fix-3c: 检查 task 最近 window_minutes 内的 crash 次数是否超限。
|
"""v2.8.1 Fix-3c: 检查 task 最近 window_minutes 内的 crash 次数是否超限。
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import logging
|
|||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
logger = logging.getLogger("moziplus-v2.experience")
|
logger = logging.getLogger("moziplus-v2.experience")
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ class Experience:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, data: Dict[str, Any]) -> Experience:
|
def from_dict(cls, data: Dict[str, Any]) -> Experience:
|
||||||
return cls(**{k: v for k, v in data.items() if k != "id"},
|
return cls(**{k: v for k, v in data.items() if k != "id"},
|
||||||
experience_id=data.get("id"))
|
experience_id=data.get("id"))
|
||||||
|
|
||||||
|
|
||||||
class ExperienceStore:
|
class ExperienceStore:
|
||||||
@@ -284,7 +284,7 @@ class ExperienceDistiller:
|
|||||||
all_tags.append(task_type)
|
all_tags.append(task_type)
|
||||||
|
|
||||||
results = self.store.search(tags=all_tags if all_tags else None,
|
results = self.store.search(tags=all_tags if all_tags else None,
|
||||||
query=query, limit=limit)
|
query=query, limit=limit)
|
||||||
|
|
||||||
# 按置信度排序
|
# 按置信度排序
|
||||||
results.sort(key=lambda e: e.confidence, reverse=True)
|
results.sort(key=lambda e: e.confidence, reverse=True)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from src.blackboard.db import get_connection
|
from src.blackboard.db import get_connection, init_db
|
||||||
from src.blackboard.queries import Queries
|
from src.blackboard.queries import Queries
|
||||||
|
|
||||||
logger = logging.getLogger("moziplus-v2.health")
|
logger = logging.getLogger("moziplus-v2.health")
|
||||||
@@ -41,6 +41,7 @@ class HealthChecker:
|
|||||||
{"healthy": bool, "zombie": bool, "stale_ticks": int,
|
{"healthy": bool, "zombie": bool, "stale_ticks": int,
|
||||||
"alert_written": bool, "resolved": bool}
|
"alert_written": bool, "resolved": bool}
|
||||||
"""
|
"""
|
||||||
|
db_key = str(db_path)
|
||||||
result: Dict[str, Any] = {
|
result: Dict[str, Any] = {
|
||||||
"healthy": True,
|
"healthy": True,
|
||||||
"zombie": False,
|
"zombie": False,
|
||||||
@@ -57,6 +58,7 @@ class HealthChecker:
|
|||||||
# 用 event count 变化判断是否有真实变更
|
# 用 event count 变化判断是否有真实变更
|
||||||
conn = queries._conn()
|
conn = queries._conn()
|
||||||
try:
|
try:
|
||||||
|
total_events = conn.execute("SELECT COUNT(*) FROM events").fetchone()[0]
|
||||||
non_tick_events = conn.execute(
|
non_tick_events = conn.execute(
|
||||||
"SELECT COUNT(*) FROM events WHERE event_type != 'daemon_tick' "
|
"SELECT COUNT(*) FROM events WHERE event_type != 'daemon_tick' "
|
||||||
"AND event_type != 'agent_zombie_detected'"
|
"AND event_type != 'agent_zombie_detected'"
|
||||||
|
|||||||
+3
-2
@@ -15,6 +15,7 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Coroutine, Dict, List, Optional
|
from typing import Any, Callable, Coroutine, Dict, List, Optional
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ class InboxWatcher:
|
|||||||
self._running = True
|
self._running = True
|
||||||
self._task = asyncio.create_task(self._loop())
|
self._task = asyncio.create_task(self._loop())
|
||||||
logger.info("Inbox watcher started (path=%s, interval=%.1fs)",
|
logger.info("Inbox watcher started (path=%s, interval=%.1fs)",
|
||||||
self.inbox_path, self.watch_interval)
|
self.inbox_path, self.watch_interval)
|
||||||
|
|
||||||
async def stop(self) -> None:
|
async def stop(self) -> None:
|
||||||
"""停止监听"""
|
"""停止监听"""
|
||||||
@@ -68,7 +69,7 @@ class InboxWatcher:
|
|||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
logger.info("Inbox watcher stopped (processed=%d, errors=%d)",
|
logger.info("Inbox watcher stopped (processed=%d, errors=%d)",
|
||||||
self._total_processed, self._total_errors)
|
self._total_processed, self._total_errors)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_running(self) -> bool:
|
def is_running(self) -> bool:
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ def notify_mail_failed(db_path: Path, original_mail_id: str,
|
|||||||
)
|
)
|
||||||
bb.create_task(notify_task)
|
bb.create_task(notify_task)
|
||||||
logger.info("Mail %s: sent failure notification to %s (original_sender=%s, reason=%s, notify_id=%s)",
|
logger.info("Mail %s: sent failure notification to %s (original_sender=%s, reason=%s, notify_id=%s)",
|
||||||
original_mail_id, target_agent, from_agent, reason, notify_id)
|
original_mail_id, target_agent, from_agent, reason, notify_id)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("notify_mail_failed: failed to send notification for mail %s: %s", original_mail_id, e)
|
logger.warning("notify_mail_failed: failed to send notification for mail %s: %s", original_mail_id, e)
|
||||||
|
|||||||
@@ -8,12 +8,15 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Dict, List, Optional
|
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from src.blackboard.models import Task
|
from src.blackboard.models import Task
|
||||||
from src.blackboard.operations import Blackboard
|
from src.blackboard.operations import Blackboard
|
||||||
|
from src.blackboard.queries import Queries
|
||||||
|
|
||||||
logger = logging.getLogger("moziplus-v2.review")
|
logger = logging.getLogger("moziplus-v2.review")
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
logger = logging.getLogger("moziplus-v2.skill")
|
logger = logging.getLogger("moziplus-v2.skill")
|
||||||
|
|
||||||
|
|||||||
@@ -1373,13 +1373,11 @@ curl -X POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_
|
|||||||
# A17: 真正的 crash → 保持 working,ticker 兜底
|
# A17: 真正的 crash → 保持 working,ticker 兜底
|
||||||
return {"outcome": "crashed", "should_retry": False, "original": "process_crash"}
|
return {"outcome": "crashed", "should_retry": False, "original": "process_crash"}
|
||||||
|
|
||||||
# stdout 为空但 exit=0:可能是正常完成但 --json 没输出
|
# A13 revised: stdout 为空但 exit=0 → 信任进程退出码,视为正常完成
|
||||||
# 查任务状态判断
|
# 实测发现 openclaw session=None + exit=0 是正常场景(inform 通知等)
|
||||||
|
# 旧逻辑按 task_status 区分,非终态判 agent_error → 导致 inform 邮件永不标 done
|
||||||
if status is None and not stdout_text.strip() and exit_code == 0:
|
if status is None and not stdout_text.strip() and exit_code == 0:
|
||||||
terminal_statuses = {"done", "review"}
|
return {"outcome": "completed", "should_retry": False}
|
||||||
if task_status in terminal_statuses:
|
|
||||||
return {"outcome": "completed", "should_retry": False}
|
|
||||||
return {"outcome": "agent_error", "should_retry": False}
|
|
||||||
|
|
||||||
# A7-A12: status=error → 不续杯,stderr 辅助分类
|
# A7-A12: status=error → 不续杯,stderr 辅助分类
|
||||||
if status == "error":
|
if status == "error":
|
||||||
|
|||||||
+4
-1
@@ -9,11 +9,14 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import subprocess
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Dict, List, Optional
|
from pathlib import Path
|
||||||
|
from typing import Any, Callable, Dict, List, Optional, Set
|
||||||
|
|
||||||
|
from src.blackboard.models import Event
|
||||||
|
|
||||||
logger = logging.getLogger("moziplus-v2.sse")
|
logger = logging.getLogger("moziplus-v2.sse")
|
||||||
|
|
||||||
|
|||||||
+26
-25
@@ -21,6 +21,7 @@ from dataclasses import dataclass, field as dc_field
|
|||||||
|
|
||||||
from src.blackboard.operations import Blackboard
|
from src.blackboard.operations import Blackboard
|
||||||
from src.blackboard.db import get_connection
|
from src.blackboard.db import get_connection
|
||||||
|
from src.blackboard.models import Task
|
||||||
from src.daemon.spawner import AgentBusyError
|
from src.daemon.spawner import AgentBusyError
|
||||||
from src.blackboard.queries import Queries
|
from src.blackboard.queries import Queries
|
||||||
from src.blackboard.registry import ProjectRegistry
|
from src.blackboard.registry import ProjectRegistry
|
||||||
@@ -34,7 +35,6 @@ class BroadcastRound:
|
|||||||
responded_agents: set = dc_field(default_factory=set) # 已返回反馈的 Agent(含 NO_REPLY)
|
responded_agents: set = dc_field(default_factory=set) # 已返回反馈的 Agent(含 NO_REPLY)
|
||||||
round_number: int = 0 # 当前第几轮(0=未开始,1=第1轮)
|
round_number: int = 0 # 当前第几轮(0=未开始,1=第1轮)
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("moziplus-v2.ticker")
|
logger = logging.getLogger("moziplus-v2.ticker")
|
||||||
|
|
||||||
|
|
||||||
@@ -391,7 +391,7 @@ class Ticker:
|
|||||||
MAX_ROUNDS = 5 # §4.5 防无限循环
|
MAX_ROUNDS = 5 # §4.5 防无限循环
|
||||||
|
|
||||||
async def _check_round_complete(self, db_path: Path,
|
async def _check_round_complete(self, db_path: Path,
|
||||||
project_id: str) -> List[str]:
|
project_id: str) -> List[str]:
|
||||||
"""检测 parent task 下所有 sub task 终态 → spawn 庞统 review
|
"""检测 parent task 下所有 sub task 终态 → spawn 庞统 review
|
||||||
|
|
||||||
流程(§4.4):
|
流程(§4.4):
|
||||||
@@ -462,7 +462,7 @@ class Ticker:
|
|||||||
"Round %d review spawned for parent %s (subs: %s)",
|
"Round %d review spawned for parent %s (subs: %s)",
|
||||||
new_round, parent_id, summary
|
new_round, parent_id, summary
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
logger.exception("Round check error for parent %s", parent_id)
|
logger.exception("Round check error for parent %s", parent_id)
|
||||||
|
|
||||||
return reviewed
|
return reviewed
|
||||||
@@ -531,9 +531,9 @@ Parent Task ID: {parent_task.id}
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
async def _spawn_pangtong_review(self, parent_task,
|
async def _spawn_pangtong_review(self, parent_task,
|
||||||
review_prompt: str,
|
review_prompt: str,
|
||||||
project_id: str,
|
project_id: str,
|
||||||
new_round: int = 0) -> bool:
|
new_round: int = 0) -> bool:
|
||||||
"""Spawn 庞统进行 review
|
"""Spawn 庞统进行 review
|
||||||
|
|
||||||
流程:
|
流程:
|
||||||
@@ -543,6 +543,7 @@ Parent Task ID: {parent_task.id}
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
agent_id = "pangtong-fujunshi"
|
agent_id = "pangtong-fujunshi"
|
||||||
|
session_id = f"review-{parent_task.id}-r{new_round}"
|
||||||
|
|
||||||
# 构造 on_complete 回调:解析庞统结论,更新 parent 状态
|
# 构造 on_complete 回调:解析庞统结论,更新 parent 状态
|
||||||
async def _on_review_complete(aid: str, outcome: str):
|
async def _on_review_complete(aid: str, outcome: str):
|
||||||
@@ -585,7 +586,7 @@ Parent Task ID: {parent_task.id}
|
|||||||
self._set_parent_reviewing(parent_task.id, project_id)
|
self._set_parent_reviewing(parent_task.id, project_id)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
except Exception:
|
except Exception as e:
|
||||||
logger.exception("Failed to spawn pangtong review for %s", parent_task.id)
|
logger.exception("Failed to spawn pangtong review for %s", parent_task.id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -602,14 +603,14 @@ Parent Task ID: {parent_task.id}
|
|||||||
(parent_id,))
|
(parent_id,))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
logger.info("Parent %s → reviewing (round review in progress)",
|
logger.info("Parent %s → reviewing (round review in progress)",
|
||||||
parent_id)
|
parent_id)
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to set parent %s to reviewing", parent_id)
|
logger.exception("Failed to set parent %s to reviewing", parent_id)
|
||||||
|
|
||||||
def _handle_review_conclusion(self, parent_id: str, project_id: str,
|
def _handle_review_conclusion(self, parent_id: str, project_id: str,
|
||||||
review_text: str, round_num: int):
|
review_text: str, round_num: int):
|
||||||
"""解析庞统 review 结论,更新 parent 状态
|
"""解析庞统 review 结论,更新 parent 状态
|
||||||
|
|
||||||
review_text 是庞统回复的文本(从 spawner session meta payloads 拼接)。
|
review_text 是庞统回复的文本(从 spawner session meta payloads 拼接)。
|
||||||
@@ -664,8 +665,8 @@ Parent Task ID: {parent_task.id}
|
|||||||
|
|
||||||
def _resolve_db_path(self, project_id: str) -> Path:
|
def _resolve_db_path(self, project_id: str) -> Path:
|
||||||
"""解析项目 DB 路径"""
|
"""解析项目 DB 路径"""
|
||||||
import src.utils as _utils
|
from src.utils import get_data_root
|
||||||
return _utils.get_data_root() / project_id / "blackboard.db"
|
return get_data_root() / project_id / "blackboard.db"
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# @mention 通知处理 (v2.9 #01)
|
# @mention 通知处理 (v2.9 #01)
|
||||||
@@ -674,7 +675,7 @@ Parent Task ID: {parent_task.id}
|
|||||||
MENTION_MAX_RETRIES = 5
|
MENTION_MAX_RETRIES = 5
|
||||||
|
|
||||||
async def _process_mentions(self, db_path: Path,
|
async def _process_mentions(self, db_path: Path,
|
||||||
project_id: str) -> List[str]:
|
project_id: str) -> List[str]:
|
||||||
"""扫描 pending mentions → spawn 被 @ 的 Agent
|
"""扫描 pending mentions → spawn 被 @ 的 Agent
|
||||||
|
|
||||||
流程(§3.4):
|
流程(§3.4):
|
||||||
@@ -766,8 +767,8 @@ Parent Task ID: {parent_task.id}
|
|||||||
from src.blackboard.blackboard import Blackboard
|
from src.blackboard.blackboard import Blackboard
|
||||||
bb2 = Blackboard(rdb_path)
|
bb2 = Blackboard(rdb_path)
|
||||||
bb2.add_comment(_t_id, "daemon",
|
bb2.add_comment(_t_id, "daemon",
|
||||||
f"@{t_row['assignee']} 审查结论: {verdict_str},请查看详情并决定接受或反驳",
|
f"@{t_row['assignee']} 审查结论: {verdict_str},请查看详情并决定接受或反驳",
|
||||||
comment_type="review")
|
comment_type="review")
|
||||||
logger.info("Rebuttal: task %s still %s after rebuttal", _t_id, verdict_str)
|
logger.info("Rebuttal: task %s still %s after rebuttal", _t_id, verdict_str)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Rebuttal on_complete failed for task %s", _t_id)
|
logger.exception("Rebuttal on_complete failed for task %s", _t_id)
|
||||||
@@ -804,7 +805,7 @@ Parent Task ID: {parent_task.id}
|
|||||||
# Agent 忙,不递增 retry_count,等下次 tick 自然重试
|
# Agent 忙,不递增 retry_count,等下次 tick 自然重试
|
||||||
logger.info("Mention spawn skipped: %s busy, will retry next tick", agent_id)
|
logger.info("Mention spawn skipped: %s busy, will retry next tick", agent_id)
|
||||||
|
|
||||||
except Exception:
|
except Exception as e:
|
||||||
logger.exception("Mention processing error for agent %s", agent_id)
|
logger.exception("Mention processing error for agent %s", agent_id)
|
||||||
for item in items:
|
for item in items:
|
||||||
try:
|
try:
|
||||||
@@ -947,7 +948,7 @@ Parent Task ID: {parent_task.id}
|
|||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
async def _dispatch_pending(self, db_path: Path,
|
async def _dispatch_pending(self, db_path: Path,
|
||||||
project_id: str) -> List[str]:
|
project_id: str) -> List[str]:
|
||||||
"""扫描 pending 任务并调度
|
"""扫描 pending 任务并调度
|
||||||
|
|
||||||
v3.0: 两条路径
|
v3.0: 两条路径
|
||||||
@@ -1241,7 +1242,7 @@ Parent Task ID: {parent_task.id}
|
|||||||
return [aid for aid in all_agents if active.get(aid, 0) == 0]
|
return [aid for aid in all_agents if active.get(aid, 0) == 0]
|
||||||
|
|
||||||
async def _dispatch_reviews(self, db_path: Path,
|
async def _dispatch_reviews(self, db_path: Path,
|
||||||
project_id: str) -> List[str]:
|
project_id: str) -> List[str]:
|
||||||
"""扫描 review 状态任务,检查是否有产出,调度审查 Agent"""
|
"""扫描 review 状态任务,检查是否有产出,调度审查 Agent"""
|
||||||
# mail 任务不走 review 流程,直接跳过
|
# mail 任务不走 review 流程,直接跳过
|
||||||
if project_id == "_mail":
|
if project_id == "_mail":
|
||||||
@@ -1343,7 +1344,7 @@ Parent Task ID: {parent_task.id}
|
|||||||
)
|
)
|
||||||
reclaimed.append(task.id)
|
reclaimed.append(task.id)
|
||||||
logger.warning("Escalated %s: no taker after %d broadcasts",
|
logger.warning("Escalated %s: no taker after %d broadcasts",
|
||||||
task.id, retry_count)
|
task.id, retry_count)
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
else:
|
else:
|
||||||
@@ -1422,7 +1423,7 @@ Parent Task ID: {parent_task.id}
|
|||||||
if ok:
|
if ok:
|
||||||
reclaimed.append(task.id)
|
reclaimed.append(task.id)
|
||||||
logger.info("Mail %s: ticker recheck found reply, marked done (%.1fm)",
|
logger.info("Mail %s: ticker recheck found reply, marked done (%.1fm)",
|
||||||
task.id, elapsed)
|
task.id, elapsed)
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
continue
|
continue
|
||||||
@@ -1439,7 +1440,7 @@ Parent Task ID: {parent_task.id}
|
|||||||
if ok:
|
if ok:
|
||||||
reclaimed.append(task.id)
|
reclaimed.append(task.id)
|
||||||
logger.warning("Task %s timed out (working %.1fm > %.1fm)",
|
logger.warning("Task %s timed out (working %.1fm > %.1fm)",
|
||||||
task.id, elapsed, timeout_minutes)
|
task.id, elapsed, timeout_minutes)
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
@@ -1500,7 +1501,7 @@ Parent Task ID: {parent_task.id}
|
|||||||
return True # 保守:查询失败假设有回复
|
return True # 保守:查询失败假设有回复
|
||||||
|
|
||||||
def _check_recent_routing(self, db_path: Path, task_id: str,
|
def _check_recent_routing(self, db_path: Path, task_id: str,
|
||||||
action_type: str) -> bool:
|
action_type: str) -> bool:
|
||||||
"""检查最近 5 分钟内是否已 dispatch 过指定类型的路由(防重复)"""
|
"""检查最近 5 分钟内是否已 dispatch 过指定类型的路由(防重复)"""
|
||||||
try:
|
try:
|
||||||
conn = get_connection(db_path)
|
conn = get_connection(db_path)
|
||||||
@@ -1578,11 +1579,11 @@ Parent Task ID: {parent_task.id}
|
|||||||
|
|
||||||
if recovery_report["total_recovered"] > 0:
|
if recovery_report["total_recovered"] > 0:
|
||||||
logger.info("Startup recovery: %d tasks recovered across %d projects",
|
logger.info("Startup recovery: %d tasks recovered across %d projects",
|
||||||
recovery_report["total_recovered"],
|
recovery_report["total_recovered"],
|
||||||
len(recovery_report["projects"]))
|
len(recovery_report["projects"]))
|
||||||
elif recovery_report["total_noop"] > 0:
|
elif recovery_report["total_noop"] > 0:
|
||||||
logger.info("Startup recovery: %d tasks kept as-is (no recovery needed)",
|
logger.info("Startup recovery: %d tasks kept as-is (no recovery needed)",
|
||||||
recovery_report["total_noop"])
|
recovery_report["total_noop"])
|
||||||
else:
|
else:
|
||||||
logger.info("Startup recovery: no non-terminal tasks found, clean start")
|
logger.info("Startup recovery: no non-terminal tasks found, clean start")
|
||||||
|
|
||||||
@@ -1628,7 +1629,7 @@ Parent Task ID: {parent_task.id}
|
|||||||
return recovered, noop_count
|
return recovered, noop_count
|
||||||
|
|
||||||
def _determine_recovery_action(self, conn, task, status: str,
|
def _determine_recovery_action(self, conn, task, status: str,
|
||||||
db_path: Path) -> Optional[str]:
|
db_path: Path) -> Optional[str]:
|
||||||
"""根据黑板线索决定恢复动作,返回 None 表示不需要干预"""
|
"""根据黑板线索决定恢复动作,返回 None 表示不需要干预"""
|
||||||
task_id = task["id"]
|
task_id = task["id"]
|
||||||
|
|
||||||
|
|||||||
+12
-12
@@ -23,15 +23,7 @@ from src.daemon.health import HealthChecker
|
|||||||
from src.daemon.experience import ExperienceDistiller, ExperienceStore
|
from src.daemon.experience import ExperienceDistiller, ExperienceStore
|
||||||
from src.daemon.inbox import InboxWatcher
|
from src.daemon.inbox import InboxWatcher
|
||||||
from src.daemon.guardrails import GuardrailEngine
|
from src.daemon.guardrails import GuardrailEngine
|
||||||
import src.utils as _utils
|
from src.utils import get_data_root
|
||||||
|
|
||||||
from src.api.blackboard_routes import router as blackboard_router
|
|
||||||
from src.api.checkpoint_routes import router as checkpoint_router
|
|
||||||
from src.api.daemon_routes import router as daemon_router
|
|
||||||
from src.api.project_routes import router as project_router
|
|
||||||
from src.api.sse_routes import router as sse_router
|
|
||||||
from src.api.mail_routes import router as mail_router
|
|
||||||
from src.api.toolchain_routes import router as toolchain_router
|
|
||||||
|
|
||||||
logger = logging.getLogger("moziplus-v2")
|
logger = logging.getLogger("moziplus-v2")
|
||||||
|
|
||||||
@@ -86,7 +78,7 @@ config = load_config()
|
|||||||
# 全局组件
|
# 全局组件
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
DATA_ROOT = _utils.get_data_root()
|
DATA_ROOT = get_data_root()
|
||||||
|
|
||||||
ticker: Optional[Ticker] = None
|
ticker: Optional[Ticker] = None
|
||||||
|
|
||||||
@@ -199,6 +191,7 @@ async def lifespan(app: FastAPI):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# ExperienceDistiller(经验自动蒸馏)
|
# ExperienceDistiller(经验自动蒸馏)
|
||||||
|
experience_config = config.get("experience", {})
|
||||||
experience_distiller = ExperienceDistiller(
|
experience_distiller = ExperienceDistiller(
|
||||||
store=ExperienceStore(store_path=DATA_ROOT / "experiences.jsonl"),
|
store=ExperienceStore(store_path=DATA_ROOT / "experiences.jsonl"),
|
||||||
)
|
)
|
||||||
@@ -259,6 +252,14 @@ app.add_middleware(
|
|||||||
# API 路由注册
|
# API 路由注册
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
from src.api.blackboard_routes import router as blackboard_router
|
||||||
|
from src.api.checkpoint_routes import router as checkpoint_router
|
||||||
|
from src.api.daemon_routes import router as daemon_router
|
||||||
|
from src.api.project_routes import router as project_router
|
||||||
|
from src.api.sse_routes import router as sse_router
|
||||||
|
from src.api.mail_routes import router as mail_router
|
||||||
|
from src.api.toolchain_routes import router as toolchain_router
|
||||||
|
|
||||||
app.include_router(blackboard_router)
|
app.include_router(blackboard_router)
|
||||||
app.include_router(checkpoint_router)
|
app.include_router(checkpoint_router)
|
||||||
app.include_router(daemon_router)
|
app.include_router(daemon_router)
|
||||||
@@ -299,17 +300,16 @@ async def list_projects_compat():
|
|||||||
DIST_DIR = Path(__file__).parent / "frontend" / "dist"
|
DIST_DIR = Path(__file__).parent / "frontend" / "dist"
|
||||||
if DIST_DIR.exists():
|
if DIST_DIR.exists():
|
||||||
# v3.1: 缓存策略 - HTML 不缓存(确保新版本生效),JS/CSS 长缓存(Vite content hash 已处理)
|
# v3.1: 缓存策略 - HTML 不缓存(确保新版本生效),JS/CSS 长缓存(Vite content hash 已处理)
|
||||||
|
import mimetypes
|
||||||
_static_app = StaticFiles(directory=str(DIST_DIR), html=True)
|
_static_app = StaticFiles(directory=str(DIST_DIR), html=True)
|
||||||
|
|
||||||
class CachedStaticFiles:
|
class CachedStaticFiles:
|
||||||
"""包装 StaticFiles,添加 Cache-Control 头"""
|
"""包装 StaticFiles,添加 Cache-Control 头"""
|
||||||
|
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
self._app = app
|
self._app = app
|
||||||
|
|
||||||
async def __call__(self, scope, receive, send):
|
async def __call__(self, scope, receive, send):
|
||||||
original_send = send
|
original_send = send
|
||||||
|
|
||||||
async def patched_send(message):
|
async def patched_send(message):
|
||||||
if message.get("type") == "http.response.start":
|
if message.get("type") == "http.response.start":
|
||||||
headers = dict(message.get("headers", []))
|
headers = dict(message.get("headers", []))
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
def get_data_root() -> Path:
|
def get_data_root() -> Path:
|
||||||
|
|||||||
Reference in New Issue
Block a user