0.3.3 外网下载修复

This commit is contained in:
liaibo
2026-01-21 18:21:10 +08:00
parent f13de35321
commit 71de56850b
45 changed files with 2499 additions and 476 deletions
@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Textarea } from "@/components/ui/textarea";
import { ScrollArea } from "@/components/ui/scroll-area";
import { clamp, MIN_AGENT_STEPS, MAX_AGENT_STEPS } from "@/lib/constants";
type ChatMsg = { role: "user" | "assistant"; content: string };
type ToolLog =
@@ -201,14 +202,14 @@ export function AiAgentPanel() {
<input
className="w-[72px] rounded border px-2 py-1 text-xs"
type="number"
min={1}
max={24}
min={MIN_AGENT_STEPS}
max={MAX_AGENT_STEPS}
step={1}
value={maxSteps}
onChange={(e) => {
const v = Number(e.target.value);
if (!Number.isFinite(v)) return;
setMaxSteps(Math.max(1, Math.min(24, Math.floor(v))));
setMaxSteps(clamp(Math.floor(v), MIN_AGENT_STEPS, MAX_AGENT_STEPS));
}}
disabled={running}
/>
@@ -15,6 +15,7 @@ import {
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { clamp, MIN_AGENT_STEPS, MAX_AGENT_STEPS } from "@/lib/constants";
type AgentMessage = { role: "user" | "assistant"; content: string };
@@ -79,8 +80,6 @@ type ChatSession = {
type PanelPage = "chat" | "tools" | "history" | "account" | "settings";
const clamp = (n: number, min: number, max: number) => Math.max(min, Math.min(max, n));
const generateId = () => {
if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
return `sess_${Math.random().toString(16).slice(2, 10)}`;
@@ -194,8 +193,8 @@ export function DocumentAiAgentPanel({
const p = (window.localStorage.getItem("doc_ai_provider") || "").trim();
const m = window.localStorage.getItem("doc_ai_model") || "";
const parsed = Number(stepsRaw);
if (Number.isFinite(parsed) && parsed >= 1) {
setMaxSteps(Math.max(1, Math.min(24, Math.floor(parsed))));
if (Number.isFinite(parsed) && parsed >= MIN_AGENT_STEPS) {
setMaxSteps(clamp(Math.floor(parsed), MIN_AGENT_STEPS, MAX_AGENT_STEPS));
}
if (p === "local" || p === "online") setAiProvider(p);
if (typeof m === "string") setAiModel(m);
@@ -270,7 +269,7 @@ export function DocumentAiAgentPanel({
// ignore
}
// 只在 documentId 变化时读取一次
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [documentId]);
useEffect(() => {
@@ -688,14 +687,14 @@ export function DocumentAiAgentPanel({
<input
className="w-[72px] rounded border px-2 py-1 text-xs"
type="number"
min={1}
max={24}
min={MIN_AGENT_STEPS}
max={MAX_AGENT_STEPS}
step={1}
value={maxSteps}
onChange={(e) => {
const v = Number(e.target.value);
if (!Number.isFinite(v)) return;
setMaxSteps(Math.max(1, Math.min(24, Math.floor(v))));
setMaxSteps(clamp(Math.floor(v), MIN_AGENT_STEPS, MAX_AGENT_STEPS));
}}
disabled={loading}
/>
@@ -23,11 +23,12 @@ import { CustomSideMenu } from "./menus/CustomSideMenu";
import { CustomSlashMenu } from "./menus/CustomSlashMenu";
import { MoveEmbedPickerHost } from "@/components/documents/move-embed-picker-host";
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
import { DocumentToc, type TocEntry } from "@/components/editor/document-toc";
import { DocumentToc, type TocEntry } from "@/components/editor/document-toc";
import { useSearchPaletteStore } from "@/store/search-palette";
import { useEditorBridgeStore } from "@/store/editor-bridge";
import type { ReferenceTarget } from "@/types/search";
import FullScreenTableEditor from "@/components/online-table/FullScreenTableEditor";
import { clamp, MIN_BLOCK_LEVEL, MAX_BLOCK_LEVEL } from "@/lib/constants";
import { ASSETS_CHANGED_EVENT, emitAssetsChanged } from "@/lib/events";
interface BlockNoteEditorProps {
@@ -64,7 +65,7 @@ const buildHeadingToc = (blocks: Block<CustomBlockSchema>[]): TocEntry[] => {
const walk = (targetBlocks: Block<CustomBlockSchema>[]) => {
targetBlocks.forEach((block) => {
if (block.type === "heading") {
const level = Math.min(5, Math.max(1, Number(block.props.level) || 1));
const level = clamp(Number(block.props.level) || 1, MIN_BLOCK_LEVEL, MAX_BLOCK_LEVEL);
counters[level - 1] += 1;
for (let i = level; i < counters.length; i += 1) {
counters[i] = 0;
@@ -656,9 +656,6 @@ const MediaBlockContent = ({ block, editor }: any) => {
>
</DropdownMenuItem>
{assetType === "file" && isOfficeDoc && (
<DropdownMenuItem onClick={() => void openWithOnlyOffice()}>使 ONLYOFFICE </DropdownMenuItem>
)}
<DropdownMenuItem
onClick={() => {
void downloadAsset();
@@ -273,7 +273,7 @@ export function MindmapAiAgentPanel({
} catch {
// ignore
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mindmapId]);
useEffect(() => {
@@ -2502,8 +2502,8 @@ const MindmapBlockView = ({
const handleImageConfirm = async () => {
const url = imageUrl.trim();
// 获取原始图片尺寸,如果没有则使用默认值
let width = Number(imageWidth) || 0;
let height = Number(imageHeight) || 0;
const width = Number(imageWidth) || 0;
const height = Number(imageHeight) || 0;
if (!url) {
window.alert("请输入图片链接");
@@ -14,6 +14,7 @@ import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
import { ImagePickerProvider } from "@/components/media/image-picker-context";
import { useRouter } from "next/navigation";
import { DocumentAiAgentPanel } from "./DocumentAiAgentPanel";
import { CONTENT_LOADING_DELAY_MS } from "@/lib/constants";
const BlockNoteEditor = dynamic(
() => import("@/components/editor/blocknote-editor").then((mod) => mod.BlockNoteEditor),
@@ -96,7 +97,7 @@ export function DocumentContent({
}
}, [documentId, editorBridge, openTableId, router]);
/* eslint-disable react-hooks/set-state-in-effect */
useEffect(() => {
setPageTitle(title ?? "无标题");
}, [title]);
@@ -108,7 +109,7 @@ export function DocumentContent({
useEffect(() => {
setStats(initialStats ?? defaultStats);
}, [initialStats]);
/* eslint-enable react-hooks/set-state-in-effect */
useEffect(() => {
let canceled = false;
@@ -128,12 +129,12 @@ export function DocumentContent({
clearTimeout(contentLoadingTimerRef.current);
contentLoadingTimerRef.current = null;
}
// 避免秒闪的加载提示:只有当加载超过短阈值时才显示提示
// 避免"秒闪"的加载提示:只有当加载超过短阈值时才显示提示
contentLoadingTimerRef.current = setTimeout(() => {
if (!canceled) {
setShowContentLoadingIndicator(true);
}
}, 200);
}, CONTENT_LOADING_DELAY_MS);
try {
const response = await fetch(`/api/documents/content?documentId=${encodeURIComponent(documentId)}`, {
@@ -503,7 +503,7 @@ const FullScreenTableEditor: React.FC<FullScreenTableEditorProps> = ({ tableId,
};
if (process.env.NODE_ENV !== "production") {
// eslint-disable-next-line no-console
console.log("[FullScreenTableEditor] init luckysheet", { tableId, sheets: options.data });
}
try {
@@ -5,6 +5,7 @@ import { Bot, Settings, Wrench, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Textarea } from "@/components/ui/textarea";
import { clamp } from "@/lib/constants";
type AgentMessage = { role: "user" | "assistant"; content: string };
@@ -35,8 +36,6 @@ const ONLINE_MODELS = [
"gemini-3-flash-preview",
] as const;
const clamp = (n: number, min: number, max: number) => Math.max(min, Math.min(max, n));
const isRecord = (v: unknown): v is Record<string, unknown> =>
typeof v === "object" && v !== null && !Array.isArray(v);
@@ -3,6 +3,7 @@
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import Link from "next/link";
import { useRouter, useSelectedLayoutSegments } from "next/navigation";
import { useAuthActions } from "@convex-dev/auth/react";
import {
ArrowRightLeft,
ArrowUpRight,
@@ -15,6 +16,7 @@ import {
LayoutGrid,
Library,
Link as LinkIcon,
LogOut,
MoreHorizontal,
PanelRightOpen,
Plus,
@@ -178,6 +180,7 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
const isLoading = sidebarQuery.isLoading;
const segments = useSelectedLayoutSegments();
const router = useRouter();
const { signOut } = useAuthActions();
const activeId = segments?.[1] ?? "";
const editorBridge = useEditorBridgeStore((state) => state.bridge);
@@ -186,6 +189,7 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
const [expanded, setExpanded] = useState<Set<string>>(() => collectNodeIds(tree));
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
const [workspaceMenuOpen, setWorkspaceMenuOpen] = useState(false);
const [signingOut, setSigningOut] = useState(false);
const [trashOpen, setTrashOpen] = useState(false);
const [trashSearch, setTrashSearch] = useState("");
const [trashTab, setTrashTab] = useState<"documents" | "assets">("documents");
@@ -1729,6 +1733,26 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
[sidebarData.activeWorkspaceId, sidebarQuery],
);
const handleSignOut = useCallback(async () => {
if (signingOut) {
return;
}
if (!window.confirm("确认退出登录吗?")) {
return;
}
setSigningOut(true);
try {
await signOut();
setWorkspaceMenuOpen(false);
router.replace("/auth");
router.refresh();
} catch (error: any) {
window.alert(`退出登录失败:${error?.message ?? "请稍后再试"}`);
} finally {
setSigningOut(false);
}
}, [router, signOut, signingOut]);
const openContextMenu = useCallback((event: React.MouseEvent, node: DocumentNode) => {
event.preventDefault();
event.stopPropagation();
@@ -1826,6 +1850,17 @@ function SidebarContent({ initialData, sidebarQuery }: SidebarContentProps) {
)}
</button>
))}
<div className="border-t border-[#f1f1f1] p-1">
<button
type="button"
className="flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm text-red-600 hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-60"
onClick={() => void handleSignOut()}
disabled={signingOut}
>
<LogOut className="h-4 w-4" />
<span>{signingOut ? "正在退出..." : "退出登录"}</span>
</button>
</div>
</div>
)}
</div>