待办:项目待办模型 + /projects/{id}/todos 接口 + 控制台待办页

This commit is contained in:
host committed 2026-10-30 05:11:06 +08:00
1 parent c10fda4e4a
commit b3b3b35042
8 files changed
+962 -23

No files matched your search

+164
View File
@@ -104,6 +104,11 @@ from cloud.wait_ops import report_handoff as _handoff_report
from cloud.wait_ops import worktree_status as _wait_worktree_status
from cloud.automations import ensure_automations_tables as _auto_ensure
from cloud.automations import list_automations as _auto_list
from cloud.todos import create_todo as _todo_create
from cloud.todos import delete_todo as _todo_delete
from cloud.todos import get_todo as _todo_get
from cloud.todos import list_todos as _todo_list
from cloud.todos import update_todo as _todo_update
from cloud.automations import triage_inbox as _auto_triage
from cloud.approvals import create_approval as _appr_create
from cloud.approvals import decide_approval as _appr_decide
@@ -3320,6 +3325,45 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
return
self._triage_inbox(project_id)
return
# /projects/{id}/todos GET 列表 / POST 新建;单条
# GET / PATCH / DELETE(项目 read / create_task)
if len(parts) == 2 and parts[1] == "todos":
if method == "GET":
err = self._check_project(user_id, project_id, "read")
if err:
self._send_json(err[0], {"error": err[1]})
return
self._todos_list(project_id, query)
elif method == "POST":
err = self._check_project(
user_id, project_id, "create_task")
if err:
self._send_json(err[0], {"error": err[1]})
return
self._todos_create(user_id, project_id)
else:
self._send_json(404, {"error": "not found"})
return
if len(parts) == 3 and parts[1] == "todos":
if method == "GET":
err = self._check_project(user_id, project_id, "read")
if err:
self._send_json(err[0], {"error": err[1]})
return
self._todos_get(project_id, parts[2])
elif method in ("PATCH", "DELETE"):
err = self._check_project(
user_id, project_id, "create_task")
if err:
self._send_json(err[0], {"error": err[1]})
return
if method == "PATCH":
self._todos_update(user_id, project_id, parts[2])
else:
self._todos_delete(user_id, project_id, parts[2])
else:
self._send_json(404, {"error": "not found"})
return
# /projects/{id}/skills GET(项目 read)
if len(parts) == 2 and parts[1] == "skills":
if method != "GET":
@@ -9132,6 +9176,126 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
"due_count": sum(1 for a in autos if a["due"]),
})
# -- 项目待办(见 cloud/todos.py)-------------------------
def _todos_list(self, project_id, query):
"""列出某项目待办,附全项目各状态计数;query 支持
status 与 q(标题/描述子串)服务端过滤,计数与
total 不受过滤影响。"""
conn = self._db()
try:
try:
todos = _todo_list(
conn, project_id,
status=query.get("status", [None])[0] or None,
q=query.get("q", [None])[0] or None)
# counts / total 是全项目口径,不受过滤影响
#(与任务列表的 status_counts 同款语义)。
all_todos = _todo_list(conn, project_id)
except Exception as exc:
self._send_json(502, {"error": str(exc)[:500]})
return
conn.commit()
finally:
conn.close()
counts = {}
for t in all_todos:
counts[t["status"]] = counts.get(t["status"], 0) + 1
self._send_json(200, {
"project_id": project_id, "todos": todos,
"counts": counts, "total": len(all_todos)})
def _todos_create(self, user_id, project_id):
"""新建待办:body title 必填;description / status /
priority / task_id / due_at / sort_order 可选。"""
body, err = self._read_json()
if err:
self._send_json(400, {"error": err})
return
conn = self._db()
try:
try:
todo = _todo_create(
conn, project_id, user_id, body.get("title"),
description=body.get("description"),
status=body.get("status"),
priority=body.get("priority"),
task_id=body.get("task_id"),
due_at=body.get("due_at"),
sort_order=body.get("sort_order"))
except ValueError as exc:
self._send_json(400, {"error": str(exc)})
return
except Exception as exc:
self._send_json(502, {"error": str(exc)[:500]})
return
finally:
conn.close()
with _auth_store(self.server.db_path) as store:
store.audit(user_id, project_id, "todo_created", todo["id"])
self._send_json(201, todo)
def _todos_get(self, project_id, todo_id):
"""单条待办:行不存在或属于别的项目时一律 404。"""
conn = self._db()
try:
try:
todo = _todo_get(conn, todo_id)
except Exception as exc:
self._send_json(502, {"error": str(exc)[:500]})
return
conn.commit()
finally:
conn.close()
if todo is None or todo["project_id"] != project_id:
self._send_json(404, {"error": "todo not found"})
return
self._send_json(200, todo)
def _todos_update(self, user_id, project_id, todo_id):
"""PATCH 待办:只改 body 里给出的字段;行不存在或
属于别的项目时一律 404;校验失败 400。"""
body, err = self._read_json()
if err:
self._send_json(400, {"error": err})
return
conn = self._db()
try:
try:
todo = _todo_update(conn, todo_id, body or {})
except ValueError as exc:
self._send_json(400, {"error": str(exc)})
return
except Exception as exc:
self._send_json(502, {"error": str(exc)[:500]})
return
finally:
conn.close()
if todo is None or todo["project_id"] != project_id:
self._send_json(404, {"error": "todo not found"})
return
with _auth_store(self.server.db_path) as store:
store.audit(user_id, project_id, "todo_updated", todo_id)
self._send_json(200, todo)
def _todos_delete(self, user_id, project_id, todo_id):
"""DELETE 待办(硬删除):行不存在或属于别的项目
时一律 404;已取消是软关闭,不走这条。"""
conn = self._db()
try:
try:
todo = _todo_delete(conn, todo_id)
except Exception as exc:
self._send_json(502, {"error": str(exc)[:500]})
return
finally:
conn.close()
if todo is None or todo["project_id"] != project_id:
self._send_json(404, {"error": "todo not found"})
return
with _auth_store(self.server.db_path) as store:
store.audit(user_id, project_id, "todo_deleted", todo_id)
self._send_json(200, todo)
def _automation_cancel(self, user_id, project_id, automation_id):
"""取消自动化(软删除):行不存在或属于别的项目时一
律 404(不泄露跨项目存在性);已取消时 409。"""