@liujitcn/kratos-uni-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/dist/index.mjs CHANGED
@@ -3,9 +3,14 @@ import { registerUserStoreExtension } from "@liujitcn/kratos-uni-app-core/stores
3
3
 
4
4
  // src/notification.ts
5
5
  import { ref } from "vue";
6
+ import { getLocaleRequestHeaders } from "@liujitcn/kratos-uni-app-core";
6
7
  import { setAppMenuBadge } from "@liujitcn/kratos-uni-app-core/navigation";
7
8
  import { hasValidToken } from "@liujitcn/kratos-uni-app-core/utils/auth";
8
- import { getRequestAccessToken, requestBaseURL } from "@liujitcn/kratos-uni-app-core/utils/http";
9
+ import {
10
+ getRequestAccessToken,
11
+ siteBaseURL,
12
+ sourceClient
13
+ } from "@liujitcn/kratos-uni-app-core/utils/http";
9
14
 
10
15
  // src/api/base/v1/notification.ts
11
16
  import { http } from "@liujitcn/kratos-uni-app-core/utils/http";
@@ -172,9 +177,13 @@ function startNotificationSse() {
172
177
  const token = await getRequestAccessToken("optional");
173
178
  if (!token || controller.signal.aborted)
174
179
  return;
175
- const sseBaseURL = requestBaseURL.replace(/\/api\/?$/, "") || window.location.origin;
176
- const response = await fetch(`${sseBaseURL}/events/base.notification`, {
177
- headers: { Accept: "text/event-stream", Authorization: token },
180
+ const response = await fetch(`${siteBaseURL}/events/base.notification`, {
181
+ headers: {
182
+ Accept: "text/event-stream",
183
+ Authorization: token,
184
+ "source-client": sourceClient,
185
+ ...getLocaleRequestHeaders()
186
+ },
178
187
  signal: controller.signal
179
188
  });
180
189
  if (response.status === 401 || response.status === 403)
@@ -730,5 +739,6 @@ var src_default = systemModule;
730
739
  export {
731
740
  src_default as default,
732
741
  pauseNotificationPolling,
733
- resumeNotificationPolling
742
+ resumeNotificationPolling,
743
+ startNotificationPolling
734
744
  };
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",
@@ -35,7 +35,7 @@
35
35
  "@dcloudio/uni-app": "3.0.0-5010520260709002",
36
36
  "@dcloudio/uni-ui": "^1.4.28",
37
37
  "vue": "^3.4.21",
38
- "@liujitcn/kratos-uni-app-core": "^0.0.59"
38
+ "@liujitcn/kratos-uni-app-core": "^0.0.61"
39
39
  },
40
40
  "devDependencies": {
41
41
  "esbuild": "^0.20.2",
@@ -3,6 +3,7 @@ import {
3
3
  handleAuthExpired,
4
4
  http,
5
5
  requestBaseURL,
6
+ sourceClient,
6
7
  } from '@liujitcn/kratos-uni-app-core/utils/http'
7
8
  import type { ListAiMessageRequest, ListAiMessageResponse } from '../../../rpc/base/v1/ai_session'
8
9
  import type {
@@ -74,7 +75,7 @@ export async function SendAiMessageStream(
74
75
  const headers: Record<string, string> = {
75
76
  Accept: 'text/event-stream',
76
77
  'Content-Type': 'application/json;charset=utf-8',
77
- 'source-client': 'miniapp',
78
+ 'source-client': sourceClient,
78
79
  ...getLocaleRequestHeaders(),
79
80
  }
80
81
  if (accessToken) {
@@ -128,7 +129,7 @@ export function StreamAiMessageByChunkedRequest(
128
129
  header: {
129
130
  Accept: 'text/event-stream',
130
131
  'Content-Type': 'application/json;charset=utf-8',
131
- 'source-client': 'miniapp',
132
+ 'source-client': sourceClient,
132
133
  ...getLocaleRequestHeaders(),
133
134
  ...(accessToken ? { Authorization: accessToken } : {}),
134
135
  },
package/src/index.ts CHANGED
@@ -14,6 +14,6 @@ registerUserStoreExtension({
14
14
  onSilentLogout: stopNotificationPolling,
15
15
  })
16
16
 
17
- export { pauseNotificationPolling, resumeNotificationPolling }
17
+ export { pauseNotificationPolling, resumeNotificationPolling, startNotificationPolling }
18
18
 
19
19
  export default systemModule
@@ -1,7 +1,12 @@
1
1
  import { ref } from 'vue'
2
+ import { getLocaleRequestHeaders } from '@liujitcn/kratos-uni-app-core'
2
3
  import { setAppMenuBadge } from '@liujitcn/kratos-uni-app-core/navigation'
3
4
  import { hasValidToken } from '@liujitcn/kratos-uni-app-core/utils/auth'
4
- import { getRequestAccessToken, requestBaseURL } from '@liujitcn/kratos-uni-app-core/utils/http'
5
+ import {
6
+ getRequestAccessToken,
7
+ siteBaseURL,
8
+ sourceClient,
9
+ } from '@liujitcn/kratos-uni-app-core/utils/http'
5
10
  import { defNotificationService } from './api/base/v1/notification'
6
11
 
7
12
  /** System 模块共享的站内信未读数。 */
@@ -70,9 +75,13 @@ function startNotificationSse(): () => void {
70
75
  try {
71
76
  const token = await getRequestAccessToken('optional')
72
77
  if (!token || controller.signal.aborted) return
73
- const sseBaseURL = requestBaseURL.replace(/\/api\/?$/, '') || window.location.origin
74
- const response = await fetch(`${sseBaseURL}/events/base.notification`, {
75
- headers: { Accept: 'text/event-stream', Authorization: token },
78
+ const response = await fetch(`${siteBaseURL}/events/base.notification`, {
79
+ headers: {
80
+ Accept: 'text/event-stream',
81
+ Authorization: token,
82
+ 'source-client': sourceClient,
83
+ ...getLocaleRequestHeaders(),
84
+ },
76
85
  signal: controller.signal,
77
86
  })
78
87
  if (response.status === 401 || response.status === 403) return
@@ -484,6 +484,8 @@ async function sendAiPayload(payload: { text: string; attachments: AiAttachment[
484
484
  if (!sessionID || isSessionSending(sessionID)) {
485
485
  return false
486
486
  }
487
+ // 发送前等待进行中的历史加载结束,避免本地消息与历史加载竞态。
488
+ await waitForMessagesLoaded(sessionID)
487
489
 
488
490
  const localUserMessage = createLocalUserMessage(payload)
489
491
  const thinkingMessage = createThinkingMessage({ sessionID })
@@ -798,10 +800,7 @@ async function ensureSessionsLoaded() {
798
800
  terminal: AI_TERMINAL,
799
801
  })
800
802
  sessions.value = normalizeSessionList(response.sessions)
801
- const sessionID = await ensureActiveSession()
802
- if (sessionID) {
803
- await loadMessages(sessionID)
804
- }
803
+ await ensureActiveSession()
805
804
  } catch (error) {
806
805
  showError(error, t('system.ai.load_sessions_failed'))
807
806
  } finally {
@@ -814,8 +813,12 @@ async function ensureActiveSession() {
814
813
  return activeSessionID.value
815
814
  }
816
815
  if (sessions.value.length > 0) {
817
- activeSessionID.value = sessions.value[0].id
818
- return activeSessionID.value
816
+ const sessionID = sessions.value[0].id
817
+ activeSessionID.value = sessionID
818
+ if (!messages.value[sessionID]?.length) {
819
+ await loadMessages(sessionID)
820
+ }
821
+ return sessionID
819
822
  }
820
823
  const sessionID = await createRemoteSession()
821
824
  if (sessionID) {
@@ -849,13 +852,14 @@ async function loadMessages(sessionID: string) {
849
852
  if (loadingSessionID.value !== sessionID) {
850
853
  return
851
854
  }
852
- messages.value[sessionID] = normalizeMessageList(response.messages)
855
+ const loaded = normalizeMessageList(response.messages)
856
+ messages.value[sessionID] = mergeWithLocalOnlyStreaming(messages.value[sessionID] ?? [], loaded)
853
857
  if (activeSessionID.value === sessionID) {
854
858
  scrollChatToBottom()
855
859
  }
856
860
  } catch (error) {
857
861
  if (loadingSessionID.value === sessionID) {
858
- messages.value[sessionID] = []
862
+ messages.value[sessionID] = mergeWithLocalOnlyStreaming(messages.value[sessionID] ?? [], [])
859
863
  }
860
864
  showError(error, t('system.ai.load_messages_failed'))
861
865
  } finally {
@@ -865,6 +869,31 @@ async function loadMessages(sessionID: string) {
865
869
  }
866
870
  }
867
871
 
872
+ /** 合并历史加载结果与仍在流式中的本地消息,避免加载覆盖进行中的对话。 */
873
+ function mergeWithLocalOnlyStreaming(current: ChatMessageItem[], loaded: ChatMessageItem[]) {
874
+ const localStreaming = current.filter(
875
+ (item) => item.localOnly && item.status === AiMessageStatus.AI_MESSAGE_STATUS_GENERATING,
876
+ )
877
+ if (!localStreaming.length) {
878
+ return loaded
879
+ }
880
+ const messageMap = new Map<string, ChatMessageItem>()
881
+ for (const item of loaded) {
882
+ messageMap.set(item.key, item)
883
+ }
884
+ for (const item of localStreaming) {
885
+ messageMap.set(item.key, item)
886
+ }
887
+ return sortMessages(Array.from(messageMap.values()))
888
+ }
889
+
890
+ /** 等待指定会话的历史加载完成,避免发送消息与加载竞态。 */
891
+ async function waitForMessagesLoaded(sessionID: string) {
892
+ while (loadingSessionID.value === sessionID) {
893
+ await new Promise((resolve) => setTimeout(resolve, 16))
894
+ }
895
+ }
896
+
868
897
  function normalizeSession(session?: Partial<AiSession> | null): AiSession {
869
898
  return {
870
899
  id: String(session?.id ?? ''),
@@ -74,7 +74,7 @@ function openAction() {
74
74
  {{ t('common.action.view') }}
75
75
  </button>
76
76
  </view>
77
- <view v-else class="empty">{{ t('common.status.loading') }}</view>
77
+ <view v-else class="empty">{{ t('core.status.loading') }}</view>
78
78
  </template>
79
79
 
80
80
  <style scoped lang="scss">
@@ -15,6 +15,7 @@ const finished = ref(false)
15
15
  const selectedView = ref(NotificationView.NOTIFICATION_VIEW_INBOX)
16
16
  const categoryId = ref<number>()
17
17
  const categoryOptions = ref<NotificationCategory[]>([])
18
+ let requestSeq = 0
18
19
  const viewOptions = [
19
20
  { value: NotificationView.NOTIFICATION_VIEW_INBOX, key: 'system.notification.view.inbox' },
20
21
  { value: NotificationView.NOTIFICATION_VIEW_UNREAD, key: 'system.notification.view.unread' },
@@ -35,9 +36,12 @@ async function loadCategories() {
35
36
 
36
37
  /** 刷新站内信列表。 */
37
38
  async function refresh() {
39
+ requestSeq++
38
40
  cursorId.value = 0
39
41
  finished.value = false
40
42
  items.value = []
43
+ // 先重置 loading,再发起新请求,避免清空列表后 loadMore 因旧 loading 直接返回。
44
+ loading.value = false
41
45
  await loadMore()
42
46
  }
43
47
 
@@ -81,6 +85,7 @@ async function deleteItem(item: Notification) {
81
85
  /** 加载下一页站内信。 */
82
86
  async function loadMore() {
83
87
  if (loading.value || finished.value) return
88
+ const seq = requestSeq
84
89
  loading.value = true
85
90
  try {
86
91
  const result = await defNotificationService.PageNotification({
@@ -91,11 +96,12 @@ async function loadMore() {
91
96
  page_num: 1,
92
97
  page_size: 20,
93
98
  })
99
+ if (seq !== requestSeq) return
94
100
  items.value.push(...result.notifications)
95
101
  finished.value = !result.has_more
96
102
  cursorId.value = result.next_cursor_id
97
103
  } finally {
98
- loading.value = false
104
+ if (seq === requestSeq) loading.value = false
99
105
  }
100
106
  }
101
107
 
@@ -3,7 +3,7 @@ import { defAuthService } from '@liujitcn/kratos-uni-app-core/api/system/app/v1/
3
3
  import { useUserStore } from '@liujitcn/kratos-uni-app-core/stores'
4
4
  import type { UserProfileForm } from '@liujitcn/kratos-uni-app-core/rpc/system/app/v1/auth'
5
5
  import { onLoad } from '@dcloudio/uni-app'
6
- import { computed, ref } from 'vue'
6
+ import { computed, onBeforeUnmount, ref } from 'vue'
7
7
  import type { BaseDictForm_DictItem } from '@liujitcn/kratos-uni-app-core/rpc/system/app/v1/base_dict'
8
8
  import { BaseUserIDType } from '@liujitcn/kratos-uni-app-core/rpc/system/common/v1/common'
9
9
  import { defBaseDictService } from '@liujitcn/kratos-uni-app-core/api/system/app/v1/base_dict'
@@ -21,6 +21,13 @@ const { t } = useI18n()
21
21
  const { safeAreaInsets } = uni.getSystemInfoSync()
22
22
 
23
23
  const imgMaxSize = ref(1024 * 1024)
24
+ const submitting = ref(false)
25
+ let navigateBackTimer: ReturnType<typeof setTimeout> | undefined
26
+
27
+ onBeforeUnmount(() => {
28
+ if (navigateBackTimer) clearTimeout(navigateBackTimer)
29
+ navigateBackTimer = undefined
30
+ })
24
31
 
25
32
  // 获取个人信息,修改个人信息需提供初始值
26
33
  const userInfo = ref({} as UserProfileForm)
@@ -196,6 +203,7 @@ const onGetPhoneNumber: UniHelper.ButtonOnGetphonenumber = async (e) => {
196
203
 
197
204
  // 点击保存提交表单
198
205
  const onSubmit = async () => {
206
+ if (submitting.value) return
199
207
  if (!userStore.ensureAuthenticated()) {
200
208
  navigateToLogin()
201
209
  return
@@ -210,25 +218,32 @@ const onSubmit = async () => {
210
218
  return
211
219
  }
212
220
 
213
- const { nick_name, gender, email, id_type, id_code } = userInfo.value
214
- await defAuthService.UpdateUserProfile({
215
- user_profile: {
216
- nick_name: nick_name,
217
- gender: gender,
218
- avatar: userInfo.value.avatar,
219
- phone: userInfo.value.phone,
220
- user_name: userInfo.value.user_name,
221
- email,
222
- id_type,
223
- id_code,
224
- },
225
- })
226
- // 更新Store昵称
227
- syncUserStoreProfile(userInfo.value)
228
- await uni.showToast({ icon: 'success', title: t('system.profile.save_success') })
229
- setTimeout(() => {
230
- uni.navigateBack()
231
- }, 400)
221
+ submitting.value = true
222
+ try {
223
+ const { nick_name, gender, email, id_type, id_code } = userInfo.value
224
+ await defAuthService.UpdateUserProfile({
225
+ user_profile: {
226
+ nick_name: nick_name,
227
+ gender: gender,
228
+ avatar: userInfo.value.avatar,
229
+ phone: userInfo.value.phone,
230
+ user_name: userInfo.value.user_name,
231
+ email,
232
+ id_type,
233
+ id_code,
234
+ },
235
+ })
236
+ // 更新Store昵称
237
+ syncUserStoreProfile(userInfo.value)
238
+ await uni.showToast({ icon: 'success', title: t('system.profile.save_success') })
239
+ if (navigateBackTimer) clearTimeout(navigateBackTimer)
240
+ navigateBackTimer = setTimeout(() => {
241
+ navigateBackTimer = undefined
242
+ uni.navigateBack()
243
+ }, 400)
244
+ } finally {
245
+ submitting.value = false
246
+ }
232
247
  }
233
248
  </script>
234
249