@fe-free/ai 5.0.0 → 6.0.2

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.
@@ -0,0 +1,166 @@
1
+ import { useMemo } from 'react';
2
+ import { create } from 'zustand';
3
+ import type { ChatMessage } from './types';
4
+
5
+ interface BaseSenderValue {
6
+ text?: string;
7
+ files?: string[];
8
+ }
9
+ interface ChatStore<
10
+ UserData extends BaseSenderValue,
11
+ AIData,
12
+ ContextData extends Record<string, any>,
13
+ > {
14
+ senderValue?: UserData;
15
+ setSenderValue: (senderValue?: UserData) => void;
16
+
17
+ messages: ChatMessage<UserData, AIData>[];
18
+ setMessages: (messages: ChatMessage<UserData, AIData>[]) => void;
19
+ addMessage: (message: ChatMessage<UserData, AIData>) => void;
20
+ updateMessage: (message: ChatMessage<UserData, AIData>) => void;
21
+ setMessagesBefore: (messages: ChatMessage<UserData, AIData>[]) => void;
22
+ setMessagesAfter: (messages: ChatMessage<UserData, AIData>[]) => void;
23
+
24
+ /** 存放Chat的上下文数据 */
25
+ contextData?: ContextData;
26
+ setContextData: (contextData?: ContextData) => void;
27
+ setContextDataWithField: (
28
+ field: keyof ContextData,
29
+ contextDataValue: ContextData[keyof ContextData],
30
+ ) => void;
31
+ setContextDataPartial: (contextDataPartial: Partial<ContextData>) => void;
32
+
33
+ reset: () => void;
34
+ }
35
+
36
+ function createChatStore<
37
+ UserData extends BaseSenderValue,
38
+ AIData,
39
+ ContextData extends Record<string, any> = any,
40
+ >() {
41
+ const useChatStore = create<ChatStore<UserData, AIData, ContextData>>((set, get, store) => ({
42
+ senderValue: undefined,
43
+ setSenderValue: (senderValue) => {
44
+ set(() => ({ senderValue }));
45
+ },
46
+
47
+ messages: [],
48
+ setMessages: (messages) => {
49
+ set(() => ({
50
+ messages: messages.map((message) => ({
51
+ // 如果没有,则用当前的时间
52
+ createdAt: Date.now(),
53
+ updatedAt: Date.now(),
54
+ ...message,
55
+ })),
56
+ }));
57
+ },
58
+ addMessage: (message) => {
59
+ set((state) => ({
60
+ messages: [
61
+ ...state.messages,
62
+ {
63
+ ...message,
64
+ // 覆盖
65
+ createdAt: Date.now(),
66
+ updatedAt: Date.now(),
67
+ },
68
+ ],
69
+ }));
70
+ },
71
+ updateMessage: (message) => {
72
+ set((state) => ({
73
+ messages: state.messages.map((m) => {
74
+ if (m.uuid === message.uuid) {
75
+ return {
76
+ ...message,
77
+ // 覆盖
78
+ updatedAt: Date.now(),
79
+ };
80
+ }
81
+ return m;
82
+ }),
83
+ }));
84
+ },
85
+ setMessagesBefore: (messagesBefore) => {
86
+ if (messagesBefore.length === 0) {
87
+ return;
88
+ }
89
+
90
+ const messages = get().messages;
91
+
92
+ const lastMessageBefore = messagesBefore[messagesBefore.length - 1];
93
+ const index = messages.findIndex((message) => message.uuid === lastMessageBefore.uuid);
94
+
95
+ // 如果 index 非 -1,则合并
96
+ // 如果 index -1,-1 + 1 为 0,即全取 message,也适用
97
+ const newMessages = [...messagesBefore, ...messages.slice(index + 1)];
98
+
99
+ set({
100
+ messages: newMessages,
101
+ });
102
+ },
103
+ setMessagesAfter: (messagesAfter) => {
104
+ if (messagesAfter.length === 0) {
105
+ return;
106
+ }
107
+
108
+ const messages = get().messages;
109
+
110
+ const firstMessageAfter = messagesAfter[0];
111
+ const index = messages.findIndex((message) => message.uuid === firstMessageAfter.uuid);
112
+
113
+ // 如果 index 非 -1,则合并
114
+ // 如果 index -1,-1 + 1 为 0,即全取 message,也适用
115
+ const newMessages = [...messages.slice(0, index), ...messagesAfter];
116
+
117
+ set({
118
+ messages: newMessages,
119
+ });
120
+ },
121
+ contextData: undefined,
122
+ setContextData: (contextData) => {
123
+ set({ contextData });
124
+ },
125
+ setContextDataWithField: (field, contextDataValue) => {
126
+ const preContextData = get().contextData;
127
+ set({
128
+ contextData: {
129
+ ...preContextData,
130
+ [field]: contextDataValue,
131
+ } as ContextData,
132
+ });
133
+ },
134
+ setContextDataPartial: (contextDataPartial) => {
135
+ const preContextData = get().contextData;
136
+ set({
137
+ contextData: {
138
+ ...preContextData,
139
+ ...contextDataPartial,
140
+ } as ContextData,
141
+ });
142
+ },
143
+ reset: () => {
144
+ set(store.getInitialState());
145
+ },
146
+ }));
147
+
148
+ const useChatStoreComputed = () => {
149
+ const messages = useChatStore((state) => state.messages);
150
+
151
+ const chatStatus = useMemo(() => {
152
+ return messages[messages.length - 1]?.status;
153
+ }, [messages]);
154
+
155
+ return {
156
+ chatStatus,
157
+ };
158
+ };
159
+
160
+ return {
161
+ useChatStore,
162
+ useChatStoreComputed,
163
+ };
164
+ }
165
+
166
+ export { createChatStore };
@@ -0,0 +1,46 @@
1
+ enum EnumChatMessageType {
2
+ SYSTEM = 'system',
3
+ }
4
+
5
+ enum EnumChatMessageStatus {
6
+ PENDING = 'pending',
7
+ STREAMING = 'streaming',
8
+ DONE = 'done',
9
+ ERROR = 'error',
10
+ }
11
+
12
+ interface ChatMessageOfSystem {
13
+ data?: any;
14
+ }
15
+
16
+ interface ChatMessageOfUser {
17
+ text?: string;
18
+ files?: string[];
19
+ }
20
+
21
+ interface ChatMessageOfAI<AIData> {
22
+ data?: AIData;
23
+ error?: any;
24
+ // 其他字段,根据业务需要使用
25
+ session_id?: string;
26
+ }
27
+
28
+ interface ChatMessage<UserData, AIData> {
29
+ uuid: string;
30
+
31
+ status?: EnumChatMessageStatus;
32
+
33
+ type?: EnumChatMessageType;
34
+ system?: ChatMessageOfSystem;
35
+ user?: UserData;
36
+ ai?: ChatMessageOfAI<AIData>;
37
+
38
+ /** 自动生成 */
39
+ createdAt?: number;
40
+ /** 自动更新 */
41
+ updatedAt?: number;
42
+ }
43
+
44
+ export { EnumChatMessageStatus, EnumChatMessageType };
45
+
46
+ export type { ChatMessage, ChatMessageOfAI, ChatMessageOfUser };
@@ -0,0 +1,41 @@
1
+ import { XRequest } from '@ant-design/x-sdk';
2
+
3
+ async function fetchStream(
4
+ url: string,
5
+ {
6
+ params,
7
+ headers,
8
+ callbacks,
9
+ ...rest
10
+ }: {
11
+ params?: Record<string, any>;
12
+ headers?: Record<string, string>;
13
+ callbacks?: {
14
+ onUpdate?: ({ event, data }: { event: string; data: string }, headers: Headers) => void;
15
+ onError?: (error: Error, errorInfo?: any) => void;
16
+ };
17
+ },
18
+ ) {
19
+ await XRequest<any, { event: string; data: string }>(url, {
20
+ ...rest,
21
+ params: params,
22
+ headers: headers,
23
+ callbacks: {
24
+ onUpdate: (message, headers) => {
25
+ // 会存在 message 为 undefined 的情况
26
+ if (message) {
27
+ callbacks?.onUpdate?.(message, headers);
28
+ }
29
+ },
30
+ onSuccess: () => {
31
+ // nothing
32
+ },
33
+ onError: (error, errorInfo) => {
34
+ console.log('XRequest onError', error);
35
+ callbacks?.onError?.(error, errorInfo);
36
+ },
37
+ },
38
+ });
39
+ }
40
+
41
+ export { fetchStream };
package/src/style.scss ADDED
@@ -0,0 +1,100 @@
1
+ .fea-markdown.x-markdown {
2
+ --hr-margin: 1em 0;
3
+ --margin-ul-ol: 0;
4
+ --margin-block: 0 0 8px 0;
5
+
6
+ ul,
7
+ ol {
8
+ li {
9
+ margin: 0;
10
+ }
11
+ }
12
+
13
+ .ant-codeHighlighter-header {
14
+ padding: 8px 12px;
15
+ }
16
+
17
+ &.x-markdown-light pre code:not([class$='-highlightCode-code'] pre code) {
18
+ padding: 0 !important;
19
+ background-color: transparent !important;
20
+ margin: 0 !important;
21
+ }
22
+
23
+ .fea-markdown-body-block-chart {
24
+ background: white;
25
+ border-radius: 6px;
26
+ margin: 0.5rem;
27
+
28
+ .fea-markdown-body-block-chart-title {
29
+ font-size: 16px;
30
+ font-weight: bold;
31
+ padding: 10px;
32
+ }
33
+ }
34
+ }
35
+
36
+ .fea-sender {
37
+ .ant-upload-select {
38
+ display: none !important;
39
+ }
40
+
41
+ .ant-upload-list {
42
+ display: none !important;
43
+ }
44
+
45
+ .ant-upload-wrapper {
46
+ display: none;
47
+ }
48
+
49
+ &.fea-sender-drag-hover {
50
+ .ant-upload-wrapper {
51
+ display: block;
52
+ position: absolute;
53
+ inset: 0;
54
+ z-index: 10;
55
+
56
+ .ant-upload-drag {
57
+ border-color: var(--color-primary);
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ @keyframes fea-sender-rectangle-white {
64
+ 0%,
65
+ 80%,
66
+ 100% {
67
+ height: 4px;
68
+ box-shadow: 0 0 var(--color-white);
69
+ }
70
+
71
+ 40% {
72
+ height: 8px;
73
+ box-shadow: 0 -8px var(--color-white);
74
+ }
75
+ }
76
+
77
+ @keyframes fea-sender-rectangle-primary {
78
+ 0%,
79
+ 80%,
80
+ 100% {
81
+ height: 4px;
82
+ box-shadow: 0 0 var(--color-primary);
83
+ }
84
+
85
+ 40% {
86
+ height: 8px;
87
+ box-shadow: 0 -8px var(--color-primary);
88
+ }
89
+ }
90
+
91
+ .fea-message-think {
92
+ &:not(.fea-message-think-deep-seek) {
93
+ .ant-think-content {
94
+ border-inline-start: none;
95
+ background-color: var(--background-color-01);
96
+ padding: 12px 16px;
97
+ border-radius: 8px;
98
+ }
99
+ }
100
+ }
@@ -0,0 +1,3 @@
1
+ <svg width="20" height="18" viewBox="0 0 20 18" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M14.9561 0C15.6719 -6.75109e-08 16.2617 -0.000981836 16.7402 0.0380859C17.2288 0.0780087 17.6787 0.164078 18.1006 0.378906C18.7546 0.712247 19.2867 1.24447 19.6201 1.89844C19.8351 2.32031 19.921 2.77023 19.9609 3.25879C20 3.7373 19.999 4.32705 19.999 5.04297V12.3477C19.999 13.0634 20 13.6534 19.9609 14.1318C19.921 14.6203 19.8349 15.0694 19.6201 15.4912C19.2867 16.1456 18.7549 16.6782 18.1006 17.0117C17.6786 17.2267 17.2289 17.3126 16.7402 17.3525C16.2617 17.3916 15.672 17.3906 14.9561 17.3906H5.04297C4.32702 17.3906 3.73732 17.3916 3.25879 17.3525C2.77019 17.3126 2.32034 17.2267 1.89844 17.0117C1.24428 16.6782 0.712259 16.1455 0.378906 15.4912C0.1642 15.0695 0.078011 14.6202 0.0380859 14.1318C-0.000970309 13.6534 -2.36304e-07 13.0634 0 12.3477V5.04297C-4.75188e-07 4.32705 -0.00100976 3.7373 0.0380859 3.25879C0.0780378 2.7702 0.163945 2.32032 0.378906 1.89844C0.712319 1.24436 1.24435 0.712298 1.89844 0.378906C2.32035 0.163931 2.77017 0.0780345 3.25879 0.0380859C3.73732 -0.00101134 4.32702 -3.93286e-07 5.04297 0H14.9561ZM5.04297 1.73828C4.29836 1.73828 3.79169 1.73951 3.40039 1.77148C3.01944 1.80264 2.82442 1.85944 2.68848 1.92871C2.36139 2.09541 2.09543 2.3614 1.92871 2.68848C1.85945 2.82441 1.80264 3.0195 1.77148 3.40039C1.73951 3.79168 1.73828 4.29841 1.73828 5.04297V12.3477C1.73828 13.092 1.73955 13.599 1.77148 13.9902C1.8026 14.3707 1.85956 14.5662 1.92871 14.7021C2.09537 15.0291 2.36157 15.2952 2.68848 15.4619C2.82441 15.5312 3.01958 15.588 3.40039 15.6191C3.79169 15.6511 4.29836 15.6514 5.04297 15.6514H14.9561C15.7006 15.6514 16.2073 15.6511 16.5986 15.6191C16.9796 15.588 17.1746 15.5312 17.3105 15.4619C17.6377 15.2952 17.9036 15.0293 18.0703 14.7021C18.1395 14.5662 18.1964 14.371 18.2275 13.9902C18.2595 13.599 18.2607 13.092 18.2607 12.3477V5.04297C18.2607 4.29841 18.2595 3.79168 18.2275 3.40039C18.1964 3.01955 18.1396 2.82441 18.0703 2.68848C17.9036 2.36151 17.6376 2.09536 17.3105 1.92871C17.1746 1.85954 16.9793 1.8026 16.5986 1.77148C16.2073 1.73954 15.7006 1.73828 14.9561 1.73828H5.04297ZM14.7822 12.1738C15.2625 12.1738 15.6523 12.5637 15.6523 13.0439C15.6521 13.5239 15.2623 13.9131 14.7822 13.9131H5.2168C4.73692 13.9129 4.34795 13.5238 4.34766 13.0439C4.34766 12.5638 4.73674 12.174 5.2168 12.1738H14.7822ZM6.08691 7.82617C6.56708 7.82626 6.95605 8.2161 6.95605 8.69629V9.56543C6.95592 10.0455 6.567 10.4345 6.08691 10.4346H5.2168C4.73683 10.4344 4.3478 10.0454 4.34766 9.56543V8.69629C4.34766 8.21618 4.73674 7.82639 5.2168 7.82617H6.08691ZM10.4346 7.82617C10.9148 7.82618 11.3047 8.21605 11.3047 8.69629V9.56543C11.3045 10.0456 10.9147 10.4346 10.4346 10.4346H9.56543C9.08527 10.4346 8.69545 10.0456 8.69531 9.56543V8.69629C8.69531 8.21605 9.08519 7.82617 9.56543 7.82617H10.4346ZM14.7822 7.82617C15.2624 7.82627 15.6514 8.21611 15.6514 8.69629V9.56543C15.6512 10.0455 15.2623 10.4345 14.7822 10.4346H13.9121C13.4321 10.4344 13.0431 10.0454 13.043 9.56543V8.69629C13.043 8.21618 13.432 7.82638 13.9121 7.82617H14.7822ZM6.08691 3.47852C6.56695 3.4786 6.95583 3.86766 6.95605 4.34766V5.21777C6.95585 5.69779 6.56696 6.08683 6.08691 6.08691H5.2168C4.73687 6.08669 4.34787 5.6977 4.34766 5.21777V4.34766C4.34788 3.86774 4.73688 3.47874 5.2168 3.47852H6.08691ZM10.4346 3.47852C10.9147 3.47852 11.3045 3.86761 11.3047 4.34766V5.21777C11.3045 5.69784 10.9147 6.08691 10.4346 6.08691H9.56543C9.08532 6.08691 8.69552 5.69784 8.69531 5.21777V4.34766C8.69554 3.86761 9.08533 3.47852 9.56543 3.47852H10.4346ZM14.7822 3.47852C15.2622 3.47861 15.6511 3.86767 15.6514 4.34766V5.21777C15.6512 5.69778 15.2623 6.08682 14.7822 6.08691H13.9121C13.4322 6.0867 13.0432 5.69771 13.043 5.21777V4.34766C13.0432 3.86774 13.4322 3.47872 13.9121 3.47852H14.7822Z"/>
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="14" height="18" viewBox="0 0 14 18" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M1.46387 8.41895C1.46399 11.2488 3.75802 13.5429 6.58789 13.543C9.41782 13.543 11.7118 11.2488 11.7119 8.41895H13.1758C13.1757 11.8102 10.6136 14.6009 7.31934 14.9648V17.2031H5.85547V14.9648C2.56181 14.6004 0.000112876 11.8097 0 8.41895H1.46387ZM6.58789 0C8.60932 0 10.248 1.63873 10.248 3.66016V8.41797C10.248 10.4394 8.60932 12.0781 6.58789 12.0781C4.56664 12.0779 2.92773 10.4393 2.92773 8.41797V3.66016C2.92775 1.63887 4.56665 0.000220666 6.58789 0Z" />
3
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M9.80022 0.000346623C10.3956 -0.0083669 10.97 0.146298 11.3912 0.566726C11.8123 0.987879 11.9677 1.56297 11.9583 2.1584C11.9496 2.75164 11.7782 3.41387 11.4935 4.09352C11.2145 4.7429 10.8741 5.36414 10.477 5.94878C10.9214 6.60592 11.2793 7.25436 11.5349 7.86575C11.8196 8.54541 11.9902 9.20764 11.9997 9.80088C12.0084 10.3963 11.853 10.9707 11.4326 11.3911C11.0114 11.8123 10.4363 11.9684 9.84088 11.9589C9.24763 11.9502 8.58613 11.7789 7.90575 11.4942C7.29726 11.2401 6.65318 10.8843 5.99894 10.4428C5.34543 10.8843 4.70135 11.2401 4.09286 11.4942C3.4132 11.7789 2.75097 11.9502 2.15773 11.9589C1.5623 11.9684 0.987938 11.8123 0.566784 11.3918C0.14563 10.9707 -0.00903481 10.3963 0.000404838 9.80088C0.00911836 9.20691 0.179032 8.54541 0.463674 7.86575C0.719271 7.25436 1.0758 6.60592 1.52019 5.94878C1.12348 5.36412 0.783555 4.74287 0.505063 4.09352C0.220421 3.41387 0.0505076 2.75164 0.041068 2.1584C0.0323545 1.56297 0.186293 0.988605 0.607447 0.567452C1.0286 0.146298 1.60369 -0.00836689 2.19912 0.00107276C2.79236 0.00978629 3.45387 0.1797 4.13425 0.464342C4.72967 0.713403 5.35922 1.05831 5.99894 1.486C6.63866 1.05759 7.26966 0.713403 7.86509 0.464342C8.54474 0.1797 9.20624 0.00978628 9.80022 0.000346623ZM2.20638 6.89057C1.9225 7.33469 1.67575 7.80148 1.46863 8.28618C1.21667 8.88887 1.09468 9.40805 1.08814 9.81686C1.08233 10.2235 1.18835 10.4733 1.33648 10.6214C1.48533 10.7695 1.73585 10.8756 2.14175 10.8697C2.55056 10.8639 3.06974 10.7419 3.67243 10.49C4.15456 10.2835 4.61891 10.0377 5.06078 9.75514C4.53958 9.34105 4.04257 8.89738 3.57222 8.42632C3.0712 7.9253 2.61374 7.40829 2.20638 6.89057ZM9.7915 6.89057C8.95508 7.9533 7.99736 8.91467 6.93782 9.75514C7.4207 10.0601 7.88905 10.307 8.32618 10.49C8.92886 10.7419 9.44877 10.8639 9.85758 10.8697C10.2642 10.8763 10.514 10.7703 10.6621 10.6214C10.8103 10.4733 10.9163 10.2235 10.9105 9.81686C10.9039 9.40805 10.7819 8.88814 10.53 8.28618C10.3226 7.80147 10.0756 7.33468 9.7915 6.89057ZM5.99894 2.82208C5.39911 3.27502 4.83182 3.76951 4.30126 4.30192C3.78316 4.81841 3.30086 5.36961 2.85772 5.95168C3.31201 6.55419 3.80822 7.12392 4.34264 7.65663C4.86105 8.1765 5.41444 8.66026 5.99894 9.10453C6.5508 8.687 7.10992 8.20268 7.65596 7.65663C8.18989 7.12387 8.68561 6.55414 9.13944 5.95168C8.69678 5.36964 8.21497 4.81845 7.69735 4.30192C7.13824 3.74281 6.56459 3.24686 5.99894 2.82208ZM2.18242 1.08881C1.77579 1.083 1.526 1.18902 1.37787 1.33715C1.22974 1.486 1.12372 1.73579 1.12953 2.14242C1.13607 2.55123 1.25806 3.07041 1.51002 3.6731C1.68647 4.09498 1.92174 4.54445 2.21074 5.00917C2.62264 4.4922 3.06362 3.99909 3.53156 3.53223C4.01281 3.05013 4.52198 2.59673 5.05643 2.17437C4.5888 1.88319 4.13715 1.64648 3.71309 1.4693C3.11041 1.21734 2.59123 1.09535 2.18242 1.08881ZM9.81619 1.08881C9.40738 1.09607 8.88748 1.21734 8.28552 1.4693C7.86146 1.64648 7.40908 1.88319 6.94218 2.17365C7.47661 2.59602 7.98578 3.04942 8.46705 3.5315C8.93477 3.99886 9.37552 4.49245 9.78715 5.0099C10.0761 4.54518 10.3136 4.0957 10.49 3.67382C10.742 3.07114 10.864 2.55196 10.8698 2.14315C10.8763 1.73652 10.7703 1.48673 10.6215 1.33787C10.4733 1.18974 10.2235 1.08373 9.81692 1.08954L9.81619 1.08881Z"/>
3
+ </svg>
@@ -0,0 +1,188 @@
1
+ function getRecordAudioOfPCM() {
2
+ let processorNode: ScriptProcessorNode | null = null;
3
+ let sourceNode: MediaStreamAudioSourceNode | null = null;
4
+ let audioContext: AudioContext | null = null;
5
+ let micStream: MediaStream | null = null;
6
+
7
+ let data: ArrayBufferLike[] = [];
8
+
9
+ async function start({
10
+ onAudio,
11
+ onError,
12
+ }: {
13
+ onAudio: (data: ArrayBufferLike) => void;
14
+ onError?: (error: Error) => void;
15
+ }): Promise<void> {
16
+ try {
17
+ // --- 初始化音频 ---
18
+ micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
19
+ audioContext = new AudioContext({ sampleRate: 16000 });
20
+ sourceNode = audioContext.createMediaStreamSource(micStream);
21
+ // ScriptProcessorNode(4096 是稳定 buffer)
22
+ processorNode = audioContext.createScriptProcessor(4096, 1, 1);
23
+
24
+ data = [];
25
+
26
+ processorNode.onaudioprocess = function (event) {
27
+ const float32Data = event.inputBuffer.getChannelData(0); // float32
28
+
29
+ // === 转成 Int16 PCM ===
30
+ const pcm16 = new Int16Array(float32Data.length);
31
+ for (let i = 0; i < float32Data.length; i++) {
32
+ const s = Math.max(-1, Math.min(1, float32Data[i]));
33
+ pcm16[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
34
+ }
35
+
36
+ data.push(pcm16.buffer);
37
+
38
+ onAudio(pcm16.buffer);
39
+ };
40
+
41
+ sourceNode.connect(processorNode);
42
+ processorNode.connect(audioContext.destination);
43
+ } catch (err) {
44
+ if (err instanceof DOMException && err.name === 'NotAllowedError') {
45
+ onError?.(new Error('请允许麦克风权限'));
46
+ } else if (err instanceof DOMException && err.name === 'NotFoundError') {
47
+ onError?.(new Error('未找到麦克风设备'));
48
+ } else if (err instanceof DOMException && err.name === 'NotReadableError') {
49
+ onError?.(new Error('麦克风被其他应用占用'));
50
+ } else {
51
+ onError?.(new Error('启动录音失败'));
52
+ }
53
+
54
+ throw err;
55
+ }
56
+ }
57
+
58
+ async function stop(): Promise<{ data: ArrayBufferLike[] }> {
59
+ if (processorNode) processorNode.disconnect();
60
+ if (sourceNode) sourceNode.disconnect();
61
+ if (audioContext) audioContext.close();
62
+ if (micStream) micStream.getTracks().forEach((track) => track.stop());
63
+
64
+ const result = data;
65
+ data = [];
66
+
67
+ return { data: result };
68
+ }
69
+
70
+ return {
71
+ start,
72
+ stop,
73
+ };
74
+ }
75
+
76
+ function getRecordAudioOfBlob() {
77
+ let mediaRecorder: MediaRecorder | null = null;
78
+ let micStream: MediaStream | null = null;
79
+ let chunks: Blob[] = [];
80
+
81
+ async function start({
82
+ onAudio,
83
+ onError,
84
+ mimeType = 'audio/webm',
85
+ }: {
86
+ onAudio?: (blob: Blob) => void;
87
+ onError?: (error: Error) => void;
88
+ mimeType?: string;
89
+ }): Promise<void> {
90
+ try {
91
+ // 获取麦克风权限
92
+ micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
93
+
94
+ // 检查浏览器是否支持指定的 MIME 类型
95
+ let finalMimeType = mimeType;
96
+ if (!MediaRecorder.isTypeSupported(mimeType)) {
97
+ // 如果不支持,尝试使用默认类型
98
+ finalMimeType = '';
99
+ console.warn(`不支持的 MIME 类型: ${mimeType},使用默认类型`);
100
+ }
101
+
102
+ // 创建 MediaRecorder 实例
103
+ mediaRecorder = new MediaRecorder(micStream, {
104
+ mimeType: finalMimeType || undefined,
105
+ });
106
+
107
+ chunks = [];
108
+
109
+ // 监听数据可用事件
110
+ mediaRecorder.ondataavailable = (event) => {
111
+ if (event.data && event.data.size > 0) {
112
+ chunks.push(event.data);
113
+ onAudio?.(event.data);
114
+ }
115
+ };
116
+
117
+ // 监听错误事件
118
+ mediaRecorder.onerror = () => {
119
+ const error = new Error('MediaRecorder 录音错误');
120
+ onError?.(error);
121
+ };
122
+
123
+ // 开始录音
124
+ mediaRecorder.start(100); // 每 100ms 触发一次 dataavailable 事件
125
+ } catch (err) {
126
+ if (err instanceof DOMException && err.name === 'NotAllowedError') {
127
+ onError?.(new Error('请允许麦克风权限'));
128
+ } else if (err instanceof DOMException && err.name === 'NotFoundError') {
129
+ onError?.(new Error('未找到麦克风设备'));
130
+ } else if (err instanceof DOMException && err.name === 'NotReadableError') {
131
+ onError?.(new Error('麦克风被其他应用占用'));
132
+ } else {
133
+ onError?.(new Error('启动录音失败'));
134
+ }
135
+
136
+ throw err;
137
+ }
138
+ }
139
+
140
+ async function stop(): Promise<{ data: Blob; base64: string }> {
141
+ return new Promise((resolve, reject) => {
142
+ if (!mediaRecorder) {
143
+ reject(new Error('MediaRecorder 未初始化'));
144
+ return;
145
+ }
146
+
147
+ function doStop() {
148
+ const blob = new Blob(chunks, { type: mediaRecorder?.mimeType || 'audio/webm' });
149
+ chunks = [];
150
+
151
+ // 停止所有轨道
152
+ if (micStream) {
153
+ micStream.getTracks().forEach((track) => track.stop());
154
+ }
155
+
156
+ // 将 Blob 转换为 base64
157
+ const reader = new FileReader();
158
+ reader.onloadend = () => {
159
+ const base64String = reader.result as string;
160
+ resolve({ data: blob, base64: base64String });
161
+ };
162
+ reader.onerror = () => {
163
+ reject(new Error('转换为 base64 失败'));
164
+ };
165
+ reader.readAsDataURL(blob);
166
+ }
167
+
168
+ // 监听停止事件
169
+ mediaRecorder.onstop = () => {
170
+ doStop();
171
+ };
172
+
173
+ // 如果正在录音,则停止
174
+ if (mediaRecorder.state === 'recording') {
175
+ mediaRecorder.stop();
176
+ } else {
177
+ doStop();
178
+ }
179
+ });
180
+ }
181
+
182
+ return {
183
+ start,
184
+ stop,
185
+ };
186
+ }
187
+
188
+ export { getRecordAudioOfBlob, getRecordAudioOfPCM };
@@ -1,75 +0,0 @@
1
- .fea-sender {
2
- .ant-upload-select {
3
- display: none !important;
4
- }
5
-
6
- .ant-upload-list {
7
- display: none !important;
8
- }
9
-
10
- .ant-upload-wrapper {
11
- display: none;
12
- }
13
-
14
- &.fea-sender-drag-hover {
15
- .ant-upload-wrapper {
16
- display: block;
17
- position: absolute;
18
- inset: 0;
19
- z-index: 10;
20
-
21
- .ant-upload-drag {
22
- border-color: theme('colors.primary');
23
- }
24
- }
25
- }
26
-
27
- .fea-sender-spinner {
28
- display: block;
29
- position: relative;
30
- width: 2px;
31
- height: 0;
32
- }
33
-
34
- .fea-sender-spinner .fea-sender-spinner-line {
35
- position: absolute;
36
- width: 2px;
37
- height: 4px;
38
- content: '';
39
- background-color: theme('colors.primary');
40
- }
41
-
42
- .fea-sender-spinner .fea-sender-spinner-line1 {
43
- left: -6px;
44
- animation: rectangle infinite 1s ease-in-out 0s;
45
- }
46
-
47
- .fea-sender-spinner .fea-sender-spinner-line2 {
48
- left: -2px;
49
- animation: rectangle infinite 1s ease-in-out 0.25s;
50
- }
51
-
52
- .fea-sender-spinner .fea-sender-spinner-line3 {
53
- right: -2px;
54
- animation: rectangle infinite 1s ease-in-out 0.5s;
55
- }
56
-
57
- .fea-sender-spinner .fea-sender-spinner-line4 {
58
- right: -6px;
59
- animation: rectangle infinite 1s ease-in-out 0.75s;
60
- }
61
-
62
- @keyframes rectangle {
63
- 0%,
64
- 80%,
65
- 100% {
66
- height: 4px;
67
- box-shadow: 0 0 theme('colors.primary');
68
- }
69
-
70
- 40% {
71
- height: 6px;
72
- box-shadow: 0 -6px theme('colors.primary');
73
- }
74
- }
75
- }
File without changes