feat: P1 补齐(自动评审队列、自动化+分诊、并发 caps、运行时审批队列)

This commit is contained in:
host committed 2026-10-27 15:53:50 +08:00
1 parent 29efc6c61f
commit e0cecd70aa
18 files changed
+3474 -27

No files matched your search

+189
View File
@@ -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)