@nextclaw/ui 0.9.6 → 0.9.8
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/CHANGELOG.md +16 -2
- package/dist/assets/ChannelsList-CIMYaIji.js +1 -0
- package/dist/assets/{ChatPage-DM1ewbWf.js → ChatPage-B5UpeEIp.js} +2 -2
- package/dist/assets/{DocBrowser-BLv77lJ0.js → DocBrowser-BJ610SPa.js} +1 -1
- package/dist/assets/{LogoBadge-D7j1al-w.js → LogoBadge-BKq1GKWP.js} +1 -1
- package/dist/assets/MarketplacePage-Bs3sLsgx.js +49 -0
- package/dist/assets/{McpMarketplacePage-DpMjaD3m.js → McpMarketplacePage-BWTguHCs.js} +2 -2
- package/dist/assets/ModelConfig-B-oTP-Bc.js +1 -0
- package/dist/assets/ProvidersList-r7bD0-R0.js +1 -0
- package/dist/assets/RemoteAccessPage-D7On6waK.js +1 -0
- package/dist/assets/{RuntimeConfig-BbX4yFKy.js → RuntimeConfig-C11xVxH9.js} +1 -1
- package/dist/assets/{SearchConfig-BmmmeyJd.js → SearchConfig-BVZdCxiM.js} +1 -1
- package/dist/assets/{SecretsConfig-CWG8J01H.js → SecretsConfig-DuEDdC3X.js} +2 -2
- package/dist/assets/SessionsConfig-Y-Blf_-K.js +2 -0
- package/dist/assets/{chat-message-CGXiVhyN.js → chat-message-B6VCCEXF.js} +1 -1
- package/dist/assets/index-DfEAJJsA.css +1 -0
- package/dist/assets/index-DvA7S11O.js +8 -0
- package/dist/assets/{label-CCSffS1D.js → label-DzwitL78.js} +1 -1
- package/dist/assets/{page-layout-ud8wZ8gX.js → page-layout-DEq5N_8L.js} +1 -1
- package/dist/assets/popover-CY54V8F6.js +1 -0
- package/dist/assets/provider-models-BOeNnjk9.js +1 -0
- package/dist/assets/{security-config-DJJUCMov.js → security-config-CgbYP57d.js} +1 -1
- package/dist/assets/skeleton-zjQZMWu9.js +1 -0
- package/dist/assets/{status-dot-Fz9-eKsl.js → status-dot-CU_P0tvO.js} +1 -1
- package/dist/assets/{switch-B-_SrMSL.js → switch-PvjTvlcs.js} +1 -1
- package/dist/assets/{tabs-custom-6Tm1ZHfS.js → tabs-custom-Bke5J9ny.js} +1 -1
- package/dist/assets/useConfirmDialog-8tzzp_oW.js +1 -0
- package/dist/assets/vendor-CmQZsDAE.js +436 -0
- package/dist/index.html +3 -3
- package/package.json +4 -4
- package/src/App.tsx +36 -39
- package/src/account/components/account-panel.tsx +93 -0
- package/src/account/managers/account.manager.ts +179 -0
- package/src/account/stores/account.store.ts +68 -0
- package/src/api/types.ts +2 -0
- package/src/app-query-client.ts +10 -0
- package/src/components/config/ProviderForm.tsx +91 -641
- package/src/components/config/ProvidersList.tsx +10 -5
- package/src/components/config/provider-advanced-settings-section.tsx +92 -0
- package/src/components/config/provider-auth-section.tsx +113 -0
- package/src/components/config/provider-enabled-field.tsx +20 -0
- package/src/components/config/provider-form-support.ts +344 -0
- package/src/components/config/provider-models-section.tsx +198 -0
- package/src/components/config/provider-pill-selector.tsx +39 -0
- package/src/components/config/provider-status-badge.tsx +21 -0
- package/src/components/layout/Sidebar.tsx +26 -0
- package/src/components/remote/RemoteAccessPage.tsx +162 -442
- package/src/hooks/useRemoteAccess.ts +7 -6
- package/src/lib/i18n.remote.ts +108 -4
- package/src/lib/provider-models.ts +2 -2
- package/src/presenter/app-presenter-context.tsx +20 -0
- package/src/presenter/app.presenter.ts +12 -0
- package/src/remote/managers/remote-access.manager.ts +196 -0
- package/src/remote/remote-access.query.ts +78 -0
- package/src/remote/stores/remote-access.store.ts +44 -0
- package/dist/assets/ChannelsList-Byfj2R01.js +0 -1
- package/dist/assets/MarketplacePage-DuskLKYh.js +0 -49
- package/dist/assets/ModelConfig-ubaecweS.js +0 -1
- package/dist/assets/ProvidersList-w8MJH2LI.js +0 -1
- package/dist/assets/RemoteAccessPage-D79_5Kbn.js +0 -1
- package/dist/assets/SessionsConfig-D-vg_Lgv.js +0 -2
- package/dist/assets/index-COrhpAdh.css +0 -1
- package/dist/assets/index-CeRbsQ90.js +0 -8
- package/dist/assets/index-Ct7FQpxN.js +0 -1
- package/dist/assets/popover-Bfoe6YBX.js +0 -1
- package/dist/assets/provider-models-D3B_xWXx.js +0 -1
- package/dist/assets/skeleton-IOOTmHzP.js +0 -1
- package/dist/assets/useConfirmDialog-BeOW2bOI.js +0 -5
- package/dist/assets/vendor-CwsIoNvJ.js +0 -442
package/dist/index.html
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
8
8
|
<title>NextClaw - 系统配置</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-
|
|
11
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-DvA7S11O.js"></script>
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-CmQZsDAE.js">
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DfEAJJsA.css">
|
|
12
12
|
</head>
|
|
13
13
|
|
|
14
14
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/ui",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
"zod": "^3.23.8",
|
|
29
29
|
"zustand": "^5.0.2",
|
|
30
30
|
"@nextclaw/agent-chat": "0.1.1",
|
|
31
|
-
"@nextclaw/agent-chat-ui": "0.2.1",
|
|
32
|
-
"@nextclaw/ncp": "0.3.1",
|
|
33
31
|
"@nextclaw/ncp-http-agent-client": "0.3.1",
|
|
34
|
-
"@nextclaw/ncp-react": "0.3.2"
|
|
32
|
+
"@nextclaw/ncp-react": "0.3.2",
|
|
33
|
+
"@nextclaw/agent-chat-ui": "0.2.1",
|
|
34
|
+
"@nextclaw/ncp": "0.3.1"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@testing-library/react": "^16.3.0",
|
package/src/App.tsx
CHANGED
|
@@ -1,21 +1,15 @@
|
|
|
1
1
|
import { lazy, Suspense } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
3
|
+
import { AccountPanel } from '@/account/components/account-panel';
|
|
4
|
+
import { appQueryClient } from '@/app-query-client';
|
|
3
5
|
import { LoginPage } from '@/components/auth/login-page';
|
|
4
6
|
import { AppLayout } from '@/components/layout/AppLayout';
|
|
5
7
|
import { useAuthStatus } from '@/hooks/use-auth';
|
|
6
8
|
import { useWebSocket } from '@/hooks/useWebSocket';
|
|
9
|
+
import { AppPresenterProvider } from '@/presenter/app-presenter-context';
|
|
7
10
|
import { Toaster } from 'sonner';
|
|
8
11
|
import { Routes, Route, Navigate } from 'react-router-dom';
|
|
9
12
|
|
|
10
|
-
const queryClient = new QueryClient({
|
|
11
|
-
defaultOptions: {
|
|
12
|
-
queries: {
|
|
13
|
-
retry: 1,
|
|
14
|
-
refetchOnWindowFocus: true
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
|
|
19
13
|
const ModelConfigPage = lazy(async () => ({ default: (await import('@/components/config/ModelConfig')).ModelConfig }));
|
|
20
14
|
const ChatPage = lazy(async () => ({ default: (await import('@/components/chat/ChatPage')).ChatPage }));
|
|
21
15
|
const SearchConfigPage = lazy(async () => ({ default: (await import('@/components/config/SearchConfig')).SearchConfig }));
|
|
@@ -38,36 +32,39 @@ function LazyRoute({ children }: { children: JSX.Element }) {
|
|
|
38
32
|
}
|
|
39
33
|
|
|
40
34
|
function ProtectedApp() {
|
|
41
|
-
useWebSocket(
|
|
35
|
+
useWebSocket(appQueryClient); // Initialize WebSocket connection
|
|
42
36
|
|
|
43
37
|
return (
|
|
44
|
-
<
|
|
45
|
-
<
|
|
46
|
-
<
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
38
|
+
<AppPresenterProvider>
|
|
39
|
+
<AppLayout>
|
|
40
|
+
<div className="w-full h-full">
|
|
41
|
+
<Routes>
|
|
42
|
+
<Route path="/chat/skills" element={<Navigate to="/skills" replace />} />
|
|
43
|
+
<Route path="/chat/cron" element={<Navigate to="/cron" replace />} />
|
|
44
|
+
<Route path="/chat/:sessionId?" element={<LazyRoute><ChatPage view="chat" /></LazyRoute>} />
|
|
45
|
+
<Route path="/skills" element={<LazyRoute><ChatPage view="skills" /></LazyRoute>} />
|
|
46
|
+
<Route path="/cron" element={<LazyRoute><ChatPage view="cron" /></LazyRoute>} />
|
|
47
|
+
<Route path="/model" element={<LazyRoute><ModelConfigPage /></LazyRoute>} />
|
|
48
|
+
<Route path="/search" element={<LazyRoute><SearchConfigPage /></LazyRoute>} />
|
|
49
|
+
<Route path="/providers" element={<LazyRoute><ProvidersListPage /></LazyRoute>} />
|
|
50
|
+
<Route path="/channels" element={<LazyRoute><ChannelsListPage /></LazyRoute>} />
|
|
51
|
+
<Route path="/runtime" element={<LazyRoute><RuntimeConfigPage /></LazyRoute>} />
|
|
52
|
+
<Route path="/remote" element={<LazyRoute><RemoteAccessPage /></LazyRoute>} />
|
|
53
|
+
<Route path="/security" element={<LazyRoute><SecurityConfigPage /></LazyRoute>} />
|
|
54
|
+
<Route path="/sessions" element={<LazyRoute><SessionsConfigPage /></LazyRoute>} />
|
|
55
|
+
<Route path="/secrets" element={<LazyRoute><SecretsConfigPage /></LazyRoute>} />
|
|
56
|
+
<Route path="/settings" element={<Navigate to="/model" replace />} />
|
|
57
|
+
<Route path="/marketplace/skills" element={<Navigate to="/skills" replace />} />
|
|
58
|
+
<Route path="/marketplace" element={<Navigate to="/marketplace/plugins" replace />} />
|
|
59
|
+
<Route path="/marketplace/mcp" element={<LazyRoute><McpMarketplacePage /></LazyRoute>} />
|
|
60
|
+
<Route path="/marketplace/:type" element={<LazyRoute><MarketplacePage /></LazyRoute>} />
|
|
61
|
+
<Route path="/" element={<Navigate to="/chat" replace />} />
|
|
62
|
+
<Route path="*" element={<Navigate to="/chat" replace />} />
|
|
63
|
+
</Routes>
|
|
64
|
+
</div>
|
|
65
|
+
</AppLayout>
|
|
66
|
+
<AccountPanel />
|
|
67
|
+
</AppPresenterProvider>
|
|
71
68
|
);
|
|
72
69
|
}
|
|
73
70
|
|
|
@@ -87,7 +84,7 @@ function AuthGate() {
|
|
|
87
84
|
|
|
88
85
|
export default function AppContent() {
|
|
89
86
|
return (
|
|
90
|
-
<QueryClientProvider client={
|
|
87
|
+
<QueryClientProvider client={appQueryClient}>
|
|
91
88
|
<AuthGate />
|
|
92
89
|
<Toaster position="top-right" richColors />
|
|
93
90
|
</QueryClientProvider>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Button } from '@/components/ui/button';
|
|
2
|
+
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
|
3
|
+
import { useRemoteStatus } from '@/hooks/useRemoteAccess';
|
|
4
|
+
import { formatDateTime, t } from '@/lib/i18n';
|
|
5
|
+
import { useAccountStore } from '@/account/stores/account.store';
|
|
6
|
+
import { useAppPresenter } from '@/presenter/app-presenter-context';
|
|
7
|
+
import { KeyRound, LogOut, SquareArrowOutUpRight } from 'lucide-react';
|
|
8
|
+
import { useEffect } from 'react';
|
|
9
|
+
|
|
10
|
+
function AccountValueRow(props: { label: string; value?: string | null }) {
|
|
11
|
+
return (
|
|
12
|
+
<div className="flex items-start justify-between gap-4 py-2 text-sm">
|
|
13
|
+
<span className="text-gray-500">{props.label}</span>
|
|
14
|
+
<span className="text-right text-gray-900">{props.value?.trim() || '-'}</span>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function AccountPanel() {
|
|
20
|
+
const presenter = useAppPresenter();
|
|
21
|
+
const remoteStatus = useRemoteStatus();
|
|
22
|
+
const panelOpen = useAccountStore((state) => state.panelOpen);
|
|
23
|
+
const authSessionId = useAccountStore((state) => state.authSessionId);
|
|
24
|
+
const authVerificationUri = useAccountStore((state) => state.authVerificationUri);
|
|
25
|
+
const authExpiresAt = useAccountStore((state) => state.authExpiresAt);
|
|
26
|
+
const authStatusMessage = useAccountStore((state) => state.authStatusMessage);
|
|
27
|
+
const status = remoteStatus.data;
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
presenter.accountManager.syncRemoteStatus(status);
|
|
31
|
+
}, [presenter, status]);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Dialog open={panelOpen} onOpenChange={(open) => (open ? presenter.accountManager.openAccountPanel() : presenter.accountManager.closeAccountPanel())}>
|
|
35
|
+
<DialogContent className="max-w-xl">
|
|
36
|
+
<DialogHeader>
|
|
37
|
+
<DialogTitle className="flex items-center gap-2">
|
|
38
|
+
<KeyRound className="h-5 w-5 text-primary" />
|
|
39
|
+
{t('accountPanelTitle')}
|
|
40
|
+
</DialogTitle>
|
|
41
|
+
<DialogDescription>{t('accountPanelDescription')}</DialogDescription>
|
|
42
|
+
</DialogHeader>
|
|
43
|
+
|
|
44
|
+
{status?.account.loggedIn ? (
|
|
45
|
+
<div className="space-y-4">
|
|
46
|
+
<div className="rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3">
|
|
47
|
+
<p className="text-sm font-medium text-emerald-800">{t('accountPanelSignedInTitle')}</p>
|
|
48
|
+
<p className="mt-1 text-sm text-emerald-700">{t('accountPanelSignedInDescription')}</p>
|
|
49
|
+
</div>
|
|
50
|
+
<div className="rounded-2xl border border-gray-200 bg-gray-50 px-4 py-3">
|
|
51
|
+
<AccountValueRow label={t('remoteAccountEmail')} value={status.account.email} />
|
|
52
|
+
<AccountValueRow label={t('remoteAccountRole')} value={status.account.role} />
|
|
53
|
+
</div>
|
|
54
|
+
<div className="flex flex-wrap gap-3">
|
|
55
|
+
<Button onClick={() => void presenter.accountManager.openNextClawWeb()}>
|
|
56
|
+
<SquareArrowOutUpRight className="mr-2 h-4 w-4" />
|
|
57
|
+
{t('remoteOpenDeviceList')}
|
|
58
|
+
</Button>
|
|
59
|
+
<Button variant="outline" onClick={() => void presenter.accountManager.logout()}>
|
|
60
|
+
<LogOut className="mr-2 h-4 w-4" />
|
|
61
|
+
{t('remoteLogout')}
|
|
62
|
+
</Button>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
) : (
|
|
66
|
+
<div className="space-y-4">
|
|
67
|
+
<div className="rounded-2xl border border-gray-200 bg-gray-50 px-4 py-3">
|
|
68
|
+
<p className="text-sm font-medium text-gray-900">{t('accountPanelSignedOutTitle')}</p>
|
|
69
|
+
<p className="mt-1 text-sm text-gray-600">{t('accountPanelSignedOutDescription')}</p>
|
|
70
|
+
{authSessionId ? (
|
|
71
|
+
<div className="mt-3 border-t border-white/80 pt-3">
|
|
72
|
+
<AccountValueRow label={t('remoteBrowserAuthSession')} value={authSessionId} />
|
|
73
|
+
<AccountValueRow label={t('remoteBrowserAuthExpiresAt')} value={authExpiresAt ? formatDateTime(authExpiresAt) : '-'} />
|
|
74
|
+
</div>
|
|
75
|
+
) : null}
|
|
76
|
+
</div>
|
|
77
|
+
{authStatusMessage ? <p className="text-sm text-gray-600">{authStatusMessage}</p> : null}
|
|
78
|
+
<div className="flex flex-wrap gap-3">
|
|
79
|
+
<Button onClick={() => void presenter.accountManager.startBrowserSignIn()}>
|
|
80
|
+
{authSessionId ? t('remoteBrowserAuthActionRetry') : t('remoteBrowserAuthAction')}
|
|
81
|
+
</Button>
|
|
82
|
+
{authVerificationUri ? (
|
|
83
|
+
<Button variant="outline" onClick={() => presenter.accountManager.resumeBrowserSignIn()}>
|
|
84
|
+
{t('remoteBrowserAuthResume')}
|
|
85
|
+
</Button>
|
|
86
|
+
) : null}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
</DialogContent>
|
|
91
|
+
</Dialog>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { logoutRemote, pollRemoteBrowserAuth, startRemoteBrowserAuth } from '@/api/remote';
|
|
2
|
+
import type { RemoteAccessView } from '@/api/remote.types';
|
|
3
|
+
import type { AccountPendingAction } from '@/account/stores/account.store';
|
|
4
|
+
import { useAccountStore } from '@/account/stores/account.store';
|
|
5
|
+
import {
|
|
6
|
+
ensureRemoteStatus,
|
|
7
|
+
refreshRemoteStatus,
|
|
8
|
+
resolveRemotePlatformApiBase,
|
|
9
|
+
resolveRemoteWebBase
|
|
10
|
+
} from '@/remote/remote-access.query';
|
|
11
|
+
import { formatDateTime, t } from '@/lib/i18n';
|
|
12
|
+
import { toast } from 'sonner';
|
|
13
|
+
|
|
14
|
+
type SignedInContinuation = (action: AccountPendingAction, status: RemoteAccessView) => Promise<void>;
|
|
15
|
+
|
|
16
|
+
export class AccountManager {
|
|
17
|
+
private authPollTimerId: number | null = null;
|
|
18
|
+
|
|
19
|
+
private afterSignedIn: SignedInContinuation | null = null;
|
|
20
|
+
|
|
21
|
+
bindSignedInContinuation = (handler: SignedInContinuation) => {
|
|
22
|
+
this.afterSignedIn = handler;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
openAccountPanel = () => {
|
|
26
|
+
useAccountStore.getState().openPanel();
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
closeAccountPanel = () => {
|
|
30
|
+
useAccountStore.getState().closePanel();
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
syncRemoteStatus = (status: RemoteAccessView | undefined) => {
|
|
34
|
+
if (!status?.account.loggedIn) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
this.clearPollTimer();
|
|
38
|
+
useAccountStore.getState().clearBrowserAuth();
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
ensureSignedIn = async (params?: { pendingAction?: AccountPendingAction; apiBase?: string }) => {
|
|
42
|
+
const status = await ensureRemoteStatus();
|
|
43
|
+
if (status.account.loggedIn) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (params?.pendingAction) {
|
|
47
|
+
useAccountStore.getState().setPendingAction(params.pendingAction);
|
|
48
|
+
}
|
|
49
|
+
this.openAccountPanel();
|
|
50
|
+
await this.startBrowserSignIn({ apiBase: params?.apiBase, status });
|
|
51
|
+
return false;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
startBrowserSignIn = async (params?: { apiBase?: string; status?: RemoteAccessView }) => {
|
|
55
|
+
try {
|
|
56
|
+
const status = params?.status ?? (await ensureRemoteStatus());
|
|
57
|
+
const result = await startRemoteBrowserAuth({
|
|
58
|
+
apiBase: resolveRemotePlatformApiBase(status, params?.apiBase)
|
|
59
|
+
});
|
|
60
|
+
useAccountStore.getState().beginBrowserAuth({
|
|
61
|
+
sessionId: result.sessionId,
|
|
62
|
+
verificationUri: result.verificationUri,
|
|
63
|
+
expiresAt: result.expiresAt,
|
|
64
|
+
intervalMs: result.intervalMs,
|
|
65
|
+
statusMessage: t('remoteBrowserAuthWaiting')
|
|
66
|
+
});
|
|
67
|
+
const opened = window.open(result.verificationUri, '_blank', 'noopener,noreferrer');
|
|
68
|
+
if (!opened) {
|
|
69
|
+
useAccountStore.getState().setAuthStatusMessage(t('remoteBrowserAuthPopupBlocked'));
|
|
70
|
+
}
|
|
71
|
+
this.scheduleBrowserAuthPoll();
|
|
72
|
+
} catch (error) {
|
|
73
|
+
const message = error instanceof Error ? error.message : t('remoteBrowserAuthStartFailed');
|
|
74
|
+
toast.error(`${t('remoteBrowserAuthStartFailed')}: ${message}`);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
resumeBrowserSignIn = () => {
|
|
79
|
+
const verificationUri = useAccountStore.getState().authVerificationUri;
|
|
80
|
+
if (!verificationUri) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
window.open(verificationUri, '_blank', 'noopener,noreferrer');
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
logout = async () => {
|
|
87
|
+
try {
|
|
88
|
+
await logoutRemote();
|
|
89
|
+
useAccountStore.getState().clearPendingAction();
|
|
90
|
+
useAccountStore.getState().clearBrowserAuth();
|
|
91
|
+
await refreshRemoteStatus();
|
|
92
|
+
toast.success(t('remoteLogoutSuccess'));
|
|
93
|
+
} catch (error) {
|
|
94
|
+
const message = error instanceof Error ? error.message : t('remoteLogoutFailed');
|
|
95
|
+
toast.error(`${t('remoteLogoutFailed')}: ${message}`);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
openNextClawWeb = async () => {
|
|
100
|
+
const status = await ensureRemoteStatus();
|
|
101
|
+
const webBase = resolveRemoteWebBase(status);
|
|
102
|
+
if (!webBase) {
|
|
103
|
+
toast.error(t('remoteOpenWebUnavailable'));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
window.open(webBase, '_blank', 'noopener,noreferrer');
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
private scheduleBrowserAuthPoll = () => {
|
|
110
|
+
this.clearPollTimer();
|
|
111
|
+
const { authSessionId, authPollIntervalMs } = useAccountStore.getState();
|
|
112
|
+
if (!authSessionId) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
this.authPollTimerId = window.setTimeout(async () => {
|
|
116
|
+
await this.pollBrowserSignIn();
|
|
117
|
+
}, authPollIntervalMs);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
private pollBrowserSignIn = async () => {
|
|
121
|
+
const store = useAccountStore.getState();
|
|
122
|
+
if (!store.authSessionId) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const status = await ensureRemoteStatus();
|
|
128
|
+
const result = await pollRemoteBrowserAuth({
|
|
129
|
+
sessionId: store.authSessionId,
|
|
130
|
+
apiBase: resolveRemotePlatformApiBase(status)
|
|
131
|
+
});
|
|
132
|
+
if (result.status === 'pending') {
|
|
133
|
+
useAccountStore.getState().updateBrowserAuth({
|
|
134
|
+
statusMessage: t('remoteBrowserAuthWaiting'),
|
|
135
|
+
intervalMs: result.nextPollMs ?? 1500
|
|
136
|
+
});
|
|
137
|
+
this.scheduleBrowserAuthPoll();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (result.status === 'expired') {
|
|
141
|
+
this.clearPollTimer();
|
|
142
|
+
useAccountStore.getState().clearBrowserAuth();
|
|
143
|
+
toast.error(result.message || t('remoteBrowserAuthExpired'));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
useAccountStore.getState().setAuthStatusMessage(t('remoteBrowserAuthCompleted'));
|
|
148
|
+
const nextStatus = await refreshRemoteStatus();
|
|
149
|
+
const { pendingAction } = useAccountStore.getState();
|
|
150
|
+
this.clearPollTimer();
|
|
151
|
+
useAccountStore.getState().clearBrowserAuth();
|
|
152
|
+
toast.success(t('remoteLoginSuccess'));
|
|
153
|
+
if (pendingAction && this.afterSignedIn) {
|
|
154
|
+
await this.afterSignedIn(pendingAction, nextStatus);
|
|
155
|
+
}
|
|
156
|
+
useAccountStore.getState().clearPendingAction();
|
|
157
|
+
} catch (error) {
|
|
158
|
+
this.clearPollTimer();
|
|
159
|
+
useAccountStore.getState().clearBrowserAuth();
|
|
160
|
+
const message = error instanceof Error ? error.message : t('remoteBrowserAuthPollFailed');
|
|
161
|
+
toast.error(`${t('remoteBrowserAuthPollFailed')}: ${message}`);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
private clearPollTimer = () => {
|
|
166
|
+
if (this.authPollTimerId !== null) {
|
|
167
|
+
window.clearTimeout(this.authPollTimerId);
|
|
168
|
+
this.authPollTimerId = null;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
getBrowserAuthSummary = () => {
|
|
173
|
+
const store = useAccountStore.getState();
|
|
174
|
+
return {
|
|
175
|
+
sessionId: store.authSessionId,
|
|
176
|
+
expiresAt: store.authExpiresAt ? formatDateTime(store.authExpiresAt) : '-'
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
|
|
3
|
+
export type AccountPendingAction =
|
|
4
|
+
| {
|
|
5
|
+
type: 'enable-remote';
|
|
6
|
+
}
|
|
7
|
+
| null;
|
|
8
|
+
|
|
9
|
+
type AccountStoreState = {
|
|
10
|
+
panelOpen: boolean;
|
|
11
|
+
authSessionId: string | null;
|
|
12
|
+
authVerificationUri: string | null;
|
|
13
|
+
authExpiresAt: string | null;
|
|
14
|
+
authStatusMessage: string;
|
|
15
|
+
authPollIntervalMs: number;
|
|
16
|
+
pendingAction: AccountPendingAction;
|
|
17
|
+
openPanel: () => void;
|
|
18
|
+
closePanel: () => void;
|
|
19
|
+
setPendingAction: (next: AccountPendingAction) => void;
|
|
20
|
+
clearPendingAction: () => void;
|
|
21
|
+
beginBrowserAuth: (payload: {
|
|
22
|
+
sessionId: string;
|
|
23
|
+
verificationUri: string;
|
|
24
|
+
expiresAt: string;
|
|
25
|
+
intervalMs: number;
|
|
26
|
+
statusMessage: string;
|
|
27
|
+
}) => void;
|
|
28
|
+
updateBrowserAuth: (patch: { statusMessage?: string; intervalMs?: number }) => void;
|
|
29
|
+
clearBrowserAuth: () => void;
|
|
30
|
+
setAuthStatusMessage: (message: string) => void;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const useAccountStore = create<AccountStoreState>((set) => ({
|
|
34
|
+
panelOpen: false,
|
|
35
|
+
authSessionId: null,
|
|
36
|
+
authVerificationUri: null,
|
|
37
|
+
authExpiresAt: null,
|
|
38
|
+
authStatusMessage: '',
|
|
39
|
+
authPollIntervalMs: 1500,
|
|
40
|
+
pendingAction: null,
|
|
41
|
+
openPanel: () => set({ panelOpen: true }),
|
|
42
|
+
closePanel: () => set({ panelOpen: false }),
|
|
43
|
+
setPendingAction: (next) => set({ pendingAction: next }),
|
|
44
|
+
clearPendingAction: () => set({ pendingAction: null }),
|
|
45
|
+
beginBrowserAuth: ({ sessionId, verificationUri, expiresAt, intervalMs, statusMessage }) =>
|
|
46
|
+
set({
|
|
47
|
+
panelOpen: true,
|
|
48
|
+
authSessionId: sessionId,
|
|
49
|
+
authVerificationUri: verificationUri,
|
|
50
|
+
authExpiresAt: expiresAt,
|
|
51
|
+
authPollIntervalMs: intervalMs,
|
|
52
|
+
authStatusMessage: statusMessage
|
|
53
|
+
}),
|
|
54
|
+
updateBrowserAuth: ({ statusMessage, intervalMs }) =>
|
|
55
|
+
set((state) => ({
|
|
56
|
+
authStatusMessage: statusMessage ?? state.authStatusMessage,
|
|
57
|
+
authPollIntervalMs: intervalMs ?? state.authPollIntervalMs
|
|
58
|
+
})),
|
|
59
|
+
clearBrowserAuth: () =>
|
|
60
|
+
set({
|
|
61
|
+
authSessionId: null,
|
|
62
|
+
authVerificationUri: null,
|
|
63
|
+
authExpiresAt: null,
|
|
64
|
+
authStatusMessage: '',
|
|
65
|
+
authPollIntervalMs: 1500
|
|
66
|
+
}),
|
|
67
|
+
setAuthStatusMessage: (message) => set({ authStatusMessage: message })
|
|
68
|
+
}));
|
package/src/api/types.ts
CHANGED
|
@@ -19,6 +19,7 @@ export type AppMetaView = {
|
|
|
19
19
|
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "adaptive" | "xhigh";
|
|
20
20
|
|
|
21
21
|
export type ProviderConfigView = {
|
|
22
|
+
enabled: boolean;
|
|
22
23
|
displayName?: string;
|
|
23
24
|
apiKeySet: boolean;
|
|
24
25
|
apiKeyMasked?: string;
|
|
@@ -30,6 +31,7 @@ export type ProviderConfigView = {
|
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
export type ProviderConfigUpdate = {
|
|
34
|
+
enabled?: boolean;
|
|
33
35
|
displayName?: string | null;
|
|
34
36
|
apiKey?: string | null;
|
|
35
37
|
apiBase?: string | null;
|