@nextclaw/ui 0.12.20-beta.0 → 0.12.20-beta.2
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 +19 -0
- package/dist/assets/api-BcqDx0tm.js +15 -0
- package/dist/assets/app-manager-provider-DVYBjif-.js +1 -0
- package/dist/assets/{app-navigation.config-BORqHkbN.js → app-navigation.config-BOVDFMnp.js} +1 -1
- package/dist/assets/{channels-list-page-sISO_4Yj.js → channels-list-page-CsoI4OJm.js} +2 -2
- package/dist/assets/{chat-ChCu7LQD.js → chat-ahMH_i_K.js} +6 -6
- package/dist/assets/chat-page-BxlXY-MB.js +1 -0
- package/dist/assets/chunk-JZWAC4HX-u4uYphxM.js +3 -0
- package/dist/assets/{desktop-update-config-BfJ5iSeY.js → desktop-update-config-CD6-2PfI.js} +1 -1
- package/dist/assets/{dialog-B-CXiFPZ.js → dialog-csshWetU.js} +1 -1
- package/dist/assets/{dist-DYVfg3q5.js → dist-Bl94Ahwx.js} +1 -1
- package/dist/assets/{es2015-BXroVnPi.js → es2015-JCM5-KtW.js} +1 -1
- package/dist/assets/index-CM-57d8J.js +2 -0
- package/dist/assets/index-D8MKmXtO.css +1 -0
- package/dist/assets/marketplace-page-DJGDpTAo.js +1 -0
- package/dist/assets/{marketplace-page-C9oZ01rM.js → marketplace-page-DxlxHCFm.js} +2 -2
- package/dist/assets/{mcp-marketplace-page-DuEixgSs.js → mcp-marketplace-page-5UjYRWOR.js} +2 -2
- package/dist/assets/mcp-marketplace-page-C1XaHZZO.js +1 -0
- package/dist/assets/{model-config-mfhqEZBG.js → model-config-PccJ9XyH.js} +1 -1
- package/dist/assets/{notice-card-CozHB03G.js → notice-card-CCgk6FvF.js} +1 -1
- package/dist/assets/{popover-CPUPma-w.js → popover-YAsxDBhY.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-CL9sti2I.js → provider-scoped-model-input-CzpF7cug.js} +1 -1
- package/dist/assets/{providers-list-HPmL2akJ.js → providers-list-8qDMER8o.js} +1 -1
- package/dist/assets/remote-D4TtLPAp.js +1 -0
- package/dist/assets/runtime-config-page-DWJHrV7H.js +1 -0
- package/dist/assets/{search-config-Bcnk9VlL.js → search-config-D3a65l3r.js} +1 -1
- package/dist/assets/{secrets-config-Dde-5Y1w.js → secrets-config-CoMlR_7i.js} +2 -2
- package/dist/assets/{select-BELPuXLW.js → select-DIZrwsKU.js} +1 -1
- package/dist/assets/{sessions-config-page-CG49_0Z6.js → sessions-config-page-QjH5tgjr.js} +2 -2
- package/dist/assets/{setting-row-D5DtT6Ny.js → setting-row-DiQyrE81.js} +1 -1
- package/dist/assets/{tag-chip-D9BWWgYg.js → tag-chip-C3wDBe_-.js} +1 -1
- package/dist/assets/theme-provider-W704JWF8.js +1 -0
- package/dist/assets/{tooltip-CI0rpNee.js → tooltip-Dq5Xehpk.js} +1 -1
- package/dist/assets/use-config-BQJjq1mP.js +1 -0
- package/dist/assets/{use-confirm-dialog-hbynwWf2.js → use-confirm-dialog-DBoV5n5P.js} +1 -1
- package/dist/assets/{use-infinite-scroll-loader-Cw5qQr3-.js → use-infinite-scroll-loader-JAicqVC5.js} +1 -1
- package/dist/assets/{use-viewport-layout-CWHVDC6z.js → use-viewport-layout-BX3XqzJ4.js} +1 -1
- package/dist/index.html +17 -17
- package/package.json +8 -6
- package/src/app/hooks/use-realtime-query-bridge.ts +5 -5
- package/src/features/channels/components/config/channel-form.tsx +3 -3
- package/src/features/chat/hooks/use-ncp-chat-page-data.ts +7 -6
- package/src/features/chat/pages/ncp-chat-page.test.ts +22 -8
- package/src/features/chat/utils/chat-session-preference-governance.utils.test.tsx +114 -0
- package/src/features/chat/utils/chat-session-preference-governance.utils.ts +30 -36
- package/src/shared/components/common/brand-header.test.tsx +7 -2
- package/src/shared/components/common/brand-header.tsx +34 -4
- package/src/shared/lib/api/index.ts +12 -12
- package/src/shared/lib/api/ncp-session.test.ts +17 -18
- package/src/shared/lib/api/raw-client.utils.ts +3 -126
- package/src/shared/lib/api/services/agents.service.ts +18 -0
- package/src/shared/lib/api/services/channel-auth.service.ts +21 -0
- package/src/shared/lib/api/{client.ts → services/client.service.ts} +45 -1
- package/src/shared/lib/api/services/config.service.ts +171 -0
- package/src/shared/lib/api/services/marketplace.service.ts +66 -0
- package/src/shared/lib/api/services/mcp-marketplace.service.ts +70 -0
- package/src/shared/lib/api/services/ncp-attachments.service.ts +14 -0
- package/src/shared/lib/api/services/ncp-session.service.ts +39 -0
- package/src/shared/lib/api/services/remote.service.ts +50 -0
- package/src/shared/lib/api/services/runtime-control.service.ts +18 -0
- package/src/shared/lib/api/services/runtime-update.service.ts +26 -0
- package/src/shared/lib/api/services/server-path.service.ts +16 -0
- package/src/shared/lib/transport/index.ts +1 -0
- package/src/shared/lib/transport/local-transport.service.ts +24 -4
- package/src/shared/lib/transport/remote-transport.service.ts +1 -1
- package/src/shared/lib/transport/request-raw-api-response.utils.ts +133 -0
- package/src/shared/lib/transport/transport.types.ts +8 -2
- package/dist/assets/api-C412zuay.js +0 -15
- package/dist/assets/app-manager-provider-Cm-KiZZG.js +0 -1
- package/dist/assets/chat-page-BCaNZJGT.js +0 -1
- package/dist/assets/chunk-JZWAC4HX-DvbcIVPf.js +0 -3
- package/dist/assets/index-CUmk8xFK.css +0 -1
- package/dist/assets/index-CqPDhosM.js +0 -2
- package/dist/assets/marketplace-page-C8uaWkfd.js +0 -1
- package/dist/assets/mcp-marketplace-page-rNqr6ZpD.js +0 -1
- package/dist/assets/remote-oDlAdgVA.js +0 -1
- package/dist/assets/runtime-config-page-BCshTAAE.js +0 -1
- package/dist/assets/theme-provider-DeBrTglS.js +0 -1
- package/dist/assets/use-config-CrWZ_TSF.js +0 -1
- package/src/shared/lib/api/agents.ts +0 -34
- package/src/shared/lib/api/channel-auth.ts +0 -35
- package/src/shared/lib/api/config.ts +0 -362
- package/src/shared/lib/api/marketplace.ts +0 -156
- package/src/shared/lib/api/mcp-marketplace.ts +0 -138
- package/src/shared/lib/api/ncp-attachments.ts +0 -41
- package/src/shared/lib/api/ncp-session.ts +0 -78
- package/src/shared/lib/api/remote.ts +0 -86
- package/src/shared/lib/api/runtime-control.ts +0 -34
- package/src/shared/lib/api/runtime-update.service.ts +0 -50
- package/src/shared/lib/api/server-path.ts +0 -46
|
@@ -160,37 +160,14 @@ export function resolveRecentSessionPreferredValue<T>(params: {
|
|
|
160
160
|
return bestValue;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
selectedSessionKey?: string | null;
|
|
166
|
-
sessionType?: string | null;
|
|
167
|
-
}): string | undefined {
|
|
168
|
-
const { sessions, selectedSessionKey, sessionType } = params;
|
|
169
|
-
return resolveRecentSessionPreferredValue<string>({
|
|
170
|
-
sessions,
|
|
171
|
-
selectedSessionKey,
|
|
172
|
-
sessionType,
|
|
173
|
-
readPreference: (session) => session.preferredModel?.trim() || undefined
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export function resolveRecentSessionPreferredThinking(params: {
|
|
178
|
-
sessions: readonly SessionEntryView[];
|
|
179
|
-
selectedSessionKey?: string | null;
|
|
180
|
-
sessionType?: string | null;
|
|
181
|
-
}): ThinkingLevel | undefined {
|
|
182
|
-
const { sessions, selectedSessionKey, sessionType } = params;
|
|
183
|
-
return resolveRecentSessionPreferredValue<ThinkingLevel>({
|
|
184
|
-
sessions,
|
|
185
|
-
selectedSessionKey,
|
|
186
|
-
sessionType,
|
|
187
|
-
readPreference: (session) => session.preferredThinking ?? undefined
|
|
188
|
-
});
|
|
163
|
+
function buildSyncKey(parts: unknown[]): string {
|
|
164
|
+
return parts.map((part) => (part == null ? '' : String(part))).join('\u0002');
|
|
189
165
|
}
|
|
190
166
|
|
|
191
167
|
type UseSyncSessionPreferenceParams<T> = {
|
|
192
168
|
isPreferenceAvailable: boolean;
|
|
193
169
|
emptyValue: T;
|
|
170
|
+
syncKey: string;
|
|
194
171
|
selectedSessionKey?: string | null;
|
|
195
172
|
selectedSessionExists?: boolean;
|
|
196
173
|
setValue: Dispatch<SetStateAction<T>>;
|
|
@@ -201,6 +178,7 @@ function useSyncSessionPreference<T>(params: UseSyncSessionPreferenceParams<T>)
|
|
|
201
178
|
const {
|
|
202
179
|
isPreferenceAvailable,
|
|
203
180
|
emptyValue,
|
|
181
|
+
syncKey,
|
|
204
182
|
selectedSessionKey,
|
|
205
183
|
selectedSessionExists = false,
|
|
206
184
|
setValue,
|
|
@@ -208,27 +186,31 @@ function useSyncSessionPreference<T>(params: UseSyncSessionPreferenceParams<T>)
|
|
|
208
186
|
} = params;
|
|
209
187
|
const previousSessionKeyRef = useRef<string | null | undefined>(undefined);
|
|
210
188
|
const resolveValueRef = useRef(resolveValue);
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
resolveValueRef.current = resolveValue;
|
|
214
|
-
}, [resolveValue]);
|
|
189
|
+
const lastSyncedValueRef = useRef<T>(emptyValue);
|
|
190
|
+
resolveValueRef.current = resolveValue;
|
|
215
191
|
|
|
216
192
|
useEffect(() => {
|
|
217
193
|
const sessionChanged = previousSessionKeyRef.current !== selectedSessionKey;
|
|
218
194
|
if (!isPreferenceAvailable) {
|
|
219
195
|
setValue(emptyValue);
|
|
196
|
+
lastSyncedValueRef.current = emptyValue;
|
|
220
197
|
previousSessionKeyRef.current = selectedSessionKey;
|
|
221
198
|
return;
|
|
222
199
|
}
|
|
223
|
-
setValue((prev) =>
|
|
224
|
-
resolveValueRef.current({
|
|
225
|
-
currentValue:
|
|
200
|
+
setValue((prev) => {
|
|
201
|
+
const next = resolveValueRef.current({
|
|
202
|
+
currentValue:
|
|
203
|
+
!sessionChanged && Object.is(prev, lastSyncedValueRef.current)
|
|
204
|
+
? emptyValue
|
|
205
|
+
: prev,
|
|
226
206
|
sessionChanged,
|
|
227
207
|
preserveCurrentValueOnSessionChange: sessionChanged && Boolean(selectedSessionKey) && !selectedSessionExists
|
|
228
|
-
})
|
|
229
|
-
|
|
208
|
+
});
|
|
209
|
+
lastSyncedValueRef.current = next;
|
|
210
|
+
return next;
|
|
211
|
+
});
|
|
230
212
|
previousSessionKeyRef.current = selectedSessionKey;
|
|
231
|
-
}, [emptyValue, isPreferenceAvailable, selectedSessionExists, selectedSessionKey, setValue]);
|
|
213
|
+
}, [emptyValue, isPreferenceAvailable, selectedSessionExists, selectedSessionKey, setValue, syncKey]);
|
|
232
214
|
}
|
|
233
215
|
|
|
234
216
|
export function useSyncSelectedModel(params: {
|
|
@@ -244,6 +226,12 @@ export function useSyncSelectedModel(params: {
|
|
|
244
226
|
useSyncSessionPreference<string>({
|
|
245
227
|
isPreferenceAvailable: modelOptions.length > 0,
|
|
246
228
|
emptyValue: '',
|
|
229
|
+
syncKey: buildSyncKey([
|
|
230
|
+
modelOptions.map((option) => option.value).join('\u0001'),
|
|
231
|
+
selectedSessionPreferredModel,
|
|
232
|
+
fallbackPreferredModel,
|
|
233
|
+
defaultModel
|
|
234
|
+
]),
|
|
247
235
|
selectedSessionKey,
|
|
248
236
|
selectedSessionExists,
|
|
249
237
|
setValue: setSelectedModel,
|
|
@@ -273,6 +261,12 @@ export function useSyncSelectedThinking(params: {
|
|
|
273
261
|
useSyncSessionPreference<ThinkingLevel | null>({
|
|
274
262
|
isPreferenceAvailable: supportedThinkingLevels.length > 0,
|
|
275
263
|
emptyValue: null,
|
|
264
|
+
syncKey: buildSyncKey([
|
|
265
|
+
supportedThinkingLevels.join('\u0001'),
|
|
266
|
+
selectedSessionPreferredThinking,
|
|
267
|
+
fallbackPreferredThinking,
|
|
268
|
+
defaultThinkingLevel
|
|
269
|
+
]),
|
|
276
270
|
selectedSessionKey,
|
|
277
271
|
selectedSessionExists,
|
|
278
272
|
setValue: setSelectedThinkingLevel,
|
|
@@ -60,7 +60,8 @@ describe('BrandHeader', () => {
|
|
|
60
60
|
mocks.downloadUpdate.mockReset();
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
-
it('shows update progress next to the product version', () => {
|
|
63
|
+
it('shows update progress next to the product version', async () => {
|
|
64
|
+
const user = userEvent.setup();
|
|
64
65
|
useRuntimeUpdateStore.setState({
|
|
65
66
|
supported: true,
|
|
66
67
|
initialized: true,
|
|
@@ -96,7 +97,11 @@ describe('BrandHeader', () => {
|
|
|
96
97
|
|
|
97
98
|
renderBrandHeader();
|
|
98
99
|
|
|
99
|
-
|
|
100
|
+
const version = screen.getByText('v0.18.11');
|
|
101
|
+
expect(version).toBeTruthy();
|
|
102
|
+
expect(screen.getAllByText('v0.18.11')).toHaveLength(1);
|
|
103
|
+
await user.hover(version);
|
|
104
|
+
expect(await screen.findAllByText('v0.18.11')).toHaveLength(2);
|
|
100
105
|
expect(screen.getByText('下载 50%')).toBeTruthy();
|
|
101
106
|
expect(screen.queryByRole('button', { name: '更新' })).toBeNull();
|
|
102
107
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { UpdateSnapshot } from '@nextclaw/kernel';
|
|
2
2
|
import { runtimeUpdateManager, useRuntimeUpdateStore } from '@/features/system-status';
|
|
3
3
|
import { useAppMeta } from '@/shared/hooks/use-config';
|
|
4
|
-
import type
|
|
4
|
+
import { type ReactNode, useState } from 'react';
|
|
5
5
|
import { RuntimeStatusEntry } from '@/app/components/layout/runtime-status-entry';
|
|
6
6
|
import { cn } from '@/shared/lib/utils';
|
|
7
7
|
import { t } from '@/shared/lib/i18n';
|
|
@@ -15,6 +15,7 @@ export function BrandHeader({ className, suffix }: BrandHeaderProps) {
|
|
|
15
15
|
const { data } = useAppMeta();
|
|
16
16
|
const productName = data?.name ?? 'NextClaw';
|
|
17
17
|
const productVersion = data?.productVersion?.trim();
|
|
18
|
+
const versionLabel = productVersion ? `v${productVersion}` : null;
|
|
18
19
|
const resolvedSuffix = suffix ?? <RuntimeStatusEntry />;
|
|
19
20
|
|
|
20
21
|
return (
|
|
@@ -22,9 +23,11 @@ export function BrandHeader({ className, suffix }: BrandHeaderProps) {
|
|
|
22
23
|
<div className="h-7 w-7 rounded-lg overflow-hidden flex items-center justify-center">
|
|
23
24
|
<img src="/logo.svg" alt={productName} className="h-full w-full object-contain" />
|
|
24
25
|
</div>
|
|
25
|
-
<div className="flex items-baseline gap-2
|
|
26
|
-
<
|
|
27
|
-
|
|
26
|
+
<div className="flex min-w-0 items-baseline gap-2">
|
|
27
|
+
<div className="flex min-w-0 flex-1 items-baseline gap-2">
|
|
28
|
+
<span className="shrink-0 text-[15px] font-semibold tracking-[-0.01em] text-gray-800">{productName}</span>
|
|
29
|
+
{versionLabel ? <BrandVersionLabel versionLabel={versionLabel} /> : null}
|
|
30
|
+
</div>
|
|
28
31
|
<RuntimeUpdateInlineStatus />
|
|
29
32
|
{resolvedSuffix ? <span className="inline-flex items-center shrink-0">{resolvedSuffix}</span> : null}
|
|
30
33
|
</div>
|
|
@@ -32,6 +35,33 @@ export function BrandHeader({ className, suffix }: BrandHeaderProps) {
|
|
|
32
35
|
);
|
|
33
36
|
}
|
|
34
37
|
|
|
38
|
+
function BrandVersionLabel({ versionLabel }: { versionLabel: string }) {
|
|
39
|
+
const [isTooltipOpen, setIsTooltipOpen] = useState(false);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<span
|
|
43
|
+
className="relative min-w-0 flex-1"
|
|
44
|
+
onMouseEnter={() => setIsTooltipOpen(true)}
|
|
45
|
+
onMouseLeave={() => setIsTooltipOpen(false)}
|
|
46
|
+
onFocus={() => setIsTooltipOpen(true)}
|
|
47
|
+
onBlur={() => setIsTooltipOpen(false)}
|
|
48
|
+
>
|
|
49
|
+
<span
|
|
50
|
+
tabIndex={0}
|
|
51
|
+
aria-label={versionLabel}
|
|
52
|
+
className="block min-w-0 truncate text-[13px] font-medium text-gray-500 outline-none"
|
|
53
|
+
>
|
|
54
|
+
{versionLabel}
|
|
55
|
+
</span>
|
|
56
|
+
{isTooltipOpen ? (
|
|
57
|
+
<span className="pointer-events-none absolute left-0 top-full z-[var(--z-tooltip)] mt-1 w-max max-w-none whitespace-nowrap rounded-md border bg-popover px-3 py-1.5 text-xs font-medium text-popover-foreground shadow-md">
|
|
58
|
+
{versionLabel}
|
|
59
|
+
</span>
|
|
60
|
+
) : null}
|
|
61
|
+
</span>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
35
65
|
function RuntimeUpdateInlineStatus() {
|
|
36
66
|
const { supported, busyAction, snapshot } = useRuntimeUpdateStore();
|
|
37
67
|
if (!supported || !snapshot) {
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
export * from './agents';
|
|
1
|
+
export * from './services/agents.service';
|
|
2
2
|
export * from './api-base';
|
|
3
3
|
export * from './auth.types';
|
|
4
|
-
export * from './channel-auth';
|
|
4
|
+
export * from './services/channel-auth.service';
|
|
5
5
|
export * from './channel-auth.types';
|
|
6
6
|
export * from './chat-session-type.types';
|
|
7
|
-
export * from './client';
|
|
8
|
-
export * from './config';
|
|
9
|
-
export * from './marketplace';
|
|
10
|
-
export * from './mcp-marketplace';
|
|
11
|
-
export * from './ncp-attachments';
|
|
12
|
-
export * from './ncp-session';
|
|
7
|
+
export * from './services/client.service';
|
|
8
|
+
export * from './services/config.service';
|
|
9
|
+
export * from './services/marketplace.service';
|
|
10
|
+
export * from './services/mcp-marketplace.service';
|
|
11
|
+
export * from './services/ncp-attachments.service';
|
|
12
|
+
export * from './services/ncp-session.service';
|
|
13
13
|
export * from './ncp-session.types';
|
|
14
14
|
export * from './ncp-session-query-cache';
|
|
15
15
|
export * from './raw-client.utils';
|
|
16
|
-
export * from './remote';
|
|
16
|
+
export * from './services/remote.service';
|
|
17
17
|
export * from './remote.types';
|
|
18
|
-
export * from './runtime-control';
|
|
18
|
+
export * from './services/runtime-control.service';
|
|
19
19
|
export * from './runtime-control.types';
|
|
20
|
-
export * from './runtime-update.service';
|
|
21
|
-
export * from './server-path';
|
|
20
|
+
export * from './services/runtime-update.service';
|
|
21
|
+
export * from './services/server-path.service';
|
|
22
22
|
export * from './types';
|
|
@@ -1,37 +1,36 @@
|
|
|
1
|
-
import { fetchNcpSessionSkills } from './ncp-session';
|
|
2
|
-
import {
|
|
1
|
+
import { fetchNcpSessionSkills } from './services/ncp-session.service';
|
|
2
|
+
import { nextclawClient } from './services/client.service';
|
|
3
3
|
|
|
4
|
-
vi.mock('./client', () => ({
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
vi.mock('./services/client.service', () => ({
|
|
5
|
+
nextclawClient: {
|
|
6
|
+
sessions: {
|
|
7
|
+
listSkills: vi.fn()
|
|
8
|
+
}
|
|
7
9
|
}
|
|
8
10
|
}));
|
|
9
11
|
|
|
10
12
|
describe('api/ncp-session', () => {
|
|
11
13
|
beforeEach(() => {
|
|
12
|
-
vi.mocked(
|
|
13
|
-
vi.mocked(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
refs: [],
|
|
19
|
-
records: []
|
|
20
|
-
}
|
|
14
|
+
vi.mocked(nextclawClient.sessions.listSkills).mockReset();
|
|
15
|
+
vi.mocked(nextclawClient.sessions.listSkills).mockResolvedValue({
|
|
16
|
+
sessionId: 'session-1',
|
|
17
|
+
total: 0,
|
|
18
|
+
refs: [],
|
|
19
|
+
records: []
|
|
21
20
|
});
|
|
22
21
|
});
|
|
23
22
|
|
|
24
23
|
it('does not send an empty projectRoot query when no override is provided', async () => {
|
|
25
24
|
await fetchNcpSessionSkills('session-1', { projectRoot: null });
|
|
26
25
|
|
|
27
|
-
expect(
|
|
26
|
+
expect(nextclawClient.sessions.listSkills).toHaveBeenCalledWith('session-1', { projectRoot: null });
|
|
28
27
|
});
|
|
29
28
|
|
|
30
29
|
it('sends projectRoot only when the override is non-empty', async () => {
|
|
31
30
|
await fetchNcpSessionSkills('session-1', { projectRoot: ' /tmp/project-alpha ' });
|
|
32
31
|
|
|
33
|
-
expect(
|
|
34
|
-
'/
|
|
35
|
-
);
|
|
32
|
+
expect(nextclawClient.sessions.listSkills).toHaveBeenCalledWith('session-1', {
|
|
33
|
+
projectRoot: ' /tmp/project-alpha '
|
|
34
|
+
});
|
|
36
35
|
});
|
|
37
36
|
});
|
|
@@ -1,132 +1,9 @@
|
|
|
1
1
|
import { API_BASE } from './api-base';
|
|
2
|
-
import
|
|
3
|
-
import { systemStatusManager } from '@/features/system-status';
|
|
4
|
-
|
|
5
|
-
function compactSnippet(text: string) {
|
|
6
|
-
return text.replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function inferNonJsonHint(endpoint: string, status: number): string | undefined {
|
|
10
|
-
if (
|
|
11
|
-
status === 404 &&
|
|
12
|
-
endpoint.startsWith('/api/config/providers/') &&
|
|
13
|
-
endpoint.endsWith('/test')
|
|
14
|
-
) {
|
|
15
|
-
return 'Provider test endpoint is missing. This usually means nextclaw runtime version is outdated.';
|
|
16
|
-
}
|
|
17
|
-
if (status === 401 || status === 403) {
|
|
18
|
-
return 'Authentication failed. Check apiKey and custom headers.';
|
|
19
|
-
}
|
|
20
|
-
if (status === 429) {
|
|
21
|
-
return 'Rate limited by upstream provider. Retry later or switch model/provider.';
|
|
22
|
-
}
|
|
23
|
-
if (status >= 500) {
|
|
24
|
-
return 'Upstream service error. Retry later and inspect server logs if it persists.';
|
|
25
|
-
}
|
|
26
|
-
return undefined;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function formatUnknownFetchError(error: unknown): {
|
|
30
|
-
summary: string;
|
|
31
|
-
details: Record<string, unknown>;
|
|
32
|
-
} {
|
|
33
|
-
if (error instanceof Error) {
|
|
34
|
-
const name = error.name?.trim() || 'Error';
|
|
35
|
-
const message = error.message?.trim() || 'Unknown error';
|
|
36
|
-
return {
|
|
37
|
-
summary: `${name}: ${message}`,
|
|
38
|
-
details: {
|
|
39
|
-
errorName: name,
|
|
40
|
-
errorMessage: message,
|
|
41
|
-
...(error.stack?.trim() ? { errorStack: error.stack.trim() } : {})
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
return {
|
|
46
|
-
summary: String(error ?? 'Unknown error'),
|
|
47
|
-
details: {
|
|
48
|
-
errorName: 'NonError',
|
|
49
|
-
errorMessage: String(error ?? 'Unknown error')
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
}
|
|
2
|
+
import { requestRawApiResponse as requestRawTransportApiResponse } from '@/shared/lib/transport';
|
|
53
3
|
|
|
54
4
|
export async function requestRawApiResponse<T>(
|
|
55
5
|
endpoint: string,
|
|
56
6
|
options: RequestInit = {}
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
const method = (options.method || 'GET').toUpperCase();
|
|
60
|
-
|
|
61
|
-
let response: Response;
|
|
62
|
-
try {
|
|
63
|
-
response = await fetch(url, {
|
|
64
|
-
credentials: 'include',
|
|
65
|
-
headers: {
|
|
66
|
-
'Content-Type': 'application/json',
|
|
67
|
-
...options.headers
|
|
68
|
-
},
|
|
69
|
-
...options
|
|
70
|
-
});
|
|
71
|
-
} catch (error) {
|
|
72
|
-
const formatted = formatUnknownFetchError(error);
|
|
73
|
-
systemStatusManager.reportTransportFailure(formatted.summary);
|
|
74
|
-
return {
|
|
75
|
-
ok: false,
|
|
76
|
-
error: {
|
|
77
|
-
code: 'NETWORK_ERROR',
|
|
78
|
-
message: `Fetch failed on ${method} ${endpoint} | ${formatted.summary}`,
|
|
79
|
-
details: {
|
|
80
|
-
method,
|
|
81
|
-
endpoint,
|
|
82
|
-
url,
|
|
83
|
-
...formatted.details
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const text = await response.text();
|
|
90
|
-
let data: ApiResponse<T> | null = null;
|
|
91
|
-
if (text) {
|
|
92
|
-
try {
|
|
93
|
-
data = JSON.parse(text) as ApiResponse<T>;
|
|
94
|
-
} catch {
|
|
95
|
-
// fall through to build a synthetic error response
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (!data) {
|
|
100
|
-
const snippet = text ? compactSnippet(text) : '';
|
|
101
|
-
const hint = inferNonJsonHint(endpoint, response.status);
|
|
102
|
-
const parts = [`Non-JSON response (${response.status} ${response.statusText}) on ${method} ${endpoint}`];
|
|
103
|
-
if (snippet) {
|
|
104
|
-
parts.push(`body=${snippet}`);
|
|
105
|
-
}
|
|
106
|
-
if (hint) {
|
|
107
|
-
parts.push(`hint=${hint}`);
|
|
108
|
-
}
|
|
109
|
-
return {
|
|
110
|
-
ok: false,
|
|
111
|
-
error: {
|
|
112
|
-
code: 'INVALID_RESPONSE',
|
|
113
|
-
message: parts.join(' | '),
|
|
114
|
-
details: {
|
|
115
|
-
status: response.status,
|
|
116
|
-
statusText: response.statusText,
|
|
117
|
-
method,
|
|
118
|
-
endpoint,
|
|
119
|
-
url,
|
|
120
|
-
bodySnippet: snippet || undefined,
|
|
121
|
-
hint
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (!response.ok) {
|
|
128
|
-
return data as ApiResponse<T>;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return data as ApiResponse<T>;
|
|
7
|
+
) {
|
|
8
|
+
return await requestRawTransportApiResponse<T>(API_BASE, endpoint, options);
|
|
132
9
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { nextclawClient } from "./client.service";
|
|
2
|
+
import type { AgentCreateRequest, AgentDeleteResult, AgentProfileView, AgentUpdateRequest } from "@/shared/lib/api/types";
|
|
3
|
+
|
|
4
|
+
export async function fetchAgents(): Promise<{ agents: AgentProfileView[] }> {
|
|
5
|
+
return { agents: await nextclawClient.agents.list() };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function createAgent(data: AgentCreateRequest): Promise<AgentProfileView> {
|
|
9
|
+
return await nextclawClient.agents.create(data);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function updateAgent(agentId: string, data: AgentUpdateRequest): Promise<AgentProfileView> {
|
|
13
|
+
return await nextclawClient.agents.update(agentId, data);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function deleteAgent(agentId: string): Promise<AgentDeleteResult> {
|
|
17
|
+
return await nextclawClient.agents.delete(agentId);
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { nextclawClient } from './client.service';
|
|
2
|
+
import type {
|
|
3
|
+
ChannelAuthPollRequest,
|
|
4
|
+
ChannelAuthPollResult,
|
|
5
|
+
ChannelAuthStartRequest,
|
|
6
|
+
ChannelAuthStartResult
|
|
7
|
+
} from '@/shared/lib/api/channel-auth.types';
|
|
8
|
+
|
|
9
|
+
export async function startChannelAuth(
|
|
10
|
+
channel: string,
|
|
11
|
+
data: ChannelAuthStartRequest = {}
|
|
12
|
+
): Promise<ChannelAuthStartResult> {
|
|
13
|
+
return await nextclawClient.channelAuth.start(channel, data);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function pollChannelAuth(
|
|
17
|
+
channel: string,
|
|
18
|
+
data: ChannelAuthPollRequest
|
|
19
|
+
): Promise<ChannelAuthPollResult> {
|
|
20
|
+
return await nextclawClient.channelAuth.poll(channel, data);
|
|
21
|
+
}
|
|
@@ -1,5 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createNextClawClient,
|
|
3
|
+
type NextClawRealtimeEvent,
|
|
4
|
+
type NextClawTransport,
|
|
5
|
+
type NextClawTransportRequestInput,
|
|
6
|
+
type NextClawTransportUploadInput
|
|
7
|
+
} from '@nextclaw/client-sdk';
|
|
8
|
+
import { API_BASE } from '@/shared/lib/api/api-base';
|
|
1
9
|
import { appClient } from '@/shared/lib/transport';
|
|
2
|
-
import type { ApiResponse } from '
|
|
10
|
+
import type { ApiResponse } from '@/shared/lib/api/types';
|
|
11
|
+
|
|
12
|
+
const nextclawUiTransport: NextClawTransport = {
|
|
13
|
+
request: async <T>({ body, method, path, signal, timeoutMs }: NextClawTransportRequestInput): Promise<T> => {
|
|
14
|
+
return await appClient.request({
|
|
15
|
+
method,
|
|
16
|
+
path,
|
|
17
|
+
...(body !== undefined ? { body } : {}),
|
|
18
|
+
...(signal ? { signal } : {}),
|
|
19
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {})
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
upload: async <T>({ formData, headers, path, signal }: NextClawTransportUploadInput): Promise<T> => {
|
|
23
|
+
const response = await fetch(`${API_BASE}${path}`, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
body: formData,
|
|
26
|
+
credentials: 'include',
|
|
27
|
+
...(headers ? { headers } : {}),
|
|
28
|
+
...(signal ? { signal } : {})
|
|
29
|
+
});
|
|
30
|
+
const payload = (await response.json()) as ApiResponse<unknown>;
|
|
31
|
+
if (!response.ok || !payload.ok) {
|
|
32
|
+
throw new Error(readApiErrorMessage(payload, `Upload failed for ${path}`));
|
|
33
|
+
}
|
|
34
|
+
return payload.data as T;
|
|
35
|
+
},
|
|
36
|
+
subscribe: (handler: (event: NextClawRealtimeEvent) => void) => appClient.subscribe(handler)
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const nextclawClient = createNextClawClient({
|
|
40
|
+
baseUrl: API_BASE,
|
|
41
|
+
transport: nextclawUiTransport
|
|
42
|
+
});
|
|
3
43
|
|
|
4
44
|
export async function requestApiResponse<T>(
|
|
5
45
|
endpoint: string,
|
|
@@ -64,3 +104,7 @@ function parseRequestBody(body: BodyInit | null | undefined): unknown {
|
|
|
64
104
|
}
|
|
65
105
|
return body;
|
|
66
106
|
}
|
|
107
|
+
|
|
108
|
+
function readApiErrorMessage(payload: ApiResponse<unknown>, fallback: string): string {
|
|
109
|
+
return !payload.ok ? payload.error.message : fallback;
|
|
110
|
+
}
|