mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-27 12:47:13 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b121c66f18 | |||
| 536534865e | |||
| ff528e8e0b | |||
| f7914a8900 | |||
| c18fc9dfe3 | |||
| bc40418bda | |||
| 45ed6efceb | |||
| c67a503532 | |||
| 8b14d2ab8e | |||
| 97428310b9 | |||
| 338ee733cb | |||
| c3299fd1a6 |
@@ -1,59 +0,0 @@
|
|||||||
name: Build and deploy production
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [deploy/prod]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: langbot-production
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
env:
|
|
||||||
CORE_IMAGE: ${{ secrets.DOCKER_USERNAME }}/langbot
|
|
||||||
CLOUD_IMAGE: ${{ secrets.DOCKER_USERNAME }}/langbot-cloud-core
|
|
||||||
SPACE_REF: 58253c53933f95d81b035fbe2efedb55b6c1a82b
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-deploy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
environment: production
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: docker/setup-buildx-action@v3
|
|
||||||
- uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
|
||||||
- name: Build exact Core image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
${{ env.CORE_IMAGE }}:prod-${{ github.sha }}
|
|
||||||
${{ env.CORE_IMAGE }}:deploy-prod
|
|
||||||
cache-from: type=gha,scope=core-prod
|
|
||||||
cache-to: type=gha,mode=max,scope=core-prod
|
|
||||||
- name: Checkout production Cloud adapter
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
repository: langbot-app/langbot-space
|
|
||||||
ref: ${{ env.SPACE_REF }}
|
|
||||||
token: ${{ secrets.CLA_PAT }}
|
|
||||||
path: .space
|
|
||||||
- name: Build exact Cloud Core image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .space
|
|
||||||
file: .space/Dockerfile.cloud
|
|
||||||
push: true
|
|
||||||
build-args: LANGBOT_CORE_IMAGE=${{ env.CORE_IMAGE }}:prod-${{ github.sha }}
|
|
||||||
tags: |
|
|
||||||
${{ env.CLOUD_IMAGE }}:prod-${{ github.sha }}
|
|
||||||
${{ env.CLOUD_IMAGE }}:deploy-prod
|
|
||||||
cache-from: type=gha,scope=cloud-core-prod
|
|
||||||
cache-to: type=gha,mode=max,scope=cloud-core-prod
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -Eeuo pipefail
|
|
||||||
|
|
||||||
cd /opt/langbot-cloud-prod
|
|
||||||
TAG=${1:?usage: deploy.sh prod-<40-char-sha>}
|
|
||||||
[[ "$TAG" =~ ^prod-[0-9a-f]{40}$ ]] || { echo 'invalid immutable image tag' >&2; exit 2; }
|
|
||||||
[[ -s .env ]] || { echo '/opt/langbot-cloud-prod/.env is missing' >&2; exit 3; }
|
|
||||||
|
|
||||||
rendered_compose=$(docker compose config)
|
|
||||||
grep -Fq 'LANGBOT_SPACE_CONTROL_PLANE_URL: https://space.langbot.app' <<<"$rendered_compose" || {
|
|
||||||
echo 'Cloud control-plane URL must be https://space.langbot.app' >&2
|
|
||||||
exit 4
|
|
||||||
}
|
|
||||||
grep -Fq 'SPACE__URL: https://space.langbot.app' <<<"$rendered_compose" || {
|
|
||||||
echo 'Cloud user-facing Space URL must be https://space.langbot.app' >&2
|
|
||||||
exit 5
|
|
||||||
}
|
|
||||||
grep -Eq 'LANGBOT_TELEMETRY_INGEST_TOKEN: .+' <<<"$rendered_compose" || {
|
|
||||||
echo 'Cloud telemetry ingest token must be configured' >&2
|
|
||||||
exit 6
|
|
||||||
}
|
|
||||||
|
|
||||||
update_env() {
|
|
||||||
local key=$1 value=$2
|
|
||||||
python3 - "$key" "$value" <<'PY'
|
|
||||||
from pathlib import Path
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
path = Path('.env')
|
|
||||||
key, value = sys.argv[1:]
|
|
||||||
lines = path.read_text().splitlines()
|
|
||||||
updated = False
|
|
||||||
for index, line in enumerate(lines):
|
|
||||||
if line.startswith(f'{key}='):
|
|
||||||
lines[index] = f'{key}={value}'
|
|
||||||
updated = True
|
|
||||||
break
|
|
||||||
if not updated:
|
|
||||||
lines.append(f'{key}={value}')
|
|
||||||
temporary = Path('.env.tmp')
|
|
||||||
temporary.write_text('\n'.join(lines) + '\n')
|
|
||||||
os.chmod(temporary, 0o600)
|
|
||||||
temporary.replace(path)
|
|
||||||
PY
|
|
||||||
}
|
|
||||||
update_env LANGBOT_IMAGE_TAG "$TAG"
|
|
||||||
set -a
|
|
||||||
. ./.env
|
|
||||||
set +a
|
|
||||||
: "${CLOUD_V2_CONTROL_PLANE_TOKEN:?CLOUD_V2_CONTROL_PLANE_TOKEN is required}"
|
|
||||||
|
|
||||||
for attempt in 1 2 3 4 5; do
|
|
||||||
if docker compose pull postgres redis migrate plugin-runtime core; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if [ "$attempt" -eq 5 ]; then
|
|
||||||
echo "docker compose pull failed after $attempt attempts" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
delay=$((attempt * 10))
|
|
||||||
echo "docker compose pull failed (attempt $attempt/5); retrying in ${delay}s" >&2
|
|
||||||
sleep "$delay"
|
|
||||||
done
|
|
||||||
docker compose up -d postgres redis
|
|
||||||
for _ in $(seq 1 60); do
|
|
||||||
if docker compose exec -T postgres pg_isready -U langbot_operator -d langbot >/dev/null 2>&1; then break; fi
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
docker compose exec -T postgres pg_isready -U langbot_operator -d langbot >/dev/null
|
|
||||||
|
|
||||||
docker compose exec -T postgres psql -v ON_ERROR_STOP=1 -U langbot_operator -d langbot \
|
|
||||||
-v runtime_password="$POSTGRES_RUNTIME_PASSWORD" <<'SQL'
|
|
||||||
SELECT format('CREATE ROLE langbot_runtime LOGIN PASSWORD %L', :'runtime_password')
|
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'langbot_runtime')\gexec
|
|
||||||
ALTER ROLE langbot_runtime PASSWORD :'runtime_password';
|
|
||||||
GRANT CONNECT ON DATABASE langbot TO langbot_runtime;
|
|
||||||
REVOKE CREATE ON SCHEMA public FROM PUBLIC, langbot_runtime;
|
|
||||||
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM langbot_runtime;
|
|
||||||
REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM langbot_runtime;
|
|
||||||
ALTER DEFAULT PRIVILEGES FOR ROLE langbot_operator IN SCHEMA public REVOKE ALL ON TABLES FROM langbot_runtime;
|
|
||||||
ALTER DEFAULT PRIVILEGES FOR ROLE langbot_operator IN SCHEMA public REVOKE ALL ON SEQUENCES FROM langbot_runtime;
|
|
||||||
GRANT USAGE ON SCHEMA public TO langbot_runtime;
|
|
||||||
SQL
|
|
||||||
|
|
||||||
docker compose --profile tools run --rm migrate
|
|
||||||
|
|
||||||
docker compose up -d --remove-orphans plugin-runtime core
|
|
||||||
for _ in $(seq 1 90); do
|
|
||||||
if docker compose exec -T core python -c 'import urllib.request; urllib.request.urlopen("http://127.0.0.1:5300/healthz", timeout=3)' >/dev/null 2>&1; then
|
|
||||||
docker compose ps
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
docker compose logs --tail=200 core plugin-runtime >&2
|
|
||||||
exit 1
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: pgvector/pgvector:pg17
|
|
||||||
container_name: langbot-cloud-postgres
|
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
|
||||||
POSTGRES_DB: langbot
|
|
||||||
POSTGRES_USER: langbot_operator
|
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_OPERATOR_PASSWORD}
|
|
||||||
volumes:
|
|
||||||
- postgres-data:/var/lib/postgresql/data
|
|
||||||
healthcheck:
|
|
||||||
test: [CMD-SHELL, "pg_isready -U langbot_operator -d langbot"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 30
|
|
||||||
networks: [internal]
|
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:7.4-alpine
|
|
||||||
container_name: langbot-cloud-redis
|
|
||||||
restart: unless-stopped
|
|
||||||
command: [redis-server, --appendonly, "yes", --requirepass, "${REDIS_PASSWORD}"]
|
|
||||||
volumes:
|
|
||||||
- redis-data:/data
|
|
||||||
healthcheck:
|
|
||||||
test: [CMD-SHELL, "redis-cli -a \"$${REDIS_PASSWORD}\" ping | grep PONG"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 20
|
|
||||||
environment:
|
|
||||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
|
||||||
networks: [internal]
|
|
||||||
|
|
||||||
migrate:
|
|
||||||
image: rockchin/langbot-cloud-core:${LANGBOT_IMAGE_TAG}
|
|
||||||
profiles: [tools]
|
|
||||||
command: [uv, run, langbot, migrate, --cloud]
|
|
||||||
environment: &core-env
|
|
||||||
TZ: Asia/Shanghai
|
|
||||||
SYSTEM__INSTANCE_ID: ${CLOUD_V2_INSTANCE_UUID}
|
|
||||||
SYSTEM__EDITION: cloud
|
|
||||||
SYSTEM__RECOVERY_KEY: ${SYSTEM_RECOVERY_KEY}
|
|
||||||
SYSTEM__JWT__SECRET: ${JWT_SECRET}
|
|
||||||
SYSTEM__LIMITATION__MAX_BOTS: "2"
|
|
||||||
SYSTEM__LIMITATION__MAX_PIPELINES: "3"
|
|
||||||
SYSTEM__LIMITATION__MAX_EXTENSIONS: "3"
|
|
||||||
SYSTEM__LIMITATION__MAX_KNOWLEDGE_BASES: "2"
|
|
||||||
API__WEBHOOK_PREFIX: https://cloud.langbot.app
|
|
||||||
API__WEBUI_URL: https://cloud.langbot.app
|
|
||||||
WORKSPACE__INVITATIONS__PUBLIC_WEB_URL: https://cloud.langbot.app
|
|
||||||
DATABASE__USE: postgresql
|
|
||||||
DATABASE__POSTGRESQL__URL: postgresql+asyncpg://langbot_runtime:${POSTGRES_RUNTIME_PASSWORD}@postgres:5432/langbot
|
|
||||||
DATABASE__CLOUD_MIGRATION__OPERATOR_DSN_ENV: LANGBOT_CLOUD_MIGRATION_DSN
|
|
||||||
LANGBOT_CLOUD_MIGRATION_DSN: postgresql://langbot_operator:${POSTGRES_OPERATOR_PASSWORD}@postgres:5432/langbot
|
|
||||||
VDB__USE: pgvector
|
|
||||||
VDB__PGVECTOR__USE_BUSINESS_DATABASE: "true"
|
|
||||||
VDB__PGVECTOR__ALLOWED_DIMENSIONS: "384,512,768,1024,1536"
|
|
||||||
PLUGIN__ENABLE: "true"
|
|
||||||
PLUGIN__RUNTIME_WS_URL: ws://plugin-runtime:5400/control/ws
|
|
||||||
PLUGIN__DISPLAY_PLUGIN_DEBUG_URL: wss://cloud.langbot.app/plugin/debug/ws
|
|
||||||
PLUGIN__WORKER__MAX_CPUS: "0.25"
|
|
||||||
PLUGIN__WORKER__MAX_MEMORY_MB: "256"
|
|
||||||
PLUGIN__WORKER__MAX_PIDS: "128"
|
|
||||||
PLUGIN__WORKER__MAX_WORKERS: "16"
|
|
||||||
PLUGIN__WORKER__MAX_TOTAL_CPUS: "4.0"
|
|
||||||
PLUGIN__WORKER__MAX_TOTAL_MEMORY_MB: "4096"
|
|
||||||
PLUGIN__WORKER__REQUIRE_HARD_LIMITS: "true"
|
|
||||||
LANGBOT_PLUGIN_RUNTIME_CONTROL_TOKEN: ${PLUGIN_RUNTIME_CONTROL_TOKEN}
|
|
||||||
# Cloud v2 currently grants no managed Box capability. Keep the shared
|
|
||||||
# runtime deployed but disable Core integration until a hard-quota-capable
|
|
||||||
# backend can satisfy the fail-closed Cloud readiness contract.
|
|
||||||
BOX__ENABLED: "false"
|
|
||||||
BOX__BACKEND: nsjail
|
|
||||||
BOX__RUNTIME__ENDPOINT: ws://box:5410
|
|
||||||
BOX__ADMISSION__REQUIRED: "true"
|
|
||||||
BOX__ADMISSION__LOGICAL_SESSION_ID: global
|
|
||||||
BOX__ADMISSION__REQUIRED_BACKEND: nsjail
|
|
||||||
BOX__ADMISSION__MAX_SESSIONS: "1"
|
|
||||||
BOX__ADMISSION__MAX_MANAGED_PROCESSES: "0"
|
|
||||||
BOX__ADMISSION__CPUS: "0.25"
|
|
||||||
BOX__ADMISSION__MEMORY_MB: "256"
|
|
||||||
BOX__ADMISSION__WORKSPACE_QUOTA_MB: "256"
|
|
||||||
BOX__LOCAL__HOST_ROOT: /app/data/box
|
|
||||||
BOX__LOCAL__DEFAULT_WORKSPACE: /app/data/box
|
|
||||||
BOX__LOCAL__ALLOWED_MOUNT_ROOTS: /app/data/box
|
|
||||||
LANGBOT_BOX_CONTROL_TOKEN: ${BOX_CONTROL_TOKEN}
|
|
||||||
MCP__STDIO__ENABLED: "false"
|
|
||||||
LANGBOT_SPACE_CONTROL_PLANE_URL: https://space.langbot.app
|
|
||||||
LANGBOT_SPACE_CONTROL_PLANE_TOKEN: ${CLOUD_V2_CONTROL_PLANE_TOKEN}
|
|
||||||
LANGBOT_TELEMETRY_INGEST_TOKEN: ${CLOUD_V2_CONTROL_PLANE_TOKEN}
|
|
||||||
LANGBOT_SPACE_CONTROL_PLANE_PUBLIC_KEY: ${CLOUD_V2_MANIFEST_PUBLIC_KEY}
|
|
||||||
LANGBOT_SPACE_CONTROL_PLANE_KEY_ID: ${CLOUD_V2_MANIFEST_KEY_ID}
|
|
||||||
SPACE__URL: https://space.langbot.app
|
|
||||||
depends_on:
|
|
||||||
postgres: {condition: service_healthy}
|
|
||||||
networks: [internal]
|
|
||||||
|
|
||||||
plugin-runtime:
|
|
||||||
image: rockchin/langbot:${LANGBOT_IMAGE_TAG}
|
|
||||||
container_name: langbot-cloud-plugin-runtime
|
|
||||||
restart: unless-stopped
|
|
||||||
command: [uv, run, python, -m, langbot_plugin.cli.__init__, rt]
|
|
||||||
environment:
|
|
||||||
LANGBOT_PLUGIN_RUNTIME_CONTROL_TOKEN: ${PLUGIN_RUNTIME_CONTROL_TOKEN}
|
|
||||||
volumes:
|
|
||||||
- plugin-data:/app/data
|
|
||||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
|
||||||
cgroup: host
|
|
||||||
privileged: true
|
|
||||||
expose: ["5400"]
|
|
||||||
networks: [internal]
|
|
||||||
|
|
||||||
box:
|
|
||||||
image: rockchin/langbot:${LANGBOT_IMAGE_TAG}
|
|
||||||
container_name: langbot-cloud-box
|
|
||||||
restart: unless-stopped
|
|
||||||
command: [uv, run, lbp, box, --host, 0.0.0.0, --ws-control-port, "5410"]
|
|
||||||
environment:
|
|
||||||
LANGBOT_BOX_CONTROL_TOKEN: ${BOX_CONTROL_TOKEN}
|
|
||||||
LANGBOT_BOX_ROOT: /app/data/box
|
|
||||||
volumes:
|
|
||||||
- box-data:/app/data/box
|
|
||||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
|
||||||
cgroup: host
|
|
||||||
privileged: true
|
|
||||||
expose: ["5410"]
|
|
||||||
networks: [internal]
|
|
||||||
|
|
||||||
core:
|
|
||||||
image: rockchin/langbot-cloud-core:${LANGBOT_IMAGE_TAG}
|
|
||||||
container_name: langbot-cloud-core
|
|
||||||
restart: unless-stopped
|
|
||||||
environment: *core-env
|
|
||||||
volumes:
|
|
||||||
- core-data:/app/data
|
|
||||||
- box-data:/app/data/box
|
|
||||||
depends_on:
|
|
||||||
postgres: {condition: service_healthy}
|
|
||||||
redis: {condition: service_healthy}
|
|
||||||
plugin-runtime: {condition: service_started}
|
|
||||||
box: {condition: service_started}
|
|
||||||
expose: ["5300"]
|
|
||||||
healthcheck:
|
|
||||||
test: [CMD-SHELL, "python -c 'import urllib.request; urllib.request.urlopen(\"http://127.0.0.1:5300/healthz\", timeout=3)'" ]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 30
|
|
||||||
start_period: 30s
|
|
||||||
networks: [internal, shared-network]
|
|
||||||
|
|
||||||
networks:
|
|
||||||
internal:
|
|
||||||
shared-network:
|
|
||||||
external: true
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres-data:
|
|
||||||
redis-data:
|
|
||||||
plugin-data:
|
|
||||||
box-data:
|
|
||||||
core-data:
|
|
||||||
+1
-1
@@ -71,7 +71,7 @@ dependencies = [
|
|||||||
"chromadb>=1.0.0,<2.0.0",
|
"chromadb>=1.0.0,<2.0.0",
|
||||||
"qdrant-client (>=1.15.1,<2.0.0)",
|
"qdrant-client (>=1.15.1,<2.0.0)",
|
||||||
"pyseekdb==1.1.0.post3",
|
"pyseekdb==1.1.0.post3",
|
||||||
"langbot-plugin @ git+https://github.com/langbot-app/langbot-plugin-sdk.git@7b559da430a50f80a7d30c9d3d66f088503ddbb3",
|
"langbot-plugin @ git+https://github.com/langbot-app/langbot-plugin-sdk.git@555a58e5db3de28e977b08dd4cd116b332848a19",
|
||||||
"asyncpg>=0.30.0",
|
"asyncpg>=0.30.0",
|
||||||
"line-bot-sdk>=3.19.0",
|
"line-bot-sdk>=3.19.0",
|
||||||
"matrix-nio>=0.25.2",
|
"matrix-nio>=0.25.2",
|
||||||
|
|||||||
@@ -368,6 +368,10 @@ class BoxRuntimeConnector(ManagedRuntimeConnector):
|
|||||||
if not self._control_token and allow_generate:
|
if not self._control_token and allow_generate:
|
||||||
self._control_token = secrets.token_urlsafe(48)
|
self._control_token = secrets.token_urlsafe(48)
|
||||||
if not self._control_token:
|
if not self._control_token:
|
||||||
|
if getattr(getattr(self.ap, 'deployment', None), 'mode', 'oss') == 'cloud':
|
||||||
|
raise BoxRuntimeUnavailableError(
|
||||||
|
f'{BOX_CONTROL_TOKEN_ENV} must be configured with a strong shared secret for a Cloud Box runtime'
|
||||||
|
)
|
||||||
return ''
|
return ''
|
||||||
try:
|
try:
|
||||||
self._control_token = validate_control_token(self._control_token)
|
self._control_token = validate_control_token(self._control_token)
|
||||||
|
|||||||
@@ -249,6 +249,10 @@ class Application:
|
|||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
'plugin_runtime_connected': bool(
|
||||||
|
self.plugin_connector is not None
|
||||||
|
and getattr(self.plugin_connector, '_runtime_available', lambda: False)()
|
||||||
|
),
|
||||||
}
|
}
|
||||||
mcp_loader = getattr(self.tool_mgr, 'mcp_tool_loader', None)
|
mcp_loader = getattr(self.tool_mgr, 'mcp_tool_loader', None)
|
||||||
runtime_stats.update(
|
runtime_stats.update(
|
||||||
|
|||||||
@@ -264,6 +264,11 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
|||||||
if not self._control_token and allow_generate:
|
if not self._control_token and allow_generate:
|
||||||
self._control_token = secrets.token_urlsafe(48)
|
self._control_token = secrets.token_urlsafe(48)
|
||||||
if not self._control_token:
|
if not self._control_token:
|
||||||
|
if self.runtime_profile == 'shared':
|
||||||
|
raise PluginRuntimeNotConnectedError(
|
||||||
|
f'{PLUGIN_RUNTIME_CONTROL_TOKEN_ENV} must be configured with a strong shared secret '
|
||||||
|
'for a Cloud Plugin Runtime'
|
||||||
|
)
|
||||||
return {}
|
return {}
|
||||||
try:
|
try:
|
||||||
self._control_token = validate_runtime_secret(
|
self._control_token = validate_runtime_secret(
|
||||||
|
|||||||
@@ -1579,7 +1579,7 @@ class RuntimeConnectionHandler(handler.Handler):
|
|||||||
return await self.call_action(
|
return await self.call_action(
|
||||||
LangBotToRuntimeAction.RECONCILE_PLUGIN_INSTALLATIONS,
|
LangBotToRuntimeAction.RECONCILE_PLUGIN_INSTALLATIONS,
|
||||||
request.model_dump(),
|
request.model_dump(),
|
||||||
timeout=120,
|
timeout=300,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def apply_plugin_installation(
|
async def apply_plugin_installation(
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from langbot.pkg.box.connector import BoxRuntimeConnector
|
|||||||
_CONTROL_TOKEN = 'box-control-token-that-is-longer-than-32-bytes'
|
_CONTROL_TOKEN = 'box-control-token-that-is-longer-than-32-bytes'
|
||||||
|
|
||||||
|
|
||||||
def make_app(logger: Mock, runtime_endpoint: str = ''):
|
def make_app(logger: Mock, runtime_endpoint: str = '', *, cloud: bool = False):
|
||||||
return SimpleNamespace(
|
return SimpleNamespace(
|
||||||
logger=logger,
|
logger=logger,
|
||||||
workspace_service=SimpleNamespace(instance_uuid='instance-a'),
|
workspace_service=SimpleNamespace(instance_uuid='instance-a'),
|
||||||
@@ -42,6 +42,7 @@ def make_app(logger: Mock, runtime_endpoint: str = ''):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
deployment=SimpleNamespace(mode='cloud' if cloud else 'oss'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -315,6 +316,14 @@ def test_external_box_runtime_control_headers_are_tokenless_when_secret_is_unset
|
|||||||
assert connector.get_control_headers() == {BOX_INSTANCE_HEADER: 'instance-a'}
|
assert connector.get_control_headers() == {BOX_INSTANCE_HEADER: 'instance-a'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_cloud_box_runtime_rejects_missing_control_secret(monkeypatch: pytest.MonkeyPatch):
|
||||||
|
monkeypatch.delenv(BOX_CONTROL_TOKEN_ENV, raising=False)
|
||||||
|
connector = BoxRuntimeConnector(make_app(Mock(), runtime_endpoint='http://box-runtime:5410', cloud=True))
|
||||||
|
|
||||||
|
with pytest.raises(BoxRuntimeUnavailableError, match=BOX_CONTROL_TOKEN_ENV):
|
||||||
|
connector.get_control_headers()
|
||||||
|
|
||||||
|
|
||||||
def test_external_box_runtime_rejects_invalid_configured_control_token(monkeypatch: pytest.MonkeyPatch):
|
def test_external_box_runtime_rejects_invalid_configured_control_token(monkeypatch: pytest.MonkeyPatch):
|
||||||
monkeypatch.setenv(BOX_CONTROL_TOKEN_ENV, 'too-short')
|
monkeypatch.setenv(BOX_CONTROL_TOKEN_ENV, 'too-short')
|
||||||
connector = BoxRuntimeConnector(make_app(Mock(), runtime_endpoint='http://box-runtime:5410'))
|
connector = BoxRuntimeConnector(make_app(Mock(), runtime_endpoint='http://box-runtime:5410'))
|
||||||
|
|||||||
@@ -87,7 +87,10 @@ async def test_runtime_resource_stats_are_aggregate_and_constant_time() -> None:
|
|||||||
app.platform_mgr = SimpleNamespace(_bots_by_key={})
|
app.platform_mgr = SimpleNamespace(_bots_by_key={})
|
||||||
app.pipeline_mgr = SimpleNamespace(_pipelines_by_key={})
|
app.pipeline_mgr = SimpleNamespace(_pipelines_by_key={})
|
||||||
app.rag_mgr = SimpleNamespace(knowledge_bases={})
|
app.rag_mgr = SimpleNamespace(knowledge_bases={})
|
||||||
app.plugin_connector = SimpleNamespace(_known_desired_states={'installation': object()})
|
app.plugin_connector = SimpleNamespace(
|
||||||
|
_known_desired_states={'installation': object()},
|
||||||
|
_runtime_available=lambda: True,
|
||||||
|
)
|
||||||
app.persistence_mgr = SimpleNamespace(
|
app.persistence_mgr = SimpleNamespace(
|
||||||
get_resource_stats=lambda: {
|
get_resource_stats=lambda: {
|
||||||
'configured_capacity': 20,
|
'configured_capacity': 20,
|
||||||
@@ -140,3 +143,4 @@ async def test_runtime_resource_stats_are_aggregate_and_constant_time() -> None:
|
|||||||
}
|
}
|
||||||
assert stats['models']['providers'] == 1
|
assert stats['models']['providers'] == 1
|
||||||
assert stats['runtimes']['plugin_installations'] == 1
|
assert stats['runtimes']['plugin_installations'] == 1
|
||||||
|
assert stats['runtimes']['plugin_runtime_connected'] is True
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from langbot_plugin.runtime.security import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def make_connector() -> PluginRuntimeConnector:
|
def make_connector(*, cloud: bool = False) -> PluginRuntimeConnector:
|
||||||
app = SimpleNamespace(
|
app = SimpleNamespace(
|
||||||
logger=Mock(),
|
logger=Mock(),
|
||||||
instance_config=SimpleNamespace(
|
instance_config=SimpleNamespace(
|
||||||
@@ -34,6 +34,7 @@ def make_connector() -> PluginRuntimeConnector:
|
|||||||
'space': {'url': ''},
|
'space': {'url': ''},
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
deployment=SimpleNamespace(mode='cloud' if cloud else 'oss'),
|
||||||
)
|
)
|
||||||
return PluginRuntimeConnector(app, AsyncMock())
|
return PluginRuntimeConnector(app, AsyncMock())
|
||||||
|
|
||||||
@@ -332,6 +333,14 @@ def test_external_runtime_control_headers_are_empty_when_secret_is_unset(monkeyp
|
|||||||
assert connector._control_headers(allow_generate=False) == {}
|
assert connector._control_headers(allow_generate=False) == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_cloud_runtime_rejects_missing_control_secret(monkeypatch):
|
||||||
|
monkeypatch.delenv(PLUGIN_RUNTIME_CONTROL_TOKEN_ENV, raising=False)
|
||||||
|
connector = make_connector(cloud=True)
|
||||||
|
|
||||||
|
with pytest.raises(PluginRuntimeNotConnectedError, match=PLUGIN_RUNTIME_CONTROL_TOKEN_ENV):
|
||||||
|
connector._control_headers(allow_generate=False)
|
||||||
|
|
||||||
|
|
||||||
def test_local_runtime_control_headers_generate_ephemeral_secret(monkeypatch):
|
def test_local_runtime_control_headers_generate_ephemeral_secret(monkeypatch):
|
||||||
monkeypatch.delenv(PLUGIN_RUNTIME_CONTROL_TOKEN_ENV, raising=False)
|
monkeypatch.delenv(PLUGIN_RUNTIME_CONTROL_TOKEN_ENV, raising=False)
|
||||||
connector = make_connector()
|
connector = make_connector()
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ from types import SimpleNamespace
|
|||||||
from unittest.mock import AsyncMock, MagicMock, Mock
|
from unittest.mock import AsyncMock, MagicMock, Mock
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from langbot_plugin.entities.io.actions.enums import PluginToRuntimeAction
|
from langbot_plugin.entities.io.actions.enums import LangBotToRuntimeAction, PluginToRuntimeAction
|
||||||
from langbot_plugin.entities.io.context import ActionContext, InstallationBinding
|
from langbot_plugin.entities.io.context import ActionContext, InstallationBinding, PluginInstallationDesiredState
|
||||||
|
|
||||||
|
|
||||||
def make_handler(app):
|
def make_handler(app):
|
||||||
@@ -67,6 +67,20 @@ def make_handler(app):
|
|||||||
return runtime_handler
|
return runtime_handler
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_reconcile_plugin_installations_allows_cloud_cold_start_to_finish():
|
||||||
|
app = SimpleNamespace()
|
||||||
|
runtime_handler = make_handler(app)
|
||||||
|
runtime_handler.call_action = AsyncMock(return_value={})
|
||||||
|
binding = next(iter(runtime_handler._installation_bindings.values()))[0]
|
||||||
|
desired = PluginInstallationDesiredState(binding=binding, enabled=True)
|
||||||
|
|
||||||
|
await runtime_handler.reconcile_plugin_installations((desired,))
|
||||||
|
|
||||||
|
assert runtime_handler.call_action.await_args.args[0] == LangBotToRuntimeAction.RECONCILE_PLUGIN_INSTALLATIONS
|
||||||
|
assert runtime_handler.call_action.await_args.kwargs['timeout'] == 300
|
||||||
|
|
||||||
|
|
||||||
class TestHandlerQueryVariables:
|
class TestHandlerQueryVariables:
|
||||||
"""Tests for handler query variable logic."""
|
"""Tests for handler query variable logic."""
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 2
|
revision = 3
|
||||||
requires-python = ">=3.11, <4.0"
|
requires-python = ">=3.11, <4.0"
|
||||||
resolution-markers = [
|
resolution-markers = [
|
||||||
"python_full_version >= '3.14' and sys_platform == 'win32'",
|
"python_full_version >= '3.14' and sys_platform == 'win32'",
|
||||||
@@ -2125,7 +2125,7 @@ requires-dist = [
|
|||||||
{ name = "ebooklib", specifier = ">=0.18" },
|
{ name = "ebooklib", specifier = ">=0.18" },
|
||||||
{ name = "gewechat-client", specifier = ">=0.1.5" },
|
{ name = "gewechat-client", specifier = ">=0.1.5" },
|
||||||
{ name = "html2text", specifier = ">=2024.2.26" },
|
{ name = "html2text", specifier = ">=2024.2.26" },
|
||||||
{ name = "langbot-plugin", git = "https://github.com/langbot-app/langbot-plugin-sdk.git?rev=7b559da430a50f80a7d30c9d3d66f088503ddbb3" },
|
{ name = "langbot-plugin", git = "https://github.com/langbot-app/langbot-plugin-sdk.git?rev=555a58e5db3de28e977b08dd4cd116b332848a19" },
|
||||||
{ name = "langchain", specifier = ">=1.3.9" },
|
{ name = "langchain", specifier = ">=1.3.9" },
|
||||||
{ name = "langchain-core", specifier = ">=1.3.3" },
|
{ name = "langchain-core", specifier = ">=1.3.3" },
|
||||||
{ name = "langchain-text-splitters", specifier = ">=1.1.2" },
|
{ name = "langchain-text-splitters", specifier = ">=1.1.2" },
|
||||||
@@ -2191,8 +2191,8 @@ dev = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "langbot-plugin"
|
name = "langbot-plugin"
|
||||||
version = "0.5.1"
|
version = "0.5.3"
|
||||||
source = { git = "https://github.com/langbot-app/langbot-plugin-sdk.git?rev=7b559da430a50f80a7d30c9d3d66f088503ddbb3#7b559da430a50f80a7d30c9d3d66f088503ddbb3" }
|
source = { git = "https://github.com/langbot-app/langbot-plugin-sdk.git?rev=555a58e5db3de28e977b08dd4cd116b332848a19#555a58e5db3de28e977b08dd4cd116b332848a19" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiofiles" },
|
{ name = "aiofiles" },
|
||||||
{ name = "aiohttp" },
|
{ name = "aiohttp" },
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
|
|
||||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||||
import {
|
import {
|
||||||
userInfo,
|
|
||||||
systemInfo,
|
systemInfo,
|
||||||
bootstrapWorkspaceSession,
|
bootstrapWorkspaceSession,
|
||||||
initializeSystemInfo,
|
initializeSystemInfo,
|
||||||
@@ -490,24 +489,6 @@ export default function WizardPage() {
|
|||||||
t,
|
t,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ---- Space auth redirect ----
|
|
||||||
|
|
||||||
const handleSpaceAuth = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const callbackUrl = `${window.location.origin}/auth/space/callback`;
|
|
||||||
const resp = await httpClient.getSpaceAuthorizeUrl(callbackUrl);
|
|
||||||
window.location.href = resp.authorize_url;
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to get space authorize URL', err);
|
|
||||||
toast.error(t('wizard.spaceAuthError'));
|
|
||||||
}
|
|
||||||
}, [t]);
|
|
||||||
|
|
||||||
// ---- Check if local account ----
|
|
||||||
// Re-evaluated after remote data fetch (when userInfo is populated)
|
|
||||||
const isLocalAccount =
|
|
||||||
!isLoading && (!userInfo || userInfo.account_type === 'local');
|
|
||||||
|
|
||||||
// ---- Skip handler ----
|
// ---- Skip handler ----
|
||||||
const [showSkipConfirm, setShowSkipConfirm] = useState(false);
|
const [showSkipConfirm, setShowSkipConfirm] = useState(false);
|
||||||
const [isSkipping, setIsSkipping] = useState(false);
|
const [isSkipping, setIsSkipping] = useState(false);
|
||||||
@@ -662,8 +643,6 @@ export default function WizardPage() {
|
|||||||
runnerOptions={runnerOptions}
|
runnerOptions={runnerOptions}
|
||||||
selected={selectedRunner}
|
selected={selectedRunner}
|
||||||
onSelect={handleSelectRunner}
|
onSelect={handleSelectRunner}
|
||||||
isLocalAccount={isLocalAccount}
|
|
||||||
onSpaceAuth={handleSpaceAuth}
|
|
||||||
runnerConfigItems={selectedRunnerConfigItems}
|
runnerConfigItems={selectedRunnerConfigItems}
|
||||||
runnerConfigValues={runnerConfig}
|
runnerConfigValues={runnerConfig}
|
||||||
onRunnerConfigChange={setRunnerConfig}
|
onRunnerConfigChange={setRunnerConfig}
|
||||||
@@ -1006,8 +985,6 @@ function StepAIEngine({
|
|||||||
runnerOptions,
|
runnerOptions,
|
||||||
selected,
|
selected,
|
||||||
onSelect,
|
onSelect,
|
||||||
isLocalAccount,
|
|
||||||
onSpaceAuth,
|
|
||||||
runnerConfigItems,
|
runnerConfigItems,
|
||||||
runnerConfigValues,
|
runnerConfigValues,
|
||||||
onRunnerConfigChange,
|
onRunnerConfigChange,
|
||||||
@@ -1015,8 +992,6 @@ function StepAIEngine({
|
|||||||
runnerOptions: { name: string; label: { en_US: string; zh_Hans: string } }[];
|
runnerOptions: { name: string; label: { en_US: string; zh_Hans: string } }[];
|
||||||
selected: string | null;
|
selected: string | null;
|
||||||
onSelect: (name: string) => void;
|
onSelect: (name: string) => void;
|
||||||
isLocalAccount: boolean;
|
|
||||||
onSpaceAuth: () => void;
|
|
||||||
runnerConfigItems: IDynamicFormItemSchema[];
|
runnerConfigItems: IDynamicFormItemSchema[];
|
||||||
runnerConfigValues: Record<string, unknown>;
|
runnerConfigValues: Record<string, unknown>;
|
||||||
onRunnerConfigChange: (v: Record<string, unknown>) => void;
|
onRunnerConfigChange: (v: Record<string, unknown>) => void;
|
||||||
@@ -1127,28 +1102,6 @@ function StepAIEngine({
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Space promotion banner */}
|
|
||||||
{selected === 'local-agent' && isLocalAccount && (
|
|
||||||
<div className="animate-in fade-in slide-in-from-left-2 duration-300">
|
|
||||||
<div className="relative rounded-lg p-[2px] bg-gradient-to-r from-purple-500 via-pink-500 to-orange-500">
|
|
||||||
<div className="rounded-[calc(0.5rem-2px)] bg-background p-3 flex flex-col items-center gap-2 text-center">
|
|
||||||
<Sparkles className="w-6 h-6 text-purple-500 shrink-0" />
|
|
||||||
<p className="text-xs font-medium">
|
|
||||||
{t('wizard.spaceBanner.message')}
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={onSpaceAuth}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{t('wizard.spaceBanner.action')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1807,7 +1807,6 @@ const enUS = {
|
|||||||
botCreateSuccess: 'Bot created successfully!',
|
botCreateSuccess: 'Bot created successfully!',
|
||||||
botSaveSuccess: 'Bot configuration saved and enabled!',
|
botSaveSuccess: 'Bot configuration saved and enabled!',
|
||||||
createError: 'Failed to create resources',
|
createError: 'Failed to create resources',
|
||||||
spaceAuthError: 'Failed to initiate Space authorization',
|
|
||||||
skipSaveError: 'Failed to save skip status. Please try again.',
|
skipSaveError: 'Failed to save skip status. Please try again.',
|
||||||
completeSaveError: 'Failed to save completion status. Please try again.',
|
completeSaveError: 'Failed to save completion status. Please try again.',
|
||||||
step: {
|
step: {
|
||||||
@@ -1836,11 +1835,6 @@ const enUS = {
|
|||||||
description:
|
description:
|
||||||
"Choose the AI engine that will power your bot's intelligence.",
|
"Choose the AI engine that will power your bot's intelligence.",
|
||||||
},
|
},
|
||||||
spaceBanner: {
|
|
||||||
message:
|
|
||||||
'Connect to LangBot Space for free trial model credits and zero-config instant setup!',
|
|
||||||
action: 'Authorize with Space',
|
|
||||||
},
|
|
||||||
config: {
|
config: {
|
||||||
botInfo: 'Bot Information',
|
botInfo: 'Bot Information',
|
||||||
botNamePlaceholder: 'Enter bot name',
|
botNamePlaceholder: 'Enter bot name',
|
||||||
|
|||||||
@@ -1672,7 +1672,6 @@ const esES = {
|
|||||||
botCreateSuccess: '¡Bot creado correctamente!',
|
botCreateSuccess: '¡Bot creado correctamente!',
|
||||||
botSaveSuccess: '¡Configuración del Bot guardada y activada!',
|
botSaveSuccess: '¡Configuración del Bot guardada y activada!',
|
||||||
createError: 'Error al crear los recursos',
|
createError: 'Error al crear los recursos',
|
||||||
spaceAuthError: 'Error al iniciar la autorización de Space',
|
|
||||||
skipSaveError:
|
skipSaveError:
|
||||||
'Error al guardar el estado de omisión. Por favor, inténtalo de nuevo.',
|
'Error al guardar el estado de omisión. Por favor, inténtalo de nuevo.',
|
||||||
completeSaveError:
|
completeSaveError:
|
||||||
@@ -1705,11 +1704,6 @@ const esES = {
|
|||||||
description:
|
description:
|
||||||
'Elige el motor de IA que impulsará la inteligencia de tu Bot.',
|
'Elige el motor de IA que impulsará la inteligencia de tu Bot.',
|
||||||
},
|
},
|
||||||
spaceBanner: {
|
|
||||||
message:
|
|
||||||
'¡Conéctate a LangBot Space para obtener créditos de prueba gratuitos y configuración instantánea sin esfuerzo!',
|
|
||||||
action: 'Autorizar con Space',
|
|
||||||
},
|
|
||||||
config: {
|
config: {
|
||||||
botInfo: 'Información del Bot',
|
botInfo: 'Información del Bot',
|
||||||
botNamePlaceholder: 'Introduce el nombre del Bot',
|
botNamePlaceholder: 'Introduce el nombre del Bot',
|
||||||
|
|||||||
@@ -1722,7 +1722,6 @@ const jaJP = {
|
|||||||
botCreateSuccess: 'ボットが正常に作成されました!',
|
botCreateSuccess: 'ボットが正常に作成されました!',
|
||||||
botSaveSuccess: 'ボット設定が保存され、有効になりました!',
|
botSaveSuccess: 'ボット設定が保存され、有効になりました!',
|
||||||
createError: 'リソースの作成に失敗しました',
|
createError: 'リソースの作成に失敗しました',
|
||||||
spaceAuthError: 'Space 認証の開始に失敗しました',
|
|
||||||
skipSaveError: 'スキップ状態の保存に失敗しました。もう一度お試しください。',
|
skipSaveError: 'スキップ状態の保存に失敗しました。もう一度お試しください。',
|
||||||
completeSaveError: '完了状態の保存に失敗しました。もう一度お試しください。',
|
completeSaveError: '完了状態の保存に失敗しました。もう一度お試しください。',
|
||||||
step: {
|
step: {
|
||||||
@@ -1753,11 +1752,6 @@ const jaJP = {
|
|||||||
description:
|
description:
|
||||||
'ボットのインテリジェンスを駆動するAIエンジンを選択してください。',
|
'ボットのインテリジェンスを駆動するAIエンジンを選択してください。',
|
||||||
},
|
},
|
||||||
spaceBanner: {
|
|
||||||
message:
|
|
||||||
'LangBot Spaceに接続して、無料トライアルモデルクレジットとゼロ設定の即時セットアップを入手!',
|
|
||||||
action: 'Spaceで認証',
|
|
||||||
},
|
|
||||||
config: {
|
config: {
|
||||||
botInfo: 'ボット情報',
|
botInfo: 'ボット情報',
|
||||||
botNamePlaceholder: 'ボット名を入力',
|
botNamePlaceholder: 'ボット名を入力',
|
||||||
|
|||||||
@@ -1645,7 +1645,6 @@ const ruRU = {
|
|||||||
botCreateSuccess: 'Бот успешно создан!',
|
botCreateSuccess: 'Бот успешно создан!',
|
||||||
botSaveSuccess: 'Конфигурация бота сохранена и включена!',
|
botSaveSuccess: 'Конфигурация бота сохранена и включена!',
|
||||||
createError: 'Не удалось создать ресурсы',
|
createError: 'Не удалось создать ресурсы',
|
||||||
spaceAuthError: 'Не удалось инициировать авторизацию через Space',
|
|
||||||
skipSaveError: 'Не удалось сохранить статус пропуска. Повторите попытку.',
|
skipSaveError: 'Не удалось сохранить статус пропуска. Повторите попытку.',
|
||||||
completeSaveError:
|
completeSaveError:
|
||||||
'Не удалось сохранить статус завершения. Повторите попытку.',
|
'Не удалось сохранить статус завершения. Повторите попытку.',
|
||||||
@@ -1675,11 +1674,6 @@ const ruRU = {
|
|||||||
description:
|
description:
|
||||||
'Выберите ИИ-движок, который будет управлять интеллектом вашего бота.',
|
'Выберите ИИ-движок, который будет управлять интеллектом вашего бота.',
|
||||||
},
|
},
|
||||||
spaceBanner: {
|
|
||||||
message:
|
|
||||||
'Подключитесь к LangBot Space для бесплатных пробных кредитов и мгновенной настройки!',
|
|
||||||
action: 'Авторизация через Space',
|
|
||||||
},
|
|
||||||
config: {
|
config: {
|
||||||
botInfo: 'Информация о боте',
|
botInfo: 'Информация о боте',
|
||||||
botNamePlaceholder: 'Введите имя бота',
|
botNamePlaceholder: 'Введите имя бота',
|
||||||
|
|||||||
@@ -1612,7 +1612,6 @@ const thTH = {
|
|||||||
botCreateSuccess: 'สร้าง Bot สำเร็จ!',
|
botCreateSuccess: 'สร้าง Bot สำเร็จ!',
|
||||||
botSaveSuccess: 'บันทึกและเปิดใช้งาน Bot สำเร็จ!',
|
botSaveSuccess: 'บันทึกและเปิดใช้งาน Bot สำเร็จ!',
|
||||||
createError: 'ไม่สามารถสร้างทรัพยากรได้',
|
createError: 'ไม่สามารถสร้างทรัพยากรได้',
|
||||||
spaceAuthError: 'ไม่สามารถเริ่มต้นการยืนยันสิทธิ์ Space ได้',
|
|
||||||
skipSaveError: 'ไม่สามารถบันทึกสถานะการข้ามได้ กรุณาลองใหม่',
|
skipSaveError: 'ไม่สามารถบันทึกสถานะการข้ามได้ กรุณาลองใหม่',
|
||||||
completeSaveError: 'ไม่สามารถบันทึกสถานะการเสร็จสิ้นได้ กรุณาลองใหม่',
|
completeSaveError: 'ไม่สามารถบันทึกสถานะการเสร็จสิ้นได้ กรุณาลองใหม่',
|
||||||
step: {
|
step: {
|
||||||
@@ -1640,11 +1639,6 @@ const thTH = {
|
|||||||
title: 'เลือกเครื่องมือ AI',
|
title: 'เลือกเครื่องมือ AI',
|
||||||
description: 'เลือกเครื่องมือ AI ที่จะขับเคลื่อนความฉลาดของ Bot',
|
description: 'เลือกเครื่องมือ AI ที่จะขับเคลื่อนความฉลาดของ Bot',
|
||||||
},
|
},
|
||||||
spaceBanner: {
|
|
||||||
message:
|
|
||||||
'เชื่อมต่อกับ LangBot Space เพื่อรับเครดิตทดลองใช้โมเดลฟรีและตั้งค่าทันทีโดยไม่ต้องกำหนดค่า!',
|
|
||||||
action: 'ยืนยันสิทธิ์กับ Space',
|
|
||||||
},
|
|
||||||
config: {
|
config: {
|
||||||
botInfo: 'ข้อมูล Bot',
|
botInfo: 'ข้อมูล Bot',
|
||||||
botNamePlaceholder: 'กรอกชื่อ Bot',
|
botNamePlaceholder: 'กรอกชื่อ Bot',
|
||||||
|
|||||||
@@ -1638,7 +1638,6 @@ const viVN = {
|
|||||||
botCreateSuccess: 'Tạo Bot thành công!',
|
botCreateSuccess: 'Tạo Bot thành công!',
|
||||||
botSaveSuccess: 'Cấu hình Bot đã lưu và bật!',
|
botSaveSuccess: 'Cấu hình Bot đã lưu và bật!',
|
||||||
createError: 'Tạo tài nguyên thất bại',
|
createError: 'Tạo tài nguyên thất bại',
|
||||||
spaceAuthError: 'Khởi tạo ủy quyền Space thất bại',
|
|
||||||
skipSaveError: 'Lưu trạng thái bỏ qua thất bại. Vui lòng thử lại.',
|
skipSaveError: 'Lưu trạng thái bỏ qua thất bại. Vui lòng thử lại.',
|
||||||
completeSaveError: 'Lưu trạng thái hoàn tất thất bại. Vui lòng thử lại.',
|
completeSaveError: 'Lưu trạng thái hoàn tất thất bại. Vui lòng thử lại.',
|
||||||
step: {
|
step: {
|
||||||
@@ -1666,11 +1665,6 @@ const viVN = {
|
|||||||
title: 'Chọn công cụ AI',
|
title: 'Chọn công cụ AI',
|
||||||
description: 'Chọn công cụ AI sẽ cung cấp trí tuệ cho Bot của bạn.',
|
description: 'Chọn công cụ AI sẽ cung cấp trí tuệ cho Bot của bạn.',
|
||||||
},
|
},
|
||||||
spaceBanner: {
|
|
||||||
message:
|
|
||||||
'Kết nối với LangBot Space để nhận tín dụng dùng thử mô hình miễn phí và thiết lập tức thì không cần cấu hình!',
|
|
||||||
action: 'Ủy quyền với Space',
|
|
||||||
},
|
|
||||||
config: {
|
config: {
|
||||||
botInfo: 'Thông tin Bot',
|
botInfo: 'Thông tin Bot',
|
||||||
botNamePlaceholder: 'Nhập tên Bot',
|
botNamePlaceholder: 'Nhập tên Bot',
|
||||||
|
|||||||
@@ -1730,7 +1730,6 @@ const zhHans = {
|
|||||||
botCreateSuccess: '机器人创建成功!',
|
botCreateSuccess: '机器人创建成功!',
|
||||||
botSaveSuccess: '机器人配置已保存并启用!',
|
botSaveSuccess: '机器人配置已保存并启用!',
|
||||||
createError: '创建资源失败',
|
createError: '创建资源失败',
|
||||||
spaceAuthError: '无法发起 Space 授权',
|
|
||||||
skipSaveError: '保存跳过状态失败,请重试。',
|
skipSaveError: '保存跳过状态失败,请重试。',
|
||||||
completeSaveError: '保存完成状态失败,请重试。',
|
completeSaveError: '保存完成状态失败,请重试。',
|
||||||
step: {
|
step: {
|
||||||
@@ -1756,10 +1755,6 @@ const zhHans = {
|
|||||||
title: '选择 AI 引擎',
|
title: '选择 AI 引擎',
|
||||||
description: '选择驱动机器人智能的 AI 引擎。',
|
description: '选择驱动机器人智能的 AI 引擎。',
|
||||||
},
|
},
|
||||||
spaceBanner: {
|
|
||||||
message: '接入 LangBot Space,获取免费试用模型额度,零配置极速开箱!',
|
|
||||||
action: '前往授权登录',
|
|
||||||
},
|
|
||||||
config: {
|
config: {
|
||||||
botInfo: '机器人信息',
|
botInfo: '机器人信息',
|
||||||
botNamePlaceholder: '请输入机器人名称',
|
botNamePlaceholder: '请输入机器人名称',
|
||||||
|
|||||||
@@ -1566,7 +1566,6 @@ const zhHant = {
|
|||||||
botCreateSuccess: '機器人建立成功!',
|
botCreateSuccess: '機器人建立成功!',
|
||||||
botSaveSuccess: '機器人配置已儲存並啟用!',
|
botSaveSuccess: '機器人配置已儲存並啟用!',
|
||||||
createError: '建立資源失敗',
|
createError: '建立資源失敗',
|
||||||
spaceAuthError: '無法發起 Space 授權',
|
|
||||||
skipSaveError: '儲存跳過狀態失敗,請重試。',
|
skipSaveError: '儲存跳過狀態失敗,請重試。',
|
||||||
completeSaveError: '儲存完成狀態失敗,請重試。',
|
completeSaveError: '儲存完成狀態失敗,請重試。',
|
||||||
step: {
|
step: {
|
||||||
@@ -1592,10 +1591,6 @@ const zhHant = {
|
|||||||
title: '選擇 AI 引擎',
|
title: '選擇 AI 引擎',
|
||||||
description: '選擇驅動機器人智慧的 AI 引擎。',
|
description: '選擇驅動機器人智慧的 AI 引擎。',
|
||||||
},
|
},
|
||||||
spaceBanner: {
|
|
||||||
message: '接入 LangBot Space,取得免費試用模型額度,零配置極速開箱!',
|
|
||||||
action: '前往授權登入',
|
|
||||||
},
|
|
||||||
config: {
|
config: {
|
||||||
botInfo: '機器人資訊',
|
botInfo: '機器人資訊',
|
||||||
botNamePlaceholder: '請輸入機器人名稱',
|
botNamePlaceholder: '請輸入機器人名稱',
|
||||||
|
|||||||
Reference in New Issue
Block a user