mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-25 11:37:13 +00:00
feat(wizard): streamline custom model onboarding
This commit is contained in:
@@ -75,6 +75,8 @@ shape as the corresponding HTTP API request body. Discover resources with the
|
||||
`list_*` / `get_*` tools before mutating; identifiers are UUIDs. Reads require
|
||||
`resource.view`; mutations require `resource.manage`. All service calls inherit
|
||||
the immutable Workspace context authenticated at the MCP transport boundary.
|
||||
Pass `is_default: true` to `create_pipeline` only when the Workspace does not
|
||||
already have a default pipeline.
|
||||
|
||||
## How to use
|
||||
|
||||
|
||||
@@ -39,7 +39,13 @@ class PipelinesRouterGroup(group.RouterGroup):
|
||||
permission=Permission.RESOURCE_MANAGE,
|
||||
)
|
||||
async def _(request_context: RequestContext) -> str:
|
||||
pipeline_uuid = await self.ap.pipeline_service.create_pipeline(request_context, await quart.request.json)
|
||||
pipeline_data = await quart.request.json
|
||||
create_as_default = pipeline_data.get('is_default') is True
|
||||
pipeline_uuid = await self.ap.pipeline_service.create_pipeline(
|
||||
request_context,
|
||||
pipeline_data,
|
||||
default=create_as_default,
|
||||
)
|
||||
return self.success(data={'uuid': pipeline_uuid})
|
||||
|
||||
@self.route(
|
||||
|
||||
@@ -147,7 +147,16 @@ class LangBotMCPServer:
|
||||
)
|
||||
async def create_pipeline(pipeline_data: dict) -> str:
|
||||
context = _authorized(Permission.RESOURCE_MANAGE)
|
||||
return _dump({'uuid': await ap.pipeline_service.create_pipeline(context, pipeline_data)})
|
||||
create_as_default = pipeline_data.get('is_default') is True
|
||||
return _dump(
|
||||
{
|
||||
'uuid': await ap.pipeline_service.create_pipeline(
|
||||
context,
|
||||
pipeline_data,
|
||||
default=create_as_default,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@mcp.tool(description='Update a pipeline by UUID. `pipeline_data` matches the PUT body.')
|
||||
async def update_pipeline(pipeline_uuid: str, pipeline_data: dict) -> str:
|
||||
|
||||
@@ -254,6 +254,22 @@ class TestPipelinesCRUDEndpoints:
|
||||
assert data['code'] == 0
|
||||
assert 'uuid' in data['data']
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_default_pipeline_forwards_default_flag(self, quart_test_client, fake_pipeline_app):
|
||||
"""POST /api/v1/pipelines explicitly creates a default pipeline."""
|
||||
fake_pipeline_app.pipeline_service.create_pipeline.reset_mock()
|
||||
|
||||
response = await quart_test_client.post(
|
||||
'/api/v1/pipelines',
|
||||
headers={'Authorization': 'Bearer test_token'},
|
||||
json={'name': 'Default Pipeline', 'config': {}, 'is_default': True},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
call = fake_pipeline_app.pipeline_service.create_pipeline.await_args
|
||||
assert call.kwargs == {'default': True}
|
||||
assert call.args[1]['is_default'] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_pipeline_success(self, quart_test_client):
|
||||
"""PUT /api/v1/pipelines/{uuid} updates pipeline."""
|
||||
|
||||
@@ -33,7 +33,7 @@ const getFormSchema = (t: (key: string) => string) =>
|
||||
|
||||
interface ProviderFormProps {
|
||||
providerId?: string;
|
||||
onFormSubmit: () => void;
|
||||
onFormSubmit: (providerUuid: string) => void | Promise<void>;
|
||||
onFormCancel: () => void;
|
||||
}
|
||||
|
||||
@@ -171,14 +171,16 @@ export default function ProviderForm({
|
||||
};
|
||||
|
||||
try {
|
||||
let savedProviderUuid = providerId;
|
||||
if (providerId) {
|
||||
await httpClient.updateModelProvider(providerId, data);
|
||||
toast.success(t('models.providerSaved'));
|
||||
} else {
|
||||
await httpClient.createModelProvider(data);
|
||||
const response = await httpClient.createModelProvider(data);
|
||||
savedProviderUuid = response.uuid;
|
||||
toast.success(t('models.providerCreated'));
|
||||
}
|
||||
onFormSubmit();
|
||||
await onFormSubmit(savedProviderUuid as string);
|
||||
} catch (err) {
|
||||
toast.error(t('models.providerSaveError') + (err as CustomApiError).msg);
|
||||
}
|
||||
|
||||
@@ -150,7 +150,9 @@ export class BackendClient extends BaseHttpClient {
|
||||
return this.get(`/api/v1/provider/models/llm/${uuid}`);
|
||||
}
|
||||
|
||||
public createProviderLLMModel(model: LLMModel): Promise<object> {
|
||||
public createProviderLLMModel(
|
||||
model: Omit<LLMModel, 'uuid'>,
|
||||
): Promise<{ uuid: string }> {
|
||||
return this.post('/api/v1/provider/models/llm', model);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,409 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Check,
|
||||
Eye,
|
||||
Loader2,
|
||||
Pencil,
|
||||
RefreshCw,
|
||||
Wrench,
|
||||
} from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import ProviderForm from '@/app/home/components/models-dialog/component/provider-form/ProviderForm';
|
||||
import type { ScannedProviderModel } from '@/app/infra/entities/api';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type ModelSetupMode = 'scan' | 'manual';
|
||||
type ScanFallbackReason = 'failed' | 'empty' | null;
|
||||
|
||||
export interface OwnModelSelection {
|
||||
source: ModelSetupMode;
|
||||
providerUuid: string;
|
||||
model: ScannedProviderModel;
|
||||
}
|
||||
|
||||
interface OwnModelSetupProps {
|
||||
onBack: () => void;
|
||||
onSelectionChange: (selection: OwnModelSelection | null) => void;
|
||||
}
|
||||
|
||||
export default function OwnModelSetup({
|
||||
onBack,
|
||||
onSelectionChange,
|
||||
}: OwnModelSetupProps) {
|
||||
const { t } = useTranslation();
|
||||
const [providerUuid, setProviderUuid] = useState<string | null>(null);
|
||||
const [showProviderForm, setShowProviderForm] = useState(true);
|
||||
const [mode, setMode] = useState<ModelSetupMode>('scan');
|
||||
const [models, setModels] = useState<ScannedProviderModel[]>([]);
|
||||
const [selectedModelId, setSelectedModelId] = useState<string | null>(null);
|
||||
const [isScanning, setIsScanning] = useState(false);
|
||||
const [scanFallbackReason, setScanFallbackReason] =
|
||||
useState<ScanFallbackReason>(null);
|
||||
const [manualModelName, setManualModelName] = useState('');
|
||||
const [manualContextLength, setManualContextLength] = useState('');
|
||||
const [manualVision, setManualVision] = useState(false);
|
||||
const [manualFunctionCall, setManualFunctionCall] = useState(false);
|
||||
|
||||
const parsedManualContextLength = useMemo(() => {
|
||||
if (!manualContextLength.trim()) return null;
|
||||
const value = Number(manualContextLength);
|
||||
return Number.isInteger(value) && value > 0 ? value : undefined;
|
||||
}, [manualContextLength]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== 'manual' || !providerUuid) return;
|
||||
if (!manualModelName.trim() || parsedManualContextLength === undefined) {
|
||||
onSelectionChange(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const abilities = [
|
||||
...(manualVision ? ['vision'] : []),
|
||||
...(manualFunctionCall ? ['func_call'] : []),
|
||||
];
|
||||
const modelName = manualModelName.trim();
|
||||
onSelectionChange({
|
||||
source: 'manual',
|
||||
providerUuid,
|
||||
model: {
|
||||
id: modelName,
|
||||
name: modelName,
|
||||
type: 'llm',
|
||||
abilities,
|
||||
context_length: parsedManualContextLength,
|
||||
already_added: false,
|
||||
},
|
||||
});
|
||||
}, [
|
||||
manualFunctionCall,
|
||||
manualModelName,
|
||||
manualVision,
|
||||
mode,
|
||||
onSelectionChange,
|
||||
parsedManualContextLength,
|
||||
providerUuid,
|
||||
]);
|
||||
|
||||
const scanModels = useCallback(
|
||||
async (uuid: string) => {
|
||||
setMode('scan');
|
||||
setIsScanning(true);
|
||||
setScanFallbackReason(null);
|
||||
setModels([]);
|
||||
setSelectedModelId(null);
|
||||
onSelectionChange(null);
|
||||
|
||||
try {
|
||||
const response = await httpClient.scanProviderModels(uuid, 'llm');
|
||||
const availableModels = response.models.filter(
|
||||
(model) => model.type === 'llm' && !model.already_added,
|
||||
);
|
||||
setModels(availableModels);
|
||||
if (availableModels.length === 0) {
|
||||
setScanFallbackReason('empty');
|
||||
setMode('manual');
|
||||
}
|
||||
} catch {
|
||||
setScanFallbackReason('failed');
|
||||
setMode('manual');
|
||||
} finally {
|
||||
setIsScanning(false);
|
||||
}
|
||||
},
|
||||
[onSelectionChange],
|
||||
);
|
||||
|
||||
const handleProviderSaved = useCallback(
|
||||
async (uuid: string) => {
|
||||
setProviderUuid(uuid);
|
||||
setShowProviderForm(false);
|
||||
await scanModels(uuid);
|
||||
},
|
||||
[scanModels],
|
||||
);
|
||||
|
||||
const handleSelectModel = useCallback(
|
||||
(model: ScannedProviderModel) => {
|
||||
if (!providerUuid) return;
|
||||
setSelectedModelId(model.id);
|
||||
onSelectionChange({ source: 'scan', providerUuid, model });
|
||||
},
|
||||
[onSelectionChange, providerUuid],
|
||||
);
|
||||
|
||||
const handleModeChange = useCallback(
|
||||
(value: string) => {
|
||||
setMode(value as ModelSetupMode);
|
||||
setSelectedModelId(null);
|
||||
onSelectionChange(null);
|
||||
},
|
||||
[onSelectionChange],
|
||||
);
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
onSelectionChange(null);
|
||||
onBack();
|
||||
}, [onBack, onSelectionChange]);
|
||||
|
||||
const handleEditProvider = useCallback(() => {
|
||||
setSelectedModelId(null);
|
||||
onSelectionChange(null);
|
||||
setShowProviderForm(true);
|
||||
}, [onSelectionChange]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-3xl space-y-5">
|
||||
<div>
|
||||
<Button variant="ghost" size="sm" onClick={handleBack}>
|
||||
<ArrowLeft className="mr-1.5 size-4" />
|
||||
{t('wizard.aiEngine.backToChoices')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl font-semibold">
|
||||
{t('wizard.aiEngine.ownModelSetupTitle')}
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{t('wizard.aiEngine.ownModelSetupDescription')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{showProviderForm ? (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">
|
||||
{t('wizard.aiEngine.addProviderTitle')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t('wizard.aiEngine.addProviderDescription')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ProviderForm
|
||||
providerId={providerUuid ?? undefined}
|
||||
onFormSubmit={handleProviderSaved}
|
||||
onFormCancel={() =>
|
||||
providerUuid ? setShowProviderForm(false) : handleBack()
|
||||
}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3 border-b pb-3">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold">
|
||||
{t('wizard.aiEngine.selectModelTitle')}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('wizard.aiEngine.selectScannedModelDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
title={t('wizard.aiEngine.editProvider')}
|
||||
onClick={handleEditProvider}
|
||||
>
|
||||
<Pencil className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Tabs value={mode} onValueChange={handleModeChange}>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="scan">
|
||||
{t('wizard.aiEngine.scanModelMode')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="manual">
|
||||
{t('wizard.aiEngine.manualModelMode')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="scan" className="mt-4">
|
||||
{isScanning ? (
|
||||
<div className="flex min-h-48 items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
{t('wizard.aiEngine.scanningModels')}
|
||||
</div>
|
||||
) : models.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{models.map((model) => {
|
||||
const selected = selectedModelId === model.id;
|
||||
return (
|
||||
<button
|
||||
key={model.id}
|
||||
type="button"
|
||||
className={cn(
|
||||
'flex min-h-20 items-center gap-3 rounded-md border p-3 text-left transition-colors hover:border-primary/60 hover:bg-accent/40',
|
||||
selected &&
|
||||
'border-primary bg-accent ring-1 ring-primary',
|
||||
)}
|
||||
onClick={() => handleSelectModel(model)}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
'flex size-5 shrink-0 items-center justify-center rounded-full border',
|
||||
selected &&
|
||||
'border-primary bg-primary text-primary-foreground',
|
||||
)}
|
||||
>
|
||||
{selected && <Check className="size-3" />}
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-sm font-medium">
|
||||
{model.display_name || model.name}
|
||||
</span>
|
||||
<span className="block truncate text-xs text-muted-foreground">
|
||||
{model.name}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={isScanning || !providerUuid}
|
||||
onClick={() => providerUuid && scanModels(providerUuid)}
|
||||
>
|
||||
<RefreshCw className="mr-1.5 size-4" />
|
||||
{t('wizard.aiEngine.rescanModels')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-48 flex-col items-center justify-center gap-3 border border-dashed p-6 text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t(
|
||||
scanFallbackReason === 'failed'
|
||||
? 'wizard.aiEngine.scanModelsFailed'
|
||||
: 'wizard.aiEngine.noScannedModels',
|
||||
)}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!providerUuid}
|
||||
onClick={() => providerUuid && scanModels(providerUuid)}
|
||||
>
|
||||
<RefreshCw className="mr-1.5 size-4" />
|
||||
{t('wizard.aiEngine.rescanModels')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="manual" className="mt-4 space-y-5">
|
||||
{scanFallbackReason && (
|
||||
<div className="border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-200">
|
||||
{t(
|
||||
scanFallbackReason === 'failed'
|
||||
? 'wizard.aiEngine.manualFallbackFailed'
|
||||
: 'wizard.aiEngine.manualFallbackEmpty',
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="wizard-manual-model-name">
|
||||
{t('wizard.aiEngine.manualModelId')}
|
||||
<span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="wizard-manual-model-name"
|
||||
value={manualModelName}
|
||||
onChange={(event) => setManualModelName(event.target.value)}
|
||||
placeholder={t('wizard.aiEngine.manualModelIdPlaceholder')}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('wizard.aiEngine.manualModelIdDescription')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 border-t pt-4">
|
||||
<p className="text-sm font-medium">
|
||||
{t('wizard.aiEngine.manualModelOptions')}
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="wizard-manual-context-length">
|
||||
{t('models.contextLength')}
|
||||
</Label>
|
||||
<Input
|
||||
id="wizard-manual-context-length"
|
||||
type="number"
|
||||
min={1}
|
||||
step={1}
|
||||
value={manualContextLength}
|
||||
onChange={(event) =>
|
||||
setManualContextLength(event.target.value)
|
||||
}
|
||||
placeholder={t('models.contextLengthPlaceholder')}
|
||||
/>
|
||||
{parsedManualContextLength === undefined && (
|
||||
<p className="text-xs text-destructive">
|
||||
{t('models.contextLengthInvalid')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="wizard-manual-vision"
|
||||
checked={manualVision}
|
||||
onCheckedChange={(checked) =>
|
||||
setManualVision(checked === true)
|
||||
}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="wizard-manual-vision"
|
||||
className="flex items-center gap-1.5"
|
||||
>
|
||||
<Eye className="size-4" />
|
||||
{t('models.visionAbility')}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="wizard-manual-function-call"
|
||||
checked={manualFunctionCall}
|
||||
onCheckedChange={(checked) =>
|
||||
setManualFunctionCall(checked === true)
|
||||
}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="wizard-manual-function-call"
|
||||
className="flex items-center gap-1.5"
|
||||
>
|
||||
<Wrench className="size-4" />
|
||||
{t('models.functionCallAbility')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+171
-30
@@ -40,6 +40,9 @@ import {
|
||||
} from '@/app/home/components/dynamic-form/DynamicFormItemConfig';
|
||||
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
||||
import { BotLogListComponent } from '@/app/home/bots/components/bot-log/view/BotLogListComponent';
|
||||
import OwnModelSetup, {
|
||||
OwnModelSelection,
|
||||
} from '@/app/wizard/components/OwnModelSetup';
|
||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||
import {
|
||||
groupByCategory,
|
||||
@@ -49,8 +52,11 @@ import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
|
||||
import i18n from 'i18next';
|
||||
|
||||
import {
|
||||
configureLocalAgentPrimaryModel,
|
||||
ensureHttpBotSigningSecret,
|
||||
findDefaultPipeline,
|
||||
getErrorMessage,
|
||||
isWebhookModeEnabled,
|
||||
} from '@/app/wizard/utils';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -120,6 +126,8 @@ export default function WizardPage() {
|
||||
const [aiChoice, setAiChoice] = useState<
|
||||
'external' | 'own-model' | 'more-features' | null
|
||||
>(null);
|
||||
const [ownModelSelection, setOwnModelSelection] =
|
||||
useState<OwnModelSelection | null>(null);
|
||||
|
||||
// ---- Helper: persist wizard progress to backend (fire-and-forget) ----
|
||||
const saveProgress = useCallback(
|
||||
@@ -357,6 +365,9 @@ export default function WizardPage() {
|
||||
const goPrev = useCallback(() => {
|
||||
if (currentStep > 0) {
|
||||
const prevStep = currentStep - 1;
|
||||
if (currentStep === 2) {
|
||||
setOwnModelSelection(null);
|
||||
}
|
||||
setCurrentStep(prevStep);
|
||||
saveProgress({ step: prevStep });
|
||||
}
|
||||
@@ -434,7 +445,7 @@ export default function WizardPage() {
|
||||
}, [selectedAdapter, adapters, t, saveProgress]);
|
||||
|
||||
// ---- Save Bot Config & Enable (Step 1) ----
|
||||
// Creates a recommended Local Agent pipeline, binds it, and enables the bot.
|
||||
// Binds the bot to the Workspace default pipeline and enables it.
|
||||
|
||||
const handleSaveBot = useCallback(async () => {
|
||||
if (!createdBotUuid || !selectedAdapter) return;
|
||||
@@ -442,42 +453,74 @@ export default function WizardPage() {
|
||||
|
||||
let createdPipelineThisAttempt: string | null = null;
|
||||
try {
|
||||
let pipelineUuid = createdPipelineUuid;
|
||||
const pipelinesResponse = await httpClient.getPipelines(
|
||||
'updated_at',
|
||||
'DESC',
|
||||
);
|
||||
const defaultPipeline = findDefaultPipeline(pipelinesResponse.pipelines);
|
||||
let pipelineUuid = defaultPipeline?.uuid ?? null;
|
||||
let createdDefaultPipeline = false;
|
||||
|
||||
if (!pipelineUuid) {
|
||||
const recommendedModel = await httpClient.getWizardRecommendedModel();
|
||||
const pipelineResp = await httpClient.createPipeline({
|
||||
name: `${botName} Agent`,
|
||||
description: botDescription || '',
|
||||
config: {},
|
||||
is_default: true,
|
||||
});
|
||||
pipelineUuid = pipelineResp.uuid;
|
||||
createdPipelineThisAttempt = pipelineUuid;
|
||||
const createdPipeline = await httpClient.getPipeline(pipelineUuid);
|
||||
const aiConfig = createdPipeline.pipeline.config.ai as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const localAgentConfig = (aiConfig['local-agent'] ?? {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
createdDefaultPipeline = true;
|
||||
}
|
||||
|
||||
const pipelineData = await httpClient.getPipeline(pipelineUuid);
|
||||
const fullConfig = pipelineData.pipeline.config as unknown as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const aiConfig = (fullConfig.ai ?? {}) as Record<string, unknown>;
|
||||
const runnerConfig = (aiConfig.runner ?? {}) as Record<string, unknown>;
|
||||
const localAgentConfig = (aiConfig['local-agent'] ?? {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const modelConfig = (localAgentConfig.model ?? {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const usesLocalAgent =
|
||||
createdDefaultPipeline || runnerConfig.runner === 'local-agent';
|
||||
const needsPrimaryModel =
|
||||
usesLocalAgent &&
|
||||
(typeof modelConfig.primary !== 'string' || !modelConfig.primary);
|
||||
|
||||
if (createdDefaultPipeline || needsPrimaryModel) {
|
||||
const recommendedModel = await httpClient.getWizardRecommendedModel();
|
||||
await httpClient.updatePipeline(pipelineUuid, {
|
||||
name: `${botName} Agent`,
|
||||
description: botDescription || '',
|
||||
name: pipelineData.pipeline.name,
|
||||
description: pipelineData.pipeline.description || '',
|
||||
config: {
|
||||
...createdPipeline.pipeline.config,
|
||||
...fullConfig,
|
||||
ai: {
|
||||
...aiConfig,
|
||||
runner: { runner: 'local-agent' },
|
||||
runner: createdDefaultPipeline
|
||||
? { ...runnerConfig, runner: 'local-agent' }
|
||||
: runnerConfig,
|
||||
'local-agent': {
|
||||
...localAgentConfig,
|
||||
model: { primary: recommendedModel.uuid, fallbacks: [] },
|
||||
model: {
|
||||
...modelConfig,
|
||||
primary: recommendedModel.uuid,
|
||||
fallbacks: Array.isArray(modelConfig.fallbacks)
|
||||
? modelConfig.fallbacks
|
||||
: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
setCreatedPipelineUuid(pipelineUuid);
|
||||
}
|
||||
setCreatedPipelineUuid(pipelineUuid);
|
||||
|
||||
const configToSave = ensureHttpBotSigningSecret(
|
||||
selectedAdapter,
|
||||
@@ -537,7 +580,6 @@ export default function WizardPage() {
|
||||
botName,
|
||||
botDescription,
|
||||
adapterConfig,
|
||||
createdPipelineUuid,
|
||||
t,
|
||||
saveProgress,
|
||||
]);
|
||||
@@ -559,9 +601,14 @@ export default function WizardPage() {
|
||||
const handleFinish = useCallback(async () => {
|
||||
if (!aiChoice || !createdBotUuid || !createdPipelineUuid) return;
|
||||
if (aiChoice === 'external' && !selectedRunner) return;
|
||||
if (aiChoice === 'own-model' && !ownModelSelection) return;
|
||||
setIsSubmitting(true);
|
||||
let externalPipelineUuid: string | null = null;
|
||||
let externalPipelineBound = false;
|
||||
let createdOwnModelUuid: string | null = null;
|
||||
let ownModelPipelineUuid: string | null = null;
|
||||
let ownModelPipelineBound = false;
|
||||
let originalOwnModelBot: Bot | null = null;
|
||||
|
||||
try {
|
||||
if (aiChoice === 'external' && selectedRunner) {
|
||||
@@ -599,18 +646,91 @@ export default function WizardPage() {
|
||||
externalPipelineBound = true;
|
||||
}
|
||||
|
||||
await completeWizard();
|
||||
if (aiChoice === 'own-model') {
|
||||
navigate(`/home/pipelines?id=${createdPipelineUuid}`, {
|
||||
replace: true,
|
||||
if (aiChoice === 'own-model' && ownModelSelection) {
|
||||
const modelResponse = await httpClient.createProviderLLMModel({
|
||||
name: ownModelSelection.model.name,
|
||||
provider_uuid: ownModelSelection.providerUuid,
|
||||
abilities: ownModelSelection.model.abilities ?? [],
|
||||
reasoning_config: { level: 'provider_default' },
|
||||
context_length: ownModelSelection.model.context_length ?? null,
|
||||
extra_args: {},
|
||||
});
|
||||
} else {
|
||||
navigate('/home', { replace: true });
|
||||
createdOwnModelUuid = modelResponse.uuid;
|
||||
|
||||
const pipelineResponse = await httpClient.createPipeline({
|
||||
name: `${botName} Custom Agent`,
|
||||
description: botDescription || '',
|
||||
config: {},
|
||||
});
|
||||
ownModelPipelineUuid = pipelineResponse.uuid;
|
||||
const createdPipeline =
|
||||
await httpClient.getPipeline(ownModelPipelineUuid);
|
||||
const fullConfig = createdPipeline.pipeline.config as unknown as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
await httpClient.updatePipeline(ownModelPipelineUuid, {
|
||||
name: `${botName} Custom Agent`,
|
||||
description: botDescription || '',
|
||||
config: configureLocalAgentPrimaryModel(
|
||||
fullConfig,
|
||||
createdOwnModelUuid,
|
||||
),
|
||||
});
|
||||
|
||||
originalOwnModelBot = (await httpClient.getBot(createdBotUuid)).bot;
|
||||
await httpClient.updateBot(createdBotUuid, {
|
||||
name: originalOwnModelBot.name,
|
||||
description: originalOwnModelBot.description,
|
||||
adapter: originalOwnModelBot.adapter,
|
||||
adapter_config: originalOwnModelBot.adapter_config,
|
||||
enable: originalOwnModelBot.enable,
|
||||
use_pipeline_uuid: ownModelPipelineUuid,
|
||||
});
|
||||
ownModelPipelineBound = true;
|
||||
}
|
||||
|
||||
await completeWizard();
|
||||
navigate('/home', { replace: true });
|
||||
} catch (err) {
|
||||
if (externalPipelineUuid && !externalPipelineBound) {
|
||||
await httpClient.deletePipeline(externalPipelineUuid).catch(() => {});
|
||||
}
|
||||
if (createdOwnModelUuid) {
|
||||
let canCleanUpOwnModelResources = !ownModelPipelineBound;
|
||||
if (ownModelPipelineBound && originalOwnModelBot) {
|
||||
try {
|
||||
await httpClient.updateBot(createdBotUuid, {
|
||||
name: originalOwnModelBot.name,
|
||||
description: originalOwnModelBot.description,
|
||||
adapter: originalOwnModelBot.adapter,
|
||||
adapter_config: originalOwnModelBot.adapter_config,
|
||||
enable: originalOwnModelBot.enable,
|
||||
use_pipeline_uuid: originalOwnModelBot.use_pipeline_uuid,
|
||||
});
|
||||
canCleanUpOwnModelResources = true;
|
||||
} catch {
|
||||
canCleanUpOwnModelResources = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (canCleanUpOwnModelResources) {
|
||||
let pipelineDeleted = ownModelPipelineUuid === null;
|
||||
if (ownModelPipelineUuid) {
|
||||
try {
|
||||
await httpClient.deletePipeline(ownModelPipelineUuid);
|
||||
pipelineDeleted = true;
|
||||
} catch {
|
||||
pipelineDeleted = false;
|
||||
}
|
||||
}
|
||||
if (pipelineDeleted) {
|
||||
await httpClient
|
||||
.deleteProviderLLMModel(createdOwnModelUuid)
|
||||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
const apiErr = err as { msg?: string };
|
||||
toast.error(
|
||||
t('wizard.createError') + (apiErr?.msg ? `: ${apiErr.msg}` : ''),
|
||||
@@ -626,6 +746,7 @@ export default function WizardPage() {
|
||||
botName,
|
||||
botDescription,
|
||||
runnerConfig,
|
||||
ownModelSelection,
|
||||
completeWizard,
|
||||
navigate,
|
||||
t,
|
||||
@@ -813,6 +934,7 @@ export default function WizardPage() {
|
||||
runnerConfigItems={selectedRunnerConfigItems}
|
||||
runnerConfigValues={runnerConfig}
|
||||
onRunnerConfigChange={setRunnerConfig}
|
||||
onOwnModelSelectionChange={setOwnModelSelection}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -851,7 +973,8 @@ export default function WizardPage() {
|
||||
disabled={
|
||||
!canProceed() ||
|
||||
isSubmitting ||
|
||||
(aiChoice === 'external' && !selectedRunner)
|
||||
(aiChoice === 'external' && !selectedRunner) ||
|
||||
(aiChoice === 'own-model' && !ownModelSelection)
|
||||
}
|
||||
>
|
||||
{isSubmitting && (
|
||||
@@ -860,7 +983,7 @@ export default function WizardPage() {
|
||||
{aiChoice === 'external'
|
||||
? t('wizard.aiEngine.createExternal')
|
||||
: aiChoice === 'own-model'
|
||||
? t('wizard.aiEngine.configurePipeline')
|
||||
? t('wizard.aiEngine.finishWithModel')
|
||||
: t('wizard.aiEngine.openWorkbench')}
|
||||
</Button>
|
||||
)}
|
||||
@@ -1071,6 +1194,13 @@ function StepBotConfig({
|
||||
return a ? extractI18nObject(a.label) : (selectedAdapterName ?? '');
|
||||
}, [adapters, selectedAdapterName]);
|
||||
|
||||
const webhookModeEnabled = useMemo(
|
||||
() =>
|
||||
isWebhookModeEnabled(adapterConfigItems, adapterConfigValues) &&
|
||||
Boolean(webhookUrl),
|
||||
[adapterConfigItems, adapterConfigValues, webhookUrl],
|
||||
);
|
||||
|
||||
// Stable callback ref
|
||||
const onAdapterConfigRef = useRef(onAdapterConfigChange);
|
||||
onAdapterConfigRef.current = onAdapterConfigChange;
|
||||
@@ -1144,7 +1274,7 @@ function StepBotConfig({
|
||||
<MessageSquare className="size-3 text-white" />
|
||||
) : selectedAdapterName === 'http_bot' ? (
|
||||
<Send className="size-3 text-white" />
|
||||
) : webhookUrl ? (
|
||||
) : webhookModeEnabled ? (
|
||||
<Webhook className="size-3 text-white" />
|
||||
) : (
|
||||
<Loader2 className="size-3 animate-spin text-white" />
|
||||
@@ -1165,12 +1295,12 @@ function StepBotConfig({
|
||||
? t('wizard.botConfig.pageBotTestPrompt')
|
||||
: selectedAdapterName === 'http_bot'
|
||||
? t('wizard.botConfig.httpTestPrompt')
|
||||
: webhookUrl
|
||||
: webhookModeEnabled
|
||||
? t('wizard.botConfig.webhookTestPrompt')
|
||||
: t('wizard.botConfig.waitingForMessage')}
|
||||
</p>
|
||||
|
||||
{!messageReceived && webhookUrl && (
|
||||
{!messageReceived && webhookModeEnabled && (
|
||||
<div className="mt-3 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap border bg-background px-2.5 py-2 text-xs">
|
||||
@@ -1324,6 +1454,7 @@ function StepAIEngine({
|
||||
runnerConfigItems,
|
||||
runnerConfigValues,
|
||||
onRunnerConfigChange,
|
||||
onOwnModelSelectionChange,
|
||||
}: {
|
||||
runnerOptions: { name: string; label: { en_US: string; zh_Hans: string } }[];
|
||||
choice: 'external' | 'own-model' | 'more-features' | null;
|
||||
@@ -1337,6 +1468,7 @@ function StepAIEngine({
|
||||
runnerConfigItems: IDynamicFormItemSchema[];
|
||||
runnerConfigValues: Record<string, unknown>;
|
||||
onRunnerConfigChange: (v: Record<string, unknown>) => void;
|
||||
onOwnModelSelectionChange: (selection: OwnModelSelection | null) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -1374,6 +1506,15 @@ function StepAIEngine({
|
||||
},
|
||||
];
|
||||
|
||||
if (choice === 'own-model') {
|
||||
return (
|
||||
<OwnModelSetup
|
||||
onBack={() => onChoiceChange(null)}
|
||||
onSelectionChange={onOwnModelSelectionChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (choice !== 'external') {
|
||||
return (
|
||||
<div className="space-y-6 max-w-4xl mx-auto">
|
||||
|
||||
@@ -34,3 +34,71 @@ export function ensureHttpBotSigningSecret(
|
||||
inbound_secret: createSigningSecret(),
|
||||
};
|
||||
}
|
||||
|
||||
export function findDefaultPipeline<
|
||||
T extends { uuid?: string; is_default?: boolean },
|
||||
>(pipelines: T[]): T | undefined {
|
||||
return pipelines.find(
|
||||
(pipeline) =>
|
||||
pipeline.is_default === true &&
|
||||
typeof pipeline.uuid === 'string' &&
|
||||
pipeline.uuid.length > 0,
|
||||
);
|
||||
}
|
||||
|
||||
interface WebhookConfigItem {
|
||||
name: string;
|
||||
show_if?: {
|
||||
field: string;
|
||||
operator: 'eq' | 'neq' | 'in';
|
||||
value: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export function isWebhookModeEnabled(
|
||||
configItems: WebhookConfigItem[],
|
||||
configValues: Record<string, unknown>,
|
||||
): boolean {
|
||||
const webhookField = configItems.find((item) => item.name === 'webhook_url');
|
||||
if (!webhookField) return false;
|
||||
if (!webhookField.show_if) return true;
|
||||
|
||||
const condition = webhookField.show_if;
|
||||
const actualValue = configValues[condition.field];
|
||||
if (condition.operator === 'eq') return actualValue === condition.value;
|
||||
if (condition.operator === 'neq') return actualValue !== condition.value;
|
||||
return (
|
||||
Array.isArray(condition.value) && condition.value.includes(actualValue)
|
||||
);
|
||||
}
|
||||
|
||||
export function configureLocalAgentPrimaryModel(
|
||||
config: Record<string, unknown>,
|
||||
modelUuid: string,
|
||||
): Record<string, unknown> {
|
||||
const aiConfig = (config.ai ?? {}) as Record<string, unknown>;
|
||||
const runnerConfig = (aiConfig.runner ?? {}) as Record<string, unknown>;
|
||||
const localAgentConfig = (aiConfig['local-agent'] ?? {}) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
const modelConfig = (localAgentConfig.model ?? {}) as Record<string, unknown>;
|
||||
|
||||
return {
|
||||
...config,
|
||||
ai: {
|
||||
...aiConfig,
|
||||
runner: { ...runnerConfig, runner: 'local-agent' },
|
||||
'local-agent': {
|
||||
...localAgentConfig,
|
||||
model: {
|
||||
...modelConfig,
|
||||
primary: modelUuid,
|
||||
fallbacks: Array.isArray(modelConfig.fallbacks)
|
||||
? modelConfig.fallbacks
|
||||
: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1859,7 +1859,35 @@ const enUS = {
|
||||
'Connect Dify, n8n, Coze, or another platform and replace the bot pipeline.',
|
||||
ownModelTitle: 'Use My Own Model',
|
||||
ownModelDescription:
|
||||
'Open the current Local Agent pipeline and configure your own model.',
|
||||
'Add a provider, then scan or manually enter a model to finish setup.',
|
||||
ownModelSetupTitle: 'Add Your Own Model',
|
||||
ownModelSetupDescription:
|
||||
'Add a model provider. Chat models are scanned automatically, or you can enter a model ID manually.',
|
||||
addProviderTitle: 'Add Provider',
|
||||
addProviderDescription:
|
||||
'Enter the provider details and API key used to connect and scan models.',
|
||||
selectModelTitle: 'Choose a Model',
|
||||
selectScannedModelTitle: 'Choose a Model',
|
||||
selectScannedModelDescription:
|
||||
'The selected model will be the primary model of a new pipeline, and the bot will switch to it.',
|
||||
scanModelMode: 'Scan Models',
|
||||
manualModelMode: 'Add Manually',
|
||||
scanningModels: 'Scanning available models…',
|
||||
noScannedModels:
|
||||
'No available chat models were found. Check the provider configuration.',
|
||||
scanModelsFailed:
|
||||
'Model scanning failed. Check the URL and API key, then try again.',
|
||||
manualFallbackFailed:
|
||||
'Automatic scanning failed. Enter a model ID supported by the provider.',
|
||||
manualFallbackEmpty:
|
||||
'No models were found. Enter a model ID supported by the provider.',
|
||||
manualModelId: 'Model ID',
|
||||
manualModelIdPlaceholder: 'For example: gpt-4o',
|
||||
manualModelIdDescription:
|
||||
'Enter the model parameter used in model requests.',
|
||||
manualModelOptions: 'Optional Model Capabilities',
|
||||
editProvider: 'Edit provider',
|
||||
rescanModels: 'Scan models again',
|
||||
moreFeaturesTitle: 'Add More Agent Features',
|
||||
moreFeaturesDescription:
|
||||
'Open the workbench to add tools, knowledge, and other capabilities.',
|
||||
@@ -1867,7 +1895,7 @@ const enUS = {
|
||||
'Select a runner for the external agent and configure its connection.',
|
||||
backToChoices: 'Back to options',
|
||||
createExternal: 'Create and Bind',
|
||||
configurePipeline: 'Configure Pipeline',
|
||||
finishWithModel: 'Use Selected Model & Finish',
|
||||
openWorkbench: 'Open Workbench',
|
||||
},
|
||||
spaceBanner: {
|
||||
|
||||
@@ -1776,14 +1776,42 @@ const jaJP = {
|
||||
'Dify、n8n、Coze などを接続し、ボットのパイプラインを置き換えます。',
|
||||
ownModelTitle: '自分のモデルを使用',
|
||||
ownModelDescription:
|
||||
'現在の Local Agent パイプラインを開き、自分のモデルを設定します。',
|
||||
'プロバイダーを追加し、モデルをスキャンまたは手動入力して設定を完了します。',
|
||||
ownModelSetupTitle: '自分のモデルを追加',
|
||||
ownModelSetupDescription:
|
||||
'モデルプロバイダーを追加すると自動スキャンされます。モデル ID の手動入力も可能です。',
|
||||
addProviderTitle: 'プロバイダーを追加',
|
||||
addProviderDescription:
|
||||
'接続とモデルスキャンに使用するプロバイダー情報と API キーを入力します。',
|
||||
selectModelTitle: 'モデルを選択',
|
||||
selectScannedModelTitle: 'モデルを選択',
|
||||
selectScannedModelDescription:
|
||||
'選択したモデルを新しいパイプラインのメインモデルに設定し、ボットをそのパイプラインへ切り替えます。',
|
||||
scanModelMode: 'モデルをスキャン',
|
||||
manualModelMode: '手動で追加',
|
||||
scanningModels: '利用可能なモデルをスキャン中…',
|
||||
noScannedModels:
|
||||
'利用可能なチャットモデルが見つかりません。プロバイダー設定を確認してください。',
|
||||
scanModelsFailed:
|
||||
'モデルのスキャンに失敗しました。URL と API キーを確認して再試行してください。',
|
||||
manualFallbackFailed:
|
||||
'自動スキャンに失敗しました。プロバイダーが対応するモデル ID を直接入力できます。',
|
||||
manualFallbackEmpty:
|
||||
'モデルが見つかりませんでした。プロバイダーが対応するモデル ID を直接入力できます。',
|
||||
manualModelId: 'モデル ID',
|
||||
manualModelIdPlaceholder: '例:gpt-4o',
|
||||
manualModelIdDescription:
|
||||
'モデルリクエストで実際に使用する model パラメーターを入力します。',
|
||||
manualModelOptions: '任意のモデル機能',
|
||||
editProvider: 'プロバイダーを編集',
|
||||
rescanModels: 'モデルを再スキャン',
|
||||
moreFeaturesTitle: 'Agent に機能を追加',
|
||||
moreFeaturesDescription:
|
||||
'ワークベンチを開き、ツールやナレッジなどの機能を追加します。',
|
||||
runnerDescription: '外部 Agent の Runner を選択し、接続を設定します。',
|
||||
backToChoices: '選択肢に戻る',
|
||||
createExternal: '作成して関連付ける',
|
||||
configurePipeline: 'パイプラインを設定',
|
||||
finishWithModel: '選択したモデルを使用して完了',
|
||||
openWorkbench: 'ワークベンチを開く',
|
||||
},
|
||||
spaceBanner: {
|
||||
|
||||
@@ -1772,13 +1772,37 @@ const zhHans = {
|
||||
externalDescription:
|
||||
'接入 Dify、n8n、Coze 等平台,并替换当前机器人的流水线。',
|
||||
ownModelTitle: '改成使用自己的模型',
|
||||
ownModelDescription: '进入当前 Local Agent 流水线,配置你自己的模型。',
|
||||
ownModelDescription:
|
||||
'添加模型供应商,自动扫描或手动填写模型以快速完成引导。',
|
||||
ownModelSetupTitle: '添加你自己的模型',
|
||||
ownModelSetupDescription:
|
||||
'先添加模型供应商,保存后会自动扫描,也可以手动填写模型 ID。',
|
||||
addProviderTitle: '添加供应商',
|
||||
addProviderDescription: '填写供应商和 API Key,用于连接并扫描模型。',
|
||||
selectModelTitle: '选择模型',
|
||||
selectScannedModelTitle: '选择一个模型',
|
||||
selectScannedModelDescription:
|
||||
'选中的模型将作为新流水线的主模型,机器人会切换到这条流水线。',
|
||||
scanModelMode: '扫描模型',
|
||||
manualModelMode: '手动添加',
|
||||
scanningModels: '正在扫描可用模型…',
|
||||
noScannedModels: '没有扫描到可用的对话模型,请检查供应商配置。',
|
||||
scanModelsFailed: '模型扫描失败,请检查地址和 API Key 后重试。',
|
||||
manualFallbackFailed: '自动扫描失败,你可以直接填写中转站支持的模型 ID。',
|
||||
manualFallbackEmpty:
|
||||
'没有扫描到可用模型,你可以直接填写中转站支持的模型 ID。',
|
||||
manualModelId: '模型 ID',
|
||||
manualModelIdPlaceholder: '例如:gpt-4o',
|
||||
manualModelIdDescription: '填写模型请求中实际使用的 model 参数。',
|
||||
manualModelOptions: '可选模型能力',
|
||||
editProvider: '修改供应商',
|
||||
rescanModels: '重新扫描模型',
|
||||
moreFeaturesTitle: '给现在的 Agent 配置更多功能',
|
||||
moreFeaturesDescription: '进入工作台,为 Agent 添加工具、知识库等能力。',
|
||||
runnerDescription: '选择外部 Agent 的 Runner 并完成连接配置。',
|
||||
backToChoices: '返回选项',
|
||||
createExternal: '创建并绑定',
|
||||
configurePipeline: '配置流水线',
|
||||
finishWithModel: '使用所选模型并完成',
|
||||
openWorkbench: '进入工作台',
|
||||
},
|
||||
spaceBanner: {
|
||||
|
||||
@@ -27,7 +27,13 @@ function loadWizardUtils() {
|
||||
return loadedModule.exports;
|
||||
}
|
||||
|
||||
const { ensureHttpBotSigningSecret, getErrorMessage } = loadWizardUtils();
|
||||
const {
|
||||
configureLocalAgentPrimaryModel,
|
||||
ensureHttpBotSigningSecret,
|
||||
findDefaultPipeline,
|
||||
getErrorMessage,
|
||||
isWebhookModeEnabled,
|
||||
} = loadWizardUtils();
|
||||
|
||||
test('generates an HTTP Bot signing secret when signatures are enabled', () => {
|
||||
const config = ensureHttpBotSigningSecret('http_bot', {
|
||||
@@ -59,3 +65,57 @@ test('extracts the backend message from structured API errors', () => {
|
||||
);
|
||||
assert.equal(getErrorMessage(new Error('Network failed')), 'Network failed');
|
||||
});
|
||||
|
||||
test('selects only a usable Workspace default pipeline', () => {
|
||||
const pipelines = [
|
||||
{ uuid: 'recent-pipeline', is_default: false },
|
||||
{ uuid: '', is_default: true },
|
||||
{ uuid: 'default-pipeline', is_default: true },
|
||||
];
|
||||
|
||||
assert.equal(findDefaultPipeline(pipelines)?.uuid, 'default-pipeline');
|
||||
});
|
||||
|
||||
test('configures the selected model as the Local Agent primary model', () => {
|
||||
const config = {
|
||||
trigger: { prefix: '!' },
|
||||
ai: {
|
||||
runner: { runner: 'plugin:external', timeout: 30 },
|
||||
'local-agent': {
|
||||
model: { primary: 'old-model', fallbacks: ['fallback-model'] },
|
||||
tools: { enabled: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const updated = configureLocalAgentPrimaryModel(config, 'selected-model');
|
||||
|
||||
assert.equal(updated.ai.runner.runner, 'local-agent');
|
||||
assert.equal(updated.ai.runner.timeout, 30);
|
||||
assert.equal(updated.ai['local-agent'].model.primary, 'selected-model');
|
||||
assert.deepEqual(updated.ai['local-agent'].model.fallbacks, [
|
||||
'fallback-model',
|
||||
]);
|
||||
assert.deepEqual(updated.ai['local-agent'].tools, { enabled: true });
|
||||
assert.deepEqual(updated.trigger, { prefix: '!' });
|
||||
});
|
||||
|
||||
test('shows webhook guidance only when the adapter webhook mode is active', () => {
|
||||
const dualModeFields = [
|
||||
{
|
||||
name: 'webhook_url',
|
||||
show_if: { field: 'enable-webhook', operator: 'eq', value: true },
|
||||
},
|
||||
];
|
||||
|
||||
assert.equal(
|
||||
isWebhookModeEnabled(dualModeFields, { 'enable-webhook': false }),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
isWebhookModeEnabled(dualModeFields, { 'enable-webhook': true }),
|
||||
true,
|
||||
);
|
||||
assert.equal(isWebhookModeEnabled([{ name: 'webhook_url' }], {}), true);
|
||||
assert.equal(isWebhookModeEnabled([], {}), false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user