@liujitcn/kratos-taro-app-system 0.0.58 → 0.0.60
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/package.json +3 -3
- package/src/api/base/v1/ai_message.ts +3 -2
- package/src/notification.ts +13 -4
- package/src/views/pages/my/my.tsx +1 -1
- package/src/views/pagesMember/ai/index.tsx +33 -6
- package/src/views/pagesMember/message/detail.tsx +1 -1
- package/src/views/pagesMember/message/index.tsx +23 -6
- package/src/views/pagesMember/profile/profile.tsx +21 -6
- package/src/views/pagesMember/settings/settings.tsx +14 -3
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git@github.com:liujitcn/kratos-admin.git"
|
|
9
9
|
},
|
|
10
|
-
"version": "0.0.
|
|
10
|
+
"version": "0.0.60",
|
|
11
11
|
"type": "module",
|
|
12
12
|
"files": [
|
|
13
13
|
"dist",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"@tarojs/components": "4.2.1",
|
|
39
39
|
"@tarojs/taro": "4.2.1",
|
|
40
40
|
"react": "18.3.1",
|
|
41
|
-
"@liujitcn/kratos-taro-app-core": "^0.0.
|
|
42
|
-
"@liujitcn/kratos-taro-app-ui": "^0.0.
|
|
41
|
+
"@liujitcn/kratos-taro-app-core": "^0.0.60",
|
|
42
|
+
"@liujitcn/kratos-taro-app-ui": "^0.0.60"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "pnpm run build:entries && pnpm pack --pack-destination ../../../dist/npm",
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
handleAuthExpired,
|
|
4
4
|
http,
|
|
5
5
|
requestBaseURL,
|
|
6
|
+
sourceClient,
|
|
6
7
|
} from '@liujitcn/kratos-taro-app-core/utils/http'
|
|
7
8
|
import { getLocaleRequestHeaders, t } from '@liujitcn/kratos-taro-app-core'
|
|
8
9
|
import Taro from '@tarojs/taro'
|
|
@@ -76,7 +77,7 @@ export async function SendAiMessageStream(
|
|
|
76
77
|
const headers: Record<string, string> = {
|
|
77
78
|
Accept: 'text/event-stream',
|
|
78
79
|
'Content-Type': 'application/json;charset=utf-8',
|
|
79
|
-
'source-client':
|
|
80
|
+
'source-client': sourceClient,
|
|
80
81
|
...getLocaleRequestHeaders(),
|
|
81
82
|
}
|
|
82
83
|
if (accessToken) {
|
|
@@ -130,7 +131,7 @@ export function StreamAiMessageByChunkedRequest(
|
|
|
130
131
|
header: {
|
|
131
132
|
Accept: 'text/event-stream',
|
|
132
133
|
'Content-Type': 'application/json;charset=utf-8',
|
|
133
|
-
'source-client':
|
|
134
|
+
'source-client': sourceClient,
|
|
134
135
|
...getLocaleRequestHeaders(),
|
|
135
136
|
...(accessToken ? { Authorization: accessToken } : {}),
|
|
136
137
|
},
|
package/src/notification.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { getLocaleRequestHeaders } from '@liujitcn/kratos-taro-app-core'
|
|
1
2
|
import { setAppMenuBadge } from '@liujitcn/kratos-taro-app-core/navigation'
|
|
2
3
|
import { getToken } from '@liujitcn/kratos-taro-app-core/utils/auth'
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
getRequestAccessToken,
|
|
6
|
+
siteBaseURL,
|
|
7
|
+
sourceClient,
|
|
8
|
+
} from '@liujitcn/kratos-taro-app-core/utils/http'
|
|
4
9
|
import { defNotificationService } from './api/base/v1/notification'
|
|
5
10
|
|
|
6
11
|
/** System 模块共享的站内信未读数。 */
|
|
@@ -71,9 +76,13 @@ function startNotificationSse(): () => void {
|
|
|
71
76
|
try {
|
|
72
77
|
const token = await getRequestAccessToken()
|
|
73
78
|
if (!token || controller.signal.aborted) return
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
const response = await fetch(`${siteBaseURL}/events/base.notification`, {
|
|
80
|
+
headers: {
|
|
81
|
+
Accept: 'text/event-stream',
|
|
82
|
+
Authorization: token,
|
|
83
|
+
'source-client': sourceClient,
|
|
84
|
+
...getLocaleRequestHeaders(),
|
|
85
|
+
},
|
|
77
86
|
signal: controller.signal,
|
|
78
87
|
})
|
|
79
88
|
if (response.status === 401 || response.status === 403) return
|
|
@@ -190,6 +190,13 @@ export default function AiPage() {
|
|
|
190
190
|
|
|
191
191
|
const isSessionSending = (sessionID: string) => Boolean(sessionID && sendingSessionMapRef.current[sessionID])
|
|
192
192
|
|
|
193
|
+
/** 等待指定会话的历史加载完成,避免发送消息与加载竞态。 */
|
|
194
|
+
const waitForMessagesLoaded = async (sessionID: string) => {
|
|
195
|
+
while (loadingSessionIDRef.current === sessionID) {
|
|
196
|
+
await new Promise((resolve) => setTimeout(resolve, 16))
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
193
200
|
const scrollChatToBottom = () => {
|
|
194
201
|
setChatBottomAnchor('')
|
|
195
202
|
if (scrollTimer.current) clearTimeout(scrollTimer.current)
|
|
@@ -292,8 +299,12 @@ export default function AiPage() {
|
|
|
292
299
|
const ensureActiveSession = async () => {
|
|
293
300
|
if (activeSessionIDRef.current) return activeSessionIDRef.current
|
|
294
301
|
if (sessionsRef.current.length > 0) {
|
|
295
|
-
|
|
296
|
-
|
|
302
|
+
const sessionID = sessionsRef.current[0].id
|
|
303
|
+
setActiveSessionID(sessionID)
|
|
304
|
+
if (!messagesRef.current[sessionID]?.length) {
|
|
305
|
+
await loadMessages(sessionID)
|
|
306
|
+
}
|
|
307
|
+
return sessionID
|
|
297
308
|
}
|
|
298
309
|
const sessionID = await createRemoteSession()
|
|
299
310
|
if (sessionID) {
|
|
@@ -309,10 +320,13 @@ export default function AiPage() {
|
|
|
309
320
|
try {
|
|
310
321
|
const response = await defAiSessionService.ListAiMessage({ session_id: sessionID })
|
|
311
322
|
if (loadingSessionIDRef.current !== sessionID) return
|
|
312
|
-
|
|
323
|
+
const loaded = normalizeMessageList(response.messages)
|
|
324
|
+
updateSessionMessages(sessionID, (current) => mergeWithLocalOnlyStreaming(current, loaded))
|
|
313
325
|
if (activeSessionIDRef.current === sessionID) scrollChatToBottom()
|
|
314
326
|
} catch (error) {
|
|
315
|
-
if (loadingSessionIDRef.current === sessionID)
|
|
327
|
+
if (loadingSessionIDRef.current === sessionID) {
|
|
328
|
+
updateSessionMessages(sessionID, (current) => mergeWithLocalOnlyStreaming(current, []))
|
|
329
|
+
}
|
|
316
330
|
showError(error, t('system.ai.load_messages_failed'))
|
|
317
331
|
} finally {
|
|
318
332
|
if (loadingSessionIDRef.current === sessionID) setLoadingSessionID('')
|
|
@@ -325,8 +339,7 @@ export default function AiPage() {
|
|
|
325
339
|
try {
|
|
326
340
|
const response = await defAiSessionService.ListAiSession({ terminal: AI_TERMINAL })
|
|
327
341
|
setSessions(normalizeSessionList(response.sessions))
|
|
328
|
-
|
|
329
|
-
if (sessionID) await loadMessages(sessionID)
|
|
342
|
+
await ensureActiveSession()
|
|
330
343
|
} catch (error) {
|
|
331
344
|
showError(error, t('system.ai.load_sessions_failed'))
|
|
332
345
|
} finally {
|
|
@@ -426,6 +439,8 @@ export default function AiPage() {
|
|
|
426
439
|
const sendAiPayload = async (payload: { text: string; attachments: AiAttachment[] }) => {
|
|
427
440
|
const sessionID = await ensureActiveSession()
|
|
428
441
|
if (!sessionID || isSessionSending(sessionID)) return false
|
|
442
|
+
// 发送前等待进行中的历史加载结束,避免本地消息与历史加载竞态。
|
|
443
|
+
await waitForMessagesLoaded(sessionID)
|
|
429
444
|
updateSessionMessages(sessionID, (current) =>
|
|
430
445
|
sortMessages([...current, createLocalUserMessage(payload), createThinkingMessage({ sessionID })]),
|
|
431
446
|
)
|
|
@@ -772,6 +787,18 @@ function normalizeMessageList(list?: AiMessage[] | null) {
|
|
|
772
787
|
return sortMessages(list.filter(Boolean).flatMap((item) => [mapMessageItem(item, 'user'), mapMessageItem(item, 'ai')]))
|
|
773
788
|
}
|
|
774
789
|
|
|
790
|
+
/** 合并历史加载结果与仍在流式中的本地消息,避免加载覆盖进行中的对话。 */
|
|
791
|
+
function mergeWithLocalOnlyStreaming(current: ChatMessageItem[], loaded: ChatMessageItem[]) {
|
|
792
|
+
const localStreaming = current.filter(
|
|
793
|
+
(item) => item.localOnly && item.status === AiMessageStatus.AI_MESSAGE_STATUS_GENERATING,
|
|
794
|
+
)
|
|
795
|
+
if (!localStreaming.length) return loaded
|
|
796
|
+
const messageMap = new Map<string, ChatMessageItem>()
|
|
797
|
+
for (const item of loaded) messageMap.set(item.key, item)
|
|
798
|
+
for (const item of localStreaming) messageMap.set(item.key, item)
|
|
799
|
+
return sortMessages(Array.from(messageMap.values()))
|
|
800
|
+
}
|
|
801
|
+
|
|
775
802
|
function hasSuccessfulAiMessages(list: ChatMessageItem[]) {
|
|
776
803
|
return list.some((item) => item.status === AiMessageStatus.AI_MESSAGE_STATUS_SUCCESS)
|
|
777
804
|
}
|
|
@@ -46,7 +46,7 @@ export default function MessageDetailPage() {
|
|
|
46
46
|
navigateAppView(detail.action_target, params)
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
if (!detail) return <View className='empty'>{t('
|
|
49
|
+
if (!detail) return <View className='empty'>{t('core.status.loading')}</View>
|
|
50
50
|
return (
|
|
51
51
|
<View className='detail-page'>
|
|
52
52
|
<Text className='detail-title'>{detail.title}</Text>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useDidShow } from '@tarojs/taro'
|
|
2
2
|
import { Button, ScrollView, Text, View } from '@tarojs/components'
|
|
3
|
-
import { useState } from 'react'
|
|
3
|
+
import { useRef, useState } from 'react'
|
|
4
4
|
import { navigateAppView, t } from '@liujitcn/kratos-taro-app-core'
|
|
5
5
|
import { UniIcon } from '@liujitcn/kratos-taro-app-ui'
|
|
6
6
|
import { defNotificationService } from '../../../api/base/v1/notification'
|
|
@@ -22,6 +22,9 @@ export default function MessageInboxPage() {
|
|
|
22
22
|
const [categoryOptions, setCategoryOptions] = useState<
|
|
23
23
|
NotificationCategory[]
|
|
24
24
|
>([])
|
|
25
|
+
const requestSeqRef = useRef(0)
|
|
26
|
+
const loadingRef = useRef(false)
|
|
27
|
+
const finishedRef = useRef(false)
|
|
25
28
|
const viewOptions = [
|
|
26
29
|
{ value: NotificationView.NOTIFICATION_VIEW_INBOX, key: 'system.notification.view.inbox' },
|
|
27
30
|
{ value: NotificationView.NOTIFICATION_VIEW_UNREAD, key: 'system.notification.view.unread' },
|
|
@@ -47,6 +50,9 @@ export default function MessageInboxPage() {
|
|
|
47
50
|
|
|
48
51
|
/** 刷新站内信列表。 */
|
|
49
52
|
async function refresh(view = selectedView, category = categoryId) {
|
|
53
|
+
const seq = ++requestSeqRef.current
|
|
54
|
+
loadingRef.current = true
|
|
55
|
+
finishedRef.current = false
|
|
50
56
|
setItems([])
|
|
51
57
|
setCursorId(0)
|
|
52
58
|
setFinished(false)
|
|
@@ -60,17 +66,24 @@ export default function MessageInboxPage() {
|
|
|
60
66
|
page_num: 1,
|
|
61
67
|
page_size: 20,
|
|
62
68
|
})
|
|
69
|
+
if (seq !== requestSeqRef.current) return
|
|
63
70
|
setItems(result.notifications)
|
|
64
71
|
setCursorId(result.next_cursor_id)
|
|
72
|
+
finishedRef.current = !result.has_more
|
|
65
73
|
setFinished(!result.has_more)
|
|
66
74
|
} finally {
|
|
67
|
-
|
|
75
|
+
if (seq === requestSeqRef.current) {
|
|
76
|
+
loadingRef.current = false
|
|
77
|
+
setLoading(false)
|
|
78
|
+
}
|
|
68
79
|
}
|
|
69
80
|
}
|
|
70
81
|
|
|
71
82
|
/** 加载下一页。 */
|
|
72
83
|
async function loadMore() {
|
|
73
|
-
if (
|
|
84
|
+
if (loadingRef.current || finishedRef.current) return
|
|
85
|
+
const seq = requestSeqRef.current
|
|
86
|
+
loadingRef.current = true
|
|
74
87
|
setLoading(true)
|
|
75
88
|
try {
|
|
76
89
|
const result = await defNotificationService.PageNotification({
|
|
@@ -81,12 +94,16 @@ export default function MessageInboxPage() {
|
|
|
81
94
|
page_num: 1,
|
|
82
95
|
page_size: 20,
|
|
83
96
|
})
|
|
84
|
-
|
|
85
|
-
setItems(
|
|
97
|
+
if (seq !== requestSeqRef.current) return
|
|
98
|
+
setItems((current) => [...current, ...result.notifications])
|
|
86
99
|
setCursorId(result.next_cursor_id)
|
|
100
|
+
finishedRef.current = !result.has_more
|
|
87
101
|
setFinished(!result.has_more)
|
|
88
102
|
} finally {
|
|
89
|
-
|
|
103
|
+
if (seq === requestSeqRef.current) {
|
|
104
|
+
loadingRef.current = false
|
|
105
|
+
setLoading(false)
|
|
106
|
+
}
|
|
90
107
|
}
|
|
91
108
|
}
|
|
92
109
|
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
View,
|
|
11
11
|
} from '@tarojs/components'
|
|
12
12
|
import Taro, { useLoad } from '@tarojs/taro'
|
|
13
|
-
import { useEffect, useState } from 'react'
|
|
13
|
+
import { useEffect, useRef, useState } from 'react'
|
|
14
14
|
import defaultAvatar from '@liujitcn/kratos-taro-app-core/static/images/avatar.png'
|
|
15
15
|
import navigatorBackground from '@liujitcn/kratos-taro-app-core/static/images/navigator_bg.png'
|
|
16
16
|
import { defAuthService } from '@liujitcn/kratos-taro-app-core/api/system/app/v1/auth'
|
|
@@ -45,6 +45,8 @@ export default function ProfilePage() {
|
|
|
45
45
|
avatar: '',
|
|
46
46
|
})
|
|
47
47
|
const [genderList, setGenderList] = useState<BaseDictForm_DictItem[]>([])
|
|
48
|
+
const submittingRef = useRef(false)
|
|
49
|
+
const navigateBackTimerRef = useRef<ReturnType<typeof setTimeout>>()
|
|
48
50
|
const ensureAuthenticated = useUserStore((state) => state.ensureAuthenticated)
|
|
49
51
|
const safeTop = Taro.getWindowInfo().safeArea?.top || 0
|
|
50
52
|
const idTypeOptions = [
|
|
@@ -63,7 +65,13 @@ export default function ProfilePage() {
|
|
|
63
65
|
|
|
64
66
|
useEffect(() => {
|
|
65
67
|
void Taro.setNavigationBarTitle({ title: t('system.profile.title') })
|
|
66
|
-
}, [locale
|
|
68
|
+
}, [locale])
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
return () => {
|
|
72
|
+
if (navigateBackTimerRef.current) clearTimeout(navigateBackTimerRef.current)
|
|
73
|
+
}
|
|
74
|
+
}, [])
|
|
67
75
|
|
|
68
76
|
const requireAuth = () => {
|
|
69
77
|
if (ensureAuthenticated()) return true
|
|
@@ -122,6 +130,7 @@ export default function ProfilePage() {
|
|
|
122
130
|
}
|
|
123
131
|
|
|
124
132
|
const onSubmit = async () => {
|
|
133
|
+
if (submittingRef.current) return
|
|
125
134
|
if (!requireAuth()) return
|
|
126
135
|
if (userInfo.email && !EMAIL_PATTERN.test(userInfo.email)) {
|
|
127
136
|
await Taro.showToast({ icon: 'none', title: t('system.profile.email_invalid') })
|
|
@@ -131,10 +140,16 @@ export default function ProfilePage() {
|
|
|
131
140
|
await Taro.showToast({ icon: 'none', title: t('system.profile.id_code_invalid') })
|
|
132
141
|
return
|
|
133
142
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
143
|
+
submittingRef.current = true
|
|
144
|
+
try {
|
|
145
|
+
await defAuthService.UpdateUserProfile({ user_profile: userInfo })
|
|
146
|
+
await useUserStore.getState().getUserProfile()
|
|
147
|
+
await Taro.showToast({ icon: 'success', title: t('system.profile.save_success') })
|
|
148
|
+
if (navigateBackTimerRef.current) clearTimeout(navigateBackTimerRef.current)
|
|
149
|
+
navigateBackTimerRef.current = setTimeout(() => void Taro.navigateBack(), 400)
|
|
150
|
+
} finally {
|
|
151
|
+
submittingRef.current = false
|
|
152
|
+
}
|
|
138
153
|
}
|
|
139
154
|
|
|
140
155
|
return (
|
|
@@ -36,7 +36,7 @@ export default function SettingsPage() {
|
|
|
36
36
|
|
|
37
37
|
useEffect(() => {
|
|
38
38
|
void Taro.setNavigationBarTitle({ title: t('system.settings.title') })
|
|
39
|
-
}, [locale
|
|
39
|
+
}, [locale])
|
|
40
40
|
|
|
41
41
|
const loadMfaStatus = async (force = false) => {
|
|
42
42
|
if (!useUserStore.getState().isAuthenticated() || (mfaStatusLoading && !force)) return
|
|
@@ -109,6 +109,15 @@ export default function SettingsPage() {
|
|
|
109
109
|
void loadMfaStatus()
|
|
110
110
|
})
|
|
111
111
|
|
|
112
|
+
/** 登录态失效时请求层已弹窗引导重新登录,页面侧不再叠加失败提示。 */
|
|
113
|
+
const isAuthExpiredError = (error: unknown) => {
|
|
114
|
+
if (error && typeof error === 'object') {
|
|
115
|
+
const statusCode = (error as { statusCode?: number }).statusCode
|
|
116
|
+
if (statusCode === 401 || statusCode === 403) return true
|
|
117
|
+
}
|
|
118
|
+
return error instanceof Error && /auth (required|expired)/.test(error.message)
|
|
119
|
+
}
|
|
120
|
+
|
|
112
121
|
const onLogout = async () => {
|
|
113
122
|
if (logoutLoading) return
|
|
114
123
|
const result = await Taro.showModal({
|
|
@@ -120,8 +129,10 @@ export default function SettingsPage() {
|
|
|
120
129
|
try {
|
|
121
130
|
await logout()
|
|
122
131
|
await Taro.navigateBack()
|
|
123
|
-
} catch {
|
|
124
|
-
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (!isAuthExpiredError(error)) {
|
|
134
|
+
await Taro.showToast({ icon: 'none', title: t('system.settings.logout_failed') })
|
|
135
|
+
}
|
|
125
136
|
} finally {
|
|
126
137
|
setLogoutLoading(false)
|
|
127
138
|
}
|