split: handler mixins for oauth/identity/project/automation/fullauto (94 methods) on integration base 02835f6
- CloudControlPlaneHandler mixes in OAuthMixin, IdentityMemoryMixin, ProjectMixin, AutomationMixin, FullAutoMixin; shared helpers move to cloud/handler_support.py with server.py re-exports (old import paths keep working, incl. _validate_env_branch for tests). - Method bodies moved verbatim; routers, base plumbing, task/lifecycle, webhooks stay in server.py. No >100-line method body split. - test_handler_split_conformance.py (6 tests) pins resolution, layer bans and method accounting (238 total, 144 + 94, no duplicates). - test_p1_security_server.py: monkeypatch targets follow the defining module (handler_project MAX_PROJECTS_PER_USER, handler_identity relay helpers); assertions unchanged. (cherry picked from commit 92165a2b097126470e4827327d96d592a253adf1)
This commit is contained in:
1 parent
c900a0f78d
commit
935b530f84
9 files changed
+3439
-3119
No files matched your search
@@ -0,0 +1,537 @@
|
||||
"""Handler mixin:AutomationMixin。
|
||||
|
||||
由 CloudControlPlaneHandler 拆出,方法体逐字搬移;
|
||||
self.* 调用(含其它 mixin 的方法与留守的 _send_json / _db 等)
|
||||
在混入后的同一类上解析,行为不变。本模块不 import cloud.server。
|
||||
"""
|
||||
|
||||
from cloud.automations import cancel_automation as _auto_cancel
|
||||
from cloud.automations import create_automation as _auto_create
|
||||
from cloud.automations import ensure_automations_tables as _auto_ensure
|
||||
from cloud.automations import get_automation as _auto_get
|
||||
from cloud.automations import list_automations as _auto_list
|
||||
from cloud.automations import list_automation_runs as _auto_runs
|
||||
from cloud.automations import triage_inbox as _auto_triage
|
||||
from cloud.automations import update_automation as _auto_update
|
||||
from cloud.lifecycle import bind_automation as _life_bind
|
||||
from cloud.lifecycle import get_binding as _life_get_binding
|
||||
from cloud.lifecycle import unbind_automation as _life_unbind
|
||||
from cloud.marker_tools import parse_first_run_at as _parse_first_run_at
|
||||
from cloud.push import (
|
||||
create_subscription as _push_create,
|
||||
dispatch_event as _push_dispatch,
|
||||
)
|
||||
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.handler_support import (
|
||||
_auth_store as _auth_store,
|
||||
_ensure_followups_table as _ensure_followups_table,
|
||||
_task_to_dict as _task_to_dict,
|
||||
)
|
||||
|
||||
|
||||
class AutomationMixin:
|
||||
"""AutomationMixin 领域方法(见模块 docstring)。"""
|
||||
|
||||
# -- 推送订阅与分发 ---------------------------------------------------
|
||||
def _push_create_endpoint(self, user_id, project_id):
|
||||
body, err = self._read_json()
|
||||
if err:
|
||||
self._send_json(400, {"error": err})
|
||||
return
|
||||
try:
|
||||
sub = _push_create(
|
||||
self.server.db_path, project_id, user_id,
|
||||
body.get("channel"), body.get("target"),
|
||||
body.get("preference") or "instant")
|
||||
except ValueError as e:
|
||||
self._send_json(400, {"error": str(e)})
|
||||
return
|
||||
# 审计只记订阅 id 与通道:target 全文不记(Slack webhook
|
||||
# URL 本身含 secret 路径,记进审计等于把 secret 落盘)。
|
||||
with _auth_store(self.server.db_path) as store:
|
||||
store.audit(user_id, project_id, "push_subscription_created",
|
||||
"subscription_id=%s channel=%s"
|
||||
% (sub["id"], sub["channel"]))
|
||||
self._send_json(201, sub)
|
||||
|
||||
def _push_dispatch_endpoint(self, user_id):
|
||||
body, err = self._read_json()
|
||||
if err:
|
||||
self._send_json(400, {"error": err})
|
||||
return
|
||||
event_type = body.get("event_type")
|
||||
project_id = body.get("project_id")
|
||||
if not event_type or not project_id:
|
||||
self._send_json(400, {"error": "event_type and project_id required"})
|
||||
return
|
||||
# P0 安全批后:/admin/push/dispatch 是管理端点,只认
|
||||
# CODEX_GLOBAL_ADMINS——该口径已强于 P1 要求的 create_task,
|
||||
# 不再降级。被拒与成功都记审计;目标 SSRF 校验在
|
||||
# push.dispatch_event / 真实发送层做。
|
||||
if not self._is_global_admin(user_id):
|
||||
self._audit_admin(user_id, "admin_push_denied",
|
||||
"dispatch %s %s" % (event_type, project_id))
|
||||
self._send_json(403, {"error": "forbidden"})
|
||||
return
|
||||
# 分发:sender=None 时用 push 的内置真实发送端
|
||||
# (webhook/slack 真 POST,webpush/apns/fcm 仍报缺凭证);
|
||||
# 显式注入的 sender 优先于内置端(见 push.dispatch_event)。
|
||||
result = _push_dispatch(
|
||||
self.server.db_path, event_type, project_id,
|
||||
body.get("payload") or {}, sender=None)
|
||||
self._audit_admin(user_id, "admin_push_dispatch",
|
||||
"dispatch %s %s" % (event_type, project_id))
|
||||
self._send_json(200, result)
|
||||
|
||||
# -- webhooks(签名认证,不走 X-API-Key)---------------------------
|
||||
# -- 自动化与分诊收件箱(P1 第 2 项)---------------------------
|
||||
# 权限口径与其它项目级写操作同款:创建/取消用 create_task
|
||||
# (不是 manage_project,因为自动化是项目成员的日常编排,
|
||||
# 不是改项目归属/仓库这类 owner 专属操作;评审/审批等写
|
||||
# 操作同样用 create_task);读用 read。状态变更记审计。
|
||||
# 失败口径:校验失败 400;模块/数据库层意外失败时回 502
|
||||
# 透传错误原文(截断 500 字),绝不回 201 假装建成功——
|
||||
# 注意 _dispatch_safe 的兜底是 500,这里显式 502 优先于它。
|
||||
def _automation_create(self, user_id, project_id):
|
||||
"""创建自动化定义:只登记定义与到期状态,不跑模型、
|
||||
不另起调度器(执行器后续走 claim 门,见 automations
|
||||
模块文档)。body:name、prompt 必填;interval_seconds
|
||||
可选(默认 86400,最小 60);repo_url 可选;
|
||||
first_run_at 可选(epoch 秒或 ISO8601 字符串,与
|
||||
marker 的 parse_first_run_at 同款解析,过去则建完
|
||||
即到期);run_once 可选(true 时跑过一次后不再到期)。
|
||||
|
||||
P1 配额门:创建时先过与建任务同一道配额门(美元配额
|
||||
与无价 token cap,超限 402),避免“定义免费建、到期
|
||||
无限跑”;单自动化的 spend 上限即创建人配额本身——
|
||||
每次到期产出的任务以 created_by=创建人建任务,走
|
||||
POST /tasks 的配额门(触发路径若绕过服务端建任务,
|
||||
则不在本门覆盖内,见残留)。本服务端没有自动化触发
|
||||
端点,触发侧的门不在本次 server.py 改动内。"""
|
||||
body, err = self._read_json()
|
||||
if err:
|
||||
self._send_json(400, {"error": err})
|
||||
return
|
||||
if self._reject_if_over_quota(user_id):
|
||||
return
|
||||
interval = body.get("interval_seconds", 86400)
|
||||
if interval is None:
|
||||
interval = 86400
|
||||
first_run_at = body.get("first_run_at")
|
||||
if first_run_at is not None:
|
||||
first_run_at = _parse_first_run_at(first_run_at)
|
||||
if first_run_at is None:
|
||||
self._send_json(400, {"error": "invalid first_run_at"})
|
||||
return
|
||||
run_once = body.get("run_once", False)
|
||||
if not isinstance(run_once, bool):
|
||||
self._send_json(400, {"error": "run_once must be boolean"})
|
||||
return
|
||||
conn = self._db()
|
||||
try:
|
||||
try:
|
||||
auto = _auto_create(
|
||||
conn, project_id, body.get("name"),
|
||||
body.get("prompt"), user_id, interval,
|
||||
body.get("repo_url"),
|
||||
first_run_at=first_run_at, run_once=run_once)
|
||||
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, "automation_created",
|
||||
auto["id"])
|
||||
self._send_json(201, auto)
|
||||
|
||||
def _automation_list(self, project_id):
|
||||
"""列出某项目全部自动化定义(含已取消),附到期计数。"""
|
||||
conn = self._db()
|
||||
try:
|
||||
try:
|
||||
autos = _auto_list(conn, project_id)
|
||||
except Exception as exc:
|
||||
self._send_json(502, {"error": str(exc)[:500]})
|
||||
return
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
self._send_json(200, {
|
||||
"project_id": project_id,
|
||||
"automations": autos,
|
||||
"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。"""
|
||||
conn = self._db()
|
||||
try:
|
||||
try:
|
||||
ok, auto = _auto_cancel(conn, automation_id)
|
||||
except Exception as exc:
|
||||
self._send_json(502, {"error": str(exc)[:500]})
|
||||
return
|
||||
finally:
|
||||
conn.close()
|
||||
if auto is None or auto["project_id"] != project_id:
|
||||
self._send_json(404, {"error": "automation not found"})
|
||||
return
|
||||
if not ok:
|
||||
self._send_json(409, {
|
||||
"error": "automation already cancelled"})
|
||||
return
|
||||
with _auth_store(self.server.db_path) as store:
|
||||
store.audit(user_id, project_id, "automation_cancelled",
|
||||
automation_id)
|
||||
self._send_json(200, auto)
|
||||
|
||||
def _automation_get(self, user_id, project_id, automation_id):
|
||||
"""GET 单条自动化(轮询用):定义 + 最近产出记录。
|
||||
行不存在或属于别的项目时一律 404;产出记录读失败
|
||||
时不让整条定义也 502(runs 回空列表 + note)。"""
|
||||
conn = self._db()
|
||||
try:
|
||||
auto = _auto_get(conn, automation_id)
|
||||
runs = []
|
||||
runs_note = ""
|
||||
if auto is not None:
|
||||
try:
|
||||
runs = _auto_runs(conn, automation_id)
|
||||
except Exception:
|
||||
runs_note = "runs unavailable"
|
||||
finally:
|
||||
conn.close()
|
||||
if auto is None or auto["project_id"] != project_id:
|
||||
self._send_json(404, {"error": "automation not found"})
|
||||
return
|
||||
conn = self._db()
|
||||
try:
|
||||
binding = _life_get_binding(conn, automation_id)
|
||||
finally:
|
||||
conn.close()
|
||||
self._send_json(200, dict(auto, runs=runs,
|
||||
runs_note=runs_note,
|
||||
heartbeat_binding=binding))
|
||||
|
||||
def _automation_bind_get(self, user_id, project_id, automation_id):
|
||||
conn = self._db()
|
||||
try:
|
||||
auto = _auto_get(conn, automation_id)
|
||||
binding = _life_get_binding(conn, automation_id)
|
||||
finally:
|
||||
conn.close()
|
||||
if auto is None or auto["project_id"] != project_id:
|
||||
self._send_json(404, {"error": "automation not found"})
|
||||
return
|
||||
self._send_json(200, {"automation_id": automation_id,
|
||||
"binding": binding})
|
||||
|
||||
def _automation_bind(self, user_id, project_id, automation_id):
|
||||
"""POST .../automations/{aid}/bind {task_id, mode}:
|
||||
把自动化绑到已有线程。mode=heartbeat 表示到期唤醒
|
||||
该线程(走 thread/message 投递路);mode=cron 表示
|
||||
到期独立跑新任务(绑定只作标记,不唤醒)。目标任务
|
||||
必须在同一项目,否则 404(不泄露跨项目存在性)。
|
||||
权限 create_task;记 automation_bound 审计。"""
|
||||
body, jerr = self._read_json()
|
||||
if jerr:
|
||||
self._send_json(400, {"error": jerr})
|
||||
return
|
||||
conn = self._db()
|
||||
try:
|
||||
auto = _auto_get(conn, automation_id)
|
||||
if auto is None or auto["project_id"] != project_id:
|
||||
self._send_json(404, {"error": "automation not found"})
|
||||
return
|
||||
if auto["status"] != "active":
|
||||
self._send_json(409, {
|
||||
"error": "cancelled automation cannot be bound"})
|
||||
return
|
||||
target_id = body.get("task_id")
|
||||
target = self._fetch_task(target_id) \
|
||||
if isinstance(target_id, str) else None
|
||||
if target is None:
|
||||
self._send_json(404, {"error": "task not found"})
|
||||
return
|
||||
target = _task_to_dict(target)
|
||||
if target["project_id"] != project_id:
|
||||
self._send_json(404, {"error": "task not found"})
|
||||
return
|
||||
try:
|
||||
binding = _life_bind(
|
||||
conn, automation_id, project_id, target_id,
|
||||
body.get("mode", "heartbeat"))
|
||||
except ValueError as exc:
|
||||
self._send_json(400, {"error": str(exc)})
|
||||
return
|
||||
finally:
|
||||
conn.close()
|
||||
with _auth_store(self.server.db_path) as store:
|
||||
store.audit(user_id, project_id, "automation_bound",
|
||||
"%s:%s" % (automation_id, target_id))
|
||||
self._send_json(200, {"automation_id": automation_id,
|
||||
"binding": binding})
|
||||
|
||||
def _automation_unbind(self, user_id, project_id, automation_id):
|
||||
conn = self._db()
|
||||
try:
|
||||
auto = _auto_get(conn, automation_id)
|
||||
if auto is None or auto["project_id"] != project_id:
|
||||
self._send_json(404, {"error": "automation not found"})
|
||||
return
|
||||
ok = _life_unbind(conn, automation_id)
|
||||
finally:
|
||||
conn.close()
|
||||
if not ok:
|
||||
self._send_json(404, {"error": "no binding"})
|
||||
return
|
||||
with _auth_store(self.server.db_path) as store:
|
||||
store.audit(user_id, project_id, "automation_unbound",
|
||||
automation_id)
|
||||
self._send_json(200, {"automation_id": automation_id,
|
||||
"deleted": True})
|
||||
|
||||
def _automation_heartbeat(self, user_id, project_id, automation_id):
|
||||
"""POST .../automations/{aid}/heartbeat {message?}:
|
||||
手动触发一次心跳唤醒——到期触发器(host run-due /
|
||||
run_due_automation)调的就是这个口,服务端本身不
|
||||
起定时器。无绑定 409;mode=cron 409(cron 不唤醒
|
||||
线程);message 缺省用自动化 prompt。投递走
|
||||
_deliver_to_thread,与 thread/message 同款复活路,
|
||||
配额门同款。记 automation_heartbeat_sent 审计。"""
|
||||
body, jerr = self._read_json()
|
||||
if jerr:
|
||||
self._send_json(400, {"error": jerr})
|
||||
return
|
||||
conn = self._db()
|
||||
try:
|
||||
auto = _auto_get(conn, automation_id)
|
||||
binding = _life_get_binding(conn, automation_id)
|
||||
finally:
|
||||
conn.close()
|
||||
if auto is None or auto["project_id"] != project_id:
|
||||
self._send_json(404, {"error": "automation not found"})
|
||||
return
|
||||
if binding is None:
|
||||
self._send_json(409, {
|
||||
"error": "automation has no heartbeat binding"})
|
||||
return
|
||||
if binding["mode"] != "heartbeat":
|
||||
self._send_json(409, {
|
||||
"error": "cron binding does not wake a thread;"
|
||||
" use the automation run path"})
|
||||
return
|
||||
target_row = self._fetch_task(binding["task_id"])
|
||||
if target_row is None:
|
||||
self._send_json(404, {"error": "bound task not found"})
|
||||
return
|
||||
target = _task_to_dict(target_row)
|
||||
if target["project_id"] != project_id:
|
||||
self._send_json(404, {"error": "bound task not found"})
|
||||
return
|
||||
if self._reject_if_over_quota_for_task(user_id, target):
|
||||
return
|
||||
message = body.get("message") or auto.get("prompt") or ""
|
||||
if not isinstance(message, str) or not message.strip():
|
||||
self._send_json(400, {"error": "message required"})
|
||||
return
|
||||
message = message.strip()
|
||||
if len(message) > 20000:
|
||||
self._send_json(400, {"error": "message too long"})
|
||||
return
|
||||
code, payload = self._deliver_to_thread(
|
||||
user_id, target, message, "queued",
|
||||
audit_action="automation_heartbeat_sent")
|
||||
self._send_json(code, dict(payload,
|
||||
automation_id=automation_id))
|
||||
|
||||
def _automation_update(self, user_id, project_id, automation_id):
|
||||
"""PATCH 单条自动化(automation_update):改名、改
|
||||
prompt、改间隔、改 repo_url,至少给一个字段。注意
|
||||
语义边界:更新不重置 last_run_at(改间隔只影响下次
|
||||
到期,不立即触发);已取消的定义 409(取消是终态,
|
||||
改它必须重建);改名撞同项目 active 名时 400;本
|
||||
端点不改“变化时通知”本身——到期产出的任务终态仍
|
||||
走 worker 现有 push 派发(见 automations 模块文档)。
|
||||
权限 create_task;记 automation_updated 审计。"""
|
||||
body, jerr = self._read_json()
|
||||
if jerr:
|
||||
self._send_json(400, {"error": jerr})
|
||||
return
|
||||
if body.get("first_run_at") is not None and \
|
||||
isinstance(body.get("first_run_at"), str):
|
||||
parsed = _parse_first_run_at(body["first_run_at"])
|
||||
if parsed is None:
|
||||
self._send_json(400, {"error": "invalid first_run_at"})
|
||||
return
|
||||
body = dict(body, first_run_at=parsed)
|
||||
conn = self._db()
|
||||
try:
|
||||
try:
|
||||
ok, auto = _auto_update(conn, automation_id, body)
|
||||
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 auto is None or auto["project_id"] != project_id:
|
||||
self._send_json(404, {"error": "automation not found"})
|
||||
return
|
||||
if not ok:
|
||||
self._send_json(409, {
|
||||
"error": "automation already cancelled"})
|
||||
return
|
||||
with _auth_store(self.server.db_path) as store:
|
||||
store.audit(user_id, project_id, "automation_updated",
|
||||
automation_id)
|
||||
self._send_json(200, auto)
|
||||
|
||||
def _triage_inbox(self, project_id):
|
||||
"""分诊收件箱(只读):到期自动化、失败/中断任务、
|
||||
待处理后续消息、最近自动化产出。见 automations 模块
|
||||
triage_inbox 的字段说明。"""
|
||||
conn = self._db()
|
||||
try:
|
||||
try:
|
||||
_auto_ensure(conn)
|
||||
_ensure_followups_table(conn)
|
||||
inbox = _auto_triage(conn, project_id)
|
||||
except Exception as exc:
|
||||
self._send_json(502, {"error": str(exc)[:500]})
|
||||
return
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
self._send_json(200, inbox)
|
||||
Reference in new issue
Block a user