@alicloud/appflow-chat 0.0.4-alpha.5 → 0.0.4-alpha.7
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/dist/appflow-chat.cjs.js +236 -149
- package/dist/appflow-chat.esm.js +5917 -5750
- package/dist/types/index.d.ts +3 -0
- package/package.json +1 -1
- package/src/App.tsx +11 -3
- package/src/components/AudioPlayer.tsx +246 -0
- package/src/components/MessageBubble.tsx +55 -43
- package/src/services/ChatService.ts +8 -2
package/dist/types/index.d.ts
CHANGED
|
@@ -491,6 +491,7 @@ export declare interface HistoryMessage {
|
|
|
491
491
|
name: string;
|
|
492
492
|
url: string;
|
|
493
493
|
}[];
|
|
494
|
+
audio?: string;
|
|
494
495
|
}
|
|
495
496
|
|
|
496
497
|
/**
|
|
@@ -623,6 +624,8 @@ export declare interface MessageBubbleProps {
|
|
|
623
624
|
name: string;
|
|
624
625
|
url: string;
|
|
625
626
|
}[];
|
|
627
|
+
/** 语音消息URL */
|
|
628
|
+
audio?: string;
|
|
626
629
|
/** 自定义类名 */
|
|
627
630
|
className?: string;
|
|
628
631
|
/** 自定义样式 */
|
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -16,7 +16,12 @@ const mockModels: ModelInfo[] = [
|
|
|
16
16
|
const mockUpload = async (file: File): Promise<string> => {
|
|
17
17
|
return new Promise((resolve) => {
|
|
18
18
|
setTimeout(() => {
|
|
19
|
-
|
|
19
|
+
// 音频文件返回 blob URL,以便本地测试播放
|
|
20
|
+
if (file.type.startsWith('audio/')) {
|
|
21
|
+
resolve(URL.createObjectURL(file));
|
|
22
|
+
} else {
|
|
23
|
+
resolve(`https://example.com/files/${file.name}`);
|
|
24
|
+
}
|
|
20
25
|
}, 1500);
|
|
21
26
|
});
|
|
22
27
|
};
|
|
@@ -47,6 +52,7 @@ function App() {
|
|
|
47
52
|
status: 'Running' | 'Success' | 'Error';
|
|
48
53
|
images?: string[];
|
|
49
54
|
files?: { name: string; url: string }[];
|
|
55
|
+
audio?: string;
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
const [messages, setMessages] = useState<Message[]>([]);
|
|
@@ -54,14 +60,15 @@ function App() {
|
|
|
54
60
|
const handleSubmit = (data: ChatSenderSubmitData) => {
|
|
55
61
|
addLog(`发送消息: text="${data.text}", model=${data.modelId}, images=${data.images.length}, files=${data.files.length}, audio=${data.audio || 'none'}, webSearch=${data.webSearch}`);
|
|
56
62
|
|
|
57
|
-
//
|
|
63
|
+
// 构建用户消息(包含图片、文件和语音)
|
|
58
64
|
const userMsg: Message = {
|
|
59
65
|
id: `msg-${Date.now()}`,
|
|
60
66
|
role: 'user',
|
|
61
|
-
content: data.text,
|
|
67
|
+
content: data.audio ? '' : data.text,
|
|
62
68
|
status: 'Success',
|
|
63
69
|
images: data.images.length > 0 ? data.images : undefined,
|
|
64
70
|
files: data.files.length > 0 ? data.files : undefined,
|
|
71
|
+
audio: data.audio || undefined,
|
|
65
72
|
};
|
|
66
73
|
|
|
67
74
|
// 构建 bot 占位消息
|
|
@@ -144,6 +151,7 @@ function App() {
|
|
|
144
151
|
status={msg.status}
|
|
145
152
|
images={msg.images}
|
|
146
153
|
files={msg.files}
|
|
154
|
+
audio={msg.audio}
|
|
147
155
|
/>
|
|
148
156
|
))}
|
|
149
157
|
</div>
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AudioPlayer - 语音消息播放器组件
|
|
3
|
+
* 用于在消息气泡中展示语音消息,支持播放/暂停、进度条、波形动画、时长显示
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useRef, useState, useCallback, useEffect } from 'react';
|
|
7
|
+
import styled, { keyframes, css } from 'styled-components';
|
|
8
|
+
import {
|
|
9
|
+
PlayCircleOutlined,
|
|
10
|
+
PauseCircleOutlined,
|
|
11
|
+
} from '@ant-design/icons';
|
|
12
|
+
|
|
13
|
+
// ==================== 类型定义 ====================
|
|
14
|
+
|
|
15
|
+
export interface AudioPlayerProps {
|
|
16
|
+
/** 音频文件URL */
|
|
17
|
+
src: string;
|
|
18
|
+
/** 消息角色,影响配色 */
|
|
19
|
+
role?: 'user' | 'bot';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ==================== 样式组件 ====================
|
|
23
|
+
|
|
24
|
+
const waveAnimation = keyframes`
|
|
25
|
+
0%, 100% { height: 4px; }
|
|
26
|
+
50% { height: 16px; }
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
const AudioCard = styled.div<{ $role: 'user' | 'bot' }>`
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
gap: 10px;
|
|
33
|
+
background: ${props => props.$role === 'user' ? '#e5effe' : '#e8f4fd'};
|
|
34
|
+
border-radius: 12px;
|
|
35
|
+
padding: 10px 14px;
|
|
36
|
+
max-width: 100%;
|
|
37
|
+
box-sizing: border-box;
|
|
38
|
+
min-width: 200px;
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
const PlayButton = styled.span<{ $role: 'user' | 'bot' }>`
|
|
42
|
+
font-size: 28px;
|
|
43
|
+
cursor: pointer;
|
|
44
|
+
flex-shrink: 0;
|
|
45
|
+
color: ${props => props.$role === 'user' ? '#667eea' : '#1677ff'};
|
|
46
|
+
display: flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
transition: opacity 0.2s;
|
|
49
|
+
|
|
50
|
+
&:hover {
|
|
51
|
+
opacity: 0.8;
|
|
52
|
+
}
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
const AudioBody = styled.div`
|
|
56
|
+
flex: 1;
|
|
57
|
+
display: flex;
|
|
58
|
+
flex-direction: column;
|
|
59
|
+
gap: 6px;
|
|
60
|
+
min-width: 0;
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
const WaveformContainer = styled.div`
|
|
64
|
+
display: flex;
|
|
65
|
+
align-items: center;
|
|
66
|
+
gap: 2px;
|
|
67
|
+
height: 20px;
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
const WaveBar = styled.div<{ $active: boolean; $playing: boolean; $delay: number }>`
|
|
71
|
+
width: 3px;
|
|
72
|
+
border-radius: 2px;
|
|
73
|
+
background: ${props => props.$active ? '#667eea' : '#c0c8d8'};
|
|
74
|
+
transition: background 0.15s;
|
|
75
|
+
|
|
76
|
+
${props => props.$playing && props.$active ? css`
|
|
77
|
+
animation: ${waveAnimation} 0.6s ease-in-out infinite;
|
|
78
|
+
animation-delay: ${props.$delay}s;
|
|
79
|
+
` : css`
|
|
80
|
+
height: ${props.$active ? '12px' : `${4 + Math.random() * 10}px`};
|
|
81
|
+
`}
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
const ProgressBar = styled.div`
|
|
85
|
+
position: relative;
|
|
86
|
+
height: 3px;
|
|
87
|
+
background: rgba(0, 0, 0, 0.08);
|
|
88
|
+
border-radius: 2px;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
overflow: visible;
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
const ProgressFill = styled.div<{ $progress: number }>`
|
|
94
|
+
height: 100%;
|
|
95
|
+
background: #667eea;
|
|
96
|
+
border-radius: 2px;
|
|
97
|
+
width: ${props => props.$progress}%;
|
|
98
|
+
transition: width 0.1s linear;
|
|
99
|
+
position: relative;
|
|
100
|
+
|
|
101
|
+
&::after {
|
|
102
|
+
content: '';
|
|
103
|
+
position: absolute;
|
|
104
|
+
right: -4px;
|
|
105
|
+
top: 50%;
|
|
106
|
+
transform: translateY(-50%);
|
|
107
|
+
width: 8px;
|
|
108
|
+
height: 8px;
|
|
109
|
+
border-radius: 50%;
|
|
110
|
+
background: #667eea;
|
|
111
|
+
opacity: 0;
|
|
112
|
+
transition: opacity 0.2s;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
${ProgressBar}:hover &::after {
|
|
116
|
+
opacity: 1;
|
|
117
|
+
}
|
|
118
|
+
`;
|
|
119
|
+
|
|
120
|
+
const AudioDuration = styled.span`
|
|
121
|
+
font-size: 12px;
|
|
122
|
+
color: #888;
|
|
123
|
+
flex-shrink: 0;
|
|
124
|
+
min-width: 32px;
|
|
125
|
+
text-align: right;
|
|
126
|
+
font-variant-numeric: tabular-nums;
|
|
127
|
+
`;
|
|
128
|
+
|
|
129
|
+
// ==================== 工具函数 ====================
|
|
130
|
+
|
|
131
|
+
/** 格式化秒数为 m:ss */
|
|
132
|
+
function formatDuration(seconds: number): string {
|
|
133
|
+
if (!seconds || !isFinite(seconds)) return '0:00';
|
|
134
|
+
const m = Math.floor(seconds / 60);
|
|
135
|
+
const s = Math.floor(seconds % 60);
|
|
136
|
+
return `${m}:${s.toString().padStart(2, '0')}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 生成固定的波形条高度 */
|
|
140
|
+
const WAVE_BARS = Array.from({ length: 20 }, (_, i) => {
|
|
141
|
+
// 用正弦函数生成自然的波形高度
|
|
142
|
+
const base = Math.sin((i / 20) * Math.PI) * 12 + 4;
|
|
143
|
+
return Math.max(4, Math.min(18, base + (Math.random() * 4 - 2)));
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ==================== 组件实现 ====================
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* AudioPlayer - 语音消息播放器
|
|
150
|
+
*
|
|
151
|
+
* 紧凑的播放器组件,包含播放/暂停按钮 + 波形条 + 进度条 + 时长显示。
|
|
152
|
+
* 风格类似钉钉/微信的聊天语音消息,可独立使用无需外层气泡包裹。
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* ```tsx
|
|
156
|
+
* <AudioPlayer src="https://example.com/audio.webm" role="user" />
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
export const AudioPlayer: React.FC<AudioPlayerProps> = ({ src, role = 'user' }) => {
|
|
160
|
+
const audioRef = useRef<HTMLAudioElement>(null);
|
|
161
|
+
const progressRef = useRef<HTMLDivElement>(null);
|
|
162
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
163
|
+
const [currentTime, setCurrentTime] = useState(0);
|
|
164
|
+
const [duration, setDuration] = useState(0);
|
|
165
|
+
|
|
166
|
+
const progress = duration > 0 ? (currentTime / duration) * 100 : 0;
|
|
167
|
+
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
const audio = audioRef.current;
|
|
170
|
+
if (!audio) return;
|
|
171
|
+
|
|
172
|
+
const onLoadedMetadata = () => setDuration(audio.duration);
|
|
173
|
+
const onTimeUpdate = () => setCurrentTime(audio.currentTime);
|
|
174
|
+
const onEnded = () => {
|
|
175
|
+
setIsPlaying(false);
|
|
176
|
+
setCurrentTime(0);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
audio.addEventListener('loadedmetadata', onLoadedMetadata);
|
|
180
|
+
audio.addEventListener('timeupdate', onTimeUpdate);
|
|
181
|
+
audio.addEventListener('ended', onEnded);
|
|
182
|
+
|
|
183
|
+
return () => {
|
|
184
|
+
audio.removeEventListener('loadedmetadata', onLoadedMetadata);
|
|
185
|
+
audio.removeEventListener('timeupdate', onTimeUpdate);
|
|
186
|
+
audio.removeEventListener('ended', onEnded);
|
|
187
|
+
};
|
|
188
|
+
}, []);
|
|
189
|
+
|
|
190
|
+
const togglePlay = useCallback(() => {
|
|
191
|
+
const audio = audioRef.current;
|
|
192
|
+
if (!audio) return;
|
|
193
|
+
|
|
194
|
+
if (isPlaying) {
|
|
195
|
+
audio.pause();
|
|
196
|
+
setIsPlaying(false);
|
|
197
|
+
} else {
|
|
198
|
+
audio.play().then(() => setIsPlaying(true)).catch(() => {});
|
|
199
|
+
}
|
|
200
|
+
}, [isPlaying]);
|
|
201
|
+
|
|
202
|
+
const handleProgressClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
|
203
|
+
const audio = audioRef.current;
|
|
204
|
+
const bar = progressRef.current;
|
|
205
|
+
if (!audio || !bar || !duration) return;
|
|
206
|
+
|
|
207
|
+
const rect = bar.getBoundingClientRect();
|
|
208
|
+
const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
|
|
209
|
+
audio.currentTime = ratio * duration;
|
|
210
|
+
setCurrentTime(audio.currentTime);
|
|
211
|
+
}, [duration]);
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<AudioCard $role={role}>
|
|
215
|
+
<audio ref={audioRef} src={src} preload="metadata" />
|
|
216
|
+
<PlayButton $role={role} onClick={togglePlay}>
|
|
217
|
+
{isPlaying ? <PauseCircleOutlined /> : <PlayCircleOutlined />}
|
|
218
|
+
</PlayButton>
|
|
219
|
+
<AudioBody>
|
|
220
|
+
<WaveformContainer>
|
|
221
|
+
{WAVE_BARS.map((h, i) => {
|
|
222
|
+
const barProgress = (i / WAVE_BARS.length) * 100;
|
|
223
|
+
const isActive = barProgress <= progress;
|
|
224
|
+
return (
|
|
225
|
+
<WaveBar
|
|
226
|
+
key={i}
|
|
227
|
+
$active={isActive}
|
|
228
|
+
$playing={isPlaying}
|
|
229
|
+
$delay={i * 0.05}
|
|
230
|
+
style={!isPlaying || !isActive ? { height: `${h}px` } : undefined}
|
|
231
|
+
/>
|
|
232
|
+
);
|
|
233
|
+
})}
|
|
234
|
+
</WaveformContainer>
|
|
235
|
+
<ProgressBar ref={progressRef} onClick={handleProgressClick}>
|
|
236
|
+
<ProgressFill $progress={progress} />
|
|
237
|
+
</ProgressBar>
|
|
238
|
+
</AudioBody>
|
|
239
|
+
<AudioDuration>
|
|
240
|
+
{isPlaying || currentTime > 0 ? formatDuration(currentTime) : formatDuration(duration)}
|
|
241
|
+
</AudioDuration>
|
|
242
|
+
</AudioCard>
|
|
243
|
+
);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
export default AudioPlayer;
|
|
@@ -8,6 +8,7 @@ import React, { useEffect, useState, useCallback } from 'react';
|
|
|
8
8
|
import styled from 'styled-components';
|
|
9
9
|
import { Modal, Image, Space } from 'antd';
|
|
10
10
|
import { MessageAttachments } from './MessageAttachments';
|
|
11
|
+
import { AudioPlayer } from './AudioPlayer';
|
|
11
12
|
import { loadEchartsScript } from '@/utils/loadEcharts';
|
|
12
13
|
import { DocReferences, DocReferenceItem } from './DocReferences';
|
|
13
14
|
import { WebSearchPanel } from './WebSearchPanel';
|
|
@@ -52,6 +53,8 @@ export interface MessageBubbleProps {
|
|
|
52
53
|
images?: string[];
|
|
53
54
|
/** 文件列表(用户消息中上传的文件) */
|
|
54
55
|
files?: { name: string; url: string }[];
|
|
56
|
+
/** 语音消息URL */
|
|
57
|
+
audio?: string;
|
|
55
58
|
/** 自定义类名 */
|
|
56
59
|
className?: string;
|
|
57
60
|
/** 自定义样式 */
|
|
@@ -267,6 +270,7 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
|
|
267
270
|
references = [],
|
|
268
271
|
images,
|
|
269
272
|
files,
|
|
273
|
+
audio,
|
|
270
274
|
className,
|
|
271
275
|
style,
|
|
272
276
|
onReferenceClick,
|
|
@@ -340,6 +344,9 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
|
|
340
344
|
const handleReferenceClick = onReferenceClick || defaultReferenceClick;
|
|
341
345
|
const handleWebSearchClick = onWebSearchClick || defaultWebSearchClick;
|
|
342
346
|
|
|
347
|
+
// 纯语音消息:跳过 StyledBubble 包裹,直接渲染 AudioPlayer
|
|
348
|
+
const isAudioOnly = !!(audio && !content);
|
|
349
|
+
|
|
343
350
|
return (
|
|
344
351
|
<>
|
|
345
352
|
<StyledContainer
|
|
@@ -347,50 +354,55 @@ export const MessageBubble: React.FC<MessageBubbleProps> = ({
|
|
|
347
354
|
className={`appflow-sdk-message-bubble ${className || ''}`}
|
|
348
355
|
style={style}
|
|
349
356
|
>
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
<
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
{references && references.length > 0 && status === 'Success' && (
|
|
374
|
-
<ReferencesContainer>
|
|
375
|
-
<DocReferences
|
|
376
|
-
items={references}
|
|
377
|
-
status={status}
|
|
378
|
-
onItemClick={handleReferenceClick}
|
|
379
|
-
onWebSearchClick={handleWebSearchClick}
|
|
357
|
+
{isAudioOnly ? (
|
|
358
|
+
/* 纯语音消息:直接渲染播放器,无气泡包裹 */
|
|
359
|
+
<AudioPlayer src={audio} role={role} />
|
|
360
|
+
) : (
|
|
361
|
+
<StyledBubble $role={role}>
|
|
362
|
+
{/* 使用核心组件渲染内容 */}
|
|
363
|
+
<BubbleContent
|
|
364
|
+
content={content}
|
|
365
|
+
status={status}
|
|
366
|
+
role={role}
|
|
367
|
+
>
|
|
368
|
+
{/* HumanVerify事件 - 人工审核表单 */}
|
|
369
|
+
{eventType === 'humanVerify' && humanVerifyData && (
|
|
370
|
+
<HumanVerify
|
|
371
|
+
data={humanVerifyData}
|
|
372
|
+
onSubmit={onHumanVerifySubmit}
|
|
373
|
+
/>
|
|
374
|
+
)}
|
|
375
|
+
|
|
376
|
+
{/* HistoryCard 事件 - 历史对话中的审核卡片 */}
|
|
377
|
+
{eventType === 'historyCard' && historyCardData && (
|
|
378
|
+
<HistoryCard
|
|
379
|
+
data={historyCardData}
|
|
380
380
|
/>
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
381
|
+
)}
|
|
382
|
+
|
|
383
|
+
{/* 参考资料 */}
|
|
384
|
+
{references && references.length > 0 && status === 'Success' && (
|
|
385
|
+
<ReferencesContainer>
|
|
386
|
+
<DocReferences
|
|
387
|
+
items={references}
|
|
388
|
+
status={status}
|
|
389
|
+
onItemClick={handleReferenceClick}
|
|
390
|
+
onWebSearchClick={handleWebSearchClick}
|
|
391
|
+
/>
|
|
392
|
+
</ReferencesContainer>
|
|
393
|
+
)}
|
|
394
|
+
</BubbleContent>
|
|
395
|
+
|
|
396
|
+
{/* 附件展示区域:图片和文件 */}
|
|
397
|
+
<MessageAttachments
|
|
398
|
+
role={role}
|
|
399
|
+
images={images}
|
|
400
|
+
files={files}
|
|
401
|
+
/>
|
|
402
|
+
|
|
403
|
+
{contextHolder}
|
|
404
|
+
</StyledBubble>
|
|
405
|
+
)}
|
|
394
406
|
</StyledContainer>
|
|
395
407
|
|
|
396
408
|
{/* 默认的网页搜索抽屉(仅在用户未传入onWebSearchClick时使用) */}
|
|
@@ -81,6 +81,7 @@ export interface HistoryMessage {
|
|
|
81
81
|
sessionId?: string;
|
|
82
82
|
images?: string[];
|
|
83
83
|
files?: { name: string; url: string }[];
|
|
84
|
+
audio?: string;
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
export interface ChatSession {
|
|
@@ -729,15 +730,19 @@ class ChatService {
|
|
|
729
730
|
content = item.message;
|
|
730
731
|
}
|
|
731
732
|
|
|
733
|
+
// 语音消息:messageType === 'audio' 时,message 是音频文件 URL
|
|
734
|
+
const isAudioMessage = messageType === 'audio';
|
|
735
|
+
|
|
732
736
|
messages.push({
|
|
733
737
|
id: item.id || `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
734
738
|
role: 'user',
|
|
735
|
-
content,
|
|
739
|
+
content: isAudioMessage ? '' : content,
|
|
736
740
|
messageType,
|
|
737
741
|
gmtCreate: item.gmtCreate,
|
|
738
742
|
sessionId: item.sessionId,
|
|
739
743
|
images,
|
|
740
744
|
files,
|
|
745
|
+
audio: isAudioMessage ? content : undefined,
|
|
741
746
|
});
|
|
742
747
|
}
|
|
743
748
|
|
|
@@ -757,7 +762,8 @@ class ChatService {
|
|
|
757
762
|
}
|
|
758
763
|
}
|
|
759
764
|
|
|
760
|
-
|
|
765
|
+
// 接口返回的数据是倒序的(最新的在前),需要反转为正序(最早的在前)
|
|
766
|
+
return messages.reverse();
|
|
761
767
|
}
|
|
762
768
|
|
|
763
769
|
return [];
|