feat(activity): 任务会话历史反向分页 + SSE 可靠性整改

后端:
- ActivityPage(会话过滤 main/plan/intent + before 反向分页)、ActivityMaxID
  (任务级快照游标)、ListByKindPage(Intent 列表分页,突破固定 300 上限)
- 新增 GET /api/exploration/activity/history(items/snapshot_cursor/
  earliest_cursor/has_more);snapshot_cursor 为任务级游标
- streamActivity:一次 2000 → 循环补偿追平;输出标准 SSE id 行;支持
  Last-Event-ID;补偿查询错误记录日志并关闭连接
- intents 支持分页形态(?before/?page → {items,has_more}),无参数保持旧裸数组
- 新增 idx_act_worker(exploration_id, worker, id) 支撑 Main/Plan 反向翻页
  (schema.sql 幂等,旧库启动自动补建,无迁移)

前端:
- sessions-tab 单一 allActivity → 分会话缓存;首屏只加载 Main 最新页并据其
  快照游标建唯一任务级 SSE;Plan/Worker 点击懒加载
- 统一 mergeBySeq 合并去重;未激活会话只维护 lastTs/unread;向上滚动分页
  加载旧历史并保持滚动位置;每会话内存上限裁剪
- 按 session_key 写各自缓存 + 每 key 请求令牌防会话切换串数据;SSE 连接状态
  可见、加载失败可重试,异常不再静默为"暂无消息"
- api 新增 activityHistory / intentsPage

测试:db/activity_page_test.go 覆盖多 Agent 过滤、反向翻页无重复无遗漏、
快照游标、Intent 超页分页(已对真实 PostgreSQL 通过)
This commit is contained in:
Autumn-27 committed 2026-08-11 09:13:12 -04:00
1 parent 95aba396c2
commit 79493d5a4a
6 files changed
+906 -130

No files matched your search

+30
View File
@@ -247,6 +247,36 @@ export const api = {
return get<{ items: Activity[]; cursor: number }>(`/exploration/activity?${q.toString()}`).then((r) => ({ items: arr(r.items), cursor: r.cursor ?? 0 }));
},
activityDetail: (id: number, task?: string) => get<{ detail: string }>(`/exploration/activity/${id}${tq(task)}`),
// Reverse-paginated session history. session = "main" | "plan" | "intent:<id>".
// before=0 → latest page; before=<id> → the older page ending before that id.
// snapshotCursor is the TASK-level max id at query time — open the task SSE at
// since=snapshotCursor so history (id≤cursor) and the live tail (id>cursor) meet
// gap-free. hasMore = still-older steps exist (drives scroll-up loading).
activityHistory: (task: string, session: string, before = 0, limit = 200) => {
const q = new URLSearchParams({ session, limit: String(limit) });
if (task) q.set("task", task);
if (before > 0) q.set("before", String(before));
return get<{ items: Activity[]; snapshot_cursor: number; earliest_cursor: number; has_more: boolean }>(
`/exploration/activity/history?${q.toString()}`,
).then((r) => ({
items: arr(r.items),
snapshotCursor: r.snapshot_cursor ?? 0,
earliestCursor: r.earliest_cursor ?? 0,
hasMore: !!r.has_more,
}));
},
// Paged worker (intent) session list — reaches past the legacy fixed 300 cap.
// before=0 → newest page; before=<id> → older page. has_more = older intents exist.
intentsPage: (task: string, before = 0, limit = 300) => {
const q = new URLSearchParams({ limit: String(limit) });
if (task) q.set("task", task);
q.set("before", String(before > 0 ? before : 0));
q.set("page", "1"); // marker so the backend returns the paged {items,has_more} shape
return get<{ items: TaskNode[]; has_more: boolean }>(`/exploration/intents?${q.toString()}`).then((r) => ({
items: arr(r.items),
hasMore: !!r.has_more,
}));
},
// ---- traffic / audit / report / chat ----
audit: (task?: string) => get<Audit>(`/audit${tq(task)}`),