* 新增 MemoryGatewayConfig 和 MemoryConfig dataclass,用于配置管理。 * 实现 MemoryGatewayUserCredential 和 MemoryGatewayCredentialStore,用于处理用户凭据。 * 创建 MemoryGatewayService,用于管理与 Memory Gateway 的交互。 * 开发用于记忆设置的 JSON 配置文件。 * 增强单元测试,覆盖新功能,包括凭据存储和服务行为。 * 更新 entrypoint 和实例创建脚本,以初始化 Memory Gateway 用户存储。
160 lines
4.9 KiB
Bash
Executable File
160 lines
4.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
APP_PUBLIC_PORT="${APP_PUBLIC_PORT:-8080}"
|
|
APP_FRONTEND_PORT="${APP_FRONTEND_PORT:-3000}"
|
|
APP_BACKEND_PORT="${APP_BACKEND_PORT:-18080}"
|
|
UVICORN_LOOP="${UVICORN_LOOP:-asyncio}"
|
|
UVICORN_HTTP="${UVICORN_HTTP:-h11}"
|
|
UVICORN_WS="${UVICORN_WS:-websockets}"
|
|
BEAVER_HOME="${BEAVER_HOME:-/root/.beaver}"
|
|
BEAVER_CONFIG_PATH="${BEAVER_CONFIG_PATH:-$BEAVER_HOME/config.json}"
|
|
BEAVER_WORKSPACE="${BEAVER_WORKSPACE:-$BEAVER_HOME/workspace}"
|
|
BEAVER_AUTH_FILE="${BEAVER_AUTH_FILE:-$BEAVER_HOME/web_auth_users.json}"
|
|
BEAVER_MEMORY_GATEWAY_USERS_PATH="${BEAVER_MEMORY_GATEWAY_USERS_PATH:-$BEAVER_HOME/memory_gateway_users.json}"
|
|
BEAVER_RUNTIME_ENV_FILE="${BEAVER_RUNTIME_ENV_FILE:-$BEAVER_HOME/runtime.env}"
|
|
BEAVER_INITIAL_SKILLS_DIR="${BEAVER_INITIAL_SKILLS_DIR:-/opt/app/initial-skills}"
|
|
BEAVER_INITIAL_SKILLS_EXCLUDE="${BEAVER_INITIAL_SKILLS_EXCLUDE:-officebench-mcp}"
|
|
|
|
log() {
|
|
printf '[app-instance] %s\n' "$*"
|
|
}
|
|
|
|
require_file() {
|
|
local path="$1"
|
|
local message="$2"
|
|
if [[ ! -f "$path" ]]; then
|
|
printf '[app-instance] %s: %s\n' "$message" "$path" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
seed_initial_skills() {
|
|
local initial_skills_dir="$1"
|
|
local target_dir="$2"
|
|
|
|
if [[ ! -d "$initial_skills_dir" ]]; then
|
|
return
|
|
fi
|
|
if [[ ! -f "$initial_skills_dir/_index/published.json" ]]; then
|
|
log "initial skills source has no published index, skipping: ${initial_skills_dir}"
|
|
return
|
|
fi
|
|
|
|
mkdir -p "$target_dir"
|
|
INITIAL_SKILLS_DIR="$initial_skills_dir" TARGET_DIR="$target_dir" INITIAL_SKILLS_EXCLUDE="$BEAVER_INITIAL_SKILLS_EXCLUDE" python - <<'PY'
|
|
import json
|
|
import os
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
initial = Path(os.environ["INITIAL_SKILLS_DIR"]).resolve()
|
|
target = Path(os.environ["TARGET_DIR"]).resolve()
|
|
excluded = {item.strip() for item in os.environ.get("INITIAL_SKILLS_EXCLUDE", "").split(",") if item.strip()}
|
|
|
|
for child in sorted(initial.iterdir()):
|
|
if child.name.startswith(".") or child.name in excluded:
|
|
continue
|
|
destination = target / child.name
|
|
if destination.exists():
|
|
continue
|
|
if child.is_dir():
|
|
shutil.copytree(child, destination)
|
|
elif child.is_file():
|
|
shutil.copy2(child, destination)
|
|
|
|
for index_name in ("published", "disabled"):
|
|
initial_index = initial / "_index" / f"{index_name}.json"
|
|
target_index = target / "_index" / f"{index_name}.json"
|
|
if not initial_index.exists():
|
|
continue
|
|
try:
|
|
initial_items = json.loads(initial_index.read_text(encoding="utf-8")).get("items", [])
|
|
except json.JSONDecodeError:
|
|
initial_items = []
|
|
if target_index.exists():
|
|
try:
|
|
target_items = json.loads(target_index.read_text(encoding="utf-8")).get("items", [])
|
|
except json.JSONDecodeError:
|
|
target_items = []
|
|
else:
|
|
target_items = []
|
|
merged = []
|
|
for item in [*target_items, *initial_items]:
|
|
text = str(item).strip()
|
|
if text in excluded:
|
|
continue
|
|
if text and text not in merged:
|
|
merged.append(text)
|
|
target_index.parent.mkdir(parents=True, exist_ok=True)
|
|
target_index.write_text(json.dumps({"items": merged}, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
PY
|
|
}
|
|
|
|
cleanup() {
|
|
local status=$?
|
|
|
|
if [[ -n "${NGINX_PID:-}" ]]; then
|
|
kill "${NGINX_PID}" 2>/dev/null || true
|
|
fi
|
|
if [[ -n "${FRONTEND_PID:-}" ]]; then
|
|
kill "${FRONTEND_PID}" 2>/dev/null || true
|
|
fi
|
|
if [[ -n "${BACKEND_PID:-}" ]]; then
|
|
kill "${BACKEND_PID}" 2>/dev/null || true
|
|
fi
|
|
|
|
wait 2>/dev/null || true
|
|
exit "$status"
|
|
}
|
|
|
|
trap cleanup EXIT INT TERM
|
|
|
|
mkdir -p "$BEAVER_HOME" "$BEAVER_WORKSPACE"
|
|
|
|
if [[ ! -f "$BEAVER_MEMORY_GATEWAY_USERS_PATH" ]]; then
|
|
printf '{\n "users": {}\n}\n' >"$BEAVER_MEMORY_GATEWAY_USERS_PATH"
|
|
chmod 600 "$BEAVER_MEMORY_GATEWAY_USERS_PATH"
|
|
fi
|
|
|
|
if [[ -f "$BEAVER_RUNTIME_ENV_FILE" ]]; then
|
|
set -a
|
|
. "$BEAVER_RUNTIME_ENV_FILE"
|
|
set +a
|
|
fi
|
|
|
|
require_file "$BEAVER_CONFIG_PATH" "Missing Beaver config"
|
|
seed_initial_skills "$BEAVER_INITIAL_SKILLS_DIR" "$BEAVER_WORKSPACE/skills"
|
|
|
|
export BEAVER_AUTH_FILE
|
|
export BEAVER_MEMORY_GATEWAY_USERS_PATH
|
|
export BEAVER_RUNTIME_ENV_FILE
|
|
export BEAVER_HOME
|
|
export BEAVER_CONFIG_PATH
|
|
export BEAVER_WORKSPACE
|
|
export BEAVER_INITIAL_SKILLS_DIR
|
|
export BEAVER_INITIAL_SKILLS_EXCLUDE
|
|
export PORT="$APP_FRONTEND_PORT"
|
|
export HOSTNAME="127.0.0.1"
|
|
export PYTHONFAULTHANDLER="${PYTHONFAULTHANDLER:-1}"
|
|
|
|
log "starting Beaver backend on 127.0.0.1:${APP_BACKEND_PORT} (loop=${UVICORN_LOOP}, http=${UVICORN_HTTP}, ws=${UVICORN_WS})"
|
|
(
|
|
cd /opt/app/backend
|
|
python -m uvicorn "beaver.interfaces.web.app:create_app" --factory --host 127.0.0.1 --port "$APP_BACKEND_PORT" --loop "$UVICORN_LOOP" --http "$UVICORN_HTTP" --ws "$UVICORN_WS"
|
|
) &
|
|
BACKEND_PID=$!
|
|
|
|
log "starting frontend on 127.0.0.1:${APP_FRONTEND_PORT}"
|
|
(
|
|
cd /opt/app/frontend
|
|
node server.js
|
|
) &
|
|
FRONTEND_PID=$!
|
|
|
|
log "starting nginx on 0.0.0.0:${APP_PUBLIC_PORT}"
|
|
nginx -c /opt/app/nginx.conf -g 'daemon off;' &
|
|
NGINX_PID=$!
|
|
|
|
wait -n "$BACKEND_PID" "$FRONTEND_PID" "$NGINX_PID"
|