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