fix(web): sort tasks by recent activity
This commit is contained in:
parent
cd6a76cba9
commit
13e3e50371
|
|
@ -5,6 +5,10 @@
|
|||
> 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。
|
||||
> 工程口径的完整记录见 `PROGRESS.md` / git log。
|
||||
|
||||
## 0.60.18 — 2026-07-31
|
||||
|
||||
- 左侧任务列表默认按最近更新时间排序,继续处理过的对话会自动回到列表前面;创建时间、名称和状态排序仍可手动选择。
|
||||
|
||||
## 0.60.17 — 2026-07-31
|
||||
|
||||
- 修复读取论文、PDF 等外部内容时,个别不可见字符可能导致工具结果保存失败并中断对话的问题。
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。
|
||||
|
||||
最后更新:2026-07-31(消息 JSONB NUL 字符兼容,bump 0.60.17)
|
||||
最后更新:2026-07-31(任务列表默认按最近更新时间排序,bump 0.60.18)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -23,6 +23,7 @@
|
|||
|
||||
### 2026-07-31
|
||||
|
||||
- **07-31 / 0.60.18 / 任务列表默认按最近更新时间排序**:普通任务列表的前后端默认排序统一由创建时间倒序改为更新时间倒序,最近继续处理的对话会回到顶部;显式 `ordering=-created_at` 等既有排序参数和全部下拉选项保持兼容。新增默认值、非法参数回退及创建时间兼容 3 项单元测试,Python/JavaScript 语法与 diff 格式检查通过;无 schema、migration 或依赖变化。
|
||||
- **07-31 / 0.60.17 / 消息 JSONB NUL 字符兼容**:外部工具从论文、PDF 等来源提取的文本可能夹带真实 NUL,Python JSON 会编码为 PostgreSQL `jsonb` 无法转换成 text 的 `\u0000`,导致 tool message INSERT 回滚并中断 run。新增递归清洗并在 `Message.payload` ORM 写入边界统一守门,`Session.append` 同步清洗内存上下文,保留制表符、换行及其余 Unicode;无 schema、migration 或存量数据改动。相关 6 项单元测试、Python 编译及 diff 格式检查通过;未设置 `ZCBOT_TEST_DB_URL`,未连接数据库。
|
||||
- **07-31 / 0.60.16 / platform 历史 task 稳定进入 + 新建任务名称可选**:普通 platform iframe 不再复用 localStorage 缓存 JWT 抢跑,始终等待父页面本次 `zcbot-token` 后再进入初始 task,消除旧 token 401 后界面永久停在“加载中”的时序;企微/App fragment 登录路径不变。`POST /v1/tasks` 省略/留空 name 且提供 working_dir 时自动以“新对话”占位并启用首条消息标题生成,旧 `auto_title` 语义继续兼容;快速新对话不再发送冗余字段,自定义创建统一为工作目录必填、任务名选填,并换用可搜索/可创建的目录组合框。27 项无 DB 测试、Python 编译、3 个 JavaScript 语法及 diff 格式检查通过;DB 路由用例已补,未配置 `ZCBOT_TEST_DB_URL` 时安全跳过。
|
||||
- **07-31 / 0.60.15 / 嵌入初始 task 直接进入工作目录**:platform iframe、企业微信和 App 携带 `task_id` 首次进入时,由 `enterApp` 直接选择目标 task,跳过新对话草稿及 user_root 文件面板初始化,随后沿既有 `selectTask` 链路同步 `working_dir`;401 重签仍不重复定位,普通登录空态不变。JavaScript 语法、7 项静态前端测试及 diff 格式检查通过;无 API、schema、migration 或依赖变化。
|
||||
|
|
|
|||
2
RUN.md
2
RUN.md
|
|
@ -265,7 +265,7 @@ curl --noproxy '*' -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8765/v1/ta
|
|||
| `POST /v1/auth/login_password` | dev SPA 邮箱密码;body `{email, password}` → `{token,...,email,...}`;邮箱不存在 / 密码错 / 未设密码统一 403 | 豁免 |
|
||||
| `POST /v1/auth/change_password` | dev SPA 顶栏「改密码」;body `{old_password, new_password}`(user_id 取自 JWT)→ `{ok:true}`;新密码 <6→400、旧密码错 / 无密码(platform_key 建的行)→403 | 必填 |
|
||||
| `POST /v1/tasks` | 创建 task,body `{name?, working_dir?, description?, skill?}`;省略/留空 `name` 时必须传 `working_dir`,后端以“新对话”占位并在首条消息后自动命名;显式 `name` 时仍可省略 `working_dir` 并沿用同名目录。旧字段 `auto_title` 仅作兼容保留,新调用方无需传 | 必填 |
|
||||
| `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=` | 列任务,默认 `-updated_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 | 必填 |
|
||||
| `PATCH /v1/tasks/{id}` | `{status?,description?,name?,skill?}`;active 不让从 web 切回 | 必填 |
|
||||
| `DELETE /v1/tasks/{id}` | **软删**(204):置 `deleted_at`,从列表隐藏;messages/usage_events 及工作目录文件全部保留(留作语料 + 可恢复),不动任何磁盘文件;已软删幂等 204 | 必填 |
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||
# 改版本只动这一行。
|
||||
__version__ = "0.60.17"
|
||||
__version__ = "0.60.18"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
import unittest
|
||||
|
||||
from web.common import parse_ordering
|
||||
|
||||
|
||||
class TaskOrderingTests(unittest.TestCase):
|
||||
def test_default_ordering_uses_latest_update_first(self):
|
||||
ordering = parse_ordering(None)
|
||||
|
||||
self.assertEqual([str(clause) for clause in ordering], ["tasks.updated_at DESC"])
|
||||
|
||||
def test_invalid_ordering_falls_back_to_latest_update_first(self):
|
||||
ordering = parse_ordering("not_a_field")
|
||||
|
||||
self.assertEqual([str(clause) for clause in ordering], ["tasks.updated_at DESC"])
|
||||
|
||||
def test_explicit_created_at_ordering_remains_supported(self):
|
||||
ordering = parse_ordering("-created_at")
|
||||
|
||||
self.assertEqual([str(clause) for clause in ordering], ["tasks.created_at DESC"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -31,7 +31,7 @@ STATUS_WRITABLE = ("completed", "abandoned") # web 不让从 web 端切回 acti
|
|||
# 改由 /v1/channel_tasks 单独取、前端做成固定卡片。新增渠道在此追加即可。
|
||||
CHANNEL_MIRROR_KINDS = ("wechat", "wecom")
|
||||
ORDER_FIELDS = ("created_at", "updated_at", "name", "status")
|
||||
ORDER_DEFAULT = "-created_at"
|
||||
ORDER_DEFAULT = "-updated_at"
|
||||
|
||||
|
||||
def norm_path(p: str) -> str:
|
||||
|
|
@ -57,7 +57,7 @@ def outline_snippet(text: Optional[str], maxlen: int = 48) -> str:
|
|||
def parse_ordering(s: Optional[str]) -> list:
|
||||
"""DRF 风格 `ordering` 解析:逗号分隔多字段,`-` 前缀代表 desc。
|
||||
|
||||
allowlist 见 `ORDER_FIELDS`;非法字段静默丢弃。全部非法或空串 → `ORDER_DEFAULT`(`-created_at`)。
|
||||
allowlist 见 `ORDER_FIELDS`;非法字段静默丢弃。全部非法或空串 → `ORDER_DEFAULT`(`-updated_at`)。
|
||||
返回 sqlalchemy `order_by` 列表(可直接 `*expand`)。
|
||||
"""
|
||||
spec = (s or "").strip() or ORDER_DEFAULT
|
||||
|
|
@ -75,7 +75,7 @@ def parse_ordering(s: Optional[str]) -> list:
|
|||
cols.append(col.asc() if asc else col.desc())
|
||||
if not cols:
|
||||
# 用户传了全无效字段 → fallback 默认
|
||||
cols = [Task.created_at.desc()]
|
||||
cols = [Task.updated_at.desc()]
|
||||
return cols
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ def register_task_routes(app, *, require_user) -> None:
|
|||
- `run_status` 逗号分隔,allowlist `idle/running/cancelling/error/cancelled`;非法值静默忽略
|
||||
(dev SPA 拉同 wd 活跃 task 用,通常 `running,cancelling`)
|
||||
- `ordering` DRF 风格,逗号分隔,`-field` 倒序;allowlist `created_at/updated_at/name/status`;
|
||||
非法字段静默忽略;**默认 `-created_at`**(创建时间倒序)
|
||||
非法字段静默忽略;**默认 `-updated_at`**(更新时间倒序)
|
||||
返回标准分页壳 `{page, page_size, count, results}` —— count 供前端算总页数。
|
||||
"""
|
||||
# clamp + sanitize
|
||||
|
|
|
|||
|
|
@ -1780,10 +1780,10 @@
|
|||
<option value="">(全部目录)</option>
|
||||
</select>
|
||||
<select id="filter-order" class="small" style="flex:1; width:auto;" title="排序方式">
|
||||
<option value="-updated_at">更新时间 ↓(新→旧)</option>
|
||||
<option value="updated_at">更新时间 ↑(旧→新)</option>
|
||||
<option value="-created_at">创建时间 ↓(新→旧)</option>
|
||||
<option value="created_at">创建时间 ↑(旧→新)</option>
|
||||
<option value="-updated_at">更新时间 ↓</option>
|
||||
<option value="updated_at">更新时间 ↑</option>
|
||||
<option value="name">名称 A→Z</option>
|
||||
<option value="-name">名称 Z→A</option>
|
||||
<option value="status,-created_at">状态分组(同状态按时间倒序)</option>
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export async function loadTaskList({ append = false } = {}) {
|
|||
const wd = $("filter-wd").value.trim();
|
||||
if (wd) params.set("working_dir", wd);
|
||||
const ord = $("filter-order").value;
|
||||
if (ord && ord !== "-created_at") params.set("ordering", ord); // 默认值不发送,URL 更干净
|
||||
if (ord && ord !== "-updated_at") params.set("ordering", ord); // 默认值不发送,URL 更干净
|
||||
state.taskLoading = true;
|
||||
setSentinel(append ? "加载中…" : "");
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in New Issue