mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-14 08:26:07 +00:00
feat(mcp): support mcp resources (#2215)
* feat(mcp): support mcp resources * feat(web): split MCP resources into tab * docs: add MCP resources PR review * feat(mcp): productionize resource support * feat(mcp): scope local agent tools and resources * fix(web): gate space embedding models behind login * fix(web): prevent clipped space model CTA * test: update preproc resource tool expectations * fix(web): expose skill authoring tools in selector --------- Co-authored-by: yang.xiang <yang.xiang@advancegroup.com> Co-authored-by: Hyu <chenhyu@proton.me> Co-authored-by: Junyan Qin <rockchinq@gmail.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
DynamicFormItemType,
|
||||
IDynamicFormItemSchema,
|
||||
SYSTEM_FIELD_PREFIX,
|
||||
} from '@/app/infra/entities/form/dynamic';
|
||||
@@ -57,6 +58,95 @@ 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());
|
||||
}
|
||||
|
||||
switch (spec.type) {
|
||||
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.
|
||||
*/
|
||||
@@ -322,9 +412,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>;
|
||||
@@ -368,68 +465,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) => {
|
||||
let fieldSchema;
|
||||
switch (item.type) {
|
||||
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;
|
||||
default:
|
||||
fieldSchema = z.string();
|
||||
}
|
||||
let fieldSchema = getValueSchema(item);
|
||||
|
||||
if (
|
||||
item.required &&
|
||||
@@ -454,7 +499,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 {
|
||||
@@ -493,7 +538,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;
|
||||
@@ -508,10 +553,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.
|
||||
@@ -524,7 +575,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;
|
||||
@@ -544,7 +595,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;
|
||||
@@ -555,7 +606,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);
|
||||
@@ -786,6 +837,41 @@ export default function DynamicFormComponent({
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
config.type === DynamicFormItemType.RICH_TOOLS_SELECTOR ||
|
||||
config.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={config}
|
||||
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 (config.type === 'boolean') {
|
||||
return (
|
||||
@@ -816,7 +902,10 @@ export default function DynamicFormComponent({
|
||||
<DynamicFormItemComponent
|
||||
config={config}
|
||||
field={field}
|
||||
formValues={watchedValues as Record<string, unknown>}
|
||||
onFileUploaded={onFileUploaded}
|
||||
setFormValue={setFormValue}
|
||||
systemContext={systemContext}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
@@ -853,7 +942,10 @@ export default function DynamicFormComponent({
|
||||
<DynamicFormItemComponent
|
||||
config={config}
|
||||
field={field}
|
||||
formValues={watchedValues as Record<string, unknown>}
|
||||
onFileUploaded={onFileUploaded}
|
||||
setFormValue={setFormValue}
|
||||
systemContext={systemContext}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
|
||||
@@ -64,15 +64,23 @@ 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';
|
||||
|
||||
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[]>([]);
|
||||
@@ -103,10 +111,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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -174,27 +206,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]);
|
||||
|
||||
@@ -248,6 +266,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:
|
||||
@@ -377,10 +405,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
|
||||
@@ -481,7 +509,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' }}
|
||||
@@ -558,7 +586,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>
|
||||
@@ -574,9 +605,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] = [];
|
||||
@@ -586,29 +623,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(
|
||||
@@ -652,10 +829,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
|
||||
@@ -786,7 +963,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' }}
|
||||
@@ -1383,6 +1560,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
|
||||
|
||||
@@ -56,6 +56,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
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -427,13 +427,14 @@ export default function PipelineFormComponent({
|
||||
const forcedBoxTemplate =
|
||||
systemInfo.limitation?.force_box_session_id_template || '';
|
||||
const boxScopeForced = !!forcedBoxTemplate;
|
||||
const stageSystemContext =
|
||||
stage.name === 'local-agent'
|
||||
? {
|
||||
box_available: boxAvailable,
|
||||
box_scope_editable: boxAvailable && !boxScopeForced,
|
||||
}
|
||||
: undefined;
|
||||
const isLocalAgentStage = formName === 'ai' && stage.name === 'local-agent';
|
||||
const stageSystemContext = isLocalAgentStage
|
||||
? {
|
||||
box_available: boxAvailable,
|
||||
box_scope_editable: boxAvailable && !boxScopeForced,
|
||||
pipeline_id: pipelineId,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
// When the deployment pins every pipeline to a fixed sandbox scope (SaaS
|
||||
// ``force_box_session_id_template``), the Sandbox Scope selector is locked.
|
||||
@@ -446,12 +447,26 @@ export default function PipelineFormComponent({
|
||||
const stageInitialValues: Record<string, any> =
|
||||
(form.watch(formName) as Record<string, any>)?.[stage.name] || {};
|
||||
const effectiveInitialValues =
|
||||
stage.name === 'local-agent' && 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}>
|
||||
@@ -467,9 +482,7 @@ export default function PipelineFormComponent({
|
||||
<DynamicFormComponent
|
||||
itemConfigList={stage.config}
|
||||
initialValues={effectiveInitialValues}
|
||||
onSubmit={(values) => {
|
||||
handleDynamicFormEmit(formName, stage.name, values);
|
||||
}}
|
||||
onSubmit={emitStageValues}
|
||||
systemContext={stageSystemContext}
|
||||
/>
|
||||
</CardContent>
|
||||
|
||||
@@ -563,6 +563,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 =
|
||||
@@ -617,11 +622,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',
|
||||
|
||||
@@ -40,6 +40,8 @@ import {
|
||||
BoxSessionInfo,
|
||||
ApiRespMCPServers,
|
||||
ApiRespMCPServer,
|
||||
ApiRespMCPResources,
|
||||
ApiRespMCPResourceContents,
|
||||
MCPServer,
|
||||
ApiRespModelProviders,
|
||||
ApiRespModelProvider,
|
||||
@@ -269,10 +271,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[];
|
||||
}> {
|
||||
@@ -287,15 +299,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 ============
|
||||
@@ -865,8 +894,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> {
|
||||
@@ -926,6 +958,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');
|
||||
|
||||
@@ -824,7 +824,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',
|
||||
@@ -844,6 +846,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',
|
||||
@@ -935,9 +942,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',
|
||||
@@ -953,6 +963,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',
|
||||
|
||||
@@ -838,7 +838,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',
|
||||
@@ -858,6 +860,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',
|
||||
@@ -952,9 +960,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',
|
||||
@@ -970,6 +981,40 @@ 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.',
|
||||
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',
|
||||
|
||||
@@ -830,7 +830,9 @@ const jaJP = {
|
||||
toolsFound: '個のツール',
|
||||
unknownError: '不明なエラー',
|
||||
noToolsFound: 'ツールが見つかりません',
|
||||
noResourcesFound: 'リソースが見つかりません',
|
||||
tabTools: 'ツール',
|
||||
tabResources: 'リソース',
|
||||
tabDocs: 'ドキュメント',
|
||||
noReadme: 'ドキュメントがありません',
|
||||
parseResultFailed: 'テスト結果の解析に失敗しました',
|
||||
@@ -850,6 +852,12 @@ const jaJP = {
|
||||
toolCount: 'ツール:{{count}}',
|
||||
parameterCount: 'パラメータ:{{count}}',
|
||||
noParameters: 'パラメータなし',
|
||||
resourceCount: 'リソース:{{count}}',
|
||||
resourceBinaryContent: 'バイナリコンテンツ(表示できません)',
|
||||
resourceBinaryOmitted:
|
||||
'リソース安全ポリシーによりバイナリコンテンツを省略しました',
|
||||
resourceTruncated: 'バイトまたはトークンの上限により内容を切り詰めました',
|
||||
resourceReadFailed: 'リソースの読み込みに失敗しました',
|
||||
statusConnected: '接続済み',
|
||||
statusDisconnected: '未接続',
|
||||
statusError: '接続エラー',
|
||||
@@ -939,9 +947,12 @@ const jaJP = {
|
||||
selectPlugins: 'プラグインを選択',
|
||||
pluginsTitle: 'プラグイン',
|
||||
mcpServersTitle: 'MCPサーバー',
|
||||
mcpResourcesTitle: 'MCPリソース',
|
||||
noMCPServersSelected: 'MCPサーバーが選択されていません',
|
||||
noMCPResourcesAvailable: '利用可能なMCPリソースがありません',
|
||||
addMCPServer: 'MCPサーバーを追加',
|
||||
selectMCPServers: 'MCPサーバーを選択',
|
||||
enableMCPResourceAgentRead: 'モデルの読み取りを許可',
|
||||
toolCount: '{{count}}個のツール',
|
||||
noPluginsInstalled: 'インストールされているプラグインがありません',
|
||||
noMCPServersConfigured: '設定されているMCPサーバーがありません',
|
||||
@@ -957,6 +968,40 @@ 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 サーバーのツールだけが表示されます。',
|
||||
selectTools: 'ツールを選択',
|
||||
resourcesTitle: 'リソース',
|
||||
resourcesDescription:
|
||||
'この Local Agent が読み取れる MCP リソースとナレッジベースを選択します。',
|
||||
knowledgeBases: 'ナレッジベース',
|
||||
mcpResources: 'MCP リソース',
|
||||
mcpResourcesScopeTooltip:
|
||||
'拡張機能で現在許可されている MCP サーバーのリソースだけが表示されます。',
|
||||
enableMCPResourceRead: 'モデルによる MCP リソース読み取りを許可',
|
||||
mcpResourceReadTooltip:
|
||||
'無効にすると、選択済みの MCP リソースもモデルコンテキストに注入されません。',
|
||||
noMCPResourcesAvailable: '利用可能な MCP リソースがありません',
|
||||
selectKnowledgeBases: 'ナレッジベースを選択',
|
||||
},
|
||||
debugDialog: {
|
||||
title: 'パイプラインのチャット',
|
||||
|
||||
@@ -835,7 +835,9 @@ const ruRU = {
|
||||
toolsFound: 'инструментов',
|
||||
unknownError: 'Неизвестная ошибка',
|
||||
noToolsFound: 'Инструменты не найдены',
|
||||
noResourcesFound: 'Ресурсы не найдены',
|
||||
tabTools: 'Инструменты',
|
||||
tabResources: 'Ресурсы',
|
||||
tabDocs: 'Документация',
|
||||
noReadme: 'Документация отсутствует',
|
||||
parseResultFailed: 'Не удалось разобрать результат теста',
|
||||
@@ -855,6 +857,12 @@ const ruRU = {
|
||||
toolCount: 'Инструменты: {{count}}',
|
||||
parameterCount: 'Параметры: {{count}}',
|
||||
noParameters: 'Нет параметров',
|
||||
resourceCount: 'Ресурсы: {{count}}',
|
||||
resourceBinaryContent: 'Двоичное содержимое (невозможно отобразить)',
|
||||
resourceBinaryOmitted:
|
||||
'Двоичное содержимое опущено согласно политике безопасности ресурсов',
|
||||
resourceTruncated: 'Содержимое усечено по лимитам байтов или токенов',
|
||||
resourceReadFailed: 'Не удалось прочитать содержимое ресурса',
|
||||
statusConnected: 'Подключён',
|
||||
statusDisconnected: 'Отключён',
|
||||
statusError: 'Ошибка подключения',
|
||||
@@ -945,9 +953,12 @@ const ruRU = {
|
||||
selectPlugins: 'Выберите плагины',
|
||||
pluginsTitle: 'Плагины',
|
||||
mcpServersTitle: 'MCP-серверы',
|
||||
mcpResourcesTitle: 'MCP-ресурсы',
|
||||
noMCPServersSelected: 'MCP-серверы не выбраны',
|
||||
noMCPResourcesAvailable: 'Нет доступных MCP-ресурсов',
|
||||
addMCPServer: 'Добавить MCP-сервер',
|
||||
selectMCPServers: 'Выберите MCP-серверы',
|
||||
enableMCPResourceAgentRead: 'Разрешить модели чтение',
|
||||
toolCount: '{{count}} инструментов',
|
||||
noPluginsInstalled: 'Нет установленных плагинов',
|
||||
noMCPServersConfigured: 'Нет настроенных MCP-серверов',
|
||||
@@ -963,6 +974,40 @@ 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-серверов, разрешённых сейчас в Расширениях.',
|
||||
selectTools: 'Выбрать инструменты',
|
||||
resourcesTitle: 'Ресурсы',
|
||||
resourcesDescription:
|
||||
'Выберите MCP-ресурсы и базы знаний для этого Local Agent.',
|
||||
knowledgeBases: 'Базы знаний',
|
||||
mcpResources: 'MCP-ресурсы',
|
||||
mcpResourcesScopeTooltip:
|
||||
'Здесь показаны только ресурсы MCP-серверов, разрешённых сейчас в Расширениях.',
|
||||
enableMCPResourceRead: 'Разрешить модели читать MCP-ресурсы',
|
||||
mcpResourceReadTooltip:
|
||||
'Если выключено, выбранные MCP-ресурсы не будут добавляться в контекст модели.',
|
||||
noMCPResourcesAvailable: 'Нет доступных MCP-ресурсов',
|
||||
selectKnowledgeBases: 'Выбрать базы знаний',
|
||||
},
|
||||
debugDialog: {
|
||||
title: 'Чат конвейера',
|
||||
|
||||
@@ -813,7 +813,9 @@ const thTH = {
|
||||
toolsFound: 'เครื่องมือ',
|
||||
unknownError: 'ข้อผิดพลาดที่ไม่ทราบสาเหตุ',
|
||||
noToolsFound: 'ไม่พบเครื่องมือ',
|
||||
noResourcesFound: 'ไม่พบทรัพยากร',
|
||||
tabTools: 'เครื่องมือ',
|
||||
tabResources: 'ทรัพยากร',
|
||||
tabDocs: 'เอกสาร',
|
||||
noReadme: 'ไม่มีเอกสาร',
|
||||
parseResultFailed: 'ไม่สามารถแยกวิเคราะห์ผลการทดสอบได้',
|
||||
@@ -833,6 +835,11 @@ const thTH = {
|
||||
toolCount: 'เครื่องมือ: {{count}}',
|
||||
parameterCount: 'พารามิเตอร์: {{count}}',
|
||||
noParameters: 'ไม่มีพารามิเตอร์',
|
||||
resourceCount: 'ทรัพยากร: {{count}}',
|
||||
resourceBinaryContent: 'เนื้อหาไบนารี (ไม่สามารถแสดงได้)',
|
||||
resourceBinaryOmitted: 'ละเว้นเนื้อหาไบนารีตามนโยบายความปลอดภัยของทรัพยากร',
|
||||
resourceTruncated: 'ตัดเนื้อหาตามขีดจำกัดไบต์หรือโทเคน',
|
||||
resourceReadFailed: 'ไม่สามารถอ่านเนื้อหาทรัพยากรได้',
|
||||
statusConnected: 'เชื่อมต่อแล้ว',
|
||||
statusDisconnected: 'ไม่ได้เชื่อมต่อ',
|
||||
statusError: 'ข้อผิดพลาดการเชื่อมต่อ',
|
||||
@@ -922,9 +929,12 @@ const thTH = {
|
||||
selectPlugins: 'เลือกปลั๊กอิน',
|
||||
pluginsTitle: 'ปลั๊กอิน',
|
||||
mcpServersTitle: 'เซิร์ฟเวอร์ MCP',
|
||||
mcpResourcesTitle: 'ทรัพยากร MCP',
|
||||
noMCPServersSelected: 'ไม่ได้เลือกเซิร์ฟเวอร์ MCP',
|
||||
noMCPResourcesAvailable: 'ไม่มีทรัพยากร MCP ที่พร้อมใช้งาน',
|
||||
addMCPServer: 'เพิ่มเซิร์ฟเวอร์ MCP',
|
||||
selectMCPServers: 'เลือกเซิร์ฟเวอร์ MCP',
|
||||
enableMCPResourceAgentRead: 'อนุญาตให้โมเดลอ่าน',
|
||||
toolCount: '{{count}} เครื่องมือ',
|
||||
noPluginsInstalled: 'ไม่มีปลั๊กอินที่ติดตั้ง',
|
||||
noMCPServersConfigured: 'ไม่มีเซิร์ฟเวอร์ MCP ที่กำหนดค่า',
|
||||
@@ -940,6 +950,40 @@ 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 ที่อนุญาตอยู่ในส่วนขยาย',
|
||||
selectTools: 'เลือกเครื่องมือ',
|
||||
resourcesTitle: 'ทรัพยากร',
|
||||
resourcesDescription:
|
||||
'เลือกทรัพยากร MCP และคลังความรู้สำหรับ Local Agent นี้',
|
||||
knowledgeBases: 'คลังความรู้',
|
||||
mcpResources: 'ทรัพยากร MCP',
|
||||
mcpResourcesScopeTooltip:
|
||||
'ที่นี่จะแสดงเฉพาะทรัพยากรจากเซิร์ฟเวอร์ MCP ที่อนุญาตอยู่ในส่วนขยาย',
|
||||
enableMCPResourceRead: 'อนุญาตให้โมเดลอ่านทรัพยากร MCP',
|
||||
mcpResourceReadTooltip:
|
||||
'เมื่อปิด ทรัพยากร MCP ที่เลือกไว้จะไม่ถูกใส่เข้าไปในบริบทของโมเดล',
|
||||
noMCPResourcesAvailable: 'ไม่มีทรัพยากร MCP ที่พร้อมใช้งาน',
|
||||
selectKnowledgeBases: 'เลือกคลังความรู้',
|
||||
},
|
||||
debugDialog: {
|
||||
title: 'แชท Pipeline',
|
||||
|
||||
@@ -828,7 +828,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',
|
||||
@@ -848,6 +850,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',
|
||||
@@ -937,9 +945,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',
|
||||
@@ -955,6 +966,40 @@ 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.',
|
||||
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',
|
||||
|
||||
@@ -790,7 +790,9 @@ const zhHans = {
|
||||
toolsFound: '个工具',
|
||||
unknownError: '未知错误',
|
||||
noToolsFound: '未找到任何工具',
|
||||
noResourcesFound: '未找到任何资源',
|
||||
tabTools: '工具',
|
||||
tabResources: '资源',
|
||||
tabDocs: '文档',
|
||||
noReadme: '暂无文档',
|
||||
parseResultFailed: '解析测试结果失败',
|
||||
@@ -810,6 +812,11 @@ const zhHans = {
|
||||
toolCount: '工具:{{count}}',
|
||||
parameterCount: '参数:{{count}}',
|
||||
noParameters: '无参数',
|
||||
resourceCount: '资源:{{count}}',
|
||||
resourceBinaryContent: '二进制内容(无法显示)',
|
||||
resourceBinaryOmitted: '二进制内容已按资源安全策略省略',
|
||||
resourceTruncated: '内容已按字节或 token 限制截断',
|
||||
resourceReadFailed: '读取资源内容失败',
|
||||
statusConnected: '已连接',
|
||||
statusDisconnected: '未连接',
|
||||
statusError: '连接错误',
|
||||
@@ -895,9 +902,12 @@ const zhHans = {
|
||||
selectPlugins: '选择插件',
|
||||
pluginsTitle: '插件',
|
||||
mcpServersTitle: 'MCP 服务器',
|
||||
mcpResourcesTitle: 'MCP 资源',
|
||||
noMCPServersSelected: '未选择任何 MCP 服务器',
|
||||
noMCPResourcesAvailable: '暂无可用 MCP 资源',
|
||||
addMCPServer: '添加 MCP 服务器',
|
||||
selectMCPServers: '选择 MCP 服务器',
|
||||
enableMCPResourceAgentRead: '允许模型读取',
|
||||
toolCount: '{{count}} 个工具',
|
||||
noPluginsInstalled: '无已安装的插件',
|
||||
noMCPServersConfigured: '无已配置的 MCP 服务器',
|
||||
@@ -913,6 +923,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: '流水线对话',
|
||||
|
||||
@@ -789,7 +789,9 @@ const zhHant = {
|
||||
toolsFound: '個工具',
|
||||
unknownError: '未知錯誤',
|
||||
noToolsFound: '未找到任何工具',
|
||||
noResourcesFound: '未找到任何資源',
|
||||
tabTools: '工具',
|
||||
tabResources: '資源',
|
||||
tabDocs: '文件',
|
||||
noReadme: '暫無文件',
|
||||
parseResultFailed: '解析測試結果失敗',
|
||||
@@ -809,6 +811,11 @@ const zhHant = {
|
||||
toolCount: '工具:{{count}}',
|
||||
parameterCount: '參數:{{count}}',
|
||||
noParameters: '無參數',
|
||||
resourceCount: '資源:{{count}}',
|
||||
resourceBinaryContent: '二進位內容(無法顯示)',
|
||||
resourceBinaryOmitted: '二進位內容已依資源安全策略省略',
|
||||
resourceTruncated: '內容已依位元組或 token 限制截斷',
|
||||
resourceReadFailed: '讀取資源內容失敗',
|
||||
statusConnected: '已連線',
|
||||
statusDisconnected: '未連線',
|
||||
statusError: '連接錯誤',
|
||||
@@ -894,9 +901,12 @@ const zhHant = {
|
||||
selectPlugins: '選擇插件',
|
||||
pluginsTitle: '插件',
|
||||
mcpServersTitle: 'MCP 伺服器',
|
||||
mcpResourcesTitle: 'MCP 資源',
|
||||
noMCPServersSelected: '未選擇任何 MCP 伺服器',
|
||||
noMCPResourcesAvailable: '暫無可用 MCP 資源',
|
||||
addMCPServer: '新增 MCP 伺服器',
|
||||
selectMCPServers: '選擇 MCP 伺服器',
|
||||
enableMCPResourceAgentRead: '允許模型讀取',
|
||||
toolCount: '{{count}} 個工具',
|
||||
noPluginsInstalled: '無已安裝的插件',
|
||||
noMCPServersConfigured: '無已配置的 MCP 伺服器',
|
||||
@@ -912,6 +922,38 @@ 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 伺服器提供的工具。',
|
||||
selectTools: '選擇工具',
|
||||
resourcesTitle: '資源',
|
||||
resourcesDescription: '選擇此內建 Agent 可以讀取的 MCP 資源和知識庫。',
|
||||
knowledgeBases: '知識庫',
|
||||
mcpResources: 'MCP 資源',
|
||||
mcpResourcesScopeTooltip:
|
||||
'這裡僅展示擴展集成目前允許的 MCP 伺服器暴露的資源。',
|
||||
enableMCPResourceRead: '允許模型讀取 MCP 資源',
|
||||
mcpResourceReadTooltip:
|
||||
'關閉後,即使已選擇資源,也不會把 MCP 資源內容注入給模型。',
|
||||
noMCPResourcesAvailable: '暫無可用 MCP 資源',
|
||||
selectKnowledgeBases: '選擇知識庫',
|
||||
},
|
||||
debugDialog: {
|
||||
title: '流程線對話',
|
||||
|
||||
Reference in New Issue
Block a user