fix(lint): 修复 PR #14 引入的 lint 回退 (119→0)
PR #14 从旧分支复制文件导致回退了 PR #10 的 lint 修复。 修复内容: - autoflake 移除未使用导入/变量 - autopep8 修复缩进/空格 - 手动修复 F821(pathlib→Path), F541(f-string), F841(未使用变量) - 所有修复均通过 flake8 --max-line-length=120 --extend-ignore=E501 检查 (0 errors)
This commit is contained in:
@@ -5,14 +5,14 @@ from __future__ import annotations
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
|
||||
from src.blackboard.operations import Blackboard
|
||||
from src.blackboard.models import Task, Review
|
||||
from src.blackboard.queries import Queries
|
||||
from src.blackboard.db import VALID_STATUSES, VALID_TRANSITIONS, COMMENT_TYPES, OUTPUT_TYPES
|
||||
from src.blackboard.db import VALID_STATUSES, OUTPUT_TYPES
|
||||
from src.blackboard.registry import ProjectRegistry
|
||||
|
||||
from src.utils import get_data_root
|
||||
@@ -59,7 +59,10 @@ async def list_tasks(project_id: str,
|
||||
assignee: Optional[str] = None,
|
||||
parent_task: Optional[str] = None):
|
||||
bb = _bb(project_id)
|
||||
tasks = bb.list_tasks(status=status, assignee=assignee, parent_task=parent_task)
|
||||
tasks = bb.list_tasks(
|
||||
status=status,
|
||||
assignee=assignee,
|
||||
parent_task=parent_task)
|
||||
return {"tasks": [_task_to_dict(t) for t in tasks]}
|
||||
|
||||
|
||||
@@ -79,10 +82,12 @@ async def get_task(project_id: str, task_id: str,
|
||||
result["outputs_count"] = detail.get("outputs_count", 0)
|
||||
result["review_status"] = detail.get("review_status")
|
||||
result["latest_event_detail"] = detail.get("latest_event_detail")
|
||||
result["comments"] = [dict(c.__dict__) for c in bb.get_comments(task_id)]
|
||||
result["comments"] = [dict(c.__dict__)
|
||||
for c in bb.get_comments(task_id)]
|
||||
result["outputs"] = [dict(o.__dict__) for o in bb.get_outputs(task_id)]
|
||||
result["reviews"] = [dict(r.__dict__) for r in bb.get_reviews(task_id)]
|
||||
result["decisions"] = [dict(d.__dict__) for d in bb.get_decisions(task_id)]
|
||||
result["decisions"] = [dict(d.__dict__)
|
||||
for d in bb.get_decisions(task_id)]
|
||||
result["events"] = q.task_events(task_id)
|
||||
result["experiences"] = q.task_experiences(task_id)
|
||||
return result
|
||||
@@ -134,7 +139,8 @@ async def create_task(project_id: str, body: Dict[str, Any]):
|
||||
priority=body.get("priority", 5),
|
||||
assignee=assignee,
|
||||
assigned_by=body.get("assigned_by", "user"),
|
||||
depends_on=json.dumps(body["depends_on"]) if "depends_on" in body else None,
|
||||
depends_on=json.dumps(
|
||||
body["depends_on"]) if "depends_on" in body else None,
|
||||
parent_task=body.get("parent_task"),
|
||||
risk_level=body.get("risk_level", "standard"),
|
||||
stage=body.get("stage"),
|
||||
@@ -175,7 +181,8 @@ async def _generate_title(description: str) -> str | None:
|
||||
resp = client.chat.completions.create(
|
||||
model=model,
|
||||
messages=[
|
||||
{"role": "system", "content": "你是一个任务标题生成器。根据用户的需求描述,生成一个简洁的中文标题(5-15字),只输出标题,不要任何其他内容。"},
|
||||
{"role": "system",
|
||||
"content": "你是一个任务标题生成器。根据用户的需求描述,生成一个简洁的中文标题(5-15字),只输出标题,不要任何其他内容。"},
|
||||
{"role": "user", "content": description[:500]},
|
||||
],
|
||||
max_tokens=50,
|
||||
@@ -187,7 +194,8 @@ async def _generate_title(description: str) -> str | None:
|
||||
return title
|
||||
except Exception as e:
|
||||
import logging
|
||||
logging.getLogger("moziplus-v2").warning(f"Title generation failed: {e}")
|
||||
logging.getLogger(
|
||||
"moziplus-v2").warning(f"Title generation failed: {e}")
|
||||
return None
|
||||
|
||||
|
||||
@@ -205,7 +213,8 @@ async def task_progress(project_id: str, task_id: str):
|
||||
async def claim_task(project_id: str, task_id: str, body: Dict[str, Any]):
|
||||
bb = _bb(project_id)
|
||||
if not bb.claim_task(task_id, body["agent"]):
|
||||
raise HTTPException(409, "Claim failed (already claimed or wrong assignee)")
|
||||
raise HTTPException(
|
||||
409, "Claim failed (already claimed or wrong assignee)")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@@ -240,7 +249,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,
|
||||
agent=body.get("agent")):
|
||||
agent=body.get("agent")):
|
||||
raise HTTPException(409, {
|
||||
"error": "transition_failed",
|
||||
"detail": f"Status update failed for {task_id}",
|
||||
@@ -265,6 +274,7 @@ async def update_status(project_id: str, task_id: str, body: Dict[str, Any]):
|
||||
# --- @mention 自动提取(#04) ---
|
||||
_KNOWN_AGENT_IDS: list = []
|
||||
|
||||
|
||||
def _init_agent_ids():
|
||||
"""从配置文件加载 Agent ID 列表"""
|
||||
global _KNOWN_AGENT_IDS
|
||||
@@ -272,18 +282,32 @@ def _init_agent_ids():
|
||||
return
|
||||
try:
|
||||
import yaml
|
||||
cfg_path = os.path.join(os.path.dirname(__file__), "..", "..", "config", "default.yaml")
|
||||
cfg_path = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"..",
|
||||
"..",
|
||||
"config",
|
||||
"default.yaml")
|
||||
with open(cfg_path) as f:
|
||||
cfg = yaml.safe_load(f)
|
||||
_KNOWN_AGENT_IDS = list(cfg.get("daemon", {}).get("agent_profiles", {}).keys())
|
||||
_KNOWN_AGENT_IDS = list(
|
||||
cfg.get(
|
||||
"daemon",
|
||||
{}).get(
|
||||
"agent_profiles",
|
||||
{}).keys())
|
||||
except Exception:
|
||||
_KNOWN_AGENT_IDS = []
|
||||
|
||||
|
||||
def _extract_mentions(text: str) -> list:
|
||||
"""从文本中自动提取 @agent-id 格式的 mention"""
|
||||
import re
|
||||
_init_agent_ids()
|
||||
candidates = set(re.findall(r'@([a-z][a-z0-9]*(?:-[a-z][a-z0-9]*)+)', text))
|
||||
candidates = set(
|
||||
re.findall(
|
||||
r'@([a-z][a-z0-9]*(?:-[a-z][a-z0-9]*)+)',
|
||||
text))
|
||||
return [a for a in candidates if a in _KNOWN_AGENT_IDS]
|
||||
|
||||
|
||||
@@ -317,8 +341,8 @@ async def add_comment(project_id: str, task_id: str, body: Dict[str, Any]):
|
||||
merged_mentions = list(set(explicit_mentions + auto_mentions))
|
||||
|
||||
cid = bb.add_comment(task_id, body["author"], comment_body,
|
||||
comment_type=body.get("comment_type", "general"),
|
||||
mentions=merged_mentions)
|
||||
comment_type=body.get("comment_type", "general"),
|
||||
mentions=merged_mentions)
|
||||
if merged_mentions:
|
||||
bb.record_mentions(cid, task_id, merged_mentions)
|
||||
# #10: SSE 通知前端黑板有新 comment
|
||||
@@ -395,7 +419,8 @@ async def write_output(project_id: str, task_id: str, body: Dict[str, Any]):
|
||||
)
|
||||
os.makedirs(artifacts_dir, exist_ok=True)
|
||||
# 安全文件名
|
||||
safe_name = "".join(c if c.isalnum() or c in "._-" else "_" for c in title)
|
||||
safe_name = "".join(
|
||||
c if c.isalnum() or c in "._-" else "_" for c in title)
|
||||
if not safe_name:
|
||||
safe_name = "output"
|
||||
file_path = os.path.join(artifacts_dir, safe_name)
|
||||
@@ -424,8 +449,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]):
|
||||
bb = _bb(project_id)
|
||||
did = bb.add_decision(task_id, body["decider"], body["decision"],
|
||||
body["rationale"],
|
||||
alternatives=body.get("alternatives"))
|
||||
body["rationale"],
|
||||
alternatives=body.get("alternatives"))
|
||||
return {"ok": True, "decision_id": did}
|
||||
|
||||
|
||||
@@ -435,7 +460,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]):
|
||||
bb = _bb(project_id)
|
||||
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}
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ from typing import Optional
|
||||
from src.blackboard.operations import Blackboard
|
||||
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"])
|
||||
|
||||
|
||||
# ── 请求模型 ──
|
||||
@@ -50,10 +52,12 @@ def list_checkpoints(project_id: str, task_id: str):
|
||||
|
||||
|
||||
@router.post("")
|
||||
def create_checkpoint(project_id: str, task_id: str, req: CreateCheckpointRequest):
|
||||
def create_checkpoint(project_id: str, task_id: str,
|
||||
req: CreateCheckpointRequest):
|
||||
"""Agent 创建 checkpoint"""
|
||||
if req.type not in ("verify", "decision", "action"):
|
||||
raise HTTPException(status_code=400, detail=f"Invalid checkpoint type: {req.type}")
|
||||
raise HTTPException(status_code=400,
|
||||
detail=f"Invalid checkpoint type: {req.type}")
|
||||
|
||||
bb = _bb(project_id)
|
||||
# 验证 task 存在
|
||||
@@ -73,10 +77,15 @@ def create_checkpoint(project_id: str, task_id: str, req: CreateCheckpointReques
|
||||
|
||||
|
||||
@router.post("/{checkpoint_id}/approve")
|
||||
def approve_checkpoint(project_id: str, task_id: str, checkpoint_id: str, req: ResolveCheckpointRequest):
|
||||
def approve_checkpoint(project_id: str, task_id: str,
|
||||
checkpoint_id: str, req: ResolveCheckpointRequest):
|
||||
"""用户通过 checkpoint → 自动推进 task 状态"""
|
||||
bb = _bb(project_id)
|
||||
result = bb.resolve_checkpoint(checkpoint_id, "approve", req.resolved_by, req.note)
|
||||
result = bb.resolve_checkpoint(
|
||||
checkpoint_id,
|
||||
"approve",
|
||||
req.resolved_by,
|
||||
req.note)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail="Checkpoint not found")
|
||||
if "error" in result:
|
||||
@@ -97,10 +106,15 @@ def approve_checkpoint(project_id: str, task_id: str, checkpoint_id: str, req: R
|
||||
|
||||
|
||||
@router.post("/{checkpoint_id}/reject")
|
||||
def reject_checkpoint(project_id: str, task_id: str, checkpoint_id: str, req: ResolveCheckpointRequest):
|
||||
def reject_checkpoint(project_id: str, task_id: str,
|
||||
checkpoint_id: str, req: ResolveCheckpointRequest):
|
||||
"""用户驳回 checkpoint → task 回到 working"""
|
||||
bb = _bb(project_id)
|
||||
result = bb.resolve_checkpoint(checkpoint_id, "reject", req.resolved_by, req.note)
|
||||
result = bb.resolve_checkpoint(
|
||||
checkpoint_id,
|
||||
"reject",
|
||||
req.resolved_by,
|
||||
req.note)
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail="Checkpoint not found")
|
||||
if "error" in result:
|
||||
|
||||
+19
-8
@@ -9,7 +9,7 @@ from __future__ import annotations
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
|
||||
@@ -34,7 +34,9 @@ def _get_valid_agents() -> set:
|
||||
except Exception:
|
||||
pass
|
||||
# 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"])
|
||||
|
||||
@@ -97,7 +99,10 @@ async def list_mail(
|
||||
):
|
||||
"""Mail 列表(按时间倒序)"""
|
||||
bb = _bb()
|
||||
tasks = bb.list_tasks(status=status, assignee=to_agent, assigned_by=from_agent)
|
||||
tasks = bb.list_tasks(
|
||||
status=status,
|
||||
assignee=to_agent,
|
||||
assigned_by=from_agent)
|
||||
|
||||
mails = []
|
||||
for t in tasks:
|
||||
@@ -222,13 +227,16 @@ async def send_mail(body: Dict[str, Any]):
|
||||
|
||||
# A8: 只有原邮件的双方能回复(严格 1 对 1)
|
||||
if from_agent not in (orig_from, orig_to):
|
||||
raise HTTPException(400, f"只有邮件的发送者或接收者可以回复")
|
||||
raise HTTPException(400, "只有邮件的发送者或接收者可以回复")
|
||||
|
||||
# A6/A7: 自动纠正 to → 原邮件发件者
|
||||
to_agent = body.get("to", "").strip()
|
||||
corrected_to = orig_from # 回复方向固定: reply → original sender
|
||||
if to_agent and to_agent != corrected_to:
|
||||
auto_corrected = {"field": "to", "original": to_agent, "corrected": corrected_to}
|
||||
auto_corrected = {
|
||||
"field": "to",
|
||||
"original": to_agent,
|
||||
"corrected": corrected_to}
|
||||
to_agent = corrected_to
|
||||
else:
|
||||
# --- A2: to 必填(非回复场景) ---
|
||||
@@ -255,7 +263,8 @@ async def send_mail(body: Dict[str, Any]):
|
||||
conversation_id = body.get("conversation_id")
|
||||
if not conversation_id and original:
|
||||
try:
|
||||
orig_meta = json.loads(original.must_haves) if original.must_haves else {}
|
||||
orig_meta = json.loads(
|
||||
original.must_haves) if original.must_haves else {}
|
||||
conversation_id = orig_meta.get("conversation_id")
|
||||
except Exception:
|
||||
pass
|
||||
@@ -310,10 +319,12 @@ async def delete_mail(prefix: Optional[str] = Query(None)):
|
||||
for t in tasks:
|
||||
if t.title and t.title.startswith(prefix):
|
||||
if t.status not in ("cancelled",):
|
||||
bb.update_task_status(t.id, "cancelled", agent="mail-cleanup-api")
|
||||
bb.update_task_status(
|
||||
t.id, "cancelled", agent="mail-cleanup-api")
|
||||
deleted_ids.append(t.id)
|
||||
|
||||
return {"ok": True, "deleted_count": len(deleted_ids), "deleted_ids": deleted_ids}
|
||||
return {"ok": True, "deleted_count": len(
|
||||
deleted_ids), "deleted_ids": deleted_ids}
|
||||
|
||||
|
||||
@router.patch("/{mail_id}")
|
||||
|
||||
+22
-10
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
|
||||
@@ -31,8 +31,10 @@ async def list_projects():
|
||||
if db_path.exists():
|
||||
try:
|
||||
conn = sqlite3.connect(str(db_path), timeout=5)
|
||||
total = conn.execute("SELECT COUNT(*) FROM tasks WHERE status != 'cancelled'").fetchone()[0]
|
||||
active = conn.execute("SELECT COUNT(*) FROM tasks WHERE COALESCE(archived,0)=0").fetchone()[0]
|
||||
total = conn.execute(
|
||||
"SELECT COUNT(*) FROM tasks WHERE status != 'cancelled'").fetchone()[0]
|
||||
active = conn.execute(
|
||||
"SELECT COUNT(*) FROM tasks WHERE COALESCE(archived,0)=0").fetchone()[0]
|
||||
archived = total - active
|
||||
conn.close()
|
||||
info['task_count'] = active
|
||||
@@ -45,8 +47,10 @@ async def list_projects():
|
||||
if general_db.exists() and "_general" not in projects:
|
||||
try:
|
||||
conn = sqlite3.connect(str(general_db), timeout=5)
|
||||
total = conn.execute("SELECT COUNT(*) FROM tasks WHERE status != 'cancelled'").fetchone()[0]
|
||||
active = conn.execute("SELECT COUNT(*) FROM tasks WHERE COALESCE(archived,0)=0").fetchone()[0]
|
||||
total = conn.execute(
|
||||
"SELECT COUNT(*) FROM tasks WHERE status != 'cancelled'").fetchone()[0]
|
||||
active = conn.execute(
|
||||
"SELECT COUNT(*) FROM tasks WHERE COALESCE(archived,0)=0").fetchone()[0]
|
||||
conn.close()
|
||||
projects["_general"] = {
|
||||
"id": "_general", "name": "一般任务", "description": "无项目归属的通用任务",
|
||||
@@ -60,8 +64,10 @@ async def list_projects():
|
||||
if general_db_check.exists():
|
||||
try:
|
||||
conn = sqlite3.connect(str(general_db_check), timeout=5)
|
||||
total = conn.execute("SELECT COUNT(*) FROM tasks WHERE status != 'cancelled'").fetchone()[0]
|
||||
active = conn.execute("SELECT COUNT(*) FROM tasks WHERE COALESCE(archived,0)=0").fetchone()[0]
|
||||
total = conn.execute(
|
||||
"SELECT COUNT(*) FROM tasks WHERE status != 'cancelled'").fetchone()[0]
|
||||
active = conn.execute(
|
||||
"SELECT COUNT(*) FROM tasks WHERE COALESCE(archived,0)=0").fetchone()[0]
|
||||
conn.close()
|
||||
projects["_general"]["task_count"] = active
|
||||
projects["_general"]["task_count_total"] = total
|
||||
@@ -76,7 +82,7 @@ async def list_projects():
|
||||
async def create_project(body: Dict[str, Any]):
|
||||
reg = _registry()
|
||||
try:
|
||||
info = reg.create_project(
|
||||
reg.create_project(
|
||||
body["id"], body["name"],
|
||||
agents=body.get("agents", []),
|
||||
description=body.get("description", ""),
|
||||
@@ -173,7 +179,10 @@ async def move_task(project_id: str, task_id: str, body: Dict[str, Any]):
|
||||
depends_on=child.depends_on, must_haves=child.must_haves,
|
||||
)
|
||||
tgt_bb.create_task(moved_child)
|
||||
src_bb.update_task_status(child.id, "cancelled", detail=f"Moved to {target_project}")
|
||||
src_bb.update_task_status(
|
||||
child.id,
|
||||
"cancelled",
|
||||
detail=f"Moved to {target_project}")
|
||||
moved_ids.append(child.id)
|
||||
|
||||
# 移动主任务
|
||||
@@ -186,7 +195,10 @@ async def move_task(project_id: str, task_id: str, body: Dict[str, Any]):
|
||||
depends_on=task.depends_on, must_haves=task.must_haves,
|
||||
)
|
||||
tgt_bb.create_task(moved_task)
|
||||
src_bb.update_task_status(task_id, "cancelled", detail=f"Moved to {target_project}")
|
||||
src_bb.update_task_status(
|
||||
task_id,
|
||||
"cancelled",
|
||||
detail=f"Moved to {target_project}")
|
||||
moved_ids.insert(0, task_id)
|
||||
|
||||
return {"ok": True, "moved_to": target_project, "moved_ids": moved_ids}
|
||||
|
||||
+49
-16
@@ -46,7 +46,8 @@ _TTL_SECONDS = 7 * 24 * 3600
|
||||
_idempotency_lock = asyncio.Lock()
|
||||
|
||||
|
||||
def _is_duplicate(event: str, delivery: str, payload: Optional[Dict[str, Any]] = None) -> bool:
|
||||
def _is_duplicate(event: str, delivery: str,
|
||||
payload: Optional[Dict[str, Any]] = None) -> bool:
|
||||
"""检查 Webhook 是否重复投递,自动清理过期条目。
|
||||
|
||||
双重去重策略:
|
||||
@@ -56,7 +57,8 @@ def _is_duplicate(event: str, delivery: str, payload: Optional[Dict[str, Any]] =
|
||||
"""
|
||||
now = time.time()
|
||||
# 清理过期条目
|
||||
while _delivery_timestamps and (now - _delivery_timestamps[0][0]) > _TTL_SECONDS:
|
||||
while _delivery_timestamps and (
|
||||
now - _delivery_timestamps[0][0]) > _TTL_SECONDS:
|
||||
_, key = _delivery_timestamps.pop(0)
|
||||
_delivery_cache.discard(key)
|
||||
|
||||
@@ -77,7 +79,11 @@ def _is_duplicate(event: str, delivery: str, payload: Optional[Dict[str, Any]] =
|
||||
content_hash = hashlib.sha256(content.encode()).hexdigest()[:16]
|
||||
content_key = f"content:{event}:{pr_num}:{sender}:{content_hash}"
|
||||
if content_key in _delivery_cache:
|
||||
logger.info("Content-based duplicate detected: %s PR#%s by %s", event, pr_num, sender)
|
||||
logger.info(
|
||||
"Content-based duplicate detected: %s PR#%s by %s",
|
||||
event,
|
||||
pr_num,
|
||||
sender)
|
||||
return True
|
||||
_delivery_cache.add(content_key)
|
||||
_delivery_timestamps.append((now, content_key))
|
||||
@@ -137,8 +143,16 @@ async def _fetch_pr_files(repo: str, pr_number: int) -> Tuple[List[str], str]:
|
||||
last_error = str(e)
|
||||
if attempt < 2:
|
||||
await asyncio.sleep(0.5 * (attempt + 1))
|
||||
logger.warning("Retry %d/3 fetching PR files: %s/pulls/%d", attempt + 1, repo, pr_number)
|
||||
logger.warning("Failed to fetch PR files after 3 retries: %s/pulls/%d - %s", repo, pr_number, last_error)
|
||||
logger.warning(
|
||||
"Retry %d/3 fetching PR files: %s/pulls/%d",
|
||||
attempt + 1,
|
||||
repo,
|
||||
pr_number)
|
||||
logger.warning(
|
||||
"Failed to fetch PR files after 3 retries: %s/pulls/%d - %s",
|
||||
repo,
|
||||
pr_number,
|
||||
last_error)
|
||||
return [], f"获取文件列表失败(重试3次): {last_error}"
|
||||
|
||||
|
||||
@@ -166,7 +180,6 @@ def _calc_risk_level(changed_files: List[str]) -> str:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
MAIL_PROJECT_ID = "_mail"
|
||||
|
||||
|
||||
@@ -252,7 +265,8 @@ async def _handle_pull_request(payload: Dict[str, Any]) -> None:
|
||||
|
||||
pr = payload.get("pull_request")
|
||||
if not pr or not isinstance(pr, dict):
|
||||
logger.warning("pull_request event missing pull_request field, skipping")
|
||||
logger.warning(
|
||||
"pull_request event missing pull_request field, skipping")
|
||||
return
|
||||
repo = _repo_fullname(payload)
|
||||
pr_number = pr.get("number", 0)
|
||||
@@ -266,7 +280,8 @@ async def _handle_pull_request(payload: Dict[str, Any]) -> None:
|
||||
if fetch_error:
|
||||
file_list = f"⚠️ {fetch_error}"
|
||||
else:
|
||||
file_list = "\n".join(f"- {f}" for f in changed_files) if changed_files else "(无文件变更)"
|
||||
file_list = "\n".join(
|
||||
f"- {f}" for f in changed_files) if changed_files else "(无文件变更)"
|
||||
|
||||
text = render_template("review_request", {
|
||||
"repo": repo,
|
||||
@@ -291,11 +306,13 @@ async def _handle_pull_request_review(payload: Dict[str, Any]) -> None:
|
||||
"""
|
||||
review = payload.get("review")
|
||||
if not review or not isinstance(review, dict):
|
||||
logger.warning("pull_request_review event missing review field, skipping")
|
||||
logger.warning(
|
||||
"pull_request_review event missing review field, skipping")
|
||||
return
|
||||
pr = payload.get("pull_request")
|
||||
if not pr or not isinstance(pr, dict):
|
||||
logger.warning("pull_request_review event missing pull_request field, skipping")
|
||||
logger.warning(
|
||||
"pull_request_review event missing pull_request field, skipping")
|
||||
return
|
||||
|
||||
# 兼容两种 payload 格式提取 state
|
||||
@@ -319,7 +336,15 @@ async def _handle_pull_request_review(payload: Dict[str, Any]) -> None:
|
||||
pr_title = pr.get("title", "")
|
||||
pr_author = pr.get("user", {}).get("login", "unknown")
|
||||
# 兼容:org webhook 的 review 没有 user,从 sender 取
|
||||
reviewer = review.get("user", {}).get("login", "") or payload.get("sender", {}).get("login", "unknown")
|
||||
reviewer = review.get(
|
||||
"user",
|
||||
{}).get(
|
||||
"login",
|
||||
"") or payload.get(
|
||||
"sender",
|
||||
{}).get(
|
||||
"login",
|
||||
"unknown")
|
||||
review_body = review.get("body", "") or review.get("content", "(无评论)")
|
||||
|
||||
result_map = {"APPROVED": "通过 ✓", "REQUEST_CHANGES": "驳回 ✗"}
|
||||
@@ -366,7 +391,8 @@ async def _handle_issues(payload: Dict[str, Any]) -> None:
|
||||
logger.debug("Issue assigned but no assignee found, skipping")
|
||||
return
|
||||
|
||||
labels_list = [lbl.get("name", "") for lbl in (issue.get("labels") or [])]
|
||||
labels_list = [lbl.get("name", "")
|
||||
for lbl in (issue.get("labels") or [])]
|
||||
labels = ", ".join(labels_list) if labels_list else "(无标签)"
|
||||
issue_body = issue.get("body", "(无描述)")
|
||||
brief = issue_title[:20].replace(" ", "-").lower()
|
||||
@@ -417,7 +443,9 @@ async def _handle_issue_comment(payload: Dict[str, Any]) -> None:
|
||||
|
||||
# 已关闭的 Issue/PR 不再发送 CI 失败通知
|
||||
if issue.get("state") == "closed":
|
||||
logger.debug("Skipping CI failure notification for closed issue #%s", issue.get("number"))
|
||||
logger.debug(
|
||||
"Skipping CI failure notification for closed issue #%s",
|
||||
issue.get("number"))
|
||||
return
|
||||
|
||||
repo = _repo_fullname(payload)
|
||||
@@ -485,7 +513,8 @@ async def gitea_webhook(
|
||||
# 1. 签名验证
|
||||
if not _verify_signature(body, x_gitea_signature):
|
||||
logger.warning("Webhook signature verification failed")
|
||||
return Response(status_code=403, content="signature verification failed")
|
||||
return Response(status_code=403,
|
||||
content="signature verification failed")
|
||||
|
||||
# 3. 解析 payload(提前解析,用于幂等检查)
|
||||
try:
|
||||
@@ -498,14 +527,18 @@ async def gitea_webhook(
|
||||
if x_gitea_event and x_gitea_delivery:
|
||||
async with _idempotency_lock:
|
||||
if _is_duplicate(x_gitea_event, x_gitea_delivery, payload):
|
||||
logger.debug("Duplicate webhook: %s/%s", x_gitea_event, x_gitea_delivery)
|
||||
logger.debug(
|
||||
"Duplicate webhook: %s/%s",
|
||||
x_gitea_event,
|
||||
x_gitea_delivery)
|
||||
return Response(status_code=200, content="duplicate")
|
||||
|
||||
# 4. 查找 handler
|
||||
handler = _EVENT_HANDLERS.get(x_gitea_event or "")
|
||||
if not handler:
|
||||
logger.debug("Unhandled event type: %s", x_gitea_event)
|
||||
return Response(status_code=200, content=f"unhandled event: {x_gitea_event}")
|
||||
return Response(status_code=200,
|
||||
content=f"unhandled event: {x_gitea_event}")
|
||||
|
||||
# 5. 执行 handler
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user