@blade-hq/agent-react 2610.0.0-beta.73 → 2610.0.0-beta.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/components/MarkdownContent.d.ts +15 -0
- package/dist/components/markdown/heal-incomplete.d.ts +7 -0
- package/dist/components/markdown/incremental.d.ts +73 -0
- package/dist/components/markdown/index.d.ts +18 -0
- package/dist/components/markdown/parse.d.ts +19 -0
- package/dist/components/markdown/render.d.ts +79 -0
- package/dist/components/markdown/sanitize.d.ts +50 -0
- package/dist/components/markdown/types.d.ts +53 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +13831 -1667
- package/dist/index.js.map +1 -1
- package/dist/style.css +27 -8
- package/dist/style.full.css +28 -9
- package/package.json +2 -2
- package/public-api.md +82 -4
- package/dist/chunk-IA6J2NTB.js +0 -28232
- package/dist/chunk-IA6J2NTB.js.map +0 -1
- package/dist/highlighted-body-B3W2YXNL-YF4GVJGP.js +0 -28
- package/dist/highlighted-body-B3W2YXNL-YF4GVJGP.js.map +0 -1
- package/dist/mermaid-3ZIDBTTL-QL4YGD6I.js +0 -9
- package/dist/mermaid-3ZIDBTTL-QL4YGD6I.js.map +0 -1
package/README.md
CHANGED
|
@@ -554,7 +554,7 @@ blade-chat::part(input-toolbar) { gap: 12px; }
|
|
|
554
554
|
|
|
555
555
|
为方便单包引入,本包 re-export 了 agent-client 的全部公开声明(`BladeClient`、`AgentSession`、`SessionState`、协议类型等),文档一律见 [agent-client 的 README](../agent-client/README.md)。完整签名见 [public-api.md](./public-api.md)。
|
|
556
556
|
|
|
557
|
-
本包自有声明:`BladeProvider`、`BladeProviderProps`、`useBladeClient`、`useAgentSession`、`UseAgentSessionOptions`、`UseAgentSessionResult`、`useMessagePin`、`UseMessagePinOptions`、`useTypewriterReveal`、`UseTypewriterRevealResult`、`useReplay`、`UseReplayResult`、`ReplayMismatch`、`ReplayBar`、`ReplayBarProps`、`ReplayMismatchPrompt`、`ReplayMismatchPromptProps`、`ChatView`、`ChatViewProps`、`ChatViewClassNames`、`ChatViewRenderers`、`ChatViewSlots`、`FollowupInteractionEvent`、`ToolCallRenderer`、`CurrentPlanPanel`、`PlanUpdateBlock`、`PlanUpdateData`、`PlanUpdateDisplayState`、`PlanStepStatus`、`getPlanUpdateDisplayState`、`isPlanUpdateTool`、`parsePlanUpdate`、`pickCurrentPlanStep`、`PLAN_AUTO_COLLAPSE_MS`、`MarkdownContent`、`MarkdownContentProps`、`parseWhatIfPrompt`、`ParsedWhatIfPrompt`、`WhatIfQuote`、`WhatIfUserBubble`、`WhatIfUserBubbleProps`、`SessionQueuePanel`、`SessionQueuePanelProps`、`isQueuePanelVisible`。
|
|
557
|
+
本包自有声明:`BladeProvider`、`BladeProviderProps`、`useBladeClient`、`useAgentSession`、`UseAgentSessionOptions`、`UseAgentSessionResult`、`useMessagePin`、`UseMessagePinOptions`、`useTypewriterReveal`、`UseTypewriterRevealResult`、`useReplay`、`UseReplayResult`、`ReplayMismatch`、`ReplayBar`、`ReplayBarProps`、`ReplayMismatchPrompt`、`ReplayMismatchPromptProps`、`ChatView`、`ChatViewProps`、`ChatViewClassNames`、`ChatViewRenderers`、`ChatViewSlots`、`FollowupInteractionEvent`、`ToolCallRenderer`、`CurrentPlanPanel`、`PlanUpdateBlock`、`PlanUpdateData`、`PlanUpdateDisplayState`、`PlanStepStatus`、`getPlanUpdateDisplayState`、`isPlanUpdateTool`、`parsePlanUpdate`、`pickCurrentPlanStep`、`PLAN_AUTO_COLLAPSE_MS`、`MarkdownContent`、`MarkdownContentProps`、`MarkdownRenderer`、`MarkdownRendererProps`、`MarkdownComponents`、`AllowedTags`、`DEFAULT_ALLOWED_TAGS`、`CodeBlockOverrides`、`LinkSafetyConfig`、`RawHtmlPlugin`、`parseWhatIfPrompt`、`ParsedWhatIfPrompt`、`WhatIfQuote`、`WhatIfUserBubble`、`WhatIfUserBubbleProps`、`SessionQueuePanel`、`SessionQueuePanelProps`、`isQueuePanelVisible`、`ExternalAnchor`、`stripSystemReminders`。
|
|
558
558
|
|
|
559
559
|
## 会话消息队列
|
|
560
560
|
|
|
@@ -1,5 +1,19 @@
|
|
|
1
|
+
import { type ComponentPropsWithRef } from "react";
|
|
2
|
+
/** 内置 Web(`apps/web/src/agent/components/markdown/MarkdownContent.tsx`)
|
|
3
|
+
* 有一份同名同签名的实现,两处共用同一套"跳过代码区间"的判定逻辑,改动
|
|
4
|
+
* 时要一起确认。这里作为 SDK 侧的规范实现导出,内置 Web 直接复用,不要
|
|
5
|
+
* 各写一份(对齐 CLAUDE.md"判定逻辑不许各写一份")。 */
|
|
6
|
+
export declare function stripSystemReminders(text: string): string;
|
|
1
7
|
/** Keep inline formatting markers from being consumed by URL autolinking. */
|
|
2
8
|
export declare function normalizeAdjacentUrlFormatting(value: string): string;
|
|
9
|
+
type AnchorProps = ComponentPropsWithRef<"a">;
|
|
10
|
+
/** 链接一律新窗口打开并断开 opener,SDK 不接管站内跳转。
|
|
11
|
+
* 页内锚点(脚注引用/回链这类 `href="#fn-note"`)不能套"新窗口打开"这套
|
|
12
|
+
* 逻辑——新开一个标签页访问 `#片段` 只是把整篇文档重新打开一次,不是
|
|
13
|
+
* 滚动到页内位置,会破坏脚注的跳转体验。只有真正的外部链接才需要新窗口
|
|
14
|
+
* + 断开 opener。导出给内置 Web 直接复用——两处原来各写了一份完全相同的
|
|
15
|
+
* 判定逻辑,对齐 CLAUDE.md"判定逻辑不允许两处各写一份"。 */
|
|
16
|
+
export declare function ExternalAnchor({ children, href, ...props }: AnchorProps): import("react/jsx-runtime").JSX.Element;
|
|
3
17
|
export interface MarkdownContentProps {
|
|
4
18
|
children: string;
|
|
5
19
|
className?: string;
|
|
@@ -13,3 +27,4 @@ export interface MarkdownContentProps {
|
|
|
13
27
|
* mermaid、外链确认弹窗与文件预览。
|
|
14
28
|
*/
|
|
15
29
|
export declare function MarkdownContent({ children, className, mode, sessionId }: MarkdownContentProps): import("react/jsx-runtime").JSX.Element;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Root, RootContent } from "mdast";
|
|
2
|
+
import type { HealResult } from "./heal-incomplete";
|
|
3
|
+
/**
|
|
4
|
+
* 增量解析缓存:跨帧复用"已经确认不会再变"的块,只重新解析真正在增长的
|
|
5
|
+
* 尾部。算法对齐 vercel/streamdown PR #608(未发版,只做块级冻结,不需要
|
|
6
|
+
* 额外的"冻结代数"概念)——比 DeepSeek Harness 的实现更简单,这里直接采用。
|
|
7
|
+
*/
|
|
8
|
+
export interface ParseCache {
|
|
9
|
+
/** 当前已知的全部块(冻结 + 尚不稳定的尾部),按源码顺序——上一次
|
|
10
|
+
* 增量解析的完整结果,下一帧从这里的最后一个块的起点开始重新解析
|
|
11
|
+
* (见 `incrementalParse`),不直接拿来渲染。 */
|
|
12
|
+
blocks: RootContent[];
|
|
13
|
+
/** 上一次成功走增量路径时的完整输入,用于判断新输入是不是纯追加。 */
|
|
14
|
+
input: string;
|
|
15
|
+
/** 是否已经在文档里见过定义语法——一旦见过就一直为 true(退化成整篇
|
|
16
|
+
* 重新解析是"一票否决、不会再变回增量路径"的单向状态),不需要每帧
|
|
17
|
+
* 重新确认已经扫过的部分。 */
|
|
18
|
+
hasDefinition: boolean;
|
|
19
|
+
/** 已经确认"从文档开头到这个偏移量之间的完整行都不含定义语法"的进度——
|
|
20
|
+
* 一定落在某一行的行首(要么是 0,要么紧跟在某个 `\n` 之后),下一次
|
|
21
|
+
* 只需要从这里往后扫新增的文本,不用把已经扫过的部分重新跑一遍正则。
|
|
22
|
+
* 最后一行只要还没被 `\n` 收尾就永远不推进过它的行首——它可能还在
|
|
23
|
+
* 增长,提前当作"已确认无定义"会漏检定义语法后半截还没到达的情况。
|
|
24
|
+
* 只统计不在围栏代码块内的行——代码示例里出现 `[ref]: url` 形状的
|
|
25
|
+
* 文本很常见(这本身就是个 markdown 渲染器项目,模型很可能给出这样的
|
|
26
|
+
* 教学示例),真按字面匹配会永久误判成"文档里有定义语法",之后每一帧
|
|
27
|
+
* 都退化成整篇 O(N) 重新解析,直接抵消增量解析本来要解决的性能问题;
|
|
28
|
+
* 见 `updateDefinitionScan`。 */
|
|
29
|
+
definitionScanOffset: number;
|
|
30
|
+
/** `definitionScanOffset` 这个位置是不是落在一个还没闭合的围栏代码块
|
|
31
|
+
* 内部——跨帧持续,配合 `fenceChar`/`fenceLength` 判断后续新增的行
|
|
32
|
+
* 是不是"闭合围栏",不是每次都从头重新判断整篇文本的围栏嵌套状态。 */
|
|
33
|
+
insideFencedCode: boolean;
|
|
34
|
+
fenceChar: string | null;
|
|
35
|
+
fenceLength: number;
|
|
36
|
+
}
|
|
37
|
+
export declare function createParseCache(): ParseCache;
|
|
38
|
+
/**
|
|
39
|
+
* 一个块只有在"以空行结尾,且后面至少还有两个块"时才能确定不会再被后续
|
|
40
|
+
* 内容改写——最后一个块必然是当前解析前沿,倒数第二个块是安全边际。取
|
|
41
|
+
* 倒数第三个块及之前的块作为安全冻结区。
|
|
42
|
+
*/
|
|
43
|
+
/** 导出给增量解析等价性质测试用——测试要拿它对"整篇重新解析"的结果算出
|
|
44
|
+
* 同样的 `suppressPendingTableTail` 范围,才能公平比较两条路径的输出。 */
|
|
45
|
+
export declare function countStableBlocks(blockCount: number): number;
|
|
46
|
+
export interface IncrementalResult {
|
|
47
|
+
root: Root;
|
|
48
|
+
cache: ParseCache;
|
|
49
|
+
}
|
|
50
|
+
/** 用一个不渲染任何内容的占位节点替换掉"疑似正在长成表格、还没能确定"的
|
|
51
|
+
* 尾部段落——憋到确定下来(变成真正的 table,或者冻结成确定的普通段落)
|
|
52
|
+
* 再显示,不把过程中的原始竖线文字暴露给用户。导出给测试用:增量等价性质
|
|
53
|
+
* 测试要拿它对"整篇重新解析"的结果做同样的处理,才能验证增量路径没有
|
|
54
|
+
* 引入*额外*的差异(这里的差异是刻意的显示层降级,两边都做才是公平对比)。 */
|
|
55
|
+
export declare function suppressPendingTableTail(blocks: RootContent[], verifiedCount: number): RootContent[];
|
|
56
|
+
export interface IncrementalParseOptions {
|
|
57
|
+
/** streaming 模式下,在把"最后一个尚不稳定的块"渲染出来之前先过一遍
|
|
58
|
+
* 未闭合语法降级(给还没等到闭合符的自配对标记在末尾补上闭合符,让
|
|
59
|
+
* 它按最终形态渲染,只在对应节点上打一个"待确认"标记)。这一层健壮性
|
|
60
|
+
* 处理只影响*这一帧的显示*,不参与缓存/偏移量的持久化——补的闭合符会
|
|
61
|
+
* 改变字符串长度,如果拿它算出来的 offset 去驱动下一次的切片位置,
|
|
62
|
+
* 长度差会累积成偏移量错位,往后每一次切片都会切错地方。所以 `cache.blocks`
|
|
63
|
+
* 全程只认*原始未处理*文本解析出来的结果;这一层只用来现算现扔地重新
|
|
64
|
+
* 渲染最后那一个尚不稳定的块,每帧都重新做一遍,不持久化,不会有偏移量
|
|
65
|
+
* 累积错位的问题。 */
|
|
66
|
+
healTail?: (text: string) => HealResult;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 给定当前完整目标文本,返回可以直接渲染的 mdast 根节点,以及供下一次调用
|
|
70
|
+
* 复用的缓存。纯函数——不持有状态,调用方(渲染组件)自己在 ref 里存
|
|
71
|
+
* `ParseCache` 并逐帧传进来。
|
|
72
|
+
*/
|
|
73
|
+
export declare function incrementalParse(cache: ParseCache, input: string, options?: IncrementalParseOptions): IncrementalResult;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { MarkdownRendererProps } from "./types";
|
|
2
|
+
export type { CodeBlockOverrides, LinkSafetyConfig, MarkdownComponents, MarkdownRendererProps, RawHtmlPlugin, } from "./types";
|
|
3
|
+
export { DEFAULT_ALLOWED_TAGS, type AllowedTags } from "./sanitize";
|
|
4
|
+
/**
|
|
5
|
+
* 增量 Markdown 渲染器:`packages/agent-react` 与 `apps/web` 两处
|
|
6
|
+
* `MarkdownContent.tsx` 共用的核心引擎(对齐 `CLAUDE.md`"共享能力先做进
|
|
7
|
+
* SDK 再让内置 Web 用"这条原则)。取代原来包一层 `<Streamdown>` 的做法——
|
|
8
|
+
* 用真正的增量解析(`incremental.ts`)替换 streamdown 每次都重新 lex 全部
|
|
9
|
+
* 已到达内容的实现,解决长回复流式渲染耗时随内容长度平方增长的问题。
|
|
10
|
+
*
|
|
11
|
+
* `mode="streaming"` 时:增量解析冻结前缀 + 未闭合语法降级都生效;
|
|
12
|
+
* `mode="static"`(默认)时:整篇一次性解析,不做任何增量假设——这是
|
|
13
|
+
* "流式与历史必须产生相同结果"这条硬规则唯一允许的例外只在真正实时直播
|
|
14
|
+
* 里生效的直接体现。
|
|
15
|
+
*/
|
|
16
|
+
export declare function MarkdownRenderer({ children, className, mode, allowedTags, components, rawHtmlPlugins, codeBlockOverrides, }: MarkdownRendererProps): import("react").DetailedReactHTMLElement<{
|
|
17
|
+
className: string;
|
|
18
|
+
}, HTMLElement>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Root } from "mdast";
|
|
2
|
+
/**
|
|
3
|
+
* Markdown 字符串 -> mdast 语法树,启用 GFM(表格、任务列表、删除线、脚注、
|
|
4
|
+
* 自动链接)。不做增量缓存——那是 `incremental.ts` 的职责,这里只是纯函数
|
|
5
|
+
* 封装,方便增量解析对"整篇重新解析"和"只解析一段尾部"复用同一套扩展配置。
|
|
6
|
+
*
|
|
7
|
+
* `cjkFriendlyExtension` 覆盖了 CommonMark 默认的强调符号(`**`/`_`)
|
|
8
|
+
* flanking 判断——按标准规则,中文标点紧贴在闭合标记前、后面又紧跟着
|
|
9
|
+
* 普通汉字(既不是空白也不是标点)时,这个闭合标记既不算左 flanking
|
|
10
|
+
* 也不算右 flanking,整段 `**xxx**` 会被判定成没有真正的闭合符,渲染成
|
|
11
|
+
* 字面量星号——这是中文场景下最常见的加粗写法,例如"这一问是:**重点
|
|
12
|
+
* 内容**下一句",标准 CommonMark 库(真实生产环境验证过)都会渲染
|
|
13
|
+
* 失败。换引擎前 streamdown 通过 `@streamdown/cjk`(内部就是这个
|
|
14
|
+
* 扩展)解决了这个问题,新引擎必须带上同一个扩展,否则是从"迁移前能用"
|
|
15
|
+
* 倒退成"迁移后不能用"。必须排在 `gfm()` 之前——它需要在 GFM 扩展
|
|
16
|
+
* (比如删除线)接管同一批触发字符之前先接管强调符号的 tokenizer,
|
|
17
|
+
* 这是上游包自己文档里写明的顺序要求。
|
|
18
|
+
*/
|
|
19
|
+
export declare function parseMarkdown(markdown: string): Root;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Definition, FootnoteDefinition, FootnoteReference, Root } from "mdast";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
import { type AllowedTags } from "./sanitize";
|
|
4
|
+
import type { CodeBlockOverrides, MarkdownComponents, RawHtmlPlugin } from "./types";
|
|
5
|
+
export interface RenderContext {
|
|
6
|
+
components?: MarkdownComponents;
|
|
7
|
+
allowedTags: AllowedTags;
|
|
8
|
+
rawHtmlPlugins?: RawHtmlPlugin[];
|
|
9
|
+
codeBlockOverrides?: CodeBlockOverrides;
|
|
10
|
+
/** 收集渲染过程中遇到的脚注定义,root 渲染完之后统一在文末生成脚注区。
|
|
11
|
+
* 由 `collectFootnoteMetadata` 在渲染前对完整树做一次独立遍历填好,
|
|
12
|
+
* `renderNode` 只读不写——见该函数的注释,原因是按块缓存命中时会跳过
|
|
13
|
+
* `renderNode`,不能再依赖它的遍历顺序当副作用来源。 */
|
|
14
|
+
footnoteDefinitions: Map<string, FootnoteDefinition>;
|
|
15
|
+
/** 脚注按"引用点第一次出现的顺序"编号(1、2、3……),不是按标识符文本
|
|
16
|
+
* 本身——`[^note]` 应该显示成"1"而不是字面量"note",跟 GFM 脚注惯例
|
|
17
|
+
* 和迁移前 streamdown 的行为一致。 */
|
|
18
|
+
footnoteOrder: Map<string, number>;
|
|
19
|
+
/** 同一个脚注标识符可能被正文引用不止一次(`[^note]` 出现两次)——每个
|
|
20
|
+
* 引用点必须有自己独立的 id,不能都叫 `fnref-note`(重复 id 是无效
|
|
21
|
+
* HTML,`aria-describedby` 和跳转都只能找到第一个);这张表记录每个
|
|
22
|
+
* 标识符已经生成过的引用点 id,脚注区据此给每一次引用各自渲染一条
|
|
23
|
+
* 回链,而不是只有第一次引用能跳回去。 */
|
|
24
|
+
footnoteRefIds: Map<string, string[]>;
|
|
25
|
+
/** 每一个具体的 `footnoteReference` 节点对象自己分到的引用点 id——
|
|
26
|
+
* `footnoteRefIds` 是按标识符聚合的完整列表(脚注区渲染全部回链要用),
|
|
27
|
+
* 但单个引用点在正文里渲染自己的 `id` 属性时,需要知道"我是这个标识符
|
|
28
|
+
* 第几次出现",而按块缓存命中会跳过一部分引用点节点的渲染调用,不能在
|
|
29
|
+
* `renderNode` 遍历时临时数"目前见过几次"来推算——用节点对象本身的
|
|
30
|
+
* 引用当 key,跟渲染是否命中缓存无关。 */
|
|
31
|
+
footnoteRefIdByNode: WeakMap<FootnoteReference, string>;
|
|
32
|
+
/** 流式模式下,当前这一帧顶层块列表里的最后一个节点——`codeBlockOverrides`
|
|
33
|
+
* 那类接管整块渲染的覆盖组件(比如 mermaid 图表)不能对"还可能是最后一
|
|
34
|
+
* 块、围栏还没写完"的代码块生效:围栏没闭合时这段代码的内容每一帧都在
|
|
35
|
+
* 变,如果立刻挂载覆盖组件,它会对着一堆语法上通常无效的半截内容反复
|
|
36
|
+
* 触发一次昂贵的渲染/解析,界面观感是卡顿甚至短暂报错。只有确认后面已
|
|
37
|
+
* 经跟上了别的内容(围栏必然已经闭合,不然 CommonMark 不会开始下一个
|
|
38
|
+
* 块),或者已经不在流式模式,才让覆盖组件接管。 */
|
|
39
|
+
lastTopLevelNode: unknown;
|
|
40
|
+
isStreaming: boolean;
|
|
41
|
+
/** 引用式链接/图片(`[text][ref]` + `[ref]: url`)的定义表——mdast 只有
|
|
42
|
+
* 定义真的存在时才会把引用节点解析成 linkReference/imageReference(没
|
|
43
|
+
* 匹配上定义的引用,解析阶段就已经退化成字面量文本节点,不会走到这里),
|
|
44
|
+
* 所以渲染时按 identifier 查这张表理论上总能查到;查不到时降级成只渲染
|
|
45
|
+
* children(对链接)或者不渲染(对图片),不让整段内容凭空消失。 */
|
|
46
|
+
definitions: Map<string, Definition>;
|
|
47
|
+
}
|
|
48
|
+
/** 递归收集整棵树里的 `definition` 节点——引用式链接/图片的定义可以出现在
|
|
49
|
+
* 文档任意位置(包括引用点之前或之后、嵌套在 blockquote/list 内部),
|
|
50
|
+
* 渲染 linkReference/imageReference 之前必须先有这张完整的表,不能等
|
|
51
|
+
* 渲染到 definition 节点那一刻才收集(那时引用点可能已经渲染过了)。 */
|
|
52
|
+
export declare function collectDefinitions(root: Root): Map<string, Definition>;
|
|
53
|
+
export interface FootnoteMetadata {
|
|
54
|
+
footnoteDefinitions: Map<string, FootnoteDefinition>;
|
|
55
|
+
footnoteOrder: Map<string, number>;
|
|
56
|
+
footnoteRefIds: Map<string, string[]>;
|
|
57
|
+
footnoteRefIdByNode: WeakMap<FootnoteReference, string>;
|
|
58
|
+
}
|
|
59
|
+
/** 独立于渲染遍历、单独对完整树做一次收集——脚注编号/引用点 id/定义内容
|
|
60
|
+
* 原来是 `renderNode` 遍历到 `footnoteReference`/`footnoteDefinition`
|
|
61
|
+
* 节点时顺手填的副作用,但 `renderMdastRoot` 的按块缓存命中时会完全
|
|
62
|
+
* 跳过 `renderNode`:一段脚注引用/定义所在的块一旦被冻结,后续帧里这
|
|
63
|
+
* 几张表就再也不会被重新填充,脚注区可能因此整体消失,或者冻结边界前后
|
|
64
|
+
* 混进两套不完整的编号,跟真实点开的顺序对不上。做法上跟 `collectDefinitions`
|
|
65
|
+
* 一致:对当前这一帧完整的树做一次独立遍历,不依赖某个节点是不是真的
|
|
66
|
+
* 走过了渲染。 */
|
|
67
|
+
export declare function collectFootnoteMetadata(root: Root): FootnoteMetadata;
|
|
68
|
+
export declare function renderFootnotesSection(context: RenderContext): ReactNode;
|
|
69
|
+
/**
|
|
70
|
+
* 顶层块级渲染,带按块缓存——增量解析冻结的块在多次渲染之间保持同一个
|
|
71
|
+
* 对象引用(见 `incremental.ts`),这里以对象引用做 key 缓存渲染结果,
|
|
72
|
+
* 冻结块跨帧直接复用同一个 React 元素对象,React 的 fiber 协调在
|
|
73
|
+
* props 引用相同时会跳过整棵子树的重新渲染——只解决了解析层的 O(N²)
|
|
74
|
+
* 还不够,渲染层每帧都重新 build 一遍全部元素同样是长文档卡顿的来源。
|
|
75
|
+
*
|
|
76
|
+
* 只在顶层(`root.children`)做这一层缓存:块一旦冻结就是原子不可变单元,
|
|
77
|
+
* 跟增量解析"冻结前缀"的粒度对齐;嵌套在块内部的子节点不需要单独缓存。
|
|
78
|
+
*/
|
|
79
|
+
export declare function renderMdastRoot(root: Root, context: RenderContext, blockCache?: WeakMap<object, ReactNode>, groupIdentityMap?: WeakMap<object, object>): ReactNode[];
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 安全设计分两层,两层都要过(对齐需求 4):
|
|
3
|
+
*
|
|
4
|
+
* 1. 标签/属性白名单——只有声明过的标签名和对应属性才会被当作"结构化标签"
|
|
5
|
+
* 处理,其余原始 HTML 一律转义成文本。这一层解决"允许哪些自定义标签"。
|
|
6
|
+
* 2. 协议黑名单(不受第 1 层白名单影响)——链接 href、图片 src 在渲染前
|
|
7
|
+
* 统一检查协议,危险协议一律拒绝渲染成可交互元素。这一步在所有节点
|
|
8
|
+
* 类型的链接/图片渲染路径上统一收口成这一个函数调用,不分散在各处
|
|
9
|
+
* 判断,避免以后新增节点类型时漏掉。
|
|
10
|
+
*/
|
|
11
|
+
export type AllowedTags = Record<string, string[]>;
|
|
12
|
+
/** 按标签名/属性名查一张"名字 -> 值"的表时,不能用 `obj[key]`/`key in obj`
|
|
13
|
+
* 直接查——markdown 内容不可信,标签名可以是任意字符串,`constructor`/
|
|
14
|
+
* `toString`/`hasOwnProperty` 这类名字会命中原型链继承来的属性而不是
|
|
15
|
+
* "没有声明",把一个函数/别的东西当成配置值用下去会直接崩溃整条消息的
|
|
16
|
+
* 渲染。所有按标签名/组件名查白名单、查覆盖组件的地方都要经过这个函数。 */
|
|
17
|
+
export declare function ownLookup<T>(table: Record<string, T> | undefined, key: string): T | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* 默认放行的标准 HTML 标签——AI 生成内容里常见的、语义上安全的行内标签,
|
|
20
|
+
* 不需要调用方额外声明就可以直接渲染。调用方通过 `allowedTags` 声明的
|
|
21
|
+
* 自定义标签(比如业务里的 `system-reminder`)在这个默认集合之上叠加,
|
|
22
|
+
* 不覆盖。
|
|
23
|
+
*/
|
|
24
|
+
export declare const DEFAULT_ALLOWED_TAGS: AllowedTags;
|
|
25
|
+
/** 按标签名逐个合并属性列表,不是整表浅拼接——调用方对某个标签重新声明
|
|
26
|
+
* 属性列表(比如只是想给 `details` 多加一个属性)不应该把内置的那份
|
|
27
|
+
* 属性列表整个挤掉(`{...DEFAULT_ALLOWED_TAGS, ...custom}` 会把 `details`
|
|
28
|
+
* 内置的 `open` 属性丢掉),这跟"自定义声明是叠加在内置白名单之上"的
|
|
29
|
+
* 文档承诺矛盾。 */
|
|
30
|
+
export declare function mergeAllowedTags(custom: AllowedTags | undefined): AllowedTags;
|
|
31
|
+
/**
|
|
32
|
+
* 协议黑名单:`javascript:`(含大小写、含空白/控制字符干扰的变体)一律
|
|
33
|
+
* 拒绝。不在名单里的协议(http/https/mailto/tel/相对路径等)一律放行——
|
|
34
|
+
* 调用方如果需要更严格的白名单式协议控制,用 `linkSafety` 扩展点自己叠加,
|
|
35
|
+
* 这一层只兜底"绝对不能允许"的场景。
|
|
36
|
+
*/
|
|
37
|
+
export declare function isDangerousUrl(url: string): boolean;
|
|
38
|
+
/** 对链接 href/图片 src 做协议检查;危险协议返回 `null`(调用方据此去掉该属性或整体降级成纯文本)。 */
|
|
39
|
+
export declare function sanitizeUrl(url: string | undefined | null): string | undefined;
|
|
40
|
+
export interface ParsedTag {
|
|
41
|
+
tagName: string;
|
|
42
|
+
isClosing: boolean;
|
|
43
|
+
isSelfClosing: boolean;
|
|
44
|
+
attributes: Record<string, string>;
|
|
45
|
+
}
|
|
46
|
+
export declare function parseSingleTag(raw: string): ParsedTag | null;
|
|
47
|
+
export declare function normalizeBooleanAttributes(attributes: Record<string, string>): Record<string, unknown>;
|
|
48
|
+
/** 按标签的属性白名单过滤,同时对 `href`/`src` 额外做协议检查——两层
|
|
49
|
+
* 检查在这一个函数里收口,不分散在各处判断。 */
|
|
50
|
+
export declare function filterAttributes(tagName: string, attributes: Record<string, string>, allowedTags: AllowedTags): Record<string, string>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ComponentType, JSX, ReactNode } from "react";
|
|
2
|
+
import type { AllowedTags } from "./sanitize";
|
|
3
|
+
export type { AllowedTags };
|
|
4
|
+
/** 标签名(标准 HTML 或者 `allowedTags` 声明的自定义标签)-> React 组件。
|
|
5
|
+
* 形态对齐 streamdown 的 `components` prop,降低迁移改动量——标准标签名
|
|
6
|
+
* 拿到对应的 intrinsic props 类型(比如 `pre` 覆盖组件的 `children` 能
|
|
7
|
+
* 正确推断成 `ReactNode`),自定义标签名落到通用的 Record<string, unknown>。 */
|
|
8
|
+
export type MarkdownComponents = {
|
|
9
|
+
[Key in keyof JSX.IntrinsicElements]?: ComponentType<JSX.IntrinsicElements[Key]>;
|
|
10
|
+
} & {
|
|
11
|
+
[tagName: string]: ComponentType<Record<string, unknown>> | undefined;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* 原始 HTML 后处理钩子:解析出标签名和属性之后、渲染成 React 元素之前
|
|
15
|
+
* 调用,可以按需改写属性(比如把 kebab-case 属性名双写成 camelCase)。
|
|
16
|
+
* 返回值替换原有属性;不提供时原样使用 sanitize 之后的属性。
|
|
17
|
+
*/
|
|
18
|
+
export type RawHtmlPlugin = (tagName: string, attributes: Record<string, string>) => Record<string, string>;
|
|
19
|
+
/** 对齐 streamdown 的 `linkSafety`——目前仓库内唯一用法是 `{ enabled: false }`
|
|
20
|
+
* 显式关闭,没有消费方真的启用过跳转确认弹窗,这里只保留类型形状,
|
|
21
|
+
* `enabled: true` 时暂不渲染确认弹窗(等真的有消费方需要再补)。 */
|
|
22
|
+
export interface LinkSafetyConfig {
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
onLinkCheck?: (url: string) => Promise<boolean> | boolean;
|
|
25
|
+
renderModal?: (props: {
|
|
26
|
+
isOpen: boolean;
|
|
27
|
+
onClose: () => void;
|
|
28
|
+
onConfirm: () => void;
|
|
29
|
+
url: string;
|
|
30
|
+
}) => ReactNode;
|
|
31
|
+
}
|
|
32
|
+
/** 按围栏代码块的语言标注整体替换渲染(不是 `components.code` 那种"改写
|
|
33
|
+
* `<code>` 内部",而是连 `<pre>` 外壳都不要,整个块换成调用方自己的组件)。
|
|
34
|
+
* 给 mermaid 这类需要跳出代码块外壳、直接渲染图表/画布的场景用;语言不在
|
|
35
|
+
* 这张表里的代码块走正常的 `<pre><code class="language-xxx">` 路径。 */
|
|
36
|
+
export type CodeBlockOverrides = Record<string, ComponentType<{
|
|
37
|
+
code: string;
|
|
38
|
+
}>>;
|
|
39
|
+
export interface MarkdownRendererProps {
|
|
40
|
+
children: string;
|
|
41
|
+
className?: string;
|
|
42
|
+
/** 流式渲染传 "streaming"(打字机式增量解析 + 未闭合语法降级生效),
|
|
43
|
+
* 历史静态内容传 "static"(整篇一次性解析,不做增量缓存)。 */
|
|
44
|
+
mode?: "streaming" | "static";
|
|
45
|
+
/** 会话 id;透传给需要区分会话的下游组件(会话内图片路径解析等)。 */
|
|
46
|
+
sessionId?: string;
|
|
47
|
+
components?: MarkdownComponents;
|
|
48
|
+
/** 自定义标签的白名单声明,跟内置的标准安全标签集合叠加,不覆盖。 */
|
|
49
|
+
allowedTags?: AllowedTags;
|
|
50
|
+
rawHtmlPlugins?: RawHtmlPlugin[];
|
|
51
|
+
linkSafety?: LinkSafetyConfig;
|
|
52
|
+
codeBlockOverrides?: CodeBlockOverrides;
|
|
53
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -42,8 +42,10 @@ export { LlmChat } from "./components/LlmChat";
|
|
|
42
42
|
export type { LlmChatHandle, LlmChatProps } from "./components/LlmChat";
|
|
43
43
|
export type { LlmAdvancedSettings, LlmOverride } from "./components/LlmAdvancedSettings";
|
|
44
44
|
export { MarkdownContent } from "./components/MarkdownContent";
|
|
45
|
-
export { normalizeAdjacentUrlFormatting } from "./components/MarkdownContent";
|
|
45
|
+
export { ExternalAnchor, normalizeAdjacentUrlFormatting, stripSystemReminders } from "./components/MarkdownContent";
|
|
46
46
|
export type { MarkdownContentProps } from "./components/MarkdownContent";
|
|
47
|
+
export { DEFAULT_ALLOWED_TAGS, MarkdownRenderer } from "./components/markdown";
|
|
48
|
+
export type { AllowedTags, CodeBlockOverrides, LinkSafetyConfig, MarkdownComponents, MarkdownRendererProps, RawHtmlPlugin, } from "./components/markdown";
|
|
47
49
|
export { WhatIfUserBubble } from "./components/WhatIfUserBubble";
|
|
48
50
|
export type { WhatIfUserBubbleProps } from "./components/WhatIfUserBubble";
|
|
49
51
|
export { parseWhatIfPrompt } from "./lib/whatif-prompt";
|