mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-22 02:07:13 +00:00
feat(wizard): add inbound bot verification
This commit is contained in:
@@ -113,6 +113,24 @@ class BotsRouterGroup(group.RouterGroup):
|
|||||||
)
|
)
|
||||||
return self.success(data={'sent': True})
|
return self.success(data={'sent': True})
|
||||||
|
|
||||||
|
@self.route(
|
||||||
|
'/<bot_uuid>/test-inbound',
|
||||||
|
methods=['POST'],
|
||||||
|
auth_type=group.AuthType.USER_TOKEN,
|
||||||
|
permission=Permission.RESOURCE_MANAGE,
|
||||||
|
)
|
||||||
|
async def _(bot_uuid: str, request_context: RequestContext) -> str:
|
||||||
|
json_data = await quart.request.get_json(silent=True) or {}
|
||||||
|
try:
|
||||||
|
result = await self.ap.bot_service.send_http_bot_test_message(
|
||||||
|
request_context,
|
||||||
|
bot_uuid,
|
||||||
|
str(json_data.get('message') or ''),
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
return self.http_status(400, -1, str(exc))
|
||||||
|
return self.success(data=result)
|
||||||
|
|
||||||
@self.route(
|
@self.route(
|
||||||
'/<bot_uuid>/admins',
|
'/<bot_uuid>/admins',
|
||||||
methods=['GET'],
|
methods=['GET'],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
import json
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
|
||||||
from ....core import app
|
from ....core import app
|
||||||
@@ -8,6 +9,8 @@ from ....entity.persistence import bot as persistence_bot
|
|||||||
from ....entity.persistence import pipeline as persistence_pipeline
|
from ....entity.persistence import pipeline as persistence_pipeline
|
||||||
from ....workspace.errors import WorkspaceNotFoundError
|
from ....workspace.errors import WorkspaceNotFoundError
|
||||||
from .tenant import TenantContext, require_workspace_uuid, scope_statement
|
from .tenant import TenantContext, require_workspace_uuid, scope_statement
|
||||||
|
from ....utils import httpclient
|
||||||
|
from ....platform.sources import http_bot_signing
|
||||||
|
|
||||||
|
|
||||||
class BotService:
|
class BotService:
|
||||||
@@ -80,6 +83,7 @@ class BotService:
|
|||||||
'wecomcs',
|
'wecomcs',
|
||||||
'LINE',
|
'LINE',
|
||||||
'lark',
|
'lark',
|
||||||
|
'http_bot',
|
||||||
]:
|
]:
|
||||||
webhook_prefix = self.ap.instance_config.data['api'].get('webhook_prefix', 'http://127.0.0.1:5300')
|
webhook_prefix = self.ap.instance_config.data['api'].get('webhook_prefix', 'http://127.0.0.1:5300')
|
||||||
extra_webhook_prefix = self.ap.instance_config.data['api'].get('extra_webhook_prefix', '')
|
extra_webhook_prefix = self.ap.instance_config.data['api'].get('extra_webhook_prefix', '')
|
||||||
@@ -216,6 +220,53 @@ class BotService:
|
|||||||
|
|
||||||
return [log.to_json() for log in logs], total_count
|
return [log.to_json() for log in logs], total_count
|
||||||
|
|
||||||
|
async def send_http_bot_test_message(
|
||||||
|
self,
|
||||||
|
context: TenantContext,
|
||||||
|
bot_uuid: str,
|
||||||
|
message: str,
|
||||||
|
) -> dict:
|
||||||
|
"""Send a signed test message through the HTTP Bot public ingress."""
|
||||||
|
bot = await self.get_bot(context, bot_uuid, include_secret=True)
|
||||||
|
if bot is None:
|
||||||
|
raise WorkspaceNotFoundError('Bot not found')
|
||||||
|
if bot.get('adapter') != 'http_bot':
|
||||||
|
raise ValueError('Inbound test is only available for HTTP Bot')
|
||||||
|
if not bot.get('enable'):
|
||||||
|
raise ValueError('Bot must be enabled before sending a test message')
|
||||||
|
|
||||||
|
text = message.strip()
|
||||||
|
if not text or len(text) > 2000:
|
||||||
|
raise ValueError('Test message must contain 1 to 2000 characters')
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
'session_id': f'wizard-{uuid.uuid4().hex}',
|
||||||
|
'sender': {'id': 'wizard-user', 'name': 'Wizard Test'},
|
||||||
|
'message': [{'type': 'Plain', 'text': text}],
|
||||||
|
}
|
||||||
|
body = json.dumps(payload, ensure_ascii=False, separators=(',', ':')).encode()
|
||||||
|
config = bot.get('adapter_config') or {}
|
||||||
|
headers = {'Content-Type': 'application/json'}
|
||||||
|
if config.get('signature_required', True):
|
||||||
|
secret = str(config.get('inbound_secret') or '')
|
||||||
|
if not secret:
|
||||||
|
raise ValueError('HTTP Bot inbound signing secret is required')
|
||||||
|
timestamp, signature = http_bot_signing.sign(secret, body)
|
||||||
|
headers[http_bot_signing.HEADER_TIMESTAMP] = timestamp
|
||||||
|
headers[http_bot_signing.HEADER_SIGNATURE] = signature
|
||||||
|
|
||||||
|
port = int(self.ap.instance_config.data.get('api', {}).get('port', 5300))
|
||||||
|
session = httpclient.get_session()
|
||||||
|
async with session.post(
|
||||||
|
f'http://127.0.0.1:{port}/bots/{bot_uuid}',
|
||||||
|
data=body,
|
||||||
|
headers=headers,
|
||||||
|
) as response:
|
||||||
|
result = await httpclient.read_json_limited(response)
|
||||||
|
if response.status not in {200, 202}:
|
||||||
|
raise ValueError(result.get('msg') or f'HTTP Bot test failed with status {response.status}')
|
||||||
|
return result.get('data') or {}
|
||||||
|
|
||||||
async def send_message(
|
async def send_message(
|
||||||
self,
|
self,
|
||||||
context: TenantContext,
|
context: TenantContext,
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ Source: src/langbot/pkg/api/http/service/bot.py
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import AsyncMock, Mock, patch
|
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from langbot.pkg.api.http.service.bot import BotService
|
from langbot.pkg.api.http.service.bot import BotService
|
||||||
@@ -241,6 +242,29 @@ class TestBotServiceGetRuntimeBotInfo:
|
|||||||
assert result['adapter_runtime_values']['webhook_url'] == '/bots/wecom-uuid'
|
assert result['adapter_runtime_values']['webhook_url'] == '/bots/wecom-uuid'
|
||||||
assert result['adapter_runtime_values']['webhook_full_url'] == 'http://127.0.0.1:5300/bots/wecom-uuid'
|
assert result['adapter_runtime_values']['webhook_full_url'] == 'http://127.0.0.1:5300/bots/wecom-uuid'
|
||||||
|
|
||||||
|
async def test_get_runtime_bot_info_returns_webhook_for_http_bot(self):
|
||||||
|
ap = SimpleNamespace(
|
||||||
|
instance_config=SimpleNamespace(
|
||||||
|
data={'api': {'webhook_prefix': 'https://bot.example.com'}}
|
||||||
|
),
|
||||||
|
platform_mgr=SimpleNamespace(get_bot_by_uuid=AsyncMock(return_value=None)),
|
||||||
|
)
|
||||||
|
service = BotService(ap)
|
||||||
|
service.get_bot = AsyncMock(
|
||||||
|
return_value={
|
||||||
|
'uuid': 'http-bot-uuid',
|
||||||
|
'name': 'HTTP Bot',
|
||||||
|
'adapter': 'http_bot',
|
||||||
|
'adapter_config': {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await service.get_runtime_bot_info(WORKSPACE_UUID, 'http-bot-uuid')
|
||||||
|
|
||||||
|
assert result['adapter_runtime_values']['webhook_full_url'] == (
|
||||||
|
'https://bot.example.com/bots/http-bot-uuid'
|
||||||
|
)
|
||||||
|
|
||||||
async def test_get_runtime_bot_info_no_webhook_for_telegram(self):
|
async def test_get_runtime_bot_info_no_webhook_for_telegram(self):
|
||||||
"""Returns no webhook URL for non-webhook adapters like telegram."""
|
"""Returns no webhook URL for non-webhook adapters like telegram."""
|
||||||
# Setup
|
# Setup
|
||||||
@@ -605,6 +629,77 @@ class TestBotServiceListEventLogs:
|
|||||||
assert total == 5
|
assert total == 5
|
||||||
|
|
||||||
|
|
||||||
|
class TestBotServiceHttpBotInboundTest:
|
||||||
|
async def test_sends_signed_message_through_public_ingress(self):
|
||||||
|
ap = SimpleNamespace(
|
||||||
|
instance_config=SimpleNamespace(data={'api': {'port': 5300}}),
|
||||||
|
)
|
||||||
|
service = BotService(ap)
|
||||||
|
service.get_bot = AsyncMock(
|
||||||
|
return_value={
|
||||||
|
'uuid': 'http-bot-uuid',
|
||||||
|
'adapter': 'http_bot',
|
||||||
|
'adapter_config': {
|
||||||
|
'signature_required': True,
|
||||||
|
'inbound_secret': 'test-secret',
|
||||||
|
},
|
||||||
|
'enable': True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response = MagicMock(status=202)
|
||||||
|
session = MagicMock()
|
||||||
|
session.post.return_value.__aenter__ = AsyncMock(return_value=response)
|
||||||
|
session.post.return_value.__aexit__ = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch('langbot.pkg.api.http.service.bot.httpclient.get_session', return_value=session),
|
||||||
|
patch(
|
||||||
|
'langbot.pkg.api.http.service.bot.httpclient.read_json_limited',
|
||||||
|
new=AsyncMock(
|
||||||
|
return_value={
|
||||||
|
'code': 0,
|
||||||
|
'data': {
|
||||||
|
'session_id': 'wizard-session',
|
||||||
|
'accepted_message_id': 'in-message',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
result = await service.send_http_bot_test_message(
|
||||||
|
WORKSPACE_UUID,
|
||||||
|
'http-bot-uuid',
|
||||||
|
'hello',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result['accepted_message_id'] == 'in-message'
|
||||||
|
request = session.post.call_args
|
||||||
|
assert request.args[0] == 'http://127.0.0.1:5300/bots/http-bot-uuid'
|
||||||
|
payload = json.loads(request.kwargs['data'])
|
||||||
|
assert payload['message'] == [{'type': 'Plain', 'text': 'hello'}]
|
||||||
|
headers = request.kwargs['headers']
|
||||||
|
assert headers['X-LB-Timestamp']
|
||||||
|
assert headers['X-LB-Signature'].startswith('sha256=')
|
||||||
|
|
||||||
|
async def test_rejects_non_http_bot(self):
|
||||||
|
service = BotService(SimpleNamespace())
|
||||||
|
service.get_bot = AsyncMock(
|
||||||
|
return_value={
|
||||||
|
'uuid': 'telegram-bot',
|
||||||
|
'adapter': 'telegram',
|
||||||
|
'adapter_config': {},
|
||||||
|
'enable': True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match='only available for HTTP Bot'):
|
||||||
|
await service.send_http_bot_test_message(
|
||||||
|
WORKSPACE_UUID,
|
||||||
|
'telegram-bot',
|
||||||
|
'hello',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestBotServiceSendMessage:
|
class TestBotServiceSendMessage:
|
||||||
"""Tests for send_message method."""
|
"""Tests for send_message method."""
|
||||||
|
|
||||||
|
|||||||
@@ -461,6 +461,15 @@ export class BackendClient extends BaseHttpClient {
|
|||||||
return this.post(`/api/v1/platform/bots/${botId}/logs`, request);
|
return this.post(`/api/v1/platform/bots/${botId}/logs`, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public testHttpBotInbound(
|
||||||
|
botId: string,
|
||||||
|
message: string,
|
||||||
|
): Promise<{ session_id: string; accepted_message_id: string }> {
|
||||||
|
return this.post(`/api/v1/platform/bots/${botId}/test-inbound`, {
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public getBotSessions(
|
public getBotSessions(
|
||||||
botId: string,
|
botId: string,
|
||||||
limit: number = 100,
|
limit: number = 100,
|
||||||
|
|||||||
+125
-14
@@ -14,6 +14,9 @@ import {
|
|||||||
Cable,
|
Cable,
|
||||||
Settings2,
|
Settings2,
|
||||||
Blocks,
|
Blocks,
|
||||||
|
Copy,
|
||||||
|
Send,
|
||||||
|
Webhook,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
@@ -45,6 +48,7 @@ import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
|
|||||||
import i18n from 'i18next';
|
import i18n from 'i18next';
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -996,6 +1000,10 @@ function StepBotConfig({
|
|||||||
extraWebhookUrl: string;
|
extraWebhookUrl: string;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [testMessage, setTestMessage] = useState(
|
||||||
|
t('wizard.botConfig.httpTestDefaultMessage'),
|
||||||
|
);
|
||||||
|
const [isSendingTest, setIsSendingTest] = useState(false);
|
||||||
|
|
||||||
const adapterLabel = useMemo(() => {
|
const adapterLabel = useMemo(() => {
|
||||||
const a = adapters.find((ad) => ad.name === selectedAdapterName);
|
const a = adapters.find((ad) => ad.name === selectedAdapterName);
|
||||||
@@ -1010,6 +1018,29 @@ function StepBotConfig({
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const copyWebhookUrl = useCallback(async () => {
|
||||||
|
if (!webhookUrl) return;
|
||||||
|
await navigator.clipboard.writeText(webhookUrl);
|
||||||
|
toast.success(t('common.copySuccess'));
|
||||||
|
}, [t, webhookUrl]);
|
||||||
|
|
||||||
|
const sendHttpBotTest = useCallback(async () => {
|
||||||
|
if (!createdBotUuid || !testMessage.trim()) return;
|
||||||
|
setIsSendingTest(true);
|
||||||
|
try {
|
||||||
|
await httpClient.testHttpBotInbound(createdBotUuid, testMessage.trim());
|
||||||
|
toast.success(t('wizard.botConfig.httpTestAccepted'));
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
t('wizard.botConfig.httpTestFailed', {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsSendingTest(false);
|
||||||
|
}
|
||||||
|
}, [createdBotUuid, testMessage, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-5xl mx-auto space-y-6">
|
<div className="max-w-5xl mx-auto space-y-6">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
@@ -1019,6 +1050,100 @@ function StepBotConfig({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{botSaved && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'border px-4 py-3',
|
||||||
|
messageReceived
|
||||||
|
? 'border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950/30'
|
||||||
|
: 'border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950/30',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-full',
|
||||||
|
messageReceived ? 'bg-green-500' : 'bg-amber-500',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{messageReceived ? (
|
||||||
|
<Check className="size-3 text-white" />
|
||||||
|
) : selectedAdapterName === 'http_bot' ? (
|
||||||
|
<Send className="size-3 text-white" />
|
||||||
|
) : webhookUrl ? (
|
||||||
|
<Webhook className="size-3 text-white" />
|
||||||
|
) : (
|
||||||
|
<Loader2 className="size-3 animate-spin text-white" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
'text-sm font-medium',
|
||||||
|
messageReceived
|
||||||
|
? 'text-green-800 dark:text-green-200'
|
||||||
|
: 'text-amber-800 dark:text-amber-200',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{messageReceived
|
||||||
|
? t('wizard.botConfig.messageReceived')
|
||||||
|
: selectedAdapterName === 'http_bot'
|
||||||
|
? t('wizard.botConfig.httpTestPrompt')
|
||||||
|
: webhookUrl
|
||||||
|
? t('wizard.botConfig.webhookTestPrompt')
|
||||||
|
: t('wizard.botConfig.waitingForMessage')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{!messageReceived && webhookUrl && (
|
||||||
|
<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">
|
||||||
|
{webhookUrl}
|
||||||
|
</code>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
className="size-9 shrink-0"
|
||||||
|
onClick={copyWebhookUrl}
|
||||||
|
title={t('common.copy')}
|
||||||
|
>
|
||||||
|
<Copy className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedAdapterName === 'http_bot' && (
|
||||||
|
<div className="flex flex-col gap-2 sm:flex-row">
|
||||||
|
<Input
|
||||||
|
value={testMessage}
|
||||||
|
onChange={(event) => setTestMessage(event.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter') void sendHttpBotTest();
|
||||||
|
}}
|
||||||
|
className="bg-background"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void sendHttpBotTest()}
|
||||||
|
disabled={isSendingTest || !testMessage.trim()}
|
||||||
|
className="shrink-0"
|
||||||
|
>
|
||||||
|
{isSendingTest ? (
|
||||||
|
<Loader2 className="mr-1.5 size-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Send className="mr-1.5 size-4" />
|
||||||
|
)}
|
||||||
|
{t('wizard.botConfig.sendHttpTest')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="grid gap-6 grid-cols-1 lg:grid-cols-2">
|
<div className="grid gap-6 grid-cols-1 lg:grid-cols-2">
|
||||||
{/* Left column: Adapter config form */}
|
{/* Left column: Adapter config form */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -1082,20 +1207,6 @@ function StepBotConfig({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Bot and inbound-message verification status */}
|
|
||||||
{botSaved && (
|
|
||||||
<div className="flex items-center gap-2 px-4 py-3 rounded-lg border border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950/30">
|
|
||||||
<div className="w-5 h-5 rounded-full bg-green-500 flex items-center justify-center shrink-0">
|
|
||||||
<Check className="w-3 h-3 text-white" />
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-green-700 dark:text-green-300">
|
|
||||||
{messageReceived
|
|
||||||
? t('wizard.botConfig.messageReceived')
|
|
||||||
: t('wizard.botConfig.waitingForMessage')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right column: Bot logs */}
|
{/* Right column: Bot logs */}
|
||||||
|
|||||||
@@ -1831,6 +1831,17 @@ const enUS = {
|
|||||||
'The bot is enabled. Send it a message from your IM platform to continue.',
|
'The bot is enabled. Send it a message from your IM platform to continue.',
|
||||||
messageReceived:
|
messageReceived:
|
||||||
'The bot received an IM message. You can continue to the next step.',
|
'The bot received an IM message. You can continue to the next step.',
|
||||||
|
webhookTestPrompt:
|
||||||
|
'The callback URL is ready. Configure it on the external platform, then send the bot a real message.',
|
||||||
|
httpTestPrompt:
|
||||||
|
'HTTP Bot is enabled. Send a real inbound message here to verify the connection.',
|
||||||
|
httpTestDefaultMessage: 'Hello, this is a connection test message.',
|
||||||
|
sendHttpTest: 'Send Test Message',
|
||||||
|
httpTestAccepted:
|
||||||
|
'The test message was accepted. It will appear in the log shortly.',
|
||||||
|
httpTestMissingSecret:
|
||||||
|
'Enter an inbound signing secret and save the configuration first.',
|
||||||
|
httpTestFailed: 'Failed to send the test message: {{error}}',
|
||||||
logsTitle: 'Bot Logs',
|
logsTitle: 'Bot Logs',
|
||||||
logsDescription:
|
logsDescription:
|
||||||
'Monitor bot activity to verify the platform connection is working.',
|
'Monitor bot activity to verify the platform connection is working.',
|
||||||
|
|||||||
@@ -1748,6 +1748,17 @@ const jaJP = {
|
|||||||
'ボットが有効になりました。続行するには IM からメッセージを送信してください。',
|
'ボットが有効になりました。続行するには IM からメッセージを送信してください。',
|
||||||
messageReceived:
|
messageReceived:
|
||||||
'ボットが IM メッセージを受信しました。次のステップに進めます。',
|
'ボットが IM メッセージを受信しました。次のステップに進めます。',
|
||||||
|
webhookTestPrompt:
|
||||||
|
'コールバック URL の準備ができました。外部プラットフォームに設定し、ボットへ実際のメッセージを送信してください。',
|
||||||
|
httpTestPrompt:
|
||||||
|
'HTTP Bot が有効になりました。実際の受信メッセージを送信して接続を確認できます。',
|
||||||
|
httpTestDefaultMessage: 'こんにちは。これは接続テストメッセージです。',
|
||||||
|
sendHttpTest: 'テストメッセージを送信',
|
||||||
|
httpTestAccepted:
|
||||||
|
'テストメッセージを受け付けました。まもなくログに表示されます。',
|
||||||
|
httpTestMissingSecret:
|
||||||
|
'受信署名シークレットを入力し、先に設定を保存してください。',
|
||||||
|
httpTestFailed: 'テストメッセージの送信に失敗しました:{{error}}',
|
||||||
logsTitle: 'ボットログ',
|
logsTitle: 'ボットログ',
|
||||||
logsDescription:
|
logsDescription:
|
||||||
'ボットの活動を監視して、プラットフォーム接続が正常に動作していることを確認します。',
|
'ボットの活動を監視して、プラットフォーム接続が正常に動作していることを確認します。',
|
||||||
|
|||||||
@@ -1751,6 +1751,14 @@ const zhHans = {
|
|||||||
botSaved: '机器人配置已保存并启用,请查看日志确认连接正常。',
|
botSaved: '机器人配置已保存并启用,请查看日志确认连接正常。',
|
||||||
waitingForMessage: '机器人已启用。请在 IM 中向机器人发送一条消息以继续。',
|
waitingForMessage: '机器人已启用。请在 IM 中向机器人发送一条消息以继续。',
|
||||||
messageReceived: '机器人已成功收到 IM 消息,可以进入下一步。',
|
messageReceived: '机器人已成功收到 IM 消息,可以进入下一步。',
|
||||||
|
webhookTestPrompt:
|
||||||
|
'回调地址已就绪。将它配置到外部平台,然后向机器人发送一条真实消息。',
|
||||||
|
httpTestPrompt: 'HTTP Bot 已启用。可直接发送一条真实入站消息验证连接。',
|
||||||
|
httpTestDefaultMessage: '你好,这是一条连接测试消息。',
|
||||||
|
sendHttpTest: '发送测试消息',
|
||||||
|
httpTestAccepted: '测试消息已被机器人接收,请稍候查看日志。',
|
||||||
|
httpTestMissingSecret: '请先填写入站签名密钥并重新保存。',
|
||||||
|
httpTestFailed: '测试消息发送失败:{{error}}',
|
||||||
logsTitle: '机器人日志',
|
logsTitle: '机器人日志',
|
||||||
logsDescription: '监控机器人活动,确认平台连接是否正常工作。',
|
logsDescription: '监控机器人活动,确认平台连接是否正常工作。',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ export default defineConfig(({ mode }) => {
|
|||||||
target: apiProxyTarget,
|
target: apiProxyTarget,
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
|
'/bots': {
|
||||||
|
target: apiProxyTarget,
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
Reference in New Issue
Block a user