Merge origin/master into ji

This commit is contained in:
吉鑫升 committed 2026-06-05 10:04:11 +08:00
commit e96cc97462
54 files changed
+4900 -334

No files matched your search

@@ -44,18 +44,23 @@ import {
useArtifactContent,
useUpdateArtifactContent,
} from "@/core/artifacts/hooks";
import { urlOfArtifact } from "@/core/artifacts/utils";
import { resolveArtifactURL, urlOfArtifact } from "@/core/artifacts/utils";
import { useI18n } from "@/core/i18n/hooks";
import { downloadMarkdownAsDocx } from "@/core/scheduled-tasks/markdown-docx";
import { installSkill } from "@/core/skills/api";
import { streamdownPlugins } from "@/core/streamdown";
import { checkCodeFile, getFileName } from "@/core/utils/files";
import {
createMarkdownAnchorResolver,
injectInPageAnchorHandler,
isInPageAnchor,
scrollToInPageAnchor,
} from "@/core/utils/markdown";
import {
checkCodeFile,
getFileName,
isImageFile,
} from "@/core/utils/files";
import {
createMarkdownAnchorResolver,
injectInPageAnchorHandler,
isInPageAnchor,
scrollToInPageAnchor,
} from "@/core/utils/markdown";
import { getBackendBaseURL } from "@/core/config";
import { env } from "@/env";
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
@@ -98,29 +103,36 @@ export function ArtifactFileDetail({
className,
filepath: filepathFromProps,
threadId,
onClose,
}: {
className?: string;
filepath: string;
threadId: string;
/** 覆盖默认关闭行为(仅 setOpen(false));圆桌 Step3 文件预览用于退出沙箱模式。 */
onClose?: () => void;
}) {
const { t } = useI18n();
const { artifacts, setOpen, select } = useArtifacts();
const { isMock, thread, publicShareCode } = useThread();
const isWriteFile = useMemo(() => {
const isWriteFileVirtual = useMemo(() => {
return filepathFromProps.startsWith("write-file:");
}, [filepathFromProps]);
const filepath = useMemo(() => {
if (isWriteFile) {
if (isWriteFileVirtual) {
const url = new URL(filepathFromProps);
return decodeURIComponent(url.pathname);
}
return filepathFromProps;
}, [filepathFromProps, isWriteFile]);
}, [filepathFromProps, isWriteFileVirtual]);
/** write_file 流式预览走 tool args 文本;图片等二进制产物始终走 artifacts API。 */
const useWriteFileStreaming = useMemo(() => {
return isWriteFileVirtual && !isImageFile(filepath);
}, [filepath, isWriteFileVirtual]);
const isSkillFile = useMemo(() => {
return filepath.endsWith(".skill");
}, [filepath]);
const { isCodeFile, language } = useMemo(() => {
if (isWriteFile) {
if (useWriteFileStreaming) {
let nextLanguage = checkCodeFile(filepath).language;
nextLanguage ??= "text";
return { isCodeFile: true, language: nextLanguage };
@@ -129,18 +141,21 @@ export function ArtifactFileDetail({
return { isCodeFile: true, language: "markdown" };
}
return checkCodeFile(filepath);
}, [filepath, isWriteFile, isSkillFile]);
}, [filepath, useWriteFileStreaming, isSkillFile]);
const isSupportPreview = useMemo(() => {
return language === "html" || language === "markdown";
}, [language]);
const { content, isApplyingEdit } = useArtifactContent({
threadId,
filepath: filepathFromProps,
enabled: isCodeFile && !isWriteFile,
enabled: isCodeFile && !useWriteFileStreaming,
});
const canEditArtifact =
language === "markdown" && !isWriteFile && !isSkillFile && !isMock;
language === "markdown" &&
!useWriteFileStreaming &&
!isSkillFile &&
!isMock;
const [draftContent, setDraftContent] = useState("");
const [hasLocalEdits, setHasLocalEdits] = useState(false);
const updateArtifact = useUpdateArtifactContent();
@@ -148,12 +163,13 @@ export function ArtifactFileDetail({
const isHtmlStreaming = language === "html" && thread.isLoading;
const hasMultipleArtifacts = (artifacts?.length ?? 0) > 1;
const fileTypeLabel = useMemo(() => {
if (isImageFile(filepath)) return "IMG";
if (language === "html") return "HTML";
if (language === "markdown") return "MD";
if (isSkillFile) return "SKILL";
if (language) return language.toUpperCase();
return "FILE";
}, [isSkillFile, language]);
}, [filepath, isSkillFile, language]);
const [viewMode, setViewMode] = useState<"code" | "preview">("code");
const [allowAutoViewSwitch, setAllowAutoViewSwitch] = useState(true);
@@ -463,7 +479,7 @@ export function ArtifactFileDetail({
{fileTypeLabel}
</span>
<ArtifactTitle className="min-w-0">
{isWriteFile ? (
{useWriteFileStreaming ? (
<div className="truncate px-3">{getFileName(filepath)}</div>
) : hasMultipleArtifacts ? (
<Select value={filepath} onValueChange={select}>
@@ -540,7 +556,7 @@ export function ArtifactFileDetail({
onClick={handleSaveArtifact}
/>
)}
{!isWriteFile && filepath.endsWith(".skill") && (
{!useWriteFileStreaming && filepath.endsWith(".skill") && (
<Tooltip content={t.toolCalls.skillInstallTooltip}>
<ArtifactAction
icon={isInstalling ? LoaderIcon : PackageIcon}
@@ -559,7 +575,7 @@ export function ArtifactFileDetail({
label={t.common.openInNewWindow}
tooltip={t.common.openInNewWindow}
onClick={async () => {
if (isWriteFile) {
if (useWriteFileStreaming) {
handleOpenWriteFileInNewTab();
return;
}
@@ -597,7 +613,7 @@ export function ArtifactFileDetail({
tooltip={t.clipboard.copyToClipboard}
/>
)}
{!isWriteFile && language === "markdown" && (
{!useWriteFileStreaming && language === "markdown" && (
<ArtifactAction
icon={isExportingWord ? LoaderIcon : FileType2Icon}
label="另存为 Word"
@@ -611,7 +627,7 @@ export function ArtifactFileDetail({
label={t.common.download}
tooltip={t.common.download}
onClick={async () => {
if (isWriteFile) {
if (useWriteFileStreaming) {
handleDownloadWriteFile();
return;
}
@@ -636,7 +652,7 @@ export function ArtifactFileDetail({
className="hover:bg-background"
icon={XIcon}
label={t.common.close}
onClick={() => setOpen(false)}
onClick={() => (onClose ? onClose() : setOpen(false))}
tooltip={t.common.close}
/>
</ArtifactActions>
@@ -669,6 +685,8 @@ export function ArtifactFileDetail({
<ArtifactFilePreview
content={desensitize(displayContent ?? "")}
language={language ?? "text"}
threadId={threadId}
publicShareCode={publicShareCode}
animated={showPreviewSwitchPulse}
activeCursor={isApplyingEdit}
/>
@@ -709,14 +727,57 @@ export function ArtifactFileDetail({
);
}
function SandboxPreviewImage({
src,
alt,
threadId,
publicShareCode,
...props
}: React.ImgHTMLAttributes<HTMLImageElement> & {
threadId: string;
publicShareCode?: string;
}) {
if (!src || typeof src !== "string") {
return null;
}
let url = src;
if (src.startsWith("/mnt/")) {
if (publicShareCode) {
const normalized = src.startsWith("/") ? src : `/${src}`;
url = `${getBackendBaseURL()}/api/public/shares/${encodeURIComponent(
publicShareCode,
)}/files${normalized}`;
} else {
url = resolveArtifactURL(src, threadId);
}
}
return (
<img
{...props}
className={cn(
"my-3 max-w-full overflow-hidden rounded-lg",
props.className,
)}
src={url}
alt={alt}
/>
);
}
export function ArtifactFilePreview({
content,
language,
threadId,
publicShareCode,
animated = false,
activeCursor = false,
}: {
content: string;
language: string;
threadId: string;
publicShareCode?: string;
animated?: boolean;
activeCursor?: boolean;
}) {
@@ -812,11 +873,20 @@ export function ArtifactFilePreview({
animated && "animate-[sandbox-preview-enter_320ms_ease-out]",
)}
>
<Streamdown
className="size-full"
{...streamdownPlugins}
components={markdownComponents}
>
<Streamdown
className="size-full"
{...streamdownPlugins}
components={{
...markdownComponents,
img: (props) => (
<SandboxPreviewImage
{...props}
threadId={threadId}
publicShareCode={publicShareCode}
/>
),
}}
>
{content ?? ""}
</Streamdown>
{activeCursor && (