diff --git a/CHANGELOG.md b/CHANGELOG.md index 80613445..1b8d75d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ ## [Unreleased] +### 调整 +- 站点页按类型分组(PT 站 / 公开 BT 站;旧的 RSS 源有的话单列),去掉类型筛选;添加站点只保留 PT 站和内置公开 BT 站,自定义 RSS 源与索引器入口先下掉(已有的照常显示)。 +- 站点列表模式:操作按钮改成图标,数值列不换行,窄屏隐藏次要列;公开 BT 站的表只列出口、资源搜索、资源更新;公开站卡片不再显示 RSS。 +- 智能体页会话分两栏:对话(用户发起)/ 自动巡检(定时、事件触发),子会话随父会话。 + ## [0.3.19] - 2026-10-03 ### 修复 diff --git a/apps/server/src/modules/agent/routes.ts b/apps/server/src/modules/agent/routes.ts index 8d1c6341..75acf024 100644 --- a/apps/server/src/modules/agent/routes.ts +++ b/apps/server/src/modules/agent/routes.ts @@ -122,6 +122,12 @@ export function registerAgentRoutes( if (query.status) q = q.where("status", "=", query.status); if (query.trigger) q = q.where("trigger", "=", query.trigger); if (query.parentRunId) q = q.where("parent_run_id", "=", query.parentRunId); + if (query.group) { + // 自动 = 定时 / 事件触发,子会话按父会话归类 + const auto = sql`(trigger in ('schedule', 'event') or (trigger = 'subagent' and parent_run_id in + (select p.id from agent_run p where p.trigger in ('schedule', 'event'))))`; + q = q.where(query.group === "auto" ? auto : sql`not ${auto}`); + } const cur = decode(query.cursor); if (cur) q = q.where(sql`(created_at, id) < (${cur.at}::timestamptz, ${cur.id}::uuid)`); const rows = await q diff --git a/apps/server/test/agent/run-groups.test.ts b/apps/server/test/agent/run-groups.test.ts new file mode 100644 index 00000000..5c5ae308 --- /dev/null +++ b/apps/server/test/agent/run-groups.test.ts @@ -0,0 +1,43 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { createAgentModule } from "../../src/modules/agent/index.ts"; +import { createHarness, type Harness, pgAvailable } from "../auth-settings/harness.ts"; +import { DEV_KEY, json } from "../helpers.ts"; + +// 智能体页会话分栏:对话(用户发起)/ 自动巡检(定时、事件触发);子会话随父会话归类 +const hasPg = await pgAvailable(); + +describe.skipIf(!hasPg)("会话列表 group 筛选(真实 PG)", () => { + let h: Harness; + beforeAll(async () => { + h = await createHarness({ extraModules: [createAgentModule()] }); + }, 120_000); + afterAll(async () => { + await h?.close(); + }); + + it("auto = schedule / event 及其子会话;conversation = 其余", async () => { + const run = async (trigger: string, goal: string, parent?: string) => + ( + await h.db + .insertInto("agent_run") + .values({ trigger, goal, status: "succeeded", mode: "execute", parent_run_id: parent ?? null } as never) + .returning("id") + .executeTakeFirstOrThrow() + ).id; + const patrol = await run("schedule", "巡检"); + await run("subagent", "巡检子会话", patrol); + await run("event", "事件"); + const chat = await run("user", "对话"); + await run("subagent", "对话子会话", chat); + const goals = async (group: string) => + ( + await json( + await h.app.request(`/api/v1/agent/runs?group=${group}`, { headers: { authorization: `Bearer ${DEV_KEY}` } }), + ) + ).data.items + .map((r: { goal: string }) => r.goal) + .sort(); + expect(await goals("auto")).toEqual(["事件", "巡检", "巡检子会话"].sort()); + expect(await goals("conversation")).toEqual(["对话", "对话子会话"].sort()); + }); +}); diff --git a/apps/web/app/api/agent.ts b/apps/web/app/api/agent.ts index e70d8b89..bc3ca0f5 100644 --- a/apps/web/app/api/agent.ts +++ b/apps/web/app/api/agent.ts @@ -120,8 +120,8 @@ function useAgentMockGate(): boolean { export const agentKeys = { all: ["agent"] as const, - runs: (status?: AgentRunStatus, trigger?: AgentTrigger) => - ["agent", "list", status ?? "all", trigger ?? "any"] as const, + runs: (status?: AgentRunStatus, trigger?: AgentTrigger, group?: AgentRunGroup) => + ["agent", "list", status ?? "all", trigger ?? "any", group ?? "any"] as const, run: (id: string) => ["agent", "detail", id] as const, }; @@ -137,19 +137,28 @@ export const TRIGGER_LABEL: Record = { }; /** 会话列表:按状态、来源(trigger)在服务端筛选,游标翻页 */ -export function useAgentRuns(status?: AgentRunStatus, trigger?: AgentTrigger) { +/** 会话分栏:对话 = 用户发起;自动 = 定时巡检 / 事件触发(子会话随父会话) */ +export type AgentRunGroup = "conversation" | "auto"; +const isAutoTrigger = (t: AgentTrigger) => t === "schedule" || t === "event"; + +export function useAgentRuns(status?: AgentRunStatus, trigger?: AgentTrigger, group?: AgentRunGroup) { const mock = useAgentMockGate(); return useInfiniteQuery({ - queryKey: [...agentKeys.runs(status, trigger), mock], + queryKey: [...agentKeys.runs(status, trigger, group), mock], initialPageParam: undefined as string | undefined, queryFn: async ({ pageParam, signal }) => { if (mock) return { - items: MOCK_RUNS().filter((r) => (!status || r.status === status) && (!trigger || r.trigger === trigger)), + items: MOCK_RUNS().filter( + (r) => + (!status || r.status === status) && + (!trigger || r.trigger === trigger) && + (!group || isAutoTrigger(r.trigger) === (group === "auto")), + ), nextCursor: null, }; return apiParsed(RunPage, AGENT_BASE, { - params: cleanParams({ status, trigger, cursor: pageParam, limit: 30 }), + params: cleanParams({ status, trigger, group, cursor: pageParam, limit: 30 }), signal, }); }, diff --git a/apps/web/app/components/agent/agent-session-list.tsx b/apps/web/app/components/agent/agent-session-list.tsx index 434e92e7..6600dddf 100644 --- a/apps/web/app/components/agent/agent-session-list.tsx +++ b/apps/web/app/components/agent/agent-session-list.tsx @@ -1,23 +1,29 @@ import { Bot, ChevronRight } from "lucide-react"; import { type ReactNode, useLayoutEffect, useRef, useState } from "react"; import { Link } from "react-router"; -import { type AgentRunView, TRIGGER_LABEL, useAgentRuns } from "~/api/agent.ts"; +import { type AgentRunGroup, type AgentRunView, TRIGGER_LABEL, useAgentRuns } from "~/api/agent.ts"; import { CHEVRON_CLASS } from "~/components/agentui/disclosure.tsx"; import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx"; import { useListPagination } from "~/components/console/use-list-pagination.ts"; import { Input } from "~/components/ui/input.tsx"; import { Pagination } from "~/components/ui/pagination.tsx"; +import { Tabs, TabsList, TabsTrigger } from "~/components/ui/tabs.tsx"; import { relTime } from "~/components/workbench/release-fields.ts"; import { cn } from "~/lib/utils.ts"; import { contextLabel, RunStatusBadge } from "./agent-conversation.tsx"; import { shortGoalTitle, splitContextGoal } from "./agent-store.ts"; // 会话列表(AGUI-3):会话页左栏与抽屉的「历史」弹层共用。 -// 筛选只有搜索 + 全部 / 等你处理 / 失败;「等你处理」(等选方案、等审批)置顶;子会话收在父会话下面。 +// 分两栏:对话(用户发起)/ 自动巡检(定时、事件触发),子会话随父会话;栏内筛选只有搜索 + 全部 / 等你处理 / 失败;「等你处理」(等选方案、等审批)置顶;子会话收在父会话下面。 export type HistoryView = "all" | "pending" | "failed"; -export type HistoryFilters = { q: string; view: HistoryView }; -export const EMPTY_FILTERS: HistoryFilters = { q: "", view: "all" }; +export type HistoryFilters = { q: string; view: HistoryView; group: AgentRunGroup }; +export const EMPTY_FILTERS: HistoryFilters = { q: "", view: "all", group: "conversation" }; + +const GROUPS: { value: AgentRunGroup; label: string }[] = [ + { value: "conversation", label: "对话" }, + { value: "auto", label: "自动巡检" }, +]; const VIEWS: { value: HistoryView; label: string }[] = [ { value: "all", label: "全部" }, @@ -45,7 +51,7 @@ export type HistoryRow = { run: AgentRunView; children: AgentRunView[] }; */ export function arrangeHistory( runs: AgentRunView[], - f: HistoryFilters, + f: Pick, limit = Number.POSITIVE_INFINITY, ): { pinned: HistoryRow[]; rows: HistoryRow[] } { const uniq = [...new Map(runs.map((r) => [r.id, r] as const)).values()]; @@ -204,9 +210,9 @@ export function AgentSessionList({ if (remember) drawerMemory.filters = next; return next; }); - const runs = useAgentRuns(); + const runs = useAgentRuns(undefined, undefined, f.group); // 等你处理的会话可能是很早以前建的:单独按 waiting 取一页,保证置顶 - const waiting = useAgentRuns("waiting"); + const waiting = useAgentRuns("waiting", undefined, f.group); const loaded = runs.data?.pages.flatMap((p) => p.items) ?? []; const pending = waiting.data?.pages.flatMap((p) => p.items) ?? []; const arranged = arrangeHistory([...pending.filter(awaitingUser), ...loaded], f); @@ -236,6 +242,15 @@ export function AgentSessionList({ return (