feat: P1 补齐(自动评审队列、自动化+分诊、并发 caps、运行时审批队列)
This commit is contained in:
1 parent
29efc6c61f
commit
e0cecd70aa
18 files changed
+3474
-27
No files matched your search
+189
@@ -0,0 +1,189 @@
|
||||
"""自动代码评审:评审队列记录。
|
||||
|
||||
评审队列是服务端的一张登记表,不跑模型:调用方对某个任务
|
||||
发起评审时记一条 requested 记录,评审执行方(目前是
|
||||
host.py code-review,走协议 review/start)跑完后把 findings
|
||||
写回同一条记录,控制台评审区据此展示队列与结果。
|
||||
|
||||
与 peer_messages 同款分工:本模块只含纯表操作与协议目标
|
||||
构造,不 import cloud 其它模块,避免循环 import;服务端
|
||||
的表迁移委托到 ensure_reviews_table,失败不伪造成功。
|
||||
|
||||
协议锚点(protocol/codex_app_server_protocol.v2.schemas.json,
|
||||
逐个对过枚举,不编造):
|
||||
- 方法名 review/start,参数 ReviewStartParams:threadId 与
|
||||
target 必填,delivery 可空(inline 为默认,detached 已
|
||||
废弃,故本模块不产生 detached)。
|
||||
- ReviewTarget 四种:uncommittedChanges(工作区未提交改动)、
|
||||
baseBranch(须带 branch)、commit(须带 sha,title 可空)、
|
||||
custom(须带 instructions)。
|
||||
|
||||
状态机:requested(已登记,未执行)-> running(执行中,
|
||||
目前仅 host CLI 执行时可由调用方先置位,服务端不自动
|
||||
流转)-> completed / failed(写 findings 时落终态)。
|
||||
服务端不替执行方编造 findings:findings 为空时记录仍是
|
||||
requested,控制台如实显示“暂无 findings”。
|
||||
"""
|
||||
|
||||
import time
|
||||
import uuid
|
||||
|
||||
REVIEW_TARGET_TYPES = (
|
||||
"uncommittedChanges", "baseBranch", "commit", "custom")
|
||||
REVIEW_STATUSES = ("requested", "running", "completed", "failed")
|
||||
|
||||
_REVIEW_COLUMNS = (
|
||||
"id", "task_id", "project_id", "target_type", "branch", "sha",
|
||||
"instructions", "status", "findings", "created_by",
|
||||
"created_at", "updated_at")
|
||||
|
||||
|
||||
def build_review_target(target_type, branch=None, sha=None,
|
||||
instructions=None):
|
||||
"""按协议构造 ReviewTarget dict;组合非法时 ValueError,
|
||||
调用方转 400,绝不把非法目标发给 app-server。"""
|
||||
if target_type == "uncommittedChanges":
|
||||
return {"type": "uncommittedChanges"}
|
||||
if target_type == "baseBranch":
|
||||
if not branch or not isinstance(branch, str) or not branch.strip():
|
||||
raise ValueError("baseBranch target requires branch")
|
||||
return {"type": "baseBranch", "branch": branch.strip()}
|
||||
if target_type == "commit":
|
||||
if not sha or not isinstance(sha, str) or not sha.strip():
|
||||
raise ValueError("commit target requires sha")
|
||||
return {"type": "commit", "sha": sha.strip()}
|
||||
if target_type == "custom":
|
||||
if not instructions or not isinstance(instructions, str) \
|
||||
or not instructions.strip():
|
||||
raise ValueError("custom target requires instructions")
|
||||
return {"type": "custom", "instructions": instructions.strip()}
|
||||
raise ValueError("unknown target_type: %r" % (target_type,))
|
||||
|
||||
|
||||
def ensure_reviews_table(conn):
|
||||
"""幂等建评审队列表。调用方(服务端接口)在每次读写前
|
||||
调它,老库第一次跑到这里时自动补表,不丢数据。"""
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS task_reviews("
|
||||
"id TEXT PRIMARY KEY, "
|
||||
"task_id TEXT, "
|
||||
"project_id TEXT, "
|
||||
"target_type TEXT, "
|
||||
"branch TEXT, "
|
||||
"sha TEXT, "
|
||||
"instructions TEXT, "
|
||||
"status TEXT, "
|
||||
"findings TEXT, "
|
||||
"created_by TEXT, "
|
||||
"created_at DOUBLE PRECISION, "
|
||||
"updated_at DOUBLE PRECISION)"
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_reviews_task"
|
||||
" ON task_reviews(task_id, created_at)")
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_reviews_project"
|
||||
" ON task_reviews(project_id, created_at)")
|
||||
|
||||
|
||||
def _row_to_dict(row):
|
||||
"""行转 dict:兼容 dict 行、sqlite3.Row 与裸元组(按
|
||||
_REVIEW_COLUMNS 的 SELECT 顺序)。"""
|
||||
if row is None:
|
||||
return None
|
||||
if isinstance(row, dict):
|
||||
return {c: row.get(c) for c in _REVIEW_COLUMNS}
|
||||
try:
|
||||
keys = row.keys()
|
||||
except AttributeError:
|
||||
keys = None
|
||||
if keys is not None:
|
||||
d = dict(row)
|
||||
return {c: d.get(c) for c in _REVIEW_COLUMNS}
|
||||
return dict(zip(_REVIEW_COLUMNS, row))
|
||||
|
||||
|
||||
def _select_cols():
|
||||
return ", ".join(_REVIEW_COLUMNS)
|
||||
|
||||
|
||||
def create_review(conn, task_id, project_id, target_type, created_by,
|
||||
branch=None, sha=None, instructions=None):
|
||||
"""登记一条评审,返回记录 dict。先校验目标组合(非法
|
||||
时 ValueError,调用方转 400,表里不留脏行),成功才
|
||||
commit。"""
|
||||
build_review_target(target_type, branch, sha, instructions)
|
||||
ensure_reviews_table(conn)
|
||||
now = time.time()
|
||||
review_id = uuid.uuid4().hex
|
||||
conn.execute(
|
||||
"INSERT INTO task_reviews(id, task_id, project_id,"
|
||||
" target_type, branch, sha, instructions, status,"
|
||||
" findings, created_by, created_at, updated_at)"
|
||||
" VALUES(?, ?, ?, ?, ?, ?, ?, 'requested', NULL, ?, ?, ?)",
|
||||
(review_id, task_id, project_id, target_type,
|
||||
branch.strip() if isinstance(branch, str) else None,
|
||||
sha.strip() if isinstance(sha, str) else None,
|
||||
instructions.strip() if isinstance(instructions, str) else None,
|
||||
created_by, now, now),
|
||||
)
|
||||
conn.commit()
|
||||
return get_review(conn, review_id)
|
||||
|
||||
|
||||
def list_reviews(conn, task_id):
|
||||
"""某任务的评审队列,按创建时间升序(先登记的先审)。"""
|
||||
ensure_reviews_table(conn)
|
||||
rows = conn.execute(
|
||||
"SELECT %s FROM task_reviews WHERE task_id = ?"
|
||||
" ORDER BY created_at, id" % _select_cols(),
|
||||
(task_id,)).fetchall()
|
||||
return [_row_to_dict(r) for r in rows]
|
||||
|
||||
|
||||
def list_project_reviews(conn, project_id):
|
||||
"""某项目的评审队列(供未来项目级队列端点复用;本批
|
||||
服务端只暴露任务级队列,函数先备好不接线)。"""
|
||||
ensure_reviews_table(conn)
|
||||
rows = conn.execute(
|
||||
"SELECT %s FROM task_reviews WHERE project_id = ?"
|
||||
" ORDER BY created_at, id" % _select_cols(),
|
||||
(project_id,)).fetchall()
|
||||
return [_row_to_dict(r) for r in rows]
|
||||
|
||||
|
||||
def get_review(conn, review_id):
|
||||
"""取单条评审记录,不存在时返回 None(调用方转 404)。"""
|
||||
ensure_reviews_table(conn)
|
||||
row = conn.execute(
|
||||
"SELECT %s FROM task_reviews WHERE id = ?" % _select_cols(),
|
||||
(review_id,)).fetchone()
|
||||
return _row_to_dict(row)
|
||||
|
||||
|
||||
def update_review(conn, review_id, status=None, findings=None):
|
||||
"""更新评审状态与 findings,返回更新后记录,不存在时
|
||||
返回 None。status 非法时 ValueError(调用方转 400);
|
||||
findings 为字符串时照存(空串也存,区别于未写回的
|
||||
NULL);不传 status 时 findings 写回默认落 completed,
|
||||
即“有结果即完成”,调用方可显式传 failed 覆盖。"""
|
||||
if status is not None and status not in REVIEW_STATUSES:
|
||||
raise ValueError("unknown review status: %r" % (status,))
|
||||
if findings is not None and not isinstance(findings, str):
|
||||
raise ValueError("findings must be a string")
|
||||
ensure_reviews_table(conn)
|
||||
record = get_review(conn, review_id)
|
||||
if record is None:
|
||||
return None
|
||||
new_status = status
|
||||
if new_status is None:
|
||||
new_status = "completed" if findings is not None \
|
||||
else record["status"]
|
||||
new_findings = findings if findings is not None \
|
||||
else record["findings"]
|
||||
conn.execute(
|
||||
"UPDATE task_reviews SET status = ?, findings = ?,"
|
||||
" updated_at = ? WHERE id = ?",
|
||||
(new_status, new_findings, time.time(), review_id))
|
||||
conn.commit()
|
||||
return get_review(conn, review_id)
|
||||
Reference in new issue
Block a user