@liujitcn/kratos-taro-app-system 0.0.59 → 0.0.61

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 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.59",
10
+ "version": "0.0.61",
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.59",
42
- "@liujitcn/kratos-taro-app-ui": "^0.0.59"
41
+ "@liujitcn/kratos-taro-app-ui": "^0.0.61",
42
+ "@liujitcn/kratos-taro-app-core": "^0.0.61"
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': 'miniapp',
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': 'miniapp',
134
+ 'source-client': sourceClient,
134
135
  ...getLocaleRequestHeaders(),
135
136
  ...(accessToken ? { Authorization: accessToken } : {}),
136
137
  },
@@ -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 { getRequestAccessToken, requestBaseURL } from '@liujitcn/kratos-taro-app-core/utils/http'
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 sseBaseURL = requestBaseURL.replace(/\/api\/?$/, '') || window.location.origin
75
- const response = await fetch(`${sseBaseURL}/events/base.notification`, {
76
- headers: { Accept: 'text/event-stream', Authorization: token },
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
@@ -23,7 +23,7 @@ export default function MyPage() {
23
23
 
24
24
  useEffect(() => {
25
25
  void Taro.setNavigationBarTitle({ title: t('core.navigation.my') })
26
- }, [locale, t])
26
+ }, [locale])
27
27
 
28
28
  const openAuthenticatedPage = (route: string) => {
29
29
  if (!ensureAuthenticated()) {
@@ -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
- setActiveSessionID(sessionsRef.current[0].id)
296
- return sessionsRef.current[0].id
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
- updateSessionMessages(sessionID, normalizeMessageList(response.messages))
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) updateSessionMessages(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
- const sessionID = await ensureActiveSession()
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('common.status.loading')}</View>
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
- setLoading(false)
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 (loading || finished) return
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
- const next = [...items, ...result.notifications]
85
- setItems(next)
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
- setLoading(false)
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, t])
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
- await defAuthService.UpdateUserProfile({ user_profile: userInfo })
135
- await useUserStore.getState().getUserProfile()
136
- await Taro.showToast({ icon: 'success', title: t('system.profile.save_success') })
137
- setTimeout(() => void Taro.navigateBack(), 400)
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, t])
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
- await Taro.showToast({ icon: 'none', title: t('system.settings.logout_failed') })
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
  }