@nextclaw/ui 0.3.11 → 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-CANDXRNv.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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.3.11",
3
+ "version": "0.3.12",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
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') },
@@ -170,11 +203,12 @@ export function ChannelForm() {
170
203
  const executeAction = useExecuteConfigAction();
171
204
 
172
205
  const [formData, setFormData] = useState<Record<string, unknown>>({});
206
+ const [jsonDrafts, setJsonDrafts] = useState<Record<string, string>>({});
173
207
  const [runningActionId, setRunningActionId] = useState<string | null>(null);
174
208
 
175
209
  const channelName = channelModal.channel;
176
210
  const channelConfig = channelName ? config?.channels[channelName] : null;
177
- const fields = channelName ? CHANNEL_FIELDS[channelName] : [];
211
+ const fields = useMemo(() => (channelName ? CHANNEL_FIELDS[channelName] : []), [channelName]);
178
212
  const uiHints = schema?.uiHints;
179
213
  const scope = channelName ? `channels.${channelName}` : null;
180
214
  const actions = schema?.actions?.filter((action) => action.scope === scope) ?? [];
@@ -185,10 +219,19 @@ export function ChannelForm() {
185
219
  useEffect(() => {
186
220
  if (channelConfig) {
187
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);
188
230
  } else {
189
231
  setFormData({});
232
+ setJsonDrafts({});
190
233
  }
191
- }, [channelConfig, channelName]);
234
+ }, [channelConfig, channelName, fields]);
192
235
 
193
236
  const updateField = (name: string, value: unknown) => {
194
237
  setFormData((prev) => ({ ...prev, [name]: value }));
@@ -199,8 +242,22 @@ export function ChannelForm() {
199
242
 
200
243
  if (!channelName) return;
201
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
+
202
259
  updateChannel.mutate(
203
- { channel: channelName, data: formData },
260
+ { channel: channelName, data: payload },
204
261
  { onSuccess: () => closeChannelModal() }
205
262
  );
206
263
  };
@@ -351,6 +408,35 @@ export function ChannelForm() {
351
408
  onChange={(tags) => updateField(field.name, tags)}
352
409
  />
353
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
+ )}
354
440
  </div>
355
441
  );
356
442
  })}