@nextclaw/ui 0.2.3 → 0.2.4
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 +6 -0
- package/dist/assets/index-C4OKhpdC.css +1 -0
- package/dist/assets/index-C8nOCIVG.js +240 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/api/client.ts +15 -1
- package/src/api/config.ts +11 -1
- package/src/api/types.ts +7 -0
- package/src/components/config/ChannelForm.tsx +35 -1
- package/src/components/config/ChannelsList.tsx +13 -1
- package/src/components/config/ModelConfig.tsx +6 -5
- package/src/lib/i18n.ts +7 -0
- package/dist/assets/index-CDd3pWyf.js +0 -235
- package/dist/assets/index-CrA44GOI.css +0 -1
package/dist/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>nextclaw - 系统配置</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-C8nOCIVG.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C4OKhpdC.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
package/src/api/client.ts
CHANGED
|
@@ -16,7 +16,21 @@ async function apiRequest<T>(
|
|
|
16
16
|
...options
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
const
|
|
19
|
+
const text = await response.text();
|
|
20
|
+
let data: ApiResponse<T> | null = null;
|
|
21
|
+
if (text) {
|
|
22
|
+
try {
|
|
23
|
+
data = JSON.parse(text) as ApiResponse<T>;
|
|
24
|
+
} catch {
|
|
25
|
+
// fall through to build a synthetic error response
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!data) {
|
|
30
|
+
const snippet = text ? text.slice(0, 200) : '';
|
|
31
|
+
const message = `Non-JSON response (${response.status} ${response.statusText})${snippet ? `: ${snippet}` : ''}`;
|
|
32
|
+
return { ok: false, error: { code: 'INVALID_RESPONSE', message } };
|
|
33
|
+
}
|
|
20
34
|
|
|
21
35
|
if (!response.ok) {
|
|
22
36
|
return data as ApiResponse<T>;
|
package/src/api/config.ts
CHANGED
|
@@ -5,7 +5,8 @@ import type {
|
|
|
5
5
|
ProviderConfigView,
|
|
6
6
|
UiConfigView,
|
|
7
7
|
ChannelConfigUpdate,
|
|
8
|
-
ProviderConfigUpdate
|
|
8
|
+
ProviderConfigUpdate,
|
|
9
|
+
FeishuProbeView
|
|
9
10
|
} from './types';
|
|
10
11
|
|
|
11
12
|
// GET /api/config
|
|
@@ -84,3 +85,12 @@ export async function reloadConfig(): Promise<{ status: string }> {
|
|
|
84
85
|
}
|
|
85
86
|
return response.data;
|
|
86
87
|
}
|
|
88
|
+
|
|
89
|
+
// POST /api/channels/feishu/probe
|
|
90
|
+
export async function probeFeishu(): Promise<FeishuProbeView> {
|
|
91
|
+
const response = await api.post<FeishuProbeView>('/api/channels/feishu/probe', {});
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
throw new Error(response.error.message);
|
|
94
|
+
}
|
|
95
|
+
return response.data;
|
|
96
|
+
}
|
package/src/api/types.ts
CHANGED
|
@@ -62,6 +62,7 @@ export type ChannelSpecView = {
|
|
|
62
62
|
name: string;
|
|
63
63
|
displayName?: string;
|
|
64
64
|
enabled: boolean;
|
|
65
|
+
tutorialUrl?: string;
|
|
65
66
|
};
|
|
66
67
|
|
|
67
68
|
export type ConfigMetaView = {
|
|
@@ -69,6 +70,12 @@ export type ConfigMetaView = {
|
|
|
69
70
|
channels: ChannelSpecView[];
|
|
70
71
|
};
|
|
71
72
|
|
|
73
|
+
export type FeishuProbeView = {
|
|
74
|
+
appId: string;
|
|
75
|
+
botName?: string | null;
|
|
76
|
+
botOpenId?: string | null;
|
|
77
|
+
};
|
|
78
|
+
|
|
72
79
|
// WebSocket events
|
|
73
80
|
export type WsEvent =
|
|
74
81
|
| { type: 'config.updated'; payload: { path: string } }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
import { useConfig, useUpdateChannel } from '@/hooks/useConfig';
|
|
3
|
+
import { probeFeishu, reloadConfig } from '@/api/config';
|
|
3
4
|
import { useUiStore } from '@/stores/ui.store';
|
|
4
5
|
import {
|
|
5
6
|
Dialog,
|
|
@@ -15,6 +16,7 @@ import { Label } from '@/components/ui/label';
|
|
|
15
16
|
import { Switch } from '@/components/ui/switch';
|
|
16
17
|
import { TagInput } from '@/components/common/TagInput';
|
|
17
18
|
import { t } from '@/lib/i18n';
|
|
19
|
+
import { toast } from 'sonner';
|
|
18
20
|
import { MessageCircle, Settings, ToggleLeft, Hash, Mail, Globe, KeyRound } from 'lucide-react';
|
|
19
21
|
|
|
20
22
|
// Field icon mapping
|
|
@@ -122,6 +124,7 @@ export function ChannelForm() {
|
|
|
122
124
|
const updateChannel = useUpdateChannel();
|
|
123
125
|
|
|
124
126
|
const [formData, setFormData] = useState<Record<string, unknown>>({});
|
|
127
|
+
const [isConnecting, setIsConnecting] = useState(false);
|
|
125
128
|
|
|
126
129
|
const channelName = channelModal.channel;
|
|
127
130
|
const channelConfig = channelName ? config?.channels[channelName] : null;
|
|
@@ -150,6 +153,27 @@ export function ChannelForm() {
|
|
|
150
153
|
);
|
|
151
154
|
};
|
|
152
155
|
|
|
156
|
+
const handleVerifyConnect = async () => {
|
|
157
|
+
if (!channelName || channelName !== 'feishu') return;
|
|
158
|
+
setIsConnecting(true);
|
|
159
|
+
try {
|
|
160
|
+
const nextData = { ...formData, enabled: true };
|
|
161
|
+
if (!formData.enabled) {
|
|
162
|
+
setFormData(nextData);
|
|
163
|
+
}
|
|
164
|
+
await updateChannel.mutateAsync({ channel: channelName, data: nextData });
|
|
165
|
+
const probe = await probeFeishu();
|
|
166
|
+
await reloadConfig();
|
|
167
|
+
const botLabel = probe.botName ? ` (${probe.botName})` : '';
|
|
168
|
+
toast.success(t('feishuVerifySuccess') + botLabel);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
171
|
+
toast.error(`${t('feishuVerifyFailed')}: ${message}`);
|
|
172
|
+
} finally {
|
|
173
|
+
setIsConnecting(false);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
153
177
|
const Icon = channelIcons[channelName || ''] || channelIcons.default;
|
|
154
178
|
const gradientClass = channelColors[channelName || ''] || channelColors.default;
|
|
155
179
|
|
|
@@ -245,11 +269,21 @@ export function ChannelForm() {
|
|
|
245
269
|
</Button>
|
|
246
270
|
<Button
|
|
247
271
|
type="submit"
|
|
248
|
-
disabled={updateChannel.isPending}
|
|
272
|
+
disabled={updateChannel.isPending || isConnecting}
|
|
249
273
|
className="rounded-xl bg-gradient-to-r from-orange-400 to-amber-500 hover:from-orange-500 hover:to-amber-600 text-white border-0"
|
|
250
274
|
>
|
|
251
275
|
{updateChannel.isPending ? 'Saving...' : t('save')}
|
|
252
276
|
</Button>
|
|
277
|
+
{channelName === 'feishu' && (
|
|
278
|
+
<Button
|
|
279
|
+
type="button"
|
|
280
|
+
onClick={handleVerifyConnect}
|
|
281
|
+
disabled={updateChannel.isPending || isConnecting}
|
|
282
|
+
className="rounded-xl bg-gradient-to-r from-emerald-400 to-emerald-600 hover:from-emerald-500 hover:to-emerald-700 text-white border-0"
|
|
283
|
+
>
|
|
284
|
+
{isConnecting ? t('feishuConnecting') : t('saveVerifyConnect')}
|
|
285
|
+
</Button>
|
|
286
|
+
)}
|
|
253
287
|
</DialogFooter>
|
|
254
288
|
</form>
|
|
255
289
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useConfig, useConfigMeta } from '@/hooks/useConfig';
|
|
2
2
|
import { Button } from '@/components/ui/button';
|
|
3
3
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
4
|
-
import { MessageCircle,
|
|
4
|
+
import { MessageCircle, Mail, MessageSquare, Slack, MoreHorizontal, ExternalLink, Bell } from 'lucide-react';
|
|
5
5
|
import { useState } from 'react';
|
|
6
6
|
import { ChannelForm } from './ChannelForm';
|
|
7
7
|
import { useUiStore } from '@/stores/ui.store';
|
|
@@ -76,6 +76,18 @@ export function ChannelsList() {
|
|
|
76
76
|
|
|
77
77
|
{/* Status/Actions */}
|
|
78
78
|
<div className="flex items-center gap-4">
|
|
79
|
+
{channel.tutorialUrl && (
|
|
80
|
+
<a
|
|
81
|
+
href={channel.tutorialUrl}
|
|
82
|
+
target="_blank"
|
|
83
|
+
rel="noreferrer"
|
|
84
|
+
onClick={(e) => e.stopPropagation()}
|
|
85
|
+
className="hidden sm:inline-flex items-center gap-1 text-[11px] font-bold text-[hsl(30,10%,35%)] hover:text-[hsl(30,15%,10%)]"
|
|
86
|
+
>
|
|
87
|
+
Guide
|
|
88
|
+
<ExternalLink className="h-3 w-3" />
|
|
89
|
+
</a>
|
|
90
|
+
)}
|
|
79
91
|
<Button
|
|
80
92
|
variant="ghost"
|
|
81
93
|
size="sm"
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react';
|
|
2
|
-
import { useConfig, useUpdateModel } from '@/hooks/useConfig';
|
|
3
1
|
import { Button } from '@/components/ui/button';
|
|
2
|
+
import { Card } from '@/components/ui/card';
|
|
4
3
|
import { Input } from '@/components/ui/input';
|
|
5
4
|
import { Label } from '@/components/ui/label';
|
|
6
|
-
import { Card, CardContent, CardTitle, CardDescription } from '@/components/ui/card';
|
|
7
5
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
8
|
-
import {
|
|
6
|
+
import { useConfig, useUpdateModel } from '@/hooks/useConfig';
|
|
7
|
+
import { Folder, Loader2, Sliders, Sparkles } from 'lucide-react';
|
|
8
|
+
import { useEffect, useState } from 'react';
|
|
9
9
|
|
|
10
10
|
export function ModelConfig() {
|
|
11
11
|
const { data: config, isLoading } = useConfig();
|
|
@@ -90,9 +90,10 @@ export function ModelConfig() {
|
|
|
90
90
|
id="model"
|
|
91
91
|
value={model}
|
|
92
92
|
onChange={(e) => setModel(e.target.value)}
|
|
93
|
-
placeholder="
|
|
93
|
+
placeholder="minimax/MiniMax-M2.1"
|
|
94
94
|
className="h-12 px-4 rounded-xl border-[hsl(40,10%,92%)] bg-white focus:ring-1 focus:ring-[hsl(30,15%,10%)] transition-all"
|
|
95
95
|
/>
|
|
96
|
+
<p className="text-[12px] text-[hsl(30,8%,55%)]">Examples: minimax/MiniMax-M2.1 · anthropic/claude-opus-4-5</p>
|
|
96
97
|
</div>
|
|
97
98
|
</div>
|
|
98
99
|
|
package/src/lib/i18n.ts
CHANGED
|
@@ -98,17 +98,24 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
|
|
|
98
98
|
port: { zh: '端口', en: 'Port' },
|
|
99
99
|
open: { zh: '自动打开', en: 'Open Automatically' },
|
|
100
100
|
reloadConfig: { zh: '重载配置', en: 'Reload Config' },
|
|
101
|
+
saveVerifyConnect: { zh: '保存并验证 / 连接', en: 'Save & Verify / Connect' },
|
|
101
102
|
|
|
102
103
|
// Status
|
|
103
104
|
connected: { zh: '已连接', en: 'Connected' },
|
|
104
105
|
disconnected: { zh: '未连接', en: 'Disconnected' },
|
|
105
106
|
connecting: { zh: '连接中...', en: 'Connecting...' },
|
|
107
|
+
feishuConnecting: { zh: '验证 / 连接中...', en: 'Verifying / connecting...' },
|
|
106
108
|
|
|
107
109
|
// Messages
|
|
108
110
|
configSaved: { zh: '配置已保存', en: 'Configuration saved' },
|
|
109
111
|
configSaveFailed: { zh: '保存配置失败', en: 'Failed to save configuration' },
|
|
110
112
|
configReloaded: { zh: '配置已重载', en: 'Configuration reloaded' },
|
|
111
113
|
configReloadFailed: { zh: '重载配置失败', en: 'Failed to reload configuration' },
|
|
114
|
+
feishuVerifySuccess: {
|
|
115
|
+
zh: '验证成功,请到飞书开放平台完成事件订阅与发布后再开始使用。',
|
|
116
|
+
en: 'Verified. Please finish Feishu event subscription and app publishing before using.'
|
|
117
|
+
},
|
|
118
|
+
feishuVerifyFailed: { zh: '验证失败', en: 'Verification failed' },
|
|
112
119
|
enterTag: { zh: '输入后按回车...', en: 'Type and press Enter...' },
|
|
113
120
|
headerName: { zh: 'Header 名称', en: 'Header Name' },
|
|
114
121
|
headerValue: { zh: 'Header 值', en: 'Header Value' }
|