mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-27 04:37:13 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b121c66f18 | |||
| 536534865e | |||
| ff528e8e0b | |||
| f7914a8900 | |||
| c18fc9dfe3 | |||
| bc40418bda | |||
| 45ed6efceb | |||
| c67a503532 | |||
| 8b14d2ab8e | |||
| 97428310b9 | |||
| 338ee733cb | |||
| c3299fd1a6 | |||
| be8478e940 | |||
| 96a535827f | |||
| 9b81a0b606 | |||
| bbc912d0ef | |||
| 20710df9cb |
@@ -1,59 +0,0 @@
|
||||
name: Build and deploy production
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [deploy/prod]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: langbot-production
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
CORE_IMAGE: ${{ secrets.DOCKER_USERNAME }}/langbot
|
||||
CLOUD_IMAGE: ${{ secrets.DOCKER_USERNAME }}/langbot-cloud-core
|
||||
SPACE_REF: 58253c53933f95d81b035fbe2efedb55b6c1a82b
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build exact Core image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.CORE_IMAGE }}:prod-${{ github.sha }}
|
||||
${{ env.CORE_IMAGE }}:deploy-prod
|
||||
cache-from: type=gha,scope=core-prod
|
||||
cache-to: type=gha,mode=max,scope=core-prod
|
||||
- name: Checkout production Cloud adapter
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: langbot-app/langbot-space
|
||||
ref: ${{ env.SPACE_REF }}
|
||||
token: ${{ secrets.CLA_PAT }}
|
||||
path: .space
|
||||
- name: Build exact Cloud Core image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .space
|
||||
file: .space/Dockerfile.cloud
|
||||
push: true
|
||||
build-args: LANGBOT_CORE_IMAGE=${{ env.CORE_IMAGE }}:prod-${{ github.sha }}
|
||||
tags: |
|
||||
${{ env.CLOUD_IMAGE }}:prod-${{ github.sha }}
|
||||
${{ env.CLOUD_IMAGE }}:deploy-prod
|
||||
cache-from: type=gha,scope=cloud-core-prod
|
||||
cache-to: type=gha,mode=max,scope=cloud-core-prod
|
||||
@@ -1,97 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
cd /opt/langbot-cloud-prod
|
||||
TAG=${1:?usage: deploy.sh prod-<40-char-sha>}
|
||||
[[ "$TAG" =~ ^prod-[0-9a-f]{40}$ ]] || { echo 'invalid immutable image tag' >&2; exit 2; }
|
||||
[[ -s .env ]] || { echo '/opt/langbot-cloud-prod/.env is missing' >&2; exit 3; }
|
||||
|
||||
rendered_compose=$(docker compose config)
|
||||
grep -Fq 'LANGBOT_SPACE_CONTROL_PLANE_URL: https://space.langbot.app' <<<"$rendered_compose" || {
|
||||
echo 'Cloud control-plane URL must be https://space.langbot.app' >&2
|
||||
exit 4
|
||||
}
|
||||
grep -Fq 'SPACE__URL: https://space.langbot.app' <<<"$rendered_compose" || {
|
||||
echo 'Cloud user-facing Space URL must be https://space.langbot.app' >&2
|
||||
exit 5
|
||||
}
|
||||
grep -Eq 'LANGBOT_TELEMETRY_INGEST_TOKEN: .+' <<<"$rendered_compose" || {
|
||||
echo 'Cloud telemetry ingest token must be configured' >&2
|
||||
exit 6
|
||||
}
|
||||
|
||||
update_env() {
|
||||
local key=$1 value=$2
|
||||
python3 - "$key" "$value" <<'PY'
|
||||
from pathlib import Path
|
||||
import os
|
||||
import sys
|
||||
|
||||
path = Path('.env')
|
||||
key, value = sys.argv[1:]
|
||||
lines = path.read_text().splitlines()
|
||||
updated = False
|
||||
for index, line in enumerate(lines):
|
||||
if line.startswith(f'{key}='):
|
||||
lines[index] = f'{key}={value}'
|
||||
updated = True
|
||||
break
|
||||
if not updated:
|
||||
lines.append(f'{key}={value}')
|
||||
temporary = Path('.env.tmp')
|
||||
temporary.write_text('\n'.join(lines) + '\n')
|
||||
os.chmod(temporary, 0o600)
|
||||
temporary.replace(path)
|
||||
PY
|
||||
}
|
||||
update_env LANGBOT_IMAGE_TAG "$TAG"
|
||||
set -a
|
||||
. ./.env
|
||||
set +a
|
||||
: "${CLOUD_V2_CONTROL_PLANE_TOKEN:?CLOUD_V2_CONTROL_PLANE_TOKEN is required}"
|
||||
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if docker compose pull postgres redis migrate plugin-runtime core; then
|
||||
break
|
||||
fi
|
||||
if [ "$attempt" -eq 5 ]; then
|
||||
echo "docker compose pull failed after $attempt attempts" >&2
|
||||
exit 1
|
||||
fi
|
||||
delay=$((attempt * 10))
|
||||
echo "docker compose pull failed (attempt $attempt/5); retrying in ${delay}s" >&2
|
||||
sleep "$delay"
|
||||
done
|
||||
docker compose up -d postgres redis
|
||||
for _ in $(seq 1 60); do
|
||||
if docker compose exec -T postgres pg_isready -U langbot_operator -d langbot >/dev/null 2>&1; then break; fi
|
||||
sleep 2
|
||||
done
|
||||
docker compose exec -T postgres pg_isready -U langbot_operator -d langbot >/dev/null
|
||||
|
||||
docker compose exec -T postgres psql -v ON_ERROR_STOP=1 -U langbot_operator -d langbot \
|
||||
-v runtime_password="$POSTGRES_RUNTIME_PASSWORD" <<'SQL'
|
||||
SELECT format('CREATE ROLE langbot_runtime LOGIN PASSWORD %L', :'runtime_password')
|
||||
WHERE NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'langbot_runtime')\gexec
|
||||
ALTER ROLE langbot_runtime PASSWORD :'runtime_password';
|
||||
GRANT CONNECT ON DATABASE langbot TO langbot_runtime;
|
||||
REVOKE CREATE ON SCHEMA public FROM PUBLIC, langbot_runtime;
|
||||
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM langbot_runtime;
|
||||
REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM langbot_runtime;
|
||||
ALTER DEFAULT PRIVILEGES FOR ROLE langbot_operator IN SCHEMA public REVOKE ALL ON TABLES FROM langbot_runtime;
|
||||
ALTER DEFAULT PRIVILEGES FOR ROLE langbot_operator IN SCHEMA public REVOKE ALL ON SEQUENCES FROM langbot_runtime;
|
||||
GRANT USAGE ON SCHEMA public TO langbot_runtime;
|
||||
SQL
|
||||
|
||||
docker compose --profile tools run --rm migrate
|
||||
|
||||
docker compose up -d --remove-orphans plugin-runtime core
|
||||
for _ in $(seq 1 90); do
|
||||
if docker compose exec -T core python -c 'import urllib.request; urllib.request.urlopen("http://127.0.0.1:5300/healthz", timeout=3)' >/dev/null 2>&1; then
|
||||
docker compose ps
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
docker compose logs --tail=200 core plugin-runtime >&2
|
||||
exit 1
|
||||
@@ -1,162 +0,0 @@
|
||||
services:
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg17
|
||||
container_name: langbot-cloud-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: langbot
|
||||
POSTGRES_USER: langbot_operator
|
||||
POSTGRES_PASSWORD: ${POSTGRES_OPERATOR_PASSWORD}
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: [CMD-SHELL, "pg_isready -U langbot_operator -d langbot"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
networks: [internal]
|
||||
|
||||
redis:
|
||||
image: redis:7.4-alpine
|
||||
container_name: langbot-cloud-redis
|
||||
restart: unless-stopped
|
||||
command: [redis-server, --appendonly, "yes", --requirepass, "${REDIS_PASSWORD}"]
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
healthcheck:
|
||||
test: [CMD-SHELL, "redis-cli -a \"$${REDIS_PASSWORD}\" ping | grep PONG"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
environment:
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||
networks: [internal]
|
||||
|
||||
migrate:
|
||||
image: rockchin/langbot-cloud-core:${LANGBOT_IMAGE_TAG}
|
||||
profiles: [tools]
|
||||
command: [uv, run, langbot, migrate, --cloud]
|
||||
environment: &core-env
|
||||
TZ: Asia/Shanghai
|
||||
SYSTEM__INSTANCE_ID: ${CLOUD_V2_INSTANCE_UUID}
|
||||
SYSTEM__EDITION: cloud
|
||||
SYSTEM__RECOVERY_KEY: ${SYSTEM_RECOVERY_KEY}
|
||||
SYSTEM__JWT__SECRET: ${JWT_SECRET}
|
||||
SYSTEM__LIMITATION__MAX_BOTS: "2"
|
||||
SYSTEM__LIMITATION__MAX_PIPELINES: "3"
|
||||
SYSTEM__LIMITATION__MAX_EXTENSIONS: "3"
|
||||
SYSTEM__LIMITATION__MAX_KNOWLEDGE_BASES: "2"
|
||||
API__WEBHOOK_PREFIX: https://cloud.langbot.app
|
||||
API__WEBUI_URL: https://cloud.langbot.app
|
||||
WORKSPACE__INVITATIONS__PUBLIC_WEB_URL: https://cloud.langbot.app
|
||||
DATABASE__USE: postgresql
|
||||
DATABASE__POSTGRESQL__URL: postgresql+asyncpg://langbot_runtime:${POSTGRES_RUNTIME_PASSWORD}@postgres:5432/langbot
|
||||
DATABASE__CLOUD_MIGRATION__OPERATOR_DSN_ENV: LANGBOT_CLOUD_MIGRATION_DSN
|
||||
LANGBOT_CLOUD_MIGRATION_DSN: postgresql://langbot_operator:${POSTGRES_OPERATOR_PASSWORD}@postgres:5432/langbot
|
||||
VDB__USE: pgvector
|
||||
VDB__PGVECTOR__USE_BUSINESS_DATABASE: "true"
|
||||
VDB__PGVECTOR__ALLOWED_DIMENSIONS: "384,512,768,1024,1536"
|
||||
PLUGIN__ENABLE: "true"
|
||||
PLUGIN__RUNTIME_WS_URL: ws://plugin-runtime:5400/control/ws
|
||||
PLUGIN__DISPLAY_PLUGIN_DEBUG_URL: wss://cloud.langbot.app/plugin/debug/ws
|
||||
PLUGIN__WORKER__MAX_CPUS: "0.25"
|
||||
PLUGIN__WORKER__MAX_MEMORY_MB: "256"
|
||||
PLUGIN__WORKER__MAX_PIDS: "128"
|
||||
PLUGIN__WORKER__MAX_WORKERS: "16"
|
||||
PLUGIN__WORKER__MAX_TOTAL_CPUS: "4.0"
|
||||
PLUGIN__WORKER__MAX_TOTAL_MEMORY_MB: "4096"
|
||||
PLUGIN__WORKER__REQUIRE_HARD_LIMITS: "true"
|
||||
LANGBOT_PLUGIN_RUNTIME_CONTROL_TOKEN: ${PLUGIN_RUNTIME_CONTROL_TOKEN}
|
||||
# Cloud v2 currently grants no managed Box capability. Keep the shared
|
||||
# runtime deployed but disable Core integration until a hard-quota-capable
|
||||
# backend can satisfy the fail-closed Cloud readiness contract.
|
||||
BOX__ENABLED: "false"
|
||||
BOX__BACKEND: nsjail
|
||||
BOX__RUNTIME__ENDPOINT: ws://box:5410
|
||||
BOX__ADMISSION__REQUIRED: "true"
|
||||
BOX__ADMISSION__LOGICAL_SESSION_ID: global
|
||||
BOX__ADMISSION__REQUIRED_BACKEND: nsjail
|
||||
BOX__ADMISSION__MAX_SESSIONS: "1"
|
||||
BOX__ADMISSION__MAX_MANAGED_PROCESSES: "0"
|
||||
BOX__ADMISSION__CPUS: "0.25"
|
||||
BOX__ADMISSION__MEMORY_MB: "256"
|
||||
BOX__ADMISSION__WORKSPACE_QUOTA_MB: "256"
|
||||
BOX__LOCAL__HOST_ROOT: /app/data/box
|
||||
BOX__LOCAL__DEFAULT_WORKSPACE: /app/data/box
|
||||
BOX__LOCAL__ALLOWED_MOUNT_ROOTS: /app/data/box
|
||||
LANGBOT_BOX_CONTROL_TOKEN: ${BOX_CONTROL_TOKEN}
|
||||
MCP__STDIO__ENABLED: "false"
|
||||
LANGBOT_SPACE_CONTROL_PLANE_URL: https://space.langbot.app
|
||||
LANGBOT_SPACE_CONTROL_PLANE_TOKEN: ${CLOUD_V2_CONTROL_PLANE_TOKEN}
|
||||
LANGBOT_TELEMETRY_INGEST_TOKEN: ${CLOUD_V2_CONTROL_PLANE_TOKEN}
|
||||
LANGBOT_SPACE_CONTROL_PLANE_PUBLIC_KEY: ${CLOUD_V2_MANIFEST_PUBLIC_KEY}
|
||||
LANGBOT_SPACE_CONTROL_PLANE_KEY_ID: ${CLOUD_V2_MANIFEST_KEY_ID}
|
||||
SPACE__URL: https://space.langbot.app
|
||||
depends_on:
|
||||
postgres: {condition: service_healthy}
|
||||
networks: [internal]
|
||||
|
||||
plugin-runtime:
|
||||
image: rockchin/langbot:${LANGBOT_IMAGE_TAG}
|
||||
container_name: langbot-cloud-plugin-runtime
|
||||
restart: unless-stopped
|
||||
command: [uv, run, python, -m, langbot_plugin.cli.__init__, rt]
|
||||
environment:
|
||||
LANGBOT_PLUGIN_RUNTIME_CONTROL_TOKEN: ${PLUGIN_RUNTIME_CONTROL_TOKEN}
|
||||
volumes:
|
||||
- plugin-data:/app/data
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
cgroup: host
|
||||
privileged: true
|
||||
expose: ["5400"]
|
||||
networks: [internal]
|
||||
|
||||
box:
|
||||
image: rockchin/langbot:${LANGBOT_IMAGE_TAG}
|
||||
container_name: langbot-cloud-box
|
||||
restart: unless-stopped
|
||||
command: [uv, run, lbp, box, --host, 0.0.0.0, --ws-control-port, "5410"]
|
||||
environment:
|
||||
LANGBOT_BOX_CONTROL_TOKEN: ${BOX_CONTROL_TOKEN}
|
||||
LANGBOT_BOX_ROOT: /app/data/box
|
||||
volumes:
|
||||
- box-data:/app/data/box
|
||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
||||
cgroup: host
|
||||
privileged: true
|
||||
expose: ["5410"]
|
||||
networks: [internal]
|
||||
|
||||
core:
|
||||
image: rockchin/langbot-cloud-core:${LANGBOT_IMAGE_TAG}
|
||||
container_name: langbot-cloud-core
|
||||
restart: unless-stopped
|
||||
environment: *core-env
|
||||
volumes:
|
||||
- core-data:/app/data
|
||||
- box-data:/app/data/box
|
||||
depends_on:
|
||||
postgres: {condition: service_healthy}
|
||||
redis: {condition: service_healthy}
|
||||
plugin-runtime: {condition: service_started}
|
||||
box: {condition: service_started}
|
||||
expose: ["5300"]
|
||||
healthcheck:
|
||||
test: [CMD-SHELL, "python -c 'import urllib.request; urllib.request.urlopen(\"http://127.0.0.1:5300/healthz\", timeout=3)'" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
start_period: 30s
|
||||
networks: [internal, shared-network]
|
||||
|
||||
networks:
|
||||
internal:
|
||||
shared-network:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
redis-data:
|
||||
plugin-data:
|
||||
box-data:
|
||||
core-data:
|
||||
@@ -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 authenticate before protocol dispatch
|
||||
### Runtime control transports support opt-in shared-secret authentication
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
### 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@9d216208cdfb41f0cb7fcb64632e2a46816d6dc6",
|
||||
"langbot-plugin @ git+https://github.com/langbot-app/langbot-plugin-sdk.git@555a58e5db3de28e977b08dd4cd116b332848a19",
|
||||
"asyncpg>=0.30.0",
|
||||
"line-bot-sdk>=3.19.0",
|
||||
"matrix-nio>=0.25.2",
|
||||
|
||||
@@ -27,10 +27,11 @@ 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}`).
|
||||
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.
|
||||
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.
|
||||
|
||||
A Compose deployment may optionally set
|
||||
`LANGBOT_PLUGIN_RUNTIME_CONTROL_TOKEN` on both `langbot` and
|
||||
|
||||
@@ -6,7 +6,8 @@ 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).
|
||||
agents can browse and search the marketplaces (plugins, MCP servers, skills) and
|
||||
rank live models for automated setup.
|
||||
|
||||
## Endpoint
|
||||
|
||||
@@ -46,10 +47,12 @@ 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/*` and is read/browse only.
|
||||
`/api/v1/marketplace/*`; `select_models` mirrors `/api/v1/models/selection`.
|
||||
All tools are read-only.
|
||||
|
||||
## How to use
|
||||
|
||||
@@ -58,12 +61,16 @@ Authorization: Bearer lbpat_...uests without a valid PAT get `401 Unauthorized`.
|
||||
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`) directly.
|
||||
(`PluginService`, `MCPService`, `SkillService`, `ModelStatusService`) directly.
|
||||
- Mount: `internal/controller/api.go` at `/mcp` and `/mcp/*any`.
|
||||
- Auth: PAT via `AccountService.ValidatePersonalAccessToken`.
|
||||
- Docs: `docs/MCP_SERVER.md`.
|
||||
|
||||
@@ -398,7 +398,16 @@ 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', 'http://localhost:5401')
|
||||
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()
|
||||
|
||||
return self.success(
|
||||
data={
|
||||
|
||||
@@ -367,6 +367,12 @@ 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:
|
||||
@@ -376,19 +382,19 @@ class BoxRuntimeConnector(ManagedRuntimeConnector):
|
||||
return self._control_token
|
||||
|
||||
def get_control_headers(self) -> dict[str, str]:
|
||||
"""Headers for the instance-authenticated RPC control handshake."""
|
||||
"""Return instance-scoped RPC headers and the optional shared secret."""
|
||||
|
||||
self._ensure_control_token(allow_generate=False)
|
||||
return {
|
||||
BOX_CONTROL_TOKEN_HEADER: self._control_token,
|
||||
BOX_INSTANCE_HEADER: self._trusted_instance_uuid,
|
||||
}
|
||||
headers = {BOX_INSTANCE_HEADER: self._trusted_instance_uuid}
|
||||
if self._control_token:
|
||||
headers[BOX_CONTROL_TOKEN_HEADER] = self._control_token
|
||||
return headers
|
||||
|
||||
def get_relay_headers(
|
||||
self,
|
||||
action_context: ActionContext,
|
||||
) -> dict[str, str]:
|
||||
"""Return authenticated, placement-scoped relay handshake headers."""
|
||||
"""Return instance- and placement-scoped relay handshake headers."""
|
||||
|
||||
context = ActionContext.model_validate(action_context).without_installation()
|
||||
if context.instance_uuid != self._trusted_instance_uuid:
|
||||
|
||||
@@ -249,6 +249,10 @@ class Application:
|
||||
{},
|
||||
)
|
||||
),
|
||||
'plugin_runtime_connected': bool(
|
||||
self.plugin_connector is not None
|
||||
and getattr(self.plugin_connector, '_runtime_available', lambda: False)()
|
||||
),
|
||||
}
|
||||
mcp_loader = getattr(self.tool_mgr, 'mcp_tool_loader', None)
|
||||
runtime_stats.update(
|
||||
|
||||
@@ -264,6 +264,11 @@ 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=120,
|
||||
timeout=300,
|
||||
)
|
||||
|
||||
async def apply_plugin_installation(
|
||||
@@ -1962,11 +1962,16 @@ 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=execution_context,
|
||||
action_context=action_context,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@@ -328,8 +328,9 @@ box:
|
||||
enabled: true
|
||||
backend: 'local' # 'local' (Docker/nsjail), 'docker', 'nsjail', or 'e2b'. Can be written via BOX__BACKEND.
|
||||
runtime:
|
||||
# External WebSocket runtimes also require LANGBOT_BOX_CONTROL_TOKEN in
|
||||
# both LangBot and Box. Keep the shared secret out of this config file.
|
||||
# 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.
|
||||
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
|
||||
|
||||
@@ -235,13 +235,24 @@ 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': 'http://localhost:5401',
|
||||
'debug_url': 'ws://localhost:5401/plugin/debug/ws',
|
||||
'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
|
||||
|
||||
@@ -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 = ''):
|
||||
def make_app(logger: Mock, runtime_endpoint: str = '', *, cloud: bool = False):
|
||||
return SimpleNamespace(
|
||||
logger=logger,
|
||||
workspace_service=SimpleNamespace(instance_uuid='instance-a'),
|
||||
@@ -42,6 +42,7 @@ def make_app(logger: Mock, runtime_endpoint: str = ''):
|
||||
}
|
||||
}
|
||||
),
|
||||
deployment=SimpleNamespace(mode='cloud' if cloud else 'oss'),
|
||||
)
|
||||
|
||||
|
||||
@@ -306,10 +307,27 @@ def test_box_runtime_connector_rejects_relay_context_from_other_instance(
|
||||
)
|
||||
|
||||
|
||||
def test_external_box_runtime_fails_closed_without_control_token(monkeypatch: pytest.MonkeyPatch):
|
||||
def test_external_box_runtime_control_headers_are_tokenless_when_secret_is_unset(
|
||||
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,7 +87,10 @@ 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()})
|
||||
app.plugin_connector = SimpleNamespace(
|
||||
_known_desired_states={'installation': object()},
|
||||
_runtime_available=lambda: True,
|
||||
)
|
||||
app.persistence_mgr = SimpleNamespace(
|
||||
get_resource_stats=lambda: {
|
||||
'configured_capacity': 20,
|
||||
@@ -140,3 +143,4 @@ async def test_runtime_resource_stats_are_aggregate_and_constant_time() -> None:
|
||||
}
|
||||
assert stats['models']['providers'] == 1
|
||||
assert stats['runtimes']['plugin_installations'] == 1
|
||||
assert stats['runtimes']['plugin_runtime_connected'] is True
|
||||
|
||||
@@ -15,7 +15,7 @@ from langbot_plugin.runtime.security import (
|
||||
)
|
||||
|
||||
|
||||
def make_connector() -> PluginRuntimeConnector:
|
||||
def make_connector(*, cloud: bool = False) -> PluginRuntimeConnector:
|
||||
app = SimpleNamespace(
|
||||
logger=Mock(),
|
||||
instance_config=SimpleNamespace(
|
||||
@@ -34,6 +34,7 @@ def make_connector() -> PluginRuntimeConnector:
|
||||
'space': {'url': ''},
|
||||
}
|
||||
),
|
||||
deployment=SimpleNamespace(mode='cloud' if cloud else 'oss'),
|
||||
)
|
||||
return PluginRuntimeConnector(app, AsyncMock())
|
||||
|
||||
@@ -332,6 +333,14 @@ def test_external_runtime_control_headers_are_empty_when_secret_is_unset(monkeyp
|
||||
assert connector._control_headers(allow_generate=False) == {}
|
||||
|
||||
|
||||
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 PluginToRuntimeAction
|
||||
from langbot_plugin.entities.io.context import ActionContext, InstallationBinding
|
||||
from langbot_plugin.entities.io.actions.enums import LangBotToRuntimeAction, PluginToRuntimeAction
|
||||
from langbot_plugin.entities.io.context import ActionContext, InstallationBinding, PluginInstallationDesiredState
|
||||
|
||||
|
||||
def make_handler(app):
|
||||
@@ -67,6 +67,20 @@ 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,3 +444,23 @@ 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" },
|
||||
{ name = "cuda-pathfinder", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
]
|
||||
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" },
|
||||
{ name = "nvidia-cuda-runtime", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
cufft = [
|
||||
{ name = "nvidia-cufft" },
|
||||
{ name = "nvidia-cufft", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
cufile = [
|
||||
{ name = "nvidia-cufile" },
|
||||
{ name = "nvidia-cufile", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
cupti = [
|
||||
{ name = "nvidia-cuda-cupti" },
|
||||
{ name = "nvidia-cuda-cupti", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
curand = [
|
||||
{ name = "nvidia-curand" },
|
||||
{ name = "nvidia-curand", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
cusolver = [
|
||||
{ name = "nvidia-cusolver" },
|
||||
{ name = "nvidia-cusolver", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
cusparse = [
|
||||
{ name = "nvidia-cusparse" },
|
||||
{ name = "nvidia-cusparse", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
nvjitlink = [
|
||||
{ name = "nvidia-nvjitlink" },
|
||||
{ name = "nvidia-nvjitlink", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
nvrtc = [
|
||||
{ name = "nvidia-cuda-nvrtc" },
|
||||
{ name = "nvidia-cuda-nvrtc", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
nvtx = [
|
||||
{ name = "nvidia-nvtx" },
|
||||
{ name = "nvidia-nvtx", marker = "python_full_version >= '3.14' and sys_platform == 'linux'" },
|
||||
]
|
||||
|
||||
[[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=9d216208cdfb41f0cb7fcb64632e2a46816d6dc6" },
|
||||
{ name = "langbot-plugin", git = "https://github.com/langbot-app/langbot-plugin-sdk.git?rev=555a58e5db3de28e977b08dd4cd116b332848a19" },
|
||||
{ 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.0"
|
||||
source = { git = "https://github.com/langbot-app/langbot-plugin-sdk.git?rev=9d216208cdfb41f0cb7fcb64632e2a46816d6dc6#9d216208cdfb41f0cb7fcb64632e2a46816d6dc6" }
|
||||
version = "0.5.3"
|
||||
source = { git = "https://github.com/langbot-app/langbot-plugin-sdk.git?rev=555a58e5db3de28e977b08dd4cd116b332848a19#555a58e5db3de28e977b08dd4cd116b332848a19" }
|
||||
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" },
|
||||
{ name = "nvidia-cuda-nvrtc", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
]
|
||||
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" },
|
||||
{ name = "nvidia-cublas", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
]
|
||||
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" },
|
||||
{ name = "nvidia-nvjitlink", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
]
|
||||
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" },
|
||||
{ name = "nvidia-cusparse" },
|
||||
{ name = "nvidia-nvjitlink" },
|
||||
{ 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'" },
|
||||
]
|
||||
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" },
|
||||
{ name = "nvidia-nvjitlink", marker = "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
]
|
||||
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" },
|
||||
{ name = "numpy" },
|
||||
{ name = "scipy" },
|
||||
{ name = "threadpoolctl" },
|
||||
{ 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'" },
|
||||
]
|
||||
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" },
|
||||
{ name = "numpy", marker = "python_full_version >= '3.14'" },
|
||||
]
|
||||
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" },
|
||||
{ name = "numpy" },
|
||||
{ name = "scikit-learn" },
|
||||
{ name = "scipy" },
|
||||
{ name = "torch" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "transformers" },
|
||||
{ name = "typing-extensions" },
|
||||
{ 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'" },
|
||||
]
|
||||
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 = "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" },
|
||||
{ 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'" },
|
||||
]
|
||||
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" },
|
||||
{ name = "numpy" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "regex" },
|
||||
{ name = "safetensors" },
|
||||
{ name = "tokenizers" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "typer" },
|
||||
{ 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'" },
|
||||
]
|
||||
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" },
|
||||
{ name = "protobuf" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "anyio", marker = "sys_platform != 'win32'" },
|
||||
{ name = "protobuf", marker = "sys_platform != 'win32'" },
|
||||
{ name = "sniffio", marker = "sys_platform != 'win32'" },
|
||||
]
|
||||
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 = [
|
||||
|
||||
@@ -31,27 +31,14 @@ const getFormSchema = (t: (key: string) => string) =>
|
||||
api_key: z.string().optional(),
|
||||
});
|
||||
|
||||
export interface ProviderFormInitialValues {
|
||||
name?: string;
|
||||
requester?: string;
|
||||
base_url?: string;
|
||||
api_key?: string;
|
||||
}
|
||||
|
||||
interface ProviderFormProps {
|
||||
providerId?: string;
|
||||
initialValues?: ProviderFormInitialValues;
|
||||
onValuesChange?: (values: ProviderFormInitialValues) => void;
|
||||
submitButtonText?: string;
|
||||
onFormSubmit: (providerUuid?: string) => void;
|
||||
onFormSubmit: () => void;
|
||||
onFormCancel: () => void;
|
||||
}
|
||||
|
||||
export default function ProviderForm({
|
||||
providerId,
|
||||
initialValues,
|
||||
onValuesChange,
|
||||
submitButtonText,
|
||||
onFormSubmit,
|
||||
onFormCancel,
|
||||
}: ProviderFormProps) {
|
||||
@@ -61,22 +48,13 @@ export default function ProviderForm({
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: initialValues?.name ?? '',
|
||||
requester: initialValues?.requester ?? '',
|
||||
base_url: initialValues?.base_url ?? '',
|
||||
api_key: initialValues?.api_key ?? '',
|
||||
name: '',
|
||||
requester: '',
|
||||
base_url: '',
|
||||
api_key: '',
|
||||
},
|
||||
});
|
||||
const { setValue, watch } = form;
|
||||
|
||||
// Report form values changes to parent
|
||||
useEffect(() => {
|
||||
if (!onValuesChange) return;
|
||||
const subscription = watch((values) => {
|
||||
onValuesChange(values as ProviderFormInitialValues);
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [watch, onValuesChange]);
|
||||
const { setValue } = form;
|
||||
|
||||
const [requesterList, setRequesterList] = useState<
|
||||
{
|
||||
@@ -196,12 +174,11 @@ export default function ProviderForm({
|
||||
if (providerId) {
|
||||
await httpClient.updateModelProvider(providerId, data);
|
||||
toast.success(t('models.providerSaved'));
|
||||
onFormSubmit();
|
||||
} else {
|
||||
const resp = await httpClient.createModelProvider(data);
|
||||
await httpClient.createModelProvider(data);
|
||||
toast.success(t('models.providerCreated'));
|
||||
onFormSubmit(resp.uuid);
|
||||
}
|
||||
onFormSubmit();
|
||||
} catch (err) {
|
||||
toast.error(t('models.providerSaveError') + (err as CustomApiError).msg);
|
||||
}
|
||||
@@ -401,7 +378,7 @@ export default function ProviderForm({
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="submit">{submitButtonText || t('common.save')}</Button>
|
||||
<Button type="submit">{t('common.save')}</Button>
|
||||
<Button type="button" variant="outline" onClick={onFormCancel}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
XCircle,
|
||||
} from 'lucide-react';
|
||||
import QRCode from 'qrcode';
|
||||
import { getActiveWorkspaceUuid } from '@/app/infra/http/workspaceContext';
|
||||
|
||||
export type QrLoginPlatform =
|
||||
| 'feishu'
|
||||
@@ -55,12 +56,12 @@ const PLATFORM_CONFIGS: Record<QrLoginPlatform, PlatformConfig> = {
|
||||
},
|
||||
weixin: {
|
||||
titleKey: 'weixin.scanLogin',
|
||||
connectingKey: 'feishu.connecting',
|
||||
connectingKey: 'weixin.connecting',
|
||||
scanQRCodeKey: 'weixin.scanQRCode',
|
||||
waitingKey: 'feishu.waitingForScan',
|
||||
waitingKey: 'weixin.waitingForScan',
|
||||
successKey: 'weixin.loginSuccess',
|
||||
failedKey: 'weixin.loginFailed',
|
||||
retryKey: 'feishu.retry',
|
||||
retryKey: 'weixin.retry',
|
||||
apiBase: '/api/v1/platform/adapters/weixin/login',
|
||||
extractSuccess: (data) => ({
|
||||
token: data.token,
|
||||
@@ -146,6 +147,8 @@ 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);
|
||||
|
||||
@@ -180,18 +183,23 @@ export default function QrCodeLoginDialog({
|
||||
}
|
||||
if (sessionIdRef.current) {
|
||||
const token = localStorage.getItem('token');
|
||||
const baseUrl =
|
||||
import.meta.env.VITE_API_BASE_URL || window.location.origin;
|
||||
const workspaceUuid = sessionWorkspaceUuidRef.current;
|
||||
fetch(
|
||||
`${baseUrl}${platformConfigRef.current.apiBase}/${sessionIdRef.current}`,
|
||||
`${baseUrlRef.current}${sessionApiBaseRef.current}/${sessionIdRef.current}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
...(workspaceUuid ? { 'X-Workspace-Id': workspaceUuid } : {}),
|
||||
},
|
||||
keepalive: true,
|
||||
},
|
||||
).catch(() => {});
|
||||
sessionIdRef.current = null;
|
||||
}
|
||||
sessionWorkspaceUuidRef.current = null;
|
||||
sessionApiBaseRef.current = '';
|
||||
baseUrlRef.current = '';
|
||||
}, []);
|
||||
|
||||
const startLogin = useCallback(async () => {
|
||||
@@ -204,6 +212,7 @@ 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;
|
||||
@@ -214,7 +223,10 @@ export default function QrCodeLoginDialog({
|
||||
|
||||
const res = await fetch(`${baseUrl}${cfg.apiBase}`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
...(workspaceUuid ? { 'X-Workspace-Id': workspaceUuid } : {}),
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
@@ -225,6 +237,8 @@ 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);
|
||||
@@ -270,11 +284,19 @@ export default function QrCodeLoginDialog({
|
||||
`${baseUrlRef.current}${cfg.apiBase}/${sessionIdRef.current}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
...(workspaceUuid
|
||||
? { 'X-Workspace-Id': workspaceUuid }
|
||||
: {}),
|
||||
},
|
||||
keepalive: true,
|
||||
},
|
||||
).catch(() => {});
|
||||
sessionIdRef.current = null;
|
||||
sessionWorkspaceUuidRef.current = null;
|
||||
sessionApiBaseRef.current = '';
|
||||
baseUrlRef.current = '';
|
||||
}
|
||||
setState('expired');
|
||||
}
|
||||
@@ -286,7 +308,12 @@ export default function QrCodeLoginDialog({
|
||||
try {
|
||||
const pollRes = await fetch(
|
||||
`${baseUrl}${cfg.apiBase}/status/${session_id}`,
|
||||
{ headers: { Authorization: `Bearer ${token}` } },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
...(workspaceUuid ? { 'X-Workspace-Id': workspaceUuid } : {}),
|
||||
},
|
||||
},
|
||||
);
|
||||
if (!pollRes.ok) return;
|
||||
|
||||
|
||||
+66
-725
@@ -12,13 +12,10 @@ import {
|
||||
Loader2,
|
||||
X,
|
||||
ExternalLink,
|
||||
Rocket,
|
||||
Wrench,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import {
|
||||
userInfo,
|
||||
systemInfo,
|
||||
bootstrapWorkspaceSession,
|
||||
initializeSystemInfo,
|
||||
@@ -40,7 +37,6 @@ import {
|
||||
parseDynamicFormItemType,
|
||||
} from '@/app/home/components/dynamic-form/DynamicFormItemConfig';
|
||||
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
||||
import ProviderForm from '@/app/home/components/models-dialog/component/provider-form/ProviderForm';
|
||||
import { BotLogListComponent } from '@/app/home/bots/components/bot-log/view/BotLogListComponent';
|
||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||
import {
|
||||
@@ -51,7 +47,6 @@ 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,
|
||||
@@ -71,7 +66,6 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -97,40 +91,6 @@ export default function WizardPage() {
|
||||
{},
|
||||
);
|
||||
const [runnerConfig, setRunnerConfig] = useState<Record<string, unknown>>({});
|
||||
const [aiEngineMode, setAiEngineMode] = useState<
|
||||
'orchestrated' | 'llm' | null
|
||||
>(null);
|
||||
const [modelSource, setModelSource] = useState<'space' | 'custom' | null>(
|
||||
null,
|
||||
);
|
||||
const [providerCreated, setProviderCreated] = useState(false);
|
||||
const [createdProviderUuid, setCreatedProviderUuid] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [modelsAdded, setModelsAdded] = useState(false);
|
||||
// Sub-layer within Step 2: 0=main, 1=sub-choice, 2=provider, 3=scan, 4=config
|
||||
const [step2Layer, setStep2Layer] = useState(0);
|
||||
const [maxReachedLayer, setMaxReachedLayer] = useState(0);
|
||||
|
||||
const goToStep2Layer = useCallback(
|
||||
(layer: number) => {
|
||||
setStep2Layer(layer);
|
||||
setMaxReachedLayer((prev) => Math.max(prev, layer));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// Saved provider form data for back navigation
|
||||
const [savedProviderForm, setSavedProviderForm] = useState<{
|
||||
name?: string;
|
||||
requester?: string;
|
||||
base_url?: string;
|
||||
api_key?: string;
|
||||
}>({});
|
||||
// Saved selected scanned models for back navigation
|
||||
const [savedSelectedModels, setSavedSelectedModels] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
const [createdBotUuid, setCreatedBotUuid] = useState<string | null>(null);
|
||||
const [webhookUrl, setWebhookUrl] = useState<string>('');
|
||||
const [extraWebhookUrl, setExtraWebhookUrl] = useState<string>('');
|
||||
@@ -312,28 +272,11 @@ export default function WizardPage() {
|
||||
const handleSelectRunner = useCallback(
|
||||
(runner: string) => {
|
||||
setSelectedRunner(runner);
|
||||
goToStep2Layer(4);
|
||||
saveProgress({ step: 2, selected_runner: runner });
|
||||
},
|
||||
[saveProgress],
|
||||
);
|
||||
|
||||
const handleAiEngineModeSelect = useCallback(
|
||||
(mode: 'orchestrated' | 'llm') => {
|
||||
setAiEngineMode(mode);
|
||||
setSelectedRunner(null);
|
||||
setModelSource(null);
|
||||
setProviderCreated(false);
|
||||
setCreatedProviderUuid(null);
|
||||
setModelsAdded(false);
|
||||
setSavedProviderForm({});
|
||||
setSavedSelectedModels(new Set());
|
||||
setMaxReachedLayer(0);
|
||||
goToStep2Layer(1);
|
||||
},
|
||||
[goToStep2Layer],
|
||||
);
|
||||
|
||||
// ---- Navigation helpers ----
|
||||
|
||||
const canProceed = useCallback((): boolean => {
|
||||
@@ -343,53 +286,27 @@ export default function WizardPage() {
|
||||
case 1:
|
||||
return createdBotUuid !== null && botSaved;
|
||||
case 2:
|
||||
if (aiEngineMode === 'orchestrated') {
|
||||
return selectedRunner !== null;
|
||||
}
|
||||
if (aiEngineMode === 'llm') {
|
||||
return (
|
||||
modelSource === 'space' ||
|
||||
(modelSource === 'custom' && modelsAdded)
|
||||
);
|
||||
}
|
||||
return false;
|
||||
return selectedRunner !== null;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}, [
|
||||
currentStep,
|
||||
selectedAdapter,
|
||||
createdBotUuid,
|
||||
botSaved,
|
||||
aiEngineMode,
|
||||
selectedRunner,
|
||||
modelSource,
|
||||
modelsAdded,
|
||||
]);
|
||||
}, [currentStep, selectedAdapter, createdBotUuid, botSaved, selectedRunner]);
|
||||
|
||||
const goNext = useCallback(() => {
|
||||
if (currentStep === 2 && step2Layer < maxReachedLayer) {
|
||||
setStep2Layer(maxReachedLayer);
|
||||
return;
|
||||
}
|
||||
if (currentStep < TOTAL_STEPS - 1 && canProceed()) {
|
||||
const nextStep = currentStep + 1;
|
||||
setCurrentStep(nextStep);
|
||||
saveProgress({ step: nextStep });
|
||||
}
|
||||
}, [currentStep, step2Layer, maxReachedLayer, canProceed, saveProgress]);
|
||||
}, [currentStep, canProceed, saveProgress]);
|
||||
|
||||
const goPrev = useCallback(() => {
|
||||
if (currentStep === 2 && step2Layer > 0) {
|
||||
setStep2Layer(step2Layer - 1);
|
||||
return;
|
||||
}
|
||||
if (currentStep > 0) {
|
||||
const prevStep = currentStep - 1;
|
||||
setCurrentStep(prevStep);
|
||||
saveProgress({ step: prevStep });
|
||||
}
|
||||
}, [currentStep, step2Layer, saveProgress]);
|
||||
}, [currentStep, saveProgress]);
|
||||
|
||||
// ---- Create Bot (Step 0) ----
|
||||
// Creates a disabled bot using the adapter label as name.
|
||||
@@ -572,40 +489,6 @@ export default function WizardPage() {
|
||||
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]);
|
||||
|
||||
const handleModelSourceSelect = useCallback(
|
||||
(source: 'space' | 'custom') => {
|
||||
setModelSource(source);
|
||||
if (source === 'space') {
|
||||
handleSpaceAuth();
|
||||
} else if (providerCreated && modelsAdded) {
|
||||
goToStep2Layer(4);
|
||||
} else if (providerCreated) {
|
||||
goToStep2Layer(3);
|
||||
} else {
|
||||
goToStep2Layer(2);
|
||||
}
|
||||
},
|
||||
[handleSpaceAuth, providerCreated, modelsAdded],
|
||||
);
|
||||
|
||||
// ---- 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);
|
||||
@@ -758,41 +641,8 @@ export default function WizardPage() {
|
||||
{currentStep === 2 && (
|
||||
<StepAIEngine
|
||||
runnerOptions={runnerOptions}
|
||||
step2Layer={step2Layer}
|
||||
aiEngineMode={aiEngineMode}
|
||||
onAiEngineModeSelect={handleAiEngineModeSelect}
|
||||
selectedRunner={selectedRunner}
|
||||
onSelectRunner={handleSelectRunner}
|
||||
isLocalAccount={isLocalAccount}
|
||||
onSpaceAuth={handleSpaceAuth}
|
||||
modelSource={modelSource}
|
||||
onModelSourceSelect={handleModelSourceSelect}
|
||||
providerCreated={providerCreated}
|
||||
createdProviderUuid={createdProviderUuid}
|
||||
modelsAdded={modelsAdded}
|
||||
onProviderCreated={(uuid) => {
|
||||
setProviderCreated(true);
|
||||
setCreatedProviderUuid(uuid ?? null);
|
||||
goToStep2Layer(3);
|
||||
}}
|
||||
onModelsAdded={() => {
|
||||
setModelsAdded(true);
|
||||
setSelectedRunner('local-agent');
|
||||
goToStep2Layer(4);
|
||||
saveProgress({ step: 2, selected_runner: 'local-agent' });
|
||||
}}
|
||||
onResetModelSource={() => setModelSource(null)}
|
||||
savedProviderForm={savedProviderForm}
|
||||
onSavedProviderFormChange={setSavedProviderForm}
|
||||
savedSelectedModels={savedSelectedModels}
|
||||
onSavedSelectedModelsChange={setSavedSelectedModels}
|
||||
onGoToNextLayer={() => {
|
||||
if (modelsAdded) {
|
||||
goToStep2Layer(4);
|
||||
} else {
|
||||
goToStep2Layer(3);
|
||||
}
|
||||
}}
|
||||
selected={selectedRunner}
|
||||
onSelect={handleSelectRunner}
|
||||
runnerConfigItems={selectedRunnerConfigItems}
|
||||
runnerConfigValues={runnerConfig}
|
||||
onRunnerConfigChange={setRunnerConfig}
|
||||
@@ -1127,354 +977,21 @@ function StepBotConfig({
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Model scan sub-component (used within Step 2)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function WizardModelScan({
|
||||
providerUuid,
|
||||
initialSelectedModels,
|
||||
onSelectedModelsChange,
|
||||
onModelsAdded,
|
||||
}: {
|
||||
providerUuid: string | null;
|
||||
initialSelectedModels: Set<string>;
|
||||
onSelectedModelsChange: (v: Set<string>) => void;
|
||||
onModelsAdded: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [scanning, setScanning] = useState(false);
|
||||
const [scannedModels, setScannedModels] = useState<
|
||||
{ name: string; context_length?: number | null }[]
|
||||
>([]);
|
||||
const [selectedModels, setSelectedModels] = useState<Set<string>>(
|
||||
initialSelectedModels,
|
||||
);
|
||||
const [adding, setAdding] = useState(false);
|
||||
const [scanDone, setScanDone] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [manualModelName, setManualModelName] = useState('');
|
||||
const [manualAdding, setManualAdding] = useState(false);
|
||||
const [addedModelNames, setAddedModelNames] = useState<Set<string>>(
|
||||
new Set(initialSelectedModels),
|
||||
);
|
||||
|
||||
// Auto-scan on mount
|
||||
useEffect(() => {
|
||||
if (!providerUuid) return;
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
setScanning(true);
|
||||
try {
|
||||
const resp = await httpClient.scanProviderModels(
|
||||
providerUuid,
|
||||
'llm',
|
||||
);
|
||||
if (!cancelled) {
|
||||
setScannedModels(resp.models ?? []);
|
||||
setScanDone(true);
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) setScanDone(true);
|
||||
} finally {
|
||||
if (!cancelled) setScanning(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [providerUuid]);
|
||||
|
||||
const filteredModels = searchQuery
|
||||
? scannedModels.filter((m) =>
|
||||
m.name.toLowerCase().includes(searchQuery.toLowerCase()),
|
||||
)
|
||||
: scannedModels;
|
||||
|
||||
const toggleModel = (name: string) => {
|
||||
setSelectedModels((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(name)) next.delete(name);
|
||||
else next.add(name);
|
||||
onSelectedModelsChange(next);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const getExistingModelNames = async (): Promise<Set<string>> => {
|
||||
if (!providerUuid) return new Set();
|
||||
try {
|
||||
const resp = await httpClient.getProviderLLMModels(providerUuid);
|
||||
return new Set((resp.models ?? []).map((m: { name: string }) => m.name));
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddSelected = async () => {
|
||||
if (!providerUuid || selectedModels.size === 0) return;
|
||||
setAdding(true);
|
||||
try {
|
||||
const existing = await getExistingModelNames();
|
||||
let added = 0;
|
||||
for (const name of selectedModels) {
|
||||
if (existing.has(name)) continue;
|
||||
const model = scannedModels.find((m) => m.name === name);
|
||||
await httpClient.createProviderLLMModel({
|
||||
name,
|
||||
provider_uuid: providerUuid,
|
||||
abilities: (model as { abilities?: string[] })?.abilities || ['llm'],
|
||||
reasoning_config: { level: 'provider_default' },
|
||||
context_length: model?.context_length ?? null,
|
||||
extra_args: {},
|
||||
} as never);
|
||||
existing.add(name);
|
||||
added++;
|
||||
}
|
||||
if (added > 0) {
|
||||
toast.success(t('wizard.provider.modelsAdded', { count: added }));
|
||||
}
|
||||
setAddedModelNames((prev) => {
|
||||
const next = new Set(prev);
|
||||
for (const name of selectedModels) next.add(name);
|
||||
return next;
|
||||
});
|
||||
setSelectedModels(new Set());
|
||||
onSelectedModelsChange(new Set());
|
||||
} catch {
|
||||
toast.error(t('wizard.provider.modelsAddError'));
|
||||
} finally {
|
||||
setAdding(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleManualAdd = async () => {
|
||||
const name = manualModelName.trim();
|
||||
if (!providerUuid || !name) return;
|
||||
setManualAdding(true);
|
||||
try {
|
||||
const existing = await getExistingModelNames();
|
||||
if (existing.has(name)) {
|
||||
toast.error(t('wizard.provider.modelAlreadyExists', { name }));
|
||||
setManualAdding(false);
|
||||
return;
|
||||
}
|
||||
await httpClient.createProviderLLMModel({
|
||||
name,
|
||||
provider_uuid: providerUuid,
|
||||
abilities: ['llm'],
|
||||
reasoning_config: { level: 'provider_default' },
|
||||
context_length: null,
|
||||
extra_args: {},
|
||||
} as never);
|
||||
toast.success(t('wizard.provider.modelsAdded', { count: 1 }));
|
||||
setAddedModelNames((prev) => new Set(prev).add(name));
|
||||
setManualModelName('');
|
||||
} catch {
|
||||
toast.error(t('wizard.provider.modelsAddError'));
|
||||
} finally {
|
||||
setManualAdding(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto w-full animate-in fade-in slide-in-from-bottom-2 duration-300">
|
||||
<div className="text-center mb-6">
|
||||
<h2 className="text-xl font-semibold">
|
||||
{t('wizard.provider.scanTitle')}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t('wizard.provider.scanDescription')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg p-6 bg-card space-y-4">
|
||||
{scanning && (
|
||||
<div className="flex items-center justify-center gap-2 py-8 text-muted-foreground">
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
<span>{t('wizard.provider.scanning')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!scanning && scanDone && (
|
||||
<>
|
||||
{/* Search input for scanned models */}
|
||||
{scannedModels.length > 0 && (
|
||||
<Input
|
||||
placeholder={t('wizard.provider.searchModels')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="mb-2"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Scanned models list */}
|
||||
{scannedModels.length > 0 ? (
|
||||
<>
|
||||
<div className="max-h-48 overflow-y-auto space-y-2">
|
||||
{filteredModels.map((model) => {
|
||||
const isAdded = addedModelNames.has(model.name);
|
||||
return (
|
||||
<label
|
||||
key={model.name}
|
||||
className={cn(
|
||||
'flex items-center gap-3 p-3 rounded-lg border transition-colors',
|
||||
isAdded
|
||||
? 'opacity-50 cursor-not-allowed bg-muted/50'
|
||||
: 'cursor-pointer hover:bg-accent',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedModels.has(model.name) || isAdded}
|
||||
disabled={isAdded}
|
||||
onChange={() => toggleModel(model.name)}
|
||||
className="w-4 h-4 rounded"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-sm font-medium truncate block">
|
||||
{model.name}
|
||||
</span>
|
||||
{model.context_length && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
ctx: {model.context_length.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{isAdded && (
|
||||
<span className="text-xs text-green-600 font-medium shrink-0">
|
||||
{t('wizard.provider.alreadyAdded')}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
{filteredModels.length === 0 && (
|
||||
<p className="text-sm text-muted-foreground text-center py-4">
|
||||
{t('wizard.provider.noMatch')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-2">
|
||||
<Button
|
||||
onClick={handleAddSelected}
|
||||
disabled={selectedModels.size === 0 || adding}
|
||||
className="flex-1"
|
||||
>
|
||||
{adding && (
|
||||
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
|
||||
)}
|
||||
{t('wizard.provider.addSelected', {
|
||||
count: selectedModels.size,
|
||||
})}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={onModelsAdded}>
|
||||
{t('wizard.provider.skipModelAdd')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-4 text-muted-foreground">
|
||||
<p>{t('wizard.provider.noModelsFound')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Divider */}
|
||||
<div className="flex items-center gap-3 py-2">
|
||||
<div className="flex-1 h-px bg-border" />
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t('wizard.provider.orDivider')}
|
||||
</span>
|
||||
<div className="flex-1 h-px bg-border" />
|
||||
</div>
|
||||
|
||||
{/* Manual add */}
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder={t('wizard.provider.manualModelPlaceholder')}
|
||||
value={manualModelName}
|
||||
onChange={(e) => setManualModelName(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleManualAdd()}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleManualAdd}
|
||||
disabled={!manualModelName.trim() || manualAdding}
|
||||
>
|
||||
{manualAdding && (
|
||||
<Loader2 className="w-4 h-4 mr-1.5 animate-spin" />
|
||||
)}
|
||||
{t('wizard.provider.addManual')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Next button when models have been added */}
|
||||
{addedModelNames.size > 0 && (
|
||||
<div className="flex justify-end pt-2 border-t">
|
||||
<Button onClick={onModelsAdded}>
|
||||
{t('wizard.next')}
|
||||
<ArrowRight className="w-4 h-4 ml-1.5" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Step 2: Select & Configure AI Engine
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function StepAIEngine({
|
||||
runnerOptions,
|
||||
step2Layer,
|
||||
aiEngineMode,
|
||||
onAiEngineModeSelect,
|
||||
selectedRunner,
|
||||
onSelectRunner,
|
||||
isLocalAccount,
|
||||
onSpaceAuth,
|
||||
modelSource,
|
||||
onModelSourceSelect,
|
||||
providerCreated,
|
||||
createdProviderUuid,
|
||||
modelsAdded,
|
||||
onProviderCreated,
|
||||
onModelsAdded,
|
||||
onResetModelSource,
|
||||
savedProviderForm,
|
||||
onSavedProviderFormChange,
|
||||
savedSelectedModels,
|
||||
onSavedSelectedModelsChange,
|
||||
onGoToNextLayer,
|
||||
selected,
|
||||
onSelect,
|
||||
runnerConfigItems,
|
||||
runnerConfigValues,
|
||||
onRunnerConfigChange,
|
||||
}: {
|
||||
runnerOptions: { name: string; label: { en_US: string; zh_Hans: string } }[];
|
||||
step2Layer: number;
|
||||
aiEngineMode: 'orchestrated' | 'llm' | null;
|
||||
onAiEngineModeSelect: (mode: 'orchestrated' | 'llm') => void;
|
||||
selectedRunner: string | null;
|
||||
onSelectRunner: (name: string) => void;
|
||||
isLocalAccount: boolean;
|
||||
onSpaceAuth: () => void;
|
||||
modelSource: 'space' | 'custom' | null;
|
||||
onModelSourceSelect: (source: 'space' | 'custom') => void;
|
||||
providerCreated: boolean;
|
||||
createdProviderUuid: string | null;
|
||||
modelsAdded: boolean;
|
||||
onProviderCreated: (uuid?: string) => void;
|
||||
onModelsAdded: () => void;
|
||||
onResetModelSource: () => void;
|
||||
savedProviderForm: { name?: string; requester?: string; base_url?: string; api_key?: string };
|
||||
onSavedProviderFormChange: (v: { name?: string; requester?: string; base_url?: string; api_key?: string }) => void;
|
||||
savedSelectedModels: Set<string>;
|
||||
onSavedSelectedModelsChange: (v: Set<string>) => void;
|
||||
onGoToNextLayer: () => void;
|
||||
selected: string | null;
|
||||
onSelect: (name: string) => void;
|
||||
runnerConfigItems: IDynamicFormItemSchema[];
|
||||
runnerConfigValues: Record<string, unknown>;
|
||||
onRunnerConfigChange: (v: Record<string, unknown>) => void;
|
||||
@@ -1490,19 +1007,14 @@ function StepAIEngine({
|
||||
);
|
||||
|
||||
const runnerLabel = useMemo(() => {
|
||||
const r = runnerOptions.find((o) => o.name === selectedRunner);
|
||||
return r ? extractI18nObject(r.label) : (selectedRunner ?? '');
|
||||
}, [runnerOptions, selectedRunner]);
|
||||
const r = runnerOptions.find((o) => o.name === selected);
|
||||
return r ? extractI18nObject(r.label) : (selected ?? '');
|
||||
}, [runnerOptions, selected]);
|
||||
|
||||
// Orchestrated runners: everything except local-agent
|
||||
const orchestratedRunners = runnerOptions.filter(
|
||||
(o) => o.name !== 'local-agent',
|
||||
);
|
||||
|
||||
// ---- Layer 0: Main choice ----
|
||||
if (step2Layer === 0) {
|
||||
// Before any runner is selected: centered grid layout
|
||||
if (!selected) {
|
||||
return (
|
||||
<div className="space-y-6 max-w-3xl mx-auto">
|
||||
<div className="space-y-6 max-w-4xl mx-auto">
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl font-semibold">
|
||||
{t('wizard.aiEngine.title')}
|
||||
@@ -1511,75 +1023,12 @@ function StepAIEngine({
|
||||
{t('wizard.aiEngine.description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
||||
{/* Orchestrated Agent Apps */}
|
||||
<div
|
||||
className="relative rounded-xl border-2 border-border cursor-pointer transition-all hover:shadow-lg hover:border-primary/50 hover:scale-[1.02]"
|
||||
onClick={() => onAiEngineModeSelect('orchestrated')}
|
||||
>
|
||||
<div className="p-6 flex flex-col items-center gap-4 text-center h-full">
|
||||
<div className="w-14 h-14 rounded-full bg-muted flex items-center justify-center">
|
||||
<Wrench className="w-7 h-7 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">
|
||||
{t('wizard.aiEngine.orchestrated.title')}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t('wizard.aiEngine.orchestrated.description')}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" size="lg" className="w-full">
|
||||
{t('wizard.aiEngine.orchestrated.action')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Direct LLM */}
|
||||
<div
|
||||
className="relative rounded-xl border-2 border-border cursor-pointer transition-all hover:shadow-lg hover:border-primary/50 hover:scale-[1.02]"
|
||||
onClick={() => onAiEngineModeSelect('llm')}
|
||||
>
|
||||
<div className="p-6 flex flex-col items-center gap-4 text-center h-full">
|
||||
<div className="w-14 h-14 rounded-full bg-muted flex items-center justify-center">
|
||||
<Rocket className="w-7 h-7 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">
|
||||
{t('wizard.aiEngine.llm.title')}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t('wizard.aiEngine.llm.description')}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" size="lg" className="w-full">
|
||||
{t('wizard.aiEngine.llm.action')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Layer 1a: Orchestrated runners ----
|
||||
if (step2Layer === 1 && aiEngineMode === 'orchestrated') {
|
||||
return (
|
||||
<div className="space-y-6 max-w-4xl mx-auto">
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl font-semibold">
|
||||
{t('wizard.aiEngine.orchestrated.selectTitle')}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t('wizard.aiEngine.orchestrated.selectDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{orchestratedRunners.map((opt) => (
|
||||
{runnerOptions.map((opt) => (
|
||||
<Card
|
||||
key={opt.name}
|
||||
className="cursor-pointer transition-all hover:shadow-md hover:border-primary/50"
|
||||
onClick={() => onSelectRunner(opt.name)}
|
||||
onClick={() => onSelect(opt.name)}
|
||||
>
|
||||
<CardHeader className="flex flex-row items-center gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -1598,109 +1047,9 @@ function StepAIEngine({
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Layer 1b: LLM model source selection ----
|
||||
if (step2Layer === 1 && aiEngineMode === 'llm') {
|
||||
return (
|
||||
<div className="space-y-6 max-w-3xl mx-auto">
|
||||
<div className="text-center">
|
||||
<h2 className="text-xl font-semibold">
|
||||
{t('wizard.modelSource.title')}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t('wizard.modelSource.description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
||||
{/* LangBot Service */}
|
||||
<div
|
||||
className="relative rounded-xl border-2 border-border cursor-pointer transition-all hover:shadow-lg hover:border-primary/50 hover:scale-[1.02]"
|
||||
onClick={() => onModelSourceSelect('space')}
|
||||
>
|
||||
<div className="p-6 flex flex-col items-center gap-4 text-center h-full">
|
||||
<div className="w-14 h-14 rounded-full bg-muted flex items-center justify-center">
|
||||
<Rocket className="w-7 h-7 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">
|
||||
{t('wizard.modelSource.space.title')}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t('wizard.modelSource.space.description')}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" size="lg" className="w-full">
|
||||
{t('wizard.modelSource.space.action')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom provider */}
|
||||
<div
|
||||
className="relative rounded-xl border-2 border-border cursor-pointer transition-all hover:shadow-lg hover:border-primary/50 hover:scale-[1.02]"
|
||||
onClick={() => onModelSourceSelect('custom')}
|
||||
>
|
||||
<div className="p-6 flex flex-col items-center gap-4 text-center h-full">
|
||||
<div className="w-14 h-14 rounded-full bg-muted flex items-center justify-center">
|
||||
<Wrench className="w-7 h-7 text-muted-foreground" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">
|
||||
{t('wizard.modelSource.custom.title')}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t('wizard.modelSource.custom.description')}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" size="lg" className="w-full">
|
||||
{t('wizard.modelSource.custom.action')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Layer 2: Custom provider creation form ----
|
||||
if (step2Layer === 2) {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto w-full animate-in fade-in slide-in-from-bottom-2 duration-300">
|
||||
<div className="text-center mb-6">
|
||||
<h2 className="text-xl font-semibold">
|
||||
{t('wizard.provider.title')}
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t('wizard.provider.description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="border rounded-lg p-6 bg-card">
|
||||
<ProviderForm
|
||||
initialValues={savedProviderForm}
|
||||
onValuesChange={onSavedProviderFormChange}
|
||||
submitButtonText={t('wizard.provider.saveAndNext')}
|
||||
onFormSubmit={(uuid) => {
|
||||
onProviderCreated(uuid);
|
||||
}}
|
||||
onFormCancel={onResetModelSource}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Layer 2.5: Model scan & add after provider creation ----
|
||||
if (step2Layer === 3) {
|
||||
return (
|
||||
<WizardModelScan
|
||||
providerUuid={createdProviderUuid}
|
||||
initialSelectedModels={savedSelectedModels}
|
||||
onSelectedModelsChange={onSavedSelectedModelsChange}
|
||||
onModelsAdded={onModelsAdded}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Layer 3: Runner configuration form ----
|
||||
// After a runner is selected: left-right split layout
|
||||
// On mobile (< lg): single column, normal scroll from parent
|
||||
// On desktop (>= lg): side-by-side with independent scroll per column
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-1 lg:min-h-0 max-w-6xl mx-auto w-full">
|
||||
<div className="text-center shrink-0 mb-4">
|
||||
@@ -1711,61 +1060,53 @@ function StepAIEngine({
|
||||
</div>
|
||||
|
||||
<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 (only for orchestrated mode) */}
|
||||
{aiEngineMode === 'orchestrated' && (
|
||||
<div className="w-full lg:w-[280px] shrink-0 lg:overflow-y-auto lg:pr-3">
|
||||
<div className="space-y-3 p-1">
|
||||
{orchestratedRunners.map((opt) => {
|
||||
const isSelected = selectedRunner === opt.name;
|
||||
return (
|
||||
<Card
|
||||
key={opt.name}
|
||||
className={cn(
|
||||
'cursor-pointer transition-all',
|
||||
isSelected
|
||||
? 'ring-2 ring-primary shadow-md'
|
||||
: 'opacity-50 hover:opacity-80 hover:border-primary/50',
|
||||
)}
|
||||
onClick={() => onSelectRunner(opt.name)}
|
||||
>
|
||||
<CardHeader className="flex flex-row items-center gap-3 py-3 px-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<CardTitle
|
||||
className={cn(
|
||||
'text-sm',
|
||||
!isSelected && 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{extractI18nObject(opt.label)}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs font-mono text-muted-foreground">
|
||||
{opt.name}
|
||||
</CardDescription>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<div className="shrink-0">
|
||||
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center">
|
||||
<Check className="w-3 h-3 text-primary-foreground" />
|
||||
</div>
|
||||
{/* Left: runner list */}
|
||||
<div className="w-full lg:w-[280px] shrink-0 lg:overflow-y-auto lg:pr-3">
|
||||
{/* p-1 provides space for ring-2 (4px) to render without clipping */}
|
||||
<div className="space-y-3 p-1">
|
||||
{runnerOptions.map((opt) => {
|
||||
const isSelected = selected === opt.name;
|
||||
return (
|
||||
<Card
|
||||
key={opt.name}
|
||||
className={cn(
|
||||
'cursor-pointer transition-all',
|
||||
isSelected
|
||||
? 'ring-2 ring-primary shadow-md'
|
||||
: 'opacity-50 hover:opacity-80 hover:border-primary/50',
|
||||
)}
|
||||
onClick={() => onSelect(opt.name)}
|
||||
>
|
||||
<CardHeader className="flex flex-row items-center gap-3 py-3 px-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<CardTitle
|
||||
className={cn(
|
||||
'text-sm',
|
||||
!isSelected && 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{extractI18nObject(opt.label)}
|
||||
</CardTitle>
|
||||
<CardDescription className="text-xs font-mono text-muted-foreground">
|
||||
{opt.name}
|
||||
</CardDescription>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<div className="shrink-0">
|
||||
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center">
|
||||
<Check className="w-3 h-3 text-primary-foreground" />
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right: runner configuration */}
|
||||
<div
|
||||
className={cn(
|
||||
'shrink-0 lg:overflow-y-auto lg:pr-3 animate-in fade-in slide-in-from-right-2 duration-300',
|
||||
aiEngineMode === 'orchestrated'
|
||||
? 'w-full lg:w-[560px]'
|
||||
: 'w-full max-w-2xl mx-auto',
|
||||
)}
|
||||
>
|
||||
{/* Right: runner configuration — fixed width on desktop */}
|
||||
<div className="w-full lg:w-[560px] shrink-0 lg:overflow-y-auto lg:pr-3 animate-in fade-in slide-in-from-right-2 duration-300">
|
||||
<div className="p-1">
|
||||
{runnerConfigItems.length > 0 && (
|
||||
<Card>
|
||||
@@ -1776,7 +1117,7 @@ function StepAIEngine({
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DynamicFormComponent
|
||||
key={selectedRunner ?? 'llm'}
|
||||
key={selected}
|
||||
itemConfigList={runnerConfigItems}
|
||||
initialValues={runnerConfigValues as Record<string, object>}
|
||||
onSubmit={stableRunnerConfigCb}
|
||||
|
||||
@@ -1807,7 +1807,6 @@ 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: {
|
||||
@@ -1835,62 +1834,6 @@ const enUS = {
|
||||
title: 'Select an AI Engine',
|
||||
description:
|
||||
"Choose the AI engine that will power your bot's intelligence.",
|
||||
orchestrated: {
|
||||
title: 'Use Orchestrated Agent Apps',
|
||||
description:
|
||||
'Use pre-built Agent apps from Dify, n8n, Coze, and more.',
|
||||
action: 'Select Agent App',
|
||||
selectTitle: 'Select Agent App',
|
||||
selectDescription: 'Choose the Agent app platform you want to use.',
|
||||
},
|
||||
llm: {
|
||||
title: 'Use LLM Directly',
|
||||
description:
|
||||
'Configure a model provider and use LLM to drive your bot directly.',
|
||||
action: 'Configure LLM',
|
||||
},
|
||||
},
|
||||
spaceBanner: {
|
||||
message:
|
||||
'Connect to LangBot Space for free trial model credits and zero-config instant setup!',
|
||||
action: 'Authorize with Space',
|
||||
},
|
||||
provider: {
|
||||
title: 'Add Model Provider',
|
||||
description:
|
||||
'Add your own model provider. Configure the API key to get started.',
|
||||
scanTitle: 'Scan Available Models',
|
||||
scanDescription: 'Scanning your provider for available LLM models.',
|
||||
scanning: 'Scanning models...',
|
||||
noModelsFound: 'No models found. Please check your provider config.',
|
||||
addSelected: 'Add {{count}} selected model(s)',
|
||||
modelsAdded: 'Added {{count}} model(s)',
|
||||
modelsAddError: 'Failed to add models',
|
||||
skipModelAdd: 'Skip, add later',
|
||||
searchModels: 'Search models...',
|
||||
noMatch: 'No matching models',
|
||||
orDivider: 'or',
|
||||
manualModelPlaceholder: 'Enter model name to add manually',
|
||||
addManual: 'Add',
|
||||
saveAndNext: 'Save & Next',
|
||||
alreadyAdded: 'Added',
|
||||
modelAlreadyExists: 'Model "{{name}}" already exists',
|
||||
},
|
||||
modelSource: {
|
||||
title: 'Choose Model Source',
|
||||
description: 'Select how to provide model capabilities for your AI engine.',
|
||||
space: {
|
||||
title: 'LangBot Service',
|
||||
description:
|
||||
'Zero-config ready with free trial model credits. No API key needed — just plug and play.',
|
||||
action: 'Use LangBot Service',
|
||||
},
|
||||
custom: {
|
||||
title: 'Custom Model',
|
||||
description:
|
||||
'Bring your own model API key. Supports OpenAI, Claude, Gemini, and more.',
|
||||
action: 'Add My Own Model',
|
||||
},
|
||||
},
|
||||
config: {
|
||||
botInfo: 'Bot Information',
|
||||
@@ -1972,6 +1915,9 @@ 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',
|
||||
|
||||
@@ -220,6 +220,19 @@ 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',
|
||||
@@ -1659,7 +1672,6 @@ 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:
|
||||
@@ -1691,65 +1703,6 @@ const esES = {
|
||||
title: 'Selecciona un motor de IA',
|
||||
description:
|
||||
'Elige el motor de IA que impulsará la inteligencia de tu Bot.',
|
||||
orchestrated: {
|
||||
title: 'Usar aplicaciones Agent orquestadas',
|
||||
description:
|
||||
'Usar aplicaciones Agent predefinidas de Dify, n8n, Coze y más.',
|
||||
action: 'Seleccionar aplicación Agent',
|
||||
selectTitle: 'Seleccionar aplicación Agent',
|
||||
selectDescription:
|
||||
'Elige la plataforma de aplicación Agent que deseas usar.',
|
||||
},
|
||||
llm: {
|
||||
title: 'Usar modelo directamente',
|
||||
description:
|
||||
'Configurar un proveedor de modelos y usar el modelo para impulsar tu Bot.',
|
||||
action: 'Configurar modelo',
|
||||
},
|
||||
},
|
||||
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',
|
||||
},
|
||||
provider: {
|
||||
title: 'Agregar proveedor de modelos',
|
||||
description:
|
||||
'Agrega tu propio proveedor de modelos. Configura la API key para comenzar.',
|
||||
scanTitle: 'Escanear modelos disponibles',
|
||||
scanDescription: 'Escaneando modelos LLM de tu proveedor.',
|
||||
scanning: 'Escaneando modelos...',
|
||||
noModelsFound:
|
||||
'No se encontraron modelos. Verifica la configuración del proveedor.',
|
||||
addSelected: 'Agregar {{count}} modelo(s) seleccionado(s)',
|
||||
modelsAdded: '{{count}} modelo(s) agregado(s)',
|
||||
modelsAddError: 'Error al agregar modelos',
|
||||
skipModelAdd: 'Omitir, agregar después',
|
||||
searchModels: 'Buscar modelos...',
|
||||
noMatch: 'No hay modelos coincidentes',
|
||||
orDivider: 'o',
|
||||
manualModelPlaceholder: 'Ingresa el nombre del modelo para agregar manualmente',
|
||||
addManual: 'Agregar',
|
||||
saveAndNext: 'Guardar y siguiente',
|
||||
alreadyAdded: 'Agregado',
|
||||
modelAlreadyExists: 'El modelo "{{name}}" ya existe',
|
||||
},
|
||||
modelSource: {
|
||||
title: 'Elegir fuente del modelo',
|
||||
description:
|
||||
'Selecciona cómo proporcionar capacidades de modelo para tu motor de IA.',
|
||||
space: {
|
||||
title: 'Servicio LangBot',
|
||||
description:
|
||||
'Listo para usar sin configuración, con créditos de prueba gratuitos. No se necesita API key.',
|
||||
action: 'Usar servicio LangBot',
|
||||
},
|
||||
custom: {
|
||||
title: 'Modelo personalizado',
|
||||
description:
|
||||
'Usa tu propia API key. Compatible con OpenAI, Claude, Gemini y más.',
|
||||
action: 'Agregar mi propio modelo',
|
||||
},
|
||||
},
|
||||
config: {
|
||||
botInfo: 'Información del Bot',
|
||||
@@ -1801,6 +1754,9 @@ 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',
|
||||
|
||||
@@ -1722,7 +1722,6 @@ const jaJP = {
|
||||
botCreateSuccess: 'ボットが正常に作成されました!',
|
||||
botSaveSuccess: 'ボット設定が保存され、有効になりました!',
|
||||
createError: 'リソースの作成に失敗しました',
|
||||
spaceAuthError: 'Space 認証の開始に失敗しました',
|
||||
skipSaveError: 'スキップ状態の保存に失敗しました。もう一度お試しください。',
|
||||
completeSaveError: '完了状態の保存に失敗しました。もう一度お試しください。',
|
||||
step: {
|
||||
@@ -1752,64 +1751,6 @@ const jaJP = {
|
||||
title: 'AIエンジンを選択',
|
||||
description:
|
||||
'ボットのインテリジェンスを駆動するAIエンジンを選択してください。',
|
||||
orchestrated: {
|
||||
title: 'オーケストレーション済みAgentアプリを使用',
|
||||
description:
|
||||
'Dify、n8n、Cozeなどのプラットフォームで構築済みのAgentアプリを使用。',
|
||||
action: 'Agentアプリを選択',
|
||||
selectTitle: 'Agentアプリを選択',
|
||||
selectDescription:
|
||||
'使用するAgentアプリプラットフォームを選択してください。',
|
||||
},
|
||||
llm: {
|
||||
title: '大規模モデルを直接使用',
|
||||
description:
|
||||
'モデルプロバイダーを設定し、大規模モデルでボットを直接駆動。',
|
||||
action: '大規模モデルを設定',
|
||||
},
|
||||
},
|
||||
spaceBanner: {
|
||||
message:
|
||||
'LangBot Spaceに接続して、無料トライアルモデルクレジットとゼロ設定の即時セットアップを入手!',
|
||||
action: 'Spaceで認証',
|
||||
},
|
||||
provider: {
|
||||
title: 'モデルプロバイダーを追加',
|
||||
description:
|
||||
'独自のモデルプロバイダーを追加します。APIキーを設定すると使用可能になります。',
|
||||
scanTitle: '利用可能なモデルをスキャン',
|
||||
scanDescription: 'プロバイダーから利用可能なLLMモデルをスキャン中です。',
|
||||
scanning: 'モデルをスキャン中...',
|
||||
noModelsFound:
|
||||
'モデルが見つかりません。プロバイダー設定を確認してください。',
|
||||
addSelected: '選択した{{count}}個のモデルを追加',
|
||||
modelsAdded: '{{count}}個のモデルを追加しました',
|
||||
modelsAddError: 'モデルの追加に失敗しました',
|
||||
skipModelAdd: 'スキップ(後で追加)',
|
||||
searchModels: 'モデルを検索...',
|
||||
noMatch: '一致するモデルがありません',
|
||||
orDivider: 'または',
|
||||
manualModelPlaceholder: 'モデル名を入力して手動追加',
|
||||
addManual: '追加',
|
||||
saveAndNext: '保存して次へ',
|
||||
alreadyAdded: '追加済み',
|
||||
modelAlreadyExists: 'モデル「{{name}}」は既に存在します',
|
||||
},
|
||||
modelSource: {
|
||||
title: 'モデルソースを選択',
|
||||
description: 'AIエンジンにモデル機能を提供する方法を選択してください。',
|
||||
space: {
|
||||
title: 'LangBot サービス',
|
||||
description:
|
||||
'設定不要で利用可能。無料トライアルモデルクレジット付き。APIキー不要ですぐに使えます。',
|
||||
action: 'LangBot サービスを使用',
|
||||
},
|
||||
custom: {
|
||||
title: 'カスタムモデル',
|
||||
description:
|
||||
'お手持ちのモデルAPIキーを使用。OpenAI、Claude、Geminiなど主要モデルに対応。',
|
||||
action: '独自のモデルを追加',
|
||||
},
|
||||
},
|
||||
config: {
|
||||
botInfo: 'ボット情報',
|
||||
@@ -1890,6 +1831,9 @@ const jaJP = {
|
||||
scanQRCode: '以下のQRコードをWeChatでスキャンし、トークンを自動入力',
|
||||
loginSuccess: 'ログイン成功!トークンが自動入力されました',
|
||||
loginFailed: 'ログイン失敗',
|
||||
connecting: 'WeChatサービスに接続中...',
|
||||
waitingForScan: 'スキャン待ち',
|
||||
retry: '再試行',
|
||||
},
|
||||
dingtalk: {
|
||||
createApp: 'ワンクリックでDingTalkアプリ作成',
|
||||
|
||||
@@ -217,6 +217,19 @@ const ruRU = {
|
||||
selectModelAbilities: 'Выберите возможности модели',
|
||||
visionAbility: 'Распознавание изображений',
|
||||
functionCallAbility: 'Вызов функций',
|
||||
reasoningAbility: 'Рассуждение',
|
||||
reasoningLevel: 'Уровень рассуждений',
|
||||
reasoningLevels: {
|
||||
providerDefault: 'По умолчанию провайдера',
|
||||
disabled: 'Выключено',
|
||||
enabled: 'Включено',
|
||||
minimal: 'Минимальный',
|
||||
low: 'Низкий',
|
||||
medium: 'Средний',
|
||||
high: 'Высокий',
|
||||
xhigh: 'Очень высокий',
|
||||
max: 'Максимальный',
|
||||
},
|
||||
contextLength: 'Контекстное окно',
|
||||
contextLengthPlaceholder: 'Неизвестно',
|
||||
contextLengthInvalid:
|
||||
@@ -1632,7 +1645,6 @@ const ruRU = {
|
||||
botCreateSuccess: 'Бот успешно создан!',
|
||||
botSaveSuccess: 'Конфигурация бота сохранена и включена!',
|
||||
createError: 'Не удалось создать ресурсы',
|
||||
spaceAuthError: 'Не удалось инициировать авторизацию через Space',
|
||||
skipSaveError: 'Не удалось сохранить статус пропуска. Повторите попытку.',
|
||||
completeSaveError:
|
||||
'Не удалось сохранить статус завершения. Повторите попытку.',
|
||||
@@ -1661,65 +1673,6 @@ const ruRU = {
|
||||
title: 'Выберите ИИ-движок',
|
||||
description:
|
||||
'Выберите ИИ-движок, который будет управлять интеллектом вашего бота.',
|
||||
orchestrated: {
|
||||
title: 'Использовать оркестрированные Agent-приложения',
|
||||
description:
|
||||
'Использовать готовые Agent-приложения из Dify, n8n, Coze и других.',
|
||||
action: 'Выбрать Agent-приложение',
|
||||
selectTitle: 'Выбрать Agent-приложение',
|
||||
selectDescription:
|
||||
'Выберите платформу Agent-приложения для использования.',
|
||||
},
|
||||
llm: {
|
||||
title: 'Использовать LLM напрямую',
|
||||
description:
|
||||
'Настроить провайдера моделей и использовать LLM для управления ботом.',
|
||||
action: 'Настроить LLM',
|
||||
},
|
||||
},
|
||||
spaceBanner: {
|
||||
message:
|
||||
'Подключитесь к LangBot Space для бесплатных пробных кредитов и мгновенной настройки!',
|
||||
action: 'Авторизация через Space',
|
||||
},
|
||||
provider: {
|
||||
title: 'Добавить провайдера моделей',
|
||||
description:
|
||||
'Добавьте своего провайдера моделей. Настройте API-ключ для начала работы.',
|
||||
scanTitle: 'Сканирование доступных моделей',
|
||||
scanDescription: 'Сканирование LLM-моделей у вашего провайдера.',
|
||||
scanning: 'Сканирование моделей...',
|
||||
noModelsFound:
|
||||
'Модели не найдены. Проверьте настройки провайдера.',
|
||||
addSelected: 'Добавить {{count}} выбранную(ых)',
|
||||
modelsAdded: 'Добавлено {{count}} модель(ей)',
|
||||
modelsAddError: 'Ошибка добавления моделей',
|
||||
skipModelAdd: 'Пропустить, добавить позже',
|
||||
searchModels: 'Поиск моделей...',
|
||||
noMatch: 'Нет подходящих моделей',
|
||||
orDivider: 'или',
|
||||
manualModelPlaceholder: 'Введите имя модели для ручного добавления',
|
||||
addManual: 'Добавить',
|
||||
saveAndNext: 'Сохранить и далее',
|
||||
alreadyAdded: 'Добавлено',
|
||||
modelAlreadyExists: 'Модель "{{name}}" уже существует',
|
||||
},
|
||||
modelSource: {
|
||||
title: 'Выберите источник модели',
|
||||
description:
|
||||
'Выберите способ предоставления возможностей модели для вашего AI-движка.',
|
||||
space: {
|
||||
title: 'Сервис LangBot',
|
||||
description:
|
||||
'Готов к использованию без настройки, с бесплатными пробными кредитами. API-ключ не требуется.',
|
||||
action: 'Использовать сервис LangBot',
|
||||
},
|
||||
custom: {
|
||||
title: 'Пользовательская модель',
|
||||
description:
|
||||
'Используйте свой собственный API-ключ. Поддержка OpenAI, Claude, Gemini и других.',
|
||||
action: 'Добавить свою модель',
|
||||
},
|
||||
},
|
||||
config: {
|
||||
botInfo: 'Информация о боте',
|
||||
@@ -1771,6 +1724,9 @@ const ruRU = {
|
||||
'Отсканируйте QR-код ниже в WeChat, чтобы авторизоваться и автоматически заполнить токен',
|
||||
loginSuccess: 'Вход выполнен успешно! Токен заполнен автоматически',
|
||||
loginFailed: 'Не удалось выполнить вход',
|
||||
connecting: 'Подключение к сервису WeChat...',
|
||||
waitingForScan: 'Ожидание сканирования',
|
||||
retry: 'Повторить',
|
||||
},
|
||||
dingtalk: {
|
||||
createApp: 'Создать приложение DingTalk в один клик',
|
||||
|
||||
@@ -213,6 +213,19 @@ const thTH = {
|
||||
selectModelAbilities: 'เลือกความสามารถของโมเดล',
|
||||
visionAbility: 'ความสามารถด้านภาพ',
|
||||
functionCallAbility: 'การเรียกฟังก์ชัน',
|
||||
reasoningAbility: 'ความสามารถในการให้เหตุผล',
|
||||
reasoningLevel: 'ระดับการให้เหตุผล',
|
||||
reasoningLevels: {
|
||||
providerDefault: 'ค่าเริ่มต้นของผู้ให้บริการ',
|
||||
disabled: 'ปิด',
|
||||
enabled: 'เปิด',
|
||||
minimal: 'ต่ำสุด',
|
||||
low: 'ต่ำ',
|
||||
medium: 'ปานกลาง',
|
||||
high: 'สูง',
|
||||
xhigh: 'สูงมาก',
|
||||
max: 'สูงสุด',
|
||||
},
|
||||
contextLength: 'หน้าต่างบริบท',
|
||||
contextLengthPlaceholder: 'ไม่ทราบ',
|
||||
contextLengthInvalid: 'หน้าต่างบริบทต้องเป็นจำนวนเต็มบวก',
|
||||
@@ -1599,7 +1612,6 @@ const thTH = {
|
||||
botCreateSuccess: 'สร้าง Bot สำเร็จ!',
|
||||
botSaveSuccess: 'บันทึกและเปิดใช้งาน Bot สำเร็จ!',
|
||||
createError: 'ไม่สามารถสร้างทรัพยากรได้',
|
||||
spaceAuthError: 'ไม่สามารถเริ่มต้นการยืนยันสิทธิ์ Space ได้',
|
||||
skipSaveError: 'ไม่สามารถบันทึกสถานะการข้ามได้ กรุณาลองใหม่',
|
||||
completeSaveError: 'ไม่สามารถบันทึกสถานะการเสร็จสิ้นได้ กรุณาลองใหม่',
|
||||
step: {
|
||||
@@ -1626,63 +1638,6 @@ const thTH = {
|
||||
aiEngine: {
|
||||
title: 'เลือกเครื่องมือ AI',
|
||||
description: 'เลือกเครื่องมือ AI ที่จะขับเคลื่อนความฉลาดของ Bot',
|
||||
orchestrated: {
|
||||
title: 'ใช้แอป Agent ที่จัดเตรียมไว้',
|
||||
description:
|
||||
'ใช้แอป Agent ที่สร้างไว้จาก Dify, n8n, Coze และอื่นๆ',
|
||||
action: 'เลือกแอป Agent',
|
||||
selectTitle: 'เลือกแอป Agent',
|
||||
selectDescription: 'เลือกแพลตฟอร์มแอป Agent ที่ต้องการใช้',
|
||||
},
|
||||
llm: {
|
||||
title: 'ใช้โมเดลโดยตรง',
|
||||
description:
|
||||
'กำหนดค่าผู้ให้บริการโมเดลและใช้โมเดลขับเคลื่อน Bot โดยตรง',
|
||||
action: 'กำหนดค่าโมเดล',
|
||||
},
|
||||
},
|
||||
spaceBanner: {
|
||||
message:
|
||||
'เชื่อมต่อกับ LangBot Space เพื่อรับเครดิตทดลองใช้โมเดลฟรีและตั้งค่าทันทีโดยไม่ต้องกำหนดค่า!',
|
||||
action: 'ยืนยันสิทธิ์กับ Space',
|
||||
},
|
||||
provider: {
|
||||
title: 'เพิ่มผู้ให้บริการโมเดล',
|
||||
description:
|
||||
'เพิ่มผู้ให้บริการโมเดลของคุณเอง กำหนดค่า API key เพื่อเริ่มต้น',
|
||||
scanTitle: 'สแกนโมเดลที่ใช้ได้',
|
||||
scanDescription: 'กำลังสแกนโมเดล LLM จากผู้ให้บริการของคุณ',
|
||||
scanning: 'กำลังสแกนโมเดล...',
|
||||
noModelsFound:
|
||||
'ไม่พบโมเดล กรุณาตรวจสอบการตั้งค่าผู้ให้บริการ',
|
||||
addSelected: 'เพิ่ม {{count}} โมเดลที่เลือก',
|
||||
modelsAdded: 'เพิ่ม {{count}} โมเดลแล้ว',
|
||||
modelsAddError: 'เพิ่มโมเดลไม่สำเร็จ',
|
||||
skipModelAdd: 'ข้าม เพิ่มทีหลัง',
|
||||
searchModels: 'ค้นหาโมเดล...',
|
||||
noMatch: 'ไม่พบโมเดลที่ตรงกัน',
|
||||
orDivider: 'หรือ',
|
||||
manualModelPlaceholder: 'กรอกชื่อโมเดลเพื่อเพิ่มด้วยตนเอง',
|
||||
addManual: 'เพิ่ม',
|
||||
saveAndNext: 'บันทึกและถัดไป',
|
||||
alreadyAdded: 'เพิ่มแล้ว',
|
||||
modelAlreadyExists: 'โมเดล "{{name}}" มีอยู่แล้ว',
|
||||
},
|
||||
modelSource: {
|
||||
title: 'เลือกแหล่งโมเดล',
|
||||
description: 'เลือกวิธีการให้ความสามารถของโมเดลสำหรับ AI engine ของคุณ',
|
||||
space: {
|
||||
title: 'บริการ LangBot',
|
||||
description:
|
||||
'พร้อมใช้งานทันทีไม่ต้องตั้งค่า พร้อมเครดิตทดลองใช้โมเดลฟรี ไม่ต้องใช้ API key',
|
||||
action: 'ใช้บริการ LangBot',
|
||||
},
|
||||
custom: {
|
||||
title: 'โมเดลกำหนดเอง',
|
||||
description:
|
||||
'ใช้ API key โมเดลของคุณเอง รองรับ OpenAI, Claude, Gemini และอื่นๆ',
|
||||
action: 'เพิ่มโมเดลของฉัน',
|
||||
},
|
||||
},
|
||||
config: {
|
||||
botInfo: 'ข้อมูล Bot',
|
||||
@@ -1732,6 +1687,9 @@ const thTH = {
|
||||
'สแกนคิวอาร์โค้ดด้านล่างด้วย WeChat เพื่ออนุญาตและกรอกโทเคนอัตโนมัติ',
|
||||
loginSuccess: 'เข้าสู่ระบบสำเร็จ และกรอกโทเคนอัตโนมัติแล้ว',
|
||||
loginFailed: 'เข้าสู่ระบบไม่สำเร็จ',
|
||||
connecting: 'กำลังเชื่อมต่อบริการ WeChat...',
|
||||
waitingForScan: 'กำลังรอการสแกน',
|
||||
retry: 'ลองอีกครั้ง',
|
||||
},
|
||||
dingtalk: {
|
||||
createApp: 'สร้างแอป DingTalk ด้วยคลิกเดียว',
|
||||
|
||||
@@ -217,6 +217,19 @@ 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',
|
||||
@@ -1625,7 +1638,6 @@ 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: {
|
||||
@@ -1652,63 +1664,6 @@ const viVN = {
|
||||
aiEngine: {
|
||||
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.',
|
||||
orchestrated: {
|
||||
title: 'Sử dụng ứng dụng Agent đã được phối hợp',
|
||||
description:
|
||||
'Sử dụng các ứng dụng Agent đã được xây dựng từ Dify, n8n, Coze.',
|
||||
action: 'Chọn ứng dụng Agent',
|
||||
selectTitle: 'Chọn ứng dụng Agent',
|
||||
selectDescription: 'Chọn nền tảng ứng dụng Agent bạn muốn sử dụng.',
|
||||
},
|
||||
llm: {
|
||||
title: 'Sử dụng mô hình trực tiếp',
|
||||
description:
|
||||
'Cấu hình nhà cung cấp mô hình và sử dụng mô hình lớn để điều khiển Bot.',
|
||||
action: 'Cấu hình mô hình',
|
||||
},
|
||||
},
|
||||
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',
|
||||
},
|
||||
provider: {
|
||||
title: 'Thêm nhà cung cấp mô hình',
|
||||
description:
|
||||
'Thêm nhà cung cấp mô hình của bạn. Cấu hình API key để bắt đầu.',
|
||||
scanTitle: 'Quét mô hình khả dụng',
|
||||
scanDescription: 'Đang quét mô hình LLM từ nhà cung cấp của bạn.',
|
||||
scanning: 'Đang quét mô hình...',
|
||||
noModelsFound:
|
||||
'Không tìm thấy mô hình. Vui lòng kiểm tra cấu hình nhà cung cấp.',
|
||||
addSelected: 'Thêm {{count}} mô hình đã chọn',
|
||||
modelsAdded: 'Đã thêm {{count}} mô hình',
|
||||
modelsAddError: 'Thêm mô hình thất bại',
|
||||
skipModelAdd: 'Bỏ qua, thêm sau',
|
||||
searchModels: 'Tìm kiếm mô hình...',
|
||||
noMatch: 'Không tìm thấy mô hình phù hợp',
|
||||
orDivider: 'hoặc',
|
||||
manualModelPlaceholder: 'Nhập tên mô hình để thêm thủ công',
|
||||
addManual: 'Thêm',
|
||||
saveAndNext: 'Lưu & Tiếp theo',
|
||||
alreadyAdded: 'Đã thêm',
|
||||
modelAlreadyExists: 'Mô hình "{{name}}" đã tồn tại',
|
||||
},
|
||||
modelSource: {
|
||||
title: 'Chọn nguồn mô hình',
|
||||
description: 'Chọn cách cung cấp khả năng mô hình cho AI engine của bạn.',
|
||||
space: {
|
||||
title: 'Dịch vụ LangBot',
|
||||
description:
|
||||
'Sẵn sàng sử dụng không cần cấu hình, tặng tín dụng mô hình thử nghiệm miễn phí. Không cần API key.',
|
||||
action: 'Sử dụng dịch vụ LangBot',
|
||||
},
|
||||
custom: {
|
||||
title: 'Mô hình tùy chỉnh',
|
||||
description:
|
||||
'Sử dụng API key mô hình của riêng bạn. Hỗ trợ OpenAI, Claude, Gemini và nhiều hơn nữa.',
|
||||
action: 'Thêm mô hình của tôi',
|
||||
},
|
||||
},
|
||||
config: {
|
||||
botInfo: 'Thông tin Bot',
|
||||
@@ -1760,6 +1715,9 @@ 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',
|
||||
|
||||
@@ -1730,7 +1730,6 @@ const zhHans = {
|
||||
botCreateSuccess: '机器人创建成功!',
|
||||
botSaveSuccess: '机器人配置已保存并启用!',
|
||||
createError: '创建资源失败',
|
||||
spaceAuthError: '无法发起 Space 授权',
|
||||
skipSaveError: '保存跳过状态失败,请重试。',
|
||||
completeSaveError: '保存完成状态失败,请重试。',
|
||||
step: {
|
||||
@@ -1755,58 +1754,6 @@ const zhHans = {
|
||||
aiEngine: {
|
||||
title: '选择 AI 引擎',
|
||||
description: '选择驱动机器人智能的 AI 引擎。',
|
||||
orchestrated: {
|
||||
title: '使用编排好的 Agent 应用',
|
||||
description: '使用 Dify、n8n、Coze 等平台编排好的 Agent 应用。',
|
||||
action: '选择 Agent 应用',
|
||||
selectTitle: '选择 Agent 应用',
|
||||
selectDescription: '选择你要使用的 Agent 应用平台。',
|
||||
},
|
||||
llm: {
|
||||
title: '直接使用大模型',
|
||||
description: '配置大模型供应商,直接使用大模型驱动机器人。',
|
||||
action: '配置大模型',
|
||||
},
|
||||
},
|
||||
spaceBanner: {
|
||||
message: '接入 LangBot Space,获取免费试用模型额度,零配置极速开箱!',
|
||||
action: '前往授权登录',
|
||||
},
|
||||
provider: {
|
||||
title: '添加模型供应商',
|
||||
description: '添加你自己的模型供应商,配置 API Key 后即可使用。',
|
||||
scanTitle: '扫描可用模型',
|
||||
scanDescription: '正在从你的供应商中扫描可用的 LLM 模型。',
|
||||
scanning: '正在扫描模型...',
|
||||
noModelsFound: '未发现可用模型,请检查供应商配置。',
|
||||
addSelected: '添加选中的 {{count}} 个模型',
|
||||
modelsAdded: '已添加 {{count}} 个模型',
|
||||
modelsAddError: '添加模型失败',
|
||||
skipModelAdd: '跳过,稍后添加',
|
||||
searchModels: '搜索模型...',
|
||||
noMatch: '没有匹配的模型',
|
||||
orDivider: '或',
|
||||
manualModelPlaceholder: '输入模型名称手动添加',
|
||||
addManual: '添加',
|
||||
saveAndNext: '保存并下一步',
|
||||
alreadyAdded: '已添加',
|
||||
modelAlreadyExists: '模型 "{{name}}" 已存在',
|
||||
},
|
||||
modelSource: {
|
||||
title: '选择模型来源',
|
||||
description: '选择如何为你的 AI 引擎提供模型能力。',
|
||||
space: {
|
||||
title: 'LangBot 服务',
|
||||
description:
|
||||
'零配置即可使用,提供免费试用模型额度,开箱即用,无需自备 API Key。',
|
||||
action: '使用 LangBot 服务',
|
||||
},
|
||||
custom: {
|
||||
title: '自定义模型',
|
||||
description:
|
||||
'使用你自己的模型 API Key,支持 OpenAI、Claude、Gemini 等主流模型。',
|
||||
action: '添加我自己的模型',
|
||||
},
|
||||
},
|
||||
config: {
|
||||
botInfo: '机器人信息',
|
||||
@@ -1882,6 +1829,9 @@ const zhHans = {
|
||||
scanQRCode: '请使用微信扫描以下二维码,授权后将自动登录并填写令牌',
|
||||
loginSuccess: '登录成功!令牌已自动填入',
|
||||
loginFailed: '登录失败',
|
||||
connecting: '正在连接微信服务...',
|
||||
waitingForScan: '等待扫码中',
|
||||
retry: '重试',
|
||||
},
|
||||
dingtalk: {
|
||||
createApp: '一键创建钉钉应用',
|
||||
|
||||
@@ -205,6 +205,19 @@ const zhHant = {
|
||||
selectModelAbilities: '選擇模型能力',
|
||||
visionAbility: '視覺能力',
|
||||
functionCallAbility: '函數呼叫',
|
||||
reasoningAbility: '思考能力',
|
||||
reasoningLevel: '思考等級',
|
||||
reasoningLevels: {
|
||||
providerDefault: '供應商預設',
|
||||
disabled: '關閉',
|
||||
enabled: '開啟',
|
||||
minimal: '最低',
|
||||
low: '低',
|
||||
medium: '中',
|
||||
high: '高',
|
||||
xhigh: '極高',
|
||||
max: '最大',
|
||||
},
|
||||
contextLength: '上下文視窗',
|
||||
contextLengthPlaceholder: '未知',
|
||||
contextLengthInvalid: '上下文視窗必須是正整數',
|
||||
@@ -1553,7 +1566,6 @@ const zhHant = {
|
||||
botCreateSuccess: '機器人建立成功!',
|
||||
botSaveSuccess: '機器人配置已儲存並啟用!',
|
||||
createError: '建立資源失敗',
|
||||
spaceAuthError: '無法發起 Space 授權',
|
||||
skipSaveError: '儲存跳過狀態失敗,請重試。',
|
||||
completeSaveError: '儲存完成狀態失敗,請重試。',
|
||||
step: {
|
||||
@@ -1578,58 +1590,6 @@ const zhHant = {
|
||||
aiEngine: {
|
||||
title: '選擇 AI 引擎',
|
||||
description: '選擇驅動機器人智慧的 AI 引擎。',
|
||||
orchestrated: {
|
||||
title: '使用編排好的 Agent 應用',
|
||||
description: '使用 Dify、n8n、Coze 等平台編排好的 Agent 應用。',
|
||||
action: '選擇 Agent 應用',
|
||||
selectTitle: '選擇 Agent 應用',
|
||||
selectDescription: '選擇你要使用的 Agent 應用平台。',
|
||||
},
|
||||
llm: {
|
||||
title: '直接使用大模型',
|
||||
description: '配置大模型供應商,直接使用大模型驅動機器人。',
|
||||
action: '配置大模型',
|
||||
},
|
||||
},
|
||||
spaceBanner: {
|
||||
message: '接入 LangBot Space,取得免費試用模型額度,零配置極速開箱!',
|
||||
action: '前往授權登入',
|
||||
},
|
||||
provider: {
|
||||
title: '新增模型供應商',
|
||||
description: '新增你自己的模型供應商,配置 API Key 後即可使用。',
|
||||
scanTitle: '掃描可用模型',
|
||||
scanDescription: '正在從你的供應商中掃描可用的 LLM 模型。',
|
||||
scanning: '正在掃描模型...',
|
||||
noModelsFound: '未發現可用模型,請檢查供應商配置。',
|
||||
addSelected: '新增選取的 {{count}} 個模型',
|
||||
modelsAdded: '已新增 {{count}} 個模型',
|
||||
modelsAddError: '新增模型失敗',
|
||||
skipModelAdd: '跳過,稍後新增',
|
||||
searchModels: '搜尋模型...',
|
||||
noMatch: '沒有符合的模型',
|
||||
orDivider: '或',
|
||||
manualModelPlaceholder: '輸入模型名稱手動新增',
|
||||
addManual: '新增',
|
||||
saveAndNext: '儲存並下一步',
|
||||
alreadyAdded: '已新增',
|
||||
modelAlreadyExists: '模型「{{name}}」已存在',
|
||||
},
|
||||
modelSource: {
|
||||
title: '選擇模型來源',
|
||||
description: '選擇如何為你的 AI 引擎提供模型能力。',
|
||||
space: {
|
||||
title: 'LangBot 服務',
|
||||
description:
|
||||
'零配置即可使用,提供免費試用模型額度,開箱即用,無需自備 API Key。',
|
||||
action: '使用 LangBot 服務',
|
||||
},
|
||||
custom: {
|
||||
title: '自訂模型',
|
||||
description:
|
||||
'使用你自己的模型 API Key,支援 OpenAI、Claude、Gemini 等主流模型。',
|
||||
action: '新增我自己的模型',
|
||||
},
|
||||
},
|
||||
config: {
|
||||
botInfo: '機器人資訊',
|
||||
@@ -1705,6 +1665,9 @@ const zhHant = {
|
||||
scanQRCode: '請使用微信掃描以下 QR Code,授權後將自動登入並填寫令牌',
|
||||
loginSuccess: '登入成功!令牌已自動填入',
|
||||
loginFailed: '登入失敗',
|
||||
connecting: '正在連接微信服務...',
|
||||
waitingForScan: '等待掃碼中',
|
||||
retry: '重試',
|
||||
},
|
||||
dingtalk: {
|
||||
createApp: '一鍵建立釘釘應用',
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -11,12 +11,6 @@ export default defineConfig({
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:5300',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
|
||||
Reference in New Issue
Block a user