feat(tasks): 任务软删除(留对话轨迹做语料 + 可恢复)+ bump 0.17.0
DELETE /v1/tasks/{id} 从硬删(DELETE + CASCADE + rmdir)改为软删(置 deleted_at),
messages/usage_events 及工作目录文件全部保留,留作训练语料且可恢复;新增
POST /v1/tasks/{id}/restore;list_tasks/list_folders 计数过滤 deleted_at IS NULL;
delete_file 顶层目录 409 引用检查排除软删 task(避免"任务删了文件夹却删不掉")。
0010 migration 加 tasks.deleted_at(additive 可空,存量行自动视为未删)。
推翻 DESIGN 原 hard-cascade 决策;文件归档方案(restic 备份 + DB 事件日志)写入 DESIGN 待办。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6d6e9f79b5
commit
e87daa7c89
|
|
@ -498,7 +498,9 @@ create index on usage_events (model_profile, created_at);
|
||||||
|
|
||||||
**skill 产物全落 working_dir 不引入 artifacts 表**:中间件是用户花 token 生成的资产,可下载可替换;artifacts 表是为不确定 UX 收益预付架构成本。真嫌乱 UI 加折叠视图。
|
**skill 产物全落 working_dir 不引入 artifacts 表**:中间件是用户花 token 生成的资产,可下载可替换;artifacts 表是为不确定 UX 收益预付架构成本。真嫌乱 UI 加折叠视图。
|
||||||
|
|
||||||
**hard cascade 而非 soft orphan**:`orphaned` 让 list / resume / UI 都多一种特殊 case,"删 folder = 删项目"比"留对话残骸"自然。
|
**~~hard cascade 而非 soft orphan~~ → task 改软删除(2026-06-17 推翻)**:原决策为避免 `orphaned` 特殊 case 选硬删(`DELETE tasks` CASCADE 连带 messages/usage_events)。公测后目标变为**沉淀用户对话轨迹做训练/研究语料**,硬删 = 语料永久丢失,故推翻:`DELETE /v1/tasks/{id}` 改为置 `tasks.deleted_at`(0010 migration),从 `list_tasks` / `list_folders` 计数中过滤,messages/usage_events(CASCADE 不再触发)与工作目录文件全部保留;新增 `POST /v1/tasks/{id}/restore` 恢复。原"特殊 case"成本被一处 `WHERE deleted_at IS NULL` 收口(列表是唯一用户可见入口,按 id 取单 task 的端点不过滤,恢复/直链仍可达)。心智改为:**平台对数据 append-only,用户"删除" = 可见性状态,永不销毁字节**。物理清理留给将来的管理员工具。`delete_file` 顶层目录 409 引用检查同步排除软删 task(否则"任务都删了文件夹却删不掉"死结)。
|
||||||
|
|
||||||
|
**文件留存(归档)—— 设计已定,实现待办**(2026-06-17):任务对话靠软删除即留在 DB;但**用户文件在 FS 上,删除/覆盖即字节丢失**,需单独留存以供训练/研究。已对齐的方案(尚未实现,优先级靠后):**① 基础设施层定时增量备份做持久化地基**(restic/borg → 只进不删、内容寻址去重,定时跑;与应用代码完全解耦 → 新端点/新工具自动覆盖不会漏,且捕获删除+覆盖+最终成品,这是"删除前归档"钩子拿不到的)+ **② 应用层轻量事件日志**(删除/覆盖时只追加 user/task/path/time/reason 一条,补 ① 缺的用户意图/出处语义;放 DB 表 `data_events` 而非 jsonl,避并发追加竞争)。**起步同盘**(防误删+留语料够;不防整盘损坏 —— 已知边界,将来换备份 target 到第二块盘/异地即可,纯配置改动)。**不选**"每个删除端点内联 copytree-再删":横切关注点手写 N 处 → 易漏(删文件/夹/skill、rename/upload/i2i 覆盖入口持续增加)、只看得见删除一瞬、跨卷拷脆。覆盖(如 seedream i2i 改图)若 ① 颗粒度不够,将来在该具体工具内定点补"覆盖前快照",不铺全局钩子。
|
||||||
|
|
||||||
**0004 删 `runs` + `usage_events` 表**(2026-05-18):`runs` 表 tokens_p/c 写但从未读(真 tokens 走 tasks 累计),`started_at/finished_at/error` 也只写不读;`run_id` 单活 run 形态下对客户端 / broker / cancel 全冗余。合并 `run_status` + `run_error` 两列入 `tasks`。`usage_events` 从未真写,纯死代码,真要计费再加。**代价**:失"历史 run 元数据"(每次 LLM 调用的独立时间戳 / token 切片) — messages 表已记下产物,token 累计在 tasks,真要细粒度审计再补回 `usage_events`(届时是新需求,不是技术债)。
|
**0004 删 `runs` + `usage_events` 表**(2026-05-18):`runs` 表 tokens_p/c 写但从未读(真 tokens 走 tasks 累计),`started_at/finished_at/error` 也只写不读;`run_id` 单活 run 形态下对客户端 / broker / cancel 全冗余。合并 `run_status` + `run_error` 两列入 `tasks`。`usage_events` 从未真写,纯死代码,真要计费再加。**代价**:失"历史 run 元数据"(每次 LLM 调用的独立时间戳 / token 切片) — messages 表已记下产物,token 累计在 tasks,真要细粒度审计再补回 `usage_events`(届时是新需求,不是技术债)。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,12 @@
|
||||||
|
|
||||||
## 已完成关键能力
|
## 已完成关键能力
|
||||||
|
|
||||||
|
### 2026-06-17 / 任务软删除(留对话轨迹做语料 + 可恢复)
|
||||||
|
|
||||||
|
- 背景:公测后目标转为沉淀用户对话/文件做训练研究语料;原"hard cascade"硬删任务会连带 messages/usage_events 永久丢失,推翻该决策(DESIGN §取舍同步标注)。
|
||||||
|
- 改动:`tasks` 加 `deleted_at` 列(0010 migration,additive 可空);`DELETE /v1/tasks/{id}` 从 `DELETE` 改为置 `deleted_at=now()`,不再触发 CASCADE、不动工作目录文件(原 rmdir 清理一并去掉);`list_tasks` / `list_folders` 计数加 `WHERE deleted_at IS NULL` 过滤;新增 `POST /v1/tasks/{id}/restore` 恢复;`delete_file` 顶层目录 409 引用检查排除软删 task。
|
||||||
|
- 文件留存(归档)方案已在 DESIGN 记录(restic 备份地基 + DB 事件日志 + 起步同盘),**实现待办**,优先级靠后。bump 0.16.2 → 0.17.0。
|
||||||
|
|
||||||
### 2026-06-17 / 用户操作说明书(详 + 精简两版)+ 文献库库容 21W→100W 全量更新
|
### 2026-06-17 / 用户操作说明书(详 + 精简两版)+ 文献库库容 21W→100W 全量更新
|
||||||
|
|
||||||
- 新增 `docs/操作说明书.md`(详版)+ `docs/操作说明书-精简版.md`:面向科研用户、不出现产品代号、从登录后正式操作讲起。覆盖三栏布局、**个人文件夹 → 工作目录 → 任务**三层概念(任务≠文件夹、多任务可共享一个工作目录)、新建任务、对话、技能矩阵(含 paper)、文件管理、进阶(方案确认卡/消息目录/记忆)、任务管理、图像视频、账户存储、FAQ;截图留占位标注。突出对外优势(内部文献库、科研计算、可直接产出文件)。
|
- 新增 `docs/操作说明书.md`(详版)+ `docs/操作说明书-精简版.md`:面向科研用户、不出现产品代号、从登录后正式操作讲起。覆盖三栏布局、**个人文件夹 → 工作目录 → 任务**三层概念(任务≠文件夹、多任务可共享一个工作目录)、新建任务、对话、技能矩阵(含 paper)、文件管理、进阶(方案确认卡/消息目录/记忆)、任务管理、图像视频、账户存储、FAQ;截图留占位标注。突出对外优势(内部文献库、科研计算、可直接产出文件)。
|
||||||
|
|
|
||||||
7
RUN.md
7
RUN.md
|
|
@ -65,7 +65,7 @@ python -m venv .venv
|
||||||
|
|
||||||
# 3) DB schema 上车
|
# 3) DB schema 上车
|
||||||
.venv/Scripts/python.exe main.py db upgrade head
|
.venv/Scripts/python.exe main.py db upgrade head
|
||||||
.venv/Scripts/python.exe main.py db current # 应输出 0007 (head)
|
.venv/Scripts/python.exe main.py db current # 应输出 0010 (head)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -160,7 +160,8 @@ curl --noproxy '*' -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8765/v1/ta
|
||||||
| `GET /v1/tasks?page=&page_size=&status=&skill=&working_dir=&q=&ordering=` | 列任务,默认 `-created_at`;响应 `{page, page_size, count, results}`;`ordering` DRF 风格逗号分隔 `-field` 倒序,allowlist created_at/updated_at/name/status | 必填 |
|
| `GET /v1/tasks?page=&page_size=&status=&skill=&working_dir=&q=&ordering=` | 列任务,默认 `-created_at`;响应 `{page, page_size, count, results}`;`ordering` DRF 风格逗号分隔 `-field` 倒序,allowlist created_at/updated_at/name/status | 必填 |
|
||||||
| `GET /v1/tasks/{id}` | 单 task meta + `n_messages`;跨 user → 404 | 必填 |
|
| `GET /v1/tasks/{id}` | 单 task meta + `n_messages`;跨 user → 404 | 必填 |
|
||||||
| `PATCH /v1/tasks/{id}` | `{status?,description?,name?,skill?}`;active 不让从 web 切回 | 必填 |
|
| `PATCH /v1/tasks/{id}` | `{status?,description?,name?,skill?}`;active 不让从 web 切回 | 必填 |
|
||||||
| `DELETE /v1/tasks/{id}` | 硬删 DB 行(messages CASCADE);若 working_dir 已无其他 task 引用且 FS 目录为空 → 顺手 rmdir 清孤儿(非空 / 外部 --working-dir 静默跳过) | 必填 |
|
| `DELETE /v1/tasks/{id}` | **软删**(204):置 `deleted_at`,从列表隐藏;messages/usage_events 及工作目录文件全部保留(留作语料 + 可恢复),不动任何磁盘文件;已软删幂等 204 | 必填 |
|
||||||
|
| `POST /v1/tasks/{id}/restore` | 恢复软删的 task(置 `deleted_at=NULL`),重新出现在列表;返回 task meta;未软删幂等成功;跨 user / 不存在 → 404 | 必填 |
|
||||||
| `GET /v1/folders` | 列当前 user 工作目录 + n_tasks + last_used | 必填 |
|
| `GET /v1/folders` | 列当前 user 工作目录 + n_tasks + last_used | 必填 |
|
||||||
| `GET /v1/skills` | 列当前 user 可用 skill(内置 + 自己的);每项带 `source`(builtin/user)/`overrides_builtin`;另返 `load_errors`(用户 skill 因 frontmatter 坏未加载的) | 必填 |
|
| `GET /v1/skills` | 列当前 user 可用 skill(内置 + 自己的);每项带 `source`(builtin/user)/`overrides_builtin`;另返 `load_errors`(用户 skill 因 frontmatter 坏未加载的) | 必填 |
|
||||||
| `GET /v1/skills/{name}` | 返某 skill 完整 SKILL.md 正文(前端「技能」modal 点开查看);同名按 user wins | 必填 |
|
| `GET /v1/skills/{name}` | 返某 skill 完整 SKILL.md 正文(前端「技能」modal 点开查看);同名按 user wins | 必填 |
|
||||||
|
|
@ -683,7 +684,7 @@ sudo xfs_quota -x -c "limit -p bhard=10g zcbot_<user_uuid>" /opt
|
||||||
| Windows 控制台 emoji 崩 | Python stdout 是 GBK。用 `[OK]` / `[ng]` 等 ASCII 标签(见 memory) |
|
| Windows 控制台 emoji 崩 | Python stdout 是 GBK。用 `[OK]` / `[ng]` 等 ASCII 标签(见 memory) |
|
||||||
| `db upgrade` 报 `column already exists` | DB 已被改过,`db current` 确认 revision,必要时手 ALTER 或 `db downgrade base` 重来 |
|
| `db upgrade` 报 `column already exists` | DB 已被改过,`db current` 确认 revision,必要时手 ALTER 或 `db downgrade base` 重来 |
|
||||||
| Resume 找不到 task | dev SPA 左侧 task 列表看 task_id 是否在;或 `curl /v1/tasks` 拉 |
|
| Resume 找不到 task | dev SPA 左侧 task 列表看 task_id 是否在;或 `curl /v1/tasks` 拉 |
|
||||||
| `--working-dir` 指定后 task 删了目录还在 | 两种情况:① 目录非空(有用户文件) — 设计如此,绝不 rmtree,手动 `rm -rf <dir>` 清;② 外部 `--working-dir`(DB 存绝对路径)— 不自动清,避免误删用户外部项目。ROOT 内 + 同 working_dir 无其他 task 引用 + FS 空 → DELETE task 时已自动 rmdir |
|
| task 删了文件还在 | 现在 `DELETE /v1/tasks/{id}` 是**软删**,本就不动任何磁盘文件(留作语料 + 可恢复);要清磁盘走 `POST /v1/files/delete`。彻底物理删 task(及 messages)留给将来的管理员清理工具;当前如需手动:`psql> DELETE FROM tasks WHERE task_id=...`(messages/usage_events CASCADE) |
|
||||||
| Sandbox 容器内 `touch /workspace/x` 报 `Permission denied` | 容器 uid 1000 与 host `zcbot` 用户 uid 不一致(bind mount 保 host owner)。`docker build --build-arg HOST_UID=$(id -u zcbot)` 重建镜像 |
|
| Sandbox 容器内 `touch /workspace/x` 报 `Permission denied` | 容器 uid 1000 与 host `zcbot` 用户 uid 不一致(bind mount 保 host owner)。`docker build --build-arg HOST_UID=$(id -u zcbot)` 重建镜像 |
|
||||||
| Sandbox 容器 build 完起不来,`docker logs` 显示 iptables 报错 | 缺 NET_ADMIN cap(`--cap-add=NET_ADMIN` 漏了)或 kernel 不支持(WSL2 / OpenVZ 环境不能跑)。Ubuntu 物理 / KVM 正常。验:`docker exec ... iptables -V` |
|
| Sandbox 容器 build 完起不来,`docker logs` 显示 iptables 报错 | 缺 NET_ADMIN cap(`--cap-add=NET_ADMIN` 漏了)或 kernel 不支持(WSL2 / OpenVZ 环境不能跑)。Ubuntu 物理 / KVM 正常。验:`docker exec ... iptables -V` |
|
||||||
| 启动报 `ZCBOT_SANDBOX_BACKEND=docker but sandbox init failed: ...` | docker daemon 没起 / 用户不在 docker group / network create 失败。先跑 `main.py sandbox check` 看哪一项 err |
|
| 启动报 `ZCBOT_SANDBOX_BACKEND=docker but sandbox init failed: ...` | docker daemon 没起 / 用户不在 docker group / network create 失败。先跑 `main.py sandbox check` 看哪一项 err |
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||||
# 改版本只动这一行。
|
# 改版本只动这一行。
|
||||||
__version__ = "0.16.2"
|
__version__ = "0.17.0"
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,11 @@ class Task(Base):
|
||||||
updated_at: Mapped[datetime] = mapped_column(
|
updated_at: Mapped[datetime] = mapped_column(
|
||||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
|
DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False
|
||||||
)
|
)
|
||||||
|
# 软删除标记(0010):置时间即"逻辑删除",从列表隐藏但 DB 行 / messages / usage_events /
|
||||||
|
# 工作目录文件全部保留(留作语料 + 可恢复)。NULL = 未删。物理删只在管理员清理时走。
|
||||||
|
deleted_at: Mapped[Optional[datetime]] = mapped_column(
|
||||||
|
DateTime(timezone=True), nullable=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Message(Base):
|
class Message(Base):
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
"""tasks.deleted_at 列(任务软删除).
|
||||||
|
|
||||||
|
Revision ID: 0010
|
||||||
|
Revises: 0009
|
||||||
|
Create Date: 2026-06-17
|
||||||
|
|
||||||
|
给 tasks 加 deleted_at 列(可空,默认 NULL=未删)。DELETE /v1/tasks/{id} 从硬删
|
||||||
|
改为软删(置 deleted_at=now()),列表查询过滤 deleted_at IS NULL;新增
|
||||||
|
POST /v1/tasks/{id}/restore 恢复。软删后 messages / usage_events(原 CASCADE 不再触发)
|
||||||
|
及工作目录文件全部保留,留作训练语料并支持恢复。
|
||||||
|
|
||||||
|
只加列、不动现有数据;历史行 deleted_at 默认 NULL,自动视为"未删"。
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
|
||||||
|
revision: str = "0010"
|
||||||
|
down_revision: Union[str, None] = "0009"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
"tasks",
|
||||||
|
sa.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column("tasks", "deleted_at")
|
||||||
69
web/app.py
69
web/app.py
|
|
@ -1110,8 +1110,8 @@ def create_app() -> FastAPI:
|
||||||
s.strip() for s in (run_status or "").split(",") if s.strip() in rs_allowed
|
s.strip() for s in (run_status or "").split(",") if s.strip() in rs_allowed
|
||||||
} or None
|
} or None
|
||||||
|
|
||||||
# 组装 WHERE
|
# 组装 WHERE(软删除的 task 永不出现在列表;恢复见 /restore)
|
||||||
conditions = [Task.user_id == user_id]
|
conditions = [Task.user_id == user_id, Task.deleted_at.is_(None)]
|
||||||
if status:
|
if status:
|
||||||
conditions.append(Task.status == status)
|
conditions.append(Task.status == status)
|
||||||
if skill:
|
if skill:
|
||||||
|
|
@ -1207,7 +1207,11 @@ def create_app() -> FastAPI:
|
||||||
db_form = f"workspace/users/{user_id}/{name}"
|
db_form = f"workspace/users/{user_id}/{name}"
|
||||||
stat = s.execute(
|
stat = s.execute(
|
||||||
select(func.count(), func.max(Task.updated_at))
|
select(func.count(), func.max(Task.updated_at))
|
||||||
.where(Task.user_id == user_id, Task.working_dir == db_form)
|
.where(
|
||||||
|
Task.user_id == user_id,
|
||||||
|
Task.working_dir == db_form,
|
||||||
|
Task.deleted_at.is_(None),
|
||||||
|
)
|
||||||
).first()
|
).first()
|
||||||
n = int((stat[0] if stat else 0) or 0)
|
n = int((stat[0] if stat else 0) or 0)
|
||||||
lu = stat[1] if stat else None
|
lu = stat[1] if stat else None
|
||||||
|
|
@ -1327,41 +1331,55 @@ def create_app() -> FastAPI:
|
||||||
|
|
||||||
@app.delete("/v1/tasks/{task_id}", status_code=204, tags=["tasks"])
|
@app.delete("/v1/tasks/{task_id}", status_code=204, tags=["tasks"])
|
||||||
def delete_task(task_id: str, user_id: UUID = Depends(require_user)):
|
def delete_task(task_id: str, user_id: UUID = Depends(require_user)):
|
||||||
"""硬删除:DELETE DB 行(messages / usage_events CASCADE)。
|
"""软删除:置 deleted_at=now(),从任务列表隐藏。
|
||||||
|
|
||||||
若 working_dir 已无任何 task 引用且 FS 目录为空 → best-effort rmdir
|
DB 行 / messages / usage_events(原 CASCADE 不再触发)及工作目录文件全部保留
|
||||||
清孤儿(非空 / 不存在 / 没权限 都静默跳过 —— working_dir 视为可重生视图)。
|
—— 留作训练语料,且可经 POST /v1/tasks/{id}/restore 恢复。不动任何磁盘文件。
|
||||||
外部 --working-dir(DB 串绝对)不动,只清 ROOT 内相对路径。跨 user → 404。
|
已软删的再次调用幂等返回 204。跨 user / 不存在 → 404。
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
tid = UUID(task_id)
|
tid = UUID(task_id)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise HTTPException(404, f"invalid task id: {task_id!r}")
|
raise HTTPException(404, f"invalid task id: {task_id!r}")
|
||||||
from sqlalchemy import delete as _delete
|
|
||||||
from core.paths import from_db_path
|
|
||||||
with session_scope() as s:
|
with session_scope() as s:
|
||||||
wd_db = s.execute(
|
row = s.execute(
|
||||||
select(Task.working_dir).where(
|
select(Task.deleted_at).where(
|
||||||
Task.task_id == tid, Task.user_id == user_id,
|
Task.task_id == tid, Task.user_id == user_id,
|
||||||
)
|
)
|
||||||
).scalar_one_or_none()
|
).first()
|
||||||
if wd_db is None:
|
if row is None:
|
||||||
raise HTTPException(404, f"task not found: {tid}")
|
raise HTTPException(404, f"task not found: {tid}")
|
||||||
s.execute(
|
if row.deleted_at is None:
|
||||||
_delete(Task).where(Task.task_id == tid, Task.user_id == user_id)
|
s.execute(
|
||||||
)
|
update(Task)
|
||||||
remaining = s.execute(
|
.where(Task.task_id == tid, Task.user_id == user_id)
|
||||||
select(func.count()).select_from(Task).where(
|
.values(deleted_at=func.now())
|
||||||
Task.user_id == user_id, Task.working_dir == wd_db,
|
|
||||||
)
|
)
|
||||||
).scalar_one() or 0
|
|
||||||
if wd_db and not remaining and not Path(wd_db).is_absolute():
|
|
||||||
try:
|
|
||||||
from_db_path(wd_db).rmdir()
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
return None # 204
|
return None # 204
|
||||||
|
|
||||||
|
@app.post("/v1/tasks/{task_id}/restore", tags=["tasks"])
|
||||||
|
def restore_task(task_id: str, user_id: UUID = Depends(require_user)):
|
||||||
|
"""恢复软删除的 task(置 deleted_at=NULL),重新出现在列表。
|
||||||
|
|
||||||
|
未软删的幂等成功。跨 user / 不存在 → 404。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
tid = UUID(task_id)
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(404, f"invalid task id: {task_id!r}")
|
||||||
|
with session_scope() as s:
|
||||||
|
row = s.execute(
|
||||||
|
select(Task).where(Task.task_id == tid, Task.user_id == user_id)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
if row is None:
|
||||||
|
raise HTTPException(404, f"task not found: {tid}")
|
||||||
|
row.deleted_at = None # ORM 脏标记,session_scope 提交时落库
|
||||||
|
n = s.execute(
|
||||||
|
select(func.count()).select_from(Message).where(Message.task_id == tid)
|
||||||
|
).scalar_one()
|
||||||
|
usage = _usage_aggregates(s, [tid])
|
||||||
|
return _task_dict(row, n_messages=n, usage=usage.get(tid))
|
||||||
|
|
||||||
@app.patch("/v1/tasks/{task_id}", tags=["tasks"])
|
@app.patch("/v1/tasks/{task_id}", tags=["tasks"])
|
||||||
def patch_task(
|
def patch_task(
|
||||||
task_id: str,
|
task_id: str,
|
||||||
|
|
@ -2062,6 +2080,7 @@ def create_app() -> FastAPI:
|
||||||
select(func.count()).select_from(Task).where(
|
select(func.count()).select_from(Task).where(
|
||||||
Task.user_id == user_id,
|
Task.user_id == user_id,
|
||||||
Task.working_dir == db_form,
|
Task.working_dir == db_form,
|
||||||
|
Task.deleted_at.is_(None), # 软删 task 不再算引用
|
||||||
)
|
)
|
||||||
).scalar_one() or 0
|
).scalar_one() or 0
|
||||||
if n:
|
if n:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue