@alicloud/appflow-chat 0.0.1-beta.1

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.
Files changed (40) hide show
  1. package/README-ZH.md +188 -0
  2. package/README.md +190 -0
  3. package/dist/appflow-chat.cjs.js +1903 -0
  4. package/dist/appflow-chat.esm.js +36965 -0
  5. package/dist/types/index.d.ts +862 -0
  6. package/package.json +87 -0
  7. package/src/components/DocReferences.tsx +64 -0
  8. package/src/components/HumanVerify/CustomParamsRenderer/ArrayField.tsx +394 -0
  9. package/src/components/HumanVerify/CustomParamsRenderer/FieldRenderer.tsx +202 -0
  10. package/src/components/HumanVerify/CustomParamsRenderer/ObjectField.tsx +126 -0
  11. package/src/components/HumanVerify/CustomParamsRenderer/index.tsx +166 -0
  12. package/src/components/HumanVerify/CustomParamsRenderer/types.ts +203 -0
  13. package/src/components/HumanVerify/HistoryCard.tsx +156 -0
  14. package/src/components/HumanVerify/HumanVerify.tsx +184 -0
  15. package/src/components/HumanVerify/index.ts +11 -0
  16. package/src/components/MarkdownRenderer.tsx +195 -0
  17. package/src/components/MessageBubble.tsx +400 -0
  18. package/src/components/RichMessageBubble.tsx +283 -0
  19. package/src/components/WebSearchPanel.tsx +68 -0
  20. package/src/context/RichBubble.tsx +21 -0
  21. package/src/core/BubbleContent.tsx +75 -0
  22. package/src/core/RichBubbleContent.tsx +324 -0
  23. package/src/core/SourceContent.tsx +285 -0
  24. package/src/core/WebSearchContent.tsx +219 -0
  25. package/src/core/index.ts +16 -0
  26. package/src/hooks/usePreSignUpload.ts +36 -0
  27. package/src/index.ts +80 -0
  28. package/src/markdown/components/Chart.tsx +120 -0
  29. package/src/markdown/components/Error.tsx +39 -0
  30. package/src/markdown/components/FileDisplay.tsx +246 -0
  31. package/src/markdown/components/Loading.tsx +41 -0
  32. package/src/markdown/components/SyntaxHighlight.tsx +182 -0
  33. package/src/markdown/index.tsx +250 -0
  34. package/src/markdown/styled.ts +234 -0
  35. package/src/markdown/utils/dataProcessor.ts +89 -0
  36. package/src/services/ChatService.ts +926 -0
  37. package/src/utils/fetchEventSource.ts +65 -0
  38. package/src/utils/loadEcharts.ts +32 -0
  39. package/src/utils/loadPrism.ts +156 -0
  40. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,285 @@
1
+ /**
2
+ * SourceContent - 参考资料核心展示组件
3
+ *
4
+ * 支持的内容类型:
5
+ * - rag: 知识库参考资料
6
+ * - web_search: 网页搜索结果
7
+ */
8
+
9
+ import React, { useMemo } from 'react';
10
+ import styled from 'styled-components';
11
+ import { Image } from 'antd';
12
+ import { SearchOutlined } from '@ant-design/icons';
13
+ import { flatten, uniq } from 'lodash-es';
14
+
15
+ // 参考资料项类型
16
+ export interface SourceItem {
17
+ /** 标题 */
18
+ title?: string;
19
+ Title?: string;
20
+ /** 内容文本 */
21
+ text?: string;
22
+ Text?: string;
23
+ /** 索引序号 */
24
+ index?: string;
25
+ Index?: string;
26
+ /** 类型:rag-知识库, web_search-网页搜索 */
27
+ type?: 'rag' | 'web_search' | string;
28
+ Type?: string;
29
+ /** 图片列表 */
30
+ images?: string[];
31
+ Images?: string[];
32
+ /** 链接URL(网页搜索结果) */
33
+ url?: string;
34
+ Url?: string;
35
+ /** 文档ID */
36
+ doc_id?: string;
37
+ doc_name?: string;
38
+ index_id?: string;
39
+ }
40
+
41
+ export interface SourceContentProps {
42
+ /** 参考资料列表 */
43
+ items: SourceItem[];
44
+ /** 渲染状态 */
45
+ status?: 'Running' | 'Success' | 'Error' | string;
46
+ /** 是否使用大写字段名(PageConfig模式) */
47
+ isPageConfig?: boolean;
48
+ /** 点击参考资料回调 */
49
+ onItemClick?: (item: SourceItem) => void;
50
+ /** 点击网页搜索结果回调 */
51
+ onWebSearchClick?: (items: SourceItem[]) => void;
52
+ /** 自定义类名 */
53
+ className?: string;
54
+ /** 自定义样式 */
55
+ style?: React.CSSProperties;
56
+ }
57
+
58
+ // 样式组件
59
+ const StyledSource = styled.div`
60
+ margin-top: 16px;
61
+ border-top: 1px solid #e8e8e8;
62
+ padding-top: 16px;
63
+ `;
64
+
65
+ const StyledAnswerSource = styled.div`
66
+ display: flex;
67
+ gap: 8px;
68
+ margin-bottom: 8px;
69
+ align-items: baseline;
70
+ `;
71
+
72
+ const StyledSourceItem = styled.div`
73
+ margin-bottom: 8px;
74
+ cursor: pointer;
75
+ width: 100%;
76
+ `;
77
+
78
+ const StyledSourceItemSpace = styled.div`
79
+ display: flex;
80
+ gap: 8px;
81
+ align-items: center;
82
+ justify-content: flex-start;
83
+ `;
84
+
85
+ const StyledItemOrder = styled.div`
86
+ align-items: center;
87
+ border: 0.5px solid #2c2c73;
88
+ border-radius: 2px;
89
+ display: flex;
90
+ font-size: 10px;
91
+ height: 12px;
92
+ justify-content: center;
93
+ line-height: 1;
94
+ min-width: 14px;
95
+ white-space: nowrap;
96
+ `;
97
+
98
+ const StyledItemTitle = styled.div`
99
+ border: 0.5px solid #9296a9;
100
+ border-radius: 4px;
101
+ color: #707279;
102
+ font-size: 12px;
103
+ height: 28px;
104
+ line-height: 28px;
105
+ margin: 0;
106
+ padding: 0 8px;
107
+ width: auto;
108
+ overflow: hidden;
109
+ text-overflow: ellipsis;
110
+ white-space: nowrap;
111
+ `;
112
+
113
+ const StyledWrapSpace = styled.div`
114
+ display: flex;
115
+ flex-wrap: wrap;
116
+ gap: 8px;
117
+ `;
118
+
119
+ const StyledImageBox = styled.div`
120
+ border: 1px solid #e8e8e8;
121
+ border-radius: 4px;
122
+ padding: 4px;
123
+ `;
124
+
125
+ const StyledWebSearchSource = styled.div`
126
+ display: flex;
127
+ gap: 8px;
128
+ margin-bottom: 8px;
129
+ align-items: center;
130
+ background: rgb(230, 232, 236);
131
+ border-radius: 10px;
132
+ width: fit-content;
133
+ padding: 7px 14px;
134
+ font-size: 12px;
135
+ cursor: pointer;
136
+
137
+ &:hover {
138
+ background: rgb(220, 222, 226);
139
+ }
140
+ `;
141
+
142
+ const StyledLabel = styled.div`
143
+ color: #707279;
144
+ font-size: 12px;
145
+ min-width: 60px;
146
+ `;
147
+
148
+ // 单个参考资料项组件
149
+ interface SourceItemComponentProps {
150
+ item: SourceItem;
151
+ isPageConfig?: boolean;
152
+ onClick: (item: SourceItem) => void;
153
+ }
154
+
155
+ const SourceItemComponent: React.FC<SourceItemComponentProps> = ({
156
+ item,
157
+ isPageConfig,
158
+ onClick
159
+ }) => {
160
+ const index = isPageConfig ? item.Index : item.index;
161
+ const title = isPageConfig ? item.Title : item.title;
162
+
163
+ return (
164
+ <StyledSourceItem onClick={() => onClick(item)}>
165
+ <StyledSourceItemSpace>
166
+ {index && <StyledItemOrder>{index}</StyledItemOrder>}
167
+ {title && <StyledItemTitle>{title}</StyledItemTitle>}
168
+ </StyledSourceItemSpace>
169
+ </StyledSourceItem>
170
+ );
171
+ };
172
+
173
+ /**
174
+ * SourceContent - 参考资料核心展示组件
175
+ *
176
+ * @example
177
+ * ```tsx
178
+ * <SourceContent
179
+ * items={[
180
+ * { title: '参考文档1', text: '内容...', index: '1', type: 'rag' },
181
+ * { title: '搜索结果1', url: 'https://...', type: 'web_search' }
182
+ * ]}
183
+ * status="Success"
184
+ * onItemClick={(item) => console.log('点击了', item)}
185
+ * onWebSearchClick={(items) => console.log('网页搜索', items)}
186
+ * />
187
+ * ```
188
+ */
189
+ export const SourceContent: React.FC<SourceContentProps> = ({
190
+ items,
191
+ status = 'Success',
192
+ isPageConfig = false,
193
+ onItemClick,
194
+ onWebSearchClick,
195
+ className,
196
+ style,
197
+ }) => {
198
+ // 提取唯一图片
199
+ const uniqueImages = useMemo(() => {
200
+ let images;
201
+ if (isPageConfig) {
202
+ images = items?.filter(item => item.Images)?.map(item => item.Images);
203
+ } else {
204
+ images = items?.filter(item => item.images)?.map(item => item.images);
205
+ }
206
+ return uniq(flatten(images));
207
+ }, [items, isPageConfig]);
208
+
209
+ // RAG类型的参考资料
210
+ const ragArray = useMemo(() => {
211
+ if (isPageConfig) {
212
+ return items?.filter(item => item.Type === 'rag') || [];
213
+ } else {
214
+ return items?.filter(item => item.type === 'rag') || [];
215
+ }
216
+ }, [items, isPageConfig]);
217
+
218
+ // 网页搜索结果
219
+ const webSearchArray = useMemo(() => {
220
+ if (isPageConfig) {
221
+ return items?.filter(item => item.Type === 'web_search') || [];
222
+ } else {
223
+ return items?.filter(item => item.type === 'web_search') || [];
224
+ }
225
+ }, [items, isPageConfig]);
226
+
227
+ // 点击处理
228
+ const handleItemClick = (item: SourceItem) => {
229
+ onItemClick?.(item);
230
+ };
231
+
232
+ const handleWebSearchClick = () => {
233
+ onWebSearchClick?.(webSearchArray);
234
+ };
235
+
236
+ // 只在成功状态下显示
237
+ if (status !== 'Success') {
238
+ return null;
239
+ }
240
+
241
+ return (
242
+ <StyledSource className={className} style={style}>
243
+ {/* 网页搜索结果 */}
244
+ {webSearchArray.length > 0 && (
245
+ <StyledWebSearchSource onClick={handleWebSearchClick}>
246
+ <SearchOutlined />
247
+ <div>已搜索到{webSearchArray.length}个网页</div>
248
+ </StyledWebSearchSource>
249
+ )}
250
+
251
+ {/* RAG参考资料 */}
252
+ {ragArray.length > 0 && (
253
+ <StyledAnswerSource>
254
+ <StyledLabel>回答来源:</StyledLabel>
255
+ <div style={{ width: 'calc(100% - 70px)' }}>
256
+ {ragArray.map((item, index) => (
257
+ <SourceItemComponent
258
+ key={index}
259
+ item={item}
260
+ isPageConfig={isPageConfig}
261
+ onClick={handleItemClick}
262
+ />
263
+ ))}
264
+ </div>
265
+ </StyledAnswerSource>
266
+ )}
267
+
268
+ {/* 图片来源 */}
269
+ {uniqueImages.length > 0 && (
270
+ <StyledAnswerSource>
271
+ <StyledLabel>图片来源:</StyledLabel>
272
+ <StyledWrapSpace>
273
+ {uniqueImages.map((image, index) => (
274
+ <StyledImageBox key={index}>
275
+ <Image src={image} width={50} height={50} />
276
+ </StyledImageBox>
277
+ ))}
278
+ </StyledWrapSpace>
279
+ </StyledAnswerSource>
280
+ )}
281
+ </StyledSource>
282
+ );
283
+ };
284
+
285
+ export default SourceContent;
@@ -0,0 +1,219 @@
1
+ /**
2
+ * WebSearchContent - 网页搜索结果核心展示组件
3
+ */
4
+
5
+ import React from 'react';
6
+ import styled from 'styled-components';
7
+ import { Drawer, List, Typography } from 'antd';
8
+ import { CloseOutlined } from '@ant-design/icons';
9
+
10
+ const { Title } = Typography;
11
+
12
+ // 网页搜索结果项类型
13
+ export interface WebSearchItem {
14
+ /** 标题 */
15
+ title?: string;
16
+ Title?: string;
17
+ /** 内容摘要 */
18
+ text?: string;
19
+ Text?: string;
20
+ /** 链接URL */
21
+ url?: string;
22
+ Url?: string;
23
+ }
24
+
25
+ export interface WebSearchContentProps {
26
+ /** 搜索结果列表 */
27
+ items?: WebSearchItem[];
28
+ /** 是否显示 */
29
+ open?: boolean;
30
+ /** 关闭回调 */
31
+ onClose?: () => void;
32
+ /** 是否使用大写字段名(PageConfig模式) */
33
+ isPageConfig?: boolean;
34
+ /** 面板宽度 */
35
+ width?: number | string;
36
+ /** 挂载容器 */
37
+ getContainer?: () => HTMLElement;
38
+ /** 自定义类名 */
39
+ className?: string;
40
+ /** 自定义样式 */
41
+ style?: React.CSSProperties;
42
+ }
43
+
44
+ // 样式组件
45
+ const StyledContainer = styled.div<{ $isMobile: boolean }>`
46
+ height: 100%;
47
+ display: flex;
48
+ flex-direction: column;
49
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
50
+ ${props => props.$isMobile && `
51
+ padding: 16px;
52
+ `}
53
+ `;
54
+
55
+ const StyledContent = styled.div`
56
+ flex: 1;
57
+ overflow-y: auto;
58
+ `;
59
+
60
+ const StyledHeader = styled.div`
61
+ display: flex;
62
+ justify-content: space-between;
63
+ align-items: baseline;
64
+ padding-bottom: 16px;
65
+ border-bottom: 1px solid #f0f0f0;
66
+ margin-bottom: 16px;
67
+ `;
68
+
69
+ const StyledCloseButton = styled.div`
70
+ cursor: pointer;
71
+ padding: 4px 8px;
72
+ border-radius: 4px;
73
+ color: #666;
74
+ font-size: 14px;
75
+
76
+ &:hover {
77
+ background: #f5f5f5;
78
+ color: #333;
79
+ }
80
+ `;
81
+
82
+ const StyledMeta = styled.div`
83
+ display: flex;
84
+ align-items: flex-start;
85
+ gap: 8px;
86
+ `;
87
+
88
+ const StyledMetaIndex = styled.span`
89
+ font-weight: 600;
90
+ color: #333;
91
+ min-width: 20px;
92
+ `;
93
+
94
+ const StyledMetaTitle = styled.div`
95
+ color: #1890ff;
96
+ cursor: pointer;
97
+ flex: 1;
98
+
99
+ &:hover {
100
+ text-decoration: underline;
101
+ }
102
+ `;
103
+
104
+ const StyledMetaText = styled.div`
105
+ color: #666;
106
+ font-size: 13px;
107
+ cursor: pointer;
108
+ margin-top: 4px;
109
+
110
+ &:hover {
111
+ color: #333;
112
+ }
113
+ `;
114
+
115
+ /**
116
+ * WebSearchContent - 网页搜索结果核心展示组件
117
+ *
118
+ * @example
119
+ * ```tsx
120
+ * <WebSearchContent
121
+ * items={[
122
+ * { title: '搜索结果1', text: '内容摘要...', url: 'https://...' }
123
+ * ]}
124
+ * open={true}
125
+ * onClose={() => setOpen(false)}
126
+ * />
127
+ * ```
128
+ */
129
+ export const WebSearchContent: React.FC<WebSearchContentProps> = ({
130
+ items = [],
131
+ open = false,
132
+ onClose,
133
+ isPageConfig = false,
134
+ width = 400,
135
+ getContainer,
136
+ className,
137
+ style,
138
+ }) => {
139
+ // 判断是否移动端
140
+ const isMobile = typeof window !== 'undefined' && window.innerWidth <= 500;
141
+ const panelWidth = isMobile ? '100%' : width;
142
+
143
+ // 打开链接
144
+ const handleOpenUrl = (item: WebSearchItem) => {
145
+ const url = isPageConfig ? item.Url : item.url;
146
+ if (url) {
147
+ window.open(url, '_blank');
148
+ }
149
+ };
150
+
151
+ // 关闭处理
152
+ const handleClose = () => {
153
+ onClose?.();
154
+ };
155
+
156
+ return (
157
+ <Drawer
158
+ className={className}
159
+ style={style}
160
+ placement="right"
161
+ open={open}
162
+ mask={false}
163
+ width={panelWidth}
164
+ destroyOnClose
165
+ closable={false}
166
+ bodyStyle={{
167
+ boxShadow: 'none',
168
+ outline: 'none',
169
+ padding: '16px',
170
+ }}
171
+ getContainer={getContainer}
172
+ onClose={handleClose}
173
+ >
174
+ <StyledContainer $isMobile={isMobile}>
175
+ <StyledContent>
176
+ <StyledHeader>
177
+ <Title level={5} style={{ margin: 0 }}>搜索结果</Title>
178
+ <StyledCloseButton onClick={handleClose}>
179
+ <CloseOutlined />
180
+ </StyledCloseButton>
181
+ </StyledHeader>
182
+
183
+ <List
184
+ itemLayout="horizontal"
185
+ dataSource={items}
186
+ renderItem={(item, index) => {
187
+ const title = isPageConfig ? item.Title : item.title;
188
+ const text = isPageConfig ? item.Text : item.text;
189
+
190
+ return (
191
+ <List.Item>
192
+ <List.Item.Meta
193
+ title={
194
+ <StyledMeta>
195
+ <StyledMetaIndex>{index + 1}.</StyledMetaIndex>
196
+ <StyledMetaTitle
197
+ onClick={() => handleOpenUrl(item)}
198
+ dangerouslySetInnerHTML={{ __html: title || '' }}
199
+ />
200
+ </StyledMeta>
201
+ }
202
+ description={
203
+ <StyledMetaText
204
+ onClick={() => handleOpenUrl(item)}
205
+ dangerouslySetInnerHTML={{ __html: text || '' }}
206
+ />
207
+ }
208
+ />
209
+ </List.Item>
210
+ );
211
+ }}
212
+ />
213
+ </StyledContent>
214
+ </StyledContainer>
215
+ </Drawer>
216
+ );
217
+ };
218
+
219
+ export default WebSearchContent;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Core 组件导出
3
+ * 这些是纯展示组件,不包含业务逻辑
4
+ */
5
+
6
+ export { BubbleContent } from './BubbleContent';
7
+ export type { BubbleContentProps } from './BubbleContent';
8
+
9
+ export { RichBubbleContent } from './RichBubbleContent';
10
+ export type { RichBubbleContentProps } from './RichBubbleContent';
11
+
12
+ export { SourceContent } from './SourceContent';
13
+ export type { SourceContentProps, SourceItem } from './SourceContent';
14
+
15
+ export { WebSearchContent } from './WebSearchContent';
16
+ export type { WebSearchContentProps, WebSearchItem } from './WebSearchContent';
@@ -0,0 +1,36 @@
1
+ import axios from "axios";
2
+
3
+ export interface FetchUploadResponseData {
4
+ /** 文件全路径链接 */
5
+ url?: string;
6
+ /** 文件 oss key */
7
+ key?: string;
8
+ /** oss bucket name */
9
+ bucketName?: string;
10
+ }
11
+
12
+ /**
13
+ * 上传数据到指定bucket
14
+ * @param file 文件
15
+ * @param signedUrl 预签名URL
16
+ * @returns
17
+ */
18
+ export const fetchUploadApi = (
19
+ file: Blob,
20
+ signedUrl: string,
21
+ ): Promise<FetchUploadResponseData> => {
22
+ return new Promise<FetchUploadResponseData>((resolve, reject) => {
23
+ // 发送 PUT 请求上传文件
24
+ axios.put(signedUrl, file, {
25
+ headers: {
26
+ 'Content-Type': 'application/octet-stream',
27
+ 'x-oss-object-acl': 'private',
28
+ },
29
+ }).then(() => {
30
+ // 文件上传成功 请求体为空 暂定判断 ok
31
+ resolve({});
32
+ }).catch(error => reject(error));
33
+ });
34
+ };
35
+
36
+ export default fetchUploadApi;
package/src/index.ts ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Appflow Chat - NPM 包入口文件
3
+ *
4
+ * 使用方式:
5
+ * import { chatService, MarkdownRenderer, BubbleContent } from '@ali/appflow-chat';
6
+ */
7
+
8
+ // ==================== 服务导出 ====================
9
+ export { chatService } from './services/ChatService';
10
+ export { default as ChatService } from './services/ChatService';
11
+
12
+ // ==================== 服务类型导出 ====================
13
+ export type {
14
+ SetupConfig,
15
+ ChatConfig,
16
+ ModelInfo,
17
+ ModelCapabilities,
18
+ ChatMessage,
19
+ ChatStreamCallbacks,
20
+ ChatStream,
21
+ HistoryMessage,
22
+ ChatSession,
23
+ } from './services/ChatService';
24
+
25
+ // ==================== UI 组件导出(简化接口,包含默认交互) ====================
26
+ export { MarkdownRenderer } from './components/MarkdownRenderer';
27
+ export { MessageBubble } from './components/MessageBubble';
28
+ export { RichMessageBubble } from './components/RichMessageBubble';
29
+ export { DocReferences } from './components/DocReferences';
30
+ export { WebSearchPanel } from './components/WebSearchPanel';
31
+
32
+ // ==================== UI 组件类型导出 ====================
33
+ export type { MarkdownRendererProps } from './components/MarkdownRenderer';
34
+ export type {
35
+ MessageBubbleProps,
36
+ HumanVerifySubmitData,
37
+ HumanVerifyData,
38
+ HistoryCardData
39
+ } from './components/MessageBubble';
40
+ export type { RichMessageBubbleProps } from './components/RichMessageBubble';
41
+ export type { DocReferencesProps, DocReferenceItem } from './components/DocReferences';
42
+ export type { WebSearchPanelProps, WebSearchItem } from './components/WebSearchPanel';
43
+
44
+ // ==================== Core 组件导出(纯展示组件,供高级定制) ====================
45
+ export { BubbleContent } from './core/BubbleContent';
46
+ export { RichBubbleContent } from './core/RichBubbleContent';
47
+ export { SourceContent } from './core/SourceContent';
48
+ export { WebSearchContent } from './core/WebSearchContent';
49
+
50
+ // ==================== Core 组件类型导出 ====================
51
+ export type { BubbleContentProps } from './core/BubbleContent';
52
+ export type { RichBubbleContentProps } from './core/RichBubbleContent';
53
+ export type { SourceContentProps, SourceItem } from './core/SourceContent';
54
+ export type { WebSearchContentProps } from './core/WebSearchContent';
55
+
56
+ // ==================== Context 导出 ====================
57
+ export { useRichBubbleContext, RichBubbleProvider } from './context/RichBubble';
58
+ export type { RichBubbleContextValue } from './context/RichBubble';
59
+
60
+ // ==================== Markdown 组件导出 ====================
61
+ export { MarkdownView } from './markdown';
62
+ export type { MarkdownViewProps } from './markdown';
63
+
64
+ // ==================== HumanVerify 组件导出 ====================
65
+ export { HumanVerify } from './components/HumanVerify';
66
+ export { HistoryCard, convertSchemaToUpperCase } from './components/HumanVerify';
67
+ export { CustomParamsRenderer, useCustomParamsRenderer, validateCustomParams } from './components/HumanVerify';
68
+
69
+ // ==================== HumanVerify 组件类型导出 ====================
70
+ export type { HumanVerifyProps } from './components/HumanVerify';
71
+ export type { HistoryCardProps } from './components/HumanVerify';
72
+ export type {
73
+ CustomParamSchema,
74
+ CustomParamsRendererProps,
75
+ ValidationResult,
76
+ ValidationError,
77
+ } from './components/HumanVerify';
78
+
79
+ // ==================== 工具函数导出 ====================
80
+ export { loadEchartsScript } from './utils/loadEcharts';