chore(release): v0.3.41

智能体抽屉读 ?open= 上下文、回复链接用抽屉地址;抽屉打开不亮焦点框;语句统计重复 key。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-08 14:39:24 +08:00
1 parent 78a4d562f0
commit 1ade49fe8e
22 files changed
+62 -33

No files matched your search

+6 -1
View File
@@ -2,7 +2,7 @@
版本号遵循 [SemVer](https://semver.org/lang/zh-CN/)。所有成员包(`apps/*`、`packages/*`)与根 `deno.json` 使用同一个版本号;`/api/v1/health` 与备份清单里的版本取自 `apps/server/deno.json`。 版本号遵循 [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`。 迁移 `0093`。
@@ -23,6 +23,11 @@
- 回收站并进文件管理:左边目录树点「回收站」,右侧换成回收站列表。 - 回收站并进文件管理:左边目录树点「回收站」,右侧换成回收站列表。
- 设置页左栏只列分区,不再展开节;节是页面里的小标题,搜索和带节的链接照样能定位。 - 设置页左栏只列分区,不再展开节;节是页面里的小标题,搜索和带节的链接照样能定位。
- 旧的详情页地址(整理记录、订阅、站点、下载、任务、回收站)继续可用,会打开对应列表页上的抽屉。 - 旧的详情页地址(整理记录、订阅、站点、下载、任务、回收站)继续可用,会打开对应列表页上的抽屉。
- 在列表页开着记录抽屉时打开智能体,会带上这条记录作为上下文;智能体回复里的整理记录、下载链接直接打开对应抽屉。
- 抽屉打开时焦点落在抽屉本身,不再一打开就给第一个标签亮焦点框。
### 修复
- 运行状态 → 数据库语句统计:相同语句文本出现多行时界面报重复 key。
### 注意 ### 注意
- 会话存储格式变了:会话文件从 `agent-sessions/<会话 ID>.jsonl` 改为 `agent-sessions/<会话 ID>/` 目录。升级前已经结束的会话照常查看;升级时还在运行或挂起等待(审批、选方案、等子会话)的旧会话不能继续,会以「这个会话由旧版本创建……」失败,需要的话重新发起。 - 会话存储格式变了:会话文件从 `agent-sessions/<会话 ID>.jsonl` 改为 `agent-sessions/<会话 ID>/` 目录。升级前已经结束的会话照常查看;升级时还在运行或挂起等待(审批、选方案、等子会话)的旧会话不能继续,会以「这个会话由旧版本创建……」失败,需要的话重新发起。
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@umt/server", "name": "@umt/server",
"version": "0.3.40", "version": "0.3.41",
"exports": { ".": "./src/main.ts" }, "exports": { ".": "./src/main.ts" },
"tasks": { "tasks": {
"dev": "deno run -A --watch --env-file=../../.env src/main.ts", "dev": "deno run -A --watch --env-file=../../.env src/main.ts",
+5 -1
View File
@@ -229,7 +229,11 @@ async function solveChallenge(env: AgentEnv, registry: CapabilityRegistry, input
takeover: takeover:
status === "solved" status === "solved"
? null ? 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; return out;
} }
+1 -1
View File
@@ -239,7 +239,7 @@ describe.skipIf(!hasPg)("E14 视觉自动化闭环", () => {
expect(out.takeover).toEqual({ expect(out.takeover).toEqual({
sessionId: sid, sessionId: sid,
siteId: ids.site2fa, siteId: ids.site2fa,
path: `/console/sites/${ids.site2fa}?tab=origin`, path: `/console/sites?open=${ids.site2fa}&tab=origin`,
}); });
const r = childResults.get("2fa")!; const r = childResults.get("2fa")!;
// 输完密码回车后的截图:识别到二次验证,不给图片、会话已转等待 // 输完密码回车后的截图:识别到二次验证,不给图片、会话已转等待
@@ -40,6 +40,7 @@ import { Markdown } from "~/components/agentui/markdown.tsx";
import { MessageMarker, MessageTyping } from "~/components/agentui/message.tsx"; import { MessageMarker, MessageTyping } from "~/components/agentui/message.tsx";
import { StreamingResponse } from "~/components/agentui/streaming-response.tsx"; import { StreamingResponse } from "~/components/agentui/streaming-response.tsx";
import { consolePath } from "~/components/console/nav.ts"; import { consolePath } from "~/components/console/nav.ts";
import { recordPath } from "~/components/console/record-sheet.tsx";
import { StatusBadge } from "~/components/console/status-badge.tsx"; import { StatusBadge } from "~/components/console/status-badge.tsx";
import { TechInfo } from "~/components/console/tech-info.tsx"; import { TechInfo } from "~/components/console/tech-info.tsx";
import { formatUsd } from "~/components/schedules/labels.ts"; 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; const only = r.downloads.length === 1 ? r.downloads[0] : undefined;
facts.push({ facts.push({
text: `提交了 ${downloads} 个下载`, 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) { if (done.length) {
const only = done.length === 1 ? done[0] : undefined; const only = done.length === 1 ? done[0] : undefined;
facts.push({ facts.push({
text: `完成 ${done.length} 次整理`, 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} 个` }); if (r.imported && r.imported !== done.length) facts.push({ text: `已整理 ${r.imported} 个` });
@@ -109,7 +109,7 @@ export function AgentDrawer() {
const content = useRef<HTMLDivElement>(null); const content = useRef<HTMLDivElement>(null);
// 记下最近的非智能体页面:会话页「在侧边打开」回到这里 // 记下最近的非智能体页面:会话页「在侧边打开」回到这里
useEffect(() => noteAgentReturnPath(pathname, search), [pathname, search]); useEffect(() => noteAgentReturnPath(pathname, search), [pathname, search]);
useEffect(() => syncAgentContextToPath(pathname), [pathname]); useEffect(() => syncAgentContextToPath(pathname, search), [pathname, search]);
const name = useContextName(context); const name = useContextName(context);
const mock = agentMockEnabled(); const mock = agentMockEnabled();
// 目标前缀带名称和 ID:名称给人看,ID 给模型用 // 目标前缀带名称和 ID:名称给人看,ID 给模型用
@@ -210,7 +210,7 @@ export function AgentDrawer() {
/** 顶栏胶囊里的智能体按钮(没有侧栏的窄屏):与快捷键、侧栏入口同一套切换(页面内按钮用 openAgentDrawer 传更完整的上下文) */ /** 顶栏胶囊里的智能体按钮(没有侧栏的窄屏):与快捷键、侧栏入口同一套切换(页面内按钮用 openAgentDrawer 传更完整的上下文) */
export function AgentDrawerTrigger({ className }: { className?: string }) { export function AgentDrawerTrigger({ className }: { className?: string }) {
const { pathname } = useLocation(); const { pathname, search } = useLocation();
return ( return (
<Button <Button
variant="ghost" variant="ghost"
@@ -219,7 +219,7 @@ export function AgentDrawerTrigger({ className }: { className?: string }) {
title={`智能体(${agentShortcutLabel()})`} title={`智能体(${agentShortcutLabel()})`}
className={className} className={className}
{...{ [AGENT_LAUNCHER_ATTR]: "" }} {...{ [AGENT_LAUNCHER_ATTR]: "" }}
onClick={() => toggleAgentDrawer(pathname)} onClick={() => toggleAgentDrawer(pathname, search)}
> >
<Bot /> <Bot />
</Button> </Button>
@@ -80,6 +80,11 @@ describe("侧栏上下文", () => {
expect(ctx?.taskId).toBe("5b4e76e0-f27f-4e7a-8e08-71486387df1d"); expect(ctx?.taskId).toBe("5b4e76e0-f27f-4e7a-8e08-71486387df1d");
expect(contextFromPath("/console/sites/hdhome")?.siteId).toBe("hdhome"); expect(contextFromPath("/console/sites/hdhome")?.siteId).toBe("hdhome");
expect(contextFromPath("/console/agent")).toBeNull(); 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)}排查这个任务`; const goal = `${contextPrefix(ctx)}排查这个任务`;
expect(splitContextGoal(goal)).toEqual({ context: expect.stringContaining("taskId="), text: "排查这个任务" }); expect(splitContextGoal(goal)).toEqual({ context: expect.stringContaining("taskId="), text: "排查这个任务" });
}); });
@@ -662,7 +662,7 @@ describe("实际结果行(FIX-9):只列系统核实的事实", () => {
createElement(ResultLine, { run: RUN, report: done, live: withText("提交了 2 个下载,整理了 1 个。") }), createElement(ResultLine, { run: RUN, report: done, live: withText("提交了 2 个下载,整理了 1 个。") }),
); );
expect(text(html).replace(/\s*、\s*/g, "、")).toContain("提交了 2 个下载、完成 1 次整理、1 个进行中 · 41 秒"); 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).toContain('href="/console/downloads"');
expect(html).not.toContain("与汇报不一致"); expect(html).not.toContain("与汇报不一致");
}); });
+12 -5
View File
@@ -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; 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; if (JSON.stringify(next) === JSON.stringify(state.context)) return;
state = { ...state, context: next }; state = { ...state, context: next };
emit(); emit();
@@ -144,17 +144,24 @@ function focusAgentPageInput(): boolean {
} }
/** 快捷键 / 侧栏主入口 / 顶栏胶囊里的按钮共用的切换 */ /** 快捷键 / 侧栏主入口 / 顶栏胶囊里的按钮共用的切换 */
export function toggleAgentDrawer(pathname: string) { export function toggleAgentDrawer(pathname: string, search = "") {
const action = toggleAction(state, pathname); const action = toggleAction(state, pathname);
if (action === "close") setAgentDrawerOpen(false); if (action === "close") setAgentDrawerOpen(false);
// 智能体页面本身就是会话:聚焦页面的输入框,找不到输入框才开抽屉 // 智能体页面本身就是会话:聚焦页面的输入框,找不到输入框才开抽屉
else if (action === "focus-page" && focusAgentPageInput()) return; else if (action === "focus-page" && focusAgentPageInput()) return;
else if (state.runId) setAgentDrawerOpen(true); 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)\/([^/?#]+)/); const m = pathname.match(/^\/console\/(library|sites|tasks|downloads|organize|engine)\/([^/?#]+)/);
if (!m) return null; if (!m) return null;
const [, kind, id] = m as unknown as [string, string, string]; const [, kind, id] = m as unknown as [string, string, string];
@@ -30,16 +30,16 @@ export function useActiveAgentRuns() {
/** 全局快捷键:Ctrl/⌘+J 切换抽屉(Esc 关闭由抽屉自身处理) */ /** 全局快捷键:Ctrl/⌘+J 切换抽屉(Esc 关闭由抽屉自身处理) */
function useAgentHotkey() { function useAgentHotkey() {
const { pathname } = useLocation(); const { pathname, search } = useLocation();
useEffect(() => { useEffect(() => {
const onKey = (e: KeyboardEvent) => { const onKey = (e: KeyboardEvent) => {
if (e.repeat || !isAgentShortcut(e)) return; if (e.repeat || !isAgentShortcut(e)) return;
e.preventDefault(); e.preventDefault();
toggleAgentDrawer(pathname); toggleAgentDrawer(pathname, search);
}; };
window.addEventListener("keydown", onKey); window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey);
}, [pathname]); }, [pathname, search]);
} }
/** 全局快捷键 Ctrl/⌘+J(没有界面,挂在 console layout 里一次) */ /** 全局快捷键 Ctrl/⌘+J(没有界面,挂在 console layout 里一次) */
@@ -74,12 +74,12 @@ function CountBadge({ count, className }: { count: number; className?: string })
/** 侧栏最上方的 Agent 主入口:强调样式(棱镜环),显示进行中会话数 */ /** 侧栏最上方的 Agent 主入口:强调样式(棱镜环),显示进行中会话数 */
export function AgentNavEntry({ collapsed = false, onNavigate }: { collapsed?: boolean; onNavigate?: () => void }) { export function AgentNavEntry({ collapsed = false, onNavigate }: { collapsed?: boolean; onNavigate?: () => void }) {
const { pathname } = useLocation(); const { pathname, search } = useLocation();
const { count, summary } = useActiveAgentRuns(); const { count, summary } = useActiveAgentRuns();
const shortcut = agentShortcutLabel(); const shortcut = agentShortcutLabel();
const activate = () => { const activate = () => {
onNavigate?.(); onNavigate?.();
toggleAgentDrawer(pathname); toggleAgentDrawer(pathname, search);
}; };
const title = `智能体(${shortcut})${summary ? `\n${summary}` : ""}`; const title = `智能体(${shortcut})${summary ? `\n${summary}` : ""}`;
@@ -77,6 +77,11 @@ function RootSheet({
<SheetContent <SheetContent
side="right" side="right"
className={cn("w-full gap-0 p-0", WIDTH[width], className)} className={cn("w-full gap-0 p-0", WIDTH[width], className)}
// 打开时焦点落在抽屉本身而不是第一个按钮 / 标签,避免一打开就亮焦点框;Tab 仍从抽屉内开始
onOpenAutoFocus={(e) => {
e.preventDefault();
(e.currentTarget as HTMLElement | null)?.focus({ preventScroll: true });
}}
onEscapeKeyDown={(e) => { onEscapeKeyDown={(e) => {
// 有子视图时 Esc 先回到上一层 // 有子视图时 Esc 先回到上一层
const top = stack.current.at(-1); const top = stack.current.at(-1);
@@ -207,10 +207,12 @@ const fmtMs = (ms: number) =>
ms >= 1000 ? `${(ms / 1000).toFixed(1)} 秒` : `${ms < 10 ? ms.toFixed(2) : Math.round(ms)} 毫秒`; ms >= 1000 ? `${(ms / 1000).toFixed(1)} 秒` : `${ms < 10 ? ms.toFixed(2) : Math.round(ms)} 毫秒`;
function StatementTable({ rows }: { rows: StatementRow[] }) { function StatementTable({ rows }: { rows: StatementRow[] }) {
// 规范化后的语句文本可能重复(不同角色 / 库各一行),用序号做 key
const keyed = rows.map((r, i) => ({ ...r, rowIndex: i }));
return ( return (
<DataTable<StatementRow> <DataTable<StatementRow & { rowIndex: number }>
rows={rows} rows={keyed}
rowKey={(r) => r.query} rowKey={(r) => String(r.rowIndex)}
empty={{ title: "还没有统计" }} empty={{ title: "还没有统计" }}
columns={[ columns={[
{ {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@umt/web", "name": "@umt/web",
"version": "0.3.40", "version": "0.3.41",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"version": "0.3.40", "version": "0.3.41",
"workspace": [ "workspace": [
"./packages/adapters", "./packages/adapters",
"./packages/agent", "./packages/agent",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@umt/adapters", "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" }, "exports": { ".": "./src/index.ts", "./fsworker": "./src/fs/fsworker.ts", "./alist-mock": "./src/alist/mock.ts" },
"tasks": { "tasks": {
"test": "deno run -A npm:vitest run", "test": "deno run -A npm:vitest run",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@umt/agent", "name": "@umt/agent",
"version": "0.3.40", "version": "0.3.41",
"exports": { ".": "./src/index.ts" }, "exports": { ".": "./src/index.ts" },
"tasks": { "tasks": {
"test": "deno run -A npm:vitest run", "test": "deno run -A npm:vitest run",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@umt/contracts", "name": "@umt/contracts",
"version": "0.3.40", "version": "0.3.41",
"exports": { ".": "./src/index.ts" }, "exports": { ".": "./src/index.ts" },
"tasks": { "tasks": {
"test": "deno run -A npm:vitest run", "test": "deno run -A npm:vitest run",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@umt/db", "name": "@umt/db",
"version": "0.3.40", "version": "0.3.41",
"exports": { ".": "./src/index.ts" }, "exports": { ".": "./src/index.ts" },
"tasks": { "tasks": {
"test": "deno run -A npm:vitest run", "test": "deno run -A npm:vitest run",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@umt/domain", "name": "@umt/domain",
"version": "0.3.40", "version": "0.3.41",
"exports": { ".": "./src/index.ts" }, "exports": { ".": "./src/index.ts" },
"tasks": { "tasks": {
"test": "deno run -A npm:vitest run", "test": "deno run -A npm:vitest run",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@umt/mapping", "name": "@umt/mapping",
"version": "0.3.40", "version": "0.3.41",
"exports": { ".": "./src/index.ts" }, "exports": { ".": "./src/index.ts" },
"tasks": { "tasks": {
"test": "deno run -A npm:vitest run", "test": "deno run -A npm:vitest run",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@umt/tasks", "name": "@umt/tasks",
"version": "0.3.40", "version": "0.3.41",
"exports": { ".": "./src/index.ts" }, "exports": { ".": "./src/index.ts" },
"tasks": { "tasks": {
"test": "deno run -A npm:vitest run", "test": "deno run -A npm:vitest run",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@umt/testkit", "name": "@umt/testkit",
"version": "0.3.40", "version": "0.3.41",
"exports": { ".": "./src/index.ts" }, "exports": { ".": "./src/index.ts" },
"tasks": { "tasks": {
"test": "deno run -A npm:vitest run", "test": "deno run -A npm:vitest run",