ymq 99c1b60e31 fix: rework(回退重做)任务不调 bug 工具,只有 bug_fix 走状态机
用户纠正:rework 的语义是「重做交付件」(QC/PM 退回重做),不是「修 bug」,
不该走 start_fix→fix_bug 状态机。只有 PM 明确派发的「修复 Bug」任务(bug_fix)
才走 bug 状态机。

修正 classify_task 的 docstring + TOOL_SCHEMAS 描述,明确:
- bug_fix:开始 start_fix、完成 fix_bug
- rework/new_dev:不调 bug 工具
2026-08-23 21:52:49 +08:00

271 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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:PM 派发的「修复 Bug」任务,开始 start_fix、完成 fix_bug
- rework:回退重做(QC/PM 退回重做交付件),不调 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}