@allbluecn/web-app 0.23.1 → 0.23.3

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.
Files changed (60) hide show
  1. package/package.json +3 -3
  2. package/src/components/desktop/__tests__/desktop-state-listener.test.tsx +183 -0
  3. package/src/components/desktop/__tests__/sync-indicator.test.tsx +130 -0
  4. package/src/components/desktop/cloud-login.tsx +108 -6
  5. package/src/components/desktop/desktop-state-listener.tsx +76 -0
  6. package/src/components/desktop/shell-widgets.tsx +12 -5
  7. package/src/components/desktop/sync-indicator.tsx +62 -13
  8. package/src/components/layout/sidebar-03/__tests__/user-menu-anonymous.test.tsx +4 -0
  9. package/src/components/layout/sidebar-03/user-menu.tsx +63 -0
  10. package/src/lib/__tests__/session.test.ts +95 -0
  11. package/src/lib/auth/auth.config.ts +13 -0
  12. package/src/lib/changelog/sdk-versions.ts +1 -1
  13. package/src/lib/desktop-bridge-types.ts +36 -0
  14. package/src/lib/session.ts +15 -1
  15. package/src/paraglide/messages/_index.d.ts +11 -0
  16. package/src/paraglide/messages/_index.js +11 -0
  17. package/src/paraglide/messages/desktop_cloud_exchange_failed.d.ts +16 -0
  18. package/src/paraglide/messages/desktop_cloud_exchange_failed.js +29 -0
  19. package/src/paraglide/messages/desktop_state_cloud.d.ts +16 -0
  20. package/src/paraglide/messages/desktop_state_cloud.js +29 -0
  21. package/src/paraglide/messages/desktop_state_guest.d.ts +16 -0
  22. package/src/paraglide/messages/desktop_state_guest.js +29 -0
  23. package/src/paraglide/messages/desktop_state_transferring.d.ts +16 -0
  24. package/src/paraglide/messages/desktop_state_transferring.js +29 -0
  25. package/src/paraglide/messages/sync_bind_failed.d.ts +16 -0
  26. package/src/paraglide/messages/sync_bind_failed.js +29 -0
  27. package/src/paraglide/messages/sync_bind_in_progress.d.ts +16 -0
  28. package/src/paraglide/messages/sync_bind_in_progress.js +29 -0
  29. package/src/paraglide/messages/sync_bind_nonempty.d.ts +16 -0
  30. package/src/paraglide/messages/sync_bind_nonempty.js +29 -0
  31. package/src/paraglide/messages/sync_bind_probe_unreachable.d.ts +16 -0
  32. package/src/paraglide/messages/sync_bind_probe_unreachable.js +29 -0
  33. package/src/paraglide/messages/sync_bind_shell_unsupported.d.ts +16 -0
  34. package/src/paraglide/messages/sync_bind_shell_unsupported.js +29 -0
  35. package/src/paraglide/messages/sync_logout_failed.d.ts +16 -0
  36. package/src/paraglide/messages/sync_logout_failed.js +29 -0
  37. package/src/paraglide/messages/sync_logout_in_progress.d.ts +16 -0
  38. package/src/paraglide/messages/sync_logout_in_progress.js +29 -0
  39. package/src/routeTree.gen.ts +21 -0
  40. package/src/routes/api/sync/__tests__/account-status.test.ts +109 -0
  41. package/src/routes/api/sync/account-status.ts +39 -0
  42. package/src/server/__tests__/exchange-remote-token.test.ts +113 -0
  43. package/src/server/__tests__/remote-auth.test.ts +131 -1
  44. package/src/server/ai-gateway/__tests__/tools-authorization.test.ts +4 -2
  45. package/src/server/ai-gateway/tools/server.ts +7 -0
  46. package/src/server/desktop-bootstrap.ts +19 -3
  47. package/src/server/desktop-session-merge.ts +51 -0
  48. package/src/server/remote-auth.ts +61 -3
  49. package/src/server/serverFns/__tests__/knowledge-readable-kb.test.ts +21 -0
  50. package/src/server/serverFns/__tests__/workspace.test.ts +46 -0
  51. package/src/server/serverFns/auth/session.ts +28 -28
  52. package/src/server/serverFns/harness/attach-to-kb.ts +3 -1
  53. package/src/server/serverFns/knowledge.ts +4 -1
  54. package/src/server/serverFns/sync/transfer.ts +168 -0
  55. package/src/server/serverFns/workspace.ts +15 -0
  56. package/src/server/serverfn-public-allowlist.ts +4 -0
  57. package/src/server/sync/__tests__/ownership-transfer.test.ts +896 -0
  58. package/src/server/sync/account-status.ts +36 -0
  59. package/src/server/sync/ownership-transfer.ts +291 -0
  60. package/src/server/sync/saas-profile.ts +70 -0
@@ -15,18 +15,17 @@
15
15
  // You should have received a copy of the GNU Affero General Public License
16
16
  // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
 
18
- import { DEFAULT_AI_MODEL, DEFAULT_AI_PROVIDER } from '@/lib/ai/default-model'
19
18
  import { createServerFn } from '@tanstack/react-start'
20
19
  import { z } from 'zod'
21
20
  import { getRequest } from '@tanstack/react-start/server'
22
21
  import type { FeatureKey } from '@allbluecn/kernel'
23
- import type { Session } from '@/lib/session'
24
22
  import { getCurrentSession } from '@/lib/session'
23
+ import { resetProfileCache } from '@/lib/auth/auth.config'
25
24
  import { buildGuestSession, isDesktopServer } from '@/server/desktop-guest'
26
25
  import { ensureLocalShadowUser } from '@/server/desktop-bootstrap'
27
26
  import { readRemoteAuth, writeRemoteAuth, writeRemoteSession, clearRemoteAuth } from '@/server/remote-auth'
28
27
  import { generateEntitlementsToken } from '@/server/entitlements-token'
29
- import type { RemoteAuthData } from '@/server/remote-auth'
28
+ import { buildRemoteSession } from '@/server/desktop-session-merge'
30
29
  import { checkAuthRateLimit } from '@/server/middlewares/rate-limit'
31
30
 
32
31
  const SAAS_URL = process.env.DESKTOP_SAAS_URL || 'https://allblue.litsex.cn'
@@ -48,26 +47,9 @@ const saasProfileSchema = z.object({
48
47
  signedToken: z.string().optional(),
49
48
  })
50
49
 
51
- function buildRemoteSession(data: RemoteAuthData): Session {
52
- return {
53
- user: {
54
- id: data.userId,
55
- name: data.username ?? data.userId,
56
- email: data.email,
57
- role: (data.role ?? 'USER') as 'USER' | 'ADMIN' | 'SUPER_ADMIN',
58
- isGuest: false,
59
- // 云端头像透传(SaaS desktop-callback 下发;缺省回落首字母,修复桌面端
60
- // 当前用户头像不显示)
61
- avatarConfig: data.avatarConfig,
62
- bgShape: data.bgShape,
63
- bgColor: data.bgColor,
64
- aiProvider: DEFAULT_AI_PROVIDER,
65
- aiModel: DEFAULT_AI_MODEL,
66
- hasAiKey: false,
67
- },
68
- expires: 'never',
69
- }
70
- }
50
+ // buildRemoteSession 已抽出共享至 @/server/desktop-session-merge(A10):
51
+ // getCurrentSession 桌面回落需复用同一组装,且 lib/session 不得 import
52
+ // serverFns/**(循环依赖),组装点移至 server 域纯模块。
71
53
 
72
54
  export const getSessionFn = createServerFn({ method: 'GET' })
73
55
  .handler(async () => {
@@ -141,13 +123,18 @@ export const exchangeRemoteTokenFn = createServerFn({ method: 'POST' })
141
123
  email: profile.user.email,
142
124
  role: profile.user.role,
143
125
  })
144
- if (!signedToken) {
145
- console.warn('[exchangeRemoteTokenFn] 无法签发权益 token(缺少 ENTITLEMENTS_SIGNING_SECRET),降级 OSS')
146
- await writeRemoteSession(profile.user.id, planName, token)
147
- return { success: true, userId: profile.user.id, planName, degraded: true }
148
- }
149
126
  }
150
127
 
128
+ // session 块双路径无条件落盘(A10 修复,控制者核实的激活缺口):readRemoteAuth
129
+ // 以 data.session.userId 为登录态唯一依据,原先仅降级路径写 session——生产
130
+ // SaaS 恒发 signedToken,signedToken 路径云身份 100% 不激活、云态写入属主错
131
+ // 为 guest。writeRemoteSession / writeRemoteAuth 均 merge 语义({...prev,
132
+ // ...}),先 session 后 token 不互踩。
133
+ await writeRemoteSession(profile.user.id, planName, token)
134
+ if (!signedToken) {
135
+ console.warn('[exchangeRemoteTokenFn] 无法签发权益 token(缺少 ENTITLEMENTS_SIGNING_SECRET),降级 OSS')
136
+ return { success: true, userId: profile.user.id, planName, degraded: true }
137
+ }
151
138
  await writeRemoteAuth(signedToken, token)
152
139
  return { success: true, userId: profile.user.id, planName }
153
140
  })
@@ -159,3 +146,16 @@ export const logoutRemoteFn = createServerFn({ method: 'POST' }).validator(z.obj
159
146
  }
160
147
  throw new Error('logoutRemoteFn 仅桌面端可用')
161
148
  })
149
+
150
+ // 身份切换缓存三件套之「服务端模块缓存」(A11,设计 D1):desktop-state 身份切换
151
+ // 事件(guest↔cloud)到达 renderer 时调用——auth.config.ts 的 profileCache 是
152
+ // backend 进程内存缓存,renderer 无法直清,本 serverFn 是唯一载体。公开端点理由
153
+ // (serverfn-public-allowlist):调用时机是身份切换(登出语义延伸,会话可能已
154
+ // 失效),且只清进程内 Map、不暴露任何用户数据——与 logoutRemoteFn 同形态。
155
+ // 幂等无副作用(缓存 miss 自然回源),非桌面端调用同样安全(clear 无害)。
156
+ export const resetAuthProfileCacheFn = createServerFn({ method: 'POST' })
157
+ .validator(z.object({}))
158
+ .handler(async () => {
159
+ resetProfileCache()
160
+ return { success: true }
161
+ })
@@ -44,7 +44,7 @@ export const attachToKbFn = createServerFn({ method: "POST" })
44
44
  const userId = context.user.id
45
45
 
46
46
  const run = await assertRunOwnership(data.generationRunId, userId)
47
- await assertKbOwnership(data.kbId, userId)
47
+ const kb = await assertKbOwnership(data.kbId, userId)
48
48
 
49
49
  const text = extractText(run.result)
50
50
  if (!text?.trim()) throw new Error("Generation run has no extractable text")
@@ -63,6 +63,8 @@ export const attachToKbFn = createServerFn({ method: "POST" })
63
63
  properties: {},
64
64
  content: { markdown: text },
65
65
  createdBy: userId,
66
+ // 冗余同步 scope 列(P2 A7):kb.workspaceId 写时快照,null = stay local
67
+ syncWorkspaceId: kb.workspaceId,
66
68
  },
67
69
  select: { id: true },
68
70
  })
@@ -219,7 +219,8 @@ export const createDocumentFn = createServerFn({ method: 'POST' })
219
219
  await requirePermission(userId, 'knowledge', 'write')
220
220
  const kb = await prisma.knowledgeBase.findFirst({
221
221
  where: { id: data.kbId, ...READABLE_KB(context.user.id) },
222
- select: { id: true },
222
+ // workspaceId:文档冗余同步 scope 列(syncWorkspaceId)的来源(P2 A7)
223
+ select: { id: true, workspaceId: true },
223
224
  })
224
225
  if (!kb) throw new Error('未找到或无权限')
225
226
  // parentId 必须存在且属于同一 KB(审计 2026-10-25:防把文档挂到他人 KB 节点下)
@@ -247,6 +248,8 @@ export const createDocumentFn = createServerFn({ method: 'POST' })
247
248
  sortOrder: nextSort,
248
249
  properties: {},
249
250
  createdBy: userId,
251
+ // 同步 scope 写时快照:null(个人库)= 一期不进同步面(stay local)
252
+ syncWorkspaceId: kb.workspaceId,
250
253
  },
251
254
  select: { id: true, title: true, type: true, parentId: true },
252
255
  })
@@ -0,0 +1,168 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+
5
+ // 归属转移 serverFn(桌面本地优先 P2-A8a):renderer 在云登录成功回调链发起
6
+ // (cloudLogin(main) → exchangeRemoteTokenFn(A10) → 本 serverFn),localhost
7
+ // cookie 天然过鉴权(backend 进程无 cookie,不得由 bind-flow 直调——复审⑥ P1-8)。
8
+ // 校验链:① readRemoteAuth().userId 必须等于 data.cloudUserId(转移目标必须等于
9
+ // 已交换登录的云账号,防参数伪造;非 desktop / 无 remoteAuth 时 readRemoteAuth
10
+ // 恒 null → 拒绝);② 复核目标为空账号(isAccountEmpty,与
11
+ // /api/sync/account-status 端点同源);③ 调 transferOwnership 执行
12
+ // GUEST_USER_ID → cloudUserId 的单事务转移。outbox 语义由触发器自然产出
13
+ // (op='upsert' + base_version=0),本 serverFn 不触碰同步两表。
14
+ // 身份字段(A8b 裁定):name/avatarConfig 等可省略——云昵称/头像仅 SaaS 权威
15
+ // (remote-auth.json session 块不存 profile,renderer 拿不到),省略时 handler
16
+ // 以 remoteAuth.sessionToken 自取 SaaS profile 补齐;字段全传则用传入值(向后
17
+ // 兼容,A8a 老调用形态不变)。
18
+ // 同文件第二端点 probeCloudAccountStatusFn(claim 探测):renderer 在 exchange
19
+ // 激活后、bind:begin 前调用,代持云会话 token 探测目标账号是否空账号(云账号
20
+ // 资料与账号状态只有 SaaS 权威)——探测失败按非空处理(宁可不转不可错转)。
21
+ // 同文件第三端点 reverseTransferOwnershipFn(A9 登出反向转移):登出时把云身份
22
+ // 数据翻回 guest,详见其实现处注释。
23
+ import { z } from "zod"
24
+ import { createServerFn } from "@tanstack/react-start"
25
+ import { getPrisma } from "@allbluecn/database/get-prisma"
26
+ import { authContextMiddleware } from "@/server/middlewares/auth-context"
27
+ import { checkAuthRateLimit } from "@/server/middlewares/rate-limit"
28
+ import { readRemoteAuth, clearRemoteAuth } from "@/server/remote-auth"
29
+ import { GUEST_USER_ID, isDesktopServer } from "@/server/desktop-guest"
30
+ import { isAccountEmpty } from "@/server/sync/account-status"
31
+ import { reverseTransferOwnership, transferOwnership } from "@/server/sync/ownership-transfer"
32
+ import { fetchSaasProfile, DESKTOP_SAAS_URL } from "@/server/sync/saas-profile"
33
+
34
+ const transferInputSchema = z.object({
35
+ cloudUserId: z.string().min(1),
36
+ name: z.string().min(1).optional(),
37
+ email: z.string().email().nullish(),
38
+ avatarConfig: z.record(z.string(), z.unknown()).nullish(),
39
+ bgShape: z.string().nullish(),
40
+ bgColor: z.string().nullish(),
41
+ })
42
+
43
+ export const transferOwnershipFn = createServerFn({ method: "POST" })
44
+ .middleware([authContextMiddleware])
45
+ .validator(transferInputSchema)
46
+ .handler(async ({ data }) => {
47
+ // ① 转移目标必须等于已交换登录的云账号(readRemoteAuth 非 desktop 恒 null)
48
+ const remoteAuth = await readRemoteAuth()
49
+ if (!remoteAuth || remoteAuth.userId !== data.cloudUserId) {
50
+ throw new Error(
51
+ `[sync/transfer] 转移目标与已登录云账号不匹配(remoteAuth.userId=${remoteAuth?.userId ?? "null"})`,
52
+ )
53
+ }
54
+
55
+ // ② 复核目标为空账号(仅空账号才允许归属转移,一期唯一路径)
56
+ const prisma = await getPrisma()
57
+ if (!(await isAccountEmpty(prisma, data.cloudUserId))) {
58
+ throw new Error("[sync/transfer] 目标云账号非空账号,拒绝归属转移")
59
+ }
60
+
61
+ // ②b 身份字段补齐(A8b 裁定):name 省略 = renderer 未传身份字段(一期主
62
+ // 路径),以 remoteAuth.sessionToken 自取 SaaS profile;资料获取失败抛错走
63
+ // bind:fail——绝不以空/错资料落云身份行。字段全传则用传入值(向后兼容)。
64
+ let name = data.name
65
+ let email = data.email ?? null
66
+ let avatarConfig = (data.avatarConfig ?? null) as Record<string, unknown> | null
67
+ let bgShape = data.bgShape ?? null
68
+ let bgColor = data.bgColor ?? null
69
+ if (name === undefined) {
70
+ const profile = await fetchSaasProfile(remoteAuth.sessionToken)
71
+ name = profile.name?.trim() || data.cloudUserId
72
+ email = data.email ?? profile.email
73
+ avatarConfig = data.avatarConfig ?? profile.avatarConfig
74
+ bgShape = data.bgShape ?? profile.bgShape
75
+ bgColor = data.bgColor ?? profile.bgColor
76
+ }
77
+
78
+ // ③ 单事务转移 GUEST → cloudUserId(失败整体回滚,调用方走 bind:fail 恢复)
79
+ return transferOwnership(prisma, GUEST_USER_ID, {
80
+ id: data.cloudUserId,
81
+ name,
82
+ email,
83
+ avatarConfig,
84
+ bgShape,
85
+ bgColor,
86
+ })
87
+ })
88
+
89
+ // 登出反向转移(A9):renderer 用户菜单登出时发起,把云身份名下数据翻回 guest
90
+ // (数据保留本机且 guest 可访问)。校验链:① 桌面守卫(非 desktop 一律拒绝,同
91
+ // exchange/logoutRemote 口径);② readRemoteAuth().userId === data.cloudUserId
92
+ // (登出者必须是当前云身份本人;无 remoteAuth = 已登出/非云态 → 拒绝);③ 会话
93
+ // 身份 === data.cloudUserId(防「本地凭据会话与云身份并存」时误触发反向转移——
94
+ // authjs 会话有效时 session.user.id 是本地用户而非云 id);④ 单事务
95
+ // cloud → GUEST 反向转移(outbox/row_state 同事务清空防反向 op 上云)。
96
+ // 事务提交成功后立即清 remote-auth(幂等):反向转移一旦提交数据已 guest 属主,
97
+ // 云身份残留会让 fallback 会话显示「云态零数据」假丢数据窗口(A8b 发现 2 的镜像
98
+ // ——正向是转移成功 + 云身份未写,反向是转移成功 + 云身份未清)。renderer 侧的
99
+ // logoutRemoteFn 保留为登出编排第二步(幂等 re-clear,裁定顺序不变)。
100
+ export const reverseTransferOwnershipFn = createServerFn({ method: "POST" })
101
+ .middleware([authContextMiddleware])
102
+ .validator(z.object({ cloudUserId: z.string().min(1) }))
103
+ .handler(async ({ data, context }) => {
104
+ // ① 桌面守卫(远端部署下本端点可被任意会话调来清他人 remote-auth / 翻转数据)
105
+ if (!isDesktopServer()) {
106
+ throw new Error("reverseTransferOwnershipFn 仅桌面端可用")
107
+ }
108
+
109
+ // ② 登出者必须是当前云身份本人
110
+ const remoteAuth = await readRemoteAuth()
111
+ if (!remoteAuth || remoteAuth.userId !== data.cloudUserId) {
112
+ throw new Error(
113
+ `[sync/transfer] 登出目标与已激活云身份不匹配(remoteAuth.userId=${remoteAuth?.userId ?? "null"})`,
114
+ )
115
+ }
116
+
117
+ // ③ 会话身份必须是云身份本人(云态下 fallback 会话 user.id 即云 id)
118
+ if (context.user.id !== data.cloudUserId) {
119
+ throw new Error(
120
+ `[sync/transfer] 会话身份与登出目标不匹配(session.userId=${context.user.id})`,
121
+ )
122
+ }
123
+
124
+ // ④ 单事务反向转移 cloud → guest(失败整体回滚留在云态,renderer 解锁后可重试)
125
+ const prisma = await getPrisma()
126
+ return reverseTransferOwnership(prisma, data.cloudUserId, GUEST_USER_ID)
127
+ .then(async (result) => {
128
+ // 提交成功后清 remote-auth(幂等;clearRemoteAuth 内部吞错仅 warn,不反噬主流程)
129
+ await clearRemoteAuth()
130
+ return result
131
+ })
132
+ })
133
+
134
+ // claim 探测(A8b 编排时序第 3 步):exchange 激活后、bind:begin 前,renderer
135
+ // 经本端点代持云会话 token 探测目标云账号是否空账号(isAccountEmpty 口径在 SaaS
136
+ // 侧执行——账号状态只有 SaaS 权威,本地库看不到云端数据)。探测失败(SaaS 不可
137
+ // 达/非 200/形状漂移)一律按非空处理(宁可不转不可错转),unreachable 标记供
138
+ // renderer 区分提示;非空账号一期不支持转移。
139
+ export const probeCloudAccountStatusFn = createServerFn({ method: "POST" })
140
+ .middleware([authContextMiddleware])
141
+ .handler(async () => {
142
+ // 与 exchangeRemoteTokenFn 同口径:非桌面部署下任何客户端都能借本服务探测
143
+ // 任意 token 的账号状态(审计 2026-10-25 形态)
144
+ if (!isDesktopServer()) {
145
+ throw new Error("probeCloudAccountStatusFn 仅桌面端可用")
146
+ }
147
+ // 防把账号状态探测代理成对上游 SaaS 的暴力面
148
+ await checkAuthRateLimit("probeCloudAccountStatus", 20, 15 * 60 * 1000)
149
+
150
+ const remoteAuth = await readRemoteAuth()
151
+ if (!remoteAuth?.sessionToken) {
152
+ throw new Error("[sync/transfer] 无云会话(未交换登录),无法探测账号状态")
153
+ }
154
+
155
+ try {
156
+ const res = await fetch(`${DESKTOP_SAAS_URL}/api/sync/account-status`, {
157
+ headers: { Cookie: `auth.session-token=${remoteAuth.sessionToken}` },
158
+ credentials: "omit",
159
+ signal: AbortSignal.timeout(10_000),
160
+ })
161
+ if (!res.ok) return { empty: false, unreachable: true }
162
+ const json = (await res.json().catch(() => null)) as { empty?: unknown } | null
163
+ if (typeof json?.empty !== "boolean") return { empty: false, unreachable: true }
164
+ return json.empty ? { empty: true } : { empty: false }
165
+ } catch {
166
+ return { empty: false, unreachable: true }
167
+ }
168
+ })
@@ -303,10 +303,25 @@ export const deleteWorkspaceFn = createServerFn({ method: "POST" })
303
303
  where: { workspaceId: data.workspaceId },
304
304
  });
305
305
  } else {
306
+ // 冗余同步 scope 级联(P2 A7):kb 解绑工作区(workspaceId → null)时,
307
+ // 其文档的 syncWorkspaceId 必须同步置空,否则文档残留已消失的 scope
308
+ // (同步面按快照列归属,不回查 kb)。delete 分支文档随 FK 级联硬删,无需处理。
309
+ const unlinkedKbIds = (
310
+ await tx.knowledgeBase.findMany({
311
+ where: { workspaceId: data.workspaceId },
312
+ select: { id: true },
313
+ })
314
+ ).map((kb) => kb.id);
306
315
  await tx.knowledgeBase.updateMany({
307
316
  where: { workspaceId: data.workspaceId },
308
317
  data: { workspaceId: null },
309
318
  });
319
+ if (unlinkedKbIds.length > 0) {
320
+ await tx.knowledgeDocument.updateMany({
321
+ where: { kbId: { in: unlinkedKbIds } },
322
+ data: { syncWorkspaceId: null },
323
+ });
324
+ }
310
325
  }
311
326
 
312
327
  // 清理无 onDelete 级联的关联记录,避免外键约束阻止删除
@@ -24,6 +24,10 @@ export const SERVERFN_PUBLIC_ALLOWLIST = [
24
24
  'exchangeRemoteTokenFn',
25
25
  // 清除桌面端 remote-auth 持久化状态(登出语义,无会话也可调用)
26
26
  'logoutRemoteFn',
27
+ // 全量清空 profileCache 进程内缓存(A11 身份切换缓存三件套之一):调用时机是
28
+ // desktop-state 身份切换(登出语义延伸,会话可能已失效),只清进程内 Map
29
+ // 不暴露任何用户数据,与 logoutRemoteFn 同形态
30
+ 'resetAuthProfileCacheFn',
27
31
 
28
32
  // ── 登录/注册/初始化流程(鉴权前的入口)──
29
33
  // 登录页读取可用 OAuth provider 列表(未登录场景)