mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-31 06:37:14 +00:00
Merge remote-tracking branch 'origin/master' into dev/4.11.x
# Conflicts: # src/langbot/pkg/pipeline/preproc/preproc.py # src/langbot/pkg/pipeline/process/handlers/chat.py # src/langbot/pkg/provider/runners/localagent.py # src/langbot/pkg/provider/tools/toolmgr.py # src/langbot/templates/metadata/pipeline/ai.yaml # tests/unit_tests/test_preproc.py # web/src/app/home/components/dynamic-form/DynamicFormComponent.tsx # web/src/app/home/pipelines/components/pipeline-form/PipelineFormComponent.tsx
This commit is contained in:
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
@@ -0,0 +1,199 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Trash2, Plus, ShieldCheck } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export interface BotAdmin {
|
||||
id: number;
|
||||
launcher_type: string;
|
||||
launcher_id: string;
|
||||
}
|
||||
|
||||
interface BotAdminsDialogProps {
|
||||
botId: string;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
admins: BotAdmin[];
|
||||
onAdminsChange: () => void;
|
||||
}
|
||||
|
||||
export default function BotAdminsDialog({
|
||||
botId,
|
||||
open,
|
||||
onOpenChange,
|
||||
admins,
|
||||
onAdminsChange,
|
||||
}: BotAdminsDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
const [newType, setNewType] = useState('person');
|
||||
const [newId, setNewId] = useState('');
|
||||
const [adding, setAdding] = useState(false);
|
||||
|
||||
async function handleAdd() {
|
||||
if (!newId.trim()) return;
|
||||
setAdding(true);
|
||||
try {
|
||||
await httpClient.addBotAdmin(botId, newType, newId.trim());
|
||||
toast.success(t('bots.admins.addSuccess'));
|
||||
setNewId('');
|
||||
onAdminsChange();
|
||||
} catch (e: unknown) {
|
||||
const err = e as { msg?: string; message?: string };
|
||||
toast.error(t('bots.admins.addError') + (err?.msg ?? err?.message ?? ''));
|
||||
} finally {
|
||||
setAdding(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
try {
|
||||
await httpClient.deleteBotAdmin(botId, id);
|
||||
toast.success(t('bots.admins.deleteSuccess'));
|
||||
onAdminsChange();
|
||||
} catch (e: unknown) {
|
||||
const err = e as { msg?: string; message?: string };
|
||||
toast.error(
|
||||
t('bots.admins.deleteError') + (err?.msg ?? err?.message ?? ''),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<ShieldCheck className="size-4" />
|
||||
{t('bots.admins.title')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>{t('bots.admins.description')}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Add row */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<Select value={newType} onValueChange={setNewType}>
|
||||
<SelectTrigger className="w-28 shrink-0">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="person">
|
||||
{t('bots.admins.typePerson')}
|
||||
</SelectItem>
|
||||
<SelectItem value="group">
|
||||
{t('bots.admins.typeGroup')}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
className="flex-1"
|
||||
placeholder={t('bots.admins.placeholderId')}
|
||||
value={newId}
|
||||
onChange={(e) => setNewId(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleAdd()}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleAdd}
|
||||
disabled={adding || !newId.trim()}
|
||||
>
|
||||
<Plus className="size-4 mr-1" />
|
||||
{t('bots.admins.addAdmin')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
{admins.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground py-6 text-center">
|
||||
{t('bots.admins.noAdmins')}
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="max-h-64">
|
||||
<div className="border rounded-md overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b bg-muted/40">
|
||||
<th className="text-left px-3 py-2 font-medium text-muted-foreground w-28">
|
||||
{t('bots.admins.launcherType')}
|
||||
</th>
|
||||
<th className="text-left px-3 py-2 font-medium text-muted-foreground">
|
||||
{t('bots.admins.launcherId')}
|
||||
</th>
|
||||
<th className="w-10" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{admins.map((admin) => (
|
||||
<tr
|
||||
key={admin.id}
|
||||
className="border-b last:border-0 hover:bg-muted/30"
|
||||
>
|
||||
<td className="px-3 py-2">
|
||||
<span className="px-1.5 py-0.5 rounded bg-muted text-xs">
|
||||
{admin.launcher_type === 'person'
|
||||
? t('bots.admins.typePerson')
|
||||
: t('bots.admins.typeGroup')}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-3 py-2 font-mono">
|
||||
{admin.launcher_id}
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<button
|
||||
type="button"
|
||||
className="text-muted-foreground hover:text-destructive transition-colors"
|
||||
onClick={() => handleDelete(admin.id)}
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
// Shared hook so the session monitor and the dialog stay in sync.
|
||||
export function useBotAdmins(botId: string) {
|
||||
const [admins, setAdmins] = useState<BotAdmin[]>([]);
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
try {
|
||||
const res = await httpClient.getBotAdmins(botId);
|
||||
setAdmins(res.admins ?? []);
|
||||
} catch (error) {
|
||||
console.error('Failed to load bot admins:', error);
|
||||
}
|
||||
}, [botId]);
|
||||
|
||||
useEffect(() => {
|
||||
reload();
|
||||
}, [reload]);
|
||||
|
||||
return { admins, reload };
|
||||
}
|
||||
@@ -728,7 +728,11 @@ export default function EventBindingsEditor({
|
||||
function toggleExpand(id: string) {
|
||||
setExpandedIds((prev) => {
|
||||
const s = new Set(prev);
|
||||
s.has(id) ? s.delete(id) : s.add(id);
|
||||
if (s.has(id)) {
|
||||
s.delete(id);
|
||||
} else {
|
||||
s.add(id);
|
||||
}
|
||||
return s;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,7 +18,14 @@ import {
|
||||
Workflow,
|
||||
ThumbsUp,
|
||||
ThumbsDown,
|
||||
ShieldCheck,
|
||||
ShieldOff,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import BotAdminsDialog, {
|
||||
useBotAdmins,
|
||||
} from '@/app/home/bots/components/bot-admins/BotAdminsDialog';
|
||||
import type { BotAdmin } from '@/app/home/bots/components/bot-admins/BotAdminsDialog';
|
||||
import { copyToClipboard } from '@/app/utils/clipboard';
|
||||
import {
|
||||
MessageChainComponent,
|
||||
@@ -94,15 +101,60 @@ const BotSessionMonitor = forwardRef<
|
||||
Record<string, SessionFeedback>
|
||||
>({});
|
||||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||
const { admins, reload: reloadAdmins } = useBotAdmins(botId);
|
||||
const [adminsDialogOpen, setAdminsDialogOpen] = useState(false);
|
||||
const [togglingAdmin, setTogglingAdmin] = useState<string | null>(null);
|
||||
|
||||
const parseSessionType = (sessionId: string): string | null => {
|
||||
const idx = sessionId.indexOf('_');
|
||||
if (idx === -1) return null;
|
||||
const type = sessionId.slice(0, idx);
|
||||
if (type === 'person' || type === 'group') return type;
|
||||
const lower = sessionId.toLowerCase();
|
||||
if (lower.includes('person')) return 'person';
|
||||
if (lower.includes('group')) return 'group';
|
||||
return null;
|
||||
};
|
||||
|
||||
const isSessionAdmin = (session: SessionInfo): boolean => {
|
||||
const type = parseSessionType(session.session_id);
|
||||
const lid =
|
||||
session.user_id ??
|
||||
session.session_id.replace(
|
||||
/^.*?[._](?:PERSON|GROUP|person|group)[._]/i,
|
||||
'',
|
||||
);
|
||||
return admins.some(
|
||||
(a: BotAdmin) => a.launcher_type === type && a.launcher_id === lid,
|
||||
);
|
||||
};
|
||||
|
||||
const toggleAdmin = async (session: SessionInfo) => {
|
||||
const type = parseSessionType(session.session_id);
|
||||
if (!type) return;
|
||||
const lid =
|
||||
session.user_id ??
|
||||
session.session_id.replace(
|
||||
/^.*?[._](?:PERSON|GROUP|person|group)[._]/i,
|
||||
'',
|
||||
);
|
||||
const key = session.session_id;
|
||||
setTogglingAdmin(key);
|
||||
try {
|
||||
const existing = admins.find(
|
||||
(a: BotAdmin) => a.launcher_type === type && a.launcher_id === lid,
|
||||
);
|
||||
if (existing) {
|
||||
await httpClient.deleteBotAdmin(botId, existing.id);
|
||||
toast.success(t('bots.admins.deleteSuccess'));
|
||||
} else {
|
||||
await httpClient.addBotAdmin(botId, type, lid);
|
||||
toast.success(t('bots.admins.addSuccess'));
|
||||
}
|
||||
await reloadAdmins();
|
||||
} catch {
|
||||
toast.error(t('bots.admins.addError'));
|
||||
} finally {
|
||||
setTogglingAdmin(null);
|
||||
}
|
||||
};
|
||||
|
||||
const abbreviateId = (id: string): string => {
|
||||
if (id.length <= 10) return id;
|
||||
return `${id.slice(0, 4)}..${id.slice(-4)}`;
|
||||
@@ -384,257 +436,307 @@ const BotSessionMonitor = forwardRef<
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row h-full min-h-0 rounded-lg border overflow-hidden">
|
||||
{/* Left Panel: Session List */}
|
||||
<div className="max-h-48 md:max-h-none md:w-60 flex-shrink-0 border-b md:border-b-0 md:border-r flex flex-col min-h-0">
|
||||
{/* Session List */}
|
||||
<ScrollArea className="flex-1 min-h-0">
|
||||
{loadingSessions && sessions.length === 0 ? (
|
||||
<div className="flex items-center justify-center py-12 text-sm text-muted-foreground">
|
||||
{t('bots.sessionMonitor.loading')}
|
||||
</div>
|
||||
) : sessions.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-12 text-sm">
|
||||
{t('bots.sessionMonitor.noSessions')}
|
||||
<>
|
||||
<div className="flex flex-col md:flex-row h-full min-h-0 rounded-lg border overflow-hidden">
|
||||
{/* Left Panel: Session List */}
|
||||
<div className="max-h-48 md:max-h-none md:w-60 flex-shrink-0 border-b md:border-b-0 md:border-r flex flex-col min-h-0">
|
||||
{/* Admin header */}
|
||||
<div className="px-2 py-1.5 border-b shrink-0 flex items-center justify-between">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-1.5 text-sm font-medium hover:text-foreground transition-colors"
|
||||
onClick={() => setAdminsDialogOpen(true)}
|
||||
>
|
||||
<ShieldCheck className="size-4" />
|
||||
<span>
|
||||
{t('bots.admins.configureAdmins')}
|
||||
{admins.length > 0 && (
|
||||
<span className="ml-1 tabular-nums text-xs text-muted-foreground">
|
||||
({admins.length})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
{/* Session List */}
|
||||
<ScrollArea className="flex-1 min-h-0">
|
||||
{loadingSessions && sessions.length === 0 ? (
|
||||
<div className="flex items-center justify-center py-12 text-sm text-muted-foreground">
|
||||
{t('bots.sessionMonitor.loading')}
|
||||
</div>
|
||||
) : sessions.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-12 text-sm">
|
||||
{t('bots.sessionMonitor.noSessions')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-1.5">
|
||||
{sessions.map((session) => {
|
||||
const isSelected = selectedSessionId === session.session_id;
|
||||
const sessionType = parseSessionType(session.session_id);
|
||||
const sessionIsAdmin = isSessionAdmin(session);
|
||||
return (
|
||||
<div
|
||||
key={session.session_id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={cn(
|
||||
'w-full text-left px-2.5 py-2 rounded-md transition-colors cursor-pointer',
|
||||
isSelected ? 'bg-accent' : 'hover:bg-accent/50',
|
||||
)}
|
||||
onClick={() => setSelectedSessionId(session.session_id)}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-0.5">
|
||||
<span className="text-sm font-medium truncate mr-2">
|
||||
{session.user_name ||
|
||||
session.user_id ||
|
||||
session.session_id.slice(0, 12)}
|
||||
</span>
|
||||
<span className="text-[11px] text-muted-foreground tabular-nums flex-shrink-0">
|
||||
{formatRelativeTime(session.last_activity)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
{sessionType && (
|
||||
<span className="px-1 py-0.5 rounded bg-muted text-[10px]">
|
||||
{sessionType}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{session.user_id && (
|
||||
<span className="truncate text-[10px]">
|
||||
{abbreviateId(session.user_id)}
|
||||
</span>
|
||||
)}
|
||||
{session.is_active && (
|
||||
<span className="flex items-center gap-0.5 text-green-600 dark:text-green-400">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-green-500 inline-block" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
{/* Right Panel: Messages */}
|
||||
<div className="flex-1 flex flex-col min-h-0 min-w-0">
|
||||
{!selectedSessionId ? (
|
||||
<div className="text-center text-muted-foreground text-sm flex-1 flex items-center justify-center">
|
||||
{t('bots.sessionMonitor.selectSession')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-1.5">
|
||||
{sessions.map((session) => {
|
||||
const isSelected = selectedSessionId === session.session_id;
|
||||
return (
|
||||
<button
|
||||
key={session.session_id}
|
||||
type="button"
|
||||
className={cn(
|
||||
'w-full text-left px-2.5 py-2 rounded-md transition-colors',
|
||||
isSelected ? 'bg-accent' : 'hover:bg-accent/50',
|
||||
<>
|
||||
{/* Chat Header */}
|
||||
<div className="px-4 py-2.5 border-b shrink-0 flex items-center justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-medium truncate">
|
||||
{selectedSession?.user_name ||
|
||||
selectedSession?.user_id ||
|
||||
selectedSessionId.slice(0, 20)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground mt-0.5">
|
||||
{parseSessionType(selectedSessionId) && (
|
||||
<span>{parseSessionType(selectedSessionId)}</span>
|
||||
)}
|
||||
onClick={() => setSelectedSessionId(session.session_id)}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-0.5">
|
||||
<span className="text-sm font-medium truncate mr-2">
|
||||
{session.user_name ||
|
||||
session.user_id ||
|
||||
session.session_id.slice(0, 12)}
|
||||
</span>
|
||||
<span className="text-[11px] text-muted-foreground tabular-nums flex-shrink-0">
|
||||
{formatRelativeTime(session.last_activity)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
||||
{parseSessionType(session.session_id) && (
|
||||
<span className="px-1 py-0.5 rounded bg-muted text-[10px]">
|
||||
{parseSessionType(session.session_id)}
|
||||
{selectedSession?.user_id && (
|
||||
<>
|
||||
<span>·</span>
|
||||
<span className="font-mono">
|
||||
{selectedSession.user_id}
|
||||
</span>
|
||||
)}
|
||||
{session.platform && (
|
||||
<span className="px-1 py-0.5 rounded bg-muted text-[10px]">
|
||||
{session.platform}
|
||||
</span>
|
||||
)}
|
||||
{session.user_id && (
|
||||
<span className="truncate text-[10px]">
|
||||
{abbreviateId(session.user_id)}
|
||||
</span>
|
||||
)}
|
||||
{session.is_active && (
|
||||
<span className="flex items-center gap-0.5 text-green-600 dark:text-green-400">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyUserId(selectedSession.user_id!)}
|
||||
className="inline-flex items-center text-muted-foreground hover:text-foreground transition-colors"
|
||||
title={t('common.copy')}
|
||||
>
|
||||
{copiedUserId ? (
|
||||
<Check className="w-3 h-3 text-green-600" />
|
||||
) : (
|
||||
<Copy className="w-3 h-3" />
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{selectedSession?.is_active && (
|
||||
<>
|
||||
<span>·</span>
|
||||
<span className="flex items-center gap-1 text-green-600 dark:text-green-400">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-green-500 inline-block" />
|
||||
Active
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
{/* Right Panel: Messages */}
|
||||
<div className="flex-1 flex flex-col min-h-0 min-w-0">
|
||||
{!selectedSessionId ? (
|
||||
<div className="text-center text-muted-foreground text-sm flex-1 flex items-center justify-center">
|
||||
{t('bots.sessionMonitor.selectSession')}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Chat Header */}
|
||||
<div className="px-4 py-2.5 border-b shrink-0">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium truncate">
|
||||
{selectedSession?.user_name ||
|
||||
selectedSession?.user_id ||
|
||||
selectedSessionId.slice(0, 20)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-muted-foreground mt-0.5">
|
||||
{parseSessionType(selectedSessionId) && (
|
||||
<span>{parseSessionType(selectedSessionId)}</span>
|
||||
)}
|
||||
{selectedSession?.platform && (
|
||||
<>
|
||||
{parseSessionType(selectedSessionId) && <span>·</span>}
|
||||
<span>{selectedSession.platform}</span>
|
||||
</>
|
||||
)}
|
||||
{selectedSession?.user_id && (
|
||||
<>
|
||||
<span>·</span>
|
||||
<span className="font-mono">
|
||||
{selectedSession.user_id}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyUserId(selectedSession.user_id!)}
|
||||
className="inline-flex items-center text-muted-foreground hover:text-foreground transition-colors"
|
||||
title={t('common.copy')}
|
||||
>
|
||||
{copiedUserId ? (
|
||||
<Check className="w-3 h-3 text-green-600" />
|
||||
) : (
|
||||
<Copy className="w-3 h-3" />
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{selectedSession?.is_active && (
|
||||
<>
|
||||
<span>·</span>
|
||||
<span className="flex items-center gap-1 text-green-600 dark:text-green-400">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-green-500 inline-block" />
|
||||
Active
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{selectedSession && parseSessionType(selectedSessionId) && (
|
||||
<>
|
||||
<span>·</span>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1 transition-colors',
|
||||
isSessionAdmin(selectedSession)
|
||||
? 'text-blue-500'
|
||||
: 'text-muted-foreground hover:text-blue-500',
|
||||
)}
|
||||
disabled={togglingAdmin === selectedSessionId}
|
||||
title={
|
||||
isSessionAdmin(selectedSession)
|
||||
? t('bots.admins.removeAdminTitle')
|
||||
: t('bots.admins.setAdminTitle')
|
||||
}
|
||||
onClick={() => toggleAdmin(selectedSession)}
|
||||
>
|
||||
{isSessionAdmin(selectedSession) ? (
|
||||
<ShieldCheck className="size-3.5" />
|
||||
) : (
|
||||
<ShieldOff className="size-3.5" />
|
||||
)}
|
||||
<span>{t('bots.admins.adminBadge')}</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages Area */}
|
||||
<ScrollArea
|
||||
ref={messagesContainerRef}
|
||||
className="flex-1 px-4 py-4 overflow-y-auto min-h-0"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{loadingMessages ? (
|
||||
<div className="text-center text-muted-foreground py-12 text-sm">
|
||||
{t('bots.sessionMonitor.loading')}
|
||||
</div>
|
||||
) : messages.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-12 text-sm">
|
||||
{t('bots.sessionMonitor.noMessages')}
|
||||
</div>
|
||||
) : (
|
||||
messages.map((msg, msgIndex) => {
|
||||
const isUser = isUserMessage(msg);
|
||||
const isDiscarded =
|
||||
msg.status === 'discarded' ||
|
||||
msg.pipeline_id === PIPELINE_DISCARD;
|
||||
// For bot replies, find feedback linked to the preceding user message
|
||||
let msgFeedback: SessionFeedback | undefined;
|
||||
if (!isUser) {
|
||||
for (let i = msgIndex - 1; i >= 0; i--) {
|
||||
if (isUserMessage(messages[i])) {
|
||||
msgFeedback = feedbackMap[messages[i].id];
|
||||
break;
|
||||
{/* Messages Area */}
|
||||
<ScrollArea
|
||||
ref={messagesContainerRef}
|
||||
className="flex-1 px-4 py-4 overflow-y-auto min-h-0"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{loadingMessages ? (
|
||||
<div className="text-center text-muted-foreground py-12 text-sm">
|
||||
{t('bots.sessionMonitor.loading')}
|
||||
</div>
|
||||
) : messages.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-12 text-sm">
|
||||
{t('bots.sessionMonitor.noMessages')}
|
||||
</div>
|
||||
) : (
|
||||
messages.map((msg, msgIndex) => {
|
||||
const isUser = isUserMessage(msg);
|
||||
const isDiscarded =
|
||||
msg.status === 'discarded' ||
|
||||
msg.pipeline_id === PIPELINE_DISCARD;
|
||||
// For bot replies, find feedback linked to the preceding user message
|
||||
let msgFeedback: SessionFeedback | undefined;
|
||||
if (!isUser) {
|
||||
for (let i = msgIndex - 1; i >= 0; i--) {
|
||||
if (isUserMessage(messages[i])) {
|
||||
msgFeedback = feedbackMap[messages[i].id];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={msg.id}
|
||||
className={cn(
|
||||
'flex',
|
||||
isUser ? 'justify-end' : 'justify-start',
|
||||
)}
|
||||
>
|
||||
return (
|
||||
<div
|
||||
key={msg.id}
|
||||
className={cn(
|
||||
'max-w-3xl px-4 py-2.5 rounded-2xl text-sm',
|
||||
isUser
|
||||
? 'bg-primary/10 rounded-br-sm'
|
||||
: 'bg-muted rounded-bl-sm',
|
||||
msg.status === 'error' && 'ring-1 ring-red-400/50',
|
||||
isDiscarded && 'opacity-60',
|
||||
'flex',
|
||||
isUser ? 'justify-end' : 'justify-start',
|
||||
)}
|
||||
>
|
||||
{renderMessageContent(msg)}
|
||||
{/* Role label + pipeline + timestamp */}
|
||||
<div
|
||||
className={cn(
|
||||
'text-[11px] mt-1.5 flex items-center gap-1.5 text-muted-foreground',
|
||||
'max-w-3xl px-4 py-2.5 rounded-2xl text-sm',
|
||||
isUser
|
||||
? 'bg-primary/10 rounded-br-sm'
|
||||
: 'bg-muted rounded-bl-sm',
|
||||
msg.status === 'error' &&
|
||||
'ring-1 ring-red-400/50',
|
||||
isDiscarded && 'opacity-60',
|
||||
)}
|
||||
>
|
||||
<span>
|
||||
{isUser
|
||||
? t('bots.sessionMonitor.userMessage', {
|
||||
defaultValue: 'User',
|
||||
})
|
||||
: t('bots.sessionMonitor.botMessage', {
|
||||
defaultValue: 'Assistant',
|
||||
{renderMessageContent(msg)}
|
||||
{/* Role label + pipeline + timestamp */}
|
||||
<div
|
||||
className={cn(
|
||||
'text-[11px] mt-1.5 flex items-center gap-1.5 text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
<span>
|
||||
{isUser
|
||||
? t('bots.sessionMonitor.userMessage', {
|
||||
defaultValue: 'User',
|
||||
})
|
||||
: t('bots.sessionMonitor.botMessage', {
|
||||
defaultValue: 'Assistant',
|
||||
})}
|
||||
</span>
|
||||
<span className="tabular-nums">
|
||||
{formatTime(msg.timestamp)}
|
||||
</span>
|
||||
{isDiscarded ? (
|
||||
<span className="inline-flex items-center gap-0.5 text-destructive">
|
||||
<Ban className="w-3 h-3" />
|
||||
{t('bots.sessionMonitor.discarded', {
|
||||
defaultValue: 'Discarded',
|
||||
})}
|
||||
</span>
|
||||
<span className="tabular-nums">
|
||||
{formatTime(msg.timestamp)}
|
||||
</span>
|
||||
{isDiscarded ? (
|
||||
<span className="inline-flex items-center gap-0.5 text-destructive">
|
||||
<Ban className="w-3 h-3" />
|
||||
{t('bots.sessionMonitor.discarded', {
|
||||
defaultValue: 'Discarded',
|
||||
})}
|
||||
</span>
|
||||
) : msg.pipeline_name ? (
|
||||
<span className="inline-flex items-center gap-0.5 opacity-70">
|
||||
<Workflow className="w-3 h-3" />
|
||||
{msg.pipeline_name}
|
||||
</span>
|
||||
) : null}
|
||||
{msg.status === 'error' && (
|
||||
<span className="text-red-500">error</span>
|
||||
)}
|
||||
{msg.runner_name && (
|
||||
<span className="inline-flex items-center gap-0.5 opacity-70">
|
||||
<Bot className="w-3 h-3" />
|
||||
{msg.runner_name}
|
||||
</span>
|
||||
)}
|
||||
{/* Feedback indicator — same line, pushed right */}
|
||||
{!isUser &&
|
||||
msgFeedback &&
|
||||
(msgFeedback.feedback_type === 1 ? (
|
||||
<span className="inline-flex items-center gap-1 ml-auto text-green-600 dark:text-green-400 cursor-default relative group">
|
||||
<ThumbsUp className="w-3 h-3 flex-shrink-0" />
|
||||
{t('monitoring.feedback.like')}
|
||||
{msgFeedback.feedback_content && (
|
||||
<span className="hidden group-hover:block absolute bottom-full right-0 mb-1 px-3 py-1.5 rounded-lg bg-popover border text-popover-foreground text-xs whitespace-nowrap shadow-md z-10">
|
||||
{msgFeedback.feedback_content}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 ml-auto text-red-500 dark:text-red-400 cursor-default relative group">
|
||||
<ThumbsDown className="w-3 h-3 flex-shrink-0" />
|
||||
{t('monitoring.feedback.dislike')}
|
||||
{msgFeedback.feedback_content && (
|
||||
<span className="hidden group-hover:block absolute bottom-full right-0 mb-1 px-3 py-1.5 rounded-lg bg-popover border text-popover-foreground text-xs whitespace-nowrap shadow-md z-10">
|
||||
{msgFeedback.feedback_content}
|
||||
</span>
|
||||
)}
|
||||
) : msg.pipeline_name ? (
|
||||
<span className="inline-flex items-center gap-0.5 opacity-70">
|
||||
<Workflow className="w-3 h-3" />
|
||||
{msg.pipeline_name}
|
||||
</span>
|
||||
))}
|
||||
) : null}
|
||||
{msg.status === 'error' && (
|
||||
<span className="text-red-500">error</span>
|
||||
)}
|
||||
{msg.runner_name && (
|
||||
<span className="inline-flex items-center gap-0.5 opacity-70">
|
||||
<Bot className="w-3 h-3" />
|
||||
{msg.runner_name}
|
||||
</span>
|
||||
)}
|
||||
{/* Feedback indicator — same line, pushed right */}
|
||||
{!isUser &&
|
||||
msgFeedback &&
|
||||
(msgFeedback.feedback_type === 1 ? (
|
||||
<span className="inline-flex items-center gap-1 ml-auto text-green-600 dark:text-green-400 cursor-default relative group">
|
||||
<ThumbsUp className="w-3 h-3 flex-shrink-0" />
|
||||
{t('monitoring.feedback.like')}
|
||||
{msgFeedback.feedback_content && (
|
||||
<span className="hidden group-hover:block absolute bottom-full right-0 mb-1 px-3 py-1.5 rounded-lg bg-popover border text-popover-foreground text-xs whitespace-nowrap shadow-md z-10">
|
||||
{msgFeedback.feedback_content}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 ml-auto text-red-500 dark:text-red-400 cursor-default relative group">
|
||||
<ThumbsDown className="w-3 h-3 flex-shrink-0" />
|
||||
{t('monitoring.feedback.dislike')}
|
||||
{msgFeedback.feedback_content && (
|
||||
<span className="hidden group-hover:block absolute bottom-full right-0 mb-1 px-3 py-1.5 rounded-lg bg-popover border text-popover-foreground text-xs whitespace-nowrap shadow-md z-10">
|
||||
{msgFeedback.feedback_content}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</>
|
||||
)}
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BotAdminsDialog
|
||||
botId={botId}
|
||||
open={adminsDialogOpen}
|
||||
onOpenChange={setAdminsDialogOpen}
|
||||
admins={admins}
|
||||
onAdminsChange={reloadAdmins}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
DynamicFormItemType,
|
||||
IDynamicFormItemSchema,
|
||||
SYSTEM_FIELD_PREFIX,
|
||||
DynamicFormItemType,
|
||||
} from '@/app/infra/entities/form/dynamic';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
@@ -59,6 +59,97 @@ function resolveShowIfValue(
|
||||
return externalDependentValues?.[field];
|
||||
}
|
||||
|
||||
type DynamicFormValueSpec = Pick<
|
||||
IDynamicFormItemSchema,
|
||||
'default' | 'name' | 'required' | 'type'
|
||||
>;
|
||||
|
||||
function getValueSpecs(item: IDynamicFormItemSchema): DynamicFormValueSpec[] {
|
||||
if (item.type === DynamicFormItemType.RICH_TOOLS_SELECTOR) {
|
||||
return [
|
||||
item,
|
||||
{
|
||||
name: 'enable-all-tools',
|
||||
type: DynamicFormItemType.BOOLEAN,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (item.type === DynamicFormItemType.RESOURCES_SELECTOR) {
|
||||
return [
|
||||
item,
|
||||
{
|
||||
name: 'mcp-resources',
|
||||
type: DynamicFormItemType.UNKNOWN,
|
||||
required: false,
|
||||
default: [],
|
||||
},
|
||||
{
|
||||
name: 'mcp-resource-agent-read-enabled',
|
||||
type: DynamicFormItemType.BOOLEAN,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [item];
|
||||
}
|
||||
|
||||
function getValueSchema(spec: DynamicFormValueSpec) {
|
||||
if (spec.name === 'mcp-resources') {
|
||||
return z.array(z.any());
|
||||
}
|
||||
|
||||
const normalizedType = normalizeItemType(spec.type);
|
||||
|
||||
switch (normalizedType) {
|
||||
case DynamicFormItemType.INT:
|
||||
return z.number();
|
||||
case DynamicFormItemType.FLOAT:
|
||||
return z.number();
|
||||
case DynamicFormItemType.BOOLEAN:
|
||||
return z.boolean();
|
||||
case DynamicFormItemType.STRING:
|
||||
return z.string();
|
||||
case DynamicFormItemType.STRING_ARRAY:
|
||||
return z.array(z.string());
|
||||
case DynamicFormItemType.SELECT:
|
||||
return z.string();
|
||||
case DynamicFormItemType.LLM_MODEL_SELECTOR:
|
||||
return z.string();
|
||||
case DynamicFormItemType.EMBEDDING_MODEL_SELECTOR:
|
||||
return z.string();
|
||||
case DynamicFormItemType.RERANK_MODEL_SELECTOR:
|
||||
return z.string();
|
||||
case DynamicFormItemType.KNOWLEDGE_BASE_SELECTOR:
|
||||
return z.string();
|
||||
case DynamicFormItemType.KNOWLEDGE_BASE_MULTI_SELECTOR:
|
||||
case DynamicFormItemType.RESOURCES_SELECTOR:
|
||||
case DynamicFormItemType.RICH_TOOLS_SELECTOR:
|
||||
case DynamicFormItemType.TOOLS_SELECTOR:
|
||||
return z.array(z.string());
|
||||
case DynamicFormItemType.BOT_SELECTOR:
|
||||
return z.string();
|
||||
case DynamicFormItemType.MODEL_FALLBACK_SELECTOR:
|
||||
return z.object({
|
||||
primary: z.string(),
|
||||
fallbacks: z.array(z.string()),
|
||||
});
|
||||
case DynamicFormItemType.PROMPT_EDITOR:
|
||||
return z.array(
|
||||
z.object({
|
||||
content: z.string(),
|
||||
role: z.string(),
|
||||
}),
|
||||
);
|
||||
default:
|
||||
return z.string();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display-only component for embed code fields with copy animation.
|
||||
*/
|
||||
@@ -331,9 +422,16 @@ export default function DynamicFormComponent({
|
||||
// model-fallback-selector) is coerced to the expected shape
|
||||
// so that downstream components never crash.
|
||||
const normalizeFieldValue = (
|
||||
item: IDynamicFormItemSchema,
|
||||
item: DynamicFormValueSpec,
|
||||
value: unknown,
|
||||
): unknown => {
|
||||
if (
|
||||
item.name === 'mcp-resources' ||
|
||||
item.type === DynamicFormItemType.RESOURCES_SELECTOR ||
|
||||
item.type === DynamicFormItemType.RICH_TOOLS_SELECTOR
|
||||
) {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
if (item.type === 'model-fallback-selector') {
|
||||
if (value != null && typeof value === 'object' && !Array.isArray(value)) {
|
||||
const obj = value as Record<string, unknown>;
|
||||
@@ -377,74 +475,16 @@ export default function DynamicFormComponent({
|
||||
[itemConfigList],
|
||||
);
|
||||
|
||||
const editableValueSpecs = useMemo(
|
||||
() => editableItems.flatMap(getValueSpecs),
|
||||
[editableItems],
|
||||
);
|
||||
|
||||
// 根据 itemConfigList 动态生成 zod schema
|
||||
const formSchema = z.object(
|
||||
editableItems.reduce(
|
||||
editableValueSpecs.reduce(
|
||||
(acc, item) => {
|
||||
// Normalize type to handle plugin manifest type names
|
||||
const normalizedType = normalizeItemType(item.type);
|
||||
|
||||
let fieldSchema;
|
||||
switch (normalizedType) {
|
||||
case 'integer':
|
||||
fieldSchema = z.number();
|
||||
break;
|
||||
case 'float':
|
||||
fieldSchema = z.number();
|
||||
break;
|
||||
case 'boolean':
|
||||
fieldSchema = z.boolean();
|
||||
break;
|
||||
case 'string':
|
||||
fieldSchema = z.string();
|
||||
break;
|
||||
case 'array[string]':
|
||||
fieldSchema = z.array(z.string());
|
||||
break;
|
||||
case 'select':
|
||||
fieldSchema = z.string();
|
||||
break;
|
||||
case 'llm-model-selector':
|
||||
fieldSchema = z.string();
|
||||
break;
|
||||
case 'embedding-model-selector':
|
||||
fieldSchema = z.string();
|
||||
break;
|
||||
case 'rerank-model-selector':
|
||||
fieldSchema = z.string();
|
||||
break;
|
||||
case 'knowledge-base-selector':
|
||||
fieldSchema = z.string();
|
||||
break;
|
||||
case 'knowledge-base-multi-selector':
|
||||
fieldSchema = z.array(z.string());
|
||||
break;
|
||||
case 'bot-selector':
|
||||
fieldSchema = z.string();
|
||||
break;
|
||||
case 'tools-selector':
|
||||
fieldSchema = z.array(z.string());
|
||||
break;
|
||||
case 'model-fallback-selector':
|
||||
fieldSchema = z.object({
|
||||
primary: z.string(),
|
||||
fallbacks: z.array(z.string()),
|
||||
});
|
||||
break;
|
||||
case 'prompt-editor':
|
||||
fieldSchema = z.array(
|
||||
z.object({
|
||||
content: z.string(),
|
||||
role: z.string(),
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case 'text':
|
||||
fieldSchema = z.string();
|
||||
break;
|
||||
default:
|
||||
fieldSchema = z.string();
|
||||
}
|
||||
let fieldSchema = getValueSchema(item);
|
||||
|
||||
if (
|
||||
item.required &&
|
||||
@@ -469,7 +509,7 @@ export default function DynamicFormComponent({
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: editableItems.reduce((acc, item) => {
|
||||
defaultValues: editableValueSpecs.reduce((acc, item) => {
|
||||
// 优先使用 initialValues,如果没有则使用默认值
|
||||
const rawValue = initialValues?.[item.name] ?? item.default;
|
||||
return {
|
||||
@@ -508,7 +548,7 @@ export default function DynamicFormComponent({
|
||||
|
||||
if (initialValues && hasRealChange) {
|
||||
// 合并默认值和初始值
|
||||
const mergedValues = editableItems.reduce(
|
||||
const mergedValues = editableValueSpecs.reduce(
|
||||
(acc, item) => {
|
||||
const rawValue = initialValues[item.name] ?? item.default;
|
||||
acc[item.name] = normalizeFieldValue(item, rawValue) as object;
|
||||
@@ -523,10 +563,16 @@ export default function DynamicFormComponent({
|
||||
|
||||
previousInitialValues.current = initialValues;
|
||||
}
|
||||
}, [initialValues, form, editableItems]);
|
||||
}, [initialValues, form, editableValueSpecs]);
|
||||
|
||||
// Get reactive form values for conditional rendering
|
||||
const watchedValues = form.watch();
|
||||
const setFormValue = (name: string, value: unknown) => {
|
||||
form.setValue(name as keyof FormValues, value as never, {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
};
|
||||
|
||||
// Stable ref for onSubmit to avoid re-triggering the effect when the
|
||||
// parent passes a new closure on every render.
|
||||
@@ -539,7 +585,7 @@ export default function DynamicFormComponent({
|
||||
// even if the user saves without modifying any field.
|
||||
// form.watch(callback) only fires on subsequent changes, not on mount.
|
||||
const formValues = form.getValues();
|
||||
const initialFinalValues = editableItems.reduce(
|
||||
const initialFinalValues = editableValueSpecs.reduce(
|
||||
(acc, item) => {
|
||||
acc[item.name] = formValues[item.name] ?? item.default;
|
||||
return acc;
|
||||
@@ -559,7 +605,7 @@ export default function DynamicFormComponent({
|
||||
|
||||
const subscription = form.watch(() => {
|
||||
const formValues = form.getValues();
|
||||
const finalValues = editableItems.reduce(
|
||||
const finalValues = editableValueSpecs.reduce(
|
||||
(acc, item) => {
|
||||
acc[item.name] = formValues[item.name] ?? item.default;
|
||||
return acc;
|
||||
@@ -570,7 +616,7 @@ export default function DynamicFormComponent({
|
||||
previousInitialValues.current = finalValues as Record<string, object>;
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [form, editableItems]);
|
||||
}, [form, editableValueSpecs]);
|
||||
|
||||
// State for QR code login dialog
|
||||
const [qrDialogOpen, setQrDialogOpen] = useState(false);
|
||||
@@ -808,6 +854,41 @@ export default function DynamicFormComponent({
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
normalizedConfig.type === DynamicFormItemType.RICH_TOOLS_SELECTOR ||
|
||||
normalizedConfig.type === DynamicFormItemType.RESOURCES_SELECTOR
|
||||
) {
|
||||
return (
|
||||
<FormField
|
||||
key={config.id}
|
||||
control={form.control}
|
||||
name={config.name as keyof FormValues}
|
||||
render={({ field }) => (
|
||||
<FormItem className="min-w-0">
|
||||
<FormControl>
|
||||
<div
|
||||
className={cn(
|
||||
'min-w-0 max-w-full overflow-x-hidden',
|
||||
isFieldDisabled && 'pointer-events-none opacity-60',
|
||||
)}
|
||||
>
|
||||
<DynamicFormItemComponent
|
||||
config={normalizedConfig}
|
||||
field={field}
|
||||
formValues={watchedValues as Record<string, unknown>}
|
||||
onFileUploaded={onFileUploaded}
|
||||
setFormValue={setFormValue}
|
||||
systemContext={systemContext}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Boolean fields use a special inline layout
|
||||
if (normalizedConfig.type === 'boolean') {
|
||||
return (
|
||||
@@ -838,7 +919,10 @@ export default function DynamicFormComponent({
|
||||
<DynamicFormItemComponent
|
||||
config={normalizedConfig}
|
||||
field={field}
|
||||
formValues={watchedValues as Record<string, unknown>}
|
||||
onFileUploaded={onFileUploaded}
|
||||
setFormValue={setFormValue}
|
||||
systemContext={systemContext}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
@@ -875,7 +959,10 @@ export default function DynamicFormComponent({
|
||||
<DynamicFormItemComponent
|
||||
config={normalizedConfig}
|
||||
field={field}
|
||||
formValues={watchedValues as Record<string, unknown>}
|
||||
onFileUploaded={onFileUploaded}
|
||||
setFormValue={setFormValue}
|
||||
systemContext={systemContext}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
|
||||
@@ -64,6 +64,8 @@ import {
|
||||
import SettingsDialog, {
|
||||
SettingsSection,
|
||||
} from '@/app/home/components/settings-dialog/SettingsDialog';
|
||||
import ToolResourceSelectors from '@/app/home/components/dynamic-form/ToolResourceSelectors';
|
||||
import { LANGBOT_MODELS_PROVIDER_REQUESTER } from '@/app/home/components/models-dialog/types';
|
||||
|
||||
function getPluginComponentIconURL(value?: string): string | null {
|
||||
if (!value?.startsWith('plugin:')) {
|
||||
@@ -113,11 +115,17 @@ function SelectOptionContent({
|
||||
export default function DynamicFormItemComponent({
|
||||
config,
|
||||
field,
|
||||
formValues,
|
||||
onFileUploaded,
|
||||
setFormValue,
|
||||
systemContext,
|
||||
}: {
|
||||
config: IDynamicFormItemSchema;
|
||||
field: ControllerRenderProps<any, any>;
|
||||
formValues?: Record<string, unknown>;
|
||||
onFileUploaded?: (fileKey: string) => void;
|
||||
setFormValue?: (name: string, value: unknown) => void;
|
||||
systemContext?: Record<string, unknown>;
|
||||
}) {
|
||||
const [llmModels, setLlmModels] = useState<LLMModel[]>([]);
|
||||
const [embeddingModels, setEmbeddingModels] = useState<EmbeddingModel[]>([]);
|
||||
@@ -148,10 +156,34 @@ export default function DynamicFormItemComponent({
|
||||
});
|
||||
};
|
||||
|
||||
const fetchEmbeddingModels = () => {
|
||||
httpClient
|
||||
.getProviderEmbeddingModels()
|
||||
.then((resp) => {
|
||||
setEmbeddingModels(resp.models);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(t('embedding.getModelListError') + err.msg);
|
||||
});
|
||||
};
|
||||
|
||||
const fetchRerankModels = () => {
|
||||
httpClient
|
||||
.getProviderRerankModels()
|
||||
.then((resp) => {
|
||||
setRerankModels(resp.models);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('Failed to load rerank models: ' + err.msg);
|
||||
});
|
||||
};
|
||||
|
||||
const handleModelsDialogChange = (open: boolean) => {
|
||||
setModelsDialogOpen(open);
|
||||
if (!open) {
|
||||
fetchLlmModels();
|
||||
fetchEmbeddingModels();
|
||||
fetchRerankModels();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -219,27 +251,13 @@ export default function DynamicFormItemComponent({
|
||||
|
||||
useEffect(() => {
|
||||
if (config.type === DynamicFormItemType.EMBEDDING_MODEL_SELECTOR) {
|
||||
httpClient
|
||||
.getProviderEmbeddingModels()
|
||||
.then((resp) => {
|
||||
setEmbeddingModels(resp.models);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(t('embedding.getModelListError') + err.msg);
|
||||
});
|
||||
fetchEmbeddingModels();
|
||||
}
|
||||
}, [config.type]);
|
||||
|
||||
useEffect(() => {
|
||||
if (config.type === DynamicFormItemType.RERANK_MODEL_SELECTOR) {
|
||||
httpClient
|
||||
.getProviderRerankModels()
|
||||
.then((resp) => {
|
||||
setRerankModels(resp.models);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error('Failed to load rerank models: ' + err.msg);
|
||||
});
|
||||
fetchRerankModels();
|
||||
}
|
||||
}, [config.type]);
|
||||
|
||||
@@ -293,6 +311,16 @@ export default function DynamicFormItemComponent({
|
||||
}
|
||||
}, [config.type]);
|
||||
|
||||
const handleCompositePatch = (patch: Record<string, unknown>) => {
|
||||
for (const [name, value] of Object.entries(patch)) {
|
||||
if (setFormValue) {
|
||||
setFormValue(name, value);
|
||||
} else if (name === field.name) {
|
||||
field.onChange(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
switch (config.type) {
|
||||
case DynamicFormItemType.INT:
|
||||
case DynamicFormItemType.FLOAT:
|
||||
@@ -461,10 +489,10 @@ export default function DynamicFormItemComponent({
|
||||
case DynamicFormItemType.LLM_MODEL_SELECTOR:
|
||||
// Separate space models from regular models
|
||||
const spaceModels = llmModels.filter(
|
||||
(m) => m.provider?.requester === 'space-chat-completions',
|
||||
(m) => m.provider?.requester === LANGBOT_MODELS_PROVIDER_REQUESTER,
|
||||
);
|
||||
const regularModels = llmModels.filter(
|
||||
(m) => m.provider?.requester !== 'space-chat-completions',
|
||||
(m) => m.provider?.requester !== LANGBOT_MODELS_PROVIDER_REQUESTER,
|
||||
);
|
||||
|
||||
// Group regular models by provider
|
||||
@@ -565,7 +593,7 @@ export default function DynamicFormItemComponent({
|
||||
</div>
|
||||
))}
|
||||
{/* Blurred remaining models with login overlay */}
|
||||
<div className="relative">
|
||||
<div className="relative min-h-10">
|
||||
<div
|
||||
className="select-none overflow-hidden"
|
||||
style={{ maxHeight: '3rem' }}
|
||||
@@ -642,7 +670,10 @@ export default function DynamicFormItemComponent({
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9 shrink-0"
|
||||
onClick={() => setModelsDialogOpen(true)}
|
||||
onClick={() => {
|
||||
setSettingsSection('models');
|
||||
setModelsDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Settings className="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
@@ -658,9 +689,15 @@ export default function DynamicFormItemComponent({
|
||||
</div>
|
||||
);
|
||||
|
||||
case DynamicFormItemType.EMBEDDING_MODEL_SELECTOR:
|
||||
// Group embedding models by provider
|
||||
const groupedEmbeddingModels = embeddingModels.reduce(
|
||||
case DynamicFormItemType.EMBEDDING_MODEL_SELECTOR: {
|
||||
const spaceEmbeddingModels = embeddingModels.filter(
|
||||
(m) => m.provider?.requester === LANGBOT_MODELS_PROVIDER_REQUESTER,
|
||||
);
|
||||
const regularEmbeddingModels = embeddingModels.filter(
|
||||
(m) => m.provider?.requester !== LANGBOT_MODELS_PROVIDER_REQUESTER,
|
||||
);
|
||||
|
||||
const groupedEmbeddingModels = regularEmbeddingModels.reduce(
|
||||
(acc, model) => {
|
||||
const providerName = model.provider?.name || 'Unknown';
|
||||
if (!acc[providerName]) acc[providerName] = [];
|
||||
@@ -670,29 +707,169 @@ export default function DynamicFormItemComponent({
|
||||
{} as Record<string, EmbeddingModel[]>,
|
||||
);
|
||||
|
||||
const groupedSpaceEmbeddingModels = spaceEmbeddingModels.reduce(
|
||||
(acc, model) => {
|
||||
const providerName =
|
||||
model.provider?.name || model.provider?.requester || 'Unknown';
|
||||
if (!acc[providerName]) acc[providerName] = [];
|
||||
acc[providerName].push(model);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, EmbeddingModel[]>,
|
||||
);
|
||||
|
||||
const previewEmbeddingModelNames = [
|
||||
'text-embedding-3-large',
|
||||
'text-embedding-3-small',
|
||||
'bge-m3',
|
||||
'jina-embeddings-v3',
|
||||
'qwen3-embedding-8b',
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-md min-w-0">
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="min-w-0 bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue placeholder={t('knowledge.selectEmbeddingModel')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(groupedEmbeddingModels).map(
|
||||
([providerName, models]) => (
|
||||
<SelectGroup key={providerName}>
|
||||
<SelectLabel>{providerName}</SelectLabel>
|
||||
{models.map((model) => (
|
||||
<SelectItem key={model.uuid} value={model.uuid}>
|
||||
{model.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
<div className="flex w-full max-w-md min-w-0 items-center gap-1.5">
|
||||
<div className="min-w-0 flex-1">
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="min-w-0 bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue
|
||||
placeholder={t('knowledge.selectEmbeddingModel')}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(groupedEmbeddingModels).map(
|
||||
([providerName, models]) => (
|
||||
<SelectGroup key={providerName}>
|
||||
<SelectLabel>{providerName}</SelectLabel>
|
||||
{models.map((model) => (
|
||||
<SelectItem key={model.uuid} value={model.uuid}>
|
||||
{model.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
),
|
||||
)}
|
||||
{showSpaceLoginCTA ? (
|
||||
<SelectGroup>
|
||||
<SelectLabel>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<Sparkles className="h-3.5 w-3.5 text-purple-500" />
|
||||
{t('models.langbotModels')}
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
asChild
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
>
|
||||
<Info className="h-3 w-3 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-[240px]">
|
||||
{t('models.spaceTrialTooltip')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</SelectLabel>
|
||||
<div
|
||||
className="relative"
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
>
|
||||
{(spaceEmbeddingModels.length > 0
|
||||
? spaceEmbeddingModels.map((m) => m.name)
|
||||
: previewEmbeddingModelNames
|
||||
)
|
||||
.slice(0, 3)
|
||||
.map((name) => (
|
||||
<div
|
||||
key={name}
|
||||
className="relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm text-muted-foreground/60"
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
))}
|
||||
<div className="relative min-h-10">
|
||||
<div
|
||||
className="select-none overflow-hidden"
|
||||
style={{ maxHeight: '3rem' }}
|
||||
>
|
||||
{(spaceEmbeddingModels.length > 0
|
||||
? spaceEmbeddingModels.map((m) => m.name)
|
||||
: previewEmbeddingModelNames
|
||||
)
|
||||
.slice(3)
|
||||
.map((name) => (
|
||||
<div
|
||||
key={name}
|
||||
className="flex w-full items-center py-1.5 pl-8 pr-2 text-sm text-muted-foreground/40 blur-[2px]"
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-gradient-to-b from-transparent to-background/80">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7 text-xs px-3 gap-1.5 shadow-sm"
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleSpaceLogin();
|
||||
}}
|
||||
>
|
||||
<Sparkles className="h-3 w-3" />
|
||||
{t('models.unlockModels')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SelectGroup>
|
||||
),
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : !systemInfo.disable_models_service ? (
|
||||
Object.entries(groupedSpaceEmbeddingModels).map(
|
||||
([providerName, models]) => (
|
||||
<SelectGroup key={providerName}>
|
||||
<SelectLabel>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<Sparkles className="h-3.5 w-3.5 text-purple-500" />
|
||||
{providerName}
|
||||
</span>
|
||||
</SelectLabel>
|
||||
{models.map((model) => (
|
||||
<SelectItem key={model.uuid} value={model.uuid}>
|
||||
{model.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
),
|
||||
)
|
||||
) : null}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9 shrink-0"
|
||||
onClick={() => {
|
||||
setSettingsSection('models');
|
||||
setModelsDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Settings className="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">{t('models.title')}</TooltipContent>
|
||||
</Tooltip>
|
||||
<SettingsDialog
|
||||
open={modelsDialogOpen}
|
||||
onOpenChange={handleModelsDialogChange}
|
||||
section={settingsSection}
|
||||
onSectionChange={setSettingsSection}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case DynamicFormItemType.RERANK_MODEL_SELECTOR:
|
||||
const groupedRerankModels = rerankModels.reduce(
|
||||
@@ -736,10 +913,10 @@ export default function DynamicFormItemComponent({
|
||||
case DynamicFormItemType.MODEL_FALLBACK_SELECTOR: {
|
||||
// Separate space models from regular models
|
||||
const fbSpaceModels = llmModels.filter(
|
||||
(m) => m.provider?.requester === 'space-chat-completions',
|
||||
(m) => m.provider?.requester === LANGBOT_MODELS_PROVIDER_REQUESTER,
|
||||
);
|
||||
const fbRegularModels = llmModels.filter(
|
||||
(m) => m.provider?.requester !== 'space-chat-completions',
|
||||
(m) => m.provider?.requester !== LANGBOT_MODELS_PROVIDER_REQUESTER,
|
||||
);
|
||||
|
||||
// Group regular models by provider
|
||||
@@ -870,7 +1047,7 @@ export default function DynamicFormItemComponent({
|
||||
</div>
|
||||
))}
|
||||
{/* Blurred remaining models with login overlay */}
|
||||
<div className="relative">
|
||||
<div className="relative min-h-10">
|
||||
<div
|
||||
className="select-none overflow-hidden"
|
||||
style={{ maxHeight: '3rem' }}
|
||||
@@ -1467,6 +1644,32 @@ export default function DynamicFormItemComponent({
|
||||
</>
|
||||
);
|
||||
|
||||
case DynamicFormItemType.RICH_TOOLS_SELECTOR:
|
||||
return (
|
||||
<ToolResourceSelectors
|
||||
mode="tools"
|
||||
pipelineId={systemContext?.pipeline_id as string | undefined}
|
||||
value={{
|
||||
...(formValues || {}),
|
||||
[field.name]: field.value,
|
||||
}}
|
||||
onChange={handleCompositePatch}
|
||||
/>
|
||||
);
|
||||
|
||||
case DynamicFormItemType.RESOURCES_SELECTOR:
|
||||
return (
|
||||
<ToolResourceSelectors
|
||||
mode="resources"
|
||||
pipelineId={systemContext?.pipeline_id as string | undefined}
|
||||
value={{
|
||||
...(formValues || {}),
|
||||
[field.name]: field.value,
|
||||
}}
|
||||
onChange={handleCompositePatch}
|
||||
/>
|
||||
);
|
||||
|
||||
case DynamicFormItemType.PROMPT_EDITOR: {
|
||||
// Guard: field.value may be undefined when the form resets or
|
||||
// initialValues haven't propagated yet. Fall back to a default
|
||||
|
||||
@@ -68,6 +68,13 @@ export function getDefaultValues(
|
||||
return acc;
|
||||
}
|
||||
acc[item.name] = item.default;
|
||||
if (item.type === DynamicFormItemType.RICH_TOOLS_SELECTOR) {
|
||||
acc['enable-all-tools'] = true;
|
||||
}
|
||||
if (item.type === DynamicFormItemType.RESOURCES_SELECTOR) {
|
||||
acc['mcp-resources'] = [];
|
||||
acc['mcp-resource-agent-read-enabled'] = true;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,206 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { ImagePlus, Loader2, Paperclip, Send, X } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
|
||||
const MAX_ATTACHMENTS = 3;
|
||||
const MAX_IMAGE_BYTES = 1024 * 1024;
|
||||
|
||||
type FeedbackAttachment = {
|
||||
name: string;
|
||||
mime_type: string;
|
||||
data_url: string;
|
||||
};
|
||||
|
||||
function readImageFile(file: File): Promise<FeedbackAttachment> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
reject(new Error('not_image'));
|
||||
return;
|
||||
}
|
||||
if (file.size > MAX_IMAGE_BYTES) {
|
||||
reject(new Error('too_large'));
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const dataUrl = String(reader.result || '');
|
||||
if (!dataUrl.startsWith('data:image/')) {
|
||||
reject(new Error('not_image'));
|
||||
return;
|
||||
}
|
||||
resolve({
|
||||
name: file.name || 'pasted-image.png',
|
||||
mime_type: file.type || 'image/png',
|
||||
data_url: dataUrl,
|
||||
});
|
||||
};
|
||||
reader.onerror = () => reject(reader.error || new Error('read_failed'));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
const FEEDBACK_I18N_PREFIX = 'monitoring.feedback';
|
||||
|
||||
export function FeedbackPopoverContent({
|
||||
onSubmitted,
|
||||
}: {
|
||||
onSubmitted?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const tf = useCallback(
|
||||
(key: string) => t(`${FEEDBACK_I18N_PREFIX}.${key}`),
|
||||
[t],
|
||||
);
|
||||
const [content, setContent] = useState('');
|
||||
const [attachments, setAttachments] = useState<FeedbackAttachment[]>([]);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const addFiles = useCallback(
|
||||
async (files: File[]) => {
|
||||
const slots = MAX_ATTACHMENTS - attachments.length;
|
||||
if (slots <= 0) {
|
||||
toast.error(tf('tooManyImages'));
|
||||
return;
|
||||
}
|
||||
const picked = files.slice(0, slots);
|
||||
const next: FeedbackAttachment[] = [];
|
||||
for (const file of picked) {
|
||||
try {
|
||||
next.push(await readImageFile(file));
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : '';
|
||||
toast.error(
|
||||
msg === 'too_large' ? tf('imageTooLarge') : tf('imageOnly'),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (next.length > 0) {
|
||||
setAttachments((prev) => [...prev, ...next].slice(0, MAX_ATTACHMENTS));
|
||||
}
|
||||
},
|
||||
[attachments.length, tf],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const onPaste = (event: ClipboardEvent) => {
|
||||
const files = Array.from(event.clipboardData?.files || []).filter(
|
||||
(file) => file.type.startsWith('image/'),
|
||||
);
|
||||
if (files.length > 0) {
|
||||
event.preventDefault();
|
||||
void addFiles(files);
|
||||
}
|
||||
};
|
||||
window.addEventListener('paste', onPaste);
|
||||
return () => window.removeEventListener('paste', onPaste);
|
||||
}, [addFiles]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const trimmed = content.trim();
|
||||
if (!trimmed) {
|
||||
toast.error(tf('contentRequired'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setSubmitting(true);
|
||||
await httpClient.submitFeedback({
|
||||
content: trimmed,
|
||||
attachments,
|
||||
});
|
||||
toast.success(tf('submitSuccess'));
|
||||
setContent('');
|
||||
setAttachments([]);
|
||||
onSubmitted?.();
|
||||
} catch {
|
||||
toast.error(tf('submitFailed'));
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3" onClick={(e) => e.stopPropagation()}>
|
||||
<div>
|
||||
<div className="text-sm font-medium">{tf('title')}</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{tf('description')}
|
||||
</p>
|
||||
</div>
|
||||
<Textarea
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder={tf('placeholder')}
|
||||
maxLength={5000}
|
||||
className="min-h-32 resize-none text-sm"
|
||||
/>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{attachments.map((item, index) => (
|
||||
<div
|
||||
key={`${item.name}-${index}`}
|
||||
className="relative size-16 overflow-hidden rounded-md border"
|
||||
>
|
||||
<img
|
||||
src={item.data_url}
|
||||
alt={item.name}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setAttachments((prev) => prev.filter((_, i) => i !== index))
|
||||
}
|
||||
className="absolute right-1 top-1 rounded-full bg-black/60 p-0.5 text-white"
|
||||
aria-label={tf('removeImage')}
|
||||
>
|
||||
<X className="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
void addFiles(Array.from(e.target.files || []));
|
||||
e.target.value = '';
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<ImagePlus className="mr-1 size-4" />
|
||||
{tf('attachImage')}
|
||||
</Button>
|
||||
</div>
|
||||
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Paperclip className="size-3" />
|
||||
{attachments.length}/{MAX_ATTACHMENTS}
|
||||
</span>
|
||||
</div>
|
||||
<Button className="w-full" onClick={handleSubmit} disabled={submitting}>
|
||||
{submitting ? (
|
||||
<Loader2 className="mr-2 size-4 animate-spin" />
|
||||
) : (
|
||||
<Send className="mr-2 size-4" />
|
||||
)}
|
||||
{tf('submit')}
|
||||
</Button>
|
||||
<p className="text-[11px] leading-relaxed text-muted-foreground">
|
||||
{tf('privacyHint')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -103,6 +103,7 @@ import {
|
||||
} from '@/components/ui/popover';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useSidebarData, SidebarEntityItem } from './SidebarDataContext';
|
||||
import { FeedbackPopoverContent } from './FeedbackPopover';
|
||||
|
||||
// Compare two version strings, returns true if v1 > v2
|
||||
function compareVersions(v1: string, v2: string): boolean {
|
||||
@@ -1666,6 +1667,7 @@ export default function HomeSidebar({
|
||||
);
|
||||
const [hasNewVersion, setHasNewVersion] = useState(false);
|
||||
const [versionDialogOpen, setVersionDialogOpen] = useState(false);
|
||||
const [feedbackOpen, setFeedbackOpen] = useState(false);
|
||||
const [userEmail, setUserEmail] = useState<string>('');
|
||||
const [starCount, setStarCount] = useState<number | null>(null);
|
||||
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
||||
@@ -2138,10 +2140,8 @@ export default function HomeSidebar({
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
window.open(
|
||||
'https://github.com/langbot-app/LangBot/issues',
|
||||
'_blank',
|
||||
);
|
||||
setUserMenuOpen(false);
|
||||
setFeedbackOpen(true);
|
||||
}}
|
||||
>
|
||||
<Lightbulb />
|
||||
@@ -2193,6 +2193,18 @@ export default function HomeSidebar({
|
||||
</SidebarFooter>
|
||||
</Sidebar>
|
||||
|
||||
<Dialog open={feedbackOpen} onOpenChange={setFeedbackOpen}>
|
||||
<DialogContent className="w-[calc(100vw-2rem)] sm:max-w-[380px]">
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>{t('monitoring.feedback.title')}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('monitoring.feedback.description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<FeedbackPopoverContent onSubmitted={() => setFeedbackOpen(false)} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<SettingsDialog
|
||||
open={settingsOpen}
|
||||
onOpenChange={handleSettingsOpenChange}
|
||||
|
||||
@@ -45,6 +45,8 @@ import MCPReadme from '@/app/home/mcp/components/mcp-form/MCPReadme';
|
||||
import {
|
||||
MCPServerRuntimeInfo,
|
||||
MCPTool,
|
||||
MCPResource,
|
||||
MCPResourceContent,
|
||||
MCPServer,
|
||||
MCPSessionStatus,
|
||||
MCPServerExtraArgsRemote,
|
||||
@@ -244,13 +246,121 @@ function ToolsList({ tools, t }: { tools: MCPTool[]; t: TFunction }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ResourcesList({
|
||||
resources,
|
||||
serverName,
|
||||
t,
|
||||
}: {
|
||||
resources: MCPResource[];
|
||||
serverName: string;
|
||||
t: (key: string) => string;
|
||||
}) {
|
||||
const [expandedUri, setExpandedUri] = React.useState<string | null>(null);
|
||||
const [resourceContent, setResourceContent] =
|
||||
React.useState<MCPResourceContent | null>(null);
|
||||
const [loadingContent, setLoadingContent] = React.useState(false);
|
||||
|
||||
const handleToggleResource = async (uri: string) => {
|
||||
if (expandedUri === uri) {
|
||||
setExpandedUri(null);
|
||||
setResourceContent(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setExpandedUri(uri);
|
||||
setResourceContent(null);
|
||||
setLoadingContent(true);
|
||||
|
||||
try {
|
||||
const resp = await httpClient.readMCPServerResource(
|
||||
serverName,
|
||||
uri,
|
||||
65536,
|
||||
);
|
||||
if (resp.contents && resp.contents.length > 0) {
|
||||
setResourceContent(resp.contents[0]);
|
||||
}
|
||||
} catch {
|
||||
setResourceContent(null);
|
||||
} finally {
|
||||
setLoadingContent(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2 pb-6">
|
||||
{resources.map((resource, index) => (
|
||||
<Card key={index} className="py-3 shadow-none">
|
||||
<CardHeader
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleToggleResource(resource.uri)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-sm">{resource.name}</CardTitle>
|
||||
{resource.mime_type && (
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
{resource.mime_type}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{resource.description && (
|
||||
<CardDescription className="text-xs">
|
||||
{resource.description}
|
||||
</CardDescription>
|
||||
)}
|
||||
<div className="text-xs text-muted-foreground font-mono break-all">
|
||||
{resource.uri}
|
||||
</div>
|
||||
</CardHeader>
|
||||
{expandedUri === resource.uri && (
|
||||
<CardContent>
|
||||
{loadingContent ? (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t('mcp.loading')}
|
||||
</div>
|
||||
) : resourceContent?.type === 'text' && resourceContent.text ? (
|
||||
<div className="space-y-2">
|
||||
<pre className="text-xs bg-muted p-3 rounded-md overflow-auto max-h-[200px] whitespace-pre-wrap break-all">
|
||||
{resourceContent.text}
|
||||
</pre>
|
||||
{resourceContent.truncated && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t('mcp.resourceTruncated')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : resourceContent?.type === 'blob' ? (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{resourceContent.binary_omitted
|
||||
? t('mcp.resourceBinaryOmitted')
|
||||
: t('mcp.resourceBinaryContent')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t('mcp.resourceReadFailed')}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type RuntimePanelContent = 'all' | 'tools' | 'resources';
|
||||
|
||||
function RuntimePanel({
|
||||
mcpTesting,
|
||||
runtimeInfo,
|
||||
serverName,
|
||||
content = 'all',
|
||||
t,
|
||||
}: {
|
||||
mcpTesting: boolean;
|
||||
runtimeInfo: MCPServerRuntimeInfo | null;
|
||||
serverName: string;
|
||||
content?: RuntimePanelContent;
|
||||
t: TFunction;
|
||||
}) {
|
||||
// Show tools whenever we have runtime info — either an edit-mode server or a
|
||||
@@ -259,7 +369,9 @@ function RuntimePanel({
|
||||
if (!runtimeInfo) {
|
||||
return (
|
||||
<div className="flex min-h-[280px] items-center justify-center rounded-lg border border-dashed text-sm text-muted-foreground">
|
||||
{t('mcp.noToolsFound')}
|
||||
{content === 'resources'
|
||||
? t('mcp.noResourcesFound')
|
||||
: t('mcp.noToolsFound')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -267,6 +379,15 @@ function RuntimePanel({
|
||||
const isConnected =
|
||||
!mcpTesting && runtimeInfo.status === MCPSessionStatus.CONNECTED;
|
||||
const tools = runtimeInfo.tools || [];
|
||||
const resources = runtimeInfo.resources || [];
|
||||
const showTools = content === 'all' || content === 'tools';
|
||||
const showResources = content === 'all' || content === 'resources';
|
||||
const empty =
|
||||
content === 'tools'
|
||||
? tools.length === 0
|
||||
: content === 'resources'
|
||||
? resources.length === 0
|
||||
: tools.length === 0 && resources.length === 0;
|
||||
|
||||
return (
|
||||
<section className="space-y-4">
|
||||
@@ -276,11 +397,26 @@ function RuntimePanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isConnected && tools.length > 0 && <ToolsList tools={tools} t={t} />}
|
||||
{isConnected && showTools && tools.length > 0 && (
|
||||
<ToolsList tools={tools} t={t} />
|
||||
)}
|
||||
|
||||
{isConnected && tools.length === 0 && (
|
||||
{isConnected && showResources && resources.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{content === 'all' && (
|
||||
<div className="text-sm font-medium">
|
||||
{t('mcp.resourceCount', { count: resources.length })}
|
||||
</div>
|
||||
)}
|
||||
<ResourcesList resources={resources} serverName={serverName} t={t} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isConnected && empty && (
|
||||
<div className="flex min-h-[220px] items-center justify-center rounded-lg border border-dashed text-sm text-muted-foreground">
|
||||
{t('mcp.noToolsFound')}
|
||||
{content === 'resources'
|
||||
? t('mcp.noResourcesFound')
|
||||
: t('mcp.noToolsFound')}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
@@ -732,6 +868,8 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
||||
error_message: errorMsg,
|
||||
tool_count: 0,
|
||||
tools: [],
|
||||
resource_count: 0,
|
||||
resources: [],
|
||||
});
|
||||
} else {
|
||||
if (isEditMode) {
|
||||
@@ -1026,20 +1164,29 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
||||
);
|
||||
|
||||
const runtimePanel = (
|
||||
<RuntimePanel mcpTesting={mcpTesting} runtimeInfo={runtimeInfo} t={t} />
|
||||
<RuntimePanel
|
||||
mcpTesting={mcpTesting}
|
||||
runtimeInfo={runtimeInfo}
|
||||
serverName={form.getValues('name')}
|
||||
t={t}
|
||||
/>
|
||||
);
|
||||
|
||||
// In edit mode the right side shows a tablist switching between the live
|
||||
// Tools list and the Docs (README captured from LangBot Space at install).
|
||||
// Tools/resources lists and the Docs (README captured from LangBot Space at install).
|
||||
// Create mode has neither, so it falls back to the bare runtime placeholder.
|
||||
// The tool count lives in the tab label (only when connected); the panel
|
||||
// Counts live in the tab labels (only when connected); the panel
|
||||
// body itself no longer repeats a title/subtitle.
|
||||
const toolsConnected =
|
||||
const runtimeConnected =
|
||||
!mcpTesting && runtimeInfo?.status === MCPSessionStatus.CONNECTED;
|
||||
const toolsCount = runtimeInfo?.tools?.length ?? 0;
|
||||
const toolsTabLabel = toolsConnected
|
||||
const resourcesCount = runtimeInfo?.resources?.length ?? 0;
|
||||
const toolsTabLabel = runtimeConnected
|
||||
? `${t('mcp.tabTools')} ${toolsCount}`
|
||||
: t('mcp.tabTools');
|
||||
const resourcesTabLabel = runtimeConnected
|
||||
? `${t('mcp.tabResources')} ${resourcesCount}`
|
||||
: t('mcp.tabResources');
|
||||
|
||||
const detailPanel = isEditMode ? (
|
||||
<Tabs defaultValue="tools" className="flex h-full min-h-0 flex-col">
|
||||
@@ -1050,6 +1197,9 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
||||
<TabsTrigger value="tools" className="flex-none px-4">
|
||||
{toolsTabLabel}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="resources" className="flex-none px-4">
|
||||
{resourcesTabLabel}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="docs" className="mt-4 min-h-0 flex-1 overflow-y-auto">
|
||||
<MCPReadme readme={readme} />
|
||||
@@ -1058,7 +1208,25 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
||||
value="tools"
|
||||
className="mt-4 min-h-0 flex-1 overflow-y-auto"
|
||||
>
|
||||
{runtimePanel}
|
||||
<RuntimePanel
|
||||
mcpTesting={mcpTesting}
|
||||
runtimeInfo={runtimeInfo}
|
||||
serverName={form.getValues('name')}
|
||||
content="tools"
|
||||
t={t}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent
|
||||
value="resources"
|
||||
className="mt-4 min-h-0 flex-1 overflow-y-auto"
|
||||
>
|
||||
<RuntimePanel
|
||||
mcpTesting={mcpTesting}
|
||||
runtimeInfo={runtimeInfo}
|
||||
serverName={form.getValues('name')}
|
||||
content="resources"
|
||||
t={t}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
) : (
|
||||
|
||||
@@ -164,7 +164,7 @@ export default function TokenMonitoring({
|
||||
}, [fetchStats]);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (!stats) return [];
|
||||
if (!stats || !Array.isArray(stats.timeseries)) return [];
|
||||
return stats.timeseries.map((p) => ({
|
||||
bucket: p.bucket,
|
||||
input: p.input_tokens,
|
||||
@@ -198,7 +198,7 @@ export default function TokenMonitoring({
|
||||
);
|
||||
}
|
||||
|
||||
if (!stats || stats.summary.total_calls === 0) {
|
||||
if (!stats || !stats.summary || stats.summary.total_calls === 0) {
|
||||
return (
|
||||
<div className="bg-card rounded-xl border p-6">
|
||||
<div className="h-[260px] flex flex-col items-center justify-center text-muted-foreground gap-2">
|
||||
@@ -209,7 +209,8 @@ export default function TokenMonitoring({
|
||||
);
|
||||
}
|
||||
|
||||
const { summary, by_model } = stats;
|
||||
const summary = stats.summary;
|
||||
const by_model = Array.isArray(stats.by_model) ? stats.by_model : [];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
@@ -74,7 +74,7 @@ export default function SystemStatusCard({
|
||||
: await httpClient.getBoxSessions().catch(() => [] as BoxSessionInfo[]);
|
||||
setPluginStatus(plugin);
|
||||
setBoxStatus(box);
|
||||
setBoxSessions(sessions);
|
||||
setBoxSessions(Array.isArray(sessions) ? sessions : []);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -34,14 +34,16 @@ export default function TrafficChart({
|
||||
const { t } = useTranslation();
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (!messages.length && !llmCalls.length) {
|
||||
const safeMessages = Array.isArray(messages) ? messages : [];
|
||||
const safeLlmCalls = Array.isArray(llmCalls) ? llmCalls : [];
|
||||
if (!safeMessages.length && !safeLlmCalls.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Combine all timestamps and find the range
|
||||
const allTimestamps = [
|
||||
...messages.map((m) => m.timestamp.getTime()),
|
||||
...llmCalls.map((c) => c.timestamp.getTime()),
|
||||
...safeMessages.map((m) => m.timestamp.getTime()),
|
||||
...safeLlmCalls.map((c) => c.timestamp.getTime()),
|
||||
];
|
||||
|
||||
if (allTimestamps.length === 0) return [];
|
||||
@@ -99,7 +101,7 @@ export default function TrafficChart({
|
||||
}
|
||||
|
||||
// Count messages per bucket
|
||||
messages.forEach((msg) => {
|
||||
safeMessages.forEach((msg) => {
|
||||
const bucket =
|
||||
Math.floor(msg.timestamp.getTime() / bucketSize) * bucketSize;
|
||||
const point = buckets.get(bucket);
|
||||
@@ -109,7 +111,7 @@ export default function TrafficChart({
|
||||
});
|
||||
|
||||
// Count LLM calls per bucket
|
||||
llmCalls.forEach((call) => {
|
||||
safeLlmCalls.forEach((call) => {
|
||||
const bucket =
|
||||
Math.floor(call.timestamp.getTime() / bucketSize) * bucketSize;
|
||||
const point = buckets.get(bucket);
|
||||
|
||||
@@ -92,18 +92,46 @@ export function useMonitoringData(filterState: FilterState) {
|
||||
limit: 50,
|
||||
});
|
||||
|
||||
const overview = response?.overview ?? {
|
||||
total_messages: 0,
|
||||
llm_calls: 0,
|
||||
embedding_calls: 0,
|
||||
model_calls: 0,
|
||||
success_rate: 100,
|
||||
active_sessions: 0,
|
||||
};
|
||||
const messages = Array.isArray(response?.messages)
|
||||
? response.messages
|
||||
: [];
|
||||
const llmCalls = Array.isArray(response?.llmCalls)
|
||||
? response.llmCalls
|
||||
: [];
|
||||
const embeddingCalls = Array.isArray(response?.embeddingCalls)
|
||||
? response.embeddingCalls
|
||||
: [];
|
||||
const sessions = Array.isArray(response?.sessions)
|
||||
? response.sessions
|
||||
: [];
|
||||
const errors = Array.isArray(response?.errors) ? response.errors : [];
|
||||
const totalCount = response?.totalCount ?? {
|
||||
messages: messages.length,
|
||||
llmCalls: llmCalls.length,
|
||||
embeddingCalls: embeddingCalls.length,
|
||||
sessions: sessions.length,
|
||||
errors: errors.length,
|
||||
};
|
||||
|
||||
// Transform the response to match MonitoringData interface
|
||||
const transformedData: MonitoringData = {
|
||||
overview: {
|
||||
totalMessages: response.overview.total_messages,
|
||||
llmCalls: response.overview.llm_calls,
|
||||
embeddingCalls: response.overview.embedding_calls || 0,
|
||||
modelCalls:
|
||||
response.overview.model_calls || response.overview.llm_calls,
|
||||
successRate: response.overview.success_rate,
|
||||
activeSessions: response.overview.active_sessions,
|
||||
totalMessages: overview.total_messages,
|
||||
llmCalls: overview.llm_calls,
|
||||
embeddingCalls: overview.embedding_calls || 0,
|
||||
modelCalls: overview.model_calls || overview.llm_calls,
|
||||
successRate: overview.success_rate,
|
||||
activeSessions: overview.active_sessions,
|
||||
},
|
||||
messages: response.messages.map(
|
||||
messages: messages.map(
|
||||
(msg: {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
@@ -136,7 +164,7 @@ export function useMonitoringData(filterState: FilterState) {
|
||||
variables: msg.variables,
|
||||
}),
|
||||
),
|
||||
llmCalls: response.llmCalls.map(
|
||||
llmCalls: llmCalls.map(
|
||||
(call: {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
@@ -173,7 +201,7 @@ export function useMonitoringData(filterState: FilterState) {
|
||||
messageId: call.message_id,
|
||||
}),
|
||||
),
|
||||
embeddingCalls: (response.embeddingCalls || []).map(
|
||||
embeddingCalls: embeddingCalls.map(
|
||||
(call: {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
@@ -208,7 +236,7 @@ export function useMonitoringData(filterState: FilterState) {
|
||||
),
|
||||
// Create merged modelCalls array from llmCalls and embeddingCalls
|
||||
modelCalls: [] as ModelCall[], // Will be populated after transform
|
||||
sessions: response.sessions.map(
|
||||
sessions: sessions.map(
|
||||
(session: {
|
||||
session_id: string;
|
||||
bot_id: string;
|
||||
@@ -236,7 +264,7 @@ export function useMonitoringData(filterState: FilterState) {
|
||||
userId: session.user_id,
|
||||
}),
|
||||
),
|
||||
errors: response.errors.map(
|
||||
errors: errors.map(
|
||||
(error: {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
@@ -264,11 +292,11 @@ export function useMonitoringData(filterState: FilterState) {
|
||||
}),
|
||||
),
|
||||
totalCount: {
|
||||
messages: response.totalCount.messages,
|
||||
llmCalls: response.totalCount.llmCalls,
|
||||
embeddingCalls: response.totalCount.embeddingCalls || 0,
|
||||
sessions: response.totalCount.sessions,
|
||||
errors: response.totalCount.errors,
|
||||
messages: totalCount.messages,
|
||||
llmCalls: totalCount.llmCalls,
|
||||
embeddingCalls: totalCount.embeddingCalls || 0,
|
||||
sessions: totalCount.sessions,
|
||||
errors: totalCount.errors,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -12,16 +12,40 @@ import {
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Plus, X, Server, Wrench, Sparkles } from 'lucide-react';
|
||||
import { CircleHelp, Plus, X, Server, Wrench, Sparkles } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { Plugin } from '@/app/infra/entities/plugin';
|
||||
import { MCPServer, Skill } from '@/app/infra/entities/api';
|
||||
import PluginComponentList from '@/app/home/plugins/components/plugin-installed/PluginComponentList';
|
||||
import { BoxUnavailableNotice } from '@/app/home/components/BoxUnavailableNotice';
|
||||
import { useBoxStatus } from '@/app/infra/hooks/useBoxStatus';
|
||||
|
||||
function InfoTooltip({ label }: { label: string }) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex size-4 items-center justify-center rounded-full text-muted-foreground transition-colors hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
|
||||
aria-label={label}
|
||||
>
|
||||
<CircleHelp className="size-3.5" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-[280px]">
|
||||
{label}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PipelineExtension({
|
||||
pipelineId,
|
||||
}: {
|
||||
@@ -418,9 +442,14 @@ export default function PipelineExtension({
|
||||
{/* MCP Servers Section */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-foreground">
|
||||
{t('pipelines.extensions.mcpServersTitle')}
|
||||
</h3>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<h3 className="text-sm font-semibold text-foreground">
|
||||
{t('pipelines.extensions.mcpServersTitle')}
|
||||
</h3>
|
||||
<InfoTooltip
|
||||
label={t('pipelines.extensions.mcpServersScopeTooltip')}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Label
|
||||
htmlFor="enable-all-mcp-servers"
|
||||
@@ -428,6 +457,9 @@ export default function PipelineExtension({
|
||||
>
|
||||
{t('pipelines.extensions.enableAllMCPServers')}
|
||||
</Label>
|
||||
<InfoTooltip
|
||||
label={t('pipelines.extensions.enableAllMCPServersTooltip')}
|
||||
/>
|
||||
<Switch
|
||||
id="enable-all-mcp-servers"
|
||||
checked={enableAllMCPServers}
|
||||
|
||||
@@ -374,10 +374,12 @@ export default function PipelineFormComponent({
|
||||
const isLocalAgentRunner =
|
||||
stage.name === 'local-agent' ||
|
||||
stage.name === 'plugin:langbot/local-agent/default';
|
||||
const isLocalAgentStage = formName === 'ai' && isLocalAgentRunner;
|
||||
const stageSystemContext = isLocalAgentRunner
|
||||
? {
|
||||
box_available: boxAvailable,
|
||||
box_scope_editable: boxAvailable && !boxScopeForced,
|
||||
pipeline_id: pipelineId,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
@@ -424,10 +426,6 @@ export default function PipelineFormComponent({
|
||||
|
||||
const isPluginRunner =
|
||||
currentRunner && currentRunner.startsWith('plugin:');
|
||||
const stageSystemContext =
|
||||
stage.name === 'plugin:langbot/local-agent/default'
|
||||
? { box_available: boxAvailable }
|
||||
: undefined;
|
||||
if (isPluginRunner) {
|
||||
const runnerConfigs = (form.watch('ai.runner_config') as any) || {};
|
||||
const stageInitialValues = runnerConfigs[stage.name] || {};
|
||||
@@ -468,6 +466,11 @@ export default function PipelineFormComponent({
|
||||
// opt-in via ``disable_if`` + ``disabled_tooltip`` rather than every page
|
||||
// hard-coding a banner. Field-level gating keeps unrelated fields
|
||||
// untouched.
|
||||
// ``box_scope_editable`` folds the two reasons the Sandbox Scope selector
|
||||
// can be locked into a single flag the yaml ``disable_if`` consumes:
|
||||
// 1. Box sandbox is unavailable, or
|
||||
// 2. the deployment pins all pipelines to a fixed scope via
|
||||
// ``system.limitation.force_box_session_id_template`` (SaaS).
|
||||
// When the deployment pins every pipeline to a fixed sandbox scope (SaaS
|
||||
// ``force_box_session_id_template``), the Sandbox Scope selector is locked.
|
||||
// The runtime already overrides the scope on every exec, but the stored
|
||||
@@ -479,12 +482,26 @@ export default function PipelineFormComponent({
|
||||
const stageInitialValues: Record<string, any> =
|
||||
(form.watch(formName) as Record<string, any>)?.[stage.name] || {};
|
||||
const effectiveInitialValues =
|
||||
isLocalAgentRunner && boxScopeForced
|
||||
isLocalAgentStage && boxScopeForced
|
||||
? {
|
||||
...stageInitialValues,
|
||||
'box-session-id-template': forcedBoxTemplate,
|
||||
}
|
||||
: stageInitialValues;
|
||||
const emitStageValues = (values: object) => {
|
||||
if (!isLocalAgentStage) {
|
||||
handleDynamicFormEmit(formName, stage.name, values);
|
||||
return;
|
||||
}
|
||||
|
||||
const latestStageValues =
|
||||
((form.getValues(formName) as Record<string, any>) || {})[stage.name] ||
|
||||
{};
|
||||
handleDynamicFormEmit(formName, stage.name, {
|
||||
...latestStageValues,
|
||||
...values,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card key={stage.name}>
|
||||
@@ -500,9 +517,7 @@ export default function PipelineFormComponent({
|
||||
<DynamicFormComponent
|
||||
itemConfigList={stage.config}
|
||||
initialValues={effectiveInitialValues}
|
||||
onSubmit={(values) => {
|
||||
handleDynamicFormEmit(formName, stage.name, values);
|
||||
}}
|
||||
onSubmit={emitStageValues}
|
||||
systemContext={stageSystemContext}
|
||||
/>
|
||||
</CardContent>
|
||||
|
||||
@@ -618,6 +618,11 @@ export interface MCPServerRuntimeInfo {
|
||||
* server runs inside Box. Absent when Box is unavailable. */
|
||||
box_session_id?: string;
|
||||
box_enabled?: boolean;
|
||||
resource_count: number;
|
||||
resources: MCPResource[];
|
||||
resource_template_count?: number;
|
||||
resource_templates?: MCPResourceTemplate[];
|
||||
resource_capabilities?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type MCPServer =
|
||||
@@ -672,11 +677,67 @@ export interface MCPTool {
|
||||
parameters?: object;
|
||||
}
|
||||
|
||||
export interface MCPResource {
|
||||
uri: string;
|
||||
name: string;
|
||||
title?: string;
|
||||
description: string;
|
||||
mime_type: string;
|
||||
size?: number;
|
||||
icons?: object[];
|
||||
annotations?: Record<string, unknown>;
|
||||
_meta?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface MCPResourceTemplate {
|
||||
uri_template: string;
|
||||
name: string;
|
||||
title?: string;
|
||||
description: string;
|
||||
mime_type: string;
|
||||
icons?: object[];
|
||||
annotations?: Record<string, unknown>;
|
||||
_meta?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface MCPResourceContent {
|
||||
uri: string;
|
||||
mime_type: string;
|
||||
type: 'text' | 'blob';
|
||||
text?: string;
|
||||
blob?: string | null;
|
||||
bytes?: number;
|
||||
truncated?: boolean;
|
||||
binary_omitted?: boolean;
|
||||
_meta?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ApiRespMCPResources {
|
||||
resources: MCPResource[];
|
||||
resource_templates?: MCPResourceTemplate[];
|
||||
resource_capabilities?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ApiRespMCPResourceContents {
|
||||
contents: MCPResourceContent[];
|
||||
server_name?: string;
|
||||
server_uuid?: string;
|
||||
uri?: string;
|
||||
source?: string;
|
||||
bytes?: number;
|
||||
truncated?: boolean;
|
||||
cache_hit?: boolean;
|
||||
warnings?: string[];
|
||||
}
|
||||
|
||||
export interface PluginTool {
|
||||
name: string;
|
||||
description: string;
|
||||
human_desc: string;
|
||||
parameters: object;
|
||||
source?: 'builtin' | 'plugin' | 'mcp' | 'skill';
|
||||
source_name?: string;
|
||||
source_id?: string;
|
||||
}
|
||||
|
||||
export interface ApiRespTools {
|
||||
|
||||
@@ -67,6 +67,8 @@ export enum DynamicFormItemType {
|
||||
PLUGIN_SELECTOR = 'plugin-selector',
|
||||
BOT_SELECTOR = 'bot-selector',
|
||||
TOOLS_SELECTOR = 'tools-selector',
|
||||
RICH_TOOLS_SELECTOR = 'rich-tools-selector',
|
||||
RESOURCES_SELECTOR = 'resources-selector',
|
||||
WEBHOOK_URL = 'webhook-url',
|
||||
EMBED_CODE = 'embed-code',
|
||||
QR_CODE_LOGIN = 'qr-code-login',
|
||||
|
||||
@@ -44,6 +44,8 @@ import {
|
||||
BoxSessionInfo,
|
||||
ApiRespMCPServers,
|
||||
ApiRespMCPServer,
|
||||
ApiRespMCPResources,
|
||||
ApiRespMCPResourceContents,
|
||||
MCPServer,
|
||||
ApiRespModelProviders,
|
||||
ApiRespModelProvider,
|
||||
@@ -304,10 +306,20 @@ export class BackendClient extends BaseHttpClient {
|
||||
enable_all_plugins: boolean;
|
||||
enable_all_mcp_servers: boolean;
|
||||
enable_all_skills: boolean;
|
||||
mcp_resource_agent_read_enabled: boolean;
|
||||
bound_plugins: Array<{ author: string; name: string }>;
|
||||
available_plugins: Plugin[];
|
||||
bound_mcp_servers: string[];
|
||||
available_mcp_servers: MCPServer[];
|
||||
bound_mcp_resources: Array<{
|
||||
server_uuid?: string;
|
||||
server_name?: string;
|
||||
uri: string;
|
||||
mode?: string;
|
||||
enabled?: boolean;
|
||||
max_bytes?: number;
|
||||
max_tokens?: number;
|
||||
}>;
|
||||
bound_skills: string[];
|
||||
available_skills: Skill[];
|
||||
}> {
|
||||
@@ -322,15 +334,32 @@ export class BackendClient extends BaseHttpClient {
|
||||
enable_all_mcp_servers: boolean = true,
|
||||
bound_skills: string[] = [],
|
||||
enable_all_skills: boolean = true,
|
||||
bound_mcp_resources?: Array<{
|
||||
server_uuid?: string;
|
||||
server_name?: string;
|
||||
uri: string;
|
||||
mode?: string;
|
||||
enabled?: boolean;
|
||||
max_bytes?: number;
|
||||
max_tokens?: number;
|
||||
}>,
|
||||
mcp_resource_agent_read_enabled?: boolean,
|
||||
): Promise<object> {
|
||||
return this.put(`/api/v1/pipelines/${uuid}/extensions`, {
|
||||
const payload: Record<string, unknown> = {
|
||||
bound_plugins,
|
||||
bound_mcp_servers,
|
||||
enable_all_plugins,
|
||||
enable_all_mcp_servers,
|
||||
bound_skills,
|
||||
enable_all_skills,
|
||||
});
|
||||
};
|
||||
if (bound_mcp_resources !== undefined) {
|
||||
payload.bound_mcp_resources = bound_mcp_resources;
|
||||
}
|
||||
if (mcp_resource_agent_read_enabled !== undefined) {
|
||||
payload.mcp_resource_agent_read_enabled = mcp_resource_agent_read_enabled;
|
||||
}
|
||||
return this.put(`/api/v1/pipelines/${uuid}/extensions`, payload);
|
||||
}
|
||||
|
||||
// ============ WebSocket Chat API ============
|
||||
@@ -429,6 +458,27 @@ export class BackendClient extends BaseHttpClient {
|
||||
return this.delete(`/api/v1/platform/bots/${uuid}`);
|
||||
}
|
||||
|
||||
public getBotAdmins(botId: string): Promise<{
|
||||
admins: Array<{ id: number; launcher_type: string; launcher_id: string }>;
|
||||
}> {
|
||||
return this.get(`/api/v1/platform/bots/${botId}/admins`);
|
||||
}
|
||||
|
||||
public addBotAdmin(
|
||||
botId: string,
|
||||
launcher_type: string,
|
||||
launcher_id: string,
|
||||
): Promise<{ id: number }> {
|
||||
return this.post(`/api/v1/platform/bots/${botId}/admins`, {
|
||||
launcher_type,
|
||||
launcher_id,
|
||||
});
|
||||
}
|
||||
|
||||
public deleteBotAdmin(botId: string, adminId: number): Promise<object> {
|
||||
return this.delete(`/api/v1/platform/bots/${botId}/admins/${adminId}`);
|
||||
}
|
||||
|
||||
public getBotLogs(
|
||||
botId: string,
|
||||
request: GetBotLogsRequest,
|
||||
@@ -879,8 +929,11 @@ export class BackendClient extends BaseHttpClient {
|
||||
|
||||
// ========== Tools ==========
|
||||
|
||||
public getTools(): Promise<ApiRespTools> {
|
||||
return this.get('/api/v1/tools');
|
||||
public getTools(pipelineId?: string): Promise<ApiRespTools> {
|
||||
return this.get(
|
||||
'/api/v1/tools',
|
||||
pipelineId ? { pipeline_uuid: pipelineId } : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
public getToolDetail(toolName: string): Promise<ApiRespToolDetail> {
|
||||
@@ -940,6 +993,28 @@ export class BackendClient extends BaseHttpClient {
|
||||
return this.post('/api/v1/mcp/servers', { source });
|
||||
}
|
||||
|
||||
public getMCPServerResources(
|
||||
serverName: string,
|
||||
): Promise<ApiRespMCPResources> {
|
||||
return this.get(
|
||||
`/api/v1/mcp/servers/${encodeURIComponent(serverName)}/resources`,
|
||||
);
|
||||
}
|
||||
|
||||
public readMCPServerResource(
|
||||
serverName: string,
|
||||
uri: string,
|
||||
maxBytes?: number,
|
||||
): Promise<ApiRespMCPResourceContents> {
|
||||
return this.post(
|
||||
`/api/v1/mcp/servers/${encodeURIComponent(serverName)}/resources/read`,
|
||||
{
|
||||
uri,
|
||||
max_bytes: maxBytes,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ============ System API ============
|
||||
public getSystemInfo(): Promise<ApiRespSystemInfo> {
|
||||
return this.get('/api/v1/system/info');
|
||||
@@ -1367,6 +1442,17 @@ export class BackendClient extends BaseHttpClient {
|
||||
return this.post('/api/v1/survey/dismiss', { survey_id: surveyId });
|
||||
}
|
||||
|
||||
public submitFeedback(data: {
|
||||
content: string;
|
||||
attachments?: Array<{
|
||||
name: string;
|
||||
mime_type: string;
|
||||
data_url: string;
|
||||
}>;
|
||||
}): Promise<object> {
|
||||
return this.post('/api/v1/survey/feedback', data);
|
||||
}
|
||||
|
||||
// ============ Skills API ============
|
||||
|
||||
public getSkills(): Promise<ApiRespSkills> {
|
||||
|
||||
@@ -35,7 +35,7 @@ const enUS = {
|
||||
emptyPassword: 'Please enter your password',
|
||||
language: 'Language',
|
||||
helpDocs: 'Get Help',
|
||||
featureRequest: 'Feature Request',
|
||||
featureRequest: 'Feedback',
|
||||
starOnGitHub: 'Star on GitHub',
|
||||
joinDiscord: 'Join our Discord',
|
||||
create: 'Create',
|
||||
@@ -505,6 +505,26 @@ const enUS = {
|
||||
userMessage: 'User',
|
||||
botMessage: 'Assistant',
|
||||
},
|
||||
admins: {
|
||||
title: 'Admins',
|
||||
description:
|
||||
"Launchers (person/group IDs) that have admin privilege for this bot's commands",
|
||||
addAdmin: 'Add Admin',
|
||||
launcherType: 'Type',
|
||||
launcherId: 'ID',
|
||||
typePerson: 'Person',
|
||||
typeGroup: 'Group',
|
||||
placeholderId: 'User or group ID',
|
||||
addSuccess: 'Admin added',
|
||||
addError: 'Failed to add admin: ',
|
||||
deleteSuccess: 'Admin removed',
|
||||
deleteError: 'Failed to remove admin: ',
|
||||
noAdmins: 'No admins configured',
|
||||
setAdminTitle: 'Set as admin',
|
||||
adminBadge: 'Admin',
|
||||
configureAdmins: 'Manage Admins',
|
||||
removeAdminTitle: 'Remove admin',
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
title: 'Agent',
|
||||
@@ -916,7 +936,9 @@ const enUS = {
|
||||
toolsFound: 'tools',
|
||||
unknownError: 'Unknown error',
|
||||
noToolsFound: 'No tools found',
|
||||
noResourcesFound: 'No resources found',
|
||||
tabTools: 'Tools',
|
||||
tabResources: 'Resources',
|
||||
tabDocs: 'Docs',
|
||||
noReadme: 'No documentation available',
|
||||
parseResultFailed: 'Failed to parse test result',
|
||||
@@ -936,6 +958,11 @@ const enUS = {
|
||||
toolCount: 'Tools: {{count}}',
|
||||
parameterCount: 'Parameters: {{count}}',
|
||||
noParameters: 'No parameters',
|
||||
resourceCount: 'Resources: {{count}}',
|
||||
resourceBinaryContent: 'Binary content (cannot be displayed)',
|
||||
resourceBinaryOmitted: 'Binary content omitted by resource safety policy',
|
||||
resourceTruncated: 'Content truncated by byte or token limits',
|
||||
resourceReadFailed: 'Failed to read resource content',
|
||||
statusConnected: 'Connected',
|
||||
statusDisconnected: 'Disconnected',
|
||||
statusError: 'Connection Error',
|
||||
@@ -1027,9 +1054,12 @@ const enUS = {
|
||||
selectPlugins: 'Select Plugins',
|
||||
pluginsTitle: 'Plugins',
|
||||
mcpServersTitle: 'MCP Servers',
|
||||
mcpResourcesTitle: 'MCP Resources',
|
||||
noMCPServersSelected: 'No MCP servers selected',
|
||||
noMCPResourcesAvailable: 'No MCP resources available',
|
||||
addMCPServer: 'Add MCP Server',
|
||||
selectMCPServers: 'Select MCP Servers',
|
||||
enableMCPResourceAgentRead: 'Agent read',
|
||||
toolCount: '{{count}} tools',
|
||||
noPluginsInstalled: 'No installed plugins',
|
||||
noMCPServersConfigured: 'No configured MCP servers',
|
||||
@@ -1045,6 +1075,42 @@ const enUS = {
|
||||
addSkill: 'Add Skill',
|
||||
selectSkills: 'Select Skills',
|
||||
noSkillsAvailable: 'No skills available',
|
||||
mcpServersScopeTooltip:
|
||||
'This only controls which MCP servers are bound to the pipeline. Choose exact MCP tools and resources in AI Feature > Local Agent.',
|
||||
enableAllMCPServersTooltip:
|
||||
'When enabled, all configured and enabled MCP servers become candidates for MCP tools and resources in AI Feature.',
|
||||
},
|
||||
localAgent: {
|
||||
toolsTitle: 'Tools',
|
||||
toolsDescription:
|
||||
'Select plugin, MCP, skill, and built-in tools available to this Local Agent.',
|
||||
toolsScopeTooltip:
|
||||
'MCP tools only come from MCP servers bound in Extensions. Bind another MCP server there to make its tools selectable here.',
|
||||
enableAllTools: 'Enable all tools',
|
||||
allToolsEnabled: 'All available tools are enabled',
|
||||
noToolsSelected: 'No tools selected',
|
||||
editTools: 'Edit tools',
|
||||
builtinTools: 'Built-in tools',
|
||||
pluginTools: 'Plugin tools',
|
||||
skillTools: 'Skill tools',
|
||||
mcpTools: 'MCP tools',
|
||||
mcpToolsScopeTooltip:
|
||||
'Only tools from MCP servers currently allowed in Extensions are shown here.',
|
||||
skillToolsScopeTooltip:
|
||||
'Skill tools are available when LangBot skill service and the Box sandbox backend are ready. They let the agent activate or register skills.',
|
||||
selectTools: 'Select tools',
|
||||
resourcesTitle: 'Resources',
|
||||
resourcesDescription:
|
||||
'Select MCP resources and knowledge bases available to this Local Agent.',
|
||||
knowledgeBases: 'Knowledge bases',
|
||||
mcpResources: 'MCP resources',
|
||||
mcpResourcesScopeTooltip:
|
||||
'Only resources exposed by MCP servers currently allowed in Extensions are shown here.',
|
||||
enableMCPResourceRead: 'Allow model to read MCP resources',
|
||||
mcpResourceReadTooltip:
|
||||
'When disabled, selected MCP resources are not injected into model context.',
|
||||
noMCPResourcesAvailable: 'No MCP resources available',
|
||||
selectKnowledgeBases: 'Select knowledge bases',
|
||||
},
|
||||
debugDialog: {
|
||||
title: 'Pipeline Chat',
|
||||
@@ -1474,6 +1540,22 @@ const enUS = {
|
||||
inaccurateReasons: 'Inaccurate Reasons',
|
||||
platform: 'Platform',
|
||||
exportFeedback: 'Export Feedback',
|
||||
description:
|
||||
'Tell us what went wrong or what could be better. Instance UUID and login account are included for diagnosis.',
|
||||
placeholder: 'Describe your suggestion, issue, or reproduction steps...',
|
||||
attachImage: 'Add image',
|
||||
screenshot: 'Screenshot',
|
||||
submit: 'Submit feedback',
|
||||
privacyHint:
|
||||
'Do not include secrets, passwords, or private chat content.',
|
||||
contentRequired: 'Please enter feedback first',
|
||||
imageOnly: 'Only image attachments are supported',
|
||||
imageTooLarge: 'Each image must be under 1MB',
|
||||
tooManyImages: 'You can attach up to 3 images',
|
||||
screenshotFailed: 'Screenshot failed. Try pasting or uploading an image.',
|
||||
submitSuccess: 'Feedback submitted. Thanks!',
|
||||
submitFailed: 'Failed to submit feedback. Please try again later.',
|
||||
removeImage: 'Remove image',
|
||||
},
|
||||
queries: {
|
||||
title: 'Queries',
|
||||
|
||||
@@ -456,6 +456,26 @@ const esES = {
|
||||
userMessage: 'Usuario',
|
||||
botMessage: 'Asistente',
|
||||
},
|
||||
admins: {
|
||||
title: 'Admins',
|
||||
description:
|
||||
"Launchers (person/group IDs) that have admin privilege for this bot's commands",
|
||||
addAdmin: 'Add Admin',
|
||||
launcherType: 'Type',
|
||||
launcherId: 'ID',
|
||||
typePerson: 'Person',
|
||||
typeGroup: 'Group',
|
||||
placeholderId: 'User or group ID',
|
||||
addSuccess: 'Admin added',
|
||||
addError: 'Failed to add admin: ',
|
||||
deleteSuccess: 'Admin removed',
|
||||
deleteError: 'Failed to remove admin: ',
|
||||
noAdmins: 'No admins configured',
|
||||
setAdminTitle: 'Set as admin',
|
||||
removeAdminTitle: 'Remove admin',
|
||||
adminBadge: 'Admin',
|
||||
configureAdmins: 'Manage Admins',
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
title: 'Agent',
|
||||
@@ -871,7 +891,9 @@ const esES = {
|
||||
toolsFound: 'herramientas',
|
||||
unknownError: 'Error desconocido',
|
||||
noToolsFound: 'No se encontraron herramientas',
|
||||
noResourcesFound: 'No se encontraron recursos',
|
||||
tabTools: 'Herramientas',
|
||||
tabResources: 'Recursos',
|
||||
tabDocs: 'Documentación',
|
||||
noReadme: 'No hay documentación disponible',
|
||||
parseResultFailed: 'Error al analizar el resultado de la prueba',
|
||||
@@ -891,6 +913,12 @@ const esES = {
|
||||
toolCount: 'Herramientas: {{count}}',
|
||||
parameterCount: 'Parámetros: {{count}}',
|
||||
noParameters: 'Sin parámetros',
|
||||
resourceCount: 'Recursos: {{count}}',
|
||||
resourceBinaryContent: 'Contenido binario (no se puede mostrar)',
|
||||
resourceBinaryOmitted:
|
||||
'Contenido binario omitido por la política de seguridad de recursos',
|
||||
resourceTruncated: 'Contenido truncado por límites de bytes o tokens',
|
||||
resourceReadFailed: 'Error al leer el contenido del recurso',
|
||||
statusConnected: 'Conectado',
|
||||
statusDisconnected: 'Desconectado',
|
||||
statusError: 'Error de conexión',
|
||||
@@ -985,9 +1013,12 @@ const esES = {
|
||||
selectPlugins: 'Seleccionar plugins',
|
||||
pluginsTitle: 'Plugins',
|
||||
mcpServersTitle: 'Servidores MCP',
|
||||
mcpResourcesTitle: 'Recursos MCP',
|
||||
noMCPServersSelected: 'No hay servidores MCP seleccionados',
|
||||
noMCPResourcesAvailable: 'No hay recursos MCP disponibles',
|
||||
addMCPServer: 'Añadir servidor MCP',
|
||||
selectMCPServers: 'Seleccionar servidores MCP',
|
||||
enableMCPResourceAgentRead: 'Permitir lectura del modelo',
|
||||
toolCount: '{{count}} herramientas',
|
||||
noPluginsInstalled: 'No hay plugins instalados',
|
||||
noMCPServersConfigured: 'No hay servidores MCP configurados',
|
||||
@@ -1003,6 +1034,42 @@ const esES = {
|
||||
addSkill: 'Añadir skill',
|
||||
selectSkills: 'Seleccionar skills',
|
||||
noSkillsAvailable: 'No hay skills disponibles',
|
||||
mcpServersScopeTooltip:
|
||||
'Aquí solo se controla qué servidores MCP se vinculan al Pipeline. Las herramientas y recursos MCP concretos se eligen en AI Feature > Local Agent.',
|
||||
enableAllMCPServersTooltip:
|
||||
'Al activarlo, todos los servidores MCP configurados y habilitados serán candidatos para herramientas y recursos MCP en AI Feature.',
|
||||
},
|
||||
localAgent: {
|
||||
toolsTitle: 'Herramientas',
|
||||
toolsDescription:
|
||||
'Selecciona las herramientas de plugins, MCP e integradas disponibles para este Local Agent.',
|
||||
toolsScopeTooltip:
|
||||
'Las herramientas MCP solo provienen de servidores MCP vinculados en Extensiones. Vincula allí otro servidor para poder seleccionarlo aquí.',
|
||||
enableAllTools: 'Activar todas las herramientas',
|
||||
allToolsEnabled: 'Todas las herramientas disponibles están activadas',
|
||||
noToolsSelected: 'No hay herramientas seleccionadas',
|
||||
editTools: 'Editar herramientas',
|
||||
builtinTools: 'Herramientas integradas',
|
||||
pluginTools: 'Herramientas de plugin',
|
||||
skillTools: 'Herramientas de skill',
|
||||
mcpTools: 'Herramientas MCP',
|
||||
mcpToolsScopeTooltip:
|
||||
'Aquí solo se muestran herramientas de servidores MCP permitidos actualmente en Extensiones.',
|
||||
skillToolsScopeTooltip:
|
||||
'Las herramientas de skill aparecen cuando el servicio de skills de LangBot y el backend de sandbox Box están disponibles. Permiten al agente activar o registrar skills.',
|
||||
selectTools: 'Seleccionar herramientas',
|
||||
resourcesTitle: 'Recursos',
|
||||
resourcesDescription:
|
||||
'Selecciona los recursos MCP y bases de conocimiento disponibles para este Local Agent.',
|
||||
knowledgeBases: 'Bases de conocimiento',
|
||||
mcpResources: 'Recursos MCP',
|
||||
mcpResourcesScopeTooltip:
|
||||
'Aquí solo se muestran recursos expuestos por servidores MCP permitidos actualmente en Extensiones.',
|
||||
enableMCPResourceRead: 'Permitir que el modelo lea recursos MCP',
|
||||
mcpResourceReadTooltip:
|
||||
'Si se desactiva, los recursos MCP seleccionados no se inyectarán en el contexto del modelo.',
|
||||
noMCPResourcesAvailable: 'No hay recursos MCP disponibles',
|
||||
selectKnowledgeBases: 'Seleccionar bases de conocimiento',
|
||||
},
|
||||
debugDialog: {
|
||||
title: 'Chat del Pipeline',
|
||||
@@ -1448,6 +1515,22 @@ const esES = {
|
||||
inaccurateReasons: 'Razones de inexactitud',
|
||||
platform: 'Plataforma',
|
||||
exportFeedback: 'Exportar comentarios',
|
||||
description:
|
||||
'Tell us what went wrong or what could be better. Instance UUID and login account are included for diagnosis.',
|
||||
placeholder: 'Describe your suggestion, issue, or reproduction steps...',
|
||||
attachImage: 'Add image',
|
||||
screenshot: 'Screenshot',
|
||||
submit: 'Submit feedback',
|
||||
privacyHint:
|
||||
'Do not include secrets, passwords, or private chat content.',
|
||||
contentRequired: 'Please enter feedback first',
|
||||
imageOnly: 'Only image attachments are supported',
|
||||
imageTooLarge: 'Each image must be under 1MB',
|
||||
tooManyImages: 'You can attach up to 3 images',
|
||||
screenshotFailed: 'Screenshot failed. Try pasting or uploading an image.',
|
||||
submitSuccess: 'Feedback submitted. Thanks!',
|
||||
submitFailed: 'Failed to submit feedback. Please try again later.',
|
||||
removeImage: 'Remove image',
|
||||
},
|
||||
queries: {
|
||||
title: 'Consultas',
|
||||
|
||||
@@ -36,7 +36,7 @@ const jaJP = {
|
||||
emptyPassword: 'パスワードを入力してください',
|
||||
language: '言語',
|
||||
helpDocs: 'ヘルプドキュメント',
|
||||
featureRequest: '機能リクエスト',
|
||||
featureRequest: 'フィードバック',
|
||||
starOnGitHub: 'GitHubでStarする',
|
||||
joinDiscord: 'Discord に参加',
|
||||
create: '作成',
|
||||
@@ -489,6 +489,26 @@ const jaJP = {
|
||||
userMessage: 'ユーザー',
|
||||
botMessage: 'アシスタント',
|
||||
},
|
||||
admins: {
|
||||
title: 'Admins',
|
||||
description:
|
||||
"Launchers (person/group IDs) that have admin privilege for this bot's commands",
|
||||
addAdmin: 'Add Admin',
|
||||
launcherType: 'Type',
|
||||
launcherId: 'ID',
|
||||
typePerson: 'Person',
|
||||
typeGroup: 'Group',
|
||||
placeholderId: 'User or group ID',
|
||||
addSuccess: 'Admin added',
|
||||
addError: 'Failed to add admin: ',
|
||||
deleteSuccess: 'Admin removed',
|
||||
deleteError: 'Failed to remove admin: ',
|
||||
noAdmins: 'No admins configured',
|
||||
setAdminTitle: 'Set as admin',
|
||||
removeAdminTitle: 'Remove admin',
|
||||
adminBadge: 'Admin',
|
||||
configureAdmins: 'Manage Admins',
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
title: 'Agent',
|
||||
@@ -898,7 +918,9 @@ const jaJP = {
|
||||
toolsFound: '個のツール',
|
||||
unknownError: '不明なエラー',
|
||||
noToolsFound: 'ツールが見つかりません',
|
||||
noResourcesFound: 'リソースが見つかりません',
|
||||
tabTools: 'ツール',
|
||||
tabResources: 'リソース',
|
||||
tabDocs: 'ドキュメント',
|
||||
noReadme: 'ドキュメントがありません',
|
||||
parseResultFailed: 'テスト結果の解析に失敗しました',
|
||||
@@ -918,6 +940,12 @@ const jaJP = {
|
||||
toolCount: 'ツール:{{count}}',
|
||||
parameterCount: 'パラメータ:{{count}}',
|
||||
noParameters: 'パラメータなし',
|
||||
resourceCount: 'リソース:{{count}}',
|
||||
resourceBinaryContent: 'バイナリコンテンツ(表示できません)',
|
||||
resourceBinaryOmitted:
|
||||
'リソース安全ポリシーによりバイナリコンテンツを省略しました',
|
||||
resourceTruncated: 'バイトまたはトークンの上限により内容を切り詰めました',
|
||||
resourceReadFailed: 'リソースの読み込みに失敗しました',
|
||||
statusConnected: '接続済み',
|
||||
statusDisconnected: '未接続',
|
||||
statusError: '接続エラー',
|
||||
@@ -1007,9 +1035,12 @@ const jaJP = {
|
||||
selectPlugins: 'プラグインを選択',
|
||||
pluginsTitle: 'プラグイン',
|
||||
mcpServersTitle: 'MCPサーバー',
|
||||
mcpResourcesTitle: 'MCPリソース',
|
||||
noMCPServersSelected: 'MCPサーバーが選択されていません',
|
||||
noMCPResourcesAvailable: '利用可能なMCPリソースがありません',
|
||||
addMCPServer: 'MCPサーバーを追加',
|
||||
selectMCPServers: 'MCPサーバーを選択',
|
||||
enableMCPResourceAgentRead: 'モデルの読み取りを許可',
|
||||
toolCount: '{{count}}個のツール',
|
||||
noPluginsInstalled: 'インストールされているプラグインがありません',
|
||||
noMCPServersConfigured: '設定されているMCPサーバーがありません',
|
||||
@@ -1025,6 +1056,42 @@ const jaJP = {
|
||||
addSkill: 'スキルを追加',
|
||||
selectSkills: 'スキルを選択',
|
||||
noSkillsAvailable: '利用可能なスキルがありません',
|
||||
mcpServersScopeTooltip:
|
||||
'ここでは、このパイプラインに紐付ける MCP サーバーだけを管理します。個別の MCP ツールとリソースは AI 機能の Local Agent で選択します。',
|
||||
enableAllMCPServersTooltip:
|
||||
'有効にすると、設定済みで有効なすべての MCP サーバーが AI 機能の MCP ツールとリソース候補になります。',
|
||||
},
|
||||
localAgent: {
|
||||
toolsTitle: 'ツール',
|
||||
toolsDescription:
|
||||
'この Local Agent が使用できるプラグイン、MCP、組み込みツールを選択します。',
|
||||
toolsScopeTooltip:
|
||||
'MCP ツールは拡張機能で紐付けられた MCP サーバーからのみ表示されます。追加するには先に拡張機能でサーバーを紐付けてください。',
|
||||
enableAllTools: 'すべてのツールを有効化',
|
||||
allToolsEnabled: '利用可能なすべてのツールが有効です',
|
||||
noToolsSelected: 'ツールが選択されていません',
|
||||
editTools: 'ツールを編集',
|
||||
builtinTools: '組み込みツール',
|
||||
pluginTools: 'プラグインツール',
|
||||
skillTools: 'スキルツール',
|
||||
mcpTools: 'MCP ツール',
|
||||
mcpToolsScopeTooltip:
|
||||
'拡張機能で現在許可されている MCP サーバーのツールだけが表示されます。',
|
||||
skillToolsScopeTooltip:
|
||||
'スキルツールは LangBot のスキルサービスと Box サンドボックスバックエンドが利用可能なときに表示され、Agent がスキルを有効化または登録できるようにします。',
|
||||
selectTools: 'ツールを選択',
|
||||
resourcesTitle: 'リソース',
|
||||
resourcesDescription:
|
||||
'この Local Agent が読み取れる MCP リソースとナレッジベースを選択します。',
|
||||
knowledgeBases: 'ナレッジベース',
|
||||
mcpResources: 'MCP リソース',
|
||||
mcpResourcesScopeTooltip:
|
||||
'拡張機能で現在許可されている MCP サーバーのリソースだけが表示されます。',
|
||||
enableMCPResourceRead: 'モデルによる MCP リソース読み取りを許可',
|
||||
mcpResourceReadTooltip:
|
||||
'無効にすると、選択済みの MCP リソースもモデルコンテキストに注入されません。',
|
||||
noMCPResourcesAvailable: '利用可能な MCP リソースがありません',
|
||||
selectKnowledgeBases: 'ナレッジベースを選択',
|
||||
},
|
||||
debugDialog: {
|
||||
title: 'パイプラインのチャット',
|
||||
@@ -1456,6 +1523,22 @@ const jaJP = {
|
||||
inaccurateReasons: '不正確な理由',
|
||||
platform: 'プラットフォーム',
|
||||
exportFeedback: 'フィードバックをエクスポート',
|
||||
description:
|
||||
'問題点や改善案を教えてください。診断のため、インスタンス UUID、ログインアカウント、ページ情報も送信されます。',
|
||||
placeholder: '提案、問題、再現手順を入力してください...',
|
||||
attachImage: '画像を追加',
|
||||
screenshot: 'スクリーンショット',
|
||||
submit: '送信',
|
||||
privacyHint: '秘密鍵、パスワード、個人的な会話内容は含めないでください。',
|
||||
contentRequired: 'フィードバック内容を入力してください',
|
||||
imageOnly: '画像のみ添付できます',
|
||||
imageTooLarge: '画像は 1 枚 2MB 未満にしてください',
|
||||
tooManyImages: '画像は最大 3 枚まで添付できます',
|
||||
screenshotFailed:
|
||||
'スクリーンショットに失敗しました。貼り付けまたはアップロードを試してください。',
|
||||
submitSuccess: 'フィードバックを送信しました。ありがとうございます!',
|
||||
submitFailed: '送信に失敗しました。後でもう一度お試しください。',
|
||||
removeImage: '画像を削除',
|
||||
},
|
||||
messageDetails: {
|
||||
noData: 'このクエリにはLLM呼び出しやエラーがありません',
|
||||
|
||||
@@ -454,6 +454,26 @@ const ruRU = {
|
||||
userMessage: 'Пользователь',
|
||||
botMessage: 'Ассистент',
|
||||
},
|
||||
admins: {
|
||||
title: 'Admins',
|
||||
description:
|
||||
"Launchers (person/group IDs) that have admin privilege for this bot's commands",
|
||||
addAdmin: 'Add Admin',
|
||||
launcherType: 'Type',
|
||||
launcherId: 'ID',
|
||||
typePerson: 'Person',
|
||||
typeGroup: 'Group',
|
||||
placeholderId: 'User or group ID',
|
||||
addSuccess: 'Admin added',
|
||||
addError: 'Failed to add admin: ',
|
||||
deleteSuccess: 'Admin removed',
|
||||
deleteError: 'Failed to remove admin: ',
|
||||
noAdmins: 'No admins configured',
|
||||
setAdminTitle: 'Set as admin',
|
||||
removeAdminTitle: 'Remove admin',
|
||||
adminBadge: 'Admin',
|
||||
configureAdmins: 'Manage Admins',
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
title: 'Agent',
|
||||
@@ -865,7 +885,9 @@ const ruRU = {
|
||||
toolsFound: 'инструментов',
|
||||
unknownError: 'Неизвестная ошибка',
|
||||
noToolsFound: 'Инструменты не найдены',
|
||||
noResourcesFound: 'Ресурсы не найдены',
|
||||
tabTools: 'Инструменты',
|
||||
tabResources: 'Ресурсы',
|
||||
tabDocs: 'Документация',
|
||||
noReadme: 'Документация отсутствует',
|
||||
parseResultFailed: 'Не удалось разобрать результат теста',
|
||||
@@ -885,6 +907,12 @@ const ruRU = {
|
||||
toolCount: 'Инструменты: {{count}}',
|
||||
parameterCount: 'Параметры: {{count}}',
|
||||
noParameters: 'Нет параметров',
|
||||
resourceCount: 'Ресурсы: {{count}}',
|
||||
resourceBinaryContent: 'Двоичное содержимое (невозможно отобразить)',
|
||||
resourceBinaryOmitted:
|
||||
'Двоичное содержимое опущено согласно политике безопасности ресурсов',
|
||||
resourceTruncated: 'Содержимое усечено по лимитам байтов или токенов',
|
||||
resourceReadFailed: 'Не удалось прочитать содержимое ресурса',
|
||||
statusConnected: 'Подключён',
|
||||
statusDisconnected: 'Отключён',
|
||||
statusError: 'Ошибка подключения',
|
||||
@@ -975,9 +1003,12 @@ const ruRU = {
|
||||
selectPlugins: 'Выберите плагины',
|
||||
pluginsTitle: 'Плагины',
|
||||
mcpServersTitle: 'MCP-серверы',
|
||||
mcpResourcesTitle: 'MCP-ресурсы',
|
||||
noMCPServersSelected: 'MCP-серверы не выбраны',
|
||||
noMCPResourcesAvailable: 'Нет доступных MCP-ресурсов',
|
||||
addMCPServer: 'Добавить MCP-сервер',
|
||||
selectMCPServers: 'Выберите MCP-серверы',
|
||||
enableMCPResourceAgentRead: 'Разрешить модели чтение',
|
||||
toolCount: '{{count}} инструментов',
|
||||
noPluginsInstalled: 'Нет установленных плагинов',
|
||||
noMCPServersConfigured: 'Нет настроенных MCP-серверов',
|
||||
@@ -993,6 +1024,42 @@ const ruRU = {
|
||||
addSkill: 'Добавить навык',
|
||||
selectSkills: 'Выбрать навыки',
|
||||
noSkillsAvailable: 'Нет доступных навыков',
|
||||
mcpServersScopeTooltip:
|
||||
'Здесь задаётся только привязка MCP-серверов к конвейеру. Конкретные MCP-инструменты и ресурсы выбираются в AI Feature > Local Agent.',
|
||||
enableAllMCPServersTooltip:
|
||||
'Если включено, все настроенные и включённые MCP-серверы станут кандидатами для инструментов и ресурсов MCP в AI Feature.',
|
||||
},
|
||||
localAgent: {
|
||||
toolsTitle: 'Инструменты',
|
||||
toolsDescription:
|
||||
'Выберите инструменты плагинов, MCP и встроенные инструменты для этого Local Agent.',
|
||||
toolsScopeTooltip:
|
||||
'MCP-инструменты берутся только из MCP-серверов, привязанных в Расширениях. Чтобы добавить источник, сначала привяжите там сервер.',
|
||||
enableAllTools: 'Включить все инструменты',
|
||||
allToolsEnabled: 'Все доступные инструменты включены',
|
||||
noToolsSelected: 'Инструменты не выбраны',
|
||||
editTools: 'Редактировать инструменты',
|
||||
builtinTools: 'Встроенные инструменты',
|
||||
pluginTools: 'Инструменты плагинов',
|
||||
skillTools: 'Инструменты навыков',
|
||||
mcpTools: 'Инструменты MCP',
|
||||
mcpToolsScopeTooltip:
|
||||
'Здесь показаны только инструменты MCP-серверов, разрешённых сейчас в Расширениях.',
|
||||
skillToolsScopeTooltip:
|
||||
'Инструменты навыков доступны, когда сервис навыков LangBot и backend песочницы Box готовы. Они позволяют агенту активировать или регистрировать навыки.',
|
||||
selectTools: 'Выбрать инструменты',
|
||||
resourcesTitle: 'Ресурсы',
|
||||
resourcesDescription:
|
||||
'Выберите MCP-ресурсы и базы знаний для этого Local Agent.',
|
||||
knowledgeBases: 'Базы знаний',
|
||||
mcpResources: 'MCP-ресурсы',
|
||||
mcpResourcesScopeTooltip:
|
||||
'Здесь показаны только ресурсы MCP-серверов, разрешённых сейчас в Расширениях.',
|
||||
enableMCPResourceRead: 'Разрешить модели читать MCP-ресурсы',
|
||||
mcpResourceReadTooltip:
|
||||
'Если выключено, выбранные MCP-ресурсы не будут добавляться в контекст модели.',
|
||||
noMCPResourcesAvailable: 'Нет доступных MCP-ресурсов',
|
||||
selectKnowledgeBases: 'Выбрать базы знаний',
|
||||
},
|
||||
debugDialog: {
|
||||
title: 'Чат конвейера',
|
||||
@@ -1421,6 +1488,22 @@ const ruRU = {
|
||||
inaccurateReasons: 'Причины неточности',
|
||||
platform: 'Платформа',
|
||||
exportFeedback: 'Экспорт отзывов',
|
||||
description:
|
||||
'Tell us what went wrong or what could be better. Instance UUID and login account are included for diagnosis.',
|
||||
placeholder: 'Describe your suggestion, issue, or reproduction steps...',
|
||||
attachImage: 'Add image',
|
||||
screenshot: 'Screenshot',
|
||||
submit: 'Submit feedback',
|
||||
privacyHint:
|
||||
'Do not include secrets, passwords, or private chat content.',
|
||||
contentRequired: 'Please enter feedback first',
|
||||
imageOnly: 'Only image attachments are supported',
|
||||
imageTooLarge: 'Each image must be under 1MB',
|
||||
tooManyImages: 'You can attach up to 3 images',
|
||||
screenshotFailed: 'Screenshot failed. Try pasting or uploading an image.',
|
||||
submitSuccess: 'Feedback submitted. Thanks!',
|
||||
submitFailed: 'Failed to submit feedback. Please try again later.',
|
||||
removeImage: 'Remove image',
|
||||
},
|
||||
queries: {
|
||||
title: 'Запросы',
|
||||
|
||||
@@ -440,6 +440,26 @@ const thTH = {
|
||||
userMessage: 'ผู้ใช้',
|
||||
botMessage: 'ผู้ช่วย',
|
||||
},
|
||||
admins: {
|
||||
title: 'Admins',
|
||||
description:
|
||||
"Launchers (person/group IDs) that have admin privilege for this bot's commands",
|
||||
addAdmin: 'Add Admin',
|
||||
launcherType: 'Type',
|
||||
launcherId: 'ID',
|
||||
typePerson: 'Person',
|
||||
typeGroup: 'Group',
|
||||
placeholderId: 'User or group ID',
|
||||
addSuccess: 'Admin added',
|
||||
addError: 'Failed to add admin: ',
|
||||
deleteSuccess: 'Admin removed',
|
||||
deleteError: 'Failed to remove admin: ',
|
||||
noAdmins: 'No admins configured',
|
||||
setAdminTitle: 'Set as admin',
|
||||
removeAdminTitle: 'Remove admin',
|
||||
adminBadge: 'Admin',
|
||||
configureAdmins: 'Manage Admins',
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
title: 'Agent',
|
||||
@@ -843,7 +863,9 @@ const thTH = {
|
||||
toolsFound: 'เครื่องมือ',
|
||||
unknownError: 'ข้อผิดพลาดที่ไม่ทราบสาเหตุ',
|
||||
noToolsFound: 'ไม่พบเครื่องมือ',
|
||||
noResourcesFound: 'ไม่พบทรัพยากร',
|
||||
tabTools: 'เครื่องมือ',
|
||||
tabResources: 'ทรัพยากร',
|
||||
tabDocs: 'เอกสาร',
|
||||
noReadme: 'ไม่มีเอกสาร',
|
||||
parseResultFailed: 'ไม่สามารถแยกวิเคราะห์ผลการทดสอบได้',
|
||||
@@ -863,6 +885,11 @@ const thTH = {
|
||||
toolCount: 'เครื่องมือ: {{count}}',
|
||||
parameterCount: 'พารามิเตอร์: {{count}}',
|
||||
noParameters: 'ไม่มีพารามิเตอร์',
|
||||
resourceCount: 'ทรัพยากร: {{count}}',
|
||||
resourceBinaryContent: 'เนื้อหาไบนารี (ไม่สามารถแสดงได้)',
|
||||
resourceBinaryOmitted: 'ละเว้นเนื้อหาไบนารีตามนโยบายความปลอดภัยของทรัพยากร',
|
||||
resourceTruncated: 'ตัดเนื้อหาตามขีดจำกัดไบต์หรือโทเคน',
|
||||
resourceReadFailed: 'ไม่สามารถอ่านเนื้อหาทรัพยากรได้',
|
||||
statusConnected: 'เชื่อมต่อแล้ว',
|
||||
statusDisconnected: 'ไม่ได้เชื่อมต่อ',
|
||||
statusError: 'ข้อผิดพลาดการเชื่อมต่อ',
|
||||
@@ -952,9 +979,12 @@ const thTH = {
|
||||
selectPlugins: 'เลือกปลั๊กอิน',
|
||||
pluginsTitle: 'ปลั๊กอิน',
|
||||
mcpServersTitle: 'เซิร์ฟเวอร์ MCP',
|
||||
mcpResourcesTitle: 'ทรัพยากร MCP',
|
||||
noMCPServersSelected: 'ไม่ได้เลือกเซิร์ฟเวอร์ MCP',
|
||||
noMCPResourcesAvailable: 'ไม่มีทรัพยากร MCP ที่พร้อมใช้งาน',
|
||||
addMCPServer: 'เพิ่มเซิร์ฟเวอร์ MCP',
|
||||
selectMCPServers: 'เลือกเซิร์ฟเวอร์ MCP',
|
||||
enableMCPResourceAgentRead: 'อนุญาตให้โมเดลอ่าน',
|
||||
toolCount: '{{count}} เครื่องมือ',
|
||||
noPluginsInstalled: 'ไม่มีปลั๊กอินที่ติดตั้ง',
|
||||
noMCPServersConfigured: 'ไม่มีเซิร์ฟเวอร์ MCP ที่กำหนดค่า',
|
||||
@@ -970,6 +1000,42 @@ const thTH = {
|
||||
addSkill: 'เพิ่มสกิล',
|
||||
selectSkills: 'เลือกสกิล',
|
||||
noSkillsAvailable: 'ไม่มีสกิลที่พร้อมใช้งาน',
|
||||
mcpServersScopeTooltip:
|
||||
'ส่วนนี้ใช้ควบคุมว่า Pipeline ผูกกับเซิร์ฟเวอร์ MCP ใดเท่านั้น ส่วนเครื่องมือและทรัพยากร MCP รายตัวให้เลือกใน AI Feature > Local Agent',
|
||||
enableAllMCPServersTooltip:
|
||||
'เมื่อเปิดใช้ เซิร์ฟเวอร์ MCP ที่ตั้งค่าและเปิดใช้งานทั้งหมดจะเป็นตัวเลือกสำหรับเครื่องมือและทรัพยากร MCP ใน AI Feature',
|
||||
},
|
||||
localAgent: {
|
||||
toolsTitle: 'เครื่องมือ',
|
||||
toolsDescription:
|
||||
'เลือกเครื่องมือจากปลั๊กอิน MCP และเครื่องมือในตัวสำหรับ Local Agent นี้',
|
||||
toolsScopeTooltip:
|
||||
'เครื่องมือ MCP จะแสดงจากเซิร์ฟเวอร์ MCP ที่ผูกไว้ในส่วนขยายเท่านั้น หากต้องการเพิ่มแหล่งเครื่องมือ ให้ไปผูกเซิร์ฟเวอร์ที่นั่นก่อน',
|
||||
enableAllTools: 'เปิดใช้เครื่องมือทั้งหมด',
|
||||
allToolsEnabled: 'เปิดใช้เครื่องมือที่มีทั้งหมดแล้ว',
|
||||
noToolsSelected: 'ยังไม่ได้เลือกเครื่องมือ',
|
||||
editTools: 'แก้ไขเครื่องมือ',
|
||||
builtinTools: 'เครื่องมือในตัว',
|
||||
pluginTools: 'เครื่องมือปลั๊กอิน',
|
||||
skillTools: 'เครื่องมือสกิล',
|
||||
mcpTools: 'เครื่องมือ MCP',
|
||||
mcpToolsScopeTooltip:
|
||||
'ที่นี่จะแสดงเฉพาะเครื่องมือจากเซิร์ฟเวอร์ MCP ที่อนุญาตอยู่ในส่วนขยาย',
|
||||
skillToolsScopeTooltip:
|
||||
'เครื่องมือสกิลจะแสดงเมื่อบริการสกิลของ LangBot และแบ็กเอนด์แซนด์บ็อกซ์ Box พร้อมใช้งาน เพื่อให้ Agent เปิดใช้หรือลงทะเบียนสกิลได้',
|
||||
selectTools: 'เลือกเครื่องมือ',
|
||||
resourcesTitle: 'ทรัพยากร',
|
||||
resourcesDescription:
|
||||
'เลือกทรัพยากร MCP และคลังความรู้สำหรับ Local Agent นี้',
|
||||
knowledgeBases: 'คลังความรู้',
|
||||
mcpResources: 'ทรัพยากร MCP',
|
||||
mcpResourcesScopeTooltip:
|
||||
'ที่นี่จะแสดงเฉพาะทรัพยากรจากเซิร์ฟเวอร์ MCP ที่อนุญาตอยู่ในส่วนขยาย',
|
||||
enableMCPResourceRead: 'อนุญาตให้โมเดลอ่านทรัพยากร MCP',
|
||||
mcpResourceReadTooltip:
|
||||
'เมื่อปิด ทรัพยากร MCP ที่เลือกไว้จะไม่ถูกใส่เข้าไปในบริบทของโมเดล',
|
||||
noMCPResourcesAvailable: 'ไม่มีทรัพยากร MCP ที่พร้อมใช้งาน',
|
||||
selectKnowledgeBases: 'เลือกคลังความรู้',
|
||||
},
|
||||
debugDialog: {
|
||||
title: 'แชท Pipeline',
|
||||
@@ -1390,6 +1456,22 @@ const thTH = {
|
||||
inaccurateReasons: 'เหตุผลที่ไม่ถูกต้อง',
|
||||
platform: 'แพลตฟอร์ม',
|
||||
exportFeedback: 'ส่งออกความคิดเห็น',
|
||||
description:
|
||||
'Tell us what went wrong or what could be better. Instance UUID and login account are included for diagnosis.',
|
||||
placeholder: 'Describe your suggestion, issue, or reproduction steps...',
|
||||
attachImage: 'Add image',
|
||||
screenshot: 'Screenshot',
|
||||
submit: 'Submit feedback',
|
||||
privacyHint:
|
||||
'Do not include secrets, passwords, or private chat content.',
|
||||
contentRequired: 'Please enter feedback first',
|
||||
imageOnly: 'Only image attachments are supported',
|
||||
imageTooLarge: 'Each image must be under 1MB',
|
||||
tooManyImages: 'You can attach up to 3 images',
|
||||
screenshotFailed: 'Screenshot failed. Try pasting or uploading an image.',
|
||||
submitSuccess: 'Feedback submitted. Thanks!',
|
||||
submitFailed: 'Failed to submit feedback. Please try again later.',
|
||||
removeImage: 'Remove image',
|
||||
},
|
||||
queries: {
|
||||
title: 'คำค้นหา',
|
||||
|
||||
@@ -450,6 +450,26 @@ const viVN = {
|
||||
userMessage: 'Người dùng',
|
||||
botMessage: 'Trợ lý',
|
||||
},
|
||||
admins: {
|
||||
title: 'Admins',
|
||||
description:
|
||||
"Launchers (person/group IDs) that have admin privilege for this bot's commands",
|
||||
addAdmin: 'Add Admin',
|
||||
launcherType: 'Type',
|
||||
launcherId: 'ID',
|
||||
typePerson: 'Person',
|
||||
typeGroup: 'Group',
|
||||
placeholderId: 'User or group ID',
|
||||
addSuccess: 'Admin added',
|
||||
addError: 'Failed to add admin: ',
|
||||
deleteSuccess: 'Admin removed',
|
||||
deleteError: 'Failed to remove admin: ',
|
||||
noAdmins: 'No admins configured',
|
||||
setAdminTitle: 'Set as admin',
|
||||
removeAdminTitle: 'Remove admin',
|
||||
adminBadge: 'Admin',
|
||||
configureAdmins: 'Manage Admins',
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
title: 'Agent',
|
||||
@@ -858,7 +878,9 @@ const viVN = {
|
||||
toolsFound: 'công cụ',
|
||||
unknownError: 'Lỗi không xác định',
|
||||
noToolsFound: 'Không tìm thấy công cụ nào',
|
||||
noResourcesFound: 'Không tìm thấy tài nguyên nào',
|
||||
tabTools: 'Công cụ',
|
||||
tabResources: 'Tài nguyên',
|
||||
tabDocs: 'Tài liệu',
|
||||
noReadme: 'Không có tài liệu',
|
||||
parseResultFailed: 'Phân tích kết quả kiểm tra thất bại',
|
||||
@@ -878,6 +900,12 @@ const viVN = {
|
||||
toolCount: 'Công cụ: {{count}}',
|
||||
parameterCount: 'Tham số: {{count}}',
|
||||
noParameters: 'Không có tham số',
|
||||
resourceCount: 'Tài nguyên: {{count}}',
|
||||
resourceBinaryContent: 'Nội dung nhị phân (không thể hiển thị)',
|
||||
resourceBinaryOmitted:
|
||||
'Nội dung nhị phân đã bị lược bỏ theo chính sách an toàn tài nguyên',
|
||||
resourceTruncated: 'Nội dung đã bị cắt theo giới hạn byte hoặc token',
|
||||
resourceReadFailed: 'Không thể đọc nội dung tài nguyên',
|
||||
statusConnected: 'Đã kết nối',
|
||||
statusDisconnected: 'Đã ngắt kết nối',
|
||||
statusError: 'Lỗi kết nối',
|
||||
@@ -967,9 +995,12 @@ const viVN = {
|
||||
selectPlugins: 'Chọn Plugin',
|
||||
pluginsTitle: 'Plugin',
|
||||
mcpServersTitle: 'Máy chủ MCP',
|
||||
mcpResourcesTitle: 'Tài nguyên MCP',
|
||||
noMCPServersSelected: 'Chưa chọn máy chủ MCP nào',
|
||||
noMCPResourcesAvailable: 'Không có tài nguyên MCP nào',
|
||||
addMCPServer: 'Thêm máy chủ MCP',
|
||||
selectMCPServers: 'Chọn máy chủ MCP',
|
||||
enableMCPResourceAgentRead: 'Cho phép mô hình đọc',
|
||||
toolCount: '{{count}} công cụ',
|
||||
noPluginsInstalled: 'Chưa cài đặt plugin nào',
|
||||
noMCPServersConfigured: 'Chưa cấu hình máy chủ MCP nào',
|
||||
@@ -985,6 +1016,42 @@ const viVN = {
|
||||
addSkill: 'Thêm kỹ năng',
|
||||
selectSkills: 'Chọn kỹ năng',
|
||||
noSkillsAvailable: 'Không có kỹ năng khả dụng',
|
||||
mcpServersScopeTooltip:
|
||||
'Tại đây chỉ kiểm soát máy chủ MCP được liên kết với Pipeline. Công cụ và tài nguyên MCP cụ thể được chọn trong AI Feature > Local Agent.',
|
||||
enableAllMCPServersTooltip:
|
||||
'Khi bật, mọi máy chủ MCP đã cấu hình và bật sẽ trở thành ứng viên cho công cụ và tài nguyên MCP trong AI Feature.',
|
||||
},
|
||||
localAgent: {
|
||||
toolsTitle: 'Công cụ',
|
||||
toolsDescription:
|
||||
'Chọn công cụ plugin, MCP và công cụ tích hợp sẵn cho Local Agent này.',
|
||||
toolsScopeTooltip:
|
||||
'Công cụ MCP chỉ đến từ máy chủ MCP đã liên kết trong Tiện ích mở rộng. Hãy liên kết máy chủ tại đó trước nếu muốn chọn thêm tại đây.',
|
||||
enableAllTools: 'Bật tất cả công cụ',
|
||||
allToolsEnabled: 'Tất cả công cụ khả dụng đã được bật',
|
||||
noToolsSelected: 'Chưa chọn công cụ nào',
|
||||
editTools: 'Sửa công cụ',
|
||||
builtinTools: 'Công cụ tích hợp sẵn',
|
||||
pluginTools: 'Công cụ plugin',
|
||||
skillTools: 'Công cụ kỹ năng',
|
||||
mcpTools: 'Công cụ MCP',
|
||||
mcpToolsScopeTooltip:
|
||||
'Tại đây chỉ hiển thị công cụ từ máy chủ MCP hiện được cho phép trong Tiện ích mở rộng.',
|
||||
skillToolsScopeTooltip:
|
||||
'Công cụ kỹ năng khả dụng khi dịch vụ kỹ năng LangBot và backend sandbox Box đã sẵn sàng. Chúng cho phép Agent kích hoạt hoặc đăng ký kỹ năng.',
|
||||
selectTools: 'Chọn công cụ',
|
||||
resourcesTitle: 'Tài nguyên',
|
||||
resourcesDescription:
|
||||
'Chọn tài nguyên MCP và kho tri thức cho Local Agent này.',
|
||||
knowledgeBases: 'Kho tri thức',
|
||||
mcpResources: 'Tài nguyên MCP',
|
||||
mcpResourcesScopeTooltip:
|
||||
'Tại đây chỉ hiển thị tài nguyên từ máy chủ MCP hiện được cho phép trong Tiện ích mở rộng.',
|
||||
enableMCPResourceRead: 'Cho phép mô hình đọc tài nguyên MCP',
|
||||
mcpResourceReadTooltip:
|
||||
'Khi tắt, tài nguyên MCP đã chọn sẽ không được đưa vào ngữ cảnh mô hình.',
|
||||
noMCPResourcesAvailable: 'Không có tài nguyên MCP nào',
|
||||
selectKnowledgeBases: 'Chọn kho tri thức',
|
||||
},
|
||||
debugDialog: {
|
||||
title: 'Trò chuyện Pipeline',
|
||||
@@ -1414,6 +1481,22 @@ const viVN = {
|
||||
inaccurateReasons: 'Lý do không chính xác',
|
||||
platform: 'Nền tảng',
|
||||
exportFeedback: 'Xuất phản hồi',
|
||||
description:
|
||||
'Tell us what went wrong or what could be better. Instance UUID and login account are included for diagnosis.',
|
||||
placeholder: 'Describe your suggestion, issue, or reproduction steps...',
|
||||
attachImage: 'Add image',
|
||||
screenshot: 'Screenshot',
|
||||
submit: 'Submit feedback',
|
||||
privacyHint:
|
||||
'Do not include secrets, passwords, or private chat content.',
|
||||
contentRequired: 'Please enter feedback first',
|
||||
imageOnly: 'Only image attachments are supported',
|
||||
imageTooLarge: 'Each image must be under 1MB',
|
||||
tooManyImages: 'You can attach up to 3 images',
|
||||
screenshotFailed: 'Screenshot failed. Try pasting or uploading an image.',
|
||||
submitSuccess: 'Feedback submitted. Thanks!',
|
||||
submitFailed: 'Failed to submit feedback. Please try again later.',
|
||||
removeImage: 'Remove image',
|
||||
},
|
||||
queries: {
|
||||
title: 'Truy vấn',
|
||||
|
||||
@@ -34,7 +34,7 @@ const zhHans = {
|
||||
emptyPassword: '请输入密码',
|
||||
language: '语言',
|
||||
helpDocs: '帮助文档',
|
||||
featureRequest: '需求建议',
|
||||
featureRequest: '建议反馈',
|
||||
starOnGitHub: '在 GitHub 上 Star',
|
||||
joinDiscord: '加入 Discord 社区',
|
||||
create: '创建',
|
||||
@@ -486,6 +486,25 @@ const zhHans = {
|
||||
userMessage: '用户',
|
||||
botMessage: '助手',
|
||||
},
|
||||
admins: {
|
||||
title: '管理员',
|
||||
description: '拥有此机器人命令管理员权限的会话(用户/群组 ID)',
|
||||
addAdmin: '添加管理员',
|
||||
launcherType: '类型',
|
||||
launcherId: 'ID',
|
||||
typePerson: '私聊',
|
||||
typeGroup: '群聊',
|
||||
placeholderId: '用户或群组 ID',
|
||||
addSuccess: '添加成功',
|
||||
addError: '添加失败:',
|
||||
deleteSuccess: '已移除',
|
||||
deleteError: '移除失败:',
|
||||
noAdmins: '暂无管理员',
|
||||
setAdminTitle: '设为管理员',
|
||||
adminBadge: '管理员',
|
||||
configureAdmins: '配置管理员',
|
||||
removeAdminTitle: '移除管理员权限',
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
title: 'Agent',
|
||||
@@ -878,7 +897,9 @@ const zhHans = {
|
||||
toolsFound: '个工具',
|
||||
unknownError: '未知错误',
|
||||
noToolsFound: '未找到任何工具',
|
||||
noResourcesFound: '未找到任何资源',
|
||||
tabTools: '工具',
|
||||
tabResources: '资源',
|
||||
tabDocs: '文档',
|
||||
noReadme: '暂无文档',
|
||||
parseResultFailed: '解析测试结果失败',
|
||||
@@ -898,6 +919,11 @@ const zhHans = {
|
||||
toolCount: '工具:{{count}}',
|
||||
parameterCount: '参数:{{count}}',
|
||||
noParameters: '无参数',
|
||||
resourceCount: '资源:{{count}}',
|
||||
resourceBinaryContent: '二进制内容(无法显示)',
|
||||
resourceBinaryOmitted: '二进制内容已按资源安全策略省略',
|
||||
resourceTruncated: '内容已按字节或 token 限制截断',
|
||||
resourceReadFailed: '读取资源内容失败',
|
||||
statusConnected: '已连接',
|
||||
statusDisconnected: '未连接',
|
||||
statusError: '连接错误',
|
||||
@@ -983,9 +1009,12 @@ const zhHans = {
|
||||
selectPlugins: '选择插件',
|
||||
pluginsTitle: '插件',
|
||||
mcpServersTitle: 'MCP 服务器',
|
||||
mcpResourcesTitle: 'MCP 资源',
|
||||
noMCPServersSelected: '未选择任何 MCP 服务器',
|
||||
noMCPResourcesAvailable: '暂无可用 MCP 资源',
|
||||
addMCPServer: '添加 MCP 服务器',
|
||||
selectMCPServers: '选择 MCP 服务器',
|
||||
enableMCPResourceAgentRead: '允许模型读取',
|
||||
toolCount: '{{count}} 个工具',
|
||||
noPluginsInstalled: '无已安装的插件',
|
||||
noMCPServersConfigured: '无已配置的 MCP 服务器',
|
||||
@@ -1001,6 +1030,41 @@ const zhHans = {
|
||||
addSkill: '添加技能',
|
||||
selectSkills: '选择技能',
|
||||
noSkillsAvailable: '暂无可用技能',
|
||||
mcpServersScopeTooltip:
|
||||
'这里仅控制此流水线绑定哪些 MCP 服务器;具体 MCP 工具和资源在 AI 能力的内置 Agent 表单中选择。',
|
||||
enableAllMCPServersTooltip:
|
||||
'开启后,所有已配置且启用的 MCP 服务器都会进入 AI 能力里的 MCP 工具和资源候选范围。',
|
||||
},
|
||||
localAgent: {
|
||||
toolsTitle: '工具',
|
||||
toolsDescription:
|
||||
'选择此内置 Agent 可以调用的插件、MCP、技能和内置工具。',
|
||||
toolsScopeTooltip:
|
||||
'MCP 工具只会从扩展集成中已绑定的 MCP 服务器里出现;如需增加 MCP 工具来源,请先到扩展集成绑定对应服务器。',
|
||||
enableAllTools: '启用所有工具',
|
||||
allToolsEnabled: '已启用所有可用工具',
|
||||
noToolsSelected: '未选择任何工具',
|
||||
editTools: '编辑工具',
|
||||
builtinTools: '内置工具',
|
||||
pluginTools: '插件工具',
|
||||
skillTools: '技能工具',
|
||||
mcpTools: 'MCP 工具',
|
||||
mcpToolsScopeTooltip:
|
||||
'这里仅展示扩展集成当前允许的 MCP 服务器提供的工具。',
|
||||
skillToolsScopeTooltip:
|
||||
'技能工具会在 LangBot 技能服务和 Box 沙箱后端可用时出现,用于让 Agent 激活或注册技能。',
|
||||
selectTools: '选择工具',
|
||||
resourcesTitle: '资源',
|
||||
resourcesDescription: '选择此内置 Agent 可以读取的 MCP 资源和知识库。',
|
||||
knowledgeBases: '知识库',
|
||||
mcpResources: 'MCP 资源',
|
||||
mcpResourcesScopeTooltip:
|
||||
'这里仅展示扩展集成当前允许的 MCP 服务器暴露的资源。',
|
||||
enableMCPResourceRead: '允许模型读取 MCP 资源',
|
||||
mcpResourceReadTooltip:
|
||||
'关闭后,即使已选择资源,也不会把 MCP 资源内容注入给模型。',
|
||||
noMCPResourcesAvailable: '暂无可用 MCP 资源',
|
||||
selectKnowledgeBases: '选择知识库',
|
||||
},
|
||||
debugDialog: {
|
||||
title: '流水线对话',
|
||||
@@ -1408,6 +1472,21 @@ const zhHans = {
|
||||
inaccurateReasons: '不准确原因',
|
||||
platform: '平台',
|
||||
exportFeedback: '导出反馈',
|
||||
description:
|
||||
'告诉我们遇到的问题或想要的改进。提交时会附带实例 UUID 和登录账号,方便定位。',
|
||||
placeholder: '请描述你的建议、问题或复现步骤...',
|
||||
attachImage: '添加图片',
|
||||
screenshot: '截图',
|
||||
submit: '提交反馈',
|
||||
privacyHint: '请勿提交敏感密钥、密码或私人聊天内容。',
|
||||
contentRequired: '请先填写反馈内容',
|
||||
imageOnly: '仅支持图片附件',
|
||||
imageTooLarge: '单张图片不能超过 1MB',
|
||||
tooManyImages: '最多添加 3 张图片',
|
||||
screenshotFailed: '截图失败,请尝试粘贴或上传图片',
|
||||
submitSuccess: '反馈已提交,感谢!',
|
||||
submitFailed: '反馈提交失败,请稍后重试',
|
||||
removeImage: '移除图片',
|
||||
},
|
||||
queries: {
|
||||
title: '查询记录',
|
||||
|
||||
@@ -426,6 +426,25 @@ const zhHant = {
|
||||
userMessage: '使用者',
|
||||
botMessage: '助手',
|
||||
},
|
||||
admins: {
|
||||
title: '管理員',
|
||||
description: '擁有此機器人指令管理員權限的會話(使用者/群組 ID)',
|
||||
addAdmin: '新增管理員',
|
||||
launcherType: '類型',
|
||||
launcherId: 'ID',
|
||||
typePerson: '私聊',
|
||||
typeGroup: '群組',
|
||||
placeholderId: '使用者或群組 ID',
|
||||
addSuccess: '新增成功',
|
||||
addError: '新增失敗:',
|
||||
deleteSuccess: '已移除',
|
||||
deleteError: '移除失敗:',
|
||||
noAdmins: '尚無管理員',
|
||||
setAdminTitle: '設為管理員',
|
||||
removeAdminTitle: '移除管理員權限',
|
||||
adminBadge: '管理員',
|
||||
configureAdmins: '設定管理員',
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
title: 'Agent',
|
||||
@@ -817,7 +836,9 @@ const zhHant = {
|
||||
toolsFound: '個工具',
|
||||
unknownError: '未知錯誤',
|
||||
noToolsFound: '未找到任何工具',
|
||||
noResourcesFound: '未找到任何資源',
|
||||
tabTools: '工具',
|
||||
tabResources: '資源',
|
||||
tabDocs: '文件',
|
||||
noReadme: '暫無文件',
|
||||
parseResultFailed: '解析測試結果失敗',
|
||||
@@ -837,6 +858,11 @@ const zhHant = {
|
||||
toolCount: '工具:{{count}}',
|
||||
parameterCount: '參數:{{count}}',
|
||||
noParameters: '無參數',
|
||||
resourceCount: '資源:{{count}}',
|
||||
resourceBinaryContent: '二進位內容(無法顯示)',
|
||||
resourceBinaryOmitted: '二進位內容已依資源安全策略省略',
|
||||
resourceTruncated: '內容已依位元組或 token 限制截斷',
|
||||
resourceReadFailed: '讀取資源內容失敗',
|
||||
statusConnected: '已連線',
|
||||
statusDisconnected: '未連線',
|
||||
statusError: '連接錯誤',
|
||||
@@ -922,9 +948,12 @@ const zhHant = {
|
||||
selectPlugins: '選擇插件',
|
||||
pluginsTitle: '插件',
|
||||
mcpServersTitle: 'MCP 伺服器',
|
||||
mcpResourcesTitle: 'MCP 資源',
|
||||
noMCPServersSelected: '未選擇任何 MCP 伺服器',
|
||||
noMCPResourcesAvailable: '暫無可用 MCP 資源',
|
||||
addMCPServer: '新增 MCP 伺服器',
|
||||
selectMCPServers: '選擇 MCP 伺服器',
|
||||
enableMCPResourceAgentRead: '允許模型讀取',
|
||||
toolCount: '{{count}} 個工具',
|
||||
noPluginsInstalled: '無已安裝的插件',
|
||||
noMCPServersConfigured: '無已配置的 MCP 伺服器',
|
||||
@@ -940,6 +969,40 @@ const zhHant = {
|
||||
addSkill: '新增技能',
|
||||
selectSkills: '選擇技能',
|
||||
noSkillsAvailable: '暫無可用技能',
|
||||
mcpServersScopeTooltip:
|
||||
'這裡僅控制此流程線綁定哪些 MCP 伺服器;具體 MCP 工具和資源在 AI 能力的內建 Agent 表單中選擇。',
|
||||
enableAllMCPServersTooltip:
|
||||
'啟用後,所有已配置且啟用的 MCP 伺服器都會進入 AI 能力中的 MCP 工具和資源候選範圍。',
|
||||
},
|
||||
localAgent: {
|
||||
toolsTitle: '工具',
|
||||
toolsDescription: '選擇此內建 Agent 可以調用的插件、MCP 和內建工具。',
|
||||
toolsScopeTooltip:
|
||||
'MCP 工具只會從擴展集成中已綁定的 MCP 伺服器裡出現;如需增加 MCP 工具來源,請先到擴展集成綁定對應伺服器。',
|
||||
enableAllTools: '啟用所有工具',
|
||||
allToolsEnabled: '已啟用所有可用工具',
|
||||
noToolsSelected: '未選擇任何工具',
|
||||
editTools: '編輯工具',
|
||||
builtinTools: '內建工具',
|
||||
pluginTools: '插件工具',
|
||||
skillTools: '技能工具',
|
||||
mcpTools: 'MCP 工具',
|
||||
mcpToolsScopeTooltip:
|
||||
'這裡僅展示擴展集成目前允許的 MCP 伺服器提供的工具。',
|
||||
skillToolsScopeTooltip:
|
||||
'技能工具會在 LangBot 技能服務和 Box 沙箱後端可用時出現,用於讓 Agent 啟用或註冊技能。',
|
||||
selectTools: '選擇工具',
|
||||
resourcesTitle: '資源',
|
||||
resourcesDescription: '選擇此內建 Agent 可以讀取的 MCP 資源和知識庫。',
|
||||
knowledgeBases: '知識庫',
|
||||
mcpResources: 'MCP 資源',
|
||||
mcpResourcesScopeTooltip:
|
||||
'這裡僅展示擴展集成目前允許的 MCP 伺服器暴露的資源。',
|
||||
enableMCPResourceRead: '允許模型讀取 MCP 資源',
|
||||
mcpResourceReadTooltip:
|
||||
'關閉後,即使已選擇資源,也不會把 MCP 資源內容注入給模型。',
|
||||
noMCPResourcesAvailable: '暫無可用 MCP 資源',
|
||||
selectKnowledgeBases: '選擇知識庫',
|
||||
},
|
||||
debugDialog: {
|
||||
title: '流程線對話',
|
||||
@@ -1347,6 +1410,21 @@ const zhHant = {
|
||||
inaccurateReasons: '不準確原因',
|
||||
platform: '平台',
|
||||
exportFeedback: '匯出反饋',
|
||||
description:
|
||||
'告訴我們遇到的問題或想要的改進。提交時會附帶實例 UUID 和登入帳號,方便定位。',
|
||||
placeholder: '請描述你的建議、問題或重現步驟...',
|
||||
attachImage: '新增圖片',
|
||||
screenshot: '截圖',
|
||||
submit: '提交反饋',
|
||||
privacyHint: '請勿提交敏感金鑰、密碼或私人聊天內容。',
|
||||
contentRequired: '請先填寫反饋內容',
|
||||
imageOnly: '僅支援圖片附件',
|
||||
imageTooLarge: '單張圖片不能超過 1MB',
|
||||
tooManyImages: '最多新增 3 張圖片',
|
||||
screenshotFailed: '截圖失敗,請嘗試貼上或上傳圖片',
|
||||
submitSuccess: '反饋已提交,感謝!',
|
||||
submitFailed: '反饋提交失敗,請稍後再試',
|
||||
removeImage: '移除圖片',
|
||||
},
|
||||
messageDetails: {
|
||||
noData: '此查詢沒有LLM調用或錯誤記錄',
|
||||
|
||||
Reference in New Issue
Block a user