@nextclaw/ui 0.3.10 → 0.3.12

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
@@ -6,8 +6,8 @@
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-DO3sh5Tk.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-DM9Q3WUX.css">
9
+ <script type="module" crossorigin src="/assets/index-DTd23uLj.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-D3arfjLX.css">
11
11
  </head>
12
12
 
13
13
  <body>
@@ -0,0 +1,11 @@
1
+ <svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="128" height="128" rx="28" fill="#07C160" />
3
+ <circle cx="50" cy="55" r="26" fill="white" />
4
+ <circle cx="82" cy="73" r="24" fill="#CFF6E2" />
5
+ <circle cx="40" cy="52" r="3.5" fill="#07C160" />
6
+ <circle cx="51" cy="52" r="3.5" fill="#07C160" />
7
+ <circle cx="62" cy="52" r="3.5" fill="#07C160" />
8
+ <circle cx="72" cy="70" r="3.5" fill="#07C160" />
9
+ <circle cx="83" cy="70" r="3.5" fill="#07C160" />
10
+ <circle cx="94" cy="70" r="3.5" fill="#07C160" />
11
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,11 @@
1
+ <svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="128" height="128" rx="28" fill="#07C160" />
3
+ <circle cx="50" cy="55" r="26" fill="white" />
4
+ <circle cx="82" cy="73" r="24" fill="#CFF6E2" />
5
+ <circle cx="40" cy="52" r="3.5" fill="#07C160" />
6
+ <circle cx="51" cy="52" r="3.5" fill="#07C160" />
7
+ <circle cx="62" cy="52" r="3.5" fill="#07C160" />
8
+ <circle cx="72" cy="70" r="3.5" fill="#07C160" />
9
+ <circle cx="83" cy="70" r="3.5" fill="#07C160" />
10
+ <circle cx="94" cy="70" r="3.5" fill="#07C160" />
11
+ </svg>
package/src/App.tsx CHANGED
@@ -4,6 +4,7 @@ import { AppLayout } from '@/components/layout/AppLayout';
4
4
  import { ModelConfig } from '@/components/config/ModelConfig';
5
5
  import { ProvidersList } from '@/components/config/ProvidersList';
6
6
  import { ChannelsList } from '@/components/config/ChannelsList';
7
+ import { RuntimeConfig } from '@/components/config/RuntimeConfig';
7
8
  import { useWebSocket } from '@/hooks/useWebSocket';
8
9
  import { Toaster } from 'sonner';
9
10
 
@@ -28,6 +29,8 @@ function AppContent() {
28
29
  return <ProvidersList />;
29
30
  case 'channels':
30
31
  return <ChannelsList />;
32
+ case 'runtime':
33
+ return <RuntimeConfig />;
31
34
  default:
32
35
  return <ModelConfig />;
33
36
  }
package/src/api/config.ts CHANGED
@@ -6,6 +6,7 @@ import type {
6
6
  ProviderConfigView,
7
7
  ChannelConfigUpdate,
8
8
  ProviderConfigUpdate,
9
+ RuntimeConfigUpdate,
9
10
  ConfigActionExecuteRequest,
10
11
  ConfigActionExecuteResult
11
12
  } from './types';
@@ -78,6 +79,20 @@ export async function updateChannel(
78
79
  return response.data;
79
80
  }
80
81
 
82
+ // PUT /api/config/runtime
83
+ export async function updateRuntime(
84
+ data: RuntimeConfigUpdate
85
+ ): Promise<Pick<ConfigView, 'agents' | 'bindings' | 'session'>> {
86
+ const response = await api.put<Pick<ConfigView, 'agents' | 'bindings' | 'session'>>(
87
+ '/api/config/runtime',
88
+ data
89
+ );
90
+ if (!response.ok) {
91
+ throw new Error(response.error.message);
92
+ }
93
+ return response.data;
94
+ }
95
+
81
96
  // POST /api/config/actions/:id/execute
82
97
  export async function executeConfigAction(
83
98
  actionId: string,
package/src/api/types.ts CHANGED
@@ -24,6 +24,44 @@ export type ProviderConfigUpdate = {
24
24
  wireApi?: "auto" | "chat" | "responses" | null;
25
25
  };
26
26
 
27
+ export type AgentProfileView = {
28
+ id: string;
29
+ default?: boolean;
30
+ workspace?: string;
31
+ model?: string;
32
+ maxTokens?: number;
33
+ maxToolIterations?: number;
34
+ };
35
+
36
+ export type BindingPeerView = {
37
+ kind: "direct" | "group" | "channel";
38
+ id: string;
39
+ };
40
+
41
+ export type AgentBindingView = {
42
+ agentId: string;
43
+ match: {
44
+ channel: string;
45
+ accountId?: string;
46
+ peer?: BindingPeerView;
47
+ };
48
+ };
49
+
50
+ export type SessionConfigView = {
51
+ dmScope?: "main" | "per-peer" | "per-channel-peer" | "per-account-channel-peer";
52
+ agentToAgent?: {
53
+ maxPingPongTurns?: number;
54
+ };
55
+ };
56
+
57
+ export type RuntimeConfigUpdate = {
58
+ agents?: {
59
+ list?: AgentProfileView[];
60
+ };
61
+ bindings?: AgentBindingView[];
62
+ session?: SessionConfigView;
63
+ };
64
+
27
65
  export type ChannelConfigUpdate = Record<string, unknown>;
28
66
 
29
67
  export type ConfigView = {
@@ -34,6 +72,7 @@ export type ConfigView = {
34
72
  maxTokens?: number;
35
73
  maxToolIterations?: number;
36
74
  };
75
+ list?: AgentProfileView[];
37
76
  context?: {
38
77
  bootstrap?: {
39
78
  files?: string[];
@@ -50,6 +89,8 @@ export type ConfigView = {
50
89
  };
51
90
  providers: Record<string, ProviderConfigView>;
52
91
  channels: Record<string, Record<string, unknown>>;
92
+ bindings?: AgentBindingView[];
93
+ session?: SessionConfigView;
53
94
  tools?: Record<string, unknown>;
54
95
  gateway?: Record<string, unknown>;
55
96
  };
@@ -1,4 +1,4 @@
1
- import { useEffect, useState } from 'react';
1
+ import { useEffect, useMemo, useState } from 'react';
2
2
  import { useConfig, useConfigSchema, useUpdateChannel, useExecuteConfigAction } from '@/hooks/useConfig';
3
3
  import { useUiStore } from '@/stores/ui.store';
4
4
  import {
@@ -20,6 +20,23 @@ import { toast } from 'sonner';
20
20
  import { MessageCircle, Settings, ToggleLeft, Hash, Mail, Globe, KeyRound } from 'lucide-react';
21
21
  import type { ConfigActionManifest } from '@/api/types';
22
22
 
23
+ type ChannelFieldType = 'boolean' | 'text' | 'email' | 'password' | 'number' | 'tags' | 'select' | 'json';
24
+ type ChannelOption = { value: string; label: string };
25
+ type ChannelField = { name: string; type: ChannelFieldType; label: string; options?: ChannelOption[] };
26
+
27
+ const DM_POLICY_OPTIONS: ChannelOption[] = [
28
+ { value: 'pairing', label: 'pairing' },
29
+ { value: 'allowlist', label: 'allowlist' },
30
+ { value: 'open', label: 'open' },
31
+ { value: 'disabled', label: 'disabled' }
32
+ ];
33
+
34
+ const GROUP_POLICY_OPTIONS: ChannelOption[] = [
35
+ { value: 'open', label: 'open' },
36
+ { value: 'allowlist', label: 'allowlist' },
37
+ { value: 'disabled', label: 'disabled' }
38
+ ];
39
+
23
40
  // Field icon mapping
24
41
  const getFieldIcon = (fieldName: string) => {
25
42
  if (fieldName.includes('token') || fieldName.includes('secret') || fieldName.includes('password')) {
@@ -41,12 +58,19 @@ const getFieldIcon = (fieldName: string) => {
41
58
  };
42
59
 
43
60
  // Channel field definitions
44
- const CHANNEL_FIELDS: Record<string, Array<{ name: string; type: string; label: string }>> = {
61
+ const CHANNEL_FIELDS: Record<string, ChannelField[]> = {
45
62
  telegram: [
46
63
  { name: 'enabled', type: 'boolean', label: t('enabled') },
47
64
  { name: 'token', type: 'password', label: t('botToken') },
48
65
  { name: 'allowFrom', type: 'tags', label: t('allowFrom') },
49
- { name: 'proxy', type: 'text', label: t('proxy') }
66
+ { name: 'proxy', type: 'text', label: t('proxy') },
67
+ { name: 'accountId', type: 'text', label: 'Account ID' },
68
+ { name: 'dmPolicy', type: 'select', label: 'DM Policy', options: DM_POLICY_OPTIONS },
69
+ { name: 'groupPolicy', type: 'select', label: 'Group Policy', options: GROUP_POLICY_OPTIONS },
70
+ { name: 'groupAllowFrom', type: 'tags', label: 'Group Allow From' },
71
+ { name: 'requireMention', type: 'boolean', label: 'Require Mention' },
72
+ { name: 'mentionPatterns', type: 'tags', label: 'Mention Patterns' },
73
+ { name: 'groups', type: 'json', label: 'Group Rules (JSON)' }
50
74
  ],
51
75
  discord: [
52
76
  { name: 'enabled', type: 'boolean', label: t('enabled') },
@@ -54,7 +78,16 @@ const CHANNEL_FIELDS: Record<string, Array<{ name: string; type: string; label:
54
78
  { name: 'allowBots', type: 'boolean', label: 'Allow Bot Messages' },
55
79
  { name: 'allowFrom', type: 'tags', label: t('allowFrom') },
56
80
  { name: 'gatewayUrl', type: 'text', label: t('gatewayUrl') },
57
- { name: 'intents', type: 'number', label: t('intents') }
81
+ { name: 'intents', type: 'number', label: t('intents') },
82
+ { name: 'proxy', type: 'text', label: t('proxy') },
83
+ { name: 'mediaMaxMb', type: 'number', label: 'Attachment Max Size (MB)' },
84
+ { name: 'accountId', type: 'text', label: 'Account ID' },
85
+ { name: 'dmPolicy', type: 'select', label: 'DM Policy', options: DM_POLICY_OPTIONS },
86
+ { name: 'groupPolicy', type: 'select', label: 'Group Policy', options: GROUP_POLICY_OPTIONS },
87
+ { name: 'groupAllowFrom', type: 'tags', label: 'Group Allow From' },
88
+ { name: 'requireMention', type: 'boolean', label: 'Require Mention' },
89
+ { name: 'mentionPatterns', type: 'tags', label: 'Mention Patterns' },
90
+ { name: 'groups', type: 'json', label: 'Group Rules (JSON)' }
58
91
  ],
59
92
  whatsapp: [
60
93
  { name: 'enabled', type: 'boolean', label: t('enabled') },
@@ -75,6 +108,16 @@ const CHANNEL_FIELDS: Record<string, Array<{ name: string; type: string; label:
75
108
  { name: 'clientSecret', type: 'password', label: t('clientSecret') },
76
109
  { name: 'allowFrom', type: 'tags', label: t('allowFrom') }
77
110
  ],
111
+ wecom: [
112
+ { name: 'enabled', type: 'boolean', label: t('enabled') },
113
+ { name: 'corpId', type: 'text', label: t('corpId') },
114
+ { name: 'agentId', type: 'text', label: t('agentId') },
115
+ { name: 'secret', type: 'password', label: t('secret') },
116
+ { name: 'token', type: 'password', label: t('token') },
117
+ { name: 'callbackPort', type: 'number', label: t('callbackPort') },
118
+ { name: 'callbackPath', type: 'text', label: t('callbackPath') },
119
+ { name: 'allowFrom', type: 'tags', label: t('allowFrom') }
120
+ ],
78
121
  slack: [
79
122
  { name: 'enabled', type: 'boolean', label: t('enabled') },
80
123
  { name: 'mode', type: 'text', label: t('mode') },
@@ -160,11 +203,12 @@ export function ChannelForm() {
160
203
  const executeAction = useExecuteConfigAction();
161
204
 
162
205
  const [formData, setFormData] = useState<Record<string, unknown>>({});
206
+ const [jsonDrafts, setJsonDrafts] = useState<Record<string, string>>({});
163
207
  const [runningActionId, setRunningActionId] = useState<string | null>(null);
164
208
 
165
209
  const channelName = channelModal.channel;
166
210
  const channelConfig = channelName ? config?.channels[channelName] : null;
167
- const fields = channelName ? CHANNEL_FIELDS[channelName] : [];
211
+ const fields = useMemo(() => (channelName ? CHANNEL_FIELDS[channelName] : []), [channelName]);
168
212
  const uiHints = schema?.uiHints;
169
213
  const scope = channelName ? `channels.${channelName}` : null;
170
214
  const actions = schema?.actions?.filter((action) => action.scope === scope) ?? [];
@@ -175,10 +219,19 @@ export function ChannelForm() {
175
219
  useEffect(() => {
176
220
  if (channelConfig) {
177
221
  setFormData({ ...channelConfig });
222
+ const nextDrafts: Record<string, string> = {};
223
+ fields
224
+ .filter((field) => field.type === 'json')
225
+ .forEach((field) => {
226
+ const value = channelConfig[field.name];
227
+ nextDrafts[field.name] = JSON.stringify(value ?? {}, null, 2);
228
+ });
229
+ setJsonDrafts(nextDrafts);
178
230
  } else {
179
231
  setFormData({});
232
+ setJsonDrafts({});
180
233
  }
181
- }, [channelConfig, channelName]);
234
+ }, [channelConfig, channelName, fields]);
182
235
 
183
236
  const updateField = (name: string, value: unknown) => {
184
237
  setFormData((prev) => ({ ...prev, [name]: value }));
@@ -189,8 +242,22 @@ export function ChannelForm() {
189
242
 
190
243
  if (!channelName) return;
191
244
 
245
+ const payload: Record<string, unknown> = { ...formData };
246
+ for (const field of fields) {
247
+ if (field.type !== 'json') {
248
+ continue;
249
+ }
250
+ const raw = jsonDrafts[field.name] ?? '';
251
+ try {
252
+ payload[field.name] = raw.trim() ? JSON.parse(raw) : {};
253
+ } catch {
254
+ toast.error(`Invalid JSON for ${field.name}`);
255
+ return;
256
+ }
257
+ }
258
+
192
259
  updateChannel.mutate(
193
- { channel: channelName, data: formData },
260
+ { channel: channelName, data: payload },
194
261
  { onSuccess: () => closeChannelModal() }
195
262
  );
196
263
  };
@@ -341,6 +408,35 @@ export function ChannelForm() {
341
408
  onChange={(tags) => updateField(field.name, tags)}
342
409
  />
343
410
  )}
411
+
412
+ {field.type === 'select' && (
413
+ <select
414
+ id={field.name}
415
+ value={(formData[field.name] as string) || ''}
416
+ onChange={(e) => updateField(field.name, e.target.value)}
417
+ className="flex h-10 w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm"
418
+ >
419
+ {(field.options ?? []).map((option) => (
420
+ <option key={option.value} value={option.value}>
421
+ {option.label}
422
+ </option>
423
+ ))}
424
+ </select>
425
+ )}
426
+
427
+ {field.type === 'json' && (
428
+ <textarea
429
+ id={field.name}
430
+ value={jsonDrafts[field.name] ?? '{}'}
431
+ onChange={(event) =>
432
+ setJsonDrafts((prev) => ({
433
+ ...prev,
434
+ [field.name]: event.target.value
435
+ }))
436
+ }
437
+ className="min-h-[120px] w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-mono"
438
+ />
439
+ )}
344
440
  </div>
345
441
  );
346
442
  })}