chore: 移除所有 Citations 相关逻辑,为后续重构做准备

- Backend: 删除 lead_agent / general_purpose 中的 citations_format 与引用相关 reminder;artifacts 下载不再对 markdown 做 citation 清洗,统一走 FileResponse,保留 Response 用于二进制 inline
- Frontend: 删除 core/citations 模块、inline-citation、safe-citation-content;新增 MarkdownContent 仅做 Markdown 渲染;消息/artifact 预览与复制均使用原始 content
- i18n: 移除 citations 命名空间(loadingCitations、loadingCitationsWithCount)
- 技能与 demo: 措辞改为 references,demo 数据去掉 <citations> 块
- 文档: 更新 CLAUDE/AGENTS/README 描述,新增按文件 diff 的代码变更总结

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
LofiSu
2026-02-09 16:24:01 +08:00
parent cef8d389fd
commit 46048c76ce
27 changed files with 1043 additions and 894 deletions
@@ -0,0 +1,42 @@
"use client";
import type { ImgHTMLAttributes } from "react";
import type { ReactNode } from "react";
import {
MessageResponse,
type MessageResponseProps,
} from "@/components/ai-elements/message";
import { streamdownPlugins } from "@/core/streamdown";
export type MarkdownContentProps = {
content: string;
isLoading: boolean;
rehypePlugins: MessageResponseProps["rehypePlugins"];
className?: string;
remarkPlugins?: MessageResponseProps["remarkPlugins"];
isHuman?: boolean;
img?: (props: ImgHTMLAttributes<HTMLImageElement> & { threadId?: string; maxWidth?: string }) => ReactNode;
};
/** Renders markdown content. */
export function MarkdownContent({
content,
rehypePlugins,
className,
remarkPlugins = streamdownPlugins.remarkPlugins,
img,
}: MarkdownContentProps) {
if (!content) return null;
const components = img ? { img } : undefined;
return (
<MessageResponse
className={className}
remarkPlugins={remarkPlugins}
rehypePlugins={rehypePlugins}
components={components}
>
{content}
</MessageResponse>
);
}
@@ -39,9 +39,7 @@ import { useArtifacts } from "../artifacts";
import { FlipDisplay } from "../flip-display";
import { Tooltip } from "../tooltip";
import { useThread } from "./context";
import { SafeCitationContent } from "./safe-citation-content";
import { MarkdownContent } from "./markdown-content";
export function MessageGroup({
className,
@@ -120,7 +118,7 @@ export function MessageGroup({
<ChainOfThoughtStep
key={step.id}
label={
<SafeCitationContent
<MarkdownContent
content={step.reasoning ?? ""}
isLoading={isLoading}
rehypePlugins={rehypePlugins}
@@ -128,12 +126,7 @@ export function MessageGroup({
}
></ChainOfThoughtStep>
) : (
<ToolCall
key={step.id}
{...step}
isLoading={isLoading}
rehypePlugins={rehypePlugins}
/>
<ToolCall key={step.id} {...step} isLoading={isLoading} />
),
)}
{lastToolCallStep && (
@@ -143,7 +136,6 @@ export function MessageGroup({
{...lastToolCallStep}
isLast={true}
isLoading={isLoading}
rehypePlugins={rehypePlugins}
/>
</FlipDisplay>
)}
@@ -178,7 +170,7 @@ export function MessageGroup({
<ChainOfThoughtStep
key={lastReasoningStep.id}
label={
<SafeCitationContent
<MarkdownContent
content={lastReasoningStep.reasoning ?? ""}
isLoading={isLoading}
rehypePlugins={rehypePlugins}
@@ -201,7 +193,6 @@ function ToolCall({
result,
isLast = false,
isLoading = false,
rehypePlugins,
}: {
id?: string;
messageId?: string;
@@ -210,15 +201,10 @@ function ToolCall({
result?: string | Record<string, unknown>;
isLast?: boolean;
isLoading?: boolean;
rehypePlugins: ReturnType<typeof useRehypeSplitWordsIntoSpans>;
}) {
const { t } = useI18n();
const { setOpen, autoOpen, autoSelect, selectedArtifact, select } =
useArtifacts();
const { thread } = useThread();
const threadIsLoading = thread.isLoading;
const fileContent = typeof args.content === "string" ? args.content : "";
if (name === "web_search") {
let label: React.ReactNode = t.toolCalls.searchForRelatedInfo;
@@ -364,42 +350,27 @@ function ToolCall({
}, 100);
}
const isMarkdown =
path?.toLowerCase().endsWith(".md") ||
path?.toLowerCase().endsWith(".markdown");
return (
<>
<ChainOfThoughtStep
key={id}
className="cursor-pointer"
label={description}
icon={NotebookPenIcon}
onClick={() => {
select(
new URL(
`write-file:${path}?message_id=${messageId}&tool_call_id=${id}`,
).toString(),
);
setOpen(true);
}}
>
{path && (
<ChainOfThoughtSearchResult className="cursor-pointer">
{path}
</ChainOfThoughtSearchResult>
)}
</ChainOfThoughtStep>
{isMarkdown && (
<SafeCitationContent
content={fileContent}
isLoading={threadIsLoading && isLast}
rehypePlugins={rehypePlugins}
loadingOnly
className="mt-2 ml-8"
/>
<ChainOfThoughtStep
key={id}
className="cursor-pointer"
label={description}
icon={NotebookPenIcon}
onClick={() => {
select(
new URL(
`write-file:${path}?message_id=${messageId}&tool_call_id=${id}`,
).toString(),
);
setOpen(true);
}}
>
{path && (
<ChainOfThoughtSearchResult className="cursor-pointer">
{path}
</ChainOfThoughtSearchResult>
)}
</>
</ChainOfThoughtStep>
);
} else if (name === "bash") {
const description: string | undefined = (args as { description: string })
@@ -12,7 +12,6 @@ import {
} from "@/components/ai-elements/message";
import { Badge } from "@/components/ui/badge";
import { resolveArtifactURL } from "@/core/artifacts/utils";
import { removeAllCitations } from "@/core/citations";
import {
extractContentFromMessage,
extractReasoningContentFromMessage,
@@ -24,7 +23,7 @@ import { humanMessagePlugins } from "@/core/streamdown";
import { cn } from "@/lib/utils";
import { CopyButton } from "../copy-button";
import { SafeCitationContent } from "./safe-citation-content";
import { MarkdownContent } from "./markdown-content";
export function MessageListItem({
className,
@@ -54,11 +53,11 @@ export function MessageListItem({
>
<div className="flex gap-1">
<CopyButton
clipboardData={removeAllCitations(
clipboardData={
extractContentFromMessage(message) ??
extractReasoningContentFromMessage(message) ??
""
)}
}
/>
</div>
</MessageToolbar>
@@ -154,7 +153,7 @@ function MessageContent_({
return (
<AIElementMessageContent className={className}>
{filesList}
<SafeCitationContent
<MarkdownContent
content={contentToParse}
isLoading={isLoading}
rehypePlugins={[...rehypePlugins, [rehypeKatex, { output: "html" }]]}
@@ -26,7 +26,7 @@ import { StreamingIndicator } from "../streaming-indicator";
import { MessageGroup } from "./message-group";
import { MessageListItem } from "./message-list-item";
import { SafeCitationContent } from "./safe-citation-content";
import { MarkdownContent } from "./markdown-content";
import { MessageListSkeleton } from "./skeleton";
import { SubtaskCard } from "./subtask-card";
@@ -69,7 +69,7 @@ export function MessageList({
const message = group.messages[0];
if (message && hasContent(message)) {
return (
<SafeCitationContent
<MarkdownContent
key={group.id}
content={extractContentFromMessage(message)}
isLoading={thread.isLoading}
@@ -89,7 +89,7 @@ export function MessageList({
return (
<div className="w-full" key={group.id}>
{group.messages[0] && hasContent(group.messages[0]) && (
<SafeCitationContent
<MarkdownContent
content={extractContentFromMessage(group.messages[0])}
isLoading={thread.isLoading}
rehypePlugins={rehypePlugins}
@@ -1,85 +0,0 @@
"use client";
import type { ImgHTMLAttributes } from "react";
import type { ReactNode } from "react";
import { useMemo } from "react";
import {
CitationsLoadingIndicator,
createCitationMarkdownComponents,
} from "@/components/ai-elements/inline-citation";
import {
MessageResponse,
type MessageResponseProps,
} from "@/components/ai-elements/message";
import {
shouldShowCitationLoading,
useParsedCitations,
type UseParsedCitationsResult,
} from "@/core/citations";
import { streamdownPlugins } from "@/core/streamdown";
import { cn } from "@/lib/utils";
export type SafeCitationContentProps = {
content: string;
isLoading: boolean;
rehypePlugins: MessageResponseProps["rehypePlugins"];
className?: string;
remarkPlugins?: MessageResponseProps["remarkPlugins"];
isHuman?: boolean;
img?: (props: ImgHTMLAttributes<HTMLImageElement> & { threadId?: string; maxWidth?: string }) => ReactNode;
/** When true, only show loading indicator or null (e.g. write_file step). */
loadingOnly?: boolean;
/** When set, use instead of default MessageResponse (e.g. artifact preview). */
renderBody?: (parsed: UseParsedCitationsResult) => ReactNode;
};
/** Single place for citation-aware body: shows loading until citations complete (no half-finished refs), else body. */
export function SafeCitationContent({
content,
isLoading,
rehypePlugins,
className,
remarkPlugins = streamdownPlugins.remarkPlugins,
isHuman = false,
img,
loadingOnly = false,
renderBody,
}: SafeCitationContentProps) {
const parsed = useParsedCitations(content);
const { citations, cleanContent, citationMap } = parsed;
const showLoading = shouldShowCitationLoading(content, cleanContent, isLoading);
const components = useMemo(
() =>
createCitationMarkdownComponents({
citationMap,
isHuman,
isLoadingCitations: false,
img,
}),
[citationMap, isHuman, img],
);
if (showLoading) {
return (
<CitationsLoadingIndicator
citations={citations}
className={cn("my-2", className)}
/>
);
}
if (loadingOnly) return null;
if (renderBody) return renderBody(parsed);
if (!cleanContent) return null;
return (
<MessageResponse
className={className}
remarkPlugins={remarkPlugins}
rehypePlugins={rehypePlugins}
components={components}
>
{cleanContent}
</MessageResponse>
);
}
@@ -29,7 +29,7 @@ import { cn } from "@/lib/utils";
import { FlipDisplay } from "../flip-display";
import { SafeCitationContent } from "./safe-citation-content";
import { MarkdownContent } from "./markdown-content";
export function SubtaskCard({
className,
@@ -153,7 +153,7 @@ export function SubtaskCard({
<ChainOfThoughtStep
label={
task.result ? (
<SafeCitationContent
<MarkdownContent
content={task.result}
isLoading={false}
rehypePlugins={rehypePlugins}