@nextclaw/ui 0.5.21 → 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 (37) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/assets/ChannelsList-dmUfRDVE.js +1 -0
  3. package/dist/assets/{ChatPage-BUm3UPap.js → ChatPage-CmRkhiCX.js} +1 -1
  4. package/dist/assets/{CronConfig-9dYfTRJl.js → CronConfig-B15BA--M.js} +1 -1
  5. package/dist/assets/{DocBrowser-BIV0vpA0.js → DocBrowser-CBwMA2wK.js} +1 -1
  6. package/dist/assets/{MarketplacePage-2Zi0JSVi.js → MarketplacePage-BZRz4tCA.js} +1 -1
  7. package/dist/assets/{ModelConfig-h21P5rV0.js → ModelConfig-CByjZNvf.js} +1 -1
  8. package/dist/assets/{ProvidersList-DEaK1a3y.js → ProvidersList-BbsQEnoZ.js} +1 -1
  9. package/dist/assets/{RuntimeConfig-DXMzf-gF.js → RuntimeConfig-CrQsFzP7.js} +1 -1
  10. package/dist/assets/SecretsConfig-DPZqWmry.js +3 -0
  11. package/dist/assets/{SessionsConfig-SdXvn_9E.js → SessionsConfig-Bk7RCsWw.js} +2 -2
  12. package/dist/assets/{action-link-C9xMkxl2.js → action-link-CKSHFT5k.js} +1 -1
  13. package/dist/assets/{card-Cnqfntk5.js → card-6hv7Kf0F.js} +1 -1
  14. package/dist/assets/{dialog-DJs630RE.js → dialog-Bmy_bApp.js} +1 -1
  15. package/dist/assets/index-BsDasSXm.css +1 -0
  16. package/dist/assets/index-y6creQ7S.js +2 -0
  17. package/dist/assets/{label-CXGuE6Oa.js → label-Btp6gGxV.js} +1 -1
  18. package/dist/assets/{page-layout-BVZlyPFt.js → page-layout-DWVKbt_g.js} +1 -1
  19. package/dist/assets/{switch-BLF45eI3.js → switch-AeXayTRS.js} +1 -1
  20. package/dist/assets/{tabs-custom-DQ0GpEV5.js → tabs-custom-DEZwNpXo.js} +1 -1
  21. package/dist/assets/useConfig-BiQH98MD.js +1 -0
  22. package/dist/assets/{useConfirmDialog-CK7KAyDf.js → useConfirmDialog-BY0hni-H.js} +1 -1
  23. package/dist/assets/{vendor-RXIbhDBC.js → vendor-H2M3a_4Z.js} +1 -1
  24. package/dist/index.html +3 -3
  25. package/package.json +1 -1
  26. package/src/App.tsx +2 -0
  27. package/src/api/config.ts +16 -0
  28. package/src/api/types.ts +52 -0
  29. package/src/components/config/ChannelForm.tsx +9 -0
  30. package/src/components/config/SecretsConfig.tsx +469 -0
  31. package/src/components/layout/Sidebar.tsx +6 -1
  32. package/src/hooks/useConfig.ts +17 -0
  33. package/src/lib/i18n.ts +42 -0
  34. package/dist/assets/ChannelsList-TFFw4Cem.js +0 -1
  35. package/dist/assets/index-CrUDzcei.js +0 -2
  36. package/dist/assets/index-Zy7fAOe1.css +0 -1
  37. package/dist/assets/useConfig-vFQvF4kn.js +0 -1
@@ -0,0 +1,469 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import { useConfig, useUpdateSecrets } from '@/hooks/useConfig';
3
+ import { Button } from '@/components/ui/button';
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
5
+ import { Input } from '@/components/ui/input';
6
+ import { Label } from '@/components/ui/label';
7
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
8
+ import { Switch } from '@/components/ui/switch';
9
+ import { PageHeader, PageLayout } from '@/components/layout/page-layout';
10
+ import type { SecretProviderView, SecretRefView, SecretSourceView } from '@/api/types';
11
+ import { t } from '@/lib/i18n';
12
+ import { Plus, Save, Trash2 } from 'lucide-react';
13
+ import { toast } from 'sonner';
14
+
15
+ type ProviderRow = {
16
+ alias: string;
17
+ source: SecretSourceView;
18
+ prefix: string;
19
+ path: string;
20
+ command: string;
21
+ argsText: string;
22
+ cwd: string;
23
+ timeoutMs: number;
24
+ };
25
+
26
+ type RefRow = {
27
+ path: string;
28
+ source: SecretSourceView;
29
+ provider: string;
30
+ id: string;
31
+ };
32
+
33
+ const SOURCE_OPTIONS: SecretSourceView[] = ['env', 'file', 'exec'];
34
+
35
+ function createProviderRow(alias = ''): ProviderRow {
36
+ return {
37
+ alias,
38
+ source: 'env',
39
+ prefix: '',
40
+ path: '',
41
+ command: '',
42
+ argsText: '',
43
+ cwd: '',
44
+ timeoutMs: 5000
45
+ };
46
+ }
47
+
48
+ function createRefRow(): RefRow {
49
+ return {
50
+ path: '',
51
+ source: 'env',
52
+ provider: '',
53
+ id: ''
54
+ };
55
+ }
56
+
57
+ function providerToRow(alias: string, provider: SecretProviderView): ProviderRow {
58
+ if (provider.source === 'env') {
59
+ return {
60
+ ...createProviderRow(alias),
61
+ source: 'env',
62
+ prefix: provider.prefix ?? ''
63
+ };
64
+ }
65
+
66
+ if (provider.source === 'file') {
67
+ return {
68
+ ...createProviderRow(alias),
69
+ source: 'file',
70
+ path: provider.path
71
+ };
72
+ }
73
+
74
+ return {
75
+ ...createProviderRow(alias),
76
+ source: 'exec',
77
+ command: provider.command,
78
+ argsText: (provider.args ?? []).join('\n'),
79
+ cwd: provider.cwd ?? '',
80
+ timeoutMs: provider.timeoutMs ?? 5000
81
+ };
82
+ }
83
+
84
+ function rowToProvider(row: ProviderRow): SecretProviderView {
85
+ if (row.source === 'env') {
86
+ return {
87
+ source: 'env',
88
+ ...(row.prefix.trim() ? { prefix: row.prefix.trim() } : {})
89
+ };
90
+ }
91
+
92
+ if (row.source === 'file') {
93
+ return {
94
+ source: 'file',
95
+ path: row.path.trim(),
96
+ format: 'json'
97
+ };
98
+ }
99
+
100
+ return {
101
+ source: 'exec',
102
+ command: row.command.trim(),
103
+ args: row.argsText
104
+ .split('\n')
105
+ .map((item) => item.trim())
106
+ .filter(Boolean),
107
+ ...(row.cwd.trim() ? { cwd: row.cwd.trim() } : {}),
108
+ timeoutMs: Math.max(1, Math.trunc(row.timeoutMs || 5000))
109
+ };
110
+ }
111
+
112
+ export function SecretsConfig() {
113
+ const { data: config, isLoading } = useConfig();
114
+ const updateSecrets = useUpdateSecrets();
115
+
116
+ const [enabled, setEnabled] = useState(true);
117
+ const [defaultEnv, setDefaultEnv] = useState('');
118
+ const [defaultFile, setDefaultFile] = useState('');
119
+ const [defaultExec, setDefaultExec] = useState('');
120
+ const [providers, setProviders] = useState<ProviderRow[]>([]);
121
+ const [refs, setRefs] = useState<RefRow[]>([]);
122
+
123
+ useEffect(() => {
124
+ const secrets = config?.secrets;
125
+ if (!secrets) {
126
+ setEnabled(true);
127
+ setDefaultEnv('');
128
+ setDefaultFile('');
129
+ setDefaultExec('');
130
+ setProviders([]);
131
+ setRefs([]);
132
+ return;
133
+ }
134
+
135
+ setEnabled(Boolean(secrets.enabled));
136
+ setDefaultEnv(secrets.defaults.env ?? '');
137
+ setDefaultFile(secrets.defaults.file ?? '');
138
+ setDefaultExec(secrets.defaults.exec ?? '');
139
+
140
+ const nextProviders = Object.entries(secrets.providers).map(([alias, provider]) =>
141
+ providerToRow(alias, provider)
142
+ );
143
+ const nextRefs = Object.entries(secrets.refs).map(([path, ref]) => ({
144
+ path,
145
+ source: ref.source,
146
+ provider: ref.provider ?? '',
147
+ id: ref.id
148
+ }));
149
+
150
+ setProviders(nextProviders);
151
+ setRefs(nextRefs);
152
+ }, [config?.secrets]);
153
+
154
+ const providerAliases = useMemo(() => {
155
+ const aliases = providers
156
+ .map((item) => item.alias.trim())
157
+ .filter(Boolean);
158
+ return Array.from(new Set(aliases));
159
+ }, [providers]);
160
+
161
+ const updateProvider = (index: number, patch: Partial<ProviderRow>) => {
162
+ setProviders((prev) => prev.map((entry, cursor) => (cursor === index ? { ...entry, ...patch } : entry)));
163
+ };
164
+
165
+ const updateRef = (index: number, patch: Partial<RefRow>) => {
166
+ setRefs((prev) => prev.map((entry, cursor) => (cursor === index ? { ...entry, ...patch } : entry)));
167
+ };
168
+
169
+ const handleSave = () => {
170
+ try {
171
+ const providerMap: Record<string, SecretProviderView> = {};
172
+ for (const [index, row] of providers.entries()) {
173
+ const alias = row.alias.trim();
174
+ if (!alias) {
175
+ throw new Error(`${t('providerAlias')} #${index + 1} ${t('isRequired')}`);
176
+ }
177
+
178
+ if (providerMap[alias]) {
179
+ throw new Error(`${t('providerAlias')}: ${alias} (${t('duplicate')})`);
180
+ }
181
+
182
+ if (row.source === 'file' && !row.path.trim()) {
183
+ throw new Error(`${t('secretFilePath')} #${index + 1} ${t('isRequired')}`);
184
+ }
185
+
186
+ if (row.source === 'exec' && !row.command.trim()) {
187
+ throw new Error(`${t('secretExecCommand')} #${index + 1} ${t('isRequired')}`);
188
+ }
189
+
190
+ providerMap[alias] = rowToProvider(row);
191
+ }
192
+
193
+ const refMap: Record<string, SecretRefView> = {};
194
+ for (const [index, row] of refs.entries()) {
195
+ const path = row.path.trim();
196
+ const id = row.id.trim();
197
+ if (!path) {
198
+ throw new Error(`${t('secretConfigPath')} #${index + 1} ${t('isRequired')}`);
199
+ }
200
+ if (!id) {
201
+ throw new Error(`${t('secretId')} #${index + 1} ${t('isRequired')}`);
202
+ }
203
+
204
+ const provider = row.provider.trim();
205
+ if (provider && !providerMap[provider]) {
206
+ throw new Error(`${t('secretProviderAlias')}: ${provider} ${t('notFound')}`);
207
+ }
208
+
209
+ refMap[path] = {
210
+ source: row.source,
211
+ ...(provider ? { provider } : {}),
212
+ id
213
+ };
214
+ }
215
+
216
+ updateSecrets.mutate({
217
+ data: {
218
+ enabled,
219
+ defaults: {
220
+ env: defaultEnv.trim() || null,
221
+ file: defaultFile.trim() || null,
222
+ exec: defaultExec.trim() || null
223
+ },
224
+ providers: providerMap,
225
+ refs: refMap
226
+ }
227
+ });
228
+ } catch (error) {
229
+ const message = error instanceof Error ? error.message : String(error);
230
+ toast.error(message);
231
+ }
232
+ };
233
+
234
+ if (isLoading) {
235
+ return <div className="p-8 text-gray-400">{t('loading')}</div>;
236
+ }
237
+
238
+ return (
239
+ <PageLayout className="space-y-6">
240
+ <PageHeader title={t('secretsPageTitle')} description={t('secretsPageDescription')} />
241
+
242
+ <Card>
243
+ <CardHeader>
244
+ <CardTitle>{t('secrets')}</CardTitle>
245
+ <CardDescription>{t('secretsEnabledHelp')}</CardDescription>
246
+ </CardHeader>
247
+ <CardContent className="space-y-4">
248
+ <div className="flex items-center justify-between rounded-xl border border-gray-200 p-3">
249
+ <div>
250
+ <p className="text-sm font-medium text-gray-800">{t('enabled')}</p>
251
+ <p className="text-xs text-gray-500">{t('secretsEnabledHelp')}</p>
252
+ </div>
253
+ <Switch checked={enabled} onCheckedChange={setEnabled} />
254
+ </div>
255
+
256
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
257
+ <div className="space-y-2">
258
+ <Label>{t('defaultEnvProvider')}</Label>
259
+ <Select value={defaultEnv || '__none__'} onValueChange={(value) => setDefaultEnv(value === '__none__' ? '' : value)}>
260
+ <SelectTrigger>
261
+ <SelectValue placeholder={t('noneOption')} />
262
+ </SelectTrigger>
263
+ <SelectContent>
264
+ <SelectItem value="__none__">{t('noneOption')}</SelectItem>
265
+ {providerAliases.map((alias) => (
266
+ <SelectItem key={alias} value={alias}>
267
+ {alias}
268
+ </SelectItem>
269
+ ))}
270
+ </SelectContent>
271
+ </Select>
272
+ </div>
273
+
274
+ <div className="space-y-2">
275
+ <Label>{t('defaultFileProvider')}</Label>
276
+ <Select value={defaultFile || '__none__'} onValueChange={(value) => setDefaultFile(value === '__none__' ? '' : value)}>
277
+ <SelectTrigger>
278
+ <SelectValue placeholder={t('noneOption')} />
279
+ </SelectTrigger>
280
+ <SelectContent>
281
+ <SelectItem value="__none__">{t('noneOption')}</SelectItem>
282
+ {providerAliases.map((alias) => (
283
+ <SelectItem key={alias} value={alias}>
284
+ {alias}
285
+ </SelectItem>
286
+ ))}
287
+ </SelectContent>
288
+ </Select>
289
+ </div>
290
+
291
+ <div className="space-y-2">
292
+ <Label>{t('defaultExecProvider')}</Label>
293
+ <Select value={defaultExec || '__none__'} onValueChange={(value) => setDefaultExec(value === '__none__' ? '' : value)}>
294
+ <SelectTrigger>
295
+ <SelectValue placeholder={t('noneOption')} />
296
+ </SelectTrigger>
297
+ <SelectContent>
298
+ <SelectItem value="__none__">{t('noneOption')}</SelectItem>
299
+ {providerAliases.map((alias) => (
300
+ <SelectItem key={alias} value={alias}>
301
+ {alias}
302
+ </SelectItem>
303
+ ))}
304
+ </SelectContent>
305
+ </Select>
306
+ </div>
307
+ </div>
308
+ </CardContent>
309
+ </Card>
310
+
311
+ <Card>
312
+ <CardHeader>
313
+ <CardTitle>{t('secretProvidersTitle')}</CardTitle>
314
+ <CardDescription>{t('secretProvidersDescription')}</CardDescription>
315
+ </CardHeader>
316
+ <CardContent className="space-y-3">
317
+ {providers.map((provider, index) => (
318
+ <div key={`provider-${index}`} className="rounded-xl border border-gray-200 p-3 space-y-3">
319
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-3">
320
+ <Input
321
+ value={provider.alias}
322
+ onChange={(event) => updateProvider(index, { alias: event.target.value })}
323
+ placeholder={t('providerAlias')}
324
+ />
325
+ <Select value={provider.source} onValueChange={(value) => updateProvider(index, { source: value as SecretSourceView })}>
326
+ <SelectTrigger>
327
+ <SelectValue />
328
+ </SelectTrigger>
329
+ <SelectContent>
330
+ {SOURCE_OPTIONS.map((source) => (
331
+ <SelectItem key={source} value={source}>
332
+ {source}
333
+ </SelectItem>
334
+ ))}
335
+ </SelectContent>
336
+ </Select>
337
+ <Button type="button" variant="outline" onClick={() => setProviders((prev) => prev.filter((_, i) => i !== index))}>
338
+ <Trash2 className="h-4 w-4 mr-2" />
339
+ {t('removeProvider')}
340
+ </Button>
341
+ </div>
342
+
343
+ {provider.source === 'env' && (
344
+ <Input
345
+ value={provider.prefix}
346
+ onChange={(event) => updateProvider(index, { prefix: event.target.value })}
347
+ placeholder={t('envPrefix')}
348
+ />
349
+ )}
350
+
351
+ {provider.source === 'file' && (
352
+ <Input
353
+ value={provider.path}
354
+ onChange={(event) => updateProvider(index, { path: event.target.value })}
355
+ placeholder={t('secretFilePath')}
356
+ />
357
+ )}
358
+
359
+ {provider.source === 'exec' && (
360
+ <div className="space-y-2">
361
+ <Input
362
+ value={provider.command}
363
+ onChange={(event) => updateProvider(index, { command: event.target.value })}
364
+ placeholder={t('secretExecCommand')}
365
+ />
366
+ <textarea
367
+ className="min-h-[84px] w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-mono"
368
+ value={provider.argsText}
369
+ onChange={(event) => updateProvider(index, { argsText: event.target.value })}
370
+ placeholder={t('secretExecArgs')}
371
+ />
372
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-2">
373
+ <Input
374
+ value={provider.cwd}
375
+ onChange={(event) => updateProvider(index, { cwd: event.target.value })}
376
+ placeholder={t('secretExecCwd')}
377
+ />
378
+ <Input
379
+ type="number"
380
+ min={1}
381
+ value={provider.timeoutMs}
382
+ onChange={(event) => updateProvider(index, { timeoutMs: Number.parseInt(event.target.value, 10) || 5000 })}
383
+ placeholder={t('secretExecTimeoutMs')}
384
+ />
385
+ </div>
386
+ </div>
387
+ )}
388
+ </div>
389
+ ))}
390
+
391
+ <Button type="button" variant="outline" onClick={() => setProviders((prev) => [...prev, createProviderRow()])}>
392
+ <Plus className="h-4 w-4 mr-2" />
393
+ {t('addSecretProvider')}
394
+ </Button>
395
+ </CardContent>
396
+ </Card>
397
+
398
+ <Card>
399
+ <CardHeader>
400
+ <CardTitle>{t('secretRefsTitle')}</CardTitle>
401
+ <CardDescription>{t('secretRefsDescription')}</CardDescription>
402
+ </CardHeader>
403
+ <CardContent className="space-y-3">
404
+ {refs.map((ref, index) => (
405
+ <div key={`ref-${index}`} className="rounded-xl border border-gray-200 p-3 space-y-3">
406
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
407
+ <Input
408
+ value={ref.path}
409
+ onChange={(event) => updateRef(index, { path: event.target.value })}
410
+ placeholder={t('secretConfigPath')}
411
+ />
412
+ <Input
413
+ value={ref.id}
414
+ onChange={(event) => updateRef(index, { id: event.target.value })}
415
+ placeholder={t('secretId')}
416
+ />
417
+ <Select value={ref.source} onValueChange={(value) => updateRef(index, { source: value as SecretSourceView })}>
418
+ <SelectTrigger>
419
+ <SelectValue />
420
+ </SelectTrigger>
421
+ <SelectContent>
422
+ {SOURCE_OPTIONS.map((source) => (
423
+ <SelectItem key={source} value={source}>
424
+ {source}
425
+ </SelectItem>
426
+ ))}
427
+ </SelectContent>
428
+ </Select>
429
+ <div className="grid grid-cols-[1fr_auto] gap-2">
430
+ <Select
431
+ value={ref.provider || '__none__'}
432
+ onValueChange={(value) => updateRef(index, { provider: value === '__none__' ? '' : value })}
433
+ >
434
+ <SelectTrigger>
435
+ <SelectValue placeholder={t('secretProviderAlias')} />
436
+ </SelectTrigger>
437
+ <SelectContent>
438
+ <SelectItem value="__none__">{t('noneOption')}</SelectItem>
439
+ {providerAliases.map((alias) => (
440
+ <SelectItem key={alias} value={alias}>
441
+ {alias}
442
+ </SelectItem>
443
+ ))}
444
+ </SelectContent>
445
+ </Select>
446
+ <Button type="button" variant="outline" onClick={() => setRefs((prev) => prev.filter((_, i) => i !== index))}>
447
+ <Trash2 className="h-4 w-4" />
448
+ </Button>
449
+ </div>
450
+ </div>
451
+ </div>
452
+ ))}
453
+
454
+ <Button type="button" variant="outline" onClick={() => setRefs((prev) => [...prev, createRefRow()])}>
455
+ <Plus className="h-4 w-4 mr-2" />
456
+ {t('addSecretRef')}
457
+ </Button>
458
+ </CardContent>
459
+ </Card>
460
+
461
+ <div className="flex justify-end">
462
+ <Button type="button" onClick={handleSave} disabled={updateSecrets.isPending}>
463
+ <Save className="h-4 w-4 mr-2" />
464
+ {updateSecrets.isPending ? t('saving') : t('save')}
465
+ </Button>
466
+ </div>
467
+ </PageLayout>
468
+ );
469
+ }
@@ -1,7 +1,7 @@
1
1
  import { cn } from '@/lib/utils';
2
2
  import { LANGUAGE_OPTIONS, t, type I18nLanguage } from '@/lib/i18n';
3
3
  import { THEME_OPTIONS, type UiTheme } from '@/lib/theme';
4
- import { Cpu, GitBranch, History, MessageCircle, MessageSquare, Sparkles, BookOpen, Plug, BrainCircuit, AlarmClock, Languages, Palette } from 'lucide-react';
4
+ import { Cpu, GitBranch, History, MessageCircle, MessageSquare, Sparkles, BookOpen, Plug, BrainCircuit, AlarmClock, Languages, Palette, KeyRound } from 'lucide-react';
5
5
  import { NavLink } from 'react-router-dom';
6
6
  import { useDocBrowser } from '@/components/doc-browser';
7
7
  import { useI18n } from '@/components/providers/I18nProvider';
@@ -66,6 +66,11 @@ export function Sidebar() {
66
66
  label: t('cron'),
67
67
  icon: AlarmClock,
68
68
  },
69
+ {
70
+ target: '/secrets',
71
+ label: t('secrets'),
72
+ icon: KeyRound,
73
+ },
69
74
  {
70
75
  target: '/marketplace/plugins',
71
76
  label: t('marketplaceFilterPlugins'),
@@ -7,6 +7,7 @@ import {
7
7
  updateProvider,
8
8
  updateChannel,
9
9
  updateRuntime,
10
+ updateSecrets,
10
11
  executeConfigAction,
11
12
  fetchSessions,
12
13
  fetchSessionHistory,
@@ -109,6 +110,22 @@ export function useUpdateRuntime() {
109
110
  });
110
111
  }
111
112
 
113
+ export function useUpdateSecrets() {
114
+ const queryClient = useQueryClient();
115
+
116
+ return useMutation({
117
+ mutationFn: ({ data }: { data: unknown }) =>
118
+ updateSecrets(data as Parameters<typeof updateSecrets>[0]),
119
+ onSuccess: () => {
120
+ queryClient.invalidateQueries({ queryKey: ['config'] });
121
+ toast.success(t('configSavedApplied'));
122
+ },
123
+ onError: (error: Error) => {
124
+ toast.error(t('configSaveFailed') + ': ' + error.message);
125
+ }
126
+ });
127
+ }
128
+
112
129
  export function useExecuteConfigAction() {
113
130
  return useMutation({
114
131
  mutationFn: ({ actionId, data }: { actionId: string; data: unknown }) =>
package/src/lib/i18n.ts CHANGED
@@ -128,6 +128,7 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
128
128
  providers: { zh: '提供商', en: 'Providers' },
129
129
  channels: { zh: '渠道', en: 'Channels' },
130
130
  cron: { zh: '定时任务', en: 'Cron Jobs' },
131
+ secrets: { zh: '密钥管理', en: 'Secrets' },
131
132
  runtime: { zh: '路由与运行时', en: 'Routing & Runtime' },
132
133
  marketplace: { zh: '市场', en: 'Marketplace' },
133
134
 
@@ -149,10 +150,14 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
149
150
  all: { zh: '全部', en: 'All' },
150
151
  prev: { zh: '上一页', en: 'Prev' },
151
152
  next: { zh: '下一页', en: 'Next' },
153
+ noneOption: { zh: '无', en: 'None' },
152
154
  language: { zh: '语言', en: 'Language' },
153
155
  theme: { zh: '主题', en: 'Theme' },
154
156
  themeWarm: { zh: '暖色', en: 'Warm' },
155
157
  themeCool: { zh: '冷色', en: 'Cool' },
158
+ isRequired: { zh: '必填', en: 'is required' },
159
+ duplicate: { zh: '重复', en: 'duplicate' },
160
+ notFound: { zh: '未找到', en: 'not found' },
156
161
 
157
162
  // Model
158
163
  modelPageTitle: { zh: '模型配置', en: 'Model Configuration' },
@@ -315,6 +320,43 @@ export const LABELS: Record<string, { zh: string; en: string }> = {
315
320
  addBinding: { zh: '添加绑定', en: 'Add Binding' },
316
321
  saveRuntimeSettings: { zh: '保存运行时设置', en: 'Save Runtime Settings' },
317
322
 
323
+ // Secrets
324
+ secretsPageTitle: { zh: '密钥管理', en: 'Secrets Management' },
325
+ secretsPageDescription: {
326
+ zh: '集中管理 secrets.providers、secrets.defaults 与 secrets.refs。',
327
+ en: 'Manage secrets.providers, secrets.defaults, and secrets.refs in one place.'
328
+ },
329
+ secretsEnabledHelp: {
330
+ zh: '关闭后不会解析 `{{secret:*}}` 引用。',
331
+ en: 'When disabled, `{{secret:*}}` refs are not resolved.'
332
+ },
333
+ defaultEnvProvider: { zh: '默认 Env 提供器', en: 'Default Env Provider' },
334
+ defaultFileProvider: { zh: '默认 File 提供器', en: 'Default File Provider' },
335
+ defaultExecProvider: { zh: '默认 Exec 提供器', en: 'Default Exec Provider' },
336
+ secretProvidersTitle: { zh: 'Secret Providers', en: 'Secret Providers' },
337
+ secretProvidersDescription: {
338
+ zh: '定义可复用的 secrets provider(env/file/exec)。',
339
+ en: 'Define reusable secret providers (env/file/exec).'
340
+ },
341
+ providerAlias: { zh: '提供器别名', en: 'Provider Alias' },
342
+ removeProvider: { zh: '移除提供器', en: 'Remove Provider' },
343
+ envPrefix: { zh: '环境变量前缀', en: 'Environment Prefix' },
344
+ secretFilePath: { zh: 'Secrets 文件路径', en: 'Secrets File Path' },
345
+ secretExecCommand: { zh: '执行命令', en: 'Exec Command' },
346
+ secretExecArgs: { zh: '命令参数(每行一个)', en: 'Exec Args (one per line)' },
347
+ secretExecCwd: { zh: '执行目录(可选)', en: 'Exec Working Directory (optional)' },
348
+ secretExecTimeoutMs: { zh: '超时(毫秒)', en: 'Timeout (ms)' },
349
+ addSecretProvider: { zh: '添加 Provider', en: 'Add Provider' },
350
+ secretRefsTitle: { zh: 'Secret Refs', en: 'Secret Refs' },
351
+ secretRefsDescription: {
352
+ zh: '把配置路径映射到 secret 引用(source/provider/id)。',
353
+ en: 'Map config paths to secret refs (source/provider/id).'
354
+ },
355
+ secretConfigPath: { zh: '配置路径', en: 'Config Path' },
356
+ secretId: { zh: 'Secret ID', en: 'Secret ID' },
357
+ secretProviderAlias: { zh: 'Provider 别名', en: 'Provider Alias' },
358
+ addSecretRef: { zh: '添加 Ref', en: 'Add Ref' },
359
+
318
360
  // Sessions
319
361
  sessionsPageTitle: { zh: '会话管理', en: 'Sessions' },
320
362
  sessionsPageDescription: {
@@ -1 +0,0 @@
1
- import{r as v,j as a,ac as ae,M as S,ad as M,ae as N,aa as te,_ as ne,ab as le,af as se,ag as oe,n as H,ah as re,ai as ce,aj as ie}from"./vendor-RXIbhDBC.js";import{u as _,a as q,i as me,j as pe,I as D,g as de}from"./useConfig-vFQvF4kn.js";import{t as e,c as G,u as J,S as be,b as ue,d as ge,e as ye,g as xe}from"./index-CrUDzcei.js";import{D as he,a as fe,b as we,c as je,d as ve,e as ke}from"./dialog-DJs630RE.js";import{B as F,P as Ce,a as Pe}from"./page-layout-BVZlyPFt.js";import{L as Ne}from"./label-CXGuE6Oa.js";import{S as Se}from"./switch-BLF45eI3.js";import{h as T}from"./config-hints-CApS3K_7.js";import{T as Ie}from"./tabs-custom-DQ0GpEV5.js";import{C as De,a as Fe,L as Te,d as Me,S as Ae,b as Le,c as Ue,A as Ee}from"./action-link-C9xMkxl2.js";function Be({value:n,onChange:d,className:i,placeholder:m=""}){const[c,y]=v.useState(""),b=u=>{u.key==="Enter"&&c.trim()?(u.preventDefault(),d([...n,c.trim()]),y("")):u.key==="Backspace"&&!c&&n.length>0&&d(n.slice(0,-1))},f=u=>{d(n.filter((o,g)=>g!==u))};return a.jsxs("div",{className:G("flex flex-wrap gap-2 p-2 border rounded-md min-h-[42px]",i),children:[n.map((u,o)=>a.jsxs("span",{className:"inline-flex items-center gap-1 px-2 py-1 bg-primary text-primary-foreground rounded text-sm",children:[u,a.jsx("button",{type:"button",onClick:()=>f(o),className:"hover:text-red-300 transition-colors",children:a.jsx(ae,{className:"h-3 w-3"})})]},o)),a.jsx("input",{type:"text",value:c,onChange:u=>y(u.target.value),onKeyDown:b,className:"flex-1 outline-none min-w-[100px] bg-transparent text-sm",placeholder:m||e("enterTag")})]})}const U=[{value:"pairing",label:"pairing"},{value:"allowlist",label:"allowlist"},{value:"open",label:"open"},{value:"disabled",label:"disabled"}],E=[{value:"open",label:"open"},{value:"allowlist",label:"allowlist"},{value:"disabled",label:"disabled"}],Oe=[{value:"off",label:"off"},{value:"partial",label:"partial"},{value:"block",label:"block"},{value:"progress",label:"progress"}],$e=n=>n.includes("token")||n.includes("secret")||n.includes("password")?a.jsx(te,{className:"h-3.5 w-3.5 text-gray-500"}):n.includes("url")||n.includes("host")?a.jsx(ne,{className:"h-3.5 w-3.5 text-gray-500"}):n.includes("email")||n.includes("mail")?a.jsx(M,{className:"h-3.5 w-3.5 text-gray-500"}):n.includes("id")||n.includes("from")?a.jsx(le,{className:"h-3.5 w-3.5 text-gray-500"}):n==="enabled"||n==="consentGranted"?a.jsx(se,{className:"h-3.5 w-3.5 text-gray-500"}):a.jsx(oe,{className:"h-3.5 w-3.5 text-gray-500"});function B(){return{telegram:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"token",type:"password",label:e("botToken")},{name:"allowFrom",type:"tags",label:e("allowFrom")},{name:"proxy",type:"text",label:e("proxy")},{name:"accountId",type:"text",label:e("accountId")},{name:"dmPolicy",type:"select",label:e("dmPolicy"),options:U},{name:"groupPolicy",type:"select",label:e("groupPolicy"),options:E},{name:"groupAllowFrom",type:"tags",label:e("groupAllowFrom")},{name:"requireMention",type:"boolean",label:e("requireMention")},{name:"mentionPatterns",type:"tags",label:e("mentionPatterns")},{name:"groups",type:"json",label:e("groupRulesJson")}],discord:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"token",type:"password",label:e("botToken")},{name:"allowBots",type:"boolean",label:e("allowBotMessages")},{name:"allowFrom",type:"tags",label:e("allowFrom")},{name:"gatewayUrl",type:"text",label:e("gatewayUrl")},{name:"intents",type:"number",label:e("intents")},{name:"proxy",type:"text",label:e("proxy")},{name:"mediaMaxMb",type:"number",label:e("attachmentMaxSizeMb")},{name:"streaming",type:"select",label:e("streamingMode"),options:Oe},{name:"draftChunk",type:"json",label:e("draftChunkingJson")},{name:"textChunkLimit",type:"number",label:e("textChunkLimit")},{name:"accountId",type:"text",label:e("accountId")},{name:"dmPolicy",type:"select",label:e("dmPolicy"),options:U},{name:"groupPolicy",type:"select",label:e("groupPolicy"),options:E},{name:"groupAllowFrom",type:"tags",label:e("groupAllowFrom")},{name:"requireMention",type:"boolean",label:e("requireMention")},{name:"mentionPatterns",type:"tags",label:e("mentionPatterns")},{name:"groups",type:"json",label:e("groupRulesJson")}],whatsapp:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"bridgeUrl",type:"text",label:e("bridgeUrl")},{name:"allowFrom",type:"tags",label:e("allowFrom")}],feishu:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"appId",type:"text",label:e("appId")},{name:"appSecret",type:"password",label:e("appSecret")},{name:"encryptKey",type:"password",label:e("encryptKey")},{name:"verificationToken",type:"password",label:e("verificationToken")},{name:"allowFrom",type:"tags",label:e("allowFrom")}],dingtalk:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"clientId",type:"text",label:e("clientId")},{name:"clientSecret",type:"password",label:e("clientSecret")},{name:"allowFrom",type:"tags",label:e("allowFrom")}],wecom:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"corpId",type:"text",label:e("corpId")},{name:"agentId",type:"text",label:e("agentId")},{name:"secret",type:"password",label:e("secret")},{name:"token",type:"password",label:e("token")},{name:"callbackPort",type:"number",label:e("callbackPort")},{name:"callbackPath",type:"text",label:e("callbackPath")},{name:"allowFrom",type:"tags",label:e("allowFrom")}],slack:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"mode",type:"text",label:e("mode")},{name:"webhookPath",type:"text",label:e("webhookPath")},{name:"allowBots",type:"boolean",label:e("allowBotMessages")},{name:"botToken",type:"password",label:e("botToken")},{name:"appToken",type:"password",label:e("appToken")}],email:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"consentGranted",type:"boolean",label:e("consentGranted")},{name:"imapHost",type:"text",label:e("imapHost")},{name:"imapPort",type:"number",label:e("imapPort")},{name:"imapUsername",type:"text",label:e("imapUsername")},{name:"imapPassword",type:"password",label:e("imapPassword")},{name:"fromAddress",type:"email",label:e("fromAddress")}],mochat:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"baseUrl",type:"text",label:e("baseUrl")},{name:"clawToken",type:"password",label:e("clawToken")},{name:"agentUserId",type:"text",label:e("agentUserId")},{name:"allowFrom",type:"tags",label:e("allowFrom")}],qq:[{name:"enabled",type:"boolean",label:e("enabled")},{name:"appId",type:"text",label:e("appId")},{name:"secret",type:"password",label:e("appSecret")},{name:"markdownSupport",type:"boolean",label:e("markdownSupport")},{name:"allowFrom",type:"tags",label:e("allowFrom")}]}}const O={telegram:S,slack:S,email:M,default:S},$={telegram:"from-primary-300 to-primary-600",slack:"from-primary-200 to-primary-500",email:"from-primary-100 to-primary-400",default:"from-gray-300 to-gray-500"};function I(n){return typeof n=="object"&&n!==null&&!Array.isArray(n)}function K(n,d){const i={...n};for(const[m,c]of Object.entries(d)){const y=i[m];if(I(y)&&I(c)){i[m]=K(y,c);continue}i[m]=c}return i}function Re(n,d){const i=n.split("."),m={};let c=m;for(let y=0;y<i.length-1;y+=1){const b=i[y];c[b]={},c=c[b]}return c[i[i.length-1]]=d,m}function He(){var A,L;const{channelModal:n,closeChannelModal:d}=J(),{data:i}=_(),{data:m}=q(),c=me(),y=pe(),[b,f]=v.useState({}),[u,o]=v.useState({}),[g,x]=v.useState(null),l=n.channel,w=l?i==null?void 0:i.channels[l]:null,k=l?B()[l]??[]:[],C=m==null?void 0:m.uiHints,P=l?`channels.${l}`:null,z=((A=m==null?void 0:m.actions)==null?void 0:A.filter(t=>t.scope===P))??[],V=l&&(((L=T(`channels.${l}`,C))==null?void 0:L.label)??l);v.useEffect(()=>{if(w){f({...w});const t={};(l?B()[l]??[]:[]).filter(r=>r.type==="json").forEach(r=>{const h=w[r.name];t[r.name]=JSON.stringify(h??{},null,2)}),o(t)}else f({}),o({})},[w,l]);const j=(t,s)=>{f(r=>({...r,[t]:s}))},Y=t=>{if(t.preventDefault(),!l)return;const s={...b};for(const r of k){if(r.type!=="json")continue;const h=u[r.name]??"";try{s[r.name]=h.trim()?JSON.parse(h):{}}catch{N.error(`${e("invalidJson")}: ${r.name}`);return}}c.mutate({channel:l,data:s},{onSuccess:()=>d()})},W=t=>{if(!t||!l)return;const s=t.channels;if(!I(s))return;const r=s[l];I(r)&&f(h=>K(h,r))},X=async t=>{if(!(!l||!P)){x(t.id);try{let s={...b};t.saveBeforeRun&&(s={...s,...t.savePatch??{}},f(s),await c.mutateAsync({channel:l,data:s}));const r=await y.mutateAsync({actionId:t.id,data:{scope:P,draftConfig:Re(P,s)}});W(r.patch),r.ok?N.success(r.message||e("success")):N.error(r.message||e("error"))}catch(s){const r=s instanceof Error?s.message:String(s);N.error(`${e("error")}: ${r}`)}finally{x(null)}}},Q=O[l||""]||O.default,Z=$[l||""]||$.default;return a.jsx(he,{open:n.open,onOpenChange:d,children:a.jsxs(fe,{className:"sm:max-w-[550px] max-h-[85vh] overflow-hidden flex flex-col",children:[a.jsx(we,{children:a.jsxs("div",{className:"flex items-center gap-3",children:[a.jsx("div",{className:`h-10 w-10 rounded-xl bg-gradient-to-br ${Z} flex items-center justify-center`,children:a.jsx(Q,{className:"h-5 w-5 text-white"})}),a.jsxs("div",{children:[a.jsx(je,{className:"capitalize",children:V}),a.jsx(ve,{children:e("configureMessageChannelParameters")})]})]})}),a.jsxs("form",{onSubmit:Y,className:"flex flex-col flex-1 overflow-hidden",children:[a.jsx("div",{className:"flex-1 overflow-y-auto custom-scrollbar py-2 pr-2 space-y-5",children:k.map(t=>{const s=l?T(`channels.${l}.${t.name}`,C):void 0,r=(s==null?void 0:s.label)??t.label,h=s==null?void 0:s.placeholder;return a.jsxs("div",{className:"space-y-2.5",children:[a.jsxs(Ne,{htmlFor:t.name,className:"text-sm font-medium text-gray-900 flex items-center gap-2",children:[$e(t.name),r]}),t.type==="boolean"&&a.jsxs("div",{className:"flex items-center justify-between p-3 rounded-xl bg-gray-50",children:[a.jsx("span",{className:"text-sm text-gray-500",children:b[t.name]?e("enabled"):e("disabled")}),a.jsx(Se,{id:t.name,checked:b[t.name]||!1,onCheckedChange:p=>j(t.name,p),className:"data-[state=checked]:bg-emerald-500"})]}),(t.type==="text"||t.type==="email")&&a.jsx(D,{id:t.name,type:t.type,value:b[t.name]||"",onChange:p=>j(t.name,p.target.value),placeholder:h,className:"rounded-xl"}),t.type==="password"&&a.jsx(D,{id:t.name,type:"password",value:b[t.name]||"",onChange:p=>j(t.name,p.target.value),placeholder:h??e("leaveBlankToKeepUnchanged"),className:"rounded-xl"}),t.type==="number"&&a.jsx(D,{id:t.name,type:"number",value:b[t.name]||0,onChange:p=>j(t.name,parseInt(p.target.value)||0),placeholder:h,className:"rounded-xl"}),t.type==="tags"&&a.jsx(Be,{value:b[t.name]||[],onChange:p=>j(t.name,p)}),t.type==="select"&&a.jsxs(be,{value:b[t.name]||"",onValueChange:p=>j(t.name,p),children:[a.jsx(ue,{className:"rounded-xl",children:a.jsx(ge,{})}),a.jsx(ye,{children:(t.options??[]).map(p=>a.jsx(xe,{value:p.value,children:p.label},p.value))})]}),t.type==="json"&&a.jsx("textarea",{id:t.name,value:u[t.name]??"{}",onChange:p=>o(ee=>({...ee,[t.name]:p.target.value})),className:"min-h-[120px] w-full rounded-lg border border-gray-200 bg-white px-3 py-2 text-xs font-mono"})]},t.name)})}),a.jsxs(ke,{className:"pt-4 flex-shrink-0",children:[a.jsx(F,{type:"button",variant:"outline",onClick:d,children:e("cancel")}),a.jsx(F,{type:"submit",disabled:c.isPending||!!g,children:c.isPending?e("saving"):e("save")}),z.filter(t=>t.trigger==="manual").map(t=>a.jsx(F,{type:"button",onClick:()=>X(t),disabled:c.isPending||!!g,variant:"secondary",children:g===t.id?e("connecting"):t.title},t.id))]})]})]})})}const R={telegram:S,slack:ce,email:M,webhook:re,default:H},_e={telegram:"channelDescTelegram",slack:"channelDescSlack",email:"channelDescEmail",webhook:"channelDescWebhook",discord:"channelDescDiscord",feishu:"channelDescFeishu"};function Ze(){const{data:n}=_(),{data:d}=de(),{data:i}=q(),{openChannelModal:m}=J(),[c,y]=v.useState("active"),b=i==null?void 0:i.uiHints;if(!n||!d)return a.jsx("div",{className:"p-8 text-gray-400",children:e("channelsLoading")});const f=[{id:"active",label:e("channelsTabEnabled"),count:d.channels.filter(o=>{var g;return(g=n.channels[o.name])==null?void 0:g.enabled}).length},{id:"all",label:e("channelsTabAll"),count:d.channels.length}],u=d.channels.filter(o=>{var x;const g=((x=n.channels[o.name])==null?void 0:x.enabled)||!1;return c==="all"||g});return a.jsxs(Ce,{children:[a.jsx(Pe,{title:e("channelsPageTitle")}),a.jsx(Ie,{tabs:f,activeTab:c,onChange:y}),a.jsx("div",{className:"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4",children:u.map(o=>{const g=n.channels[o.name],x=(g==null?void 0:g.enabled)||!1,l=R[o.name]||R.default,w=T(`channels.${o.name}`,b),k=(w==null?void 0:w.help)||e(_e[o.name]||"channelDescriptionDefault");return a.jsxs(De,{onClick:()=>m(o.name),children:[a.jsxs(Fe,{children:[a.jsx(Te,{name:o.name,src:Me(o.name),className:G("h-11 w-11 rounded-xl border transition-all",x?"bg-white border-primary/30":"bg-white border-gray-200/60 group-hover:border-gray-300"),imgClassName:"h-5 w-5",fallback:a.jsx(l,{className:"h-5 w-5"})}),a.jsx(Ae,{status:x?"active":"inactive",label:x?e("statusActive"):e("statusInactive")})]}),a.jsx(Le,{title:o.displayName||o.name,description:k}),a.jsxs(Ue,{children:[a.jsx(Ee,{label:x?e("actionConfigure"):e("actionEnable")}),o.tutorialUrl&&a.jsx("a",{href:o.tutorialUrl,target:"_blank",rel:"noreferrer",onClick:C=>C.stopPropagation(),className:"flex items-center justify-center h-6 w-6 rounded-md text-gray-300 hover:text-gray-500 hover:bg-gray-100/60 transition-colors",title:e("channelsGuideTitle"),children:a.jsx(ie,{className:"h-3.5 w-3.5"})})]})]},o.name)})}),u.length===0&&a.jsxs("div",{className:"flex flex-col items-center justify-center py-16 text-center",children:[a.jsx("div",{className:"h-14 w-14 flex items-center justify-center rounded-xl bg-gray-100/80 mb-4",children:a.jsx(H,{className:"h-6 w-6 text-gray-300"})}),a.jsx("h3",{className:"text-[14px] font-semibold text-gray-900 mb-1.5",children:e("channelsEmptyTitle")}),a.jsx("p",{className:"text-[13px] text-gray-400 max-w-sm",children:e("channelsEmptyDescription")})]}),a.jsx(He,{})]})}export{Ze as ChannelsList};