182 lines
8.2 KiB
Python
182 lines
8.2 KiB
Python
"""复盘能力 — 项目执行完后的问题收集 + 技能提议(通用、产线无关)。
|
||
|
||
职责(方案 P2/P3 的代码层):
|
||
- project_retrospective_data:四类问题源统一收集(冒泡问题/退回重做/编排缺口/Bug),
|
||
返回结构化素材给 PM 复盘任务,PM 不重复实现查询(「重复逻辑写 py」原则)。
|
||
- propose_skill:PM 复盘产出的技能提议落 skill_proposals(source=agent, status=pending),
|
||
进技能管理建议列表待人工审核,不直接改技能库(门禁守源头)。
|
||
"""
|
||
|
||
import json
|
||
import logging
|
||
|
||
from sqlor.dbpools import DBPools
|
||
from appPublic.uniqueID import getID
|
||
|
||
DBNAME = "pipeline"
|
||
logger = logging.getLogger("pipeline.retrospective_capability")
|
||
|
||
|
||
def _get_db():
|
||
db = DBPools()
|
||
if not db.databases:
|
||
from appPublic.jsonConfig import getConfig
|
||
config = getConfig()
|
||
if config and config.databases:
|
||
db.databases = config.databases
|
||
return db, DBNAME
|
||
|
||
|
||
def _rows_to_dicts(recs, limit=200):
|
||
out = []
|
||
for r in (recs or [])[:limit]:
|
||
# sqlor 行是 DictObject:dict(rec) 才能取全部列(vars() 只取内部属性 → 空字典,
|
||
# 2026-08-31 测试机冒烟实测:project 名空壳即此因)。照抄 feature_capability._rec_to_dict。
|
||
if isinstance(r, dict):
|
||
out.append(dict(r))
|
||
continue
|
||
try:
|
||
out.append(dict(r))
|
||
continue
|
||
except (TypeError, ValueError):
|
||
pass
|
||
if hasattr(r, "to_dict"):
|
||
try:
|
||
out.append(r.to_dict())
|
||
continue
|
||
except Exception:
|
||
pass
|
||
out.append({})
|
||
return out
|
||
|
||
|
||
async def project_retrospective_data(project_id, who=None, agent_id=None,
|
||
task_id=None, iteration_id=None):
|
||
"""收集本项目执行中的全部问题素材(四类源合并 + 统计)。返回结构化文本(JSON)。
|
||
|
||
四类问题源(全部已落库,本函数只查不写):
|
||
① 冒泡问题 pipeline_agent_questions:question + 解决记录(answer/answered_by)
|
||
② 退回重做 audit_log:qc_reject/reject 记录(detail=退回意见,who=被退角色)
|
||
③ 编排缺口 pipeline_pm_notices:gap_text
|
||
④ Bug sd_bugs:title/description/fix_description(按项目迭代关联)
|
||
"""
|
||
if not project_id:
|
||
return "FAIL: 缺少 project_id"
|
||
db, dbname = _get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
# ① 冒泡问题(含解决记录)
|
||
bubbles = _rows_to_dicts(await sor.sqlExe(
|
||
"SELECT problem_type, question, from_role, current_handler_role, status, "
|
||
"answer, answered_by, created_at FROM pipeline_agent_questions "
|
||
"WHERE tenant_id=${pid}$ ORDER BY created_at ASC LIMIT 100",
|
||
{"pid": project_id}))
|
||
# ② 退回重做(审计:谁被退、退回意见)
|
||
reworks = _rows_to_dicts(await sor.sqlExe(
|
||
"SELECT action, who, detail, created_at FROM audit_log "
|
||
"WHERE tenant_id=${pid}$ AND entity='pipeline_tasks' "
|
||
"AND action IN ('qc_reject','reject') ORDER BY created_at ASC LIMIT 100",
|
||
{"pid": project_id}))
|
||
# ③ 编排缺口
|
||
gaps = _rows_to_dicts(await sor.sqlExe(
|
||
"SELECT gap_text, status FROM pipeline_pm_notices "
|
||
"WHERE tenant_id=${pid}$ ORDER BY created_at ASC LIMIT 50",
|
||
{"pid": project_id}))
|
||
# ④ Bug(项目 → 迭代 → Bug)
|
||
iters = _rows_to_dicts(await sor.sqlExe(
|
||
"SELECT id, iteration_name FROM sd_iterations WHERE project_id=${pid}$",
|
||
{"pid": project_id}))
|
||
bugs = []
|
||
if iters:
|
||
ids = ",".join("'" + str(i.get('id', '')).replace("'", "") + "'" for i in iters)
|
||
bugs = _rows_to_dicts(await sor.sqlExe(
|
||
"SELECT title, description, severity, status, fix_description, created_at "
|
||
"FROM sd_bugs WHERE iteration_id IN (" + ids + ") "
|
||
"ORDER BY created_at ASC LIMIT 100", {}))
|
||
# 统计:工期
|
||
precs = await sor.sqlExe(
|
||
"SELECT name, created_at, status FROM sd_projects WHERE id=${pid}$",
|
||
{"pid": project_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
proj = _rows_to_dicts(precs)
|
||
proj = proj[0] if proj else {}
|
||
|
||
problems = []
|
||
for b in bubbles:
|
||
problems.append({
|
||
"type": "bubble", "problem": b.get('question', ''),
|
||
"solution": b.get('answer', '') or '(未记录解决方法)',
|
||
"problem_type": b.get('problem_type', ''),
|
||
"roles": "%s→%s" % (b.get('from_role', ''), b.get('current_handler_role', '')),
|
||
"time": str(b.get('created_at', ''))})
|
||
for r in reworks:
|
||
problems.append({
|
||
"type": "rework", "problem": r.get('detail', '') or '(无退回意见)',
|
||
"solution": '(重做后通过,对比新旧交付件可见改法)',
|
||
"role": r.get('who', ''), "via": r.get('action', ''),
|
||
"time": str(r.get('created_at', ''))})
|
||
for g in gaps:
|
||
problems.append({
|
||
"type": "gap", "problem": g.get('gap_text', ''),
|
||
"solution": '(PM 处置记录见任务链)', "time": ''})
|
||
for bug in bugs:
|
||
problems.append({
|
||
"type": "bug", "problem": "%s:%s" % (bug.get('title', ''), (bug.get('description', '') or '')[:300]),
|
||
"solution": bug.get('fix_description', '') or '(未记录修复说明)',
|
||
"severity": bug.get('severity', ''), "status": bug.get('status', ''),
|
||
"time": str(bug.get('created_at', ''))})
|
||
|
||
data = {
|
||
"project": proj.get('name', ''), "project_status": proj.get('status', ''),
|
||
"project_created_at": str(proj.get('created_at', '')),
|
||
"problems": problems,
|
||
"stats": {
|
||
"bubble_count": len(bubbles), "rework_count": len(reworks),
|
||
"gap_count": len(gaps), "bug_count": len(bugs),
|
||
"total": len(problems),
|
||
},
|
||
}
|
||
return json.dumps(data, ensure_ascii=False, default=str)
|
||
|
||
|
||
async def propose_skill(project_id, name, description="", content="",
|
||
who=None, agent_id=None, task_id=None, iteration_id=None):
|
||
"""提交技能提议(复盘产出)。落 skill_proposals,source=agent,status=pending。
|
||
|
||
提议只是草稿:进技能管理建议列表待人工审核(pending→testing→approved→published),
|
||
不直接改技能库。content 为 SKILL.md 草稿,头部应带目标定位注释
|
||
(<!-- target: roles/agent.xxx/yyy(修改) --> 或 <!-- target: 新建 -->)。
|
||
"""
|
||
name = (name or '').strip()
|
||
content = (content or '').strip()
|
||
if not name or not content:
|
||
return False, "需要技能名和内容(content 为 SKILL.md 草稿正文)"
|
||
if len(content) < 80:
|
||
return False, "提议内容过短(<80字):SKILL.md 草稿须含触发条件/问题现象/根因/处理方法"
|
||
db, dbname = _get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
# org/pipeline 从项目取(提议按机构隔离 + 产线归属)
|
||
precs = await sor.sqlExe(
|
||
"SELECT org_id, pipeline_id FROM sd_projects WHERE id=${pid}$",
|
||
{"pid": project_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
org_id = '0'
|
||
pipeline_id = ''
|
||
if precs:
|
||
org_id = getattr(precs[0], 'org_id', '') or '0'
|
||
pipeline_id = getattr(precs[0], 'pipeline_id', '') or ''
|
||
pid = getID()
|
||
await sor.C('skill_proposals', {
|
||
'id': pid,
|
||
'name': name[:100],
|
||
'description': (description or '')[:500],
|
||
'content': content,
|
||
'source': 'agent',
|
||
'status': 'pending',
|
||
'org_id': org_id,
|
||
'pipeline_id': pipeline_id,
|
||
'created_by': who or 'agent.pm',
|
||
})
|
||
await sor.sqlExe("COMMIT", {})
|
||
logger.info("propose_skill: %s project=%s by=%s", name, project_id, who)
|
||
return True, f"已提交技能提议 '{name}'(待审核,暂不生效)"
|