190 lines
7.5 KiB
Python
190 lines
7.5 KiB
Python
"""自动代码评审:评审队列记录。
|
||
|
||
评审队列是服务端的一张登记表,不跑模型:调用方对某个任务
|
||
发起评审时记一条 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)
|