498 lines
21 KiB
Python
498 lines
21 KiB
Python
"""SDLC 项目级人类任务清单 capability。
|
||
|
||
人类待办三类来源(统一视图,见 list_my_human_todos / count_my_human_tasks):
|
||
- general 显式人类任务 → 落 pipeline_human_tasks(owner/PM 派发)
|
||
- bug_acceptance bug 验收任务 → 落 pipeline_human_tasks(bug_flow 在 verified 时创建)
|
||
- question 冒泡问题 → 不落此表,union 查 pipeline_agent_questions
|
||
|
||
处理权限:处理者须同机构(org_id 匹配)+ 匹配 assignee_role 或 assignee_id。
|
||
|
||
状态:status(pending/done/rejected) + qc_status(pending/passed/rejected,仅 general 走 QC)。
|
||
"""
|
||
|
||
import json
|
||
import logging
|
||
|
||
from sqlor.dbpools import DBPools
|
||
from appPublic.uniqueID import getID
|
||
|
||
DBNAME = "pipeline"
|
||
logger = logging.getLogger("pipeline.human_task_capability")
|
||
|
||
S_PENDING = "pending"
|
||
S_DONE = "done"
|
||
S_REJECTED = "rejected"
|
||
|
||
QC_PENDING = "pending"
|
||
QC_PASSED = "passed"
|
||
QC_REJECTED = "rejected"
|
||
|
||
T_GENERAL = "general"
|
||
T_BUG_ACCEPT = "bug_acceptance"
|
||
T_REQ_CONFIRM = "requirement_confirmation"
|
||
T_DESIGN_CONFIRM = "design_confirmation"
|
||
|
||
|
||
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 _rec_to_dict(rec):
|
||
if rec is None:
|
||
return {}
|
||
if isinstance(rec, dict):
|
||
return dict(rec)
|
||
try:
|
||
return dict(rec)
|
||
except (TypeError, ValueError):
|
||
return {}
|
||
|
||
|
||
async def _get_user_org(sor, user_id):
|
||
"""查用户 org_id。"""
|
||
if not user_id:
|
||
return ""
|
||
recs = await sor.sqlExe(
|
||
"SELECT orgid FROM users WHERE id=${u}$ LIMIT 1", {"u": user_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
return getattr(recs[0], 'orgid', '') if recs else ''
|
||
|
||
|
||
async def _get_user_roles(sor, user_id):
|
||
"""查用户 RBAC 角色名列表({orgtypeid}.{name} 格式)。"""
|
||
roles = []
|
||
if not user_id:
|
||
return roles
|
||
recs = await sor.sqlExe(
|
||
"SELECT r.orgtypeid, r.name FROM userrole ur JOIN role r ON ur.roleid=r.id "
|
||
"WHERE ur.userid=${u}$", {"u": user_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
for r in (recs or []):
|
||
o = getattr(r, 'orgtypeid', '') or ''
|
||
n = getattr(r, 'name', '') or ''
|
||
if o and n:
|
||
roles.append(f"{o}.{n}")
|
||
return roles
|
||
|
||
|
||
async def create_human_task(project_id, title, description="", task_type=T_GENERAL,
|
||
assignee_role=None, assignee_id=None,
|
||
iteration_id=None, bug_id=None, created_by=None, task_id="",
|
||
form_schema=None):
|
||
"""派发项目级人类任务。assignee_role 或 assignee_id 至少指定一个。
|
||
|
||
task_id:确认类任务绑定原引擎任务 id(用于确认后继续派发/退回重做)。
|
||
form_schema:动态表单声明(可选,与问题通道同格式)——待办详情按它渲染
|
||
上传/填写界面,人类在待办里直接完成任务(文件落项目工作空间,路径进
|
||
result_data 供 QC 与任务链消费)。
|
||
返回 (True, human_task_id) 或 (False, 错误信息)
|
||
"""
|
||
if not project_id:
|
||
return False, "缺少 project_id"
|
||
if not title or not title.strip():
|
||
return False, "缺少标题"
|
||
if not assignee_role and not assignee_id:
|
||
return False, "须指定 assignee_role 或 assignee_id"
|
||
db, dbname = _get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
# 项目存在性 + 迭代(未显式传时取当前迭代)
|
||
proj = await sor.sqlExe(
|
||
"SELECT id, org_id FROM sd_projects WHERE id=${pid}$", {"pid": project_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
if not proj:
|
||
return False, "项目不存在"
|
||
if not iteration_id:
|
||
it = await sor.sqlExe(
|
||
"SELECT id FROM sd_iterations WHERE project_id=${pid}$ "
|
||
"AND status='in_progress' ORDER BY seq_no DESC LIMIT 1",
|
||
{"pid": project_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
iteration_id = getattr(it[0], 'id', '') if it else ''
|
||
|
||
hid = getID()
|
||
_fs_json = json.dumps(form_schema, ensure_ascii=False) if form_schema else None
|
||
await sor.C('pipeline_human_tasks', {
|
||
'id': hid,
|
||
'task_id': task_id or '', # 确认类任务绑定原引擎任务 id
|
||
'step_name': '', # 同上
|
||
'version': 1,
|
||
'task_type': task_type or T_GENERAL,
|
||
'assignee_role': assignee_role or '',
|
||
'assignee_id': assignee_id or '',
|
||
'form_schema': _fs_json,
|
||
'result_data': None,
|
||
'status': S_PENDING,
|
||
'submitted_by': created_by or '',
|
||
'project_id': project_id,
|
||
'iteration_id': iteration_id or '',
|
||
'bug_id': bug_id or '',
|
||
'qc_status': QC_PENDING,
|
||
'title': title.strip(),
|
||
'description': description or '',
|
||
})
|
||
await sor.sqlExe("COMMIT", {})
|
||
logger.info("create_human_task: %s project=%s type=%s title=%s",
|
||
hid, project_id, task_type, title.strip())
|
||
return True, hid
|
||
|
||
|
||
async def confirm_stage_gate(human_task_id, confirmed, comment, operator_id):
|
||
"""需求/设计人工确认节点(owner 确认门禁)。
|
||
|
||
confirmed=True → 确认通过:任务 done + 创建下一角色任务(继续任务链)。
|
||
confirmed=False → 不确定:任务 rejected + 退回原角色重做(带修改意见)。
|
||
|
||
确认任务由 pm_review_run 在 requirement/design 审核通过后创建,
|
||
task_id 字段存原引擎任务 id(用于此处继续/退回)。仅 owner 可确认。
|
||
"""
|
||
if not human_task_id:
|
||
return False, "缺少 human_task_id"
|
||
if not operator_id:
|
||
return False, "未登录"
|
||
db, dbname = _get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
recs = await sor.sqlExe(
|
||
"SELECT * FROM pipeline_human_tasks WHERE id=${hid}$", {"hid": human_task_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
if not recs:
|
||
return False, "确认任务不存在"
|
||
ht = _rec_to_dict(recs[0])
|
||
task_type = ht.get('task_type', '') or ''
|
||
if task_type not in (T_REQ_CONFIRM, T_DESIGN_CONFIRM):
|
||
return False, "该任务不是需求/设计确认任务"
|
||
if ht.get('status') != S_PENDING:
|
||
return False, f"任务不在待确认状态 (当前: {ht.get('status')})"
|
||
|
||
# owner 校验:确认任务 assignee_id=owner,须等于本人
|
||
assignee_id = ht.get('assignee_id', '') or ''
|
||
if assignee_id and operator_id != assignee_id:
|
||
return False, "仅项目 owner 可确认"
|
||
|
||
pid = ht.get('project_id', '')
|
||
original_task_id = ht.get('task_id', '') or ''
|
||
result_json = json.dumps({"confirmed": bool(confirmed), "comment": comment or ''},
|
||
ensure_ascii=False)
|
||
|
||
if confirmed:
|
||
await sor.sqlExe(
|
||
"UPDATE pipeline_human_tasks SET status='done', result_data=${rd}$, "
|
||
"submitted_by=${oid}$, submitted_at=NOW() WHERE id=${hid}$",
|
||
{"rd": result_json, "oid": operator_id, "hid": human_task_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
if original_task_id:
|
||
from .agent_loop import _create_next_task, _get_next_role
|
||
trecs = await sor.R('pipeline_tasks', {'id': original_task_id})
|
||
if trecs:
|
||
t = trecs[0]
|
||
nrole = await _get_next_role(getattr(t, 'role', ''), pid)
|
||
if nrole:
|
||
next_tid, next_title = await _create_next_task(
|
||
sor, pid, t, nrole, comment or "owner 确认通过")
|
||
await sor.sqlExe("COMMIT", {})
|
||
return True, f"已确认,继续派发 {nrole}"
|
||
return True, "已确认"
|
||
else:
|
||
await sor.sqlExe(
|
||
"UPDATE pipeline_human_tasks SET status='rejected', result_data=${rd}$, "
|
||
"submitted_by=${oid}$, submitted_at=NOW() WHERE id=${hid}$",
|
||
{"rd": result_json, "oid": operator_id, "hid": human_task_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
if original_task_id:
|
||
from .agent_loop import _rollback_task_chain
|
||
trecs = await sor.R('pipeline_tasks', {'id': original_task_id})
|
||
if trecs:
|
||
t = trecs[0]
|
||
role = getattr(t, 'role', '')
|
||
r = await _rollback_task_chain(
|
||
sor, pid, original_task_id, role,
|
||
comment or "owner 确认不通过,退回重做")
|
||
await sor.sqlExe("COMMIT", {})
|
||
return True, f"已退回重做: {r.get('new_task_id', '')}"
|
||
return True, "已退回重做"
|
||
|
||
|
||
async def complete_human_task(human_task_id, result_data, operator_id=None):
|
||
"""处理者提交完成(done)。校验同机构 + 匹配 assignee。
|
||
|
||
general 任务 done 后创建 qc_review 的 QC 任务(复用 qc poller → qc_review_run)。
|
||
bug_acceptance 任务不走这里(走 bug_accept)。
|
||
"""
|
||
if not human_task_id:
|
||
return False, "缺少 human_task_id"
|
||
if not operator_id:
|
||
return False, "未登录"
|
||
db, dbname = _get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
recs = await sor.sqlExe(
|
||
"SELECT * FROM pipeline_human_tasks WHERE id=${hid}$", {"hid": human_task_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
if not recs:
|
||
return False, "人类任务不存在"
|
||
ht = _rec_to_dict(recs[0])
|
||
if ht.get('status') != S_PENDING:
|
||
return False, f"任务不在待处理状态 (当前: {ht.get('status')})"
|
||
|
||
# 同机构校验
|
||
pid = ht.get('project_id', '')
|
||
proj = await sor.sqlExe(
|
||
"SELECT org_id FROM sd_projects WHERE id=${pid}$", {"pid": pid})
|
||
await sor.sqlExe("COMMIT", {})
|
||
proj_org = getattr(proj[0], 'org_id', '') if proj else ''
|
||
user_org = await _get_user_org(sor, operator_id)
|
||
if proj_org and user_org != proj_org:
|
||
return False, "仅同机构用户可处理该任务"
|
||
|
||
# 匹配 assignee:assignee_id 指定则须等于本人;assignee_role 指定则本人角色须含之
|
||
assignee_id = ht.get('assignee_id', '') or ''
|
||
assignee_role = ht.get('assignee_role', '') or ''
|
||
if assignee_id:
|
||
if operator_id != assignee_id:
|
||
return False, "该任务未指派给你"
|
||
elif assignee_role:
|
||
roles = await _get_user_roles(sor, operator_id)
|
||
if assignee_role not in roles:
|
||
return False, "该任务未指派给你的角色"
|
||
|
||
task_type = ht.get('task_type', '') or T_GENERAL
|
||
result_json = json.dumps(result_data, ensure_ascii=False, default=str) \
|
||
if isinstance(result_data, (dict, list)) else str(result_data or '')
|
||
|
||
if task_type == T_BUG_ACCEPT:
|
||
return False, "bug 验收任务请走 bug_accept 接口"
|
||
|
||
# general:done + 触发 QC(创建 qc_review 任务)
|
||
await sor.sqlExe(
|
||
"UPDATE pipeline_human_tasks SET status='done', result_data=${rd}$, "
|
||
"submitted_by=${oid}$, submitted_at=NOW() WHERE id=${hid}$",
|
||
{"rd": result_json, "oid": operator_id, "hid": human_task_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
|
||
# 创建 QC 任务(复用 qc poller → qc_review_run)
|
||
qid = getID()
|
||
qparams = {
|
||
"task_kind": "human_task_qc",
|
||
"human_task_id": human_task_id,
|
||
"title": ht.get('title', '') or '',
|
||
"description": (ht.get('description', '') or '')[:2000],
|
||
"iteration_id": ht.get('iteration_id', '') or '',
|
||
}
|
||
# 补 title(pipeline_human_tasks 有 title 列,已在上方 result 里带)
|
||
await sor.C('pipeline_tasks', {
|
||
'id': qid, 'tenant_id': pid, 'pipeline_id': 'role_task',
|
||
'owner_id': 'human_task_qc',
|
||
'title': f"人类任务 QC 检查({human_task_id[:8]})",
|
||
'state': 'qc_review', 'role': 'agent.qc',
|
||
'params': json.dumps(qparams, ensure_ascii=False),
|
||
})
|
||
await sor.sqlExe("COMMIT", {})
|
||
return True, human_task_id
|
||
|
||
|
||
async def qc_human_task(human_task_id, passed, comment=None, operator_id=None):
|
||
"""QC 检查人类任务(general)。passed → qc_status=passed;否则 qc_status=rejected + status=pending 退回。"""
|
||
if not human_task_id:
|
||
return False, "缺少 human_task_id"
|
||
db, dbname = _get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
recs = await sor.sqlExe(
|
||
"SELECT status FROM pipeline_human_tasks WHERE id=${hid}$", {"hid": human_task_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
if not recs:
|
||
return False, "人类任务不存在"
|
||
if passed:
|
||
await sor.sqlExe(
|
||
"UPDATE pipeline_human_tasks SET qc_status='passed', qc_comment=${c}$ "
|
||
"WHERE id=${hid}$", {"c": comment or '', "hid": human_task_id})
|
||
else:
|
||
# 退回重做:status 回 pending,qc_status=rejected
|
||
await sor.sqlExe(
|
||
"UPDATE pipeline_human_tasks SET status='pending', qc_status='rejected', "
|
||
"qc_comment=${c}$ WHERE id=${hid}$", {"c": comment or '', "hid": human_task_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
return True, human_task_id
|
||
|
||
|
||
async def list_project_human_tasks(project_id, status=None, assignee_id=None):
|
||
"""列出项目人类任务(显式任务 + bug 验收,不含冒泡问题)。"""
|
||
db, dbname = _get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
conds = ["project_id=${pid}$"]
|
||
params = {"pid": project_id}
|
||
if status:
|
||
conds.append("status=${st}$")
|
||
params["st"] = status
|
||
if assignee_id:
|
||
conds.append("assignee_id=${aid}$")
|
||
params["aid"] = assignee_id
|
||
where = " AND ".join(conds)
|
||
recs = await sor.sqlExe(
|
||
f"SELECT * FROM pipeline_human_tasks WHERE {where} ORDER BY created_at DESC",
|
||
params)
|
||
await sor.sqlExe("COMMIT", {})
|
||
return [_rec_to_dict(r) for r in (recs or [])]
|
||
|
||
|
||
async def has_blocking_human_task(sor, project_id):
|
||
"""当前迭代是否有未完成的人类任务(流转门禁用)。
|
||
|
||
general:status=pending 或 status=done 但 qc_status!=passed → 阻塞
|
||
bug_acceptance:status=pending → 阻塞(done/rejected = 已验收出结果,不阻塞)
|
||
"""
|
||
if not project_id:
|
||
return False
|
||
it = await sor.sqlExe(
|
||
"SELECT id FROM sd_iterations WHERE project_id=${pid}$ AND status='in_progress' "
|
||
"ORDER BY seq_no DESC LIMIT 1", {"pid": project_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
if not it:
|
||
return False
|
||
iid = getattr(it[0], 'id', '') or ''
|
||
if not iid:
|
||
return False
|
||
recs = await sor.sqlExe(
|
||
"SELECT COUNT(*) AS c FROM pipeline_human_tasks "
|
||
"WHERE iteration_id=${iid}$ AND ("
|
||
" (task_type='general' AND (status='pending' OR (status='done' AND qc_status != 'passed'))) "
|
||
" OR (task_type='bug_acceptance' AND status='pending')"
|
||
" OR (task_type IN ('requirement_confirmation','design_confirmation') AND status='pending')"
|
||
")",
|
||
{"iid": iid})
|
||
await sor.sqlExe("COMMIT", {})
|
||
c = getattr(recs[0], 'c', 0) if recs else 0
|
||
return int(c) > 0
|
||
|
||
|
||
async def count_my_human_tasks(user_id, project_id=None):
|
||
"""角标 X = pending 问题(agentid=我) + 人类任务(assignee_id=我) + 人类任务(assignee_role∈我角色)。"""
|
||
if not user_id:
|
||
return 0
|
||
db, dbname = _get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
# ① pending 问题 current_handler_agentid = 我
|
||
q_cond = "status='pending' AND current_handler_agentid=${u}$"
|
||
q_params = {"u": user_id}
|
||
if project_id:
|
||
q_cond += " AND tenant_id=${pid}$"
|
||
q_params["pid"] = project_id
|
||
q = await sor.sqlExe(
|
||
f"SELECT COUNT(*) AS c FROM pipeline_agent_questions WHERE {q_cond}", q_params)
|
||
await sor.sqlExe("COMMIT", {})
|
||
|
||
# ② + ③ 人类任务(assignee_id=我 或 assignee_role∈我角色)
|
||
roles = await _get_user_roles(sor, user_id)
|
||
ht_cond = "status='pending' AND (assignee_id=${u}$"
|
||
ht_params = {"u": user_id}
|
||
if roles:
|
||
ph = ",".join(f"'{r}'" for r in roles)
|
||
ht_cond += f" OR assignee_role IN ({ph})"
|
||
ht_cond += ")"
|
||
if project_id:
|
||
ht_cond += " AND project_id=${pid}$"
|
||
ht_params["pid"] = project_id
|
||
ht = await sor.sqlExe(
|
||
f"SELECT COUNT(*) AS c FROM pipeline_human_tasks WHERE {ht_cond}", ht_params)
|
||
await sor.sqlExe("COMMIT", {})
|
||
|
||
n_q = getattr(q[0], 'c', 0) if q else 0
|
||
n_ht = getattr(ht[0], 'c', 0) if ht else 0
|
||
return int(n_q) + int(n_ht)
|
||
|
||
|
||
async def list_my_human_todos(user_id, limit=100):
|
||
"""跨项目「我的待办」列表:union 人类任务 + 冒泡问题,按创建时间倒序。"""
|
||
if not user_id:
|
||
return []
|
||
db, dbname = _get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
roles = await _get_user_roles(sor, user_id)
|
||
role_ph = ",".join(f"'{r}'" for r in roles) if roles else "''"
|
||
|
||
# 人类任务:assignee_id=我 或 assignee_role∈我角色,pending
|
||
ht = await sor.sqlExe(
|
||
f"SELECT id, project_id, iteration_id, bug_id, task_type, assignee_role, "
|
||
f"assignee_id, status, qc_status, created_at, submitted_by, result_data, "
|
||
f"title, description "
|
||
f"FROM pipeline_human_tasks "
|
||
f"WHERE status='pending' AND (assignee_id=${{u}}$ OR assignee_role IN ({role_ph})) "
|
||
f"ORDER BY created_at DESC LIMIT {limit}", {"u": user_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
|
||
# 冒泡问题:current_handler_agentid=我 或 current_handler_role∈我角色,pending
|
||
qs = await sor.sqlExe(
|
||
f"SELECT id, tenant_id AS project_id, task_id, from_role, question, "
|
||
f"problem_type, current_handler_role, current_handler_agentid, created_at "
|
||
f"FROM pipeline_agent_questions "
|
||
f"WHERE status='pending' AND (current_handler_agentid=${{u}}$ "
|
||
f"OR current_handler_role IN ({role_ph})) "
|
||
f"ORDER BY created_at DESC LIMIT {limit}", {"u": user_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
|
||
todos = []
|
||
for r in (ht or []):
|
||
d = _rec_to_dict(r)
|
||
d['source'] = 'human_task'
|
||
todos.append(d)
|
||
for r in (qs or []):
|
||
d = _rec_to_dict(r)
|
||
d['source'] = 'question'
|
||
todos.append(d)
|
||
todos.sort(key=lambda x: str(x.get('created_at') or ''), reverse=True)
|
||
return todos[:limit]
|
||
|
||
|
||
async def bug_accept(bug_id, accept, user_id, comment=None):
|
||
"""bug 验收:accept=True → bug closed + 验收任务 done;accept=False → bug reopen + 任务 rejected。
|
||
|
||
校验 user_id == bug.reporter_id。返回 (True, 消息) 或 (False, 错误)。
|
||
"""
|
||
if not bug_id:
|
||
return False, "缺少 bug_id"
|
||
if not user_id:
|
||
return False, "未登录"
|
||
db, dbname = _get_db()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
brecs = await sor.sqlExe(
|
||
"SELECT reporter_id, iteration_id, status FROM sd_bugs WHERE id=${bid}$",
|
||
{"bid": bug_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
if not brecs:
|
||
return False, "Bug 不存在"
|
||
reporter_id = getattr(brecs[0], 'reporter_id', '') or ''
|
||
iteration_id = getattr(brecs[0], 'iteration_id', '') or ''
|
||
if reporter_id and user_id != reporter_id:
|
||
return False, "仅 Bug 报告人可验收"
|
||
|
||
if accept:
|
||
from . import bug_capability
|
||
ok, msg = await bug_capability.close_bug(bug_id, iteration_id, who='human')
|
||
if not ok:
|
||
return False, f"关闭失败: {msg}"
|
||
# 验收任务 → done
|
||
await sor.sqlExe(
|
||
"UPDATE pipeline_human_tasks SET status='done', result_data=${c}$, "
|
||
"submitted_by=${u}$, submitted_at=NOW() "
|
||
"WHERE bug_id=${bid}$ AND task_type='bug_acceptance' AND status='pending'",
|
||
{"c": json.dumps({"accept": True, "comment": comment or ''}, ensure_ascii=False),
|
||
"u": user_id, "bid": bug_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
return True, "验收通过,Bug 已关闭"
|
||
else:
|
||
from . import bug_capability
|
||
ok, msg = await bug_capability.reopen_bug(bug_id, iteration_id, who='human')
|
||
if not ok:
|
||
return False, f"重开失败: {msg}"
|
||
await sor.sqlExe(
|
||
"UPDATE pipeline_human_tasks SET status='rejected', result_data=${c}$, "
|
||
"submitted_by=${u}$, submitted_at=NOW() "
|
||
"WHERE bug_id=${bid}$ AND task_type='bug_acceptance' AND status='pending'",
|
||
{"c": json.dumps({"accept": False, "comment": comment or ''}, ensure_ascii=False),
|
||
"u": user_id, "bid": bug_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
return True, "验收不通过,Bug 已重新打开"
|