124 lines
4.1 KiB
Python
124 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
||
"""人审工作台:只读 muse.db,写面仅 reviews / revisions / adopt。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import sys
|
||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||
from pathlib import Path
|
||
from urllib.parse import parse_qs, urlparse
|
||
|
||
PROJECT_ROOT = next(
|
||
parent
|
||
for parent in (Path(__file__).resolve().parent, *Path(__file__).resolve().parents)
|
||
if (parent / "AGENTS.md").is_file() and (parent / ".git").exists()
|
||
)
|
||
if str(PROJECT_ROOT) not in sys.path:
|
||
sys.path.insert(0, str(PROJECT_ROOT))
|
||
|
||
from muse.flow.adopt import adopt_candidate # noqa: E402
|
||
from muse.store import add_review, connect, default_db_path # noqa: E402
|
||
|
||
|
||
WRITE_ACTIONS = frozenset({"reviews", "revisions", "adopt"})
|
||
|
||
|
||
def _db():
|
||
return connect(default_db_path())
|
||
|
||
|
||
def queue_payload() -> list[dict]:
|
||
with _db() as conn:
|
||
rows = conn.execute(
|
||
"SELECT id, kind, created_at, output_text FROM runs ORDER BY created_at DESC LIMIT 50"
|
||
).fetchall()
|
||
return [dict(row) for row in rows]
|
||
|
||
|
||
def adopt(run_id: str, reviewer: str, reason: str) -> int:
|
||
"""采纳必须调用 muse.flow,不在 web 层直接写 reviews。"""
|
||
|
||
return adopt_candidate(run_id, reviewer, reason)
|
||
|
||
|
||
def revise(run_id: str, reviewer: str, reason: str, before_text: str, after_text: str) -> int:
|
||
return add_review(
|
||
run_id=run_id,
|
||
target="candidate",
|
||
action="revise",
|
||
reviewer=reviewer,
|
||
reason=reason,
|
||
before_text=before_text,
|
||
after_text=after_text,
|
||
)
|
||
|
||
|
||
class Handler(BaseHTTPRequestHandler):
|
||
def _send(self, code: int, body: str, content_type: str = "text/html; charset=utf-8") -> None:
|
||
data = body.encode("utf-8")
|
||
self.send_response(code)
|
||
self.send_header("Content-Type", content_type)
|
||
self.send_header("Content-Length", str(len(data)))
|
||
self.end_headers()
|
||
self.wfile.write(data)
|
||
|
||
def do_GET(self) -> None: # noqa: N802
|
||
parsed = urlparse(self.path)
|
||
if parsed.path in {"/", "/queue"}:
|
||
items = queue_payload()
|
||
rows = "".join(
|
||
f"<li><a href='/review/{item['id']}'>{item['id']}</a> {item['kind']}</li>"
|
||
for item in items
|
||
)
|
||
self._send(200, f"<main><h1>待审队列</h1><ul>{rows}</ul></main>")
|
||
return
|
||
if parsed.path.startswith("/review/"):
|
||
run_id = parsed.path.rsplit("/", 1)[-1]
|
||
self._send(200, f"<main><h1>审查 {run_id}</h1><pre tabindex='0'></pre></main>")
|
||
return
|
||
self._send(404, "not found")
|
||
|
||
def do_POST(self) -> None: # noqa: N802
|
||
parsed = urlparse(self.path)
|
||
length = int(self.headers.get("Content-Length") or 0)
|
||
raw = self.rfile.read(length).decode("utf-8") if length else ""
|
||
form = {k: v[0] for k, v in parse_qs(raw).items()}
|
||
action = parsed.path.strip("/")
|
||
if action not in WRITE_ACTIONS:
|
||
self._send(404, "unknown write")
|
||
return
|
||
reviewer = form.get("reviewer") or ""
|
||
run_id = form.get("run_id") or ""
|
||
reason = form.get("reason") or ""
|
||
if action == "adopt":
|
||
review_id = adopt(run_id, reviewer, reason)
|
||
elif action == "revisions" or (action == "reviews" and form.get("action") == "revise"):
|
||
review_id = revise(
|
||
run_id,
|
||
reviewer,
|
||
reason,
|
||
form.get("before_text") or "",
|
||
form.get("after_text") or "",
|
||
)
|
||
else:
|
||
review_id = add_review(
|
||
run_id=run_id,
|
||
target=form.get("target") or "candidate",
|
||
action=form.get("action") or "reject",
|
||
reviewer=reviewer,
|
||
reason=reason,
|
||
)
|
||
self._send(200, json.dumps({"reviewId": review_id}), "application/json")
|
||
|
||
def log_message(self, fmt: str, *args) -> None:
|
||
return
|
||
|
||
|
||
def serve(host: str = "127.0.0.1", port: int = 8770) -> None:
|
||
ThreadingHTTPServer((host, port), Handler).serve_forever()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
serve()
|