213 lines
8.7 KiB
Python
213 lines
8.7 KiB
Python
"""商机产线报告/审批能力:研发报告编写 → 人工确认 → 研发审批流程。
|
||
|
||
状态机(REPORT_TRANSITIONS,opp_common):
|
||
draft → confirmed → approval_initiated → approved / rejected
|
||
└ rejected → draft(可重来)
|
||
|
||
门禁纪律:
|
||
- confirm 只能 draft → confirmed,且是人工决策(由人工任务回流驱动)
|
||
- initiate_approval 只能 confirmed → approval_initiated(未经确认禁止发起审批)
|
||
- 审批结论由人工决策回流(approve/reject),agent 不得自批自审
|
||
"""
|
||
|
||
import logging
|
||
|
||
from .opp_common import (
|
||
get_db, new_id, rec_to_dict, rows_to_dicts, can_transition,
|
||
RP_DRAFT, RP_CONFIRMED, RP_APPROVAL_INITIATED, RP_APPROVED, RP_REJECTED,
|
||
AP_INITIATED, AP_APPROVED, AP_REJECTED,
|
||
HT_REPORT_CONFIRM, HT_DEV_APPROVAL,
|
||
find_human_task, create_human_task, get_report,
|
||
)
|
||
|
||
logger = logging.getLogger("pipeline.opportunity.report")
|
||
|
||
|
||
async def create_report(project_id, software, title="", analysis="",
|
||
created_by="agent.opportunity"):
|
||
"""创建研发报告草稿。返回 (True, report_id) 或 (False, 错误)。"""
|
||
if not software:
|
||
return False, "缺少 software(软件/主题名)"
|
||
if not title:
|
||
title = "%s 研发机会分析报告" % software
|
||
db, dbname = get_db()
|
||
rid = new_id()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
await sor.C("opp_reports", {
|
||
"id": rid,
|
||
"project_id": project_id or "",
|
||
"software": software,
|
||
"title": title,
|
||
"content": analysis or "",
|
||
"status": RP_DRAFT,
|
||
"created_by": created_by,
|
||
})
|
||
await sor.sqlExe("COMMIT", {})
|
||
return True, rid
|
||
|
||
|
||
async def update_report(report_id, content):
|
||
"""更新草稿报告内容(仅 draft 可改)。"""
|
||
db, dbname = get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
rep = await get_report(sor, report_id)
|
||
if not rep:
|
||
return False, "报告不存在: %s" % report_id
|
||
if rep.get("status") != RP_DRAFT:
|
||
return False, "状态 %s 不可修改(仅草稿可编辑)" % rep.get("status")
|
||
await sor.U("opp_reports", {"id": report_id, "content": content})
|
||
await sor.sqlExe("COMMIT", {})
|
||
return True, "已更新"
|
||
|
||
|
||
async def list_reports(project_id="", status=""):
|
||
db, dbname = get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
sql = ("SELECT id, project_id, software, title, status, created_by, "
|
||
"created_at, updated_at FROM opp_reports WHERE 1=1")
|
||
args = {}
|
||
if project_id:
|
||
sql += " AND project_id=${pid}$"
|
||
args["pid"] = project_id
|
||
if status:
|
||
sql += " AND status=${st}$"
|
||
args["st"] = status
|
||
sql += " ORDER BY created_at DESC LIMIT 50"
|
||
recs = await sor.sqlExe(sql, args)
|
||
await sor.sqlExe("COMMIT", {})
|
||
return rows_to_dicts(recs)
|
||
|
||
|
||
async def get_report_full(report_id):
|
||
db, dbname = get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
rep = await get_report(sor, report_id)
|
||
return rep
|
||
|
||
|
||
async def submit_for_confirmation(report_id, note=""):
|
||
"""agent 完成报告后提交待人工确认:发人工确认任务(门禁)。
|
||
|
||
报告保持 draft,创建/复用 pending 人工任务。人工确认后才进 confirmed。
|
||
"""
|
||
db, dbname = get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
rep = await get_report(sor, report_id)
|
||
if not rep:
|
||
return False, "报告不存在: %s" % report_id
|
||
if rep.get("status") != RP_DRAFT:
|
||
return False, "状态 %s 不可提交确认" % rep.get("status")
|
||
exist = await find_human_task(sor, rep.get("project_id", ""),
|
||
HT_REPORT_CONFIRM, status="pending")
|
||
if exist:
|
||
return True, "已有待确认任务: %s" % exist.get("id")
|
||
hid = await create_human_task(
|
||
sor, rep.get("project_id", ""), HT_REPORT_CONFIRM,
|
||
"确认研发报告:%s" % rep.get("title", ""),
|
||
note or "请审阅研发报告《%s》,确认后发起研发审批。" % rep.get("title", ""))
|
||
await sor.U("opp_reports", {
|
||
"id": report_id, "confirm_task_id": hid})
|
||
await sor.sqlExe("COMMIT", {})
|
||
return True, "已提交人工确认任务: %s" % hid
|
||
|
||
|
||
async def confirm_report(report_id, ok, operator=""):
|
||
"""人工确认回流(门禁)。ok=True → confirmed;ok=False → 退回草稿待改。"""
|
||
db, dbname = get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
rep = await get_report(sor, report_id)
|
||
if not rep:
|
||
return False, "报告不存在: %s" % report_id
|
||
if rep.get("status") != RP_DRAFT:
|
||
return False, "状态 %s 无法确认(仅草稿可确认)" % rep.get("status")
|
||
new_status = RP_CONFIRMED if ok else RP_DRAFT
|
||
await sor.U("opp_reports", {
|
||
"id": report_id, "status": new_status,
|
||
"confirmed_by": operator or "human"})
|
||
# 完结对应的人工任务
|
||
hid = rep.get("confirm_task_id")
|
||
if hid:
|
||
await sor.U("pipeline_human_tasks", {
|
||
"id": hid, "status": "done" if ok else "rejected"})
|
||
await sor.sqlExe("COMMIT", {})
|
||
return True, "已确认" if ok else "已退回草稿"
|
||
|
||
|
||
async def initiate_approval(report_id, note="", created_by="agent.opportunity"):
|
||
"""发起研发审批:要求报告已 confirmed(人工确认门禁)。"""
|
||
db, dbname = get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
rep = await get_report(sor, report_id)
|
||
if not rep:
|
||
return False, "报告不存在: %s" % report_id
|
||
if not await can_transition(rep.get("status"), RP_APPROVAL_INITIATED):
|
||
return False, "状态 %s 不可发起审批(须先人工确认)" % rep.get("status")
|
||
aid = new_id()
|
||
await sor.C("opp_approvals", {
|
||
"id": aid,
|
||
"report_id": report_id,
|
||
"project_id": rep.get("project_id", ""),
|
||
"status": AP_INITIATED,
|
||
"note": note,
|
||
"created_by": created_by,
|
||
})
|
||
await sor.U("opp_reports", {
|
||
"id": report_id, "status": RP_APPROVAL_INITIATED})
|
||
# 发人工审批任务
|
||
await create_human_task(
|
||
sor, rep.get("project_id", ""), HT_DEV_APPROVAL,
|
||
"研发审批:%s" % rep.get("title", ""),
|
||
note or "请审批《%s》的研发立项。" % rep.get("title", ""))
|
||
await sor.sqlExe("COMMIT", {})
|
||
return True, aid
|
||
|
||
|
||
async def resolve_approval(approval_id, result, operator=""):
|
||
"""审批结论回流(人工决策)。result=approved/rejected。"""
|
||
if result not in (AP_APPROVED, AP_REJECTED):
|
||
return False, "result 必须是 approved 或 rejected"
|
||
db, dbname = get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
recs = await sor.sqlExe(
|
||
"SELECT id, report_id, status FROM opp_approvals WHERE id=${i}$",
|
||
{"i": approval_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
if not recs:
|
||
return False, "审批单不存在: %s" % approval_id
|
||
rec = rec_to_dict(recs[0])
|
||
if rec.get("status") != AP_INITIATED:
|
||
return False, "审批单状态 %s 无法结论" % rec.get("status")
|
||
report_status = RP_APPROVED if result == AP_APPROVED else RP_REJECTED
|
||
await sor.U("opp_approvals", {
|
||
"id": approval_id, "status": result,
|
||
"resolved_by": operator or "human"})
|
||
await sor.U("opp_reports", {
|
||
"id": rec.get("report_id"), "status": report_status})
|
||
# 完结人工审批任务
|
||
ht = await find_human_task(sor, rec.get("project_id", ""),
|
||
HT_DEV_APPROVAL, status="pending")
|
||
if ht:
|
||
await sor.U("pipeline_human_tasks", {
|
||
"id": ht.get("id"),
|
||
"status": "done" if result == AP_APPROVED else "rejected"})
|
||
await sor.sqlExe("COMMIT", {})
|
||
return True, result
|
||
|
||
|
||
async def list_approvals(project_id="", status=""):
|
||
db, dbname = get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
sql = ("SELECT id, report_id, project_id, status, note, created_by, "
|
||
"created_at, resolved_at FROM opp_approvals WHERE 1=1")
|
||
args = {}
|
||
if project_id:
|
||
sql += " AND project_id=${pid}$"
|
||
args["pid"] = project_id
|
||
if status:
|
||
sql += " AND status=${st}$"
|
||
args["st"] = status
|
||
sql += " ORDER BY created_at DESC LIMIT 50"
|
||
recs = await sor.sqlExe(sql, args)
|
||
await sor.sqlExe("COMMIT", {})
|
||
return rows_to_dicts(recs)
|