@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.
- package/package.json +3 -3
- package/src/components/desktop/__tests__/desktop-state-listener.test.tsx +183 -0
- package/src/components/desktop/__tests__/sync-indicator.test.tsx +130 -0
- package/src/components/desktop/cloud-login.tsx +108 -6
- package/src/components/desktop/desktop-state-listener.tsx +76 -0
- package/src/components/desktop/shell-widgets.tsx +12 -5
- package/src/components/desktop/sync-indicator.tsx +62 -13
- package/src/components/layout/sidebar-03/__tests__/user-menu-anonymous.test.tsx +4 -0
- package/src/components/layout/sidebar-03/user-menu.tsx +63 -0
- package/src/lib/__tests__/session.test.ts +95 -0
- package/src/lib/auth/auth.config.ts +13 -0
- package/src/lib/changelog/sdk-versions.ts +1 -1
- package/src/lib/desktop-bridge-types.ts +36 -0
- package/src/lib/session.ts +15 -1
- package/src/paraglide/messages/_index.d.ts +11 -0
- package/src/paraglide/messages/_index.js +11 -0
- package/src/paraglide/messages/desktop_cloud_exchange_failed.d.ts +16 -0
- package/src/paraglide/messages/desktop_cloud_exchange_failed.js +29 -0
- package/src/paraglide/messages/desktop_state_cloud.d.ts +16 -0
- package/src/paraglide/messages/desktop_state_cloud.js +29 -0
- package/src/paraglide/messages/desktop_state_guest.d.ts +16 -0
- package/src/paraglide/messages/desktop_state_guest.js +29 -0
- package/src/paraglide/messages/desktop_state_transferring.d.ts +16 -0
- package/src/paraglide/messages/desktop_state_transferring.js +29 -0
- package/src/paraglide/messages/sync_bind_failed.d.ts +16 -0
- package/src/paraglide/messages/sync_bind_failed.js +29 -0
- package/src/paraglide/messages/sync_bind_in_progress.d.ts +16 -0
- package/src/paraglide/messages/sync_bind_in_progress.js +29 -0
- package/src/paraglide/messages/sync_bind_nonempty.d.ts +16 -0
- package/src/paraglide/messages/sync_bind_nonempty.js +29 -0
- package/src/paraglide/messages/sync_bind_probe_unreachable.d.ts +16 -0
- package/src/paraglide/messages/sync_bind_probe_unreachable.js +29 -0
- package/src/paraglide/messages/sync_bind_shell_unsupported.d.ts +16 -0
- package/src/paraglide/messages/sync_bind_shell_unsupported.js +29 -0
- package/src/paraglide/messages/sync_logout_failed.d.ts +16 -0
- package/src/paraglide/messages/sync_logout_failed.js +29 -0
- package/src/paraglide/messages/sync_logout_in_progress.d.ts +16 -0
- package/src/paraglide/messages/sync_logout_in_progress.js +29 -0
- package/src/routeTree.gen.ts +21 -0
- package/src/routes/api/sync/__tests__/account-status.test.ts +109 -0
- package/src/routes/api/sync/account-status.ts +39 -0
- package/src/server/__tests__/exchange-remote-token.test.ts +113 -0
- package/src/server/__tests__/remote-auth.test.ts +131 -1
- package/src/server/ai-gateway/__tests__/tools-authorization.test.ts +4 -2
- package/src/server/ai-gateway/tools/server.ts +7 -0
- package/src/server/desktop-bootstrap.ts +19 -3
- package/src/server/desktop-session-merge.ts +51 -0
- package/src/server/remote-auth.ts +61 -3
- package/src/server/serverFns/__tests__/knowledge-readable-kb.test.ts +21 -0
- package/src/server/serverFns/__tests__/workspace.test.ts +46 -0
- package/src/server/serverFns/auth/session.ts +28 -28
- package/src/server/serverFns/harness/attach-to-kb.ts +3 -1
- package/src/server/serverFns/knowledge.ts +4 -1
- package/src/server/serverFns/sync/transfer.ts +168 -0
- package/src/server/serverFns/workspace.ts +15 -0
- package/src/server/serverfn-public-allowlist.ts +4 -0
- package/src/server/sync/__tests__/ownership-transfer.test.ts +896 -0
- package/src/server/sync/account-status.ts +36 -0
- package/src/server/sync/ownership-transfer.ts +291 -0
- package/src/server/sync/saas-profile.ts +70 -0
|
@@ -5,8 +5,13 @@
|
|
|
5
5
|
// 桌面同步状态指示器(迁移设计 §3.7 最小 UI):顶栏小圆点 + 同步状态。
|
|
6
6
|
// 仅 DEPLOY_TARGET=desktop 渲染;云端部署零成本(组件返回 null)。
|
|
7
7
|
// 数据源:desktopBridge.onSyncState(backend parentPort 上报的引擎状态机)。
|
|
8
|
+
// A11 三态徽标:desktopBridge.desktopState 查询 + onDesktopState 订阅(main 侧
|
|
9
|
+
// desktop-state 状态机),展示数据属主三态(guest/transferring/cloud)——位置
|
|
10
|
+
// 跟随既有指示器(本组件扩展,不新造摆放位置),位于同步相位行上方。
|
|
8
11
|
import { useEffect, useState } from "react"
|
|
9
12
|
import { getDesktopBridge } from "@/lib/desktop-bridge-types"
|
|
13
|
+
import type { DesktopStateKind } from "@/lib/desktop-bridge-types"
|
|
14
|
+
import { m } from "@/paraglide/messages.js"
|
|
10
15
|
|
|
11
16
|
interface SyncState {
|
|
12
17
|
phase: "idle" | "pushing" | "pulling"
|
|
@@ -21,8 +26,30 @@ const LABELS: Record<SyncState["phase"], string> = {
|
|
|
21
26
|
pulling: "拉取中",
|
|
22
27
|
}
|
|
23
28
|
|
|
29
|
+
// 三态徽标形态(跟随既有 LOCAL 徽标 = 圆点 + 文案):guest 灰(本地属主)、
|
|
30
|
+
// transferring 琥珀脉冲(转移进行中)、cloud 绿(云属主,与「已同步」同色系)
|
|
31
|
+
const STATE_DOT: Record<DesktopStateKind, string> = {
|
|
32
|
+
guest: "bg-muted-foreground",
|
|
33
|
+
transferring: "bg-amber-500 animate-pulse",
|
|
34
|
+
cloud: "bg-emerald-500",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 文案在渲染时解析(paraglide 按请求/cookie locale——模块顶层调用会把 locale
|
|
38
|
+
// 固化在 import 时刻,语言切换后不更新);非导出模块函数保 Fast Refresh 纯组件边界
|
|
39
|
+
function stateLabelOf(kind: DesktopStateKind): string {
|
|
40
|
+
switch (kind) {
|
|
41
|
+
case "guest":
|
|
42
|
+
return m.desktop_state_guest()
|
|
43
|
+
case "transferring":
|
|
44
|
+
return m.desktop_state_transferring()
|
|
45
|
+
case "cloud":
|
|
46
|
+
return m.desktop_state_cloud()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
24
50
|
export function DesktopSyncIndicator() {
|
|
25
51
|
const [state, setState] = useState<SyncState | null>(null)
|
|
52
|
+
const [deskState, setDeskState] = useState<DesktopStateKind | null>(null)
|
|
26
53
|
const [mounted, setMounted] = useState(false)
|
|
27
54
|
|
|
28
55
|
useEffect(() => {
|
|
@@ -30,31 +57,53 @@ export function DesktopSyncIndicator() {
|
|
|
30
57
|
const bridge = getDesktopBridge()
|
|
31
58
|
if (!bridge) return
|
|
32
59
|
const off = bridge.onSyncState((s) => setState(s as SyncState))
|
|
33
|
-
|
|
60
|
+
// A11 三态:查询补当前态(页面 reload 后推送事件已错过)+ 订阅变更推送
|
|
61
|
+
void bridge.desktopState
|
|
62
|
+
?.()
|
|
63
|
+
.then((p) => setDeskState(p.kind))
|
|
64
|
+
.catch(() => null)
|
|
65
|
+
const offState = bridge.onDesktopState?.((p) => setDeskState(p.kind))
|
|
66
|
+
return () => {
|
|
67
|
+
off()
|
|
68
|
+
offState?.()
|
|
69
|
+
}
|
|
34
70
|
}, [])
|
|
35
71
|
|
|
36
72
|
if (!mounted || !getDesktopBridge()) return null
|
|
37
|
-
if (!state) return null
|
|
73
|
+
if (!state && !deskState) return null
|
|
38
74
|
|
|
39
|
-
const hasError = state
|
|
40
|
-
const busy = state
|
|
75
|
+
const hasError = state?.lastError !== null && state?.lastError !== undefined
|
|
76
|
+
const busy = state?.phase === "pushing" || state?.phase === "pulling"
|
|
41
77
|
const dotColor = hasError ? "bg-destructive" : busy ? "bg-amber-500 animate-pulse" : "bg-emerald-500"
|
|
42
78
|
const label = hasError
|
|
43
79
|
? "同步失败"
|
|
44
80
|
: busy
|
|
45
|
-
? LABELS[state
|
|
46
|
-
: state.pending > 0
|
|
81
|
+
? LABELS[state!.phase]
|
|
82
|
+
: state && state.pending > 0
|
|
47
83
|
? `待同步 ${state.pending}`
|
|
48
84
|
: LABELS.idle
|
|
49
85
|
|
|
50
86
|
return (
|
|
51
|
-
<div
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
87
|
+
<div className="space-y-0.5" data-testid="desktop-sync-indicator">
|
|
88
|
+
{deskState && (
|
|
89
|
+
<div
|
|
90
|
+
className="flex items-center gap-1.5 px-2 py-1 text-xs text-muted-foreground"
|
|
91
|
+
data-testid="desktop-state-badge"
|
|
92
|
+
data-desktop-state={deskState}
|
|
93
|
+
>
|
|
94
|
+
<span className={`inline-block size-2 rounded-full ${STATE_DOT[deskState]}`} aria-hidden />
|
|
95
|
+
<span>{stateLabelOf(deskState)}</span>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
{state && (
|
|
99
|
+
<div
|
|
100
|
+
className="flex items-center gap-1.5 px-2 py-1 text-xs text-muted-foreground"
|
|
101
|
+
title={hasError ? `同步失败:${state.lastError}` : `上次同步 ${state.lastSyncedAt ?? "—"}`}
|
|
102
|
+
>
|
|
103
|
+
<span className={`inline-block size-2 rounded-full ${dotColor}`} aria-hidden />
|
|
104
|
+
<span>{label}</span>
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
58
107
|
</div>
|
|
59
108
|
)
|
|
60
109
|
}
|
|
@@ -19,6 +19,10 @@ vi.mock('@/components/ui/sidebar', () => ({
|
|
|
19
19
|
}))
|
|
20
20
|
vi.mock('@/components/ui/toast', () => ({ toast: { success: vi.fn(), error: vi.fn() } }))
|
|
21
21
|
vi.mock('@/server/serverFns/auth/session', () => ({ logoutRemoteFn: vi.fn() }))
|
|
22
|
+
// A9:user-menu 反向转移登出链路新增 import——拦住 serverFn 模块图(勿真触
|
|
23
|
+
// desktop-bootstrap/@allbluecn/database 模块级单例,照本文件 session mock 先例)
|
|
24
|
+
vi.mock('@/server/serverFns/sync/transfer', () => ({ reverseTransferOwnershipFn: vi.fn() }))
|
|
25
|
+
vi.mock('@/lib/desktop-bridge-types', () => ({ getDesktopBridge: () => null }))
|
|
22
26
|
vi.mock('@/components/shared-kit', () => ({
|
|
23
27
|
UserAvatar: ({ name }: { name?: string }) => <div data-testid="avatar">{name}</div>,
|
|
24
28
|
}))
|
|
@@ -23,7 +23,11 @@ import {
|
|
|
23
23
|
Settings,
|
|
24
24
|
Users,
|
|
25
25
|
} from "lucide-react";
|
|
26
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
26
27
|
import { logoutRemoteFn } from '@/server/serverFns/auth/session'
|
|
28
|
+
import { reverseTransferOwnershipFn } from '@/server/serverFns/sync/transfer'
|
|
29
|
+
import { getDesktopBridge } from '@/lib/desktop-bridge-types'
|
|
30
|
+
import type { DesktopBridgeApi } from '@/lib/desktop-bridge-types'
|
|
27
31
|
import { toast } from '@/components/ui/toast'
|
|
28
32
|
|
|
29
33
|
import {
|
|
@@ -49,6 +53,56 @@ import type { AvatarConfig } from "@allbluecn/kernel";
|
|
|
49
53
|
import { SparklesText } from "@/components/magicui/sparkles-text";
|
|
50
54
|
import { m } from "@/paraglide/messages.js";
|
|
51
55
|
|
|
56
|
+
/**
|
|
57
|
+
* 桌面云态登出(桌面本地优先 P2-A9 反向转移编排,裁定顺序):登出 = 把云身份名下
|
|
58
|
+
* 数据翻回 guest(数据保留本机),双会话清理后回到 guest 态。
|
|
59
|
+
* ① 置转移锁(transferLock,复用 bind 锁文件——引擎每 tick 判定同源,「转移进行中
|
|
60
|
+
* 引擎勿 tick」;旧壳无此 IPC 优雅降级:反向转移单事务自身原子,锁为双保险);
|
|
61
|
+
* ② reverseTransferOwnershipFn(serverFn:校验 remoteAuth/会话本人 → 单事务
|
|
62
|
+
* cloud→guest + outbox/row_state 同事务清空防反向 op 上云 + 重建 guest 行;
|
|
63
|
+
* 事务提交后 server 侧已清 remote-auth——崩窗口收敛,A8b 发现 2 的镜像);
|
|
64
|
+
* ③ 双会话清理:logoutRemoteFn(幂等 re-clear remote-auth)+ clearCloudSession
|
|
65
|
+
* (清 cloud-session.json,引擎 guest 守卫回 idle)→ 清锁(先清云会话再解锁,
|
|
66
|
+
* 引擎恢复 tick 前云会话已不可用);
|
|
67
|
+
* ④ queryClient.clear() + 整页 reload——reload 后 getSessionFn 回落 guest 会话。
|
|
68
|
+
* 失败路径(锁失败 / 转移失败):解锁 + toast sync_logout_failed + 不 reload(留在
|
|
69
|
+
* 云态可重试;转移失败单事务已回滚,数据仍是云属主,重试幂等)。模块内局部函数
|
|
70
|
+
* (非导出):本文件唯一导出是 UserMenu 组件,勿破坏 Fast Refresh 纯组件边界。
|
|
71
|
+
*/
|
|
72
|
+
async function desktopCloudLogout(
|
|
73
|
+
bridge: DesktopBridgeApi,
|
|
74
|
+
cloudUserId: string,
|
|
75
|
+
queryClient: QueryClient,
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
toast.info(m.sync_logout_in_progress());
|
|
78
|
+
// ① 转移锁
|
|
79
|
+
let lock: { ok: boolean; error?: string } = { ok: true };
|
|
80
|
+
try {
|
|
81
|
+
lock = bridge.transferLock ? await bridge.transferLock("begin") : { ok: true };
|
|
82
|
+
} catch {
|
|
83
|
+
lock = { ok: false };
|
|
84
|
+
}
|
|
85
|
+
if (!lock.ok) {
|
|
86
|
+
toast.error(m.sync_logout_failed());
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// ② 反向转移(失败:单事务回滚留在云态,解锁可重试)
|
|
90
|
+
try {
|
|
91
|
+
await reverseTransferOwnershipFn({ data: { cloudUserId } });
|
|
92
|
+
} catch {
|
|
93
|
+
try { await bridge.transferLock?.("end"); } catch { /* 尽力而为 */ }
|
|
94
|
+
toast.error(m.sync_logout_failed());
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// ③ 双会话清理(均幂等 best-effort:serverFn 已清 remote-auth,此处兜底)
|
|
98
|
+
try { await logoutRemoteFn({ data: {} }); } catch { /* 尽力而为 */ }
|
|
99
|
+
try { await bridge.clearCloudSession?.(); } catch { /* 尽力而为 */ }
|
|
100
|
+
try { await bridge.transferLock?.("end"); } catch { /* 尽力而为 */ }
|
|
101
|
+
// ④ 缓存清空 + 整页刷新(reload 后 desktop-state 回 guest)
|
|
102
|
+
queryClient.clear();
|
|
103
|
+
window.location.reload();
|
|
104
|
+
}
|
|
105
|
+
|
|
52
106
|
export function UserMenu() {
|
|
53
107
|
const { isMobile } = useSidebar();
|
|
54
108
|
const { data: clientSession } = useSession();
|
|
@@ -83,6 +137,15 @@ export function UserMenu() {
|
|
|
83
137
|
window.location.reload();
|
|
84
138
|
return;
|
|
85
139
|
}
|
|
140
|
+
// 桌面云态(A9):桌面壳存在且无本地 authjs 会话(clientSession 为空)=当前
|
|
141
|
+
// 会话是 remote-auth 回落的轻量云身份,登出走反向转移编排(数据翻回 guest)。
|
|
142
|
+
// 本地凭据登录(clientSession 非空)与 web 照旧走 signOut 链——云身份回落仅在
|
|
143
|
+
// authjs 会话无效时生效,clientSession 为空是云态判别式。
|
|
144
|
+
const bridge = getDesktopBridge();
|
|
145
|
+
if (bridge && !clientSession && user?.id) {
|
|
146
|
+
await desktopCloudLogout(bridge, user.id, queryClient);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
86
149
|
await signOut();
|
|
87
150
|
queryClient.resetQueries({ queryKey: ['session'] });
|
|
88
151
|
await router.navigate({ to: "/login" });
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// AllBlue - 理想之海
|
|
3
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
+
|
|
5
|
+
// getCurrentSession 桌面回落单点测试(桌面本地优先 P2 / A10)。
|
|
6
|
+
// 优先级链钉死(复审③ B1/B2):authjs session(有效则返回)→ desktop 时
|
|
7
|
+
// remoteAuth → null。start-authjs / auth.config / remote-auth 全 mock:
|
|
8
|
+
// 不触真实 JWT 解密、不真读写 DESKTOP_USER_DATA_DIR 下的 remote-auth.json
|
|
9
|
+
// (#578 fs 实现勿真写盘)。desktop 判定走 DEPLOY_TARGET env stub
|
|
10
|
+
// (isDesktopServer 调用时读 env,见 desktop-guest.ts:40)。
|
|
11
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
12
|
+
import { getCurrentSession } from '../session'
|
|
13
|
+
import type { Session } from '../session'
|
|
14
|
+
|
|
15
|
+
const { getSessionMock, getAuthConfigMock, readRemoteAuthMock } = vi.hoisted(() => ({
|
|
16
|
+
getSessionMock: vi.fn(),
|
|
17
|
+
getAuthConfigMock: vi.fn(async () => ({})),
|
|
18
|
+
readRemoteAuthMock: vi.fn(),
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
vi.mock('start-authjs', () => ({
|
|
22
|
+
getSession: getSessionMock,
|
|
23
|
+
}))
|
|
24
|
+
|
|
25
|
+
vi.mock('@/lib/auth/auth.config', () => ({
|
|
26
|
+
getAuthConfig: getAuthConfigMock,
|
|
27
|
+
}))
|
|
28
|
+
|
|
29
|
+
vi.mock('@/server/remote-auth', () => ({
|
|
30
|
+
readRemoteAuth: readRemoteAuthMock,
|
|
31
|
+
}))
|
|
32
|
+
|
|
33
|
+
const AUTHED: Session = {
|
|
34
|
+
user: { id: 'authjs-user-1', name: 'AuthJS 用户' },
|
|
35
|
+
expires: '2026-01-01T00:00:00.000Z',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const REMOTE_AUTH = {
|
|
39
|
+
userId: 'cloud-user-abc',
|
|
40
|
+
planName: 'pro',
|
|
41
|
+
features: {},
|
|
42
|
+
sessionToken: 'cloud-session-token',
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const REQ = () => new Request('http://localhost/api/auth/session')
|
|
46
|
+
|
|
47
|
+
describe('getCurrentSession 桌面回落(A10 优先级链)', () => {
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
vi.unstubAllEnvs()
|
|
50
|
+
vi.clearAllMocks()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('①a authjs session 有效 → 直接返回,desktop 下也不读 remoteAuth', async () => {
|
|
54
|
+
vi.stubEnv('DEPLOY_TARGET', 'desktop')
|
|
55
|
+
getSessionMock.mockResolvedValue(AUTHED)
|
|
56
|
+
const session = await getCurrentSession(REQ())
|
|
57
|
+
expect(session?.user?.id).toBe('authjs-user-1')
|
|
58
|
+
expect(readRemoteAuthMock).not.toHaveBeenCalled()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('①b+② authjs 无效 + desktop + remoteAuth 有 → 组装轻量云身份(user.id=cloudUserId)', async () => {
|
|
62
|
+
vi.stubEnv('DEPLOY_TARGET', 'desktop')
|
|
63
|
+
getSessionMock.mockResolvedValue(null)
|
|
64
|
+
readRemoteAuthMock.mockResolvedValue(REMOTE_AUTH)
|
|
65
|
+
const session = await getCurrentSession(REQ())
|
|
66
|
+
// 审查重点 4 前半:回落 session 的属主必须是云 userId(非 guest / 非 authjs 残影)
|
|
67
|
+
expect(session?.user?.id).toBe('cloud-user-abc')
|
|
68
|
+
// 组装形态与 buildRemoteSession 对齐(RemoteAuthData 实际字段,不虚构)
|
|
69
|
+
expect(session?.user?.name).toBe('cloud-user-abc') // username 缺省回落 userId
|
|
70
|
+
expect(session?.user?.isGuest).toBe(false)
|
|
71
|
+
expect(session?.user?.hasAiKey).toBe(false)
|
|
72
|
+
expect(session?.expires).toBe('never')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('①c authjs 无效 + desktop + remoteAuth 无 → null', async () => {
|
|
76
|
+
vi.stubEnv('DEPLOY_TARGET', 'desktop')
|
|
77
|
+
getSessionMock.mockResolvedValue(null)
|
|
78
|
+
readRemoteAuthMock.mockResolvedValue(null)
|
|
79
|
+
expect(await getCurrentSession(REQ())).toBeNull()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('③ 非 desktop 不读 remoteAuth(env 判定先于 fs 读取)', async () => {
|
|
83
|
+
vi.stubEnv('DEPLOY_TARGET', '')
|
|
84
|
+
getSessionMock.mockResolvedValue(null)
|
|
85
|
+
expect(await getCurrentSession(REQ())).toBeNull()
|
|
86
|
+
expect(readRemoteAuthMock).not.toHaveBeenCalled()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('无 request → null(既有守卫不回归,不触发任何读取)', async () => {
|
|
90
|
+
vi.stubEnv('DEPLOY_TARGET', 'desktop')
|
|
91
|
+
expect(await getCurrentSession()).toBeNull()
|
|
92
|
+
expect(getSessionMock).not.toHaveBeenCalled()
|
|
93
|
+
expect(readRemoteAuthMock).not.toHaveBeenCalled()
|
|
94
|
+
})
|
|
95
|
+
})
|
|
@@ -74,6 +74,19 @@ export function invalidateProfileCache(userId: string): void {
|
|
|
74
74
|
profileCache.delete(userId)
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* 全量清空 profile 缓存(桌面本地优先 A11 缓存三件套之一):身份切换(guest↔
|
|
79
|
+
* cloud)后旧身份的缓存条目必须整体失效——按 userId 逐个 invalidate 需要知道旧
|
|
80
|
+
* id,身份切换场景里调用方(renderer 经 resetAuthProfileCacheFn)恰恰没有可靠
|
|
81
|
+
* 的旧 id,全量 clear 是唯一正确形态。服务端进程内存缓存,清空零风险(miss 后
|
|
82
|
+
* 自然回源)。桌面 guest/remote-auth 回落会话不经过本缓存(buildGuestSession /
|
|
83
|
+
* buildRemoteSession 不查 profileCache),本清理覆盖的是「本地凭据 authjs 会话
|
|
84
|
+
* 与桌面身份切换并存」的混合场景(getCachedProfile(token.id) 消费面)。
|
|
85
|
+
*/
|
|
86
|
+
export function resetProfileCache(): void {
|
|
87
|
+
profileCache.clear()
|
|
88
|
+
}
|
|
89
|
+
|
|
77
90
|
async function queryProfile(userId: string): Promise<CachedProfile | null> {
|
|
78
91
|
const [dbUser, providerConfigs, sub] = await Promise.all([
|
|
79
92
|
prisma.user.findUnique({
|
|
@@ -112,7 +112,7 @@ export const SDK_GROUPS: SdkGroup[] = [
|
|
|
112
112
|
"items": [
|
|
113
113
|
{
|
|
114
114
|
"name": "@allbluecn/web-app",
|
|
115
|
-
"version": "v0.23.
|
|
115
|
+
"version": "v0.23.3",
|
|
116
116
|
"lastUpdate": "Nov 1, 2026",
|
|
117
117
|
"source": "apps/web/package.json",
|
|
118
118
|
"docsUrl": "https://github.com/allbluecn/allblue"
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
export interface DesktopBridgeApi {
|
|
8
8
|
appInfo: () => Promise<{ version: string; clientId: string; platform: string; backendPort: number | null }>
|
|
9
9
|
cloudLogin: (email: string, password: string) => Promise<{ ok: boolean; userId?: string; error?: string }>
|
|
10
|
+
/** A10 轻量云身份激活:登录成功后取 cloud-session.json 解出的纯 session
|
|
11
|
+
* token(renderer 再走 exchangeRemoteTokenFn 换权益并写 remote-auth)。 */
|
|
12
|
+
exchangeCloudSession: () => Promise<{ ok: boolean; token?: string; error?: string }>
|
|
10
13
|
conflictRpc: (
|
|
11
14
|
method: "list" | "applyLocal" | "keepRemote" | "keepBoth",
|
|
12
15
|
...args: unknown[]
|
|
@@ -18,6 +21,39 @@ export interface DesktopBridgeApi {
|
|
|
18
21
|
onRenamed: (cb: (info: { table: string; from: string; to: string }) => void) => () => void
|
|
19
22
|
/** macOS 全屏态切换(enter/leave-full-screen);旧壳无此 API 时 undefined */
|
|
20
23
|
onFullscreenChange?: (cb: (fullscreen: boolean) => void) => () => void
|
|
24
|
+
/** A8b 归属转移 bind-flow(main 侧阶段编排;本段为 add-only 追加,不改既有段落):
|
|
25
|
+
* - bindBegin:停引擎自动 tick(bind 锁文件)+ PGlite 数据目录文件级快照;
|
|
26
|
+
* - bindDone:清快照 + 解锁 + 请求 backend 触发一次 push;
|
|
27
|
+
* - bindFail:停 backend → 还原快照 → 重启 backend(恢复 guest 态)+ 解锁。
|
|
28
|
+
* 转移 serverFn(transferOwnershipFn)由 renderer 自行调用(localhost cookie
|
|
29
|
+
* 过鉴权),不经本桥。旧壳(无此三方法)undefined。 */
|
|
30
|
+
bindBegin?: () => Promise<{ ok: boolean; error?: string }>
|
|
31
|
+
bindDone?: () => Promise<{ ok: boolean; error?: string }>
|
|
32
|
+
bindFail?: () => Promise<{ ok: boolean; error?: string }>
|
|
33
|
+
/** A9 登出反向转移(本段为 add-only 追加,不改既有段落):transferLock 置/清
|
|
34
|
+
* 转移锁——复用 bind 锁文件(引擎每 tick 判定同源,语义「转移进行中,引擎勿
|
|
35
|
+
* tick」);clearCloudSession 清 cloud-session.json(登出后引擎 guest 守卫回
|
|
36
|
+
* idle)。旧壳无此二方法 undefined——renderer 优雅降级(反向转移单事务自身
|
|
37
|
+
* 原子,锁为防 tick 并发的双保险;旧壳无云会话文件可清,调用方 optional-
|
|
38
|
+
* chaining)。 */
|
|
39
|
+
transferLock?: (action: "begin" | "end") => Promise<{ ok: boolean; error?: string }>
|
|
40
|
+
clearCloudSession?: () => Promise<{ ok: boolean; error?: string }>
|
|
41
|
+
/** A11 desktop-state 三态(本段为 add-only 追加,不改既有段落):main 侧状态机
|
|
42
|
+
* (guest | transferring | cloud,数据属主视角)的查询 + 推送双面。查询用于
|
|
43
|
+
* 页面 reload 后补齐错过的推送事件;推送由 main 侧 bind/登出流 dispatch 时
|
|
44
|
+
* 发送。旧壳(无此二方法)undefined——徽标与缓存失效监听 optional-chaining
|
|
45
|
+
* 优雅降级。 */
|
|
46
|
+
desktopState?: () => Promise<DesktopStatePayload>
|
|
47
|
+
onDesktopState?: (cb: (payload: DesktopStatePayload) => void) => () => void
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A11 desktop-state 三态载荷(main 侧 desktop-state.json 持久化态的广播形状):
|
|
51
|
+
* guest 本地访客属主 / transferring 归属转移进行中 / cloud 云账号属主。 */
|
|
52
|
+
export type DesktopStateKind = "guest" | "transferring" | "cloud"
|
|
53
|
+
|
|
54
|
+
export interface DesktopStatePayload {
|
|
55
|
+
kind: DesktopStateKind
|
|
56
|
+
updatedAt: string
|
|
21
57
|
}
|
|
22
58
|
|
|
23
59
|
declare global {
|
package/src/lib/session.ts
CHANGED
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
import { getSession } from 'start-authjs'
|
|
19
19
|
import type { AuthSession } from 'start-authjs'
|
|
20
20
|
import { getAuthConfig } from './auth/auth.config'
|
|
21
|
+
// A10(桌面本地优先 P2):单条豁免 import(eslint 文件级放行)——
|
|
22
|
+
// desktop-session-merge 为纯数据组装 + remote-auth 纯 fs 读取(node:fs +
|
|
23
|
+
// env 守卫),无 prisma/server 重依赖链,客户端/云端部署零副作用。
|
|
24
|
+
import { getDesktopRemoteSession } from '@/server/desktop-session-merge'
|
|
21
25
|
|
|
22
26
|
export type SessionUser = NonNullable<AuthSession['user']> & {
|
|
23
27
|
id?: string
|
|
@@ -42,7 +46,17 @@ export type Session = Omit<AuthSession, 'user'> & { user?: SessionUser | null }
|
|
|
42
46
|
export async function getCurrentSession(request?: Request): Promise<Session | null> {
|
|
43
47
|
if (!request) return null
|
|
44
48
|
try {
|
|
45
|
-
|
|
49
|
+
const session = (await getSession(request, await getAuthConfig())) as Session | null
|
|
50
|
+
// 有效性口径与 getSessionFn 一致:有 user.id 才算有效(session callback
|
|
51
|
+
// 对 DB 无行用户返回 user: null 的残影不算登录态)
|
|
52
|
+
if (session?.user?.id) return session
|
|
53
|
+
// 桌面本地优先(P2 A10):authjs session 无效且为 desktop 部署时,回落
|
|
54
|
+
// remote-auth.json 的轻量云身份(user.id = 云 userId)。本函数是鉴权单点
|
|
55
|
+
// (requireAuth/authContextMiddleware 全链复用),回落在此一处生效、
|
|
56
|
+
// 604 处自动受益;非 desktop 恒 null(desktop 判定先于 fs 读取,零 IO)。
|
|
57
|
+
const remoteSession = await getDesktopRemoteSession()
|
|
58
|
+
if (remoteSession) return remoteSession
|
|
59
|
+
return session
|
|
46
60
|
} catch (error) {
|
|
47
61
|
// JWT 解密失败(AUTH_SECRET 轮换 / 跨应用同名 cookie 污染)等效于无 session:
|
|
48
62
|
// 返回 null 走未登录分支;@auth/core 会自行清理坏 cookie。
|
|
@@ -1975,6 +1975,7 @@ export * from "./dashboard_stats_anyhubinstalled2.js";
|
|
|
1975
1975
|
export * from "./dashboard_stats_completedtoday1.js";
|
|
1976
1976
|
export * from "./dashboard_stats_prdtotal1.js";
|
|
1977
1977
|
export * from "./dashboard_stats_unreadcomments1.js";
|
|
1978
|
+
export * from "./desktop_cloud_exchange_failed.js";
|
|
1978
1979
|
export * from "./desktop_connection_cancel.js";
|
|
1979
1980
|
export * from "./desktop_connection_confirm.js";
|
|
1980
1981
|
export * from "./desktop_connection_description.js";
|
|
@@ -2012,6 +2013,9 @@ export * from "./desktop_onboarding_selfhosted_title1.js";
|
|
|
2012
2013
|
export * from "./desktop_onboarding_selfhosted_urllabel2.js";
|
|
2013
2014
|
export * from "./desktop_onboarding_subtitle.js";
|
|
2014
2015
|
export * from "./desktop_onboarding_title.js";
|
|
2016
|
+
export * from "./desktop_state_cloud.js";
|
|
2017
|
+
export * from "./desktop_state_guest.js";
|
|
2018
|
+
export * from "./desktop_state_transferring.js";
|
|
2015
2019
|
export * from "./detail_contentplaceholder1.js";
|
|
2016
2020
|
export * from "./detail_deletedescription1.js";
|
|
2017
2021
|
export * from "./detail_deletetitle1.js";
|
|
@@ -4905,6 +4909,13 @@ export * from "./stylefield_height1.js";
|
|
|
4905
4909
|
export * from "./stylefield_selectcolor2.js";
|
|
4906
4910
|
export * from "./stylefield_width1.js";
|
|
4907
4911
|
export * from "./summary_months.js";
|
|
4912
|
+
export * from "./sync_bind_failed.js";
|
|
4913
|
+
export * from "./sync_bind_in_progress.js";
|
|
4914
|
+
export * from "./sync_bind_nonempty.js";
|
|
4915
|
+
export * from "./sync_bind_probe_unreachable.js";
|
|
4916
|
+
export * from "./sync_bind_shell_unsupported.js";
|
|
4917
|
+
export * from "./sync_logout_failed.js";
|
|
4918
|
+
export * from "./sync_logout_in_progress.js";
|
|
4908
4919
|
export * from "./taginput_add1.js";
|
|
4909
4920
|
export * from "./taginput_placeholder1.js";
|
|
4910
4921
|
export * from "./taginput_remove1.js";
|
|
@@ -1977,6 +1977,7 @@ export * from './dashboard_stats_anyhubinstalled2.js'
|
|
|
1977
1977
|
export * from './dashboard_stats_completedtoday1.js'
|
|
1978
1978
|
export * from './dashboard_stats_prdtotal1.js'
|
|
1979
1979
|
export * from './dashboard_stats_unreadcomments1.js'
|
|
1980
|
+
export * from './desktop_cloud_exchange_failed.js'
|
|
1980
1981
|
export * from './desktop_connection_cancel.js'
|
|
1981
1982
|
export * from './desktop_connection_confirm.js'
|
|
1982
1983
|
export * from './desktop_connection_description.js'
|
|
@@ -2014,6 +2015,9 @@ export * from './desktop_onboarding_selfhosted_title1.js'
|
|
|
2014
2015
|
export * from './desktop_onboarding_selfhosted_urllabel2.js'
|
|
2015
2016
|
export * from './desktop_onboarding_subtitle.js'
|
|
2016
2017
|
export * from './desktop_onboarding_title.js'
|
|
2018
|
+
export * from './desktop_state_cloud.js'
|
|
2019
|
+
export * from './desktop_state_guest.js'
|
|
2020
|
+
export * from './desktop_state_transferring.js'
|
|
2017
2021
|
export * from './detail_contentplaceholder1.js'
|
|
2018
2022
|
export * from './detail_deletedescription1.js'
|
|
2019
2023
|
export * from './detail_deletetitle1.js'
|
|
@@ -4907,6 +4911,13 @@ export * from './stylefield_height1.js'
|
|
|
4907
4911
|
export * from './stylefield_selectcolor2.js'
|
|
4908
4912
|
export * from './stylefield_width1.js'
|
|
4909
4913
|
export * from './summary_months.js'
|
|
4914
|
+
export * from './sync_bind_failed.js'
|
|
4915
|
+
export * from './sync_bind_in_progress.js'
|
|
4916
|
+
export * from './sync_bind_nonempty.js'
|
|
4917
|
+
export * from './sync_bind_probe_unreachable.js'
|
|
4918
|
+
export * from './sync_bind_shell_unsupported.js'
|
|
4919
|
+
export * from './sync_logout_failed.js'
|
|
4920
|
+
export * from './sync_logout_in_progress.js'
|
|
4910
4921
|
export * from './taginput_add1.js'
|
|
4911
4922
|
export * from './taginput_placeholder1.js'
|
|
4912
4923
|
export * from './taginput_remove1.js'
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* | output |
|
|
3
|
+
* | --- |
|
|
4
|
+
* | "Cloud identity activation failed. Please retry." |
|
|
5
|
+
*
|
|
6
|
+
* @param {Desktop_Cloud_Exchange_FailedInputs} inputs
|
|
7
|
+
* @param {{ locale?: "zh" | "en" }} options
|
|
8
|
+
* @returns {LocalizedString}
|
|
9
|
+
*/
|
|
10
|
+
export const desktop_cloud_exchange_failed: ((inputs?: Desktop_Cloud_Exchange_FailedInputs, options?: {
|
|
11
|
+
locale?: "zh" | "en";
|
|
12
|
+
}) => LocalizedString) & import("../runtime.js").MessageMetadata<Desktop_Cloud_Exchange_FailedInputs, {
|
|
13
|
+
locale?: "zh" | "en";
|
|
14
|
+
}, {}>;
|
|
15
|
+
export type LocalizedString = import("../runtime.js").LocalizedString;
|
|
16
|
+
export type Desktop_Cloud_Exchange_FailedInputs = {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
import { getLocale, experimentalStaticLocale } from '../runtime.js';
|
|
3
|
+
|
|
4
|
+
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
|
|
5
|
+
|
|
6
|
+
/** @typedef {{}} Desktop_Cloud_Exchange_FailedInputs */
|
|
7
|
+
|
|
8
|
+
const zh_desktop_cloud_exchange_failed = /** @type {(inputs: Desktop_Cloud_Exchange_FailedInputs) => LocalizedString} */ () => {
|
|
9
|
+
return /** @type {LocalizedString} */ (`云端身份激活失败,请重试`)
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const en_desktop_cloud_exchange_failed = /** @type {(inputs: Desktop_Cloud_Exchange_FailedInputs) => LocalizedString} */ () => {
|
|
13
|
+
return /** @type {LocalizedString} */ (`Cloud identity activation failed. Please retry.`)
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* | output |
|
|
18
|
+
* | --- |
|
|
19
|
+
* | "Cloud identity activation failed. Please retry." |
|
|
20
|
+
*
|
|
21
|
+
* @param {Desktop_Cloud_Exchange_FailedInputs} inputs
|
|
22
|
+
* @param {{ locale?: "zh" | "en" }} options
|
|
23
|
+
* @returns {LocalizedString}
|
|
24
|
+
*/
|
|
25
|
+
export const desktop_cloud_exchange_failed = /** @type {((inputs?: Desktop_Cloud_Exchange_FailedInputs, options?: { locale?: "zh" | "en" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Desktop_Cloud_Exchange_FailedInputs, { locale?: "zh" | "en" }, {}>} */ ((inputs = {}, options = {}) => {
|
|
26
|
+
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
|
|
27
|
+
if (locale === "en") return en_desktop_cloud_exchange_failed(inputs)
|
|
28
|
+
return zh_desktop_cloud_exchange_failed(inputs)
|
|
29
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* | output |
|
|
3
|
+
* | --- |
|
|
4
|
+
* | "Cloud synced" |
|
|
5
|
+
*
|
|
6
|
+
* @param {Desktop_State_CloudInputs} inputs
|
|
7
|
+
* @param {{ locale?: "zh" | "en" }} options
|
|
8
|
+
* @returns {LocalizedString}
|
|
9
|
+
*/
|
|
10
|
+
export const desktop_state_cloud: ((inputs?: Desktop_State_CloudInputs, options?: {
|
|
11
|
+
locale?: "zh" | "en";
|
|
12
|
+
}) => LocalizedString) & import("../runtime.js").MessageMetadata<Desktop_State_CloudInputs, {
|
|
13
|
+
locale?: "zh" | "en";
|
|
14
|
+
}, {}>;
|
|
15
|
+
export type LocalizedString = import("../runtime.js").LocalizedString;
|
|
16
|
+
export type Desktop_State_CloudInputs = {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
import { getLocale, experimentalStaticLocale } from '../runtime.js';
|
|
3
|
+
|
|
4
|
+
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
|
|
5
|
+
|
|
6
|
+
/** @typedef {{}} Desktop_State_CloudInputs */
|
|
7
|
+
|
|
8
|
+
const zh_desktop_state_cloud = /** @type {(inputs: Desktop_State_CloudInputs) => LocalizedString} */ () => {
|
|
9
|
+
return /** @type {LocalizedString} */ (`云端已绑定`)
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const en_desktop_state_cloud = /** @type {(inputs: Desktop_State_CloudInputs) => LocalizedString} */ () => {
|
|
13
|
+
return /** @type {LocalizedString} */ (`Cloud synced`)
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* | output |
|
|
18
|
+
* | --- |
|
|
19
|
+
* | "Cloud synced" |
|
|
20
|
+
*
|
|
21
|
+
* @param {Desktop_State_CloudInputs} inputs
|
|
22
|
+
* @param {{ locale?: "zh" | "en" }} options
|
|
23
|
+
* @returns {LocalizedString}
|
|
24
|
+
*/
|
|
25
|
+
export const desktop_state_cloud = /** @type {((inputs?: Desktop_State_CloudInputs, options?: { locale?: "zh" | "en" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Desktop_State_CloudInputs, { locale?: "zh" | "en" }, {}>} */ ((inputs = {}, options = {}) => {
|
|
26
|
+
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
|
|
27
|
+
if (locale === "en") return en_desktop_state_cloud(inputs)
|
|
28
|
+
return zh_desktop_state_cloud(inputs)
|
|
29
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* | output |
|
|
3
|
+
* | --- |
|
|
4
|
+
* | "Local mode" |
|
|
5
|
+
*
|
|
6
|
+
* @param {Desktop_State_GuestInputs} inputs
|
|
7
|
+
* @param {{ locale?: "zh" | "en" }} options
|
|
8
|
+
* @returns {LocalizedString}
|
|
9
|
+
*/
|
|
10
|
+
export const desktop_state_guest: ((inputs?: Desktop_State_GuestInputs, options?: {
|
|
11
|
+
locale?: "zh" | "en";
|
|
12
|
+
}) => LocalizedString) & import("../runtime.js").MessageMetadata<Desktop_State_GuestInputs, {
|
|
13
|
+
locale?: "zh" | "en";
|
|
14
|
+
}, {}>;
|
|
15
|
+
export type LocalizedString = import("../runtime.js").LocalizedString;
|
|
16
|
+
export type Desktop_State_GuestInputs = {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
import { getLocale, experimentalStaticLocale } from '../runtime.js';
|
|
3
|
+
|
|
4
|
+
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
|
|
5
|
+
|
|
6
|
+
/** @typedef {{}} Desktop_State_GuestInputs */
|
|
7
|
+
|
|
8
|
+
const zh_desktop_state_guest = /** @type {(inputs: Desktop_State_GuestInputs) => LocalizedString} */ () => {
|
|
9
|
+
return /** @type {LocalizedString} */ (`本地模式`)
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const en_desktop_state_guest = /** @type {(inputs: Desktop_State_GuestInputs) => LocalizedString} */ () => {
|
|
13
|
+
return /** @type {LocalizedString} */ (`Local mode`)
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* | output |
|
|
18
|
+
* | --- |
|
|
19
|
+
* | "Local mode" |
|
|
20
|
+
*
|
|
21
|
+
* @param {Desktop_State_GuestInputs} inputs
|
|
22
|
+
* @param {{ locale?: "zh" | "en" }} options
|
|
23
|
+
* @returns {LocalizedString}
|
|
24
|
+
*/
|
|
25
|
+
export const desktop_state_guest = /** @type {((inputs?: Desktop_State_GuestInputs, options?: { locale?: "zh" | "en" }) => LocalizedString) & import('../runtime.js').MessageMetadata<Desktop_State_GuestInputs, { locale?: "zh" | "en" }, {}>} */ ((inputs = {}, options = {}) => {
|
|
26
|
+
const locale = experimentalStaticLocale ?? options.locale ?? getLocale()
|
|
27
|
+
if (locale === "en") return en_desktop_state_guest(inputs)
|
|
28
|
+
return zh_desktop_state_guest(inputs)
|
|
29
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* | output |
|
|
3
|
+
* | --- |
|
|
4
|
+
* | "Transferring data…" |
|
|
5
|
+
*
|
|
6
|
+
* @param {Desktop_State_TransferringInputs} inputs
|
|
7
|
+
* @param {{ locale?: "zh" | "en" }} options
|
|
8
|
+
* @returns {LocalizedString}
|
|
9
|
+
*/
|
|
10
|
+
export const desktop_state_transferring: ((inputs?: Desktop_State_TransferringInputs, options?: {
|
|
11
|
+
locale?: "zh" | "en";
|
|
12
|
+
}) => LocalizedString) & import("../runtime.js").MessageMetadata<Desktop_State_TransferringInputs, {
|
|
13
|
+
locale?: "zh" | "en";
|
|
14
|
+
}, {}>;
|
|
15
|
+
export type LocalizedString = import("../runtime.js").LocalizedString;
|
|
16
|
+
export type Desktop_State_TransferringInputs = {};
|