mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-22 02:07:13 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a4fadc375 | |||
| f2ba540ffb | |||
| 97b176aef2 | |||
| 7c387f75e1 | |||
| b0566f4c9d |
@@ -0,0 +1,59 @@
|
||||
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
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,162 @@
|
||||
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:
|
||||
@@ -103,11 +103,11 @@ This log records implementation choices made while delivering the Workspace arch
|
||||
- Decision: New Core JWTs require `iss=langbot-core`, an audience derived from the immutable instance UUID, and an expiry. Legacy community tokens are accepted only when they have the historical issuer, carry no audience, and the active policy is the OSS singleton policy.
|
||||
- Reason: A token issued by one instance must not authenticate against another instance that happens to share a secret, and a compatibility decoder must not become an alternate path around the SaaS trust boundary.
|
||||
|
||||
### Runtime control transports support opt-in shared-secret authentication
|
||||
### Runtime control transports authenticate before protocol dispatch
|
||||
|
||||
- Decision: OSS external Plugin Runtime and Box WebSocket control channels preserve tokenless standalone compatibility when the corresponding control token is unset. When a Runtime configures a token, it validates the independent shared secret in the handshake before protocol dispatch. Locally managed child processes still receive ephemeral secrets through their environment; secrets are not placed in URLs, process arguments, request payloads, or logs. Box additionally pins the first control channel to one declared instance identity. Plugin Runtime debug and control credentials remain separate.
|
||||
- Reason: Local OSS development must remain backward compatible, while exposed or shared Runtime endpoints can opt into transport authentication. Separating control and debug credentials also limits accidental privilege reuse.
|
||||
- Deployment consequence: Docker Compose and Kubernetes should wire one strong shared secret to each host/runtime pair. Both sides must use the same value for protection to be effective; a Runtime configured with a token rejects clients that omit it or send a different value.
|
||||
- Decision: External Plugin Runtime and Box WebSocket control channels require independent strong shared secrets in handshake headers. Locally managed child processes receive ephemeral secrets through their environment; secrets are not placed in URLs, process arguments, request payloads, or logs. Box additionally binds the first authenticated control channel to one trusted instance. Plugin Runtime debug and control credentials remain separate.
|
||||
- Reason: Workspace context inside an RPC payload is not trustworthy until the transport peer itself is authenticated. Separating control and debug credentials also limits accidental privilege reuse.
|
||||
- Deployment consequence: Docker Compose and Kubernetes wire one shared secret to each host/runtime pair. An empty external-runtime secret fails startup instead of silently exposing an unauthenticated socket.
|
||||
|
||||
### Dashboard WebSocket sessions are tenant runtime objects
|
||||
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ dependencies = [
|
||||
"chromadb>=1.0.0,<2.0.0",
|
||||
"qdrant-client (>=1.15.1,<2.0.0)",
|
||||
"pyseekdb==1.1.0.post3",
|
||||
"langbot-plugin @ git+https://github.com/langbot-app/langbot-plugin-sdk.git@555a58e5db3de28e977b08dd4cd116b332848a19",
|
||||
"langbot-plugin @ git+https://github.com/langbot-app/langbot-plugin-sdk.git@9d216208cdfb41f0cb7fcb64632e2a46816d6dc6",
|
||||
"asyncpg>=0.30.0",
|
||||
"line-bot-sdk>=3.19.0",
|
||||
"matrix-nio>=0.25.2",
|
||||
|
||||
@@ -27,11 +27,10 @@ The `all` / `box` profile starts three services:
|
||||
- `langbot_box` — Box sandbox runtime (`:5410`). Uses the host Docker socket to
|
||||
spawn sandbox containers, so the **Box root host path and in-container path
|
||||
must be identical** (`BOX__LOCAL__HOST_ROOT=${LANGBOT_BOX_ROOT:-${PWD}/data/box}`).
|
||||
OSS allows its RPC and managed-process relay to run without a token when both
|
||||
sides leave `LANGBOT_BOX_CONTROL_TOKEN` unset. For an exposed endpoint, set
|
||||
the same value of at least 32 non-whitespace characters in both the LangBot
|
||||
and Box containers. Generate it once with `openssl rand -hex 32`; never put
|
||||
it in `box.runtime.endpoint` or commit it to config.
|
||||
Its RPC and managed-process relay require a shared
|
||||
`LANGBOT_BOX_CONTROL_TOKEN` (at least 32 non-whitespace characters) in both
|
||||
the LangBot and Box containers. Generate it once with `openssl rand -hex 32`;
|
||||
never put it in `box.runtime.endpoint` or commit it to config.
|
||||
|
||||
A Compose deployment may optionally set
|
||||
`LANGBOT_PLUGIN_RUNTIME_CONTROL_TOKEN` on both `langbot` and
|
||||
|
||||
@@ -6,8 +6,7 @@ description: Browse and search the LangBot Space marketplaces (plugins, MCP serv
|
||||
# LangBot Space MCP Operations
|
||||
|
||||
LangBot Space (space.langbot.app) exposes an **MCP server** so user-facing AI
|
||||
agents can browse and search the marketplaces (plugins, MCP servers, skills) and
|
||||
rank live models for automated setup.
|
||||
agents can browse and search the marketplaces (plugins, MCP servers, skills).
|
||||
|
||||
## Endpoint
|
||||
|
||||
@@ -47,12 +46,10 @@ Authorization: Bearer lbpat_...uests without a valid PAT get `401 Unauthorized`.
|
||||
| `list_plugins` / `search_plugins` / `get_plugin` | Plugin marketplace |
|
||||
| `list_mcp_servers` / `search_mcp_servers` / `get_mcp_server` | MCP-server marketplace |
|
||||
| `list_skills` / `search_skills` / `get_skill` | Skill marketplace |
|
||||
| `select_models` | Live best-first model list for setup wizards; optional `category` filter |
|
||||
|
||||
`list_*` and `search_*` are paged (`page`, `page_size`). `get_*` takes
|
||||
`author` + `name`. The tool surface mirrors the REST endpoints under
|
||||
`/api/v1/marketplace/*`; `select_models` mirrors `/api/v1/models/selection`.
|
||||
All tools are read-only.
|
||||
`/api/v1/marketplace/*` and is read/browse only.
|
||||
|
||||
## How to use
|
||||
|
||||
@@ -61,16 +58,12 @@ All tools are read-only.
|
||||
3. Use `search_plugins` / `search_mcp_servers` / `search_skills` to find items,
|
||||
then `get_*` for details (e.g. to obtain author/name for installation in
|
||||
LangBot itself).
|
||||
4. For automatic local-agent setup, call `select_models` (optionally with
|
||||
`category`) and choose the first compatible item. Ordering is latest probe
|
||||
state (available, unprobed, unavailable), then Space recommendation. Each
|
||||
item includes `availability.up`, `last_probed_at`, latency, and HTTP status.
|
||||
|
||||
## Implementation & maintenance (for Space developers)
|
||||
|
||||
- Server: `internal/controller/mcp/server.go` (official Go MCP SDK
|
||||
`github.com/modelcontextprotocol/go-sdk`). Tools call the service layer
|
||||
(`PluginService`, `MCPService`, `SkillService`, `ModelStatusService`) directly.
|
||||
(`PluginService`, `MCPService`, `SkillService`) directly.
|
||||
- Mount: `internal/controller/api.go` at `/mcp` and `/mcp/*any`.
|
||||
- Auth: PAT via `AccountService.ValidatePersonalAccessToken`.
|
||||
- Docs: `docs/MCP_SERVER.md`.
|
||||
|
||||
@@ -113,6 +113,24 @@ class BotsRouterGroup(group.RouterGroup):
|
||||
)
|
||||
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(
|
||||
'/<bot_uuid>/admins',
|
||||
methods=['GET'],
|
||||
|
||||
@@ -398,16 +398,7 @@ class PluginsRouterGroup(group.RouterGroup):
|
||||
|
||||
# Get debug URL from config
|
||||
plugin_config = self.ap.instance_config.data.get('plugin', {})
|
||||
debug_url = plugin_config.get(
|
||||
'display_plugin_debug_url',
|
||||
'ws://localhost:5401/plugin/debug/ws',
|
||||
)
|
||||
parsed_debug_url = urlparse(debug_url)
|
||||
if parsed_debug_url.scheme in {'http', 'https'}:
|
||||
debug_url = parsed_debug_url._replace(
|
||||
scheme='wss' if parsed_debug_url.scheme == 'https' else 'ws',
|
||||
path=parsed_debug_url.path or '/plugin/debug/ws',
|
||||
).geturl()
|
||||
debug_url = plugin_config.get('display_plugin_debug_url', 'http://localhost:5401')
|
||||
|
||||
return self.success(
|
||||
data={
|
||||
|
||||
@@ -206,6 +206,20 @@ class SystemRouterGroup(group.RouterGroup):
|
||||
|
||||
return self.success(data={})
|
||||
|
||||
@self.route(
|
||||
'/wizard/recommended-model',
|
||||
methods=['GET'],
|
||||
auth_type=group.AuthType.USER_TOKEN,
|
||||
permission=Permission.RESOURCE_MANAGE,
|
||||
)
|
||||
async def _(request_context: RequestContext) -> str:
|
||||
"""Resolve Space's best available chat model to this Workspace."""
|
||||
try:
|
||||
model = await self.ap.space_service.get_recommended_chat_model(request_context)
|
||||
except ValueError as exc:
|
||||
return self.http_status(503, -1, str(exc))
|
||||
return self.success(data=model)
|
||||
|
||||
@self.route(
|
||||
'/tasks',
|
||||
methods=['GET'],
|
||||
|
||||
@@ -14,6 +14,13 @@ from ...service.user import ControlPlaneDirectoryRequiredError, PublicRegistrati
|
||||
|
||||
@group.group_class('user', '/api/v1/user')
|
||||
class UserRouterGroup(group.RouterGroup):
|
||||
@staticmethod
|
||||
def _origin(value: str) -> tuple[str, str, int | None] | None:
|
||||
parsed = urlsplit(value)
|
||||
if parsed.scheme not in {'http', 'https'} or not parsed.hostname:
|
||||
return None
|
||||
return parsed.scheme, parsed.hostname.casefold(), parsed.port
|
||||
|
||||
def _validate_space_redirect_uri(self, redirect_uri: str, *, bind: bool) -> str:
|
||||
parsed = urlsplit(redirect_uri)
|
||||
if (
|
||||
@@ -31,8 +38,17 @@ class UserRouterGroup(group.RouterGroup):
|
||||
if query != {'mode': ['bind']}:
|
||||
raise ValueError('Invalid Space binding redirect_uri')
|
||||
elif query:
|
||||
raise ValueError('Invalid LangBot Account login redirect_uri')
|
||||
raise ValueError('Invalid Space login redirect_uri')
|
||||
|
||||
redirect_origin = self._origin(redirect_uri)
|
||||
api_config = self.ap.instance_config.data.get('api', {})
|
||||
trusted_origins = {
|
||||
self._origin(str(api_config.get(config_key, '') or '').strip())
|
||||
for config_key in ('webui_url', 'webhook_prefix')
|
||||
}
|
||||
trusted_origins.discard(None)
|
||||
if redirect_origin not in trusted_origins:
|
||||
raise ValueError('Untrusted redirect_uri origin')
|
||||
return redirect_uri
|
||||
|
||||
async def initialize(self) -> None:
|
||||
@@ -400,7 +416,7 @@ class UserRouterGroup(group.RouterGroup):
|
||||
'Bind the LangBot Account with the same email as this local Account',
|
||||
)
|
||||
except ValueError:
|
||||
return self.http_status(400, -1, 'LangBot Account binding failed')
|
||||
return self.http_status(400, -1, 'Space account binding failed')
|
||||
except Exception:
|
||||
raise
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
import json
|
||||
import sqlalchemy
|
||||
|
||||
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 ....workspace.errors import WorkspaceNotFoundError
|
||||
from .tenant import TenantContext, require_workspace_uuid, scope_statement
|
||||
from ....utils import httpclient
|
||||
from ....platform.sources import http_bot_signing
|
||||
|
||||
|
||||
class BotService:
|
||||
@@ -80,6 +83,7 @@ class BotService:
|
||||
'wecomcs',
|
||||
'LINE',
|
||||
'lark',
|
||||
'http_bot',
|
||||
]:
|
||||
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', '')
|
||||
@@ -216,6 +220,53 @@ class BotService:
|
||||
|
||||
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(
|
||||
self,
|
||||
context: TenantContext,
|
||||
|
||||
@@ -11,6 +11,9 @@ import sqlalchemy
|
||||
from ....core import app
|
||||
from ....entity.persistence import user
|
||||
from ....entity.dto.space_model import SpaceModel
|
||||
from ....entity.dto.space_model import SpaceModelSelection
|
||||
from ....entity.persistence import model as persistence_model
|
||||
from ....cloud.model_catalog import LANGBOT_MODELS_PROVIDER_REQUESTER
|
||||
|
||||
|
||||
_CREDITS_CACHE_TTL_SECONDS = 60
|
||||
@@ -238,3 +241,79 @@ class SpaceService:
|
||||
raise ValueError(f'Failed to get models: {data.get("msg")}')
|
||||
models_data = data.get('data', {}).get('models', [])
|
||||
return [SpaceModel.model_validate(model_dict) for model_dict in models_data]
|
||||
|
||||
async def get_model_selection(self, category: str) -> typing.List[SpaceModelSelection]:
|
||||
"""Return Space models in the availability-ranked selection order."""
|
||||
space_url = self._get_space_config()['url']
|
||||
session = httpclient.get_session()
|
||||
async with session.get(
|
||||
f'{space_url}/api/v1/models/selection',
|
||||
params={'category': category},
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
error = await httpclient.read_text_limited(response)
|
||||
raise ValueError(f'Failed to get model selection: {error}')
|
||||
payload = await httpclient.read_json_limited(response)
|
||||
if payload.get('code') != 0:
|
||||
raise ValueError(f'Failed to get model selection: {payload.get("msg")}')
|
||||
|
||||
data = payload.get('data', [])
|
||||
if isinstance(data, dict):
|
||||
data = data.get('models', data.get('items', []))
|
||||
if not isinstance(data, list):
|
||||
raise ValueError('Failed to get model selection: invalid response')
|
||||
|
||||
models = []
|
||||
for selection in data:
|
||||
if isinstance(selection, dict) and isinstance(selection.get('model'), dict):
|
||||
models.append(selection['model'])
|
||||
else:
|
||||
models.append(selection)
|
||||
return [SpaceModelSelection.model_validate(model) for model in models]
|
||||
|
||||
async def get_recommended_chat_model(self, context: typing.Any) -> dict:
|
||||
"""Resolve Space's first ranked chat model to a local Workspace model."""
|
||||
selection = await self.get_model_selection('chat')
|
||||
if not selection:
|
||||
raise ValueError('No recommended chat model is available')
|
||||
recommended = selection[0]
|
||||
|
||||
async def find_local_model():
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.select(persistence_model.LLMModel)
|
||||
.join(
|
||||
persistence_model.ModelProvider,
|
||||
sqlalchemy.and_(
|
||||
persistence_model.ModelProvider.workspace_uuid
|
||||
== persistence_model.LLMModel.workspace_uuid,
|
||||
persistence_model.ModelProvider.uuid == persistence_model.LLMModel.provider_uuid,
|
||||
),
|
||||
)
|
||||
.where(
|
||||
persistence_model.LLMModel.workspace_uuid == context.workspace_uuid,
|
||||
persistence_model.ModelProvider.requester == LANGBOT_MODELS_PROVIDER_REQUESTER,
|
||||
sqlalchemy.or_(
|
||||
persistence_model.LLMModel.uuid == recommended.uuid,
|
||||
persistence_model.LLMModel.name == recommended.model_id,
|
||||
),
|
||||
)
|
||||
)
|
||||
return result.first()
|
||||
|
||||
local_model = await find_local_model()
|
||||
if local_model is None:
|
||||
# OSS synchronizes the public catalog locally. Refresh once in case
|
||||
# the recommendation was published after this process started.
|
||||
from ..context import ExecutionContext
|
||||
|
||||
try:
|
||||
await self.ap.model_mgr.sync_new_models_from_space(
|
||||
ExecutionContext.from_request(context)
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
local_model = await find_local_model()
|
||||
|
||||
if local_model is None:
|
||||
raise ValueError('Recommended chat model is not available in this Workspace')
|
||||
return {'uuid': local_model.uuid, 'name': local_model.name}
|
||||
|
||||
@@ -114,7 +114,7 @@ class UserService:
|
||||
if purpose == 'login' and account_uuid is not None:
|
||||
raise ValueError('Login state cannot be bound to an Account')
|
||||
if purpose != 'login' and launch_workspace_uuid is not None:
|
||||
raise ValueError('Launch Workspace state is only valid for LangBot Account login')
|
||||
raise ValueError('Launch Workspace state is only valid for Space login')
|
||||
if ttl_seconds <= 0:
|
||||
raise ValueError('OAuth state lifetime must be positive')
|
||||
|
||||
@@ -327,7 +327,7 @@ class UserService:
|
||||
normalized_email = normalize_email(user_email)
|
||||
if self._uses_control_plane_directory():
|
||||
raise ControlPlaneDirectoryRequiredError(
|
||||
'Cloud invitation registration must use a LangBot Account to preserve control-plane identity'
|
||||
'Cloud invitation registration must use a Space account to preserve control-plane identity'
|
||||
)
|
||||
invitation, _ = await self.ap.workspace_collaboration_service.inspect_invitation(invitation_token)
|
||||
if invitation.normalized_email != normalized_email:
|
||||
@@ -394,7 +394,7 @@ class UserService:
|
||||
|
||||
# Check if this user has a local password set
|
||||
if not user_obj.password:
|
||||
raise ValueError('请使用 LangBot 账号登录')
|
||||
raise ValueError('请使用 Space 账户登录')
|
||||
|
||||
await self._verify_password(user_obj.password, password)
|
||||
|
||||
@@ -825,7 +825,7 @@ class UserService:
|
||||
# Check if this Space account is already bound to another user
|
||||
existing_space_user = await self.get_user_by_space_account_uuid(space_account_uuid)
|
||||
if existing_space_user and existing_space_user.normalized_email != normalize_email(user_email):
|
||||
raise ValueError('This LangBot Account is already bound to another user')
|
||||
raise ValueError('This Space account is already bound to another user')
|
||||
|
||||
# Update local account to Space account
|
||||
normalized_email = normalize_email(user_email)
|
||||
|
||||
@@ -367,12 +367,6 @@ class BoxRuntimeConnector(ManagedRuntimeConnector):
|
||||
def _ensure_control_token(self, *, allow_generate: bool) -> str:
|
||||
if not self._control_token and allow_generate:
|
||||
self._control_token = secrets.token_urlsafe(48)
|
||||
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 ''
|
||||
try:
|
||||
self._control_token = validate_control_token(self._control_token)
|
||||
except ValueError as exc:
|
||||
@@ -382,19 +376,19 @@ class BoxRuntimeConnector(ManagedRuntimeConnector):
|
||||
return self._control_token
|
||||
|
||||
def get_control_headers(self) -> dict[str, str]:
|
||||
"""Return instance-scoped RPC headers and the optional shared secret."""
|
||||
"""Headers for the instance-authenticated RPC control handshake."""
|
||||
|
||||
self._ensure_control_token(allow_generate=False)
|
||||
headers = {BOX_INSTANCE_HEADER: self._trusted_instance_uuid}
|
||||
if self._control_token:
|
||||
headers[BOX_CONTROL_TOKEN_HEADER] = self._control_token
|
||||
return headers
|
||||
return {
|
||||
BOX_CONTROL_TOKEN_HEADER: self._control_token,
|
||||
BOX_INSTANCE_HEADER: self._trusted_instance_uuid,
|
||||
}
|
||||
|
||||
def get_relay_headers(
|
||||
self,
|
||||
action_context: ActionContext,
|
||||
) -> dict[str, str]:
|
||||
"""Return instance- and placement-scoped relay handshake headers."""
|
||||
"""Return authenticated, placement-scoped relay handshake headers."""
|
||||
|
||||
context = ActionContext.model_validate(action_context).without_installation()
|
||||
if context.instance_uuid != self._trusted_instance_uuid:
|
||||
|
||||
@@ -249,10 +249,6 @@ 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)
|
||||
runtime_stats.update(
|
||||
|
||||
@@ -47,3 +47,10 @@ class SpaceModel(pydantic.BaseModel):
|
||||
status: str
|
||||
created_at: str | None = None
|
||||
updated_at: str | None = None
|
||||
|
||||
|
||||
class SpaceModelSelection(pydantic.BaseModel):
|
||||
"""Minimal model identity returned by the ranked selection endpoint."""
|
||||
|
||||
uuid: str
|
||||
model_id: str
|
||||
|
||||
@@ -17,4 +17,4 @@ class SpaceAccountBindingRequiredError(AccountEmailMismatchError):
|
||||
code = 'space_account_binding_required'
|
||||
|
||||
def __str__(self) -> str:
|
||||
return 'This local account must bind a LangBot Account from Account settings before LangBot Account login'
|
||||
return 'This local Account must bind Space from Account settings before Space login'
|
||||
|
||||
@@ -264,11 +264,6 @@ class PluginRuntimeConnector(ManagedRuntimeConnector):
|
||||
if not self._control_token and allow_generate:
|
||||
self._control_token = secrets.token_urlsafe(48)
|
||||
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 {}
|
||||
try:
|
||||
self._control_token = validate_runtime_secret(
|
||||
|
||||
@@ -1579,7 +1579,7 @@ class RuntimeConnectionHandler(handler.Handler):
|
||||
return await self.call_action(
|
||||
LangBotToRuntimeAction.RECONCILE_PLUGIN_INSTALLATIONS,
|
||||
request.model_dump(),
|
||||
timeout=300,
|
||||
timeout=120,
|
||||
)
|
||||
|
||||
async def apply_plugin_installation(
|
||||
@@ -1962,16 +1962,11 @@ class RuntimeConnectionHandler(handler.Handler):
|
||||
|
||||
async def get_debug_info(self, execution_context: ExecutionContext) -> dict[str, Any]:
|
||||
"""Get debug information including debug key and WS URL"""
|
||||
action_context = ActionContext(
|
||||
instance_uuid=execution_context.instance_uuid,
|
||||
workspace_uuid=execution_context.workspace_uuid,
|
||||
placement_generation=execution_context.placement_generation,
|
||||
)
|
||||
result = await self.call_action(
|
||||
LangBotToRuntimeAction.GET_DEBUG_INFO,
|
||||
{},
|
||||
timeout=10,
|
||||
action_context=action_context,
|
||||
action_context=execution_context,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@@ -328,9 +328,8 @@ box:
|
||||
enabled: true
|
||||
backend: 'local' # 'local' (Docker/nsjail), 'docker', 'nsjail', or 'e2b'. Can be written via BOX__BACKEND.
|
||||
runtime:
|
||||
# LANGBOT_BOX_CONTROL_TOKEN is optional for OSS external WebSocket
|
||||
# runtimes. To protect an exposed endpoint, set the same strong secret
|
||||
# in both LangBot and Box. Keep it out of this config file.
|
||||
# External WebSocket runtimes also require LANGBOT_BOX_CONTROL_TOKEN in
|
||||
# both LangBot and Box. Keep the shared secret out of this config file.
|
||||
endpoint: '' # External Box Runtime base URL, e.g. 'ws://127.0.0.1:5410'. Leave empty for local auto-managed runtime.
|
||||
limits:
|
||||
max_sessions: 64
|
||||
|
||||
@@ -1240,6 +1240,14 @@
|
||||
// Root container
|
||||
var root = document.createElement("div");
|
||||
root.id = "langbot-widget-root";
|
||||
root.langbotDestroy = function () {
|
||||
wsDisconnect();
|
||||
if (state.historyReloadTimer) {
|
||||
clearTimeout(state.historyReloadTimer);
|
||||
state.historyReloadTimer = null;
|
||||
}
|
||||
root.remove();
|
||||
};
|
||||
document.body.appendChild(root);
|
||||
|
||||
var shadow = root.attachShadow({ mode: "open" });
|
||||
|
||||
@@ -235,24 +235,13 @@ async def test_debug_key_requires_resource_manage_permission(plugin_security_api
|
||||
assert operator_denied.status_code == 403
|
||||
assert allowed.status_code == 200
|
||||
assert (await allowed.get_json())['data'] == {
|
||||
'debug_url': 'ws://localhost:5401/plugin/debug/ws',
|
||||
'debug_url': 'http://localhost:5401',
|
||||
'plugin_debug_key': 'runtime-debug-secret',
|
||||
'expires_at': '2026-08-04T12:00:00Z',
|
||||
}
|
||||
application.plugin_connector.get_debug_info.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_debug_info_uses_websocket_endpoint_for_legacy_config(plugin_security_api):
|
||||
application, client, _ = plugin_security_api
|
||||
application.instance_config.data['plugin'].pop('display_plugin_debug_url')
|
||||
|
||||
response = await client.get('/api/v1/plugins/debug-info', headers=_headers('manager-token'))
|
||||
|
||||
assert response.status_code == 200
|
||||
assert (await response.get_json())['data']['debug_url'] == 'ws://localhost:5401/plugin/debug/ws'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_viewer_cannot_read_plugin_runtime_logs(plugin_security_api):
|
||||
application, client, _ = plugin_security_api
|
||||
|
||||
@@ -165,51 +165,34 @@ async def test_bind_state_is_account_bound_and_requires_authentication(space_oau
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_redirect_allows_any_http_or_https_origin(space_oauth_api):
|
||||
async def test_redirect_origin_and_callback_path_are_restricted(space_oauth_api):
|
||||
_, client = space_oauth_api
|
||||
|
||||
responses = [
|
||||
await client.get(
|
||||
'/api/v1/user/space/authorize-url',
|
||||
query_string={'redirect_uri': redirect_uri},
|
||||
headers={'Origin': 'https://irrelevant.example'},
|
||||
)
|
||||
for redirect_uri in (
|
||||
'https://langbot.example/auth/space/callback',
|
||||
'https://gateway.example:8443/auth/space/callback',
|
||||
'https://192.0.2.10/auth/space/callback',
|
||||
'http://localhost:5300/auth/space/callback',
|
||||
'http://127.0.0.1:5300/auth/space/callback',
|
||||
'http://[::1]:5300/auth/space/callback',
|
||||
'http://langbot.example/auth/space/callback',
|
||||
'http://192.0.2.10:5300/auth/space/callback',
|
||||
)
|
||||
]
|
||||
wrong_origin = await client.get(
|
||||
'/api/v1/user/space/authorize-url',
|
||||
query_string={'redirect_uri': 'https://evil.example/auth/space/callback'},
|
||||
headers={'Origin': 'http://localhost'},
|
||||
)
|
||||
wrong_path = await client.get(
|
||||
'/api/v1/user/space/authorize-url',
|
||||
query_string={'redirect_uri': 'http://localhost/arbitrary'},
|
||||
headers={'Origin': 'http://localhost'},
|
||||
)
|
||||
forged_origin = await client.get(
|
||||
'/api/v1/user/space/authorize-url',
|
||||
query_string={'redirect_uri': 'https://evil.example/auth/space/callback'},
|
||||
headers={'Origin': 'https://evil.example'},
|
||||
)
|
||||
forged_host = await client.get(
|
||||
'/api/v1/user/space/authorize-url',
|
||||
query_string={'redirect_uri': 'https://evil.example/auth/space/callback'},
|
||||
headers={'Host': 'evil.example'},
|
||||
)
|
||||
|
||||
assert all(response.status_code == 200 for response in responses)
|
||||
payloads = [await response.get_json() for response in responses]
|
||||
assert all(payload['code'] == 0 for payload in payloads)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_redirect_rejects_invalid_callback_shape(space_oauth_api):
|
||||
_, client = space_oauth_api
|
||||
|
||||
responses = [
|
||||
await client.get(
|
||||
'/api/v1/user/space/authorize-url',
|
||||
query_string={'redirect_uri': redirect_uri},
|
||||
)
|
||||
for redirect_uri in (
|
||||
'https://langbot.example/arbitrary',
|
||||
'https://langbot.example/auth/space/callback?next=https://evil.example',
|
||||
'https://user@langbot.example/auth/space/callback',
|
||||
'https://langbot.example/auth/space/callback#fragment',
|
||||
)
|
||||
]
|
||||
|
||||
payloads = [await response.get_json() for response in responses]
|
||||
assert all(payload['code'] == 1 for payload in payloads)
|
||||
assert (await wrong_origin.get_json())['code'] == 1
|
||||
assert (await wrong_path.get_json())['code'] == 1
|
||||
assert (await forged_origin.get_json())['code'] == 1
|
||||
assert (await forged_host.get_json())['code'] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -9,8 +9,9 @@ Source: src/langbot/pkg/api/http/service/bot.py
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock, patch
|
||||
from types import SimpleNamespace
|
||||
import json
|
||||
import uuid
|
||||
|
||||
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_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):
|
||||
"""Returns no webhook URL for non-webhook adapters like telegram."""
|
||||
# Setup
|
||||
@@ -605,6 +629,77 @@ class TestBotServiceListEventLogs:
|
||||
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:
|
||||
"""Tests for send_message method."""
|
||||
|
||||
|
||||
@@ -820,6 +820,100 @@ class TestSpaceServiceGetModels:
|
||||
await service.get_models()
|
||||
|
||||
|
||||
class TestSpaceServiceGetModelSelection:
|
||||
"""Tests for availability-ranked model selection."""
|
||||
|
||||
@pytest.mark.parametrize('response_shape', ['direct', 'models-envelope', 'availability-wrapper'])
|
||||
async def test_preserves_selection_order_and_category_query(self, response_shape):
|
||||
ap = SimpleNamespace(instance_config=SimpleNamespace(data={}))
|
||||
service = SpaceService(ap)
|
||||
models = [
|
||||
{
|
||||
'uuid': 'best-model',
|
||||
'model_id': 'best-chat-model',
|
||||
'provider': 'provider-1',
|
||||
'category': 'chat',
|
||||
'status': 'active',
|
||||
},
|
||||
{
|
||||
'uuid': 'fallback-model',
|
||||
'model_id': 'fallback-chat-model',
|
||||
'provider': 'provider-2',
|
||||
'category': 'chat',
|
||||
'status': 'active',
|
||||
},
|
||||
]
|
||||
if response_shape == 'models-envelope':
|
||||
data = {'models': models}
|
||||
elif response_shape == 'availability-wrapper':
|
||||
data = [
|
||||
{'model': model, 'latency_ms': index + 10, 'http_code': 200}
|
||||
for index, model in enumerate(models)
|
||||
]
|
||||
else:
|
||||
data = models
|
||||
payload = {'code': 0, 'data': data}
|
||||
mock_response = MagicMock(status=200)
|
||||
|
||||
with (
|
||||
patch('langbot.pkg.api.http.service.space.httpclient.get_session') as get_session,
|
||||
patch(
|
||||
'langbot.pkg.api.http.service.space.httpclient.read_json_limited',
|
||||
new=AsyncMock(return_value=payload),
|
||||
),
|
||||
):
|
||||
session = MagicMock()
|
||||
session.get.return_value.__aenter__ = AsyncMock(return_value=mock_response)
|
||||
session.get.return_value.__aexit__ = AsyncMock(return_value=None)
|
||||
get_session.return_value = session
|
||||
|
||||
result = await service.get_model_selection('chat')
|
||||
|
||||
assert [model.uuid for model in result] == ['best-model', 'fallback-model']
|
||||
session.get.assert_called_once_with(
|
||||
'https://space.langbot.app/api/v1/models/selection',
|
||||
params={'category': 'chat'},
|
||||
)
|
||||
|
||||
async def test_recommended_model_uses_first_selection_and_refreshes_once(self):
|
||||
local_model = SimpleNamespace(uuid='local-model-uuid', name='best-chat-model')
|
||||
persistence = SimpleNamespace(
|
||||
execute_async=AsyncMock(
|
||||
side_effect=[
|
||||
_create_mock_result(first_item=None),
|
||||
_create_mock_result(first_item=local_model),
|
||||
]
|
||||
)
|
||||
)
|
||||
model_mgr = SimpleNamespace(sync_new_models_from_space=AsyncMock())
|
||||
ap = SimpleNamespace(
|
||||
instance_config=SimpleNamespace(data={}),
|
||||
persistence_mgr=persistence,
|
||||
model_mgr=model_mgr,
|
||||
)
|
||||
service = SpaceService(ap)
|
||||
service.get_model_selection = AsyncMock(
|
||||
return_value=[
|
||||
SimpleNamespace(uuid='best-upstream-uuid', model_id='best-chat-model'),
|
||||
SimpleNamespace(uuid='fallback-upstream-uuid', model_id='fallback-chat-model'),
|
||||
]
|
||||
)
|
||||
context = SimpleNamespace(
|
||||
instance_uuid='instance',
|
||||
workspace_uuid='workspace',
|
||||
placement_generation=1,
|
||||
principal=SimpleNamespace(),
|
||||
entitlement_revision=0,
|
||||
)
|
||||
|
||||
result = await service.get_recommended_chat_model(context)
|
||||
|
||||
assert result == {'uuid': 'local-model-uuid', 'name': 'best-chat-model'}
|
||||
service.get_model_selection.assert_awaited_once_with('chat')
|
||||
model_mgr.sync_new_models_from_space.assert_awaited_once()
|
||||
assert persistence.execute_async.await_count == 2
|
||||
|
||||
|
||||
class TestSpaceServiceCreditsCache:
|
||||
"""Tests for credits cache behavior."""
|
||||
|
||||
|
||||
@@ -377,7 +377,7 @@ class TestUserServiceAuthenticate:
|
||||
service = UserService(ap)
|
||||
|
||||
# Execute & Verify
|
||||
with pytest.raises(ValueError, match='请使用 LangBot 账号登录'):
|
||||
with pytest.raises(ValueError, match='请使用 Space 账户登录'):
|
||||
await service.authenticate('space@example.com', 'password')
|
||||
|
||||
|
||||
@@ -726,7 +726,7 @@ class TestUserServiceCreateOrUpdateSpaceUser:
|
||||
)
|
||||
service = UserService(ap)
|
||||
|
||||
with pytest.raises(ControlPlaneDirectoryRequiredError, match='LangBot Account'):
|
||||
with pytest.raises(ControlPlaneDirectoryRequiredError, match='Space account'):
|
||||
await service.register_invited_account('invite-token', 'member@example.com', 'password')
|
||||
|
||||
async def test_create_or_update_new_space_user_first_init(self):
|
||||
|
||||
@@ -24,7 +24,7 @@ from langbot.pkg.box.connector import BoxRuntimeConnector
|
||||
_CONTROL_TOKEN = 'box-control-token-that-is-longer-than-32-bytes'
|
||||
|
||||
|
||||
def make_app(logger: Mock, runtime_endpoint: str = '', *, cloud: bool = False):
|
||||
def make_app(logger: Mock, runtime_endpoint: str = ''):
|
||||
return SimpleNamespace(
|
||||
logger=logger,
|
||||
workspace_service=SimpleNamespace(instance_uuid='instance-a'),
|
||||
@@ -42,7 +42,6 @@ def make_app(logger: Mock, runtime_endpoint: str = '', *, cloud: bool = False):
|
||||
}
|
||||
}
|
||||
),
|
||||
deployment=SimpleNamespace(mode='cloud' if cloud else 'oss'),
|
||||
)
|
||||
|
||||
|
||||
@@ -307,27 +306,10 @@ def test_box_runtime_connector_rejects_relay_context_from_other_instance(
|
||||
)
|
||||
|
||||
|
||||
def test_external_box_runtime_control_headers_are_tokenless_when_secret_is_unset(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
def test_external_box_runtime_fails_closed_without_control_token(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.delenv(BOX_CONTROL_TOKEN_ENV, raising=False)
|
||||
connector = BoxRuntimeConnector(make_app(Mock(), runtime_endpoint='http://box-runtime:5410'))
|
||||
|
||||
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):
|
||||
monkeypatch.setenv(BOX_CONTROL_TOKEN_ENV, 'too-short')
|
||||
connector = BoxRuntimeConnector(make_app(Mock(), runtime_endpoint='http://box-runtime:5410'))
|
||||
|
||||
with pytest.raises(BoxRuntimeUnavailableError, match=BOX_CONTROL_TOKEN_ENV):
|
||||
connector.get_control_headers()
|
||||
|
||||
|
||||
@@ -87,10 +87,7 @@ async def test_runtime_resource_stats_are_aggregate_and_constant_time() -> None:
|
||||
app.platform_mgr = SimpleNamespace(_bots_by_key={})
|
||||
app.pipeline_mgr = SimpleNamespace(_pipelines_by_key={})
|
||||
app.rag_mgr = SimpleNamespace(knowledge_bases={})
|
||||
app.plugin_connector = SimpleNamespace(
|
||||
_known_desired_states={'installation': object()},
|
||||
_runtime_available=lambda: True,
|
||||
)
|
||||
app.plugin_connector = SimpleNamespace(_known_desired_states={'installation': object()})
|
||||
app.persistence_mgr = SimpleNamespace(
|
||||
get_resource_stats=lambda: {
|
||||
'configured_capacity': 20,
|
||||
@@ -143,4 +140,3 @@ async def test_runtime_resource_stats_are_aggregate_and_constant_time() -> None:
|
||||
}
|
||||
assert stats['models']['providers'] == 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(*, cloud: bool = False) -> PluginRuntimeConnector:
|
||||
def make_connector() -> PluginRuntimeConnector:
|
||||
app = SimpleNamespace(
|
||||
logger=Mock(),
|
||||
instance_config=SimpleNamespace(
|
||||
@@ -34,7 +34,6 @@ def make_connector(*, cloud: bool = False) -> PluginRuntimeConnector:
|
||||
'space': {'url': ''},
|
||||
}
|
||||
),
|
||||
deployment=SimpleNamespace(mode='cloud' if cloud else 'oss'),
|
||||
)
|
||||
return PluginRuntimeConnector(app, AsyncMock())
|
||||
|
||||
@@ -333,14 +332,6 @@ def test_external_runtime_control_headers_are_empty_when_secret_is_unset(monkeyp
|
||||
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):
|
||||
monkeypatch.delenv(PLUGIN_RUNTIME_CONTROL_TOKEN_ENV, raising=False)
|
||||
connector = make_connector()
|
||||
|
||||
@@ -9,8 +9,8 @@ from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock, Mock
|
||||
import pytest
|
||||
|
||||
from langbot_plugin.entities.io.actions.enums import LangBotToRuntimeAction, PluginToRuntimeAction
|
||||
from langbot_plugin.entities.io.context import ActionContext, InstallationBinding, PluginInstallationDesiredState
|
||||
from langbot_plugin.entities.io.actions.enums import PluginToRuntimeAction
|
||||
from langbot_plugin.entities.io.context import ActionContext, InstallationBinding
|
||||
|
||||
|
||||
def make_handler(app):
|
||||
@@ -67,20 +67,6 @@ def make_handler(app):
|
||||
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:
|
||||
"""Tests for handler query variable logic."""
|
||||
|
||||
|
||||
@@ -444,23 +444,3 @@ async def test_host_to_runtime_action_carries_trusted_connector_context():
|
||||
'runtime_id': 'runtime-a',
|
||||
}
|
||||
assert request.get('context') is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_debug_info_converts_execution_context_to_sdk_action_context():
|
||||
runtime_handler, _app, _installation_context = make_handler()
|
||||
runtime_handler.call_action = AsyncMock(return_value={'plugin_debug_key': 'debug-key'})
|
||||
execution_context = ExecutionContext(
|
||||
instance_uuid='instance-a',
|
||||
workspace_uuid='workspace-a',
|
||||
placement_generation=7,
|
||||
)
|
||||
|
||||
result = await runtime_handler.get_debug_info(execution_context)
|
||||
|
||||
assert result == {'plugin_debug_key': 'debug-key'}
|
||||
assert runtime_handler.call_action.await_args.kwargs['action_context'] == ActionContext(
|
||||
instance_uuid='instance-a',
|
||||
workspace_uuid='workspace-a',
|
||||
placement_generation=7,
|
||||
)
|
||||
|
||||
@@ -1018,7 +1018,7 @@ name = "cuda-bindings"
|
||||
version = "13.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cuda-pathfinder", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
{ name = "cuda-pathfinder" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/51/6b/457ca12dad3ee9bfcc9a545cfd6b64b359ba49de40f776f6e028e678f262/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5879712accf6e14bb01aa5e67440eb84998b8d104b509cc7a6dc0b8f656a474", size = 6053539, upload-time = "2026-05-29T23:11:43.19Z" },
|
||||
@@ -1051,34 +1051,34 @@ wheels = [
|
||||
|
||||
[package.optional-dependencies]
|
||||
cudart = [
|
||||
{ name = "nvidia-cuda-runtime", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cuda-runtime" },
|
||||
]
|
||||
cufft = [
|
||||
{ name = "nvidia-cufft", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cufft" },
|
||||
]
|
||||
cufile = [
|
||||
{ name = "nvidia-cufile", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cufile" },
|
||||
]
|
||||
cupti = [
|
||||
{ name = "nvidia-cuda-cupti", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cuda-cupti" },
|
||||
]
|
||||
curand = [
|
||||
{ name = "nvidia-curand", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-curand" },
|
||||
]
|
||||
cusolver = [
|
||||
{ name = "nvidia-cusolver", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cusolver" },
|
||||
]
|
||||
cusparse = [
|
||||
{ name = "nvidia-cusparse", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cusparse" },
|
||||
]
|
||||
nvjitlink = [
|
||||
{ name = "nvidia-nvjitlink", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nvjitlink" },
|
||||
]
|
||||
nvrtc = [
|
||||
{ name = "nvidia-cuda-nvrtc", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cuda-nvrtc" },
|
||||
]
|
||||
nvtx = [
|
||||
{ name = "nvidia-nvtx", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nvtx" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2125,7 +2125,7 @@ requires-dist = [
|
||||
{ name = "ebooklib", specifier = ">=0.18" },
|
||||
{ name = "gewechat-client", specifier = ">=0.1.5" },
|
||||
{ name = "html2text", specifier = ">=2024.2.26" },
|
||||
{ name = "langbot-plugin", git = "https://github.com/langbot-app/langbot-plugin-sdk.git?rev=555a58e5db3de28e977b08dd4cd116b332848a19" },
|
||||
{ name = "langbot-plugin", git = "https://github.com/langbot-app/langbot-plugin-sdk.git?rev=9d216208cdfb41f0cb7fcb64632e2a46816d6dc6" },
|
||||
{ name = "langchain", specifier = ">=1.3.9" },
|
||||
{ name = "langchain-core", specifier = ">=1.3.3" },
|
||||
{ name = "langchain-text-splitters", specifier = ">=1.1.2" },
|
||||
@@ -2191,8 +2191,8 @@ dev = [
|
||||
|
||||
[[package]]
|
||||
name = "langbot-plugin"
|
||||
version = "0.5.3"
|
||||
source = { git = "https://github.com/langbot-app/langbot-plugin-sdk.git?rev=555a58e5db3de28e977b08dd4cd116b332848a19#555a58e5db3de28e977b08dd4cd116b332848a19" }
|
||||
version = "0.5.0"
|
||||
source = { git = "https://github.com/langbot-app/langbot-plugin-sdk.git?rev=9d216208cdfb41f0cb7fcb64632e2a46816d6dc6#9d216208cdfb41f0cb7fcb64632e2a46816d6dc6" }
|
||||
dependencies = [
|
||||
{ name = "aiofiles" },
|
||||
{ name = "aiohttp" },
|
||||
@@ -3238,7 +3238,7 @@ name = "nvidia-cublas"
|
||||
version = "13.1.1.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-cuda-nvrtc", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
{ name = "nvidia-cuda-nvrtc" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" },
|
||||
@@ -3277,7 +3277,7 @@ name = "nvidia-cudnn-cu13"
|
||||
version = "9.20.0.48"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-cublas", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
{ name = "nvidia-cublas" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" },
|
||||
@@ -3289,7 +3289,7 @@ name = "nvidia-cufft"
|
||||
version = "12.0.0.61"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-nvjitlink", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
{ name = "nvidia-nvjitlink" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" },
|
||||
@@ -3319,9 +3319,9 @@ name = "nvidia-cusolver"
|
||||
version = "12.0.4.66"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-cublas", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
{ name = "nvidia-cusparse", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
{ name = "nvidia-nvjitlink", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
{ name = "nvidia-cublas" },
|
||||
{ name = "nvidia-cusparse" },
|
||||
{ name = "nvidia-nvjitlink" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" },
|
||||
@@ -3333,7 +3333,7 @@ name = "nvidia-cusparse"
|
||||
version = "12.6.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nvidia-nvjitlink", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
{ name = "nvidia-nvjitlink" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" },
|
||||
@@ -5163,10 +5163,10 @@ name = "scikit-learn"
|
||||
version = "1.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "joblib", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "numpy", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "scipy", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "threadpoolctl", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "joblib" },
|
||||
{ name = "numpy" },
|
||||
{ name = "scipy" },
|
||||
{ name = "threadpoolctl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" }
|
||||
wheels = [
|
||||
@@ -5213,7 +5213,7 @@ name = "scipy"
|
||||
version = "1.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "numpy", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "numpy" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" }
|
||||
wheels = [
|
||||
@@ -5284,14 +5284,14 @@ name = "sentence-transformers"
|
||||
version = "5.2.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "huggingface-hub", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "numpy", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "scikit-learn", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "scipy", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "torch", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "tqdm", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "transformers", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "huggingface-hub" },
|
||||
{ name = "numpy" },
|
||||
{ name = "scikit-learn" },
|
||||
{ name = "scipy" },
|
||||
{ name = "torch" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "transformers" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/30/21664028fc0776eb1ca024879480bbbab36f02923a8ff9e4cae5a150fa35/sentence_transformers-5.2.3.tar.gz", hash = "sha256:3cd3044e1f3fe859b6a1b66336aac502eaae5d3dd7d5c8fc237f37fbf58137c7", size = 381623, upload-time = "2026-02-17T14:05:20.238Z" }
|
||||
wheels = [
|
||||
@@ -5664,21 +5664,21 @@ name = "torch"
|
||||
version = "2.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cuda-bindings", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "cuda-toolkit", extra = ["cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "filelock", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "fsspec", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "jinja2", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "networkx", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "nvidia-cublas", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cudnn-cu13", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cusparselt-cu13", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nccl-cu13", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nvshmem-cu13", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "setuptools", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "sympy", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "triton", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
{ name = "typing-extensions", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "cuda-bindings", marker = "sys_platform == 'linux'" },
|
||||
{ name = "cuda-toolkit", extra = ["cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" },
|
||||
{ name = "filelock" },
|
||||
{ name = "fsspec" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "networkx" },
|
||||
{ name = "nvidia-cublas", marker = "sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" },
|
||||
{ name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" },
|
||||
{ name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" },
|
||||
{ name = "setuptools" },
|
||||
{ name = "sympy" },
|
||||
{ name = "triton", marker = "sys_platform == 'linux'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/59/38/7028d3be540f1dcdf41660a2b01d0c51d2cb73915fe370d84e4d277a6d47/torch-2.12.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ef81f503912effea2ce3d9b12a2e3a6ed488943e91271c90c7a829f60baf6aa2", size = 87975425, upload-time = "2026-06-17T21:08:34.094Z" },
|
||||
@@ -5720,15 +5720,15 @@ name = "transformers"
|
||||
version = "5.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "huggingface-hub", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "numpy", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "packaging", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "pyyaml", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "regex", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "safetensors", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "tokenizers", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "tqdm", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "typer", marker = "python_full_version >= '3.14'" },
|
||||
{ name = "huggingface-hub" },
|
||||
{ name = "numpy" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "regex" },
|
||||
{ name = "safetensors" },
|
||||
{ name = "tokenizers" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "typer" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/1a/70e830d53ecc96ce69cfa8de38f163712d2b43ac52fbd743f39f56025c31/transformers-5.3.0.tar.gz", hash = "sha256:009555b364029da9e2946d41f1c5de9f15e6b1df46b189b7293f33a161b9c557", size = 8830831, upload-time = "2026-03-04T17:41:46.119Z" }
|
||||
wheels = [
|
||||
@@ -5989,9 +5989,9 @@ name = "valkey-glide"
|
||||
version = "2.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio", marker = "sys_platform != 'win32'" },
|
||||
{ name = "protobuf", marker = "sys_platform != 'win32'" },
|
||||
{ name = "sniffio", marker = "sys_platform != 'win32'" },
|
||||
{ name = "anyio" },
|
||||
{ name = "protobuf" },
|
||||
{ name = "sniffio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/a2/582b34c6acc8dc857c537f6007459cba48dfa0dc404789a657e5c1a998c0/valkey_glide-2.4.1.tar.gz", hash = "sha256:f1155d84156d11b90488aa67e90102f0bf98a45314f5b99308ac9074c05f7241", size = 898030, upload-time = "2026-05-28T21:41:55.881Z" }
|
||||
wheels = [
|
||||
|
||||
+5
-1
@@ -1 +1,5 @@
|
||||
VITE_API_BASE_URL=http://localhost:5300
|
||||
# Leave empty in development to use Vite's same-origin proxy. This keeps API,
|
||||
# login, and WebSocket requests working when the UI is opened from another
|
||||
# device on the local network.
|
||||
VITE_API_BASE_URL=
|
||||
VITE_API_PROXY_TARGET=http://127.0.0.1:5300
|
||||
|
||||
@@ -20,6 +20,7 @@ export function BotLogListComponent({
|
||||
autoExpandImages = false,
|
||||
hideDetailedLogsLink = false,
|
||||
hideToolbar = false,
|
||||
onMessageReceived,
|
||||
}: {
|
||||
botId: string;
|
||||
/** When true, log entries with images are rendered expanded by default */
|
||||
@@ -28,6 +29,8 @@ export function BotLogListComponent({
|
||||
hideDetailedLogsLink?: boolean;
|
||||
/** When true, hides the entire toolbar (auto-refresh, level filter, detailed logs link) */
|
||||
hideToolbar?: boolean;
|
||||
/** Called after an inbound person/group message appears in the bot log. */
|
||||
onMessageReceived?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
@@ -41,6 +44,8 @@ export function BotLogListComponent({
|
||||
]);
|
||||
const listContainerRef = useRef<HTMLDivElement>(null);
|
||||
const botLogListRef = useRef<BotLog[]>(botLogList);
|
||||
const onMessageReceivedRef = useRef(onMessageReceived);
|
||||
onMessageReceivedRef.current = onMessageReceived;
|
||||
|
||||
const logLevels = [
|
||||
{ value: 'error', label: 'ERROR' },
|
||||
@@ -108,6 +113,9 @@ export function BotLogListComponent({
|
||||
manager.subscribeLogPush(handleBotLogPush);
|
||||
manager.loadFirstPage().then((response) => {
|
||||
setBotLogList(response.reverse());
|
||||
if (response.some((log) => Boolean(log.message_session_id))) {
|
||||
onMessageReceivedRef.current?.();
|
||||
}
|
||||
});
|
||||
listenScroll();
|
||||
}
|
||||
@@ -138,6 +146,9 @@ export function BotLogListComponent({
|
||||
|
||||
function handleBotLogPush(response: BotLog[]) {
|
||||
setBotLogList(response.reverse());
|
||||
if (response.some((log) => Boolean(log.message_session_id))) {
|
||||
onMessageReceivedRef.current?.();
|
||||
}
|
||||
}
|
||||
|
||||
const handleScroll = useCallback(
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
XCircle,
|
||||
} from 'lucide-react';
|
||||
import QRCode from 'qrcode';
|
||||
import { getActiveWorkspaceUuid } from '@/app/infra/http/workspaceContext';
|
||||
|
||||
export type QrLoginPlatform =
|
||||
| 'feishu'
|
||||
@@ -56,12 +55,12 @@ const PLATFORM_CONFIGS: Record<QrLoginPlatform, PlatformConfig> = {
|
||||
},
|
||||
weixin: {
|
||||
titleKey: 'weixin.scanLogin',
|
||||
connectingKey: 'weixin.connecting',
|
||||
connectingKey: 'feishu.connecting',
|
||||
scanQRCodeKey: 'weixin.scanQRCode',
|
||||
waitingKey: 'weixin.waitingForScan',
|
||||
waitingKey: 'feishu.waitingForScan',
|
||||
successKey: 'weixin.loginSuccess',
|
||||
failedKey: 'weixin.loginFailed',
|
||||
retryKey: 'weixin.retry',
|
||||
retryKey: 'feishu.retry',
|
||||
apiBase: '/api/v1/platform/adapters/weixin/login',
|
||||
extractSuccess: (data) => ({
|
||||
token: data.token,
|
||||
@@ -147,8 +146,6 @@ export default function QrCodeLoginDialog({
|
||||
const checkExpiredRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
const sessionIdRef = useRef<string | null>(null);
|
||||
const sessionWorkspaceUuidRef = useRef<string | null>(null);
|
||||
const sessionApiBaseRef = useRef('');
|
||||
const baseUrlRef = useRef('');
|
||||
const cleanedRef = useRef(false);
|
||||
|
||||
@@ -183,23 +180,18 @@ export default function QrCodeLoginDialog({
|
||||
}
|
||||
if (sessionIdRef.current) {
|
||||
const token = localStorage.getItem('token');
|
||||
const workspaceUuid = sessionWorkspaceUuidRef.current;
|
||||
const baseUrl =
|
||||
import.meta.env.VITE_API_BASE_URL || window.location.origin;
|
||||
fetch(
|
||||
`${baseUrlRef.current}${sessionApiBaseRef.current}/${sessionIdRef.current}`,
|
||||
`${baseUrl}${platformConfigRef.current.apiBase}/${sessionIdRef.current}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
...(workspaceUuid ? { 'X-Workspace-Id': workspaceUuid } : {}),
|
||||
},
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
keepalive: true,
|
||||
},
|
||||
).catch(() => {});
|
||||
sessionIdRef.current = null;
|
||||
}
|
||||
sessionWorkspaceUuidRef.current = null;
|
||||
sessionApiBaseRef.current = '';
|
||||
baseUrlRef.current = '';
|
||||
}, []);
|
||||
|
||||
const startLogin = useCallback(async () => {
|
||||
@@ -212,7 +204,6 @@ export default function QrCodeLoginDialog({
|
||||
setSuccessMeta('');
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
const workspaceUuid = getActiveWorkspaceUuid();
|
||||
const baseUrl = import.meta.env.VITE_API_BASE_URL || window.location.origin;
|
||||
baseUrlRef.current = baseUrl;
|
||||
const cfg = platformConfigRef.current;
|
||||
@@ -223,10 +214,7 @@ export default function QrCodeLoginDialog({
|
||||
|
||||
const res = await fetch(`${baseUrl}${cfg.apiBase}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
...(workspaceUuid ? { 'X-Workspace-Id': workspaceUuid } : {}),
|
||||
},
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
@@ -237,8 +225,6 @@ export default function QrCodeLoginDialog({
|
||||
|
||||
const { session_id, qr_data_url, qr_url, expire_at } = json.data;
|
||||
sessionIdRef.current = session_id;
|
||||
sessionWorkspaceUuidRef.current = workspaceUuid;
|
||||
sessionApiBaseRef.current = cfg.apiBase;
|
||||
|
||||
if (qr_data_url) {
|
||||
setQrDataUrl(qr_data_url);
|
||||
@@ -284,19 +270,11 @@ export default function QrCodeLoginDialog({
|
||||
`${baseUrlRef.current}${cfg.apiBase}/${sessionIdRef.current}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
...(workspaceUuid
|
||||
? { 'X-Workspace-Id': workspaceUuid }
|
||||
: {}),
|
||||
},
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
keepalive: true,
|
||||
},
|
||||
).catch(() => {});
|
||||
sessionIdRef.current = null;
|
||||
sessionWorkspaceUuidRef.current = null;
|
||||
sessionApiBaseRef.current = '';
|
||||
baseUrlRef.current = '';
|
||||
}
|
||||
setState('expired');
|
||||
}
|
||||
@@ -308,12 +286,7 @@ export default function QrCodeLoginDialog({
|
||||
try {
|
||||
const pollRes = await fetch(
|
||||
`${baseUrl}${cfg.apiBase}/status/${session_id}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
...(workspaceUuid ? { 'X-Workspace-Id': workspaceUuid } : {}),
|
||||
},
|
||||
},
|
||||
{ headers: { Authorization: `Bearer ${token}` } },
|
||||
);
|
||||
if (!pollRes.ok) return;
|
||||
|
||||
|
||||
@@ -363,7 +363,9 @@ export interface WizardProgress {
|
||||
step: number;
|
||||
selected_adapter: string | null;
|
||||
created_bot_uuid: string | null;
|
||||
created_pipeline_uuid?: string | null;
|
||||
bot_saved: boolean;
|
||||
message_received?: boolean;
|
||||
selected_runner: string | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -461,6 +461,15 @@ export class BackendClient extends BaseHttpClient {
|
||||
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(
|
||||
botId: string,
|
||||
limit: number = 100,
|
||||
@@ -1046,12 +1055,21 @@ export class BackendClient extends BaseHttpClient {
|
||||
step: number;
|
||||
selected_adapter: string | null;
|
||||
created_bot_uuid: string | null;
|
||||
created_pipeline_uuid?: string | null;
|
||||
bot_saved: boolean;
|
||||
message_received?: boolean;
|
||||
selected_runner: string | null;
|
||||
}): Promise<void> {
|
||||
return this.put('/api/v1/system/wizard/progress', progress);
|
||||
}
|
||||
|
||||
public getWizardRecommendedModel(): Promise<{
|
||||
uuid: string;
|
||||
name: string;
|
||||
}> {
|
||||
return this.get('/api/v1/system/wizard/recommended-model');
|
||||
}
|
||||
|
||||
public getAsyncTasks(params?: {
|
||||
type?: string;
|
||||
kind?: string;
|
||||
|
||||
+480
-173
@@ -8,24 +8,26 @@ import {
|
||||
ArrowRight,
|
||||
Check,
|
||||
Sparkles,
|
||||
PartyPopper,
|
||||
Loader2,
|
||||
X,
|
||||
ExternalLink,
|
||||
Cable,
|
||||
Settings2,
|
||||
Blocks,
|
||||
Copy,
|
||||
Send,
|
||||
Webhook,
|
||||
MessageSquare,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import {
|
||||
userInfo,
|
||||
systemInfo,
|
||||
bootstrapWorkspaceSession,
|
||||
initializeSystemInfo,
|
||||
} from '@/app/infra/http';
|
||||
import {
|
||||
Adapter,
|
||||
Bot,
|
||||
Pipeline,
|
||||
WizardProgress,
|
||||
} from '@/app/infra/entities/api';
|
||||
import { Adapter, Bot, WizardProgress } from '@/app/infra/entities/api';
|
||||
import { IDynamicFormItemSchema } from '@/app/infra/entities/form/dynamic';
|
||||
import {
|
||||
PipelineConfigTab,
|
||||
@@ -47,6 +49,7 @@ import { getAdapterDocUrl } from '@/app/infra/entities/adapter-docs';
|
||||
import i18n from 'i18next';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -70,7 +73,7 @@ import {
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const TOTAL_STEPS = 4;
|
||||
const TOTAL_STEPS = 3;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main Wizard Page (full-screen, no sidebar)
|
||||
@@ -92,6 +95,9 @@ export default function WizardPage() {
|
||||
);
|
||||
const [runnerConfig, setRunnerConfig] = useState<Record<string, unknown>>({});
|
||||
const [createdBotUuid, setCreatedBotUuid] = useState<string | null>(null);
|
||||
const [createdPipelineUuid, setCreatedPipelineUuid] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [webhookUrl, setWebhookUrl] = useState<string>('');
|
||||
const [extraWebhookUrl, setExtraWebhookUrl] = useState<string>('');
|
||||
|
||||
@@ -105,6 +111,10 @@ export default function WizardPage() {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isSavingBot, setIsSavingBot] = useState(false);
|
||||
const [botSaved, setBotSaved] = useState(false);
|
||||
const [messageReceived, setMessageReceived] = useState(false);
|
||||
const [aiChoice, setAiChoice] = useState<
|
||||
'external' | 'own-model' | 'more-features' | null
|
||||
>(null);
|
||||
|
||||
// ---- Helper: persist wizard progress to backend (fire-and-forget) ----
|
||||
const saveProgress = useCallback(
|
||||
@@ -113,14 +123,25 @@ export default function WizardPage() {
|
||||
step: overrides.step ?? currentStep,
|
||||
selected_adapter: overrides.selected_adapter ?? selectedAdapter,
|
||||
created_bot_uuid: overrides.created_bot_uuid ?? createdBotUuid,
|
||||
created_pipeline_uuid:
|
||||
overrides.created_pipeline_uuid ?? createdPipelineUuid,
|
||||
bot_saved: overrides.bot_saved ?? botSaved,
|
||||
message_received: overrides.message_received ?? messageReceived,
|
||||
selected_runner: overrides.selected_runner ?? selectedRunner,
|
||||
};
|
||||
httpClient.saveWizardProgress(progress).catch((err) => {
|
||||
console.error('Failed to save wizard progress', err);
|
||||
});
|
||||
},
|
||||
[currentStep, selectedAdapter, createdBotUuid, botSaved, selectedRunner],
|
||||
[
|
||||
currentStep,
|
||||
selectedAdapter,
|
||||
createdBotUuid,
|
||||
createdPipelineUuid,
|
||||
botSaved,
|
||||
messageReceived,
|
||||
selectedRunner,
|
||||
],
|
||||
);
|
||||
|
||||
// ---- Fetch remote data & restore progress ----
|
||||
@@ -158,7 +179,9 @@ export default function WizardPage() {
|
||||
|
||||
setSelectedAdapter(progress.selected_adapter);
|
||||
setCreatedBotUuid(progress.created_bot_uuid);
|
||||
setCreatedPipelineUuid(progress.created_pipeline_uuid ?? null);
|
||||
setBotSaved(progress.bot_saved ?? false);
|
||||
setMessageReceived(progress.message_received ?? false);
|
||||
setSelectedRunner(progress.selected_runner);
|
||||
|
||||
// Restore bot name from fetched bot data
|
||||
@@ -182,7 +205,9 @@ export default function WizardPage() {
|
||||
step: 0,
|
||||
selected_adapter: null,
|
||||
created_bot_uuid: null,
|
||||
created_pipeline_uuid: null,
|
||||
bot_saved: false,
|
||||
message_received: false,
|
||||
selected_runner: null,
|
||||
})
|
||||
.catch(() => {});
|
||||
@@ -210,7 +235,9 @@ export default function WizardPage() {
|
||||
const runnerOptions = useMemo(() => {
|
||||
if (!runnerStage) return [];
|
||||
const runnerField = runnerStage.config.find((c) => c.name === 'runner');
|
||||
return runnerField?.options ?? [];
|
||||
return (runnerField?.options ?? []).filter(
|
||||
(option) => option.name !== 'local-agent',
|
||||
);
|
||||
}, [runnerStage]);
|
||||
|
||||
const selectedRunnerConfigStage: PipelineConfigStage | undefined =
|
||||
@@ -284,13 +311,20 @@ export default function WizardPage() {
|
||||
case 0:
|
||||
return selectedAdapter !== null;
|
||||
case 1:
|
||||
return createdBotUuid !== null && botSaved;
|
||||
return createdBotUuid !== null && botSaved && messageReceived;
|
||||
case 2:
|
||||
return selectedRunner !== null;
|
||||
return aiChoice !== null;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}, [currentStep, selectedAdapter, createdBotUuid, botSaved, selectedRunner]);
|
||||
}, [
|
||||
currentStep,
|
||||
selectedAdapter,
|
||||
createdBotUuid,
|
||||
botSaved,
|
||||
messageReceived,
|
||||
aiChoice,
|
||||
]);
|
||||
|
||||
const goNext = useCallback(() => {
|
||||
if (currentStep < TOTAL_STEPS - 1 && canProceed()) {
|
||||
@@ -359,7 +393,9 @@ export default function WizardPage() {
|
||||
step: 1,
|
||||
selected_adapter: selectedAdapter,
|
||||
created_bot_uuid: resp.uuid,
|
||||
created_pipeline_uuid: null,
|
||||
bot_saved: false,
|
||||
message_received: false,
|
||||
selected_runner: null,
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -373,21 +409,61 @@ export default function WizardPage() {
|
||||
}, [selectedAdapter, adapters, t, saveProgress]);
|
||||
|
||||
// ---- Save Bot Config & Enable (Step 1) ----
|
||||
// Updates the bot's adapter config and enables it.
|
||||
// Creates a recommended Local Agent pipeline, binds it, and enables the bot.
|
||||
|
||||
const handleSaveBot = useCallback(async () => {
|
||||
if (!createdBotUuid || !selectedAdapter) return;
|
||||
setIsSavingBot(true);
|
||||
|
||||
let createdPipelineThisAttempt: string | null = null;
|
||||
try {
|
||||
let pipelineUuid = createdPipelineUuid;
|
||||
if (!pipelineUuid) {
|
||||
const recommendedModel = await httpClient.getWizardRecommendedModel();
|
||||
const pipelineResp = await httpClient.createPipeline({
|
||||
name: `${botName} Agent`,
|
||||
description: botDescription || '',
|
||||
config: {},
|
||||
});
|
||||
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
|
||||
>;
|
||||
await httpClient.updatePipeline(pipelineUuid, {
|
||||
name: `${botName} Agent`,
|
||||
description: botDescription || '',
|
||||
config: {
|
||||
...createdPipeline.pipeline.config,
|
||||
ai: {
|
||||
...aiConfig,
|
||||
runner: { runner: 'local-agent' },
|
||||
'local-agent': {
|
||||
...localAgentConfig,
|
||||
model: { primary: recommendedModel.uuid, fallbacks: [] },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
setCreatedPipelineUuid(pipelineUuid);
|
||||
}
|
||||
|
||||
await httpClient.updateBot(createdBotUuid, {
|
||||
name: botName,
|
||||
description: botDescription || '',
|
||||
adapter: selectedAdapter,
|
||||
adapter_config: adapterConfig,
|
||||
enable: true,
|
||||
use_pipeline_uuid: pipelineUuid,
|
||||
});
|
||||
setBotSaved(true);
|
||||
setMessageReceived(false);
|
||||
|
||||
// Re-fetch runtime info to get updated webhook URL(s)
|
||||
try {
|
||||
@@ -404,8 +480,19 @@ export default function WizardPage() {
|
||||
}
|
||||
|
||||
// Persist progress
|
||||
saveProgress({ step: 1, bot_saved: true });
|
||||
saveProgress({
|
||||
step: 1,
|
||||
bot_saved: true,
|
||||
message_received: false,
|
||||
created_pipeline_uuid: pipelineUuid,
|
||||
});
|
||||
} catch (err) {
|
||||
if (createdPipelineThisAttempt) {
|
||||
await httpClient
|
||||
.deletePipeline(createdPipelineThisAttempt)
|
||||
.catch(() => {});
|
||||
setCreatedPipelineUuid(null);
|
||||
}
|
||||
const apiErr = err as { msg?: string };
|
||||
toast.error(
|
||||
t('wizard.createError') + (apiErr?.msg ? `: ${apiErr.msg}` : ''),
|
||||
@@ -419,60 +506,80 @@ export default function WizardPage() {
|
||||
botName,
|
||||
botDescription,
|
||||
adapterConfig,
|
||||
createdPipelineUuid,
|
||||
t,
|
||||
saveProgress,
|
||||
]);
|
||||
|
||||
// ---- Create Pipeline & Link (Step 2 finish) ----
|
||||
const handleMessageReceived = useCallback(() => {
|
||||
if (messageReceived) return;
|
||||
setMessageReceived(true);
|
||||
saveProgress({ step: 1, message_received: true });
|
||||
}, [messageReceived, saveProgress]);
|
||||
|
||||
const completeWizard = useCallback(async () => {
|
||||
await httpClient.updateWizardStatus('completed');
|
||||
systemInfo.wizard_status = 'completed';
|
||||
systemInfo.wizard_progress = null;
|
||||
}, []);
|
||||
|
||||
// ---- Complete the optional AI Engine step ----
|
||||
|
||||
const handleFinish = useCallback(async () => {
|
||||
if (!selectedRunner || !createdBotUuid) return;
|
||||
if (!aiChoice || !createdBotUuid || !createdPipelineUuid) return;
|
||||
if (aiChoice === 'external' && !selectedRunner) return;
|
||||
setIsSubmitting(true);
|
||||
let externalPipelineUuid: string | null = null;
|
||||
let externalPipelineBound = false;
|
||||
|
||||
try {
|
||||
// 1. Create pipeline (backend fills config from default template)
|
||||
const pipeline: Pipeline = {
|
||||
name: `${botName} Pipeline`,
|
||||
description: botDescription || '',
|
||||
config: {},
|
||||
};
|
||||
const pipelineResp = await httpClient.createPipeline(pipeline);
|
||||
if (aiChoice === 'external' && selectedRunner) {
|
||||
const pipelineResp = await httpClient.createPipeline({
|
||||
name: `${botName} External Agent`,
|
||||
description: botDescription || '',
|
||||
config: {},
|
||||
});
|
||||
externalPipelineUuid = pipelineResp.uuid;
|
||||
const createdPipeline = await httpClient.getPipeline(pipelineResp.uuid);
|
||||
const fullConfig = createdPipeline.pipeline.config;
|
||||
await httpClient.updatePipeline(pipelineResp.uuid, {
|
||||
name: `${botName} External Agent`,
|
||||
description: botDescription || '',
|
||||
config: {
|
||||
...fullConfig,
|
||||
ai: {
|
||||
...fullConfig.ai,
|
||||
runner: { runner: selectedRunner },
|
||||
[selectedRunner]: runnerConfig,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 2. Fetch the created pipeline to get the full default config
|
||||
// (includes trigger, safety, ai, output sections).
|
||||
// Then merge only the AI section with the wizard's runner config.
|
||||
const createdPipeline = await httpClient.getPipeline(pipelineResp.uuid);
|
||||
const fullConfig = createdPipeline.pipeline.config;
|
||||
const botData = await httpClient.getBot(createdBotUuid);
|
||||
const existingBot = botData.bot;
|
||||
await httpClient.updateBot(createdBotUuid, {
|
||||
name: existingBot.name,
|
||||
description: existingBot.description,
|
||||
adapter: existingBot.adapter,
|
||||
adapter_config: existingBot.adapter_config,
|
||||
enable: existingBot.enable,
|
||||
use_pipeline_uuid: pipelineResp.uuid,
|
||||
});
|
||||
externalPipelineBound = true;
|
||||
}
|
||||
|
||||
const mergedConfig = {
|
||||
...fullConfig,
|
||||
ai: {
|
||||
...fullConfig.ai,
|
||||
runner: { runner: selectedRunner },
|
||||
[selectedRunner]: runnerConfig,
|
||||
},
|
||||
};
|
||||
|
||||
await httpClient.updatePipeline(pipelineResp.uuid, {
|
||||
name: `${botName} Pipeline`,
|
||||
description: botDescription || '',
|
||||
config: mergedConfig,
|
||||
});
|
||||
|
||||
// 3. Link pipeline to the bot created in Step 1
|
||||
const botData = await httpClient.getBot(createdBotUuid);
|
||||
const existingBot = botData.bot;
|
||||
await httpClient.updateBot(createdBotUuid, {
|
||||
name: existingBot.name,
|
||||
description: existingBot.description,
|
||||
adapter: existingBot.adapter,
|
||||
adapter_config: existingBot.adapter_config,
|
||||
enable: existingBot.enable,
|
||||
use_pipeline_uuid: pipelineResp.uuid,
|
||||
});
|
||||
|
||||
setCurrentStep(3);
|
||||
await completeWizard();
|
||||
if (aiChoice === 'own-model') {
|
||||
navigate(`/home/pipelines?id=${createdPipelineUuid}`, {
|
||||
replace: true,
|
||||
});
|
||||
} else {
|
||||
navigate('/home', { replace: true });
|
||||
}
|
||||
} catch (err) {
|
||||
if (externalPipelineUuid && !externalPipelineBound) {
|
||||
await httpClient.deletePipeline(externalPipelineUuid).catch(() => {});
|
||||
}
|
||||
const apiErr = err as { msg?: string };
|
||||
toast.error(
|
||||
t('wizard.createError') + (apiErr?.msg ? `: ${apiErr.msg}` : ''),
|
||||
@@ -483,12 +590,34 @@ export default function WizardPage() {
|
||||
}, [
|
||||
selectedRunner,
|
||||
createdBotUuid,
|
||||
createdPipelineUuid,
|
||||
aiChoice,
|
||||
botName,
|
||||
botDescription,
|
||||
runnerConfig,
|
||||
completeWizard,
|
||||
navigate,
|
||||
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 ----
|
||||
const [showSkipConfirm, setShowSkipConfirm] = useState(false);
|
||||
const [isSkipping, setIsSkipping] = useState(false);
|
||||
@@ -505,7 +634,9 @@ export default function WizardPage() {
|
||||
step: 0,
|
||||
selected_adapter: null,
|
||||
created_bot_uuid: null,
|
||||
created_pipeline_uuid: null,
|
||||
bot_saved: false,
|
||||
message_received: false,
|
||||
selected_runner: null,
|
||||
});
|
||||
systemInfo.wizard_progress = null;
|
||||
@@ -533,7 +664,6 @@ export default function WizardPage() {
|
||||
t('wizard.step.platform'),
|
||||
t('wizard.step.botConfig'),
|
||||
t('wizard.step.aiEngine'),
|
||||
t('wizard.step.done'),
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -548,7 +678,7 @@ export default function WizardPage() {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<LanguageSelector />
|
||||
{currentStep < 3 && (
|
||||
{currentStep < TOTAL_STEPS && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -633,6 +763,8 @@ export default function WizardPage() {
|
||||
createdBotUuid={createdBotUuid}
|
||||
isSavingBot={isSavingBot}
|
||||
botSaved={botSaved}
|
||||
messageReceived={messageReceived}
|
||||
onMessageReceived={handleMessageReceived}
|
||||
onSaveBot={handleSaveBot}
|
||||
webhookUrl={webhookUrl}
|
||||
extraWebhookUrl={extraWebhookUrl}
|
||||
@@ -641,18 +773,21 @@ export default function WizardPage() {
|
||||
{currentStep === 2 && (
|
||||
<StepAIEngine
|
||||
runnerOptions={runnerOptions}
|
||||
choice={aiChoice}
|
||||
onChoiceChange={setAiChoice}
|
||||
selected={selectedRunner}
|
||||
onSelect={handleSelectRunner}
|
||||
isLocalAccount={isLocalAccount}
|
||||
onSpaceAuth={handleSpaceAuth}
|
||||
runnerConfigItems={selectedRunnerConfigItems}
|
||||
runnerConfigValues={runnerConfig}
|
||||
onRunnerConfigChange={setRunnerConfig}
|
||||
/>
|
||||
)}
|
||||
{currentStep === 3 && <StepDone />}
|
||||
</div>
|
||||
|
||||
{/* Footer navigation */}
|
||||
{currentStep < 3 && (
|
||||
{currentStep < TOTAL_STEPS && (
|
||||
<div className="shrink-0 flex justify-between items-center px-4 sm:px-6 py-3 sm:py-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -682,12 +817,20 @@ export default function WizardPage() {
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleFinish}
|
||||
disabled={!canProceed() || isSubmitting}
|
||||
disabled={
|
||||
!canProceed() ||
|
||||
isSubmitting ||
|
||||
(aiChoice === 'external' && !selectedRunner)
|
||||
}
|
||||
>
|
||||
{isSubmitting && (
|
||||
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
|
||||
)}
|
||||
{t('wizard.finish')}
|
||||
{aiChoice === 'external'
|
||||
? t('wizard.aiEngine.createExternal')
|
||||
: aiChoice === 'own-model'
|
||||
? t('wizard.aiEngine.configurePipeline')
|
||||
: t('wizard.aiEngine.openWorkbench')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -828,6 +971,35 @@ function StepPlatform({
|
||||
// Step 1: Bot Configuration + Logs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function PageBotFloatingWidget({
|
||||
botUuid,
|
||||
title,
|
||||
}: {
|
||||
botUuid: string;
|
||||
title?: string;
|
||||
}) {
|
||||
useEffect(() => {
|
||||
const script = document.createElement('script');
|
||||
script.src = `${window.location.origin}/api/v1/embed/${botUuid}/widget.js`;
|
||||
script.dataset.title = title || 'LangBot';
|
||||
document.body.appendChild(script);
|
||||
|
||||
return () => {
|
||||
script.remove();
|
||||
const root = document.getElementById('langbot-widget-root') as
|
||||
| (HTMLElement & { langbotDestroy?: () => void })
|
||||
| null;
|
||||
if (root?.langbotDestroy) {
|
||||
root.langbotDestroy();
|
||||
} else {
|
||||
root?.remove();
|
||||
}
|
||||
};
|
||||
}, [botUuid, title]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function StepBotConfig({
|
||||
adapterConfigItems,
|
||||
adapterConfigValues,
|
||||
@@ -837,6 +1009,8 @@ function StepBotConfig({
|
||||
createdBotUuid,
|
||||
isSavingBot,
|
||||
botSaved,
|
||||
messageReceived,
|
||||
onMessageReceived,
|
||||
onSaveBot,
|
||||
webhookUrl,
|
||||
extraWebhookUrl,
|
||||
@@ -849,11 +1023,17 @@ function StepBotConfig({
|
||||
createdBotUuid: string | null;
|
||||
isSavingBot: boolean;
|
||||
botSaved: boolean;
|
||||
messageReceived: boolean;
|
||||
onMessageReceived: () => void;
|
||||
onSaveBot: () => void;
|
||||
webhookUrl: string;
|
||||
extraWebhookUrl: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [testMessage, setTestMessage] = useState(
|
||||
t('wizard.botConfig.httpTestDefaultMessage'),
|
||||
);
|
||||
const [isSendingTest, setIsSendingTest] = useState(false);
|
||||
|
||||
const adapterLabel = useMemo(() => {
|
||||
const a = adapters.find((ad) => ad.name === selectedAdapterName);
|
||||
@@ -868,8 +1048,42 @@ 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 (
|
||||
<div className="max-w-5xl mx-auto space-y-6">
|
||||
{selectedAdapterName === 'web_page_bot' && botSaved && createdBotUuid && (
|
||||
<PageBotFloatingWidget
|
||||
botUuid={createdBotUuid}
|
||||
title={
|
||||
typeof adapterConfigValues.title === 'string'
|
||||
? adapterConfigValues.title
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl font-semibold">{t('wizard.botConfig.title')}</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
@@ -877,6 +1091,104 @@ function StepBotConfig({
|
||||
</p>
|
||||
</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 === 'web_page_bot' ? (
|
||||
<MessageSquare 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 === 'web_page_bot'
|
||||
? t('wizard.botConfig.pageBotTestPrompt')
|
||||
: 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">
|
||||
{/* Left column: Adapter config form */}
|
||||
<div className="space-y-4">
|
||||
@@ -940,18 +1252,6 @@ function StepBotConfig({
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Bot saved indicator */}
|
||||
{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">
|
||||
{t('wizard.botConfig.botSaved')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right column: Bot logs */}
|
||||
@@ -968,6 +1268,7 @@ function StepBotConfig({
|
||||
botId={createdBotUuid}
|
||||
autoExpandImages
|
||||
hideToolbar
|
||||
onMessageReceived={onMessageReceived}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -983,15 +1284,25 @@ function StepBotConfig({
|
||||
|
||||
function StepAIEngine({
|
||||
runnerOptions,
|
||||
choice,
|
||||
onChoiceChange,
|
||||
selected,
|
||||
onSelect,
|
||||
isLocalAccount,
|
||||
onSpaceAuth,
|
||||
runnerConfigItems,
|
||||
runnerConfigValues,
|
||||
onRunnerConfigChange,
|
||||
}: {
|
||||
runnerOptions: { name: string; label: { en_US: string; zh_Hans: string } }[];
|
||||
choice: 'external' | 'own-model' | 'more-features' | null;
|
||||
onChoiceChange: (
|
||||
choice: 'external' | 'own-model' | 'more-features' | null,
|
||||
) => void;
|
||||
selected: string | null;
|
||||
onSelect: (name: string) => void;
|
||||
isLocalAccount: boolean;
|
||||
onSpaceAuth: () => void;
|
||||
runnerConfigItems: IDynamicFormItemSchema[];
|
||||
runnerConfigValues: Record<string, unknown>;
|
||||
onRunnerConfigChange: (v: Record<string, unknown>) => void;
|
||||
@@ -1011,6 +1322,63 @@ function StepAIEngine({
|
||||
return r ? extractI18nObject(r.label) : (selected ?? '');
|
||||
}, [runnerOptions, selected]);
|
||||
|
||||
const choices = [
|
||||
{
|
||||
id: 'external' as const,
|
||||
icon: Cable,
|
||||
title: t('wizard.aiEngine.externalTitle'),
|
||||
description: t('wizard.aiEngine.externalDescription'),
|
||||
},
|
||||
{
|
||||
id: 'own-model' as const,
|
||||
icon: Settings2,
|
||||
title: t('wizard.aiEngine.ownModelTitle'),
|
||||
description: t('wizard.aiEngine.ownModelDescription'),
|
||||
},
|
||||
{
|
||||
id: 'more-features' as const,
|
||||
icon: Blocks,
|
||||
title: t('wizard.aiEngine.moreFeaturesTitle'),
|
||||
description: t('wizard.aiEngine.moreFeaturesDescription'),
|
||||
},
|
||||
];
|
||||
|
||||
if (choice !== 'external') {
|
||||
return (
|
||||
<div className="space-y-6 max-w-4xl mx-auto">
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl font-semibold">
|
||||
{t('wizard.aiEngine.title')}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t('wizard.aiEngine.optionalDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{choices.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<Card
|
||||
key={item.id}
|
||||
className={cn(
|
||||
'cursor-pointer transition-all hover:border-primary/50',
|
||||
choice === item.id && 'ring-2 ring-primary',
|
||||
)}
|
||||
onClick={() => onChoiceChange(item.id)}
|
||||
>
|
||||
<CardHeader>
|
||||
<Icon className="size-6 text-primary" />
|
||||
<CardTitle className="text-base">{item.title}</CardTitle>
|
||||
<CardDescription>{item.description}</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Before any runner is selected: centered grid layout
|
||||
if (!selected) {
|
||||
return (
|
||||
@@ -1020,9 +1388,13 @@ function StepAIEngine({
|
||||
{t('wizard.aiEngine.title')}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t('wizard.aiEngine.description')}
|
||||
{t('wizard.aiEngine.runnerDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={() => onChoiceChange(null)}>
|
||||
<ArrowLeft className="size-4 mr-1.5" />
|
||||
{t('wizard.aiEngine.backToChoices')}
|
||||
</Button>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{runnerOptions.map((opt) => (
|
||||
<Card
|
||||
@@ -1059,6 +1431,16 @@ function StepAIEngine({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="self-start mb-3"
|
||||
onClick={() => onChoiceChange(null)}
|
||||
>
|
||||
<ArrowLeft className="size-4 mr-1.5" />
|
||||
{t('wizard.aiEngine.backToChoices')}
|
||||
</Button>
|
||||
|
||||
<div className="flex flex-col lg:flex-row lg:justify-center gap-6 lg:flex-1 lg:min-h-0 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
||||
{/* Left: runner list */}
|
||||
<div className="w-full lg:w-[280px] shrink-0 lg:overflow-y-auto lg:pr-3">
|
||||
@@ -1102,6 +1484,28 @@ function StepAIEngine({
|
||||
</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>
|
||||
|
||||
@@ -1132,100 +1536,3 @@ function StepAIEngine({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Step 3: Done
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function StepDone() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [particles] = useState(() =>
|
||||
Array.from({ length: 30 }, (_, i) => ({
|
||||
id: i,
|
||||
left: Math.random() * 100,
|
||||
delay: Math.random() * 2,
|
||||
duration: 2 + Math.random() * 2,
|
||||
size: 4 + Math.random() * 6,
|
||||
color: [
|
||||
'bg-purple-400',
|
||||
'bg-pink-400',
|
||||
'bg-orange-400',
|
||||
'bg-blue-400',
|
||||
'bg-green-400',
|
||||
'bg-yellow-400',
|
||||
][Math.floor(Math.random() * 6)],
|
||||
})),
|
||||
);
|
||||
|
||||
const [isCompleting, setIsCompleting] = useState(false);
|
||||
|
||||
const handleBack = useCallback(async () => {
|
||||
setIsCompleting(true);
|
||||
try {
|
||||
if (systemInfo.wizard_status === 'none') {
|
||||
await httpClient.updateWizardStatus('completed');
|
||||
systemInfo.wizard_status = 'completed';
|
||||
}
|
||||
// Always clear persisted progress so re-entering starts fresh
|
||||
await httpClient.saveWizardProgress({
|
||||
step: 0,
|
||||
selected_adapter: null,
|
||||
created_bot_uuid: null,
|
||||
bot_saved: false,
|
||||
selected_runner: null,
|
||||
});
|
||||
systemInfo.wizard_progress = null;
|
||||
} catch {
|
||||
toast.error(t('wizard.completeSaveError'));
|
||||
setIsCompleting(false);
|
||||
return;
|
||||
}
|
||||
setIsCompleting(false);
|
||||
navigate('/home/bots');
|
||||
}, [navigate, t]);
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col items-center justify-center h-full min-h-[400px]">
|
||||
{/* Confetti particles */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
{particles.map((p) => (
|
||||
<div
|
||||
key={p.id}
|
||||
className={cn('absolute rounded-full opacity-0', p.color)}
|
||||
style={{
|
||||
left: `${p.left}%`,
|
||||
width: p.size,
|
||||
height: p.size,
|
||||
animation: `wizardConfetti ${p.duration}s ease-out ${p.delay}s forwards`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<PartyPopper className="w-16 h-16 text-primary mb-4" />
|
||||
<h2 className="text-2xl font-bold">{t('wizard.done.title')}</h2>
|
||||
<p className="text-muted-foreground mt-2 text-center max-w-md">
|
||||
{t('wizard.done.description')}
|
||||
</p>
|
||||
<Button className="mt-6" onClick={handleBack} disabled={isCompleting}>
|
||||
{isCompleting && <Loader2 className="w-4 h-4 mr-1.5 animate-spin" />}
|
||||
{t('wizard.done.backToWorkbench')}
|
||||
</Button>
|
||||
|
||||
<style>{`
|
||||
@keyframes wizardConfetti {
|
||||
0% {
|
||||
transform: translateY(100vh) rotate(0deg);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-20vh) rotate(720deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -85,18 +85,18 @@ const enUS = {
|
||||
'Recommended: Use official stable model APIs and cloud services',
|
||||
loginLocal: 'Login with local account',
|
||||
loginWithPassword: 'Login with password',
|
||||
spaceLoginTitle: 'Login with LangBot Account',
|
||||
spaceLoginTitle: 'Login with Space',
|
||||
spaceLoginDescription:
|
||||
'Scan the QR code or visit the link below to authorize',
|
||||
spaceLoginUserCode: 'Your code',
|
||||
spaceLoginExpires: 'Code expires in {{seconds}} seconds',
|
||||
spaceLoginWaiting: 'Waiting for authorization...',
|
||||
spaceLoginSuccess: 'Authorization successful',
|
||||
spaceLoginFailed: 'LangBot Account login failed',
|
||||
spaceLoginFailed: 'Space login failed',
|
||||
spaceLoginExpired: 'Authorization code expired, please try again',
|
||||
spaceLoginCancel: 'Cancel',
|
||||
spaceLoginVisitLink: 'Visit link',
|
||||
spaceLoginProcessing: 'Logging in with LangBot Account',
|
||||
spaceLoginProcessing: 'Logging in with Space',
|
||||
spaceLoginProcessingDescription:
|
||||
'Please wait while we complete your login...',
|
||||
spaceLoginSuccessDescription: 'Redirecting to LangBot...',
|
||||
@@ -105,7 +105,7 @@ const enUS = {
|
||||
backToLogin: 'Back to Login',
|
||||
backToHome: 'Back to Home',
|
||||
spaceAccountCannotChangePassword:
|
||||
'LangBot Accounts cannot change password here',
|
||||
'Space accounts cannot change password here',
|
||||
theme: 'Theme',
|
||||
changePassword: 'Change Password',
|
||||
currentPassword: 'Current Password',
|
||||
@@ -254,9 +254,8 @@ const enUS = {
|
||||
llmModels: 'LLM Models',
|
||||
localProvider: 'Local',
|
||||
localProviderDescription: 'Models configured and managed locally',
|
||||
spaceProviderDescription: 'Models synced from your LangBot Account',
|
||||
spaceDisabledForLocalAccount:
|
||||
'Login with LangBot Account to use cloud models',
|
||||
spaceProviderDescription: 'Models synced from your Space account',
|
||||
spaceDisabledForLocalAccount: 'Login with Space to use cloud models',
|
||||
syncModels: 'Sync',
|
||||
syncSuccess: 'Sync complete: {{created}} created, {{updated}} updated',
|
||||
syncError: 'Sync failed: ',
|
||||
@@ -292,15 +291,15 @@ const enUS = {
|
||||
langbotModelsDescription: 'Cloud models powered by LangBot Space',
|
||||
credits: 'Credits',
|
||||
loginWithSpace: 'Login with LangBot Account',
|
||||
loginToUseModels: 'Login with LangBot Account to use cloud models',
|
||||
loginToUseModels: 'Login with Space to use cloud models',
|
||||
ownerMustBindSpace:
|
||||
'The Workspace owner must connect a LangBot Account for LangBot Models.',
|
||||
'The Workspace owner must connect Space for LangBot Models.',
|
||||
usesOwnerSpaceBilling:
|
||||
"Uses the Workspace owner's LangBot Account billing and credits.",
|
||||
"Uses the Workspace owner's Space billing and credits.",
|
||||
noModels: 'No models configured',
|
||||
langbotModels: 'LangBot Models',
|
||||
spaceTrialTooltip:
|
||||
'Free trial credits available! Login with LangBot Account to access cloud models with zero configuration.',
|
||||
'Free trial credits available! Login with Space to access cloud models with zero configuration.',
|
||||
unlockModels: 'Login to use',
|
||||
editProvider: 'Edit Provider',
|
||||
addProvider: 'Add Provider',
|
||||
@@ -1219,13 +1218,13 @@ const enUS = {
|
||||
adminAccountNote:
|
||||
'The account you use here will be set as the administrator account',
|
||||
register: 'Register',
|
||||
initWithSpace: 'Initialize with LangBot Account',
|
||||
initWithSpace: 'Initialize with Space',
|
||||
spaceRecommended:
|
||||
'Recommended: Use official stable model APIs and cloud services',
|
||||
spaceInfoTip1:
|
||||
'Space provides unified account authentication services without uploading any of your sensitive information.',
|
||||
spaceInfoTip2:
|
||||
'Logging in with a LangBot Account gives you access to LangBot Models and other cloud services, including free model call credits to help you get started quickly.',
|
||||
'Logging in with a Space account gives you access to LangBot Models and other cloud services, including free model call credits to help you get started quickly.',
|
||||
spaceInfoTip3:
|
||||
'Your login method does not affect other features. You can configure and use models from other sources at any time.',
|
||||
registerLocal: 'Register local account',
|
||||
@@ -1282,32 +1281,32 @@ const enUS = {
|
||||
passwordNotSet: 'Not Set',
|
||||
passwordSetDescription:
|
||||
'Password is set, you can login with email and password',
|
||||
spaceStatus: 'LangBot Account',
|
||||
spaceStatus: 'Space Account',
|
||||
spaceBound: 'Bound',
|
||||
spaceNotBound: 'Not Bound',
|
||||
spaceBoundDescription:
|
||||
'LangBot Account bound, official model APIs and cloud services available',
|
||||
bindSpace: 'Bind LangBot Account',
|
||||
'Space account bound, official model APIs and cloud services available',
|
||||
bindSpace: 'Bind Space Account',
|
||||
bindSpaceDescription: 'Bind to use official model APIs and cloud services',
|
||||
bindSpaceButton: 'Bind',
|
||||
bindSpaceConfirmTitle: 'Confirm Binding',
|
||||
bindSpaceConfirmDescription:
|
||||
'You are about to bind your local instance to a LangBot Account',
|
||||
'You are about to bind your local instance to a Space account',
|
||||
bindSpaceWarning:
|
||||
'After binding, your login email will be changed from {{localEmail}} to the LangBot Account email.',
|
||||
bindSpaceSuccess: 'LangBot Account bound successfully',
|
||||
bindSpaceFailed: 'Failed to bind LangBot Account',
|
||||
'After binding, your login email will be changed from {{localEmail}} to the Space account email.',
|
||||
bindSpaceSuccess: 'Space account bound successfully',
|
||||
bindSpaceFailed: 'Failed to bind Space account',
|
||||
bindSpaceInvalidState:
|
||||
'Invalid bind request. Please try again from account settings.',
|
||||
setPasswordHint: 'Set a password to login with email and password',
|
||||
spaceEmailMismatch:
|
||||
'The LangBot Account login email does not match the local account email.',
|
||||
'The Space login email does not match the local account email.',
|
||||
space_account_not_registeredTitle: 'Account not registered',
|
||||
space_account_not_registered:
|
||||
'No local account is registered for this LangBot Account email. Ask the Workspace owner for an invitation.',
|
||||
space_account_binding_requiredTitle: 'LangBot Account connection required',
|
||||
'No local account is registered for this Space email. Ask the Workspace owner for an invitation.',
|
||||
space_account_binding_requiredTitle: 'Space connection required',
|
||||
space_account_binding_required:
|
||||
'This local account must connect a LangBot Account from Account settings before using LangBot Account login.',
|
||||
'This local account must connect Space from Account settings before using Space login.',
|
||||
},
|
||||
workspace: {
|
||||
title: 'Workspace',
|
||||
@@ -1808,6 +1807,7 @@ const enUS = {
|
||||
botCreateSuccess: 'Bot created successfully!',
|
||||
botSaveSuccess: 'Bot configuration saved and enabled!',
|
||||
createError: 'Failed to create resources',
|
||||
spaceAuthError: 'Failed to initiate Space authorization',
|
||||
skipSaveError: 'Failed to save skip status. Please try again.',
|
||||
completeSaveError: 'Failed to save completion status. Please try again.',
|
||||
step: {
|
||||
@@ -1827,6 +1827,23 @@ const enUS = {
|
||||
resaveBot: 'Re-save Configuration',
|
||||
botSaved:
|
||||
'Bot configuration saved and enabled. Check the logs to verify the connection.',
|
||||
waitingForMessage:
|
||||
'The bot is enabled. Send it a message from your IM platform to continue.',
|
||||
messageReceived:
|
||||
'The bot received an IM message. You can continue to the next step.',
|
||||
pageBotTestPrompt:
|
||||
'Page Bot is enabled. Click the chat bubble in the lower-right corner and send a message to verify the full conversation flow.',
|
||||
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',
|
||||
logsDescription:
|
||||
'Monitor bot activity to verify the platform connection is working.',
|
||||
@@ -1835,6 +1852,28 @@ const enUS = {
|
||||
title: 'Select an AI Engine',
|
||||
description:
|
||||
"Choose the AI engine that will power your bot's intelligence.",
|
||||
optionalDescription:
|
||||
'This step is optional. Choose how you want to continue with the current agent.',
|
||||
externalTitle: 'Connect an External Agent',
|
||||
externalDescription:
|
||||
'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.',
|
||||
moreFeaturesTitle: 'Add More Agent Features',
|
||||
moreFeaturesDescription:
|
||||
'Open the workbench to add tools, knowledge, and other capabilities.',
|
||||
runnerDescription:
|
||||
'Select a runner for the external agent and configure its connection.',
|
||||
backToChoices: 'Back to options',
|
||||
createExternal: 'Create and Bind',
|
||||
configurePipeline: 'Configure Pipeline',
|
||||
openWorkbench: 'Open Workbench',
|
||||
},
|
||||
spaceBanner: {
|
||||
message:
|
||||
'Connect to LangBot Space for free trial model credits and zero-config instant setup!',
|
||||
action: 'Authorize with Space',
|
||||
},
|
||||
config: {
|
||||
botInfo: 'Bot Information',
|
||||
@@ -1916,9 +1955,6 @@ const enUS = {
|
||||
'Scan the QR code below with WeChat to authorize and automatically fill in the token',
|
||||
loginSuccess: 'Login successful! Token has been filled in',
|
||||
loginFailed: 'Login failed',
|
||||
connecting: 'Connecting to WeChat service...',
|
||||
waitingForScan: 'Waiting for scan',
|
||||
retry: 'Retry',
|
||||
},
|
||||
dingtalk: {
|
||||
createApp: 'One-Click Create DingTalk App',
|
||||
|
||||
@@ -88,19 +88,19 @@ const esES = {
|
||||
'Recomendado: Usa API de modelos oficiales estables y servicios en la nube',
|
||||
loginLocal: 'Iniciar sesión con cuenta local',
|
||||
loginWithPassword: 'Iniciar sesión con contraseña',
|
||||
spaceLoginTitle: 'Iniciar sesión con una cuenta de LangBot',
|
||||
spaceLoginTitle: 'Iniciar sesión con Space',
|
||||
spaceLoginDescription:
|
||||
'Escanea el código QR o visita el enlace para autorizar',
|
||||
spaceLoginUserCode: 'Tu código',
|
||||
spaceLoginExpires: 'El código expira en {{seconds}} segundos',
|
||||
spaceLoginWaiting: 'Esperando autorización...',
|
||||
spaceLoginSuccess: 'Autorización exitosa',
|
||||
spaceLoginFailed: 'Error de inicio de sesión con una cuenta de LangBot',
|
||||
spaceLoginFailed: 'Error de inicio de sesión con Space',
|
||||
spaceLoginExpired:
|
||||
'El código de autorización ha expirado, por favor inténtalo de nuevo',
|
||||
spaceLoginCancel: 'Cancelar',
|
||||
spaceLoginVisitLink: 'Visitar enlace',
|
||||
spaceLoginProcessing: 'Iniciando sesión con una cuenta de LangBot',
|
||||
spaceLoginProcessing: 'Iniciando sesión con Space',
|
||||
spaceLoginProcessingDescription:
|
||||
'Por favor espera mientras completamos tu inicio de sesión...',
|
||||
spaceLoginSuccessDescription: 'Redirigiendo a LangBot...',
|
||||
@@ -109,7 +109,7 @@ const esES = {
|
||||
backToLogin: 'Volver al inicio de sesión',
|
||||
backToHome: 'Volver al inicio',
|
||||
spaceAccountCannotChangePassword:
|
||||
'Las cuentas de LangBot no pueden cambiar la contraseña aquí',
|
||||
'Las cuentas de Space no pueden cambiar la contraseña aquí',
|
||||
theme: 'Tema',
|
||||
changePassword: 'Cambiar contraseña',
|
||||
currentPassword: 'Contraseña actual',
|
||||
@@ -220,19 +220,6 @@ const esES = {
|
||||
selectModelAbilities: 'Seleccionar capacidades del modelo',
|
||||
visionAbility: 'Capacidad de visión',
|
||||
functionCallAbility: 'Llamada a funciones',
|
||||
reasoningAbility: 'Razonamiento',
|
||||
reasoningLevel: 'Nivel de razonamiento',
|
||||
reasoningLevels: {
|
||||
providerDefault: 'Predeterminado del proveedor',
|
||||
disabled: 'Desactivado',
|
||||
enabled: 'Activado',
|
||||
minimal: 'Mínimo',
|
||||
low: 'Bajo',
|
||||
medium: 'Medio',
|
||||
high: 'Alto',
|
||||
xhigh: 'Extra alto',
|
||||
max: 'Máximo',
|
||||
},
|
||||
contextLength: 'Ventana de contexto',
|
||||
contextLengthPlaceholder: 'Desconocido',
|
||||
contextLengthInvalid: 'La ventana de contexto debe ser un entero positivo',
|
||||
@@ -261,10 +248,9 @@ const esES = {
|
||||
llmModels: 'Modelos LLM',
|
||||
localProvider: 'Local',
|
||||
localProviderDescription: 'Modelos configurados y gestionados localmente',
|
||||
spaceProviderDescription:
|
||||
'Modelos sincronizados desde tu cuenta de LangBot',
|
||||
spaceProviderDescription: 'Modelos sincronizados desde tu cuenta de Space',
|
||||
spaceDisabledForLocalAccount:
|
||||
'Inicia sesión con una cuenta de LangBot para usar modelos en la nube',
|
||||
'Inicia sesión con Space para usar modelos en la nube',
|
||||
syncModels: 'Sincronizar',
|
||||
syncSuccess:
|
||||
'Sincronización completa: {{created}} creados, {{updated}} actualizados',
|
||||
@@ -303,12 +289,11 @@ const esES = {
|
||||
langbotModelsDescription: 'Modelos en la nube impulsados por LangBot Space',
|
||||
credits: 'Créditos',
|
||||
loginWithSpace: 'Iniciar sesión con una cuenta de LangBot',
|
||||
loginToUseModels:
|
||||
'Inicia sesión con una cuenta de LangBot para usar modelos en la nube',
|
||||
loginToUseModels: 'Inicia sesión con Space para usar modelos en la nube',
|
||||
noModels: 'No hay modelos configurados',
|
||||
langbotModels: 'Modelos LangBot',
|
||||
spaceTrialTooltip:
|
||||
'¡Créditos de prueba gratuitos disponibles! Inicia sesión con una cuenta de LangBot para acceder a modelos en la nube sin configuración.',
|
||||
'¡Créditos de prueba gratuitos disponibles! Inicia sesión con Space para acceder a modelos en la nube sin configuración.',
|
||||
unlockModels: 'Inicia sesión para usar',
|
||||
editProvider: 'Editar proveedor',
|
||||
addProvider: 'Añadir proveedor',
|
||||
@@ -343,9 +328,9 @@ const esES = {
|
||||
},
|
||||
|
||||
ownerMustBindSpace:
|
||||
'The Workspace owner must connect a LangBot Account for LangBot Models.',
|
||||
'The Workspace owner must connect Space for LangBot Models.',
|
||||
usesOwnerSpaceBilling:
|
||||
"Uses the Workspace owner's LangBot Account billing and credits.",
|
||||
"Uses the Workspace owner's Space billing and credits.",
|
||||
},
|
||||
bots: {
|
||||
title: 'Bots',
|
||||
@@ -1253,13 +1238,13 @@ const esES = {
|
||||
adminAccountNote:
|
||||
'La cuenta que uses aquí se establecerá como cuenta de administrador',
|
||||
register: 'Registrarse',
|
||||
initWithSpace: 'Inicializar con una cuenta de LangBot',
|
||||
initWithSpace: 'Inicializar con Space',
|
||||
spaceRecommended:
|
||||
'Recomendado: Usa API de modelos oficiales estables y servicios en la nube',
|
||||
spaceInfoTip1:
|
||||
'Space proporciona servicios de autenticación unificada de cuentas sin subir ninguna de tu información sensible.',
|
||||
spaceInfoTip2:
|
||||
'Iniciar sesión con una cuenta de LangBot te da acceso a los modelos de LangBot y otros servicios en la nube, incluyendo créditos gratuitos de llamadas a modelos para ayudarte a comenzar rápidamente.',
|
||||
'Iniciar sesión con una cuenta de Space te da acceso a los modelos de LangBot y otros servicios en la nube, incluyendo créditos gratuitos de llamadas a modelos para ayudarte a comenzar rápidamente.',
|
||||
spaceInfoTip3:
|
||||
'Tu método de inicio de sesión no afecta otras funciones. Puedes configurar y usar modelos de otras fuentes en cualquier momento.',
|
||||
registerLocal: 'Registrar cuenta local',
|
||||
@@ -1317,35 +1302,35 @@ const esES = {
|
||||
passwordNotSet: 'No establecida',
|
||||
passwordSetDescription:
|
||||
'La contraseña está establecida, puedes iniciar sesión con correo y contraseña',
|
||||
spaceStatus: 'Cuenta de LangBot',
|
||||
spaceStatus: 'Cuenta de Space',
|
||||
spaceBound: 'Vinculada',
|
||||
spaceNotBound: 'No vinculada',
|
||||
spaceBoundDescription:
|
||||
'Cuenta de LangBot vinculada, API de modelos oficiales y servicios en la nube disponibles',
|
||||
bindSpace: 'Vincular cuenta de LangBot',
|
||||
'Cuenta de Space vinculada, API de modelos oficiales y servicios en la nube disponibles',
|
||||
bindSpace: 'Vincular cuenta de Space',
|
||||
bindSpaceDescription:
|
||||
'Vincular para usar API de modelos oficiales y servicios en la nube',
|
||||
bindSpaceButton: 'Vincular',
|
||||
bindSpaceConfirmTitle: 'Confirmar vinculación',
|
||||
bindSpaceConfirmDescription:
|
||||
'Estás a punto de vincular tu instancia local a una cuenta de LangBot',
|
||||
'Estás a punto de vincular tu instancia local a una cuenta de Space',
|
||||
bindSpaceWarning:
|
||||
'Después de vincular, tu correo de inicio de sesión se cambiará de {{localEmail}} al correo de la cuenta de LangBot.',
|
||||
bindSpaceSuccess: 'Cuenta de LangBot vinculada correctamente',
|
||||
bindSpaceFailed: 'Error al vincular la cuenta de LangBot',
|
||||
'Después de vincular, tu correo de inicio de sesión se cambiará de {{localEmail}} al correo de la cuenta de Space.',
|
||||
bindSpaceSuccess: 'Cuenta de Space vinculada correctamente',
|
||||
bindSpaceFailed: 'Error al vincular la cuenta de Space',
|
||||
bindSpaceInvalidState:
|
||||
'Solicitud de vinculación no válida. Por favor, inténtalo de nuevo desde la configuración de la cuenta.',
|
||||
setPasswordHint:
|
||||
'Establece una contraseña para iniciar sesión con correo y contraseña',
|
||||
spaceEmailMismatch:
|
||||
'El correo de la cuenta de LangBot no coincide con el correo de la cuenta local',
|
||||
'El correo de inicio de sesión de Space no coincide con el correo de la cuenta local',
|
||||
|
||||
space_account_not_registeredTitle: 'Account not registered',
|
||||
space_account_not_registered:
|
||||
'No local account is registered for this LangBot Account email. Ask the Workspace owner for an invitation.',
|
||||
space_account_binding_requiredTitle: 'LangBot Account connection required',
|
||||
'No local account is registered for this Space email. Ask the Workspace owner for an invitation.',
|
||||
space_account_binding_requiredTitle: 'Space connection required',
|
||||
space_account_binding_required:
|
||||
'This local account must connect a LangBot Account from Account settings before using LangBot Account login.',
|
||||
'This local account must connect Space from Account settings before using Space login.',
|
||||
},
|
||||
monitoring: {
|
||||
title: 'Panel de control',
|
||||
@@ -1674,6 +1659,7 @@ const esES = {
|
||||
botCreateSuccess: '¡Bot creado correctamente!',
|
||||
botSaveSuccess: '¡Configuración del Bot guardada y activada!',
|
||||
createError: 'Error al crear los recursos',
|
||||
spaceAuthError: 'Error al iniciar la autorización de Space',
|
||||
skipSaveError:
|
||||
'Error al guardar el estado de omisión. Por favor, inténtalo de nuevo.',
|
||||
completeSaveError:
|
||||
@@ -1706,6 +1692,11 @@ const esES = {
|
||||
description:
|
||||
'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: {
|
||||
botInfo: 'Información del Bot',
|
||||
botNamePlaceholder: 'Introduce el nombre del Bot',
|
||||
@@ -1756,9 +1747,6 @@ const esES = {
|
||||
loginSuccess:
|
||||
'¡Inicio de sesión correcto! El token se ha rellenado automáticamente',
|
||||
loginFailed: 'Error al iniciar sesión',
|
||||
connecting: 'Conectando con el servicio de WeChat...',
|
||||
waitingForScan: 'Esperando escaneo',
|
||||
retry: 'Reintentar',
|
||||
},
|
||||
dingtalk: {
|
||||
createApp: 'Crear aplicación de DingTalk con un clic',
|
||||
|
||||
@@ -86,19 +86,19 @@ const jaJP = {
|
||||
'おすすめ:公式の安定したモデル API とクラウドサービスを利用',
|
||||
loginLocal: 'ローカルアカウントでログイン',
|
||||
loginWithPassword: 'パスワードでログイン',
|
||||
spaceLoginTitle: 'LangBot アカウントでログイン',
|
||||
spaceLoginTitle: 'Space でログイン',
|
||||
spaceLoginDescription:
|
||||
'QRコードをスキャンするか、下のリンクにアクセスして認証してください',
|
||||
spaceLoginUserCode: '認証コード',
|
||||
spaceLoginExpires: 'コードは {{seconds}} 秒後に期限切れになります',
|
||||
spaceLoginWaiting: '認証を待っています...',
|
||||
spaceLoginSuccess: '認証に成功しました',
|
||||
spaceLoginFailed: 'LangBot アカウントログインに失敗しました',
|
||||
spaceLoginFailed: 'Space ログインに失敗しました',
|
||||
spaceLoginExpired:
|
||||
'認証コードの有効期限が切れました。もう一度お試しください',
|
||||
spaceLoginCancel: 'キャンセル',
|
||||
spaceLoginVisitLink: 'リンクにアクセス',
|
||||
spaceLoginProcessing: 'LangBot アカウントでログイン中',
|
||||
spaceLoginProcessing: 'Space でログイン中',
|
||||
spaceLoginProcessingDescription:
|
||||
'ログインを完了しています。しばらくお待ちください...',
|
||||
spaceLoginSuccessDescription: 'LangBot にリダイレクト中...',
|
||||
@@ -107,7 +107,7 @@ const jaJP = {
|
||||
backToLogin: 'ログインに戻る',
|
||||
backToHome: 'ホームに戻る',
|
||||
spaceAccountCannotChangePassword:
|
||||
'LangBot アカウントはここでパスワードを変更できません',
|
||||
'Space アカウントはここでパスワードを変更できません',
|
||||
theme: 'テーマ',
|
||||
changePassword: 'パスワードを変更',
|
||||
currentPassword: '現在のパスワード',
|
||||
@@ -257,9 +257,8 @@ const jaJP = {
|
||||
llmModels: 'LLM モデル',
|
||||
localProvider: 'ローカル',
|
||||
localProviderDescription: 'ローカルで設定・管理されているモデル',
|
||||
spaceProviderDescription: 'LangBot アカウントから同期されたモデル',
|
||||
spaceDisabledForLocalAccount:
|
||||
'LangBot アカウントでログインしてクラウドモデルを使用',
|
||||
spaceProviderDescription: 'Space アカウントから同期されたモデル',
|
||||
spaceDisabledForLocalAccount: 'Space でログインしてクラウドモデルを使用',
|
||||
syncModels: '同期',
|
||||
syncSuccess: '同期完了:{{created}} 件作成、{{updated}} 件更新',
|
||||
syncError: '同期に失敗しました:',
|
||||
@@ -297,15 +296,15 @@ const jaJP = {
|
||||
langbotModelsDescription: 'LangBot Space が提供するクラウドモデル',
|
||||
credits: 'クレジット',
|
||||
loginWithSpace: 'LangBot アカウントでログイン',
|
||||
loginToUseModels: 'LangBot アカウントでログインしてクラウドモデルを使用',
|
||||
loginToUseModels: 'Space でログインしてクラウドモデルを使用',
|
||||
ownerMustBindSpace:
|
||||
'LangBot モデルを使うにはワークスペース所有者が LangBot アカウントを連携する必要があります。',
|
||||
'LangBot モデルを使うにはワークスペース所有者が Space を連携する必要があります。',
|
||||
usesOwnerSpaceBilling:
|
||||
'ワークスペース所有者の LangBot アカウント課金とクレジットを使用します。',
|
||||
'ワークスペース所有者の Space 課金とクレジットを使用します。',
|
||||
noModels: 'モデルがありません',
|
||||
langbotModels: 'LangBot モデル',
|
||||
spaceTrialTooltip:
|
||||
'無料トライアルクレジットが利用可能!LangBot アカウントでログインして、設定不要でクラウドモデルを使用できます。',
|
||||
'無料トライアルクレジットが利用可能!Space でログインして、設定不要でクラウドモデルを使用できます。',
|
||||
unlockModels: 'ログインして使用',
|
||||
editProvider: 'プロバイダーを編集',
|
||||
addProvider: 'プロバイダーを追加',
|
||||
@@ -1224,13 +1223,13 @@ const jaJP = {
|
||||
adminAccountNote:
|
||||
'ここで初期化されたアカウントは管理者アカウントとして使用されます',
|
||||
register: '登録',
|
||||
initWithSpace: 'LangBot アカウントで初期化',
|
||||
initWithSpace: 'Space で初期化',
|
||||
spaceRecommended:
|
||||
'おすすめ:公式の安定したモデル API とクラウドサービスを利用',
|
||||
spaceInfoTip1:
|
||||
'Space は統一されたアカウント認証サービスを提供し、機密情報をアップロードすることはありません。',
|
||||
spaceInfoTip2:
|
||||
'LangBot アカウントでログインすると、LangBot Models などのクラウドサービスを利用でき、無料のモデル呼び出しクレジットで迅速に開始できます。',
|
||||
'Space アカウントでログインすると、LangBot Models などのクラウドサービスを利用でき、無料のモデル呼び出しクレジットで迅速に開始できます。',
|
||||
spaceInfoTip3:
|
||||
'ログイン方法は他の機能に影響しません。いつでも他のソースからモデルを設定して使用できます。',
|
||||
registerLocal: 'ローカルアカウントを登録',
|
||||
@@ -1287,33 +1286,33 @@ const jaJP = {
|
||||
passwordNotSet: '未設定',
|
||||
passwordSetDescription:
|
||||
'パスワードが設定されています。メールとパスワードでログインできます',
|
||||
spaceStatus: 'LangBot アカウント',
|
||||
spaceStatus: 'Space アカウント',
|
||||
spaceBound: '連携済み',
|
||||
spaceNotBound: '未連携',
|
||||
spaceBoundDescription:
|
||||
'LangBot アカウントと連携済み、公式モデル API とクラウドサービスが利用可能',
|
||||
bindSpace: 'LangBot アカウントを連携',
|
||||
'Space アカウントと連携済み、公式モデル API とクラウドサービスが利用可能',
|
||||
bindSpace: 'Space アカウントを連携',
|
||||
bindSpaceDescription: '連携して公式モデル API とクラウドサービスを利用',
|
||||
bindSpaceButton: '連携',
|
||||
bindSpaceConfirmTitle: '連携を確認',
|
||||
bindSpaceConfirmDescription:
|
||||
'ローカルインスタンスを LangBot アカウントに連携しようとしています',
|
||||
'ローカルインスタンスを Space アカウントに連携しようとしています',
|
||||
bindSpaceWarning:
|
||||
'連携後、ログインメールアドレスは {{localEmail}} から LangBot アカウントのメールアドレスに変更されます。',
|
||||
bindSpaceSuccess: 'LangBot アカウントの連携に成功しました',
|
||||
bindSpaceFailed: 'LangBot アカウントの連携に失敗しました',
|
||||
'連携後、ログインメールアドレスは {{localEmail}} から Space アカウントのメールアドレスに変更されます。',
|
||||
bindSpaceSuccess: 'Space アカウントの連携に成功しました',
|
||||
bindSpaceFailed: 'Space アカウントの連携に失敗しました',
|
||||
bindSpaceInvalidState:
|
||||
'無効な連携リクエストです。アカウント設定から再度お試しください。',
|
||||
setPasswordHint:
|
||||
'パスワードを設定するとメールとパスワードでログインできます',
|
||||
spaceEmailMismatch:
|
||||
'LangBot アカウントのメールアドレスがローカルアカウントのメールアドレスと一致しません',
|
||||
'Spaceログインのメールアドレスがローカルアカウントのメールアドレスと一致しません',
|
||||
space_account_not_registeredTitle: 'アカウントが登録されていません',
|
||||
space_account_not_registered:
|
||||
'この LangBot アカウントのメールアドレスのローカルアカウントはありません。ワークスペース所有者に招待を依頼してください。',
|
||||
space_account_binding_requiredTitle: 'LangBot アカウントの連携が必要です',
|
||||
'この Space メールアドレスのローカルアカウントはありません。ワークスペース所有者に招待を依頼してください。',
|
||||
space_account_binding_requiredTitle: 'Space の連携が必要です',
|
||||
space_account_binding_required:
|
||||
'LangBot アカウントログインを使用する前に、アカウント設定でこのローカルアカウントを LangBot アカウントに連携してください。',
|
||||
'Space ログインを使用する前に、アカウント設定でこのローカルアカウントを Space に連携してください。',
|
||||
},
|
||||
workspace: {
|
||||
title: 'ワークスペース',
|
||||
@@ -1723,6 +1722,7 @@ const jaJP = {
|
||||
botCreateSuccess: 'ボットが正常に作成されました!',
|
||||
botSaveSuccess: 'ボット設定が保存され、有効になりました!',
|
||||
createError: 'リソースの作成に失敗しました',
|
||||
spaceAuthError: 'Space 認証の開始に失敗しました',
|
||||
skipSaveError: 'スキップ状態の保存に失敗しました。もう一度お試しください。',
|
||||
completeSaveError: '完了状態の保存に失敗しました。もう一度お試しください。',
|
||||
step: {
|
||||
@@ -1744,6 +1744,23 @@ const jaJP = {
|
||||
resaveBot: '設定を再保存',
|
||||
botSaved:
|
||||
'ボット設定が保存され、有効になりました。ログを確認して接続を検証してください。',
|
||||
waitingForMessage:
|
||||
'ボットが有効になりました。続行するには IM からメッセージを送信してください。',
|
||||
messageReceived:
|
||||
'ボットが IM メッセージを受信しました。次のステップに進めます。',
|
||||
pageBotTestPrompt:
|
||||
'ページボットが有効になりました。右下のチャットバブルをクリックしてメッセージを送信し、会話フロー全体を確認してください。',
|
||||
webhookTestPrompt:
|
||||
'コールバック URL の準備ができました。外部プラットフォームに設定し、ボットへ実際のメッセージを送信してください。',
|
||||
httpTestPrompt:
|
||||
'HTTP Bot が有効になりました。実際の受信メッセージを送信して接続を確認できます。',
|
||||
httpTestDefaultMessage: 'こんにちは。これは接続テストメッセージです。',
|
||||
sendHttpTest: 'テストメッセージを送信',
|
||||
httpTestAccepted:
|
||||
'テストメッセージを受け付けました。まもなくログに表示されます。',
|
||||
httpTestMissingSecret:
|
||||
'受信署名シークレットを入力し、先に設定を保存してください。',
|
||||
httpTestFailed: 'テストメッセージの送信に失敗しました:{{error}}',
|
||||
logsTitle: 'ボットログ',
|
||||
logsDescription:
|
||||
'ボットの活動を監視して、プラットフォーム接続が正常に動作していることを確認します。',
|
||||
@@ -1752,6 +1769,27 @@ const jaJP = {
|
||||
title: 'AIエンジンを選択',
|
||||
description:
|
||||
'ボットのインテリジェンスを駆動するAIエンジンを選択してください。',
|
||||
optionalDescription:
|
||||
'このステップは任意です。現在の Agent をどのように設定するか選択してください。',
|
||||
externalTitle: '外部プラットフォームの Agent を接続',
|
||||
externalDescription:
|
||||
'Dify、n8n、Coze などを接続し、ボットのパイプラインを置き換えます。',
|
||||
ownModelTitle: '自分のモデルを使用',
|
||||
ownModelDescription:
|
||||
'現在の Local Agent パイプラインを開き、自分のモデルを設定します。',
|
||||
moreFeaturesTitle: 'Agent に機能を追加',
|
||||
moreFeaturesDescription:
|
||||
'ワークベンチを開き、ツールやナレッジなどの機能を追加します。',
|
||||
runnerDescription: '外部 Agent の Runner を選択し、接続を設定します。',
|
||||
backToChoices: '選択肢に戻る',
|
||||
createExternal: '作成して関連付ける',
|
||||
configurePipeline: 'パイプラインを設定',
|
||||
openWorkbench: 'ワークベンチを開く',
|
||||
},
|
||||
spaceBanner: {
|
||||
message:
|
||||
'LangBot Spaceに接続して、無料トライアルモデルクレジットとゼロ設定の即時セットアップを入手!',
|
||||
action: 'Spaceで認証',
|
||||
},
|
||||
config: {
|
||||
botInfo: 'ボット情報',
|
||||
@@ -1832,9 +1870,6 @@ const jaJP = {
|
||||
scanQRCode: '以下のQRコードをWeChatでスキャンし、トークンを自動入力',
|
||||
loginSuccess: 'ログイン成功!トークンが自動入力されました',
|
||||
loginFailed: 'ログイン失敗',
|
||||
connecting: 'WeChatサービスに接続中...',
|
||||
waitingForScan: 'スキャン待ち',
|
||||
retry: '再試行',
|
||||
},
|
||||
dingtalk: {
|
||||
createApp: 'ワンクリックでDingTalkアプリ作成',
|
||||
|
||||
@@ -85,18 +85,18 @@ const ruRU = {
|
||||
'Рекомендуется: Используйте официальные стабильные API моделей и облачные сервисы',
|
||||
loginLocal: 'Войти с локальной учётной записью',
|
||||
loginWithPassword: 'Войти с паролем',
|
||||
spaceLoginTitle: 'Войти с аккаунтом LangBot',
|
||||
spaceLoginTitle: 'Войти через Space',
|
||||
spaceLoginDescription:
|
||||
'Отсканируйте QR-код или перейдите по ссылке ниже для авторизации',
|
||||
spaceLoginUserCode: 'Ваш код',
|
||||
spaceLoginExpires: 'Код истекает через {{seconds}} секунд',
|
||||
spaceLoginWaiting: 'Ожидание авторизации...',
|
||||
spaceLoginSuccess: 'Авторизация успешна',
|
||||
spaceLoginFailed: 'Ошибка входа с аккаунтом LangBot',
|
||||
spaceLoginFailed: 'Ошибка входа через Space',
|
||||
spaceLoginExpired: 'Код авторизации истёк, попробуйте снова',
|
||||
spaceLoginCancel: 'Отмена',
|
||||
spaceLoginVisitLink: 'Перейти по ссылке',
|
||||
spaceLoginProcessing: 'Вход с аккаунтом LangBot',
|
||||
spaceLoginProcessing: 'Вход через Space',
|
||||
spaceLoginProcessingDescription:
|
||||
'Пожалуйста, подождите, пока мы завершим вход...',
|
||||
spaceLoginSuccessDescription: 'Перенаправление в LangBot...',
|
||||
@@ -105,7 +105,7 @@ const ruRU = {
|
||||
backToLogin: 'Вернуться к входу',
|
||||
backToHome: 'На главную',
|
||||
spaceAccountCannotChangePassword:
|
||||
'Для аккаунтов LangBot невозможно изменить пароль здесь',
|
||||
'Для аккаунтов Space невозможно изменить пароль здесь',
|
||||
theme: 'Тема',
|
||||
changePassword: 'Изменить пароль',
|
||||
currentPassword: 'Текущий пароль',
|
||||
@@ -217,19 +217,6 @@ const ruRU = {
|
||||
selectModelAbilities: 'Выберите возможности модели',
|
||||
visionAbility: 'Распознавание изображений',
|
||||
functionCallAbility: 'Вызов функций',
|
||||
reasoningAbility: 'Рассуждение',
|
||||
reasoningLevel: 'Уровень рассуждений',
|
||||
reasoningLevels: {
|
||||
providerDefault: 'По умолчанию провайдера',
|
||||
disabled: 'Выключено',
|
||||
enabled: 'Включено',
|
||||
minimal: 'Минимальный',
|
||||
low: 'Низкий',
|
||||
medium: 'Средний',
|
||||
high: 'Высокий',
|
||||
xhigh: 'Очень высокий',
|
||||
max: 'Максимальный',
|
||||
},
|
||||
contextLength: 'Контекстное окно',
|
||||
contextLengthPlaceholder: 'Неизвестно',
|
||||
contextLengthInvalid:
|
||||
@@ -259,9 +246,9 @@ const ruRU = {
|
||||
localProvider: 'Локальный',
|
||||
localProviderDescription: 'Модели, настроенные и управляемые локально',
|
||||
spaceProviderDescription:
|
||||
'Модели, синхронизированные из вашего аккаунта LangBot',
|
||||
'Модели, синхронизированные из вашего аккаунта Space',
|
||||
spaceDisabledForLocalAccount:
|
||||
'Войдите с аккаунтом LangBot, чтобы использовать облачные модели',
|
||||
'Войдите через Space, чтобы использовать облачные модели',
|
||||
syncModels: 'Синхронизировать',
|
||||
syncSuccess:
|
||||
'Синхронизация завершена: {{created}} создано, {{updated}} обновлено',
|
||||
@@ -300,12 +287,11 @@ const ruRU = {
|
||||
langbotModelsDescription: 'Облачные модели на базе LangBot Space',
|
||||
credits: 'Кредиты',
|
||||
loginWithSpace: 'Войти с аккаунтом LangBot',
|
||||
loginToUseModels:
|
||||
'Войдите с аккаунтом LangBot, чтобы использовать облачные модели',
|
||||
loginToUseModels: 'Войдите через Space, чтобы использовать облачные модели',
|
||||
noModels: 'Модели не настроены',
|
||||
langbotModels: 'Модели LangBot',
|
||||
spaceTrialTooltip:
|
||||
'Доступны бесплатные пробные кредиты! Войдите с аккаунтом LangBot, чтобы получить доступ к облачным моделям без настройки.',
|
||||
'Доступны бесплатные пробные кредиты! Войдите через Space, чтобы получить доступ к облачным моделям без настройки.',
|
||||
unlockModels: 'Войдите для использования',
|
||||
editProvider: 'Редактировать провайдера',
|
||||
addProvider: 'Добавить провайдера',
|
||||
@@ -341,9 +327,9 @@ const ruRU = {
|
||||
},
|
||||
|
||||
ownerMustBindSpace:
|
||||
'The Workspace owner must connect a LangBot Account for LangBot Models.',
|
||||
'The Workspace owner must connect Space for LangBot Models.',
|
||||
usesOwnerSpaceBilling:
|
||||
"Uses the Workspace owner's LangBot Account billing and credits.",
|
||||
"Uses the Workspace owner's Space billing and credits.",
|
||||
},
|
||||
bots: {
|
||||
title: 'Боты',
|
||||
@@ -1230,13 +1216,13 @@ const ruRU = {
|
||||
adminAccountNote:
|
||||
'Указанная учётная запись будет настроена как администратор',
|
||||
register: 'Регистрация',
|
||||
initWithSpace: 'Инициализация с аккаунтом LangBot',
|
||||
initWithSpace: 'Инициализация через Space',
|
||||
spaceRecommended:
|
||||
'Рекомендуется: Используйте официальные стабильные API моделей и облачные сервисы',
|
||||
spaceInfoTip1:
|
||||
'Space предоставляет единую службу аутентификации без загрузки конфиденциальной информации.',
|
||||
spaceInfoTip2:
|
||||
'Вход с аккаунтом LangBot даёт доступ к моделям LangBot и облачным сервисам, включая бесплатные кредиты для быстрого старта.',
|
||||
'Вход через Space даёт доступ к моделям LangBot и облачным сервисам, включая бесплатные кредиты для быстрого старта.',
|
||||
spaceInfoTip3:
|
||||
'Способ входа не влияет на другие функции. Вы можете настроить модели из других источников в любое время.',
|
||||
registerLocal: 'Зарегистрировать локальную учётную запись',
|
||||
@@ -1292,34 +1278,34 @@ const ruRU = {
|
||||
passwordNotSet: 'Не установлен',
|
||||
passwordSetDescription:
|
||||
'Пароль установлен, вы можете входить с email и паролем',
|
||||
spaceStatus: 'Аккаунт LangBot',
|
||||
spaceStatus: 'Аккаунт Space',
|
||||
spaceBound: 'Привязан',
|
||||
spaceNotBound: 'Не привязан',
|
||||
spaceBoundDescription:
|
||||
'Аккаунт LangBot привязан, доступны официальные API моделей и облачные сервисы',
|
||||
bindSpace: 'Привязать аккаунт LangBot',
|
||||
'Аккаунт Space привязан, доступны официальные API моделей и облачные сервисы',
|
||||
bindSpace: 'Привязать аккаунт Space',
|
||||
bindSpaceDescription:
|
||||
'Привяжите для использования официальных API моделей и облачных сервисов',
|
||||
bindSpaceButton: 'Привязать',
|
||||
bindSpaceConfirmTitle: 'Подтверждение привязки',
|
||||
bindSpaceConfirmDescription:
|
||||
'Вы собираетесь привязать локальный экземпляр к аккаунту LangBot',
|
||||
'Вы собираетесь привязать локальный экземпляр к аккаунту Space',
|
||||
bindSpaceWarning:
|
||||
'После привязки ваш email для входа будет изменён с {{localEmail}} на email аккаунта LangBot.',
|
||||
bindSpaceSuccess: 'Аккаунт LangBot успешно привязан',
|
||||
bindSpaceFailed: 'Не удалось привязать аккаунт LangBot',
|
||||
'После привязки ваш email для входа будет изменён с {{localEmail}} на email аккаунта Space.',
|
||||
bindSpaceSuccess: 'Аккаунт Space успешно привязан',
|
||||
bindSpaceFailed: 'Не удалось привязать аккаунт Space',
|
||||
bindSpaceInvalidState:
|
||||
'Недействительный запрос привязки. Повторите попытку из настроек аккаунта.',
|
||||
setPasswordHint: 'Установите пароль для входа с email и паролем',
|
||||
spaceEmailMismatch:
|
||||
'Email входа с аккаунтом LangBot не совпадает с email локальной учётной записи',
|
||||
'Email входа через Space не совпадает с email локальной учётной записи',
|
||||
|
||||
space_account_not_registeredTitle: 'Account not registered',
|
||||
space_account_not_registered:
|
||||
'No local account is registered for this LangBot Account email. Ask the Workspace owner for an invitation.',
|
||||
space_account_binding_requiredTitle: 'LangBot Account connection required',
|
||||
'No local account is registered for this Space email. Ask the Workspace owner for an invitation.',
|
||||
space_account_binding_requiredTitle: 'Space connection required',
|
||||
space_account_binding_required:
|
||||
'This local account must connect a LangBot Account from Account settings before using LangBot Account login.',
|
||||
'This local account must connect Space from Account settings before using Space login.',
|
||||
},
|
||||
monitoring: {
|
||||
title: 'Мониторинг',
|
||||
@@ -1646,6 +1632,7 @@ const ruRU = {
|
||||
botCreateSuccess: 'Бот успешно создан!',
|
||||
botSaveSuccess: 'Конфигурация бота сохранена и включена!',
|
||||
createError: 'Не удалось создать ресурсы',
|
||||
spaceAuthError: 'Не удалось инициировать авторизацию через Space',
|
||||
skipSaveError: 'Не удалось сохранить статус пропуска. Повторите попытку.',
|
||||
completeSaveError:
|
||||
'Не удалось сохранить статус завершения. Повторите попытку.',
|
||||
@@ -1675,6 +1662,11 @@ const ruRU = {
|
||||
description:
|
||||
'Выберите ИИ-движок, который будет управлять интеллектом вашего бота.',
|
||||
},
|
||||
spaceBanner: {
|
||||
message:
|
||||
'Подключитесь к LangBot Space для бесплатных пробных кредитов и мгновенной настройки!',
|
||||
action: 'Авторизация через Space',
|
||||
},
|
||||
config: {
|
||||
botInfo: 'Информация о боте',
|
||||
botNamePlaceholder: 'Введите имя бота',
|
||||
@@ -1725,9 +1717,6 @@ const ruRU = {
|
||||
'Отсканируйте QR-код ниже в WeChat, чтобы авторизоваться и автоматически заполнить токен',
|
||||
loginSuccess: 'Вход выполнен успешно! Токен заполнен автоматически',
|
||||
loginFailed: 'Не удалось выполнить вход',
|
||||
connecting: 'Подключение к сервису WeChat...',
|
||||
waitingForScan: 'Ожидание сканирования',
|
||||
retry: 'Повторить',
|
||||
},
|
||||
dingtalk: {
|
||||
createApp: 'Создать приложение DingTalk в один клик',
|
||||
|
||||
@@ -85,18 +85,18 @@ const thTH = {
|
||||
'แนะนำ: ใช้ API โมเดลที่เสถียรอย่างเป็นทางการและบริการคลาวด์',
|
||||
loginLocal: 'เข้าสู่ระบบด้วยบัญชีท้องถิ่น',
|
||||
loginWithPassword: 'เข้าสู่ระบบด้วยรหัสผ่าน',
|
||||
spaceLoginTitle: 'เข้าสู่ระบบด้วยบัญชี LangBot',
|
||||
spaceLoginTitle: 'เข้าสู่ระบบด้วย Space',
|
||||
spaceLoginDescription:
|
||||
'สแกน QR code หรือเข้าชมลิงก์ด้านล่างเพื่อยืนยันสิทธิ์',
|
||||
spaceLoginUserCode: 'รหัสของคุณ',
|
||||
spaceLoginExpires: 'รหัสจะหมดอายุใน {{seconds}} วินาที',
|
||||
spaceLoginWaiting: 'กำลังรอการยืนยันสิทธิ์...',
|
||||
spaceLoginSuccess: 'ยืนยันสิทธิ์สำเร็จ',
|
||||
spaceLoginFailed: 'เข้าสู่ระบบด้วยบัญชี LangBot ล้มเหลว',
|
||||
spaceLoginFailed: 'เข้าสู่ระบบ Space ล้มเหลว',
|
||||
spaceLoginExpired: 'รหัสยืนยันหมดอายุแล้ว กรุณาลองใหม่',
|
||||
spaceLoginCancel: 'ยกเลิก',
|
||||
spaceLoginVisitLink: 'เข้าชมลิงก์',
|
||||
spaceLoginProcessing: 'กำลังเข้าสู่ระบบด้วยบัญชี LangBot',
|
||||
spaceLoginProcessing: 'กำลังเข้าสู่ระบบด้วย Space',
|
||||
spaceLoginProcessingDescription: 'กรุณารอสักครู่ขณะดำเนินการเข้าสู่ระบบ...',
|
||||
spaceLoginSuccessDescription: 'กำลังเปลี่ยนเส้นทางไปยัง LangBot...',
|
||||
spaceLoginError: 'เข้าสู่ระบบล้มเหลว',
|
||||
@@ -104,7 +104,7 @@ const thTH = {
|
||||
backToLogin: 'กลับไปหน้าเข้าสู่ระบบ',
|
||||
backToHome: 'กลับไปหน้าแรก',
|
||||
spaceAccountCannotChangePassword:
|
||||
'บัญชี LangBot ไม่สามารถเปลี่ยนรหัสผ่านได้ที่นี่',
|
||||
'บัญชี Space ไม่สามารถเปลี่ยนรหัสผ่านได้ที่นี่',
|
||||
theme: 'ธีม',
|
||||
changePassword: 'เปลี่ยนรหัสผ่าน',
|
||||
currentPassword: 'รหัสผ่านปัจจุบัน',
|
||||
@@ -213,19 +213,6 @@ const thTH = {
|
||||
selectModelAbilities: 'เลือกความสามารถของโมเดล',
|
||||
visionAbility: 'ความสามารถด้านภาพ',
|
||||
functionCallAbility: 'การเรียกฟังก์ชัน',
|
||||
reasoningAbility: 'ความสามารถในการให้เหตุผล',
|
||||
reasoningLevel: 'ระดับการให้เหตุผล',
|
||||
reasoningLevels: {
|
||||
providerDefault: 'ค่าเริ่มต้นของผู้ให้บริการ',
|
||||
disabled: 'ปิด',
|
||||
enabled: 'เปิด',
|
||||
minimal: 'ต่ำสุด',
|
||||
low: 'ต่ำ',
|
||||
medium: 'ปานกลาง',
|
||||
high: 'สูง',
|
||||
xhigh: 'สูงมาก',
|
||||
max: 'สูงสุด',
|
||||
},
|
||||
contextLength: 'หน้าต่างบริบท',
|
||||
contextLengthPlaceholder: 'ไม่ทราบ',
|
||||
contextLengthInvalid: 'หน้าต่างบริบทต้องเป็นจำนวนเต็มบวก',
|
||||
@@ -252,9 +239,8 @@ const thTH = {
|
||||
llmModels: 'โมเดล LLM',
|
||||
localProvider: 'ท้องถิ่น',
|
||||
localProviderDescription: 'โมเดลที่กำหนดค่าและจัดการในเครื่อง',
|
||||
spaceProviderDescription: 'โมเดลที่ซิงค์จากบัญชี LangBot ของคุณ',
|
||||
spaceDisabledForLocalAccount:
|
||||
'เข้าสู่ระบบด้วยบัญชี LangBot เพื่อใช้โมเดลคลาวด์',
|
||||
spaceProviderDescription: 'โมเดลที่ซิงค์จากบัญชี Space ของคุณ',
|
||||
spaceDisabledForLocalAccount: 'เข้าสู่ระบบด้วย Space เพื่อใช้โมเดลคลาวด์',
|
||||
syncModels: 'ซิงค์',
|
||||
syncSuccess:
|
||||
'ซิงค์เสร็จสมบูรณ์: สร้าง {{created}} รายการ, อัปเดต {{updated}} รายการ',
|
||||
@@ -290,11 +276,11 @@ const thTH = {
|
||||
langbotModelsDescription: 'โมเดลคลาวด์ขับเคลื่อนโดย LangBot Space',
|
||||
credits: 'เครดิต',
|
||||
loginWithSpace: 'เข้าสู่ระบบด้วยบัญชี LangBot',
|
||||
loginToUseModels: 'เข้าสู่ระบบด้วยบัญชี LangBot เพื่อใช้โมเดลคลาวด์',
|
||||
loginToUseModels: 'เข้าสู่ระบบด้วย Space เพื่อใช้โมเดลคลาวด์',
|
||||
noModels: 'ยังไม่มีโมเดลที่กำหนดค่า',
|
||||
langbotModels: 'โมเดล LangBot',
|
||||
spaceTrialTooltip:
|
||||
'มีเครดิตทดลองใช้งานฟรี! เข้าสู่ระบบด้วยบัญชี LangBot เพื่อเข้าถึงโมเดลคลาวด์โดยไม่ต้องตั้งค่า',
|
||||
'มีเครดิตทดลองใช้งานฟรี! เข้าสู่ระบบด้วย Space เพื่อเข้าถึงโมเดลคลาวด์โดยไม่ต้องตั้งค่า',
|
||||
unlockModels: 'เข้าสู่ระบบเพื่อใช้งาน',
|
||||
editProvider: 'แก้ไขผู้ให้บริการ',
|
||||
addProvider: 'เพิ่มผู้ให้บริการ',
|
||||
@@ -328,9 +314,9 @@ const thTH = {
|
||||
},
|
||||
|
||||
ownerMustBindSpace:
|
||||
'The Workspace owner must connect a LangBot Account for LangBot Models.',
|
||||
'The Workspace owner must connect Space for LangBot Models.',
|
||||
usesOwnerSpaceBilling:
|
||||
"Uses the Workspace owner's LangBot Account billing and credits.",
|
||||
"Uses the Workspace owner's Space billing and credits.",
|
||||
},
|
||||
bots: {
|
||||
title: 'บอท',
|
||||
@@ -1203,13 +1189,13 @@ const thTH = {
|
||||
description: 'นี่เป็นครั้งแรกที่คุณเริ่มใช้งาน LangBot',
|
||||
adminAccountNote: 'บัญชีที่คุณใช้ที่นี่จะถูกตั้งเป็นบัญชีผู้ดูแลระบบ',
|
||||
register: 'ลงทะเบียน',
|
||||
initWithSpace: 'เริ่มต้นด้วยบัญชี LangBot',
|
||||
initWithSpace: 'เริ่มต้นด้วย Space',
|
||||
spaceRecommended:
|
||||
'แนะนำ: ใช้ API โมเดลที่เสถียรอย่างเป็นทางการและบริการคลาวด์',
|
||||
spaceInfoTip1:
|
||||
'Space ให้บริการยืนยันตัวตนแบบรวมโดยไม่อัปโหลดข้อมูลสำคัญใดๆ ของคุณ',
|
||||
spaceInfoTip2:
|
||||
'การเข้าสู่ระบบด้วยบัญชี LangBot ช่วยให้คุณเข้าถึงโมเดล LangBot และบริการคลาวด์อื่นๆ รวมถึงเครดิตเรียกใช้โมเดลฟรีเพื่อช่วยให้คุณเริ่มต้นได้อย่างรวดเร็ว',
|
||||
'การเข้าสู่ระบบด้วยบัญชี Space ช่วยให้คุณเข้าถึงโมเดล LangBot และบริการคลาวด์อื่นๆ รวมถึงเครดิตเรียกใช้โมเดลฟรีเพื่อช่วยให้คุณเริ่มต้นได้อย่างรวดเร็ว',
|
||||
spaceInfoTip3:
|
||||
'วิธีการเข้าสู่ระบบของคุณไม่มีผลต่อฟีเจอร์อื่นๆ คุณสามารถกำหนดค่าและใช้โมเดลจากแหล่งอื่นได้ตลอดเวลา',
|
||||
registerLocal: 'ลงทะเบียนบัญชีท้องถิ่น',
|
||||
@@ -1264,32 +1250,30 @@ const thTH = {
|
||||
passwordNotSet: 'ยังไม่ได้ตั้งค่า',
|
||||
passwordSetDescription:
|
||||
'ตั้งรหัสผ่านแล้ว คุณสามารถเข้าสู่ระบบด้วยอีเมลและรหัสผ่าน',
|
||||
spaceStatus: 'บัญชี LangBot',
|
||||
spaceStatus: 'บัญชี Space',
|
||||
spaceBound: 'ผูกแล้ว',
|
||||
spaceNotBound: 'ยังไม่ผูก',
|
||||
spaceBoundDescription:
|
||||
'ผูกบัญชี LangBot แล้ว สามารถใช้ API โมเดลอย่างเป็นทางการและบริการคลาวด์ได้',
|
||||
bindSpace: 'ผูกบัญชี LangBot',
|
||||
'ผูกบัญชี Space แล้ว สามารถใช้ API โมเดลอย่างเป็นทางการและบริการคลาวด์ได้',
|
||||
bindSpace: 'ผูกบัญชี Space',
|
||||
bindSpaceDescription: 'ผูกเพื่อใช้ API โมเดลอย่างเป็นทางการและบริการคลาวด์',
|
||||
bindSpaceButton: 'ผูก',
|
||||
bindSpaceConfirmTitle: 'ยืนยันการผูก',
|
||||
bindSpaceConfirmDescription:
|
||||
'คุณกำลังจะผูกอินสแตนซ์ท้องถิ่นกับบัญชี LangBot',
|
||||
bindSpaceConfirmDescription: 'คุณกำลังจะผูกอินสแตนซ์ท้องถิ่นกับบัญชี Space',
|
||||
bindSpaceWarning:
|
||||
'หลังจากผูกแล้ว อีเมลเข้าสู่ระบบของคุณจะเปลี่ยนจาก {{localEmail}} เป็นอีเมลบัญชี LangBot',
|
||||
bindSpaceSuccess: 'ผูกบัญชี LangBot สำเร็จ',
|
||||
bindSpaceFailed: 'ผูกบัญชี LangBot ล้มเหลว',
|
||||
'หลังจากผูกแล้ว อีเมลเข้าสู่ระบบของคุณจะเปลี่ยนจาก {{localEmail}} เป็นอีเมลบัญชี Space',
|
||||
bindSpaceSuccess: 'ผูกบัญชี Space สำเร็จ',
|
||||
bindSpaceFailed: 'ผูกบัญชี Space ล้มเหลว',
|
||||
bindSpaceInvalidState: 'คำขอผูกไม่ถูกต้อง กรุณาลองใหม่จากการตั้งค่าบัญชี',
|
||||
setPasswordHint: 'ตั้งรหัสผ่านเพื่อเข้าสู่ระบบด้วยอีเมลและรหัสผ่าน',
|
||||
spaceEmailMismatch:
|
||||
'อีเมลเข้าสู่ระบบด้วยบัญชี LangBot ไม่ตรงกับอีเมลบัญชีท้องถิ่น',
|
||||
spaceEmailMismatch: 'อีเมลเข้าสู่ระบบ Space ไม่ตรงกับอีเมลบัญชีท้องถิ่น',
|
||||
|
||||
space_account_not_registeredTitle: 'Account not registered',
|
||||
space_account_not_registered:
|
||||
'No local account is registered for this LangBot Account email. Ask the Workspace owner for an invitation.',
|
||||
space_account_binding_requiredTitle: 'LangBot Account connection required',
|
||||
'No local account is registered for this Space email. Ask the Workspace owner for an invitation.',
|
||||
space_account_binding_requiredTitle: 'Space connection required',
|
||||
space_account_binding_required:
|
||||
'This local account must connect a LangBot Account from Account settings before using LangBot Account login.',
|
||||
'This local account must connect Space from Account settings before using Space login.',
|
||||
},
|
||||
monitoring: {
|
||||
title: 'แดชบอร์ด',
|
||||
@@ -1615,6 +1599,7 @@ const thTH = {
|
||||
botCreateSuccess: 'สร้าง Bot สำเร็จ!',
|
||||
botSaveSuccess: 'บันทึกและเปิดใช้งาน Bot สำเร็จ!',
|
||||
createError: 'ไม่สามารถสร้างทรัพยากรได้',
|
||||
spaceAuthError: 'ไม่สามารถเริ่มต้นการยืนยันสิทธิ์ Space ได้',
|
||||
skipSaveError: 'ไม่สามารถบันทึกสถานะการข้ามได้ กรุณาลองใหม่',
|
||||
completeSaveError: 'ไม่สามารถบันทึกสถานะการเสร็จสิ้นได้ กรุณาลองใหม่',
|
||||
step: {
|
||||
@@ -1642,6 +1627,11 @@ const thTH = {
|
||||
title: 'เลือกเครื่องมือ AI',
|
||||
description: 'เลือกเครื่องมือ AI ที่จะขับเคลื่อนความฉลาดของ Bot',
|
||||
},
|
||||
spaceBanner: {
|
||||
message:
|
||||
'เชื่อมต่อกับ LangBot Space เพื่อรับเครดิตทดลองใช้โมเดลฟรีและตั้งค่าทันทีโดยไม่ต้องกำหนดค่า!',
|
||||
action: 'ยืนยันสิทธิ์กับ Space',
|
||||
},
|
||||
config: {
|
||||
botInfo: 'ข้อมูล Bot',
|
||||
botNamePlaceholder: 'กรอกชื่อ Bot',
|
||||
@@ -1690,9 +1680,6 @@ const thTH = {
|
||||
'สแกนคิวอาร์โค้ดด้านล่างด้วย WeChat เพื่ออนุญาตและกรอกโทเคนอัตโนมัติ',
|
||||
loginSuccess: 'เข้าสู่ระบบสำเร็จ และกรอกโทเคนอัตโนมัติแล้ว',
|
||||
loginFailed: 'เข้าสู่ระบบไม่สำเร็จ',
|
||||
connecting: 'กำลังเชื่อมต่อบริการ WeChat...',
|
||||
waitingForScan: 'กำลังรอการสแกน',
|
||||
retry: 'ลองอีกครั้ง',
|
||||
},
|
||||
dingtalk: {
|
||||
createApp: 'สร้างแอป DingTalk ด้วยคลิกเดียว',
|
||||
|
||||
@@ -86,18 +86,18 @@ const viVN = {
|
||||
'Khuyến nghị: Sử dụng API mô hình ổn định chính thức và dịch vụ đám mây',
|
||||
loginLocal: 'Đăng nhập với tài khoản cục bộ',
|
||||
loginWithPassword: 'Đăng nhập bằng mật khẩu',
|
||||
spaceLoginTitle: 'Đăng nhập bằng tài khoản LangBot',
|
||||
spaceLoginTitle: 'Đăng nhập với Space',
|
||||
spaceLoginDescription:
|
||||
'Quét mã QR hoặc truy cập liên kết bên dưới để ủy quyền',
|
||||
spaceLoginUserCode: 'Mã của bạn',
|
||||
spaceLoginExpires: 'Mã hết hạn sau {{seconds}} giây',
|
||||
spaceLoginWaiting: 'Đang chờ ủy quyền...',
|
||||
spaceLoginSuccess: 'Ủy quyền thành công',
|
||||
spaceLoginFailed: 'Đăng nhập bằng tài khoản LangBot thất bại',
|
||||
spaceLoginFailed: 'Đăng nhập Space thất bại',
|
||||
spaceLoginExpired: 'Mã ủy quyền đã hết hạn, vui lòng thử lại',
|
||||
spaceLoginCancel: 'Hủy',
|
||||
spaceLoginVisitLink: 'Truy cập liên kết',
|
||||
spaceLoginProcessing: 'Đang đăng nhập bằng tài khoản LangBot',
|
||||
spaceLoginProcessing: 'Đang đăng nhập với Space',
|
||||
spaceLoginProcessingDescription:
|
||||
'Vui lòng chờ trong khi chúng tôi hoàn tất đăng nhập...',
|
||||
spaceLoginSuccessDescription: 'Đang chuyển hướng đến LangBot...',
|
||||
@@ -106,7 +106,7 @@ const viVN = {
|
||||
backToLogin: 'Quay lại đăng nhập',
|
||||
backToHome: 'Quay lại trang chủ',
|
||||
spaceAccountCannotChangePassword:
|
||||
'Tài khoản LangBot không thể đổi mật khẩu tại đây',
|
||||
'Tài khoản Space không thể đổi mật khẩu tại đây',
|
||||
theme: 'Giao diện',
|
||||
changePassword: 'Đổi mật khẩu',
|
||||
currentPassword: 'Mật khẩu hiện tại',
|
||||
@@ -217,19 +217,6 @@ const viVN = {
|
||||
selectModelAbilities: 'Chọn khả năng mô hình',
|
||||
visionAbility: 'Khả năng thị giác',
|
||||
functionCallAbility: 'Gọi hàm',
|
||||
reasoningAbility: 'Khả năng suy luận',
|
||||
reasoningLevel: 'Mức độ suy luận',
|
||||
reasoningLevels: {
|
||||
providerDefault: 'Mặc định của nhà cung cấp',
|
||||
disabled: 'Tắt',
|
||||
enabled: 'Bật',
|
||||
minimal: 'Tối thiểu',
|
||||
low: 'Thấp',
|
||||
medium: 'Trung bình',
|
||||
high: 'Cao',
|
||||
xhigh: 'Rất cao',
|
||||
max: 'Tối đa',
|
||||
},
|
||||
contextLength: 'Cửa sổ ngữ cảnh',
|
||||
contextLengthPlaceholder: 'Không rõ',
|
||||
contextLengthInvalid: 'Cửa sổ ngữ cảnh phải là số nguyên dương',
|
||||
@@ -258,9 +245,9 @@ const viVN = {
|
||||
localProvider: 'Cục bộ',
|
||||
localProviderDescription: 'Các mô hình được cấu hình và quản lý cục bộ',
|
||||
spaceProviderDescription:
|
||||
'Các mô hình được đồng bộ từ tài khoản LangBot của bạn',
|
||||
'Các mô hình được đồng bộ từ tài khoản Space của bạn',
|
||||
spaceDisabledForLocalAccount:
|
||||
'Đăng nhập bằng tài khoản LangBot để sử dụng mô hình đám mây',
|
||||
'Đăng nhập với Space để sử dụng mô hình đám mây',
|
||||
syncModels: 'Đồng bộ',
|
||||
syncSuccess:
|
||||
'Đồng bộ hoàn tất: {{created}} đã tạo, {{updated}} đã cập nhật',
|
||||
@@ -297,12 +284,11 @@ const viVN = {
|
||||
langbotModelsDescription: 'Mô hình đám mây được cung cấp bởi LangBot Space',
|
||||
credits: 'Tín dụng',
|
||||
loginWithSpace: 'Đăng nhập bằng tài khoản LangBot',
|
||||
loginToUseModels:
|
||||
'Đăng nhập bằng tài khoản LangBot để sử dụng mô hình đám mây',
|
||||
loginToUseModels: 'Đăng nhập với Space để sử dụng mô hình đám mây',
|
||||
noModels: 'Chưa cấu hình mô hình nào',
|
||||
langbotModels: 'Mô hình LangBot',
|
||||
spaceTrialTooltip:
|
||||
'Có tín dụng dùng thử miễn phí! Đăng nhập bằng tài khoản LangBot để truy cập mô hình đám mây không cần cấu hình.',
|
||||
'Có tín dụng dùng thử miễn phí! Đăng nhập với Space để truy cập mô hình đám mây không cần cấu hình.',
|
||||
unlockModels: 'Đăng nhập để sử dụng',
|
||||
editProvider: 'Chỉnh sửa nhà cung cấp',
|
||||
addProvider: 'Thêm nhà cung cấp',
|
||||
@@ -337,9 +323,9 @@ const viVN = {
|
||||
},
|
||||
|
||||
ownerMustBindSpace:
|
||||
'The Workspace owner must connect a LangBot Account for LangBot Models.',
|
||||
'The Workspace owner must connect Space for LangBot Models.',
|
||||
usesOwnerSpaceBilling:
|
||||
"Uses the Workspace owner's LangBot Account billing and credits.",
|
||||
"Uses the Workspace owner's Space billing and credits.",
|
||||
},
|
||||
bots: {
|
||||
title: 'Bot',
|
||||
@@ -1223,13 +1209,13 @@ const viVN = {
|
||||
adminAccountNote:
|
||||
'Tài khoản bạn sử dụng ở đây sẽ được đặt làm tài khoản quản trị viên',
|
||||
register: 'Đăng ký',
|
||||
initWithSpace: 'Khởi tạo bằng tài khoản LangBot',
|
||||
initWithSpace: 'Khởi tạo với Space',
|
||||
spaceRecommended:
|
||||
'Khuyến nghị: Sử dụng API mô hình ổn định chính thức và dịch vụ đám mây',
|
||||
spaceInfoTip1:
|
||||
'Space cung cấp dịch vụ xác thực tài khoản thống nhất mà không tải lên bất kỳ thông tin nhạy cảm nào của bạn.',
|
||||
spaceInfoTip2:
|
||||
'Đăng nhập bằng tài khoản LangBot cho phép bạn truy cập Mô hình LangBot và các dịch vụ đám mây khác, bao gồm tín dụng gọi mô hình miễn phí để giúp bạn bắt đầu nhanh chóng.',
|
||||
'Đăng nhập bằng tài khoản Space cho phép bạn truy cập Mô hình LangBot và các dịch vụ đám mây khác, bao gồm tín dụng gọi mô hình miễn phí để giúp bạn bắt đầu nhanh chóng.',
|
||||
spaceInfoTip3:
|
||||
'Phương thức đăng nhập của bạn không ảnh hưởng đến các tính năng khác. Bạn có thể cấu hình và sử dụng mô hình từ các nguồn khác bất cứ lúc nào.',
|
||||
registerLocal: 'Đăng ký tài khoản cục bộ',
|
||||
@@ -1286,34 +1272,34 @@ const viVN = {
|
||||
passwordNotSet: 'Chưa đặt',
|
||||
passwordSetDescription:
|
||||
'Mật khẩu đã được đặt, bạn có thể đăng nhập bằng email và mật khẩu',
|
||||
spaceStatus: 'Tài khoản LangBot',
|
||||
spaceStatus: 'Tài khoản Space',
|
||||
spaceBound: 'Đã liên kết',
|
||||
spaceNotBound: 'Chưa liên kết',
|
||||
spaceBoundDescription:
|
||||
'Tài khoản LangBot đã liên kết, có thể sử dụng API mô hình chính thức và dịch vụ đám mây',
|
||||
bindSpace: 'Liên kết tài khoản LangBot',
|
||||
'Tài khoản Space đã liên kết, có thể sử dụng API mô hình chính thức và dịch vụ đám mây',
|
||||
bindSpace: 'Liên kết tài khoản Space',
|
||||
bindSpaceDescription:
|
||||
'Liên kết để sử dụng API mô hình chính thức và dịch vụ đám mây',
|
||||
bindSpaceButton: 'Liên kết',
|
||||
bindSpaceConfirmTitle: 'Xác nhận liên kết',
|
||||
bindSpaceConfirmDescription:
|
||||
'Bạn sắp liên kết phiên bản cục bộ với tài khoản LangBot',
|
||||
'Bạn sắp liên kết phiên bản cục bộ với tài khoản Space',
|
||||
bindSpaceWarning:
|
||||
'Sau khi liên kết, email đăng nhập của bạn sẽ được đổi từ {{localEmail}} sang email tài khoản LangBot.',
|
||||
bindSpaceSuccess: 'Liên kết tài khoản LangBot thành công',
|
||||
bindSpaceFailed: 'Liên kết tài khoản LangBot thất bại',
|
||||
'Sau khi liên kết, email đăng nhập của bạn sẽ được đổi từ {{localEmail}} sang email tài khoản Space.',
|
||||
bindSpaceSuccess: 'Liên kết tài khoản Space thành công',
|
||||
bindSpaceFailed: 'Liên kết tài khoản Space thất bại',
|
||||
bindSpaceInvalidState:
|
||||
'Yêu cầu liên kết không hợp lệ. Vui lòng thử lại từ cài đặt tài khoản.',
|
||||
setPasswordHint: 'Đặt mật khẩu để đăng nhập bằng email và mật khẩu',
|
||||
spaceEmailMismatch:
|
||||
'Email tài khoản LangBot không khớp với email tài khoản cục bộ',
|
||||
'Email đăng nhập Space không khớp với email tài khoản cục bộ',
|
||||
|
||||
space_account_not_registeredTitle: 'Account not registered',
|
||||
space_account_not_registered:
|
||||
'No local account is registered for this LangBot Account email. Ask the Workspace owner for an invitation.',
|
||||
space_account_binding_requiredTitle: 'LangBot Account connection required',
|
||||
'No local account is registered for this Space email. Ask the Workspace owner for an invitation.',
|
||||
space_account_binding_requiredTitle: 'Space connection required',
|
||||
space_account_binding_required:
|
||||
'This local account must connect a LangBot Account from Account settings before using LangBot Account login.',
|
||||
'This local account must connect Space from Account settings before using Space login.',
|
||||
},
|
||||
monitoring: {
|
||||
title: 'Bảng điều khiển',
|
||||
@@ -1639,6 +1625,7 @@ const viVN = {
|
||||
botCreateSuccess: 'Tạo Bot thành công!',
|
||||
botSaveSuccess: 'Cấu hình Bot đã lưu và bật!',
|
||||
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.',
|
||||
completeSaveError: 'Lưu trạng thái hoàn tất thất bại. Vui lòng thử lại.',
|
||||
step: {
|
||||
@@ -1666,6 +1653,11 @@ const viVN = {
|
||||
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.',
|
||||
},
|
||||
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: {
|
||||
botInfo: 'Thông tin Bot',
|
||||
botNamePlaceholder: 'Nhập tên Bot',
|
||||
@@ -1716,9 +1708,6 @@ const viVN = {
|
||||
'Quét mã QR bên dưới bằng WeChat để ủy quyền và tự động điền token',
|
||||
loginSuccess: 'Đăng nhập thành công! Token đã được điền tự động',
|
||||
loginFailed: 'Đăng nhập thất bại',
|
||||
connecting: 'Đang kết nối tới dịch vụ WeChat...',
|
||||
waitingForScan: 'Đang chờ quét mã',
|
||||
retry: 'Thử lại',
|
||||
},
|
||||
dingtalk: {
|
||||
createApp: 'Tạo ứng dụng DingTalk chỉ với một lần nhấp',
|
||||
|
||||
@@ -83,24 +83,24 @@ const zhHans = {
|
||||
spaceLoginRecommended: '推荐:使用官方提供的稳定模型 API 和云服务',
|
||||
loginLocal: '使用本地账号登录',
|
||||
loginWithPassword: '通过密码登录',
|
||||
spaceLoginTitle: '通过 LangBot 账号登录',
|
||||
spaceLoginTitle: '通过 Space 登录',
|
||||
spaceLoginDescription: '扫描二维码或访问下方链接进行授权',
|
||||
spaceLoginUserCode: '您的验证码',
|
||||
spaceLoginExpires: '验证码将在 {{seconds}} 秒后过期',
|
||||
spaceLoginWaiting: '等待授权中...',
|
||||
spaceLoginSuccess: '授权成功',
|
||||
spaceLoginFailed: 'LangBot 账号登录失败',
|
||||
spaceLoginFailed: 'Space 登录失败',
|
||||
spaceLoginExpired: '验证码已过期,请重试',
|
||||
spaceLoginCancel: '取消',
|
||||
spaceLoginVisitLink: '访问链接',
|
||||
spaceLoginProcessing: '正在通过 LangBot 账号登录',
|
||||
spaceLoginProcessing: '正在通过 Space 登录',
|
||||
spaceLoginProcessingDescription: '请稍候,正在完成登录...',
|
||||
spaceLoginSuccessDescription: '正在跳转到 LangBot...',
|
||||
spaceLoginError: '登录失败',
|
||||
spaceLoginNoCode: '缺少授权码',
|
||||
backToLogin: '返回登录',
|
||||
backToHome: '返回首页',
|
||||
spaceAccountCannotChangePassword: 'LangBot 账号无法在此修改密码',
|
||||
spaceAccountCannotChangePassword: 'Space 账户无法在此修改密码',
|
||||
theme: '主题',
|
||||
changePassword: '修改密码',
|
||||
currentPassword: '当前密码',
|
||||
@@ -243,8 +243,8 @@ const zhHans = {
|
||||
llmModels: '对话模型',
|
||||
localProvider: '本地',
|
||||
localProviderDescription: '在本地配置和管理的模型',
|
||||
spaceProviderDescription: '从您的 LangBot 账号同步的模型',
|
||||
spaceDisabledForLocalAccount: '使用 LangBot 账号登录以使用云端模型',
|
||||
spaceProviderDescription: '从您的 Space 账户同步的模型',
|
||||
spaceDisabledForLocalAccount: '使用 Space 登录以使用云端模型',
|
||||
syncModels: '同步',
|
||||
syncSuccess: '同步完成:创建 {{created}} 个,更新 {{updated}} 个',
|
||||
syncError: '同步失败:',
|
||||
@@ -279,14 +279,13 @@ const zhHans = {
|
||||
langbotModelsDescription: 'LangBot Space 提供的云端模型',
|
||||
credits: '积分',
|
||||
loginWithSpace: '使用 LangBot 账号登录',
|
||||
loginToUseModels: '通过 LangBot 账号登录以使用云端模型',
|
||||
ownerMustBindSpace:
|
||||
'工作区所有者需要绑定 LangBot 账号才能使用 LangBot 模型。',
|
||||
usesOwnerSpaceBilling: '使用工作区所有者的 LangBot 账号计费与积分。',
|
||||
loginToUseModels: '通过 Space 登录以使用云端模型',
|
||||
ownerMustBindSpace: '工作区所有者需要绑定 Space 才能使用 LangBot 模型。',
|
||||
usesOwnerSpaceBilling: '使用工作区所有者的 Space 计费与积分。',
|
||||
noModels: '暂无模型',
|
||||
langbotModels: 'LangBot 模型',
|
||||
spaceTrialTooltip:
|
||||
'免费试用积分已就绪!通过 LangBot 账号登录即可零配置使用云端模型。',
|
||||
'免费试用积分已就绪!通过 Space 登录即可零配置使用云端模型。',
|
||||
unlockModels: '登录以使用',
|
||||
editProvider: '编辑供应商',
|
||||
addProvider: '添加供应商',
|
||||
@@ -1162,11 +1161,11 @@ const zhHans = {
|
||||
description: '这是您首次启动 LangBot',
|
||||
adminAccountNote: '您在此处初始化使用的账号将作为管理员账号',
|
||||
register: '注册',
|
||||
initWithSpace: '通过 LangBot 账号初始化',
|
||||
initWithSpace: '通过 Space 初始化',
|
||||
spaceRecommended: '推荐:使用官方提供的稳定模型 API 和云服务',
|
||||
spaceInfoTip1: 'Space 提供统一的账户鉴权服务,不会上传您的任何敏感信息。',
|
||||
spaceInfoTip2:
|
||||
'使用 LangBot 账号登录可使用 LangBot Models 等云服务,您将会获得一定的免费模型调用额度帮助您快速起步。',
|
||||
'使用 Space 账户登录可使用 LangBot Models 等云服务,您将会获得一定的免费模型调用额度帮助您快速起步。',
|
||||
spaceInfoTip3:
|
||||
'登录方式不会影响其他功能,您在任何情况下都可以配置使用其他来源的模型。',
|
||||
registerLocal: '注册本地账号',
|
||||
@@ -1220,28 +1219,28 @@ const zhHans = {
|
||||
passwordSet: '已设置',
|
||||
passwordNotSet: '未设置',
|
||||
passwordSetDescription: '您已设置本地密码,可使用邮箱密码登录',
|
||||
spaceStatus: 'LangBot 账号',
|
||||
spaceStatus: 'Space 账户',
|
||||
spaceBound: '已绑定',
|
||||
spaceNotBound: '未绑定',
|
||||
spaceBoundDescription: '已绑定 LangBot 账号,可使用官方模型 API 和云服务',
|
||||
bindSpace: '绑定 LangBot 账号',
|
||||
spaceBoundDescription: '已绑定 Space 账户,可使用官方模型 API 和云服务',
|
||||
bindSpace: '绑定 Space 账户',
|
||||
bindSpaceDescription: '绑定后可使用官方模型 API 和云服务',
|
||||
bindSpaceButton: '绑定',
|
||||
bindSpaceConfirmTitle: '确认绑定',
|
||||
bindSpaceConfirmDescription: '您即将把本地实例绑定到 LangBot 账号',
|
||||
bindSpaceConfirmDescription: '您即将把本地实例绑定到 Space 账户',
|
||||
bindSpaceWarning:
|
||||
'绑定后,您的登录邮箱将从 {{localEmail}} 更改为 LangBot 账号的邮箱。',
|
||||
bindSpaceSuccess: 'LangBot 账号绑定成功',
|
||||
bindSpaceFailed: '绑定 LangBot 账号失败',
|
||||
'绑定后,您的登录邮箱将从 {{localEmail}} 更改为 Space 账户的邮箱。',
|
||||
bindSpaceSuccess: 'Space 账户绑定成功',
|
||||
bindSpaceFailed: '绑定 Space 账户失败',
|
||||
bindSpaceInvalidState: '无效的绑定请求,请从账户设置重新发起',
|
||||
setPasswordHint: '设置密码后可使用邮箱密码登录',
|
||||
spaceEmailMismatch: 'LangBot 账号邮箱与本实例账号邮箱不匹配',
|
||||
spaceEmailMismatch: 'Space登录账号邮箱与本实例账号邮箱不匹配',
|
||||
space_account_not_registeredTitle: '账户尚未注册',
|
||||
space_account_not_registered:
|
||||
'此 LangBot 账号邮箱尚无本地账户,请联系工作区所有者获取邀请。',
|
||||
space_account_binding_requiredTitle: '需要绑定 LangBot 账号',
|
||||
'此 Space 邮箱尚无本地账户,请联系工作区所有者获取邀请。',
|
||||
space_account_binding_requiredTitle: '需要绑定 Space',
|
||||
space_account_binding_required:
|
||||
'此本地账户必须先在账户设置中绑定 LangBot 账号,才能使用 LangBot 账号登录。',
|
||||
'此本地账户必须先在账户设置中绑定 Space,才能使用 Space 登录。',
|
||||
},
|
||||
workspace: {
|
||||
title: '工作区',
|
||||
@@ -1731,6 +1730,7 @@ const zhHans = {
|
||||
botCreateSuccess: '机器人创建成功!',
|
||||
botSaveSuccess: '机器人配置已保存并启用!',
|
||||
createError: '创建资源失败',
|
||||
spaceAuthError: '无法发起 Space 授权',
|
||||
skipSaveError: '保存跳过状态失败,请重试。',
|
||||
completeSaveError: '保存完成状态失败,请重试。',
|
||||
step: {
|
||||
@@ -1749,12 +1749,41 @@ const zhHans = {
|
||||
saveBot: '保存并启用',
|
||||
resaveBot: '重新保存配置',
|
||||
botSaved: '机器人配置已保存并启用,请查看日志确认连接正常。',
|
||||
waitingForMessage: '机器人已启用。请在 IM 中向机器人发送一条消息以继续。',
|
||||
messageReceived: '机器人已成功收到 IM 消息,可以进入下一步。',
|
||||
pageBotTestPrompt:
|
||||
'页面机器人已启用。点击右下角聊天气泡并发送一条消息,验证完整对话链路。',
|
||||
webhookTestPrompt:
|
||||
'回调地址已就绪。将它配置到外部平台,然后向机器人发送一条真实消息。',
|
||||
httpTestPrompt: 'HTTP Bot 已启用。可直接发送一条真实入站消息验证连接。',
|
||||
httpTestDefaultMessage: '你好,这是一条连接测试消息。',
|
||||
sendHttpTest: '发送测试消息',
|
||||
httpTestAccepted: '测试消息已被机器人接收,请稍候查看日志。',
|
||||
httpTestMissingSecret: '请先填写入站签名密钥并重新保存。',
|
||||
httpTestFailed: '测试消息发送失败:{{error}}',
|
||||
logsTitle: '机器人日志',
|
||||
logsDescription: '监控机器人活动,确认平台连接是否正常工作。',
|
||||
},
|
||||
aiEngine: {
|
||||
title: '选择 AI 引擎',
|
||||
description: '选择驱动机器人智能的 AI 引擎。',
|
||||
optionalDescription: '这一步可选。选择接下来要如何完善当前 Agent。',
|
||||
externalTitle: '接入外部平台 Agent',
|
||||
externalDescription:
|
||||
'接入 Dify、n8n、Coze 等平台,并替换当前机器人的流水线。',
|
||||
ownModelTitle: '改成使用自己的模型',
|
||||
ownModelDescription: '进入当前 Local Agent 流水线,配置你自己的模型。',
|
||||
moreFeaturesTitle: '给现在的 Agent 配置更多功能',
|
||||
moreFeaturesDescription: '进入工作台,为 Agent 添加工具、知识库等能力。',
|
||||
runnerDescription: '选择外部 Agent 的 Runner 并完成连接配置。',
|
||||
backToChoices: '返回选项',
|
||||
createExternal: '创建并绑定',
|
||||
configurePipeline: '配置流水线',
|
||||
openWorkbench: '进入工作台',
|
||||
},
|
||||
spaceBanner: {
|
||||
message: '接入 LangBot Space,获取免费试用模型额度,零配置极速开箱!',
|
||||
action: '前往授权登录',
|
||||
},
|
||||
config: {
|
||||
botInfo: '机器人信息',
|
||||
@@ -1830,9 +1859,6 @@ const zhHans = {
|
||||
scanQRCode: '请使用微信扫描以下二维码,授权后将自动登录并填写令牌',
|
||||
loginSuccess: '登录成功!令牌已自动填入',
|
||||
loginFailed: '登录失败',
|
||||
connecting: '正在连接微信服务...',
|
||||
waitingForScan: '等待扫码中',
|
||||
retry: '重试',
|
||||
},
|
||||
dingtalk: {
|
||||
createApp: '一键创建钉钉应用',
|
||||
|
||||
@@ -83,24 +83,24 @@ const zhHant = {
|
||||
spaceLoginRecommended: '推薦:使用官方提供的穩定模型 API 和雲服務',
|
||||
loginLocal: '使用本地帳號登入',
|
||||
loginWithPassword: '透過密碼登入',
|
||||
spaceLoginTitle: '透過 LangBot 帳號登入',
|
||||
spaceLoginTitle: '透過 Space 登入',
|
||||
spaceLoginDescription: '掃描二維碼或訪問下方連結進行授權',
|
||||
spaceLoginUserCode: '您的驗證碼',
|
||||
spaceLoginExpires: '驗證碼將在 {{seconds}} 秒後過期',
|
||||
spaceLoginWaiting: '等待授權中...',
|
||||
spaceLoginSuccess: '授權成功',
|
||||
spaceLoginFailed: 'LangBot 帳號登入失敗',
|
||||
spaceLoginFailed: 'Space 登入失敗',
|
||||
spaceLoginExpired: '驗證碼已過期,請重試',
|
||||
spaceLoginCancel: '取消',
|
||||
spaceLoginVisitLink: '訪問連結',
|
||||
spaceLoginProcessing: '正在透過 LangBot 帳號登入',
|
||||
spaceLoginProcessing: '正在透過 Space 登入',
|
||||
spaceLoginProcessingDescription: '請稍候,正在完成登入...',
|
||||
spaceLoginSuccessDescription: '正在跳轉到 LangBot...',
|
||||
spaceLoginError: '登入失敗',
|
||||
spaceLoginNoCode: '缺少授權碼',
|
||||
backToLogin: '返回登入',
|
||||
backToHome: '返回首頁',
|
||||
spaceAccountCannotChangePassword: 'LangBot 帳號無法在此修改密碼',
|
||||
spaceAccountCannotChangePassword: 'Space 帳戶無法在此修改密碼',
|
||||
theme: '主題',
|
||||
changePassword: '修改密碼',
|
||||
currentPassword: '當前密碼',
|
||||
@@ -205,19 +205,6 @@ const zhHant = {
|
||||
selectModelAbilities: '選擇模型能力',
|
||||
visionAbility: '視覺能力',
|
||||
functionCallAbility: '函數呼叫',
|
||||
reasoningAbility: '思考能力',
|
||||
reasoningLevel: '思考等級',
|
||||
reasoningLevels: {
|
||||
providerDefault: '供應商預設',
|
||||
disabled: '關閉',
|
||||
enabled: '開啟',
|
||||
minimal: '最低',
|
||||
low: '低',
|
||||
medium: '中',
|
||||
high: '高',
|
||||
xhigh: '極高',
|
||||
max: '最大',
|
||||
},
|
||||
contextLength: '上下文視窗',
|
||||
contextLengthPlaceholder: '未知',
|
||||
contextLengthInvalid: '上下文視窗必須是正整數',
|
||||
@@ -244,8 +231,8 @@ const zhHant = {
|
||||
llmModels: '對話模型',
|
||||
localProvider: '本地',
|
||||
localProviderDescription: '在本地設定和管理的模型',
|
||||
spaceProviderDescription: '從您的 LangBot 帳號同步的模型',
|
||||
spaceDisabledForLocalAccount: '使用 LangBot 帳號登入以使用雲端模型',
|
||||
spaceProviderDescription: '從您的 Space 帳戶同步的模型',
|
||||
spaceDisabledForLocalAccount: '使用 Space 登入以使用雲端模型',
|
||||
syncModels: '同步',
|
||||
syncSuccess: '同步完成:建立 {{created}} 個,更新 {{updated}} 個',
|
||||
syncError: '同步失敗:',
|
||||
@@ -279,11 +266,11 @@ const zhHant = {
|
||||
langbotModelsDescription: '由 LangBot Space 提供的雲端模型',
|
||||
credits: '積分',
|
||||
loginWithSpace: '使用 LangBot 帳號登入',
|
||||
loginToUseModels: '使用 LangBot 帳號登入以使用雲端模型',
|
||||
loginToUseModels: '使用 Space 登入以使用雲端模型',
|
||||
noModels: '暫無模型',
|
||||
langbotModels: 'LangBot 模型',
|
||||
spaceTrialTooltip:
|
||||
'免費試用積分已就緒!使用 LangBot 帳號登入即可零設定使用雲端模型。',
|
||||
'免費試用積分已就緒!使用 Space 登入即可零設定使用雲端模型。',
|
||||
unlockModels: '登入以使用',
|
||||
editProvider: '編輯供應商',
|
||||
addProvider: '新增供應商',
|
||||
@@ -317,9 +304,9 @@ const zhHant = {
|
||||
},
|
||||
|
||||
ownerMustBindSpace:
|
||||
'The Workspace owner must connect a LangBot Account for LangBot Models.',
|
||||
'The Workspace owner must connect Space for LangBot Models.',
|
||||
usesOwnerSpaceBilling:
|
||||
"Uses the Workspace owner's LangBot Account billing and credits.",
|
||||
"Uses the Workspace owner's Space billing and credits.",
|
||||
},
|
||||
bots: {
|
||||
title: '機器人',
|
||||
@@ -1163,11 +1150,11 @@ const zhHant = {
|
||||
description: '這是您首次啟動 LangBot',
|
||||
adminAccountNote: '您在此處初始化使用的帳號將作為管理員帳號',
|
||||
register: '註冊',
|
||||
initWithSpace: '透過 LangBot 帳號初始化',
|
||||
initWithSpace: '透過 Space 初始化',
|
||||
spaceRecommended: '推薦:使用官方提供的穩定模型 API 和雲服務',
|
||||
spaceInfoTip1: 'Space 提供統一的帳戶鑑權服務,不會上傳您的任何敏感資訊。',
|
||||
spaceInfoTip2:
|
||||
'使用 LangBot 帳號登入可使用 LangBot Models 等雲服務,您將會獲得一定的免費模型調用額度幫助您快速起步。',
|
||||
'使用 Space 帳戶登入可使用 LangBot Models 等雲服務,您將會獲得一定的免費模型調用額度幫助您快速起步。',
|
||||
spaceInfoTip3:
|
||||
'登入方式不會影響其他功能,您在任何情況下都可以配置使用其他來源的模型。',
|
||||
registerLocal: '註冊本地帳號',
|
||||
@@ -1221,29 +1208,29 @@ const zhHant = {
|
||||
passwordSet: '已設定',
|
||||
passwordNotSet: '未設定',
|
||||
passwordSetDescription: '您已設定本地密碼,可使用電子郵件密碼登入',
|
||||
spaceStatus: 'LangBot 帳號',
|
||||
spaceStatus: 'Space 帳戶',
|
||||
spaceBound: '已綁定',
|
||||
spaceNotBound: '未綁定',
|
||||
spaceBoundDescription: '已綁定 LangBot 帳號,可使用官方模型 API 和雲服務',
|
||||
bindSpace: '綁定 LangBot 帳號',
|
||||
spaceBoundDescription: '已綁定 Space 帳戶,可使用官方模型 API 和雲服務',
|
||||
bindSpace: '綁定 Space 帳戶',
|
||||
bindSpaceDescription: '綁定後可使用官方模型 API 和雲服務',
|
||||
bindSpaceButton: '綁定',
|
||||
bindSpaceConfirmTitle: '確認綁定',
|
||||
bindSpaceConfirmDescription: '您即將把本地實例綁定到 LangBot 帳號',
|
||||
bindSpaceConfirmDescription: '您即將把本地實例綁定到 Space 帳戶',
|
||||
bindSpaceWarning:
|
||||
'綁定後,您的登入電子郵件將從 {{localEmail}} 更改為 LangBot 帳號的電子郵件。',
|
||||
bindSpaceSuccess: 'LangBot 帳號綁定成功',
|
||||
bindSpaceFailed: '綁定 LangBot 帳號失敗',
|
||||
'綁定後,您的登入電子郵件將從 {{localEmail}} 更改為 Space 帳戶的電子郵件。',
|
||||
bindSpaceSuccess: 'Space 帳戶綁定成功',
|
||||
bindSpaceFailed: '綁定 Space 帳戶失敗',
|
||||
bindSpaceInvalidState: '無效的綁定請求,請從帳戶設定重新發起',
|
||||
setPasswordHint: '設定密碼後可使用電子郵件密碼登入',
|
||||
spaceEmailMismatch: 'LangBot 帳號電子郵件與本實例帳號電子郵件不匹配',
|
||||
spaceEmailMismatch: 'Space登入帳號電子郵件與本實例帳號電子郵件不匹配',
|
||||
|
||||
space_account_not_registeredTitle: 'Account not registered',
|
||||
space_account_not_registered:
|
||||
'No local account is registered for this LangBot Account email. Ask the Workspace owner for an invitation.',
|
||||
space_account_binding_requiredTitle: 'LangBot Account connection required',
|
||||
'No local account is registered for this Space email. Ask the Workspace owner for an invitation.',
|
||||
space_account_binding_requiredTitle: 'Space connection required',
|
||||
space_account_binding_required:
|
||||
'This local account must connect a LangBot Account from Account settings before using LangBot Account login.',
|
||||
'This local account must connect Space from Account settings before using Space login.',
|
||||
},
|
||||
monitoring: {
|
||||
title: '儀表盤',
|
||||
@@ -1566,6 +1553,7 @@ const zhHant = {
|
||||
botCreateSuccess: '機器人建立成功!',
|
||||
botSaveSuccess: '機器人配置已儲存並啟用!',
|
||||
createError: '建立資源失敗',
|
||||
spaceAuthError: '無法發起 Space 授權',
|
||||
skipSaveError: '儲存跳過狀態失敗,請重試。',
|
||||
completeSaveError: '儲存完成狀態失敗,請重試。',
|
||||
step: {
|
||||
@@ -1591,6 +1579,10 @@ const zhHant = {
|
||||
title: '選擇 AI 引擎',
|
||||
description: '選擇驅動機器人智慧的 AI 引擎。',
|
||||
},
|
||||
spaceBanner: {
|
||||
message: '接入 LangBot Space,取得免費試用模型額度,零配置極速開箱!',
|
||||
action: '前往授權登入',
|
||||
},
|
||||
config: {
|
||||
botInfo: '機器人資訊',
|
||||
botNamePlaceholder: '請輸入機器人名稱',
|
||||
@@ -1665,9 +1657,6 @@ const zhHant = {
|
||||
scanQRCode: '請使用微信掃描以下 QR Code,授權後將自動登入並填寫令牌',
|
||||
loginSuccess: '登入成功!令牌已自動填入',
|
||||
loginFailed: '登入失敗',
|
||||
connecting: '正在連接微信服務...',
|
||||
waitingForScan: '等待掃碼中',
|
||||
retry: '重試',
|
||||
},
|
||||
dingtalk: {
|
||||
createApp: '一鍵建立釘釘應用',
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readdirSync, readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
|
||||
const localeDir = new URL('../../src/i18n/locales/', import.meta.url);
|
||||
const localeFiles = readdirSync(localeDir).filter((name) =>
|
||||
name.endsWith('.ts'),
|
||||
);
|
||||
|
||||
const deprecatedAccountCopy = [
|
||||
/Initialize with Space/i,
|
||||
/Login with Space/i,
|
||||
/Logging in with Space/i,
|
||||
/Space login/i,
|
||||
/Space accounts?/i,
|
||||
/Bind Space Account/i,
|
||||
/Authorize with Space/i,
|
||||
/通过 Space 登录/,
|
||||
/使用 Space 登录/,
|
||||
/Space 登录/,
|
||||
/Space 账户/,
|
||||
/Space 帳戶/,
|
||||
/绑定 Space/,
|
||||
/綁定 Space/,
|
||||
/Space アカウント/,
|
||||
/Space でログイン/,
|
||||
/cuenta de Space/i,
|
||||
/cuentas de Space/i,
|
||||
/cuenta Space/i,
|
||||
/tài khoản Space/i,
|
||||
/บัญชี Space/,
|
||||
/аккаунт(?:ов|а)? Space/i,
|
||||
/аккаунт Space/i,
|
||||
];
|
||||
|
||||
test('user-facing account authentication copy uses LangBot Account terminology', () => {
|
||||
const violations = [];
|
||||
|
||||
for (const file of localeFiles) {
|
||||
const source = readFileSync(new URL(file, localeDir), 'utf8');
|
||||
for (const pattern of deprecatedAccountCopy) {
|
||||
if (pattern.test(source)) violations.push(`${file}: ${pattern}`);
|
||||
}
|
||||
}
|
||||
|
||||
assert.deepEqual(violations, []);
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
const root = process.cwd();
|
||||
const dialogPath = path.join(
|
||||
root,
|
||||
'src/app/home/components/qrcode-login/QrCodeLoginDialog.tsx',
|
||||
);
|
||||
const localeDir = path.join(root, 'src/i18n/locales');
|
||||
|
||||
const dialogSource = fs.readFileSync(dialogPath, 'utf8');
|
||||
|
||||
test('QR credential exchanges preserve the active Workspace scope', () => {
|
||||
assert.match(dialogSource, /getActiveWorkspaceUuid/);
|
||||
assert.match(
|
||||
dialogSource,
|
||||
/sessionWorkspaceUuidRef\.current = workspaceUuid/,
|
||||
);
|
||||
assert.match(
|
||||
dialogSource,
|
||||
/const workspaceUuid = sessionWorkspaceUuidRef\.current/,
|
||||
);
|
||||
assert.match(dialogSource, /sessionApiBaseRef\.current = cfg\.apiBase/);
|
||||
assert.match(
|
||||
dialogSource,
|
||||
/`\$\{baseUrlRef\.current\}\$\{sessionApiBaseRef\.current\}\/\$\{sessionIdRef\.current\}`/,
|
||||
);
|
||||
assert.match(dialogSource, /'X-Workspace-Id': workspaceUuid/);
|
||||
|
||||
const workspaceHeaderUses = dialogSource.match(
|
||||
/'X-Workspace-Id': workspaceUuid/g,
|
||||
);
|
||||
assert.equal(
|
||||
workspaceHeaderUses?.length,
|
||||
4,
|
||||
'start, poll, expiry cleanup, and dialog cleanup must all retain Workspace scope',
|
||||
);
|
||||
});
|
||||
|
||||
test('WeChat QR login never reuses Feishu progress copy', () => {
|
||||
const weixinConfig = dialogSource.match(
|
||||
/weixin:\s*\{[\s\S]*?apiBase:\s*'\/api\/v1\/platform\/adapters\/weixin\/login'/,
|
||||
)?.[0];
|
||||
assert.ok(weixinConfig, 'WeChat platform config is missing');
|
||||
assert.match(weixinConfig, /connectingKey:\s*'weixin\.connecting'/);
|
||||
assert.match(weixinConfig, /waitingKey:\s*'weixin\.waitingForScan'/);
|
||||
assert.match(weixinConfig, /retryKey:\s*'weixin\.retry'/);
|
||||
assert.doesNotMatch(weixinConfig, /feishu\./);
|
||||
|
||||
for (const locale of [
|
||||
'en-US.ts',
|
||||
'es-ES.ts',
|
||||
'ja-JP.ts',
|
||||
'ru-RU.ts',
|
||||
'th-TH.ts',
|
||||
'vi-VN.ts',
|
||||
'zh-Hans.ts',
|
||||
'zh-Hant.ts',
|
||||
]) {
|
||||
const source = fs.readFileSync(path.join(localeDir, locale), 'utf8');
|
||||
const block = source.match(/weixin:\s*\{[\s\S]*?\n\s*\},/)?.[0];
|
||||
assert.ok(block, `${locale} is missing the WeChat locale block`);
|
||||
for (const key of ['connecting', 'waitingForScan', 'retry']) {
|
||||
assert.match(
|
||||
block,
|
||||
new RegExp(`\\b${key}:`),
|
||||
`${locale} is missing weixin.${key}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
+34
-13
@@ -1,18 +1,39 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), '');
|
||||
const apiProxyTarget = env.VITE_API_PROXY_TARGET || 'http://127.0.0.1:5300';
|
||||
|
||||
return {
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: apiProxyTarget,
|
||||
changeOrigin: true,
|
||||
ws: true,
|
||||
},
|
||||
'/mcp': {
|
||||
target: apiProxyTarget,
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/bots': {
|
||||
target: apiProxyTarget,
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user