diff --git a/CHANGELOG.md b/CHANGELOG.md index 690bf27f..11e0f0de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ 版本号遵循 [SemVer](https://semver.org/lang/zh-CN/)。所有成员包(`apps/*`、`packages/*`)与根 `deno.json` 使用同一个版本号;`/api/v1/health` 与备份清单里的版本取自 `apps/server/deno.json`。 -## [Unreleased] +## [0.3.41] - 2026-10-08 迁移 `0093`。 @@ -23,6 +23,11 @@ - 回收站并进文件管理:左边目录树点「回收站」,右侧换成回收站列表。 - 设置页左栏只列分区,不再展开节;节是页面里的小标题,搜索和带节的链接照样能定位。 - 旧的详情页地址(整理记录、订阅、站点、下载、任务、回收站)继续可用,会打开对应列表页上的抽屉。 +- 在列表页开着记录抽屉时打开智能体,会带上这条记录作为上下文;智能体回复里的整理记录、下载链接直接打开对应抽屉。 +- 抽屉打开时焦点落在抽屉本身,不再一打开就给第一个标签亮焦点框。 + +### 修复 +- 运行状态 → 数据库语句统计:相同语句文本出现多行时界面报重复 key。 ### 注意 - 会话存储格式变了:会话文件从 `agent-sessions/<会话 ID>.jsonl` 改为 `agent-sessions/<会话 ID>/` 目录。升级前已经结束的会话照常查看;升级时还在运行或挂起等待(审批、选方案、等子会话)的旧会话不能继续,会以「这个会话由旧版本创建……」失败,需要的话重新发起。 diff --git a/apps/server/deno.json b/apps/server/deno.json index 10507cae..38515425 100644 --- a/apps/server/deno.json +++ b/apps/server/deno.json @@ -1,6 +1,6 @@ { "name": "@umt/server", - "version": "0.3.40", + "version": "0.3.41", "exports": { ".": "./src/main.ts" }, "tasks": { "dev": "deno run -A --watch --env-file=../../.env src/main.ts", diff --git a/apps/server/src/modules/agent/challenge.ts b/apps/server/src/modules/agent/challenge.ts index 0d9049e6..ece278a4 100644 --- a/apps/server/src/modules/agent/challenge.ts +++ b/apps/server/src/modules/agent/challenge.ts @@ -229,7 +229,11 @@ async function solveChallenge(env: AgentEnv, registry: CapabilityRegistry, input takeover: status === "solved" ? null - : { sessionId: input.sessionId, siteId: view0.siteId, path: `/console/sites/${view0.siteId}?tab=origin` }, + : { + sessionId: input.sessionId, + siteId: view0.siteId, + path: `/console/sites?open=${encodeURIComponent(view0.siteId)}&tab=origin`, + }, }; return out; } diff --git a/apps/server/test/e2e/vision.test.ts b/apps/server/test/e2e/vision.test.ts index 05c10704..5b2dcfce 100644 --- a/apps/server/test/e2e/vision.test.ts +++ b/apps/server/test/e2e/vision.test.ts @@ -239,7 +239,7 @@ describe.skipIf(!hasPg)("E14 视觉自动化闭环", () => { expect(out.takeover).toEqual({ sessionId: sid, siteId: ids.site2fa, - path: `/console/sites/${ids.site2fa}?tab=origin`, + path: `/console/sites?open=${ids.site2fa}&tab=origin`, }); const r = childResults.get("2fa")!; // 输完密码回车后的截图:识别到二次验证,不给图片、会话已转等待 diff --git a/apps/web/app/components/agent/agent-conversation.tsx b/apps/web/app/components/agent/agent-conversation.tsx index 92d172a2..6fd2b1a3 100644 --- a/apps/web/app/components/agent/agent-conversation.tsx +++ b/apps/web/app/components/agent/agent-conversation.tsx @@ -40,6 +40,7 @@ import { Markdown } from "~/components/agentui/markdown.tsx"; import { MessageMarker, MessageTyping } from "~/components/agentui/message.tsx"; import { StreamingResponse } from "~/components/agentui/streaming-response.tsx"; import { consolePath } from "~/components/console/nav.ts"; +import { recordPath } from "~/components/console/record-sheet.tsx"; import { StatusBadge } from "~/components/console/status-badge.tsx"; import { TechInfo } from "~/components/console/tech-info.tsx"; import { formatUsd } from "~/components/schedules/labels.ts"; @@ -673,14 +674,14 @@ export function resultFacts(report: GoalReport | null): ResultFact[] { const only = r.downloads.length === 1 ? r.downloads[0] : undefined; facts.push({ text: `提交了 ${downloads} 个下载`, - to: only ? `/console/downloads/${only.downloadTaskId}` : r.downloads.length ? "/console/downloads" : undefined, + to: only ? recordPath("downloads", only.downloadTaskId) : r.downloads.length ? "/console/downloads" : undefined, }); } if (done.length) { const only = done.length === 1 ? done[0] : undefined; facts.push({ text: `完成 ${done.length} 次整理`, - to: only ? `/console/organize/${only.recordId}` : "/console/organize", + to: only ? recordPath("organize", only.recordId) : "/console/organize", }); } if (r.imported && r.imported !== done.length) facts.push({ text: `已整理 ${r.imported} 个` }); diff --git a/apps/web/app/components/agent/agent-drawer.tsx b/apps/web/app/components/agent/agent-drawer.tsx index 2f9c9730..57ac4938 100644 --- a/apps/web/app/components/agent/agent-drawer.tsx +++ b/apps/web/app/components/agent/agent-drawer.tsx @@ -109,7 +109,7 @@ export function AgentDrawer() { const content = useRef(null); // 记下最近的非智能体页面:会话页「在侧边打开」回到这里 useEffect(() => noteAgentReturnPath(pathname, search), [pathname, search]); - useEffect(() => syncAgentContextToPath(pathname), [pathname]); + useEffect(() => syncAgentContextToPath(pathname, search), [pathname, search]); const name = useContextName(context); const mock = agentMockEnabled(); // 目标前缀带名称和 ID:名称给人看,ID 给模型用 @@ -210,7 +210,7 @@ export function AgentDrawer() { /** 顶栏胶囊里的智能体按钮(没有侧栏的窄屏):与快捷键、侧栏入口同一套切换(页面内按钮用 openAgentDrawer 传更完整的上下文) */ export function AgentDrawerTrigger({ className }: { className?: string }) { - const { pathname } = useLocation(); + const { pathname, search } = useLocation(); return ( diff --git a/apps/web/app/components/agent/agent-helpers.test.ts b/apps/web/app/components/agent/agent-helpers.test.ts index 475c8a09..fc66d44b 100644 --- a/apps/web/app/components/agent/agent-helpers.test.ts +++ b/apps/web/app/components/agent/agent-helpers.test.ts @@ -80,6 +80,11 @@ describe("侧栏上下文", () => { expect(ctx?.taskId).toBe("5b4e76e0-f27f-4e7a-8e08-71486387df1d"); expect(contextFromPath("/console/sites/hdhome")?.siteId).toBe("hdhome"); expect(contextFromPath("/console/agent")).toBeNull(); + // 列表页的记录抽屉(?open=)与日志页的任务抽屉(?run=) + expect(contextFromPath("/console/sites", "?open=hdhome&tab=rss")?.siteId).toBe("hdhome"); + expect(contextFromPath("/console/organize", "?open=r1")?.recordId).toBe("r1"); + expect(contextFromPath("/console/logs", "?type=task&run=t9")?.taskId).toBe("t9"); + expect(contextFromPath("/console/sites", "?tab=rss")).toBeNull(); const goal = `${contextPrefix(ctx)}排查这个任务`; expect(splitContextGoal(goal)).toEqual({ context: expect.stringContaining("taskId="), text: "排查这个任务" }); }); diff --git a/apps/web/app/components/agent/agent-session-ui.test.ts b/apps/web/app/components/agent/agent-session-ui.test.ts index 761874db..ae65b172 100644 --- a/apps/web/app/components/agent/agent-session-ui.test.ts +++ b/apps/web/app/components/agent/agent-session-ui.test.ts @@ -662,7 +662,7 @@ describe("实际结果行(FIX-9):只列系统核实的事实", () => { createElement(ResultLine, { run: RUN, report: done, live: withText("提交了 2 个下载,整理了 1 个。") }), ); expect(text(html).replace(/\s*、\s*/g, "、")).toContain("提交了 2 个下载、完成 1 次整理、1 个进行中 · 41 秒"); - expect(html).toContain(`href="/console/organize/${ID(3)}"`); + expect(html).toContain(`href="/console/organize?open=${ID(3)}"`); expect(html).toContain('href="/console/downloads"'); expect(html).not.toContain("与汇报不一致"); }); diff --git a/apps/web/app/components/agent/agent-store.ts b/apps/web/app/components/agent/agent-store.ts index 54d5947c..97f74fa2 100644 --- a/apps/web/app/components/agent/agent-store.ts +++ b/apps/web/app/components/agent/agent-store.ts @@ -87,9 +87,9 @@ export function newAgentConversation() { * 抽屉开着时路由变了(浏览器前进后退、会话里的链接):还在新对话、上下文是按路由推断的,就换成新页面的对象; * 页面按钮带进来的上下文、已经在看的会话不动。 */ -export function syncAgentContextToPath(pathname: string) { +export function syncAgentContextToPath(pathname: string, search = "") { if (!state.open || state.runId || (state.context && !state.context.fromPath)) return; - const next = contextFromPath(pathname); + const next = contextFromPath(pathname, search); if (JSON.stringify(next) === JSON.stringify(state.context)) return; state = { ...state, context: next }; emit(); @@ -144,17 +144,24 @@ function focusAgentPageInput(): boolean { } /** 快捷键 / 侧栏主入口 / 顶栏胶囊里的按钮共用的切换 */ -export function toggleAgentDrawer(pathname: string) { +export function toggleAgentDrawer(pathname: string, search = "") { const action = toggleAction(state, pathname); if (action === "close") setAgentDrawerOpen(false); // 智能体页面本身就是会话:聚焦页面的输入框,找不到输入框才开抽屉 else if (action === "focus-page" && focusAgentPageInput()) return; else if (state.runId) setAgentDrawerOpen(true); - else openAgentDrawer(contextFromPath(pathname)); + else openAgentDrawer(contextFromPath(pathname, search)); } /** 按当前路由推断页面上下文(顶栏按钮用;页面内按钮直接传更完整的上下文) */ -export function contextFromPath(pathname: string): AgentContext | null { +export function contextFromPath(pathname: string, search = ""): AgentContext | null { + // 列表页上开着的记录抽屉(?open=,console-ia §9)等同于旧的详情页;日志页的任务抽屉是 ?run= + const q = new URLSearchParams(search); + const open = q.get("open"); + const listPage = pathname.match(/^\/console\/(sites|downloads|organize)\/?$/); + if (listPage && open) return contextFromPath(`/console/${listPage[1]}/${open}`); + const run = q.get("run"); + if (/^\/console\/logs\/?$/.test(pathname) && run) return contextFromPath(`/console/engine/${run}`); const m = pathname.match(/^\/console\/(library|sites|tasks|downloads|organize|engine)\/([^/?#]+)/); if (!m) return null; const [, kind, id] = m as unknown as [string, string, string]; diff --git a/apps/web/app/components/console/agent-launcher.tsx b/apps/web/app/components/console/agent-launcher.tsx index ce4a1312..0175494d 100644 --- a/apps/web/app/components/console/agent-launcher.tsx +++ b/apps/web/app/components/console/agent-launcher.tsx @@ -30,16 +30,16 @@ export function useActiveAgentRuns() { /** 全局快捷键:Ctrl/⌘+J 切换抽屉(Esc 关闭由抽屉自身处理) */ function useAgentHotkey() { - const { pathname } = useLocation(); + const { pathname, search } = useLocation(); useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.repeat || !isAgentShortcut(e)) return; e.preventDefault(); - toggleAgentDrawer(pathname); + toggleAgentDrawer(pathname, search); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); - }, [pathname]); + }, [pathname, search]); } /** 全局快捷键 Ctrl/⌘+J(没有界面,挂在 console layout 里一次) */ @@ -74,12 +74,12 @@ function CountBadge({ count, className }: { count: number; className?: string }) /** 侧栏最上方的 Agent 主入口:强调样式(棱镜环),显示进行中会话数 */ export function AgentNavEntry({ collapsed = false, onNavigate }: { collapsed?: boolean; onNavigate?: () => void }) { - const { pathname } = useLocation(); + const { pathname, search } = useLocation(); const { count, summary } = useActiveAgentRuns(); const shortcut = agentShortcutLabel(); const activate = () => { onNavigate?.(); - toggleAgentDrawer(pathname); + toggleAgentDrawer(pathname, search); }; const title = `智能体(${shortcut})${summary ? `\n${summary}` : ""}`; diff --git a/apps/web/app/components/console/detail-sheet.tsx b/apps/web/app/components/console/detail-sheet.tsx index 541b057b..404f5495 100644 --- a/apps/web/app/components/console/detail-sheet.tsx +++ b/apps/web/app/components/console/detail-sheet.tsx @@ -77,6 +77,11 @@ function RootSheet({ { + e.preventDefault(); + (e.currentTarget as HTMLElement | null)?.focus({ preventScroll: true }); + }} onEscapeKeyDown={(e) => { // 有子视图时 Esc 先回到上一层 const top = stack.current.at(-1); diff --git a/apps/web/app/components/status/storage-section.tsx b/apps/web/app/components/status/storage-section.tsx index bc1f9c9a..f5a84927 100644 --- a/apps/web/app/components/status/storage-section.tsx +++ b/apps/web/app/components/status/storage-section.tsx @@ -207,10 +207,12 @@ const fmtMs = (ms: number) => ms >= 1000 ? `${(ms / 1000).toFixed(1)} 秒` : `${ms < 10 ? ms.toFixed(2) : Math.round(ms)} 毫秒`; function StatementTable({ rows }: { rows: StatementRow[] }) { + // 规范化后的语句文本可能重复(不同角色 / 库各一行),用序号做 key + const keyed = rows.map((r, i) => ({ ...r, rowIndex: i })); return ( - - rows={rows} - rowKey={(r) => r.query} + + rows={keyed} + rowKey={(r) => String(r.rowIndex)} empty={{ title: "还没有统计" }} columns={[ { diff --git a/apps/web/package.json b/apps/web/package.json index 5596d637..f13781ae 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@umt/web", - "version": "0.3.40", + "version": "0.3.41", "private": true, "type": "module", "scripts": { diff --git a/deno.json b/deno.json index 1b02d7dc..143f6e3e 100644 --- a/deno.json +++ b/deno.json @@ -1,5 +1,5 @@ { - "version": "0.3.40", + "version": "0.3.41", "workspace": [ "./packages/adapters", "./packages/agent", diff --git a/packages/adapters/deno.json b/packages/adapters/deno.json index 04eec5a9..ba6d379b 100644 --- a/packages/adapters/deno.json +++ b/packages/adapters/deno.json @@ -1,6 +1,6 @@ { "name": "@umt/adapters", - "version": "0.3.40", + "version": "0.3.41", "exports": { ".": "./src/index.ts", "./fsworker": "./src/fs/fsworker.ts", "./alist-mock": "./src/alist/mock.ts" }, "tasks": { "test": "deno run -A npm:vitest run", diff --git a/packages/agent/deno.json b/packages/agent/deno.json index 5ee1e6c0..ae649588 100644 --- a/packages/agent/deno.json +++ b/packages/agent/deno.json @@ -1,6 +1,6 @@ { "name": "@umt/agent", - "version": "0.3.40", + "version": "0.3.41", "exports": { ".": "./src/index.ts" }, "tasks": { "test": "deno run -A npm:vitest run", diff --git a/packages/contracts/deno.json b/packages/contracts/deno.json index 31083430..8d824809 100644 --- a/packages/contracts/deno.json +++ b/packages/contracts/deno.json @@ -1,6 +1,6 @@ { "name": "@umt/contracts", - "version": "0.3.40", + "version": "0.3.41", "exports": { ".": "./src/index.ts" }, "tasks": { "test": "deno run -A npm:vitest run", diff --git a/packages/db/deno.json b/packages/db/deno.json index 74e96157..e81752dc 100644 --- a/packages/db/deno.json +++ b/packages/db/deno.json @@ -1,6 +1,6 @@ { "name": "@umt/db", - "version": "0.3.40", + "version": "0.3.41", "exports": { ".": "./src/index.ts" }, "tasks": { "test": "deno run -A npm:vitest run", diff --git a/packages/domain/deno.json b/packages/domain/deno.json index a2451dd9..504e889b 100644 --- a/packages/domain/deno.json +++ b/packages/domain/deno.json @@ -1,6 +1,6 @@ { "name": "@umt/domain", - "version": "0.3.40", + "version": "0.3.41", "exports": { ".": "./src/index.ts" }, "tasks": { "test": "deno run -A npm:vitest run", diff --git a/packages/mapping/deno.json b/packages/mapping/deno.json index 469a76e8..1b42b3c4 100644 --- a/packages/mapping/deno.json +++ b/packages/mapping/deno.json @@ -1,6 +1,6 @@ { "name": "@umt/mapping", - "version": "0.3.40", + "version": "0.3.41", "exports": { ".": "./src/index.ts" }, "tasks": { "test": "deno run -A npm:vitest run", diff --git a/packages/tasks/deno.json b/packages/tasks/deno.json index 886626ec..af8a8131 100644 --- a/packages/tasks/deno.json +++ b/packages/tasks/deno.json @@ -1,6 +1,6 @@ { "name": "@umt/tasks", - "version": "0.3.40", + "version": "0.3.41", "exports": { ".": "./src/index.ts" }, "tasks": { "test": "deno run -A npm:vitest run", diff --git a/packages/testkit/deno.json b/packages/testkit/deno.json index e963a4a1..cec4b794 100644 --- a/packages/testkit/deno.json +++ b/packages/testkit/deno.json @@ -1,6 +1,6 @@ { "name": "@umt/testkit", - "version": "0.3.40", + "version": "0.3.41", "exports": { ".": "./src/index.ts" }, "tasks": { "test": "deno run -A npm:vitest run",