server: 任务列表一次聚合回 total_tokens

_list_tasks 在取附件的同时用 _task_tokens_map 一次查
cost_records 按 task_id 求和(GROUP BY),一屏只多一条
查询,不按任务逐个调成本接口。无记录计 0、表不存在时
全计 0 且列表不失败;_task_to_dict 新增 total_tokens
参数,默认 None(非列表调用方不把“未算”写成 0)。
前端 TaskListSection 的 token 栏位此前已接好 total_tokens,
补上后列表行直接显示真数,不再显示 token —;前端注释
与占位文案同步更新。
新测试 test_task_list_tokens.py 3 项:HTTP 列表聚合
(actual+estimated 加总、无记录为 0)、缺表不炸、
_task_to_dict 默认 None。
This commit is contained in:
host committed 2026-10-29 18:27:53 +08:00
1 parent 65a875ada9
commit 9eedee5a3d
4 files changed
+168 -14

No files matched your search

+38 -2
View File
@@ -1233,6 +1233,37 @@ def _attachments_map(conn, task_ids):
return out
def _task_tokens_map(conn, task_ids):
"""一批任务的 cost_records token 合计:{task_id: total_tokens}。
任务列表接口用一次查询取全量,不按任务逐条调成本接口
(一屏 N 个任务就是 N 个请求,会拖慢列表刷新、还容易撞
限流)。无成本记录的任务计 0(不是缺失);cost_records
表还不存在时(全新库、CostStore 未建过表)同样全计 0,
列表绝不因此失败——0 在这里就是“未记录到任何 token”,
与 _metrics 的缺表按空统计口径一致。
"""
out = {tid: 0 for tid in task_ids}
if not task_ids:
return out
placeholders = ", ".join("?" * len(task_ids))
try:
rows = conn.execute(
"SELECT task_id, COALESCE(SUM(total_tokens),0) AS t"
" FROM cost_records WHERE task_id IN (%s)"
" GROUP BY task_id" % placeholders,
tuple(task_ids),
).fetchall()
except (sqlite3.OperationalError, _PgOperationalError):
return out
for row in rows:
tid = row[0] if isinstance(row, tuple) else row["task_id"]
if tid in out:
out[tid] = int(row[1] if isinstance(row, tuple)
else row["t"] or 0)
return out
def _ensure_task_checkpoints_table(conn):
"""幂等建断点表(P0 三件其一):server init_db(PG 与 SQLite
两支)与 worker 迁移兜底共用。DOUBLE PRECISION 在两后端都可用。
@@ -1663,7 +1694,7 @@ def _row_get(row, name):
return None
def _task_to_dict(row, attachments=None):
def _task_to_dict(row, attachments=None, total_tokens=None):
depends_on = row["depends_on"]
try:
depends_on = json.loads(depends_on) if depends_on else []
@@ -1719,6 +1750,9 @@ def _task_to_dict(row, attachments=None):
"interrupted_reason": _row_get(row, "interrupted_reason"),
"auto_resume_count": _row_get(row, "auto_resume_count") or 0,
"no_time_limit": _row_get(row, "no_time_limit") or 0,
# 列表接口由 _task_tokens_map 一次聚合传入;其它调用方
# 不传时为 None(前端显示占位),绝不把“未算”写成 0。
"total_tokens": total_tokens,
}
@@ -4390,10 +4424,12 @@ class CloudControlPlaneHandler(BaseHTTPRequestHandler):
(project_id,),
).fetchall()
amap = _attachments_map(conn, [r["id"] for r in rows])
tmap = _task_tokens_map(conn, [r["id"] for r in rows])
finally:
conn.close()
self._send_json(200, {"tasks": [
_task_to_dict(r, amap[r["id"]]) for r in rows]})
_task_to_dict(r, amap[r["id"]], tmap.get(r["id"], 0))
for r in rows]})
def _fetch_task(self, task_id):
conn = self._db()