@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/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-CDd3pWyf.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-CrA44GOI.css">
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/api/client.ts CHANGED
@@ -16,7 +16,21 @@ async function apiRequest<T>(
16
16
  ...options
17
17
  });
18
18
 
19
- const data = await response.json();
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, Settings2, Bell, Mail, MessageSquare, Slack, MoreHorizontal, Plus } from 'lucide-react';
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 { Loader2, Save, Sparkles, Sliders, Folder } from 'lucide-react';
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="e.g. gpt-4, claude-3"
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' }