Merge branch 'deai2'

Co-Authored-By: Claude Fable 5.1 <[email protected]>

# Conflicts:
#	apps/server/src/modules/acquisition-organize/services/organize.ts
#	apps/web/app/routes/console/sites.$siteId.tsx
This commit is contained in:
root committed 2026-09-29 21:26:16 +08:00
commit 12bc4664b6
247 files changed
+2658 -2148

No files matched your search

@@ -7,7 +7,7 @@ import { AGENT_LAUNCHER_ATTR } from "~/components/agent/agent-drawer.tsx";
import {
agentShortcutLabel,
isAgentShortcut,
splitContextGoal,
shortGoalTitle,
toggleAgentDrawer,
useAgentDrawer,
} from "~/components/agent/agent-store.ts";
@@ -24,7 +24,7 @@ export function useActiveAgentRuns() {
const latest = active[0];
return {
count: active.length,
summary: latest ? `${RUN_STATUS_LABEL[latest.status]} · ${splitContextGoal(latest.goal).text}` : null,
summary: latest ? `${RUN_STATUS_LABEL[latest.status]} · ${shortGoalTitle(latest.goal)}` : null,
};
}, [runs.data]);
}
@@ -128,7 +128,7 @@ function useHideOnScroll(): boolean {
/**
* 右下角悬浮按钮 + 全局快捷键。挂在 console layout 里一次。
* - Agent 会话页本身不显示(页面就是 Agent);最小化小窗在右下角时让位给小窗。
* - 抽屉打开时变成"收起",停在抽屉左侧(窄屏抽屉占满全宽,用抽屉自带的关闭钮,FAB 隐藏)。
* - 只在没有侧栏的窄屏显示(xl 起侧栏顶部就是入口);抽屉打开时隐藏,用抽屉自带的关闭钮。
*/
export function AgentLauncher() {
useAgentHotkey();
@@ -145,7 +145,7 @@ export function AgentLauncher() {
const onAgentPage = /^\/console\/agent(\/|$)/.test(pathname);
if (!host || (onAgentPage && !open) || (minimized && runId && !open)) return null;
const label = open ? "收起 Agent" : count ? `打开 Agent(${count} 个会话进行中)` : "打开 Agent";
const label = open ? "收起智能体" : count ? `打开智能体(${count} 个会话进行中)` : "打开智能体";
return createPortal(
<button
type="button"
@@ -154,14 +154,15 @@ export function AgentLauncher() {
aria-label={label}
aria-keyshortcuts="Control+J Meta+J"
aria-expanded={open}
title={`${open ? "收起" : "打开"} Agent(${shortcut})${summary && !open ? `\n${summary}` : ""}`}
title={`${open ? "收起" : "打开"}智能体(${shortcut})${summary && !open ? `\n${summary}` : ""}`}
style={{ bottom: `calc(${BASE_BOTTOM + lift}px + env(safe-area-inset-bottom))` }}
className={cn(
"pg-float prism-ring group pointer-events-auto fixed right-4 z-40 flex h-14 min-w-14 cursor-pointer items-center justify-center gap-3 rounded-full text-foreground outline-none select-none",
"transition-[transform,bottom,right] duration-layout ease-spring active:scale-[0.96] focus-visible:ring-[3px] focus-visible:ring-ring/50 motion-reduce:transition-none",
"animate-in fade-in-0 zoom-in-95 duration-base motion-reduce:animate-none",
"md:px-2 md:pr-5",
open && "z-[60] hidden md:right-[calc(36rem+1rem)] md:flex",
// 桌面(xl 起)侧栏已有入口,抽屉打开时用抽屉自己的关闭钮:这两种情况都不显示悬浮按钮
open ? "hidden" : "xl:hidden",
!open && scrolledAway && "translate-x-[calc(100%+2rem)] focus-visible:translate-x-0",
)}
>
@@ -171,7 +172,7 @@ export function AgentLauncher() {
</span>
<span className="hidden min-w-0 flex-col items-start text-left md:flex">
<span className="flex items-center gap-2 text-control leading-5 font-semibold">
{open ? "收起" : "Agent"}
{open ? "收起" : "智能体"}
{count && !open ? <PulseDot /> : null}
{count && !open ? <CountBadge count={count} /> : null}
<kbd className="rounded-tight border border-border/70 px-1 font-sans text-label leading-4 font-medium text-muted-foreground">
@@ -179,7 +180,7 @@ export function AgentLauncher() {
</kbd>
</span>
<span className="max-w-56 truncate text-meta leading-4 text-muted-foreground">
{open ? "Esc 关闭" : (summary ?? "交代目标,边做边汇报")}
{open ? "Esc 关闭" : (summary ?? "")}
</span>
</span>
</button>,
@@ -196,7 +197,7 @@ export function AgentNavEntry({ collapsed = false, onNavigate }: { collapsed?: b
onNavigate?.();
toggleAgentDrawer(pathname);
};
const title = `Agent(${shortcut})${summary ? `\n${summary}` : ""}`;
const title = `智能体(${shortcut})${summary ? `\n${summary}` : ""}`;
if (collapsed) {
return (
@@ -205,7 +206,7 @@ export function AgentNavEntry({ collapsed = false, onNavigate }: { collapsed?: b
{...{ [AGENT_LAUNCHER_ATTR]: "" }}
onClick={activate}
title={title}
aria-label={count ? `打开 Agent(${count} 个会话进行中)` : "打开 Agent"}
aria-label={count ? `打开智能体(${count} 个会话进行中)` : "打开智能体"}
aria-keyshortcuts="Control+J Meta+J"
className="prism-ring relative mb-2 grid h-10 w-10 shrink-0 cursor-pointer place-items-center rounded-xl text-foreground outline-none transition-transform duration-micro active:scale-[0.95] focus-visible:ring-2 focus-visible:ring-ring"
>
@@ -228,12 +229,10 @@ export function AgentNavEntry({ collapsed = false, onNavigate }: { collapsed?: b
</span>
<span className="flex min-w-0 flex-1 flex-col">
<span className="flex items-center gap-2 text-sm font-semibold">
Agent
智能体
{count ? <PulseDot /> : null}
</span>
<span className="truncate text-meta text-muted-foreground">
{count ? `${count} 个会话进行中` : "交代目标,边做边汇报"}
</span>
{count ? <span className="truncate text-meta text-muted-foreground">{count} 个会话进行中</span> : null}
</span>
{count ? (
<CountBadge count={count} />
@@ -250,8 +249,8 @@ export function AgentNavEntry({ collapsed = false, onNavigate }: { collapsed?: b
export function AgentShortcutsHelp() {
const shortcut = agentShortcutLabel();
const rows: [string, string][] = [
[shortcut, "打开 / 收起 Agent,附带当前页面"],
["Esc", "关闭 Agent"],
[shortcut, "打开 / 收起智能体,附带当前页面"],
["Esc", "关闭智能体"],
["Enter", "发送目标;Shift+Enter 换行"],
];
return (
@@ -6,15 +6,15 @@ import { groupRows, readCollapsed, toggleCollapsed, writeCollapsed } from "./dat
type Row = { id: string; status: string; name: string };
const rows: Row[] = [
{ id: "1", status: "待识别", name: "a.mkv" },
{ id: "1", status: "未识别", name: "a.mkv" },
{ id: "2", status: "已登记", name: "b.mkv" },
{ id: "3", status: "待识别", name: "c.mkv" },
{ id: "3", status: "未识别", name: "c.mkv" },
];
describe("DataTable groupBy:分组逻辑", () => {
it("按首次出现顺序分组,组内保持原顺序", () => {
expect(groupRows(rows, (r) => r.status).map((g) => [g.key, g.rows.map((r) => r.id)])).toEqual([
["待识别", ["1", "3"]],
["未识别", ["1", "3"]],
["已登记", ["2"]],
]);
});
@@ -24,7 +24,7 @@ describe("DataTable groupBy:分组逻辑", () => {
(r) => r.status,
(a, b) => a.localeCompare(b, "zh-CN"),
);
expect(g.map((x) => x.key)).toEqual(["待识别", "已登记"].sort((a, b) => a.localeCompare(b, "zh-CN")));
expect(g.map((x) => x.key)).toEqual(["未识别", "已登记"].sort((a, b) => a.localeCompare(b, "zh-CN")));
});
it("折叠切换返回新集合", () => {
const a = new Set<string>();
@@ -72,8 +72,8 @@ describe("DataTable groupBy:渲染", () => {
}),
);
expect(html.match(/aria-expanded="true"/g)).toHaveLength(2);
expect(html).toContain("待识别");
expect(html).toMatch(/待识别<\/span><span[^>]*>2<\/span>/);
expect(html).toContain("未识别");
expect(html).toMatch(/未识别<\/span><span[^>]*>2<\/span>/);
expect(html).toMatch(/已登记<\/span><span[^>]*>1<\/span>/);
for (const n of ["a.mkv", "b.mkv", "c.mkv"]) expect(html).toContain(n);
});
+4 -4
View File
@@ -19,10 +19,10 @@ describe("控制台导航", () => {
it("IA-3:5 组,按定稿清单逐项列出,路径唯一且都在 /console 下", () => {
expect(NAV_GROUPS.map((g) => g.title)).toEqual(["概览", "资源", "订阅", "整理", "系统"]);
expect(NAV_GROUPS.map((g) => g.items.map((i) => i.label))).toEqual([
["首页", "任务", "计划", "Agent 会话"],
["站点", "RSS 订阅"],
["首页", "任务", "计划", "智能体会话"],
["站点", "RSS"],
["订阅", "规则"],
["下载面板", "整理记录", "文件管理", "媒体概览"],
["下载面板", "整理记录", "文件管理", "媒体库"],
["设置", "审计", "运行状态"],
]);
// IA-3 逐项清单 14 项 + FE-6 计划页
@@ -44,7 +44,7 @@ describe("控制台导航", () => {
it("按最长前缀匹配当前项", () => {
expect(findNavItem("/console")?.label).toBe("首页");
expect(findNavItem("/console/")?.label).toBe("首页");
expect(findNavItem("/console/library/abc")?.label).toBe("媒体概览");
expect(findNavItem("/console/library/abc")?.label).toBe("媒体库");
expect(findNavItem("/console/tasks/r1")?.label).toBe("任务");
expect(findNavItem("/console/schedules")?.label).toBe("计划");
expect(findNavItem("/console/downloads")?.label).toBe("下载面板");
+13 -13
View File
@@ -46,25 +46,25 @@ export const NAV_GROUPS: NavGroup[] = [
to: "/console",
label: "首页",
icon: House,
description: "正在进行、需要处理、计划、最近结果与运行状态",
description: "在忙什么、要我处理什么、最近入库了什么",
},
{
to: "/console/tasks",
label: "任务",
icon: ListTodo,
description: "每次运行都是一条任务:定时、事件、手动、Agent 触发都在这里",
description: "最近处理了什么、有没有失败",
},
{
to: "/console/schedules",
label: "计划",
icon: CalendarCog,
description: "定时任务的开关、间隔和下次运行:自动任务与 Agent 巡检",
description: "智能体巡检与自动任务的开关和间隔",
},
{
to: "/console/agent",
label: "Agent 会话",
label: "智能体会话",
icon: Bot,
description: "交代目标,Agent 在授权内执行并汇报",
description: "给智能体一个目标,看它做了什么",
},
],
},
@@ -75,9 +75,9 @@ export const NAV_GROUPS: NavGroup[] = [
to: "/console/sites",
label: "站点",
icon: Globe,
description: "站点、凭证、账号数据与健康;资源、快照与 Feed",
description: "站点账号、资源与 RSS",
},
{ to: "/console/sources", label: "RSS 订阅", icon: Rss, description: "全部 Feed 与抓取状态" },
{ to: "/console/sources", label: "RSS", icon: Rss, description: "全部 RSS 与抓取状态" },
],
},
{
@@ -90,7 +90,7 @@ export const NAV_GROUPS: NavGroup[] = [
{
title: "整理",
items: [
{ to: "/console/downloads", label: "下载面板", icon: Download, description: "下载任务、下载计划与缓存水位" },
{ to: "/console/downloads", label: "下载面板", icon: Download, description: "下载任务、下载计划与下载盘" },
{
to: "/console/organize",
label: "整理记录",
@@ -101,13 +101,13 @@ export const NAV_GROUPS: NavGroup[] = [
to: "/console/files",
label: "文件管理",
icon: FolderTree,
description: "浏览登记目录,改名、移动、删除与整理",
description: "浏览下载目录和媒体库,改名、移动、删除与整理",
},
{
to: "/console/library",
label: "媒体概览",
label: "媒体库",
icon: Library,
description: "作品、季 / 集、版本目标、外部身份与待识别资源",
description: "作品、季与集、未识别资源",
},
],
},
@@ -118,7 +118,7 @@ export const NAV_GROUPS: NavGroup[] = [
to: "/console/settings",
label: "设置",
icon: Settings,
description: "下载器、媒体服务器、库根、站点账号、Agent 上游与自动化",
description: "下载器、媒体服务器、媒体库目录、站点账号、模型服务与自动化",
},
{
to: "/console/audit",
@@ -130,7 +130,7 @@ export const NAV_GROUPS: NavGroup[] = [
to: "/console/status",
label: "运行状态",
icon: Gauge,
description: "进程心跳、事件循环、数据库、磁盘与挂载根",
description: "服务是否正常、磁盘与数据库",
},
],
},
@@ -0,0 +1,53 @@
import { ChevronDown } from "lucide-react";
import { type ReactNode, useId, useState } from "react";
import { CHEVRON_CLASS, Disclosure } from "~/components/agentui/disclosure.tsx";
import { type KvItem, KvList } from "~/components/console/kv-list.tsx";
import { useUiPrefs } from "~/lib/ui-prefs.ts";
import { cn } from "~/lib/utils.ts";
/**
* 技术信息折叠区:ID、hash、trace、token 数、耗时、模型 ID、原始字段、内部状态码都放这里,默认收起
* (设置 → 界面可改为默认展开)。值为空的项不显示;全部为空时整块不渲染。
*/
export function TechInfo({
items,
children,
title = "技术信息",
className,
columns = 2,
}: {
items?: (KvItem | null | false | undefined)[];
children?: ReactNode;
title?: string;
className?: string;
columns?: 1 | 2;
}) {
const prefs = useUiPrefs();
const [override, setOverride] = useState<boolean | null>(null);
const open = override ?? prefs.techInfoExpanded;
const id = useId();
const rows = (items ?? []).filter(
(it): it is KvItem => !!it && it.value !== null && it.value !== undefined && it.value !== "",
);
if (!rows.length && !children) return null;
return (
<div className={cn("min-w-0", className)} data-slot="tech-info">
<button
type="button"
aria-expanded={open}
aria-controls={id}
onClick={() => setOverride(!open)}
className="inline-flex items-center gap-1 rounded-md py-1 text-meta text-muted-foreground transition-colors duration-micro hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none"
>
{title}
<ChevronDown className={cn("size-3.5", CHEVRON_CLASS, open && "rotate-180")} aria-hidden />
</button>
<Disclosure id={id} open={open}>
<div className="grid gap-3 pt-2 pb-1">
{rows.length ? <KvList items={rows.map((r) => ({ mono: true, ...r }))} columns={columns} /> : null}
{children}
</div>
</Disclosure>
</div>
);
}