chore(toolchain): upgrade Bun to 1.4.4 and migrate console to TanStack Router
This commit is contained in:
1 parent
b44ebc2ba1
commit
3b9376c75a
134 files changed
+893
-692
No files matched your search
+1
-1
@@ -1 +1 @@
|
||||
1.4.3
|
||||
1.4.4
|
||||
@@ -1,7 +1,6 @@
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
.react-router/
|
||||
coverage/
|
||||
*.tsbuildinfo
|
||||
.env
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
# 单容器镜像:一个 bun build --compile 出的 umt-server 二进制,同时承担 API(含 web 静态 SPA)与 worker。
|
||||
# PostgreSQL 不在本镜像内,见 docker-compose.yml。
|
||||
# 构建:docker build -t uhd-media-tool . 运行时版本锁定 Bun 1.4.3(ADR-007)
|
||||
# 构建:docker build -t uhd-media-tool . 运行时版本锁定 Bun 1.4.4(ADR-007)
|
||||
# ffprobe 取自静态构建镜像(单个 static-pie 二进制,约 135 MB;Debian 的 ffmpeg 包要拉 200+ 个依赖、约 450 MB)
|
||||
ARG FFPROBE_IMAGE=mwader/static-ffmpeg:9.0.2@sha256:7d9bdaaf887f7e6ce6151f67325c344074b5ff1fb75316011c3376503e449a7b
|
||||
FROM ${FFPROBE_IMAGE} AS ffprobe
|
||||
@@ -8,7 +8,7 @@ FROM ${FFPROBE_IMAGE} AS ffprobe
|
||||
# 与 PG 18 服务端同主版本的备份工具,避免 Debian 默认 17 客户端拒绝导出 PG 18。
|
||||
FROM postgres:18.6-trixie@sha256:5a5a84b19854a9ffaa54082c166ff4ec27473a361e496e5ea167f298f2da9722 AS postgres-tools
|
||||
|
||||
FROM oven/bun:1.4.3 AS build
|
||||
FROM oven/bun:1.4.4 AS build
|
||||
ARG APT_MIRROR=""
|
||||
RUN if [ -n "$APT_MIRROR" ]; then sed -i "s|deb.debian.org|$APT_MIRROR|g" /etc/apt/sources.list.d/debian.sources; fi \
|
||||
&& apt-get update && apt-get install -y --no-install-recommends build-essential libnode-dev && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Hono } from "hono";
|
||||
import { serveStatic } from "hono/bun";
|
||||
|
||||
/**
|
||||
* 托管 React Router SPA 构建产物(build/client):存在的文件直接返回,
|
||||
* 托管 TanStack Router SPA 构建产物(build/client):存在的文件直接返回,
|
||||
* 其余非 /api 的 GET 回退 index.html 交给前端路由。/api 下的未知路径仍走 JSON 404。
|
||||
* 注意 hono/deno 的 serveStatic 要求 path 相对 root,写绝对路径会 404。
|
||||
*/
|
||||
|
||||
@@ -31,15 +31,15 @@ export const copyFile: typeof fsp.copyFile = async (a, b, mode) => {
|
||||
return fsp.copyFile(a, b, mode);
|
||||
};
|
||||
|
||||
export const rm: typeof fsp.rm = async (p, o) => {
|
||||
export const rm: typeof fsp.rm = (async (p: P, o?: import("node:fs").RmOptions) => {
|
||||
assertLocalPaths("rm", s(p));
|
||||
return fsp.rm(p, o);
|
||||
};
|
||||
return (fsp.rm as (path: P, options?: import("node:fs").RmOptions) => Promise<void>)(p, o);
|
||||
}) as typeof fsp.rm;
|
||||
|
||||
export const rmdir: typeof fsp.rmdir = async (p, o) => {
|
||||
export const rmdir: typeof fsp.rmdir = (async (p: P, o?: import("node:fs").RmDirOptions) => {
|
||||
assertLocalPaths("rmdir", s(p));
|
||||
return fsp.rmdir(p, o);
|
||||
};
|
||||
return (fsp.rmdir as (path: P, options?: import("node:fs").RmDirOptions) => Promise<void>)(p, o);
|
||||
}) as typeof fsp.rmdir;
|
||||
|
||||
export const unlink: typeof fsp.unlink = async (p) => {
|
||||
assertLocalPaths("unlink", s(p));
|
||||
|
||||
@@ -2,11 +2,13 @@ import { defineConfig } from "vitest/config";
|
||||
|
||||
// 三组测试,按 groupOrder 依次跑:
|
||||
// unit —— 纯内存、无 PG,文件并行;
|
||||
// pg —— 真实 PG、假引擎(每文件独立 schema),文件并行;
|
||||
// pg —— 真实 PG、假引擎(每文件独立 schema),文件串行;
|
||||
// serial —— 起真实引擎 / worker / 后台循环 / 子进程的集成与 e2e,文件级串行。并发跑时 worker、轮询、迁移
|
||||
// 互相抢 CPU 与连接,会把"入库→状态推进"这类时序拉长到超时,串行让每个文件拿到稳定的时序预算。
|
||||
// 新测试缺省进 pg;起真实 worker / 引擎 / 子进程的加进 SERIAL;确认不碰 PG 的加进 UNIT。
|
||||
// 并行文件数受开发库连接数约束(max_connections 100,每个 harness 连接池 5–10):MAX_WORKERS 别随手调大。
|
||||
// OpenWorkflow 0.11 的 CREATE INDEX CONCURRENTLY 使用 1s lock_timeout,即使不同 schema,
|
||||
// 也会等其它测试的长事务快照;PG 文件串行才能稳定完成迁移。
|
||||
// unit 文件并行数仍受机器资源约束:MAX_WORKERS 别随手调大。
|
||||
const UNIT = [
|
||||
"test/api-registry.test.ts",
|
||||
"test/organize-in-place-verify.test.ts",
|
||||
@@ -56,6 +58,7 @@ export default defineConfig({
|
||||
name: "pg",
|
||||
include: ["test/**/*.test.ts"],
|
||||
exclude: [...UNIT, ...SERIAL],
|
||||
fileParallelism: false,
|
||||
// 迁移类用例在用例体里建 schema + 迁移,并行时排队等同一把锁,缺省 5s 不够
|
||||
testTimeout: 60_000,
|
||||
sequence: { groupOrder: 1 },
|
||||
|
||||
+5
-3
@@ -1,20 +1,22 @@
|
||||
# @umt/web
|
||||
|
||||
React Router 8 SPA 控制台(`ssr:false`),沿用 uhd_web 设计系统(token 见 `app/app.css`,规范见 `docs/design/`)。只通过 `/api/v1` 与 server 通信,请求/响应类型一律来自 `@umt/contracts`,不手写重复 DTO。开发端口 5180,`/api` 代理到 http://localhost:4380。
|
||||
TanStack Router SPA 控制台,沿用 uhd_web 设计系统(token 见 `app/app.css`,规范见 `docs/design/`)。只通过 `/api/v1` 与 server 通信,请求/响应类型一律来自 `@umt/contracts`,不手写重复 DTO。开发端口 5180,`/api` 默认代理到 http://localhost:4390,可用 `VITE_API_TARGET` 覆盖。
|
||||
|
||||
## 命令
|
||||
|
||||
在仓库根目录执行:
|
||||
|
||||
- `bun run dev:web`:开发服务器(5180)。
|
||||
- `cd apps/web && bun run build`:生产构建到 `build/client`,由 server 的 `WEB_ROOT` 同端口托管。
|
||||
- `cd apps/web && bun run build`:TanStack Router SPA 生产构建到 `build/client`,由 server 的 `WEB_ROOT` 同端口托管。
|
||||
- `cd apps/web && bun run test`:单测(`app/**/*.test.ts`)。
|
||||
- `bun run test:web:smoke`:Playwright 全页面冒烟,前提与环境变量见仓库 README。
|
||||
- `bun run check:tokens`:设计 token 守卫。
|
||||
|
||||
## 目录
|
||||
|
||||
- `app/routes/`:页面(`routes.ts` 登记全部路由);`app/routes/console/` 为控制台。
|
||||
- `app/routes/`:页面(`app/router.tsx` 登记全部路由);`app/routes/console/` 为控制台。
|
||||
- `index.html` 与 `app/entry.client.tsx`:Vite 浏览器入口;`app/root.tsx` 提供主题、查询缓存与页面标题。
|
||||
- `app/lib/router-compat.tsx`:现有链接与导航 hook 的兼容入口,查询参数保持字符串和重复值,旧页面的 `clientLoader` 重定向由 `app/router.tsx` 执行。
|
||||
- `app/api/`:按资源分的 react-query hook,路径与 schema 取 `@umt/contracts` 的 `Api` 注册表。
|
||||
- `app/components/`:业务组件与 `ui/` 基础组件。
|
||||
- Agent 页的 `?mock=1` 演示模式只在开发构建或后端 `pi.provider = "fake"` 时生效,生产构建连真实模型时忽略。
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
RotateCcw,
|
||||
} from "lucide-react";
|
||||
import { type ReactNode, useEffect, useId, useRef, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
type AgentRunView,
|
||||
@@ -47,6 +46,7 @@ import { formatUsd } from "~/components/schedules/labels.ts";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { humanizeReason } from "~/lib/reasons.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { formatTokens } from "~/lib/tokens.ts";
|
||||
import { cn, formatDateTime } from "~/lib/utils.ts";
|
||||
import { InlineApproval, OptionsCard } from "./agent-interaction.tsx";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Bot, ChevronDown, Maximize2, SquarePen, X } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Link, useLocation } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { agentMockEnabled, useAgentRun, useCreateAgentRun } from "~/api/agent.ts";
|
||||
import { useDownloadTask } from "~/api/downloads.ts";
|
||||
@@ -11,6 +10,7 @@ import { Button } from "~/components/ui/button.tsx";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover.tsx";
|
||||
import { Sheet, SheetClose, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "~/components/ui/sheet.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { Link, useLocation } from "~/lib/router-compat.tsx";
|
||||
import { DRAFT_DRAWER_GOAL, writeDraft } from "./agent-nav.ts";
|
||||
import { AgentRunPanel, GoalComposer } from "./agent-run-panel.tsx";
|
||||
import { AgentSessionList, drawerListMemory } from "./agent-session-list.tsx";
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
import { type AgentRunStatus, EventType } from "@umt/contracts";
|
||||
import { Bot, ChevronDown, FlaskConical, ListChecks, RefreshCw, RotateCcw, WifiOff } from "lucide-react";
|
||||
import { type ReactNode, useId, useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
type AgentSessionOptions,
|
||||
@@ -28,6 +27,7 @@ import { TechInfo } from "~/components/console/tech-info.tsx";
|
||||
import { formatUsd } from "~/components/schedules/labels.ts";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { useNavigate } from "~/lib/router-compat.tsx";
|
||||
import { useSse, useSseStatus } from "~/lib/sse.ts";
|
||||
import { formatTokens } from "~/lib/tokens.ts";
|
||||
import { cn, formatDateTime } from "~/lib/utils.ts";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Bot, ChevronLeft, ChevronRight, Search } from "lucide-react";
|
||||
import { useLayoutEffect, useRef, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { type AgentRunView, isRunActive, TRIGGER_LABEL, useAgentRuns } from "~/api/agent.ts";
|
||||
import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
||||
import { useListPagination } from "~/components/console/use-list-pagination.ts";
|
||||
@@ -9,6 +8,7 @@ import { Input } from "~/components/ui/input.tsx";
|
||||
import type { PaginationProps } from "~/components/ui/pagination.tsx";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select.tsx";
|
||||
import { relTime } from "~/components/workbench/release-fields.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
import { contextLabel, RunStatusBadge } from "./agent-conversation.tsx";
|
||||
import { type ListMemory, newListMemory, revealScrollTop } from "./agent-nav.ts";
|
||||
|
||||
@@ -2,10 +2,10 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import type { AgentRunEvent } from "@umt/contracts";
|
||||
import { createElement, type ReactNode } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { MemoryRouter } from "react-router";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { AgentRunView, GoalReport } from "~/api/agent.ts";
|
||||
import { qk } from "~/lib/query-keys.ts";
|
||||
import { MemoryRouter } from "~/lib/router-compat.tsx";
|
||||
import {
|
||||
AgentConversation,
|
||||
claimedCounts,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { AgentRunNode, AgentRunStatus } from "@umt/contracts";
|
||||
import { ChevronDown, CornerLeftUp, ExternalLink, GitBranch, LoaderCircle } from "lucide-react";
|
||||
import { createContext, type ReactNode, useContext, useEffect, useId, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { agentMockEnabled, isRunActive, useAgentRun } from "~/api/agent.ts";
|
||||
import { CHEVRON_CLASS, Disclosure } from "~/components/agentui/disclosure.tsx";
|
||||
import { LoadingState } from "~/components/console/empty-state.tsx";
|
||||
import { formatUsd } from "~/components/schedules/labels.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
import { AgentConversation, RunStatusBadge } from "./agent-conversation.tsx";
|
||||
import { shortGoalTitle } from "./agent-store.ts";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// Adapted from agentui (https://github.com/ashish200729/agentui), components/agents/citations.tsx
|
||||
// Copyright (c) 2026 Saurabh Chauhan. MIT License, see ./LICENSE-agentui.md
|
||||
// 改造:去掉 favicon 抓取(控制台链接都是站内),改成可选图标槽;站内链接走 react-router 的 Link(`to`),
|
||||
// 改造:去掉 favicon 抓取(控制台链接都是站内),改成可选图标槽;站内链接走路由兼容层的 Link(`to`),
|
||||
// 外链仍新窗口打开;新增 `meta`(右侧状态文字);入场/折叠去 motion,用 Disclosure 与 tw-animate;字号/圆角换 token。
|
||||
import { BookOpenText, ChevronDown, ExternalLink } from "lucide-react";
|
||||
import { type ReactNode, useId, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
import { CHEVRON_CLASS, Disclosure } from "./disclosure.tsx";
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { BrowserResourceResult, BrowserSessionView } from "@umt/contracts";
|
||||
import { Download, Settings2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { useSubmitRelease } from "~/api/releases.ts";
|
||||
import { ManualDownloadDialog } from "~/components/downloads/manual-download-dialog.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { workbenchResource } from "~/components/workbench/download-dialog.tsx";
|
||||
import { apiData, errorMessage } from "~/lib/api.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
|
||||
/** 控件位于 iframe/截图外;直接解析当前详情页并交给共同手动下载入口。 */
|
||||
export function ResourceDownload({ session }: { session: BrowserSessionView }) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { PasswordChangeRequest, type SessionInfo } from "@umt/contracts";
|
||||
import { KeyRound, LoaderCircle, LogOut, ShieldCheck, UserRound } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { useChangePassword, useLogout } from "~/api/auth.ts";
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
import { Input } from "~/components/ui/input.tsx";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover.tsx";
|
||||
import { errorMessage, LOGIN_PATH } from "~/lib/api.ts";
|
||||
import { useNavigate } from "~/lib/router-compat.tsx";
|
||||
import { cn, formatDateTime } from "~/lib/utils.ts";
|
||||
import { FormField, zodFieldErrors } from "./form-field.tsx";
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Bot, Keyboard } from "lucide-react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useLocation } from "react-router";
|
||||
import { isRunActive, RUN_STATUS_LABEL, useAgentRuns } from "~/api/agent.ts";
|
||||
import { AGENT_LAUNCHER_ATTR } from "~/components/agent/agent-drawer.tsx";
|
||||
import {
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
shortGoalTitle,
|
||||
toggleAgentDrawer,
|
||||
} from "~/components/agent/agent-store.ts";
|
||||
import { useLocation } from "~/lib/router-compat.tsx";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
|
||||
// 智能体抽屉的入口只有一个按钮:桌面是侧栏最上方的「智能体」,没有侧栏的窄屏是顶栏胶囊里的智能体按钮
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Decision } from "@umt/contracts";
|
||||
import { ArrowRight, Bell, CheckCheck } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { useAttention } from "~/api/attention.ts";
|
||||
import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
||||
import { consolePath } from "~/components/console/nav.ts";
|
||||
@@ -10,6 +9,7 @@ import { SEVERITY } from "~/components/library/labels.ts";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover.tsx";
|
||||
import { formatRelative } from "~/lib/labels.ts";
|
||||
import { Link, useNavigate } from "~/lib/router-compat.tsx";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
|
||||
// 顶栏铃铛(2026-10-06 起只放两类):需要你批准的智能体操作(会话里的审批 / 方案选择、计划审批、原站页面等你接手)
|
||||
|
||||
@@ -12,16 +12,18 @@ import {
|
||||
} from "./nav.ts";
|
||||
import { recordPath, taskPath } from "./record-sheet.tsx";
|
||||
|
||||
/** routes.ts 里登记的控制台路由(相对 /console) */
|
||||
/** TanStack 路由表里登记的控制台路由(相对 /console) */
|
||||
const ROUTE_PATTERNS = [
|
||||
"",
|
||||
...[...readFileSync(new URL("../../routes.ts", import.meta.url), "utf8").matchAll(/route\("([^"*]+)",/g)].map(
|
||||
"agent",
|
||||
"agent/$runId",
|
||||
...[...readFileSync(new URL("../../router.tsx", import.meta.url), "utf8").matchAll(/page\("([^"*]+)",/g)].map(
|
||||
(m) => m[1] as string,
|
||||
),
|
||||
];
|
||||
const matchesRoute = (pathname: string) => {
|
||||
const rest = pathname.replace(/^\/console\/?/, "");
|
||||
return ROUTE_PATTERNS.some((p) => new RegExp(`^${p.replace(/:[^/]+/g, "[^/]+")}$`).test(rest));
|
||||
return ROUTE_PATTERNS.some((p) => new RegExp(`^${p.replace(/\$[^/]+/g, "[^/]+")}$`).test(rest));
|
||||
};
|
||||
|
||||
describe("控制台导航", () => {
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { useCallback, useRef } from "react";
|
||||
import { Link, type LinkProps, type Location, useLocation, useNavigate, useSearchParams } from "react-router";
|
||||
import { consolePath } from "~/components/console/nav.ts";
|
||||
import {
|
||||
Link,
|
||||
type LinkProps,
|
||||
type Location,
|
||||
useLocation,
|
||||
useNavigate,
|
||||
useSearchParams,
|
||||
} from "~/lib/router-compat.tsx";
|
||||
|
||||
// 记录抽屉的地址约定(docs/design/console-ia.md §9):从列表打开的记录 = 列表页上的右侧抽屉,
|
||||
// 打开的记录写在 ?open=<id>(其余参数不动),刷新、分享、前进后退都能还原;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback } from "react";
|
||||
import { useSearchParams } from "react-router";
|
||||
import { useSearchParams } from "~/lib/router-compat.tsx";
|
||||
|
||||
/**
|
||||
* 列表页筛选状态写进 URL query(docs/design/console-ia.md §3.6),刷新和分享链接不丢筛选。
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Construction } from "lucide-react";
|
||||
import { useLocation } from "react-router";
|
||||
import { useLocation } from "~/lib/router-compat.tsx";
|
||||
import { EmptyState } from "./empty-state.tsx";
|
||||
import { findNavItem } from "./nav.ts";
|
||||
import { PageHeader } from "./page-header.tsx";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { AttentionSummary, Decision } from "@umt/contracts";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
import { approvalHref, HEALTH_HREF } from "~/components/console/inbox-bell.tsx";
|
||||
import { consolePath } from "~/components/console/nav.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { ORGANIZE_ATTENTION_PATH } from "~/routes/console/redirects.ts";
|
||||
|
||||
// 首页与运行状态共用的「需要关注」汇总计数(2026-10-06 去掉待处理中心后):
|
||||
|
||||
@@ -2,9 +2,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import type { Dashboard, SystemHealth, SystemResources } from "@umt/contracts";
|
||||
import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { MemoryRouter } from "react-router";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { qk } from "~/lib/query-keys.ts";
|
||||
import { MemoryRouter } from "~/lib/router-compat.tsx";
|
||||
import { HomeRuntime } from "./home-runtime.tsx";
|
||||
|
||||
// 首页运行状态简版卡:整体健康、CPU、内存、磁盘,以及下载器 / 站点 / 媒体库目录摘要,点进去是运行状态页。
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { type Dashboard, DownloadBoard, type HealthLevel } from "@umt/contracts";
|
||||
import { ArrowRight, Download, Gauge, type LucideIcon } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { useSitesWithAccounts } from "~/api/dashboard.ts";
|
||||
import { apiParsed } from "~/api/parse.ts";
|
||||
import { useSystemHealth, useSystemResources } from "~/api/system.ts";
|
||||
@@ -11,6 +10,7 @@ import { consolePath } from "~/components/console/nav.ts";
|
||||
import { StatusBadge, type StatusTone } from "~/components/console/status-badge.tsx";
|
||||
import { Sparkline, usageTone } from "~/components/status/resource-section.tsx";
|
||||
import { formatRelative } from "~/lib/labels.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { cn, formatBytes } from "~/lib/utils.ts";
|
||||
|
||||
const LEVEL: Record<HealthLevel, { label: string; tone: StatusTone }> = {
|
||||
|
||||
@@ -2,8 +2,8 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { type DiscoverItem, DiscoverItem as DiscoverItemSchema } from "@umt/contracts";
|
||||
import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { MemoryRouter } from "react-router";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { MemoryRouter } from "~/lib/router-compat.tsx";
|
||||
import { DiscoverCard } from "./discover-card.tsx";
|
||||
import {
|
||||
apiQuery,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { useDiscoverSection } from "~/api/discover.ts";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { ChipRow } from "./chips.tsx";
|
||||
import { DiscoverCard, DiscoverCardSkeleton } from "./discover-card.tsx";
|
||||
import {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { type DiscoverItem, type DiscoverMatchCandidate, tmdbImageUrl } from "@umt/contracts";
|
||||
import { ExternalLink, Search } from "lucide-react";
|
||||
import { type ReactNode, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { useDiscoverMatch } from "~/api/discover.ts";
|
||||
import { consolePath } from "~/components/console/nav.ts";
|
||||
@@ -10,6 +9,7 @@ import { SubscriptionFormDialog } from "~/components/subscriptions/subscription-
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "~/components/ui/dialog.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { Link, useNavigate } from "~/lib/router-compat.tsx";
|
||||
import type { CardAction } from "./discover-card.tsx";
|
||||
import { itemHref } from "./discover-state.ts";
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { cursorPage, Task, type TaskKind, type TaskStatus } from "@umt/contracts";
|
||||
import { RotateCcw, Square } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { apiMutate } from "~/api/parse.ts";
|
||||
import { DetailSection } from "~/components/console/detail-sheet.tsx";
|
||||
@@ -11,6 +10,7 @@ import { StatusBadge } from "~/components/console/status-badge.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { actorText } from "~/lib/labels.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { formatDateTime } from "~/lib/utils.ts";
|
||||
|
||||
// 旧业务任务(task 表)的展示零件:引擎只负责推进,task 表是面向用户的投影;取消只停后续步骤,重试只对失败任务。
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
import type { TimelineEvent, TimelineStage } from "~/api/tasks.ts";
|
||||
import { LoadingState } from "~/components/console/empty-state.tsx";
|
||||
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
||||
import { Timeline } from "~/components/console/timeline.tsx";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
|
||||
// 业务时间线(参考 MovieClaw task-activity):把观察 → 计划 → 下载 → 整理 → Agent 串成一个业务过程,
|
||||
// 阶段计数与首页/侧栏同口径;每条事件都有文字说明,不只靠颜色。
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Dashboard } from "@umt/contracts";
|
||||
import { Brush, HardDrive, SlidersHorizontal } from "lucide-react";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { useCacheCleanup, useCacheVolumes } from "~/api/downloads.ts";
|
||||
import { downloaders } from "~/api/instances.ts";
|
||||
@@ -12,6 +11,7 @@ import { StatusBadge } from "~/components/console/status-badge.tsx";
|
||||
import { TechInfo } from "~/components/console/tech-info.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { Link, useNavigate } from "~/lib/router-compat.tsx";
|
||||
import { cn, formatBytes } from "~/lib/utils.ts";
|
||||
|
||||
type Volume = Dashboard["cache"]["volumes"][number];
|
||||
|
||||
@@ -7,12 +7,12 @@ import type {
|
||||
} from "@umt/contracts";
|
||||
import { ArrowDown, ArrowUp, CheckCircle2, Download } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import { Link } from "react-router";
|
||||
import type { DataColumn } from "~/components/console/data-table.tsx";
|
||||
import { consolePath } from "~/components/console/nav.ts";
|
||||
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
||||
import { SegmentedControl } from "~/components/ui/segmented.tsx";
|
||||
import { relTime } from "~/components/workbench/release-fields.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { cn, formatBytes, formatDateTime } from "~/lib/utils.ts";
|
||||
import { formatEta, formatSpeed, liveStateLabel, organizeState, peersText, progressOf } from "./board-format.ts";
|
||||
import { DownloadStatusBadges, PauseResume, ProgressLine } from "./download-detail.tsx";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { PublicOrganizationRecord } from "@umt/contracts";
|
||||
import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { MemoryRouter } from "react-router";
|
||||
import { expect, it } from "vitest";
|
||||
import { MemoryRouter } from "~/lib/router-compat.tsx";
|
||||
import { DownloadOrganizeIssue } from "./download-detail.tsx";
|
||||
|
||||
const render = (status: PublicOrganizationRecord["status"]) =>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { AssetVersion, DownloadSaveTarget, ManualAcquisitionResult } from "@umt/contracts";
|
||||
import { ArrowDown, ArrowUp, Bot, ChevronDown, Download, Search, Settings2, X } from "lucide-react";
|
||||
import { useEffect, useId, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { useDownloadSavePaths } from "~/api/downloads.ts";
|
||||
import { useEntityDetail } from "~/api/library.ts";
|
||||
@@ -27,6 +26,7 @@ import { Input } from "~/components/ui/input.tsx";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select.tsx";
|
||||
import { absTime, relTime } from "~/components/workbench/release-fields.ts";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { Link, useNavigate } from "~/lib/router-compat.tsx";
|
||||
import { cn, formatBytes } from "~/lib/utils.ts";
|
||||
import {
|
||||
buildManualOptions,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { DownloadBoardPlanItem } from "@umt/contracts";
|
||||
import { Check, X } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { useDecideApproval, useIsAdminSession } from "~/api/agent.ts";
|
||||
import { useCancelPlan, usePlanWhy } from "~/api/downloads.ts";
|
||||
@@ -14,6 +13,7 @@ import { TechInfo } from "~/components/console/tech-info.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { reasonCode } from "~/lib/reasons.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { formatBytes, formatDateTime } from "~/lib/utils.ts";
|
||||
import { PLAN_WAIT, PURPOSE_LABEL, planWaitHint, whyText } from "./plan-format.ts";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Decision, EpisodeMappingPreview } from "@umt/contracts";
|
||||
import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { MemoryRouter } from "react-router";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { MemoryRouter } from "~/lib/router-compat.tsx";
|
||||
import {
|
||||
agentSimulationText,
|
||||
EpisodeDecisionBody,
|
||||
|
||||
@@ -8,7 +8,6 @@ import type {
|
||||
} from "@umt/contracts";
|
||||
import { BellOff, Bot, Check, LoaderCircle, PencilLine } from "lucide-react";
|
||||
import { useId, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { useResolveDecision } from "~/api/decisions.ts";
|
||||
import { useEntityWords, useEpisodeMappingPreview } from "~/api/episode-mapping.ts";
|
||||
@@ -19,6 +18,7 @@ import { Button } from "~/components/ui/button.tsx";
|
||||
import { Switch } from "~/components/ui/switch.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { humanizeReason } from "~/lib/reasons.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { canonicalText, episodeCode, mappingHref, scopeText, sourceText } from "./model.ts";
|
||||
|
||||
// 作品页集数映射里的「集数待确认」(episode_mapping 内部事项):来源季集、原因、建议的那一行作品识别词与它的模拟结果;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { MemoryRouter } from "react-router";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { MemoryRouter } from "~/lib/router-compat.tsx";
|
||||
import { EpisodeMappingPanel } from "./mapping-panel.tsx";
|
||||
|
||||
vi.mock("~/api/episode-mapping.ts", () => ({
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { EpisodeMappingPreview, EpisodeSuggestion } from "@umt/contracts";
|
||||
import { Wand2 } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useLocation, useSearchParams } from "react-router";
|
||||
import { useEntityWords, useEpisodeMappingPreview } from "~/api/episode-mapping.ts";
|
||||
import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
||||
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
||||
import { WordsEditor } from "~/components/identify-words/words-editor.tsx";
|
||||
import { Chip, Panel } from "~/components/library/parts.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { useLocation, useSearchParams } from "~/lib/router-compat.tsx";
|
||||
import { EpisodeIssuesList } from "./issues.tsx";
|
||||
import { groupMethodsText, lineFromSearch, MAPPING_ANCHOR } from "./model.ts";
|
||||
import { PreviewTable } from "./preview-table.tsx";
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ChevronRight, Info } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
import { useEpisodeMappingPreview } from "~/api/episode-mapping.ts";
|
||||
import { useEntityDetail } from "~/api/library.ts";
|
||||
import { consolePath } from "~/components/console/nav.ts";
|
||||
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
||||
import { Panel } from "~/components/library/parts.tsx";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { EpisodeIssueReasons } from "./issues.tsx";
|
||||
import { absoluteGroups, groupMethodsText, mappingHref } from "./model.ts";
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
Wand2,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
type FileEntry,
|
||||
@@ -56,6 +55,7 @@ import {
|
||||
} from "~/components/ui/dialog.tsx";
|
||||
import { Input } from "~/components/ui/input.tsx";
|
||||
import { Switch } from "~/components/ui/switch.tsx";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { cn, formatBytes } from "~/lib/utils.ts";
|
||||
import { DirPicker } from "./dir-picker.tsx";
|
||||
import {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { LoaderCircle, ScanSearch } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { type FileEntry, type FileRoot, filesErrorText, useFileStat, useProbe } from "~/api/files.ts";
|
||||
import { RECORD_STATUS_LABEL, RECORD_STATUS_TONE, useRecordsForEntity } from "~/api/records.ts";
|
||||
@@ -13,6 +12,7 @@ import { SLOT_STATUS } from "~/components/library/labels.ts";
|
||||
import type { WizardEntity } from "~/components/organize-wizard/model.ts";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { downloadLabel } from "~/lib/labels.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { formatBytes, formatDateTime } from "~/lib/utils.ts";
|
||||
import { EntryStatus } from "./file-list.tsx";
|
||||
import { FilePreviewSection } from "./file-preview.tsx";
|
||||
|
||||
@@ -19,11 +19,11 @@ import {
|
||||
Wand2,
|
||||
} from "lucide-react";
|
||||
import { type ReactNode, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import type { FileEntry } from "~/api/files.ts";
|
||||
import { RecordLink } from "~/components/console/record-sheet.tsx";
|
||||
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
||||
import { relTime } from "~/components/workbench/release-fields.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { cn, formatBytes } from "~/lib/utils.ts";
|
||||
import type { MenuAnchor } from "./context-menu.tsx";
|
||||
import {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Clock, File as FileIcon, Folder, LoaderCircle, RotateCcw, Trash, Trash2 } from "lucide-react";
|
||||
import { type ReactNode, useMemo, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { useSession } from "~/api/auth.ts";
|
||||
import {
|
||||
@@ -33,6 +32,7 @@ import {
|
||||
import { Input } from "~/components/ui/input.tsx";
|
||||
import { relTime } from "~/components/workbench/release-fields.ts";
|
||||
import { actorText } from "~/lib/labels.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { formatBytes, formatDateTime } from "~/lib/utils.ts";
|
||||
|
||||
const DAY = 86_400_000;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { EntityWordsSummary } from "@umt/contracts";
|
||||
import { ExternalLink, Plus } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { useEntityWords, useEntityWordsList, useGlobalWords } from "~/api/episode-mapping.ts";
|
||||
import { type DataColumn, DataTable } from "~/components/console/data-table.tsx";
|
||||
import { DetailSheet } from "~/components/console/detail-sheet.tsx";
|
||||
@@ -22,6 +21,7 @@ import {
|
||||
DialogTitle,
|
||||
} from "~/components/ui/dialog.tsx";
|
||||
import { actorText } from "~/lib/labels.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { formatDateTime } from "~/lib/utils.ts";
|
||||
import { WordsEditor } from "./words-editor.tsx";
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
} from "@umt/contracts";
|
||||
import { Check, LoaderCircle, Search } from "lucide-react";
|
||||
import { type FormEvent, useId, useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { useEntitySearch, useLockField, useMergeEntity, useSetEntityIdentity } from "~/api/library.ts";
|
||||
@@ -28,6 +27,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~
|
||||
import { Switch } from "~/components/ui/switch.tsx";
|
||||
import { Textarea } from "~/components/ui/textarea.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { useNavigate } from "~/lib/router-compat.tsx";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
import { ENTITY_FIELD_LABEL, EVIDENCE_LEVEL, IDENTITY_STATUS, MEDIA_KIND } from "./labels.ts";
|
||||
import { Tone } from "./parts.tsx";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { AliasKind, type TitleAlias } from "@umt/contracts";
|
||||
import { ExternalLink, Lock, Plus, Trash2 } from "lucide-react";
|
||||
import { type FormEvent, useEffect, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { useAliases, useCreateAlias, useDeleteAlias } from "~/api/identity.ts";
|
||||
@@ -15,6 +14,7 @@ import { Input } from "~/components/ui/input.tsx";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select.tsx";
|
||||
import { Switch } from "~/components/ui/switch.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { EntityPicker, type PickedEntity } from "./identity-entity-picker.tsx";
|
||||
import { METADATA_SOURCE } from "./labels.ts";
|
||||
import { Chip, Panel } from "./parts.tsx";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { EnrichFieldName, IdentityPoolItem, MetadataEnrichOutput } from "@umt/contracts";
|
||||
import { ExternalLink, LoaderCircle, Sparkles } from "lucide-react";
|
||||
import { useId, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { enrichPosterUrl, useApplyEnrich, useEnrichPool } from "~/api/identity.ts";
|
||||
import { DetailSection } from "~/components/console/detail-sheet.tsx";
|
||||
@@ -10,6 +9,7 @@ import { Button } from "~/components/ui/button.tsx";
|
||||
import { Label } from "~/components/ui/label.tsx";
|
||||
import { Switch } from "~/components/ui/switch.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { ENRICH_FIELD_LABEL, ENRICH_SKIPPED, ENRICH_SOURCE_KIND, EVIDENCE_LEVEL } from "./labels.ts";
|
||||
import { Chip } from "./parts.tsx";
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ExternalIdentity, IdentityRevision } from "@umt/contracts";
|
||||
import { ExternalLink, Lock, LockOpen } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { useIdentityRevisions, useUpdateIdentity } from "~/api/identity.ts";
|
||||
import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx";
|
||||
@@ -11,6 +10,7 @@ import { consolePath } from "~/components/console/nav.ts";
|
||||
import { Timeline } from "~/components/console/timeline.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { formatDateTime } from "~/lib/utils.ts";
|
||||
import { EVIDENCE_LEVEL, IDENTITY_STATUS, VERIFY_STATUS } from "./labels.ts";
|
||||
import { Tone } from "./parts.tsx";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { IdentityPoolItem, PoolCandidate } from "@umt/contracts";
|
||||
import { BellOff, ExternalLink, LoaderCircle, RotateCw } from "lucide-react";
|
||||
import { useId, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { useAssignPool, useDismissPool, useRetryPool } from "~/api/identity.ts";
|
||||
import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx";
|
||||
@@ -14,6 +13,7 @@ import { Button } from "~/components/ui/button.tsx";
|
||||
import { Input } from "~/components/ui/input.tsx";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { formatDateTime } from "~/lib/utils.ts";
|
||||
import { EnrichSection } from "./identity-enrich.tsx";
|
||||
import { EVIDENCE_LEVEL, POOL_REASON, POOL_STATUS } from "./labels.ts";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { LogEntry } from "@umt/contracts";
|
||||
import { Bot, ExternalLink, History } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { useEngineRun } from "~/api/engine.ts";
|
||||
import { type AgentContext, openAgentDrawer } from "~/components/agent/agent-store.ts";
|
||||
import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx";
|
||||
@@ -15,6 +14,7 @@ import { LegacyTaskSheet } from "~/components/tasks/legacy-task.tsx";
|
||||
import { RunActions, TaskDetailBody, TaskStatusLine } from "~/components/tasks/task-detail.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { actorTypeLabel, formatDuration } from "~/lib/labels.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { formatDateTime } from "~/lib/utils.ts";
|
||||
import {
|
||||
actorLabel,
|
||||
|
||||
@@ -2,10 +2,10 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { LOG_STREAM_EVENT, type LogEntry } from "@umt/contracts";
|
||||
import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { MemoryRouter } from "react-router";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { LogStreamClient, type LogStreamStatus, upsertEntries } from "~/api/logs.ts";
|
||||
import { qk } from "~/lib/query-keys.ts";
|
||||
import { MemoryRouter } from "~/lib/router-compat.tsx";
|
||||
import LogsPage from "~/routes/console/logs.tsx";
|
||||
import { agentGoalFor, logBadge, logSubtitle, logsPath, logTitle, objectText, sinceToFrom } from "./labels.ts";
|
||||
import { agentContextFor } from "./log-detail.tsx";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { EventType, type IdentifyCompletedPayload, TmdbMediaDetail } from "@umt/contracts";
|
||||
import { Eye, FileStack, LoaderCircle, RotateCw, Search } from "lucide-react";
|
||||
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
type FileRef,
|
||||
@@ -27,6 +26,7 @@ import { Button } from "~/components/ui/button.tsx";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "~/components/ui/dialog.tsx";
|
||||
import { Input } from "~/components/ui/input.tsx";
|
||||
import { Switch } from "~/components/ui/switch.tsx";
|
||||
import { Link, useNavigate } from "~/lib/router-compat.tsx";
|
||||
import { useSse } from "~/lib/sse.ts";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
import { candidateEntity, EntityPicker } from "./entity-picker.tsx";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import type { EngineRun } from "@umt/contracts";
|
||||
import { FileStack, ListOrdered, Loader2, RefreshCw } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
import { useDownloadTask } from "~/api/downloads.ts";
|
||||
import { useEngineRun, useEngineRuns, useEngineRunsPage } from "~/api/engine.ts";
|
||||
import { useScrapeRecords } from "~/api/files.ts";
|
||||
@@ -16,6 +15,7 @@ import { Button } from "~/components/ui/button.tsx";
|
||||
import { Pagination } from "~/components/ui/pagination.tsx";
|
||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "~/components/ui/sheet.tsx";
|
||||
import { qk } from "~/lib/query-keys.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { useSse } from "~/lib/sse.ts";
|
||||
import { organizeQueueStage, organizeRunInput, scrapeQueueStage } from "./queue-model.ts";
|
||||
import { queueCountLabel } from "./record-filter-model.ts";
|
||||
|
||||
@@ -2,8 +2,8 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import type { PublicOrganizationRecord, RecordAttention } from "@umt/contracts";
|
||||
import { createElement, type ReactNode } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { MemoryRouter } from "react-router";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { MemoryRouter } from "~/lib/router-compat.tsx";
|
||||
import { OrganizeRecordSheet } from "./organize-record-sheet.tsx";
|
||||
|
||||
const state = vi.hoisted(() => ({ record: null as PublicOrganizationRecord | null }));
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { PublicOrganizationFile, PublicPathRef } from "@umt/contracts";
|
||||
import { ArrowRight, ChevronRight, Trash2, Wand2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { ACTION_LABEL, RECORD_STATUS_LABEL, RECORD_STATUS_TONE, TRIGGER_LABEL, useRecord } from "~/api/records.ts";
|
||||
import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx";
|
||||
import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
||||
@@ -19,6 +18,7 @@ import { OrganizeWizard } from "~/components/organize-wizard/organize-wizard.tsx
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { actorTypeLabel } from "~/lib/labels.ts";
|
||||
import { reasonCode, reasonLine } from "~/lib/reasons.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { cn, formatBytes, formatDateTime } from "~/lib/utils.ts";
|
||||
|
||||
const FILE_STATUS: Record<PublicOrganizationFile["status"], { label: string; tone: StatusTone }> = {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { FilesScrapeRecordsResult, type ScrapeFileResult } from "@umt/contracts";
|
||||
import { Link } from "react-router";
|
||||
import { apiParsed } from "~/api/parse.ts";
|
||||
import { DetailSection } from "~/components/console/detail-sheet.tsx";
|
||||
import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
||||
@@ -9,6 +8,7 @@ import { taskPath } from "~/components/console/record-sheet.tsx";
|
||||
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
||||
import { EXPECTED_SCRAPE_SKIPS, SCRAPE_STATUS, scrapeCounts } from "~/components/library/scrape-panel.tsx";
|
||||
import { reasonLine } from "~/lib/reasons.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { formatDateTime } from "~/lib/utils.ts";
|
||||
|
||||
/** 失败的文件,以及缺元数据、认不出集号等不在预期内的跳过 */
|
||||
|
||||
@@ -2,9 +2,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import type { PublicOrganizationRecord, RecordAttention } from "@umt/contracts";
|
||||
import { createElement, type ReactNode } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { MemoryRouter } from "react-router";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { recordNeedsRefresh } from "~/api/records.ts";
|
||||
import { MemoryRouter } from "~/lib/router-compat.tsx";
|
||||
import { attentionLine, primaryAction, RecordAttentionActions } from "./record-attention.tsx";
|
||||
|
||||
// 整理记录「需要处理」的行内处理(2026-10-06 去掉待处理中心):原因一句话 + 推荐动作,一键执行。
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { PublicOrganizationRecord } from "@umt/contracts";
|
||||
import { BellOff, Bot, ChevronRight, LoaderCircle, Wand2 } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { useAgentIdentifyRecord, useDismissRecordAttention } from "~/api/records.ts";
|
||||
import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx";
|
||||
@@ -15,6 +14,7 @@ import { mappingHref } from "~/components/episode-mapping/model.ts";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { humanizeReason, reasonCode } from "~/lib/reasons.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { canReorganize } from "./record-parts.ts";
|
||||
|
||||
// 整理记录「需要处理」的行内处理(2026-10-06 去掉待处理中心后,整理侧的问题在这里就地处理):
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { RuleGroup } from "@umt/contracts";
|
||||
import { ListChecks, ShieldOff, Tags, Trash2 } from "lucide-react";
|
||||
import { useSearchParams } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { type RuleRefs, useDeleteRuleGroup, useUpdateRuleGroup } from "~/api/rules.ts";
|
||||
import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx";
|
||||
@@ -13,6 +12,7 @@ import { Button } from "~/components/ui/button.tsx";
|
||||
import { Pagination } from "~/components/ui/pagination.tsx";
|
||||
import { Switch } from "~/components/ui/switch.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { useSearchParams } from "~/lib/router-compat.tsx";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
import { DEFAULT_CHAIN_LABEL, RULE_KIND, RULE_SOURCE, ruleSummary, SUPPRESS_NOTE, scopeText } from "./labels.ts";
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { type AgentCheckConfig, CAPABILITY_CATALOG, type KnownCapabilityId, type ScheduledJob } from "@umt/contracts";
|
||||
import { ChevronRight, History, LoaderCircle, Pencil, Play, RotateCcw, SkipForward, Trash2 } from "lucide-react";
|
||||
import { type ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { useDeleteSchedule } from "~/api/schedules.ts";
|
||||
import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx";
|
||||
@@ -12,6 +11,7 @@ import { Button } from "~/components/ui/button.tsx";
|
||||
import { Switch } from "~/components/ui/switch.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { agentRunLabel, formatRelative } from "~/lib/labels.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { formatTokens } from "~/lib/tokens.ts";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
import { NextRun, useJobActions } from "./auto-task-list.tsx";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { ScheduledJob, ScheduleGroup, ScheduleSpecInput } from "@umt/contracts";
|
||||
import { ChevronRight, History, LoaderCircle, Play, RotateCcw, SkipForward } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { useRunScheduleNow, useSkipSchedule, useUpdateSchedule } from "~/api/schedules.ts";
|
||||
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
||||
@@ -11,6 +10,7 @@ import { Pagination } from "~/components/ui/pagination.tsx";
|
||||
import { Switch } from "~/components/ui/switch.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { formatRelative } from "~/lib/labels.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { readCollapseState, writeCollapseState } from "~/lib/ui-prefs.ts";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
import { IntervalSelect } from "./interval-select.tsx";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { LoaderCircle, Sparkles } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { useCreateAgentRun } from "~/api/agent.ts";
|
||||
import { FormField } from "~/components/console/form-field.tsx";
|
||||
@@ -16,6 +15,7 @@ import {
|
||||
} from "~/components/ui/dialog.tsx";
|
||||
import { Textarea } from "~/components/ui/textarea.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { useNavigate } from "~/lib/router-compat.tsx";
|
||||
|
||||
// 用对话起草巡检:用户一句话描述需求 → 发起智能体对话(起草巡检 Skill)→ 智能体建成未启用的巡检,
|
||||
// 对话里给「检查并启用」卡片,用户在计划页检查、修改后启用。
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ScheduledJob } from "@umt/contracts";
|
||||
import { History } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
import { useScheduleRuns } from "~/api/schedules.ts";
|
||||
import { DetailSheet } from "~/components/console/detail-sheet.tsx";
|
||||
import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
||||
@@ -9,6 +8,7 @@ import { StatusBadge } from "~/components/console/status-badge.tsx";
|
||||
import { useListPagination } from "~/components/console/use-list-pagination.ts";
|
||||
import { Pagination } from "~/components/ui/pagination.tsx";
|
||||
import { agentRunLabel } from "~/lib/labels.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { describeSchedule, formatClock, formatMs, RUN_STATUS, TRIGGER_LABEL } from "./labels.ts";
|
||||
|
||||
/** 历史抽屉:最近 50 次运行(新→旧) */
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
import { SCHEDULE_CHANGED_EVENT, type ScheduledJob } from "@umt/contracts";
|
||||
import { AlertTriangle, Bot, CalendarClock, Plus, Sparkles } from "lucide-react";
|
||||
import { type ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { useLocation } from "react-router";
|
||||
import { usePiUpstreamModels, usePiUpstreams } from "~/api/pi.ts";
|
||||
import { useScheduleBoard } from "~/api/schedules.ts";
|
||||
import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
||||
@@ -10,6 +9,7 @@ import { useListPagination } from "~/components/console/use-list-pagination.ts";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { Pagination } from "~/components/ui/pagination.tsx";
|
||||
import { qk } from "~/lib/query-keys.ts";
|
||||
import { useLocation } from "~/lib/router-compat.tsx";
|
||||
import { useSse } from "~/lib/sse.ts";
|
||||
import { AgentCheckCard, type DefaultUpstream } from "./agent-check-card.tsx";
|
||||
import { AgentCheckDialog } from "./agent-check-dialog.tsx";
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { type SearchAction, type SearchWork, tmdbImageUrl } from "@umt/contracts";
|
||||
import { CornerDownLeft, Film, Globe, LoaderCircle, type LucideIcon, Search } from "lucide-react";
|
||||
import { type ReactNode, useEffect, useId, useMemo, useRef, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { useSearch } from "~/api/search.ts";
|
||||
import { settingsPath } from "~/components/console/nav.ts";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogTitle } from "~/components/ui/dialog.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { Link, useNavigate } from "~/lib/router-compat.tsx";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
import { SearchHistory, useSearchHistory } from "./search-history.tsx";
|
||||
import { actionIcon, mediaKindLabel } from "./search-labels.ts";
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
UsersRound,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { type ReleaseSearchFilters, useRefreshReleaseSearch, useReleaseSearch } from "~/api/search.ts";
|
||||
import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
||||
@@ -27,6 +26,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~
|
||||
import { ReleaseDetailSheet } from "~/components/workbench/release-detail-sheet.tsx";
|
||||
import { absTime, relTime, remainingLabel } from "~/components/workbench/release-fields.ts";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { cn, formatBytes } from "~/lib/utils.ts";
|
||||
import { QuickFilterBar } from "./quick-filter-bar.tsx";
|
||||
import {
|
||||
|
||||
@@ -4,8 +4,8 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import type { MediaServerInventory, SearchRelease, WorkInventory } from "@umt/contracts";
|
||||
import { createElement } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { MemoryRouter } from "react-router";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { MemoryRouter } from "~/lib/router-compat.tsx";
|
||||
import { parseWorkRef, type ReleaseFilterState, toApiFilters, UnmatchedReleases } from "./release-results.tsx";
|
||||
import { resolvePick } from "./site-picker-dialog.tsx";
|
||||
import { inventoryBadge, mediaServerSummary, releaseSearchPath, slotLine, workHref, workRef } from "./work-card.tsx";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { SearchWork } from "@umt/contracts";
|
||||
import { ArrowRight, Film, PackageSearch } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { mediaServers } from "~/api/instances.ts";
|
||||
import { useSearch } from "~/api/search.ts";
|
||||
import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
||||
@@ -9,6 +8,7 @@ import { settingsPath } from "~/components/console/nav.ts";
|
||||
import { useListPagination } from "~/components/console/use-list-pagination.ts";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { Pagination } from "~/components/ui/pagination.tsx";
|
||||
import { Link, useNavigate } from "~/lib/router-compat.tsx";
|
||||
import { actionIcon } from "./search-labels.ts";
|
||||
import { SitePickerDialog } from "./site-picker-dialog.tsx";
|
||||
import { releaseSearchPath, WorkCard, workRef } from "./work-card.tsx";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { SearchSiteMode, Site } from "@umt/contracts";
|
||||
import { PackageSearch } from "lucide-react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { useSiteOptions } from "~/api/instances.ts";
|
||||
import { useSettings } from "~/api/settings.ts";
|
||||
import { consolePath } from "~/components/console/nav.ts";
|
||||
@@ -15,6 +14,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "~/components/ui/dialog.tsx";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
|
||||
// 搜资源前选站点:默认(站点页勾了「默认参与搜索」的)/ 全部启用站点 / 指定站点。
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { MediaServerInventory, SearchWork, WorkInventory } from "@umt/contracts";
|
||||
import { ArrowRight, PackageSearch } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
import { type MediaServerRef, useMediaServerInventory } from "~/api/search.ts";
|
||||
import { consolePath } from "~/components/console/nav.ts";
|
||||
import { StatusBadge, type StatusTone } from "~/components/console/status-badge.tsx";
|
||||
import { CoverPlaceholder } from "~/components/library/parts.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { mediaKindLabel } from "./search-labels.ts";
|
||||
|
||||
// 作品卡(SEARCH-2 第一阶段):TMDB 海报 / 年份 / 类型 / 简介摘要 + 库内状态;「详情」进作品详情页,「搜索资源」先选站点。
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { useEnrichStats } from "~/api/identity.ts";
|
||||
import { useSettings } from "~/api/settings.ts";
|
||||
import { useSites } from "~/api/sites.ts";
|
||||
@@ -9,6 +8,7 @@ import { consolePath } from "~/components/console/nav.ts";
|
||||
import { ENRICH_SOURCE_KIND } from "~/components/library/labels.ts";
|
||||
import { SearchDefaultDialog } from "~/components/sites/search-default-dialog.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { num } from "./resource-section.tsx";
|
||||
import { SettingsBlock } from "./settings-block.tsx";
|
||||
import { type CardSpec, n, SettingsCard, SettingsGate } from "./settings-card.tsx";
|
||||
|
||||
@@ -2,9 +2,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { type CacheRegionList, DEFAULT_SETTINGS } from "@umt/contracts";
|
||||
import { createElement } from "react";
|
||||
import { renderToString } from "react-dom/server";
|
||||
import { MemoryRouter } from "react-router";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { qk } from "~/lib/query-keys.ts";
|
||||
import { MemoryRouter } from "~/lib/router-compat.tsx";
|
||||
import { CacheSection, hitDetailText, hitRateText } from "./cache.tsx";
|
||||
|
||||
// 设置 → 缓存:缓存区表显示统一元数据缓存的命中率(最近 7 天、三个进程合计)。
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
} from "@umt/contracts";
|
||||
import { selectAcquisitionPolicy } from "@umt/domain";
|
||||
import { cloneElement, type ReactElement, useId, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { filesErrorText, useFileRoots } from "~/api/files.ts";
|
||||
import { downloaders } from "~/api/instances.ts";
|
||||
@@ -30,6 +29,7 @@ import { Input } from "~/components/ui/input.tsx";
|
||||
import { Pagination } from "~/components/ui/pagination.tsx";
|
||||
import { Switch } from "~/components/ui/switch.tsx";
|
||||
import { Textarea } from "~/components/ui/textarea.tsx";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { formatDateTime } from "~/lib/utils.ts";
|
||||
import { SaveBar, SettingsBlock } from "./settings-block.tsx";
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { DEFAULT_SETTINGS, DEPRECATED_SETTINGS_FIELDS, type GlobalSettings, SETTINGS_SECTIONS } from "@umt/contracts";
|
||||
import { createElement } from "react";
|
||||
import { renderToString } from "react-dom/server";
|
||||
import { MemoryRouter } from "react-router";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { qk } from "~/lib/query-keys.ts";
|
||||
import { MemoryRouter } from "~/lib/router-compat.tsx";
|
||||
import { cacheRow } from "./cache.tsx";
|
||||
import { SETTINGS_CARDS, searchSettings } from "./search-index.ts";
|
||||
import { parseSettingsHash, SECTION_GROUPS, SECTIONS, settingsHash } from "./sections.tsx";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Search, X } from "lucide-react";
|
||||
import { useId, useMemo, useRef, useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { Input } from "~/components/ui/input.tsx";
|
||||
import { useNavigate } from "~/lib/router-compat.tsx";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
import { type SettingsSearchHit, searchSettings } from "./search-index.ts";
|
||||
import { settingsHash } from "./sections.tsx";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
import { consolePath } from "~/components/console/nav.ts";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { BrowserProvidersBlock } from "./browser-providers.tsx";
|
||||
import { num, str } from "./resource-section.tsx";
|
||||
import { type CardSpec, n, SettingsCard, SettingsGate } from "./settings-card.tsx";
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { type ReactNode, useState } from "react";
|
||||
import { useSearchParams } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { useFeeds } from "~/api/feeds.ts";
|
||||
import { snapshotStale, useSnapshotSite, useToggleSite, useUpdateSite } from "~/api/sites.ts";
|
||||
@@ -24,6 +23,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover
|
||||
import { Switch } from "~/components/ui/switch.tsx";
|
||||
import { circuitLabel, relTime } from "~/components/workbench/release-fields.ts";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { useSearchParams } from "~/lib/router-compat.tsx";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
import type { SiteDialog, useSiteChecks } from "./site-dialogs.tsx";
|
||||
import {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { SiteAccount, SiteHealthResult, SiteListItem, SiteLoginResult } from "@umt/contracts";
|
||||
import { CircleCheck, CircleX, KeyRound, LoaderCircle, TriangleAlert } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { downloaders } from "~/api/instances.ts";
|
||||
import {
|
||||
@@ -29,6 +28,7 @@ import { Input } from "~/components/ui/input.tsx";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select.tsx";
|
||||
import { Switch } from "~/components/ui/switch.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
import {
|
||||
CredentialFields,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { AgentSpendBucket, AgentSpendKind } from "@umt/contracts";
|
||||
import { BellRing, Bot } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
import { useAgentSpend, useAttention } from "~/api/attention.ts";
|
||||
import { ErrorState, LoadingState } from "~/components/console/empty-state.tsx";
|
||||
import { settingsPath } from "~/components/console/nav.ts";
|
||||
import { AttentionList, attentionRows } from "~/components/dashboard/attention-summary.tsx";
|
||||
import { formatRelative } from "~/lib/labels.ts";
|
||||
import { humanizeReason } from "~/lib/reasons.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
|
||||
// 运行状态页(2026-10-06):「需要关注」汇总(整理需要处理、等你批准、系统健康,系统健康逐条列出、恢复后自动消失)
|
||||
// 与智能体用量(今天 / 最近 7 天按会话类型;不设上限,只展示)。
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { CoverageCell, SubscriptionCoverage } from "@umt/contracts";
|
||||
import { Clock, Download, ExternalLink, Wand2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { useFillSlot } from "~/api/subscriptions.ts";
|
||||
import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx";
|
||||
@@ -9,6 +8,7 @@ import { KvList } from "~/components/console/kv-list.tsx";
|
||||
import { consolePath } from "~/components/console/nav.ts";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
import {
|
||||
CELL_LABEL,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Pause, Pencil, Play, RefreshCw, RotateCcw, Search } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
useRefreshCoverage,
|
||||
@@ -40,6 +39,7 @@ import { SubscriptionFormDialog } from "~/components/subscriptions/subscription-
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { actorText } from "~/lib/labels.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { cn, formatDateTime } from "~/lib/utils.ts";
|
||||
|
||||
// 订阅对比视图(FE-5,docs/design/subscriptions.md §2):季页签 → 集 × 收录版本矩阵;收录版本排序与自动补全;订阅信息。
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { AssetOperation, DownloadTask, Task } from "@umt/contracts";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { z } from "zod";
|
||||
import { apiParsed, isMissingEndpoint } from "~/api/parse.ts";
|
||||
import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx";
|
||||
@@ -11,6 +10,7 @@ import type { StatusTone } from "~/components/console/status-badge.tsx";
|
||||
import { Timeline } from "~/components/console/timeline.tsx";
|
||||
import { TASK_KIND_LABEL, TaskActions, TaskStatusLine, TaskSummary } from "~/components/downloads/business-tasks.tsx";
|
||||
import { TTL_SECOND } from "~/lib/api.ts";
|
||||
import { useNavigate } from "~/lib/router-compat.tsx";
|
||||
|
||||
// 日志页任务抽屉打开的 id 不是引擎运行时(旧链接 /console/tasks/:id):先当旧业务任务查,
|
||||
// 有引擎运行就换成那条;没有就在抽屉里显示旧任务的概况与文件操作;再查不到当下载任务 id,换到下载面板的抽屉。
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { EngineRunDetail, EngineStepAttempt } from "@umt/contracts";
|
||||
import { Ban, ChevronDown, ExternalLink, Repeat, ScrollText } from "lucide-react";
|
||||
import { type ReactNode, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { ENGINE_TERMINAL, useCancelEngineRun, useEngineRun, useRetryEngineRun } from "~/api/engine.ts";
|
||||
import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx";
|
||||
@@ -15,6 +14,7 @@ import { BUSINESS_OBJECT_TYPE, objectLogsPath } from "~/components/logs/labels.t
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { formatDuration, formatRelative, spanSec } from "~/lib/labels.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { cn, formatDateTime } from "~/lib/utils.ts";
|
||||
import {
|
||||
BUSINESS_KIND_LABEL,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { InventoryEntry, WorkbenchRelease } from "@umt/contracts";
|
||||
import { ArrowDown, ArrowUp, Bot, Check, RefreshCw } from "lucide-react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { useEntityDetail } from "~/api/inventory.ts";
|
||||
import { useRefreshReleaseDetail, useReleaseDetail } from "~/api/releases.ts";
|
||||
@@ -13,6 +12,7 @@ import { KvList } from "~/components/console/kv-list.tsx";
|
||||
import { StatusBadge } from "~/components/console/status-badge.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { useUiPrefs } from "~/lib/ui-prefs.ts";
|
||||
import { formatBytes, formatDateTime } from "~/lib/utils.ts";
|
||||
import { DownloadActions } from "./download-dialog.tsx";
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { RouterProvider } from "@tanstack/react-router";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { initInputModality } from "~/lib/input-modality.ts";
|
||||
import { router } from "./router.tsx";
|
||||
|
||||
initInputModality();
|
||||
createRoot(document.getElementById("root")!).render(<RouterProvider router={router} />);
|
||||
@@ -0,0 +1,56 @@
|
||||
import { createMemoryHistory, createRootRoute, createRoute, createRouter } from "@tanstack/react-router";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { navigateTo, parseUrlSearch, resolveTo, stringifyUrlSearch } from "./router-compat.tsx";
|
||||
|
||||
const current = { pathname: "/console/files", search: "?view=trash", hash: "#old" };
|
||||
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
describe("路由目标兼容", () => {
|
||||
it("对象目标不继承未指定的查询和锚点", () => {
|
||||
expect(resolveTo({ pathname: "/console/settings", hash: "#account/totp" }, current)).toBe(
|
||||
"/console/settings#account/totp",
|
||||
);
|
||||
expect(resolveTo({ search: "?view=all" }, current)).toBe("/console/files?view=all");
|
||||
});
|
||||
|
||||
it("仅有锚点的字符串目标保留当前查询", () => {
|
||||
expect(resolveTo("#details", current)).toBe("/console/files?view=trash#details");
|
||||
});
|
||||
|
||||
it("重复定位同一设置字段生成新的导航 key,后退恢复原来的业务 state", async () => {
|
||||
vi.stubGlobal("window", { origin: "http://localhost" });
|
||||
vi.stubGlobal("crypto", undefined);
|
||||
const root = createRootRoute();
|
||||
const settings = createRoute({ getParentRoute: () => root, path: "/console/settings" });
|
||||
const history = createMemoryHistory({ initialEntries: ["/console/settings"] });
|
||||
const router = createRouter({
|
||||
routeTree: root.addChildren([settings]),
|
||||
history,
|
||||
isServer: false,
|
||||
parseSearch: parseUrlSearch,
|
||||
stringifySearch: stringifyUrlSearch,
|
||||
});
|
||||
const unsubscribe = history.subscribe(() => void router.load());
|
||||
try {
|
||||
await router.load();
|
||||
const target = new URL("/console/settings?tag=one&tag=two#account/totp", window.origin);
|
||||
const state = { field: "account.username", sheetFrom: "/console/settings" };
|
||||
await navigateTo(router, target, { state, preventScrollReset: true });
|
||||
const firstKey = router.state.location.state.__TSR_key;
|
||||
const firstIndex = history.location.state.__TSR_index;
|
||||
await navigateTo(router, target, { state, preventScrollReset: true });
|
||||
expect(router.state.location.state.__TSR_key).not.toBe(firstKey);
|
||||
expect(history.location.state.__TSR_index).toBe(firstIndex + 1);
|
||||
expect(router.state.location.state).toMatchObject(state);
|
||||
expect(router.state.location.search).toEqual({ tag: ["one", "two"] });
|
||||
expect(router.state.location.hash).toBe("account/totp");
|
||||
history.back();
|
||||
await router.load();
|
||||
expect(router.state.location.state.__TSR_key).toBe(firstKey);
|
||||
expect(router.state.location.state).toMatchObject(state);
|
||||
} finally {
|
||||
unsubscribe();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,185 @@
|
||||
import {
|
||||
type AnyRouter,
|
||||
Outlet,
|
||||
useRouter,
|
||||
useRouterState,
|
||||
useParams as useTanstackParams,
|
||||
} from "@tanstack/react-router";
|
||||
import type { AnchorHTMLAttributes, MouseEvent, ReactNode } from "react";
|
||||
import { createContext, forwardRef, useCallback, useContext } from "react";
|
||||
|
||||
// Keep URL search values as strings while the console's existing links move to TanStack Router.
|
||||
export function parseUrlSearch(value: string): Record<string, string | string[]> {
|
||||
const result: Record<string, string | string[]> = Object.create(null);
|
||||
for (const [key, part] of new URLSearchParams(value)) {
|
||||
const previous = result[key];
|
||||
result[key] = previous === undefined ? part : [...(Array.isArray(previous) ? previous : [previous]), part];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function stringifyUrlSearch(search: Record<string, unknown>): string {
|
||||
const params = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(search)) {
|
||||
if (value == null) continue;
|
||||
for (const part of Array.isArray(value) ? value : [value]) params.append(key, String(part));
|
||||
}
|
||||
const value = params.toString();
|
||||
return value ? `?${value}` : "";
|
||||
}
|
||||
|
||||
export type Location = {
|
||||
pathname: string;
|
||||
search: string;
|
||||
hash: string;
|
||||
state: unknown;
|
||||
key: string;
|
||||
};
|
||||
const TestLocation = createContext<Location | null>(null);
|
||||
let navigationSequence = 0;
|
||||
|
||||
declare module "@tanstack/react-router" {
|
||||
interface HistoryState {
|
||||
__umtNavigationKey?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export function useLocation(): Location {
|
||||
const testLocation = useContext(TestLocation);
|
||||
// The test provider is fixed for a render tree; production always uses RouterProvider.
|
||||
if (testLocation) return testLocation;
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: provider branch is stable for the component lifetime
|
||||
const location = useRouterState({ select: (state) => state.location });
|
||||
return {
|
||||
pathname: location.pathname,
|
||||
search: location.searchStr,
|
||||
hash: location.hash ? (location.hash.startsWith("#") ? location.hash : `#${location.hash}`) : "",
|
||||
state: location.state,
|
||||
key: location.state.__TSR_key ?? location.href,
|
||||
};
|
||||
}
|
||||
|
||||
type To = string | { pathname?: string; search?: string; hash?: string };
|
||||
type NavigationOptions = { replace?: boolean; state?: unknown; preventScrollReset?: boolean };
|
||||
|
||||
export function navigateTo(router: AnyRouter, target: URL, options: NavigationOptions = {}) {
|
||||
return router.navigate({
|
||||
to: target.pathname,
|
||||
search: parseUrlSearch(target.search),
|
||||
hash: target.hash.slice(1),
|
||||
replace: options.replace,
|
||||
resetScroll: !options.preventScrollReset,
|
||||
// 显式导航即使地址和业务 state 相同,也要更新 key,让设置搜索能再次定位同一字段。
|
||||
state: {
|
||||
...((options.state ?? {}) as Record<string, unknown>),
|
||||
__umtNavigationKey: `${Date.now()}:${++navigationSequence}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveTo(to: To, current: Pick<Location, "pathname" | "search" | "hash">): string {
|
||||
if (typeof to === "string") {
|
||||
if (to.startsWith("?") || to.startsWith("#"))
|
||||
return `${current.pathname}${to.startsWith("#") ? current.search : ""}${to}`;
|
||||
return to;
|
||||
}
|
||||
return `${to.pathname ?? current.pathname}${to.search ?? ""}${to.hash ?? ""}`;
|
||||
}
|
||||
|
||||
export function useNavigate() {
|
||||
const router = useRouter({ warn: false });
|
||||
const { pathname, search, hash } = useLocation();
|
||||
return useCallback(
|
||||
(to: To | number, options: NavigationOptions = {}) => {
|
||||
if (typeof to === "number") {
|
||||
router?.history.go(to);
|
||||
return;
|
||||
}
|
||||
const target = new URL(resolveTo(to, { pathname, search, hash }), window.location.origin);
|
||||
if (target.origin !== window.location.origin) {
|
||||
window.location.assign(target.href);
|
||||
return;
|
||||
}
|
||||
if (!router) return;
|
||||
void navigateTo(router, target, options);
|
||||
},
|
||||
[router, pathname, search, hash],
|
||||
);
|
||||
}
|
||||
|
||||
type SearchInput = URLSearchParams | Record<string, string>;
|
||||
export function useSearchParams(): [
|
||||
URLSearchParams,
|
||||
(next: SearchInput | ((current: URLSearchParams) => SearchInput), options?: NavigationOptions) => void,
|
||||
] {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const set = useCallback(
|
||||
(next: SearchInput | ((current: URLSearchParams) => SearchInput), options?: NavigationOptions) => {
|
||||
const value = typeof next === "function" ? next(new URLSearchParams(location.search)) : next;
|
||||
const params = value instanceof URLSearchParams ? value : new URLSearchParams(value);
|
||||
navigate({ search: params.toString() ? `?${params}` : "" }, options);
|
||||
},
|
||||
[location.search, navigate],
|
||||
);
|
||||
return [new URLSearchParams(location.search), set];
|
||||
}
|
||||
|
||||
export function useParams<T extends Record<string, string | undefined> = Record<string, string | undefined>>(): T {
|
||||
const testLocation = useContext(TestLocation);
|
||||
if (testLocation) return {} as T;
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: provider branch is stable for the component lifetime
|
||||
return useTanstackParams({ strict: false } as never) as T;
|
||||
}
|
||||
|
||||
export type LinkProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> & {
|
||||
to: To;
|
||||
replace?: boolean;
|
||||
state?: unknown;
|
||||
preventScrollReset?: boolean;
|
||||
};
|
||||
|
||||
export const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
|
||||
{ to, replace, state, preventScrollReset, onClick, target, children, ...props },
|
||||
ref,
|
||||
) {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const href = resolveTo(to, location);
|
||||
const click = (event: MouseEvent<HTMLAnchorElement>) => {
|
||||
onClick?.(event);
|
||||
if (
|
||||
event.defaultPrevented ||
|
||||
event.button !== 0 ||
|
||||
event.metaKey ||
|
||||
event.ctrlKey ||
|
||||
event.shiftKey ||
|
||||
event.altKey ||
|
||||
target === "_blank" ||
|
||||
props.download
|
||||
)
|
||||
return;
|
||||
const url = new URL(href, window.location.origin);
|
||||
if (url.origin !== window.location.origin) return;
|
||||
event.preventDefault();
|
||||
navigate(href, { replace, state, preventScrollReset });
|
||||
};
|
||||
return (
|
||||
<a {...props} ref={ref} href={href} target={target} onClick={click}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
});
|
||||
|
||||
export function redirect(to: string): Response {
|
||||
return new Response(null, { status: 302, headers: { Location: to } });
|
||||
}
|
||||
|
||||
export { Outlet };
|
||||
|
||||
// Existing component tests mount only a memory navigation context.
|
||||
export function MemoryRouter({ children, initialEntries }: { children?: ReactNode; initialEntries?: string[] }) {
|
||||
const url = new URL(initialEntries?.[0] ?? "/", "http://localhost");
|
||||
const location: Location = { pathname: url.pathname, search: url.search, hash: url.hash, state: null, key: "test" };
|
||||
return <TestLocation value={location}>{children}</TestLocation>;
|
||||
}
|
||||
+24
-75
@@ -1,92 +1,41 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { Outlet, useRouterState } from "@tanstack/react-router";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import { useState } from "react";
|
||||
import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { pageTitle } from "~/components/console/nav.ts";
|
||||
import { GlassTooltip } from "~/components/glass-tooltip.tsx";
|
||||
import { Toaster } from "~/components/ui/sonner.tsx";
|
||||
import { initInputModality } from "~/lib/input-modality.ts";
|
||||
|
||||
import type { Route } from "./+types/root";
|
||||
import "@fontsource-variable/inter-tight";
|
||||
import "@fontsource-variable/space-grotesk";
|
||||
import "./app.css";
|
||||
|
||||
// 触屏 / 鼠标判断要在首屏渲染前定下来(写到 <html data-input>),见 input-modality.ts
|
||||
if (typeof window !== "undefined") initInputModality();
|
||||
|
||||
export const meta: Route.MetaFunction = () => [{ title: "UHD 媒体自动化控制台" }];
|
||||
|
||||
export function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="zh-CN" suppressHydrationWarning>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body suppressHydrationWarning>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
themes={["light", "dark"]}
|
||||
enableSystem={false}
|
||||
storageKey="umt-theme"
|
||||
>
|
||||
{children}
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
{/* 玻璃提示条不叠放(叠起来后面的字会透出来):展开排列、间隔 8px;状态色只在图标上,不开 richColors */}
|
||||
<Toaster position="top-center" duration={1500} expand gap={8} />
|
||||
<GlassTooltip />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const pathname = useRouterState({ select: (state) => state.location.pathname });
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { staleTime: 30_000, refetchOnWindowFocus: false },
|
||||
},
|
||||
defaultOptions: { queries: { staleTime: 30_000, refetchOnWindowFocus: false } },
|
||||
}),
|
||||
);
|
||||
useEffect(() => {
|
||||
const title = pathname === "/login" ? "登录" : pageTitle(pathname);
|
||||
document.title = title ? `${title} · UHD 媒体自动化控制台` : "UHD 媒体自动化控制台";
|
||||
}, [pathname]);
|
||||
// SPA 首屏主题由 index.html 初始化;next-themes 的服务端引导脚本在客户端保持惰性。
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Outlet />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function HydrateFallback() {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
||||
let message = "出错了";
|
||||
let details = "发生了未预期的错误。";
|
||||
let stack: string | undefined;
|
||||
|
||||
if (isRouteErrorResponse(error)) {
|
||||
message = error.status === 404 ? "404" : "错误";
|
||||
details = error.status === 404 ? "页面不存在。" : error.statusText || details;
|
||||
} else if (import.meta.env.DEV && error instanceof Error) {
|
||||
details = error.message;
|
||||
stack = error.stack;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="container mx-auto p-4 pt-16">
|
||||
<h1 className="text-title font-semibold">{message}</h1>
|
||||
<p className="mt-2 text-muted-foreground">{details}</p>
|
||||
{stack && (
|
||||
<pre className="mt-4 w-full overflow-x-auto p-4 text-label">
|
||||
<code>{stack}</code>
|
||||
</pre>
|
||||
)}
|
||||
</main>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
themes={["light", "dark"]}
|
||||
enableSystem={false}
|
||||
storageKey="umt-theme"
|
||||
scriptProps={{ type: "text/plain" }}
|
||||
>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Outlet />
|
||||
<Toaster position="top-center" duration={1500} expand gap={8} />
|
||||
<GlassTooltip />
|
||||
</QueryClientProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { createRootRoute, createRoute, createRouter, lazyRouteComponent, redirect } from "@tanstack/react-router";
|
||||
import type { ComponentType } from "react";
|
||||
import { EmptyState } from "./components/console/empty-state.tsx";
|
||||
import { Button } from "./components/ui/button.tsx";
|
||||
import { parseUrlSearch, stringifyUrlSearch } from "./lib/router-compat.tsx";
|
||||
import App from "./root.tsx";
|
||||
|
||||
type PageModule = { default: ComponentType; clientLoader?: (args: { request: Request }) => Promise<unknown> | unknown };
|
||||
const modules = import.meta.glob<PageModule>("./routes/**/*.tsx");
|
||||
|
||||
async function loadPage(modulePath: string, href: string) {
|
||||
const module = await modules[`./routes/${modulePath}.tsx`]?.();
|
||||
if (!module) throw new Error(`路由模块不存在: ${modulePath}`);
|
||||
const result = await module.clientLoader?.({ request: new Request(new URL(href, window.location.origin)) });
|
||||
if (result instanceof Response && result.status >= 300 && result.status < 400) {
|
||||
const target = new URL(result.headers.get("Location") ?? "/console", window.location.origin);
|
||||
throw redirect({
|
||||
to: target.pathname,
|
||||
search: parseUrlSearch(target.search),
|
||||
hash: target.hash.slice(1),
|
||||
replace: true,
|
||||
} as never);
|
||||
}
|
||||
}
|
||||
|
||||
const rootRoute = createRootRoute({
|
||||
component: App,
|
||||
notFoundComponent: lazyRouteComponent(() => modules["./routes/not-found.tsx"]!()),
|
||||
errorComponent: () => (
|
||||
<main className="grid min-h-screen place-items-center bg-background px-4">
|
||||
<EmptyState
|
||||
title="页面加载失败"
|
||||
description="刷新页面后重试。"
|
||||
action={<Button onClick={() => window.location.reload()}>刷新页面</Button>}
|
||||
/>
|
||||
</main>
|
||||
),
|
||||
});
|
||||
const indexRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/",
|
||||
beforeLoad: () => {
|
||||
throw redirect({ to: "/console" } as never);
|
||||
},
|
||||
});
|
||||
const loginRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "login",
|
||||
component: lazyRouteComponent(() => modules["./routes/login.tsx"]!()),
|
||||
});
|
||||
const consoleRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "console",
|
||||
component: lazyRouteComponent(() => modules["./routes/console/layout.tsx"]!()),
|
||||
});
|
||||
|
||||
function page(path: string, modulePath: string) {
|
||||
return createRoute({
|
||||
getParentRoute: () => consoleRoute,
|
||||
path,
|
||||
component: lazyRouteComponent(() => modules[`./routes/console/${modulePath}.tsx`]!()),
|
||||
beforeLoad: ({ location }) => loadPage(`console/${modulePath}`, location.href),
|
||||
});
|
||||
}
|
||||
|
||||
const agentRoute = createRoute({
|
||||
getParentRoute: () => consoleRoute,
|
||||
path: "agent",
|
||||
component: lazyRouteComponent(() => modules["./routes/console/agent.tsx"]!()),
|
||||
});
|
||||
const agentIndexRoute = createRoute({
|
||||
getParentRoute: () => agentRoute,
|
||||
path: "/",
|
||||
component: lazyRouteComponent(() => modules["./routes/console/agent._index.tsx"]!()),
|
||||
});
|
||||
const agentRunRoute = createRoute({
|
||||
getParentRoute: () => agentRoute,
|
||||
path: "$runId",
|
||||
component: lazyRouteComponent(() => modules["./routes/console/agent.$runId.tsx"]!()),
|
||||
});
|
||||
|
||||
const routeTree = rootRoute.addChildren([
|
||||
indexRoute,
|
||||
loginRoute,
|
||||
consoleRoute.addChildren([
|
||||
page("/", "home"),
|
||||
page("inbox", "inbox"),
|
||||
page("library", "library"),
|
||||
page("library/$id", "library.$id"),
|
||||
page("media/tmdb/$type/$id", "media.tmdb.$type.$id"),
|
||||
page("identity", "identity"),
|
||||
page("organize", "organize"),
|
||||
page("organize/$id", "organize.$id"),
|
||||
page("files", "files"),
|
||||
page("files/trash", "files.trash"),
|
||||
page("discover", "discover"),
|
||||
page("subscriptions", "subscriptions"),
|
||||
page("subscriptions/$id", "subscriptions.$id"),
|
||||
page("preferences", "preferences"),
|
||||
page("sites", "sites"),
|
||||
page("sites/$siteId", "sites.$siteId"),
|
||||
page("sources", "sources"),
|
||||
page("rules", "rules"),
|
||||
page("downloads", "downloads"),
|
||||
page("downloads/plans", "downloads.plans"),
|
||||
page("downloads/plans/$id", "downloads.plans"),
|
||||
page("downloads/$id", "downloads.$id"),
|
||||
page("tasks", "tasks"),
|
||||
page("tasks/$id", "tasks.$id"),
|
||||
agentRoute.addChildren([agentIndexRoute, agentRunRoute]),
|
||||
page("engine", "engine"),
|
||||
page("engine/$runId", "engine.$runId"),
|
||||
page("audit", "audit"),
|
||||
page("logs", "logs"),
|
||||
page("settings", "settings"),
|
||||
page("schedules", "schedules"),
|
||||
page("search", "search"),
|
||||
page("status", "status"),
|
||||
]),
|
||||
]);
|
||||
|
||||
export const router = createRouter({
|
||||
routeTree,
|
||||
parseSearch: parseUrlSearch,
|
||||
stringifySearch: stringifyUrlSearch,
|
||||
scrollRestoration: true,
|
||||
defaultPreload: false,
|
||||
});
|
||||
|
||||
declare module "@tanstack/react-router" {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { index, layout, prefix, type RouteConfig, route } from "@react-router/dev/routes";
|
||||
|
||||
// 显式路由表(W3-1 维护,其他工作区不改这里)。
|
||||
// 控制台全部挂在 /console 下;/ 重定向到 /console;/login 为登录页(不经过控制台外壳)。
|
||||
// 新增工作区:在这里加一行 + components/console/nav.ts 加导航项。
|
||||
export default [
|
||||
index("routes/index-redirect.tsx"),
|
||||
route("login", "routes/login.tsx"),
|
||||
...prefix("console", [
|
||||
layout("routes/console/layout.tsx", [
|
||||
index("routes/console/home.tsx"),
|
||||
route("inbox", "routes/console/inbox.tsx"),
|
||||
route("library", "routes/console/library.tsx"),
|
||||
route("library/:id", "routes/console/library.$id.tsx"),
|
||||
route("media/tmdb/:type/:id", "routes/console/media.tmdb.$type.$id.tsx"),
|
||||
route("identity", "routes/console/identity.tsx"),
|
||||
route("organize", "routes/console/organize.tsx"),
|
||||
route("organize/:id", "routes/console/organize.$id.tsx"),
|
||||
route("files", "routes/console/files.tsx"),
|
||||
route("files/trash", "routes/console/files.trash.tsx"),
|
||||
route("discover", "routes/console/discover.tsx"),
|
||||
route("subscriptions", "routes/console/subscriptions.tsx"),
|
||||
route("subscriptions/:id", "routes/console/subscriptions.$id.tsx"),
|
||||
route("preferences", "routes/console/preferences.tsx"),
|
||||
route("sites", "routes/console/sites.tsx"),
|
||||
route("sites/:siteId", "routes/console/sites.$siteId.tsx"),
|
||||
route("sources", "routes/console/sources.tsx"),
|
||||
route("rules", "routes/console/rules.tsx"),
|
||||
route("downloads", "routes/console/downloads.tsx"),
|
||||
route("downloads/plans", "routes/console/downloads.plans.tsx"),
|
||||
route("downloads/plans/:id", "routes/console/downloads.plans.tsx", { id: "downloads-plan-legacy" }),
|
||||
route("downloads/:id", "routes/console/downloads.$id.tsx"),
|
||||
route("tasks", "routes/console/tasks.tsx"),
|
||||
route("tasks/:id", "routes/console/tasks.$id.tsx"),
|
||||
// 智能体:列表页与会话页共用外壳(标题 + 左栏会话列表),切换时只换右栏
|
||||
route("agent", "routes/console/agent.tsx", [
|
||||
index("routes/console/agent._index.tsx"),
|
||||
route(":runId", "routes/console/agent.$runId.tsx"),
|
||||
]),
|
||||
route("engine", "routes/console/engine.tsx"),
|
||||
route("engine/:runId", "routes/console/engine.$runId.tsx"),
|
||||
route("audit", "routes/console/audit.tsx"),
|
||||
route("logs", "routes/console/logs.tsx"),
|
||||
route("settings", "routes/console/settings.tsx"),
|
||||
route("schedules", "routes/console/schedules.tsx"),
|
||||
route("search", "routes/console/search.tsx"),
|
||||
route("status", "routes/console/status.tsx"),
|
||||
]),
|
||||
]),
|
||||
route("*", "routes/not-found.tsx"),
|
||||
] satisfies RouteConfig;
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ArrowLeft, PanelRightOpen } from "lucide-react";
|
||||
import { Link, useNavigate, useParams } from "react-router";
|
||||
import { agentMockEnabled } from "~/api/agent.ts";
|
||||
import { agentListHref } from "~/components/agent/agent-nav.ts";
|
||||
import { AgentRunPanel } from "~/components/agent/agent-run-panel.tsx";
|
||||
import { agentReturnPath, showAgentRunInDrawer } from "~/components/agent/agent-store.ts";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { Link, useNavigate, useParams } from "~/lib/router-compat.tsx";
|
||||
|
||||
// 会话页的右栏:和抽屉里同一个会话组件(顶部状态与统计、会话流、底部输入区);标题与左栏会话列表在外壳 agent.tsx,
|
||||
// 切换会话不重挂载。子会话在会话流里派出它的位置。
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { agentMockEnabled, useCreateAgentRun } from "~/api/agent.ts";
|
||||
import { agentRunHref, DRAFT_PAGE_GOAL, writeDraft } from "~/components/agent/agent-nav.ts";
|
||||
import { GoalComposer } from "~/components/agent/agent-run-panel.tsx";
|
||||
import { quickGoalsFor } from "~/components/agent/quick-goals.ts";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { useNavigate } from "~/lib/router-compat.tsx";
|
||||
|
||||
// 智能体会话页的右栏:新对话(和抽屉里同一个组件;标题与左栏会话列表在外壳 agent.tsx)
|
||||
export default function AgentIndexPage() {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { FlaskConical } from "lucide-react";
|
||||
import { Outlet, useParams } from "react-router";
|
||||
import { agentMockEnabled } from "~/api/agent.ts";
|
||||
import { agentRunHref, pageListMemory } from "~/components/agent/agent-nav.ts";
|
||||
import { AgentSessionList } from "~/components/agent/agent-session-list.tsx";
|
||||
import { PageHeader } from "~/components/console/page-header.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { Outlet, useParams } from "~/lib/router-compat.tsx";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useSearchParams } from "react-router";
|
||||
import { PageHeader } from "~/components/console/page-header.tsx";
|
||||
import {
|
||||
type ExploreState,
|
||||
@@ -10,6 +9,7 @@ import {
|
||||
import { ExploreView } from "~/components/discover/explore-view.tsx";
|
||||
import { RecommendView } from "~/components/discover/recommend-view.tsx";
|
||||
import { Tabs, TabsList, TabsTrigger } from "~/components/ui/tabs.tsx";
|
||||
import { useSearchParams } from "~/lib/router-compat.tsx";
|
||||
|
||||
// 发现(对照 MP 推荐 + 探索,docs/porting/moviepilot-discover.md):
|
||||
// 「推荐」按分区横排 TMDB / 豆瓣 / Bangumi 的热门与榜单,每区「查看全部」带参数切到「探索」;
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { DownloadBoardItem, DownloadBoardRow, DownloadBoardState, DownloadT
|
||||
import { EventType } from "@umt/contracts";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useSearchParams } from "react-router";
|
||||
import { useBoardPlanRow, useDownloadBoard, useDownloadTask, usePlan } from "~/api/downloads.ts";
|
||||
import { isMissingEndpoint } from "~/api/parse.ts";
|
||||
import { type DataColumn, DataTable } from "~/components/console/data-table.tsx";
|
||||
@@ -25,6 +24,7 @@ import { DownloadDetail } from "~/components/downloads/download-detail.tsx";
|
||||
import { DROPPED_STATUS, droppedReason } from "~/components/downloads/plan-format.ts";
|
||||
import { PlanDetail, WhySection } from "~/components/downloads/plan-row.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { useSearchParams } from "~/lib/router-compat.tsx";
|
||||
import { useSse } from "~/lib/sse.ts";
|
||||
|
||||
export const meta = () => [{ title: "下载面板 · UHD 媒体自动化控制台" }];
|
||||
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { useSession } from "~/api/auth.ts";
|
||||
import {
|
||||
@@ -102,6 +101,7 @@ import { SegmentedControl } from "~/components/ui/segmented.tsx";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select.tsx";
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "~/components/ui/sheet.tsx";
|
||||
import { relTime } from "~/components/workbench/release-fields.ts";
|
||||
import { useNavigate, useSearchParams } from "~/lib/router-compat.tsx";
|
||||
import { useSse } from "~/lib/sse.ts";
|
||||
import { cn, formatBytes } from "~/lib/utils.ts";
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
} from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import { useRef } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { useAttention } from "~/api/attention.ts";
|
||||
import { useDashboard, useSitesWithAccounts } from "~/api/dashboard.ts";
|
||||
@@ -34,6 +33,7 @@ import { Button } from "~/components/ui/button.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { formatRelative } from "~/lib/labels.ts";
|
||||
import { qk } from "~/lib/query-keys.ts";
|
||||
import { Link } from "~/lib/router-compat.tsx";
|
||||
import { useSse } from "~/lib/sse.ts";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { SessionInfo } from "@umt/contracts";
|
||||
import { Clapperboard, LoaderCircle, Menu, PanelLeftClose, PanelLeftOpen } from "lucide-react";
|
||||
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { Link, Outlet, useLocation, useMatches } from "react-router";
|
||||
import { useSession } from "~/api/auth.ts";
|
||||
import { AgentDrawer, AgentDrawerTrigger } from "~/components/agent/agent-drawer.tsx";
|
||||
import { outletKey } from "~/components/agent/agent-store.ts";
|
||||
@@ -18,6 +17,7 @@ import { ThemeToggle } from "~/components/theme-toggle.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "~/components/ui/sheet.tsx";
|
||||
import { redirectToLogin } from "~/lib/api.ts";
|
||||
import { Link, Outlet, useLocation } from "~/lib/router-compat.tsx";
|
||||
import { cn } from "~/lib/utils.ts";
|
||||
|
||||
const PORTAL_NAME = "媒体自动化";
|
||||
@@ -173,7 +173,7 @@ function ConsoleShell({ session }: { session: SessionInfo }) {
|
||||
const [sheetOpen, setSheetOpen] = useState(false);
|
||||
const activeLabel = pageTitle(location.pathname) ?? PORTAL_NAME;
|
||||
// 路由 handle.layout === "wide"(文件管理):内容区不限最大宽度、不留外边距,页面自己排版
|
||||
const wide = useMatches().some((m) => (m.handle as { layout?: string } | undefined)?.layout === "wide");
|
||||
const wide = location.pathname === "/console/files";
|
||||
|
||||
return (
|
||||
<div className="app-scrollbars relative isolate flex min-h-screen overflow-x-clip bg-background">
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
Tv,
|
||||
} from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Link, useParams } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import { mediaServers } from "~/api/instances.ts";
|
||||
import {
|
||||
@@ -94,6 +93,7 @@ import { CELL_LABEL, type CellKind, cellKind } from "~/components/subscriptions/
|
||||
import { SubscriptionFormDialog } from "~/components/subscriptions/subscription-form.tsx";
|
||||
import { Button } from "~/components/ui/button.tsx";
|
||||
import { errorMessage } from "~/lib/api.ts";
|
||||
import { Link, useParams } from "~/lib/router-compat.tsx";
|
||||
import { useUiPrefs } from "~/lib/ui-prefs.ts";
|
||||
import { cn, formatBytes, formatDateTime } from "~/lib/utils.ts";
|
||||
|
||||
|
||||
Loaded 100 of 134 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user