@nextclaw/ui 0.3.8 → 0.3.9
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 +10 -0
- package/dist/assets/index-BN_YuYNC.js +230 -0
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/api/config.ts +11 -4
- package/src/api/types.ts +50 -4
- package/src/components/config/ChannelForm.tsx +94 -20
- package/src/hooks/useConfig.ts +12 -1
- package/dist/assets/index-DI6zQUcL.js +0 -230
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-BN_YuYNC.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="/assets/index-DahcMyga.css">
|
|
11
11
|
</head>
|
|
12
12
|
|
package/package.json
CHANGED
package/src/api/config.ts
CHANGED
|
@@ -6,7 +6,8 @@ import type {
|
|
|
6
6
|
ProviderConfigView,
|
|
7
7
|
ChannelConfigUpdate,
|
|
8
8
|
ProviderConfigUpdate,
|
|
9
|
-
|
|
9
|
+
ConfigActionExecuteRequest,
|
|
10
|
+
ConfigActionExecuteResult
|
|
10
11
|
} from './types';
|
|
11
12
|
|
|
12
13
|
// GET /api/config
|
|
@@ -77,9 +78,15 @@ export async function updateChannel(
|
|
|
77
78
|
return response.data;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
// POST /api/
|
|
81
|
-
export async function
|
|
82
|
-
|
|
81
|
+
// POST /api/config/actions/:id/execute
|
|
82
|
+
export async function executeConfigAction(
|
|
83
|
+
actionId: string,
|
|
84
|
+
data: ConfigActionExecuteRequest
|
|
85
|
+
): Promise<ConfigActionExecuteResult> {
|
|
86
|
+
const response = await api.post<ConfigActionExecuteResult>(
|
|
87
|
+
`/api/config/actions/${actionId}/execute`,
|
|
88
|
+
data
|
|
89
|
+
);
|
|
83
90
|
if (!response.ok) {
|
|
84
91
|
throw new Error(response.error.message);
|
|
85
92
|
}
|
package/src/api/types.ts
CHANGED
|
@@ -96,14 +96,60 @@ export type ConfigUiHints = Record<string, ConfigUiHint>;
|
|
|
96
96
|
export type ConfigSchemaResponse = {
|
|
97
97
|
schema: Record<string, unknown>;
|
|
98
98
|
uiHints: ConfigUiHints;
|
|
99
|
+
actions: ConfigActionManifest[];
|
|
99
100
|
version: string;
|
|
100
101
|
generatedAt: string;
|
|
101
102
|
};
|
|
102
103
|
|
|
103
|
-
export type
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
export type ConfigActionType = 'httpProbe' | 'oauthStart' | 'webhookVerify' | 'openUrl' | 'copyToken';
|
|
105
|
+
|
|
106
|
+
export type ConfigActionManifest = {
|
|
107
|
+
id: string;
|
|
108
|
+
version: string;
|
|
109
|
+
scope: string;
|
|
110
|
+
title: string;
|
|
111
|
+
description?: string;
|
|
112
|
+
type: ConfigActionType;
|
|
113
|
+
trigger: 'manual' | 'afterSave';
|
|
114
|
+
requires?: string[];
|
|
115
|
+
request: {
|
|
116
|
+
method: 'GET' | 'POST' | 'PUT';
|
|
117
|
+
path: string;
|
|
118
|
+
timeoutMs?: number;
|
|
119
|
+
};
|
|
120
|
+
success?: {
|
|
121
|
+
message?: string;
|
|
122
|
+
};
|
|
123
|
+
failure?: {
|
|
124
|
+
message?: string;
|
|
125
|
+
};
|
|
126
|
+
saveBeforeRun?: boolean;
|
|
127
|
+
savePatch?: Record<string, unknown>;
|
|
128
|
+
resultMap?: Record<string, string>;
|
|
129
|
+
policy?: {
|
|
130
|
+
roles?: string[];
|
|
131
|
+
rateLimitKey?: string;
|
|
132
|
+
cooldownMs?: number;
|
|
133
|
+
audit?: boolean;
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export type ConfigActionExecuteRequest = {
|
|
138
|
+
scope?: string;
|
|
139
|
+
draftConfig?: Record<string, unknown>;
|
|
140
|
+
context?: {
|
|
141
|
+
actor?: string;
|
|
142
|
+
traceId?: string;
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export type ConfigActionExecuteResult = {
|
|
147
|
+
ok: boolean;
|
|
148
|
+
status: 'success' | 'failed';
|
|
149
|
+
message: string;
|
|
150
|
+
data?: Record<string, unknown>;
|
|
151
|
+
patch?: Record<string, unknown>;
|
|
152
|
+
nextActions?: string[];
|
|
107
153
|
};
|
|
108
154
|
|
|
109
155
|
// WebSocket events
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
|
-
import { useConfig, useConfigSchema, useUpdateChannel } from '@/hooks/useConfig';
|
|
3
|
-
import { probeFeishu } from '@/api/config';
|
|
2
|
+
import { useConfig, useConfigSchema, useUpdateChannel, useExecuteConfigAction } from '@/hooks/useConfig';
|
|
4
3
|
import { useUiStore } from '@/stores/ui.store';
|
|
5
4
|
import {
|
|
6
5
|
Dialog,
|
|
@@ -19,6 +18,7 @@ import { t } from '@/lib/i18n';
|
|
|
19
18
|
import { hintForPath } from '@/lib/config-hints';
|
|
20
19
|
import { toast } from 'sonner';
|
|
21
20
|
import { MessageCircle, Settings, ToggleLeft, Hash, Mail, Globe, KeyRound } from 'lucide-react';
|
|
21
|
+
import type { ConfigActionManifest } from '@/api/types';
|
|
22
22
|
|
|
23
23
|
// Field icon mapping
|
|
24
24
|
const getFieldIcon = (fieldName: string) => {
|
|
@@ -51,6 +51,7 @@ const CHANNEL_FIELDS: Record<string, Array<{ name: string; type: string; label:
|
|
|
51
51
|
discord: [
|
|
52
52
|
{ name: 'enabled', type: 'boolean', label: t('enabled') },
|
|
53
53
|
{ name: 'token', type: 'password', label: t('botToken') },
|
|
54
|
+
{ name: 'allowBots', type: 'boolean', label: 'Allow Bot Messages' },
|
|
54
55
|
{ name: 'allowFrom', type: 'tags', label: t('allowFrom') },
|
|
55
56
|
{ name: 'gatewayUrl', type: 'text', label: t('gatewayUrl') },
|
|
56
57
|
{ name: 'intents', type: 'number', label: t('intents') }
|
|
@@ -78,6 +79,7 @@ const CHANNEL_FIELDS: Record<string, Array<{ name: string; type: string; label:
|
|
|
78
79
|
{ name: 'enabled', type: 'boolean', label: t('enabled') },
|
|
79
80
|
{ name: 'mode', type: 'text', label: t('mode') },
|
|
80
81
|
{ name: 'webhookPath', type: 'text', label: t('webhookPath') },
|
|
82
|
+
{ name: 'allowBots', type: 'boolean', label: 'Allow Bot Messages' },
|
|
81
83
|
{ name: 'botToken', type: 'password', label: t('botToken') },
|
|
82
84
|
{ name: 'appToken', type: 'password', label: t('appToken') }
|
|
83
85
|
],
|
|
@@ -120,19 +122,52 @@ const channelColors: Record<string, string> = {
|
|
|
120
122
|
default: 'from-slate-400 to-gray-500'
|
|
121
123
|
};
|
|
122
124
|
|
|
125
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
126
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function deepMergeRecords(base: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown> {
|
|
130
|
+
const next: Record<string, unknown> = { ...base };
|
|
131
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
132
|
+
const prev = next[key];
|
|
133
|
+
if (isRecord(prev) && isRecord(value)) {
|
|
134
|
+
next[key] = deepMergeRecords(prev, value);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
next[key] = value;
|
|
138
|
+
}
|
|
139
|
+
return next;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function buildScopeDraft(scope: string, value: Record<string, unknown>): Record<string, unknown> {
|
|
143
|
+
const segments = scope.split('.');
|
|
144
|
+
const output: Record<string, unknown> = {};
|
|
145
|
+
let cursor: Record<string, unknown> = output;
|
|
146
|
+
for (let index = 0; index < segments.length - 1; index += 1) {
|
|
147
|
+
const segment = segments[index];
|
|
148
|
+
cursor[segment] = {};
|
|
149
|
+
cursor = cursor[segment] as Record<string, unknown>;
|
|
150
|
+
}
|
|
151
|
+
cursor[segments[segments.length - 1]] = value;
|
|
152
|
+
return output;
|
|
153
|
+
}
|
|
154
|
+
|
|
123
155
|
export function ChannelForm() {
|
|
124
156
|
const { channelModal, closeChannelModal } = useUiStore();
|
|
125
157
|
const { data: config } = useConfig();
|
|
126
158
|
const { data: schema } = useConfigSchema();
|
|
127
159
|
const updateChannel = useUpdateChannel();
|
|
160
|
+
const executeAction = useExecuteConfigAction();
|
|
128
161
|
|
|
129
162
|
const [formData, setFormData] = useState<Record<string, unknown>>({});
|
|
130
|
-
const [
|
|
163
|
+
const [runningActionId, setRunningActionId] = useState<string | null>(null);
|
|
131
164
|
|
|
132
165
|
const channelName = channelModal.channel;
|
|
133
166
|
const channelConfig = channelName ? config?.channels[channelName] : null;
|
|
134
167
|
const fields = channelName ? CHANNEL_FIELDS[channelName] : [];
|
|
135
168
|
const uiHints = schema?.uiHints;
|
|
169
|
+
const scope = channelName ? `channels.${channelName}` : null;
|
|
170
|
+
const actions = schema?.actions?.filter((action) => action.scope === scope) ?? [];
|
|
136
171
|
const channelLabel = channelName
|
|
137
172
|
? hintForPath(`channels.${channelName}`, uiHints)?.label ?? channelName
|
|
138
173
|
: channelName;
|
|
@@ -160,23 +195,59 @@ export function ChannelForm() {
|
|
|
160
195
|
);
|
|
161
196
|
};
|
|
162
197
|
|
|
163
|
-
const
|
|
164
|
-
if (!
|
|
165
|
-
|
|
198
|
+
const applyActionPatchToForm = (patch?: Record<string, unknown>) => {
|
|
199
|
+
if (!patch || !channelName) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const channelsNode = patch.channels;
|
|
203
|
+
if (!isRecord(channelsNode)) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const channelPatch = channelsNode[channelName];
|
|
207
|
+
if (!isRecord(channelPatch)) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
setFormData((prev) => deepMergeRecords(prev, channelPatch));
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const handleManualAction = async (action: ConfigActionManifest) => {
|
|
214
|
+
if (!channelName || !scope) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
setRunningActionId(action.id);
|
|
166
219
|
try {
|
|
167
|
-
|
|
168
|
-
|
|
220
|
+
let nextData = { ...formData };
|
|
221
|
+
|
|
222
|
+
if (action.saveBeforeRun) {
|
|
223
|
+
nextData = {
|
|
224
|
+
...nextData,
|
|
225
|
+
...(action.savePatch ?? {})
|
|
226
|
+
};
|
|
169
227
|
setFormData(nextData);
|
|
228
|
+
await updateChannel.mutateAsync({ channel: channelName, data: nextData });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const result = await executeAction.mutateAsync({
|
|
232
|
+
actionId: action.id,
|
|
233
|
+
data: {
|
|
234
|
+
scope,
|
|
235
|
+
draftConfig: buildScopeDraft(scope, nextData)
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
applyActionPatchToForm(result.patch);
|
|
240
|
+
|
|
241
|
+
if (result.ok) {
|
|
242
|
+
toast.success(result.message || t('success'));
|
|
243
|
+
} else {
|
|
244
|
+
toast.error(result.message || t('error'));
|
|
170
245
|
}
|
|
171
|
-
await updateChannel.mutateAsync({ channel: channelName, data: nextData });
|
|
172
|
-
const probe = await probeFeishu();
|
|
173
|
-
const botLabel = probe.botName ? ` (${probe.botName})` : '';
|
|
174
|
-
toast.success(t('feishuVerifySuccess') + botLabel);
|
|
175
246
|
} catch (error) {
|
|
176
247
|
const message = error instanceof Error ? error.message : String(error);
|
|
177
|
-
toast.error(`${t('
|
|
248
|
+
toast.error(`${t('error')}: ${message}`);
|
|
178
249
|
} finally {
|
|
179
|
-
|
|
250
|
+
setRunningActionId(null);
|
|
180
251
|
}
|
|
181
252
|
};
|
|
182
253
|
|
|
@@ -284,20 +355,23 @@ export function ChannelForm() {
|
|
|
284
355
|
</Button>
|
|
285
356
|
<Button
|
|
286
357
|
type="submit"
|
|
287
|
-
disabled={updateChannel.isPending ||
|
|
358
|
+
disabled={updateChannel.isPending || Boolean(runningActionId)}
|
|
288
359
|
>
|
|
289
360
|
{updateChannel.isPending ? 'Saving...' : t('save')}
|
|
290
361
|
</Button>
|
|
291
|
-
{
|
|
362
|
+
{actions
|
|
363
|
+
.filter((action) => action.trigger === 'manual')
|
|
364
|
+
.map((action) => (
|
|
292
365
|
<Button
|
|
366
|
+
key={action.id}
|
|
293
367
|
type="button"
|
|
294
|
-
onClick={
|
|
295
|
-
disabled={updateChannel.isPending ||
|
|
368
|
+
onClick={() => handleManualAction(action)}
|
|
369
|
+
disabled={updateChannel.isPending || Boolean(runningActionId)}
|
|
296
370
|
variant="secondary"
|
|
297
371
|
>
|
|
298
|
-
{
|
|
372
|
+
{runningActionId === action.id ? t('connecting') : action.title}
|
|
299
373
|
</Button>
|
|
300
|
-
|
|
374
|
+
))}
|
|
301
375
|
</DialogFooter>
|
|
302
376
|
</form>
|
|
303
377
|
</div>
|
package/src/hooks/useConfig.ts
CHANGED
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
fetchConfigSchema,
|
|
6
6
|
updateModel,
|
|
7
7
|
updateProvider,
|
|
8
|
-
updateChannel
|
|
8
|
+
updateChannel,
|
|
9
|
+
executeConfigAction
|
|
9
10
|
} from '@/api/config';
|
|
10
11
|
import { toast } from 'sonner';
|
|
11
12
|
import { t } from '@/lib/i18n';
|
|
@@ -81,3 +82,13 @@ export function useUpdateChannel() {
|
|
|
81
82
|
}
|
|
82
83
|
});
|
|
83
84
|
}
|
|
85
|
+
|
|
86
|
+
export function useExecuteConfigAction() {
|
|
87
|
+
return useMutation({
|
|
88
|
+
mutationFn: ({ actionId, data }: { actionId: string; data: unknown }) =>
|
|
89
|
+
executeConfigAction(actionId, data as Parameters<typeof executeConfigAction>[1]),
|
|
90
|
+
onError: (error: Error) => {
|
|
91
|
+
toast.error(t('error') + ': ' + error.message);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|