feat(web): 任务详情页新增探索链路播报板

按创建时间分页平铺展示探索节点,作为探索链路图的时间轴视图,
复用同一份 exploration_nodes 数据,不影响链路图。

- 后端 NodesPage/NodesByIDs/EdgesTouching:分页下推 SQL,
  支持 kind/state 过滤、payload/origin 搜索、正倒序,
  并补出当页节点的相邻边与对端节点
- 新增 GET /api/exploration/nodes(仅本任务节点,不含继承)
- 播报板:时间轴分行、按天分组、类型快筛、搜索防抖、
  自动刷新、直播位未读提示、展开看上下游与 payload
- 补 mock,NEXT_PUBLIC_MOCK=1 下可无后端预览
This commit is contained in:
Autumn-27 committed 2026-09-17 08:42:49 -04:00
1 parent 13ce2ea6e3
commit c03e008d7b
7 files changed
+1053 -37

No files matched your search

+34 -8
View File
@@ -31,6 +31,8 @@ import type {
DeleteTaskResult,
Edge,
EvidenceBodyPreview,
ExplorationNodePage,
ExplorationNodeQuery,
Finding,
FindingAssetTree,
FindingDeepenResponse,
@@ -655,6 +657,25 @@ export const api = {
.then(arr)
.catch(() => [] as ConvTokenSummary[]),
explorationGraph: (task?: string) => get<{ nodes: TaskNode[]; edges: Edge[] }>(`/exploration/graph${tq(task)}`),
// 播报板:服务端按创建顺序分页的探索节点(默认最新在前)。
explorationNodes: (task: string, query: ExplorationNodeQuery = {}) => {
const q = new URLSearchParams();
if (task) q.set("task", task);
q.set("page", String(query.page ?? 1));
q.set("size", String(query.size ?? 20));
if (query.kinds?.length) q.set("kind", query.kinds.join(","));
if (query.states?.length) q.set("state", query.states.join(","));
if (query.q?.trim()) q.set("q", query.q.trim());
if (query.order === "asc") q.set("order", "asc");
return get<ExplorationNodePage>(`/exploration/nodes?${q.toString()}`).then((r) => ({
items: arr(r.items),
total: r.total ?? 0,
page: r.page ?? 1,
size: r.size ?? query.size ?? 20,
edges: arr(r.edges),
refs: r.refs ?? {},
}));
},
activity: (task?: string, opts?: { intent?: string; since?: number; limit?: number }) => {
const q = new URLSearchParams();
if (task) q.set("task", task);
@@ -742,7 +763,10 @@ export const api = {
return r.text();
},
chatMentions: (kind: string, query: string, signal?: AbortSignal, cursor = "") =>
http<{ items: ChatMention[]; next_cursor?: string }>(`/chat/mentions?${new URLSearchParams({ kind, q: query, cursor })}`, { signal }),
http<{ items: ChatMention[]; next_cursor?: string }>(
`/chat/mentions?${new URLSearchParams({ kind, q: query, cursor })}`,
{ signal },
),
chat: (message: string, task?: string, attachments?: ChatAttachment[], seg?: number) =>
post<{ reply: string; mode: string }>(`/chat${tq(task)}`, { message, attachments, seg }),
chatStatus: (taskId: string) => get<{ running: boolean }>(`/tasks/${taskId}/chat/status`),
@@ -1073,16 +1097,18 @@ export const api = {
interceptDecide: (id: number, decision: "allowed" | "denied") =>
post<{ ok: boolean }>(`/intercept/pending/${id}/decide`, { decision }),
interceptExecution: (id: number, conversationId?: number) =>
get<import("@/lib/types").InterceptExecution>(`/intercept/history/${id}/execution${conversationId ? `?conversation=${conversationId}` : ""}`),
get<import("@/lib/types").InterceptExecution>(
`/intercept/history/${id}/execution${conversationId ? `?conversation=${conversationId}` : ""}`,
),
interceptDetail: (id: number) => get<InterceptDetail>(`/intercept/history/${id}`),
interceptHistory: () => get<{ items: InterceptApprovalRow[] }>("/intercept/history").then((r) => arr(r.items)),
interceptHistoryPage: (page = 1, size = 20, filter: InterceptApprovalFilter = {}) =>
get<{ items: InterceptApprovalRow[]; total?: number }>(`/intercept/history?${interceptPageQuery(page, size, filter)}`).then(
(r) => ({
items: arr(r.items),
total: r.total ?? r.items?.length ?? 0,
}),
),
get<{ items: InterceptApprovalRow[]; total?: number }>(
`/intercept/history?${interceptPageQuery(page, size, filter)}`,
).then((r) => ({
items: arr(r.items),
total: r.total ?? r.items?.length ?? 0,
})),
interceptTask: (taskId: string) =>
get<{ items: InterceptApprovalRow[] }>(`/intercept/task/${taskId}`).then((r) => arr(r.items)),
interceptTaskPage: (taskId: string, page = 1, size = 20, filter: InterceptApprovalFilter = {}) =>