根因修复(develop 不调 fix_bug 导致 bug 恒 open、链断): 1. 三条任务创建路径打 task_kind 标记:_create_next_task=new_dev、 _rollback_task_chain=rework、_pm_create_tasks 按标题判 bug_fix/new_dev 2. bug_capability 新增 classify_task 工具:读 task_kind 标记判断任务来源 (老任务按 rollback_from/title 兜底),develop 据此决定是否走 fix_bug 状态机 3. TOOL_SCHEMAS 注册 classify_task + capability_ctx 注入 task_id 4. 增强能力:_collect_module_skills 运行时扫描 workspace repos/*/skill/SKILL.md, 作为「项目模块」scope 注入技能目录 + load_skill 支持加载模块技能全文, 让项目角色知道引用的业务模块怎么用(架构/数据模型/挂载函数/坑)
270 lines
11 KiB
Python
270 lines
11 KiB
Python
"""Bug 能力 — sd_bugs 的状态机语义化迁移(通用、产线无关)。
|
||
|
||
定位:Bug CRUD 走 xls2ui 生成的端点;本模块只做「生命周期状态流转」——
|
||
每个迁移 CAS 原子(防并发/越权)+ 租户隔离 + 审计。
|
||
|
||
Bug 的状态机/流转规则在 bug skill 里(LLM 读 skill 判断合法性);本模块只固化操作原语。
|
||
|
||
角色规范:role 参数用 agent.{role}(无前缀自动补 agent.);人角色用 {orgtype}.{role}。
|
||
|
||
scope 约定:sd_bugs 无 project_id 列,用 iteration_id 做范围校验(CAS 的 WHERE 里带 iteration_id)。
|
||
"""
|
||
|
||
import logging
|
||
import json
|
||
from datetime import datetime
|
||
from sqlor.dbpools import DBPools
|
||
from appPublic.uniqueID import getID
|
||
from .audit import record_audit
|
||
|
||
DBNAME = "pipeline"
|
||
logger = logging.getLogger("pipeline.bug_capability")
|
||
|
||
TABLE = "sd_bugs"
|
||
|
||
# Bug 状态(SDLC 默认,状态机语义见 bug skill)
|
||
S_OPEN = "open" # 新建
|
||
S_CONFIRMED = "confirmed" # 已确认
|
||
S_FIXING = "fixing" # 修复中
|
||
S_FIXED = "fixed" # 已修复
|
||
S_VERIFIED = "verified" # 已验证
|
||
S_CLOSED = "closed" # 已关闭
|
||
S_REJECTED = "rejected" # 已驳回
|
||
|
||
|
||
def _get_db():
|
||
db = DBPools()
|
||
if not db.databases:
|
||
from appPublic.jsonConfig import getConfig
|
||
config = getConfig()
|
||
if config.databases:
|
||
db.databases = config.databases
|
||
return db, DBNAME
|
||
|
||
|
||
def _normalize_role(role):
|
||
"""角色规范:agent 角色补 agent. 前缀;人角色 {orgtype}.{role} 保留原样。"""
|
||
role = (role or "").strip()
|
||
if not role:
|
||
return ""
|
||
if "." in role:
|
||
return role
|
||
return f"agent.{role}"
|
||
|
||
|
||
async def _transition(bug_id, iteration_id, from_states, to_state, action,
|
||
extra_updates=None, who=None, agent_id=None, detail=None):
|
||
"""CAS 状态迁移(支持多 from 状态 + 额外字段更新)+ 审计。返回 (ok, message)。"""
|
||
if not bug_id or not iteration_id:
|
||
return False, "缺少 bug_id 或 iteration_id"
|
||
if isinstance(from_states, str):
|
||
from_states = [from_states]
|
||
from_in = ",".join(f"${{s{i}}}$" for i in range(len(from_states)))
|
||
params = {"to": to_state, "bid": bug_id, "iid": iteration_id}
|
||
for i, s in enumerate(from_states):
|
||
params[f"s{i}"] = s
|
||
|
||
extra_sets = ""
|
||
for col, val in (extra_updates or {}).items():
|
||
key = col
|
||
params[key] = val
|
||
extra_sets += f", {col}=${{{key}}}$"
|
||
|
||
db, dbname = _get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
await sor.sqlExe(
|
||
f"UPDATE {TABLE} SET status=${{to}}$, updated_at=NOW(){extra_sets} "
|
||
f"WHERE id=${{bid}}$ AND iteration_id=${{iid}}$ AND status IN ({from_in})",
|
||
params)
|
||
recs = await sor.R(TABLE, {'id': bug_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
if not recs:
|
||
return False, "Bug 不存在"
|
||
cur = getattr(recs[0], 'status', '')
|
||
if cur != to_state:
|
||
return False, f"状态迁移失败(CAS): 期望 from={from_states} 实际 status={cur}"
|
||
await record_audit(iteration_id, TABLE, bug_id, action,
|
||
from_state="|".join(from_states), to_state=to_state,
|
||
who=who, agent_id=agent_id, detail=detail, sor=sor)
|
||
return True, to_state
|
||
|
||
|
||
async def report_bug(iteration_id, title, description="", severity="major",
|
||
priority="P1", case_id="", step_name="", reporter_type="agent",
|
||
reporter_id="", assignee_id="", who=None, agent_id=None):
|
||
"""上报 Bug:新建记录,status=open。返回 (ok, bug_id_or_message)。"""
|
||
if not iteration_id:
|
||
return False, "缺少 iteration_id"
|
||
if not title or not title.strip():
|
||
return False, "缺少 Bug 标题"
|
||
db, dbname = _get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
bid = getID()
|
||
await sor.C(TABLE, {
|
||
'id': bid,
|
||
'iteration_id': iteration_id,
|
||
'case_id': case_id or '',
|
||
'step_name': step_name or '',
|
||
'title': title.strip(),
|
||
'description': description or '',
|
||
'severity': severity or 'major',
|
||
'priority': priority or 'P1',
|
||
'status': S_OPEN,
|
||
'reporter_type': reporter_type or 'agent',
|
||
'reporter_id': reporter_id or '',
|
||
'assignee_id': assignee_id or '',
|
||
})
|
||
await record_audit(iteration_id, TABLE, bid, 'report',
|
||
to_state=S_OPEN, who=_normalize_role(who),
|
||
agent_id=agent_id, sor=sor)
|
||
logger.info("report_bug: %s iteration=%s", bid, iteration_id)
|
||
return True, bid
|
||
|
||
|
||
async def confirm_bug(bug_id, iteration_id, who=None, agent_id=None):
|
||
"""确认 Bug:open → confirmed。"""
|
||
return await _transition(bug_id, iteration_id, [S_OPEN], S_CONFIRMED, 'confirm',
|
||
who=who, agent_id=agent_id)
|
||
|
||
|
||
async def start_fix(bug_id, iteration_id, assignee_id="", who=None, agent_id=None):
|
||
"""开始修复:confirmed/open → fixing(记录处理人)。
|
||
|
||
from 允许 open:PM review_rollback 回退 develop 处理部署/功能 Bug 时,走的是「回退重做」任务链
|
||
(隐含确认缺陷有效),不经过 confirm_bug,bug 停在 open。若 start_fix 只认 confirmed,
|
||
develop 在回退重做任务里修完缺陷后无法 start_fix→fix_bug,bug 永远 open、闭环断在 fixed 环节。
|
||
"""
|
||
return await _transition(bug_id, iteration_id, [S_CONFIRMED, S_OPEN], S_FIXING, 'start_fix',
|
||
extra_updates={"assignee_id": assignee_id or ''},
|
||
who=who, agent_id=agent_id)
|
||
|
||
|
||
async def fix_bug(bug_id, iteration_id, fix_description="", fix_commit="",
|
||
who=None, agent_id=None):
|
||
"""修复完成:fixing → fixed(附修复说明 + 提交)。"""
|
||
return await _transition(bug_id, iteration_id, [S_FIXING], S_FIXED, 'fix',
|
||
extra_updates={"fix_description": fix_description or '',
|
||
"fix_commit": fix_commit or ''},
|
||
who=who, agent_id=agent_id, detail=fix_description)
|
||
|
||
|
||
async def verify_bug(bug_id, iteration_id, verified_by="", who=None, agent_id=None):
|
||
"""验证修复:fixed → verified(记录验证人)。"""
|
||
return await _transition(bug_id, iteration_id, [S_FIXED], S_VERIFIED, 'verify',
|
||
extra_updates={"verified_by": verified_by or ''},
|
||
who=who, agent_id=agent_id)
|
||
|
||
|
||
async def close_bug(bug_id, iteration_id, who=None, agent_id=None):
|
||
"""关闭 Bug:verified → closed(记录关闭时间)。"""
|
||
return await _transition(bug_id, iteration_id, [S_VERIFIED], S_CLOSED, 'close',
|
||
extra_updates={"closed_at": datetime.now().strftime('%Y-%m-%d %H:%M:%S')},
|
||
who=who, agent_id=agent_id)
|
||
|
||
|
||
async def reject_bug(bug_id, iteration_id, who=None, agent_id=None, comment=None):
|
||
"""驳回 Bug:open/confirmed → rejected(附意见)。"""
|
||
if not comment or not comment.strip():
|
||
return False, "驳回必须附意见"
|
||
return await _transition(bug_id, iteration_id, [S_OPEN, S_CONFIRMED], S_REJECTED, 'reject',
|
||
who=who, agent_id=agent_id, detail=comment)
|
||
|
||
|
||
async def reopen_bug(bug_id, iteration_id, who=None, agent_id=None, comment=None):
|
||
"""重新打开:closed/rejected → open。"""
|
||
return await _transition(bug_id, iteration_id, [S_CLOSED, S_REJECTED], S_OPEN, 'reopen',
|
||
who=who, agent_id=agent_id, detail=comment)
|
||
|
||
|
||
async def set_bug_state(bug_id, iteration_id, from_state, to_state,
|
||
who=None, agent_id=None, detail=None):
|
||
"""通用 CAS 状态迁移兜底(跨产线自定义状态机用)。"""
|
||
return await _transition(bug_id, iteration_id, [from_state], to_state, 'set_state',
|
||
who=who, agent_id=agent_id, detail=detail)
|
||
|
||
|
||
async def list_bugs(iteration_id, status=None, severity=None, limit=100) -> list:
|
||
"""列出 Bug(可按状态/严重度过滤)。"""
|
||
db, dbname = _get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
conditions = ["iteration_id=${iid}$"]
|
||
params = {"iid": iteration_id}
|
||
if status:
|
||
conditions.append("status=${status}$")
|
||
params["status"] = status
|
||
if severity:
|
||
conditions.append("severity=${sev}$")
|
||
params["sev"] = severity
|
||
where = " AND ".join(conditions)
|
||
try:
|
||
limit = int(limit)
|
||
except (TypeError, ValueError):
|
||
limit = 100
|
||
sql = (f"SELECT * FROM {TABLE} WHERE {where} "
|
||
f"ORDER BY created_at DESC LIMIT {limit}")
|
||
recs = await sor.sqlExe(sql, params)
|
||
# 释放 SELECT 元数据锁
|
||
await sor.sqlExe("COMMIT", {})
|
||
result = []
|
||
for rec in (recs or []):
|
||
result.append(_rec_to_dict(rec))
|
||
return result
|
||
|
||
|
||
def _rec_to_dict(rec):
|
||
"""把 sqlor 记录对象转成 dict(sqlor 行是 DictObject,必须 dict(rec) 取列)。"""
|
||
if isinstance(rec, dict):
|
||
return dict(rec)
|
||
try:
|
||
return dict(rec)
|
||
except (TypeError, ValueError):
|
||
if hasattr(rec, 'to_dict'):
|
||
try:
|
||
return rec.to_dict()
|
||
except Exception:
|
||
pass
|
||
return {}
|
||
|
||
|
||
async def classify_task(task_id, iteration_id="", who=None, agent_id=None):
|
||
"""判断任务来源:new_dev(新开发) / bug_fix(修复 Bug) / rework(质检重做)。
|
||
|
||
优先读任务 params.task_kind 标记(三条创建路径已打);老任务无标记则按
|
||
params.rollback_from / title 兜底推断。develop 据此决定是否走 fix_bug 状态机:
|
||
- bug_fix / rework:任务开始 start_fix、完成 fix_bug
|
||
- new_dev:纯开发,不走 bug 状态机(除非过程中自己 report 了新 bug)
|
||
|
||
返回 (ok, {"task_kind": ..., "reason": ..., "title": ...} 或错误消息)。
|
||
"""
|
||
if not task_id:
|
||
return False, "缺少 task_id"
|
||
db, dbname = _get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
recs = await sor.sqlExe(
|
||
"SELECT id, title, params FROM pipeline_tasks WHERE id=${tid}$", {"tid": task_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
if not recs:
|
||
return False, f"任务不存在: {task_id}"
|
||
rec = recs[0]
|
||
title = getattr(rec, 'title', '') or ''
|
||
params_str = getattr(rec, 'params', '{}') or '{}'
|
||
try:
|
||
params = json.loads(params_str) if isinstance(params_str, str) else (params_str or {})
|
||
except (json.JSONDecodeError, TypeError):
|
||
params = {}
|
||
|
||
kind = (params.get('task_kind') or '').strip()
|
||
reason = "params.task_kind 标记"
|
||
if not kind:
|
||
# 老任务兜底:rollback_from 存在 → rework;title 含「修复 Bug」→ bug_fix;否则 new_dev
|
||
if params.get('rollback_from'):
|
||
kind = 'rework'
|
||
reason = "params.rollback_from 存在(回退重做)"
|
||
elif ('修复 Bug' in title) or ('修复Bug' in title) or ('修复bug' in title):
|
||
kind = 'bug_fix'
|
||
reason = "title 含「修复 Bug」"
|
||
else:
|
||
kind = 'new_dev'
|
||
reason = "无 bug 相关标记,默认新开发"
|
||
return True, {"task_id": task_id, "task_kind": kind, "title": title, "reason": reason}
|