ipanel-rnapp-ajb-base 1.0.40 → 1.0.42
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/app/.local_dev/localMode.js +3 -0
- package/app/App.js +26 -7
- package/app/api/ipanelCloudApi.js +58 -4
- package/app/assets/img/adjust_scene_link.jpg +0 -0
- package/app/assets/img/moji_weather/W0.png +0 -0
- package/app/assets/img/moji_weather/W1.png +0 -0
- package/app/assets/img/moji_weather/W10.png +0 -0
- package/app/assets/img/moji_weather/W13.png +0 -0
- package/app/assets/img/moji_weather/W14.png +0 -0
- package/app/assets/img/moji_weather/W15.png +0 -0
- package/app/assets/img/moji_weather/W16.png +0 -0
- package/app/assets/img/moji_weather/W17.png +0 -0
- package/app/assets/img/moji_weather/W18.png +0 -0
- package/app/assets/img/moji_weather/W19.png +0 -0
- package/app/assets/img/moji_weather/W2.png +0 -0
- package/app/assets/img/moji_weather/W20.png +0 -0
- package/app/assets/img/moji_weather/W29.png +0 -0
- package/app/assets/img/moji_weather/W3.png +0 -0
- package/app/assets/img/moji_weather/W30.png +0 -0
- package/app/assets/img/moji_weather/W31.png +0 -0
- package/app/assets/img/moji_weather/W32.png +0 -0
- package/app/assets/img/moji_weather/W33.png +0 -0
- package/app/assets/img/moji_weather/W34.png +0 -0
- package/app/assets/img/moji_weather/W35.png +0 -0
- package/app/assets/img/moji_weather/W36.png +0 -0
- package/app/assets/img/moji_weather/W4.png +0 -0
- package/app/assets/img/moji_weather/W44.png +0 -0
- package/app/assets/img/moji_weather/W45.png +0 -0
- package/app/assets/img/moji_weather/W46.png +0 -0
- package/app/assets/img/moji_weather/W5.png +0 -0
- package/app/assets/img/moji_weather/W6.png +0 -0
- package/app/assets/img/moji_weather/W7.png +0 -0
- package/app/assets/img/moji_weather/W8.png +0 -0
- package/app/assets/img/moji_weather/W9.png +0 -0
- package/app/assets/img/voice-white.png +0 -0
- package/app/assets/img/voice.png +0 -0
- package/app/assets/img/voice_wave1.png +0 -0
- package/app/assets/img/voice_wave2.png +0 -0
- package/app/assets/img/weather_bg.png +0 -0
- package/app/base.js +67 -9
- package/app/components/common/Cell/index.js +1 -1
- package/app/components/common/Dialog/prompt.js +2 -0
- package/app/hooks/useStreamingText.js +222 -0
- package/app/screens/BaseScreen.js +95 -222
- package/app/screens/ChatModeScreen/ChatTemplates/ActionPrompt/PromptContents/alarm.js +33 -0
- package/app/screens/ChatModeScreen/ChatTemplates/ActionPrompt/PromptContents/index.js +11 -0
- package/app/screens/ChatModeScreen/ChatTemplates/ActionPrompt/PromptContents/itee_control_report.js +32 -0
- package/app/screens/ChatModeScreen/ChatTemplates/ActionPrompt/PromptContents/itee_state_change.js +41 -0
- package/app/screens/ChatModeScreen/ChatTemplates/ActionPrompt/PromptContents/itee_upgrade_status.js +31 -0
- package/app/screens/ChatModeScreen/ChatTemplates/ActionPrompt/index.js +240 -0
- package/app/screens/ChatModeScreen/ChatTemplates/AiAnswer/index.js +116 -0
- package/app/screens/ChatModeScreen/ChatTemplates/DeviceFaq/index.js +202 -0
- package/app/screens/ChatModeScreen/ChatTemplates/DeviceStatus/index.js +422 -0
- package/app/screens/ChatModeScreen/ChatTemplates/Link/index.js +62 -0
- package/app/screens/ChatModeScreen/ChatTemplates/SceneReport/index.js +526 -0
- package/app/screens/ChatModeScreen/ChatTemplates/SportHealthReport/index.js +298 -0
- package/app/screens/ChatModeScreen/ChatTemplates/Weather/index.js +263 -0
- package/app/screens/ChatModeScreen/ChatTemplates/commonStyle.js +63 -0
- package/app/screens/ChatModeScreen/ChatTemplates/index.js +116 -0
- package/app/screens/ChatModeScreen/chat/MyChat.js +423 -0
- package/app/screens/ChatModeScreen/components/ChatInputBar/README.md +102 -0
- package/app/screens/ChatModeScreen/components/ChatInputBar/TabBarInput.js +222 -0
- package/app/screens/ChatModeScreen/components/ChatInputBar/doc.md +920 -0
- package/app/screens/ChatModeScreen/components/ChatInputBar/index.js +1066 -0
- package/app/screens/ChatModeScreen/components/ChatMessage/index.js +321 -0
- package/app/screens/ChatModeScreen/components/EditableText/index.js +59 -0
- package/app/screens/ChatModeScreen/components/ErrorBoundary/index.js +108 -0
- package/app/screens/ChatModeScreen/components/IncrementalMarkdownText/index.js +566 -0
- package/app/screens/ChatModeScreen/components/SimpleProfileScreen.js +224 -0
- package/app/screens/ChatModeScreen/index.js +150 -0
- package/app/screens/CommonScreens/BossScreen/index.js +54 -5
- package/app/screens/CommonScreens/EditInvoiceTitleScreen/index.js +1 -0
- package/app/screens/CommonScreens/FamilyDetailScreen/index.js +1 -1
- package/app/screens/CommonScreens/HomedLoginScreen/index.js +2 -1
- package/app/screens/LoginScreen/index.js +1555 -571
- package/app/screens/LoginScreen/indexSbk.js +950 -0
- package/app/screens/RegisteCountScreen/index.js +41 -39
- package/app/screens/WebviewScreen.js +216 -11
- package/app/utils/common.js +131 -0
- package/app/utils/http.js +33 -27
- package/app/utils/navigationPackageUtils.js +275 -0
- package/bin/local-dev.js +8 -0
- package/package.json +3 -3
package/app/App.js
CHANGED
|
@@ -40,15 +40,19 @@ import storage from "./storage";
|
|
|
40
40
|
|
|
41
41
|
import { navigationRef } from "./utils/RootNavigation";
|
|
42
42
|
import checkLogin from "./hooks/checkLogin";
|
|
43
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
getModuleVersionInfo,
|
|
45
|
+
getUserInfo,
|
|
46
|
+
delThirdId,
|
|
47
|
+
} from "./api/ipanelCloudApi";
|
|
44
48
|
import { appReport } from "./utils/common";
|
|
45
49
|
|
|
46
50
|
// 基础导航页、登录页
|
|
47
51
|
import BaseScreen from "./screens/BaseScreen";
|
|
48
|
-
import SimpleBaseScreen from "./screens/SimpleBaseScreen.js"
|
|
49
|
-
import LoginScreen from "./screens/LoginScreen";
|
|
50
|
-
import
|
|
51
|
-
import LoginEduScreen from "./screens/LoginScreen/indexEdu"
|
|
52
|
+
import SimpleBaseScreen from "./screens/SimpleBaseScreen.js"; //简单导航:非爱家宝
|
|
53
|
+
import LoginScreen from "./screens/LoginScreen/index";
|
|
54
|
+
import SbkLoginScreen from "./screens/LoginScreen/indexSbk"; // 声必可屏登录页
|
|
55
|
+
import LoginEduScreen from "./screens/LoginScreen/indexEdu"; //教鞭登录页
|
|
52
56
|
import ResePasswordScreen from "./screens/ResePassword";
|
|
53
57
|
import SbkScreen from "./screens/SbkScreen";
|
|
54
58
|
|
|
@@ -103,6 +107,9 @@ import DeviceAnswerDetailScreen from "./screens/CommonScreens/DeviceAnswerDetail
|
|
|
103
107
|
// Boss绑定页面
|
|
104
108
|
import BindBossScreen from "./screens/CommonScreens/BossScreen";
|
|
105
109
|
|
|
110
|
+
// ChatModeScreen专用简化版"我的"页面
|
|
111
|
+
import SimpleProfileScreen from "./screens/ChatModeScreen/components/SimpleProfileScreen";
|
|
112
|
+
|
|
106
113
|
const Stack = createStackNavigator();
|
|
107
114
|
|
|
108
115
|
const envVersion = NativeModules?.RNToNativeEmitter?.serverCluster || "alpha";
|
|
@@ -121,7 +128,7 @@ if (Platform.OS === "android") {
|
|
|
121
128
|
// 公共 栈路由页面
|
|
122
129
|
const CommonScreens = {
|
|
123
130
|
Home: {
|
|
124
|
-
component: appName===
|
|
131
|
+
component: appName === "教鞭" ? SimpleBaseScreen : BaseScreen,
|
|
125
132
|
screenOptions: {
|
|
126
133
|
title: "首页",
|
|
127
134
|
headerShown: false,
|
|
@@ -129,7 +136,12 @@ const CommonScreens = {
|
|
|
129
136
|
},
|
|
130
137
|
},
|
|
131
138
|
Login: {
|
|
132
|
-
component:
|
|
139
|
+
component:
|
|
140
|
+
appName === "教鞭"
|
|
141
|
+
? LoginEduScreen
|
|
142
|
+
: DeviceInfo.getBrand() === "Allwinner"
|
|
143
|
+
? SbkLoginScreen /* 声必可屏 */
|
|
144
|
+
: LoginScreen,
|
|
133
145
|
screenOptions: {
|
|
134
146
|
title: "登录",
|
|
135
147
|
headerShown: false,
|
|
@@ -224,6 +236,13 @@ const CommonScreens = {
|
|
|
224
236
|
headerShown: false,
|
|
225
237
|
},
|
|
226
238
|
},
|
|
239
|
+
SimpleProfileScreen: {
|
|
240
|
+
component: SimpleProfileScreen,
|
|
241
|
+
screenOptions: {
|
|
242
|
+
title: "我的",
|
|
243
|
+
headerShown: false,
|
|
244
|
+
},
|
|
245
|
+
},
|
|
227
246
|
};
|
|
228
247
|
|
|
229
248
|
// screen分组
|
|
@@ -18,8 +18,63 @@ export const ADG_SERVICE = `${config.IPANEL_CLOUD_BASE_ADG_URI}/s/install_ea`;
|
|
|
18
18
|
|
|
19
19
|
export const APARTMENT_SERVICE = `${config.IPANEL_CLOUD_BASE_URI}/s/smart_home_manage/admin-api`;
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const CLOUDHOMECENTER_URL = `${config.IPANEL_CLOUD_BASE_URI}/s/smart_home/cloudhomecenter`;
|
|
22
|
+
|
|
23
|
+
// 获取百度ASR token,支持缓存和自动刷新
|
|
24
|
+
export const getBaiduAsrToken = async () => {
|
|
25
|
+
const BAIDU_ASR_TOKEN_KEY = 'baiduasrtoken';
|
|
26
|
+
const {storage} = global.base;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const cachedTokenInfo = await storage.get(BAIDU_ASR_TOKEN_KEY);
|
|
30
|
+
// 减去一个缓冲时间,比如5分钟,提前刷新
|
|
31
|
+
if (
|
|
32
|
+
cachedTokenInfo &&
|
|
33
|
+
cachedTokenInfo.accessToken &&
|
|
34
|
+
cachedTokenInfo.expiresAt > Date.now() - 5 * 60 * 1000
|
|
35
|
+
) {
|
|
36
|
+
return cachedTokenInfo.accessToken;
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('Get baidu asr token from storage error', error);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const result = await request({
|
|
43
|
+
url: CLOUDHOMECENTER_URL,
|
|
44
|
+
method: 'POST',
|
|
45
|
+
data: {
|
|
46
|
+
path: 'asrproxy/token',
|
|
47
|
+
},
|
|
48
|
+
}).catch(err => {
|
|
49
|
+
console.error('Failed to fetch Baidu ASR token', err);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (result && result.code === 0) {
|
|
53
|
+
const tokenData = result.result;
|
|
54
|
+
|
|
55
|
+
if (tokenData && tokenData.access_token && tokenData.expires_in) {
|
|
56
|
+
const expiresAt = Date.now() + tokenData.expires_in * 1000; // expires_in是秒,转换为毫秒
|
|
57
|
+
try {
|
|
58
|
+
await storage.set(BAIDU_ASR_TOKEN_KEY, {
|
|
59
|
+
accessToken: tokenData.access_token,
|
|
60
|
+
expiresAt: expiresAt,
|
|
61
|
+
});
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('Set baidu asr token to storage error', error);
|
|
64
|
+
}
|
|
65
|
+
return tokenData.access_token;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return '';
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// 百度ASR语音识别服务,支持动态token获取
|
|
72
|
+
export const baiduAsr = async (cuid, len, speechBase64) => {
|
|
73
|
+
const token = await getBaiduAsrToken();
|
|
74
|
+
if (!token) {
|
|
75
|
+
throw new Error('Failed to obtain Baidu ASR token');
|
|
76
|
+
}
|
|
77
|
+
|
|
23
78
|
return request({
|
|
24
79
|
url: "https://vop.baidu.com/pro_api", // 语音识别极速版调用地址
|
|
25
80
|
method: "POST",
|
|
@@ -28,8 +83,7 @@ export const baiduAsr = (cuid, len, speechBase64) => {
|
|
|
28
83
|
rate: 16000,
|
|
29
84
|
dev_pid: 80001,
|
|
30
85
|
channel: 1,
|
|
31
|
-
token:
|
|
32
|
-
"24.5298600e65d054e08a1290fcc75f174f.2592000.1691141832.282335-17005893",
|
|
86
|
+
token: token,
|
|
33
87
|
cuid: cuid,
|
|
34
88
|
len,
|
|
35
89
|
speech: speechBase64, // xxx为 base64(FILE_CONTENT)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/app/base.js
CHANGED
|
@@ -385,6 +385,65 @@ global.base.getBundleInfo = (moduleName) => {
|
|
|
385
385
|
});
|
|
386
386
|
};
|
|
387
387
|
|
|
388
|
+
/**
|
|
389
|
+
* 加载包并注册组件的公共函数
|
|
390
|
+
* @param {Array} packages - 要加载的包数组
|
|
391
|
+
* @param {Object} options - 配置选项
|
|
392
|
+
* @param {boolean} options.registerStackScreens - 是否注册StackScreens,默认true
|
|
393
|
+
* @param {boolean} options.showErrorToast - 是否显示错误Toast,默认true
|
|
394
|
+
* @param {string} options.errorPrefix - 错误日志前缀,默认为空
|
|
395
|
+
* @returns {Promise<{success: boolean, components: Array}>} 加载结果
|
|
396
|
+
*/
|
|
397
|
+
global.base.loadAndRegisterPackages = async (packages, options = {}) => {
|
|
398
|
+
const {
|
|
399
|
+
registerStackScreens = true,
|
|
400
|
+
showErrorToast = true,
|
|
401
|
+
errorPrefix = '',
|
|
402
|
+
} = options;
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
// 1. 加载包
|
|
406
|
+
const loadFlag = await global.base
|
|
407
|
+
.loadPackages({ packages })
|
|
408
|
+
.catch((errors) => {
|
|
409
|
+
console.log(`${errorPrefix}包加载失败:`, JSON.stringify(errors));
|
|
410
|
+
return false;
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
if (!loadFlag) {
|
|
414
|
+
if (showErrorToast) {
|
|
415
|
+
global.toast?.showToast("网络繁忙");
|
|
416
|
+
}
|
|
417
|
+
console.log(`${errorPrefix}包加载失败`);
|
|
418
|
+
return { success: false, components: [] };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 2. 获取远程组件
|
|
422
|
+
const componentPromises = packages.map((pack) =>
|
|
423
|
+
global.base.getRemoteComponentByName(pack.moduleName)
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
const components = await Promise.all(componentPromises);
|
|
427
|
+
|
|
428
|
+
// 3. 注册组件(如果需要)
|
|
429
|
+
if (registerStackScreens) {
|
|
430
|
+
components.forEach((component) => {
|
|
431
|
+
typeof component?.registerStackScreens === "function" &&
|
|
432
|
+
component.registerStackScreens();
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
console.log(`${errorPrefix}包加载和组件注册成功`);
|
|
437
|
+
return { success: true, components };
|
|
438
|
+
} catch (error) {
|
|
439
|
+
console.log(`${errorPrefix}loadAndRegisterPackages失败:`, error);
|
|
440
|
+
if (showErrorToast) {
|
|
441
|
+
global.toast?.showToast("加载失败");
|
|
442
|
+
}
|
|
443
|
+
return { success: false, components: [] };
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
|
|
388
447
|
// 动态加载指定模块并注册为独立页面,然后跳转
|
|
389
448
|
global.base.navigateModule = async ({
|
|
390
449
|
operatorId,
|
|
@@ -425,20 +484,19 @@ global.base.navigateModule = async ({
|
|
|
425
484
|
|
|
426
485
|
const packages = result.data.packages;
|
|
427
486
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
});
|
|
487
|
+
// 使用公共函数加载包,但不注册StackScreens(使用特殊的GlobalStackScreen注册)
|
|
488
|
+
const loadResult = await global.base.loadAndRegisterPackages(packages, {
|
|
489
|
+
registerStackScreens: false, // navigateModule使用特殊的注册方式
|
|
490
|
+
showErrorToast: true,
|
|
491
|
+
errorPrefix: `动态加载${moduleName}`,
|
|
492
|
+
});
|
|
435
493
|
|
|
436
|
-
if (!
|
|
437
|
-
global.toast.showToast("网络繁忙");
|
|
494
|
+
if (!loadResult.success) {
|
|
438
495
|
console.log("网络繁忙");
|
|
439
496
|
return false;
|
|
440
497
|
}
|
|
441
498
|
|
|
499
|
+
// 使用navigateModule专用的注册方式
|
|
442
500
|
const registerFlag =
|
|
443
501
|
await global.base.registerRemoteComponentGlobalStackScreen(moduleName, {
|
|
444
502
|
title: packages[0].moduleChineseName,
|
|
@@ -96,6 +96,7 @@ const Prompt = ({ modelRef, setModalVisible }) => {
|
|
|
96
96
|
autoFocus={!!modelRef?.autoFocus}
|
|
97
97
|
onChangeText={setInput}
|
|
98
98
|
keyboardType={modelRef?.keyboardType}
|
|
99
|
+
returnKeyType={modelRef?.returnKeyType||'done'}
|
|
99
100
|
leftIcon={modelRef?.leftIcon}
|
|
100
101
|
containerStyle={{
|
|
101
102
|
height: 31,
|
|
@@ -148,6 +149,7 @@ const Prompt = ({ modelRef, setModalVisible }) => {
|
|
|
148
149
|
autoFocus={!!modelRef?.autoFocus2}
|
|
149
150
|
onChangeText={setInput2}
|
|
150
151
|
keyboardType={modelRef?.keyboardType2}
|
|
152
|
+
returnKeyType={modelRef?.returnKeyType2||'done'}
|
|
151
153
|
leftIcon={modelRef?.leftIcon2}
|
|
152
154
|
containerStyle={{
|
|
153
155
|
height: 31,
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import {useState, useRef, useEffect, useCallback} from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 流式文本状态管理Hook
|
|
5
|
+
* 专门用于处理智能客服的实时文本流
|
|
6
|
+
*/
|
|
7
|
+
export default function useStreamingText() {
|
|
8
|
+
const [streamingMessages, setStreamingMessages] = useState(new Map());
|
|
9
|
+
const streamBufferRef = useRef(new Map());
|
|
10
|
+
const streamTimerRef = useRef(new Map());
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 更新流式消息内容
|
|
14
|
+
* @param {string} messageId - 消息ID
|
|
15
|
+
* @param {string} content - 新的内容
|
|
16
|
+
* @param {boolean} isComplete - 是否完成
|
|
17
|
+
*/
|
|
18
|
+
const updateStreamingMessage = useCallback(
|
|
19
|
+
(messageId, content, isComplete = false) => {
|
|
20
|
+
setStreamingMessages(prev => {
|
|
21
|
+
const newMap = new Map(prev);
|
|
22
|
+
newMap.set(messageId, {
|
|
23
|
+
content,
|
|
24
|
+
isComplete,
|
|
25
|
+
lastUpdate: Date.now(),
|
|
26
|
+
});
|
|
27
|
+
return newMap;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// 如果消息完成,清理相关的缓冲区和定时器
|
|
31
|
+
if (isComplete) {
|
|
32
|
+
streamBufferRef.current.delete(messageId);
|
|
33
|
+
if (streamTimerRef.current.has(messageId)) {
|
|
34
|
+
clearTimeout(streamTimerRef.current.get(messageId));
|
|
35
|
+
streamTimerRef.current.delete(messageId);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
[],
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 处理增量文本数据
|
|
44
|
+
* @param {string} messageId - 消息ID
|
|
45
|
+
* @param {string} newChunk - 新的文本块
|
|
46
|
+
* @param {number} debounceMs - 防抖时间(毫秒)
|
|
47
|
+
*/
|
|
48
|
+
const appendStreamingText = useCallback(
|
|
49
|
+
(messageId, newChunk, debounceMs = 100) => {
|
|
50
|
+
// 更新缓冲区
|
|
51
|
+
const currentBuffer = streamBufferRef.current.get(messageId) || '';
|
|
52
|
+
const updatedContent = currentBuffer + newChunk;
|
|
53
|
+
streamBufferRef.current.set(messageId, updatedContent);
|
|
54
|
+
|
|
55
|
+
// 清除之前的定时器
|
|
56
|
+
if (streamTimerRef.current.has(messageId)) {
|
|
57
|
+
clearTimeout(streamTimerRef.current.get(messageId));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 设置新的防抖定时器
|
|
61
|
+
const timer = setTimeout(() => {
|
|
62
|
+
updateStreamingMessage(messageId, updatedContent, false);
|
|
63
|
+
}, debounceMs);
|
|
64
|
+
|
|
65
|
+
streamTimerRef.current.set(messageId, timer);
|
|
66
|
+
},
|
|
67
|
+
[updateStreamingMessage],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 完成流式消息
|
|
72
|
+
* @param {string} messageId - 消息ID
|
|
73
|
+
* @param {string} finalContent - 最终内容(可选,如果不提供则使用缓冲区内容)
|
|
74
|
+
*/
|
|
75
|
+
const completeStreamingMessage = useCallback(
|
|
76
|
+
(messageId, finalContent = null) => {
|
|
77
|
+
const content =
|
|
78
|
+
finalContent || streamBufferRef.current.get(messageId) || '';
|
|
79
|
+
updateStreamingMessage(messageId, content, true);
|
|
80
|
+
},
|
|
81
|
+
[updateStreamingMessage],
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 获取流式消息状态
|
|
86
|
+
* @param {string} messageId - 消息ID
|
|
87
|
+
* @returns {Object|null} 消息状态对象
|
|
88
|
+
*/
|
|
89
|
+
const getStreamingMessage = useCallback(
|
|
90
|
+
messageId => {
|
|
91
|
+
return streamingMessages.get(messageId) || null;
|
|
92
|
+
},
|
|
93
|
+
[streamingMessages],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 清除特定的流式消息
|
|
98
|
+
* @param {string} messageId - 消息ID
|
|
99
|
+
*/
|
|
100
|
+
const clearStreamingMessage = useCallback(messageId => {
|
|
101
|
+
setStreamingMessages(prev => {
|
|
102
|
+
const newMap = new Map(prev);
|
|
103
|
+
newMap.delete(messageId);
|
|
104
|
+
return newMap;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
streamBufferRef.current.delete(messageId);
|
|
108
|
+
if (streamTimerRef.current.has(messageId)) {
|
|
109
|
+
clearTimeout(streamTimerRef.current.get(messageId));
|
|
110
|
+
streamTimerRef.current.delete(messageId);
|
|
111
|
+
}
|
|
112
|
+
}, []);
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 清除所有流式消息
|
|
116
|
+
*/
|
|
117
|
+
const clearAllStreamingMessages = useCallback(() => {
|
|
118
|
+
setStreamingMessages(new Map());
|
|
119
|
+
streamBufferRef.current.clear();
|
|
120
|
+
|
|
121
|
+
// 清理所有定时器
|
|
122
|
+
streamTimerRef.current.forEach(timer => clearTimeout(timer));
|
|
123
|
+
streamTimerRef.current.clear();
|
|
124
|
+
}, []);
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 处理chat类型消息的流式更新
|
|
128
|
+
* @param {Object} chatMessage - chat类型的消息对象
|
|
129
|
+
*/
|
|
130
|
+
const handleChatMessage = useCallback(
|
|
131
|
+
chatMessage => {
|
|
132
|
+
const messageId = chatMessage.MsgId;
|
|
133
|
+
|
|
134
|
+
switch (chatMessage.Chat.type) {
|
|
135
|
+
case 'data':
|
|
136
|
+
// 累积式流数据
|
|
137
|
+
const totalData = chatMessage.Chat.payload.total_data || '';
|
|
138
|
+
updateStreamingMessage(messageId, totalData, false);
|
|
139
|
+
break;
|
|
140
|
+
|
|
141
|
+
case 'end':
|
|
142
|
+
// 流结束
|
|
143
|
+
let finalContent = '';
|
|
144
|
+
if (chatMessage.Chat.payload.message.tool_calls) {
|
|
145
|
+
finalContent = `${
|
|
146
|
+
chatMessage.Chat.payload.message.content || ''
|
|
147
|
+
}<正在调用 ${
|
|
148
|
+
chatMessage.Chat.payload.message.tool_calls[0].function.name
|
|
149
|
+
} 工具>`;
|
|
150
|
+
} else {
|
|
151
|
+
finalContent = chatMessage.Chat.payload.message.content || '';
|
|
152
|
+
}
|
|
153
|
+
completeStreamingMessage(messageId, finalContent);
|
|
154
|
+
break;
|
|
155
|
+
|
|
156
|
+
case 'tool_call_begin':
|
|
157
|
+
completeStreamingMessage(
|
|
158
|
+
messageId,
|
|
159
|
+
`<正在调用 ${chatMessage.Chat.payload.tool_calls[0].function.name} 工具>`,
|
|
160
|
+
);
|
|
161
|
+
break;
|
|
162
|
+
|
|
163
|
+
case 'tool_call_end':
|
|
164
|
+
completeStreamingMessage(messageId, '<工具调用结束>');
|
|
165
|
+
break;
|
|
166
|
+
|
|
167
|
+
case 'info':
|
|
168
|
+
let infoContent = '';
|
|
169
|
+
switch (chatMessage.Chat.payload.message) {
|
|
170
|
+
case 'processing':
|
|
171
|
+
infoContent = 'AI正在思考...';
|
|
172
|
+
break;
|
|
173
|
+
case 'stoped':
|
|
174
|
+
infoContent = '<用户主动停止>';
|
|
175
|
+
break;
|
|
176
|
+
default:
|
|
177
|
+
infoContent = chatMessage.Chat.payload.message;
|
|
178
|
+
}
|
|
179
|
+
completeStreamingMessage(messageId, infoContent);
|
|
180
|
+
break;
|
|
181
|
+
|
|
182
|
+
case 'system_error':
|
|
183
|
+
completeStreamingMessage(
|
|
184
|
+
messageId,
|
|
185
|
+
chatMessage.Chat.payload.message.content,
|
|
186
|
+
);
|
|
187
|
+
break;
|
|
188
|
+
|
|
189
|
+
case 'error':
|
|
190
|
+
completeStreamingMessage(
|
|
191
|
+
messageId,
|
|
192
|
+
chatMessage.Chat.payload.message.error,
|
|
193
|
+
);
|
|
194
|
+
break;
|
|
195
|
+
|
|
196
|
+
default:
|
|
197
|
+
completeStreamingMessage(messageId, JSON.stringify(chatMessage.Chat));
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
[updateStreamingMessage, completeStreamingMessage],
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// 清理效果
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
const currentTimers = streamTimerRef.current;
|
|
206
|
+
return () => {
|
|
207
|
+
// 组件卸载时清理所有定时器
|
|
208
|
+
currentTimers.forEach(timer => clearTimeout(timer));
|
|
209
|
+
};
|
|
210
|
+
}, []);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
streamingMessages,
|
|
214
|
+
updateStreamingMessage,
|
|
215
|
+
appendStreamingText,
|
|
216
|
+
completeStreamingMessage,
|
|
217
|
+
getStreamingMessage,
|
|
218
|
+
clearStreamingMessage,
|
|
219
|
+
clearAllStreamingMessages,
|
|
220
|
+
handleChatMessage,
|
|
221
|
+
};
|
|
222
|
+
}
|