@nextclaw/ui 0.9.8 → 0.9.10
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 +12 -0
- package/dist/assets/ChannelsList-BgJbR6E9.js +1 -0
- package/dist/assets/{ChatPage-B5UpeEIp.js → ChatPage-Bv9UJPse.js} +24 -24
- package/dist/assets/{DocBrowser-BJ610SPa.js → DocBrowser-Dw9BGO1m.js} +1 -1
- package/dist/assets/{LogoBadge-BKq1GKWP.js → LogoBadge-CLc2B6st.js} +1 -1
- package/dist/assets/{MarketplacePage-Bs3sLsgx.js → MarketplacePage-ChqCNL7k.js} +1 -1
- package/dist/assets/{McpMarketplacePage-BWTguHCs.js → McpMarketplacePage-B3PF-7ED.js} +1 -1
- package/dist/assets/ModelConfig-Dqz_NOow.js +1 -0
- package/dist/assets/{ProvidersList-r7bD0-R0.js → ProvidersList-D2WaZShJ.js} +1 -1
- package/dist/assets/{RemoteAccessPage-D7On6waK.js → RemoteAccessPage-D_l9irp4.js} +1 -1
- package/dist/assets/{RuntimeConfig-C11xVxH9.js → RuntimeConfig-TDxQLuGy.js} +1 -1
- package/dist/assets/{SearchConfig-BVZdCxiM.js → SearchConfig-gba64nGn.js} +1 -1
- package/dist/assets/{SecretsConfig-DuEDdC3X.js → SecretsConfig-DpL8wgly.js} +1 -1
- package/dist/assets/{SessionsConfig-Y-Blf_-K.js → SessionsConfig-CAODVTNW.js} +1 -1
- package/dist/assets/{chat-message-B6VCCEXF.js → chat-message-CSG50nNb.js} +1 -1
- package/dist/assets/index-DaEflNCE.js +8 -0
- package/dist/assets/{label-DzwitL78.js → label-3T28q3PJ.js} +1 -1
- package/dist/assets/{page-layout-DEq5N_8L.js → page-layout-BrXOQeua.js} +1 -1
- package/dist/assets/{popover-CY54V8F6.js → popover-BrBJjElY.js} +1 -1
- package/dist/assets/{security-config-CgbYP57d.js → security-config-oGAhN4Zf.js} +1 -1
- package/dist/assets/{skeleton-zjQZMWu9.js → skeleton-CIPQUKo2.js} +1 -1
- package/dist/assets/{status-dot-CU_P0tvO.js → status-dot-QL3hmT1d.js} +1 -1
- package/dist/assets/{switch-PvjTvlcs.js → switch-Dbt2kUg2.js} +1 -1
- package/dist/assets/{tabs-custom-Bke5J9ny.js → tabs-custom-y5hdkzXk.js} +1 -1
- package/dist/assets/{useConfirmDialog-8tzzp_oW.js → useConfirmDialog-B4zwBVbl.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +2 -2
- package/src/App.tsx +2 -2
- package/src/api/client.ts +6 -6
- package/src/api/config.ts +3 -2
- package/src/api/remote.types.ts +0 -1
- package/src/components/chat/chat-stream/transport.ts +67 -16
- package/src/components/config/ModelConfig.test.tsx +78 -0
- package/src/components/config/ModelConfig.tsx +4 -1
- package/src/components/config/provider-form-support.ts +2 -1
- package/src/hooks/use-realtime-query-bridge.ts +108 -0
- package/src/lib/i18n.remote.ts +3 -3
- package/src/transport/app-client.ts +107 -0
- package/src/transport/index.ts +9 -0
- package/src/transport/local.transport.ts +286 -0
- package/src/transport/remote.transport.ts +398 -0
- package/src/transport/transport.types.ts +41 -0
- package/dist/assets/ChannelsList-CIMYaIji.js +0 -1
- package/dist/assets/ModelConfig-B-oTP-Bc.js +0 -1
- package/dist/assets/index-DvA7S11O.js +0 -8
- package/src/api/websocket.ts +0 -79
- package/src/hooks/useWebSocket.ts +0 -190
package/dist/index.html
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
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-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-DaEflNCE.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vendor-CmQZsDAE.js">
|
|
11
11
|
<link rel="stylesheet" crossorigin href="/assets/index-DfEAJJsA.css">
|
|
12
12
|
</head>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/ui",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.10",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"zustand": "^5.0.2",
|
|
30
30
|
"@nextclaw/agent-chat": "0.1.1",
|
|
31
31
|
"@nextclaw/ncp-http-agent-client": "0.3.1",
|
|
32
|
-
"@nextclaw/ncp-react": "0.3.2",
|
|
33
32
|
"@nextclaw/agent-chat-ui": "0.2.1",
|
|
33
|
+
"@nextclaw/ncp-react": "0.3.2",
|
|
34
34
|
"@nextclaw/ncp": "0.3.1"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
package/src/App.tsx
CHANGED
|
@@ -5,7 +5,7 @@ import { appQueryClient } from '@/app-query-client';
|
|
|
5
5
|
import { LoginPage } from '@/components/auth/login-page';
|
|
6
6
|
import { AppLayout } from '@/components/layout/AppLayout';
|
|
7
7
|
import { useAuthStatus } from '@/hooks/use-auth';
|
|
8
|
-
import {
|
|
8
|
+
import { useRealtimeQueryBridge } from '@/hooks/use-realtime-query-bridge';
|
|
9
9
|
import { AppPresenterProvider } from '@/presenter/app-presenter-context';
|
|
10
10
|
import { Toaster } from 'sonner';
|
|
11
11
|
import { Routes, Route, Navigate } from 'react-router-dom';
|
|
@@ -32,7 +32,7 @@ function LazyRoute({ children }: { children: JSX.Element }) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
function ProtectedApp() {
|
|
35
|
-
|
|
35
|
+
useRealtimeQueryBridge(appQueryClient);
|
|
36
36
|
|
|
37
37
|
return (
|
|
38
38
|
<AppPresenterProvider>
|
package/src/api/client.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ApiResponse } from './types';
|
|
2
2
|
|
|
3
|
-
const DEFAULT_API_BASE = 'http://127.0.0.1:
|
|
3
|
+
const DEFAULT_API_BASE = 'http://127.0.0.1:55667';
|
|
4
4
|
const API_BASE = (() => {
|
|
5
5
|
const envBase = import.meta.env.VITE_API_BASE?.trim();
|
|
6
6
|
if (envBase) {
|
|
@@ -42,7 +42,7 @@ function inferNonJsonHint(endpoint: string, status: number): string | undefined
|
|
|
42
42
|
return undefined;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
async function
|
|
45
|
+
export async function requestApiResponse<T>(
|
|
46
46
|
endpoint: string,
|
|
47
47
|
options: RequestInit = {}
|
|
48
48
|
): Promise<ApiResponse<T>> {
|
|
@@ -104,19 +104,19 @@ async function apiRequest<T>(
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
export const api = {
|
|
107
|
-
get: <T>(path: string) =>
|
|
107
|
+
get: <T>(path: string) => requestApiResponse<T>(path, { method: 'GET' }),
|
|
108
108
|
put: <T>(path: string, body: unknown) =>
|
|
109
|
-
|
|
109
|
+
requestApiResponse<T>(path, {
|
|
110
110
|
method: 'PUT',
|
|
111
111
|
body: JSON.stringify(body)
|
|
112
112
|
}),
|
|
113
113
|
post: <T>(path: string, body: unknown) =>
|
|
114
|
-
|
|
114
|
+
requestApiResponse<T>(path, {
|
|
115
115
|
method: 'POST',
|
|
116
116
|
body: JSON.stringify(body)
|
|
117
117
|
}),
|
|
118
118
|
delete: <T>(path: string) =>
|
|
119
|
-
|
|
119
|
+
requestApiResponse<T>(path, {
|
|
120
120
|
method: 'DELETE'
|
|
121
121
|
})
|
|
122
122
|
};
|
package/src/api/config.ts
CHANGED
|
@@ -144,8 +144,9 @@ export async function fetchConfigSchema(): Promise<ConfigSchemaResponse> {
|
|
|
144
144
|
// PUT /api/config/model
|
|
145
145
|
export async function updateModel(data: {
|
|
146
146
|
model: string;
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
workspace?: string;
|
|
148
|
+
}): Promise<{ model: string; workspace?: string }> {
|
|
149
|
+
const response = await api.put<{ model: string; workspace?: string }>('/api/config/model', data);
|
|
149
150
|
if (!response.ok) {
|
|
150
151
|
throw new Error(response.error.message);
|
|
151
152
|
}
|
package/src/api/remote.types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { fetchChatRuns,
|
|
1
|
+
import { fetchChatRuns, stopChatTurn } from '@/api/config';
|
|
2
|
+
import { appClient } from '@/transport';
|
|
2
3
|
import type { ActiveRunState, SendMessageParams, StreamDeltaEvent, StreamReadyEvent, StreamSessionEvent } from './types';
|
|
3
4
|
|
|
4
5
|
function buildSendTurnPayload(item: SendMessageParams, requestedSkills: string[]) {
|
|
@@ -32,12 +33,37 @@ export async function openSendTurnStream(params: {
|
|
|
32
33
|
onDelta: (event: StreamDeltaEvent) => void;
|
|
33
34
|
onSessionEvent: (event: StreamSessionEvent) => void;
|
|
34
35
|
}) {
|
|
35
|
-
|
|
36
|
+
let readySessionKey = '';
|
|
37
|
+
const session = appClient.openStream<{ reply?: string; sessionKey?: string }>({
|
|
38
|
+
method: 'POST',
|
|
39
|
+
path: '/api/chat/turn/stream',
|
|
40
|
+
body: buildSendTurnPayload(params.item, params.requestedSkills),
|
|
36
41
|
signal: params.signal,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
onEvent: (event) => {
|
|
43
|
+
if (event.name === 'ready') {
|
|
44
|
+
const payload = (event.payload ?? {}) as StreamReadyEvent;
|
|
45
|
+
if (typeof payload.sessionKey === 'string' && payload.sessionKey.trim()) {
|
|
46
|
+
readySessionKey = payload.sessionKey;
|
|
47
|
+
}
|
|
48
|
+
params.onReady(payload);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (event.name === 'delta') {
|
|
52
|
+
params.onDelta((event.payload ?? { delta: '' }) as StreamDeltaEvent);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (event.name === 'session_event') {
|
|
56
|
+
params.onSessionEvent({ data: event.payload as StreamSessionEvent['data'] });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
40
59
|
});
|
|
60
|
+
const result = await session.finished;
|
|
61
|
+
return {
|
|
62
|
+
sessionKey: typeof result?.sessionKey === 'string' && result.sessionKey.trim()
|
|
63
|
+
? result.sessionKey
|
|
64
|
+
: readySessionKey,
|
|
65
|
+
reply: typeof result?.reply === 'string' ? result.reply : ''
|
|
66
|
+
};
|
|
41
67
|
}
|
|
42
68
|
|
|
43
69
|
export async function openResumeRunStream(params: {
|
|
@@ -48,18 +74,43 @@ export async function openResumeRunStream(params: {
|
|
|
48
74
|
onDelta: (event: StreamDeltaEvent) => void;
|
|
49
75
|
onSessionEvent: (event: StreamSessionEvent) => void;
|
|
50
76
|
}) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
77
|
+
let readySessionKey = '';
|
|
78
|
+
const query = new URLSearchParams();
|
|
79
|
+
if (typeof params.fromEventIndex === 'number') {
|
|
80
|
+
query.set('fromEventIndex', String(Math.max(0, Math.trunc(params.fromEventIndex))));
|
|
81
|
+
}
|
|
82
|
+
const path =
|
|
83
|
+
`/api/chat/runs/${encodeURIComponent(params.runId)}/stream`
|
|
84
|
+
+ (query.size > 0 ? `?${query.toString()}` : '');
|
|
85
|
+
const session = appClient.openStream<{ reply?: string; sessionKey?: string }>({
|
|
86
|
+
method: 'GET',
|
|
87
|
+
path,
|
|
88
|
+
signal: params.signal,
|
|
89
|
+
onEvent: (event) => {
|
|
90
|
+
if (event.name === 'ready') {
|
|
91
|
+
const payload = (event.payload ?? {}) as StreamReadyEvent;
|
|
92
|
+
if (typeof payload.sessionKey === 'string' && payload.sessionKey.trim()) {
|
|
93
|
+
readySessionKey = payload.sessionKey;
|
|
94
|
+
}
|
|
95
|
+
params.onReady(payload);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (event.name === 'delta') {
|
|
99
|
+
params.onDelta((event.payload ?? { delta: '' }) as StreamDeltaEvent);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (event.name === 'session_event') {
|
|
103
|
+
params.onSessionEvent({ data: event.payload as StreamSessionEvent['data'] });
|
|
104
|
+
}
|
|
61
105
|
}
|
|
62
|
-
);
|
|
106
|
+
});
|
|
107
|
+
const result = await session.finished;
|
|
108
|
+
return {
|
|
109
|
+
sessionKey: typeof result?.sessionKey === 'string' && result.sessionKey.trim()
|
|
110
|
+
? result.sessionKey
|
|
111
|
+
: readySessionKey,
|
|
112
|
+
reply: typeof result?.reply === 'string' ? result.reply : ''
|
|
113
|
+
};
|
|
63
114
|
}
|
|
64
115
|
|
|
65
116
|
export async function requestStopRun(activeRun: ActiveRunState): Promise<void> {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { ModelConfig } from '@/components/config/ModelConfig';
|
|
4
|
+
|
|
5
|
+
const mocks = vi.hoisted(() => ({
|
|
6
|
+
mutate: vi.fn(),
|
|
7
|
+
configQuery: {
|
|
8
|
+
data: {
|
|
9
|
+
agents: {
|
|
10
|
+
defaults: {
|
|
11
|
+
model: 'openai/gpt-5.2',
|
|
12
|
+
workspace: '~/old-workspace'
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
providers: {
|
|
16
|
+
openai: {
|
|
17
|
+
enabled: true,
|
|
18
|
+
apiKeySet: true,
|
|
19
|
+
models: ['gpt-5.2']
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
isLoading: false
|
|
24
|
+
},
|
|
25
|
+
metaQuery: {
|
|
26
|
+
data: {
|
|
27
|
+
providers: [
|
|
28
|
+
{
|
|
29
|
+
name: 'openai',
|
|
30
|
+
displayName: 'OpenAI',
|
|
31
|
+
modelPrefix: 'openai',
|
|
32
|
+
defaultModels: ['openai/gpt-5.2'],
|
|
33
|
+
keywords: [],
|
|
34
|
+
envKey: 'OPENAI_API_KEY'
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
schemaQuery: {
|
|
40
|
+
data: {
|
|
41
|
+
uiHints: {}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
vi.mock('@/hooks/useConfig', () => ({
|
|
47
|
+
useConfig: () => mocks.configQuery,
|
|
48
|
+
useConfigMeta: () => mocks.metaQuery,
|
|
49
|
+
useConfigSchema: () => mocks.schemaQuery,
|
|
50
|
+
useUpdateModel: () => ({
|
|
51
|
+
mutate: mocks.mutate,
|
|
52
|
+
isPending: false
|
|
53
|
+
})
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
describe('ModelConfig', () => {
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
mocks.mutate.mockReset();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('submits the workspace together with the selected model', async () => {
|
|
62
|
+
const user = userEvent.setup();
|
|
63
|
+
|
|
64
|
+
render(<ModelConfig />);
|
|
65
|
+
|
|
66
|
+
const workspaceInput = await screen.findByLabelText('Default Path');
|
|
67
|
+
await user.clear(workspaceInput);
|
|
68
|
+
await user.type(workspaceInput, '~/new-workspace');
|
|
69
|
+
await user.click(screen.getByRole('button', { name: /save/i }));
|
|
70
|
+
|
|
71
|
+
await waitFor(() => {
|
|
72
|
+
expect(mocks.mutate).toHaveBeenCalledWith({
|
|
73
|
+
model: 'openai/gpt-5.2',
|
|
74
|
+
workspace: '~/new-workspace'
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -95,7 +95,10 @@ export function ModelConfig() {
|
|
|
95
95
|
|
|
96
96
|
const handleSubmit = (e: React.FormEvent) => {
|
|
97
97
|
e.preventDefault();
|
|
98
|
-
updateModel.mutate({
|
|
98
|
+
updateModel.mutate({
|
|
99
|
+
model: composedModel,
|
|
100
|
+
workspace
|
|
101
|
+
});
|
|
99
102
|
};
|
|
100
103
|
|
|
101
104
|
if (isLoading) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t } from '@/lib/i18n';
|
|
2
|
+
import type { getLanguage } from '@/lib/i18n';
|
|
2
3
|
import type { ProviderConfigUpdate, ProviderConfigView, ThinkingLevel } from '@/api/types';
|
|
3
4
|
|
|
4
5
|
type WireApiType = 'auto' | 'chat' | 'responses';
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { appClient } from '@/transport';
|
|
3
|
+
import { useUiStore } from '@/stores/ui.store';
|
|
4
|
+
import type { QueryClient } from '@tanstack/react-query';
|
|
5
|
+
|
|
6
|
+
function shouldInvalidateConfigQuery(configPath: string) {
|
|
7
|
+
const normalized = configPath.trim().toLowerCase();
|
|
8
|
+
if (!normalized) {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
if (normalized.startsWith('plugins') || normalized.startsWith('skills')) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function invalidateMarketplaceQueries(queryClient: QueryClient | undefined, configPath: string): void {
|
|
18
|
+
if (configPath.startsWith('plugins')) {
|
|
19
|
+
queryClient?.invalidateQueries({ queryKey: ['ncp-session-types'] });
|
|
20
|
+
queryClient?.invalidateQueries({ queryKey: ['marketplace-installed', 'plugin'] });
|
|
21
|
+
queryClient?.invalidateQueries({ queryKey: ['marketplace-items'] });
|
|
22
|
+
}
|
|
23
|
+
if (configPath.startsWith('mcp')) {
|
|
24
|
+
queryClient?.invalidateQueries({ queryKey: ['marketplace-mcp-installed'] });
|
|
25
|
+
queryClient?.invalidateQueries({ queryKey: ['marketplace-mcp-items'] });
|
|
26
|
+
queryClient?.invalidateQueries({ queryKey: ['marketplace-mcp-doctor'] });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function useRealtimeQueryBridge(queryClient?: QueryClient) {
|
|
31
|
+
const { setConnectionStatus } = useUiStore();
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const invalidateSessionQueries = (sessionKey?: string) => {
|
|
35
|
+
if (!queryClient) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
queryClient.invalidateQueries({ queryKey: ['sessions'] });
|
|
39
|
+
queryClient.invalidateQueries({ queryKey: ['ncp-sessions'] });
|
|
40
|
+
if (sessionKey && sessionKey.trim().length > 0) {
|
|
41
|
+
queryClient.invalidateQueries({ queryKey: ['session-history', sessionKey.trim()] });
|
|
42
|
+
queryClient.invalidateQueries({ queryKey: ['ncp-session-messages', sessionKey.trim()] });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
queryClient.invalidateQueries({ queryKey: ['session-history'] });
|
|
46
|
+
queryClient.invalidateQueries({ queryKey: ['ncp-session-messages'] });
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
setConnectionStatus('connecting');
|
|
50
|
+
|
|
51
|
+
return appClient.subscribe((event) => {
|
|
52
|
+
if (event.type === 'connection.open') {
|
|
53
|
+
setConnectionStatus('connected');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (event.type === 'connection.close') {
|
|
58
|
+
setConnectionStatus('disconnected');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (event.type === 'connection.error') {
|
|
63
|
+
setConnectionStatus('disconnected');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (event.type === 'config.updated') {
|
|
68
|
+
const configPath = typeof event.payload?.path === 'string' ? event.payload.path : '';
|
|
69
|
+
if (queryClient && shouldInvalidateConfigQuery(configPath)) {
|
|
70
|
+
queryClient.invalidateQueries({ queryKey: ['config'] });
|
|
71
|
+
}
|
|
72
|
+
if (configPath.startsWith('session')) {
|
|
73
|
+
invalidateSessionQueries();
|
|
74
|
+
}
|
|
75
|
+
invalidateMarketplaceQueries(queryClient, configPath);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (event.type === 'run.updated') {
|
|
80
|
+
if (!queryClient) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const sessionKey = event.payload.run.sessionKey;
|
|
84
|
+
const runId = event.payload.run.runId;
|
|
85
|
+
queryClient.invalidateQueries({ queryKey: ['chat-runs'] });
|
|
86
|
+
if (sessionKey) {
|
|
87
|
+
queryClient.invalidateQueries({ queryKey: ['sessions'] });
|
|
88
|
+
queryClient.invalidateQueries({ queryKey: ['session-history', sessionKey] });
|
|
89
|
+
} else {
|
|
90
|
+
queryClient.invalidateQueries({ queryKey: ['session-history'] });
|
|
91
|
+
}
|
|
92
|
+
if (runId) {
|
|
93
|
+
queryClient.invalidateQueries({ queryKey: ['chat-run', runId] });
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (event.type === 'session.updated') {
|
|
99
|
+
invalidateSessionQueries(event.payload.sessionKey);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (event.type === 'error') {
|
|
104
|
+
console.error('Realtime transport error:', event.payload.message);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}, [queryClient, setConnectionStatus]);
|
|
108
|
+
}
|
package/src/lib/i18n.remote.ts
CHANGED
|
@@ -189,10 +189,10 @@ export const REMOTE_LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
189
189
|
remoteEmail: { zh: '邮箱', en: 'Email' },
|
|
190
190
|
remotePassword: { zh: '密码', en: 'Password' },
|
|
191
191
|
remotePasswordPlaceholder: { zh: '请输入你的平台密码', en: 'Enter your platform password' },
|
|
192
|
-
remoteRegisterIfNeeded: { zh: '
|
|
192
|
+
remoteRegisterIfNeeded: { zh: '首次验证自动创建账号', en: 'Auto-create on First Verification' },
|
|
193
193
|
remoteRegisterIfNeededHelp: {
|
|
194
|
-
zh: '
|
|
195
|
-
en: 'When enabled, the UI will
|
|
194
|
+
zh: '如果邮箱还没有对应账号,平台会在验证码验证成功后自动创建账号并保存登录态。',
|
|
195
|
+
en: 'When enabled, the UI will sign in on the platform and then save the resulting login token.'
|
|
196
196
|
},
|
|
197
197
|
remoteLogin: { zh: '登录平台', en: 'Login to Platform' },
|
|
198
198
|
remoteCreateAccount: { zh: '注册并登录', en: 'Create Account & Login' },
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { API_BASE } from '@/api/client';
|
|
2
|
+
import { LocalAppTransport } from './local.transport';
|
|
3
|
+
import { RemoteSessionMultiplexTransport } from './remote.transport';
|
|
4
|
+
import type { AppTransport, RemoteRuntimeInfo, RequestInput, StreamInput, StreamSession } from './transport.types';
|
|
5
|
+
|
|
6
|
+
const REMOTE_RUNTIME_PATH = '/_remote/runtime';
|
|
7
|
+
|
|
8
|
+
async function resolveRuntime(apiBase: string): Promise<AppTransport> {
|
|
9
|
+
const runtimeUrl = `${apiBase.replace(/\/$/, '')}${REMOTE_RUNTIME_PATH}`;
|
|
10
|
+
let response: Response;
|
|
11
|
+
try {
|
|
12
|
+
response = await fetch(runtimeUrl, {
|
|
13
|
+
method: 'GET',
|
|
14
|
+
credentials: 'include',
|
|
15
|
+
cache: 'no-store'
|
|
16
|
+
});
|
|
17
|
+
} catch {
|
|
18
|
+
return new LocalAppTransport({ apiBase });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (response.status === 404) {
|
|
22
|
+
return new LocalAppTransport({ apiBase });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const payload = await response.json() as { ok?: boolean; data?: RemoteRuntimeInfo };
|
|
26
|
+
if (response.ok && payload.ok && payload.data?.mode === 'remote') {
|
|
27
|
+
return new RemoteSessionMultiplexTransport(payload.data, apiBase);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (response.status >= 400) {
|
|
31
|
+
return new RemoteSessionMultiplexTransport({
|
|
32
|
+
mode: 'remote',
|
|
33
|
+
protocolVersion: 1,
|
|
34
|
+
wsPath: '/_remote/ws'
|
|
35
|
+
}, apiBase);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return new LocalAppTransport({ apiBase });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class AppClient {
|
|
42
|
+
private transportPromise: Promise<AppTransport> | null = null;
|
|
43
|
+
|
|
44
|
+
constructor(private readonly apiBase: string = API_BASE) {}
|
|
45
|
+
|
|
46
|
+
private async getTransport(): Promise<AppTransport> {
|
|
47
|
+
if (!this.transportPromise) {
|
|
48
|
+
this.transportPromise = resolveRuntime(this.apiBase);
|
|
49
|
+
}
|
|
50
|
+
return await this.transportPromise;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async request<T>(input: RequestInput): Promise<T> {
|
|
54
|
+
return await (await this.getTransport()).request<T>(input);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
openStream<TFinal = unknown>(input: StreamInput): StreamSession<TFinal> {
|
|
58
|
+
let currentSession: StreamSession<TFinal> | null = null;
|
|
59
|
+
let resolveFinished!: (value: TFinal) => void;
|
|
60
|
+
let rejectFinished!: (error: Error) => void;
|
|
61
|
+
const finished = new Promise<TFinal>((resolve, reject) => {
|
|
62
|
+
resolveFinished = resolve;
|
|
63
|
+
rejectFinished = reject;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
void this.getTransport()
|
|
67
|
+
.then((transport) => {
|
|
68
|
+
currentSession = transport.openStream<TFinal>(input);
|
|
69
|
+
void currentSession.finished.then(resolveFinished).catch((error) => {
|
|
70
|
+
rejectFinished(error instanceof Error ? error : new Error(String(error)));
|
|
71
|
+
});
|
|
72
|
+
})
|
|
73
|
+
.catch((error) => {
|
|
74
|
+
rejectFinished(error instanceof Error ? error : new Error(String(error)));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
finished,
|
|
79
|
+
cancel: () => currentSession?.cancel()
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
subscribe(handler: (event: Parameters<Parameters<AppTransport['subscribe']>[0]>[0]) => void): () => void {
|
|
84
|
+
let unsubscribe = () => {};
|
|
85
|
+
let active = true;
|
|
86
|
+
void this.getTransport().then((transport) => {
|
|
87
|
+
if (!active) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
unsubscribe = transport.subscribe(handler);
|
|
91
|
+
}).catch((error) => {
|
|
92
|
+
handler({
|
|
93
|
+
type: 'connection.error',
|
|
94
|
+
payload: {
|
|
95
|
+
message: error instanceof Error ? error.message : String(error)
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return () => {
|
|
101
|
+
active = false;
|
|
102
|
+
unsubscribe();
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const appClient = new AppClient();
|