@nextclaw/ui 0.5.20 → 0.5.22

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/assets/ChannelsList-dmUfRDVE.js +1 -0
  3. package/dist/assets/ChatPage-CmRkhiCX.js +32 -0
  4. package/dist/assets/CronConfig-B15BA--M.js +1 -0
  5. package/dist/assets/{DocBrowser-C35MebbI.js → DocBrowser-CBwMA2wK.js} +1 -1
  6. package/dist/assets/{MarketplacePage-HjEQ8sFt.js → MarketplacePage-BZRz4tCA.js} +1 -1
  7. package/dist/assets/{ModelConfig-BpBoi1sz.js → ModelConfig-CByjZNvf.js} +1 -1
  8. package/dist/assets/ProvidersList-BbsQEnoZ.js +1 -0
  9. package/dist/assets/RuntimeConfig-CrQsFzP7.js +1 -0
  10. package/dist/assets/SecretsConfig-DPZqWmry.js +3 -0
  11. package/dist/assets/SessionsConfig-Bk7RCsWw.js +2 -0
  12. package/dist/assets/{action-link-CSScZ_id.js → action-link-CKSHFT5k.js} +1 -1
  13. package/dist/assets/{card-Cj58-DCd.js → card-6hv7Kf0F.js} +1 -1
  14. package/dist/assets/chat-message-B7oqvJ2d.js +3 -0
  15. package/dist/assets/{dialog-Ce8jNftN.js → dialog-Bmy_bApp.js} +2 -2
  16. package/dist/assets/index-BsDasSXm.css +1 -0
  17. package/dist/assets/index-y6creQ7S.js +2 -0
  18. package/dist/assets/{label-CQdP2NhF.js → label-Btp6gGxV.js} +1 -1
  19. package/dist/assets/{page-layout-Byyxptub.js → page-layout-DWVKbt_g.js} +1 -1
  20. package/dist/assets/{switch-ChJzdp0x.js → switch-AeXayTRS.js} +1 -1
  21. package/dist/assets/{tabs-custom-DWlAbbCy.js → tabs-custom-DEZwNpXo.js} +1 -1
  22. package/dist/assets/useConfig-BiQH98MD.js +1 -0
  23. package/dist/assets/{useConfirmDialog-B7iWHb5k.js → useConfirmDialog-BY0hni-H.js} +1 -1
  24. package/dist/assets/{vendor-Dz2q6Qmc.js → vendor-H2M3a_4Z.js} +87 -62
  25. package/dist/index.html +3 -3
  26. package/package.json +4 -1
  27. package/src/App.tsx +2 -0
  28. package/src/api/config.ts +16 -0
  29. package/src/api/types.ts +55 -1
  30. package/src/components/chat/ChatPage.tsx +4 -39
  31. package/src/components/chat/ChatThread.tsx +210 -0
  32. package/src/components/config/ChannelForm.tsx +9 -0
  33. package/src/components/config/SecretsConfig.tsx +469 -0
  34. package/src/components/config/SessionsConfig.tsx +3 -1
  35. package/src/components/layout/Sidebar.tsx +6 -1
  36. package/src/hooks/useConfig.ts +17 -0
  37. package/src/index.css +69 -0
  38. package/src/lib/chat-message.ts +215 -0
  39. package/src/lib/i18n.ts +52 -0
  40. package/dist/assets/ChannelsList-DqgRRdUH.js +0 -1
  41. package/dist/assets/ChatPage-BQyomkth.js +0 -1
  42. package/dist/assets/CronConfig-Bmg449JI.js +0 -1
  43. package/dist/assets/ProvidersList-0tYTV40v.js +0 -1
  44. package/dist/assets/RuntimeConfig-B_WI-DHf.js +0 -1
  45. package/dist/assets/SessionsConfig-BEt-f6WS.js +0 -2
  46. package/dist/assets/index-CPFSdkyQ.css +0 -1
  47. package/dist/assets/index-C_z1Na9N.js +0 -2
  48. package/dist/assets/useConfig-8lC_4LwH.js +0 -1
package/dist/index.html CHANGED
@@ -6,9 +6,9 @@
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-C_z1Na9N.js"></script>
10
- <link rel="modulepreload" crossorigin href="/assets/vendor-Dz2q6Qmc.js">
11
- <link rel="stylesheet" crossorigin href="/assets/index-CPFSdkyQ.css">
9
+ <script type="module" crossorigin src="/assets/index-y6creQ7S.js"></script>
10
+ <link rel="modulepreload" crossorigin href="/assets/vendor-H2M3a_4Z.js">
11
+ <link rel="stylesheet" crossorigin href="/assets/index-BsDasSXm.css">
12
12
  </head>
13
13
 
14
14
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/ui",
3
- "version": "0.5.20",
3
+ "version": "0.5.22",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -19,7 +19,10 @@
19
19
  "react": "^18.3.1",
20
20
  "react-dom": "^18.3.1",
21
21
  "react-hook-form": "^7.53.2",
22
+ "react-markdown": "^10.1.0",
22
23
  "react-router-dom": "^7.13.0",
24
+ "rehype-sanitize": "^6.0.0",
25
+ "remark-gfm": "^4.0.1",
23
26
  "sonner": "^1.7.1",
24
27
  "tailwind-merge": "^2.5.4",
25
28
  "zod": "^3.23.8",
package/src/App.tsx CHANGED
@@ -21,6 +21,7 @@ const ChannelsListPage = lazy(async () => ({ default: (await import('@/component
21
21
  const RuntimeConfigPage = lazy(async () => ({ default: (await import('@/components/config/RuntimeConfig')).RuntimeConfig }));
22
22
  const SessionsConfigPage = lazy(async () => ({ default: (await import('@/components/config/SessionsConfig')).SessionsConfig }));
23
23
  const CronConfigPage = lazy(async () => ({ default: (await import('@/components/config/CronConfig')).CronConfig }));
24
+ const SecretsConfigPage = lazy(async () => ({ default: (await import('@/components/config/SecretsConfig')).SecretsConfig }));
24
25
  const MarketplacePage = lazy(async () => ({ default: (await import('@/components/marketplace/MarketplacePage')).MarketplacePage }));
25
26
 
26
27
  function RouteFallback() {
@@ -47,6 +48,7 @@ function AppContent() {
47
48
  <Route path="/runtime" element={<LazyRoute><RuntimeConfigPage /></LazyRoute>} />
48
49
  <Route path="/sessions" element={<LazyRoute><SessionsConfigPage /></LazyRoute>} />
49
50
  <Route path="/cron" element={<LazyRoute><CronConfigPage /></LazyRoute>} />
51
+ <Route path="/secrets" element={<LazyRoute><SecretsConfigPage /></LazyRoute>} />
50
52
  <Route path="/marketplace" element={<Navigate to="/marketplace/plugins" replace />} />
51
53
  <Route path="/marketplace/:type" element={<LazyRoute><MarketplacePage /></LazyRoute>} />
52
54
  <Route path="/" element={<Navigate to="/chat" replace />} />
package/src/api/config.ts CHANGED
@@ -7,6 +7,8 @@ import type {
7
7
  ChannelConfigUpdate,
8
8
  ProviderConfigUpdate,
9
9
  RuntimeConfigUpdate,
10
+ SecretsConfigUpdate,
11
+ SecretsView,
10
12
  ConfigActionExecuteRequest,
11
13
  ConfigActionExecuteResult,
12
14
  SessionsListView,
@@ -103,6 +105,20 @@ export async function updateRuntime(
103
105
  return response.data;
104
106
  }
105
107
 
108
+ // PUT /api/config/secrets
109
+ export async function updateSecrets(
110
+ data: SecretsConfigUpdate
111
+ ): Promise<SecretsView> {
112
+ const response = await api.put<SecretsView>(
113
+ '/api/config/secrets',
114
+ data
115
+ );
116
+ if (!response.ok) {
117
+ throw new Error(response.error.message);
118
+ }
119
+ return response.data;
120
+ }
121
+
106
122
  // POST /api/config/actions/:id/execute
107
123
  export async function executeConfigAction(
108
124
  actionId: string,
package/src/api/types.ts CHANGED
@@ -73,10 +73,12 @@ export type SessionsListView = {
73
73
 
74
74
  export type SessionMessageView = {
75
75
  role: string;
76
- content: string;
76
+ content: unknown;
77
77
  timestamp: string;
78
78
  name?: string;
79
79
  tool_call_id?: string;
80
+ tool_calls?: Array<Record<string, unknown>>;
81
+ reasoning_content?: string;
80
82
  };
81
83
 
82
84
  export type SessionHistoryView = {
@@ -173,6 +175,57 @@ export type RuntimeConfigUpdate = {
173
175
  session?: SessionConfigView;
174
176
  };
175
177
 
178
+ export type SecretSourceView = "env" | "file" | "exec";
179
+
180
+ export type SecretRefView = {
181
+ source: SecretSourceView;
182
+ provider?: string;
183
+ id: string;
184
+ };
185
+
186
+ export type SecretProviderEnvView = {
187
+ source: "env";
188
+ prefix?: string;
189
+ };
190
+
191
+ export type SecretProviderFileView = {
192
+ source: "file";
193
+ path: string;
194
+ format?: "json";
195
+ };
196
+
197
+ export type SecretProviderExecView = {
198
+ source: "exec";
199
+ command: string;
200
+ args?: string[];
201
+ cwd?: string;
202
+ timeoutMs?: number;
203
+ };
204
+
205
+ export type SecretProviderView = SecretProviderEnvView | SecretProviderFileView | SecretProviderExecView;
206
+
207
+ export type SecretsView = {
208
+ enabled: boolean;
209
+ defaults: {
210
+ env?: string;
211
+ file?: string;
212
+ exec?: string;
213
+ };
214
+ providers: Record<string, SecretProviderView>;
215
+ refs: Record<string, SecretRefView>;
216
+ };
217
+
218
+ export type SecretsConfigUpdate = {
219
+ enabled?: boolean;
220
+ defaults?: {
221
+ env?: string | null;
222
+ file?: string | null;
223
+ exec?: string | null;
224
+ };
225
+ providers?: Record<string, SecretProviderView> | null;
226
+ refs?: Record<string, SecretRefView> | null;
227
+ };
228
+
176
229
  export type ChannelConfigUpdate = Record<string, unknown>;
177
230
 
178
231
  export type ConfigView = {
@@ -205,6 +258,7 @@ export type ConfigView = {
205
258
  session?: SessionConfigView;
206
259
  tools?: Record<string, unknown>;
207
260
  gateway?: Record<string, unknown>;
261
+ secrets?: SecretsView;
208
262
  };
209
263
 
210
264
  export type ProviderSpecView = {
@@ -6,9 +6,10 @@ import { Button } from '@/components/ui/button';
6
6
  import { Input } from '@/components/ui/input';
7
7
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
8
8
  import { PageHeader, PageLayout } from '@/components/layout/page-layout';
9
+ import { ChatThread } from '@/components/chat/ChatThread';
9
10
  import { cn } from '@/lib/utils';
10
11
  import { formatDateTime, t } from '@/lib/i18n';
11
- import { Bot, MessageSquareText, Plus, RefreshCw, Search, Send, Trash2, User } from 'lucide-react';
12
+ import { MessageSquareText, Plus, RefreshCw, Search, Send, Trash2 } from 'lucide-react';
12
13
 
13
14
  const CHAT_SESSION_STORAGE_KEY = 'nextclaw.ui.chat.activeSession';
14
15
 
@@ -61,30 +62,6 @@ function sessionDisplayName(session: SessionEntryView): string {
61
62
  return chunks[chunks.length - 1] || session.key;
62
63
  }
63
64
 
64
- function MessageBubble({ message }: { message: SessionMessageView }) {
65
- const role = message.role.toLowerCase();
66
- const isUser = role === 'user';
67
- return (
68
- <div className={cn('flex w-full', isUser ? 'justify-end' : 'justify-start')}>
69
- <div
70
- className={cn(
71
- 'max-w-[88%] rounded-2xl px-4 py-3 shadow-sm border',
72
- isUser
73
- ? 'bg-primary text-white border-primary rounded-br-md'
74
- : 'bg-white text-gray-800 border-gray-200 rounded-bl-md'
75
- )}
76
- >
77
- <div className="mb-1 flex items-center gap-2 text-[11px] opacity-80">
78
- {isUser ? <User className="h-3.5 w-3.5" /> : <Bot className="h-3.5 w-3.5" />}
79
- <span className="font-semibold">{message.role}</span>
80
- <span>{formatDateTime(message.timestamp)}</span>
81
- </div>
82
- <div className="whitespace-pre-wrap break-words text-sm leading-relaxed">{message.content}</div>
83
- </div>
84
- </div>
85
- );
86
- }
87
-
88
65
  export function ChatPage() {
89
66
  const [query, setQuery] = useState('');
90
67
  const [draft, setDraft] = useState('');
@@ -342,7 +319,7 @@ export function ChatPage() {
342
319
  </Button>
343
320
  </div>
344
321
 
345
- <div ref={threadRef} className="flex-1 min-h-0 overflow-y-auto custom-scrollbar px-5 py-5 space-y-3">
322
+ <div ref={threadRef} className="flex-1 min-h-0 overflow-y-auto custom-scrollbar px-5 py-5">
346
323
  {!selectedSessionKey ? (
347
324
  <div className="h-full flex items-center justify-center">
348
325
  <div className="text-center text-gray-500">
@@ -358,19 +335,7 @@ export function ChatPage() {
358
335
  {mergedMessages.length === 0 ? (
359
336
  <div className="text-sm text-gray-500">{t('chatNoMessages')}</div>
360
337
  ) : (
361
- mergedMessages.map((message, index) => (
362
- <MessageBubble
363
- key={`${message.timestamp}-${message.role}-${index}`}
364
- message={message}
365
- />
366
- ))
367
- )}
368
- {sendChatTurn.isPending && (
369
- <div className="flex justify-start">
370
- <div className="rounded-2xl rounded-bl-md border border-gray-200 bg-white px-4 py-3 text-sm text-gray-500">
371
- {t('chatTyping')}
372
- </div>
373
- </div>
338
+ <ChatThread messages={mergedMessages} isSending={sendChatTurn.isPending} />
374
339
  )}
375
340
  </>
376
341
  )}
@@ -0,0 +1,210 @@
1
+ import { useMemo } from 'react';
2
+ import type { SessionMessageView } from '@/api/types';
3
+ import { cn } from '@/lib/utils';
4
+ import { extractMessageText, extractToolCards, groupChatMessages, type ChatRole, type ToolCard } from '@/lib/chat-message';
5
+ import { formatDateTime, t } from '@/lib/i18n';
6
+ import ReactMarkdown from 'react-markdown';
7
+ import rehypeSanitize from 'rehype-sanitize';
8
+ import remarkGfm from 'remark-gfm';
9
+ import { Bot, Clock3, FileSearch, Globe, Search, SendHorizontal, Terminal, User, Wrench } from 'lucide-react';
10
+
11
+ type ChatThreadProps = {
12
+ messages: SessionMessageView[];
13
+ isSending: boolean;
14
+ className?: string;
15
+ };
16
+
17
+ const MARKDOWN_MAX_CHARS = 140_000;
18
+ const TOOL_OUTPUT_PREVIEW_MAX = 220;
19
+
20
+ function trimMarkdown(value: string): string {
21
+ if (value.length <= MARKDOWN_MAX_CHARS) {
22
+ return value;
23
+ }
24
+ return `${value.slice(0, MARKDOWN_MAX_CHARS)}\n\n…`;
25
+ }
26
+
27
+ function roleTitle(role: ChatRole): string {
28
+ if (role === 'user') return t('chatRoleUser');
29
+ if (role === 'assistant') return t('chatRoleAssistant');
30
+ if (role === 'tool') return t('chatRoleTool');
31
+ if (role === 'system') return t('chatRoleSystem');
32
+ return t('chatRoleMessage');
33
+ }
34
+
35
+ function renderToolIcon(name: string) {
36
+ const lowered = name.toLowerCase();
37
+ if (lowered.includes('exec') || lowered.includes('shell') || lowered.includes('command')) {
38
+ return <Terminal className="h-3.5 w-3.5" />;
39
+ }
40
+ if (lowered.includes('search')) {
41
+ return <Search className="h-3.5 w-3.5" />;
42
+ }
43
+ if (lowered.includes('fetch') || lowered.includes('http') || lowered.includes('web')) {
44
+ return <Globe className="h-3.5 w-3.5" />;
45
+ }
46
+ if (lowered.includes('read') || lowered.includes('file')) {
47
+ return <FileSearch className="h-3.5 w-3.5" />;
48
+ }
49
+ if (lowered.includes('message') || lowered.includes('send')) {
50
+ return <SendHorizontal className="h-3.5 w-3.5" />;
51
+ }
52
+ if (lowered.includes('cron') || lowered.includes('schedule')) {
53
+ return <Clock3 className="h-3.5 w-3.5" />;
54
+ }
55
+ return <Wrench className="h-3.5 w-3.5" />;
56
+ }
57
+
58
+ function RoleAvatar({ role }: { role: ChatRole }) {
59
+ if (role === 'user') {
60
+ return (
61
+ <div className="h-8 w-8 rounded-full bg-primary text-white flex items-center justify-center shadow-sm">
62
+ <User className="h-4 w-4" />
63
+ </div>
64
+ );
65
+ }
66
+ if (role === 'assistant') {
67
+ return (
68
+ <div className="h-8 w-8 rounded-full bg-slate-900 text-white flex items-center justify-center shadow-sm">
69
+ <Bot className="h-4 w-4" />
70
+ </div>
71
+ );
72
+ }
73
+ return (
74
+ <div className="h-8 w-8 rounded-full bg-amber-100 text-amber-700 flex items-center justify-center shadow-sm">
75
+ <Wrench className="h-4 w-4" />
76
+ </div>
77
+ );
78
+ }
79
+
80
+ function MarkdownBlock({ text, role }: { text: string; role: ChatRole }) {
81
+ const isUser = role === 'user';
82
+ return (
83
+ <div className={cn('chat-markdown', isUser ? 'chat-markdown-user' : 'chat-markdown-assistant')}>
84
+ <ReactMarkdown
85
+ remarkPlugins={[remarkGfm]}
86
+ rehypePlugins={[rehypeSanitize]}
87
+ components={{
88
+ a: ({ ...props }) => (
89
+ <a {...props} target="_blank" rel="noreferrer noopener" />
90
+ )
91
+ }}
92
+ >
93
+ {trimMarkdown(text)}
94
+ </ReactMarkdown>
95
+ </div>
96
+ );
97
+ }
98
+
99
+ function ToolCardView({ card }: { card: ToolCard }) {
100
+ const title = card.kind === 'call' ? t('chatToolCall') : t('chatToolResult');
101
+ const output = card.text?.trim() ?? '';
102
+ const showDetails = output.length > TOOL_OUTPUT_PREVIEW_MAX || output.includes('\n');
103
+ const preview = showDetails ? `${output.slice(0, TOOL_OUTPUT_PREVIEW_MAX)}…` : output;
104
+
105
+ return (
106
+ <div className="rounded-xl border border-amber-200/80 bg-amber-50/60 px-3 py-2.5">
107
+ <div className="flex items-center gap-2 text-xs text-amber-800 font-semibold">
108
+ {renderToolIcon(card.name)}
109
+ <span>{title}</span>
110
+ <span className="font-mono text-[11px] text-amber-900/80">{card.name}</span>
111
+ </div>
112
+ {card.detail && (
113
+ <div className="mt-1 text-[11px] text-amber-800/90 font-mono break-words">{card.detail}</div>
114
+ )}
115
+ {card.kind === 'result' && (
116
+ <div className="mt-2">
117
+ {!output ? (
118
+ <div className="text-[11px] text-amber-700/80">{t('chatToolNoOutput')}</div>
119
+ ) : showDetails ? (
120
+ <details className="group">
121
+ <summary className="cursor-pointer text-[11px] text-amber-700">{t('chatToolOutput')}</summary>
122
+ <pre className="mt-2 rounded-lg border border-amber-200 bg-amber-100/40 p-2 text-[11px] whitespace-pre-wrap break-words text-amber-900">
123
+ {output}
124
+ </pre>
125
+ </details>
126
+ ) : (
127
+ <pre className="rounded-lg border border-amber-200 bg-amber-100/40 p-2 text-[11px] whitespace-pre-wrap break-words text-amber-900">
128
+ {preview}
129
+ </pre>
130
+ )}
131
+ </div>
132
+ )}
133
+ </div>
134
+ );
135
+ }
136
+
137
+ function MessageCard({ message, role }: { message: SessionMessageView; role: ChatRole }) {
138
+ const text = extractMessageText(message.content).trim();
139
+ const toolCards = extractToolCards(message);
140
+ const reasoning = typeof message.reasoning_content === 'string' ? message.reasoning_content.trim() : '';
141
+ const shouldRenderText = Boolean(text) && !(role === 'tool' && toolCards.length > 0);
142
+ const isUser = role === 'user';
143
+
144
+ return (
145
+ <div
146
+ className={cn(
147
+ 'rounded-2xl border px-4 py-3 shadow-sm',
148
+ isUser
149
+ ? 'bg-primary text-white border-primary'
150
+ : role === 'assistant'
151
+ ? 'bg-white text-gray-900 border-gray-200'
152
+ : 'bg-orange-50/70 text-gray-900 border-orange-200/80'
153
+ )}
154
+ >
155
+ {shouldRenderText && <MarkdownBlock text={text} role={role} />}
156
+ {reasoning && (
157
+ <details className="mt-3">
158
+ <summary className={cn('cursor-pointer text-xs', isUser ? 'text-primary-100' : 'text-gray-500')}>
159
+ {t('chatReasoning')}
160
+ </summary>
161
+ <pre className={cn('mt-2 text-[11px] whitespace-pre-wrap break-words rounded-lg p-2', isUser ? 'bg-primary-700/60' : 'bg-gray-100')}>
162
+ {reasoning}
163
+ </pre>
164
+ </details>
165
+ )}
166
+ {toolCards.length > 0 && (
167
+ <div className="mt-3 space-y-2">
168
+ {toolCards.map((card, index) => (
169
+ <ToolCardView key={`${card.kind}-${card.name}-${card.callId ?? index}`} card={card} />
170
+ ))}
171
+ </div>
172
+ )}
173
+ </div>
174
+ );
175
+ }
176
+
177
+ export function ChatThread({ messages, isSending, className }: ChatThreadProps) {
178
+ const groups = useMemo(() => groupChatMessages(messages), [messages]);
179
+
180
+ return (
181
+ <div className={cn('space-y-5', className)}>
182
+ {groups.map((group) => {
183
+ const isUser = group.role === 'user';
184
+ return (
185
+ <div key={group.key} className={cn('flex gap-3', isUser ? 'justify-end' : 'justify-start')}>
186
+ {!isUser && <RoleAvatar role={group.role} />}
187
+ <div className={cn('max-w-[88%] min-w-[280px] space-y-2', isUser && 'flex flex-col items-end')}>
188
+ {group.messages.map((message, index) => (
189
+ <MessageCard key={`${group.key}-${index}`} message={message} role={group.role} />
190
+ ))}
191
+ <div className={cn('text-[11px] px-1', isUser ? 'text-primary-300' : 'text-gray-400')}>
192
+ {roleTitle(group.role)} · {formatDateTime(group.timestamp)}
193
+ </div>
194
+ </div>
195
+ {isUser && <RoleAvatar role={group.role} />}
196
+ </div>
197
+ );
198
+ })}
199
+
200
+ {isSending && (
201
+ <div className="flex gap-3 justify-start">
202
+ <RoleAvatar role="assistant" />
203
+ <div className="rounded-2xl border border-gray-200 bg-white px-4 py-3 text-sm text-gray-500 shadow-sm">
204
+ {t('chatTyping')}
205
+ </div>
206
+ </div>
207
+ )}
208
+ </div>
209
+ );
210
+ }
@@ -256,6 +256,15 @@ export function ChannelForm() {
256
256
  if (!channelName) return;
257
257
 
258
258
  const payload: Record<string, unknown> = { ...formData };
259
+ for (const field of fields) {
260
+ if (field.type !== 'password') {
261
+ continue;
262
+ }
263
+ const value = payload[field.name];
264
+ if (typeof value !== 'string' || value.length === 0) {
265
+ delete payload[field.name];
266
+ }
267
+ }
259
268
  for (const field of fields) {
260
269
  if (field.type !== 'json') {
261
270
  continue;