feat: AirPlan V2 — 全专家插件强制路由 + 事件系统规范化

P0-8 扩大: do_mode.py finish_worker 全专家插件强制路由
- GUI→XDB, network→NDB, C/C++→SDB, done→Rvr, blocked/failed→Dbg
- 证据去重: 已有 xdbSessions/ndbSessions/sdbReports/rvrReviewed 则跳过

P1-GAP17: 事件 emit 规范化
- 新增 7 个事件常量 (TASK_ENTERED, TASK_FINISHED, ENGINE_ENTERED 等)
- 全部 emit 调用替换字符串字面量为常量,零残留
- 30 个事件类型常量全部定义且唯一

P1-GAP18: 事件日志原子轮转
- emit 计数器每 128 次检查轮转,避免每次 emit 读文件
- 清除未使用的 _emit_with_completion/_pending_merge_complete
- 原子轮转: tempfile+os.replace 保证不损坏

eng 极端接管: 强制调用全部专家插件 (Dbg/XDB/NDB/SDB/Rvr)
commands/do.md: 更新为全专家插件路由文档

全量测试: 69 通过, 0 失败

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
AirPlan
2026-06-12 15:56:44 +08:00
commit 6130478c96
73 changed files with 10622 additions and 0 deletions

100
lib/air_runtime/todo_parser.py Executable file
View File

@@ -0,0 +1,100 @@
"""
TODO 解析器 — V2 修复 P1-8列索引从表头推导不再硬编码 cells[1]/cells[6]/cells[7]。
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from pathlib import Path
@dataclass
class TodoTask:
task_id: str
task: str
files_dirs: str = ""
status: str = "TODO"
done_when: str = ""
validations: str = ""
adr: str = ""
def parse_tasks(todo_path: Path) -> list[TodoTask]:
"""从 todo.md 解析任务列表,动态检测列索引。"""
if not todo_path.exists():
return []
content = todo_path.read_text(encoding="utf-8")
lines = [l.strip() for l in content.splitlines() if l.strip()]
# 找到 Markdown 表格头
header_idx = -1
for i, line in enumerate(lines):
if line.startswith("|") and "Task" in line and "Status" in line:
header_idx = i
break
if header_idx < 0:
return []
# 解析列名
header_line = lines[header_idx]
header_cols = [c.strip() for c in header_line.split("|") if c.strip()]
# 建立列名 → 索引映射
col_map = {}
for idx, col_name in enumerate(header_cols):
col_name_lower = col_name.lower()
if "task" in col_name_lower:
col_map["task"] = idx
elif "status" in col_name_lower:
col_map["status"] = idx
elif "files" in col_name_lower or "dir" in col_name_lower:
col_map["files_dirs"] = idx
elif "done" in col_name_lower or "when" in col_name_lower:
col_map["done_when"] = idx
elif "valid" in col_name_lower:
col_map["validations"] = idx
elif "adr" in col_name_lower:
col_map["adr"] = idx
# 跳过表头和分隔符
tasks = []
for line in lines[header_idx + 2:]:
if not line.startswith("|"):
continue
cells = [c.strip() for c in line.split("|") if len(c.strip()) > 0]
if not cells:
continue
task_cell = cells[col_map.get("task", 0)] if col_map.get("task", 0) < len(cells) else ""
# 优先提取 [T-xxx] 方括号格式的 ID若没有则尝试从开头提取 G-001/H-000 类短 ID
tid_match = re.match(r"\[([A-Za-z0-9_\-\.]+)\]", task_cell)
if tid_match:
task_id = tid_match.group(1)
else:
short_match = re.match(r"^([A-Z]+-\d+[a-z]*)", task_cell)
task_id = short_match.group(1) if short_match else task_cell
task = task_cell
status = cells[col_map.get("status", 1)] if col_map.get("status", 1) < len(cells) else "TODO"
files_dirs = cells[col_map.get("files_dirs", 2)] if col_map.get("files_dirs", 2) < len(cells) else ""
done_when = cells[col_map.get("done_when", 3)] if col_map.get("done_when", 3) < len(cells) else ""
validations = cells[col_map.get("validations", 4)] if col_map.get("validations", 4) < len(cells) else ""
adr = cells[col_map.get("adr", 5)] if col_map.get("adr", 5) < len(cells) else ""
# 清理标记
task_id = re.sub(r"^\[|\]$", "", task_id).strip()
if task_id and task_id != "---":
tasks.append(TodoTask(
task_id=task_id,
task=task,
files_dirs=files_dirs,
status=status.upper() if status else "TODO",
done_when=done_when,
validations=validations,
adr=adr,
))
return tasks